rclone-api 1.1.61__tar.gz → 1.1.62__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.1.61 → rclone_api-1.1.62}/PKG-INFO +1 -1
  2. {rclone_api-1.1.61 → rclone_api-1.1.62}/pyproject.toml +1 -1
  3. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/mount.py +83 -1
  4. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/rclone.py +104 -46
  5. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api.egg-info/PKG-INFO +1 -1
  6. {rclone_api-1.1.61 → rclone_api-1.1.62}/.aiderignore +0 -0
  7. {rclone_api-1.1.61 → rclone_api-1.1.62}/.github/workflows/lint.yml +0 -0
  8. {rclone_api-1.1.61 → rclone_api-1.1.62}/.github/workflows/push_macos.yml +0 -0
  9. {rclone_api-1.1.61 → rclone_api-1.1.62}/.github/workflows/push_ubuntu.yml +0 -0
  10. {rclone_api-1.1.61 → rclone_api-1.1.62}/.github/workflows/push_win.yml +0 -0
  11. {rclone_api-1.1.61 → rclone_api-1.1.62}/.gitignore +0 -0
  12. {rclone_api-1.1.61 → rclone_api-1.1.62}/.pylintrc +0 -0
  13. {rclone_api-1.1.61 → rclone_api-1.1.62}/.vscode/launch.json +0 -0
  14. {rclone_api-1.1.61 → rclone_api-1.1.62}/.vscode/settings.json +0 -0
  15. {rclone_api-1.1.61 → rclone_api-1.1.62}/.vscode/tasks.json +0 -0
  16. {rclone_api-1.1.61 → rclone_api-1.1.62}/LICENSE +0 -0
  17. {rclone_api-1.1.61 → rclone_api-1.1.62}/MANIFEST.in +0 -0
  18. {rclone_api-1.1.61 → rclone_api-1.1.62}/README.md +0 -0
  19. {rclone_api-1.1.61 → rclone_api-1.1.62}/clean +0 -0
  20. {rclone_api-1.1.61 → rclone_api-1.1.62}/install +0 -0
  21. {rclone_api-1.1.61 → rclone_api-1.1.62}/lint +0 -0
  22. {rclone_api-1.1.61 → rclone_api-1.1.62}/requirements.testing.txt +0 -0
  23. {rclone_api-1.1.61 → rclone_api-1.1.62}/setup.cfg +0 -0
  24. {rclone_api-1.1.61 → rclone_api-1.1.62}/setup.py +0 -0
  25. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/__init__.py +0 -0
  26. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/assets/example.txt +0 -0
  27. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/cli.py +0 -0
  28. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/cmd/copy_large_s3.py +0 -0
  29. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/cmd/list_files.py +0 -0
  30. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/completed_process.py +0 -0
  31. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/config.py +0 -0
  32. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/convert.py +0 -0
  33. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/deprecated.py +0 -0
  34. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/diff.py +0 -0
  35. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/dir.py +0 -0
  36. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/dir_listing.py +0 -0
  37. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/exec.py +0 -0
  38. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/experimental/flags.py +0 -0
  39. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/experimental/flags_base.py +0 -0
  40. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/file.py +0 -0
  41. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/filelist.py +0 -0
  42. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/group_files.py +0 -0
  43. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/process.py +0 -0
  44. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/profile/mount_copy_bytes.py +0 -0
  45. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/remote.py +0 -0
  46. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/rpath.py +0 -0
  47. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/s3/api.py +0 -0
  48. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/s3/basic_ops.py +0 -0
  49. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/s3/chunk_file.py +0 -0
  50. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/s3/chunk_types.py +0 -0
  51. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/s3/create.py +0 -0
  52. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/s3/types.py +0 -0
  53. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/s3/upload_file_multipart.py +0 -0
  54. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/scan_missing_folders.py +0 -0
  55. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/types.py +0 -0
  56. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/util.py +0 -0
  57. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api/walk.py +0 -0
  58. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api.egg-info/SOURCES.txt +0 -0
  59. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api.egg-info/dependency_links.txt +0 -0
  60. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api.egg-info/entry_points.txt +0 -0
  61. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api.egg-info/requires.txt +0 -0
  62. {rclone_api-1.1.61 → rclone_api-1.1.62}/src/rclone_api.egg-info/top_level.txt +0 -0
  63. {rclone_api-1.1.61 → rclone_api-1.1.62}/test +0 -0
  64. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/archive/test_paramiko.py.disabled +0 -0
  65. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_cmd_list_files.py +0 -0
  66. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_copy.py +0 -0
  67. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_copy_bytes.py +0 -0
  68. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_copy_file_resumable_s3.py +0 -0
  69. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_copy_files.py +0 -0
  70. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_diff.py +0 -0
  71. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_group_files.py +0 -0
  72. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_is_synced.py +0 -0
  73. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_ls.py +0 -0
  74. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_mount.py +0 -0
  75. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_mount_s3.py +0 -0
  76. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_obscure.py +0 -0
  77. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_rclone_config.py +0 -0
  78. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_remote_control.py +0 -0
  79. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_remotes.py +0 -0
  80. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_s3.py +0 -0
  81. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_scan_missing_folders.py +0 -0
  82. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_size_files.py +0 -0
  83. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_size_suffix.py +0 -0
  84. {rclone_api-1.1.61 → rclone_api-1.1.62}/tests/test_walk.py +0 -0
  85. {rclone_api-1.1.61 → rclone_api-1.1.62}/tox.ini +0 -0
  86. {rclone_api-1.1.61 → rclone_api-1.1.62}/upload_package.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.1.61
