@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.
- package/es/middlewares/dynamic-resource.d.ts +31 -0
- package/es/middlewares/dynamic-resource.js +266 -0
- package/es/middlewares/static-resource.d.ts +0 -1
- package/es/middlewares/static-resource.js +44 -34
- package/es/middlewares.d.ts +1 -0
- package/es/middlewares.js +1 -0
- package/es/utils.d.ts +24 -0
- package/es/utils.js +102 -0
- package/lib/middlewares/dynamic-resource.d.ts +31 -0
- package/lib/middlewares/dynamic-resource.js +264 -0
- package/lib/middlewares/static-resource.d.ts +0 -1
- package/lib/middlewares/static-resource.js +43 -36
- package/lib/middlewares.d.ts +1 -0
- package/lib/middlewares.js +11 -0
- package/lib/utils.d.ts +24 -0
- package/lib/utils.js +110 -1
- package/package.json +3 -1
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 =
|
|
108
|
+
const fileName = getFileNameFromReq(req);
|
|
82
109
|
try {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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)) {
|
package/es/middlewares.d.ts
CHANGED
package/es/middlewares.js
CHANGED
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 =
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
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,
|
|
119
|
+
const fileName = (0, _utils.getFileNameFromReq)(req);
|
|
92
120
|
try {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
package/lib/middlewares.d.ts
CHANGED
package/lib/middlewares.js
CHANGED
|
@@ -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 =
|
|
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.
|
|
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"
|