seafile: New backend for seafile server

This commit is contained in:
Fred 2020-04-25 18:55:18 +01:00 committed by Nick Craig-Wood
parent 62cfe3f384
commit c754e89906
20 changed files with 3291 additions and 0 deletions

View File

@ -59,6 +59,7 @@ Rclone *("rsync for cloud storage")* is a command line program to sync files and
* QingStor [:page_facing_up:](https://rclone.org/qingstor/) * QingStor [:page_facing_up:](https://rclone.org/qingstor/)
* Rackspace Cloud Files [:page_facing_up:](https://rclone.org/swift/) * Rackspace Cloud Files [:page_facing_up:](https://rclone.org/swift/)
* Scaleway [:page_facing_up:](https://rclone.org/s3/#scaleway) * Scaleway [:page_facing_up:](https://rclone.org/s3/#scaleway)
* Seafile [:page_facing_up:](https://rclone.org/seafile/)
* SFTP [:page_facing_up:](https://rclone.org/sftp/) * SFTP [:page_facing_up:](https://rclone.org/sftp/)
* StackPath [:page_facing_up:](https://rclone.org/s3/#stackpath) * StackPath [:page_facing_up:](https://rclone.org/s3/#stackpath)
* SugarSync [:page_facing_up:](https://rclone.org/sugarsync/) * SugarSync [:page_facing_up:](https://rclone.org/sugarsync/)

View File

@ -31,6 +31,7 @@ import (
_ "github.com/rclone/rclone/backend/putio" _ "github.com/rclone/rclone/backend/putio"
_ "github.com/rclone/rclone/backend/qingstor" _ "github.com/rclone/rclone/backend/qingstor"
_ "github.com/rclone/rclone/backend/s3" _ "github.com/rclone/rclone/backend/s3"
_ "github.com/rclone/rclone/backend/seafile"
_ "github.com/rclone/rclone/backend/sftp" _ "github.com/rclone/rclone/backend/sftp"
_ "github.com/rclone/rclone/backend/sharefile" _ "github.com/rclone/rclone/backend/sharefile"
_ "github.com/rclone/rclone/backend/sugarsync" _ "github.com/rclone/rclone/backend/sugarsync"

View File

@ -0,0 +1,153 @@
package api
// Some api objects are duplicated with only small differences,
// it's because the returned JSON objects are very inconsistent between api calls
// AuthenticationRequest contains user credentials
type AuthenticationRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// AuthenticationResult is returned by a call to the authentication api
type AuthenticationResult struct {
Token string `json:"token"`
Errors []string `json:"non_field_errors"`
}
// AccountInfo contains simple user properties
type AccountInfo struct {
Usage int64 `json:"usage"`
Total int64 `json:"total"`
Email string `json:"email"`
Name string `json:"name"`
}
// ServerInfo contains server information
type ServerInfo struct {
Version string `json:"version"`
}
// DefaultLibrary when none specified
type DefaultLibrary struct {
ID string `json:"repo_id"`
Exists bool `json:"exists"`
}
// CreateLibraryRequest contains the information needed to create a library
type CreateLibraryRequest struct {
Name string `json:"name"`
Description string `json:"desc"`
Password string `json:"passwd"`
}
// Library properties. Please note not all properties are going to be useful for rclone
type Library struct {
Encrypted bool `json:"encrypted"`
Owner string `json:"owner"`
ID string `json:"id"`
Size int `json:"size"`
Name string `json:"name"`
Modified int64 `json:"mtime"`
}
// CreateLibrary properties. Seafile is not consistent and returns different types for different API calls
type CreateLibrary struct {
ID string `json:"repo_id"`
Name string `json:"repo_name"`
}
// FileType is either "dir" or "file"
type FileType string
// File types
var (
FileTypeDir FileType = "dir"
FileTypeFile FileType = "file"
)
// FileDetail contains file properties (for older api v2.0)
type FileDetail struct {
ID string `json:"id"`
Type FileType `json:"type"`
Name string `json:"name"`
Size int64 `json:"size"`
Parent string `json:"parent_dir"`
Modified string `json:"last_modified"`
}
// DirEntries contains a list of DirEntry
type DirEntries struct {
Entries []DirEntry `json:"dirent_list"`
}
// DirEntry contains a directory entry
type DirEntry struct {
ID string `json:"id"`
Type FileType `json:"type"`
Name string `json:"name"`
Size int64 `json:"size"`
Path string `json:"parent_dir"`
Modified int64 `json:"mtime"`
}
// Operation is move, copy or rename
type Operation string
// Operations
var (
CopyFileOperation Operation = "copy"
MoveFileOperation Operation = "move"
RenameFileOperation Operation = "rename"
)
// FileOperationRequest is sent to the api to copy, move or rename a file
type FileOperationRequest struct {
Operation Operation `json:"operation"`
DestinationLibraryID string `json:"dst_repo"` // For copy/move operation
DestinationPath string `json:"dst_dir"` // For copy/move operation
NewName string `json:"newname"` // Only to be used by the rename operation
}
// FileInfo is returned by a server file copy/move/rename (new api v2.1)
type FileInfo struct {
Type string `json:"type"`
LibraryID string `json:"repo_id"`
Path string `json:"parent_dir"`
Name string `json:"obj_name"`
ID string `json:"obj_id"`
Size int64 `json:"size"`
}
// CreateDirRequest only contain an operation field
type CreateDirRequest struct {
Operation string `json:"operation"`
}
// DirectoryDetail contains the directory details specific to the getDirectoryDetails call
type DirectoryDetail struct {
ID string `json:"repo_id"`
Name string `json:"name"`
Path string `json:"path"`
}
// ShareLinkRequest contains the information needed to create or list shared links
type ShareLinkRequest struct {
LibraryID string `json:"repo_id"`
Path string `json:"path"`
}
// SharedLink contains the information returned by a call to shared link creation
type SharedLink struct {
Link string `json:"link"`
IsExpired bool `json:"is_expired"`
}
// BatchSourceDestRequest contains JSON parameters for sending a batch copy or move operation
type BatchSourceDestRequest struct {
SrcLibraryID string `json:"src_repo_id"`
SrcParentDir string `json:"src_parent_dir"`
SrcItems []string `json:"src_dirents"`
DstLibraryID string `json:"dst_repo_id"`
DstParentDir string `json:"dst_parent_dir"`
}

127
backend/seafile/object.go Normal file
View File

@ -0,0 +1,127 @@
package seafile
import (
"context"
"io"
"time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/hash"
)
// Object describes a seafile object (also commonly called a file)
type Object struct {
fs *Fs // what this object is part of
id string // internal ID of object
remote string // The remote path (full path containing library name if target at root)
pathInLibrary string // Path of the object without the library name
size int64 // size of the object
modTime time.Time // modification time of the object
libraryID string // Needed to download the file
}
// ==================== Interface fs.DirEntry ====================
// Return a string version
func (o *Object) String() string {
if o == nil {
return "<nil>"
}
return o.remote
}
// Remote returns the remote string
func (o *Object) Remote() string {
return o.remote
}
// ModTime returns last modified time
func (o *Object) ModTime(context.Context) time.Time {
return o.modTime
}
// Size returns the size of an object in bytes
func (o *Object) Size() int64 {
return o.size
}
// ==================== Interface fs.ObjectInfo ====================
// Fs returns the parent Fs
func (o *Object) Fs() fs.Info {
return o.fs
}
// Hash returns the selected checksum of the file
// If no checksum is available it returns ""
func (o *Object) Hash(ctx context.Context, ty hash.Type) (string, error) {
return "", hash.ErrUnsupported
}
// Storable says whether this object can be stored
func (o *Object) Storable() bool {
return true
}
// ==================== Interface fs.Object ====================
// SetModTime sets the metadata on the object to set the modification date
func (o *Object) SetModTime(ctx context.Context, t time.Time) error {
return fs.ErrorCantSetModTime
}
// Open opens the file for read. Call Close() on the returned io.ReadCloser
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
downloadLink, err := o.fs.getDownloadLink(ctx, o.libraryID, o.pathInLibrary)
if err != nil {
return nil, err
}
reader, err := o.fs.download(ctx, downloadLink, o.Size(), options...)
if err != nil {
return nil, err
}
return reader, nil
}
// Update in to the object with the modTime given of the given size
//
// When called from outside a Fs by rclone, src.Size() will always be >= 0.
// But for unknown-sized objects (indicated by src.Size() == -1), Upload should either
// return an error or update the object properly (rather than e.g. calling panic).
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
// The upload sometimes return a temporary 500 error
// We cannot use the pacer to retry uploading the file as the upload link is single use only
for retry := 0; retry <= 3; retry++ {
uploadLink, err := o.fs.getUploadLink(ctx, o.libraryID)
if err != nil {
return err
}
uploaded, err := o.fs.upload(ctx, in, uploadLink, o.pathInLibrary)
if err == ErrorInternalDuringUpload {
// This is a temporary error, try again with a new upload link
continue
}
if err != nil {
return err
}
// Set the properties from the upload back to the object
o.size = uploaded.Size
o.id = uploaded.ID
return nil
}
return ErrorInternalDuringUpload
}
// Remove this object
func (o *Object) Remove(ctx context.Context) error {
return o.fs.deleteFile(ctx, o.libraryID, o.pathInLibrary)
}
// ==================== Optional Interface fs.IDer ====================
// ID returns the ID of the Object if known, or "" if not
func (o *Object) ID() string {
return o.id
}

67
backend/seafile/pacer.go Normal file
View File

@ -0,0 +1,67 @@
package seafile
import (
"fmt"
"net/url"
"sync"
"time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/lib/pacer"
)
const (
minSleep = 100 * time.Millisecond
maxSleep = 10 * time.Second
decayConstant = 2 // bigger for slower decay, exponential
)
// Use only one pacer per server URL
var (
pacers map[string]*fs.Pacer
pacerMutex sync.Mutex
)
func init() {
pacers = make(map[string]*fs.Pacer, 0)
}
// getPacer returns the unique pacer for that remote URL
func getPacer(remote string) *fs.Pacer {
pacerMutex.Lock()
defer pacerMutex.Unlock()
remote = parseRemote(remote)
if existing, found := pacers[remote]; found {
return existing
}
pacers[remote] = fs.NewPacer(
pacer.NewDefault(
pacer.MinSleep(minSleep),
pacer.MaxSleep(maxSleep),
pacer.DecayConstant(decayConstant),
),
)
return pacers[remote]
}
// parseRemote formats a remote url into "hostname:port"
func parseRemote(remote string) string {
remoteURL, err := url.Parse(remote)
if err != nil {
// Return a default value in the very unlikely event we're not going to parse remote
fs.Infof(nil, "Cannot parse remote %s", remote)
return "default"
}
host := remoteURL.Hostname()
port := remoteURL.Port()
if port == "" {
if remoteURL.Scheme == "https" {
port = "443"
} else {
port = "80"
}
}
return fmt.Sprintf("%s:%s", host, port)
}

1247
backend/seafile/seafile.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,123 @@
package seafile
import (
"path"
"testing"
"github.com/stretchr/testify/assert"
)
type pathData struct {
configLibrary string // Library specified in the config
configRoot string // Root directory specified in the config
argumentPath string // Path given as an argument in the command line
expectedLibrary string
expectedPath string
}
// Test the method to split a library name and a path
// from a mix of configuration data and path command line argument
func TestSplitPath(t *testing.T) {
testData := []pathData{
pathData{
configLibrary: "",
configRoot: "",
argumentPath: "",
expectedLibrary: "",
expectedPath: "",
},
pathData{
configLibrary: "",
configRoot: "",
argumentPath: "Library",
expectedLibrary: "Library",
expectedPath: "",
},
pathData{
configLibrary: "",
configRoot: "",
argumentPath: path.Join("Library", "path", "to", "file"),
expectedLibrary: "Library",
expectedPath: path.Join("path", "to", "file"),
},
pathData{
configLibrary: "Library",
configRoot: "",
argumentPath: "",
expectedLibrary: "Library",
expectedPath: "",
},
pathData{
configLibrary: "Library",
configRoot: "",
argumentPath: "path",
expectedLibrary: "Library",
expectedPath: "path",
},
pathData{
configLibrary: "Library",
configRoot: "",
argumentPath: path.Join("path", "to", "file"),
expectedLibrary: "Library",
expectedPath: path.Join("path", "to", "file"),
},
pathData{
configLibrary: "Library",
configRoot: "root",
argumentPath: "",
expectedLibrary: "Library",
expectedPath: "root",
},
pathData{
configLibrary: "Library",
configRoot: path.Join("root", "path"),
argumentPath: "",
expectedLibrary: "Library",
expectedPath: path.Join("root", "path"),
},
pathData{
configLibrary: "Library",
configRoot: "root",
argumentPath: "path",
expectedLibrary: "Library",
expectedPath: path.Join("root", "path"),
},
pathData{
configLibrary: "Library",
configRoot: "root",
argumentPath: path.Join("path", "to", "file"),
expectedLibrary: "Library",
expectedPath: path.Join("root", "path", "to", "file"),
},
pathData{
configLibrary: "Library",
configRoot: path.Join("root", "path"),
argumentPath: path.Join("subpath", "to", "file"),
expectedLibrary: "Library",
expectedPath: path.Join("root", "path", "subpath", "to", "file"),
},
}
for _, test := range testData {
fs := &Fs{
libraryName: test.configLibrary,
rootDirectory: test.configRoot,
}
libraryName, path := fs.splitPath(test.argumentPath)
assert.Equal(t, test.expectedLibrary, libraryName)
assert.Equal(t, test.expectedPath, path)
}
}
func TestSplitPathIntoSlice(t *testing.T) {
testData := map[string][]string{
"1": {"1"},
"/1": {"1"},
"/1/": {"1"},
"1/2/3": {"1", "2", "3"},
}
for input, expected := range testData {
output := splitPath(input)
assert.Equal(t, expected, output)
}
}

View File

@ -0,0 +1,17 @@
// Test Seafile filesystem interface
package seafile_test
import (
"testing"
"github.com/rclone/rclone/backend/seafile"
"github.com/rclone/rclone/fstest/fstests"
)
// TestIntegration runs integration tests against the remote
func TestIntegration(t *testing.T) {
fstests.Run(t, &fstests.Opt{
RemoteName: "TestSeafile:",
NilObject: (*seafile.Object)(nil),
})
}

1083
backend/seafile/webapi.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -54,6 +54,7 @@ docs = [
"pcloud.md", "pcloud.md",
"premiumizeme.md", "premiumizeme.md",
"putio.md", "putio.md",
"seafile.md",
"sftp.md", "sftp.md",
"sugarsync.md", "sugarsync.md",
"union.md", "union.md",

View File

@ -51,6 +51,7 @@ Rclone is a command line program to sync files and directories to and from:
* {{< provider name="Rackspace Cloud Files" home="https://www.rackspace.com/cloud/files" config="/swift/" >}} * {{< provider name="Rackspace Cloud Files" home="https://www.rackspace.com/cloud/files" config="/swift/" >}}
* {{< provider name="rsync.net" home="https://rsync.net/products/rclone.html" config="/sftp/#rsync-net" >}} * {{< provider name="rsync.net" home="https://rsync.net/products/rclone.html" config="/sftp/#rsync-net" >}}
* {{< provider name="Scaleway" home="https://www.scaleway.com/object-storage/" config="/s3/#scaleway" >}} * {{< provider name="Scaleway" home="https://www.scaleway.com/object-storage/" config="/s3/#scaleway" >}}
* {{< provider name="Seafile" home="https://www.seafile.com/" config="/seafile/" >}}
* {{< provider name="SFTP" home="https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol" config="/sftp/" >}} * {{< provider name="SFTP" home="https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol" config="/sftp/" >}}
* {{< provider name="StackPath" home="https://www.stackpath.com/products/object-storage/" config="/s3/#stackpath" >}} * {{< provider name="StackPath" home="https://www.stackpath.com/products/object-storage/" config="/s3/#stackpath" >}}
* {{< provider name="SugarSync" home="https://sugarsync.com/" config="/sugarsync/" >}} * {{< provider name="SugarSync" home="https://sugarsync.com/" config="/sugarsync/" >}}

View File

@ -50,6 +50,7 @@ See the following for detailed instructions for
* [premiumize.me](/premiumizeme/) * [premiumize.me](/premiumizeme/)
* [put.io](/putio/) * [put.io](/putio/)
* [QingStor](/qingstor/) * [QingStor](/qingstor/)
* [Seafile](/seafile/)
* [SFTP](/sftp/) * [SFTP](/sftp/)
* [SugarSync](/sugarsync/) * [SugarSync](/sugarsync/)
* [Union](/union/) * [Union](/union/)

View File

@ -43,6 +43,7 @@ Here is an overview of the major features of each cloud storage system.
| premiumize.me | - | No | Yes | No | R | | premiumize.me | - | No | Yes | No | R |
| put.io | CRC-32 | Yes | No | Yes | R | | put.io | CRC-32 | Yes | No | Yes | R |
| QingStor | MD5 | No | No | No | R/W | | QingStor | MD5 | No | No | No | R/W |
| Seafile | - | No | No | No | - |
| SFTP | MD5, SHA1 ‡ | Yes | Depends | No | - | | SFTP | MD5, SHA1 ‡ | Yes | Depends | No | - |
| SugarSync | - | No | No | No | - | | SugarSync | - | No | No | No | - |
| WebDAV | MD5, SHA1 ††| Yes ††† | Depends | No | - | | WebDAV | MD5, SHA1 ††| Yes ††† | Depends | No | - |
@ -342,6 +343,7 @@ operations more efficient.
| premiumize.me | Yes | No | Yes | Yes | No | No | No | Yes | Yes | Yes | | premiumize.me | Yes | No | Yes | Yes | No | No | No | Yes | Yes | Yes |
| put.io | Yes | No | Yes | Yes | Yes | No | Yes | No [#2178](https://github.com/rclone/rclone/issues/2178) | Yes | Yes | | put.io | Yes | No | Yes | Yes | Yes | No | Yes | No [#2178](https://github.com/rclone/rclone/issues/2178) | Yes | Yes |
| QingStor | No | Yes | No | No | Yes | Yes | No | No [#2178](https://github.com/rclone/rclone/issues/2178) | No | No | | QingStor | No | Yes | No | No | Yes | Yes | No | No [#2178](https://github.com/rclone/rclone/issues/2178) | No | No |
| Seafile | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| SFTP | No | No | Yes | Yes | No | No | Yes | No [#2178](https://github.com/rclone/rclone/issues/2178) | Yes | Yes | | SFTP | No | No | Yes | Yes | No | No | Yes | No [#2178](https://github.com/rclone/rclone/issues/2178) | Yes | Yes |
| SugarSync | Yes | Yes | Yes | Yes | No | No | Yes | Yes | No | Yes | | SugarSync | Yes | Yes | Yes | Yes | No | No | Yes | Yes | No | Yes |
| WebDAV | Yes | Yes | Yes | Yes | No | No | Yes ‡ | No [#2178](https://github.com/rclone/rclone/issues/2178) | Yes | Yes | | WebDAV | Yes | Yes | Yes | Yes | No | No | Yes ‡ | No [#2178](https://github.com/rclone/rclone/issues/2178) | Yes | Yes |

251
docs/content/seafile.md Normal file
View File

@ -0,0 +1,251 @@
---
title: "Seafile"
description: "Seafile"
date: "2020-05-02"
---
<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.
### Root mode vs Library mode ###
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**.
### Configuration in root mode ###
Here is an example of making a seafile configuration. First run
rclone config
This will guide you through an interactive setup process. To authenticate
you will need the URL of your server, your email (or username) and your password.
```
No remotes found - make a new one
n) New remote
s) Set configuration password
q) Quit config
n/s/q> n
name> seafile
Type of storage to configure.
Enter a string value. Press Enter for the default ("").
Choose a number from below, or type in your own value
[snip]
XX / Seafile
\ "seafile"
[snip]
Storage> seafile
** See help for seafile backend at: https://rclone.org/seafile/ **
URL of seafile host to connect to
Enter a string value. Press Enter for the default ("").
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
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
y/g> y
Enter the password:
password:
Confirm the password:
password:
Name of the library. Leave blank to access all non-encrypted libraries.
Enter a string value. Press Enter for the default ("").
library>
Library password (for encrypted libraries only). Leave blank if you pass it through the command line.
y) Yes type in my own password
g) Generate random password
n) No leave this optional password blank (default)
y/g/n> n
Edit advanced config? (y/n)
y) Yes
n) No (default)
y/n> n
Remote config
--------------------
[seafile]
type = seafile
url = http://my.seafile.server/
user = me@example.com
password = *** ENCRYPTED ***
--------------------
y) Yes this is OK (default)
e) Edit this remote
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
See all libraries
rclone lsd seafile:
Create a new library
rclone mkdir seafile:library
List the contents of a library
rclone ls seafile:library
Sync `/home/local/directory` to the remote library, deleting any
excess files in the library.
rclone sync /home/local/directory seafile:library
### Configuration in library mode ###
```
No remotes found - make a new one
n) New remote
s) Set configuration password
q) Quit config
n/s/q> n
name> seafile
Type of storage to configure.
Enter a string value. Press Enter for the default ("").
Choose a number from below, or type in your own value
[snip]
XX / Seafile
\ "seafile"
[snip]
Storage> seafile
** See help for seafile backend at: https://rclone.org/seafile/ **
URL of seafile host to connect to
Enter a string value. Press Enter for the default ("").
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
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
y/g> y
Enter the password:
password:
Confirm the password:
password:
Name of the library. Leave blank to access all non-encrypted libraries.
Enter a string value. Press Enter for the default ("").
library> My Library
Library password (for encrypted libraries only). Leave blank if you pass it through the command line.
y) Yes type in my own password
g) Generate random password
n) No leave this optional password blank (default)
y/g/n> n
Edit advanced config? (y/n)
y) Yes
n) No (default)
y/n> n
Remote config
--------------------
[seafile]
type = seafile
url = http://my.seafile.server/
user = me@example.com
password = *** ENCRYPTED ***
library = My Library
--------------------
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote
y/e/d> y
```
You specified `My Library` during the configuration. The root of the remote is pointing at the
root of the library `My Library`:
See all files in the library:
rclone lsd seafile:
Create a new directory inside the library
rclone mkdir seafile:directory
List the contents of a directory
rclone ls seafile:directory
Sync `/home/local/directory` to the remote library, deleting any
excess files in the library.
rclone sync /home/local/directory seafile:
### --fast-list ###
Seafile version 7+ supports `--fast-list` which allows you to use fewer
transactions in exchange for more memory. See the [rclone
docs](/docs/#fast-list) for more details.
Please note this is not supported on seafile server version 6.x
#### Restricted filename characters
In addition to the [default restricted characters set](/overview/#restricted-characters)
the following characters are also replaced:
| Character | Value | Replacement |
| --------- |:-----:|:-----------:|
| / | 0x2F | |
| " | 0x22 | |
| \ | 0x5C | |
Invalid UTF-8 bytes will also be [replaced](/overview/#invalid-utf8),
as they can't be used in JSON strings.
### Seafile and rclone link ###
Rclone supports generating share links for non-encrypted libraries only.
They can either be for a file or a directory:
```
rclone link seafile:seafile-tutorial.doc
http://my.seafile.server/f/fdcd8a2f93f84b8b90f4/
```
or if run on a directory you will get:
```
rclone link seafile:dir
http://my.seafile.server/d/9ea2455f6f55478bbb0d/
```
Please note a share link is unique for each file or directory. If you run a link command on a file/dir
that has already been shared, you will get the exact same link.
### Compatibility ###
It has been actively tested using the [seafile docker image](https://github.com/haiwen/seafile-docker) of these versions:
- 6.3.4 community edition
- 7.0.5 community edition
- 7.1.3 community edition
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 -->
<!--- autogenerated options stop -->

View File

@ -86,6 +86,7 @@
<li><a href="/pcloud/"><i class="fa fa-cloud"></i> pCloud</a></li> <li><a href="/pcloud/"><i class="fa fa-cloud"></i> pCloud</a></li>
<li><a href="/premiumizeme/"><i class="fa fa-user"></i> premiumize.me</a></li> <li><a href="/premiumizeme/"><i class="fa fa-user"></i> premiumize.me</a></li>
<li><a href="/putio/"><i class="fas fa-parking"></i> put.io</a></li> <li><a href="/putio/"><i class="fas fa-parking"></i> put.io</a></li>
<li><a href="/seafile/"><i class="fa fa-server"></i> Seafile</a></li>
<li><a href="/sftp/"><i class="fa fa-server"></i> SFTP</a></li> <li><a href="/sftp/"><i class="fa fa-server"></i> SFTP</a></li>
<li><a href="/sugarsync/"><i class="fas fa-dove"></i> SugarSync</a></li> <li><a href="/sugarsync/"><i class="fas fa-dove"></i> SugarSync</a></li>
<li><a href="/union/"><i class="fa fa-link"></i> Union (merge backends)</a></li> <li><a href="/union/"><i class="fa fa-link"></i> Union (merge backends)</a></li>

View File

@ -264,3 +264,14 @@ backends:
remote: "TestMailru:" remote: "TestMailru:"
subdir: false subdir: false
fastlist: false fastlist: false
- backend: "seafile"
remote: "TestSeafileV6:"
fastlist: false
ignore:
- TestIntegration/FsMkdir/FsPutFiles/FsDirMove
- backend: "seafile"
remote: "TestSeafile:"
fastlist: true
- backend: "seafile"
remote: "TestSeafileEncrypted:"
fastlist: true

View File

@ -0,0 +1,60 @@
#!/bin/bash
set -e
# environment variables passed on docker-compose
export NAME=seafile7
export MYSQL_ROOT_PASSWORD=pixenij4zacoguq0kopamid6
export SEAFILE_ADMIN_EMAIL=seafile@rclone.org
export SEAFILE_ADMIN_PASSWORD=pixenij4zacoguq0kopamid6
export SEAFILE_IP=127.0.0.1
export SEAFILE_PORT=8087
export SEAFILE_TEST_DATA=${SEAFILE_TEST_DATA:-/tmp/seafile-test-data}
export SEAFILE_VERSION=latest
# make sure the data directory exists
mkdir -p ${SEAFILE_TEST_DATA}/${NAME}
# docker-compose project directory
COMPOSE_DIR=$(dirname "$0")/seafile
start() {
docker-compose --project-directory ${COMPOSE_DIR} --project-name ${NAME} --file ${COMPOSE_DIR}/docker-compose.yml up -d
# it takes some time for the database to be created
sleep 60
# authentication token answer should be like: {"token":"dbf58423f1632b5b679a13b0929f1d0751d9250c"}
TOKEN=`curl --silent \
--data-urlencode username=${SEAFILE_ADMIN_EMAIL} -d password=${SEAFILE_ADMIN_PASSWORD} \
http://${SEAFILE_IP}:${SEAFILE_PORT}/api2/auth-token/ \
| sed 's/^{"token":"\(.*\)"}$/\1/'`
# create default library
curl -X POST -H "Authorization: Token ${TOKEN}" "http://${SEAFILE_IP}:${SEAFILE_PORT}/api2/default-repo/"
echo _connect=${SEAFILE_IP}:${SEAFILE_PORT}
echo type=seafile
echo url=http://${SEAFILE_IP}:${SEAFILE_PORT}/
echo user=${SEAFILE_ADMIN_EMAIL}
echo pass=$(rclone obscure ${SEAFILE_ADMIN_PASSWORD})
echo library=My Library
}
stop() {
if status ; then
docker-compose --project-directory ${COMPOSE_DIR} --project-name ${NAME} --file ${COMPOSE_DIR}/docker-compose.yml down
fi
}
status() {
if docker ps --format "{{.Names}}" | grep ^${NAME}_seafile_1$ >/dev/null ; then
echo "$NAME running"
else
echo "$NAME not running"
return 1
fi
return 0
}
. $(dirname "$0")/run.bash

View File

@ -0,0 +1,65 @@
#!/bin/bash
set -e
# local variables
TEST_LIBRARY=Encrypted
TEST_LIBRARY_PASSWORD=SecretKey
# environment variables passed on docker-compose
export NAME=seafile7encrypted
export MYSQL_ROOT_PASSWORD=pixenij4zacoguq0kopamid6
export SEAFILE_ADMIN_EMAIL=seafile@rclone.org
export SEAFILE_ADMIN_PASSWORD=pixenij4zacoguq0kopamid6
export SEAFILE_IP=127.0.0.1
export SEAFILE_PORT=8088
export SEAFILE_TEST_DATA=${SEAFILE_TEST_DATA:-/tmp/seafile-test-data}
export SEAFILE_VERSION=latest
# make sure the data directory exists
mkdir -p ${SEAFILE_TEST_DATA}/${NAME}
# docker-compose project directory
COMPOSE_DIR=$(dirname "$0")/seafile
start() {
docker-compose --project-directory ${COMPOSE_DIR} --project-name ${NAME} --file ${COMPOSE_DIR}/docker-compose.yml up -d
# it takes some time for the database to be created
sleep 60
# authentication token answer should be like: {"token":"dbf58423f1632b5b679a13b0929f1d0751d9250c"}
TOKEN=`curl --silent \
--data-urlencode username=${SEAFILE_ADMIN_EMAIL} -d password=${SEAFILE_ADMIN_PASSWORD} \
http://${SEAFILE_IP}:${SEAFILE_PORT}/api2/auth-token/ \
| sed 's/^{"token":"\(.*\)"}$/\1/'`
# create encrypted library
curl -X POST -d "name=${TEST_LIBRARY}&passwd=${TEST_LIBRARY_PASSWORD}" -H "Authorization: Token ${TOKEN}" "http://${SEAFILE_IP}:${SEAFILE_PORT}/api2/repos/"
echo _connect=${SEAFILE_IP}:${SEAFILE_PORT}
echo type=seafile
echo url=http://${SEAFILE_IP}:${SEAFILE_PORT}/
echo user=${SEAFILE_ADMIN_EMAIL}
echo pass=$(rclone obscure ${SEAFILE_ADMIN_PASSWORD})
echo library=${TEST_LIBRARY}
echo library_key=$(rclone obscure ${TEST_LIBRARY_PASSWORD})
}
stop() {
if status ; then
docker-compose --project-directory ${COMPOSE_DIR} --project-name ${NAME} --file ${COMPOSE_DIR}/docker-compose.yml down
fi
}
status() {
if docker ps --format "{{.Names}}" | grep ^${NAME}_seafile_1$ >/dev/null ; then
echo "$NAME running"
else
echo "$NAME not running"
return 1
fi
return 0
}
. $(dirname "$0")/run.bash

View File

@ -0,0 +1,48 @@
#!/bin/bash
set -e
# local variables
NAME=seafile6
SEAFILE_IP=127.0.0.1
SEAFILE_PORT=8086
SEAFILE_ADMIN_EMAIL=seafile@rclone.org
SEAFILE_ADMIN_PASSWORD=qebiwob7wafixif8sojiboj4
SEAFILE_TEST_DATA=${SEAFILE_TEST_DATA:-/tmp/seafile-test-data}
SEAFILE_VERSION=latest
. $(dirname "$0")/docker.bash
start() {
# make sure the data directory exists
mkdir -p ${SEAFILE_TEST_DATA}/${NAME}
docker run --rm -d --name $NAME \
-e SEAFILE_SERVER_HOSTNAME=${SEAFILE_IP}:${SEAFILE_PORT} \
-e SEAFILE_ADMIN_EMAIL=${SEAFILE_ADMIN_EMAIL} \
-e SEAFILE_ADMIN_PASSWORD=${SEAFILE_ADMIN_PASSWORD} \
-v ${SEAFILE_TEST_DATA}/${NAME}:/shared \
-p ${SEAFILE_IP}:${SEAFILE_PORT}:80 \
seafileltd/seafile:${SEAFILE_VERSION}
# it takes some time for the database to be created
sleep 60
# authentication token answer should be like: {"token":"dbf58423f1632b5b679a13b0929f1d0751d9250c"}
TOKEN=`curl --silent \
--data-urlencode username=${SEAFILE_ADMIN_EMAIL} -d password=${SEAFILE_ADMIN_PASSWORD} \
http://${SEAFILE_IP}:${SEAFILE_PORT}/api2/auth-token/ \
| sed 's/^{"token":"\(.*\)"}$/\1/'`
# create default library
curl -X POST -H "Authorization: Token ${TOKEN}" "http://${SEAFILE_IP}:${SEAFILE_PORT}/api2/default-repo/"
echo _connect=${SEAFILE_IP}:${SEAFILE_PORT}
echo type=seafile
echo url=http://${SEAFILE_IP}:${SEAFILE_PORT}/
echo user=${SEAFILE_ADMIN_EMAIL}
echo pass=$(rclone obscure ${SEAFILE_ADMIN_PASSWORD})
echo library=My Library
}
. $(dirname "$0")/run.bash

View File

@ -0,0 +1,31 @@
version: '2.0'
services:
db:
image: mariadb:10.1
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_LOG_CONSOLE=true
volumes:
- ${SEAFILE_TEST_DATA}/${NAME}/seafile-mysql/db:/var/lib/mysql
memcached:
image: memcached:1.5.6
entrypoint: memcached -m 256
seafile:
image: seafileltd/seafile-mc:${SEAFILE_VERSION}
ports:
- "${SEAFILE_IP}:${SEAFILE_PORT}:80"
volumes:
- ${SEAFILE_TEST_DATA}/${NAME}/seafile-data:/shared
environment:
- DB_HOST=db
- DB_ROOT_PASSWD=${MYSQL_ROOT_PASSWORD}
- TIME_ZONE=Etc/UTC
- SEAFILE_ADMIN_EMAIL=${SEAFILE_ADMIN_EMAIL}
- SEAFILE_ADMIN_PASSWORD=${SEAFILE_ADMIN_PASSWORD}
- SEAFILE_SERVER_LETSENCRYPT=false
- SEAFILE_SERVER_HOSTNAME=${SEAFILE_IP}:${SEAFILE_PORT}
depends_on:
- db
- memcached