From 23d576c4384cb5dd94e904dc67aec7af0dae5079 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Wed, 7 Jan 2026 20:15:54 +0000 Subject: [PATCH] refactor: extract state management into dedicated module and persist across restarts Move state handling out of daemon and paths into a new state module. - Introduce state.py for loading, writing, and maintaining state.json - Persist recorded file metadata across daemon restarts - Add cleanup of stale state entries when files are deleted - Rename cleanup_old_files to cleanup_physical_files for clarity - Ensure state file is created lazily if missing --- rewind/daemon.py | 27 ++++++---------------- rewind/paths.py | 24 ++----------------- rewind/state.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 42 deletions(-) create mode 100644 rewind/state.py diff --git a/rewind/daemon.py b/rewind/daemon.py index 4d83346..caf8c9a 100755 --- a/rewind/daemon.py +++ b/rewind/daemon.py @@ -8,7 +8,8 @@ import subprocess from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler -from rewind.paths import load_state, write_state, load_config +from rewind.paths import load_config +from rewind.state import add_file_to_state, create_state_file_if_needed, cleanup_state_files INTERVAL = 10 running = True @@ -40,7 +41,7 @@ def stop_recording(con: obs.ReqClient) -> None: con.stop_record() print("Stopped recording") -def cleanup_old_files(directory: str, max_age_seconds: int) -> None: +def cleanup_physical_files(directory: str, max_age_seconds: int) -> None: for filename in os.listdir(directory): file_path = os.path.join(directory, filename) if os.path.isfile(file_path): @@ -49,22 +50,6 @@ def cleanup_old_files(directory: str, max_age_seconds: int) -> None: os.remove(file_path) print(f"Removed old file: {file_path}") -def create_state_file() -> None: - state = {"files": []} - write_state(state) - -def add_file_to_state(file_path: str) -> None: - state = load_state() - files = state.get("files", []) - - files.append({ - "path": file_path, - "timestamp": datetime.datetime.now().timestamp(), - }) - - state["files"] = files - write_state(state) - def handle_shutdown(signum, frame): global running running = False @@ -85,7 +70,8 @@ def main() -> None: recording_dir = con.get_record_directory().record_directory start_recording(con) - create_state_file() + + create_state_file_if_needed() try: event_handler = Handler() @@ -94,7 +80,8 @@ def main() -> None: observer.start() while running: - cleanup_old_files(recording_dir, config["record"]["max_record_time"]) + cleanup_physical_files(recording_dir, config["record"]["max_record_time"]) + cleanup_state_files() time.sleep(INTERVAL) finally: stop_recording(con) diff --git a/rewind/paths.py b/rewind/paths.py index 6f59c6b..1d09bf1 100644 --- a/rewind/paths.py +++ b/rewind/paths.py @@ -8,6 +8,7 @@ from importlib import resources APP_NAME = "rewind" USER_CONFIG = Path.home() / ".rewind.toml" +STATE_NAME = "state.json" def load_config() -> dict: if USER_CONFIG.exists(): @@ -16,25 +17,4 @@ def load_config() -> dict: # fallback to packaged default with resources.files("rewind").joinpath("config.toml").open("rb") as f: - return tomllib.load(f) - -def state_dir() -> Path: - base = os.path.expanduser("~/.local/share") - path = Path(base) / APP_NAME - path.mkdir(parents=True, exist_ok=True) - return path - -def get_state_file_path() -> Path: - return state_dir() / "state.json" - -def load_state() -> dict: - if not get_state_file_path().exists(): - return {"files": []} - with get_state_file_path().open() as f: - return json.load(f) - -def write_state(state: dict) -> None: - tmp = get_state_file_path().with_suffix(".tmp") - with tmp.open("w") as f: - json.dump(state, f, indent=2) - tmp.replace(get_state_file_path()) # atomic + return tomllib.load(f) \ No newline at end of file diff --git a/rewind/state.py b/rewind/state.py new file mode 100644 index 0000000..5d45336 --- /dev/null +++ b/rewind/state.py @@ -0,0 +1,60 @@ +import datetime +import json +import os + +from rewind.paths import load_config +from pathlib import Path + +APP_NAME = "rewind" +STATE_NAME = "state.json" + +def state_dir() -> Path: + base = os.path.expanduser("~/.local/share") + path = Path(base) / APP_NAME + path.mkdir(parents=True, exist_ok=True) + return path + +def get_state_file_path() -> Path: + return state_dir() / STATE_NAME + +def load_state() -> dict: + if not get_state_file_path().exists(): + return {"files": []} + with get_state_file_path().open() as f: + return json.load(f) + +def write_state(state: dict) -> None: + tmp = get_state_file_path().with_suffix(".tmp") + with tmp.open("w") as f: + json.dump(state, f, indent=2) + tmp.replace(get_state_file_path()) # atomic + +def add_file_to_state(file_path: str) -> None: + state = load_state() + files = state.get("files", []) + + files.append({ + "path": file_path, + "timestamp": datetime.datetime.now().timestamp(), + }) + + state["files"] = files + write_state(state) + +def cleanup_state_files() -> None: + state = load_state() + files = state.get("files", []) + + # Remove old files from state + for file in files: + if not os.path.exists(file["path"]): + files.remove(file) + print(f"Removed non-existent file from state: {file['path']}") + + state["files"] = files + write_state(state) + +def create_state_file_if_needed() -> None: + if not os.path.exists(get_state_file_path()): + state = {"files": []} + write_state(state)