Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jobs:
if: ${{ github.ref != 'refs/heads/master' }}
uses: wagoid/commitlint-github-action@v6

- name: Use Node.js 16.x
- name: Use Node.js 18.x
uses: actions/setup-node@v4
with:
node-version: 16.x
node-version: 18.x

- name: Install packages
run: npm ci
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const s3Handler = new S3Handler(s3Client, 'my-bucket-name');
// Get object
await s3Handler.getObject('my-key');

// Get object Buffer (response.Body is accumulate in a Buffer)
await s3Handler.getObjectBuffer('my-key');

// Put object
await s3Handler.putObject('my-key', Buffer.from('hello-world'));

Expand All @@ -39,6 +42,9 @@ await s3Handler.listObjects('my-prefix');

// Delete object
await s3Handler.deleteObject('my-key');

// Delete several objects
await s3Handler.deleteObjects([{ Key: 'my-key1' }, {Key: 'my-key2'}]);
```

### Readable Stream Usage
Expand All @@ -51,7 +57,8 @@ const s3Client = new S3Client({ region: 'eu-west-1' });

const s3Handler = new S3Handler(s3Client, 'my-bucket-name');

const readable = s3Handler.getObjectStream('my-super-heavy-file');
const response = s3Handler.getObject('my-super-heavy-file');
const readable = response.Body as Readable;

readable.on('data', (message) => {
console.log(message);
Expand Down
3,464 changes: 1,651 additions & 1,813 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"test": "test"
},
"engines": {
"node": ">=16.0.0"
"node": ">=18.0.0"
},
"keywords": [
"aws",
Expand All @@ -34,6 +34,11 @@
"test:watch": "env NODE_ENV=test mocha --watch",
"test": "npm run test:lint && npm run test:types && npm run test:cover"
},
"peerDependencies": {
"@aws-sdk/client-sqs": "^3.667.0",
"@aws-sdk/lib-storage": "^3.750.0",
"@aws-sdk/s3-request-presigner": "^3.750.0"
},
"devDependencies": {
"@aws-sdk/types": "^3.734.0",
"@sagacify/eslint-config": "^1.2.0",
Expand Down Expand Up @@ -119,10 +124,5 @@
"@semantic-release/git",
"@semantic-release/github"
]
},
"dependencies": {
"@aws-sdk/client-s3": "^3.750.0",
"@aws-sdk/lib-storage": "^3.750.0",
"@aws-sdk/s3-request-presigner": "^3.750.0"
}
}
61 changes: 38 additions & 23 deletions src/S3Handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
DeleteObjectsCommandInput,
GetObjectCommand,
GetObjectCommandInput,
GetObjectCommandOutput,
ListObjectsV2Command,
ListObjectsV2CommandInput,
ObjectIdentifier,
Expand All @@ -14,7 +15,6 @@ import {
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Configuration, Upload } from '@aws-sdk/lib-storage';
import { Readable } from 'node:stream';
import { RequestPresigningArguments, StreamingBlobPayloadInputTypes } from '@smithy/types';

export class S3Handler {
Expand All @@ -30,35 +30,40 @@ export class S3Handler {
return this.client;
}

async getObject(
key: string,
options: Omit<GetObjectCommandInput, 'Bucket' | 'Key'> = {}
): Promise<Buffer | null> {
const response = await this.client.send(
async getObject(key: string, options: Omit<GetObjectCommandInput, 'Bucket' | 'Key'> = {}) {
return await this.client.send(
new GetObjectCommand({
Bucket: this.bucket,
Key: key,
...options
})
);
}

if (response.Body) {
const outputReadable = await response.Body.transformToByteArray();
return Buffer.from(outputReadable);
async getObjectBuffer(
key: string,
options: Omit<GetObjectCommandInput, 'Bucket' | 'Key'> = {}
): Promise<
Omit<GetObjectCommandOutput, 'Body'> & {
Body?: Buffer;
}
> {
const response = await this.getObject(key, options);

return null;
}
if (response.Body === undefined) {
// explicitly override Body with undefined so that its type matches the expected type
return { ...response, Body: undefined };
}

async getObjectStream(key: string, options: Omit<GetObjectCommandInput, 'Bucket' | 'Key'> = {}) {
const response = await this.client.send(
new GetObjectCommand({
Bucket: this.bucket,
Key: key,
...options
})
);
return response.Body as Readable;
const byteArray = await response.Body.transformToByteArray();
// This creates a view of the <ArrayBuffer> without copying the underlying memory
// See: https://nodejs.org/api/buffer.html#static-method-bufferfromarraybuffer-byteoffset-length
const bodyBuffer = Buffer.from(byteArray.buffer, byteArray.byteOffset, byteArray.byteLength);

return {
...response,
Body: bodyBuffer
};
}

async putFolder(folderName: string) {
Expand All @@ -72,11 +77,11 @@ export class S3Handler {

async putObject(
key: string,
buffer: Buffer,
body: PutObjectCommandInput['Body'],
options: Omit<PutObjectCommandInput, 'Bucket' | 'Key' | 'Body'> = {}
) {
const putObjectCommand = new PutObjectCommand({
Body: buffer,
Body: body,
Bucket: this.bucket,
Key: key,
...options
Expand Down Expand Up @@ -141,13 +146,23 @@ export class S3Handler {
return this.client.send(deleteObjectsCommand);
}

async generatePresignedUrl(
async generateGetPresignedUrl(
key: string,
signedUrlOptions: RequestPresigningArguments = {},
getObjectOptions: Omit<GetObjectCommandInput, 'Bucket' | 'Key'> = {}
) {
const command = new GetObjectCommand({ Bucket: this.bucket, Key: key, ...getObjectOptions });
return getSignedUrl(this.client, command, signedUrlOptions);
}

async generatePutPresignedUrl(
key: string,
signedUrlOptions: RequestPresigningArguments = {},
getObjectOptions: Omit<PutObjectCommandInput, 'Bucket' | 'Key'> = {}
) {
const command = new PutObjectCommand({ Bucket: this.bucket, Key: key, ...getObjectOptions });
return getSignedUrl(this.client, command, signedUrlOptions);
}
}

export default S3Handler;
18 changes: 9 additions & 9 deletions test/src/getObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import { sdkStreamMixin } from '@smithy/util-stream';
describe('S3Handler.getObject', () => {
const s3ClientMock = mockClient(S3Client);

afterEach(() => {
beforeEach(() => {
s3ClientMock.reset();
});

it('should get an object buffer', async () => {
it('should get an object stream', async () => {
const stream = new Readable();
stream.push('hello world');
stream.push(null); // end of stream
Expand All @@ -21,19 +21,19 @@ describe('S3Handler.getObject', () => {

s3ClientMock.on(GetObjectCommand).resolvesOnce({ Body: sdkStream });

const s3Handler = new S3Handler(s3ClientMock as unknown as S3Client, 'my-dummy-bucket');
const s3Handler = new S3Handler(new S3Client({}), 'my-dummy-bucket');

const objectBuffer = await s3Handler.getObject('my-key');
const objectStream = await s3Handler.getObject('my-key');

expect(s3ClientMock.commandCalls(GetObjectCommand).length).equal(1);
expect(s3ClientMock.commandCalls(GetObjectCommand)[0].args[0].input).deep.equal({
Bucket: 'my-dummy-bucket',
Key: 'my-key'
});
expect(objectBuffer).deep.equal(Buffer.from('hello world'));
expect(objectStream.Body).equal(sdkStream);
});

it('should get an object buffer with additional options', async () => {
it('should get an object stream with additional options', async () => {
const stream = new Readable();
stream.push('hello world');
stream.push(null); // end of stream
Expand All @@ -42,16 +42,16 @@ describe('S3Handler.getObject', () => {

s3ClientMock.on(GetObjectCommand).resolvesOnce({ Body: sdkStream });

const s3Handler = new S3Handler(s3ClientMock as unknown as S3Client, 'my-dummy-bucket');
const s3Handler = new S3Handler(new S3Client({}), 'my-dummy-bucket');

const objectBuffer = await s3Handler.getObject('my-key', { Range: 'bytes=0-5' });
const objectStream = await s3Handler.getObject('my-key', { Range: 'bytes=0-5' });

expect(s3ClientMock.commandCalls(GetObjectCommand).length).equal(1);
expect(s3ClientMock.commandCalls(GetObjectCommand)[0].args[0].input).deep.equal({
Bucket: 'my-dummy-bucket',
Key: 'my-key',
Range: 'bytes=0-5'
});
expect(objectBuffer).deep.equal(Buffer.from('hello world'));
expect(objectStream.Body).equal(sdkStream);
});
});
20 changes: 10 additions & 10 deletions test/src/getObjectStream.ts → test/src/getObjectBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import { S3Handler } from '../../src/S3Handler';
import { Readable } from 'stream';
import { sdkStreamMixin } from '@smithy/util-stream';

describe('S3Handler.getObjectStream', () => {
describe('S3Handler.getObjectBuffer', () => {
const s3ClientMock = mockClient(S3Client);

beforeEach(() => {
afterEach(() => {
s3ClientMock.reset();
});

it('should get an object stream', async () => {
it('should get an object buffer', async () => {
const stream = new Readable();
stream.push('hello world');
stream.push(null); // end of stream
Expand All @@ -21,19 +21,19 @@ describe('S3Handler.getObjectStream', () => {

s3ClientMock.on(GetObjectCommand).resolvesOnce({ Body: sdkStream });

const s3Handler = new S3Handler(new S3Client({}), 'my-dummy-bucket');
const s3Handler = new S3Handler(s3ClientMock as unknown as S3Client, 'my-dummy-bucket');

const objectStream = await s3Handler.getObjectStream('my-key');
const responseBuffer = await s3Handler.getObjectBuffer('my-key');

expect(s3ClientMock.commandCalls(GetObjectCommand).length).equal(1);
expect(s3ClientMock.commandCalls(GetObjectCommand)[0].args[0].input).deep.equal({
Bucket: 'my-dummy-bucket',
Key: 'my-key'
});
expect(objectStream).equal(sdkStream);
expect(responseBuffer.Body).deep.equal(Buffer.from('hello world'));
});

it('should get an object stream with additional options', async () => {
it('should get an object buffer with additional options', async () => {
const stream = new Readable();
stream.push('hello world');
stream.push(null); // end of stream
Expand All @@ -42,16 +42,16 @@ describe('S3Handler.getObjectStream', () => {

s3ClientMock.on(GetObjectCommand).resolvesOnce({ Body: sdkStream });

const s3Handler = new S3Handler(new S3Client({}), 'my-dummy-bucket');
const s3Handler = new S3Handler(s3ClientMock as unknown as S3Client, 'my-dummy-bucket');

const objectStream = await s3Handler.getObjectStream('my-key', { Range: 'bytes=0-5' });
const responseBuffer = await s3Handler.getObjectBuffer('my-key', { Range: 'bytes=0-5' });

expect(s3ClientMock.commandCalls(GetObjectCommand).length).equal(1);
expect(s3ClientMock.commandCalls(GetObjectCommand)[0].args[0].input).deep.equal({
Bucket: 'my-dummy-bucket',
Key: 'my-key',
Range: 'bytes=0-5'
});
expect(objectStream).equal(sdkStream);
expect(responseBuffer.Body).deep.equal(Buffer.from('hello world'));
});
});