Skip to content

Commit

Permalink
feat(node-fs): add support for multiple dirs (#203)
Browse files Browse the repository at this point in the history
Co-authored-by: Pooya Parsa <[email protected]>
  • Loading branch information
Aareksio and pi0 authored Jan 12, 2024
1 parent be4b092 commit 912203e
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 42 deletions.
99 changes: 57 additions & 42 deletions src/storage/node-fs.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,79 @@
import type { Stats } from "node:fs";
import { resolve, parse, join } from "pathe";
import { createError } from "h3";
import { cachedPromise, getEnv } from "../utils";
import type { IPXStorage } from "../types";

export type NodeFSSOptions = {
dir?: string;
dir?: string | string[];
maxAge?: number;
};

export function ipxFSStorage(_options: NodeFSSOptions = {}): IPXStorage {
const rootDir = resolve(_options.dir || getEnv("IPX_FS_DIR") || ".");
const dirs = resolveDirs(_options.dir);
const maxAge = _options.maxAge || getEnv("IPX_FS_MAX_AGE");

const _resolve = (id: string) => {
const resolved = join(rootDir, id);
if (!isValidPath(resolved) || !resolved.startsWith(rootDir)) {
const _getFS = cachedPromise(() =>
import("node:fs/promises").catch(() => {
throw createError({
statusCode: 403,
statusText: `IPX_FORBIDDEN_PATH`,
message: `Forbidden path: ${id}`,
statusCode: 500,
statusText: `IPX_FILESYSTEM_ERROR`,
message: `Failed to resolve filesystem module`,
});
}
return resolved;
};

const _getFS = cachedPromise(() => import("node:fs/promises"));

return {
name: "ipx:node-fs",
async getMeta(id) {
const fsPath = _resolve(id);
}),
);

let stats: Stats;
const resolveFile = async (id: string) => {
const fs = await _getFS();
for (const dir of dirs) {
const filePath = join(dir, id);
if (!isValidPath(filePath) || !filePath.startsWith(dir)) {
throw createError({
statusCode: 403,
statusText: `IPX_FORBIDDEN_PATH`,
message: `Forbidden path: ${id}`,
});
}
try {
const fs = await _getFS();
stats = await fs.stat(fsPath);
const stats = await fs.stat(filePath);
if (!stats.isFile()) {
// Keep looking in other dirs we are looking for a file!
continue;
}
return {
stats,
read: () => fs.readFile(filePath),
};
} catch (error: any) {
throw error.code === "ENOENT"
? createError({
statusCode: 404,
statusText: `IPX_FILE_NOT_FOUND`,
message: `File not found: ${id}`,
})
: createError({
statusCode: 403,
statusText: `IPX_FORBIDDEN_FILE`,
message: `File access forbidden: (${error.code}) ${id}`,
});
}
if (!stats.isFile()) {
if (error.code === "ENOENT") {
// Keep looking in other dirs
continue;
}
throw createError({
statusCode: 400,
statusText: `IPX_INVALID_FILE`,
message: `Path should be a file: ${id}`,
statusCode: 403,
statusText: `IPX_FORBIDDEN_FILE`,
message: `Cannot access file: ${id}`,
});
}
}
throw createError({
statusCode: 404,
statusText: `IPX_FILE_NOT_FOUND`,
message: `File not found: ${id}`,
});
};

return {
name: "ipx:node-fs",
async getMeta(id) {
const { stats } = await resolveFile(id);
return {
mtime: stats.mtime,
maxAge,
};
},
async getData(id) {
const fsPath = _resolve(id);
const fs = await _getFS();
const contents = await fs.readFile(fsPath);
return contents;
const { read } = await resolveFile(id);
return read();
},
};
}
Expand All @@ -85,3 +92,11 @@ function isValidPath(fp: string) {
}
return true;
}

function resolveDirs(dirs?: string | string[]) {
if (!dirs || !Array.isArray(dirs)) {
const dir = resolve(dirs || getEnv("IPX_FS_DIR") || ".");
return [dir];
}
return dirs.map((dirs) => resolve(dirs));
}
Binary file added test/assets2/bliss.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/assets2/unjs.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 53 additions & 0 deletions test/fs-dirs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { fileURLToPath } from "node:url";
import { describe, it, expect, beforeAll } from "vitest";
import { IPX, createIPX, ipxFSStorage } from "../src";

describe("ipx: fs with multiple dirs", () => {
let ipx: IPX;

beforeAll(() => {
ipx = createIPX({
storage: ipxFSStorage({
dir: ["assets", "assets2"].map((d) =>
fileURLToPath(new URL(d, import.meta.url)),
),
}),
});
});

it("local file: 1st layer", async () => {
const source = await ipx("giphy.gif");
const { data, format } = await source.process();
expect(data).toBeInstanceOf(Buffer);
expect(format).toBe("gif");
});

it("local file: 2nd layer", async () => {
const source = await ipx("unjs.jpg");
const { data, format } = await source.process();
expect(data).toBeInstanceOf(Buffer);
expect(format).toBe("jpeg");
});

it("local file: priority", async () => {
const source = await ipx("bliss.jpg");
const { data, format, meta } = await source.process();
expect(data).toBeInstanceOf(Buffer);
expect(format).toBe("jpeg");
expect(meta?.height).toBe(2160);
});

it("error: not found", async () => {
const source = await ipx("unknown.png");
await expect(() => source.process()).rejects.toThrowError(
"File not found: /unknown.png",
);
});

it("error: forbidden path", async () => {
const source = await ipx("*.png");
await expect(() => source.process()).rejects.toThrowError(
"Forbidden path: /*.png",
);
});
});

0 comments on commit 912203e

Please sign in to comment.