github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/backend/seafile/webapi.go (about) 1 package seafile 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "net/http" 10 "net/url" 11 "path" 12 "strings" 13 14 "github.com/pkg/errors" 15 "github.com/rclone/rclone/backend/seafile/api" 16 "github.com/rclone/rclone/fs" 17 "github.com/rclone/rclone/lib/readers" 18 "github.com/rclone/rclone/lib/rest" 19 ) 20 21 // Start of the API URLs 22 const ( 23 APIv20 = "api2/repos/" 24 APIv21 = "api/v2.1/repos/" 25 ) 26 27 // Errors specific to seafile fs 28 var ( 29 ErrorInternalDuringUpload = errors.New("Internal server error during file upload") 30 ) 31 32 // ==================== Seafile API ==================== 33 34 func (f *Fs) getAuthorizationToken(ctx context.Context) (string, error) { 35 return getAuthorizationToken(ctx, f.srv, f.opt.User, f.opt.Password, "") 36 } 37 38 // getAuthorizationToken can be called outside of an fs (during configuration of the remote to get the authentication token) 39 // it's doing a single call (no pacer involved) 40 func getAuthorizationToken(ctx context.Context, srv *rest.Client, user, password, oneTimeCode string) (string, error) { 41 // API Documentation 42 // https://download.seafile.com/published/web-api/home.md#user-content-Quick%20Start 43 opts := rest.Opts{ 44 Method: "POST", 45 Path: "api2/auth-token/", 46 ExtraHeaders: map[string]string{"Authorization": ""}, // unset the Authorization for this request 47 IgnoreStatus: true, // so we can load the error messages back into result 48 } 49 50 // 2FA 51 if oneTimeCode != "" { 52 opts.ExtraHeaders["X-SEAFILE-OTP"] = oneTimeCode 53 } 54 55 request := api.AuthenticationRequest{ 56 Username: user, 57 Password: password, 58 } 59 result := api.AuthenticationResult{} 60 61 _, err := srv.CallJSON(ctx, &opts, &request, &result) 62 if err != nil { 63 // This is only going to be http errors here 64 return "", errors.Wrap(err, "failed to authenticate") 65 } 66 if result.Errors != nil && len(result.Errors) > 0 { 67 return "", errors.New(strings.Join(result.Errors, ", ")) 68 } 69 if result.Token == "" { 70 // No error in "non_field_errors" field but still empty token 71 return "", errors.New("failed to authenticate") 72 } 73 return result.Token, nil 74 } 75 76 func (f *Fs) getServerInfo(ctx context.Context) (account *api.ServerInfo, err error) { 77 // API Documentation 78 // https://download.seafile.com/published/web-api/v2.1/server-info.md#user-content-Get%20Server%20Information 79 opts := rest.Opts{ 80 Method: "GET", 81 Path: "api2/server-info/", 82 } 83 84 result := api.ServerInfo{} 85 86 var resp *http.Response 87 err = f.pacer.Call(func() (bool, error) { 88 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 89 return f.shouldRetry(resp, err) 90 }) 91 if err != nil { 92 if resp != nil { 93 if resp.StatusCode == 401 || resp.StatusCode == 403 { 94 return nil, fs.ErrorPermissionDenied 95 } 96 } 97 return nil, errors.Wrap(err, "failed to get server info") 98 } 99 return &result, nil 100 } 101 102 func (f *Fs) getUserAccountInfo(ctx context.Context) (account *api.AccountInfo, err error) { 103 // API Documentation 104 // https://download.seafile.com/published/web-api/v2.1/account.md#user-content-Check%20Account%20Info 105 opts := rest.Opts{ 106 Method: "GET", 107 Path: "api2/account/info/", 108 } 109 110 result := api.AccountInfo{} 111 112 var resp *http.Response 113 err = f.pacer.Call(func() (bool, error) { 114 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 115 return f.shouldRetry(resp, err) 116 }) 117 if err != nil { 118 if resp != nil { 119 if resp.StatusCode == 401 || resp.StatusCode == 403 { 120 return nil, fs.ErrorPermissionDenied 121 } 122 } 123 return nil, errors.Wrap(err, "failed to get account info") 124 } 125 return &result, nil 126 } 127 128 func (f *Fs) getLibraries(ctx context.Context) ([]api.Library, error) { 129 // API Documentation 130 // https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-List%20Libraries 131 opts := rest.Opts{ 132 Method: "GET", 133 Path: APIv20, 134 } 135 136 result := make([]api.Library, 1) 137 138 var resp *http.Response 139 var err error 140 err = f.pacer.Call(func() (bool, error) { 141 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 142 return f.shouldRetry(resp, err) 143 }) 144 if err != nil { 145 if resp != nil { 146 if resp.StatusCode == 401 || resp.StatusCode == 403 { 147 return nil, fs.ErrorPermissionDenied 148 } 149 } 150 return nil, errors.Wrap(err, "failed to get libraries") 151 } 152 return result, nil 153 } 154 155 func (f *Fs) createLibrary(ctx context.Context, libraryName, password string) (library *api.CreateLibrary, err error) { 156 // API Documentation 157 // https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Create%20Library 158 opts := rest.Opts{ 159 Method: "POST", 160 Path: APIv20, 161 } 162 163 request := api.CreateLibraryRequest{ 164 Name: f.opt.Enc.FromStandardName(libraryName), 165 Description: "Created by rclone", 166 Password: password, 167 } 168 result := &api.CreateLibrary{} 169 170 var resp *http.Response 171 err = f.pacer.Call(func() (bool, error) { 172 resp, err = f.srv.CallJSON(ctx, &opts, &request, &result) 173 return f.shouldRetry(resp, err) 174 }) 175 if err != nil { 176 if resp != nil { 177 if resp.StatusCode == 401 || resp.StatusCode == 403 { 178 return nil, fs.ErrorPermissionDenied 179 } 180 } 181 return nil, errors.Wrap(err, "failed to create library") 182 } 183 return result, nil 184 } 185 186 func (f *Fs) deleteLibrary(ctx context.Context, libraryID string) error { 187 // API Documentation 188 // https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Create%20Library 189 opts := rest.Opts{ 190 Method: "DELETE", 191 Path: APIv20 + libraryID + "/", 192 } 193 194 result := "" 195 196 var resp *http.Response 197 var err error 198 err = f.pacer.Call(func() (bool, error) { 199 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 200 return f.shouldRetry(resp, err) 201 }) 202 if err != nil { 203 if resp != nil { 204 if resp.StatusCode == 401 || resp.StatusCode == 403 { 205 return fs.ErrorPermissionDenied 206 } 207 } 208 return errors.Wrap(err, "failed to delete library") 209 } 210 return nil 211 } 212 213 func (f *Fs) decryptLibrary(ctx context.Context, libraryID, password string) error { 214 // API Documentation 215 // https://download.seafile.com/published/web-api/v2.1/library-encryption.md#user-content-Decrypt%20Library 216 if libraryID == "" { 217 return errors.New("cannot list files without a library") 218 } 219 // This is another call that cannot accept a JSON input so we have to build it manually 220 opts := rest.Opts{ 221 Method: "POST", 222 Path: APIv20 + libraryID + "/", 223 ContentType: "application/x-www-form-urlencoded", 224 Body: bytes.NewBuffer([]byte("password=" + f.opt.Enc.FromStandardName(password))), 225 NoResponse: true, 226 } 227 var resp *http.Response 228 var err error 229 err = f.pacer.Call(func() (bool, error) { 230 resp, err = f.srv.Call(ctx, &opts) 231 return f.shouldRetry(resp, err) 232 }) 233 if err != nil { 234 if resp != nil { 235 if resp.StatusCode == 400 { 236 return errors.New("incorrect password") 237 } 238 if resp.StatusCode == 409 { 239 fs.Debugf(nil, "library is not encrypted") 240 return nil 241 } 242 } 243 return errors.Wrap(err, "failed to decrypt library") 244 } 245 return nil 246 } 247 248 func (f *Fs) getDirectoryEntriesAPIv21(ctx context.Context, libraryID, dirPath string, recursive bool) ([]api.DirEntry, error) { 249 // API Documentation 250 // https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-List%20Items%20in%20Directory 251 // This is using the undocumented version 2.1 of the API (so we can use the recursive option which is not available in the version 2) 252 if libraryID == "" { 253 return nil, errors.New("cannot list files without a library") 254 } 255 dirPath = path.Join("/", dirPath) 256 257 recursiveFlag := "0" 258 if recursive { 259 recursiveFlag = "1" 260 } 261 opts := rest.Opts{ 262 Method: "GET", 263 Path: APIv21 + libraryID + "/dir/", 264 Parameters: url.Values{ 265 "recursive": {recursiveFlag}, 266 "p": {f.opt.Enc.FromStandardPath(dirPath)}, 267 }, 268 } 269 result := &api.DirEntries{} 270 var resp *http.Response 271 var err error 272 err = f.pacer.Call(func() (bool, error) { 273 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 274 return f.shouldRetry(resp, err) 275 }) 276 if err != nil { 277 if resp != nil { 278 if resp.StatusCode == 401 || resp.StatusCode == 403 { 279 return nil, fs.ErrorPermissionDenied 280 } 281 if resp.StatusCode == 404 { 282 return nil, fs.ErrorDirNotFound 283 } 284 if resp.StatusCode == 440 { 285 // Encrypted library and password not provided 286 return nil, fs.ErrorPermissionDenied 287 } 288 } 289 return nil, errors.Wrap(err, "failed to get directory contents") 290 } 291 292 // Clean up encoded names 293 for index, fileInfo := range result.Entries { 294 fileInfo.Name = f.opt.Enc.ToStandardName(fileInfo.Name) 295 fileInfo.Path = f.opt.Enc.ToStandardPath(fileInfo.Path) 296 result.Entries[index] = fileInfo 297 } 298 return result.Entries, nil 299 } 300 301 func (f *Fs) getDirectoryDetails(ctx context.Context, libraryID, dirPath string) (*api.DirectoryDetail, error) { 302 // API Documentation 303 // https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Get%20Directory%20Detail 304 if libraryID == "" { 305 return nil, errors.New("cannot read directory without a library") 306 } 307 dirPath = path.Join("/", dirPath) 308 309 opts := rest.Opts{ 310 Method: "GET", 311 Path: APIv21 + libraryID + "/dir/detail/", 312 Parameters: url.Values{"path": {f.opt.Enc.FromStandardPath(dirPath)}}, 313 } 314 result := &api.DirectoryDetail{} 315 var resp *http.Response 316 var err error 317 err = f.pacer.Call(func() (bool, error) { 318 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 319 return f.shouldRetry(resp, err) 320 }) 321 if err != nil { 322 if resp != nil { 323 if resp.StatusCode == 401 || resp.StatusCode == 403 { 324 return nil, fs.ErrorPermissionDenied 325 } 326 if resp.StatusCode == 404 { 327 return nil, fs.ErrorDirNotFound 328 } 329 } 330 return nil, errors.Wrap(err, "failed to get directory details") 331 } 332 result.Name = f.opt.Enc.ToStandardName(result.Name) 333 result.Path = f.opt.Enc.ToStandardPath(result.Path) 334 return result, nil 335 } 336 337 // createDir creates a new directory. The API will add a number to the directory name if it already exist 338 func (f *Fs) createDir(ctx context.Context, libraryID, dirPath string) error { 339 // API Documentation 340 // https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Create%20New%20Directory 341 if libraryID == "" { 342 return errors.New("cannot create directory without a library") 343 } 344 dirPath = path.Join("/", dirPath) 345 346 // This call *cannot* handle json parameters in the body, so we have to build the request body manually 347 opts := rest.Opts{ 348 Method: "POST", 349 Path: APIv20 + libraryID + "/dir/", 350 Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(dirPath)}}, 351 NoRedirect: true, 352 ContentType: "application/x-www-form-urlencoded", 353 Body: bytes.NewBuffer([]byte("operation=mkdir")), 354 NoResponse: true, 355 } 356 357 var resp *http.Response 358 var err error 359 err = f.pacer.Call(func() (bool, error) { 360 resp, err = f.srv.Call(ctx, &opts) 361 return f.shouldRetry(resp, err) 362 }) 363 if err != nil { 364 if resp != nil { 365 if resp.StatusCode == 401 || resp.StatusCode == 403 { 366 return fs.ErrorPermissionDenied 367 } 368 } 369 return errors.Wrap(err, "failed to create directory") 370 } 371 return nil 372 } 373 374 func (f *Fs) renameDir(ctx context.Context, libraryID, dirPath, newName string) error { 375 // API Documentation 376 // https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Rename%20Directory 377 if libraryID == "" { 378 return errors.New("cannot rename directory without a library") 379 } 380 dirPath = path.Join("/", dirPath) 381 382 // This call *cannot* handle json parameters in the body, so we have to build the request body manually 383 postParameters := url.Values{ 384 "operation": {"rename"}, 385 "newname": {f.opt.Enc.FromStandardPath(newName)}, 386 } 387 388 opts := rest.Opts{ 389 Method: "POST", 390 Path: APIv20 + libraryID + "/dir/", 391 Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(dirPath)}}, 392 ContentType: "application/x-www-form-urlencoded", 393 Body: bytes.NewBuffer([]byte(postParameters.Encode())), 394 NoResponse: true, 395 } 396 397 var resp *http.Response 398 var err error 399 err = f.pacer.Call(func() (bool, error) { 400 resp, err = f.srv.Call(ctx, &opts) 401 return f.shouldRetry(resp, err) 402 }) 403 if err != nil { 404 if resp != nil { 405 if resp.StatusCode == 401 || resp.StatusCode == 403 { 406 return fs.ErrorPermissionDenied 407 } 408 } 409 return errors.Wrap(err, "failed to rename directory") 410 } 411 return nil 412 } 413 414 func (f *Fs) moveDir(ctx context.Context, srcLibraryID, srcDir, srcName, dstLibraryID, dstPath string) error { 415 // API Documentation 416 // https://download.seafile.com/published/web-api/v2.1/files-directories-batch-op.md#user-content-Batch%20Move%20Items%20Synchronously 417 if srcLibraryID == "" || dstLibraryID == "" || srcName == "" { 418 return errors.New("libraryID and/or file path argument(s) missing") 419 } 420 srcDir = path.Join("/", srcDir) 421 dstPath = path.Join("/", dstPath) 422 423 opts := rest.Opts{ 424 Method: "POST", 425 Path: APIv21 + "sync-batch-move-item/", 426 NoResponse: true, 427 } 428 429 request := &api.BatchSourceDestRequest{ 430 SrcLibraryID: srcLibraryID, 431 SrcParentDir: f.opt.Enc.FromStandardPath(srcDir), 432 SrcItems: []string{f.opt.Enc.FromStandardPath(srcName)}, 433 DstLibraryID: dstLibraryID, 434 DstParentDir: f.opt.Enc.FromStandardPath(dstPath), 435 } 436 437 var resp *http.Response 438 var err error 439 err = f.pacer.Call(func() (bool, error) { 440 resp, err = f.srv.CallJSON(ctx, &opts, &request, nil) 441 return f.shouldRetry(resp, err) 442 }) 443 if err != nil { 444 if resp != nil { 445 if resp.StatusCode == 401 || resp.StatusCode == 403 { 446 return fs.ErrorPermissionDenied 447 } 448 if resp.StatusCode == 404 { 449 return fs.ErrorObjectNotFound 450 } 451 } 452 return errors.Wrap(err, fmt.Sprintf("failed to move directory '%s' from '%s' to '%s'", srcName, srcDir, dstPath)) 453 } 454 455 return nil 456 } 457 458 func (f *Fs) deleteDir(ctx context.Context, libraryID, filePath string) error { 459 // API Documentation 460 // https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-Delete%20Directory 461 if libraryID == "" { 462 return errors.New("cannot delete directory without a library") 463 } 464 filePath = path.Join("/", filePath) 465 466 opts := rest.Opts{ 467 Method: "DELETE", 468 Path: APIv20 + libraryID + "/dir/", 469 Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}}, 470 NoResponse: true, 471 } 472 473 var resp *http.Response 474 var err error 475 err = f.pacer.Call(func() (bool, error) { 476 resp, err = f.srv.CallJSON(ctx, &opts, nil, nil) 477 return f.shouldRetry(resp, err) 478 }) 479 if err != nil { 480 if resp != nil { 481 if resp.StatusCode == 401 || resp.StatusCode == 403 { 482 return fs.ErrorPermissionDenied 483 } 484 } 485 return errors.Wrap(err, "failed to delete directory") 486 } 487 return nil 488 } 489 490 func (f *Fs) getFileDetails(ctx context.Context, libraryID, filePath string) (*api.FileDetail, error) { 491 // API Documentation 492 // https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Get%20File%20Detail 493 if libraryID == "" { 494 return nil, errors.New("cannot open file without a library") 495 } 496 filePath = path.Join("/", filePath) 497 498 opts := rest.Opts{ 499 Method: "GET", 500 Path: APIv20 + libraryID + "/file/detail/", 501 Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}}, 502 } 503 result := &api.FileDetail{} 504 var resp *http.Response 505 var err error 506 err = f.pacer.Call(func() (bool, error) { 507 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 508 return f.shouldRetry(resp, err) 509 }) 510 if err != nil { 511 if resp != nil { 512 if resp.StatusCode == 404 { 513 return nil, fs.ErrorObjectNotFound 514 } 515 if resp.StatusCode == 401 || resp.StatusCode == 403 { 516 return nil, fs.ErrorPermissionDenied 517 } 518 } 519 return nil, errors.Wrap(err, "failed to get file details") 520 } 521 result.Name = f.opt.Enc.ToStandardName(result.Name) 522 result.Parent = f.opt.Enc.ToStandardPath(result.Parent) 523 return result, nil 524 } 525 526 func (f *Fs) deleteFile(ctx context.Context, libraryID, filePath string) error { 527 // API Documentation 528 // https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Delete%20File 529 if libraryID == "" { 530 return errors.New("cannot delete file without a library") 531 } 532 filePath = path.Join("/", filePath) 533 534 opts := rest.Opts{ 535 Method: "DELETE", 536 Path: APIv20 + libraryID + "/file/", 537 Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}}, 538 NoResponse: true, 539 } 540 err := f.pacer.Call(func() (bool, error) { 541 resp, err := f.srv.CallJSON(ctx, &opts, nil, nil) 542 return f.shouldRetry(resp, err) 543 }) 544 if err != nil { 545 return errors.Wrap(err, "failed to delete file") 546 } 547 return nil 548 } 549 550 func (f *Fs) getDownloadLink(ctx context.Context, libraryID, filePath string) (string, error) { 551 // API Documentation 552 // https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Download%20File 553 if libraryID == "" { 554 return "", errors.New("cannot download file without a library") 555 } 556 filePath = path.Join("/", filePath) 557 558 opts := rest.Opts{ 559 Method: "GET", 560 Path: APIv20 + libraryID + "/file/", 561 Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}}, 562 } 563 result := "" 564 var resp *http.Response 565 var err error 566 err = f.pacer.Call(func() (bool, error) { 567 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 568 return f.shouldRetry(resp, err) 569 }) 570 if err != nil { 571 if resp != nil { 572 if resp.StatusCode == 404 { 573 return "", fs.ErrorObjectNotFound 574 } 575 } 576 return "", errors.Wrap(err, "failed to get download link") 577 } 578 return result, nil 579 } 580 581 func (f *Fs) download(ctx context.Context, url string, size int64, options ...fs.OpenOption) (io.ReadCloser, error) { 582 // Check if we need to download partial content 583 var start, end int64 = 0, size 584 partialContent := false 585 for _, option := range options { 586 switch x := option.(type) { 587 case *fs.SeekOption: 588 start = x.Offset 589 partialContent = true 590 case *fs.RangeOption: 591 if x.Start >= 0 { 592 start = x.Start 593 if x.End > 0 && x.End < size { 594 end = x.End + 1 595 } 596 } else { 597 // {-1, 20} should load the last 20 characters [len-20:len] 598 start = size - x.End 599 } 600 partialContent = true 601 default: 602 if option.Mandatory() { 603 fs.Logf(nil, "Unsupported mandatory option: %v", option) 604 } 605 } 606 } 607 // Build the http request 608 opts := rest.Opts{ 609 Method: "GET", 610 RootURL: url, 611 Options: options, 612 } 613 var resp *http.Response 614 var err error 615 err = f.pacer.Call(func() (bool, error) { 616 resp, err = f.srv.Call(ctx, &opts) 617 return f.shouldRetry(resp, err) 618 }) 619 if err != nil { 620 if resp != nil { 621 if resp.StatusCode == 404 { 622 return nil, fmt.Errorf("file not found '%s'", url) 623 } 624 } 625 return nil, err 626 } 627 // Non-encrypted libraries are accepting the HTTP Range header, 628 // BUT encrypted libraries are simply ignoring it 629 if partialContent && resp.StatusCode == 200 { 630 // Partial content was requested through a Range header, but a full content was sent instead 631 rangeDownloadNotice.Do(func() { 632 fs.Logf(nil, "%s ignored our request of partial content. This is probably because encrypted libraries are not accepting range requests. Loading this file might be slow!", f.String()) 633 }) 634 if start > 0 { 635 // We need to read and discard the beginning of the data... 636 _, err = io.CopyN(ioutil.Discard, resp.Body, start) 637 if err != nil { 638 return nil, err 639 } 640 } 641 // ... and return a limited reader for the remaining of the data 642 return readers.NewLimitedReadCloser(resp.Body, end-start), nil 643 } 644 return resp.Body, nil 645 } 646 647 func (f *Fs) getUploadLink(ctx context.Context, libraryID string) (string, error) { 648 // API Documentation 649 // https://download.seafile.com/published/web-api/v2.1/file-upload.md 650 if libraryID == "" { 651 return "", errors.New("cannot upload file without a library") 652 } 653 opts := rest.Opts{ 654 Method: "GET", 655 Path: APIv20 + libraryID + "/upload-link/", 656 } 657 result := "" 658 var resp *http.Response 659 var err error 660 err = f.pacer.Call(func() (bool, error) { 661 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 662 return f.shouldRetry(resp, err) 663 }) 664 if err != nil { 665 if resp != nil { 666 if resp.StatusCode == 401 || resp.StatusCode == 403 { 667 return "", fs.ErrorPermissionDenied 668 } 669 } 670 return "", errors.Wrap(err, "failed to get upload link") 671 } 672 return result, nil 673 } 674 675 func (f *Fs) upload(ctx context.Context, in io.Reader, uploadLink, filePath string) (*api.FileDetail, error) { 676 // API Documentation 677 // https://download.seafile.com/published/web-api/v2.1/file-upload.md 678 fileDir, filename := path.Split(filePath) 679 parameters := url.Values{ 680 "parent_dir": {"/"}, 681 "relative_path": {f.opt.Enc.FromStandardPath(fileDir)}, 682 "need_idx_progress": {"true"}, 683 "replace": {"1"}, 684 } 685 formReader, contentType, _, err := rest.MultipartUpload(in, parameters, "file", f.opt.Enc.FromStandardName(filename)) 686 if err != nil { 687 return nil, errors.Wrap(err, "failed to make multipart upload") 688 } 689 690 opts := rest.Opts{ 691 Method: "POST", 692 RootURL: uploadLink, 693 Body: formReader, 694 ContentType: contentType, 695 Parameters: url.Values{"ret-json": {"1"}}, // It needs to be on the url, not in the body parameters 696 } 697 result := make([]api.FileDetail, 1) 698 var resp *http.Response 699 // If an error occurs during the call, do not attempt to retry: The upload link is single use only 700 err = f.pacer.CallNoRetry(func() (bool, error) { 701 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 702 return f.shouldRetryUpload(ctx, resp, err) 703 }) 704 if err != nil { 705 if resp != nil { 706 if resp.StatusCode == 401 || resp.StatusCode == 403 { 707 return nil, fs.ErrorPermissionDenied 708 } 709 if resp.StatusCode == 500 { 710 // This is a temporary error - we will get a new upload link before retrying 711 return nil, ErrorInternalDuringUpload 712 } 713 } 714 return nil, errors.Wrap(err, "failed to upload file") 715 } 716 if len(result) > 0 { 717 result[0].Parent = f.opt.Enc.ToStandardPath(result[0].Parent) 718 result[0].Name = f.opt.Enc.ToStandardName(result[0].Name) 719 return &result[0], nil 720 } 721 return nil, nil 722 } 723 724 func (f *Fs) listShareLinks(ctx context.Context, libraryID, remote string) ([]api.SharedLink, error) { 725 // API Documentation 726 // https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-List%20Share%20Link%20of%20a%20Folder%20(File) 727 if libraryID == "" { 728 return nil, errors.New("cannot get share links without a library") 729 } 730 remote = path.Join("/", remote) 731 732 opts := rest.Opts{ 733 Method: "GET", 734 Path: "api/v2.1/share-links/", 735 Parameters: url.Values{"repo_id": {libraryID}, "path": {f.opt.Enc.FromStandardPath(remote)}}, 736 } 737 result := make([]api.SharedLink, 1) 738 var resp *http.Response 739 var err error 740 err = f.pacer.Call(func() (bool, error) { 741 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 742 return f.shouldRetry(resp, err) 743 }) 744 if err != nil { 745 if resp != nil { 746 if resp.StatusCode == 401 || resp.StatusCode == 403 { 747 return nil, fs.ErrorPermissionDenied 748 } 749 if resp.StatusCode == 404 { 750 return nil, fs.ErrorObjectNotFound 751 } 752 } 753 return nil, errors.Wrap(err, "failed to list shared links") 754 } 755 return result, nil 756 } 757 758 // createShareLink will only work with non-encrypted libraries 759 func (f *Fs) createShareLink(ctx context.Context, libraryID, remote string) (*api.SharedLink, error) { 760 // API Documentation 761 // https://download.seafile.com/published/web-api/v2.1/share-links.md#user-content-Create%20Share%20Link 762 if libraryID == "" { 763 return nil, errors.New("cannot create a shared link without a library") 764 } 765 remote = path.Join("/", remote) 766 767 opts := rest.Opts{ 768 Method: "POST", 769 Path: "api/v2.1/share-links/", 770 } 771 request := &api.ShareLinkRequest{ 772 LibraryID: libraryID, 773 Path: f.opt.Enc.FromStandardPath(remote), 774 } 775 result := &api.SharedLink{} 776 var resp *http.Response 777 var err error 778 err = f.pacer.Call(func() (bool, error) { 779 resp, err = f.srv.CallJSON(ctx, &opts, &request, &result) 780 return f.shouldRetry(resp, err) 781 }) 782 if err != nil { 783 if resp != nil { 784 if resp.StatusCode == 401 || resp.StatusCode == 403 { 785 return nil, fs.ErrorPermissionDenied 786 } 787 if resp.StatusCode == 404 { 788 return nil, fs.ErrorObjectNotFound 789 } 790 } 791 return nil, errors.Wrap(err, "failed to create a shared link") 792 } 793 return result, nil 794 } 795 796 func (f *Fs) copyFile(ctx context.Context, srcLibraryID, srcPath, dstLibraryID, dstPath string) (*api.FileInfo, error) { 797 // API Documentation 798 // https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Copy%20File 799 // It's using the api/v2.1 which is not in the documentation (as of Apr 2020) but works better than api2 800 if srcLibraryID == "" || dstLibraryID == "" { 801 return nil, errors.New("libraryID and/or file path argument(s) missing") 802 } 803 srcPath = path.Join("/", srcPath) 804 dstPath = path.Join("/", dstPath) 805 806 opts := rest.Opts{ 807 Method: "POST", 808 Path: APIv21 + srcLibraryID + "/file/", 809 Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(srcPath)}}, 810 } 811 request := &api.FileOperationRequest{ 812 Operation: api.CopyFileOperation, 813 DestinationLibraryID: dstLibraryID, 814 DestinationPath: f.opt.Enc.FromStandardPath(dstPath), 815 } 816 result := &api.FileInfo{} 817 var resp *http.Response 818 var err error 819 err = f.pacer.Call(func() (bool, error) { 820 resp, err = f.srv.CallJSON(ctx, &opts, &request, &result) 821 return f.shouldRetry(resp, err) 822 }) 823 if err != nil { 824 if resp != nil { 825 if resp.StatusCode == 401 || resp.StatusCode == 403 { 826 return nil, fs.ErrorPermissionDenied 827 } 828 if resp.StatusCode == 404 { 829 fs.Debugf(nil, "Copy: %s", err) 830 return nil, fs.ErrorObjectNotFound 831 } 832 } 833 return nil, errors.Wrap(err, fmt.Sprintf("failed to copy file %s:'%s' to %s:'%s'", srcLibraryID, srcPath, dstLibraryID, dstPath)) 834 } 835 return f.decodeFileInfo(result), nil 836 } 837 838 func (f *Fs) moveFile(ctx context.Context, srcLibraryID, srcPath, dstLibraryID, dstPath string) (*api.FileInfo, error) { 839 // API Documentation 840 // https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Move%20File 841 // It's using the api/v2.1 which is not in the documentation (as of Apr 2020) but works better than api2 842 if srcLibraryID == "" || dstLibraryID == "" { 843 return nil, errors.New("libraryID and/or file path argument(s) missing") 844 } 845 srcPath = path.Join("/", srcPath) 846 dstPath = path.Join("/", dstPath) 847 848 opts := rest.Opts{ 849 Method: "POST", 850 Path: APIv21 + srcLibraryID + "/file/", 851 Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(srcPath)}}, 852 } 853 request := &api.FileOperationRequest{ 854 Operation: api.MoveFileOperation, 855 DestinationLibraryID: dstLibraryID, 856 DestinationPath: f.opt.Enc.FromStandardPath(dstPath), 857 } 858 result := &api.FileInfo{} 859 var resp *http.Response 860 var err error 861 err = f.pacer.Call(func() (bool, error) { 862 resp, err = f.srv.CallJSON(ctx, &opts, &request, &result) 863 return f.shouldRetry(resp, err) 864 }) 865 if err != nil { 866 if resp != nil { 867 if resp.StatusCode == 401 || resp.StatusCode == 403 { 868 return nil, fs.ErrorPermissionDenied 869 } 870 if resp.StatusCode == 404 { 871 fs.Debugf(nil, "Move: %s", err) 872 return nil, fs.ErrorObjectNotFound 873 } 874 } 875 return nil, errors.Wrap(err, fmt.Sprintf("failed to move file %s:'%s' to %s:'%s'", srcLibraryID, srcPath, dstLibraryID, dstPath)) 876 } 877 return f.decodeFileInfo(result), nil 878 } 879 880 func (f *Fs) renameFile(ctx context.Context, libraryID, filePath, newname string) (*api.FileInfo, error) { 881 // API Documentation 882 // https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Rename%20File 883 // It's using the api/v2.1 which is not in the documentation (as of Apr 2020) but works better than api2 884 if libraryID == "" || newname == "" { 885 return nil, errors.New("libraryID and/or file path argument(s) missing") 886 } 887 filePath = path.Join("/", filePath) 888 889 opts := rest.Opts{ 890 Method: "POST", 891 Path: APIv21 + libraryID + "/file/", 892 Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}}, 893 } 894 request := &api.FileOperationRequest{ 895 Operation: api.RenameFileOperation, 896 NewName: f.opt.Enc.FromStandardName(newname), 897 } 898 result := &api.FileInfo{} 899 var resp *http.Response 900 var err error 901 err = f.pacer.Call(func() (bool, error) { 902 resp, err = f.srv.CallJSON(ctx, &opts, &request, &result) 903 return f.shouldRetry(resp, err) 904 }) 905 if err != nil { 906 if resp != nil { 907 if resp.StatusCode == 401 || resp.StatusCode == 403 { 908 return nil, fs.ErrorPermissionDenied 909 } 910 if resp.StatusCode == 404 { 911 fs.Debugf(nil, "Rename: %s", err) 912 return nil, fs.ErrorObjectNotFound 913 } 914 } 915 return nil, errors.Wrap(err, fmt.Sprintf("failed to rename file '%s' to '%s'", filePath, newname)) 916 } 917 return f.decodeFileInfo(result), nil 918 } 919 920 func (f *Fs) decodeFileInfo(input *api.FileInfo) *api.FileInfo { 921 input.Name = f.opt.Enc.ToStandardName(input.Name) 922 input.Path = f.opt.Enc.ToStandardPath(input.Path) 923 return input 924 } 925 926 func (f *Fs) emptyLibraryTrash(ctx context.Context, libraryID string) error { 927 // API Documentation 928 // https://download.seafile.com/published/web-api/v2.1/libraries.md#user-content-Clean%20Library%20Trash 929 if libraryID == "" { 930 return errors.New("cannot clean up trash without a library") 931 } 932 opts := rest.Opts{ 933 Method: "DELETE", 934 Path: APIv21 + libraryID + "/trash/", 935 NoResponse: true, 936 } 937 var resp *http.Response 938 var err error 939 err = f.pacer.Call(func() (bool, error) { 940 resp, err = f.srv.CallJSON(ctx, &opts, nil, nil) 941 return f.shouldRetry(resp, err) 942 }) 943 if err != nil { 944 if resp != nil { 945 if resp.StatusCode == 401 || resp.StatusCode == 403 { 946 return fs.ErrorPermissionDenied 947 } 948 if resp.StatusCode == 404 { 949 return fs.ErrorObjectNotFound 950 } 951 } 952 return errors.Wrap(err, "failed empty the library trash") 953 } 954 return nil 955 } 956 957 // === API v2 from the official documentation, but that have been replaced by the much better v2.1 (undocumented as of Apr 2020) 958 // === getDirectoryEntriesAPIv2 is needed to keep compatibility with seafile v6, 959 // === the others can probably be removed after the API v2.1 is documented 960 961 func (f *Fs) getDirectoryEntriesAPIv2(ctx context.Context, libraryID, dirPath string) ([]api.DirEntry, error) { 962 // API Documentation 963 // https://download.seafile.com/published/web-api/v2.1/directories.md#user-content-List%20Items%20in%20Directory 964 if libraryID == "" { 965 return nil, errors.New("cannot list files without a library") 966 } 967 dirPath = path.Join("/", dirPath) 968 969 opts := rest.Opts{ 970 Method: "GET", 971 Path: APIv20 + libraryID + "/dir/", 972 Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(dirPath)}}, 973 } 974 result := make([]api.DirEntry, 1) 975 var resp *http.Response 976 var err error 977 err = f.pacer.Call(func() (bool, error) { 978 resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) 979 return f.shouldRetry(resp, err) 980 }) 981 if err != nil { 982 if resp != nil { 983 if resp.StatusCode == 401 || resp.StatusCode == 403 { 984 return nil, fs.ErrorPermissionDenied 985 } 986 if resp.StatusCode == 404 { 987 return nil, fs.ErrorDirNotFound 988 } 989 if resp.StatusCode == 440 { 990 // Encrypted library and password not provided 991 return nil, fs.ErrorPermissionDenied 992 } 993 } 994 return nil, errors.Wrap(err, "failed to get directory contents") 995 } 996 997 // Clean up encoded names 998 for index, fileInfo := range result { 999 fileInfo.Name = f.opt.Enc.ToStandardName(fileInfo.Name) 1000 fileInfo.Path = f.opt.Enc.ToStandardPath(fileInfo.Path) 1001 result[index] = fileInfo 1002 } 1003 return result, nil 1004 } 1005 1006 func (f *Fs) copyFileAPIv2(ctx context.Context, srcLibraryID, srcPath, dstLibraryID, dstPath string) (*api.FileInfo, error) { 1007 // API Documentation 1008 // https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Copy%20File 1009 if srcLibraryID == "" || dstLibraryID == "" { 1010 return nil, errors.New("libraryID and/or file path argument(s) missing") 1011 } 1012 srcPath = path.Join("/", srcPath) 1013 dstPath = path.Join("/", dstPath) 1014 1015 // Older API does not seem to accept JSON input here either 1016 postParameters := url.Values{ 1017 "operation": {"copy"}, 1018 "dst_repo": {dstLibraryID}, 1019 "dst_dir": {f.opt.Enc.FromStandardPath(dstPath)}, 1020 } 1021 opts := rest.Opts{ 1022 Method: "POST", 1023 Path: APIv20 + srcLibraryID + "/file/", 1024 Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(srcPath)}}, 1025 ContentType: "application/x-www-form-urlencoded", 1026 Body: bytes.NewBuffer([]byte(postParameters.Encode())), 1027 } 1028 result := &api.FileInfo{} 1029 var resp *http.Response 1030 var err error 1031 err = f.pacer.Call(func() (bool, error) { 1032 resp, err = f.srv.Call(ctx, &opts) 1033 return f.shouldRetry(resp, err) 1034 }) 1035 if err != nil { 1036 if resp != nil { 1037 if resp.StatusCode == 401 || resp.StatusCode == 403 { 1038 return nil, fs.ErrorPermissionDenied 1039 } 1040 } 1041 return nil, errors.Wrap(err, fmt.Sprintf("failed to copy file %s:'%s' to %s:'%s'", srcLibraryID, srcPath, dstLibraryID, dstPath)) 1042 } 1043 err = rest.DecodeJSON(resp, &result) 1044 if err != nil { 1045 return nil, err 1046 } 1047 return f.decodeFileInfo(result), nil 1048 } 1049 1050 func (f *Fs) renameFileAPIv2(ctx context.Context, libraryID, filePath, newname string) error { 1051 // API Documentation 1052 // https://download.seafile.com/published/web-api/v2.1/file.md#user-content-Rename%20File 1053 if libraryID == "" || newname == "" { 1054 return errors.New("libraryID and/or file path argument(s) missing") 1055 } 1056 filePath = path.Join("/", filePath) 1057 1058 // No luck with JSON input with the older api2 1059 postParameters := url.Values{ 1060 "operation": {"rename"}, 1061 "reloaddir": {"true"}, // This is an undocumented trick to avoid an http code 301 response (found in https://github.com/haiwen/seahub/blob/master/seahub/api2/views.py) 1062 "newname": {f.opt.Enc.FromStandardName(newname)}, 1063 } 1064 1065 opts := rest.Opts{ 1066 Method: "POST", 1067 Path: APIv20 + libraryID + "/file/", 1068 Parameters: url.Values{"p": {f.opt.Enc.FromStandardPath(filePath)}}, 1069 ContentType: "application/x-www-form-urlencoded", 1070 Body: bytes.NewBuffer([]byte(postParameters.Encode())), 1071 NoRedirect: true, 1072 NoResponse: true, 1073 } 1074 var resp *http.Response 1075 var err error 1076 err = f.pacer.Call(func() (bool, error) { 1077 resp, err = f.srv.Call(ctx, &opts) 1078 return f.shouldRetry(resp, err) 1079 }) 1080 if err != nil { 1081 if resp != nil { 1082 if resp.StatusCode == 301 { 1083 // This is the normal response from the server 1084 return nil 1085 } 1086 if resp.StatusCode == 401 || resp.StatusCode == 403 { 1087 return fs.ErrorPermissionDenied 1088 } 1089 if resp.StatusCode == 404 { 1090 return fs.ErrorObjectNotFound 1091 } 1092 } 1093 return errors.Wrap(err, "failed to rename file") 1094 } 1095 return nil 1096 }