diff --git a/packages/plugins/@nocobase/plugin-backups/src/server/__tests__/managers/backup.test.ts b/packages/plugins/@nocobase/plugin-backups/src/server/__tests__/managers/backup.test.ts index 1400570968a..725b2f01ac7 100644 --- a/packages/plugins/@nocobase/plugin-backups/src/server/__tests__/managers/backup.test.ts +++ b/packages/plugins/@nocobase/plugin-backups/src/server/__tests__/managers/backup.test.ts @@ -12,7 +12,7 @@ import { BackupManager, BackupSettings } from '../../managers/backup'; import { MockServer } from '@nocobase/test'; import path from 'path'; import { storagePathJoin } from '@nocobase/utils'; -import { BACKUP_EXTENSION, METADATA_EXTENSION } from '../../utils'; +import { BACKUP_EXTENSION, METADATA_EXTENSION, SETTINGS } from '../../utils'; import fs from 'fs'; import * as cp from 'child_process'; import PluginFileManagerServer from '@nocobase/plugin-file-manager'; @@ -126,6 +126,55 @@ describe('BackupManager', async () => { expect(files).toContain(`${backupFileBaseName}.nbdata`); }); + it('should upload using configured cloud storage when backup options omit storageId', async () => { + const uploadFile = vi.fn().mockResolvedValue({ filename: 'cloud-backups/backup_for_unit_tests.nbdata' }); + const storageRepository = { + findOne: vi.fn().mockResolvedValue({ + id: 1, + title: 'Aliyun OSS', + type: 'ali-oss', + name: 'aliyun-oss', + baseUrl: '', + options: {}, + }), + }; + const getRepository = app.db.getRepository.bind(app.db); + const getRepositorySpy = vi.spyOn(app.db, 'getRepository').mockImplementation((name: string) => { + if (name === 'storages') { + return storageRepository as ReturnType; + } + return getRepository(name); + }); + const pmGetSpy = vi.spyOn(app.pm, 'get').mockReturnValue({ + uploadFile, + } as unknown as PluginFileManagerServer); + const settingsRepository = app.db.getRepository(SETTINGS); + const settings = await settingsRepository.findOne(); + await settingsRepository.update({ + values: { storageId: 1 }, + filterByTk: settings.get('id'), + }); + const backupSettings = await settingsRepository.findOne(); + const backupManager = new BackupManager(app, null, backupSettings); + + try { + await backupManager.backup(backupFileBaseName, { + description: 'Manual backup from settings page', + }); + + expect(storageRepository.findOne).toHaveBeenCalledWith({ + filterByTk: 1, + }); + expect(uploadFile).toHaveBeenCalledWith({ + filePath: finalBackupFilePath, + storageName: 'aliyun-oss', + }); + } finally { + getRepositorySpy.mockRestore(); + pmGetSpy.mockRestore(); + } + }); + it('should honor enableFilesBackup from backup options', async () => { const backupManager = new BackupManager(app, null, defaultBackupSettings); const fileCollection = { diff --git a/packages/plugins/@nocobase/plugin-backups/src/server/managers/backup.ts b/packages/plugins/@nocobase/plugin-backups/src/server/managers/backup.ts index 1518fcd5544..bf446844355 100644 --- a/packages/plugins/@nocobase/plugin-backups/src/server/managers/backup.ts +++ b/packages/plugins/@nocobase/plugin-backups/src/server/managers/backup.ts @@ -36,7 +36,7 @@ import { const BACKUP_METADATA_VERSION = 2; export interface BackupSettings { - storageId?: string; + storageId?: string | number | null; encryptionPassword: string; enableFilesBackup: boolean; keep?: number; @@ -71,6 +71,10 @@ export interface BackupTaskResult { inProgress: boolean; } +type BackupSettingsInput = BackupSettings & { + toJSON?: () => Partial; +}; + export class BackupManager { app: Application; ctx: ResourcerContext | null; // when triggered by cron job, ctx is null @@ -83,10 +87,10 @@ export class BackupManager { #uploadDir: string; #aesKeyPath: string; - constructor(app: Application, ctx: ResourcerContext | null, settings: BackupSettings) { + constructor(app: Application, ctx: ResourcerContext | null, settings: BackupSettingsInput) { this.app = app; this.ctx = ctx; - this.#settings = settings; + this.#settings = this.#normalizeBackupSettings(settings); this.#dbAdapter = getDBAdapter(app.db.options); this.#backupTasksCacheName = BACKUP_TASKS_CACHE_NAME; this.#backupPrefix = 'backup_'; @@ -116,6 +120,14 @@ export class BackupManager { this.#backupTasksCacheName = backupTasksCacheName; } + #normalizeBackupSettings(settings: BackupSettingsInput): BackupSettings { + if (typeof settings.toJSON === 'function') { + return settings.toJSON() as BackupSettings; + } + + return { ...settings }; + } + async createBackupName() { await this.#dbAdapter.check('backup'); await fsPromises.mkdir(this.#backupDir, { recursive: true }); @@ -454,7 +466,7 @@ export class BackupManager { return output; } - async #uploadFiles(filePath: string, storageId?: string) { + async #uploadFiles(filePath: string, storageId?: BackupSettings['storageId']) { if (!storageId) { return; }