CH-J Server Managerspráva serverů přes SSH
Menu
Vydané zdroje

CH-J Server Manager

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J Proprietary Software License 1.14

Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.

24,9 KB · 478 řádkůStáhnout soubor
1"use strict";
3const crypto = require("node:crypto");
4const fs = require("node:fs");
5const os = require("node:os");
6const path = require("node:path");
7const archiver = require("archiver");
9const MAX_TEXT_BYTES = 25 * 1024 * 1024;
10const MAX_TRANSFER_BYTES = 16 * 1024 * 1024 * 1024;
11const MAX_BATCH_ENTRIES = 10000;
12const MAX_SELECTED_ENTRIES = 100;
13const ARCHIVE_FORMATS = new Set(["zip", "tar", "tar.gz"]);
14const DEFINITE_EDITOR_FAILURES = new Set([
15 "FILE_CONFLICT", "FILE_PERMISSION_DENIED", "FILE_NOT_FOUND", "FILE_UNSAFE_PATH", "FILE_UNSAFE_TYPE_OR_LINK",
16 "FILE_UNSAFE_WORKSPACE", "FILE_UNSAFE_PRIVILEGED_DIRECTORY", "FILE_UPLOAD_INCOMPLETE", "FILE_TOO_LARGE",
17 "FILE_METADATA_UNSUPPORTED", "FILE_REMOTE_IO_FAILED", "FILE_SUDO_FAILED", "FILE_DEPENDENCY_UNAVAILABLE",
18 "FILE_SUDO_PASSWORD_INVALID", "FILE_SUDO_AUTHORIZATION_REQUIRED", "FILE_LINUX_REQUIRED"
19]);
21function normalizeRemotePath(value) {
22 const raw = String(value || "").trim();
23 if (!raw || raw.length > 4096 || !raw.startsWith("/") || /[\0\r\n]/.test(raw)) throw new Error("Remote path must be an absolute path.");
24 return path.posix.normalize(raw);
27function call(sftp, method, ...args) {
28 return new Promise((resolve, reject) => {
29 sftp[method](...args, (error, result) => error ? reject(error) : resolve(result));
30 });
33function safeEntryName(value) {
34 const name = String(value || "");
35 if (!name || name === "." || name === ".." || /[\/\\\0]/.test(name)) throw new Error("Remote entry name cannot be represented safely on the local filesystem.");
36 return name;
39function selectedPaths(result) {
40 if (!result || result.canceled) return [];
41 const values = Array.isArray(result.paths) ? result.paths : result.path ? [result.path] : [];
42 return [...new Set(values.map(String).filter(Boolean))].slice(0, MAX_SELECTED_ENTRIES + 1);
45class RemoteFileService {
46 constructor(options) {
47 this.sessionManager = options.sessionManager;
48 this.selectUploadPath = options.selectUploadPath;
49 this.selectDownloadPath = options.selectDownloadPath;
50 this.selectDownloadDirectory = options.selectDownloadDirectory;
51 this.selectArchivePath = options.selectArchivePath;
52 this.logger = options.logger;
53 this.edits = new Map();
54 this.legacyEdits = new Map();
55 }
57 async list(sessionId, remotePath) {
58 const target = normalizeRemotePath(remotePath || "/");
59 return this._withSftp(sessionId, async (sftp) => {
60 const entries = await call(sftp, "readdir", target);
61 return (entries || []).filter((entry) => entry.filename && entry.filename !== "." && entry.filename !== "..").map((entry) => {
62 const attrs = entry.attrs || {};
63 const isDirectory = typeof attrs.isDirectory === "function" && attrs.isDirectory();
64 const isSymbolicLink = typeof attrs.isSymbolicLink === "function" && attrs.isSymbolicLink();
65 return {
66 name: String(entry.filename),
67 path: path.posix.join(target, entry.filename),
68 type: isDirectory ? "directory" : isSymbolicLink ? "symlink" : "file",
69 size: Math.max(0, Number(attrs.size) || 0),
70 modifiedAt: Number(attrs.mtime) > 0 ? new Date(Number(attrs.mtime) * 1000).toISOString() : null,
71 mode: Number(attrs.mode) || 0
72 };
73 }).sort((left, right) => {
74 const order = { directory: 0, file: 1, symlink: 2 };
75 return order[left.type] === order[right.type] ? left.name.localeCompare(right.name) : order[left.type] - order[right.type];
76 });
77 });
78 }
80 _editorPath(value) {
81 const raw = String(value || "");
82 if (!raw || raw.length > 4096 || !raw.startsWith("/") || /[\0\r\n]/.test(raw)) throw new Error("Remote path must be an absolute path.");
83 if (raw.split("/").some((part) => part === "." || part === "..")) throw new Error("Editor paths cannot contain traversal components.");
84 return path.posix.normalize(raw);
85 }
87 _identity(sessionId) {
88 const session = this.sessionManager.list().find((entry) => entry.sessionId === sessionId && entry.state === "connected");
89 if (!session) throw new Error("The editor session is not connected.");
90 return JSON.stringify([sessionId, session.profileId, session.host, session.port, session.username]);
91 }
93 async readText(sessionId, remotePath, options = {}, owner = "core") {
94 const target = this._editorPath(remotePath);
95 const identity = this._identity(sessionId);
96 const value = await this.sessionManager.remoteEditor(sessionId, { operation: "read", path: target }, options);
97 const data = Buffer.from(value.data, "base64");
98 if (data.length > MAX_TEXT_BYTES) throw new Error(`Text editor limit is ${MAX_TEXT_BYTES} bytes.`);
99 let text;
100 try { text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(data); }
101 catch { throw new Error("The selected file is not valid UTF-8 text."); }
102 if (crypto.createHash("sha256").update(data).digest("hex") !== value.baseline.hash) throw new Error("Remote editor content verification failed.");
103 if (this.edits.size >= 1000) throw new Error("Close unused editor documents before opening more files.");
104 const editId = crypto.randomBytes(16).toString("hex");
105 this.edits.set(editId, { target, identity, owner, baseline: value.baseline, busy: false, uncertain: false });
106 const key = JSON.stringify([owner, identity, target]);
107 // A legacy call has no document identifier. Once two documents are open,
108 // choosing either baseline would silently defeat conflict detection.
109 this.legacyEdits.set(key, this.legacyEdits.has(key) ? null : editId);
110 return { path: target, text, size: data.length, editId, baselineHash: value.baseline.hash, metadata: value.baseline,
111 modifiedAt: new Date(Number(BigInt(value.baseline.mtimeNs) / 1000000n)).toISOString() };
112 }
114 async writeText(sessionId, remotePath, text, options = {}, owner = "core") {
115 const target = this._editorPath(remotePath);
116 const data = Buffer.from(String(text ?? ""), "utf8");
117 if (data.length > MAX_TEXT_BYTES) throw new Error(`Text editor limit is ${MAX_TEXT_BYTES} bytes.`);
118 const identity = this._identity(sessionId);
119 const legacyId = this.legacyEdits.get(JSON.stringify([owner, identity, target]));
120 if (!options.editId && legacyId === null) {
121 const error = new Error("FILE_EDIT_ID_REQUIRED: Multiple documents are open for this path; pass the editId returned by readText.");
122 error.code = "FILE_EDIT_ID_REQUIRED";
123 throw error;
124 }
125 const editId = options.editId || legacyId;
126 const edit = this.edits.get(editId);
127 if (!edit || edit.target !== target || edit.identity !== identity || edit.owner !== owner) throw new Error("Reopen this file to capture an editor baseline before saving.");
128 if (edit.busy || edit.uncertain) {
129 const error = new Error("FILE_RESULT_UNKNOWN: Inspect recovery before starting another save.");
130 error.code = "FILE_RESULT_UNKNOWN";
131 throw error;
132 }
133 edit.busy = true;
134 let recovery;
135 let submitted = false;
136 try {
137 const hash = crypto.createHash("sha256").update(data).digest("hex");
138 const id = crypto.randomBytes(16).toString("hex");
139 recovery = await this.sessionManager.remoteEditor(sessionId, { operation: "prepare", id, path: target, baseline: edit.baseline, hash });
140 await this._uploadEditorCopy(sessionId, recovery.temporary, data);
141 submitted = true;
142 const saved = await this.sessionManager.remoteEditor(sessionId, {
143 operation: "finalize", id, uid: recovery.uid, path: target, baseline: edit.baseline, hash
144 }, options);
145 if (saved.status !== "saved" || saved.hash !== hash || saved.baseline?.hash !== hash) {
146 const error = new Error("Remote save could not be confirmed."); error.code = "FILE_RESULT_UNKNOWN"; throw error;
147 }
148 edit.baseline = saved.baseline;
149 let recoveryAvailable = false;
150 try { await this.sessionManager.remoteEditor(sessionId, { operation: "cleanup", id, uid: recovery.uid }, options); }
151 catch { recoveryAvailable = true; }
152 this.logger?.info("Remote editor save confirmed.", { sessionId, path: target, bytes: data.length });
153 return { path: target, size: data.length, status: "saved", editId, baselineHash: hash, recoveryId: id, recoveryAvailable };
154 } catch (error) {
155 const knownFailure = DEFINITE_EDITOR_FAILURES.has(error.code);
156 edit.uncertain = submitted && !knownFailure;
157 error.code = edit.uncertain ? "FILE_RESULT_UNKNOWN" : error.code || "FILE_UPLOAD_FAILED";
158 error.recoveryId = recovery?.recoveryId;
159 error.recoveryPath = recovery?.temporary;
160 error.status = edit.uncertain ? "unknown" : "failed";
161 error.message = `${error.code}: Save was not confirmed. Keep editor content and inspect recovery${recovery ? " (" + recovery.recoveryId + ")" : ""}.`;
162 throw error;
163 } finally {
164 edit.busy = false;
165 if (edit.dispose) this.closeText(editId, owner);
166 }
167 }
169 async saveText(sessionId, remotePath, text, options = {}, owner = "core") {
170 try { return { ok: true, value: await this.writeText(sessionId, remotePath, text, options, owner) }; }
171 catch (error) { return { ok: false, error: { code: error.code || "FILE_SAVE_FAILED", message: error.message,
172 status: error.status || "failed", recoveryId: error.recoveryId, recoveryPath: error.recoveryPath } }; }
173 }
175 _uploadEditorCopy(sessionId, temporary, data) {
176 return new Promise((resolve, reject) => {
177 let sftp;
178 let settled = false;
179 const finish = (error) => {
180 if (settled) return;
181 settled = true;
182 clearTimeout(timer);
183 try { sftp?.end?.(); } catch {}
184 if (error) reject(error); else resolve();
185 };
186 const timer = setTimeout(() => finish(Object.assign(new Error("Editor upload timed out."), { code: "FILE_UPLOAD_TIMEOUT" })), 60000);
187 timer.unref?.();
188 Promise.resolve().then(() => this.sessionManager.openSftp(sessionId)).then((channel) => {
189 if (settled) { try { channel.end?.(); } catch {} return; }
190 sftp = channel;
191 call(sftp, "writeFile", temporary, data, { flag: "wx", mode: 0o600 }).then(() => finish(), finish);
192 }, finish);
193 });
194 }
196 async _recovery(sessionId, operation, recoveryId, options = {}) {
197 const request = { operation, id: recoveryId };
198 if (options.sudo === true) {
199 const identity = await this.sessionManager.remoteEditor(sessionId, { operation: "identity" });
200 request.uid = identity.uid;
201 }
202 return this.sessionManager.remoteEditor(sessionId, request, options);
203 }
205 listRecovery(sessionId, options = {}) {
206 return this._recovery(sessionId, "list", undefined, options);
207 }
209 async readRecovery(sessionId, recoveryId, options = {}) {
210 const result = await this._recovery(sessionId, "inspect", recoveryId, options);
211 const data = Buffer.from(result.data, "base64");
212 if (!result.complete || data.length > MAX_TEXT_BYTES || crypto.createHash("sha256").update(data).digest("hex") !== result.hash) {
213 throw new Error("FILE_RECOVERY_UNVERIFIED: Recovery content could not be verified.");
214 }
215 let text;
216 try { text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(data); }
217 catch { throw new Error("Recovery file is not valid UTF-8 text."); }
218 const { data: _data, ...summary } = result;
219 return { ...summary, text };
220 }
222 cleanupRecovery(sessionId, recoveryId, options = {}) {
223 return this._recovery(sessionId, "cleanup", recoveryId, options);
224 }
226 closeText(editId, owner = "core") {
227 const edit = this.edits.get(editId);
228 if (!edit || edit.owner !== owner || edit.busy) return { closed: false };
229 this.edits.delete(editId);
230 const key = JSON.stringify([owner, edit.identity, edit.target]);
231 const remaining = [...this.edits].filter(([, other]) => other.owner === owner && other.identity === edit.identity && other.target === edit.target);
232 if (!remaining.length) this.legacyEdits.delete(key);
233 else this.legacyEdits.set(key, remaining.length === 1 ? remaining[0][0] : null);
234 return { closed: true };
235 }
237 cleanupPlugin(owner) {
238 for (const [id, edit] of this.edits) {
239 if (edit.owner !== owner) continue;
240 if (edit.busy) edit.dispose = true;
241 else this.closeText(id, owner);
242 }
243 }
245 async createFile(sessionId, remotePath) {
246 const target = normalizeRemotePath(remotePath);
247 if (target === "/") throw new Error("The remote root cannot be a file.");
248 return this._withSftp(sessionId, async (sftp) => {
249 await call(sftp, "writeFile", target, Buffer.alloc(0), { flag: "wx", mode: 0o644 });
250 this.logger?.warn("Remote file created.", { sessionId, path: target });
251 return { path: target, size: 0 };
252 });
253 }
255 async mkdir(sessionId, remotePath) {
256 const target = normalizeRemotePath(remotePath);
257 if (target === "/") throw new Error("The remote root already exists.");
258 return this._withSftp(sessionId, async (sftp) => {
259 await call(sftp, "mkdir", target, { mode: 0o755 });
260 this.logger?.warn("Remote directory created.", { sessionId, path: target });
261 return { path: target };
262 });
263 }
265 async rename(sessionId, fromPath, toPath) {
266 const from = normalizeRemotePath(fromPath); const to = normalizeRemotePath(toPath);
267 if (from === "/" || to === "/") throw new Error("The remote root cannot be renamed.");
268 return this._withSftp(sessionId, async (sftp) => {
269 await call(sftp, "rename", from, to);
270 this.logger?.warn("Remote entry renamed.", { sessionId, from, to });
271 return { from, to };
272 });
273 }
275 async remove(sessionId, remotePath) {
276 const target = normalizeRemotePath(remotePath);
277 if (target === "/") throw new Error("The remote root cannot be removed.");
278 return this._withSftp(sessionId, async (sftp) => {
279 const attrs = await call(sftp, "lstat", target);
280 const isDirectory = typeof attrs?.isDirectory === "function" && attrs.isDirectory();
281 await call(sftp, isDirectory ? "rmdir" : "unlink", target);
282 this.logger?.warn("Remote entry removed.", { sessionId, path: target, type: isDirectory ? "directory" : "file" });
283 return { path: target, type: isDirectory ? "directory" : "file" };
284 });
285 }
287 async removeMany(sessionId, remotePaths, recursive = false) {
288 const targets = this._normalizeSelection(remotePaths);
289 if (targets.some((target) => target === "/")) throw new Error("The remote root cannot be removed.");
290 return this._withSftp(sessionId, async (sftp) => {
291 const state = { entries: 0, bytes: 0 };
292 for (const target of [...targets].sort((left, right) => right.length - left.length)) await this._removeRemoteEntry(sftp, target, Boolean(recursive), state);
293 this.logger?.warn("Remote entries removed.", { sessionId, selected: targets.length, entries: state.entries, recursive: Boolean(recursive) });
294 return { selected: targets.length, entries: state.entries };
295 });
296 }
298 async upload(sessionId, remoteDirectory) {
299 const directory = normalizeRemotePath(remoteDirectory || "/");
300 if (typeof this.selectUploadPath !== "function") throw new Error("Upload dialog is unavailable.");
301 const selected = await this.selectUploadPath();
302 const localPaths = selectedPaths(selected);
303 if (!localPaths.length) return { canceled: true, items: [] };
304 if (localPaths.length > MAX_SELECTED_ENTRIES) throw new Error(`At most ${MAX_SELECTED_ENTRIES} entries can be uploaded at once.`);
305 return this._withSftp(sessionId, async (sftp) => {
306 const state = { entries: 0, bytes: 0 };
307 const items = [];
308 for (const localPath of localPaths) {
309 const target = path.posix.join(directory, path.basename(localPath));
310 const result = await this._uploadLocalEntry(sftp, localPath, target, state);
311 items.push({ path: target, type: result.type, size: result.size });
312 }
313 this.logger?.info("Entries uploaded through Core.", { sessionId, directory, entries: state.entries, bytes: state.bytes });
314 return { canceled: false, items, entries: state.entries, size: state.bytes };
315 });
316 }
318 async download(sessionId, remotePath) {
319 const target = normalizeRemotePath(remotePath);
320 if (typeof this.selectDownloadPath !== "function") throw new Error("Download dialog is unavailable.");
321 return this._withSftp(sessionId, async (sftp) => {
322 const attrs = await call(sftp, "stat", target);
323 if (typeof attrs?.isDirectory === "function" && attrs.isDirectory()) throw new Error("Directory download is not available in this first File Manager version.");
324 const size = Math.max(0, Number(attrs?.size) || 0);
325 if (size > MAX_TRANSFER_BYTES) throw new Error("Remote file exceeds the transfer size limit.");
326 const selected = await this.selectDownloadPath(path.posix.basename(target));
327 if (!selected || selected.canceled || !selected.path) return { canceled: true };
328 await call(sftp, "fastGet", target, selected.path);
329 this.logger?.info("File downloaded through Core.", { sessionId, path: target, bytes: size });
330 return { canceled: false, path: target, size };
331 });
332 }
334 async downloadMany(sessionId, remotePaths) {
335 const targets = this._normalizeSelection(remotePaths);
336 if (typeof this.selectDownloadDirectory !== "function") throw new Error("Download directory dialog is unavailable.");
337 const selected = await this.selectDownloadDirectory();
338 if (!selected || selected.canceled || !selected.path) return { canceled: true, items: [] };
339 return this._withSftp(sessionId, async (sftp) => {
340 const state = { entries: 0, bytes: 0 };
341 const items = [];
342 for (const target of targets) {
343 if (target === "/") throw new Error("Downloading the entire remote root is not allowed in one batch.");
344 const localPath = path.join(selected.path, safeEntryName(path.posix.basename(target)));
345 const result = await this._downloadRemoteEntry(sftp, target, localPath, state);
346 items.push({ path: target, type: result.type, size: result.size });
347 }
348 this.logger?.info("Entries downloaded through Core.", { sessionId, entries: state.entries, bytes: state.bytes });
349 return { canceled: false, items, entries: state.entries, size: state.bytes };
350 });
351 }
353 async downloadArchive(sessionId, remotePaths, requestedFormat) {
354 const targets = this._normalizeSelection(remotePaths);
355 if (targets.some((target) => target === "/")) throw new Error("Archiving the entire remote root is not allowed.");
356 const format = String(requestedFormat || "zip").toLowerCase() === "tgz" ? "tar.gz" : String(requestedFormat || "zip").toLowerCase();
357 if (!ARCHIVE_FORMATS.has(format)) throw new Error("Supported archive formats are zip, tar and tar.gz.");
358 if (typeof this.selectArchivePath !== "function") throw new Error("Archive save dialog is unavailable.");
359 const baseName = targets.length === 1 ? safeEntryName(path.posix.basename(targets[0])) : "remote-selection";
360 const selected = await this.selectArchivePath(`${baseName}.${format}`, format);
361 if (!selected || selected.canceled || !selected.path) return { canceled: true };
362 const temporaryRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), "chj-file-archive-"));
363 try {
364 const transfer = await this._withSftp(sessionId, async (sftp) => {
365 const state = { entries: 0, bytes: 0 };
366 for (const target of targets) {
367 await this._downloadRemoteEntry(sftp, target, path.join(temporaryRoot, safeEntryName(path.posix.basename(target))), state);
368 }
369 return state;
370 });
371 const archiveSize = await this._createArchive(temporaryRoot, selected.path, format);
372 this.logger?.info("Remote selection archived through Core.", { sessionId, format, entries: transfer.entries, sourceBytes: transfer.bytes, archiveBytes: archiveSize });
373 return { canceled: false, path: selected.path, format, entries: transfer.entries, sourceSize: transfer.bytes, size: archiveSize };
374 } finally {
375 await fs.promises.rm(temporaryRoot, { recursive: true, force: true }).catch(() => {});
376 }
377 }
379 _normalizeSelection(remotePaths) {
380 const values = Array.isArray(remotePaths) ? remotePaths : [remotePaths];
381 const targets = [...new Set(values.filter((value) => value !== undefined && value !== null).map(normalizeRemotePath))];
382 if (!targets.length || targets.length > MAX_SELECTED_ENTRIES) throw new Error(`Select between 1 and ${MAX_SELECTED_ENTRIES} remote entries.`);
383 return targets;
384 }
386 _countTransfer(state, bytes = 0) {
387 state.entries += 1;
388 state.bytes += Math.max(0, Number(bytes) || 0);
389 if (state.entries > MAX_BATCH_ENTRIES) throw new Error(`Transfer exceeds the ${MAX_BATCH_ENTRIES} entry limit.`);
390 if (state.bytes > MAX_TRANSFER_BYTES) throw new Error("Transfer exceeds the aggregate size limit.");
391 }
393 async _uploadLocalEntry(sftp, localPath, remotePath, state) {
394 const attrs = await fs.promises.lstat(localPath);
395 if (attrs.isSymbolicLink()) throw new Error("Uploading symbolic links is not supported.");
396 if (attrs.isDirectory()) {
397 this._countTransfer(state);
398 try { await call(sftp, "mkdir", remotePath, { mode: 0o755 }); }
399 catch (error) {
400 const existing = await call(sftp, "stat", remotePath).catch(() => null);
401 if (!existing || typeof existing.isDirectory !== "function" || !existing.isDirectory()) throw error;
402 }
403 for (const entry of await fs.promises.readdir(localPath, { withFileTypes: true })) {
404 const name = safeEntryName(entry.name);
405 await this._uploadLocalEntry(sftp, path.join(localPath, name), path.posix.join(remotePath, name), state);
406 }
407 return { type: "directory", size: 0 };
408 }
409 if (!attrs.isFile()) throw new Error("Only regular files and directories can be uploaded.");
410 this._countTransfer(state, attrs.size);
411 const temporary = path.posix.join(path.posix.dirname(remotePath), `.${path.posix.basename(remotePath)}.chj-upload-${crypto.randomBytes(6).toString("hex")}.tmp`);
412 try {
413 await call(sftp, "fastPut", localPath, temporary);
414 await call(sftp, "rename", temporary, remotePath);
415 } catch (error) {
416 try { await call(sftp, "unlink", temporary); } catch {}
417 throw error;
418 }
419 return { type: "file", size: attrs.size };
420 }
422 async _downloadRemoteEntry(sftp, remotePath, localPath, state) {
423 const attrs = await call(sftp, "lstat", remotePath);
424 if (typeof attrs?.isSymbolicLink === "function" && attrs.isSymbolicLink()) throw new Error(`Symbolic links are not included in recursive downloads: ${remotePath}`);
425 if (typeof attrs?.isDirectory === "function" && attrs.isDirectory()) {
426 this._countTransfer(state);
427 await fs.promises.mkdir(localPath, { recursive: false, mode: 0o700 }).catch((error) => { if (error.code !== "EEXIST") throw error; });
428 for (const entry of await call(sftp, "readdir", remotePath) || []) {
429 const name = safeEntryName(entry.filename);
430 await this._downloadRemoteEntry(sftp, path.posix.join(remotePath, name), path.join(localPath, name), state);
431 }
432 return { type: "directory", size: 0 };
433 }
434 const size = Math.max(0, Number(attrs?.size) || 0);
435 this._countTransfer(state, size);
436 await fs.promises.mkdir(path.dirname(localPath), { recursive: true, mode: 0o700 });
437 const temporary = path.join(path.dirname(localPath), `.${path.basename(localPath)}.chj-download-${crypto.randomBytes(6).toString("hex")}.tmp`);
438 try {
439 await call(sftp, "fastGet", remotePath, temporary);
440 await fs.promises.rename(temporary, localPath);
441 } catch (error) {
442 await fs.promises.unlink(temporary).catch(() => {});
443 throw error;
444 }
445 return { type: "file", size };
446 }
448 async _removeRemoteEntry(sftp, remotePath, recursive, state) {
449 const attrs = await call(sftp, "lstat", remotePath);
450 this._countTransfer(state, Number(attrs?.size) || 0);
451 if (typeof attrs?.isDirectory === "function" && attrs.isDirectory()) {
452 if (recursive) for (const entry of await call(sftp, "readdir", remotePath) || []) await this._removeRemoteEntry(sftp, path.posix.join(remotePath, safeEntryName(entry.filename)), true, state);
453 await call(sftp, "rmdir", remotePath);
454 } else await call(sftp, "unlink", remotePath);
455 }
457 _createArchive(sourceRoot, destination, format) {
458 return new Promise((resolve, reject) => {
459 const output = fs.createWriteStream(destination, { flags: "w", mode: 0o600 });
460 const archive = format === "zip" ? archiver("zip", { zlib: { level: 9 } }) : archiver("tar", { gzip: format === "tar.gz", gzipOptions: { level: 9 } });
461 let settled = false;
462 const fail = (error) => { if (settled) return; settled = true; try { archive.destroy(); } catch {} reject(error); };
463 output.on("close", () => { if (!settled) { settled = true; resolve(Number(archive.pointer()) || 0); } });
464 output.on("error", fail); archive.on("warning", fail); archive.on("error", fail);
465 archive.pipe(output);
466 archive.directory(sourceRoot, false);
467 Promise.resolve(archive.finalize()).catch(fail);
468 });
469 }
471 async _withSftp(sessionId, action) {
472 const sftp = await this.sessionManager.openSftp(sessionId);
473 try { return await action(sftp); }
474 finally { try { sftp.end?.(); } catch {} }
475 }
478module.exports = { ARCHIVE_FORMATS, MAX_BATCH_ENTRIES, MAX_TEXT_BYTES, MAX_TRANSFER_BYTES, RemoteFileService, normalizeRemotePath, safeEntryName };

SHA-256: e4bb3fe7160d59a22a33b8a7c346f3e74fdfdda8d21bbff3af6b30e785da8f0b

SHA-256 archivu: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0