Compare commits

...
Author SHA1 Message Date
bincxz 21dd4ca0c8 test(sftp): cover EISDIR through local download publication 2026-09-27 14:27:41 +08:00
netcatty-bot cc433e7f1e fix(#3483): automated AI fix 2026-09-22 06:44:22 +00:00
3 changed files with 90 additions and 4 deletions

No files matched your search

+4 -1
View File
@@ -16,7 +16,10 @@ async function publishLocalFileExclusive(source, target, assertNotCancelled = ()
const linkedStat = await fs.promises.lstat(source);
return { dev: linkedStat.dev, ino: linkedStat.ino, size: linkedStat.size };
} catch (error) {
if (!["ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EPERM", "EACCES", "EXDEV"].includes(error?.code)) throw error;
// EISDIR is the misleading libuv mapping for Win32 ERROR_INVALID_FUNCTION
// when the volume has no hard-link support (exFAT/FAT32); the source here
// is always a regular prepared file, never a directory (nodejs/node#65817).
if (!["ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EPERM", "EACCES", "EXDEV", "EISDIR"].includes(error?.code)) throw error;
}
let input;
@@ -0,0 +1,83 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const tempDirBridge = require("./tempDirBridge.cjs");
const { publishLocalFileExclusive } = require("./localFilePublish.cjs");
function makeTempDir(prefix) {
return fs.mkdtempSync(`${tempDirBridge.getTempFilePath(prefix)}-`);
}
function stubLink(impl) {
const original = fs.promises.link;
fs.promises.link = impl;
return () => { fs.promises.link = original; };
}
test("publishLocalFileExclusive falls back to copy when hardlink fails with EISDIR", async () => {
const dir = makeTempDir("netcatty-publish-eisdir-");
try {
const source = path.join(dir, "staged");
const target = path.join(dir, "target");
fs.writeFileSync(source, "hello hardlink fallback");
const restore = stubLink(async () => {
// libuv maps Win32 ERROR_INVALID_FUNCTION on exFAT/FAT32 to EISDIR.
const error = new Error("EISDIR: illegal operation on a directory, link 'src' -> 'dest'");
error.code = "EISDIR";
throw error;
});
try {
const identity = await publishLocalFileExclusive(source, target);
const stat = fs.lstatSync(target);
assert.equal(fs.readFileSync(target, "utf8"), "hello hardlink fallback");
assert.equal(stat.isFile(), true);
assert.equal(identity.dev, stat.dev);
assert.equal(identity.ino, stat.ino);
assert.equal(identity.size, stat.size);
// The prepared source still exists for the caller to unlink.
assert.equal(fs.existsSync(source), true);
} finally {
restore();
}
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test("publishLocalFileExclusive still hardlinks on volumes that support it", async () => {
const dir = makeTempDir("netcatty-publish-link-");
try {
const source = path.join(dir, "staged");
const target = path.join(dir, "target");
fs.writeFileSync(source, "hardlinked publish");
const identity = await publishLocalFileExclusive(source, target);
const [sourceStat, targetStat] = [fs.lstatSync(source), fs.lstatSync(target)];
assert.equal(sourceStat.ino, targetStat.ino);
assert.deepEqual(identity, { dev: targetStat.dev, ino: targetStat.ino, size: targetStat.size });
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test("publishLocalFileExclusive rethrows unexpected link errors", async () => {
const dir = makeTempDir("netcatty-publish-eexist-");
try {
const source = path.join(dir, "staged");
const target = path.join(dir, "target");
fs.writeFileSync(source, "unrelated failure");
const restore = stubLink(async () => {
const error = new Error("EEXIST: file already exists");
error.code = "EEXIST";
throw error;
});
try {
await assert.rejects(() => publishLocalFileExclusive(source, target), { code: "EEXIST" });
} finally {
restore();
}
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
@@ -51,8 +51,8 @@ test("an explicitly absent destination is never moved aside if another writer cr
assert.equal(fs.readFileSync(target, "utf8"), "concurrent");
});
for (const failCopy of [false, true]) {
test(`exclusive copy fallback ${failCopy ? "retains recovery files after a write failure" : "publishes complete bytes without hardlinks"}`, async (t) => {
for (const [failCopy, linkErrorCode] of [[false, "ENOTSUP"], [true, "ENOTSUP"], [false, "EISDIR"]]) {
test(`exclusive copy fallback on ${linkErrorCode} ${failCopy ? "retains recovery files after a write failure" : "publishes complete bytes without hardlinks"}`, async (t) => {
const root = fs.mkdtempSync(`${temp.getTempFilePath("publish-fallback")}-`);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const staged = path.join(root, "staged");
@@ -61,7 +61,7 @@ for (const failCopy of [false, true]) {
fs.writeFileSync(staged, payload);
fs.writeFileSync(target, "original");
const link = fs.promises.link;
fs.promises.link = async () => { throw Object.assign(new Error("hardlinks unavailable"), { code: "ENOTSUP" }); };
fs.promises.link = async () => { throw Object.assign(new Error("hardlinks unavailable"), { code: linkErrorCode }); };
t.after(() => { fs.promises.link = link; });
if (failCopy) {
const open = fs.promises.open;