fix: proper progress and copying
This commit is contained in:
@@ -219,7 +219,9 @@ export class PublishEngine extends EventEmitter {
|
|||||||
exclude?: string[],
|
exclude?: string[],
|
||||||
): Promise<DirectoryUploadResult> {
|
): Promise<DirectoryUploadResult> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
onProgress(0, 'Starting rsync...');
|
onProgress(0, `Starting rsync → ${dest}`);
|
||||||
|
let filesTransferred = 0;
|
||||||
|
|
||||||
rsync(
|
rsync(
|
||||||
{
|
{
|
||||||
src,
|
src,
|
||||||
@@ -227,17 +229,31 @@ export class PublishEngine extends EventEmitter {
|
|||||||
ssh: true,
|
ssh: true,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
times: true,
|
times: true,
|
||||||
args: ['--update', '--compress'],
|
args: ['--update', '--compress', '--verbose'],
|
||||||
exclude: exclude || [],
|
exclude: exclude || [],
|
||||||
|
onStdout: (data: string | Buffer) => {
|
||||||
|
const lines = data.toString().split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
if (trimmed.startsWith('sending ')) continue;
|
||||||
|
if (/\bbytes\b/.test(trimmed)) continue;
|
||||||
|
if (/total size is/.test(trimmed)) continue;
|
||||||
|
if (/speedup is/.test(trimmed)) continue;
|
||||||
|
filesTransferred++;
|
||||||
|
onProgress(
|
||||||
|
Math.min(filesTransferred, 99),
|
||||||
|
`${trimmed} → ${dest}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
(error, stdout, _stderr, _cmd) => {
|
},
|
||||||
|
(error, _stdout, _stderr, _cmd) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
} else {
|
} else {
|
||||||
const lines = stdout.trim().split('\n').filter((l: string) => l.length > 0);
|
onProgress(100, `rsync complete: ${filesTransferred} files transferred`);
|
||||||
const count = lines.length;
|
resolve({ filesUploaded: filesTransferred, filesSkipped: 0 });
|
||||||
onProgress(100, `rsync complete: ${count} files transferred`);
|
|
||||||
resolve({ filesUploaded: count, filesSkipped: 0 });
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -348,6 +348,113 @@ describe('PublishEngine', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── rsync live progress via onStdout ──────────────────────────────────
|
||||||
|
|
||||||
|
describe('rsync mode – live progress', () => {
|
||||||
|
const rsyncCredentials: PublishCredentials = { ...defaultCredentials, sshMode: 'rsync' };
|
||||||
|
|
||||||
|
it('should pass --verbose flag so rsync reports each file', async () => {
|
||||||
|
await engine.uploadHtml(rsyncCredentials, vi.fn());
|
||||||
|
const [options] = mockRsync.mock.calls[0];
|
||||||
|
expect(options.args).toContain('--verbose');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should provide onStdout callback to rsync options', async () => {
|
||||||
|
await engine.uploadHtml(rsyncCredentials, vi.fn());
|
||||||
|
const [options] = mockRsync.mock.calls[0];
|
||||||
|
expect(typeof options.onStdout).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report filenames from rsync stdout as progress messages', async () => {
|
||||||
|
mockRsync.mockImplementation((options: any, callback: any) => {
|
||||||
|
if (options.onStdout) {
|
||||||
|
options.onStdout('sending incremental file list\n');
|
||||||
|
options.onStdout('index.html\n');
|
||||||
|
options.onStdout('about.html\n');
|
||||||
|
options.onStdout('css/style.css\n');
|
||||||
|
}
|
||||||
|
callback(null, 'index.html\nabout.html\ncss/style.css\n', '', 'rsync cmd');
|
||||||
|
});
|
||||||
|
|
||||||
|
const onProgress = vi.fn();
|
||||||
|
await engine.uploadHtml(rsyncCredentials, onProgress);
|
||||||
|
|
||||||
|
const messages = onProgress.mock.calls.map(([, m]: [number, string]) => m);
|
||||||
|
expect(messages.some(m => m.includes('index.html'))).toBe(true);
|
||||||
|
expect(messages.some(m => m.includes('about.html'))).toBe(true);
|
||||||
|
expect(messages.some(m => m.includes('css/style.css'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show the remote destination in progress messages', async () => {
|
||||||
|
mockRsync.mockImplementation((options: any, callback: any) => {
|
||||||
|
if (options.onStdout) {
|
||||||
|
options.onStdout('index.html\n');
|
||||||
|
}
|
||||||
|
callback(null, 'index.html\n', '', 'rsync cmd');
|
||||||
|
});
|
||||||
|
|
||||||
|
const onProgress = vi.fn();
|
||||||
|
await engine.uploadHtml(rsyncCredentials, onProgress);
|
||||||
|
|
||||||
|
const messages = onProgress.mock.calls.map(([, m]: [number, string]) => m);
|
||||||
|
expect(messages.some(m => m.includes('example.com'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip non-file lines from rsync output', async () => {
|
||||||
|
mockRsync.mockImplementation((options: any, callback: any) => {
|
||||||
|
if (options.onStdout) {
|
||||||
|
options.onStdout('sending incremental file list\n');
|
||||||
|
options.onStdout('index.html\n');
|
||||||
|
options.onStdout('\n');
|
||||||
|
options.onStdout('sent 1234 bytes received 56 bytes\n');
|
||||||
|
options.onStdout('total size is 9876 speedup is 2.50\n');
|
||||||
|
}
|
||||||
|
callback(null, 'index.html\n', '', 'rsync cmd');
|
||||||
|
});
|
||||||
|
|
||||||
|
const onProgress = vi.fn();
|
||||||
|
await engine.uploadHtml(rsyncCredentials, onProgress);
|
||||||
|
|
||||||
|
const messages = onProgress.mock.calls.map(([, m]: [number, string]) => m);
|
||||||
|
expect(messages.some(m => m.includes('sending incremental'))).toBe(false);
|
||||||
|
expect(messages.some(m => m.includes('sent 1234'))).toBe(false);
|
||||||
|
expect(messages.some(m => m.includes('total size'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multi-line chunks from onStdout', async () => {
|
||||||
|
mockRsync.mockImplementation((options: any, callback: any) => {
|
||||||
|
if (options.onStdout) {
|
||||||
|
// rsync may send multiple files in a single stdout chunk
|
||||||
|
options.onStdout('file1.html\nfile2.html\nfile3.html\n');
|
||||||
|
}
|
||||||
|
callback(null, 'file1.html\nfile2.html\nfile3.html\n', '', 'rsync cmd');
|
||||||
|
});
|
||||||
|
|
||||||
|
const onProgress = vi.fn();
|
||||||
|
await engine.uploadHtml(rsyncCredentials, onProgress);
|
||||||
|
|
||||||
|
const messages = onProgress.mock.calls.map(([, m]: [number, string]) => m);
|
||||||
|
expect(messages.some(m => m.includes('file1.html'))).toBe(true);
|
||||||
|
expect(messages.some(m => m.includes('file2.html'))).toBe(true);
|
||||||
|
expect(messages.some(m => m.includes('file3.html'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reach 100% on completion with file count', async () => {
|
||||||
|
mockRsync.mockImplementation((options: any, callback: any) => {
|
||||||
|
if (options.onStdout) {
|
||||||
|
options.onStdout('file1.html\nfile2.html\n');
|
||||||
|
}
|
||||||
|
callback(null, 'file1.html\nfile2.html\n', '', 'rsync cmd');
|
||||||
|
});
|
||||||
|
|
||||||
|
const onProgress = vi.fn();
|
||||||
|
await engine.uploadHtml(rsyncCredentials, onProgress);
|
||||||
|
|
||||||
|
const last = onProgress.mock.calls[onProgress.mock.calls.length - 1];
|
||||||
|
expect(last[0]).toBe(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── per-file progress across methods ──────────────────────────────────
|
// ── per-file progress across methods ──────────────────────────────────
|
||||||
|
|
||||||
describe('per-file progress', () => {
|
describe('per-file progress', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user