Compare commits
12 Commits
f2b676043c
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
ae0beac9e0
|
|||
|
072c2a26e6
|
|||
|
b0c165b8b0
|
|||
|
6b8686351a
|
|||
|
dcca9c5167
|
|||
|
041ede22e1
|
|||
|
adfded92d0
|
|||
|
7fae1b154a
|
|||
|
3dbd7fc445
|
|||
|
10a79554d3
|
|||
|
a922eaa542
|
|||
|
8836a0120b
|
@@ -1,4 +1,4 @@
|
|||||||
# copyright (c) 2026 paul retourné
|
# Copyright (c) 2026 paul retourné
|
||||||
# spdx-license-identifier: gpl-3.0-or-later
|
# spdx-license-identifier: gpl-3.0-or-later
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
# 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):
|
from typing import NoReturn
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class RemoteMountedError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class InvalidMountError(BaseException):
|
class InvalidMountError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class UnknownSSHError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class FatalSyncError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def unisync_exit_fatal(reason:str) -> NoReturn:
|
||||||
|
print(reason)
|
||||||
|
sys.exit(1)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from unisync.argparser import create_argparser
|
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.runners import unisync_sync, unisync_add, unisync_mount
|
||||||
from unisync.config import load_config
|
from unisync.config import load_config
|
||||||
from unisync.synchroniser import Synchroniser
|
from unisync.synchroniser import Synchroniser
|
||||||
@@ -38,7 +39,10 @@ def main():
|
|||||||
|
|
||||||
paths_manager = PathsManager(Path(config.roots.local), config.other.cache_dir_path)
|
paths_manager = PathsManager(Path(config.roots.local), config.other.cache_dir_path)
|
||||||
|
|
||||||
|
try:
|
||||||
cli_args.func(synchroniser, paths_manager, config)
|
cli_args.func(synchroniser, paths_manager, config)
|
||||||
|
except UnknownSSHError:
|
||||||
|
unisync_exit_fatal("Connection failed quitting")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ from unisync.synchroniser import Synchroniser
|
|||||||
from unisync.paths import PathsManager
|
from unisync.paths import PathsManager
|
||||||
from unisync.config import Config
|
from unisync.config import Config
|
||||||
|
|
||||||
|
|
||||||
def unisync_sync(synchroniser:Synchroniser, paths_manager:PathsManager, config: Config):
|
def unisync_sync(synchroniser:Synchroniser, paths_manager:PathsManager, config: Config):
|
||||||
del config # The function signature must be the same for all runners
|
del config # The function signature must be the same for all runners
|
||||||
if synchroniser.create_ssh_master_connection() != 0:
|
|
||||||
print("Connection failed quitting")
|
synchroniser.create_ssh_master_connection()
|
||||||
return 1
|
|
||||||
print("Connected to the remote.")
|
print("Connected to the remote.")
|
||||||
|
|
||||||
synchroniser.sync_files(paths_manager.get_paths_to_sync())
|
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):
|
def unisync_add(synchroniser:Synchroniser, paths_manager:PathsManager, config: Config):
|
||||||
del config # The function signature must be the same for all runners
|
del config # The function signature must be the same for all runners
|
||||||
if synchroniser.create_ssh_master_connection() != 0:
|
|
||||||
print("Connection failed quitting")
|
synchroniser.create_ssh_master_connection()
|
||||||
return 1
|
|
||||||
print("Connected to the remote.")
|
print("Connected to the remote.")
|
||||||
|
|
||||||
# TODO config or cli to skip this first sync
|
# TODO config or cli to skip this first sync
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -232,7 +240,7 @@ class Synchroniser:
|
|||||||
command.append(f"BelowPath {path}")
|
command.append(f"BelowPath {path}")
|
||||||
|
|
||||||
if force:
|
if force:
|
||||||
command.append("-force")
|
command.append("-prefer")
|
||||||
command.append(remote_root)
|
command.append(remote_root)
|
||||||
command.append("-batch")
|
command.append("-batch")
|
||||||
|
|
||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user