diff --git a/src/s3.ts b/src/s3.ts index 8925b39..242c0e0 100644 --- a/src/s3.ts +++ b/src/s3.ts @@ -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 @@ -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 { - return s3.send( - new HeadObjectCommand({ - Bucket: bucket, - Key: key, - }), - ); +export async function headS3Object(s3: S3Client, bucket: string, key: string): Promise { + 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; + } } /** @@ -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 { - 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 { + 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 { - 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 { + 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 { diff --git a/test/s3.spec.ts b/test/s3.spec.ts index c70663a..5cebbf4 100644 --- a/test/s3.spec.ts +++ b/test/s3.spec.ts @@ -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', () => { @@ -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'); + } + });