rclone-api 1.3.28__py2.py3-none-any.whl → 1.4.1__py2.py3-none-any.whl

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 (34) hide show
  1. rclone_api/__init__.py +491 -4
  2. rclone_api/cmd/copy_large_s3.py +17 -10
  3. rclone_api/db/db.py +3 -3
  4. rclone_api/detail/copy_file_parts.py +382 -0
  5. rclone_api/dir.py +1 -1
  6. rclone_api/dir_listing.py +1 -1
  7. rclone_api/file.py +8 -0
  8. rclone_api/file_part.py +198 -0
  9. rclone_api/file_stream.py +52 -0
  10. rclone_api/http_server.py +15 -21
  11. rclone_api/{rclone.py → rclone_impl.py} +153 -321
  12. rclone_api/remote.py +3 -3
  13. rclone_api/rpath.py +11 -4
  14. rclone_api/s3/chunk_task.py +3 -19
  15. rclone_api/s3/multipart/file_info.py +7 -0
  16. rclone_api/s3/multipart/finished_piece.py +38 -0
  17. rclone_api/s3/multipart/upload_info.py +62 -0
  18. rclone_api/s3/{chunk_types.py → multipart/upload_state.py} +3 -99
  19. rclone_api/s3/s3_multipart_uploader.py +138 -28
  20. rclone_api/s3/types.py +1 -1
  21. rclone_api/s3/upload_file_multipart.py +6 -13
  22. rclone_api/scan_missing_folders.py +1 -1
  23. rclone_api/types.py +136 -165
  24. rclone_api/util.py +22 -2
  25. {rclone_api-1.3.28.dist-info → rclone_api-1.4.1.dist-info}/METADATA +1 -1
  26. rclone_api-1.4.1.dist-info/RECORD +55 -0
  27. rclone_api/mount_read_chunker.py +0 -130
  28. rclone_api/profile/mount_copy_bytes.py +0 -311
  29. rclone_api-1.3.28.dist-info/RECORD +0 -51
  30. /rclone_api/{walk.py → detail/walk.py} +0 -0
  31. {rclone_api-1.3.28.dist-info → rclone_api-1.4.1.dist-info}/LICENSE +0 -0
  32. {rclone_api-1.3.28.dist-info → rclone_api-1.4.1.dist-info}/WHEEL +0 -0
  33. {rclone_api-1.3.28.dist-info → rclone_api-1.4.1.dist-info}/entry_points.txt +0 -0
  34. {rclone_api-1.3.28.dist-info → rclone_api-1.4.1.dist-info}/top_level.txt +0 -0
rclone_api/__init__.py CHANGED
@@ -1,5 +1,9 @@
1
1
  # Import logging module to activate default configuration
2
2
 
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Generator
6
+
3
7
  from rclone_api import log
4
8
 
5
9
  from .completed_process import CompletedProcess
@@ -8,17 +12,501 @@ from .diff import DiffItem, DiffOption, DiffType
8
12
  from .dir import Dir
9
13
  from .dir_listing import DirListing
10
14
  from .file import File, FileItem
15
+ from .file_stream import FilesStream
11
16
  from .filelist import FileList
12
17
  from .http_server import HttpFetcher, HttpServer, Range
13
18
 
14
19
  # Import the configure_logging function to make it available at package level
15
20
  from .log import configure_logging, setup_default_logging
21
+ from .mount import Mount
16
22
  from .process import Process
17
- from .rclone import Rclone, rclone_verbose
18
23
  from .remote import Remote
19
24
  from .rpath import RPath
20
25
  from .s3.types import MultiUploadResult
