rclone-api 1.0.87__tar.gz → 1.0.89__tar.gz
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-1.0.87 → rclone_api-1.0.89}/.gitignore +5 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/PKG-INFO +2 -3
- {rclone_api-1.0.87 → rclone_api-1.0.89}/pyproject.toml +7 -1
- {rclone_api-1.0.87 → rclone_api-1.0.89}/setup.py +0 -1
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/__init__.py +5 -1
- rclone_api-1.0.89/src/rclone_api/config.py +75 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/group_files.py +4 -1
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/rclone.py +227 -49
- rclone_api-1.0.89/src/rclone_api/s3/api.py +72 -0
- rclone_api-1.0.89/src/rclone_api/s3/basic_ops.py +61 -0
- rclone_api-1.0.89/src/rclone_api/s3/chunk_uploader.py +538 -0
- rclone_api-1.0.89/src/rclone_api/s3/create.py +71 -0
- rclone_api-1.0.89/src/rclone_api/s3/types.py +55 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/types.py +5 -3
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/util.py +30 -4
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api.egg-info/PKG-INFO +2 -3
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api.egg-info/SOURCES.txt +10 -2
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api.egg-info/requires.txt +1 -0
- rclone_api-1.0.89/tests/archive/test_paramiko.py.disabled +326 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_diff.py +2 -2
- rclone_api-1.0.89/tests/test_mounted_ranged_download.py +151 -0
- rclone_api-1.0.89/tests/test_rclone_config.py +70 -0
- rclone_api-1.0.89/tests/test_s3.py +113 -0
- rclone_api-1.0.87/src/rclone_api/config.py +0 -8
- rclone_api-1.0.87/tests/test_serve_webdav.py +0 -108
- {rclone_api-1.0.87 → rclone_api-1.0.89}/.aiderignore +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/.github/workflows/lint.yml +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/.github/workflows/push_macos.yml +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/.github/workflows/push_ubuntu.yml +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/.github/workflows/push_win.yml +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/.pylintrc +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/.vscode/launch.json +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/.vscode/settings.json +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/.vscode/tasks.json +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/LICENSE +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/MANIFEST.in +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/README.md +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/clean +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/install +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/lint +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/requirements.testing.txt +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/setup.cfg +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/cmd/list_files.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/completed_process.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/convert.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/deprecated.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/diff.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/dir.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/dir_listing.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/exec.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/file.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/filelist.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/process.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/rpath.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/scan_missing_folders.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api/walk.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api.egg-info/entry_points.txt +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/test +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_cmd_list_files.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_copy.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_copy_files.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_group_files.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_is_synced.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_ls.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_mount.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_mount_s3.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_mount_webdav.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_obscure.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_remote_control.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_remotes.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_scan_missing_folders.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_size_files.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tests/test_walk.py +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/tox.ini +0 -0
- {rclone_api-1.0.87 → rclone_api-1.0.89}/upload_package.sh +0 -0
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: rclone_api
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.89
|
|
4
4
|
Summary: rclone api in python
|
|
5
5
|
Home-page: https://github.com/zackees/rclone-api
|
|
6
|
-
Maintainer: Zachary Vorhies
|
|
7
6
|
License: BSD 3-Clause License
|
|
8
7
|
Keywords: template-python-cmd
|
|
9
8
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -12,8 +11,8 @@ Description-Content-Type: text/markdown
|
|
|
12
11
|
License-File: LICENSE
|
|
13
12
|
Requires-Dist: pyright>=1.1.393
|
|
14
13
|
Requires-Dist: python-dotenv>=1.0.0
|
|
14
|
+
Requires-Dist: boto3<=1.35.99,>=1.20.1
|
|
15
15
|
Dynamic: home-page
|
|
16
|
-
Dynamic: maintainer
|
|
17
16
|
|
|
18
17
|
# rclone-api
|
|
19
18
|
|
|
@@ -13,9 +13,15 @@ classifiers = ["Programming Language :: Python :: 3"]
|
|
|
13
13
|
dependencies = [
|
|
14
14
|
"pyright>=1.1.393",
|
|
15
15
|
"python-dotenv>=1.0.0",
|
|
16
|
+
|
|
17
|
+
# BOTO3 Library needs to be pinned to a specific version
|
|
18
|
+
# BackBlaze S3 fails with checksum header which it doesn't support after 1.35.99
|
|
19
|
+
# The 1.20.1 was the earliest one I checked that worked and is not the true lower bound.
|
|
20
|
+
"boto3>=1.20.1,<=1.35.99",
|
|
16
21
|
]
|
|
22
|
+
|
|
17
23
|
# Change this with the version number bump.
|
|
18
|
-
version = "1.0.
|
|
24
|
+
version = "1.0.89"
|
|
19
25
|
|
|
20
26
|
[tool.setuptools]
|
|
21
27
|
package-dir = {"" = "src"}
|
|
@@ -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,75 @@
|
|
|
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 provider(self) -> str:
|
|
14
|
+
return self.data["provider"]
|
|
15
|
+
|
|
16
|
+
def access_key_id(self) -> str:
|
|
17
|
+
return self.data["access_key_id"]
|
|
18
|
+
|
|
19
|
+
def secret_access_key(self) -> str:
|
|
20
|
+
return self.data["secret_access_key"]
|
|
21
|
+
|
|
22
|
+
def endpoint(self) -> str | None:
|
|
23
|
+
return self.data.get("endpoint")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Parsed:
|
|
28
|
+
# sections: List[ParsedSection]
|
|
29
|
+
sections: dict[str, Section]
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def parse(content: str) -> "Parsed":
|
|
33
|
+
return parse_rclone_config(content)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Config:
|
|
38
|
+
"""Rclone configuration dataclass."""
|
|
39
|
+
|
|
40
|
+
text: str
|
|
41
|
+
|
|
42
|
+
def parse(self) -> Parsed:
|
|
43
|
+
return Parsed.parse(self.text)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_rclone_config(content: str) -> Parsed:
|
|
47
|
+
"""
|
|
48
|
+
Parses an rclone configuration file and returns a list of RcloneConfigSection objects.
|
|
49
|
+
|
|
50
|
+
Each section in the file starts with a line like [section_name]
|
|
51
|
+
followed by key=value pairs.
|
|
52
|
+
"""
|
|
53
|
+
sections: List[Section] = []
|
|
54
|
+
current_section: Section | None = None
|
|
55
|
+
|
|
56
|
+
lines = content.splitlines()
|
|
57
|
+
for line in lines:
|
|
58
|
+
line = line.strip()
|
|
59
|
+
# Skip empty lines and comments (assumed to start with '#' or ';')
|
|
60
|
+
if not line or line.startswith(("#", ";")):
|
|
61
|
+
continue
|
|
62
|
+
# New section header detected
|
|
63
|
+
if line.startswith("[") and line.endswith("]"):
|
|
64
|
+
section_name = line[1:-1].strip()
|
|
65
|
+
current_section = Section(name=section_name)
|
|
66
|
+
sections.append(current_section)
|
|
67
|
+
elif "=" in line and current_section is not None:
|
|
68
|
+
# Parse key and value, splitting only on the first '=' found
|
|
69
|
+
key, value = line.split("=", 1)
|
|
70
|
+
current_section.add(key.strip(), value.strip())
|
|
71
|
+
|
|
72
|
+
data: dict[str, Section] = {}
|
|
73
|
+
for section in sections:
|
|
74
|
+
data[section.name] = section
|
|
75
|
+
return Parsed(sections=data)
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -331,6 +345,7 @@ class Rclone:
|
|
|
331
345
|
src: File | str,
|
|
332
346
|
dst: File | str,
|
|
333
347
|
check: bool | None = None,
|
|
348
|
+
verbose: bool | None = None,
|
|
334
349
|
other_args: list[str] | None = None,
|
|
335
350
|
) -> None:
|
|
336
351
|
"""Copy multiple files from source to destination.
|
|
@@ -341,6 +356,7 @@ class Rclone:
|
|
|
341
356
|
payload: Dictionary of source and destination file paths
|
|
342
357
|
"""
|
|
343
358
|
check = get_check(check)
|
|
359
|
+
verbose = get_verbose(verbose)
|
|
344
360
|
src = src if isinstance(src, str) else str(src.path)
|
|
345
361
|
dst = dst if isinstance(dst, str) else str(dst.path)
|
|
346
362
|
cmd_list: list[str] = ["copyto", src, dst]
|
|
@@ -397,7 +413,7 @@ class Rclone:
|
|
|
397
413
|
using_fast_list = "--fast-list" in other_args
|
|
398
414
|
if using_fast_list:
|
|
399
415
|
warnings.warn(
|
|
400
|
-
"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."
|
|
401
417
|
)
|
|
402
418
|
|
|
403
419
|
if max_partition_workers > 1:
|
|
@@ -652,6 +668,114 @@ class Rclone:
|
|
|
652
668
|
except subprocess.CalledProcessError:
|
|
653
669
|
return False
|
|
654
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
|
+
other_args: list[str] = [
|
|
685
|
+
"--no-modtime",
|
|
686
|
+
"--vfs-read-wait",
|
|
687
|
+
"1s",
|
|
688
|
+
"--vfs-disk-space-total-size",
|
|
689
|
+
str(2 * chunk_size * concurrent_chunks), # purge quickly.
|
|
690
|
+
"--vfs-read-chunk-size",
|
|
691
|
+
str(chunk_size),
|
|
692
|
+
"--vfs-read-chunk-size-limit",
|
|
693
|
+
str(chunk_size * concurrent_chunks),
|
|
694
|
+
"--vfs-read-chunk-streams",
|
|
695
|
+
str(concurrent_chunks),
|
|
696
|
+
"--vfs-fast-fingerprint",
|
|
697
|
+
]
|
|
698
|
+
mount_path = Path("rclone_api_upload_mount")
|
|
699
|
+
src_path = Path(src)
|
|
700
|
+
name = src_path.name
|
|
701
|
+
|
|
702
|
+
parent_path = str(src_path.parent.as_posix())
|
|
703
|
+
with self.scoped_mount(
|
|
704
|
+
parent_path,
|
|
705
|
+
mount_path,
|
|
706
|
+
use_links=True,
|
|
707
|
+
vfs_cache_mode="minimal",
|
|
708
|
+
other_args=other_args,
|
|
709
|
+
):
|
|
710
|
+
# raise NotImplementedError("Not implemented yet")
|
|
711
|
+
from rclone_api.s3.create import S3Credentials
|
|
712
|
+
from rclone_api.util import S3PathInfo, split_s3_path
|
|
713
|
+
|
|
714
|
+
path_info: S3PathInfo = split_s3_path(dst)
|
|
715
|
+
remote = path_info.remote
|
|
716
|
+
bucket_name = path_info.bucket
|
|
717
|
+
s3_key = path_info.key
|
|
718
|
+
parsed: Parsed = self.config.parse()
|
|
719
|
+
sections: dict[str, Section] = parsed.sections
|
|
720
|
+
if remote not in sections:
|
|
721
|
+
raise ValueError(
|
|
722
|
+
f"Remote {remote} not found in rclone config, remotes are: {sections.keys()}"
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
section: Section = sections[remote]
|
|
726
|
+
provider: str = section.provider()
|
|
727
|
+
provider_enum = S3Provider.from_str(provider)
|
|
728
|
+
|
|
729
|
+
s3_creds: S3Credentials = S3Credentials(
|
|
730
|
+
provider=provider_enum,
|
|
731
|
+
access_key_id=section.access_key_id(),
|
|
732
|
+
secret_access_key=section.secret_access_key(),
|
|
733
|
+
endpoint_url=section.endpoint(),
|
|
734
|
+
)
|
|
735
|
+
print(s3_creds)
|
|
736
|
+
# create_s3_client
|
|
737
|
+
|
|
738
|
+
print(f"Info: {section}")
|
|
739
|
+
from rclone_api.s3.api import S3Client
|
|
740
|
+
|
|
741
|
+
client = S3Client(s3_creds)
|
|
742
|
+
print(f"Client: {client}")
|
|
743
|
+
|
|
744
|
+
config: S3MutliPartUploadConfig = S3MutliPartUploadConfig(
|
|
745
|
+
chunk_size=chunk_size,
|
|
746
|
+
retries=retries,
|
|
747
|
+
resume_path_json=save_state_json,
|
|
748
|
+
max_chunks_before_suspension=max_chunks_before_suspension,
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
src_file = mount_path / name
|
|
752
|
+
|
|
753
|
+
print(f"Uploading {name} to {s3_key} in bucket {bucket_name}")
|
|
754
|
+
print(f"Source: {src_path}")
|
|
755
|
+
print(f"bucket_name: {bucket_name}")
|
|
756
|
+
print(f"upload_config: {config}")
|
|
757
|
+
|
|
758
|
+
upload_target: S3UploadTarget
|
|
759
|
+
upload_config: S3MutliPartUploadConfig
|
|
760
|
+
|
|
761
|
+
upload_target = S3UploadTarget(
|
|
762
|
+
bucket_name=bucket_name,
|
|
763
|
+
src_file=src_file,
|
|
764
|
+
s3_key=s3_key,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
upload_config = S3MutliPartUploadConfig(
|
|
768
|
+
chunk_size=chunk_size,
|
|
769
|
+
retries=retries,
|
|
770
|
+
resume_path_json=save_state_json,
|
|
771
|
+
max_chunks_before_suspension=max_chunks_before_suspension,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
out: MultiUploadResult = client.upload_file_multipart(
|
|
775
|
+
upload_target=upload_target, upload_config=upload_config
|
|
776
|
+
)
|
|
777
|
+
return out
|
|
778
|
+
|
|
655
779
|
def copy_dir(
|
|
656
780
|
self, src: str | Dir, dst: str | Dir, args: list[str] | None = None
|
|
657
781
|
) -> CompletedProcess:
|
|
@@ -680,10 +804,10 @@ class Rclone:
|
|
|
680
804
|
self,
|
|
681
805
|
src: Remote | Dir | str,
|
|
682
806
|
outdir: Path,
|
|
683
|
-
allow_writes=False,
|
|
684
|
-
use_links=
|
|
685
|
-
vfs_cache_mode=
|
|
686
|
-
|
|
807
|
+
allow_writes: bool | None = False,
|
|
808
|
+
use_links: bool | None = None,
|
|
809
|
+
vfs_cache_mode: str | None = None,
|
|
810
|
+
other_args: list[str] | None = None,
|
|
687
811
|
) -> Process:
|
|
688
812
|
"""Mount a remote or directory to a local path.
|
|
689
813
|
|
|
@@ -697,6 +821,9 @@ class Rclone:
|
|
|
697
821
|
Raises:
|
|
698
822
|
subprocess.CalledProcessError: If the mount operation fails
|
|
699
823
|
"""
|
|
824
|
+
allow_writes = allow_writes or False
|
|
825
|
+
use_links = use_links or True
|
|
826
|
+
vfs_cache_mode = vfs_cache_mode or "full"
|
|
700
827
|
if outdir.exists():
|
|
701
828
|
is_empty = not list(outdir.iterdir())
|
|
702
829
|
if not is_empty:
|
|
@@ -704,12 +831,13 @@ class Rclone:
|
|
|
704
831
|
f"Mount directory already exists and is not empty: {outdir}"
|
|
705
832
|
)
|
|
706
833
|
outdir.rmdir()
|
|
707
|
-
|
|
834
|
+
|
|
835
|
+
if _IS_WINDOWS:
|
|
836
|
+
# Windows -> Must create parent directories only if they don't exist
|
|
708
837
|
outdir.parent.mkdir(parents=True, exist_ok=True)
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
)
|
|
838
|
+
else:
|
|
839
|
+
# Linux -> Must create parent directories and the directory itself
|
|
840
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
713
841
|
src_str = convert_to_str(src)
|
|
714
842
|
cmd_list: list[str] = ["mount", src_str, str(outdir)]
|
|
715
843
|
if not allow_writes:
|
|
@@ -719,19 +847,50 @@ class Rclone:
|
|
|
719
847
|
if vfs_cache_mode:
|
|
720
848
|
cmd_list.append("--vfs-cache-mode")
|
|
721
849
|
cmd_list.append(vfs_cache_mode)
|
|
722
|
-
if
|
|
723
|
-
cmd_list +=
|
|
850
|
+
if other_args:
|
|
851
|
+
cmd_list += other_args
|
|
724
852
|
proc = self._launch_process(cmd_list)
|
|
725
853
|
wait_for_mount(outdir, proc)
|
|
726
854
|
return proc
|
|
727
855
|
|
|
856
|
+
@contextmanager
|
|
857
|
+
def scoped_mount(
|
|
858
|
+
self,
|
|
859
|
+
src: Remote | Dir | str,
|
|
860
|
+
outdir: Path,
|
|
861
|
+
allow_writes: bool | None = None,
|
|
862
|
+
use_links: bool | None = None,
|
|
863
|
+
vfs_cache_mode: str | None = None,
|
|
864
|
+
other_args: list[str] | None = None,
|
|
865
|
+
) -> Generator[Process, None, None]:
|
|
866
|
+
"""Like mount, but can be used in a context manager."""
|
|
867
|
+
proc = self.mount(
|
|
868
|
+
src,
|
|
869
|
+
outdir,
|
|
870
|
+
allow_writes=allow_writes,
|
|
871
|
+
use_links=use_links,
|
|
872
|
+
vfs_cache_mode=vfs_cache_mode,
|
|
873
|
+
other_args=other_args,
|
|
874
|
+
)
|
|
875
|
+
try:
|
|
876
|
+
yield proc
|
|
877
|
+
except Exception as e:
|
|
878
|
+
stack_trace = traceback.format_exc()
|
|
879
|
+
warnings.warn(f"Error in scoped_mount: {e}\n\nStack Trace:\n{stack_trace}")
|
|
880
|
+
raise
|
|
881
|
+
finally:
|
|
882
|
+
if proc.poll() is None:
|
|
883
|
+
proc.terminate()
|
|
884
|
+
proc.wait()
|
|
885
|
+
|
|
886
|
+
@deprecated("mount")
|
|
728
887
|
def mount_webdav(
|
|
729
888
|
self,
|
|
730
889
|
url: str,
|
|
731
890
|
outdir: Path,
|
|
732
|
-
vfs_cache_mode=
|
|
891
|
+
vfs_cache_mode: str | None = None,
|
|
733
892
|
vfs_disk_space_total_size: str | None = "10G",
|
|
734
|
-
|
|
893
|
+
other_args: list[str] | None = None,
|
|
735
894
|
) -> Process:
|
|
736
895
|
"""Mount a remote or directory to a local path.
|
|
737
896
|
|
|
@@ -745,6 +904,20 @@ class Rclone:
|
|
|
745
904
|
Raises:
|
|
746
905
|
subprocess.CalledProcessError: If the mount operation fails
|
|
747
906
|
"""
|
|
907
|
+
other_args = other_args or []
|
|
908
|
+
if vfs_cache_mode is None:
|
|
909
|
+
if "--vfs-cache-mode" in other_args:
|
|
910
|
+
pass
|
|
911
|
+
else:
|
|
912
|
+
vfs_cache_mode = "full"
|
|
913
|
+
elif "--vfs-cache-mode" in other_args:
|
|
914
|
+
warnings.warn(
|
|
915
|
+
f"vfs_cache_mode is set to {vfs_cache_mode} but --vfs-cache-mode is already in other_args"
|
|
916
|
+
)
|
|
917
|
+
idx = other_args.index("--vfs-cache-mode")
|
|
918
|
+
other_args.pop(idx)
|
|
919
|
+
other_args.pop(idx) # also the next value which will be the cache mode.
|
|
920
|
+
|
|
748
921
|
if outdir.exists():
|
|
749
922
|
is_empty = not list(outdir.iterdir())
|
|
750
923
|
if not is_empty:
|
|
@@ -755,10 +928,11 @@ class Rclone:
|
|
|
755
928
|
|
|
756
929
|
src_str = url
|
|
757
930
|
cmd_list: list[str] = ["mount", src_str, str(outdir)]
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
931
|
+
if vfs_cache_mode:
|
|
932
|
+
cmd_list.append("--vfs-cache-mode")
|
|
933
|
+
cmd_list.append(vfs_cache_mode)
|
|
934
|
+
if other_args:
|
|
935
|
+
cmd_list += other_args
|
|
762
936
|
if vfs_disk_space_total_size is not None:
|
|
763
937
|
cmd_list.append("--vfs-cache-max-size")
|
|
764
938
|
cmd_list.append(vfs_disk_space_total_size)
|
|
@@ -766,6 +940,7 @@ class Rclone:
|
|
|
766
940
|
wait_for_mount(outdir, proc)
|
|
767
941
|
return proc
|
|
768
942
|
|
|
943
|
+
# Settings optimized for s3.
|
|
769
944
|
def mount_s3(
|
|
770
945
|
self,
|
|
771
946
|
url: str,
|
|
@@ -787,7 +962,7 @@ class Rclone:
|
|
|
787
962
|
vfs_fast_fingerprint: bool = True,
|
|
788
963
|
# vfs-refresh
|
|
789
964
|
vfs_refresh: bool = True,
|
|
790
|
-
|
|
965
|
+
other_args: list[str] | None = None,
|
|
791
966
|
) -> Process:
|
|
792
967
|
"""Mount a remote or directory to a local path.
|
|
793
968
|
|
|
@@ -795,44 +970,44 @@ class Rclone:
|
|
|
795
970
|
src: Remote or directory to mount
|
|
796
971
|
outdir: Local path to mount to
|
|
797
972
|
"""
|
|
798
|
-
|
|
973
|
+
other_args = other_args or []
|
|
799
974
|
if modtime_strategy is not None:
|
|
800
|
-
|
|
975
|
+
other_args.append(f"--{modtime_strategy.value}")
|
|
801
976
|
if (vfs_cache_mode == "full" or vfs_cache_mode == "writes") and (
|
|
802
|
-
transfers is not None and "--transfers" not in
|
|
977
|
+
transfers is not None and "--transfers" not in other_args
|
|
803
978
|
):
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
if dir_cache_time is not None and "--dir-cache-time" not in
|
|
807
|
-
|
|
808
|
-
|
|
979
|
+
other_args.append("--transfers")
|
|
980
|
+
other_args.append(str(transfers))
|
|
981
|
+
if dir_cache_time is not None and "--dir-cache-time" not in other_args:
|
|
982
|
+
other_args.append("--dir-cache-time")
|
|
983
|
+
other_args.append(dir_cache_time)
|
|
809
984
|
if (
|
|
810
985
|
vfs_disk_space_total_size is not None
|
|
811
|
-
and "--vfs-cache-max-size" not in
|
|
986
|
+
and "--vfs-cache-max-size" not in other_args
|
|
812
987
|
):
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
if vfs_refresh and "--vfs-refresh" not in
|
|
816
|
-
|
|
817
|
-
if attribute_timeout is not None and "--attr-timeout" not in
|
|
818
|
-
|
|
819
|
-
|
|
988
|
+
other_args.append("--vfs-cache-max-size")
|
|
989
|
+
other_args.append(vfs_disk_space_total_size)
|
|
990
|
+
if vfs_refresh and "--vfs-refresh" not in other_args:
|
|
991
|
+
other_args.append("--vfs-refresh")
|
|
992
|
+
if attribute_timeout is not None and "--attr-timeout" not in other_args:
|
|
993
|
+
other_args.append("--attr-timeout")
|
|
994
|
+
other_args.append(attribute_timeout)
|
|
820
995
|
if vfs_read_chunk_streams:
|
|
821
|
-
|
|
822
|
-
|
|
996
|
+
other_args.append("--vfs-read-chunk-streams")
|
|
997
|
+
other_args.append(str(vfs_read_chunk_streams))
|
|
823
998
|
if vfs_read_chunk_size:
|
|
824
|
-
|
|
825
|
-
|
|
999
|
+
other_args.append("--vfs-read-chunk-size")
|
|
1000
|
+
other_args.append(vfs_read_chunk_size)
|
|
826
1001
|
if vfs_fast_fingerprint:
|
|
827
|
-
|
|
1002
|
+
other_args.append("--vfs-fast-fingerprint")
|
|
828
1003
|
|
|
829
|
-
|
|
1004
|
+
other_args = other_args if other_args else None
|
|
830
1005
|
return self.mount(
|
|
831
1006
|
url,
|
|
832
1007
|
outdir,
|
|
833
1008
|
allow_writes=allow_writes,
|
|
834
1009
|
vfs_cache_mode=vfs_cache_mode,
|
|
835
|
-
|
|
1010
|
+
other_args=other_args,
|
|
836
1011
|
)
|
|
837
1012
|
|
|
838
1013
|
def serve_webdav(
|
|
@@ -842,6 +1017,7 @@ class Rclone:
|
|
|
842
1017
|
password: str,
|
|
843
1018
|
addr: str = "localhost:2049",
|
|
844
1019
|
allow_other: bool = False,
|
|
1020
|
+
other_args: list[str] | None = None,
|
|
845
1021
|
) -> Process:
|
|
846
1022
|
"""Serve a remote or directory via NFS.
|
|
847
1023
|
|
|
@@ -861,6 +1037,8 @@ class Rclone:
|
|
|
861
1037
|
cmd_list.extend(["--user", user, "--pass", password])
|
|
862
1038
|
if allow_other:
|
|
863
1039
|
cmd_list.append("--allow-other")
|
|
1040
|
+
if other_args:
|
|
1041
|
+
cmd_list += other_args
|
|
864
1042
|
proc = self._launch_process(cmd_list)
|
|
865
1043
|
time.sleep(2) # give it a moment to start
|
|
866
1044
|
if proc.poll() is not None:
|
|
@@ -881,7 +1059,7 @@ class Rclone:
|
|
|
881
1059
|
check = get_check(check)
|
|
882
1060
|
if fast_list or (other_args and "--fast-list" in other_args):
|
|
883
1061
|
warnings.warn(
|
|
884
|
-
"It's not recommended to use --fast-list with size_files as the entire repository has to be
|
|
1062
|
+
"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."
|
|
885
1063
|
)
|
|
886
1064
|
files = list(files)
|
|
887
1065
|
all_files: list[File] = []
|