@quatrain/storage-s3 1.1.14 → 1.1.15
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/LICENSE.md +15 -0
- package/README.md +26 -1
- package/package.json +38 -40
- package/src/S3StorageAdapter.ts +242 -0
- package/src/index.ts +3 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# LICENSE UPDATE NOTICE
|
|
2
|
+
|
|
3
|
+
As of 01/01/2026, Quatrain Core is licensed under the **GNU Affero General Public License v3.0 (AGPL v3)**.
|
|
4
|
+
Previous versions remain under the MIT License.
|
|
5
|
+
|
|
6
|
+
## Why AGPL?
|
|
7
|
+
|
|
8
|
+
We believe in open collaboration for the development ecosystem. The AGPL ensures that any modification or deployment of this BaaS stack, including over a network, benefits the entire community.
|
|
9
|
+
|
|
10
|
+
## Commercial Services & Enterprise Usage
|
|
11
|
+
|
|
12
|
+
We provide official deployment services, technical training, and certification for Quatrain Core.
|
|
13
|
+
For organizations requiring a non-copyleft license (commercial license) or custom proprietary integrations, please contact the copyright holder: **Quatrain Technologies**.
|
|
14
|
+
|
|
15
|
+
Copyright © 2024-2026 Quatrain Technologies. All Rights Reserved.
|
package/README.md
CHANGED
|
@@ -1 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @quatrain/storage-s3
|
|
2
|
+
|
|
3
|
+
A storage adapter for AWS S3 and other S3-compatible services like MinIO or DigitalOcean Spaces.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Implements the `@quatrain/storage` abstract adapter.
|
|
8
|
+
- Works with any S3-compatible object storage provider.
|
|
9
|
+
- Leverages the official AWS SDK for JavaScript (`@aws-sdk/client-s3`).
|
|
10
|
+
- Supports multipart uploads for large files.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @quatrain/storage-s3 @aws-sdk/client-s3
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { Storage } from '@quatrain/storage';
|
|
22
|
+
import { S3StorageAdapter } from '@quatrain/storage-s3';
|
|
23
|
+
|
|
24
|
+
const adapter = new S3StorageAdapter({ config: { region: 'us-east-1', credentials: { ... } } });
|
|
25
|
+
Storage.addAdapter(adapter, 'default', true);
|
|
26
|
+
```
|
package/package.json
CHANGED
|
@@ -1,41 +1,39 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
}
|
|
2
|
+
"name": "@quatrain/storage-s3",
|
|
3
|
+
"version": "1.1.15",
|
|
4
|
+
"description": "Storage adapter for S3 compatible services",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"bun": "src/index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"LICENSE.md",
|
|
10
|
+
"src/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"author": "Quatrain Développement SAS <developers@quatrain.com>",
|
|
15
|
+
"license": "AGPL-3.0-only",
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@tsconfig/recommended": "^1.0.1",
|
|
18
|
+
"@types/jest": "^27.0.3",
|
|
19
|
+
"@types/node": "^22.10.1",
|
|
20
|
+
"jest": "^30.2.0",
|
|
21
|
+
"jest-node-exports-resolver": "^1.1.6",
|
|
22
|
+
"trace-unhandled": "^2.0.1",
|
|
23
|
+
"ts-jest": "^27.1.2",
|
|
24
|
+
"ts-node": "^10.4.0",
|
|
25
|
+
"typescript": "^5.1.5"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@aws-sdk/client-s3": "^3.337.0",
|
|
29
|
+
"@aws-sdk/s3-request-presigner": "^3.342.0",
|
|
30
|
+
"@quatrain/storage": "^1.1.24",
|
|
31
|
+
"fs-extra": "^11.2.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test-ci": "jest --runInBand",
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"wbuild": "tsc --watch",
|
|
37
|
+
"bump-to": "yarn version"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AbstractStorageAdapter,
|
|
3
|
+
Storage,
|
|
4
|
+
FileType,
|
|
5
|
+
FileResponseLinkType,
|
|
6
|
+
StorageParameters,
|
|
7
|
+
DownloadFileMetaType,
|
|
8
|
+
} from '@quatrain/storage'
|
|
9
|
+
import { Readable, Stream } from 'stream'
|
|
10
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
|
|
11
|
+
import { createWriteStream } from 'fs'
|
|
12
|
+
import {
|
|
13
|
+
S3Client,
|
|
14
|
+
PutObjectCommand,
|
|
15
|
+
GetObjectCommand,
|
|
16
|
+
DeleteObjectCommand,
|
|
17
|
+
CopyObjectCommand,
|
|
18
|
+
HeadObjectCommand,
|
|
19
|
+
ListBucketsCommand,
|
|
20
|
+
} from '@aws-sdk/client-s3'
|
|
21
|
+
|
|
22
|
+
export class S3StorageAdapter extends AbstractStorageAdapter {
|
|
23
|
+
protected _client: S3Client
|
|
24
|
+
|
|
25
|
+
constructor(params: StorageParameters) {
|
|
26
|
+
super(params)
|
|
27
|
+
|
|
28
|
+
const config = {
|
|
29
|
+
forcePathStyle: true,
|
|
30
|
+
region: this._params.config.region,
|
|
31
|
+
endpoint: this._params.config.endpoint,
|
|
32
|
+
credentials: {
|
|
33
|
+
accessKeyId: this._params.config.accesskey,
|
|
34
|
+
secretAccessKey: this._params.config.secret,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this._client = new S3Client(config)
|
|
39
|
+
|
|
40
|
+
Storage.info(
|
|
41
|
+
`AWS Storage Adapter initialized in ${
|
|
42
|
+
this._params.config.endpoint || this._params.config.region
|
|
43
|
+
}`
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async test(): Promise<boolean> {
|
|
48
|
+
try {
|
|
49
|
+
const command = new ListBucketsCommand()
|
|
50
|
+
const res = await this._client.send(command)
|
|
51
|
+
//Storage.info(`S3 Buckets: ${JSON.stringify(res.Buckets)}`)
|
|
52
|
+
return true
|
|
53
|
+
} catch (err) {
|
|
54
|
+
Storage.error(`Failed to connect to S3: ${(err as Error).message}`)
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getDriver() {
|
|
60
|
+
return this._client
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async streamToBuffer(stream: Stream): Promise<Buffer> {
|
|
64
|
+
return new Promise<Buffer>((resolve, reject) => {
|
|
65
|
+
const _buf: any[] = []
|
|
66
|
+
|
|
67
|
+
stream.on('data', (chunk) => _buf.push(chunk))
|
|
68
|
+
stream.on('end', () => resolve(Buffer.concat(_buf)))
|
|
69
|
+
stream.on('error', (err) => reject(err))
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async create(file: FileType, stream: Readable): Promise<FileType> {
|
|
74
|
+
try {
|
|
75
|
+
const input = {
|
|
76
|
+
Bucket: file.bucket || this._params.config.bucket,
|
|
77
|
+
Key: file.ref,
|
|
78
|
+
Body: await this.streamToBuffer(stream),
|
|
79
|
+
ContentType: file.contentType,
|
|
80
|
+
}
|
|
81
|
+
Storage.debug(`Uploading ${file.ref} to ${file.bucket}`)
|
|
82
|
+
|
|
83
|
+
const command = new PutObjectCommand(input)
|
|
84
|
+
await this._client.send(command)
|
|
85
|
+
|
|
86
|
+
return file
|
|
87
|
+
} catch (err) {
|
|
88
|
+
Storage.error(
|
|
89
|
+
`Failed to upload file ${file.ref} to ${file.bucket}: ${
|
|
90
|
+
(err as Error).message
|
|
91
|
+
}`
|
|
92
|
+
)
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Failed to upload file ${file.ref} to ${file.bucket}: ${
|
|
95
|
+
(err as Error).message
|
|
96
|
+
}`
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async copy(file: FileType, destFile: FileType) {
|
|
102
|
+
Storage.debug(
|
|
103
|
+
`Copying file ${file.bucket}/${file.ref} to ${file.bucket}/${destFile.ref}`
|
|
104
|
+
)
|
|
105
|
+
const command = new CopyObjectCommand({
|
|
106
|
+
CopySource: encodeURI(`${file.bucket}/${file.ref}`),
|
|
107
|
+
Bucket: file.bucket,
|
|
108
|
+
Key: encodeURI(destFile.ref),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await this._client.send(command)
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error(err)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Move file to given destination
|
|
120
|
+
* There is no native command in AWS SDK for this, we need to copy then delete source media
|
|
121
|
+
* @param file
|
|
122
|
+
* @param destFile
|
|
123
|
+
* @returns
|
|
124
|
+
*/
|
|
125
|
+
async move(file: FileType, destFile: FileType): Promise<FileType> {
|
|
126
|
+
try {
|
|
127
|
+
Storage.debug(
|
|
128
|
+
`Moving file ${file.bucket}/${file.ref} to ${file.bucket}/${destFile.ref}`
|
|
129
|
+
)
|
|
130
|
+
await this.copy(file, destFile)
|
|
131
|
+
await this.delete(file)
|
|
132
|
+
} catch (err) {
|
|
133
|
+
Storage.error(
|
|
134
|
+
`Failed to move file from ${file.ref} to ${destFile.ref}: ${
|
|
135
|
+
(err as Error).message
|
|
136
|
+
}`
|
|
137
|
+
)
|
|
138
|
+
console.log(err)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return destFile
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async getUrl(file: FileType, expiresIn = 3600) {
|
|
145
|
+
const command = new GetObjectCommand({
|
|
146
|
+
Bucket: file.bucket,
|
|
147
|
+
Key: encodeURI(file.ref),
|
|
148
|
+
})
|
|
149
|
+
Storage.debug(`Creating public url for ${file.bucket}/${file.ref}`)
|
|
150
|
+
const url = await getSignedUrl(this._client, command, { expiresIn })
|
|
151
|
+
return { url, expiresIn }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async delete(file: FileType) {
|
|
155
|
+
Storage.debug(`Deleting file ${file.bucket}/${file.ref}`)
|
|
156
|
+
const command = new DeleteObjectCommand({
|
|
157
|
+
Bucket: file.bucket,
|
|
158
|
+
Key: file.ref,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
await this._client?.send(command)
|
|
162
|
+
Storage.info(`Object ${file.ref} successfully deleted`)
|
|
163
|
+
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async getReadable(file: FileType): Promise<Readable> {
|
|
168
|
+
Storage.debug(`GET Readable for ${file.ref}`)
|
|
169
|
+
const command = new GetObjectCommand({
|
|
170
|
+
Bucket: file.bucket || this._params.config.bucket,
|
|
171
|
+
Key: file.ref,
|
|
172
|
+
})
|
|
173
|
+
const item = await this._client?.send(command)
|
|
174
|
+
const ByteArray: any = await item?.Body?.transformToByteArray()
|
|
175
|
+
const buffer = Buffer.from(ByteArray, 'base64')
|
|
176
|
+
const readable = new Readable()
|
|
177
|
+
readable.push(buffer)
|
|
178
|
+
readable.push(null)
|
|
179
|
+
|
|
180
|
+
return readable
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async getMetaData(file: FileType): Promise<FileType> {
|
|
184
|
+
const command = new HeadObjectCommand({
|
|
185
|
+
Bucket: file.bucket,
|
|
186
|
+
Key: file.ref,
|
|
187
|
+
})
|
|
188
|
+
const item = await this._client.send(command)
|
|
189
|
+
const meta = {
|
|
190
|
+
...file,
|
|
191
|
+
contentType: item.ContentType,
|
|
192
|
+
size: item.ContentLength,
|
|
193
|
+
lastModified: item.LastModified,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return meta
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async stream(file: FileType, res: any) {
|
|
200
|
+
const command = new GetObjectCommand({
|
|
201
|
+
Bucket: file.bucket,
|
|
202
|
+
Key: file.ref,
|
|
203
|
+
})
|
|
204
|
+
const item = await this._client.send(command)
|
|
205
|
+
const ByteArray: any = await item.Body?.transformToByteArray()
|
|
206
|
+
const buffer = Buffer.from(ByteArray, 'base64')
|
|
207
|
+
const readable = new Readable()
|
|
208
|
+
readable.push(buffer)
|
|
209
|
+
readable.push(null)
|
|
210
|
+
|
|
211
|
+
return readable.pipe(res)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async download(file: FileType, meta: DownloadFileMetaType) {
|
|
215
|
+
const stream: Readable = await this.getReadable(file)
|
|
216
|
+
await new Promise((res) =>
|
|
217
|
+
stream.pipe(createWriteStream(meta.path)).on('close', res)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return meta.path
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async getUploadUrl(
|
|
224
|
+
file: FileType,
|
|
225
|
+
expiresIn = 3600
|
|
226
|
+
): Promise<FileResponseLinkType> {
|
|
227
|
+
const command = new PutObjectCommand({
|
|
228
|
+
Bucket: file.bucket || this._params.config.bucket,
|
|
229
|
+
Key: file.ref,
|
|
230
|
+
ContentType: file.contentType,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const url = await getSignedUrl(this._client, command, { expiresIn })
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
url,
|
|
237
|
+
method: 'PUT',
|
|
238
|
+
accept: file.contentType || 'application/octet-stream',
|
|
239
|
+
expiresIn,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
package/src/index.ts
ADDED