rclone-api 1.0.40__py2.py3-none-any.whl → 1.0.42__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/group_files.py +145 -0
- rclone_api/rclone.py +81 -48
- rclone_api/util.py +0 -12
- {rclone_api-1.0.40.dist-info → rclone_api-1.0.42.dist-info}/METADATA +1 -1
- {rclone_api-1.0.40.dist-info → rclone_api-1.0.42.dist-info}/RECORD +9 -8
- {rclone_api-1.0.40.dist-info → rclone_api-1.0.42.dist-info}/LICENSE +0 -0
- {rclone_api-1.0.40.dist-info → rclone_api-1.0.42.dist-info}/WHEEL +0 -0
- {rclone_api-1.0.40.dist-info → rclone_api-1.0.42.dist-info}/entry_points.txt +0 -0
- {rclone_api-1.0.40.dist-info → rclone_api-1.0.42.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,145 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
|
3
|
+
|
4
|
+
@dataclass
|
5
|
+
class FilePathParts:
|
6
|
+
"""File path dataclass."""
|
7
|
+
|
8
|
+
remote: str
|
9
|
+
parents: list[str]
|
10
|
+
name: str
|
11
|
+
|
12
|
+
|
13
|
+
def parse_file(file_path: str) -> FilePathParts:
|
14
|
+
"""Parse file path into parts."""
|
15
|
+
assert not file_path.endswith("/"), "This looks like a directory path"
|
16
|
+
parts = file_path.split(":")
|
17
|
+
remote = parts[0]
|
18
|
+
path = parts[1]
|
19
|
+
if path.startswith("/"):
|
20
|
+
path = path[1:]
|
21
|
+
parents = path.split("/")
|
22
|
+
if len(parents) == 1:
|
23
|
+
return FilePathParts(remote=remote, parents=[], name=parents[0])
|
24
|
+
name = parents.pop()
|
25
|
+
return FilePathParts(remote=remote, parents=parents, name=name)
|
26
|
+
|
27
|
+
|
28
|
+
class TreeNode:
|
29
|
+
def __init__(
|
30
|
+
self,
|
31
|
+
name: str,
|
32
|
+
child_nodes: dict[str, "TreeNode"] | None = None,
|
33
|
+
files: list[str] | None = None,
|
34
|
+
parent: "TreeNode | None" = None,
|
35
|
+
):
|
36
|
+
self.name = name
|
37
|
+
self.child_nodes = child_nodes or {}
|
38
|
+
self.files = files or []
|
39
|
+
self.count = 0
|
40
|
+
self.parent = parent
|
41
|
+
|
42
|
+
def add_count(self):
|
43
|
+
self.count += 1
|
44
|
+
if self.parent:
|
45
|
+
self.parent.add_count()
|
46
|
+
|
47
|
+
def get_path(self) -> str:
|
48
|
+
paths_reversed: list[str] = [self.name]
|
49
|
+
node: TreeNode | None = self
|
50
|
+
assert node is not None
|
51
|
+
while node := node.parent:
|
52
|
+
paths_reversed.append(node.name)
|
53
|
+
return "/".join(reversed(paths_reversed))
|
54
|
+
|
55
|
+
def get_child_subpaths(self, parent_path: str | None = None) -> list[str]:
|
56
|
+
paths: list[str] = []
|
57
|
+
for child in self.child_nodes.values():
|
58
|
+
child_paths = child.get_child_subpaths(parent_path=child.name)
|
59
|
+
paths.extend(child_paths)
|
60
|
+
for file in self.files:
|
61
|
+
if parent_path:
|
62
|
+
file = f"{parent_path}/{file}"
|
63
|
+
paths.append(file)
|
64
|
+
return paths
|
65
|
+
|
66
|
+
def __repr__(self, indent: int = 0) -> str:
|
67
|
+
# return f"{self.name}: {self.count}, {len(self.children)}"
|
68
|
+
leftpad = " " * indent
|
69
|
+
msg = f"{leftpad}{self.name}: {self.count}"
|
70
|
+
if self.child_nodes:
|
71
|
+
# msg += f"\n {len(self.children)} children"
|
72
|
+
msg += "\n"
|
73
|
+
for child in self.child_nodes.values():
|
74
|
+
if isinstance(child, TreeNode):
|
75
|
+
msg += child.__repr__(indent + 2)
|
76
|
+
else:
|
77
|
+
msg += f"{leftpad} {child}\n"
|
78
|
+
return msg
|
79
|
+
|
80
|
+
|
81
|
+
def _merge(node: TreeNode, parent_path: str, out: dict[str, list[str]]) -> None:
|
82
|
+
parent_path = parent_path + "/" + node.name
|
83
|
+
this_count = node.count
|
84
|
+
child_count = 0
|
85
|
+
children_has_files = False
|
86
|
+
if not node.child_nodes and not node.files:
|
87
|
+
return # done
|
88
|
+
|
89
|
+
if node.files:
|
90
|
+
children_has_files = True
|
91
|
+
filelist = out.setdefault(parent_path, [])
|
92
|
+
# for file in node.files:
|
93
|
+
# filelist.append(file)
|
94
|
+
# out[parent_path] = filelist
|
95
|
+
paths = node.get_child_subpaths()
|
96
|
+
for path in paths:
|
97
|
+
filelist.append(path)
|
98
|
+
out[parent_path] = filelist
|
99
|
+
return
|
100
|
+
|
101
|
+
for child in node.child_nodes.values():
|
102
|
+
child_count += child.count
|
103
|
+
child_count += len(node.files)
|
104
|
+
for file in node.files:
|
105
|
+
child_count += 1
|
106
|
+
|
107
|
+
if child_count != this_count or children_has_files:
|
108
|
+
# print(
|
109
|
+
# f"Cannot merge {node.name} because different counts or has children with files"
|
110
|
+
# )
|
111
|
+
filelist = out.setdefault(parent_path, [])
|
112
|
+
for child in node.child_nodes.values():
|
113
|
+
subpaths = child.get_child_subpaths()
|
114
|
+
filelist.extend(subpaths)
|
115
|
+
out[parent_path] = filelist
|
116
|
+
else:
|
117
|
+
for child in node.child_nodes.values():
|
118
|
+
_merge(child, parent_path, out)
|
119
|
+
|
120
|
+
|
121
|
+
def group_files(files: list[str]) -> dict[str, list[str]]:
|
122
|
+
"""split between filename and parent directory path"""
|
123
|
+
tree: dict[str, TreeNode] = {}
|
124
|
+
for file in files:
|
125
|
+
parts = parse_file(file)
|
126
|
+
remote = parts.remote
|
127
|
+
node: TreeNode = tree.setdefault(remote, TreeNode(remote))
|
128
|
+
for parent in parts.parents:
|
129
|
+
is_last = parent == parts.parents[-1]
|
130
|
+
node = node.child_nodes.setdefault(parent, TreeNode(parent, parent=node))
|
131
|
+
if is_last:
|
132
|
+
node.files.append(parts.name)
|
133
|
+
node.add_count()
|
134
|
+
outpaths: dict[str, list[str]] = {}
|
135
|
+
for _, node in tree.items():
|
136
|
+
_merge(node, "", outpaths)
|
137
|
+
out: dict[str, list[str]] = {}
|
138
|
+
for path, files in outpaths.items():
|
139
|
+
# fixup path
|
140
|
+
assert path.startswith("/"), "Path should start with /"
|
141
|
+
path = path[1:]
|
142
|
+
# replace the first / with :
|
143
|
+
path = path.replace("/", ":", 1)
|
144
|
+
out[path] = files
|
145
|
+
return out
|
rclone_api/rclone.py
CHANGED
@@ -5,6 +5,7 @@ Unit test file.
|
|
5
5
|
import subprocess
|
6
6
|
import time
|
7
7
|
import warnings
|
8
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
8
9
|
from enum import Enum
|
9
10
|
from fnmatch import fnmatch
|
10
11
|
from pathlib import Path
|
@@ -20,12 +21,20 @@ from rclone_api.diff import DiffItem, diff_stream_from_running_process
|
|
20
21
|
from rclone_api.dir_listing import DirListing
|
21
22
|
from rclone_api.exec import RcloneExec
|
22
23
|
from rclone_api.file import File
|
24
|
+
from rclone_api.group_files import group_files
|
23
25
|
from rclone_api.process import Process
|
24
26
|
from rclone_api.remote import Remote
|
25
27
|
from rclone_api.rpath import RPath
|
26
|
-
from rclone_api.util import
|
28
|
+
from rclone_api.util import (
|
29
|
+
get_rclone_exe,
|
30
|
+
get_verbose,
|
31
|
+
to_path,
|
32
|
+
wait_for_mount,
|
33
|
+
)
|
27
34
|
from rclone_api.walk import walk
|
28
35
|
|
36
|
+
EXECUTOR = ThreadPoolExecutor(16)
|
37
|
+
|
29
38
|
|
30
39
|
class ModTimeStrategy(Enum):
|
31
40
|
USE_SERVER_MODTIME = "use-server-modtime"
|
@@ -197,7 +206,7 @@ class Rclone:
|
|
197
206
|
cmd_list: list[str] = ["copyto", src, dst]
|
198
207
|
self._run(cmd_list)
|
199
208
|
|
200
|
-
def copyfiles(self, files: str | File | list[str] | list[File]) -> None:
|
209
|
+
def copyfiles(self, files: str | File | list[str] | list[File], check=True) -> None:
|
201
210
|
"""Copy multiple files from source to destination.
|
202
211
|
|
203
212
|
Warning - slow.
|
@@ -209,31 +218,42 @@ class Rclone:
|
|
209
218
|
if len(payload) == 0:
|
210
219
|
return
|
211
220
|
|
212
|
-
datalists: dict[str, list[str]] =
|
221
|
+
datalists: dict[str, list[str]] = group_files(payload)
|
213
222
|
out: subprocess.CompletedProcess | None = None
|
214
223
|
|
224
|
+
futures: list[Future] = []
|
225
|
+
|
215
226
|
for remote, files in datalists.items():
|
216
|
-
with TemporaryDirectory() as tmpdir:
|
217
|
-
include_files_txt = Path(tmpdir) / "include_files.txt"
|
218
|
-
include_files_txt.write_text("\n".join(files), encoding="utf-8")
|
219
|
-
|
220
|
-
# print(include_files_txt)
|
221
|
-
cmd_list: list[str] = [
|
222
|
-
"delete",
|
223
|
-
remote,
|
224
|
-
"--files-from",
|
225
|
-
str(include_files_txt),
|
226
|
-
"--checkers",
|
227
|
-
"1000",
|
228
|
-
"--transfers",
|
229
|
-
"1000",
|
230
|
-
]
|
231
|
-
out = self._run(cmd_list)
|
232
|
-
if out.returncode != 0:
|
233
|
-
print(out)
|
234
|
-
raise ValueError(f"Error deleting files: {out.stderr}")
|
235
227
|
|
236
|
-
|
228
|
+
def _task(files=files) -> subprocess.CompletedProcess:
|
229
|
+
with TemporaryDirectory() as tmpdir:
|
230
|
+
include_files_txt = Path(tmpdir) / "include_files.txt"
|
231
|
+
include_files_txt.write_text("\n".join(files), encoding="utf-8")
|
232
|
+
|
233
|
+
# print(include_files_txt)
|
234
|
+
cmd_list: list[str] = [
|
235
|
+
"delete",
|
236
|
+
remote,
|
237
|
+
"--files-from",
|
238
|
+
str(include_files_txt),
|
239
|
+
"--checkers",
|
240
|
+
"1000",
|
241
|
+
"--transfers",
|
242
|
+
"1000",
|
243
|
+
]
|
244
|
+
out = self._run(cmd_list)
|
245
|
+
return out
|
246
|
+
|
247
|
+
fut: Future = EXECUTOR.submit(_task)
|
248
|
+
futures.append(fut)
|
249
|
+
for fut in futures:
|
250
|
+
out = fut.result()
|
251
|
+
assert out is not None
|
252
|
+
if out.returncode != 0:
|
253
|
+
if check:
|
254
|
+
raise ValueError(f"Error deleting files: {out.stderr}")
|
255
|
+
else:
|
256
|
+
warnings.warn(f"Error deleting files: {out.stderr}")
|
237
257
|
|
238
258
|
def copy(self, src: Dir | str, dst: Dir | str) -> CompletedProcess:
|
239
259
|
"""Copy files from source to destination.
|
@@ -263,6 +283,7 @@ class Rclone:
|
|
263
283
|
files: str | File | list[str] | list[File],
|
264
284
|
check=True,
|
265
285
|
rmdirs=False,
|
286
|
+
verbose: bool | None = None,
|
266
287
|
other_args: list[str] | None = None,
|
267
288
|
) -> CompletedProcess:
|
268
289
|
"""Delete a directory"""
|
@@ -276,41 +297,53 @@ class Rclone:
|
|
276
297
|
)
|
277
298
|
return CompletedProcess.from_subprocess(cp)
|
278
299
|
|
279
|
-
datalists: dict[str, list[str]] =
|
280
|
-
out: subprocess.CompletedProcess | None = None
|
281
|
-
|
300
|
+
datalists: dict[str, list[str]] = group_files(payload)
|
282
301
|
completed_processes: list[subprocess.CompletedProcess] = []
|
302
|
+
verbose = get_verbose(verbose)
|
303
|
+
|
304
|
+
futures: list[Future] = []
|
283
305
|
|
284
306
|
for remote, files in datalists.items():
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
307
|
+
|
308
|
+
def _task(files=files, check=check) -> subprocess.CompletedProcess:
|
309
|
+
with TemporaryDirectory() as tmpdir:
|
310
|
+
include_files_txt = Path(tmpdir) / "include_files.txt"
|
311
|
+
include_files_txt.write_text("\n".join(files), encoding="utf-8")
|
312
|
+
|
313
|
+
# print(include_files_txt)
|
314
|
+
cmd_list: list[str] = [
|
315
|
+
"delete",
|
316
|
+
remote,
|
317
|
+
"--files-from",
|
318
|
+
str(include_files_txt),
|
319
|
+
"--checkers",
|
320
|
+
"1000",
|
321
|
+
"--transfers",
|
322
|
+
"1000",
|
323
|
+
]
|
324
|
+
if verbose:
|
325
|
+
cmd_list.append("-vvvv")
|
326
|
+
if rmdirs:
|
327
|
+
cmd_list.append("--rmdirs")
|
328
|
+
if other_args:
|
329
|
+
cmd_list += other_args
|
330
|
+
out = self._run(cmd_list, check=check)
|
306
331
|
if out.returncode != 0:
|
307
332
|
if check:
|
308
333
|
completed_processes.append(out)
|
309
334
|
raise ValueError(f"Error deleting files: {out}")
|
310
335
|
else:
|
311
336
|
warnings.warn(f"Error deleting files: {out}")
|
337
|
+
return out
|
338
|
+
|
339
|
+
fut: Future = EXECUTOR.submit(_task)
|
340
|
+
futures.append(fut)
|
341
|
+
|
342
|
+
for fut in futures:
|
343
|
+
out = fut.result()
|
344
|
+
assert out is not None
|
345
|
+
completed_processes.append(out)
|
312
346
|
|
313
|
-
assert out is not None
|
314
347
|
return CompletedProcess(completed_processes)
|
315
348
|
|
316
349
|
@deprecated("delete_files")
|
rclone_api/util.py
CHANGED
@@ -129,15 +129,3 @@ def wait_for_mount(path: Path, mount_process: Any, timeout: int = 60) -> None:
|
|
129
129
|
if path.exists():
|
130
130
|
return
|
131
131
|
raise TimeoutError(f"Path {path} did not exist after {timeout} seconds")
|
132
|
-
|
133
|
-
|
134
|
-
def partition_files(files: list[str]) -> dict[str, list[str]]:
|
135
|
-
"""split between filename and parent directory path"""
|
136
|
-
datalists: dict[str, list[str]] = {}
|
137
|
-
for f in files:
|
138
|
-
base = os.path.basename(f)
|
139
|
-
parent_path = os.path.dirname(f)
|
140
|
-
if parent_path not in datalists:
|
141
|
-
datalists[parent_path] = []
|
142
|
-
datalists[parent_path].append(base)
|
143
|
-
return datalists
|
@@ -10,17 +10,18 @@ rclone_api/dir_listing.py,sha256=9Qqf2SUswrOEkyqmaH23V51I18X6ePiXb9B1vUwRF5o,157
|
|
10
10
|
rclone_api/exec.py,sha256=HWmnU2Jwb-3EttSbAJSaLloYA7YI2mHTzRJ5VEri9aM,941
|
11
11
|
rclone_api/file.py,sha256=D02iHJW1LhfOiM_R_yPHP8_ApnDiYrkuraVcrV8-qkw,1246
|
12
12
|
rclone_api/filelist.py,sha256=xbiusvNgaB_b_kQOZoHMJJxn6TWGtPrWd2J042BI28o,767
|
13
|
+
rclone_api/group_files.py,sha256=xHE3_gvvMWgvLk33TyTNAa0GRFvIRo2qkwsEKA-yujU,4730
|
13
14
|
rclone_api/process.py,sha256=RrMfTe0bndmJ6gBK67ioqNvCstJ8aTC8RlGX1XBLlcw,4191
|
14
|
-
rclone_api/rclone.py,sha256=
|
15
|
+
rclone_api/rclone.py,sha256=zKLlHykEs43kt-FB3_YFvwPmRcHY2a3-fShMfGEjYH0,21004
|
15
16
|
rclone_api/remote.py,sha256=c9hlRKBCg1BFB9MCINaQIoCg10qyAkeqiS4brl8ce-8,343
|
16
17
|
rclone_api/rpath.py,sha256=8ZA_1wxWtskwcy0I8V2VbjKDmzPkiWd8Q2JQSvh-sYE,2586
|
17
|
-
rclone_api/util.py,sha256=
|
18
|
+
rclone_api/util.py,sha256=sUjH5NmsawmNbPMY7V6hD8vFJXCwbl44XM1kuij3tA0,3918
|
18
19
|
rclone_api/walk.py,sha256=kca0t1GAnF6FLclN01G8NG__Qe-ggodLtAbQSHyVPng,2968
|
19
20
|
rclone_api/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
|
20
21
|
rclone_api/cmd/list_files.py,sha256=x8FHODEilwKqwdiU1jdkeJbLwOqUkUQuDWPo2u_zpf0,741
|
21
|
-
rclone_api-1.0.
|
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.
|
22
|
+
rclone_api-1.0.42.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
|
23
|
+
rclone_api-1.0.42.dist-info/METADATA,sha256=f5ePfkFNusMVvM9SSIdDsLXuZOUFKYF_ak8yAKnuzc4,4489
|
24
|
+
rclone_api-1.0.42.dist-info/WHEEL,sha256=9Hm2OB-j1QcCUq9Jguht7ayGIIZBRTdOXD1qg9cCgPM,109
|
25
|
+
rclone_api-1.0.42.dist-info/entry_points.txt,sha256=XUoTX3m7CWxdj2VAKhEuO0NMOfX2qf-OcEDFwdyk9ZE,72
|
26
|
+
rclone_api-1.0.42.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
|
27
|
+
rclone_api-1.0.42.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|