fix(minio): support mc_path and harden sync against transient objects

This commit is contained in:
William Valentin
2026-02-19 13:18:20 -08:00
parent b5d691a99f
commit d4f4be068c
12 changed files with 238 additions and 21 deletions
+12
View File
@@ -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
View File
@@ -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,
};