3
+ Version: 1.1.62
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.1.61"
26
+ version = "1.1.62"
27
27
 
28
28
  [tool.setuptools]
29
29
  package-dir = {"" = "src"}
@@ -6,12 +6,14 @@ import subprocess
6
6
  import time
7
7
  import warnings
8
8
  import weakref
9
- from concurrent.futures import ThreadPoolExecutor
9
+ from concurrent.futures import Future, ThreadPoolExecutor
10
10
  from dataclasses import dataclass
11
11
  from pathlib import Path
12
+ from threading import Lock, Semaphore
12
13
  from typing import Any
13
14
 
14
15
  from rclone_api.process import Process
16
+ from rclone_api.types import SizeSuffix
15
17
 
16
18
  _SYSTEM = platform.system() # "Linux", "Darwin", "Windows", etc.
17
19
 
@@ -283,3 +285,83 @@ def _cache_dir_delete_on_exit(cache_dir: Path) -> None:
283
285
  shutil.rmtree(cache_dir)
284
286
  except Exception as e:
285
287
  warnings.warn(f"Error removing cache directory {cache_dir}: {e}")
288
+
289
+
290
+ class MultiMountFileChunker:
291
+ def __init__(
292
+ self,
293
+ filename: str,
294
+ chunk_size: SizeSuffix,
295
+ mounts: list[Mount],
296
+ executor: ThreadPoolExecutor,
297
+ ) -> None:
298
+ self.filename = filename
299
+ self.chunk_size = chunk_size
300
+ self.executor = executor
301
+ self.mounts_processing: list[Mount] = []
302
+ self.mounts_availabe: list[Mount] = mounts
303
+ self.semaphore = Semaphore(len(mounts))
304
+ self.lock = Lock()
305
+
306
+ def close(self) -> None:
307
+ self.executor.shutdown(wait=False, cancel_futures=True)
308
+ with ThreadPoolExecutor() as executor:
309
+ for mount in self.mounts_processing:
310
+ executor.submit(lambda: mount.close())
311
+
312
+ def fetch(self, offset: int, size: int) -> bytes | Exception:
313
+ try:
314
+ assert offset % self.chunk_size.as_int() == 0
315
+ assert size <= self.chunk_size.as_int()
316
+ assert size > 0
317
+ assert offset >= 0
318
+
319
+ chunks: list[tuple[int, int]] = []
320
+ start = offset
321
+ end = offset + size
322
+ while start < end:
323
+ chunk_size = min(self.chunk_size.as_int(), end - start)
324
+ chunks.append((start, chunk_size))
325
+ start += chunk_size
326
+
327
+ futures: list[Future[bytes | Exception]] = []
328
+ for start, chunk_size in chunks:
329
+ self.semaphore.acquire()
330
+ with self.lock:
331
+ mount = self.mounts_availabe.pop()
332
+ self.mounts_processing.append(mount)
333
+
334
+ path = mount.mount_path / self.filename
335
+
336
+ def task() -> bytes | Exception:
337
+ try:
338
+ with path.open("rb") as f:
339
+ f.seek(offset)
340
+ return f.read(size)
341
+ except KeyboardInterrupt as e:
342
+ import _thread
343
+
344
+ _thread.interrupt_main()
345
+ return Exception(e)
346
+ except Exception as e:
347
+ return e
348
+ finally:
349
+ with self.lock:
350
+ self.mounts_processing.remove(mount)
351
+ self.mounts_availabe.append(mount)
352
+ self.semaphore.release()
353
+
354
+ fut = self.executor.submit(task)
355
+ futures.append(fut)
356
+
357
+ out: bytes = b""
358
+ for fut in futures:
359
+ chunk: bytes | Exception = fut.result()
360
+ if isinstance(chunk, Exception):
361
+ return chunk
362
+ else:
363
+ out += chunk
364
+ del chunk
365
+ return out
366
+ except Exception as e:
367
+ return e
@@ -25,7 +25,7 @@ from rclone_api.dir_listing import DirListing
25
25
  from rclone_api.exec import RcloneExec
