Compare commits

..

6 Commits

Author SHA1 Message Date
dcca9c5167 Merge branch 'errors' into dev
Raise Exceptions instead of using return codes
2026-01-30 17:43:01 +01:00
041ede22e1 paths : make TimeoutExpired handling clearer 2026-01-30 17:41:53 +01:00
adfded92d0 synchroniser : raise error instead of returning a value
Raise a FatalSyncError when the synchronisation fails instead of
returning the unison return code.
2026-01-30 17:40:41 +01:00
7fae1b154a errors : Derive Errors from Exception
According to the docs user defined Exceptions should derive from
Exception not BaseException.
2026-01-30 17:39:28 +01:00
a922eaa542 synchroniser : use exception instead of return codes
In create_ssh_master_connection return codes where used instead of
proper error handling with exception, replace these codes with the
raising of an appropriate exception.
2026-01-20 22:23:09 +01:00
8836a0120b errors : adds UnknownSSHError
This error is used to report that an unknown error happened during an
invocation of ssh.
2026-01-20 22:22:00 +01:00
3 changed files with 41 additions and 26 deletions

View File

@@ -1,8 +1,14 @@
# Copyright (C) 2025-2026 Paul Retourné # Copyright (C) 2025-2026 Paul Retourné
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
class RemoteMountedError(BaseException): class RemoteMountedError(Exception):
pass pass
class InvalidMountError(BaseException): class InvalidMountError(Exception):
pass
class UnknownSSHError(Exception):
pass
class FatalSyncError(Exception):
pass pass

View File

@@ -73,9 +73,9 @@ class PathsManager:
try: try:
paths = self.user_select_files() paths = self.user_select_files()
break break
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired as e:
if input("Timeout expired do you want to retry (y/n): ") != "y": if input("Timeout expired do you want to retry (y/n): ") != "y":
raise raise e
self.write_new_paths(paths) self.write_new_paths(paths)
def get_paths_to_sync(self) -> list[str]: def get_paths_to_sync(self) -> list[str]:

View File

@@ -16,7 +16,7 @@ import logging
from pathlib import Path from pathlib import Path
from typing import cast from typing import cast
from unisync.errors import RemoteMountedError, InvalidMountError from unisync.errors import RemoteMountedError, InvalidMountError, UnknownSSHError, FatalSyncError
from unisync.config import BackupConfig from unisync.config import BackupConfig
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -92,7 +92,7 @@ class Synchroniser:
f"Name {backup.backupprefix[:-1]}" f"Name {backup.backupprefix[:-1]}"
]) ])
def create_ssh_master_connection(self, control_path:str="~/.ssh/control_%C", connection_timeout:int=60) -> int: def create_ssh_master_connection(self, control_path:str="~/.ssh/control_%C", connection_timeout:int=60) -> None:
"""Creates an ssh master connection. """Creates an ssh master connection.
It is used so the user only has to authenticate once to the remote server. It is used so the user only has to authenticate once to the remote server.
@@ -105,8 +105,14 @@ class Synchroniser:
connection_timeout: connection_timeout:
Time given to the user to authenticate to the remote server. Time given to the user to authenticate to the remote server.
On slow connections one might want to increase this. On slow connections one might want to increase this.
Returns:
An error code (0 success, 1 TimeoutExpired, 2 KeyboardInterrupt). Raises:
subprocess.TimeoutExpired:
The user didn't finish loging in in time.
KeyboardInterrupt:
The user interrupted the process.
UnknownSSHError:
An error occured during the connection.
""" """
self.control_path = os.path.expanduser(control_path) self.control_path = os.path.expanduser(control_path)
command = [ command = [
@@ -121,16 +127,15 @@ class Synchroniser:
# TODO: Raise an exception instead of changing the return value # TODO: Raise an exception instead of changing the return value
try: try:
ret_code = master_ssh.wait(timeout=connection_timeout) ret_code = master_ssh.wait(timeout=connection_timeout)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired as e:
print("Time to login expired", file=sys.stderr) print("Time to login expired", file=sys.stderr)
return 1 raise e
except KeyboardInterrupt: except KeyboardInterrupt as e:
return 2 raise e
if ret_code != 0: if ret_code != 0:
print("Login to remote failed", file=sys.stderr) print("Login to remote failed", file=sys.stderr)
return ret_code raise UnknownSSHError
return 0
def close_ssh_master_connection(self) -> int: def close_ssh_master_connection(self) -> int:
@@ -149,18 +154,18 @@ class Synchroniser:
close = subprocess.Popen(command) close = subprocess.Popen(command)
return close.wait() return close.wait()
def sync_files(self, paths:list, force:bool=False) -> int: def sync_files(self, paths:list, force:bool=False) -> None:
"""Synchronises the files. """Synchronises the files.
Args: Args:
paths: List of paths to synchronise. paths: List of paths to synchronise.
force: Force the changes from remote to local. force: Force the changes from remote to local.
Returns: Raises:
The return code of sync. FatalSyncError: A fatal error occured during the synchronisation.
""" """
return self.sync( self.sync(
f"ssh://{self.remote_user}@{self.remote_ip}/{self.remote_dir}/.data", f"ssh://{self.remote_user}@{self.remote_ip}/{self.remote_dir}/.data",
self.local, self.local,
paths=paths, paths=paths,
@@ -168,16 +173,16 @@ class Synchroniser:
other=self.files_extra other=self.files_extra
) )
def sync_links(self, ignore:list) -> int: def sync_links(self, ignore:list) -> None:
"""Synchronises the links, they must exist already. """Synchronises the links, they must exist already.
Args: Args:
ignore: List of paths to ignore. ignore: List of paths to ignore.
Returns: Raises:
The return code of sync. FatalSyncError: A fatal error occured during the synchronisation.
""" """
return self.sync( self.sync(
f"ssh://{self.remote_user}@{self.remote_ip}/{self.remote_dir}/links", f"ssh://{self.remote_user}@{self.remote_ip}/{self.remote_dir}/links",
self.local, self.local,
ignore=ignore, ignore=ignore,
@@ -187,7 +192,7 @@ class Synchroniser:
def sync(self, remote_root:str, local_root:str, def sync(self, remote_root:str, local_root:str,
paths:list=[], ignore:list=[], force:bool=False, paths:list=[], ignore:list=[], force:bool=False,
other:list=[] other:list=[]
) -> int: ) -> None:
"""Performs the synchronisation by calling unison. """Performs the synchronisation by calling unison.
Args: Args:
@@ -206,8 +211,11 @@ class Synchroniser:
They will be added to the command as is no - in front. They will be added to the command as is no - in front.
For exemple backups are implemented using this argument. For exemple backups are implemented using this argument.
Returns: Raises:
the unison return code see section 6.11 of the documentation FatalSyncError:
If unison returns 3 it means either a fatal error occured or the synchronisation
was interrupted.
If this happens propagate the error to unisync.
""" """
command = [ "/usr/bin/unison", "-root", remote_root, "-root", local_root ] command = [ "/usr/bin/unison", "-root", remote_root, "-root", local_root ]
for arg in self.args_bool: for arg in self.args_bool:
@@ -241,7 +249,8 @@ class Synchroniser:
proc = subprocess.Popen(command) proc = subprocess.Popen(command)
ret_code = proc.wait() ret_code = proc.wait()
return ret_code if ret_code == 3:
raise FatalSyncError("Synchronisation could not be completed")
def update_links(self, background:bool=True): def update_links(self, background:bool=True):
"""Updates the links on the remote. """Updates the links on the remote.