Initial release of SIMKEEP

This commit is contained in:
SIMKEEP committed 2026-10-06 15:49:27 +08:00
commit bb3fa56a48
46 files changed
+4617

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
import os
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
root=Path(__file__).resolve().parent.parent
source=Path(os.getenv('SIMKEEP_DB',str(root/'data'/'simkeep.db'))).resolve()
if not source.is_file():
raise SystemExit('数据库尚未创建')
directory=source.parent/'backups'
directory.mkdir(mode=0o700,exist_ok=True)
target=directory/f"simkeep-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S-%f')}.db"
fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
os.close(fd)
with sqlite3.connect(f'{source.as_uri()}?mode=ro',uri=True) as original,sqlite3.connect(target) as backup:
original.backup(backup)
print(f'备份完成:{target}')
+25
View File
@@ -0,0 +1,25 @@
"""Publish only browser assets. Server files, databases and .env are never copied."""
import argparse
import shutil
from pathlib import Path
ROOT=Path(__file__).resolve().parent.parent
ASSETS=('index.html','style.css','app.js','api.js','core.js','money.js','countries.js','favicon.svg')
parser=argparse.ArgumentParser()
parser.add_argument('--output',default='dist')
args=parser.parse_args()
target=(ROOT/args.output).resolve()
if target.parent!=ROOT or target.name not in ('dist','preview'):
parser.error('output must be dist or preview in this project')
for asset in ASSETS:
if not (ROOT/asset).is_file():
raise SystemExit(f'Missing asset: {asset}')
target.mkdir(exist_ok=True)
for asset in target.iterdir():
if asset.is_file() and asset.name not in ASSETS:
asset.unlink()
for asset in ASSETS:
temp=target/(asset+'.next')
shutil.copyfile(ROOT/asset,temp)
temp.replace(target/asset)
print(f'Published {len(ASSETS)} browser assets to {target}')
+60
View File
@@ -0,0 +1,60 @@
"""Restore a consistent SIMKEEP backup from stdin while the application is stopped."""
import argparse
import os
import shutil
import sqlite3
import sys
import tempfile
from contextlib import closing
from pathlib import Path
TABLES={'users','sessions','cards','events','bindings','notification_jobs','system_state'}
def restore_database(destination,stream,replace=False):
destination=Path(destination).resolve()
if destination.exists() and not replace:
raise ValueError('数据库已存在;先备份并停止应用,确认替换后使用 --replace')
staged=None
try:
with tempfile.TemporaryDirectory(prefix='simkeep-import-') as directory:
incoming=Path(directory)/'backup.db'
with incoming.open('wb') as target:
shutil.copyfileobj(stream,target)
if incoming.stat().st_size==0:
raise ValueError('备份为空,数据库未修改')
with closing(sqlite3.connect(f'{incoming.as_uri()}?mode=ro',uri=True)) as source:
if source.execute('PRAGMA integrity_check').fetchone()[0]!='ok':
raise ValueError('备份完整性检查失败,数据库未修改')
tables={row[0] for row in source.execute("SELECT name FROM sqlite_master WHERE type='table'")}
if not TABLES.issubset(tables):
raise ValueError('请选择完整的 SIMKEEP 数据库备份,数据库未修改')
destination.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
with tempfile.NamedTemporaryFile(prefix='.simkeep-restore-',suffix='.db',dir=destination.parent,delete=False) as target:
staged=Path(target.name)
with closing(sqlite3.connect(staged)) as target:
source.backup(target)
if target.execute('PRAGMA integrity_check').fetchone()[0]!='ok':
raise ValueError('恢复文件完整性检查失败,数据库未修改')
staged.chmod(0o600)
for suffix in ('-wal','-shm'):
Path(str(destination)+suffix).unlink(missing_ok=True)
staged.replace(destination)
except sqlite3.DatabaseError:
raise ValueError('备份格式无效或已损坏,数据库未修改') from None
finally:
if staged is not None:
staged.unlink(missing_ok=True)
if __name__=='__main__':
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--replace',action='store_true',help='明确替换现有数据库;须先备份并停止应用')
args=parser.parse_args()
root=Path(__file__).resolve().parent.parent
destination=os.getenv('SIMKEEP_DB',str(root/'data'/'simkeep.db'))
try:
restore_database(destination,sys.stdin.buffer,args.replace)
except (ValueError,OSError) as error:
parser.exit(1,f'恢复失败:{error}\n')
print('数据库恢复完成')