rclone/backend/seafile/seafile.go
Nick Craig-Wood 339d3e8ee6 netstorage,quatrix,seafile: fix Root to return correct directory when pointing to a file
This fixes the TestIntegration/FsMkdir/FsPutFiles/FsIsFile/FsRoot
integration test.
2024-03-07 14:44:45 +00:00

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{}
)