UNPKG

@xec-sh/core

Version:

Universal shell execution engine

385 lines 15.1 kB
import { stat } from 'node:fs/promises'; import { join, dirname, relative, isAbsolute } from 'node:path'; import { escapeArg } from './shell-escape.js'; export class TransferEngine { constructor(engine) { this.engine = engine; } emitEvent(event, data) { if ('emit' in this.engine && typeof this.engine.emit === 'function') { this.engine.emit(event, { ...data, timestamp: new Date(), adapter: 'local' }); } } async copy(source, dest, options = {}) { const startTime = Date.now(); const sourceEnv = this.parseEnvironment(source); const destEnv = this.parseEnvironment(dest); this.emitEvent('transfer:start', { source: sourceEnv.raw, destination: destEnv.raw, direction: sourceEnv.type === 'local' ? 'upload' : 'download' }); try { const result = await this.executeTransfer(sourceEnv, destEnv, 'copy', options); const finalResult = { ...result, success: true, duration: Date.now() - startTime }; this.emitEvent('transfer:complete', { source: sourceEnv.raw, destination: destEnv.raw, direction: sourceEnv.type === 'local' ? 'upload' : 'download', bytesTransferred: finalResult.bytesTransferred, duration: finalResult.duration }); return finalResult; } catch (error) { const duration = Date.now() - startTime; this.emitEvent('transfer:error', { source: sourceEnv.raw, destination: destEnv.raw, direction: sourceEnv.type === 'local' ? 'upload' : 'download', error: error instanceof Error ? error.message : String(error) }); return { success: false, filesTransferred: 0, bytesTransferred: 0, errors: [error], duration }; } } async move(source, dest, options = {}) { const startTime = Date.now(); const sourceEnv = this.parseEnvironment(source); const destEnv = this.parseEnvironment(dest); this.emitEvent('transfer:start', { source: sourceEnv.raw, destination: destEnv.raw, direction: sourceEnv.type === 'local' ? 'upload' : 'download' }); try { const result = await this.executeTransfer(sourceEnv, destEnv, 'move', options); const finalResult = { ...result, success: true, duration: Date.now() - startTime }; this.emitEvent('transfer:complete', { source: sourceEnv.raw, destination: destEnv.raw, direction: sourceEnv.type === 'local' ? 'upload' : 'download', bytesTransferred: finalResult.bytesTransferred, duration: finalResult.duration }); return finalResult; } catch (error) { const duration = Date.now() - startTime; this.emitEvent('transfer:error', { source: sourceEnv.raw, destination: destEnv.raw, direction: sourceEnv.type === 'local' ? 'upload' : 'download', error: error instanceof Error ? error.message : String(error) }); return { success: false, filesTransferred: 0, bytesTransferred: 0, errors: [error], duration }; } } async sync(source, dest, options = {}) { return this.copy(source, dest, { ...options, deleteExtra: true }); } parseEnvironment(path) { const sshMatch = path.match(/^ssh:\/\/(?:([^@]+)@)?([^/]+)(.*)$/); if (sshMatch) { return { type: 'ssh', user: sshMatch[1], host: sshMatch[2], path: sshMatch[3] || '/', raw: path }; } const dockerMatch = path.match(/^docker:\/\/([^:]+):(.*)$/); if (dockerMatch) { return { type: 'docker', container: dockerMatch[1], path: dockerMatch[2] || '/', raw: path }; } return { type: 'local', path: isAbsolute(path) ? path : join(process.cwd(), path), raw: path }; } async executeTransfer(source, dest, operation, options) { const key = `${source.type}-${dest.type}`; switch (key) { case 'local-local': return this.localToLocal(source, dest, operation, options); case 'local-ssh': return this.localToSsh(source, dest, operation, options); case 'local-docker': return this.localToDocker(source, dest, operation, options); case 'ssh-local': return this.sshToLocal(source, dest, operation, options); case 'ssh-ssh': return this.sshToSsh(source, dest, operation, options); case 'ssh-docker': return this.sshToDocker(source, dest, operation, options); case 'docker-local': return this.dockerToLocal(source, dest, operation, options); case 'docker-ssh': return this.dockerToSsh(source, dest, operation, options); case 'docker-docker': return this.dockerToDocker(source, dest, operation, options); default: throw new Error(`Unsupported transfer: ${source.type} to ${dest.type}`); } } async localToLocal(source, dest, operation, options) { const sourcePath = escapeArg(source.path); const destPath = escapeArg(dest.path); let command; if (operation === 'copy') { const flags = this.buildCpFlags(options); command = `cp ${flags} ${sourcePath} ${destPath}`; } else { command = `mv ${options.overwrite ? '-f' : '-n'} ${sourcePath} ${destPath}`; } await this.engine.execute({ command, shell: true }); const stats = await this.getTransferStats(source.path, options); return stats; } async localToSsh(source, dest, operation, options) { const $ssh = this.engine.ssh({ host: dest.host, username: dest.user || 'root' }); if (options.recursive) { await $ssh.uploadDirectory(source.path, dest.path); } else { await $ssh.uploadFile(source.path, dest.path); } if (operation === 'move') { await this.engine.execute({ command: `rm -rf ${escapeArg(source.path)}`, shell: true }); } const stats = await this.getTransferStats(source.path, options); return stats; } async localToDocker(source, dest, operation, options) { const sourcePath = escapeArg(source.path); const containerPath = `${dest.container}:${dest.path}`; const command = `docker cp ${sourcePath} ${containerPath}`; await this.engine.execute({ command, shell: true }); if (operation === 'move') { await this.engine.execute({ command: `rm -rf ${sourcePath}`, shell: true }); } const stats = await this.getTransferStats(source.path, options); return stats; } async sshToLocal(source, dest, operation, options) { const $ssh = this.engine.ssh({ host: source.host, username: source.user || 'root' }); if (options.recursive) { const remotePath = source.path; const localPath = dest.path; await this.engine.execute({ command: `mkdir -p ${escapeArg(localPath)}`, shell: true }); await $ssh `tar -cf - -C ${dirname(remotePath)} ${relative(dirname(remotePath), remotePath)} | tar -xf - -C ${localPath}`; } else { await $ssh.downloadFile(source.path, dest.path); } if (operation === 'move') { await $ssh `rm -rf ${source.path}`; } const stats = await this.getTransferStats(dest.path, options); return stats; } async sshToSsh(source, dest, operation, options) { if (source.host === dest.host) { const $ssh = this.engine.ssh({ host: source.host, username: source.user || 'root' }); const command = operation === 'copy' ? `cp ${this.buildCpFlags(options)} ${escapeArg(source.path)} ${escapeArg(dest.path)}` : `mv ${options.overwrite ? '-f' : '-n'} ${escapeArg(source.path)} ${escapeArg(dest.path)}`; await $ssh `${command}`; } else { const tempPath = `/tmp/xec-transfer-${Date.now()}`; await this.sshToLocal(source, { type: 'local', path: tempPath, raw: tempPath }, 'copy', options); await this.localToSsh({ type: 'local', path: tempPath, raw: tempPath }, dest, 'copy', options); await this.engine.execute({ command: `rm -rf ${escapeArg(tempPath)}`, shell: true }); if (operation === 'move') { const $sshSource = this.engine.ssh({ host: source.host, username: source.user || 'root' }); await $sshSource `rm -rf ${source.path}`; } } return { filesTransferred: 1, bytesTransferred: 0, errors: [] }; } async sshToDocker(source, dest, operation, options) { const tempPath = `/tmp/xec-transfer-${Date.now()}`; await this.sshToLocal(source, { type: 'local', path: tempPath, raw: tempPath }, 'copy', options); await this.localToDocker({ type: 'local', path: tempPath, raw: tempPath }, dest, 'copy', options); await this.engine.execute({ command: `rm -rf ${escapeArg(tempPath)}`, shell: true }); if (operation === 'move') { const $ssh = this.engine.ssh({ host: source.host, username: source.user || 'root' }); await $ssh `rm -rf ${source.path}`; } return { filesTransferred: 1, bytesTransferred: 0, errors: [] }; } async dockerToLocal(source, dest, operation, options) { const containerPath = `${source.container}:${source.path}`; const destPath = escapeArg(dest.path); const command = `docker cp ${containerPath} ${destPath}`; await this.engine.execute({ command, shell: true }); if (operation === 'move') { await this.engine.execute({ command: `docker exec ${source.container} rm -rf ${escapeArg(source.path)}`, shell: true }); } return { filesTransferred: 1, bytesTransferred: 0, errors: [] }; } async dockerToSsh(source, dest, operation, options) { const tempPath = `/tmp/xec-transfer-${Date.now()}`; await this.dockerToLocal(source, { type: 'local', path: tempPath, raw: tempPath }, 'copy', options); await this.localToSsh({ type: 'local', path: tempPath, raw: tempPath }, dest, 'copy', options); await this.engine.execute({ command: `rm -rf ${escapeArg(tempPath)}`, shell: true }); if (operation === 'move') { await this.engine.execute({ command: `docker exec ${source.container} rm -rf ${escapeArg(source.path)}`, shell: true }); } return { filesTransferred: 1, bytesTransferred: 0, errors: [] }; } async dockerToDocker(source, dest, operation, options) { if (source.container === dest.container) { const command = operation === 'copy' ? `docker exec ${source.container} cp ${this.buildCpFlags(options)} ${escapeArg(source.path)} ${escapeArg(dest.path)}` : `docker exec ${source.container} mv ${options.overwrite ? '-f' : '-n'} ${escapeArg(source.path)} ${escapeArg(dest.path)}`; await this.engine.execute({ command, shell: true }); } else { const tempPath = `/tmp/ush-transfer-${Date.now()}`; await this.dockerToLocal(source, { type: 'local', path: tempPath, raw: tempPath }, 'copy', options); await this.localToDocker({ type: 'local', path: tempPath, raw: tempPath }, dest, 'copy', options); await this.engine.execute({ command: `rm -rf ${escapeArg(tempPath)}`, shell: true }); if (operation === 'move') { await this.engine.execute({ command: `docker exec ${source.container} rm -rf ${escapeArg(source.path)}`, shell: true }); } } return { filesTransferred: 1, bytesTransferred: 0, errors: [] }; } buildCpFlags(options) { const flags = []; if (options.recursive) flags.push('-r'); if (options.preserveMode) flags.push('-p'); if (options.preserveTimestamps) flags.push('-p'); if (!options.followSymlinks) flags.push('-P'); if (options.overwrite === false) flags.push('-n'); return flags.join(' '); } buildScpFlags(options) { const flags = []; if (options.recursive) flags.push('-r'); if (options.preserveMode) flags.push('-p'); if (options.compress) flags.push('-C'); return flags.join(' '); } buildExcludeFlags(options) { const flags = []; if (options.exclude) { for (const pattern of options.exclude) { flags.push(`--exclude=${escapeArg(pattern)}`); } } return flags.join(' '); } async getTransferStats(path, options) { try { const stats = await stat(path); if (stats.isFile()) { return { filesTransferred: 1, bytesTransferred: stats.size, errors: [] }; } else if (stats.isDirectory() && options.recursive) { return { filesTransferred: 1, bytesTransferred: 0, errors: [] }; } } catch { } return { filesTransferred: 1, bytesTransferred: 0, errors: [] }; } } //# sourceMappingURL=transfer.js.map