26
26
  from rclone_api.file import File
27
27
  from rclone_api.group_files import group_files
28
- from rclone_api.mount import Mount, clean_mount, prepare_mount
28
+ from rclone_api.mount import Mount, MultiMountFileChunker, clean_mount, prepare_mount
29
29
  from rclone_api.process import Process
30
30
  from rclone_api.remote import Remote
31
31
  from rclone_api.rpath import RPath
@@ -829,7 +829,7 @@ class Rclone:
829
829
  """Copy bytes from a file to another file."""
830
830
  from rclone_api.util import random_str
831
831
 
832
- tmp_mnt = Path("tmp_mnt") / random_str(12)
832
+ tmp_mnts = Path("tmp_mnts") / random_str(12)
833
833
  src_parent_path = Path(src).parent.as_posix()
834
834
  src_file = Path(src).name
835
835
  other_args: list[str] = [
@@ -860,7 +860,7 @@ class Rclone:
860
860
  # use scoped mount to do the read, then write the bytes to the destination
861
861
  with self.scoped_mount(
862
862
  src_parent_path,
863
- tmp_mnt,
863
+ tmp_mnts,
864
864
  use_links=True,
865
865
  verbose=mount_log is not None,
866
866
  vfs_cache_mode="minimal",
@@ -869,7 +869,7 @@ class Rclone:
869
869
  cache_dir=cache_dir,
870
870
  cache_dir_delete_on_exit=True,
871
871
  ):
872
- src_file_mnt = tmp_mnt / src_file
872
+ src_file_mnt = tmp_mnts / src_file
873
873
  with open(src_file_mnt, "rb") as f:
874
874
  f.seek(offset)
875
875
  data = f.read(length)
@@ -883,6 +883,85 @@ class Rclone:
883
883
  except Exception as e:
884
884
  return e
885
885
 
886
+ def get_multi_mount_file_chunker(
887
+ self,
888
+ src: str,
889
+ chunk_size: SizeSuffix,
890
+ threads: int,
891
+ mount_log: Path | None,
892
+ direct_io: bool,
893
+ ) -> MultiMountFileChunker:
894
+ from rclone_api.util import random_str
895
+
896
+ mounts: list[Mount] = []
897
+ vfs_read_chunk_size = chunk_size
898
+ vfs_read_chunk_size_limit = chunk_size
899
+ vfs_read_chunk_streams = 0
900
+ vfs_disk_space_total_size = chunk_size
901
+ other_args: list[str] = []
902
+ other_args += ["--no-modtime"]
903
+ other_args += ["--vfs-read-chunk-size", vfs_read_chunk_size.as_str()]
904
+ other_args += [
905
+ "--vfs-read-chunk-size-limit",
906
+ vfs_read_chunk_size_limit.as_str(),
907
+ ]
908
+ other_args += ["--vfs-read-chunk-streams", str(vfs_read_chunk_streams)]
909
+ other_args += [
910
+ "--vfs-disk-space-total-size",
911
+ vfs_disk_space_total_size.as_str(),
912
+ ]
913
+ other_args += ["--read-only"]
914
+ if direct_io:
915
+ other_args += ["--direct-io"]
916
+
917
+ filename = Path(src).name
918
+ with ThreadPoolExecutor(max_workers=threads) as executor:
919
+ futures: list[Future] = []
920
+ try:
921
+ for i in range(threads):
922
+ tmp_mnts = Path("tmp_mnts") / random_str(12)
923
+ verbose = mount_log is not None
924
+ clean_mount(tmp_mnts, verbose=verbose)
925
+ prepare_mount(tmp_mnts, verbose=verbose)
926
+ src_parent_path = Path(src).parent.as_posix()
927
+
928
+ def task(src_parent_path=src_parent_path, tmp_mnts=tmp_mnts):
929
+ return self.mount(
930
+ src=src_parent_path,
931
+ outdir=tmp_mnts,
932
+ allow_writes=False,
933
+ use_links=True,
934
+ vfs_cache_mode="minimal",
935
+ verbose=False,
936
+ cache_dir=Path("cache") / random_str(12),
937
+ cache_dir_delete_on_exit=True,
938
+ log=mount_log,
939
+ other_args=other_args,
940
+ )
941
+
942
+ futures.append(executor.submit(task))
943
+ mount_errors: list[Exception] = []
944
+ for fut in futures:
945
+ try:
946
+ mount = fut.result()
947
+ mounts.append(mount)
948
+ except Exception as er:
949
+ mount_errors.append(er)
950
+ if mount_errors:
951
+ raise Exception(mount_errors)
952
+ except Exception:
953
+ for mount in mounts:
954
+ mount.close()
955
+ raise
956
+ executor = ThreadPoolExecutor(max_workers=threads)
957
+ filechunker: MultiMountFileChunker = MultiMountFileChunker(
958
+ filename=filename,
959
+ chunk_size=chunk_size,
960
+ mounts=mounts,
961
+ executor=executor,
962
+ )
963
+ return filechunker
964
+
886
965
  def copy_bytes(
887
966
  self,
888
967
  src: str,
@@ -897,49 +976,28 @@ class Rclone:
897
976
  ) -> bytes | Exception:
898
977
  """Copy bytes from a file to another file."""
899
978
  # determine number of threads from chunk size
900
- threads = min(max_threads, length // chunk_size.as_int())
901
-
902
- if threads == 1:
903
- return self._copy_bytes(
904
- src,
905
- offset,
906
- length,
907
- transfers=1,
908
- outfile=outfile,
909
- mount_log=mount_log,
910
- direct_io=direct_io,
911
- )
912
- else:
913
- futures: list[Future] = []
914
- with ThreadPoolExecutor(max_workers=threads) as executor:
915
- for i in range(threads):
916
- start = i * chunk_size.as_int()
917
- end = start + chunk_size.as_int()
918
- if i == threads - 1:
919
- end = length
920
- future = executor.submit(
921
- self._copy_bytes,
922
- src,
923
- offset + start,
924
- end - start,
925
- transfers=1,
926
- outfile=None,
927
- mount_log=mount_log,
928
- direct_io=direct_io,
929
- )
930
- futures.append(future)
931
- data: bytes = b""
932
- for future in futures:
933
- chunk: bytes | Exception = future.result()
934
- if isinstance(chunk, Exception):
935
- return chunk
936
- data += chunk
937
- if outfile is not None:
938
- with open(outfile, "wb") as out:
939
- out.write(data)
940
- del data
941
- return bytes(0)
979
+ threads = max(1, min(max_threads, length // chunk_size.as_int()))
980
+ filechunker = self.get_multi_mount_file_chunker(
981
+ src=src,
982
+ chunk_size=chunk_size,
983
+ threads=threads,
984
+ mount_log=mount_log,
985
+ direct_io=direct_io,
986
+ )
987
+ try:
988
+ data = filechunker.fetch(offset, length)
989
+ if isinstance(data, Exception):
990
+ raise data
991
+ if outfile is None:
942
992
  return data
993
+ with open(outfile, "wb") as out:
994
+ out.write(data)
995
+ del data
996
+ return bytes(0)
997
+ except Exception as e:
998
+ return e
999
+ finally:
1000
+ filechunker.close()
943
1001
 
944
1002
  def copy_dir(
945
1003
  self, src: str | Dir, dst: str | Dir, args: list[str] | None = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.1.61
3
+ Version: 1.1.62
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  License: BSD 3-Clause License
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