Spaces:
Sleeping
Sleeping
File size: 4,249 Bytes
ba37a9b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | 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 },
);
}
|