All files / src/storage r2-object-store.ts

77.27% Statements 34/44
57.14% Branches 8/14
83.33% Functions 5/6
77.27% Lines 34/44

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 591x                 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 }));
  }
}