@blocklet/uploader-server 0.1.91 → 0.1.93
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.js +26 -69
- 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.js +17 -70
- 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 +6 -5
|
@@ -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
|
+
}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
2
|
import { join, basename } from "path";
|
|
3
3
|
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
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
setPDFDownloadHeader,
|
|
9
|
+
logger,
|
|
10
|
+
calculateCacheControl,
|
|
11
|
+
serveResource,
|
|
12
|
+
scanDirectory,
|
|
13
|
+
getFileNameFromReq
|
|
14
|
+
} from "../utils.js";
|
|
9
15
|
import { ImageBinDid } from "../constants.js";
|
|
10
|
-
import ms from "ms";
|
|
11
16
|
const ImgResourceType = "imgpack";
|
|
12
17
|
let skipRunningCheck = false;
|
|
13
18
|
let resourceTypes = [
|
|
@@ -46,34 +51,14 @@ export const mappingResource = async () => {
|
|
|
46
51
|
const { dir, whitelist, blacklist, originDir, blockletInfo } = resource;
|
|
47
52
|
if (existsSync(dir)) {
|
|
48
53
|
try {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
if (whitelist?.length && !whitelist.some((ext) => file.endsWith(ext))) {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (blacklist?.length && blacklist.some((ext) => file.endsWith(ext))) {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const contentType = mime.lookup(filePath) || "application/octet-stream";
|
|
66
|
-
resourcesMap.set(file, {
|
|
67
|
-
filePath,
|
|
68
|
-
dir,
|
|
69
|
-
originDir,
|
|
70
|
-
blockletInfo,
|
|
71
|
-
whitelist,
|
|
72
|
-
blacklist,
|
|
73
|
-
mtime: stat.mtime,
|
|
74
|
-
size: stat.size,
|
|
75
|
-
contentType
|
|
76
|
-
});
|
|
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);
|
|
77
62
|
}
|
|
78
63
|
} catch (err) {
|
|
79
64
|
logger.error(`Error scanning directory ${dir}:`, err);
|
|
@@ -100,20 +85,10 @@ export const initStaticResourceMiddleware = ({
|
|
|
100
85
|
skipRunningCheck: _skipRunningCheck
|
|
101
86
|
} = {}) => {
|
|
102
87
|
skipRunningCheck = !!_skipRunningCheck;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const milliseconds = ms(maxAge);
|
|
108
|
-
maxAgeInSeconds = typeof milliseconds === "number" ? milliseconds / 1e3 : 31536e3;
|
|
109
|
-
} catch (e) {
|
|
110
|
-
logger.warn(`Invalid maxAge format: ${maxAge}, using default 1 year (31536000 seconds)`);
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
maxAgeInSeconds = maxAge;
|
|
114
|
-
}
|
|
115
|
-
const cacheControl = `public, max-age=${maxAgeInSeconds}`;
|
|
116
|
-
const cacheControlImmutable = `${cacheControl}, immutable`;
|
|
88
|
+
const { cacheControl, cacheControlImmutable } = calculateCacheControl(
|
|
89
|
+
options.maxAge || "365d",
|
|
90
|
+
options.immutable !== false
|
|
91
|
+
);
|
|
117
92
|
if (_resourceTypes?.length > 0) {
|
|
118
93
|
resourceTypes = _resourceTypes.map((item) => {
|
|
119
94
|
if (typeof item === "string") {
|
|
@@ -130,33 +105,15 @@ export const initStaticResourceMiddleware = ({
|
|
|
130
105
|
}
|
|
131
106
|
mappingResource();
|
|
132
107
|
return (req, res, next) => {
|
|
133
|
-
const fileName =
|
|
108
|
+
const fileName = getFileNameFromReq(req);
|
|
134
109
|
try {
|
|
135
110
|
const resource = resourcesMap.get(fileName);
|
|
136
111
|
if (resource) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (options.setHeaders && typeof options.setHeaders === "function") {
|
|
142
|
-
const statObj = { mtime: resource.mtime, size: resource.size };
|
|
143
|
-
options.setHeaders(res, resource.filePath, statObj);
|
|
144
|
-
}
|
|
145
|
-
const ifModifiedSince = req.headers["if-modified-since"];
|
|
146
|
-
if (ifModifiedSince) {
|
|
147
|
-
const ifModifiedSinceDate = new Date(ifModifiedSince);
|
|
148
|
-
if (resource.mtime <= ifModifiedSinceDate) {
|
|
149
|
-
res.statusCode = 304;
|
|
150
|
-
res.end();
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
const fileStream = createReadStream(resource.filePath);
|
|
155
|
-
fileStream.on("error", (error) => {
|
|
156
|
-
logger.error(`Error streaming file ${resource.filePath}:`, error);
|
|
157
|
-
next(error);
|
|
112
|
+
serveResource(req, res, next, resource, {
|
|
113
|
+
...options,
|
|
114
|
+
cacheControl,
|
|
115
|
+
cacheControlImmutable
|
|
158
116
|
});
|
|
159
|
-
fileStream.pipe(res);
|
|
160
117
|
} else {
|
|
161
118
|
next();
|
|
162
119
|
}
|
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
|
+
}
|
|
@@ -9,10 +9,8 @@ var _path = require("path");
|
|
|
9
9
|
var _config = _interopRequireDefault(require("@blocklet/sdk/lib/config"));
|
|
10
10
|
var _component = _interopRequireWildcard(require("@blocklet/sdk/lib/component"));
|
|
11
11
|
var _urlJoin = _interopRequireDefault(require("url-join"));
|
|
12
|
-
var _mimeTypes = _interopRequireDefault(require("mime-types"));
|
|
13
12
|
var _utils = require("../utils");
|
|
14
13
|
var _constants = require("../constants");
|
|
15
|
-
var _ms = _interopRequireDefault(require("ms"));
|
|
16
14
|
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); }
|
|
17
15
|
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; }
|
|
18
16
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -60,34 +58,14 @@ const mappingResource = async () => {
|
|
|
60
58
|
} = resource;
|
|
61
59
|
if ((0, _fs.existsSync)(dir)) {
|
|
62
60
|
try {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (whitelist?.length && !whitelist.some(ext => file.endsWith(ext))) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (blacklist?.length && blacklist.some(ext => file.endsWith(ext))) {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
const contentType = _mimeTypes.default.lookup(filePath) || "application/octet-stream";
|
|
80
|
-
resourcesMap.set(file, {
|
|
81
|
-
filePath,
|
|
82
|
-
dir,
|
|
83
|
-
originDir,
|
|
84
|
-
blockletInfo,
|
|
85
|
-
whitelist,
|
|
86
|
-
blacklist,
|
|
87
|
-
mtime: stat.mtime,
|
|
88
|
-
size: stat.size,
|
|
89
|
-
contentType
|
|
90
|
-
});
|
|
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);
|
|
91
69
|
}
|
|
92
70
|
} catch (err) {
|
|
93
71
|
_utils.logger.error(`Error scanning directory ${dir}:`, err);
|
|
@@ -118,20 +96,10 @@ const initStaticResourceMiddleware = ({
|
|
|
118
96
|
skipRunningCheck: _skipRunningCheck
|
|
119
97
|
} = {}) => {
|
|
120
98
|
skipRunningCheck = !!_skipRunningCheck;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const milliseconds = (0, _ms.default)(maxAge);
|
|
126
|
-
maxAgeInSeconds = typeof milliseconds === "number" ? milliseconds / 1e3 : 31536e3;
|
|
127
|
-
} catch (e) {
|
|
128
|
-
_utils.logger.warn(`Invalid maxAge format: ${maxAge}, using default 1 year (31536000 seconds)`);
|
|
129
|
-
}
|
|
130
|
-
} else {
|
|
131
|
-
maxAgeInSeconds = maxAge;
|
|
132
|
-
}
|
|
133
|
-
const cacheControl = `public, max-age=${maxAgeInSeconds}`;
|
|
134
|
-
const cacheControlImmutable = `${cacheControl}, immutable`;
|
|
99
|
+
const {
|
|
100
|
+
cacheControl,
|
|
101
|
+
cacheControlImmutable
|
|
102
|
+
} = (0, _utils.calculateCacheControl)(options.maxAge || "365d", options.immutable !== false);
|
|
135
103
|
if (_resourceTypes?.length > 0) {
|
|
136
104
|
resourceTypes = _resourceTypes.map(item => {
|
|
137
105
|
if (typeof item === "string") {
|
|
@@ -148,36 +116,15 @@ const initStaticResourceMiddleware = ({
|
|
|
148
116
|
}
|
|
149
117
|
mappingResource();
|
|
150
118
|
return (req, res, next) => {
|
|
151
|
-
const fileName = (0,
|
|
119
|
+
const fileName = (0, _utils.getFileNameFromReq)(req);
|
|
152
120
|
try {
|
|
153
121
|
const resource = resourcesMap.get(fileName);
|
|
154
122
|
if (resource) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (options.setHeaders && typeof options.setHeaders === "function") {
|
|
160
|
-
const statObj = {
|
|
161
|
-
mtime: resource.mtime,
|
|
162
|
-
size: resource.size
|
|
163
|
-
};
|
|
164
|
-
options.setHeaders(res, resource.filePath, statObj);
|
|
165
|
-
}
|
|
166
|
-
const ifModifiedSince = req.headers["if-modified-since"];
|
|
167
|
-
if (ifModifiedSince) {
|
|
168
|
-
const ifModifiedSinceDate = new Date(ifModifiedSince);
|
|
169
|
-
if (resource.mtime <= ifModifiedSinceDate) {
|
|
170
|
-
res.statusCode = 304;
|
|
171
|
-
res.end();
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
const fileStream = (0, _fs.createReadStream)(resource.filePath);
|
|
176
|
-
fileStream.on("error", error => {
|
|
177
|
-
_utils.logger.error(`Error streaming file ${resource.filePath}:`, error);
|
|
178
|
-
next(error);
|
|
123
|
+
(0, _utils.serveResource)(req, res, next, resource, {
|
|
124
|
+
...options,
|
|
125
|
+
cacheControl,
|
|
126
|
+
cacheControlImmutable
|
|
179
127
|
});
|
|
180
|
-
fileStream.pipe(res);
|
|
181
128
|
} else {
|
|
182
129
|
next();
|
|
183
130
|
}
|
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.93",
|
|
4
4
|
"description": "blocklet upload server",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -49,16 +49,17 @@
|
|
|
49
49
|
"axios": "^1.7.8"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@abtnode/cron": "^1.16.
|
|
53
|
-
"@blocklet/constant": "^1.16.
|
|
54
|
-
"@blocklet/logger": "^1.16.
|
|
55
|
-
"@blocklet/sdk": "^1.16.
|
|
52
|
+
"@abtnode/cron": "^1.16.41",
|
|
53
|
+
"@blocklet/constant": "^1.16.41",
|
|
54
|
+
"@blocklet/logger": "^1.16.41",
|
|
55
|
+
"@blocklet/sdk": "^1.16.41",
|
|
56
56
|
"@tus/file-store": "1.0.0",
|
|
57
57
|
"@tus/server": "1.0.0",
|
|
58
58
|
"@uppy/companion": "4.15.1",
|
|
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",
|
|
64
65
|
"ms": "^2.1.3",
|