easyclone 0.1.0__tar.gz → 0.3.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.
Files changed (32) hide show
  1. {easyclone-0.1.0/src/easyclone.egg-info → easyclone-0.3.0}/PKG-INFO +25 -12
  2. {easyclone-0.1.0 → easyclone-0.3.0}/README.md +24 -11
  3. {easyclone-0.1.0 → easyclone-0.3.0}/pyproject.toml +1 -1
  4. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/config.py +7 -17
  5. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/ipc/server.py +1 -0
  6. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/main.py +17 -6
  7. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/rclone/__init__.py +1 -1
  8. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/rclone/backup.py +0 -1
  9. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/rclone/create_dirs.py +1 -1
  10. easyclone-0.3.0/src/easyclone/rclone/operations.py +35 -0
  11. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/shared/sync_status.py +10 -1
  12. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/utils/__init__.py +1 -1
  13. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/utils/essentials.py +15 -0
  14. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/utils/path_manipulation.py +13 -5
  15. easyclone-0.3.0/src/easyclone/utypes/config.py +26 -0
  16. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/utypes/models.py +5 -0
  17. {easyclone-0.1.0 → easyclone-0.3.0/src/easyclone.egg-info}/PKG-INFO +25 -12
  18. easyclone-0.1.0/src/easyclone/rclone/operations.py +0 -48
  19. easyclone-0.1.0/src/easyclone/utypes/config.py +0 -16
  20. {easyclone-0.1.0 → easyclone-0.3.0}/LICENSE +0 -0
  21. {easyclone-0.1.0 → easyclone-0.3.0}/setup.cfg +0 -0
  22. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/__main__.py +0 -0
  23. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/ipc/__init__.py +0 -0
  24. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/ipc/client.py +0 -0
  25. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/shared/__init__.py +0 -0
  26. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/utypes/__init__.py +0 -0
  27. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone/utypes/enums.py +0 -0
  28. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone.egg-info/SOURCES.txt +0 -0
  29. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone.egg-info/dependency_links.txt +0 -0
  30. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone.egg-info/entry_points.txt +0 -0
  31. {easyclone-0.1.0 → easyclone-0.3.0}/src/easyclone.egg-info/requires.txt +0 -0
  32. {easyclone-0.1.0 → easyclone-0.3.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.1.0
3
+ Version: 0.3.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
@@ -19,15 +19,28 @@ Dynamic: license-file
19
19
 
20
20
  You define what to back up, where to back it up, and EasyClone handles the syncs and copies — clean, fast, and reliable.
21
21
 
22
- ## 🚀 Features
22
+ ## Features
23
23
 
24
- * 🔁 Sync & Copy support per-path
25
- * 📁 Backup multiple paths at once
26
- * 🧠 Human-friendly TOML config
27
- * 🛠️ IPC-ready architecture for future GUI or monitoring tools
28
- * 🔊 Optional verbose logging
24
+ * Sync & Copy support per-path
25
+ * Backup multiple paths at once
26
+ * Human-friendly TOML config
27
+ * IPC-ready architecture for future GUI or monitoring tools
28
+ * Optional verbose logging
29
29
 
30
- ## 🛠️ Requirements
30
+ ## Installation
31
+
32
+ Install it with `pip` or `pipx`:
33
+
34
+ ```bash
35
+ pip install easyclone
36
+ pipx install easyclone
37
+ ```
38
+
39
+ ## Configuration
40
+
41
+ The config file is at `~/.config/easyclone/config.toml`
42
+
43
+ ## Requirements
31
44
 
32
45
  * Python **3.13+**
33
46
  * [`rclone`](https://rclone.org/) installed and accessible in your `$PATH`
@@ -35,7 +48,7 @@ You define what to back up, where to back it up, and EasyClone handles the syncs
35
48
  * `toml>=0.10.2`
36
49
  * `typer>=0.16.0`
37
50
 
38
- ## 🧪 Example Usage
51
+ ## Example Usage
39
52
 
40
53
  ```bash
41
54
  easyclone start-backup
@@ -47,16 +60,16 @@ It will:
47
60
  * Copy the paths in `copy_paths`
48
61
  * Use the `remote_name` and `root_dir` to target your cloud storage
49
62
 
50
- ## 🙋‍♀️ Contributing
63
+ ## Contributing
51
64
 
52
65
  PRs welcome. Bug reports even more welcome.
53
66
 
54
- ## FAQ
67
+ ## FAQ
55
68
 
56
69
  Why does it create the folders first?
57
70
  > 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.
58
71
 
59
- ## 📄 License
72
+ ## License
60
73
 
61
74
  GPLv3 — do whatever you want, just don't blame me if you sync your `/` folder to the cloud :)
62
75
 
@@ -4,15 +4,28 @@
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
- ## 🚀 Features
7
+ ## Features
8
8
 
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
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
- ## 🛠️ Requirements
15
+ ## Installation
16
+
17
+ Install it with `pip` or `pipx`:
18
+
19
+ ```bash
20
+ pip install easyclone
21
+ pipx install easyclone
22
+ ```
23
+
24
+ ## Configuration
25
+
26
+ The config file is at `~/.config/easyclone/config.toml`
27
+
28
+ ## Requirements
16
29
 
17
30
  * Python **3.13+**
18
31
  * [`rclone`](https://rclone.org/) installed and accessible in your `$PATH`
@@ -20,7 +33,7 @@ You define what to back up, where to back it up, and EasyClone handles the syncs
20
33
  * `toml>=0.10.2`
21
34
  * `typer>=0.16.0`
22
35
 
23
- ## 🧪 Example Usage
36
+ ## Example Usage
24
37
 
25
38
  ```bash
26
39
  easyclone start-backup
@@ -32,16 +45,16 @@ It will:
32
45
  * Copy the paths in `copy_paths`
33
46
  * Use the `remote_name` and `root_dir` to target your cloud storage
34
47
 
35
- ## 🙋‍♀️ Contributing
48
+ ## Contributing
36
49
 
37
50
  PRs welcome. Bug reports even more welcome.
38
51
 
39
- ## FAQ
52
+ ## FAQ
40
53
 
41
54
  Why does it create the folders first?
42
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.
43
56
 
44
- ## 📄 License
57
+ ## License
45
58
 
46
59
  GPLv3 — do whatever you want, just don't blame me if you sync your `/` folder to the cloud :)
47
60
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "easyclone"
3
- version = "0.1.0"
3
+ version = "0.3.0"
4
4
  description = "Very convenient Rclone bulk backup wrapper"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -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, RcloneConfigModel
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,11 +52,14 @@ 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()
72
62
 
73
-
74
63
  try:
75
64
  with open(self._path) as f:
76
65
  parsed_string = f.read()
@@ -84,6 +73,7 @@ class Config:
84
73
  try:
85
74
  parsed_toml = toml.loads(parsed_string)
86
75
  validated_config = ConfigModel.model_validate(parsed_toml)
76
+ validated_config = self._config_normalize(validated_config)
87
77
  return validated_config
88
78
  except Exception as e:
89
79
  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 backup_copy_operation, backup_sync_operation
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 backup_copy_operation(verbose_state)
32
- await backup_sync_operation(verbose_state)
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()
@@ -1,3 +1,3 @@
1
1
  from .backup import backup
2
2
  from .create_dirs import create_dirs_array
3
- from .operations import backup_sync_operation, backup_copy_operation
3
+ from .operations import make_backup_operation
@@ -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}:{str(cfg.backup.root_dir).rstrip("/")}", "path_type": "dir",})
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,15 +1,20 @@
1
+ import json
1
2
  from pathlib import Path
2
3
  import os
3
4
  from easyclone.utypes.enums import PathType
4
- from easyclone.utypes.models import PathItem
5
+ from easyclone.utypes.models import OrganizedPaths, PathItem
5
6
 
6
- def organize_paths(paths: list[str], remote_name: str) -> list[PathItem]:
7
+ def organize_paths(paths: list[str], remote_name: str) -> OrganizedPaths:
7
8
  from easyclone.config import cfg
8
9
  source_dest_array: list[PathItem] = []
10
+ empty_paths: list[str] = []
9
11
  root_dir = cfg.backup.root_dir
10
12
 
11
13
  for path in paths:
12
- p = Path(path).expanduser()
14
+ p = Path(os.path.expandvars(os.path.expanduser(path)))
15
+
16
+ if not os.path.exists(p):
17
+ empty_paths.append(path)
13
18
 
14
19
  if p.is_dir():
15
20
  source_dest_array.append({
@@ -24,8 +29,11 @@ def organize_paths(paths: list[str], remote_name: str) -> list[PathItem]:
24
29
  "dest": f"{remote_name}:{root_dir}{dest_dir}",
25
30
  "path_type": PathType.FILE.value
26
31
  })
27
-
28
- return source_dest_array
32
+
33
+ return {
34
+ "valid_paths": source_dest_array,
35
+ "empty_paths": empty_paths
36
+ }
29
37
 
30
38
  def collapseuser(path: str) -> str:
31
39
  """
@@ -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()
@@ -9,3 +9,8 @@ class SyncStatusItem(PathItem):
9
9
  id: str
10
10
  status: str
11
11
  operation_type: str
12
+
13
+ class OrganizedPaths(TypedDict):
14
+ valid_paths: list[PathItem]
15
+ empty_paths: list[str]
16
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyclone
3
- Version: 0.1.0
3
+ Version: 0.3.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
@@ -19,15 +19,28 @@ Dynamic: license-file
19
19
 
20
20
  You define what to back up, where to back it up, and EasyClone handles the syncs and copies — clean, fast, and reliable.
21
21
 
22
- ## 🚀 Features
22
+ ## Features
23
23
 
24
- * 🔁 Sync & Copy support per-path
25
- * 📁 Backup multiple paths at once
26
- * 🧠 Human-friendly TOML config
27
- * 🛠️ IPC-ready architecture for future GUI or monitoring tools
28
- * 🔊 Optional verbose logging
24
+ * Sync & Copy support per-path
25
+ * Backup multiple paths at once
26
+ * Human-friendly TOML config
27
+ * IPC-ready architecture for future GUI or monitoring tools
28
+ * Optional verbose logging
29
29
 
30
- ## 🛠️ Requirements
30
+ ## Installation
31
+
32
+ Install it with `pip` or `pipx`:
33
+
34
+ ```bash
35
+ pip install easyclone
36
+ pipx install easyclone
37
+ ```
38
+
39
+ ## Configuration
40
+
41
+ The config file is at `~/.config/easyclone/config.toml`
42
+
43
+ ## Requirements
31
44
 
32
45
  * Python **3.13+**
33
46
  * [`rclone`](https://rclone.org/) installed and accessible in your `$PATH`
@@ -35,7 +48,7 @@ You define what to back up, where to back it up, and EasyClone handles the syncs
35
48
  * `toml>=0.10.2`
36
49
  * `typer>=0.16.0`
37
50
 
38
- ## 🧪 Example Usage
51
+ ## Example Usage
39
52
 
40
53
  ```bash
41
54
  easyclone start-backup
@@ -47,16 +60,16 @@ It will:
47
60
  * Copy the paths in `copy_paths`
48
61
  * Use the `remote_name` and `root_dir` to target your cloud storage
49
62
 
50
- ## 🙋‍♀️ Contributing
63
+ ## Contributing
51
64
 
52
65
  PRs welcome. Bug reports even more welcome.
53
66
 
54
- ## FAQ
67
+ ## FAQ
55
68
 
56
69
  Why does it create the folders first?
57
70
  > 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.
58
71
 
59
- ## 📄 License
72
+ ## License
60
73
 
61
74
  GPLv3 — do whatever you want, just don't blame me if you sync your `/` folder to the cloud :)
62
75
 
@@ -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
- )
@@ -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