@xec-sh/core
Version:
Universal shell execution engine
385 lines • 15.1 kB
JavaScript
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