mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2026-03-01 12:44:37 +00:00
86 lines
2.7 KiB
TypeScript
86 lines
2.7 KiB
TypeScript
import { S3Client, DeleteObjectCommand, ObjectCannedACL } from '@aws-sdk/client-s3'
|
|
import { Upload } from '@aws-sdk/lib-storage'
|
|
|
|
import type { S3Config } from '@config/index'
|
|
|
|
import { FileUploadCallback, FileDeleteCallback } from './types'
|
|
|
|
let cachedClient: S3Client | null = null
|
|
let cachedConfig: S3Config | null = null
|
|
|
|
const getS3Client = (config: S3Config): S3Client => {
|
|
if (cachedClient) {
|
|
if (
|
|
cachedConfig?.AWS_ENDPOINT !== config.AWS_ENDPOINT ||
|
|
cachedConfig?.AWS_ACCESS_KEY_ID !== config.AWS_ACCESS_KEY_ID ||
|
|
cachedConfig?.AWS_SECRET_ACCESS_KEY !== config.AWS_SECRET_ACCESS_KEY
|
|
) {
|
|
throw new Error('S3Client singleton was created with different credentials')
|
|
}
|
|
return cachedClient
|
|
}
|
|
const { AWS_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = config
|
|
cachedClient = new S3Client({
|
|
credentials: {
|
|
accessKeyId: AWS_ACCESS_KEY_ID,
|
|
secretAccessKey: AWS_SECRET_ACCESS_KEY,
|
|
},
|
|
endpoint: AWS_ENDPOINT,
|
|
forcePathStyle: true,
|
|
})
|
|
cachedConfig = config
|
|
return cachedClient
|
|
}
|
|
|
|
export const s3Service = (config: S3Config, prefix: string) => {
|
|
const { AWS_BUCKET: Bucket } = config
|
|
const s3 = getS3Client(config)
|
|
|
|
const uploadFile: FileUploadCallback = async ({ createReadStream, uniqueFilename, mimetype }) => {
|
|
const s3Location = prefix.length > 0 ? `${prefix}/${uniqueFilename}` : uniqueFilename
|
|
|
|
const params = {
|
|
Bucket,
|
|
Key: s3Location,
|
|
ACL: ObjectCannedACL.public_read,
|
|
CacheControl: 'public, max-age=604800',
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
ContentType: mimetype,
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
|
|
Body: createReadStream(),
|
|
}
|
|
const command = new Upload({ client: s3, params })
|
|
const data = await command.done()
|
|
let { Location: location } = data
|
|
if (!location) {
|
|
throw new Error('File upload did not return `Location`')
|
|
}
|
|
|
|
if (!location.startsWith('https://') && !location.startsWith('http://')) {
|
|
// Ensure the location has a protocol. Hetzner sometimes does not return a protocol in the location.
|
|
location = `https://${location}`
|
|
}
|
|
|
|
return location
|
|
}
|
|
|
|
const deleteFile: FileDeleteCallback = async (url) => {
|
|
let { pathname } = new URL(url, 'http://example.org') // dummy domain to avoid invalid URL error
|
|
pathname = pathname.substring(1) // remove first character '/'
|
|
const prefix = `${Bucket}/`
|
|
if (pathname.startsWith(prefix)) {
|
|
pathname = pathname.slice(prefix.length)
|
|
}
|
|
const params = {
|
|
Bucket,
|
|
Key: pathname,
|
|
}
|
|
await s3.send(new DeleteObjectCommand(params))
|
|
}
|
|
|
|
return {
|
|
uploadFile,
|
|
deleteFile,
|
|
}
|
|
}
|