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
This commit is contained in:
@@ -8,7 +8,8 @@ import subprocess
|
|||||||
|
|
||||||
from watchdog.observers import Observer
|
from watchdog.observers import Observer
|
||||||
from watchdog.events import FileSystemEventHandler
|
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
|
INTERVAL = 10
|
||||||
running = True
|
running = True
|
||||||
@@ -40,7 +41,7 @@ def stop_recording(con: obs.ReqClient) -> None:
|
|||||||
con.stop_record()
|
con.stop_record()
|
||||||
print("Stopped recording")
|
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):
|
for filename in os.listdir(directory):
|
||||||
file_path = os.path.join(directory, filename)
|
file_path = os.path.join(directory, filename)
|
||||||
if os.path.isfile(file_path):
|
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)
|
os.remove(file_path)
|
||||||
print(f"Removed old file: {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):
|
def handle_shutdown(signum, frame):
|
||||||
global running
|
global running
|
||||||
running = False
|
running = False
|
||||||
@@ -85,7 +70,8 @@ def main() -> None:
|
|||||||
|
|
||||||
recording_dir = con.get_record_directory().record_directory
|
recording_dir = con.get_record_directory().record_directory
|
||||||
start_recording(con)
|
start_recording(con)
|
||||||
create_state_file()
|
|
||||||
|
create_state_file_if_needed()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
event_handler = Handler()
|
event_handler = Handler()
|
||||||
@@ -94,7 +80,8 @@ def main() -> None:
|
|||||||
observer.start()
|
observer.start()
|
||||||
|
|
||||||
while running:
|
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)
|
time.sleep(INTERVAL)
|
||||||
finally:
|
finally:
|
||||||
stop_recording(con)
|
stop_recording(con)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from importlib import resources
|
|||||||
|
|
||||||
APP_NAME = "rewind"
|
APP_NAME = "rewind"
|
||||||
USER_CONFIG = Path.home() / ".rewind.toml"
|
USER_CONFIG = Path.home() / ".rewind.toml"
|
||||||
|
STATE_NAME = "state.json"
|
||||||
|
|
||||||
def load_config() -> dict:
|
def load_config() -> dict:
|
||||||
if USER_CONFIG.exists():
|
if USER_CONFIG.exists():
|
||||||
@@ -16,25 +17,4 @@ def load_config() -> dict:
|
|||||||
|
|
||||||
# fallback to packaged default
|
# fallback to packaged default
|
||||||
with resources.files("rewind").joinpath("config.toml").open("rb") as f:
|
with resources.files("rewind").joinpath("config.toml").open("rb") as f:
|
||||||
return tomllib.load(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
|
|
||||||
60
rewind/state.py
Normal file
60
rewind/state.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user