Release ForgeFlow 0.6.0
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
const fs = require('node:fs/promises');
|
||||
const crypto = require('node:crypto');
|
||||
const path = require('node:path').posix;
|
||||
|
||||
function loadSshClient() {
|
||||
try { return require('ssh2').Client; }
|
||||
@@ -120,6 +121,63 @@ class SshService {
|
||||
});
|
||||
}
|
||||
|
||||
async uploadBuffer(serverId, remotePath, content, { mode = 0o600 } = {}) {
|
||||
const server = this.store.getServer(serverId);
|
||||
if (!server?.hostFingerprint) {
|
||||
const error = new Error('Test and trust the SSH server fingerprint before uploading deployment assets.');
|
||||
error.code = 'SSH_HOST_NOT_TRUSTED';
|
||||
throw error;
|
||||
}
|
||||
const target = String(remotePath || '').replace(/\\/g, '/');
|
||||
if (!target.startsWith('/') || target.includes('\0') || target.split('/').includes('..')) throw new Error('Remote upload path must be an absolute safe Unix path.');
|
||||
const data = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
||||
return this.withClient(serverId, (client) => new Promise((resolve, reject) => {
|
||||
client.sftp((sftpError, sftp) => {
|
||||
if (sftpError) { reject(sftpError); return; }
|
||||
const directory = path.dirname(target);
|
||||
const mkdirParts = directory.split('/').filter(Boolean);
|
||||
let current = '';
|
||||
const makeNext = (index) => {
|
||||
if (index >= mkdirParts.length) {
|
||||
const stream = sftp.createWriteStream(target, { mode });
|
||||
stream.once('error', reject);
|
||||
stream.once('close', () => resolve({ remotePath: target, size: data.length }));
|
||||
stream.end(data);
|
||||
return;
|
||||
}
|
||||
current += `/${mkdirParts[index]}`;
|
||||
const ensureDirectory = () => {
|
||||
sftp.stat(current, (statError, attributes) => {
|
||||
if (!statError) {
|
||||
if (typeof attributes?.isDirectory === 'function' && !attributes.isDirectory()) {
|
||||
reject(new Error(`Remote upload parent exists but is not a directory: ${current}`));
|
||||
return;
|
||||
}
|
||||
makeNext(index + 1);
|
||||
return;
|
||||
}
|
||||
if (![2, 'ENOENT'].includes(statError.code)) { reject(statError); return; }
|
||||
sftp.mkdir(current, { mode: 0o755 }, (mkdirError) => {
|
||||
if (!mkdirError) { makeNext(index + 1); return; }
|
||||
sftp.stat(current, (retryError, retryAttributes) => {
|
||||
if (!retryError && (typeof retryAttributes?.isDirectory !== 'function' || retryAttributes.isDirectory())) makeNext(index + 1);
|
||||
else reject(mkdirError);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
ensureDirectory();
|
||||
};
|
||||
makeNext(0);
|
||||
});
|
||||
}), { trustOnFirstUse: false });
|
||||
}
|
||||
|
||||
async uploadFile(serverId, localPath, remotePath, options = {}) {
|
||||
const data = await fs.readFile(localPath);
|
||||
return this.uploadBuffer(serverId, remotePath, data, options);
|
||||
}
|
||||
|
||||
async test(serverId, { trustOnFirstUse = true } = {}) {
|
||||
return this.withClient(serverId, async (client, server, fingerprint) => {
|
||||
const result = await this.execClient(client, 'uname -srm && command -v git && (docker compose version || docker-compose version)', { timeout: 30_000 });
|
||||
|
||||
Reference in New Issue
Block a user