@settlemint/sdk-minio
Version:
MinIO integration module for SettleMint SDK, providing S3-compatible object storage capabilities
521 lines (514 loc) • 19.1 kB
JavaScript
/* SettleMint MinIO SDK - Object Storage */
import { ensureServer } from "@settlemint/sdk-utils/runtime";
import { UrlSchema, validate } from "@settlemint/sdk-utils/validation";
import { Client } from "minio";
import { z } from "zod";
//#region src/helpers/client-options.schema.ts
/**
* Schema for validating server client options for the MinIO client.
*/
const ServerClientOptionsSchema = z.object({
instance: UrlSchema,
accessKey: z.string().min(1, "Access key cannot be empty"),
secretKey: z.string().min(1, "Secret key cannot be empty")
});
//#endregion
//#region src/helpers/schema.ts
/**
* Schema for file metadata stored in MinIO.
* Defines the structure and validation rules for file information.
*/
const FileMetadataSchema = z.object({
id: z.string(),
name: z.string(),
contentType: z.string(),
size: z.number(),
uploadedAt: z.string().datetime(),
etag: z.string(),
url: z.string().url().optional()
});
/**
* Default bucket name to use for file storage when none is specified.
*/
const DEFAULT_BUCKET = "uploads";
//#endregion
//#region src/helpers/executor.ts
/**
* Executes a MinIO operation using the provided client
*
* @param client - MinIO client to use
* @param operation - The operation to execute
* @returns The result of the operation execution
* @throws Will throw an error if the operation fails
*
* @example
* import { createServerMinioClient, createListObjectsOperation, executeMinioOperation } from "@settlemint/sdk-minio";
* const { client } = createServerMinioClient({
* instance: process.env.SETTLEMINT_MINIO_ENDPOINT!,
* accessKey: process.env.SETTLEMINT_MINIO_ACCESS_KEY!,
* secretKey: process.env.SETTLEMINT_MINIO_SECRET_KEY!
* });
* const listOperation = createListObjectsOperation("my-bucket", "prefix/");
* const result = await executeMinioOperation(client, listOperation);
*/
async function executeMinioOperation(client, operation) {
ensureServer();
return operation.execute(client);
}
//#endregion
//#region src/helpers/operations.ts
/**
* Creates an operation to list objects in a bucket
*
* @param bucket - The bucket name to list objects from
* @param prefix - Optional prefix to filter objects (like a folder path)
* @returns A MinioOperation that lists objects when executed
* @throws Will throw an error if the operation fails
*
* @example
* import { createListObjectsOperation, executeMinioOperation } from "@settlemint/sdk-minio";
*
* const listOperation = createListObjectsOperation("my-bucket", "folder/");
* const objects = await executeMinioOperation(client, listOperation);
*/
function createListObjectsOperation(bucket, prefix = "") {
return { execute: async (client) => {
const objectsStream = client.listObjects(bucket, prefix, true);
const objects = [];
return new Promise((resolve, reject) => {
objectsStream.on("data", (obj) => {
if (obj.name && typeof obj.size === "number" && obj.etag && obj.lastModified) {
objects.push({
name: obj.name,
prefix: obj.prefix,
size: obj.size,
etag: obj.etag,
lastModified: obj.lastModified
});
}
});
objectsStream.on("error", (err) => {
reject(err);
});
objectsStream.on("end", () => {
resolve(objects);
});
});
} };
}
/**
* Creates an operation to get an object's metadata
*
* @param bucket - The bucket name containing the object
* @param objectName - The object name/path
* @returns A MinioOperation that gets object stats when executed
* @throws Will throw an error if the operation fails
*
* @example
* import { createStatObjectOperation, executeMinioOperation } from "@settlemint/sdk-minio";
*
* const statOperation = createStatObjectOperation("my-bucket", "folder/file.txt");
* const stats = await executeMinioOperation(client, statOperation);
*/
function createStatObjectOperation(bucket, objectName) {
return { execute: async (client) => {
return client.statObject(bucket, objectName);
} };
}
/**
* Creates an operation to upload a buffer to MinIO
*
* @param bucket - The bucket name to upload to
* @param objectName - The object name/path to create
* @param buffer - The buffer containing the file data
* @param metadata - Optional metadata to attach to the object
* @returns A MinioOperation that uploads the buffer when executed
* @throws Will throw an error if the operation fails
*
* @example
* import { createUploadOperation, executeMinioOperation } from "@settlemint/sdk-minio";
*
* const buffer = Buffer.from("file content");
* const uploadOperation = createUploadOperation("my-bucket", "folder/file.txt", buffer, { "content-type": "text/plain" });
* const result = await executeMinioOperation(client, uploadOperation);
*/
function createUploadOperation(bucket, objectName, buffer, metadata) {
return { execute: async (client) => {
return client.putObject(bucket, objectName, buffer, undefined, metadata);
} };
}
/**
* Creates an operation to delete an object from MinIO
*
* @param bucket - The bucket name containing the object
* @param objectName - The object name/path to delete
* @returns A MinioOperation that deletes the object when executed
* @throws Will throw an error if the operation fails
*
* @example
* import { createDeleteOperation, executeMinioOperation } from "@settlemint/sdk-minio";
*
* const deleteOperation = createDeleteOperation("my-bucket", "folder/file.txt");
* await executeMinioOperation(client, deleteOperation);
*/
function createDeleteOperation(bucket, objectName) {
return { execute: async (client) => {
return client.removeObject(bucket, objectName);
} };
}
/**
* Creates an operation to generate a presigned URL for an object
*
* @param bucket - The bucket name containing the object
* @param objectName - The object name/path
* @param expirySeconds - How long the URL should be valid for in seconds
* @returns A MinioOperation that creates a presigned URL when executed
* @throws Will throw an error if the operation fails
*
* @example
* import { createPresignedUrlOperation, executeMinioOperation } from "@settlemint/sdk-minio";
*
* const urlOperation = createPresignedUrlOperation("my-bucket", "folder/file.txt", 3600);
* const url = await executeMinioOperation(client, urlOperation);
*/
function createPresignedUrlOperation(bucket, objectName, expirySeconds) {
return { execute: async (client) => {
return client.presignedGetObject(bucket, objectName, expirySeconds);
} };
}
/**
* Creates an operation to generate a presigned PUT URL for direct uploads
*
* @param bucket - The bucket name to upload to
* @param objectName - The object name/path to create
* @param expirySeconds - How long the URL should be valid for in seconds
* @returns A MinioOperation that creates a presigned PUT URL when executed
* @throws Will throw an error if the operation fails
*
* @example
* import { createPresignedPutOperation, executeMinioOperation } from "@settlemint/sdk-minio";
*
* const putUrlOperation = createPresignedPutOperation("my-bucket", "folder/file.txt", 3600);
* const url = await executeMinioOperation(client, putUrlOperation);
*/
function createPresignedPutOperation(bucket, objectName, expirySeconds) {
return { execute: async (client) => {
return client.presignedPutObject(bucket, objectName, expirySeconds);
} };
}
/**
* Creates a simplified upload function bound to a specific client
*
* @param client - The MinIO client to use for uploads
* @returns A function that uploads buffers to MinIO
* @throws Will throw an error if the operation fails
*
* @example
* import { createSimpleUploadOperation, getMinioClient } from "@settlemint/sdk-minio";
*
* const client = await getMinioClient();
* const uploadFn = createSimpleUploadOperation(client);
* const result = await uploadFn(buffer, "my-bucket", "folder/file.txt", { "content-type": "text/plain" });
*/
function createSimpleUploadOperation(client) {
return async (buffer, bucket, objectName, metadata) => {
return client.putObject(bucket, objectName, buffer, undefined, metadata);
};
}
//#endregion
//#region src/helpers/functions.ts
/**
* Helper function to normalize paths and prevent double slashes
*
* @param path - The path to normalize
* @param fileName - The filename to append
* @returns The normalized path with filename
* @throws Will throw an error if the path is too long (max 1000 characters)
*/
function normalizePath(path, fileName) {
if (path.length > 1e3) {
throw new Error("Path is too long");
}
const cleanPath = path.replace(/\/+$/, "");
if (!cleanPath) {
return fileName;
}
return `${cleanPath}/${fileName}`;
}
/**
* Gets a list of files with optional prefix filter
*
* @param client - The MinIO client to use
* @param prefix - Optional prefix to filter files (like a folder path)
* @param bucket - Optional bucket name (defaults to DEFAULT_BUCKET)
* @returns Array of file metadata objects
* @throws Will throw an error if the operation fails or client initialization fails
*
* @example
* import { createServerMinioClient, getFilesList } from "@settlemint/sdk-minio";
*
* const { client } = createServerMinioClient({
* instance: process.env.SETTLEMINT_MINIO_ENDPOINT!,
* accessKey: process.env.SETTLEMINT_MINIO_ACCESS_KEY!,
* secretKey: process.env.SETTLEMINT_MINIO_SECRET_KEY!
* });
*
* const files = await getFilesList(client, "documents/");
*/
async function getFilesList(client, prefix = "", bucket = DEFAULT_BUCKET) {
ensureServer();
console.log(`Listing files with prefix: "${prefix}" in bucket: "${bucket}"`);
try {
const listOperation = createListObjectsOperation(bucket, prefix);
const objects = await executeMinioOperation(client, listOperation);
console.log(`Found ${objects.length} files in MinIO`);
const fileObjects = await Promise.allSettled(objects.map(async (obj) => {
try {
const presignedUrlOperation = createPresignedUrlOperation(bucket, obj.name, 3600);
const url = await executeMinioOperation(client, presignedUrlOperation);
return {
id: obj.name,
name: obj.name.split("/").pop() || obj.name,
contentType: "application/octet-stream",
size: obj.size,
uploadedAt: obj.lastModified.toISOString(),
etag: obj.etag,
url
};
} catch (error) {
console.warn(`Failed to generate presigned URL for ${obj.name}:`, error);
return {
id: obj.name,
name: obj.name.split("/").pop() || obj.name,
contentType: "application/octet-stream",
size: obj.size,
uploadedAt: obj.lastModified.toISOString(),
etag: obj.etag
};
}
})).then((results) => results.filter((result) => result.status === "fulfilled").map((result) => result.value));
return validate(FileMetadataSchema.array(), fileObjects);
} catch (error) {
console.error("Failed to list files:", error);
throw new Error(`Failed to list files: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Gets a single file by its object name
*
* @param client - The MinIO client to use
* @param fileId - The file identifier/path
* @param bucket - Optional bucket name (defaults to DEFAULT_BUCKET)
* @returns File metadata with presigned URL
* @throws Will throw an error if the file doesn't exist or client initialization fails
*
* @example
* import { createServerMinioClient, getFileByObjectName } from "@settlemint/sdk-minio";
*
* const { client } = createServerMinioClient({
* instance: process.env.SETTLEMINT_MINIO_ENDPOINT!,
* accessKey: process.env.SETTLEMINT_MINIO_ACCESS_KEY!,
* secretKey: process.env.SETTLEMINT_MINIO_SECRET_KEY!
* });
*
* const file = await getFileByObjectName(client, "documents/report.pdf");
*/
async function getFileById(client, fileId, bucket = DEFAULT_BUCKET) {
ensureServer();
console.log(`Getting file details for: ${fileId} in bucket: ${bucket}`);
try {
const statOperation = createStatObjectOperation(bucket, fileId);
const statResult = await executeMinioOperation(client, statOperation);
const presignedUrlOperation = createPresignedUrlOperation(bucket, fileId, 3600);
const url = await executeMinioOperation(client, presignedUrlOperation);
let size = 0;
if (statResult.metaData["content-length"]) {
const parsedSize = Number.parseInt(statResult.metaData["content-length"], 10);
if (!Number.isNaN(parsedSize) && parsedSize >= 0 && Number.isFinite(parsedSize)) {
size = parsedSize;
}
} else if (typeof statResult.size === "number" && !Number.isNaN(statResult.size)) {
size = statResult.size;
}
const fileMetadata = {
id: fileId,
name: fileId.split("/").pop() || fileId,
contentType: statResult.metaData["content-type"] || "application/octet-stream",
size,
uploadedAt: statResult.lastModified.toISOString(),
etag: statResult.etag,
url
};
return validate(FileMetadataSchema, fileMetadata);
} catch (error) {
console.error(`Failed to get file ${fileId}:`, error);
throw new Error(`Failed to get file ${fileId}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Deletes a file from storage
*
* @param client - The MinIO client to use
* @param fileId - The file identifier/path
* @param bucket - Optional bucket name (defaults to DEFAULT_BUCKET)
* @returns Success status
* @throws Will throw an error if deletion fails or client initialization fails
*
* @example
* import { createServerMinioClient, deleteFile } from "@settlemint/sdk-minio";
*
* const { client } = createServerMinioClient({
* instance: process.env.SETTLEMINT_MINIO_ENDPOINT!,
* accessKey: process.env.SETTLEMINT_MINIO_ACCESS_KEY!,
* secretKey: process.env.SETTLEMINT_MINIO_SECRET_KEY!
* });
*
* await deleteFile(client, "documents/report.pdf");
*/
async function deleteFile(client, fileId, bucket = DEFAULT_BUCKET) {
ensureServer();
try {
const deleteOperation = createDeleteOperation(bucket, fileId);
await executeMinioOperation(client, deleteOperation);
return true;
} catch (error) {
console.error(`Failed to delete file ${fileId}:`, error);
throw new Error(`Failed to delete file ${fileId}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Creates a presigned upload URL for direct browser uploads
*
* @param client - The MinIO client to use
* @param fileName - The file name to use
* @param path - Optional path/folder
* @param bucket - Optional bucket name (defaults to DEFAULT_BUCKET)
* @param expirySeconds - How long the URL should be valid for
* @returns Presigned URL for PUT operation
* @throws Will throw an error if URL creation fails or client initialization fails
*
* @example
* import { createServerMinioClient, createPresignedUploadUrl } from "@settlemint/sdk-minio";
*
* const { client } = createServerMinioClient({
* instance: process.env.SETTLEMINT_MINIO_ENDPOINT!,
* accessKey: process.env.SETTLEMINT_MINIO_ACCESS_KEY!,
* secretKey: process.env.SETTLEMINT_MINIO_SECRET_KEY!
* });
*
* // Generate the presigned URL on the server
* const url = await createPresignedUploadUrl(client, "report.pdf", "documents/");
*
* // Send the URL to the client/browser via HTTP response
* return Response.json({ uploadUrl: url });
*
* // Then in the browser:
* const response = await fetch('/api/get-upload-url');
* const { uploadUrl } = await response.json();
* await fetch(uploadUrl, {
* method: 'PUT',
* headers: { 'Content-Type': 'application/pdf' },
* body: pdfFile
* });
*/
async function createPresignedUploadUrl(client, fileName, path = "", bucket = DEFAULT_BUCKET, expirySeconds = 3600) {
ensureServer();
try {
const safeFileName = fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
const objectName = normalizePath(path, safeFileName);
const presignedPutOperation = createPresignedPutOperation(bucket, objectName, expirySeconds);
const url = await executeMinioOperation(client, presignedPutOperation);
if (!url) {
throw new Error("Failed to generate presigned upload URL");
}
return url;
} catch (error) {
console.error("Failed to create presigned upload URL:", error);
throw new Error(`Failed to create presigned upload URL: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Uploads a buffer directly to storage
*
* @param client - The MinIO client to use
* @param buffer - The buffer to upload
* @param objectName - The full object name/path
* @param contentType - The content type of the file
* @param bucket - Optional bucket name (defaults to DEFAULT_BUCKET)
* @returns The uploaded file metadata
* @throws Will throw an error if upload fails or client initialization fails
*
* @example
* import { createServerMinioClient, uploadBuffer } from "@settlemint/sdk-minio";
*
* const { client } = createServerMinioClient({
* instance: process.env.SETTLEMINT_MINIO_ENDPOINT!,
* accessKey: process.env.SETTLEMINT_MINIO_ACCESS_KEY!,
* secretKey: process.env.SETTLEMINT_MINIO_SECRET_KEY!
* });
*
* const buffer = Buffer.from("Hello, world!");
* const uploadedFile = await uploadFile(client, buffer, "documents/hello.txt", "text/plain");
*/
async function uploadFile(client, buffer, objectName, contentType, bucket = DEFAULT_BUCKET) {
ensureServer();
try {
const metadata = {
"content-type": contentType,
"upload-time": new Date().toISOString()
};
const simpleUploadFn = createSimpleUploadOperation(client);
const result = await simpleUploadFn(buffer, bucket, objectName, metadata);
const presignedUrlOperation = createPresignedUrlOperation(bucket, objectName, 3600);
const url = await executeMinioOperation(client, presignedUrlOperation);
const fileName = objectName.split("/").pop() || objectName;
const fileMetadata = {
id: objectName,
name: fileName,
contentType,
size: buffer.length,
uploadedAt: new Date().toISOString(),
etag: result.etag,
url
};
return validate(FileMetadataSchema, fileMetadata);
} catch (error) {
console.error("Failed to upload file:", error);
throw new Error(`Failed to upload file: ${error instanceof Error ? error.message : String(error)}`);
}
}
//#endregion
//#region src/minio.ts
/**
* Creates a MinIO client for server-side use with authentication.
*
* @param options - The server client options for configuring the MinIO client
* @returns An object containing the initialized MinIO client
* @throws Will throw an error if not called on the server or if the options fail validation
*
* @example
* import { createServerMinioClient } from "@settlemint/sdk-minio";
*
* const { client } = createServerMinioClient({
* instance: process.env.SETTLEMINT_MINIO_ENDPOINT!,
* accessKey: process.env.SETTLEMINT_MINIO_ACCESS_KEY!,
* secretKey: process.env.SETTLEMINT_MINIO_SECRET_KEY!
* });
* client.listBuckets();
*/
function createServerMinioClient(options) {
ensureServer();
const validatedOptions = validate(ServerClientOptionsSchema, options);
const url = new URL(validatedOptions.instance);
return { client: new Client({
endPoint: url.hostname,
accessKey: validatedOptions.accessKey,
secretKey: validatedOptions.secretKey,
useSSL: url.protocol !== "http:",
port: url.port ? Number(url.port) : undefined,
region: "eu-central-1"
}) };
}
//#endregion
export { DEFAULT_BUCKET, createPresignedUploadUrl, createServerMinioClient, deleteFile, getFileById, getFilesList, uploadFile };
//# sourceMappingURL=minio.js.map