easyclone 0.2.0__tar.gz → 1.0.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.2.0/src/easyclone.egg-info → easyclone-1.0.0}/PKG-INFO +21 -12
- {easyclone-0.2.0 → easyclone-1.0.0}/README.md +15 -11
- {easyclone-0.2.0 → easyclone-1.0.0}/pyproject.toml +10 -1
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/config.py +7 -20
- easyclone-1.0.0/src/easyclone/ipc/__init__.py +2 -0
- easyclone-1.0.0/src/easyclone/main.py +155 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/utils/path_manipulation.py +3 -2
- easyclone-1.0.0/src/easyclone/utypes/config.py +26 -0
- {easyclone-0.2.0 → easyclone-1.0.0/src/easyclone.egg-info}/PKG-INFO +21 -12
- easyclone-0.2.0/src/easyclone/ipc/__init__.py +0 -2
- easyclone-0.2.0/src/easyclone/main.py +0 -70
- easyclone-0.2.0/src/easyclone/utypes/config.py +0 -16
- {easyclone-0.2.0 → easyclone-1.0.0}/LICENSE +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/setup.cfg +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/__main__.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/ipc/client.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/ipc/server.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/rclone/__init__.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/rclone/backup.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/rclone/create_dirs.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/rclone/operations.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/shared/__init__.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/shared/sync_status.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/utils/__init__.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/utils/essentials.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/utypes/__init__.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/utypes/enums.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone/utypes/models.py +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone.egg-info/SOURCES.txt +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone.egg-info/dependency_links.txt +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone.egg-info/entry_points.txt +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone.egg-info/requires.txt +0 -0
- {easyclone-0.2.0 → easyclone-1.0.0}/src/easyclone.egg-info/top_level.txt +0 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: easyclone
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Very convenient Rclone bulk backup wrapper
|
|
5
|
+
Author-email: Ahmet Arda Kavakcı <ahmetardakavakci@gmail.com>
|
|
6
|
+
Maintainer-email: Ahmet Arda Kavakcı <ahmetardakavakci@gmail.com>
|
|
7
|
+
License-Expression: GPL-3.0-or-later
|
|
5
8
|
Project-URL: Repository, https://github.com/dybdeskarphet/easyclone
|
|
6
9
|
Project-URL: Documentation, https://github.com/dybdeskarphet/easyclone
|
|
7
10
|
Project-URL: Issues, https://github.com/dybdeskarphet/easyclone/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/dybdeskarphet/easyclone/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: backup,sync,google,drive,easyclone
|
|
8
13
|
Requires-Python: >=3.13
|
|
9
14
|
Description-Content-Type: text/markdown
|
|
10
15
|
License-File: LICENSE
|
|
@@ -19,13 +24,13 @@ Dynamic: license-file
|
|
|
19
24
|
|
|
20
25
|
You define what to back up, where to back it up, and EasyClone handles the syncs and copies — clean, fast, and reliable.
|
|
21
26
|
|
|
22
|
-
##
|
|
27
|
+
## Features
|
|
23
28
|
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
+
* Sync & Copy support per-path
|
|
30
|
+
* Backup multiple paths at once
|
|
31
|
+
* Human-friendly TOML config
|
|
32
|
+
* IPC-ready architecture for future GUI or monitoring tools
|
|
33
|
+
* Optional verbose logging
|
|
29
34
|
|
|
30
35
|
## Installation
|
|
31
36
|
|
|
@@ -36,7 +41,11 @@ pip install easyclone
|
|
|
36
41
|
pipx install easyclone
|
|
37
42
|
```
|
|
38
43
|
|
|
39
|
-
##
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
The config file is at `~/.config/easyclone/config.toml`
|
|
47
|
+
|
|
48
|
+
## Requirements
|
|
40
49
|
|
|
41
50
|
* Python **3.13+**
|
|
42
51
|
* [`rclone`](https://rclone.org/) installed and accessible in your `$PATH`
|
|
@@ -44,7 +53,7 @@ pipx install easyclone
|
|
|
44
53
|
* `toml>=0.10.2`
|
|
45
54
|
* `typer>=0.16.0`
|
|
46
55
|
|
|
47
|
-
##
|
|
56
|
+
## Example Usage
|
|
48
57
|
|
|
49
58
|
```bash
|
|
50
59
|
easyclone start-backup
|
|
@@ -56,16 +65,16 @@ It will:
|
|
|
56
65
|
* Copy the paths in `copy_paths`
|
|
57
66
|
* Use the `remote_name` and `root_dir` to target your cloud storage
|
|
58
67
|
|
|
59
|
-
##
|
|
68
|
+
## Contributing
|
|
60
69
|
|
|
61
70
|
PRs welcome. Bug reports even more welcome.
|
|
62
71
|
|
|
63
|
-
##
|
|
72
|
+
## FAQ
|
|
64
73
|
|
|
65
74
|
Why does it create the folders first?
|
|
66
75
|
> Because services like Google Drive support multiple folders with the same name in the same directory. So when you try to concurrently backup paths from the same directory, it will create the parent directory more than once, and we don't want that.
|
|
67
76
|
|
|
68
|
-
##
|
|
77
|
+
## License
|
|
69
78
|
|
|
70
79
|
GPLv3 — do whatever you want, just don't blame me if you sync your `/` folder to the cloud :)
|
|
71
80
|
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
You define what to back up, where to back it up, and EasyClone handles the syncs and copies — clean, fast, and reliable.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Features
|
|
8
8
|
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Sync & Copy support per-path
|
|
10
|
+
* Backup multiple paths at once
|
|
11
|
+
* Human-friendly TOML config
|
|
12
|
+
* IPC-ready architecture for future GUI or monitoring tools
|
|
13
|
+
* Optional verbose logging
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
@@ -21,7 +21,11 @@ pip install easyclone
|
|
|
21
21
|
pipx install easyclone
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
The config file is at `~/.config/easyclone/config.toml`
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
25
29
|
|
|
26
30
|
* Python **3.13+**
|
|
27
31
|
* [`rclone`](https://rclone.org/) installed and accessible in your `$PATH`
|
|
@@ -29,7 +33,7 @@ pipx install easyclone
|
|
|
29
33
|
* `toml>=0.10.2`
|
|
30
34
|
* `typer>=0.16.0`
|
|
31
35
|
|
|
32
|
-
##
|
|
36
|
+
## Example Usage
|
|
33
37
|
|
|
34
38
|
```bash
|
|
35
39
|
easyclone start-backup
|
|
@@ -41,16 +45,16 @@ It will:
|
|
|
41
45
|
* Copy the paths in `copy_paths`
|
|
42
46
|
* Use the `remote_name` and `root_dir` to target your cloud storage
|
|
43
47
|
|
|
44
|
-
##
|
|
48
|
+
## Contributing
|
|
45
49
|
|
|
46
50
|
PRs welcome. Bug reports even more welcome.
|
|
47
51
|
|
|
48
|
-
##
|
|
52
|
+
## FAQ
|
|
49
53
|
|
|
50
54
|
Why does it create the folders first?
|
|
51
55
|
> Because services like Google Drive support multiple folders with the same name in the same directory. So when you try to concurrently backup paths from the same directory, it will create the parent directory more than once, and we don't want that.
|
|
52
56
|
|
|
53
|
-
##
|
|
57
|
+
## License
|
|
54
58
|
|
|
55
59
|
GPLv3 — do whatever you want, just don't blame me if you sync your `/` folder to the cloud :)
|
|
56
60
|
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "easyclone"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "1.0.0"
|
|
4
4
|
description = "Very convenient Rclone bulk backup wrapper"
|
|
5
|
+
keywords = ["backup", "sync", "google", "drive", "easyclone"]
|
|
6
|
+
authors = [
|
|
7
|
+
{name = "Ahmet Arda Kavakcı", email = "ahmetardakavakci@gmail.com"},
|
|
8
|
+
]
|
|
9
|
+
maintainers = [
|
|
10
|
+
{name = "Ahmet Arda Kavakcı", email = "ahmetardakavakci@gmail.com"},
|
|
11
|
+
]
|
|
5
12
|
readme = "README.md"
|
|
13
|
+
license = "GPL-3.0-or-later"
|
|
6
14
|
requires-python = ">=3.13"
|
|
7
15
|
dependencies = [
|
|
8
16
|
"pydantic>=2.11.5",
|
|
@@ -17,6 +25,7 @@ easyclone = "easyclone.main:app"
|
|
|
17
25
|
Repository = "https://github.com/dybdeskarphet/easyclone"
|
|
18
26
|
Documentation = "https://github.com/dybdeskarphet/easyclone"
|
|
19
27
|
Issues = "https://github.com/dybdeskarphet/easyclone/issues"
|
|
28
|
+
Changelog = "https://github.com/dybdeskarphet/easyclone/blob/main/CHANGELOG.md"
|
|
20
29
|
|
|
21
30
|
[tool.basedpyright]
|
|
22
31
|
reportAny = false
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
import json
|
|
2
3
|
from threading import Lock
|
|
3
4
|
from os import getenv
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from easyclone.utypes.enums import LogLevel
|
|
6
|
-
from easyclone.utypes.config import BackupConfigModel, ConfigModel
|
|
7
|
+
from easyclone.utypes.config import BackupConfigModel, ConfigModel
|
|
7
8
|
import toml
|
|
8
9
|
|
|
9
10
|
class Config:
|
|
@@ -31,21 +32,6 @@ class Config:
|
|
|
31
32
|
copy_paths=[],
|
|
32
33
|
remote_name="GoogleDrive",
|
|
33
34
|
root_dir="Backups/PC",
|
|
34
|
-
verbose_log=False
|
|
35
|
-
),
|
|
36
|
-
rclone=RcloneConfigModel(
|
|
37
|
-
args=[
|
|
38
|
-
"--update",
|
|
39
|
-
"--verbose",
|
|
40
|
-
"--transfers 30",
|
|
41
|
-
"--checkers 8",
|
|
42
|
-
"--contimeout 60s",
|
|
43
|
-
"--timeout 300s",
|
|
44
|
-
"--retries 3",
|
|
45
|
-
"--low-level-retries 10",
|
|
46
|
-
"--stats 1s"
|
|
47
|
-
],
|
|
48
|
-
concurrent_limit=50
|
|
49
35
|
)
|
|
50
36
|
)
|
|
51
37
|
|
|
@@ -66,6 +52,10 @@ class Config:
|
|
|
66
52
|
|
|
67
53
|
self._path = config_file
|
|
68
54
|
|
|
55
|
+
def _config_normalize(self, config: ConfigModel):
|
|
56
|
+
config.backup.root_dir = config.backup.root_dir.strip("/")
|
|
57
|
+
return config
|
|
58
|
+
|
|
69
59
|
def _load_config(self):
|
|
70
60
|
from easyclone.utils.essentials import log
|
|
71
61
|
self._get_config_path()
|
|
@@ -83,10 +73,7 @@ class Config:
|
|
|
83
73
|
try:
|
|
84
74
|
parsed_toml = toml.loads(parsed_string)
|
|
85
75
|
validated_config = ConfigModel.model_validate(parsed_toml)
|
|
86
|
-
|
|
87
|
-
# Normalize config
|
|
88
|
-
validated_config.backup.root_dir = validated_config.backup.root_dir.strip("/")
|
|
89
|
-
|
|
76
|
+
validated_config = self._config_normalize(validated_config)
|
|
90
77
|
return validated_config
|
|
91
78
|
except Exception as e:
|
|
92
79
|
log(f"Invalid config: {e}", LogLevel.ERROR)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
from easyclone.config import cfg
|
|
5
|
+
from easyclone.ipc.client import listen_ipc
|
|
6
|
+
from easyclone.ipc.server import start_status_server
|
|
7
|
+
from easyclone.rclone.operations import make_backup_operation
|
|
8
|
+
import typer
|
|
9
|
+
import json
|
|
10
|
+
from easyclone.shared import sync_status
|
|
11
|
+
from easyclone.utils.essentials import exit_if_currently_running, exit_if_no_rclone
|
|
12
|
+
from easyclone.utypes.enums import CommandType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
help="Very convenient Rclone bulk backup wrapper",
|
|
17
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def ipc():
|
|
22
|
+
server = await start_status_server()
|
|
23
|
+
async with server:
|
|
24
|
+
await server.serve_forever()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command(help="Starts the backup process using the details in the config file.")
|
|
28
|
+
def start_backup(
|
|
29
|
+
verbose: Annotated[
|
|
30
|
+
bool,
|
|
31
|
+
typer.Option(
|
|
32
|
+
"--verbose", "-v", help="Enables the rclone logging (overrides config)."
|
|
33
|
+
),
|
|
34
|
+
] = False,
|
|
35
|
+
):
|
|
36
|
+
|
|
37
|
+
import atexit
|
|
38
|
+
from os import path, remove
|
|
39
|
+
import signal
|
|
40
|
+
from types import FrameType
|
|
41
|
+
from easyclone.ipc.client import SOCKET_PATH
|
|
42
|
+
from easyclone.utils.essentials import log
|
|
43
|
+
from easyclone.utypes.enums import LogLevel
|
|
44
|
+
|
|
45
|
+
def cleanup():
|
|
46
|
+
if path.exists(SOCKET_PATH):
|
|
47
|
+
log("Cleaning up socket file...", LogLevel.WARN)
|
|
48
|
+
remove(SOCKET_PATH)
|
|
49
|
+
|
|
50
|
+
_ = atexit.register(cleanup)
|
|
51
|
+
|
|
52
|
+
def handle_signal(_signum: int, _frame: FrameType | None):
|
|
53
|
+
cleanup()
|
|
54
|
+
sys.exit(0)
|
|
55
|
+
|
|
56
|
+
_ = signal.signal(signal.SIGINT, handle_signal)
|
|
57
|
+
_ = signal.signal(signal.SIGTERM, handle_signal)
|
|
58
|
+
|
|
59
|
+
async def start():
|
|
60
|
+
|
|
61
|
+
exit_if_no_rclone()
|
|
62
|
+
exit_if_currently_running()
|
|
63
|
+
await sync_status.set_total_path_count(
|
|
64
|
+
len(cfg.backup.sync_paths) + len(cfg.backup.copy_paths)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
_ipc_task = asyncio.create_task(ipc())
|
|
68
|
+
|
|
69
|
+
verbose_state = verbose or cfg.backup.verbose_log
|
|
70
|
+
|
|
71
|
+
await make_backup_operation(
|
|
72
|
+
CommandType.COPY, cfg.backup.copy_paths, verbose_state
|
|
73
|
+
)()
|
|
74
|
+
await make_backup_operation(
|
|
75
|
+
CommandType.SYNC, cfg.backup.sync_paths, verbose_state
|
|
76
|
+
)()
|
|
77
|
+
|
|
78
|
+
asyncio.run(start())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command(help="Gets status information about the backup process.")
|
|
82
|
+
def get_status(
|
|
83
|
+
all_args: Annotated[
|
|
84
|
+
bool,
|
|
85
|
+
typer.Option("--all", "-a", help="Show all the backup status information."),
|
|
86
|
+
] = False,
|
|
87
|
+
show_total: Annotated[
|
|
88
|
+
bool, typer.Option("--show-total", "-t", help="Show the total amount of paths.")
|
|
89
|
+
] = False,
|
|
90
|
+
show_current: Annotated[
|
|
91
|
+
bool,
|
|
92
|
+
typer.Option(
|
|
93
|
+
"--show-current", "-c", help="Show the total amount of pending paths."
|
|
94
|
+
),
|
|
95
|
+
] = False,
|
|
96
|
+
show_operations: Annotated[
|
|
97
|
+
bool,
|
|
98
|
+
typer.Option(
|
|
99
|
+
"--show-operations", "-o", help="Show currently running operations."
|
|
100
|
+
),
|
|
101
|
+
] = False,
|
|
102
|
+
show_finished_path_count: Annotated[
|
|
103
|
+
bool,
|
|
104
|
+
typer.Option(
|
|
105
|
+
"--show-finished", "-f", help="Show the total amount of finished paths."
|
|
106
|
+
),
|
|
107
|
+
] = False,
|
|
108
|
+
show_operation_count: Annotated[
|
|
109
|
+
bool,
|
|
110
|
+
typer.Option(
|
|
111
|
+
"--get-operation-count",
|
|
112
|
+
"-O",
|
|
113
|
+
help="Show the total amount of running operations.",
|
|
114
|
+
),
|
|
115
|
+
] = False,
|
|
116
|
+
show_empty_paths: Annotated[
|
|
117
|
+
bool, typer.Option("--show-empty-paths", "-e", help="Show all the empty paths.")
|
|
118
|
+
] = False,
|
|
119
|
+
):
|
|
120
|
+
data: Any = asyncio.run(listen_ipc())
|
|
121
|
+
|
|
122
|
+
args = [
|
|
123
|
+
show_total,
|
|
124
|
+
show_current,
|
|
125
|
+
show_operations,
|
|
126
|
+
show_operation_count,
|
|
127
|
+
show_finished_path_count,
|
|
128
|
+
show_empty_paths,
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
if not any(args) or all_args:
|
|
132
|
+
print(json.dumps(data, indent=2))
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
if show_total:
|
|
136
|
+
print(data["total_path_count"])
|
|
137
|
+
|
|
138
|
+
if show_current:
|
|
139
|
+
print(data["operation_count"])
|
|
140
|
+
|
|
141
|
+
if show_operations:
|
|
142
|
+
print(json.dumps(data["operations"], indent=2))
|
|
143
|
+
|
|
144
|
+
if show_operation_count:
|
|
145
|
+
print(data["operation_count"])
|
|
146
|
+
|
|
147
|
+
if show_finished_path_count:
|
|
148
|
+
print(data["finished_path_count"])
|
|
149
|
+
|
|
150
|
+
if show_empty_paths:
|
|
151
|
+
print(data["empty_paths"])
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
app()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
import os
|
|
3
4
|
from easyclone.utypes.enums import PathType
|
|
@@ -10,7 +11,7 @@ def organize_paths(paths: list[str], remote_name: str) -> OrganizedPaths:
|
|
|
10
11
|
root_dir = cfg.backup.root_dir
|
|
11
12
|
|
|
12
13
|
for path in paths:
|
|
13
|
-
p = Path(path
|
|
14
|
+
p = Path(os.path.expandvars(os.path.expanduser(path)))
|
|
14
15
|
|
|
15
16
|
if not os.path.exists(p):
|
|
16
17
|
empty_paths.append(path)
|
|
@@ -28,7 +29,7 @@ def organize_paths(paths: list[str], remote_name: str) -> OrganizedPaths:
|
|
|
28
29
|
"dest": f"{remote_name}:{root_dir}{dest_dir}",
|
|
29
30
|
"path_type": PathType.FILE.value
|
|
30
31
|
})
|
|
31
|
-
|
|
32
|
+
|
|
32
33
|
return {
|
|
33
34
|
"valid_paths": source_dest_array,
|
|
34
35
|
"empty_paths": empty_paths
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
class BackupConfigModel(BaseModel):
|
|
4
|
+
sync_paths: list[str]
|
|
5
|
+
copy_paths: list[str]
|
|
6
|
+
remote_name: str
|
|
7
|
+
root_dir: str
|
|
8
|
+
verbose_log: bool = False
|
|
9
|
+
|
|
10
|
+
class RcloneConfigModel(BaseModel):
|
|
11
|
+
args: list[str] = [
|
|
12
|
+
"--update",
|
|
13
|
+
"--verbose",
|
|
14
|
+
"--transfers 30",
|
|
15
|
+
"--checkers 8",
|
|
16
|
+
"--contimeout 60s",
|
|
17
|
+
"--timeout 300s",
|
|
18
|
+
"--retries 3",
|
|
19
|
+
"--low-level-retries 10",
|
|
20
|
+
"--stats 1s"
|
|
21
|
+
]
|
|
22
|
+
concurrent_limit: int = 50
|
|
23
|
+
|
|
24
|
+
class ConfigModel(BaseModel):
|
|
25
|
+
backup: BackupConfigModel
|
|
26
|
+
rclone: RcloneConfigModel = RcloneConfigModel()
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: easyclone
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Very convenient Rclone bulk backup wrapper
|
|
5
|
+
Author-email: Ahmet Arda Kavakcı <ahmetardakavakci@gmail.com>
|
|
6
|
+
Maintainer-email: Ahmet Arda Kavakcı <ahmetardakavakci@gmail.com>
|
|
7
|
+
License-Expression: GPL-3.0-or-later
|
|
5
8
|
Project-URL: Repository, https://github.com/dybdeskarphet/easyclone
|
|
6
9
|
Project-URL: Documentation, https://github.com/dybdeskarphet/easyclone
|
|
7
10
|
Project-URL: Issues, https://github.com/dybdeskarphet/easyclone/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/dybdeskarphet/easyclone/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: backup,sync,google,drive,easyclone
|
|
8
13
|
Requires-Python: >=3.13
|
|
9
14
|
Description-Content-Type: text/markdown
|
|
10
15
|
License-File: LICENSE
|
|
@@ -19,13 +24,13 @@ Dynamic: license-file
|
|
|
19
24
|
|
|
20
25
|
You define what to back up, where to back it up, and EasyClone handles the syncs and copies — clean, fast, and reliable.
|
|
21
26
|
|
|
22
|
-
##
|
|
27
|
+
## Features
|
|
23
28
|
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
+
* Sync & Copy support per-path
|
|
30
|
+
* Backup multiple paths at once
|
|
31
|
+
* Human-friendly TOML config
|
|
32
|
+
* IPC-ready architecture for future GUI or monitoring tools
|
|
33
|
+
* Optional verbose logging
|
|
29
34
|
|
|
30
35
|
## Installation
|
|
31
36
|
|
|
@@ -36,7 +41,11 @@ pip install easyclone
|
|
|
36
41
|
pipx install easyclone
|
|
37
42
|
```
|
|
38
43
|
|
|
39
|
-
##
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
The config file is at `~/.config/easyclone/config.toml`
|
|
47
|
+
|
|
48
|
+
## Requirements
|
|
40
49
|
|
|
41
50
|
* Python **3.13+**
|
|
42
51
|
* [`rclone`](https://rclone.org/) installed and accessible in your `$PATH`
|
|
@@ -44,7 +53,7 @@ pipx install easyclone
|
|
|
44
53
|
* `toml>=0.10.2`
|
|
45
54
|
* `typer>=0.16.0`
|
|
46
55
|
|
|
47
|
-
##
|
|
56
|
+
## Example Usage
|
|
48
57
|
|
|
49
58
|
```bash
|
|
50
59
|
easyclone start-backup
|
|
@@ -56,16 +65,16 @@ It will:
|
|
|
56
65
|
* Copy the paths in `copy_paths`
|
|
57
66
|
* Use the `remote_name` and `root_dir` to target your cloud storage
|
|
58
67
|
|
|
59
|
-
##
|
|
68
|
+
## Contributing
|
|
60
69
|
|
|
61
70
|
PRs welcome. Bug reports even more welcome.
|
|
62
71
|
|
|
63
|
-
##
|
|
72
|
+
## FAQ
|
|
64
73
|
|
|
65
74
|
Why does it create the folders first?
|
|
66
75
|
> Because services like Google Drive support multiple folders with the same name in the same directory. So when you try to concurrently backup paths from the same directory, it will create the parent directory more than once, and we don't want that.
|
|
67
76
|
|
|
68
|
-
##
|
|
77
|
+
## License
|
|
69
78
|
|
|
70
79
|
GPLv3 — do whatever you want, just don't blame me if you sync your `/` folder to the cloud :)
|
|
71
80
|
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from typing import Annotated, Any
|
|
3
|
-
from easyclone.config import cfg
|
|
4
|
-
from easyclone.ipc.client import listen_ipc
|
|
5
|
-
from easyclone.ipc.server import start_status_server
|
|
6
|
-
from easyclone.rclone.operations import make_backup_operation
|
|
7
|
-
import typer
|
|
8
|
-
import json
|
|
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
|
|
12
|
-
|
|
13
|
-
app = typer.Typer(
|
|
14
|
-
help="Very convenient Rclone bulk backup wrapper",
|
|
15
|
-
context_settings={"help_option_names": ["-h", "--help"]}
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
async def ipc():
|
|
19
|
-
server = await start_status_server()
|
|
20
|
-
async with server:
|
|
21
|
-
await server.serve_forever()
|
|
22
|
-
|
|
23
|
-
@app.command(help="Starts the backup process using the details in the config file.")
|
|
24
|
-
def start_backup(verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enables the rclone logging (overrides config).")] = False):
|
|
25
|
-
async def start():
|
|
26
|
-
exit_if_no_rclone()
|
|
27
|
-
exit_if_currently_running()
|
|
28
|
-
await sync_status.set_total_path_count(len(cfg.backup.sync_paths) + len(cfg.backup.copy_paths))
|
|
29
|
-
|
|
30
|
-
_ipc_task = asyncio.create_task(ipc())
|
|
31
|
-
|
|
32
|
-
verbose_state = verbose or cfg.backup.verbose_log
|
|
33
|
-
|
|
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)()
|
|
36
|
-
|
|
37
|
-
asyncio.run(start())
|
|
38
|
-
|
|
39
|
-
@app.command(help="Gets status information about the backup process.")
|
|
40
|
-
def get_status(
|
|
41
|
-
all: Annotated[bool, typer.Option("--all", "-a", help="Show all the backup status information.")] = False,
|
|
42
|
-
show_total: Annotated[bool, typer.Option("--show-total", "-t", help="Show the total amount of paths.")] = False,
|
|
43
|
-
show_current: Annotated[bool, typer.Option("--show-current", "-c", help="Show the total amount of pending paths.")] = 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
|
|
47
|
-
):
|
|
48
|
-
data: Any = asyncio.run(listen_ipc())
|
|
49
|
-
|
|
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):
|
|
51
|
-
print(json.dumps(data, indent=2))
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
if show_total:
|
|
55
|
-
print(data["total_path_count"])
|
|
56
|
-
|
|
57
|
-
if show_current:
|
|
58
|
-
print(data["operation_count"])
|
|
59
|
-
|
|
60
|
-
if show_operations:
|
|
61
|
-
print(json.dumps(data["operations"], indent=2))
|
|
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
|
-
|
|
69
|
-
if __name__ == "__main__":
|
|
70
|
-
app()
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from pydantic import BaseModel
|
|
2
|
-
|
|
3
|
-
class BackupConfigModel(BaseModel):
|
|
4
|
-
sync_paths: list[str]
|
|
5
|
-
copy_paths: list[str]
|
|
6
|
-
remote_name: str
|
|
7
|
-
root_dir: str
|
|
8
|
-
verbose_log: bool
|
|
9
|
-
|
|
10
|
-
class RcloneConfigModel(BaseModel):
|
|
11
|
-
args: list[str]
|
|
12
|
-
concurrent_limit: int
|
|
13
|
-
|
|
14
|
-
class ConfigModel(BaseModel):
|
|
15
|
-
backup: BackupConfigModel
|
|
16
|
-
rclone: RcloneConfigModel
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|