rclone-api 1.2.1__tar.gz → 1.2.3__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 (86) hide show
  1. {rclone_api-1.2.1 → rclone_api-1.2.3}/.gitignore +1 -0
  2. {rclone_api-1.2.1 → rclone_api-1.2.3}/PKG-INFO +1 -1
  3. {rclone_api-1.2.1 → rclone_api-1.2.3}/pyproject.toml +1 -1
  4. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/cmd/copy_large_s3.py +1 -1
  5. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/mount.py +15 -5
  6. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/rclone.py +113 -95
  7. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/s3/api.py +7 -1
  8. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/s3/chunk_types.py +10 -2
  9. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/s3/types.py +1 -0
  10. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/s3/upload_file_multipart.py +11 -8
  11. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api.egg-info/PKG-INFO +1 -1
  12. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_copy_file_resumable_s3.py +3 -1
  13. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_s3.py +1 -0
  14. {rclone_api-1.2.1 → rclone_api-1.2.3}/.aiderignore +0 -0
  15. {rclone_api-1.2.1 → rclone_api-1.2.3}/.github/workflows/lint.yml +0 -0
  16. {rclone_api-1.2.1 → rclone_api-1.2.3}/.github/workflows/push_macos.yml +0 -0
  17. {rclone_api-1.2.1 → rclone_api-1.2.3}/.github/workflows/push_ubuntu.yml +0 -0
  18. {rclone_api-1.2.1 → rclone_api-1.2.3}/.github/workflows/push_win.yml +0 -0
  19. {rclone_api-1.2.1 → rclone_api-1.2.3}/.pylintrc +0 -0
  20. {rclone_api-1.2.1 → rclone_api-1.2.3}/.vscode/launch.json +0 -0
  21. {rclone_api-1.2.1 → rclone_api-1.2.3}/.vscode/settings.json +0 -0
  22. {rclone_api-1.2.1 → rclone_api-1.2.3}/.vscode/tasks.json +0 -0
  23. {rclone_api-1.2.1 → rclone_api-1.2.3}/LICENSE +0 -0
  24. {rclone_api-1.2.1 → rclone_api-1.2.3}/MANIFEST.in +0 -0
  25. {rclone_api-1.2.1 → rclone_api-1.2.3}/README.md +0 -0
  26. {rclone_api-1.2.1 → rclone_api-1.2.3}/clean +0 -0
  27. {rclone_api-1.2.1 → rclone_api-1.2.3}/install +0 -0
  28. {rclone_api-1.2.1 → rclone_api-1.2.3}/lint +0 -0
  29. {rclone_api-1.2.1 → rclone_api-1.2.3}/requirements.testing.txt +0 -0
  30. {rclone_api-1.2.1 → rclone_api-1.2.3}/setup.cfg +0 -0
  31. {rclone_api-1.2.1 → rclone_api-1.2.3}/setup.py +0 -0
  32. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/__init__.py +0 -0
  33. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/assets/example.txt +0 -0
  34. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/cli.py +0 -0
  35. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/cmd/list_files.py +0 -0
  36. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/completed_process.py +0 -0
  37. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/config.py +0 -0
  38. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/convert.py +0 -0
  39. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/deprecated.py +0 -0
  40. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/diff.py +0 -0
  41. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/dir.py +0 -0
  42. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/dir_listing.py +0 -0
  43. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/exec.py +0 -0
  44. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/experimental/flags.py +0 -0
  45. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/experimental/flags_base.py +0 -0
  46. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/file.py +0 -0
  47. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/filelist.py +0 -0
  48. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/group_files.py +0 -0
  49. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/process.py +0 -0
  50. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/profile/mount_copy_bytes.py +0 -0
  51. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/remote.py +0 -0
  52. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/rpath.py +0 -0
  53. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/s3/basic_ops.py +0 -0
  54. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/s3/chunk_file.py +0 -0
  55. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/s3/create.py +0 -0
  56. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/scan_missing_folders.py +0 -0
  57. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/types.py +0 -0
  58. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/util.py +0 -0
  59. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api/walk.py +0 -0
  60. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api.egg-info/SOURCES.txt +0 -0
  61. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api.egg-info/dependency_links.txt +0 -0
  62. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api.egg-info/entry_points.txt +0 -0
  63. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api.egg-info/requires.txt +0 -0
  64. {rclone_api-1.2.1 → rclone_api-1.2.3}/src/rclone_api.egg-info/top_level.txt +0 -0
  65. {rclone_api-1.2.1 → rclone_api-1.2.3}/test +0 -0
  66. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/archive/test_paramiko.py.disabled +0 -0
  67. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_cmd_list_files.py +0 -0
  68. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_copy.py +0 -0
  69. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_copy_bytes.py +0 -0
  70. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_copy_files.py +0 -0
  71. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_diff.py +0 -0
  72. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_group_files.py +0 -0
  73. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_is_synced.py +0 -0
  74. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_ls.py +0 -0
  75. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_mount.py +0 -0
  76. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_mount_s3.py +0 -0
  77. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_obscure.py +0 -0
  78. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_rclone_config.py +0 -0
  79. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_remote_control.py +0 -0
  80. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_remotes.py +0 -0
  81. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_scan_missing_folders.py +0 -0
  82. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_size_files.py +0 -0
  83. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_size_suffix.py +0 -0
  84. {rclone_api-1.2.1 → rclone_api-1.2.3}/tests/test_walk.py +0 -0
  85. {rclone_api-1.2.1 → rclone_api-1.2.3}/tox.ini +0 -0
  86. {rclone_api-1.2.1 → rclone_api-1.2.3}/upload_package.sh +0 -0
