@blocklet/uploader-server
Version:
blocklet upload server
267 lines (266 loc) • 9.38 kB
JavaScript
import { join, basename } from "path";
import { watch, existsSync, statSync } from "fs";
import config from "@blocklet/sdk/lib/config";
import {
logger,
calculateCacheControl,
serveResource,
scanDirectory,
getFileNameFromReq
} from "../utils.js";
import { globSync } from "glob";
export function initDynamicResourceMiddleware(options) {
if (!options.resourcePaths || !options.resourcePaths.length) {
throw new Error("resourcePaths is required");
}
const dynamicResourceMap = /* @__PURE__ */ new Map();
const directoryPathConfigMap = /* @__PURE__ */ new Map();
let watchers = {};
const debounceMap = /* @__PURE__ */ new Map();
const DEBOUNCE_TIME = 300;
const cacheOptions = {
maxAge: "365d",
immutable: true,
...options.cacheOptions
};
const { cacheControl, cacheControlImmutable } = calculateCacheControl(cacheOptions.maxAge, cacheOptions.immutable);
function shouldIncludeFile(filename, pathConfig) {
if (pathConfig.whitelist?.length && !pathConfig.whitelist.some((ext) => filename.endsWith(ext))) {
return false;
}
if (pathConfig.blacklist?.length && pathConfig.blacklist.some((ext) => filename.endsWith(ext))) {
return false;
}
return true;
}
function watchDirectory(directory, pathConfig, isParent = false) {
if (watchers[directory]) {
return watchers[directory];
}
try {
const watchOptions = {
persistent: options.watchOptions?.persistent !== false,
recursive: options.watchOptions?.depth !== void 0 ? false : true
};
directoryPathConfigMap.set(directory, pathConfig);
const watcher = watch(directory, watchOptions, (eventType, filename) => {
if (!filename) return;
if (options.watchOptions?.ignorePatterns?.some(
(pattern) => filename.startsWith(pattern) || new RegExp(pattern).test(filename)
)) {
return;
}
const fullPath = join(directory, filename);
if (isParent && eventType === "rename" && existsSync(fullPath)) {
try {
const stat = statSync(fullPath);
if (stat.isDirectory()) {
const dirPattern = pathConfig.path.substring(pathConfig.path.indexOf("*"));
const regex = new RegExp(dirPattern.replace(/\*/g, ".*"));
if (regex.test(fullPath)) {
watchDirectory(fullPath, pathConfig);
if (debounceMap.has("scan")) {
clearTimeout(debounceMap.get("scan"));
}
debounceMap.set(
"scan",
setTimeout(() => {
scanDirectories();
debounceMap.delete("scan");
}, DEBOUNCE_TIME)
);
}
}
} catch (err) {
}
return;
}
if (eventType === "change" || eventType === "rename") {
const baseName = basename(filename);
const debounceKey = `${directory}:${baseName}`;
if (debounceMap.has(debounceKey)) {
clearTimeout(debounceMap.get(debounceKey));
}
debounceMap.set(
debounceKey,
setTimeout(() => {
processFileChange(directory, baseName, fullPath, eventType, pathConfig);
debounceMap.delete(debounceKey);
}, DEBOUNCE_TIME)
);
}
});
watchers[directory] = watcher;
return watcher;
} catch (err) {
logger.error(`Error watching directory ${directory}:`, err);
return null;
}
}
function processFileChange(directory, baseName, fullPath, eventType, pathConfig) {
if (existsSync(fullPath)) {
try {
const stat = statSync(fullPath);
if (stat.isDirectory()) return;
if (!shouldIncludeFile(baseName, pathConfig)) {
return;
}
try {
const resourceFile = scanDirectory(directory, {
whitelist: pathConfig.whitelist,
blacklist: pathConfig.blacklist,
originDir: directory
}).get(baseName);
if (resourceFile) {
let shouldAdd = true;
if (dynamicResourceMap.has(baseName) && options.conflictResolution) {
switch (options.conflictResolution) {
case "last-match":
break;
case "error":
logger.error(`Resource conflict: ${baseName} exists in multiple directories`);
break;
case "first-match":
default:
shouldAdd = false;
break;
}
}
if (shouldAdd) {
dynamicResourceMap.set(baseName, resourceFile);
if (options.onFileChange) {
options.onFileChange(fullPath, eventType);
}
logger.debug(`Updated resource: ${baseName}`);
}
}
} catch (err) {
logger.debug(`Error updating resource for ${fullPath}:`, err);
}
} catch (err) {
logger.debug(`Error handling file change for ${fullPath}:`, err);
}
} else {
if (dynamicResourceMap.has(baseName)) {
dynamicResourceMap.delete(baseName);
if (options.onFileChange) {
options.onFileChange(fullPath, "delete");
}
logger.debug(`Removed resource: ${baseName}`);
}
}
}
async function scanDirectories() {
const initialSize = dynamicResourceMap.size;
for (const pathConfig of options.resourcePaths) {
try {
let directories = [];
if (pathConfig.path.includes("*")) {
try {
const pattern = pathConfig.path;
const parentDir = pathConfig.path.substring(0, pathConfig.path.indexOf("*")).replace(/\/+$/, "");
directories = globSync(pattern).filter((dir) => {
try {
return existsSync(dir) && statSync(dir).isDirectory();
} catch (err) {
return false;
}
});
try {
watchDirectory(parentDir, pathConfig, true);
} catch (err) {
logger.debug(`Error watching parent directory ${parentDir}:`, err);
}
} catch (err) {
logger.error(`Error finding directories for pattern ${pathConfig.path}:`, err);
const plainPath = pathConfig.path.replace(/\*/g, "");
if (existsSync(plainPath)) {
directories.push(plainPath);
}
}
} else {
if (existsSync(pathConfig.path)) {
directories.push(pathConfig.path);
}
}
let totalResources = 0;
for (const directory of directories) {
watchDirectory(directory, pathConfig);
const dirMap = scanDirectory(directory, {
whitelist: pathConfig.whitelist,
blacklist: pathConfig.blacklist,
originDir: directory
});
if (dirMap.size > 0) {
Array.from(dirMap.entries()).forEach(([key, value]) => {
if (dynamicResourceMap.has(key)) {
switch (options.conflictResolution) {
case "last-match":
dynamicResourceMap.set(key, value);
break;
case "error":
logger.error(`Resource conflict: ${key} exists in multiple directories`);
break;
case "first-match":
default:
break;
}
} else {
dynamicResourceMap.set(key, value);
totalResources++;
}
});
}
}
if (totalResources > 0) {
logger.info(`Added ${totalResources} resources from ${pathConfig.path} pattern`);
}
} catch (err) {
logger.error(`Error scanning directories for path ${pathConfig.path}:`, err);
}
}
if ((dynamicResourceMap.size !== initialSize || initialSize === 0) && options.onReady) {
options.onReady(dynamicResourceMap.size);
}
return dynamicResourceMap;
}
function cleanup() {
debounceMap.forEach((timer) => clearTimeout(timer));
debounceMap.clear();
for (const key in watchers) {
try {
watchers[key].close();
} catch (err) {
}
}
watchers = {};
dynamicResourceMap.clear();
directoryPathConfigMap.clear();
logger.debug("Dynamic resource middleware cleaned up");
}
if (options.componentDid && config.env.componentDid !== options.componentDid) {
const emptyMiddleware = (req, res, next) => next();
return Object.assign(emptyMiddleware, { cleanup });
}
scanDirectories();
const middleware = (req, res, next) => {
const fileName = getFileNameFromReq(req);
try {
const resource = dynamicResourceMap.get(fileName);
if (resource) {
serveResource(req, res, next, resource, {
...cacheOptions,
setHeaders: options.setHeaders,
cacheControl,
cacheControlImmutable
});
} else {
next();
}
} catch (error) {
logger.error("Error serving dynamic resource:", error);
next();
}
};
return Object.assign(middleware, { cleanup });
}