UNPKG

@blocklet/uploader-server

Version:

blocklet upload server

267 lines (266 loc) 9.38 kB
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 }); }