Make save operation transactional #3

Merged
mdaleo404 merged 1 commits from transaction_patch into main 2025-12-23 16:15:46 +00:00
4 changed files with 54 additions and 38 deletions
Showing only changes of commit 9658f534ea - Show all commits

View File

@@ -5,7 +5,7 @@
# chguard # chguard
<div align="center"> <div align="center">
<img src="chguard.png" alt="chguard logo" width="256" /> <img src="https://git.sysmd.uk/guardutils/chguard/src/branch/main/chguard.png" alt="chguard logo" width="256" />
</div> </div>

View File

@@ -248,42 +248,49 @@ def main() -> None:
root = normalize_root(args.save) root = normalize_root(args.save)
if state_exists(conn, args.name): try:
if not args.overwrite: with conn: # start transaction
raise SystemExit( if state_exists(conn, args.name):
f"State '{args.name}' already exists (use --overwrite)" if not args.overwrite:
) raise SystemExit(
delete_state(conn, args.name) f"State '{args.name}' already exists (use --overwrite)"
)
# if the new save fails, this delete_state step will also roll back
delete_state(conn, args.name, commit=False)
state_id = create_state(conn, args.name, str(root), os.getuid()) state_id = create_state(
conn, args.name, str(root), os.getuid(), commit=False
# Abort early if root-owned files exist and user is not root.
# This prevents creating snapshots that cannot be meaningfully restored.
for entry in scan_tree(root, excludes=args.exclude):
if entry.uid == 0 and not _is_root():
raise SystemExit(
"This path contains root-owned files.\n"
"Saving this state requires sudo."
) )
conn.execute( # Abort early if root-owned files exist and user is not root.
""" # This prevents creating snapshots that cannot be meaningfully restored.
INSERT INTO entries (state_id, path, type, mode, uid, gid) for entry in scan_tree(root, excludes=args.exclude):
VALUES (?, ?, ?, ?, ?, ?) if entry.uid == 0 and not _is_root():
""", raise SystemExit(
( "This path contains root-owned files.\n"
state_id, "Saving this state requires sudo."
entry.path, )
entry.type,
entry.mode,
entry.uid,
entry.gid,
),
)
conn.commit() conn.execute(
console.print(f"Saved state '{args.name}' for {root}") """
return INSERT INTO entries (state_id, path, type, mode, uid, gid)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
state_id,
entry.path,
entry.type,
entry.mode,
entry.uid,
entry.gid,
),
)
console.print(f"Saved state '{args.name}' for {root}")
return
except SystemExit:
raise
if args.restore: if args.restore:
if not args.state: if not args.state:

View File

@@ -61,19 +61,28 @@ def state_exists(conn: sqlite3.Connection, name: str) -> bool:
def create_state( def create_state(
conn: sqlite3.Connection, name: str, root_path: str, created_by_uid: int conn: sqlite3.Connection,
name: str,
root_path: str,
created_by_uid: int,
*,
commit: bool = True,
) -> int: ) -> int:
cur = conn.execute( cur = conn.execute(
"INSERT INTO states (name, root_path, created_at, created_by_uid) VALUES (?, ?, ?, ?)", "INSERT INTO states (name, root_path, created_at, created_by_uid) VALUES (?, ?, ?, ?)",
(name, root_path, utc_now_iso(), created_by_uid), (name, root_path, utc_now_iso(), created_by_uid),
) )
conn.commit() if commit:
conn.commit()
return int(cur.lastrowid) return int(cur.lastrowid)
def delete_state(conn: sqlite3.Connection, name: str) -> int: def delete_state(
conn: sqlite3.Connection, name: str, commit: bool = True
) -> int:
cur = conn.execute("DELETE FROM states WHERE name = ?", (name,)) cur = conn.execute("DELETE FROM states WHERE name = ?", (name,))
conn.commit() if commit:
conn.commit()
return cur.rowcount return cur.rowcount

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "chguard" name = "chguard"
version = "0.2.1" version = "0.2.2"
description = "Safety-first tool to snapshot and restore filesystem ownership and permissions." description = "Safety-first tool to snapshot and restore filesystem ownership and permissions."
authors = ["Marco D'Aleo <marco@marcodaleo.com>"] authors = ["Marco D'Aleo <marco@marcodaleo.com>"]
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"