github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/linkbox/linkbox.go (about) 1 // Package linkbox provides an interface to the linkbox.to Cloud storage system. 2 // 3 // API docs: https://www.linkbox.to/api-docs 4 package linkbox 5 6 /* 7 Extras 8 - PublicLink - NO - sharing doesn't share the actual file, only a page with it on 9 - Move - YES - have Move and Rename file APIs so is possible 10 - MoveDir - NO - probably not possible - have Move but no Rename 11 */ 12 13 import ( 14 "bytes" 15 "context" 16 "crypto/md5" 17 "fmt" 18 "io" 19 "net/http" 20 "net/url" 21 "path" 22 "regexp" 23 "strconv" 24 "strings" 25 "time" 26 27 "github.com/rclone/rclone/fs" 28 "github.com/rclone/rclone/fs/config/configmap" 29 "github.com/rclone/rclone/fs/config/configstruct" 30 "github.com/rclone/rclone/fs/fserrors" 31 "github.com/rclone/rclone/fs/fshttp" 32 "github.com/rclone/rclone/fs/hash" 33 "github.com/rclone/rclone/lib/dircache" 34 "github.com/rclone/rclone/lib/pacer" 35 "github.com/rclone/rclone/lib/rest" 36 ) 37 38 const ( 39 maxEntitiesPerPage = 1000 40 minSleep = 200 * time.Millisecond 41 maxSleep = 2 * time.Second 42 pacerBurst = 1 43 linkboxAPIURL = "https://www.linkbox.to/api/open/" 44 rootID = "0" // ID of root directory 45 ) 46 47 func init() { 48 fsi := &fs.RegInfo{ 49 Name: "linkbox", 50 Description: "Linkbox", 51 NewFs: NewFs, 52 Options: []fs.Option{{ 53 Name: "token", 54 Help: "Token from https://www.linkbox.to/admin/account", 55 Sensitive: true, 56 Required: true, 57 }}, 58 } 59 fs.Register(fsi) 60 } 61 62 // Options defines the configuration for this backend 63 type Options struct { 64 Token string `config:"token"` 65 } 66 67 // Fs stores the interface to the remote Linkbox files 68 type Fs struct { 69 name string 70 root string 71 opt Options // options for this backend 72 features *fs.Features // optional features 73 ci *fs.ConfigInfo // global config 74 srv *rest.Client // the connection to the server 75 dirCache *dircache.DirCache // Map of directory path to directory id 76 pacer *fs.Pacer 77 } 78 79 // Object is a remote object that has been stat'd (so it exists, but is not necessarily open for reading) 80 type Object struct { 81 fs *Fs 82 remote string 83 size int64 84 modTime time.Time 85 contentType string 86 fullURL string 87 dirID int64 88 itemID string // and these IDs are for files 89 id int64 // these IDs appear to apply to directories 90 isDir bool 91 } 92 93 // NewFs creates a new Fs object from the name and root. It connects to 94 // the host specified in the config file. 95 func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) { 96 root = strings.Trim(root, "/") 97 // Parse config into Options struct 98 99 opt := new(Options) 100 err := configstruct.Set(m, opt) 101 if err != nil { 102 return nil, err 103 } 104 105 ci := fs.GetConfig(ctx) 106 107 f := &Fs{ 108 name: name, 109 opt: *opt, 110 root: root, 111 ci: ci, 112 srv: rest.NewClient(fshttp.NewClient(ctx)), 113 114 pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep))), 115 } 116 f.dirCache = dircache.New(root, rootID, f) 117 118 f.features = (&fs.Features{ 119 CanHaveEmptyDirectories: true, 120 CaseInsensitive: true, 121 }).Fill(ctx, f) 122 123 // Find the current root 124 err = f.dirCache.FindRoot(ctx, false) 125 if err != nil { 126 // Assume it is a file 127 newRoot, remote := dircache.SplitPath(root) 128 tempF := *f 129 tempF.dirCache = dircache.New(newRoot, rootID, &tempF) 130 tempF.root = newRoot 131 // Make new Fs which is the parent 132 err = tempF.dirCache.FindRoot(ctx, false) 133 if err != nil { 134 // No root so return old f 135 return f, nil 136 } 137 _, err := tempF.NewObject(ctx, remote) 138 if err != nil { 139 if err == fs.ErrorObjectNotFound { 140 // File doesn't exist so return old f 141 return f, nil 142 } 143 return nil, err 144 } 145 f.features.Fill(ctx, &tempF) 146 // XXX: update the old f here instead of returning tempF, since 147 // `features` were already filled with functions having *f as a receiver. 148 // See https://github.com/rclone/rclone/issues/2182 149 f.dirCache = tempF.dirCache 150 f.root = tempF.root 151 // return an error with an fs which points to the parent 152 return f, fs.ErrorIsFile 153 } 154 return f, nil 155 } 156 157 type entity struct { 158 Type string `json:"type"` 159 Name string `json:"name"` 160 URL string `json:"url"` 161 Ctime int64 `json:"ctime"` 162 Size int64 `json:"size"` 163 ID int64 `json:"id"` 164 Pid int64 `json:"pid"` 165 ItemID string `json:"item_id"` 166 } 167 168 // Return true if the entity is a directory 169 func (e *entity) isDir() bool { 170 return e.Type == "dir" || e.Type == "sdir" 171 } 172 173 type data struct { 174 Entities []entity `json:"list"` 175 } 176 type fileSearchRes struct { 177 response 178 SearchData data `json:"data"` 179 } 180 181 // Set an object info from an entity 182 func (o *Object) set(e *entity) { 183 o.modTime = time.Unix(e.Ctime, 0) 184 o.contentType = e.Type 185 o.size = e.Size 186 o.fullURL = e.URL 187 o.isDir = e.isDir() 188 o.id = e.ID 189 o.itemID = e.ItemID 190 o.dirID = e.Pid 191 } 192 193 // Call linkbox with the query in opts and return result 194 // 195 // This will be checked for error and an error will be returned if Status != 1 196 func getUnmarshaledResponse(ctx context.Context, f *Fs, opts *rest.Opts, result interface{}) error { 197 err := f.pacer.Call(func() (bool, error) { 198 resp, err := f.srv.CallJSON(ctx, opts, nil, &result) 199 return f.shouldRetry(ctx, resp, err) 200 }) 201 if err != nil { 202 return err 203 } 204 responser := result.(responser) 205 if responser.IsError() { 206 return responser 207 } 208 return nil 209 } 210 211 // list the objects into the function supplied 212 // 213 // If directories is set it only sends directories 214 // User function to process a File item from listAll 215 // 216 // Should return true to finish processing 217 type listAllFn func(*entity) bool 218 219 // Search is a bit fussy about which characters match 220 // 221 // If the name doesn't match this then do an dir list instead 222 // N.B.: Linkbox doesn't support search by name that is longer than 50 chars 223 var searchOK = regexp.MustCompile(`^[a-zA-Z0-9_ -.]{1,50}$`) 224 225 // Lists the directory required calling the user function on each item found 226 // 227 // If the user fn ever returns true then it early exits with found = true 228 // 229 // If you set name then search ignores dirID. name is a substring 230 // search also so name="dir" matches "sub dir" also. This filters it 231 // down so it only returns items in dirID 232 func (f *Fs) listAll(ctx context.Context, dirID string, name string, fn listAllFn) (found bool, err error) { 233 var ( 234 pageNumber = 0 235 numberOfEntities = maxEntitiesPerPage 236 ) 237 name = strings.TrimSpace(name) // search doesn't like spaces 238 if !searchOK.MatchString(name) { 239 // If name isn't good then do an unbounded search 240 name = "" 241 } 242 243 OUTER: 244 for numberOfEntities == maxEntitiesPerPage { 245 pageNumber++ 246 opts := &rest.Opts{ 247 Method: "GET", 248 RootURL: linkboxAPIURL, 249 Path: "file_search", 250 Parameters: url.Values{ 251 "token": {f.opt.Token}, 252 "name": {name}, 253 "pid": {dirID}, 254 "pageNo": {itoa(pageNumber)}, 255 "pageSize": {itoa64(maxEntitiesPerPage)}, 256 }, 257 } 258 259 var responseResult fileSearchRes 260 err = getUnmarshaledResponse(ctx, f, opts, &responseResult) 261 if err != nil { 262 return false, fmt.Errorf("getting files failed: %w", err) 263 } 264 265 numberOfEntities = len(responseResult.SearchData.Entities) 266 267 for _, entity := range responseResult.SearchData.Entities { 268 if itoa64(entity.Pid) != dirID { 269 // when name != "" this returns from all directories, so ignore not this one 270 continue 271 } 272 if fn(&entity) { 273 found = true 274 break OUTER 275 } 276 } 277 if pageNumber > 100000 { 278 return false, fmt.Errorf("too many results") 279 } 280 } 281 return found, nil 282 } 283 284 // Turn 64 bit int to string 285 func itoa64(i int64) string { 286 return strconv.FormatInt(i, 10) 287 } 288 289 // Turn int to string 290 func itoa(i int) string { 291 return itoa64(int64(i)) 292 } 293 294 func splitDirAndName(remote string) (dir string, name string) { 295 lastSlashPosition := strings.LastIndex(remote, "/") 296 if lastSlashPosition == -1 { 297 dir = "" 298 name = remote 299 } else { 300 dir = remote[:lastSlashPosition] 301 name = remote[lastSlashPosition+1:] 302 } 303 304 // fs.Debugf(nil, "splitDirAndName remote = {%s}, dir = {%s}, name = {%s}", remote, dir, name) 305 306 return dir, name 307 } 308 309 // FindLeaf finds a directory of name leaf in the folder with ID directoryID 310 func (f *Fs) FindLeaf(ctx context.Context, directoryID, leaf string) (directoryIDOut string, found bool, err error) { 311 // Find the leaf in directoryID 312 found, err = f.listAll(ctx, directoryID, leaf, func(entity *entity) bool { 313 if entity.isDir() && strings.EqualFold(entity.Name, leaf) { 314 directoryIDOut = itoa64(entity.ID) 315 return true 316 } 317 return false 318 }) 319 return directoryIDOut, found, err 320 } 321 322 // Returned from "folder_create" 323 type folderCreateRes struct { 324 response 325 Data struct { 326 DirID int64 `json:"dirId"` 327 } `json:"data"` 328 } 329 330 // CreateDir makes a directory with dirID as parent and name leaf 331 func (f *Fs) CreateDir(ctx context.Context, dirID, leaf string) (newID string, err error) { 332 // fs.Debugf(f, "CreateDir(%q, %q)\n", dirID, leaf) 333 opts := &rest.Opts{ 334 Method: "GET", 335 RootURL: linkboxAPIURL, 336 Path: "folder_create", 337 Parameters: url.Values{ 338 "token": {f.opt.Token}, 339 "name": {leaf}, 340 "pid": {dirID}, 341 "isShare": {"0"}, 342 "canInvite": {"1"}, 343 "canShare": {"1"}, 344 "withBodyImg": {"1"}, 345 "desc": {""}, 346 }, 347 } 348 349 response := folderCreateRes{} 350 err = getUnmarshaledResponse(ctx, f, opts, &response) 351 if err != nil { 352 // response status 1501 means that directory already exists 353 if response.Status == 1501 { 354 return newID, fmt.Errorf("couldn't find already created directory: %w", fs.ErrorDirNotFound) 355 } 356 return newID, fmt.Errorf("CreateDir failed: %w", err) 357 358 } 359 if response.Data.DirID == 0 { 360 return newID, fmt.Errorf("API returned 0 for ID of newly created directory") 361 } 362 return itoa64(response.Data.DirID), nil 363 } 364 365 // List the objects and directories in dir into entries. The 366 // entries can be returned in any order but should be for a 367 // complete directory. 368 // 369 // dir should be "" to list the root, and should not have 370 // trailing slashes. 371 // 372 // This should return ErrDirNotFound if the directory isn't 373 // found. 374 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 375 // fs.Debugf(f, "List method dir = {%s}", dir) 376 directoryID, err := f.dirCache.FindDir(ctx, dir, false) 377 if err != nil { 378 return nil, err 379 } 380 _, err = f.listAll(ctx, directoryID, "", func(entity *entity) bool { 381 remote := path.Join(dir, entity.Name) 382 if entity.isDir() { 383 id := itoa64(entity.ID) 384 modTime := time.Unix(entity.Ctime, 0) 385 d := fs.NewDir(remote, modTime).SetID(id).SetParentID(itoa64(entity.Pid)) 386 entries = append(entries, d) 387 // cache the directory ID for later lookups 388 f.dirCache.Put(remote, id) 389 } else { 390 o := &Object{ 391 fs: f, 392 remote: remote, 393 } 394 o.set(entity) 395 entries = append(entries, o) 396 } 397 return false 398 }) 399 if err != nil { 400 return nil, err 401 } 402 return entries, nil 403 } 404 405 // get an entity with leaf from dirID 406 func getEntity(ctx context.Context, f *Fs, leaf string, directoryID string, token string) (*entity, error) { 407 var result *entity 408 var resultErr = fs.ErrorObjectNotFound 409 _, err := f.listAll(ctx, directoryID, leaf, func(entity *entity) bool { 410 if strings.EqualFold(entity.Name, leaf) { 411 // fs.Debugf(f, "getObject found entity.Name {%s} name {%s}", entity.Name, name) 412 if entity.isDir() { 413 result = nil 414 resultErr = fs.ErrorIsDir 415 } else { 416 result = entity 417 resultErr = nil 418 } 419 return true 420 } 421 return false 422 }) 423 if err != nil { 424 return nil, err 425 } 426 return result, resultErr 427 } 428 429 // NewObject finds the Object at remote. If it can't be found 430 // it returns the error ErrorObjectNotFound. 431 // 432 // If remote points to a directory then it should return 433 // ErrorIsDir if possible without doing any extra work, 434 // otherwise ErrorObjectNotFound. 435 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 436 leaf, dirID, err := f.dirCache.FindPath(ctx, remote, false) 437 if err != nil { 438 if err == fs.ErrorDirNotFound { 439 return nil, fs.ErrorObjectNotFound 440 } 441 return nil, err 442 } 443 444 entity, err := getEntity(ctx, f, leaf, dirID, f.opt.Token) 445 if err != nil { 446 return nil, err 447 } 448 o := &Object{ 449 fs: f, 450 remote: remote, 451 } 452 o.set(entity) 453 return o, nil 454 } 455 456 // Mkdir makes the directory (container, bucket) 457 // 458 // Shouldn't return an error if it already exists 459 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 460 _, err := f.dirCache.FindDir(ctx, dir, true) 461 return err 462 } 463 464 func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error { 465 if check { 466 entries, err := f.List(ctx, dir) 467 if err != nil { 468 return err 469 } 470 if len(entries) != 0 { 471 return fs.ErrorDirectoryNotEmpty 472 } 473 } 474 475 directoryID, err := f.dirCache.FindDir(ctx, dir, false) 476 if err != nil { 477 return err 478 } 479 opts := &rest.Opts{ 480 Method: "GET", 481 RootURL: linkboxAPIURL, 482 Path: "folder_del", 483 Parameters: url.Values{ 484 "token": {f.opt.Token}, 485 "dirIds": {directoryID}, 486 }, 487 } 488 489 response := response{} 490 err = getUnmarshaledResponse(ctx, f, opts, &response) 491 if err != nil { 492 // Linkbox has some odd error returns here 493 if response.Status == 403 || response.Status == 500 { 494 return fs.ErrorDirNotFound 495 } 496 return fmt.Errorf("purge error: %w", err) 497 } 498 499 f.dirCache.FlushDir(dir) 500 if err != nil { 501 return err 502 } 503 return nil 504 } 505 506 // Rmdir removes the directory (container, bucket) if empty 507 // 508 // Return an error if it doesn't exist or isn't empty 509 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 510 return f.purgeCheck(ctx, dir, true) 511 } 512 513 // SetModTime sets modTime on a particular file 514 func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error { 515 return fs.ErrorCantSetModTime 516 } 517 518 // Open opens the file for read. Call Close() on the returned io.ReadCloser 519 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) { 520 var res *http.Response 521 downloadURL := o.fullURL 522 if downloadURL == "" { 523 _, name := splitDirAndName(o.Remote()) 524 newObject, err := getEntity(ctx, o.fs, name, itoa64(o.dirID), o.fs.opt.Token) 525 if err != nil { 526 return nil, err 527 } 528 if newObject == nil { 529 // fs.Debugf(o.fs, "Open entity is empty: name = {%s}", name) 530 return nil, fs.ErrorObjectNotFound 531 } 532 533 downloadURL = newObject.URL 534 } 535 536 opts := &rest.Opts{ 537 Method: "GET", 538 RootURL: downloadURL, 539 Options: options, 540 } 541 542 err := o.fs.pacer.Call(func() (bool, error) { 543 var err error 544 res, err = o.fs.srv.Call(ctx, opts) 545 return o.fs.shouldRetry(ctx, res, err) 546 }) 547 548 if err != nil { 549 return nil, fmt.Errorf("Open failed: %w", err) 550 } 551 552 return res.Body, nil 553 } 554 555 // Update in to the object with the modTime given of the given size 556 // 557 // When called from outside an Fs by rclone, src.Size() will always be >= 0. 558 // But for unknown-sized objects (indicated by src.Size() == -1), Upload should either 559 // return an error or update the object properly (rather than e.g. calling panic). 560 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) { 561 size := src.Size() 562 if size == 0 { 563 return fs.ErrorCantUploadEmptyFiles 564 } else if size < 0 { 565 return fmt.Errorf("can't upload files of unknown length") 566 } 567 568 remote := o.Remote() 569 570 // remove the file if it exists 571 if o.itemID != "" { 572 fs.Debugf(o, "Update: removing old file") 573 err = o.Remove(ctx) 574 if err != nil { 575 fs.Errorf(o, "Update: failed to remove existing file: %v", err) 576 } 577 o.itemID = "" 578 } else { 579 tmpObject, err := o.fs.NewObject(ctx, remote) 580 if err == nil { 581 fs.Debugf(o, "Update: removing old file") 582 err = tmpObject.Remove(ctx) 583 if err != nil { 584 fs.Errorf(o, "Update: failed to remove existing file: %v", err) 585 } 586 } 587 } 588 589 first10m := io.LimitReader(in, 10_485_760) 590 first10mBytes, err := io.ReadAll(first10m) 591 if err != nil { 592 return fmt.Errorf("Update err in reading file: %w", err) 593 } 594 595 // get upload authorization (step 1) 596 opts := &rest.Opts{ 597 Method: "GET", 598 RootURL: linkboxAPIURL, 599 Path: "get_upload_url", 600 Options: options, 601 Parameters: url.Values{ 602 "token": {o.fs.opt.Token}, 603 "fileMd5ofPre10m": {fmt.Sprintf("%x", md5.Sum(first10mBytes))}, 604 "fileSize": {itoa64(size)}, 605 }, 606 } 607 608 getFirstStepResult := getUploadURLResponse{} 609 err = getUnmarshaledResponse(ctx, o.fs, opts, &getFirstStepResult) 610 if err != nil { 611 if getFirstStepResult.Status != 600 { 612 return fmt.Errorf("Update err in unmarshaling response: %w", err) 613 } 614 } 615 616 switch getFirstStepResult.Status { 617 case 1: 618 // upload file using link from first step 619 var res *http.Response 620 621 file := io.MultiReader(bytes.NewReader(first10mBytes), in) 622 623 opts := &rest.Opts{ 624 Method: "PUT", 625 RootURL: getFirstStepResult.Data.SignURL, 626 Options: options, 627 Body: file, 628 ContentLength: &size, 629 } 630 631 err = o.fs.pacer.CallNoRetry(func() (bool, error) { 632 res, err = o.fs.srv.Call(ctx, opts) 633 return o.fs.shouldRetry(ctx, res, err) 634 }) 635 636 if err != nil { 637 return fmt.Errorf("update err in uploading file: %w", err) 638 } 639 640 _, err = io.ReadAll(res.Body) 641 if err != nil { 642 return fmt.Errorf("update err in reading response: %w", err) 643 } 644 645 case 600: 646 // Status means that we don't need to upload file 647 // We need only to make second step 648 default: 649 return fmt.Errorf("got unexpected message from Linkbox: %s", getFirstStepResult.Message) 650 } 651 652 leaf, dirID, err := o.fs.dirCache.FindPath(ctx, remote, false) 653 if err != nil { 654 return err 655 } 656 657 // create file item at Linkbox (second step) 658 opts = &rest.Opts{ 659 Method: "GET", 660 RootURL: linkboxAPIURL, 661 Path: "folder_upload_file", 662 Options: options, 663 Parameters: url.Values{ 664 "token": {o.fs.opt.Token}, 665 "fileMd5ofPre10m": {fmt.Sprintf("%x", md5.Sum(first10mBytes))}, 666 "fileSize": {itoa64(size)}, 667 "pid": {dirID}, 668 "diyName": {leaf}, 669 }, 670 } 671 672 getSecondStepResult := getUploadURLResponse{} 673 err = getUnmarshaledResponse(ctx, o.fs, opts, &getSecondStepResult) 674 if err != nil { 675 return fmt.Errorf("Update second step failed: %w", err) 676 } 677 678 // Try a few times to read the object after upload for eventual consistency 679 const maxTries = 10 680 var sleepTime = 100 * time.Millisecond 681 var entity *entity 682 for try := 1; try <= maxTries; try++ { 683 entity, err = getEntity(ctx, o.fs, leaf, dirID, o.fs.opt.Token) 684 if err == nil { 685 break 686 } 687 if err != fs.ErrorObjectNotFound { 688 return fmt.Errorf("Update failed to read object: %w", err) 689 } 690 fs.Debugf(o, "Trying to read object after upload: try again in %v (%d/%d)", sleepTime, try, maxTries) 691 time.Sleep(sleepTime) 692 sleepTime *= 2 693 } 694 if err != nil { 695 return err 696 } 697 o.set(entity) 698 return nil 699 } 700 701 // Remove this object 702 func (o *Object) Remove(ctx context.Context) error { 703 opts := &rest.Opts{ 704 Method: "GET", 705 RootURL: linkboxAPIURL, 706 Path: "file_del", 707 Parameters: url.Values{ 708 "token": {o.fs.opt.Token}, 709 "itemIds": {o.itemID}, 710 }, 711 } 712 requestResult := getUploadURLResponse{} 713 err := getUnmarshaledResponse(ctx, o.fs, opts, &requestResult) 714 if err != nil { 715 return fmt.Errorf("could not Remove: %w", err) 716 717 } 718 return nil 719 } 720 721 // ModTime returns the modification time of the remote http file 722 func (o *Object) ModTime(ctx context.Context) time.Time { 723 return o.modTime 724 } 725 726 // Remote the name of the remote HTTP file, relative to the fs root 727 func (o *Object) Remote() string { 728 return o.remote 729 } 730 731 // Size returns the size in bytes of the remote http file 732 func (o *Object) Size() int64 { 733 return o.size 734 } 735 736 // String returns the URL to the remote HTTP file 737 func (o *Object) String() string { 738 if o == nil { 739 return "<nil>" 740 } 741 return o.remote 742 } 743 744 // Fs is the filesystem this remote http file object is located within 745 func (o *Object) Fs() fs.Info { 746 return o.fs 747 } 748 749 // Hash returns "" since HTTP (in Go or OpenSSH) doesn't support remote calculation of hashes 750 func (o *Object) Hash(ctx context.Context, r hash.Type) (string, error) { 751 return "", hash.ErrUnsupported 752 } 753 754 // Storable returns whether the remote http file is a regular file 755 // (not a directory, symbolic link, block device, character device, named pipe, etc.) 756 func (o *Object) Storable() bool { 757 return true 758 } 759 760 // Features returns the optional features of this Fs 761 // Info provides a read only interface to information about a filesystem. 762 func (f *Fs) Features() *fs.Features { 763 return f.features 764 } 765 766 // Name of the remote (as passed into NewFs) 767 // Name returns the configured name of the file system 768 func (f *Fs) Name() string { 769 return f.name 770 } 771 772 // Root of the remote (as passed into NewFs) 773 func (f *Fs) Root() string { 774 return f.root 775 } 776 777 // String returns a description of the FS 778 func (f *Fs) String() string { 779 return fmt.Sprintf("Linkbox root '%s'", f.root) 780 } 781 782 // Precision of the ModTimes in this Fs 783 func (f *Fs) Precision() time.Duration { 784 return fs.ModTimeNotSupported 785 } 786 787 // Hashes returns hash.HashNone to indicate remote hashing is unavailable 788 // Returns the supported hash types of the filesystem 789 func (f *Fs) Hashes() hash.Set { 790 return hash.Set(hash.None) 791 } 792 793 /* 794 { 795 "data": { 796 "signUrl": "http://xx -- Then CURL PUT your file with sign url " 797 }, 798 "msg": "please use this url to upload (PUT method)", 799 "status": 1 800 } 801 */ 802 803 // All messages have these items 804 type response struct { 805 Message string `json:"msg"` 806 Status int `json:"status"` 807 } 808 809 // IsError returns whether response represents an error 810 func (r *response) IsError() bool { 811 return r.Status != 1 812 } 813 814 // Error returns the error state of this response 815 func (r *response) Error() string { 816 return fmt.Sprintf("Linkbox error %d: %s", r.Status, r.Message) 817 } 818 819 // responser is interface covering the response so we can use it when it is embedded. 820 type responser interface { 821 IsError() bool 822 Error() string 823 } 824 825 type getUploadURLData struct { 826 SignURL string `json:"signUrl"` 827 } 828 829 type getUploadURLResponse struct { 830 response 831 Data getUploadURLData `json:"data"` 832 } 833 834 // Put in to the remote path with the modTime given of the given size 835 // 836 // When called from outside an Fs by rclone, src.Size() will always be >= 0. 837 // But for unknown-sized objects (indicated by src.Size() == -1), Put should either 838 // return an error or upload it properly (rather than e.g. calling panic). 839 // 840 // May create the object even if it returns an error - if so 841 // will return the object and the error, otherwise will return 842 // nil and the error 843 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 844 o := &Object{ 845 fs: f, 846 remote: src.Remote(), 847 size: src.Size(), 848 } 849 dir, _ := splitDirAndName(src.Remote()) 850 err := f.Mkdir(ctx, dir) 851 if err != nil { 852 return nil, err 853 } 854 err = o.Update(ctx, in, src, options...) 855 return o, err 856 } 857 858 // Purge all files in the directory specified 859 // 860 // Implement this if you have a way of deleting all the files 861 // quicker than just running Remove() on the result of List() 862 // 863 // Return an error if it doesn't exist 864 func (f *Fs) Purge(ctx context.Context, dir string) error { 865 return f.purgeCheck(ctx, dir, false) 866 } 867 868 // retryErrorCodes is a slice of error codes that we will retry 869 var retryErrorCodes = []int{ 870 429, // Too Many Requests. 871 500, // Internal Server Error 872 502, // Bad Gateway 873 503, // Service Unavailable 874 504, // Gateway Timeout 875 509, // Bandwidth Limit Exceeded 876 } 877 878 // shouldRetry determines whether a given err rates being retried 879 func (f *Fs) shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) { 880 if fserrors.ContextError(ctx, &err) { 881 return false, err 882 } 883 return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err 884 } 885 886 // DirCacheFlush resets the directory cache - used in testing as an 887 // optional interface 888 func (f *Fs) DirCacheFlush() { 889 f.dirCache.ResetRoot() 890 } 891 892 // Check the interfaces are satisfied 893 var ( 894 _ fs.Fs = &Fs{} 895 _ fs.Purger = &Fs{} 896 _ fs.DirCacheFlusher = &Fs{} 897 _ fs.Object = &Object{} 898 )