373 lines
10 KiB
TypeScript
373 lines
10 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
UploadController,
|
|
startUploadScanningTask,
|
|
uploadEntriesDirect,
|
|
uploadFromDataTransfer,
|
|
uploadFromFileList,
|
|
} from "../../lib/uploadService.ts";
|
|
|
|
function createDataTransfer(files: File[]): DataTransfer {
|
|
return {
|
|
items: { length: 0 },
|
|
files,
|
|
} as unknown as DataTransfer;
|
|
}
|
|
|
|
function createDataTransferWithNullEntries(files: File[]): DataTransfer {
|
|
const items = files.map((file) => ({
|
|
kind: "file",
|
|
getAsFile: () => file,
|
|
webkitGetAsEntry: () => null,
|
|
}));
|
|
return {
|
|
items,
|
|
files,
|
|
} as unknown as DataTransfer;
|
|
}
|
|
|
|
test("upload scanning task can be shown and cancelled before transfers start", () => {
|
|
const events: string[] = [];
|
|
const scanningTask = startUploadScanningTask(
|
|
{
|
|
onScanningStart: (taskId) => events.push(`start:${taskId}`),
|
|
onScanningEnd: (taskId) => events.push(`end:${taskId}`),
|
|
onTaskCancelled: (taskId) => events.push(`cancel:${taskId}`),
|
|
},
|
|
"scan-folder-1",
|
|
);
|
|
|
|
assert.equal(scanningTask.isOpen(), true);
|
|
scanningTask.cancel();
|
|
scanningTask.complete();
|
|
|
|
assert.equal(scanningTask.isOpen(), false);
|
|
assert.deepEqual(events, ["start:scan-folder-1", "cancel:scan-folder-1"]);
|
|
});
|
|
|
|
test("clears the scanning placeholder when every dropped file is skipped by conflict resolution", async () => {
|
|
const events: string[] = [];
|
|
const file = new File(["local"], "conflict.txt", { lastModified: 1234 });
|
|
|
|
const results = await uploadFromDataTransfer(
|
|
createDataTransfer([file]),
|
|
{
|
|
targetPath: "/target",
|
|
sftpId: null,
|
|
isLocal: true,
|
|
bridge: {
|
|
mkdirSftp: async () => {},
|
|
statLocal: async () => ({ type: "file", size: 10, lastModified: 1000 }),
|
|
writeLocalFile: async () => {
|
|
throw new Error("skipped conflicts should not upload");
|
|
},
|
|
},
|
|
joinPath: (base, name) => `${base}/${name}`,
|
|
callbacks: {
|
|
onScanningStart: () => events.push("scan:start"),
|
|
onScanningEnd: () => events.push("scan:end"),
|
|
onTaskCreated: () => events.push("task:create"),
|
|
},
|
|
resolveConflict: async () => "skip",
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(results, [
|
|
{ fileName: "conflict.txt", success: false, cancelled: true },
|
|
]);
|
|
assert.deepEqual(events, ["scan:start", "scan:end"]);
|
|
});
|
|
|
|
test("uploads DataTransfer files when entry extraction returns no entries", async () => {
|
|
const file = new File(["picked"], "picked.txt", { lastModified: 1234 });
|
|
const uploadedPaths: string[] = [];
|
|
|
|
const results = await uploadFromDataTransfer(
|
|
createDataTransferWithNullEntries([file]),
|
|
{
|
|
targetPath: "/target",
|
|
sftpId: "sftp-1",
|
|
isLocal: false,
|
|
bridge: {
|
|
mkdirSftp: async () => {},
|
|
writeSftpBinary: async (_sftpId, path) => {
|
|
uploadedPaths.push(path);
|
|
},
|
|
},
|
|
joinPath: (base, name) => `${base}/${name}`,
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(uploadedPaths, ["/target/picked.txt"]);
|
|
assert.deepEqual(results, [
|
|
{ fileName: "picked.txt", success: true },
|
|
]);
|
|
});
|
|
|
|
test("uploads picked folder files with their relative directory structure", async () => {
|
|
const file = new File(["nested"], "file.txt", { lastModified: 1234 });
|
|
Object.defineProperty(file, "webkitRelativePath", {
|
|
value: "folder/sub/file.txt",
|
|
});
|
|
const madeDirs: string[] = [];
|
|
const uploadedPaths: string[] = [];
|
|
|
|
const results = await uploadFromFileList(
|
|
[file],
|
|
{
|
|
targetPath: "/target",
|
|
sftpId: "sftp-1",
|
|
isLocal: false,
|
|
bridge: {
|
|
mkdirSftp: async (_sftpId, path) => {
|
|
madeDirs.push(path);
|
|
},
|
|
writeSftpBinary: async (_sftpId, path) => {
|
|
uploadedPaths.push(path);
|
|
},
|
|
},
|
|
joinPath: (base, name) => `${base}/${name}`,
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(madeDirs, ["/target/folder", "/target/folder/sub"]);
|
|
assert.deepEqual(uploadedPaths, ["/target/folder/sub/file.txt"]);
|
|
assert.deepEqual(results, [
|
|
{ fileName: "folder/sub/file.txt", success: true },
|
|
]);
|
|
});
|
|
|
|
test("does not replace an existing directory when uploading a same-named file", async () => {
|
|
const file = new File(["local"], "dddd", { lastModified: 1234 });
|
|
const deletedPaths: string[] = [];
|
|
const uploadedPaths: string[] = [];
|
|
|
|
const results = await uploadFromFileList(
|
|
[file],
|
|
{
|
|
targetPath: "/target",
|
|
sftpId: "sftp-1",
|
|
isLocal: false,
|
|
bridge: {
|
|
mkdirSftp: async () => {},
|
|
statSftp: async (_sftpId, path) =>
|
|
path === "/target/dddd"
|
|
? { type: "directory", size: 0, lastModified: 1000 }
|
|
: null,
|
|
deleteSftp: async (_sftpId, path) => {
|
|
deletedPaths.push(path);
|
|
},
|
|
writeSftpBinary: async (_sftpId, path) => {
|
|
uploadedPaths.push(path);
|
|
},
|
|
},
|
|
joinPath: (base, name) => `${base}/${name}`,
|
|
resolveConflict: async () => "replace",
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(deletedPaths, []);
|
|
assert.deepEqual(uploadedPaths, []);
|
|
assert.equal(results.length, 1);
|
|
assert.equal(results[0].fileName, "dddd");
|
|
assert.equal(results[0].success, false);
|
|
assert.match(results[0].error ?? "", /directory/i);
|
|
});
|
|
|
|
test("counts apply-to-all upload conflicts by incoming and existing type", async () => {
|
|
const files = [
|
|
new File(["local"], "existing-file", { lastModified: 1234 }),
|
|
new File(["local"], "existing-directory", { lastModified: 1234 }),
|
|
];
|
|
const conflictCounts: number[] = [];
|
|
|
|
const results = await uploadFromFileList(
|
|
files,
|
|
{
|
|
targetPath: "/target",
|
|
sftpId: "sftp-1",
|
|
isLocal: false,
|
|
bridge: {
|
|
mkdirSftp: async () => {},
|
|
statSftp: async (_sftpId, path) => {
|
|
if (path === "/target/existing-file") {
|
|
return { type: "file", size: 2, lastModified: 1000 };
|
|
}
|
|
if (path === "/target/existing-directory") {
|
|
return { type: "directory", size: 0, lastModified: 1000 };
|
|
}
|
|
return null;
|
|
},
|
|
writeSftpBinary: async () => {
|
|
throw new Error("skipped conflicts should not upload");
|
|
},
|
|
},
|
|
joinPath: (base, name) => `${base}/${name}`,
|
|
resolveConflict: async (conflict) => {
|
|
conflictCounts.push(conflict.applyToAllCount);
|
|
return "skip";
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(conflictCounts, [1, 1]);
|
|
assert.deepEqual(results, [
|
|
{ fileName: "existing-file", success: false, cancelled: true },
|
|
{ fileName: "existing-directory", success: false, cancelled: true },
|
|
]);
|
|
});
|
|
|
|
test("uploads path-backed clipboard files through stream transfer", async () => {
|
|
const transfers: Array<{ sourcePath: string; targetPath: string; totalBytes?: number }> = [];
|
|
const taskTotals: number[] = [];
|
|
|
|
const results = await uploadEntriesDirect(
|
|
[
|
|
{
|
|
file: null,
|
|
localPath: "/Users/me/Desktop/report.txt",
|
|
relativePath: "report.txt",
|
|
isDirectory: false,
|
|
size: 42,
|
|
},
|
|
],
|
|
{
|
|
targetPath: "/target",
|
|
sftpId: "sftp-1",
|
|
isLocal: false,
|
|
bridge: {
|
|
mkdirSftp: async () => {},
|
|
startStreamTransfer: async (payload) => {
|
|
transfers.push({
|
|
sourcePath: payload.sourcePath,
|
|
targetPath: payload.targetPath,
|
|
totalBytes: payload.totalBytes,
|
|
});
|
|
return { transferId: payload.transferId };
|
|
},
|
|
},
|
|
joinPath: (base, name) => `${base}/${name}`,
|
|
callbacks: {
|
|
onTaskCreated: (task) => taskTotals.push(task.totalBytes),
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(taskTotals, [42]);
|
|
assert.deepEqual(transfers, [
|
|
{
|
|
sourcePath: "/Users/me/Desktop/report.txt",
|
|
targetPath: "/target/report.txt",
|
|
totalBytes: 42,
|
|
},
|
|
]);
|
|
assert.deepEqual(results, [
|
|
{ fileName: "report.txt", success: true },
|
|
]);
|
|
});
|
|
|
|
test("copies path-backed clipboard files into local panes through stream transfer", async () => {
|
|
const transfers: Array<{ sourcePath: string; targetPath: string; targetType: string; totalBytes?: number }> = [];
|
|
|
|
const results = await uploadEntriesDirect(
|
|
[
|
|
{
|
|
file: null,
|
|
localPath: "/Users/me/Desktop/report.txt",
|
|
relativePath: "report.txt",
|
|
isDirectory: false,
|
|
size: 42,
|
|
},
|
|
],
|
|
{
|
|
targetPath: "/target",
|
|
sftpId: null,
|
|
isLocal: true,
|
|
bridge: {
|
|
mkdirLocal: async () => {},
|
|
startStreamTransfer: async (payload) => {
|
|
transfers.push({
|
|
sourcePath: payload.sourcePath,
|
|
targetPath: payload.targetPath,
|
|
targetType: payload.targetType,
|
|
totalBytes: payload.totalBytes,
|
|
});
|
|
return { transferId: payload.transferId };
|
|
},
|
|
},
|
|
joinPath: (base, name) => `${base}/${name}`,
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(transfers, [
|
|
{
|
|
sourcePath: "/Users/me/Desktop/report.txt",
|
|
targetPath: "/target/report.txt",
|
|
targetType: "local",
|
|
totalBytes: 42,
|
|
},
|
|
]);
|
|
assert.deepEqual(results, [
|
|
{ fileName: "report.txt", success: true },
|
|
]);
|
|
});
|
|
|
|
test("reports empty directory creation failures", async () => {
|
|
const madeDirs: string[] = [];
|
|
|
|
const results = await uploadEntriesDirect(
|
|
[
|
|
{ file: null, relativePath: "folder", isDirectory: true },
|
|
{ file: null, relativePath: "folder/empty", isDirectory: true },
|
|
],
|
|
{
|
|
targetPath: "/target",
|
|
sftpId: "sftp-1",
|
|
isLocal: false,
|
|
bridge: {
|
|
mkdirSftp: async (_sftpId, path) => {
|
|
madeDirs.push(path);
|
|
if (path.endsWith("/empty")) {
|
|
throw new Error("permission denied");
|
|
}
|
|
},
|
|
},
|
|
joinPath: (base, name) => `${base}/${name}`,
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(madeDirs, ["/target/folder", "/target/folder/empty"]);
|
|
assert.deepEqual(results, [
|
|
{ fileName: "folder/empty", success: false, error: "permission denied" },
|
|
]);
|
|
});
|
|
|
|
test("does not restart a direct upload that was already cancelled", async () => {
|
|
const controller = new UploadController();
|
|
await controller.cancel();
|
|
let mkdirCalled = false;
|
|
|
|
const results = await uploadEntriesDirect(
|
|
[{ file: null, relativePath: "folder", isDirectory: true }],
|
|
{
|
|
targetPath: "/target",
|
|
sftpId: "sftp-1",
|
|
isLocal: false,
|
|
bridge: {
|
|
mkdirSftp: async () => {
|
|
mkdirCalled = true;
|
|
},
|
|
},
|
|
joinPath: (base, name) => `${base}/${name}`,
|
|
},
|
|
controller,
|
|
);
|
|
|
|
assert.equal(mkdirCalled, false);
|
|
assert.deepEqual(results, [
|
|
{ fileName: "", success: false, cancelled: true },
|
|
]);
|
|
});
|