From 90ca53356df17f53b379d09511ee5eba5432905c Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 29 Apr 2026 14:11:00 +0000 Subject: [PATCH] fix: restore platform backup data --- ts/classes/backup-manager.ts | 108 ++++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 33 deletions(-) diff --git a/ts/classes/backup-manager.ts b/ts/classes/backup-manager.ts index 02ace14..eaae61b 100644 --- a/ts/classes/backup-manager.ts +++ b/ts/classes/backup-manager.ts @@ -206,14 +206,12 @@ export class BackupManager { for (const resourceType of resourceTypes) { const dataDir = `${tempDir}/data/${resourceType}`; try { - for await (const entry of Deno.readDir(dataDir)) { - if (entry.isFile) { - items.push({ - stream: plugins.nodeFs.createReadStream(`${dataDir}/${entry.name}`), - name: `data/${resourceType}/${entry.name}`, - type: 'data', - }); - } + for await (const filePath of this.walkFiles(dataDir)) { + items.push({ + stream: plugins.nodeFs.createReadStream(filePath), + name: plugins.path.relative(tempDir, filePath).replaceAll('\\', '/'), + type: 'data', + }); } } catch { // Directory may not exist if export produced no files @@ -819,7 +817,7 @@ export class BackupManager { throw new Error('MongoDB service not running'); } - const connectionUri = credentials.connectionUri || credentials.MONGODB_URI; + const connectionUri = credentials.connectionUri || credentials.connectionString || credentials.MONGODB_URI; if (!connectionUri) { throw new Error('MongoDB connection URI not found in credentials'); } @@ -836,19 +834,8 @@ export class BackupManager { throw new Error(`mongodump failed: ${result.stderr}`); } - const container = await this.oneboxRef.docker.getContainerById(mongoService.containerId); - if (!container) { - throw new Error('MongoDB container not found'); - } - - const copyResult = await this.oneboxRef.docker.execInContainer(mongoService.containerId, [ - 'cat', - archivePath, - ]); - const localPath = `${dataDir}/${resource.resourceName}.archive`; - const encoder = new TextEncoder(); - await Deno.writeFile(localPath, encoder.encode(copyResult.stdout)); + await this.copyFromContainer(mongoService.containerId, archivePath, localPath); await this.oneboxRef.docker.execInContainer(mongoService.containerId, ['rm', archivePath]); @@ -868,7 +855,7 @@ export class BackupManager { const bucketDir = `${dataDir}/${resource.resourceName}`; await Deno.mkdir(bucketDir, { recursive: true }); - const s3Info = this.getS3ConnectionInfo(credentials); + const s3Info = await this.getReachableS3ConnectionInfo(credentials, resource.platformServiceId); const s3Client = this.createS3Client(s3Info); let objectCount = 0; let continuationToken: string | undefined; @@ -1137,6 +1124,36 @@ export class BackupManager { return imageName; } + private async copyFromContainer( + containerId: string, + containerPath: string, + outputPath: string, + ): Promise { + await this.runDockerCp([`${containerId}:${containerPath}`, outputPath], 'docker cp from container failed'); + } + + private async copyToContainer( + inputPath: string, + containerId: string, + containerPath: string, + ): Promise { + await this.runDockerCp([inputPath, `${containerId}:${containerPath}`], 'docker cp to container failed'); + } + + private async runDockerCp(args: string[], errorMessage: string): Promise { + const command = new Deno.Command('docker', { + args: ['cp', ...args], + stdout: 'piped', + stderr: 'piped', + }); + const result = await command.output(); + + if (!result.success) { + const stderr = new TextDecoder().decode(result.stderr).trim(); + throw new Error(`${errorMessage}: ${stderr}`); + } + } + /** * Restore platform resources for a service */ @@ -1241,22 +1258,14 @@ export class BackupManager { } const archivePath = `${dataDir}/${backupResourceName}.archive`; - const connectionUri = credentials.connectionUri || credentials.MONGODB_URI; + const connectionUri = credentials.connectionUri || credentials.connectionString || credentials.MONGODB_URI; if (!connectionUri) { throw new Error('MongoDB connection URI not found'); } - const archiveData = await Deno.readFile(archivePath); const containerArchivePath = `/tmp/${resource.resourceName}.archive`; - - const base64Data = btoa(String.fromCharCode(...archiveData)); - - await this.oneboxRef.docker.execInContainer(mongoService.containerId, [ - 'bash', - '-c', - `echo '${base64Data}' | base64 -d > ${containerArchivePath}`, - ]); + await this.copyToContainer(archivePath, mongoService.containerId, containerArchivePath); const result = await this.oneboxRef.docker.execInContainer(mongoService.containerId, [ 'mongorestore', @@ -1288,7 +1297,7 @@ export class BackupManager { const bucketDir = `${dataDir}/${backupResourceName}`; - const s3Info = this.getS3ConnectionInfo(credentials); + const s3Info = await this.getReachableS3ConnectionInfo(credentials, resource.platformServiceId); const s3Client = this.createS3Client(s3Info); let uploadedCount = 0; @@ -1619,6 +1628,39 @@ export class BackupManager { }; } + private async getReachableS3ConnectionInfo( + credentials: Record, + platformServiceId: number, + ): Promise { + const s3Info = this.getS3ConnectionInfo(credentials); + + let endpointUrl: URL; + try { + endpointUrl = new URL(s3Info.endpoint); + } catch { + return s3Info; + } + + if (endpointUrl.hostname !== 'onebox-minio') { + return s3Info; + } + + const platformService = this.oneboxRef.database.getPlatformServiceById(platformServiceId); + const hostPort = platformService?.containerId + ? await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 9000) + : null; + if (!hostPort) { + return s3Info; + } + + endpointUrl.hostname = '127.0.0.1'; + endpointUrl.port = String(hostPort); + return { + ...s3Info, + endpoint: endpointUrl.toString().replace(/\/$/, ''), + }; + } + private createS3Client(s3Info: IS3ConnectionInfo) { return new plugins.awsS3.S3Client({ endpoint: s3Info.endpoint,