aegida-console / lib / storage / s3.ts
s3.ts
Raw
import "server-only";

import {
  CreateBucketCommand,
  DeleteObjectCommand,
  GetObjectCommand,
  HeadBucketCommand,
  PutObjectCommand,
  S3Client,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

let client: S3Client | undefined;

export function getObjectStore() {
  client ??= new S3Client({
    endpoint: requiredEnv("S3_ENDPOINT"),
    region: process.env.S3_REGION ?? "us-east-1",
    forcePathStyle: process.env.S3_FORCE_PATH_STYLE === "true",
    credentials: {
      accessKeyId: requiredEnv("S3_ACCESS_KEY_ID"),
      secretAccessKey: requiredEnv("S3_SECRET_ACCESS_KEY"),
    },
  });
  return client;
}

export async function ensureAttachmentBucket() {
  const store = getObjectStore();
  const Bucket = requiredEnv("S3_BUCKET");
  try {
    await store.send(new HeadBucketCommand({ Bucket }));
  } catch {
    await store.send(new CreateBucketCommand({ Bucket }));
  }
}

export async function putAttachmentObject(input: {
  key: string;
  body: Uint8Array | ArrayBuffer;
  contentType: string;
}) {
  await getObjectStore().send(
    new PutObjectCommand({
      Bucket: requiredEnv("S3_BUCKET"),
      Key: input.key,
      Body: input.body instanceof ArrayBuffer ? new Uint8Array(input.body) : input.body,
      ContentType: input.contentType,
    }),
  );
}

export async function deleteAttachmentObject(key: string) {
  await getObjectStore().send(
    new DeleteObjectCommand({ Bucket: requiredEnv("S3_BUCKET"), Key: key }),
  );
}

export function getAttachmentDownloadUrl(key: string) {
  return getSignedUrl(
    getObjectStore(),
    new GetObjectCommand({ Bucket: requiredEnv("S3_BUCKET"), Key: key }),
    { expiresIn: 600 },
  );
}

export async function getAttachmentObject(key: string): Promise<ReadableStream<Uint8Array>> {
  const result = await getObjectStore().send(
    new GetObjectCommand({ Bucket: requiredEnv("S3_BUCKET"), Key: key }),
  );

  if (!result.Body) {
    throw new Error("Attachment object has no body");
  }

  return result.Body.transformToWebStream() as ReadableStream<Uint8Array>;
}

function requiredEnv(name: string) {
  const value = process.env[name];
  if (!value) throw new Error(`Missing required environment variable: ${name}`);
  return value;
}