Compare commits
16 Commits
a0eb371a65
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
ec8030fc81
|
|||
|
f050dcc94f
|
|||
|
f40a5c9276
|
|||
|
0e80ba0b0d
|
|||
|
a223f04909
|
|||
|
e42ae71862
|
|||
|
58c7f7d1be
|
|||
|
eefb21faff
|
|||
|
941c467fc2
|
|||
|
4dcab777ca
|
|||
|
a169890351
|
|||
|
b70070ba1a
|
|||
|
bd72d740e6
|
|||
|
e43c16adb3
|
|||
|
10200fceb9
|
|||
|
138bc6d24a
|
@@ -23,5 +23,4 @@ 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 in the upcoming weeks.
|
I am in the early stages of the developement process, this should be usable someday (hopefully).
|
||||||
Help will be welcome in the future but is not desirable right now as I want to shape this the way I want to.
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ 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"}]
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,36 @@
|
|||||||
# Copyright (C) 2025 Paul Retourné
|
# Copyright (C) 2025-2026 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() -> argparse.ArgumentParser:
|
def create_argparser(sync_function, add_function, mount_function) -> 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="""
|
epilog="Copyright © 2025-2026 Paul Retourné.\n"
|
||||||
Copyright © 2025 Paul Retourné.
|
"License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.",
|
||||||
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>."""
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||||
)
|
)
|
||||||
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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2025 Paul Retourné
|
# Copyright (C) 2025-2026 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,16 +7,18 @@ 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 | None = 22
|
port: int
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
"""
|
"""
|
||||||
@@ -44,15 +46,15 @@ class UnisonConfig:
|
|||||||
"""
|
"""
|
||||||
Dataclass keeping unison specific configurations
|
Dataclass keeping unison specific configurations
|
||||||
"""
|
"""
|
||||||
bools: list = field(default_factory=list)
|
bools: list
|
||||||
values: dict = field(default_factory=dict)
|
values: dict
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class OtherConfig:
|
class OtherConfig:
|
||||||
"""
|
"""
|
||||||
Dataclass keeping miscellanous configuration options
|
Dataclass keeping miscellanous configuration options
|
||||||
"""
|
"""
|
||||||
cache_dir_path: Path = Path("~/.unisync").expanduser()
|
cache_dir_path: Path
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Config:
|
class Config:
|
||||||
@@ -62,7 +64,7 @@ class Config:
|
|||||||
server: ServerConfig
|
server: ServerConfig
|
||||||
roots: RootsConfig
|
roots: RootsConfig
|
||||||
unison: UnisonConfig
|
unison: UnisonConfig
|
||||||
other: OtherConfig = field(default_factory=OtherConfig)
|
other: OtherConfig
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path:str) -> Config:
|
def load_config(config_path:str) -> Config:
|
||||||
@@ -79,18 +81,22 @@ 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=""),
|
config.get(server_section, "sshargs", fallback=DEFAULT_SERVER_SSHARGS),
|
||||||
config.get(server_section, "hostname", fallback=""),
|
config.get(server_section, "hostname", fallback=DEFAULT_SERVER_HOSTNAME),
|
||||||
config.get(server_section, "ip", fallback=""),
|
config.get(server_section, "ip", fallback=DEFAULT_SERVER_IP),
|
||||||
config.getint(server_section, "port", fallback=None)
|
config.getint(server_section, "port", fallback=DEFAULT_SERVER_PORT)
|
||||||
)
|
)
|
||||||
roots_config = RootsConfig(
|
roots_config = RootsConfig(
|
||||||
config.get(roots_section, "local"),
|
config.get(roots_section, "local", fallback=DEFAULT_ROOTS_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()
|
||||||
@@ -104,4 +110,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)
|
return Config(server_config, roots_config, unison_config, other_config)
|
||||||
|
|||||||
18
src/unisync/defaults.py
Normal file
18
src/unisync/defaults.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 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()
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# Copyright (C) 2025-2026 Paul Retourné
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
class RemoteMountedError(BaseException):
|
class RemoteMountedError(BaseException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
# Copyright (C) 2025 Paul Retourné
|
# Copyright (C) 2025-2026 Paul Retourné
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from argparser import create_argparser
|
from pathlib import Path
|
||||||
from config import RootsConfig, ServerConfig, Config, load_config
|
|
||||||
from synchroniser import Synchroniser
|
from unisync.argparser import create_argparser
|
||||||
from pathlib import Path, PosixPath
|
from unisync.runners import unisync_sync, unisync_add, unisync_mount
|
||||||
from paths import *
|
from unisync.config import load_config
|
||||||
|
from unisync.synchroniser import Synchroniser
|
||||||
|
from unisync.paths import *
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = create_argparser()
|
parser = create_argparser(unisync_sync, unisync_add, unisync_mount)
|
||||||
base_namespace = parser.parse_args()
|
cli_args = parser.parse_args()
|
||||||
|
|
||||||
config_path = os.path.expanduser("~/.config/unisync/config.ini")
|
config_path = os.path.expanduser("~/.config/unisync/config.ini")
|
||||||
if base_namespace.config != None and os.path.isfile(base_namespace.config):
|
# Check if --config is set
|
||||||
config = load_config(base_namespace.config)
|
if cli_args.config != None and os.path.isfile(cli_args.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 make the command line arguments work and override the config options
|
# TODO replace the next line with something to do if no config file is found
|
||||||
|
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,
|
||||||
@@ -33,18 +39,7 @@ 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)
|
||||||
|
|
||||||
if synchroniser.create_ssh_master_connection() != 0:
|
cli_args.func(synchroniser, paths_manager)
|
||||||
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__":
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2025 Paul Retourné
|
# Copyright (C) 2025-2026 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("w") as f:
|
with self.paths_file.open("a") as f:
|
||||||
for p in paths_to_add:
|
for p in paths_to_add:
|
||||||
f.write(p + "\n")
|
f.write(p + "\n")
|
||||||
|
|
||||||
|
|||||||
36
src/unisync/runners.py
Normal file
36
src/unisync/runners.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 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()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2025 Paul Retourné
|
# Copyright (C) 2025-2026 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 errors import RemoteMountedError, InvalidMountError
|
from unisync.errors import RemoteMountedError, InvalidMountError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user