rclone-api 1.0.17__tar.gz → 1.0.19__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. {rclone_api-1.0.17 → rclone_api-1.0.19}/.gitignore +1 -1
  2. {rclone_api-1.0.17 → rclone_api-1.0.19}/PKG-INFO +2 -1
  3. {rclone_api-1.0.17 → rclone_api-1.0.19}/lint +2 -0
  4. {rclone_api-1.0.17 → rclone_api-1.0.19}/pyproject.toml +3 -2
  5. {rclone_api-1.0.17 → rclone_api-1.0.19}/requirements.testing.txt +2 -0
  6. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/dir.py +4 -2
  7. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/process.py +3 -1
  8. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/rclone.py +97 -5
  9. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/walk.py +40 -7
  10. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api.egg-info/PKG-INFO +2 -1
  11. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api.egg-info/SOURCES.txt +3 -0
  12. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api.egg-info/requires.txt +1 -0
  13. {rclone_api-1.0.17 → rclone_api-1.0.19}/tests/test_ls.py +1 -0
  14. rclone_api-1.0.19/tests/test_mount_webdav.py +114 -0
  15. rclone_api-1.0.17/tests/test_walk.py → rclone_api-1.0.19/tests/test_obscure.py +7 -8
  16. rclone_api-1.0.19/tests/test_serve_webdav.py +108 -0
  17. rclone_api-1.0.19/tests/test_walk.py +86 -0
  18. {rclone_api-1.0.17 → rclone_api-1.0.19}/.aiderignore +0 -0
  19. {rclone_api-1.0.17 → rclone_api-1.0.19}/.github/workflows/lint.yml +0 -0
  20. {rclone_api-1.0.17 → rclone_api-1.0.19}/.github/workflows/push_macos.yml +0 -0
  21. {rclone_api-1.0.17 → rclone_api-1.0.19}/.github/workflows/push_ubuntu.yml +0 -0
  22. {rclone_api-1.0.17 → rclone_api-1.0.19}/.github/workflows/push_win.yml +0 -0
  23. {rclone_api-1.0.17 → rclone_api-1.0.19}/.pylintrc +0 -0
  24. {rclone_api-1.0.17 → rclone_api-1.0.19}/.vscode/launch.json +0 -0
  25. {rclone_api-1.0.17 → rclone_api-1.0.19}/.vscode/settings.json +0 -0
  26. {rclone_api-1.0.17 → rclone_api-1.0.19}/.vscode/tasks.json +0 -0
  27. {rclone_api-1.0.17 → rclone_api-1.0.19}/LICENSE +0 -0
  28. {rclone_api-1.0.17 → rclone_api-1.0.19}/MANIFEST.in +0 -0
  29. {rclone_api-1.0.17 → rclone_api-1.0.19}/README.md +0 -0
  30. {rclone_api-1.0.17 → rclone_api-1.0.19}/clean +0 -0
  31. {rclone_api-1.0.17 → rclone_api-1.0.19}/install +0 -0
  32. {rclone_api-1.0.17 → rclone_api-1.0.19}/setup.cfg +0 -0
  33. {rclone_api-1.0.17 → rclone_api-1.0.19}/setup.py +0 -0
  34. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/__init__.py +0 -0
  35. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/assets/example.txt +0 -0
  36. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/cli.py +0 -0
  37. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/config.py +0 -0
  38. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/convert.py +0 -0
  39. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/dir_listing.py +0 -0
  40. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/exec.py +0 -0
  41. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/file.py +0 -0
  42. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/filelist.py +0 -0
  43. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/remote.py +0 -0
  44. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/rpath.py +0 -0
  45. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api/util.py +0 -0
  46. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api.egg-info/dependency_links.txt +0 -0
  47. {rclone_api-1.0.17 → rclone_api-1.0.19}/src/rclone_api.egg-info/top_level.txt +0 -0
  48. {rclone_api-1.0.17 → rclone_api-1.0.19}/test +0 -0
  49. {rclone_api-1.0.17 → rclone_api-1.0.19}/tests/test_copy.py +0 -0
  50. {rclone_api-1.0.17 → rclone_api-1.0.19}/tests/test_is_synced.py +0 -0
  51. {rclone_api-1.0.17 → rclone_api-1.0.19}/tests/test_mount.py +0 -0
  52. {rclone_api-1.0.17 → rclone_api-1.0.19}/tests/test_remotes.py +0 -0
  53. {rclone_api-1.0.17 → rclone_api-1.0.19}/tox.ini +0 -0
  54. {rclone_api-1.0.17 → rclone_api-1.0.19}/upload_package.sh +0 -0
