Skip to content

Commit 3f93bec

Browse files
committed
fix(backup): Redis BGSAVE detection, safe metadata write, bind mount restore
Redis: capture BGSAVE output and fail explicitly if it does not return the expected success message, so a misconfigured Redis surfaces as an error instead of silently producing a stale or empty dump. Metadata: replace heredoc with sshExecChannel stdin write to avoid DOCKFLOW_EOF delimiter collision when JSON content spans multiple lines. Bind mount restore: replace glob rm with find -mindepth 1 -delete so permission errors are not silenced and tar only runs after a clean clear.
1 parent 5fb4096 commit 3f93bec

1 file changed

Lines changed: 9 additions & 11 deletions

File tree

cli/src/services/backup.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import type { SSHKeyConnection } from '../types';
1414
import type { BackupDbType, BackupAccessoryConfig } from '../utils/config';
1515
import { ok, err, type Result } from '../types';
16-
import { sshExec, shellEscape } from '../utils/ssh';
16+
import { sshExec, sshExecChannel, shellEscape } from '../utils/ssh';
1717
import { formatBytes, printDebug } from '../utils/output';
1818
import { findSwarmContainer } from './orchestrator/swarm/swarm-utils';
1919
import { SwarmStackBackend } from './orchestrator/swarm/swarm-stack';
@@ -167,7 +167,7 @@ const DB_STRATEGIES: Record<Exclude<BackupDbType, 'raw' | 'volume'>, DbStrategy>
167167
buildDumpCommand() {
168168
// Trigger BGSAVE and wait for it to complete by polling LASTSAVE.
169169
// The BGSAVE/LASTSAVE output is redirected to /dev/null — only the raw RDB bytes are emitted.
170-
return 'BEFORE=$(redis-cli LASTSAVE) && redis-cli BGSAVE > /dev/null && for i in $(seq 1 30); do AFTER=$(redis-cli LASTSAVE); [ "$AFTER" != "$BEFORE" ] && break; sleep 1; done && cat /data/dump.rdb';
170+
return 'BEFORE=$(redis-cli LASTSAVE) && OUT=$(redis-cli BGSAVE) && echo "$OUT" | grep -q "Background saving started" || { echo "BGSAVE failed: $OUT" >&2; exit 1; } && for i in $(seq 1 30); do AFTER=$(redis-cli LASTSAVE); [ "$AFTER" != "$BEFORE" ] && break; sleep 1; done && cat /data/dump.rdb';
171171
},
172172
buildRestoreCommand() {
173173
// Write the RDB file, then SHUTDOWN NOSAVE so Redis doesn't overwrite it
@@ -392,10 +392,9 @@ export class Backup {
392392
nodePort: nodeConn.port,
393393
};
394394

395-
await sshExec(
396-
nodeConn,
397-
`cat > '${shellEscape(metaPath)}' << 'DOCKFLOW_EOF'\n${JSON.stringify(metadata, null, 2)}\nDOCKFLOW_EOF`
398-
);
395+
const { stream, done } = await sshExecChannel(nodeConn, `cat > '${shellEscape(metaPath)}'`);
396+
stream.end(JSON.stringify(metadata, null, 2));
397+
await done;
399398

400399
return ok(metadata);
401400
}
@@ -554,10 +553,9 @@ export class Backup {
554553
// Extended metadata includes per-volume/bind details
555554
const extendedMeta = { ...metadata, volumes: volumeEntries };
556555
const metaPath = `${backupDir}/${backupId}.meta.json`;
557-
await sshExec(
558-
nodeConn,
559-
`cat > '${shellEscape(metaPath)}' << 'DOCKFLOW_EOF'\n${JSON.stringify(extendedMeta, null, 2)}\nDOCKFLOW_EOF`
560-
);
556+
const { stream, done } = await sshExecChannel(nodeConn, `cat > '${shellEscape(metaPath)}'`);
557+
stream.end(JSON.stringify(extendedMeta, null, 2));
558+
await done;
561559

562560
return ok(metadata);
563561
}
@@ -607,7 +605,7 @@ export class Backup {
607605
if (mountType === 'bind' && entry.sourcePath) {
608606
// Bind mount: clear target and extract directly on the host
609607
const src = shellEscape(entry.sourcePath);
610-
restoreCmd = `rm -rf '${src}'/* '${src}'/..?* '${src}'/.[!.]* 2>/dev/null; tar xf - -C '${src}'`;
608+
restoreCmd = `find '${src}' -mindepth 1 -delete && tar xf - -C '${src}'`;
611609
} else {
612610
// Named volume: extract via temporary alpine container
613611
const fullVolumeName = existingVolumes.find(

0 commit comments

Comments
 (0)