rclone-api 1.0.13__tar.gz → 1.0.14__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- rclone_api-1.0.14/.github/workflows/lint.yml +35 -0
- rclone_api-1.0.14/.github/workflows/push_macos.yml +32 -0
- rclone_api-1.0.14/.github/workflows/push_ubuntu.yml +32 -0
- rclone_api-1.0.14/.github/workflows/push_win.yml +34 -0
- {rclone_api-1.0.13/src/rclone_api.egg-info → rclone_api-1.0.14}/PKG-INFO +1 -1
- {rclone_api-1.0.13 → rclone_api-1.0.14}/pyproject.toml +1 -1
- {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/__init__.py +1 -1
- rclone_api-1.0.14/src/rclone_api/config.py +8 -0
- rclone_api-1.0.14/src/rclone_api/convert.py +31 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/dir.py +16 -0
- rclone_api-1.0.14/src/rclone_api/dir_listing.py +40 -0
- rclone_api-1.0.13/src/rclone_api/config.py → rclone_api-1.0.14/src/rclone_api/exec.py +1 -1
- {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/file.py +15 -0
- rclone_api-1.0.14/src/rclone_api/rclone.py +214 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/rpath.py +21 -10
- {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/util.py +16 -4
- rclone_api-1.0.14/src/rclone_api/walk.py +70 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14/src/rclone_api.egg-info}/PKG-INFO +1 -1
- {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api.egg-info/SOURCES.txt +11 -2
- rclone_api-1.0.14/tests/test_copy.py +106 -0
- rclone_api-1.0.14/tests/test_is_synced.py +75 -0
- rclone_api-1.0.13/tests/test_simple.py → rclone_api-1.0.14/tests/test_ls.py +21 -23
- rclone_api-1.0.14/tests/test_remotes.py +70 -0
- rclone_api-1.0.14/tests/test_walk.py +68 -0
- rclone_api-1.0.13/src/rclone_api/dir_listing.py +0 -14
- rclone_api-1.0.13/src/rclone_api/rclone.py +0 -97
- rclone_api-1.0.13/src/rclone_api/types.py +0 -24
- rclone_api-1.0.13/src/rclone_api/walk.py +0 -87
- {rclone_api-1.0.13 → rclone_api-1.0.14}/.aiderignore +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/.gitignore +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/.pylintrc +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/.vscode/launch.json +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/.vscode/settings.json +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/.vscode/tasks.json +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/LICENSE +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/MANIFEST.in +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/README.md +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/clean +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/install +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/lint +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/requirements.testing.txt +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/setup.cfg +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/setup.py +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api.egg-info/requires.txt +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/test +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/tox.ini +0 -0
- {rclone_api-1.0.13 → rclone_api-1.0.14}/upload_package.sh +0 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
name: Linting
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
test:
|
7
|
+
runs-on: ${{ matrix.os }}
|
8
|
+
strategy:
|
9
|
+
matrix:
|
10
|
+
python-version: [3.11]
|
11
|
+
os: [ubuntu-latest]
|
12
|
+
steps:
|
13
|
+
- uses: actions/checkout@v4
|
14
|
+
- name: Set up Python ${{ matrix.python-version }}
|
15
|
+
uses: actions/setup-python@v5
|
16
|
+
with:
|
17
|
+
python-version: ${{ matrix.python-version }}
|
18
|
+
- uses: actions/cache@v4
|
19
|
+
name: Configure pip caching
|
20
|
+
with:
|
21
|
+
path: ~/.cache/pip
|
22
|
+
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
23
|
+
restore-keys: |
|
24
|
+
${{ runner.os }}-pip-
|
25
|
+
|
26
|
+
- name: Install uv
|
27
|
+
uses: astral-sh/setup-uv@v5
|
28
|
+
|
29
|
+
- name: Install
|
30
|
+
run: |
|
31
|
+
python -m pip install uv
|
32
|
+
./install
|
33
|
+
- name: Run Linting
|
34
|
+
run: |
|
35
|
+
./lint
|
@@ -0,0 +1,32 @@
|
|
1
|
+
name: MacOS_Tests
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
test:
|
7
|
+
runs-on: ${{ matrix.os }}
|
8
|
+
strategy:
|
9
|
+
matrix:
|
10
|
+
python-version: [3.11]
|
11
|
+
os: [macos-latest]
|
12
|
+
steps:
|
13
|
+
- uses: actions/checkout@v4
|
14
|
+
- name: Set up Python ${{ matrix.python-version }}
|
15
|
+
uses: actions/setup-python@v5
|
16
|
+
with:
|
17
|
+
python-version: ${{ matrix.python-version }}
|
18
|
+
- uses: actions/cache@v4
|
19
|
+
name: Configure pip caching
|
20
|
+
with:
|
21
|
+
path: ~/.cache/pip
|
22
|
+
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
23
|
+
restore-keys: |
|
24
|
+
${{ runner.os }}-pip-
|
25
|
+
- name: Install uv
|
26
|
+
uses: astral-sh/setup-uv@v5
|
27
|
+
- name: Install
|
28
|
+
run: |
|
29
|
+
./install
|
30
|
+
- name: Run Tests
|
31
|
+
run: |
|
32
|
+
./test
|
@@ -0,0 +1,32 @@
|
|
1
|
+
name: Ubuntu_Tests
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
test:
|
7
|
+
runs-on: ${{ matrix.os }}
|
8
|
+
strategy:
|
9
|
+
matrix:
|
10
|
+
python-version: [3.11]
|
11
|
+
os: [ubuntu-latest]
|
12
|
+
steps:
|
13
|
+
- uses: actions/checkout@v4
|
14
|
+
- name: Set up Python ${{ matrix.python-version }}
|
15
|
+
uses: actions/setup-python@v5
|
16
|
+
with:
|
17
|
+
python-version: ${{ matrix.python-version }}
|
18
|
+
- uses: actions/cache@v4
|
19
|
+
name: Configure pip caching
|
20
|
+
with:
|
21
|
+
path: ~/.cache/pip
|
22
|
+
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
23
|
+
restore-keys: |
|
24
|
+
${{ runner.os }}-pip-
|
25
|
+
- name: Install uv
|
26
|
+
uses: astral-sh/setup-uv@v5
|
27
|
+
- name: Install
|
28
|
+
run: |
|
29
|
+
./install
|
30
|
+
- name: Run Tests
|
31
|
+
run: |
|
32
|
+
./test
|
@@ -0,0 +1,34 @@
|
|
1
|
+
name: Win_Tests
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
test:
|
7
|
+
runs-on: ${{ matrix.os }}
|
8
|
+
strategy:
|
9
|
+
matrix:
|
10
|
+
python-version: [3.11]
|
11
|
+
os: [windows-latest]
|
12
|
+
steps:
|
13
|
+
- uses: actions/checkout@v4
|
14
|
+
- name: Set up Python ${{ matrix.python-version }}
|
15
|
+
uses: actions/setup-python@v5
|
16
|
+
with:
|
17
|
+
python-version: ${{ matrix.python-version }}
|
18
|
+
- uses: actions/cache@v4
|
19
|
+
name: Configure pip caching
|
20
|
+
with:
|
21
|
+
path: ~/.cache/pip
|
22
|
+
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
|
23
|
+
restore-keys: |
|
24
|
+
${{ runner.os }}-pip-
|
25
|
+
- name: Install uv
|
26
|
+
uses: astral-sh/setup-uv@v5
|
27
|
+
- name: Install
|
28
|
+
run: |
|
29
|
+
./install
|
30
|
+
shell: bash
|
31
|
+
- name: Run Tests
|
32
|
+
run: |
|
33
|
+
./test
|
34
|
+
shell: bash
|
@@ -1,9 +1,9 @@
|
|
1
|
+
from .config import Config
|
1
2
|
from .dir import Dir
|
2
3
|
from .dir_listing import DirListing
|
3
4
|
from .file import File
|
4
5
|
from .rclone import Rclone
|
5
6
|
from .remote import Remote
|
6
7
|
from .rpath import RPath
|
7
|
-
from .types import Config
|
8
8
|
|
9
9
|
__all__ = ["Rclone", "File", "Config", "Remote", "Dir", "RPath", "DirListing"]
|
@@ -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)}")
|
@@ -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
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
from rclone_api.rpath import RPath
|
4
|
+
|
5
|
+
|
6
|
+
class DirListing:
|
7
|
+
"""Remote file dataclass."""
|
8
|
+
|
9
|
+
def __init__(self, dirs_and_files: list[RPath]) -> None:
|
10
|
+
from rclone_api.dir import Dir
|
11
|
+
from rclone_api.file import File
|
12
|
+
|
13
|
+
self.dirs: list[Dir] = [Dir(d) for d in dirs_and_files if d.is_dir]
|
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)
|
@@ -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
|
|
@@ -28,5 +34,14 @@ class File:
|
|
28
34
|
result = self.path.rclone._run(["cat", self.path.path])
|
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
|
@@ -0,0 +1,214 @@
|
|
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.remote import Remote
|
18
|
+
from rclone_api.rpath import RPath
|
19
|
+
from rclone_api.util import get_rclone_exe, to_path
|
20
|
+
from rclone_api.walk import walk
|
21
|
+
|
22
|
+
|
23
|
+
class Rclone:
|
24
|
+
def __init__(
|
25
|
+
self, rclone_conf: Path | Config, rclone_exe: Path | None = None
|
26
|
+
) -> None:
|
27
|
+
if isinstance(rclone_conf, Path):
|
28
|
+
if not rclone_conf.exists():
|
29
|
+
raise ValueError(f"Rclone config file not found: {rclone_conf}")
|
30
|
+
self._exec = RcloneExec(rclone_conf, get_rclone_exe(rclone_exe))
|
31
|
+
|
32
|
+
def _run(self, cmd: list[str]) -> subprocess.CompletedProcess:
|
33
|
+
return self._exec.execute(cmd)
|
34
|
+
|
35
|
+
def ls(
|
36
|
+
self,
|
37
|
+
path: Dir | Remote | str,
|
38
|
+
max_depth: int | None = None,
|
39
|
+
glob: str | None = None,
|
40
|
+
) -> DirListing:
|
41
|
+
"""List files in the given path.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
path: Remote path or Remote object to list
|
45
|
+
max_depth: Maximum recursion depth (0 means no recursion)
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
List of File objects found at the path
|
49
|
+
"""
|
50
|
+
|
51
|
+
if isinstance(path, str):
|
52
|
+
path = Dir(
|
53
|
+
to_path(path, self)
|
54
|
+
) # assume it's a directory if ls is being called.
|
55
|
+
|
56
|
+
cmd = ["lsjson"]
|
57
|
+
if max_depth is not None:
|
58
|
+
cmd.append("--recursive")
|
59
|
+
if max_depth > -1:
|
60
|
+
cmd.append("--max-depth")
|
61
|
+
cmd.append(str(max_depth))
|
62
|
+
cmd.append(str(path))
|
63
|
+
remote = path.remote if isinstance(path, Dir) else path
|
64
|
+
assert isinstance(remote, Remote)
|
65
|
+
|
66
|
+
cp = self._run(cmd)
|
67
|
+
text = cp.stdout
|
68
|
+
parent_path: str | None = None
|
69
|
+
if isinstance(path, Dir):
|
70
|
+
parent_path = path.path.path
|
71
|
+
paths: list[RPath] = RPath.from_json_str(text, remote, parent_path=parent_path)
|
72
|
+
# print(parent_path)
|
73
|
+
for o in paths:
|
74
|
+
o.set_rclone(self)
|
75
|
+
|
76
|
+
# do we have a glob pattern?
|
77
|
+
if glob is not None:
|
78
|
+
paths = [p for p in paths if fnmatch(p.path, glob)]
|
79
|
+
return DirListing(paths)
|
80
|
+
|
81
|
+
def listremotes(self) -> list[Remote]:
|
82
|
+
cmd = ["listremotes"]
|
83
|
+
cp = self._run(cmd)
|
84
|
+
text: str = cp.stdout
|
85
|
+
tmp = text.splitlines()
|
86
|
+
tmp = [t.strip() for t in tmp]
|
87
|
+
# strip out ":" from the end
|
88
|
+
tmp = [t.replace(":", "") for t in tmp]
|
89
|
+
out = [Remote(name=t, rclone=self) for t in tmp]
|
90
|
+
return out
|
91
|
+
|
92
|
+
def walk(
|
93
|
+
self, path: Dir | Remote | str, max_depth: int = -1
|
94
|
+
) -> Generator[DirListing, None, None]:
|
95
|
+
"""Walk through the given path recursively.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
path: Remote path or Remote object to walk through
|
99
|
+
max_depth: Maximum depth to traverse (-1 for unlimited)
|
100
|
+
|
101
|
+
Yields:
|
102
|
+
DirListing: Directory listing for each directory encountered
|
103
|
+
"""
|
104
|
+
if isinstance(path, Dir):
|
105
|
+
# Create a Remote object for the path
|
106
|
+
remote = path.remote
|
107
|
+
rpath = RPath(
|
108
|
+
remote=remote,
|
109
|
+
path=path.path.path,
|
110
|
+
name=path.path.name,
|
111
|
+
size=0,
|
112
|
+
mime_type="inode/directory",
|
113
|
+
mod_time="",
|
114
|
+
is_dir=True,
|
115
|
+
)
|
116
|
+
rpath.set_rclone(self)
|
117
|
+
dir_obj = Dir(rpath)
|
118
|
+
elif isinstance(path, str):
|
119
|
+
dir_obj = Dir(to_path(path, self))
|
120
|
+
elif isinstance(path, Remote):
|
121
|
+
dir_obj = Dir(path)
|
122
|
+
else:
|
123
|
+
assert f"Invalid type for path: {type(path)}"
|
124
|
+
|
125
|
+
yield from walk(dir_obj, max_depth=max_depth)
|
126
|
+
|
127
|
+
def copyfile(self, src: File | str, dst: File | str) -> None:
|
128
|
+
"""Copy a single file from source to destination.
|
129
|
+
|
130
|
+
Args:
|
131
|
+
src: Source file path (including remote if applicable)
|
132
|
+
dst: Destination file path (including remote if applicable)
|
133
|
+
|
134
|
+
Raises:
|
135
|
+
subprocess.CalledProcessError: If the copy operation fails
|
136
|
+
"""
|
137
|
+
src = src if isinstance(src, str) else str(src.path)
|
138
|
+
dst = dst if isinstance(dst, str) else str(dst.path)
|
139
|
+
cmd_list: list[str] = ["copyto", src, dst]
|
140
|
+
self._run(cmd_list)
|
141
|
+
|
142
|
+
def copyfiles(self, filelist: dict[File, File] | dict[str, str]) -> None:
|
143
|
+
"""Copy multiple files from source to destination.
|
144
|
+
|
145
|
+
Warning - slow.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
payload: Dictionary of source and destination file paths
|
149
|
+
"""
|
150
|
+
str_dict: dict[str, str] = {}
|
151
|
+
for src, dst in filelist.items():
|
152
|
+
src = src if isinstance(src, str) else str(src.path)
|
153
|
+
dst = dst if isinstance(dst, str) else str(dst.path)
|
154
|
+
str_dict[src] = dst
|
155
|
+
|
156
|
+
with ThreadPoolExecutor(max_workers=64) as executor:
|
157
|
+
for src, dst in str_dict.items(): # warning - slow
|
158
|
+
cmd_list: list[str] = ["copyto", src, dst]
|
159
|
+
# self._run(cmd_list)
|
160
|
+
executor.submit(self._run, cmd_list)
|
161
|
+
|
162
|
+
def copy(self, src: Dir, dst: Dir) -> None:
|
163
|
+
"""Copy files from source to destination.
|
164
|
+
|
165
|
+
Args:
|
166
|
+
src: Source directory
|
167
|
+
dst: Destination directory
|
168
|
+
"""
|
169
|
+
src_dir = src.path.path
|
170
|
+
dst_dir = dst.path.path
|
171
|
+
cmd_list: list[str] = ["copy", src_dir, dst_dir]
|
172
|
+
self._run(cmd_list)
|
173
|
+
|
174
|
+
def purge(self, path: Dir | str) -> None:
|
175
|
+
"""Purge a directory"""
|
176
|
+
# path should always be a string
|
177
|
+
path = path if isinstance(path, str) else str(path.path)
|
178
|
+
cmd_list: list[str] = ["purge", str(path)]
|
179
|
+
self._run(cmd_list)
|
180
|
+
|
181
|
+
def deletefiles(self, files: str | File | list[str] | list[File]) -> None:
|
182
|
+
"""Delete a directory"""
|
183
|
+
payload: list[str] = convert_to_filestr_list(files)
|
184
|
+
cmd_list: list[str] = ["delete"] + payload
|
185
|
+
self._run(cmd_list)
|
186
|
+
|
187
|
+
def exists(self, path: Dir | Remote | str | File) -> bool:
|
188
|
+
"""Check if a file or directory exists."""
|
189
|
+
arg: str = convert_to_str(path)
|
190
|
+
assert isinstance(arg, str)
|
191
|
+
try:
|
192
|
+
self.ls(arg)
|
193
|
+
return True
|
194
|
+
except subprocess.CalledProcessError:
|
195
|
+
return False
|
196
|
+
|
197
|
+
def is_synced(self, src: str | Dir, dst: str | Dir) -> bool:
|
198
|
+
"""Check if two directories are in sync."""
|
199
|
+
src = convert_to_str(src)
|
200
|
+
dst = convert_to_str(dst)
|
201
|
+
cmd_list: list[str] = ["check", str(src), str(dst)]
|
202
|
+
try:
|
203
|
+
self._run(cmd_list)
|
204
|
+
return True
|
205
|
+
except subprocess.CalledProcessError:
|
206
|
+
return False
|
207
|
+
|
208
|
+
def copy_dir(self, src: str | Dir, dst: str | Dir) -> None:
|
209
|
+
"""Copy a directory from source to destination."""
|
210
|
+
# convert src to str, also dst
|
211
|
+
src = convert_to_str(src)
|
212
|
+
dst = convert_to_str(dst)
|
213
|
+
cmd_list: list[str] = ["copy", src, dst]
|
214
|
+
self._run(cmd_list)
|
@@ -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)
|
@@ -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
|
|
@@ -90,8 +94,16 @@ def rclone_execute(
|
|
90
94
|
cmd_str = subprocess.list2cmdline(cmd)
|
91
95
|
print(f"Running: {cmd_str}")
|
92
96
|
cp = subprocess.run(
|
93
|
-
cmd, capture_output=True, encoding="utf-8", check=
|
97
|
+
cmd, capture_output=True, encoding="utf-8", check=False, shell=False
|
94
98
|
)
|
99
|
+
if cp.returncode != 0:
|
100
|
+
cmd_str = subprocess.list2cmdline(cmd)
|
101
|
+
print(
|
102
|
+
f"Error running: {cmd_str}, returncode: {cp.returncode}\n{cp.stdout}\n{cp.stderr}"
|
103
|
+
)
|
104
|
+
raise subprocess.CalledProcessError(
|
105
|
+
cp.returncode, cmd, cp.stdout, cp.stderr
|
106
|
+
)
|
95
107
|
return cp
|
96
108
|
finally:
|
97
109
|
if tempdir:
|