d0d41fe847
This introduces a new fs.Option flag, Sensitive and uses this along with IsPassword to redact the info in the config file for support purposes. It adds this flag into backends where appropriate. It was necessary to add oauthutil.SharedOptions to some backends as they were missing them. Fixes #5209
1968 lines
58 KiB
Go
1968 lines
58 KiB
Go
//go:build !plan9 && !js
|
|
// +build !plan9,!js
|
|
|
|
// Package cache implements a virtual provider to cache existing remotes.
|
|
package cache
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"os/signal"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/backend/crypt"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/cache"
|
|
"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/config/obscure"
|
|
"github.com/rclone/rclone/fs/fspath"
|
|
"github.com/rclone/rclone/fs/hash"
|
|
"github.com/rclone/rclone/fs/rc"
|
|
"github.com/rclone/rclone/fs/walk"
|
|
"github.com/rclone/rclone/lib/atexit"
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
const (
|
|
// DefCacheChunkSize is the default value for chunk size
|
|
DefCacheChunkSize = fs.SizeSuffix(5 * 1024 * 1024)
|
|
// DefCacheTotalChunkSize is the default value for the maximum size of stored chunks
|
|
DefCacheTotalChunkSize = fs.SizeSuffix(10 * 1024 * 1024 * 1024)
|
|
// DefCacheChunkCleanInterval is the interval at which chunks are cleaned
|
|
DefCacheChunkCleanInterval = fs.Duration(time.Minute)
|
|
// DefCacheInfoAge is the default value for object info age
|
|
DefCacheInfoAge = fs.Duration(6 * time.Hour)
|
|
// DefCacheReadRetries is the default value for read retries
|
|
DefCacheReadRetries = 10
|
|
// DefCacheTotalWorkers is how many workers run in parallel to download chunks
|
|
DefCacheTotalWorkers = 4
|
|
// DefCacheChunkNoMemory will enable or disable in-memory storage for chunks
|
|
DefCacheChunkNoMemory = false
|
|
// DefCacheRps limits the number of requests per second to the source FS
|
|
DefCacheRps = -1
|
|
// DefCacheWrites will cache file data on writes through the cache
|
|
DefCacheWrites = false
|
|
// DefCacheTmpWaitTime says how long should files be stored in local cache before being uploaded
|
|
DefCacheTmpWaitTime = fs.Duration(15 * time.Second)
|
|
// DefCacheDbWaitTime defines how long the cache backend should wait for the DB to be available
|
|
DefCacheDbWaitTime = fs.Duration(1 * time.Second)
|
|
)
|
|
|
|
// Register with Fs
|
|
func init() {
|
|
fs.Register(&fs.RegInfo{
|
|
Name: "cache",
|
|
Description: "Cache a remote",
|
|
NewFs: NewFs,
|
|
CommandHelp: commandHelp,
|
|
Options: []fs.Option{{
|
|
Name: "remote",
|
|
Help: "Remote to cache.\n\nNormally should contain a ':' and a path, e.g. \"myremote:path/to/dir\",\n\"myremote:bucket\" or maybe \"myremote:\" (not recommended).",
|
|
Required: true,
|
|
}, {
|
|
Name: "plex_url",
|
|
Help: "The URL of the Plex server.",
|
|
}, {
|
|
Name: "plex_username",
|
|
Help: "The username of the Plex user.",
|
|
Sensitive: true,
|
|
}, {
|
|
Name: "plex_password",
|
|
Help: "The password of the Plex user.",
|
|
IsPassword: true,
|
|
}, {
|
|
Name: "plex_token",
|
|
Help: "The plex token for authentication - auto set normally.",
|
|
Hide: fs.OptionHideBoth,
|
|
Advanced: true,
|
|
Sensitive: true,
|
|
}, {
|
|
Name: "plex_insecure",
|
|
Help: "Skip all certificate verification when connecting to the Plex server.",
|
|
Advanced: true,
|
|
}, {
|
|
Name: "chunk_size",
|
|
Help: `The size of a chunk (partial file data).
|
|
|
|
Use lower numbers for slower connections. If the chunk size is
|
|
changed, any downloaded chunks will be invalid and cache-chunk-path
|
|
will need to be cleared or unexpected EOF errors will occur.`,
|
|
Default: DefCacheChunkSize,
|
|
Examples: []fs.OptionExample{{
|
|
Value: "1M",
|
|
Help: "1 MiB",
|
|
}, {
|
|
Value: "5M",
|
|
Help: "5 MiB",
|
|
}, {
|
|
Value: "10M",
|
|
Help: "10 MiB",
|
|
}},
|
|
}, {
|
|
Name: "info_age",
|
|
Help: `How long to cache file structure information (directory listings, file size, times, etc.).
|
|
If all write operations are done through the cache then you can safely make
|
|
this value very large as the cache store will also be updated in real time.`,
|
|
Default: DefCacheInfoAge,
|
|
Examples: []fs.OptionExample{{
|
|
Value: "1h",
|
|
Help: "1 hour",
|
|
}, {
|
|
Value: "24h",
|
|
Help: "24 hours",
|
|
}, {
|
|
Value: "48h",
|
|
Help: "48 hours",
|
|
}},
|
|
}, {
|
|
Name: "chunk_total_size",
|
|
Help: `The total size that the chunks can take up on the local disk.
|
|
|
|
If the cache exceeds this value then it will start to delete the
|
|
oldest chunks until it goes under this value.`,
|
|
Default: DefCacheTotalChunkSize,
|
|
Examples: []fs.OptionExample{{
|
|
Value: "500M",
|
|
Help: "500 MiB",
|
|
}, {
|
|
Value: "1G",
|
|
Help: "1 GiB",
|
|
}, {
|
|
Value: "10G",
|
|
Help: "10 GiB",
|
|
}},
|
|
}, {
|
|
Name: "db_path",
|
|
Default: filepath.Join(config.GetCacheDir(), "cache-backend"),
|
|
Help: "Directory to store file structure metadata DB.\n\nThe remote name is used as the DB file name.",
|
|
Advanced: true,
|
|
}, {
|
|
Name: "chunk_path",
|
|
Default: filepath.Join(config.GetCacheDir(), "cache-backend"),
|
|
Help: `Directory to cache chunk files.
|
|
|
|
Path to where partial file data (chunks) are stored locally. The remote
|
|
name is appended to the final path.
|
|
|
|
This config follows the "--cache-db-path". If you specify a custom
|
|
location for "--cache-db-path" and don't specify one for "--cache-chunk-path"
|
|
then "--cache-chunk-path" will use the same path as "--cache-db-path".`,
|
|
Advanced: true,
|
|
}, {
|
|
Name: "db_purge",
|
|
Default: false,
|
|
Help: "Clear all the cached data for this remote on start.",
|
|
Hide: fs.OptionHideConfigurator,
|
|
Advanced: true,
|
|
}, {
|
|
Name: "chunk_clean_interval",
|
|
Default: DefCacheChunkCleanInterval,
|
|
Help: `How often should the cache perform cleanups of the chunk storage.
|
|
|
|
The default value should be ok for most people. If you find that the
|
|
cache goes over "cache-chunk-total-size" too often then try to lower
|
|
this value to force it to perform cleanups more often.`,
|
|
Advanced: true,
|
|
}, {
|
|
Name: "read_retries",
|
|
Default: DefCacheReadRetries,
|
|
Help: `How many times to retry a read from a cache storage.
|
|
|
|
Since reading from a cache stream is independent from downloading file
|
|
data, readers can get to a point where there's no more data in the
|
|
cache. Most of the times this can indicate a connectivity issue if
|
|
cache isn't able to provide file data anymore.
|
|
|
|
For really slow connections, increase this to a point where the stream is
|
|
able to provide data but your experience will be very stuttering.`,
|
|
Advanced: true,
|
|
}, {
|
|
Name: "workers",
|
|
Default: DefCacheTotalWorkers,
|
|
Help: `How many workers should run in parallel to download chunks.
|
|
|
|
Higher values will mean more parallel processing (better CPU needed)
|
|
and more concurrent requests on the cloud provider. This impacts
|
|
several aspects like the cloud provider API limits, more stress on the
|
|
hardware that rclone runs on but it also means that streams will be
|
|
more fluid and data will be available much more faster to readers.
|
|
|
|
**Note**: If the optional Plex integration is enabled then this
|
|
setting will adapt to the type of reading performed and the value
|
|
specified here will be used as a maximum number of workers to use.`,
|
|
Advanced: true,
|
|
}, {
|
|
Name: "chunk_no_memory",
|
|
Default: DefCacheChunkNoMemory,
|
|
Help: `Disable the in-memory cache for storing chunks during streaming.
|
|
|
|
By default, cache will keep file data during streaming in RAM as well
|
|
to provide it to readers as fast as possible.
|
|
|
|
This transient data is evicted as soon as it is read and the number of
|
|
chunks stored doesn't exceed the number of workers. However, depending
|
|
on other settings like "cache-chunk-size" and "cache-workers" this footprint
|
|
can increase if there are parallel streams too (multiple files being read
|
|
at the same time).
|
|
|
|
If the hardware permits it, use this feature to provide an overall better
|
|
performance during streaming but it can also be disabled if RAM is not
|
|
available on the local machine.`,
|
|
Advanced: true,
|
|
}, {
|
|
Name: "rps",
|
|
Default: int(DefCacheRps),
|
|
Help: `Limits the number of requests per second to the source FS (-1 to disable).
|
|
|
|
This setting places a hard limit on the number of requests per second
|
|
that cache will be doing to the cloud provider remote and try to
|
|
respect that value by setting waits between reads.
|
|
|
|
If you find that you're getting banned or limited on the cloud
|
|
provider through cache and know that a smaller number of requests per
|
|
second will allow you to work with it then you can use this setting
|
|
for that.
|
|
|
|
A good balance of all the other settings should make this setting
|
|
useless but it is available to set for more special cases.
|
|
|
|
**NOTE**: This will limit the number of requests during streams but
|
|
other API calls to the cloud provider like directory listings will
|
|
still pass.`,
|
|
Advanced: true,
|
|
}, {
|
|
Name: "writes",
|
|
Default: DefCacheWrites,
|
|
Help: `Cache file data on writes through the FS.
|
|
|
|
If you need to read files immediately after you upload them through
|
|
cache you can enable this flag to have their data stored in the
|
|
cache store at the same time during upload.`,
|
|
Advanced: true,
|
|
}, {
|
|
Name: "tmp_upload_path",
|
|
Default: "",
|
|
Help: `Directory to keep temporary files until they are uploaded.
|
|
|
|
This is the path where cache will use as a temporary storage for new
|
|
files that need to be uploaded to the cloud provider.
|
|
|
|
Specifying a value will enable this feature. Without it, it is
|
|
completely disabled and files will be uploaded directly to the cloud
|
|
provider`,
|
|
Advanced: true,
|
|
}, {
|
|
Name: "tmp_wait_time",
|
|
Default: DefCacheTmpWaitTime,
|
|
Help: `How long should files be stored in local cache before being uploaded.
|
|
|
|
This is the duration that a file must wait in the temporary location
|
|
_cache-tmp-upload-path_ before it is selected for upload.
|
|
|
|
Note that only one file is uploaded at a time and it can take longer
|
|
to start the upload if a queue formed for this purpose.`,
|
|
Advanced: true,
|
|
}, {
|
|
Name: "db_wait_time",
|
|
Default: DefCacheDbWaitTime,
|
|
Help: `How long to wait for the DB to be available - 0 is unlimited.
|
|
|
|
Only one process can have the DB open at any one time, so rclone waits
|
|
for this duration for the DB to become available before it gives an
|
|
error.
|
|
|
|
If you set it to 0 then it will wait forever.`,
|
|
Advanced: true,
|
|
}},
|
|
})
|
|
}
|
|
|
|
// Options defines the configuration for this backend
|
|
type Options struct {
|
|
Remote string `config:"remote"`
|
|
PlexURL string `config:"plex_url"`
|
|
PlexUsername string `config:"plex_username"`
|
|
PlexPassword string `config:"plex_password"`
|
|
PlexToken string `config:"plex_token"`
|
|
PlexInsecure bool `config:"plex_insecure"`
|
|
ChunkSize fs.SizeSuffix `config:"chunk_size"`
|
|
InfoAge fs.Duration `config:"info_age"`
|
|
ChunkTotalSize fs.SizeSuffix `config:"chunk_total_size"`
|
|
DbPath string `config:"db_path"`
|
|
ChunkPath string `config:"chunk_path"`
|
|
DbPurge bool `config:"db_purge"`
|
|
ChunkCleanInterval fs.Duration `config:"chunk_clean_interval"`
|
|
ReadRetries int `config:"read_retries"`
|
|
TotalWorkers int `config:"workers"`
|
|
ChunkNoMemory bool `config:"chunk_no_memory"`
|
|
Rps int `config:"rps"`
|
|
StoreWrites bool `config:"writes"`
|
|
TempWritePath string `config:"tmp_upload_path"`
|
|
TempWaitTime fs.Duration `config:"tmp_wait_time"`
|
|
DbWaitTime fs.Duration `config:"db_wait_time"`
|
|
}
|
|
|
|
// Fs represents a wrapped fs.Fs
|
|
type Fs struct {
|
|
fs.Fs
|
|
wrapper fs.Fs
|
|
|
|
name string
|
|
root string
|
|
opt Options // parsed options
|
|
features *fs.Features // optional features
|
|
cache *Persistent
|
|
tempFs fs.Fs
|
|
|
|
lastChunkCleanup time.Time
|
|
cleanupMu sync.Mutex
|
|
rateLimiter *rate.Limiter
|
|
plexConnector *plexConnector
|
|
backgroundRunner *backgroundWriter
|
|
cleanupChan chan bool
|
|
parentsForgetFn []func(string, fs.EntryType)
|
|
notifiedRemotes map[string]bool
|
|
notifiedMu sync.Mutex
|
|
parentsForgetMu sync.Mutex
|
|
}
|
|
|
|
// parseRootPath returns a cleaned root path and a nil error or "" and an error when the path is invalid
|
|
func parseRootPath(path string) (string, error) {
|
|
return strings.Trim(path, "/"), nil
|
|
}
|
|
|
|
var warnDeprecated sync.Once
|
|
|
|
// NewFs constructs an Fs from the path, container:path
|
|
func NewFs(ctx context.Context, name, rootPath string, m configmap.Mapper) (fs.Fs, error) {
|
|
warnDeprecated.Do(func() {
|
|
fs.Logf(nil, "WARNING: Cache backend is deprecated and may be removed in future. Please use VFS instead.")
|
|
})
|
|
|
|
// Parse config into Options struct
|
|
opt := new(Options)
|
|
err := configstruct.Set(m, opt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if opt.ChunkTotalSize < opt.ChunkSize*fs.SizeSuffix(opt.TotalWorkers) {
|
|
return nil, fmt.Errorf("don't set cache-chunk-total-size(%v) less than cache-chunk-size(%v) * cache-workers(%v)",
|
|
opt.ChunkTotalSize, opt.ChunkSize, opt.TotalWorkers)
|
|
}
|
|
|
|
if strings.HasPrefix(opt.Remote, name+":") {
|
|
return nil, errors.New("can't point cache remote at itself - check the value of the remote setting")
|
|
}
|
|
|
|
rpath, err := parseRootPath(rootPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to clean root path %q: %w", rootPath, err)
|
|
}
|
|
|
|
remotePath := fspath.JoinRootPath(opt.Remote, rootPath)
|
|
wrappedFs, wrapErr := cache.Get(ctx, remotePath)
|
|
if wrapErr != nil && wrapErr != fs.ErrorIsFile {
|
|
return nil, fmt.Errorf("failed to make remote %q to wrap: %w", remotePath, wrapErr)
|
|
}
|
|
var fsErr error
|
|
fs.Debugf(name, "wrapped %v:%v at root %v", wrappedFs.Name(), wrappedFs.Root(), rpath)
|
|
if wrapErr == fs.ErrorIsFile {
|
|
fsErr = fs.ErrorIsFile
|
|
rpath = cleanPath(path.Dir(rpath))
|
|
}
|
|
// configure cache backend
|
|
if opt.DbPurge {
|
|
fs.Debugf(name, "Purging the DB")
|
|
}
|
|
f := &Fs{
|
|
Fs: wrappedFs,
|
|
name: name,
|
|
root: rpath,
|
|
opt: *opt,
|
|
lastChunkCleanup: time.Now().Truncate(time.Hour * 24 * 30),
|
|
cleanupChan: make(chan bool, 1),
|
|
notifiedRemotes: make(map[string]bool),
|
|
}
|
|
cache.PinUntilFinalized(f.Fs, f)
|
|
rps := rate.Inf
|
|
if opt.Rps > 0 {
|
|
rps = rate.Limit(float64(opt.Rps))
|
|
}
|
|
f.rateLimiter = rate.NewLimiter(rps, opt.TotalWorkers)
|
|
|
|
f.plexConnector = &plexConnector{}
|
|
if opt.PlexURL != "" {
|
|
if opt.PlexToken != "" {
|
|
f.plexConnector, err = newPlexConnectorWithToken(f, opt.PlexURL, opt.PlexToken, opt.PlexInsecure)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to the Plex API %v: %w", opt.PlexURL, err)
|
|
}
|
|
} else {
|
|
if opt.PlexPassword != "" && opt.PlexUsername != "" {
|
|
decPass, err := obscure.Reveal(opt.PlexPassword)
|
|
if err != nil {
|
|
decPass = opt.PlexPassword
|
|
}
|
|
f.plexConnector, err = newPlexConnector(f, opt.PlexURL, opt.PlexUsername, decPass, opt.PlexInsecure, func(token string) {
|
|
m.Set("plex_token", token)
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to the Plex API %v: %w", opt.PlexURL, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
dbPath := f.opt.DbPath
|
|
chunkPath := f.opt.ChunkPath
|
|
// if the dbPath is non default but the chunk path is default, we overwrite the last to follow the same one as dbPath
|
|
if dbPath != filepath.Join(config.GetCacheDir(), "cache-backend") &&
|
|
chunkPath == filepath.Join(config.GetCacheDir(), "cache-backend") {
|
|
chunkPath = dbPath
|
|
}
|
|
if filepath.Ext(dbPath) != "" {
|
|
dbPath = filepath.Dir(dbPath)
|
|
}
|
|
if filepath.Ext(chunkPath) != "" {
|
|
chunkPath = filepath.Dir(chunkPath)
|
|
}
|
|
err = os.MkdirAll(dbPath, os.ModePerm)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create cache directory %v: %w", dbPath, err)
|
|
}
|
|
err = os.MkdirAll(chunkPath, os.ModePerm)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create cache directory %v: %w", chunkPath, err)
|
|
}
|
|
|
|
dbPath = filepath.Join(dbPath, name+".db")
|
|
chunkPath = filepath.Join(chunkPath, name)
|
|
fs.Infof(name, "Cache DB path: %v", dbPath)
|
|
fs.Infof(name, "Cache chunk path: %v", chunkPath)
|
|
f.cache, err = GetPersistent(dbPath, chunkPath, &Features{
|
|
PurgeDb: opt.DbPurge,
|
|
DbWaitTime: time.Duration(opt.DbWaitTime),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to start cache db: %w", err)
|
|
}
|
|
// Trap SIGINT and SIGTERM to close the DB handle gracefully
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, syscall.SIGHUP)
|
|
atexit.Register(func() {
|
|
if opt.PlexURL != "" {
|
|
f.plexConnector.closeWebsocket()
|
|
}
|
|
f.StopBackgroundRunners()
|
|
})
|
|
go func() {
|
|
for {
|
|
s := <-c
|
|
if s == syscall.SIGHUP {
|
|
fs.Infof(f, "Clearing cache from signal")
|
|
f.DirCacheFlush()
|
|
}
|
|
}
|
|
}()
|
|
|
|
fs.Infof(name, "Chunk Memory: %v", !f.opt.ChunkNoMemory)
|
|
fs.Infof(name, "Chunk Size: %v", f.opt.ChunkSize)
|
|
fs.Infof(name, "Chunk Total Size: %v", f.opt.ChunkTotalSize)
|
|
fs.Infof(name, "Chunk Clean Interval: %v", f.opt.ChunkCleanInterval)
|
|
fs.Infof(name, "Workers: %v", f.opt.TotalWorkers)
|
|
fs.Infof(name, "File Age: %v", f.opt.InfoAge)
|
|
if f.opt.StoreWrites {
|
|
fs.Infof(name, "Cache Writes: enabled")
|
|
}
|
|
|
|
if f.opt.TempWritePath != "" {
|
|
err = os.MkdirAll(f.opt.TempWritePath, os.ModePerm)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create cache directory %v: %w", f.opt.TempWritePath, err)
|
|
}
|
|
f.opt.TempWritePath = filepath.ToSlash(f.opt.TempWritePath)
|
|
f.tempFs, err = cache.Get(ctx, f.opt.TempWritePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create temp fs: %w", err)
|
|
}
|
|
fs.Infof(name, "Upload Temp Rest Time: %v", f.opt.TempWaitTime)
|
|
fs.Infof(name, "Upload Temp FS: %v", f.opt.TempWritePath)
|
|
f.backgroundRunner, _ = initBackgroundUploader(f)
|
|
go f.backgroundRunner.run()
|
|
}
|
|
|
|
go func() {
|
|
for {
|
|
time.Sleep(time.Duration(f.opt.ChunkCleanInterval))
|
|
select {
|
|
case <-f.cleanupChan:
|
|
fs.Infof(f, "stopping cleanup")
|
|
return
|
|
default:
|
|
fs.Debugf(f, "starting cleanup")
|
|
f.CleanUpCache(false)
|
|
}
|
|
}
|
|
}()
|
|
|
|
if doChangeNotify := wrappedFs.Features().ChangeNotify; doChangeNotify != nil {
|
|
pollInterval := make(chan time.Duration, 1)
|
|
pollInterval <- time.Duration(f.opt.ChunkCleanInterval)
|
|
doChangeNotify(ctx, f.receiveChangeNotify, pollInterval)
|
|
}
|
|
|
|
f.features = (&fs.Features{
|
|
CanHaveEmptyDirectories: true,
|
|
DuplicateFiles: false, // storage doesn't permit this
|
|
}).Fill(ctx, f).Mask(ctx, wrappedFs).WrapsFs(f, wrappedFs)
|
|
// override only those features that use a temp fs and it doesn't support them
|
|
//f.features.ChangeNotify = f.ChangeNotify
|
|
if f.opt.TempWritePath != "" {
|
|
if f.tempFs.Features().Move == nil {
|
|
f.features.Move = nil
|
|
}
|
|
if f.tempFs.Features().Move == nil {
|
|
f.features.Move = nil
|
|
}
|
|
if f.tempFs.Features().DirMove == nil {
|
|
f.features.DirMove = nil
|
|
}
|
|
if f.tempFs.Features().MergeDirs == nil {
|
|
f.features.MergeDirs = nil
|
|
}
|
|
}
|
|
// even if the wrapped fs doesn't support it, we still want it
|
|
f.features.DirCacheFlush = f.DirCacheFlush
|
|
|
|
rc.Add(rc.Call{
|
|
Path: "cache/expire",
|
|
Fn: f.httpExpireRemote,
|
|
Title: "Purge a remote from cache",
|
|
Help: `
|
|
Purge a remote from the cache backend. Supports either a directory or a file.
|
|
Params:
|
|
- remote = path to remote (required)
|
|
- withData = true/false to delete cached data (chunks) as well (optional)
|
|
|
|
Eg
|
|
|
|
rclone rc cache/expire remote=path/to/sub/folder/
|
|
rclone rc cache/expire remote=/ withData=true
|
|
`,
|
|
})
|
|
|
|
rc.Add(rc.Call{
|
|
Path: "cache/stats",
|
|
Fn: f.httpStats,
|
|
Title: "Get cache stats",
|
|
Help: `
|
|
Show statistics for the cache remote.
|
|
`,
|
|
})
|
|
|
|
rc.Add(rc.Call{
|
|
Path: "cache/fetch",
|
|
Fn: f.rcFetch,
|
|
Title: "Fetch file chunks",
|
|
Help: `
|
|
Ensure the specified file chunks are cached on disk.
|
|
|
|
The chunks= parameter specifies the file chunks to check.
|
|
It takes a comma separated list of array slice indices.
|
|
The slice indices are similar to Python slices: start[:end]
|
|
|
|
start is the 0 based chunk number from the beginning of the file
|
|
to fetch inclusive. end is 0 based chunk number from the beginning
|
|
of the file to fetch exclusive.
|
|
Both values can be negative, in which case they count from the back
|
|
of the file. The value "-5:" represents the last 5 chunks of a file.
|
|
|
|
Some valid examples are:
|
|
":5,-5:" -> the first and last five chunks
|
|
"0,-2" -> the first and the second last chunk
|
|
"0:10" -> the first ten chunks
|
|
|
|
Any parameter with a key that starts with "file" can be used to
|
|
specify files to fetch, e.g.
|
|
|
|
rclone rc cache/fetch chunks=0 file=hello file2=home/goodbye
|
|
|
|
File names will automatically be encrypted when the a crypt remote
|
|
is used on top of the cache.
|
|
|
|
`,
|
|
})
|
|
|
|
return f, fsErr
|
|
}
|
|
|
|
func (f *Fs) httpStats(ctx context.Context, in rc.Params) (out rc.Params, err error) {
|
|
out = make(rc.Params)
|
|
m, err := f.Stats()
|
|
if err != nil {
|
|
return out, fmt.Errorf("error while getting cache stats")
|
|
}
|
|
out["status"] = "ok"
|
|
out["stats"] = m
|
|
return out, nil
|
|
}
|
|
|
|
func (f *Fs) unwrapRemote(remote string) string {
|
|
remote = cleanPath(remote)
|
|
if remote != "" {
|
|
// if it's wrapped by crypt we need to check what format we got
|
|
if cryptFs, yes := f.isWrappedByCrypt(); yes {
|
|
_, err := cryptFs.DecryptFileName(remote)
|
|
// if it failed to decrypt then it is a decrypted format and we need to encrypt it
|
|
if err != nil {
|
|
return cryptFs.EncryptFileName(remote)
|
|
}
|
|
// else it's an encrypted format and we can use it as it is
|
|
}
|
|
}
|
|
return remote
|
|
}
|
|
|
|
func (f *Fs) httpExpireRemote(ctx context.Context, in rc.Params) (out rc.Params, err error) {
|
|
out = make(rc.Params)
|
|
remoteInt, ok := in["remote"]
|
|
if !ok {
|
|
return out, fmt.Errorf("remote is needed")
|
|
}
|
|
remote := remoteInt.(string)
|
|
withData := false
|
|
_, ok = in["withData"]
|
|
if ok {
|
|
withData = true
|
|
}
|
|
|
|
remote = f.unwrapRemote(remote)
|
|
if !f.cache.HasEntry(path.Join(f.Root(), remote)) {
|
|
return out, fmt.Errorf("%s doesn't exist in cache", remote)
|
|
}
|
|
|
|
co := NewObject(f, remote)
|
|
err = f.cache.GetObject(co)
|
|
if err != nil { // it could be a dir
|
|
cd := NewDirectory(f, remote)
|
|
err := f.cache.ExpireDir(cd)
|
|
if err != nil {
|
|
return out, fmt.Errorf("error expiring directory: %w", err)
|
|
}
|
|
// notify vfs too
|
|
f.notifyChangeUpstream(cd.Remote(), fs.EntryDirectory)
|
|
out["status"] = "ok"
|
|
out["message"] = fmt.Sprintf("cached directory cleared: %v", remote)
|
|
return out, nil
|
|
}
|
|
// expire the entry
|
|
err = f.cache.ExpireObject(co, withData)
|
|
if err != nil {
|
|
return out, fmt.Errorf("error expiring file: %w", err)
|
|
}
|
|
// notify vfs too
|
|
f.notifyChangeUpstream(co.Remote(), fs.EntryObject)
|
|
|
|
out["status"] = "ok"
|
|
out["message"] = fmt.Sprintf("cached file cleared: %v", remote)
|
|
return out, nil
|
|
}
|
|
|
|
func (f *Fs) rcFetch(ctx context.Context, in rc.Params) (rc.Params, error) {
|
|
type chunkRange struct {
|
|
start, end int64
|
|
}
|
|
parseChunks := func(ranges string) (crs []chunkRange, err error) {
|
|
for _, part := range strings.Split(ranges, ",") {
|
|
var start, end int64 = 0, math.MaxInt64
|
|
switch ints := strings.Split(part, ":"); len(ints) {
|
|
case 1:
|
|
start, err = strconv.ParseInt(ints[0], 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid range: %q", part)
|
|
}
|
|
end = start + 1
|
|
case 2:
|
|
if ints[0] != "" {
|
|
start, err = strconv.ParseInt(ints[0], 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid range: %q", part)
|
|
}
|
|
}
|
|
if ints[1] != "" {
|
|
end, err = strconv.ParseInt(ints[1], 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid range: %q", part)
|
|
}
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("invalid range: %q", part)
|
|
}
|
|
crs = append(crs, chunkRange{start: start, end: end})
|
|
}
|
|
return
|
|
}
|
|
walkChunkRange := func(cr chunkRange, size int64, cb func(chunk int64)) {
|
|
if size <= 0 {
|
|
return
|
|
}
|
|
chunks := (size-1)/f.ChunkSize() + 1
|
|
|
|
start, end := cr.start, cr.end
|
|
if start < 0 {
|
|
start += chunks
|
|
}
|
|
if end <= 0 {
|
|
end += chunks
|
|
}
|
|
if end <= start {
|
|
return
|
|
}
|
|
switch {
|
|
case start < 0:
|
|
start = 0
|
|
case start >= chunks:
|
|
return
|
|
}
|
|
switch {
|
|
case end <= start:
|
|
end = start + 1
|
|
case end >= chunks:
|
|
end = chunks
|
|
}
|
|
for i := start; i < end; i++ {
|
|
cb(i)
|
|
}
|
|
}
|
|
walkChunkRanges := func(crs []chunkRange, size int64, cb func(chunk int64)) {
|
|
for _, cr := range crs {
|
|
walkChunkRange(cr, size, cb)
|
|
}
|
|
}
|
|
|
|
v, ok := in["chunks"]
|
|
if !ok {
|
|
return nil, errors.New("missing chunks parameter")
|
|
}
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return nil, errors.New("invalid chunks parameter")
|
|
}
|
|
delete(in, "chunks")
|
|
crs, err := parseChunks(s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid chunks parameter: %w", err)
|
|
}
|
|
var files [][2]string
|
|
for k, v := range in {
|
|
if !strings.HasPrefix(k, "file") {
|
|
return nil, fmt.Errorf("invalid parameter %s=%s", k, v)
|
|
}
|
|
switch v := v.(type) {
|
|
case string:
|
|
files = append(files, [2]string{v, f.unwrapRemote(v)})
|
|
default:
|
|
return nil, fmt.Errorf("invalid parameter %s=%s", k, v)
|
|
}
|
|
}
|
|
type fileStatus struct {
|
|
Error string
|
|
FetchedChunks int
|
|
}
|
|
fetchedChunks := make(map[string]fileStatus, len(files))
|
|
for _, pair := range files {
|
|
file, remote := pair[0], pair[1]
|
|
var status fileStatus
|
|
o, err := f.NewObject(ctx, remote)
|
|
if err != nil {
|
|
fetchedChunks[file] = fileStatus{Error: err.Error()}
|
|
continue
|
|
}
|
|
co := o.(*Object)
|
|
err = co.refreshFromSource(ctx, true)
|
|
if err != nil {
|
|
fetchedChunks[file] = fileStatus{Error: err.Error()}
|
|
continue
|
|
}
|
|
handle := NewObjectHandle(ctx, co, f)
|
|
handle.UseMemory = false
|
|
handle.scaleWorkers(1)
|
|
walkChunkRanges(crs, co.Size(), func(chunk int64) {
|
|
_, err := handle.getChunk(chunk * f.ChunkSize())
|
|
if err != nil {
|
|
if status.Error == "" {
|
|
status.Error = err.Error()
|
|
}
|
|
} else {
|
|
status.FetchedChunks++
|
|
}
|
|
})
|
|
fetchedChunks[file] = status
|
|
}
|
|
|
|
return rc.Params{"status": fetchedChunks}, nil
|
|
}
|
|
|
|
// receiveChangeNotify is a wrapper to notifications sent from the wrapped FS about changed files
|
|
func (f *Fs) receiveChangeNotify(forgetPath string, entryType fs.EntryType) {
|
|
if crypt, yes := f.isWrappedByCrypt(); yes {
|
|
decryptedPath, err := crypt.DecryptFileName(forgetPath)
|
|
if err == nil {
|
|
fs.Infof(decryptedPath, "received cache expiry notification")
|
|
} else {
|
|
fs.Infof(forgetPath, "received cache expiry notification")
|
|
}
|
|
} else {
|
|
fs.Infof(forgetPath, "received cache expiry notification")
|
|
}
|
|
// notify upstreams too (vfs)
|
|
f.notifyChangeUpstream(forgetPath, entryType)
|
|
|
|
var cd *Directory
|
|
if entryType == fs.EntryObject {
|
|
co := NewObject(f, forgetPath)
|
|
err := f.cache.GetObject(co)
|
|
if err != nil {
|
|
fs.Debugf(f, "got change notification for non cached entry %v", co)
|
|
}
|
|
err = f.cache.ExpireObject(co, true)
|
|
if err != nil {
|
|
fs.Debugf(forgetPath, "notify: error expiring '%v': %v", co, err)
|
|
}
|
|
cd = NewDirectory(f, cleanPath(path.Dir(co.Remote())))
|
|
} else {
|
|
cd = NewDirectory(f, forgetPath)
|
|
}
|
|
// we expire the dir
|
|
err := f.cache.ExpireDir(cd)
|
|
if err != nil {
|
|
fs.Debugf(forgetPath, "notify: error expiring '%v': %v", cd, err)
|
|
} else {
|
|
fs.Debugf(forgetPath, "notify: expired '%v'", cd)
|
|
}
|
|
|
|
f.notifiedMu.Lock()
|
|
defer f.notifiedMu.Unlock()
|
|
f.notifiedRemotes[forgetPath] = true
|
|
f.notifiedRemotes[cd.Remote()] = true
|
|
}
|
|
|
|
// notifyChangeUpstreamIfNeeded will check if the wrapped remote doesn't notify on changes
|
|
// or if we use a temp fs
|
|
func (f *Fs) notifyChangeUpstreamIfNeeded(remote string, entryType fs.EntryType) {
|
|
if f.Fs.Features().ChangeNotify == nil || f.opt.TempWritePath != "" {
|
|
f.notifyChangeUpstream(remote, entryType)
|
|
}
|
|
}
|
|
|
|
// notifyChangeUpstream will loop through all the upstreams and notify
|
|
// of the provided remote (should be only a dir)
|
|
func (f *Fs) notifyChangeUpstream(remote string, entryType fs.EntryType) {
|
|
f.parentsForgetMu.Lock()
|
|
defer f.parentsForgetMu.Unlock()
|
|
if len(f.parentsForgetFn) > 0 {
|
|
for _, fn := range f.parentsForgetFn {
|
|
fn(remote, entryType)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ChangeNotify can subscribe multiple callers
|
|
// this is coupled with the wrapped fs ChangeNotify (if it supports it)
|
|
// and also notifies other caches (i.e VFS) to clear out whenever something changes
|
|
func (f *Fs) ChangeNotify(ctx context.Context, notifyFunc func(string, fs.EntryType), pollInterval <-chan time.Duration) {
|
|
f.parentsForgetMu.Lock()
|
|
defer f.parentsForgetMu.Unlock()
|
|
fs.Debugf(f, "subscribing to ChangeNotify")
|
|
f.parentsForgetFn = append(f.parentsForgetFn, notifyFunc)
|
|
go func() {
|
|
for range pollInterval {
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Name of the remote (as passed into NewFs)
|
|
func (f *Fs) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
// Root of the remote (as passed into NewFs)
|
|
func (f *Fs) Root() string {
|
|
return f.root
|
|
}
|
|
|
|
// Features returns the optional features of this Fs
|
|
func (f *Fs) Features() *fs.Features {
|
|
return f.features
|
|
}
|
|
|
|
// String returns a description of the FS
|
|
func (f *Fs) String() string {
|
|
return fmt.Sprintf("Cache remote %s:%s", f.name, f.root)
|
|
}
|
|
|
|
// ChunkSize returns the configured chunk size
|
|
func (f *Fs) ChunkSize() int64 {
|
|
return int64(f.opt.ChunkSize)
|
|
}
|
|
|
|
// InfoAge returns the configured file age
|
|
func (f *Fs) InfoAge() time.Duration {
|
|
return time.Duration(f.opt.InfoAge)
|
|
}
|
|
|
|
// TempUploadWaitTime returns the configured temp file upload wait time
|
|
func (f *Fs) TempUploadWaitTime() time.Duration {
|
|
return time.Duration(f.opt.TempWaitTime)
|
|
}
|
|
|
|
// NewObject finds the Object at remote.
|
|
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
|
var err error
|
|
|
|
fs.Debugf(f, "new object '%s'", remote)
|
|
co := NewObject(f, remote)
|
|
// search for entry in cache and validate it
|
|
err = f.cache.GetObject(co)
|
|
if err != nil {
|
|
fs.Debugf(remote, "find: error: %v", err)
|
|
} else if time.Now().After(co.CacheTs.Add(time.Duration(f.opt.InfoAge))) {
|
|
fs.Debugf(co, "find: cold object: %+v", co)
|
|
} else {
|
|
fs.Debugf(co, "find: warm object: %v, expiring on: %v", co, co.CacheTs.Add(time.Duration(f.opt.InfoAge)))
|
|
return co, nil
|
|
}
|
|
|
|
// search for entry in source or temp fs
|
|
var obj fs.Object
|
|
if f.opt.TempWritePath != "" {
|
|
obj, err = f.tempFs.NewObject(ctx, remote)
|
|
// not found in temp fs
|
|
if err != nil {
|
|
fs.Debugf(remote, "find: not found in local cache fs")
|
|
obj, err = f.Fs.NewObject(ctx, remote)
|
|
} else {
|
|
fs.Debugf(obj, "find: found in local cache fs")
|
|
}
|
|
} else {
|
|
obj, err = f.Fs.NewObject(ctx, remote)
|
|
}
|
|
|
|
// not found in either fs
|
|
if err != nil {
|
|
fs.Debugf(obj, "find failed: not found in either local or remote fs")
|
|
return nil, err
|
|
}
|
|
|
|
// cache the new entry
|
|
co = ObjectFromOriginal(ctx, f, obj).persist()
|
|
fs.Debugf(co, "find: cached object")
|
|
return co, nil
|
|
}
|
|
|
|
// List the objects and directories in dir into entries
|
|
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
|
|
fs.Debugf(f, "list '%s'", dir)
|
|
cd := ShallowDirectory(f, dir)
|
|
|
|
// search for cached dir entries and validate them
|
|
entries, err = f.cache.GetDirEntries(cd)
|
|
if err != nil {
|
|
fs.Debugf(dir, "list: error: %v", err)
|
|
} else if time.Now().After(cd.CacheTs.Add(time.Duration(f.opt.InfoAge))) {
|
|
fs.Debugf(dir, "list: cold listing: %v", cd.CacheTs)
|
|
} else if len(entries) == 0 {
|
|
// TODO: read empty dirs from source?
|
|
fs.Debugf(dir, "list: empty listing")
|
|
} else {
|
|
fs.Debugf(dir, "list: warm %v from cache for: %v, expiring on: %v", len(entries), cd.abs(), cd.CacheTs.Add(time.Duration(f.opt.InfoAge)))
|
|
fs.Debugf(dir, "list: cached entries: %v", entries)
|
|
return entries, nil
|
|
}
|
|
|
|
// we first search any temporary files stored locally
|
|
var cachedEntries fs.DirEntries
|
|
if f.opt.TempWritePath != "" {
|
|
queuedEntries, err := f.cache.searchPendingUploadFromDir(cd.abs())
|
|
if err != nil {
|
|
fs.Errorf(dir, "list: error getting pending uploads: %v", err)
|
|
} else {
|
|
fs.Debugf(dir, "list: read %v from temp fs", len(queuedEntries))
|
|
fs.Debugf(dir, "list: temp fs entries: %v", queuedEntries)
|
|
|
|
for _, queuedRemote := range queuedEntries {
|
|
queuedEntry, err := f.tempFs.NewObject(ctx, f.cleanRootFromPath(queuedRemote))
|
|
if err != nil {
|
|
fs.Debugf(dir, "list: temp file not found in local fs: %v", err)
|
|
continue
|
|
}
|
|
co := ObjectFromOriginal(ctx, f, queuedEntry).persist()
|
|
fs.Debugf(co, "list: cached temp object")
|
|
cachedEntries = append(cachedEntries, co)
|
|
}
|
|
}
|
|
}
|
|
|
|
// search from the source
|
|
sourceEntries, err := f.Fs.List(ctx, dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fs.Debugf(dir, "list: read %v from source", len(sourceEntries))
|
|
fs.Debugf(dir, "list: source entries: %v", sourceEntries)
|
|
|
|
sort.Sort(sourceEntries)
|
|
for _, entry := range entries {
|
|
entryRemote := entry.Remote()
|
|
i := sort.Search(len(sourceEntries), func(i int) bool { return sourceEntries[i].Remote() >= entryRemote })
|
|
if i < len(sourceEntries) && sourceEntries[i].Remote() == entryRemote {
|
|
continue
|
|
}
|
|
fp := path.Join(f.Root(), entryRemote)
|
|
switch entry.(type) {
|
|
case fs.Object:
|
|
_ = f.cache.RemoveObject(fp)
|
|
case fs.Directory:
|
|
_ = f.cache.RemoveDir(fp)
|
|
}
|
|
fs.Debugf(dir, "list: remove entry: %v", entryRemote)
|
|
}
|
|
entries = nil //nolint:ineffassign
|
|
|
|
// and then iterate over the ones from source (temp Objects will override source ones)
|
|
var batchDirectories []*Directory
|
|
sort.Sort(cachedEntries)
|
|
tmpCnt := len(cachedEntries)
|
|
for _, entry := range sourceEntries {
|
|
switch o := entry.(type) {
|
|
case fs.Object:
|
|
// skip over temporary objects (might be uploading)
|
|
oRemote := o.Remote()
|
|
i := sort.Search(tmpCnt, func(i int) bool { return cachedEntries[i].Remote() >= oRemote })
|
|
if i < tmpCnt && cachedEntries[i].Remote() == oRemote {
|
|
continue
|
|
}
|
|
co := ObjectFromOriginal(ctx, f, o).persist()
|
|
cachedEntries = append(cachedEntries, co)
|
|
fs.Debugf(dir, "list: cached object: %v", co)
|
|
case fs.Directory:
|
|
cdd := DirectoryFromOriginal(ctx, f, o)
|
|
// check if the dir isn't expired and add it in cache if it isn't
|
|
if cdd2, err := f.cache.GetDir(cdd.abs()); err != nil || time.Now().Before(cdd2.CacheTs.Add(time.Duration(f.opt.InfoAge))) {
|
|
batchDirectories = append(batchDirectories, cdd)
|
|
}
|
|
cachedEntries = append(cachedEntries, cdd)
|
|
default:
|
|
fs.Debugf(entry, "list: Unknown object type %T", entry)
|
|
}
|
|
}
|
|
err = f.cache.AddBatchDir(batchDirectories)
|
|
if err != nil {
|
|
fs.Errorf(dir, "list: error caching directories from listing %v", dir)
|
|
} else {
|
|
fs.Debugf(dir, "list: cached directories: %v", len(batchDirectories))
|
|
}
|
|
|
|
// cache dir meta
|
|
t := time.Now()
|
|
cd.CacheTs = &t
|
|
err = f.cache.AddDir(cd)
|
|
if err != nil {
|
|
fs.Errorf(cd, "list: save error: '%v'", err)
|
|
} else {
|
|
fs.Debugf(dir, "list: cached dir: '%v', cache ts: %v", cd.abs(), cd.CacheTs)
|
|
}
|
|
|
|
return cachedEntries, nil
|
|
}
|
|
|
|
func (f *Fs) recurse(ctx context.Context, dir string, list *walk.ListRHelper) error {
|
|
entries, err := f.List(ctx, dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := 0; i < len(entries); i++ {
|
|
innerDir, ok := entries[i].(fs.Directory)
|
|
if ok {
|
|
err := f.recurse(ctx, innerDir.Remote(), list)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err := list.Add(entries[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListR lists the objects and directories of the Fs starting
|
|
// from dir recursively into out.
|
|
func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (err error) {
|
|
fs.Debugf(f, "list recursively from '%s'", dir)
|
|
|
|
// we check if the source FS supports ListR
|
|
// if it does, we'll use that to get all the entries, cache them and return
|
|
do := f.Fs.Features().ListR
|
|
if do != nil {
|
|
return do(ctx, dir, func(entries fs.DirEntries) error {
|
|
// we got called back with a set of entries so let's cache them and call the original callback
|
|
for _, entry := range entries {
|
|
switch o := entry.(type) {
|
|
case fs.Object:
|
|
_ = f.cache.AddObject(ObjectFromOriginal(ctx, f, o))
|
|
case fs.Directory:
|
|
_ = f.cache.AddDir(DirectoryFromOriginal(ctx, f, o))
|
|
default:
|
|
return fmt.Errorf("unknown object type %T", entry)
|
|
}
|
|
}
|
|
|
|
// call the original callback
|
|
return callback(entries)
|
|
})
|
|
}
|
|
|
|
// if we're here, we're gonna do a standard recursive traversal and cache everything
|
|
list := walk.NewListRHelper(callback)
|
|
err = f.recurse(ctx, dir, list)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return list.Flush()
|
|
}
|
|
|
|
// Mkdir makes the directory (container, bucket)
|
|
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
|
|
fs.Debugf(f, "mkdir '%s'", dir)
|
|
err := f.Fs.Mkdir(ctx, dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fs.Debugf(dir, "mkdir: created dir in source fs")
|
|
|
|
cd := NewDirectory(f, cleanPath(dir))
|
|
err = f.cache.AddDir(cd)
|
|
if err != nil {
|
|
fs.Errorf(dir, "mkdir: add error: %v", err)
|
|
} else {
|
|
fs.Debugf(cd, "mkdir: added to cache")
|
|
}
|
|
// expire parent of new dir
|
|
parentCd := NewDirectory(f, cleanPath(path.Dir(dir)))
|
|
err = f.cache.ExpireDir(parentCd)
|
|
if err != nil {
|
|
fs.Errorf(parentCd, "mkdir: cache expire error: %v", err)
|
|
} else {
|
|
fs.Infof(parentCd, "mkdir: cache expired")
|
|
}
|
|
// advertise to ChangeNotify if wrapped doesn't do that
|
|
f.notifyChangeUpstreamIfNeeded(parentCd.Remote(), fs.EntryDirectory)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Rmdir removes the directory (container, bucket) if empty
|
|
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
|
fs.Debugf(f, "rmdir '%s'", dir)
|
|
|
|
if f.opt.TempWritePath != "" {
|
|
// pause background uploads
|
|
f.backgroundRunner.pause()
|
|
defer f.backgroundRunner.play()
|
|
|
|
// we check if the source exists on the remote and make the same move on it too if it does
|
|
// otherwise, we skip this step
|
|
_, err := f.UnWrap().List(ctx, dir)
|
|
if err == nil {
|
|
err := f.Fs.Rmdir(ctx, dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fs.Debugf(dir, "rmdir: removed dir in source fs")
|
|
}
|
|
|
|
var queuedEntries []*Object
|
|
err = walk.ListR(ctx, f.tempFs, dir, true, -1, walk.ListObjects, func(entries fs.DirEntries) error {
|
|
for _, o := range entries {
|
|
if oo, ok := o.(fs.Object); ok {
|
|
co := ObjectFromOriginal(ctx, f, oo)
|
|
queuedEntries = append(queuedEntries, co)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
fs.Errorf(dir, "rmdir: error getting pending uploads: %v", err)
|
|
} else {
|
|
fs.Debugf(dir, "rmdir: read %v from temp fs", len(queuedEntries))
|
|
fs.Debugf(dir, "rmdir: temp fs entries: %v", queuedEntries)
|
|
if len(queuedEntries) > 0 {
|
|
fs.Errorf(dir, "rmdir: temporary dir not empty: %v", queuedEntries)
|
|
return fs.ErrorDirectoryNotEmpty
|
|
}
|
|
}
|
|
} else {
|
|
err := f.Fs.Rmdir(ctx, dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fs.Debugf(dir, "rmdir: removed dir in source fs")
|
|
}
|
|
|
|
// remove dir data
|
|
d := NewDirectory(f, dir)
|
|
err := f.cache.RemoveDir(d.abs())
|
|
if err != nil {
|
|
fs.Errorf(dir, "rmdir: remove error: %v", err)
|
|
} else {
|
|
fs.Debugf(d, "rmdir: removed from cache")
|
|
}
|
|
// expire parent
|
|
parentCd := NewDirectory(f, cleanPath(path.Dir(dir)))
|
|
err = f.cache.ExpireDir(parentCd)
|
|
if err != nil {
|
|
fs.Errorf(dir, "rmdir: cache expire error: %v", err)
|
|
} else {
|
|
fs.Infof(parentCd, "rmdir: cache expired")
|
|
}
|
|
// advertise to ChangeNotify if wrapped doesn't do that
|
|
f.notifyChangeUpstreamIfNeeded(parentCd.Remote(), fs.EntryDirectory)
|
|
|
|
return nil
|
|
}
|
|
|
|
// DirMove moves src, srcRemote to this remote at dstRemote
|
|
// using server-side move operations.
|
|
func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
|
|
fs.Debugf(f, "move dir '%s'/'%s' -> '%s'/'%s'", src.Root(), srcRemote, f.Root(), dstRemote)
|
|
|
|
do := f.Fs.Features().DirMove
|
|
if do == nil {
|
|
return fs.ErrorCantDirMove
|
|
}
|
|
srcFs, ok := src.(*Fs)
|
|
if !ok {
|
|
fs.Errorf(srcFs, "can't move directory - not same remote type")
|
|
return fs.ErrorCantDirMove
|
|
}
|
|
if srcFs.Fs.Name() != f.Fs.Name() {
|
|
fs.Errorf(srcFs, "can't move directory - not wrapping same remotes")
|
|
return fs.ErrorCantDirMove
|
|
}
|
|
|
|
if f.opt.TempWritePath != "" {
|
|
// pause background uploads
|
|
f.backgroundRunner.pause()
|
|
defer f.backgroundRunner.play()
|
|
|
|
_, errInWrap := srcFs.UnWrap().List(ctx, srcRemote)
|
|
_, errInTemp := f.tempFs.List(ctx, srcRemote)
|
|
// not found in either fs
|
|
if errInWrap != nil && errInTemp != nil {
|
|
return fs.ErrorDirNotFound
|
|
}
|
|
|
|
// we check if the source exists on the remote and make the same move on it too if it does
|
|
// otherwise, we skip this step
|
|
if errInWrap == nil {
|
|
err := do(ctx, srcFs.UnWrap(), srcRemote, dstRemote)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fs.Debugf(srcRemote, "movedir: dir moved in the source fs")
|
|
}
|
|
// we need to check if the directory exists in the temp fs
|
|
// and skip the move if it doesn't
|
|
if errInTemp != nil {
|
|
goto cleanup
|
|
}
|
|
|
|
var queuedEntries []*Object
|
|
err := walk.ListR(ctx, f.tempFs, srcRemote, true, -1, walk.ListObjects, func(entries fs.DirEntries) error {
|
|
for _, o := range entries {
|
|
if oo, ok := o.(fs.Object); ok {
|
|
co := ObjectFromOriginal(ctx, f, oo)
|
|
queuedEntries = append(queuedEntries, co)
|
|
if co.tempFileStartedUpload() {
|
|
fs.Errorf(co, "can't move - upload has already started. need to finish that")
|
|
return fs.ErrorCantDirMove
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fs.Debugf(srcRemote, "dirmove: read %v from temp fs", len(queuedEntries))
|
|
fs.Debugf(srcRemote, "dirmove: temp fs entries: %v", queuedEntries)
|
|
|
|
do := f.tempFs.Features().DirMove
|
|
if do == nil {
|
|
fs.Errorf(srcRemote, "dirmove: can't move dir in temp fs")
|
|
return fs.ErrorCantDirMove
|
|
}
|
|
err = do(ctx, f.tempFs, srcRemote, dstRemote)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = f.cache.ReconcileTempUploads(ctx, f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
err := do(ctx, srcFs.UnWrap(), srcRemote, dstRemote)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fs.Debugf(srcRemote, "movedir: dir moved in the source fs")
|
|
}
|
|
cleanup:
|
|
|
|
// delete src dir from cache along with all chunks
|
|
srcDir := NewDirectory(srcFs, srcRemote)
|
|
err := f.cache.RemoveDir(srcDir.abs())
|
|
if err != nil {
|
|
fs.Errorf(srcDir, "dirmove: remove error: %v", err)
|
|
} else {
|
|
fs.Debugf(srcDir, "dirmove: removed cached dir")
|
|
}
|
|
// expire src parent
|
|
srcParent := NewDirectory(f, cleanPath(path.Dir(srcRemote)))
|
|
err = f.cache.ExpireDir(srcParent)
|
|
if err != nil {
|
|
fs.Errorf(srcParent, "dirmove: cache expire error: %v", err)
|
|
} else {
|
|
fs.Debugf(srcParent, "dirmove: cache expired")
|
|
}
|
|
// advertise to ChangeNotify if wrapped doesn't do that
|
|
f.notifyChangeUpstreamIfNeeded(srcParent.Remote(), fs.EntryDirectory)
|
|
|
|
// expire parent dir at the destination path
|
|
dstParent := NewDirectory(f, cleanPath(path.Dir(dstRemote)))
|
|
err = f.cache.ExpireDir(dstParent)
|
|
if err != nil {
|
|
fs.Errorf(dstParent, "dirmove: cache expire error: %v", err)
|
|
} else {
|
|
fs.Debugf(dstParent, "dirmove: cache expired")
|
|
}
|
|
// advertise to ChangeNotify if wrapped doesn't do that
|
|
f.notifyChangeUpstreamIfNeeded(dstParent.Remote(), fs.EntryDirectory)
|
|
// TODO: precache dst dir and save the chunks
|
|
|
|
return nil
|
|
}
|
|
|
|
// cacheReader will split the stream of a reader to be cached at the same time it is read by the original source
|
|
func (f *Fs) cacheReader(u io.Reader, src fs.ObjectInfo, originalRead func(inn io.Reader)) {
|
|
// create the pipe and tee reader
|
|
pr, pw := io.Pipe()
|
|
tr := io.TeeReader(u, pw)
|
|
|
|
// create channel to synchronize
|
|
done := make(chan bool)
|
|
defer close(done)
|
|
|
|
go func() {
|
|
// notify the cache reader that we're complete after the source FS finishes
|
|
defer func() {
|
|
_ = pw.Close()
|
|
}()
|
|
// process original reading
|
|
originalRead(tr)
|
|
// signal complete
|
|
done <- true
|
|
}()
|
|
|
|
go func() {
|
|
var offset int64
|
|
for {
|
|
chunk := make([]byte, f.opt.ChunkSize)
|
|
readSize, err := io.ReadFull(pr, chunk)
|
|
// we ignore 3 failures which are ok:
|
|
// 1. EOF - original reading finished and we got a full buffer too
|
|
// 2. ErrUnexpectedEOF - original reading finished and partial buffer
|
|
// 3. ErrClosedPipe - source remote reader was closed (usually means it reached the end) and we need to stop too
|
|
// if we have a different error: we're going to error out the original reading too and stop this
|
|
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF && err != io.ErrClosedPipe {
|
|
fs.Errorf(src, "error saving new data in cache. offset: %v, err: %v", offset, err)
|
|
_ = pr.CloseWithError(err)
|
|
break
|
|
}
|
|
// if we have some bytes we cache them
|
|
if readSize > 0 {
|
|
chunk = chunk[:readSize]
|
|
err2 := f.cache.AddChunk(cleanPath(path.Join(f.root, src.Remote())), chunk, offset)
|
|
if err2 != nil {
|
|
fs.Errorf(src, "error saving new data in cache '%v'", err2)
|
|
_ = pr.CloseWithError(err2)
|
|
break
|
|
}
|
|
offset += int64(readSize)
|
|
}
|
|
// stuff should be closed but let's be sure
|
|
if err == io.EOF || err == io.ErrUnexpectedEOF || err == io.ErrClosedPipe {
|
|
_ = pr.Close()
|
|
break
|
|
}
|
|
}
|
|
|
|
// signal complete
|
|
done <- true
|
|
}()
|
|
|
|
// wait until both are done
|
|
for c := 0; c < 2; c++ {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
type putFn func(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error)
|
|
|
|
// put in to the remote path
|
|
func (f *Fs) put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options []fs.OpenOption, put putFn) (fs.Object, error) {
|
|
var err error
|
|
var obj fs.Object
|
|
|
|
// queue for upload and store in temp fs if configured
|
|
if f.opt.TempWritePath != "" {
|
|
// we need to clear the caches before a put through temp fs
|
|
parentCd := NewDirectory(f, cleanPath(path.Dir(src.Remote())))
|
|
_ = f.cache.ExpireDir(parentCd)
|
|
f.notifyChangeUpstreamIfNeeded(parentCd.Remote(), fs.EntryDirectory)
|
|
|
|
obj, err = f.tempFs.Put(ctx, in, src, options...)
|
|
if err != nil {
|
|
fs.Errorf(obj, "put: failed to upload in temp fs: %v", err)
|
|
return nil, err
|
|
}
|
|
fs.Infof(obj, "put: uploaded in temp fs")
|
|
err = f.cache.addPendingUpload(path.Join(f.Root(), src.Remote()), false)
|
|
if err != nil {
|
|
fs.Errorf(obj, "put: failed to queue for upload: %v", err)
|
|
return nil, err
|
|
}
|
|
fs.Infof(obj, "put: queued for upload")
|
|
// if cache writes is enabled write it first through cache
|
|
} else if f.opt.StoreWrites {
|
|
f.cacheReader(in, src, func(inn io.Reader) {
|
|
obj, err = put(ctx, inn, src, options...)
|
|
})
|
|
if err == nil {
|
|
fs.Debugf(obj, "put: uploaded to remote fs and saved in cache")
|
|
}
|
|
// last option: save it directly in remote fs
|
|
} else {
|
|
obj, err = put(ctx, in, src, options...)
|
|
if err == nil {
|
|
fs.Debugf(obj, "put: uploaded to remote fs")
|
|
}
|
|
}
|
|
// validate and stop if errors are found
|
|
if err != nil {
|
|
fs.Errorf(src, "put: error uploading: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
// cache the new file
|
|
cachedObj := ObjectFromOriginal(ctx, f, obj)
|
|
|
|
// deleting cached chunks and info to be replaced with new ones
|
|
_ = f.cache.RemoveObject(cachedObj.abs())
|
|
|
|
cachedObj.persist()
|
|
fs.Debugf(cachedObj, "put: added to cache")
|
|
|
|
// expire parent
|
|
parentCd := NewDirectory(f, cleanPath(path.Dir(cachedObj.Remote())))
|
|
err = f.cache.ExpireDir(parentCd)
|
|
if err != nil {
|
|
fs.Errorf(cachedObj, "put: cache expire error: %v", err)
|
|
} else {
|
|
fs.Infof(parentCd, "put: cache expired")
|
|
}
|
|
// advertise to ChangeNotify
|
|
f.notifyChangeUpstreamIfNeeded(parentCd.Remote(), fs.EntryDirectory)
|
|
|
|
return cachedObj, nil
|
|
}
|
|
|
|
// Put in to the remote path with the modTime given of the given size
|
|
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
|
fs.Debugf(f, "put data at '%s'", src.Remote())
|
|
return f.put(ctx, in, src, options, f.Fs.Put)
|
|
}
|
|
|
|
// PutUnchecked uploads the object
|
|
func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
|
do := f.Fs.Features().PutUnchecked
|
|
if do == nil {
|
|
return nil, errors.New("can't PutUnchecked")
|
|
}
|
|
fs.Debugf(f, "put data unchecked in '%s'", src.Remote())
|
|
return f.put(ctx, in, src, options, do)
|
|
}
|
|
|
|
// PutStream uploads the object
|
|
func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
|
do := f.Fs.Features().PutStream
|
|
if do == nil {
|
|
return nil, errors.New("can't PutStream")
|
|
}
|
|
fs.Debugf(f, "put data streaming in '%s'", src.Remote())
|
|
return f.put(ctx, in, src, options, do)
|
|
}
|
|
|
|
// Copy src to this remote using server-side copy operations.
|
|
func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
|
|
fs.Debugf(f, "copy obj '%s' -> '%s'", src, remote)
|
|
|
|
do := f.Fs.Features().Copy
|
|
if do == nil {
|
|
fs.Errorf(src, "source remote (%v) doesn't support Copy", src.Fs())
|
|
return nil, fs.ErrorCantCopy
|
|
}
|
|
if f.opt.TempWritePath != "" && src.Fs() == f.tempFs {
|
|
return nil, fs.ErrorCantCopy
|
|
}
|
|
// the source must be a cached object or we abort
|
|
srcObj, ok := src.(*Object)
|
|
if !ok {
|
|
fs.Errorf(srcObj, "can't copy - not same remote type")
|
|
return nil, fs.ErrorCantCopy
|
|
}
|
|
// both the source cache fs and this cache fs need to wrap the same remote
|
|
if srcObj.CacheFs.Fs.Name() != f.Fs.Name() {
|
|
fs.Errorf(srcObj, "can't copy - not wrapping same remotes")
|
|
return nil, fs.ErrorCantCopy
|
|
}
|
|
// refresh from source or abort
|
|
if err := srcObj.refreshFromSource(ctx, false); err != nil {
|
|
fs.Errorf(f, "can't copy %v - %v", src, err)
|
|
return nil, fs.ErrorCantCopy
|
|
}
|
|
|
|
if srcObj.isTempFile() {
|
|
// we check if the feature is still active
|
|
if f.opt.TempWritePath == "" {
|
|
fs.Errorf(srcObj, "can't copy - this is a local cached file but this feature is turned off this run")
|
|
return nil, fs.ErrorCantCopy
|
|
}
|
|
|
|
do = srcObj.ParentFs.Features().Copy
|
|
if do == nil {
|
|
fs.Errorf(src, "parent remote (%v) doesn't support Copy", srcObj.ParentFs)
|
|
return nil, fs.ErrorCantCopy
|
|
}
|
|
}
|
|
|
|
obj, err := do(ctx, srcObj.Object, remote)
|
|
if err != nil {
|
|
fs.Errorf(srcObj, "error moving in cache: %v", err)
|
|
return nil, err
|
|
}
|
|
fs.Debugf(obj, "copy: file copied")
|
|
|
|
// persist new
|
|
co := ObjectFromOriginal(ctx, f, obj).persist()
|
|
fs.Debugf(co, "copy: added to cache")
|
|
// expire the destination path
|
|
parentCd := NewDirectory(f, cleanPath(path.Dir(co.Remote())))
|
|
err = f.cache.ExpireDir(parentCd)
|
|
if err != nil {
|
|
fs.Errorf(parentCd, "copy: cache expire error: %v", err)
|
|
} else {
|
|
fs.Infof(parentCd, "copy: cache expired")
|
|
}
|
|
// advertise to ChangeNotify if wrapped doesn't do that
|
|
f.notifyChangeUpstreamIfNeeded(parentCd.Remote(), fs.EntryDirectory)
|
|
// expire src parent
|
|
srcParent := NewDirectory(f, cleanPath(path.Dir(src.Remote())))
|
|
err = f.cache.ExpireDir(srcParent)
|
|
if err != nil {
|
|
fs.Errorf(srcParent, "copy: cache expire error: %v", err)
|
|
} else {
|
|
fs.Infof(srcParent, "copy: cache expired")
|
|
}
|
|
// advertise to ChangeNotify if wrapped doesn't do that
|
|
f.notifyChangeUpstreamIfNeeded(srcParent.Remote(), fs.EntryDirectory)
|
|
|
|
return co, nil
|
|
}
|
|
|
|
// Move src to this remote using server-side move operations.
|
|
func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
|
|
fs.Debugf(f, "moving obj '%s' -> %s", src, remote)
|
|
|
|
// if source fs doesn't support move abort
|
|
do := f.Fs.Features().Move
|
|
if do == nil {
|
|
fs.Errorf(src, "source remote (%v) doesn't support Move", src.Fs())
|
|
return nil, fs.ErrorCantMove
|
|
}
|
|
// the source must be a cached object or we abort
|
|
srcObj, ok := src.(*Object)
|
|
if !ok {
|
|
fs.Errorf(srcObj, "can't move - not same remote type")
|
|
return nil, fs.ErrorCantMove
|
|
}
|
|
// both the source cache fs and this cache fs need to wrap the same remote
|
|
if srcObj.CacheFs.Fs.Name() != f.Fs.Name() {
|
|
fs.Errorf(srcObj, "can't move - not wrapping same remote types")
|
|
return nil, fs.ErrorCantMove
|
|
}
|
|
// refresh from source or abort
|
|
if err := srcObj.refreshFromSource(ctx, false); err != nil {
|
|
fs.Errorf(f, "can't move %v - %v", src, err)
|
|
return nil, fs.ErrorCantMove
|
|
}
|
|
|
|
// if this is a temp object then we perform the changes locally
|
|
if srcObj.isTempFile() {
|
|
// we check if the feature is still active
|
|
if f.opt.TempWritePath == "" {
|
|
fs.Errorf(srcObj, "can't move - this is a local cached file but this feature is turned off this run")
|
|
return nil, fs.ErrorCantMove
|
|
}
|
|
// pause background uploads
|
|
f.backgroundRunner.pause()
|
|
defer f.backgroundRunner.play()
|
|
|
|
// started uploads can't be moved until they complete
|
|
if srcObj.tempFileStartedUpload() {
|
|
fs.Errorf(srcObj, "can't move - upload has already started. need to finish that")
|
|
return nil, fs.ErrorCantMove
|
|
}
|
|
do = f.tempFs.Features().Move
|
|
|
|
// we must also update the pending queue
|
|
err := f.cache.updatePendingUpload(srcObj.abs(), func(item *tempUploadInfo) error {
|
|
item.DestPath = path.Join(f.Root(), remote)
|
|
item.AddedOn = time.Now()
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
fs.Errorf(srcObj, "failed to rename queued file for upload: %v", err)
|
|
return nil, fs.ErrorCantMove
|
|
}
|
|
fs.Debugf(srcObj, "move: queued file moved to %v", remote)
|
|
}
|
|
|
|
obj, err := do(ctx, srcObj.Object, remote)
|
|
if err != nil {
|
|
fs.Errorf(srcObj, "error moving: %v", err)
|
|
return nil, err
|
|
}
|
|
fs.Debugf(obj, "move: file moved")
|
|
|
|
// remove old
|
|
err = f.cache.RemoveObject(srcObj.abs())
|
|
if err != nil {
|
|
fs.Errorf(srcObj, "move: remove error: %v", err)
|
|
} else {
|
|
fs.Debugf(srcObj, "move: removed from cache")
|
|
}
|
|
// expire old parent
|
|
parentCd := NewDirectory(f, cleanPath(path.Dir(srcObj.Remote())))
|
|
err = f.cache.ExpireDir(parentCd)
|
|
if err != nil {
|
|
fs.Errorf(parentCd, "move: parent cache expire error: %v", err)
|
|
} else {
|
|
fs.Infof(parentCd, "move: cache expired")
|
|
}
|
|
// advertise to ChangeNotify if wrapped doesn't do that
|
|
f.notifyChangeUpstreamIfNeeded(parentCd.Remote(), fs.EntryDirectory)
|
|
// persist new
|
|
cachedObj := ObjectFromOriginal(ctx, f, obj).persist()
|
|
fs.Debugf(cachedObj, "move: added to cache")
|
|
// expire new parent
|
|
parentCd = NewDirectory(f, cleanPath(path.Dir(cachedObj.Remote())))
|
|
err = f.cache.ExpireDir(parentCd)
|
|
if err != nil {
|
|
fs.Errorf(parentCd, "move: expire error: %v", err)
|
|
} else {
|
|
fs.Infof(parentCd, "move: cache expired")
|
|
}
|
|
// advertise to ChangeNotify if wrapped doesn't do that
|
|
f.notifyChangeUpstreamIfNeeded(parentCd.Remote(), fs.EntryDirectory)
|
|
|
|
return cachedObj, nil
|
|
}
|
|
|
|
// Hashes returns the supported hash sets.
|
|
func (f *Fs) Hashes() hash.Set {
|
|
return f.Fs.Hashes()
|
|
}
|
|
|
|
// Purge all files in the directory
|
|
func (f *Fs) Purge(ctx context.Context, dir string) error {
|
|
if dir == "" {
|
|
// FIXME this isn't quite right as it should purge the dir prefix
|
|
fs.Infof(f, "purging cache")
|
|
f.cache.Purge()
|
|
}
|
|
|
|
do := f.Fs.Features().Purge
|
|
if do == nil {
|
|
return fs.ErrorCantPurge
|
|
}
|
|
|
|
err := do(ctx, dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CleanUp the trash in the Fs
|
|
func (f *Fs) CleanUp(ctx context.Context) error {
|
|
f.CleanUpCache(false)
|
|
|
|
do := f.Fs.Features().CleanUp
|
|
if do == nil {
|
|
return nil
|
|
}
|
|
|
|
return do(ctx)
|
|
}
|
|
|
|
// About gets quota information from the Fs
|
|
func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
|
|
do := f.Fs.Features().About
|
|
if do == nil {
|
|
return nil, errors.New("not supported by underlying remote")
|
|
}
|
|
return do(ctx)
|
|
}
|
|
|
|
// Stats returns stats about the cache storage
|
|
func (f *Fs) Stats() (map[string]map[string]interface{}, error) {
|
|
return f.cache.Stats()
|
|
}
|
|
|
|
// openRateLimited will execute a closure under a rate limiter watch
|
|
func (f *Fs) openRateLimited(fn func() (io.ReadCloser, error)) (io.ReadCloser, error) {
|
|
var err error
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
|
defer cancel()
|
|
start := time.Now()
|
|
|
|
if err = f.rateLimiter.Wait(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
elapsed := time.Since(start)
|
|
if elapsed > time.Second*2 {
|
|
fs.Debugf(f, "rate limited: %s", elapsed)
|
|
}
|
|
return fn()
|
|
}
|
|
|
|
// CleanUpCache will cleanup only the cache data that is expired
|
|
func (f *Fs) CleanUpCache(ignoreLastTs bool) {
|
|
f.cleanupMu.Lock()
|
|
defer f.cleanupMu.Unlock()
|
|
|
|
if ignoreLastTs || time.Now().After(f.lastChunkCleanup.Add(time.Duration(f.opt.ChunkCleanInterval))) {
|
|
f.cache.CleanChunksBySize(int64(f.opt.ChunkTotalSize))
|
|
f.lastChunkCleanup = time.Now()
|
|
}
|
|
}
|
|
|
|
// StopBackgroundRunners will signal all the runners to stop their work
|
|
// can be triggered from a terminate signal or from testing between runs
|
|
func (f *Fs) StopBackgroundRunners() {
|
|
f.cleanupChan <- false
|
|
if f.opt.TempWritePath != "" && f.backgroundRunner != nil && f.backgroundRunner.isRunning() {
|
|
f.backgroundRunner.close()
|
|
}
|
|
f.cache.Close()
|
|
fs.Debugf(f, "Services stopped")
|
|
}
|
|
|
|
// UnWrap returns the Fs that this Fs is wrapping
|
|
func (f *Fs) UnWrap() fs.Fs {
|
|
return f.Fs
|
|
}
|
|
|
|
// WrapFs returns the Fs that is wrapping this Fs
|
|
func (f *Fs) WrapFs() fs.Fs {
|
|
return f.wrapper
|
|
}
|
|
|
|
// SetWrapper sets the Fs that is wrapping this Fs
|
|
func (f *Fs) SetWrapper(wrapper fs.Fs) {
|
|
f.wrapper = wrapper
|
|
}
|
|
|
|
// isWrappedByCrypt checks if this is wrapped by a crypt remote
|
|
func (f *Fs) isWrappedByCrypt() (*crypt.Fs, bool) {
|
|
if f.wrapper == nil {
|
|
return nil, false
|
|
}
|
|
c, ok := f.wrapper.(*crypt.Fs)
|
|
return c, ok
|
|
}
|
|
|
|
// cleanRootFromPath trims the root of the current fs from a path
|
|
func (f *Fs) cleanRootFromPath(p string) string {
|
|
if f.Root() != "" {
|
|
p = p[len(f.Root()):] // trim out root
|
|
if len(p) > 0 { // remove first separator
|
|
p = p[1:]
|
|
}
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
func (f *Fs) isRootInPath(p string) bool {
|
|
if f.Root() == "" {
|
|
return true
|
|
}
|
|
return strings.HasPrefix(p, f.Root()+"/")
|
|
}
|
|
|
|
// MergeDirs merges the contents of all the directories passed
|
|
// in into the first one and rmdirs the other directories.
|
|
func (f *Fs) MergeDirs(ctx context.Context, dirs []fs.Directory) error {
|
|
do := f.Fs.Features().MergeDirs
|
|
if do == nil {
|
|
return errors.New("MergeDirs not supported")
|
|
}
|
|
for _, dir := range dirs {
|
|
_ = f.cache.RemoveDir(dir.Remote())
|
|
}
|
|
return do(ctx, dirs)
|
|
}
|
|
|
|
// DirCacheFlush flushes the dir cache
|
|
func (f *Fs) DirCacheFlush() {
|
|
_ = f.cache.RemoveDir("")
|
|
}
|
|
|
|
// GetBackgroundUploadChannel returns a channel that can be listened to for remote activities that happen
|
|
// in the background
|
|
func (f *Fs) GetBackgroundUploadChannel() chan BackgroundUploadState {
|
|
if f.opt.TempWritePath != "" {
|
|
return f.backgroundRunner.notifyCh
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *Fs) isNotifiedRemote(remote string) bool {
|
|
f.notifiedMu.Lock()
|
|
defer f.notifiedMu.Unlock()
|
|
|
|
n, ok := f.notifiedRemotes[remote]
|
|
if !ok || !n {
|
|
return false
|
|
}
|
|
|
|
delete(f.notifiedRemotes, remote)
|
|
return n
|
|
}
|
|
|
|
func cleanPath(p string) string {
|
|
p = path.Clean(p)
|
|
if p == "." || p == "/" {
|
|
p = ""
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
// UserInfo returns info about the connected user
|
|
func (f *Fs) UserInfo(ctx context.Context) (map[string]string, error) {
|
|
do := f.Fs.Features().UserInfo
|
|
if do == nil {
|
|
return nil, fs.ErrorNotImplemented
|
|
}
|
|
return do(ctx)
|
|
}
|
|
|
|
// Disconnect the current user
|
|
func (f *Fs) Disconnect(ctx context.Context) error {
|
|
do := f.Fs.Features().Disconnect
|
|
if do == nil {
|
|
return fs.ErrorNotImplemented
|
|
}
|
|
return do(ctx)
|
|
}
|
|
|
|
// Shutdown the backend, closing any background tasks and any
|
|
// cached connections.
|
|
func (f *Fs) Shutdown(ctx context.Context) error {
|
|
do := f.Fs.Features().Shutdown
|
|
if do == nil {
|
|
return nil
|
|
}
|
|
return do(ctx)
|
|
}
|
|
|
|
var commandHelp = []fs.CommandHelp{
|
|
{
|
|
Name: "stats",
|
|
Short: "Print stats on the cache backend in JSON format.",
|
|
},
|
|
}
|
|
|
|
// Command the backend to run a named command
|
|
//
|
|
// The command run is name
|
|
// args may be used to read arguments from
|
|
// opts may be used to read optional arguments from
|
|
//
|
|
// The result should be capable of being JSON encoded
|
|
// If it is a string or a []string it will be shown to the user
|
|
// otherwise it will be JSON encoded and shown to the user like that
|
|
func (f *Fs) Command(ctx context.Context, name string, arg []string, opt map[string]string) (interface{}, error) {
|
|
switch name {
|
|
case "stats":
|
|
return f.Stats()
|
|
default:
|
|
return nil, fs.ErrorCommandNotFound
|
|
}
|
|
}
|
|
|
|
// Check the interfaces are satisfied
|
|
var (
|
|
_ fs.Fs = (*Fs)(nil)
|
|
_ fs.Purger = (*Fs)(nil)
|
|
_ fs.Copier = (*Fs)(nil)
|
|
_ fs.Mover = (*Fs)(nil)
|
|
_ fs.DirMover = (*Fs)(nil)
|
|
_ fs.PutUncheckeder = (*Fs)(nil)
|
|
_ fs.PutStreamer = (*Fs)(nil)
|
|
_ fs.CleanUpper = (*Fs)(nil)
|
|
_ fs.UnWrapper = (*Fs)(nil)
|
|
_ fs.Wrapper = (*Fs)(nil)
|
|
_ fs.ListRer = (*Fs)(nil)
|
|
_ fs.ChangeNotifier = (*Fs)(nil)
|
|
_ fs.Abouter = (*Fs)(nil)
|
|
_ fs.UserInfoer = (*Fs)(nil)
|
|
_ fs.Disconnecter = (*Fs)(nil)
|
|
_ fs.Commander = (*Fs)(nil)
|
|
_ fs.MergeDirser = (*Fs)(nil)
|
|
_ fs.Shutdowner = (*Fs)(nil)
|
|
)
|