From e40c1bedde11c4440e2be0b74989073decaab5ad Mon Sep 17 00:00:00 2001 From: Fabrizio Date: Thu, 6 Apr 2023 09:00:44 +0100 Subject: [PATCH] feat: custom file size limit and mime types at bucket level (#151) --- infra/storage/Dockerfile | 2 +- src/lib/types.ts | 2 + src/packages/StorageBucketApi.ts | 26 ++++++++-- test/__snapshots__/storageApi.test.ts.snap | 8 ++++ test/storageApi.test.ts | 26 +++++++++- test/storageFileApi.test.ts | 56 ++++++++++++++++++++++ 6 files changed, 114 insertions(+), 6 deletions(-) diff --git a/infra/storage/Dockerfile b/infra/storage/Dockerfile index 7ffd6d7..c5d0683 100644 --- a/infra/storage/Dockerfile +++ b/infra/storage/Dockerfile @@ -1,3 +1,3 @@ -FROM supabase/storage-api:v0.28.0 +FROM supabase/storage-api:v0.29.1 RUN apk add curl --no-cache \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index e1d2390..8ad239b 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -2,6 +2,8 @@ export interface Bucket { id: string name: string owner: string + file_size_limit?: number + allowed_mime_types?: string[] created_at: string updated_at: string public: boolean diff --git a/src/packages/StorageBucketApi.ts b/src/packages/StorageBucketApi.ts index 6741a97..eb20931 100644 --- a/src/packages/StorageBucketApi.ts +++ b/src/packages/StorageBucketApi.ts @@ -74,11 +74,15 @@ export default class StorageBucketApi { * * @param id A unique identifier for the bucket you are creating. * @param options.public The visibility of the bucket. Public buckets don't require an authorization token to download objects, but still require a valid token for all other operations. By default, buckets are private. + * @param options.fileSizeLimit specifies the file size limit that this bucket can accept during upload + * @param options.allowedMimeTypes specifies the allowed mime types that this bucket can accept during upload * @returns newly created bucket id */ async createBucket( id: string, - options: { public: boolean } = { public: false } + options: { public: boolean; fileSizeLimit?: number | string; allowedMimeTypes?: string[] } = { + public: false, + } ): Promise< | { data: Pick @@ -93,7 +97,13 @@ export default class StorageBucketApi { const data = await post( this.fetch, `${this.url}/bucket`, - { id, name: id, public: options.public }, + { + id, + name: id, + public: options.public, + file_size_limit: options.fileSizeLimit, + allowed_mime_types: options.allowedMimeTypes, + }, { headers: this.headers } ) return { data, error: null } @@ -111,10 +121,12 @@ export default class StorageBucketApi { * * @param id A unique identifier for the bucket you are updating. * @param options.public The visibility of the bucket. Public buckets don't require an authorization token to download objects, but still require a valid token for all other operations. + * @param options.fileSizeLimit specifies the file size limit that this bucket can accept during upload + * @param options.allowedMimeTypes specifies the allowed mime types that this bucket can accept during upload */ async updateBucket( id: string, - options: { public: boolean } + options: { public: boolean; fileSizeLimit?: number | string; allowedMimeTypes?: string[] } ): Promise< | { data: { message: string } @@ -129,7 +141,13 @@ export default class StorageBucketApi { const data = await put( this.fetch, `${this.url}/bucket/${id}`, - { id, name: id, public: options.public }, + { + id, + name: id, + public: options.public, + file_size_limit: options.fileSizeLimit, + allowed_mime_types: options.allowedMimeTypes, + }, { headers: this.headers } ) return { data, error: null } diff --git a/test/__snapshots__/storageApi.test.ts.snap b/test/__snapshots__/storageApi.test.ts.snap index cfe84b0..ed67a38 100644 --- a/test/__snapshots__/storageApi.test.ts.snap +++ b/test/__snapshots__/storageApi.test.ts.snap @@ -2,7 +2,9 @@ exports[`bucket api Get bucket by id 1`] = ` Object { + "allowed_mime_types": null, "created_at": "2021-02-17T04:43:32.770206+00:00", + "file_size_limit": 0, "id": "bucket2", "name": "bucket2", "owner": "4d56e902-f0a0-4662-8448-a4d9e643c142", @@ -25,6 +27,12 @@ Object { } `; +exports[`bucket api partially update bucket 1`] = ` +Object { + "message": "Successfully updated", +} +`; + exports[`bucket api update bucket 1`] = ` Object { "message": "Successfully updated", diff --git a/test/storageApi.test.ts b/test/storageApi.test.ts index e18c673..ec70a30 100644 --- a/test/storageApi.test.ts +++ b/test/storageApi.test.ts @@ -42,11 +42,35 @@ describe('bucket api', () => { }) test('update bucket', async () => { - const updateRes = await storage.updateBucket(newBucketName, { public: true }) + const newBucketName = `my-new-bucket-${Date.now()}` + await storage.createBucket(newBucketName) + const updateRes = await storage.updateBucket(newBucketName, { + public: true, + fileSizeLimit: '20mb', + allowedMimeTypes: ['image/jpeg'], + }) expect(updateRes.error).toBeNull() expect(updateRes.data).toMatchSnapshot() const getRes = await storage.getBucket(newBucketName) expect(getRes.data!.public).toBe(true) + expect(getRes.data!.file_size_limit).toBe(20000000) + expect(getRes.data!.allowed_mime_types).toEqual(['image/jpeg']) + }) + + test('partially update bucket', async () => { + const newBucketName = `my-new-bucket-${Date.now()}` + await storage.createBucket(newBucketName, { + public: true, + fileSizeLimit: '20mb', + allowedMimeTypes: ['image/jpeg'], + }) + const updateRes = await storage.updateBucket(newBucketName, { public: false }) + expect(updateRes.error).toBeNull() + expect(updateRes.data).toMatchSnapshot() + const getRes = await storage.getBucket(newBucketName) + expect(getRes.data!.public).toBe(false) + expect(getRes.data!.file_size_limit).toBe(20000000) + expect(getRes.data!.allowed_mime_types).toEqual(['image/jpeg']) }) test('empty bucket', async () => { diff --git a/test/storageFileApi.test.ts b/test/storageFileApi.test.ts index f5136cc..c678da1 100644 --- a/test/storageFileApi.test.ts +++ b/test/storageFileApi.test.ts @@ -159,6 +159,62 @@ describe('Object API', () => { expect(updateRes.error).toBeNull() expect(updateRes.data?.path).toEqual(uploadPath) }) + + test('can upload a file within the file size limit', async () => { + const bucketName = 'with-limit' + Date.now() + await storage.createBucket(bucketName, { + public: true, + fileSizeLimit: '1mb', + }) + + const res = await storage.from(bucketName).upload(uploadPath, file) + expect(res.error).toBeNull() + }) + + test('cannot upload a file that exceed the file size limit', async () => { + const bucketName = 'with-limit' + Date.now() + await storage.createBucket(bucketName, { + public: true, + fileSizeLimit: '1kb', + }) + + const res = await storage.from(bucketName).upload(uploadPath, file) + expect(res.error).toEqual({ + error: 'Payload too large', + message: 'The object exceeded the maximum allowed size', + statusCode: '413', + }) + }) + + test('can upload a file with a valid mime type', async () => { + const bucketName = 'with-limit' + Date.now() + await storage.createBucket(bucketName, { + public: true, + allowedMimeTypes: ['image/png'], + }) + + const res = await storage.from(bucketName).upload(uploadPath, file, { + contentType: 'image/png', + }) + expect(res.error).toBeNull() + }) + + test('cannot upload a file an invalid mime type', async () => { + const bucketName = 'with-limit' + Date.now() + await storage.createBucket(bucketName, { + public: true, + allowedMimeTypes: ['image/png'], + }) + + const res = await storage.from(bucketName).upload(uploadPath, file, { + contentType: 'image/jpeg', + }) + expect(res.error).toEqual({ + error: 'invalid_mime_type', + message: 'mime type not supported', + statusCode: '422', + }) + }) }) describe('File operations', () => {