rclone-api 1.0.12__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.14/PKG-INFO +34 -0
- rclone_api-1.0.14/README.md +18 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/pyproject.toml +44 -44
- {rclone_api-1.0.12 → 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.12 → rclone_api-1.0.14}/src/rclone_api/dir.py +23 -1
- rclone_api-1.0.14/src/rclone_api/dir_listing.py +40 -0
- rclone_api-1.0.12/src/rclone_api/config.py → rclone_api-1.0.14/src/rclone_api/exec.py +1 -1
- {rclone_api-1.0.12 → rclone_api-1.0.14}/src/rclone_api/file.py +14 -2
- rclone_api-1.0.14/src/rclone_api/rclone.py +214 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/src/rclone_api/rpath.py +27 -9
- rclone_api-1.0.14/src/rclone_api/util.py +113 -0
- rclone_api-1.0.14/src/rclone_api/walk.py +70 -0
- rclone_api-1.0.14/src/rclone_api.egg-info/PKG-INFO +34 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/src/rclone_api.egg-info/SOURCES.txt +12 -2
- rclone_api-1.0.14/src/rclone_api.egg-info/requires.txt +1 -0
- 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.14/tests/test_ls.py +117 -0
- 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.12/PKG-INFO +0 -36
- rclone_api-1.0.12/README.md +0 -21
- rclone_api-1.0.12/src/rclone_api/dir_listing.py +0 -14
- rclone_api-1.0.12/src/rclone_api/rclone.py +0 -90
- rclone_api-1.0.12/src/rclone_api/types.py +0 -24
- rclone_api-1.0.12/src/rclone_api/util.py +0 -49
- rclone_api-1.0.12/src/rclone_api/walk.py +0 -87
- rclone_api-1.0.12/src/rclone_api.egg-info/PKG-INFO +0 -36
- rclone_api-1.0.12/tests/test_simple.py +0 -16
- {rclone_api-1.0.12 → rclone_api-1.0.14}/.aiderignore +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/.gitignore +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/.pylintrc +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/.vscode/launch.json +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/.vscode/settings.json +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/.vscode/tasks.json +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/LICENSE +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/MANIFEST.in +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/clean +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/install +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/lint +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/requirements.testing.txt +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/setup.cfg +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/setup.py +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/src/rclone_api/assets/example.txt +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/src/rclone_api/cli.py +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/src/rclone_api/remote.py +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/src/rclone_api.egg-info/dependency_links.txt +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/src/rclone_api.egg-info/top_level.txt +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/test +0 -0
- {rclone_api-1.0.12 → rclone_api-1.0.14}/tox.ini +0 -0
- {rclone_api-1.0.12 → 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
|
@@ -0,0 +1,34 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: rclone_api
|
3
|
+
Version: 1.0.14
|
4
|
+
Summary: rclone api in python
|
5
|
+
Home-page: https://github.com/zackees/rclone-api
|
6
|
+
Maintainer: Zachary Vorhies
|
7
|
+
License: BSD 3-Clause License
|
8
|
+
Keywords: template-python-cmd
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Requires-Python: >=3.10
|
11
|
+
Description-Content-Type: text/markdown
|
12
|
+
License-File: LICENSE
|
13
|
+
Requires-Dist: python-dotenv>=1.0.0
|
14
|
+
Dynamic: home-page
|
15
|
+
Dynamic: maintainer
|
16
|
+
|
17
|
+
# rclone-api
|
18
|
+
|
19
|
+
[](https://github.com/zackees/rclone-api/actions/workflows/lint.yml)
|
20
|
+
[](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml)
|
21
|
+
[](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml)
|
22
|
+
[](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml)
|
23
|
+
|
24
|
+
Api version of rclone. It's well tested. It's just released so this readme is a little to be desired.
|
25
|
+
|
26
|
+
To develop software, run `. ./activate`
|
27
|
+
|
28
|
+
# Windows
|
29
|
+
|
30
|
+
This environment requires you to use `git-bash`.
|
31
|
+
|
32
|
+
# Linting
|
33
|
+
|
34
|
+
Run `./lint`
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# rclone-api
|
2
|
+
|
3
|
+
[](https://github.com/zackees/rclone-api/actions/workflows/lint.yml)
|
4
|
+
[](https://github.com/zackees/rclone-api/actions/workflows/push_macos.yml)
|
5
|
+
[](https://github.com/zackees/rclone-api/actions/workflows/push_ubuntu.yml)
|
6
|
+
[](https://github.com/zackees/rclone-api/actions/workflows/push_win.yml)
|
7
|
+
|
8
|
+
Api version of rclone. It's well tested. It's just released so this readme is a little to be desired.
|
9
|
+
|
10
|
+
To develop software, run `. ./activate`
|
11
|
+
|
12
|
+
# Windows
|
13
|
+
|
14
|
+
This environment requires you to use `git-bash`.
|
15
|
+
|
16
|
+
# Linting
|
17
|
+
|
18
|
+
Run `./lint`
|
@@ -1,44 +1,44 @@
|
|
1
|
-
[build-system]
|
2
|
-
requires = ["setuptools>=65.5.1", "setuptools-scm", "wheel"]
|
3
|
-
build-backend = "setuptools.build_meta"
|
4
|
-
|
5
|
-
[project]
|
6
|
-
name = "rclone_api"
|
7
|
-
readme = "README.md"
|
8
|
-
description = "rclone api in python"
|
9
|
-
requires-python = ">=3.
|
10
|
-
keywords = ["template-python-cmd"]
|
11
|
-
license = { text = "BSD 3-Clause License" }
|
12
|
-
classifiers = ["Programming Language :: Python :: 3"]
|
13
|
-
dependencies = [
|
14
|
-
|
15
|
-
]
|
16
|
-
# Change this with the version number bump.
|
17
|
-
version = "1.0.
|
18
|
-
|
19
|
-
[tool.setuptools]
|
20
|
-
package-dir = {"" = "src"}
|
21
|
-
|
22
|
-
[tool.ruff]
|
23
|
-
line-length = 200
|
24
|
-
|
25
|
-
[tool.pylint."MESSAGES CONTROL"]
|
26
|
-
good-names = [
|
27
|
-
"c",
|
28
|
-
"i",
|
29
|
-
"ok",
|
30
|
-
"id",
|
31
|
-
"e",
|
32
|
-
"f"
|
33
|
-
]
|
34
|
-
disable = [
|
35
|
-
"missing-function-docstring",
|
36
|
-
"missing-module-docstring"
|
37
|
-
]
|
38
|
-
|
39
|
-
[tool.isort]
|
40
|
-
profile = "black"
|
41
|
-
|
42
|
-
[tool.mypy]
|
43
|
-
ignore_missing_imports = true
|
44
|
-
disable_error_code = ["import-untyped"]
|
1
|
+
[build-system]
|
2
|
+
requires = ["setuptools>=65.5.1", "setuptools-scm", "wheel"]
|
3
|
+
build-backend = "setuptools.build_meta"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "rclone_api"
|
7
|
+
readme = "README.md"
|
8
|
+
description = "rclone api in python"
|
9
|
+
requires-python = ">=3.10"
|
10
|
+
keywords = ["template-python-cmd"]
|
11
|
+
license = { text = "BSD 3-Clause License" }
|
12
|
+
classifiers = ["Programming Language :: Python :: 3"]
|
13
|
+
dependencies = [
|
14
|
+
"python-dotenv>=1.0.0"
|
15
|
+
]
|
16
|
+
# Change this with the version number bump.
|
17
|
+
version = "1.0.14"
|
18
|
+
|
19
|
+
[tool.setuptools]
|
20
|
+
package-dir = {"" = "src"}
|
21
|
+
|
22
|
+
[tool.ruff]
|
23
|
+
line-length = 200
|
24
|
+
|
25
|
+
[tool.pylint."MESSAGES CONTROL"]
|
26
|
+
good-names = [
|
27
|
+
"c",
|
28
|
+
"i",
|
29
|
+
"ok",
|
30
|
+
"id",
|
31
|
+
"e",
|
32
|
+
"f"
|
33
|
+
]
|
34
|
+
disable = [
|
35
|
+
"missing-function-docstring",
|
36
|
+
"missing-module-docstring"
|
37
|
+
]
|
38
|
+
|
39
|
+
[tool.isort]
|
40
|
+
profile = "black"
|
41
|
+
|
42
|
+
[tool.mypy]
|
43
|
+
ignore_missing_imports = true
|
44
|
+
disable_error_code = ["import-untyped"]
|
@@ -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
|
@@ -8,6 +9,14 @@ from rclone_api.rpath import RPath
|
|
8
9
|
class Dir:
|
9
10
|
"""Remote file dataclass."""
|
10
11
|
|
12
|
+
@property
|
13
|
+
def remote(self) -> Remote:
|
14
|
+
return self.path.remote
|
15
|
+
|
16
|
+
@property
|
17
|
+
def name(self) -> str:
|
18
|
+
return self.path.name
|
19
|
+
|
11
20
|
def __init__(self, path: RPath | Remote) -> None:
|
12
21
|
"""Initialize Dir with either an RPath or Remote.
|
13
22
|
|
@@ -17,6 +26,7 @@ class Dir:
|
|
17
26
|
if isinstance(path, Remote):
|
18
27
|
# Need to create an RPath for the Remote's root
|
19
28
|
self.path = RPath(
|
29
|
+
remote=path,
|
20
30
|
path=str(path),
|
21
31
|
name=str(path),
|
22
32
|
size=0,
|
@@ -28,11 +38,14 @@ class Dir:
|
|
28
38
|
self.path.set_rclone(path.rclone)
|
29
39
|
else:
|
30
40
|
self.path = path
|
41
|
+
# self.path.set_rclone(self.path.remote.rclone)
|
42
|
+
assert self.path.rclone is not None
|
31
43
|
|
32
44
|
def ls(self, max_depth: int = 0) -> DirListing:
|
33
45
|
"""List files and directories in the given path."""
|
34
46
|
assert self.path.rclone is not None
|
35
|
-
|
47
|
+
dir = Dir(self.path)
|
48
|
+
return self.path.rclone.ls(dir, max_depth=max_depth)
|
36
49
|
|
37
50
|
def walk(self, max_depth: int = -1) -> Generator[DirListing, None, None]:
|
38
51
|
"""List files and directories in the given path."""
|
@@ -41,5 +54,14 @@ class Dir:
|
|
41
54
|
assert self.path.rclone is not None
|
42
55
|
return walk(self, max_depth=max_depth)
|
43
56
|
|
57
|
+
def to_json(self) -> dict:
|
58
|
+
"""Convert the Dir to a JSON serializable dictionary."""
|
59
|
+
return self.path.to_json()
|
60
|
+
|
44
61
|
def __str__(self) -> str:
|
45
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)
|
@@ -12,6 +12,10 @@ class File:
|
|
12
12
|
) -> None:
|
13
13
|
self.path = path
|
14
14
|
|
15
|
+
@property
|
16
|
+
def name(self) -> str:
|
17
|
+
return self.path.name
|
18
|
+
|
15
19
|
def read_text(self) -> str:
|
16
20
|
"""Read the file contents as bytes.
|
17
21
|
|
@@ -30,6 +34,14 @@ class File:
|
|
30
34
|
result = self.path.rclone._run(["cat", self.path.path])
|
31
35
|
return result.stdout
|
32
36
|
|
37
|
+
def to_json(self) -> dict:
|
38
|
+
"""Convert the File to a JSON serializable dictionary."""
|
39
|
+
return self.path.to_json()
|
40
|
+
|
33
41
|
def __str__(self) -> str:
|
34
|
-
|
35
|
-
|
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)
|