@@ -160,6 +160,21 @@ export class FileService implements FileSystemOperations {
160160
161161 const fileSize = parseInt ( statResult . data . stdout . trim ( ) , 10 ) ;
162162
163+ if ( Number . isNaN ( fileSize ) ) {
164+ return {
165+ success : false ,
166+ error : {
167+ message : `Failed to parse file size for '${ path } ': invalid stat output` ,
168+ code : ErrorCode . FILESYSTEM_ERROR ,
169+ details : {
170+ path,
171+ operation : Operation . FILE_READ ,
172+ stderr : `Unexpected stat output: ${ statResult . data . stdout } `
173+ } satisfies FileSystemContext
174+ }
175+ } ;
176+ }
177+
163178 // 4. Detect MIME type using file command
164179 const mimeCommand = `file --mime-type -b ${ escapedPath } ` ;
165180 const mimeResult = await this . sessionManager . executeInSession (
@@ -200,13 +215,7 @@ export class FileService implements FileSystemOperations {
200215 const mimeType = mimeResult . data . stdout . trim ( ) ;
201216
202217 // 5. Determine if file is binary based on MIME type
203- // Text MIME types: text/*, application/json, application/xml, application/javascript, etc.
204- const isBinary =
205- ! mimeType . startsWith ( 'text/' ) &&
206- ! mimeType . includes ( 'json' ) &&
207- ! mimeType . includes ( 'xml' ) &&
208- ! mimeType . includes ( 'javascript' ) &&
209- ! mimeType . includes ( 'x-empty' ) ;
218+ const isBinary = this . isBinaryMimeType ( mimeType ) ;
210219
211220 // 6. Read file with appropriate encoding
212221 // Respect user's encoding preference if provided, otherwise use MIME-based detection
@@ -1062,6 +1071,189 @@ export class FileService implements FileSystemOperations {
10621071 }
10631072 }
10641073
1074+ /**
1075+ * Get file metadata
1076+ * Optimized for scenarios where you need file characteristics
1077+ * (size, type, encoding) before processing, without the overhead
1078+ * of reading potentially large files. Used by readFileStreamOperation.
1079+ */
1080+ async getFileMetadata (
1081+ path : string ,
1082+ sessionId = 'default'
1083+ ) : Promise < ServiceResult < FileMetadata > > {
1084+ try {
1085+ // 1. Validate path for security
1086+ const validation = this . security . validatePath ( path ) ;
1087+ if ( ! validation . isValid ) {
1088+ return {
1089+ success : false ,
1090+ error : {
1091+ message : `Invalid path format for '${ path } ': ${ validation . errors . join ( ', ' ) } ` ,
1092+ code : ErrorCode . VALIDATION_FAILED ,
1093+ details : {
1094+ validationErrors : validation . errors . map ( ( e ) => ( {
1095+ field : 'path' ,
1096+ message : e ,
1097+ code : 'INVALID_PATH'
1098+ } ) )
1099+ } satisfies ValidationFailedContext
1100+ }
1101+ } ;
1102+ }
1103+
1104+ // 2. Check if file exists using session-aware check
1105+ const existsResult = await this . exists ( path , sessionId ) ;
1106+ if ( ! existsResult . success ) {
1107+ return {
1108+ success : false ,
1109+ error : existsResult . error
1110+ } ;
1111+ }
1112+
1113+ if ( ! existsResult . data ) {
1114+ return {
1115+ success : false ,
1116+ error : {
1117+ message : `File not found: ${ path } ` ,
1118+ code : ErrorCode . FILE_NOT_FOUND ,
1119+ details : {
1120+ path,
1121+ operation : Operation . FILE_READ
1122+ } satisfies FileNotFoundContext
1123+ }
1124+ } ;
1125+ }
1126+
1127+ // 3. Get file size using stat
1128+ const escapedPath = shellEscape ( path ) ;
1129+ const statCommand = `stat -c '%s' ${ escapedPath } 2>/dev/null` ;
1130+ const statResult = await this . sessionManager . executeInSession (
1131+ sessionId ,
1132+ statCommand
1133+ ) ;
1134+
1135+ if ( ! statResult . success ) {
1136+ return {
1137+ success : false ,
1138+ error : {
1139+ message : `Failed to get file size for '${ path } '` ,
1140+ code : ErrorCode . FILESYSTEM_ERROR ,
1141+ details : {
1142+ path,
1143+ operation : Operation . FILE_READ ,
1144+ stderr : 'Command execution failed'
1145+ } satisfies FileSystemContext
1146+ }
1147+ } ;
1148+ }
1149+
1150+ if ( statResult . data . exitCode !== 0 ) {
1151+ return {
1152+ success : false ,
1153+ error : {
1154+ message : `Failed to get file size for '${ path } '` ,
1155+ code : ErrorCode . FILESYSTEM_ERROR ,
1156+ details : {
1157+ path,
1158+ operation : Operation . FILE_READ ,
1159+ stderr : statResult . data . stderr
1160+ } satisfies FileSystemContext
1161+ }
1162+ } ;
1163+ }
1164+
1165+ const fileSize = parseInt ( statResult . data . stdout . trim ( ) , 10 ) ;
1166+
1167+ if ( Number . isNaN ( fileSize ) ) {
1168+ return {
1169+ success : false ,
1170+ error : {
1171+ message : `Failed to parse file size for '${ path } ': invalid stat output` ,
1172+ code : ErrorCode . FILESYSTEM_ERROR ,
1173+ details : {
1174+ path,
1175+ operation : Operation . FILE_READ ,
1176+ stderr : `Unexpected stat output: ${ statResult . data . stdout } `
1177+ } satisfies FileSystemContext
1178+ }
1179+ } ;
1180+ }
1181+
1182+ // 4. Detect MIME type using file command
1183+ const mimeCommand = `file --mime-type -b ${ escapedPath } ` ;
1184+ const mimeResult = await this . sessionManager . executeInSession (
1185+ sessionId ,
1186+ mimeCommand
1187+ ) ;
1188+
1189+ if ( ! mimeResult . success ) {
1190+ return {
1191+ success : false ,
1192+ error : {
1193+ message : `Failed to detect MIME type for '${ path } '` ,
1194+ code : ErrorCode . FILESYSTEM_ERROR ,
1195+ details : {
1196+ path,
1197+ operation : Operation . FILE_READ ,
1198+ stderr : 'Command execution failed'
1199+ } satisfies FileSystemContext
1200+ }
1201+ } ;
1202+ }
1203+
1204+ if ( mimeResult . data . exitCode !== 0 ) {
1205+ return {
1206+ success : false ,
1207+ error : {
1208+ message : `Failed to detect MIME type for '${ path } '` ,
1209+ code : ErrorCode . FILESYSTEM_ERROR ,
1210+ details : {
1211+ path,
1212+ operation : Operation . FILE_READ ,
1213+ stderr : mimeResult . data . stderr
1214+ } satisfies FileSystemContext
1215+ }
1216+ } ;
1217+ }
1218+
1219+ const mimeType = mimeResult . data . stdout . trim ( ) ;
1220+
1221+ // 5. Determine if file is binary based on MIME type
1222+ const isBinary = this . isBinaryMimeType ( mimeType ) ;
1223+
1224+ return {
1225+ success : true ,
1226+ data : {
1227+ mimeType,
1228+ size : fileSize ,
1229+ isBinary,
1230+ encoding : isBinary ? 'base64' : 'utf-8'
1231+ }
1232+ } ;
1233+ } catch ( error ) {
1234+ const errorMessage =
1235+ error instanceof Error ? error . message : 'Unknown error' ;
1236+ this . logger . error (
1237+ 'Failed to get file metadata' ,
1238+ error instanceof Error ? error : undefined ,
1239+ { path }
1240+ ) ;
1241+
1242+ return {
1243+ success : false ,
1244+ error : {
1245+ message : `Failed to get file metadata for '${ path } ': ${ errorMessage } ` ,
1246+ code : ErrorCode . FILESYSTEM_ERROR ,
1247+ details : {
1248+ path,
1249+ operation : Operation . FILE_READ ,
1250+ stderr : errorMessage
1251+ } satisfies FileSystemContext
1252+ }
1253+ } ;
1254+ }
1255+ }
1256+
10651257 // Convenience methods with ServiceResult wrapper for higher-level operations
10661258
10671259 async readFile (
@@ -1366,6 +1558,20 @@ export class FileService implements FileSystemOperations {
13661558 } ;
13671559 }
13681560
1561+ /**
1562+ * Determine if a MIME type represents binary content.
1563+ * Text MIME types: text/*, application/json, application/xml, application/javascript, etc.
1564+ */
1565+ private isBinaryMimeType ( mimeType : string ) : boolean {
1566+ return (
1567+ ! mimeType . startsWith ( 'text/' ) &&
1568+ ! mimeType . includes ( 'json' ) &&
1569+ ! mimeType . includes ( 'xml' ) &&
1570+ ! mimeType . includes ( 'javascript' ) &&
1571+ ! mimeType . includes ( 'x-empty' )
1572+ ) ;
1573+ }
1574+
13691575 /**
13701576 * Stream a file using Server-Sent Events (SSE)
13711577 * Sends metadata, chunks, and completion events
@@ -1382,7 +1588,7 @@ export class FileService implements FileSystemOperations {
13821588 start : async ( controller ) => {
13831589 try {
13841590 // 1. Get file metadata
1385- const metadataResult = await this . read ( path , { } , sessionId ) ;
1591+ const metadataResult = await this . getFileMetadata ( path , sessionId ) ;
13861592
13871593 if ( ! metadataResult . success ) {
13881594 const errorEvent = {
@@ -1396,18 +1602,7 @@ export class FileService implements FileSystemOperations {
13961602 return ;
13971603 }
13981604
1399- const metadata = metadataResult . metadata ;
1400- if ( ! metadata ) {
1401- const errorEvent = {
1402- type : 'error' ,
1403- error : 'Failed to get file metadata'
1404- } ;
1405- controller . enqueue (
1406- encoder . encode ( `data: ${ JSON . stringify ( errorEvent ) } \n\n` )
1407- ) ;
1408- controller . close ( ) ;
1409- return ;
1410- }
1605+ const metadata = metadataResult . data ;
14111606
14121607 // 2. Send metadata event
14131608 const metadataEvent = {
0 commit comments