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.
Files changed (31) hide show
  1. {easyclone-0.1.0/src/easyclone.egg-info → easyclone-0.2.0}/PKG-INFO +10 -1
  2. {easyclone-0.1.0 → easyclone-0.2.0}/README.md +9 -0
  3. {easyclone-0.1.0 → easyclone-0.2.0}/pyproject.toml +1 -1
  4. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/config.py +4 -1
  5. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/ipc/server.py +1 -0
  6. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/main.py +17 -6
  7. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/rclone/__init__.py +1 -1
  8. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/rclone/backup.py +0 -1
  9. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/rclone/create_dirs.py +1 -1
  10. easyclone-0.2.0/src/easyclone/rclone/operations.py +35 -0
  11. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/shared/sync_status.py +10 -1
  12. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utils/__init__.py +1 -1
  13. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utils/essentials.py +15 -0
  14. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utils/path_manipulation.py +10 -3
  15. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utypes/models.py +5 -0
  16. {easyclone-0.1.0 → easyclone-0.2.0/src/easyclone.egg-info}/PKG-INFO +10 -1
  17. easyclone-0.1.0/src/easyclone/rclone/operations.py +0 -48
  18. {easyclone-0.1.0 → easyclone-0.2.0}/LICENSE +0 -0
  19. {easyclone-0.1.0 → easyclone-0.2.0}/setup.cfg +0 -0
  20. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/__main__.py +0 -0
  21. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/ipc/__init__.py +0 -0
  22. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/ipc/client.py +0 -0
  23. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/shared/__init__.py +0 -0
  24. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utypes/__init__.py +0 -0
  25. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utypes/config.py +0 -0
  26. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone/utypes/enums.py +0 -0
  27. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone.egg-info/SOURCES.txt +0 -0
  28. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone.egg-info/dependency_links.txt +0 -0
  29. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone.egg-info/entry_points.txt +0 -0
  30. {easyclone-0.1.0 → easyclone-0.2.0}/src/easyclone.egg-info/requires.txt +0 -0
  31. {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.1.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+**
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "easyclone"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Very convenient Rclone bulk backup wrapper"
5
5
  readme = "README.md"
6
6
  requires-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 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,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) -> list[PathItem]:
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 source_dest_array
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
  """
@@ -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.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