rclone-api 1.0.17__tar.gz → 1.0.18__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {rclone_api-1.0.17 → rclone_api-1.0.18}/.gitignore +1 -1
- {rclone_api-1.0.17 → rclone_api-1.0.18}/PKG-INFO +1 -1
- {rclone_api-1.0.17 → rclone_api-1.0.18}/pyproject.toml +1 -1
- {rclone_api-1.0.17 → rclone_api-1.0.18}/requirements.testing.txt +1 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/process.py +3 -1
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/rclone.py +84 -1
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api.egg-info/PKG-INFO +1 -1
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api.egg-info/SOURCES.txt +1 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/test +1 -1
- {rclone_api-1.0.17 → rclone_api-1.0.18}/tests/test_ls.py +1 -0
- rclone_api-1.0.18/tests/test_serve_webdav.py +158 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/.aiderignore +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/.github/workflows/lint.yml +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/.github/workflows/push_macos.yml +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/.github/workflows/push_ubuntu.yml +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/.github/workflows/push_win.yml +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/.pylintrc +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/.vscode/launch.json +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/.vscode/settings.json +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/.vscode/tasks.json +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/LICENSE +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/MANIFEST.in +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/README.md +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/clean +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/install +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/lint +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/setup.cfg +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/setup.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/__init__.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/config.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/convert.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/dir.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/dir_listing.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/exec.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/file.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/filelist.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/rpath.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/util.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api/walk.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api.egg-info/requires.txt +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/tests/test_copy.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/tests/test_is_synced.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/tests/test_mount.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/tests/test_remotes.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/tests/test_walk.py +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/tox.ini +0 -0
- {rclone_api-1.0.17 → rclone_api-1.0.18}/upload_package.sh +0 -0
@@ -69,8 +69,10 @@ class Process:
|
|
69
69
|
tmpfile = Path(self.tempdir.name) / "rclone.conf"
|
70
70
|
tmpfile.write_text(args.rclone_conf.text, encoding="utf-8")
|
71
71
|
rclone_conf = tmpfile
|
72
|
+
self.needs_cleanup = True
|
72
73
|
else:
|
73
74
|
rclone_conf = args.rclone_conf
|
75
|
+
self.needs_cleanup = False
|
74
76
|
|
75
77
|
assert rclone_conf.exists()
|
76
78
|
|
@@ -85,7 +87,7 @@ class Process:
|
|
85
87
|
self.process = subprocess.Popen(self.cmd, shell=False)
|
86
88
|
|
87
89
|
def cleanup(self) -> None:
|
88
|
-
if self.tempdir:
|
90
|
+
if self.tempdir and self.needs_cleanup:
|
89
91
|
try:
|
90
92
|
self.tempdir.cleanup()
|
91
93
|
except Exception as e:
|
@@ -239,7 +239,13 @@ class Rclone:
|
|
239
239
|
return self._run(cmd_list)
|
240
240
|
|
241
241
|
def mount(
|
242
|
-
self,
|
242
|
+
self,
|
243
|
+
src: Remote | Dir | str,
|
244
|
+
outdir: Path,
|
245
|
+
allow_writes=False,
|
246
|
+
use_links=True,
|
247
|
+
vfs_cache_mode="full",
|
248
|
+
other_cmds: list[str] | None = None,
|
243
249
|
) -> Process:
|
244
250
|
"""Mount a remote or directory to a local path.
|
245
251
|
|
@@ -266,8 +272,85 @@ class Rclone:
|
|
266
272
|
cmd_list.append("--read-only")
|
267
273
|
if use_links:
|
268
274
|
cmd_list.append("--links")
|
275
|
+
if vfs_cache_mode:
|
276
|
+
cmd_list.append("--vfs-cache-mode")
|
277
|
+
cmd_list.append(vfs_cache_mode)
|
278
|
+
if other_cmds:
|
279
|
+
cmd_list += other_cmds
|
269
280
|
proc = self._launch_process(cmd_list)
|
270
281
|
time.sleep(2) # give it a moment to mount
|
271
282
|
if proc.poll() is not None:
|
272
283
|
raise ValueError("Mount process failed to start")
|
273
284
|
return proc
|
285
|
+
|
286
|
+
def mount_webdav(
|
287
|
+
self,
|
288
|
+
url: str,
|
289
|
+
outdir: Path,
|
290
|
+
vfs_cache_mode="full",
|
291
|
+
other_cmds: list[str] | None = None,
|
292
|
+
) -> Process:
|
293
|
+
"""Mount a remote or directory to a local path.
|
294
|
+
|
295
|
+
Args:
|
296
|
+
src: Remote or directory to mount
|
297
|
+
outdir: Local path to mount to
|
298
|
+
|
299
|
+
Returns:
|
300
|
+
CompletedProcess from the mount command execution
|
301
|
+
|
302
|
+
Raises:
|
303
|
+
subprocess.CalledProcessError: If the mount operation fails
|
304
|
+
"""
|
305
|
+
if outdir.exists():
|
306
|
+
is_empty = not list(outdir.iterdir())
|
307
|
+
if not is_empty:
|
308
|
+
raise ValueError(
|
309
|
+
f"Mount directory already exists and is not empty: {outdir}"
|
310
|
+
)
|
311
|
+
outdir.rmdir()
|
312
|
+
|
313
|
+
src_str = url
|
314
|
+
cmd_list: list[str] = ["mount", src_str, str(outdir)]
|
315
|
+
cmd_list.append("--vfs-cache-mode")
|
316
|
+
cmd_list.append(vfs_cache_mode)
|
317
|
+
if other_cmds:
|
318
|
+
cmd_list += other_cmds
|
319
|
+
proc = self._launch_process(cmd_list)
|
320
|
+
# proc = rclone_exec.launch_process(cmd_list)
|
321
|
+
time.sleep(2)
|
322
|
+
if proc.poll() is not None:
|
323
|
+
raise ValueError("Mount process failed to start")
|
324
|
+
return proc
|
325
|
+
|
326
|
+
def serve_webdav(
|
327
|
+
self,
|
328
|
+
src: Remote | Dir | str,
|
329
|
+
user: str,
|
330
|
+
password: str,
|
331
|
+
addr: str = "localhost:2049",
|
332
|
+
allow_other: bool = False,
|
333
|
+
) -> Process:
|
334
|
+
"""Serve a remote or directory via NFS.
|
335
|
+
|
336
|
+
Args:
|
337
|
+
src: Remote or directory to serve
|
338
|
+
addr: Network address and port to serve on (default: localhost:2049)
|
339
|
+
allow_other: Allow other users to access the share
|
340
|
+
|
341
|
+
Returns:
|
342
|
+
Process: The running NFS server process
|
343
|
+
|
344
|
+
Raises:
|
345
|
+
ValueError: If the NFS server fails to start
|
346
|
+
"""
|
347
|
+
src_str = convert_to_str(src)
|
348
|
+
cmd_list: list[str] = ["serve", "webdav", "--addr", addr, src_str]
|
349
|
+
cmd_list.extend(["--user", user, "--pass", password])
|
350
|
+
if allow_other:
|
351
|
+
cmd_list.append("--allow-other")
|
352
|
+
proc = self._launch_process(cmd_list)
|
353
|
+
time.sleep(2) # give it a moment to start
|
354
|
+
if proc.poll() is not None:
|
355
|
+
raise ValueError("NFS serve process failed to start")
|
356
|
+
return proc
|
@@ -0,0 +1,158 @@
|
|
1
|
+
"""
|
2
|
+
Unit test file.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import unittest
|
7
|
+
from pathlib import Path
|
8
|
+
|
9
|
+
import httpx
|
10
|
+
from dotenv import load_dotenv
|
11
|
+
|
12
|
+
from rclone_api import Process, Rclone, Remote
|
13
|
+
|
14
|
+
load_dotenv()
|
15
|
+
|
16
|
+
BUCKET_NAME = os.getenv("BUCKET_NAME") # Default if not in .env
|
17
|
+
|
18
|
+
|
19
|
+
def _generate_rclone_config() -> Path:
|
20
|
+
|
21
|
+
# BUCKET_NAME = os.getenv("BUCKET_NAME", "TorrentBooks") # Default if not in .env
|
22
|
+
|
23
|
+
# Load additional environment variables
|
24
|
+
BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
|
25
|
+
BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
|
26
|
+
# BUCKET_URL = os.getenv("BUCKET_URL")
|
27
|
+
BUCKET_URL = "sfo3.digitaloceanspaces.com"
|
28
|
+
|
29
|
+
config_text = f"""
|
30
|
+
[dst]
|
31
|
+
type = s3
|
32
|
+
provider = DigitalOcean
|
33
|
+
access_key_id = {BUCKET_KEY_PUBLIC}
|
34
|
+
secret_access_key = {BUCKET_KEY_SECRET}
|
35
|
+
endpoint = {BUCKET_URL}
|
36
|
+
bucket = {BUCKET_NAME}
|
37
|
+
|
38
|
+
|
39
|
+
[webdav]
|
40
|
+
type = webdav
|
41
|
+
user = guest
|
42
|
+
# encrypted password for "1234"
|
43
|
+
pass = d4IbQLV9W0JhI2tm5Zp88hpMtEg
|
44
|
+
url = http://localhost:8089
|
45
|
+
vendor = rclone
|
46
|
+
"""
|
47
|
+
|
48
|
+
# out = Config(config_text)
|
49
|
+
out = Path("rclone_nfs.conf")
|
50
|
+
out.write_text(config_text, encoding="utf-8")
|
51
|
+
return out
|
52
|
+
|
53
|
+
|
54
|
+
class RcloneNfsServeTest(unittest.TestCase):
|
55
|
+
"""Test rclone functionality."""
|
56
|
+
|
57
|
+
def setUp(self) -> None:
|
58
|
+
"""Check if all required environment variables are set before running tests."""
|
59
|
+
required_vars = [
|
60
|
+
"BUCKET_NAME",
|
61
|
+
"BUCKET_KEY_SECRET",
|
62
|
+
"BUCKET_KEY_PUBLIC",
|
63
|
+
"BUCKET_URL",
|
64
|
+
]
|
65
|
+
missing = [var for var in required_vars if not os.getenv(var)]
|
66
|
+
if missing:
|
67
|
+
self.skipTest(
|
68
|
+
f"Missing required environment variables: {', '.join(missing)}"
|
69
|
+
)
|
70
|
+
os.environ["RCLONE_API_VERBOSE"] = "1"
|
71
|
+
self.config = _generate_rclone_config()
|
72
|
+
self.rclone = Rclone(self.config)
|
73
|
+
|
74
|
+
def tearDown(self):
|
75
|
+
if self.config.exists():
|
76
|
+
self.config.unlink()
|
77
|
+
|
78
|
+
def test_serve_webdav(self) -> None:
|
79
|
+
"""Test basic NFS serve functionality."""
|
80
|
+
config = _generate_rclone_config()
|
81
|
+
rclone = Rclone(config)
|
82
|
+
|
83
|
+
# Start NFS server for the remote
|
84
|
+
remote = Remote("dst", rclone=rclone)
|
85
|
+
# serve = Remote("webdav", rclone=rclone)
|
86
|
+
test_addr = "localhost:8089"
|
87
|
+
user = "guest"
|
88
|
+
password = "1234"
|
89
|
+
|
90
|
+
process = rclone.serve_webdav(
|
91
|
+
f"{remote.name}:{BUCKET_NAME}", addr=test_addr, user=user, password=password
|
92
|
+
)
|
93
|
+
mount_proc: Process | None = None
|
94
|
+
|
95
|
+
try:
|
96
|
+
# Verify process is running
|
97
|
+
self.assertIsNone(process.poll())
|
98
|
+
response = httpx.get(f"http://{test_addr}/", auth=(user, password))
|
99
|
+
# Note that windows is kinda broken and returns internal server error
|
100
|
+
is_serving = response.status_code == 200
|
101
|
+
self.assertTrue(is_serving)
|
102
|
+
|
103
|
+
finally:
|
104
|
+
# Clean up
|
105
|
+
process.terminate()
|
106
|
+
process.wait()
|
107
|
+
if mount_proc:
|
108
|
+
mount_proc.terminate()
|
109
|
+
mount_proc.wait()
|
110
|
+
|
111
|
+
# Verify process terminated
|
112
|
+
self.assertIsNotNone(process.poll())
|
113
|
+
|
114
|
+
def test_serve_webdav_and_mount(self) -> None:
|
115
|
+
"""Test basic NFS serve functionality."""
|
116
|
+
config = _generate_rclone_config()
|
117
|
+
rclone = Rclone(config)
|
118
|
+
|
119
|
+
# Start NFS server for the remote
|
120
|
+
remote = Remote("dst", rclone=rclone)
|
121
|
+
# serve = Remote("webdav", rclone=rclone)
|
122
|
+
test_addr = "localhost:8089"
|
123
|
+
user = "guest"
|
124
|
+
password = "1234"
|
125
|
+
|
126
|
+
process = rclone.serve_webdav(
|
127
|
+
f"{remote.name}:{BUCKET_NAME}", addr=test_addr, user=user, password=password
|
128
|
+
)
|
129
|
+
mount_proc: Process | None = None
|
130
|
+
|
131
|
+
try:
|
132
|
+
# Verify process is running
|
133
|
+
self.assertIsNone(process.poll())
|
134
|
+
mount_point = Path("test_mount2")
|
135
|
+
mount_proc = rclone.mount_webdav("webdav:", mount_point)
|
136
|
+
# test that the mount point exists
|
137
|
+
self.assertTrue(mount_point.exists())
|
138
|
+
# test the folder is not empty
|
139
|
+
dirs = next(mount_point.iterdir())
|
140
|
+
self.assertTrue(dirs.is_dir())
|
141
|
+
|
142
|
+
finally:
|
143
|
+
# Clean up
|
144
|
+
if mount_proc:
|
145
|
+
mount_proc.terminate()
|
146
|
+
mount_proc.wait()
|
147
|
+
process.terminate()
|
148
|
+
process.wait()
|
149
|
+
if mount_proc:
|
150
|
+
mount_proc.terminate()
|
151
|
+
mount_proc.wait()
|
152
|
+
|
153
|
+
# Verify process terminated
|
154
|
+
self.assertIsNotNone(process.poll())
|
155
|
+
|
156
|
+
|
157
|
+
if __name__ == "__main__":
|
158
|
+
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
|