Compare commits

...

6 Commits

Author SHA1 Message Date
5af5374f77 config : fallback to 22 instead of None
The configparser fallback option was None set it to use 22 instead as
None doesn't make sense
2026-01-01 17:30:28 +01:00
27924013d9 Bug fixes and small improvements
Fix :
- paths : true instead of True
- paths : Path has no len convert to str first to get the number of
  characters

Improvements :
- Replace all PosixPath by Path
2026-01-01 17:24:46 +01:00
48179034a7 Adds prerequisites to README 2025-12-31 00:03:59 +01:00
f9001ecb9d Adds usage of the paths manager 2025-12-31 00:03:17 +01:00
86a6c8acce Adds error handling for the paths 2025-12-30 17:56:03 +01:00
4f6f48247d Add classes for error handling 2025-12-30 17:54:47 +01:00
6 changed files with 53 additions and 17 deletions

View File

@@ -1,6 +1,19 @@
Unisync is a data synchronisation tool written in python and based on [unison](https://github.com/bcpierce00/unison).
I couldn't find a tool to fulfill the requirements I had for a synchronisation tool so I am creating my own as a wrapper around unison.
# Prerequisite
You need to have the following tools installed.
Locally :
- unison
- sshfs
- nnn
Remotely :
- unison
- cleanlinks and lndir (Should be in `xutils-dev` when using apt)
# Goal
Unisync purpose is to keep personal data synchronised between multiple machines without needing to have all the data present an all the machines at the same time. For example you might not need to have your movies on your laptop but still want them on your desktop at home or you might want to keep your old pictures only on a server.

View File

@@ -3,7 +3,7 @@
from configparser import UNNAMED_SECTION
from dataclasses import dataclass, field
from pathlib import Path, PosixPath
from pathlib import Path
import ipaddress
import configparser
@@ -13,7 +13,7 @@ class ServerConfig:
Dataclass keeping the config for connecting to the server
"""
user: str
sshargs: list[str] | None = field(default_factory=list)
sshargs: str = ""
hostname: str = ""
ip: str = ""
port: int = 22
@@ -52,7 +52,7 @@ class OtherConfig:
"""
Dataclass keeping miscellanous configuration options
"""
cache_dir_path: PosixPath = Path("~/.unisync").expanduser()
cache_dir_path: Path = Path("~/.unisync").expanduser()
@dataclass
class Config:
@@ -82,10 +82,10 @@ def load_config(config_path:str) -> Config:
server_config = ServerConfig(
config.get(server_section, "user"),
config.get(server_section, "sshargs", fallback=None),
config.get(server_section, "hostname", fallback=None),
config.get(server_section, "ip", fallback=None),
config.getint(server_section, "port", fallback=None)
config.get(server_section, "sshargs", fallback=""),
config.get(server_section, "hostname", fallback=""),
config.get(server_section, "ip", fallback=""),
config.getint(server_section, "port", fallback=22)
)
roots_config = RootsConfig(
config.get(roots_section, "local"),

5
src/unisync/errors.py Normal file
View File

@@ -0,0 +1,5 @@
class RemoteMountedError(BaseException):
pass
class InvalidMountError(BaseException):
pass

View File

@@ -5,6 +5,8 @@ import os
from argparser import create_argparser
from config import RootsConfig, ServerConfig, Config, load_config
from synchroniser import Synchroniser
from pathlib import Path, PosixPath
from paths import *
def main():
parser = create_argparser()
@@ -29,14 +31,19 @@ def main():
config.unison.values
)
paths_manager = PathsManager(Path(config.roots.local), config.other.cache_dir_path)
if synchroniser.create_ssh_master_connection() != 0:
print("Connection failed quitting")
return 1
print("Connected to the remote.")
synchroniser.sync_files(["salut"])
#synchroniser.sync_files()
#synchroniser.update_links(background=False)
#synchroniser.mount_remote_dir()
synchroniser.close_ssh_master_connection()
print(paths_manager.get_paths_to_sync())

View File

@@ -4,12 +4,13 @@
import os.path
import subprocess
import sys
from pathlib import Path, PosixPath
from pathlib import Path
class PathsManager:
def __init__(self, local_dir:PosixPath, cache_dir:PosixPath):
def __init__(self, local_dir:Path, cache_dir:Path):
"""
Creates a PathsManager with the necessary data
Args:
@@ -24,7 +25,7 @@ class PathsManager:
raise ValueError("Invalid cache directory")
self.cache_dir = cache_dir
self.paths_file:PosixPath = self.cache_dir / "paths"
self.paths_file:Path = self.cache_dir / "paths"
if not self.paths_file.is_file():
raise ValueError("The paths file does not exist")
@@ -48,7 +49,7 @@ class PathsManager:
"-p", "-",
self.local_dir
]
nnn_process = subprocess.Popen(command, stdout=subprocess.PIPE)
nnn_process:subprocess.Popen = subprocess.Popen(command, stdout=subprocess.PIPE)
try:
ret_code = nnn_process.wait(timeout=choice_timeout)
except subprocess.TimeoutExpired as e:
@@ -57,18 +58,18 @@ class PathsManager:
if ret_code != 0:
print("File selection failed", file=sys.stderr)
raise subprocess.CalledProcessError("File selection failed")
raise subprocess.CalledProcessError(1, "File selection failed")
paths_list:list[str] = []
while (next_path := nnn_process.stdout.readline()) != b'':
next_path = next_path.decode().strip()
# Make the path relative to the top directory
next_path = next_path[len(self.local_dir):].lstrip("/")
next_path = next_path[len(str(self.local_dir)):].lstrip("/")
paths_list.append(next_path)
return paths_list
def add_files_to_sync(self):
while true:
while True:
try:
paths = self.user_select_files()
break

View File

@@ -9,6 +9,8 @@ import logging
from pathlib import Path
from errors import RemoteMountedError, InvalidMountError
logger = logging.getLogger(__name__)
class Synchroniser:
@@ -175,16 +177,24 @@ class Synchroniser:
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
"""
# Get the absolute path to the correct .data directory resolving symlinks
path_to_mount:Path = Path(f"{self.local}/../.data").resolve()
if path_to_mount.is_mount():
raise RemoteMountedError
# Check if it is an empty directory
if not path_to_mount.is_dir() or any(path_to_mount.iterdir()):
raise InvalidMountError
command = [
"/usr/bin/sshfs",
"-o", "ControlPath={self.control_path}",
"-o", "ServerAliveInterval=15",
"-p", str(self.remote_port),
f"{self.remote_user}@{self.remote_ip}:{self.remote_dir}/.data",
# Get the absolute path to the correct .data directory resolving symlinks
str(Path(f"{self.local}/../.data").resolve())
str(path_to_mount)
]
completed_process = subprocess.run(command)
completed_process.check_returncode()