easyclone 0.1.0__py3-none-any.whl

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/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .main import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
easyclone/config.py ADDED
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+ from threading import Lock
3
+ from os import getenv
4
+ from pathlib import Path
5
+ from easyclone.utypes.enums import LogLevel
6
+ from easyclone.utypes.config import BackupConfigModel, ConfigModel, RcloneConfigModel
7
+ import toml
8
+
9
+ class Config:
10
+ _instance: Config | None = None
11
+ _lock: Lock = Lock()
12
+ _path: Path = Path.home() / '.config' / "easyclone" / "config.toml"
13
+ _config: ConfigModel | None = None
14
+
15
+ def __new__(cls):
16
+ if cls._instance is None:
17
+ with cls._lock:
18
+ instance = super().__new__(cls)
19
+ instance._get_config_path()
20
+ instance._config = instance._load_config()
21
+ cls._instance = instance
22
+
23
+ return cls._instance
24
+
25
+ def _get_config_path(self):
26
+ xdg_config_home = getenv("XDG_CONFIG_HOME")
27
+
28
+ empty_config = ConfigModel(
29
+ backup=BackupConfigModel(
30
+ sync_paths=[],
31
+ copy_paths=[],
32
+ remote_name="GoogleDrive",
33
+ 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
+ )
50
+ )
51
+
52
+ if xdg_config_home:
53
+ config_dir = Path(xdg_config_home) / 'easyclone'
54
+ else:
55
+ config_dir = Path.home() / '.config' / "easyclone"
56
+
57
+ config_file = config_dir / "config.toml"
58
+
59
+ if not config_dir.exists():
60
+ config_dir.mkdir(parents=True, exist_ok=True)
61
+
62
+ if not config_file.exists():
63
+ config_file.touch()
64
+ with open(config_file, "w") as f:
65
+ _ = toml.dump(empty_config.model_dump(), f)
66
+
67
+ self._path = config_file
68
+
69
+ def _load_config(self):
70
+ from easyclone.utils.essentials import log
71
+ self._get_config_path()
72
+
73
+
74
+ try:
75
+ with open(self._path) as f:
76
+ parsed_string = f.read()
77
+ except FileNotFoundError:
78
+ log(f"Config file not found at {self._path}.", LogLevel.ERROR)
79
+ exit(1)
80
+ except Exception as e:
81
+ log(f"Error while opening the config file {self._path}: {e}", LogLevel.ERROR)
82
+ exit(1)
83
+
84
+ try:
85
+ parsed_toml = toml.loads(parsed_string)
86
+ validated_config = ConfigModel.model_validate(parsed_toml)
87
+ return validated_config
88
+ except Exception as e:
89
+ log(f"Invalid config: {e}", LogLevel.ERROR)
90
+ exit(1)
91
+
92
+ def config(self) -> ConfigModel:
93
+ if self._config is None:
94
+ raise RuntimeError("Config is not loaded yet")
95
+ return self._config
96
+
97
+ cfg = Config().config()
@@ -0,0 +1,2 @@
1
+ from .server import start_status_server
2
+ from .client import listen_ipc
@@ -0,0 +1,26 @@
1
+ import asyncio
2
+ import json
3
+
4
+ from easyclone.utils.essentials import log
5
+ from easyclone.utypes.enums import LogLevel
6
+
7
+ SOCKET_PATH = "/tmp/easyclone.sock"
8
+
9
+ async def listen_ipc():
10
+ try:
11
+ reader, writer = await asyncio.open_unix_connection(SOCKET_PATH)
12
+ except (FileNotFoundError, ConnectionRefusedError):
13
+ log("No tasks are running at the moment.", LogLevel.ERROR)
14
+ exit(1)
15
+
16
+ try:
17
+ line = await reader.readline()
18
+ if not line:
19
+ log("No tasks are running at the moment", LogLevel.ERROR)
20
+ exit(1)
21
+
22
+ data = json.loads(line.decode())
23
+ return data
24
+ finally:
25
+ writer.close()
26
+ await writer.wait_closed()
@@ -0,0 +1,50 @@
1
+ import asyncio
2
+ import json
3
+ from pathlib import Path
4
+ from easyclone.utypes.enums import LogLevel
5
+ from easyclone.shared import sync_status
6
+ from easyclone.utils.essentials import log
7
+
8
+ SOCKET_PATH = "/tmp/easyclone.sock"
9
+
10
+ async def handle_client(_reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
11
+ try:
12
+ while True:
13
+ message = json.dumps({
14
+ "total_path_count": await sync_status.get_total_path(),
15
+ "finished_path_count": await sync_status.get_currently_finished(),
16
+ "operation_count": await sync_status.get_operation_count(),
17
+ "operations": await sync_status.get_operations()
18
+ }).encode() + b"\n"
19
+
20
+ writer.write(message)
21
+
22
+ await writer.drain()
23
+ await asyncio.sleep(0.5)
24
+ except (ConnectionResetError, BrokenPipeError, asyncio.IncompleteReadError):
25
+ pass
26
+ except Exception as e:
27
+ print(f"Unexpected error: {e}")
28
+ finally:
29
+ try:
30
+ writer.close()
31
+ await writer.wait_closed()
32
+ except BrokenPipeError:
33
+ pass
34
+
35
+ async def start_status_server():
36
+ try:
37
+ Path(SOCKET_PATH).unlink()
38
+ except FileNotFoundError as e:
39
+ pass
40
+ except Exception as e:
41
+ log(f"Something happened while connecting to the socket for status server: {e}", LogLevel.ERROR)
42
+ raise
43
+
44
+ try:
45
+ server = await asyncio.start_unix_server(handle_client, path=SOCKET_PATH)
46
+ except Exception as e:
47
+ log(f"Couldn't create the UNIX server: {e}", LogLevel.ERROR)
48
+ raise
49
+
50
+ return server
easyclone/main.py ADDED
@@ -0,0 +1,59 @@
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 backup_copy_operation, backup_sync_operation
7
+ import typer
8
+ import json
9
+
10
+ from easyclone.shared import sync_status
11
+
12
+ app = typer.Typer(
13
+ help="Very convenient Rclone bulk backup wrapper",
14
+ context_settings={"help_option_names": ["-h", "--help"]}
15
+ )
16
+
17
+ async def ipc():
18
+ server = await start_status_server()
19
+ async with server:
20
+ await server.serve_forever()
21
+
22
+ @app.command(help="Starts the backup process using the details in the config file.")
23
+ def start_backup(verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enables the rclone logging (overrides config).")] = False):
24
+ async def start():
25
+ await sync_status.set_total_path_count(len(cfg.backup.sync_paths) + len(cfg.backup.copy_paths))
26
+
27
+ _ipc_task = asyncio.create_task(ipc())
28
+
29
+ verbose_state = verbose or cfg.backup.verbose_log
30
+
31
+ await backup_copy_operation(verbose_state)
32
+ await backup_sync_operation(verbose_state)
33
+
34
+ asyncio.run(start())
35
+
36
+ @app.command(help="Gets status information about the backup process.")
37
+ def get_status(
38
+ all: Annotated[bool, typer.Option("--all", "-a", help="Show all the backup status information.")] = False,
39
+ show_total: Annotated[bool, typer.Option("--show-total", "-t", help="Show the total amount of paths.")] = False,
40
+ 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
42
+ ):
43
+ data: Any = asyncio.run(listen_ipc())
44
+
45
+ if (not show_total and not show_current and not show_operations or all) or (show_total and show_current and show_operations):
46
+ print(json.dumps(data, indent=2))
47
+ return
48
+
49
+ if show_total:
50
+ print(data["total_path_count"])
51
+
52
+ if show_current:
53
+ print(data["operation_count"])
54
+
55
+ if show_operations:
56
+ print(json.dumps(data["operations"], indent=2))
57
+
58
+ if __name__ == "__main__":
59
+ app()
@@ -0,0 +1,3 @@
1
+ from .backup import backup
2
+ from .create_dirs import create_dirs_array
3
+ from .operations import backup_sync_operation, backup_copy_operation
@@ -0,0 +1,59 @@
1
+ import asyncio
2
+ import shlex
3
+ from easyclone.shared import sync_status
4
+ from easyclone.utypes.enums import BackupLog, BackupStatus, CommandType, LogLevel, RcloneOperationType
5
+ from easyclone.utypes.models import PathItem
6
+ from easyclone.utils.essentials import log
7
+ from easyclone.utils.path_manipulation import collapseuser
8
+
9
+ async def backup_command(rclone_command: list[str], source: str, dest: str, path_type: str, command_type: CommandType, verbose: bool = False):
10
+ cmd = rclone_command + [source, dest]
11
+ operation_name = command_type.value.capitalize() + "ing"
12
+
13
+ log(f"{operation_name} {collapseuser(source)}", BackupLog.WAIT)
14
+ process = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
15
+ process_id = await sync_status.add_operation(source=source, dest=dest, path_type=path_type, status=BackupStatus.IN_PROGRESS, operation_type=RcloneOperationType.BACKUP)
16
+
17
+ stdout, stderr = await process.communicate()
18
+ stdout = stdout.decode(errors="ignore").strip()
19
+ stderr = stderr.decode(errors="ignore").strip()
20
+
21
+ collapsed_source = collapseuser(source)
22
+ match process.returncode:
23
+ case 0:
24
+ log(f"Backed up successfully: {collapsed_source}", BackupLog.OK)
25
+ case 1:
26
+ log(f"Back up operation failed: {collapsed_source}", BackupLog.ERR)
27
+ case _:
28
+ log(f"Back up operation failed with {process.returncode} exit code: {collapsed_source}", BackupLog.ERR)
29
+
30
+ await sync_status.delete_operation(process_id)
31
+ await sync_status.add_currently_finished()
32
+
33
+ if verbose:
34
+ if stderr:
35
+ log(f"{stderr}", LogLevel.WARN)
36
+ if stdout:
37
+ log(f"{stdout}", LogLevel.LOG)
38
+
39
+ async def backup(paths: list[PathItem], command_type: CommandType, rclone_args: list[str], semaphore: asyncio.Semaphore, verbose: bool = False):
40
+ cmd = ["rclone", command_type.value]
41
+
42
+
43
+ for arg in rclone_args:
44
+ parts = shlex.split(arg)
45
+ cmd += parts
46
+
47
+ # I have to do this because python doesn't have anon coroutines
48
+ async def backup_task(source: str, dest: str, path_type: str):
49
+ async with semaphore:
50
+ await backup_command(cmd, source, dest, path_type, command_type, verbose)
51
+
52
+
53
+ tasks = [
54
+ backup_task(path["source"], path["dest"], path["path_type"])
55
+ for path in paths
56
+ ]
57
+
58
+ return await asyncio.gather(*tasks)
59
+
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+ from easyclone.shared import sync_status
4
+ from easyclone.utils.essentials import log
5
+ from easyclone.utypes.models import PathItem
6
+ from easyclone.utypes.enums import BackupLog, BackupStatus, LogLevel, PathType, RcloneOperationType
7
+ import os
8
+ from pathlib import Path
9
+ from collections import deque
10
+ from easyclone.config import cfg
11
+
12
+ class DirNode:
13
+ def __init__(self, name: str, details: PathItem):
14
+ self.name: str = name
15
+ self.details: PathItem = details
16
+ self.children: list[DirNode] = []
17
+
18
+ def find_child(self, name: str) -> "DirNode | None":
19
+ for child in self.children:
20
+ if child.name == name:
21
+ return child
22
+ return None
23
+
24
+ def add_child(self, name: str, details: PathItem) -> "DirNode":
25
+ existing = self.find_child(name)
26
+ if existing:
27
+ return existing
28
+ new_child = DirNode(name, details)
29
+ self.children.append(new_child)
30
+ return new_child
31
+
32
+ def print_tree(self, level: int=0):
33
+ indent = " " * level
34
+ print(f"{indent}- {self.name} ({self.details.get('dest')})")
35
+ for child in sorted(self.children, key=lambda c: c.name):
36
+ child.print_tree(level + 1)
37
+
38
+ def create_dirs_array(path_list: list[PathItem]):
39
+ only_dirs: list[PathItem] = []
40
+
41
+ for path in path_list:
42
+ new_dir_path: PathItem
43
+
44
+ if path.get("type") == "dir":
45
+ new_dir_path = path
46
+ else:
47
+ new_dir_path = {
48
+ "source": os.path.dirname(path.get("source")),
49
+ "dest": path.get("dest"),
50
+ "path_type": PathType.DIR.value
51
+ }
52
+
53
+ if new_dir_path not in only_dirs:
54
+ only_dirs.append(new_dir_path)
55
+
56
+ return only_dirs
57
+
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",})
60
+
61
+ for path_item in path_list:
62
+ source_str = path_item.get("source")
63
+ dest_str = path_item.get("dest")
64
+
65
+ source_parts = Path(source_str).parts
66
+ dest_main, _, dest_path = dest_str.partition(":")
67
+
68
+ if dest_path:
69
+ dest_parts = Path(dest_path).parts
70
+ else:
71
+ dest_parts = []
72
+
73
+ current = root
74
+
75
+ for i in range(1, len(source_parts)):
76
+ source_sub_path = str(Path(*source_parts[:i+1]))
77
+
78
+ if dest_path:
79
+ dest_sub_path = f"{dest_main}:{Path(*dest_parts[:i+2])}"
80
+ else:
81
+ dest_sub_path = ""
82
+
83
+ node_details: PathItem = {
84
+ "source": source_sub_path,
85
+ "dest": dest_sub_path,
86
+ "path_type": PathType.DIR.value
87
+ }
88
+
89
+ part_name = source_parts[i]
90
+ current = current.add_child(part_name, node_details)
91
+
92
+ return root
93
+
94
+ async def create_folder_command(source: str, dest: str, verbose: bool):
95
+ lsd_cmd = ["rclone", "lsd", dest]
96
+ log(f"Checking if directory exist at {dest}", BackupLog.WAIT)
97
+
98
+ process = await asyncio.create_subprocess_exec(*lsd_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
99
+ stdout, stderr = await process.communicate()
100
+
101
+ if process.returncode == 0:
102
+ log(f"Directory exist at {dest}", BackupLog.OK)
103
+ return
104
+
105
+ mkdir_cmd = ["rclone", "mkdir", dest]
106
+
107
+ log(f"Creating a directory at {dest}", BackupLog.WAIT)
108
+ process = await asyncio.create_subprocess_exec(*mkdir_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
109
+ process_id = await sync_status.add_operation(source=source, dest=dest, path_type=PathType.DIR.value, status=BackupStatus.IN_PROGRESS, operation_type=RcloneOperationType.MKDIR)
110
+
111
+ stdout, stderr = await process.communicate()
112
+ stdout = stdout.decode(errors="ignore").strip()
113
+ stderr = stderr.decode(errors="ignore").strip()
114
+
115
+ match process.returncode:
116
+ case 0:
117
+ log(f"Directory created successfully: {dest}", BackupLog.OK)
118
+ case 1:
119
+ log(f"Couldn't create the directory: {dest}", BackupLog.ERR)
120
+ case _:
121
+ log(f"Operation failed with {process.returncode} exit code: {dest}", BackupLog.ERR)
122
+
123
+ await sync_status.delete_operation(process_id)
124
+
125
+ if verbose:
126
+ if stderr:
127
+ log(f"{stderr}", LogLevel.WARN)
128
+ if stdout:
129
+ log(f"{stdout}", LogLevel.LOG)
130
+
131
+ async def create_folders_on_remote(nodes: list[DirNode], semaphore: asyncio.Semaphore, verbose: bool):
132
+ async def mkdir_task(source: str, dest: str):
133
+ async with semaphore:
134
+ await create_folder_command(source, dest, verbose)
135
+
136
+ tasks = [mkdir_task(node.details.get("source"), node.details.get("dest")) for node in nodes]
137
+
138
+ _ = await asyncio.gather(*tasks)
139
+
140
+ async def traverse_and_create_folders_by_depth(root: DirNode, verbose: bool, semaphore: asyncio.Semaphore):
141
+ queue = deque([(root, 0)])
142
+ current_depth = 0
143
+ current_level_nodes: list[DirNode] = []
144
+
145
+ while queue:
146
+ node, depth = queue.popleft()
147
+
148
+ if depth != current_depth:
149
+ await create_folders_on_remote(current_level_nodes, semaphore, verbose)
150
+ current_level_nodes = []
151
+ current_depth = depth
152
+
153
+ current_level_nodes.append(node)
154
+
155
+ for child in node.children:
156
+ queue.append((child, depth + 1))
157
+
158
+ if current_level_nodes:
159
+ await create_folders_on_remote(current_level_nodes, semaphore, verbose)
@@ -0,0 +1,48 @@
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
+ )
@@ -0,0 +1,3 @@
1
+ from .sync_status import SyncStatus
2
+
3
+ sync_status = SyncStatus()
@@ -0,0 +1,61 @@
1
+ import asyncio
2
+ from easyclone.utypes.enums import BackupStatus, RcloneOperationType
3
+ from easyclone.utypes.models import SyncStatusItem
4
+ import uuid
5
+
6
+ class SyncStatus:
7
+ def __init__(self):
8
+ self.total_path_count: int = 0
9
+ self.operations: list[SyncStatusItem] = []
10
+ self.lock: asyncio.Lock = asyncio.Lock()
11
+ self.finished_path_count: int = 0
12
+
13
+ async def add_operation(self, source: str, dest: str, path_type: str, status: BackupStatus, operation_type: RcloneOperationType):
14
+ random_id = str(uuid.uuid4())
15
+
16
+ async with self.lock:
17
+ self.operations.append({
18
+ "id": random_id,
19
+ "source": source,
20
+ "dest": dest,
21
+ "status": status.value,
22
+ "path_type": path_type,
23
+ "operation_type": operation_type.value
24
+ })
25
+
26
+ return random_id
27
+
28
+ async def delete_operation(self, target_id: str):
29
+ async with self.lock:
30
+ for index, item in enumerate(self.operations):
31
+ if item.get("id") == target_id:
32
+ del self.operations[index]
33
+ break;
34
+
35
+ async def get_operations(self):
36
+ async with self.lock:
37
+ return self.operations
38
+
39
+ async def get_operation_count(self):
40
+ async with self.lock:
41
+ return len(self.operations)
42
+
43
+ async def set_total_path_count(self, count: int):
44
+ async with self.lock:
45
+ self.total_path_count = count
46
+
47
+ async def reset_total_paths(self):
48
+ async with self.lock:
49
+ self.total_path_count = 0
50
+
51
+ async def get_total_path(self):
52
+ async with self.lock:
53
+ return self.total_path_count
54
+
55
+ async def add_currently_finished(self):
56
+ async with self.lock:
57
+ self.finished_path_count += 1
58
+
59
+ async def get_currently_finished(self):
60
+ async with self.lock:
61
+ return self.finished_path_count
@@ -0,0 +1,2 @@
1
+ from .essentials import log
2
+ from .path_manipulation import organize_paths, collapseuser
@@ -0,0 +1,24 @@
1
+ from easyclone.utypes.enums import LogLevel, BackupLog
2
+
3
+ def log(message: str, logtype: LogLevel | BackupLog) -> None:
4
+ color = "\033[32;1m"
5
+
6
+ match logtype:
7
+ case LogLevel.ERROR | BackupLog.ERR:
8
+ # RED
9
+ color = "\033[31;1m"
10
+ case LogLevel.LOG:
11
+ # BLUE
12
+ color = "\033[34;1m"
13
+ case LogLevel.INFO | BackupLog.OK:
14
+ # GREEN
15
+ color = "\033[32;1m"
16
+ case LogLevel.WARN | BackupLog.WAIT:
17
+ # YELLOW
18
+ color = "\033[33;1m"
19
+
20
+ full_msg = f"{color}{logtype.value}"
21
+ if(isinstance(logtype, LogLevel)):
22
+ full_msg = full_msg + ":"
23
+
24
+ print(f"{full_msg}\033[0m {message}")
@@ -0,0 +1,37 @@
1
+ from pathlib import Path
2
+ import os
3
+ from easyclone.utypes.enums import PathType
4
+ from easyclone.utypes.models import PathItem
5
+
6
+ def organize_paths(paths: list[str], remote_name: str) -> list[PathItem]:
7
+ from easyclone.config import cfg
8
+ source_dest_array: list[PathItem] = []
9
+ root_dir = cfg.backup.root_dir
10
+
11
+ for path in paths:
12
+ p = Path(path).expanduser()
13
+
14
+ if p.is_dir():
15
+ source_dest_array.append({
16
+ "source": f"{p}",
17
+ "dest": f"{remote_name}:{root_dir}{p}",
18
+ "path_type": PathType.DIR.value
19
+ })
20
+ elif p.is_file():
21
+ dest_dir = p.parent
22
+ source_dest_array.append({
23
+ "source": f"{p}",
24
+ "dest": f"{remote_name}:{root_dir}{dest_dir}",
25
+ "path_type": PathType.FILE.value
26
+ })
27
+
28
+ return source_dest_array
29
+
30
+ def collapseuser(path: str) -> str:
31
+ """
32
+ Opposite of path.expanduser()
33
+ """
34
+ home = os.path.expanduser("~")
35
+ if path.startswith(home):
36
+ return path.replace(home, "~", 1)
37
+ return path
@@ -0,0 +1,3 @@
1
+ from .config import *
2
+ from .enums import *
3
+ from .models import *
@@ -0,0 +1,16 @@
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