Compare commits

...

12 Commits

Author SHA1 Message Date
ae0beac9e0 Catch UnknownSSHError and do not use return codes
The runners were still checking for the return codes of
create_ssh_master_connection instead of catching the exception.
We now catch the exceptions we calling the runners in main.
2026-01-31 12:30:47 +01:00
072c2a26e6 errors : Add program ending function
Add a function that quits the program using sys.exit.
This is useful when we enconter a fatal error.
2026-01-31 11:57:09 +01:00
b0c165b8b0 Revert "paths : make TimeoutExpired handling clearer"
This reverts commit 041ede22e1.

There is no point in using "as e" and "raise e", the original version was
better.
2026-01-31 11:56:51 +01:00
6b8686351a defaults : Capitalize the c of Copyright 2026-01-30 19:12:37 +01:00
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
3dbd7fc445 doc : remove extra whitespace 2026-01-30 17:14:35 +01:00
10a79554d3 synchroniser : use prefer instead of force
When doing a forced synchronisation use the prefer directive instead of
force. This makes unison choose the remote version in case of conflicts
only and not for every change. This allows the add subcommand to be used
for adding a file to the sync (that is already present remotly) as well
as adding a brand new file from the local machine (after creating it or
downloading from somewhere for example).
2026-01-30 17:06:55 +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
6 changed files with 59 additions and 34 deletions

View File

@@ -17,7 +17,7 @@ Unisync tries to solve two problems that are often solved separately but never t
Unisync solves this problem by placing each file on your local machine but with only the selected files and folders being physically present on your drive,
the others are replaced by symbolic links pointing to a directory that is mounted from your server.
See this
See this
:ref:`example_how_it_works`.
.. _unison: https://github.com/bcpierce00/unison

View File

@@ -1,4 +1,4 @@
# copyright (c) 2026 paul retourné
# Copyright (c) 2026 paul retourné
# spdx-license-identifier: gpl-3.0-or-later
from pathlib import Path

View File

@@ -1,8 +1,21 @@
# Copyright (C) 2025-2026 Paul Retourné
# SPDX-License-Identifier: GPL-3.0-or-later
class RemoteMountedError(BaseException):
from typing import NoReturn
import sys
class RemoteMountedError(Exception):
pass
class InvalidMountError(BaseException):
class InvalidMountError(Exception):
pass
class UnknownSSHError(Exception):
pass
class FatalSyncError(Exception):
pass
def unisync_exit_fatal(reason:str) -> NoReturn:
print(reason)
sys.exit(1)

View File

@@ -4,6 +4,7 @@
from pathlib import Path
from unisync.argparser import create_argparser
from unisync.errors import UnknownSSHError, unisync_exit_fatal
from unisync.runners import unisync_sync, unisync_add, unisync_mount
from unisync.config import load_config
from unisync.synchroniser import Synchroniser
@@ -38,7 +39,10 @@ def main():
paths_manager = PathsManager(Path(config.roots.local), config.other.cache_dir_path)
cli_args.func(synchroniser, paths_manager, config)
try:
cli_args.func(synchroniser, paths_manager, config)
except UnknownSSHError:
unisync_exit_fatal("Connection failed quitting")
if __name__ == "__main__":

View File

@@ -5,11 +5,11 @@ from unisync.synchroniser import Synchroniser
from unisync.paths import PathsManager
from unisync.config import Config
def unisync_sync(synchroniser:Synchroniser, paths_manager:PathsManager, config: Config):
del config # The function signature must be the same for all runners
if synchroniser.create_ssh_master_connection() != 0:
print("Connection failed quitting")
return 1
synchroniser.create_ssh_master_connection()
print("Connected to the remote.")
synchroniser.sync_files(paths_manager.get_paths_to_sync())
@@ -24,9 +24,8 @@ def unisync_sync(synchroniser:Synchroniser, paths_manager:PathsManager, config:
def unisync_add(synchroniser:Synchroniser, paths_manager:PathsManager, config: Config):
del config # The function signature must be the same for all runners
if synchroniser.create_ssh_master_connection() != 0:
print("Connection failed quitting")
return 1
synchroniser.create_ssh_master_connection()
print("Connected to the remote.")
# TODO config or cli to skip this first sync

View File

@@ -16,7 +16,7 @@ import logging
from pathlib import Path
from typing import cast
from unisync.errors import RemoteMountedError, InvalidMountError
from unisync.errors import RemoteMountedError, InvalidMountError, UnknownSSHError, FatalSyncError
from unisync.config import BackupConfig
logger = logging.getLogger(__name__)
@@ -92,7 +92,7 @@ class Synchroniser:
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.
It is used so the user only has to authenticate once to the remote server.
@@ -105,8 +105,14 @@ class Synchroniser:
connection_timeout:
Time given to the user to authenticate to the remote server.
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)
command = [
@@ -121,16 +127,15 @@ class Synchroniser:
# TODO: Raise an exception instead of changing the return value
try:
ret_code = master_ssh.wait(timeout=connection_timeout)
except subprocess.TimeoutExpired:
except subprocess.TimeoutExpired as e:
print("Time to login expired", file=sys.stderr)
return 1
except KeyboardInterrupt:
return 2
raise e
except KeyboardInterrupt as e:
raise e
if ret_code != 0:
print("Login to remote failed", file=sys.stderr)
return ret_code
return 0
raise UnknownSSHError
def close_ssh_master_connection(self) -> int:
@@ -149,18 +154,18 @@ class Synchroniser:
close = subprocess.Popen(command)
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.
Args:
paths: List of paths to synchronise.
force: Force the changes from remote to local.
Returns:
The return code of sync.
Raises:
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",
self.local,
paths=paths,
@@ -168,16 +173,16 @@ class Synchroniser:
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.
Args:
ignore: List of paths to ignore.
Returns:
The return code of sync.
Raises:
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",
self.local,
ignore=ignore,
@@ -187,7 +192,7 @@ class Synchroniser:
def sync(self, remote_root:str, local_root:str,
paths:list=[], ignore:list=[], force:bool=False,
other:list=[]
) -> int:
) -> None:
"""Performs the synchronisation by calling unison.
Args:
@@ -206,8 +211,11 @@ class Synchroniser:
They will be added to the command as is no - in front.
For exemple backups are implemented using this argument.
Returns:
the unison return code see section 6.11 of the documentation
Raises:
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 ]
for arg in self.args_bool:
@@ -232,7 +240,7 @@ class Synchroniser:
command.append(f"BelowPath {path}")
if force:
command.append("-force")
command.append("-prefer")
command.append(remote_root)
command.append("-batch")
@@ -241,7 +249,8 @@ class Synchroniser:
proc = subprocess.Popen(command)
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):
"""Updates the links on the remote.