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