feat: add iCal calendar subscriptions
This commit is contained in:
@@ -152,6 +152,18 @@ def parse_archive_path(path: Path, *, max_archive_bytes: int = MAX_ARCHIVE_BYTES
|
||||
content_names = set(names) - {"manifest.json"}
|
||||
if set(checksums) != content_names:
|
||||
raise backup_error("backup_manifest_mismatch", "manifest 与 ZIP 条目不一致")
|
||||
# Calendar subscriptions are an additive backup-v2 entity. Accept archives
|
||||
# produced by older helpers that checksum the new file but omit its count.
|
||||
optional_entities = {"calendar_subscriptions"}
|
||||
undeclared_optional = {
|
||||
f"data/{name}.json" for name in optional_entities - set(declared_entities)
|
||||
}
|
||||
if set(declared_entities) != {
|
||||
name[5:-5]
|
||||
for name in content_names - undeclared_optional
|
||||
if name.startswith("data/") and name.endswith(".json")
|
||||
}:
|
||||
raise backup_error("backup_manifest_mismatch", "manifest 实体清单不一致")
|
||||
entities: dict[str, list[dict]] = {}
|
||||
files: dict[str, StagedBlob] = {}
|
||||
for index, name in enumerate(sorted(content_names)):
|
||||
@@ -170,7 +182,11 @@ def parse_archive_path(path: Path, *, max_archive_bytes: int = MAX_ARCHIVE_BYTES
|
||||
if not isinstance(checksums[name], str) or actual_digest != checksums[name]:
|
||||
raise backup_error("backup_checksum_mismatch", "备份校验和不匹配")
|
||||
if set(declared_entities) != set(entities):
|
||||
raise backup_error("backup_manifest_mismatch", "manifest 实体清单不一致")
|
||||
required_declared = set(entities) - optional_entities
|
||||
if set(declared_entities) != required_declared or any(
|
||||
entities.get(name) for name in optional_entities - set(declared_entities)
|
||||
):
|
||||
raise backup_error("backup_manifest_mismatch", "manifest 实体清单不一致")
|
||||
if any(type(count) is not int or count < 0 or count != len(entities[name]) for name, count in declared_entities.items()):
|
||||
raise backup_error("backup_manifest_mismatch", "manifest 实体数量不一致")
|
||||
except HTTPException:
|
||||
|
||||
@@ -19,6 +19,7 @@ from backend.models import (
|
||||
BackupImport,
|
||||
BackupImportEntity,
|
||||
BackupPreflight,
|
||||
CalendarSubscription,
|
||||
Countdown,
|
||||
Folder,
|
||||
Habit,
|
||||
@@ -65,6 +66,7 @@ ENTITY_MODELS = {
|
||||
"habit_pauses": HabitPause,
|
||||
"countdowns": Countdown,
|
||||
"memos": Memo,
|
||||
"calendar_subscriptions": CalendarSubscription,
|
||||
"attachments": Attachment,
|
||||
}
|
||||
RELATIONS = {
|
||||
@@ -392,6 +394,9 @@ def _validate_recurrence_graph(parsed: ParsedArchive) -> None:
|
||||
|
||||
def validate_archive(parsed: ParsedArchive) -> None:
|
||||
unknown = set(parsed.entities) - set(ENTITY_MODELS)
|
||||
# Calendar subscriptions were introduced after backup v2. Treat their
|
||||
# absence as an empty collection so archives from older Dodo releases remain restorable.
|
||||
parsed.entities.setdefault("calendar_subscriptions", [])
|
||||
missing = set(ENTITY_MODELS) - set(parsed.entities)
|
||||
if unknown:
|
||||
raise backup_error("backup_entity_unknown", "备份包含未知实体")
|
||||
@@ -611,7 +616,16 @@ async def restore_v2(
|
||||
await db.execute(delete(HabitPause).where(
|
||||
HabitPause.habit_id.in_(select(Habit.id).where(Habit.user_id == user.id))
|
||||
))
|
||||
for model in (Attachment, Memo, Countdown, Task, Habit, TaskList, Folder):
|
||||
for model in (
|
||||
Attachment,
|
||||
CalendarSubscription,
|
||||
Memo,
|
||||
Countdown,
|
||||
Task,
|
||||
Habit,
|
||||
TaskList,
|
||||
Folder,
|
||||
):
|
||||
await db.execute(delete(model).where(model.user_id == user.id))
|
||||
await db.execute(delete(BackupImportEntity).where(BackupImportEntity.user_id == user.id))
|
||||
await db.execute(delete(BackupImport).where(BackupImport.user_id == user.id))
|
||||
|
||||
Reference in New Issue
Block a user