Spaces:
Sleeping
Sleeping
| import { | |
| DeleteObjectCommand, | |
| GetObjectCommand, | |
| PutObjectCommand, | |
| S3Client, | |
| } from '@aws-sdk/client-s3'; | |
| import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; | |
| import * as fs from 'fs'; | |
| import * as path from 'path'; | |
| import { pipeline } from 'stream/promises'; | |
| import { Readable } from 'stream'; | |
| import { config } from '../config'; | |
| const R2_PREFIX = 'r2://'; | |
| let client: S3Client | null = null; | |
| export interface R2ObjectRef { | |
| bucket: string; | |
| key: string; | |
| } | |
| function getR2Client(): S3Client { | |
| if (!client) { | |
| client = new S3Client({ | |
| region: config.r2Region, | |
| endpoint: config.r2Endpoint, | |
| forcePathStyle: true, | |
| credentials: { | |
| accessKeyId: config.r2AccessKeyId, | |
| secretAccessKey: config.r2SecretAccessKey, | |
| }, | |
| }); | |
| } | |
| return client; | |
| } | |
| export function isR2ObjectRef(value: string): boolean { | |
| return value.startsWith(R2_PREFIX); | |
| } | |
| export function createR2ObjectRef(bucket: string, key: string): string { | |
| return `${R2_PREFIX}${bucket}/${key.replace(/^\/+/, '')}`; | |
| } | |
| export function parseR2ObjectRef(value: string): R2ObjectRef { | |
| if (!isR2ObjectRef(value)) { | |
| throw new Error('Not an R2 object reference'); | |
| } | |
| const withoutScheme = value.slice(R2_PREFIX.length); | |
| const slashIndex = withoutScheme.indexOf('/'); | |
| if (slashIndex <= 0 || slashIndex === withoutScheme.length - 1) { | |
| throw new Error('Invalid R2 object reference'); | |
| } | |
| return { | |
| bucket: withoutScheme.slice(0, slashIndex), | |
| key: withoutScheme.slice(slashIndex + 1), | |
| }; | |
| } | |
| export async function putR2Buffer( | |
| bucket: string, | |
| key: string, | |
| body: Buffer, | |
| contentType: string, | |
| ): Promise<string> { | |
| await getR2Client().send( | |
| new PutObjectCommand({ | |
| Bucket: bucket, | |
| Key: key, | |
| Body: body, | |
| ContentLength: body.length, | |
| ContentType: contentType, | |
| }), | |
| ); | |
| return createR2ObjectRef(bucket, key); | |
| } | |
| export async function putR2File( | |
| bucket: string, | |
| key: string, | |
| localPath: string, | |
| contentType: string, | |
| ): Promise<string> { | |
| const stat = await fs.promises.stat(localPath); | |
| await getR2Client().send( | |
| new PutObjectCommand({ | |
| Bucket: bucket, | |
| Key: key, | |
| Body: fs.createReadStream(localPath), | |
| ContentLength: stat.size, | |
| ContentType: contentType, | |
| }), | |
| ); | |
| return createR2ObjectRef(bucket, key); | |
| } | |
| export async function downloadR2File( | |
| objectRef: string, | |
| localPath: string, | |
| ): Promise<void> { | |
| const { bucket, key } = parseR2ObjectRef(objectRef); | |
| const response = await getR2Client().send( | |
| new GetObjectCommand({ Bucket: bucket, Key: key }), | |
| ); | |
| if (!response.Body) throw new Error(`R2 object body is empty: ${objectRef}`); | |
| await fs.promises.mkdir(path.dirname(localPath), { recursive: true }); | |
| await pipeline(response.Body as Readable, fs.createWriteStream(localPath)); | |
| } | |
| export async function readR2Text(objectRef: string): Promise<string | null> { | |
| const { bucket, key } = parseR2ObjectRef(objectRef); | |
| try { | |
| const response = await getR2Client().send( | |
| new GetObjectCommand({ Bucket: bucket, Key: key }), | |
| ); | |
| if (!response.Body) return null; | |
| return await response.Body.transformToString('utf-8'); | |
| } catch (error: unknown) { | |
| const name = error instanceof Error ? error.name : ''; | |
| const status = (error as { $metadata?: { httpStatusCode?: number } })?.$metadata | |
| ?.httpStatusCode; | |
| if (name === 'NoSuchKey' || name === 'NotFound' || status === 404) return null; | |
| throw error; | |
| } | |
| } | |
| export async function deleteR2Object(objectRef: string): Promise<void> { | |
| const { bucket, key } = parseR2ObjectRef(objectRef); | |
| await getR2Client().send( | |
| new DeleteObjectCommand({ Bucket: bucket, Key: key }), | |
| ); | |
| } | |
| export async function createR2SignedDownloadUrl( | |
| objectRef: string, | |
| expiresInSeconds: number, | |
| downloadFileName?: string, | |
| ): Promise<string> { | |
| const { bucket, key } = parseR2ObjectRef(objectRef); | |
| const disposition = downloadFileName | |
| ? `attachment; filename*=UTF-8''${encodeURIComponent(downloadFileName)}` | |
| : undefined; | |
| return getSignedUrl( | |
| getR2Client(), | |
| new GetObjectCommand({ | |
| Bucket: bucket, | |
| Key: key, | |
| ResponseContentDisposition: disposition, | |
| }), | |
| { expiresIn: expiresInSeconds }, | |
| ); | |
| } | |