202 lines
5.1 KiB
TypeScript
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();
|