2019-06-26 18:39:01 +00:00
|
|
|
package fichier
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
2019-07-28 17:47:38 +00:00
|
|
|
"github.com/rclone/rclone/fs"
|
2020-01-14 17:33:35 +00:00
|
|
|
"github.com/rclone/rclone/fs/config"
|
2019-07-28 17:47:38 +00:00
|
|
|
"github.com/rclone/rclone/fs/config/configmap"
|
|
|
|
"github.com/rclone/rclone/fs/config/configstruct"
|
|
|
|
"github.com/rclone/rclone/fs/fshttp"
|
|
|
|
"github.com/rclone/rclone/fs/hash"
|
|
|
|
"github.com/rclone/rclone/lib/dircache"
|
2020-01-14 17:33:35 +00:00
|
|
|
"github.com/rclone/rclone/lib/encoder"
|
2019-07-28 17:47:38 +00:00
|
|
|
"github.com/rclone/rclone/lib/pacer"
|
|
|
|
"github.com/rclone/rclone/lib/rest"
|
2019-06-26 18:39:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2020-04-24 01:15:52 +00:00
|
|
|
rootID = "0"
|
|
|
|
apiBaseURL = "https://api.1fichier.com/v1"
|
|
|
|
minSleep = 400 * time.Millisecond // api is extremely rate limited now
|
|
|
|
maxSleep = 5 * time.Second
|
|
|
|
decayConstant = 2 // bigger for slower decay, exponential
|
|
|
|
attackConstant = 0 // start with max sleep
|
2019-06-26 18:39:01 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
fs.Register(&fs.RegInfo{
|
|
|
|
Name: "fichier",
|
|
|
|
Description: "1Fichier",
|
2020-11-05 18:02:26 +00:00
|
|
|
Config: func(ctx context.Context, name string, config configmap.Mapper) {
|
2019-06-26 18:39:01 +00:00
|
|
|
},
|
|
|
|
NewFs: NewFs,
|
2020-01-14 17:33:35 +00:00
|
|
|
Options: []fs.Option{{
|
|
|
|
Help: "Your API Key, get it from https://1fichier.com/console/params.pl",
|
|
|
|
Name: "api_key",
|
|
|
|
}, {
|
|
|
|
Help: "If you want to download a shared folder, add this parameter",
|
|
|
|
Name: "shared_folder",
|
|
|
|
Required: false,
|
|
|
|
Advanced: true,
|
|
|
|
}, {
|
|
|
|
Name: config.ConfigEncoding,
|
|
|
|
Help: config.ConfigEncodingHelp,
|
|
|
|
Advanced: true,
|
2020-01-14 21:51:49 +00:00
|
|
|
// Characters that need escaping
|
|
|
|
//
|
|
|
|
// '\\': '\', // FULLWIDTH REVERSE SOLIDUS
|
|
|
|
// '<': '<', // FULLWIDTH LESS-THAN SIGN
|
|
|
|
// '>': '>', // FULLWIDTH GREATER-THAN SIGN
|
|
|
|
// '"': '"', // FULLWIDTH QUOTATION MARK - not on the list but seems to be reserved
|
|
|
|
// '\'': ''', // FULLWIDTH APOSTROPHE
|
|
|
|
// '$': '$', // FULLWIDTH DOLLAR SIGN
|
|
|
|
// '`': '`', // FULLWIDTH GRAVE ACCENT
|
|
|
|
//
|
|
|
|
// Leading space and trailing space
|
|
|
|
Default: (encoder.Display |
|
|
|
|
encoder.EncodeBackSlash |
|
|
|
|
encoder.EncodeSingleQuote |
|
|
|
|
encoder.EncodeBackQuote |
|
|
|
|
encoder.EncodeDoubleQuote |
|
|
|
|
encoder.EncodeLtGt |
|
|
|
|
encoder.EncodeDollar |
|
|
|
|
encoder.EncodeLeftSpace |
|
|
|
|
encoder.EncodeRightSpace |
|
|
|
|
encoder.EncodeInvalidUtf8),
|
2020-01-14 17:33:35 +00:00
|
|
|
}},
|
2019-06-26 18:39:01 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Options defines the configuration for this backend
|
|
|
|
type Options struct {
|
2020-01-14 17:33:35 +00:00
|
|
|
APIKey string `config:"api_key"`
|
|
|
|
SharedFolder string `config:"shared_folder"`
|
|
|
|
Enc encoder.MultiEncoder `config:"encoding"`
|
2019-06-26 18:39:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Fs is the interface a cloud storage system must provide
|
|
|
|
type Fs struct {
|
|
|
|
root string
|
|
|
|
name string
|
|
|
|
features *fs.Features
|
2020-01-14 17:33:35 +00:00
|
|
|
opt Options
|
2019-06-26 18:39:01 +00:00
|
|
|
dirCache *dircache.DirCache
|
|
|
|
baseClient *http.Client
|
|
|
|
pacer *fs.Pacer
|
|
|
|
rest *rest.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
// FindLeaf finds a directory of name leaf in the folder with ID pathID
|
|
|
|
func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
|
2019-07-19 23:50:57 +00:00
|
|
|
folderID, err := strconv.Atoi(pathID)
|
|
|
|
if err != nil {
|
|
|
|
return "", false, err
|
|
|
|
}
|
2019-09-04 19:00:37 +00:00
|
|
|
folders, err := f.listFolders(ctx, folderID)
|
2019-06-26 18:39:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, folder := range folders.SubFolders {
|
|
|
|
if folder.Name == leaf {
|
|
|
|
pathIDOut := strconv.Itoa(folder.ID)
|
|
|
|
return pathIDOut, true, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateDir makes a directory with pathID as parent and name leaf
|
|
|
|
func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) {
|
2019-07-19 23:50:57 +00:00
|
|
|
folderID, err := strconv.Atoi(pathID)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2019-09-04 19:00:37 +00:00
|
|
|
resp, err := f.makeFolder(ctx, leaf, folderID)
|
2019-06-26 18:39:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return strconv.Itoa(resp.FolderID), err
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
// String returns a description of the FS
|
|
|
|
func (f *Fs) String() string {
|
|
|
|
return fmt.Sprintf("1Fichier root '%s'", f.root)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Precision of the ModTimes in this Fs
|
|
|
|
func (f *Fs) Precision() time.Duration {
|
|
|
|
return fs.ModTimeNotSupported
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hashes returns the supported hash types of the filesystem
|
|
|
|
func (f *Fs) Hashes() hash.Set {
|
|
|
|
return hash.Set(hash.Whirlpool)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Features returns the optional features of this Fs
|
|
|
|
func (f *Fs) Features() *fs.Features {
|
|
|
|
return f.features
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewFs makes a new Fs object from the path
|
|
|
|
//
|
|
|
|
// The path is of the form remote:path
|
|
|
|
//
|
|
|
|
// Remotes are looked up in the config file. If the remote isn't
|
|
|
|
// found then NotFoundInConfigFile will be returned.
|
|
|
|
//
|
|
|
|
// On Windows avoid single character remote names as they can be mixed
|
|
|
|
// up with drive letters.
|
2020-11-05 15:18:51 +00:00
|
|
|
func NewFs(ctx context.Context, name string, root string, config configmap.Mapper) (fs.Fs, error) {
|
2019-06-26 18:39:01 +00:00
|
|
|
opt := new(Options)
|
|
|
|
err := configstruct.Set(config, opt)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If using a Shared Folder override root
|
|
|
|
if opt.SharedFolder != "" {
|
|
|
|
root = ""
|
|
|
|
}
|
|
|
|
|
|
|
|
//workaround for wonky parser
|
|
|
|
root = strings.Trim(root, "/")
|
|
|
|
|
|
|
|
f := &Fs{
|
|
|
|
name: name,
|
|
|
|
root: root,
|
2020-01-14 17:33:35 +00:00
|
|
|
opt: *opt,
|
2020-11-05 11:33:32 +00:00
|
|
|
pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant), pacer.AttackConstant(attackConstant))),
|
2019-06-26 18:39:01 +00:00
|
|
|
baseClient: &http.Client{},
|
|
|
|
}
|
|
|
|
|
|
|
|
f.features = (&fs.Features{
|
|
|
|
DuplicateFiles: true,
|
|
|
|
CanHaveEmptyDirectories: true,
|
2020-11-29 15:13:51 +00:00
|
|
|
ReadMimeType: true,
|
2020-11-05 16:00:40 +00:00
|
|
|
}).Fill(ctx, f)
|
2019-06-26 18:39:01 +00:00
|
|
|
|
2020-11-13 15:24:43 +00:00
|
|
|
client := fshttp.NewClient(ctx)
|
2019-06-26 18:39:01 +00:00
|
|
|
|
|
|
|
f.rest = rest.NewClient(client).SetRoot(apiBaseURL)
|
|
|
|
|
2020-01-14 17:33:35 +00:00
|
|
|
f.rest.SetHeader("Authorization", "Bearer "+f.opt.APIKey)
|
2019-06-26 18:39:01 +00:00
|
|
|
|
|
|
|
f.dirCache = dircache.New(root, rootID, f)
|
|
|
|
|
|
|
|
// Find the current root
|
|
|
|
err = f.dirCache.FindRoot(ctx, false)
|
|
|
|
if err != nil {
|
|
|
|
// Assume it is a file
|
|
|
|
newRoot, remote := dircache.SplitPath(root)
|
|
|
|
tempF := *f
|
|
|
|
tempF.dirCache = dircache.New(newRoot, rootID, &tempF)
|
|
|
|
tempF.root = newRoot
|
|
|
|
// Make new Fs which is the parent
|
|
|
|
err = tempF.dirCache.FindRoot(ctx, false)
|
|
|
|
if err != nil {
|
|
|
|
// No root so return old f
|
|
|
|
return f, nil
|
|
|
|
}
|
|
|
|
_, err := tempF.NewObject(ctx, remote)
|
|
|
|
if err != nil {
|
|
|
|
if err == fs.ErrorObjectNotFound {
|
|
|
|
// File doesn't exist so return old f
|
|
|
|
return f, nil
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-11-05 16:00:40 +00:00
|
|
|
f.features.Fill(ctx, &tempF)
|
2019-06-26 18:39:01 +00:00
|
|
|
// XXX: update the old f here instead of returning tempF, since
|
|
|
|
// `features` were already filled with functions having *f as a receiver.
|
2019-07-28 17:47:38 +00:00
|
|
|
// See https://github.com/rclone/rclone/issues/2182
|
2019-06-26 18:39:01 +00:00
|
|
|
f.dirCache = tempF.dirCache
|
|
|
|
f.root = tempF.root
|
|
|
|
// return an error with an fs which points to the parent
|
|
|
|
return f, fs.ErrorIsFile
|
|
|
|
}
|
|
|
|
return f, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// List the objects and directories in dir into entries. The
|
|
|
|
// entries can be returned in any order but should be for a
|
|
|
|
// complete directory.
|
|
|
|
//
|
|
|
|
// dir should be "" to list the root, and should not have
|
|
|
|
// trailing slashes.
|
|
|
|
//
|
|
|
|
// This should return ErrDirNotFound if the directory isn't
|
|
|
|
// found.
|
|
|
|
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
|
2020-01-14 17:33:35 +00:00
|
|
|
if f.opt.SharedFolder != "" {
|
|
|
|
return f.listSharedFiles(ctx, f.opt.SharedFolder)
|
2019-06-26 18:39:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
dirContent, err := f.listDir(ctx, dir)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return dirContent, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewObject finds the Object at remote. If it can't be found
|
|
|
|
// it returns the error ErrorObjectNotFound.
|
|
|
|
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
|
2020-05-11 16:24:37 +00:00
|
|
|
leaf, directoryID, err := f.dirCache.FindPath(ctx, remote, false)
|
2019-06-26 18:39:01 +00:00
|
|
|
if err != nil {
|
|
|
|
if err == fs.ErrorDirNotFound {
|
|
|
|
return nil, fs.ErrorObjectNotFound
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-07-19 23:50:57 +00:00
|
|
|
folderID, err := strconv.Atoi(directoryID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-09-04 19:00:37 +00:00
|
|
|
files, err := f.listFiles(ctx, folderID)
|
2019-06-26 18:39:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, file := range files.Items {
|
|
|
|
if file.Filename == leaf {
|
|
|
|
path, ok := f.dirCache.GetInv(directoryID)
|
|
|
|
|
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("Cannot find dir in dircache")
|
|
|
|
}
|
|
|
|
|
|
|
|
return f.newObjectFromFile(ctx, path, file), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, fs.ErrorObjectNotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
// Put in to the remote path with the modTime given of the given size
|
|
|
|
//
|
2020-05-25 06:05:53 +00:00
|
|
|
// When called from outside an Fs by rclone, src.Size() will always be >= 0.
|
2019-06-26 18:39:01 +00:00
|
|
|
// But for unknown-sized objects (indicated by src.Size() == -1), Put should either
|
|
|
|
// return an error or upload it properly (rather than e.g. calling panic).
|
|
|
|
//
|
|
|
|
// May create the object even if it returns an error - if so
|
|
|
|
// will return the object and the error, otherwise will return
|
|
|
|
// nil and the error
|
|
|
|
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
Spelling fixes
Fix spelling of: above, already, anonymous, associated,
authentication, bandwidth, because, between, blocks, calculate,
candidates, cautious, changelog, cleaner, clipboard, command,
completely, concurrently, considered, constructs, corrupt, current,
daemon, dependencies, deprecated, directory, dispatcher, download,
eligible, ellipsis, encrypter, endpoint, entrieslist, essentially,
existing writers, existing, expires, filesystem, flushing, frequently,
hierarchy, however, implementation, implements, inaccurate,
individually, insensitive, longer, maximum, metadata, modified,
multipart, namedirfirst, nextcloud, obscured, opened, optional,
owncloud, pacific, passphrase, password, permanently, persimmon,
positive, potato, protocol, quota, receiving, recommends, referring,
requires, revisited, satisfied, satisfies, satisfy, semver,
serialized, session, storage, strategies, stringlist, successful,
supported, surprise, temporarily, temporary, transactions, unneeded,
update, uploads, wrapped
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2020-10-09 00:17:24 +00:00
|
|
|
existingObj, err := f.NewObject(ctx, src.Remote())
|
2019-06-26 18:39:01 +00:00
|
|
|
switch err {
|
|
|
|
case nil:
|
Spelling fixes
Fix spelling of: above, already, anonymous, associated,
authentication, bandwidth, because, between, blocks, calculate,
candidates, cautious, changelog, cleaner, clipboard, command,
completely, concurrently, considered, constructs, corrupt, current,
daemon, dependencies, deprecated, directory, dispatcher, download,
eligible, ellipsis, encrypter, endpoint, entrieslist, essentially,
existing writers, existing, expires, filesystem, flushing, frequently,
hierarchy, however, implementation, implements, inaccurate,
individually, insensitive, longer, maximum, metadata, modified,
multipart, namedirfirst, nextcloud, obscured, opened, optional,
owncloud, pacific, passphrase, password, permanently, persimmon,
positive, potato, protocol, quota, receiving, recommends, referring,
requires, revisited, satisfied, satisfies, satisfy, semver,
serialized, session, storage, strategies, stringlist, successful,
supported, surprise, temporarily, temporary, transactions, unneeded,
update, uploads, wrapped
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2020-10-09 00:17:24 +00:00
|
|
|
return existingObj, existingObj.Update(ctx, in, src, options...)
|
2019-06-26 18:39:01 +00:00
|
|
|
case fs.ErrorObjectNotFound:
|
|
|
|
// Not found so create it
|
|
|
|
return f.PutUnchecked(ctx, in, src, options...)
|
|
|
|
default:
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// putUnchecked uploads the object with the given name and size
|
|
|
|
//
|
|
|
|
// This will create a duplicate if we upload a new file without
|
|
|
|
// checking to see if there is one already - use Put() for that.
|
|
|
|
func (f *Fs) putUnchecked(ctx context.Context, in io.Reader, remote string, size int64, options ...fs.OpenOption) (fs.Object, error) {
|
2020-09-28 08:08:52 +00:00
|
|
|
if size > int64(300e9) {
|
2019-06-26 18:39:01 +00:00
|
|
|
return nil, errors.New("File too big, cant upload")
|
|
|
|
} else if size == 0 {
|
|
|
|
return nil, fs.ErrorCantUploadEmptyFiles
|
|
|
|
}
|
|
|
|
|
2019-09-04 19:00:37 +00:00
|
|
|
nodeResponse, err := f.getUploadNode(ctx)
|
2019-06-26 18:39:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-05-11 16:24:37 +00:00
|
|
|
leaf, directoryID, err := f.dirCache.FindPath(ctx, remote, true)
|
2019-06-26 18:39:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-03-21 22:11:02 +00:00
|
|
|
_, err = f.uploadFile(ctx, in, size, leaf, directoryID, nodeResponse.ID, nodeResponse.URL, options...)
|
2019-06-26 18:39:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-09-04 19:00:37 +00:00
|
|
|
fileUploadResponse, err := f.endUpload(ctx, nodeResponse.ID, nodeResponse.URL)
|
2019-06-26 18:39:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(fileUploadResponse.Links) != 1 {
|
|
|
|
return nil, errors.New("unexpected amount of files")
|
|
|
|
}
|
|
|
|
|
|
|
|
link := fileUploadResponse.Links[0]
|
|
|
|
fileSize, err := strconv.ParseInt(link.Size, 10, 64)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Object{
|
|
|
|
fs: f,
|
|
|
|
remote: remote,
|
|
|
|
file: File{
|
|
|
|
ACL: 0,
|
|
|
|
CDN: 0,
|
|
|
|
Checksum: link.Whirlpool,
|
|
|
|
ContentType: "",
|
|
|
|
Date: time.Now().Format("2006-01-02 15:04:05"),
|
|
|
|
Filename: link.Filename,
|
|
|
|
Pass: 0,
|
2019-10-01 11:34:38 +00:00
|
|
|
Size: fileSize,
|
2019-06-26 18:39:01 +00:00
|
|
|
URL: link.Download,
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// PutUnchecked uploads the object
|
|
|
|
//
|
|
|
|
// This will create a duplicate if we upload a new file without
|
|
|
|
// checking to see if there is one already - use Put() for that.
|
|
|
|
func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
|
|
|
|
return f.putUnchecked(ctx, in, src.Remote(), src.Size(), options...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mkdir makes the directory (container, bucket)
|
|
|
|
//
|
|
|
|
// Shouldn't return an error if it already exists
|
|
|
|
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
|
2020-05-11 16:24:37 +00:00
|
|
|
_, err := f.dirCache.FindDir(ctx, dir, true)
|
2019-06-26 18:39:01 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rmdir removes the directory (container, bucket) if empty
|
|
|
|
//
|
|
|
|
// Return an error if it doesn't exist or isn't empty
|
|
|
|
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
|
2019-07-19 23:50:57 +00:00
|
|
|
directoryID, err := f.dirCache.FindDir(ctx, dir, false)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
folderID, err := strconv.Atoi(directoryID)
|
2019-06-26 18:39:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-09-04 19:00:37 +00:00
|
|
|
_, err = f.removeFolder(ctx, dir, folderID)
|
2019-06-26 18:39:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
f.dirCache.FlushDir(dir)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check the interfaces are satisfied
|
|
|
|
var (
|
|
|
|
_ fs.Fs = (*Fs)(nil)
|
|
|
|
_ fs.PutUncheckeder = (*Fs)(nil)
|
|
|
|
_ dircache.DirCacher = (*Fs)(nil)
|
|
|
|
)
|