@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.
@@ -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 UploadParams = {
28
+ type BaseUploadParams = {
27
29
  bucket: string
28
30
  filename: string
29
- path: string
30
31
  type?: string | null
31
- // can be undefined, we will remove it
32
- metadata?: {
33
- [key: string]: string | undefined
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 makeSureBucketExists(client: any, bucketName: string) {
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
- const fileBytes = fs.readFileSync(path)
171
+
172
+ const fileBytes = path ? (await fsp.open(path)).createReadStream() : body
152
173
 
153
174
  const objectStore = ObjectStore(bucketName)
154
- await makeSureBucketExists(objectStore, bucketName)
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
- bucketName: string,
186
- filename: string,
187
- stream: ReadStream | ReadableStream,
188
- extra = {}
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 makeSureBucketExists(objectStore, bucketName)
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 `/files/signed${path}${query}`
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 makeSureBucketExists(objectStore, bucketName)
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 makeSureBucketExists(objectStore, bucketName)
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(streamUpload(bucketName, path, fs.createReadStream(local)))
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
+ }
@@ -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
+ }
@@ -4,3 +4,6 @@ export { generator } from "./structures"
4
4
  export * as testContainerUtils from "./testContainerUtils"
5
5
  export * as utils from "./utils"
6
6
  export * from "./jestUtils"
7
+ import * as minio from "./minio"
8
+
9
+ export const objectStoreTestProviders = { minio }
@@ -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
+ }