Vercel/src/platforms/storage/minio.ts
Sam Becker b8c01492b8
Store optimized assets for all photos (#304)
* Begin storing optimized photo files

* Increase optimized image size to 1080

* Refactor photo/storage modules

* Refine storage file naming api

* Simplify photo storage api

* Finalize photo storage api

* Start storing/serving optimized photos

* Finalize optimized photo asset generation

* Temporarily allow static optimization on PREVIEW branches

* Restore static optimization as production-only

* Remove og image inline-flex class

* Tweak convert upload signature

* Refactor optimized file storage

* Display optimized files when they exist in photo form

* Create small disclosure component

* Report photo storage files more accurately

* Sort optimized files

* Generate optimized storage files when updating/syncing photos

* Include source bucket when copying files with MinIO

* Make deleting files more resilient
2025-09-06 23:20:20 -05:00

96 lines
2.8 KiB
TypeScript

import {
S3Client,
CopyObjectCommand,
ListObjectsCommand,
PutObjectCommand,
DeleteObjectCommand,
} from '@aws-sdk/client-s3';
import { StorageListResponse, generateStorageId } from '.';
import { formatBytes } from '@/utility/number';
const MINIO_BUCKET = process.env.NEXT_PUBLIC_MINIO_BUCKET ?? '';
const MINIO_DOMAIN = process.env.NEXT_PUBLIC_MINIO_DOMAIN ?? '';
const MINIO_PORT = process.env.NEXT_PUBLIC_MINIO_PORT ?? '';
const MINIO_DISABLE_SSL = process.env.NEXT_PUBLIC_MINIO_DISABLE_SSL === '1';
const MINIO_ACCESS_KEY = process.env.MINIO_ACCESS_KEY ?? '';
const MINIO_SECRET_ACCESS_KEY = process.env.MINIO_SECRET_ACCESS_KEY ?? '';
const PROTOCOL = MINIO_DISABLE_SSL ? 'http' : 'https';
const ENDPOINT = MINIO_BUCKET && MINIO_DOMAIN
? `${PROTOCOL}://${MINIO_DOMAIN}${MINIO_PORT ? `:${MINIO_PORT}` : ''}`
: undefined;
export const MINIO_BASE_URL = ENDPOINT
? `${ENDPOINT}/${MINIO_BUCKET}`
: undefined;
export const minioClient = () => new S3Client({
region: 'us-east-1',
endpoint: ENDPOINT,
credentials: {
accessKeyId: MINIO_ACCESS_KEY,
secretAccessKey: MINIO_SECRET_ACCESS_KEY,
},
forcePathStyle: true,
});
const urlForKey = (key?: string) => `${MINIO_BASE_URL}/${key}`;
export const isUrlFromMinio = (url?: string) =>
MINIO_BASE_URL && url?.startsWith(MINIO_BASE_URL);
export const minioPutObjectCommandForKey = (Key: string) =>
new PutObjectCommand({ Bucket: MINIO_BUCKET, Key });
export const minioPut = async (
file: Buffer,
fileName: string,
): Promise<string> =>
minioClient().send(new PutObjectCommand({
Bucket: MINIO_BUCKET,
Key: fileName,
Body: file,
}))
.then(() => urlForKey(fileName));
export const minioCopy = async (
fileNameSource: string,
fileNameDestination: string,
addRandomSuffix?: boolean,
) => {
const name = fileNameSource.split('.')[0];
const extension = fileNameSource.split('.')[1];
const Key = addRandomSuffix
? `${name}-${generateStorageId()}.${extension}`
: fileNameDestination;
return minioClient().send(new CopyObjectCommand({
Bucket: MINIO_BUCKET,
// Bucket behavior seems to differ from R2 + S3
CopySource: `${MINIO_BUCKET}/${fileNameSource}`,
Key,
}))
.then(() => urlForKey(Key));
};
export const minioList = async (
Prefix: string,
): Promise<StorageListResponse> =>
minioClient().send(new ListObjectsCommand({
Bucket: MINIO_BUCKET,
Prefix,
}))
.then((data) => data.Contents?.map(({ Key, LastModified, Size }) => ({
url: urlForKey(Key),
fileName: Key ?? '',
uploadedAt: LastModified,
size: Size ? formatBytes(Size) : undefined,
})) ?? []);
export const minioDelete = async (Key: string): Promise<void> => {
const deleteObjectCommand = new DeleteObjectCommand({
Bucket: MINIO_BUCKET,
Key,
});
await minioClient().send(deleteObjectCommand);
};