feat: extend BackupData model

This commit is contained in:
shiyu
2025-12-08 23:45:43 +08:00
parent 050577cf62
commit 12a3bb8efc
3 changed files with 130 additions and 14 deletions

View File

@@ -1,4 +1,5 @@
import json import json
from datetime import datetime
from fastapi import HTTPException from fastapi import HTTPException
from tortoise.transactions import in_transaction from tortoise.transactions import in_transaction
@@ -6,8 +7,12 @@ from tortoise.transactions import in_transaction
from domain.backup.types import BackupData from domain.backup.types import BackupData
from domain.config.service import VERSION from domain.config.service import VERSION
from models.database import ( from models.database import (
AIDefaultModel,
AIModel,
AIProvider,
AutomationTask, AutomationTask,
Configuration, Configuration,
Plugin,
ShareLink, ShareLink,
StorageAdapter, StorageAdapter,
UserAccount, UserAccount,
@@ -23,22 +28,38 @@ class BackupService:
tasks = await AutomationTask.all().values() tasks = await AutomationTask.all().values()
shares = await ShareLink.all().values() shares = await ShareLink.all().values()
configs = await Configuration.all().values() configs = await Configuration.all().values()
providers = await AIProvider.all().values()
models = await AIModel.all().values()
default_models = await AIDefaultModel.all().values()
plugins = await Plugin.all().values()
for share in shares: share_links = cls._serialize_datetime_fields(
share["created_at"] = ( shares, ["created_at", "expires_at"]
share["created_at"].isoformat() if share.get("created_at") else None )
) ai_providers = cls._serialize_datetime_fields(
share["expires_at"] = ( providers, ["created_at", "updated_at"]
share["expires_at"].isoformat() if share.get("expires_at") else None )
) ai_models = cls._serialize_datetime_fields(
models, ["created_at", "updated_at"]
)
ai_default_models = cls._serialize_datetime_fields(
default_models, ["created_at", "updated_at"]
)
plugin_items = cls._serialize_datetime_fields(
plugins, ["created_at", "updated_at"]
)
return BackupData( return BackupData(
version=VERSION, version=VERSION,
storage_adapters=list(adapters), storage_adapters=list(adapters),
user_accounts=list(users), user_accounts=list(users),
automation_tasks=list(tasks), automation_tasks=list(tasks),
share_links=list(shares), share_links=share_links,
configurations=list(configs), configurations=list(configs),
ai_providers=ai_providers,
ai_models=ai_models,
ai_default_models=ai_default_models,
plugins=plugin_items,
) )
@classmethod @classmethod
@@ -59,6 +80,10 @@ class BackupService:
await StorageAdapter.all().using_db(conn).delete() await StorageAdapter.all().using_db(conn).delete()
await UserAccount.all().using_db(conn).delete() await UserAccount.all().using_db(conn).delete()
await Configuration.all().using_db(conn).delete() await Configuration.all().using_db(conn).delete()
await AIDefaultModel.all().using_db(conn).delete()
await AIModel.all().using_db(conn).delete()
await AIProvider.all().using_db(conn).delete()
await Plugin.all().using_db(conn).delete()
if payload.configurations: if payload.configurations:
await Configuration.bulk_create( await Configuration.bulk_create(
@@ -86,6 +111,93 @@ class BackupService:
if payload.share_links: if payload.share_links:
await ShareLink.bulk_create( await ShareLink.bulk_create(
[ShareLink(**share) for share in payload.share_links], [
ShareLink(**share)
for share in cls._parse_datetime_fields(
payload.share_links, ["created_at", "expires_at"]
)
],
using_db=conn, using_db=conn,
) )
if payload.ai_providers:
await AIProvider.bulk_create(
[
AIProvider(**item)
for item in cls._parse_datetime_fields(
payload.ai_providers, ["created_at", "updated_at"]
)
],
using_db=conn,
)
if payload.ai_models:
await AIModel.bulk_create(
[
AIModel(**item)
for item in cls._parse_datetime_fields(
payload.ai_models, ["created_at", "updated_at"]
)
],
using_db=conn,
)
if payload.ai_default_models:
await AIDefaultModel.bulk_create(
[
AIDefaultModel(**item)
for item in cls._parse_datetime_fields(
payload.ai_default_models, ["created_at", "updated_at"]
)
],
using_db=conn,
)
if payload.plugins:
await Plugin.bulk_create(
[
Plugin(**item)
for item in cls._parse_datetime_fields(
payload.plugins, ["created_at", "updated_at"]
)
],
using_db=conn,
)
@staticmethod
def _serialize_datetime_fields(
records: list[dict], fields: list[str]
) -> list[dict]:
serialized: list[dict] = []
for record in records:
item = dict(record)
for field in fields:
value = item.get(field)
if isinstance(value, datetime):
item[field] = value.isoformat()
serialized.append(item)
return serialized
@staticmethod
def _parse_datetime_fields(
records: list[dict], fields: list[str]
) -> list[dict]:
parsed: list[dict] = []
for record in records:
item = dict(record)
for field in fields:
value = item.get(field)
if isinstance(value, str):
item[field] = BackupService._from_iso(value)
parsed.append(item)
return parsed
@staticmethod
def _from_iso(value: str) -> datetime | None:
if not value:
return None
normalized = value.replace("Z", "+00:00")
try:
return datetime.fromisoformat(normalized)
except ValueError as exc: # noqa: BLE001
raise HTTPException(status_code=400, detail="无效的日期格式") from exc

View File

@@ -10,3 +10,7 @@ class BackupData(BaseModel):
automation_tasks: list[dict[str, Any]] = Field(default_factory=list) automation_tasks: list[dict[str, Any]] = Field(default_factory=list)
share_links: list[dict[str, Any]] = Field(default_factory=list) share_links: list[dict[str, Any]] = Field(default_factory=list)
configurations: list[dict[str, Any]] = Field(default_factory=list) configurations: list[dict[str, Any]] = Field(default_factory=list)
ai_providers: list[dict[str, Any]] = Field(default_factory=list)
ai_models: list[dict[str, Any]] = Field(default_factory=list)
ai_default_models: list[dict[str, Any]] = Field(default_factory=list)
plugins: list[dict[str, Any]] = Field(default_factory=list)

View File

@@ -1,5 +1,5 @@
import { memo, useState } from 'react'; import { memo, useState } from 'react';
import { Button, Typography, Upload, message, Modal } from 'antd'; import { Button, Typography, Upload, message, Modal, Card } from 'antd';
import PageCard from '../../components/PageCard'; import PageCard from '../../components/PageCard';
import { UploadOutlined, DownloadOutlined } from '@ant-design/icons'; import { UploadOutlined, DownloadOutlined } from '@ant-design/icons';
import { backupApi } from '../../api/backup'; import { backupApi } from '../../api/backup';
@@ -55,7 +55,7 @@ const BackupPage = memo(function BackupPage() {
<PageCard title={t('Backup & Restore')}> <PageCard title={t('Backup & Restore')}>
<div style={{ display: 'flex', gap: '16px' }}> <div style={{ display: 'flex', gap: '16px' }}>
<PageCard title={t('Export')} style={{ flex: 1 }}> <Card title={t('Export')} style={{ flex: 1 }}>
<Paragraph> <Paragraph>
{t('Export all data (adapters, users, tasks, shares) into a JSON file.')} {t('Export all data (adapters, users, tasks, shares) into a JSON file.')}
<Text strong>{t('Keep your backup file safe.')}</Text> <Text strong>{t('Keep your backup file safe.')}</Text>
@@ -67,8 +67,8 @@ const BackupPage = memo(function BackupPage() {
> >
{t('Export Backup')} {t('Export Backup')}
</Button> </Button>
</PageCard> </Card>
<PageCard title={t('Import')} style={{ flex: 1 }}> <Card title={t('Import')} style={{ flex: 1 }}>
<Paragraph> <Paragraph>
{t('Restore data from a previously exported JSON file.')} {t('Restore data from a previously exported JSON file.')}
<Text strong type="danger">{t('Warning: This will clear and overwrite existing data.')}</Text> <Text strong type="danger">{t('Warning: This will clear and overwrite existing data.')}</Text>
@@ -81,7 +81,7 @@ const BackupPage = memo(function BackupPage() {
{t('Choose File and Restore')} {t('Choose File and Restore')}
</Button> </Button>
</Upload> </Upload>
</PageCard> </Card>
</div> </div>
</PageCard> </PageCard>
); );