21
- from .types import ListingOption, Order, SizeResult, SizeSuffix
26
+ from .types import ListingOption, Order, PartInfo, SizeResult, SizeSuffix
27
+
28
+ setup_default_logging()
29
+
30
+
31
+ def rclone_verbose(val: bool | None) -> bool:
32
+ from rclone_api.rclone_impl import rclone_verbose as _rclone_verbose
33
+
34
+ return _rclone_verbose(val)
35
+
36
+
37
+ class Rclone:
38
+ def __init__(
39
+ self, rclone_conf: Path | Config, rclone_exe: Path | None = None
40
+ ) -> None:
41
+ from rclone_api.rclone_impl import RcloneImpl
42
+
43
+ self.impl: RcloneImpl = RcloneImpl(rclone_conf, rclone_exe)
44
+
45
+ def webgui(self, other_args: list[str] | None = None) -> Process:
46
+ """Launch the Rclone web GUI."""
47
+ return self.impl.webgui(other_args=other_args)
48
+
49
+ def launch_server(
50
+ self,
51
+ addr: str,
52
+ user: str | None = None,
53
+ password: str | None = None,
54
+ other_args: list[str] | None = None,
55
+ ) -> Process:
56
+ """Launch the Rclone server so it can receive commands"""
57
+ return self.impl.launch_server(
58
+ addr=addr, user=user, password=password, other_args=other_args
59
+ )
60
+
61
+ def remote_control(
62
+ self,
63
+ addr: str,
64
+ user: str | None = None,
65
+ password: str | None = None,
66
+ capture: bool | None = None,
67
+ other_args: list[str] | None = None,
68
+ ) -> CompletedProcess:
69
+ return self.impl.remote_control(
70
+ addr=addr,
71
+ user=user,
72
+ password=password,
73
+ capture=capture,
74
+ other_args=other_args,
75
+ )
76
+
77
+ def obscure(self, password: str) -> str:
78
+ """Obscure a password for use in rclone config files."""
79
+ return self.impl.obscure(password=password)
80
+
81
+ def ls_stream(
82
+ self,
83
+ path: str,
84
+ max_depth: int = -1,
85
+ fast_list: bool = False,
86
+ ) -> FilesStream:
87
+ """
88
+ List files in the given path
89
+
90
+ Args:
91
+ src: Remote path to list
92
+ max_depth: Maximum recursion depth (-1 for unlimited)
93
+ fast_list: Use fast list (only use when getting THE entire data repository from the root/bucket, or it's small)
94
+ """
95
+ return self.impl.ls_stream(path=path, max_depth=max_depth, fast_list=fast_list)
96
+
97
+ def save_to_db(
98
+ self,
99
+ src: str,
100
+ db_url: str,
101
+ max_depth: int = -1,
102
+ fast_list: bool = False,
103
+ ) -> None:
104
+ """
105
+ Save files to a database (sqlite, mysql, postgres)
106
+
107
+ Args:
108
+ src: Remote path to list, this will be used to populate an entire table, so always use the root-most path.
109
+ db_url: Database URL, like sqlite:///data.db or mysql://user:pass@localhost/db or postgres://user:pass@localhost/db
110
+ max_depth: Maximum depth to traverse (-1 for unlimited)
111
+ fast_list: Use fast list (only use when getting THE entire data repository from the root/bucket)
112
+
113
+ """
114
+ return self.impl.save_to_db(
115
+ src=src, db_url=db_url, max_depth=max_depth, fast_list=fast_list
116
+ )
117
+
118
+ def ls(
119
+ self,
120
+ path: Dir | Remote | str | None = None,
121
+ max_depth: int | None = None,
122
+ glob: str | None = None,
123
+ order: Order = Order.NORMAL,
124
+ listing_option: ListingOption = ListingOption.ALL,
125
+ ) -> DirListing:
126
+ return self.impl.ls(
127
+ path=path,
128
+ max_depth=max_depth,
129
+ glob=glob,
130
+ order=order,
131
+ listing_option=listing_option,
132
+ )
133
+
134
+ def listremotes(self) -> list[Remote]:
135
+ return self.impl.listremotes()
136
+
137
+ def diff(
138
+ self,
139
+ src: str,
140
+ dst: str,
141
+ min_size: (
142
+ str | None
143
+ ) = None, # e. g. "1MB" - see rclone documentation: https://rclone.org/commands/rclone_check/
144
+ max_size: (
145
+ str | None
146
+ ) = None, # e. g. "1GB" - see rclone documentation: https://rclone.org/commands/rclone_check/
147
+ diff_option: DiffOption = DiffOption.COMBINED,
148
+ fast_list: bool = True,
149
+ size_only: bool | None = None,
150
+ checkers: int | None = None,
151
+ other_args: list[str] | None = None,
152
+ ) -> Generator[DiffItem, None, None]:
153
+ """Be extra careful with the src and dst values. If you are off by one
154
+ parent directory, you will get a huge amount of false diffs."""
155
+ return self.impl.diff(
156
+ src=src,
157
+ dst=dst,
158
+ min_size=min_size,
159
+ max_size=max_size,
160
+ diff_option=diff_option,
161
+ fast_list=fast_list,
162
+ size_only=size_only,
163
+ checkers=checkers,
164
+ other_args=other_args,
165
+ )
166
+
167
+ def walk(
168
+ self,
169
+ path: Dir | Remote | str,
170
+ max_depth: int = -1,
171
+ breadth_first: bool = True,
172
+ order: Order = Order.NORMAL,
173
+ ) -> Generator[DirListing, None, None]:
174
+ """Walk through the given path recursively.
175
+
176
+ Args:
177
+ path: Remote path or Remote object to walk through
178
+ max_depth: Maximum depth to traverse (-1 for unlimited)
179
+
180
+ Yields:
181
+ DirListing: Directory listing for each directory encountered
182
+ """
183
+ return self.impl.walk(
184
+ path=path, max_depth=max_depth, breadth_first=breadth_first, order=order
185
+ )
186
+
187
+ def scan_missing_folders(
188
+ self,
189
+ src: Dir | Remote | str,
190
+ dst: Dir | Remote | str,
191
+ max_depth: int = -1,
192
+ order: Order = Order.NORMAL,
193
+ ) -> Generator[Dir, None, None]:
194
+ """Walk through the given path recursively.
195
+
196
+ WORK IN PROGRESS!!
197
+
198
+ Args:
199
+ src: Source directory or Remote to walk through
200
+ dst: Destination directory or Remote to walk through
201
+ max_depth: Maximum depth to traverse (-1 for unlimited)
202
+
203
+ Yields:
204
+ DirListing: Directory listing for each directory encountered
205
+ """
206
+ return self.impl.scan_missing_folders(
207
+ src=src, dst=dst, max_depth=max_depth, order=order
208
+ )
209
+
210
+ def cleanup(
211
+ self, path: str, other_args: list[str] | None = None
212
+ ) -> CompletedProcess:
213
+ """Cleanup any resources used by the Rclone instance."""
214
+ return self.impl.cleanup(path=path, other_args=other_args)
215
+
216
+ def copy_to(
217
+ self,
218
+ src: File | str,
219
+ dst: File | str,
220
+ check: bool | None = None,
221
+ verbose: bool | None = None,
222
+ other_args: list[str] | None = None,
223
+ ) -> CompletedProcess:
224
+ """Copy one file from source to destination.
225
+
226
+ Warning - slow.
227
+
228
+ """
229
+ return self.impl.copy_to(
230
+ src=src, dst=dst, check=check, verbose=verbose, other_args=other_args
231
+ )
232
+
233
+ def copy_files(
234
+ self,
235
+ src: str,
236
+ dst: str,
237
+ files: list[str] | Path,
238
+ check: bool | None = None,
239
+ max_backlog: int | None = None,
240
+ verbose: bool | None = None,
241
+ checkers: int | None = None,
242
+ transfers: int | None = None,
243
+ low_level_retries: int | None = None,
244
+ retries: int | None = None,
245
+ retries_sleep: str | None = None,
246
+ metadata: bool | None = None,
247
+ timeout: str | None = None,
248
+ max_partition_workers: int | None = None,
249
+ multi_thread_streams: int | None = None,
250
+ other_args: list[str] | None = None,
251
+ ) -> list[CompletedProcess]:
252
+ """Copy multiple files from source to destination.
253
+
254
+ Args:
255
+ payload: Dictionary of source and destination file paths
256
+ """
257
+ return self.impl.copy_files(
258
+ src=src,
259
+ dst=dst,
260
+ files=files,
261
+ check=check,
262
+ max_backlog=max_backlog,
263
+ verbose=verbose,
264
+ checkers=checkers,
265
+ transfers=transfers,
266
+ low_level_retries=low_level_retries,
267
+ retries=retries,
268
+ retries_sleep=retries_sleep,
269
+ metadata=metadata,
270
+ timeout=timeout,
271
+ max_partition_workers=max_partition_workers,
272
+ multi_thread_streams=multi_thread_streams,
273
+ other_args=other_args,
274
+ )
275
+
276
+ def copy(
277
+ self,
278
+ src: Dir | str,
279
+ dst: Dir | str,
280
+ check: bool | None = None,
281
+ transfers: int | None = None,
282
+ checkers: int | None = None,
283
+ multi_thread_streams: int | None = None,
284
+ low_level_retries: int | None = None,
285
+ retries: int | None = None,
286
+ other_args: list[str] | None = None,
287
+ ) -> CompletedProcess:
288
+ """Copy files from source to destination.
289
+
290
+ Args:
291
+ src: Source directory
292
+ dst: Destination directory
293
+ """
294
+ return self.impl.copy(
295
+ src=src,
296
+ dst=dst,
297
+ check=check,
298
+ transfers=transfers,
299
+ checkers=checkers,
300
+ multi_thread_streams=multi_thread_streams,
301
+ low_level_retries=low_level_retries,
302
+ retries=retries,
303
+ other_args=other_args,
304
+ )
305
+
306
+ def purge(self, path: Dir | str) -> CompletedProcess:
307
+ """Purge a directory"""
308
+ return self.impl.purge(path=path)
309
+
310
+ def delete_files(
311
+ self,
312
+ files: str | File | list[str] | list[File],
313
+ check: bool | None = None,
314
+ rmdirs=False,
315
+ verbose: bool | None = None,
316
+ max_partition_workers: int | None = None,
317
+ other_args: list[str] | None = None,
318
+ ) -> CompletedProcess:
319
+ """Delete a directory"""
320
+ return self.impl.delete_files(
321
+ files=files,
322
+ check=check,
323
+ rmdirs=rmdirs,
324
+ verbose=verbose,
325
+ max_partition_workers=max_partition_workers,
326
+ other_args=other_args,
327
+ )
328
+
329
+ def exists(self, path: Dir | Remote | str | File) -> bool:
330
+ """Check if a file or directory exists."""
331
+ return self.impl.exists(path=path)
332
+
333
+ def is_synced(self, src: str | Dir, dst: str | Dir) -> bool:
334
+ """Check if two directories are in sync."""
335
+ return self.impl.is_synced(src=src, dst=dst)
336
+
337
+ def modtime(self, src: str) -> str | Exception:
338
+ """Get the modification time of a file or directory."""
339
+ return self.impl.modtime(src=src)
340
+
341
+ def modtime_dt(self, src: str) -> datetime | Exception:
342
+ """Get the modification time of a file or directory."""
343
+ return self.impl.modtime_dt(src=src)
344
+
345
+ def write_text(
346
+ self,
347
+ text: str,
348
+ dst: str,
349
+ ) -> Exception | None:
350
+ """Write text to a file."""
351
+ return self.impl.write_text(text=text, dst=dst)
352
+
353
+ def write_bytes(
354
+ self,
355
+ data: bytes,
356
+ dst: str,
357
+ ) -> Exception | None:
358
+ """Write bytes to a file."""
359
+ return self.impl.write_bytes(data=data, dst=dst)
360
+
361
+ def read_bytes(self, src: str) -> bytes | Exception:
362
+ """Read bytes from a file."""
363
+ return self.impl.read_bytes(src=src)
364
+
365
+ def read_text(self, src: str) -> str | Exception:
366
+ """Read text from a file."""
367
+ return self.impl.read_text(src=src)
368
+
369
+ def copy_file_resumable_s3(
370
+ self,
371
+ src: str,
372
+ dst: str,
373
+ save_state_json: Path,
374
+ chunk_size: SizeSuffix | None = None,
375
+ read_threads: int = 8,
376
+ write_threads: int = 8,
377
+ retries: int = 3,
378
+ verbose: bool | None = None,
379
+ max_chunks_before_suspension: int | None = None,
380
+ backend_log: Path | None = None,
381
+ ) -> MultiUploadResult:
382
+ """For massive files that rclone can't handle in one go, this function will copy the file in chunks to an S3 store"""
383
+ return self.impl.copy_file_resumable_s3(
384
+ src=src,
385
+ dst=dst,
386
+ save_state_json=save_state_json,
387
+ chunk_size=chunk_size,
388
+ read_threads=read_threads,
389
+ write_threads=write_threads,
390
+ retries=retries,
391
+ verbose=verbose,
392
+ max_chunks_before_suspension=max_chunks_before_suspension,
393
+ backend_log=backend_log,
394
+ )
395
+
396
+ def copy_bytes(
397
+ self,
398
+ src: str,
399
+ offset: int | SizeSuffix,
400
+ length: int | SizeSuffix,
401
+ outfile: Path,
402
+ other_args: list[str] | None = None,
403
+ ) -> Exception | None:
404
+ """Copy a slice of bytes from the src file to dst."""
405
+ return self.impl.copy_bytes(
406
+ src=src,
407
+ offset=offset,
408
+ length=length,
409
+ outfile=outfile,
410
+ other_args=other_args,
411
+ )
412
+
413
+ def copy_dir(
414
+ self, src: str | Dir, dst: str | Dir, args: list[str] | None = None
415
+ ) -> CompletedProcess:
416
+ """Copy a directory from source to destination."""
417
+ # convert src to str, also dst
418
+ return self.impl.copy_dir(src=src, dst=dst, args=args)
419
+
420
+ def copy_remote(
421
+ self, src: Remote, dst: Remote, args: list[str] | None = None
422
+ ) -> CompletedProcess:
423
+ """Copy a remote to another remote."""
424
+ return self.impl.copy_remote(src=src, dst=dst, args=args)
425
+
426
+ def copy_file_parts(
427
+ self,
428
+ src: str, # src:/Bucket/path/myfile.large.zst
429
+ dst_dir: str, # dst:/Bucket/path/myfile.large.zst-parts/part.{part_number:05d}.start-end
430
+ part_infos: list[PartInfo] | None = None,
431
+ threads: int = 1, # Number of reader and writer threads to use
432
+ ) -> Exception | None:
433
+ """Copy a file in parts."""
434
+ return self.impl.copy_file_parts(
435
+ src=src, dst_dir=dst_dir, part_infos=part_infos, threads=threads
436
+ )
437
+
438
+ def mount(
439
+ self,
440
+ src: Remote | Dir | str,
441
+ outdir: Path,
442
+ allow_writes: bool | None = False,
443
+ use_links: bool | None = None,
444
+ vfs_cache_mode: str | None = None,
445
+ verbose: bool | None = None,
446
+ cache_dir: Path | None = None,
447
+ cache_dir_delete_on_exit: bool | None = None,
448
+ log: Path | None = None,
449
+ other_args: list[str] | None = None,
450
+ ) -> Mount:
451
+ """Mount a remote or directory to a local path.
452
+
453
+ Args:
454
+ src: Remote or directory to mount
455
+ outdir: Local path to mount to
456
+
457
+ Returns:
458
+ CompletedProcess from the mount command execution
459
+
460
+ Raises:
461
+ subprocess.CalledProcessError: If the mount operation fails
462
+ """
463
+ return self.impl.mount(
464
+ src=src,
465
+ outdir=outdir,
466
+ allow_writes=allow_writes,
467
+ use_links=use_links,
468
+ vfs_cache_mode=vfs_cache_mode,
469
+ verbose=verbose,
470
+ cache_dir=cache_dir,
471
+ cache_dir_delete_on_exit=cache_dir_delete_on_exit,
472
+ log=log,
473
+ other_args=other_args,
474
+ )
475
+
476
+ def serve_http(
477
+ self,
478
+ src: str,
479
+ addr: str = "localhost:8080",
480
+ other_args: list[str] | None = None,
481
+ ) -> HttpServer:
482
+ """Serve a remote or directory via HTTP. The returned HttpServer has a client which can be used to
483
+ fetch files or parts.
484
+
485
+ Args:
486
+ src: Remote or directory to serve
487
+ addr: Network address and port to serve on (default: localhost:8080)
488
+ """
489
+ return self.impl.serve_http(src=src, addr=addr, other_args=other_args)
490
+
491
+ def size_files(
492
+ self,
493
+ src: str,
494
+ files: list[str],
495
+ fast_list: bool = False, # Recommend that this is False
496
+ other_args: list[str] | None = None,
497
+ check: bool | None = False,
498
+ verbose: bool | None = None,
499
+ ) -> SizeResult:
500
+ """Get the size of a list of files. Example of files items: "remote:bucket/to/file"."""
501
+ return self.impl.size_files(
502
+ src=src,
503
+ files=files,
504
+ fast_list=fast_list,
505
+ other_args=other_args,
506
+ check=check,
507
+ verbose=verbose,
508
+ )
509
+
22
510
 
