A backup filename cannot establish that recent articles are inside it. K-POP PULSE’s operating records retain a WAL-aware procedure. I reproduced the principle in a new synthetic database and wrote a small local program another website can adapt.
This is not the original backup script. The new code creates a file with SQLite’s backup API and inspects the destination’s integrity and counts. It is distinct from a restore of the original operating site; a full website also needs other material and service relationships.
Compare a main-file copy of an active synthetic database
The new test stores a baseline row and completes a checkpoint, then commits a second row while keeping the connection open. With a WAL file actually present, a copy of only the main database returns the baseline. A subsequent backup-API snapshot can be reopened to read both IDs and the second row’s text.
This is a present demonstration with new synthetic material, not evidence that recent K-POP articles were lost. Comparing the two outputs on the same inspected input gives a concrete basis beyond counting filenames or reading their sizes.
Use an input contract that does not overwrite a destination
The source must be an existing local SQLite file; the destination must be a new file. The same source/target, existing output and missing source are rejected. Parent directories are not created. An exclusive reservation prevents beginning by truncating an existing destination.
The source connection reads database rows in read-only mode, but its SQLite environment can still require sidecar access or creation. That setting is not a promise of zero filesystem effects. The small checks do not certify network filesystems or databases using arbitrary extensions.
Close the handles and inspect a standalone copy
Only the new destination is changed to a journal mode suitable for a standalone file. The source mode and rows are not changed. The program checks destination integrity and reports user-table counts plus file size and hash. The test also closes handles and reopens the copy to inspect expected rows and body text.
An integrity pass does not establish business-data correctness. Compare the content the service needed to retain, not just counts. Equal counts can conceal changed prose. An actual service recovery needs its own checks of addresses, body content and linked assets.
Return failure and completion as different outcomes
An output can be partial during execution. Wait for exit0 and ok=true before treating it as complete. Input and database failures return a general error with exit2 and remove only this invocation’s newly created output and sidecars. Checks verify that an existing destination’s bytes survive rejection.
The backup-progress limit defaults to30seconds and can be set up to3600seconds. It is not a deadline covering hashing and every later inspection. Large files and special tables need environment-specific checks rather than a weaker condition introduced to eliminate a failure.
Distinguish a database snapshot from a website recovery
A snapshot reflects its backup reading, not all later commits or uncommitted changes. External images, videos, configuration and other files require separate retention. Backing up several databases independently does not establish one cross-database atomic moment.
I do not rename file creation as a completed restore. A website using the program should inventory its needed files and service links, then inspect actual queries and flows from a recovered copy. These tests did not restore the original site or change its operating data.
Bring the checks and application notes with the program
The new CLI uses Python’s standard library. Supply your local --source and --destination. Begin with your own test database and a new path to inspect permissions and journal behavior. It sends no data over the network and does not restore a cloud operating database.
Seventeen checks passed, covering actual synthetic WAL activation, main-only omission, snapshot content, standalone reopening, existing-file protection and real CLI execution. The implementation and ZIP share the checks and notes. When extending to another database or media set, continue verifying that the intended retained content can actually be read.
Explore the implementation
New local snapshot implementation. Check its contract on your own test database.
"""Create a new local SQLite snapshot through its backup API; never overwrite a destination."""
import argparse,hashlib,json,os,sqlite3,sys,time
from contextlib import closing
from pathlib import Path
def sqlite_snapshot(source,destination,*,timeout_seconds=30):
source=Path(source).resolve(strict=True);destination=Path(destination).absolute()
if not source.is_file() or source==destination.resolve():raise ValueError('Distinct database file required')
if isinstance(timeout_seconds,bool) or not isinstance(timeout_seconds,(int,float)) or not 0<timeout_seconds<=3600:
raise ValueError('Bounded positive timeout required')
if destination.exists() or destination.is_symlink():raise FileExistsError('Destination already exists')
if not destination.parent.is_dir():raise ValueError('Destination directory must exist')
# Reserve the exact destination before SQLite opens it, so an existing file is never truncated.
fd=os.open(destination,os.O_CREAT|os.O_EXCL|os.O_WRONLY,0o600);os.close(fd)
began=time.monotonic();complete=False
try:
with closing(sqlite3.connect(source.as_uri()+'?mode=ro',uri=True,timeout=min(timeout_seconds,5))) as src:
with closing(sqlite3.connect(destination)) as dst:
def progress(status,remaining,total):
if time.monotonic()-began>timeout_seconds:raise TimeoutError('Snapshot time limit')
src.backup(dst,pages=128,progress=progress,sleep=0.05)
# Change only the new destination to standalone rollback-journal mode.
if dst.execute('PRAGMA journal_mode=DELETE').fetchone()[0]!='delete':raise ValueError('Standalone snapshot mode unavailable')
integrity=dst.execute('PRAGMA integrity_check').fetchall()
if integrity!=[('ok',)]:raise ValueError('Snapshot integrity check failed')
tables=[r[0] for r in dst.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")]
counts={name:dst.execute('SELECT count(*) FROM "'+name.replace('"','""')+'"').fetchone()[0] for name in tables}
# Close both handles before treating the new standalone database as complete.
src.close();dst.close()
digest=hashlib.sha256()
with destination.open('rb') as stream:
for chunk in iter(lambda:stream.read(1024*1024),b''):digest.update(chunk)
complete=True
return {'ok':True,'bytes':destination.stat().st_size,'sha256':digest.hexdigest(),'table_rows':counts,'integrity':'ok','sqlite_version':sqlite3.sqlite_version,'scope':'New local database snapshot at its backup reading, not all future commits, external media backup or production restore.'}
finally:
# Handles are closed before cleanup, including exceptional paths.
if 'src' in locals():src.close()
if 'dst' in locals():dst.close()
if not complete:
for suffix in ['', '-journal','-wal','-shm']:
candidate=Path(str(destination)+suffix)
if candidate.exists():candidate.unlink()
def main():
p=argparse.ArgumentParser();p.add_argument('--source',type=Path,required=True);p.add_argument('--destination',type=Path,required=True);p.add_argument('--timeout',type=float,default=30);a=p.parse_args()
try:result=sqlite_snapshot(a.source,a.destination,timeout_seconds=a.timeout)
except (OSError,ValueError,sqlite3.Error,TimeoutError):
print(json.dumps({'ok':False,'error':'Snapshot unavailable; inspect source access, destination and input contract'}));return 2
print(json.dumps(result,ensure_ascii=False));return 0
if __name__=='__main__':raise SystemExit(main())