Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 | 1x 2x 2x 2x 2x 1x 1x 3x 3x 3x 1x 2x 2x 2x 2x 1x 3x 3x 3x 1x 1x 1x 3x 3x 3x 2x 2x 2x 2x 3x 1x 2x 2x 1x | import {
DeleteObjectCommand,
GetObjectCommand,
PutObjectCommand,
type S3Client,
} from "@aws-sdk/client-s3";
import { AppError } from "@ontrack/backend-common";
import type { ObjectStore, StoredObject } from "./object-store.js";
function errorName(err: unknown): string {
return err && typeof err === "object" && "name" in err
? String((err as { name: unknown }).name)
: "";
}
function httpStatus(err: unknown): number | undefined {
return (err as { $metadata?: { httpStatusCode?: number } } | undefined)?.$metadata?.httpStatusCode;
}
/** Произвольные объекты (аватары) в S3-совместимом бакете (Cloudflare R2). */
export class R2ObjectStore implements ObjectStore {
constructor(
private readonly client: S3Client,
private readonly bucket: string,
) {}
async put(key: string, body: Buffer, contentType: string): Promise<void> {
await this.client.send(
new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: body, ContentType: contentType }),
);
}
async get(key: string): Promise<StoredObject> {
try {
const res = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: key }));
if (!res.Body) {
throw new AppError(404, "OBJECT_NOT_FOUND", "Object has no body");
}
const bytes = await res.Body.transformToByteArray();
return {
body: Buffer.from(bytes),
contentType: res.ContentType ?? "application/octet-stream",
};
} catch (err) {
if (err instanceof AppError) {
throw err;
}
if (errorName(err) === "NoSuchKey" || httpStatus(err) === 404) {
throw new AppError(404, "OBJECT_NOT_FOUND", "Object not found in storage");
}
throw err;
}
}
async delete(key: string): Promise<void> {
await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key }));
}
}
|