gadget/gadget-code/src/services/storage.ts
2026-04-28 09:20:37 -04:00

202 lines
5.1 KiB
TypeScript

// src/services/storage.ts
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// All Rights Reserved
import env from "../config/env.js";
import { Stream as NodeStream } from "node:stream";
import { Client as MinioClient, BucketItemStat } from "minio";
/*
* WebApp needs the type, and it's not exposed any other way. It's not even an
* uncommon type. If there's a better way to expose the type, I don't know it.
*/
import { UploadedObjectInfo } from "../../node_modules/minio/dist/esm/internal/type.mjs";
export type MinioObjectMetadata = Record<string, string>;
export interface MinioFile {
bucket: string;
key: string;
filePath?: string;
metadata?: MinioObjectMetadata;
}
export interface MinioStreamFileRange {
offset: number;
length: number;
}
export interface MinioStreamFile extends MinioFile {
range?: MinioStreamFileRange;
}
export interface MinioBuffer {
bucket: string;
key: string;
buffer: Buffer;
metadata?: MinioObjectMetadata;
}
import { DtpService } from "../lib/service.js";
class StorageService extends DtpService {
minio?: MinioClient;
get name(): string {
return "StorageService";
}
get slug(): string {
return "storage";
}
constructor() {
super();
}
async start(): Promise<void> {
try {
this.log.info("creating MinIO transport", {
endPoint: env.minio.endpoint,
port: env.minio.port,
useSSL: env.minio.useSsl,
accessKey: env.minio.accessKey,
});
this.minio = new MinioClient({
endPoint: env.minio.endpoint,
port: env.minio.port,
useSSL: env.minio.useSsl,
accessKey: env.minio.accessKey,
secretKey: env.minio.secretKey,
});
} catch (error) {
this.log.error("failed to connect to S3/MinIO", { error });
const e = new Error("Not connected to storage", { cause: error });
e.statusCode = 500;
throw e;
}
}
async stop(): Promise<void> {}
async uploadBuffer(buffer: MinioBuffer): Promise<void> {
if (!this.minio) {
const error = new Error("Not connected to storage");
error.statusCode = 503;
throw error;
}
const response = await this.minio.putObject(
buffer.bucket,
buffer.key,
buffer.buffer,
buffer.buffer.length,
buffer.metadata,
);
this.log.debug("uploaded buffer to storage", { response });
}
async uploadFile(fileInfo: MinioFile): Promise<UploadedObjectInfo> {
if (!this.minio) {
const error = new Error("Not connected to storage");
error.statusCode = 503;
throw error;
}
if (!fileInfo.filePath) {
const error = new Error("Must specify filePath of file to upload");
error.statusCode = 400;
throw error;
}
return this.minio.fPutObject(
fileInfo.bucket,
fileInfo.key,
fileInfo.filePath,
fileInfo.metadata,
);
}
async statFile(file: MinioFile): Promise<BucketItemStat> {
return this.statObject(file.bucket, file.key);
}
async statObject(bucket: string, key: string): Promise<BucketItemStat> {
if (!this.minio) {
const error = new Error("Not connected to storage");
error.statusCode = 503;
throw error;
}
this.log.debug("retrieving object status", { bucket, key });
return this.minio.statObject(bucket, key);
}
async downloadFile(fileInfo: MinioFile): Promise<void> {
if (!this.minio) {
const error = new Error("Not connected to storage");
error.statusCode = 503;
throw error;
}
if (!fileInfo.filePath) {
const error = new Error("Must specify filePath of file to download");
error.statusCode = 400;
throw error;
}
this.log.debug("downloading object to file", {
bucket: fileInfo.bucket,
key: fileInfo.key,
filePath: fileInfo.filePath,
});
return this.minio.fGetObject(
fileInfo.bucket,
fileInfo.key,
fileInfo.filePath,
);
}
async openDownloadStream(fileInfo: MinioStreamFile): Promise<NodeStream> {
if (!this.minio) {
const error = new Error("Not connected to storage");
error.statusCode = 503;
throw error;
}
if (fileInfo.range) {
this.log.debug("fetching partial object", {
bucket: fileInfo.bucket,
key: fileInfo.key,
range: fileInfo.range,
});
const stream: NodeStream = await this.minio.getPartialObject(
fileInfo.bucket,
fileInfo.key,
fileInfo.range.offset,
fileInfo.range.length,
);
return stream;
}
this.log.debug("fetching object", {
bucket: fileInfo.bucket,
key: fileInfo.key,
});
const stream: NodeStream = await this.minio.getObject(
fileInfo.bucket,
fileInfo.key,
);
return stream;
}
async removeObject(bucket: string, key: string): Promise<void> {
if (!this.minio) {
const error = new Error("Not connected to storage");
error.statusCode = 503;
throw error;
}
this.log.debug("removing object", { bucket, key });
await this.minio.removeObject(bucket, key);
}
}
export { NodeStream };
export default new StorageService();