github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/fichier/fichier.go (about) 1 // Package fichier provides an interface to the 1Fichier storage system. 2 package fichier 3 4 import ( 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/rclone/rclone/fs" 15 "github.com/rclone/rclone/fs/config" 16 "github.com/rclone/rclone/fs/config/configmap" 17 "github.com/rclone/rclone/fs/config/configstruct" 18 "github.com/rclone/rclone/fs/fshttp" 19 "github.com/rclone/rclone/fs/hash" 20 "github.com/rclone/rclone/lib/dircache" 21 "github.com/rclone/rclone/lib/encoder" 22 "github.com/rclone/rclone/lib/pacer" 23 "github.com/rclone/rclone/lib/rest" 24 ) 25 26 const ( 27 rootID = "0" 28 apiBaseURL = "https://api.1fichier.com/v1" 29 minSleep = 400 * time.Millisecond // api is extremely rate limited now 30 maxSleep = 5 * time.Second 31 decayConstant = 2 // bigger for slower decay, exponential 32 attackConstant = 0 // start with max sleep 33 ) 34 35 func init() { 36 fs.Register(&fs.RegInfo{ 37 Name: "fichier", 38 Description: "1Fichier", 39 NewFs: NewFs, 40 Options: []fs.Option{{ 41 Help: "Your API Key, get it from https://1fichier.com/console/params.pl.", 42 Name: "api_key", 43 Sensitive: true, 44 }, { 45 Help: "If you want to download a shared folder, add this parameter.", 46 Name: "shared_folder", 47 Advanced: true, 48 }, { 49 Help: "If you want to download a shared file that is password protected, add this parameter.", 50 Name: "file_password", 51 Advanced: true, 52 IsPassword: true, 53 }, { 54 Help: "If you want to list the files in a shared folder that is password protected, add this parameter.", 55 Name: "folder_password", 56 Advanced: true, 57 IsPassword: true, 58 }, { 59 Help: "Set if you wish to use CDN download links.", 60 Name: "cdn", 61 Default: false, 62 Advanced: true, 63 }, { 64 Name: config.ConfigEncoding, 65 Help: config.ConfigEncodingHelp, 66 Advanced: true, 67 // Characters that need escaping 68 // 69 // '\\': '\', // FULLWIDTH REVERSE SOLIDUS 70 // '<': '<', // FULLWIDTH LESS-THAN SIGN 71 // '>': '>', // FULLWIDTH GREATER-THAN SIGN 72 // '"': '"', // FULLWIDTH QUOTATION MARK - not on the list but seems to be reserved 73 // '\'': ''', // FULLWIDTH APOSTROPHE 74 // '$': '$', // FULLWIDTH DOLLAR SIGN 75 // '`': '`', // FULLWIDTH GRAVE ACCENT 76 // 77 // Leading space and trailing space 78 Default: (encoder.Display | 79 encoder.EncodeBackSlash | 80 encoder.EncodeSingleQuote | 81 encoder.EncodeBackQuote | 82 encoder.EncodeDoubleQuote | 83 encoder.EncodeLtGt | 84 encoder.EncodeDollar | 85 encoder.EncodeLeftSpace | 86 encoder.EncodeRightSpace | 87 encoder.EncodeInvalidUtf8), 88 }}, 89 }) 90 } 91 92 // Options defines the configuration for this backend 93 type Options struct { 94 APIKey string `config:"api_key"` 95 SharedFolder string `config:"shared_folder"` 96 FilePassword string `config:"file_password"` 97 FolderPassword string `config:"folder_password"` 98 CDN bool `config:"cdn"` 99 Enc encoder.MultiEncoder `config:"encoding"` 100 } 101 102 // Fs is the interface a cloud storage system must provide 103 type Fs struct { 104 root string 105 name string 106 features *fs.Features 107 opt Options 108 dirCache *dircache.DirCache 109 baseClient *http.Client 110 pacer *fs.Pacer 111 rest *rest.Client 112 } 113 114 // FindLeaf finds a directory of name leaf in the folder with ID pathID 115 func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) { 116 folderID, err := strconv.Atoi(pathID) 117 if err != nil { 118 return "", false, err 119 } 120 folders, err := f.listFolders(ctx, folderID) 121 if err != nil { 122 return "", false, err 123 } 124 125 for _, folder := range folders.SubFolders { 126 if folder.Name == leaf { 127 pathIDOut := strconv.Itoa(folder.ID) 128 return pathIDOut, true, nil 129 } 130 } 131 132 return "", false, nil 133 } 134 135 // CreateDir makes a directory with pathID as parent and name leaf 136 func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) { 137 folderID, err := strconv.Atoi(pathID) 138 if err != nil { 139 return "", err 140 } 141 resp, err := f.makeFolder(ctx, leaf, folderID) 142 if err != nil { 143 return "", err 144 } 145 return strconv.Itoa(resp.FolderID), err 146 } 147 148 // Name of the remote (as passed into NewFs) 149 func (f *Fs) Name() string { 150 return f.name 151 } 152 153 // Root of the remote (as passed into NewFs) 154 func (f *Fs) Root() string { 155 return f.root 156 } 157 158 // String returns a description of the FS 159 func (f *Fs) String() string { 160 return fmt.Sprintf("1Fichier root '%s'", f.root) 161 } 162 163 // Precision of the ModTimes in this Fs 164 func (f *Fs) Precision() time.Duration { 165 return fs.ModTimeNotSupported 166 } 167 168 // Hashes returns the supported hash types of the filesystem 169 func (f *Fs) Hashes() hash.Set { 170 return hash.Set(hash.Whirlpool) 171 } 172 173 // Features returns the optional features of this Fs 174 func (f *Fs) Features() *fs.Features { 175 return f.features 176 } 177 178 // NewFs makes a new Fs object from the path 179 // 180 // The path is of the form remote:path 181 // 182 // Remotes are looked up in the config file. If the remote isn't 183 // found then NotFoundInConfigFile will be returned. 184 // 185 // On Windows avoid single character remote names as they can be mixed 186 // up with drive letters. 187 func NewFs(ctx context.Context, name string, root string, config configmap.Mapper) (fs.Fs, error) { 188 opt := new(Options) 189 err := configstruct.Set(config, opt) 190 if err != nil { 191 return nil, err 192 } 193 194 // If using a Shared Folder override root 195 if opt.SharedFolder != "" { 196 root = "" 197 } 198 199 //workaround for wonky parser 200 root = strings.Trim(root, "/") 201 202 f := &Fs{ 203 name: name, 204 root: root, 205 opt: *opt, 206 pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant), pacer.AttackConstant(attackConstant))), 207 baseClient: &http.Client{}, 208 } 209 210 f.features = (&fs.Features{ 211 DuplicateFiles: true, 212 CanHaveEmptyDirectories: true, 213 ReadMimeType: true, 214 }).Fill(ctx, f) 215 216 client := fshttp.NewClient(ctx) 217 218 f.rest = rest.NewClient(client).SetRoot(apiBaseURL) 219 220 f.rest.SetHeader("Authorization", "Bearer "+f.opt.APIKey) 221 222 f.dirCache = dircache.New(root, rootID, f) 223 224 // Find the current root 225 err = f.dirCache.FindRoot(ctx, false) 226 if err != nil { 227 // Assume it is a file 228 newRoot, remote := dircache.SplitPath(root) 229 tempF := *f 230 tempF.dirCache = dircache.New(newRoot, rootID, &tempF) 231 tempF.root = newRoot 232 // Make new Fs which is the parent 233 err = tempF.dirCache.FindRoot(ctx, false) 234 if err != nil { 235 // No root so return old f 236 return f, nil 237 } 238 _, err := tempF.NewObject(ctx, remote) 239 if err != nil { 240 if err == fs.ErrorObjectNotFound { 241 // File doesn't exist so return old f 242 return f, nil 243 } 244 return nil, err 245 } 246 f.features.Fill(ctx, &tempF) 247 // XXX: update the old f here instead of returning tempF, since 248 // `features` were already filled with functions having *f as a receiver. 249 // See https://github.com/rclone/rclone/issues/2182 250 f.dirCache = tempF.dirCache 251 f.root = tempF.root 252 // return an error with an fs which points to the parent 253 return f, fs.ErrorIsFile 254 } 255 return f, nil 256 } 257 258 // List the objects and directories in dir into entries. The 259 // entries can be returned in any order but should be for a 260 // complete directory. 261 // 262 // dir should be "" to list the root, and should not have 263 // trailing slashes. 264 // 265 // This should return ErrDirNotFound if the directory isn't 266 // found. 267 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 268 if f.opt.SharedFolder != "" { 269 return f.listSharedFiles(ctx, f.opt.SharedFolder) 270 } 271 272 dirContent, err := f.listDir(ctx, dir) 273 if err != nil { 274 return nil, err 275 } 276 277 return dirContent, nil 278 } 279 280 // NewObject finds the Object at remote. If it can't be found 281 // it returns the error ErrorObjectNotFound. 282 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 283 leaf, directoryID, err := f.dirCache.FindPath(ctx, remote, false) 284 if err != nil { 285 if err == fs.ErrorDirNotFound { 286 return nil, fs.ErrorObjectNotFound 287 } 288 return nil, err 289 } 290 291 folderID, err := strconv.Atoi(directoryID) 292 if err != nil { 293 return nil, err 294 } 295 files, err := f.listFiles(ctx, folderID) 296 if err != nil { 297 return nil, err 298 } 299 300 for _, file := range files.Items { 301 if file.Filename == leaf { 302 path, ok := f.dirCache.GetInv(directoryID) 303 304 if !ok { 305 return nil, errors.New("cannot find dir in dircache") 306 } 307 308 return f.newObjectFromFile(ctx, path, file), nil 309 } 310 } 311 312 return nil, fs.ErrorObjectNotFound 313 } 314 315 // Put in to the remote path with the modTime given of the given size 316 // 317 // When called from outside an Fs by rclone, src.Size() will always be >= 0. 318 // But for unknown-sized objects (indicated by src.Size() == -1), Put should either 319 // return an error or upload it properly (rather than e.g. calling panic). 320 // 321 // May create the object even if it returns an error - if so 322 // will return the object and the error, otherwise will return 323 // nil and the error 324 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 325 existingObj, err := f.NewObject(ctx, src.Remote()) 326 switch err { 327 case nil: 328 return existingObj, existingObj.Update(ctx, in, src, options...) 329 case fs.ErrorObjectNotFound: 330 // Not found so create it 331 return f.PutUnchecked(ctx, in, src, options...) 332 default: 333 return nil, err 334 } 335 } 336 337 // putUnchecked uploads the object with the given name and size 338 // 339 // This will create a duplicate if we upload a new file without 340 // checking to see if there is one already - use Put() for that. 341 func (f *Fs) putUnchecked(ctx context.Context, in io.Reader, remote string, size int64, options ...fs.OpenOption) (fs.Object, error) { 342 if size > int64(300e9) { 343 return nil, errors.New("File too big, can't upload") 344 } else if size == 0 { 345 return nil, fs.ErrorCantUploadEmptyFiles 346 } 347 348 nodeResponse, err := f.getUploadNode(ctx) 349 if err != nil { 350 return nil, err 351 } 352 353 leaf, directoryID, err := f.dirCache.FindPath(ctx, remote, true) 354 if err != nil { 355 return nil, err 356 } 357 358 _, err = f.uploadFile(ctx, in, size, leaf, directoryID, nodeResponse.ID, nodeResponse.URL, options...) 359 if err != nil { 360 return nil, err 361 } 362 363 fileUploadResponse, err := f.endUpload(ctx, nodeResponse.ID, nodeResponse.URL) 364 if err != nil { 365 return nil, err 366 } 367 368 if len(fileUploadResponse.Links) == 0 { 369 return nil, errors.New("upload response not found") 370 } else if len(fileUploadResponse.Links) > 1 { 371 fs.Debugf(remote, "Multiple upload responses found, using the first") 372 } 373 374 link := fileUploadResponse.Links[0] 375 fileSize, err := strconv.ParseInt(link.Size, 10, 64) 376 377 if err != nil { 378 return nil, err 379 } 380 381 return &Object{ 382 fs: f, 383 remote: remote, 384 file: File{ 385 CDN: 0, 386 Checksum: link.Whirlpool, 387 ContentType: "", 388 Date: time.Now().Format("2006-01-02 15:04:05"), 389 Filename: link.Filename, 390 Pass: 0, 391 Size: fileSize, 392 URL: link.Download, 393 }, 394 }, nil 395 } 396 397 // PutUnchecked uploads the object 398 // 399 // This will create a duplicate if we upload a new file without 400 // checking to see if there is one already - use Put() for that. 401 func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 402 return f.putUnchecked(ctx, in, src.Remote(), src.Size(), options...) 403 } 404 405 // Mkdir makes the directory (container, bucket) 406 // 407 // Shouldn't return an error if it already exists 408 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 409 _, err := f.dirCache.FindDir(ctx, dir, true) 410 return err 411 } 412 413 // Rmdir removes the directory (container, bucket) if empty 414 // 415 // Return an error if it doesn't exist or isn't empty 416 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 417 directoryID, err := f.dirCache.FindDir(ctx, dir, false) 418 if err != nil { 419 return err 420 } 421 422 folderID, err := strconv.Atoi(directoryID) 423 if err != nil { 424 return err 425 } 426 427 _, err = f.removeFolder(ctx, dir, folderID) 428 if err != nil { 429 return err 430 } 431 432 f.dirCache.FlushDir(dir) 433 434 return nil 435 } 436 437 // Move src to this remote using server side move operations. 438 func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 439 srcObj, ok := src.(*Object) 440 if !ok { 441 fs.Debugf(src, "Can't move - not same remote type") 442 return nil, fs.ErrorCantMove 443 } 444 445 // Find current directory ID 446 _, currentDirectoryID, err := f.dirCache.FindPath(ctx, remote, false) 447 if err != nil { 448 return nil, err 449 } 450 451 // Create temporary object 452 dstObj, leaf, directoryID, err := f.createObject(ctx, remote) 453 if err != nil { 454 return nil, err 455 } 456 457 // If it is in the correct directory, just rename it 458 var url string 459 if currentDirectoryID == directoryID { 460 resp, err := f.renameFile(ctx, srcObj.file.URL, leaf) 461 if err != nil { 462 return nil, fmt.Errorf("couldn't rename file: %w", err) 463 } 464 if resp.Status != "OK" { 465 return nil, fmt.Errorf("couldn't rename file: %s", resp.Message) 466 } 467 url = resp.URLs[0].URL 468 } else { 469 folderID, err := strconv.Atoi(directoryID) 470 if err != nil { 471 return nil, err 472 } 473 resp, err := f.moveFile(ctx, srcObj.file.URL, folderID, leaf) 474 if err != nil { 475 return nil, fmt.Errorf("couldn't move file: %w", err) 476 } 477 if resp.Status != "OK" { 478 return nil, fmt.Errorf("couldn't move file: %s", resp.Message) 479 } 480 url = resp.URLs[0] 481 } 482 483 file, err := f.readFileInfo(ctx, url) 484 if err != nil { 485 return nil, errors.New("couldn't read file data") 486 } 487 dstObj.setMetaData(*file) 488 return dstObj, nil 489 } 490 491 // DirMove moves src, srcRemote to this remote at dstRemote 492 // using server-side move operations. 493 // 494 // Will only be called if src.Fs().Name() == f.Name() 495 // 496 // If it isn't possible then return fs.ErrorCantDirMove. 497 // 498 // If destination exists then return fs.ErrorDirExists. 499 // 500 // This is complicated by the fact that we can't use moveDir to move 501 // to a different directory AND rename at the same time as it can 502 // overwrite files in the source directory. 503 func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error { 504 srcFs, ok := src.(*Fs) 505 if !ok { 506 fs.Debugf(srcFs, "Can't move directory - not same remote type") 507 return fs.ErrorCantDirMove 508 } 509 510 srcID, _, _, dstDirectoryID, dstLeaf, err := f.dirCache.DirMove(ctx, srcFs.dirCache, srcFs.root, srcRemote, f.root, dstRemote) 511 if err != nil { 512 return err 513 } 514 srcIDnumeric, err := strconv.Atoi(srcID) 515 if err != nil { 516 return err 517 } 518 dstDirectoryIDnumeric, err := strconv.Atoi(dstDirectoryID) 519 if err != nil { 520 return err 521 } 522 523 var resp *MoveDirResponse 524 resp, err = f.moveDir(ctx, srcIDnumeric, dstLeaf, dstDirectoryIDnumeric) 525 if err != nil { 526 return fmt.Errorf("couldn't rename leaf: %w", err) 527 } 528 if resp.Status != "OK" { 529 return fmt.Errorf("couldn't rename leaf: %s", resp.Message) 530 } 531 532 srcFs.dirCache.FlushDir(srcRemote) 533 return nil 534 } 535 536 // Copy src to this remote using server side move operations. 537 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 538 srcObj, ok := src.(*Object) 539 if !ok { 540 fs.Debugf(src, "Can't move - not same remote type") 541 return nil, fs.ErrorCantMove 542 } 543 544 // Create temporary object 545 dstObj, leaf, directoryID, err := f.createObject(ctx, remote) 546 if err != nil { 547 return nil, err 548 } 549 550 folderID, err := strconv.Atoi(directoryID) 551 if err != nil { 552 return nil, err 553 } 554 resp, err := f.copyFile(ctx, srcObj.file.URL, folderID, leaf) 555 if err != nil { 556 return nil, fmt.Errorf("couldn't move file: %w", err) 557 } 558 if resp.Status != "OK" { 559 return nil, fmt.Errorf("couldn't move file: %s", resp.Message) 560 } 561 562 file, err := f.readFileInfo(ctx, resp.URLs[0].ToURL) 563 if err != nil { 564 return nil, errors.New("couldn't read file data") 565 } 566 dstObj.setMetaData(*file) 567 return dstObj, nil 568 } 569 570 // About gets quota information 571 func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) { 572 opts := rest.Opts{ 573 Method: "POST", 574 Path: "/user/info.cgi", 575 ContentType: "application/json", 576 } 577 var accountInfo AccountInfo 578 var resp *http.Response 579 err = f.pacer.Call(func() (bool, error) { 580 resp, err = f.rest.CallJSON(ctx, &opts, nil, &accountInfo) 581 return shouldRetry(ctx, resp, err) 582 }) 583 if err != nil { 584 return nil, fmt.Errorf("failed to read user info: %w", err) 585 } 586 587 // FIXME max upload size would be useful to use in Update 588 usage = &fs.Usage{ 589 Used: fs.NewUsageValue(accountInfo.ColdStorage), // bytes in use 590 Total: fs.NewUsageValue(accountInfo.AvailableColdStorage), // bytes total 591 Free: fs.NewUsageValue(accountInfo.AvailableColdStorage - accountInfo.ColdStorage), // bytes free 592 } 593 return usage, nil 594 } 595 596 // PublicLink adds a "readable by anyone with link" permission on the given file or folder. 597 func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) { 598 o, err := f.NewObject(ctx, remote) 599 if err != nil { 600 return "", err 601 } 602 return o.(*Object).file.URL, nil 603 } 604 605 // Check the interfaces are satisfied 606 var ( 607 _ fs.Fs = (*Fs)(nil) 608 _ fs.Mover = (*Fs)(nil) 609 _ fs.DirMover = (*Fs)(nil) 610 _ fs.Copier = (*Fs)(nil) 611 _ fs.PublicLinker = (*Fs)(nil) 612 _ fs.PutUncheckeder = (*Fs)(nil) 613 _ dircache.DirCacher = (*Fs)(nil) 614 )