Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
30 changes: 23 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,23 @@ class S3Adapter {

// For a given config object, filename, and data, store a file in S3
// Returns a promise containing the S3 object creation response
async createFile(filename, data, contentType, options = {}) {
async createFile(filename, data, contentType, options = {}, config = {}) {
let key_without_prefix = filename;
if (typeof this._generateKey === 'function') {
const candidate = this._generateKey(filename, contentType, options);
key_without_prefix =
candidate && typeof candidate.then === 'function' ? await candidate : candidate;
if (typeof key_without_prefix !== 'string' || key_without_prefix.length === 0) {
throw new Error('generateKey must return a non-empty string');
}
}

const params = {
Bucket: this._bucket,
Key: this._bucketPrefix + filename,
Key: this._bucketPrefix + key_without_prefix,
Body: data,
};

if (this._generateKey instanceof Function) {
params.Key = this._bucketPrefix + this._generateKey(filename);
}
if (this._fileAcl) {
if (this._fileAcl === 'none') {
delete params.ACL;
Expand Down Expand Up @@ -177,11 +184,20 @@ class S3Adapter {
}
await this.createBucket();
const command = new PutObjectCommand(params);
const response = await this._s3Client.send(command);
await this._s3Client.send(command);
const endpoint = this._endpoint || `https://${this._bucket}.s3.${this._region}.amazonaws.com`;
const location = `${endpoint}/${params.Key}`;

return Object.assign(response || {}, { Location: location });
let url;
if (config?.mount && config?.applicationId) { // if config has required properties for getFileLocation
url = await this.getFileLocation(config, key_without_prefix);
}

return {
location: location, // actual upload location, used for tests
name: key_without_prefix, // filename in storage, consistent with other adapters
...url ? { url: url } : {} // url (optionally presigned) or non-direct access url
};
}

async deleteFile(filename) {
Expand Down
135 changes: 132 additions & 3 deletions spec/test.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,7 @@ describe('S3Adapter tests', () => {
const s3 = getMockS3Adapter(options);
const fileName = 'randomFileName.txt';
const response = s3.createFile(fileName, 'hello world', 'text/utf8').then(value => {
const url = new URL(value.Location);
const url = new URL(value.location);
expect(url.pathname.indexOf(fileName) > 13).toBe(true);
});
promises.push(response);
Expand All @@ -740,7 +740,7 @@ describe('S3Adapter tests', () => {
const s3 = getMockS3Adapter(options);
const fileName = 'foo/randomFileName.txt';
const response = s3.createFile(fileName, 'hello world', 'text/utf8').then(value => {
const url = new URL(value.Location);
const url = new URL(value.location);
expect(url.pathname.substring(1)).toEqual(options.bucketPrefix + fileName);
});
promises.push(response);
Expand All @@ -750,7 +750,7 @@ describe('S3Adapter tests', () => {
const s3 = getMockS3Adapter(options);
const fileName = 'foo/randomFileName.txt';
const response = s3.createFile(fileName, 'hello world', 'text/utf8').then(value => {
const url = new URL(value.Location);
const url = new URL(value.location);
expect(url.pathname.indexOf('foo/')).toEqual(6);
expect(url.pathname.indexOf('random') > 13).toBe(true);
});
Expand Down Expand Up @@ -867,6 +867,135 @@ describe('S3Adapter tests', () => {
expect(commandArg).toBeInstanceOf(PutObjectCommand);
expect(commandArg.input.ACL).toBeUndefined();
});

it('should return url when config is provided', async () => {
const options = {
bucket: 'bucket-1',
presignedUrl: true
};
const s3 = new S3Adapter(options);

const mockS3Response = {
ETag: '"mock-etag"',
VersionId: 'mock-version',
Location: 'mock-location'
};
s3ClientMock.send.and.returnValue(Promise.resolve(mockS3Response));
s3._s3Client = s3ClientMock;

// Mock getFileLocation to return a presigned URL
spyOn(s3, 'getFileLocation').and.returnValue(Promise.resolve('https://presigned-url.com/file.txt'));

const result = await s3.createFile(
'file.txt',
'hello world',
'text/utf8',
{},
{ mount: 'http://example.com', applicationId: 'test123' }
);

expect(result).toEqual({
location: jasmine.any(String),
name: 'file.txt',
url: 'https://presigned-url.com/file.txt'
});
});

it('should handle generateKey function errors', async () => {
const options = {
bucket: 'bucket-1',
generateKey: () => {
throw new Error('Generate key failed');
}
};
const s3 = new S3Adapter(options);
s3._s3Client = s3ClientMock;

await expectAsync(
s3.createFile('file.txt', 'hello world', 'text/utf8', {})
).toBeRejectedWithError('Generate key failed');
});

it('should handle async generateKey function', async () => {
const options = {
bucket: 'bucket-1',
generateKey: async (filename) => {
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 10));
return `async-${filename}`;
}
};
const s3 = new S3Adapter(options);
s3ClientMock.send.and.returnValue(Promise.resolve({}));
s3._s3Client = s3ClientMock;

await s3.createFile('file.txt', 'hello world', 'text/utf8', {});

expect(s3ClientMock.send).toHaveBeenCalledTimes(2);
const commands = s3ClientMock.send.calls.all();
const commandArg = commands[1].args[0];
expect(commandArg).toBeInstanceOf(PutObjectCommand);
expect(commandArg.input.Key).toBe('async-file.txt');
});

it('should handle generateKey that returns a Promise', async () => {
const options = {
bucket: 'bucket-1',
generateKey: (filename) => {
return Promise.resolve(`promise-${filename}`);
}
};
const s3 = new S3Adapter(options);
s3ClientMock.send.and.returnValue(Promise.resolve({}));
s3._s3Client = s3ClientMock;

await s3.createFile('file.txt', 'hello world', 'text/utf8', {});

expect(s3ClientMock.send).toHaveBeenCalledTimes(2);
const commands = s3ClientMock.send.calls.all();
const commandArg = commands[1].args[0];
expect(commandArg).toBeInstanceOf(PutObjectCommand);
expect(commandArg.input.Key).toBe('promise-file.txt');
});

it('should validate generateKey returns a non-empty string', async () => {
const options = {
bucket: 'bucket-1',
generateKey: () => ''
};
const s3 = new S3Adapter(options);
s3._s3Client = s3ClientMock;

await expectAsync(
s3.createFile('file.txt', 'hello world', 'text/utf8', {})
).toBeRejectedWithError('generateKey must return a non-empty string');
});

it('should validate generateKey returns a string (not number)', async () => {
const options = {
bucket: 'bucket-1',
generateKey: () => 12345
};
const s3 = new S3Adapter(options);
s3._s3Client = s3ClientMock;

await expectAsync(
s3.createFile('file.txt', 'hello world', 'text/utf8', {})
).toBeRejectedWithError('generateKey must return a non-empty string');
});

it('should validate async generateKey returns a string', async () => {
const options = {
bucket: 'bucket-1',
generateKey: async () => null
};
const s3 = new S3Adapter(options);
s3._s3Client = s3ClientMock;

await expectAsync(
s3.createFile('file.txt', 'hello world', 'text/utf8', {})
).toBeRejectedWithError('generateKey must return a non-empty string');
});
});

describe('handleFileStream', () => {
Expand Down