rclone-api 1.0.68__py2.py3-none-any.whl → 1.0.69__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.
- rclone_api/diff_walk.py +143 -0
- rclone_api/dir.py +46 -2
- rclone_api/rclone.py +32 -13
- rclone_api/types.py +12 -0
- rclone_api/walk.py +10 -9
- {rclone_api-1.0.68.dist-info → rclone_api-1.0.69.dist-info}/METADATA +1 -1
- {rclone_api-1.0.68.dist-info → rclone_api-1.0.69.dist-info}/RECORD +11 -9
- {rclone_api-1.0.68.dist-info → rclone_api-1.0.69.dist-info}/LICENSE +0 -0
- {rclone_api-1.0.68.dist-info → rclone_api-1.0.69.dist-info}/WHEEL +0 -0
- {rclone_api-1.0.68.dist-info → rclone_api-1.0.69.dist-info}/entry_points.txt +0 -0
- {rclone_api-1.0.68.dist-info → rclone_api-1.0.69.dist-info}/top_level.txt +0 -0
rclone_api/diff_walk.py
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
from concurrent.futures import ThreadPoolExecutor
|
2
|
+
from queue import Queue
|
3
|
+
from threading import Thread
|
4
|
+
from typing import Generator
|
5
|
+
|
6
|
+
from rclone_api import Dir
|
7
|
+
from rclone_api.dir_listing import DirListing
|
8
|
+
from rclone_api.types import ListingOption
|
9
|
+
from rclone_api.walk import walk_runner_depth_first
|
10
|
+
|
11
|
+
_MAX_OUT_QUEUE_SIZE = 50
|
12
|
+
|
13
|
+
|
14
|
+
# ONLY Works from src -> dst diffing.
|
15
|
+
def _async_diff_dir_walk_task(
|
16
|
+
src: Dir, dst: Dir, max_depth: int, out_queue: Queue[Dir | None], reverse=False
|
17
|
+
) -> None:
|
18
|
+
|
19
|
+
try:
|
20
|
+
stack = [(src, dst)]
|
21
|
+
while stack:
|
22
|
+
curr_src, curr_dst = stack.pop()
|
23
|
+
curr_src = curr_src
|
24
|
+
curr_dst = curr_dst
|
25
|
+
|
26
|
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
27
|
+
# src_dir_listing = src.ls(listing_option=ListingOption.DIRS_ONLY)
|
28
|
+
# dst_dir_listing = dst.ls(listing_option=ListingOption.DIRS_ONLY)
|
29
|
+
t1 = executor.submit(
|
30
|
+
src.ls, listing_option=ListingOption.DIRS_ONLY, reverse=reverse
|
31
|
+
)
|
32
|
+
t2 = executor.submit(
|
33
|
+
dst.ls, listing_option=ListingOption.DIRS_ONLY, reverse=reverse
|
34
|
+
)
|
35
|
+
src_dir_listing: DirListing = t1.result()
|
36
|
+
dst_dir_listing: DirListing = t2.result()
|
37
|
+
|
38
|
+
# dirlisting = current_dir.ls()
|
39
|
+
# if reverse:
|
40
|
+
# dirlisting.dirs.reverse()
|
41
|
+
# if depth != 0:
|
42
|
+
# for subdir in dirlisting.dirs: # Process deeper directories first
|
43
|
+
# # stack.append((child, depth - 1 if depth > 0 else depth))
|
44
|
+
# next_depth = depth - 1 if depth > 0 else depth
|
45
|
+
# _walk_runner_depth_first(
|
46
|
+
# subdir, next_depth, out_queue, reverse=reverse
|
47
|
+
# )
|
48
|
+
# out_queue.put(dirlisting)
|
49
|
+
|
50
|
+
# for subdir in dst_dir_listing.dirs:
|
51
|
+
# subdir.to_string(include_remote=False)
|
52
|
+
# walk_runner_depth_first()
|
53
|
+
|
54
|
+
# find elements missing on dst
|
55
|
+
# missing_on_dst: set[Dir] = set(src_dir_listing.dirs) - set(
|
56
|
+
# dst_dir_listing.dirs
|
57
|
+
# )
|
58
|
+
# exists_on_dst: set[Dir] = set(src_dir_listing.dirs) - missing_on_dst
|
59
|
+
|
60
|
+
dst_files: list[str] = [d.name for d in dst_dir_listing.dirs]
|
61
|
+
src_files: list[str] = [d.name for d in src_dir_listing.dirs]
|
62
|
+
|
63
|
+
dst_files_set: set[str] = set(dst_files)
|
64
|
+
# src_files_set: set[str] = set(src_files)
|
65
|
+
|
66
|
+
# print(f"src_files: {src_files}")
|
67
|
+
# print(f"dst_files: {dst_files}")
|
68
|
+
|
69
|
+
matching_dirs: list[str] = []
|
70
|
+
|
71
|
+
for file in src_files:
|
72
|
+
if file not in dst_files_set:
|
73
|
+
# print(f"missing dir on src: {file}")
|
74
|
+
queue_dir_listing: Queue[DirListing | None] = Queue()
|
75
|
+
walk_runner_depth_first(
|
76
|
+
dir=curr_src,
|
77
|
+
max_depth=max_depth,
|
78
|
+
out_queue=queue_dir_listing,
|
79
|
+
reverse=reverse,
|
80
|
+
)
|
81
|
+
while dirlisting := queue_dir_listing.get():
|
82
|
+
if dirlisting is None:
|
83
|
+
break
|
84
|
+
# print(f"dirlisting: {dirlisting}")
|
85
|
+
for d in dirlisting.dirs:
|
86
|
+
out_queue.put(d)
|
87
|
+
else:
|
88
|
+
matching_dirs.append(file)
|
89
|
+
|
90
|
+
for matching_dir in matching_dirs:
|
91
|
+
# print(f"matching dir: {matching_dir}")
|
92
|
+
_async_diff_dir_walk_task(
|
93
|
+
src=curr_src / matching_dir,
|
94
|
+
dst=curr_dst / matching_dir,
|
95
|
+
max_depth=max_depth,
|
96
|
+
out_queue=out_queue,
|
97
|
+
reverse=reverse,
|
98
|
+
)
|
99
|
+
|
100
|
+
out_queue.put(None)
|
101
|
+
except KeyboardInterrupt:
|
102
|
+
import _thread
|
103
|
+
|
104
|
+
out_queue.put(None)
|
105
|
+
_thread.interrupt_main()
|
106
|
+
|
107
|
+
|
108
|
+
def diff_walk(
|
109
|
+
src: Dir,
|
110
|
+
dst: Dir,
|
111
|
+
max_depth: int = -1,
|
112
|
+
reverse: bool = False,
|
113
|
+
) -> Generator[Dir, None, None]:
|
114
|
+
"""Walk through the given directory recursively.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
dir: Directory or Remote to walk through
|
118
|
+
max_depth: Maximum depth to traverse (-1 for unlimited)
|
119
|
+
|
120
|
+
Yields:
|
121
|
+
DirListing: Directory listing for each directory encountered
|
122
|
+
"""
|
123
|
+
|
124
|
+
try:
|
125
|
+
out_queue: Queue[Dir | None] = Queue(maxsize=_MAX_OUT_QUEUE_SIZE)
|
126
|
+
|
127
|
+
def task() -> None:
|
128
|
+
_async_diff_dir_walk_task(src, dst, max_depth, out_queue, reverse=reverse)
|
129
|
+
|
130
|
+
worker = Thread(
|
131
|
+
target=task,
|
132
|
+
daemon=True,
|
133
|
+
)
|
134
|
+
worker.start()
|
135
|
+
|
136
|
+
while dirlisting := out_queue.get():
|
137
|
+
if dirlisting is None:
|
138
|
+
break
|
139
|
+
yield dirlisting
|
140
|
+
|
141
|
+
worker.join()
|
142
|
+
except KeyboardInterrupt:
|
143
|
+
pass
|
rclone_api/dir.py
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
import json
|
2
|
+
from pathlib import Path
|
2
3
|
from typing import Generator
|
3
4
|
|
4
5
|
from rclone_api.dir_listing import DirListing
|
5
6
|
from rclone_api.remote import Remote
|
6
7
|
from rclone_api.rpath import RPath
|
8
|
+
from rclone_api.types import ListingOption
|
7
9
|
|
8
10
|
|
9
11
|
class Dir:
|
@@ -41,11 +43,30 @@ class Dir:
|
|
41
43
|
# self.path.set_rclone(self.path.remote.rclone)
|
42
44
|
assert self.path.rclone is not None
|
43
45
|
|
44
|
-
def ls(
|
46
|
+
def ls(
|
47
|
+
self,
|
48
|
+
max_depth: int | None = None,
|
49
|
+
glob: str | None = None,
|
50
|
+
reverse: bool = False,
|
51
|
+
listing_option: ListingOption = ListingOption.ALL,
|
52
|
+
) -> DirListing:
|
45
53
|
"""List files and directories in the given path."""
|
46
54
|
assert self.path.rclone is not None
|
47
55
|
dir = Dir(self.path)
|
48
|
-
return self.path.rclone.ls(
|
56
|
+
return self.path.rclone.ls(
|
57
|
+
dir,
|
58
|
+
max_depth=max_depth,
|
59
|
+
glob=glob,
|
60
|
+
reverse=reverse,
|
61
|
+
listing_option=listing_option,
|
62
|
+
)
|
63
|
+
|
64
|
+
def relative_to(self, other: "Dir") -> str:
|
65
|
+
"""Return the relative path to the other directory."""
|
66
|
+
self_path = Path(self.path.path)
|
67
|
+
other_path = Path(other.path.path)
|
68
|
+
rel_path = self_path.relative_to(other_path)
|
69
|
+
return str(rel_path)
|
49
70
|
|
50
71
|
def walk(
|
51
72
|
self, breadth_first: bool, max_depth: int = -1
|
@@ -67,3 +88,26 @@ class Dir:
|
|
67
88
|
data = self.path.to_json()
|
68
89
|
data_str = json.dumps(data)
|
69
90
|
return data_str
|
91
|
+
|
92
|
+
def to_string(self, include_remote: bool = True) -> str:
|
93
|
+
"""Convert the File to a string."""
|
94
|
+
out = str(self.path)
|
95
|
+
if not include_remote:
|
96
|
+
_, out = out.split(":", 1)
|
97
|
+
return out
|
98
|
+
|
99
|
+
# / operator
|
100
|
+
def __truediv__(self, other: str) -> "Dir":
|
101
|
+
"""Join the current path with another path."""
|
102
|
+
path = Path(self.path.path) / other
|
103
|
+
rpath = RPath(
|
104
|
+
self.path.remote,
|
105
|
+
str(path),
|
106
|
+
name=other,
|
107
|
+
size=0,
|
108
|
+
mime_type="inode/directory",
|
109
|
+
mod_time="",
|
110
|
+
is_dir=True,
|
111
|
+
)
|
112
|
+
rpath.set_rclone(self.path.rclone)
|
113
|
+
return Dir(rpath)
|
rclone_api/rclone.py
CHANGED
@@ -7,7 +7,6 @@ import subprocess
|
|
7
7
|
import time
|
8
8
|
import warnings
|
9
9
|
from concurrent.futures import Future, ThreadPoolExecutor
|
10
|
-
from enum import Enum
|
11
10
|
from fnmatch import fnmatch
|
12
11
|
from pathlib import Path
|
13
12
|
from tempfile import TemporaryDirectory
|
@@ -26,6 +25,7 @@ from rclone_api.group_files import group_files
|
|
26
25
|
from rclone_api.process import Process
|
27
26
|
from rclone_api.remote import Remote
|
28
27
|
from rclone_api.rpath import RPath
|
28
|
+
from rclone_api.types import ListingOption, ModTimeStrategy
|
29
29
|
from rclone_api.util import (
|
30
30
|
get_check,
|
31
31
|
get_rclone_exe,
|
@@ -42,17 +42,6 @@ def rclone_verbose(verbose: bool | None) -> bool:
|
|
42
42
|
return bool(int(os.getenv("RCLONE_API_VERBOSE", "0")))
|
43
43
|
|
44
44
|
|
45
|
-
class ModTimeStrategy(Enum):
|
46
|
-
USE_SERVER_MODTIME = "use-server-modtime"
|
47
|
-
NO_MODTIME = "no-modtime"
|
48
|
-
|
49
|
-
|
50
|
-
class ListingOption(Enum):
|
51
|
-
DIRS_ONLY = "dirs-only"
|
52
|
-
FILES_ONLY = "files-only"
|
53
|
-
ALL = "all"
|
54
|
-
|
55
|
-
|
56
45
|
class Rclone:
|
57
46
|
def __init__(
|
58
47
|
self, rclone_conf: Path | Config, rclone_exe: Path | None = None
|
@@ -203,17 +192,20 @@ class Rclone:
|
|
203
192
|
diff_option: DiffOption = DiffOption.COMBINED,
|
204
193
|
fast_list: bool = True,
|
205
194
|
size_only: bool | None = None,
|
195
|
+
checkers: int | None = None,
|
206
196
|
other_args: list[str] | None = None,
|
207
197
|
) -> Generator[DiffItem, None, None]:
|
208
198
|
"""Be extra careful with the src and dst values. If you are off by one
|
209
199
|
parent directory, you will get a huge amount of false diffs."""
|
210
200
|
other_args = other_args or []
|
201
|
+
if checkers is None or checkers < 1:
|
202
|
+
checkers = 1000
|
211
203
|
cmd = [
|
212
204
|
"check",
|
213
205
|
src,
|
214
206
|
dst,
|
215
207
|
"--checkers",
|
216
|
-
|
208
|
+
str(checkers),
|
217
209
|
"--log-level",
|
218
210
|
"INFO",
|
219
211
|
f"--{diff_option.value}",
|
@@ -288,6 +280,33 @@ class Rclone:
|
|
288
280
|
dir_obj, max_depth=max_depth, breadth_first=breadth_first, reverse=reverse
|
289
281
|
)
|
290
282
|
|
283
|
+
def diff_walk(
|
284
|
+
self,
|
285
|
+
src: Dir | Remote | str,
|
286
|
+
dst: Dir | Remote | str,
|
287
|
+
max_depth: int = -1,
|
288
|
+
reverse: bool = False,
|
289
|
+
) -> Generator[Dir, None, None]:
|
290
|
+
"""Walk through the given path recursively.
|
291
|
+
|
292
|
+
WORK IN PROGRESS!!
|
293
|
+
|
294
|
+
Args:
|
295
|
+
src: Source directory or Remote to walk through
|
296
|
+
dst: Destination directory or Remote to walk through
|
297
|
+
max_depth: Maximum depth to traverse (-1 for unlimited)
|
298
|
+
|
299
|
+
Yields:
|
300
|
+
DirListing: Directory listing for each directory encountered
|
301
|
+
"""
|
302
|
+
from rclone_api.diff_walk import diff_walk
|
303
|
+
|
304
|
+
src_dir = Dir(to_path(src, self))
|
305
|
+
dst_dir = Dir(to_path(dst, self))
|
306
|
+
yield from diff_walk(
|
307
|
+
src=src_dir, dst=dst_dir, max_depth=max_depth, reverse=reverse
|
308
|
+
)
|
309
|
+
|
291
310
|
def cleanup(
|
292
311
|
self, path: str, other_args: list[str] | None = None
|
293
312
|
) -> CompletedProcess:
|
rclone_api/types.py
ADDED
rclone_api/walk.py
CHANGED
@@ -9,7 +9,7 @@ from rclone_api.remote import Remote
|
|
9
9
|
_MAX_OUT_QUEUE_SIZE = 50
|
10
10
|
|
11
11
|
|
12
|
-
def
|
12
|
+
def walk_runner_breadth_first(
|
13
13
|
dir: Dir,
|
14
14
|
max_depth: int,
|
15
15
|
out_queue: Queue[DirListing | None],
|
@@ -40,7 +40,7 @@ def _walk_runner_breadth_first(
|
|
40
40
|
_thread.interrupt_main()
|
41
41
|
|
42
42
|
|
43
|
-
def
|
43
|
+
def walk_runner_depth_first(
|
44
44
|
dir: Dir, max_depth: int, out_queue: Queue[DirListing | None], reverse=False
|
45
45
|
) -> None:
|
46
46
|
try:
|
@@ -54,7 +54,7 @@ def _walk_runner_depth_first(
|
|
54
54
|
for subdir in dirlisting.dirs: # Process deeper directories first
|
55
55
|
# stack.append((child, depth - 1 if depth > 0 else depth))
|
56
56
|
next_depth = depth - 1 if depth > 0 else depth
|
57
|
-
|
57
|
+
walk_runner_depth_first(
|
58
58
|
subdir, next_depth, out_queue, reverse=reverse
|
59
59
|
)
|
60
60
|
out_queue.put(dirlisting)
|
@@ -85,16 +85,17 @@ def walk(
|
|
85
85
|
# Convert Remote to Dir if needed
|
86
86
|
if isinstance(dir, Remote):
|
87
87
|
dir = Dir(dir)
|
88
|
-
out_queue: Queue[DirListing] = Queue(maxsize=_MAX_OUT_QUEUE_SIZE)
|
88
|
+
out_queue: Queue[DirListing | None] = Queue(maxsize=_MAX_OUT_QUEUE_SIZE)
|
89
89
|
|
90
|
-
|
91
|
-
|
92
|
-
|
90
|
+
def _task() -> None:
|
91
|
+
if breadth_first:
|
92
|
+
walk_runner_breadth_first(dir, max_depth, out_queue, reverse)
|
93
|
+
else:
|
94
|
+
walk_runner_depth_first(dir, max_depth, out_queue, reverse)
|
93
95
|
|
94
96
|
# Start worker thread
|
95
97
|
worker = Thread(
|
96
|
-
target=
|
97
|
-
args=(dir, max_depth, out_queue, reverse),
|
98
|
+
target=_task,
|
98
99
|
daemon=True,
|
99
100
|
)
|
100
101
|
worker.start()
|
@@ -5,23 +5,25 @@ rclone_api/config.py,sha256=tP6cU9DnCCEIRc_KP9HPur1jFLLg2QGFSxNwFm6_MVw,118
|
|
5
5
|
rclone_api/convert.py,sha256=Mx9Qo7zhkOedJd8LdhPvNGHp8znJzOk4f_2KWnoGc78,1012
|
6
6
|
rclone_api/deprecated.py,sha256=qWKpnZdYcBK7YQZKuVoWWXDwi-uqiAtbjgPcci_efow,590
|
7
7
|
rclone_api/diff.py,sha256=ggdDLUZxa13jMcPzKBcwAElmPCNWMOSR89D4yhpO74M,5264
|
8
|
-
rclone_api/
|
8
|
+
rclone_api/diff_walk.py,sha256=V8U6qwCzG7_Z6-xFpB4zP6kCq_jN30OMYo7tT92OTd0,5061
|
9
|
+
rclone_api/dir.py,sha256=8EwaGonkCeHhe-rsVl98PkEKvyf-3pFUJnXqkN6Fx24,3509
|
9
10
|
rclone_api/dir_listing.py,sha256=9Qqf2SUswrOEkyqmaH23V51I18X6ePiXb9B1vUwRF5o,1571
|
10
11
|
rclone_api/exec.py,sha256=1ovvaMXDEfLiT7BrYZyE85u_yFhEUwUNW3jPOzqknR8,1023
|
11
12
|
rclone_api/file.py,sha256=YtR5Y6c0YfXTS-sReOy2UgiSnafcAeO6b2hnbojBQD4,1423
|
12
13
|
rclone_api/filelist.py,sha256=xbiusvNgaB_b_kQOZoHMJJxn6TWGtPrWd2J042BI28o,767
|
13
14
|
rclone_api/group_files.py,sha256=kOHh6ysFDkxjldSwvW6KqmiADUC1yFCdrZRY57TvbGY,5328
|
14
15
|
rclone_api/process.py,sha256=RrMfTe0bndmJ6gBK67ioqNvCstJ8aTC8RlGX1XBLlcw,4191
|
15
|
-
rclone_api/rclone.py,sha256=
|
16
|
+
rclone_api/rclone.py,sha256=nN1bqUmiopCKP6VNG4XpSfBfWx4EuAIyxB9NevYp-OA,28854
|
16
17
|
rclone_api/remote.py,sha256=c9hlRKBCg1BFB9MCINaQIoCg10qyAkeqiS4brl8ce-8,343
|
17
18
|
rclone_api/rpath.py,sha256=8ZA_1wxWtskwcy0I8V2VbjKDmzPkiWd8Q2JQSvh-sYE,2586
|
19
|
+
rclone_api/types.py,sha256=VXA-56va2S41onAWM_UmhHlG4qeyMUEF5dsVFyfLlH8,232
|
18
20
|
rclone_api/util.py,sha256=_cvmHcJPRl2yXw4zgZiop3z-riA_8Ek6S5NDPw8cqSY,4198
|
19
|
-
rclone_api/walk.py,sha256=
|
21
|
+
rclone_api/walk.py,sha256=ndWV7WBVQLbpZu3HuJrAe1cXcmQVjT9_YPsbat158bQ,3231
|
20
22
|
rclone_api/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
|
21
23
|
rclone_api/cmd/list_files.py,sha256=x8FHODEilwKqwdiU1jdkeJbLwOqUkUQuDWPo2u_zpf0,741
|
22
|
-
rclone_api-1.0.
|
23
|
-
rclone_api-1.0.
|
24
|
-
rclone_api-1.0.
|
25
|
-
rclone_api-1.0.
|
26
|
-
rclone_api-1.0.
|
27
|
-
rclone_api-1.0.
|
24
|
+
rclone_api-1.0.69.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
|
25
|
+
rclone_api-1.0.69.dist-info/METADATA,sha256=bl94_jCcD0a0buUsLVFIQ-MPQ5O2fy-hFrvK1EvU7Ls,4489
|
26
|
+
rclone_api-1.0.69.dist-info/WHEEL,sha256=9Hm2OB-j1QcCUq9Jguht7ayGIIZBRTdOXD1qg9cCgPM,109
|
27
|
+
rclone_api-1.0.69.dist-info/entry_points.txt,sha256=XUoTX3m7CWxdj2VAKhEuO0NMOfX2qf-OcEDFwdyk9ZE,72
|
28
|
+
rclone_api-1.0.69.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
|
29
|
+
rclone_api-1.0.69.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|