/
githubmirror
/
rclone
Обзор
Документация
Войти
/
githubmirror
/
rclone
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
backend/filelu/filelu.go
369 строк
10 KB
kingston125
backend/filelu: fix recursive listing path handling and file filtering
08 июн 2026, 13:54
08 июн 2026, 13:54
e64480f
Код
Авторство
О чём код?
// 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" ) const ( defaultUploadCutoff = fs.SizeSuffix(500 * 1024 * 1024) defaultChunkSize = fs.SizeSuffix(64 * 1024 * 1024) ) // 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: "upload_cutoff", Help: "Cutoff for switching to chunked upload. Any files larger than this will be uploaded in chunks of chunk_size.", Default: defaultUploadCutoff, Advanced: true, }, { Name: "chunk_size", Help: "Chunk size to use for uploading. Used for multipart uploads.", Default: defaultChunkSize, Advanced: 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"` UploadCutoff fs.SizeSuffix `config:"upload_cutoff"` ChunkSize fs.SizeSuffix `config:"chunk_size"` } // 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 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 } for _, folder := range result.Result.Folders { if f.root == "" && dir == "" && strings.Contains(folder.Path, "/") { continue } folderPath := strings.Trim(folder.Path, "/") root := strings.Trim(f.root, "/") remotePathWithoutRoot := folderPath if root != "" { if remotePathWithoutRoot == root { continue } remotePathWithoutRoot = strings.TrimPrefix(remotePathWithoutRoot, root+"/") } remotePathWithoutRoot = strings.TrimPrefix(remotePathWithoutRoot, "/") if remotePathWithoutRoot == "" { continue } entries = append(entries, fs.NewDir(remotePathWithoutRoot, time.Now())) } for _, file := range result.Result.Files { remote := path.Join(dir, file.Name) 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) { o := &Object{ fs: f, remote: src.Remote(), } return o, o.Update(ctx, in, src, options...) } // 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) )