rclone-api 1.0.68__tar.gz → 1.0.70__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {rclone_api-1.0.68 → rclone_api-1.0.70}/PKG-INFO +1 -1
- {rclone_api-1.0.68 → rclone_api-1.0.70}/pyproject.toml +1 -1
- rclone_api-1.0.70/src/rclone_api/diff_walk.py +123 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/dir.py +46 -2
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/rclone.py +32 -13
- rclone_api-1.0.70/src/rclone_api/types.py +12 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/walk.py +10 -9
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api.egg-info/PKG-INFO +1 -1
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api.egg-info/SOURCES.txt +3 -0
- rclone_api-1.0.70/tests/test_diff_walk.py +73 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/.aiderignore +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/.github/workflows/lint.yml +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/.github/workflows/push_macos.yml +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/.github/workflows/push_ubuntu.yml +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/.github/workflows/push_win.yml +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/.gitignore +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/.pylintrc +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/.vscode/launch.json +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/.vscode/settings.json +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/.vscode/tasks.json +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/LICENSE +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/MANIFEST.in +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/README.md +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/clean +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/install +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/lint +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/requirements.testing.txt +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/setup.cfg +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/setup.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/__init__.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/cmd/list_files.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/completed_process.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/config.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/convert.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/deprecated.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/diff.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/dir_listing.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/exec.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/file.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/filelist.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/group_files.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/process.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/rpath.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api/util.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api.egg-info/entry_points.txt +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api.egg-info/requires.txt +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/test +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_cmd_list_files.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_copy.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_copy_files.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_diff.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_group_files.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_is_synced.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_ls.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_mount.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_mount_s3.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_mount_webdav.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_obscure.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_remote_control.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_remotes.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_serve_webdav.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tests/test_walk.py +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/tox.ini +0 -0
- {rclone_api-1.0.68 → rclone_api-1.0.70}/upload_package.sh +0 -0
@@ -0,0 +1,123 @@
|
|
1
|
+
import time
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
3
|
+
from queue import Empty, Queue
|
4
|
+
from threading import Thread
|
5
|
+
from typing import Generator
|
6
|
+
|
7
|
+
from rclone_api import Dir
|
8
|
+
from rclone_api.dir_listing import DirListing
|
9
|
+
from rclone_api.types import ListingOption
|
10
|
+
from rclone_api.walk import walk_runner_depth_first
|
11
|
+
|
12
|
+
_MAX_OUT_QUEUE_SIZE = 50
|
13
|
+
|
14
|
+
|
15
|
+
# ONLY Works from src -> dst diffing.
|
16
|
+
def _async_diff_dir_walk_task(
|
17
|
+
src: Dir, dst: Dir, max_depth: int, out_queue: Queue[Dir | None], reverse
|
18
|
+
) -> None:
|
19
|
+
curr_src, curr_dst = src, dst
|
20
|
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
21
|
+
# src_dir_listing = src.ls(listing_option=ListingOption.DIRS_ONLY)
|
22
|
+
# dst_dir_listing = dst.ls(listing_option=ListingOption.DIRS_ONLY)
|
23
|
+
t1 = executor.submit(
|
24
|
+
src.ls, listing_option=ListingOption.DIRS_ONLY, reverse=reverse
|
25
|
+
)
|
26
|
+
t2 = executor.submit(
|
27
|
+
dst.ls, listing_option=ListingOption.DIRS_ONLY, reverse=reverse
|
28
|
+
)
|
29
|
+
src_dir_listing: DirListing = t1.result()
|
30
|
+
dst_dir_listing: DirListing = t2.result()
|
31
|
+
next_depth = max_depth - 1 if max_depth > 0 else max_depth
|
32
|
+
dst_files: list[str] = [d.name for d in dst_dir_listing.dirs]
|
33
|
+
src_files: list[str] = [d.name for d in src_dir_listing.dirs]
|
34
|
+
dst_files_set: set[str] = set(dst_files)
|
35
|
+
matching_dirs: list[str] = []
|
36
|
+
for file in src_files:
|
37
|
+
if file not in dst_files_set:
|
38
|
+
# print(f"missing dir on src: {file}")
|
39
|
+
queue_dir_listing: Queue[DirListing | None] = Queue()
|
40
|
+
if next_depth > 0 or next_depth == -1:
|
41
|
+
walk_runner_depth_first(
|
42
|
+
dir=curr_src,
|
43
|
+
out_queue=queue_dir_listing,
|
44
|
+
reverse=reverse,
|
45
|
+
max_depth=next_depth,
|
46
|
+
)
|
47
|
+
while dirlisting := queue_dir_listing.get():
|
48
|
+
if dirlisting is None:
|
49
|
+
break
|
50
|
+
# print(f"dirlisting: {dirlisting}")
|
51
|
+
for d in dirlisting.dirs:
|
52
|
+
out_queue.put(d)
|
53
|
+
else:
|
54
|
+
matching_dirs.append(file)
|
55
|
+
|
56
|
+
for matching_dir in matching_dirs:
|
57
|
+
# print(f"matching dir: {matching_dir}")
|
58
|
+
if next_depth > 0 or next_depth == -1:
|
59
|
+
src_next = curr_src / matching_dir
|
60
|
+
dst_next = curr_dst / matching_dir
|
61
|
+
_async_diff_dir_walk_task(
|
62
|
+
src=src_next,
|
63
|
+
dst=dst_next,
|
64
|
+
max_depth=next_depth,
|
65
|
+
out_queue=out_queue,
|
66
|
+
reverse=reverse,
|
67
|
+
)
|
68
|
+
|
69
|
+
|
70
|
+
def async_diff_dir_walk_task(
|
71
|
+
src: Dir, dst: Dir, max_depth: int, out_queue: Queue[Dir | None], reverse=False
|
72
|
+
) -> None:
|
73
|
+
try:
|
74
|
+
_async_diff_dir_walk_task(src, dst, max_depth, out_queue, reverse)
|
75
|
+
except Exception:
|
76
|
+
import _thread
|
77
|
+
|
78
|
+
_thread.interrupt_main()
|
79
|
+
raise
|
80
|
+
finally:
|
81
|
+
out_queue.put(None)
|
82
|
+
|
83
|
+
|
84
|
+
def diff_walk(
|
85
|
+
src: Dir,
|
86
|
+
dst: Dir,
|
87
|
+
max_depth: int = -1,
|
88
|
+
reverse: bool = False,
|
89
|
+
) -> Generator[Dir, None, None]:
|
90
|
+
"""Walk through the given directory recursively.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
dir: Directory or Remote to walk through
|
94
|
+
max_depth: Maximum depth to traverse (-1 for unlimited)
|
95
|
+
|
96
|
+
Yields:
|
97
|
+
DirListing: Directory listing for each directory encountered
|
98
|
+
"""
|
99
|
+
|
100
|
+
try:
|
101
|
+
out_queue: Queue[Dir | None] = Queue(maxsize=_MAX_OUT_QUEUE_SIZE)
|
102
|
+
|
103
|
+
def task() -> None:
|
104
|
+
async_diff_dir_walk_task(src, dst, max_depth, out_queue, reverse=reverse)
|
105
|
+
|
106
|
+
worker = Thread(
|
107
|
+
target=task,
|
108
|
+
daemon=True,
|
109
|
+
)
|
110
|
+
worker.start()
|
111
|
+
|
112
|
+
while True:
|
113
|
+
try:
|
114
|
+
dirlisting = out_queue.get_nowait()
|
115
|
+
if dirlisting is None:
|
116
|
+
break
|
117
|
+
yield dirlisting
|
118
|
+
except Empty:
|
119
|
+
time.sleep(0.1)
|
120
|
+
|
121
|
+
worker.join()
|
122
|
+
except KeyboardInterrupt:
|
123
|
+
pass
|
@@ -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.as_posix()),
|
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)
|
@@ -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:
|
@@ -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()
|
@@ -27,6 +27,7 @@ src/rclone_api/config.py
|
|
27
27
|
src/rclone_api/convert.py
|
28
28
|
src/rclone_api/deprecated.py
|
29
29
|
src/rclone_api/diff.py
|
30
|
+
src/rclone_api/diff_walk.py
|
30
31
|
src/rclone_api/dir.py
|
31
32
|
src/rclone_api/dir_listing.py
|
32
33
|
src/rclone_api/exec.py
|
@@ -37,6 +38,7 @@ src/rclone_api/process.py
|
|
37
38
|
src/rclone_api/rclone.py
|
38
39
|
src/rclone_api/remote.py
|
39
40
|
src/rclone_api/rpath.py
|
41
|
+
src/rclone_api/types.py
|
40
42
|
src/rclone_api/util.py
|
41
43
|
src/rclone_api/walk.py
|
42
44
|
src/rclone_api.egg-info/PKG-INFO
|
@@ -51,6 +53,7 @@ tests/test_cmd_list_files.py
|
|
51
53
|
tests/test_copy.py
|
52
54
|
tests/test_copy_files.py
|
53
55
|
tests/test_diff.py
|
56
|
+
tests/test_diff_walk.py
|
54
57
|
tests/test_group_files.py
|
55
58
|
tests/test_is_synced.py
|
56
59
|
tests/test_ls.py
|
@@ -0,0 +1,73 @@
|
|
1
|
+
"""
|
2
|
+
Unit test file.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import unittest
|
7
|
+
|
8
|
+
from dotenv import load_dotenv
|
9
|
+
|
10
|
+
from rclone_api import Config, Dir, Rclone
|
11
|
+
|
12
|
+
load_dotenv()
|
13
|
+
|
14
|
+
BUCKET_NAME = os.getenv("BUCKET_NAME") # Default if not in .env
|
15
|
+
|
16
|
+
|
17
|
+
def _generate_rclone_config() -> Config:
|
18
|
+
|
19
|
+
# BUCKET_NAME = os.getenv("BUCKET_NAME", "TorrentBooks") # Default if not in .env
|
20
|
+
|
21
|
+
# Load additional environment variables
|
22
|
+
BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
|
23
|
+
BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
|
24
|
+
# BUCKET_URL = os.getenv("BUCKET_URL")
|
25
|
+
BUCKET_URL = "sfo3.digitaloceanspaces.com"
|
26
|
+
|
27
|
+
config_text = f"""
|
28
|
+
[dst]
|
29
|
+
type = s3
|
30
|
+
provider = DigitalOcean
|
31
|
+
access_key_id = {BUCKET_KEY_PUBLIC}
|
32
|
+
secret_access_key = {BUCKET_KEY_SECRET}
|
33
|
+
endpoint = {BUCKET_URL}
|
34
|
+
"""
|
35
|
+
|
36
|
+
out = Config(config_text)
|
37
|
+
return out
|
38
|
+
|
39
|
+
|
40
|
+
class RcloneDiffTests(unittest.TestCase):
|
41
|
+
"""Test rclone functionality."""
|
42
|
+
|
43
|
+
def setUp(self) -> None:
|
44
|
+
"""Check if all required environment variables are set before running tests."""
|
45
|
+
required_vars = [
|
46
|
+
"BUCKET_NAME",
|
47
|
+
"BUCKET_KEY_SECRET",
|
48
|
+
"BUCKET_KEY_PUBLIC",
|
49
|
+
"BUCKET_URL",
|
50
|
+
]
|
51
|
+
missing = [var for var in required_vars if not os.getenv(var)]
|
52
|
+
if missing:
|
53
|
+
self.skipTest(
|
54
|
+
f"Missing required environment variables: {', '.join(missing)}"
|
55
|
+
)
|
56
|
+
os.environ["RCLONE_API_VERBOSE"] = "1"
|
57
|
+
|
58
|
+
def test_diff_walk(self) -> None:
|
59
|
+
"""Test copying a single file to remote storage."""
|
60
|
+
rclone = Rclone(_generate_rclone_config())
|
61
|
+
item: Dir
|
62
|
+
all: list[Dir] = []
|
63
|
+
for item in rclone.diff_walk(
|
64
|
+
"dst:rclone-api-unit-test", "dst:rclone-api-unit-test"
|
65
|
+
):
|
66
|
+
all.append(item)
|
67
|
+
self.assertEqual(len(all), 0)
|
68
|
+
msg = "\n".join([str(item) for item in all])
|
69
|
+
print(msg)
|
70
|
+
|
71
|
+
|
72
|
+
if __name__ == "__main__":
|
73
|
+
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
|
File without changes
|
File without changes
|
File without changes
|