rclone-api 1.0.68__py2.py3-none-any.whl → 1.0.69__py2.py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|