easyclone 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {easyclone-0.1.0/src/easyclone.egg-info → easyclone-0.2.0}/PKG-INFO +10 -1
- {easyclone-0.1.0 → easyclone-0.2.0}/README.md +9 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/pyproject.toml +1 -1
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/config.py +4 -1
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/ipc/server.py +1 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/main.py +17 -6
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/rclone/__init__.py +1 -1
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/rclone/backup.py +0 -1
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/rclone/create_dirs.py +1 -1
- easyclone-0.2.0/src/easyclone/rclone/operations.py +35 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/shared/sync_status.py +10 -1
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utils/__init__.py +1 -1
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utils/essentials.py +15 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utils/path_manipulation.py +10 -3
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utypes/models.py +5 -0
- {easyclone-0.1.0 → easyclone-0.2.0/src/easyclone.egg-info}/PKG-INFO +10 -1
- easyclone-0.1.0/src/easyclone/rclone/operations.py +0 -48
- {easyclone-0.1.0 → easyclone-0.2.0}/LICENSE +0 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/setup.cfg +0 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/__main__.py +0 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/ipc/__init__.py +0 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/ipc/client.py +0 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/shared/__init__.py +0 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utypes/__init__.py +0 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utypes/config.py +0 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utypes/enums.py +0 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone.egg-info/SOURCES.txt +0 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone.egg-info/dependency_links.txt +0 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone.egg-info/entry_points.txt +0 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone.egg-info/requires.txt +0 -0
- {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: easyclone
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Very convenient Rclone bulk backup wrapper
|
|
5
5
|
Project-URL: Repository, https://github.com/dybdeskarphet/easyclone
|
|
6
6
|
Project-URL: Documentation, https://github.com/dybdeskarphet/easyclone
|
|
@@ -27,6 +27,15 @@ You define what to back up, where to back it up, and EasyClone handles the syncs
|
|
|
27
27
|
* 🛠️ IPC-ready architecture for future GUI or monitoring tools
|
|
28
28
|
* 🔊 Optional verbose logging
|
|
29
29
|
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Install it with `pip` or `pipx`:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install easyclone
|
|
36
|
+
pipx install easyclone
|
|
37
|
+
```
|
|
38
|
+
|
|
30
39
|
## 🛠️ Requirements
|
|
31
40
|
|
|
32
41
|
* Python **3.13+**
|
|
@@ -12,6 +12,15 @@ You define what to back up, where to back it up, and EasyClone handles the syncs
|
|
|
12
12
|
* 🛠️ IPC-ready architecture for future GUI or monitoring tools
|
|
13
13
|
* 🔊 Optional verbose logging
|
|
14
14
|
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Install it with `pip` or `pipx`:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install easyclone
|
|
21
|
+
pipx install easyclone
|
|
22
|
+
```
|
|
23
|
+
|
|
15
24
|
## 🛠️ Requirements
|
|
16
25
|
|
|
17
26
|
* Python **3.13+**
|
|
@@ -70,7 +70,6 @@ class Config:
|
|
|
70
70
|
from easyclone.utils.essentials import log
|
|
71
71
|
self._get_config_path()
|
|
72
72
|
|
|
73
|
-
|
|
74
73
|
try:
|
|
75
74
|
with open(self._path) as f:
|
|
76
75
|
parsed_string = f.read()
|
|
@@ -84,6 +83,10 @@ class Config:
|
|
|
84
83
|
try:
|
|
85
84
|
parsed_toml = toml.loads(parsed_string)
|
|
86
85
|
validated_config = ConfigModel.model_validate(parsed_toml)
|
|
86
|
+
|
|
87
|
+
# Normalize config
|
|
88
|
+
validated_config.backup.root_dir = validated_config.backup.root_dir.strip("/")
|
|
89
|
+
|
|
87
90
|
return validated_config
|
|
88
91
|
except Exception as e:
|
|
89
92
|
log(f"Invalid config: {e}", LogLevel.ERROR)
|
|
@@ -13,6 +13,7 @@ async def handle_client(_reader: asyncio.StreamReader, writer: asyncio.StreamWri
|
|
|
13
13
|
message = json.dumps({
|
|
14
14
|
"total_path_count": await sync_status.get_total_path(),
|
|
15
15
|
"finished_path_count": await sync_status.get_currently_finished(),
|
|
16
|
+
"empty_paths": await sync_status.get_empty_paths(),
|
|
16
17
|
"operation_count": await sync_status.get_operation_count(),
|
|
17
18
|
"operations": await sync_status.get_operations()
|
|
18
19
|
}).encode() + b"\n"
|
|
@@ -3,11 +3,12 @@ from typing import Annotated, Any
|
|
|
3
3
|
from easyclone.config import cfg
|
|
4
4
|
from easyclone.ipc.client import listen_ipc
|
|
5
5
|
from easyclone.ipc.server import start_status_server
|
|
6
|
-
from easyclone.rclone.operations import
|
|
6
|
+
from easyclone.rclone.operations import make_backup_operation
|
|
7
7
|
import typer
|
|
8
8
|
import json
|
|
9
|
-
|
|
10
9
|
from easyclone.shared import sync_status
|
|
10
|
+
from easyclone.utils.essentials import exit_if_currently_running, exit_if_no_rclone
|
|
11
|
+
from easyclone.utypes.enums import CommandType
|
|
11
12
|
|
|
12
13
|
app = typer.Typer(
|
|
13
14
|
help="Very convenient Rclone bulk backup wrapper",
|
|
@@ -22,14 +23,16 @@ async def ipc():
|
|
|
22
23
|
@app.command(help="Starts the backup process using the details in the config file.")
|
|
23
24
|
def start_backup(verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enables the rclone logging (overrides config).")] = False):
|
|
24
25
|
async def start():
|
|
26
|
+
exit_if_no_rclone()
|
|
27
|
+
exit_if_currently_running()
|
|
25
28
|
await sync_status.set_total_path_count(len(cfg.backup.sync_paths) + len(cfg.backup.copy_paths))
|
|
26
29
|
|
|
27
30
|
_ipc_task = asyncio.create_task(ipc())
|
|
28
31
|
|
|
29
32
|
verbose_state = verbose or cfg.backup.verbose_log
|
|
30
33
|
|
|
31
|
-
await
|
|
32
|
-
await
|
|
34
|
+
await make_backup_operation(CommandType.COPY, cfg.backup.copy_paths, verbose_state)()
|
|
35
|
+
await make_backup_operation(CommandType.SYNC, cfg.backup.sync_paths, verbose_state)()
|
|
33
36
|
|
|
34
37
|
asyncio.run(start())
|
|
35
38
|
|
|
@@ -38,11 +41,13 @@ def get_status(
|
|
|
38
41
|
all: Annotated[bool, typer.Option("--all", "-a", help="Show all the backup status information.")] = False,
|
|
39
42
|
show_total: Annotated[bool, typer.Option("--show-total", "-t", help="Show the total amount of paths.")] = False,
|
|
40
43
|
show_current: Annotated[bool, typer.Option("--show-current", "-c", help="Show the total amount of pending paths.")] = False,
|
|
41
|
-
show_operations: Annotated[bool, typer.Option("--show-operations", "-o", help="Show currently running operations.")] = False
|
|
44
|
+
show_operations: Annotated[bool, typer.Option("--show-operations", "-o", help="Show currently running operations.")] = False,
|
|
45
|
+
show_operation_count: Annotated[bool, typer.Option("--get-operation-count", "-O", help="Show the total amount of running operations.")] = False,
|
|
46
|
+
show_empty_paths: Annotated[bool, typer.Option("--show-empty-paths", "-e", help="Show all the empty paths.")] = False
|
|
42
47
|
):
|
|
43
48
|
data: Any = asyncio.run(listen_ipc())
|
|
44
49
|
|
|
45
|
-
if (not show_total and not show_current and not show_operations or all) or (show_total and show_current and show_operations):
|
|
50
|
+
if (not show_total and not show_current and not show_operations and not show_operation_count and not show_empty_paths or all) or (show_total and show_current and show_operations and show_operation_count and show_empty_paths):
|
|
46
51
|
print(json.dumps(data, indent=2))
|
|
47
52
|
return
|
|
48
53
|
|
|
@@ -55,5 +60,11 @@ def get_status(
|
|
|
55
60
|
if show_operations:
|
|
56
61
|
print(json.dumps(data["operations"], indent=2))
|
|
57
62
|
|
|
63
|
+
if show_operation_count:
|
|
64
|
+
print(json.dumps(data["operation_count"], indent=2))
|
|
65
|
+
|
|
66
|
+
if show_empty_paths:
|
|
67
|
+
print(json.dumps(data["empty_paths"], indent=2))
|
|
68
|
+
|
|
58
69
|
if __name__ == "__main__":
|
|
59
70
|
app()
|
|
@@ -39,7 +39,6 @@ async def backup_command(rclone_command: list[str], source: str, dest: str, path
|
|
|
39
39
|
async def backup(paths: list[PathItem], command_type: CommandType, rclone_args: list[str], semaphore: asyncio.Semaphore, verbose: bool = False):
|
|
40
40
|
cmd = ["rclone", command_type.value]
|
|
41
41
|
|
|
42
|
-
|
|
43
42
|
for arg in rclone_args:
|
|
44
43
|
parts = shlex.split(arg)
|
|
45
44
|
cmd += parts
|
|
@@ -56,7 +56,7 @@ def create_dirs_array(path_list: list[PathItem]):
|
|
|
56
56
|
return only_dirs
|
|
57
57
|
|
|
58
58
|
def create_dir_tree(path_list: list[PathItem]):
|
|
59
|
-
root = DirNode("Root", {"source": "/", "dest": f"{cfg.backup.remote_name}:{
|
|
59
|
+
root = DirNode("Root", {"source": "/", "dest": f"{cfg.backup.remote_name}:{cfg.backup.root_dir}", "path_type": "dir",})
|
|
60
60
|
|
|
61
61
|
for path_item in path_list:
|
|
62
62
|
source_str = path_item.get("source")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from easyclone.config import cfg
|
|
3
|
+
from easyclone.rclone.backup import backup
|
|
4
|
+
from easyclone.rclone.create_dirs import create_dir_tree, create_dirs_array, traverse_and_create_folders_by_depth
|
|
5
|
+
from easyclone.shared import sync_status
|
|
6
|
+
from easyclone.utils.essentials import log
|
|
7
|
+
from easyclone.utils.path_manipulation import organize_paths
|
|
8
|
+
from easyclone.utypes.enums import CommandType, LogLevel
|
|
9
|
+
|
|
10
|
+
def make_backup_operation(command_type: CommandType, paths_config: list[str], verbose: bool):
|
|
11
|
+
async def backup_operation():
|
|
12
|
+
paths = organize_paths(paths_config, cfg.backup.remote_name)
|
|
13
|
+
task_semaphore = asyncio.Semaphore(cfg.rclone.concurrent_limit)
|
|
14
|
+
dirs_task_semaphore = asyncio.Semaphore(cfg.rclone.concurrent_limit)
|
|
15
|
+
dirs_array = create_dirs_array(paths["valid_paths"])
|
|
16
|
+
dirs_root = create_dir_tree(dirs_array)
|
|
17
|
+
|
|
18
|
+
log(f"Below paths couldn't be found:\n{"\n".join(paths["empty_paths"])}\n", LogLevel.WARN)
|
|
19
|
+
for path in paths["empty_paths"]:
|
|
20
|
+
await sync_status.add_empty_path(path)
|
|
21
|
+
|
|
22
|
+
_copy_folders_create_operation = await traverse_and_create_folders_by_depth(
|
|
23
|
+
root=dirs_root,
|
|
24
|
+
verbose=verbose,
|
|
25
|
+
semaphore=dirs_task_semaphore
|
|
26
|
+
)
|
|
27
|
+
_copy_operation = await backup(
|
|
28
|
+
paths=paths["valid_paths"],
|
|
29
|
+
command_type=command_type,
|
|
30
|
+
rclone_args=cfg.rclone.args,
|
|
31
|
+
semaphore=task_semaphore,
|
|
32
|
+
verbose=verbose
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return backup_operation
|
|
@@ -7,8 +7,9 @@ class SyncStatus:
|
|
|
7
7
|
def __init__(self):
|
|
8
8
|
self.total_path_count: int = 0
|
|
9
9
|
self.operations: list[SyncStatusItem] = []
|
|
10
|
-
self.lock: asyncio.Lock = asyncio.Lock()
|
|
11
10
|
self.finished_path_count: int = 0
|
|
11
|
+
self.empty_paths: list[str] = []
|
|
12
|
+
self.lock: asyncio.Lock = asyncio.Lock()
|
|
12
13
|
|
|
13
14
|
async def add_operation(self, source: str, dest: str, path_type: str, status: BackupStatus, operation_type: RcloneOperationType):
|
|
14
15
|
random_id = str(uuid.uuid4())
|
|
@@ -59,3 +60,11 @@ class SyncStatus:
|
|
|
59
60
|
async def get_currently_finished(self):
|
|
60
61
|
async with self.lock:
|
|
61
62
|
return self.finished_path_count
|
|
63
|
+
|
|
64
|
+
async def add_empty_path(self, path: str):
|
|
65
|
+
async with self.lock:
|
|
66
|
+
self.empty_paths.append(path)
|
|
67
|
+
|
|
68
|
+
async def get_empty_paths(self):
|
|
69
|
+
async with self.lock:
|
|
70
|
+
return self.empty_paths
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
from .essentials import log
|
|
1
|
+
from .essentials import log, exit_if_no_rclone
|
|
2
2
|
from .path_manipulation import organize_paths, collapseuser
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from easyclone.utypes.enums import LogLevel, BackupLog
|
|
2
3
|
|
|
3
4
|
def log(message: str, logtype: LogLevel | BackupLog) -> None:
|
|
@@ -22,3 +23,17 @@ def log(message: str, logtype: LogLevel | BackupLog) -> None:
|
|
|
22
23
|
full_msg = full_msg + ":"
|
|
23
24
|
|
|
24
25
|
print(f"{full_msg}\033[0m {message}")
|
|
26
|
+
|
|
27
|
+
def is_tool(name: str):
|
|
28
|
+
from shutil import which
|
|
29
|
+
return which(name) is not None
|
|
30
|
+
|
|
31
|
+
def exit_if_no_rclone():
|
|
32
|
+
if not is_tool("rclone"):
|
|
33
|
+
log("Rclone is not installed on your system, 'rclone' command should be in the $PATH.", LogLevel.ERROR)
|
|
34
|
+
exit(1)
|
|
35
|
+
|
|
36
|
+
def exit_if_currently_running():
|
|
37
|
+
if os.path.exists('/tmp/easyclone.sock'):
|
|
38
|
+
log("Easyclone is already running. Delete /tmp/easyclone.sock if you think this is a mistake.", LogLevel.WARN)
|
|
39
|
+
exit(1)
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
import os
|
|
3
3
|
from easyclone.utypes.enums import PathType
|
|
4
|
-
from easyclone.utypes.models import PathItem
|
|
4
|
+
from easyclone.utypes.models import OrganizedPaths, PathItem
|
|
5
5
|
|
|
6
|
-
def organize_paths(paths: list[str], remote_name: str) ->
|
|
6
|
+
def organize_paths(paths: list[str], remote_name: str) -> OrganizedPaths:
|
|
7
7
|
from easyclone.config import cfg
|
|
8
8
|
source_dest_array: list[PathItem] = []
|
|
9
|
+
empty_paths: list[str] = []
|
|
9
10
|
root_dir = cfg.backup.root_dir
|
|
10
11
|
|
|
11
12
|
for path in paths:
|
|
12
13
|
p = Path(path).expanduser()
|
|
13
14
|
|
|
15
|
+
if not os.path.exists(p):
|
|
16
|
+
empty_paths.append(path)
|
|
17
|
+
|
|
14
18
|
if p.is_dir():
|
|
15
19
|
source_dest_array.append({
|
|
16
20
|
"source": f"{p}",
|
|
@@ -25,7 +29,10 @@ def organize_paths(paths: list[str], remote_name: str) -> list[PathItem]:
|
|
|
25
29
|
"path_type": PathType.FILE.value
|
|
26
30
|
})
|
|
27
31
|
|
|
28
|
-
return
|
|
32
|
+
return {
|
|
33
|
+
"valid_paths": source_dest_array,
|
|
34
|
+
"empty_paths": empty_paths
|
|
35
|
+
}
|
|
29
36
|
|
|
30
37
|
def collapseuser(path: str) -> str:
|
|
31
38
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: easyclone
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Very convenient Rclone bulk backup wrapper
|
|
5
5
|
Project-URL: Repository, https://github.com/dybdeskarphet/easyclone
|
|
6
6
|
Project-URL: Documentation, https://github.com/dybdeskarphet/easyclone
|
|
@@ -27,6 +27,15 @@ You define what to back up, where to back it up, and EasyClone handles the syncs
|
|
|
27
27
|
* 🛠️ IPC-ready architecture for future GUI or monitoring tools
|
|
28
28
|
* 🔊 Optional verbose logging
|
|
29
29
|
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Install it with `pip` or `pipx`:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install easyclone
|
|
36
|
+
pipx install easyclone
|
|
37
|
+
```
|
|
38
|
+
|
|
30
39
|
## 🛠️ Requirements
|
|
31
40
|
|
|
32
41
|
* Python **3.13+**
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from easyclone.config import cfg
|
|
3
|
-
from easyclone.rclone.backup import backup
|
|
4
|
-
from easyclone.rclone.create_dirs import create_dir_tree, create_dirs_array, traverse_and_create_folders_by_depth
|
|
5
|
-
from easyclone.utils.path_manipulation import organize_paths
|
|
6
|
-
from easyclone.utypes.enums import CommandType
|
|
7
|
-
|
|
8
|
-
async def backup_copy_operation(verbose: bool):
|
|
9
|
-
copy_paths = organize_paths(cfg.backup.copy_paths, cfg.backup.remote_name)
|
|
10
|
-
copy_task_semaphore = asyncio.Semaphore(cfg.rclone.concurrent_limit)
|
|
11
|
-
copy_dirs_task_semaphore = asyncio.Semaphore(cfg.rclone.concurrent_limit)
|
|
12
|
-
copy_dirs_array = create_dirs_array(copy_paths)
|
|
13
|
-
copy_dirs_root = create_dir_tree(copy_dirs_array)
|
|
14
|
-
|
|
15
|
-
_copy_folders_create_operation = await traverse_and_create_folders_by_depth(
|
|
16
|
-
root=copy_dirs_root,
|
|
17
|
-
verbose=verbose,
|
|
18
|
-
semaphore=copy_dirs_task_semaphore
|
|
19
|
-
)
|
|
20
|
-
_copy_operation = await backup(
|
|
21
|
-
paths=copy_paths,
|
|
22
|
-
command_type=CommandType.COPY,
|
|
23
|
-
rclone_args=cfg.rclone.args,
|
|
24
|
-
semaphore=copy_task_semaphore,
|
|
25
|
-
verbose=verbose
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
async def backup_sync_operation(verbose: bool):
|
|
30
|
-
sync_paths = organize_paths(cfg.backup.sync_paths, cfg.backup.remote_name)
|
|
31
|
-
sync_task_semaphore = asyncio.Semaphore(cfg.rclone.concurrent_limit)
|
|
32
|
-
sync_dirs_task_semaphore = asyncio.Semaphore(cfg.rclone.concurrent_limit)
|
|
33
|
-
sync_dirs_array = create_dirs_array(sync_paths)
|
|
34
|
-
sync_dirs_root = create_dir_tree(sync_dirs_array)
|
|
35
|
-
|
|
36
|
-
_sync_folders_create_operation = await traverse_and_create_folders_by_depth(
|
|
37
|
-
root=sync_dirs_root,
|
|
38
|
-
verbose=verbose,
|
|
39
|
-
semaphore=sync_dirs_task_semaphore
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
_sync_operation = await backup(
|
|
43
|
-
paths=sync_paths,
|
|
44
|
-
command_type=CommandType.COPY,
|
|
45
|
-
rclone_args=cfg.rclone.args,
|
|
46
|
-
semaphore=sync_task_semaphore,
|
|
47
|
-
verbose=verbose
|
|
48
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|