@quatrain/storage-supabase 1.1.17 → 1.1.19
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/lib/SupabaseStorageAdapter.js +2 -2
- package/package.json +38 -40
- package/src/SupabaseStorageAdapter.ts +250 -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.
|
|
@@ -26,7 +26,7 @@ class SupabaseStorageAdapter extends storage_1.AbstractStorageAdapter {
|
|
|
26
26
|
apikey: params.config.secret,
|
|
27
27
|
Authorization: `Bearer ${params.config.secret}`,
|
|
28
28
|
});
|
|
29
|
-
storage_1.Storage.
|
|
29
|
+
storage_1.Storage.info(`[SSA] Supabase Storage Adapter initialized`);
|
|
30
30
|
}
|
|
31
31
|
getDriver() {
|
|
32
32
|
return this._client;
|
|
@@ -80,7 +80,7 @@ class SupabaseStorageAdapter extends storage_1.AbstractStorageAdapter {
|
|
|
80
80
|
contentType: file.contentType,
|
|
81
81
|
});
|
|
82
82
|
if (error !== null) {
|
|
83
|
-
|
|
83
|
+
storage_1.Storage.error(error);
|
|
84
84
|
storage_1.Storage.error(`Unable to upload ${file.ref} to ${file.bucket}`);
|
|
85
85
|
throw new Error(`Unable to upload ${file.ref} to ${file.bucket}`);
|
|
86
86
|
}
|
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-supabase",
|
|
3
|
+
"version": "1.1.19",
|
|
4
|
+
"description": "Storage adapter for Supabase Storage",
|
|
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
|
+
"@quatrain/core": "^1.1.43",
|
|
29
|
+
"@quatrain/storage": "^1.1.24",
|
|
30
|
+
"@supabase/storage-js": "^2.7.2",
|
|
31
|
+
"@supabase/supabase-js": "^2.87.3"
|
|
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,250 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Storage,
|
|
3
|
+
AbstractStorageAdapter,
|
|
4
|
+
FileType,
|
|
5
|
+
FileResponseLinkType,
|
|
6
|
+
StorageParameters,
|
|
7
|
+
DownloadFileMetaType,
|
|
8
|
+
} from '@quatrain/storage'
|
|
9
|
+
import { Readable, Stream } from 'stream'
|
|
10
|
+
import { StorageClient } from '@supabase/storage-js'
|
|
11
|
+
import { tmpdir } from 'os'
|
|
12
|
+
import { join } from 'path'
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
|
|
15
|
+
export class SupabaseStorageAdapter extends AbstractStorageAdapter {
|
|
16
|
+
protected _client: StorageClient
|
|
17
|
+
|
|
18
|
+
constructor(params: StorageParameters) {
|
|
19
|
+
super(params)
|
|
20
|
+
this._client = new StorageClient(params.config.endpoint, {
|
|
21
|
+
apikey: params.config.secret,
|
|
22
|
+
Authorization: `Bearer ${params.config.secret}`,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
Storage.info(`[SSA] Supabase Storage Adapter initialized`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getDriver() {
|
|
29
|
+
return this._client
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getMetaData(file: FileType): Promise<FileType> {
|
|
33
|
+
return new Promise(() => file)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async test(): Promise<boolean> {
|
|
37
|
+
try {
|
|
38
|
+
const { data, error } = await this._client.listBuckets()
|
|
39
|
+
|
|
40
|
+
if (error !== null) {
|
|
41
|
+
Storage.error(`Unable to get buckets list`)
|
|
42
|
+
throw new Error(`Unable to get buckets list: ${error.message}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Storage.info(`S3 Buckets: ${JSON.stringify(data)}`)
|
|
46
|
+
|
|
47
|
+
return true
|
|
48
|
+
} catch (err) {
|
|
49
|
+
Storage.error(
|
|
50
|
+
`Failed to connect to storage: ${(err as Error).message}`
|
|
51
|
+
)
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async streamToBuffer(stream: Stream): Promise<Buffer> {
|
|
57
|
+
return new Promise<Buffer>((resolve, reject) => {
|
|
58
|
+
const _buf: any[] = []
|
|
59
|
+
|
|
60
|
+
stream.on('data', (chunk) => _buf.push(chunk))
|
|
61
|
+
stream.on('end', () => resolve(Buffer.concat(_buf)))
|
|
62
|
+
stream.on('error', (err) => reject(err))
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
toArrayBuffer(buffer: Buffer): ArrayBuffer {
|
|
67
|
+
const arrayBuffer = new ArrayBuffer(buffer.length)
|
|
68
|
+
const view = new Uint8Array(arrayBuffer)
|
|
69
|
+
for (let i = 0; i < buffer.length; ++i) {
|
|
70
|
+
view[i] = buffer[i]
|
|
71
|
+
}
|
|
72
|
+
return arrayBuffer
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async create(file: FileType, stream: Readable | string): Promise<FileType> {
|
|
76
|
+
Storage.info(`[SSA] Uploading ${file.ref} to ${file.bucket}`)
|
|
77
|
+
|
|
78
|
+
if (typeof stream === 'string') {
|
|
79
|
+
const { error } = await this._client
|
|
80
|
+
.from(file.bucket)
|
|
81
|
+
.upload(file.ref, new Blob([fs.readFileSync(stream)]), {
|
|
82
|
+
upsert: true,
|
|
83
|
+
contentType: file.contentType,
|
|
84
|
+
})
|
|
85
|
+
if (error !== null) {
|
|
86
|
+
Storage.error(error)
|
|
87
|
+
Storage.error(`Unable to upload ${file.ref} to ${file.bucket}`)
|
|
88
|
+
throw new Error(`Unable to upload ${file.ref} to ${file.bucket}`)
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
const content = new Blob([
|
|
92
|
+
this.toArrayBuffer(await this.streamToBuffer(stream)),
|
|
93
|
+
])
|
|
94
|
+
const { error } = await this._client
|
|
95
|
+
.from(file.bucket)
|
|
96
|
+
.upload(file.ref, content, {
|
|
97
|
+
// cacheControl: '3600',
|
|
98
|
+
upsert: false,
|
|
99
|
+
contentType: file.contentType,
|
|
100
|
+
})
|
|
101
|
+
if (error !== null) {
|
|
102
|
+
console.log(error)
|
|
103
|
+
Storage.error(`Unable to upload ${file.ref} to ${file.bucket}`)
|
|
104
|
+
throw new Error(`Unable to upload ${file.ref} to ${file.bucket}`)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return file
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async copy(file: FileType, destFile: FileType) {
|
|
112
|
+
try {
|
|
113
|
+
const response = await this._client
|
|
114
|
+
.from(file.bucket)
|
|
115
|
+
.copy(file.ref, destFile.ref, {
|
|
116
|
+
destinationBucket: destFile.bucket,
|
|
117
|
+
})
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error(err)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async move(file: FileType, destFile: FileType) {
|
|
124
|
+
Storage.info(
|
|
125
|
+
`Moving file ${file.ref} to ${destFile.ref} in same bucket ${file.bucket}`
|
|
126
|
+
)
|
|
127
|
+
const { error } = await this._client
|
|
128
|
+
.from(file.bucket)
|
|
129
|
+
.move(file.ref, destFile.ref)
|
|
130
|
+
|
|
131
|
+
if (error !== null) {
|
|
132
|
+
Storage.error(
|
|
133
|
+
`Unable to move ${file.ref} to ${destFile.ref}: ${error.message}`
|
|
134
|
+
)
|
|
135
|
+
throw new Error(`Unable to move ${file.ref} to ${destFile.ref}`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return destFile
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async getUrl(file: FileType, expiresIn = 3600) {
|
|
142
|
+
Storage.debug(
|
|
143
|
+
`Getting signed url for file ${file.ref} in bucket ${file.bucket}`
|
|
144
|
+
)
|
|
145
|
+
const { data, error } = await this._client
|
|
146
|
+
.from(file.bucket)
|
|
147
|
+
.createSignedUrl(file.ref, expiresIn, { download: true })
|
|
148
|
+
|
|
149
|
+
if (error !== null) {
|
|
150
|
+
throw new Error(`Unable to get signed url: ${error}`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { url: data?.signedUrl, expiresIn }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async delete(file: FileType) {
|
|
157
|
+
Storage.info(`Deleting file ${file.ref} in bucket ${file.bucket}`)
|
|
158
|
+
const { error } = await this._client.from(file.bucket).remove([file.ref])
|
|
159
|
+
|
|
160
|
+
if (error !== null) {
|
|
161
|
+
throw new Error(`Unable to delete ${file.ref}`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
Storage.info(`Object ${file.ref} successfully deleted`)
|
|
165
|
+
|
|
166
|
+
return true
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async getReadable(file: FileType): Promise<Readable> {
|
|
170
|
+
Storage.debug(`GET Readable : ${file.ref}`)
|
|
171
|
+
|
|
172
|
+
const path = join(tmpdir(), String(Date.now()))
|
|
173
|
+
const item = await this.download(file, { path, onlyContent: true })
|
|
174
|
+
const buffer = Buffer.from(item.toString(), 'base64')
|
|
175
|
+
const readable = new Readable()
|
|
176
|
+
readable.push(buffer)
|
|
177
|
+
readable.push(null)
|
|
178
|
+
|
|
179
|
+
return readable
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async stream(file: FileType, res: any) {
|
|
183
|
+
Storage.debug(`GET Stream : ${file.ref}`)
|
|
184
|
+
|
|
185
|
+
const path = join(tmpdir(), String(Date.now()))
|
|
186
|
+
const item = await this.download(file, { path, onlyContent: true })
|
|
187
|
+
const buffer = Buffer.from(item.toString(), 'base64')
|
|
188
|
+
const readable = new Readable()
|
|
189
|
+
readable.push(buffer)
|
|
190
|
+
readable.push(null)
|
|
191
|
+
|
|
192
|
+
return readable.pipe(res)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async download(
|
|
196
|
+
file: FileType,
|
|
197
|
+
meta: DownloadFileMetaType
|
|
198
|
+
): Promise<string | Blob> {
|
|
199
|
+
const { data, error } = await this._client
|
|
200
|
+
.from(file.bucket)
|
|
201
|
+
.download(file.ref)
|
|
202
|
+
|
|
203
|
+
if (error !== null) {
|
|
204
|
+
console.log(error)
|
|
205
|
+
throw new Error(`Unable to download ${file.ref}`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (meta.onlyContent) {
|
|
209
|
+
return data
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Write the downloaded data to the specified path
|
|
213
|
+
const buffer = await data.arrayBuffer()
|
|
214
|
+
fs.writeFileSync(meta.path, Buffer.from(buffer))
|
|
215
|
+
|
|
216
|
+
return meta.path
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get signed upload url for file
|
|
221
|
+
* Careful, on docker the time to live for JWT signature is defaulted to 60 sec
|
|
222
|
+
* Add SIGNED_UPLOAD_URL_EXPIRATION_TIME env variable to fix TTL
|
|
223
|
+
* @param file FileType
|
|
224
|
+
* @param _expiresIn actually ignored
|
|
225
|
+
* @returns
|
|
226
|
+
*/
|
|
227
|
+
async getUploadUrl(
|
|
228
|
+
file: FileType,
|
|
229
|
+
_expiresIn = 7200
|
|
230
|
+
): Promise<FileResponseLinkType> {
|
|
231
|
+
const { data, error } = await this._client
|
|
232
|
+
.from(file.bucket)
|
|
233
|
+
.createSignedUploadUrl(file.ref, { upsert: true })
|
|
234
|
+
|
|
235
|
+
if (error !== null) {
|
|
236
|
+
throw new Error(`Unable to get signed upload url: ${error}`)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
Storage.info(
|
|
240
|
+
`Upload URL for ${file.ref} in bucket ${file.bucket} is ${data?.signedUrl}`
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
url: data?.signedUrl,
|
|
245
|
+
method: 'PUT',
|
|
246
|
+
accept: file.contentType || 'application/octet-stream',
|
|
247
|
+
expiresIn: 7200, // fixed value in Supabase
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
package/src/index.ts
ADDED