@blocklet/uploader-server 0.1.90 → 0.1.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ export type DynamicResourcePath = {
2
+ path: string;
3
+ blacklist?: string[];
4
+ whitelist?: string[];
5
+ };
6
+ export type DynamicResourceOptions = {
7
+ componentDid?: string;
8
+ resourcePaths: DynamicResourcePath[];
9
+ watchOptions?: {
10
+ ignorePatterns?: string[];
11
+ persistent?: boolean;
12
+ usePolling?: boolean;
13
+ depth?: number;
14
+ };
15
+ cacheOptions?: {
16
+ maxAge?: string | number;
17
+ immutable?: boolean;
18
+ etag?: boolean;
19
+ lastModified?: boolean;
20
+ };
21
+ onFileChange?: (filePath: string, event: string) => void;
22
+ onReady?: (resourceCount: number) => void;
23
+ setHeaders?: (res: any, filePath: string, stat: any) => void;
24
+ conflictResolution?: 'first-match' | 'last-match' | 'error';
25
+ };
26
+ /**
27
+ * 创建动态资源中间件
28
+ */
29
+ export declare function initDynamicResourceMiddleware(options: DynamicResourceOptions): ((req: any, res: any, next: Function) => any) & {
30
+ cleanup: () => void;
31
+ };
@@ -0,0 +1,266 @@
1
+ import { join, basename } from "path";
2
+ import { watch, existsSync, statSync } from "fs";
3
+ import config from "@blocklet/sdk/lib/config";
4
+ import {
5
+ logger,
6
+ calculateCacheControl,
7
+ serveResource,
8
+ scanDirectory,
9
+ getFileNameFromReq
10
+ } from "../utils.js";
11
+ import { globSync } from "glob";
12
+ export function initDynamicResourceMiddleware(options) {
13
+ if (!options.resourcePaths || !options.resourcePaths.length) {
14
+ throw new Error("resourcePaths is required");
15
+ }
16
+ const dynamicResourceMap = /* @__PURE__ */ new Map();
17
+ const directoryPathConfigMap = /* @__PURE__ */ new Map();
18
+ let watchers = {};
19
+ const debounceMap = /* @__PURE__ */ new Map();
20
+ const DEBOUNCE_TIME = 300;
21
+ const cacheOptions = {
22
+ maxAge: "365d",
23
+ immutable: true,
24
+ ...options.cacheOptions
25
+ };
26
+ const { cacheControl, cacheControlImmutable } = calculateCacheControl(cacheOptions.maxAge, cacheOptions.immutable);
27
+ function shouldIncludeFile(filename, pathConfig) {
28
+ if (pathConfig.whitelist?.length && !pathConfig.whitelist.some((ext) => filename.endsWith(ext))) {
29
+ return false;
30
+ }
31
+ if (pathConfig.blacklist?.length && pathConfig.blacklist.some((ext) => filename.endsWith(ext))) {
32
+ return false;
33
+ }
34
+ return true;
35
+ }
36
+ function watchDirectory(directory, pathConfig, isParent = false) {
37
+ if (watchers[directory]) {
38
+ return watchers[directory];
39
+ }
40
+ try {
41
+ const watchOptions = {
42
+ persistent: options.watchOptions?.persistent !== false,
43
+ recursive: options.watchOptions?.depth !== void 0 ? false : true
44
+ };
45
+ directoryPathConfigMap.set(directory, pathConfig);
46
+ const watcher = watch(directory, watchOptions, (eventType, filename) => {
47
+ if (!filename) return;
48
+ if (options.watchOptions?.ignorePatterns?.some(
49
+ (pattern) => filename.startsWith(pattern) || new RegExp(pattern).test(filename)
50
+ )) {
51
+ return;
52
+ }
53
+ const fullPath = join(directory, filename);
54
+ if (isParent && eventType === "rename" && existsSync(fullPath)) {
55
+ try {
56
+ const stat = statSync(fullPath);
57
+ if (stat.isDirectory()) {
58
+ const dirPattern = pathConfig.path.substring(pathConfig.path.indexOf("*"));
59
+ const regex = new RegExp(dirPattern.replace(/\*/g, ".*"));
60
+ if (regex.test(fullPath)) {
61
+ watchDirectory(fullPath, pathConfig);
62
+ if (debounceMap.has("scan")) {
63
+ clearTimeout(debounceMap.get("scan"));
64
+ }
65
+ debounceMap.set(
66
+ "scan",
67
+ setTimeout(() => {
68
+ scanDirectories();
69
+ debounceMap.delete("scan");
70
+ }, DEBOUNCE_TIME)
71
+ );
72
+ }
73
+ }
74
+ } catch (err) {
75
+ }
76
+ return;
77
+ }
78
+ if (eventType === "change" || eventType === "rename") {
79
+ const baseName = basename(filename);
80
+ const debounceKey = `${directory}:${baseName}`;
81
+ if (debounceMap.has(debounceKey)) {
82
+ clearTimeout(debounceMap.get(debounceKey));
83
+ }
84
+ debounceMap.set(
85
+ debounceKey,
86
+ setTimeout(() => {
87
+ processFileChange(directory, baseName, fullPath, eventType, pathConfig);
88
+ debounceMap.delete(debounceKey);
89
+ }, DEBOUNCE_TIME)
90
+ );
91
+ }
92
+ });
93
+ watchers[directory] = watcher;
94
+ return watcher;
95
+ } catch (err) {
96
+ logger.error(`Error watching directory ${directory}:`, err);
97
+ return null;
98
+ }
99
+ }
100
+ function processFileChange(directory, baseName, fullPath, eventType, pathConfig) {
101
+ if (existsSync(fullPath)) {
102
+ try {
103
+ const stat = statSync(fullPath);
104
+ if (stat.isDirectory()) return;
105
+ if (!shouldIncludeFile(baseName, pathConfig)) {
106
+ return;
107
+ }
108
+ try {
109
+ const resourceFile = scanDirectory(directory, {
110
+ whitelist: pathConfig.whitelist,
111
+ blacklist: pathConfig.blacklist,
112
+ originDir: directory
113
+ }).get(baseName);
114
+ if (resourceFile) {
115
+ let shouldAdd = true;
116
+ if (dynamicResourceMap.has(baseName) && options.conflictResolution) {
117
+ switch (options.conflictResolution) {
118
+ case "last-match":
119
+ break;
120
+ case "error":
121
+ logger.error(`Resource conflict: ${baseName} exists in multiple directories`);
122
+ break;
123
+ case "first-match":
124
+ default:
125
+ shouldAdd = false;
126
+ break;
127
+ }
128
+ }
129
+ if (shouldAdd) {
130
+ dynamicResourceMap.set(baseName, resourceFile);
131
+ if (options.onFileChange) {
132
+ options.onFileChange(fullPath, eventType);
133
+ }
134
+ logger.debug(`Updated resource: ${baseName}`);
135
+ }
136
+ }
137
+ } catch (err) {
138
+ logger.debug(`Error updating resource for ${fullPath}:`, err);
139
+ }
140
+ } catch (err) {
141
+ logger.debug(`Error handling file change for ${fullPath}:`, err);
142
+ }
143
+ } else {
144
+ if (dynamicResourceMap.has(baseName)) {
145
+ dynamicResourceMap.delete(baseName);
146
+ if (options.onFileChange) {
147
+ options.onFileChange(fullPath, "delete");
148
+ }
149
+ logger.debug(`Removed resource: ${baseName}`);
150
+ }
151
+ }
152
+ }
153
+ async function scanDirectories() {
154
+ const initialSize = dynamicResourceMap.size;
155
+ for (const pathConfig of options.resourcePaths) {
156
+ try {
157
+ let directories = [];
158
+ if (pathConfig.path.includes("*")) {
159
+ try {
160
+ const pattern = pathConfig.path;
161
+ const parentDir = pathConfig.path.substring(0, pathConfig.path.indexOf("*")).replace(/\/+$/, "");
162
+ directories = globSync(pattern).filter((dir) => {
163
+ try {
164
+ return existsSync(dir) && statSync(dir).isDirectory();
165
+ } catch (err) {
166
+ return false;
167
+ }
168
+ });
169
+ try {
170
+ watchDirectory(parentDir, pathConfig, true);
171
+ } catch (err) {
172
+ logger.debug(`Error watching parent directory ${parentDir}:`, err);
173
+ }
174
+ } catch (err) {
175
+ logger.error(`Error finding directories for pattern ${pathConfig.path}:`, err);
176
+ const plainPath = pathConfig.path.replace(/\*/g, "");
177
+ if (existsSync(plainPath)) {
178
+ directories.push(plainPath);
179
+ }
180
+ }
181
+ } else {
182
+ if (existsSync(pathConfig.path)) {
183
+ directories.push(pathConfig.path);
184
+ }
185
+ }
186
+ let totalResources = 0;
187
+ for (const directory of directories) {
188
+ watchDirectory(directory, pathConfig);
189
+ const dirMap = scanDirectory(directory, {
190
+ whitelist: pathConfig.whitelist,
191
+ blacklist: pathConfig.blacklist,
192
+ originDir: directory
193
+ });
194
+ if (dirMap.size > 0) {
195
+ Array.from(dirMap.entries()).forEach(([key, value]) => {
196
+ if (dynamicResourceMap.has(key)) {
197
+ switch (options.conflictResolution) {
198
+ case "last-match":
199
+ dynamicResourceMap.set(key, value);
200
+ break;
201
+ case "error":
202
+ logger.error(`Resource conflict: ${key} exists in multiple directories`);
203
+ break;
204
+ case "first-match":
205
+ default:
206
+ break;
207
+ }
208
+ } else {
209
+ dynamicResourceMap.set(key, value);
210
+ totalResources++;
211
+ }
212
+ });
213
+ }
214
+ }
215
+ if (totalResources > 0) {
216
+ logger.info(`Added ${totalResources} resources from ${pathConfig.path} pattern`);
217
+ }
218
+ } catch (err) {
219
+ logger.error(`Error scanning directories for path ${pathConfig.path}:`, err);
220
+ }
221
+ }
222
+ if ((dynamicResourceMap.size !== initialSize || initialSize === 0) && options.onReady) {
223
+ options.onReady(dynamicResourceMap.size);
224
+ }
225
+ return dynamicResourceMap;
226
+ }
227
+ function cleanup() {
228
+ debounceMap.forEach((timer) => clearTimeout(timer));
229
+ debounceMap.clear();
230
+ for (const key in watchers) {
231
+ try {
232
+ watchers[key].close();
233
+ } catch (err) {
234
+ }
235
+ }
236
+ watchers = {};
237
+ dynamicResourceMap.clear();
238
+ directoryPathConfigMap.clear();
239
+ logger.debug("Dynamic resource middleware cleaned up");
240
+ }
241
+ if (options.componentDid && config.env.componentDid !== options.componentDid) {
242
+ const emptyMiddleware = (req, res, next) => next();
243
+ return Object.assign(emptyMiddleware, { cleanup });
244
+ }
245
+ scanDirectories();
246
+ const middleware = (req, res, next) => {
247
+ const fileName = getFileNameFromReq(req);
248
+ try {
249
+ const resource = dynamicResourceMap.get(fileName);
250
+ if (resource) {
251
+ serveResource(req, res, next, resource, {
252
+ ...cacheOptions,
253
+ setHeaders: options.setHeaders,
254
+ cacheControl,
255
+ cacheControlImmutable
256
+ });
257
+ } else {
258
+ next();
259
+ }
260
+ } catch (error) {
261
+ logger.error("Error serving dynamic resource:", error);
262
+ next();
263
+ }
264
+ };
265
+ return Object.assign(middleware, { cleanup });
266
+ }
@@ -6,6 +6,5 @@ type initStaticResourceMiddlewareOptions = {
6
6
  skipRunningCheck?: boolean;
7
7
  };
