rclone-api 1.1.4__tar.gz → 1.1.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {rclone_api-1.1.4 → rclone_api-1.1.6}/PKG-INFO +1 -1
  2. {rclone_api-1.1.4 → rclone_api-1.1.6}/pyproject.toml +1 -1
  3. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/__init__.py +2 -1
  4. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/cmd/copy_large_s3.py +10 -7
  5. rclone_api-1.1.6/src/rclone_api/mount.py +115 -0
  6. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/rclone.py +33 -56
  7. rclone_api-1.1.6/src/rclone_api/s3/chunk_file.py +102 -0
  8. rclone_api-1.1.6/src/rclone_api/s3/chunk_types.py +254 -0
  9. rclone_api-1.1.6/src/rclone_api/s3/chunk_uploader.py +262 -0
  10. rclone_api-1.1.6/src/rclone_api/types.py +125 -0
  11. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/util.py +8 -0
  12. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api.egg-info/PKG-INFO +1 -1
  13. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api.egg-info/SOURCES.txt +4 -0
  14. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_mounted_ranged_download.py +2 -2
  15. rclone_api-1.1.6/tests/test_size_suffix.py +21 -0
  16. rclone_api-1.1.4/src/rclone_api/s3/chunk_uploader.py +0 -602
  17. rclone_api-1.1.4/src/rclone_api/types.py +0 -35
  18. {rclone_api-1.1.4 → rclone_api-1.1.6}/.aiderignore +0 -0
  19. {rclone_api-1.1.4 → rclone_api-1.1.6}/.github/workflows/lint.yml +0 -0
  20. {rclone_api-1.1.4 → rclone_api-1.1.6}/.github/workflows/push_macos.yml +0 -0
  21. {rclone_api-1.1.4 → rclone_api-1.1.6}/.github/workflows/push_ubuntu.yml +0 -0
  22. {rclone_api-1.1.4 → rclone_api-1.1.6}/.github/workflows/push_win.yml +0 -0
  23. {rclone_api-1.1.4 → rclone_api-1.1.6}/.gitignore +0 -0
  24. {rclone_api-1.1.4 → rclone_api-1.1.6}/.pylintrc +0 -0
  25. {rclone_api-1.1.4 → rclone_api-1.1.6}/.vscode/launch.json +0 -0
  26. {rclone_api-1.1.4 → rclone_api-1.1.6}/.vscode/settings.json +0 -0
  27. {rclone_api-1.1.4 → rclone_api-1.1.6}/.vscode/tasks.json +0 -0
  28. {rclone_api-1.1.4 → rclone_api-1.1.6}/LICENSE +0 -0
  29. {rclone_api-1.1.4 → rclone_api-1.1.6}/MANIFEST.in +0 -0
  30. {rclone_api-1.1.4 → rclone_api-1.1.6}/README.md +0 -0
  31. {rclone_api-1.1.4 → rclone_api-1.1.6}/clean +0 -0
  32. {rclone_api-1.1.4 → rclone_api-1.1.6}/install +0 -0
  33. {rclone_api-1.1.4 → rclone_api-1.1.6}/lint +0 -0
  34. {rclone_api-1.1.4 → rclone_api-1.1.6}/requirements.testing.txt +0 -0
  35. {rclone_api-1.1.4 → rclone_api-1.1.6}/setup.cfg +0 -0
  36. {rclone_api-1.1.4 → rclone_api-1.1.6}/setup.py +0 -0
  37. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/assets/example.txt +0 -0
  38. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/cli.py +0 -0
  39. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/cmd/list_files.py +0 -0
  40. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/completed_process.py +0 -0
  41. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/config.py +0 -0
  42. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/convert.py +0 -0
  43. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/deprecated.py +0 -0
  44. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/diff.py +0 -0
  45. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/dir.py +0 -0
  46. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/dir_listing.py +0 -0
  47. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/exec.py +0 -0
  48. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/experimental/flags.py +0 -0
  49. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/experimental/flags_base.py +0 -0
  50. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/file.py +0 -0
  51. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/filelist.py +0 -0
  52. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/group_files.py +0 -0
  53. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/process.py +0 -0
  54. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/remote.py +0 -0
  55. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/rpath.py +0 -0
  56. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/s3/api.py +0 -0
  57. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/s3/basic_ops.py +0 -0
  58. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/s3/create.py +0 -0
  59. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/s3/types.py +0 -0
  60. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/scan_missing_folders.py +0 -0
  61. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/walk.py +0 -0
  62. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api.egg-info/dependency_links.txt +0 -0
  63. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api.egg-info/entry_points.txt +0 -0
  64. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api.egg-info/requires.txt +0 -0
  65. {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api.egg-info/top_level.txt +0 -0
  66. {rclone_api-1.1.4 → rclone_api-1.1.6}/test +0 -0
  67. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/archive/test_paramiko.py.disabled +0 -0
  68. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_cmd_list_files.py +0 -0
  69. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_copy.py +0 -0
  70. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_copy_files.py +0 -0
  71. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_diff.py +0 -0
  72. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_group_files.py +0 -0
  73. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_is_synced.py +0 -0
  74. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_ls.py +0 -0
  75. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_mount.py +0 -0
  76. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_mount_s3.py +0 -0
  77. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_mount_webdav.py +0 -0
  78. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_obscure.py +0 -0
  79. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_rclone_config.py +0 -0
  80. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_remote_control.py +0 -0
  81. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_remotes.py +0 -0
  82. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_s3.py +0 -0
  83. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_scan_missing_folders.py +0 -0
  84. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_size_files.py +0 -0
  85. {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_walk.py +0 -0
  86. {rclone_api-1.1.4 → rclone_api-1.1.6}/tox.ini +0 -0
  87. {rclone_api-1.1.4 → rclone_api-1.1.6}/upload_package.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.1.4
3
+ Version: 1.1.6
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  License: BSD 3-Clause License
@@ -21,7 +21,7 @@ dependencies = [
21
21
  ]
22
22
 
23
23
  # Change this with the version number bump.
24
- version = "1.1.4"
24
+ version = "1.1.6"
25
25
 
26
26
  [tool.setuptools]
27
27
  package-dir = {"" = "src"}
@@ -10,7 +10,7 @@ from .rclone import Rclone, rclone_verbose
10
10
  from .remote import Remote
11
11
  from .rpath import RPath
12
12
  from .s3.types import MultiUploadResult
13
- from .types import ListingOption, Order, SizeResult
13
+ from .types import ListingOption, Order, SizeResult, SizeSuffix
14
14
 
15
15
  __all__ = [
16
16
  "Rclone",
@@ -34,4 +34,5 @@ __all__ = [
34
34
  "Parsed",
35
35
  "Section",
36
36
  "MultiUploadResult",
37
+ "SizeSuffix",
37
38
  ]
@@ -2,7 +2,7 @@ import argparse
2
2
  from dataclasses import dataclass
3
3
  from pathlib import Path
4
4
 
5
- from rclone_api import MultiUploadResult, Rclone
5
+ from rclone_api import MultiUploadResult, Rclone, SizeSuffix
6
6
 
7
7
  _1MB = 1024 * 1024
8
8
 
@@ -12,7 +12,7 @@ class Args:
12
12
  config_path: Path
13
13
  src: str
14
14
  dst: str
15
- chunk_size_mb: int
15
+ chunk_size_mb: SizeSuffix
16
16
  read_concurrent_chunks: int
17
17
  retries: int
18
18
  save_state_json: Path
@@ -35,17 +35,20 @@ def _parse_args() -> Args:
35
35
  "--config", help="Path to rclone config file", type=Path, required=True
36
36
  )
37
37
  parser.add_argument(
38
- "--chunk-size-mb", help="Chunk size in MB", type=int, default=256
38
+ "--chunk-size",
39
+ help="Chunk size that will be read and uploaded in in SizeSuffix (i.e. 128M = 128 megabytes) form",
40
+ type=str,
41
+ default="128M",
39
42
  )
40
43
  parser.add_argument(
41
44
  "--read-concurrent-chunks",
42
- help="Maximum number of chunks to read",
45
+ help="Maximum number of chunks to read in a look ahead cache",
43
46
  type=int,
44
- default=4,
47
+ default=1,
45
48
  )
46
49
  parser.add_argument("--retries", help="Number of retries", type=int, default=3)
47
50
  parser.add_argument(
48
- "--resumable-json",
51
+ "--resume-json",
49
52
  help="Path to resumable JSON file",
50
53
  type=Path,
51
54
  default="resume.json",
@@ -56,7 +59,7 @@ def _parse_args() -> Args:
56
59
  config_path=Path(args.config),
57
60
  src=args.src,
58
61
  dst=args.dst,
59
- chunk_size_mb=args.chunk_size_mb,
62
+ chunk_size_mb=SizeSuffix(args.chunk_size),
60
63
  read_concurrent_chunks=args.read_concurrent_chunks,
61
64
  retries=args.retries,
62
65
  save_state_json=args.resumable_json,
@@ -0,0 +1,115 @@
1
+ import platform
2
+ import subprocess
3
+ import time
4
+ import warnings
5
+ from pathlib import Path
6
+
7
+ _SYSTEM = platform.system() # "Linux", "Darwin", "Windows", etc.
8
+
9
+
10
+ def run_command(cmd: str, verbose: bool) -> int:
11
+ """Run a shell command and print its output if verbose is True."""
12
+ if verbose:
13
+ print(f"Executing: {cmd}")
14
+ try:
15
+ result = subprocess.run(
16
+ cmd, shell=True, capture_output=True, text=True, check=False
17
+ )
18
+ if result.returncode != 0 and verbose:
19
+ print(f"Command failed: {cmd}\nStdErr: {result.stderr.strip()}")
20
+ return result.returncode
21
+ except Exception as e:
22
+ warnings.warn(f"Error running command '{cmd}': {e}")
23
+ return -1
24
+
25
+
26
+ def prepare_mount(outdir: Path, verbose: bool) -> None:
27
+ if _SYSTEM == "Windows":
28
+ # Windows -> Must create parent directories only if they don't exist
29
+ if verbose:
30
+ print(f"Creating parent directories for {outdir}")
31
+ outdir.parent.mkdir(parents=True, exist_ok=True)
32
+ else:
33
+ # Linux -> Must create parent directories and the directory itself
34
+ if verbose:
35
+ print(f"Creating directories for {outdir}")
36
+ outdir.mkdir(parents=True, exist_ok=True)
37
+
38
+
39
+ def clean_mount(mount_path: Path, verbose: bool = False) -> None:
40
+ """
41
+ Clean up a mount path across Linux, macOS, and Windows.
42
+
43
+ The function attempts to unmount the mount at mount_path, then, if the
44
+ directory is empty, removes it. On Linux it uses 'fusermount -u' (for FUSE mounts)
45
+ and 'umount'. On macOS it uses 'umount' (and optionally 'diskutil unmount'),
46
+ while on Windows it attempts to remove the mount point via 'mountvol /D'.
47
+ """
48
+ # Check if the mount path exists; if an OSError occurs, assume it exists.
49
+ try:
50
+ mount_exists = mount_path.exists()
51
+ except OSError as e:
52
+ warnings.warn(f"Error checking {mount_path}: {e}")
53
+ mount_exists = True
54
+
55
+ # Give the system a moment (if unmount is in progress, etc.)
56
+ time.sleep(2)
57
+
58
+ if not mount_exists:
59
+ if verbose:
60
+ print(f"{mount_path} does not exist; nothing to clean up.")
61
+ return
62
+
63
+ if verbose:
64
+ print(f"{mount_path} still exists, attempting to unmount and remove.")
65
+
66
+ # Platform-specific unmount procedures
67
+ if _SYSTEM == "Linux":
68
+ # Try FUSE unmount first (if applicable), then the regular umount.
69
+ run_command(f"fusermount -u {mount_path}", verbose)
70
+ run_command(f"umount {mount_path}", verbose)
71
+ elif _SYSTEM == "Darwin":
72
+ # On macOS, use umount; optionally try diskutil for stubborn mounts.
73
+ run_command(f"umount {mount_path}", verbose)
74
+ # Optionally: uncomment the next line if diskutil unmount is preferred.
75
+ # run_command(f"diskutil unmount {mount_path}", verbose)
76
+ elif _SYSTEM == "Windows":
77
+ # On Windows, remove the mount point using mountvol.
78
+ run_command(f"mountvol {mount_path} /D", verbose)
79
+ # If that does not work, try to remove the directory directly.
80
+ try:
81
+ mount_path.rmdir()
82
+ if verbose:
83
+ print(f"Successfully removed mount directory {mount_path}")
84
+ except Exception as e:
85
+ warnings.warn(f"Failed to remove mount {mount_path}: {e}")
86
+ else:
87
+ warnings.warn(f"Unsupported platform: {_SYSTEM}")
88
+
89
+ # Allow some time for the unmount commands to take effect.
90
+ time.sleep(2)
91
+
92
+ # Re-check if the mount path still exists.
93
+ try:
94
+ still_exists = mount_path.exists()
95
+ except OSError as e:
96
+ warnings.warn(f"Error re-checking {mount_path}: {e}")
97
+ still_exists = True
98
+
99
+ if still_exists:
100
+ if verbose:
101
+ print(f"{mount_path} still exists after unmount attempt.")
102
+ # Attempt to remove the directory if it is empty.
103
+ try:
104
+ # Only remove if the directory is empty.
105
+ if not any(mount_path.iterdir()):
106
+ mount_path.rmdir()
107
+ if verbose:
108
+ print(f"Removed empty mount directory {mount_path}")
109
+ else:
110
+ warnings.warn(f"{mount_path} is not empty; cannot remove.")
111
+ except Exception as e:
112
+ warnings.warn(f"Failed during cleanup of {mount_path}: {e}")
113
+ else:
114
+ if verbose:
115
+ print(f"{mount_path} successfully cleaned up.")
@@ -34,7 +34,13 @@ from rclone_api.s3.types import (
34
34
  S3Provider,
35
35
  S3UploadTarget,
36
36
  )
37
- from rclone_api.types import ListingOption, ModTimeStrategy, Order, SizeResult
37
+ from rclone_api.types import (
38
+ ListingOption,
39
+ ModTimeStrategy,
40
+ Order,
41
+ SizeResult,
42
+ SizeSuffix,
43
+ )
38
44
  from rclone_api.util import (
39
45
  get_check,
40
46
  get_rclone_exe,
@@ -673,10 +679,13 @@ class Rclone:
673
679
  src: str,
674
680
  dst: str,
675
681
  save_state_json: Path,
676
- chunk_size: int = 16
677
- * 1024
678
- * 1024, # This setting will scale the performance of the upload
679
- concurrent_chunks: int = 4, # This setting will scale the performance of the upload
682
+ chunk_size: SizeSuffix | None = None,
683
+ # 16
684
+ # * 1024
685
+ # * 1024, # This setting will scale the performance of the upload
686
+ concurrent_chunks: (
687
+ int | None
688
+ ) = None, # This setting will scale the performance of the upload
680
689
  retries: int = 3,
681
690
  verbose: bool | None = None,
682
691
  max_chunks_before_suspension: int | None = None,
@@ -687,16 +696,22 @@ class Rclone:
687
696
  from rclone_api.s3.create import S3Credentials
688
697
  from rclone_api.util import S3PathInfo, random_str, split_s3_path
689
698
 
699
+ _tmp: SizeSuffix | str = chunk_size or "16MiB"
700
+ chunk_size = SizeSuffix(_tmp)
701
+ assert chunk_size is not None
702
+ concurrent_chunks = concurrent_chunks or 4
703
+ size_limit = SizeSuffix(chunk_size * concurrent_chunks)
704
+
690
705
  other_args: list[str] = [
691
706
  "--no-modtime",
692
707
  "--vfs-read-wait",
693
708
  "1s",
694
709
  "--vfs-disk-space-total-size",
695
- str(2 * chunk_size * concurrent_chunks), # purge quickly.
710
+ size_limit.as_str(), # purge quickly.
696
711
  "--vfs-read-chunk-size",
697
- str(chunk_size),
712
+ chunk_size.as_str(),
698
713
  "--vfs-read-chunk-size-limit",
699
- str(chunk_size * concurrent_chunks),
714
+ size_limit.as_str(),
700
715
  "--vfs-read-chunk-streams",
701
716
  str(concurrent_chunks),
702
717
  "--vfs-fast-fingerprint",
@@ -765,7 +780,7 @@ class Rclone:
765
780
 
766
781
  client = S3Client(s3_creds)
767
782
  config: S3MutliPartUploadConfig = S3MutliPartUploadConfig(
768
- chunk_size=chunk_size,
783
+ chunk_size=chunk_size.as_int(),
769
784
  retries=retries,
770
785
  resume_path_json=save_state_json,
771
786
  max_chunks_before_suspension=max_chunks_before_suspension,
@@ -788,7 +803,7 @@ class Rclone:
788
803
  )
789
804
 
790
805
  upload_config = S3MutliPartUploadConfig(
791
- chunk_size=chunk_size,
806
+ chunk_size=chunk_size.as_int(),
792
807
  retries=retries,
793
808
  resume_path_json=save_state_json,
794
809
  max_chunks_before_suspension=max_chunks_before_suspension,
@@ -845,24 +860,14 @@ class Rclone:
845
860
  Raises:
846
861
  subprocess.CalledProcessError: If the mount operation fails
847
862
  """
863
+ from rclone_api.mount import clean_mount, prepare_mount
864
+
848
865
  allow_writes = allow_writes or False
849
866
  use_links = use_links or True
850
867
  verbose = get_verbose(verbose)
851
868
  vfs_cache_mode = vfs_cache_mode or "full"
852
- if outdir.exists():
853
- is_empty = not list(outdir.iterdir())
854
- if not is_empty:
855
- raise ValueError(
856
- f"Mount directory already exists and is not empty: {outdir}"
857
- )
858
- outdir.rmdir()
859
-
860
- if _IS_WINDOWS:
861
- # Windows -> Must create parent directories only if they don't exist
862
- outdir.parent.mkdir(parents=True, exist_ok=True)
863
- else:
864
- # Linux -> Must create parent directories and the directory itself
865
- outdir.mkdir(parents=True, exist_ok=True)
869
+ clean_mount(outdir, verbose=verbose)
870
+ prepare_mount(outdir, verbose=verbose)
866
871
  src_str = convert_to_str(src)
867
872
  cmd_list: list[str] = ["mount", src_str, str(outdir)]
868
873
  if not allow_writes:
@@ -914,38 +919,10 @@ class Rclone:
914
919
  if proc.poll() is None:
915
920
  proc.terminate()
916
921
  proc.wait()
917
- if not error_happened and outdir.exists():
918
- time.sleep(2)
919
- if outdir.exists():
920
- print(f"{outdir} mount still exists, attempting to remove")
921
- if not _IS_WINDOWS:
922
-
923
- def exec(cmd: str) -> int:
924
- if verbose:
925
- print(f"Executing: {cmd}")
926
- rtn = os.system(cmd)
927
- if rtn != 0 and verbose:
928
- print(f"Failed to execute: {cmd}")
929
- return rtn
930
-
931
- exec(f"fusermount -u {outdir}")
932
- exec(f"umount {outdir}")
933
- time.sleep(2)
934
- if outdir.exists():
935
- is_empty = True
936
- try:
937
- is_empty = not list(outdir.iterdir())
938
- if not is_empty:
939
- warnings.warn(f"Failed to unmount {outdir}")
940
- else:
941
- try:
942
- outdir.rmdir()
943
- except Exception as e:
944
- warnings.warn(f"Failed to remove {outdir}: {e}")
945
- except Exception as e:
946
- warnings.warn(
947
- f"Failed during mount cleanup of {outdir}: because {e}"
948
- )
922
+ if not error_happened:
923
+ from rclone_api.mount import clean_mount
924
+
925
+ clean_mount(outdir, verbose=verbose)
949
926
 
950
927
  @deprecated("mount")
951
928
  def mount_webdav(
@@ -0,0 +1,102 @@
1
+ import time
2
+ import warnings
3
+ from pathlib import Path
4
+ from queue import Queue
5
+
6
+ from rclone_api.s3.chunk_types import FileChunk, UploadState
7
+ from rclone_api.util import locked_print
8
+
9
+
10
+ def _get_file_size(file_path: Path, timeout: int = 60) -> int:
11
+ sleep_time = timeout / 60 if timeout > 0 else 1
12
+ start = time.time()
13
+ while True:
14
+ expired = time.time() - start > timeout
15
+ try:
16
+ time.sleep(sleep_time)
17
+ if file_path.exists():
18
+ return file_path.stat().st_size
19
+ except FileNotFoundError as e:
20
+ if expired:
21
+ print(f"File not found: {file_path}, exception is {e}")
22
+ raise
23
+ if expired:
24
+ raise TimeoutError(f"File {file_path} not found after {timeout} seconds")
25
+
26
+
27
+ def file_chunker(
28
+ upload_state: UploadState, max_chunks: int | None, output: Queue[FileChunk | None]
29
+ ) -> None:
30
+ count = 0
31
+
32
+ def should_stop() -> bool:
33
+ nonlocal count
34
+ if max_chunks is None:
35
+ return False
36
+ if count >= max_chunks:
37
+ return True
38
+ count += 1
39
+ if count > 10 and count % 10 == 0:
40
+ # recheck that the file size has not changed
41
+ file_size = _get_file_size(upload_state.upload_info.src_file_path)
42
+ if file_size != upload_state.upload_info.file_size:
43
+ locked_print(
44
+ f"File size changed, cannot resume, expected {upload_state.upload_info.file_size}, got {file_size}"
45
+ )
46
+ raise ValueError("File size changed, cannot resume")
47
+ return False
48
+
49
+ upload_info = upload_state.upload_info
50
+ file_path = upload_info.src_file_path
51
+ chunk_size = upload_info.chunk_size
52
+ src = Path(file_path)
53
+ # Mounted files may take a while to appear, so keep retrying.
54
+
55
+ try:
56
+ file_size = _get_file_size(src, timeout=60)
57
+ part_number = 1
58
+ done_part_numbers: set[int] = {
59
+ p.part_number for p in upload_state.parts if p is not None
60
+ }
61
+ num_parts = upload_info.total_chunks()
62
+
63
+ def next_part_number() -> int | None:
64
+ nonlocal part_number
65
+ while part_number in done_part_numbers:
66
+ part_number += 1
67
+ if part_number > num_parts:
68
+ return None
69
+ return part_number
70
+
71
+ while not should_stop():
72
+ curr_parth_num = next_part_number()
73
+ if curr_parth_num is None:
74
+ locked_print(f"File {file_path} has completed chunking all parts")
75
+ break
76
+ assert curr_parth_num is not None
77
+ offset = (curr_parth_num - 1) * chunk_size
78
+
79
+ assert offset < file_size, f"Offset {offset} is greater than file size"
80
+
81
+ # Open the file, seek, read the chunk, and close immediately.
82
+ with open(file_path, "rb") as f:
83
+ f.seek(offset)
84
+ data = f.read(chunk_size)
85
+
86
+ if not data:
87
+ warnings.warn(f"Empty data for part {part_number} of {file_path}")
88
+
89
+ file_chunk = FileChunk(
90
+ src,
91
+ upload_id=upload_info.upload_id,
92
+ part_number=part_number,
93
+ data=data, # After this, data should not be reused.
94
+ )
95
+ done_part_numbers.add(part_number)
96
+ output.put(file_chunk)
97
+ part_number += 1
98
+ except Exception as e:
99
+
100
+ warnings.warn(f"Error reading file: {e}")
101
+ finally:
102
+ output.put(None)