mirror of
https://github.com/rclone/rclone.git
synced 2025-08-19 09:52:05 +02:00
Add FileLu cloud storage backend
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
|||||||
_ "github.com/rclone/rclone/backend/dropbox"
|
_ "github.com/rclone/rclone/backend/dropbox"
|
||||||
_ "github.com/rclone/rclone/backend/fichier"
|
_ "github.com/rclone/rclone/backend/fichier"
|
||||||
_ "github.com/rclone/rclone/backend/filefabric"
|
_ "github.com/rclone/rclone/backend/filefabric"
|
||||||
|
_ "github.com/rclone/rclone/backend/filelu"
|
||||||
_ "github.com/rclone/rclone/backend/filescom"
|
_ "github.com/rclone/rclone/backend/filescom"
|
||||||
_ "github.com/rclone/rclone/backend/ftp"
|
_ "github.com/rclone/rclone/backend/ftp"
|
||||||
_ "github.com/rclone/rclone/backend/gofile"
|
_ "github.com/rclone/rclone/backend/gofile"
|
||||||
|
81
backend/filelu/api/types.go
Normal file
81
backend/filelu/api/types.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// Package api defines types for interacting with the FileLu API.
|
||||||
|
package api
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
// CreateFolderResponse represents the response for creating a folder.
|
||||||
|
type CreateFolderResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Result struct {
|
||||||
|
FldID interface{} `json:"fld_id"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteFolderResponse represents the response for deleting a folder.
|
||||||
|
type DeleteFolderResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FolderListResponse represents the response for listing folders.
|
||||||
|
type FolderListResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Result struct {
|
||||||
|
Files []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
FldID json.Number `json:"fld_id"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
FileCode string `json:"file_code"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
} `json:"files"`
|
||||||
|
Folders []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
FldID json.Number `json:"fld_id"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
} `json:"folders"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileDirectLinkResponse represents the response for a direct link to a file.
|
||||||
|
type FileDirectLinkResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Result struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileInfoResponse represents the response for file information.
|
||||||
|
type FileInfoResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Result []struct {
|
||||||
|
Size string `json:"size"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
FileCode string `json:"filecode"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteFileResponse represents the response for deleting a file.
|
||||||
|
type DeleteFileResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountInfoResponse represents the response for account information.
|
||||||
|
type AccountInfoResponse struct {
|
||||||
|
Status int `json:"status"` // HTTP status code of the response.
|
||||||
|
Msg string `json:"msg"` // Message describing the response.
|
||||||
|
Result struct {
|
||||||
|
PremiumExpire string `json:"premium_expire"` // Expiration date of premium access.
|
||||||
|
Email string `json:"email"` // User's email address.
|
||||||
|
UType string `json:"utype"` // User type (e.g., premium or free).
|
||||||
|
Storage string `json:"storage"` // Total storage available to the user.
|
||||||
|
StorageUsed string `json:"storage_used"` // Amount of storage used.
|
||||||
|
} `json:"result"` // Nested result structure containing account details.
|
||||||
|
}
|
366
backend/filelu/filelu.go
Normal file
366
backend/filelu/filelu.go
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
// Package filelu provides an interface to the FileLu storage system.
|
||||||
|
package filelu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rclone/rclone/fs"
|
||||||
|
"github.com/rclone/rclone/fs/config"
|
||||||
|
"github.com/rclone/rclone/fs/config/configmap"
|
||||||
|
"github.com/rclone/rclone/fs/config/configstruct"
|
||||||
|
"github.com/rclone/rclone/fs/fshttp"
|
||||||
|
"github.com/rclone/rclone/lib/encoder"
|
||||||
|
"github.com/rclone/rclone/lib/pacer"
|
||||||
|
"github.com/rclone/rclone/lib/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register the backend with Rclone
|
||||||
|
func init() {
|
||||||
|
fs.Register(&fs.RegInfo{
|
||||||
|
Name: "filelu",
|
||||||
|
Description: "FileLu Cloud Storage",
|
||||||
|
NewFs: NewFs,
|
||||||
|
Options: []fs.Option{{
|
||||||
|
Name: "key",
|
||||||
|
Help: "Your FileLu Rclone key from My Account",
|
||||||
|
Required: true,
|
||||||
|
Sensitive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: config.ConfigEncoding,
|
||||||
|
Help: config.ConfigEncodingHelp,
|
||||||
|
Advanced: true,
|
||||||
|
Default: (encoder.Base | // Slash,LtGt,DoubleQuote,Question,Asterisk,Pipe,Hash,Percent,BackSlash,Del,Ctl,RightSpace,InvalidUtf8,Dot
|
||||||
|
encoder.EncodeSlash |
|
||||||
|
encoder.EncodeLtGt |
|
||||||
|
encoder.EncodeExclamation |
|
||||||
|
encoder.EncodeDoubleQuote |
|
||||||
|
encoder.EncodeSingleQuote |
|
||||||
|
encoder.EncodeBackQuote |
|
||||||
|
encoder.EncodeQuestion |
|
||||||
|
encoder.EncodeDollar |
|
||||||
|
encoder.EncodeColon |
|
||||||
|
encoder.EncodeAsterisk |
|
||||||
|
encoder.EncodePipe |
|
||||||
|
encoder.EncodeHash |
|
||||||
|
encoder.EncodePercent |
|
||||||
|
encoder.EncodeBackSlash |
|
||||||
|
encoder.EncodeCrLf |
|
||||||
|
encoder.EncodeDel |
|
||||||
|
encoder.EncodeCtl |
|
||||||
|
encoder.EncodeLeftSpace |
|
||||||
|
encoder.EncodeLeftPeriod |
|
||||||
|
encoder.EncodeLeftTilde |
|
||||||
|
encoder.EncodeLeftCrLfHtVt |
|
||||||
|
encoder.EncodeRightPeriod |
|
||||||
|
encoder.EncodeRightCrLfHtVt |
|
||||||
|
encoder.EncodeSquareBracket |
|
||||||
|
encoder.EncodeSemicolon |
|
||||||
|
encoder.EncodeRightSpace |
|
||||||
|
encoder.EncodeInvalidUtf8 |
|
||||||
|
encoder.EncodeDot),
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options defines the configuration for the FileLu backend
|
||||||
|
type Options struct {
|
||||||
|
Key string `config:"key"`
|
||||||
|
Enc encoder.MultiEncoder `config:"encoding"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fs represents the FileLu file system
|
||||||
|
type Fs struct {
|
||||||
|
name string
|
||||||
|
root string
|
||||||
|
opt Options
|
||||||
|
features *fs.Features
|
||||||
|
endpoint string
|
||||||
|
pacer *pacer.Pacer
|
||||||
|
srv *rest.Client
|
||||||
|
client *http.Client
|
||||||
|
targetFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFs creates a new Fs object for FileLu
|
||||||
|
func NewFs(ctx context.Context, name string, root string, m configmap.Mapper) (fs.Fs, error) {
|
||||||
|
opt := new(Options)
|
||||||
|
err := configstruct.Set(m, opt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt.Key == "" {
|
||||||
|
return nil, fmt.Errorf("FileLu Rclone Key is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := fshttp.NewClient(ctx)
|
||||||
|
|
||||||
|
if strings.TrimSpace(root) == "" {
|
||||||
|
root = ""
|
||||||
|
}
|
||||||
|
root = strings.Trim(root, "/")
|
||||||
|
|
||||||
|
filename := ""
|
||||||
|
|
||||||
|
f := &Fs{
|
||||||
|
name: name,
|
||||||
|
opt: *opt,
|
||||||
|
endpoint: "https://filelu.com/rclone",
|
||||||
|
client: client,
|
||||||
|
srv: rest.NewClient(client).SetRoot("https://filelu.com/rclone"),
|
||||||
|
pacer: pacer.New(),
|
||||||
|
targetFile: filename,
|
||||||
|
root: root,
|
||||||
|
}
|
||||||
|
|
||||||
|
f.features = (&fs.Features{
|
||||||
|
CanHaveEmptyDirectories: true,
|
||||||
|
WriteMetadata: false,
|
||||||
|
SlowHash: true,
|
||||||
|
}).Fill(ctx, f)
|
||||||
|
|
||||||
|
rootContainer, rootDirectory := rootSplit(f.root)
|
||||||
|
if rootContainer != "" && rootDirectory != "" {
|
||||||
|
// Check to see if the (container,directory) is actually an existing file
|
||||||
|
oldRoot := f.root
|
||||||
|
newRoot, leaf := path.Split(oldRoot)
|
||||||
|
f.root = strings.Trim(newRoot, "/")
|
||||||
|
_, err := f.NewObject(ctx, leaf)
|
||||||
|
if err != nil {
|
||||||
|
if err == fs.ErrorObjectNotFound || err == fs.ErrorNotAFile {
|
||||||
|
// File doesn't exist or is a directory so return old f
|
||||||
|
f.root = strings.Trim(oldRoot, "/")
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// return an error with an fs which points to the parent
|
||||||
|
return f, fs.ErrorIsFile
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mkdir to create directory on remote server.
|
||||||
|
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
|
||||||
|
fullPath := path.Clean(f.root + "/" + dir)
|
||||||
|
_, err := f.createFolder(ctx, fullPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// About provides usage statistics for the remote
|
||||||
|
func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
|
||||||
|
accountInfo, err := f.getAccountInfo(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalStorage, err := parseStorageToBytes(accountInfo.Result.Storage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse total storage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
usedStorage, err := parseStorageToBytes(accountInfo.Result.StorageUsed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse used storage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &fs.Usage{
|
||||||
|
Total: fs.NewUsageValue(totalStorage), // Total bytes available
|
||||||
|
Used: fs.NewUsageValue(usedStorage), // Total bytes used
|
||||||
|
Free: fs.NewUsageValue(totalStorage - usedStorage),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purge deletes the directory and all its contents
|
||||||
|
func (f *Fs) Purge(ctx context.Context, dir string) error {
|
||||||
|
fullPath := path.Join(f.root, dir)
|
||||||
|
if fullPath != "" {
|
||||||
|
fullPath = "/" + strings.Trim(fullPath, "/")
|
||||||
|
}
|
||||||
|
return f.deleteFolder(ctx, fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns a list of files and folders
|
||||||
|
// List returns a list of files and folders for the given directory
|
||||||
|
func (f *Fs) List(ctx context.Context, dir string) (fs.DirEntries, error) {
|
||||||
|
// Compose full path for API call
|
||||||
|
fullPath := path.Join(f.root, dir)
|
||||||
|
fullPath = "/" + strings.Trim(fullPath, "/")
|
||||||
|
if fullPath == "/" {
|
||||||
|
fullPath = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries fs.DirEntries
|
||||||
|
result, err := f.getFolderList(ctx, fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fldMap := map[string]bool{}
|
||||||
|
for _, folder := range result.Result.Folders {
|
||||||
|
fldMap[folder.FldID.String()] = true
|
||||||
|
if f.root == "" && dir == "" && strings.Contains(folder.Path, "/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
paths := strings.Split(folder.Path, fullPath+"/")
|
||||||
|
remote := paths[0]
|
||||||
|
if len(paths) > 1 {
|
||||||
|
remote = paths[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(remote, "/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pathsWithoutRoot := strings.Split(folder.Path, "/"+f.root+"/")
|
||||||
|
remotePathWithoutRoot := pathsWithoutRoot[0]
|
||||||
|
if len(pathsWithoutRoot) > 1 {
|
||||||
|
remotePathWithoutRoot = pathsWithoutRoot[1]
|
||||||
|
}
|
||||||
|
remotePathWithoutRoot = strings.TrimPrefix(remotePathWithoutRoot, "/")
|
||||||
|
entries = append(entries, fs.NewDir(remotePathWithoutRoot, time.Now()))
|
||||||
|
}
|
||||||
|
for _, file := range result.Result.Files {
|
||||||
|
if _, ok := fldMap[file.FldID.String()]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
remote := path.Join(dir, file.Name)
|
||||||
|
// trim leading slashes
|
||||||
|
remote = strings.TrimPrefix(remote, "/")
|
||||||
|
obj := &Object{
|
||||||
|
fs: f,
|
||||||
|
remote: remote,
|
||||||
|
size: file.Size,
|
||||||
|
modTime: time.Now(),
|
||||||
|
}
|
||||||
|
entries = append(entries, obj)
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put uploads a file directly to the destination folder in the FileLu storage system.
|
||||||
|
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
||||||
|
if src.Size() == 0 {
|
||||||
|
return nil, fs.ErrorCantUploadEmptyFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
err := f.uploadFile(ctx, in, src.Remote())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newObject := &Object{
|
||||||
|
fs: f,
|
||||||
|
remote: src.Remote(),
|
||||||
|
size: src.Size(),
|
||||||
|
modTime: src.ModTime(ctx),
|
||||||
|
}
|
||||||
|
fs.Infof(f, "Put: Successfully uploaded new file %q", src.Remote())
|
||||||
|
return newObject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move moves the file to the specified location
|
||||||
|
func (f *Fs) Move(ctx context.Context, src fs.Object, destinationPath string) (fs.Object, error) {
|
||||||
|
|
||||||
|
if strings.HasPrefix(destinationPath, "/") || strings.Contains(destinationPath, ":\\") {
|
||||||
|
dir := path.Dir(destinationPath)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create destination directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := src.Open(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open source file: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := reader.Close(); err != nil {
|
||||||
|
fs.Logf(nil, "Failed to close file body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
dest, err := os.Create(destinationPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create destination file: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := dest.Close(); err != nil {
|
||||||
|
fs.Logf(nil, "Failed to close file body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err := io.Copy(dest, reader); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to copy file content: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := src.Remove(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to remove source file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := src.Open(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open source object: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := reader.Close(); err != nil {
|
||||||
|
fs.Logf(nil, "Failed to close file body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = f.uploadFile(ctx, reader, destinationPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to upload file to destination: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := src.Remove(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to delete source file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Object{
|
||||||
|
fs: f,
|
||||||
|
remote: destinationPath,
|
||||||
|
size: src.Size(),
|
||||||
|
modTime: src.ModTime(ctx),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rmdir removes a directory
|
||||||
|
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
||||||
|
fullPath := path.Join(f.root, dir)
|
||||||
|
if fullPath != "" {
|
||||||
|
fullPath = "/" + strings.Trim(fullPath, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Check if folder is empty
|
||||||
|
listResp, err := f.getFolderList(ctx, fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(listResp.Result.Files) > 0 || len(listResp.Result.Folders) > 0 {
|
||||||
|
return fmt.Errorf("Rmdir: directory %q is not empty", fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Delete the folder
|
||||||
|
return f.deleteFolder(ctx, fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the interfaces are satisfied
|
||||||
|
var (
|
||||||
|
_ fs.Fs = (*Fs)(nil)
|
||||||
|
_ fs.Purger = (*Fs)(nil)
|
||||||
|
_ fs.Abouter = (*Fs)(nil)
|
||||||
|
_ fs.Mover = (*Fs)(nil)
|
||||||
|
_ fs.Object = (*Object)(nil)
|
||||||
|
)
|
324
backend/filelu/filelu_client.go
Normal file
324
backend/filelu/filelu_client.go
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
package filelu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rclone/rclone/backend/filelu/api"
|
||||||
|
"github.com/rclone/rclone/fs"
|
||||||
|
"github.com/rclone/rclone/fs/fserrors"
|
||||||
|
"github.com/rclone/rclone/lib/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createFolder creates a folder at the specified path.
|
||||||
|
func (f *Fs) createFolder(ctx context.Context, dirPath string) (*api.CreateFolderResponse, error) {
|
||||||
|
encodedDir := f.fromStandardPath(dirPath)
|
||||||
|
apiURL := fmt.Sprintf("%s/folder/create?folder_path=%s&key=%s",
|
||||||
|
f.endpoint,
|
||||||
|
url.QueryEscape(encodedDir),
|
||||||
|
url.QueryEscape(f.opt.Key), // assuming f.opt.Key is the correct field
|
||||||
|
)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
|
result := api.CreateFolderResponse{}
|
||||||
|
err = f.pacer.Call(func() (bool, error) {
|
||||||
|
var innerErr error
|
||||||
|
resp, innerErr = f.client.Do(req)
|
||||||
|
return fserrors.ShouldRetry(innerErr), innerErr
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
fs.Logf(nil, "Failed to close response body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decoding response: %w", err)
|
||||||
|
}
|
||||||
|
if result.Status != 200 {
|
||||||
|
return nil, fmt.Errorf("error: %s", result.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.Infof(f, "Successfully created folder %q with ID %v", dirPath, result.Result.FldID)
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFolderList List both files and folders in a directory.
|
||||||
|
func (f *Fs) getFolderList(ctx context.Context, path string) (*api.FolderListResponse, error) {
|
||||||
|
encodedDir := f.fromStandardPath(path)
|
||||||
|
apiURL := fmt.Sprintf("%s/folder/list?folder_path=%s&key=%s",
|
||||||
|
f.endpoint,
|
||||||
|
url.QueryEscape(encodedDir),
|
||||||
|
url.QueryEscape(f.opt.Key),
|
||||||
|
)
|
||||||
|
|
||||||
|
var body []byte
|
||||||
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := f.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return shouldRetry(err), fmt.Errorf("failed to list directory: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
fs.Logf(nil, "Failed to close response body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error reading response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldRetryHTTP(resp.StatusCode), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response api.FolderListResponse
|
||||||
|
if err := json.NewDecoder(bytes.NewReader(body)).Decode(&response); err != nil {
|
||||||
|
return nil, fmt.Errorf("error decoding response: %w", err)
|
||||||
|
}
|
||||||
|
if response.Status != 200 {
|
||||||
|
if strings.Contains(response.Msg, "Folder not found") {
|
||||||
|
return nil, fs.ErrorDirNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("API error: %s", response.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := range response.Result.Folders {
|
||||||
|
response.Result.Folders[index].Path = f.toStandardPath(response.Result.Folders[index].Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := range response.Result.Files {
|
||||||
|
response.Result.Files[index].Name = f.toStandardPath(response.Result.Files[index].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteFolder deletes a folder at the specified path.
|
||||||
|
func (f *Fs) deleteFolder(ctx context.Context, fullPath string) error {
|
||||||
|
fullPath = f.fromStandardPath(fullPath)
|
||||||
|
deleteURL := fmt.Sprintf("%s/folder/delete?folder_path=%s&key=%s",
|
||||||
|
f.endpoint,
|
||||||
|
url.QueryEscape(fullPath),
|
||||||
|
url.QueryEscape(f.opt.Key),
|
||||||
|
)
|
||||||
|
|
||||||
|
delResp := api.DeleteFolderResponse{}
|
||||||
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", deleteURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
resp, err := f.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fserrors.ShouldRetry(err), err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
fs.Logf(nil, "Failed to close response body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &delResp); err != nil {
|
||||||
|
return false, fmt.Errorf("error decoding delete response: %w", err)
|
||||||
|
}
|
||||||
|
if delResp.Status != 200 {
|
||||||
|
return false, fmt.Errorf("delete error: %s", delResp.Msg)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.Infof(f, "Rmdir: successfully deleted %q", fullPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDirectLink of file from FileLu to download.
|
||||||
|
func (f *Fs) getDirectLink(ctx context.Context, filePath string) (string, int64, error) {
|
||||||
|
filePath = f.fromStandardPath(filePath)
|
||||||
|
apiURL := fmt.Sprintf("%s/file/direct_link?file_path=%s&key=%s",
|
||||||
|
f.endpoint,
|
||||||
|
url.QueryEscape(filePath),
|
||||||
|
url.QueryEscape(f.opt.Key),
|
||||||
|
)
|
||||||
|
|
||||||
|
result := api.FileDirectLinkResponse{}
|
||||||
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := f.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return shouldRetry(err), fmt.Errorf("failed to fetch direct link: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
fs.Logf(nil, "Failed to close response body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return false, fmt.Errorf("error decoding response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Status != 200 {
|
||||||
|
return false, fmt.Errorf("API error: %s", result.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldRetryHTTP(resp.StatusCode), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Result.URL, result.Result.Size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteFile deletes a file based on filePath
|
||||||
|
func (f *Fs) deleteFile(ctx context.Context, filePath string) error {
|
||||||
|
filePath = f.fromStandardPath(filePath)
|
||||||
|
apiURL := fmt.Sprintf("%s/file/remove?file_path=%s&key=%s",
|
||||||
|
f.endpoint,
|
||||||
|
url.QueryEscape(filePath),
|
||||||
|
url.QueryEscape(f.opt.Key),
|
||||||
|
)
|
||||||
|
|
||||||
|
result := api.DeleteFileResponse{}
|
||||||
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := f.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return shouldRetry(err), fmt.Errorf("failed to fetch direct link: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
fs.Logf(nil, "Failed to close response body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return false, fmt.Errorf("error decoding response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Status != 200 {
|
||||||
|
return false, fmt.Errorf("API error: %s", result.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldRetryHTTP(resp.StatusCode), nil
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAccountInfo retrieves account information
|
||||||
|
func (f *Fs) getAccountInfo(ctx context.Context) (*api.AccountInfoResponse, error) {
|
||||||
|
opts := rest.Opts{
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/account/info",
|
||||||
|
Parameters: url.Values{
|
||||||
|
"key": {f.opt.Key},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var result api.AccountInfoResponse
|
||||||
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
|
_, callErr := f.srv.CallJSON(ctx, &opts, nil, &result)
|
||||||
|
return fserrors.ShouldRetry(callErr), callErr
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Status != 200 {
|
||||||
|
return nil, fmt.Errorf("error: %s", result.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFileInfo retrieves file information based on file code
|
||||||
|
func (f *Fs) getFileInfo(ctx context.Context, fileCode string) (*api.FileInfoResponse, error) {
|
||||||
|
u, _ := url.Parse(f.endpoint + "/file/info2")
|
||||||
|
q := u.Query()
|
||||||
|
q.Set("file_code", fileCode) // raw path — Go handles escaping properly here
|
||||||
|
q.Set("key", f.opt.Key)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
apiURL := f.endpoint + "/file/info2?" + u.RawQuery
|
||||||
|
|
||||||
|
var body []byte
|
||||||
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := f.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return shouldRetry(err), fmt.Errorf("failed to fetch file info: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
fs.Logf(nil, "Failed to close response body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error reading response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldRetryHTTP(resp.StatusCode), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := api.FileInfoResponse{}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(bytes.NewReader(body)).Decode(&result); err != nil {
|
||||||
|
return nil, fmt.Errorf("error decoding response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Status != 200 || len(result.Result) == 0 {
|
||||||
|
return nil, fs.ErrorObjectNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
193
backend/filelu/filelu_file_uploader.go
Normal file
193
backend/filelu/filelu_file_uploader.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package filelu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rclone/rclone/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// uploadFile uploads a file to FileLu
|
||||||
|
func (f *Fs) uploadFile(ctx context.Context, fileContent io.Reader, fileFullPath string) error {
|
||||||
|
directory := path.Dir(fileFullPath)
|
||||||
|
fileName := path.Base(fileFullPath)
|
||||||
|
if directory == "." {
|
||||||
|
directory = ""
|
||||||
|
}
|
||||||
|
destinationFolderPath := path.Join(f.root, directory)
|
||||||
|
if destinationFolderPath != "" {
|
||||||
|
destinationFolderPath = "/" + strings.Trim(destinationFolderPath, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
existingEntries, err := f.List(ctx, path.Dir(fileFullPath))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, fs.ErrorDirNotFound) {
|
||||||
|
err = f.Mkdir(ctx, path.Dir(fileFullPath))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to list existing files: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range existingEntries {
|
||||||
|
if entry.Remote() == fileFullPath {
|
||||||
|
_, ok := entry.(fs.Object)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the file exists but is different, remove it
|
||||||
|
filePath := "/" + strings.Trim(destinationFolderPath+"/"+fileName, "/")
|
||||||
|
err = f.deleteFile(ctx, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete existing file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadURL, sessID, err := f.getUploadServer(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve upload server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the fileCode isn't used, just handle the error
|
||||||
|
if _, err := f.uploadFileWithDestination(ctx, uploadURL, sessID, fileName, fileContent, destinationFolderPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to upload file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUploadServer gets the upload server URL with proper key authentication
|
||||||
|
func (f *Fs) getUploadServer(ctx context.Context) (string, string, error) {
|
||||||
|
apiURL := fmt.Sprintf("%s/upload/server?key=%s", f.endpoint, url.QueryEscape(f.opt.Key))
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
SessID string `json:"sess_id"`
|
||||||
|
Result string `json:"result"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := f.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return shouldRetry(err), fmt.Errorf("failed to get upload server: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
fs.Logf(nil, "Failed to close response body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return false, fmt.Errorf("error decoding response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Status != 200 {
|
||||||
|
return false, fmt.Errorf("API error: %s", result.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldRetryHTTP(resp.StatusCode), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Result, result.SessID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// uploadFileWithDestination uploads a file directly to a specified folder using file content reader.
|
||||||
|
func (f *Fs) uploadFileWithDestination(ctx context.Context, uploadURL, sessID, fileName string, fileContent io.Reader, dirPath string) (string, error) {
|
||||||
|
destinationPath := f.fromStandardPath(dirPath)
|
||||||
|
encodedFileName := f.fromStandardPath(fileName)
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
writer := multipart.NewWriter(pw)
|
||||||
|
isDeletionRequired := false
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if err := pw.Close(); err != nil {
|
||||||
|
fs.Logf(nil, "Failed to close: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_ = writer.WriteField("sess_id", sessID)
|
||||||
|
_ = writer.WriteField("utype", "prem")
|
||||||
|
_ = writer.WriteField("fld_path", destinationPath)
|
||||||
|
|
||||||
|
part, err := writer.CreateFormFile("file_0", encodedFileName)
|
||||||
|
if err != nil {
|
||||||
|
pw.CloseWithError(fmt.Errorf("failed to create form file: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(part, fileContent); err != nil {
|
||||||
|
isDeletionRequired = true
|
||||||
|
pw.CloseWithError(fmt.Errorf("failed to copy file content: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
pw.CloseWithError(fmt.Errorf("failed to close writer: %w", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var fileCode string
|
||||||
|
err := f.pacer.Call(func() (bool, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", uploadURL, pr)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to create upload request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
resp, err := f.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return shouldRetry(err), fmt.Errorf("failed to send upload request: %w", err)
|
||||||
|
}
|
||||||
|
defer respBodyClose(resp.Body)
|
||||||
|
|
||||||
|
var result []struct {
|
||||||
|
FileCode string `json:"file_code"`
|
||||||
|
FileStatus string `json:"file_status"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return false, fmt.Errorf("failed to parse upload response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) == 0 || result[0].FileStatus != "OK" {
|
||||||
|
return false, fmt.Errorf("upload failed with status: %s", result[0].FileStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileCode = result[0].FileCode
|
||||||
|
return shouldRetryHTTP(resp.StatusCode), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil && isDeletionRequired {
|
||||||
|
// Attempt to delete the file if upload fails
|
||||||
|
_ = f.deleteFile(ctx, destinationPath+"/"+fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileCode, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// respBodyClose to check body response.
|
||||||
|
func respBodyClose(responseBody io.Closer) {
|
||||||
|
if cerr := responseBody.Close(); cerr != nil {
|
||||||
|
fmt.Printf("Error closing response body: %v\n", cerr)
|
||||||
|
}
|
||||||
|
}
|
112
backend/filelu/filelu_helper.go
Normal file
112
backend/filelu/filelu_helper.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package filelu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rclone/rclone/fs"
|
||||||
|
"github.com/rclone/rclone/fs/fserrors"
|
||||||
|
"github.com/rclone/rclone/fs/hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
// errFileNotFound represent file not found error
|
||||||
|
var errFileNotFound error = errors.New("file not found")
|
||||||
|
|
||||||
|
// getFileCode retrieves the file code for a given file path
|
||||||
|
func (f *Fs) getFileCode(ctx context.Context, filePath string) (string, error) {
|
||||||
|
// Prepare parent directory
|
||||||
|
parentDir := path.Dir(filePath)
|
||||||
|
|
||||||
|
// Call List to get all the files
|
||||||
|
result, err := f.getFolderList(ctx, parentDir)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range result.Result.Files {
|
||||||
|
filePathFromServer := parentDir + "/" + file.Name
|
||||||
|
if parentDir == "/" {
|
||||||
|
filePathFromServer = "/" + file.Name
|
||||||
|
}
|
||||||
|
if filePath == filePathFromServer {
|
||||||
|
return file.FileCode, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errFileNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Features returns the optional features of this Fs
|
||||||
|
func (f *Fs) Features() *fs.Features {
|
||||||
|
return f.features
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fs) fromStandardPath(remote string) string {
|
||||||
|
return f.opt.Enc.FromStandardPath(remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fs) toStandardPath(remote string) string {
|
||||||
|
return f.opt.Enc.ToStandardPath(remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hashes returns an empty hash set, indicating no hash support
|
||||||
|
func (f *Fs) Hashes() hash.Set {
|
||||||
|
return hash.NewHashSet() // Properly creates an empty hash set
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the remote name
|
||||||
|
func (f *Fs) Name() string {
|
||||||
|
return f.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root returns the root path
|
||||||
|
func (f *Fs) Root() string {
|
||||||
|
return f.root
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precision returns the precision of the remote
|
||||||
|
func (f *Fs) Precision() time.Duration {
|
||||||
|
return fs.ModTimeNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fs) String() string {
|
||||||
|
return fmt.Sprintf("FileLu root '%s'", f.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isFileCode checks if a string looks like a file code
|
||||||
|
func isFileCode(s string) bool {
|
||||||
|
if len(s) != 12 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, c := range s {
|
||||||
|
if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRetry(err error) bool {
|
||||||
|
return fserrors.ShouldRetry(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRetryHTTP(code int) bool {
|
||||||
|
return code == 429 || code >= 500
|
||||||
|
}
|
||||||
|
|
||||||
|
func rootSplit(absPath string) (bucket, bucketPath string) {
|
||||||
|
// No bucket
|
||||||
|
if absPath == "" {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
slash := strings.IndexRune(absPath, '/')
|
||||||
|
// Bucket but no path
|
||||||
|
if slash < 0 {
|
||||||
|
return absPath, ""
|
||||||
|
}
|
||||||
|
return absPath[:slash], absPath[slash+1:]
|
||||||
|
}
|
259
backend/filelu/filelu_object.go
Normal file
259
backend/filelu/filelu_object.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
package filelu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rclone/rclone/fs"
|
||||||
|
"github.com/rclone/rclone/fs/hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Object describes a FileLu object
|
||||||
|
type Object struct {
|
||||||
|
fs *Fs
|
||||||
|
remote string
|
||||||
|
size int64
|
||||||
|
modTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewObject creates a new Object for the given remote path
|
||||||
|
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
||||||
|
var filePath string
|
||||||
|
filePath = path.Join(f.root, remote)
|
||||||
|
filePath = "/" + strings.Trim(filePath, "/")
|
||||||
|
|
||||||
|
// Get File code
|
||||||
|
fileCode, err := f.getFileCode(ctx, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fs.ErrorObjectNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get File info
|
||||||
|
fileInfos, err := f.getFileInfo(ctx, fileCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get file info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfo := fileInfos.Result[0]
|
||||||
|
size, _ := strconv.ParseInt(fileInfo.Size, 10, 64)
|
||||||
|
|
||||||
|
returnedRemote := remote
|
||||||
|
return &Object{
|
||||||
|
fs: f,
|
||||||
|
remote: returnedRemote,
|
||||||
|
size: size,
|
||||||
|
modTime: time.Now(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open opens the object for reading
|
||||||
|
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
|
||||||
|
filePath := path.Join(o.fs.root, o.remote)
|
||||||
|
// Get direct link
|
||||||
|
directLink, size, err := o.fs.getDirectLink(ctx, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get direct link: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
o.size = size
|
||||||
|
|
||||||
|
// Offset and Count for range download
|
||||||
|
var offset int64
|
||||||
|
var count int64
|
||||||
|
fs.FixRangeOption(options, o.size)
|
||||||
|
for _, option := range options {
|
||||||
|
switch x := option.(type) {
|
||||||
|
case *fs.RangeOption:
|
||||||
|
offset, count = x.Decode(o.size)
|
||||||
|
if count < 0 {
|
||||||
|
count = o.size - offset
|
||||||
|
}
|
||||||
|
case *fs.SeekOption:
|
||||||
|
offset = x.Offset
|
||||||
|
count = o.size
|
||||||
|
default:
|
||||||
|
if option.Mandatory() {
|
||||||
|
fs.Logf(o, "Unsupported mandatory option: %v", option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var reader io.ReadCloser
|
||||||
|
err = o.fs.pacer.Call(func() (bool, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", directLink, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to create download request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := o.fs.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return shouldRetry(err), fmt.Errorf("failed to download file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
fs.Logf(nil, "Failed to close response body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return false, fmt.Errorf("failed to download file: HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the response body to handle offset and count
|
||||||
|
currentContents, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset > 0 {
|
||||||
|
if offset > int64(len(currentContents)) {
|
||||||
|
return false, fmt.Errorf("offset %d exceeds file size %d", offset, len(currentContents))
|
||||||
|
}
|
||||||
|
currentContents = currentContents[offset:]
|
||||||
|
}
|
||||||
|
if count > 0 && count < int64(len(currentContents)) {
|
||||||
|
currentContents = currentContents[:count]
|
||||||
|
}
|
||||||
|
reader = io.NopCloser(bytes.NewReader(currentContents))
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates the object with new data
|
||||||
|
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
|
||||||
|
if src.Size() <= 0 {
|
||||||
|
return fs.ErrorCantUploadEmptyFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
err := o.fs.uploadFile(ctx, in, o.remote)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to upload file: %w", err)
|
||||||
|
}
|
||||||
|
o.size = src.Size()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove deletes the object from FileLu
|
||||||
|
func (o *Object) Remove(ctx context.Context) error {
|
||||||
|
fullPath := "/" + strings.Trim(path.Join(o.fs.root, o.remote), "/")
|
||||||
|
|
||||||
|
err := o.fs.deleteFile(ctx, fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fs.Infof(o.fs, "Successfully deleted file: %s", fullPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash returns the MD5 hash of an object
|
||||||
|
func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
|
||||||
|
if t != hash.MD5 {
|
||||||
|
return "", hash.ErrUnsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileCode string
|
||||||
|
if isFileCode(o.fs.root) {
|
||||||
|
fileCode = o.fs.root
|
||||||
|
} else {
|
||||||
|
matches := regexp.MustCompile(`\((.*?)\)`).FindAllStringSubmatch(o.remote, -1)
|
||||||
|
for _, match := range matches {
|
||||||
|
if len(match) > 1 && len(match[1]) == 12 {
|
||||||
|
fileCode = match[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fileCode == "" {
|
||||||
|
return "", fmt.Errorf("no valid file code found in the remote path")
|
||||||
|
}
|
||||||
|
|
||||||
|
apiURL := fmt.Sprintf("%s/file/info?file_code=%s&key=%s",
|
||||||
|
o.fs.endpoint, url.QueryEscape(fileCode), url.QueryEscape(o.fs.opt.Key))
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Result []struct {
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
err := o.fs.pacer.Call(func() (bool, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
resp, err := o.fs.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return shouldRetry(err), err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
fs.Logf(nil, "Failed to close response body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return shouldRetryHTTP(resp.StatusCode), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if result.Status != 200 || len(result.Result) == 0 {
|
||||||
|
return "", fmt.Errorf("error: unable to fetch hash: %s", result.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Result[0].Hash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the object
|
||||||
|
func (o *Object) String() string {
|
||||||
|
return o.remote
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fs returns the parent Fs
|
||||||
|
func (o *Object) Fs() fs.Info {
|
||||||
|
return o.fs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remote returns the remote path
|
||||||
|
func (o *Object) Remote() string {
|
||||||
|
return o.remote
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the size of the object
|
||||||
|
func (o *Object) Size() int64 {
|
||||||
|
return o.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModTime returns the modification time of the object
|
||||||
|
func (o *Object) ModTime(ctx context.Context) time.Time {
|
||||||
|
return o.modTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetModTime sets the modification time of the object
|
||||||
|
func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
|
||||||
|
return fs.ErrorCantSetModTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storable indicates whether the object is storable
|
||||||
|
func (o *Object) Storable() bool {
|
||||||
|
return true
|
||||||
|
}
|
16
backend/filelu/filelu_test.go
Normal file
16
backend/filelu/filelu_test.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package filelu_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rclone/rclone/fstest/fstests"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestIntegration runs integration tests for the FileLu backend
|
||||||
|
func TestIntegration(t *testing.T) {
|
||||||
|
fstests.Run(t, &fstests.Opt{
|
||||||
|
RemoteName: "TestFileLu:",
|
||||||
|
NilObject: nil,
|
||||||
|
SkipInvalidUTF8: true,
|
||||||
|
})
|
||||||
|
}
|
15
backend/filelu/utils.go
Normal file
15
backend/filelu/utils.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package filelu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseStorageToBytes converts a storage string (e.g., "10") to bytes
|
||||||
|
func parseStorageToBytes(storage string) (int64, error) {
|
||||||
|
var gb float64
|
||||||
|
_, err := fmt.Sscanf(storage, "%f", &gb)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to parse storage: %w", err)
|
||||||
|
}
|
||||||
|
return int64(gb * 1024 * 1024 * 1024), nil
|
||||||
|
}
|
@@ -43,6 +43,7 @@ docs = [
|
|||||||
"combine.md",
|
"combine.md",
|
||||||
"dropbox.md",
|
"dropbox.md",
|
||||||
"filefabric.md",
|
"filefabric.md",
|
||||||
|
"filelu.md",
|
||||||
"filescom.md",
|
"filescom.md",
|
||||||
"ftp.md",
|
"ftp.md",
|
||||||
"gofile.md",
|
"gofile.md",
|
||||||
|
@@ -124,6 +124,7 @@ WebDAV or S3, that work out of the box.)
|
|||||||
{{< provider name="Enterprise File Fabric" home="https://storagemadeeasy.com/about/" config="/filefabric/" >}}
|
{{< provider name="Enterprise File Fabric" home="https://storagemadeeasy.com/about/" config="/filefabric/" >}}
|
||||||
{{< provider name="Fastmail Files" home="https://www.fastmail.com/" config="/webdav/#fastmail-files" >}}
|
{{< provider name="Fastmail Files" home="https://www.fastmail.com/" config="/webdav/#fastmail-files" >}}
|
||||||
{{< provider name="Files.com" home="https://www.files.com/" config="/filescom/" >}}
|
{{< provider name="Files.com" home="https://www.files.com/" config="/filescom/" >}}
|
||||||
|
{{< provider name="FileLu Cloud Storage" home="https://filelu.com/" config="/filelu/" >}}
|
||||||
{{< provider name="FlashBlade" home="https://www.purestorage.com/products/unstructured-data-storage.html" config="/s3/#pure-storage-flashblade" >}}
|
{{< provider name="FlashBlade" home="https://www.purestorage.com/products/unstructured-data-storage.html" config="/s3/#pure-storage-flashblade" >}}
|
||||||
{{< provider name="FTP" home="https://en.wikipedia.org/wiki/File_Transfer_Protocol" config="/ftp/" >}}
|
{{< provider name="FTP" home="https://en.wikipedia.org/wiki/File_Transfer_Protocol" config="/ftp/" >}}
|
||||||
{{< provider name="Gofile" home="https://gofile.io/" config="/gofile/" >}}
|
{{< provider name="Gofile" home="https://gofile.io/" config="/gofile/" >}}
|
||||||
|
275
docs/content/filelu.md
Normal file
275
docs/content/filelu.md
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
---
|
||||||
|
title: "FileLu"
|
||||||
|
description: "Rclone docs for FileLu"
|
||||||
|
versionIntroduced: "v1.70"
|
||||||
|
---
|
||||||
|
|
||||||
|
# {{< icon "fa fa-folder" >}} FileLu
|
||||||
|
|
||||||
|
[FileLu](https://filelu.com/) is a reliable cloud storage provider
|
||||||
|
offering features like secure file uploads, downloads, flexible
|
||||||
|
storage options, and sharing capabilities. With support for high
|
||||||
|
storage limits and seamless integration with rclone, FileLu makes
|
||||||
|
managing files in the cloud easy. Its cross-platform file backup
|
||||||
|
services let you upload and back up files from any internet-connected
|
||||||
|
device.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Here is an example of how to make a remote called `filelu`. First, run:
|
||||||
|
|
||||||
|
rclone config
|
||||||
|
|
||||||
|
This will guide you through an interactive setup process:
|
||||||
|
|
||||||
|
```
|
||||||
|
No remotes found, make a new one?
|
||||||
|
n) New remote
|
||||||
|
s) Set configuration password
|
||||||
|
q) Quit config
|
||||||
|
n/s/q> n
|
||||||
|
name> filelu
|
||||||
|
Type of storage to configure.
|
||||||
|
Choose a number from below, or type in your own value
|
||||||
|
[snip]
|
||||||
|
xx / FileLu Cloud Storage
|
||||||
|
\ "filelu"
|
||||||
|
[snip]
|
||||||
|
Storage> filelu
|
||||||
|
Enter your FileLu Rclone Key:
|
||||||
|
key> YOUR_FILELU_RCLONE_KEY RC_xxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
Configuration complete.
|
||||||
|
|
||||||
|
Keep this "filelu" remote?
|
||||||
|
y) Yes this is OK
|
||||||
|
e) Edit this remote
|
||||||
|
d) Delete this remote
|
||||||
|
y/e/d> y
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paths
|
||||||
|
|
||||||
|
A path without an initial `/` will operate in the `Rclone` directory.
|
||||||
|
|
||||||
|
A path with an initial `/` will operate at the root where you can see
|
||||||
|
the `Rclone` directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rclone lsf TestFileLu:/
|
||||||
|
CCTV/
|
||||||
|
Camera/
|
||||||
|
Documents/
|
||||||
|
Music/
|
||||||
|
Photos/
|
||||||
|
Rclone/
|
||||||
|
Vault/
|
||||||
|
Videos/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Commands
|
||||||
|
|
||||||
|
Create a new folder named `foldername` in the `Rclone` directory:
|
||||||
|
|
||||||
|
rclone mkdir filelu:foldername
|
||||||
|
|
||||||
|
Delete a folder on FileLu:
|
||||||
|
|
||||||
|
rclone rmdir filelu:/folder/path/
|
||||||
|
|
||||||
|
Delete a file on FileLu:
|
||||||
|
|
||||||
|
rclone delete filelu:/hello.txt
|
||||||
|
|
||||||
|
List files from your FileLu account:
|
||||||
|
|
||||||
|
rclone ls filelu:
|
||||||
|
|
||||||
|
List all folders:
|
||||||
|
|
||||||
|
rclone lsd filelu:
|
||||||
|
|
||||||
|
Copy a specific file to the FileLu root:
|
||||||
|
|
||||||
|
rclone copy D:\\hello.txt filelu:
|
||||||
|
|
||||||
|
Copy files from a local directory to a FileLu directory:
|
||||||
|
|
||||||
|
rclone copy D:/local-folder filelu:/remote-folder/path/
|
||||||
|
|
||||||
|
Download a file from FileLu into a local directory:
|
||||||
|
|
||||||
|
rclone copy filelu:/file-path/hello.txt D:/local-folder
|
||||||
|
|
||||||
|
Move files from a local directory to a FileLu directory:
|
||||||
|
|
||||||
|
rclone move D:\\local-folder filelu:/remote-path/
|
||||||
|
|
||||||
|
Sync files from a local directory to a FileLu directory:
|
||||||
|
|
||||||
|
rclone sync --interactive D:/local-folder filelu:/remote-path/
|
||||||
|
|
||||||
|
Mount remote to local Linux:
|
||||||
|
|
||||||
|
rclone mount filelu: /root/mnt --vfs-cache-mode full
|
||||||
|
|
||||||
|
Mount remote to local Windows:
|
||||||
|
|
||||||
|
rclone mount filelu: D:/local_mnt --vfs-cache-mode full
|
||||||
|
|
||||||
|
Get storage info about the FileLu account:
|
||||||
|
|
||||||
|
rclone about filelu:
|
||||||
|
|
||||||
|
All the other rclone commands are supported by this backend.
|
||||||
|
|
||||||
|
### FolderID instead of folder path
|
||||||
|
|
||||||
|
We use the FolderID instead of the folder name to prevent errors when
|
||||||
|
users have identical folder names or paths. For example, if a user has
|
||||||
|
two or three folders named "test_folders," the system may become
|
||||||
|
confused and won't know which folder to move. In large storage
|
||||||
|
systems, some clients have hundred of thousands of folders and a few
|
||||||
|
millions of files, duplicate folder names or paths are quite common.
|
||||||
|
|
||||||
|
### Modification Times and Hashes
|
||||||
|
|
||||||
|
FileLu supports both modification times and MD5 hashes.
|
||||||
|
|
||||||
|
FileLu only supports filenames and folder names up to 255 characters in length, where a
|
||||||
|
character is a Unicode character.
|
||||||
|
|
||||||
|
### Duplicated Files
|
||||||
|
|
||||||
|
When uploading and syncing via Rclone, FileLu does not allow uploading
|
||||||
|
duplicate files within the same directory. However, you can upload
|
||||||
|
duplicate files, provided they are in different directories (folders).
|
||||||
|
|
||||||
|
### Failure to Log / Invalid Credentials or KEY
|
||||||
|
|
||||||
|
Ensure that you have the correct Rclone key, which can be found in [My
|
||||||
|
Account](https://filelu.com/account/). Every time you toggle Rclone
|
||||||
|
OFF and ON in My Account, a new RC_xxxxxxxxxxxxxxxxxxxx key is
|
||||||
|
generated. Be sure to update your Rclone configuration with the new
|
||||||
|
key.
|
||||||
|
|
||||||
|
If you are connecting to your FileLu remote for the first time and
|
||||||
|
encounter an error such as:
|
||||||
|
|
||||||
|
```
|
||||||
|
Failed to create file system for "my-filelu-remote:": couldn't login: Invalid credentials
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure your Rclone Key is correct.
|
||||||
|
|
||||||
|
### Process `killed`
|
||||||
|
|
||||||
|
Accounts with large files or extensive metadata may experience
|
||||||
|
significant memory usage during list/sync operations. Ensure the
|
||||||
|
system running `rclone` has sufficient memory and CPU to handle these
|
||||||
|
operations.
|
||||||
|
|
||||||
|
{{< rem autogenerated options start" - DO NOT EDIT - instead edit fs.RegInfo in backend/filelu/filelu.go then run make backenddocs" >}}
|
||||||
|
### Standard options
|
||||||
|
|
||||||
|
Here are the Standard options specific to filelu (FileLu Cloud Storage).
|
||||||
|
|
||||||
|
#### --filelu-key
|
||||||
|
|
||||||
|
Your FileLu Rclone key from My Account
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- Config: key
|
||||||
|
- Env Var: RCLONE_FILELU_KEY
|
||||||
|
- Type: string
|
||||||
|
- Required: true
|
||||||
|
|
||||||
|
### Advanced options
|
||||||
|
|
||||||
|
Here are the Advanced options specific to filelu (FileLu Cloud Storage).
|
||||||
|
|
||||||
|
#### --filelu-description
|
||||||
|
|
||||||
|
Description of the remote.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
|
||||||
|
- Config: description
|
||||||
|
- Env Var: RCLONE_FILELU_DESCRIPTION
|
||||||
|
- Type: string
|
||||||
|
- Required: false
|
||||||
|
|
||||||
|
## Backend commands
|
||||||
|
|
||||||
|
Here are the commands specific to the filelu backend.
|
||||||
|
|
||||||
|
Run them with
|
||||||
|
|
||||||
|
rclone backend COMMAND remote:
|
||||||
|
|
||||||
|
The help below will explain what arguments each command takes.
|
||||||
|
|
||||||
|
See the [backend](/commands/rclone_backend/) command for more
|
||||||
|
info on how to pass options and arguments.
|
||||||
|
|
||||||
|
These can be run on a running backend using the rc command
|
||||||
|
[backend/command](/rc/#backend-command).
|
||||||
|
|
||||||
|
### rename
|
||||||
|
|
||||||
|
Rename a file in a FileLu directory
|
||||||
|
|
||||||
|
rclone backend rename remote: [options] [<arguments>+]
|
||||||
|
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
rclone backend rename filelu:/file-path/hello.txt "hello_new_name.txt"
|
||||||
|
|
||||||
|
|
||||||
|
### movefile
|
||||||
|
|
||||||
|
Move file within the remote FileLu directory
|
||||||
|
|
||||||
|
rclone backend movefile remote: [options] [<arguments>+]
|
||||||
|
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
rclone backend movefile filelu:/source-path/hello.txt /destination-path/
|
||||||
|
|
||||||
|
|
||||||
|
### movefolder
|
||||||
|
|
||||||
|
Move a folder on remote FileLu
|
||||||
|
|
||||||
|
rclone backend movefolder remote: [options] [<arguments>+]
|
||||||
|
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
rclone backend movefolder filelu:/sorce-fld-path/hello-folder/ /destication-fld-path/hello-folder/
|
||||||
|
|
||||||
|
|
||||||
|
### renamefolder
|
||||||
|
|
||||||
|
Rename a folder on FileLu
|
||||||
|
|
||||||
|
rclone backend renamefolder remote: [options] [<arguments>+]
|
||||||
|
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
rclone backend renamefolder filelu:/folder-path/folder-name "new-folder-name"
|
||||||
|
|
||||||
|
|
||||||
|
{{< rem autogenerated options stop >}}
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
This backend uses a custom library implementing the FileLu API. While
|
||||||
|
it supports file transfers, some advanced features may not yet be
|
||||||
|
available. Please report any issues to the [rclone forum](https://forum.rclone.org/)
|
||||||
|
for troubleshooting and updates.
|
||||||
|
|
||||||
|
For further information, visit [FileLu's website](https://filelu.com/).
|
@@ -26,6 +26,7 @@ Here is an overview of the major features of each cloud storage system.
|
|||||||
| Dropbox | DBHASH ¹ | R | Yes | No | - | - |
|
| Dropbox | DBHASH ¹ | R | Yes | No | - | - |
|
||||||
| Enterprise File Fabric | - | R/W | Yes | No | R/W | - |
|
| Enterprise File Fabric | - | R/W | Yes | No | R/W | - |
|
||||||
| Files.com | MD5, CRC32 | DR/W | Yes | No | R | - |
|
| Files.com | MD5, CRC32 | DR/W | Yes | No | R | - |
|
||||||
|
| FileLu Cloud Storage | MD5 | R/W | No | Yes | R | - |
|
||||||
| FTP | - | R/W ¹⁰ | No | No | - | - |
|
| FTP | - | R/W ¹⁰ | No | No | - | - |
|
||||||
| Gofile | MD5 | DR/W | No | Yes | R | - |
|
| Gofile | MD5 | DR/W | No | Yes | R | - |
|
||||||
| Google Cloud Storage | MD5 | R/W | No | No | R/W | - |
|
| Google Cloud Storage | MD5 | R/W | No | No | R/W | - |
|
||||||
|
@@ -66,7 +66,8 @@
|
|||||||
<a class="dropdown-item" href="/koofr/#digi-storage"><i class="fa fa-cloud fa-fw"></i> Digi Storage</a>
|
<a class="dropdown-item" href="/koofr/#digi-storage"><i class="fa fa-cloud fa-fw"></i> Digi Storage</a>
|
||||||
<a class="dropdown-item" href="/dropbox/"><i class="fab fa-dropbox fa-fw"></i> Dropbox</a>
|
<a class="dropdown-item" href="/dropbox/"><i class="fab fa-dropbox fa-fw"></i> Dropbox</a>
|
||||||
<a class="dropdown-item" href="/filefabric/"><i class="fa fa-cloud fa-fw"></i> Enterprise File Fabric</a>
|
<a class="dropdown-item" href="/filefabric/"><i class="fa fa-cloud fa-fw"></i> Enterprise File Fabric</a>
|
||||||
<a class="dropdown-item" href="/filescom/"><i class="fa fa-brands fa-files-pinwheel fa-fw"></i> Files.com</a>
|
<a class="dropdown-item" href="/filelu/"><i class="fa fa-brands fa-files-pinwheel fa-fw"></i> Files.com</a>
|
||||||
|
<a class="dropdown-item" href="/filescom/"><i class="fa fa-cloud fa-fw"></i> FileLu Cloud Storage</a>
|
||||||
<a class="dropdown-item" href="/ftp/"><i class="fa fa-file fa-fw"></i> FTP</a>
|
<a class="dropdown-item" href="/ftp/"><i class="fa fa-file fa-fw"></i> FTP</a>
|
||||||
<a class="dropdown-item" href="/gofile/"><i class="fa fa-folder fa-fw"></i> Gofile</a>
|
<a class="dropdown-item" href="/gofile/"><i class="fa fa-folder fa-fw"></i> Gofile</a>
|
||||||
<a class="dropdown-item" href="/googlecloudstorage/"><i class="fab fa-google fa-fw"></i> Google Cloud Storage</a>
|
<a class="dropdown-item" href="/googlecloudstorage/"><i class="fab fa-google fa-fw"></i> Google Cloud Storage</a>
|
||||||
|
@@ -531,3 +531,8 @@ backends:
|
|||||||
- backend: "iclouddrive"
|
- backend: "iclouddrive"
|
||||||
remote: "TestICloudDrive:"
|
remote: "TestICloudDrive:"
|
||||||
fastlist: false
|
fastlist: false
|
||||||
|
- backend: "filelu"
|
||||||
|
remote: "TestFileLu:"
|
||||||
|
fastlist: false
|
||||||
|
ignore:
|
||||||
|
- TestRWFileHandleWriteNoWrite
|
||||||
|
Reference in New Issue
Block a user