23
511
  __all__ = [
24
512
  "Rclone",
@@ -49,6 +537,5 @@ __all__ = [
49
537
  "HttpServer",
50
538
  "Range",
51
539
  "HttpFetcher",
540
+ "PartInfo",
52
541
  ]
53
-
54
- setup_default_logging()
@@ -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, SizeSuffix
5
+ from rclone_api import Rclone, SizeSuffix
6
6
 
7
7
 
8
8
  @dataclass
@@ -85,17 +85,24 @@ def main() -> int:
85
85
  args = _parse_args()
86
86
  rclone = Rclone(rclone_conf=args.config_path)
87
87
  # unit_chunk = args.chunk_size / args.threads
88
- rslt: MultiUploadResult = rclone.copy_file_resumable_s3(
88
+ # rslt: MultiUploadResult = rclone.copy_file_resumable_s3(
89
+ # src=args.src,
90
+ # dst=args.dst,
91
+ # chunk_size=args.chunk_size,
92
+ # read_threads=args.read_threads,
93
+ # write_threads=args.write_threads,
94
+ # retries=args.retries,
95
+ # save_state_json=args.save_state_json,
96
+ # verbose=args.verbose,
97
+ # )
98
+ err: Exception | None = rclone.copy_file_parts(
89
99
  src=args.src,
90
- dst=args.dst,
91
- chunk_size=args.chunk_size,
92
- read_threads=args.read_threads,
93
- write_threads=args.write_threads,
94
- retries=args.retries,
95
- save_state_json=args.save_state_json,
96
- verbose=args.verbose,
100
+ dst_dir=args.dst,
101
+ # verbose=args.verbose,
97
102
  )
98
- print(rslt)
103
+ if err is not None:
104
+ print(f"Error: {err}")
105
+ raise err
99
106
  return 0
100
107
 
101
108
 
rclone_api/db/db.py CHANGED
@@ -70,14 +70,14 @@ class DB:
70
70
  repo = self.get_or_create_repo(remote_name)
71
71
  repo.insert_files(files)
72
72
 
73
- def query_files(self, remote_name: str) -> list[FileItem]:
73
+ def query_all_files(self, remote_name: str) -> list[FileItem]:
74
74
  """Query files from the database.
75
75
 
76
76
  Args:
77
77
  remote_name: Name of the remote
78
78
  """
79
79
  repo = self.get_or_create_repo(remote_name)
80
- files = repo.get_files()
80
+ files = repo.get_all_files()
81
81
  out: list[FileItem] = []
82
82
  for file in files:
83
83
  out.append(file)
@@ -245,7 +245,7 @@ class DBRepo:
245
245
  # Return the set of FileItem objects that have a path in the existing_paths.
246
246
  return {file for file in files if file.path_no_remote in existing_paths}
247
247
 
248
- def get_files(self) -> list[FileItem]:
248
+ def get_all_files(self) -> list[FileItem]:
249
249
  """Get all files in the table.
250
250
 
251
251
  Returns: