@cocreate/file-server 1.23.0 → 1.25.0
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/package.json +4 -3
- package/src/index.js +95 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/file-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"description": "A simple file-server component in vanilla javascript. Easily configured using HTML5 data-attributes and/or JavaScript API.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"file-server",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"type": "module",
|
|
38
38
|
"main": "./src/index.js",
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@cocreate/
|
|
41
|
-
"@cocreate/
|
|
40
|
+
"@cocreate/config": "^1.19.0",
|
|
41
|
+
"@cocreate/server-side-render": "^1.19.0",
|
|
42
|
+
"@cocreate/sitemap": "^1.10.0"
|
|
42
43
|
}
|
|
43
44
|
}
|
package/src/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/********************************************************************************
|
|
2
|
-
* Copyright (C)
|
|
2
|
+
* Copyright (C) 2026 CoCreate and Contributors.
|
|
3
3
|
*
|
|
4
4
|
* This program is free software: you can redistribute it and/or modify
|
|
5
|
-
*
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
6
6
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
7
7
|
* (at your option) any later version.
|
|
8
8
|
*
|
|
@@ -23,27 +23,57 @@
|
|
|
23
23
|
|
|
24
24
|
import * as render from '@cocreate/server-side-render';
|
|
25
25
|
import * as sitemap from '@cocreate/sitemap';
|
|
26
|
+
import config from '@cocreate/config';
|
|
27
|
+
import { dotNotationToObject } from '@cocreate/utils';
|
|
26
28
|
|
|
27
|
-
// Module-level reference injected
|
|
29
|
+
// Module-level reference injected during boot to prevent ESM circular loops
|
|
28
30
|
let server = null;
|
|
29
31
|
|
|
32
|
+
// Module default configuration
|
|
33
|
+
let fileServerConfig = {
|
|
34
|
+
defaultCacheControl: 'public, max-age=3600',
|
|
35
|
+
enableSitemapCheck: true,
|
|
36
|
+
enableThemeDetection: true
|
|
37
|
+
};
|
|
38
|
+
|
|
30
39
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
40
|
+
* Loads file server configuration using @cocreate/config and queries database overrides
|
|
41
|
+
* for 'fileServer' configurations.
|
|
42
|
+
*
|
|
43
|
+
* @returns {Promise<Object>} Resolved configuration object
|
|
34
44
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
async function loadConfig() {
|
|
46
|
+
const sysConfig = await config.get('fileServer');
|
|
47
|
+
|
|
48
|
+
let dbConfig = null;
|
|
49
|
+
if (server && server.organization_id) {
|
|
50
|
+
dbConfig = await config.query({
|
|
51
|
+
type: 'fileServer',
|
|
52
|
+
organization_id: server.organization_id,
|
|
53
|
+
server: server.host,
|
|
54
|
+
cluster: server.cluster_id
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (sysConfig) {
|
|
59
|
+
dotNotationToObject(fileServerConfig, sysConfig);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (dbConfig) {
|
|
63
|
+
dotNotationToObject(fileServerConfig, dbConfig);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return fileServerConfig;
|
|
38
67
|
}
|
|
39
68
|
|
|
40
69
|
/**
|
|
41
|
-
* Main
|
|
42
|
-
*
|
|
70
|
+
* Main entry point for the CoCreate File System request handler.
|
|
71
|
+
* Handles urlObject parsing, organization lookups, localization, theme detection, and rendering.
|
|
72
|
+
*
|
|
43
73
|
* @param {Object} req - Native Node HTTP request object
|
|
44
74
|
* @param {Object} res - Native Node HTTP response object
|
|
45
75
|
*/
|
|
46
|
-
|
|
76
|
+
async function send(req, res) {
|
|
47
77
|
try {
|
|
48
78
|
const activeCrud = server ? server.crud : null;
|
|
49
79
|
|
|
@@ -110,7 +140,7 @@ export async function send(req, res) {
|
|
|
110
140
|
}
|
|
111
141
|
|
|
112
142
|
// 3. Theme Detection
|
|
113
|
-
const theme = detectTheme(req, res);
|
|
143
|
+
const theme = fileServerConfig.enableThemeDetection ? detectTheme(req, res) : null;
|
|
114
144
|
|
|
115
145
|
// 4. Resolve exact file resource and status code
|
|
116
146
|
const { file, src, statusCode, resolvedPathname } = await resolveFile({
|
|
@@ -161,7 +191,7 @@ export async function send(req, res) {
|
|
|
161
191
|
});
|
|
162
192
|
|
|
163
193
|
// 8. Async Sitemap Check
|
|
164
|
-
if (file) {
|
|
194
|
+
if (fileServerConfig.enableSitemapCheck && file) {
|
|
165
195
|
await sitemap.check({ file, host: hostname, crud: activeCrud });
|
|
166
196
|
}
|
|
167
197
|
|
|
@@ -172,15 +202,11 @@ export async function send(req, res) {
|
|
|
172
202
|
}
|
|
173
203
|
}
|
|
174
204
|
|
|
175
|
-
// ==========================================
|
|
176
|
-
// Isolated Helper Functions
|
|
177
|
-
// ==========================================
|
|
178
|
-
|
|
179
205
|
/**
|
|
180
206
|
* Normalizes slash-ending paths, resolves files via wildcard mappings,
|
|
181
207
|
* handles 404/403 fallbacks, and fetches asset sources.
|
|
182
208
|
*/
|
|
183
|
-
|
|
209
|
+
async function resolveFile({ pathname, hostname, organizationId, crud }) {
|
|
184
210
|
const activeCrud = crud || (server ? server.crud : null);
|
|
185
211
|
let statusCode = 200;
|
|
186
212
|
let file;
|
|
@@ -269,7 +295,7 @@ export async function resolveFile({ pathname, hostname, organizationId, crud })
|
|
|
269
295
|
src = file?.src || "";
|
|
270
296
|
}
|
|
271
297
|
|
|
272
|
-
// Bind source markup and calculated status directly onto the file document for downstream consumers
|
|
298
|
+
// Bind source markup and calculated status directly onto the file document for downstream consumers
|
|
273
299
|
if (file) {
|
|
274
300
|
file.src = src;
|
|
275
301
|
file.status = statusCode;
|
|
@@ -281,7 +307,7 @@ export async function resolveFile({ pathname, hostname, organizationId, crud })
|
|
|
281
307
|
/**
|
|
282
308
|
* Specifically resolves, tracks, and mirrors system fallbacks from global scopes.
|
|
283
309
|
*/
|
|
284
|
-
|
|
310
|
+
async function getDefaultFile({ fileName, hostname, organizationId, crud }) {
|
|
285
311
|
const activeCrud = crud || (server ? server.crud : null);
|
|
286
312
|
const queryData = {
|
|
287
313
|
method: "object.read",
|
|
@@ -373,7 +399,7 @@ export async function getDefaultFile({ fileName, hostname, organizationId, crud
|
|
|
373
399
|
/**
|
|
374
400
|
* Calculates a directory wildcard path pattern representation.
|
|
375
401
|
*/
|
|
376
|
-
|
|
402
|
+
function getWildcardPath(pathname) {
|
|
377
403
|
const lastIndex = pathname.lastIndexOf("/");
|
|
378
404
|
const wildcardPath = pathname.substring(0, lastIndex + 1);
|
|
379
405
|
const wildcardName = pathname.substring(lastIndex + 1);
|
|
@@ -390,7 +416,7 @@ export function getWildcardPath(pathname) {
|
|
|
390
416
|
/**
|
|
391
417
|
* Detects color scheme from HTTP headers and updates client indicators.
|
|
392
418
|
*/
|
|
393
|
-
|
|
419
|
+
function detectTheme(req, res) {
|
|
394
420
|
let theme;
|
|
395
421
|
const headerValue = req?.headers && (
|
|
396
422
|
req.headers["sec-ch-prefers-color-scheme"] ||
|
|
@@ -413,7 +439,7 @@ export function detectTheme(req, res) {
|
|
|
413
439
|
/**
|
|
414
440
|
* Standardizes directory matching redirection configurations.
|
|
415
441
|
*/
|
|
416
|
-
|
|
442
|
+
function handleTrailingSlash(pathname, urlObject, res) {
|
|
417
443
|
if (!pathname.endsWith("/") && !pathname.startsWith("/.well-known/acme-challenge")) {
|
|
418
444
|
const lastSegment = pathname.split("/").slice(-1)[0];
|
|
419
445
|
if (!lastSegment.includes(".")) {
|
|
@@ -429,7 +455,7 @@ export function handleTrailingSlash(pathname, urlObject, res) {
|
|
|
429
455
|
/**
|
|
430
456
|
* Extracts explicit or implicit preferred localization tokens from endpoints.
|
|
431
457
|
*/
|
|
432
|
-
|
|
458
|
+
function parseLanguage(pathname, headers) {
|
|
433
459
|
const bcp47Regex = /^\/([a-zA-Z]{2,3}(?:-[a-zA-Z0-9]{2,8})?)\//;
|
|
434
460
|
const langMatch = pathname.match(bcp47Regex);
|
|
435
461
|
let lang, langRegion;
|
|
@@ -455,10 +481,10 @@ export function parseLanguage(pathname, headers) {
|
|
|
455
481
|
/**
|
|
456
482
|
* Formats, cleans, parses, and executes rendering transformations based on content types.
|
|
457
483
|
*/
|
|
458
|
-
|
|
484
|
+
async function normalizeSrc({ src, contentType, pathname, file, organization, urlObject, langRegion, lang, theme, crud }) {
|
|
459
485
|
const activeCrud = crud || (server ? server.crud : null);
|
|
460
486
|
const isBase64Only = (s) =>
|
|
461
|
-
typeof s === "string" && /^[A-Za-z0-9
|
|
487
|
+
typeof s === "string" && s.length > 0 && /^[A-Za-z0-9+/=\-_]+$/.test(s.replace(/\s+/g, '')) && s.replace(/\s+/g, '').length % 4 === 0;
|
|
462
488
|
|
|
463
489
|
const parseDataUri = (s) => {
|
|
464
490
|
const m = /^data:([^;]+)(;base64)?,(.*)$/is.exec(s);
|
|
@@ -469,11 +495,25 @@ export async function normalizeSrc({ src, contentType, pathname, file, organizat
|
|
|
469
495
|
// Fonts
|
|
470
496
|
if (contentType.startsWith("font/") || /\.(woff2?|ttf|otf)$/i.test(pathname)) {
|
|
471
497
|
if (Buffer.isBuffer(src)) return src;
|
|
472
|
-
if (typeof src !== "string")
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
498
|
+
if (typeof src !== "string") return Buffer.from("");
|
|
499
|
+
|
|
500
|
+
const cleanedSrc = src.trim();
|
|
501
|
+
const d = parseDataUri(cleanedSrc);
|
|
502
|
+
if (d?.isBase64) {
|
|
503
|
+
return Buffer.from(d.data.replace(/\s+/g, ''), "base64");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const stripped = cleanedSrc.replace(/\s+/g, '').replace(/-/g, '+').replace(/_/g, '/');
|
|
507
|
+
if (isBase64Only(stripped)) {
|
|
508
|
+
return Buffer.from(stripped, "base64");
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Graceful fallback for binary or raw font strings instead of throwing
|
|
512
|
+
try {
|
|
513
|
+
return Buffer.from(src, "binary");
|
|
514
|
+
} catch (_) {
|
|
515
|
+
return Buffer.from(src);
|
|
516
|
+
}
|
|
477
517
|
}
|
|
478
518
|
|
|
479
519
|
// Data URIs
|
|
@@ -517,7 +557,6 @@ export async function normalizeSrc({ src, contentType, pathname, file, organizat
|
|
|
517
557
|
|
|
518
558
|
// HTML rendering
|
|
519
559
|
if (contentType === "text/html") {
|
|
520
|
-
// Adapter Closure to independently fetch isolated templates/chunks for SSR
|
|
521
560
|
const recursiveResolve = async (nestedPathname) => {
|
|
522
561
|
const resolved = await resolveFile({
|
|
523
562
|
pathname: nestedPathname,
|
|
@@ -552,9 +591,8 @@ export async function normalizeSrc({ src, contentType, pathname, file, organizat
|
|
|
552
591
|
/**
|
|
553
592
|
* Handles setting response headers, caching guidelines, bandwidth tracking, and returning binary strings.
|
|
554
593
|
*/
|
|
555
|
-
|
|
594
|
+
async function sendResponse({ res, src, statusCode, headers, file, organizationId, crud, organizationCacheControl }) {
|
|
556
595
|
try {
|
|
557
|
-
const activeCrud = crud || (server ? server.crud : null);
|
|
558
596
|
let cacheControl;
|
|
559
597
|
if (statusCode >= 400) {
|
|
560
598
|
cacheControl = "no-cache, no-store, must-revalidate";
|
|
@@ -566,7 +604,7 @@ export async function sendResponse({ res, src, statusCode, headers, file, organi
|
|
|
566
604
|
cacheControl = val;
|
|
567
605
|
}
|
|
568
606
|
} else {
|
|
569
|
-
cacheControl = organizationCacheControl || "public, max-age=3600";
|
|
607
|
+
cacheControl = organizationCacheControl || fileServerConfig.defaultCacheControl || "public, max-age=3600";
|
|
570
608
|
}
|
|
571
609
|
|
|
572
610
|
headers["Cache-Control"] = cacheControl;
|
|
@@ -595,17 +633,25 @@ export async function sendResponse({ res, src, statusCode, headers, file, organi
|
|
|
595
633
|
console.error("Error finalizing response payload:", error);
|
|
596
634
|
}
|
|
597
635
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Initializes the FileServer module.
|
|
639
|
+
*
|
|
640
|
+
* @param {Object} serverInstance - Core server instance
|
|
641
|
+
* @returns {Promise<Object>} FileServer interface object containing methods and config
|
|
642
|
+
*/
|
|
643
|
+
export async function init(serverInstance) {
|
|
644
|
+
server = serverInstance;
|
|
645
|
+
|
|
646
|
+
await loadConfig();
|
|
647
|
+
|
|
648
|
+
const fileServer = {
|
|
649
|
+
config: fileServerConfig,
|
|
650
|
+
send,
|
|
651
|
+
resolveFile
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
return fileServer;
|
|
609
655
|
}
|
|
610
|
-
|
|
611
|
-
export default
|
|
656
|
+
|
|
657
|
+
export default { init };
|