114 lines
3.8 KiB
Python
114 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
# AUTHOR: Daesorin
|
|
# CREATED: 2026-06-03 20:12 +0100
|
|
# UPDATED: 2026-07-24 09:40 +0100
|
|
"""
|
|
The script was modified to replace the archive flag with explicit metadata-stripping and checksum evaluation flags (`-rltz`, `--checksum`, `--no-perms`, `--omit-dir-times`) to prevent inode timestamp updates during rsync pulls, while introducing a native post-pull sanitisation function that removes iOS-injected carriage returns without altering file modification times.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
from threading import Timer
|
|
from watchdog.events import FileSystemEventHandler
|
|
from watchdog.observers import Observer
|
|
|
|
# PRODUCTION CONFIGURATION
|
|
local_dir = "//home/sgbv/obsidian/"
|
|
remote_dest = "ish:/root/obsidian/"
|
|
debounce_seconds = 2.0
|
|
|
|
# ignore workspace ui states to prevent infinite sync loops
|
|
excludes = [
|
|
"--exclude=.obsidian/workspace*",
|
|
"--exclude=.trash/",
|
|
"--exclude=.git/"
|
|
]
|
|
|
|
def build_rsync_cmd(src, dest):
|
|
# use checksum and strip posix metadata mapping to prevent inode ctime destruction
|
|
flags = [
|
|
"rsync",
|
|
"-rltz",
|
|
"--checksum",
|
|
"--delete",
|
|
"--no-perms",
|
|
"--no-owner",
|
|
"--no-group",
|
|
"--omit-dir-times"
|
|
]
|
|
return flags + excludes + [src, dest]
|
|
|
|
def sanitize_line_endings():
|
|
# strip dos carriage returns natively while preserving file modification timestamps
|
|
print("sanitising line endings...")
|
|
vault_path = Path(local_dir)
|
|
for file_path in vault_path.rglob("*.md"):
|
|
try:
|
|
content = file_path.read_bytes()
|
|
if b"\r\n" in content:
|
|
clean_content = content.replace(b"\r\n", b"\n")
|
|
# record original timestamps before writing
|
|
stat_data = file_path.stat()
|
|
file_path.write_bytes(clean_content)
|
|
# restore original access and modification timestamps
|
|
os.utime(file_path, (stat_data.st_atime, stat_data.st_mtime))
|
|
print(f"cleaned carriage returns: {file_path.name}")
|
|
except Exception as error:
|
|
print(f"sanitisation error on {file_path.name}: {error}")
|
|
|
|
def pull_sync():
|
|
print("pulling changes from ish to arch...")
|
|
subprocess.run(build_rsync_cmd(remote_dest, local_dir))
|
|
sanitize_line_endings()
|
|
print("pull complete.")
|
|
|
|
def push_sync():
|
|
print("pushing changes from arch to ish...")
|
|
subprocess.run(build_rsync_cmd(local_dir, remote_dest))
|
|
print("push complete.")
|
|
|
|
class watchdog_push_handler(FileSystemEventHandler):
|
|
def __init__(self):
|
|
self.sync_timer = None
|
|
|
|
def on_any_event(self, event):
|
|
# ignore directory changes and workspace or hidden cache updates
|
|
if event.is_directory or any(x in event.src_path for x in [".obsidian", ".trash"]):
|
|
return
|
|
|
|
print(f"local change detected: {event.event_type}")
|
|
if self.sync_timer:
|
|
self.sync_timer.cancel()
|
|
|
|
self.sync_timer = Timer(debounce_seconds, push_sync)
|
|
self.sync_timer.start()
|
|
|
|
def watch_mode():
|
|
print(f"arch watchdog listening on {local_dir}")
|
|
handler = watchdog_push_handler()
|
|
observer = Observer()
|
|
observer.schedule(handler, local_dir, recursive=True)
|
|
observer.start()
|
|
|
|
try:
|
|
while True:
|
|
time.sleep(1)
|
|
except KeyboardInterrupt:
|
|
print("\nstopping watchdog...")
|
|
observer.stop()
|
|
observer.join()
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="arch sync node")
|
|
parser.add_argument("mode", nargs="?", default="watch", choices=["watch", "push", "pull"])
|
|
args = parser.parse_args()
|
|
|
|
if args.mode == "pull":
|
|
pull_sync()
|
|
elif args.mode == "push":
|
|
push_sync()
|
|
else:
|
|
watch_mode()
|