Compare commits

..

3 Commits

Author SHA1 Message Date
78a4d9df36 gitignore : ignore docs/build
The docs will be added later but to prevent the mess when switching
between branches ignore the build folder.
2026-01-04 19:22:04 +01:00
7dd7b57e1f synchroniser : Use a consistent docstring format.
Edit the docstrings so they use a consistent format.
Also add a short module docstring.
2026-01-04 14:31:16 +01:00
b10ed69d59 defaults : change type of MISC_CACHE_DIR_PATH to str
DEFAULT_MISC_CACHE_DIR_PATH was a Path but the fallbacks of config.get
in config.py will be converted to a string so make it a string instead
and do the conversion later
2026-01-04 12:22:21 +01:00
3 changed files with 96 additions and 36 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
poetry.lock
__pycache__
docs/build

View File

@@ -15,4 +15,4 @@ DEFAULT_SERVER_PORT: int = 22
DEFAULT_ROOTS_LOCAL: str = str(Path("~/files").expanduser())
# DEFAULT_ROOTS_REMOTE: str = ""
DEFAULT_MISC_CACHE_DIR_PATH: Path = Path("~/.unisync").expanduser()
DEFAULT_MISC_CACHE_DIR_PATH: str = "~/.unisync"

View File

@@ -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()