CH-J Server Managermanagementul serverului peste SSH
Meniu
Sursă publicată

CH-J Server Manager

Navighează directoarele și fișierele pentru o anumită versiune a aplicației.

Descarcă sursa ZIP
CH-J / Server Manager 377 fișiere
core-20261019T111616Z-160a00bfcddc Adăugat: · Construit:
filesVmIntegration.test.js Adăugat: Data neînregistrată 23,7 KB · 351 linii
Cod Descarcă fișierul
1"use strict";
3// File Manager privileged editing and Transfer Manager on ONE isolated test VM (Linux or Windows) over real SSH.
4// Same environment as linuxVmIntegration.test.js: CHJ_VM_HOST, CHJ_VM_FINGERPRINT (SHA256:...), CHJ_VM_KEY,
5// CHJ_VM_USER (default ai), CHJ_VM_SUDO_PASSWORD or CHJ_VM_SUDO_PASSWORD_FILE + CHJ_VM_SUDO_PASSWORD_LINE
6// (Linux only; read at runtime, never printed). Never point this at a production server. Everything is created
7// below /etc/chj-it-<id>, ~/chj-it-<id> and the account's ~/ch-j-sm/transfers and removed afterwards.
8// Not covered here (needs extra accounts or disruptive actions on a shared VM): direct root login, NOPASSWD and
9// no-sudo accounts, a full filesystem and a reboot of the SSH server; see filesIntegration.test.js (Docker).
10const test = require("node:test");
11const assert = require("node:assert/strict");
12const crypto = require("node:crypto");
13const fs = require("node:fs");
14const os = require("node:os");
15const path = require("node:path");
16const { execFileSync } = require("node:child_process");
17const { SessionManager } = require("../src/main/sessions/sessionManager");
18const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec");
19const { RemoteFileService } = require("../src/main/files/remoteFileService");
20const { TransferManager } = require("../src/main/files/transferManager");
21const { detectPlatform } = require("../src/main/sessions/platformDetect");
23const env = process.env;
24const enabled = Boolean(env.CHJ_VM_HOST && env.CHJ_VM_FINGERPRINT && env.CHJ_VM_KEY);
25const skip = enabled ? false : "Set CHJ_VM_* to run against an isolated test VM.";
26const readSecret = () => env.CHJ_VM_SUDO_PASSWORD || (env.CHJ_VM_SUDO_PASSWORD_FILE
27 ? fs.readFileSync(env.CHJ_VM_SUDO_PASSWORD_FILE, "utf8").split("\n")[Number(env.CHJ_VM_SUDO_PASSWORD_LINE || 1) - 1].split("=").slice(1).join("=").trim() : "");
28const vmUser = env.CHJ_VM_USER || "ai";
29const host = env.CHJ_VM_HOST;
30const run = crypto.randomBytes(4).toString("hex");
31const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
32const sha = (data) => crypto.createHash("sha256").update(data).digest("hex");
33async function until(predicate, timeout = 120000, label = "condition") {
34 const started = Date.now();
35 while (!(await predicate())) { if (Date.now() - started > timeout) throw new Error(`Timed out waiting for ${label}`); await sleep(50); }
37function bigFile(file, mib) {
38 const fd = fs.openSync(file, "w"); const block = crypto.randomBytes(1024 * 1024);
39 for (let index = 0; index < mib; index += 1) fs.writeSync(fd, block);
40 fs.closeSync(fd);
41 return sha(fs.readFileSync(file));
44test(`File Manager and transfers on an isolated test VM (${host || "not configured"})`, { skip, timeout: 1800000 }, async (t) => {
45 const work = fs.mkdtempSync(path.join(os.tmpdir(), "chj-files-vm-"));
46 t.after(() => fs.rmSync(work, { recursive: true, force: true }));
47 const knownHosts = path.join(work, "known_hosts");
48 execFileSync("sh", ["-c", `ssh-keyscan -t ed25519 ${host} > '${knownHosts}' 2>/dev/null`]);
49 assert.equal(execFileSync("ssh-keygen", ["-lf", knownHosts], { encoding: "utf8" }).split(" ")[1], env.CHJ_VM_FINGERPRINT, "documented host key");
50 const hostKeys = new Map();
51 const profileService = {
52 get: () => ({ id: "vm", label: "vm", host, port: 22, username: vmUser, authMethod: "privateKey", privateKeyPath: env.CHJ_VM_KEY }),
53 getHostKey: (h, p) => hostKeys.get(`${h}:${p}`) || null, trustHostKey: (h, p, fp) => hostKeys.set(`${h}:${p}`, fp), getStoredPassword: () => "", markUsed() {}
54 };
55 const manager = new SessionManager({ profileService, latencyMonitor: { start: () => () => {} } });
56 const connect = async (sessionId) => {
57 try { await manager.connect({ sessionId, profileId: "vm" }); }
58 catch (error) {
59 if (error.code !== "HOST_KEY_UNKNOWN") throw error;
60 assert.equal(`SHA256:${Buffer.from(error.fingerprint, "hex").toString("base64").replace(/=+$/, "")}`, env.CHJ_VM_FINGERPRINT);
61 manager.trustPending({ sessionId, profileId: "vm", fingerprint: error.fingerprint });
62 await manager.connect({ sessionId, profileId: "vm" });
63 }
64 };
65 await connect("vm");
66 await connect("xfer");
67 // node:test runs after-hooks in registration order: data cleanup is registered first, the disconnect last.
68 const cleanups = [];
69 t.after(async () => {
70 transfers.shutdown();
71 try { for (const cleanup of cleanups) await cleanup(); }
72 finally { await manager.disconnectAll("test"); }
73 });
74 const executor = new PrivilegedExecutor({ sessionManager: manager });
75 manager.privilegedExecutor = executor;
76 let uploadSelection = []; let downloadTarget = work;
77 const files = new RemoteFileService({ sessionManager: manager, privilegedExecutor: executor,
78 selectUploadPath: async () => ({ canceled: false, paths: uploadSelection }), selectDownloadPath: async (name) => ({ canceled: false, path: path.join(downloadTarget, name) }),
79 selectDownloadDirectory: async () => ({ canceled: false, path: downloadTarget }) });
80 const storage = path.join(work, "state");
81 let transfers = new TransferManager({ sessionManager: manager, remoteFileService: files, storageRoot: storage, retryDelaysMs: [500, 1000, 2000] });
82 const lastJob = () => transfers.list("fm").jobs.at(-1);
83 const platform = await detectPlatform(manager, "vm");
84 t.diagnostic(`platform=${platform.os} ${platform.caption || ""}`.trim());
86 if (platform.os === "windows") {
87 const home = (await new Promise((resolve, reject) => manager.openSftp("vm").then((sftp) => sftp.realpath(".", (error, value) => { sftp.end(); error ? reject(error) : resolve(value); }), reject)));
88 const dir = path.posix.join(home, `chj-it-${run}`);
89 await files.mkdir("vm", dir);
90 cleanups.push(async () => {
91 if (!manager.list().some((entry) => entry.sessionId === "vm")) await connect("vm");
92 await files.removeMany("vm", [dir], true);
93 assert.equal((await files.list("vm", home)).some((entry) => entry.name === `chj-it-${run}`), false, "Windows test data removed");
94 });
95 await t.test("Windows: listing, editor limitation is reported clearly", async () => {
96 assert.ok((await files.list("vm", home)).some((entry) => entry.name === `chj-it-${run}`));
97 await files.createFile("vm", `${dir}/note.txt`);
98 await assert.rejects(() => files.readText("vm", `${dir}/note.txt`), { code: "FEATURE_PLATFORM_UNSUPPORTED" });
99 });
100 await t.test("Windows: upload, pause/resume, download with verification, cancel", async () => {
101 const local = path.join(work, "win.bin"); const digest = bigFile(local, 64);
102 uploadSelection = [local];
103 const plan = await transfers.planUpload("fm", "xfer", dir);
104 assert.equal(plan.mode, "direct");
105 await transfers.start("fm", plan.planId);
106 await until(() => lastJob()?.transferred > 8 * 1024 * 1024, 120000, "progress");
107 transfers.pause("fm", [lastJob().id]);
108 await until(() => lastJob().state === "paused", 30000, "paused");
109 const offset = lastJob().transferred;
110 transfers.resume("fm", [lastJob().id]);
111 await until(() => ["completed", "failed"].includes(lastJob().state), 300000, "upload");
112 assert.equal(lastJob().state, "completed", JSON.stringify(lastJob().error));
113 assert.equal(lastJob().verification.method, "sha256", "Get-FileHash verification on Windows");
114 t.diagnostic(`Windows upload paused at ${offset}, verification ${lastJob().verification?.method}`);
115 const again = await transfers.planUpload("fm", "xfer", dir);
116 await transfers.start("fm", again.planId, { conflict: "overwrite" });
117 await until(() => ["completed", "failed"].includes(lastJob().state), 60000, "overwrite attempt");
118 assert.equal(lastJob().state, "failed"); assert.equal(lastJob().error.code, "FEATURE_PLATFORM_UNSUPPORTED", "existing files are not replaced without metadata support");
119 const downloads = fs.mkdtempSync(path.join(work, "dl-")); downloadTarget = downloads;
120 const download = await transfers.planDownload("fm", "xfer", [`${dir}/win.bin`]);
121 await transfers.start("fm", download.planId);
122 await until(() => ["completed", "failed"].includes(lastJob().state), 300000, "download");
123 assert.equal(lastJob().state, "completed", JSON.stringify(lastJob().error));
124 assert.equal(sha(fs.readFileSync(path.join(downloads, "win.bin"))), digest);
125 assert.equal(lastJob().verification.method, "sha256"); assert.equal(lastJob().verification.matched, true);
126 uploadSelection = [path.join(work, "win-cancel.bin")]; fs.copyFileSync(local, uploadSelection[0]);
127 const cancel = await transfers.planUpload("fm", "xfer", dir);
128 await transfers.start("fm", cancel.planId);
129 await until(() => lastJob()?.transferred > 4 * 1024 * 1024, 120000, "progress");
130 await transfers.cancel("fm", [lastJob().id]);
131 await until(() => lastJob().state === "cancelled", 30000, "cancelled");
132 const names = (await files.list("vm", dir)).map((entry) => entry.name);
133 assert.equal(names.some((name) => name.includes("chj-part")), false); assert.equal(names.includes("win-cancel.bin"), false);
134 });
135 return;
136 }
138 const sudoPassword = readSecret();
139 assert.ok(sudoPassword, "sudo password configured for Linux VMs");
140 const auth = { sudo: true, sudoPassword };
141 const asRoot = (script, args = []) => executor.run("vm", { script: `printf '\\036CHJ_READY\\n'\n${script}`, args, sudoPassword, purpose: "files-vm-test", timeoutMs: 60000 })
142 .then((result) => result.stdout.trim());
143 const base = `/etc/chj-it-${run}`;
144 const home = (await asRoot(`getent passwd "$1" | cut -d: -f6`, [vmUser])) || `/home/${vmUser}`;
145 const userDir = `${home}/chj-it-${run}`;
146 cleanups.push(async () => {
147 if (!manager.list().some((entry) => entry.sessionId === "vm")) await connect("vm");
148 await asRoot(`rm -rf -- "$1" "$2"; rm -f -- "$3"/ch-j-sm/transfers/*.part "$3"/ch-j-sm/.*.tmp "$3"/ch-j-sm/.*.json; rmdir "$3"/ch-j-sm/transfers "$3"/ch-j-sm 2>/dev/null; true`, [base, userDir, home]);
149 assert.equal(await asRoot(`test -e "$1" || test -e "$2" && echo left || echo clean`, [base, userDir]), "clean", "test data removed");
150 });
151 const acl = await asRoot(String.raw`set -e
152umask 022; mkdir "$1" "$1/sites-available" "$1/sites-enabled"; mkdir -m 0755 "$2"; chown "$3" "$2"
153printf 'worker_processes 1;\n' > "$1/nginx.conf"
154printf 'server { listen 80; }\n' > "$1/sites-available/example.conf"; ln -s ../sites-available/example.conf "$1/sites-enabled/example.conf"
155printf 'secret=1\n' > "$1/secret.conf"; chmod 0600 "$1/secret.conf"
156printf 'shared\n' > "$1/hard.conf"; ln "$1/hard.conf" "$1/hard-link.conf"
157python3 - "$1/nginx.conf" <<'PY'
158import os, struct, sys
159p = sys.argv[1]
160os.setxattr(p, 'user.chj', b'kept')
161entries = [(1, 6, 0xffffffff), (2, 4, 1), (4, 4, 0xffffffff), (16, 4, 0xffffffff), (32, 4, 0xffffffff)]
162try:
163 os.setxattr(p, 'system.posix_acl_access', struct.pack('<I', 2) + b''.join(struct.pack('<HHI', *e) for e in entries)); print('acl')
164except OSError:
165 print('no-acl')
166PY`, [base, userDir, vmUser]);
167 t.diagnostic(`ACL on test filesystem: ${acl}`);
168 const facts = (file) => asRoot(String.raw`python3 - "$1" <<'PY'
169import base64, os, stat, sys
170s = os.lstat(sys.argv[1]); keys = sorted(os.listxattr(sys.argv[1], follow_symlinks=False))
171print(s.st_uid, s.st_gid, oct(stat.S_IMODE(s.st_mode)), [(k, base64.b64encode(os.getxattr(sys.argv[1], k, follow_symlinks=False)).decode()) for k in keys if not k.startswith('security.')])
172PY`, [file]);
173 const read = (file) => asRoot(`cat -- "$1"`, [file]);
175 await t.test("sudo account: clear refusal, password required/incorrect, then privileged read", async () => {
176 await assert.rejects(() => files.readText("vm", `${base}/secret.conf`), { code: "FILE_PERMISSION_DENIED" });
177 executor.forgetPassword("vm");
178 await assert.rejects(() => files.readText("vm", `${base}/secret.conf`, { sudo: true }), { code: "SUDO_PASSWORD_REQUIRED" });
179 await assert.rejects(() => files.readText("vm", `${base}/secret.conf`, { sudo: true, sudoPassword: "definitely-wrong" }), { code: "SUDO_PASSWORD_INCORRECT" });
180 assert.equal((await files.readText("vm", `${base}/secret.conf`, auth)).text, "secret=1\n");
181 assert.equal((await files.privileges("vm")).mode, "sudo-password");
182 });
184 await t.test("privileged save in /etc keeps owner, mode, xattr and ACL; nothing left behind", async () => {
185 const before = await facts(`${base}/nginx.conf`);
186 const opened = await files.readText("vm", `${base}/nginx.conf`);
187 assert.equal(opened.directSave, false);
188 const denied = await files.saveText("vm", `${base}/nginx.conf`, "worker_processes 2;\n", { editId: opened.editId });
189 assert.equal(denied.error.code, "FILE_PERMISSION_DENIED");
190 const saved = await files.saveText("vm", `${base}/nginx.conf`, "worker_processes 2;\n", { editId: opened.editId, ...auth, rememberSudo: true });
191 assert.equal(saved.ok, true, JSON.stringify(saved.error));
192 assert.equal(await read(`${base}/nginx.conf`), "worker_processes 2;");
193 assert.equal(await facts(`${base}/nginx.conf`), before);
194 assert.equal(await asRoot(`ls -a "$1" | grep -c chj || true`, [base]), "0");
195 });
197 await t.test("symlink kept, hardlink refused, external change detected", async () => {
198 const opened = await files.readText("vm", `${base}/sites-enabled/example.conf`);
199 assert.equal(opened.resolvedPath, `${base}/sites-available/example.conf`);
200 assert.equal((await files.saveText("vm", `${base}/sites-enabled/example.conf`, "server { listen 8080; }\n", { editId: opened.editId, sudo: true })).ok, true);
201 assert.equal(await asRoot(`readlink "$1"`, [`${base}/sites-enabled/example.conf`]), "../sites-available/example.conf");
202 assert.equal(await read(`${base}/sites-available/example.conf`), "server { listen 8080; }");
203 await assert.rejects(() => files.readText("vm", `${base}/hard.conf`, { sudo: true }), { code: "FILE_HARDLINK_UNSUPPORTED" });
204 const again = await files.readText("vm", `${base}/nginx.conf`);
205 await asRoot(`printf 'external\\n' >> "$1"`, [`${base}/nginx.conf`]);
206 const conflict = await files.saveText("vm", `${base}/nginx.conf`, "mine\n", { editId: again.editId, sudo: true });
207 assert.equal(conflict.error.code, "FILE_CONFLICT");
208 assert.match(await read(`${base}/nginx.conf`), /external$/);
209 });
211 await t.test("privileged create, mkdir, rename and remove; protected paths refused", async () => {
212 await files.mkdir("vm", `${base}/conf.d`, { sudo: true });
213 await files.createFile("vm", `${base}/conf.d/a.conf`, { sudo: true });
214 await files.rename("vm", `${base}/conf.d/a.conf`, `${base}/conf.d/b.conf`, { sudo: true });
215 await assert.rejects(() => files.createFile("vm", `${base}/conf.d/b.conf`, { sudo: true }), { code: "FILE_EXISTS" });
216 assert.equal(await asRoot(`stat -c '%u:%g:%a' "$1"`, [`${base}/conf.d/b.conf`]), "0:0:644");
217 await files.removeMany("vm", [`${base}/conf.d`], true, { sudo: true });
218 await assert.rejects(() => files.removeMany("vm", ["/etc"], true, { sudo: true }), { code: "FILE_PROTECTED_PATH" });
219 });
221 const big = path.join(work, "big.bin"); const bigHash = bigFile(big, 128);
222 await t.test("upload/download of a regular file and privileged upload/download in /etc", async () => {
223 uploadSelection = [big];
224 const plan = await transfers.planUpload("fm", "xfer", userDir);
225 assert.equal(plan.mode, "direct");
226 await transfers.start("fm", plan.planId);
227 await until(() => ["completed", "failed"].includes(lastJob()?.state), 300000, "upload");
228 assert.equal(lastJob().state, "completed", JSON.stringify(lastJob().error));
229 assert.equal(await asRoot(`sha256sum "$1" | cut -d' ' -f1`, [`${userDir}/big.bin`]), bigHash);
230 const conf = path.join(work, "nginx.conf"); fs.writeFileSync(conf, "worker_processes auto;\n".repeat(2000));
231 await asRoot(`printf 'worker_processes 1;\\n' > "$1"`, [`${base}/nginx.conf`]);
232 const before = await facts(`${base}/nginx.conf`);
233 uploadSelection = [conf];
234 const privileged = await transfers.planUpload("fm", "xfer", base);
235 assert.equal(privileged.mode, "privileged"); assert.equal(privileged.conflictCount, 1);
236 await transfers.start("fm", privileged.planId, { conflict: "overwrite", sudoPassword });
237 await until(() => ["completed", "failed", "interrupted"].includes(lastJob()?.state), 120000, "privileged upload");
238 assert.equal(lastJob().state, "completed", JSON.stringify(lastJob().error));
239 assert.equal(await asRoot(`sha256sum "$1" | cut -d' ' -f1`, [`${base}/nginx.conf`]), sha(fs.readFileSync(conf)));
240 assert.equal(await facts(`${base}/nginx.conf`), before, "metadata of the replaced /etc file preserved");
241 const downloads = fs.mkdtempSync(path.join(work, "dl-")); downloadTarget = downloads;
242 const download = await transfers.planDownload("fm", "xfer", [`${base}/secret.conf`], { sudo: true });
243 await transfers.start("fm", download.planId, { sudo: true, sudoPassword });
244 await until(() => ["completed", "failed", "interrupted"].includes(lastJob()?.state), 120000, "privileged download");
245 assert.equal(lastJob().state, "completed", JSON.stringify(lastJob().error));
246 assert.equal(fs.readFileSync(path.join(downloads, "secret.conf"), "utf8"), "secret=1\n");
247 const regular = await transfers.planDownload("fm", "xfer", [`${userDir}/big.bin`]);
248 await transfers.start("fm", regular.planId);
249 await until(() => ["completed", "failed"].includes(lastJob()?.state), 300000, "download");
250 assert.equal(sha(fs.readFileSync(path.join(downloads, "big.bin"))), bigHash);
251 assert.equal(lastJob().verification.matched, true);
252 });
254 await t.test("pause/resume continues at the confirmed offset", async () => {
255 await asRoot(`rm -f "$1"`, [`${userDir}/big.bin`]);
256 uploadSelection = [big];
257 const plan = await transfers.planUpload("fm", "xfer", userDir);
258 await transfers.start("fm", plan.planId);
259 await until(() => lastJob()?.transferred > 8 * 1024 * 1024, 120000, "progress");
260 transfers.pause("fm", [lastJob().id]);
261 await until(() => lastJob().state === "paused", 30000, "paused");
262 const offset = lastJob().transferred;
263 const part = `${userDir}/.big.bin.chj-part-${lastJob().id}`;
264 const size = await asRoot(`stat -c %s "$1"`, [part]);
265 await sleep(800);
266 assert.equal(await asRoot(`stat -c %s "$1"`, [part]), size, "no data written while paused");
267 transfers.resume("fm", [lastJob().id]);
268 await until(() => ["completed", "failed"].includes(lastJob().state), 300000, "resume");
269 assert.equal(lastJob().state, "completed", JSON.stringify(lastJob().error));
270 assert.equal(await asRoot(`sha256sum "$1" | cut -d' ' -f1`, [`${userDir}/big.bin`]), bigHash);
271 t.diagnostic(`paused at ${offset}, average ${lastJob().averageSpeed} B/s`);
272 });
274 await t.test("SSH outage of the transfer connection; reconnect resumes", async () => {
275 // 512 MiB sparse file: long enough on a LAN for the outage to hit the running data transfer.
276 const outage = path.join(work, "outage.bin"); fs.closeSync(fs.openSync(outage, "w")); fs.truncateSync(outage, 512 * 1024 * 1024);
277 const outageHash = sha(fs.readFileSync(outage));
278 uploadSelection = [outage];
279 const plan = await transfers.planUpload("fm", "xfer", userDir);
280 await transfers.start("fm", plan.planId);
281 await until(() => lastJob()?.transferred > 8 * 1024 * 1024, 120000, "progress");
282 const port = manager.sessions.get("xfer").client._sock.localPort;
283 // Kill only the server side of this one TCP connection (other sessions stay up).
284 await asRoot(String.raw`pids=$(ss -Htnp 2>/dev/null | awk -v p=":$1" '$5 ~ p"$"' | grep -o 'pid=[0-9]*' | cut -d= -f2)
285[ -n "$pids" ] || pids=$(netstat -tnp 2>/dev/null | awk -v p=":$1" '$5 ~ p"$" {split($7, a, "/"); print a[1]}')
286for p in $pids; do pids="$pids $(pgrep -P "$p" 2>/dev/null)"; done
287[ -n "$pids" ] && kill -KILL $pids; echo "port=$1 pids=" $pids`, [String(port)]).then((out) => t.diagnostic(`outage: ${out} at ${lastJob().transferred}`));
288 await until(() => ["interrupted", "reconnecting", "completed"].includes(lastJob().state), 60000, "interruption");
289 assert.notEqual(lastJob().state, "completed", "interrupted before the end");
290 const offset = lastJob().transferred;
291 assert.ok(offset < 512 * 1024 * 1024, "outage hit the data transfer");
292 assert.equal(await asRoot(`test -e "$1" && echo yes || echo no`, [`${userDir}/outage.bin`]), "no", "partial data never published");
293 await connect("xfer");
294 await until(() => ["completed", "failed"].includes(lastJob().state), 300000, "auto resume");
295 assert.equal(lastJob().state, "completed", JSON.stringify(lastJob().error));
296 assert.equal(await asRoot(`sha256sum "$1" | cut -d' ' -f1`, [`${userDir}/outage.bin`]), outageHash);
297 await asRoot(`rm -f "$1"`, [`${userDir}/outage.bin`]);
298 t.diagnostic(`interrupted at ${offset}`);
299 });
301 await t.test("application restart offers resume; cancel removes only partial data", async () => {
302 await asRoot(`rm -f "$1"`, [`${userDir}/big.bin`]);
303 uploadSelection = [big];
304 const plan = await transfers.planUpload("fm", "xfer", userDir);
305 await transfers.start("fm", plan.planId);
306 await until(() => lastJob()?.transferred > 8 * 1024 * 1024, 120000, "progress");
307 const id = lastJob().id;
308 transfers.shutdown();
309 transfers = new TransferManager({ sessionManager: manager, remoteFileService: files, storageRoot: storage, retryDelaysMs: [500, 1000, 2000] });
310 const restored = transfers.list("fm").jobs.find((entry) => entry.id === id);
311 assert.equal(restored.state, "interrupted"); assert.equal(restored.error.code, "APP_RESTARTED");
312 transfers.resume("fm", [id]);
313 await until(() => ["completed", "failed"].includes(transfers.list("fm").jobs.find((entry) => entry.id === id).state), 300000, "resume after restart");
314 assert.equal(await asRoot(`sha256sum "$1" | cut -d' ' -f1`, [`${userDir}/big.bin`]), bigHash);
315 await asRoot(`printf 'keep\\n' > "$1"; chown "$2" "$1"`, [`${userDir}/big.bin`, vmUser]);
316 const cancel = await transfers.planUpload("fm", "xfer", userDir);
317 await transfers.start("fm", cancel.planId, { conflict: "overwrite" });
318 await until(() => lastJob()?.transferred > 4 * 1024 * 1024, 120000, "progress");
319 const cancelId = lastJob().id;
320 await transfers.cancel("fm", [cancelId]);
321 await until(() => lastJob().state === "cancelled", 30000, "cancelled");
322 assert.equal(await read(`${userDir}/big.bin`), "keep");
323 assert.equal(await asRoot(`test -e "$1" && echo yes || echo no`, [`${userDir}/.big.bin.chj-part-${cancelId}`]), "no");
324 });
326 await t.test("several files concurrently; a changed source is detected", async () => {
327 const dir = path.join(work, "many"); fs.mkdirSync(dir);
328 for (let index = 0; index < 5; index += 1) fs.writeFileSync(path.join(dir, `f${index}.bin`), crypto.randomBytes((index + 1) * 2 * 1024 * 1024));
329 uploadSelection = [dir];
330 transfers.updateSettings("fm", { maxConcurrent: 3 });
331 const plan = await transfers.planUpload("fm", "xfer", userDir);
332 let peak = 0; const listener = (update) => { peak = Math.max(peak, update.summary.active); };
333 transfers.on("update", listener);
334 await transfers.start("fm", plan.planId);
335 await until(() => transfers.list("fm").jobs.filter((entry) => entry.batchId === plan.planId).every((entry) => entry.state === "completed"), 300000, "batch");
336 transfers.removeListener("update", listener);
337 assert.ok(peak >= 2 && peak <= 3, `concurrency ${peak}`);
338 const changing = path.join(work, "changing.bin"); fs.copyFileSync(big, changing);
339 uploadSelection = [changing];
340 const changed = await transfers.planUpload("fm", "xfer", userDir);
341 await transfers.start("fm", changed.planId);
342 await until(() => lastJob()?.transferred > 4 * 1024 * 1024, 120000, "progress");
343 transfers.pause("fm", [lastJob().id]);
344 await until(() => lastJob().state === "paused", 30000, "paused");
345 fs.appendFileSync(changing, "changed");
346 transfers.resume("fm", [lastJob().id]);
347 await until(() => lastJob().state === "failed", 60000, "failed");
348 assert.equal(lastJob().error.code, "FILE_SOURCE_CHANGED");
349 await transfers.cancel("fm", [lastJob().id]);
350 });
351});
SHA-25690d10a5b4cde5c6c40cc75e0a1e9d906c3bdb8822262a5bd60ae147a9842e8ea

CH-J Proprietary Software License 1.14 · Sursa este furnizată în cadrul CH-J Proprietary Software License 1.14. Disponibilitatea sa nu modifică termenii licenţei sau nu acordă drepturi suplimentare.

Archive SHA-256a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a
Fișiere