feat: add WebDAV sync support and startup/shutdown scripts

Backend:
- Add uuid, sync_version, is_deleted fields to all syncable models
- Add SyncSettings model for WebDAV configuration (AES-256-GCM encrypted passwords)
- Add crypto.py: AES-256-GCM encryption derived from JWT_SECRET via PBKDF2
- Add sync_lock.py: thread-level sync lock with 503 middleware for write blocking
- Add webdav.py: WebDAV client using requests (PUT/GET/MKCOL/DELETE)
- Add sync_service.py: push/pull/bidirectional merge with LWW conflict resolution
- Add sync router with 8 endpoints: config, test, push, pull, sync, status, remote delete
- Add UUID backfill for existing records in init_db()
- Add SQLAlchemy before_update event to auto-increment sync_version
- Register sync middleware to block writes during sync (503)

Frontend:
- Add sync API client (WebUI/src/api/sync.ts)
- Add useSyncStore with config, test, push/pull/sync operations
- Add WebDAV config + sync UI in SettingsView
- Add 503 status code handling in axios interceptor
- Add uuid field to all TypeScript type definitions

Scripts:
- Add scripts/start.bat and scripts/stop.bat for project management

Design doc: docs/plan/webdav-sync-design.md
This commit is contained in:
祀梦
2026-05-17 21:18:54 +08:00
parent 944d20dcc7
commit 0ab719500b
31 changed files with 2194 additions and 41 deletions

View File

@@ -11,7 +11,8 @@ from app.database import init_db, SessionLocal
from app.models.user_settings import UserSettings
from app.routers import api_router
from app.utils.logger import logger
from app.utils.auth import decode_access_token
from app.utils.auth import decode_access_token, get_cached_token_version, set_cached_token_version
from app.utils.sync_lock import is_syncing
from jose import JWTError
@@ -111,19 +112,39 @@ async def auth_middleware(request: Request, call_next):
except JWTError:
return JSONResponse(status_code=401, content={"detail": "登录已过期,请重新登录"})
db = SessionLocal()
try:
settings = db.query(UserSettings).filter(UserSettings.id == 1).first()
if settings and payload.get("tv") != settings.token_version:
user_id = payload.get("sub", "")
token_tv = payload.get("tv")
if token_tv is not None and user_id:
cached_tv = get_cached_token_version(user_id)
if cached_tv is None:
db = SessionLocal()
try:
settings = db.query(UserSettings).filter(UserSettings.id == 1).first()
cached_tv = settings.token_version if settings else 0
set_cached_token_version(user_id, cached_tv)
finally:
db.close()
if token_tv != cached_tv:
return JSONResponse(status_code=401, content={"detail": "密码已修改,请重新登录"})
finally:
db.close()
request.state.user = payload
return await call_next(request)
# 同步锁中间件(同步期间禁止写操作)
@app.middleware("http")
async def sync_lock_middleware(request: Request, call_next):
path = request.url.path
if is_syncing() and request.method in ("POST", "PUT", "PATCH", "DELETE"):
if not path.startswith("/api/sync"):
return JSONResponse(status_code=503, content={"detail": "正在同步数据,请稍后再试"})
return await call_next(request)
# 全局异常处理器
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):