github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/backend/fichier/fichier.go (about) 1 package fichier 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "strconv" 9 "strings" 10 "time" 11 12 "github.com/ncw/rclone/fs" 13 "github.com/ncw/rclone/fs/config/configmap" 14 "github.com/ncw/rclone/fs/config/configstruct" 15 "github.com/ncw/rclone/fs/fshttp" 16 "github.com/ncw/rclone/fs/hash" 17 "github.com/ncw/rclone/lib/dircache" 18 "github.com/ncw/rclone/lib/pacer" 19 "github.com/ncw/rclone/lib/rest" 20 "github.com/pkg/errors" 21 ) 22 23 const ( 24 rootID = "0" 25 apiBaseURL = "https://api.1fichier.com/v1" 26 minSleep = 334 * time.Millisecond // 3 API calls per second is recommended 27 maxSleep = 5 * time.Second 28 decayConstant = 2 // bigger for slower decay, exponential 29 ) 30 31 func init() { 32 fs.Register(&fs.RegInfo{ 33 Name: "fichier", 34 Description: "1Fichier", 35 Config: func(name string, config configmap.Mapper) { 36 }, 37 NewFs: NewFs, 38 Options: []fs.Option{ 39 { 40 Help: "Your API Key, get it from https://1fichier.com/console/params.pl", 41 Name: "api_key", 42 }, 43 { 44 Help: "If you want to download a shared folder, add this parameter", 45 Name: "shared_folder", 46 Required: false, 47 Advanced: true, 48 }, 49 }, 50 }) 51 } 52 53 // Options defines the configuration for this backend 54 type Options struct { 55 APIKey string `config:"api_key"` 56 SharedFolder string `config:"shared_folder"` 57 } 58 59 // Fs is the interface a cloud storage system must provide 60 type Fs struct { 61 root string 62 name string 63 features *fs.Features 64 dirCache *dircache.DirCache 65 baseClient *http.Client 66 options *Options 67 pacer *fs.Pacer 68 rest *rest.Client 69 } 70 71 // FindLeaf finds a directory of name leaf in the folder with ID pathID 72 func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) { 73 folderID, err := strconv.Atoi(pathID) 74 if err != nil { 75 return "", false, err 76 } 77 folders, err := f.listFolders(folderID) 78 if err != nil { 79 return "", false, err 80 } 81 82 for _, folder := range folders.SubFolders { 83 if folder.Name == leaf { 84 pathIDOut := strconv.Itoa(folder.ID) 85 return pathIDOut, true, nil 86 } 87 } 88 89 return "", false, nil 90 } 91 92 // CreateDir makes a directory with pathID as parent and name leaf 93 func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) { 94 folderID, err := strconv.Atoi(pathID) 95 if err != nil { 96 return "", err 97 } 98 resp, err := f.makeFolder(leaf, folderID) 99 if err != nil { 100 return "", err 101 } 102 return strconv.Itoa(resp.FolderID), err 103 } 104 105 // Name of the remote (as passed into NewFs) 106 func (f *Fs) Name() string { 107 return f.name 108 } 109 110 // Root of the remote (as passed into NewFs) 111 func (f *Fs) Root() string { 112 return f.root 113 } 114 115 // String returns a description of the FS 116 func (f *Fs) String() string { 117 return fmt.Sprintf("1Fichier root '%s'", f.root) 118 } 119 120 // Precision of the ModTimes in this Fs 121 func (f *Fs) Precision() time.Duration { 122 return fs.ModTimeNotSupported 123 } 124 125 // Hashes returns the supported hash types of the filesystem 126 func (f *Fs) Hashes() hash.Set { 127 return hash.Set(hash.Whirlpool) 128 } 129 130 // Features returns the optional features of this Fs 131 func (f *Fs) Features() *fs.Features { 132 return f.features 133 } 134 135 // NewFs makes a new Fs object from the path 136 // 137 // The path is of the form remote:path 138 // 139 // Remotes are looked up in the config file. If the remote isn't 140 // found then NotFoundInConfigFile will be returned. 141 // 142 // On Windows avoid single character remote names as they can be mixed 143 // up with drive letters. 144 func NewFs(name string, rootleaf string, config configmap.Mapper) (fs.Fs, error) { 145 root := replaceReservedChars(rootleaf) 146 opt := new(Options) 147 err := configstruct.Set(config, opt) 148 if err != nil { 149 return nil, err 150 } 151 152 // If using a Shared Folder override root 153 if opt.SharedFolder != "" { 154 root = "" 155 } 156 157 //workaround for wonky parser 158 root = strings.Trim(root, "/") 159 160 f := &Fs{ 161 name: name, 162 root: root, 163 options: opt, 164 pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), 165 baseClient: &http.Client{}, 166 } 167 168 f.features = (&fs.Features{ 169 DuplicateFiles: true, 170 CanHaveEmptyDirectories: true, 171 }).Fill(f) 172 173 client := fshttp.NewClient(fs.Config) 174 175 f.rest = rest.NewClient(client).SetRoot(apiBaseURL) 176 177 f.rest.SetHeader("Authorization", "Bearer "+f.options.APIKey) 178 179 f.dirCache = dircache.New(root, rootID, f) 180 181 ctx := context.Background() 182 183 // Find the current root 184 err = f.dirCache.FindRoot(ctx, false) 185 if err != nil { 186 // Assume it is a file 187 newRoot, remote := dircache.SplitPath(root) 188 tempF := *f 189 tempF.dirCache = dircache.New(newRoot, rootID, &tempF) 190 tempF.root = newRoot 191 // Make new Fs which is the parent 192 err = tempF.dirCache.FindRoot(ctx, false) 193 if err != nil { 194 // No root so return old f 195 return f, nil 196 } 197 _, err := tempF.NewObject(ctx, remote) 198 if err != nil { 199 if err == fs.ErrorObjectNotFound { 200 // File doesn't exist so return old f 201 return f, nil 202 } 203 return nil, err 204 } 205 f.features.Fill(&tempF) 206 // XXX: update the old f here instead of returning tempF, since 207 // `features` were already filled with functions having *f as a receiver. 208 // See https://github.com/ncw/rclone/issues/2182 209 f.dirCache = tempF.dirCache 210 f.root = tempF.root 211 // return an error with an fs which points to the parent 212 return f, fs.ErrorIsFile 213 } 214 return f, nil 215 } 216 217 // List the objects and directories in dir into entries. The 218 // entries can be returned in any order but should be for a 219 // complete directory. 220 // 221 // dir should be "" to list the root, and should not have 222 // trailing slashes. 223 // 224 // This should return ErrDirNotFound if the directory isn't 225 // found. 226 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 227 if f.options.SharedFolder != "" { 228 return f.listSharedFiles(ctx, f.options.SharedFolder) 229 } 230 231 dirContent, err := f.listDir(ctx, dir) 232 if err != nil { 233 return nil, err 234 } 235 236 return dirContent, nil 237 } 238 239 // NewObject finds the Object at remote. If it can't be found 240 // it returns the error ErrorObjectNotFound. 241 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 242 leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, remote, false) 243 if err != nil { 244 if err == fs.ErrorDirNotFound { 245 return nil, fs.ErrorObjectNotFound 246 } 247 return nil, err 248 } 249 250 folderID, err := strconv.Atoi(directoryID) 251 if err != nil { 252 return nil, err 253 } 254 files, err := f.listFiles(folderID) 255 if err != nil { 256 return nil, err 257 } 258 259 for _, file := range files.Items { 260 if file.Filename == leaf { 261 path, ok := f.dirCache.GetInv(directoryID) 262 263 if !ok { 264 return nil, errors.New("Cannot find dir in dircache") 265 } 266 267 return f.newObjectFromFile(ctx, path, file), nil 268 } 269 } 270 271 return nil, fs.ErrorObjectNotFound 272 } 273 274 // Put in to the remote path with the modTime given of the given size 275 // 276 // When called from outside a Fs by rclone, src.Size() will always be >= 0. 277 // But for unknown-sized objects (indicated by src.Size() == -1), Put should either 278 // return an error or upload it properly (rather than e.g. calling panic). 279 // 280 // May create the object even if it returns an error - if so 281 // will return the object and the error, otherwise will return 282 // nil and the error 283 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 284 exisitingObj, err := f.NewObject(ctx, src.Remote()) 285 switch err { 286 case nil: 287 return exisitingObj, exisitingObj.Update(ctx, in, src, options...) 288 case fs.ErrorObjectNotFound: 289 // Not found so create it 290 return f.PutUnchecked(ctx, in, src, options...) 291 default: 292 return nil, err 293 } 294 } 295 296 // putUnchecked uploads the object with the given name and size 297 // 298 // This will create a duplicate if we upload a new file without 299 // checking to see if there is one already - use Put() for that. 300 func (f *Fs) putUnchecked(ctx context.Context, in io.Reader, remote string, size int64, options ...fs.OpenOption) (fs.Object, error) { 301 if size > int64(100E9) { 302 return nil, errors.New("File too big, cant upload") 303 } else if size == 0 { 304 return nil, fs.ErrorCantUploadEmptyFiles 305 } 306 307 nodeResponse, err := f.getUploadNode() 308 if err != nil { 309 return nil, err 310 } 311 312 leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, remote, true) 313 if err != nil { 314 return nil, err 315 } 316 317 _, err = f.uploadFile(in, size, leaf, directoryID, nodeResponse.ID, nodeResponse.URL) 318 if err != nil { 319 return nil, err 320 } 321 322 fileUploadResponse, err := f.endUpload(nodeResponse.ID, nodeResponse.URL) 323 if err != nil { 324 return nil, err 325 } 326 327 if len(fileUploadResponse.Links) != 1 { 328 return nil, errors.New("unexpected amount of files") 329 } 330 331 link := fileUploadResponse.Links[0] 332 fileSize, err := strconv.ParseInt(link.Size, 10, 64) 333 334 if err != nil { 335 return nil, err 336 } 337 338 return &Object{ 339 fs: f, 340 remote: remote, 341 file: File{ 342 ACL: 0, 343 CDN: 0, 344 Checksum: link.Whirlpool, 345 ContentType: "", 346 Date: time.Now().Format("2006-01-02 15:04:05"), 347 Filename: link.Filename, 348 Pass: 0, 349 Size: int(fileSize), 350 URL: link.Download, 351 }, 352 }, nil 353 } 354 355 // PutUnchecked uploads the object 356 // 357 // This will create a duplicate if we upload a new file without 358 // checking to see if there is one already - use Put() for that. 359 func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 360 return f.putUnchecked(ctx, in, src.Remote(), src.Size(), options...) 361 } 362 363 // Mkdir makes the directory (container, bucket) 364 // 365 // Shouldn't return an error if it already exists 366 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 367 err := f.dirCache.FindRoot(ctx, true) 368 if err != nil { 369 return err 370 } 371 if dir != "" { 372 _, err = f.dirCache.FindDir(ctx, dir, true) 373 } 374 return err 375 } 376 377 // Rmdir removes the directory (container, bucket) if empty 378 // 379 // Return an error if it doesn't exist or isn't empty 380 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 381 err := f.dirCache.FindRoot(ctx, false) 382 if err != nil { 383 return err 384 } 385 386 directoryID, err := f.dirCache.FindDir(ctx, dir, false) 387 if err != nil { 388 return err 389 } 390 391 folderID, err := strconv.Atoi(directoryID) 392 if err != nil { 393 return err 394 } 395 396 _, err = f.removeFolder(dir, folderID) 397 if err != nil { 398 return err 399 } 400 401 f.dirCache.FlushDir(dir) 402 403 return nil 404 } 405 406 // Check the interfaces are satisfied 407 var ( 408 _ fs.Fs = (*Fs)(nil) 409 _ fs.PutUncheckeder = (*Fs)(nil) 410 _ dircache.DirCacher = (*Fs)(nil) 411 )