@cocreate/file-server 1.18.10 → 1.19.1
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/.github/workflows/automated.yml +5 -5
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/index.js +461 -458
|
@@ -22,13 +22,13 @@ jobs:
|
|
|
22
22
|
runs-on: ubuntu-latest
|
|
23
23
|
steps:
|
|
24
24
|
- name: Checkout
|
|
25
|
-
uses: actions/checkout@
|
|
25
|
+
uses: actions/checkout@v4
|
|
26
26
|
- name: Setup Node.js
|
|
27
|
-
uses: actions/setup-node@
|
|
27
|
+
uses: actions/setup-node@v4
|
|
28
28
|
with:
|
|
29
|
-
node-version:
|
|
29
|
+
node-version: 22 # Required for the latest semantic-release plugins
|
|
30
30
|
- name: Semantic Release
|
|
31
|
-
uses: cycjimmy/semantic-release-action@
|
|
31
|
+
uses: cycjimmy/semantic-release-action@v4 # Update to v4 for better Node 20+ support
|
|
32
32
|
id: semantic
|
|
33
33
|
with:
|
|
34
34
|
extra_plugins: |
|
|
@@ -36,7 +36,7 @@ jobs:
|
|
|
36
36
|
@semantic-release/git
|
|
37
37
|
@semantic-release/github
|
|
38
38
|
env:
|
|
39
|
-
GITHUB_TOKEN: "${{ secrets.
|
|
39
|
+
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" # Use the built-in token if possible
|
|
40
40
|
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
|
|
41
41
|
outputs:
|
|
42
42
|
new_release_published: "${{ steps.semantic.outputs.new_release_published }}"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [1.19.1](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.19.0...v1.19.1) (2026-04-15)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* video and audio handling ([e7b67c6](https://github.com/CoCreate-app/CoCreate-file-server/commit/e7b67c66930d309b1deeefda5720134106de2b91))
|
|
7
|
+
|
|
8
|
+
# [1.19.0](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.18.10...v1.19.0) (2026-04-08)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* support ico images ([cd9720d](https://github.com/CoCreate-app/CoCreate-file-server/commit/cd9720df7c534728a5732ea4bd44e9ce85ed7a74))
|
|
14
|
+
|
|
1
15
|
## [1.18.10](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.18.9...v1.18.10) (2025-11-17)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -21,463 +21,466 @@
|
|
|
21
21
|
// For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
|
|
22
22
|
|
|
23
23
|
class CoCreateFileSystem {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
24
|
+
constructor(render, sitemap) {
|
|
25
|
+
this.render = render;
|
|
26
|
+
this.sitemap = sitemap;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async send(req, res, crud, organization, urlObject) {
|
|
30
|
+
try {
|
|
31
|
+
const hostname = urlObject.hostname;
|
|
32
|
+
|
|
33
|
+
// --- determine preferred color scheme from browser headers ---
|
|
34
|
+
let theme;
|
|
35
|
+
const headerValue =
|
|
36
|
+
(req && req.headers && (
|
|
37
|
+
req.headers["sec-ch-prefers-color-scheme"] ||
|
|
38
|
+
req.headers["prefers-color-scheme"] ||
|
|
39
|
+
req.headers["x-prefers-color-scheme"] ||
|
|
40
|
+
req.headers["x-color-scheme"]
|
|
41
|
+
));
|
|
42
|
+
if (typeof headerValue === "string") {
|
|
43
|
+
// take first token, strip quotes, normalize
|
|
44
|
+
const token = headerValue.split(",")[0].trim().replace(/^"|"$/g, "").toLowerCase();
|
|
45
|
+
if (token === "dark" || token === "light") theme = token;
|
|
46
|
+
}
|
|
47
|
+
// optional: expose detected theme to clients / downstream code
|
|
48
|
+
if (theme) res.setHeader("X-Preferred-Color-Scheme", theme);
|
|
49
|
+
// --- end theme detection ---
|
|
50
|
+
|
|
51
|
+
let data = {
|
|
52
|
+
method: "object.read",
|
|
53
|
+
host: hostname,
|
|
54
|
+
array: "files",
|
|
55
|
+
$filter: {
|
|
56
|
+
query: {
|
|
57
|
+
host: { $in: [hostname, "*"] }
|
|
58
|
+
},
|
|
59
|
+
limit: 1
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let organization_id;
|
|
64
|
+
if (!organization || organization.error) {
|
|
65
|
+
let hostNotFound = await getDefaultFile("/hostNotFound.html");
|
|
66
|
+
return sendResponse(hostNotFound.object[0].src, 404, {
|
|
67
|
+
"Content-Type": "text/html"
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
organization_id = organization._id;
|
|
72
|
+
data.organization_id = organization_id;
|
|
73
|
+
|
|
74
|
+
res.setHeader("organization", organization_id);
|
|
75
|
+
res.setHeader("storage", !!organization.storage);
|
|
76
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
77
|
+
res.setHeader("Access-Control-Allow-Methods", "");
|
|
78
|
+
res.setHeader(
|
|
79
|
+
"Access-Control-Allow-Headers",
|
|
80
|
+
"Content-Type, Authorization"
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
let active = crud.wsManager.organizations.get(organization_id);
|
|
84
|
+
if (active === false) {
|
|
85
|
+
let balanceFalse = await getDefaultFile("/balanceFalse.html");
|
|
86
|
+
return sendResponse(balanceFalse.object[0].src, 403, {
|
|
87
|
+
"Content-Type": "text/html",
|
|
88
|
+
"Account-Balance": "false",
|
|
89
|
+
storage: organization.storage
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let parameters = urlObject.searchParams;
|
|
94
|
+
if (parameters.size) {
|
|
95
|
+
console.log("parameters", parameters);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let pathname = urlObject.pathname;
|
|
99
|
+
|
|
100
|
+
if (pathname.endsWith("/")) {
|
|
101
|
+
pathname += "index.html";
|
|
102
|
+
} else if (!pathname.startsWith("/.well-known/acme-challenge")) {
|
|
103
|
+
let directory = pathname.split("/").slice(-1)[0];
|
|
104
|
+
if (!directory.includes(".")) pathname += "/index.html";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Match both /en/ and /en-US/ style URLs
|
|
108
|
+
const bcp47Regex = /^\/([a-zA-Z]{2,3}(?:-[a-zA-Z0-9]{2,8})?)\//;
|
|
109
|
+
const langMatch = pathname.match(bcp47Regex);
|
|
110
|
+
let lang, langRegion;
|
|
111
|
+
if (langMatch) {
|
|
112
|
+
langRegion = langMatch[1];
|
|
113
|
+
lang = langRegion.split("-")[0]; // Get just the base language (e.g., 'en', 'es', 'fr')
|
|
114
|
+
let basePathname = pathname.replace("/" + langRegion, "");
|
|
115
|
+
data.$filter.query.pathname = basePathname;
|
|
116
|
+
} else {
|
|
117
|
+
// No language in URL, try Accept-Language header
|
|
118
|
+
let acceptLang = req.headers && req.headers["accept-language"];
|
|
119
|
+
if (acceptLang) {
|
|
120
|
+
// Parse the Accept-Language header, get the first language
|
|
121
|
+
let preferred = acceptLang.split(",")[0].trim();
|
|
122
|
+
if (preferred) {
|
|
123
|
+
lang = preferred.split("-")[0];
|
|
124
|
+
langRegion = preferred;
|
|
125
|
+
// --- BEGIN OPTIONAL REDIRECT ---
|
|
126
|
+
// Uncomment to enable automatic redirect to language-specific path
|
|
127
|
+
// let newPath = `/${preferred}${pathname.startsWith('/') ? '' : '/'}${pathname.replace(/^\//, '')}`;
|
|
128
|
+
// res.writeHead(302, { Location: newPath });
|
|
129
|
+
// return res.end();
|
|
130
|
+
// --- END OPTIONAL REDIRECT ---
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
data.$filter.query.pathname = pathname;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let file;
|
|
137
|
+
if (
|
|
138
|
+
pathname.startsWith("/dist") ||
|
|
139
|
+
pathname.startsWith("/admin") ||
|
|
140
|
+
[
|
|
141
|
+
"/403.html",
|
|
142
|
+
"/404.html",
|
|
143
|
+
"/offline.html",
|
|
144
|
+
"/manifest.webmanifest",
|
|
145
|
+
"/service-worker.js"
|
|
146
|
+
].includes(pathname)
|
|
147
|
+
) {
|
|
148
|
+
file = await getDefaultFile(pathname);
|
|
149
|
+
} else {
|
|
150
|
+
file = await crud.send(data);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- Wildcard fallback ---
|
|
154
|
+
if (!file || !file.object || !file.object[0]) {
|
|
155
|
+
pathname = urlObject.pathname;
|
|
156
|
+
let lastIndex = pathname.lastIndexOf("/");
|
|
157
|
+
let wildcardPath = pathname.substring(0, lastIndex + 1);
|
|
158
|
+
let wildcard = pathname.substring(lastIndex + 1);
|
|
159
|
+
|
|
160
|
+
if (wildcard.includes(".")) {
|
|
161
|
+
let fileLastIndex = wildcard.lastIndexOf(".");
|
|
162
|
+
let fileExtension = wildcard.substring(fileLastIndex);
|
|
163
|
+
wildcard = wildcardPath + "*" + fileExtension; // Create wildcard for file name
|
|
164
|
+
} else {
|
|
165
|
+
wildcard = wildcardPath + "*/index.html"; // Append '*' if it's just a path or folder
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
data.$filter.query.pathname = wildcard;
|
|
169
|
+
file = await crud.send(data);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!file || !file.object || !file.object[0]) {
|
|
173
|
+
let pageNotFound = await getDefaultFile("/404.html");
|
|
174
|
+
return sendResponse(pageNotFound.object[0].src, 404, {
|
|
175
|
+
"Content-Type": "text/html"
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
file = file.object[0];
|
|
180
|
+
if (!file["public"] || file["public"] === "false") {
|
|
181
|
+
let pageForbidden = await getDefaultFile("/403.html");
|
|
182
|
+
return sendResponse(pageForbidden.object[0].src, 403, {
|
|
183
|
+
"Content-Type": "text/html"
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let src;
|
|
188
|
+
if (file["src"]) {
|
|
189
|
+
src = file["src"];
|
|
190
|
+
} else {
|
|
191
|
+
let fileSrc = await crud.send({
|
|
192
|
+
method: "object.read",
|
|
193
|
+
host: hostname,
|
|
194
|
+
array: file["array"],
|
|
195
|
+
object: {
|
|
196
|
+
_id: file._id
|
|
197
|
+
},
|
|
198
|
+
organization_id
|
|
199
|
+
});
|
|
200
|
+
src = fileSrc[file["name"]];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!src) {
|
|
204
|
+
let pageNotFound = await getDefaultFile("/404.html");
|
|
205
|
+
return sendResponse(pageNotFound.object[0].src, 404, {
|
|
206
|
+
"Content-Type": "text/html"
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let modifiedOn = file.modified || file.created;
|
|
211
|
+
if (modifiedOn) {
|
|
212
|
+
modifiedOn = modifiedOn.on;
|
|
213
|
+
if (modifiedOn instanceof Date)
|
|
214
|
+
modifiedOn = modifiedOn.toISOString();
|
|
215
|
+
res.setHeader("Last-Modified", modifiedOn);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let contentType = file["content-type"] || "text/html";
|
|
219
|
+
|
|
220
|
+
// Normalize and decode src based on content-type and pathname.
|
|
221
|
+
async function normalizeSrc(src, contentType, pathname) {
|
|
222
|
+
const isBase64Only = (s) =>
|
|
223
|
+
typeof s === "string" && /^[A-Za-z0-9+/]+={0,2}$/.test(s) && s.length % 4 === 0;
|
|
224
|
+
|
|
225
|
+
const parseDataUri = (s) => {
|
|
226
|
+
// returns { mime, isBase64, data } or null
|
|
227
|
+
const m = /^data:([^;]+)(;base64)?,(.*)$/is.exec(s);
|
|
228
|
+
if (!m) return null;
|
|
229
|
+
return { mime: m[1].toLowerCase(), isBase64: !!m[2], data: m[3] };
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Fonts: data:font/...;base64 or plain base64 -> Buffer
|
|
233
|
+
if (contentType.startsWith("font/") || /\.(woff2?|ttf|otf)$/i.test(pathname)) {
|
|
234
|
+
if (Buffer.isBuffer(src)) return src;
|
|
235
|
+
if (typeof src !== "string") throw new Error("Invalid font src");
|
|
236
|
+
const d = parseDataUri(src);
|
|
237
|
+
if (d && d.isBase64) return Buffer.from(d.data, "base64");
|
|
238
|
+
// maybe stored as bare base64
|
|
239
|
+
if (isBase64Only(src)) return Buffer.from(src, "base64");
|
|
240
|
+
throw new Error("Font data is not valid base64 or data URI");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Data URIs
|
|
244
|
+
if (typeof src === "string") {
|
|
245
|
+
const d = parseDataUri(src);
|
|
246
|
+
if (d) {
|
|
247
|
+
// SVG: decode to utf8 string so browsers render SVG correctly
|
|
248
|
+
if (d.mime === "image/svg+xml") {
|
|
249
|
+
if (d.isBase64) return Buffer.from(d.data, "base64").toString("utf8");
|
|
250
|
+
// URI-encoded SVG payload
|
|
251
|
+
try {
|
|
252
|
+
return decodeURIComponent(d.data);
|
|
253
|
+
} catch (_) {
|
|
254
|
+
return d.data;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Raster images, Icons, Video, and Audio -> Buffer
|
|
258
|
+
// Added x-icon, vnd.microsoft.icon, and ico to process icon files properly
|
|
259
|
+
if (/^(image\/(png|jpe?g|webp|bmp|gif|x-icon|vnd\.microsoft\.icon|ico)|video\/.*|audio\/.*)$/i.test(d.mime)) {
|
|
260
|
+
if (d.isBase64) return Buffer.from(d.data, "base64");
|
|
261
|
+
}
|
|
262
|
+
// If it's a text data URI (e.g., xml) and not base64, return decoded text
|
|
263
|
+
if (!d.isBase64) {
|
|
264
|
+
try {
|
|
265
|
+
return decodeURIComponent(d.data);
|
|
266
|
+
} catch (_) {
|
|
267
|
+
return d.data;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Plain base64-only string: decode to Buffer only for binary content types
|
|
274
|
+
if (isBase64Only(src)) {
|
|
275
|
+
if (
|
|
276
|
+
contentType.startsWith("image/") ||
|
|
277
|
+
contentType.startsWith("font/") ||
|
|
278
|
+
contentType.startsWith("video/") ||
|
|
279
|
+
contentType.startsWith("audio/") ||
|
|
280
|
+
contentType === "application/octet-stream"
|
|
281
|
+
) {
|
|
282
|
+
return Buffer.from(src, "base64");
|
|
283
|
+
}
|
|
284
|
+
// textual content kept as-is
|
|
285
|
+
return src;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// HTML rendering
|
|
289
|
+
if (contentType === "text/html") {
|
|
290
|
+
return await this.render.HTML(
|
|
291
|
+
file,
|
|
292
|
+
organization,
|
|
293
|
+
urlObject,
|
|
294
|
+
langRegion,
|
|
295
|
+
lang,
|
|
296
|
+
theme
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// XML host replacement if src is string
|
|
301
|
+
if ((contentType === "text/xml" || contentType === "application/xml") && typeof src === "string") {
|
|
302
|
+
const protocol = "https://";
|
|
303
|
+
return src.replaceAll("{{$host}}", `${protocol}${hostname}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Otherwise return as-is (string, Buffer, object)
|
|
307
|
+
return src;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
src = await normalizeSrc.call(this, src, contentType, pathname);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error("Error processing file src:", err && err.message);
|
|
314
|
+
let pageNotFound = await getDefaultFile("/404.html");
|
|
315
|
+
return sendResponse(pageNotFound.object[0].src, 404, {
|
|
316
|
+
"Content-Type": "text/html"
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
sendResponse(src, 200, { "Content-Type": contentType });
|
|
321
|
+
this.sitemap.check(file, hostname);
|
|
322
|
+
|
|
323
|
+
function sendResponse(src, statusCode, headers) {
|
|
324
|
+
try {
|
|
325
|
+
let cacheControl;
|
|
326
|
+
if (statusCode >= 400) {
|
|
327
|
+
cacheControl = "no-cache, no-store, must-revalidate";
|
|
328
|
+
} else if (
|
|
329
|
+
file &&
|
|
330
|
+
file["cache-control"] !== undefined &&
|
|
331
|
+
file["cache-control"] !== null
|
|
332
|
+
) {
|
|
333
|
+
const val = file["cache-control"];
|
|
334
|
+
if (typeof val === "number" || /^\s*\d+\s*$/.test(val)) {
|
|
335
|
+
// If it's numeric (number or numeric string) treat it as max-age.
|
|
336
|
+
cacheControl = `public, max-age=${String(
|
|
337
|
+
val
|
|
338
|
+
).trim()};`;
|
|
339
|
+
} else {
|
|
340
|
+
// use the value verbatim (allow full Cache-Control strings like "no-cache" or "public, max-age=600")
|
|
341
|
+
cacheControl = val;
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
cacheControl = organization["cache-control"] || "public, max-age=3600";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Always override/set Cache-Control header so the response aligns with the file metadata/defaults
|
|
348
|
+
headers["Cache-Control"] = cacheControl;
|
|
349
|
+
// Advertise that we accept the Sec-CH-Prefers-Color-Scheme client hint.
|
|
350
|
+
// After the browser sees this in a response it may include
|
|
351
|
+
// the Sec-CH-Prefers-Color-Scheme header on subsequent requests.
|
|
352
|
+
headers["Accept-CH"] = "Sec-CH-Prefers-Color-Scheme";
|
|
353
|
+
// optional: tell browser how long to remember this preference (seconds)
|
|
354
|
+
headers["Accept-CH-Lifetime"] = "86400";
|
|
355
|
+
// ensure caches/proxies vary responses by this hint
|
|
356
|
+
headers["Vary"] = headers["Vary"]
|
|
357
|
+
? headers["Vary"] + ", Sec-CH-Prefers-Color-Scheme"
|
|
358
|
+
: "Sec-CH-Prefers-Color-Scheme";
|
|
359
|
+
|
|
360
|
+
if (src instanceof Uint8Array || Buffer.isBuffer(src)) {
|
|
361
|
+
// Ensure binary data is sent as-is
|
|
362
|
+
} else if (typeof src === "object") {
|
|
363
|
+
src = JSON.stringify(src);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (organization_id)
|
|
367
|
+
crud.wsManager.emit("setBandwidth", {
|
|
368
|
+
type: "out",
|
|
369
|
+
data: src,
|
|
370
|
+
organization_id
|
|
371
|
+
});
|
|
372
|
+
res.writeHead(statusCode, headers);
|
|
373
|
+
return res.end(src);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.log(error);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function getDefaultFile(fileName) {
|
|
380
|
+
data.$filter.query.pathname = fileName;
|
|
381
|
+
// data.$filter.query.$or[0] = { pathname: fileName }
|
|
382
|
+
let defaultFile;
|
|
383
|
+
if (fileName !== "/hostNotFound.html")
|
|
384
|
+
defaultFile = await crud.send(data);
|
|
385
|
+
|
|
386
|
+
if (
|
|
387
|
+
defaultFile &&
|
|
388
|
+
defaultFile.object &&
|
|
389
|
+
defaultFile.object[0] &&
|
|
390
|
+
defaultFile.object[0].src
|
|
391
|
+
) {
|
|
392
|
+
return defaultFile;
|
|
393
|
+
} else {
|
|
394
|
+
data.$filter.query.host.$in = ["*"];
|
|
395
|
+
data.organization_id = process.env.organization_id;
|
|
396
|
+
|
|
397
|
+
if (fileName.startsWith("/admin"))
|
|
398
|
+
data.$filter.query.pathname =
|
|
399
|
+
"/superadmin" + fileName.replace("/admin", "");
|
|
400
|
+
|
|
401
|
+
defaultFile = await crud.send(data);
|
|
402
|
+
|
|
403
|
+
if (fileName !== "/hostNotFound.html") {
|
|
404
|
+
crud.wsManager.emit("setBandwidth", {
|
|
405
|
+
type: "out",
|
|
406
|
+
data,
|
|
407
|
+
organization_id
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
crud.wsManager.emit("setBandwidth", {
|
|
411
|
+
type: "in",
|
|
412
|
+
data: defaultFile,
|
|
413
|
+
organization_id
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (
|
|
418
|
+
defaultFile &&
|
|
419
|
+
defaultFile.object &&
|
|
420
|
+
defaultFile.object[0] &&
|
|
421
|
+
defaultFile.object[0].src
|
|
422
|
+
) {
|
|
423
|
+
if (fileName.startsWith("/admin")) {
|
|
424
|
+
data.object[0].directory = "admin";
|
|
425
|
+
data.object[0].path =
|
|
426
|
+
"/admin" +
|
|
427
|
+
data.object[0].path.replace("/superadmin", "");
|
|
428
|
+
data.object[0].pathname = fileName;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
crud.send({
|
|
432
|
+
method: "object.create",
|
|
433
|
+
host: hostname,
|
|
434
|
+
array: "files",
|
|
435
|
+
object: defaultFile.object[0],
|
|
436
|
+
organization_id
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
return defaultFile;
|
|
440
|
+
} else {
|
|
441
|
+
switch (fileName) {
|
|
442
|
+
case "/403.html":
|
|
443
|
+
defaultFile.object = [
|
|
444
|
+
{
|
|
445
|
+
src: `${pathname} access not allowed for ${organization_id}`
|
|
446
|
+
}
|
|
447
|
+
];
|
|
448
|
+
break;
|
|
449
|
+
case "/404.html":
|
|
450
|
+
defaultFile.object = [
|
|
451
|
+
{
|
|
452
|
+
src: `${pathname} could not be found for ${organization_id}`
|
|
453
|
+
}
|
|
454
|
+
];
|
|
455
|
+
break;
|
|
456
|
+
case "/balanceFalse.html":
|
|
457
|
+
defaultFile.object = [
|
|
458
|
+
{
|
|
459
|
+
src: "This organizations account balance has fallen bellow 0: "
|
|
460
|
+
}
|
|
461
|
+
];
|
|
462
|
+
break;
|
|
463
|
+
case "/hostNotFound.html":
|
|
464
|
+
defaultFile.object = [
|
|
465
|
+
{
|
|
466
|
+
src:
|
|
467
|
+
"An organization could not be found using the host: " +
|
|
468
|
+
hostname +
|
|
469
|
+
" in platformDB: " +
|
|
470
|
+
process.env.organization_id
|
|
471
|
+
}
|
|
472
|
+
];
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
return defaultFile;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
481
|
+
res.end("Invalid host format");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
481
484
|
}
|
|
482
485
|
|
|
483
|
-
module.exports = CoCreateFileSystem;
|
|
486
|
+
module.exports = CoCreateFileSystem;
|