Compare commits

..

1 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
10 changed files with 49 additions and 123 deletions

View File

@@ -23,4 +23,5 @@ The issue is that you need to know what data is stored on the server to avoid co
# Developement # Developement
Unisync was at first a simple bash script but as it grew more complex I started struggling to maintain it which is why I am porting it to python. It will make everything more robust, easier to maintain and to add functionalities. Unisync was at first a simple bash script but as it grew more complex I started struggling to maintain it which is why I am porting it to python. It will make everything more robust, easier to maintain and to add functionalities.
I am in the early stages of the developement process, this should be usable someday (hopefully). I am in the early stages of the developement process, this should be usable in the upcoming weeks.
Help will be welcome in the future but is not desirable right now as I want to shape this the way I want to.

View File

@@ -10,9 +10,6 @@ requires-python = ">=3.13"
dependencies = [ dependencies = [
] ]
[project.scripts]
unisync = "unisync.main:main"
[tool.poetry] [tool.poetry]
packages = [{include = "unisync", from = "src"}] packages = [{include = "unisync", from = "src"}]

View File

@@ -1,36 +1,22 @@
# Copyright (C) 2025-2026 Paul Retourné # Copyright (C) 2025 Paul Retourné
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import argparse import argparse
def create_argparser(sync_function, add_function, mount_function) -> argparse.ArgumentParser: def create_argparser() -> argparse.ArgumentParser:
"""
Creates an argument parser to parse the command line arguments.
We use subparsers and set a default function for each to perform the correct action.
"""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog='unisync', prog='unisync',
description='File synchronisation application', description='File synchronisation application',
epilog="Copyright © 2025-2026 Paul Retourné.\n" epilog="""
"License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.", Copyright © 2025 Paul Retourné.
formatter_class=argparse.RawDescriptionHelpFormatter License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>."""
) )
parser.add_argument("local", nargs="?") parser.add_argument("local", nargs="?")
parser.add_argument("remote", nargs="?") parser.add_argument("remote", nargs="?")
parser.set_defaults(func=sync_function)
remote_addr_group = parser.add_mutually_exclusive_group() remote_addr_group = parser.add_mutually_exclusive_group()
remote_addr_group.add_argument("--ip") remote_addr_group.add_argument("--ip")
remote_addr_group.add_argument("--hostname") remote_addr_group.add_argument("--hostname")
parser.add_argument("--config", help="Path to the configuration file", metavar="path_to_config") parser.add_argument("--config", help="Path to the configuration file", metavar="path_to_config")
subparsers = parser.add_subparsers(help='Actions other than synchronisation')
parser_add = subparsers.add_parser('add', help='Add files to be synchronised.')
parser_add.set_defaults(func=add_function)
parser_mount = subparsers.add_parser('mount', help='Mount the remote.')
parser_mount.set_defaults(func=mount_function)
return parser return parser

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2025-2026 Paul Retourné # Copyright (C) 2025 Paul Retourné
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from configparser import UNNAMED_SECTION from configparser import UNNAMED_SECTION
@@ -7,18 +7,16 @@ from pathlib import Path
import ipaddress import ipaddress
import configparser import configparser
from unisync.defaults import *
@dataclass @dataclass
class ServerConfig: class ServerConfig:
""" """
Dataclass keeping the config for connecting to the server Dataclass keeping the config for connecting to the server
""" """
user: str user: str
sshargs: str sshargs: str = ""
hostname: str hostname: str = ""
ip: str ip: str = ""
port: int port: int = 22
def __post_init__(self): def __post_init__(self):
""" """
@@ -46,15 +44,15 @@ class UnisonConfig:
""" """
Dataclass keeping unison specific configurations Dataclass keeping unison specific configurations
""" """
bools: list bools: list = field(default_factory=list)
values: dict values: dict = field(default_factory=dict)
@dataclass @dataclass
class OtherConfig: class OtherConfig:
""" """
Dataclass keeping miscellanous configuration options Dataclass keeping miscellanous configuration options
""" """
cache_dir_path: Path cache_dir_path: Path = Path("~/.unisync").expanduser()
@dataclass @dataclass
class Config: class Config:
@@ -64,7 +62,7 @@ class Config:
server: ServerConfig server: ServerConfig
roots: RootsConfig roots: RootsConfig
unison: UnisonConfig unison: UnisonConfig
other: OtherConfig other: OtherConfig = field(default_factory=OtherConfig)
def load_config(config_path:str) -> Config: def load_config(config_path:str) -> Config:
@@ -81,22 +79,18 @@ def load_config(config_path:str) -> Config:
# Check if sections are provided # Check if sections are provided
server_section = "Server" if "Server" in config.sections() else UNNAMED_SECTION server_section = "Server" if "Server" in config.sections() else UNNAMED_SECTION
roots_section = "Roots" if "Roots" in config.sections() else UNNAMED_SECTION roots_section = "Roots" if "Roots" in config.sections() else UNNAMED_SECTION
other_section = "Other" if "Other" in config.sections() else UNNAMED_SECTION
server_config = ServerConfig( server_config = ServerConfig(
config.get(server_section, "user"), config.get(server_section, "user"),
config.get(server_section, "sshargs", fallback=DEFAULT_SERVER_SSHARGS), config.get(server_section, "sshargs", fallback=""),
config.get(server_section, "hostname", fallback=DEFAULT_SERVER_HOSTNAME), config.get(server_section, "hostname", fallback=""),
config.get(server_section, "ip", fallback=DEFAULT_SERVER_IP), config.get(server_section, "ip", fallback=""),
config.getint(server_section, "port", fallback=DEFAULT_SERVER_PORT) config.getint(server_section, "port", fallback=22)
) )
roots_config = RootsConfig( roots_config = RootsConfig(
config.get(roots_section, "local", fallback=DEFAULT_ROOTS_LOCAL), config.get(roots_section, "local"),
config.get(roots_section, "remote") config.get(roots_section, "remote")
) )
other_config = OtherConfig(
Path(config.get(other_section, "cache_dir_path", fallback=DEFAULT_MISC_CACHE_DIR_PATH)).expanduser()
)
args_bool = list() args_bool = list()
args_val = dict() args_val = dict()
@@ -110,4 +104,4 @@ def load_config(config_path:str) -> Config:
args_val[key] = val args_val[key] = val
unison_config = UnisonConfig(args_bool, args_val) unison_config = UnisonConfig(args_bool, args_val)
return Config(server_config, roots_config, unison_config, other_config) return Config(server_config, roots_config, unison_config)

