rclone-api 1.0.13__py2.py3-none-any.whl → 1.0.15__py2.py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|