@@ -150,3 +150,4 @@ t.py
150
150
  mount
151
151
  t2.py
152
152
  chunk_store
153
+ state.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  License: BSD 3-Clause License
@@ -23,7 +23,7 @@ dependencies = [
23
23
  ]
24
24
 
25
25
  # Change this with the version number bump.
26
- version = "1.2.1"
26
+ version = "1.2.3"
27
27
 
28
28
  [tool.setuptools]
29
29
  package-dir = {"" = "src"}
@@ -45,7 +45,7 @@ def _parse_args() -> Args:
45
45
  "--read-threads",
46
46
  help="Number of concurrent read threads per chunk, only one chunk will be read at a time",
47
47
  type=int,
48
- default=16,
48
+ default=8,
49
49
  )
50
50
  parser.add_argument(
51
51
  "--write-threads",
@@ -115,7 +115,7 @@ def prepare_mount(outdir: Path, verbose: bool) -> None:
115
115
  def wait_for_mount(
116
116
  path: Path,
117
117
  mount_process: Any,
118
- timeout: int = 10,
118
+ timeout: int = 20,
119
119
  post_mount_delay: int = 5,
120
120
  poll_interval: float = 1.0,
121
121
  check_mount_flag: bool = False,
@@ -292,6 +292,7 @@ class MultiMountFileChunker:
292
292
  def __init__(
293
293
  self,
294
294
  filename: str,
295
+ filesize: int,
295
296
  chunk_size: SizeSuffix,
296
297
  mounts: list[Mount],
297
298
  executor: ThreadPoolExecutor,
@@ -300,6 +301,7 @@ class MultiMountFileChunker:
300
301
  from rclone_api.util import get_verbose
301
302
 
302
303
  self.filename = filename
304
+ self.filesize = filesize
303
305
  self.chunk_size = chunk_size
304
306
  self.executor = executor
305
307
  self.mounts_processing: list[Mount] = []
@@ -309,7 +311,7 @@ class MultiMountFileChunker:
309
311
  self.verbose = get_verbose(verbose)
310
312
 
311
313
  def close(self) -> None:
312
- self.executor.shutdown(wait=False, cancel_futures=True)
314
+ self.executor.shutdown(wait=True, cancel_futures=True)
313
315
  with ThreadPoolExecutor() as executor:
314
316
  for mount in self.mounts_processing:
315
317
  executor.submit(lambda: mount.close())
@@ -319,9 +321,11 @@ class MultiMountFileChunker:
319
321
  print(f"Fetching data range: offset={offset}, size={size}")
320
322
  try:
321
323
  try:
324
+ if offset + size > self.filesize:
325
+ size = self.filesize - offset
322
326
  assert (
323
- offset % self.chunk_size.as_int() == 0
324
- ), f"Invalid offset: {offset}"
327
+ offset + size <= self.filesize
328
+ ), f"Invalid offset + size: {offset + size}, it is beyond the end of the file."
325
329
  assert size > 0, f"Invalid size: {size}"
326
330
  assert offset >= 0, f"Invalid offset: {offset}"
327
331
  except AssertionError as e:
@@ -356,6 +360,12 @@ class MultiMountFileChunker:
356
360
  f"Fetching chunk: offset={offset}, size={size}, path={path}"
357
361
  )
358
362
  try:
363
+ # make sure we don't overflow the file size
364
+ # assert (
365
+ # offset + size <= self.chunk_size.as_int()
366
+ # ), f"Invalid offset + size: {offset + size}, it is beyond the end of the file."
367
+ if offset + size > self.filesize:
368
+ size = self.filesize - offset
359
369
  with path.open("rb") as f:
360
370
  f.seek(offset)
361
371
  return f.read(size)
@@ -369,7 +379,7 @@ class MultiMountFileChunker:
369
379
  except Exception as e:
370
380
  stack_trace = traceback.format_exc()
371
381
  warnings.warn(
372
- f"Error fetching file chunk at offset{offset} + {size}: {e}\n{stack_trace}"
382
+ f"Error fetching file chunk at offset {offset} + {size}: {e}\n{stack_trace}"
373
383
  )
374
384
  return e
375
385
  finally:
@@ -353,13 +353,11 @@ class Rclone:
353
353
  check: bool | None = None,
354
354
  verbose: bool | None = None,
355
355
  other_args: list[str] | None = None,
356
- ) -> None:
357
- """Copy multiple files from source to destination.
356
+ ) -> CompletedProcess:
357
+ """Copy one file from source to destination.
358
358
 
