1// @effect-diagnostics nodeBuiltinImport:off - FileSystem does not expose no-follow
2// or non-blocking open flags, and the response must keep the validated descriptor.
3import * as NodeFS from "node:fs";
4import * as NodeFSP from "node:fs/promises";
5
6import * as NodeStream from "@effect/platform-node/NodeStream";
7import * as Effect from "effect/Effect";
8import * as Schema from "effect/Schema";
9
10class MediaFileOpenError extends Schema.TaggedError<MediaFileOpenError>()("MediaFileOpenError", {
11 path: Schema.String,
12 cause: Schema.Defect(),
13}) {
14 override get message(): string {
15 return `Failed to open media file '${this.path}'.`;
16 }
17}
18
19class MediaFileReadError extends Schema.TaggedError<MediaFileReadError>()("MediaFileReadError", {
20 path: Schema.String,
21 cause: Schema.Defect(),
22}) {
23 override get message(): string {
24 return `Failed to read media file '${this.path}'.`;
25 }
26}
27
28class MediaFileStatError extends Schema.TaggedError<MediaFileStatError>()("MediaFileStatError", {
29 path: Schema.String,
30 cause: Schema.Defect(),
31}) {
32 override get message(): string {
33 return `Failed to read metadata for media file '${this.path}'.`;
34 }
35}
36
37/** Holds the file identity and descriptor for one HTTP request, never a copy of its bytes. */
38export interface OpenMediaFile {
39 readonly handle: NodeFSP.FileHandle;
40 readonly info: NodeFS.BigIntStats;
41}
42
43const realpathLikeFileSystem = (filePath: string) =>
44 new Promise<string>((resolve, reject) => {
45 NodeFS.realpath(filePath, (error, resolved) => (error ? reject(error) : resolve(resolved)));
46 });
47
48/** Opens a canonical media path once. Replacements cannot change the response's source. */
49export const openMediaFile = Effect.fn("openMediaFile")(function* (
50 filePath: string,
51 identity?: { readonly device: string; readonly inode: string },
52) {
53 return yield* Effect.acquireRelease(
54 Effect.tryPromise({
55 try: async () => {
56 const before = await NodeFSP.lstat(filePath, { bigint: true });
57 if (!before.isFile() || before.ino === 0n) return null;
58 if (
59 identity &&
60 (before.dev.toString() !== identity.device || before.ino.toString() !== identity.inode)
61 ) {
62 return null;
63 }
64
65 // Windows lacks these flags; the descriptor/path identity checks still apply.
66 const handle = await NodeFSP.open(
67 filePath,
68 NodeFS.constants.O_RDONLY |
69 (NodeFS.constants.O_NOFOLLOW ?? 0) |
70 (NodeFS.constants.O_NONBLOCK ?? 0),
71 );
72 let accepted = false;
73 try {
74 const info = await handle.stat({ bigint: true });
75 if (!info.isFile() || info.dev !== before.dev || info.ino !== before.ino) return null;
76 if (
77 identity &&
78 (info.dev.toString() !== identity.device || info.ino.toString() !== identity.inode)
79 ) {
80 return null;
81 }
82 // Callers canonicalise with Effect's FileSystem.realPath, which is
83 // Node's JS realpath. fs/promises.realpath is the native binding and
84 // on Windows also expands 8.3 short names, so a path that is already
85 // canonical by the caller's rules would still look swapped here.
86 if ((await realpathLikeFileSystem(filePath)) !== filePath) return null;
87 const after = await NodeFSP.lstat(filePath, { bigint: true });
88 if (!after.isFile() || info.dev !== after.dev || info.ino !== after.ino) return null;
89 accepted = true;
90 return { handle, info } satisfies OpenMediaFile;
91 } finally {
92 if (!accepted) await handle.close();
93 }
94 },
95 catch: (cause) => new MediaFileOpenError({ path: filePath, cause }),
96 }),
97 (file) => (file ? Effect.promise(() => file.handle.close()) : Effect.void),
98 );
99});
100
101/** Reads the leading bytes of an already-validated media file, never past the end. */
102export const readMediaFileHeader = (filePath: string, file: OpenMediaFile, byteCount: number) =>
103 Effect.tryPromise({
104 try: async () => {
105 const buffer = new Uint8Array(byteCount);
106 const { bytesRead } = await file.handle.read(buffer, 0, byteCount, 0);
107 return buffer.subarray(0, bytesRead);
108 },
109 catch: (cause) => new MediaFileReadError({ path: filePath, cause }),
110 });
111
112export const statMediaFile = Effect.fn("statMediaFile")(function* (
113 filePath: string,
114 file: OpenMediaFile,
115) {
116 return yield* Effect.tryPromise({
117 try: () => file.handle.stat({ bigint: true }),
118 catch: (cause) => new MediaFileStatError({ path: filePath, cause }),
119 });
120});
121
122export const streamMediaFile = (file: OpenMediaFile, offset: bigint, bytesToRead: bigint) => {
123 const start = Number(offset);
124 const end = Number(offset + bytesToRead - 1n);
125 if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start) {
126 return null;
127 }
128 return NodeStream.fromReadable<Uint8Array>({
129 evaluate: () =>
130 file.handle.createReadStream({
131 autoClose: false,
132 start,
133 end,
134 }),
135 });
136};