rclone/backend/seafile/webapi.go
Nick Craig-Wood e43b5ce5e5 Remove github.com/pkg/errors and replace with std library version
This is possible now that we no longer support go1.12 and brings
rclone into line with standard practices in the Go world.

This also removes errors.New and errors.Errorf from lib/errors and
prefers the stdlib errors package over lib/errors.
2021-11-07 11:53:30 +00:00

1096 lines
33 KiB
Go

package seafile
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"path"
"strings"
"github.com/rclone/rclone/backend/seafile/api"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/lib/readers"
"github.com/rclone/rclone/lib/rest"
)
// Start of the API URLs
const (
APIv20 = "api2/repos/"
APIv21 = "api/v2.1/repos/"
)
// Errors specific to seafile fs
var (
ErrorInternalDuringUpload = errors.New("Internal server error during file upload")
)
// ==================== Seafile API ====================
func (f *Fs) getAuthorizationToken(ctx context.Context) (string, error) {
return getAuthorizationToken(ctx, f.srv, f.opt.User, f.opt.Password, "")
}
// getAuthorizationToken can be called outside of an 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: user,
Password: password,
}
result := api.AuthenticationResult{}
_, err := srv.CallJSON(ctx, &opts, &request, &result)
if err != nil {
// This is only going to be http errors here
return "", fmt.Errorf("failed to authenticate: %w", err)
}
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
}
func (f *Fs) getServerInfo(ctx context.Context) (account *api.ServerInfo, err error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/server-info.md#user-content-Get%20Server%20Information
opts := rest.Opts{
Method: "GET",
Path: "api2/server-info/",
}
result := api.ServerInfo{}
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)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied
}
}
return nil, fmt.Errorf("failed to get server info: %w", err)
}
return &result, nil
}
func (f *Fs) getUserAccountInfo(ctx context.Context) (account *api.AccountInfo, err error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/account.md#user-content-Check%20Account%20Info
opts := rest.Opts{
Method: "GET",
Path: "api2/account/info/",
}
result := 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)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied
}
}
return nil, fmt.Errorf("failed to get account info: %w", err)
}
return &result, nil
}
func (f *Fs) getLibraries(ctx context.Context) ([]api.Library, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-List%20Libraries
opts := rest.Opts{
Method: "GET",
Path: APIv20,
}
result := make([]api.Library, 1)
var resp *http.Response
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)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied
}
}
return nil, fmt.Errorf("failed to get libraries: %w", err)
}
return result, nil
}
func (f *Fs) createLibrary(ctx context.Context, libraryName, password string) (library *api.CreateLibrary, err error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Create%20Library
opts := rest.Opts{
Method: "POST",
Path: APIv20,
}
request := api.CreateLibraryRequest{
Name: f.opt.Enc.FromStandardName(libraryName),
Description: "Created by rclone",
Password: password,
}
result := &api.CreateLibrary{}
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)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied
}
}
return nil, fmt.Errorf("failed to create library: %w", err)
}
return result, nil
}
func (f *Fs) deleteLibrary(ctx context.Context, libraryID string) error {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Create%20Library
opts := rest.Opts{
Method: "DELETE",
Path: APIv20 + libraryID + "/",
}
result := ""
var resp *http.Response
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)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied
}
}
return fmt.Errorf("failed to delete library: %w", err)
}
return nil
}
func (f *Fs) decryptLibrary(ctx context.Context, libraryID, password string) error {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/library-encryption.md#user-content-Decrypt%20Library
if libraryID == "" {
return errors.New("cannot list files without a library")
}
// This is another call that cannot accept a JSON input so we have to build it manually
opts := rest.Opts{
Method: "POST",
Path: APIv20 + libraryID + "/",
ContentType: "application/x-www-form-urlencoded",
Body: bytes.NewBuffer([]byte("password=" + f.opt.Enc.FromStandardName(password))),
NoResponse: true,
}
var resp *http.Response
var err error
err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.Call(ctx, &opts)
return f.shouldRetry(ctx, resp, err)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 400 {
return errors.New("incorrect password")
}
if resp.StatusCode == 409 {
fs.Debugf(nil, "library is not encrypted")
return nil
}
}
return fmt.Errorf("failed to decrypt library: %w", err)
}
return nil
}
func (f *Fs) getDirectoryEntriesAPIv21(ctx context.Context, libraryID, dirPath string, recursive bool) ([]api.DirEntry, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-List%20Items%20in%20Directory
// This is using the undocumented version 2.1 of the API (so we can use the recursive option which is not available in the version 2)
if libraryID == "" {
return nil, errors.New("cannot list files without a library")
}
dirPath = path.Join("/", dirPath)
recursiveFlag := "0"
if recursive {
recursiveFlag = "1"
}
opts := rest.Opts{
Method: "GET",
Path: APIv21 + libraryID + "/dir/",
Parameters: url.Values{
"recursive": {recursiveFlag},
"p": {f.opt.Enc.FromStandardPath(dirPath)},
},
}
result := &api.DirEntries{}
var resp *http.Response
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)
})
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
}
if resp.StatusCode == 440 {
// Encrypted library and password not provided
return nil, fs.ErrorPermissionDenied
}
}
return nil, fmt.Errorf("failed to get directory contents: %w", err)
}
// Clean up encoded names
for index, fileInfo := range result.Entries {
fileInfo.Name = f.opt.Enc.ToStandardName(fileInfo.Name)
fileInfo.Path = f.opt.Enc.ToStandardPath(fileInfo.Path)
result.Entries[index] = fileInfo
}
return result.Entries, nil
}
func (f *Fs) getDirectoryDetails(ctx context.Context, libraryID, dirPath string) (*api.DirectoryDetail, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Get%20Directory%20Detail
if libraryID == "" {
return nil, errors.New("cannot read directory without a library")
}
dirPath = path.Join("/", dirPath)
opts := rest.Opts{
Method: "GET",
Path: APIv21 + libraryID + "/dir/detail/",
Parameters: url.Values{"path": {f.opt.Enc.FromStandardPath(dirPath)}},
}
result := &api.DirectoryDetail{}
var resp *http.Response
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)
})
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
}
}
return nil, fmt.Errorf("failed to get directory details: %w", err)
}
result.Name = f.opt.Enc.ToStandardName(result.Name)
result.Path = f.opt.Enc.ToStandardPath(result.Path)
return result, nil
}
// createDir creates a new directory. The API will add a number to the directory name if it already exist
func (f *Fs) createDir(ctx context.Context, libraryID, dirPath string) error {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Create%20New%20Directory
if libraryID == "" {
return errors.New("cannot create directory without a library")
}
dirPath = path.Join("/", dirPath)
// This call *cannot* handle json parameters in the body, so we have to build the request body manually
opts := rest.Opts{
Method: "POST",
Path: APIv20 + libraryID + "/dir/",
Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(dirPath)}},
NoRedirect: true,
ContentType: "application/x-www-form-urlencoded",
Body: bytes.NewBuffer([]byte("operation=mkdir")),
NoResponse: true,
}
var resp *http.Response
var err error
err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.Call(ctx, &opts)
return f.shouldRetry(ctx, resp, err)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied
}
}
return fmt.Errorf("failed to create directory: %w", err)
}
return nil
}
func (f *Fs) renameDir(ctx context.Context, libraryID, dirPath, newName string) error {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Rename%20Directory
if libraryID == "" {
return errors.New("cannot rename directory without a library")
}
dirPath = path.Join("/", dirPath)
// This call *cannot* handle json parameters in the body, so we have to build the request body manually
postParameters := url.Values{
"operation": {"rename"},
"newname": {f.opt.Enc.FromStandardPath(newName)},
}
opts := rest.Opts{
Method: "POST",
Path: APIv20 + libraryID + "/dir/",
Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(dirPath)}},
ContentType: "application/x-www-form-urlencoded",
Body: bytes.NewBuffer([]byte(postParameters.Encode())),
NoResponse: true,
}
var resp *http.Response
var err error
err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.Call(ctx, &opts)
return f.shouldRetry(ctx, resp, err)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied
}
}
return fmt.Errorf("failed to rename directory: %w", err)
}
return nil
}
func (f *Fs) moveDir(ctx context.Context, srcLibraryID, srcDir, srcName, dstLibraryID, dstPath string) error {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/files-directories-batch-op.md#user-content-Batch%20Move%20Items%20Synchronously
if srcLibraryID == "" || dstLibraryID == "" || srcName == "" {
return errors.New("libraryID and/or file path argument(s) missing")
}
srcDir = path.Join("/", srcDir)
dstPath = path.Join("/", dstPath)
opts := rest.Opts{
Method: "POST",
Path: APIv21 + "sync-batch-move-item/",
NoResponse: true,
}
request := &api.BatchSourceDestRequest{
SrcLibraryID: srcLibraryID,
SrcParentDir: f.opt.Enc.FromStandardPath(srcDir),
SrcItems: []string{f.opt.Enc.FromStandardPath(srcName)},
DstLibraryID: dstLibraryID,
DstParentDir: f.opt.Enc.FromStandardPath(dstPath),
}
var resp *http.Response
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)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied
}
if resp.StatusCode == 404 {
return fs.ErrorObjectNotFound
}
}
return fmt.Errorf("failed to move directory '%s' from '%s' to '%s': %w", srcName, srcDir, dstPath, err)
}
return nil
}
func (f *Fs) deleteDir(ctx context.Context, libraryID, filePath string) error {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Delete%20Directory
if libraryID == "" {
return errors.New("cannot delete directory without a library")
}
filePath = path.Join("/", filePath)
opts := rest.Opts{
Method: "DELETE",
Path: APIv20 + libraryID + "/dir/",
Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}},
NoResponse: true,
}
var resp *http.Response
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)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied
}
}
return fmt.Errorf("failed to delete directory: %w", err)
}
return nil
}
func (f *Fs) getFileDetails(ctx context.Context, libraryID, filePath string) (*api.FileDetail, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Get%20File%20Detail
if libraryID == "" {
return nil, errors.New("cannot open file without a library")
}
filePath = path.Join("/", filePath)
opts := rest.Opts{
Method: "GET",
Path: APIv20 + libraryID + "/file/detail/",
Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}},
}
result := &api.FileDetail{}
var resp *http.Response
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)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 404 {
return nil, fs.ErrorObjectNotFound
}
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied
}
}
return nil, fmt.Errorf("failed to get file details: %w", err)
}
result.Name = f.opt.Enc.ToStandardName(result.Name)
result.Parent = f.opt.Enc.ToStandardPath(result.Parent)
return result, nil
}
func (f *Fs) deleteFile(ctx context.Context, libraryID, filePath string) error {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Delete%20File
if libraryID == "" {
return errors.New("cannot delete file without a library")
}
filePath = path.Join("/", filePath)
opts := rest.Opts{
Method: "DELETE",
Path: APIv20 + libraryID + "/file/",
Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}},
NoResponse: true,
}
err := f.pacer.Call(func() (bool, error) {
resp, err := f.srv.CallJSON(ctx, &opts, nil, nil)
return f.shouldRetry(ctx, resp, err)
})
if err != nil {
return fmt.Errorf("failed to delete file: %w", err)
}
return nil
}
func (f *Fs) getDownloadLink(ctx context.Context, libraryID, filePath string) (string, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Download%20File
if libraryID == "" {
return "", errors.New("cannot download file without a library")
}
filePath = path.Join("/", filePath)
opts := rest.Opts{
Method: "GET",
Path: APIv20 + libraryID + "/file/",
Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}},
}
result := ""
var resp *http.Response
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)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 404 {
return "", fs.ErrorObjectNotFound
}
}
return "", fmt.Errorf("failed to get download link: %w", err)
}
return result, nil
}
func (f *Fs) download(ctx context.Context, url string, size int64, options ...fs.OpenOption) (io.ReadCloser, error) {
// Check if we need to download partial content
var start, end int64 = 0, size
partialContent := false
for _, option := range options {
switch x := option.(type) {
case *fs.SeekOption:
start = x.Offset
partialContent = true
case *fs.RangeOption:
if x.Start >= 0 {
start = x.Start
if x.End > 0 && x.End < size {
end = x.End + 1
}
} else {
// {-1, 20} should load the last 20 characters [len-20:len]
start = size - x.End
}
partialContent = true
default:
if option.Mandatory() {
fs.Logf(nil, "Unsupported mandatory option: %v", option)
}
}
}
// Build the http request
opts := rest.Opts{
Method: "GET",
RootURL: url,
Options: options,
}
var resp *http.Response
var err error
err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.Call(ctx, &opts)
return f.shouldRetry(ctx, resp, err)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 404 {
return nil, fmt.Errorf("file not found '%s'", url)
}
}
return nil, err
}
// Non-encrypted libraries are accepting the HTTP Range header,
// BUT encrypted libraries are simply ignoring it
if partialContent && resp.StatusCode == 200 {
// Partial content was requested through a Range header, but a full content was sent instead
rangeDownloadNotice.Do(func() {
fs.Logf(nil, "%s ignored our request of partial content. This is probably because encrypted libraries are not accepting range requests. Loading this file might be slow!", f.String())
})
if start > 0 {
// We need to read and discard the beginning of the data...
_, err = io.CopyN(ioutil.Discard, resp.Body, start)
if err != nil {
return nil, err
}
}
// ... and return a limited reader for the remaining of the data
return readers.NewLimitedReadCloser(resp.Body, end-start), nil
}
return resp.Body, nil
}
func (f *Fs) getUploadLink(ctx context.Context, libraryID string) (string, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/file-upload.md
if libraryID == "" {
return "", errors.New("cannot upload file without a library")
}
opts := rest.Opts{
Method: "GET",
Path: APIv20 + libraryID + "/upload-link/",
}
result := ""
var resp *http.Response
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)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return "", fs.ErrorPermissionDenied
}
}
return "", fmt.Errorf("failed to get upload link: %w", err)
}
return result, nil
}
func (f *Fs) upload(ctx context.Context, in io.Reader, uploadLink, filePath string) (*api.FileDetail, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/file-upload.md
fileDir, filename := path.Split(filePath)
parameters := url.Values{
"parent_dir": {"/"},
"relative_path": {f.opt.Enc.FromStandardPath(fileDir)},
"need_idx_progress": {"true"},
"replace": {"1"},
}
formReader, contentType, _, err := rest.MultipartUpload(ctx, in, parameters, "file", f.opt.Enc.FromStandardName(filename))
if err != nil {
return nil, fmt.Errorf("failed to make multipart upload: %w", err)
}
opts := rest.Opts{
Method: "POST",
RootURL: uploadLink,
Body: formReader,
ContentType: contentType,
Parameters: url.Values{"ret-json": {"1"}}, // It needs to be on the url, not in the body parameters
}
result := make([]api.FileDetail, 1)
var resp *http.Response
// If an error occurs during the call, do not attempt to retry: The upload link is single use only
err = f.pacer.CallNoRetry(func() (bool, error) {
resp, err = f.srv.CallJSON(ctx, &opts, nil, &result)
return f.shouldRetryUpload(ctx, resp, err)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied
}
if resp.StatusCode == 500 {
// This is a temporary error - we will get a new upload link before retrying
return nil, ErrorInternalDuringUpload
}
}
return nil, fmt.Errorf("failed to upload file: %w", err)
}
if len(result) > 0 {
result[0].Parent = f.opt.Enc.ToStandardPath(result[0].Parent)
result[0].Name = f.opt.Enc.ToStandardName(result[0].Name)
return &result[0], nil
}
return nil, nil
}
func (f *Fs) listShareLinks(ctx context.Context, libraryID, remote string) ([]api.SharedLink, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-List%20Share%20Link%20of%20a%20Folder%20(File)
if libraryID == "" {
return nil, errors.New("cannot get share links without a library")
}
remote = path.Join("/", remote)
opts := rest.Opts{
Method: "GET",
Path: "api/v2.1/share-links/",
Parameters: url.Values{"repo_id": {libraryID}, "path": {f.opt.Enc.FromStandardPath(remote)}},
}
result := make([]api.SharedLink, 1)
var resp *http.Response
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)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied
}
if resp.StatusCode == 404 {
return nil, fs.ErrorObjectNotFound
}
}
return nil, fmt.Errorf("failed to list shared links: %w", err)
}
return result, nil
}
// createShareLink will only work with non-encrypted libraries
func (f *Fs) createShareLink(ctx context.Context, libraryID, remote string) (*api.SharedLink, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-Create%20Share%20Link
if libraryID == "" {
return nil, errors.New("cannot create a shared link without a library")
}
remote = path.Join("/", remote)
opts := rest.Opts{
Method: "POST",
Path: "api/v2.1/share-links/",
}
request := &api.ShareLinkRequest{
LibraryID: libraryID,
Path: f.opt.Enc.FromStandardPath(remote),
}
result := &api.SharedLink{}
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.shouldRetry(ctx, 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.ErrorObjectNotFound
}
}
return nil, fmt.Errorf("failed to create a shared link: %w", err)
}
return result, nil
}
func (f *Fs) copyFile(ctx context.Context, srcLibraryID, srcPath, dstLibraryID, dstPath string) (*api.FileInfo, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Copy%20File
// It's using the api/v2.1 which is not in the documentation (as of Apr 2020) but works better than api2
if srcLibraryID == "" || dstLibraryID == "" {
return nil, errors.New("libraryID and/or file path argument(s) missing")
}
srcPath = path.Join("/", srcPath)
dstPath = path.Join("/", dstPath)
opts := rest.Opts{
Method: "POST",
Path: APIv21 + srcLibraryID + "/file/",
Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(srcPath)}},
}
request := &api.FileOperationRequest{
Operation: api.CopyFileOperation,
DestinationLibraryID: dstLibraryID,
DestinationPath: f.opt.Enc.FromStandardPath(dstPath),
}
result := &api.FileInfo{}
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.shouldRetry(ctx, resp, err)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied
}
if resp.StatusCode == 404 {
fs.Debugf(nil, "Copy: %s", err)
return nil, fs.ErrorObjectNotFound
}
}
return nil, fmt.Errorf("failed to copy file %s:'%s' to %s:'%s': %w", srcLibraryID, srcPath, dstLibraryID, dstPath, err)
}
return f.decodeFileInfo(result), nil
}
func (f *Fs) moveFile(ctx context.Context, srcLibraryID, srcPath, dstLibraryID, dstPath string) (*api.FileInfo, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Move%20File
// It's using the api/v2.1 which is not in the documentation (as of Apr 2020) but works better than api2
if srcLibraryID == "" || dstLibraryID == "" {
return nil, errors.New("libraryID and/or file path argument(s) missing")
}
srcPath = path.Join("/", srcPath)
dstPath = path.Join("/", dstPath)
opts := rest.Opts{
Method: "POST",
Path: APIv21 + srcLibraryID + "/file/",
Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(srcPath)}},
}
request := &api.FileOperationRequest{
Operation: api.MoveFileOperation,
DestinationLibraryID: dstLibraryID,
DestinationPath: f.opt.Enc.FromStandardPath(dstPath),
}
result := &api.FileInfo{}
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.shouldRetry(ctx, resp, err)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied
}
if resp.StatusCode == 404 {
fs.Debugf(nil, "Move: %s", err)
return nil, fs.ErrorObjectNotFound
}
}
return nil, fmt.Errorf("failed to move file %s:'%s' to %s:'%s': %w", srcLibraryID, srcPath, dstLibraryID, dstPath, err)
}
return f.decodeFileInfo(result), nil
}
func (f *Fs) renameFile(ctx context.Context, libraryID, filePath, newname string) (*api.FileInfo, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Rename%20File
// It's using the api/v2.1 which is not in the documentation (as of Apr 2020) but works better than api2
if libraryID == "" || newname == "" {
return nil, errors.New("libraryID and/or file path argument(s) missing")
}
filePath = path.Join("/", filePath)
opts := rest.Opts{
Method: "POST",
Path: APIv21 + libraryID + "/file/",
Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}},
}
request := &api.FileOperationRequest{
Operation: api.RenameFileOperation,
NewName: f.opt.Enc.FromStandardName(newname),
}
result := &api.FileInfo{}
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.shouldRetry(ctx, resp, err)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied
}
if resp.StatusCode == 404 {
fs.Debugf(nil, "Rename: %s", err)
return nil, fs.ErrorObjectNotFound
}
}
return nil, fmt.Errorf("failed to rename file '%s' to '%s': %w", filePath, newname, err)
}
return f.decodeFileInfo(result), nil
}
func (f *Fs) decodeFileInfo(input *api.FileInfo) *api.FileInfo {
input.Name = f.opt.Enc.ToStandardName(input.Name)
input.Path = f.opt.Enc.ToStandardPath(input.Path)
return input
}
func (f *Fs) emptyLibraryTrash(ctx context.Context, libraryID string) error {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Clean%20Library%20Trash
if libraryID == "" {
return errors.New("cannot clean up trash without a library")
}
opts := rest.Opts{
Method: "DELETE",
Path: APIv21 + libraryID + "/trash/",
NoResponse: true,
}
var resp *http.Response
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)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied
}
if resp.StatusCode == 404 {
return fs.ErrorObjectNotFound
}
}
return fmt.Errorf("failed empty the library trash: %w", err)
}
return nil
}
// === API v2 from the official documentation, but that have been replaced by the much better v2.1 (undocumented as of Apr 2020)
// === getDirectoryEntriesAPIv2 is needed to keep compatibility with seafile v6,
// === the others can probably be removed after the API v2.1 is documented
func (f *Fs) getDirectoryEntriesAPIv2(ctx context.Context, libraryID, dirPath string) ([]api.DirEntry, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-List%20Items%20in%20Directory
if libraryID == "" {
return nil, errors.New("cannot list files without a library")
}
dirPath = path.Join("/", dirPath)
opts := rest.Opts{
Method: "GET",
Path: APIv20 + libraryID + "/dir/",
Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(dirPath)}},
}
result := make([]api.DirEntry, 1)
var resp *http.Response
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)
})
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
}
if resp.StatusCode == 440 {
// Encrypted library and password not provided
return nil, fs.ErrorPermissionDenied
}
}
return nil, fmt.Errorf("failed to get directory contents: %w", err)
}
// Clean up encoded names
for index, fileInfo := range result {
fileInfo.Name = f.opt.Enc.ToStandardName(fileInfo.Name)
fileInfo.Path = f.opt.Enc.ToStandardPath(fileInfo.Path)
result[index] = fileInfo
}
return result, nil
}
func (f *Fs) copyFileAPIv2(ctx context.Context, srcLibraryID, srcPath, dstLibraryID, dstPath string) (*api.FileInfo, error) {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Copy%20File
if srcLibraryID == "" || dstLibraryID == "" {
return nil, errors.New("libraryID and/or file path argument(s) missing")
}
srcPath = path.Join("/", srcPath)
dstPath = path.Join("/", dstPath)
// Older API does not seem to accept JSON input here either
postParameters := url.Values{
"operation": {"copy"},
"dst_repo": {dstLibraryID},
"dst_dir": {f.opt.Enc.FromStandardPath(dstPath)},
}
opts := rest.Opts{
Method: "POST",
Path: APIv20 + srcLibraryID + "/file/",
Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(srcPath)}},
ContentType: "application/x-www-form-urlencoded",
Body: bytes.NewBuffer([]byte(postParameters.Encode())),
}
result := &api.FileInfo{}
var resp *http.Response
var err error
err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.Call(ctx, &opts)
return f.shouldRetry(ctx, resp, err)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return nil, fs.ErrorPermissionDenied
}
}
return nil, fmt.Errorf("failed to copy file %s:'%s' to %s:'%s': %w", srcLibraryID, srcPath, dstLibraryID, dstPath, err)
}
err = rest.DecodeJSON(resp, &result)
if err != nil {
return nil, err
}
return f.decodeFileInfo(result), nil
}
func (f *Fs) renameFileAPIv2(ctx context.Context, libraryID, filePath, newname string) error {
// API Documentation
// https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Rename%20File
if libraryID == "" || newname == "" {
return errors.New("libraryID and/or file path argument(s) missing")
}
filePath = path.Join("/", filePath)
// No luck with JSON input with the older api2
postParameters := url.Values{
"operation": {"rename"},
"reloaddir": {"true"}, // This is an undocumented trick to avoid an http code 301 response (found in https://github.com/haiwen/seahub/blob/master/seahub/api2/views.py)
"newname": {f.opt.Enc.FromStandardName(newname)},
}
opts := rest.Opts{
Method: "POST",
Path: APIv20 + libraryID + "/file/",
Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}},
ContentType: "application/x-www-form-urlencoded",
Body: bytes.NewBuffer([]byte(postParameters.Encode())),
NoRedirect: true,
NoResponse: true,
}
var resp *http.Response
var err error
err = f.pacer.Call(func() (bool, error) {
resp, err = f.srv.Call(ctx, &opts)
return f.shouldRetry(ctx, resp, err)
})
if err != nil {
if resp != nil {
if resp.StatusCode == 301 {
// This is the normal response from the server
return nil
}
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return fs.ErrorPermissionDenied
}
if resp.StatusCode == 404 {
return fs.ErrorObjectNotFound
}
}
return fmt.Errorf("failed to rename file: %w", err)
}
return nil
}