rclone-api 1.1.4__tar.gz → 1.1.6__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.1.4 → rclone_api-1.1.6}/PKG-INFO +1 -1
- {rclone_api-1.1.4 → rclone_api-1.1.6}/pyproject.toml +1 -1
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/__init__.py +2 -1
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/cmd/copy_large_s3.py +10 -7
- rclone_api-1.1.6/src/rclone_api/mount.py +115 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/rclone.py +33 -56
- rclone_api-1.1.6/src/rclone_api/s3/chunk_file.py +102 -0
- rclone_api-1.1.6/src/rclone_api/s3/chunk_types.py +254 -0
- rclone_api-1.1.6/src/rclone_api/s3/chunk_uploader.py +262 -0
- rclone_api-1.1.6/src/rclone_api/types.py +125 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/util.py +8 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api.egg-info/PKG-INFO +1 -1
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api.egg-info/SOURCES.txt +4 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_mounted_ranged_download.py +2 -2
- rclone_api-1.1.6/tests/test_size_suffix.py +21 -0
- rclone_api-1.1.4/src/rclone_api/s3/chunk_uploader.py +0 -602
- rclone_api-1.1.4/src/rclone_api/types.py +0 -35
- {rclone_api-1.1.4 → rclone_api-1.1.6}/.aiderignore +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/.github/workflows/lint.yml +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/.github/workflows/push_macos.yml +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/.github/workflows/push_ubuntu.yml +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/.github/workflows/push_win.yml +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/.gitignore +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/.pylintrc +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/.vscode/launch.json +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/.vscode/settings.json +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/.vscode/tasks.json +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/LICENSE +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/MANIFEST.in +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/README.md +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/clean +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/install +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/lint +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/requirements.testing.txt +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/setup.cfg +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/setup.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/cmd/list_files.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/completed_process.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/config.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/convert.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/deprecated.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/diff.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/dir.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/dir_listing.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/exec.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/experimental/flags.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/experimental/flags_base.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/file.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/filelist.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/group_files.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/process.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/rpath.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/s3/api.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/s3/basic_ops.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/s3/create.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/s3/types.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/scan_missing_folders.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api/walk.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api.egg-info/entry_points.txt +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api.egg-info/requires.txt +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/test +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/archive/test_paramiko.py.disabled +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_cmd_list_files.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_copy.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_copy_files.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_diff.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_group_files.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_is_synced.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_ls.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_mount.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_mount_s3.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_mount_webdav.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_obscure.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_rclone_config.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_remote_control.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_remotes.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_s3.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_scan_missing_folders.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_size_files.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tests/test_walk.py +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/tox.ini +0 -0
- {rclone_api-1.1.4 → rclone_api-1.1.6}/upload_package.sh +0 -0
|
@@ -10,7 +10,7 @@ from .rclone import Rclone, rclone_verbose
|
|
|
10
10
|
from .remote import Remote
|
|
11
11
|
from .rpath import RPath
|
|
12
12
|
from .s3.types import MultiUploadResult
|
|
13
|
-
from .types import ListingOption, Order, SizeResult
|
|
13
|
+
from .types import ListingOption, Order, SizeResult, SizeSuffix
|
|
14
14
|
|
|
15
15
|
__all__ = [
|
|
16
16
|
"Rclone",
|
|
@@ -34,4 +34,5 @@ __all__ = [
|
|
|
34
34
|
"Parsed",
|
|
35
35
|
"Section",
|
|
36
36
|
"MultiUploadResult",
|
|
37
|
+
"SizeSuffix",
|
|
37
38
|
]
|
|
@@ -2,7 +2,7 @@ import argparse
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from rclone_api import MultiUploadResult, Rclone
|
|
5
|
+
from rclone_api import MultiUploadResult, Rclone, SizeSuffix
|
|
6
6
|
|
|
7
7
|
_1MB = 1024 * 1024
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ class Args:
|
|
|
12
12
|
config_path: Path
|
|
13
13
|
src: str
|
|
14
14
|
dst: str
|
|
15
|
-
chunk_size_mb:
|
|
15
|
+
chunk_size_mb: SizeSuffix
|
|
16
16
|
read_concurrent_chunks: int
|
|
17
17
|
retries: int
|
|
18
18
|
save_state_json: Path
|
|
@@ -35,17 +35,20 @@ def _parse_args() -> Args:
|
|
|
35
35
|
"--config", help="Path to rclone config file", type=Path, required=True
|
|
36
36
|
)
|
|
37
37
|
parser.add_argument(
|
|
38
|
-
"--chunk-size
|
|
38
|
+
"--chunk-size",
|
|
39
|
+
help="Chunk size that will be read and uploaded in in SizeSuffix (i.e. 128M = 128 megabytes) form",
|
|
40
|
+
type=str,
|
|
41
|
+
default="128M",
|
|
39
42
|
)
|
|
40
43
|
parser.add_argument(
|
|
41
44
|
"--read-concurrent-chunks",
|
|
42
|
-
help="Maximum number of chunks to read",
|
|
45
|
+
help="Maximum number of chunks to read in a look ahead cache",
|
|
43
46
|
type=int,
|
|
44
|
-
default=
|
|
47
|
+
default=1,
|
|
45
48
|
)
|
|
46
49
|
parser.add_argument("--retries", help="Number of retries", type=int, default=3)
|
|
47
50
|
parser.add_argument(
|
|
48
|
-
"--
|
|
51
|
+
"--resume-json",
|
|
49
52
|
help="Path to resumable JSON file",
|
|
50
53
|
type=Path,
|
|
51
54
|
default="resume.json",
|
|
@@ -56,7 +59,7 @@ def _parse_args() -> Args:
|
|
|
56
59
|
config_path=Path(args.config),
|
|
57
60
|
src=args.src,
|
|
58
61
|
dst=args.dst,
|
|
59
|
-
chunk_size_mb=args.
|
|
62
|
+
chunk_size_mb=SizeSuffix(args.chunk_size),
|
|
60
63
|
read_concurrent_chunks=args.read_concurrent_chunks,
|
|
61
64
|
retries=args.retries,
|
|
62
65
|
save_state_json=args.resumable_json,
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
import subprocess
|
|
3
|
+
import time
|
|
4
|
+
import warnings
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_SYSTEM = platform.system() # "Linux", "Darwin", "Windows", etc.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_command(cmd: str, verbose: bool) -> int:
|
|
11
|
+
"""Run a shell command and print its output if verbose is True."""
|
|
12
|
+
if verbose:
|
|
13
|
+
print(f"Executing: {cmd}")
|
|
14
|
+
try:
|
|
15
|
+
result = subprocess.run(
|
|
16
|
+
cmd, shell=True, capture_output=True, text=True, check=False
|
|
17
|
+
)
|
|
18
|
+
if result.returncode != 0 and verbose:
|
|
19
|
+
print(f"Command failed: {cmd}\nStdErr: {result.stderr.strip()}")
|
|
20
|
+
return result.returncode
|
|
21
|
+
except Exception as e:
|
|
22
|
+
warnings.warn(f"Error running command '{cmd}': {e}")
|
|
23
|
+
return -1
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def prepare_mount(outdir: Path, verbose: bool) -> None:
|
|
27
|
+
if _SYSTEM == "Windows":
|
|
28
|
+
# Windows -> Must create parent directories only if they don't exist
|
|
29
|
+
if verbose:
|
|
30
|
+
print(f"Creating parent directories for {outdir}")
|
|
31
|
+
outdir.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
else:
|
|
33
|
+
# Linux -> Must create parent directories and the directory itself
|
|
34
|
+
if verbose:
|
|
35
|
+
print(f"Creating directories for {outdir}")
|
|
36
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def clean_mount(mount_path: Path, verbose: bool = False) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Clean up a mount path across Linux, macOS, and Windows.
|
|
42
|
+
|
|
43
|
+
The function attempts to unmount the mount at mount_path, then, if the
|
|
44
|
+
directory is empty, removes it. On Linux it uses 'fusermount -u' (for FUSE mounts)
|
|
45
|
+
and 'umount'. On macOS it uses 'umount' (and optionally 'diskutil unmount'),
|
|
46
|
+
while on Windows it attempts to remove the mount point via 'mountvol /D'.
|
|
47
|
+
"""
|
|
48
|
+
# Check if the mount path exists; if an OSError occurs, assume it exists.
|
|
49
|
+
try:
|
|
50
|
+
mount_exists = mount_path.exists()
|
|
51
|
+
except OSError as e:
|
|
52
|
+
warnings.warn(f"Error checking {mount_path}: {e}")
|
|
53
|
+
mount_exists = True
|
|
54
|
+
|
|
55
|
+
# Give the system a moment (if unmount is in progress, etc.)
|
|
56
|
+
time.sleep(2)
|
|
57
|
+
|
|
58
|
+
if not mount_exists:
|
|
59
|
+
if verbose:
|
|
60
|
+
print(f"{mount_path} does not exist; nothing to clean up.")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
if verbose:
|
|
64
|
+
print(f"{mount_path} still exists, attempting to unmount and remove.")
|
|
65
|
+
|
|
66
|
+
# Platform-specific unmount procedures
|
|
67
|
+
if _SYSTEM == "Linux":
|
|
68
|
+
# Try FUSE unmount first (if applicable), then the regular umount.
|
|
69
|
+
run_command(f"fusermount -u {mount_path}", verbose)
|
|
70
|
+
run_command(f"umount {mount_path}", verbose)
|
|
71
|
+
elif _SYSTEM == "Darwin":
|
|
72
|
+
# On macOS, use umount; optionally try diskutil for stubborn mounts.
|
|
73
|
+
run_command(f"umount {mount_path}", verbose)
|
|
74
|
+
# Optionally: uncomment the next line if diskutil unmount is preferred.
|
|
75
|
+
# run_command(f"diskutil unmount {mount_path}", verbose)
|
|
76
|
+
elif _SYSTEM == "Windows":
|
|
77
|
+
# On Windows, remove the mount point using mountvol.
|
|
78
|
+
run_command(f"mountvol {mount_path} /D", verbose)
|
|
79
|
+
# If that does not work, try to remove the directory directly.
|
|
80
|
+
try:
|
|
81
|
+
mount_path.rmdir()
|
|
82
|
+
if verbose:
|
|
83
|
+
print(f"Successfully removed mount directory {mount_path}")
|
|
84
|
+
except Exception as e:
|
|
85
|
+
warnings.warn(f"Failed to remove mount {mount_path}: {e}")
|
|
86
|
+
else:
|
|
87
|
+
warnings.warn(f"Unsupported platform: {_SYSTEM}")
|
|
88
|
+
|
|
89
|
+
# Allow some time for the unmount commands to take effect.
|
|
90
|
+
time.sleep(2)
|
|
91
|
+
|
|
92
|
+
# Re-check if the mount path still exists.
|
|
93
|
+
try:
|
|
94
|
+
still_exists = mount_path.exists()
|
|
95
|
+
except OSError as e:
|
|
96
|
+
warnings.warn(f"Error re-checking {mount_path}: {e}")
|
|
97
|
+
still_exists = True
|
|
98
|
+
|
|
99
|
+
if still_exists:
|
|
100
|
+
if verbose:
|
|
101
|
+
print(f"{mount_path} still exists after unmount attempt.")
|
|
102
|
+
# Attempt to remove the directory if it is empty.
|
|
103
|
+
try:
|
|
104
|
+
# Only remove if the directory is empty.
|
|
105
|
+
if not any(mount_path.iterdir()):
|
|
106
|
+
mount_path.rmdir()
|
|
107
|
+
if verbose:
|
|
108
|
+
print(f"Removed empty mount directory {mount_path}")
|
|
109
|
+
else:
|
|
110
|
+
warnings.warn(f"{mount_path} is not empty; cannot remove.")
|
|
111
|
+
except Exception as e:
|
|
112
|
+
warnings.warn(f"Failed during cleanup of {mount_path}: {e}")
|
|
113
|
+
else:
|
|
114
|
+
if verbose:
|
|
115
|
+
print(f"{mount_path} successfully cleaned up.")
|
|
@@ -34,7 +34,13 @@ from rclone_api.s3.types import (
|
|
|
34
34
|
S3Provider,
|
|
35
35
|
S3UploadTarget,
|
|
36
36
|
)
|
|
37
|
-
from rclone_api.types import
|
|
37
|
+
from rclone_api.types import (
|
|
38
|
+
ListingOption,
|
|
39
|
+
ModTimeStrategy,
|
|
40
|
+
Order,
|
|
41
|
+
SizeResult,
|
|
42
|
+
SizeSuffix,
|
|
43
|
+
)
|
|
38
44
|
from rclone_api.util import (
|
|
39
45
|
get_check,
|
|
40
46
|
get_rclone_exe,
|
|
@@ -673,10 +679,13 @@ class Rclone:
|
|
|
673
679
|
src: str,
|
|
674
680
|
dst: str,
|
|
675
681
|
save_state_json: Path,
|
|
676
|
-
chunk_size:
|
|
677
|
-
|
|
678
|
-
* 1024
|
|
679
|
-
|
|
682
|
+
chunk_size: SizeSuffix | None = None,
|
|
683
|
+
# 16
|
|
684
|
+
# * 1024
|
|
685
|
+
# * 1024, # This setting will scale the performance of the upload
|
|
686
|
+
concurrent_chunks: (
|
|
687
|
+
int | None
|
|
688
|
+
) = None, # This setting will scale the performance of the upload
|
|
680
689
|
retries: int = 3,
|
|
681
690
|
verbose: bool | None = None,
|
|
682
691
|
max_chunks_before_suspension: int | None = None,
|
|
@@ -687,16 +696,22 @@ class Rclone:
|
|
|
687
696
|
from rclone_api.s3.create import S3Credentials
|
|
688
697
|
from rclone_api.util import S3PathInfo, random_str, split_s3_path
|
|
689
698
|
|
|
699
|
+
_tmp: SizeSuffix | str = chunk_size or "16MiB"
|
|
700
|
+
chunk_size = SizeSuffix(_tmp)
|
|
701
|
+
assert chunk_size is not None
|
|
702
|
+
concurrent_chunks = concurrent_chunks or 4
|
|
703
|
+
size_limit = SizeSuffix(chunk_size * concurrent_chunks)
|
|
704
|
+
|
|
690
705
|
other_args: list[str] = [
|
|
691
706
|
"--no-modtime",
|
|
692
707
|
"--vfs-read-wait",
|
|
693
708
|
"1s",
|
|
694
709
|
"--vfs-disk-space-total-size",
|
|
695
|
-
|
|
710
|
+
size_limit.as_str(), # purge quickly.
|
|
696
711
|
"--vfs-read-chunk-size",
|
|
697
|
-
|
|
712
|
+
chunk_size.as_str(),
|
|
698
713
|
"--vfs-read-chunk-size-limit",
|
|
699
|
-
|
|
714
|
+
size_limit.as_str(),
|
|
700
715
|
"--vfs-read-chunk-streams",
|
|
701
716
|
str(concurrent_chunks),
|
|
702
717
|
"--vfs-fast-fingerprint",
|
|
@@ -765,7 +780,7 @@ class Rclone:
|
|
|
765
780
|
|
|
766
781
|
client = S3Client(s3_creds)
|
|
767
782
|
config: S3MutliPartUploadConfig = S3MutliPartUploadConfig(
|
|
768
|
-
chunk_size=chunk_size,
|
|
783
|
+
chunk_size=chunk_size.as_int(),
|
|
769
784
|
retries=retries,
|
|
770
785
|
resume_path_json=save_state_json,
|
|
771
786
|
max_chunks_before_suspension=max_chunks_before_suspension,
|
|
@@ -788,7 +803,7 @@ class Rclone:
|
|
|
788
803
|
)
|
|
789
804
|
|
|
790
805
|
upload_config = S3MutliPartUploadConfig(
|
|
791
|
-
chunk_size=chunk_size,
|
|
806
|
+
chunk_size=chunk_size.as_int(),
|
|
792
807
|
retries=retries,
|
|
793
808
|
resume_path_json=save_state_json,
|
|
794
809
|
max_chunks_before_suspension=max_chunks_before_suspension,
|
|
@@ -845,24 +860,14 @@ class Rclone:
|
|
|
845
860
|
Raises:
|
|
846
861
|
subprocess.CalledProcessError: If the mount operation fails
|
|
847
862
|
"""
|
|
863
|
+
from rclone_api.mount import clean_mount, prepare_mount
|
|
864
|
+
|
|
848
865
|
allow_writes = allow_writes or False
|
|
849
866
|
use_links = use_links or True
|
|
850
867
|
verbose = get_verbose(verbose)
|
|
851
868
|
vfs_cache_mode = vfs_cache_mode or "full"
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
if not is_empty:
|
|
855
|
-
raise ValueError(
|
|
856
|
-
f"Mount directory already exists and is not empty: {outdir}"
|
|
857
|
-
)
|
|
858
|
-
outdir.rmdir()
|
|
859
|
-
|
|
860
|
-
if _IS_WINDOWS:
|
|
861
|
-
# Windows -> Must create parent directories only if they don't exist
|
|
862
|
-
outdir.parent.mkdir(parents=True, exist_ok=True)
|
|
863
|
-
else:
|
|
864
|
-
# Linux -> Must create parent directories and the directory itself
|
|
865
|
-
outdir.mkdir(parents=True, exist_ok=True)
|
|
869
|
+
clean_mount(outdir, verbose=verbose)
|
|
870
|
+
prepare_mount(outdir, verbose=verbose)
|
|
866
871
|
src_str = convert_to_str(src)
|
|
867
872
|
cmd_list: list[str] = ["mount", src_str, str(outdir)]
|
|
868
873
|
if not allow_writes:
|
|
@@ -914,38 +919,10 @@ class Rclone:
|
|
|
914
919
|
if proc.poll() is None:
|
|
915
920
|
proc.terminate()
|
|
916
921
|
proc.wait()
|
|
917
|
-
if not error_happened
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
if not _IS_WINDOWS:
|
|
922
|
-
|
|
923
|
-
def exec(cmd: str) -> int:
|
|
924
|
-
if verbose:
|
|
925
|
-
print(f"Executing: {cmd}")
|
|
926
|
-
rtn = os.system(cmd)
|
|
927
|
-
if rtn != 0 and verbose:
|
|
928
|
-
print(f"Failed to execute: {cmd}")
|
|
929
|
-
return rtn
|
|
930
|
-
|
|
931
|
-
exec(f"fusermount -u {outdir}")
|
|
932
|
-
exec(f"umount {outdir}")
|
|
933
|
-
time.sleep(2)
|
|
934
|
-
if outdir.exists():
|
|
935
|
-
is_empty = True
|
|
936
|
-
try:
|
|
937
|
-
is_empty = not list(outdir.iterdir())
|
|
938
|
-
if not is_empty:
|
|
939
|
-
warnings.warn(f"Failed to unmount {outdir}")
|
|
940
|
-
else:
|
|
941
|
-
try:
|
|
942
|
-
outdir.rmdir()
|
|
943
|
-
except Exception as e:
|
|
944
|
-
warnings.warn(f"Failed to remove {outdir}: {e}")
|
|
945
|
-
except Exception as e:
|
|
946
|
-
warnings.warn(
|
|
947
|
-
f"Failed during mount cleanup of {outdir}: because {e}"
|
|
948
|
-
)
|
|
922
|
+
if not error_happened:
|
|
923
|
+
from rclone_api.mount import clean_mount
|
|
924
|
+
|
|
925
|
+
clean_mount(outdir, verbose=verbose)
|
|
949
926
|
|
|
950
927
|
@deprecated("mount")
|
|
951
928
|
def mount_webdav(
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import warnings
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from queue import Queue
|
|
5
|
+
|
|
6
|
+
from rclone_api.s3.chunk_types import FileChunk, UploadState
|
|
7
|
+
from rclone_api.util import locked_print
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_file_size(file_path: Path, timeout: int = 60) -> int:
|
|
11
|
+
sleep_time = timeout / 60 if timeout > 0 else 1
|
|
12
|
+
start = time.time()
|
|
13
|
+
while True:
|
|
14
|
+
expired = time.time() - start > timeout
|
|
15
|
+
try:
|
|
16
|
+
time.sleep(sleep_time)
|
|
17
|
+
if file_path.exists():
|
|
18
|
+
return file_path.stat().st_size
|
|
19
|
+
except FileNotFoundError as e:
|
|
20
|
+
if expired:
|
|
21
|
+
print(f"File not found: {file_path}, exception is {e}")
|
|
22
|
+
raise
|
|
23
|
+
if expired:
|
|
24
|
+
raise TimeoutError(f"File {file_path} not found after {timeout} seconds")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def file_chunker(
|
|
28
|
+
upload_state: UploadState, max_chunks: int | None, output: Queue[FileChunk | None]
|
|
29
|
+
) -> None:
|
|
30
|
+
count = 0
|
|
31
|
+
|
|
32
|
+
def should_stop() -> bool:
|
|
33
|
+
nonlocal count
|
|
34
|
+
if max_chunks is None:
|
|
35
|
+
return False
|
|
36
|
+
if count >= max_chunks:
|
|
37
|
+
return True
|
|
38
|
+
count += 1
|
|
39
|
+
if count > 10 and count % 10 == 0:
|
|
40
|
+
# recheck that the file size has not changed
|
|
41
|
+
file_size = _get_file_size(upload_state.upload_info.src_file_path)
|
|
42
|
+
if file_size != upload_state.upload_info.file_size:
|
|
43
|
+
locked_print(
|
|
44
|
+
f"File size changed, cannot resume, expected {upload_state.upload_info.file_size}, got {file_size}"
|
|
45
|
+
)
|
|
46
|
+
raise ValueError("File size changed, cannot resume")
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
upload_info = upload_state.upload_info
|
|
50
|
+
file_path = upload_info.src_file_path
|
|
51
|
+
chunk_size = upload_info.chunk_size
|
|
52
|
+
src = Path(file_path)
|
|
53
|
+
# Mounted files may take a while to appear, so keep retrying.
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
file_size = _get_file_size(src, timeout=60)
|
|
57
|
+
part_number = 1
|
|
58
|
+
done_part_numbers: set[int] = {
|
|
59
|
+
p.part_number for p in upload_state.parts if p is not None
|
|
60
|
+
}
|
|
61
|
+
num_parts = upload_info.total_chunks()
|
|
62
|
+
|
|
63
|
+
def next_part_number() -> int | None:
|
|
64
|
+
nonlocal part_number
|
|
65
|
+
while part_number in done_part_numbers:
|
|
66
|
+
part_number += 1
|
|
67
|
+
if part_number > num_parts:
|
|
68
|
+
return None
|
|
69
|
+
return part_number
|
|
70
|
+
|
|
71
|
+
while not should_stop():
|
|
72
|
+
curr_parth_num = next_part_number()
|
|
73
|
+
if curr_parth_num is None:
|
|
74
|
+
locked_print(f"File {file_path} has completed chunking all parts")
|
|
75
|
+
break
|
|
76
|
+
assert curr_parth_num is not None
|
|
77
|
+
offset = (curr_parth_num - 1) * chunk_size
|
|
78
|
+
|
|
79
|
+
assert offset < file_size, f"Offset {offset} is greater than file size"
|
|
80
|
+
|
|
81
|
+
# Open the file, seek, read the chunk, and close immediately.
|
|
82
|
+
with open(file_path, "rb") as f:
|
|
83
|
+
f.seek(offset)
|
|
84
|
+
data = f.read(chunk_size)
|
|
85
|
+
|
|
86
|
+
if not data:
|
|
87
|
+
warnings.warn(f"Empty data for part {part_number} of {file_path}")
|
|
88
|
+
|
|
89
|
+
file_chunk = FileChunk(
|
|
90
|
+
src,
|
|
91
|
+
upload_id=upload_info.upload_id,
|
|
92
|
+
part_number=part_number,
|
|
93
|
+
data=data, # After this, data should not be reused.
|
|
94
|
+
)
|
|
95
|
+
done_part_numbers.add(part_number)
|
|
96
|
+
output.put(file_chunk)
|
|
97
|
+
part_number += 1
|
|
98
|
+
except Exception as e:
|
|
99
|
+
|
|
100
|
+
warnings.warn(f"Error reading file: {e}")
|
|
101
|
+
finally:
|
|
102
|
+
output.put(None)
|