import { S3Client, ListObjectsCommand, PutObjectCommand, DeleteObjectCommand, CopyObjectCommand, GetObjectCommand, } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { StorageListResponse, generateStorageId } from '.'; import { removeUrlProtocol } from '@/utility/url'; import { formatBytes } from '@/utility/number'; const CLOUDFLARE_R2_BUCKET = process.env.NEXT_PUBLIC_CLOUDFLARE_R2_BUCKET ?? ''; const CLOUDFLARE_R2_ACCOUNT_ID = process.env.NEXT_PUBLIC_CLOUDFLARE_R2_ACCOUNT_ID ?? ''; const CLOUDFLARE_R2_PUBLIC_DOMAIN = removeUrlProtocol(process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_DOMAIN) ?? ''; const CLOUDFLARE_R2_ACCESS_KEY = process.env.CLOUDFLARE_R2_ACCESS_KEY ?? ''; const CLOUDFLARE_R2_SECRET_ACCESS_KEY = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY ?? ''; const CLOUDFLARE_R2_ENDPOINT = CLOUDFLARE_R2_ACCOUNT_ID ? `https://${CLOUDFLARE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com` : undefined; export const CLOUDFLARE_R2_BASE_URL_PUBLIC = CLOUDFLARE_R2_PUBLIC_DOMAIN ? `https://${CLOUDFLARE_R2_PUBLIC_DOMAIN}` : undefined; export const CLOUDFLARE_R2_BASE_URL_PRIVATE = CLOUDFLARE_R2_ENDPOINT && CLOUDFLARE_R2_BUCKET ? `${CLOUDFLARE_R2_ENDPOINT}/${CLOUDFLARE_R2_BUCKET}` : undefined; export const cloudflareR2Client = () => new S3Client({ region: 'auto', endpoint: CLOUDFLARE_R2_ENDPOINT, credentials: { accessKeyId: CLOUDFLARE_R2_ACCESS_KEY, secretAccessKey: CLOUDFLARE_R2_SECRET_ACCESS_KEY, }, }); const urlForKey = (key?: string, isPublic = true) => isPublic ? `${CLOUDFLARE_R2_BASE_URL_PUBLIC}/${key}` : `${CLOUDFLARE_R2_BASE_URL_PRIVATE}/${key}`; export const isUrlFromCloudflareR2 = (url?: string) => ( CLOUDFLARE_R2_BASE_URL_PRIVATE && url?.startsWith(CLOUDFLARE_R2_BASE_URL_PRIVATE) ) || ( CLOUDFLARE_R2_BASE_URL_PUBLIC && url?.startsWith(CLOUDFLARE_R2_BASE_URL_PUBLIC) ); export const cloudflareR2Put = async ( file: Buffer, fileName: string, ): Promise => cloudflareR2Client().send(new PutObjectCommand({ Bucket: CLOUDFLARE_R2_BUCKET, Key: fileName, Body: file, })) .then(() => urlForKey(fileName)); export const cloudflareR2Copy = async ( fileNameSource: string, fileNameDestination: string, addRandomSuffix?: boolean, ) => { const name = fileNameSource.split('.')[0]; const extension = fileNameSource.split('.')[1]; const Key = addRandomSuffix ? `${name}-${generateStorageId()}.${extension}` : fileNameDestination; return cloudflareR2Client().send(new CopyObjectCommand({ Bucket: CLOUDFLARE_R2_BUCKET, CopySource: `${CLOUDFLARE_R2_BUCKET}/${fileNameSource}`, Key, })) .then(() => urlForKey(fileNameDestination)); }; export const cloudflareR2List = async ( Prefix: string, ): Promise => cloudflareR2Client().send(new ListObjectsCommand({ Bucket: CLOUDFLARE_R2_BUCKET, Prefix, })) .then((data) => data.Contents?.map(({ Key, LastModified, Size }) => ({ url: urlForKey(Key), fileName: Key ?? '', uploadedAt: LastModified, size: Size ? formatBytes(Size) : undefined, })) ?? []); export const cloudflareR2Delete = async (Key: string) => { cloudflareR2Client().send(new DeleteObjectCommand({ Bucket: CLOUDFLARE_R2_BUCKET, Key, })); }; export const cloudflareR2GetSignedUrl = ( Key: string, method: 'GET' | 'PUT', expiresIn: number, ) => { const client = cloudflareR2Client(); const command = method === 'GET' ? new GetObjectCommand({ Bucket: CLOUDFLARE_R2_BUCKET, Key }) // eslint-disable-next-line max-len : new PutObjectCommand({ Bucket: CLOUDFLARE_R2_BUCKET, Key, ACL: 'public-read' }); return getSignedUrl(client, command, { expiresIn }); };