From 2d179304ea5f82a7468c38743efb488114b538c4 Mon Sep 17 00:00:00 2001 From: Ojaybee Date: Thu, 12 Jun 2025 11:54:35 +0100 Subject: [PATCH 1/5] Refactor daemon with memory-safe loop and logging --- media_sync_daemon.py | 95 ++++++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/media_sync_daemon.py b/media_sync_daemon.py index 816cbfb..9709e7b 100644 --- a/media_sync_daemon.py +++ b/media_sync_daemon.py @@ -1,16 +1,12 @@ -""" -Mergin Media Sync - a tool to sync media files from Mergin projects to other storage backends - -Copyright (C) 2021 Lutra Consulting - -License: MIT -""" - import argparse -import sys import datetime +import gc +import logging import os +import sys import time + +from config import config, validate_config, ConfigError, update_config_path from drivers import DriverError, create_driver from media_sync import ( create_mergin_client, @@ -19,10 +15,33 @@ mc_pull, MediaSyncError, ) -from config import config, validate_config, ConfigError, update_config_path from version import __version__ +def setup_logger(): + logger = logging.getLogger("media-sync") + logger.setLevel(logging.INFO) + handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger + + +def run_sync_cycle(mc, driver, logger): + try: + logger.info("Pulling changes from Mergin...") + files_to_sync = mc_pull(mc) + media_sync_push(mc, driver, files_to_sync) + logger.info("Sync complete.") + + # Force garbage collection + gc.collect() + + except MediaSyncError as e: + logger.error(f"Media sync error: {e}") + + def main(): parser = argparse.ArgumentParser( prog="media_sync_daemon.py", @@ -34,66 +53,54 @@ def main(): "config_file", nargs="?", default="config.yaml", - help="Path to file with configuration. Default value is config.yaml in current working directory.", + help="Path to file with configuration. Default is ./config.yaml", ) args = parser.parse_args() - - print(f"== Starting Mergin Media Sync daemon version {__version__} ==") + logger = setup_logger() + logger.info(f"== Starting Mergin Media Sync daemon v{__version__} ==") try: update_config_path(args.config_file) - except IOError as e: - print("Error:" + str(e)) + validate_config(config) + except (IOError, ConfigError) as e: + logger.error(f"Configuration error: {e}") sys.exit(1) sleep_time = config.as_int("daemon.sleep_time") - try: - validate_config(config) - except ConfigError as e: - print("Error: " + str(e)) - return - try: driver = create_driver(config) except DriverError as e: - print("Error: " + str(e)) - return + logger.error(f"Driver error: {e}") + sys.exit(1) - print("Logging in to Mergin...") + logger.info("Logging in to Mergin...") try: mc = create_mergin_client() - - # initialize or pull changes to sync with latest project version if not os.path.exists(config.project_working_dir): + logger.info("Project directory not found. Downloading from Mergin...") files_to_sync = mc_download(mc) media_sync_push(mc, driver, files_to_sync) except MediaSyncError as e: - print("Error: " + str(e)) - return + logger.error(f"Initial sync error: {e}") + sys.exit(1) - # keep running until killed by ctrl+c: - # - sleep N seconds - # - pull - # - push + logger.info("Entering sync loop...") while True: - print(datetime.datetime.now()) - try: - files_to_sync = mc_pull(mc) - media_sync_push(mc, driver, files_to_sync) + logger.info(f"Heartbeat: {datetime.datetime.utcnow().isoformat()} UTC") + run_sync_cycle(mc, driver, logger) - # check mergin client token expiration - delta = mc._auth_session["expire"] - datetime.datetime.now( - datetime.timezone.utc - ) + # Check token expiry + try: + delta = mc._auth_session["expire"] - datetime.datetime.now(datetime.timezone.utc) if delta.total_seconds() < 3600: + logger.info("Refreshing Mergin auth token...") mc = create_mergin_client() + except Exception as e: + logger.warning(f"Error checking token expiration: {e}") - except MediaSyncError as e: - print("Error: " + str(e)) - - print("Going to sleep") + logger.info(f"Sleeping for {sleep_time} seconds...") time.sleep(sleep_time) From ec09a8afc8ea63a52f302307dce473765ef9ab8e Mon Sep 17 00:00:00 2001 From: Ojaybee Date: Thu, 12 Jun 2025 13:37:42 +0100 Subject: [PATCH 2/5] Restore original file header --- media_sync_daemon.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/media_sync_daemon.py b/media_sync_daemon.py index 9709e7b..372b403 100644 --- a/media_sync_daemon.py +++ b/media_sync_daemon.py @@ -1,3 +1,11 @@ +""" +Mergin Media Sync - a tool to sync media files from Mergin projects to other storage backends + +Copyright (C) 2021 Lutra Consulting + +License: MIT +""" + import argparse import datetime import gc From 7d971b0794c627b47f3dd3777de2385b8e6e3819 Mon Sep 17 00:00:00 2001 From: Ojaybee Date: Tue, 6 Jan 2026 09:29:32 +0000 Subject: [PATCH 3/5] Changes logger.info mentions of "Mergin" to "Mergin maps server" for clarity. --- Dockerfile | 2 +- media_sync_daemon.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 91be63c..4a79774 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim-buster +FROM python:3.11-slim-bookworm # LABEL instead of MAINTAINER (fixes deprecation warning) LABEL maintainer="Martin Dobias " diff --git a/media_sync_daemon.py b/media_sync_daemon.py index 372b403..7d5ba54 100644 --- a/media_sync_daemon.py +++ b/media_sync_daemon.py @@ -38,7 +38,7 @@ def setup_logger(): def run_sync_cycle(mc, driver, logger): try: - logger.info("Pulling changes from Mergin...") + logger.info("Pulling changes from Mergin maps server...") files_to_sync = mc_pull(mc) media_sync_push(mc, driver, files_to_sync) logger.info("Sync complete.") @@ -83,11 +83,11 @@ def main(): logger.error(f"Driver error: {e}") sys.exit(1) - logger.info("Logging in to Mergin...") + logger.info("Logging in to Mergin maps server...") try: mc = create_mergin_client() if not os.path.exists(config.project_working_dir): - logger.info("Project directory not found. Downloading from Mergin...") + logger.info("Project directory not found. Downloading from Mergin maps server...") files_to_sync = mc_download(mc) media_sync_push(mc, driver, files_to_sync) except MediaSyncError as e: @@ -103,10 +103,10 @@ def main(): try: delta = mc._auth_session["expire"] - datetime.datetime.now(datetime.timezone.utc) if delta.total_seconds() < 3600: - logger.info("Refreshing Mergin auth token...") + logger.info("Refreshing Mergin maps server auth token...") mc = create_mergin_client() except Exception as e: - logger.warning(f"Error checking token expiration: {e}") + logger.warning(f"Error checking Mergin maps server token expiration: {e}") logger.info(f"Sleeping for {sleep_time} seconds...") time.sleep(sleep_time) From ce898105d62cb639159cbf2fc93c16ed2b8cd4cf Mon Sep 17 00:00:00 2001 From: Ojaybee Date: Tue, 6 Jan 2026 09:30:21 +0000 Subject: [PATCH 4/5] Removes forced garbage collection --- media_sync_daemon.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/media_sync_daemon.py b/media_sync_daemon.py index 7d5ba54..f3d0e7c 100644 --- a/media_sync_daemon.py +++ b/media_sync_daemon.py @@ -43,8 +43,7 @@ def run_sync_cycle(mc, driver, logger): media_sync_push(mc, driver, files_to_sync) logger.info("Sync complete.") - # Force garbage collection - gc.collect() + except MediaSyncError as e: logger.error(f"Media sync error: {e}") From 12cc3e00b20dd2cedc493285a243c42c0953a268 Mon Sep 17 00:00:00 2001 From: Ojaybee Date: Tue, 6 Jan 2026 10:01:49 +0000 Subject: [PATCH 5/5] Cleans up exception handling for token refresh --- media_sync_daemon.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/media_sync_daemon.py b/media_sync_daemon.py index f3d0e7c..dd9de7e 100644 --- a/media_sync_daemon.py +++ b/media_sync_daemon.py @@ -101,15 +101,28 @@ def main(): # Check token expiry try: delta = mc._auth_session["expire"] - datetime.datetime.now(datetime.timezone.utc) + except (AttributeError, KeyError, TypeError) as e: + logger.warning( + f"Error checking Mergin token expiration (skipping refresh this cycle): {e}" + ) + else: if delta.total_seconds() < 3600: logger.info("Refreshing Mergin maps server auth token...") - mc = create_mergin_client() - except Exception as e: - logger.warning(f"Error checking Mergin maps server token expiration: {e}") + try: + mc = create_mergin_client() + except MediaSyncError as e: + # MediaSyncError already wraps LoginError/ClientError from MerginClient + logger.warning( + f"Failed to refresh Mergin maps server auth token " + f"(will retry next cycle): {e}" + ) + else: + logger.info("Mergin maps server auth token refreshed successfully.") logger.info(f"Sleeping for {sleep_time} seconds...") time.sleep(sleep_time) + if __name__ == "__main__": main()