Skip to content

Commit 32a0dab

Browse files
authored
add getFileMetadata method in FileService to get only metadata (#243)
* add getFileMetadata method in FileService to get only metadata, without reading the whole file + tests * add integration test * fix code duplications, add NaN checks * Add getFileMetadata method in FileService Add a method to retrieve only file metadata.
1 parent f88e24e commit 32a0dab

File tree

4 files changed

+561
-52
lines changed

4 files changed

+561
-52
lines changed

.changeset/fresh-adults-wait.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cloudflare/sandbox': patch
3+
---
4+
5+
add getFileMetadata method in FileService to get only metadata

packages/sandbox-container/src/handlers/file-handler.ts

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -103,38 +103,7 @@ export class FileHandler extends BaseHandler<Request, Response> {
103103
const body = await this.parseRequestBody<ReadFileRequest>(request);
104104

105105
try {
106-
// Get file metadata first
107-
const metadataResult = await this.fileService.readFile(body.path, {
108-
encoding: 'utf-8'
109-
});
110-
111-
if (!metadataResult.success) {
112-
// Return error as SSE event
113-
const encoder = new TextEncoder();
114-
const errorEvent: FileStreamEvent = {
115-
type: 'error',
116-
error: metadataResult.error.message
117-
};
118-
const stream = new ReadableStream({
119-
start(controller) {
120-
controller.enqueue(
121-
encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`)
122-
);
123-
controller.close();
124-
}
125-
});
126-
127-
return new Response(stream, {
128-
headers: {
129-
'Content-Type': 'text/event-stream',
130-
'Cache-Control': 'no-cache',
131-
Connection: 'keep-alive',
132-
...context.corsHeaders
133-
}
134-
});
135-
}
136-
137-
// Create SSE stream
106+
// Create SSE stream (handles metadata fetching and errors internally)
138107
const stream = await this.fileService.readFileStreamOperation(
139108
body.path,
140109
body.sessionId

packages/sandbox-container/src/services/file-service.ts

Lines changed: 215 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)