Files
GearBox/src/server/services/storage.service.ts
Jean-Luc Makiola d519a83cc4
Some checks failed
CI / ci (push) Failing after 19s
CI / deploy (push) Has been skipped
CI / e2e (push) Has been skipped
infra: migrate deployment to Coolify with Garage S3
- 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>
2026-04-07 15:28:43 +02:00

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