github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/putio/object.go (about) 1 package putio 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "net/url" 9 "path" 10 "strconv" 11 "time" 12 13 "github.com/putdotio/go-putio/putio" 14 "github.com/rclone/rclone/fs" 15 "github.com/rclone/rclone/fs/fserrors" 16 "github.com/rclone/rclone/fs/hash" 17 ) 18 19 // Object describes a Putio object 20 // 21 // Putio Objects always have full metadata 22 type Object struct { 23 fs *Fs // what this object is part of 24 file *putio.File 25 remote string // The remote path 26 modtime time.Time 27 } 28 29 // NewObject finds the Object at remote. If it can't be found 30 // it returns the error fs.ErrorObjectNotFound. 31 func (f *Fs) NewObject(ctx context.Context, remote string) (o fs.Object, err error) { 32 // defer log.Trace(f, "remote=%v", remote)("o=%+v, err=%v", &o, &err) 33 obj := &Object{ 34 fs: f, 35 remote: remote, 36 } 37 err = obj.readEntryAndSetMetadata(ctx) 38 if err != nil { 39 return nil, err 40 } 41 return obj, err 42 } 43 44 // Return an Object from a path 45 // 46 // If it can't be found it returns the error fs.ErrorObjectNotFound. 47 func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info putio.File) (o fs.Object, err error) { 48 // defer log.Trace(f, "remote=%v, info=+v", remote, &info)("o=%+v, err=%v", &o, &err) 49 obj := &Object{ 50 fs: f, 51 remote: remote, 52 } 53 err = obj.setMetadataFromEntry(info) 54 if err != nil { 55 return nil, err 56 } 57 return obj, err 58 } 59 60 // Fs returns the parent Fs 61 func (o *Object) Fs() fs.Info { 62 return o.fs 63 } 64 65 // Return a string version 66 func (o *Object) String() string { 67 if o == nil { 68 return "<nil>" 69 } 70 return o.remote 71 } 72 73 // Remote returns the remote path 74 func (o *Object) Remote() string { 75 return o.remote 76 } 77 78 // Hash returns the dropbox special hash 79 func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) { 80 if t != hash.CRC32 { 81 return "", hash.ErrUnsupported 82 } 83 err := o.readEntryAndSetMetadata(ctx) 84 if err != nil { 85 return "", fmt.Errorf("failed to read hash from metadata: %w", err) 86 } 87 return o.file.CRC32, nil 88 } 89 90 // Size returns the size of an object in bytes 91 func (o *Object) Size() int64 { 92 if o.file == nil { 93 return 0 94 } 95 return o.file.Size 96 } 97 98 // ID returns the ID of the Object if known, or "" if not 99 func (o *Object) ID() string { 100 if o.file == nil { 101 return "" 102 } 103 return itoa(o.file.ID) 104 } 105 106 // MimeType returns the content type of the Object if 107 // known, or "" if not 108 func (o *Object) MimeType(ctx context.Context) string { 109 err := o.readEntryAndSetMetadata(ctx) 110 if err != nil { 111 return "" 112 } 113 return o.file.ContentType 114 } 115 116 // setMetadataFromEntry sets the fs data from a putio.File 117 // 118 // This isn't a complete set of metadata and has an inaccurate date 119 func (o *Object) setMetadataFromEntry(info putio.File) error { 120 o.file = &info 121 o.modtime = info.UpdatedAt.Time 122 return nil 123 } 124 125 // Reads the entry for a file from putio 126 func (o *Object) readEntry(ctx context.Context) (f *putio.File, err error) { 127 // defer log.Trace(o, "")("f=%+v, err=%v", f, &err) 128 leaf, directoryID, err := o.fs.dirCache.FindPath(ctx, o.remote, false) 129 if err != nil { 130 if err == fs.ErrorDirNotFound { 131 return nil, fs.ErrorObjectNotFound 132 } 133 return nil, err 134 } 135 var resp struct { 136 File putio.File `json:"file"` 137 } 138 err = o.fs.pacer.Call(func() (bool, error) { 139 // fs.Debugf(o, "requesting child. directoryID: %s, name: %s", directoryID, leaf) 140 req, err := o.fs.client.NewRequest(ctx, "GET", "/v2/files/"+directoryID+"/child?name="+url.QueryEscape(o.fs.opt.Enc.FromStandardName(leaf)), nil) 141 if err != nil { 142 return false, err 143 } 144 _, err = o.fs.client.Do(req, &resp) 145 if perr, ok := err.(*putio.ErrorResponse); ok && perr.Response.StatusCode == 404 { 146 return false, fs.ErrorObjectNotFound 147 } 148 return shouldRetry(ctx, err) 149 }) 150 if err != nil { 151 return nil, err 152 } 153 if resp.File.IsDir() { 154 return nil, fs.ErrorIsDir 155 } 156 return &resp.File, err 157 } 158 159 // Read entry if not set and set metadata from it 160 func (o *Object) readEntryAndSetMetadata(ctx context.Context) error { 161 if o.file != nil { 162 return nil 163 } 164 entry, err := o.readEntry(ctx) 165 if err != nil { 166 return err 167 } 168 return o.setMetadataFromEntry(*entry) 169 } 170 171 // Returns the remote path for the object 172 func (o *Object) remotePath() string { 173 return path.Join(o.fs.root, o.remote) 174 } 175 176 // ModTime returns the modification time of the object 177 // 178 // It attempts to read the objects mtime and if that isn't present the 179 // LastModified returned in the http headers 180 func (o *Object) ModTime(ctx context.Context) time.Time { 181 if o.modtime.IsZero() { 182 err := o.readEntryAndSetMetadata(ctx) 183 if err != nil { 184 fs.Debugf(o, "Failed to read metadata: %v", err) 185 return time.Now() 186 } 187 } 188 return o.modtime 189 } 190 191 // SetModTime sets the modification time of the local fs object 192 // 193 // Commits the datastore 194 func (o *Object) SetModTime(ctx context.Context, modTime time.Time) (err error) { 195 // defer log.Trace(o, "modTime=%v", modTime.String())("err=%v", &err) 196 req, err := o.fs.client.NewRequest(ctx, "POST", "/v2/files/touch?file_id="+strconv.FormatInt(o.file.ID, 10)+"&updated_at="+url.QueryEscape(modTime.Format(time.RFC3339)), nil) 197 if err != nil { 198 return err 199 } 200 // fs.Debugf(o, "setting modtime: %s", modTime.String()) 201 _, err = o.fs.client.Do(req, nil) 202 if err != nil { 203 return err 204 } 205 o.modtime = modTime 206 if o.file != nil { 207 o.file.UpdatedAt.Time = modTime 208 } 209 return nil 210 } 211 212 // Storable returns whether this object is storable 213 func (o *Object) Storable() bool { 214 return true 215 } 216 217 // Open an object for read 218 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) { 219 // defer log.Trace(o, "")("err=%v", &err) 220 var storageURL string 221 err = o.fs.pacer.Call(func() (bool, error) { 222 storageURL, err = o.fs.client.Files.URL(ctx, o.file.ID, true) 223 return shouldRetry(ctx, err) 224 }) 225 if err != nil { 226 return 227 } 228 229 var resp *http.Response 230 headers := fs.OpenOptionHeaders(options) 231 err = o.fs.pacer.Call(func() (bool, error) { 232 req, err := http.NewRequestWithContext(ctx, http.MethodGet, storageURL, nil) 233 if err != nil { 234 return shouldRetry(ctx, err) 235 } 236 req.Header.Set("User-Agent", o.fs.client.UserAgent) 237 238 // merge headers with extra headers 239 for header, value := range headers { 240 req.Header.Set(header, value) 241 } 242 // fs.Debugf(o, "opening file: id=%d", o.file.ID) 243 resp, err = o.fs.httpClient.Do(req) 244 if err != nil { 245 return shouldRetry(ctx, err) 246 } 247 if err := checkStatusCode(resp, 200, 206); err != nil { 248 return shouldRetry(ctx, err) 249 } 250 return false, nil 251 }) 252 if perr, ok := err.(*putio.ErrorResponse); ok && perr.Response.StatusCode >= 400 && perr.Response.StatusCode <= 499 { 253 _ = resp.Body.Close() 254 return nil, fserrors.NoRetryError(err) 255 } 256 if err != nil { 257 return nil, err 258 } 259 return resp.Body, nil 260 } 261 262 // Update the already existing object 263 // 264 // Copy the reader into the object updating modTime and size. 265 // 266 // The new object may have been created if an error is returned 267 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) { 268 // defer log.Trace(o, "src=%+v", src)("err=%v", &err) 269 remote := o.remotePath() 270 if ignoredFiles.MatchString(remote) { 271 fs.Logf(o, "File name disallowed - not uploading") 272 return nil 273 } 274 err = o.Remove(ctx) 275 if err != nil { 276 return err 277 } 278 newObj, err := o.fs.putUnchecked(ctx, in, src, o.remote, options...) 279 if err != nil { 280 return err 281 } 282 *o = *(newObj.(*Object)) 283 return err 284 } 285 286 // Remove an object 287 func (o *Object) Remove(ctx context.Context) (err error) { 288 // defer log.Trace(o, "")("err=%v", &err) 289 return o.fs.pacer.Call(func() (bool, error) { 290 // fs.Debugf(o, "removing file: id=%d", o.file.ID) 291 err = o.fs.client.Files.Delete(ctx, o.file.ID) 292 return shouldRetry(ctx, err) 293 }) 294 }