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 (
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

View File

@ -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 {

View File

@ -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 -->