rclone-api 1.0.41__tar.gz → 1.0.43__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of rclone-api might be problematic. Click here for more details.
- {rclone_api-1.0.41 → rclone_api-1.0.43}/PKG-INFO +1 -1
- {rclone_api-1.0.41 → rclone_api-1.0.43}/pyproject.toml +1 -1
- rclone_api-1.0.43/src/rclone_api/group_files.py +147 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/rclone.py +3 -3
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/util.py +0 -12
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api.egg-info/PKG-INFO +1 -1
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api.egg-info/SOURCES.txt +2 -0
- rclone_api-1.0.43/tests/test_group_files.py +101 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/.aiderignore +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/.github/workflows/lint.yml +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/.github/workflows/push_macos.yml +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/.github/workflows/push_ubuntu.yml +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/.github/workflows/push_win.yml +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/.gitignore +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/.pylintrc +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/.vscode/launch.json +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/.vscode/settings.json +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/.vscode/tasks.json +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/LICENSE +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/MANIFEST.in +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/README.md +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/clean +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/install +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/lint +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/requirements.testing.txt +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/setup.cfg +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/setup.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/__init__.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/cmd/list_files.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/completed_process.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/config.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/convert.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/deprecated.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/diff.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/dir.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/dir_listing.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/exec.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/file.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/filelist.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/process.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/rpath.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api/walk.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api.egg-info/entry_points.txt +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api.egg-info/requires.txt +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/test +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_cmd_list_files.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_copy.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_diff.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_is_synced.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_ls.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_mount.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_mount_s3.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_mount_webdav.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_obscure.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_remotes.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_serve_webdav.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/tests/test_walk.py +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/tox.ini +0 -0
- {rclone_api-1.0.41 → rclone_api-1.0.43}/upload_package.sh +0 -0
@@ -0,0 +1,147 @@
|
|
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
|
+
if not node.child_nodes and not node.files:
|
84
|
+
return # done
|
85
|
+
if node.files:
|
86
|
+
# we saw files, to don't try to go any deeper.
|
87
|
+
filelist = out.setdefault(parent_path, [])
|
88
|
+
# for file in node.files:
|
89
|
+
# filelist.append(file)
|
90
|
+
# out[parent_path] = filelist
|
91
|
+
paths = node.get_child_subpaths()
|
92
|
+
for path in paths:
|
93
|
+
filelist.append(path)
|
94
|
+
out[parent_path] = filelist
|
95
|
+
return
|
96
|
+
|
97
|
+
n_child_nodes = len(node.child_nodes)
|
98
|
+
|
99
|
+
if n_child_nodes < 4:
|
100
|
+
# child = list(node.child_nodes.values())[0]
|
101
|
+
# _merge(child, parent_path, out)
|
102
|
+
# return
|
103
|
+
for child in node.child_nodes.values():
|
104
|
+
_merge(child, parent_path, out)
|
105
|
+
return
|
106
|
+
|
107
|
+
filelist = out.setdefault(parent_path, [])
|
108
|
+
# for file in node.files:
|
109
|
+
# filelist.append(file)
|
110
|
+
# out[parent_path] = filelist
|
111
|
+
paths = node.get_child_subpaths()
|
112
|
+
for path in paths:
|
113
|
+
filelist.append(path)
|
114
|
+
out[parent_path] = filelist
|
115
|
+
return
|
116
|
+
|
117
|
+
|
118
|
+
def _make_tree(files: list[str]) -> dict[str, TreeNode]:
|
119
|
+
tree: dict[str, TreeNode] = {}
|
120
|
+
for file in files:
|
121
|
+
parts = parse_file(file)
|
122
|
+
remote = parts.remote
|
123
|
+
node: TreeNode = tree.setdefault(remote, TreeNode(remote))
|
124
|
+
for parent in parts.parents:
|
125
|
+
is_last = parent == parts.parents[-1]
|
126
|
+
node = node.child_nodes.setdefault(parent, TreeNode(parent, parent=node))
|
127
|
+
if is_last:
|
128
|
+
node.files.append(parts.name)
|
129
|
+
node.add_count()
|
130
|
+
return tree
|
131
|
+
|
132
|
+
|
133
|
+
def group_files(files: list[str]) -> dict[str, list[str]]:
|
134
|
+
"""split between filename and parent directory path"""
|
135
|
+
tree: dict[str, TreeNode] = _make_tree(files)
|
136
|
+
outpaths: dict[str, list[str]] = {}
|
137
|
+
for _, node in tree.items():
|
138
|
+
_merge(node, "", outpaths)
|
139
|
+
out: dict[str, list[str]] = {}
|
140
|
+
for path, files in outpaths.items():
|
141
|
+
# fixup path
|
142
|
+
assert path.startswith("/"), "Path should start with /"
|
143
|
+
path = path[1:]
|
144
|
+
# replace the first / with :
|
145
|
+
path = path.replace("/", ":", 1)
|
146
|
+
out[path] = files
|
147
|
+
return out
|
@@ -21,13 +21,13 @@ from rclone_api.diff import DiffItem, diff_stream_from_running_process
|
|
21
21
|
from rclone_api.dir_listing import DirListing
|
22
22
|
from rclone_api.exec import RcloneExec
|
23
23
|
from rclone_api.file import File
|
24
|
+
from rclone_api.group_files import group_files
|
24
25
|
from rclone_api.process import Process
|
25
26
|
from rclone_api.remote import Remote
|
26
27
|
from rclone_api.rpath import RPath
|
27
28
|
from rclone_api.util import (
|
28
29
|
get_rclone_exe,
|
29
30
|
get_verbose,
|
30
|
-
partition_files,
|
31
31
|
to_path,
|
32
32
|
wait_for_mount,
|
33
33
|
)
|
@@ -218,7 +218,7 @@ class Rclone:
|
|
218
218
|
if len(payload) == 0:
|
219
219
|
return
|
220
220
|
|
221
|
-
datalists: dict[str, list[str]] =
|
221
|
+
datalists: dict[str, list[str]] = group_files(payload)
|
222
222
|
out: subprocess.CompletedProcess | None = None
|
223
223
|
|
224
224
|
futures: list[Future] = []
|
@@ -297,7 +297,7 @@ class Rclone:
|
|
297
297
|
)
|
298
298
|
return CompletedProcess.from_subprocess(cp)
|
299
299
|
|
300
|
-
datalists: dict[str, list[str]] =
|
300
|
+
datalists: dict[str, list[str]] = group_files(payload)
|
301
301
|
completed_processes: list[subprocess.CompletedProcess] = []
|
302
302
|
verbose = get_verbose(verbose)
|
303
303
|
|
@@ -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
|
@@ -32,6 +32,7 @@ src/rclone_api/dir_listing.py
|
|
32
32
|
src/rclone_api/exec.py
|
33
33
|
src/rclone_api/file.py
|
34
34
|
src/rclone_api/filelist.py
|
35
|
+
src/rclone_api/group_files.py
|
35
36
|
src/rclone_api/process.py
|
36
37
|
src/rclone_api/rclone.py
|
37
38
|
src/rclone_api/remote.py
|
@@ -49,6 +50,7 @@ src/rclone_api/cmd/list_files.py
|
|
49
50
|
tests/test_cmd_list_files.py
|
50
51
|
tests/test_copy.py
|
51
52
|
tests/test_diff.py
|
53
|
+
tests/test_group_files.py
|
52
54
|
tests/test_is_synced.py
|
53
55
|
tests/test_ls.py
|
54
56
|
tests/test_mount.py
|
@@ -0,0 +1,101 @@
|
|
1
|
+
"""
|
2
|
+
Unit test file.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import unittest
|
6
|
+
|
7
|
+
from rclone_api.group_files import group_files
|
8
|
+
|
9
|
+
|
10
|
+
class GroupFilestest(unittest.TestCase):
|
11
|
+
"""Test rclone functionality."""
|
12
|
+
|
13
|
+
def test_simple_group_files(self) -> None:
|
14
|
+
files = [
|
15
|
+
"dst:Bucket/subdir/file1.txt",
|
16
|
+
"dst:Bucket/subdir/file2.txt",
|
17
|
+
]
|
18
|
+
groups: dict[str, list[str]] = group_files(files)
|
19
|
+
self.assertEqual(len(groups), 1)
|
20
|
+
# dst:/Bucket/subdir should be the key
|
21
|
+
self.assertIn("dst:Bucket/subdir", groups)
|
22
|
+
self.assertEqual(len(groups["dst:Bucket/subdir"]), 2)
|
23
|
+
expected_files = [
|
24
|
+
"file1.txt",
|
25
|
+
"file2.txt",
|
26
|
+
]
|
27
|
+
self.assertIn(expected_files[0], groups["dst:Bucket/subdir"])
|
28
|
+
self.assertIn(expected_files[1], groups["dst:Bucket/subdir"])
|
29
|
+
print("done")
|
30
|
+
|
31
|
+
def test_different_paths(self) -> None:
|
32
|
+
files = [
|
33
|
+
"dst:Bucket/subdir/file1.txt",
|
34
|
+
"dst:Bucket/subdir2/file2.txt",
|
35
|
+
]
|
36
|
+
groups: dict[str, list[str]] = group_files(files)
|
37
|
+
self.assertEqual(len(groups), 2)
|
38
|
+
# dst:/Bucket/subdir should be the key
|
39
|
+
self.assertIn("dst:Bucket/subdir", groups)
|
40
|
+
self.assertEqual(len(groups["dst:Bucket/subdir"]), 1)
|
41
|
+
expected_files = [
|
42
|
+
"file1.txt",
|
43
|
+
]
|
44
|
+
self.assertIn(expected_files[0], groups["dst:Bucket/subdir"])
|
45
|
+
# dst:/Bucket/subdir2 should be the key
|
46
|
+
self.assertIn("dst:Bucket/subdir2", groups)
|
47
|
+
self.assertEqual(len(groups["dst:Bucket/subdir2"]), 1)
|
48
|
+
|
49
|
+
def test_two_big_directories(self) -> None:
|
50
|
+
files = [
|
51
|
+
"dst:Bucket/subdir/file1.txt",
|
52
|
+
"dst:Bucket/subdir/file2.txt",
|
53
|
+
"dst:Bucket/subdir2/file3.txt",
|
54
|
+
"dst:Bucket/subdir2/file4.txt",
|
55
|
+
]
|
56
|
+
|
57
|
+
groups: dict[str, list[str]] = group_files(files)
|
58
|
+
self.assertEqual(len(groups), 2)
|
59
|
+
# dst:/Bucket/subdir should be the key
|
60
|
+
self.assertIn("dst:Bucket/subdir", groups)
|
61
|
+
self.assertEqual(len(groups["dst:Bucket/subdir"]), 2)
|
62
|
+
expected_files = [
|
63
|
+
"file1.txt",
|
64
|
+
"file2.txt",
|
65
|
+
]
|
66
|
+
self.assertIn(expected_files[0], groups["dst:Bucket/subdir"])
|
67
|
+
self.assertIn(expected_files[1], groups["dst:Bucket/subdir"])
|
68
|
+
# dst:/Bucket/subdir2 should be the key
|
69
|
+
self.assertIn("dst:Bucket/subdir2", groups)
|
70
|
+
self.assertEqual(len(groups["dst:Bucket/subdir2"]), 2)
|
71
|
+
expected_files = [
|
72
|
+
"file3.txt",
|
73
|
+
"file4.txt",
|
74
|
+
]
|
75
|
+
self.assertIn(expected_files[0], groups["dst:Bucket/subdir2"])
|
76
|
+
self.assertIn(expected_files[1], groups["dst:Bucket/subdir2"])
|
77
|
+
print("done")
|
78
|
+
|
79
|
+
def test_two_fine_grained(self) -> None:
|
80
|
+
files = [
|
81
|
+
"dst:TorrentBooks/libgenrs_nonfiction/204000/a2b20b2c89240ce81dec16091e18113e",
|
82
|
+
"dst:TorrentBooks/libgenrs_nonfiction/208000/155fe185bc03048b003a8e145ed097c8",
|
83
|
+
"dst:TorrentBooks/libgenrs_nonfiction/208001/155fe185bc03048b003a8e145ed097c8",
|
84
|
+
"dst:TorrentBooks/libgenrs_nonfiction/208002/155fe185bc03048b003a8e145ed097c8",
|
85
|
+
"dst:TorrentBooks/libgenrs_nonfiction/2080054/155fe185bc03048b003a8e145ed097c4",
|
86
|
+
]
|
87
|
+
# expect that this all goes under the same parent
|
88
|
+
groups: dict[str, list[str]] = group_files(files)
|
89
|
+
self.assertEqual(len(groups), 1)
|
90
|
+
# dst:/Bucket/subdir should be the key
|
91
|
+
self.assertIn("dst:TorrentBooks/libgenrs_nonfiction", groups)
|
92
|
+
self.assertEqual(len(groups["dst:TorrentBooks/libgenrs_nonfiction"]), 5)
|
93
|
+
expected_files = [
|
94
|
+
"204000/a2b20b2c89240ce81dec16091e18113e",
|
95
|
+
"208000/155fe185bc03048b003a8e145ed097c8",
|
96
|
+
]
|
97
|
+
self.assertIn(expected_files[0], groups["dst:TorrentBooks/libgenrs_nonfiction"])
|
98
|
+
|
99
|
+
|
100
|
+
if __name__ == "__main__":
|
101
|
+
unittest.main()
|
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
|
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
|
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
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|