8
8
  export declare const initStaticResourceMiddleware: ({ options, resourceTypes: _resourceTypes, express, skipRunningCheck: _skipRunningCheck, }?: initStaticResourceMiddlewareOptions) => (req: any, res: any, next: Function) => void;
9
- export declare const getCanUseResources: () => any;
10
9
  export declare const initProxyToMediaKitUploadsMiddleware: ({ options, express }?: any) => (req: any, res: any, next: Function) => Promise<any>;
11
10
  export {};
@@ -4,7 +4,14 @@ import config from "@blocklet/sdk/lib/config";
4
4
  import { getResources } from "@blocklet/sdk/lib/component";
5
5
  import joinUrl from "url-join";
6
6
  import component from "@blocklet/sdk/lib/component";
7
- import { setPDFDownloadHeader, logger } from "../utils.js";
7
+ import {
8
+ setPDFDownloadHeader,
9
+ logger,
10
+ calculateCacheControl,
11
+ serveResource,
12
+ scanDirectory,
13
+ getFileNameFromReq
14
+ } from "../utils.js";
8
15
  import { ImageBinDid } from "../constants.js";
9
16
  const ImgResourceType = "imgpack";
10
17
  let skipRunningCheck = false;
@@ -16,13 +23,14 @@ let resourceTypes = [
16
23
  // can be string or string[]
17
24
  }
