339d3e8ee6
This fixes the TestIntegration/FsMkdir/FsPutFiles/FsIsFile/FsRoot integration test.
1362 lines
39 KiB
Go
1362 lines
39 KiB
Go
// 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{}
|
|
)
|