1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363 |
- // Package seafile provides an interface to the Seafile storage system.
- package seafile
- import (
- "context"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "path"
- "strconv"
- "strings"
- "sync"
- "time"
- "github.com/coreos/go-semver/semver"
- "github.com/rclone/rclone/backend/seafile/api"
- "github.com/rclone/rclone/fs"
- "github.com/rclone/rclone/fs/config"
- "github.com/rclone/rclone/fs/config/configmap"
- "github.com/rclone/rclone/fs/config/configstruct"
- "github.com/rclone/rclone/fs/config/obscure"
- "github.com/rclone/rclone/fs/fserrors"
- "github.com/rclone/rclone/fs/fshttp"
- "github.com/rclone/rclone/fs/hash"
- "github.com/rclone/rclone/lib/bucket"
- "github.com/rclone/rclone/lib/cache"
- "github.com/rclone/rclone/lib/encoder"
- "github.com/rclone/rclone/lib/pacer"
- "github.com/rclone/rclone/lib/random"
- "github.com/rclone/rclone/lib/rest"
- )
- const (
- librariesCacheKey = "all"
- retryAfterHeader = "Retry-After"
- configURL = "url"
- configUser = "user"
- configPassword = "pass"
- config2FA = "2fa"
- configLibrary = "library"
- configLibraryKey = "library_key"
- configCreateLibrary = "create_library"
- configAuthToken = "auth_token"
- )
- // This is global to all instances of fs
- // (copying from a seafile remote to another remote would create 2 fs)
- var (
- rangeDownloadNotice sync.Once // Display the notice only once
- createLibraryMutex sync.Mutex // Mutex to protect library creation
- )
- // Register with Fs
- func init() {
- fs.Register(&fs.RegInfo{
- Name: "seafile",
- Description: "seafile",
- NewFs: NewFs,
- Config: Config,
- Options: []fs.Option{{
- Name: configURL,
- Help: "URL of seafile host to connect to.",
- Required: true,
- Examples: []fs.OptionExample{{
- Value: "https://cloud.seafile.com/",
- Help: "Connect to cloud.seafile.com.",
- }},
- Sensitive: true,
- }, {
- Name: configUser,
- Help: "User name (usually email address).",
- Required: true,
- Sensitive: true,
- }, {
- // Password is not required, it will be left blank for 2FA
- Name: configPassword,
- Help: "Password.",
- IsPassword: true,
- Sensitive: true,
- }, {
- Name: config2FA,
- Help: "Two-factor authentication ('true' if the account has 2FA enabled).",
- Default: false,
- }, {
- Name: configLibrary,
- Help: "Name of the library.\n\nLeave blank to access all non-encrypted libraries.",
- }, {
- Name: configLibraryKey,
- Help: "Library password (for encrypted libraries only).\n\nLeave blank if you pass it through the command line.",
- IsPassword: true,
- Sensitive: true,
- }, {
- Name: configCreateLibrary,
- Help: "Should rclone create a library if it doesn't exist.",
- Advanced: true,
- Default: false,
- }, {
- // Keep the authentication token after entering the 2FA code
- Name: configAuthToken,
- Help: "Authentication token.",
- Hide: fs.OptionHideBoth,
- Sensitive: true,
- }, {
- Name: config.ConfigEncoding,
- Help: config.ConfigEncodingHelp,
- Advanced: true,
- Default: (encoder.EncodeZero |
- encoder.EncodeCtl |
- encoder.EncodeSlash |
- encoder.EncodeBackSlash |
- encoder.EncodeDoubleQuote |
- encoder.EncodeInvalidUtf8),
- }},
- })
- }
- // Options defines the configuration for this backend
- type Options struct {
- URL string `config:"url"`
- User string `config:"user"`
- Password string `config:"pass"`
- Is2FA bool `config:"2fa"`
- AuthToken string `config:"auth_token"`
- LibraryName string `config:"library"`
- LibraryKey string `config:"library_key"`
- CreateLibrary bool `config:"create_library"`
- Enc encoder.MultiEncoder `config:"encoding"`
- }
- // Fs represents a remote seafile
- type Fs struct {
- name string // name of this remote
- root string // the path we are working on
- libraryName string // current library
- encrypted bool // Is this an encrypted library
- rootDirectory string // directory part of root (if any)
- opt Options // parsed options
- libraries *cache.Cache // Keep a cache of libraries
- librariesMutex sync.Mutex // Mutex to protect getLibraryID
- features *fs.Features // optional features
- endpoint *url.URL // URL of the host
- endpointURL string // endpoint as a string
- srv *rest.Client // the connection to the server
- pacer *fs.Pacer // pacer for API calls
- authMu sync.Mutex // Mutex to protect library decryption
- createDirMutex sync.Mutex // Protect creation of directories
- useOldDirectoryAPI bool // Use the old API v2 if seafile < 7
- moveDirNotAvailable bool // Version < 7.0 don't have an API to move a directory
- renew *Renew // Renew an encrypted library token
- }
- // ------------------------------------------------------------
- // NewFs constructs an Fs from the path, container:path
- func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
- // Parse config into Options struct
- opt := new(Options)
- err := configstruct.Set(m, opt)
- if err != nil {
- return nil, err
- }
- root = strings.Trim(root, "/")
- isLibraryRooted := opt.LibraryName != ""
- var libraryName, rootDirectory string
- if isLibraryRooted {
- libraryName = opt.LibraryName
- rootDirectory = root
- } else {
- libraryName, rootDirectory = bucket.Split(root)
- }
- if !strings.HasSuffix(opt.URL, "/") {
- opt.URL += "/"
- }
- if opt.Password != "" {
- var err error
- opt.Password, err = obscure.Reveal(opt.Password)
- if err != nil {
- return nil, fmt.Errorf("couldn't decrypt user password: %w", err)
- }
- }
- if opt.LibraryKey != "" {
- var err error
- opt.LibraryKey, err = obscure.Reveal(opt.LibraryKey)
- if err != nil {
- return nil, fmt.Errorf("couldn't decrypt library password: %w", err)
- }
- }
- // Parse the endpoint
- u, err := url.Parse(opt.URL)
- if err != nil {
- return nil, err
- }
- f := &Fs{
- name: name,
- root: root,
- libraryName: libraryName,
- rootDirectory: rootDirectory,
- libraries: cache.New(),
- opt: *opt,
- endpoint: u,
- endpointURL: u.String(),
- srv: rest.NewClient(fshttp.NewClient(ctx)).SetRoot(u.String()),
- pacer: getPacer(ctx, opt.URL),
- }
- f.features = (&fs.Features{
- CanHaveEmptyDirectories: true,
- BucketBased: opt.LibraryName == "",
- }).Fill(ctx, f)
- serverInfo, err := f.getServerInfo(ctx)
- if err != nil {
- return nil, err
- }
- fs.Debugf(nil, "Seafile server version %s", serverInfo.Version)
- // We don't support lower than seafile v6.0 (version 6.0 is already more than 3 years old)
- serverVersion := semver.New(serverInfo.Version)
- if serverVersion.Major < 6 {
- return nil, errors.New("unsupported Seafile server (version < 6.0)")
- }
- if serverVersion.Major < 7 {
- // Seafile 6 does not support recursive listing
- f.useOldDirectoryAPI = true
- f.features.ListR = nil
- // It also does no support moving directories
- f.moveDirNotAvailable = true
- }
- // Take the authentication token from the configuration first
- token := f.opt.AuthToken
- if token == "" {
- // If not available, send the user/password instead
- token, err = f.authorizeAccount(ctx)
- if err != nil {
- return nil, err
- }
- }
- f.setAuthorizationToken(token)
- if f.libraryName != "" {
- // Check if the library exists
- exists, err := f.libraryExists(ctx, f.libraryName)
- if err != nil {
- return f, err
- }
- if !exists {
- if f.opt.CreateLibrary {
- err := f.mkLibrary(ctx, f.libraryName, "")
- if err != nil {
- return f, err
- }
- } else {
- return f, fmt.Errorf("library '%s' was not found, and the option to create it is not activated (advanced option)", f.libraryName)
- }
- }
- libraryID, err := f.getLibraryID(ctx, f.libraryName)
- if err != nil {
- return f, err
- }
- f.encrypted, err = f.isEncrypted(ctx, libraryID)
- if err != nil {
- return f, err
- }
- if f.encrypted {
- // If we're inside an encrypted library, let's decrypt it now
- err = f.authorizeLibrary(ctx, libraryID)
- if err != nil {
- return f, err
- }
- // And remove the public link feature
- f.features.PublicLink = nil
- // renew the library password every 45 minutes
- f.renew = NewRenew(45*time.Minute, func() error {
- return f.authorizeLibrary(context.Background(), libraryID)
- })
- }
- } else {
- // Deactivate the cleaner feature since there's no library selected
- f.features.CleanUp = nil
- }
- if f.rootDirectory != "" {
- // Check to see if the root is an existing file
- remote := path.Base(rootDirectory)
- f.rootDirectory = path.Dir(rootDirectory)
- if f.rootDirectory == "." {
- f.rootDirectory = ""
- }
- _, err := f.NewObject(ctx, remote)
- if err != nil {
- if errors.Is(err, fs.ErrorObjectNotFound) || errors.Is(err, fs.ErrorNotAFile) {
- // File doesn't exist so return the original f
- f.rootDirectory = rootDirectory
- return f, nil
- }
- return f, err
- }
- // Correct root if definitely pointing to a file
- f.root = path.Dir(f.root)
- if f.root == "." || f.root == "/" {
- f.root = ""
- }
- // return an error with an fs which points to the parent
- return f, fs.ErrorIsFile
- }
- return f, nil
- }
- // Config callback for 2FA
- func Config(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) {
- serverURL, ok := m.Get(configURL)
- if !ok || serverURL == "" {
- // If there's no server URL, it means we're trying an operation at the backend level, like a "rclone authorize seafile"
- 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>: ")
- }
- u, err := url.Parse(serverURL)
- if err != nil {
- return nil, fmt.Errorf("invalid server URL %s", serverURL)
- }
- is2faEnabled, _ := m.Get(config2FA)
- if is2faEnabled != "true" {
- // no need to do anything here
- return nil, nil
- }
- username, _ := m.Get(configUser)
- if username == "" {
- return nil, errors.New("a username is required")
- }
- password, _ := m.Get(configPassword)
- if password != "" {
- password, _ = obscure.Reveal(password)
- }
- switch config.State {
- case "":
- // Empty state means it's the first call to the Config function
- if password == "" {
- return fs.ConfigPassword("password", "config_password", "Two-factor authentication: please enter your password (it won't be saved in the configuration)")
- }
- // password was successfully loaded from the config
- return fs.ConfigGoto("2fa")
- case "password":
- // password should be coming from the previous state (entered by the user)
- password = config.Result
- if password == "" {
- return fs.ConfigError("", "Password can't be blank")
- }
- // save it into the configuration file and keep going
- m.Set(configPassword, obscure.MustObscure(password))
- return fs.ConfigGoto("2fa")
- case "2fa":
- return fs.ConfigInput("2fa_do", "config_2fa", "Two-factor authentication: please enter your 2FA code")
- case "2fa_do":
- code := config.Result
- if code == "" {
- return fs.ConfigError("2fa", "2FA codes can't be blank")
- }
- // Create rest client for getAuthorizationToken
- url := u.String()
- if !strings.HasPrefix(url, "/") {
- url += "/"
- }
- srv := rest.NewClient(fshttp.NewClient(ctx)).SetRoot(url)
- // We loop asking for a 2FA code
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- token, err := getAuthorizationToken(ctx, srv, username, password, code)
- if err != nil {
- return fs.ConfigConfirm("2fa_error", true, "config_retry", fmt.Sprintf("Authentication failed: %v\n\nTry Again?", err))
- }
- if token == "" {
- return fs.ConfigConfirm("2fa_error", true, "config_retry", "Authentication failed - no token returned.\n\nTry Again?")
- }
- // Let's save the token into the configuration
- m.Set(configAuthToken, token)
- // And delete any previous entry for password
- m.Set(configPassword, "")
- // And we're done here
- return nil, nil
- case "2fa_error":
- if config.Result == "true" {
- return fs.ConfigGoto("2fa")
- }
- return nil, errors.New("2fa authentication failed")
- }
- return nil, fmt.Errorf("unknown state %q", config.State)
- }
- // Shutdown the Fs
- func (f *Fs) Shutdown(ctx context.Context) error {
- if f.renew == nil {
- return nil
- }
- f.renew.Shutdown()
- return nil
- }
- // sets the AuthorizationToken up
- func (f *Fs) setAuthorizationToken(token string) {
- f.srv.SetHeader("Authorization", "Token "+token)
- }
- // authorizeAccount gets the auth token.
- func (f *Fs) authorizeAccount(ctx context.Context) (string, error) {
- f.authMu.Lock()
- defer f.authMu.Unlock()
- token, err := f.getAuthorizationToken(ctx)
- if err != nil {
- return "", err
- }
- return token, nil
- }
- // retryErrorCodes is a slice of error codes that we will retry
- var retryErrorCodes = []int{
- 408, // Request Timeout
- 429, // Rate exceeded.
- 500, // Get occasional 500 Internal Server Error
- 503, // Service Unavailable
- 504, // Gateway Time-out
- 520, // Operation failed (We get them sometimes when running tests in parallel)
- }
- // shouldRetry returns a boolean as to whether this resp and err
- // deserve to be retried. It returns the err as a convenience
- func (f *Fs) shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
- if fserrors.ContextError(ctx, &err) {
- return false, err
- }
- // For 429 errors look at the Retry-After: header and
- // set the retry appropriately, starting with a minimum of 1
- // second if it isn't set.
- if resp != nil && (resp.StatusCode == 429) {
- var retryAfter = 1
- retryAfterString := resp.Header.Get(retryAfterHeader)
- if retryAfterString != "" {
- var err error
- retryAfter, err = strconv.Atoi(retryAfterString)
- if err != nil {
- fs.Errorf(f, "Malformed %s header %q: %v", retryAfterHeader, retryAfterString, err)
- }
- }
- return true, pacer.RetryAfterError(err, time.Duration(retryAfter)*time.Second)
- }
- return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
- }
- func (f *Fs) shouldRetryUpload(ctx context.Context, resp *http.Response, err error) (bool, error) {
- if err != nil || (resp != nil && resp.StatusCode > 400) {
- return true, err
- }
- return false, nil
- }
- // Name of the remote (as passed into NewFs)
- func (f *Fs) Name() string {
- return f.name
- }
- // Root of the remote (as passed into NewFs)
- func (f *Fs) Root() string {
- return f.root
- }
- // String converts this Fs to a string
- func (f *Fs) String() string {
- if f.libraryName == "" {
- return "seafile root"
- }
- library := "library"
- if f.encrypted {
- library = "encrypted " + library
- }
- if f.rootDirectory == "" {
- return fmt.Sprintf("seafile %s '%s'", library, f.libraryName)
- }
- return fmt.Sprintf("seafile %s '%s' path '%s'", library, f.libraryName, f.rootDirectory)
- }
- // Precision of the ModTimes in this Fs
- func (f *Fs) Precision() time.Duration {
- // The API doesn't support setting the modified time
- return fs.ModTimeNotSupported
- }
- // Hashes returns the supported hash sets.
- func (f *Fs) Hashes() hash.Set {
- return hash.Set(hash.None)
- }
- // Features returns the optional features of this Fs
- func (f *Fs) Features() *fs.Features {
- return f.features
- }
- // List the objects and directories in dir into entries. The
- // entries can be returned in any order but should be for a
- // complete directory.
- //
- // dir should be "" to list the root, and should not have
- // trailing slashes.
- //
- // This should return fs.ErrorDirNotFound if the directory isn't
- // found.
- func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
- if dir == "" && f.libraryName == "" {
- return f.listLibraries(ctx)
- }
- return f.listDir(ctx, dir, false)
- }
- // NewObject finds the Object at remote. If it can't be found
- // it returns the error fs.ErrorObjectNotFound.
- func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
- libraryName, filePath := f.splitPath(remote)
- libraryID, err := f.getLibraryID(ctx, libraryName)
- if err != nil {
- return nil, err
- }
- err = f.authorizeLibrary(ctx, libraryID)
- if err != nil {
- return nil, err
- }
- fileDetails, err := f.getFileDetails(ctx, libraryID, filePath)
- if err != nil {
- return nil, err
- }
- modTime, err := time.Parse(time.RFC3339, fileDetails.Modified)
- if err != nil {
- fs.LogPrintf(fs.LogLevelWarning, fileDetails.Modified, "Cannot parse datetime")
- }
- o := &Object{
- fs: f,
- libraryID: libraryID,
- id: fileDetails.ID,
- remote: remote,
- pathInLibrary: filePath,
- modTime: modTime,
- size: fileDetails.Size,
- }
- return o, nil
- }
- // Put in to the remote path with the modTime given of the given size
- //
- // When called from outside an Fs by rclone, src.Size() will always be >= 0.
- // But for unknown-sized objects (indicated by src.Size() == -1), Put should either
- // return an error or upload it properly (rather than e.g. calling panic).
- //
- // May create the object even if it returns an error - if so
- // will return the object and the error, otherwise will return
- // nil and the error
- func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
- object := f.newObject(ctx, src.Remote(), src.Size(), src.ModTime(ctx))
- // Check if we need to create a new library at that point
- if object.libraryID == "" {
- library, _ := f.splitPath(object.remote)
- err := f.Mkdir(ctx, library)
- if err != nil {
- return object, err
- }
- libraryID, err := f.getLibraryID(ctx, library)
- if err != nil {
- return object, err
- }
- object.libraryID = libraryID
- }
- err := object.Update(ctx, in, src, options...)
- if err != nil {
- return object, err
- }
- return object, nil
- }
- // PutStream uploads to the remote path with the modTime given but of indeterminate size
- func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
- return f.Put(ctx, in, src, options...)
- }
- // Mkdir makes the directory or library
- //
- // Shouldn't return an error if it already exists
- func (f *Fs) Mkdir(ctx context.Context, dir string) error {
- libraryName, folder := f.splitPath(dir)
- if strings.HasPrefix(dir, libraryName) {
- err := f.mkLibrary(ctx, libraryName, "")
- if err != nil {
- return err
- }
- if folder == "" {
- // No directory to create after the library
- return nil
- }
- }
- err := f.mkDir(ctx, dir)
- if err != nil {
- return err
- }
- return nil
- }
- // purgeCheck removes the root directory, if check is set then it
- // refuses to do so if it has anything in
- func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
- libraryName, dirPath := f.splitPath(dir)
- libraryID, err := f.getLibraryID(ctx, libraryName)
- if err != nil {
- return err
- }
- if check {
- directoryEntries, err := f.getDirectoryEntries(ctx, libraryID, dirPath, false)
- if err != nil {
- return err
- }
- if len(directoryEntries) > 0 {
- return fs.ErrorDirectoryNotEmpty
- }
- }
- if dirPath == "" || dirPath == "/" {
- return f.deleteLibrary(ctx, libraryID)
- }
- return f.deleteDir(ctx, libraryID, dirPath)
- }
- // Rmdir removes the directory or library if empty
- //
- // Return an error if it doesn't exist or isn't empty
- func (f *Fs) Rmdir(ctx context.Context, dir string) error {
- return f.purgeCheck(ctx, dir, true)
- }
- // ==================== Optional Interface fs.ListRer ====================
- // ListR lists the objects and directories of the Fs starting
- // from dir recursively into out.
- //
- // dir should be "" to start from the root, and should not
- // have trailing slashes.
- //
- // This should return ErrDirNotFound if the directory isn't
- // found.
- //
- // It should call callback for each tranche of entries read.
- // These need not be returned in any particular order. If
- // callback returns an error then the listing will stop
- // immediately.
- func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) error {
- var err error
- if dir == "" && f.libraryName == "" {
- libraries, err := f.listLibraries(ctx)
- if err != nil {
- return err
- }
- // Send the library list as folders
- err = callback(libraries)
- if err != nil {
- return err
- }
- // Then list each library
- for _, library := range libraries {
- err = f.listDirCallback(ctx, library.Remote(), callback)
- if err != nil {
- return err
- }
- }
- return nil
- }
- err = f.listDirCallback(ctx, dir, callback)
- if err != nil {
- return err
- }
- return nil
- }
- // ==================== Optional Interface fs.Copier ====================
- // Copy src to this remote using server-side copy operations.
- //
- // This is stored with the remote path given.
- //
- // It returns the destination Object and a possible error.
- //
- // If it isn't possible then return fs.ErrorCantCopy
- func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
- srcObj, ok := src.(*Object)
- if !ok {
- return nil, fs.ErrorCantCopy
- }
- srcLibraryName, srcPath := srcObj.fs.splitPath(src.Remote())
- srcLibraryID, err := srcObj.fs.getLibraryID(ctx, srcLibraryName)
- if err != nil {
- return nil, err
- }
- dstLibraryName, dstPath := f.splitPath(remote)
- dstLibraryID, err := f.getLibraryID(ctx, dstLibraryName)
- if err != nil {
- return nil, err
- }
- // Seafile does not accept a file name as a destination, only a path.
- // The destination filename will be the same as the original, or with (1) added in case it was already existing
- dstDir, dstFilename := path.Split(dstPath)
- // We have to make sure the destination path exists on the server or it's going to bomb out with an obscure error message
- err = f.mkMultiDir(ctx, dstLibraryID, dstDir)
- if err != nil {
- return nil, err
- }
- op, err := f.copyFile(ctx, srcLibraryID, srcPath, dstLibraryID, dstDir)
- if err != nil {
- return nil, err
- }
- if op.Name != dstFilename {
- // Destination was existing, so we need to move the file back into place
- err = f.adjustDestination(ctx, dstLibraryID, op.Name, dstPath, dstDir, dstFilename)
- if err != nil {
- return nil, err
- }
- }
- // Create a new object from the result
- return f.NewObject(ctx, remote)
- }
- // ==================== Optional Interface fs.Mover ====================
- // Move src to this remote using server-side move operations.
- //
- // This is stored with the remote path given.
- //
- // It returns the destination Object and a possible error.
- //
- // If it isn't possible then return fs.ErrorCantMove
- func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
- srcObj, ok := src.(*Object)
- if !ok {
- return nil, fs.ErrorCantMove
- }
- srcLibraryName, srcPath := srcObj.fs.splitPath(src.Remote())
- srcLibraryID, err := srcObj.fs.getLibraryID(ctx, srcLibraryName)
- if err != nil {
- return nil, err
- }
- dstLibraryName, dstPath := f.splitPath(remote)
- dstLibraryID, err := f.getLibraryID(ctx, dstLibraryName)
- if err != nil {
- return nil, err
- }
- // anchor both source and destination paths from the root so we can compare them
- srcPath = path.Join("/", srcPath)
- dstPath = path.Join("/", dstPath)
- srcDir := path.Dir(srcPath)
- dstDir, dstFilename := path.Split(dstPath)
- if srcLibraryID == dstLibraryID && srcDir == dstDir {
- // It's only a simple case of renaming the file
- _, err := f.renameFile(ctx, srcLibraryID, srcPath, dstFilename)
- if err != nil {
- return nil, err
- }
- return f.NewObject(ctx, remote)
- }
- // We have to make sure the destination path exists on the server
- err = f.mkMultiDir(ctx, dstLibraryID, dstDir)
- if err != nil {
- return nil, err
- }
- // Seafile does not accept a file name as a destination, only a path.
- // The destination filename will be the same as the original, or with (1) added in case it already exists
- op, err := f.moveFile(ctx, srcLibraryID, srcPath, dstLibraryID, dstDir)
- if err != nil {
- return nil, err
- }
- if op.Name != dstFilename {
- // Destination was existing, so we need to move the file back into place
- err = f.adjustDestination(ctx, dstLibraryID, op.Name, dstPath, dstDir, dstFilename)
- if err != nil {
- return nil, err
- }
- }
- // Create a new object from the result
- return f.NewObject(ctx, remote)
- }
- // adjustDestination rename the file
- func (f *Fs) adjustDestination(ctx context.Context, libraryID, srcFilename, dstPath, dstDir, dstFilename string) error {
- // Seafile seems to be acting strangely if the renamed file already exists (some cache issue maybe?)
- // It's better to delete the destination if it already exists
- fileDetail, err := f.getFileDetails(ctx, libraryID, dstPath)
- if err != nil && err != fs.ErrorObjectNotFound {
- return err
- }
- if fileDetail != nil {
- err = f.deleteFile(ctx, libraryID, dstPath)
- if err != nil {
- return err
- }
- }
- _, err = f.renameFile(ctx, libraryID, path.Join(dstDir, srcFilename), dstFilename)
- if err != nil {
- return err
- }
- return nil
- }
- // ==================== Optional Interface fs.DirMover ====================
- // DirMove moves src, srcRemote to this remote at dstRemote
- // using server-side move operations.
- //
- // Will only be called if src.Fs().Name() == f.Name()
- //
- // If it isn't possible then return fs.ErrorCantDirMove
- //
- // If destination exists then return fs.ErrorDirExists
- func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
- // Cast into a seafile Fs
- srcFs, ok := src.(*Fs)
- if !ok {
- return fs.ErrorCantDirMove
- }
- srcLibraryName, srcPath := srcFs.splitPath(srcRemote)
- srcLibraryID, err := srcFs.getLibraryID(ctx, srcLibraryName)
- if err != nil {
- return err
- }
- dstLibraryName, dstPath := f.splitPath(dstRemote)
- dstLibraryID, err := f.getLibraryID(ctx, dstLibraryName)
- if err != nil {
- return err
- }
- srcDir := path.Dir(srcPath)
- dstDir, dstName := path.Split(dstPath)
- // anchor both source and destination to the root so we can compare them
- srcDir = path.Join("/", srcDir)
- dstDir = path.Join("/", dstDir)
- // The destination should not exist
- entries, err := f.getDirectoryEntries(ctx, dstLibraryID, dstDir, false)
- if err != nil && err != fs.ErrorDirNotFound {
- return err
- }
- if err == nil {
- for _, entry := range entries {
- if entry.Name == dstName {
- // Destination exists
- return fs.ErrorDirExists
- }
- }
- }
- if srcLibraryID == dstLibraryID && srcDir == dstDir {
- // It's only renaming
- err = srcFs.renameDir(ctx, dstLibraryID, srcPath, dstName)
- if err != nil {
- return err
- }
- return nil
- }
- // Seafile < 7 does not support moving directories
- if f.moveDirNotAvailable {
- return fs.ErrorCantDirMove
- }
- // Make sure the destination path exists
- err = f.mkMultiDir(ctx, dstLibraryID, dstDir)
- if err != nil {
- return err
- }
- // If the destination already exists, seafile will add a " (n)" to the name.
- // Sadly this API call will not return the new given name like the move file version does
- // So the trick is to rename the directory to something random before moving it
- // After the move we rename the random name back to the expected one
- // Hopefully there won't be anything with the same name existing at destination ;)
- tempName := ".rclone-move-" + random.String(32)
- // 1- rename source
- err = srcFs.renameDir(ctx, srcLibraryID, srcPath, tempName)
- if err != nil {
- return fmt.Errorf("cannot rename source directory to a temporary name: %w", err)
- }
- // 2- move source to destination
- err = f.moveDir(ctx, srcLibraryID, srcDir, tempName, dstLibraryID, dstDir)
- if err != nil {
- // Doh! Let's rename the source back to its original name
- _ = srcFs.renameDir(ctx, srcLibraryID, path.Join(srcDir, tempName), path.Base(srcPath))
- return err
- }
- // 3- rename destination back to source name
- err = f.renameDir(ctx, dstLibraryID, path.Join(dstDir, tempName), dstName)
- if err != nil {
- return fmt.Errorf("cannot rename temporary directory to destination name: %w", err)
- }
- return nil
- }
- // ==================== Optional Interface fs.Purger ====================
- // Purge all files in the directory
- //
- // Implement this if you have a way of deleting all the files
- // quicker than just running Remove() on the result of List()
- //
- // Return an error if it doesn't exist
- func (f *Fs) Purge(ctx context.Context, dir string) error {
- return f.purgeCheck(ctx, dir, false)
- }
- // ==================== Optional Interface fs.CleanUpper ====================
- // CleanUp the trash in the Fs
- func (f *Fs) CleanUp(ctx context.Context) error {
- if f.libraryName == "" {
- return errors.New("cannot clean up at the root of the seafile server, please select a library to clean up")
- }
- libraryID, err := f.getLibraryID(ctx, f.libraryName)
- if err != nil {
- return err
- }
- return f.emptyLibraryTrash(ctx, libraryID)
- }
- // ==================== Optional Interface fs.Abouter ====================
- // About gets quota information
- func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
- accountInfo, err := f.getUserAccountInfo(ctx)
- if err != nil {
- return nil, err
- }
- usage = &fs.Usage{
- Used: fs.NewUsageValue(accountInfo.Usage), // bytes in use
- }
- if accountInfo.Total > 0 {
- usage.Total = fs.NewUsageValue(accountInfo.Total) // quota of bytes that can be used
- usage.Free = fs.NewUsageValue(accountInfo.Total - accountInfo.Usage) // bytes which can be uploaded before reaching the quota
- }
- return usage, nil
- }
- // ==================== Optional Interface fs.UserInfoer ====================
- // UserInfo returns info about the connected user
- func (f *Fs) UserInfo(ctx context.Context) (map[string]string, error) {
- accountInfo, err := f.getUserAccountInfo(ctx)
- if err != nil {
- return nil, err
- }
- return map[string]string{
- "Name": accountInfo.Name,
- "Email": accountInfo.Email,
- }, nil
- }
- // ==================== Optional Interface fs.PublicLinker ====================
- // PublicLink generates a public link to the remote path (usually readable by anyone)
- func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
- libraryName, filePath := f.splitPath(remote)
- if libraryName == "" {
- // We cannot share the whole seafile server, we need at least a library
- return "", errors.New("cannot share the root of the seafile server, please select a library to share")
- }
- libraryID, err := f.getLibraryID(ctx, libraryName)
- if err != nil {
- return "", err
- }
- // List existing links first
- shareLinks, err := f.listShareLinks(ctx, libraryID, filePath)
- if err != nil {
- return "", err
- }
- if len(shareLinks) > 0 {
- for _, shareLink := range shareLinks {
- if !shareLink.IsExpired {
- return shareLink.Link, nil
- }
- }
- }
- // No link was found
- shareLink, err := f.createShareLink(ctx, libraryID, filePath)
- if err != nil {
- return "", err
- }
- if shareLink.IsExpired {
- return "", nil
- }
- return shareLink.Link, nil
- }
- func (f *Fs) listLibraries(ctx context.Context) (entries fs.DirEntries, err error) {
- libraries, err := f.getCachedLibraries(ctx)
- if err != nil {
- return nil, errors.New("cannot load libraries")
- }
- for _, library := range libraries {
- d := fs.NewDir(library.Name, time.Unix(library.Modified, 0))
- d.SetSize(library.Size)
- entries = append(entries, d)
- }
- return entries, nil
- }
- func (f *Fs) libraryExists(ctx context.Context, libraryName string) (bool, error) {
- libraries, err := f.getCachedLibraries(ctx)
- if err != nil {
- return false, err
- }
- for _, library := range libraries {
- if library.Name == libraryName {
- return true, nil
- }
- }
- return false, nil
- }
- func (f *Fs) getLibraryID(ctx context.Context, name string) (string, error) {
- libraries, err := f.getCachedLibraries(ctx)
- if err != nil {
- return "", err
- }
- for _, library := range libraries {
- if library.Name == name {
- return library.ID, nil
- }
- }
- return "", fmt.Errorf("cannot find library '%s'", name)
- }
- func (f *Fs) isLibraryInCache(libraryName string) bool {
- f.librariesMutex.Lock()
- defer f.librariesMutex.Unlock()
- if f.libraries == nil {
- return false
- }
- value, found := f.libraries.GetMaybe(librariesCacheKey)
- if !found {
- return false
- }
- libraries := value.([]api.Library)
- for _, library := range libraries {
- if library.Name == libraryName {
- return true
- }
- }
- return false
- }
- func (f *Fs) isEncrypted(ctx context.Context, libraryID string) (bool, error) {
- libraries, err := f.getCachedLibraries(ctx)
- if err != nil {
- return false, err
- }
- for _, library := range libraries {
- if library.ID == libraryID {
- return library.Encrypted, nil
- }
- }
- return false, fmt.Errorf("cannot find library ID %s", libraryID)
- }
- func (f *Fs) authorizeLibrary(ctx context.Context, libraryID string) error {
- if libraryID == "" {
- return errors.New("a library ID is needed")
- }
- if f.opt.LibraryKey == "" {
- // We have no password to send
- return nil
- }
- encrypted, err := f.isEncrypted(ctx, libraryID)
- if err != nil {
- return err
- }
- if encrypted {
- fs.Debugf(nil, "Decrypting library %s", libraryID)
- f.authMu.Lock()
- defer f.authMu.Unlock()
- err := f.decryptLibrary(ctx, libraryID, f.opt.LibraryKey)
- if err != nil {
- return err
- }
- }
- return nil
- }
- func (f *Fs) mkLibrary(ctx context.Context, libraryName, password string) error {
- // lock specific to library creation
- // we cannot reuse the same lock as we will dead-lock ourself if the libraries are not in cache
- createLibraryMutex.Lock()
- defer createLibraryMutex.Unlock()
- if libraryName == "" {
- return errors.New("a library name is needed")
- }
- // It's quite likely that multiple go routines are going to try creating the same library
- // at the start of a sync/copy. After releasing the mutex the calls waiting would try to create
- // the same library again. So we'd better check the library exists first
- if f.isLibraryInCache(libraryName) {
- return nil
- }
- fs.Debugf(nil, "%s: Create library '%s'", f.Name(), libraryName)
- f.librariesMutex.Lock()
- defer f.librariesMutex.Unlock()
- library, err := f.createLibrary(ctx, libraryName, password)
- if err != nil {
- return err
- }
- // Stores the library details into the cache
- value, found := f.libraries.GetMaybe(librariesCacheKey)
- if !found {
- // Don't update the cache at that point
- return nil
- }
- libraries := value.([]api.Library)
- libraries = append(libraries, api.Library{
- ID: library.ID,
- Name: library.Name,
- })
- f.libraries.Put(librariesCacheKey, libraries)
- return nil
- }
- // splitPath returns the library name and the full path inside the library
- func (f *Fs) splitPath(dir string) (library, folder string) {
- library = f.libraryName
- folder = dir
- if library == "" {
- // The first part of the path is the library
- library, folder = bucket.Split(dir)
- } else if f.rootDirectory != "" {
- // Adds the root folder to the path to get a full path
- folder = path.Join(f.rootDirectory, folder)
- }
- return
- }
- func (f *Fs) listDir(ctx context.Context, dir string, recursive bool) (entries fs.DirEntries, err error) {
- libraryName, dirPath := f.splitPath(dir)
- libraryID, err := f.getLibraryID(ctx, libraryName)
- if err != nil {
- return nil, err
- }
- directoryEntries, err := f.getDirectoryEntries(ctx, libraryID, dirPath, recursive)
- if err != nil {
- return nil, err
- }
- return f.buildDirEntries(dir, libraryID, dirPath, directoryEntries, recursive), nil
- }
- // listDirCallback is calling listDir with the recursive option and is sending the result to the callback
- func (f *Fs) listDirCallback(ctx context.Context, dir string, callback fs.ListRCallback) error {
- entries, err := f.listDir(ctx, dir, true)
- if err != nil {
- return err
- }
- err = callback(entries)
- if err != nil {
- return err
- }
- return nil
- }
- func (f *Fs) buildDirEntries(parentPath, libraryID, parentPathInLibrary string, directoryEntries []api.DirEntry, recursive bool) (entries fs.DirEntries) {
- for _, entry := range directoryEntries {
- var filePath, filePathInLibrary string
- if recursive {
- // In recursive mode, paths are built from DirEntry (+ a starting point)
- entryPath := strings.TrimPrefix(entry.Path, "/")
- // If we're listing from some path inside the library (not the root)
- // there's already a path in parameter, which will also be included in the entry path
- entryPath = strings.TrimPrefix(entryPath, parentPathInLibrary)
- entryPath = strings.TrimPrefix(entryPath, "/")
- filePath = path.Join(parentPath, entryPath, entry.Name)
- filePathInLibrary = path.Join(parentPathInLibrary, entryPath, entry.Name)
- } else {
- // In non-recursive mode, paths are build from the parameters
- filePath = path.Join(parentPath, entry.Name)
- filePathInLibrary = path.Join(parentPathInLibrary, entry.Name)
- }
- if entry.Type == api.FileTypeDir {
- d := fs.
- NewDir(filePath, time.Unix(entry.Modified, 0)).
- SetSize(entry.Size).
- SetID(entry.ID)
- entries = append(entries, d)
- } else if entry.Type == api.FileTypeFile {
- object := &Object{
- fs: f,
- id: entry.ID,
- remote: filePath,
- pathInLibrary: filePathInLibrary,
- size: entry.Size,
- modTime: time.Unix(entry.Modified, 0),
- libraryID: libraryID,
- }
- entries = append(entries, object)
- }
- }
- return entries
- }
- func (f *Fs) mkDir(ctx context.Context, dir string) error {
- library, fullPath := f.splitPath(dir)
- libraryID, err := f.getLibraryID(ctx, library)
- if err != nil {
- return err
- }
- return f.mkMultiDir(ctx, libraryID, fullPath)
- }
- func (f *Fs) mkMultiDir(ctx context.Context, libraryID, dir string) error {
- // rebuild the path one by one
- currentPath := ""
- for _, singleDir := range splitPath(dir) {
- currentPath = path.Join(currentPath, singleDir)
- err := f.mkSingleDir(ctx, libraryID, currentPath)
- if err != nil {
- return err
- }
- }
- return nil
- }
- func (f *Fs) mkSingleDir(ctx context.Context, libraryID, dir string) error {
- f.createDirMutex.Lock()
- defer f.createDirMutex.Unlock()
- dirDetails, err := f.getDirectoryDetails(ctx, libraryID, dir)
- if err == nil && dirDetails != nil {
- // Don't fail if the directory exists
- return nil
- }
- if err == fs.ErrorDirNotFound {
- err = f.createDir(ctx, libraryID, dir)
- if err != nil {
- return err
- }
- return nil
- }
- return err
- }
- func (f *Fs) getDirectoryEntries(ctx context.Context, libraryID, folder string, recursive bool) ([]api.DirEntry, error) {
- if f.useOldDirectoryAPI {
- return f.getDirectoryEntriesAPIv2(ctx, libraryID, folder)
- }
- return f.getDirectoryEntriesAPIv21(ctx, libraryID, folder, recursive)
- }
- // splitPath creates a slice of paths
- func splitPath(tree string) (paths []string) {
- tree, leaf := path.Split(path.Clean(tree))
- for leaf != "" && leaf != "." {
- paths = append([]string{leaf}, paths...)
- tree, leaf = path.Split(path.Clean(tree))
- }
- return
- }
- func (f *Fs) getCachedLibraries(ctx context.Context) ([]api.Library, error) {
- f.librariesMutex.Lock()
- defer f.librariesMutex.Unlock()
- libraries, err := f.libraries.Get(librariesCacheKey, func(key string) (value interface{}, ok bool, error error) {
- // Load the libraries if not present in the cache
- libraries, err := f.getLibraries(ctx)
- if err != nil {
- return nil, false, err
- }
- return libraries, true, nil
- })
- if err != nil {
- return nil, err
- }
- // Type assertion
- return libraries.([]api.Library), nil
- }
- func (f *Fs) newObject(ctx context.Context, remote string, size int64, modTime time.Time) *Object {
- libraryName, remotePath := f.splitPath(remote)
- libraryID, _ := f.getLibraryID(ctx, libraryName) // If error it means the library does not exist (yet)
- object := &Object{
- fs: f,
- remote: remote,
- libraryID: libraryID,
- pathInLibrary: remotePath,
- size: size,
- modTime: modTime,
- }
- return object
- }
- // Check the interfaces are satisfied
- var (
- _ fs.Fs = &Fs{}
- _ fs.Abouter = &Fs{}
- _ fs.CleanUpper = &Fs{}
- _ fs.Copier = &Fs{}
- _ fs.Mover = &Fs{}
- _ fs.DirMover = &Fs{}
- _ fs.ListRer = &Fs{}
- _ fs.Purger = &Fs{}
- _ fs.PutStreamer = &Fs{}
- _ fs.PublicLinker = &Fs{}
- _ fs.UserInfoer = &Fs{}
- _ fs.Shutdowner = &Fs{}
- _ fs.Object = &Object{}
- _ fs.IDer = &Object{}
- )
|