github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/backend/box/box.go (about) 1 // Package box provides an interface to the Box 2 // object storage system. 3 package box 4 5 // FIXME Box only supports file names of 255 characters or less. Names 6 // that will not be supported are those that contain non-printable 7 // ascii, / or \, names with trailing spaces, and the special names 8 // “.” and “..”. 9 10 // FIXME box can copy a directory 11 12 import ( 13 "context" 14 "crypto/rsa" 15 "encoding/json" 16 "encoding/pem" 17 "fmt" 18 "io" 19 "io/ioutil" 20 "log" 21 "net/http" 22 "net/url" 23 "path" 24 "strconv" 25 "strings" 26 "time" 27 28 "github.com/rclone/rclone/lib/encoder" 29 "github.com/rclone/rclone/lib/jwtutil" 30 31 "github.com/youmark/pkcs8" 32 33 "github.com/pkg/errors" 34 "github.com/rclone/rclone/backend/box/api" 35 "github.com/rclone/rclone/fs" 36 "github.com/rclone/rclone/fs/config" 37 "github.com/rclone/rclone/fs/config/configmap" 38 "github.com/rclone/rclone/fs/config/configstruct" 39 "github.com/rclone/rclone/fs/config/obscure" 40 "github.com/rclone/rclone/fs/fserrors" 41 "github.com/rclone/rclone/fs/fshttp" 42 "github.com/rclone/rclone/fs/hash" 43 "github.com/rclone/rclone/lib/dircache" 44 "github.com/rclone/rclone/lib/oauthutil" 45 "github.com/rclone/rclone/lib/pacer" 46 "github.com/rclone/rclone/lib/rest" 47 "golang.org/x/oauth2" 48 "golang.org/x/oauth2/jws" 49 ) 50 51 const ( 52 rcloneClientID = "d0374ba6pgmaguie02ge15sv1mllndho" 53 rcloneEncryptedClientSecret = "sYbJYm99WB8jzeaLPU0OPDMJKIkZvD2qOn3SyEMfiJr03RdtDt3xcZEIudRhbIDL" 54 minSleep = 10 * time.Millisecond 55 maxSleep = 2 * time.Second 56 decayConstant = 2 // bigger for slower decay, exponential 57 rootURL = "https://api.box.com/2.0" 58 uploadURL = "https://upload.box.com/api/2.0" 59 listChunks = 1000 // chunk size to read directory listings 60 minUploadCutoff = 50000000 // upload cutoff can be no lower than this 61 defaultUploadCutoff = 50 * 1024 * 1024 62 tokenURL = "https://api.box.com/oauth2/token" 63 ) 64 65 // Globals 66 var ( 67 // Description of how to auth for this app 68 oauthConfig = &oauth2.Config{ 69 Scopes: nil, 70 Endpoint: oauth2.Endpoint{ 71 AuthURL: "https://app.box.com/api/oauth2/authorize", 72 TokenURL: "https://app.box.com/api/oauth2/token", 73 }, 74 ClientID: rcloneClientID, 75 ClientSecret: obscure.MustReveal(rcloneEncryptedClientSecret), 76 RedirectURL: oauthutil.RedirectURL, 77 } 78 ) 79 80 // Register with Fs 81 func init() { 82 fs.Register(&fs.RegInfo{ 83 Name: "box", 84 Description: "Box", 85 NewFs: NewFs, 86 Config: func(name string, m configmap.Mapper) { 87 jsonFile, ok := m.Get("box_config_file") 88 boxSubType, boxSubTypeOk := m.Get("box_sub_type") 89 var err error 90 if ok && boxSubTypeOk && jsonFile != "" && boxSubType != "" { 91 err = refreshJWTToken(jsonFile, boxSubType, name, m) 92 if err != nil { 93 log.Fatalf("Failed to configure token with jwt authentication: %v", err) 94 } 95 } else { 96 err = oauthutil.Config("box", name, m, oauthConfig, nil) 97 if err != nil { 98 log.Fatalf("Failed to configure token with oauth authentication: %v", err) 99 } 100 } 101 }, 102 Options: []fs.Option{{ 103 Name: config.ConfigClientID, 104 Help: "Box App Client Id.\nLeave blank normally.", 105 }, { 106 Name: config.ConfigClientSecret, 107 Help: "Box App Client Secret\nLeave blank normally.", 108 }, { 109 Name: "root_folder_id", 110 Help: "Fill in for rclone to use a non root folder as its starting point.", 111 Default: "0", 112 Advanced: true, 113 }, { 114 Name: "box_config_file", 115 Help: "Box App config.json location\nLeave blank normally.", 116 }, { 117 Name: "box_sub_type", 118 Default: "user", 119 Examples: []fs.OptionExample{{ 120 Value: "user", 121 Help: "Rclone should act on behalf of a user", 122 }, { 123 Value: "enterprise", 124 Help: "Rclone should act on behalf of a service account", 125 }}, 126 }, { 127 Name: "upload_cutoff", 128 Help: "Cutoff for switching to multipart upload (>= 50MB).", 129 Default: fs.SizeSuffix(defaultUploadCutoff), 130 Advanced: true, 131 }, { 132 Name: "commit_retries", 133 Help: "Max number of times to try committing a multipart file.", 134 Default: 100, 135 Advanced: true, 136 }, { 137 Name: config.ConfigEncoding, 138 Help: config.ConfigEncodingHelp, 139 Advanced: true, 140 // From https://developer.box.com/docs/error-codes#section-400-bad-request : 141 // > Box only supports file or folder names that are 255 characters or less. 142 // > File names containing non-printable ascii, "/" or "\", names with leading 143 // > or trailing spaces, and the special names “.” and “..” are also unsupported. 144 // 145 // Testing revealed names with leading spaces work fine. 146 // Also encode invalid UTF-8 bytes as json doesn't handle them properly. 147 Default: (encoder.Display | 148 encoder.EncodeBackSlash | 149 encoder.EncodeRightSpace | 150 encoder.EncodeInvalidUtf8), 151 }}, 152 }) 153 } 154 155 func refreshJWTToken(jsonFile string, boxSubType string, name string, m configmap.Mapper) error { 156 boxConfig, err := getBoxConfig(jsonFile) 157 if err != nil { 158 log.Fatalf("Failed to configure token: %v", err) 159 } 160 privateKey, err := getDecryptedPrivateKey(boxConfig) 161 if err != nil { 162 log.Fatalf("Failed to configure token: %v", err) 163 } 164 claims, err := getClaims(boxConfig, boxSubType) 165 if err != nil { 166 log.Fatalf("Failed to configure token: %v", err) 167 } 168 signingHeaders := getSigningHeaders(boxConfig) 169 queryParams := getQueryParams(boxConfig) 170 client := fshttp.NewClient(fs.Config) 171 err = jwtutil.Config("box", name, claims, signingHeaders, queryParams, privateKey, m, client) 172 return err 173 } 174 175 func getBoxConfig(configFile string) (boxConfig *api.ConfigJSON, err error) { 176 file, err := ioutil.ReadFile(configFile) 177 if err != nil { 178 return nil, errors.Wrap(err, "box: failed to read Box config") 179 } 180 err = json.Unmarshal(file, &boxConfig) 181 if err != nil { 182 return nil, errors.Wrap(err, "box: failed to parse Box config") 183 } 184 return boxConfig, nil 185 } 186 187 func getClaims(boxConfig *api.ConfigJSON, boxSubType string) (claims *jws.ClaimSet, err error) { 188 val, err := jwtutil.RandomHex(20) 189 if err != nil { 190 return nil, errors.Wrap(err, "box: failed to generate random string for jti") 191 } 192 193 claims = &jws.ClaimSet{ 194 Iss: boxConfig.BoxAppSettings.ClientID, 195 Sub: boxConfig.EnterpriseID, 196 Aud: tokenURL, 197 Exp: time.Now().Add(time.Second * 45).Unix(), 198 PrivateClaims: map[string]interface{}{ 199 "box_sub_type": boxSubType, 200 "aud": tokenURL, 201 "jti": val, 202 }, 203 } 204 205 return claims, nil 206 } 207 208 func getSigningHeaders(boxConfig *api.ConfigJSON) *jws.Header { 209 signingHeaders := &jws.Header{ 210 Algorithm: "RS256", 211 Typ: "JWT", 212 KeyID: boxConfig.BoxAppSettings.AppAuth.PublicKeyID, 213 } 214 215 return signingHeaders 216 } 217 218 func getQueryParams(boxConfig *api.ConfigJSON) map[string]string { 219 queryParams := map[string]string{ 220 "client_id": boxConfig.BoxAppSettings.ClientID, 221 "client_secret": boxConfig.BoxAppSettings.ClientSecret, 222 } 223 224 return queryParams 225 } 226 227 func getDecryptedPrivateKey(boxConfig *api.ConfigJSON) (key *rsa.PrivateKey, err error) { 228 229 block, rest := pem.Decode([]byte(boxConfig.BoxAppSettings.AppAuth.PrivateKey)) 230 if len(rest) > 0 { 231 return nil, errors.Wrap(err, "box: extra data included in private key") 232 } 233 234 rsaKey, err := pkcs8.ParsePKCS8PrivateKey(block.Bytes, []byte(boxConfig.BoxAppSettings.AppAuth.Passphrase)) 235 if err != nil { 236 return nil, errors.Wrap(err, "box: failed to decrypt private key") 237 } 238 239 return rsaKey.(*rsa.PrivateKey), nil 240 } 241 242 // Options defines the configuration for this backend 243 type Options struct { 244 UploadCutoff fs.SizeSuffix `config:"upload_cutoff"` 245 CommitRetries int `config:"commit_retries"` 246 Enc encoder.MultiEncoder `config:"encoding"` 247 RootFolderID string `config:"root_folder_id"` 248 } 249 250 // Fs represents a remote box 251 type Fs struct { 252 name string // name of this remote 253 root string // the path we are working on 254 opt Options // parsed options 255 features *fs.Features // optional features 256 srv *rest.Client // the connection to the one drive server 257 dirCache *dircache.DirCache // Map of directory path to directory id 258 pacer *fs.Pacer // pacer for API calls 259 tokenRenewer *oauthutil.Renew // renew the token on expiry 260 uploadToken *pacer.TokenDispenser // control concurrency 261 } 262 263 // Object describes a box object 264 // 265 // Will definitely have info but maybe not meta 266 type Object struct { 267 fs *Fs // what this object is part of 268 remote string // The remote path 269 hasMetaData bool // whether info below has been set 270 size int64 // size of the object 271 modTime time.Time // modification time of the object 272 id string // ID of the object 273 publicLink string // Public Link for the object 274 sha1 string // SHA-1 of the object content 275 } 276 277 // ------------------------------------------------------------ 278 279 // Name of the remote (as passed into NewFs) 280 func (f *Fs) Name() string { 281 return f.name 282 } 283 284 // Root of the remote (as passed into NewFs) 285 func (f *Fs) Root() string { 286 return f.root 287 } 288 289 // String converts this Fs to a string 290 func (f *Fs) String() string { 291 return fmt.Sprintf("box root '%s'", f.root) 292 } 293 294 // Features returns the optional features of this Fs 295 func (f *Fs) Features() *fs.Features { 296 return f.features 297 } 298 299 // parsePath parses a box 'url' 300 func parsePath(path string) (root string) { 301 root = strings.Trim(path, "/") 302 return 303 } 304 305 // retryErrorCodes is a slice of error codes that we will retry 306 var retryErrorCodes = []int{ 307 429, // Too Many Requests. 308 500, // Internal Server Error 309 502, // Bad Gateway 310 503, // Service Unavailable 311 504, // Gateway Timeout 312 509, // Bandwidth Limit Exceeded 313 } 314 315 // shouldRetry returns a boolean as to whether this resp and err 316 // deserve to be retried. It returns the err as a convenience 317 func shouldRetry(resp *http.Response, err error) (bool, error) { 318 authRetry := false 319 320 if resp != nil && resp.StatusCode == 401 && len(resp.Header["Www-Authenticate"]) == 1 && strings.Index(resp.Header["Www-Authenticate"][0], "expired_token") >= 0 { 321 authRetry = true 322 fs.Debugf(nil, "Should retry: %v", err) 323 } 324 return authRetry || fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err 325 } 326 327 // readMetaDataForPath reads the metadata from the path 328 func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.Item, err error) { 329 // defer fs.Trace(f, "path=%q", path)("info=%+v, err=%v", &info, &err) 330 leaf, directoryID, err := f.dirCache.FindRootAndPath(ctx, path, false) 331 if err != nil { 332 if err == fs.ErrorDirNotFound { 333 return nil, fs.ErrorObjectNotFound 334 } 335 return nil, err 336 } 337 338 found, err := f.listAll(ctx, directoryID, false, true, func(item *api.Item) bool { 339 if item.Name == leaf { 340 info = item 341 return true 342 } 343 return false 344 }) 345 if err != nil { 346 return nil, err 347 } 348 if !found { 349 return nil, fs.ErrorObjectNotFound 350 } 351 return info, nil 352 } 353 354 // errorHandler parses a non 2xx error response into an error 355 func errorHandler(resp *http.Response) error { 356 // Decode error response 357 errResponse := new(api.Error) 358 err := rest.DecodeJSON(resp, &errResponse) 359 if err != nil { 360 fs.Debugf(nil, "Couldn't decode error response: %v", err) 361 } 362 if errResponse.Code == "" { 363 errResponse.Code = resp.Status 364 } 365 if errResponse.Status == 0 { 366 errResponse.Status = resp.StatusCode 367 } 368 return errResponse 369 } 370 371 // NewFs constructs an Fs from the path, container:path 372 func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) { 373 ctx := context.Background() 374 // Parse config into Options struct 375 opt := new(Options) 376 err := configstruct.Set(m, opt) 377 if err != nil { 378 return nil, err 379 } 380 381 if opt.UploadCutoff < minUploadCutoff { 382 return nil, errors.Errorf("box: upload cutoff (%v) must be greater than equal to %v", opt.UploadCutoff, fs.SizeSuffix(minUploadCutoff)) 383 } 384 385 root = parsePath(root) 386 oAuthClient, ts, err := oauthutil.NewClient(name, m, oauthConfig) 387 if err != nil { 388 return nil, errors.Wrap(err, "failed to configure Box") 389 } 390 391 f := &Fs{ 392 name: name, 393 root: root, 394 opt: *opt, 395 srv: rest.NewClient(oAuthClient).SetRoot(rootURL), 396 pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), 397 uploadToken: pacer.NewTokenDispenser(fs.Config.Transfers), 398 } 399 f.features = (&fs.Features{ 400 CaseInsensitive: true, 401 CanHaveEmptyDirectories: true, 402 }).Fill(f) 403 f.srv.SetErrorHandler(errorHandler) 404 405 jsonFile, ok := m.Get("box_config_file") 406 boxSubType, boxSubTypeOk := m.Get("box_sub_type") 407 408 // If using box config.json and JWT, renewing should just refresh the token and 409 // should do so whether there are uploads pending or not. 410 if ok && boxSubTypeOk && jsonFile != "" && boxSubType != "" { 411 f.tokenRenewer = oauthutil.NewRenew(f.String(), ts, func() error { 412 err := refreshJWTToken(jsonFile, boxSubType, name, m) 413 return err 414 }) 415 f.tokenRenewer.Start() 416 } else { 417 // Renew the token in the background 418 f.tokenRenewer = oauthutil.NewRenew(f.String(), ts, func() error { 419 _, err := f.readMetaDataForPath(ctx, "") 420 return err 421 }) 422 } 423 424 // Get rootFolderID 425 rootID := f.opt.RootFolderID 426 f.dirCache = dircache.New(root, rootID, f) 427 428 // Find the current root 429 err = f.dirCache.FindRoot(ctx, false) 430 if err != nil { 431 // Assume it is a file 432 newRoot, remote := dircache.SplitPath(root) 433 tempF := *f 434 tempF.dirCache = dircache.New(newRoot, rootID, &tempF) 435 tempF.root = newRoot 436 // Make new Fs which is the parent 437 err = tempF.dirCache.FindRoot(ctx, false) 438 if err != nil { 439 // No root so return old f 440 return f, nil 441 } 442 _, err := tempF.newObjectWithInfo(ctx, remote, nil) 443 if err != nil { 444 if err == fs.ErrorObjectNotFound { 445 // File doesn't exist so return old f 446 return f, nil 447 } 448 return nil, err 449 } 450 f.features.Fill(&tempF) 451 // XXX: update the old f here instead of returning tempF, since 452 // `features` were already filled with functions having *f as a receiver. 453 // See https://github.com/rclone/rclone/issues/2182 454 f.dirCache = tempF.dirCache 455 f.root = tempF.root 456 // return an error with an fs which points to the parent 457 return f, fs.ErrorIsFile 458 } 459 return f, nil 460 } 461 462 // rootSlash returns root with a slash on if it is empty, otherwise empty string 463 func (f *Fs) rootSlash() string { 464 if f.root == "" { 465 return f.root 466 } 467 return f.root + "/" 468 } 469 470 // Return an Object from a path 471 // 472 // If it can't be found it returns the error fs.ErrorObjectNotFound. 473 func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.Item) (fs.Object, error) { 474 o := &Object{ 475 fs: f, 476 remote: remote, 477 } 478 var err error 479 if info != nil { 480 // Set info 481 err = o.setMetaData(info) 482 } else { 483 err = o.readMetaData(ctx) // reads info and meta, returning an error 484 } 485 if err != nil { 486 return nil, err 487 } 488 return o, nil 489 } 490 491 // NewObject finds the Object at remote. If it can't be found 492 // it returns the error fs.ErrorObjectNotFound. 493 func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 494 return f.newObjectWithInfo(ctx, remote, nil) 495 } 496 497 // FindLeaf finds a directory of name leaf in the folder with ID pathID 498 func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) { 499 // Find the leaf in pathID 500 found, err = f.listAll(ctx, pathID, true, false, func(item *api.Item) bool { 501 if item.Name == leaf { 502 pathIDOut = item.ID 503 return true 504 } 505 return false 506 }) 507 return pathIDOut, found, err 508 } 509 510 // fieldsValue creates a url.Values with fields set to those in api.Item 511 func fieldsValue() url.Values { 512 values := url.Values{} 513 values.Set("fields", api.ItemFields) 514 return values 515 } 516 517 // CreateDir makes a directory with pathID as parent and name leaf 518 func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) { 519 // fs.Debugf(f, "CreateDir(%q, %q)\n", pathID, leaf) 520 var resp *http.Response 521 var info *api.Item 522 opts := rest.Opts{ 523 Method: "POST", 524 Path: "/folders", 525 Parameters: fieldsValue(), 526 } 527 mkdir := api.CreateFolder{ 528 Name: f.opt.Enc.FromStandardName(leaf), 529 Parent: api.Parent{ 530 ID: pathID, 531 }, 532 } 533 err = f.pacer.Call(func() (bool, error) { 534 resp, err = f.srv.CallJSON(ctx, &opts, &mkdir, &info) 535 return shouldRetry(resp, err) 536 }) 537 if err != nil { 538 //fmt.Printf("...Error %v\n", err) 539 return "", err 540 } 541 // fmt.Printf("...Id %q\n", *info.Id) 542 return info.ID, nil 543 } 544 545 // list the objects into the function supplied 546 // 547 // If directories is set it only sends directories 548 // User function to process a File item from listAll 549 // 550 // Should return true to finish processing 551 type listAllFn func(*api.Item) bool 552 553 // Lists the directory required calling the user function on each item found 554 // 555 // If the user fn ever returns true then it early exits with found = true 556 func (f *Fs) listAll(ctx context.Context, dirID string, directoriesOnly bool, filesOnly bool, fn listAllFn) (found bool, err error) { 557 opts := rest.Opts{ 558 Method: "GET", 559 Path: "/folders/" + dirID + "/items", 560 Parameters: fieldsValue(), 561 } 562 opts.Parameters.Set("limit", strconv.Itoa(listChunks)) 563 offset := 0 564 OUTER: 565 for { 566 opts.Parameters.Set("offset", strconv.Itoa(offset)) 567 568 var result api.FolderItems 569 var resp *http.Response 570 err = f.pacer.Call(func() (bool, error) { 571 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 572 return shouldRetry(resp, err) 573 }) 574 if err != nil { 575 return found, errors.Wrap(err, "couldn't list files") 576 } 577 for i := range result.Entries { 578 item := &result.Entries[i] 579 if item.Type == api.ItemTypeFolder { 580 if filesOnly { 581 continue 582 } 583 } else if item.Type == api.ItemTypeFile { 584 if directoriesOnly { 585 continue 586 } 587 } else { 588 fs.Debugf(f, "Ignoring %q - unknown type %q", item.Name, item.Type) 589 continue 590 } 591 if item.ItemStatus != api.ItemStatusActive { 592 continue 593 } 594 item.Name = f.opt.Enc.ToStandardName(item.Name) 595 if fn(item) { 596 found = true 597 break OUTER 598 } 599 } 600 offset += result.Limit 601 if offset >= result.TotalCount { 602 break 603 } 604 } 605 return 606 } 607 608 // List the objects and directories in dir into entries. The 609 // entries can be returned in any order but should be for a 610 // complete directory. 611 // 612 // dir should be "" to list the root, and should not have 613 // trailing slashes. 614 // 615 // This should return ErrDirNotFound if the directory isn't 616 // found. 617 func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 618 err = f.dirCache.FindRoot(ctx, false) 619 if err != nil { 620 return nil, err 621 } 622 directoryID, err := f.dirCache.FindDir(ctx, dir, false) 623 if err != nil { 624 return nil, err 625 } 626 var iErr error 627 _, err = f.listAll(ctx, directoryID, false, false, func(info *api.Item) bool { 628 remote := path.Join(dir, info.Name) 629 if info.Type == api.ItemTypeFolder { 630 // cache the directory ID for later lookups 631 f.dirCache.Put(remote, info.ID) 632 d := fs.NewDir(remote, info.ModTime()).SetID(info.ID) 633 // FIXME more info from dir? 634 entries = append(entries, d) 635 } else if info.Type == api.ItemTypeFile { 636 o, err := f.newObjectWithInfo(ctx, remote, info) 637 if err != nil { 638 iErr = err 639 return true 640 } 641 entries = append(entries, o) 642 } 643 return false 644 }) 645 if err != nil { 646 return nil, err 647 } 648 if iErr != nil { 649 return nil, iErr 650 } 651 return entries, nil 652 } 653 654 // Creates from the parameters passed in a half finished Object which 655 // must have setMetaData called on it 656 // 657 // Returns the object, leaf, directoryID and error 658 // 659 // Used to create new objects 660 func (f *Fs) createObject(ctx context.Context, remote string, modTime time.Time, size int64) (o *Object, leaf string, directoryID string, err error) { 661 // Create the directory for the object if it doesn't exist 662 leaf, directoryID, err = f.dirCache.FindRootAndPath(ctx, remote, true) 663 if err != nil { 664 return 665 } 666 // Temporary Object under construction 667 o = &Object{ 668 fs: f, 669 remote: remote, 670 } 671 return o, leaf, directoryID, nil 672 } 673 674 // Put the object 675 // 676 // Copy the reader in to the new object which is returned 677 // 678 // The new object may have been created if an error is returned 679 func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 680 existingObj, err := f.newObjectWithInfo(ctx, src.Remote(), nil) 681 switch err { 682 case nil: 683 return existingObj, existingObj.Update(ctx, in, src, options...) 684 case fs.ErrorObjectNotFound: 685 // Not found so create it 686 return f.PutUnchecked(ctx, in, src) 687 default: 688 return nil, err 689 } 690 } 691 692 // PutStream uploads to the remote path with the modTime given of indeterminate size 693 func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 694 return f.Put(ctx, in, src, options...) 695 } 696 697 // PutUnchecked the object into the container 698 // 699 // This will produce an error if the object already exists 700 // 701 // Copy the reader in to the new object which is returned 702 // 703 // The new object may have been created if an error is returned 704 func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 705 remote := src.Remote() 706 size := src.Size() 707 modTime := src.ModTime(ctx) 708 709 o, _, _, err := f.createObject(ctx, remote, modTime, size) 710 if err != nil { 711 return nil, err 712 } 713 return o, o.Update(ctx, in, src, options...) 714 } 715 716 // Mkdir creates the container if it doesn't exist 717 func (f *Fs) Mkdir(ctx context.Context, dir string) error { 718 err := f.dirCache.FindRoot(ctx, true) 719 if err != nil { 720 return err 721 } 722 if dir != "" { 723 _, err = f.dirCache.FindDir(ctx, dir, true) 724 } 725 return err 726 } 727 728 // deleteObject removes an object by ID 729 func (f *Fs) deleteObject(ctx context.Context, id string) error { 730 opts := rest.Opts{ 731 Method: "DELETE", 732 Path: "/files/" + id, 733 NoResponse: true, 734 } 735 return f.pacer.Call(func() (bool, error) { 736 resp, err := f.srv.Call(ctx, &opts) 737 return shouldRetry(resp, err) 738 }) 739 } 740 741 // purgeCheck removes the root directory, if check is set then it 742 // refuses to do so if it has anything in 743 func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error { 744 root := path.Join(f.root, dir) 745 if root == "" { 746 return errors.New("can't purge root directory") 747 } 748 dc := f.dirCache 749 err := dc.FindRoot(ctx, false) 750 if err != nil { 751 return err 752 } 753 rootID, err := dc.FindDir(ctx, dir, false) 754 if err != nil { 755 return err 756 } 757 758 opts := rest.Opts{ 759 Method: "DELETE", 760 Path: "/folders/" + rootID, 761 Parameters: url.Values{}, 762 NoResponse: true, 763 } 764 opts.Parameters.Set("recursive", strconv.FormatBool(!check)) 765 var resp *http.Response 766 err = f.pacer.Call(func() (bool, error) { 767 resp, err = f.srv.Call(ctx, &opts) 768 return shouldRetry(resp, err) 769 }) 770 if err != nil { 771 return errors.Wrap(err, "rmdir failed") 772 } 773 f.dirCache.FlushDir(dir) 774 if err != nil { 775 return err 776 } 777 return nil 778 } 779 780 // Rmdir deletes the root folder 781 // 782 // Returns an error if it isn't empty 783 func (f *Fs) Rmdir(ctx context.Context, dir string) error { 784 return f.purgeCheck(ctx, dir, true) 785 } 786 787 // Precision return the precision of this Fs 788 func (f *Fs) Precision() time.Duration { 789 return time.Second 790 } 791 792 // Copy src to this remote using server side copy operations. 793 // 794 // This is stored with the remote path given 795 // 796 // It returns the destination Object and a possible error 797 // 798 // Will only be called if src.Fs().Name() == f.Name() 799 // 800 // If it isn't possible then return fs.ErrorCantCopy 801 func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 802 srcObj, ok := src.(*Object) 803 if !ok { 804 fs.Debugf(src, "Can't copy - not same remote type") 805 return nil, fs.ErrorCantCopy 806 } 807 err := srcObj.readMetaData(ctx) 808 if err != nil { 809 return nil, err 810 } 811 812 srcPath := srcObj.fs.rootSlash() + srcObj.remote 813 dstPath := f.rootSlash() + remote 814 if strings.ToLower(srcPath) == strings.ToLower(dstPath) { 815 return nil, errors.Errorf("can't copy %q -> %q as are same name when lowercase", srcPath, dstPath) 816 } 817 818 // Create temporary object 819 dstObj, leaf, directoryID, err := f.createObject(ctx, remote, srcObj.modTime, srcObj.size) 820 if err != nil { 821 return nil, err 822 } 823 824 // Copy the object 825 opts := rest.Opts{ 826 Method: "POST", 827 Path: "/files/" + srcObj.id + "/copy", 828 Parameters: fieldsValue(), 829 } 830 copyFile := api.CopyFile{ 831 Name: f.opt.Enc.FromStandardName(leaf), 832 Parent: api.Parent{ 833 ID: directoryID, 834 }, 835 } 836 var resp *http.Response 837 var info *api.Item 838 err = f.pacer.Call(func() (bool, error) { 839 resp, err = f.srv.CallJSON(ctx, &opts, ©File, &info) 840 return shouldRetry(resp, err) 841 }) 842 if err != nil { 843 return nil, err 844 } 845 err = dstObj.setMetaData(info) 846 if err != nil { 847 return nil, err 848 } 849 return dstObj, nil 850 } 851 852 // Purge deletes all the files and the container 853 // 854 // Optional interface: Only implement this if you have a way of 855 // deleting all the files quicker than just running Remove() on the 856 // result of List() 857 func (f *Fs) Purge(ctx context.Context) error { 858 return f.purgeCheck(ctx, "", false) 859 } 860 861 // move a file or folder 862 func (f *Fs) move(ctx context.Context, endpoint, id, leaf, directoryID string) (info *api.Item, err error) { 863 // Move the object 864 opts := rest.Opts{ 865 Method: "PUT", 866 Path: endpoint + id, 867 Parameters: fieldsValue(), 868 } 869 move := api.UpdateFileMove{ 870 Name: f.opt.Enc.FromStandardName(leaf), 871 Parent: api.Parent{ 872 ID: directoryID, 873 }, 874 } 875 var resp *http.Response 876 err = f.pacer.Call(func() (bool, error) { 877 resp, err = f.srv.CallJSON(ctx, &opts, &move, &info) 878 return shouldRetry(resp, err) 879 }) 880 if err != nil { 881 return nil, err 882 } 883 return info, nil 884 } 885 886 // About gets quota information 887 func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) { 888 opts := rest.Opts{ 889 Method: "GET", 890 Path: "/users/me", 891 } 892 var user api.User 893 var resp *http.Response 894 err = f.pacer.Call(func() (bool, error) { 895 resp, err = f.srv.CallJSON(ctx, &opts, nil, &user) 896 return shouldRetry(resp, err) 897 }) 898 if err != nil { 899 return nil, errors.Wrap(err, "failed to read user info") 900 } 901 // FIXME max upload size would be useful to use in Update 902 usage = &fs.Usage{ 903 Used: fs.NewUsageValue(user.SpaceUsed), // bytes in use 904 Total: fs.NewUsageValue(user.SpaceAmount), // bytes total 905 Free: fs.NewUsageValue(user.SpaceAmount - user.SpaceUsed), // bytes free 906 } 907 return usage, nil 908 } 909 910 // Move src to this remote using server side move operations. 911 // 912 // This is stored with the remote path given 913 // 914 // It returns the destination Object and a possible error 915 // 916 // Will only be called if src.Fs().Name() == f.Name() 917 // 918 // If it isn't possible then return fs.ErrorCantMove 919 func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) { 920 srcObj, ok := src.(*Object) 921 if !ok { 922 fs.Debugf(src, "Can't move - not same remote type") 923 return nil, fs.ErrorCantMove 924 } 925 926 // Create temporary object 927 dstObj, leaf, directoryID, err := f.createObject(ctx, remote, srcObj.modTime, srcObj.size) 928 if err != nil { 929 return nil, err 930 } 931 932 // Do the move 933 info, err := f.move(ctx, "/files/", srcObj.id, leaf, directoryID) 934 if err != nil { 935 return nil, err 936 } 937 938 err = dstObj.setMetaData(info) 939 if err != nil { 940 return nil, err 941 } 942 return dstObj, nil 943 } 944 945 // DirMove moves src, srcRemote to this remote at dstRemote 946 // using server side move operations. 947 // 948 // Will only be called if src.Fs().Name() == f.Name() 949 // 950 // If it isn't possible then return fs.ErrorCantDirMove 951 // 952 // If destination exists then return fs.ErrorDirExists 953 func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error { 954 srcFs, ok := src.(*Fs) 955 if !ok { 956 fs.Debugf(srcFs, "Can't move directory - not same remote type") 957 return fs.ErrorCantDirMove 958 } 959 srcPath := path.Join(srcFs.root, srcRemote) 960 dstPath := path.Join(f.root, dstRemote) 961 962 // Refuse to move to or from the root 963 if srcPath == "" || dstPath == "" { 964 fs.Debugf(src, "DirMove error: Can't move root") 965 return errors.New("can't move root directory") 966 } 967 968 // find the root src directory 969 err := srcFs.dirCache.FindRoot(ctx, false) 970 if err != nil { 971 return err 972 } 973 974 // find the root dst directory 975 if dstRemote != "" { 976 err = f.dirCache.FindRoot(ctx, true) 977 if err != nil { 978 return err 979 } 980 } else { 981 if f.dirCache.FoundRoot() { 982 return fs.ErrorDirExists 983 } 984 } 985 986 // Find ID of dst parent, creating subdirs if necessary 987 var leaf, directoryID string 988 findPath := dstRemote 989 if dstRemote == "" { 990 findPath = f.root 991 } 992 leaf, directoryID, err = f.dirCache.FindPath(ctx, findPath, true) 993 if err != nil { 994 return err 995 } 996 997 // Check destination does not exist 998 if dstRemote != "" { 999 _, err = f.dirCache.FindDir(ctx, dstRemote, false) 1000 if err == fs.ErrorDirNotFound { 1001 // OK 1002 } else if err != nil { 1003 return err 1004 } else { 1005 return fs.ErrorDirExists 1006 } 1007 } 1008 1009 // Find ID of src 1010 srcID, err := srcFs.dirCache.FindDir(ctx, srcRemote, false) 1011 if err != nil { 1012 return err 1013 } 1014 1015 // Do the move 1016 _, err = f.move(ctx, "/folders/", srcID, leaf, directoryID) 1017 if err != nil { 1018 return err 1019 } 1020 srcFs.dirCache.FlushDir(srcRemote) 1021 return nil 1022 } 1023 1024 // PublicLink adds a "readable by anyone with link" permission on the given file or folder. 1025 func (f *Fs) PublicLink(ctx context.Context, remote string) (string, error) { 1026 id, err := f.dirCache.FindDir(ctx, remote, false) 1027 var opts rest.Opts 1028 if err == nil { 1029 fs.Debugf(f, "attempting to share directory '%s'", remote) 1030 1031 opts = rest.Opts{ 1032 Method: "PUT", 1033 Path: "/folders/" + id, 1034 Parameters: fieldsValue(), 1035 } 1036 } else { 1037 fs.Debugf(f, "attempting to share single file '%s'", remote) 1038 o, err := f.NewObject(ctx, remote) 1039 if err != nil { 1040 return "", err 1041 } 1042 1043 if o.(*Object).publicLink != "" { 1044 return o.(*Object).publicLink, nil 1045 } 1046 1047 opts = rest.Opts{ 1048 Method: "PUT", 1049 Path: "/files/" + o.(*Object).id, 1050 Parameters: fieldsValue(), 1051 } 1052 } 1053 1054 shareLink := api.CreateSharedLink{} 1055 var info api.Item 1056 var resp *http.Response 1057 err = f.pacer.Call(func() (bool, error) { 1058 resp, err = f.srv.CallJSON(ctx, &opts, &shareLink, &info) 1059 return shouldRetry(resp, err) 1060 }) 1061 return info.SharedLink.URL, err 1062 } 1063 1064 // DirCacheFlush resets the directory cache - used in testing as an 1065 // optional interface 1066 func (f *Fs) DirCacheFlush() { 1067 f.dirCache.ResetRoot() 1068 } 1069 1070 // Hashes returns the supported hash sets. 1071 func (f *Fs) Hashes() hash.Set { 1072 return hash.Set(hash.SHA1) 1073 } 1074 1075 // ------------------------------------------------------------ 1076 1077 // Fs returns the parent Fs 1078 func (o *Object) Fs() fs.Info { 1079 return o.fs 1080 } 1081 1082 // Return a string version 1083 func (o *Object) String() string { 1084 if o == nil { 1085 return "<nil>" 1086 } 1087 return o.remote 1088 } 1089 1090 // Remote returns the remote path 1091 func (o *Object) Remote() string { 1092 return o.remote 1093 } 1094 1095 // Hash returns the SHA-1 of an object returning a lowercase hex string 1096 func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) { 1097 if t != hash.SHA1 { 1098 return "", hash.ErrUnsupported 1099 } 1100 return o.sha1, nil 1101 } 1102 1103 // Size returns the size of an object in bytes 1104 func (o *Object) Size() int64 { 1105 err := o.readMetaData(context.TODO()) 1106 if err != nil { 1107 fs.Logf(o, "Failed to read metadata: %v", err) 1108 return 0 1109 } 1110 return o.size 1111 } 1112 1113 // setMetaData sets the metadata from info 1114 func (o *Object) setMetaData(info *api.Item) (err error) { 1115 if info.Type != api.ItemTypeFile { 1116 return errors.Wrapf(fs.ErrorNotAFile, "%q is %q", o.remote, info.Type) 1117 } 1118 o.hasMetaData = true 1119 o.size = int64(info.Size) 1120 o.sha1 = info.SHA1 1121 o.modTime = info.ModTime() 1122 o.id = info.ID 1123 o.publicLink = info.SharedLink.URL 1124 return nil 1125 } 1126 1127 // readMetaData gets the metadata if it hasn't already been fetched 1128 // 1129 // it also sets the info 1130 func (o *Object) readMetaData(ctx context.Context) (err error) { 1131 if o.hasMetaData { 1132 return nil 1133 } 1134 info, err := o.fs.readMetaDataForPath(ctx, o.remote) 1135 if err != nil { 1136 if apiErr, ok := err.(*api.Error); ok { 1137 if apiErr.Code == "not_found" || apiErr.Code == "trashed" { 1138 return fs.ErrorObjectNotFound 1139 } 1140 } 1141 return err 1142 } 1143 return o.setMetaData(info) 1144 } 1145 1146 // ModTime returns the modification time of the object 1147 // 1148 // 1149 // It attempts to read the objects mtime and if that isn't present the 1150 // LastModified returned in the http headers 1151 func (o *Object) ModTime(ctx context.Context) time.Time { 1152 err := o.readMetaData(ctx) 1153 if err != nil { 1154 fs.Logf(o, "Failed to read metadata: %v", err) 1155 return time.Now() 1156 } 1157 return o.modTime 1158 } 1159 1160 // setModTime sets the modification time of the local fs object 1161 func (o *Object) setModTime(ctx context.Context, modTime time.Time) (*api.Item, error) { 1162 opts := rest.Opts{ 1163 Method: "PUT", 1164 Path: "/files/" + o.id, 1165 Parameters: fieldsValue(), 1166 } 1167 update := api.UpdateFileModTime{ 1168 ContentModifiedAt: api.Time(modTime), 1169 } 1170 var info *api.Item 1171 err := o.fs.pacer.Call(func() (bool, error) { 1172 resp, err := o.fs.srv.CallJSON(ctx, &opts, &update, &info) 1173 return shouldRetry(resp, err) 1174 }) 1175 return info, err 1176 } 1177 1178 // SetModTime sets the modification time of the local fs object 1179 func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error { 1180 info, err := o.setModTime(ctx, modTime) 1181 if err != nil { 1182 return err 1183 } 1184 return o.setMetaData(info) 1185 } 1186 1187 // Storable returns a boolean showing whether this object storable 1188 func (o *Object) Storable() bool { 1189 return true 1190 } 1191 1192 // Open an object for read 1193 func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) { 1194 if o.id == "" { 1195 return nil, errors.New("can't download - no id") 1196 } 1197 fs.FixRangeOption(options, o.size) 1198 var resp *http.Response 1199 opts := rest.Opts{ 1200 Method: "GET", 1201 Path: "/files/" + o.id + "/content", 1202 Options: options, 1203 } 1204 err = o.fs.pacer.Call(func() (bool, error) { 1205 resp, err = o.fs.srv.Call(ctx, &opts) 1206 return shouldRetry(resp, err) 1207 }) 1208 if err != nil { 1209 return nil, err 1210 } 1211 return resp.Body, err 1212 } 1213 1214 // upload does a single non-multipart upload 1215 // 1216 // This is recommended for less than 50 MB of content 1217 func (o *Object) upload(ctx context.Context, in io.Reader, leaf, directoryID string, modTime time.Time, options ...fs.OpenOption) (err error) { 1218 upload := api.UploadFile{ 1219 Name: o.fs.opt.Enc.FromStandardName(leaf), 1220 ContentModifiedAt: api.Time(modTime), 1221 ContentCreatedAt: api.Time(modTime), 1222 Parent: api.Parent{ 1223 ID: directoryID, 1224 }, 1225 } 1226 1227 var resp *http.Response 1228 var result api.FolderItems 1229 opts := rest.Opts{ 1230 Method: "POST", 1231 Body: in, 1232 MultipartMetadataName: "attributes", 1233 MultipartContentName: "contents", 1234 MultipartFileName: upload.Name, 1235 RootURL: uploadURL, 1236 Options: options, 1237 } 1238 // If object has an ID then it is existing so create a new version 1239 if o.id != "" { 1240 opts.Path = "/files/" + o.id + "/content" 1241 } else { 1242 opts.Path = "/files/content" 1243 } 1244 err = o.fs.pacer.CallNoRetry(func() (bool, error) { 1245 resp, err = o.fs.srv.CallJSON(ctx, &opts, &upload, &result) 1246 return shouldRetry(resp, err) 1247 }) 1248 if err != nil { 1249 return err 1250 } 1251 if result.TotalCount != 1 || len(result.Entries) != 1 { 1252 return errors.Errorf("failed to upload %v - not sure why", o) 1253 } 1254 return o.setMetaData(&result.Entries[0]) 1255 } 1256 1257 // Update the object with the contents of the io.Reader, modTime and size 1258 // 1259 // If existing is set then it updates the object rather than creating a new one 1260 // 1261 // The new object may have been created if an error is returned 1262 func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) { 1263 o.fs.tokenRenewer.Start() 1264 defer o.fs.tokenRenewer.Stop() 1265 1266 size := src.Size() 1267 modTime := src.ModTime(ctx) 1268 remote := o.Remote() 1269 1270 // Create the directory for the object if it doesn't exist 1271 leaf, directoryID, err := o.fs.dirCache.FindRootAndPath(ctx, remote, true) 1272 if err != nil { 1273 return err 1274 } 1275 1276 // Upload with simple or multipart 1277 if size <= int64(o.fs.opt.UploadCutoff) { 1278 err = o.upload(ctx, in, leaf, directoryID, modTime, options...) 1279 } else { 1280 err = o.uploadMultipart(ctx, in, leaf, directoryID, size, modTime, options...) 1281 } 1282 return err 1283 } 1284 1285 // Remove an object 1286 func (o *Object) Remove(ctx context.Context) error { 1287 return o.fs.deleteObject(ctx, o.id) 1288 } 1289 1290 // ID returns the ID of the Object if known, or "" if not 1291 func (o *Object) ID() string { 1292 return o.id 1293 } 1294 1295 // Check the interfaces are satisfied 1296 var ( 1297 _ fs.Fs = (*Fs)(nil) 1298 _ fs.Purger = (*Fs)(nil) 1299 _ fs.PutStreamer = (*Fs)(nil) 1300 _ fs.Copier = (*Fs)(nil) 1301 _ fs.Abouter = (*Fs)(nil) 1302 _ fs.Mover = (*Fs)(nil) 1303 _ fs.DirMover = (*Fs)(nil) 1304 _ fs.DirCacheFlusher = (*Fs)(nil) 1305 _ fs.PublicLinker = (*Fs)(nil) 1306 _ fs.Object = (*Object)(nil) 1307 _ fs.IDer = (*Object)(nil) 1308 )