Skip to content
This repository was archived by the owner on Jan 16, 2024. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ This plugin supports the following adapters:
- [Azure Blob Storage](#azure-blob-storage-adapter)
- [AWS S3-style Storage](#s3-adapter)
- [Google Cloud Storage](#gcs-adapter)
- [Supabase Storage Adapter](#supabase-adapter)

However, you can create your own adapter for any third-party service you would like to use.

Expand Down Expand Up @@ -175,6 +176,26 @@ const adapter = gcsAdapter({
// Now you can pass this adapter to the plugin
```

### Supabase Adapter

To use the Supabase adapter, some peer dependencies need to be installed:

`yarn add @supabase/storage-js fast-blob-stream`.

From there, create the adapter, passing in all of its required properties:

```js
import { supabaseAdapter } from '@payloadcms/plugin-cloud-storage/supabase';

const adapter = supabaseAdapter({
apiKey: process.env.SUPABASE_SECRET_KEY,// this env variable will have the service_role key of your supabase project
bucket: process.env.SUPABASE_BUCKET_NAME,// this env variable will have the bucket name
url: process.env.SUPABASE_ENDPOINT,// this env variable will have the endpoint of your supabase project
})

// Now you can pass this adapter to the plugin
```

### Payload Access Control

Payload ships with access control that runs *even on statically served files*. The same `read` access control property on your `upload`-enabled collections is used, and it allows you to restrict who can request your uploaded files.
Expand Down
4 changes: 4 additions & 0 deletions dev/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ GCS_ENDPOINT=http://localhost:4443
GCS_PROJECT_ID=test
GCS_BUCKET=payload-bucket

SUPABASE_SECRET_KEY=supabase-secret-key
SUPABASE_BUCKET_NAME=payload-bucket
SUPABASE_ENDPOINT=https://localhost:10000/your-endpoint

PAYLOAD_DROP_DATABASE=true
3 changes: 3 additions & 0 deletions dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"dev:azure": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER=azure nodemon",
"dev:s3": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER=s3 nodemon",
"dev:gcs": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER=gcs nodemon",
"dev:supabase": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER=supabase nodemon",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"build:server": "tsc",
"build": "yarn build:payload && yarn build:server",
Expand All @@ -18,8 +19,10 @@
"@aws-sdk/client-s3": "^3.142.0",
"@azure/storage-blob": "^12.11.0",
"@google-cloud/storage": "^6.4.2",
"@supabase/storage-js": "^2.3.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"fast-blob-stream": "^1.1.1",
"image-size": "^1.0.2",
"payload": "^1.7.2",
"probe-image-size": "^7.2.3"
Expand Down
21 changes: 17 additions & 4 deletions dev/src/payload.config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { buildConfig } from 'payload/config'
import path from 'path'
import Users from './collections/Users'
import { buildConfig } from 'payload/config'
import { cloudStorage } from '../../src'
import { s3Adapter } from '../../src/adapters/s3'
import { gcsAdapter } from '../../src/adapters/gcs'
import { azureBlobStorageAdapter } from '../../src/adapters/azure'
import { gcsAdapter } from '../../src/adapters/gcs'
import { s3Adapter } from '../../src/adapters/s3'
import { supabaseAdapter } from '../../src/adapters/supabase'
import type { Adapter } from '../../src/types'
import { Media } from './collections/Media'
import Users from './collections/Users'

let adapter: Adapter
let uploadOptions

if (process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER === 'supabase') {
adapter = supabaseAdapter({
apiKey: process.env.SUPABASE_SECRET_KEY,
bucket: process.env.SUPABASE_BUCKET_NAME,
url: process.env.SUPABASE_ENDPOINT,
})
}

if (process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER === 'azure') {
adapter = azureBlobStorageAdapter({
connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
Expand Down Expand Up @@ -85,6 +94,10 @@ export default buildConfig({
__dirname,
'../../src/adapters/azure/mock.js',
),
[path.resolve(__dirname, '../../src/adapters/supabase/index')]: path.resolve(
__dirname,
'../../src/adapters/supabase/mock.js',
)
},
},
}
Expand Down
10 changes: 10 additions & 0 deletions docs/local-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,13 @@ The default `./dev/.env.example` file comes pre-loaded with correct `env` variab
Otherwise, if you are not using the emulator, make sure your environment variables within `./dev/.env` are configured for your Google connection.

Finally, to start the Payload dev server with the GCS adapter, run `yarn dev:gcs` and then open `http://localhost:3000/admin` in your browser.

### Supabase Adapter Development

By now, this repository does not come with a Docker emulator for Supabase Storage.

If you would like to test locally this plugin, use the following steps:

1. Sign up for a free plan in Supabase platform.
1. Make sure your environment variables within `./dev/.env` are configured for your Supabase connection.
1. Finally, run `yarn dev:supabase` and then open `http://localhost:3000/admin` in your browser.
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"@aws-sdk/lib-storage": "^3.267.0",
"@azure/storage-blob": "^12.11.0",
"@google-cloud/storage": "^6.4.1",
"@supabase/storage-js": "^2.3.0",
"fast-blob-stream": "^1.1.1",
"payload": "^1.7.2"
},
"peerDependenciesMeta": {
Expand All @@ -31,6 +33,12 @@
},
"@google-cloud/storage": {
"optional": true
},
"@supabase/storage-js": {
"optional": true
},
"fast-blob-stream": {
"optional": true
}
},
"files": [
Expand All @@ -44,6 +52,7 @@
"@aws-sdk/lib-storage": "^3.267.0",
"@azure/storage-blob": "^12.11.0",
"@google-cloud/storage": "^6.4.1",
"@supabase/storage-js": "^2.3.0",
"@types/express": "^4.17.9",
"@typescript-eslint/eslint-plugin": "5.12.1",
"@typescript-eslint/parser": "5.12.1",
Expand All @@ -54,14 +63,19 @@
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-prettier": "^4.0.0",
"fast-blob-stream": "^1.1.1",
"nodemon": "^2.0.6",
"payload": "^1.7.2",
"prettier": "^2.7.1",
"rimraf": "^4.1.2",
"ts-node": "^9.1.1",
"typescript": "^4.1.3"
},
<<<<<<< HEAD
"dependencies": {}
=======
"dependencies": {
"range-parser": "^1.2.1"
}
>>>>>>> master
}
1 change: 1 addition & 0 deletions src/adapters/supabase/fileStub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'file-stub'
13 changes: 13 additions & 0 deletions src/adapters/supabase/generateURL.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import path from 'path'
import type { GenerateURL } from '../../types'

interface Args {
endpoint: string
bucket: string
}

export const getGenerateURL =
({ endpoint, bucket }: Args): GenerateURL =>
({ filename, prefix = '' }) => {
return `${endpoint}/object/public/${bucket}/${path.posix.join(prefix, filename)}`
}
15 changes: 15 additions & 0 deletions src/adapters/supabase/handleDelete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { StorageClient } from '@supabase/storage-js'
import path from 'path'
import type { HandleDelete } from '../../types'

interface Args {
getStorageClient: () => StorageClient
bucket: string
}

export const getHandleDelete = ({ getStorageClient, bucket }: Args): HandleDelete => {
return async ({ filename, doc: { prefix = '' } }) => {
const fileKey: string = path.posix.join(prefix, filename)
await getStorageClient().from(bucket).remove([fileKey])
}
}
27 changes: 27 additions & 0 deletions src/adapters/supabase/handleUpload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { StorageClient } from '@supabase/storage-js'
import fs from 'fs'
import path from 'path'
import type stream from 'stream'
import type { HandleUpload } from '../../types'

interface Args {
getStorageClient: () => StorageClient
bucket: string
prefix?: string
}

export const getHandleUpload = ({ getStorageClient, bucket, prefix = '' }: Args): HandleUpload => {
return async ({ data, file }) => {
const fileKey = path.posix.join(prefix, file.filename)

const fileBufferOrStream: Buffer | stream.Readable = file.tempFilePath
? fs.createReadStream(file.tempFilePath)
: file.buffer

await getStorageClient().from(bucket).upload(fileKey, fileBufferOrStream, {
contentType: file.mimeType,
})

return data
}
}
35 changes: 35 additions & 0 deletions src/adapters/supabase/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { StorageClient } from '@supabase/storage-js'
import type { Adapter, GeneratedAdapter } from '../../types'
import { getGenerateURL } from './generateURL'
import { getHandleDelete } from './handleDelete'
import { getHandleUpload } from './handleUpload'
import { getHandler } from './staticHandler'
import { extendWebpackConfig } from './webpack'

export interface Args {
url: string
apiKey: string
bucket: string
}

export const supabaseAdapter =
({ url, apiKey, bucket }: Args): Adapter =>
({ collection, prefix }): GeneratedAdapter => {
let storageClient: StorageClient | null = null
const getStorageClient: () => StorageClient = () => {
if (storageClient) return storageClient
storageClient = new StorageClient(url, {
apikey: apiKey,
Authorization: `Bearer ${apiKey}`,
})
return storageClient
}

return {
handleUpload: getHandleUpload({ getStorageClient, bucket, prefix }),
handleDelete: getHandleDelete({ getStorageClient, bucket }),
generateURL: getGenerateURL({ bucket, endpoint: url }),
staticHandler: getHandler({ getStorageClient, bucket, collection }),
webpack: extendWebpackConfig,
}
}
4 changes: 4 additions & 0 deletions src/adapters/supabase/mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
exports.StorageClient = () => null
exports.Upload = () => null
exports.getBucket = () => null
exports.BlobReadStream = () => null
46 changes: 46 additions & 0 deletions src/adapters/supabase/staticHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { StorageClient } from '@supabase/storage-js'
import { BlobReadStream } from 'fast-blob-stream'
import path from 'path'
import type { CollectionConfig } from 'payload/types'
import type { StaticHandler } from '../../types'
import { getFilePrefix } from '../../utilities/getFilePrefix'

interface Args {
getStorageClient: () => StorageClient
bucket: string
collection: CollectionConfig
}

export const getHandler = ({ getStorageClient, bucket, collection }: Args): StaticHandler => {
return async (req, res, next) => {
try {
const prefix = await getFilePrefix({ req, collection })
const key: string = path.posix.join(prefix, req.params.filename)

const { data } = await getStorageClient().from(bucket).list('', {
limit: 1,
offset: 0,
search: key,
})
const file = data![0]
const fileDownloaded = await getStorageClient().from(bucket).download(key)
const blobFile = fileDownloaded.data

res.set({
'Content-Length': file.metadata.contentLength,
'Content-Type': file.metadata.mimetype,
ETag: file.metadata.eTag,
})

if (blobFile) {
const readStream = new BlobReadStream(blobFile)
return readStream.pipe(res)
}

return next()
} catch (err: unknown) {
req.payload.logger.error(err)
return next()
}
}
}
19 changes: 19 additions & 0 deletions src/adapters/supabase/webpack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import path from 'path'
import type { Configuration as WebpackConfig } from 'webpack'

export const extendWebpackConfig = (existingWebpackConfig: WebpackConfig): WebpackConfig => {
const newConfig: WebpackConfig = {
...existingWebpackConfig,
resolve: {
...(existingWebpackConfig.resolve || {}),
alias: {
...(existingWebpackConfig.resolve?.alias ? existingWebpackConfig.resolve.alias : {}),
'@supabase/storage-js': path.resolve(__dirname, './mock.js'),
'fast-blob-stream': path.resolve(__dirname, './mock.js'),
fs: path.resolve(__dirname, './fileStub.js'),
},
},
}

return newConfig
}
Loading