Add capture() to spawn, add rsync itemize parser
This commit is contained in:
56
lib/itemize.js
Normal file
56
lib/itemize.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Parse rsync --itemize-changes output into a structured change list.
|
||||||
|
*
|
||||||
|
* rsync itemize format: 11-character code + space + path
|
||||||
|
*
|
||||||
|
* Code structure: YXcstpoguax
|
||||||
|
* Y = update type: > (transfer), * (message/delete), c (local change), . (no update), h (hard link)
|
||||||
|
* X = file type: f (file), d (dir), L (symlink), D (device), S (special)
|
||||||
|
* remaining chars = what changed (size, time, perms, etc.) or '+++++++++' for new
|
||||||
|
*
|
||||||
|
* We care about:
|
||||||
|
* >f... = file transferred (new or modified)
|
||||||
|
* *deleting = file deleted
|
||||||
|
* cd... = directory (ignored for delta purposes)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{ status: 'added'|'modified'|'deleted', path: string }} Change
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse rsync --itemize-changes stdout into a list of file changes.
|
||||||
|
* @param {string} output
|
||||||
|
* @returns {Change[]}
|
||||||
|
*/
|
||||||
|
export function parseItemize(output) {
|
||||||
|
const changes = [];
|
||||||
|
|
||||||
|
for (const raw of output.split('\n')) {
|
||||||
|
const line = raw.trimEnd();
|
||||||
|
if (!line) continue;
|
||||||
|
|
||||||
|
// Deleted files: "*deleting path/to/file"
|
||||||
|
if (line.startsWith('*deleting ')) {
|
||||||
|
const path = line.slice('*deleting '.length).trimStart();
|
||||||
|
// Skip directory deletions (trailing slash)
|
||||||
|
if (!path.endsWith('/')) {
|
||||||
|
changes.push({ status: 'deleted', path });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File transfers: ">f......... path" (new or modified)
|
||||||
|
if (line.length > 12 && line[0] === '>' && line[1] === 'f') {
|
||||||
|
const code = line.slice(0, 11);
|
||||||
|
const path = line.slice(12);
|
||||||
|
const isNew = code.slice(2) === '+++++++++';
|
||||||
|
changes.push({ status: isNew ? 'added' : 'modified', path });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything else (dirs, symlinks, attribute-only changes) — ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
33
lib/spawn.js
33
lib/spawn.js
@@ -4,17 +4,14 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spawn a process and stream its output.
|
* Spawn a process and stream its output to stdout/stderr.
|
||||||
* @param {string} cmd
|
* @param {string} cmd
|
||||||
* @param {string[]} args
|
* @param {string[]} args
|
||||||
* @param {{ dryRun?: boolean, label?: string }} opts
|
* @param {{ dryRun?: boolean }} opts
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
export async function run(cmd, args, { dryRun = false, label } = {}) {
|
export async function run(cmd, args, { dryRun = false } = {}) {
|
||||||
const display = [cmd, ...args].join(' ');
|
console.log(`$ ${[cmd, ...args].join(' ')}`);
|
||||||
if (label) console.log(`[${label}] ${display}`);
|
|
||||||
else console.log(`$ ${display}`);
|
|
||||||
|
|
||||||
if (dryRun) return;
|
if (dryRun) return;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -26,3 +23,25 @@ export async function run(cmd, args, { dryRun = false, label } = {}) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn a process and capture stdout as a string.
|
||||||
|
* stderr is inherited (shown to user). Never used in dry-run context.
|
||||||
|
* @param {string} cmd
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
export async function capture(cmd, args) {
|
||||||
|
console.log(`$ ${[cmd, ...args].join(' ')}`);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(cmd, args, { stdio: ['inherit', 'pipe', 'inherit'] });
|
||||||
|
const chunks = [];
|
||||||
|
child.stdout.on('data', chunk => chunks.push(chunk));
|
||||||
|
child.on('error', reject);
|
||||||
|
child.on('close', code => {
|
||||||
|
if (code === 0) resolve(Buffer.concat(chunks).toString('utf8'));
|
||||||
|
else reject(new Error(`${cmd} exited with code ${code}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user