Skip to content

Commit 1ec900c

Browse files
js07jcortes
andauthored
Apply suggestions from code review
* Create and use safeStat function * Check for response.body when fetching file * Use uuid package to generate temp file name * Use mime-types package to lookup content type * Cleanup temp file on error Co-authored-by: Jorge Cortes <[email protected]>
1 parent 4d2678d commit 1ec900c

File tree

1 file changed

+48
-43
lines changed

1 file changed

+48
-43
lines changed

platform/lib/file-stream.ts

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { Readable } from "stream";
22
import { ReadableStream } from "stream/web";
3-
import {
4-
createReadStream, promises as fs,
5-
} from "fs";
3+
import { createReadStream, createWriteStream, promises as fs } from "fs";
64
import { tmpdir } from "os";
7-
import { join } from "path";
8-
import { createWriteStream } from "fs";
5+
import { join, basename } from "path";
96
import { pipeline } from "stream/promises";
7+
import { v4 as uuidv4 } from "uuid";
8+
import mime from "mime-types";
109

1110
export interface FileMetadata {
1211
size?: number;
@@ -23,13 +22,12 @@ export interface FileMetadata {
2322
export async function getFileStream(pathOrUrl: string): Promise<Readable> {
2423
if (isUrl(pathOrUrl)) {
2524
const response = await fetch(pathOrUrl);
26-
if (!response.ok) {
25+
if (!response.ok || !response.body) {
2726
throw new Error(`Failed to fetch ${pathOrUrl}: ${response.status} ${response.statusText}`);
2827
}
2928
return Readable.fromWeb(response.body as ReadableStream<Uint8Array>);
3029
} else {
31-
// Check if file exists first (this will throw if file doesn't exist)
32-
await fs.stat(pathOrUrl);
30+
await safeStat(pathOrUrl);
3331
return createReadStream(pathOrUrl);
3432
}
3533
}
@@ -55,23 +53,32 @@ function isUrl(pathOrUrl: string): boolean {
5553
}
5654
}
5755

58-
async function getLocalFileStreamAndMetadata(filePath: string): Promise<{ stream: Readable; metadata: FileMetadata }> {
59-
const stats = await fs.stat(filePath);
56+
async function safeStat(path: string): Promise<Stats> {
57+
try {
58+
return await fs.stat(path);
59+
} catch {
60+
throw new Error(`File not found: ${path}`);
61+
}
62+
}
63+
64+
async function getLocalFileStreamAndMetadata(
65+
filePath: string
66+
): Promise<{ stream: Readable; metadata: FileMetadata }> {
67+
const stats = await safeStat(filePath);
68+
const contentType = mime.lookup(filePath) || undefined;
6069
const metadata: FileMetadata = {
6170
size: stats.size,
6271
lastModified: stats.mtime,
63-
name: filePath.split("/").pop() || filePath.split("\\").pop(),
72+
name: basename(filePath),
73+
contentType
6474
};
6575
const stream = createReadStream(filePath);
66-
return {
67-
stream,
68-
metadata,
69-
};
76+
return { stream, metadata };
7077
}
7178

7279
async function getRemoteFileStreamAndMetadata(url: string): Promise<{ stream: Readable; metadata: FileMetadata }> {
7380
const response = await fetch(url);
74-
if (!response.ok) {
81+
if (!response.ok || !response.body) {
7582
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
7683
}
7784

@@ -111,38 +118,36 @@ async function getRemoteFileStreamAndMetadata(url: string): Promise<{ stream: Re
111118

112119
async function downloadToTemporaryFile(response: Response, baseMetadata: FileMetadata): Promise<{ stream: Readable; metadata: FileMetadata }> {
113120
// Generate unique temporary file path
114-
const tempFileName = `file-stream-${Date.now()}-${Math.random().toString(36)
115-
.substring(2)}`;
121+
const tempFileName = `file-stream-${uuidv4()}`;
116122
const tempFilePath = join(tmpdir(), tempFileName);
117123
// Download to temporary file
118124
const fileStream = createWriteStream(tempFilePath);
119125
const webStream = Readable.fromWeb(response.body as ReadableStream<Uint8Array>);
120-
await pipeline(webStream, fileStream);
121-
// Get file stats
122-
const stats = await fs.stat(tempFilePath);
123-
const metadata: FileMetadata = {
124-
...baseMetadata,
125-
size: stats.size,
126-
};
127-
128-
// Create a readable stream that cleans up the temp file when done
129-
const stream = createReadStream(tempFilePath);
126+
try {
127+
await pipeline(webStream, fileStream);
128+
const stats = await fs.stat(tempFilePath);
129+
const metadata: FileMetadata = {
130+
...baseMetadata,
131+
size: stats.size
132+
};
133+
const stream = createReadStream(tempFilePath);
130134

131-
// Clean up temp file when stream is closed or ends
132-
const cleanup = async () => {
133-
try {
134-
await fs.unlink(tempFilePath);
135-
} catch {
136-
// Ignore cleanup errors (file might already be deleted)
137-
}
138-
};
135+
const cleanup = async () => {
136+
try {
137+
await fs.unlink(tempFilePath);
138+
} catch {
139+
// Ignore cleanup errors
140+
}
141+
};
139142

140-
stream.on("close", cleanup);
141-
stream.on("end", cleanup);
142-
stream.on("error", cleanup);
143+
stream.on("close", cleanup);
144+
stream.on("end", cleanup);
145+
stream.on("error", cleanup);
143146

144-
return {
145-
stream,
146-
metadata,
147-
};
147+
return { stream, metadata };
148+
} catch (err) {
149+
// Cleanup on error
150+
try { await fs.unlink(tempFilePath); } catch {}
151+
throw err;
152+
}
148153
}

0 commit comments

Comments
 (0)