18
25
  ];
19
- let canUseResources = [];
26
+ let resourcesMap = /* @__PURE__ */ new Map();
20
27
  export const mappingResource = async () => {
21
28
  try {
22
29
  const resources = getResources({
23
30
  types: resourceTypes,
24
31
  skipRunningCheck
25
32
  });
33
+ let canUseResources = [];
26
34
  canUseResources = resources.map((resource) => {
27
35
  const originDir = resource.path;
28
36
  const resourceType = resourceTypes.find(({ type }) => originDir.endsWith(type));
@@ -38,11 +46,26 @@ export const mappingResource = async () => {
38
46
  blacklist: resourceType.blacklist
39
47
  }));
40
48
  }).filter(Boolean).flat();
41
- logger.info(
42
- "Mapping can use resources count: ",
43
- canUseResources.length
44
- // canUseResources
45
- );
49
+ resourcesMap.clear();
50
+ for (const resource of canUseResources) {
51
+ const { dir, whitelist, blacklist, originDir, blockletInfo } = resource;
52
+ if (existsSync(dir)) {
53
+ try {
54
+ const dirResourceMap = scanDirectory(dir, {
55
+ whitelist,
56
+ blacklist,
57
+ originDir,
58
+ blockletInfo
59
+ });
60
+ for (const [key, value] of dirResourceMap.entries()) {
61
+ resourcesMap.set(key, value);
62
+ }
63
+ } catch (err) {
64
+ logger.error(`Error scanning directory ${dir}:`, err);
65
+ }
66
+ }
67
+ }
68
+ logger.info("Mapping resources: files count:", resourcesMap.size, "directories count:", canUseResources.length);
46
69
  return canUseResources;
