Compare commits
6 Commits
8caba75060
...
5af5374f77
| Author | SHA1 | Date | |
|---|---|---|---|
|
5af5374f77
|
|||
|
27924013d9
|
|||
|
48179034a7
|
|||
|
f9001ecb9d
|
|||
|
86a6c8acce
|
|||
|
4f6f48247d
|
13
README.md
13
README.md
@@ -1,6 +1,19 @@
|
|||||||
Unisync is a data synchronisation tool written in python and based on [unison](https://github.com/bcpierce00/unison).
|
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.
|
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
|
# 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.
|
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.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
from configparser import UNNAMED_SECTION
|
from configparser import UNNAMED_SECTION
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path, PosixPath
|
from pathlib import Path
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import configparser
|
import configparser
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ 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: list[str] | None = field(default_factory=list)
|
sshargs: str = ""
|
||||||
hostname: str = ""
|
hostname: str = ""
|
||||||
ip: str = ""
|
ip: str = ""
|
||||||
port: int = 22
|
port: int = 22
|
||||||
@@ -52,7 +52,7 @@ class OtherConfig:
|
|||||||
"""
|
"""
|
||||||
Dataclass keeping miscellanous configuration options
|
Dataclass keeping miscellanous configuration options
|
||||||
"""
|
"""
|
||||||
cache_dir_path: PosixPath = Path("~/.unisync").expanduser()
|
cache_dir_path: Path = Path("~/.unisync").expanduser()
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Config:
|
class Config:
|
||||||
@@ -82,10 +82,10 @@ def load_config(config_path:str) -> Config:
|
|||||||
|
|
||||||
server_config = ServerConfig(
|
server_config = ServerConfig(
|
||||||
config.get(server_section, "user"),
|
config.get(server_section, "user"),
|
||||||
config.get(server_section, "sshargs", fallback=None),
|
config.get(server_section, "sshargs", fallback=""),
|
||||||
config.get(server_section, "hostname", fallback=None),
|
config.get(server_section, "hostname", fallback=""),
|
||||||
config.get(server_section, "ip", fallback=None),
|
config.get(server_section, "ip", fallback=""),
|
||||||
config.getint(server_section, "port", fallback=None)
|
config.getint(server_section, "port", fallback=22)
|
||||||
)
|
)
|
||||||
roots_config = RootsConfig(
|
roots_config = RootsConfig(
|
||||||
config.get(roots_section, "local"),
|
config.get(roots_section, "local"),
|
||||||
|
|||||||
5
src/unisync/errors.py
Normal file
5
src/unisync/errors.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class RemoteMountedError(BaseException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class InvalidMountError(BaseException):
|
||||||
|
pass
|
||||||
@@ -5,6 +5,8 @@ import os
|
|||||||
from argparser import create_argparser
|
from argparser import create_argparser
|
||||||
from config import RootsConfig, ServerConfig, Config, load_config
|
from config import RootsConfig, ServerConfig, Config, load_config
|
||||||
from synchroniser import Synchroniser
|
from synchroniser import Synchroniser
|
||||||
|
from pathlib import Path, PosixPath
|
||||||
|
from paths import *
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = create_argparser()
|
parser = create_argparser()
|
||||||
@@ -29,14 +31,19 @@ def main():
|
|||||||
config.unison.values
|
config.unison.values
|
||||||
)
|
)
|
||||||
|
|
||||||
|
paths_manager = PathsManager(Path(config.roots.local), config.other.cache_dir_path)
|
||||||
|
|
||||||
if synchroniser.create_ssh_master_connection() != 0:
|
if synchroniser.create_ssh_master_connection() != 0:
|
||||||
print("Connection failed quitting")
|
print("Connection failed quitting")
|
||||||
return 1
|
return 1
|
||||||
print("Connected to the remote.")
|
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()
|
synchroniser.close_ssh_master_connection()
|
||||||
|
print(paths_manager.get_paths_to_sync())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
from pathlib import Path, PosixPath
|
from pathlib import Path
|
||||||
|
|
||||||
class PathsManager:
|
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
|
Creates a PathsManager with the necessary data
|
||||||
Args:
|
Args:
|
||||||
@@ -24,7 +25,7 @@ class PathsManager:
|
|||||||
raise ValueError("Invalid cache directory")
|
raise ValueError("Invalid cache directory")
|
||||||
self.cache_dir = cache_dir
|
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():
|
if not self.paths_file.is_file():
|
||||||
raise ValueError("The paths file does not exist")
|
raise ValueError("The paths file does not exist")
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ class PathsManager:
|
|||||||
"-p", "-",
|
"-p", "-",
|
||||||
self.local_dir
|
self.local_dir
|
||||||
]
|
]
|
||||||
nnn_process = subprocess.Popen(command, stdout=subprocess.PIPE)
|
nnn_process:subprocess.Popen = subprocess.Popen(command, stdout=subprocess.PIPE)
|
||||||
try:
|
try:
|
||||||
ret_code = nnn_process.wait(timeout=choice_timeout)
|
ret_code = nnn_process.wait(timeout=choice_timeout)
|
||||||
except subprocess.TimeoutExpired as e:
|
except subprocess.TimeoutExpired as e:
|
||||||
@@ -57,18 +58,18 @@ class PathsManager:
|
|||||||
|
|
||||||
if ret_code != 0:
|
if ret_code != 0:
|
||||||
print("File selection failed", file=sys.stderr)
|
print("File selection failed", file=sys.stderr)
|
||||||
raise subprocess.CalledProcessError("File selection failed")
|
raise subprocess.CalledProcessError(1, "File selection failed")
|
||||||
|
|
||||||
paths_list:list[str] = []
|
paths_list:list[str] = []
|
||||||
while (next_path := nnn_process.stdout.readline()) != b'':
|
while (next_path := nnn_process.stdout.readline()) != b'':
|
||||||
next_path = next_path.decode().strip()
|
next_path = next_path.decode().strip()
|
||||||
# Make the path relative to the top directory
|
# 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)
|
paths_list.append(next_path)
|
||||||
return paths_list
|
return paths_list
|
||||||
|
|
||||||
def add_files_to_sync(self):
|
def add_files_to_sync(self):
|
||||||
while true:
|
while True:
|
||||||
try:
|
try:
|
||||||
paths = self.user_select_files()
|
paths = self.user_select_files()
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import logging
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from errors import RemoteMountedError, InvalidMountError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Synchroniser:
|
class Synchroniser:
|
||||||
@@ -175,16 +177,24 @@ class Synchroniser:
|
|||||||
Mount the remote directory to make the local links work.
|
Mount the remote directory to make the local links work.
|
||||||
This is achieved using sshfs.
|
This is achieved using sshfs.
|
||||||
Raise:
|
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
|
- 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 = [
|
command = [
|
||||||
"/usr/bin/sshfs",
|
"/usr/bin/sshfs",
|
||||||
"-o", "ControlPath={self.control_path}",
|
"-o", "ControlPath={self.control_path}",
|
||||||
"-o", "ServerAliveInterval=15",
|
"-o", "ServerAliveInterval=15",
|
||||||
"-p", str(self.remote_port),
|
"-p", str(self.remote_port),
|
||||||
f"{self.remote_user}@{self.remote_ip}:{self.remote_dir}/.data",
|
f"{self.remote_user}@{self.remote_ip}:{self.remote_dir}/.data",
|
||||||
# Get the absolute path to the correct .data directory resolving symlinks
|
str(path_to_mount)
|
||||||
str(Path(f"{self.local}/../.data").resolve())
|
|
||||||
]
|
]
|
||||||
completed_process = subprocess.run(command)
|
completed_process = subprocess.run(command)
|
||||||
completed_process.check_returncode()
|
completed_process.check_returncode()
|
||||||
|
|||||||
Reference in New Issue
Block a user