359
359
  Warning - slow.
360
360
 
361
- Args:
362
- payload: Dictionary of source and destination file paths
363
361
  """
364
362
  check = get_check(check)
365
363
  verbose = get_verbose(verbose)
@@ -368,7 +366,8 @@ class Rclone:
368
366
  cmd_list: list[str] = ["copyto", src, dst]
369
367
  if other_args is not None:
370
368
  cmd_list += other_args
371
- self._run(cmd_list, check=check)
369
+ cp = self._run(cmd_list, check=check)
370
+ return CompletedProcess.from_subprocess(cp)
372
371
 
373
372
  def copy_files(
374
373
  self,
@@ -680,18 +679,17 @@ class Rclone:
680
679
  dst: str,
681
680
  save_state_json: Path,
682
681
  chunk_size: SizeSuffix | None = None,
683
- read_threads: int = 16,
682
+ read_threads: int = 8,
684
683
  write_threads: int = 16,
685
684
  retries: int = 3,
686
685
  verbose: bool | None = None,
687
686
  max_chunks_before_suspension: int | None = None,
688
- mount_path: Path | None = None,
689
687
  mount_log: Path | None = None,
690
688
  ) -> MultiUploadResult:
691
689
  """For massive files that rclone can't handle in one go, this function will copy the file in chunks to an S3 store"""
692
690
  from rclone_api.s3.api import S3Client
693
691
  from rclone_api.s3.create import S3Credentials
694
- from rclone_api.util import S3PathInfo, random_str, split_s3_path
692
+ from rclone_api.util import S3PathInfo, split_s3_path
695
693
 
696
694
  other_args: list[str] = ["--no-modtime", "--vfs-read-wait", "1s"]
697
695
  chunk_size = chunk_size or SizeSuffix("64M")
