@directus/storage-driver-s3 9.22.4 → 9.23.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +42 -36
- package/dist/index.test.js +41 -40
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -4,64 +4,70 @@ import { normalizePath } from '@directus/utils';
|
|
|
4
4
|
import { isReadableStream } from '@directus/utils/node';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
export class DriverS3 {
|
|
7
|
-
|
|
7
|
+
config;
|
|
8
8
|
client;
|
|
9
|
-
|
|
10
|
-
acl;
|
|
11
|
-
serverSideEncryption;
|
|
9
|
+
root;
|
|
12
10
|
constructor(config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.client = this.getClient();
|
|
13
|
+
this.root = this.config.root ? normalizePath(this.config.root, { removeLeading: true }) : '';
|
|
14
|
+
}
|
|
15
|
+
getClient() {
|
|
13
16
|
const s3ClientConfig = {};
|
|
14
|
-
if ((config.key && !config.secret) || (config.secret && !config.key)) {
|
|
17
|
+
if ((this.config.key && !this.config.secret) || (this.config.secret && !this.config.key)) {
|
|
15
18
|
throw new Error('Both `key` and `secret` are required when defined');
|
|
16
19
|
}
|
|
17
|
-
if (config.key && config.secret) {
|
|
20
|
+
if (this.config.key && this.config.secret) {
|
|
18
21
|
s3ClientConfig.credentials = {
|
|
19
|
-
accessKeyId: config.key,
|
|
20
|
-
secretAccessKey: config.secret,
|
|
22
|
+
accessKeyId: this.config.key,
|
|
23
|
+
secretAccessKey: this.config.secret,
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
|
-
if (config.endpoint) {
|
|
24
|
-
const protocol = config.endpoint.startsWith('https://') ? 'https:' : 'http:';
|
|
25
|
-
const hostname = config.endpoint.replace('https://', '').replace('http://', '');
|
|
26
|
+
if (this.config.endpoint) {
|
|
27
|
+
const protocol = this.config.endpoint.startsWith('https://') ? 'https:' : 'http:';
|
|
28
|
+
const hostname = this.config.endpoint.replace('https://', '').replace('http://', '');
|
|
26
29
|
s3ClientConfig.endpoint = {
|
|
27
30
|
hostname,
|
|
28
31
|
protocol,
|
|
29
32
|
path: '/',
|
|
30
33
|
};
|
|
31
34
|
}
|
|
32
|
-
if (config.region) {
|
|
33
|
-
s3ClientConfig.region = config.region;
|
|
35
|
+
if (this.config.region) {
|
|
36
|
+
s3ClientConfig.region = this.config.region;
|
|
34
37
|
}
|
|
35
|
-
if (config.forcePathStyle !== undefined) {
|
|
36
|
-
s3ClientConfig.forcePathStyle = config.forcePathStyle;
|
|
38
|
+
if (this.config.forcePathStyle !== undefined) {
|
|
39
|
+
s3ClientConfig.forcePathStyle = this.config.forcePathStyle;
|
|
37
40
|
}
|
|
38
|
-
|
|
39
|
-
this.bucket = config.bucket;
|
|
40
|
-
this.acl = config.acl;
|
|
41
|
-
this.serverSideEncryption = config.serverSideEncryption;
|
|
42
|
-
this.root = config.root ? normalizePath(config.root, { removeLeading: true }) : '';
|
|
41
|
+
return new S3Client(s3ClientConfig);
|
|
43
42
|
}
|
|
44
43
|
fullPath(filepath) {
|
|
45
44
|
return normalizePath(join(this.root, filepath));
|
|
46
45
|
}
|
|
47
46
|
async read(filepath, range) {
|
|
47
|
+
/*
|
|
48
|
+
* AWS' client default socket reusing and keepalive can cause performance issues when using it
|
|
49
|
+
* very often in rapid succession. For reads, where it's more likely to hit this limitation,
|
|
50
|
+
* we'll use a new non-shared S3 client to get around this.
|
|
51
|
+
*/
|
|
52
|
+
const client = this.getClient();
|
|
48
53
|
const commandInput = {
|
|
49
54
|
Key: this.fullPath(filepath),
|
|
50
|
-
Bucket: this.bucket,
|
|
55
|
+
Bucket: this.config.bucket,
|
|
51
56
|
};
|
|
52
57
|
if (range) {
|
|
53
58
|
commandInput.Range = `bytes=${range.start ?? ''}-${range.end ?? ''}`;
|
|
54
59
|
}
|
|
55
|
-
const { Body: stream } = await
|
|
60
|
+
const { Body: stream } = await client.send(new GetObjectCommand(commandInput));
|
|
56
61
|
if (!stream || !isReadableStream(stream)) {
|
|
57
62
|
throw new Error(`No stream returned for file "${filepath}"`);
|
|
58
63
|
}
|
|
64
|
+
stream.on('finished', () => client.destroy());
|
|
59
65
|
return stream;
|
|
60
66
|
}
|
|
61
67
|
async stat(filepath) {
|
|
62
68
|
const { ContentLength, LastModified } = await this.client.send(new HeadObjectCommand({
|
|
63
69
|
Key: this.fullPath(filepath),
|
|
64
|
-
Bucket: this.bucket,
|
|
70
|
+
Bucket: this.config.bucket,
|
|
65
71
|
}));
|
|
66
72
|
return {
|
|
67
73
|
size: ContentLength,
|
|
@@ -84,14 +90,14 @@ export class DriverS3 {
|
|
|
84
90
|
async copy(src, dest) {
|
|
85
91
|
const params = {
|
|
86
92
|
Key: this.fullPath(dest),
|
|
87
|
-
Bucket: this.bucket,
|
|
88
|
-
CopySource: `/${this.bucket}/${this.fullPath(src)}`,
|
|
93
|
+
Bucket: this.config.bucket,
|
|
94
|
+
CopySource: `/${this.config.bucket}/${this.fullPath(src)}`,
|
|
89
95
|
};
|
|
90
|
-
if (this.serverSideEncryption) {
|
|
91
|
-
params.ServerSideEncryption = this.serverSideEncryption;
|
|
96
|
+
if (this.config.serverSideEncryption) {
|
|
97
|
+
params.ServerSideEncryption = this.config.serverSideEncryption;
|
|
92
98
|
}
|
|
93
|
-
if (this.acl) {
|
|
94
|
-
params.ACL = this.acl;
|
|
99
|
+
if (this.config.acl) {
|
|
100
|
+
params.ACL = this.config.acl;
|
|
95
101
|
}
|
|
96
102
|
await this.client.send(new CopyObjectCommand(params));
|
|
97
103
|
}
|
|
@@ -99,16 +105,16 @@ export class DriverS3 {
|
|
|
99
105
|
const params = {
|
|
100
106
|
Key: this.fullPath(filepath),
|
|
101
107
|
Body: content,
|
|
102
|
-
Bucket: this.bucket,
|
|
108
|
+
Bucket: this.config.bucket,
|
|
103
109
|
};
|
|
104
110
|
if (type) {
|
|
105
111
|
params.ContentType = type;
|
|
106
112
|
}
|
|
107
|
-
if (this.acl) {
|
|
108
|
-
params.ACL = this.acl;
|
|
113
|
+
if (this.config.acl) {
|
|
114
|
+
params.ACL = this.config.acl;
|
|
109
115
|
}
|
|
110
|
-
if (this.serverSideEncryption) {
|
|
111
|
-
params.ServerSideEncryption = this.serverSideEncryption;
|
|
116
|
+
if (this.config.serverSideEncryption) {
|
|
117
|
+
params.ServerSideEncryption = this.config.serverSideEncryption;
|
|
112
118
|
}
|
|
113
119
|
const upload = new Upload({
|
|
114
120
|
client: this.client,
|
|
@@ -117,13 +123,13 @@ export class DriverS3 {
|
|
|
117
123
|
await upload.done();
|
|
118
124
|
}
|
|
119
125
|
async delete(filepath) {
|
|
120
|
-
await this.client.send(new DeleteObjectCommand({ Key: this.fullPath(filepath), Bucket: this.bucket }));
|
|
126
|
+
await this.client.send(new DeleteObjectCommand({ Key: this.fullPath(filepath), Bucket: this.config.bucket }));
|
|
121
127
|
}
|
|
122
128
|
async *list(prefix = '') {
|
|
123
129
|
let continuationToken = undefined;
|
|
124
130
|
do {
|
|
125
131
|
const listObjectsV2CommandInput = {
|
|
126
|
-
Bucket: this.bucket,
|
|
132
|
+
Bucket: this.config.bucket,
|
|
127
133
|
Prefix: this.fullPath(prefix),
|
|
128
134
|
MaxKeys: 1000,
|
|
129
135
|
};
|
package/dist/index.test.js
CHANGED
|
@@ -66,6 +66,43 @@ afterEach(() => {
|
|
|
66
66
|
vi.resetAllMocks();
|
|
67
67
|
});
|
|
68
68
|
describe('#constructor', () => {
|
|
69
|
+
let getClientBackup;
|
|
70
|
+
let sampleClient;
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
getClientBackup = DriverS3.prototype['getClient'];
|
|
73
|
+
sampleClient = {};
|
|
74
|
+
DriverS3.prototype['getClient'] = vi.fn().mockReturnValue(sampleClient);
|
|
75
|
+
});
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
DriverS3.prototype['getClient'] = getClientBackup;
|
|
78
|
+
});
|
|
79
|
+
test('Saves passed config to local property', () => {
|
|
80
|
+
const driver = new DriverS3(sample.config);
|
|
81
|
+
expect(driver['config']).toBe(sample.config);
|
|
82
|
+
});
|
|
83
|
+
test('Creates shared client', () => {
|
|
84
|
+
const driver = new DriverS3(sample.config);
|
|
85
|
+
expect(driver['getClient']).toHaveBeenCalledOnce();
|
|
86
|
+
expect(driver['client']).toBe(sampleClient);
|
|
87
|
+
});
|
|
88
|
+
test('Defaults root to empty string', () => {
|
|
89
|
+
expect(driver['root']).toBe('');
|
|
90
|
+
});
|
|
91
|
+
test('Normalizes config path when root is given', () => {
|
|
92
|
+
const mockRoot = randDirectoryPath();
|
|
93
|
+
vi.mocked(normalizePath).mockReturnValue(mockRoot);
|
|
94
|
+
const driver = new DriverS3({
|
|
95
|
+
key: sample.config.key,
|
|
96
|
+
secret: sample.config.secret,
|
|
97
|
+
bucket: sample.config.bucket,
|
|
98
|
+
root: sample.config.root,
|
|
99
|
+
});
|
|
100
|
+
expect(normalizePath).toHaveBeenCalledWith(sample.config.root, { removeLeading: true });
|
|
101
|
+
expect(driver['root']).toBe(mockRoot);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('#getClient', () => {
|
|
105
|
+
// The constructor calls getClient(), so we don't have to call it separately
|
|
69
106
|
test('Throws error if key defined but secret missing', () => {
|
|
70
107
|
try {
|
|
71
108
|
new DriverS3({ key: 'key', bucket: 'bucket' });
|
|
@@ -98,30 +135,6 @@ describe('#constructor', () => {
|
|
|
98
135
|
});
|
|
99
136
|
expect(driver['client']).toBeInstanceOf(S3Client);
|
|
100
137
|
});
|
|
101
|
-
test('Sets private bucket reference based on config', () => {
|
|
102
|
-
expect(driver['bucket']).toBe(sample.config.bucket);
|
|
103
|
-
});
|
|
104
|
-
test('Sets private acl reference based on config', () => {
|
|
105
|
-
const driver = new DriverS3({
|
|
106
|
-
key: sample.config.key,
|
|
107
|
-
secret: sample.config.secret,
|
|
108
|
-
bucket: sample.config.bucket,
|
|
109
|
-
acl: sample.config.acl,
|
|
110
|
-
});
|
|
111
|
-
expect(driver['acl']).toBe(sample.config.acl);
|
|
112
|
-
});
|
|
113
|
-
test('Sets private serverSideEncryption reference based on config', () => {
|
|
114
|
-
const driver = new DriverS3({
|
|
115
|
-
key: sample.config.key,
|
|
116
|
-
secret: sample.config.secret,
|
|
117
|
-
bucket: sample.config.bucket,
|
|
118
|
-
serverSideEncryption: sample.config.serverSideEncryption,
|
|
119
|
-
});
|
|
120
|
-
expect(driver['serverSideEncryption']).toBe(sample.config.serverSideEncryption);
|
|
121
|
-
});
|
|
122
|
-
test('Defaults root to empty string', () => {
|
|
123
|
-
expect(driver['root']).toBe('');
|
|
124
|
-
});
|
|
125
138
|
test('Sets http endpoints', () => {
|
|
126
139
|
const sampleDomain = randDomainName();
|
|
127
140
|
const sampleHttpEndpoint = `http://${sampleDomain}`;
|
|
@@ -194,18 +207,6 @@ describe('#constructor', () => {
|
|
|
194
207
|
},
|
|
195
208
|
});
|
|
196
209
|
});
|
|
197
|
-
test('Normalizes config path when root is given', () => {
|
|
198
|
-
const mockRoot = randDirectoryPath();
|
|
199
|
-
vi.mocked(normalizePath).mockReturnValue(mockRoot);
|
|
200
|
-
const driver = new DriverS3({
|
|
201
|
-
key: sample.config.key,
|
|
202
|
-
secret: sample.config.secret,
|
|
203
|
-
bucket: sample.config.bucket,
|
|
204
|
-
root: sample.config.root,
|
|
205
|
-
});
|
|
206
|
-
expect(normalizePath).toHaveBeenCalledWith(sample.config.root, { removeLeading: true });
|
|
207
|
-
expect(driver['root']).toBe(mockRoot);
|
|
208
|
-
});
|
|
209
210
|
});
|
|
210
211
|
describe('#fullPath', () => {
|
|
211
212
|
test('Returns normalized joined path', () => {
|
|
@@ -350,7 +351,7 @@ describe('#copy', () => {
|
|
|
350
351
|
});
|
|
351
352
|
});
|
|
352
353
|
test('Optionally sets ServerSideEncryption', async () => {
|
|
353
|
-
driver['
|
|
354
|
+
driver['config'].serverSideEncryption = sample.config.serverSideEncryption;
|
|
354
355
|
await driver.copy(sample.path.src, sample.path.dest);
|
|
355
356
|
expect(CopyObjectCommand).toHaveBeenCalledWith({
|
|
356
357
|
Key: sample.path.destFull,
|
|
@@ -360,7 +361,7 @@ describe('#copy', () => {
|
|
|
360
361
|
});
|
|
361
362
|
});
|
|
362
363
|
test('Optionally sets ACL', async () => {
|
|
363
|
-
driver['
|
|
364
|
+
driver['config'].acl = sample.config.acl;
|
|
364
365
|
await driver.copy(sample.path.src, sample.path.dest);
|
|
365
366
|
expect(CopyObjectCommand).toHaveBeenCalledWith({
|
|
366
367
|
Key: sample.path.destFull,
|
|
@@ -401,7 +402,7 @@ describe('#write', () => {
|
|
|
401
402
|
});
|
|
402
403
|
});
|
|
403
404
|
test('Optionally sets ServerSideEncryption', async () => {
|
|
404
|
-
driver['
|
|
405
|
+
driver['config'].serverSideEncryption = sample.config.serverSideEncryption;
|
|
405
406
|
await driver.write(sample.path.input, sample.stream);
|
|
406
407
|
expect(Upload).toHaveBeenCalledWith({
|
|
407
408
|
client: driver['client'],
|
|
@@ -414,7 +415,7 @@ describe('#write', () => {
|
|
|
414
415
|
});
|
|
415
416
|
});
|
|
416
417
|
test('Optionally sets ACL', async () => {
|
|
417
|
-
driver['
|
|
418
|
+
driver['config'].acl = sample.config.acl;
|
|
418
419
|
await driver.write(sample.path.input, sample.stream);
|
|
419
420
|
expect(Upload).toHaveBeenCalledWith({
|
|
420
421
|
client: driver['client'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/storage-driver-s3",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.23.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "S3 file storage abstraction for `@directus/storage`",
|
|
6
6
|
"repository": {
|
|
@@ -24,18 +24,18 @@
|
|
|
24
24
|
"access": "public"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@aws-sdk/abort-controller": "3.
|
|
28
|
-
"@aws-sdk/client-s3": "3.
|
|
29
|
-
"@aws-sdk/lib-storage": "3.
|
|
30
|
-
"@directus/storage": "9.
|
|
31
|
-
"@directus/utils": "9.
|
|
27
|
+
"@aws-sdk/abort-controller": "3.292.0",
|
|
28
|
+
"@aws-sdk/client-s3": "3.292.0",
|
|
29
|
+
"@aws-sdk/lib-storage": "3.292.0",
|
|
30
|
+
"@directus/storage": "9.23.3",
|
|
31
|
+
"@directus/utils": "9.23.3"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@directus/tsconfig": "0.0.6",
|
|
35
|
-
"@ngneat/falso": "6.
|
|
36
|
-
"@vitest/coverage-c8": "0.
|
|
37
|
-
"typescript": "4.9.
|
|
38
|
-
"vitest": "0.
|
|
35
|
+
"@ngneat/falso": "6.4.0",
|
|
36
|
+
"@vitest/coverage-c8": "0.29.3",
|
|
37
|
+
"typescript": "4.9.5",
|
|
38
|
+
"vitest": "0.29.3"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsc --build",
|