|
|
|
|
@@ -1,6 +1,12 @@
|
|
|
|
|
# Copyright (C) 2025-2026 Paul Retourné
|
|
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
|
|
|
|
"""Exports the Synchroniser class.
|
|
|
|
|
|
|
|
|
|
This class is used to perform all the actions that require a connection to
|
|
|
|
|
the remote.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import subprocess
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
@@ -14,9 +20,37 @@ from unisync.errors import RemoteMountedError, InvalidMountError
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
class Synchroniser:
|
|
|
|
|
"""Synchroniser used to synchronise with a server.
|
|
|
|
|
|
|
|
|
|
It is used to perform every action needing a connection to the remote.
|
|
|
|
|
Create an ssh connection.
|
|
|
|
|
Perform the various synchronisation steps (files, links).
|
|
|
|
|
Update the links on the remote.
|
|
|
|
|
Mount the remote directory.
|
|
|
|
|
Close the ssh connection.
|
|
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
|
remote: The directory to synchronise to on the remote.
|
|
|
|
|
local: The directory to synchronise from locally.
|
|
|
|
|
user: The user on the remote server.
|
|
|
|
|
ip: The ip of the remote server.
|
|
|
|
|
port: The ssh port on the remote.
|
|
|
|
|
args_bool:
|
|
|
|
|
A list of boolean arguments for unison.
|
|
|
|
|
They will be passed directly to unison when calling it.
|
|
|
|
|
For example : auto will be passed as -auto
|
|
|
|
|
args_value:
|
|
|
|
|
Same as args_bool but for key value arguments.
|
|
|
|
|
Will be passed to unison as "-key value".
|
|
|
|
|
ssh_settings:
|
|
|
|
|
Settings to pass to the underlying ssh connection.
|
|
|
|
|
Currently unused.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, remote:str, local:str, user:str, ip:str,
|
|
|
|
|
port:int=22, args_bool:list=[], args_value:dict={}, ssh_settings:dict={}):
|
|
|
|
|
"""Initialises an instance of Synchroniser.
|
|
|
|
|
"""
|
|
|
|
|
self.remote_dir:str = remote
|
|
|
|
|
self.local:str = local
|
|
|
|
|
self.args_bool:list[str] = args_bool
|
|
|
|
|
@@ -27,14 +61,21 @@ class Synchroniser:
|
|
|
|
|
self.remote_port:int = port
|
|
|
|
|
|
|
|
|
|
def create_ssh_master_connection(self, control_path:str="~/.ssh/control_%C", connection_timeout:int=60) -> int:
|
|
|
|
|
"""
|
|
|
|
|
Creates an ssh master connection so the user only has to authenticate once to the remote server.
|
|
|
|
|
The subsequent connections will be made through this master connection which speeds up connecting.
|
|
|
|
|
@control_path: Set the location of the ssh control socket
|
|
|
|
|
@connection_timeout:
|
|
|
|
|
"""Creates an ssh master connection.
|
|
|
|
|
|
|
|
|
|
It is used so the user only has to authenticate once to the remote server.
|
|
|
|
|
The subsequent connections will be made through this master connection
|
|
|
|
|
which speeds up connnection.
|
|
|
|
|
The users only have to enter their password once per synchronisation.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
control_path: Set the location of the ssh control socket
|
|
|
|
|
connection_timeout:
|
|
|
|
|
Time given to the user to authenticate to the remote server.
|
|
|
|
|
On slow connections one might want to increase this.
|
|
|
|
|
Returns 0 on success.
|
|
|
|
|
Returns:
|
|
|
|
|
An error code (0 success, 1 TimeoutExpired, 2 KeyboardInterrupt).
|
|
|
|
|
TODO change that to raising the exception.
|
|
|
|
|
"""
|
|
|
|
|
self.control_path = os.path.expanduser(control_path)
|
|
|
|
|
command = [
|
|
|
|
|
@@ -61,8 +102,10 @@ class Synchroniser:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def close_ssh_master_connection(self) -> int:
|
|
|
|
|
"""
|
|
|
|
|
Close the ssh master connection.
|
|
|
|
|
"""Closes the ssh master connection.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The return code of the ssh call.
|
|
|
|
|
"""
|
|
|
|
|
command = [
|
|
|
|
|
"/usr/bin/ssh",
|
|
|
|
|
@@ -75,8 +118,14 @@ class Synchroniser:
|
|
|
|
|
return close.wait()
|
|
|
|
|
|
|
|
|
|
def sync_files(self, paths:list, force:bool=False) -> int:
|
|
|
|
|
"""
|
|
|
|
|
Synchronises the files.
|
|
|
|
|
"""Synchronises the files.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
paths: List of paths to synchronise.
|
|
|
|
|
force: Force the changes from remote to local.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The return code of sync.
|
|
|
|
|
"""
|
|
|
|
|
return self.sync(
|
|
|
|
|
f"ssh://{self.remote_user}@{self.remote_ip}/{self.remote_dir}/.data",
|
|
|
|
|
@@ -86,8 +135,13 @@ class Synchroniser:
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def sync_links(self, ignore:list) -> int:
|
|
|
|
|
"""
|
|
|
|
|
Synchronises the links, they must exist already.
|
|
|
|
|
"""Synchronises the links, they must exist already.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
ignore: List of paths to ignore.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
The return code of sync.
|
|
|
|
|
"""
|
|
|
|
|
return self.sync(
|
|
|
|
|
f"ssh://{self.remote_user}@{self.remote_ip}/{self.remote_dir}/links",
|
|
|
|
|
@@ -97,17 +151,20 @@ class Synchroniser:
|
|
|
|
|
|
|
|
|
|
def sync(self, remote_root:str, local_root:str,
|
|
|
|
|
paths:list=[], ignore:list=[], force:bool=False) -> int:
|
|
|
|
|
"""
|
|
|
|
|
Perform the synchronisation by calling unison.
|
|
|
|
|
@remote_root: The remote root, must be a full root usable by unison.
|
|
|
|
|
@local_root: The local root, must be a full root usable by unison.
|
|
|
|
|
@paths: List of paths to synchronise
|
|
|
|
|
@ignore: List of paths to ignore
|
|
|
|
|
"""Performs the synchronisation by calling unison.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
remote_root: The remote root, must be a full root usable by unison.
|
|
|
|
|
local_root: The local root, must be a full root usable by unison.
|
|
|
|
|
paths: List of paths to synchronise
|
|
|
|
|
ignore: List of paths to ignore
|
|
|
|
|
The paths and everything under them will be ignored.
|
|
|
|
|
If you need to ignore some specific files use the arguments.
|
|
|
|
|
@force: Force all changes from remote to local.
|
|
|
|
|
force: Force all changes from remote to local.
|
|
|
|
|
Used mostly when replacing a link by the file.
|
|
|
|
|
Returns: the unison return code see section 6.11 of the documentation
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
the unison return code see section 6.11 of the documentation
|
|
|
|
|
"""
|
|
|
|
|
command = [ "/usr/bin/unison", "-root", remote_root, "-root", local_root ]
|
|
|
|
|
for arg in self.args_bool:
|
|
|
|
|
@@ -140,12 +197,13 @@ class Synchroniser:
|
|
|
|
|
return ret_code
|
|
|
|
|
|
|
|
|
|
def update_links(self, background:bool=True):
|
|
|
|
|
"""
|
|
|
|
|
Update the links on the remote.
|
|
|
|
|
"""Updates the links on the remote.
|
|
|
|
|
|
|
|
|
|
First calls cleanlinks to remove deadlinks and empty directories.
|
|
|
|
|
Then calls lndir to create the new links.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
- background: controls if the update is done in the background or waited for
|
|
|
|
|
background: controls if the update is done in the background or waited for.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
link_update_script = (f"cd {self.remote_dir}/links && "
|
|
|
|
|
@@ -173,13 +231,14 @@ class Synchroniser:
|
|
|
|
|
print("Done")
|
|
|
|
|
|
|
|
|
|
def mount_remote_dir(self):
|
|
|
|
|
"""
|
|
|
|
|
Mount the remote directory to make the local links work.
|
|
|
|
|
This is achieved using sshfs.
|
|
|
|
|
Raise:
|
|
|
|
|
- RemoteMountedError: The .data directory is already a mount point
|
|
|
|
|
- InvalidMountError: .data is either not a directory or not empty
|
|
|
|
|
- subprocess.CalledProcessError: An error occured with sshfs
|
|
|
|
|
"""Mounts the remote directory to make the local links work.
|
|
|
|
|
|
|
|
|
|
This is achieved using sshfs which may fail.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
RemoteMountedError: The .data directory is already a mount point.
|
|
|
|
|
InvalidMountError: .data is either not a directory or not empty.
|
|
|
|
|
subprocess.CalledProcessError: An error occured with sshfs.
|
|
|
|
|
"""
|
|
|
|
|
# Get the absolute path to the correct .data directory resolving symlinks
|
|
|
|
|
path_to_mount:Path = Path(f"{self.local}/../.data").resolve()
|
|
|
|
|
|