// src/services/storage.ts // Copyright (C) 2026 Robert Colbert // 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; 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 { 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 {} async uploadBuffer(buffer: MinioBuffer): Promise { 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 { 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 { return this.statObject(file.bucket, file.key); } async statObject(bucket: string, key: string): Promise { 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 { 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 { 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 { 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();