github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/seafile/seafile.go (about) 1 // Package seafile provides an interface to the Seafile storage system. 2 package seafile 3 4 import ( 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "path" 12 "strconv" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/coreos/go-semver/semver" 18 "github.com/rclone/rclone/backend/seafile/api" 19 "github.com/rclone/rclone/fs" 20 "github.com/rclone/rclone/fs/config" 21 "github.com/rclone/rclone/fs/config/configmap" 22 "github.com/rclone/rclone/fs/config/configstruct" 23 "github.com/rclone/rclone/fs/config/obscure" 24 "github.com/rclone/rclone/fs/fserrors" 25 "github.com/rclone/rclone/fs/fshttp" 26 "github.com/rclone/rclone/fs/hash" 27 "github.com/rclone/rclone/lib/bucket" 28 "github.com/rclone/rclone/lib/cache" 29 "github.com/rclone/rclone/lib/encoder" 30 "github.com/rclone/rclone/lib/pacer" 31 "github.com/rclone/rclone/lib/random" 32 "github.com/rclone/rclone/lib/rest" 33 ) 34 35 const ( 36 librariesCacheKey = "all" 37 retryAfterHeader = "Retry-After" 38 configURL = "url" 39 configUser = "user" 40 configPassword = "pass" 41 config2FA = "2fa" 42 configLibrary = "library" 43 configLibraryKey = "library_key" 44 configCreateLibrary = "create_library" 45 configAuthToken = "auth_token" 46 ) 47 48 // This is global to all instances of fs 49 // (copying from a seafile remote to another remote would create 2 fs) 50 var ( 51 rangeDownloadNotice sync.Once // Display the notice only once 52 createLibraryMutex sync.Mutex // Mutex to protect library creation 53 ) 54 55 // Register with Fs 56 func init() { 57 fs.Register(&fs.RegInfo{ 58 Name: "seafile", 59 Description: "seafile", 60 NewFs: NewFs, 61 Config: Config, 62 Options: []fs.Option{{ 63 Name: configURL, 64 Help: "URL of seafile host to connect to.", 65 Required: true, 66 Examples: []fs.OptionExample{{ 67 Value: "https://cloud.seafile.com/", 68 Help: "Connect to cloud.seafile.com.", 69 }}, 70 Sensitive: true, 71 }, { 72 Name: configUser, 73 Help: "User name (usually email address).", 74 Required: true, 75 Sensitive: true, 76 }, { 77 // Password is not required, it will be left blank for 2FA 78 Name: configPassword, 79 Help: "Password.", 80 IsPassword: true, 81 Sensitive: true, 82 }, { 83 Name: config2FA, 84 Help: "Two-factor authentication ('true' if the account has 2FA enabled).", 85 Default: false, 86 }, { 87 Name: configLibrary, 88 Help: "Name of the library.\n\nLeave blank to access all non-encrypted libraries.", 89 }, { 90 Name: configLibraryKey, 91 Help: "Library password (for encrypted libraries only).\n\nLeave blank if you pass it through the command line.", 92 IsPassword: true, 93 Sensitive: true, 94 }, { 95 Name: configCreateLibrary, 96 Help: "Should rclone create a library if it doesn't exist.", 97 Advanced: true, 98 Default: false, 99 }, { 100 // Keep the authentication token after entering the 2FA code 101 Name: configAuthToken, 102 Help: "Authentication token.", 103 Hide: fs.OptionHideBoth, 104 Sensitive: true, 105 }, { 106 Name: config.ConfigEncoding, 107 Help: config.ConfigEncodingHelp, 108 Advanced: true, 109 Default: (encoder.EncodeZero | 110 encoder.EncodeCtl | 111 encoder.EncodeSlash | 112 encoder.EncodeBackSlash | 113 encoder.EncodeDoubleQuote | 114 encoder.EncodeInvalidUtf8), 115 }}, 116 }) 117 } 118 119 // Options defines the configuration for this backend 120 type Options struct { 121 URL string `config:"url"` 122 User string `config:"user"` 123 Password string `config:"pass"` 124 Is2FA bool `config:"2fa"` 125 AuthToken string `config:"auth_token"` 126 LibraryName string `config:"library"` 127 LibraryKey string `config:"library_key"` 128 CreateLibrary bool `config:"create_library"` 129 Enc encoder.MultiEncoder `config:"encoding"` 130 } 131 132 // Fs represents a remote seafile 133 type Fs struct { 134 name string // name of this remote 135 root string // the path we are working on 136 libraryName string // current library 137 encrypted bool // Is this an encrypted library 138 rootDirectory string // directory part of root (if any) 139 opt Options // parsed options 140 libraries *cache.Cache // Keep a cache of libraries 141 librariesMutex sync.Mutex // Mutex to protect getLibraryID 142 features *fs.Features // optional features 143 endpoint *url.URL // URL of the host 144 endpointURL string // endpoint as a string 145 srv *rest.Client // the connection to the server 146 pacer *fs.Pacer // pacer for API calls 147 authMu sync.Mutex // Mutex to protect library decryption 148 createDirMutex sync.Mutex // Protect creation of directories 149 useOldDirectoryAPI bool // Use the old API v2 if seafile < 7 150 moveDirNotAvailable bool // Version < 7.0 don't have an API to move a directory 151 renew *Renew // Renew an encrypted library token 152 } 153 154 // ------------------------------------------------------------ 155 156 // NewFs constructs an Fs from the path, container:path 157 func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) { 158 // Parse config into Options struct 159 opt := new(Options) 160 err := configstruct.Set(m, opt) 161 if err != nil { 162 return nil, err 163 } 164 root = strings.Trim(root, "/") 165 isLibraryRooted := opt.LibraryName != "" 166 var libraryName, rootDirectory string 167 if isLibraryRooted { 168 libraryName = opt.LibraryName 169 rootDirectory = root 170 } else { 171 libraryName, rootDirectory = bucket.Split(root) 172 } 173 174 if !strings.HasSuffix(opt.URL, "/") { 175 opt.URL += "/" 176 } 177 if opt.Password != "" { 178 var err error 179 opt.Password, err = obscure.Reveal(opt.Password) 180 if err != nil { 181 return nil, fmt.Errorf("couldn't decrypt user password: %w", err) 182 } 183 } 184 if opt.LibraryKey != "" { 185 var err error 186 opt.LibraryKey, err = obscure.Reveal(opt.LibraryKey) 187 if err != nil { 188 return nil, fmt.Errorf("couldn't decrypt library password: %w", err) 189 } 190 } 191 192 // Parse the endpoint 193 u, err := url.Parse(opt.URL) 194 if err != nil { 195 return nil, err 196 } 197 198 f := &Fs{ 199 name: name, 200 root: root, 201 libraryName: libraryName, 202 rootDirectory: rootDirectory, 203 libraries: cache.New(), 204 opt: *opt, 205 endpoint: u, 206 endpointURL: u.String(), 207 srv: rest.NewClient(fshttp.NewClient(ctx)).SetRoot(u.String()), 208 pacer: getPacer(ctx, opt.URL), 209 } 210 f.features = (&fs.Features{ 211 CanHaveEmptyDirectories: true, 212 BucketBased: opt.LibraryName == "", 213 }).Fill(ctx, f) 214 215 serverInfo, err := f.getServerInfo(ctx) 216 if err != nil { 217 return nil, err 218 } 219 fs.Debugf(nil, "Seafile server version %s", serverInfo.Version) 220 221 // We don't support lower than seafile v6.0 (version 6.0 is already more than 3 years old) 222 serverVersion := semver.New(serverInfo.Version) 223 if serverVersion.Major < 6 { 224 return nil, errors.New("unsupported Seafile server (version < 6.0)") 225 } 226 if serverVersion.Major < 7 { 227 // Seafile 6 does not support recursive listing 228 f.useOldDirectoryAPI = true 229 f.features.ListR = nil 230 // It also does no support moving directories 231 f.moveDirNotAvailable = true 232 } 233 234 // Take the authentication token from the configuration first 235 token := f.opt.AuthToken 236 if token == "" { 237 // If not available, send the user/password instead 238 token, err = f.authorizeAccount(ctx) 239 if err != nil { 240 return nil, err 241 } 242 } 243 f.setAuthorizationToken(token) 244 245 if f.libraryName != "" { 246 // Check if the library exists 247 exists, err := f.libraryExists(ctx, f.libraryName) 248 if err != nil { 249 return f, err 250 } 251 if !exists { 252 if f.opt.CreateLibrary { 253 err := f.mkLibrary(ctx, f.libraryName, "") 254 if err != nil { 255 return f, err 256 } 257 } else { 258 return f, fmt.Errorf("library '%s' was not found, and the option to create it is not activated (advanced option)", f.libraryName) 259 } 260 } 261 libraryID, err := f.getLibraryID(ctx, f.libraryName) 262 if err != nil { 263 return f, err 264 } 265 f.encrypted, err = f.isEncrypted(ctx, libraryID) 266 if err != nil { 267 return f, err 268 } 269 if f.encrypted { 270 // If we're inside an encrypted library, let's decrypt it now 271 err = f.authorizeLibrary(ctx, libraryID) 272 if err != nil { 273 return f, err 274 } 275 // And remove the public link feature 276 f.features.PublicLink = nil 277 278 // renew the library password every 45 minutes 279 f.renew = NewRenew(45*time.Minute, func() error { 280 return f.authorizeLibrary(context.Background(), libraryID) 281 }) 282 } 283 } else { 284 // Deactivate the cleaner feature since there's no library selected 285 f.features.CleanUp = nil 286 } 287 288 if f.rootDirectory != "" { 289 // Check to see if the root is an existing file 290 remote := path.Base(rootDirectory) 291 f.rootDirectory = path.Dir(rootDirectory) 292 if f.rootDirectory == "." { 293 f.rootDirectory = "" 294 } 295 _, err := f.NewObject(ctx, remote) 296 if err != nil { 297 if errors.Is(err, fs.ErrorObjectNotFound) || errors.Is(err, fs.ErrorNotAFile) { 298 // File doesn't exist so return the original f 299 f.rootDirectory = rootDirectory 300 return f, nil 301 } 302 return f, err 303 } 304 // Correct root if definitely pointing to a file 305 f.root = path.Dir(f.root) 306 if f.root == "." || f.root == "/" { 307 f.root = "" 308 } 309 // return an error with an fs which points to the parent 310 return f, fs.ErrorIsFile 311 } 312 return f, nil 313 } 314 315 // Config callback for 2FA 316 func Config(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) { 317 serverURL, ok := m.Get(configURL) 318 if !ok || serverURL == "" { 319 // If there's no server URL, it means we're trying an operation at the backend level, like a "rclone authorize seafile" 320 return nil, errors.New("operation not supported on this remote. If you need a 2FA code on your account, use the command: rclone config reconnect <remote name>: ") 321 } 322 323 u, err := url.Parse(serverURL) 324 if err != nil { 325 return nil, fmt.Errorf("invalid server URL %s", serverURL) 326 } 327 328 is2faEnabled, _ := m.Get(config2FA) 329 if is2faEnabled != "true" { 330 // no need to do anything here 331 return nil, nil 332 } 333 334 username, _ := m.Get(configUser) 335 if username == "" { 336 return nil, errors.New("a username is required") 337 } 338 339 password, _ := m.Get(configPassword) 340 if password != "" { 341 password, _ = obscure.Reveal(password) 342 } 343 344 switch config.State { 345 case "": 346 // Empty state means it's the first call to the Config function 347 if password == "" { 348 return fs.ConfigPassword("password", "config_password", "Two-factor authentication: please enter your password (it won't be saved in the configuration)") 349 } 350 // password was successfully loaded from the config 351 return fs.ConfigGoto("2fa") 352 case "password": 353 // password should be coming from the previous state (entered by the user) 354 password = config.Result 355 if password == "" { 356 return fs.ConfigError("", "Password can't be blank") 357 } 358 // save it into the configuration file and keep going 359 m.Set(configPassword, obscure.MustObscure(password)) 360 return fs.ConfigGoto("2fa") 361 case "2fa": 362 return fs.ConfigInput("2fa_do", "config_2fa", "Two-factor authentication: please enter your 2FA code") 363 case "2fa_do": 364 code := config.Result 365 if code == "" { 366 return fs.ConfigError("2fa", "2FA codes can't be blank") 367 } 368 369 // Create rest client for getAuthorizationToken 370 url := u.String() 371 if !strings.HasPrefix(url, "/") { 372 url += "/" 373 } 374 srv := rest.NewClient(fshttp.NewClient(ctx)).SetRoot(url) 375 376 // We loop asking for a 2FA code 377 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 378 defer cancel() 379 380 token, err := getAuthorizationToken(ctx, srv, username, password, code) 381 if err != nil { 382 return fs.ConfigConfirm("2fa_error", true, "config_retry", fmt.Sprintf("Authentication failed: %v\n\nTry Again?", err)) 383 } 384 if token == "" { 385 return fs.ConfigConfirm("2fa_error", true, "config_retry", "Authentication failed - no token returned.\n\nTry Again?") 386 } 387 // Let's save the token into the configuration 388 m.Set(configAuthToken, token) 389 // And delete any previous entry for password 390 m.Set(configPassword, "") 391 // And we're done here 392 return nil, nil 393 case "2fa_error": 394 if config.Result == "true" { 395 return fs.ConfigGoto("2fa") 396 } 397 return nil, errors.New("2fa authentication failed") 398 } 399 return nil, fmt.Errorf("unknown state %q", config.State) 400 } 401 402 // Shutdown the Fs 403 func (f *Fs) Shutdown(ctx context.Context) error { 404 if f.renew == nil { 405 return nil 406 } 407 f.renew.Shutdown() 408 return nil 409 } 410 411 // sets the AuthorizationToken up 412 func (f *Fs) setAuthorizationToken(token string) { 413 f.srv.SetHeader("Authorization", "Token "+token) 414 } 415 416 // authorizeAccount gets the auth token. 417 func (f *Fs) authorizeAccount(ctx context.Context) (string, error) { 418 f.authMu.Lock() 419 defer f.authMu.Unlock() 420 421 token, err := f.getAuthorizationToken(ctx) 422 if err != nil { 423 return "", err 424 } 425 return token, nil 426 } 427 428 // retryErrorCodes is a slice of error codes that we will retry 429 var retryErrorCodes = []int{ 430 408, // Request Timeout 431 429, // Rate exceeded. 432 500, // Get occasional 500 Internal Server Error 433 503, // Service Unavailable 434 504, // Gateway Time-out 435 520, // Operation failed (We get them sometimes when running tests in parallel) 436 } 437 438 // shouldRetry returns a boolean as to whether this resp and err 439 // deserve to be retried. It returns the err as a convenience 440 func (f *Fs) shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) { 441 if fserrors.ContextError(ctx, &err) { 442 return false, err 443 } 444 // For 429 errors look at the Retry-After: header and 445 // set the retry appropriately, starting with a minimum of 1 446 // second if it isn't set. 447 if resp != nil && (resp.StatusCode == 429) { 448 var retryAfter = 1 449 retryAfterString := resp.Header.Get(retryAfterHeader) 450 if retryAfterString != "" { 451 var err error 452 retryAfter, err = strconv.Atoi(retryAfterString) 453 if err != nil { 454 fs.Errorf(f, "Malformed %s header %q: %v", retryAfterHeader, retryAfterString, err) 455 } 456 } 457 return true, pacer.RetryAfterError(err, time.Duration(retryAfter)*time.Second) 458 } 459 return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err 460 } 461 462 func (f *Fs) shouldRetryUpload(ctx context.Context, resp *http.Response, err error) (bool, error) { 463 if err != nil || (resp != nil && resp.StatusCode > 400) { 464 return true, err 465 } 466 return false, nil 467 } 468 469 // Name of the remote (as passed into NewFs) 470 func (f *Fs) Name() string { 471 return f.name 472 } 473 474 // Root of the remote (as passed into NewFs) 475 func (f *Fs) Root() string { 476 return f.root 477 } 478 479 // String converts this Fs to a string 480 func (f *Fs) String() string { 481 if f.libraryName == "" { 482 return "seafile root" 483 } 484 library := "library" 485 if f.encrypted { 486 library = "encrypted " + library 487 } 488 if f.rootDirectory == "" { 489 return fmt.Sprintf("seafile %s '%s'", library, f.libraryName) 490 } 491 return fmt.Sprintf("seafile %s '%s' path '%s'", library, f.libraryName, f.rootDirectory) 492 } 493 494 // Precision of the ModTimes in this Fs 495 func (f *Fs) Precision() time.Duration { 496 // The API doesn't support setting the modified time 497 return fs.ModTimeNotSupported 498 } 499 500 // Hashes returns the supported hash sets. 501 func (f *Fs) Hashes() hash.Set { 502 return hash.Set(hash.None) 503 } 504 505 // Features returns the optional features of this Fs 506 func (f *Fs) Features() *fs.Features { 507 return f.features 508 } 509 510 // List the objects and directories in dir into entries. The 511 // entries can be returned in any order but should be for a 512 // complete directory. 513 // 514 // dir should be "" to list the root, and should not have 515 // trailing slashes. 516 // 517 // This should return fs.ErrorDirNotFound if the directory isn't 518 // found. 519 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 520 if dir == "" && f.libraryName == "" { 521 return f.listLibraries(ctx) 522 } 523 return f.listDir(ctx, dir, false) 524 } 525 526 // NewObject finds the Object at remote. If it can't be found 527 // it returns the error fs.ErrorObjectNotFound. 528 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 529 libraryName, filePath := f.splitPath(remote) 530 libraryID, err := f.getLibraryID(ctx, libraryName) 531 if err != nil { 532 return nil, err 533 } 534 err = f.authorizeLibrary(ctx, libraryID) 535 if err != nil { 536 return nil, err 537 } 538 539 fileDetails, err := f.getFileDetails(ctx, libraryID, filePath) 540 if err != nil { 541 return nil, err 542 } 543 544 modTime, err := time.Parse(time.RFC3339, fileDetails.Modified) 545 if err != nil { 546 fs.LogPrintf(fs.LogLevelWarning, fileDetails.Modified, "Cannot parse datetime") 547 } 548 549 o := &Object{ 550 fs: f, 551 libraryID: libraryID, 552 id: fileDetails.ID, 553 remote: remote, 554 pathInLibrary: filePath, 555 modTime: modTime, 556 size: fileDetails.Size, 557 } 558 return o, nil 559 } 560 561 // Put in to the remote path with the modTime given of the given size 562 // 563 // When called from outside an Fs by rclone, src.Size() will always be >= 0. 564 // But for unknown-sized objects (indicated by src.Size() == -1), Put should either 565 // return an error or upload it properly (rather than e.g. calling panic). 566 // 567 // May create the object even if it returns an error - if so 568 // will return the object and the error, otherwise will return 569 // nil and the error 570 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 571 object := f.newObject(ctx, src.Remote(), src.Size(), src.ModTime(ctx)) 572 // Check if we need to create a new library at that point 573 if object.libraryID == "" { 574 library, _ := f.splitPath(object.remote) 575 err := f.Mkdir(ctx, library) 576 if err != nil { 577 return object, err 578 } 579 libraryID, err := f.getLibraryID(ctx, library) 580 if err != nil { 581 return object, err 582 } 583 object.libraryID = libraryID 584 } 585 err := object.Update(ctx, in, src, options...) 586 if err != nil { 587 return object, err 588 } 589 return object, nil 590 } 591 592 // PutStream uploads to the remote path with the modTime given but of indeterminate size 593 func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 594 return f.Put(ctx, in, src, options...) 595 } 596 597 // Mkdir makes the directory or library 598 // 599 // Shouldn't return an error if it already exists 600 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 601 libraryName, folder := f.splitPath(dir) 602 if strings.HasPrefix(dir, libraryName) { 603 err := f.mkLibrary(ctx, libraryName, "") 604 if err != nil { 605 return err 606 } 607 if folder == "" { 608 // No directory to create after the library 609 return nil 610 } 611 } 612 err := f.mkDir(ctx, dir) 613 if err != nil { 614 return err 615 } 616 return nil 617 } 618 619 // purgeCheck removes the root directory, if check is set then it 620 // refuses to do so if it has anything in 621 func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error { 622 libraryName, dirPath := f.splitPath(dir) 623 libraryID, err := f.getLibraryID(ctx, libraryName) 624 if err != nil { 625 return err 626 } 627 628 if check { 629 directoryEntries, err := f.getDirectoryEntries(ctx, libraryID, dirPath, false) 630 if err != nil { 631 return err 632 } 633 if len(directoryEntries) > 0 { 634 return fs.ErrorDirectoryNotEmpty 635 } 636 } 637 638 if dirPath == "" || dirPath == "/" { 639 return f.deleteLibrary(ctx, libraryID) 640 } 641 return f.deleteDir(ctx, libraryID, dirPath) 642 } 643 644 // Rmdir removes the directory or library if empty 645 // 646 // Return an error if it doesn't exist or isn't empty 647 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 648 return f.purgeCheck(ctx, dir, true) 649 } 650 651 // ==================== Optional Interface fs.ListRer ==================== 652 653 // ListR lists the objects and directories of the Fs starting 654 // from dir recursively into out. 655 // 656 // dir should be "" to start from the root, and should not 657 // have trailing slashes. 658 // 659 // This should return ErrDirNotFound if the directory isn't 660 // found. 661 // 662 // It should call callback for each tranche of entries read. 663 // These need not be returned in any particular order. If 664 // callback returns an error then the listing will stop 665 // immediately. 666 func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) error { 667 var err error 668 669 if dir == "" && f.libraryName == "" { 670 libraries, err := f.listLibraries(ctx) 671 if err != nil { 672 return err 673 } 674 // Send the library list as folders 675 err = callback(libraries) 676 if err != nil { 677 return err 678 } 679 680 // Then list each library 681 for _, library := range libraries { 682 err = f.listDirCallback(ctx, library.Remote(), callback) 683 if err != nil { 684 return err 685 } 686 } 687 return nil 688 } 689 err = f.listDirCallback(ctx, dir, callback) 690 if err != nil { 691 return err 692 } 693 return nil 694 } 695 696 // ==================== Optional Interface fs.Copier ==================== 697 698 // Copy src to this remote using server-side copy operations. 699 // 700 // This is stored with the remote path given. 701 // 702 // It returns the destination Object and a possible error. 703 // 704 // If it isn't possible then return fs.ErrorCantCopy 705 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 706 srcObj, ok := src.(*Object) 707 if !ok { 708 return nil, fs.ErrorCantCopy 709 } 710 srcLibraryName, srcPath := srcObj.fs.splitPath(src.Remote()) 711 srcLibraryID, err := srcObj.fs.getLibraryID(ctx, srcLibraryName) 712 if err != nil { 713 return nil, err 714 } 715 dstLibraryName, dstPath := f.splitPath(remote) 716 dstLibraryID, err := f.getLibraryID(ctx, dstLibraryName) 717 if err != nil { 718 return nil, err 719 } 720 721 // Seafile does not accept a file name as a destination, only a path. 722 // The destination filename will be the same as the original, or with (1) added in case it was already existing 723 dstDir, dstFilename := path.Split(dstPath) 724 725 // We have to make sure the destination path exists on the server or it's going to bomb out with an obscure error message 726 err = f.mkMultiDir(ctx, dstLibraryID, dstDir) 727 if err != nil { 728 return nil, err 729 } 730 731 op, err := f.copyFile(ctx, srcLibraryID, srcPath, dstLibraryID, dstDir) 732 if err != nil { 733 return nil, err 734 } 735 736 if op.Name != dstFilename { 737 // Destination was existing, so we need to move the file back into place 738 err = f.adjustDestination(ctx, dstLibraryID, op.Name, dstPath, dstDir, dstFilename) 739 if err != nil { 740 return nil, err 741 } 742 } 743 // Create a new object from the result 744 return f.NewObject(ctx, remote) 745 } 746 747 // ==================== Optional Interface fs.Mover ==================== 748 749 // Move src to this remote using server-side move operations. 750 // 751 // This is stored with the remote path given. 752 // 753 // It returns the destination Object and a possible error. 754 // 755 // If it isn't possible then return fs.ErrorCantMove 756 func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 757 srcObj, ok := src.(*Object) 758 if !ok { 759 return nil, fs.ErrorCantMove 760 } 761 762 srcLibraryName, srcPath := srcObj.fs.splitPath(src.Remote()) 763 srcLibraryID, err := srcObj.fs.getLibraryID(ctx, srcLibraryName) 764 if err != nil { 765 return nil, err 766 } 767 dstLibraryName, dstPath := f.splitPath(remote) 768 dstLibraryID, err := f.getLibraryID(ctx, dstLibraryName) 769 if err != nil { 770 return nil, err 771 } 772 773 // anchor both source and destination paths from the root so we can compare them 774 srcPath = path.Join("/", srcPath) 775 dstPath = path.Join("/", dstPath) 776 777 srcDir := path.Dir(srcPath) 778 dstDir, dstFilename := path.Split(dstPath) 779 780 if srcLibraryID == dstLibraryID && srcDir == dstDir { 781 // It's only a simple case of renaming the file 782 _, err := f.renameFile(ctx, srcLibraryID, srcPath, dstFilename) 783 if err != nil { 784 return nil, err 785 } 786 return f.NewObject(ctx, remote) 787 } 788 789 // We have to make sure the destination path exists on the server 790 err = f.mkMultiDir(ctx, dstLibraryID, dstDir) 791 if err != nil { 792 return nil, err 793 } 794 795 // Seafile does not accept a file name as a destination, only a path. 796 // The destination filename will be the same as the original, or with (1) added in case it already exists 797 op, err := f.moveFile(ctx, srcLibraryID, srcPath, dstLibraryID, dstDir) 798 if err != nil { 799 return nil, err 800 } 801 802 if op.Name != dstFilename { 803 // Destination was existing, so we need to move the file back into place 804 err = f.adjustDestination(ctx, dstLibraryID, op.Name, dstPath, dstDir, dstFilename) 805 if err != nil { 806 return nil, err 807 } 808 } 809 810 // Create a new object from the result 811 return f.NewObject(ctx, remote) 812 } 813 814 // adjustDestination rename the file 815 func (f *Fs) adjustDestination(ctx context.Context, libraryID, srcFilename, dstPath, dstDir, dstFilename string) error { 816 // Seafile seems to be acting strangely if the renamed file already exists (some cache issue maybe?) 817 // It's better to delete the destination if it already exists 818 fileDetail, err := f.getFileDetails(ctx, libraryID, dstPath) 819 if err != nil && err != fs.ErrorObjectNotFound { 820 return err 821 } 822 if fileDetail != nil { 823 err = f.deleteFile(ctx, libraryID, dstPath) 824 if err != nil { 825 return err 826 } 827 } 828 _, err = f.renameFile(ctx, libraryID, path.Join(dstDir, srcFilename), dstFilename) 829 if err != nil { 830 return err 831 } 832 833 return nil 834 } 835 836 // ==================== Optional Interface fs.DirMover ==================== 837 838 // DirMove moves src, srcRemote to this remote at dstRemote 839 // using server-side move operations. 840 // 841 // Will only be called if src.Fs().Name() == f.Name() 842 // 843 // If it isn't possible then return fs.ErrorCantDirMove 844 // 845 // If destination exists then return fs.ErrorDirExists 846 func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error { 847 848 // Cast into a seafile Fs 849 srcFs, ok := src.(*Fs) 850 if !ok { 851 return fs.ErrorCantDirMove 852 } 853 854 srcLibraryName, srcPath := srcFs.splitPath(srcRemote) 855 srcLibraryID, err := srcFs.getLibraryID(ctx, srcLibraryName) 856 if err != nil { 857 return err 858 } 859 dstLibraryName, dstPath := f.splitPath(dstRemote) 860 dstLibraryID, err := f.getLibraryID(ctx, dstLibraryName) 861 if err != nil { 862 return err 863 } 864 865 srcDir := path.Dir(srcPath) 866 dstDir, dstName := path.Split(dstPath) 867 868 // anchor both source and destination to the root so we can compare them 869 srcDir = path.Join("/", srcDir) 870 dstDir = path.Join("/", dstDir) 871 872 // The destination should not exist 873 entries, err := f.getDirectoryEntries(ctx, dstLibraryID, dstDir, false) 874 if err != nil && err != fs.ErrorDirNotFound { 875 return err 876 } 877 if err == nil { 878 for _, entry := range entries { 879 if entry.Name == dstName { 880 // Destination exists 881 return fs.ErrorDirExists 882 } 883 } 884 } 885 if srcLibraryID == dstLibraryID && srcDir == dstDir { 886 // It's only renaming 887 err = srcFs.renameDir(ctx, dstLibraryID, srcPath, dstName) 888 if err != nil { 889 return err 890 } 891 return nil 892 } 893 894 // Seafile < 7 does not support moving directories 895 if f.moveDirNotAvailable { 896 return fs.ErrorCantDirMove 897 } 898 899 // Make sure the destination path exists 900 err = f.mkMultiDir(ctx, dstLibraryID, dstDir) 901 if err != nil { 902 return err 903 } 904 905 // If the destination already exists, seafile will add a " (n)" to the name. 906 // Sadly this API call will not return the new given name like the move file version does 907 // So the trick is to rename the directory to something random before moving it 908 // After the move we rename the random name back to the expected one 909 // Hopefully there won't be anything with the same name existing at destination ;) 910 tempName := ".rclone-move-" + random.String(32) 911 912 // 1- rename source 913 err = srcFs.renameDir(ctx, srcLibraryID, srcPath, tempName) 914 if err != nil { 915 return fmt.Errorf("cannot rename source directory to a temporary name: %w", err) 916 } 917 918 // 2- move source to destination 919 err = f.moveDir(ctx, srcLibraryID, srcDir, tempName, dstLibraryID, dstDir) 920 if err != nil { 921 // Doh! Let's rename the source back to its original name 922 _ = srcFs.renameDir(ctx, srcLibraryID, path.Join(srcDir, tempName), path.Base(srcPath)) 923 return err 924 } 925 926 // 3- rename destination back to source name 927 err = f.renameDir(ctx, dstLibraryID, path.Join(dstDir, tempName), dstName) 928 if err != nil { 929 return fmt.Errorf("cannot rename temporary directory to destination name: %w", err) 930 } 931 932 return nil 933 } 934 935 // ==================== Optional Interface fs.Purger ==================== 936 937 // Purge all files in the directory 938 // 939 // Implement this if you have a way of deleting all the files 940 // quicker than just running Remove() on the result of List() 941 // 942 // Return an error if it doesn't exist 943 func (f *Fs) Purge(ctx context.Context, dir string) error { 944 return f.purgeCheck(ctx, dir, false) 945 } 946 947 // ==================== Optional Interface fs.CleanUpper ==================== 948 949 // CleanUp the trash in the Fs 950 func (f *Fs) CleanUp(ctx context.Context) error { 951 if f.libraryName == "" { 952 return errors.New("cannot clean up at the root of the seafile server, please select a library to clean up") 953 } 954 libraryID, err := f.getLibraryID(ctx, f.libraryName) 955 if err != nil { 956 return err 957 } 958 return f.emptyLibraryTrash(ctx, libraryID) 959 } 960 961 // ==================== Optional Interface fs.Abouter ==================== 962 963 // About gets quota information 964 func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) { 965 accountInfo, err := f.getUserAccountInfo(ctx) 966 if err != nil { 967 return nil, err 968 } 969 970 usage = &fs.Usage{ 971 Used: fs.NewUsageValue(accountInfo.Usage), // bytes in use 972 } 973 if accountInfo.Total > 0 { 974 usage.Total = fs.NewUsageValue(accountInfo.Total) // quota of bytes that can be used 975 usage.Free = fs.NewUsageValue(accountInfo.Total - accountInfo.Usage) // bytes which can be uploaded before reaching the quota 976 } 977 return usage, nil 978 } 979 980 // ==================== Optional Interface fs.UserInfoer ==================== 981 982 // UserInfo returns info about the connected user 983 func (f *Fs) UserInfo(ctx context.Context) (map[string]string, error) { 984 accountInfo, err := f.getUserAccountInfo(ctx) 985 if err != nil { 986 return nil, err 987 } 988 return map[string]string{ 989 "Name": accountInfo.Name, 990 "Email": accountInfo.Email, 991 }, nil 992 } 993 994 // ==================== Optional Interface fs.PublicLinker ==================== 995 996 // PublicLink generates a public link to the remote path (usually readable by anyone) 997 func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) { 998 libraryName, filePath := f.splitPath(remote) 999 if libraryName == "" { 1000 // We cannot share the whole seafile server, we need at least a library 1001 return "", errors.New("cannot share the root of the seafile server, please select a library to share") 1002 } 1003 libraryID, err := f.getLibraryID(ctx, libraryName) 1004 if err != nil { 1005 return "", err 1006 } 1007 1008 // List existing links first 1009 shareLinks, err := f.listShareLinks(ctx, libraryID, filePath) 1010 if err != nil { 1011 return "", err 1012 } 1013 if len(shareLinks) > 0 { 1014 for _, shareLink := range shareLinks { 1015 if !shareLink.IsExpired { 1016 return shareLink.Link, nil 1017 } 1018 } 1019 } 1020 // No link was found 1021 shareLink, err := f.createShareLink(ctx, libraryID, filePath) 1022 if err != nil { 1023 return "", err 1024 } 1025 if shareLink.IsExpired { 1026 return "", nil 1027 } 1028 return shareLink.Link, nil 1029 } 1030 1031 func (f *Fs) listLibraries(ctx context.Context) (entries fs.DirEntries, err error) { 1032 libraries, err := f.getCachedLibraries(ctx) 1033 if err != nil { 1034 return nil, errors.New("cannot load libraries") 1035 } 1036 1037 for _, library := range libraries { 1038 d := fs.NewDir(library.Name, time.Unix(library.Modified, 0)) 1039 d.SetSize(library.Size) 1040 entries = append(entries, d) 1041 } 1042 1043 return entries, nil 1044 } 1045 1046 func (f *Fs) libraryExists(ctx context.Context, libraryName string) (bool, error) { 1047 libraries, err := f.getCachedLibraries(ctx) 1048 if err != nil { 1049 return false, err 1050 } 1051 1052 for _, library := range libraries { 1053 if library.Name == libraryName { 1054 return true, nil 1055 } 1056 } 1057 return false, nil 1058 } 1059 1060 func (f *Fs) getLibraryID(ctx context.Context, name string) (string, error) { 1061 libraries, err := f.getCachedLibraries(ctx) 1062 if err != nil { 1063 return "", err 1064 } 1065 1066 for _, library := range libraries { 1067 if library.Name == name { 1068 return library.ID, nil 1069 } 1070 } 1071 return "", fmt.Errorf("cannot find library '%s'", name) 1072 } 1073 1074 func (f *Fs) isLibraryInCache(libraryName string) bool { 1075 f.librariesMutex.Lock() 1076 defer f.librariesMutex.Unlock() 1077 1078 if f.libraries == nil { 1079 return false 1080 } 1081 value, found := f.libraries.GetMaybe(librariesCacheKey) 1082 if !found { 1083 return false 1084 } 1085 libraries := value.([]api.Library) 1086 for _, library := range libraries { 1087 if library.Name == libraryName { 1088 return true 1089 } 1090 } 1091 return false 1092 } 1093 1094 func (f *Fs) isEncrypted(ctx context.Context, libraryID string) (bool, error) { 1095 libraries, err := f.getCachedLibraries(ctx) 1096 if err != nil { 1097 return false, err 1098 } 1099 1100 for _, library := range libraries { 1101 if library.ID == libraryID { 1102 return library.Encrypted, nil 1103 } 1104 } 1105 return false, fmt.Errorf("cannot find library ID %s", libraryID) 1106 } 1107 1108 func (f *Fs) authorizeLibrary(ctx context.Context, libraryID string) error { 1109 if libraryID == "" { 1110 return errors.New("a library ID is needed") 1111 } 1112 if f.opt.LibraryKey == "" { 1113 // We have no password to send 1114 return nil 1115 } 1116 encrypted, err := f.isEncrypted(ctx, libraryID) 1117 if err != nil { 1118 return err 1119 } 1120 if encrypted { 1121 fs.Debugf(nil, "Decrypting library %s", libraryID) 1122 f.authMu.Lock() 1123 defer f.authMu.Unlock() 1124 err := f.decryptLibrary(ctx, libraryID, f.opt.LibraryKey) 1125 if err != nil { 1126 return err 1127 } 1128 } 1129 return nil 1130 } 1131 1132 func (f *Fs) mkLibrary(ctx context.Context, libraryName, password string) error { 1133 // lock specific to library creation 1134 // we cannot reuse the same lock as we will dead-lock ourself if the libraries are not in cache 1135 createLibraryMutex.Lock() 1136 defer createLibraryMutex.Unlock() 1137 1138 if libraryName == "" { 1139 return errors.New("a library name is needed") 1140 } 1141 1142 // It's quite likely that multiple go routines are going to try creating the same library 1143 // at the start of a sync/copy. After releasing the mutex the calls waiting would try to create 1144 // the same library again. So we'd better check the library exists first 1145 if f.isLibraryInCache(libraryName) { 1146 return nil 1147 } 1148 1149 fs.Debugf(nil, "%s: Create library '%s'", f.Name(), libraryName) 1150 f.librariesMutex.Lock() 1151 defer f.librariesMutex.Unlock() 1152 1153 library, err := f.createLibrary(ctx, libraryName, password) 1154 if err != nil { 1155 return err 1156 } 1157 // Stores the library details into the cache 1158 value, found := f.libraries.GetMaybe(librariesCacheKey) 1159 if !found { 1160 // Don't update the cache at that point 1161 return nil 1162 } 1163 libraries := value.([]api.Library) 1164 libraries = append(libraries, api.Library{ 1165 ID: library.ID, 1166 Name: library.Name, 1167 }) 1168 f.libraries.Put(librariesCacheKey, libraries) 1169 return nil 1170 } 1171 1172 // splitPath returns the library name and the full path inside the library 1173 func (f *Fs) splitPath(dir string) (library, folder string) { 1174 library = f.libraryName 1175 folder = dir 1176 if library == "" { 1177 // The first part of the path is the library 1178 library, folder = bucket.Split(dir) 1179 } else if f.rootDirectory != "" { 1180 // Adds the root folder to the path to get a full path 1181 folder = path.Join(f.rootDirectory, folder) 1182 } 1183 return 1184 } 1185 1186 func (f *Fs) listDir(ctx context.Context, dir string, recursive bool) (entries fs.DirEntries, err error) { 1187 libraryName, dirPath := f.splitPath(dir) 1188 libraryID, err := f.getLibraryID(ctx, libraryName) 1189 if err != nil { 1190 return nil, err 1191 } 1192 1193 directoryEntries, err := f.getDirectoryEntries(ctx, libraryID, dirPath, recursive) 1194 if err != nil { 1195 return nil, err 1196 } 1197 1198 return f.buildDirEntries(dir, libraryID, dirPath, directoryEntries, recursive), nil 1199 } 1200 1201 // listDirCallback is calling listDir with the recursive option and is sending the result to the callback 1202 func (f *Fs) listDirCallback(ctx context.Context, dir string, callback fs.ListRCallback) error { 1203 entries, err := f.listDir(ctx, dir, true) 1204 if err != nil { 1205 return err 1206 } 1207 err = callback(entries) 1208 if err != nil { 1209 return err 1210 } 1211 return nil 1212 } 1213 1214 func (f *Fs) buildDirEntries(parentPath, libraryID, parentPathInLibrary string, directoryEntries []api.DirEntry, recursive bool) (entries fs.DirEntries) { 1215 for _, entry := range directoryEntries { 1216 var filePath, filePathInLibrary string 1217 if recursive { 1218 // In recursive mode, paths are built from DirEntry (+ a starting point) 1219 entryPath := strings.TrimPrefix(entry.Path, "/") 1220 // If we're listing from some path inside the library (not the root) 1221 // there's already a path in parameter, which will also be included in the entry path 1222 entryPath = strings.TrimPrefix(entryPath, parentPathInLibrary) 1223 entryPath = strings.TrimPrefix(entryPath, "/") 1224 1225 filePath = path.Join(parentPath, entryPath, entry.Name) 1226 filePathInLibrary = path.Join(parentPathInLibrary, entryPath, entry.Name) 1227 } else { 1228 // In non-recursive mode, paths are build from the parameters 1229 filePath = path.Join(parentPath, entry.Name) 1230 filePathInLibrary = path.Join(parentPathInLibrary, entry.Name) 1231 } 1232 if entry.Type == api.FileTypeDir { 1233 d := fs. 1234 NewDir(filePath, time.Unix(entry.Modified, 0)). 1235 SetSize(entry.Size). 1236 SetID(entry.ID) 1237 entries = append(entries, d) 1238 } else if entry.Type == api.FileTypeFile { 1239 object := &Object{ 1240 fs: f, 1241 id: entry.ID, 1242 remote: filePath, 1243 pathInLibrary: filePathInLibrary, 1244 size: entry.Size, 1245 modTime: time.Unix(entry.Modified, 0), 1246 libraryID: libraryID, 1247 } 1248 entries = append(entries, object) 1249 } 1250 } 1251 return entries 1252 } 1253 1254 func (f *Fs) mkDir(ctx context.Context, dir string) error { 1255 library, fullPath := f.splitPath(dir) 1256 libraryID, err := f.getLibraryID(ctx, library) 1257 if err != nil { 1258 return err 1259 } 1260 return f.mkMultiDir(ctx, libraryID, fullPath) 1261 } 1262 1263 func (f *Fs) mkMultiDir(ctx context.Context, libraryID, dir string) error { 1264 // rebuild the path one by one 1265 currentPath := "" 1266 for _, singleDir := range splitPath(dir) { 1267 currentPath = path.Join(currentPath, singleDir) 1268 err := f.mkSingleDir(ctx, libraryID, currentPath) 1269 if err != nil { 1270 return err 1271 } 1272 } 1273 return nil 1274 } 1275 1276 func (f *Fs) mkSingleDir(ctx context.Context, libraryID, dir string) error { 1277 f.createDirMutex.Lock() 1278 defer f.createDirMutex.Unlock() 1279 1280 dirDetails, err := f.getDirectoryDetails(ctx, libraryID, dir) 1281 if err == nil && dirDetails != nil { 1282 // Don't fail if the directory exists 1283 return nil 1284 } 1285 if err == fs.ErrorDirNotFound { 1286 err = f.createDir(ctx, libraryID, dir) 1287 if err != nil { 1288 return err 1289 } 1290 return nil 1291 } 1292 return err 1293 } 1294 1295 func (f *Fs) getDirectoryEntries(ctx context.Context, libraryID, folder string, recursive bool) ([]api.DirEntry, error) { 1296 if f.useOldDirectoryAPI { 1297 return f.getDirectoryEntriesAPIv2(ctx, libraryID, folder) 1298 } 1299 return f.getDirectoryEntriesAPIv21(ctx, libraryID, folder, recursive) 1300 } 1301 1302 // splitPath creates a slice of paths 1303 func splitPath(tree string) (paths []string) { 1304 tree, leaf := path.Split(path.Clean(tree)) 1305 for leaf != "" && leaf != "." { 1306 paths = append([]string{leaf}, paths...) 1307 tree, leaf = path.Split(path.Clean(tree)) 1308 } 1309 return 1310 } 1311 1312 func (f *Fs) getCachedLibraries(ctx context.Context) ([]api.Library, error) { 1313 f.librariesMutex.Lock() 1314 defer f.librariesMutex.Unlock() 1315 1316 libraries, err := f.libraries.Get(librariesCacheKey, func(key string) (value interface{}, ok bool, error error) { 1317 // Load the libraries if not present in the cache 1318 libraries, err := f.getLibraries(ctx) 1319 if err != nil { 1320 return nil, false, err 1321 } 1322 return libraries, true, nil 1323 }) 1324 if err != nil { 1325 return nil, err 1326 } 1327 // Type assertion 1328 return libraries.([]api.Library), nil 1329 } 1330 1331 func (f *Fs) newObject(ctx context.Context, remote string, size int64, modTime time.Time) *Object { 1332 libraryName, remotePath := f.splitPath(remote) 1333 libraryID, _ := f.getLibraryID(ctx, libraryName) // If error it means the library does not exist (yet) 1334 1335 object := &Object{ 1336 fs: f, 1337 remote: remote, 1338 libraryID: libraryID, 1339 pathInLibrary: remotePath, 1340 size: size, 1341 modTime: modTime, 1342 } 1343 return object 1344 } 1345 1346 // Check the interfaces are satisfied 1347 var ( 1348 _ fs.Fs = &Fs{} 1349 _ fs.Abouter = &Fs{} 1350 _ fs.CleanUpper = &Fs{} 1351 _ fs.Copier = &Fs{} 1352 _ fs.Mover = &Fs{} 1353 _ fs.DirMover = &Fs{} 1354 _ fs.ListRer = &Fs{} 1355 _ fs.Purger = &Fs{} 1356 _ fs.PutStreamer = &Fs{} 1357 _ fs.PublicLinker = &Fs{} 1358 _ fs.UserInfoer = &Fs{} 1359 _ fs.Shutdowner = &Fs{} 1360 _ fs.Object = &Object{} 1361 _ fs.IDer = &Object{} 1362 )