forked from TrueCloudLab/rclone
seafile: implement 2FA
This commit is contained in:
parent
56c9fdb53c
commit
5f71d186b2
3 changed files with 329 additions and 111 deletions
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue