@st4rbugs/multer-minio-storage
Version:
Multer Minio Streaming Storage With Hooks and Middleware for Buffer Transformation
241 lines (236 loc) • 8.97 kB
JavaScript
"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
});