fix(minio): support mc_path and harden sync against transient objects
This commit is contained in:
@@ -18,4 +18,16 @@ describe('backup internals', () => {
|
||||
expect(backupInternals.buildObjectKey('/flynn/daily/', 'a.tar.gz')).toBe('flynn/daily/a.tar.gz');
|
||||
expect(backupInternals.buildObjectKey('', 'a.tar.gz')).toBe('a.tar.gz');
|
||||
});
|
||||
|
||||
it('resolves custom mc path with fallback', () => {
|
||||
expect(backupInternals.resolveMcPath('/usr/local/bin/mc')).toBe('/usr/local/bin/mc');
|
||||
expect(backupInternals.resolveMcPath('')).toBe('mc');
|
||||
expect(backupInternals.resolveMcPath(undefined)).toBe('mc');
|
||||
});
|
||||
|
||||
it('formats missing mc binary errors with setup hint', () => {
|
||||
const error = new Error('spawn mc ENOENT') as Error & { code?: string };
|
||||
error.code = 'ENOENT';
|
||||
expect(backupInternals.formatMinioCliError(error, '/custom/mc')).toContain('MinIO client binary not found: /custom/mc');
|
||||
});
|
||||
});
|
||||
|
||||
+27
-4
@@ -61,6 +61,21 @@ function buildObjectKey(prefix: string, fileName: string): string {
|
||||
return trimmed.length > 0 ? `${trimmed}/${fileName}` : fileName;
|
||||
}
|
||||
|
||||
function resolveMcPath(pathValue: string | undefined): string {
|
||||
const trimmed = pathValue?.trim();
|
||||
return trimmed && trimmed.length > 0 ? trimmed : 'mc';
|
||||
}
|
||||
|
||||
function formatMinioCliError(error: unknown, mcPath: string): string {
|
||||
if (error && typeof error === 'object' && 'code' in error && (error as { code?: unknown }).code === 'ENOENT') {
|
||||
return `MinIO client binary not found: ${mcPath}. Install MinIO Client (mc) or set \`backup.minio.mc_path\` to the full binary path.`;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function collectExistingEntries(opts: {
|
||||
dataDir: string;
|
||||
backupConfig: BackupConfig;
|
||||
@@ -137,12 +152,18 @@ export async function runBackupSnapshot(opts: BackupRunOptions): Promise<BackupR
|
||||
...process.env,
|
||||
[`MC_HOST_${alias}`]: host,
|
||||
};
|
||||
const mcPath = resolveMcPath(opts.backupConfig.minio.mc_path);
|
||||
|
||||
await execFileAsync('mc', ['mb', '--ignore-existing', `${alias}/${bucket}`], { env });
|
||||
let remotePath = '';
|
||||
try {
|
||||
await execFileAsync(mcPath, ['mb', '--ignore-existing', `${alias}/${bucket}`], { env });
|
||||
|
||||
const objectKey = buildObjectKey(opts.backupConfig.minio.prefix, fileName);
|
||||
const remotePath = `${alias}/${bucket}/${objectKey}`;
|
||||
await execFileAsync('mc', ['cp', archivePath, remotePath], { env });
|
||||
const objectKey = buildObjectKey(opts.backupConfig.minio.prefix, fileName);
|
||||
remotePath = `${alias}/${bucket}/${objectKey}`;
|
||||
await execFileAsync(mcPath, ['cp', archivePath, remotePath], { env });
|
||||
} catch (error) {
|
||||
throw new Error(formatMinioCliError(error, mcPath));
|
||||
}
|
||||
|
||||
auditLogger?.systemConfig('backup', 'upload', {
|
||||
archive_path: archivePath,
|
||||
@@ -164,5 +185,7 @@ export const backupInternals = {
|
||||
buildObjectKey,
|
||||
collectExistingEntries,
|
||||
expandHomePath,
|
||||
formatMinioCliError,
|
||||
fileTimestamp,
|
||||
resolveMcPath,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user