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
37export interface OpenMediaFile {
38 readonly handle: NodeFSP.FileHandle;
39 readonly info: NodeFS.BigIntStats;
40}
41
42const realpathLikeFileSystem = (filePath: string) =>
43 new Promise<string>((resolve, reject) => {
44 NodeFS.realpath(filePath, (error, resolved) => (error ? reject(error) : resolve(resolved)));
45 });
46
47export const openMediaFile = Effect.fn("openMediaFile")(function* (
48 filePath: string,
49 identity?: { readonly device: string; readonly inode: string },
50) {
51 return yield* Effect.acquireRelease(
52 Effect.tryPromise({
53 try: async () => {
54 const before = await NodeFSP.lstat(filePath, { bigint: true });
55 if (!before.isFile() || before.ino === 0n) return null;
56 if (
57 identity &&
58 (before.dev.toString() !== identity.device || before.ino.toString() !== identity.inode)
59 ) {
60 return null;
61 }
62
63 // Windows lacks these flags; the descriptor/path identity checks still apply.
64 const handle = await NodeFSP.open(
65 filePath,
66 NodeFS.constants.O_RDONLY |
67 (NodeFS.constants.O_NOFOLLOW ?? 0) |
68 (NodeFS.constants.O_NONBLOCK ?? 0),
69 );
70 let accepted = false;
71 try {
72 const info = await handle.stat({ bigint: true });
73 if (!info.isFile() || info.dev !== before.dev || info.ino !== before.ino) return null;
74 if (
75 identity &&
76 (info.dev.toString() !== identity.device || info.ino.toString() !== identity.inode)
77 ) {
78 return null;
79 }
80 // Match FileSystem.realPath's JS behavior; native realpath expands Windows 8.3 names too.
81 if ((await realpathLikeFileSystem(filePath)) !== filePath) return null;
82 const after = await NodeFSP.lstat(filePath, { bigint: true });
83 if (!after.isFile() || info.dev !== after.dev || info.ino !== after.ino) return null;
84 accepted = true;
85 return { handle, info } satisfies OpenMediaFile;
86 } finally {
87 if (!accepted) await handle.close();
88 }
89 },
90 catch: (cause) => new MediaFileOpenError({ path: filePath, cause }),
91 }),
92 (file) => (file ? Effect.promise(() => file.handle.close()) : Effect.void),
93 );
94});
95
96export const readMediaFileHeader = (filePath: string, file: OpenMediaFile, byteCount: number) =>
97 Effect.tryPromise({
98 try: async () => {
99 const buffer = new Uint8Array(byteCount);
100 const { bytesRead } = await file.handle.read(buffer, 0, byteCount, 0);
101 return buffer.subarray(0, bytesRead);
102 },
103 catch: (cause) => new MediaFileReadError({ path: filePath, cause }),
104 });
105
106export const statMediaFile = Effect.fn("statMediaFile")(function* (
107 filePath: string,
108 file: OpenMediaFile,
109) {
110 return yield* Effect.tryPromise({
111 try: () => file.handle.stat({ bigint: true }),
112 catch: (cause) => new MediaFileStatError({ path: filePath, cause }),
113 });
114});
115
116export const streamMediaFile = (file: OpenMediaFile, offset: bigint, bytesToRead: bigint) => {
117 const start = Number(offset);
118 const end = Number(offset + bytesToRead - 1n);
119 if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start) {
120 return null;
121 }
122 return NodeStream.fromReadable<Uint8Array>({
123 evaluate: () =>
124 file.handle.createReadStream({
125 autoClose: false,
126 start,
127 end,
128 }),
129 });
130};