UNPKG

@blocklet/uploader-server

Version:

blocklet upload server

335 lines (334 loc) 11.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.calculateCacheControl = calculateCacheControl; exports.checkTrustedReferer = checkTrustedReferer; exports.getFileHash = void 0; exports.getFileNameFromReq = getFileNameFromReq; exports.getMediaKitFileStream = getMediaKitFileStream; exports.getTrustedDomainsCache = getTrustedDomainsCache; exports.logger = void 0; exports.proxyImageDownload = proxyImageDownload; exports.removeExifFromFile = void 0; exports.scanDirectory = scanDirectory; exports.serveResource = serveResource; exports.setPDFDownloadHeader = setPDFDownloadHeader; exports.uploadToMediaKit = uploadToMediaKit; var _axios = _interopRequireDefault(require("axios")); var _path = _interopRequireWildcard(require("path")); var _urlJoin = _interopRequireDefault(require("url-join")); var _isbot = require("isbot"); var _component = _interopRequireDefault(require("@blocklet/sdk/lib/component")); var _constants = require("./constants"); var _crypto = _interopRequireDefault(require("crypto")); var _verifySign = require("@blocklet/sdk/lib/util/verify-sign"); var _formData = _interopRequireDefault(require("form-data")); var _omit = _interopRequireDefault(require("lodash/omit")); var _ms = _interopRequireDefault(require("ms")); var _mimeTypes = _interopRequireDefault(require("mime-types")); var _fs = require("fs"); var _exifBeGone = _interopRequireDefault(require("exif-be-gone")); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } let logger = exports.logger = console; const isProduction = process.env.NODE_ENV === "production" || process.env.ABT_NODE_SERVICE_ENV === "production"; if (process.env.BLOCKLET_LOG_DIR) { try { const initLogger = require("@blocklet/logger"); exports.logger = logger = initLogger("uploader-server"); logger.info("uploader-server logger init success"); } catch (error) {} } const DEFAULT_TTL = 5 * 60 * 1e3; const appUrl = process.env.BLOCKLET_APP_URL || ""; const trustedDomainsCache = { timestamp: 0, domains: [] }; async function getTrustedDomainsCache({ forceUpdate = false, ttl = DEFAULT_TTL } = {}) { if (!appUrl) { return []; } const now = Date.now(); if (!forceUpdate && trustedDomainsCache.domains.length > 0) { if (now - trustedDomainsCache.timestamp < ttl) { return trustedDomainsCache.domains; } } try { if (!trustedDomainsCache?.domains?.length) { trustedDomainsCache.domains = await _axios.default.get((0, _urlJoin.default)(appUrl, "/.well-known/service/api/federated/getTrustedDomains")).then(res => res.data); trustedDomainsCache.timestamp = now; } } catch (error) {} return trustedDomainsCache.domains; } async function checkTrustedReferer(req, res, next) { if (!isProduction) { return next?.(); } if ((0, _isbot.isbot)(req.get("user-agent"))) { return next?.(); } const referer = req.headers.referer; if (!referer) { return res.status(403).send("Access denied"); } const allowedDomains = await getTrustedDomainsCache(); const refererHost = new URL(referer).hostname; if (allowedDomains?.length && !allowedDomains.some(domain => refererHost.includes(domain))) { return res.status(403).send("Access denied"); } next?.(); } async function proxyImageDownload(req, res, next) { let { url } = { ...req.query, ...req.body }; if (url) { url = encodeURI(url); try { const { headers, data, status } = await (0, _axios.default)({ method: "get", url, responseType: "stream", timeout: 30 * 1e3, headers: { // Add common headers to mimic browser request "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" } }); if (data && status >= 200 && status < 302) { res.setHeader("Content-Type", headers["content-type"]); data.pipe(res); } else { throw new Error("download image error"); } } catch (err) { logger.error("Proxy url failed: ", err); res.status(400).send("Proxy url failed"); } } else { res.status(400).send('Parameter "url" is required'); } } function setPDFDownloadHeader(req, res) { if (_path.default.extname(req.path) === ".pdf") { const filename = req.query?.filename ?? req?.path; res.setHeader("Content-Disposition", `attachment; ${filename ? `filename="${filename}"` : ""}`); } } const getFileHash = async (filePath, maxBytes = 5 * 1024 * 1024) => { const hash = _crypto.default.createHash("md5"); const readStream = (0, _fs.createReadStream)(filePath, { start: 0, end: maxBytes - 1, highWaterMark: 1024 * 1024 // 1MB chunks }); for await (const chunk of readStream) { hash.update(chunk.toString()); } return hash.digest("hex"); }; exports.getFileHash = getFileHash; async function uploadToMediaKit({ filePath, fileName, base64, extraComponentCallOptions }) { if (!filePath && !base64) { throw new Error("filePath or base64 is required"); } if (base64) { if (!fileName) { throw new Error("fileName is required when base64 is provided"); } const res = await _component.default.call({ name: _constants.ImageBinDid, path: "/api/sdk/uploads", data: { base64, filename: fileName }, ...(0, _omit.default)(extraComponentCallOptions, ["name", "path", "data"]) }); return res; } if (filePath) { const fileStream = (0, _fs.createReadStream)(filePath); const filename = fileName || _path.default.basename(filePath); const form = new _formData.default(); const fileHash = await getFileHash(filePath); form.append("file", fileStream); form.append("filename", filename); form.append("hash", fileHash); const res = await _component.default.call({ name: _constants.ImageBinDid, path: "/api/sdk/uploads", data: form, headers: { "x-component-upload-sig": (0, _verifySign.getSignData)({ data: { filename, hash: fileHash }, method: "POST", url: "/api/sdk/uploads", params: extraComponentCallOptions?.params || {} }).sig, ...extraComponentCallOptions?.headers }, ...(0, _omit.default)(extraComponentCallOptions, ["name", "path", "data", "headers"]) }, { retries: 0 }); return res; } } async function getMediaKitFileStream(filePath) { const fileName = _path.default.basename(filePath); const res = await _component.default.call({ name: _constants.ImageBinDid, path: (0, _urlJoin.default)("/uploads", fileName), responseType: "stream", method: "GET" }); return res; } function calculateCacheControl(maxAge = "365d", immutable = true) { let maxAgeInSeconds = 31536e3; if (typeof maxAge === "string") { try { const milliseconds = (0, _ms.default)(maxAge); maxAgeInSeconds = typeof milliseconds === "number" ? milliseconds / 1e3 : 31536e3; } catch (e) { logger.warn(`Invalid maxAge format: ${maxAge}, using default 1 year (31536000 seconds)`); } } else { maxAgeInSeconds = maxAge; } const cacheControl = `public, max-age=${maxAgeInSeconds}`; const cacheControlImmutable = `${cacheControl}, immutable`; return { cacheControl, cacheControlImmutable, maxAgeInSeconds }; } function serveResource(req, res, next, resource, options = {}) { try { res.setHeader("Content-Type", resource.contentType); res.setHeader("Content-Length", resource.size); res.setHeader("Last-Modified", resource.mtime.toUTCString()); const { cacheControl, cacheControlImmutable } = calculateCacheControl(options.maxAge || "365d", options.immutable !== false); res.setHeader("Cache-Control", options.immutable === false ? cacheControl : cacheControlImmutable); if (options.setHeaders && typeof options.setHeaders === "function") { const statObj = { mtime: resource.mtime, size: resource.size }; options.setHeaders(res, resource.filePath, statObj); } const ifModifiedSince = req.headers["if-modified-since"]; if (ifModifiedSince) { const ifModifiedSinceDate = new Date(ifModifiedSince); if (resource.mtime <= ifModifiedSinceDate) { res.statusCode = 304; res.end(); return; } } const fileStream = (0, _fs.createReadStream)(resource.filePath); fileStream.on("error", error => { logger.error(`Error streaming file ${resource.filePath}:`, error); next(error); }); fileStream.pipe(res); } catch (error) { logger.error("Error serving static file:", error); next(error); } } function scanDirectory(directory, options = {}) { const resourceMap = /* @__PURE__ */new Map(); if (!(0, _fs.existsSync)(directory)) { return resourceMap; } try { const files = (0, _fs.readdirSync)(directory); for (const file of files) { const filePath = (0, _path.join)(directory, file); let stat; try { stat = (0, _fs.statSync)(filePath); if (stat.isDirectory()) continue; } catch (e) { continue; } if (options.whitelist?.length && !options.whitelist.some(ext => file.endsWith(ext))) { continue; } if (options.blacklist?.length && options.blacklist.some(ext => file.endsWith(ext))) { continue; } const contentType = _mimeTypes.default.lookup(filePath) || "application/octet-stream"; resourceMap.set(file, { filePath, dir: directory, originDir: options.originDir || directory, blockletInfo: options.blockletInfo || {}, whitelist: options.whitelist, blacklist: options.blacklist, mtime: stat.mtime, size: stat.size, contentType }); } } catch (err) { logger.error(`Error scanning directory ${directory}:`, err); } return resourceMap; } function getFileNameFromReq(req) { const pathname = req.path || req.url?.split("?")[0]; return _path.default.basename(decodeURIComponent(pathname || "")); } const removeExifFromFile = async filePath => { return new Promise(async (resolve, reject) => { try { (0, _fs.statSync)(filePath); } catch (e) { reject(e); return; } const tempPath = filePath + ".tmp"; const reader = (0, _fs.createReadStream)(filePath); const writer = (0, _fs.createWriteStream)(tempPath); reader.pipe(new _exifBeGone.default()).pipe(writer).on("finish", () => { (0, _fs.renameSync)(tempPath, filePath); resolve(); }).on("error", err => { logger.error("[exif-be-gone] failed", err); (0, _fs.unlinkSync)(tempPath); reject(err); }); }); }; exports.removeExifFromFile = removeExifFromFile;