Add support for storing images in S3 (#896)

This commit is contained in:
solidDoWant 2025-04-20 11:47:49 -05:00 committed by GitHub
parent 1d1ea295c0
commit 099d893da9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 29 additions and 9 deletions

View File

@ -1,5 +1,5 @@
import { stat } from "node:fs/promises";
import type { BunFile } from "bun";
import type { Stats } from "node:fs";
import type { BunFile, S3File, S3Stats } from "bun";
import { type SQL, and, eq, sql } from "drizzle-orm";
import Elysia, { type Context, t } from "elysia";
import { prefix } from "~/base";
@ -15,6 +15,7 @@ import { sqlarr } from "~/db/utils";
import { KError } from "~/models/error";
import { bubble } from "~/models/examples";
import { AcceptLanguage, isUuid, processLanguages } from "~/models/utils";
import { getFile } from "~/utils";
import { imageDir } from "./seed/images";
function getRedirectToImageHandler({
@ -98,7 +99,7 @@ export const imagesH = new Elysia({ tags: ["images"] })
"/images/:id",
async ({ params: { id }, query: { quality }, headers: reqHeaders }) => {
const path = `${imageDir}/${id}.${quality}.jpg`;
const file = Bun.file(path);
const file = getFile(path);
const etag = await generateETag(file);
if (await isCached(reqHeaders, etag, path))
@ -366,8 +367,13 @@ export async function isCached(
if (headers["if-modified-since"]) {
const ifModifiedSince = headers["if-modified-since"];
let lastModified: Date | undefined;
const stat = await getFile(filePath).stat();
try {
lastModified = (await stat(filePath)).mtime;
if ((stat as S3Stats).lastModified) {
lastModified = (stat as S3Stats).lastModified;
} else if ((stat as Stats).mtime) {
lastModified = (stat as Stats).mtime;
}
} catch {
/* empty */
}
@ -382,7 +388,7 @@ export async function isCached(
return false;
}
export async function generateETag(file: BunFile) {
export async function generateETag(file: BunFile | S3File) {
const hash = new Bun.CryptoHasher("md5");
hash.update(await file.arrayBuffer());

View File

@ -1,4 +1,3 @@
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { encode } from "blurhash";
import { type SQL, and, eq, is, lt, sql } from "drizzle-orm";
@ -9,10 +8,9 @@ import sharp from "sharp";
import { type Transaction, db } from "~/db";
import { mqueue } from "~/db/schema/mqueue";
import type { Image } from "~/models/utils";
import { getFile } from "~/utils";
export const imageDir = process.env.IMAGES_PATH ?? "./images";
await mkdir(imageDir, { recursive: true });
export const defaultBlurhash = "000000";
type ImageTask = {
@ -164,7 +162,9 @@ async function downloadImage(id: string, url: string): Promise<string> {
await Promise.all(
Object.entries(resolutions).map(async ([resolution, dimensions]) => {
const buffer = await image.clone().resize(dimensions.width).toBuffer();
await writeFile(path.join(imageDir, `${id}.${resolution}.jpg`), buffer);
const file = getFile(path.join(imageDir, `${id}.${resolution}.jpg`));
await Bun.write(file, buffer, { mode: 0o660 });
}),
);

View File

@ -1,3 +1,5 @@
import type { BunFile, S3File } from "bun";
// remove indent in multi-line comments
export const comment = (str: TemplateStringsArray, ...values: any[]) =>
str
@ -14,3 +16,15 @@ export function getYear(date: string) {
export type Prettify<T> = {
[K in keyof T]: Prettify<T[K]>;
} & {};
// Returns either a filesystem-backed file, or a S3-backed file,
// depending on whether or not S3 environment variables are set.
export function getFile(path: string): BunFile | S3File {
if ("S3_BUCKET" in process.env || "AWS_BUCKET" in process.env) {
// This will use a S3 client configured via environment variables.
// See https://bun.sh/docs/api/s3#credentials for more details.
return Bun.s3.file(path);
}
return Bun.file(path);
}