rclone-api 1.0.88__py2.py3-none-any.whl → 1.0.90__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rclone_api/__init__.py +5 -1
- rclone_api/cmd/copy_large_s3.py +99 -0
- rclone_api/config.py +80 -1
- rclone_api/group_files.py +4 -1
- rclone_api/rclone.py +238 -49
- rclone_api/s3/api.py +73 -0
- rclone_api/s3/basic_ops.py +61 -0
- rclone_api/s3/chunk_uploader.py +538 -0
- rclone_api/s3/create.py +69 -0
- rclone_api/s3/types.py +58 -0
- rclone_api/types.py +5 -3
- rclone_api/util.py +32 -4
- {rclone_api-1.0.88.dist-info → rclone_api-1.0.90.dist-info}/METADATA +2 -3
- rclone_api-1.0.90.dist-info/RECORD +35 -0
- {rclone_api-1.0.88.dist-info → rclone_api-1.0.90.dist-info}/WHEEL +1 -1
- {rclone_api-1.0.88.dist-info → rclone_api-1.0.90.dist-info}/entry_points.txt +1 -0
- rclone_api-1.0.88.dist-info/RECORD +0 -29
- {rclone_api-1.0.88.dist-info → rclone_api-1.0.90.dist-info}/LICENSE +0 -0
- {rclone_api-1.0.88.dist-info → rclone_api-1.0.90.dist-info}/top_level.txt +0 -0
rclone_api/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .completed_process import CompletedProcess
|
|
2
|
-
from .config import Config
|
|
2
|
+
from .config import Config, Parsed, Section
|
|
3
3
|
from .diff import DiffItem, DiffOption, DiffType
|
|
4
4
|
from .dir import Dir
|
|
5
5
|
from .dir_listing import DirListing
|
|
@@ -9,6 +9,7 @@ from .process import Process
|
|
|
9
9
|
from .rclone import Rclone, rclone_verbose
|
|
10
10
|
from .remote import Remote
|
|
11
11
|
from .rpath import RPath
|
|
12
|
+
from .s3.types import MultiUploadResult
|
|
12
13
|
from .types import ListingOption, Order, SizeResult
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
@@ -30,4 +31,7 @@ __all__ = [
|
|
|
30
31
|
"Order",
|
|
31
32
|
"ListingOption",
|
|
32
33
|
"SizeResult",
|
|
34
|
+
"Parsed",
|
|
35
|
+
"Section",
|
|
36
|
+
"MultiUploadResult",
|
|
33
37
|
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from rclone_api import MultiUploadResult, Rclone
|
|
6
|
+
|
|
7
|
+
_1MB = 1024 * 1024
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Args:
|
|
12
|
+
config_path: Path
|
|
13
|
+
src: str
|
|
14
|
+
dst: str
|
|
15
|
+
chunk_size_mb: int
|
|
16
|
+
read_concurrent_chunks: int
|
|
17
|
+
retries: int
|
|
18
|
+
save_state_json: Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def list_files(rclone: Rclone, path: str):
|
|
22
|
+
"""List files in a remote path."""
|
|
23
|
+
for dirlisting in rclone.walk(path):
|
|
24
|
+
for file in dirlisting.files:
|
|
25
|
+
print(file.path)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_args() -> Args:
|
|
29
|
+
parser = argparse.ArgumentParser(description="List files in a remote path.")
|
|
30
|
+
parser.add_argument("src", help="File to copy")
|
|
31
|
+
parser.add_argument("dst", help="Destination file")
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--config", help="Path to rclone config file", type=Path, required=True
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--chunk-size-mb", help="Chunk size in MB", type=int, default=256
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--read-concurrent-chunks",
|
|
40
|
+
help="Maximum number of chunks to read",
|
|
41
|
+
type=int,
|
|
42
|
+
default=4,
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument("--retries", help="Number of retries", type=int, default=3)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--resumable-json",
|
|
47
|
+
help="Path to resumable JSON file",
|
|
48
|
+
type=Path,
|
|
49
|
+
default="resume.json",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
args = parser.parse_args()
|
|
53
|
+
out = Args(
|
|
54
|
+
config_path=Path(args.config),
|
|
55
|
+
src=args.src,
|
|
56
|
+
dst=args.dst,
|
|
57
|
+
chunk_size_mb=args.chunk_size_mb,
|
|
58
|
+
read_concurrent_chunks=args.read_concurrent_chunks,
|
|
59
|
+
retries=args.retries,
|
|
60
|
+
save_state_json=args.resumable_json,
|
|
61
|
+
)
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main() -> int:
|
|
66
|
+
"""Main entry point."""
|
|
67
|
+
args = _parse_args()
|
|
68
|
+
rclone = Rclone(rclone_conf=args.config_path)
|
|
69
|
+
rslt: MultiUploadResult = rclone.copy_file_resumable_s3(
|
|
70
|
+
src=args.src,
|
|
71
|
+
dst=args.dst,
|
|
72
|
+
chunk_size=args.chunk_size_mb * _1MB,
|
|
73
|
+
concurrent_chunks=args.read_concurrent_chunks,
|
|
74
|
+
retries=args.retries,
|
|
75
|
+
save_state_json=args.save_state_json,
|
|
76
|
+
)
|
|
77
|
+
print(rslt)
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
import os
|
|
83
|
+
import sys
|
|
84
|
+
|
|
85
|
+
here = Path(__file__).parent
|
|
86
|
+
project_root = here.parent.parent.parent
|
|
87
|
+
print(f"project_root: {project_root}")
|
|
88
|
+
os.chdir(str(project_root))
|
|
89
|
+
cwd = Path(__file__).parent
|
|
90
|
+
print(f"cwd: {cwd}")
|
|
91
|
+
sys.argv.append("--config")
|
|
92
|
+
sys.argv.append("rclone.conf")
|
|
93
|
+
sys.argv.append(
|
|
94
|
+
"45061:aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst.torrent"
|
|
95
|
+
)
|
|
96
|
+
sys.argv.append(
|
|
97
|
+
"dst:TorrentBooks/aa_misc_data/aa_misc_data/world_lending_library_2024_11.tar.zst.torrent"
|
|
98
|
+
)
|
|
99
|
+
main()
|
rclone_api/config.py
CHANGED
|
@@ -1,4 +1,48 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Section:
|
|
7
|
+
name: str
|
|
8
|
+
data: Dict[str, str] = field(default_factory=dict)
|
|
9
|
+
|
|
10
|
+
def add(self, key: str, value: str) -> None:
|
|
11
|
+
self.data[key] = value
|
|
12
|
+
|
|
13
|
+
def type(self) -> str:
|
|
14
|
+
return self.data["type"]
|
|
15
|
+
|
|
16
|
+
def provider(self) -> str | None:
|
|
17
|
+
return self.data.get("provider")
|
|
18
|
+
|
|
19
|
+
def access_key_id(self) -> str:
|
|
20
|
+
if "access_key_id" in self.data:
|
|
21
|
+
return self.data["access_key_id"]
|
|
22
|
+
elif "account" in self.data:
|
|
23
|
+
return self.data["account"]
|
|
24
|
+
raise KeyError("No access key found")
|
|
25
|
+
|
|
26
|
+
def secret_access_key(self) -> str:
|
|
27
|
+
# return self.data["secret_access_key"]
|
|
28
|
+
if "secret_access_key" in self.data:
|
|
29
|
+
return self.data["secret_access_key"]
|
|
30
|
+
elif "key" in self.data:
|
|
31
|
+
return self.data["key"]
|
|
32
|
+
raise KeyError("No secret access key found")
|
|
33
|
+
|
|
34
|
+
def endpoint(self) -> str | None:
|
|
35
|
+
return self.data.get("endpoint")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Parsed:
|
|
40
|
+
# sections: List[ParsedSection]
|
|
41
|
+
sections: dict[str, Section]
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def parse(content: str) -> "Parsed":
|
|
45
|
+
return parse_rclone_config(content)
|
|
2
46
|
|
|
3
47
|
|
|
4
48
|
@dataclass
|
|
@@ -6,3 +50,38 @@ class Config:
|
|
|
6
50
|
"""Rclone configuration dataclass."""
|
|
7
51
|
|
|
8
52
|
text: str
|
|
53
|
+
|
|
54
|
+
def parse(self) -> Parsed:
|
|
55
|
+
return Parsed.parse(self.text)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parse_rclone_config(content: str) -> Parsed:
|
|
59
|
+
"""
|
|
60
|
+
Parses an rclone configuration file and returns a list of RcloneConfigSection objects.
|
|
61
|
+
|
|
62
|
+
Each section in the file starts with a line like [section_name]
|
|
63
|
+
followed by key=value pairs.
|
|
64
|
+
"""
|
|
65
|
+
sections: List[Section] = []
|
|
66
|
+
current_section: Section | None = None
|
|
67
|
+
|
|
68
|
+
lines = content.splitlines()
|
|
69
|
+
for line in lines:
|
|
70
|
+
line = line.strip()
|
|
71
|
+
# Skip empty lines and comments (assumed to start with '#' or ';')
|
|
72
|
+
if not line or line.startswith(("#", ";")):
|
|
73
|
+
continue
|
|
74
|
+
# New section header detected
|
|
75
|
+
if line.startswith("[") and line.endswith("]"):
|
|
76
|
+
section_name = line[1:-1].strip()
|
|
77
|
+
current_section = Section(name=section_name)
|
|
78
|
+
sections.append(current_section)
|
|
79
|
+
elif "=" in line and current_section is not None:
|
|
80
|
+
# Parse key and value, splitting only on the first '=' found
|
|
81
|
+
key, value = line.split("=", 1)
|
|
82
|
+
current_section.add(key.strip(), value.strip())
|
|
83
|
+
|
|
84
|
+
data: dict[str, Section] = {}
|
|
85
|
+
for section in sections:
|
|
86
|
+
data[section.name] = section
|
|
87
|
+
return Parsed(sections=data)
|
rclone_api/group_files.py
CHANGED
|
@@ -68,7 +68,10 @@ class TreeNode:
|
|
|
68
68
|
paths_reversed: list[str] = [self.name]
|
|
69
69
|
node: TreeNode | None = self
|
|
70
70
|
assert node is not None
|
|
71
|
-
while
|
|
71
|
+
while True:
|
|
72
|
+
node = node.parent
|
|
73
|
+
if node is None:
|
|
74
|
+
break
|
|
72
75
|
paths_reversed.append(node.name)
|
|
73
76
|
return "/".join(reversed(paths_reversed))
|
|
74
77
|
|
rclone_api/rclone.py
CHANGED
|
@@ -6,8 +6,10 @@ import os
|
|
|
6
6
|
import random
|
|
7
7
|
import subprocess
|
|
8
8
|
import time
|
|
9
|
+
import traceback
|
|
9
10
|
import warnings
|
|
10
11
|
from concurrent.futures import Future, ThreadPoolExecutor
|
|
12
|
+
from contextlib import contextmanager
|
|
11
13
|
from fnmatch import fnmatch
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
from tempfile import TemporaryDirectory
|
|
@@ -15,7 +17,7 @@ from typing import Generator
|
|
|
15
17
|
|
|
16
18
|
from rclone_api import Dir
|
|
17
19
|
from rclone_api.completed_process import CompletedProcess
|
|
18
|
-
from rclone_api.config import Config
|
|
20
|
+
from rclone_api.config import Config, Parsed, Section
|
|
19
21
|
from rclone_api.convert import convert_to_filestr_list, convert_to_str
|
|
20
22
|
from rclone_api.deprecated import deprecated
|
|
21
23
|
from rclone_api.diff import DiffItem, DiffOption, diff_stream_from_running_process
|
|
@@ -26,12 +28,13 @@ from rclone_api.group_files import group_files
|
|
|
26
28
|
from rclone_api.process import Process
|
|
27
29
|
from rclone_api.remote import Remote
|
|
28
30
|
from rclone_api.rpath import RPath
|
|
29
|
-
from rclone_api.types import (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
from rclone_api.s3.types import (
|
|
32
|
+
MultiUploadResult,
|
|
33
|
+
S3MutliPartUploadConfig,
|
|
34
|
+
S3Provider,
|
|
35
|
+
S3UploadTarget,
|
|
34
36
|
)
|
|
37
|
+
from rclone_api.types import ListingOption, ModTimeStrategy, Order, SizeResult
|
|
35
38
|
from rclone_api.util import (
|
|
36
39
|
get_check,
|
|
37
40
|
get_rclone_exe,
|
|
@@ -41,6 +44,8 @@ from rclone_api.util import (
|
|
|
41
44
|
)
|
|
42
45
|
from rclone_api.walk import walk
|
|
43
46
|
|
|
47
|
+
_IS_WINDOWS = os.name == "nt"
|
|
48
|
+
|
|
44
49
|
|
|
45
50
|
def rclone_verbose(verbose: bool | None) -> bool:
|
|
46
51
|
if verbose is not None:
|
|
@@ -48,6 +53,14 @@ def rclone_verbose(verbose: bool | None) -> bool:
|
|
|
48
53
|
return bool(int(os.getenv("RCLONE_API_VERBOSE", "0")))
|
|
49
54
|
|
|
50
55
|
|
|
56
|
+
def _to_rclone_conf(config: Config | Path) -> Config:
|
|
57
|
+
if isinstance(config, Path):
|
|
58
|
+
content = config.read_text(encoding="utf-8")
|
|
59
|
+
return Config(content)
|
|
60
|
+
else:
|
|
61
|
+
return config
|
|
62
|
+
|
|
63
|
+
|
|
51
64
|
class Rclone:
|
|
52
65
|
def __init__(
|
|
53
66
|
self, rclone_conf: Path | Config, rclone_exe: Path | None = None
|
|
@@ -56,6 +69,7 @@ class Rclone:
|
|
|
56
69
|
if not rclone_conf.exists():
|
|
57
70
|
raise ValueError(f"Rclone config file not found: {rclone_conf}")
|
|
58
71
|
self._exec = RcloneExec(rclone_conf, get_rclone_exe(rclone_exe))
|
|
72
|
+
self.config: Config = _to_rclone_conf(rclone_conf)
|
|
59
73
|
|
|
60
74
|
def _run(
|
|
61
75
|
self, cmd: list[str], check: bool = False, capture: bool | None = None
|
|
@@ -399,7 +413,7 @@ class Rclone:
|
|
|
399
413
|
using_fast_list = "--fast-list" in other_args
|
|
400
414
|
if using_fast_list:
|
|
401
415
|
warnings.warn(
|
|
402
|
-
"It's not recommended to use --fast-list with copy_files as the entire repository has to be
|
|
416
|
+
"It's not recommended to use --fast-list with copy_files as this will perform poorly on large repositories since the entire repository has to be scanned."
|
|
403
417
|
)
|
|
404
418
|
|
|
405
419
|
if max_partition_workers > 1:
|
|
@@ -654,6 +668,127 @@ class Rclone:
|
|
|
654
668
|
except subprocess.CalledProcessError:
|
|
655
669
|
return False
|
|
656
670
|
|
|
671
|
+
def copy_file_resumable_s3(
|
|
672
|
+
self,
|
|
673
|
+
src: str,
|
|
674
|
+
dst: str,
|
|
675
|
+
save_state_json: Path,
|
|
676
|
+
chunk_size: int = 16
|
|
677
|
+
* 1024
|
|
678
|
+
* 1024, # This setting will scale the performance of the upload
|
|
679
|
+
concurrent_chunks: int = 4, # This setting will scale the performance of the upload
|
|
680
|
+
retries: int = 3,
|
|
681
|
+
max_chunks_before_suspension: int | None = None,
|
|
682
|
+
) -> MultiUploadResult:
|
|
683
|
+
"""For massive files that rclone can't handle in one go, this function will copy the file in chunks to an S3 store"""
|
|
684
|
+
from rclone_api.s3.api import S3Client
|
|
685
|
+
from rclone_api.s3.create import S3Credentials
|
|
686
|
+
from rclone_api.util import S3PathInfo, split_s3_path
|
|
687
|
+
|
|
688
|
+
other_args: list[str] = [
|
|
689
|
+
"--no-modtime",
|
|
690
|
+
"--vfs-read-wait",
|
|
691
|
+
"1s",
|
|
692
|
+
"--vfs-disk-space-total-size",
|
|
693
|
+
str(2 * chunk_size * concurrent_chunks), # purge quickly.
|
|
694
|
+
"--vfs-read-chunk-size",
|
|
695
|
+
str(chunk_size),
|
|
696
|
+
"--vfs-read-chunk-size-limit",
|
|
697
|
+
str(chunk_size * concurrent_chunks),
|
|
698
|
+
"--vfs-read-chunk-streams",
|
|
699
|
+
str(concurrent_chunks),
|
|
700
|
+
"--vfs-fast-fingerprint",
|
|
701
|
+
]
|
|
702
|
+
mount_path = Path("rclone_api_upload_mount")
|
|
703
|
+
src_path = Path(src)
|
|
704
|
+
name = src_path.name
|
|
705
|
+
|
|
706
|
+
parent_path = str(src_path.parent.as_posix())
|
|
707
|
+
with self.scoped_mount(
|
|
708
|
+
parent_path,
|
|
709
|
+
mount_path,
|
|
710
|
+
use_links=True,
|
|
711
|
+
vfs_cache_mode="minimal",
|
|
712
|
+
other_args=other_args,
|
|
713
|
+
):
|
|
714
|
+
# raise NotImplementedError("Not implemented yet")
|
|
715
|
+
|
|
716
|
+
path_info: S3PathInfo = split_s3_path(dst)
|
|
717
|
+
remote = path_info.remote
|
|
718
|
+
bucket_name = path_info.bucket
|
|
719
|
+
s3_key = path_info.key
|
|
720
|
+
parsed: Parsed = self.config.parse()
|
|
721
|
+
sections: dict[str, Section] = parsed.sections
|
|
722
|
+
if remote not in sections:
|
|
723
|
+
raise ValueError(
|
|
724
|
+
f"Remote {remote} not found in rclone config, remotes are: {sections.keys()}"
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
section: Section = sections[remote]
|
|
728
|
+
dst_type = section.type()
|
|
729
|
+
if dst_type != "s3" and dst_type != "b2":
|
|
730
|
+
raise ValueError(
|
|
731
|
+
f"Remote {remote} is not an S3 remote, it is of type {dst_type}"
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
def get_provider_str(section=section) -> str | None:
|
|
735
|
+
type: str = section.type()
|
|
736
|
+
provider: str | None = section.provider()
|
|
737
|
+
if provider is not None:
|
|
738
|
+
return provider
|
|
739
|
+
if type == "b2":
|
|
740
|
+
return S3Provider.BACKBLAZE.value
|
|
741
|
+
if type != "s3":
|
|
742
|
+
raise ValueError(f"Remote {remote} is not an S3 remote")
|
|
743
|
+
return S3Provider.S3.value
|
|
744
|
+
|
|
745
|
+
provider: str = get_provider_str() or S3Provider.S3.value
|
|
746
|
+
provider_enum = S3Provider.from_str(provider)
|
|
747
|
+
|
|
748
|
+
s3_creds: S3Credentials = S3Credentials(
|
|
749
|
+
provider=provider_enum,
|
|
750
|
+
access_key_id=section.access_key_id(),
|
|
751
|
+
secret_access_key=section.secret_access_key(),
|
|
752
|
+
endpoint_url=section.endpoint(),
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
client = S3Client(s3_creds)
|
|
756
|
+
config: S3MutliPartUploadConfig = S3MutliPartUploadConfig(
|
|
757
|
+
chunk_size=chunk_size,
|
|
758
|
+
retries=retries,
|
|
759
|
+
resume_path_json=save_state_json,
|
|
760
|
+
max_chunks_before_suspension=max_chunks_before_suspension,
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
src_file = mount_path / name
|
|
764
|
+
|
|
765
|
+
print(f"Uploading {name} to {s3_key} in bucket {bucket_name}")
|
|
766
|
+
print(f"Source: {src_path}")
|
|
767
|
+
print(f"bucket_name: {bucket_name}")
|
|
768
|
+
print(f"upload_config: {config}")
|
|
769
|
+
|
|
770
|
+
upload_target: S3UploadTarget
|
|
771
|
+
upload_config: S3MutliPartUploadConfig
|
|
772
|
+
|
|
773
|
+
upload_target = S3UploadTarget(
|
|
774
|
+
bucket_name=bucket_name,
|
|
775
|
+
src_file=src_file,
|
|
776
|
+
s3_key=s3_key,
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
upload_config = S3MutliPartUploadConfig(
|
|
780
|
+
chunk_size=chunk_size,
|
|
781
|
+
retries=retries,
|
|
782
|
+
resume_path_json=save_state_json,
|
|
783
|
+
max_chunks_before_suspension=max_chunks_before_suspension,
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
out: MultiUploadResult = client.upload_file_multipart(
|
|
787
|
+
upload_target=upload_target,
|
|
788
|
+
upload_config=upload_config
|
|
789
|
+
)
|
|
790
|
+
return out
|
|
791
|
+
|
|
657
792
|
def copy_dir(
|
|
658
793
|
self, src: str | Dir, dst: str | Dir, args: list[str] | None = None
|
|
659
794
|
) -> CompletedProcess:
|
|
@@ -682,10 +817,10 @@ class Rclone:
|
|
|
682
817
|
self,
|
|
683
818
|
src: Remote | Dir | str,
|
|
684
819
|
outdir: Path,
|
|
685
|
-
allow_writes=False,
|
|
686
|
-
use_links=
|
|
687
|
-
vfs_cache_mode=
|
|
688
|
-
|
|
820
|
+
allow_writes: bool | None = False,
|
|
821
|
+
use_links: bool | None = None,
|
|
822
|
+
vfs_cache_mode: str | None = None,
|
|
823
|
+
other_args: list[str] | None = None,
|
|
689
824
|
) -> Process:
|
|
690
825
|
"""Mount a remote or directory to a local path.
|
|
691
826
|
|
|
@@ -699,6 +834,9 @@ class Rclone:
|
|
|
699
834
|
Raises:
|
|
700
835
|
subprocess.CalledProcessError: If the mount operation fails
|
|
701
836
|
"""
|
|
837
|
+
allow_writes = allow_writes or False
|
|
838
|
+
use_links = use_links or True
|
|
839
|
+
vfs_cache_mode = vfs_cache_mode or "full"
|
|
702
840
|
if outdir.exists():
|
|
703
841
|
is_empty = not list(outdir.iterdir())
|
|
704
842
|
if not is_empty:
|
|
@@ -706,12 +844,13 @@ class Rclone:
|
|
|
706
844
|
f"Mount directory already exists and is not empty: {outdir}"
|
|
707
845
|
)
|
|
708
846
|
outdir.rmdir()
|
|
709
|
-
|
|
847
|
+
|
|
848
|
+
if _IS_WINDOWS:
|
|
849
|
+
# Windows -> Must create parent directories only if they don't exist
|
|
710
850
|
outdir.parent.mkdir(parents=True, exist_ok=True)
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
)
|
|
851
|
+
else:
|
|
852
|
+
# Linux -> Must create parent directories and the directory itself
|
|
853
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
715
854
|
src_str = convert_to_str(src)
|
|
716
855
|
cmd_list: list[str] = ["mount", src_str, str(outdir)]
|
|
717
856
|
if not allow_writes:
|
|
@@ -721,19 +860,50 @@ class Rclone:
|
|
|
721
860
|
if vfs_cache_mode:
|
|
722
861
|
cmd_list.append("--vfs-cache-mode")
|
|
723
862
|
cmd_list.append(vfs_cache_mode)
|
|
724
|
-
if
|
|
725
|
-
cmd_list +=
|
|
863
|
+
if other_args:
|
|
864
|
+
cmd_list += other_args
|
|
726
865
|
proc = self._launch_process(cmd_list)
|
|
727
866
|
wait_for_mount(outdir, proc)
|
|
728
867
|
return proc
|
|
729
868
|
|
|
869
|
+
@contextmanager
|
|
870
|
+
def scoped_mount(
|
|
871
|
+
self,
|
|
872
|
+
src: Remote | Dir | str,
|
|
873
|
+
outdir: Path,
|
|
874
|
+
allow_writes: bool | None = None,
|
|
875
|
+
use_links: bool | None = None,
|
|
876
|
+
vfs_cache_mode: str | None = None,
|
|
877
|
+
other_args: list[str] | None = None,
|
|
878
|
+
) -> Generator[Process, None, None]:
|
|
879
|
+
"""Like mount, but can be used in a context manager."""
|
|
880
|
+
proc = self.mount(
|
|
881
|
+
src,
|
|
882
|
+
outdir,
|
|
883
|
+
allow_writes=allow_writes,
|
|
884
|
+
use_links=use_links,
|
|
885
|
+
vfs_cache_mode=vfs_cache_mode,
|
|
886
|
+
other_args=other_args,
|
|
887
|
+
)
|
|
888
|
+
try:
|
|
889
|
+
yield proc
|
|
890
|
+
except Exception as e:
|
|
891
|
+
stack_trace = traceback.format_exc()
|
|
892
|
+
warnings.warn(f"Error in scoped_mount: {e}\n\nStack Trace:\n{stack_trace}")
|
|
893
|
+
raise
|
|
894
|
+
finally:
|
|
895
|
+
if proc.poll() is None:
|
|
896
|
+
proc.terminate()
|
|
897
|
+
proc.wait()
|
|
898
|
+
|
|
899
|
+
@deprecated("mount")
|
|
730
900
|
def mount_webdav(
|
|
731
901
|
self,
|
|
732
902
|
url: str,
|
|
733
903
|
outdir: Path,
|
|
734
|
-
vfs_cache_mode=
|
|
904
|
+
vfs_cache_mode: str | None = None,
|
|
735
905
|
vfs_disk_space_total_size: str | None = "10G",
|
|
736
|
-
|
|
906
|
+
other_args: list[str] | None = None,
|
|
737
907
|
) -> Process:
|
|
738
908
|
"""Mount a remote or directory to a local path.
|
|
739
909
|
|
|
@@ -747,6 +917,20 @@ class Rclone:
|
|
|
747
917
|
Raises:
|
|
748
918
|
subprocess.CalledProcessError: If the mount operation fails
|
|
749
919
|
"""
|
|
920
|
+
other_args = other_args or []
|
|
921
|
+
if vfs_cache_mode is None:
|
|
922
|
+
if "--vfs-cache-mode" in other_args:
|
|
923
|
+
pass
|
|
924
|
+
else:
|
|
925
|
+
vfs_cache_mode = "full"
|
|
926
|
+
elif "--vfs-cache-mode" in other_args:
|
|
927
|
+
warnings.warn(
|
|
928
|
+
f"vfs_cache_mode is set to {vfs_cache_mode} but --vfs-cache-mode is already in other_args"
|
|
929
|
+
)
|
|
930
|
+
idx = other_args.index("--vfs-cache-mode")
|
|
931
|
+
other_args.pop(idx)
|
|
932
|
+
other_args.pop(idx) # also the next value which will be the cache mode.
|
|
933
|
+
|
|
750
934
|
if outdir.exists():
|
|
751
935
|
is_empty = not list(outdir.iterdir())
|
|
752
936
|
if not is_empty:
|
|
@@ -757,10 +941,11 @@ class Rclone:
|
|
|
757
941
|
|
|
758
942
|
src_str = url
|
|
759
943
|
cmd_list: list[str] = ["mount", src_str, str(outdir)]
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
944
|
+
if vfs_cache_mode:
|
|
945
|
+
cmd_list.append("--vfs-cache-mode")
|
|
946
|
+
cmd_list.append(vfs_cache_mode)
|
|
947
|
+
if other_args:
|
|
948
|
+
cmd_list += other_args
|
|
764
949
|
if vfs_disk_space_total_size is not None:
|
|
765
950
|
cmd_list.append("--vfs-cache-max-size")
|
|
766
951
|
cmd_list.append(vfs_disk_space_total_size)
|
|
@@ -768,6 +953,7 @@ class Rclone:
|
|
|
768
953
|
wait_for_mount(outdir, proc)
|
|
769
954
|
return proc
|
|
770
955
|
|
|
956
|
+
# Settings optimized for s3.
|
|
771
957
|
def mount_s3(
|
|
772
958
|
self,
|
|
773
959
|
url: str,
|
|
@@ -789,7 +975,7 @@ class Rclone:
|
|
|
789
975
|
vfs_fast_fingerprint: bool = True,
|
|
790
976
|
# vfs-refresh
|
|
791
977
|
vfs_refresh: bool = True,
|
|
792
|
-
|
|
978
|
+
other_args: list[str] | None = None,
|
|
793
979
|
) -> Process:
|
|
794
980
|
"""Mount a remote or directory to a local path.
|
|
795
981
|
|
|
@@ -797,44 +983,44 @@ class Rclone:
|
|
|
797
983
|
src: Remote or directory to mount
|
|
798
984
|
outdir: Local path to mount to
|
|
799
985
|
"""
|
|
800
|
-
|
|
986
|
+
other_args = other_args or []
|
|
801
987
|
if modtime_strategy is not None:
|
|
802
|
-
|
|
988
|
+
other_args.append(f"--{modtime_strategy.value}")
|
|
803
989
|
if (vfs_cache_mode == "full" or vfs_cache_mode == "writes") and (
|
|
804
|
-
transfers is not None and "--transfers" not in
|
|
990
|
+
transfers is not None and "--transfers" not in other_args
|
|
805
991
|
):
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
if dir_cache_time is not None and "--dir-cache-time" not in
|
|
809
|
-
|
|
810
|
-
|
|
992
|
+
other_args.append("--transfers")
|
|
993
|
+
other_args.append(str(transfers))
|
|
994
|
+
if dir_cache_time is not None and "--dir-cache-time" not in other_args:
|
|
995
|
+
other_args.append("--dir-cache-time")
|
|
996
|
+
other_args.append(dir_cache_time)
|
|
811
997
|
if (
|
|
812
998
|
vfs_disk_space_total_size is not None
|
|
813
|
-
and "--vfs-cache-max-size" not in
|
|
999
|
+
and "--vfs-cache-max-size" not in other_args
|
|
814
1000
|
):
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
if vfs_refresh and "--vfs-refresh" not in
|
|
818
|
-
|
|
819
|
-
if attribute_timeout is not None and "--attr-timeout" not in
|
|
820
|
-
|
|
821
|
-
|
|
1001
|
+
other_args.append("--vfs-cache-max-size")
|
|
1002
|
+
other_args.append(vfs_disk_space_total_size)
|
|
1003
|
+
if vfs_refresh and "--vfs-refresh" not in other_args:
|
|
1004
|
+
other_args.append("--vfs-refresh")
|
|
1005
|
+
if attribute_timeout is not None and "--attr-timeout" not in other_args:
|
|
1006
|
+
other_args.append("--attr-timeout")
|
|
1007
|
+
other_args.append(attribute_timeout)
|
|
822
1008
|
if vfs_read_chunk_streams:
|
|
823
|
-
|
|
824
|
-
|
|
1009
|
+
other_args.append("--vfs-read-chunk-streams")
|
|
1010
|
+
other_args.append(str(vfs_read_chunk_streams))
|
|
825
1011
|
if vfs_read_chunk_size:
|
|
826
|
-
|
|
827
|
-
|
|
1012
|
+
other_args.append("--vfs-read-chunk-size")
|
|
1013
|
+
other_args.append(vfs_read_chunk_size)
|
|
828
1014
|
if vfs_fast_fingerprint:
|
|
829
|
-
|
|
1015
|
+
other_args.append("--vfs-fast-fingerprint")
|
|
830
1016
|
|
|
831
|
-
|
|
1017
|
+
other_args = other_args if other_args else None
|
|
832
1018
|
return self.mount(
|
|
833
1019
|
url,
|
|
834
1020
|
outdir,
|
|
835
1021
|
allow_writes=allow_writes,
|
|
836
1022
|
vfs_cache_mode=vfs_cache_mode,
|
|
837
|
-
|
|
1023
|
+
other_args=other_args,
|
|
838
1024
|
)
|
|
839
1025
|
|
|
840
1026
|
def serve_webdav(
|
|
@@ -844,6 +1030,7 @@ class Rclone:
|
|
|
844
1030
|
password: str,
|
|
845
1031
|
addr: str = "localhost:2049",
|
|
846
1032
|
allow_other: bool = False,
|
|
1033
|
+
other_args: list[str] | None = None,
|
|
847
1034
|
) -> Process:
|
|
848
1035
|
"""Serve a remote or directory via NFS.
|
|
849
1036
|
|
|
@@ -863,6 +1050,8 @@ class Rclone:
|
|
|
863
1050
|
cmd_list.extend(["--user", user, "--pass", password])
|
|
864
1051
|
if allow_other:
|
|
865
1052
|
cmd_list.append("--allow-other")
|
|
1053
|
+
if other_args:
|
|
1054
|
+
cmd_list += other_args
|
|
866
1055
|
proc = self._launch_process(cmd_list)
|
|
867
1056
|
time.sleep(2) # give it a moment to start
|
|
868
1057
|
if proc.poll() is not None:
|
|
@@ -883,7 +1072,7 @@ class Rclone:
|
|
|
883
1072
|
check = get_check(check)
|
|
884
1073
|
if fast_list or (other_args and "--fast-list" in other_args):
|
|
885
1074
|
warnings.warn(
|
|
886
|
-
"It's not recommended to use --fast-list with size_files as the entire repository has to be
|
|
1075
|
+
"It's not recommended to use --fast-list with size_files as this will perform poorly on large repositories since the entire repository has to be scanned."
|
|
887
1076
|
)
|
|
888
1077
|
files = list(files)
|
|
889
1078
|
all_files: list[File] = []
|