rclone-api 1.0.22__tar.gz → 1.0.24__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {rclone_api-1.0.22 → rclone_api-1.0.24}/PKG-INFO +1 -1
- {rclone_api-1.0.22 → rclone_api-1.0.24}/pyproject.toml +4 -1
- rclone_api-1.0.24/src/rclone_api/cmd/list_files.py +27 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/rclone.py +52 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api.egg-info/PKG-INFO +1 -1
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api.egg-info/SOURCES.txt +4 -0
- rclone_api-1.0.24/src/rclone_api.egg-info/entry_points.txt +2 -0
- rclone_api-1.0.24/tests/test_cmd_list_files.py +83 -0
- rclone_api-1.0.24/tests/test_mount_s3.py +105 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/.aiderignore +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/.github/workflows/lint.yml +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/.github/workflows/push_macos.yml +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/.github/workflows/push_ubuntu.yml +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/.github/workflows/push_win.yml +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/.gitignore +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/.pylintrc +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/.vscode/launch.json +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/.vscode/settings.json +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/.vscode/tasks.json +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/LICENSE +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/MANIFEST.in +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/README.md +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/clean +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/install +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/lint +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/requirements.testing.txt +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/setup.cfg +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/setup.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/__init__.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/config.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/convert.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/dir.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/dir_listing.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/exec.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/file.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/filelist.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/process.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/rpath.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/util.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api/walk.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api.egg-info/requires.txt +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/test +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/tests/test_copy.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/tests/test_is_synced.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/tests/test_ls.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/tests/test_mount.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/tests/test_mount_webdav.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/tests/test_obscure.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/tests/test_remotes.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/tests/test_serve_webdav.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/tests/test_walk.py +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/tox.ini +0 -0
- {rclone_api-1.0.22 → rclone_api-1.0.24}/upload_package.sh +0 -0
@@ -15,7 +15,7 @@ dependencies = [
|
|
15
15
|
"python-dotenv>=1.0.0",
|
16
16
|
]
|
17
17
|
# Change this with the version number bump.
|
18
|
-
version = "1.0.
|
18
|
+
version = "1.0.24"
|
19
19
|
|
20
20
|
[tool.setuptools]
|
21
21
|
package-dir = {"" = "src"}
|
@@ -43,3 +43,6 @@ profile = "black"
|
|
43
43
|
[tool.mypy]
|
44
44
|
ignore_missing_imports = true
|
45
45
|
disable_error_code = ["import-untyped"]
|
46
|
+
|
47
|
+
[project.scripts]
|
48
|
+
rclone-api-listfiles = "rclone_api.cmd.list_files:main"
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import argparse
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
from rclone_api import Rclone
|
5
|
+
|
6
|
+
|
7
|
+
def list_files(rclone: Rclone, path: str):
|
8
|
+
"""List files in a remote path."""
|
9
|
+
for dirlisting in rclone.walk(path):
|
10
|
+
for file in dirlisting.files:
|
11
|
+
print(file.path)
|
12
|
+
|
13
|
+
|
14
|
+
def _parse_args() -> argparse.Namespace:
|
15
|
+
parser = argparse.ArgumentParser(description="List files in a remote path.")
|
16
|
+
parser.add_argument("--config", help="Path to rclone config file", required=True)
|
17
|
+
parser.add_argument("path", help="Remote path to list")
|
18
|
+
return parser.parse_args()
|
19
|
+
|
20
|
+
|
21
|
+
def main() -> int:
|
22
|
+
"""Main entry point."""
|
23
|
+
args = _parse_args()
|
24
|
+
path = args.path
|
25
|
+
rclone = Rclone(Path(args.config))
|
26
|
+
list_files(rclone, path)
|
27
|
+
return 0
|
@@ -6,6 +6,7 @@ import subprocess
|
|
6
6
|
import time
|
7
7
|
import warnings
|
8
8
|
from concurrent.futures import ThreadPoolExecutor
|
9
|
+
from enum import Enum
|
9
10
|
from fnmatch import fnmatch
|
10
11
|
from pathlib import Path
|
11
12
|
from typing import Generator
|
@@ -23,6 +24,11 @@ from rclone_api.util import get_rclone_exe, to_path, wait_for_mount
|
|
23
24
|
from rclone_api.walk import walk
|
24
25
|
|
25
26
|
|
27
|
+
class ModTimeStrategy(Enum):
|
28
|
+
USE_SERVER_MODTIME = "use-server-modtime"
|
29
|
+
NO_MODTIME = "no-modtime"
|
30
|
+
|
31
|
+
|
26
32
|
class Rclone:
|
27
33
|
def __init__(
|
28
34
|
self, rclone_conf: Path | Config, rclone_exe: Path | None = None
|
@@ -299,6 +305,7 @@ class Rclone:
|
|
299
305
|
url: str,
|
300
306
|
outdir: Path,
|
301
307
|
vfs_cache_mode="full",
|
308
|
+
vfs_disk_space_total_size: str | None = "10G",
|
302
309
|
other_cmds: list[str] | None = None,
|
303
310
|
) -> Process:
|
304
311
|
"""Mount a remote or directory to a local path.
|
@@ -327,10 +334,55 @@ class Rclone:
|
|
327
334
|
cmd_list.append(vfs_cache_mode)
|
328
335
|
if other_cmds:
|
329
336
|
cmd_list += other_cmds
|
337
|
+
if vfs_disk_space_total_size is not None:
|
338
|
+
cmd_list.append("--vfs-cache-max-size")
|
339
|
+
cmd_list.append(vfs_disk_space_total_size)
|
330
340
|
proc = self._launch_process(cmd_list)
|
331
341
|
wait_for_mount(outdir, proc)
|
332
342
|
return proc
|
333
343
|
|
344
|
+
def mount_s3(
|
345
|
+
self,
|
346
|
+
url: str,
|
347
|
+
outdir: Path,
|
348
|
+
vfs_cache_mode="full",
|
349
|
+
transfers: int | None = 16,
|
350
|
+
modtime_strategy: (
|
351
|
+
ModTimeStrategy | None
|
352
|
+
) = ModTimeStrategy.USE_SERVER_MODTIME, # speeds up S3 operations
|
353
|
+
vfs_read_chunk_streams: int | None = 16,
|
354
|
+
vfs_read_chunk_size: str | None = "4M",
|
355
|
+
vfs_fast_fingerprint: bool = True,
|
356
|
+
other_cmds: list[str] | None = None,
|
357
|
+
) -> Process:
|
358
|
+
"""Mount a remote or directory to a local path.
|
359
|
+
|
360
|
+
Args:
|
361
|
+
src: Remote or directory to mount
|
362
|
+
outdir: Local path to mount to
|
363
|
+
"""
|
364
|
+
other_cmds = other_cmds or []
|
365
|
+
if modtime_strategy is not None:
|
366
|
+
other_cmds.append(f"--{modtime_strategy.value}")
|
367
|
+
if (vfs_cache_mode == "full" or vfs_cache_mode == "writes") and (
|
368
|
+
transfers is not None
|
369
|
+
):
|
370
|
+
other_cmds.append("--transfers")
|
371
|
+
other_cmds.append(str(transfers))
|
372
|
+
if vfs_read_chunk_streams:
|
373
|
+
other_cmds.append("--vfs-read-chunk-streams")
|
374
|
+
other_cmds.append(str(vfs_read_chunk_streams))
|
375
|
+
if vfs_read_chunk_size:
|
376
|
+
other_cmds.append("--vfs-read-chunk-size")
|
377
|
+
other_cmds.append(vfs_read_chunk_size)
|
378
|
+
if vfs_fast_fingerprint:
|
379
|
+
other_cmds.append("--vfs-fast-fingerprint")
|
380
|
+
|
381
|
+
other_cmds = other_cmds if other_cmds else None
|
382
|
+
return self.mount(
|
383
|
+
url, outdir, vfs_cache_mode=vfs_cache_mode, other_cmds=other_cmds
|
384
|
+
)
|
385
|
+
|
334
386
|
def serve_webdav(
|
335
387
|
self,
|
336
388
|
src: Remote | Dir | str,
|
@@ -38,13 +38,17 @@ src/rclone_api/walk.py
|
|
38
38
|
src/rclone_api.egg-info/PKG-INFO
|
39
39
|
src/rclone_api.egg-info/SOURCES.txt
|
40
40
|
src/rclone_api.egg-info/dependency_links.txt
|
41
|
+
src/rclone_api.egg-info/entry_points.txt
|
41
42
|
src/rclone_api.egg-info/requires.txt
|
42
43
|
src/rclone_api.egg-info/top_level.txt
|
43
44
|
src/rclone_api/assets/example.txt
|
45
|
+
src/rclone_api/cmd/list_files.py
|
46
|
+
tests/test_cmd_list_files.py
|
44
47
|
tests/test_copy.py
|
45
48
|
tests/test_is_synced.py
|
46
49
|
tests/test_ls.py
|
47
50
|
tests/test_mount.py
|
51
|
+
tests/test_mount_s3.py
|
48
52
|
tests/test_mount_webdav.py
|
49
53
|
tests/test_obscure.py
|
50
54
|
tests/test_remotes.py
|
@@ -0,0 +1,83 @@
|
|
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, Rclone, Remote
|
11
|
+
from rclone_api.cmd.list_files import list_files
|
12
|
+
|
13
|
+
load_dotenv()
|
14
|
+
|
15
|
+
BUCKET_NAME = os.getenv("BUCKET_NAME") # Default if not in .env
|
16
|
+
|
17
|
+
|
18
|
+
def _generate_rclone_config() -> 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
|
+
out = Config(config_text)
|
39
|
+
return out
|
40
|
+
|
41
|
+
|
42
|
+
class RcloneLsTests(unittest.TestCase):
|
43
|
+
"""Test rclone functionality."""
|
44
|
+
|
45
|
+
def setUp(self) -> None:
|
46
|
+
"""Check if all required environment variables are set before running tests."""
|
47
|
+
required_vars = [
|
48
|
+
"BUCKET_NAME",
|
49
|
+
"BUCKET_KEY_SECRET",
|
50
|
+
"BUCKET_KEY_PUBLIC",
|
51
|
+
"BUCKET_URL",
|
52
|
+
]
|
53
|
+
missing = [var for var in required_vars if not os.getenv(var)]
|
54
|
+
if missing:
|
55
|
+
self.skipTest(
|
56
|
+
f"Missing required environment variables: {', '.join(missing)}"
|
57
|
+
)
|
58
|
+
os.environ["RCLONE_API_VERBOSE"] = "1"
|
59
|
+
|
60
|
+
def test_list_remotes(self) -> None:
|
61
|
+
rclone = Rclone(_generate_rclone_config())
|
62
|
+
|
63
|
+
remotes: list[Remote] = rclone.listremotes()
|
64
|
+
self.assertGreater(len(remotes), 0)
|
65
|
+
for remote in remotes:
|
66
|
+
self.assertIsInstance(remote, Remote)
|
67
|
+
print(remote)
|
68
|
+
print("done")
|
69
|
+
|
70
|
+
def test_cmd_list_files(self) -> None:
|
71
|
+
"""Test listing the root directory of the bucket.
|
72
|
+
|
73
|
+
Verifies that we can:
|
74
|
+
1. Connect to the bucket
|
75
|
+
2. List its contents
|
76
|
+
3. Get both directories and files as proper types
|
77
|
+
"""
|
78
|
+
rclone = Rclone(_generate_rclone_config())
|
79
|
+
list_files(rclone, f"dst:{BUCKET_NAME}")
|
80
|
+
|
81
|
+
|
82
|
+
if __name__ == "__main__":
|
83
|
+
unittest.main()
|
@@ -0,0 +1,105 @@
|
|
1
|
+
"""
|
2
|
+
Unit test file for testing rclone mount functionality.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import subprocess
|
7
|
+
import unittest
|
8
|
+
from pathlib import Path
|
9
|
+
|
10
|
+
from dotenv import load_dotenv
|
11
|
+
|
12
|
+
from rclone_api import Config, Process, Rclone
|
13
|
+
|
14
|
+
load_dotenv()
|
15
|
+
|
16
|
+
|
17
|
+
def _generate_rclone_config() -> Config:
|
18
|
+
# Load environment variables
|
19
|
+
BUCKET_KEY_SECRET = os.getenv("BUCKET_KEY_SECRET")
|
20
|
+
BUCKET_KEY_PUBLIC = os.getenv("BUCKET_KEY_PUBLIC")
|
21
|
+
BUCKET_URL = "sfo3.digitaloceanspaces.com"
|
22
|
+
|
23
|
+
config_text = f"""
|
24
|
+
[dst]
|
25
|
+
type = s3
|
26
|
+
provider = DigitalOcean
|
27
|
+
access_key_id = {BUCKET_KEY_PUBLIC}
|
28
|
+
secret_access_key = {BUCKET_KEY_SECRET}
|
29
|
+
endpoint = {BUCKET_URL}
|
30
|
+
"""
|
31
|
+
return Config(config_text)
|
32
|
+
|
33
|
+
|
34
|
+
class RcloneMountS3Tests(unittest.TestCase):
|
35
|
+
"""Test rclone mount functionality."""
|
36
|
+
|
37
|
+
def setUp(self) -> None:
|
38
|
+
"""Check if all required environment variables are set before running tests."""
|
39
|
+
required_vars = [
|
40
|
+
"BUCKET_NAME",
|
41
|
+
"BUCKET_KEY_SECRET",
|
42
|
+
"BUCKET_KEY_PUBLIC",
|
43
|
+
"BUCKET_URL",
|
44
|
+
]
|
45
|
+
missing = [var for var in required_vars if not os.getenv(var)]
|
46
|
+
if missing:
|
47
|
+
self.skipTest(
|
48
|
+
f"Missing required environment variables: {', '.join(missing)}"
|
49
|
+
)
|
50
|
+
|
51
|
+
self.bucket_name = os.getenv("BUCKET_NAME")
|
52
|
+
self.mount_point = Path("test_mount")
|
53
|
+
# Create mount point directory if it doesn't exist
|
54
|
+
# self.mount_point.mkdir(exist_ok=True)
|
55
|
+
# make parents
|
56
|
+
parent = self.mount_point.parent
|
57
|
+
if not parent.exists():
|
58
|
+
parent.mkdir(parents=True)
|
59
|
+
|
60
|
+
os.environ["RCLONE_API_VERBOSE"] = "1"
|
61
|
+
self.rclone = Rclone(_generate_rclone_config())
|
62
|
+
|
63
|
+
def test_mount(self) -> None:
|
64
|
+
"""Test mounting a remote bucket."""
|
65
|
+
remote_path = f"dst:{self.bucket_name}"
|
66
|
+
process: Process | None = None
|
67
|
+
|
68
|
+
try:
|
69
|
+
# Start the mount process
|
70
|
+
process = self.rclone.mount_s3(remote_path, self.mount_point)
|
71
|
+
self.assertIsNone(
|
72
|
+
process.poll(), "Mount process should still be running after 2 seconds"
|
73
|
+
)
|
74
|
+
|
75
|
+
# Verify mount point exists and is accessible
|
76
|
+
self.assertTrue(self.mount_point.exists())
|
77
|
+
self.assertTrue(self.mount_point.is_dir())
|
78
|
+
|
79
|
+
# Check if we can list contents
|
80
|
+
contents = list(self.mount_point.iterdir())
|
81
|
+
self.assertGreater(
|
82
|
+
len(contents), 0, "Mounted directory should not be empty"
|
83
|
+
)
|
84
|
+
|
85
|
+
except subprocess.CalledProcessError as e:
|
86
|
+
self.fail(f"Mount operation failed: {str(e)}")
|
87
|
+
finally:
|
88
|
+
# Cleanup will happen in tearDown
|
89
|
+
if process:
|
90
|
+
if process.poll() is None:
|
91
|
+
process.kill()
|
92
|
+
stdout = process.stdout
|
93
|
+
if stdout:
|
94
|
+
# stdout is a buffered reader
|
95
|
+
for line in stdout:
|
96
|
+
print(line)
|
97
|
+
stderr = process.stderr
|
98
|
+
if stderr:
|
99
|
+
for line in stderr:
|
100
|
+
print(line)
|
101
|
+
process.kill()
|
102
|
+
|
103
|
+
|
104
|
+
if __name__ == "__main__":
|
105
|
+
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
|