seafile: implement 2FA

This commit is contained in:
Fred 2020-05-18 22:56:54 +01:00 committed by Nick Craig-Wood
parent 56c9fdb53c
commit 5f71d186b2
3 changed files with 329 additions and 111 deletions

View File

@ -32,8 +32,16 @@ import (
) )
const ( const (
librariesCacheKey = "all" librariesCacheKey = "all"
retryAfterHeader = "Retry-After" 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 // This is global to all instances of fs
@ -49,8 +57,9 @@ func init() {
Name: "seafile", Name: "seafile",
Description: "seafile", Description: "seafile",
NewFs: NewFs, NewFs: NewFs,
Config: Config,
Options: []fs.Option{{ Options: []fs.Option{{
Name: "url", Name: configURL,
Help: "URL of seafile host to connect to", Help: "URL of seafile host to connect to",
Required: true, Required: true,
Examples: []fs.OptionExample{{ Examples: []fs.OptionExample{{
@ -58,26 +67,35 @@ func init() {
Help: "Connect to cloud.seafile.com", Help: "Connect to cloud.seafile.com",
}}, }},
}, { }, {
Name: "user", Name: configUser,
Help: "User name", Help: "User name (usually email address)",
Required: true, Required: true,
}, { }, {
Name: "pass", // Password is not required, it will be left blank for 2FA
Name: configPassword,
Help: "Password", Help: "Password",
IsPassword: true, IsPassword: true,
Required: true,
}, { }, {
Name: "library", Name: config2FA,
Help: "Two-factor authentication ('true' if the account has 2FA enabled)",
Default: false,
}, {
Name: configLibrary,
Help: "Name of the library. Leave blank to access all non-encrypted libraries.", Help: "Name of the library. Leave blank to access all non-encrypted libraries.",
}, { }, {
Name: "library_key", Name: configLibraryKey,
Help: "Library password (for encrypted libraries only). Leave blank if you pass it through the command line.", Help: "Library password (for encrypted libraries only). Leave blank if you pass it through the command line.",
IsPassword: true, IsPassword: true,
}, { }, {
Name: "create_library", Name: configCreateLibrary,
Help: "Should create library if it doesn't exist", Help: "Should rclone create a library if it doesn't exist",
Advanced: true, Advanced: true,
Default: false, Default: false,
}, {
// Keep the authentication token after entering the 2FA code
Name: configAuthToken,
Help: "Authentication token",
Hide: fs.OptionHideBoth,
}, { }, {
Name: config.ConfigEncoding, Name: config.ConfigEncoding,
Help: config.ConfigEncodingHelp, Help: config.ConfigEncodingHelp,
@ -97,6 +115,8 @@ type Options struct {
URL string `config:"url"` URL string `config:"url"`
User string `config:"user"` User string `config:"user"`
Password string `config:"pass"` Password string `config:"pass"`
Is2FA bool `config:"2fa"`
AuthToken string `config:"auth_token"`
LibraryName string `config:"library"` LibraryName string `config:"library"`
LibraryKey string `config:"library_key"` LibraryKey string `config:"library_key"`
CreateLibrary bool `config:"create_library"` CreateLibrary bool `config:"create_library"`
@ -205,10 +225,16 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
f.moveDirNotAvailable = true f.moveDirNotAvailable = true
} }
err = f.authorizeAccount(ctx) // Take the authentication token from the configuration first
if err != nil { token := f.opt.AuthToken
return nil, err 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 != "" { if f.libraryName != "" {
// Check if the library exists // Check if the library exists
@ -270,26 +296,108 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
return f, nil return f, nil
} }
// Config callback for 2FA
func Config(name string, m configmap.Mapper) {
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"
fmt.Print("\nOperation not supported on this remote.\nIf you need a 2FA code on your account, use the command:\n\nrclone config reconnect <remote name>:\n\n")
return
}
// Stop if we are running non-interactive config
if fs.Config.AutoConfirm {
return
}
u, err := url.Parse(serverURL)
if err != nil {
fs.Errorf(nil, "Invalid server URL %s", serverURL)
return
}
is2faEnabled, _ := m.Get(config2FA)
if is2faEnabled != "true" {
fmt.Println("Two-factor authentication is not enabled on this account.")
return
}
username, _ := m.Get(configUser)
if username == "" {
fs.Errorf(nil, "A username is required")
return
}
password, _ := m.Get(configPassword)
if password != "" {
password, _ = obscure.Reveal(password)
}
// Just make sure we do have a password
for password == "" {
fmt.Print("Two-factor authentication: please enter your password (it won't be saved in the configuration)\npassword> ")
password = config.ReadPassword()
}
// Create rest client for getAuthorizationToken
url := u.String()
if !strings.HasPrefix(url, "/") {
url += "/"
}
srv := rest.NewClient(fshttp.NewClient(fs.Config)).SetRoot(url)
// We loop asking for a 2FA code
for {
code := ""
for code == "" {
fmt.Print("Two-factor authentication: please enter your 2FA code\n2fa code> ")
code = config.ReadLine()
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
fmt.Println("Authenticating...")
token, err := getAuthorizationToken(ctx, srv, username, password, code)
if err != nil {
fmt.Printf("Authentication failed: %v\n", err)
tryAgain := strings.ToLower(config.ReadNonEmptyLine("Do you want to try again (y/n)?"))
if tryAgain != "y" && tryAgain != "yes" {
// The user is giving up, we're done here
break
}
}
if token != "" {
fmt.Println("Success!")
// Let's save the token into the configuration
m.Set(configAuthToken, token)
// And delete any previous entry for password
m.Set(configPassword, "")
config.SaveConfig()
// And we're done here
break
}
}
}
// sets the AuthorizationToken up // sets the AuthorizationToken up
func (f *Fs) setAuthorizationToken(token string) { func (f *Fs) setAuthorizationToken(token string) {
f.srv.SetHeader("Authorization", "Token "+token) f.srv.SetHeader("Authorization", "Token "+token)
} }
// authorizeAccount gets the auth token. // authorizeAccount gets the auth token.
func (f *Fs) authorizeAccount(ctx context.Context) error { func (f *Fs) authorizeAccount(ctx context.Context) (string, error) {
f.authMu.Lock() f.authMu.Lock()
defer f.authMu.Unlock() defer f.authMu.Unlock()
token, err := f.getAuthorizationToken(ctx) token, err := f.getAuthorizationToken(ctx)
if err != nil { if err != nil {
return err return "", err
} }
f.setAuthorizationToken(token) return token, nil
return nil
} }
// retryErrorCodes is a slice of error codes that we will retry // retryErrorCodes is a slice of error codes that we will retry
var retryErrorCodes = []int{ var retryErrorCodes = []int{
401, // Unauthorized (eg "Token has expired")
408, // Request Timeout 408, // Request Timeout
429, // Rate exceeded. 429, // Rate exceeded.
500, // Get occasional 500 Internal Server Error 500, // Get occasional 500 Internal Server Error
@ -298,9 +406,9 @@ var retryErrorCodes = []int{
520, // Operation failed (We get them sometimes when running tests in parallel) 520, // Operation failed (We get them sometimes when running tests in parallel)
} }
// shouldRetryNoAuth returns a boolean as to whether this resp and err // shouldRetry returns a boolean as to whether this resp and err
// deserve to be retried. It returns the err as a convenience // deserve to be retried. It returns the err as a convenience
func (f *Fs) shouldRetryNoReauth(resp *http.Response, err error) (bool, error) { func (f *Fs) shouldRetry(resp *http.Response, err error) (bool, error) {
// For 429 errors look at the Retry-After: header and // For 429 errors look at the Retry-After: header and
// set the retry appropriately, starting with a minimum of 1 // set the retry appropriately, starting with a minimum of 1
// second if it isn't set. // second if it isn't set.
@ -319,22 +427,6 @@ func (f *Fs) shouldRetryNoReauth(resp *http.Response, err error) (bool, error) {
return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
} }
// 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) {
// It looks like seafile is using the 403 error code instead of the standard 401.
if resp != nil && (resp.StatusCode == 401 || resp.StatusCode == 403) {
fs.Debugf(f, "Unauthorized: %v", err)
// Reauth
authErr := f.authorizeAccount(ctx)
if authErr != nil {
err = authErr
}
return true, err
}
return f.shouldRetryNoReauth(resp, err)
}
func (f *Fs) shouldRetryUpload(ctx context.Context, resp *http.Response, err error) (bool, error) { func (f *Fs) shouldRetryUpload(ctx context.Context, resp *http.Response, err error) (bool, error) {
if err != nil || (resp != nil && resp.StatusCode > 400) { if err != nil || (resp != nil && resp.StatusCode > 400) {
return true, err return true, err

View File

@ -32,37 +32,44 @@ var (
// ==================== Seafile API ==================== // ==================== Seafile API ====================
func (f *Fs) getAuthorizationToken(ctx context.Context) (string, error) { func (f *Fs) getAuthorizationToken(ctx context.Context) (string, error) {
// API Socumentation return getAuthorizationToken(ctx, f.srv, f.opt.User, f.opt.Password, "")
}
// getAuthorizationToken can be called outside of a fs (during configuration of the remote to get the authentication token)
// it's doing a single call (no pacer involved)
func getAuthorizationToken(ctx context.Context, srv *rest.Client, user, password, oneTimeCode string) (string, error) {
// API Documentation
// https://download.seafile.com/published/web-api/home.md#user-content-Quick%20Start // https://download.seafile.com/published/web-api/home.md#user-content-Quick%20Start
opts := rest.Opts{ opts := rest.Opts{
Method: "POST", Method: "POST",
Path: "api2/auth-token/", Path: "api2/auth-token/",
ExtraHeaders: map[string]string{"Authorization": ""}, // unset the Authorization for this request ExtraHeaders: map[string]string{"Authorization": ""}, // unset the Authorization for this request
IgnoreStatus: true, // so we can load the error messages back into result
}
// 2FA
if oneTimeCode != "" {
opts.ExtraHeaders["X-SEAFILE-OTP"] = oneTimeCode
} }
request := api.AuthenticationRequest{ request := api.AuthenticationRequest{
Username: f.opt.User, Username: user,
Password: f.opt.Password, Password: password,
} }
result := api.AuthenticationResult{} result := api.AuthenticationResult{}
var resp *http.Response _, err := srv.CallJSON(ctx, &opts, &request, &result)
var err error
err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
return f.shouldRetryNoReauth(resp, err)
})
if err != nil { if err != nil {
if resp != nil { // This is only going to be http errors here
if resp.StatusCode == 403 {
return "", fs.ErrorPermissionDenied
}
}
return "", errors.Wrap(err, "failed to authenticate") return "", errors.Wrap(err, "failed to authenticate")
} }
if result.Errors != nil && len(result.Errors) > 1 { if result.Errors != nil && len(result.Errors) > 0 {
return "", errors.New(strings.Join(result.Errors, ", ")) return "", errors.New(strings.Join(result.Errors, ", "))
} }
if result.Token == "" {
// No error in "non_field_errors" field but still empty token
return "", errors.New("failed to authenticate")
}
return result.Token, nil return result.Token, nil
} }
@ -79,11 +86,11 @@ func (f *Fs) getServerInfo(ctx context.Context) (account *api.ServerInfo, err er
var resp *http.Response var resp *http.Response
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied return nil, fs.ErrorPermissionDenied
} }
} }
@ -105,11 +112,11 @@ func (f *Fs) getUserAccountInfo(ctx context.Context) (account *api.AccountInfo,
var resp *http.Response var resp *http.Response
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied return nil, fs.ErrorPermissionDenied
} }
} }
@ -132,11 +139,11 @@ func (f *Fs) getLibraries(ctx context.Context) ([]api.Library, error) {
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied return nil, fs.ErrorPermissionDenied
} }
} }
@ -163,11 +170,11 @@ func (f *Fs) createLibrary(ctx context.Context, libraryName, password string) (l
var resp *http.Response var resp *http.Response
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result) resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied return nil, fs.ErrorPermissionDenied
} }
} }
@ -190,11 +197,11 @@ func (f *Fs) deleteLibrary(ctx context.Context, libraryID string) error {
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied return fs.ErrorPermissionDenied
} }
} }
@ -221,7 +228,7 @@ func (f *Fs) decryptLibrary(ctx context.Context, libraryID, password string) err
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.Call(ctx, &opts) resp, err = f.srv.Call(ctx, &opts)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
@ -264,10 +271,13 @@ func (f *Fs) getDirectoryEntriesAPIv21(ctx context.Context, libraryID, dirPath s
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied
}
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
return nil, fs.ErrorDirNotFound return nil, fs.ErrorDirNotFound
} }
@ -306,11 +316,11 @@ func (f *Fs) getDirectoryDetails(ctx context.Context, libraryID, dirPath string)
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied return nil, fs.ErrorPermissionDenied
} }
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
@ -348,11 +358,11 @@ func (f *Fs) createDir(ctx context.Context, libraryID, dirPath string) error {
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.Call(ctx, &opts) resp, err = f.srv.Call(ctx, &opts)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied return fs.ErrorPermissionDenied
} }
} }
@ -388,11 +398,11 @@ func (f *Fs) renameDir(ctx context.Context, libraryID, dirPath, newName string)
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.Call(ctx, &opts) resp, err = f.srv.Call(ctx, &opts)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied return fs.ErrorPermissionDenied
} }
} }
@ -428,11 +438,11 @@ func (f *Fs) moveDir(ctx context.Context, srcLibraryID, srcDir, srcName, dstLibr
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, &request, nil) resp, err = f.srv.CallJSON(ctx, &opts, &request, nil)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied return fs.ErrorPermissionDenied
} }
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
@ -464,11 +474,11 @@ func (f *Fs) deleteDir(ctx context.Context, libraryID, filePath string) error {
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, nil) resp, err = f.srv.CallJSON(ctx, &opts, nil, nil)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied return fs.ErrorPermissionDenied
} }
} }
@ -495,14 +505,14 @@ func (f *Fs) getFileDetails(ctx context.Context, libraryID, filePath string) (*a
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
return nil, fs.ErrorObjectNotFound return nil, fs.ErrorObjectNotFound
} }
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied return nil, fs.ErrorPermissionDenied
} }
} }
@ -529,7 +539,7 @@ func (f *Fs) deleteFile(ctx context.Context, libraryID, filePath string) error {
} }
err := f.pacer.Call(func() (bool, error) { err := f.pacer.Call(func() (bool, error) {
resp, err := f.srv.CallJSON(ctx, &opts, nil, nil) resp, err := f.srv.CallJSON(ctx, &opts, nil, nil)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
return errors.Wrap(err, "failed to delete file") return errors.Wrap(err, "failed to delete file")
@ -555,7 +565,7 @@ func (f *Fs) getDownloadLink(ctx context.Context, libraryID, filePath string) (s
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
@ -604,7 +614,7 @@ func (f *Fs) download(ctx context.Context, url string, size int64, options ...fs
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.Call(ctx, &opts) resp, err = f.srv.Call(ctx, &opts)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
@ -649,11 +659,11 @@ func (f *Fs) getUploadLink(ctx context.Context, libraryID string) (string, error
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return "", fs.ErrorPermissionDenied return "", fs.ErrorPermissionDenied
} }
} }
@ -693,7 +703,7 @@ func (f *Fs) upload(ctx context.Context, in io.Reader, uploadLink, filePath stri
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied return nil, fs.ErrorPermissionDenied
} }
if resp.StatusCode == 500 { if resp.StatusCode == 500 {
@ -729,11 +739,11 @@ func (f *Fs) listShareLinks(ctx context.Context, libraryID, remote string) ([]ap
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied return nil, fs.ErrorPermissionDenied
} }
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
@ -767,11 +777,11 @@ func (f *Fs) createShareLink(ctx context.Context, libraryID, remote string) (*ap
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result) resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied return nil, fs.ErrorPermissionDenied
} }
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
@ -808,11 +818,11 @@ func (f *Fs) copyFile(ctx context.Context, srcLibraryID, srcPath, dstLibraryID,
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result) resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied return nil, fs.ErrorPermissionDenied
} }
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
@ -850,11 +860,11 @@ func (f *Fs) moveFile(ctx context.Context, srcLibraryID, srcPath, dstLibraryID,
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result) resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied return nil, fs.ErrorPermissionDenied
} }
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
@ -890,11 +900,11 @@ func (f *Fs) renameFile(ctx context.Context, libraryID, filePath, newname string
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result) resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied return nil, fs.ErrorPermissionDenied
} }
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
@ -928,11 +938,11 @@ func (f *Fs) emptyLibraryTrash(ctx context.Context, libraryID string) error {
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, nil) resp, err = f.srv.CallJSON(ctx, &opts, nil, nil)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied return fs.ErrorPermissionDenied
} }
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
@ -966,10 +976,13 @@ func (f *Fs) getDirectoryEntriesAPIv2(ctx context.Context, libraryID, dirPath st
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied
}
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
return nil, fs.ErrorDirNotFound return nil, fs.ErrorDirNotFound
} }
@ -1017,11 +1030,11 @@ func (f *Fs) copyFileAPIv2(ctx context.Context, srcLibraryID, srcPath, dstLibrar
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.Call(ctx, &opts) resp, err = f.srv.Call(ctx, &opts)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied return nil, fs.ErrorPermissionDenied
} }
} }
@ -1062,7 +1075,7 @@ func (f *Fs) renameFileAPIv2(ctx context.Context, libraryID, filePath, newname s
var err error var err error
err = f.pacer.Call(func() (bool, error) { err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.Call(ctx, &opts) resp, err = f.srv.Call(ctx, &opts)
return f.shouldRetry(ctx, resp, err) return f.shouldRetry(resp, err)
}) })
if err != nil { if err != nil {
if resp != nil { if resp != nil {
@ -1070,7 +1083,7 @@ func (f *Fs) renameFileAPIv2(ctx context.Context, libraryID, filePath, newname s
// This is the normal response from the server // This is the normal response from the server
return nil return nil
} }
if resp.StatusCode == 403 { if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied return fs.ErrorPermissionDenied
} }
if resp.StatusCode == 404 { if resp.StatusCode == 404 {

View File

@ -1,16 +1,17 @@
--- ---
title: "Seafile" title: "Seafile"
description: "Seafile" description: "Seafile"
date: "2020-05-02" date: "2020-05-19"
--- ---
<i class="fa fa-server"></i>Seafile <i class="fa fa-server"></i>Seafile
---------------------------------------- ----------------------------------------
This is a backend for the [Seafile](https://www.seafile.com/) storage service. This is a backend for the [Seafile](https://www.seafile.com/) storage service:
It works with both the free community edition, or the professional edition. - It works with both the free community edition or the professional edition.
Seafile versions 6.x and 7.x are all supported. - Seafile versions 6.x and 7.x are all supported.
Encrypted libraries are also supported. - Encrypted libraries are also supported.
- It supports 2FA enabled users
### Root mode vs Library mode ### ### Root mode vs Library mode ###
@ -18,11 +19,11 @@ There are two distinct modes you can setup your remote:
- you point your remote to the **root of the server**, meaning you don't specify a library during the configuration: - you point your remote to the **root of the server**, meaning you don't specify a library during the configuration:
Paths are specified as `remote:library`. You may put subdirectories in too, eg `remote:library/path/to/dir`. Paths are specified as `remote:library`. You may put subdirectories in too, eg `remote:library/path/to/dir`.
- you point your remote to a specific library during the configuration: - you point your remote to a specific library during the configuration:
Paths are specified as `remote:path/to/dir`. **This is the recommended mode when using encrypted libraries**. Paths are specified as `remote:path/to/dir`. **This is the recommended mode when using encrypted libraries**. (_This mode is possibly slightly faster than the root mode_)
### Configuration in root mode ### ### Configuration in root mode ###
Here is an example of making a seafile configuration. First run Here is an example of making a seafile configuration for a user with **no** two-factor authentication. First run
rclone config rclone config
@ -52,17 +53,21 @@ Choose a number from below, or type in your own value
1 / Connect to cloud.seafile.com 1 / Connect to cloud.seafile.com
\ "https://cloud.seafile.com/" \ "https://cloud.seafile.com/"
url> http://my.seafile.server/ url> http://my.seafile.server/
User name User name (usually email address)
Enter a string value. Press Enter for the default (""). Enter a string value. Press Enter for the default ("").
user> me@example.com user> me@example.com
Password Password
y) Yes type in my own password y) Yes type in my own password
g) Generate random password g) Generate random password
n) No leave this optional password blank (default)
y/g> y y/g> y
Enter the password: Enter the password:
password: password:
Confirm the password: Confirm the password:
password: password:
Two-factor authentication ('true' if the account has 2FA enabled)
Enter a boolean value (true or false). Press Enter for the default ("false").
2fa> false
Name of the library. Leave blank to access all non-encrypted libraries. Name of the library. Leave blank to access all non-encrypted libraries.
Enter a string value. Press Enter for the default (""). Enter a string value. Press Enter for the default ("").
library> library>
@ -76,12 +81,14 @@ y) Yes
n) No (default) n) No (default)
y/n> n y/n> n
Remote config Remote config
Two-factor authentication is not enabled on this account.
-------------------- --------------------
[seafile] [seafile]
type = seafile type = seafile
url = http://my.seafile.server/ url = http://my.seafile.server/
user = me@example.com user = me@example.com
password = *** ENCRYPTED *** pass = *** ENCRYPTED ***
2fa = false
-------------------- --------------------
y) Yes this is OK (default) y) Yes this is OK (default)
e) Edit this remote e) Edit this remote
@ -89,7 +96,7 @@ d) Delete this remote
y/e/d> y y/e/d> y
``` ```
This remote is called `seafile`. It's pointing to the root of your seafile server and can now be used like this This remote is called `seafile`. It's pointing to the root of your seafile server and can now be used like this:
See all libraries See all libraries
@ -110,6 +117,8 @@ excess files in the library.
### Configuration in library mode ### ### Configuration in library mode ###
Here's an example of a configuration in library mode with a user that has the two-factor authentication enabled. Your 2FA code will be asked at the end of the configuration, and will attempt to authenticate you:
``` ```
No remotes found - make a new one No remotes found - make a new one
n) New remote n) New remote
@ -133,17 +142,21 @@ Choose a number from below, or type in your own value
1 / Connect to cloud.seafile.com 1 / Connect to cloud.seafile.com
\ "https://cloud.seafile.com/" \ "https://cloud.seafile.com/"
url> http://my.seafile.server/ url> http://my.seafile.server/
User name User name (usually email address)
Enter a string value. Press Enter for the default (""). Enter a string value. Press Enter for the default ("").
user> me@example.com user> me@example.com
Password Password
y) Yes type in my own password y) Yes type in my own password
g) Generate random password g) Generate random password
n) No leave this optional password blank (default)
y/g> y y/g> y
Enter the password: Enter the password:
password: password:
Confirm the password: Confirm the password:
password: password:
Two-factor authentication ('true' if the account has 2FA enabled)
Enter a boolean value (true or false). Press Enter for the default ("false").
2fa> true
Name of the library. Leave blank to access all non-encrypted libraries. Name of the library. Leave blank to access all non-encrypted libraries.
Enter a string value. Press Enter for the default (""). Enter a string value. Press Enter for the default ("").
library> My Library library> My Library
@ -157,12 +170,17 @@ y) Yes
n) No (default) n) No (default)
y/n> n y/n> n
Remote config Remote config
Two-factor authentication: please enter your 2FA code
2fa code> 123456
Authenticating...
Success!
-------------------- --------------------
[seafile] [seafile]
type = seafile type = seafile
url = http://my.seafile.server/ url = http://my.seafile.server/
user = me@example.com user = me@example.com
password = *** ENCRYPTED *** pass =
2fa = true
library = My Library library = My Library
-------------------- --------------------
y) Yes this is OK (default) y) Yes this is OK (default)
@ -171,6 +189,8 @@ d) Delete this remote
y/e/d> y y/e/d> y
``` ```
You'll notice your password is blank in the configuration. It's because we only need the password to authenticate you once.
You specified `My Library` during the configuration. The root of the remote is pointing at the You specified `My Library` during the configuration. The root of the remote is pointing at the
root of the library `My Library`: root of the library `My Library`:
@ -246,6 +266,99 @@ Versions below 6.0 are not supported.
Versions between 6.0 and 6.3 haven't been tested and might not work properly. Versions between 6.0 and 6.3 haven't been tested and might not work properly.
<!--- autogenerated options start - DO NOT EDIT, instead edit fs.RegInfo in backend/seafile/seafile.go then run make backenddocs --> <!--- autogenerated options start - DO NOT EDIT, instead edit fs.RegInfo in backend/seafile/seafile.go then run make backenddocs -->
### Standard Options
Here are the standard options specific to seafile (seafile).
#### --seafile-url
URL of seafile host to connect to
- Config: url
- Env Var: RCLONE_SEAFILE_URL
- Type: string
- Default: ""
- Examples:
- "https://cloud.seafile.com/"
- Connect to cloud.seafile.com
#### --seafile-user
User name (usually email address)
- Config: user
- Env Var: RCLONE_SEAFILE_USER
- Type: string
- Default: ""
#### --seafile-pass
Password
- Config: pass
- Env Var: RCLONE_SEAFILE_PASS
- Type: string
- Default: ""
#### --seafile-2fa
Two-factor authentication ('true' if the account has 2FA enabled)
- Config: 2fa
- Env Var: RCLONE_SEAFILE_2FA
- Type: bool
- Default: false
#### --seafile-library
Name of the library. Leave blank to access all non-encrypted libraries.
- Config: library
- Env Var: RCLONE_SEAFILE_LIBRARY
- Type: string
- Default: ""
#### --seafile-library-key
Library password (for encrypted libraries only). Leave blank if you pass it through the command line.
- Config: library_key
- Env Var: RCLONE_SEAFILE_LIBRARY_KEY
- Type: string
- Default: ""
#### --seafile-auth-token
Authentication token
- Config: auth_token
- Env Var: RCLONE_SEAFILE_AUTH_TOKEN
- Type: string
- Default: ""
### Advanced Options
Here are the advanced options specific to seafile (seafile).
#### --seafile-create-library
Should rclone create a library if it doesn't exist
- Config: create_library
- Env Var: RCLONE_SEAFILE_CREATE_LIBRARY
- Type: bool
- Default: false
#### --seafile-encoding
This sets the encoding for the backend.
See: the [encoding section in the overview](/overview/#encoding) for more info.
- Config: encoding
- Env Var: RCLONE_SEAFILE_ENCODING
- Type: MultiEncoder
- Default: Slash,DoubleQuote,BackSlash,Ctl,InvalidUtf8
<!--- autogenerated options stop --> <!--- autogenerated options stop -->