@@ -144,4 +144,4 @@ uv.lock
144
144
  !.aider.conf.yml
145
145
  !.aiderignore
146
146
 
147
- rclone.conf
147
+ rclone*.conf
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.0.17
3
+ Version: 1.0.19
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  Maintainer: Zachary Vorhies
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3
10
10
  Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
+ Requires-Dist: pyright>=1.1.393
13
14
  Requires-Dist: python-dotenv>=1.0.0
14
15
  Dynamic: home-page
15
16
  Dynamic: maintainer
@@ -11,5 +11,7 @@ echo Running isort src tests
11
11
  uv run isort --profile black src tests
12
12
  echo Running mypy src
13
13
  uv run mypy src tests
14
+ echo Running pyright src tests
15
+ uv run pyright src tests --threads
14
16
  echo Linting complete!
15
17
  exit 0
@@ -11,10 +11,11 @@ keywords = ["template-python-cmd"]
11
11
  license = { text = "BSD 3-Clause License" }
12
12
  classifiers = ["Programming Language :: Python :: 3"]
13
13
  dependencies = [
14
- "python-dotenv>=1.0.0"
14
+ "pyright>=1.1.393",
15
+ "python-dotenv>=1.0.0",
15
16
  ]
16
17
  # Change this with the version number bump.
17
- version = "1.0.17"
18
+ version = "1.0.19"
18
19
 
19
20
  [tool.setuptools]
20
21
  package-dir = {"" = "src"}
@@ -5,3 +5,5 @@ pytest
5
5
  tox
6
6
  ruff
7
7
  pytest-xdist
8
+ httpx
9
+ pyright
@@ -47,12 +47,14 @@ class Dir:
47
47
  dir = Dir(self.path)
48
48
  return self.path.rclone.ls(dir, max_depth=max_depth)
49
49
 
50
- def walk(self, max_depth: int = -1) -> Generator[DirListing, None, None]:
50
+ def walk(
51
+ self, breadth_first: bool, max_depth: int = -1
52
+ ) -> Generator[DirListing, None, None]:
51
53
  """List files and directories in the given path."""
52
54
  from rclone_api.walk import walk
53
55
 
54
56
  assert self.path.rclone is not None
55
- return walk(self, max_depth=max_depth)
57
+ return walk(self, breadth_first=breadth_first, max_depth=max_depth)
56
58
 
57
59
  def to_json(self) -> dict:
58
60
  """Convert the Dir to a JSON serializable dictionary."""
@@ -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:
@@ -38,6 +38,12 @@ class Rclone:
38
38
  def _launch_process(self, cmd: list[str]) -> Process:
39
39
  return self._exec.launch_process(cmd)
40
40
 
