Skip to content

Commit e0cb828

Browse files
committed
dedupe get logic
1 parent 476ce89 commit e0cb828

File tree

1 file changed

+34
-110
lines changed

1 file changed

+34
-110
lines changed

packages/s3-file-storage/src/lib/s3-file-storage.ts

Lines changed: 34 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -298,68 +298,77 @@ export class S3FileStorage implements FileStorage {
298298

299299
/**
300300
* Returns the file with the given key, or `null` if no such key exists.
301-
* Uses a HEAD request to get metadata and creates a LazyFile that will only fetch the content when needed.
301+
* If `eager` is true, the file content is fetched immediately.
302+
* Otherwise, a HEAD request is used to get metadata, and a LazyFile is created
303+
* that will only fetch the content when its stream is accessed.
302304
*/
303305
async get(key: string): Promise<File | null> {
304-
return this.eager ? this.getEager(key) : this.getLazy(key);
305-
}
306-
307-
private async getEager(key: string): Promise<LazyFile | null> {
308306
const objectUrl = this.getObjectUrl(key);
309-
310-
const initial = await this.aws.fetch(objectUrl, {
311-
method: 'GET',
312-
});
313-
314-
if (!initial.ok) {
315-
return null;
307+
let initialResponse: Response | null = null;
308+
let responseHeaders: Headers;
309+
310+
if (this.eager) {
311+
const eagerResponse = await this.aws.fetch(objectUrl, { method: 'GET' });
312+
if (!eagerResponse.ok) {
313+
if (eagerResponse.status === 404) return null;
314+
throw new Error(`Failed to get file: ${eagerResponse.statusText}`);
315+
}
316+
initialResponse = eagerResponse;
317+
responseHeaders = initialResponse.headers;
318+
} else {
319+
const lazyResponse = await this.aws.fetch(objectUrl, { method: 'HEAD' });
320+
if (!lazyResponse.ok) {
321+
if (lazyResponse.status === 404) return null;
322+
throw new Error(`Failed to get file metadata: ${lazyResponse.statusText}`);
323+
}
324+
responseHeaders = lazyResponse.headers;
316325
}
317326

318327
const {
319328
name,
320329
lastModified,
321330
type,
322331
size
323-
} = this.extractMetadata(key, initial.headers);
332+
} = this.extractMetadata(key, responseHeaders);
324333

325-
// Store AWS client and key in variables that can be captured by the closure
334+
// Store AWS client in a variable that can be captured by the closure
326335
const aws = this.aws;
327336

328-
// Create LazyContent implementation that will fetch the file only when needed
329337
const lazyContent: LazyContent = {
330338
byteLength: size,
331339
stream(start?: number, end?: number): ReadableStream<Uint8Array> {
332340
return new ReadableStream({
333341
async start(controller) {
334342
const headers: Record<string, string> = {};
335343
if (start !== undefined || end !== undefined) {
336-
337-
// it's valid to pass a start without an end
338344
let range = `bytes=${start ?? 0}-`;
339345
if (end !== undefined) {
346+
// Range header is inclusive, so subtract 1 from end if specified
340347
range += (end - 1);
341348
}
342-
343349
headers['Range'] = range;
344350
}
345351

346352
try {
347353
let reader: ReadableStreamDefaultReader<Uint8Array>;
348-
if (!headers['Range'] && !initial.bodyUsed) {
349-
// If no range is specified and the body has not been used, we can use the initial response's body
350-
reader = initial.body!.getReader();
351-
354+
// If eager loading provided an initial response, no range is requested,
355+
// and its body hasn't been used, we can use its body.
356+
if (!headers['Range'] && initialResponse?.body && !initialResponse.bodyUsed) {
357+
reader = initialResponse.body.getReader();
352358
} else {
359+
// Otherwise, fetch the content (or range)
353360
const response = await aws.fetch(objectUrl, {
354361
method: 'GET',
355362
headers
356363
});
357364

358365
if (!response.ok) {
359-
throw new Error(`Failed to fetch file: ${response.statusText}`);
366+
throw new Error(`Failed to fetch file content: ${response.statusText}`);
360367
}
361-
362-
reader = response.body!.getReader();
368+
if (!response.body) {
369+
throw new Error('Response body is null');
370+
}
371+
reader = response.body.getReader();
363372
}
364373

365374
while (true) {
@@ -387,91 +396,6 @@ export class S3FileStorage implements FileStorage {
387396
);
388397
}
389398

390-
private async getLazy(key: string): Promise<File | null> {
391-
// First do a HEAD request to get metadata without downloading the file
392-
const headResponse = await this.aws.fetch(this.getObjectUrl(key), {
393-
method: 'HEAD',
394-
});
395-
396-
if (!headResponse.ok) {
397-
return null;
398-
}
399-
400-
const contentLength = headResponse.headers.get('content-length');
401-
const contentType = headResponse.headers.get('content-type') || '';
402-
const lastModifiedHeader = headResponse.headers.get('last-modified');
403-
const lastModified = lastModifiedHeader ? new Date(lastModifiedHeader).getTime() : Date.now();
404-
405-
// Try to get the file name from metadata
406-
let fileName = key.split('/').pop() || key;
407-
408-
const metadataName = headResponse.headers.get('x-amz-meta-name');
409-
const metadataLastModified = headResponse.headers.get('x-amz-meta-lastModified');
410-
const metadataType = headResponse.headers.get('x-amz-meta-type');
411-
412-
if (metadataName) {
413-
fileName = decodeURI(metadataName);
414-
}
415-
416-
// Store AWS client and key in variables that can be captured by the closure
417-
const aws = this.aws;
418-
const objectUrl = this.getObjectUrl(key);
419-
420-
// Create LazyContent implementation that will fetch the file only when needed
421-
const lazyContent: LazyContent = {
422-
byteLength: contentLength ? parseInt(contentLength, 10) : 0,
423-
stream(start?: number, end?: number): ReadableStream<Uint8Array> {
424-
return new ReadableStream({
425-
async start(controller) {
426-
const headers: Record<string, string> = {};
427-
if (start !== undefined || end !== undefined) {
428-
429-
// it's valid to pass a start without an end
430-
let range = `bytes=${start ?? 0}-`;
431-
if (end !== undefined) {
432-
range += (end - 1);
433-
}
434-
435-
headers['Range'] = range;
436-
}
437-
438-
try {
439-
const response = await aws.fetch(objectUrl, {
440-
method: 'GET',
441-
headers
442-
});
443-
444-
if (!response.ok) {
445-
throw new Error(`Failed to fetch file: ${response.statusText}`);
446-
}
447-
448-
const reader = response.body!.getReader();
449-
450-
while (true) {
451-
const { done, value } = await reader.read();
452-
if (done) break;
453-
controller.enqueue(value);
454-
}
455-
456-
controller.close();
457-
} catch (error) {
458-
controller.error(error);
459-
}
460-
}
461-
});
462-
}
463-
};
464-
465-
return new LazyFile(
466-
lazyContent,
467-
fileName,
468-
{
469-
type: metadataType || contentType,
470-
lastModified: metadataLastModified ? parseInt(metadataLastModified, 10) : lastModified
471-
}
472-
);
473-
}
474-
475399
async put(key: string, file: File): Promise<File> {
476400
await this.set(key, file);
477401
return (await this.get(key))!;

0 commit comments

Comments
 (0)