mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-09-24 15:49:09 +00:00
443202ba4e
* fix(sftp): instant folder-drop scan UI and drop full-tree pre-scan Restore interleaved folder transfers as the only non-compressed path, remove the folder pre-scan setting, and make local folder drops show a scanning task with live discovered counts via native listLocalTree (with cancel + progress). UI shows "done · found" instead of a fake grand-total percentage while discovery is still growing. * fix(sftp): make folder scan cancellation bridge-safe * fix(sftp): preserve path-only terminal drops * fix(terminal): accept native folder drop paths * fix(sftp): keep scan task cancellable during setup * fix(sftp): preserve scan failure state during setup errors * fix(sftp): keep folder scan task across retries * fix(sftp): cancel scan task immediately * fix(sftp): keep cancellation active through session setup * fix(sftp): settle failed drop upload retry * fix(sftp): preserve cancellation in final scan cleanup * fix(sftp): harden folder scan cancellation and conflicts * fix(sftp): finish Codex follow-ups on folder scan cancel UX - Abort sibling listLocalTree walks when one multi-root scan fails - Prefer ERR_*_CANCELLED over matching "cancel" in error text - Clear scanning phase on settle; only label live rows as Scanning - Omit directory metadata sizes from drop entries used for totals * fix(sftp): stream folder discovery into concurrent uploads Local path-backed folder drops now edge-scan and edge-transfer: listLocalTree streams entry batches while upload workers consume them, so the first files start before a 100k-file walk finishes. Cancel aborts the walk, clears the queue, and stops creating child tasks. Compressed uploads still materialize fully then pack. * fix(sftp): harden progressive folder UX, retry, and size-based uploads Align the SFTP panel with transfer-center progress, keep nested tree batches until tree-end, honor pause during progressive enqueue, add low-cost retry for external drag-drop files, and drop whole-file upload digests for industry size-based resume (download live-log prefix checks unchanged). * fix(sftp): address multi-agent review on progressive upload and retry Gate progressive drops to path-backed folders only, prefer pool sessions on drag-drop retry with in-flight and cancel guards, soft-check upload mtime without fail-open contentVerifiedSeparately, skip multi-root pause HOL, and fail closed when the local tree stream misses tree-end. * fix(sftp): finish multi-root pause HOL and parent rollup guards Race-wait on all latched parents so resuming a non-head folder can proceed, and only roll progressive parents to completed after terminal failed status. * fix(sftp): address Codex progressive conflict and hang findings Add root-level conflict preflight to progressive folder uploads, wake all backpressure waiters, preserve cancel listeners across uploadEntriesDirect, and surface empty-directory mkdir failures.
146 lines
4.1 KiB
TypeScript
146 lines
4.1 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
captureDropPayload,
|
|
formatDropScanLabel,
|
|
getFileExtension,
|
|
hasFileExtension,
|
|
localTreeToDropEntries,
|
|
materializeDropEntries,
|
|
type LocalTreeListEntry,
|
|
} from "./sftpFileUtils.ts";
|
|
|
|
test("hasFileExtension identifies extensionless and dotted filenames", () => {
|
|
assert.equal(hasFileExtension("nginx"), false);
|
|
assert.equal(hasFileExtension("my-binary"), false);
|
|
assert.equal(hasFileExtension(".git"), false);
|
|
assert.equal(getFileExtension("nginx"), "file");
|
|
|
|
assert.equal(hasFileExtension("readme.txt"), true);
|
|
assert.equal(hasFileExtension(".bashrc"), false);
|
|
assert.equal(hasFileExtension("archive.tar.gz"), true);
|
|
});
|
|
|
|
test("formatDropScanLabel summarizes dropped root names", () => {
|
|
assert.equal(formatDropScanLabel([]), "Scanning files...");
|
|
assert.equal(formatDropScanLabel([{ name: "docs", isDirectory: true }]), "docs");
|
|
assert.equal(
|
|
formatDropScanLabel([
|
|
{ name: "a", isDirectory: true },
|
|
{ name: "b", isDirectory: true },
|
|
]),
|
|
"a, b",
|
|
);
|
|
assert.equal(
|
|
formatDropScanLabel([
|
|
{ name: "a", isDirectory: true },
|
|
{ name: "b", isDirectory: true },
|
|
{ name: "c", isDirectory: true },
|
|
]),
|
|
"a, b +1",
|
|
);
|
|
});
|
|
|
|
test("captureDropPayload reads webkit entries synchronously", () => {
|
|
const file = new File(["x"], "note.txt");
|
|
Object.defineProperty(file, "path", { value: "/tmp/note.txt" });
|
|
const dataTransfer = {
|
|
items: [{
|
|
kind: "file",
|
|
getAsFile: () => file,
|
|
webkitGetAsEntry: () => ({
|
|
name: "note.txt",
|
|
isFile: true,
|
|
isDirectory: false,
|
|
}),
|
|
}],
|
|
files: [file],
|
|
} as unknown as DataTransfer;
|
|
|
|
const payload = captureDropPayload(dataTransfer);
|
|
assert.equal(payload.roots.length, 1);
|
|
assert.equal(payload.roots[0].name, "note.txt");
|
|
assert.equal(payload.roots[0].isDirectory, false);
|
|
assert.equal(payload.roots[0].localPath, "/tmp/note.txt");
|
|
});
|
|
|
|
test("materializeDropEntries prefers listLocalTree for directory roots with paths", async () => {
|
|
const progress: Array<{ fileCount: number; directoryCount: number }> = [];
|
|
const tree: LocalTreeListEntry[] = [
|
|
{
|
|
localPath: "/tmp/project",
|
|
relativePath: "project",
|
|
type: "directory",
|
|
size: 0,
|
|
lastModified: 1,
|
|
},
|
|
{
|
|
localPath: "/tmp/project/src",
|
|
relativePath: "project/src",
|
|
type: "directory",
|
|
size: 0,
|
|
lastModified: 1,
|
|
},
|
|
{
|
|
localPath: "/tmp/project/src/main.ts",
|
|
relativePath: "project/src/main.ts",
|
|
type: "file",
|
|
size: 12,
|
|
lastModified: 2,
|
|
},
|
|
];
|
|
|
|
const entries = await materializeDropEntries(
|
|
{
|
|
roots: [{
|
|
name: "project",
|
|
isDirectory: true,
|
|
localPath: "/tmp/project",
|
|
}],
|
|
filesFallback: [],
|
|
},
|
|
{
|
|
listLocalTree: async (path, options) => {
|
|
assert.equal(path, "/tmp/project");
|
|
options?.onProgress?.({ fileCount: 1, directoryCount: 2, entryCount: 3 });
|
|
return tree;
|
|
},
|
|
onProgress: (p) => progress.push({ fileCount: p.fileCount, directoryCount: p.directoryCount }),
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(
|
|
entries.map((entry) => ({
|
|
relativePath: entry.relativePath,
|
|
isDirectory: entry.isDirectory,
|
|
localPath: entry.localPath,
|
|
size: entry.size,
|
|
})),
|
|
[
|
|
{ relativePath: "project", isDirectory: true, localPath: "/tmp/project", size: undefined },
|
|
{ relativePath: "project/src", isDirectory: true, localPath: "/tmp/project/src", size: undefined },
|
|
{
|
|
relativePath: "project/src/main.ts",
|
|
isDirectory: false,
|
|
localPath: "/tmp/project/src/main.ts",
|
|
size: 12,
|
|
},
|
|
],
|
|
);
|
|
assert.ok(progress.some((p) => p.fileCount === 1 && p.directoryCount === 2));
|
|
});
|
|
|
|
test("localTreeToDropEntries preserves file sizes without File handles", () => {
|
|
const entries = localTreeToDropEntries([{
|
|
localPath: "/tmp/a.txt",
|
|
relativePath: "a.txt",
|
|
type: "file",
|
|
size: 42,
|
|
lastModified: 9,
|
|
}]);
|
|
assert.equal(entries[0].file, null);
|
|
assert.equal(entries[0].localPath, "/tmp/a.txt");
|
|
assert.equal(entries[0].size, 42);
|
|
});
|