41
+ def obscure(self, password: str) -> str:
42
+ """Obscure a password for use in rclone config files."""
43
+ cmd_list: list[str] = ["obscure", password]
44
+ cp = self._run(cmd_list)
45
+ return cp.stdout.strip()
46
+
41
47
  def ls(
42
48
  self,
43
49
  path: Dir | Remote | str,
@@ -61,8 +67,9 @@ class Rclone:
61
67
 
62
68
  cmd = ["lsjson"]
63
69
  if max_depth is not None:
64
- cmd.append("--recursive")
65
- if max_depth > -1:
70
+ if max_depth < 0:
71
+ cmd.append("--recursive")
72
+ if max_depth > 0:
66
73
  cmd.append("--max-depth")
67
74
  cmd.append(str(max_depth))
68
75
  cmd.append(str(path))
@@ -96,7 +103,7 @@ class Rclone:
96
103
  return out
97
104
 
98
105
  def walk(
99
- self, path: Dir | Remote | str, max_depth: int = -1
106
+ self, path: Dir | Remote | str, max_depth: int = -1, breadth_first: bool = True
100
107
  ) -> Generator[DirListing, None, None]:
101
108
  """Walk through the given path recursively.
102
109
 
@@ -107,6 +114,7 @@ class Rclone:
107
114
  Yields:
108
115
  DirListing: Directory listing for each directory encountered
109
116
  """
117
+ dir_obj: Dir
110
118
  if isinstance(path, Dir):
111
119
  # Create a Remote object for the path
112
120
  remote = path.remote
@@ -126,9 +134,10 @@ class Rclone:
126
134
  elif isinstance(path, Remote):
127
135
  dir_obj = Dir(path)
128
136
  else:
137
+ dir_obj = Dir(path) # shut up pyright
129
138
  assert f"Invalid type for path: {type(path)}"
130
139
 
131
- yield from walk(dir_obj, max_depth=max_depth)
140
+ yield from walk(dir_obj, max_depth=max_depth, breadth_first=breadth_first)
132
141
 
133
142
  def copyfile(self, src: File | str, dst: File | str) -> None:
134
143
  """Copy a single file from source to destination.
@@ -239,7 +248,13 @@ class Rclone:
239
248
  return self._run(cmd_list)
240
249
 
241
250
  def mount(
242
- self, src: Remote | Dir | str, outdir: Path, allow_writes=False, use_links=True
251
+ self,
252
+ src: Remote | Dir | str,
253
+ outdir: Path,
254
+ allow_writes=False,
255
+ use_links=True,
256
+ vfs_cache_mode="full",
257
+ other_cmds: list[str] | None = None,
243
258
  ) -> Process:
244
259
  """Mount a remote or directory to a local path.
245
260
 
@@ -266,8 +281,85 @@ class Rclone:
266
281
  cmd_list.append("--read-only")
267
282
  if use_links:
268
283
  cmd_list.append("--links")
284
+ if vfs_cache_mode:
285
+ cmd_list.append("--vfs-cache-mode")
286
+ cmd_list.append(vfs_cache_mode)
287
+ if other_cmds:
288
+ cmd_list += other_cmds
269
289
  proc = self._launch_process(cmd_list)
270
290
  time.sleep(2) # give it a moment to mount
271
291
  if proc.poll() is not None:
272
292
  raise ValueError("Mount process failed to start")
273
293
  return proc
294
+
295
+ def mount_webdav(
296
+ self,
297
+ url: str,
298
+ outdir: Path,
299
+ vfs_cache_mode="full",
300
+ other_cmds: list[str] | None = None,
301
+ ) -> Process:
302
+ """Mount a remote or directory to a local path.
303
+
304
+ Args:
305
+ src: Remote or directory to mount
306
+ outdir: Local path to mount to
307
+
308
+ Returns:
309
+ CompletedProcess from the mount command execution
310
+
311
+ Raises:
312
+ subprocess.CalledProcessError: If the mount operation fails
313
+ """
314
+ if outdir.exists():
315
+ is_empty = not list(outdir.iterdir())
316
+ if not is_empty:
317
+ raise ValueError(
318
+ f"Mount directory already exists and is not empty: {outdir}"
319
+ )
320
+ outdir.rmdir()
321
+
322
+ src_str = url
323
+ cmd_list: list[str] = ["mount", src_str, str(outdir)]
324
+ cmd_list.append("--vfs-cache-mode")
325
+ cmd_list.append(vfs_cache_mode)
326
+ if other_cmds:
327
+ cmd_list += other_cmds
328
+ proc = self._launch_process(cmd_list)
329
+ # proc = rclone_exec.launch_process(cmd_list)
330
+ time.sleep(2)
331
+ if proc.poll() is not None:
332
+ raise ValueError("Mount process failed to start")
333
+ return proc
334
+
335
+ def serve_webdav(
336
+ self,
337
+ src: Remote | Dir | str,
338
+ user: str,
339
+ password: str,
340
+ addr: str = "localhost:2049",
341
+ allow_other: bool = False,
342
+ ) -> Process:
343
+ """Serve a remote or directory via NFS.
344
+
345
+ Args:
346
+ src: Remote or directory to serve
347
+ addr: Network address and port to serve on (default: localhost:2049)
348
+ allow_other: Allow other users to access the share
349
+
350
+ Returns:
351
+ Process: The running NFS server process
352
+
353
+ Raises:
354
+ ValueError: If the NFS server fails to start
355
+ """
356
+ src_str = convert_to_str(src)
357
+ cmd_list: list[str] = ["serve", "webdav", "--addr", addr, src_str]
358
+ cmd_list.extend(["--user", user, "--pass", password])
359
+ if allow_other:
360
+ cmd_list.append("--allow-other")
361
+ proc = self._launch_process(cmd_list)
362
+ time.sleep(2) # give it a moment to start
363
+ if proc.poll() is not None:
364
+ raise ValueError("NFS serve process failed to start")
365
+ return proc
@@ -9,9 +9,11 @@ from rclone_api.remote import Remote
9
9
  _MAX_OUT_QUEUE_SIZE = 50
10
10
 
11
11
 
12
- def _walk_runner(
13
- queue: Queue[Dir], max_depth: int, out_queue: Queue[DirListing | None]
12
+ def _walk_runner_breadth_first(
13
+ dir: Dir, max_depth: int, out_queue: Queue[DirListing | None]
14
14
  ) -> None:
15
+ queue: Queue[Dir] = Queue()
16
+ queue.put(dir)
15
17
  try:
16
18
  while not queue.empty():
17
19
  current_dir = queue.get()
@@ -35,7 +37,35 @@ def _walk_runner(
35
37
  _thread.interrupt_main()
36
38
 
37
39
 
38
- def walk(dir: Dir | Remote, max_depth: int = -1) -> Generator[DirListing, None, None]:
40
+ def _walk_runner_depth_first(
41
+ dir: Dir, max_depth: int, out_queue: Queue[DirListing | None]
42
+ ) -> None:
43
+ try:
44
+ stack = [(dir, max_depth)]
45
+ while stack:
46
+ current_dir, depth = stack.pop()
47
+ dirlisting = current_dir.ls()
48
+ if depth != 0:
49
+ for subdir in reversed(
50
+ dirlisting.dirs
51
+ ): # Process deeper directories first
52
+ # stack.append((child, depth - 1 if depth > 0 else depth))
53
+ next_depth = depth - 1 if depth > 0 else depth
54
+ _walk_runner_depth_first(subdir, next_depth, out_queue)
55
+ out_queue.put(dirlisting)
56
+ out_queue.put(None)
57
+ except KeyboardInterrupt:
58
+ import _thread
59
+
60
+ out_queue.put(None)
61
+ _thread.interrupt_main()
62
+
63
+
64
+ def walk(
65
+ dir: Dir | Remote,
66
+ breadth_first: bool,
67
+ max_depth: int = -1,
68
+ ) -> Generator[DirListing, None, None]:
39
69
  """Walk through the given directory recursively.
40
70
 
41
71
  Args:
@@ -49,14 +79,17 @@ def walk(dir: Dir | Remote, max_depth: int = -1) -> Generator[DirListing, None,
49
79
  # Convert Remote to Dir if needed
50
80
  if isinstance(dir, Remote):
51
81
  dir = Dir(dir)
52
-
53
- in_queue: Queue[Dir] = Queue()
54
82
  out_queue: Queue[DirListing] = Queue(maxsize=_MAX_OUT_QUEUE_SIZE)
55
- in_queue.put(dir)
83
+
84
+ strategy = (
85
+ _walk_runner_breadth_first if breadth_first else _walk_runner_depth_first
86
+ )
56
87
 
57
88
  # Start worker thread
58
89
  worker = Thread(
59
- target=_walk_runner, args=(in_queue, max_depth, out_queue), daemon=True
90
+ target=strategy,
91
+ args=(dir, max_depth, out_queue),
92
+ daemon=True,
60
93
  )
61
94
  worker.start()
62
95
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.0.17
3
+ Version: 1.0.19
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  Maintainer: Zachary Vorhies
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3
10
10
  Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
+ Requires-Dist: pyright>=1.1.393
13
14
  Requires-Dist: python-dotenv>=1.0.0
14
15
  Dynamic: home-page
15
16
  Dynamic: maintainer
@@ -45,5 +45,8 @@ tests/test_copy.py
45
45
  tests/test_is_synced.py
46
46
  tests/test_ls.py
47
47
  tests/test_mount.py
48
+ tests/test_mount_webdav.py
49
+ tests/test_obscure.py
48
50
  tests/test_remotes.py
51
+ tests/test_serve_webdav.py
49
52
  tests/test_walk.py
@@ -1 +1,2 @@
1
+ pyright>=1.1.393
1
2
  python-dotenv>=1.0.0
@@ -31,6 +31,7 @@ provider = DigitalOcean
31
31
  access_key_id = {BUCKET_KEY_PUBLIC}
32
32
  secret_access_key = {BUCKET_KEY_SECRET}
33
33
  endpoint = {BUCKET_URL}
34
+ bucket = {BUCKET_NAME}
34
35
  """
35
36
 
36
37
  out = Config(config_text)
@@ -0,0 +1,114 @@
1
+ """
2
+ Unit test file.
3
+ """
4
+
5
+ import os
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ from dotenv import load_dotenv
10
+
11
+ from rclone_api import Config, Process, Rclone, Remote
12
+
13
+ load_dotenv()
14
+
15
+ BUCKET_NAME = os.getenv("BUCKET_NAME") # Default if not in .env
16
+
17
+
18
+ def _generate_rclone_config(port: int) -> Config:
19
+
20
+ # BUCKET_NAME = os.getenv("BUCKET_NAME", "TorrentBooks") # Default if not in .env
21
+
22
+ # Load additional environment variables
23
+ BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
24
+ BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
25
+ # BUCKET_URL = os.getenv("BUCKET_URL")
26
+ BUCKET_URL = "sfo3.digitaloceanspaces.com"
27
+
28
+ config_text = f"""
29
+ [dst]
30
+ type = s3
31
+ provider = DigitalOcean
32
+ access_key_id = {BUCKET_KEY_PUBLIC}
33
+ secret_access_key = {BUCKET_KEY_SECRET}
34
+ endpoint = {BUCKET_URL}
35
+ bucket = {BUCKET_NAME}
36
+
37
+
38
+ [webdav]
39
+ type = webdav
40
+ user = guest
41
+ # obscured password for "1234", use Rclone.obscure("1234") to generate
42
+ pass = d4IbQLV9W0JhI2tm5Zp88hpMtEg
43
+ url = http://localhost:{port}
44
+ vendor = rclone
45
+ """
46
+
47
+ out = Config(config_text)
48
+ return out
49
+
50
+
51
+ class RcloneMountWebdavTester(unittest.TestCase):
52
+ """Test rclone functionality."""
53
+
54
+ def setUp(self) -> None:
55
+ """Check if all required environment variables are set before running tests."""
56
+ required_vars = [
57
+ "BUCKET_NAME",
58
+ "BUCKET_KEY_SECRET",
59
+ "BUCKET_KEY_PUBLIC",
60
+ "BUCKET_URL",
61
+ ]
62
+ missing = [var for var in required_vars if not os.getenv(var)]
63
+ if missing:
64
+ self.skipTest(
65
+ f"Missing required environment variables: {', '.join(missing)}"
66
+ )
67
+ os.environ["RCLONE_API_VERBOSE"] = "1"
68
+
69
+ def test_serve_webdav_and_mount(self) -> None:
70
+ """Test basic NFS serve functionality."""
71
+ port = 8090
72
+ config = _generate_rclone_config(port)
73
+ rclone = Rclone(config)
74
+
75
+ # Start NFS server for the remote
76
+ remote = Remote("dst", rclone=rclone)
77
+ # serve = Remote("webdav", rclone=rclone)
78
+ test_addr = f"localhost:{port}"
79
+ user = "guest"
80
+ password = "1234"
81
+
82
+ process = rclone.serve_webdav(
83
+ f"{remote.name}:{BUCKET_NAME}", addr=test_addr, user=user, password=password
84
+ )
85
+ mount_proc: Process | None = None
86
+
87
+ try:
88
+ # Verify process is running
89
+ self.assertIsNone(process.poll())
90
+ mount_point = Path("test_mount2")
91
+ mount_proc = rclone.mount_webdav("webdav:", mount_point)
92
+ # test that the mount point exists
93
+ self.assertTrue(mount_point.exists())
94
+ # test the folder is not empty
95
+ next_path = next(mount_point.iterdir())
96
+ self.assertIsNotNone(next_path)
97
+
98
+ finally:
99
+ # Clean up
100
+ if mount_proc:
101
+ mount_proc.terminate()
102
+ mount_proc.wait()
103
+ process.terminate()
104
+ process.wait()
105
+ if mount_proc:
106
+ mount_proc.terminate()
107
+ mount_proc.wait()
108
+
109
+ # Verify process terminated
110
+ self.assertIsNotNone(process.poll())
111
+
112
+
113
+ if __name__ == "__main__":
114
+ unittest.main()
@@ -7,7 +7,7 @@ import unittest
7
7
 
8
8
  from dotenv import load_dotenv
9
9
 
10
- from rclone_api import Config, DirListing, Rclone
10
+ from rclone_api import Config, Rclone
11
11
 
12
12
  load_dotenv()
13
13
 
@@ -31,13 +31,14 @@ provider = DigitalOcean
31
31
  access_key_id = {BUCKET_KEY_PUBLIC}
32
32
  secret_access_key = {BUCKET_KEY_SECRET}
33
33
  endpoint = {BUCKET_URL}
34
+ bucket = {BUCKET_NAME}
34
35
  """
35
36
 
36
37
  out = Config(config_text)
37
38
  return out
38
39
 
39
40
 
40
- class RcloneWalkTest(unittest.TestCase):
41
+ class RcloneLsTests(unittest.TestCase):
41
42
  """Test rclone functionality."""
42
43
 
43
44
  def setUp(self) -> None:
@@ -55,13 +56,11 @@ class RcloneWalkTest(unittest.TestCase):
55
56
  )
56
57
  os.environ["RCLONE_API_VERBOSE"] = "1"
57
58
 
58
- def test_walk(self) -> None:
59
+ def test_list_remotes(self) -> None:
59
60
  rclone = Rclone(_generate_rclone_config())
60
- # rclone.walk
61
- dirlisting: DirListing
62
- for dirlisting in rclone.walk(f"dst:{BUCKET_NAME}", max_depth=1):
63
- print(dirlisting)
64
- print("done")
61
+ obscured = rclone.obscure("1234")
62
+ self.assertNotEqual(obscured, "1234")
63
+ print()
65
64
 
66
65
 
67
66
  if __name__ == "__main__":
@@ -0,0 +1,108 @@
1
+ """
2
+ Unit test file.
3
+ """
4
+
5
+ import os
6
+ import unittest
7
+
8
+ import httpx
9
+ from dotenv import load_dotenv
10
+
11
+ from rclone_api import Config, Process, Rclone, Remote
12
+
13
+ load_dotenv()
14
+
15
+ BUCKET_NAME = os.getenv("BUCKET_NAME") # Default if not in .env
16
+
17
+
18
+ def _generate_rclone_config(port: int) -> Config:
19
+
20
+ # BUCKET_NAME = os.getenv("BUCKET_NAME", "TorrentBooks") # Default if not in .env
21
+
22
+ # Load additional environment variables
23
+ BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
24
+ BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
25
+ # BUCKET_URL = os.getenv("BUCKET_URL")
26
+ BUCKET_URL = "sfo3.digitaloceanspaces.com"
27
+
28
+ config_text = f"""
29
+ [dst]
30
+ type = s3
31
+ provider = DigitalOcean
32
+ access_key_id = {BUCKET_KEY_PUBLIC}
33
+ secret_access_key = {BUCKET_KEY_SECRET}
34
+ endpoint = {BUCKET_URL}
35
+ bucket = {BUCKET_NAME}
36
+
37
+
38
+ [webdav]
39
+ type = webdav
40
+ user = guest
41
+ # obscured password for "1234", use Rclone.obscure("1234") to generate
42
+ pass = d4IbQLV9W0JhI2tm5Zp88hpMtEg
43
+ url = http://localhost:{port}
44
+ vendor = rclone
45
+ """
46
+
47
+ out = Config(config_text)
48
+ return out
49
+
50
+
51
+ class RcloneServeWebdavTester(unittest.TestCase):
52
+ """Test rclone functionality."""
53
+
54
+ def setUp(self) -> None:
55
+ """Check if all required environment variables are set before running tests."""
56
+ required_vars = [
57
+ "BUCKET_NAME",
58
+ "BUCKET_KEY_SECRET",
59
+ "BUCKET_KEY_PUBLIC",
60
+ "BUCKET_URL",
61
+ ]
62
+ missing = [var for var in required_vars if not os.getenv(var)]
63
+ if missing:
64
+ self.skipTest(
65
+ f"Missing required environment variables: {', '.join(missing)}"
66
+ )
67
+ os.environ["RCLONE_API_VERBOSE"] = "1"
68
+
69
+ def test_serve_webdav(self) -> None:
70
+ """Test basic NFS serve functionality."""
71
+ port = 8089
72
+ config = _generate_rclone_config(port)
73
+ rclone = Rclone(config)
74
+
75
+ # Start NFS server for the remote
76
+ remote = Remote("dst", rclone=rclone)
77
+ # serve = Remote("webdav", rclone=rclone)
78
+ test_addr = f"localhost:{port}"
79
+ user = "guest"
80
+ password = "1234"
81
+
82
+ process = rclone.serve_webdav(
83
+ f"{remote.name}:{BUCKET_NAME}", addr=test_addr, user=user, password=password
84
+ )
85
+ mount_proc: Process | None = None
86
+
87
+ try:
88
+ # Verify process is running
89
+ self.assertIsNone(process.poll())
90
+ response = httpx.get(f"http://{test_addr}/", auth=(user, password))
91
+ # Note that windows is kinda broken and returns internal server error
92
+ is_serving = response.status_code == 200
93
+ self.assertTrue(is_serving)
94
+
95
+ finally:
96
+ # Clean up
97
+ process.terminate()
98
+ process.wait()
99
+ if mount_proc:
100
+ mount_proc.terminate()
101
+ mount_proc.wait()
102
+
103
+ # Verify process terminated
104
+ self.assertIsNotNone(process.poll())
105
+
106
+
107
+ if __name__ == "__main__":
108
+ unittest.main()
@@ -0,0 +1,86 @@
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, DirListing, 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 RcloneWalkTest(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_walk(self) -> None:
59
+ rclone = Rclone(_generate_rclone_config())
60
+ # rclone.walk
61
+ dirlisting: DirListing
62
+ is_first = True
63
+ for dirlisting in rclone.walk(f"dst:{BUCKET_NAME}", max_depth=1):
64
+ if is_first:
65
+ # assert just one file
66
+ # assert len(dirlisting.files) == 1
67
+ self.assertEqual(len(dirlisting.files), 1)
68
+ # assert it's first.txt
69
+ self.assertEqual(dirlisting.files[0].name, "first.txt")
70
+ is_first = False
71
+ print(dirlisting)
72
+ print("done")
73
+
74
+ def test_walk_depth_first(self) -> None:
75
+ rclone = Rclone(_generate_rclone_config())
76
+ # rclone.walk
77
+ dirlisting: DirListing
78
+ for dirlisting in rclone.walk(
79
+ f"dst:{BUCKET_NAME}", max_depth=1, breadth_first=False
80
+ ):
81
+ print(dirlisting)
82
+ print("done")
83
+
84
+
85
+ if __name__ == "__main__":
86
+ 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