rclone-api 1.0.13__py2.py3-none-any.whl → 1.0.15__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 +12 -2
- rclone_api/config.py +3 -14
- rclone_api/convert.py +31 -0
- rclone_api/dir.py +16 -0
- rclone_api/dir_listing.py +28 -2
- rclone_api/{types.py → exec.py} +3 -8
- rclone_api/file.py +16 -1
- rclone_api/filelist.py +30 -0
- rclone_api/rclone.py +234 -97
- rclone_api/rpath.py +21 -10
- rclone_api/util.py +17 -4
- rclone_api/walk.py +49 -66
- {rclone_api-1.0.13.dist-info → rclone_api-1.0.15.dist-info}/METADATA +1 -1
- rclone_api-1.0.15.dist-info/RECORD +20 -0
- rclone_api-1.0.13.dist-info/RECORD +0 -18
- {rclone_api-1.0.13.dist-info → rclone_api-1.0.15.dist-info}/LICENSE +0 -0
- {rclone_api-1.0.13.dist-info → rclone_api-1.0.15.dist-info}/WHEEL +0 -0
- {rclone_api-1.0.13.dist-info → rclone_api-1.0.15.dist-info}/top_level.txt +0 -0
rclone_api/__init__.py
CHANGED
@@ -1,9 +1,19 @@
|
|
1
|
+
from .config import Config
|
1
2
|
from .dir import Dir
|
2
3
|
from .dir_listing import DirListing
|
3
4
|
from .file import File
|
5
|
+
from .filelist import FileList
|
4
6
|
from .rclone import Rclone
|
5
7
|
from .remote import Remote
|
6
8
|
from .rpath import RPath
|
7
|
-
from .types import Config
|
8
9
|
|
9
|
-
__all__ = [
|
10
|
+
__all__ = [
|
11
|
+
"Rclone",
|
12
|
+
"File",
|
13
|
+
"Config",
|
14
|
+
"Remote",
|
15
|
+
"Dir",
|
16
|
+
"RPath",
|
17
|
+
"DirListing",
|
18
|
+
"FileList",
|
19
|
+
]
|
rclone_api/config.py
CHANGED
@@ -1,19 +1,8 @@
|
|
1
|
-
import subprocess
|
2
1
|
from dataclasses import dataclass
|
3
|
-
from pathlib import Path
|
4
|
-
|
5
|
-
from rclone_api.types import Config
|
6
2
|
|
7
3
|
|
8
4
|
@dataclass
|
9
|
-
class
|
10
|
-
"""Rclone
|
11
|
-
|
12
|
-
rclone_config: Path | Config
|
13
|
-
rclone_exe: Path
|
14
|
-
|
15
|
-
def execute(self, cmd: list[str]) -> subprocess.CompletedProcess:
|
16
|
-
"""Execute rclone command."""
|
17
|
-
from rclone_api.util import rclone_execute
|
5
|
+
class Config:
|
6
|
+
"""Rclone configuration dataclass."""
|
18
7
|
|
19
|
-
|
8
|
+
text: str
|
rclone_api/convert.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
from rclone_api.dir import Dir
|
2
|
+
from rclone_api.file import File
|
3
|
+
from rclone_api.remote import Remote
|
4
|
+
|
5
|
+
|
6
|
+
def convert_to_filestr_list(files: str | File | list[str] | list[File]) -> list[str]:
|
7
|
+
out: list[str] = []
|
8
|
+
if isinstance(files, str):
|
9
|
+
out.append(files)
|
10
|
+
elif isinstance(files, File):
|
11
|
+
out.append(str(files.path))
|
12
|
+
elif isinstance(files, list):
|
13
|
+
for f in files:
|
14
|
+
if isinstance(f, File):
|
15
|
+
f = str(f.path)
|
16
|
+
out.append(f)
|
17
|
+
else:
|
18
|
+
raise ValueError(f"Invalid type for file: {type(files)}")
|
19
|
+
return out
|
20
|
+
|
21
|
+
|
22
|
+
def convert_to_str(file_or_dir: str | File | Dir | Remote) -> str:
|
23
|
+
if isinstance(file_or_dir, str):
|
24
|
+
return file_or_dir
|
25
|
+
if isinstance(file_or_dir, File):
|
26
|
+
return str(file_or_dir.path)
|
27
|
+
if isinstance(file_or_dir, Dir):
|
28
|
+
return str(file_or_dir.path)
|
29
|
+
if isinstance(file_or_dir, Remote):
|
30
|
+
return str(file_or_dir)
|
31
|
+
raise ValueError(f"Invalid type for file_or_dir: {type(file_or_dir)}")
|
rclone_api/dir.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import json
|
1
2
|
from typing import Generator
|
2
3
|
|
3
4
|
from rclone_api.dir_listing import DirListing
|
@@ -12,6 +13,10 @@ class Dir:
|
|
12
13
|
def remote(self) -> Remote:
|
13
14
|
return self.path.remote
|
14
15
|
|
16
|
+
@property
|
17
|
+
def name(self) -> str:
|
18
|
+
return self.path.name
|
19
|
+
|
15
20
|
def __init__(self, path: RPath | Remote) -> None:
|
16
21
|
"""Initialize Dir with either an RPath or Remote.
|
17
22
|
|
@@ -33,6 +38,8 @@ class Dir:
|
|
33
38
|
self.path.set_rclone(path.rclone)
|
34
39
|
else:
|
35
40
|
self.path = path
|
41
|
+
# self.path.set_rclone(self.path.remote.rclone)
|
42
|
+
assert self.path.rclone is not None
|
36
43
|
|
37
44
|
def ls(self, max_depth: int = 0) -> DirListing:
|
38
45
|
"""List files and directories in the given path."""
|
@@ -47,5 +54,14 @@ class Dir:
|
|
47
54
|
assert self.path.rclone is not None
|
48
55
|
return walk(self, max_depth=max_depth)
|
49
56
|
|
57
|
+
def to_json(self) -> dict:
|
58
|
+
"""Convert the Dir to a JSON serializable dictionary."""
|
59
|
+
return self.path.to_json()
|
60
|
+
|
50
61
|
def __str__(self) -> str:
|
51
62
|
return str(self.path)
|
63
|
+
|
64
|
+
def __repr__(self) -> str:
|
65
|
+
data = self.path.to_json()
|
66
|
+
data_str = json.dumps(data)
|
67
|
+
return data_str
|
rclone_api/dir_listing.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
import json
|
2
|
+
|
3
3
|
from rclone_api.rpath import RPath
|
4
4
|
|
5
5
|
|
@@ -12,3 +12,29 @@ class DirListing:
|
|
12
12
|
|
13
13
|
self.dirs: list[Dir] = [Dir(d) for d in dirs_and_files if d.is_dir]
|
14
14
|
self.files: list[File] = [File(f) for f in dirs_and_files if not f.is_dir]
|
15
|
+
|
16
|
+
def __str__(self) -> str:
|
17
|
+
n_files = len(self.files)
|
18
|
+
n_dirs = len(self.dirs)
|
19
|
+
msg = f"Files: {n_files}\n"
|
20
|
+
if n_files > 0:
|
21
|
+
for f in self.files:
|
22
|
+
msg += f" {f}\n"
|
23
|
+
msg += f"Dirs: {n_dirs}\n"
|
24
|
+
if n_dirs > 0:
|
25
|
+
for d in self.dirs:
|
26
|
+
msg += f" {d}\n"
|
27
|
+
return msg
|
28
|
+
|
29
|
+
def __repr__(self) -> str:
|
30
|
+
dirs: list = []
|
31
|
+
files: list = []
|
32
|
+
for d in self.dirs:
|
33
|
+
dirs.append(d.path.to_json())
|
34
|
+
for f in self.files:
|
35
|
+
files.append(f.path.to_json())
|
36
|
+
json_obj = {
|
37
|
+
"dirs": dirs,
|
38
|
+
"files": files,
|
39
|
+
}
|
40
|
+
return json.dumps(json_obj, indent=2)
|
rclone_api/{types.py → exec.py}
RENAMED
@@ -2,12 +2,7 @@ import subprocess
|
|
2
2
|
from dataclasses import dataclass
|
3
3
|
from pathlib import Path
|
4
4
|
|
5
|
-
|
6
|
-
@dataclass
|
7
|
-
class Config:
|
8
|
-
"""Rclone configuration dataclass."""
|
9
|
-
|
10
|
-
text: str
|
5
|
+
from rclone_api.config import Config
|
11
6
|
|
12
7
|
|
13
8
|
@dataclass
|
@@ -17,8 +12,8 @@ class RcloneExec:
|
|
17
12
|
rclone_config: Path | Config
|
18
13
|
rclone_exe: Path
|
19
14
|
|
20
|
-
def execute(self, cmd: list[str]) -> subprocess.CompletedProcess:
|
15
|
+
def execute(self, cmd: list[str], check: bool) -> subprocess.CompletedProcess:
|
21
16
|
"""Execute rclone command."""
|
22
17
|
from rclone_api.util import rclone_execute
|
23
18
|
|
24
|
-
return rclone_execute(cmd, self.rclone_config, self.rclone_exe)
|
19
|
+
return rclone_execute(cmd, self.rclone_config, self.rclone_exe, check=check)
|
rclone_api/file.py
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
import json
|
2
|
+
|
1
3
|
from rclone_api.rpath import RPath
|
2
4
|
|
3
5
|
|
@@ -10,6 +12,10 @@ class File:
|
|
10
12
|
) -> None:
|
11
13
|
self.path = path
|
12
14
|
|
15
|
+
@property
|
16
|
+
def name(self) -> str:
|
17
|
+
return self.path.name
|
18
|
+
|
13
19
|
def read_text(self) -> str:
|
14
20
|
"""Read the file contents as bytes.
|
15
21
|
|
@@ -25,8 +31,17 @@ class File:
|
|
25
31
|
if self.path.is_dir:
|
26
32
|
raise RuntimeError("Cannot read a directory as bytes")
|
27
33
|
|
28
|
-
result = self.path.rclone._run(["cat", self.path.path])
|
34
|
+
result = self.path.rclone._run(["cat", self.path.path], check=True)
|
29
35
|
return result.stdout
|
30
36
|
|
37
|
+
def to_json(self) -> dict:
|
38
|
+
"""Convert the File to a JSON serializable dictionary."""
|
39
|
+
return self.path.to_json()
|
40
|
+
|
31
41
|
def __str__(self) -> str:
|
32
42
|
return str(self.path)
|
43
|
+
|
44
|
+
def __repr__(self) -> str:
|
45
|
+
data = self.path.to_json()
|
46
|
+
data_str = json.dumps(data)
|
47
|
+
return data_str
|
rclone_api/filelist.py
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
|
3
|
+
from rclone_api.dir import Dir
|
4
|
+
from rclone_api.dir_listing import DirListing
|
5
|
+
from rclone_api.file import File
|
6
|
+
from rclone_api.rpath import RPath
|
7
|
+
|
8
|
+
|
9
|
+
@dataclass
|
10
|
+
class FileList:
|
11
|
+
"""Remote file dataclass."""
|
12
|
+
|
13
|
+
dirs: list[Dir]
|
14
|
+
files: list[File]
|
15
|
+
|
16
|
+
def _to_dir_list(self) -> list[RPath]:
|
17
|
+
pathlist: list[RPath] = []
|
18
|
+
for d in self.dirs:
|
19
|
+
pathlist.append(d.path)
|
20
|
+
for f in self.files:
|
21
|
+
pathlist.append(f.path)
|
22
|
+
return pathlist
|
23
|
+
|
24
|
+
def __str__(self) -> str:
|
25
|
+
pathlist: list[RPath] = self._to_dir_list()
|
26
|
+
return str(DirListing(pathlist))
|
27
|
+
|
28
|
+
def __repr__(self) -> str:
|
29
|
+
pathlist: list[RPath] = self._to_dir_list()
|
30
|
+
return repr(DirListing(pathlist))
|
rclone_api/rclone.py
CHANGED
@@ -1,97 +1,234 @@
|
|
1
|
-
"""
|
2
|
-
Unit test file.
|
3
|
-
"""
|
4
|
-
|
5
|
-
import subprocess
|
6
|
-
from
|
7
|
-
from
|
8
|
-
|
9
|
-
from
|
10
|
-
|
11
|
-
from rclone_api
|
12
|
-
from rclone_api.
|
13
|
-
from rclone_api.
|
14
|
-
from rclone_api.
|
15
|
-
from rclone_api.
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
1
|
+
"""
|
2
|
+
Unit test file.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import subprocess
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
7
|
+
from fnmatch import fnmatch
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import Generator
|
10
|
+
|
11
|
+
from rclone_api import Dir
|
12
|
+
from rclone_api.config import Config
|
13
|
+
from rclone_api.convert import convert_to_filestr_list, convert_to_str
|
14
|
+
from rclone_api.dir_listing import DirListing
|
15
|
+
from rclone_api.exec import RcloneExec
|
16
|
+
from rclone_api.file import File
|
17
|
+
from rclone_api.filelist import FileList
|
18
|
+
from rclone_api.remote import Remote
|
19
|
+
from rclone_api.rpath import RPath
|
20
|
+
from rclone_api.util import get_rclone_exe, to_path
|
21
|
+
from rclone_api.walk import walk
|
22
|
+
|
23
|
+
|
24
|
+
class Rclone:
|
25
|
+
def __init__(
|
26
|
+
self, rclone_conf: Path | Config, rclone_exe: Path | None = None
|
27
|
+
) -> None:
|
28
|
+
if isinstance(rclone_conf, Path):
|
29
|
+
if not rclone_conf.exists():
|
30
|
+
raise ValueError(f"Rclone config file not found: {rclone_conf}")
|
31
|
+
self._exec = RcloneExec(rclone_conf, get_rclone_exe(rclone_exe))
|
32
|
+
|
33
|
+
def _run(self, cmd: list[str], check: bool = True) -> subprocess.CompletedProcess:
|
34
|
+
return self._exec.execute(cmd, check=check)
|
35
|
+
|
36
|
+
def ls(
|
37
|
+
self,
|
38
|
+
path: Dir | Remote | str,
|
39
|
+
max_depth: int | None = None,
|
40
|
+
glob: str | None = None,
|
41
|
+
) -> DirListing:
|
42
|
+
"""List files in the given path.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
path: Remote path or Remote object to list
|
46
|
+
max_depth: Maximum recursion depth (0 means no recursion)
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
List of File objects found at the path
|
50
|
+
"""
|
51
|
+
|
52
|
+
if isinstance(path, str):
|
53
|
+
path = Dir(
|
54
|
+
to_path(path, self)
|
55
|
+
) # assume it's a directory if ls is being called.
|
56
|
+
|
57
|
+
cmd = ["lsjson"]
|
58
|
+
if max_depth is not None:
|
59
|
+
cmd.append("--recursive")
|
60
|
+
if max_depth > -1:
|
61
|
+
cmd.append("--max-depth")
|
62
|
+
cmd.append(str(max_depth))
|
63
|
+
cmd.append(str(path))
|
64
|
+
remote = path.remote if isinstance(path, Dir) else path
|
65
|
+
assert isinstance(remote, Remote)
|
66
|
+
|
67
|
+
cp = self._run(cmd, check=True)
|
68
|
+
text = cp.stdout
|
69
|
+
parent_path: str | None = None
|
70
|
+
if isinstance(path, Dir):
|
71
|
+
parent_path = path.path.path
|
72
|
+
paths: list[RPath] = RPath.from_json_str(text, remote, parent_path=parent_path)
|
73
|
+
# print(parent_path)
|
74
|
+
for o in paths:
|
75
|
+
o.set_rclone(self)
|
76
|
+
|
77
|
+
# do we have a glob pattern?
|
78
|
+
if glob is not None:
|
79
|
+
paths = [p for p in paths if fnmatch(p.path, glob)]
|
80
|
+
return DirListing(paths)
|
81
|
+
|
82
|
+
def listremotes(self) -> list[Remote]:
|
83
|
+
cmd = ["listremotes"]
|
84
|
+
cp = self._run(cmd)
|
85
|
+
text: str = cp.stdout
|
86
|
+
tmp = text.splitlines()
|
87
|
+
tmp = [t.strip() for t in tmp]
|
88
|
+
# strip out ":" from the end
|
89
|
+
tmp = [t.replace(":", "") for t in tmp]
|
90
|
+
out = [Remote(name=t, rclone=self) for t in tmp]
|
91
|
+
return out
|
92
|
+
|
93
|
+
def walk(
|
94
|
+
self, path: Dir | Remote | str, max_depth: int = -1
|
95
|
+
) -> Generator[DirListing, None, None]:
|
96
|
+
"""Walk through the given path recursively.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
path: Remote path or Remote object to walk through
|
100
|
+
max_depth: Maximum depth to traverse (-1 for unlimited)
|
101
|
+
|
102
|
+
Yields:
|
103
|
+
DirListing: Directory listing for each directory encountered
|
104
|
+
"""
|
105
|
+
if isinstance(path, Dir):
|
106
|
+
# Create a Remote object for the path
|
107
|
+
remote = path.remote
|
108
|
+
rpath = RPath(
|
109
|
+
remote=remote,
|
110
|
+
path=path.path.path,
|
111
|
+
name=path.path.name,
|
112
|
+
size=0,
|
113
|
+
mime_type="inode/directory",
|
114
|
+
mod_time="",
|
115
|
+
is_dir=True,
|
116
|
+
)
|
117
|
+
rpath.set_rclone(self)
|
118
|
+
dir_obj = Dir(rpath)
|
119
|
+
elif isinstance(path, str):
|
120
|
+
dir_obj = Dir(to_path(path, self))
|
121
|
+
elif isinstance(path, Remote):
|
122
|
+
dir_obj = Dir(path)
|
123
|
+
else:
|
124
|
+
assert f"Invalid type for path: {type(path)}"
|
125
|
+
|
126
|
+
yield from walk(dir_obj, max_depth=max_depth)
|
127
|
+
|
128
|
+
def copyfile(self, src: File | str, dst: File | str) -> None:
|
129
|
+
"""Copy a single file from source to destination.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
src: Source file path (including remote if applicable)
|
133
|
+
dst: Destination file path (including remote if applicable)
|
134
|
+
|
135
|
+
Raises:
|
136
|
+
subprocess.CalledProcessError: If the copy operation fails
|
137
|
+
"""
|
138
|
+
src = src if isinstance(src, str) else str(src.path)
|
139
|
+
dst = dst if isinstance(dst, str) else str(dst.path)
|
140
|
+
cmd_list: list[str] = ["copyto", src, dst]
|
141
|
+
self._run(cmd_list)
|
142
|
+
|
143
|
+
def copyfiles(self, filelist: dict[File, File] | dict[str, str]) -> None:
|
144
|
+
"""Copy multiple files from source to destination.
|
145
|
+
|
146
|
+
Warning - slow.
|
147
|
+
|
148
|
+
Args:
|
149
|
+
payload: Dictionary of source and destination file paths
|
150
|
+
"""
|
151
|
+
str_dict: dict[str, str] = {}
|
152
|
+
for src, dst in filelist.items():
|
153
|
+
src = src if isinstance(src, str) else str(src.path)
|
154
|
+
dst = dst if isinstance(dst, str) else str(dst.path)
|
155
|
+
str_dict[src] = dst
|
156
|
+
|
157
|
+
with ThreadPoolExecutor(max_workers=64) as executor:
|
158
|
+
for src, dst in str_dict.items(): # warning - slow
|
159
|
+
cmd_list: list[str] = ["copyto", src, dst]
|
160
|
+
# self._run(cmd_list)
|
161
|
+
executor.submit(self._run, cmd_list)
|
162
|
+
|
163
|
+
def copy(
|
164
|
+
self, src: Dir | str, dst: Dir | str, filelist: FileList | None = None
|
165
|
+
) -> subprocess.CompletedProcess:
|
166
|
+
"""Copy files from source to destination.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
src: Source directory
|
170
|
+
dst: Destination directory
|
171
|
+
"""
|
172
|
+
# src_dir = src.path.path
|
173
|
+
# dst_dir = dst.path.path
|
174
|
+
src_dir = convert_to_str(src)
|
175
|
+
dst_dir = convert_to_str(dst)
|
176
|
+
cmd_list: list[str] = ["copy", src_dir, dst_dir]
|
177
|
+
return self._run(cmd_list)
|
178
|
+
|
179
|
+
def purge(self, path: Dir | str) -> subprocess.CompletedProcess:
|
180
|
+
"""Purge a directory"""
|
181
|
+
# path should always be a string
|
182
|
+
path = path if isinstance(path, str) else str(path.path)
|
183
|
+
cmd_list: list[str] = ["purge", str(path)]
|
184
|
+
return self._run(cmd_list)
|
185
|
+
|
186
|
+
def deletefiles(
|
187
|
+
self, files: str | File | list[str] | list[File]
|
188
|
+
) -> subprocess.CompletedProcess:
|
189
|
+
"""Delete a directory"""
|
190
|
+
payload: list[str] = convert_to_filestr_list(files)
|
191
|
+
cmd_list: list[str] = ["delete"] + payload
|
192
|
+
return self._run(cmd_list)
|
193
|
+
|
194
|
+
def exists(self, path: Dir | Remote | str | File) -> bool:
|
195
|
+
"""Check if a file or directory exists."""
|
196
|
+
arg: str = convert_to_str(path)
|
197
|
+
assert isinstance(arg, str)
|
198
|
+
try:
|
199
|
+
self.ls(arg)
|
200
|
+
return True
|
201
|
+
except subprocess.CalledProcessError:
|
202
|
+
return False
|
203
|
+
|
204
|
+
def is_synced(self, src: str | Dir, dst: str | Dir) -> bool:
|
205
|
+
"""Check if two directories are in sync."""
|
206
|
+
src = convert_to_str(src)
|
207
|
+
dst = convert_to_str(dst)
|
208
|
+
cmd_list: list[str] = ["check", str(src), str(dst)]
|
209
|
+
try:
|
210
|
+
self._run(cmd_list)
|
211
|
+
return True
|
212
|
+
except subprocess.CalledProcessError:
|
213
|
+
return False
|
214
|
+
|
215
|
+
def copy_dir(
|
216
|
+
self, src: str | Dir, dst: str | Dir, args: list[str] | None = None
|
217
|
+
) -> subprocess.CompletedProcess:
|
218
|
+
"""Copy a directory from source to destination."""
|
219
|
+
# convert src to str, also dst
|
220
|
+
src = convert_to_str(src)
|
221
|
+
dst = convert_to_str(dst)
|
222
|
+
cmd_list: list[str] = ["copy", src, dst]
|
223
|
+
if args is not None:
|
224
|
+
cmd_list += args
|
225
|
+
return self._run(cmd_list)
|
226
|
+
|
227
|
+
def copy_remote(
|
228
|
+
self, src: Remote, dst: Remote, args: list[str] | None = None
|
229
|
+
) -> subprocess.CompletedProcess:
|
230
|
+
"""Copy a remote to another remote."""
|
231
|
+
cmd_list: list[str] = ["copy", str(src), str(dst)]
|
232
|
+
if args is not None:
|
233
|
+
cmd_list += args
|
234
|
+
return self._run(cmd_list)
|
rclone_api/rpath.py
CHANGED
@@ -19,9 +19,6 @@ class RPath:
|
|
19
19
|
) -> None:
|
20
20
|
from rclone_api.rclone import Rclone
|
21
21
|
|
22
|
-
if "dst:" in path:
|
23
|
-
raise ValueError(f"Invalid path: {path}")
|
24
|
-
|
25
22
|
self.remote = remote
|
26
23
|
self.path = path
|
27
24
|
self.name = name
|
@@ -39,11 +36,16 @@ class RPath:
|
|
39
36
|
self.rclone = rclone
|
40
37
|
|
41
38
|
@staticmethod
|
42
|
-
def from_dict(
|
39
|
+
def from_dict(
|
40
|
+
data: dict, remote: Remote, parent_path: str | None = None
|
41
|
+
) -> "RPath":
|
43
42
|
"""Create a File from a dictionary."""
|
43
|
+
path = data["Path"]
|
44
|
+
if parent_path is not None:
|
45
|
+
path = f"{parent_path}/{path}"
|
44
46
|
return RPath(
|
45
47
|
remote,
|
46
|
-
|
48
|
+
path,
|
47
49
|
data["Name"],
|
48
50
|
data["Size"],
|
49
51
|
data["MimeType"],
|
@@ -53,21 +55,25 @@ class RPath:
|
|
53
55
|
)
|
54
56
|
|
55
57
|
@staticmethod
|
56
|
-
def from_array(
|
58
|
+
def from_array(
|
59
|
+
data: list[dict], remote: Remote, parent_path: str | None = None
|
60
|
+
) -> list["RPath"]:
|
57
61
|
"""Create a File from a dictionary."""
|
58
62
|
out: list[RPath] = []
|
59
63
|
for d in data:
|
60
|
-
file: RPath = RPath.from_dict(d, remote)
|
64
|
+
file: RPath = RPath.from_dict(d, remote, parent_path)
|
61
65
|
out.append(file)
|
62
66
|
return out
|
63
67
|
|
64
68
|
@staticmethod
|
65
|
-
def from_json_str(
|
69
|
+
def from_json_str(
|
70
|
+
json_str: str, remote: Remote, parent_path: str | None = None
|
71
|
+
) -> list["RPath"]:
|
66
72
|
"""Create a File from a JSON string."""
|
67
73
|
json_obj = json.loads(json_str)
|
68
74
|
if isinstance(json_obj, dict):
|
69
|
-
return [RPath.from_dict(json_obj, remote)]
|
70
|
-
return RPath.from_array(json_obj, remote)
|
75
|
+
return [RPath.from_dict(json_obj, remote, parent_path)]
|
76
|
+
return RPath.from_array(json_obj, remote, parent_path)
|
71
77
|
|
72
78
|
def to_json(self) -> dict:
|
73
79
|
return {
|
@@ -82,3 +88,8 @@ class RPath:
|
|
82
88
|
|
83
89
|
def __str__(self) -> str:
|
84
90
|
return f"{self.remote.name}:{self.path}"
|
91
|
+
|
92
|
+
def __repr__(self):
|
93
|
+
data = self.to_json()
|
94
|
+
data["Remote"] = self.remote.name
|
95
|
+
return json.dumps(data)
|
rclone_api/util.py
CHANGED
@@ -5,10 +5,10 @@ from pathlib import Path
|
|
5
5
|
from tempfile import TemporaryDirectory
|
6
6
|
from typing import Any
|
7
7
|
|
8
|
+
from rclone_api.config import Config
|
8
9
|
from rclone_api.dir import Dir
|
9
10
|
from rclone_api.remote import Remote
|
10
11
|
from rclone_api.rpath import RPath
|
11
|
-
from rclone_api.types import Config
|
12
12
|
|
13
13
|
# from .rclone import Rclone
|
14
14
|
|
@@ -25,7 +25,7 @@ def to_path(item: Dir | Remote | str, rclone: Any) -> RPath:
|
|
25
25
|
remote_name = parts[0]
|
26
26
|
path = ":".join(parts[1:])
|
27
27
|
remote = Remote(name=remote_name, rclone=rclone)
|
28
|
-
|
28
|
+
out = RPath(
|
29
29
|
remote=remote,
|
30
30
|
path=path,
|
31
31
|
name="",
|
@@ -34,10 +34,12 @@ def to_path(item: Dir | Remote | str, rclone: Any) -> RPath:
|
|
34
34
|
mod_time="",
|
35
35
|
is_dir=True,
|
36
36
|
)
|
37
|
+
out.set_rclone(rclone)
|
38
|
+
return out
|
37
39
|
elif isinstance(item, Dir):
|
38
40
|
return item.path
|
39
41
|
elif isinstance(item, Remote):
|
40
|
-
|
42
|
+
out = RPath(
|
41
43
|
remote=item,
|
42
44
|
path=str(item),
|
43
45
|
name=str(item),
|
@@ -46,6 +48,8 @@ def to_path(item: Dir | Remote | str, rclone: Any) -> RPath:
|
|
46
48
|
mod_time="",
|
47
49
|
is_dir=True,
|
48
50
|
)
|
51
|
+
out.set_rclone(rclone)
|
52
|
+
return out
|
49
53
|
else:
|
50
54
|
raise ValueError(f"Invalid type for item: {type(item)}")
|
51
55
|
|
@@ -71,6 +75,7 @@ def rclone_execute(
|
|
71
75
|
cmd: list[str],
|
72
76
|
rclone_conf: Path | Config,
|
73
77
|
rclone_exe: Path,
|
78
|
+
check: bool,
|
74
79
|
verbose: bool | None = None,
|
75
80
|
) -> subprocess.CompletedProcess:
|
76
81
|
tempdir: TemporaryDirectory | None = None
|
@@ -90,8 +95,16 @@ def rclone_execute(
|
|
90
95
|
cmd_str = subprocess.list2cmdline(cmd)
|
91
96
|
print(f"Running: {cmd_str}")
|
92
97
|
cp = subprocess.run(
|
93
|
-
cmd, capture_output=True, encoding="utf-8", check=
|
98
|
+
cmd, capture_output=True, encoding="utf-8", check=check, shell=False
|
94
99
|
)
|
100
|
+
if cp.returncode != 0:
|
101
|
+
cmd_str = subprocess.list2cmdline(cmd)
|
102
|
+
print(
|
103
|
+
f"Error running: {cmd_str}, returncode: {cp.returncode}\n{cp.stdout}\n{cp.stderr}"
|
104
|
+
)
|
105
|
+
raise subprocess.CalledProcessError(
|
106
|
+
cp.returncode, cmd, cp.stdout, cp.stderr
|
107
|
+
)
|
95
108
|
return cp
|
96
109
|
finally:
|
97
110
|
if tempdir:
|
rclone_api/walk.py
CHANGED
@@ -1,87 +1,70 @@
|
|
1
|
-
from
|
2
|
-
from
|
1
|
+
from queue import Queue
|
2
|
+
from threading import Thread
|
3
3
|
from typing import Generator
|
4
4
|
|
5
5
|
from rclone_api import Dir
|
6
6
|
from rclone_api.dir_listing import DirListing
|
7
7
|
from rclone_api.remote import Remote
|
8
8
|
|
9
|
+
_MAX_OUT_QUEUE_SIZE = 50
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
|
12
|
+
def _walk_runner(
|
13
|
+
queue: Queue[Dir], max_depth: int, out_queue: Queue[DirListing | None]
|
14
|
+
) -> None:
|
15
|
+
try:
|
16
|
+
while not queue.empty():
|
17
|
+
current_dir = queue.get()
|
18
|
+
dirlisting = current_dir.ls()
|
19
|
+
out_queue.put(dirlisting)
|
20
|
+
dirs = dirlisting.dirs
|
21
|
+
|
22
|
+
if max_depth != 0 and len(dirs) > 0:
|
23
|
+
for child in dirs:
|
24
|
+
queue.put(child)
|
25
|
+
if max_depth < 0:
|
26
|
+
continue
|
27
|
+
if max_depth > 0:
|
28
|
+
max_depth -= 1
|
29
|
+
out_queue.put(None)
|
30
|
+
except KeyboardInterrupt:
|
31
|
+
import _thread
|
32
|
+
|
33
|
+
out_queue.put(None)
|
34
|
+
|
35
|
+
_thread.interrupt_main()
|
36
|
+
|
37
|
+
|
38
|
+
def walk(dir: Dir | Remote, max_depth: int = -1) -> Generator[DirListing, None, None]:
|
13
39
|
"""Walk through the given directory recursively.
|
14
40
|
|
15
41
|
Args:
|
16
42
|
dir: Directory or Remote to walk through
|
17
43
|
max_depth: Maximum depth to traverse (-1 for unlimited)
|
18
|
-
max_workers: Maximum number of concurrent workers
|
19
44
|
|
20
45
|
Yields:
|
21
46
|
DirListing: Directory listing for each directory encountered
|
22
47
|
"""
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
results: Queue[DirListing | Exception] = Queue()
|
48
|
+
try:
|
49
|
+
# Convert Remote to Dir if needed
|
50
|
+
if isinstance(dir, Remote):
|
51
|
+
dir = Dir(dir)
|
28
52
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
# Add timeout to allow checking for sentinel value
|
33
|
-
try:
|
34
|
-
current_dir, depth = pending.get(timeout=0.1)
|
35
|
-
except Empty:
|
36
|
-
continue
|
53
|
+
in_queue: Queue[Dir] = Queue()
|
54
|
+
out_queue: Queue[DirListing] = Queue(maxsize=_MAX_OUT_QUEUE_SIZE)
|
55
|
+
in_queue.put(dir)
|
37
56
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
57
|
+
# Start worker thread
|
58
|
+
worker = Thread(
|
59
|
+
target=_walk_runner, args=(in_queue, max_depth, out_queue), daemon=True
|
60
|
+
)
|
61
|
+
worker.start()
|
42
62
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
if max_depth == -1 or depth < max_depth:
|
47
|
-
for d in listing.dirs:
|
48
|
-
pending.put((d, depth + 1))
|
49
|
-
|
50
|
-
pending.task_done()
|
51
|
-
except Exception as e:
|
52
|
-
results.put(e)
|
53
|
-
pending.task_done()
|
63
|
+
while dirlisting := out_queue.get():
|
64
|
+
if dirlisting is None:
|
54
65
|
break
|
55
|
-
|
56
|
-
|
57
|
-
# Start workers
|
58
|
-
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
59
|
-
workers = [executor.submit(worker) for _ in range(max_workers)]
|
60
|
-
|
61
|
-
# Start walking
|
62
|
-
pending.put((dir, 0))
|
63
|
-
|
64
|
-
# Process results while workers are running
|
65
|
-
completed = 0
|
66
|
-
while completed < max_workers:
|
67
|
-
try:
|
68
|
-
result = results.get(timeout=0.1)
|
69
|
-
if isinstance(result, Exception):
|
70
|
-
# Propagate exception
|
71
|
-
raise result
|
72
|
-
yield result
|
73
|
-
except Empty:
|
74
|
-
# Check if any workers have completed
|
75
|
-
completed = sum(1 for w in workers if w.done())
|
76
|
-
continue
|
77
|
-
|
78
|
-
# Signal workers to stop
|
79
|
-
for _ in range(max_workers):
|
80
|
-
pending.put((None, 0))
|
66
|
+
yield dirlisting
|
81
67
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
if isinstance(result, Exception):
|
86
|
-
raise result
|
87
|
-
yield result
|
68
|
+
worker.join()
|
69
|
+
except KeyboardInterrupt:
|
70
|
+
pass
|
@@ -0,0 +1,20 @@
|
|
1
|
+
rclone_api/__init__.py,sha256=BqlqMsxNo5YkSo0ov7CRM5WsyAh2yJQa9Fpodc-rQ7I,344
|
2
|
+
rclone_api/cli.py,sha256=dibfAZIh0kXWsBbfp3onKLjyZXo54mTzDjUdzJlDlWo,231
|
3
|
+
rclone_api/config.py,sha256=tP6cU9DnCCEIRc_KP9HPur1jFLLg2QGFSxNwFm6_MVw,118
|
4
|
+
rclone_api/convert.py,sha256=Mx9Qo7zhkOedJd8LdhPvNGHp8znJzOk4f_2KWnoGc78,1012
|
5
|
+
rclone_api/dir.py,sha256=xUV4i9_E7QHrqHld2IPBlon1CGaAp8qQYMKfJTyVvoQ,2088
|
6
|
+
rclone_api/dir_listing.py,sha256=8t5Jx9ZVOJPqGKTJbWaES6bjgogUT2bnpbPVWwK1Fcs,1124
|
7
|
+
rclone_api/exec.py,sha256=djQrWtziTIroEb5YJQxDyTJDGK39xN0fWtv0HJGnjgk,498
|
8
|
+
rclone_api/file.py,sha256=D02iHJW1LhfOiM_R_yPHP8_ApnDiYrkuraVcrV8-qkw,1246
|
9
|
+
rclone_api/filelist.py,sha256=xbiusvNgaB_b_kQOZoHMJJxn6TWGtPrWd2J042BI28o,767
|
10
|
+
rclone_api/rclone.py,sha256=BQaT11O0eyCLxGrXDNJ7fXeVxgmg4j5UsDSjoTuRPfg,8267
|
11
|
+
rclone_api/remote.py,sha256=c9hlRKBCg1BFB9MCINaQIoCg10qyAkeqiS4brl8ce-8,343
|
12
|
+
rclone_api/rpath.py,sha256=8ZA_1wxWtskwcy0I8V2VbjKDmzPkiWd8Q2JQSvh-sYE,2586
|
13
|
+
rclone_api/util.py,sha256=24rHFv6NqgXPaqhpy4spGyVcoN2GjrDwEWtoD7LRllk,3345
|
14
|
+
rclone_api/walk.py,sha256=J78-bY2AhNpt2ICsI5LqmXRE7oC6wVDoKoicIoU6XMg,1953
|
15
|
+
rclone_api/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
|
16
|
+
rclone_api-1.0.15.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
|
17
|
+
rclone_api-1.0.15.dist-info/METADATA,sha256=WbUA2T_9meHMVxa5XtK14ypTSRDG8KxJbH2t3J4VDYM,1375
|
18
|
+
rclone_api-1.0.15.dist-info/WHEEL,sha256=9Hm2OB-j1QcCUq9Jguht7ayGIIZBRTdOXD1qg9cCgPM,109
|
19
|
+
rclone_api-1.0.15.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
|
20
|
+
rclone_api-1.0.15.dist-info/RECORD,,
|
@@ -1,18 +0,0 @@
|
|
1
|
-
rclone_api/__init__.py,sha256=YH7KQaPwUiJWJiRf0NRKD7XHhMXsxWdXDjt9WLSwdjA,265
|
2
|
-
rclone_api/cli.py,sha256=dibfAZIh0kXWsBbfp3onKLjyZXo54mTzDjUdzJlDlWo,231
|
3
|
-
rclone_api/config.py,sha256=UujjDNcpwgipgdyFPMhC3zikvlsrurUvvZiwXm5NlPg,471
|
4
|
-
rclone_api/dir.py,sha256=tu0-Yy1s3O1MVY7Nkb6NOh6t9dXJ9DQvqOZVlWIexrI,1629
|
5
|
-
rclone_api/dir_listing.py,sha256=NWleKHCCRW7_eh9JfRwE6r3QbjmiHD5ZEGQcd2vm4uY,458
|
6
|
-
rclone_api/file.py,sha256=nJwJT7v2KwY_8g7UB3Y7O6pFfHn3nri9dJ3GS-cN3EE,874
|
7
|
-
rclone_api/rclone.py,sha256=upMqUxYKC3LCjHlhI66_IdOD1t5PKo7V64sSdw0vAlk,3074
|
8
|
-
rclone_api/remote.py,sha256=c9hlRKBCg1BFB9MCINaQIoCg10qyAkeqiS4brl8ce-8,343
|
9
|
-
rclone_api/rpath.py,sha256=QHD5zDjDIkivs3kCRYtTL8mP-DspXIvBVYWHSf_Y6Rg,2263
|
10
|
-
rclone_api/types.py,sha256=Yp15HrjwZonSQhE323R0WP7fA4NWqKjAfM7z3OwHpWI,518
|
11
|
-
rclone_api/util.py,sha256=xYUj0B9W5HDauTHh7Z8cnEQsTEuXvRcVNLtk3ZanlGM,2894
|
12
|
-
rclone_api/walk.py,sha256=3GKnu9P5aPNiftfoi52U7wo1wixZxwxdqqS0_IlMGCE,2816
|
13
|
-
rclone_api/assets/example.txt,sha256=lTBovRjiz0_TgtAtbA1C5hNi2ffbqnNPqkKg6UiKCT8,54
|
14
|
-
rclone_api-1.0.13.dist-info/LICENSE,sha256=b6pOoifSXiUaz_lDS84vWlG3fr4yUKwB8fzkrH9R8bQ,1064
|
15
|
-
rclone_api-1.0.13.dist-info/METADATA,sha256=yEL4GNzAc-CwJt-rJVmDAl08L13RqytSSs1ZQR4KMA0,1375
|
16
|
-
rclone_api-1.0.13.dist-info/WHEEL,sha256=9Hm2OB-j1QcCUq9Jguht7ayGIIZBRTdOXD1qg9cCgPM,109
|
17
|
-
rclone_api-1.0.13.dist-info/top_level.txt,sha256=EvZ7uuruUpe9RiUyEp25d1Keq7PWYNT0O_-mr8FCG5g,11
|
18
|
-
rclone_api-1.0.13.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|