Skip to content

Commit 0c23e77

Browse files
authored
Merge pull request #24 from codebude/feature/docs-content-update
feature/docs content update
2 parents a2a1b9b + bdd4478 commit 0c23e77

19 files changed

Lines changed: 76 additions & 8 deletions

backend/app/services/backup_restore.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,77 @@
2626
_LOCK_FILE: str = "backup_restore.lock"
2727

2828

29+
def _remove_wal_files(db_path: str) -> None:
30+
"""Remove stale WAL and SHM files left by a WAL-mode database.
31+
32+
Must be called after replacing the database file on disk and before
33+
opening any new connection, otherwise SQLite will try to replay the old
34+
WAL into the new database — causing B‑tree corruption.
35+
"""
36+
for suffix in ("-wal", "-shm"):
37+
path = f"{db_path}{suffix}"
38+
if os.path.isfile(path):
39+
os.remove(path)
40+
41+
42+
def _run_alembic_migrations() -> None:
43+
"""Run all pending alembic migrations to bring the database schema up to date.
44+
45+
This is necessary when restoring a backup from an older release whose schema
46+
may be behind the current codebase.
47+
"""
48+
from alembic.config import Config
49+
from alembic.command import upgrade
50+
51+
alembic_cfg = Config(os.path.join(os.path.dirname(__file__), "..", "..", "alembic.ini"))
52+
upgrade(alembic_cfg, "head")
53+
54+
55+
def _stamp_alembic_head_if_fresh() -> None:
56+
"""Stamp alembic_version at the current head when the version table is absent
57+
or empty.
58+
59+
After ``SQLModel.metadata.create_all()`` creates tables directly (bypassing
60+
alembic), alembic would otherwise try to re-run the initial migration and fail
61+
with *table already exists*.
62+
63+
When ``alembic_version`` already has a row (e.g. an old backup was restored),
64+
stamping is skipped so that ``alembic upgrade head`` can apply pending
65+
migrations on top of whatever revision the backup was at.
66+
"""
67+
from alembic.config import Config
68+
from alembic import command
69+
from alembic.script import ScriptDirectory
70+
71+
import sqlite3
72+
73+
db_path = _extract_db_path(settings.database_url)
74+
conn = sqlite3.connect(db_path)
75+
try:
76+
cursor = conn.execute(
77+
"SELECT name FROM sqlite_master WHERE type='table' AND name='alembic_version'"
78+
)
79+
table_exists = cursor.fetchone() is not None
80+
if table_exists:
81+
row_count = conn.execute("SELECT COUNT(*) FROM alembic_version").fetchone()[0]
82+
if row_count > 0:
83+
return
84+
finally:
85+
conn.close()
86+
87+
alembic_cfg = Config(os.path.join(os.path.dirname(__file__), "..", "..", "alembic.ini"))
88+
script = ScriptDirectory.from_config(alembic_cfg)
89+
head = script.get_current_head()
90+
if head:
91+
command.stamp(alembic_cfg, head)
92+
93+
2994
def _recreate_engine() -> None:
3095
"""Replace the global SQLAlchemy engine with a fresh one.
3196
3297
Called after restoring the database on disk so the app picks up the new data.
98+
Also runs pending alembic migrations for backward compatibility with backups
99+
created by older releases.
33100
"""
34101
from app.database import create_engine as _create_engine
35102
from sqlmodel import SQLModel
@@ -39,6 +106,8 @@ def _recreate_engine() -> None:
39106
connect_args={"check_same_thread": False},
40107
)
41108
SQLModel.metadata.create_all(new_engine)
109+
_stamp_alembic_head_if_fresh()
110+
_run_alembic_migrations()
42111
db_mod.engine = new_engine
43112

44113

@@ -272,6 +341,7 @@ def _rollback_safety_backup(safety_dir: str, database_url: str, data_dir: str) -
272341
safety_db = os.path.join(safety_dir, "database.db")
273342
if os.path.isfile(safety_db):
274343
shutil.copy2(safety_db, db_path)
344+
_remove_wal_files(db_path)
275345
for item_name in os.listdir(safety_dir):
276346
if item_name == "database.db":
277347
continue
@@ -368,6 +438,8 @@ def restore_backup(
368438
os.remove(tmp_path)
369439
raise
370440

441+
_remove_wal_files(db_path)
442+
371443
tmp_db_path = _extract_db_path(database_url)
372444
# NOTE: sqlite3.connect() context manager manages transactions, NOT
373445
# the connection itself. See note in _vacuum_into_backup above.

backend/tests/test_backup_restore.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -546,14 +546,13 @@ def test_restore_backup_single_file_too_large(valid_backup_zip: bytes, tmp_db_pa
546546

547547
# ── _recreate_engine ──────────────────────────────────────────────────────────
548548

549-
def test_recreate_engine(monkeypatch: MonkeyPatch) -> None:
549+
def test_recreate_engine(monkeypatch: MonkeyPatch, tmp_path: Path) -> None:
550550
"""_recreate_engine should replace app.database.engine."""
551551
import app.database as db_mod
552552

553553
original_engine = db_mod.engine
554+
tmp_db = str(tmp_path / "test.db")
554555
try:
555-
# Use a temp DB path
556-
tmp_db = "/tmp/test_recreate_engine.db"
557556
conn = sqlite3.connect(tmp_db)
558557
conn.execute("CREATE TABLE IF NOT EXISTS t (id INT)")
559558
conn.commit()
@@ -563,10 +562,7 @@ def test_recreate_engine(monkeypatch: MonkeyPatch) -> None:
563562
br._recreate_engine()
564563
new_engine = db_mod.engine
565564
assert new_engine is not original_engine
566-
567-
os.remove(tmp_db)
568565
finally:
569-
# Dispose the newly created engine before restoring the original
570566
if db_mod.engine is not original_engine:
571567
db_mod.engine.dispose()
572568
db_mod.engine = original_engine

docs/guide/using-librislog/administration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ Click "Delete" to remove a user account. You cannot delete your own account from
2525

2626
### Creating a Backup
2727

28-
Downloads the entire SQLite database as a `.db` file. This is a complete snapshot of your library, users, and settings.
28+
Downloads a ZIP archive containing the SQLite database, cover images, and import temp files. This is a complete snapshot of your library, users, and settings.
2929

3030
### Restoring a Backup
3131

32-
Upload a previously downloaded `.db` file to restore the database. The app validates the backup before applying it.
32+
Upload a previously downloaded `.zip` backup file to restore the database. The app validates the backup before applying it. Backups from older versions are automatically migrated to the current schema on restore.
3333

3434
::: warning
3535
Restoring overwrites all current data. Create a fresh backup first if you want to preserve your current library.
-740 Bytes
Loading
836 Bytes
Loading
-892 Bytes
Loading
578 Bytes
Loading
1.58 KB
Loading
88.3 KB
Loading
-752 Bytes
Loading

0 commit comments

Comments
 (0)