11import { Readable } from "stream" ;
22import { ReadableStream } from "stream/web" ;
3- import {
4- createReadStream , promises as fs ,
5- } from "fs" ;
3+ import { createReadStream , createWriteStream , promises as fs } from "fs" ;
64import { tmpdir } from "os" ;
7- import { join } from "path" ;
8- import { createWriteStream } from "fs" ;
5+ import { join , basename } from "path" ;
96import { pipeline } from "stream/promises" ;
7+ import { v4 as uuidv4 } from "uuid" ;
8+ import mime from "mime-types" ;
109
1110export interface FileMetadata {
1211 size ?: number ;
@@ -23,13 +22,12 @@ export interface FileMetadata {
2322export 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
7279async 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
112119async 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