feat(plugin-cloud-storage): implement storage packages (#5928)
This commit is contained in:
@@ -15,7 +15,7 @@ services:
|
|||||||
- '/var/run/docker.sock:/var/run/docker.sock'
|
- '/var/run/docker.sock:/var/run/docker.sock'
|
||||||
|
|
||||||
azure-storage:
|
azure-storage:
|
||||||
image: mcr.microsoft.com/azure-storage/azurite:3.18.0
|
image: mcr.microsoft.com/azure-storage/azurite:latest
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
restart: always
|
restart: always
|
||||||
command: 'azurite --loose --blobHost 0.0.0.0 --tableHost 0.0.0.0 --queueHost 0.0.0.0'
|
command: 'azurite --loose --blobHost 0.0.0.0 --tableHost 0.0.0.0 --queueHost 0.0.0.0'
|
||||||
|
|||||||
10
packages/storage-azure/.eslintignore
Normal file
10
packages/storage-azure/.eslintignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
7
packages/storage-azure/.eslintrc.cjs
Normal file
7
packages/storage-azure/.eslintrc.cjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
10
packages/storage-azure/.prettierignore
Normal file
10
packages/storage-azure/.prettierignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
15
packages/storage-azure/.swcrc
Normal file
15
packages/storage-azure/.swcrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/swcrc",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"jsc": {
|
||||||
|
"target": "esnext",
|
||||||
|
"parser": {
|
||||||
|
"syntax": "typescript",
|
||||||
|
"tsx": true,
|
||||||
|
"dts": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"type": "es6"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/storage-azure/LICENSE.md
Normal file
22
packages/storage-azure/LICENSE.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018-2022 Payload CMS, LLC <info@payloadcms.com>
|
||||||
|
Portions Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
1
packages/storage-azure/README.md
Normal file
1
packages/storage-azure/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Azure Storage
|
||||||
61
packages/storage-azure/package.json
Normal file
61
packages/storage-azure/package.json
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"name": "@payloadcms/storage-azure",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Payload storage adapter for Azure Blob Storage",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/payloadcms/payload.git",
|
||||||
|
"directory": "packages/storage-azure"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://payloadcms.com",
|
||||||
|
"author": "Payload CMS, Inc.",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm build:swc && pnpm build:types",
|
||||||
|
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||||
|
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||||
|
"build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build",
|
||||||
|
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||||
|
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./src/index.ts",
|
||||||
|
"require": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@azure/abort-controller": "^1.1.0",
|
||||||
|
"@azure/storage-blob": "^12.11.0",
|
||||||
|
"@payloadcms/plugin-cloud-storage": "workspace:*",
|
||||||
|
"range-parser": "^1.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/range-parser": "^1.2.7",
|
||||||
|
"payload": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"payload": "workspace:*"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.20.2"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
14
packages/storage-azure/src/generateURL.ts
Normal file
14
packages/storage-azure/src/generateURL.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { GenerateURL } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
interface Args {
|
||||||
|
baseURL: string
|
||||||
|
containerName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getGenerateURL =
|
||||||
|
({ baseURL, containerName }: Args): GenerateURL =>
|
||||||
|
({ filename, prefix = '' }) => {
|
||||||
|
return `${baseURL}/${containerName}/${path.posix.join(prefix, filename)}`
|
||||||
|
}
|
||||||
17
packages/storage-azure/src/handleDelete.ts
Normal file
17
packages/storage-azure/src/handleDelete.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { ContainerClient } from '@azure/storage-blob'
|
||||||
|
import type { HandleDelete } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
42
packages/storage-azure/src/handleUpload.ts
Normal file
42
packages/storage-azure/src/handleUpload.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { ContainerClient } from '@azure/storage-blob'
|
||||||
|
import type { HandleUpload } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import { AbortController } from '@azure/abort-controller'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { Readable } from 'stream'
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
106
packages/storage-azure/src/index.ts
Normal file
106
packages/storage-azure/src/index.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import type { ContainerClient } from '@azure/storage-blob'
|
||||||
|
import type {
|
||||||
|
Adapter,
|
||||||
|
PluginOptions as CloudStoragePluginOptions,
|
||||||
|
CollectionOptions,
|
||||||
|
GeneratedAdapter,
|
||||||
|
} from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { Config, Plugin } from 'payload/config'
|
||||||
|
|
||||||
|
import { BlobServiceClient } from '@azure/storage-blob'
|
||||||
|
import { cloudStorage } from '@payloadcms/plugin-cloud-storage'
|
||||||
|
|
||||||
|
import { getGenerateURL } from './generateURL.js'
|
||||||
|
import { getHandleDelete } from './handleDelete.js'
|
||||||
|
import { getHandleUpload } from './handleUpload.js'
|
||||||
|
import { getHandler } from './staticHandler.js'
|
||||||
|
|
||||||
|
export type AzureStorageOptions = {
|
||||||
|
allowContainerCreate: boolean
|
||||||
|
baseURL: string
|
||||||
|
/**
|
||||||
|
* Collection options to apply the Azure Blob adapter to.
|
||||||
|
*/
|
||||||
|
collections: Record<string, Omit<CollectionOptions, 'adapter'> | true>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Azure Blob storage connection string
|
||||||
|
*/
|
||||||
|
connectionString: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Azure Blob storage container name
|
||||||
|
*/
|
||||||
|
containerName: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to enable the plugin
|
||||||
|
*
|
||||||
|
* Default: true
|
||||||
|
*/
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type AzureStoragePlugin = (azureStorageArgs: AzureStorageOptions) => Plugin
|
||||||
|
|
||||||
|
export const azureStorage: AzureStoragePlugin =
|
||||||
|
(azureStorageOptions: AzureStorageOptions) =>
|
||||||
|
(incomingConfig: Config): Config => {
|
||||||
|
if (azureStorageOptions.enabled === false) {
|
||||||
|
return incomingConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapter = azureStorageInternal(azureStorageOptions)
|
||||||
|
|
||||||
|
// Add adapter to each collection option object
|
||||||
|
const collectionsWithAdapter: CloudStoragePluginOptions['collections'] = Object.entries(
|
||||||
|
azureStorageOptions.collections,
|
||||||
|
).reduce(
|
||||||
|
(acc, [slug, collOptions]) => ({
|
||||||
|
...acc,
|
||||||
|
[slug]: {
|
||||||
|
...(collOptions === true ? {} : collOptions),
|
||||||
|
adapter,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{} as Record<string, CollectionOptions>,
|
||||||
|
)
|
||||||
|
|
||||||
|
return cloudStorage({
|
||||||
|
collections: collectionsWithAdapter,
|
||||||
|
})(incomingConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
function azureStorageInternal({
|
||||||
|
allowContainerCreate,
|
||||||
|
baseURL,
|
||||||
|
connectionString,
|
||||||
|
containerName,
|
||||||
|
}: AzureStorageOptions): Adapter {
|
||||||
|
let storageClient: ContainerClient | null = null
|
||||||
|
const getStorageClient = () => {
|
||||||
|
if (storageClient) return storageClient
|
||||||
|
|
||||||
|
const blobServiceClient = BlobServiceClient.fromConnectionString(connectionString)
|
||||||
|
storageClient = blobServiceClient.getContainerClient(containerName)
|
||||||
|
return storageClient
|
||||||
|
}
|
||||||
|
|
||||||
|
const createContainerIfNotExists = () => {
|
||||||
|
void getStorageClient().createIfNotExists({ access: 'blob' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({ collection, prefix }): GeneratedAdapter => {
|
||||||
|
return {
|
||||||
|
generateURL: getGenerateURL({ baseURL, containerName }),
|
||||||
|
handleDelete: getHandleDelete({ collection, getStorageClient }),
|
||||||
|
handleUpload: getHandleUpload({
|
||||||
|
collection,
|
||||||
|
getStorageClient,
|
||||||
|
prefix,
|
||||||
|
}),
|
||||||
|
staticHandler: getHandler({ collection, getStorageClient }),
|
||||||
|
...(allowContainerCreate && { onInit: createContainerIfNotExists }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
packages/storage-azure/src/staticHandler.ts
Normal file
61
packages/storage-azure/src/staticHandler.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import type { ContainerClient } from '@azure/storage-blob'
|
||||||
|
import type { StaticHandler } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import { getFilePrefix } from '@payloadcms/plugin-cloud-storage/utilities'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import { getRangeFromHeader } from './utils/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,
|
||||||
|
String(req.headers.get('range')),
|
||||||
|
)
|
||||||
|
|
||||||
|
const blob = await blockBlobClient.download(start, end)
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
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
|
||||||
|
if (!nodeStream) {
|
||||||
|
throw new Error('No readable stream body')
|
||||||
|
}
|
||||||
|
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/storage-azure/src/utils/getRangeFromHeader.ts
Normal file
26
packages/storage-azure/src/utils/getRangeFromHeader.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { BlockBlobClient } from '@azure/storage-blob'
|
||||||
|
|
||||||
|
import parseRange from 'range-parser'
|
||||||
|
|
||||||
|
export 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]
|
||||||
|
}
|
||||||
20
packages/storage-azure/tsconfig.json
Normal file
20
packages/storage-azure/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true, // Make sure typescript knows that this module depends on their references
|
||||||
|
"noEmit": false /* Do not emit outputs. */,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||||
|
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||||
|
"strict": true,
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
],
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../payload" },
|
||||||
|
{ "path": "../plugin-cloud-storage" },
|
||||||
|
]
|
||||||
|
}
|
||||||
10
packages/storage-gcs/.eslintignore
Normal file
10
packages/storage-gcs/.eslintignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
7
packages/storage-gcs/.eslintrc.cjs
Normal file
7
packages/storage-gcs/.eslintrc.cjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
10
packages/storage-gcs/.prettierignore
Normal file
10
packages/storage-gcs/.prettierignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
15
packages/storage-gcs/.swcrc
Normal file
15
packages/storage-gcs/.swcrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/swcrc",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"jsc": {
|
||||||
|
"target": "esnext",
|
||||||
|
"parser": {
|
||||||
|
"syntax": "typescript",
|
||||||
|
"tsx": true,
|
||||||
|
"dts": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"type": "es6"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/storage-gcs/LICENSE.md
Normal file
22
packages/storage-gcs/LICENSE.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018-2022 Payload CMS, LLC <info@payloadcms.com>
|
||||||
|
Portions Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
1
packages/storage-gcs/README.md
Normal file
1
packages/storage-gcs/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Nodemailer Email Adapter
|
||||||
58
packages/storage-gcs/package.json
Normal file
58
packages/storage-gcs/package.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "@payloadcms/storage-gcs",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Payload storage adapter for Google Cloud Storage",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/payloadcms/payload.git",
|
||||||
|
"directory": "packages/storage-gcs"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://payloadcms.com",
|
||||||
|
"author": "Payload CMS, Inc.",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm build:swc && pnpm build:types",
|
||||||
|
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||||
|
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||||
|
"build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build",
|
||||||
|
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||||
|
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./src/index.ts",
|
||||||
|
"require": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@payloadcms/plugin-cloud-storage": "workspace:*",
|
||||||
|
"@google-cloud/storage": "^7.7.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"payload": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"payload": "workspace:*"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.20.2"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
17
packages/storage-gcs/src/generateURL.ts
Normal file
17
packages/storage-gcs/src/generateURL.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Storage } from '@google-cloud/storage'
|
||||||
|
import type { GenerateURL } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
17
packages/storage-gcs/src/handleDelete.ts
Normal file
17
packages/storage-gcs/src/handleDelete.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Storage } from '@google-cloud/storage'
|
||||||
|
import type { HandleDelete } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
37
packages/storage-gcs/src/handleUpload.ts
Normal file
37
packages/storage-gcs/src/handleUpload.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Storage } from '@google-cloud/storage'
|
||||||
|
import type { HandleUpload } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
97
packages/storage-gcs/src/index.ts
Normal file
97
packages/storage-gcs/src/index.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import type { StorageOptions } from '@google-cloud/storage'
|
||||||
|
import type {
|
||||||
|
Adapter,
|
||||||
|
PluginOptions as CloudStoragePluginOptions,
|
||||||
|
CollectionOptions,
|
||||||
|
GeneratedAdapter,
|
||||||
|
} from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { Config, Plugin } from 'payload/config'
|
||||||
|
|
||||||
|
import { Storage } from '@google-cloud/storage'
|
||||||
|
import { cloudStorage } from '@payloadcms/plugin-cloud-storage'
|
||||||
|
|
||||||
|
import { getGenerateURL } from './generateURL.js'
|
||||||
|
import { getHandleDelete } from './handleDelete.js'
|
||||||
|
import { getHandleUpload } from './handleUpload.js'
|
||||||
|
import { getHandler } from './staticHandler.js'
|
||||||
|
|
||||||
|
export interface GcsStorageOptions {
|
||||||
|
acl?: 'Private' | 'Public'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the bucket to use.
|
||||||
|
*/
|
||||||
|
bucket: string
|
||||||
|
/**
|
||||||
|
* Collection options to apply the S3 adapter to.
|
||||||
|
*/
|
||||||
|
collections: Record<string, Omit<CollectionOptions, 'adapter'> | true>
|
||||||
|
/**
|
||||||
|
* Whether or not to enable the plugin
|
||||||
|
*
|
||||||
|
* Default: true
|
||||||
|
*/
|
||||||
|
enabled?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Google Cloud Storage client configuration.
|
||||||
|
*
|
||||||
|
* @see https://github.com/googleapis/nodejs-storage
|
||||||
|
*/
|
||||||
|
options: StorageOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type GcsStoragePlugin = (gcsStorageArgs: GcsStorageOptions) => Plugin
|
||||||
|
|
||||||
|
export const gcsStorage: GcsStoragePlugin =
|
||||||
|
(gcsStorageOptions: GcsStorageOptions) =>
|
||||||
|
(incomingConfig: Config): Config => {
|
||||||
|
if (gcsStorageOptions.enabled === false) {
|
||||||
|
return incomingConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapter = gcsStorageInternal(gcsStorageOptions)
|
||||||
|
|
||||||
|
// Add adapter to each collection option object
|
||||||
|
const collectionsWithAdapter: CloudStoragePluginOptions['collections'] = Object.entries(
|
||||||
|
gcsStorageOptions.collections,
|
||||||
|
).reduce(
|
||||||
|
(acc, [slug, collOptions]) => ({
|
||||||
|
...acc,
|
||||||
|
[slug]: {
|
||||||
|
...(collOptions === true ? {} : collOptions),
|
||||||
|
adapter,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{} as Record<string, CollectionOptions>,
|
||||||
|
)
|
||||||
|
|
||||||
|
return cloudStorage({
|
||||||
|
collections: collectionsWithAdapter,
|
||||||
|
})(incomingConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
function gcsStorageInternal({ acl, bucket, options }: GcsStorageOptions): Adapter {
|
||||||
|
return ({ collection, prefix }): GeneratedAdapter => {
|
||||||
|
let storageClient: Storage | null = null
|
||||||
|
|
||||||
|
const getStorageClient = (): Storage => {
|
||||||
|
if (storageClient) return storageClient
|
||||||
|
storageClient = new Storage(options)
|
||||||
|
return storageClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
generateURL: getGenerateURL({ bucket, getStorageClient }),
|
||||||
|
handleDelete: getHandleDelete({ bucket, getStorageClient }),
|
||||||
|
handleUpload: getHandleUpload({
|
||||||
|
acl,
|
||||||
|
bucket,
|
||||||
|
collection,
|
||||||
|
getStorageClient,
|
||||||
|
prefix,
|
||||||
|
}),
|
||||||
|
staticHandler: getHandler({ bucket, collection, getStorageClient }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
packages/storage-gcs/src/staticHandler.ts
Normal file
51
packages/storage-gcs/src/staticHandler.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { Storage } from '@google-cloud/storage'
|
||||||
|
import type { StaticHandler } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import { getFilePrefix } from '@payloadcms/plugin-cloud-storage/utilities'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
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': String(metadata.contentType),
|
||||||
|
ETag: String(metadata.etag),
|
||||||
|
}),
|
||||||
|
status: 200,
|
||||||
|
})
|
||||||
|
} catch (err: unknown) {
|
||||||
|
req.payload.logger.error(err)
|
||||||
|
return new Response('Internal Server Error', { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
packages/storage-gcs/tsconfig.json
Normal file
20
packages/storage-gcs/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true, // Make sure typescript knows that this module depends on their references
|
||||||
|
"noEmit": false /* Do not emit outputs. */,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||||
|
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||||
|
"strict": true,
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
],
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../payload" },
|
||||||
|
{ "path": "../plugin-cloud-storage" },
|
||||||
|
]
|
||||||
|
}
|
||||||
10
packages/storage-s3/.eslintignore
Normal file
10
packages/storage-s3/.eslintignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
7
packages/storage-s3/.eslintrc.cjs
Normal file
7
packages/storage-s3/.eslintrc.cjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
10
packages/storage-s3/.prettierignore
Normal file
10
packages/storage-s3/.prettierignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
15
packages/storage-s3/.swcrc
Normal file
15
packages/storage-s3/.swcrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/swcrc",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"jsc": {
|
||||||
|
"target": "esnext",
|
||||||
|
"parser": {
|
||||||
|
"syntax": "typescript",
|
||||||
|
"tsx": true,
|
||||||
|
"dts": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"type": "es6"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/storage-s3/LICENSE.md
Normal file
22
packages/storage-s3/LICENSE.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018-2022 Payload CMS, LLC <info@payloadcms.com>
|
||||||
|
Portions Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
1
packages/storage-s3/README.md
Normal file
1
packages/storage-s3/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Nodemailer Email Adapter
|
||||||
15
packages/storage-s3/docker-compose.yml
Normal file
15
packages/storage-s3/docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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'
|
||||||
59
packages/storage-s3/package.json
Normal file
59
packages/storage-s3/package.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "@payloadcms/storage-s3",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Payload storage adapter for Amazon S3",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/payloadcms/payload.git",
|
||||||
|
"directory": "packages/storage-s3"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://payloadcms.com",
|
||||||
|
"author": "Payload CMS, Inc.",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm build:swc && pnpm build:types",
|
||||||
|
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||||
|
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||||
|
"build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build",
|
||||||
|
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||||
|
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./src/index.ts",
|
||||||
|
"require": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@payloadcms/plugin-cloud-storage": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.525.0",
|
||||||
|
"@aws-sdk/lib-storage": "^3.525.0",
|
||||||
|
"payload": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"payload": "workspace:*"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.20.2"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
16
packages/storage-s3/src/generateURL.ts
Normal file
16
packages/storage-s3/src/generateURL.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type * as AWS from '@aws-sdk/client-s3'
|
||||||
|
import type { GenerateURL } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
interface Args {
|
||||||
|
bucket: string
|
||||||
|
config: AWS.S3ClientConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getGenerateURL =
|
||||||
|
({ bucket, config: { endpoint } }: Args): GenerateURL =>
|
||||||
|
({ filename, prefix = '' }) => {
|
||||||
|
const stringifiedEndpoint = typeof endpoint === 'string' ? endpoint : endpoint?.toString()
|
||||||
|
return `${stringifiedEndpoint}/${bucket}/${path.posix.join(prefix, filename)}`
|
||||||
|
}
|
||||||
18
packages/storage-s3/src/handleDelete.ts
Normal file
18
packages/storage-s3/src/handleDelete.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type * as AWS from '@aws-sdk/client-s3'
|
||||||
|
import type { HandleDelete } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
61
packages/storage-s3/src/handleUpload.ts
Normal file
61
packages/storage-s3/src/handleUpload.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import type * as AWS from '@aws-sdk/client-s3'
|
||||||
|
import type { HandleUpload } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import { Upload } from '@aws-sdk/lib-storage'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
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 = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
109
packages/storage-s3/src/index.ts
Normal file
109
packages/storage-s3/src/index.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import type {
|
||||||
|
Adapter,
|
||||||
|
PluginOptions as CloudStoragePluginOptions,
|
||||||
|
CollectionOptions,
|
||||||
|
GeneratedAdapter,
|
||||||
|
} from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { Config, Plugin } from 'payload/config'
|
||||||
|
|
||||||
|
import * as AWS from '@aws-sdk/client-s3'
|
||||||
|
import { cloudStorage } from '@payloadcms/plugin-cloud-storage'
|
||||||
|
|
||||||
|
import { getGenerateURL } from './generateURL.js'
|
||||||
|
import { getHandleDelete } from './handleDelete.js'
|
||||||
|
import { getHandleUpload } from './handleUpload.js'
|
||||||
|
import { getHandler } from './staticHandler.js'
|
||||||
|
|
||||||
|
export type S3StorageOptions = {
|
||||||
|
/**
|
||||||
|
* Access control list for uploaded files.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection options to apply the S3 adapter to.
|
||||||
|
*/
|
||||||
|
collections: Record<string, Omit<CollectionOptions, 'adapter'> | true>
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to disable local storage
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
disableLocalStorage?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to enable the plugin
|
||||||
|
*
|
||||||
|
* Default: true
|
||||||
|
*/
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type S3StoragePlugin = (storageS3Args: S3StorageOptions) => Plugin
|
||||||
|
|
||||||
|
export const s3Storage: S3StoragePlugin =
|
||||||
|
(s3StorageOptions: S3StorageOptions) =>
|
||||||
|
(incomingConfig: Config): Config => {
|
||||||
|
if (s3StorageOptions.enabled === false) {
|
||||||
|
return incomingConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapter = s3StorageInternal(s3StorageOptions)
|
||||||
|
|
||||||
|
// Add adapter to each collection option object
|
||||||
|
const collectionsWithAdapter: CloudStoragePluginOptions['collections'] = Object.entries(
|
||||||
|
s3StorageOptions.collections,
|
||||||
|
).reduce(
|
||||||
|
(acc, [slug, collOptions]) => ({
|
||||||
|
...acc,
|
||||||
|
[slug]: {
|
||||||
|
...(collOptions === true ? {} : collOptions),
|
||||||
|
adapter,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{} as Record<string, CollectionOptions>,
|
||||||
|
)
|
||||||
|
|
||||||
|
return cloudStorage({
|
||||||
|
collections: collectionsWithAdapter,
|
||||||
|
})(incomingConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
function s3StorageInternal({ acl, bucket, config = {} }: S3StorageOptions): Adapter {
|
||||||
|
return ({ collection, prefix }): GeneratedAdapter => {
|
||||||
|
let storageClient: AWS.S3 | null = null
|
||||||
|
const getStorageClient: () => AWS.S3 = () => {
|
||||||
|
if (storageClient) return storageClient
|
||||||
|
storageClient = new AWS.S3(config)
|
||||||
|
return storageClient
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
generateURL: getGenerateURL({ bucket, config }),
|
||||||
|
handleDelete: getHandleDelete({ bucket, getStorageClient }),
|
||||||
|
handleUpload: getHandleUpload({
|
||||||
|
acl,
|
||||||
|
bucket,
|
||||||
|
collection,
|
||||||
|
getStorageClient,
|
||||||
|
prefix,
|
||||||
|
}),
|
||||||
|
staticHandler: getHandler({ bucket, collection, getStorageClient }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
packages/storage-s3/src/staticHandler.ts
Normal file
54
packages/storage-s3/src/staticHandler.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type * as AWS from '@aws-sdk/client-s3'
|
||||||
|
import type { StaticHandler } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import { getFilePrefix } from '@payloadcms/plugin-cloud-storage/utilities'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
interface Args {
|
||||||
|
bucket: string
|
||||||
|
collection: CollectionConfig
|
||||||
|
getStorageClient: () => AWS.S3
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a stream into a promise that resolves with a Buffer
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const streamToBuffer = async (readableStream: any) => {
|
||||||
|
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': String(object.AcceptRanges),
|
||||||
|
'Content-Length': String(object.ContentLength),
|
||||||
|
'Content-Type': String(object.ContentType),
|
||||||
|
ETag: String(object.ETag),
|
||||||
|
}),
|
||||||
|
status: 200,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
req.payload.logger.error(err)
|
||||||
|
return new Response('Internal Server Error', { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
packages/storage-s3/tsconfig.json
Normal file
20
packages/storage-s3/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true, // Make sure typescript knows that this module depends on their references
|
||||||
|
"noEmit": false /* Do not emit outputs. */,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||||
|
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||||
|
"strict": true,
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
],
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../payload" },
|
||||||
|
{ "path": "../plugin-cloud-storage" },
|
||||||
|
]
|
||||||
|
}
|
||||||
10
packages/storage-vercel-blob/.eslintignore
Normal file
10
packages/storage-vercel-blob/.eslintignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
7
packages/storage-vercel-blob/.eslintrc.cjs
Normal file
7
packages/storage-vercel-blob/.eslintrc.cjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
10
packages/storage-vercel-blob/.prettierignore
Normal file
10
packages/storage-vercel-blob/.prettierignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
15
packages/storage-vercel-blob/.swcrc
Normal file
15
packages/storage-vercel-blob/.swcrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/swcrc",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"jsc": {
|
||||||
|
"target": "esnext",
|
||||||
|
"parser": {
|
||||||
|
"syntax": "typescript",
|
||||||
|
"tsx": true,
|
||||||
|
"dts": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"type": "es6"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/storage-vercel-blob/LICENSE.md
Normal file
22
packages/storage-vercel-blob/LICENSE.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018-2022 Payload CMS, LLC <info@payloadcms.com>
|
||||||
|
Portions Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
1
packages/storage-vercel-blob/README.md
Normal file
1
packages/storage-vercel-blob/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Vercel Blob Storage
|
||||||
58
packages/storage-vercel-blob/package.json
Normal file
58
packages/storage-vercel-blob/package.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "@payloadcms/storage-vercel-blob",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Payload storage adapter for Vercel Blob Storage",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/payloadcms/payload.git",
|
||||||
|
"directory": "packages/storage-vercel-blob"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://payloadcms.com",
|
||||||
|
"author": "Payload CMS, Inc.",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm build:swc && pnpm build:types",
|
||||||
|
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||||
|
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||||
|
"build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build",
|
||||||
|
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||||
|
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./src/index.ts",
|
||||||
|
"require": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@payloadcms/plugin-cloud-storage": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vercel/blob": "^0.22.3",
|
||||||
|
"payload": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"payload": "workspace:*"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.20.2"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
14
packages/storage-vercel-blob/src/generateURL.ts
Normal file
14
packages/storage-vercel-blob/src/generateURL.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { GenerateURL } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
type GenerateUrlArgs = {
|
||||||
|
baseUrl: string
|
||||||
|
prefix?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getGenerateUrl = ({ baseUrl }: GenerateUrlArgs): GenerateURL => {
|
||||||
|
return ({ filename, prefix = '' }) => {
|
||||||
|
return `${baseUrl}/${path.posix.join(prefix, filename)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
19
packages/storage-vercel-blob/src/handleDelete.ts
Normal file
19
packages/storage-vercel-blob/src/handleDelete.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { HandleDelete } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
|
||||||
|
import { del } from '@vercel/blob'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
39
packages/storage-vercel-blob/src/handleUpload.ts
Normal file
39
packages/storage-vercel-blob/src/handleUpload.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { HandleUpload } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
|
||||||
|
import { put } from '@vercel/blob'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import type { VercelBlobStorageOptions } from './index.js'
|
||||||
|
|
||||||
|
type HandleUploadArgs = Omit<VercelBlobStorageOptions, 'collections'> & {
|
||||||
|
baseUrl: string
|
||||||
|
prefix?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
133
packages/storage-vercel-blob/src/index.ts
Normal file
133
packages/storage-vercel-blob/src/index.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import type {
|
||||||
|
PluginOptions as CloudStoragePluginOptions,
|
||||||
|
CollectionOptions,
|
||||||
|
} from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { Adapter, GeneratedAdapter } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { Config, Plugin } from 'payload/config'
|
||||||
|
|
||||||
|
import { cloudStorage } from '@payloadcms/plugin-cloud-storage'
|
||||||
|
|
||||||
|
import { getGenerateUrl } from './generateURL.js'
|
||||||
|
import { getHandleDelete } from './handleDelete.js'
|
||||||
|
import { getHandleUpload } from './handleUpload.js'
|
||||||
|
import { getStaticHandler } from './staticHandler.js'
|
||||||
|
|
||||||
|
export type VercelBlobStorageOptions = {
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collections to apply the Vercel Blob adapter to
|
||||||
|
*/
|
||||||
|
collections: Record<string, Omit<CollectionOptions, 'adapter'> | true>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to enable the plugin
|
||||||
|
*
|
||||||
|
* Default: true
|
||||||
|
*/
|
||||||
|
enabled?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vercel Blob storage read/write token
|
||||||
|
*
|
||||||
|
* Usually process.env.BLOB_READ_WRITE_TOKEN set by Vercel
|
||||||
|
*/
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultUploadOptions: Partial<VercelBlobStorageOptions> = {
|
||||||
|
access: 'public',
|
||||||
|
addRandomSuffix: false,
|
||||||
|
cacheControlMaxAge: 60 * 60 * 24 * 365, // 1 year
|
||||||
|
enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
type VercelBlobStoragePlugin = (vercelBlobStorageOpts: VercelBlobStorageOptions) => Plugin
|
||||||
|
|
||||||
|
export const vercelBlobStorage: VercelBlobStoragePlugin =
|
||||||
|
(options: VercelBlobStorageOptions) =>
|
||||||
|
(incomingConfig: Config): Config => {
|
||||||
|
if (options.enabled === false) {
|
||||||
|
return incomingConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.token) {
|
||||||
|
throw new Error('The token argument is required for the Vercel Blob adapter.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse storeId from token
|
||||||
|
const storeId = options.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 optionsWithDefaults = {
|
||||||
|
...defaultUploadOptions,
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = `https://${storeId}.${optionsWithDefaults.access}.blob.vercel-storage.com`
|
||||||
|
|
||||||
|
const adapter = vercelBlobStorageInternal({ ...optionsWithDefaults, baseUrl })
|
||||||
|
|
||||||
|
// Add adapter to each collection option object
|
||||||
|
const collectionsWithAdapter: CloudStoragePluginOptions['collections'] = Object.entries(
|
||||||
|
options.collections,
|
||||||
|
).reduce(
|
||||||
|
(acc, [slug, collOptions]) => ({
|
||||||
|
...acc,
|
||||||
|
[slug]: {
|
||||||
|
...(collOptions === true ? {} : collOptions),
|
||||||
|
adapter,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{} as Record<string, CollectionOptions>,
|
||||||
|
)
|
||||||
|
|
||||||
|
return cloudStorage({
|
||||||
|
collections: collectionsWithAdapter,
|
||||||
|
})(incomingConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
function vercelBlobStorageInternal(
|
||||||
|
options: VercelBlobStorageOptions & { baseUrl: string },
|
||||||
|
): Adapter {
|
||||||
|
return ({ collection, prefix }): GeneratedAdapter => {
|
||||||
|
const { access, addRandomSuffix, baseUrl, cacheControlMaxAge, token } = options
|
||||||
|
return {
|
||||||
|
generateURL: getGenerateUrl({ baseUrl, prefix }),
|
||||||
|
handleDelete: getHandleDelete({ baseUrl, prefix, token: options.token }),
|
||||||
|
handleUpload: getHandleUpload({
|
||||||
|
access,
|
||||||
|
addRandomSuffix,
|
||||||
|
baseUrl,
|
||||||
|
cacheControlMaxAge,
|
||||||
|
prefix,
|
||||||
|
token,
|
||||||
|
}),
|
||||||
|
staticHandler: getStaticHandler({ baseUrl, token }, collection),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
packages/storage-vercel-blob/src/staticHandler.ts
Normal file
51
packages/storage-vercel-blob/src/staticHandler.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { StaticHandler } from '@payloadcms/plugin-cloud-storage/types'
|
||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
import { getFilePrefix } from '@payloadcms/plugin-cloud-storage/utilities'
|
||||||
|
import { head } from '@vercel/blob'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
packages/storage-vercel-blob/tsconfig.json
Normal file
20
packages/storage-vercel-blob/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true, // Make sure typescript knows that this module depends on their references
|
||||||
|
"noEmit": false /* Do not emit outputs. */,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||||
|
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||||
|
"strict": true,
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
],
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../payload" },
|
||||||
|
{ "path": "../plugin-cloud-storage" },
|
||||||
|
]
|
||||||
|
}
|
||||||
140
pnpm-lock.yaml
generated
140
pnpm-lock.yaml
generated
@@ -1381,6 +1381,70 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../payload
|
version: link:../payload
|
||||||
|
|
||||||
|
packages/storage-azure:
|
||||||
|
dependencies:
|
||||||
|
'@azure/abort-controller':
|
||||||
|
specifier: ^1.1.0
|
||||||
|
version: 1.1.0
|
||||||
|
'@azure/storage-blob':
|
||||||
|
specifier: ^12.11.0
|
||||||
|
version: 12.17.0
|
||||||
|
'@payloadcms/plugin-cloud-storage':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../plugin-cloud-storage
|
||||||
|
range-parser:
|
||||||
|
specifier: ^1.2.1
|
||||||
|
version: 1.2.1
|
||||||
|
devDependencies:
|
||||||
|
'@types/range-parser':
|
||||||
|
specifier: ^1.2.7
|
||||||
|
version: 1.2.7
|
||||||
|
payload:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../payload
|
||||||
|
|
||||||
|
packages/storage-gcs:
|
||||||
|
dependencies:
|
||||||
|
'@google-cloud/storage':
|
||||||
|
specifier: ^7.7.0
|
||||||
|
version: 7.9.0
|
||||||
|
'@payloadcms/plugin-cloud-storage':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../plugin-cloud-storage
|
||||||
|
devDependencies:
|
||||||
|
payload:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../payload
|
||||||
|
|
||||||
|
packages/storage-s3:
|
||||||
|
dependencies:
|
||||||
|
'@payloadcms/plugin-cloud-storage':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../plugin-cloud-storage
|
||||||
|
devDependencies:
|
||||||
|
'@aws-sdk/client-s3':
|
||||||
|
specifier: ^3.525.0
|
||||||
|
version: 3.550.0
|
||||||
|
'@aws-sdk/lib-storage':
|
||||||
|
specifier: ^3.525.0
|
||||||
|
version: 3.550.0(@aws-sdk/client-s3@3.550.0)
|
||||||
|
payload:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../payload
|
||||||
|
|
||||||
|
packages/storage-vercel-blob:
|
||||||
|
dependencies:
|
||||||
|
'@payloadcms/plugin-cloud-storage':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../plugin-cloud-storage
|
||||||
|
devDependencies:
|
||||||
|
'@vercel/blob':
|
||||||
|
specifier: ^0.22.3
|
||||||
|
version: 0.22.3
|
||||||
|
payload:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../payload
|
||||||
|
|
||||||
packages/translations:
|
packages/translations:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@payloadcms/eslint-config':
|
'@payloadcms/eslint-config':
|
||||||
@@ -1591,6 +1655,18 @@ importers:
|
|||||||
'@payloadcms/richtext-slate':
|
'@payloadcms/richtext-slate':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/richtext-slate
|
version: link:../packages/richtext-slate
|
||||||
|
'@payloadcms/storage-azure':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/storage-azure
|
||||||
|
'@payloadcms/storage-gcs':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/storage-gcs
|
||||||
|
'@payloadcms/storage-s3':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/storage-s3
|
||||||
|
'@payloadcms/storage-vercel-blob':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/storage-vercel-blob
|
||||||
'@payloadcms/translations':
|
'@payloadcms/translations':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/translations
|
version: link:../packages/translations
|
||||||
@@ -2352,21 +2428,19 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
|
|
||||||
/@azure/abort-controller@2.1.1:
|
/@azure/abort-controller@2.1.2:
|
||||||
resolution: {integrity: sha512-NhzeNm5zu2fPlwGXPUjzsRCRuPx5demaZyNcyNYJDqpa/Sbxzvo/RYt9IwUaAOnDW5+r7J9UOE6f22TQnb9nhQ==}
|
resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@azure/core-auth@1.7.1:
|
/@azure/core-auth@1.7.1:
|
||||||
resolution: {integrity: sha512-dyeQwvgthqs/SlPVQbZQetpslXceHd4i5a7M/7z/lGEAVwnSluabnQOjF2/dk/hhWgMISusv1Ytp4mQ8JNy62A==}
|
resolution: {integrity: sha512-dyeQwvgthqs/SlPVQbZQetpslXceHd4i5a7M/7z/lGEAVwnSluabnQOjF2/dk/hhWgMISusv1Ytp4mQ8JNy62A==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@azure/abort-controller': 2.1.1
|
'@azure/abort-controller': 2.1.2
|
||||||
'@azure/core-util': 1.8.1
|
'@azure/core-util': 1.8.1
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@azure/core-http@3.0.4:
|
/@azure/core-http@3.0.4:
|
||||||
resolution: {integrity: sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ==}
|
resolution: {integrity: sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ==}
|
||||||
@@ -2388,24 +2462,21 @@ packages:
|
|||||||
xml2js: 0.5.0
|
xml2js: 0.5.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@azure/core-lro@2.7.1:
|
/@azure/core-lro@2.7.1:
|
||||||
resolution: {integrity: sha512-kXSlrNHOCTVZMxpXNRqzgh9/j4cnNXU5Hf2YjMyjddRhCXFiFRzmNaqwN+XO9rGTsCOIaaG7M67zZdyliXZG9g==}
|
resolution: {integrity: sha512-kXSlrNHOCTVZMxpXNRqzgh9/j4cnNXU5Hf2YjMyjddRhCXFiFRzmNaqwN+XO9rGTsCOIaaG7M67zZdyliXZG9g==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@azure/abort-controller': 2.1.1
|
'@azure/abort-controller': 2.1.2
|
||||||
'@azure/core-util': 1.8.1
|
'@azure/core-util': 1.8.1
|
||||||
'@azure/logger': 1.1.1
|
'@azure/logger': 1.1.1
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@azure/core-paging@1.6.1:
|
/@azure/core-paging@1.6.1:
|
||||||
resolution: {integrity: sha512-3tKIQXSU3mlN+ITz0m2pXLnKK3oQ6/EVcW8ud011Iq+M0rx6Wnm7NUEpoMeOAEedeKlPtemrQzO6YWoDR71O5w==}
|
resolution: {integrity: sha512-3tKIQXSU3mlN+ITz0m2pXLnKK3oQ6/EVcW8ud011Iq+M0rx6Wnm7NUEpoMeOAEedeKlPtemrQzO6YWoDR71O5w==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@azure/core-tracing@1.0.0-preview.13:
|
/@azure/core-tracing@1.0.0-preview.13:
|
||||||
resolution: {integrity: sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==}
|
resolution: {integrity: sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==}
|
||||||
@@ -2413,22 +2484,19 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.8.0
|
'@opentelemetry/api': 1.8.0
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@azure/core-util@1.8.1:
|
/@azure/core-util@1.8.1:
|
||||||
resolution: {integrity: sha512-L3voj0StUdJ+YKomvwnTv7gHzguJO+a6h30pmmZdRprJCM+RJlGMPxzuh4R7lhQu1jNmEtaHX5wvTgWLDAmbGQ==}
|
resolution: {integrity: sha512-L3voj0StUdJ+YKomvwnTv7gHzguJO+a6h30pmmZdRprJCM+RJlGMPxzuh4R7lhQu1jNmEtaHX5wvTgWLDAmbGQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@azure/abort-controller': 2.1.1
|
'@azure/abort-controller': 2.1.2
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@azure/logger@1.1.1:
|
/@azure/logger@1.1.1:
|
||||||
resolution: {integrity: sha512-/+4TtokaGgC+MnThdf6HyIH9Wrjp+CnCn3Nx3ggevN7FFjjNyjqg0yLlc2i9S+Z2uAzI8GYOo35Nzb1MhQ89MA==}
|
resolution: {integrity: sha512-/+4TtokaGgC+MnThdf6HyIH9Wrjp+CnCn3Nx3ggevN7FFjjNyjqg0yLlc2i9S+Z2uAzI8GYOo35Nzb1MhQ89MA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@azure/storage-blob@12.17.0:
|
/@azure/storage-blob@12.17.0:
|
||||||
resolution: {integrity: sha512-sM4vpsCpcCApagRW5UIjQNlNylo02my2opgp0Emi8x888hZUvJ3dN69Oq20cEGXkMUWnoCrBaB0zyS3yeB87sQ==}
|
resolution: {integrity: sha512-sM4vpsCpcCApagRW5UIjQNlNylo02my2opgp0Emi8x888hZUvJ3dN69Oq20cEGXkMUWnoCrBaB0zyS3yeB87sQ==}
|
||||||
@@ -2444,7 +2512,6 @@ packages:
|
|||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@babel/code-frame@7.24.2:
|
/@babel/code-frame@7.24.2:
|
||||||
resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==}
|
resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==}
|
||||||
@@ -3845,17 +3912,14 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
arrify: 2.0.1
|
arrify: 2.0.1
|
||||||
extend: 3.0.2
|
extend: 3.0.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@google-cloud/projectify@4.0.0:
|
/@google-cloud/projectify@4.0.0:
|
||||||
resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==}
|
resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@google-cloud/promisify@4.0.0:
|
/@google-cloud/promisify@4.0.0:
|
||||||
resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==}
|
resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@google-cloud/storage@7.9.0:
|
/@google-cloud/storage@7.9.0:
|
||||||
resolution: {integrity: sha512-PlFl7g3r91NmXtZHXsSEfTZES5ysD3SSBWmX4iBdQ2TFH7tN/Vn/IhnVELCHtgh1vc+uYPZ7XvRYaqtDCdghIA==}
|
resolution: {integrity: sha512-PlFl7g3r91NmXtZHXsSEfTZES5ysD3SSBWmX4iBdQ2TFH7tN/Vn/IhnVELCHtgh1vc+uYPZ7XvRYaqtDCdghIA==}
|
||||||
@@ -3881,7 +3945,6 @@ packages:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@hapi/hoek@9.3.0:
|
/@hapi/hoek@9.3.0:
|
||||||
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
|
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
|
||||||
@@ -4756,7 +4819,6 @@ packages:
|
|||||||
/@opentelemetry/api@1.8.0:
|
/@opentelemetry/api@1.8.0:
|
||||||
resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==}
|
resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@pkgjs/parseargs@0.11.0:
|
/@pkgjs/parseargs@0.11.0:
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
@@ -5587,7 +5649,6 @@ packages:
|
|||||||
/@tootallnate/once@2.0.0:
|
/@tootallnate/once@2.0.0:
|
||||||
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@trysound/sax@0.2.0:
|
/@trysound/sax@0.2.0:
|
||||||
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
||||||
@@ -5666,7 +5727,6 @@ packages:
|
|||||||
|
|
||||||
/@types/caseless@0.12.5:
|
/@types/caseless@0.12.5:
|
||||||
resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==}
|
resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/command-exists@1.2.3:
|
/@types/command-exists@1.2.3:
|
||||||
resolution: {integrity: sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ==}
|
resolution: {integrity: sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ==}
|
||||||
@@ -5995,7 +6055,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.12.5
|
'@types/node': 20.12.5
|
||||||
form-data: 3.0.1
|
form-data: 3.0.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/node@20.12.5:
|
/@types/node@20.12.5:
|
||||||
resolution: {integrity: sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==}
|
resolution: {integrity: sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==}
|
||||||
@@ -6132,7 +6191,6 @@ packages:
|
|||||||
'@types/node': 20.12.5
|
'@types/node': 20.12.5
|
||||||
'@types/tough-cookie': 4.0.5
|
'@types/tough-cookie': 4.0.5
|
||||||
form-data: 2.5.1
|
form-data: 2.5.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/responselike@1.0.3:
|
/@types/responselike@1.0.3:
|
||||||
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
|
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
|
||||||
@@ -6174,13 +6232,11 @@ packages:
|
|||||||
|
|
||||||
/@types/tough-cookie@4.0.5:
|
/@types/tough-cookie@4.0.5:
|
||||||
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
|
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/tunnel@0.0.3:
|
/@types/tunnel@0.0.3:
|
||||||
resolution: {integrity: sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==}
|
resolution: {integrity: sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.12.5
|
'@types/node': 20.12.5
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/uuid@8.3.4:
|
/@types/uuid@8.3.4:
|
||||||
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
|
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
|
||||||
@@ -6782,7 +6838,6 @@ packages:
|
|||||||
debug: 4.3.4(supports-color@5.5.0)
|
debug: 4.3.4(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/agent-base@7.1.1:
|
/agent-base@7.1.1:
|
||||||
resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
|
resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==}
|
||||||
@@ -6791,7 +6846,6 @@ packages:
|
|||||||
debug: 4.3.4(supports-color@5.5.0)
|
debug: 4.3.4(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/aggregate-error@3.1.0:
|
/aggregate-error@3.1.0:
|
||||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||||
@@ -7039,7 +7093,6 @@ packages:
|
|||||||
/arrify@2.0.1:
|
/arrify@2.0.1:
|
||||||
resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==}
|
resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ast-types-flow@0.0.8:
|
/ast-types-flow@0.0.8:
|
||||||
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
|
||||||
@@ -7055,7 +7108,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}
|
resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
retry: 0.13.1
|
retry: 0.13.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/asynckit@0.4.0:
|
/asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
@@ -7243,7 +7295,6 @@ packages:
|
|||||||
|
|
||||||
/bignumber.js@9.1.2:
|
/bignumber.js@9.1.2:
|
||||||
resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==}
|
resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/bin-check@4.1.0:
|
/bin-check@4.1.0:
|
||||||
resolution: {integrity: sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==}
|
resolution: {integrity: sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==}
|
||||||
@@ -7744,7 +7795,6 @@ packages:
|
|||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-db: 1.52.0
|
mime-db: 1.52.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/compute-scroll-into-view@1.0.20:
|
/compute-scroll-into-view@1.0.20:
|
||||||
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
|
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
|
||||||
@@ -8799,7 +8849,6 @@ packages:
|
|||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
stream-shift: 1.0.3
|
stream-shift: 1.0.3
|
||||||
dev: true
|
|
||||||
|
|
||||||
/eastasianwidth@0.2.0:
|
/eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
@@ -8860,7 +8909,6 @@ packages:
|
|||||||
|
|
||||||
/ent@2.2.0:
|
/ent@2.2.0:
|
||||||
resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==}
|
resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/entities@4.5.0:
|
/entities@4.5.0:
|
||||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
@@ -9635,7 +9683,6 @@ packages:
|
|||||||
|
|
||||||
/extend@3.0.2:
|
/extend@3.0.2:
|
||||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/fast-base64-decode@1.0.0:
|
/fast-base64-decode@1.0.0:
|
||||||
resolution: {integrity: sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==}
|
resolution: {integrity: sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==}
|
||||||
@@ -9685,7 +9732,6 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
strnum: 1.0.5
|
strnum: 1.0.5
|
||||||
dev: true
|
|
||||||
|
|
||||||
/fastest-levenshtein@1.0.16:
|
/fastest-levenshtein@1.0.16:
|
||||||
resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
|
resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
|
||||||
@@ -9915,7 +9961,6 @@ packages:
|
|||||||
asynckit: 0.4.0
|
asynckit: 0.4.0
|
||||||
combined-stream: 1.0.8
|
combined-stream: 1.0.8
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
dev: true
|
|
||||||
|
|
||||||
/form-data@3.0.1:
|
/form-data@3.0.1:
|
||||||
resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
|
resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
|
||||||
@@ -9924,7 +9969,6 @@ packages:
|
|||||||
asynckit: 0.4.0
|
asynckit: 0.4.0
|
||||||
combined-stream: 1.0.8
|
combined-stream: 1.0.8
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
dev: true
|
|
||||||
|
|
||||||
/form-data@4.0.0:
|
/form-data@4.0.0:
|
||||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||||
@@ -10020,7 +10064,6 @@ packages:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/gcp-metadata@6.1.0:
|
/gcp-metadata@6.1.0:
|
||||||
resolution: {integrity: sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==}
|
resolution: {integrity: sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==}
|
||||||
@@ -10031,7 +10074,6 @@ packages:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/gensync@1.0.0-beta.2:
|
/gensync@1.0.0-beta.2:
|
||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
@@ -10246,7 +10288,6 @@ packages:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/gopd@1.0.1:
|
/gopd@1.0.1:
|
||||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||||
@@ -10330,7 +10371,6 @@ packages:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/gzip-size@6.0.0:
|
/gzip-size@6.0.0:
|
||||||
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
|
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
|
||||||
@@ -10480,7 +10520,6 @@ packages:
|
|||||||
debug: 4.3.4(supports-color@5.5.0)
|
debug: 4.3.4(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/http-status@1.6.2:
|
/http-status@1.6.2:
|
||||||
resolution: {integrity: sha512-oUExvfNckrpTpDazph7kNG8sQi5au3BeTo0idaZFXEhTaJKu7GNJCLHI0rYY2wljm548MSTM+Ljj/c6anqu2zQ==}
|
resolution: {integrity: sha512-oUExvfNckrpTpDazph7kNG8sQi5au3BeTo0idaZFXEhTaJKu7GNJCLHI0rYY2wljm548MSTM+Ljj/c6anqu2zQ==}
|
||||||
@@ -10502,7 +10541,6 @@ packages:
|
|||||||
debug: 4.3.4(supports-color@5.5.0)
|
debug: 4.3.4(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/https-proxy-agent@7.0.4:
|
/https-proxy-agent@7.0.4:
|
||||||
resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
|
resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==}
|
||||||
@@ -10512,7 +10550,6 @@ packages:
|
|||||||
debug: 4.3.4(supports-color@5.5.0)
|
debug: 4.3.4(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/human-signals@2.1.0:
|
/human-signals@2.1.0:
|
||||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||||
@@ -11595,7 +11632,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
|
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
bignumber.js: 9.1.2
|
bignumber.js: 9.1.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/json-buffer@3.0.1:
|
/json-buffer@3.0.1:
|
||||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||||
@@ -11713,7 +11749,6 @@ packages:
|
|||||||
buffer-equal-constant-time: 1.0.1
|
buffer-equal-constant-time: 1.0.1
|
||||||
ecdsa-sig-formatter: 1.0.11
|
ecdsa-sig-formatter: 1.0.11
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/jws@3.2.2:
|
/jws@3.2.2:
|
||||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||||
@@ -11727,7 +11762,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
jwa: 2.0.0
|
jwa: 2.0.0
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/jwt-decode@4.0.0:
|
/jwt-decode@4.0.0:
|
||||||
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
|
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
|
||||||
@@ -12109,7 +12143,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
|
||||||
|
|
||||||
/mimic-fn@2.1.0:
|
/mimic-fn@2.1.0:
|
||||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||||
@@ -14505,12 +14538,10 @@ packages:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/retry@0.13.1:
|
/retry@0.13.1:
|
||||||
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
|
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/reusify@1.0.4:
|
/reusify@1.0.4:
|
||||||
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
|
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
|
||||||
@@ -14610,7 +14641,6 @@ packages:
|
|||||||
|
|
||||||
/sax@1.3.0:
|
/sax@1.3.0:
|
||||||
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
|
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/saxes@6.0.0:
|
/saxes@6.0.0:
|
||||||
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
@@ -15068,11 +15098,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==}
|
resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
stubs: 3.0.0
|
stubs: 3.0.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/stream-shift@1.0.3:
|
/stream-shift@1.0.3:
|
||||||
resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==}
|
resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/streamsearch@1.1.0:
|
/streamsearch@1.1.0:
|
||||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||||
@@ -15251,7 +15279,6 @@ packages:
|
|||||||
|
|
||||||
/stubs@3.0.0:
|
/stubs@3.0.0:
|
||||||
resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==}
|
resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/styled-jsx@5.1.1(@babel/core@7.24.4)(react@18.2.0):
|
/styled-jsx@5.1.1(@babel/core@7.24.4)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
|
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
|
||||||
@@ -15417,7 +15444,6 @@ packages:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/temp-dir@2.0.0:
|
/temp-dir@2.0.0:
|
||||||
resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
|
resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
|
||||||
@@ -15756,7 +15782,6 @@ packages:
|
|||||||
/tunnel@0.0.6:
|
/tunnel@0.0.6:
|
||||||
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
|
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
|
||||||
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
|
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/turbo-darwin-64@1.13.2:
|
/turbo-darwin-64@1.13.2:
|
||||||
resolution: {integrity: sha512-CCSuD8CfmtncpohCuIgq7eAzUas0IwSbHfI8/Q3vKObTdXyN8vAo01gwqXjDGpzG9bTEVedD0GmLbD23dR0MLA==}
|
resolution: {integrity: sha512-CCSuD8CfmtncpohCuIgq7eAzUas0IwSbHfI8/Q3vKObTdXyN8vAo01gwqXjDGpzG9bTEVedD0GmLbD23dR0MLA==}
|
||||||
@@ -16058,7 +16083,6 @@ packages:
|
|||||||
/uuid@8.3.2:
|
/uuid@8.3.2:
|
||||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
|
||||||
|
|
||||||
/uuid@9.0.0:
|
/uuid@9.0.0:
|
||||||
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
|
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
|
||||||
@@ -16416,12 +16440,10 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
sax: 1.3.0
|
sax: 1.3.0
|
||||||
xmlbuilder: 11.0.1
|
xmlbuilder: 11.0.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/xmlbuilder@11.0.1:
|
/xmlbuilder@11.0.1:
|
||||||
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/xmlchars@2.2.0:
|
/xmlchars@2.2.0:
|
||||||
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
|
||||||
|
|||||||
@@ -31,6 +31,10 @@
|
|||||||
"@payloadcms/plugin-stripe": "workspace:*",
|
"@payloadcms/plugin-stripe": "workspace:*",
|
||||||
"@payloadcms/richtext-lexical": "workspace:*",
|
"@payloadcms/richtext-lexical": "workspace:*",
|
||||||
"@payloadcms/richtext-slate": "workspace:*",
|
"@payloadcms/richtext-slate": "workspace:*",
|
||||||
|
"@payloadcms/storage-azure": "workspace:*",
|
||||||
|
"@payloadcms/storage-gcs": "workspace:*",
|
||||||
|
"@payloadcms/storage-s3": "workspace:*",
|
||||||
|
"@payloadcms/storage-vercel-blob": "workspace:*",
|
||||||
"@payloadcms/translations": "workspace:*",
|
"@payloadcms/translations": "workspace:*",
|
||||||
"@payloadcms/ui": "workspace:*",
|
"@payloadcms/ui": "workspace:*",
|
||||||
"comment-json": "^4.2.3",
|
"comment-json": "^4.2.3",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Media } from './collections/Media.js'
|
|||||||
import { MediaWithPrefix } from './collections/MediaWithPrefix.js'
|
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'
|
||||||
|
|
||||||
let adapter: Adapter
|
let adapter: Adapter
|
||||||
let uploadOptions
|
let uploadOptions
|
||||||
@@ -22,10 +23,6 @@ dotenv.config({
|
|||||||
path: path.resolve(process.cwd(), './test/plugin-cloud-storage/.env.emulated'),
|
path: path.resolve(process.cwd(), './test/plugin-cloud-storage/.env.emulated'),
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Using plugin-cloud-storage adapter: ${process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER === 'azure') {
|
if (process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER === 'azure') {
|
||||||
adapter = azureBlobStorageAdapter({
|
adapter = azureBlobStorageAdapter({
|
||||||
allowContainerCreate: process.env.AZURE_STORAGE_ALLOW_CONTAINER_CREATE === 'true',
|
allowContainerCreate: process.env.AZURE_STORAGE_ALLOW_CONTAINER_CREATE === 'true',
|
||||||
@@ -114,6 +111,12 @@ export default buildConfigWithDefaults({
|
|||||||
password: devUser.password,
|
password: devUser.password,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await createTestBucket()
|
||||||
|
|
||||||
|
payload.logger.info(
|
||||||
|
`Using plugin-cloud-storage adapter: ${process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER}`,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
cloudStorage({
|
cloudStorage({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { describeIfInCIOrHasLocalstack } from '../helpers.js'
|
|||||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||||
import configPromise from './config.js'
|
import configPromise from './config.js'
|
||||||
import { mediaSlug, mediaWithPrefixSlug, prefix } from './shared.js'
|
import { mediaSlug, mediaWithPrefixSlug, prefix } from './shared.js'
|
||||||
|
import { clearTestBucket, 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)
|
||||||
@@ -25,7 +26,7 @@ describe('@payloadcms/plugin-cloud-storage', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const TEST_BUCKET = 'payload-bucket'
|
const TEST_BUCKET = process.env.S3_BUCKET
|
||||||
|
|
||||||
let client: AWS.S3Client
|
let client: AWS.S3Client
|
||||||
describeIfInCIOrHasLocalstack()('plugin-cloud-storage', () => {
|
describeIfInCIOrHasLocalstack()('plugin-cloud-storage', () => {
|
||||||
@@ -42,11 +43,11 @@ describe('@payloadcms/plugin-cloud-storage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await createTestBucket()
|
await createTestBucket()
|
||||||
await clearTestBucket()
|
await clearTestBucket(client)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await clearTestBucket()
|
await clearTestBucket(client)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can upload', async () => {
|
it('can upload', async () => {
|
||||||
@@ -63,7 +64,7 @@ describe('@payloadcms/plugin-cloud-storage', () => {
|
|||||||
uploadId: upload.id,
|
uploadId: upload.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(upload.url).toEqual(`/api/${mediaSlug}/file/${upload.filename as string}`)
|
expect(upload.url).toEqual(`/api/${mediaSlug}/file/${String(upload.filename)}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can upload with prefix', async () => {
|
it('can upload with prefix', async () => {
|
||||||
@@ -97,38 +98,6 @@ describe('@payloadcms/plugin-cloud-storage', () => {
|
|||||||
it.todo('can upload')
|
it.todo('can upload')
|
||||||
})
|
})
|
||||||
|
|
||||||
async function createTestBucket() {
|
|
||||||
const makeBucketRes = await client.send(new AWS.CreateBucketCommand({ Bucket: TEST_BUCKET }))
|
|
||||||
|
|
||||||
if (makeBucketRes.$metadata.httpStatusCode !== 200) {
|
|
||||||
throw new Error(`Failed to create bucket. ${makeBucketRes.$metadata.httpStatusCode}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clearTestBucket() {
|
|
||||||
const listedObjects = await client.send(
|
|
||||||
new AWS.ListObjectsV2Command({
|
|
||||||
Bucket: TEST_BUCKET,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!listedObjects?.Contents?.length) return
|
|
||||||
|
|
||||||
const deleteParams = {
|
|
||||||
Bucket: TEST_BUCKET,
|
|
||||||
Delete: { Objects: [] },
|
|
||||||
}
|
|
||||||
|
|
||||||
listedObjects.Contents.forEach(({ Key }) => {
|
|
||||||
deleteParams.Delete.Objects.push({ Key })
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteResult = await client.send(new AWS.DeleteObjectsCommand(deleteParams))
|
|
||||||
if (deleteResult.Errors?.length) {
|
|
||||||
throw new Error(JSON.stringify(deleteResult.Errors))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function verifyUploads({
|
async function verifyUploads({
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
uploadId,
|
uploadId,
|
||||||
|
|||||||
48
test/plugin-cloud-storage/utils.ts
Normal file
48
test/plugin-cloud-storage/utils.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import * as AWS from '@aws-sdk/client-s3'
|
||||||
|
|
||||||
|
const getS3Client = () => {
|
||||||
|
return new AWS.S3({
|
||||||
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
|
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
|
||||||
|
region: process.env.S3_REGION,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTestBucket(bucketName?: string) {
|
||||||
|
const client = getS3Client()
|
||||||
|
const makeBucketRes = await client.send(
|
||||||
|
new AWS.CreateBucketCommand({ Bucket: bucketName || process.env.S3_BUCKET }),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (makeBucketRes.$metadata.httpStatusCode !== 200) {
|
||||||
|
throw new Error(`Failed to create bucket. ${makeBucketRes.$metadata.httpStatusCode}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearTestBucket(client: AWS.S3Client, bucketName?: string) {
|
||||||
|
const listedObjects = await client.send(
|
||||||
|
new AWS.ListObjectsV2Command({
|
||||||
|
Bucket: bucketName || process.env.S3_BUCKET,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!listedObjects?.Contents?.length) return
|
||||||
|
|
||||||
|
const deleteParams = {
|
||||||
|
Bucket: bucketName || process.env.S3_BUCKET,
|
||||||
|
Delete: { Objects: [] },
|
||||||
|
}
|
||||||
|
|
||||||
|
listedObjects.Contents.forEach(({ Key }) => {
|
||||||
|
deleteParams.Delete.Objects.push({ Key })
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteResult = await client.send(new AWS.DeleteObjectsCommand(deleteParams))
|
||||||
|
if (deleteResult.Errors?.length) {
|
||||||
|
throw new Error(JSON.stringify(deleteResult.Errors))
|
||||||
|
}
|
||||||
|
}
|
||||||
8
test/storage-azure/.eslintrc.cjs
Normal file
8
test/storage-azure/.eslintrc.cjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
ignorePatterns: ['payload-types.ts'],
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.eslint.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
34
test/storage-azure/collections/Media.ts
Normal file
34
test/storage-azure/collections/Media.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const Media: CollectionConfig = {
|
||||||
|
slug: 'media',
|
||||||
|
upload: {
|
||||||
|
disableLocalStorage: true,
|
||||||
|
resizeOptions: {
|
||||||
|
position: 'center',
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
},
|
||||||
|
imageSizes: [
|
||||||
|
{
|
||||||
|
height: 400,
|
||||||
|
width: 400,
|
||||||
|
crop: 'center',
|
||||||
|
name: 'square',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 900,
|
||||||
|
height: 450,
|
||||||
|
crop: 'center',
|
||||||
|
name: 'sixteenByNineMedium',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'alt',
|
||||||
|
label: 'Alt Text',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
9
test/storage-azure/collections/MediaWithPrefix.ts
Normal file
9
test/storage-azure/collections/MediaWithPrefix.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const MediaWithPrefix: CollectionConfig = {
|
||||||
|
slug: 'media-with-prefix',
|
||||||
|
upload: {
|
||||||
|
disableLocalStorage: false,
|
||||||
|
},
|
||||||
|
fields: [],
|
||||||
|
}
|
||||||
16
test/storage-azure/collections/Users.ts
Normal file
16
test/storage-azure/collections/Users.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const Users: CollectionConfig = {
|
||||||
|
slug: 'users',
|
||||||
|
auth: true,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'email',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
// Email added by default
|
||||||
|
// Add more fields as needed
|
||||||
|
],
|
||||||
|
}
|
||||||
48
test/storage-azure/config.ts
Normal file
48
test/storage-azure/config.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { azureStorage } from '@payloadcms/storage-azure'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
|
import { devUser } from '../credentials.js'
|
||||||
|
import { Media } from './collections/Media.js'
|
||||||
|
import { MediaWithPrefix } from './collections/MediaWithPrefix.js'
|
||||||
|
import { Users } from './collections/Users.js'
|
||||||
|
import { mediaSlug, mediaWithPrefixSlug, prefix } from './shared.js'
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
let uploadOptions
|
||||||
|
|
||||||
|
// Load config to work with emulated services
|
||||||
|
dotenv.config({
|
||||||
|
path: path.resolve(dirname, '../plugin-cloud-storage/.env.emulated'),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default buildConfigWithDefaults({
|
||||||
|
collections: [Media, MediaWithPrefix, Users],
|
||||||
|
onInit: async (payload) => {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: devUser.email,
|
||||||
|
password: devUser.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
azureStorage({
|
||||||
|
collections: {
|
||||||
|
[mediaSlug]: true,
|
||||||
|
[mediaWithPrefixSlug]: {
|
||||||
|
prefix,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allowContainerCreate: process.env.AZURE_STORAGE_ALLOW_CONTAINER_CREATE === 'true',
|
||||||
|
baseURL: process.env.AZURE_STORAGE_ACCOUNT_BASEURL,
|
||||||
|
connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
|
||||||
|
containerName: process.env.AZURE_STORAGE_CONTAINER_NAME,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
upload: uploadOptions,
|
||||||
|
})
|
||||||
3
test/storage-azure/shared.ts
Normal file
3
test/storage-azure/shared.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const mediaSlug = 'media'
|
||||||
|
export const mediaWithPrefixSlug = 'media-with-prefix'
|
||||||
|
export const prefix = 'test-prefix'
|
||||||
13
test/storage-azure/tsconfig.eslint.json
Normal file
13
test/storage-azure/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
// extend your base config to share compilerOptions, etc
|
||||||
|
//"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
// ensure that nobody can accidentally use this config for a build
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
// whatever paths you intend to lint
|
||||||
|
"./**/*.ts",
|
||||||
|
"./**/*.tsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
8
test/storage-gcs/.eslintrc.cjs
Normal file
8
test/storage-gcs/.eslintrc.cjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
ignorePatterns: ['payload-types.ts'],
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.eslint.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
34
test/storage-gcs/collections/Media.ts
Normal file
34
test/storage-gcs/collections/Media.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const Media: CollectionConfig = {
|
||||||
|
slug: 'media',
|
||||||
|
upload: {
|
||||||
|
disableLocalStorage: true,
|
||||||
|
resizeOptions: {
|
||||||
|
position: 'center',
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
},
|
||||||
|
imageSizes: [
|
||||||
|
{
|
||||||
|
height: 400,
|
||||||
|
width: 400,
|
||||||
|
crop: 'center',
|
||||||
|
name: 'square',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 900,
|
||||||
|
height: 450,
|
||||||
|
crop: 'center',
|
||||||
|
name: 'sixteenByNineMedium',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'alt',
|
||||||
|
label: 'Alt Text',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
9
test/storage-gcs/collections/MediaWithPrefix.ts
Normal file
9
test/storage-gcs/collections/MediaWithPrefix.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const MediaWithPrefix: CollectionConfig = {
|
||||||
|
slug: 'media-with-prefix',
|
||||||
|
upload: {
|
||||||
|
disableLocalStorage: false,
|
||||||
|
},
|
||||||
|
fields: [],
|
||||||
|
}
|
||||||
16
test/storage-gcs/collections/Users.ts
Normal file
16
test/storage-gcs/collections/Users.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const Users: CollectionConfig = {
|
||||||
|
slug: 'users',
|
||||||
|
auth: true,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'email',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
// Email added by default
|
||||||
|
// Add more fields as needed
|
||||||
|
],
|
||||||
|
}
|
||||||
49
test/storage-gcs/config.ts
Normal file
49
test/storage-gcs/config.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { gcsStorage } from '@payloadcms/storage-gcs'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
|
import { devUser } from '../credentials.js'
|
||||||
|
import { Media } from './collections/Media.js'
|
||||||
|
import { MediaWithPrefix } from './collections/MediaWithPrefix.js'
|
||||||
|
import { Users } from './collections/Users.js'
|
||||||
|
import { mediaSlug, mediaWithPrefixSlug, prefix } from './shared.js'
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
let uploadOptions
|
||||||
|
|
||||||
|
// Load config to work with emulated services
|
||||||
|
dotenv.config({
|
||||||
|
path: path.resolve(dirname, '../plugin-cloud-storage/.env.emulated'),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default buildConfigWithDefaults({
|
||||||
|
collections: [Media, MediaWithPrefix, Users],
|
||||||
|
onInit: async (payload) => {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: devUser.email,
|
||||||
|
password: devUser.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
gcsStorage({
|
||||||
|
collections: {
|
||||||
|
[mediaSlug]: true,
|
||||||
|
[mediaWithPrefixSlug]: {
|
||||||
|
prefix,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bucket: process.env.GCS_BUCKET,
|
||||||
|
options: {
|
||||||
|
apiEndpoint: process.env.GCS_ENDPOINT,
|
||||||
|
projectId: process.env.GCS_PROJECT_ID,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
upload: uploadOptions,
|
||||||
|
})
|
||||||
3
test/storage-gcs/shared.ts
Normal file
3
test/storage-gcs/shared.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const mediaSlug = 'media'
|
||||||
|
export const mediaWithPrefixSlug = 'media-with-prefix'
|
||||||
|
export const prefix = 'test-prefix'
|
||||||
13
test/storage-gcs/tsconfig.eslint.json
Normal file
13
test/storage-gcs/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
// extend your base config to share compilerOptions, etc
|
||||||
|
//"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
// ensure that nobody can accidentally use this config for a build
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
// whatever paths you intend to lint
|
||||||
|
"./**/*.ts",
|
||||||
|
"./**/*.tsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
32
test/storage-s3/.env.emulated
Normal file
32
test/storage-s3/.env.emulated
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Sample creds for working locally with docker-compose
|
||||||
|
|
||||||
|
MONGODB_URI=mongodb://localhost/payload-plugin-cloud-storage
|
||||||
|
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:3000
|
||||||
|
PAYLOAD_SECRET=45ligj345ligj4wl5igj4lw5igj45ligj45wlijl
|
||||||
|
PAYLOAD_CONFIG_PATH=config.ts
|
||||||
|
|
||||||
|
AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://localhost:10000/devstoreaccount1;QueueEndpoint=http://localhost:10001/devstoreaccount1;
|
||||||
|
AZURE_STORAGE_CONTAINER_NAME=az-media
|
||||||
|
AZURE_STORAGE_ALLOW_CONTAINER_CREATE=true
|
||||||
|
AZURE_STORAGE_ACCOUNT_BASEURL=http://localhost:10000/devstoreaccount1
|
||||||
|
|
||||||
|
S3_ENDPOINT=http://localhost:4566
|
||||||
|
S3_ACCESS_KEY_ID=payloadAccessKey
|
||||||
|
S3_SECRET_ACCESS_KEY=alwiejglaiwhewlihgawe
|
||||||
|
S3_BUCKET=payload-bucket
|
||||||
|
S3_FORCE_PATH_STYLE=true
|
||||||
|
S3_REGION=us-east-1
|
||||||
|
|
||||||
|
GCS_ENDPOINT=http://localhost:4443
|
||||||
|
GCS_PROJECT_ID=test
|
||||||
|
GCS_BUCKET=payload-bucket
|
||||||
|
|
||||||
|
R2_ENDPOINT=https://cloudflare-generated-domain.r2.cloudflarestorage.com
|
||||||
|
R2_REGION=auto
|
||||||
|
R2_ACCESS_KEY_ID=access-key-id
|
||||||
|
R2_SECRET_ACCESS_KEY=secret-access-key
|
||||||
|
R2_BUCKET=payload-bucket
|
||||||
|
R2_FORCE_PATH_STYLE=
|
||||||
|
|
||||||
|
PAYLOAD_DROP_DATABASE=true
|
||||||
|
PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER=s3
|
||||||
8
test/storage-s3/.eslintrc.cjs
Normal file
8
test/storage-s3/.eslintrc.cjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
ignorePatterns: ['payload-types.ts'],
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.eslint.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
34
test/storage-s3/collections/Media.ts
Normal file
34
test/storage-s3/collections/Media.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const Media: CollectionConfig = {
|
||||||
|
slug: 'media',
|
||||||
|
upload: {
|
||||||
|
disableLocalStorage: true,
|
||||||
|
resizeOptions: {
|
||||||
|
position: 'center',
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
},
|
||||||
|
imageSizes: [
|
||||||
|
{
|
||||||
|
height: 400,
|
||||||
|
width: 400,
|
||||||
|
crop: 'center',
|
||||||
|
name: 'square',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 900,
|
||||||
|
height: 450,
|
||||||
|
crop: 'center',
|
||||||
|
name: 'sixteenByNineMedium',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'alt',
|
||||||
|
label: 'Alt Text',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
9
test/storage-s3/collections/MediaWithPrefix.ts
Normal file
9
test/storage-s3/collections/MediaWithPrefix.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const MediaWithPrefix: CollectionConfig = {
|
||||||
|
slug: 'media-with-prefix',
|
||||||
|
upload: {
|
||||||
|
disableLocalStorage: false,
|
||||||
|
},
|
||||||
|
fields: [],
|
||||||
|
}
|
||||||
16
test/storage-s3/collections/Users.ts
Normal file
16
test/storage-s3/collections/Users.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const Users: CollectionConfig = {
|
||||||
|
slug: 'users',
|
||||||
|
auth: true,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'email',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
// Email added by default
|
||||||
|
// Add more fields as needed
|
||||||
|
],
|
||||||
|
}
|
||||||
54
test/storage-s3/config.ts
Normal file
54
test/storage-s3/config.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { s3Storage } from '@payloadcms/storage-s3'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
|
import { devUser } from '../credentials.js'
|
||||||
|
import { Media } from './collections/Media.js'
|
||||||
|
import { MediaWithPrefix } from './collections/MediaWithPrefix.js'
|
||||||
|
import { Users } from './collections/Users.js'
|
||||||
|
import { mediaSlug, mediaWithPrefixSlug, prefix } from './shared.js'
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
let uploadOptions
|
||||||
|
|
||||||
|
// Load config to work with emulated services
|
||||||
|
dotenv.config({
|
||||||
|
path: path.resolve(dirname, '../plugin-cloud-storage/.env.emulated'),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default buildConfigWithDefaults({
|
||||||
|
collections: [Media, MediaWithPrefix, Users],
|
||||||
|
onInit: async (payload) => {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: devUser.email,
|
||||||
|
password: devUser.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
s3Storage({
|
||||||
|
collections: {
|
||||||
|
[mediaSlug]: true,
|
||||||
|
[mediaWithPrefixSlug]: {
|
||||||
|
prefix,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bucket: process.env.S3_BUCKET,
|
||||||
|
config: {
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||||
|
},
|
||||||
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
|
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
|
||||||
|
region: process.env.S3_REGION,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
upload: uploadOptions,
|
||||||
|
})
|
||||||
155
test/storage-s3/int.spec.ts
Normal file
155
test/storage-s3/int.spec.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import type { Payload } from 'payload'
|
||||||
|
|
||||||
|
import * as AWS from '@aws-sdk/client-s3'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||||
|
import configPromise from './config.js'
|
||||||
|
import { mediaSlug, mediaWithPrefixSlug, prefix } from './shared.js'
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
let payload: Payload
|
||||||
|
|
||||||
|
describe('@payloadcms/storage-s3', () => {
|
||||||
|
const TEST_BUCKET = process.env.S3_BUCKET
|
||||||
|
let client: AWS.S3Client
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
;({ payload } = await initPayloadInt(configPromise))
|
||||||
|
|
||||||
|
client = new AWS.S3({
|
||||||
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
|
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
|
||||||
|
region: process.env.S3_REGION,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await createTestBucket()
|
||||||
|
await clearTestBucket()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (typeof payload.db.destroy === 'function') {
|
||||||
|
await payload.db.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
afterEach(async () => {
|
||||||
|
await clearTestBucket()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can upload', async () => {
|
||||||
|
const upload = await payload.create({
|
||||||
|
collection: mediaSlug,
|
||||||
|
data: {},
|
||||||
|
filePath: path.resolve(dirname, '../uploads/image.png'),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(upload.id).toBeTruthy()
|
||||||
|
|
||||||
|
await verifyUploads({
|
||||||
|
collectionSlug: mediaSlug,
|
||||||
|
uploadId: upload.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(upload.url).toEqual(`/api/${mediaSlug}/file/${String(upload.filename)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can upload with prefix', async () => {
|
||||||
|
const upload = await payload.create({
|
||||||
|
collection: mediaWithPrefixSlug,
|
||||||
|
data: {},
|
||||||
|
filePath: path.resolve(dirname, '../uploads/image.png'),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(upload.id).toBeTruthy()
|
||||||
|
|
||||||
|
await verifyUploads({
|
||||||
|
collectionSlug: mediaWithPrefixSlug,
|
||||||
|
uploadId: upload.id,
|
||||||
|
prefix,
|
||||||
|
})
|
||||||
|
expect(upload.url).toEqual(`/api/${mediaWithPrefixSlug}/file/${String(upload.filename)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('R2', () => {
|
||||||
|
it.todo('can upload')
|
||||||
|
})
|
||||||
|
|
||||||
|
async function createTestBucket() {
|
||||||
|
const makeBucketRes = await client.send(new AWS.CreateBucketCommand({ Bucket: TEST_BUCKET }))
|
||||||
|
|
||||||
|
if (makeBucketRes.$metadata.httpStatusCode !== 200) {
|
||||||
|
throw new Error(`Failed to create bucket. ${makeBucketRes.$metadata.httpStatusCode}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearTestBucket() {
|
||||||
|
const listedObjects = await client.send(
|
||||||
|
new AWS.ListObjectsV2Command({
|
||||||
|
Bucket: TEST_BUCKET,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!listedObjects?.Contents?.length) return
|
||||||
|
|
||||||
|
const deleteParams = {
|
||||||
|
Bucket: TEST_BUCKET,
|
||||||
|
Delete: { Objects: [] },
|
||||||
|
}
|
||||||
|
|
||||||
|
listedObjects.Contents.forEach(({ Key }) => {
|
||||||
|
deleteParams.Delete.Objects.push({ Key })
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteResult = await client.send(new AWS.DeleteObjectsCommand(deleteParams))
|
||||||
|
if (deleteResult.Errors?.length) {
|
||||||
|
throw new Error(JSON.stringify(deleteResult.Errors))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyUploads({
|
||||||
|
collectionSlug,
|
||||||
|
uploadId,
|
||||||
|
prefix = '',
|
||||||
|
}: {
|
||||||
|
collectionSlug: string
|
||||||
|
prefix?: string
|
||||||
|
uploadId: number | string
|
||||||
|
}) {
|
||||||
|
const uploadData = (await payload.findByID({
|
||||||
|
collection: collectionSlug,
|
||||||
|
id: uploadId,
|
||||||
|
})) as unknown as { filename: string; sizes: Record<string, { filename: string }> }
|
||||||
|
|
||||||
|
const fileKeys = Object.keys(uploadData.sizes || {}).map((key) => {
|
||||||
|
const rawFilename = uploadData.sizes[key].filename
|
||||||
|
return prefix ? `${prefix}/${rawFilename}` : rawFilename
|
||||||
|
})
|
||||||
|
|
||||||
|
fileKeys.push(`${prefix ? `${prefix}/` : ''}${uploadData.filename}`)
|
||||||
|
try {
|
||||||
|
for (const key of fileKeys) {
|
||||||
|
const { $metadata } = await client.send(
|
||||||
|
new AWS.HeadObjectCommand({ Bucket: TEST_BUCKET, Key: key }),
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($metadata.httpStatusCode !== 200) {
|
||||||
|
console.error('Error verifying uploads', key, $metadata)
|
||||||
|
throw new Error(`Error verifying uploads: ${key}, ${$metadata.httpStatusCode}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify each size was properly uploaded
|
||||||
|
expect($metadata.httpStatusCode).toBe(200)
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('Error verifying uploads:', fileKeys, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
3
test/storage-s3/shared.ts
Normal file
3
test/storage-s3/shared.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const mediaSlug = 'media'
|
||||||
|
export const mediaWithPrefixSlug = 'media-with-prefix'
|
||||||
|
export const prefix = 'test-prefix'
|
||||||
13
test/storage-s3/tsconfig.eslint.json
Normal file
13
test/storage-s3/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
// extend your base config to share compilerOptions, etc
|
||||||
|
//"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
// ensure that nobody can accidentally use this config for a build
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
// whatever paths you intend to lint
|
||||||
|
"./**/*.ts",
|
||||||
|
"./**/*.tsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
8
test/storage-vercel-blob/.eslintrc.cjs
Normal file
8
test/storage-vercel-blob/.eslintrc.cjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
ignorePatterns: ['payload-types.ts'],
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.eslint.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
34
test/storage-vercel-blob/collections/Media.ts
Normal file
34
test/storage-vercel-blob/collections/Media.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const Media: CollectionConfig = {
|
||||||
|
slug: 'media',
|
||||||
|
upload: {
|
||||||
|
disableLocalStorage: true,
|
||||||
|
resizeOptions: {
|
||||||
|
position: 'center',
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
},
|
||||||
|
imageSizes: [
|
||||||
|
{
|
||||||
|
height: 400,
|
||||||
|
width: 400,
|
||||||
|
crop: 'center',
|
||||||
|
name: 'square',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 900,
|
||||||
|
height: 450,
|
||||||
|
crop: 'center',
|
||||||
|
name: 'sixteenByNineMedium',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'alt',
|
||||||
|
label: 'Alt Text',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
9
test/storage-vercel-blob/collections/MediaWithPrefix.ts
Normal file
9
test/storage-vercel-blob/collections/MediaWithPrefix.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const MediaWithPrefix: CollectionConfig = {
|
||||||
|
slug: 'media-with-prefix',
|
||||||
|
upload: {
|
||||||
|
disableLocalStorage: false,
|
||||||
|
},
|
||||||
|
fields: [],
|
||||||
|
}
|
||||||
16
test/storage-vercel-blob/collections/Users.ts
Normal file
16
test/storage-vercel-blob/collections/Users.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { CollectionConfig } from 'payload/types'
|
||||||
|
|
||||||
|
export const Users: CollectionConfig = {
|
||||||
|
slug: 'users',
|
||||||
|
auth: true,
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'email',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: () => true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
// Email added by default
|
||||||
|
// Add more fields as needed
|
||||||
|
],
|
||||||
|
}
|
||||||
45
test/storage-vercel-blob/config.ts
Normal file
45
test/storage-vercel-blob/config.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
|
import { devUser } from '../credentials.js'
|
||||||
|
import { Media } from './collections/Media.js'
|
||||||
|
import { MediaWithPrefix } from './collections/MediaWithPrefix.js'
|
||||||
|
import { Users } from './collections/Users.js'
|
||||||
|
import { mediaSlug, mediaWithPrefixSlug, prefix } from './shared.js'
|
||||||
|
const filename = fileURLToPath(import.meta.url)
|
||||||
|
const dirname = path.dirname(filename)
|
||||||
|
|
||||||
|
let uploadOptions
|
||||||
|
|
||||||
|
// TODO: Load this into CI or have shared creds
|
||||||
|
dotenv.config({
|
||||||
|
path: path.resolve(dirname, '.env'),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default buildConfigWithDefaults({
|
||||||
|
collections: [Media, MediaWithPrefix, Users],
|
||||||
|
onInit: async (payload) => {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: devUser.email,
|
||||||
|
password: devUser.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
vercelBlobStorage({
|
||||||
|
collections: {
|
||||||
|
[mediaSlug]: true,
|
||||||
|
[mediaWithPrefixSlug]: {
|
||||||
|
prefix,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
token: process.env.BLOB_READ_WRITE_TOKEN,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
upload: uploadOptions,
|
||||||
|
})
|
||||||
3
test/storage-vercel-blob/shared.ts
Normal file
3
test/storage-vercel-blob/shared.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const mediaSlug = 'media'
|
||||||
|
export const mediaWithPrefixSlug = 'media-with-prefix'
|
||||||
|
export const prefix = 'test-prefix'
|
||||||
13
test/storage-vercel-blob/tsconfig.eslint.json
Normal file
13
test/storage-vercel-blob/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
// extend your base config to share compilerOptions, etc
|
||||||
|
//"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
// ensure that nobody can accidentally use this config for a build
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
// whatever paths you intend to lint
|
||||||
|
"./**/*.ts",
|
||||||
|
"./**/*.tsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user