@blocklet/uploader-server
Version:
blocklet upload server
335 lines (334 loc) • 11.7 kB
JavaScript
;
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;