UNPKG

@st4rbugs/multer-minio-storage

Version:

Multer Minio Streaming Storage With Hooks and Middleware for Buffer Transformation

241 lines (236 loc) 8.97 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { CountingStream: () => CountingStream, default: () => index_default }); module.exports = __toCommonJS(index_exports); // src/StreamingStorage/index.ts var import_crypto = require("crypto"); var import_path = require("path"); var import_stream = require("stream"); var MinioStreamingStorage = class { client; bucket; prefix; getObjectName; fileFilter; onBeforeUpload; transforms; metadata; autoCreateBucket; trustHeaderSize; region; contentTypeMap; /** Internal Meta from stream */ _imageMeta = /* @__PURE__ */ new Map(); constructor(options) { if (!("client" in options)) throw new Error("Minio client is required."); if (!("bucket" in options)) throw new Error("Bucket name is required."); this.client = options.client; this.bucket = options.bucket; this.prefix = options.prefix; this.getObjectName = options.getObjectName; this.fileFilter = options.fileFilter; this.onBeforeUpload = options.onBeforeUpload; this.transforms = options.transforms ?? (options.transform ? [options.transform] : void 0); this.metadata = options.metadata; this.autoCreateBucket = Boolean(options.autoCreateBucket); this.region = options.region; this.trustHeaderSize = Boolean(options.trustHeaderSize); this.contentTypeMap = options.contentTypeMap ?? {}; this._init().catch((err) => { if (err instanceof Error) console.error(`MinIO connection failed: ${err.message}`); else console.error("MinIO connection failed:", err); }); } async __ensureBucketExists() { const exists = await this.client.bucketExists(this.bucket); if (!exists && this.autoCreateBucket) { await this.client.makeBucket(this.bucket, this.region ?? ""); console.info(`Bucket "${this.bucket}" created in region "${this.region ?? "default"}".`); } else if (!exists) { throw new Error(`Bucket "${this.bucket}" does not exist. Please create it or enable autoCreateBucket.`); } } async _init() { try { const buckets = await this.client.listBuckets(); await this.__ensureBucketExists(); console.info("MinIO connected sucessfully. Available buckets:", buckets.map((b) => b.name).join(", ")); } catch (error) { const err = error; if (err?.code === "ECONNREFUSED") { throw new Error("Connection refused. Please check your MinIO server."); } else if (err?.message) { throw new Error("Unable to connect to MinIO: " + err.message); } else { console.error(err); throw new Error("Unable to connect to MinIO"); } } } _getFutureContentType(file) { const type = file.mimetype.split("/")[0]?.toLowerCase(); if (!type) return file.mimetype; if (type in this.contentTypeMap) return this.contentTypeMap[type]; return file.mimetype; } _getFutureMeta(file) { return { "Original-MimeType": file.mimetype, "Original-Name": file.originalname, "Field-Name": file.fieldname, "Content-Type": this._getFutureContentType(file) }; } _generateObjectName(_file) { let name = (0, import_crypto.randomUUID)().toString(); if (this.prefix !== void 0) name = import_path.posix.join(this.prefix, name); return name; } _generateFullPath(objectName, folder) { if (folder !== void 0) return import_path.posix.join("/", this.bucket, folder, objectName); return import_path.posix.join("/", this.bucket, objectName); } _generatePath(objectName, folder, prefix = "/") { if (folder !== void 0) return import_path.posix.join(prefix, folder, objectName); return import_path.posix.join(prefix, objectName); } _getMetaFromMap(id, file, key) { const meta = this._imageMeta.get(id); this._imageMeta.delete(id); if (meta === void 0) { console.warn("Warn: Image meta not found for", id); return { mimetype: file.mimetype, size: file.size, dimension: { width: 0, height: 0 }, key }; } return { mimetype: `${file.mimetype.split("/")[0]}/${meta.format}`, size: meta.size, dimension: { width: meta.width, height: meta.height }, key }; } _getContentSizeFromRequestHeaders(req) { const contentLength = req.headers["content-length"]; const isBulk = "files" in req; if (isBulk) { console.warn("Warn: Bulk upload detected. Not setting file size from headers."); return void 0; } if (contentLength === void 0) { console.warn("Warn: Content-Length header is missing from the request."); return void 0; } const parsed = parseInt(contentLength, 10); if (isNaN(parsed)) { console.warn("Warn: Content-Length header is not a number:", contentLength); return void 0; } if (parsed < 0) { console.warn("Warn: Content-Length header is negative:", parsed); return void 0; } return parsed; } async _handleFile(req, file, callback) { let passThrough = null; let inputStream = void 0; try { if (this.fileFilter) { const isFileAllowed = this.fileFilter(req, file); if (!isFileAllowed) return callback(new Error("File rejected by fileFilter.")); } if (this.onBeforeUpload) await Promise.resolve(this.onBeforeUpload(req, file)); inputStream = file.stream; if (this.transforms && this.transforms.length > 0) { for (const t of this.transforms) { const tstream = t(req, file); inputStream = inputStream.pipe(tstream); } inputStream.on("info", (info) => { const id = file.originalname; this._imageMeta.set(id, info); }); } const metaData = this.metadata ? this.metadata(req, file) : this._getFutureMeta(file); passThrough = new import_stream.PassThrough(); inputStream.pipe(passThrough); const filename = this.getObjectName ? this.getObjectName(req, file, this.bucket) : this._generateObjectName(file); const folder = typeof req.query.folder === "string" ? req.query.folder : void 0; const size = file.size ?? (this.trustHeaderSize ? this._getContentSizeFromRequestHeaders(req) : void 0); const path = this._generateFullPath(filename, folder); const relativePath = this._generatePath(filename, folder); console.debug("Uploading file to MinIO:", file.originalname, "as", filename, "to", path); await this.client.putObject(this.bucket, relativePath, passThrough, size, metaData); const key = this._generatePath(filename, folder, ""); const meta = this._getMetaFromMap(file.originalname, file, key); callback(null, { destination: this.bucket, filename, path, ...meta }); } catch (error) { const err = error; console.error("Error uploading file to MinIO:", err); callback(err); } finally { if (inputStream) { if (passThrough) inputStream.unpipe(passThrough).removeAllListeners(); else inputStream.removeAllListeners(); inputStream = void 0; } if (passThrough) passThrough.removeAllListeners().end().destroy(); passThrough = null; } } async _removeFile(_req, file, callback) { try { console.debug("Removing", file.originalname, "from MinIO:", file.filename); if (!file.path) throw new Error("File path is missing from the request."); await this.client.removeObject(this.bucket, file.filename); callback(null); } catch (error) { const err = error; callback(err); } } }; // src/utils/CountingStream.ts var import_stream2 = require("stream"); var CountingStream = class extends import_stream2.Transform { bytesRead = 0; _transform(chunk, encoding, callback) { this.bytesRead += chunk.length; callback(null, chunk); } async getBytesRead() { return new Promise((resolve, reject) => { this.on("finish", () => resolve(this.bytesRead)); this.on("error", reject); }); } }; // src/index.ts var index_default = MinioStreamingStorage; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { CountingStream });