mirror of
https://github.com/rclone/rclone.git
synced 2025-07-09 16:57:13 +02:00
367 lines
9.5 KiB
Go
367 lines
9.5 KiB
Go
// 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)
|
|
)
|