47
70
  } catch (error) {
48
71
  logger.error(error);
@@ -56,12 +79,16 @@ events.on(Events.componentStarted, () => mappingResource());
56
79
  events.on(Events.componentStopped, () => mappingResource());
57
80
  events.on(Events.componentUpdated, () => mappingResource());
58
81
  export const initStaticResourceMiddleware = ({
59
- options,
82
+ options = {},
60
83
  resourceTypes: _resourceTypes = resourceTypes,
61
84
  express,
62
85
  skipRunningCheck: _skipRunningCheck
63
86
  } = {}) => {
64
87
  skipRunningCheck = !!_skipRunningCheck;
88
+ const { cacheControl, cacheControlImmutable } = calculateCacheControl(
89
+ options.maxAge || "365d",
90
+ options.immutable !== false
91
+ );
65
92
  if (_resourceTypes?.length > 0) {
66
93
  resourceTypes = _resourceTypes.map((item) => {
67
94
  if (typeof item === "string") {
@@ -78,41 +105,24 @@ export const initStaticResourceMiddleware = ({
78
105
  }
79
106
  mappingResource();
80
107
  return (req, res, next) => {
81
- const fileName = basename(req.path || req.url?.split("?")[0]);
108
+ const fileName = getFileNameFromReq(req);
82
109
  try {
83
- const matchCanUseResourceItem = canUseResources.find((item) => {
84
- const normalizedPath = join(item.dir, fileName);
85
- if (!normalizedPath?.startsWith(item.dir)) {
86
- return false;
87
- }
88
- if (!existsSync(normalizedPath)) {
89
- return false;
90
- }
91
- const { whitelist, blacklist } = item;
92
- if (whitelist?.length && !whitelist.some((ext) => fileName?.endsWith(ext))) {
93
- return false;
94
- }
95
- if (blacklist?.length && blacklist.some((ext) => fileName?.endsWith(ext))) {
96
- return false;
97
- }
98
- return true;
99
- });
100
- if (matchCanUseResourceItem) {
101
- express.static(matchCanUseResourceItem.dir, {
102
- maxAge: "365d",
103
- immutable: true,
104
- index: false,
105
- ...options
106
- })(req, res, next);
110
+ const resource = resourcesMap.get(fileName);
111
+ if (resource) {
112
+ serveResource(req, res, next, resource, {
113
+ ...options,
114
+ cacheControl,
115
+ cacheControlImmutable
116
+ });
107
117
  } else {
108
118
  next();
109
119
  }
110
120
  } catch (error) {
121
+ logger.error("Error serving static file:", error);
111
122
  next();
112
123
  }
113
124
  };
114
125
  };
115
- export const getCanUseResources = () => canUseResources;
116
126
  export const initProxyToMediaKitUploadsMiddleware = ({ options, express } = {}) => {
117
127
  return async (req, res, next) => {
118
128
  if (!component.getComponentWebEndpoint(ImageBinDid)) {
@@ -1,3 +1,4 @@
1
1
  export * from './middlewares/local-storage';
2
2
  export * from './middlewares/companion';
3
3
  export * from './middlewares/static-resource';
4
+ export * from './middlewares/dynamic-resource';
package/es/middlewares.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./middlewares/local-storage.js";
2
2
  export * from "./middlewares/companion.js";
3
3
  export * from "./middlewares/static-resource.js";
4
+ export * from "./middlewares/dynamic-resource.js";
package/es/utils.d.ts CHANGED
@@ -25,4 +25,28 @@ export declare function uploadToMediaKit({ filePath, fileName, base64, extraComp
25
25
  extraComponentCallOptions?: CallComponentOptions;
26
26
  }): Promise<import("axios").AxiosResponse<import("http").IncomingMessage, any> | undefined>;
27
27
  export declare function getMediaKitFileStream(filePath: string): Promise<import("axios").AxiosResponse<import("http").IncomingMessage, any>>;
28
+ export type ResourceFile = {
29
+ filePath: string;
30
+ dir: string;
31
+ originDir: string;
32
+ blockletInfo: any;
33
+ whitelist?: string[];
34
+ blacklist?: string[];
35
+ mtime: Date;
36
+ size: number;
37
+ contentType: string;
38
+ };
39
+ export declare function calculateCacheControl(maxAge?: string | number, immutable?: boolean): {
40
+ cacheControl: string;
41
+ cacheControlImmutable: string;
42
+ maxAgeInSeconds: number;
43
+ };
44
+ export declare function serveResource(req: any, res: any, next: Function, resource: ResourceFile, options?: any): void;
45
+ export declare function scanDirectory(directory: string, options?: {
46
+ whitelist?: string[];
47
+ blacklist?: string[];
48
+ originDir?: string;
49
+ blockletInfo?: any;
50
+ }): Map<string, ResourceFile>;
51
+ export declare function getFileNameFromReq(req: any): any;
28
52
  export {};
package/es/utils.js CHANGED
@@ -9,6 +9,10 @@ import crypto from "crypto";
9
9
  import { getSignData } from "@blocklet/sdk/lib/util/verify-sign";
10
10
  import FormData from "form-data";
11
11
  import omit from "lodash/omit";
12
+ import ms from "ms";
13
+ import mime from "mime-types";
14
+ import { existsSync, readdirSync, statSync } from "fs";
15
+ import { join } from "path";
12
16
  export let logger = console;
13
17
  if (process.env.BLOCKLET_LOG_DIR) {
14
18
  try {
@@ -175,3 +179,101 @@ export async function getMediaKitFileStream(filePath) {
175
179
  });
176
180
  return res;
177
181
  }
182
+ export function calculateCacheControl(maxAge = "365d", immutable = true) {
183
+ let maxAgeInSeconds = 31536e3;
184
+ if (typeof maxAge === "string") {
185
+ try {
186
+ const milliseconds = ms(maxAge);
187
+ maxAgeInSeconds = typeof milliseconds === "number" ? milliseconds / 1e3 : 31536e3;
188
+ } catch (e) {
189
+ logger.warn(`Invalid maxAge format: ${maxAge}, using default 1 year (31536000 seconds)`);
190
+ }
191
+ } else {
192
+ maxAgeInSeconds = maxAge;
193
+ }
194
+ const cacheControl = `public, max-age=${maxAgeInSeconds}`;
195
+ const cacheControlImmutable = `${cacheControl}, immutable`;
196
+ return {
197
+ cacheControl,
198
+ cacheControlImmutable,
199
+ maxAgeInSeconds
200
+ };
201
+ }
202
+ export function serveResource(req, res, next, resource, options = {}) {
203
+ try {
204
+ res.setHeader("Content-Type", resource.contentType);
205
+ res.setHeader("Content-Length", resource.size);
206
+ res.setHeader("Last-Modified", resource.mtime.toUTCString());
207
+ const { cacheControl, cacheControlImmutable } = calculateCacheControl(
208
+ options.maxAge || "365d",
209
+ options.immutable !== false
210
+ );
211
+ res.setHeader("Cache-Control", options.immutable === false ? cacheControl : cacheControlImmutable);
212
+ if (options.setHeaders && typeof options.setHeaders === "function") {
213
+ const statObj = { mtime: resource.mtime, size: resource.size };
214
+ options.setHeaders(res, resource.filePath, statObj);
215
+ }
216
+ const ifModifiedSince = req.headers["if-modified-since"];
217
+ if (ifModifiedSince) {
218
+ const ifModifiedSinceDate = new Date(ifModifiedSince);
219
+ if (resource.mtime <= ifModifiedSinceDate) {
220
+ res.statusCode = 304;
221
+ res.end();
222
+ return;
223
+ }
224
+ }
225
+ const fileStream = createReadStream(resource.filePath);
226
+ fileStream.on("error", (error) => {
227
+ logger.error(`Error streaming file ${resource.filePath}:`, error);
228
+ next(error);
229
+ });
230
+ fileStream.pipe(res);
231
+ } catch (error) {
232
+ logger.error("Error serving static file:", error);
233
+ next(error);
234
+ }
235
+ }
236
+ export function scanDirectory(directory, options = {}) {
237
+ const resourceMap = /* @__PURE__ */ new Map();
238
+ if (!existsSync(directory)) {
239
+ return resourceMap;
240
+ }
241
+ try {
242
+ const files = readdirSync(directory);
243
+ for (const file of files) {
244
+ const filePath = join(directory, file);
245
+ let stat;
246
+ try {
247
+ stat = statSync(filePath);
248
+ if (stat.isDirectory()) continue;
249
+ } catch (e) {
250
+ continue;
251
+ }
252
+ if (options.whitelist?.length && !options.whitelist.some((ext) => file.endsWith(ext))) {
253
+ continue;
254
+ }
255
+ if (options.blacklist?.length && options.blacklist.some((ext) => file.endsWith(ext))) {
256
+ continue;
257
+ }
258
+ const contentType = mime.lookup(filePath) || "application/octet-stream";
259
+ resourceMap.set(file, {
260
+ filePath,
261
+ dir: directory,
262
+ originDir: options.originDir || directory,
263
+ blockletInfo: options.blockletInfo || {},
264
+ whitelist: options.whitelist,
265
+ blacklist: options.blacklist,
266
+ mtime: stat.mtime,
267
+ size: stat.size,
268
+ contentType
269
+ });
270
+ }
271
+ } catch (err) {
272
+ logger.error(`Error scanning directory ${directory}:`, err);
273
+ }
274
+ return resourceMap;
275
+ }
276
+ export function getFileNameFromReq(req) {
277
+ const pathname = req.path || req.url?.split("?")[0];
278
+ return path.basename(decodeURIComponent(pathname || ""));
279
+ }
@@ -0,0 +1,31 @@
1
+ export type DynamicResourcePath = {
2
+ path: string;
3
+ blacklist?: string[];
4
+ whitelist?: string[];
5
+ };
6
+ export type DynamicResourceOptions = {
7
+ componentDid?: string;
8
+ resourcePaths: DynamicResourcePath[];
9
+ watchOptions?: {
10
+ ignorePatterns?: string[];
11
+ persistent?: boolean;
12
+ usePolling?: boolean;
13
+ depth?: number;
14
+ };
15
+ cacheOptions?: {
16
+ maxAge?: string | number;
17
+ immutable?: boolean;
18
+ etag?: boolean;
19
+ lastModified?: boolean;
20
+ };
21
+ onFileChange?: (filePath: string, event: string) => void;
22
+ onReady?: (resourceCount: number) => void;
23
+ setHeaders?: (res: any, filePath: string, stat: any) => void;
24
+ conflictResolution?: 'first-match' | 'last-match' | 'error';
25
+ };
26
+ /**
27
+ * 创建动态资源中间件
28
+ */
29
+ export declare function initDynamicResourceMiddleware(options: DynamicResourceOptions): ((req: any, res: any, next: Function) => any) & {
30
+ cleanup: () => void;
31
+ };
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.initDynamicResourceMiddleware = initDynamicResourceMiddleware;
7
+ var _path = require("path");
8
+ var _fs = require("fs");
9
+ var _config = _interopRequireDefault(require("@blocklet/sdk/lib/config"));
10
+ var _utils = require("../utils");
11
+ var _glob = require("glob");
12
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
+ function initDynamicResourceMiddleware(options) {
14
+ if (!options.resourcePaths || !options.resourcePaths.length) {
15
+ throw new Error("resourcePaths is required");
16
+ }
17
+ const dynamicResourceMap = /* @__PURE__ */new Map();
18
+ const directoryPathConfigMap = /* @__PURE__ */new Map();
19
+ let watchers = {};
20
+ const debounceMap = /* @__PURE__ */new Map();
21
+ const DEBOUNCE_TIME = 300;
22
+ const cacheOptions = {
23
+ maxAge: "365d",
24
+ immutable: true,
25
+ ...options.cacheOptions
26
+ };
27
+ const {
28
+ cacheControl,
29
+ cacheControlImmutable
30
+ } = (0, _utils.calculateCacheControl)(cacheOptions.maxAge, cacheOptions.immutable);
31
+ function shouldIncludeFile(filename, pathConfig) {
32
+ if (pathConfig.whitelist?.length && !pathConfig.whitelist.some(ext => filename.endsWith(ext))) {
33
+ return false;
34
+ }
35
+ if (pathConfig.blacklist?.length && pathConfig.blacklist.some(ext => filename.endsWith(ext))) {
36
+ return false;
37
+ }
38
+ return true;
39
+ }
40
+ function watchDirectory(directory, pathConfig, isParent = false) {
41
+ if (watchers[directory]) {
42
+ return watchers[directory];
43
+ }
44
+ try {
45
+ const watchOptions = {
46
+ persistent: options.watchOptions?.persistent !== false,
47
+ recursive: options.watchOptions?.depth !== void 0 ? false : true
48
+ };
49
+ directoryPathConfigMap.set(directory, pathConfig);
50
+ const watcher = (0, _fs.watch)(directory, watchOptions, (eventType, filename) => {
51
+ if (!filename) return;
52
+ if (options.watchOptions?.ignorePatterns?.some(pattern => filename.startsWith(pattern) || new RegExp(pattern).test(filename))) {
53
+ return;
54
+ }
55
+ const fullPath = (0, _path.join)(directory, filename);
56
+ if (isParent && eventType === "rename" && (0, _fs.existsSync)(fullPath)) {
57
+ try {
58
+ const stat = (0, _fs.statSync)(fullPath);
59
+ if (stat.isDirectory()) {
60
+ const dirPattern = pathConfig.path.substring(pathConfig.path.indexOf("*"));
61
+ const regex = new RegExp(dirPattern.replace(/\*/g, ".*"));
62
+ if (regex.test(fullPath)) {
63
+ watchDirectory(fullPath, pathConfig);
64
+ if (debounceMap.has("scan")) {
65
+ clearTimeout(debounceMap.get("scan"));
66
+ }
67
+ debounceMap.set("scan", setTimeout(() => {
68
+ scanDirectories();
69
+ debounceMap.delete("scan");
70
+ }, DEBOUNCE_TIME));
71
+ }
72
+ }
73
+ } catch (err) {}
74
+ return;
75
+ }
76
+ if (eventType === "change" || eventType === "rename") {
77
+ const baseName = (0, _path.basename)(filename);
78
+ const debounceKey = `${directory}:${baseName}`;
79
+ if (debounceMap.has(debounceKey)) {
80
+ clearTimeout(debounceMap.get(debounceKey));
81
+ }
82
+ debounceMap.set(debounceKey, setTimeout(() => {
83
+ processFileChange(directory, baseName, fullPath, eventType, pathConfig);
84
+ debounceMap.delete(debounceKey);
85
+ }, DEBOUNCE_TIME));
86
+ }
87
+ });
88
+ watchers[directory] = watcher;
89
+ return watcher;
90
+ } catch (err) {
91
+ _utils.logger.error(`Error watching directory ${directory}:`, err);
92
+ return null;
93
+ }
94
+ }
95
+ function processFileChange(directory, baseName, fullPath, eventType, pathConfig) {
96
+ if ((0, _fs.existsSync)(fullPath)) {
97
+ try {
98
+ const stat = (0, _fs.statSync)(fullPath);
99
+ if (stat.isDirectory()) return;
100
+ if (!shouldIncludeFile(baseName, pathConfig)) {
101
+ return;
102
+ }
103
+ try {
104
+ const resourceFile = (0, _utils.scanDirectory)(directory, {
105
+ whitelist: pathConfig.whitelist,
106
+ blacklist: pathConfig.blacklist,
107
+ originDir: directory
108
+ }).get(baseName);
109
+ if (resourceFile) {
110
+ let shouldAdd = true;
111
+ if (dynamicResourceMap.has(baseName) && options.conflictResolution) {
112
+ switch (options.conflictResolution) {
113
+ case "last-match":
114
+ break;
115
+ case "error":
116
+ _utils.logger.error(`Resource conflict: ${baseName} exists in multiple directories`);
117
+ break;
118
+ case "first-match":
119
+ default:
120
+ shouldAdd = false;
121
+ break;
122
+ }
123
+ }
124
+ if (shouldAdd) {
125
+ dynamicResourceMap.set(baseName, resourceFile);
126
+ if (options.onFileChange) {
127
+ options.onFileChange(fullPath, eventType);
128
+ }
129
+ _utils.logger.debug(`Updated resource: ${baseName}`);
130
+ }
131
+ }
132
+ } catch (err) {
133
+ _utils.logger.debug(`Error updating resource for ${fullPath}:`, err);
134
+ }
135
+ } catch (err) {
136
+ _utils.logger.debug(`Error handling file change for ${fullPath}:`, err);
137
+ }
138
+ } else {
139
+ if (dynamicResourceMap.has(baseName)) {
140
+ dynamicResourceMap.delete(baseName);
141
+ if (options.onFileChange) {
142
+ options.onFileChange(fullPath, "delete");
143
+ }
144
+ _utils.logger.debug(`Removed resource: ${baseName}`);
145
+ }
146
+ }
147
+ }
148
+ async function scanDirectories() {
149
+ const initialSize = dynamicResourceMap.size;
150
+ for (const pathConfig of options.resourcePaths) {
151
+ try {
152
+ let directories = [];
153
+ if (pathConfig.path.includes("*")) {
154
+ try {
155
+ const pattern = pathConfig.path;
156
+ const parentDir = pathConfig.path.substring(0, pathConfig.path.indexOf("*")).replace(/\/+$/, "");
157
+ directories = (0, _glob.globSync)(pattern).filter(dir => {
158
+ try {
159
+ return (0, _fs.existsSync)(dir) && (0, _fs.statSync)(dir).isDirectory();
160
+ } catch (err) {
161
+ return false;
162
+ }
163
+ });
164
+ try {
165
+ watchDirectory(parentDir, pathConfig, true);
166
+ } catch (err) {
167
+ _utils.logger.debug(`Error watching parent directory ${parentDir}:`, err);
168
+ }
169
+ } catch (err) {
170
+ _utils.logger.error(`Error finding directories for pattern ${pathConfig.path}:`, err);
171
+ const plainPath = pathConfig.path.replace(/\*/g, "");
172
+ if ((0, _fs.existsSync)(plainPath)) {
173
+ directories.push(plainPath);
174
+ }
175
+ }
176
+ } else {
177
+ if ((0, _fs.existsSync)(pathConfig.path)) {
178
+ directories.push(pathConfig.path);
179
+ }
180
+ }
181
+ let totalResources = 0;
182
+ for (const directory of directories) {
183
+ watchDirectory(directory, pathConfig);
184
+ const dirMap = (0, _utils.scanDirectory)(directory, {
185
+ whitelist: pathConfig.whitelist,
186
+ blacklist: pathConfig.blacklist,
187
+ originDir: directory
188
+ });
189
+ if (dirMap.size > 0) {
190
+ Array.from(dirMap.entries()).forEach(([key, value]) => {
191
+ if (dynamicResourceMap.has(key)) {
192
+ switch (options.conflictResolution) {
193
+ case "last-match":
194
+ dynamicResourceMap.set(key, value);
195
+ break;
196
+ case "error":
197
+ _utils.logger.error(`Resource conflict: ${key} exists in multiple directories`);
198
+ break;
199
+ case "first-match":
200
+ default:
201
+ break;
202
+ }
203
+ } else {
204
+ dynamicResourceMap.set(key, value);
205
+ totalResources++;
206
+ }
207
+ });
208
+ }
209
+ }
210
+ if (totalResources > 0) {
211
+ _utils.logger.info(`Added ${totalResources} resources from ${pathConfig.path} pattern`);
212
+ }
213
+ } catch (err) {
214
+ _utils.logger.error(`Error scanning directories for path ${pathConfig.path}:`, err);
215
+ }
216
+ }
217
+ if ((dynamicResourceMap.size !== initialSize || initialSize === 0) && options.onReady) {
218
+ options.onReady(dynamicResourceMap.size);
219
+ }
220
+ return dynamicResourceMap;
221
+ }
222
+ function cleanup() {
223
+ debounceMap.forEach(timer => clearTimeout(timer));
224
+ debounceMap.clear();
225
+ for (const key in watchers) {
226
+ try {
227
+ watchers[key].close();
228
+ } catch (err) {}
229
+ }
230
+ watchers = {};
231
+ dynamicResourceMap.clear();
232
+ directoryPathConfigMap.clear();
233
+ _utils.logger.debug("Dynamic resource middleware cleaned up");
234
+ }
235
+ if (options.componentDid && _config.default.env.componentDid !== options.componentDid) {
236
+ const emptyMiddleware = (req, res, next) => next();
237
+ return Object.assign(emptyMiddleware, {
238
+ cleanup
239
+ });
240
+ }
241
+ scanDirectories();
242
+ const middleware = (req, res, next) => {
243
+ const fileName = (0, _utils.getFileNameFromReq)(req);
244
+ try {
245
+ const resource = dynamicResourceMap.get(fileName);
246
+ if (resource) {
247
+ (0, _utils.serveResource)(req, res, next, resource, {
248
+ ...cacheOptions,
249
+ setHeaders: options.setHeaders,
250
+ cacheControl,
251
+ cacheControlImmutable
252
+ });
253
+ } else {
254
+ next();
255
+ }
256
+ } catch (error) {
257
+ _utils.logger.error("Error serving dynamic resource:", error);
258
+ next();
259
+ }
260
+ };
261
+ return Object.assign(middleware, {
262
+ cleanup
263
+ });
264
+ }
@@ -6,6 +6,5 @@ type initStaticResourceMiddlewareOptions = {
6
6
  skipRunningCheck?: boolean;
7
7
  };
8
8
  export declare const initStaticResourceMiddleware: ({ options, resourceTypes: _resourceTypes, express, skipRunningCheck: _skipRunningCheck, }?: initStaticResourceMiddlewareOptions) => (req: any, res: any, next: Function) => void;
9
- export declare const getCanUseResources: () => any;
10
9
  export declare const initProxyToMediaKitUploadsMiddleware: ({ options, express }?: any) => (req: any, res: any, next: Function) => Promise<any>;
11
10
  export {};
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.mappingResource = exports.initStaticResourceMiddleware = exports.initProxyToMediaKitUploadsMiddleware = exports.getCanUseResources = void 0;
6
+ exports.mappingResource = exports.initStaticResourceMiddleware = exports.initProxyToMediaKitUploadsMiddleware = void 0;
7
7
  var _fs = require("fs");
8
8
  var _path = require("path");
9
9
  var _config = _interopRequireDefault(require("@blocklet/sdk/lib/config"));
@@ -22,13 +22,14 @@ let resourceTypes = [{
22
22
  folder: ""
23
23
  // can be string or string[]
24
24
  }];
25
- let canUseResources = [];
25
+ let resourcesMap = /* @__PURE__ */new Map();
26
26
  const mappingResource = async () => {
27
27
  try {
28
28
  const resources = (0, _component.getResources)({
29
29
  types: resourceTypes,
30
30
  skipRunningCheck
31
31
  });
32
+ let canUseResources = [];
32
33
  canUseResources = resources.map(resource => {
33
34
  const originDir = resource.path;
34
35
  const resourceType = resourceTypes.find(({
@@ -46,9 +47,32 @@ const mappingResource = async () => {
46
47
  blacklist: resourceType.blacklist
47
48
  }));
48
49
  }).filter(Boolean).flat();
49
- _utils.logger.info("Mapping can use resources count: ", canUseResources.length
50
- // canUseResources
51
- );
50
+ resourcesMap.clear();
51
+ for (const resource of canUseResources) {
52
+ const {
53
+ dir,
54
+ whitelist,
55
+ blacklist,
56
+ originDir,
57
+ blockletInfo
58
+ } = resource;
59
+ if ((0, _fs.existsSync)(dir)) {
60
+ try {
61
+ const dirResourceMap = (0, _utils.scanDirectory)(dir, {
62
+ whitelist,
63
+ blacklist,
64
+ originDir,
65
+ blockletInfo
66
+ });
67
+ for (const [key, value] of dirResourceMap.entries()) {
68
+ resourcesMap.set(key, value);
69
+ }
70
+ } catch (err) {
71
+ _utils.logger.error(`Error scanning directory ${dir}:`, err);
72
+ }
73
+ }
74
+ }
75
+ _utils.logger.info("Mapping resources: files count:", resourcesMap.size, "directories count:", canUseResources.length);
52
76
  return canUseResources;
53
77
  } catch (error) {
54
78
  _utils.logger.error(error);
@@ -66,12 +90,16 @@ events.on(Events.componentStarted, () => mappingResource());
66
90
  events.on(Events.componentStopped, () => mappingResource());
67
91
  events.on(Events.componentUpdated, () => mappingResource());
68
92
  const initStaticResourceMiddleware = ({
69
- options,
93
+ options = {},
70
94
  resourceTypes: _resourceTypes = resourceTypes,
71
95
  express,
72
96
  skipRunningCheck: _skipRunningCheck
73
97
  } = {}) => {
74
98
  skipRunningCheck = !!_skipRunningCheck;
99
+ const {
100
+ cacheControl,
101
+ cacheControlImmutable
102
+ } = (0, _utils.calculateCacheControl)(options.maxAge || "365d", options.immutable !== false);
75
103
  if (_resourceTypes?.length > 0) {
76
104
  resourceTypes = _resourceTypes.map(item => {
77
105
  if (typeof item === "string") {
@@ -88,46 +116,25 @@ const initStaticResourceMiddleware = ({
88
116
  }
89
117
  mappingResource();
90
118
  return (req, res, next) => {
91
- const fileName = (0, _path.basename)(req.path || req.url?.split("?")[0]);
119
+ const fileName = (0, _utils.getFileNameFromReq)(req);
92
120
  try {
93
- const matchCanUseResourceItem = canUseResources.find(item => {
94
- const normalizedPath = (0, _path.join)(item.dir, fileName);
95
- if (!normalizedPath?.startsWith(item.dir)) {
96
- return false;
97
- }
98
- if (!(0, _fs.existsSync)(normalizedPath)) {
99
- return false;
100
- }
101
- const {
102
- whitelist,
103
- blacklist
104
- } = item;
105
- if (whitelist?.length && !whitelist.some(ext => fileName?.endsWith(ext))) {
106
- return false;
107
- }
108
- if (blacklist?.length && blacklist.some(ext => fileName?.endsWith(ext))) {
109
- return false;
110
- }
111
- return true;
112
- });
113
- if (matchCanUseResourceItem) {
114
- express.static(matchCanUseResourceItem.dir, {
115
- maxAge: "365d",
116
- immutable: true,
117
- index: false,
118
- ...options
119
- })(req, res, next);
121
+ const resource = resourcesMap.get(fileName);
122
+ if (resource) {
123
+ (0, _utils.serveResource)(req, res, next, resource, {
124
+ ...options,
125
+ cacheControl,
126
+ cacheControlImmutable
127
+ });
120
128
  } else {
121
129
  next();
122
130
  }
123
131
  } catch (error) {
132
+ _utils.logger.error("Error serving static file:", error);
124
133
  next();
125
134
  }
126
135
  };
127
136
  };
128
137
  exports.initStaticResourceMiddleware = initStaticResourceMiddleware;
129
- const getCanUseResources = () => canUseResources;
130
- exports.getCanUseResources = getCanUseResources;
131
138
  const initProxyToMediaKitUploadsMiddleware = ({
132
139
  options,
133
140
  express
@@ -1,3 +1,4 @@
1
1
  export * from './middlewares/local-storage';
2
2
  export * from './middlewares/companion';
3
3
  export * from './middlewares/static-resource';
4
+ export * from './middlewares/dynamic-resource';
@@ -35,4 +35,15 @@ Object.keys(_staticResource).forEach(function (key) {
35
35
  return _staticResource[key];
36
36
  }
37
37
  });
38
+ });
39
+ var _dynamicResource = require("./middlewares/dynamic-resource");
40
+ Object.keys(_dynamicResource).forEach(function (key) {
41
+ if (key === "default" || key === "__esModule") return;
42
+ if (key in exports && exports[key] === _dynamicResource[key]) return;
43
+ Object.defineProperty(exports, key, {
44
+ enumerable: true,
45
+ get: function () {
46
+ return _dynamicResource[key];
47
+ }
48
+ });
38
49
  });
package/lib/utils.d.ts CHANGED
@@ -25,4 +25,28 @@ export declare function uploadToMediaKit({ filePath, fileName, base64, extraComp
25
25
  extraComponentCallOptions?: CallComponentOptions;
26
26
  }): Promise<import("axios").AxiosResponse<import("http").IncomingMessage, any> | undefined>;
27
27
  export declare function getMediaKitFileStream(filePath: string): Promise<import("axios").AxiosResponse<import("http").IncomingMessage, any>>;
28
+ export type ResourceFile = {
29
+ filePath: string;
30
+ dir: string;
31
+ originDir: string;
32
+ blockletInfo: any;
33
+ whitelist?: string[];
34
+ blacklist?: string[];
35
+ mtime: Date;
36
+ size: number;
37
+ contentType: string;
38
+ };
39
+ export declare function calculateCacheControl(maxAge?: string | number, immutable?: boolean): {
40
+ cacheControl: string;
41
+ cacheControlImmutable: string;
42
+ maxAgeInSeconds: number;
43
+ };
44
+ export declare function serveResource(req: any, res: any, next: Function, resource: ResourceFile, options?: any): void;
45
+ export declare function scanDirectory(directory: string, options?: {
46
+ whitelist?: string[];
47
+ blacklist?: string[];
48
+ originDir?: string;
49
+ blockletInfo?: any;
50
+ }): Map<string, ResourceFile>;
51
+ export declare function getFileNameFromReq(req: any): any;
28
52
  export {};
package/lib/utils.js CHANGED
@@ -3,16 +3,20 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.calculateCacheControl = calculateCacheControl;
6
7
  exports.checkTrustedReferer = checkTrustedReferer;
7
8
  exports.getFileHash = void 0;
9
+ exports.getFileNameFromReq = getFileNameFromReq;
8
10
  exports.getMediaKitFileStream = getMediaKitFileStream;
9
11
  exports.getTrustedDomainsCache = getTrustedDomainsCache;
10
12
  exports.logger = void 0;
11
13
  exports.proxyImageDownload = proxyImageDownload;
14
+ exports.scanDirectory = scanDirectory;
15
+ exports.serveResource = serveResource;
12
16
  exports.setPDFDownloadHeader = setPDFDownloadHeader;
13
17
  exports.uploadToMediaKit = uploadToMediaKit;
14
18
  var _axios = _interopRequireDefault(require("axios"));
15
- var _path = _interopRequireDefault(require("path"));
19
+ var _path = _interopRequireWildcard(require("path"));
16
20
  var _urlJoin = _interopRequireDefault(require("url-join"));
17
21
  var _isbot = require("isbot");
18
22
  var _component = _interopRequireDefault(require("@blocklet/sdk/lib/component"));
@@ -22,6 +26,10 @@ var _crypto = _interopRequireDefault(require("crypto"));
22
26
  var _verifySign = require("@blocklet/sdk/lib/util/verify-sign");
23
27
  var _formData = _interopRequireDefault(require("form-data"));
24
28
  var _omit = _interopRequireDefault(require("lodash/omit"));
29
+ var _ms = _interopRequireDefault(require("ms"));
30
+ var _mimeTypes = _interopRequireDefault(require("mime-types"));
31
+ 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); }
32
+ 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; }
25
33
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
26
34
  let logger = exports.logger = console;
27
35
  if (process.env.BLOCKLET_LOG_DIR) {
@@ -191,4 +199,105 @@ async function getMediaKitFileStream(filePath) {
191
199
  method: "GET"
192
200
  });
193
201
  return res;
202
+ }
203
+ function calculateCacheControl(maxAge = "365d", immutable = true) {
204
+ let maxAgeInSeconds = 31536e3;
205
+ if (typeof maxAge === "string") {
206
+ try {
207
+ const milliseconds = (0, _ms.default)(maxAge);
208
+ maxAgeInSeconds = typeof milliseconds === "number" ? milliseconds / 1e3 : 31536e3;
209
+ } catch (e) {
210
+ logger.warn(`Invalid maxAge format: ${maxAge}, using default 1 year (31536000 seconds)`);
211
+ }
212
+ } else {
213
+ maxAgeInSeconds = maxAge;
214
+ }
215
+ const cacheControl = `public, max-age=${maxAgeInSeconds}`;
216
+ const cacheControlImmutable = `${cacheControl}, immutable`;
217
+ return {
218
+ cacheControl,
219
+ cacheControlImmutable,
220
+ maxAgeInSeconds
221
+ };
222
+ }
223
+ function serveResource(req, res, next, resource, options = {}) {
224
+ try {
225
+ res.setHeader("Content-Type", resource.contentType);
226
+ res.setHeader("Content-Length", resource.size);
227
+ res.setHeader("Last-Modified", resource.mtime.toUTCString());
228
+ const {
229
+ cacheControl,
230
+ cacheControlImmutable
231
+ } = calculateCacheControl(options.maxAge || "365d", options.immutable !== false);
232
+ res.setHeader("Cache-Control", options.immutable === false ? cacheControl : cacheControlImmutable);
233
+ if (options.setHeaders && typeof options.setHeaders === "function") {
234
+ const statObj = {
235
+ mtime: resource.mtime,
236
+ size: resource.size
237
+ };
238
+ options.setHeaders(res, resource.filePath, statObj);
239
+ }
240
+ const ifModifiedSince = req.headers["if-modified-since"];
241
+ if (ifModifiedSince) {
242
+ const ifModifiedSinceDate = new Date(ifModifiedSince);
243
+ if (resource.mtime <= ifModifiedSinceDate) {
244
+ res.statusCode = 304;
245
+ res.end();
246
+ return;
247
+ }
248
+ }
249
+ const fileStream = (0, _fs.createReadStream)(resource.filePath);
250
+ fileStream.on("error", error => {
251
+ logger.error(`Error streaming file ${resource.filePath}:`, error);
252
+ next(error);
253
+ });
254
+ fileStream.pipe(res);
255
+ } catch (error) {
256
+ logger.error("Error serving static file:", error);
257
+ next(error);
258
+ }
259
+ }
260
+ function scanDirectory(directory, options = {}) {
261
+ const resourceMap = /* @__PURE__ */new Map();
262
+ if (!(0, _fs.existsSync)(directory)) {
263
+ return resourceMap;
264
+ }
265
+ try {
266
+ const files = (0, _fs.readdirSync)(directory);
267
+ for (const file of files) {
268
+ const filePath = (0, _path.join)(directory, file);
269
+ let stat;
270
+ try {
271
+ stat = (0, _fs.statSync)(filePath);
272
+ if (stat.isDirectory()) continue;
273
+ } catch (e) {
274
+ continue;
275
+ }
276
+ if (options.whitelist?.length && !options.whitelist.some(ext => file.endsWith(ext))) {
277
+ continue;
278
+ }
279
+ if (options.blacklist?.length && options.blacklist.some(ext => file.endsWith(ext))) {
280
+ continue;
281
+ }
282
+ const contentType = _mimeTypes.default.lookup(filePath) || "application/octet-stream";
283
+ resourceMap.set(file, {
284
+ filePath,
285
+ dir: directory,
286
+ originDir: options.originDir || directory,
287
+ blockletInfo: options.blockletInfo || {},
288
+ whitelist: options.whitelist,
289
+ blacklist: options.blacklist,
290
+ mtime: stat.mtime,
291
+ size: stat.size,
292
+ contentType
293
+ });
294
+ }
295
+ } catch (err) {
296
+ logger.error(`Error scanning directory ${directory}:`, err);
297
+ }
298
+ return resourceMap;
299
+ }
300
+ function getFileNameFromReq(req) {
301
+ const pathname = req.path || req.url?.split("?")[0];
302
+ return _path.default.basename(decodeURIComponent(pathname || ""));
194
303
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/uploader-server",
3
- "version": "0.1.90",
3
+ "version": "0.1.92",
4
4
  "description": "blocklet upload server",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -59,8 +59,10 @@
59
59
  "axios": "^1.7.8",
60
60
  "body-parser": "^1.20.3",
61
61
  "express-session": "1.17.3",
62
+ "glob": "^11.0.1",
62
63
  "isbot": "^5.1.17",
63
64
  "mime-types": "^2.1.35",
65
+ "ms": "^2.1.3",
64
66
  "p-queue": "6.6.2",
65
67
  "ufo": "^1.5.4",
66
68
  "url-join": "^4.0.1"