2015-12-07 05:01:03 +01:00
package yandex
import (
2019-06-17 10:34:30 +02:00
"context"
2015-12-07 05:01:03 +01:00
"encoding/json"
"fmt"
"io"
"log"
2018-11-19 18:27:00 +01:00
"net/http"
"net/url"
2015-12-07 05:01:03 +01:00
"path"
2018-11-19 18:27:00 +01:00
"strconv"
2015-12-07 05:01:03 +01:00
"strings"
"time"
2016-06-06 22:23:54 +02:00
"github.com/pkg/errors"
2019-07-28 19:47:38 +02:00
"github.com/rclone/rclone/backend/yandex/api"
"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/config/obscure"
2019-10-05 11:22:43 +02:00
"github.com/rclone/rclone/fs/encodings"
2019-07-28 19:47:38 +02:00
"github.com/rclone/rclone/fs/fserrors"
"github.com/rclone/rclone/fs/hash"
"github.com/rclone/rclone/lib/oauthutil"
"github.com/rclone/rclone/lib/pacer"
"github.com/rclone/rclone/lib/readers"
"github.com/rclone/rclone/lib/rest"
2015-12-07 05:01:03 +01:00
"golang.org/x/oauth2"
)
2019-10-05 11:22:43 +02:00
const enc = encodings . Yandex
2015-12-07 05:01:03 +01:00
//oAuth
const (
2016-02-28 20:57:19 +01:00
rcloneClientID = "ac39b43b9eba4cae8ffb788c06d816a8"
2016-08-14 13:04:43 +02:00
rcloneEncryptedClientSecret = "EfyyNZ3YUEwXM5yAhi72G9YwKn2mkFrYwJNS7cY0TJAhFlX9K-uJFbGlpO-RYjrJ"
2018-11-19 18:27:00 +01:00
rootURL = "https://cloud-api.yandex.com/v1/disk"
minSleep = 10 * time . Millisecond
maxSleep = 2 * time . Second // may needs to be increased, testing needed
decayConstant = 2 // bigger for slower decay, exponential
2015-12-07 05:01:03 +01:00
)
// Globals
var (
// Description of how to auth for this app
oauthConfig = & oauth2 . Config {
Endpoint : oauth2 . Endpoint {
AuthURL : "https://oauth.yandex.com/authorize" , //same as https://oauth.yandex.ru/authorize
TokenURL : "https://oauth.yandex.com/token" , //same as https://oauth.yandex.ru/token
} ,
ClientID : rcloneClientID ,
2018-01-18 21:19:55 +01:00
ClientSecret : obscure . MustReveal ( rcloneEncryptedClientSecret ) ,
2015-12-07 05:01:03 +01:00
RedirectURL : oauthutil . RedirectURL ,
}
)
// Register with Fs
func init ( ) {
2016-02-18 12:35:25 +01:00
fs . Register ( & fs . RegInfo {
2016-02-15 19:11:53 +01:00
Name : "yandex" ,
Description : "Yandex Disk" ,
NewFs : NewFs ,
2018-05-14 19:06:57 +02:00
Config : func ( name string , m configmap . Mapper ) {
err := oauthutil . Config ( "yandex" , name , m , oauthConfig )
2015-12-07 05:01:03 +01:00
if err != nil {
log . Fatalf ( "Failed to configure token: %v" , err )
2018-11-19 18:27:00 +01:00
return
2015-12-07 05:01:03 +01:00
}
} ,
Options : [ ] fs . Option { {
2018-01-12 17:30:54 +01:00
Name : config . ConfigClientID ,
2018-05-14 19:06:57 +02:00
Help : "Yandex Client Id\nLeave blank normally." ,
2015-12-07 05:01:03 +01:00
} , {
2018-01-12 17:30:54 +01:00
Name : config . ConfigClientSecret ,
2018-05-14 19:06:57 +02:00
Help : "Yandex Client Secret\nLeave blank normally." ,
2018-11-19 18:27:00 +01:00
} , {
Name : "unlink" ,
Help : "Remove existing public link to file/folder with link command rather than creating.\nDefault is false, meaning link command will create or retrieve public link." ,
Default : false ,
Advanced : true ,
2015-12-07 05:01:03 +01:00
} } ,
} )
}
2018-05-14 19:06:57 +02:00
// Options defines the configuration for this backend
type Options struct {
2018-11-19 18:27:00 +01:00
Token string ` config:"token" `
Unlink bool ` config:"unlink" `
2018-05-14 19:06:57 +02:00
}
2015-12-07 05:01:03 +01:00
// Fs represents a remote yandex
type Fs struct {
2018-05-05 12:33:01 +02:00
name string
2018-11-19 18:27:00 +01:00
root string // root path
opt Options // parsed options
features * fs . Features // optional features
srv * rest . Client // the connection to the yandex server
2019-02-09 21:52:15 +01:00
pacer * fs . Pacer // pacer for API calls
2018-11-19 18:27:00 +01:00
diskRoot string // root path with "disk:/" container name
2015-12-07 05:01:03 +01:00
}
// Object describes a swift object
type Object struct {
2018-11-19 18:27:00 +01:00
fs * Fs // what this object is part of
remote string // The remote path
hasMetaData bool // whether info below has been set
md5sum string // The MD5Sum of the object
size int64 // Bytes in the object
modTime time . Time // Modified time of the object
mimeType string // Content type according to the server
2015-12-07 05:01:03 +01:00
}
// ------------------------------------------------------------
// 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 converts this Fs to a string
func ( f * Fs ) String ( ) string {
return fmt . Sprintf ( "Yandex %s" , f . root )
}
2018-11-19 18:27:00 +01:00
// Precision return the precision of this Fs
func ( f * Fs ) Precision ( ) time . Duration {
return time . Nanosecond
}
// Hashes returns the supported hash sets.
func ( f * Fs ) Hashes ( ) hash . Set {
return hash . Set ( hash . MD5 )
}
2017-01-13 18:21:47 +01:00
// Features returns the optional features of this Fs
func ( f * Fs ) Features ( ) * fs . Features {
return f . features
}
2018-11-19 18:27:00 +01:00
// retryErrorCodes is a slice of error codes that we will retry
var retryErrorCodes = [ ] int {
429 , // Too Many Requests.
500 , // Internal Server Error
502 , // Bad Gateway
503 , // Service Unavailable
504 , // Gateway Timeout
509 , // Bandwidth Limit Exceeded
}
// shouldRetry returns a boolean as to whether this resp and err
// deserve to be retried. It returns the err as a convenience
func shouldRetry ( resp * http . Response , err error ) ( bool , error ) {
return fserrors . ShouldRetry ( err ) || fserrors . ShouldRetryHTTP ( resp , retryErrorCodes ) , err
}
// errorHandler parses a non 2xx error response into an error
func errorHandler ( resp * http . Response ) error {
// Decode error response
errResponse := new ( api . ErrorResponse )
err := rest . DecodeJSON ( resp , & errResponse )
if err != nil {
fs . Debugf ( nil , "Couldn't decode error response: %v" , err )
}
if errResponse . Message == "" {
errResponse . Message = resp . Status
}
if errResponse . StatusCode == 0 {
errResponse . StatusCode = resp . StatusCode
}
return errResponse
}
// Sets root in f
func ( f * Fs ) setRoot ( root string ) {
//Set root path
f . root = strings . Trim ( root , "/" )
//Set disk root path.
//Adding "disk:" to root path as all paths on disk start with it
var diskRoot string
if f . root == "" {
diskRoot = "disk:/"
} else {
diskRoot = "disk:/" + f . root + "/"
}
f . diskRoot = diskRoot
}
// filePath returns a escaped file path (f.root, file)
func ( f * Fs ) filePath ( file string ) string {
return path . Join ( f . diskRoot , file )
}
// dirPath returns a escaped file path (f.root, file) ending with '/'
func ( f * Fs ) dirPath ( file string ) string {
return path . Join ( f . diskRoot , file ) + "/"
}
2019-09-04 21:00:37 +02:00
func ( f * Fs ) readMetaDataForPath ( ctx context . Context , path string , options * api . ResourceInfoRequestOptions ) ( * api . ResourceInfoResponse , error ) {
2018-11-19 18:27:00 +01:00
opts := rest . Opts {
Method : "GET" ,
Path : "/resources" ,
Parameters : url . Values { } ,
}
2019-10-05 11:22:43 +02:00
opts . Parameters . Set ( "path" , enc . FromStandardPath ( path ) )
2018-11-19 18:27:00 +01:00
if options . SortMode != nil {
opts . Parameters . Set ( "sort" , options . SortMode . String ( ) )
}
if options . Limit != 0 {
opts . Parameters . Set ( "limit" , strconv . FormatUint ( options . Limit , 10 ) )
}
if options . Offset != 0 {
opts . Parameters . Set ( "offset" , strconv . FormatUint ( options . Offset , 10 ) )
}
if options . Fields != nil {
opts . Parameters . Set ( "fields" , strings . Join ( options . Fields , "," ) )
}
var err error
var info api . ResourceInfoResponse
var resp * http . Response
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = f . srv . CallJSON ( ctx , & opts , nil , & info )
2018-11-19 18:27:00 +01:00
return shouldRetry ( resp , err )
} )
2015-12-07 05:01:03 +01:00
if err != nil {
return nil , err
}
2018-11-19 18:27:00 +01:00
2019-10-05 11:22:43 +02:00
info . Name = enc . ToStandardName ( info . Name )
2018-11-19 18:27:00 +01:00
return & info , nil
2015-12-07 05:01:03 +01:00
}
// NewFs constructs an Fs from the path, container:path
2018-05-14 19:06:57 +02:00
func NewFs ( name , root string , m configmap . Mapper ) ( fs . Fs , error ) {
2019-09-04 21:00:37 +02:00
ctx := context . TODO ( )
2018-05-14 19:06:57 +02:00
// Parse config into Options struct
opt := new ( Options )
err := configstruct . Set ( m , opt )
if err != nil {
return nil , err
}
2018-11-19 18:27:00 +01:00
token , err := oauthutil . GetToken ( name , m )
2015-12-07 05:01:03 +01:00
if err != nil {
2018-11-19 18:27:00 +01:00
log . Fatalf ( "Couldn't read OAuth token (this should never happen)." )
}
if token . RefreshToken == "" {
log . Fatalf ( "Unable to get RefreshToken. If you are upgrading from older versions of rclone, please run `rclone config` and re-configure this backend." )
}
if token . TokenType != "OAuth" {
token . TokenType = "OAuth"
err = oauthutil . PutToken ( name , m , token , false )
if err != nil {
log . Fatalf ( "Couldn't save OAuth token (this should never happen)." )
}
log . Printf ( "Automatically upgraded OAuth config." )
}
oAuthClient , _ , err := oauthutil . NewClient ( name , m , oauthConfig )
if err != nil {
log . Fatalf ( "Failed to configure Yandex: %v" , err )
2015-12-07 05:01:03 +01:00
}
f := & Fs {
2018-11-19 18:27:00 +01:00
name : name ,
opt : * opt ,
srv : rest . NewClient ( oAuthClient ) . SetRoot ( rootURL ) ,
2019-02-09 21:52:15 +01:00
pacer : fs . NewPacer ( pacer . NewDefault ( pacer . MinSleep ( minSleep ) , pacer . MaxSleep ( maxSleep ) , pacer . DecayConstant ( decayConstant ) ) ) ,
2015-12-07 05:01:03 +01:00
}
2018-11-19 18:27:00 +01:00
f . setRoot ( root )
2017-08-09 16:27:43 +02:00
f . features = ( & fs . Features {
ReadMimeType : true ,
WriteMimeType : true ,
CanHaveEmptyDirectories : true ,
} ) . Fill ( f )
2018-11-19 18:27:00 +01:00
f . srv . SetErrorHandler ( errorHandler )
2015-12-07 05:01:03 +01:00
// Check to see if the object exists and is a file
//request object meta info
2018-11-19 18:27:00 +01:00
// Check to see if the object exists and is a file
//request object meta info
2019-09-04 21:00:37 +02:00
if info , err := f . readMetaDataForPath ( ctx , f . diskRoot , & api . ResourceInfoRequestOptions { } ) ; err != nil {
2018-11-19 18:27:00 +01:00
2015-12-07 05:01:03 +01:00
} else {
2018-11-19 18:27:00 +01:00
if info . ResourceType == "file" {
2018-08-18 13:12:19 +02:00
rootDir := path . Dir ( root )
if rootDir == "." {
rootDir = ""
}
f . setRoot ( rootDir )
2016-06-21 19:01:53 +02:00
// return an error with an fs which points to the parent
return f , fs . ErrorIsFile
2015-12-07 05:01:03 +01:00
}
}
return f , nil
}
2017-06-30 11:54:14 +02:00
// Convert a list item into a DirEntry
2019-09-04 21:00:37 +02:00
func ( f * Fs ) itemToDirEntry ( ctx context . Context , remote string , object * api . ResourceInfoResponse ) ( fs . DirEntry , error ) {
2017-06-11 23:43:31 +02:00
switch object . ResourceType {
case "dir" :
t , err := time . Parse ( time . RFC3339Nano , object . Modified )
if err != nil {
return nil , errors . Wrap ( err , "error parsing time in directory item" )
}
2019-01-11 18:17:46 +01:00
d := fs . NewDir ( remote , t ) . SetSize ( object . Size )
2017-06-11 23:43:31 +02:00
return d , nil
case "file" :
2019-09-04 21:00:37 +02:00
o , err := f . newObjectWithInfo ( ctx , remote , object )
2017-06-11 23:43:31 +02:00
if err != nil {
return nil , err
}
return o , nil
default :
fs . Debugf ( f , "Unknown resource type %q" , object . ResourceType )
}
return nil , nil
}
2016-04-21 21:06:21 +02:00
2017-06-11 23:43:31 +02:00
// 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.
2019-06-17 10:34:30 +02:00
func ( f * Fs ) List ( ctx context . Context , dir string ) ( entries fs . DirEntries , err error ) {
2018-11-19 18:27:00 +01:00
root := f . dirPath ( dir )
var limit uint64 = 1000 // max number of objects per request
var itemsCount uint64 // number of items per page in response
var offset uint64 // for the next page of requests
2017-02-25 14:39:16 +01:00
for {
2018-11-19 18:27:00 +01:00
opts := & api . ResourceInfoRequestOptions {
Limit : limit ,
Offset : offset ,
}
2019-09-04 21:00:37 +02:00
info , err := f . readMetaDataForPath ( ctx , root , opts )
2018-11-19 18:27:00 +01:00
2017-02-25 14:39:16 +01:00
if err != nil {
2018-11-19 18:27:00 +01:00
if apiErr , ok := err . ( * api . ErrorResponse ) ; ok {
// does not exist
if apiErr . ErrorName == "DiskNotFoundError" {
return nil , fs . ErrorDirNotFound
}
2017-04-26 19:16:59 +02:00
}
2017-06-11 23:43:31 +02:00
return nil , err
2017-02-25 14:39:16 +01:00
}
2018-11-19 18:27:00 +01:00
itemsCount = uint64 ( len ( info . Embedded . Items ) )
2017-02-25 14:39:16 +01:00
2018-11-19 18:27:00 +01:00
if info . ResourceType == "dir" {
2017-02-25 14:39:16 +01:00
//list all subdirs
2018-11-19 18:27:00 +01:00
for _ , element := range info . Embedded . Items {
2019-10-05 11:22:43 +02:00
element . Name = enc . ToStandardName ( element . Name )
2017-02-25 14:39:16 +01:00
remote := path . Join ( dir , element . Name )
2019-09-04 21:00:37 +02:00
entry , err := f . itemToDirEntry ( ctx , remote , & element )
2017-06-11 23:43:31 +02:00
if err != nil {
return nil , err
}
if entry != nil {
entries = append ( entries , entry )
2016-04-21 21:06:21 +02:00
}
}
2018-11-19 18:27:00 +01:00
} else if info . ResourceType == "file" {
return nil , fs . ErrorIsFile
2016-04-21 21:06:21 +02:00
}
2017-02-25 14:39:16 +01:00
//offset for the next page of items
offset += itemsCount
//check if we reached end of list
if itemsCount < limit {
break
}
2016-04-21 21:06:21 +02:00
}
2015-12-07 05:01:03 +01:00
2018-11-19 18:27:00 +01:00
return entries , nil
2015-12-07 05:01:03 +01:00
}
2016-06-25 22:58:34 +02:00
// Return an Object from a path
2015-12-07 05:01:03 +01:00
//
2016-06-25 22:23:20 +02:00
// If it can't be found it returns the error fs.ErrorObjectNotFound.
2019-09-04 21:00:37 +02:00
func ( f * Fs ) newObjectWithInfo ( ctx context . Context , remote string , info * api . ResourceInfoResponse ) ( fs . Object , error ) {
2017-02-25 16:23:27 +01:00
o := & Object {
2015-12-07 05:01:03 +01:00
fs : f ,
remote : remote ,
}
2017-02-25 16:23:27 +01:00
var err error
2015-12-07 05:01:03 +01:00
if info != nil {
2017-02-25 12:09:57 +01:00
err = o . setMetaData ( info )
2015-12-07 05:01:03 +01:00
} else {
2019-09-04 21:00:37 +02:00
err = o . readMetaData ( ctx )
2018-11-19 18:27:00 +01:00
if apiErr , ok := err . ( * api . ErrorResponse ) ; ok {
// does not exist
if apiErr . ErrorName == "DiskNotFoundError" {
return nil , fs . ErrorObjectNotFound
}
}
2017-02-25 12:09:57 +01:00
}
if err != nil {
return nil , err
2015-12-07 05:01:03 +01:00
}
2016-06-25 22:23:20 +02:00
return o , nil
2015-12-07 05:01:03 +01:00
}
2018-11-19 18:27:00 +01:00
// NewObject finds the Object at remote. If it can't be found it
// returns the error fs.ErrorObjectNotFound.
2019-06-17 10:34:30 +02:00
func ( f * Fs ) NewObject ( ctx context . Context , remote string ) ( fs . Object , error ) {
2019-09-04 21:00:37 +02:00
return f . newObjectWithInfo ( ctx , remote , nil )
2015-12-07 05:01:03 +01:00
}
2018-11-19 18:27:00 +01:00
// Creates from the parameters passed in a half finished Object which
// must have setMetaData called on it
//
// Used to create new objects
func ( f * Fs ) createObject ( remote string , modTime time . Time , size int64 ) ( o * Object ) {
// Temporary Object under construction
o = & Object {
fs : f ,
remote : remote ,
size : size ,
modTime : modTime ,
2015-12-07 05:01:03 +01:00
}
2018-11-19 18:27:00 +01:00
return o
2015-12-07 05:01:03 +01:00
}
// Put the object
//
// Copy the reader in to the new object which is returned
//
// The new object may have been created if an error is returned
2019-06-17 10:34:30 +02:00
func ( f * Fs ) Put ( ctx context . Context , in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( fs . Object , error ) {
o := f . createObject ( src . Remote ( ) , src . ModTime ( ctx ) , src . Size ( ) )
return o , o . Update ( ctx , in , src , options ... )
2015-12-07 05:01:03 +01:00
}
2017-08-19 14:07:23 +02:00
// PutStream uploads to the remote path with the modTime given of indeterminate size
2019-06-17 10:34:30 +02:00
func ( f * Fs ) PutStream ( ctx context . Context , in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( fs . Object , error ) {
return f . Put ( ctx , in , src , options ... )
2017-08-19 14:07:23 +02:00
}
2018-11-19 18:27:00 +01:00
// CreateDir makes a directory
2019-09-04 21:00:37 +02:00
func ( f * Fs ) CreateDir ( ctx context . Context , path string ) ( err error ) {
2018-11-19 18:27:00 +01:00
//fmt.Printf("CreateDir: %s\n", path)
var resp * http . Response
opts := rest . Opts {
Method : "PUT" ,
Path : "/resources" ,
Parameters : url . Values { } ,
NoResponse : true ,
}
2019-10-05 11:22:43 +02:00
// If creating a directory with a : use (undocumented) disk: prefix
if strings . IndexRune ( path , ':' ) >= 0 {
path = "disk:" + path
}
opts . Parameters . Set ( "path" , enc . FromStandardPath ( path ) )
2018-11-19 18:27:00 +01:00
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = f . srv . Call ( ctx , & opts )
2018-11-19 18:27:00 +01:00
return shouldRetry ( resp , err )
} )
if err != nil {
2019-10-05 11:22:43 +02:00
// fmt.Printf("CreateDir %q Error: %s\n", path, err.Error())
2018-11-19 18:27:00 +01:00
return err
}
// fmt.Printf("...Id %q\n", *info.Id)
return nil
}
// This really needs improvement and especially proper error checking
// but Yandex does not publish a List of possible errors and when they're
// expected to occur.
2019-09-04 21:00:37 +02:00
func ( f * Fs ) mkDirs ( ctx context . Context , path string ) ( err error ) {
2018-11-19 18:27:00 +01:00
//trim filename from path
//dirString := strings.TrimSuffix(path, filepath.Base(path))
//trim "disk:" from path
dirString := strings . TrimPrefix ( path , "disk:" )
if dirString == "" {
return nil
}
2019-09-04 21:00:37 +02:00
if err = f . CreateDir ( ctx , dirString ) ; err != nil {
2018-11-19 18:27:00 +01:00
if apiErr , ok := err . ( * api . ErrorResponse ) ; ok {
// allready exists
if apiErr . ErrorName != "DiskPathPointsToExistentDirectoryError" {
// 2 if it fails then create all directories in the path from root.
dirs := strings . Split ( dirString , "/" ) //path separator
var mkdirpath = "/" //path separator /
for _ , element := range dirs {
if element != "" {
mkdirpath += element + "/" //path separator /
2019-09-04 21:00:37 +02:00
if err = f . CreateDir ( ctx , mkdirpath ) ; err != nil {
2018-11-19 18:27:00 +01:00
// ignore errors while creating dirs
}
}
}
}
return nil
}
}
return err
}
2019-09-04 21:00:37 +02:00
func ( f * Fs ) mkParentDirs ( ctx context . Context , resPath string ) error {
2018-11-19 18:27:00 +01:00
// defer log.Trace(dirPath, "")("")
// chop off trailing / if it exists
if strings . HasSuffix ( resPath , "/" ) {
resPath = resPath [ : len ( resPath ) - 1 ]
}
parent := path . Dir ( resPath )
if parent == "." {
parent = ""
}
2019-09-04 21:00:37 +02:00
return f . mkDirs ( ctx , parent )
2018-11-19 18:27:00 +01:00
}
2015-12-07 05:01:03 +01:00
// Mkdir creates the container if it doesn't exist
2019-06-17 10:34:30 +02:00
func ( f * Fs ) Mkdir ( ctx context . Context , dir string ) error {
2018-11-19 18:27:00 +01:00
path := f . filePath ( dir )
2019-09-04 21:00:37 +02:00
return f . mkDirs ( ctx , path )
2018-11-19 18:27:00 +01:00
}
// waitForJob waits for the job with status in url to complete
2019-09-04 21:00:37 +02:00
func ( f * Fs ) waitForJob ( ctx context . Context , location string ) ( err error ) {
2018-11-19 18:27:00 +01:00
opts := rest . Opts {
RootURL : location ,
Method : "GET" ,
2016-11-25 22:52:43 +01:00
}
2018-11-19 18:27:00 +01:00
deadline := time . Now ( ) . Add ( fs . Config . Timeout )
for time . Now ( ) . Before ( deadline ) {
var resp * http . Response
var body [ ] byte
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = f . srv . Call ( ctx , & opts )
2018-11-19 18:27:00 +01:00
if err != nil {
return fserrors . ShouldRetry ( err ) , err
}
body , err = rest . ReadBody ( resp )
return fserrors . ShouldRetry ( err ) , err
} )
if err != nil {
return err
}
// Try to decode the body first as an api.AsyncOperationStatus
var status api . AsyncStatus
err = json . Unmarshal ( body , & status )
if err != nil {
return errors . Wrapf ( err , "async status result not JSON: %q" , body )
}
switch status . Status {
case "failure" :
return errors . Errorf ( "async operation returned %q" , status . Status )
case "success" :
return nil
}
time . Sleep ( 1 * time . Second )
}
return errors . Errorf ( "async operation didn't complete after %v" , fs . Config . Timeout )
2015-12-07 05:01:03 +01:00
}
2019-09-04 21:00:37 +02:00
func ( f * Fs ) delete ( ctx context . Context , path string , hardDelete bool ) ( err error ) {
2018-11-19 18:27:00 +01:00
opts := rest . Opts {
Method : "DELETE" ,
Path : "/resources" ,
Parameters : url . Values { } ,
}
2019-10-05 11:22:43 +02:00
opts . Parameters . Set ( "path" , enc . FromStandardPath ( path ) )
2018-11-19 18:27:00 +01:00
opts . Parameters . Set ( "permanently" , strconv . FormatBool ( hardDelete ) )
var resp * http . Response
var body [ ] byte
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = f . srv . Call ( ctx , & opts )
2018-11-19 18:27:00 +01:00
if err != nil {
return fserrors . ShouldRetry ( err ) , err
}
body , err = rest . ReadBody ( resp )
return fserrors . ShouldRetry ( err ) , err
} )
if err != nil {
return err
}
// if 202 Accepted it's an async operation we have to wait for it complete before retuning
if resp . StatusCode == 202 {
var info api . AsyncInfo
err = json . Unmarshal ( body , & info )
if err != nil {
return errors . Wrapf ( err , "async info result not JSON: %q" , body )
}
2019-09-04 21:00:37 +02:00
return f . waitForJob ( ctx , info . HRef )
2018-11-19 18:27:00 +01:00
}
return nil
2015-12-07 05:01:03 +01:00
}
// purgeCheck remotes the root directory, if check is set then it
// refuses to do so if it has anything in
2019-09-04 21:00:37 +02:00
func ( f * Fs ) purgeCheck ( ctx context . Context , dir string , check bool ) error {
2018-11-19 18:27:00 +01:00
root := f . filePath ( dir )
2015-12-07 05:01:03 +01:00
if check {
//to comply with rclone logic we check if the directory is empty before delete.
//send request to get list of objects in this directory.
2019-09-04 21:00:37 +02:00
info , err := f . readMetaDataForPath ( ctx , root , & api . ResourceInfoRequestOptions { } )
2015-12-07 05:01:03 +01:00
if err != nil {
2016-06-12 16:06:02 +02:00
return errors . Wrap ( err , "rmdir failed" )
2015-12-07 05:01:03 +01:00
}
2018-11-19 18:27:00 +01:00
if len ( info . Embedded . Items ) != 0 {
return fs . ErrorDirectoryNotEmpty
2015-12-07 05:01:03 +01:00
}
}
//delete directory
2019-09-04 21:00:37 +02:00
return f . delete ( ctx , root , false )
2015-12-07 05:01:03 +01:00
}
2018-11-19 18:27:00 +01:00
// Rmdir deletes the container
//
// Returns an error if it isn't empty
2019-06-17 10:34:30 +02:00
func ( f * Fs ) Rmdir ( ctx context . Context , dir string ) error {
2019-09-04 21:00:37 +02:00
return f . purgeCheck ( ctx , dir , true )
2015-12-07 05:01:03 +01:00
}
// Purge deletes all the files and the container
//
// Optional interface: Only implement this if you have a way of
// deleting all the files quicker than just running Remove() on the
// result of List()
2019-06-17 10:34:30 +02:00
func ( f * Fs ) Purge ( ctx context . Context ) error {
2019-09-04 21:00:37 +02:00
return f . purgeCheck ( ctx , "" , false )
2015-12-07 05:01:03 +01:00
}
2019-02-07 18:41:17 +01:00
// copyOrMoves copies or moves directories or files depending on the method parameter
2019-09-04 21:00:37 +02:00
func ( f * Fs ) copyOrMove ( ctx context . Context , method , src , dst string , overwrite bool ) ( err error ) {
2018-11-19 18:27:00 +01:00
opts := rest . Opts {
Method : "POST" ,
Path : "/resources/" + method ,
Parameters : url . Values { } ,
}
2019-10-05 11:22:43 +02:00
opts . Parameters . Set ( "from" , enc . FromStandardPath ( src ) )
opts . Parameters . Set ( "path" , enc . FromStandardPath ( dst ) )
2018-11-19 18:27:00 +01:00
opts . Parameters . Set ( "overwrite" , strconv . FormatBool ( overwrite ) )
var resp * http . Response
var body [ ] byte
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = f . srv . Call ( ctx , & opts )
2018-11-19 18:27:00 +01:00
if err != nil {
return fserrors . ShouldRetry ( err ) , err
}
body , err = rest . ReadBody ( resp )
return fserrors . ShouldRetry ( err ) , err
} )
if err != nil {
return err
}
// if 202 Accepted it's an async operation we have to wait for it complete before retuning
if resp . StatusCode == 202 {
var info api . AsyncInfo
err = json . Unmarshal ( body , & info )
if err != nil {
return errors . Wrapf ( err , "async info result not JSON: %q" , body )
}
2019-09-04 21:00:37 +02:00
return f . waitForJob ( ctx , info . HRef )
2018-11-19 18:27:00 +01:00
}
return nil
}
// Copy src to this remote using server side copy operations.
//
// This is stored with the remote path given
//
// It returns the destination Object and a possible error
//
// Will only be called if src.Fs().Name() == f.Name()
//
// If it isn't possible then return fs.ErrorCantCopy
2019-06-17 10:34:30 +02:00
func ( f * Fs ) Copy ( ctx context . Context , src fs . Object , remote string ) ( fs . Object , error ) {
2018-11-19 18:27:00 +01:00
srcObj , ok := src . ( * Object )
if ! ok {
fs . Debugf ( src , "Can't copy - not same remote type" )
return nil , fs . ErrorCantCopy
}
dstPath := f . filePath ( remote )
2019-09-04 21:00:37 +02:00
err := f . mkParentDirs ( ctx , dstPath )
2018-11-19 18:27:00 +01:00
if err != nil {
return nil , err
}
2019-09-04 21:00:37 +02:00
err = f . copyOrMove ( ctx , "copy" , srcObj . filePath ( ) , dstPath , false )
2018-11-19 18:27:00 +01:00
if err != nil {
return nil , errors . Wrap ( err , "couldn't copy file" )
}
2019-06-17 10:34:30 +02:00
return f . NewObject ( ctx , remote )
2018-11-19 18:27:00 +01:00
}
// Move src to this remote using server side move operations.
//
// This is stored with the remote path given
//
// It returns the destination Object and a possible error
//
// Will only be called if src.Fs().Name() == f.Name()
//
// If it isn't possible then return fs.ErrorCantMove
2019-06-17 10:34:30 +02:00
func ( f * Fs ) Move ( ctx context . Context , src fs . Object , remote string ) ( fs . Object , error ) {
2018-11-19 18:27:00 +01:00
srcObj , ok := src . ( * Object )
if ! ok {
fs . Debugf ( src , "Can't move - not same remote type" )
return nil , fs . ErrorCantMove
}
dstPath := f . filePath ( remote )
2019-09-04 21:00:37 +02:00
err := f . mkParentDirs ( ctx , dstPath )
2018-11-19 18:27:00 +01:00
if err != nil {
return nil , err
}
2019-09-04 21:00:37 +02:00
err = f . copyOrMove ( ctx , "move" , srcObj . filePath ( ) , dstPath , false )
2018-11-19 18:27:00 +01:00
if err != nil {
return nil , errors . Wrap ( err , "couldn't move file" )
}
2019-06-17 10:34:30 +02:00
return f . NewObject ( ctx , remote )
2018-11-19 18:27:00 +01:00
}
// DirMove moves src, srcRemote to this remote at dstRemote
// using server side move operations.
//
// Will only be called if src.Fs().Name() == f.Name()
//
// If it isn't possible then return fs.ErrorCantDirMove
//
// If destination exists then return fs.ErrorDirExists
2019-06-17 10:34:30 +02:00
func ( f * Fs ) DirMove ( ctx context . Context , src fs . Fs , srcRemote , dstRemote string ) error {
2018-11-19 18:27:00 +01:00
srcFs , ok := src . ( * Fs )
if ! ok {
fs . Debugf ( srcFs , "Can't move directory - not same remote type" )
return fs . ErrorCantDirMove
}
srcPath := path . Join ( srcFs . diskRoot , srcRemote )
dstPath := f . dirPath ( dstRemote )
//fmt.Printf("Move src: %s (FullPath: %s), dst: %s (FullPath: %s)\n", srcRemote, srcPath, dstRemote, dstPath)
// Refuse to move to or from the root
if srcPath == "disk:/" || dstPath == "disk:/" {
fs . Debugf ( src , "DirMove error: Can't move root" )
return errors . New ( "can't move root directory" )
}
2019-09-04 21:00:37 +02:00
err := f . mkParentDirs ( ctx , dstPath )
2018-11-19 18:27:00 +01:00
if err != nil {
return err
}
2019-09-04 21:00:37 +02:00
_ , err = f . readMetaDataForPath ( ctx , dstPath , & api . ResourceInfoRequestOptions { } )
2018-11-19 18:27:00 +01:00
if apiErr , ok := err . ( * api . ErrorResponse ) ; ok {
// does not exist
if apiErr . ErrorName == "DiskNotFoundError" {
// OK
}
} else if err != nil {
return err
} else {
return fs . ErrorDirExists
}
2019-09-04 21:00:37 +02:00
err = f . copyOrMove ( ctx , "move" , srcPath , dstPath , false )
2018-11-19 18:27:00 +01:00
if err != nil {
return errors . Wrap ( err , "couldn't move directory" )
}
return nil
}
// PublicLink generates a public link to the remote path (usually readable by anyone)
2019-06-17 10:34:30 +02:00
func ( f * Fs ) PublicLink ( ctx context . Context , remote string ) ( link string , err error ) {
2018-11-19 18:27:00 +01:00
var path string
if f . opt . Unlink {
path = "/resources/unpublish"
} else {
path = "/resources/publish"
}
opts := rest . Opts {
Method : "PUT" ,
2019-10-05 11:22:43 +02:00
Path : enc . FromStandardPath ( path ) ,
2018-11-19 18:27:00 +01:00
Parameters : url . Values { } ,
NoResponse : true ,
}
2019-10-05 11:22:43 +02:00
opts . Parameters . Set ( "path" , enc . FromStandardPath ( f . filePath ( remote ) ) )
2018-11-19 18:27:00 +01:00
var resp * http . Response
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = f . srv . Call ( ctx , & opts )
2018-11-19 18:27:00 +01:00
return shouldRetry ( resp , err )
} )
if apiErr , ok := err . ( * api . ErrorResponse ) ; ok {
// does not exist
if apiErr . ErrorName == "DiskNotFoundError" {
return "" , fs . ErrorObjectNotFound
}
}
if err != nil {
if f . opt . Unlink {
return "" , errors . Wrap ( err , "couldn't remove public link" )
}
return "" , errors . Wrap ( err , "couldn't create public link" )
}
2019-09-04 21:00:37 +02:00
info , err := f . readMetaDataForPath ( ctx , f . filePath ( remote ) , & api . ResourceInfoRequestOptions { } )
2018-11-19 18:27:00 +01:00
if err != nil {
return "" , err
}
if info . PublicURL == "" {
return "" , errors . New ( "couldn't create public link - no link path received" )
}
return info . PublicURL , nil
}
2017-09-07 19:10:39 +02:00
// CleanUp permanently deletes all trashed files/folders
2019-06-17 10:34:30 +02:00
func ( f * Fs ) CleanUp ( ctx context . Context ) ( err error ) {
2018-11-19 18:27:00 +01:00
var resp * http . Response
opts := rest . Opts {
Method : "DELETE" ,
Path : "/trash/resources" ,
NoResponse : true ,
}
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = f . srv . Call ( ctx , & opts )
2018-11-19 18:27:00 +01:00
return shouldRetry ( resp , err )
} )
return err
2017-09-07 19:10:39 +02:00
}
2018-11-19 18:27:00 +01:00
// About gets quota information
2019-06-17 10:34:30 +02:00
func ( f * Fs ) About ( ctx context . Context ) ( * fs . Usage , error ) {
2018-11-19 18:27:00 +01:00
opts := rest . Opts {
Method : "GET" ,
Path : "/" ,
}
var resp * http . Response
var info api . DiskInfo
var err error
err = f . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = f . srv . CallJSON ( ctx , & opts , nil , & info )
2018-11-19 18:27:00 +01:00
return shouldRetry ( resp , err )
} )
if err != nil {
return nil , err
}
usage := & fs . Usage {
Total : fs . NewUsageValue ( info . TotalSpace ) ,
Used : fs . NewUsageValue ( info . UsedSpace ) ,
Free : fs . NewUsageValue ( info . TotalSpace - info . UsedSpace ) ,
}
return usage , nil
2016-01-11 13:39:33 +01:00
}
2015-12-07 05:01:03 +01:00
// ------------------------------------------------------------
// Fs returns the parent Fs
2016-02-18 12:35:25 +01:00
func ( o * Object ) Fs ( ) fs . Info {
2015-12-07 05:01:03 +01:00
return o . fs
}
// Return a string version
func ( o * Object ) String ( ) string {
if o == nil {
return "<nil>"
}
return o . remote
}
// Remote returns the remote path
func ( o * Object ) Remote ( ) string {
return o . remote
}
2018-11-19 18:27:00 +01:00
// Returns the full remote path for the object
func ( o * Object ) filePath ( ) string {
return o . fs . filePath ( o . remote )
}
// setMetaData sets the fs data from a storage.Object
func ( o * Object ) setMetaData ( info * api . ResourceInfoResponse ) ( err error ) {
o . hasMetaData = true
o . size = info . Size
o . md5sum = info . Md5
o . mimeType = info . MimeType
var modTimeString string
modTimeObj , ok := info . CustomProperties [ "rclone_modified" ]
if ok {
// read modTime from rclone_modified custom_property of object
modTimeString , ok = modTimeObj . ( string )
2016-01-11 13:39:33 +01:00
}
2018-11-19 18:27:00 +01:00
if ! ok {
// read modTime from Modified property of object as a fallback
modTimeString = info . Modified
}
t , err := time . Parse ( time . RFC3339Nano , modTimeString )
if err != nil {
return errors . Wrapf ( err , "failed to parse modtime from %q" , modTimeString )
}
o . modTime = t
return nil
2015-12-07 05:01:03 +01:00
}
2018-11-19 18:27:00 +01:00
// readMetaData reads ands sets the new metadata for a storage.Object
2019-09-04 21:00:37 +02:00
func ( o * Object ) readMetaData ( ctx context . Context ) ( err error ) {
2018-11-19 18:27:00 +01:00
if o . hasMetaData {
return nil
}
2019-09-04 21:00:37 +02:00
info , err := o . fs . readMetaDataForPath ( ctx , o . filePath ( ) , & api . ResourceInfoRequestOptions { } )
2018-11-19 18:27:00 +01:00
if err != nil {
return err
}
if info . ResourceType != "file" {
return fs . ErrorNotAFile
}
return o . setMetaData ( info )
2015-12-07 05:01:03 +01:00
}
// ModTime returns the modification time of the object
//
// It attempts to read the objects mtime and if that isn't present the
// LastModified returned in the http headers
2019-06-17 10:34:30 +02:00
func ( o * Object ) ModTime ( ctx context . Context ) time . Time {
2019-09-04 21:00:37 +02:00
err := o . readMetaData ( ctx )
2015-12-07 05:01:03 +01:00
if err != nil {
2017-02-09 12:01:20 +01:00
fs . Logf ( o , "Failed to read metadata: %v" , err )
2015-12-07 05:01:03 +01:00
return time . Now ( )
}
return o . modTime
}
2018-11-19 18:27:00 +01:00
// Size returns the size of an object in bytes
func ( o * Object ) Size ( ) int64 {
2019-09-04 21:00:37 +02:00
ctx := context . TODO ( )
err := o . readMetaData ( ctx )
2018-11-19 18:27:00 +01:00
if err != nil {
fs . Logf ( o , "Failed to read metadata: %v" , err )
return 0
}
return o . size
2015-12-07 05:01:03 +01:00
}
2018-11-19 18:27:00 +01:00
// Hash returns the Md5sum of an object returning a lowercase hex string
2019-06-17 10:34:30 +02:00
func ( o * Object ) Hash ( ctx context . Context , t hash . Type ) ( string , error ) {
2018-11-19 18:27:00 +01:00
if t != hash . MD5 {
return "" , hash . ErrUnsupported
}
return o . md5sum , nil
}
// Storable returns whether this object is storable
func ( o * Object ) Storable ( ) bool {
return true
}
2019-09-04 21:00:37 +02:00
func ( o * Object ) setCustomProperty ( ctx context . Context , property string , value string ) ( err error ) {
2018-11-19 18:27:00 +01:00
var resp * http . Response
opts := rest . Opts {
Method : "PATCH" ,
Path : "/resources" ,
Parameters : url . Values { } ,
NoResponse : true ,
}
2019-10-05 11:22:43 +02:00
opts . Parameters . Set ( "path" , enc . FromStandardPath ( o . filePath ( ) ) )
2018-11-19 18:27:00 +01:00
rcm := map [ string ] interface { } {
property : value ,
}
cpr := api . CustomPropertyResponse { CustomProperties : rcm }
err = o . fs . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = o . fs . srv . CallJSON ( ctx , & opts , & cpr , nil )
2018-11-19 18:27:00 +01:00
return shouldRetry ( resp , err )
} )
return err
2015-12-07 05:01:03 +01:00
}
// SetModTime sets the modification time of the local fs object
//
// Commits the datastore
2019-06-17 10:34:30 +02:00
func ( o * Object ) SetModTime ( ctx context . Context , modTime time . Time ) error {
2016-09-21 23:13:24 +02:00
// set custom_property 'rclone_modified' of object to modTime
2019-09-04 21:00:37 +02:00
err := o . setCustomProperty ( ctx , "rclone_modified" , modTime . Format ( time . RFC3339Nano ) )
2016-09-21 23:13:24 +02:00
if err != nil {
return err
}
o . modTime = modTime
return nil
2015-12-07 05:01:03 +01:00
}
2018-11-19 18:27:00 +01:00
// Open an object for read
2019-06-17 10:34:30 +02:00
func ( o * Object ) Open ( ctx context . Context , options ... fs . OpenOption ) ( in io . ReadCloser , err error ) {
2018-11-19 18:27:00 +01:00
// prepare download
var resp * http . Response
var dl api . AsyncInfo
opts := rest . Opts {
Method : "GET" ,
Path : "/resources/download" ,
Parameters : url . Values { } ,
}
2019-10-05 11:22:43 +02:00
opts . Parameters . Set ( "path" , enc . FromStandardPath ( o . filePath ( ) ) )
2018-11-19 18:27:00 +01:00
err = o . fs . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = o . fs . srv . CallJSON ( ctx , & opts , nil , & dl )
2018-11-19 18:27:00 +01:00
return shouldRetry ( resp , err )
} )
if err != nil {
return nil , err
}
// perform the download
opts = rest . Opts {
RootURL : dl . HRef ,
Method : "GET" ,
Options : options ,
}
err = o . fs . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = o . fs . srv . Call ( ctx , & opts )
2018-11-19 18:27:00 +01:00
return shouldRetry ( resp , err )
} )
if err != nil {
return nil , err
}
return resp . Body , err
2015-12-07 05:01:03 +01:00
}
2019-09-04 21:00:37 +02:00
func ( o * Object ) upload ( ctx context . Context , in io . Reader , overwrite bool , mimeType string ) ( err error ) {
2018-11-19 18:27:00 +01:00
// prepare upload
var resp * http . Response
var ur api . AsyncInfo
opts := rest . Opts {
Method : "GET" ,
Path : "/resources/upload" ,
Parameters : url . Values { } ,
}
2019-10-05 11:22:43 +02:00
opts . Parameters . Set ( "path" , enc . FromStandardPath ( o . filePath ( ) ) )
2018-11-19 18:27:00 +01:00
opts . Parameters . Set ( "overwrite" , strconv . FormatBool ( overwrite ) )
err = o . fs . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = o . fs . srv . CallJSON ( ctx , & opts , nil , & ur )
2018-11-19 18:27:00 +01:00
return shouldRetry ( resp , err )
} )
if err != nil {
return err
}
// perform the actual upload
opts = rest . Opts {
RootURL : ur . HRef ,
Method : "PUT" ,
ContentType : mimeType ,
Body : in ,
NoResponse : true ,
}
err = o . fs . pacer . Call ( func ( ) ( bool , error ) {
2019-09-04 21:00:37 +02:00
resp , err = o . fs . srv . Call ( ctx , & opts )
2018-11-19 18:27:00 +01:00
return shouldRetry ( resp , err )
} )
return err
2015-12-07 05:01:03 +01:00
}
// Update the already existing object
//
// Copy the reader into the object updating modTime and size
//
// The new object may have been created if an error is returned
2019-06-17 10:34:30 +02:00
func ( o * Object ) Update ( ctx context . Context , in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) error {
2018-11-19 18:27:00 +01:00
in1 := readers . NewCountingReader ( in )
2019-06-17 10:34:30 +02:00
modTime := src . ModTime ( ctx )
2018-11-19 18:27:00 +01:00
remote := o . filePath ( )
2016-02-18 12:35:25 +01:00
2015-12-07 05:01:03 +01:00
//create full path to file before upload.
2019-09-04 21:00:37 +02:00
err := o . fs . mkParentDirs ( ctx , remote )
2018-11-19 18:27:00 +01:00
if err != nil {
return err
2015-12-07 05:01:03 +01:00
}
2018-11-19 18:27:00 +01:00
2015-12-07 05:01:03 +01:00
//upload file
2019-09-04 21:00:37 +02:00
err = o . upload ( ctx , in1 , true , fs . MimeType ( ctx , src ) )
2018-11-19 18:27:00 +01:00
if err != nil {
return err
2015-12-07 05:01:03 +01:00
}
2019-02-07 18:41:17 +01:00
//if file uploaded successfully then return metadata
2018-11-19 18:27:00 +01:00
o . modTime = modTime
o . md5sum = "" // according to unit tests after put the md5 is empty.
o . size = int64 ( in1 . BytesRead ( ) ) // better solution o.readMetaData() ?
//and set modTime of uploaded file
2019-06-17 10:34:30 +02:00
err = o . SetModTime ( ctx , modTime )
2015-12-07 05:01:03 +01:00
2018-11-19 18:27:00 +01:00
return err
2015-12-07 05:01:03 +01:00
}
2018-11-19 18:27:00 +01:00
// Remove an object
2019-06-17 10:34:30 +02:00
func ( o * Object ) Remove ( ctx context . Context ) error {
2019-09-04 21:00:37 +02:00
return o . fs . delete ( ctx , o . filePath ( ) , false )
2015-12-07 05:01:03 +01:00
}
2016-09-21 23:13:24 +02:00
// MimeType of an Object if known, "" otherwise
2019-06-17 10:34:30 +02:00
func ( o * Object ) MimeType ( ctx context . Context ) string {
2016-09-21 23:13:24 +02:00
return o . mimeType
}
2015-12-07 05:01:03 +01:00
// Check the interfaces are satisfied
var (
2018-11-19 18:27:00 +01:00
_ fs . Fs = ( * Fs ) ( nil )
_ fs . Purger = ( * Fs ) ( nil )
_ fs . Copier = ( * Fs ) ( nil )
_ fs . Mover = ( * Fs ) ( nil )
_ fs . DirMover = ( * Fs ) ( nil )
_ fs . PublicLinker = ( * Fs ) ( nil )
_ fs . CleanUpper = ( * Fs ) ( nil )
_ fs . Abouter = ( * Fs ) ( nil )
_ fs . Object = ( * Object ) ( nil )
_ fs . MimeTyper = ( * Object ) ( nil )
2015-12-07 05:01:03 +01:00
)