@@ -717,105 +715,117 @@ class Rclone:
717
715
  other_args += ["--direct-io"]
718
716
  # --vfs-cache-max-size
719
717
  other_args += ["--vfs-cache-max-size", vfs_disk_space_total_size.as_str()]
720
- mount_path = mount_path or Path("tmp_mnts") / random_str(12)
718
+ mount_path = Path("tmp_mnts") / "RCLONE_API_DYNAMIC_MOUNT"
721
719
  src_path = Path(src)
722
720
  name = src_path.name
723
- parent_path = str(src_path.parent.as_posix())
724
- with self.scoped_mount(
725
- parent_path,
726
- mount_path,
727
- use_links=True,
728
- vfs_cache_mode="minimal",
729
- verbose=False,
730
- log=mount_log,
731
- other_args=other_args,
732
- ):
733
- path_info: S3PathInfo = split_s3_path(dst)
734
- remote = path_info.remote
735
- bucket_name = path_info.bucket
736
- s3_key = path_info.key
737
- parsed: Parsed = self.config.parse()
738
- sections: dict[str, Section] = parsed.sections
739
- if remote not in sections:
740
- raise ValueError(
741
- f"Remote {remote} not found in rclone config, remotes are: {sections.keys()}"
742
- )
743
721
 
744
- section: Section = sections[remote]
745
- dst_type = section.type()
746
- if dst_type != "s3" and dst_type != "b2":
747
- raise ValueError(
748
- f"Remote {remote} is not an S3 remote, it is of type {dst_type}"
749
- )
750
-
751
- def get_provider_str(section=section) -> str | None:
752
- type: str = section.type()
753
- provider: str | None = section.provider()
754
- if provider is not None:
755
- return provider
756
- if type == "b2":
757
- return S3Provider.BACKBLAZE.value
758
- if type != "s3":
759
- raise ValueError(f"Remote {remote} is not an S3 remote")
760
- return S3Provider.S3.value
761
-
762
- provider: str
763
- if provided_provider_str := get_provider_str():
764
- if verbose:
765
- print(f"Using provided provider: {provided_provider_str}")
766
- provider = provided_provider_str
767
- else:
768
- if verbose:
769
- print(f"Using default provider: {S3Provider.S3.value}")
770
- provider = S3Provider.S3.value
771
- provider_enum = S3Provider.from_str(provider)
772
-
773
- s3_creds: S3Credentials = S3Credentials(
774
- provider=provider_enum,
775
- access_key_id=section.access_key_id(),
776
- secret_access_key=section.secret_access_key(),
777
- endpoint_url=section.endpoint(),
722
+ src_parent_path = Path(src).parent.as_posix()
723
+ size_result: SizeResult = self.size_files(src_parent_path, [name])
724
+
725
+ target_size = SizeSuffix(size_result.total_size)
726
+ if target_size < SizeSuffix("5M"):
727
+ # fallback to normal copy
728
+ completed_proc = self.copy_to(src, dst, check=True)
729
+ if completed_proc.ok:
730
+ return MultiUploadResult.UPLOADED_FRESH
731
+ if size_result.total_size <= 0:
732
+ raise ValueError(
733
+ f"File {src} has size {size_result.total_size}, is this a directory?"
778
734
  )
779
735
 
780
- chunk_fetcher: MultiMountFileChunker = self.get_multi_mount_file_chunker(
781
- src=src_path.as_posix(),
782
- chunk_size=chunk_size,
783
- threads=read_threads,
784
- mount_log=mount_log,
785
- direct_io=True,
736
+ path_info: S3PathInfo = split_s3_path(dst)
737
+ remote = path_info.remote
738
+ bucket_name = path_info.bucket
739
+ s3_key = path_info.key
740
+ parsed: Parsed = self.config.parse()
741
+ sections: dict[str, Section] = parsed.sections
742
+ if remote not in sections:
743
+ raise ValueError(
744
+ f"Remote {remote} not found in rclone config, remotes are: {sections.keys()}"
786
745
  )
787
746
 
788
- client = S3Client(s3_creds)
789
- upload_config: S3MutliPartUploadConfig = S3MutliPartUploadConfig(
790
- chunk_size=chunk_size.as_int(),
791
- chunk_fetcher=chunk_fetcher.fetch,
792
- max_write_threads=write_threads,
793
- retries=retries,
794
- resume_path_json=save_state_json,
795
- max_chunks_before_suspension=max_chunks_before_suspension,
747
+ section: Section = sections[remote]
748
+ dst_type = section.type()
749
+ if dst_type != "s3" and dst_type != "b2":
750
+ raise ValueError(
751
+ f"Remote {remote} is not an S3 remote, it is of type {dst_type}"
796
752
  )
797
753
 
798
- src_file = mount_path / name
754
+ def get_provider_str(section=section) -> str | None:
755
+ type: str = section.type()
756
+ provider: str | None = section.provider()
757
+ if provider is not None:
758
+ return provider
759
+ if type == "b2":
760
+ return S3Provider.BACKBLAZE.value
761
+ if type != "s3":
762
+ raise ValueError(f"Remote {remote} is not an S3 remote")
763
+ return S3Provider.S3.value
764
+
765
+ provider: str
766
+ if provided_provider_str := get_provider_str():
767
+ if verbose:
768
+ print(f"Using provided provider: {provided_provider_str}")
769
+ provider = provided_provider_str
770
+ else:
771
+ if verbose:
772
+ print(f"Using default provider: {S3Provider.S3.value}")
773
+ provider = S3Provider.S3.value
774
+ provider_enum = S3Provider.from_str(provider)
775
+
776
+ s3_creds: S3Credentials = S3Credentials(
777
+ provider=provider_enum,
778
+ access_key_id=section.access_key_id(),
779
+ secret_access_key=section.secret_access_key(),
780
+ endpoint_url=section.endpoint(),
781
+ )
799
782
 
800
- print(f"Uploading {name} to {s3_key} in bucket {bucket_name}")
801
- print(f"Source: {src_path}")
802
- print(f"bucket_name: {bucket_name}")
803
- print(f"upload_config: {upload_config}")
783
+ chunk_fetcher: MultiMountFileChunker = self.get_multi_mount_file_chunker(
784
+ src=src_path.as_posix(),
785
+ chunk_size=chunk_size,
786
+ threads=read_threads,
787
+ mount_log=mount_log,
788
+ direct_io=True,
789
+ )
804
790
 
805
- upload_target = S3UploadTarget(
806
- bucket_name=bucket_name,
807
- src_file=src_file,
808
- s3_key=s3_key,
809
- )
791
+ client = S3Client(s3_creds)
792
+ upload_config: S3MutliPartUploadConfig = S3MutliPartUploadConfig(
793
+ chunk_size=chunk_size.as_int(),
794
+ chunk_fetcher=chunk_fetcher.fetch,
795
+ max_write_threads=write_threads,
796
+ retries=retries,
797
+ resume_path_json=save_state_json,
798
+ max_chunks_before_suspension=max_chunks_before_suspension,
799
+ )
810
800
 
811
- try:
812
- out: MultiUploadResult = client.upload_file_multipart(
813
- upload_target=upload_target,
814
- upload_config=upload_config,
815
- )
816
- return out
817
- finally:
818
- chunk_fetcher.close()
801
+ src_file = mount_path / name
802
+
803
+ print(f"Uploading {name} to {s3_key} in bucket {bucket_name}")
804
+ print(f"Source: {src_path}")
805
+ print(f"bucket_name: {bucket_name}")
806
+ print(f"upload_config: {upload_config}")
807
+
808
+ # get the file size
809
+
810
+ upload_target = S3UploadTarget(
811
+ src_file=src_file,
812
+ src_file_size=size_result.total_size,
813
+ bucket_name=bucket_name,
814
+ s3_key=s3_key,
815
+ )
816
+
817
+ try:
818
+ out: MultiUploadResult = client.upload_file_multipart(
819
+ upload_target=upload_target,
820
+ upload_config=upload_config,
821
+ )
822
+ return out
823
+ except Exception as e:
824
+ print(f"Error uploading file: {e}")
825
+ traceback.print_exc()
826
+ raise
827
+ finally:
828
+ chunk_fetcher.close()
819
829
 
820
830
  def _copy_bytes(
821
831
  self,
@@ -963,9 +973,17 @@ class Rclone:
963
973
  for mount in mounts:
964
974
  mount.close()
965
975
  raise
976
+
977
+ src_path: Path = Path(src)
978
+ src_parent_path = src_path.parent.as_posix()
979
+ name = src_path.name
980
+ size_result: SizeResult = self.size_files(src_parent_path, [name])
981
+ filesize = size_result.total_size
982
+
966
983
  executor = ThreadPoolExecutor(max_workers=threads)
967
984
  filechunker: MultiMountFileChunker = MultiMountFileChunker(
968
985
  filename=filename,
986
+ filesize=filesize,
969
987
  chunk_size=chunk_size,
970
988
  mounts=mounts,
971
989
  executor=executor,
@@ -58,7 +58,12 @@ class S3Client:
58
58
  bucket_name = upload_target.bucket_name
59
59
 
60
60
  try:
61
- filesize = upload_target.src_file.stat().st_size
61
+
62
+ if upload_target.src_file_size is None:
63
+ filesize = upload_target.src_file.stat().st_size
64
+ else:
65
+ filesize = upload_target.src_file_size
66
+
62
67
  if filesize < _MIN_THRESHOLD_FOR_CHUNKING:
63
68
  warnings.warn(
64
69
  f"File size {filesize} is less than the minimum threshold for chunking ({_MIN_THRESHOLD_FOR_CHUNKING}), switching to single threaded upload."
@@ -73,6 +78,7 @@ class S3Client:
73
78
  chunk_fetcher=upload_config.chunk_fetcher,
74
79
  bucket_name=bucket_name,
75
80
  file_path=upload_target.src_file,
81
+ file_size=filesize,
76
82
  object_name=upload_target.s3_key,
77
83
  resumable_info_path=resume_path_json,
78
84
  chunk_size=chunk_size,
@@ -74,6 +74,10 @@ class FileChunk:
74
74
  return b""
75
75
 
76
76
  def close(self):
77
+ import traceback
78
+
79
+ stacktrace = traceback.format_stack()
80
+ locked_print(f"Closing file chunk: {self.filepart}\n{stacktrace}")
77
81
  if self.filepart.exists():
78
82
  self.filepart.unlink()
79
83
 
@@ -172,8 +176,12 @@ class UploadState:
172
176
  lock: Lock = Lock()
173
177
  parts: list[FinishedPiece | None] = field(default_factory=list)
174
178
 
175
- def update_source_file(self, src_file: Path) -> None:
176
- new_file_size = os.path.getsize(src_file)
179
+ def update_source_file(self, src_file: Path, known_file_size: int | None) -> None:
180
+ new_file_size = (
181
+ known_file_size
182
+ if known_file_size is not None
183
+ else os.path.getsize(src_file)
184
+ )
177
185
  if new_file_size != self.upload_info.file_size:
178
186
  raise ValueError("File size changed, cannot resume")
179
187
  self.upload_info.src_file_path = src_file
@@ -37,6 +37,7 @@ class S3UploadTarget:
37
37
  """Target information for S3 upload."""
38
38
 
39
39
  src_file: Path
40
+ src_file_size: int | None
40
41
  bucket_name: str
41
42
  s3_key: str
42
43
 
@@ -76,6 +76,7 @@ def prepare_upload_file_multipart(
76
76
  s3_client: BaseClient,
77
77
  bucket_name: str,
78
78
  file_path: Path,
79
+ file_size: int | None,
79
80
  object_name: str,
80
81
  chunk_size: int,
81
82
  retries: int,
@@ -89,7 +90,7 @@ def prepare_upload_file_multipart(
89
90
  mpu = s3_client.create_multipart_upload(Bucket=bucket_name, Key=object_name)
90
91
  upload_id = mpu["UploadId"]
91
92
 
92
- file_size = os.path.getsize(file_path)
93
+ file_size = file_size if file_size is not None else os.path.getsize(file_path)
93
94
 
94
95
  upload_info: UploadInfo = UploadInfo(
95
96
  s3_client=s3_client,
@@ -121,6 +122,7 @@ def upload_file_multipart(
121
122
  chunk_fetcher: Callable[[int, int], Future[bytes | Exception]],
122
123
  bucket_name: str,
123
124
  file_path: Path,
125
+ file_size: int | None,
124
126
  object_name: str,
125
127
  resumable_info_path: Path | None,
126
128
  chunk_size: int = 16 * 1024 * 1024, # Default chunk size is 16MB; can be overridden
@@ -130,12 +132,12 @@ def upload_file_multipart(
130
132
  abort_transfer_on_failure: bool = False,
131
133
  ) -> MultiUploadResult:
132
134
  """Upload a file to the bucket using multipart upload with customizable chunk size."""
133
- file_size = os.path.getsize(str(file_path))
134
- if chunk_size > file_size:
135
- warnings.warn(
136
- f"Chunk size {chunk_size} is greater than file size {file_size}, using file size"
137
- )
138
- chunk_size = file_size
135
+ file_size = file_size if file_size is not None else os.path.getsize(str(file_path))
136
+ # if chunk_size > file_size:
137
+ # warnings.warn(
138
+ # f"Chunk size {chunk_size} is greater than file size {file_size}, using file size"
139
+ # )
140
+ # chunk_size = file_size
139
141
 
140
142
  if chunk_size < _MIN_UPLOAD_CHUNK_SIZE:
141
143
  raise ValueError(
@@ -160,6 +162,7 @@ def upload_file_multipart(
160
162
  s3_client=s3_client,
161
163
  bucket_name=bucket_name,
162
164
  file_path=file_path,
165
+ file_size=file_size,
163
166
  object_name=object_name,
164
167
  chunk_size=chunk_size,
165
168
  retries=retries,
@@ -194,7 +197,7 @@ def upload_file_multipart(
194
197
  upload_state = loaded_state
195
198
 
196
199
  try:
197
- upload_state.update_source_file(file_path)
200
+ upload_state.update_source_file(file_path, file_size)
198
201
  except ValueError as e:
199
202
  locked_print(f"Cannot resume upload: {e}, size changed, starting over")
200
203
  _abort_previous_upload(upload_state)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  License: BSD 3-Clause License
@@ -135,8 +135,10 @@ class RcloneCopyResumableFileToS3(unittest.TestCase):
135
135
  print(f"Config file written to: {_CONFIG_PATH}")
136
136
  rclone = Rclone(_CONFIG_PATH)
137
137
  save_state_json = Path("state.json")
138
+ if save_state_json.exists():
139
+ save_state_json.unlink()
138
140
  rclone.copy_file_resumable_s3(
139
- src="src:aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst.torrent",
141
+ src="dst:rclone-api-unit-test/zachs_video/perpetualmaniac_an_authoritative_school_teacher_forcing_a_stude_c65528d3-aa6f-4777-a56b-a919856d44e1.png",
140
142
  dst="dst:rclone-api-unit-test/test_data/test.torrent.testwrite",
141
143
  chunk_size=SizeSuffix("16MB"),
142
144
  retries=0,
@@ -81,6 +81,7 @@ class RcloneS3Tester(unittest.TestCase):
81
81
 
82
82
  upload_target: S3UploadTarget = S3UploadTarget(
83
83
  src_file=Path(tmpfile),
84
+ src_file_size=filesize,
84
85
  bucket_name=BUCKET_NAME,
85
86
  s3_key=dst_path,
86
87
  )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes