diff --git a/README.md b/README.md
index d6746a0..c800480 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
# chguard
-

+
diff --git a/chguard/cli.py b/chguard/cli.py
index fa4e1f3..e825d37 100644
--- a/chguard/cli.py
+++ b/chguard/cli.py
@@ -248,42 +248,49 @@ def main() -> None:
root = normalize_root(args.save)
- if state_exists(conn, args.name):
- if not args.overwrite:
- raise SystemExit(
- f"State '{args.name}' already exists (use --overwrite)"
- )
- delete_state(conn, args.name)
+ try:
+ with conn: # start transaction
+ if state_exists(conn, args.name):
+ if not args.overwrite:
+ raise SystemExit(
+ 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())
-
- # 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."
+ state_id = create_state(
+ conn, args.name, str(root), os.getuid(), commit=False
)
- conn.execute(
- """
- INSERT INTO entries (state_id, path, type, mode, uid, gid)
- VALUES (?, ?, ?, ?, ?, ?)
- """,
- (
- state_id,
- entry.path,
- entry.type,
- entry.mode,
- entry.uid,
- entry.gid,
- ),
- )
+ # 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.commit()
- console.print(f"Saved state '{args.name}' for {root}")
- return
+ conn.execute(
+ """
+ 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 not args.state:
diff --git a/chguard/db.py b/chguard/db.py
index 515b762..16989bd 100644
--- a/chguard/db.py
+++ b/chguard/db.py
@@ -61,19 +61,28 @@ def state_exists(conn: sqlite3.Connection, name: str) -> bool:
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:
cur = conn.execute(
"INSERT INTO states (name, root_path, created_at, created_by_uid) VALUES (?, ?, ?, ?)",
(name, root_path, utc_now_iso(), created_by_uid),
)
- conn.commit()
+ if commit:
+ conn.commit()
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,))
- conn.commit()
+ if commit:
+ conn.commit()
return cur.rowcount
diff --git a/pyproject.toml b/pyproject.toml
index 8212e32..4fcebdf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "chguard"
-version = "0.2.1"
+version = "0.2.2"
description = "Safety-first tool to snapshot and restore filesystem ownership and permissions."
authors = ["Marco D'Aleo "]
license = "GPL-3.0-or-later"