- Remove docker-compose files (Coolify manages services individually) - Replace MinIO with Garage (S3-compatible, actively maintained) - Add CI deploy job: build+push :develop image on every green Develop push - Add Coolify webhook trigger for automatic redeployment - Update README, .env.example, and storage references - Rename migrate script to provider-agnostic name Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
82 lines
2.0 KiB
TypeScript
82 lines
2.0 KiB
TypeScript
import {
|
|
DeleteObjectCommand,
|
|
GetObjectCommand,
|
|
PutObjectCommand,
|
|
S3Client,
|
|
} from "@aws-sdk/client-s3";
|
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
|
|
// S3 API abstraction — provider-agnostic (Garage, Cloudflare R2, AWS S3).
|
|
|
|
const s3 = new S3Client({
|
|
endpoint: process.env.S3_ENDPOINT,
|
|
region: process.env.S3_REGION ?? "us-east-1",
|
|
credentials: {
|
|
accessKeyId: process.env.S3_ACCESS_KEY!,
|
|
secretAccessKey: process.env.S3_SECRET_KEY!,
|
|
},
|
|
forcePathStyle: true, // REQUIRED for Garage and most S3-compatible services
|
|
});
|
|
|
|
const bucket = process.env.S3_BUCKET ?? "gearbox-images";
|
|
const presignExpiry = Number.parseInt(
|
|
process.env.S3_PRESIGN_EXPIRY ?? "3600",
|
|
10,
|
|
);
|
|
|
|
export async function uploadImage(
|
|
buffer: Buffer | ArrayBuffer,
|
|
filename: string,
|
|
contentType: string,
|
|
): Promise<void> {
|
|
await s3.send(
|
|
new PutObjectCommand({
|
|
Bucket: bucket,
|
|
Key: filename,
|
|
Body: Buffer.from(buffer),
|
|
ContentType: contentType,
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function deleteImage(filename: string): Promise<void> {
|
|
await s3.send(
|
|
new DeleteObjectCommand({
|
|
Bucket: bucket,
|
|
Key: filename,
|
|
}),
|
|
);
|
|
}
|
|
|
|
export async function getImageUrl(filename: string): Promise<string> {
|
|
const command = new GetObjectCommand({
|
|
Bucket: bucket,
|
|
Key: filename,
|
|
});
|
|
return getSignedUrl(s3, command, { expiresIn: presignExpiry });
|
|
}
|
|
|
|
/**
|
|
* Enrich a record that has an imageFilename with a presigned imageUrl.
|
|
* Returns null imageUrl when imageFilename is null.
|
|
*/
|
|
export async function withImageUrl<T extends { imageFilename: string | null }>(
|
|
record: T,
|
|
): Promise<T & { imageUrl: string | null }> {
|
|
return {
|
|
...record,
|
|
imageUrl: record.imageFilename
|
|
? await getImageUrl(record.imageFilename)
|
|
: null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Batch version of withImageUrl. Uses Promise.all for parallelism.
|
|
*/
|
|
export async function withImageUrls<T extends { imageFilename: string | null }>(
|
|
records: T[],
|
|
): Promise<(T & { imageUrl: string | null })[]> {
|
|
return Promise.all(records.map((record) => withImageUrl(record)));
|
|
}
|