github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/backend/fichier/fichier.go (about) 1 package fichier 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "strconv" 9 "strings" 10 "time" 11 12 "github.com/pkg/errors" 13 "github.com/rclone/rclone/fs" 14 "github.com/rclone/rclone/fs/config" 15 "github.com/rclone/rclone/fs/config/configmap" 16 "github.com/rclone/rclone/fs/config/configstruct" 17 "github.com/rclone/rclone/fs/fshttp" 18 "github.com/rclone/rclone/fs/hash" 19 "github.com/rclone/rclone/lib/dircache" 20 "github.com/rclone/rclone/lib/encoder" 21 "github.com/rclone/rclone/lib/pacer" 22 "github.com/rclone/rclone/lib/rest" 23 ) 24 25 const ( 26 rootID = "0" 27 apiBaseURL = "https://api.1fichier.com/v1" 28 minSleep = 334 * time.Millisecond // 3 API calls per second is recommended 29 maxSleep = 5 * time.Second 30 decayConstant = 2 // bigger for slower decay, exponential 31 ) 32 33 func init() { 34 fs.Register(&fs.RegInfo{ 35 Name: "fichier", 36 Description: "1Fichier", 37 Config: func(name string, config configmap.Mapper) { 38 }, 39 NewFs: NewFs, 40 Options: []fs.Option{{ 41 Help: "Your API Key, get it from https://1fichier.com/console/params.pl", 42 Name: "api_key", 43 }, { 44 Help: "If you want to download a shared folder, add this parameter", 45 Name: "shared_folder", 46 Required: false, 47 Advanced: true, 48 }, { 49 Name: config.ConfigEncoding, 50 Help: config.ConfigEncodingHelp, 51 Advanced: true, 52 // Characters that need escaping 53 // 54 // '\\': '\', // FULLWIDTH REVERSE SOLIDUS 55 // '<': '<', // FULLWIDTH LESS-THAN SIGN 56 // '>': '>', // FULLWIDTH GREATER-THAN SIGN 57 // '"': '"', // FULLWIDTH QUOTATION MARK - not on the list but seems to be reserved 58 // '\'': ''', // FULLWIDTH APOSTROPHE 59 // '$': '$', // FULLWIDTH DOLLAR SIGN 60 // '`': '`', // FULLWIDTH GRAVE ACCENT 61 // 62 // Leading space and trailing space 63 Default: (encoder.Display | 64 encoder.EncodeBackSlash | 65 encoder.EncodeSingleQuote | 66 encoder.EncodeBackQuote | 67 encoder.EncodeDoubleQuote | 68 encoder.EncodeLtGt | 69 encoder.EncodeDollar | 70 encoder.EncodeLeftSpace | 71 encoder.EncodeRightSpace | 72 encoder.EncodeInvalidUtf8), 73 }}, 74 }) 75 } 76 77 // Options defines the configuration for this backend 78 type Options struct { 79 APIKey string `config:"api_key"` 80 SharedFolder string `config:"shared_folder"` 81 Enc encoder.MultiEncoder `config:"encoding"` 82 } 83 84 // Fs is the interface a cloud storage system must provide 85 type Fs struct { 86 root string 87 name string 88 features *fs.Features 89 opt Options 90 dirCache *dircache.DirCache 91 baseClient *http.Client 92 pacer *fs.Pacer 93 rest *rest.Client 94 } 95 96 // FindLeaf finds a directory of name leaf in the folder with ID pathID 97 func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) { 98 folderID, err := strconv.Atoi(pathID) 99 if err != nil { 100 return "", false, err 101 } 102 folders, err := f.listFolders(ctx, folderID) 103 if err != nil { 104 return "", false, err 105 } 106 107 for _, folder := range folders.SubFolders { 108 if folder.Name == leaf { 109 pathIDOut := strconv.Itoa(folder.ID) 110 return pathIDOut, true, nil 111 } 112 } 113 114 return "", false, nil 115 } 116 117 // CreateDir makes a directory with pathID as parent and name leaf 118 func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) { 119 folderID, err := strconv.Atoi(pathID) 120 if err != nil { 121 return "", err 122 } 123 resp, err := f.makeFolder(ctx, leaf, folderID) 124 if err != nil { 125 return "", err 126 } 127 return strconv.Itoa(resp.FolderID), err 128 } 129 130 // Name of the remote (as passed into NewFs) 131 func (f *Fs) Name() string { 132 return f.name 133 } 134 135 // Root of the remote (as passed into NewFs) 136 func (f *Fs) Root() string { 137 return f.root 138 } 139 140 // String returns a description of the FS 141 func (f *Fs) String() string { 142 return fmt.Sprintf("1Fichier root '%s'", f.root) 143 } 144 145 // Precision of the ModTimes in this Fs 146 func (f *Fs) Precision() time.Duration { 147 return fs.ModTimeNotSupported 148 } 149 150 // Hashes returns the supported hash types of the filesystem 151 func (f *Fs) Hashes() hash.Set { 152 return hash.Set(hash.Whirlpool) 153 } 154 155 // Features returns the optional features of this Fs 156 func (f *Fs) Features() *fs.Features { 157 return f.features 158 } 159 160 // NewFs makes a new Fs object from the path 161 // 162 // The path is of the form remote:path 163 // 164 // Remotes are looked up in the config file. If the remote isn't 165 // found then NotFoundInConfigFile will be returned. 166 // 167 // On Windows avoid single character remote names as they can be mixed 168 // up with drive letters. 169 func NewFs(name string, root string, config configmap.Mapper) (fs.Fs, error) { 170 opt := new(Options) 171 err := configstruct.Set(config, opt) 172 if err != nil { 173 return nil, err 174 } 175 176 // If using a Shared Folder override root 177 if opt.SharedFolder != "" { 178 root = "" 179 } 180 181 //workaround for wonky parser 182 root = strings.Trim(root, "/") 183 184 f := &Fs{ 185 name: name, 186 root: root, 187 opt: *opt, 188 pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), 189 baseClient: &http.Client{}, 190 } 191 192 f.features = (&fs.Features{ 193 DuplicateFiles: true, 194 CanHaveEmptyDirectories: true, 195 }).Fill(f) 196 197 client := fshttp.NewClient(fs.Config) 198 199 f.rest = rest.NewClient(client).SetRoot(apiBaseURL) 200 201 f.rest.SetHeader("Authorization", "Bearer "+f.opt.APIKey) 202 203 f.dirCache = dircache.New(root, rootID, f) 204 205 ctx := context.Background() 206 207 // Find the current root 208 err = f.dirCache.FindRoot(ctx, false) 209 if err != nil { 210 // Assume it is a file 211 newRoot, remote := dircache.SplitPath(root) 212 tempF := *f 213 tempF.dirCache = dircache.New(newRoot, rootID, &tempF) 214 tempF.root = newRoot 215 // Make new Fs which is the parent 216 err = tempF.dirCache.FindRoot(ctx, false) 217 if err != nil { 218 // No root so return old f 219 return f, nil 220 } 221 _, err := tempF.NewObject(ctx, remote) 222 if err != nil { 223 if err == fs.ErrorObjectNotFound { 224 // File doesn't exist so return old f 225 return f, nil 226 } 227 return nil, err 228 } 229 f.features.Fill(&tempF) 230 // XXX: update the old f here instead of returning tempF, since 231 // `features` were already filled with functions having *f as a receiver. 232 // See https://github.com/rclone/rclone/issues/2182 233 f.dirCache = tempF.dirCache 234 f.root = tempF.root 235 // return an error with an fs which points to the parent 236 return f, fs.ErrorIsFile 237 } 238 return f, nil 239 } 240 241 // List the objects and directories in dir into entries. The 242 // entries can be returned in any order but should be for a 243 // complete directory. 244 // 245 // dir should be "" to list the root, and should not have 246 // trailing slashes. 247 // 248 // This should return ErrDirNotFound if the directory isn't 249 // found. 250 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 251 if f.opt.SharedFolder != "" { 252 return f.listSharedFiles(ctx, f.opt.SharedFolder) 253 } 254 255 dirContent, err := f.listDir(ctx, dir) 256 if err != nil { 257 return nil, err 258 } 259 260 return dirContent, nil 261 } 262 263 // NewObject finds the Object at remote. If it can't be found 264 // it returns the error ErrorObjectNotFound. 265 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 266 leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, remote, false) 267 if err != nil { 268 if err == fs.ErrorDirNotFound { 269 return nil, fs.ErrorObjectNotFound 270 } 271 return nil, err 272 } 273 274 folderID, err := strconv.Atoi(directoryID) 275 if err != nil { 276 return nil, err 277 } 278 files, err := f.listFiles(ctx, folderID) 279 if err != nil { 280 return nil, err 281 } 282 283 for _, file := range files.Items { 284 if file.Filename == leaf { 285 path, ok := f.dirCache.GetInv(directoryID) 286 287 if !ok { 288 return nil, errors.New("Cannot find dir in dircache") 289 } 290 291 return f.newObjectFromFile(ctx, path, file), nil 292 } 293 } 294 295 return nil, fs.ErrorObjectNotFound 296 } 297 298 // Put in to the remote path with the modTime given of the given size 299 // 300 // When called from outside a Fs by rclone, src.Size() will always be >= 0. 301 // But for unknown-sized objects (indicated by src.Size() == -1), Put should either 302 // return an error or upload it properly (rather than e.g. calling panic). 303 // 304 // May create the object even if it returns an error - if so 305 // will return the object and the error, otherwise will return 306 // nil and the error 307 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 308 exisitingObj, err := f.NewObject(ctx, src.Remote()) 309 switch err { 310 case nil: 311 return exisitingObj, exisitingObj.Update(ctx, in, src, options...) 312 case fs.ErrorObjectNotFound: 313 // Not found so create it 314 return f.PutUnchecked(ctx, in, src, options...) 315 default: 316 return nil, err 317 } 318 } 319 320 // putUnchecked uploads the object with the given name and size 321 // 322 // This will create a duplicate if we upload a new file without 323 // checking to see if there is one already - use Put() for that. 324 func (f *Fs) putUnchecked(ctx context.Context, in io.Reader, remote string, size int64, options ...fs.OpenOption) (fs.Object, error) { 325 if size > int64(100e9) { 326 return nil, errors.New("File too big, cant upload") 327 } else if size == 0 { 328 return nil, fs.ErrorCantUploadEmptyFiles 329 } 330 331 nodeResponse, err := f.getUploadNode(ctx) 332 if err != nil { 333 return nil, err 334 } 335 336 leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, remote, true) 337 if err != nil { 338 return nil, err 339 } 340 341 _, err = f.uploadFile(ctx, in, size, leaf, directoryID, nodeResponse.ID, nodeResponse.URL) 342 if err != nil { 343 return nil, err 344 } 345 346 fileUploadResponse, err := f.endUpload(ctx, nodeResponse.ID, nodeResponse.URL) 347 if err != nil { 348 return nil, err 349 } 350 351 if len(fileUploadResponse.Links) != 1 { 352 return nil, errors.New("unexpected amount of files") 353 } 354 355 link := fileUploadResponse.Links[0] 356 fileSize, err := strconv.ParseInt(link.Size, 10, 64) 357 358 if err != nil { 359 return nil, err 360 } 361 362 return &Object{ 363 fs: f, 364 remote: remote, 365 file: File{ 366 ACL: 0, 367 CDN: 0, 368 Checksum: link.Whirlpool, 369 ContentType: "", 370 Date: time.Now().Format("2006-01-02 15:04:05"), 371 Filename: link.Filename, 372 Pass: 0, 373 Size: fileSize, 374 URL: link.Download, 375 }, 376 }, nil 377 } 378 379 // PutUnchecked uploads the object 380 // 381 // This will create a duplicate if we upload a new file without 382 // checking to see if there is one already - use Put() for that. 383 func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 384 return f.putUnchecked(ctx, in, src.Remote(), src.Size(), options...) 385 } 386 387 // Mkdir makes the directory (container, bucket) 388 // 389 // Shouldn't return an error if it already exists 390 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 391 err := f.dirCache.FindRoot(ctx, true) 392 if err != nil { 393 return err 394 } 395 if dir != "" { 396 _, err = f.dirCache.FindDir(ctx, dir, true) 397 } 398 return err 399 } 400 401 // Rmdir removes the directory (container, bucket) if empty 402 // 403 // Return an error if it doesn't exist or isn't empty 404 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 405 err := f.dirCache.FindRoot(ctx, false) 406 if err != nil { 407 return err 408 } 409 410 directoryID, err := f.dirCache.FindDir(ctx, dir, false) 411 if err != nil { 412 return err 413 } 414 415 folderID, err := strconv.Atoi(directoryID) 416 if err != nil { 417 return err 418 } 419 420 _, err = f.removeFolder(ctx, dir, folderID) 421 if err != nil { 422 return err 423 } 424 425 f.dirCache.FlushDir(dir) 426 427 return nil 428 } 429 430 // Check the interfaces are satisfied 431 var ( 432 _ fs.Fs = (*Fs)(nil) 433 _ fs.PutUncheckeder = (*Fs)(nil) 434 _ dircache.DirCacher = (*Fs)(nil) 435 )