This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import AWS from 'aws-sdk' | |
import { UploadedFile } from 'express-fileupload' | |
import { | |
BeforeOperationHook, | |
BeforeValidateHook, | |
BeforeChangeHook, | |
AfterChangeHook, | |
BeforeReadHook, | |
AfterReadHook, | |
BeforeDeleteHook, | |
AfterDeleteHook, | |
AfterErrorHook, | |
BeforeLoginHook, | |
AfterLoginHook, | |
AfterForgotPasswordHook, | |
} from 'payload/dist/collections/config/types' | |
import { APIError } from 'payload/errors' | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
function isUploadedFile(object: any): object is UploadedFile { | |
return 'data' in object; | |
} | |
let instance: AWS.S3 = null | |
const getCurrentS3Instance = (): AWS.S3 => { | |
if (instance === null) { | |
throw new APIError("S3 has not been initialized. Ensure you're calling `init()` with your S3 credentials before using these hooks.") | |
} | |
return instance | |
} | |
const options: FileOptions = { | |
bucket: null, | |
acl: 'private' | |
} | |
const getBucketName = (): string => { | |
if (options.bucket === null) { | |
throw new APIError("Bucket name has not been initialized. Ensure you're calling `init()` with your S3 credentials and file options before using these hooks.") | |
} | |
return options.bucket | |
} | |
export const uploadToS3: AfterChangeHook = async ({ doc, req }) => { | |
let uploadedFile: UploadedFile; | |
if (isUploadedFile(req.files.file)) { | |
uploadedFile = req.files.file | |
} else { | |
uploadedFile = req.files.file[0] | |
} | |
const s3 = getCurrentS3Instance() | |
const bucket = getBucketName() | |
await s3.putObject({ | |
Bucket: bucket, | |
Key: String(doc.filename), | |
Body: uploadedFile.data, | |
ACL: 'public-read' | |
}).promise() | |
return doc | |
} | |
export const getS3Url: AfterReadHook = async ({ doc }) => { | |
return { | |
...doc, | |
filename: `https://${process.env.SPACES_NAME}.${process.env.SPACES_REGION}.cdn.digitaloceanspaces.com/${doc.filename}` | |
} | |
} | |
export const deleteFromS3: AfterDeleteHook = async ({ doc }) => { | |
const s3 = getCurrentS3Instance() | |
await s3.deleteObject({ | |
Bucket: process.env.SPACES_NAME, | |
Key: String(doc.filename), | |
}).promise() | |
} | |
type PayloadCollectionHooks = { | |
beforeOperation?: BeforeOperationHook[]; | |
beforeValidate?: BeforeValidateHook[]; | |
beforeChange?: BeforeChangeHook[]; | |
afterChange?: AfterChangeHook[]; | |
beforeRead?: BeforeReadHook[]; | |
afterRead?: AfterReadHook[]; | |
beforeDelete?: BeforeDeleteHook[]; | |
afterDelete?: AfterDeleteHook[]; | |
afterError?: AfterErrorHook; | |
beforeLogin?: BeforeLoginHook[]; | |
afterLogin?: AfterLoginHook[]; | |
afterForgotPassword?: AfterForgotPasswordHook[]; | |
} | |
type FileOptions = { | |
bucket: string; | |
acl?: 'private' | 'public-read'; | |
} | |
export function init(s3Configuration: AWS.S3.ClientConfiguration, fileOptions: FileOptions): void { | |
instance = new AWS.S3(s3Configuration) | |
options.bucket = fileOptions.bucket | |
if (fileOptions.acl) { | |
options.acl = fileOptions.acl | |
} | |
} | |
export function withS3Storage( | |
s3Configuration: AWS.S3.ClientConfiguration, | |
fileOptions: FileOptions, | |
hooks?: PayloadCollectionHooks, | |
): PayloadCollectionHooks { | |
init(s3Configuration, fileOptions) | |
const { | |
afterChange = [], | |
afterRead = [], | |
afterDelete = [], | |
...rest | |
} = hooks || {} | |
return { | |
afterChange: [ | |
uploadToS3, | |
...afterChange | |
], | |
afterRead: [ | |
getS3Url, | |
...afterRead | |
], | |
afterDelete: [ | |
deleteFromS3, | |
...afterDelete | |
], | |
...rest | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment