@payloadcms/plugin-cloud-storage 1.0.14 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +10 -0
- package/.eslintrc.js +14 -14
- package/.gitignore +248 -0
- package/.idea/.gitignore +5 -0
- package/.idea/httpRequests/2023-04-07T152957.206.png +0 -0
- package/.idea/httpRequests/2023-04-07T153025.403.html +10 -0
- package/.idea/httpRequests/2023-04-07T153146.200.png +0 -0
- package/.idea/httpRequests/http-client.cookies +1 -0
- package/.idea/httpRequests/http-requests-log.http +74 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/jsLinters/eslint.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/plugin-cloud-storage.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/.idea/workspace.xml +269 -0
- package/.prettierignore +1 -0
- package/.prettierrc.js +8 -0
- package/.vscode/launch.json +28 -0
- package/.vscode/settings.json +9 -0
- package/LICENSE.md +22 -22
- package/README.md +178 -174
- package/azure.d.ts +1 -1
- package/azure.js +1 -1
- package/dev/.env +25 -0
- package/dev/.env.example +21 -0
- package/dev/build/127.d2c2ffcfff69fabfdd1b.js +1 -0
- package/dev/build/16.17dbe03b1d0a96f3e564.js +2 -0
- package/dev/build/16.17dbe03b1d0a96f3e564.js.LICENSE.txt +8 -0
- package/dev/build/171.bbcbae3ea90468ad0cad.js +2 -0
- package/dev/build/171.bbcbae3ea90468ad0cad.js.LICENSE.txt +8 -0
- package/dev/build/18.e50c27edff6716f930d9.js +1 -0
- package/dev/build/205.33c7a29683ba98de93e0.js +1 -0
- package/dev/build/2211c49456cd07331ea9.woff +0 -0
- package/dev/build/234.79395f82c18207c13766.js +1 -0
- package/dev/build/266.9d4a240b3e0985bd7dd5.js +1 -0
- package/dev/build/296.4c5d646257b42c915834.js +1 -0
- package/dev/build/304.40dbe690de322c8f7c0d.js +2 -0
- package/dev/build/304.40dbe690de322c8f7c0d.js.LICENSE.txt +37 -0
- package/dev/build/349.446c12bffd3905085fdb.js +1 -0
- package/dev/build/354.5acd04b85b96a9839125.js +1 -0
- package/dev/build/40ad7515b8674bb854a1.woff2 +0 -0
- package/dev/build/422.086542466cdc9f6a2437.js +2 -0
- package/dev/build/422.086542466cdc9f6a2437.js.LICENSE.txt +6 -0
- package/dev/build/491.0bfe1bb0ecfe383179aa.js +1 -0
- package/dev/build/4d8845b830f4e8e2affb.png +0 -0
- package/dev/build/51922ceb71da289688d3.woff2 +0 -0
- package/dev/build/522443364fda49e9e0ed.woff2 +0 -0
- package/dev/build/531.1c6f53f3b44a3c45b444.js +2 -0
- package/dev/build/531.1c6f53f3b44a3c45b444.js.LICENSE.txt +6 -0
- package/dev/build/570.f2d9b99706765fbf0225.js +1 -0
- package/dev/build/599.570a04990d5806004f61.js +1 -0
- package/dev/build/5b718d9772de251a8c0a.woff2 +0 -0
- package/dev/build/778.41ae26bcd617861ad586.js +1 -0
- package/dev/build/783.0117995f2ff6036d6746.js +1 -0
- package/dev/build/787999a6af6a17efbc7c.woff +0 -0
- package/dev/build/78b8935fb481e11c92ce.woff +0 -0
- package/dev/build/860.7688681d3269f3f16e9a.js +1 -0
- package/dev/build/892.1a4ca5ac67d81038ceec.js +1 -0
- package/dev/build/896.d8cb1160388dc29d6364.js +1 -0
- package/dev/build/8b4ddd0d08500553efde.woff +0 -0
- package/dev/build/8f612153248094525d9d.woff +0 -0
- package/dev/build/995.cc11e738ff81a85821b4.js +1 -0
- package/dev/build/9c7dfd0036f7bd24b053.woff2 +0 -0
- package/dev/build/a1cfdc5b5250b7c4b481.woff2 +0 -0
- package/dev/build/d7aeda9e48ce098e7b48.woff +0 -0
- package/dev/build/e009f21405b4d7e89367.woff2 +0 -0
- package/dev/build/e7caa9e17af6ac87d182.woff +0 -0
- package/dev/build/ebcc1430049fddb274f8.svg +15 -0
- package/dev/build/efe8f6a3b46446cc9135.woff +0 -0
- package/dev/build/f53bb8d4b29adc903703.woff2 +0 -0
- package/dev/build/index.html +1 -0
- package/dev/build/main.a2003d502fbb9aaa3e8d.js +2 -0
- package/dev/build/main.a2003d502fbb9aaa3e8d.js.LICENSE.txt +57 -0
- package/dev/build/styles.css +1 -0
- package/dev/build/styles.fa29d16b0baf5b98a1cf.js +1 -0
- package/dev/nodemon.json +8 -0
- package/dev/package.json +32 -0
- package/dev/src/collections/Media.ts +56 -0
- package/dev/src/collections/Users.ts +23 -0
- package/dev/src/mocks/fsMock.js +1 -0
- package/dev/src/mocks/promisifyMock.js +1 -0
- package/dev/src/payload.config.ts +111 -0
- package/dev/src/server.ts +26 -0
- package/dev/tsconfig.json +20 -0
- package/dist/adapters/azure/fileStub.d.ts +2 -0
- package/dist/adapters/azure/fileStub.js +4 -0
- package/dist/adapters/azure/fileStub.js.map +1 -0
- package/dist/adapters/azure/generateURL.d.ts +7 -7
- package/dist/adapters/azure/generateURL.js +15 -15
- package/dist/adapters/azure/handleDelete.d.ts +9 -9
- package/dist/adapters/azure/handleDelete.js +63 -63
- package/dist/adapters/azure/handleUpload.d.ts +10 -10
- package/dist/adapters/azure/handleUpload.js +80 -65
- package/dist/adapters/azure/handleUpload.js.map +1 -1
- package/dist/adapters/azure/index.d.ts +8 -8
- package/dist/adapters/azure/index.js +42 -42
- package/dist/adapters/azure/mock.d.ts +13 -7
- package/dist/adapters/azure/mock.js +12 -8
- package/dist/adapters/azure/mock.js.map +1 -1
- package/dist/adapters/azure/staticHandler.d.ts +9 -9
- package/dist/adapters/azure/staticHandler.js +81 -77
- package/dist/adapters/azure/staticHandler.js.map +1 -1
- package/dist/adapters/azure/webpack.d.ts +2 -2
- package/dist/adapters/azure/webpack.js +24 -24
- package/dist/adapters/azure/webpack.js.map +1 -1
- package/dist/adapters/gcs/generateURL.d.ts +8 -8
- package/dist/adapters/gcs/generateURL.js +15 -15
- package/dist/adapters/gcs/handleDelete.d.ts +8 -8
- package/dist/adapters/gcs/handleDelete.js +62 -62
- package/dist/adapters/gcs/handleUpload.d.ts +12 -12
- package/dist/adapters/gcs/handleUpload.js +72 -72
- package/dist/adapters/gcs/index.d.ts +8 -8
- package/dist/adapters/gcs/index.js +35 -35
- package/dist/adapters/gcs/mock.d.ts +1 -1
- package/dist/adapters/gcs/mock.js +4 -4
- package/dist/adapters/gcs/staticHandler.d.ts +10 -10
- package/dist/adapters/gcs/staticHandler.js +76 -76
- package/dist/adapters/gcs/webpack.d.ts +2 -2
- package/dist/adapters/gcs/webpack.js +24 -24
- package/dist/adapters/s3/fileStub.d.ts +2 -2
- package/dist/adapters/s3/fileStub.js +3 -3
- package/dist/adapters/s3/generateURL.d.ts +8 -8
- package/dist/adapters/s3/generateURL.js +15 -15
- package/dist/adapters/s3/handleDelete.d.ts +8 -8
- package/dist/adapters/s3/handleDelete.js +63 -63
- package/dist/adapters/s3/handleUpload.d.ts +12 -12
- package/dist/adapters/s3/handleUpload.js +93 -93
- package/dist/adapters/s3/index.d.ts +8 -8
- package/dist/adapters/s3/index.js +59 -59
- package/dist/adapters/s3/mock.d.ts +8 -8
- package/dist/adapters/s3/mock.js +9 -9
- package/dist/adapters/s3/staticHandler.d.ts +10 -10
- package/dist/adapters/s3/staticHandler.js +80 -80
- package/dist/adapters/s3/webpack.d.ts +2 -2
- package/dist/adapters/s3/webpack.js +24 -24
- package/dist/fields/getFields.d.ts +11 -11
- package/dist/fields/getFields.js +118 -118
- package/dist/hooks/afterDelete.d.ts +10 -10
- package/dist/hooks/afterDelete.js +88 -88
- package/dist/hooks/afterRead.d.ts +12 -12
- package/dist/hooks/afterRead.js +79 -79
- package/dist/hooks/beforeChange.d.ts +10 -10
- package/dist/hooks/beforeChange.js +77 -77
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -5
- package/dist/plugin.d.ts +3 -3
- package/dist/plugin.js +124 -124
- package/dist/types.d.ts +62 -62
- package/dist/types.js +2 -2
- package/dist/utilities/getFilePrefix.d.ts +5 -5
- package/dist/utilities/getFilePrefix.js +80 -80
- package/dist/utilities/getIncomingFiles.d.ts +7 -7
- package/dist/utilities/getIncomingFiles.js +37 -37
- package/dist/utilities/getRangeFromHeader.d.ts +6 -0
- package/dist/utilities/getRangeFromHeader.js +67 -0
- package/dist/utilities/getRangeFromHeader.js.map +1 -0
- package/dist/webpack.d.ts +9 -9
- package/dist/webpack.js +39 -39
- package/docs/local-dev.md +47 -0
- package/eslint-config/index.js +15 -0
- package/eslint-config/rules/import.js +38 -0
- package/eslint-config/rules/prettier.js +7 -0
- package/eslint-config/rules/style.js +21 -0
- package/eslint-config/rules/typescript.js +628 -0
- package/gcs.d.ts +1 -1
- package/gcs.js +1 -1
- package/package.json +67 -64
- package/s3.d.ts +1 -1
- package/s3.js +1 -1
- package/src/adapters/azure/emulator/docker-compose.yml +16 -0
- package/src/adapters/azure/fileStub.js +1 -0
- package/src/adapters/azure/generateURL.ts +13 -0
- package/src/adapters/azure/handleDelete.ts +16 -0
- package/src/adapters/azure/handleUpload.ts +41 -0
- package/src/adapters/azure/index.ts +47 -0
- package/src/adapters/azure/mock.js +13 -0
- package/src/adapters/azure/staticHandler.ts +38 -0
- package/src/adapters/azure/webpack.ts +20 -0
- package/src/adapters/gcs/emulator/docker-compose.yml +15 -0
- package/src/adapters/gcs/generateURL.ts +16 -0
- package/src/adapters/gcs/handleDelete.ts +16 -0
- package/src/adapters/gcs/handleUpload.ts +34 -0
- package/src/adapters/gcs/index.ts +37 -0
- package/src/adapters/gcs/mock.js +3 -0
- package/src/adapters/gcs/staticHandler.ts +34 -0
- package/src/adapters/gcs/webpack.ts +17 -0
- package/src/adapters/s3/emulator/docker-compose.yml +15 -0
- package/src/adapters/s3/fileStub.js +1 -0
- package/src/adapters/s3/generateURL.ts +14 -0
- package/src/adapters/s3/handleDelete.ts +17 -0
- package/src/adapters/s3/handleUpload.ts +62 -0
- package/src/adapters/s3/index.ts +38 -0
- package/src/adapters/s3/mock.js +9 -0
- package/src/adapters/s3/staticHandler.ts +40 -0
- package/src/adapters/s3/webpack.ts +19 -0
- package/src/fields/getFields.ts +155 -0
- package/src/hooks/afterDelete.ts +35 -0
- package/src/hooks/afterRead.ts +38 -0
- package/src/hooks/beforeChange.ts +30 -0
- package/src/index.ts +1 -0
- package/src/plugin.ts +94 -0
- package/src/types.ts +73 -0
- package/src/utilities/getFilePrefix.ts +26 -0
- package/src/utilities/getIncomingFiles.ts +44 -0
- package/src/utilities/getRangeFromHeader.ts +27 -0
- package/src/webpack.ts +46 -0
- package/tsconfig.json +23 -0
- package/yarn.lock +8155 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as AWS from '@aws-sdk/client-s3'
|
|
2
|
+
import type { Adapter, GeneratedAdapter } from '../../types'
|
|
3
|
+
import { getGenerateURL } from './generateURL'
|
|
4
|
+
import { getHandler } from './staticHandler'
|
|
5
|
+
import { getHandleDelete } from './handleDelete'
|
|
6
|
+
import { getHandleUpload } from './handleUpload'
|
|
7
|
+
import { extendWebpackConfig } from './webpack'
|
|
8
|
+
|
|
9
|
+
export interface Args {
|
|
10
|
+
config: AWS.S3ClientConfig
|
|
11
|
+
bucket: string
|
|
12
|
+
acl?: 'private' | 'public-read'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const s3Adapter =
|
|
16
|
+
({ config, bucket, acl }: Args): Adapter =>
|
|
17
|
+
({ collection, prefix }): GeneratedAdapter => {
|
|
18
|
+
let storageClient: AWS.S3 | null = null
|
|
19
|
+
const getStorageClient: () => AWS.S3 = () => {
|
|
20
|
+
if (storageClient) return storageClient
|
|
21
|
+
storageClient = new AWS.S3(config)
|
|
22
|
+
return storageClient
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
handleUpload: getHandleUpload({
|
|
27
|
+
collection,
|
|
28
|
+
getStorageClient,
|
|
29
|
+
bucket,
|
|
30
|
+
acl,
|
|
31
|
+
prefix,
|
|
32
|
+
}),
|
|
33
|
+
handleDelete: getHandleDelete({ getStorageClient, bucket }),
|
|
34
|
+
generateURL: getGenerateURL({ bucket, config }),
|
|
35
|
+
staticHandler: getHandler({ bucket, getStorageClient, collection }),
|
|
36
|
+
webpack: extendWebpackConfig,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
exports.S3 = () => null
|
|
2
|
+
exports.Upload = () => null
|
|
3
|
+
|
|
4
|
+
exports.HeadObjectCommand = () => null
|
|
5
|
+
exports.PutObjectCommand = () => null
|
|
6
|
+
exports.UploadPartCommand = () => null
|
|
7
|
+
exports.CreateMultipartUploadCommand = () => null
|
|
8
|
+
exports.CompleteMultipartUploadCommand = () => null
|
|
9
|
+
exports.PutObjectTaggingCommand = () => null
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import type { Readable } from 'stream'
|
|
3
|
+
import type * as AWS from '@aws-sdk/client-s3'
|
|
4
|
+
import type { CollectionConfig } from 'payload/types'
|
|
5
|
+
import type { StaticHandler } from '../../types'
|
|
6
|
+
import { getFilePrefix } from '../../utilities/getFilePrefix'
|
|
7
|
+
|
|
8
|
+
interface Args {
|
|
9
|
+
getStorageClient: () => AWS.S3
|
|
10
|
+
bucket: string
|
|
11
|
+
collection: CollectionConfig
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const getHandler = ({ getStorageClient, bucket, collection }: Args): StaticHandler => {
|
|
15
|
+
return async (req, res, next) => {
|
|
16
|
+
try {
|
|
17
|
+
const prefix = await getFilePrefix({ req, collection })
|
|
18
|
+
|
|
19
|
+
const object = await getStorageClient().getObject({
|
|
20
|
+
Bucket: bucket,
|
|
21
|
+
Key: path.posix.join(prefix, req.params.filename),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
res.set({
|
|
25
|
+
'Content-Length': object.ContentLength,
|
|
26
|
+
'Content-Type': object.ContentType,
|
|
27
|
+
ETag: object.ETag,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
if (object?.Body) {
|
|
31
|
+
return (object.Body as Readable).pipe(res)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return next()
|
|
35
|
+
} catch (err: unknown) {
|
|
36
|
+
req.payload.logger.error(err)
|
|
37
|
+
return next()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Configuration as WebpackConfig } from 'webpack'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
export const extendWebpackConfig = (existingWebpackConfig: WebpackConfig): WebpackConfig => {
|
|
5
|
+
const newConfig: WebpackConfig = {
|
|
6
|
+
...existingWebpackConfig,
|
|
7
|
+
resolve: {
|
|
8
|
+
...(existingWebpackConfig.resolve || {}),
|
|
9
|
+
alias: {
|
|
10
|
+
...(existingWebpackConfig.resolve?.alias ? existingWebpackConfig.resolve.alias : {}),
|
|
11
|
+
'@aws-sdk/client-s3': path.resolve(__dirname, './mock.js'),
|
|
12
|
+
'@aws-sdk/lib-storage': path.resolve(__dirname, './mock.js'),
|
|
13
|
+
fs: path.resolve(__dirname, './fileStub.js'),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return newConfig
|
|
19
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import type { GroupField, TextField } from 'payload/dist/fields/config/types'
|
|
3
|
+
import type { CollectionConfig, Field } from 'payload/types'
|
|
4
|
+
import { getAfterReadHook } from '../hooks/afterRead'
|
|
5
|
+
import type { GeneratedAdapter, GenerateFileURL } from '../types'
|
|
6
|
+
|
|
7
|
+
interface Args {
|
|
8
|
+
collection: CollectionConfig
|
|
9
|
+
disablePayloadAccessControl?: true
|
|
10
|
+
generateFileURL?: GenerateFileURL
|
|
11
|
+
prefix?: string
|
|
12
|
+
adapter: GeneratedAdapter
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const getFields = ({
|
|
16
|
+
adapter,
|
|
17
|
+
collection,
|
|
18
|
+
disablePayloadAccessControl,
|
|
19
|
+
generateFileURL,
|
|
20
|
+
prefix,
|
|
21
|
+
}: Args): Field[] => {
|
|
22
|
+
const baseURLField: Field = {
|
|
23
|
+
name: 'url',
|
|
24
|
+
label: 'URL',
|
|
25
|
+
type: 'text',
|
|
26
|
+
admin: {
|
|
27
|
+
readOnly: true,
|
|
28
|
+
hidden: true,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const basePrefixField: Field = {
|
|
33
|
+
name: 'prefix',
|
|
34
|
+
type: 'text',
|
|
35
|
+
admin: {
|
|
36
|
+
readOnly: true,
|
|
37
|
+
hidden: true,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const fields = [...collection.fields]
|
|
42
|
+
|
|
43
|
+
// Inject a hook into all URL fields to generate URLs
|
|
44
|
+
|
|
45
|
+
let existingURLFieldIndex = -1
|
|
46
|
+
|
|
47
|
+
const existingURLField = fields.find((existingField, i) => {
|
|
48
|
+
if ('name' in existingField && existingField.name === 'url') {
|
|
49
|
+
existingURLFieldIndex = i
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}) as TextField
|
|
54
|
+
|
|
55
|
+
if (existingURLFieldIndex > -1) {
|
|
56
|
+
fields.splice(existingURLFieldIndex, 1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fields.push({
|
|
60
|
+
...baseURLField,
|
|
61
|
+
...(existingURLField || {}),
|
|
62
|
+
hooks: {
|
|
63
|
+
afterRead: [
|
|
64
|
+
getAfterReadHook({ adapter, collection, disablePayloadAccessControl, generateFileURL }),
|
|
65
|
+
...(existingURLField?.hooks?.afterRead || []),
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (typeof collection.upload === 'object' && collection.upload.imageSizes) {
|
|
71
|
+
let existingSizesFieldIndex = -1
|
|
72
|
+
|
|
73
|
+
const existingSizesField = fields.find((existingField, i) => {
|
|
74
|
+
if ('name' in existingField && existingField.name === 'sizes') {
|
|
75
|
+
existingSizesFieldIndex = i
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return false
|
|
80
|
+
}) as GroupField
|
|
81
|
+
|
|
82
|
+
if (existingSizesFieldIndex > -1) {
|
|
83
|
+
fields.splice(existingSizesFieldIndex, 1)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sizesField: Field = {
|
|
87
|
+
...(existingSizesField || {}),
|
|
88
|
+
name: 'sizes',
|
|
89
|
+
type: 'group',
|
|
90
|
+
admin: {
|
|
91
|
+
hidden: true,
|
|
92
|
+
},
|
|
93
|
+
fields: collection.upload.imageSizes.map(size => {
|
|
94
|
+
const existingSizeField = existingSizesField?.fields.find(
|
|
95
|
+
existingField => 'name' in existingField && existingField.name === size.name,
|
|
96
|
+
) as GroupField
|
|
97
|
+
|
|
98
|
+
const existingSizeURLField = existingSizeField?.fields.find(
|
|
99
|
+
existingField => 'name' in existingField && existingField.name === 'url',
|
|
100
|
+
) as GroupField
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
...existingSizeField,
|
|
104
|
+
name: size.name,
|
|
105
|
+
type: 'group',
|
|
106
|
+
fields: [
|
|
107
|
+
{
|
|
108
|
+
...(existingSizeURLField || {}),
|
|
109
|
+
...baseURLField,
|
|
110
|
+
hooks: {
|
|
111
|
+
afterRead: [
|
|
112
|
+
getAfterReadHook({
|
|
113
|
+
adapter,
|
|
114
|
+
collection,
|
|
115
|
+
size,
|
|
116
|
+
disablePayloadAccessControl,
|
|
117
|
+
generateFileURL,
|
|
118
|
+
}),
|
|
119
|
+
...(existingSizeURLField?.hooks?.afterRead || []),
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
}
|
|
125
|
+
}),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fields.push(sizesField)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// If prefix is enabled, save it to db
|
|
132
|
+
if (prefix) {
|
|
133
|
+
let existingPrefixFieldIndex = -1
|
|
134
|
+
|
|
135
|
+
const existingPrefixField = fields.find((existingField, i) => {
|
|
136
|
+
if ('name' in existingField && existingField.name === 'prefix') {
|
|
137
|
+
existingPrefixFieldIndex = i
|
|
138
|
+
return true
|
|
139
|
+
}
|
|
140
|
+
return false
|
|
141
|
+
}) as TextField
|
|
142
|
+
|
|
143
|
+
if (existingPrefixFieldIndex > -1) {
|
|
144
|
+
fields.splice(existingPrefixFieldIndex, 1)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fields.push({
|
|
148
|
+
...basePrefixField,
|
|
149
|
+
...(existingPrefixField || {}),
|
|
150
|
+
defaultValue: path.posix.join(prefix),
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return fields
|
|
155
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { TypeWithID } from 'payload/dist/globals/config/types'
|
|
2
|
+
import type { FileData } from 'payload/dist/uploads/types'
|
|
3
|
+
import type { CollectionAfterDeleteHook, CollectionConfig } from 'payload/types'
|
|
4
|
+
import type { GeneratedAdapter, TypeWithPrefix } from '../types'
|
|
5
|
+
|
|
6
|
+
interface Args {
|
|
7
|
+
collection: CollectionConfig
|
|
8
|
+
adapter: GeneratedAdapter
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const getAfterDeleteHook = ({
|
|
12
|
+
collection,
|
|
13
|
+
adapter,
|
|
14
|
+
}: Args): CollectionAfterDeleteHook<FileData & TypeWithID & TypeWithPrefix> => {
|
|
15
|
+
return async ({ req, doc }) => {
|
|
16
|
+
try {
|
|
17
|
+
const filesToDelete: string[] = [
|
|
18
|
+
doc.filename,
|
|
19
|
+
...Object.values(doc?.sizes || []).map(resizedFileData => resizedFileData?.filename),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const promises = filesToDelete.map(async filename => {
|
|
23
|
+
await adapter.handleDelete({ collection, doc, req, filename })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
await Promise.all(promises)
|
|
27
|
+
} catch (err: unknown) {
|
|
28
|
+
req.payload.logger.error(
|
|
29
|
+
`There was an error while deleting files corresponding to the ${collection.labels?.singular} with ID ${doc.id}:`,
|
|
30
|
+
)
|
|
31
|
+
req.payload.logger.error(err)
|
|
32
|
+
}
|
|
33
|
+
return doc
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ImageSize } from 'payload/dist/uploads/types'
|
|
2
|
+
import type { CollectionConfig, FieldHook } from 'payload/types'
|
|
3
|
+
import type { GeneratedAdapter, GenerateFileURL } from '../types'
|
|
4
|
+
|
|
5
|
+
interface Args {
|
|
6
|
+
collection: CollectionConfig
|
|
7
|
+
adapter: GeneratedAdapter
|
|
8
|
+
disablePayloadAccessControl?: boolean
|
|
9
|
+
size?: ImageSize
|
|
10
|
+
generateFileURL?: GenerateFileURL
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const getAfterReadHook =
|
|
14
|
+
({ collection, adapter, size, disablePayloadAccessControl, generateFileURL }: Args): FieldHook =>
|
|
15
|
+
async ({ data, value }) => {
|
|
16
|
+
const filename = size ? data?.sizes?.[size.name]?.filename : data?.filename
|
|
17
|
+
const prefix = data?.prefix
|
|
18
|
+
let url = value
|
|
19
|
+
|
|
20
|
+
if (disablePayloadAccessControl && filename) {
|
|
21
|
+
url = await adapter.generateURL({
|
|
22
|
+
collection,
|
|
23
|
+
filename,
|
|
24
|
+
prefix,
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (generateFileURL) {
|
|
29
|
+
url = await generateFileURL({
|
|
30
|
+
collection,
|
|
31
|
+
filename,
|
|
32
|
+
prefix,
|
|
33
|
+
size,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return url
|
|
38
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TypeWithID } from 'payload/dist/collections/config/types'
|
|
2
|
+
import type { FileData } from 'payload/dist/uploads/types'
|
|
3
|
+
import type { CollectionBeforeChangeHook, CollectionConfig } from 'payload/types'
|
|
4
|
+
import type { GeneratedAdapter } from '../types'
|
|
5
|
+
import { getIncomingFiles } from '../utilities/getIncomingFiles'
|
|
6
|
+
|
|
7
|
+
interface Args {
|
|
8
|
+
collection: CollectionConfig
|
|
9
|
+
adapter: GeneratedAdapter
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const getBeforeChangeHook =
|
|
13
|
+
({ collection, adapter }: Args): CollectionBeforeChangeHook<FileData & TypeWithID> =>
|
|
14
|
+
async ({ req, data }) => {
|
|
15
|
+
try {
|
|
16
|
+
const files = getIncomingFiles({ req, data })
|
|
17
|
+
|
|
18
|
+
const promises = files.map(async file => {
|
|
19
|
+
await adapter.handleUpload({ collection, data, req, file })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
await Promise.all(promises)
|
|
23
|
+
} catch (err: unknown) {
|
|
24
|
+
req.payload.logger.error(
|
|
25
|
+
`There was an error while uploading files corresponding to the collection ${collection.slug} with filename ${data.filename}:`,
|
|
26
|
+
)
|
|
27
|
+
req.payload.logger.error(err)
|
|
28
|
+
}
|
|
29
|
+
return data
|
|
30
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cloudStorage } from './plugin'
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Config } from 'payload/config'
|
|
2
|
+
import { extendWebpackConfig } from './webpack'
|
|
3
|
+
import type { PluginOptions } from './types'
|
|
4
|
+
import { getBeforeChangeHook } from './hooks/beforeChange'
|
|
5
|
+
import { getAfterDeleteHook } from './hooks/afterDelete'
|
|
6
|
+
import { getFields } from './fields/getFields'
|
|
7
|
+
|
|
8
|
+
// This plugin extends all targeted collections by offloading uploaded files
|
|
9
|
+
// to cloud storage instead of solely storing files locally.
|
|
10
|
+
|
|
11
|
+
// It is based on an adapter approach, where adapters can be written for any cloud provider.
|
|
12
|
+
// Adapters are responsible for providing four actions that this plugin will use:
|
|
13
|
+
// 1. handleUpload, 2. handleDelete, 3. generateURL, 4. staticHandler
|
|
14
|
+
|
|
15
|
+
// Optionally, the adapter can specify any Webpack config overrides if they are necessary.
|
|
16
|
+
|
|
17
|
+
export const cloudStorage =
|
|
18
|
+
(pluginOptions: PluginOptions) =>
|
|
19
|
+
(config: Config): Config => {
|
|
20
|
+
const { collections: allCollectionOptions } = pluginOptions
|
|
21
|
+
|
|
22
|
+
const webpack = extendWebpackConfig({ options: pluginOptions, config })
|
|
23
|
+
|
|
24
|
+
const initFunctions: (() => void)[] = []
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...config,
|
|
28
|
+
admin: {
|
|
29
|
+
...(config.admin || {}),
|
|
30
|
+
webpack,
|
|
31
|
+
},
|
|
32
|
+
collections: (config.collections || []).map(existingCollection => {
|
|
33
|
+
const options = allCollectionOptions[existingCollection.slug]
|
|
34
|
+
|
|
35
|
+
if (options?.adapter) {
|
|
36
|
+
const adapter = options.adapter({
|
|
37
|
+
collection: existingCollection,
|
|
38
|
+
prefix: options.prefix,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if (adapter.onInit) initFunctions.push(adapter.onInit)
|
|
42
|
+
|
|
43
|
+
const fields = getFields({
|
|
44
|
+
collection: existingCollection,
|
|
45
|
+
disablePayloadAccessControl: options.disablePayloadAccessControl,
|
|
46
|
+
generateFileURL: options.generateFileURL,
|
|
47
|
+
prefix: options.prefix,
|
|
48
|
+
adapter,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const handlers = [
|
|
52
|
+
...(typeof existingCollection.upload === 'object' &&
|
|
53
|
+
Array.isArray(existingCollection.upload.handlers)
|
|
54
|
+
? existingCollection.upload.handlers
|
|
55
|
+
: []),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
if (!options.disablePayloadAccessControl) {
|
|
59
|
+
handlers.push(adapter.staticHandler)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
...existingCollection,
|
|
64
|
+
upload: {
|
|
65
|
+
...(typeof existingCollection.upload === 'object' ? existingCollection.upload : {}),
|
|
66
|
+
handlers,
|
|
67
|
+
disableLocalStorage:
|
|
68
|
+
typeof options.disableLocalStorage === 'boolean'
|
|
69
|
+
? options.disableLocalStorage
|
|
70
|
+
: true,
|
|
71
|
+
},
|
|
72
|
+
hooks: {
|
|
73
|
+
...(existingCollection.hooks || {}),
|
|
74
|
+
beforeChange: [
|
|
75
|
+
...(existingCollection.hooks?.beforeChange || []),
|
|
76
|
+
getBeforeChangeHook({ adapter, collection: existingCollection }),
|
|
77
|
+
],
|
|
78
|
+
afterDelete: [
|
|
79
|
+
...(existingCollection.hooks?.afterDelete || []),
|
|
80
|
+
getAfterDeleteHook({ adapter, collection: existingCollection }),
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
fields,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return existingCollection
|
|
88
|
+
}),
|
|
89
|
+
onInit: async payload => {
|
|
90
|
+
initFunctions.forEach(fn => fn())
|
|
91
|
+
if (config.onInit) await config.onInit(payload)
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { NextFunction, Response } from 'express'
|
|
2
|
+
import type { TypeWithID } from 'payload/dist/collections/config/types'
|
|
3
|
+
import type { FileData, ImageSize } from 'payload/dist/uploads/types'
|
|
4
|
+
import type { CollectionConfig, PayloadRequest } from 'payload/types'
|
|
5
|
+
import type { Configuration as WebpackConfig } from 'webpack'
|
|
6
|
+
|
|
7
|
+
export interface File {
|
|
8
|
+
buffer: Buffer
|
|
9
|
+
filename: string
|
|
10
|
+
filesize: number
|
|
11
|
+
mimeType: string
|
|
12
|
+
tempFilePath?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type HandleUpload = (args: {
|
|
16
|
+
collection: CollectionConfig
|
|
17
|
+
req: PayloadRequest
|
|
18
|
+
data: any
|
|
19
|
+
file: File
|
|
20
|
+
}) => Promise<void> | void
|
|
21
|
+
|
|
22
|
+
export interface TypeWithPrefix {
|
|
23
|
+
prefix?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type HandleDelete = (args: {
|
|
27
|
+
collection: CollectionConfig
|
|
28
|
+
req: PayloadRequest
|
|
29
|
+
doc: TypeWithID & FileData & TypeWithPrefix
|
|
30
|
+
filename: string
|
|
31
|
+
}) => Promise<void> | void
|
|
32
|
+
|
|
33
|
+
export type GenerateURL = (args: {
|
|
34
|
+
filename: string
|
|
35
|
+
collection: CollectionConfig
|
|
36
|
+
prefix?: string
|
|
37
|
+
}) => string | Promise<string>
|
|
38
|
+
|
|
39
|
+
export type StaticHandler = (
|
|
40
|
+
req: PayloadRequest,
|
|
41
|
+
res: Response,
|
|
42
|
+
next: NextFunction,
|
|
43
|
+
) => Promise<unknown> | unknown
|
|
44
|
+
|
|
45
|
+
export interface GeneratedAdapter {
|
|
46
|
+
handleUpload: HandleUpload
|
|
47
|
+
handleDelete: HandleDelete
|
|
48
|
+
generateURL: GenerateURL
|
|
49
|
+
staticHandler: StaticHandler
|
|
50
|
+
webpack?: (config: WebpackConfig) => WebpackConfig
|
|
51
|
+
onInit?: () => void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type Adapter = (args: { collection: CollectionConfig; prefix?: string }) => GeneratedAdapter
|
|
55
|
+
|
|
56
|
+
export type GenerateFileURL = (args: {
|
|
57
|
+
collection: CollectionConfig
|
|
58
|
+
filename: string
|
|
59
|
+
prefix?: string
|
|
60
|
+
size?: ImageSize
|
|
61
|
+
}) => Promise<string> | string
|
|
62
|
+
|
|
63
|
+
export interface CollectionOptions {
|
|
64
|
+
disableLocalStorage?: boolean
|
|
65
|
+
disablePayloadAccessControl?: true
|
|
66
|
+
generateFileURL?: GenerateFileURL
|
|
67
|
+
prefix?: string
|
|
68
|
+
adapter: Adapter | null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PluginOptions {
|
|
72
|
+
collections: Record<string, CollectionOptions>
|
|
73
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CollectionConfig, PayloadRequest } from 'payload/types'
|
|
2
|
+
import { IncomingUploadType } from 'payload/dist/uploads/types'
|
|
3
|
+
|
|
4
|
+
export async function getFilePrefix({
|
|
5
|
+
req,
|
|
6
|
+
collection,
|
|
7
|
+
}: {
|
|
8
|
+
req: PayloadRequest
|
|
9
|
+
collection: CollectionConfig
|
|
10
|
+
}): Promise<string> {
|
|
11
|
+
const imageSizes = (collection?.upload as IncomingUploadType)?.imageSizes || []
|
|
12
|
+
const files = await req.payload.find({
|
|
13
|
+
collection: collection.slug,
|
|
14
|
+
where: {
|
|
15
|
+
or: [
|
|
16
|
+
{
|
|
17
|
+
filename: { equals: req.params.filename },
|
|
18
|
+
},
|
|
19
|
+
...imageSizes.map(imageSize => ({
|
|
20
|
+
[`sizes.${imageSize.name}.filename`]: { equals: req.params.filename },
|
|
21
|
+
})),
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
return files?.docs?.[0]?.prefix || ''
|
|
26
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { FileData } from 'payload/dist/uploads/types'
|
|
2
|
+
import type { PayloadRequest } from 'payload/types'
|
|
3
|
+
import type { File } from '../types'
|
|
4
|
+
|
|
5
|
+
export function getIncomingFiles({
|
|
6
|
+
req,
|
|
7
|
+
data,
|
|
8
|
+
}: {
|
|
9
|
+
data: Partial<FileData>
|
|
10
|
+
req: PayloadRequest
|
|
11
|
+
}): File[] {
|
|
12
|
+
const file = req.files?.file
|
|
13
|
+
|
|
14
|
+
let files: File[] = []
|
|
15
|
+
|
|
16
|
+
if (file && data.filename && data.mimeType) {
|
|
17
|
+
const mainFile: File = {
|
|
18
|
+
filename: data.filename,
|
|
19
|
+
mimeType: data.mimeType,
|
|
20
|
+
buffer: file.data,
|
|
21
|
+
tempFilePath: file.tempFilePath,
|
|
22
|
+
filesize: file.size,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
files = [mainFile]
|
|
26
|
+
|
|
27
|
+
if (data?.sizes) {
|
|
28
|
+
Object.entries(data.sizes).forEach(([key, resizedFileData]) => {
|
|
29
|
+
if (req.payloadUploadSizes?.[key] && data.mimeType) {
|
|
30
|
+
files = files.concat([
|
|
31
|
+
{
|
|
32
|
+
filename: `${resizedFileData.filename}`,
|
|
33
|
+
mimeType: data.mimeType,
|
|
34
|
+
buffer: req.payloadUploadSizes[key],
|
|
35
|
+
filesize: req.payloadUploadSizes[key].length,
|
|
36
|
+
},
|
|
37
|
+
])
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return files
|
|
44
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BlockBlobClient } from '@azure/storage-blob'
|
|
2
|
+
import parseRange from 'range-parser'
|
|
3
|
+
|
|
4
|
+
const getRangeFromHeader = async (
|
|
5
|
+
blockBlobClient: BlockBlobClient,
|
|
6
|
+
rangeHeader?: string,
|
|
7
|
+
): Promise<{ start: number; end: number | undefined }> => {
|
|
8
|
+
const fullRange = { start: 0, end: undefined }
|
|
9
|
+
|
|
10
|
+
if (!rangeHeader) {
|
|
11
|
+
return fullRange
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const size = await blockBlobClient.getProperties().then(props => props.contentLength)
|
|
15
|
+
if (size === undefined) {
|
|
16
|
+
return fullRange
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const range = parseRange(size, rangeHeader)
|
|
20
|
+
if (range === -1 || range === -2 || range.type !== 'bytes' || range.length !== 1) {
|
|
21
|
+
return fullRange
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return range[0]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default getRangeFromHeader
|
package/src/webpack.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Config } from 'payload/config'
|
|
2
|
+
import type { Configuration as WebpackConfig } from 'webpack'
|
|
3
|
+
import type { GeneratedAdapter, PluginOptions } from './types'
|
|
4
|
+
|
|
5
|
+
interface Args {
|
|
6
|
+
config: Config
|
|
7
|
+
options: PluginOptions
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const extendWebpackConfig =
|
|
11
|
+
({ config, options }: Args): ((webpackConfig: WebpackConfig) => WebpackConfig) =>
|
|
12
|
+
webpackConfig => {
|
|
13
|
+
const existingWebpackConfig =
|
|
14
|
+
typeof config.admin?.webpack === 'function'
|
|
15
|
+
? config.admin.webpack(webpackConfig)
|
|
16
|
+
: webpackConfig
|
|
17
|
+
|
|
18
|
+
const newConfig: WebpackConfig = {
|
|
19
|
+
...existingWebpackConfig,
|
|
20
|
+
resolve: {
|
|
21
|
+
...(existingWebpackConfig.resolve || {}),
|
|
22
|
+
alias: {
|
|
23
|
+
...(existingWebpackConfig.resolve?.alias ? existingWebpackConfig.resolve.alias : {}),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return Object.entries(options.collections).reduce(
|
|
29
|
+
(resultingWebpackConfig, [slug, collectionOptions]) => {
|
|
30
|
+
const matchedCollection = config.collections?.find(coll => coll.slug === slug)
|
|
31
|
+
|
|
32
|
+
if (matchedCollection && typeof collectionOptions.adapter === 'function') {
|
|
33
|
+
const adapter: GeneratedAdapter = collectionOptions.adapter({
|
|
34
|
+
collection: matchedCollection,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
if (adapter.webpack) {
|
|
38
|
+
return adapter.webpack(resultingWebpackConfig)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return resultingWebpackConfig
|
|
43
|
+
},
|
|
44
|
+
newConfig,
|
|
45
|
+
)
|
|
46
|
+
}
|