UNPKG

@settlemint/sdk-minio

Version:

MinIO integration module for SettleMint SDK, providing S3-compatible object storage capabilities

521 lines (514 loc) • 19.1 kB
/* 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