View File

@@ -1,18 +0,0 @@
# copyright (c) 2026 paul retourné
# spdx-license-identifier: gpl-3.0-or-later
from pathlib import Path
# Commented out values are part of the config but are required so there is no defaults.
# This allows this file to be a list of all the config options.
# DEFAULT_SERVER_USER: str = ""
DEFAULT_SERVER_SSHARGS: str = ""
DEFAULT_SERVER_HOSTNAME: str = ""
DEFAULT_SERVER_IP: str = ""
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()

View File

@@ -1,6 +1,3 @@
# Copyright (C) 2025-2026 Paul Retourné
# SPDX-License-Identifier: GPL-3.0-or-later
class RemoteMountedError(BaseException): class RemoteMountedError(BaseException):
pass pass

View File

@@ -1,32 +1,26 @@
# Copyright (C) 2025-2026 Paul Retourné # Copyright (C) 2025 Paul Retourné
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import os import os
from pathlib import Path from argparser import create_argparser
from config import RootsConfig, ServerConfig, Config, load_config
from unisync.argparser import create_argparser from synchroniser import Synchroniser
from unisync.runners import unisync_sync, unisync_add, unisync_mount from pathlib import Path, PosixPath
from unisync.config import load_config from paths import *
from unisync.synchroniser import Synchroniser
from unisync.paths import *
def main(): def main():
parser = create_argparser(unisync_sync, unisync_add, unisync_mount) parser = create_argparser()
cli_args = parser.parse_args() base_namespace = parser.parse_args()
config_path = os.path.expanduser("~/.config/unisync/config.ini") config_path = os.path.expanduser("~/.config/unisync/config.ini")
# Check if --config is set if base_namespace.config != None and os.path.isfile(base_namespace.config):
if cli_args.config != None and os.path.isfile(cli_args.config): config = load_config(base_namespace.config)
config = load_config(cli_args.config)
elif os.path.isfile(config_path): elif os.path.isfile(config_path):
config = load_config(config_path) config = load_config(config_path)
else: else:
# TODO replace the next line with something to do if no config file is found # TODO make the command line arguments work and override the config options
config = load_config(config_path)
pass pass
# TODO make the command line arguments work and override the config options
synchroniser = Synchroniser( synchroniser = Synchroniser(
config.roots.remote, config.roots.remote,
config.roots.local, config.roots.local,
@@ -39,7 +33,18 @@ 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)
cli_args.func(synchroniser, paths_manager) if synchroniser.create_ssh_master_connection() != 0:
print("Connection failed quitting")
return 1
print("Connected to the remote.")
#synchroniser.sync_files()
#synchroniser.update_links(background=False)
#synchroniser.mount_remote_dir()
synchroniser.close_ssh_master_connection()
print(paths_manager.get_paths_to_sync())
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2025-2026 Paul Retourné # Copyright (C) 2025 Paul Retourné
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
@@ -106,7 +106,7 @@ class PathsManager:
if not is_contained and new_path not in paths_to_add: if not is_contained and new_path not in paths_to_add:
paths_to_add.append(new_path) paths_to_add.append(new_path)
with self.paths_file.open("a") as f: with self.paths_file.open("w") as f:
for p in paths_to_add: for p in paths_to_add:
f.write(p + "\n") f.write(p + "\n")

View File

@@ -1,36 +0,0 @@
# Copyright (C) 2025-2026 Paul Retourné
# SPDX-License-Identifier: GPL-3.0-or-later
from unisync.synchroniser import Synchroniser
from unisync.paths import PathsManager
def unisync_sync(synchroniser:Synchroniser, paths_manager:PathsManager):
if synchroniser.create_ssh_master_connection() != 0:
print("Connection failed quitting")
return 1
print("Connected to the remote.")
synchroniser.sync_files(paths_manager.get_paths_to_sync())
synchroniser.sync_links(paths_manager.get_paths_to_sync())
# TODO check the config options and do or don't do the following
synchroniser.update_links()
#synchroniser.mount_remote_dir()
synchroniser.close_ssh_master_connection()
def unisync_add(synchroniser:Synchroniser, paths_manager:PathsManager):
if synchroniser.create_ssh_master_connection() != 0:
print("Connection failed quitting")
return 1
print("Connected to the remote.")
paths_manager.add_files_to_sync()
synchroniser.close_ssh_master_connection()
def unisync_mount(synchroniser:Synchroniser, paths_manager:PathsManager):
synchroniser.mount_remote_dir()

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2025-2026 Paul Retourné # Copyright (C) 2025 Paul Retourné
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import subprocess import subprocess
@@ -9,7 +9,7 @@ import logging
from pathlib import Path from pathlib import Path
from unisync.errors import RemoteMountedError, InvalidMountError from errors import RemoteMountedError, InvalidMountError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)