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 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__ = ["Rclone", "File", "Config", "Remote", "Dir", "RPath", "DirListing"]
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 RcloneExec:
10
- """Rclone execution dataclass."""
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
- return rclone_execute(cmd, self.rclone_config, self.rclone_exe)
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
- # from rclone_api.dir import Dir
2
- # from rclone_api.file import File
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)
@@ -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 pathlib import Path
7
- from typing import Generator
8
-
9
- from rclone_api import Dir
10
- from rclone_api.dir_listing import DirListing
11
- from rclone_api.remote import Remote
12
- from rclone_api.rpath import RPath
13
- from rclone_api.types import Config, RcloneExec
14
- from rclone_api.util import get_rclone_exe
15
- from rclone_api.walk import walk
16
-
17
-
18
- class Rclone:
19
- def __init__(
20
- self, rclone_conf: Path | Config, rclone_exe: Path | None = None
21
- ) -> None:
22
- if isinstance(rclone_conf, Path):
23
- if not rclone_conf.exists():
24
- raise ValueError(f"Rclone config file not found: {rclone_conf}")
25
- self._exec = RcloneExec(rclone_conf, get_rclone_exe(rclone_exe))
26
-
27
- def _run(self, cmd: list[str]) -> subprocess.CompletedProcess:
28
- return self._exec.execute(cmd)
29
-
30
- def ls(self, path: Dir | Remote | str, max_depth: int | None = None) -> DirListing:
31
- """List files in the given path.
32
-
33
- Args:
34
- path: Remote path or Remote object to list
35
- max_depth: Maximum recursion depth (0 means no recursion)
36
-
37
- Returns:
38
- List of File objects found at the path
39
- """
40
- cmd = ["lsjson"]
41
- if max_depth is not None:
42
- cmd.append("--recursive")
43
- if max_depth > -1:
44
- cmd.append("--max-depth")
45
- cmd.append(str(max_depth))
46
- cmd.append(str(path))
47
- remote = path.remote if isinstance(path, Dir) else path
48
- assert isinstance(remote, Remote)
49
-
50
- cp = self._run(cmd)
51
- text = cp.stdout
52
- paths: list[RPath] = RPath.from_json_str(text, remote)
53
- for o in paths:
54
- o.set_rclone(self)
55
- return DirListing(paths)
56
-
57
- def listremotes(self) -> list[Remote]:
58
- cmd = ["listremotes"]
59
- cp = self._run(cmd)
60
- text: str = cp.stdout
61
- tmp = text.splitlines()
62
- tmp = [t.strip() for t in tmp]
63
- # strip out ":" from the end
64
- tmp = [t.replace(":", "") for t in tmp]
65
- out = [Remote(name=t, rclone=self) for t in tmp]
66
- return out
67
-
68
- def walk(
69
- self, path: Dir | Remote, max_depth: int = -1
70
- ) -> Generator[DirListing, None, None]:
71
- """Walk through the given path recursively.
72
-
73
- Args:
74
- path: Remote path or Remote object to walk through
75
- max_depth: Maximum depth to traverse (-1 for unlimited)
76
-
77
- Yields:
78
- DirListing: Directory listing for each directory encountered
79
- """
80
- if isinstance(path, Dir):
81
- # Create a Remote object for the path
82
- remote = path.remote
83
- rpath = RPath(
84
- remote=remote,
85
- path=path.path.path,
86
- name=path.path.name,
87
- size=0,
88
- mime_type="inode/directory",
89
- mod_time="",
90
- is_dir=True,
91
- )
92
- rpath.set_rclone(self)
93
- dir_obj = Dir(rpath)
94
- else:
95
- dir_obj = Dir(path)
96
-
97
- yield from walk(dir_obj, max_depth=max_depth)
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(data: dict, remote: Remote) -> "RPath":
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
- data["Path"],
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(data: list[dict], remote: Remote) -> list["RPath"]:
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(json_str: str, remote: Remote) -> list["RPath"]:
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
- return RPath(
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
- return RPath(
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=True, shell=False
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 concurrent.futures import ThreadPoolExecutor
2
- from queue import Empty, Queue
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
- def walk(
11
- dir: Dir | Remote, max_depth: int = -1, max_workers: int = 4
12
- ) -> Generator[DirListing, None, None]:
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
- # Convert Remote to Dir if needed
24
- if isinstance(dir, Remote):
25
- dir = Dir(dir)
26
- pending: Queue[tuple[Dir | None, int]] = Queue()
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
- def worker():
30
- while True:
31
- try:
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
- # Check for sentinel value
39
- if current_dir is None:
40
- pending.task_done()
41
- break
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
- listing = current_dir.ls()
44
- results.put(listing)
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
- return None
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
- # Drain any remaining results
83
- while not results.empty():
84
- result = results.get()
85
- if isinstance(result, Exception):
86
- raise result
87
- yield result
68
+ worker.join()
69
+ except KeyboardInterrupt:
70
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: rclone_api
3
- Version: 1.0.13
3
+ Version: 1.0.15
4
4
  Summary: rclone api in python
5
5
  Home-page: https://github.com/zackees/rclone-api
6
6
  Maintainer: Zachary Vorhies
@@ -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,,