@budibase/backend-core 2.23.11 → 2.23.12
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 +91 -14
- package/dist/index.js.map +3 -3
- package/dist/index.js.meta.json +1 -1
- package/dist/package.json +4 -4
- package/dist/plugins.js +2 -0
- package/dist/plugins.js.map +2 -2
- package/dist/plugins.js.meta.json +1 -1
- package/dist/src/environment.d.ts +1 -0
- package/dist/src/environment.js +2 -1
- package/dist/src/environment.js.map +1 -1
- package/dist/src/objectStore/objectStore.d.ts +24 -6
- package/dist/src/objectStore/objectStore.js +61 -14
- package/dist/src/objectStore/objectStore.js.map +1 -1
- package/dist/src/objectStore/utils.d.ts +3 -0
- package/dist/src/objectStore/utils.js +21 -1
- package/dist/src/objectStore/utils.js.map +1 -1
- package/dist/tests/core/utilities/index.d.ts +4 -0
- package/dist/tests/core/utilities/index.js +3 -1
- package/dist/tests/core/utilities/index.js.map +1 -1
- package/dist/tests/core/utilities/minio.d.ts +2 -0
- package/dist/tests/core/utilities/minio.js +53 -0
- package/dist/tests/core/utilities/minio.js.map +1 -0
- package/package.json +4 -4
- package/src/environment.ts +2 -0
- package/src/objectStore/objectStore.ts +97 -24
- package/src/objectStore/utils.ts +26 -0
- package/tests/core/utilities/index.ts +3 -0
- package/tests/core/utilities/minio.ts +34 -0
|
@@ -7,31 +7,41 @@ import tar from "tar-fs"
|
|
|
7
7
|
import zlib from "zlib"
|
|
8
8
|
import { promisify } from "util"
|
|
9
9
|
import { join } from "path"
|
|
10
|
-
import fs, { ReadStream } from "fs"
|
|
10
|
+
import fs, { PathLike, ReadStream } from "fs"
|
|
11
11
|
import env from "../environment"
|
|
12
|
-
import { budibaseTempDir } from "./utils"
|
|
12
|
+
import { bucketTTLConfig, budibaseTempDir } from "./utils"
|
|
13
13
|
import { v4 } from "uuid"
|
|
14
14
|
import { APP_PREFIX, APP_DEV_PREFIX } from "../db"
|
|
15
|
+
import fsp from "fs/promises"
|
|
15
16
|
|
|
16
17
|
const streamPipeline = promisify(stream.pipeline)
|
|
17
18
|
// use this as a temporary store of buckets that are being created
|
|
18
19
|
const STATE = {
|
|
19
20
|
bucketCreationPromises: {},
|
|
20
21
|
}
|
|
22
|
+
const signedFilePrefix = "/files/signed"
|
|
21
23
|
|
|
22
24
|
type ListParams = {
|
|
23
25
|
ContinuationToken?: string
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
type
|
|
28
|
+
type BaseUploadParams = {
|
|
27
29
|
bucket: string
|
|
28
30
|
filename: string
|
|
29
|
-
path: string
|
|
30
31
|
type?: string | null
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
metadata?: { [key: string]: string | undefined }
|
|
33
|
+
body?: ReadableStream | Buffer
|
|
34
|
+
ttl?: number
|
|
35
|
+
addTTL?: boolean
|
|
36
|
+
extra?: any
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type UploadParams = BaseUploadParams & {
|
|
40
|
+
path?: string | PathLike
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type StreamUploadParams = BaseUploadParams & {
|
|
44
|
+
stream: ReadStream
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
const CONTENT_TYPE_MAP: any = {
|
|
@@ -41,6 +51,8 @@ const CONTENT_TYPE_MAP: any = {
|
|
|
41
51
|
js: "application/javascript",
|
|
42
52
|
json: "application/json",
|
|
43
53
|
gz: "application/gzip",
|
|
54
|
+
svg: "image/svg+xml",
|
|
55
|
+
form: "multipart/form-data",
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
const STRING_CONTENT_TYPES = [
|
|
@@ -105,7 +117,10 @@ export function ObjectStore(
|
|
|
105
117
|
* Given an object store and a bucket name this will make sure the bucket exists,
|
|
106
118
|
* if it does not exist then it will create it.
|
|
107
119
|
*/
|
|
108
|
-
export async function
|
|
120
|
+
export async function createBucketIfNotExists(
|
|
121
|
+
client: any,
|
|
122
|
+
bucketName: string
|
|
123
|
+
): Promise<{ created: boolean; exists: boolean }> {
|
|
109
124
|
bucketName = sanitizeBucket(bucketName)
|
|
110
125
|
try {
|
|
111
126
|
await client
|
|
@@ -113,15 +128,16 @@ export async function makeSureBucketExists(client: any, bucketName: string) {
|
|
|
113
128
|
Bucket: bucketName,
|
|
114
129
|
})
|
|
115
130
|
.promise()
|
|
131
|
+
return { created: false, exists: true }
|
|
116
132
|
} catch (err: any) {
|
|
117
133
|
const promises: any = STATE.bucketCreationPromises
|
|
118
134
|
const doesntExist = err.statusCode === 404,
|
|
119
135
|
noAccess = err.statusCode === 403
|
|
120
136
|
if (promises[bucketName]) {
|
|
121
137
|
await promises[bucketName]
|
|
138
|
+
return { created: false, exists: true }
|
|
122
139
|
} else if (doesntExist || noAccess) {
|
|
123
140
|
if (doesntExist) {
|
|
124
|
-
// bucket doesn't exist create it
|
|
125
141
|
promises[bucketName] = client
|
|
126
142
|
.createBucket({
|
|
127
143
|
Bucket: bucketName,
|
|
@@ -129,13 +145,15 @@ export async function makeSureBucketExists(client: any, bucketName: string) {
|
|
|
129
145
|
.promise()
|
|
130
146
|
await promises[bucketName]
|
|
131
147
|
delete promises[bucketName]
|
|
148
|
+
return { created: true, exists: false }
|
|
149
|
+
} else {
|
|
150
|
+
throw new Error("Access denied to object store bucket." + err)
|
|
132
151
|
}
|
|
133
152
|
} else {
|
|
134
153
|
throw new Error("Unable to write to object store bucket.")
|
|
135
154
|
}
|
|
136
155
|
}
|
|
137
156
|
}
|
|
138
|
-
|
|
139
157
|
/**
|
|
140
158
|
* Uploads the contents of a file given the required parameters, useful when
|
|
141
159
|
* temp files in use (for example file uploaded as an attachment).
|
|
@@ -146,12 +164,22 @@ export async function upload({
|
|
|
146
164
|
path,
|
|
147
165
|
type,
|
|
148
166
|
metadata,
|
|
167
|
+
body,
|
|
168
|
+
ttl,
|
|
149
169
|
}: UploadParams) {
|
|
150
170
|
const extension = filename.split(".").pop()
|
|
151
|
-
|
|
171
|
+
|
|
172
|
+
const fileBytes = path ? (await fsp.open(path)).createReadStream() : body
|
|
152
173
|
|
|
153
174
|
const objectStore = ObjectStore(bucketName)
|
|
154
|
-
await
|
|
175
|
+
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
|
176
|
+
|
|
177
|
+
if (ttl && (bucketCreated.created || bucketCreated.exists)) {
|
|
178
|
+
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
|
179
|
+
if (objectStore.putBucketLifecycleConfiguration) {
|
|
180
|
+
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
155
183
|
|
|
156
184
|
let contentType = type
|
|
157
185
|
if (!contentType) {
|
|
@@ -174,6 +202,7 @@ export async function upload({
|
|
|
174
202
|
}
|
|
175
203
|
config.Metadata = metadata
|
|
176
204
|
}
|
|
205
|
+
|
|
177
206
|
return objectStore.upload(config).promise()
|
|
178
207
|
}
|
|
179
208
|
|
|
@@ -181,14 +210,24 @@ export async function upload({
|
|
|
181
210
|
* Similar to the upload function but can be used to send a file stream
|
|
182
211
|
* through to the object store.
|
|
183
212
|
*/
|
|
184
|
-
export async function streamUpload(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
213
|
+
export async function streamUpload({
|
|
214
|
+
bucket: bucketName,
|
|
215
|
+
stream,
|
|
216
|
+
filename,
|
|
217
|
+
type,
|
|
218
|
+
extra,
|
|
219
|
+
ttl,
|
|
220
|
+
}: StreamUploadParams) {
|
|
221
|
+
const extension = filename.split(".").pop()
|
|
190
222
|
const objectStore = ObjectStore(bucketName)
|
|
191
|
-
await
|
|
223
|
+
const bucketCreated = await createBucketIfNotExists(objectStore, bucketName)
|
|
224
|
+
|
|
225
|
+
if (ttl && (bucketCreated.created || bucketCreated.exists)) {
|
|
226
|
+
let ttlConfig = bucketTTLConfig(bucketName, ttl)
|
|
227
|
+
if (objectStore.putBucketLifecycleConfiguration) {
|
|
228
|
+
await objectStore.putBucketLifecycleConfiguration(ttlConfig).promise()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
192
231
|
|
|
193
232
|
// Set content type for certain known extensions
|
|
194
233
|
if (filename?.endsWith(".js")) {
|
|
@@ -203,10 +242,18 @@ export async function streamUpload(
|
|
|
203
242
|
}
|
|
204
243
|
}
|
|
205
244
|
|
|
245
|
+
let contentType = type
|
|
246
|
+
if (!contentType) {
|
|
247
|
+
contentType = extension
|
|
248
|
+
? CONTENT_TYPE_MAP[extension.toLowerCase()]
|
|
249
|
+
: CONTENT_TYPE_MAP.txt
|
|
250
|
+
}
|
|
251
|
+
|
|
206
252
|
const params = {
|
|
207
253
|
Bucket: sanitizeBucket(bucketName),
|
|
208
254
|
Key: sanitizeKey(filename),
|
|
209
255
|
Body: stream,
|
|
256
|
+
ContentType: contentType,
|
|
210
257
|
...extra,
|
|
211
258
|
}
|
|
212
259
|
return objectStore.upload(params).promise()
|
|
@@ -286,7 +333,7 @@ export function getPresignedUrl(
|
|
|
286
333
|
const signedUrl = new URL(url)
|
|
287
334
|
const path = signedUrl.pathname
|
|
288
335
|
const query = signedUrl.search
|
|
289
|
-
return
|
|
336
|
+
return `${signedFilePrefix}${path}${query}`
|
|
290
337
|
}
|
|
291
338
|
}
|
|
292
339
|
|
|
@@ -341,7 +388,7 @@ export async function retrieveDirectory(bucketName: string, path: string) {
|
|
|
341
388
|
*/
|
|
342
389
|
export async function deleteFile(bucketName: string, filepath: string) {
|
|
343
390
|
const objectStore = ObjectStore(bucketName)
|
|
344
|
-
await
|
|
391
|
+
await createBucketIfNotExists(objectStore, bucketName)
|
|
345
392
|
const params = {
|
|
346
393
|
Bucket: bucketName,
|
|
347
394
|
Key: sanitizeKey(filepath),
|
|
@@ -351,7 +398,7 @@ export async function deleteFile(bucketName: string, filepath: string) {
|
|
|
351
398
|
|
|
352
399
|
export async function deleteFiles(bucketName: string, filepaths: string[]) {
|
|
353
400
|
const objectStore = ObjectStore(bucketName)
|
|
354
|
-
await
|
|
401
|
+
await createBucketIfNotExists(objectStore, bucketName)
|
|
355
402
|
const params = {
|
|
356
403
|
Bucket: bucketName,
|
|
357
404
|
Delete: {
|
|
@@ -412,7 +459,13 @@ export async function uploadDirectory(
|
|
|
412
459
|
if (file.isDirectory()) {
|
|
413
460
|
uploads.push(uploadDirectory(bucketName, local, path))
|
|
414
461
|
} else {
|
|
415
|
-
uploads.push(
|
|
462
|
+
uploads.push(
|
|
463
|
+
streamUpload({
|
|
464
|
+
bucket: bucketName,
|
|
465
|
+
filename: path,
|
|
466
|
+
stream: fs.createReadStream(local),
|
|
467
|
+
})
|
|
468
|
+
)
|
|
416
469
|
}
|
|
417
470
|
}
|
|
418
471
|
await Promise.all(uploads)
|
|
@@ -467,3 +520,23 @@ export async function getReadStream(
|
|
|
467
520
|
}
|
|
468
521
|
return client.getObject(params).createReadStream()
|
|
469
522
|
}
|
|
523
|
+
|
|
524
|
+
/*
|
|
525
|
+
Given a signed url like '/files/signed/tmp-files-attachments/app_123456/myfile.txt' extract
|
|
526
|
+
the bucket and the path from it
|
|
527
|
+
*/
|
|
528
|
+
export function extractBucketAndPath(
|
|
529
|
+
url: string
|
|
530
|
+
): { bucket: string; path: string } | null {
|
|
531
|
+
const baseUrl = url.split("?")[0]
|
|
532
|
+
|
|
533
|
+
const regex = new RegExp(`^${signedFilePrefix}/(?<bucket>[^/]+)/(?<path>.+)$`)
|
|
534
|
+
const match = baseUrl.match(regex)
|
|
535
|
+
|
|
536
|
+
if (match && match.groups) {
|
|
537
|
+
const { bucket, path } = match.groups
|
|
538
|
+
return { bucket, path }
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return null
|
|
542
|
+
}
|
package/src/objectStore/utils.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from "path"
|
|
|
2
2
|
import { tmpdir } from "os"
|
|
3
3
|
import fs from "fs"
|
|
4
4
|
import env from "../environment"
|
|
5
|
+
import { PutBucketLifecycleConfigurationRequest } from "aws-sdk/clients/s3"
|
|
5
6
|
|
|
6
7
|
/****************************************************
|
|
7
8
|
* NOTE: When adding a new bucket - name *
|
|
@@ -15,6 +16,7 @@ export const ObjectStoreBuckets = {
|
|
|
15
16
|
TEMPLATES: env.TEMPLATES_BUCKET_NAME,
|
|
16
17
|
GLOBAL: env.GLOBAL_BUCKET_NAME,
|
|
17
18
|
PLUGINS: env.PLUGIN_BUCKET_NAME,
|
|
19
|
+
TEMP: env.TEMP_BUCKET_NAME,
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
const bbTmp = join(tmpdir(), ".budibase")
|
|
@@ -29,3 +31,27 @@ try {
|
|
|
29
31
|
export function budibaseTempDir() {
|
|
30
32
|
return bbTmp
|
|
31
33
|
}
|
|
34
|
+
|
|
35
|
+
export const bucketTTLConfig = (
|
|
36
|
+
bucketName: string,
|
|
37
|
+
days: number
|
|
38
|
+
): PutBucketLifecycleConfigurationRequest => {
|
|
39
|
+
const lifecycleRule = {
|
|
40
|
+
ID: `${bucketName}-ExpireAfter${days}days`,
|
|
41
|
+
Prefix: "",
|
|
42
|
+
Status: "Enabled",
|
|
43
|
+
Expiration: {
|
|
44
|
+
Days: days,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
const lifecycleConfiguration = {
|
|
48
|
+
Rules: [lifecycleRule],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const params = {
|
|
52
|
+
Bucket: bucketName,
|
|
53
|
+
LifecycleConfiguration: lifecycleConfiguration,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return params
|
|
57
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { GenericContainer, Wait, StartedTestContainer } from "testcontainers"
|
|
2
|
+
import { AbstractWaitStrategy } from "testcontainers/build/wait-strategies/wait-strategy"
|
|
3
|
+
import env from "../../../src/environment"
|
|
4
|
+
|
|
5
|
+
let container: StartedTestContainer | undefined
|
|
6
|
+
|
|
7
|
+
class ObjectStoreWaitStrategy extends AbstractWaitStrategy {
|
|
8
|
+
async waitUntilReady(container: any, boundPorts: any, startTime?: Date) {
|
|
9
|
+
const logs = Wait.forListeningPorts()
|
|
10
|
+
await logs.waitUntilReady(container, boundPorts, startTime)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function start(): Promise<void> {
|
|
15
|
+
container = await new GenericContainer("minio/minio")
|
|
16
|
+
.withExposedPorts(9000)
|
|
17
|
+
.withCommand(["server", "/data"])
|
|
18
|
+
.withEnvironment({
|
|
19
|
+
MINIO_ACCESS_KEY: "budibase",
|
|
20
|
+
MINIO_SECRET_KEY: "budibase",
|
|
21
|
+
})
|
|
22
|
+
.withWaitStrategy(new ObjectStoreWaitStrategy().withStartupTimeout(30000))
|
|
23
|
+
.start()
|
|
24
|
+
|
|
25
|
+
const port = container.getMappedPort(9000)
|
|
26
|
+
env._set("MINIO_URL", `http://0.0.0.0:${port}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function stop() {
|
|
30
|
+
if (container) {
|
|
31
|
+
await container.stop()
|
|
32
|
+
container = undefined
|
|
33
|
+
}
|
|
34
|
+
}
|