github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/fichier/api.go (about) 1 package fichier 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "net/url" 10 "regexp" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/rclone/rclone/fs" 16 "github.com/rclone/rclone/fs/fserrors" 17 "github.com/rclone/rclone/lib/rest" 18 ) 19 20 // retryErrorCodes is a slice of error codes that we will retry 21 var retryErrorCodes = []int{ 22 429, // Too Many Requests. 23 403, // Forbidden (may happen when request limit is exceeded) 24 500, // Internal Server Error 25 502, // Bad Gateway 26 503, // Service Unavailable 27 504, // Gateway Timeout 28 509, // Bandwidth Limit Exceeded 29 } 30 31 var errorRegex = regexp.MustCompile(`#(\d{1,3})`) 32 33 func parseFichierError(err error) int { 34 matches := errorRegex.FindStringSubmatch(err.Error()) 35 if len(matches) == 0 { 36 return 0 37 } 38 code, err := strconv.Atoi(matches[1]) 39 if err != nil { 40 fs.Debugf(nil, "failed parsing fichier error: %v", err) 41 return 0 42 } 43 return code 44 } 45 46 // shouldRetry returns a boolean as to whether this resp and err 47 // deserve to be retried. It returns the err as a convenience 48 func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) { 49 if fserrors.ContextError(ctx, &err) { 50 return false, err 51 } 52 // 1Fichier uses HTTP error code 403 (Forbidden) for all kinds of errors with 53 // responses looking like this: "{\"message\":\"Flood detected: IP Locked #374\",\"status\":\"KO\"}" 54 // 55 // We attempt to parse the actual 1Fichier error code from this body and handle it accordingly 56 // Most importantly #374 (Flood detected: IP locked) which the integration tests provoke 57 // The list below is far from complete and should be expanded if we see any more error codes. 58 if err != nil { 59 switch parseFichierError(err) { 60 case 93: 61 return false, err // No such user 62 case 186: 63 return false, err // IP blocked? 64 case 374: 65 fs.Debugf(nil, "Sleeping for 30 seconds due to: %v", err) 66 time.Sleep(30 * time.Second) 67 default: 68 } 69 } 70 return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err 71 } 72 73 var isAlphaNumeric = regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString 74 75 func (f *Fs) createObject(ctx context.Context, remote string) (o *Object, leaf string, directoryID string, err error) { 76 // Create the directory for the object if it doesn't exist 77 leaf, directoryID, err = f.dirCache.FindPath(ctx, remote, true) 78 if err != nil { 79 return 80 } 81 // Temporary Object under construction 82 o = &Object{ 83 fs: f, 84 remote: remote, 85 } 86 return o, leaf, directoryID, nil 87 } 88 89 func (f *Fs) readFileInfo(ctx context.Context, url string) (*File, error) { 90 request := FileInfoRequest{ 91 URL: url, 92 } 93 opts := rest.Opts{ 94 Method: "POST", 95 Path: "/file/info.cgi", 96 } 97 98 var file File 99 err := f.pacer.Call(func() (bool, error) { 100 resp, err := f.rest.CallJSON(ctx, &opts, &request, &file) 101 return shouldRetry(ctx, resp, err) 102 }) 103 if err != nil { 104 return nil, fmt.Errorf("couldn't read file info: %w", err) 105 } 106 107 return &file, err 108 } 109 110 // maybe do some actual validation later if necessary 111 func validToken(token *GetTokenResponse) bool { 112 return token.Status == "OK" 113 } 114 115 func (f *Fs) getDownloadToken(ctx context.Context, url string) (*GetTokenResponse, error) { 116 request := DownloadRequest{ 117 URL: url, 118 Single: 1, 119 Pass: f.opt.FilePassword, 120 } 121 if f.opt.CDN { 122 request.CDN = 1 123 } 124 opts := rest.Opts{ 125 Method: "POST", 126 Path: "/download/get_token.cgi", 127 } 128 129 var token GetTokenResponse 130 err := f.pacer.Call(func() (bool, error) { 131 resp, err := f.rest.CallJSON(ctx, &opts, &request, &token) 132 doretry, err := shouldRetry(ctx, resp, err) 133 return doretry || !validToken(&token), err 134 }) 135 if err != nil { 136 return nil, fmt.Errorf("couldn't list files: %w", err) 137 } 138 139 return &token, nil 140 } 141 142 func fileFromSharedFile(file *SharedFile) File { 143 return File{ 144 URL: file.Link, 145 Filename: file.Filename, 146 Size: file.Size, 147 } 148 } 149 150 func (f *Fs) listSharedFiles(ctx context.Context, id string) (entries fs.DirEntries, err error) { 151 opts := rest.Opts{ 152 Method: "GET", 153 RootURL: "https://1fichier.com/dir/", 154 Path: id, 155 Parameters: map[string][]string{"json": {"1"}}, 156 ContentType: "application/x-www-form-urlencoded", 157 } 158 if f.opt.FolderPassword != "" { 159 opts.Method = "POST" 160 opts.Parameters = nil 161 opts.Body = strings.NewReader("json=1&pass=" + url.QueryEscape(f.opt.FolderPassword)) 162 } 163 164 var sharedFiles SharedFolderResponse 165 err = f.pacer.Call(func() (bool, error) { 166 resp, err := f.rest.CallJSON(ctx, &opts, nil, &sharedFiles) 167 return shouldRetry(ctx, resp, err) 168 }) 169 if err != nil { 170 return nil, fmt.Errorf("couldn't list files: %w", err) 171 } 172 173 entries = make([]fs.DirEntry, len(sharedFiles)) 174 175 for i, sharedFile := range sharedFiles { 176 entries[i] = f.newObjectFromFile(ctx, "", fileFromSharedFile(&sharedFile)) 177 } 178 179 return entries, nil 180 } 181 182 func (f *Fs) listFiles(ctx context.Context, directoryID int) (filesList *FilesList, err error) { 183 // fs.Debugf(f, "Requesting files for dir `%s`", directoryID) 184 request := ListFilesRequest{ 185 FolderID: directoryID, 186 } 187 188 opts := rest.Opts{ 189 Method: "POST", 190 Path: "/file/ls.cgi", 191 } 192 193 filesList = &FilesList{} 194 err = f.pacer.Call(func() (bool, error) { 195 resp, err := f.rest.CallJSON(ctx, &opts, &request, filesList) 196 return shouldRetry(ctx, resp, err) 197 }) 198 if err != nil { 199 return nil, fmt.Errorf("couldn't list files: %w", err) 200 } 201 for i := range filesList.Items { 202 item := &filesList.Items[i] 203 item.Filename = f.opt.Enc.ToStandardName(item.Filename) 204 } 205 206 return filesList, nil 207 } 208 209 func (f *Fs) listFolders(ctx context.Context, directoryID int) (foldersList *FoldersList, err error) { 210 // fs.Debugf(f, "Requesting folders for id `%s`", directoryID) 211 212 request := ListFolderRequest{ 213 FolderID: directoryID, 214 } 215 216 opts := rest.Opts{ 217 Method: "POST", 218 Path: "/folder/ls.cgi", 219 } 220 221 foldersList = &FoldersList{} 222 err = f.pacer.Call(func() (bool, error) { 223 resp, err := f.rest.CallJSON(ctx, &opts, &request, foldersList) 224 return shouldRetry(ctx, resp, err) 225 }) 226 if err != nil { 227 return nil, fmt.Errorf("couldn't list folders: %w", err) 228 } 229 foldersList.Name = f.opt.Enc.ToStandardName(foldersList.Name) 230 for i := range foldersList.SubFolders { 231 folder := &foldersList.SubFolders[i] 232 folder.Name = f.opt.Enc.ToStandardName(folder.Name) 233 } 234 235 // fs.Debugf(f, "Got FoldersList for id `%s`", directoryID) 236 237 return foldersList, err 238 } 239 240 func (f *Fs) listDir(ctx context.Context, dir string) (entries fs.DirEntries, err error) { 241 directoryID, err := f.dirCache.FindDir(ctx, dir, false) 242 if err != nil { 243 return nil, err 244 } 245 246 folderID, err := strconv.Atoi(directoryID) 247 if err != nil { 248 return nil, err 249 } 250 251 files, err := f.listFiles(ctx, folderID) 252 if err != nil { 253 return nil, err 254 } 255 256 folders, err := f.listFolders(ctx, folderID) 257 if err != nil { 258 return nil, err 259 } 260 261 entries = make([]fs.DirEntry, len(files.Items)+len(folders.SubFolders)) 262 263 for i, item := range files.Items { 264 entries[i] = f.newObjectFromFile(ctx, dir, item) 265 } 266 267 for i, folder := range folders.SubFolders { 268 createDate, err := time.Parse("2006-01-02 15:04:05", folder.CreateDate) 269 if err != nil { 270 return nil, err 271 } 272 273 fullPath := getRemote(dir, folder.Name) 274 folderID := strconv.Itoa(folder.ID) 275 276 entries[len(files.Items)+i] = fs.NewDir(fullPath, createDate).SetID(folderID) 277 278 // fs.Debugf(f, "Put Path `%s` for id `%d` into dircache", fullPath, folder.ID) 279 f.dirCache.Put(fullPath, folderID) 280 } 281 282 return entries, nil 283 } 284 285 func (f *Fs) newObjectFromFile(ctx context.Context, dir string, item File) *Object { 286 return &Object{ 287 fs: f, 288 remote: getRemote(dir, item.Filename), 289 file: item, 290 } 291 } 292 293 func getRemote(dir, fileName string) string { 294 if dir == "" { 295 return fileName 296 } 297 298 return dir + "/" + fileName 299 } 300 301 func (f *Fs) makeFolder(ctx context.Context, leaf string, folderID int) (response *MakeFolderResponse, err error) { 302 name := f.opt.Enc.FromStandardName(leaf) 303 // fs.Debugf(f, "Creating folder `%s` in id `%s`", name, directoryID) 304 305 request := MakeFolderRequest{ 306 FolderID: folderID, 307 Name: name, 308 } 309 310 opts := rest.Opts{ 311 Method: "POST", 312 Path: "/folder/mkdir.cgi", 313 } 314 315 response = &MakeFolderResponse{} 316 err = f.pacer.Call(func() (bool, error) { 317 resp, err := f.rest.CallJSON(ctx, &opts, &request, response) 318 return shouldRetry(ctx, resp, err) 319 }) 320 if err != nil { 321 return nil, fmt.Errorf("couldn't create folder: %w", err) 322 } 323 324 // fs.Debugf(f, "Created Folder `%s` in id `%s`", name, directoryID) 325 326 return response, err 327 } 328 329 func (f *Fs) removeFolder(ctx context.Context, name string, folderID int) (response *GenericOKResponse, err error) { 330 // fs.Debugf(f, "Removing folder with id `%s`", directoryID) 331 332 request := &RemoveFolderRequest{ 333 FolderID: folderID, 334 } 335 336 opts := rest.Opts{ 337 Method: "POST", 338 Path: "/folder/rm.cgi", 339 } 340 341 response = &GenericOKResponse{} 342 var resp *http.Response 343 err = f.pacer.Call(func() (bool, error) { 344 resp, err = f.rest.CallJSON(ctx, &opts, request, response) 345 return shouldRetry(ctx, resp, err) 346 }) 347 if err != nil { 348 return nil, fmt.Errorf("couldn't remove folder: %w", err) 349 } 350 if response.Status != "OK" { 351 return nil, fmt.Errorf("can't remove folder: %s", response.Message) 352 } 353 354 // fs.Debugf(f, "Removed Folder with id `%s`", directoryID) 355 356 return response, nil 357 } 358 359 func (f *Fs) deleteFile(ctx context.Context, url string) (response *GenericOKResponse, err error) { 360 request := &RemoveFileRequest{ 361 Files: []RmFile{ 362 {url}, 363 }, 364 } 365 366 opts := rest.Opts{ 367 Method: "POST", 368 Path: "/file/rm.cgi", 369 } 370 371 response = &GenericOKResponse{} 372 err = f.pacer.Call(func() (bool, error) { 373 resp, err := f.rest.CallJSON(ctx, &opts, request, response) 374 return shouldRetry(ctx, resp, err) 375 }) 376 377 if err != nil { 378 return nil, fmt.Errorf("couldn't remove file: %w", err) 379 } 380 381 // fs.Debugf(f, "Removed file with url `%s`", url) 382 383 return response, nil 384 } 385 386 func (f *Fs) moveFile(ctx context.Context, url string, folderID int, rename string) (response *MoveFileResponse, err error) { 387 request := &MoveFileRequest{ 388 URLs: []string{url}, 389 FolderID: folderID, 390 Rename: rename, 391 } 392 393 opts := rest.Opts{ 394 Method: "POST", 395 Path: "/file/mv.cgi", 396 } 397 398 response = &MoveFileResponse{} 399 err = f.pacer.Call(func() (bool, error) { 400 resp, err := f.rest.CallJSON(ctx, &opts, request, response) 401 return shouldRetry(ctx, resp, err) 402 }) 403 404 if err != nil { 405 return nil, fmt.Errorf("couldn't copy file: %w", err) 406 } 407 408 return response, nil 409 } 410 411 func (f *Fs) moveDir(ctx context.Context, folderID int, newLeaf string, destinationFolderID int) (response *MoveDirResponse, err error) { 412 request := &MoveDirRequest{ 413 FolderID: folderID, 414 DestinationFolderID: destinationFolderID, 415 Rename: newLeaf, 416 // DestinationUser: destinationUser, 417 } 418 419 opts := rest.Opts{ 420 Method: "POST", 421 Path: "/folder/mv.cgi", 422 } 423 424 response = &MoveDirResponse{} 425 err = f.pacer.Call(func() (bool, error) { 426 resp, err := f.rest.CallJSON(ctx, &opts, request, response) 427 return shouldRetry(ctx, resp, err) 428 }) 429 430 if err != nil { 431 return nil, fmt.Errorf("couldn't move dir: %w", err) 432 } 433 434 return response, nil 435 } 436 437 func (f *Fs) copyFile(ctx context.Context, url string, folderID int, rename string) (response *CopyFileResponse, err error) { 438 request := &CopyFileRequest{ 439 URLs: []string{url}, 440 FolderID: folderID, 441 Rename: rename, 442 } 443 444 opts := rest.Opts{ 445 Method: "POST", 446 Path: "/file/cp.cgi", 447 } 448 449 response = &CopyFileResponse{} 450 err = f.pacer.Call(func() (bool, error) { 451 resp, err := f.rest.CallJSON(ctx, &opts, request, response) 452 return shouldRetry(ctx, resp, err) 453 }) 454 455 if err != nil { 456 return nil, fmt.Errorf("couldn't copy file: %w", err) 457 } 458 459 return response, nil 460 } 461 462 func (f *Fs) renameFile(ctx context.Context, url string, newName string) (response *RenameFileResponse, err error) { 463 request := &RenameFileRequest{ 464 URLs: []RenameFileURL{ 465 { 466 URL: url, 467 Filename: newName, 468 }, 469 }, 470 } 471 472 opts := rest.Opts{ 473 Method: "POST", 474 Path: "/file/rename.cgi", 475 } 476 477 response = &RenameFileResponse{} 478 err = f.pacer.Call(func() (bool, error) { 479 resp, err := f.rest.CallJSON(ctx, &opts, request, response) 480 return shouldRetry(ctx, resp, err) 481 }) 482 483 if err != nil { 484 return nil, fmt.Errorf("couldn't rename file: %w", err) 485 } 486 487 return response, nil 488 } 489 490 func (f *Fs) getUploadNode(ctx context.Context) (response *GetUploadNodeResponse, err error) { 491 // fs.Debugf(f, "Requesting Upload node") 492 493 opts := rest.Opts{ 494 Method: "GET", 495 ContentType: "application/json", // 1Fichier API is bad 496 Path: "/upload/get_upload_server.cgi", 497 } 498 499 response = &GetUploadNodeResponse{} 500 err = f.pacer.Call(func() (bool, error) { 501 resp, err := f.rest.CallJSON(ctx, &opts, nil, response) 502 return shouldRetry(ctx, resp, err) 503 }) 504 if err != nil { 505 return nil, fmt.Errorf("didn't get an upload node: %w", err) 506 } 507 508 // fs.Debugf(f, "Got Upload node") 509 510 return response, err 511 } 512 513 func (f *Fs) uploadFile(ctx context.Context, in io.Reader, size int64, fileName, folderID, uploadID, node string, options ...fs.OpenOption) (response *http.Response, err error) { 514 // fs.Debugf(f, "Uploading File `%s`", fileName) 515 516 fileName = f.opt.Enc.FromStandardName(fileName) 517 518 if len(uploadID) > 10 || !isAlphaNumeric(uploadID) { 519 return nil, errors.New("invalid UploadID") 520 } 521 522 opts := rest.Opts{ 523 Method: "POST", 524 Path: "/upload.cgi", 525 Parameters: map[string][]string{ 526 "id": {uploadID}, 527 }, 528 NoResponse: true, 529 Body: in, 530 ContentLength: &size, 531 Options: options, 532 MultipartContentName: "file[]", 533 MultipartFileName: fileName, 534 MultipartParams: map[string][]string{ 535 "did": {folderID}, 536 }, 537 } 538 539 if node != "" { 540 opts.RootURL = "https://" + node 541 } 542 543 err = f.pacer.CallNoRetry(func() (bool, error) { 544 resp, err := f.rest.CallJSON(ctx, &opts, nil, nil) 545 return shouldRetry(ctx, resp, err) 546 }) 547 548 if err != nil { 549 return nil, fmt.Errorf("couldn't upload file: %w", err) 550 } 551 552 // fs.Debugf(f, "Uploaded File `%s`", fileName) 553 554 return response, err 555 } 556 557 func (f *Fs) endUpload(ctx context.Context, uploadID string, nodeurl string) (response *EndFileUploadResponse, err error) { 558 // fs.Debugf(f, "Ending File Upload `%s`", uploadID) 559 560 if len(uploadID) > 10 || !isAlphaNumeric(uploadID) { 561 return nil, errors.New("invalid UploadID") 562 } 563 564 opts := rest.Opts{ 565 Method: "GET", 566 Path: "/end.pl", 567 RootURL: "https://" + nodeurl, 568 Parameters: map[string][]string{ 569 "xid": {uploadID}, 570 }, 571 ExtraHeaders: map[string]string{ 572 "JSON": "1", 573 }, 574 } 575 576 response = &EndFileUploadResponse{} 577 err = f.pacer.Call(func() (bool, error) { 578 resp, err := f.rest.CallJSON(ctx, &opts, nil, response) 579 return shouldRetry(ctx, resp, err) 580 }) 581 582 if err != nil { 583 return nil, fmt.Errorf("couldn't finish file upload: %w", err) 584 } 585 586 return response, err 587 }