mirror of
https://github.com/rclone/rclone
synced 2024-11-21 22:50:16 +01:00
seafile: implement 2FA
This commit is contained in:
parent
56c9fdb53c
commit
5f71d186b2
@ -32,8 +32,16 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
librariesCacheKey = "all"
|
||||
retryAfterHeader = "Retry-After"
|
||||
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
|
||||
@ -49,8 +57,9 @@ func init() {
|
||||
Name: "seafile",
|
||||
Description: "seafile",
|
||||
NewFs: NewFs,
|
||||
Config: Config,
|
||||
Options: []fs.Option{{
|
||||
Name: "url",
|
||||
Name: configURL,
|
||||
Help: "URL of seafile host to connect to",
|
||||
Required: true,
|
||||
Examples: []fs.OptionExample{{
|
||||
@ -58,26 +67,35 @@ func init() {
|
||||
Help: "Connect to cloud.seafile.com",
|
||||
}},
|
||||
}, {
|
||||
Name: "user",
|
||||
Help: "User name",
|
||||
Name: configUser,
|
||||
Help: "User name (usually email address)",
|
||||
Required: true,
|
||||
}, {
|
||||
Name: "pass",
|
||||
// Password is not required, it will be left blank for 2FA
|
||||
Name: configPassword,
|
||||
Help: "Password",
|
||||
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.",
|
||||
}, {
|
||||
Name: "library_key",
|
||||
Name: configLibraryKey,
|
||||
Help: "Library password (for encrypted libraries only). Leave blank if you pass it through the command line.",
|
||||
IsPassword: true,
|
||||
}, {
|
||||
Name: "create_library",
|
||||
Help: "Should create library if it doesn't exist",
|
||||
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,
|
||||
}, {
|
||||
Name: config.ConfigEncoding,
|
||||
Help: config.ConfigEncodingHelp,
|
||||
@ -97,6 +115,8 @@ 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"`
|
||||
@ -205,10 +225,16 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||
f.moveDirNotAvailable = true
|
||||
}
|
||||
|
||||
err = f.authorizeAccount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// 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
|
||||
@ -270,26 +296,108 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||
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
|
||||
func (f *Fs) setAuthorizationToken(token string) {
|
||||
f.srv.SetHeader("Authorization", "Token "+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()
|
||||
defer f.authMu.Unlock()
|
||||
|
||||
token, err := f.getAuthorizationToken(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
f.setAuthorizationToken(token)
|
||||
return nil
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// retryErrorCodes is a slice of error codes that we will retry
|
||||
var retryErrorCodes = []int{
|
||||
401, // Unauthorized (eg "Token has expired")
|
||||
408, // Request Timeout
|
||||
429, // Rate exceeded.
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
// set the retry appropriately, starting with a minimum of 1
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if err != nil || (resp != nil && resp.StatusCode > 400) {
|
||||
return true, err
|
||||
|
@ -32,37 +32,44 @@ var (
|
||||
// ==================== Seafile API ====================
|
||||
|
||||
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
|
||||
opts := rest.Opts{
|
||||
Method: "POST",
|
||||
Path: "api2/auth-token/",
|
||||
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{
|
||||
Username: f.opt.User,
|
||||
Password: f.opt.Password,
|
||||
Username: user,
|
||||
Password: password,
|
||||
}
|
||||
result := api.AuthenticationResult{}
|
||||
|
||||
var resp *http.Response
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
||||
return f.shouldRetryNoReauth(resp, err)
|
||||
})
|
||||
_, err := srv.CallJSON(ctx, &opts, &request, &result)
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
return "", fs.ErrorPermissionDenied
|
||||
}
|
||||
}
|
||||
// This is only going to be http errors here
|
||||
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, ", "))
|
||||
}
|
||||
if result.Token == "" {
|
||||
// No error in "non_field_errors" field but still empty token
|
||||
return "", errors.New("failed to authenticate")
|
||||
}
|
||||
return result.Token, nil
|
||||
}
|
||||
|
||||
@ -79,11 +86,11 @@ func (f *Fs) getServerInfo(ctx context.Context) (account *api.ServerInfo, err er
|
||||
var resp *http.Response
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
}
|
||||
@ -105,11 +112,11 @@ func (f *Fs) getUserAccountInfo(ctx context.Context) (account *api.AccountInfo,
|
||||
var resp *http.Response
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
}
|
||||
@ -132,11 +139,11 @@ func (f *Fs) getLibraries(ctx context.Context) ([]api.Library, error) {
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
}
|
||||
@ -163,11 +170,11 @@ func (f *Fs) createLibrary(ctx context.Context, libraryName, password string) (l
|
||||
var resp *http.Response
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
}
|
||||
@ -190,11 +197,11 @@ func (f *Fs) deleteLibrary(ctx context.Context, libraryID string) error {
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return fs.ErrorPermissionDenied
|
||||
}
|
||||
}
|
||||
@ -221,7 +228,7 @@ func (f *Fs) decryptLibrary(ctx context.Context, libraryID, password string) err
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.Call(ctx, &opts)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
@ -264,10 +271,13 @@ func (f *Fs) getDirectoryEntriesAPIv21(ctx context.Context, libraryID, dirPath s
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, fs.ErrorDirNotFound
|
||||
}
|
||||
@ -306,11 +316,11 @@ func (f *Fs) getDirectoryDetails(ctx context.Context, libraryID, dirPath string)
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
@ -348,11 +358,11 @@ func (f *Fs) createDir(ctx context.Context, libraryID, dirPath string) error {
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.Call(ctx, &opts)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return fs.ErrorPermissionDenied
|
||||
}
|
||||
}
|
||||
@ -388,11 +398,11 @@ func (f *Fs) renameDir(ctx context.Context, libraryID, dirPath, newName string)
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.Call(ctx, &opts)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return fs.ErrorPermissionDenied
|
||||
}
|
||||
}
|
||||
@ -428,11 +438,11 @@ func (f *Fs) moveDir(ctx context.Context, srcLibraryID, srcDir, srcName, dstLibr
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, &request, nil)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return fs.ErrorPermissionDenied
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
@ -464,11 +474,11 @@ func (f *Fs) deleteDir(ctx context.Context, libraryID, filePath string) error {
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, nil)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return fs.ErrorPermissionDenied
|
||||
}
|
||||
}
|
||||
@ -495,14 +505,14 @@ func (f *Fs) getFileDetails(ctx context.Context, libraryID, filePath string) (*a
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, fs.ErrorObjectNotFound
|
||||
}
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
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) {
|
||||
resp, err := f.srv.CallJSON(ctx, &opts, nil, nil)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
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
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
@ -604,7 +614,7 @@ func (f *Fs) download(ctx context.Context, url string, size int64, options ...fs
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.Call(ctx, &opts)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
@ -649,11 +659,11 @@ func (f *Fs) getUploadLink(ctx context.Context, libraryID string) (string, error
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return "", fs.ErrorPermissionDenied
|
||||
}
|
||||
}
|
||||
@ -693,7 +703,7 @@ func (f *Fs) upload(ctx context.Context, in io.Reader, uploadLink, filePath stri
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
if resp.StatusCode == 500 {
|
||||
@ -729,11 +739,11 @@ func (f *Fs) listShareLinks(ctx context.Context, libraryID, remote string) ([]ap
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
@ -767,11 +777,11 @@ func (f *Fs) createShareLink(ctx context.Context, libraryID, remote string) (*ap
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
@ -808,11 +818,11 @@ func (f *Fs) copyFile(ctx context.Context, srcLibraryID, srcPath, dstLibraryID,
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
@ -850,11 +860,11 @@ func (f *Fs) moveFile(ctx context.Context, srcLibraryID, srcPath, dstLibraryID,
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
@ -890,11 +900,11 @@ func (f *Fs) renameFile(ctx context.Context, libraryID, filePath, newname string
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, &request, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
@ -928,11 +938,11 @@ func (f *Fs) emptyLibraryTrash(ctx context.Context, libraryID string) error {
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, nil)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return fs.ErrorPermissionDenied
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
@ -966,10 +976,13 @@ func (f *Fs) getDirectoryEntriesAPIv2(ctx context.Context, libraryID, dirPath st
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, fs.ErrorDirNotFound
|
||||
}
|
||||
@ -1017,11 +1030,11 @@ func (f *Fs) copyFileAPIv2(ctx context.Context, srcLibraryID, srcPath, dstLibrar
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.Call(ctx, &opts)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return nil, fs.ErrorPermissionDenied
|
||||
}
|
||||
}
|
||||
@ -1062,7 +1075,7 @@ func (f *Fs) renameFileAPIv2(ctx context.Context, libraryID, filePath, newname s
|
||||
var err error
|
||||
err = f.pacer.Call(func() (bool, error) {
|
||||
resp, err = f.srv.Call(ctx, &opts)
|
||||
return f.shouldRetry(ctx, resp, err)
|
||||
return f.shouldRetry(resp, err)
|
||||
})
|
||||
if err != 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
|
||||
return nil
|
||||
}
|
||||
if resp.StatusCode == 403 {
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return fs.ErrorPermissionDenied
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
|
@ -1,16 +1,17 @@
|
||||
---
|
||||
title: "Seafile"
|
||||
description: "Seafile"
|
||||
date: "2020-05-02"
|
||||
date: "2020-05-19"
|
||||
---
|
||||
|
||||
<i class="fa fa-server"></i>Seafile
|
||||
----------------------------------------
|
||||
|
||||
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.
|
||||
Seafile versions 6.x and 7.x are all supported.
|
||||
Encrypted libraries are also supported.
|
||||
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.
|
||||
- Seafile versions 6.x and 7.x are all supported.
|
||||
- Encrypted libraries are also supported.
|
||||
- It supports 2FA enabled users
|
||||
|
||||
### 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:
|
||||
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:
|
||||
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 ###
|
||||
|
||||
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
|
||||
|
||||
@ -52,17 +53,21 @@ Choose a number from below, or type in your own value
|
||||
1 / Connect to cloud.seafile.com
|
||||
\ "https://cloud.seafile.com/"
|
||||
url> http://my.seafile.server/
|
||||
User name
|
||||
User name (usually email address)
|
||||
Enter a string value. Press Enter for the default ("").
|
||||
user> me@example.com
|
||||
Password
|
||||
y) Yes type in my own password
|
||||
g) Generate random password
|
||||
n) No leave this optional password blank (default)
|
||||
y/g> y
|
||||
Enter the password:
|
||||
password:
|
||||
Confirm the 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.
|
||||
Enter a string value. Press Enter for the default ("").
|
||||
library>
|
||||
@ -76,12 +81,14 @@ y) Yes
|
||||
n) No (default)
|
||||
y/n> n
|
||||
Remote config
|
||||
Two-factor authentication is not enabled on this account.
|
||||
--------------------
|
||||
[seafile]
|
||||
type = seafile
|
||||
url = http://my.seafile.server/
|
||||
user = me@example.com
|
||||
password = *** ENCRYPTED ***
|
||||
pass = *** ENCRYPTED ***
|
||||
2fa = false
|
||||
--------------------
|
||||
y) Yes this is OK (default)
|
||||
e) Edit this remote
|
||||
@ -89,7 +96,7 @@ d) Delete this remote
|
||||
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
|
||||
|
||||
@ -110,6 +117,8 @@ excess files in the library.
|
||||
|
||||
### 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
|
||||
n) New remote
|
||||
@ -133,17 +142,21 @@ Choose a number from below, or type in your own value
|
||||
1 / Connect to cloud.seafile.com
|
||||
\ "https://cloud.seafile.com/"
|
||||
url> http://my.seafile.server/
|
||||
User name
|
||||
User name (usually email address)
|
||||
Enter a string value. Press Enter for the default ("").
|
||||
user> me@example.com
|
||||
Password
|
||||
y) Yes type in my own password
|
||||
g) Generate random password
|
||||
n) No leave this optional password blank (default)
|
||||
y/g> y
|
||||
Enter the password:
|
||||
password:
|
||||
Confirm the 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.
|
||||
Enter a string value. Press Enter for the default ("").
|
||||
library> My Library
|
||||
@ -157,12 +170,17 @@ y) Yes
|
||||
n) No (default)
|
||||
y/n> n
|
||||
Remote config
|
||||
Two-factor authentication: please enter your 2FA code
|
||||
2fa code> 123456
|
||||
Authenticating...
|
||||
Success!
|
||||
--------------------
|
||||
[seafile]
|
||||
type = seafile
|
||||
url = http://my.seafile.server/
|
||||
user = me@example.com
|
||||
password = *** ENCRYPTED ***
|
||||
pass =
|
||||
2fa = true
|
||||
library = My Library
|
||||
--------------------
|
||||
y) Yes this is OK (default)
|
||||
@ -171,6 +189,8 @@ d) Delete this remote
|
||||
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
|
||||
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.
|
||||
|
||||
<!--- 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 -->
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user