Uverejnený zdroj
Stiahnuť zdroj ZIP
CH-J Server Manager
Prechádzať adresáre a súbory pre konkrétne vydanie aplikácie.
CH-J / Server Manager
377 súbory
core-20261019T111616Z-160a00bfcddc
Pridané: · Postavený:
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_LINE6
// (Linux only; read at runtime, never printed). Never point this at a production server. Everything is created7
// 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 and9
// no-sudo accounts, a full filesystem and a reboot of the SSH server; see filesIntegration.test.js (Docker).10
const test = require("node:test");11
const assert = require("node:assert/strict");12
const crypto = require("node:crypto");13
const fs = require("node:fs");14
const os = require("node:os");15
const path = require("node:path");16
const { execFileSync } = require("node:child_process");17
const { SessionManager } = require("../src/main/sessions/sessionManager");18
const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec");19
const { RemoteFileService } = require("../src/main/files/remoteFileService");20
const { TransferManager } = require("../src/main/files/transferManager");21
const { detectPlatform } = require("../src/main/sessions/platformDetect");23
const env = process.env;24
const enabled = Boolean(env.CHJ_VM_HOST && env.CHJ_VM_FINGERPRINT && env.CHJ_VM_KEY);25
const skip = enabled ? false : "Set CHJ_VM_* to run against an isolated test VM.";26
const readSecret = () => env.CHJ_VM_SUDO_PASSWORD || (env.CHJ_VM_SUDO_PASSWORD_FILE27
? 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() : "");28
const vmUser = env.CHJ_VM_USER || "ai";29
const host = env.CHJ_VM_HOST;30
const run = crypto.randomBytes(4).toString("hex");31
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));32
const sha = (data) => crypto.createHash("sha256").update(data).digest("hex");33
async 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); }36
}37
function 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));42
}44
test(`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 -e152
umask 022; mkdir "$1" "$1/sites-available" "$1/sites-enabled"; mkdir -m 0755 "$2"; chown "$3" "$2"153
printf 'worker_processes 1;\n' > "$1/nginx.conf"154
printf 'server { listen 80; }\n' > "$1/sites-available/example.conf"; ln -s ../sites-available/example.conf "$1/sites-enabled/example.conf"155
printf 'secret=1\n' > "$1/secret.conf"; chmod 0600 "$1/secret.conf"156
printf 'shared\n' > "$1/hard.conf"; ln "$1/hard.conf" "$1/hard-link.conf"157
python3 - "$1/nginx.conf" <<'PY'158
import os, struct, sys159
p = sys.argv[1]160
os.setxattr(p, 'user.chj', b'kept')161
entries = [(1, 6, 0xffffffff), (2, 4, 1), (4, 4, 0xffffffff), (16, 4, 0xffffffff), (32, 4, 0xffffffff)]162
try:163
os.setxattr(p, 'system.posix_acl_access', struct.pack('<I', 2) + b''.join(struct.pack('<HHI', *e) for e in entries)); print('acl')164
except OSError:165
print('no-acl')166
PY`, [base, userDir, vmUser]);167
t.diagnostic(`ACL on test filesystem: ${acl}`);168
const facts = (file) => asRoot(String.raw`python3 - "$1" <<'PY'169
import base64, os, stat, sys170
s = os.lstat(sys.argv[1]); keys = sorted(os.listxattr(sys.argv[1], follow_symlinks=False))171
print(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.')])172
PY`, [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]}')286
for p in $pids; do pids="$pids $(pgrep -P "$p" 2>/dev/null)"; done287
[ -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-256
90d10a5b4cde5c6c40cc75e0a1e9d906c3bdb8822262a5bd60ae147a9842e8eaCH-J Proprietary Software License 1.14 · Zdroj sa poskytuje v rámci CH-J Proprietary Software License 1.14. Jeho dostupnosť nemení licenčné podmienky ani neposkytuje dodatočné práva.
Archív SHA-256
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a