Add WebDAV backup sync and armv7 Docker builds
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import Fastify, { type FastifyInstance } from 'fastify';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mkdtempSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
type DbModule = typeof import('../../db/index.js');
|
||||
|
||||
describe('settings backup webdav api', () => {
|
||||
let app: FastifyInstance;
|
||||
let db: DbModule['db'];
|
||||
let schema: DbModule['schema'];
|
||||
let dataDir = '';
|
||||
|
||||
beforeAll(async () => {
|
||||
dataDir = mkdtempSync(join(tmpdir(), 'metapi-settings-backup-webdav-'));
|
||||
process.env.DATA_DIR = dataDir;
|
||||
await import('../../db/migrate.js');
|
||||
const dbModule = await import('../../db/index.js');
|
||||
const settingsRoutesModule = await import('./settings.js');
|
||||
|
||||
db = dbModule.db;
|
||||
schema = dbModule.schema;
|
||||
|
||||
app = Fastify();
|
||||
await app.register(settingsRoutesModule.settingsRoutes);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await db.delete(schema.settings).run();
|
||||
await db.delete(schema.events).run();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
delete process.env.DATA_DIR;
|
||||
});
|
||||
|
||||
it('saves and returns masked webdav config', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/backup/webdav',
|
||||
payload: {
|
||||
enabled: true,
|
||||
fileUrl: 'https://dav.example.com/backups/metapi.json',
|
||||
username: 'alice',
|
||||
password: 'secret-pass',
|
||||
exportType: 'accounts',
|
||||
autoSyncEnabled: true,
|
||||
autoSyncCron: '0 */6 * * *',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = response.json() as {
|
||||
success?: boolean;
|
||||
config?: {
|
||||
fileUrl?: string;
|
||||
username?: string;
|
||||
exportType?: string;
|
||||
autoSyncEnabled?: boolean;
|
||||
autoSyncCron?: string;
|
||||
hasPassword?: boolean;
|
||||
passwordMasked?: string;
|
||||
};
|
||||
};
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.config).toMatchObject({
|
||||
fileUrl: 'https://dav.example.com/backups/metapi.json',
|
||||
username: 'alice',
|
||||
exportType: 'accounts',
|
||||
autoSyncEnabled: true,
|
||||
autoSyncCron: '0 */6 * * *',
|
||||
hasPassword: true,
|
||||
});
|
||||
expect(body.config?.passwordMasked).toBeTruthy();
|
||||
|
||||
const saved = await db.select().from(schema.settings).where(eq(schema.settings.key, 'backup_webdav_config_v1')).get();
|
||||
expect(saved?.value).toContain('"fileUrl":"https://dav.example.com/backups/metapi.json"');
|
||||
expect(saved?.value).toContain('"password":"secret-pass"');
|
||||
});
|
||||
|
||||
it('exports current backup to webdav through settings route', async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
||||
fetchSpy.mockResolvedValue(new Response(null, { status: 201 }));
|
||||
|
||||
await db.insert(schema.settings).values({
|
||||
key: 'backup_webdav_config_v1',
|
||||
value: JSON.stringify({
|
||||
enabled: true,
|
||||
fileUrl: 'https://dav.example.com/backups/metapi.json',
|
||||
username: 'alice',
|
||||
password: 'secret-pass',
|
||||
exportType: 'all',
|
||||
autoSyncEnabled: false,
|
||||
autoSyncCron: '0 * * * *',
|
||||
}),
|
||||
}).run();
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/settings/backup/webdav/export',
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = response.json() as { success?: boolean; fileUrl?: string };
|
||||
expect(body.success).toBe(true);
|
||||
expect(body.fileUrl).toBe('https://dav.example.com/backups/metapi.json');
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,16 @@ import { upsertSetting } from '../../db/upsertSetting.js';
|
||||
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
|
||||
import { updateBalanceRefreshCron, updateCheckinSchedule, updateLogCleanupSettings } from '../../services/checkinScheduler.js';
|
||||
import { sendNotification } from '../../services/notifyService.js';
|
||||
import { exportBackup, importBackup, type BackupExportType } from '../../services/backupService.js';
|
||||
import {
|
||||
exportBackup,
|
||||
exportBackupToWebdav,
|
||||
getBackupWebdavConfig,
|
||||
importBackup,
|
||||
importBackupFromWebdav,
|
||||
reloadBackupWebdavScheduler,
|
||||
saveBackupWebdavConfig,
|
||||
type BackupExportType,
|
||||
} from '../../services/backupService.js';
|
||||
import { startBackgroundTask } from '../../services/backgroundTaskService.js';
|
||||
import {
|
||||
maskConnectionString,
|
||||
@@ -75,6 +84,17 @@ interface SystemProxyTestBody {
|
||||
proxyUrl?: unknown;
|
||||
}
|
||||
|
||||
interface BackupWebdavConfigBody {
|
||||
enabled?: unknown;
|
||||
fileUrl?: unknown;
|
||||
username?: unknown;
|
||||
password?: unknown;
|
||||
clearPassword?: unknown;
|
||||
exportType?: unknown;
|
||||
autoSyncEnabled?: unknown;
|
||||
autoSyncCron?: unknown;
|
||||
}
|
||||
|
||||
type RuntimeDatabaseConfig = {
|
||||
dialect: MigrationDialect;
|
||||
connectionString: string;
|
||||
@@ -1267,6 +1287,9 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
for (const item of result.appliedSettings) {
|
||||
applyImportedSettingToRuntime(item.key, item.value);
|
||||
}
|
||||
if (result.appliedSettings.some((item) => item.key === 'backup_webdav_config_v1')) {
|
||||
await reloadBackupWebdavScheduler();
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: '导入完成',
|
||||
@@ -1280,6 +1303,65 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/settings/backup/webdav', async () => {
|
||||
return getBackupWebdavConfig();
|
||||
});
|
||||
|
||||
app.put<{ Body: BackupWebdavConfigBody }>('/api/settings/backup/webdav', async (request, reply) => {
|
||||
try {
|
||||
const body = request.body || {};
|
||||
const result = await saveBackupWebdavConfig({
|
||||
enabled: body.enabled === undefined ? undefined : body.enabled === true,
|
||||
fileUrl: body.fileUrl === undefined ? undefined : String(body.fileUrl || ''),
|
||||
username: body.username === undefined ? undefined : String(body.username || ''),
|
||||
password: body.password === undefined ? undefined : String(body.password),
|
||||
clearPassword: body.clearPassword === true,
|
||||
exportType: body.exportType === undefined ? undefined : String(body.exportType || '') as BackupExportType,
|
||||
autoSyncEnabled: body.autoSyncEnabled === undefined ? undefined : body.autoSyncEnabled === true,
|
||||
autoSyncCron: body.autoSyncCron === undefined ? undefined : String(body.autoSyncCron || ''),
|
||||
});
|
||||
return result;
|
||||
} catch (err: any) {
|
||||
return reply.code(400).send({
|
||||
success: false,
|
||||
message: err?.message || 'WebDAV 配置保存失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post<{ Body: { type?: string } }>('/api/settings/backup/webdav/export', async (request, reply) => {
|
||||
try {
|
||||
const rawType = typeof request.body?.type === 'string' ? request.body.type.trim().toLowerCase() : '';
|
||||
const type: BackupExportType | undefined = rawType === 'all' || rawType === 'accounts' || rawType === 'preferences'
|
||||
? rawType
|
||||
: undefined;
|
||||
return await exportBackupToWebdav(type);
|
||||
} catch (err: any) {
|
||||
return reply.code(400).send({
|
||||
success: false,
|
||||
message: err?.message || 'WebDAV 导出失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/settings/backup/webdav/import', async (_, reply) => {
|
||||
try {
|
||||
const result = await importBackupFromWebdav();
|
||||
for (const item of result.appliedSettings) {
|
||||
applyImportedSettingToRuntime(item.key, item.value);
|
||||
}
|
||||
if (result.appliedSettings.some((item) => item.key === 'backup_webdav_config_v1')) {
|
||||
await reloadBackupWebdavScheduler();
|
||||
}
|
||||
return result;
|
||||
} catch (err: any) {
|
||||
return reply.code(400).send({
|
||||
success: false,
|
||||
message: err?.message || 'WebDAV 导入失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/settings/notify/test', async (_, reply) => {
|
||||
try {
|
||||
const result = await sendNotification(
|
||||
|
||||
Reference in New Issue
Block a user