mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
fix(cli): snapshot staging folders no longer fail past 260 characters on Windows
This commit is contained in:
@@ -1,14 +1,7 @@
|
||||
import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
realpathSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { mkdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
import { CaptureDirRefusedError } from "./captureErrors.js";
|
||||
import { createStagingDir } from "../utils/stagingDir.js";
|
||||
|
||||
type WriteOptions = Parameters<typeof writeFileSync>[2];
|
||||
|
||||
@@ -25,7 +18,7 @@ function publishCaptureFileSync(
|
||||
data: string | NodeJS.ArrayBufferView,
|
||||
options: Exclude<WriteOptions, string>,
|
||||
): void {
|
||||
const stagingDir = mkdtempSync(join(dirname(path), ".capture-"));
|
||||
const stagingDir = createStagingDir(dirname(path), ".capture-");
|
||||
try {
|
||||
const staged = join(stagingDir, basename(path));
|
||||
writeCaptureFileSync(staged, data, { ...options, flag: "wx" });
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
* existing destination is replaced only after the download succeeds.
|
||||
*/
|
||||
|
||||
import { chmodSync, mkdirSync, mkdtempSync, renameSync, rmSync, statSync } from "node:fs";
|
||||
import { chmodSync, mkdirSync, renameSync, rmSync, statSync } from "node:fs";
|
||||
import { open } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import { resolveWritePath } from "@hyperframes/core/atomic-file";
|
||||
import { createStagingDir } from "../utils/stagingDir.js";
|
||||
|
||||
// The first request asks for PART_BYTES; a larger file is split into up to
|
||||
// CONNECTIONS ranged GETs. A server that ignores Range answers 200 and is streamed.
|
||||
@@ -78,7 +79,7 @@ export async function downloadToFile(
|
||||
// Writes through a caller-selected symlink, as before.
|
||||
const destination = resolveWritePath(destPath);
|
||||
const previous = statSync(destination, { throwIfNoEntry: false });
|
||||
const stage = mkdtempSync(join(dirname(destination), ".hf-download-"));
|
||||
const stage = createStagingDir(dirname(destination), ".hf-download-");
|
||||
const stagedFile = join(stage, "download");
|
||||
let bytes = 0;
|
||||
let firstError: unknown;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { lstatSync, chmodSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { lstatSync, chmodSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
import { mkdirWithinProject, realpath } from "@hyperframes/core";
|
||||
import { createStagingDir } from "../utils/stagingDir.js";
|
||||
|
||||
export { realProjectRoot as registryRoot } from "@hyperframes/core";
|
||||
|
||||
@@ -46,7 +47,7 @@ export function publishRegistryFile(
|
||||
const parent = realpath(dirname(path));
|
||||
assertContained(root, parent);
|
||||
const destination = resolve(parent, basename(path));
|
||||
const stage = mkdtempSync(join(parent, ".hf-install-"));
|
||||
const stage = createStagingDir(parent, ".hf-install-");
|
||||
try {
|
||||
const stagedFile = join(stage, "file");
|
||||
writeFileSync(stagedFile, bytes, { flag: "wx" });
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { rmSync, statSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, join } from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import * as fs from "node:fs";
|
||||
import { createStagingDir } from "./stagingDir.js";
|
||||
|
||||
vi.mock("node:fs", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof fs>();
|
||||
return {
|
||||
...original,
|
||||
mkdtempSync: vi.fn(() => {
|
||||
throw Object.assign(new Error("path too long"), { code: "ENOENT" });
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const roots: string[] = [];
|
||||
const root = () => {
|
||||
const made = join(tmpdir(), `hf-staging-${process.pid}-${roots.length}`);
|
||||
fs.mkdirSync(made, { recursive: true });
|
||||
roots.push(made);
|
||||
return made;
|
||||
};
|
||||
afterEach(() => {
|
||||
for (const dir of roots.splice(0)) rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("createStagingDir", () => {
|
||||
it("makes a fresh private directory without mkdtemp, which fails on long Windows paths", () => {
|
||||
const parent = root();
|
||||
const first = createStagingDir(parent, ".stage-");
|
||||
const second = createStagingDir(parent, ".stage-");
|
||||
expect(first).not.toBe(second);
|
||||
expect(basename(first)).toMatch(/^\.stage-[0-9a-f]{8}$/);
|
||||
expect(statSync(first).isDirectory()).toBe(true);
|
||||
if (process.platform !== "win32") expect(statSync(first).mode & 0o777).toBe(0o700);
|
||||
expect(fs.mkdtempSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("tries another name when one is taken, and gives up after five", () => {
|
||||
const parent = root();
|
||||
const taken = createStagingDir(parent, ".s-", () => "same");
|
||||
expect(taken).toBe(join(parent, ".s-same"));
|
||||
const names = ["same", "same", "next"];
|
||||
expect(createStagingDir(parent, ".s-", () => names.shift() ?? "x")).toBe(
|
||||
join(parent, ".s-next"),
|
||||
);
|
||||
expect(() => createStagingDir(parent, ".s-", () => "same")).toThrow(/EEXIST/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const TRIES = 5;
|
||||
|
||||
/** A fresh private directory in `parent`. Not mkdtemp: on Windows that call fails once its path passes 260 characters. */
|
||||
export function createStagingDir(
|
||||
parent: string,
|
||||
prefix: string,
|
||||
suffix = () => randomBytes(4).toString("hex"),
|
||||
): string {
|
||||
for (let attempt = 1; ; attempt++) {
|
||||
const dir = join(parent, `${prefix}${suffix()}`);
|
||||
try {
|
||||
mkdirSync(dir, { mode: 0o700 });
|
||||
return dir;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "EEXIST" || attempt >= TRIES) throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user