fix(plugin-cloud-storage): actually deprecate adapters (#9640)

In v2, plugin-cloud-storage exported some adapters that were marked for
deprecation. These were replaced by standalone `@payloadcms/storage-*`
packages. More detail located in [this section of the migration
guide](https://github.com/payloadcms/payload/blob/main/docs/migration-guide/overview.mdx#payloadcmsplugin-cloud-storage).

Unfortunately, these exports were not removed prior to releasing 3.0.
This commit is contained in:
Elliot DeNolf
2024-12-31 09:14:56 -05:00
committed by GitHub
parent 07e86c0f20
commit c4b31c079f
33 changed files with 42 additions and 1044 deletions

View File

@@ -64,8 +64,8 @@
"dev:vercel-postgres": "cross-env PAYLOAD_DATABASE=vercel-postgres pnpm runts ./test/dev.ts", "dev:vercel-postgres": "cross-env PAYLOAD_DATABASE=vercel-postgres pnpm runts ./test/dev.ts",
"devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev", "devsafe": "node ./scripts/delete-recursively.js '**/.next' && pnpm dev",
"docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start", "docker:restart": "pnpm docker:stop --remove-orphans && pnpm docker:start",
"docker:start": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml up -d", "docker:start": "docker compose -f test/docker-compose.yml up -d",
"docker:stop": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml down", "docker:stop": "docker compose -f test/docker-compose.yml down",
"force:build": "pnpm run build:core:force", "force:build": "pnpm run build:core:force",
"lint": "turbo run lint --concurrency 1 --continue", "lint": "turbo run lint --concurrency 1 --continue",
"lint-staged": "lint-staged", "lint-staged": "lint-staged",

View File

@@ -33,26 +33,6 @@
"import": "./src/exports/utilities.ts", "import": "./src/exports/utilities.ts",
"types": "./src/exports/utilities.ts", "types": "./src/exports/utilities.ts",
"default": "./src/exports/utilities.ts" "default": "./src/exports/utilities.ts"
},
"./azure": {
"import": "./src/exports/azure.ts",
"types": "./src/exports/azure.ts",
"default": "./src/exports/azure.ts"
},
"./gcs": {
"import": "./src/exports/gcs.ts",
"types": "./src/exports/gcs.ts",
"default": "./src/exports/gcs.ts"
},
"./s3": {
"import": "./src/exports/s3.ts",
"types": "./src/exports/s3.ts",
"default": "./src/exports/s3.ts"
},
"./vercelBlob": {
"import": "./src/exports/vercelBlob.ts",
"types": "./src/exports/vercelBlob.ts",
"default": "./src/exports/vercelBlob.ts"
} }
}, },
"main": "./src/index.ts", "main": "./src/index.ts",
@@ -77,43 +57,12 @@
"range-parser": "^1.2.1" "range-parser": "^1.2.1"
}, },
"devDependencies": { "devDependencies": {
"@aws-sdk/client-s3": "^3.614.0",
"@aws-sdk/lib-storage": "^3.614.0",
"@azure/storage-blob": "^12.11.0",
"@google-cloud/storage": "^7.7.0",
"@types/find-node-modules": "^2.1.2", "@types/find-node-modules": "^2.1.2",
"@vercel/blob": "^0.22.3",
"payload": "workspace:*" "payload": "workspace:*"
}, },
"peerDependencies": { "peerDependencies": {
"@aws-sdk/client-s3": "^3.614.0",
"@aws-sdk/lib-storage": "^3.614.0",
"@azure/abort-controller": "^1.0.0",
"@azure/storage-blob": "^12.11.0",
"@google-cloud/storage": "^7.7.0",
"@vercel/blob": "^0.22.3",
"payload": "workspace:*" "payload": "workspace:*"
}, },
"peerDependenciesMeta": {
"@aws-sdk/client-s3": {
"optional": true
},
"@aws-sdk/lib-storage": {
"optional": true
},
"@azure/abort-controller": {
"optional": true
},
"@azure/storage-blob": {
"optional": true
},
"@google-cloud/storage": {
"optional": true
},
"@vercel/blob": {
"optional": true
}
},
"publishConfig": { "publishConfig": {
"exports": { "exports": {
".": { ".": {
@@ -130,26 +79,6 @@
"import": "./dist/exports/utilities.js", "import": "./dist/exports/utilities.js",
"types": "./dist/exports/utilities.d.ts", "types": "./dist/exports/utilities.d.ts",
"default": "./dist/exports/utilities.js" "default": "./dist/exports/utilities.js"
},
"./azure": {
"import": "./dist/exports/azure.js",
"types": "./dist/exports/azure.d.ts",
"default": "./dist/exports/azure.js"
},
"./gcs": {
"import": "./dist/exports/gcs.js",
"types": "./dist/exports/gcs.d.ts",
"default": "./dist/exports/gcs.js"
},
"./s3": {
"import": "./dist/exports/s3.js",
"types": "./dist/exports/s3.d.ts",
"default": "./dist/exports/s3.js"
},
"./vercelBlob": {
"import": "./dist/exports/vercelBlob.js",
"types": "./dist/exports/vercelBlob.d.ts",
"default": "./dist/exports/vercelBlob.js"
} }
}, },
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -1,16 +0,0 @@
version: '3'
services:
azure-storage:
image: mcr.microsoft.com/azure-storage/azurite:3.18.0
restart: always
command: 'azurite --loose --blobHost 0.0.0.0 --tableHost 0.0.0.0 --queueHost 0.0.0.0'
ports:
- '10000:10000'
- '10001:10001'
- '10002:10002'
volumes:
- ./azurestoragedata:/data"
volumes:
azurestoragedata:

View File

@@ -1,14 +0,0 @@
import path from 'path'
import type { GenerateURL } from '../../types.js'
interface Args {
baseURL: string
containerName: string
}
export const getGenerateURL =
({ baseURL, containerName }: Args): GenerateURL =>
({ filename, prefix = '' }) => {
return `${baseURL}/${containerName}/${path.posix.join(prefix, filename)}`
}

View File

@@ -1,18 +0,0 @@
import type { ContainerClient } from '@azure/storage-blob'
import type { CollectionConfig } from 'payload'
import path from 'path'
import type { HandleDelete } from '../../types.js'
interface Args {
collection: CollectionConfig
getStorageClient: () => ContainerClient
}
export const getHandleDelete = ({ getStorageClient }: Args): HandleDelete => {
return async ({ doc: { prefix = '' }, filename }) => {
const blockBlobClient = getStorageClient().getBlockBlobClient(path.posix.join(prefix, filename))
await blockBlobClient.deleteIfExists()
}
}

View File

@@ -1,43 +0,0 @@
import type { ContainerClient } from '@azure/storage-blob'
import type { CollectionConfig } from 'payload'
import { AbortController } from '@azure/abort-controller'
import fs from 'fs'
import path from 'path'
import { Readable } from 'stream'
import type { HandleUpload } from '../../types.js'
interface Args {
collection: CollectionConfig
getStorageClient: () => ContainerClient
prefix?: string
}
const multipartThreshold = 1024 * 1024 * 50 // 50MB
export const getHandleUpload = ({ getStorageClient, prefix = '' }: Args): HandleUpload => {
return async ({ data, file }) => {
const fileKey = path.posix.join(data.prefix || prefix, file.filename)
const blockBlobClient = getStorageClient().getBlockBlobClient(fileKey)
// when there are no temp files, or the upload is less than the threshold size, do not stream files
if (!file.tempFilePath && file.buffer.length > 0 && file.buffer.length < multipartThreshold) {
await blockBlobClient.upload(file.buffer, file.buffer.byteLength, {
blobHTTPHeaders: { blobContentType: file.mimeType },
})
return data
}
const fileBufferOrStream: Readable = file.tempFilePath
? fs.createReadStream(file.tempFilePath)
: Readable.from(file.buffer)
await blockBlobClient.uploadStream(fileBufferOrStream, 4 * 1024 * 1024, 4, {
abortSignal: AbortController.timeout(30 * 60 * 1000),
})
return data
}
}

View File

@@ -1,74 +0,0 @@
import type { ContainerClient } from '@azure/storage-blob'
import { BlobServiceClient } from '@azure/storage-blob'
import type { Adapter, GeneratedAdapter } from '../../types.js'
import { getGenerateURL } from './generateURL.js'
import { getHandleDelete } from './handleDelete.js'
import { getHandleUpload } from './handleUpload.js'
import { getHandler } from './staticHandler.js'
export interface Args {
allowContainerCreate: boolean
baseURL: string
connectionString: string
containerName: string
}
/**
* @deprecated Use [`@payloadcms/azure`](https://www.npmjs.com/package/@payloadcms/azure) instead.
*
* This adapter has been superceded by `@payloadcms/azure` and will be removed in Payload 3.0.
*/
export const azureBlobStorageAdapter = ({
allowContainerCreate,
baseURL,
connectionString,
containerName,
}: Args): Adapter => {
if (!BlobServiceClient) {
throw new Error(
'The package @azure/storage-blob is not installed, but is required for the plugin-cloud-storage Azure adapter. Please install it.',
)
}
let storageClient: ContainerClient | null = null
const getStorageClient = () => {
if (storageClient) {
return storageClient
}
let blobServiceClient = null
try {
blobServiceClient = BlobServiceClient.fromConnectionString(connectionString)
} catch (error) {
if (/is not a constructor$/.test(error.message)) {
throw new Error(
'The package @azure/storage-blob is not installed, but is required for the plugin-cloud-storage Azure adapter. Please install it.',
)
}
// Re-throw other unexpected errors.
throw error
}
return (storageClient = blobServiceClient.getContainerClient(containerName))
}
const createContainerIfNotExists = () => {
getStorageClient().createIfNotExists({ access: 'blob' })
}
return ({ collection, prefix }): GeneratedAdapter => {
return {
name: 'azure',
generateURL: getGenerateURL({ baseURL, containerName }),
handleDelete: getHandleDelete({ collection, getStorageClient }),
handleUpload: getHandleUpload({
collection,
getStorageClient,
prefix,
}),
staticHandler: getHandler({ collection, getStorageClient }),
...(allowContainerCreate && { onInit: createContainerIfNotExists }),
}
}
}

View File

@@ -1,55 +0,0 @@
import type { ContainerClient } from '@azure/storage-blob'
import type { CollectionConfig } from 'payload'
import path from 'path'
import type { StaticHandler } from '../../types.js'
import { getFilePrefix } from '../../utilities/getFilePrefix.js'
import getRangeFromHeader from '../../utilities/getRangeFromHeader.js'
interface Args {
collection: CollectionConfig
getStorageClient: () => ContainerClient
}
export const getHandler = ({ collection, getStorageClient }: Args): StaticHandler => {
return async (req, { params: { filename } }) => {
try {
const prefix = await getFilePrefix({ collection, filename, req })
const blockBlobClient = getStorageClient().getBlockBlobClient(
path.posix.join(prefix, filename),
)
const { end, start } = await getRangeFromHeader(blockBlobClient, req.headers.get('range'))
const blob = await blockBlobClient.download(start, end)
const response = blob._response
// Manually create a ReadableStream for the web from a Node.js stream.
const readableStream = new ReadableStream({
start(controller) {
const nodeStream = blob.readableStreamBody
nodeStream.on('data', (chunk) => {
controller.enqueue(new Uint8Array(chunk))
})
nodeStream.on('end', () => {
controller.close()
})
nodeStream.on('error', (err) => {
controller.error(err)
})
},
})
return new Response(readableStream, {
headers: response.headers.rawHeaders(),
status: response.status,
})
} catch (err: unknown) {
req.payload.logger.error(err)
return new Response('Internal Server Error', { status: 500 })
}
}
}

View File

@@ -1,26 +0,0 @@
version: '3'
services:
google-cloud-storage:
image: fsouza/fake-gcs-server
restart: always
command:
[
'-scheme',
'http',
'-port',
'4443',
'-public-host',
'http://localhost:4443',
'-external-url',
'http://localhost:4443',
'-backend',
'memory',
]
ports:
- '4443:4443'
volumes:
- ./google-cloud-storage/payload-bucket:/data/payload-bucket
volumes:
google-cloud-storage:

View File

@@ -1,18 +0,0 @@
import type { Storage } from '@google-cloud/storage'
import path from 'path'
import type { GenerateURL } from '../../types.js'
interface Args {
bucket: string
getStorageClient: () => Storage
}
export const getGenerateURL =
({ bucket, getStorageClient }: Args): GenerateURL =>
({ filename, prefix = '' }) => {
return decodeURIComponent(
getStorageClient().bucket(bucket).file(path.posix.join(prefix, filename)).publicUrl(),
)
}

View File

@@ -1,18 +0,0 @@
import type { Storage } from '@google-cloud/storage'
import path from 'path'
import type { HandleDelete } from '../../types.js'
interface Args {
bucket: string
getStorageClient: () => Storage
}
export const getHandleDelete = ({ bucket, getStorageClient }: Args): HandleDelete => {
return async ({ doc: { prefix = '' }, filename }) => {
await getStorageClient().bucket(bucket).file(path.posix.join(prefix, filename)).delete({
ignoreNotFound: true,
})
}
}

View File

@@ -1,38 +0,0 @@
import type { Storage } from '@google-cloud/storage'
import type { CollectionConfig } from 'payload'
import path from 'path'
import type { HandleUpload } from '../../types.js'
interface Args {
acl?: 'Private' | 'Public'
bucket: string
collection: CollectionConfig
getStorageClient: () => Storage
prefix?: string
}
export const getHandleUpload = ({
acl,
bucket,
getStorageClient,
prefix = '',
}: Args): HandleUpload => {
return async ({ data, file }) => {
const fileKey = path.posix.join(data.prefix || prefix, file.filename)
const gcsFile = getStorageClient().bucket(bucket).file(fileKey)
await gcsFile.save(file.buffer, {
metadata: {
contentType: file.mimeType,
},
})
if (acl) {
await gcsFile[`make${acl}`]()
}
return data
}
}

View File

@@ -1,65 +0,0 @@
import type { StorageOptions } from '@google-cloud/storage'
import { Storage } from '@google-cloud/storage'
import type { Adapter, GeneratedAdapter } from '../../types.js'
import { getGenerateURL } from './generateURL.js'
import { getHandleDelete } from './handleDelete.js'
import { getHandleUpload } from './handleUpload.js'
import { getHandler } from './staticHandler.js'
export interface Args {
acl?: 'Private' | 'Public'
bucket: string
options: StorageOptions
}
/**
* @deprecated Use [`@payloadcms/storage-gcs`](https://www.npmjs.com/package/@payloadcms/storage-gcs) instead.
*
* This adapter has been superceded by `@payloadcms/storage-gcs` and will be removed in Payload 3.0.
*/
export const gcsAdapter =
({ acl, bucket, options }: Args): Adapter =>
({ collection, prefix }): GeneratedAdapter => {
if (!Storage) {
throw new Error(
'The package @google-cloud/storage is not installed, but is required for the plugin-cloud-storage GCS adapter. Please install it.',
)
}
let storageClient: null | Storage = null
const getStorageClient = (): Storage => {
if (storageClient) {
return storageClient
}
try {
storageClient = new Storage(options)
} catch (error) {
if (/is not a constructor$/.test(error.message)) {
throw new Error(
'The package @google-cloud/storage is not installed, but is required for the plugin-cloud-storage GCS adapter. Please install it.',
)
}
// Re-throw other unexpected errors.
throw error
}
return storageClient
}
return {
name: 'gcs',
generateURL: getGenerateURL({ bucket, getStorageClient }),
handleDelete: getHandleDelete({ bucket, getStorageClient }),
handleUpload: getHandleUpload({
acl,
bucket,
collection,
getStorageClient,
prefix,
}),
staticHandler: getHandler({ bucket, collection, getStorageClient }),
}
}

View File

@@ -1,53 +0,0 @@
import type { Storage } from '@google-cloud/storage'
import type { CollectionConfig } from 'payload'
import path from 'path'
import type { StaticHandler } from '../../types.js'
import { getFilePrefix } from '../../utilities/getFilePrefix.js'
interface Args {
bucket: string
collection: CollectionConfig
getStorageClient: () => Storage
}
export const getHandler = ({ bucket, collection, getStorageClient }: Args): StaticHandler => {
return async (req, { params: { filename } }) => {
try {
const prefix = await getFilePrefix({ collection, filename, req })
const file = getStorageClient().bucket(bucket).file(path.posix.join(prefix, filename))
const [metadata] = await file.getMetadata()
// Manually create a ReadableStream for the web from a Node.js stream.
const readableStream = new ReadableStream({
start(controller) {
const nodeStream = file.createReadStream()
nodeStream.on('data', (chunk) => {
controller.enqueue(new Uint8Array(chunk))
})
nodeStream.on('end', () => {
controller.close()
})
nodeStream.on('error', (err) => {
controller.error(err)
})
},
})
return new Response(readableStream, {
headers: new Headers({
'Content-Length': String(metadata.size),
'Content-Type': metadata.contentType,
ETag: metadata.etag,
}),
status: 200,
})
} catch (err: unknown) {
req.payload.logger.error(err)
return new Response('Internal Server Error', { status: 500 })
}
}
}

View File

@@ -1,15 +0,0 @@
version: '3.2'
services:
localstack:
image: localstack/localstack:latest
container_name: localstack_demo
ports:
- '4563-4599:4563-4599'
- '8055:8080'
environment:
- SERVICES=s3
- DEBUG=1
- DATA_DIR=/tmp/localstack/data
volumes:
- './.localstack:/var/lib/localstack'
- '/var/run/docker.sock:/var/run/docker.sock'

View File

@@ -1,16 +0,0 @@
import type * as AWS from '@aws-sdk/client-s3'
import path from 'path'
import type { GenerateURL } from '../../types.js'
interface Args {
bucket: string
config: AWS.S3ClientConfig
}
export const getGenerateURL =
({ bucket, config: { endpoint } }: Args): GenerateURL =>
({ filename, prefix = '' }) => {
return `${endpoint}/${bucket}/${path.posix.join(prefix, filename)}`
}

View File

@@ -1,19 +0,0 @@
import type * as AWS from '@aws-sdk/client-s3'
import path from 'path'
import type { HandleDelete } from '../../types.js'
interface Args {
bucket: string
getStorageClient: () => AWS.S3
}
export const getHandleDelete = ({ bucket, getStorageClient }: Args): HandleDelete => {
return async ({ doc: { prefix = '' }, filename }) => {
await getStorageClient().deleteObject({
Bucket: bucket,
Key: path.posix.join(prefix, filename),
})
}
}

View File

@@ -1,63 +0,0 @@
import type * as AWS from '@aws-sdk/client-s3'
import type { CollectionConfig } from 'payload'
import type stream from 'stream'
import { Upload } from '@aws-sdk/lib-storage'
import fs from 'fs'
import path from 'path'
import type { HandleUpload } from '../../types.js'
interface Args {
acl?: 'private' | 'public-read'
bucket: string
collection: CollectionConfig
getStorageClient: () => AWS.S3
prefix?: string
}
const multipartThreshold = 1024 * 1024 * 50 // 50MB
export const getHandleUpload = ({
acl,
bucket,
getStorageClient,
prefix = '',
}: Args): HandleUpload => {
return async ({ data, file }) => {
const fileKey = path.posix.join(data.prefix || prefix, file.filename)
const fileBufferOrStream: Buffer | stream.Readable = file.tempFilePath
? fs.createReadStream(file.tempFilePath)
: file.buffer
if (file.buffer.length > 0 && file.buffer.length < multipartThreshold) {
await getStorageClient().putObject({
ACL: acl,
Body: fileBufferOrStream,
Bucket: bucket,
ContentType: file.mimeType,
Key: fileKey,
})
return data
}
const parallelUploadS3 = new Upload({
client: getStorageClient(),
params: {
ACL: acl,
Body: fileBufferOrStream,
Bucket: bucket,
ContentType: file.mimeType,
Key: fileKey,
},
partSize: multipartThreshold,
queueSize: 4,
})
await parallelUploadS3.done()
return data
}
}

View File

@@ -1,71 +0,0 @@
import * as AWS from '@aws-sdk/client-s3'
import type { Adapter, GeneratedAdapter } from '../../types.js'
import { getGenerateURL } from './generateURL.js'
import { getHandleDelete } from './handleDelete.js'
import { getHandleUpload } from './handleUpload.js'
import { getHandler } from './staticHandler.js'
export interface Args {
acl?: 'private' | 'public-read'
/**
* Bucket name to upload files to.
*
* Must follow [AWS S3 bucket naming conventions](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html).
*/
bucket: string
/**
* AWS S3 client configuration. Highly dependent on your AWS setup.
*
* [AWS.S3ClientConfig Docs](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/interfaces/s3clientconfig.html)
*/
config: AWS.S3ClientConfig
}
/**
* @deprecated Use [`@payloadcms/storage-s3`](https://www.npmjs.com/package/@payloadcms/storage-s3) instead.
*
* This adapter has been superceded by `@payloadcms/storage-s3` and will be removed in Payload 3.0.
*/
export const s3Adapter =
({ acl, bucket, config = {} }: Args): Adapter =>
({ collection, prefix }): GeneratedAdapter => {
if (!AWS) {
throw new Error(
'The packages @aws-sdk/client-s3, @aws-sdk/lib-storage and aws-crt are not installed, but are required for the plugin-cloud-storage S3 adapter. Please install them.',
)
}
let storageClient: AWS.S3 | null = null
const getStorageClient: () => AWS.S3 = () => {
if (storageClient) {
return storageClient
}
try {
storageClient = new AWS.S3(config)
} catch (error) {
if (/is not a constructor$/.test(error.message)) {
throw new Error(
'The packages @aws-sdk/client-s3, @aws-sdk/lib-storage and aws-crt are not installed, but are required for the plugin-cloud-storage S3 adapter. Please install them.',
)
}
// Re-throw other unexpected errors.
throw error
}
return storageClient
}
return {
name: 's3',
generateURL: getGenerateURL({ bucket, config }),
handleDelete: getHandleDelete({ bucket, getStorageClient }),
handleUpload: getHandleUpload({
acl,
bucket,
collection,
getStorageClient,
prefix,
}),
staticHandler: getHandler({ bucket, collection, getStorageClient }),
}
}

View File

@@ -1,55 +0,0 @@
import type * as AWS from '@aws-sdk/client-s3'
import type { CollectionConfig } from 'payload'
import path from 'path'
import type { StaticHandler } from '../../types.js'
import { getFilePrefix } from '../../utilities/getFilePrefix.js'
interface Args {
bucket: string
collection: CollectionConfig
getStorageClient: () => AWS.S3
}
// Convert a stream into a promise that resolves with a Buffer
const streamToBuffer = async (readableStream) => {
const chunks = []
for await (const chunk of readableStream) {
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
}
return Buffer.concat(chunks)
}
export const getHandler = ({ bucket, collection, getStorageClient }: Args): StaticHandler => {
return async (req, { params: { filename } }) => {
try {
const prefix = await getFilePrefix({ collection, filename, req })
const object = await getStorageClient().getObject({
Bucket: bucket,
Key: path.posix.join(prefix, filename),
})
if (!object.Body) {
return new Response(null, { status: 404, statusText: 'Not Found' })
}
const bodyBuffer = await streamToBuffer(object.Body)
return new Response(bodyBuffer, {
headers: new Headers({
'Accept-Ranges': object.AcceptRanges,
'Content-Length': String(object.ContentLength),
'Content-Type': object.ContentType,
ETag: object.ETag,
}),
status: 200,
})
} catch (err) {
req.payload.logger.error(err)
return new Response('Internal Server Error', { status: 500 })
}
}
}

View File

@@ -1,14 +0,0 @@
import path from 'path'
import type { GenerateURL } from '../../types.js'
type GenerateUrlArgs = {
baseUrl: string
prefix?: string
}
export const getGenerateUrl = ({ baseUrl }: GenerateUrlArgs): GenerateURL => {
return ({ filename, prefix = '' }) => {
return `${baseUrl}/${path.posix.join(prefix, filename)}`
}
}

View File

@@ -1,19 +0,0 @@
import { del } from '@vercel/blob'
import path from 'path'
import type { HandleDelete } from '../../types.js'
type HandleDeleteArgs = {
baseUrl: string
prefix?: string
token: string
}
export const getHandleDelete = ({ baseUrl, token }: HandleDeleteArgs): HandleDelete => {
return async ({ doc: { prefix = '' }, filename }) => {
const fileUrl = `${baseUrl}/${path.posix.join(prefix, filename)}`
const deletedBlob = await del(fileUrl, { token })
return deletedBlob
}
}

View File

@@ -1,39 +0,0 @@
import { put } from '@vercel/blob'
import path from 'path'
import type { HandleUpload } from '../../types.js'
import type { VercelBlobAdapterUploadOptions } from './index.js'
type HandleUploadArgs = {
baseUrl: string
prefix?: string
token: string
} & VercelBlobAdapterUploadOptions
export const getHandleUpload = ({
access = 'public',
addRandomSuffix,
baseUrl,
cacheControlMaxAge,
prefix = '',
token,
}: HandleUploadArgs): HandleUpload => {
return async ({ data, file: { buffer, filename, mimeType } }) => {
const fileKey = path.posix.join(data.prefix || prefix, filename)
const result = await put(fileKey, buffer, {
access,
addRandomSuffix,
cacheControlMaxAge,
contentType: mimeType,
token,
})
// Get filename with suffix from returned url
if (addRandomSuffix) {
data.filename = result.url.replace(`${baseUrl}/`, '')
}
return data
}
}

View File

@@ -1,88 +0,0 @@
import type { Adapter, GeneratedAdapter } from '../../types.js'
import { getGenerateUrl } from './generateURL.js'
import { getHandleDelete } from './handleDelete.js'
import { getHandleUpload } from './handleUpload.js'
import { getStaticHandler } from './staticHandler.js'
export interface VercelBlobAdapterArgs {
options?: VercelBlobAdapterUploadOptions
/**
* Vercel Blob storage read/write token
*
* Usually process.env.BLOB_READ_WRITE_TOKEN set by Vercel
*/
token: string
}
export interface VercelBlobAdapterUploadOptions {
/**
* Access control level
*
* @default 'public'
*/
access?: 'public'
/**
* Add a random suffix to the uploaded file name
*
* @default false
*/
addRandomSuffix?: boolean
/**
* Cache-Control max-age in seconds
*
* @default 31536000 (1 year)
*/
cacheControlMaxAge?: number
}
const defaultUploadOptions: VercelBlobAdapterUploadOptions = {
access: 'public',
addRandomSuffix: false,
cacheControlMaxAge: 60 * 60 * 24 * 365, // 1 year
}
/**
* @deprecated Use [`@payloadcms/storage-vercel-blob`](https://www.npmjs.com/package/@payloadcms/storage-vercel-blob) instead.
*
* This adapter has been superceded by `@payloadcms/storage-vercel-blob` and will be removed in Payload 3.0.
*/
export const vercelBlobAdapter =
({ options = {}, token }: VercelBlobAdapterArgs): Adapter =>
({ collection, prefix }): GeneratedAdapter => {
if (!token) {
throw new Error('The token argument is required for the Vercel Blob adapter.')
}
// Parse storeId from token
const storeId = token.match(/^vercel_blob_rw_([a-z\d]+)_[a-z\d]+$/i)?.[1].toLowerCase()
if (!storeId) {
throw new Error(
'Invalid token format for Vercel Blob adapter. Should be vercel_blob_rw_<store_id>_<random_string>.',
)
}
const { access, addRandomSuffix, cacheControlMaxAge } = {
...defaultUploadOptions,
...options,
}
const baseUrl = `https://${storeId}.${access}.blob.vercel-storage.com`
return {
name: 'vercel-blob',
generateURL: getGenerateUrl({ baseUrl, prefix }),
handleDelete: getHandleDelete({ baseUrl, prefix, token }),
handleUpload: getHandleUpload({
access,
addRandomSuffix,
baseUrl,
cacheControlMaxAge,
prefix,
token,
}),
staticHandler: getStaticHandler({ baseUrl, token }, collection),
}
}

View File

@@ -1,53 +0,0 @@
import type { CollectionConfig } from 'payload'
import { head } from '@vercel/blob'
import path from 'path'
import type { StaticHandler } from '../../types.js'
import { getFilePrefix } from '../../utilities/getFilePrefix.js'
type StaticHandlerArgs = {
baseUrl: string
token: string
}
export const getStaticHandler = (
{ baseUrl, token }: StaticHandlerArgs,
collection: CollectionConfig,
): StaticHandler => {
return async (req, { params: { filename } }) => {
try {
const prefix = await getFilePrefix({ collection, filename, req })
const fileUrl = `${baseUrl}/${path.posix.join(prefix, filename)}`
const blobMetadata = await head(fileUrl, { token })
if (!blobMetadata) {
return new Response(null, { status: 404, statusText: 'Not Found' })
}
const { contentDisposition, contentType, size } = blobMetadata
const response = await fetch(fileUrl)
const blob = await response.blob()
if (!blob) {
return new Response(null, { status: 204, statusText: 'No Content' })
}
const bodyBuffer = await blob.arrayBuffer()
return new Response(bodyBuffer, {
headers: new Headers({
'Content-Disposition': contentDisposition,
'Content-Length': String(size),
'Content-Type': contentType,
}),
status: 200,
})
} catch (err: unknown) {
req.payload.logger.error({ err, msg: 'Unexpected error in staticHandler' })
return new Response('Internal Server Error', { status: 500 })
}
}
}

View File

@@ -1 +0,0 @@
export { azureBlobStorageAdapter } from '../adapters/azure/index.js'

View File

@@ -1 +0,0 @@
export { gcsAdapter } from '../adapters/gcs/index.js'

View File

@@ -1 +0,0 @@
export { s3Adapter } from '../adapters/s3/index.js'

View File

@@ -1 +0,0 @@
export { vercelBlobAdapter } from '../adapters/vercelBlob/index.js'

View File

@@ -1,28 +0,0 @@
import type { BlockBlobClient } from '@azure/storage-blob'
import parseRange from 'range-parser'
const getRangeFromHeader = async (
blockBlobClient: BlockBlobClient,
rangeHeader?: string,
): Promise<{ end: number | undefined; start: number }> => {
const fullRange = { end: undefined, start: 0 }
if (!rangeHeader) {
return fullRange
}
const size = await blockBlobClient.getProperties().then((props) => props.contentLength)
if (size === undefined) {
return fullRange
}
const range = parseRange(size, rangeHeader)
if (range === -1 || range === -2 || range.type !== 'bytes' || range.length !== 1) {
return fullRange
}
return range[0]
}
export default getRangeFromHeader

18
pnpm-lock.yaml generated
View File

@@ -968,9 +968,6 @@ importers:
packages/plugin-cloud-storage: packages/plugin-cloud-storage:
dependencies: dependencies:
'@azure/abort-controller':
specifier: ^1.0.0
version: 1.1.0
find-node-modules: find-node-modules:
specifier: ^2.1.3 specifier: ^2.1.3
version: 2.1.3 version: 2.1.3
@@ -978,24 +975,9 @@ importers:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
devDependencies: devDependencies:
'@aws-sdk/client-s3':
specifier: ^3.614.0
version: 3.687.0
'@aws-sdk/lib-storage':
specifier: ^3.614.0
version: 3.687.0(@aws-sdk/client-s3@3.687.0)
'@azure/storage-blob':
specifier: ^12.11.0
version: 12.25.0
'@google-cloud/storage':
specifier: ^7.7.0
version: 7.14.0
'@types/find-node-modules': '@types/find-node-modules':
specifier: ^2.1.2 specifier: ^2.1.2
version: 2.1.2 version: 2.1.2
'@vercel/blob':
specifier: ^0.22.3
version: 0.22.3
payload: payload:
specifier: workspace:* specifier: workspace:*
version: link:../payload version: link:../payload

View File

@@ -1,9 +1,8 @@
import type { Adapter } from '@payloadcms/plugin-cloud-storage/types' import type { Plugin } from 'payload'
import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage' import { azureStorage } from '@payloadcms/storage-azure'
import { azureBlobStorageAdapter } from '@payloadcms/plugin-cloud-storage/azure' import { gcsStorage } from '@payloadcms/storage-gcs'
import { gcsAdapter } from '@payloadcms/plugin-cloud-storage/gcs' import { s3Storage } from '@payloadcms/storage-s3'
import { s3Adapter } from '@payloadcms/plugin-cloud-storage/s3'
import dotenv from 'dotenv' import dotenv from 'dotenv'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import path from 'path' import path from 'path'
@@ -15,10 +14,11 @@ import { MediaWithPrefix } from './collections/MediaWithPrefix.js'
import { Users } from './collections/Users.js' import { Users } from './collections/Users.js'
import { mediaSlug, mediaWithPrefixSlug, prefix } from './shared.js' import { mediaSlug, mediaWithPrefixSlug, prefix } from './shared.js'
import { createTestBucket } from './utils.js' import { createTestBucket } from './utils.js'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
let adapter: Adapter let storagePlugin: Plugin
let uploadOptions let uploadOptions
// Load config to work with emulated services // Load config to work with emulated services
@@ -27,19 +27,28 @@ dotenv.config({
}) })
if (process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER === 'azure') { if (process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER === 'azure') {
adapter = azureBlobStorageAdapter({ storagePlugin = azureStorage({
collections: {
[mediaSlug]: true,
[mediaWithPrefixSlug]: {
prefix,
},
},
allowContainerCreate: process.env.AZURE_STORAGE_ALLOW_CONTAINER_CREATE === 'true', allowContainerCreate: process.env.AZURE_STORAGE_ALLOW_CONTAINER_CREATE === 'true',
baseURL: process.env.AZURE_STORAGE_ACCOUNT_BASEURL, baseURL: process.env.AZURE_STORAGE_ACCOUNT_BASEURL,
connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING, connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
containerName: process.env.AZURE_STORAGE_CONTAINER_NAME, containerName: process.env.AZURE_STORAGE_CONTAINER_NAME,
}) })
// uploadOptions = {
// useTempFiles: true,
// }
} }
if (process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER === 'gcs') { if (process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER === 'gcs') {
adapter = gcsAdapter({ storagePlugin = gcsStorage({
collections: {
[mediaSlug]: true,
[mediaWithPrefixSlug]: {
prefix,
},
},
bucket: process.env.GCS_BUCKET, bucket: process.env.GCS_BUCKET,
options: { options: {
apiEndpoint: process.env.GCS_ENDPOINT, apiEndpoint: process.env.GCS_ENDPOINT,
@@ -57,7 +66,13 @@ if (
useTempFiles: true, useTempFiles: true,
} }
adapter = s3Adapter({ storagePlugin = s3Storage({
collections: {
[mediaSlug]: true,
[mediaWithPrefixSlug]: {
prefix,
},
},
bucket: process.env.S3_BUCKET, bucket: process.env.S3_BUCKET,
config: { config: {
credentials: { credentials: {
@@ -72,16 +87,22 @@ if (
} }
if (process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER === 'r2') { if (process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER === 'r2') {
adapter = s3Adapter({ s3Storage({
collections: {
[mediaSlug]: true,
[mediaWithPrefixSlug]: {
prefix,
},
},
bucket: process.env.R2_BUCKET, bucket: process.env.R2_BUCKET,
config: { config: {
credentials: { credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID, accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
}, },
endpoint: process.env.R2_ENDPOINT, endpoint: process.env.S3_ENDPOINT,
forcePathStyle: process.env.R2_FORCE_PATH_STYLE === 'true', forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
region: process.env.R2_REGION, region: process.env.S3_REGION,
}, },
}) })
} }
@@ -126,19 +147,7 @@ export default buildConfigWithDefaults({
`Using plugin-cloud-storage adapter: ${process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER}`, `Using plugin-cloud-storage adapter: ${process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER}`,
) )
}, },
plugins: [ plugins: [storagePlugin],
cloudStoragePlugin({
collections: {
[mediaSlug]: {
adapter,
},
[mediaWithPrefixSlug]: {
adapter,
prefix,
},
},
}),
],
upload: uploadOptions, upload: uploadOptions,
typescript: { typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'), outputFile: path.resolve(dirname, 'payload-types.ts'),