Skip to content

Commit

Permalink
fix: handle non-existing objects properly
Browse files Browse the repository at this point in the history
  • Loading branch information
james-hu committed Mar 27, 2024
1 parent f15cadb commit 849f5a7
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 37 deletions.
87 changes: 57 additions & 30 deletions src/s3.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { CopyObjectCommand, CopyObjectCommandOutput, DeleteObjectCommand, DeleteObjectCommandOutput, GetObjectCommand, HeadObjectCommand, HeadObjectCommandOutput, ListObjectsV2Command, ListObjectsV2CommandInput, PutObjectCommand, PutObjectCommandInput, PutObjectOutput, S3Client } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

import { fetchAllByContinuationToken } from './aws-utils';
import { PossibleAwsError, fetchAllByContinuationToken } from './aws-utils';

const ERROR_NAME_NO_SUCH_KEY = 'NoSuchKey';

/**
* URL encode the object key, and also replace "%20" with " " and "%2F with "/" which is the convention of AWS
Expand Down Expand Up @@ -60,19 +62,26 @@ export async function copyS3Object(s3: S3Client, srcBucket: string, srcEncodedKe
}

/**
* Get details of the S3 object without downloading its content
* Get details of the S3 object without downloading its content.
* @param s3 S3Client
* @param bucket bucket of the source object
* @param key object key (without URL encoding)
* @returns S3 command output
* @returns S3 command output, or `undefined` if the object does not exist.
*/
export async function headS3Object(s3: S3Client, bucket: string, key: string): Promise<HeadObjectCommandOutput> {
return s3.send(
new HeadObjectCommand({
Bucket: bucket,
Key: key,
}),
);
export async function headS3Object(s3: S3Client, bucket: string, key: string): Promise<HeadObjectCommandOutput | undefined> {
try {
return await s3.send(
new HeadObjectCommand({
Bucket: bucket,
Key: key,
}),
);
} catch (error) {
if ((error as PossibleAwsError).name === ERROR_NAME_NO_SUCH_KEY) {
return undefined;
}
throw error;
}
}

/**
Expand All @@ -81,36 +90,54 @@ export async function headS3Object(s3: S3Client, bucket: string, key: string): P
* @param bucket bucket of the source object
* @param key object key (without URL encoding)
* @param encoding Text encoding of the content, if not specified then "utf8" will be used
* @returns Content of the S3 object as a string. If the object does not have content, an empty string will be returned.
* @returns Content of the S3 object as a string.
* If the object does not have content, an empty string will be returned.
* If the object does not exist, `undefined` will be returned.
*/
export async function getS3ObjectContentString(s3: S3Client, bucket: string, key: string, encoding = 'utf8'): Promise<string> {
const data = await s3.send(
new GetObjectCommand({
Bucket: bucket,
Key: key,
}),
);
return data?.Body ? data.Body.transformToString(encoding) : '';
export async function getS3ObjectContentString(s3: S3Client, bucket: string, key: string, encoding = 'utf8'): Promise<string | undefined> {
try {
const data = await s3.send(
new GetObjectCommand({
Bucket: bucket,
Key: key,
}),
);
return data?.Body ? data.Body.transformToString(encoding) : '';
} catch (error) {
if ((error as PossibleAwsError).name === ERROR_NAME_NO_SUCH_KEY) {
return undefined;
}
throw error;
}
}

/**
* Get the content of the S3 object as a string.
* Get the content of the S3 object as a Uint8Array.
* @param s3 S3Client
* @param bucket bucket of the source object
* @param key object key (without URL encoding)
* @param range See https://www.rfc-editor.org/rfc/rfc9110.html#name-range
* @param encoding Text encoding of the content, if not specified then "utf8" will be used
* @returns Content of the S3 object as a string. If the object does not have content, an empty string will be returned.
* @returns Content of the S3 object as a Uint8Array.
* If the object does not have content, an empty Uint8Array will be returned.
* If the object does not exist, `undefined` will be returned.
*/
export async function getS3ObjectContentByteArray(s3: S3Client, bucket: string, key: string, range?: string): Promise<Uint8Array> {
const data = await s3.send(
new GetObjectCommand({
Bucket: bucket,
Key: key,
Range: range,
}),
);
return data?.Body ? data.Body.transformToByteArray(): new Uint8Array();
export async function getS3ObjectContentByteArray(s3: S3Client, bucket: string, key: string, range?: string): Promise<Uint8Array | undefined> {
try {
const data = await s3.send(
new GetObjectCommand({
Bucket: bucket,
Key: key,
Range: range,
}),
);
return data?.Body ? data.Body.transformToByteArray(): new Uint8Array();
} catch (error) {
if ((error as PossibleAwsError).name === ERROR_NAME_NO_SUCH_KEY) {
return undefined;
}
throw error;
}
}

export interface S3ObjectSummary {
Expand Down
45 changes: 38 additions & 7 deletions test/s3.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { S3Client } from '@aws-sdk/client-s3';
import { expect } from 'chai';

import { decodeS3ObjectKey, encodeS3ObjectKey } from '../src/s3';
import { decodeS3ObjectKey, encodeS3ObjectKey, getS3ObjectContentByteArray, headS3Object, scanS3Bucket } from '../src/s3';

const testBucketName = process.env.TEST_BUCKET_NAME;
const testExistingObjectKey = process.env.TEST_EXISTING_OBJECT_KEY;
const testNonExistingObjectKey = process.env.TEST_NON_EXISTING_OBJECT_KEY;

describe('s3', () => {
it('should decodeS3ObjectKey work', () => {
Expand All @@ -19,10 +24,36 @@ describe('s3', () => {
key = 'xyz/abc/with space in path/with中文.jpg';
expect(decodeS3ObjectKey(encodeS3ObjectKey(key))).to.equal(key);
});
// it('should scanS3Bucket', async () => {
// const s3 = new S3Client();
// const objs = await scanS3Bucket(s3, '');
// console.log(objs);
// console.log(objs.length);
// });

if (testBucketName && testExistingObjectKey && testNonExistingObjectKey) {
it('should scanS3Bucket work', async () => {
const s3 = new S3Client();
const objs = await scanS3Bucket(s3, testBucketName);
// console.log(objs);
expect(objs.length).to.be.greaterThan(0);
});
it('should headS3Object work with existing object', async () => {
const s3 = new S3Client();
const result = await headS3Object(s3, testBucketName, testExistingObjectKey);
expect(result!.$metadata?.httpStatusCode).to.equal(200);
});
it('should getS3ObjectContentByteArray work with existing object', async () => {
const s3 = new S3Client();
const result = await getS3ObjectContentByteArray(s3, testBucketName, testExistingObjectKey);
expect(result!.length).to.be.greaterThan(0);
});
it('should headS3Object work with non-existing object', async () => {
const s3 = new S3Client();
const result = await getS3ObjectContentByteArray(s3, testBucketName, testNonExistingObjectKey);
expect(result).to.be.undefined;
});
it('should getS3ObjectContentByteArray work with non-existing object', async () => {
const s3 = new S3Client();
const result = await getS3ObjectContentByteArray(s3, testBucketName, testNonExistingObjectKey);
expect(result).to.be.undefined;
});
} else {
console.log('Skipping s3 tests because TEST_BUCKET_NAME, TEST_EXISTING_OBJECT_KEY, and TEST_NON_EXISTING_OBJECT_KEY are not set');
}

});

0 comments on commit 849f5a7

Please sign in to comment.