koa-classic-server 2.1.4 โ 2.4.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/README.md +110 -35
- package/__tests__/deprecation-warnings.test.js +217 -0
- package/__tests__/publicWwwTest/test-page.html +1 -0
- package/__tests__/symlink.test.js +438 -0
- package/__tests__/useOriginalUrl.test.js +213 -0
- package/docs/CHANGELOG.md +160 -0
- package/docs/DOCUMENTATION.md +53 -5
- package/index.cjs +140 -38
- package/package.json +1 -1
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,166 @@ All notable changes to koa-classic-server will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.4.0] - 2026-02-28
|
|
9
|
+
|
|
10
|
+
### ๐ Bug Fix
|
|
11
|
+
|
|
12
|
+
#### Fixed Symlink Support in Index File Discovery and Directory Listing
|
|
13
|
+
- **Issue**: On systems where served files are symbolic links (NixOS buildFHSEnv, Docker bind mounts, `npm link`, Capistrano-style deploys), `findIndexFile()` failed because `dirent.isFile()` returns `false` for symlinks. This caused `GET /` to show directory listing instead of rendering the index file, and `GET /index.ejs` to return 404.
|
|
14
|
+
- **Impact**: HIGH - Server unusable on NixOS/buildFHSEnv and similar environments
|
|
15
|
+
- **Fix**: Added `isFileOrSymlinkToFile()` / `isDirOrSymlinkToDir()` helpers that follow symlinks via `fs.promises.stat()` only when `dirent.isSymbolicLink()` is true, adding zero overhead for regular files.
|
|
16
|
+
- **Code**: `index.cjs` - new helpers + `findIndexFile()` + `show_dir()`
|
|
17
|
+
|
|
18
|
+
### โจ Improvements
|
|
19
|
+
|
|
20
|
+
#### Directory Listing Symlink Indicators
|
|
21
|
+
- Symlinks to files/directories show `( Symlink )` label next to the name
|
|
22
|
+
- Broken/circular symlinks show `( Broken Symlink )` label (name visible but not clickable)
|
|
23
|
+
- Symlinks resolved to effective type for MIME and size display (e.g. symlink to dir shows `DIR`)
|
|
24
|
+
- Sorting uses effective type (symlink-to-dir sorts with directories)
|
|
25
|
+
|
|
26
|
+
### ๐งช Testing
|
|
27
|
+
- Added `__tests__/symlink.test.js` with 17 tests covering:
|
|
28
|
+
- Regular file as index (regression)
|
|
29
|
+
- Symlink to file as index (string and RegExp patterns)
|
|
30
|
+
- Direct GET to symlinked file
|
|
31
|
+
- EJS template via symlink
|
|
32
|
+
- Symlink to directory (listing and file access)
|
|
33
|
+
- Broken and circular symlinks
|
|
34
|
+
- Directory listing indicators (`( Symlink )`, `( Broken Symlink )`)
|
|
35
|
+
- Regular file regression (no false symlink indicator)
|
|
36
|
+
- All 187 existing tests still pass (zero regressions)
|
|
37
|
+
|
|
38
|
+
### ๐ฆ Package Changes
|
|
39
|
+
- **Semver**: Minor version bump (new feature, backward compatible)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## [2.3.0] - 2026-01-03
|
|
44
|
+
|
|
45
|
+
### ๐ Renamed Options (with Backward Compatibility)
|
|
46
|
+
|
|
47
|
+
#### Renamed Caching Options for Clarity
|
|
48
|
+
- **Old Names** (DEPRECATED): `enableCaching`, `cacheMaxAge`
|
|
49
|
+
- **New Names**: `browserCacheEnabled`, `browserCacheMaxAge`
|
|
50
|
+
- **Reason**: Improved clarity - these options specifically control browser-side HTTP caching
|
|
51
|
+
- **Backward Compatible**: Old names still work but display deprecation warnings
|
|
52
|
+
|
|
53
|
+
#### Deprecation Warnings
|
|
54
|
+
When using deprecated option names, a warning is displayed on the terminal:
|
|
55
|
+
```
|
|
56
|
+
[koa-classic-server] DEPRECATION WARNING: The "enableCaching" option is deprecated and will be removed in future versions.
|
|
57
|
+
Current usage: enableCaching: true
|
|
58
|
+
Recommended: browserCacheEnabled: true
|
|
59
|
+
Please update your configuration to use the new option name.
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### ๐ Documentation Updates
|
|
63
|
+
|
|
64
|
+
- Updated README.md with new option names
|
|
65
|
+
- Updated JSDoc comments in index.cjs
|
|
66
|
+
- Added deprecation notes in Options table
|
|
67
|
+
- All examples updated to use new names
|
|
68
|
+
|
|
69
|
+
### ๐ง Changes
|
|
70
|
+
|
|
71
|
+
- **index.cjs**: Lines 109-135 - Added backward compatibility logic with deprecation warnings
|
|
72
|
+
- **index.cjs**: Lines 47-58 - Updated JSDoc comments
|
|
73
|
+
- **index.cjs**: Lines 350, 361 - Updated code to use new option names
|
|
74
|
+
- **README.md**: Updated all references to use new names, added deprecation notes
|
|
75
|
+
- **package.json**: Version bumped from `2.2.0` to `2.3.0`
|
|
76
|
+
|
|
77
|
+
### โ ๏ธ Migration Guide
|
|
78
|
+
|
|
79
|
+
**No immediate changes required** - old option names still work.
|
|
80
|
+
|
|
81
|
+
**Recommended migration:**
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
// Old (still works, but deprecated)
|
|
85
|
+
app.use(koaClassicServer('/public', {
|
|
86
|
+
enableCaching: true,
|
|
87
|
+
cacheMaxAge: 3600
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
// New (recommended)
|
|
91
|
+
app.use(koaClassicServer('/public', {
|
|
92
|
+
browserCacheEnabled: true,
|
|
93
|
+
browserCacheMaxAge: 3600
|
|
94
|
+
}));
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Timeline:**
|
|
98
|
+
- **v2.3.0**: Old names work with deprecation warnings
|
|
99
|
+
- **Future versions**: Old names may be removed (will be announced in advance)
|
|
100
|
+
|
|
101
|
+
### ๐ฆ Package Changes
|
|
102
|
+
|
|
103
|
+
- **Version**: `2.2.0` โ `2.3.0`
|
|
104
|
+
- **Semver**: Minor version bump (new feature names, backward compatible)
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## [2.2.0] - 2026-01-03
|
|
109
|
+
|
|
110
|
+
### โจ Features
|
|
111
|
+
|
|
112
|
+
#### Added useOriginalUrl Option
|
|
113
|
+
- **New Option**: `useOriginalUrl` (Boolean, default: `true`)
|
|
114
|
+
- **Purpose**: Controls URL resolution for file serving - use `ctx.originalUrl` (immutable) or `ctx.url` (mutable)
|
|
115
|
+
- **Use Case**: Compatibility with URL rewriting middleware (i18n, routing)
|
|
116
|
+
- **Backward Compatible**: Default value `true` maintains existing behavior
|
|
117
|
+
|
|
118
|
+
#### URL Rewriting Middleware Support
|
|
119
|
+
- **Problem Solved**: koa-classic-server previously used `ctx.href` (based on `ctx.originalUrl`), which caused 404 errors when middleware rewrites URLs by modifying `ctx.url`
|
|
120
|
+
- **Solution**: Set `useOriginalUrl: false` to use the rewritten URL from `ctx.url` instead
|
|
121
|
+
- **Example**: i18n middleware that strips language prefixes (`/it/page.html` โ `/page.html`)
|
|
122
|
+
|
|
123
|
+
### ๐ Documentation
|
|
124
|
+
|
|
125
|
+
- Added comprehensive `useOriginalUrl` documentation in README.md
|
|
126
|
+
- Added JSDoc comments in index.cjs
|
|
127
|
+
- Included practical i18n middleware example
|
|
128
|
+
- Added option to API reference table
|
|
129
|
+
|
|
130
|
+
### ๐ง Changes
|
|
131
|
+
|
|
132
|
+
- **index.cjs**: Line 108 - Added `useOriginalUrl` option initialization
|
|
133
|
+
- **index.cjs**: Lines 117-125 - Modified URL construction logic to support both `ctx.originalUrl` and `ctx.url`
|
|
134
|
+
- **README.md**: Added detailed section explaining `useOriginalUrl` with examples
|
|
135
|
+
- **package.json**: Version bumped from `2.1.4` to `2.2.0`
|
|
136
|
+
|
|
137
|
+
### ๐ก Usage Example
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
// i18n middleware example
|
|
141
|
+
app.use(async (ctx, next) => {
|
|
142
|
+
if (ctx.path.match(/^\/it\//)) {
|
|
143
|
+
ctx.url = ctx.path.replace(/^\/it/, ''); // /it/page.html โ /page.html
|
|
144
|
+
}
|
|
145
|
+
await next();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
app.use(koaClassicServer('/www', {
|
|
149
|
+
useOriginalUrl: false // Use rewritten URL
|
|
150
|
+
}));
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### โ ๏ธ Migration Notes
|
|
154
|
+
|
|
155
|
+
**No breaking changes** - this is a backward-compatible release.
|
|
156
|
+
|
|
157
|
+
- **Default behavior unchanged**: `useOriginalUrl` defaults to `true`
|
|
158
|
+
- **No code changes required** for existing implementations
|
|
159
|
+
- **New feature**: Set `useOriginalUrl: false` if you use URL rewriting middleware
|
|
160
|
+
|
|
161
|
+
### ๐ฆ Package Changes
|
|
162
|
+
|
|
163
|
+
- **Version**: `2.1.4` โ `2.2.0`
|
|
164
|
+
- **Semver**: Minor version bump (new feature, backward compatible)
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
8
168
|
## [2.1.3] - 2025-11-25
|
|
9
169
|
|
|
10
170
|
### ๐ง Configuration Changes
|
package/docs/DOCUMENTATION.md
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
- Navigazione parent directory con link ".. Parent Directory"
|
|
36
36
|
- Indicazione chiara del tipo di risorsa (DIR, MIME type)
|
|
37
37
|
- Gestione e visualizzazione cartelle riservate
|
|
38
|
-
- Supporto link simbolici
|
|
38
|
+
- Supporto completo link simbolici (symlink a file, directory, broken e circolari)
|
|
39
39
|
|
|
40
40
|
#### 3. Supporto Template Engine
|
|
41
41
|
- Integrazione flessibile con motori di template (es. EJS, Pug, Handlebars)
|
|
@@ -777,6 +777,49 @@ http://localhost:3000/file.txt/ โ http://localhost:3000/file.txt
|
|
|
777
777
|
|
|
778
778
|
Questo assicura comportamento coerente indipendentemente dal trailing slash.
|
|
779
779
|
|
|
780
|
+
### Gestione dei Link Simbolici (Symlink)
|
|
781
|
+
|
|
782
|
+
Il middleware supporta completamente i link simbolici (symlink). Questo รจ fondamentale in ambienti dove i file serviti sono symlink anzichรฉ file regolari, come ad esempio:
|
|
783
|
+
|
|
784
|
+
- **NixOS con buildFHSEnv** (chroot-like): i file nella directory www/ appaiono come symlink al Nix store
|
|
785
|
+
- **Docker bind mounts**: i file montati possono risultare come symlink
|
|
786
|
+
- **npm link**: i pacchetti linkati sono symlink
|
|
787
|
+
- **Deploy Capistrano-style**: la directory `current` รจ un symlink alla release attiva
|
|
788
|
+
|
|
789
|
+
#### Comportamento
|
|
790
|
+
|
|
791
|
+
Il middleware segue i symlink in modo trasparente tramite `fs.promises.stat()` (che risolve il symlink al target reale), ma solo quando `dirent.isSymbolicLink()` รจ `true`. Per i file regolari non viene effettuata alcuna chiamata aggiuntiva (zero overhead).
|
|
792
|
+
|
|
793
|
+
#### Risoluzione Index File
|
|
794
|
+
|
|
795
|
+
Quando il middleware cerca un file index in una directory (opzione `index`), i symlink che puntano a file regolari vengono inclusi nella ricerca, sia con pattern stringa che RegExp:
|
|
796
|
+
|
|
797
|
+
```
|
|
798
|
+
Directory:
|
|
799
|
+
index.ejs โ /nix/store/.../index.ejs (symlink a file)
|
|
800
|
+
style.css (file regolare)
|
|
801
|
+
|
|
802
|
+
Config: index: ['index.ejs']
|
|
803
|
+
Risultato: Serve index.ejs attraverso il symlink โ
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
#### Directory Listing
|
|
807
|
+
|
|
808
|
+
Nel directory listing, i symlink sono identificati visivamente:
|
|
809
|
+
|
|
810
|
+
| Caso | Indicatore | Cliccabile | Tipo mostrato |
|
|
811
|
+
|------|-----------|------------|---------------|
|
|
812
|
+
| Symlink a file | `( Symlink )` | Sรฌ | MIME type del target |
|
|
813
|
+
| Symlink a directory | `( Symlink )` | Sรฌ | `DIR` |
|
|
814
|
+
| Symlink rotto/circolare | `( Broken Symlink )` | No | `unknown` |
|
|
815
|
+
| File/directory regolare | nessuno | Sรฌ | tipo reale |
|
|
816
|
+
|
|
817
|
+
#### Casi Limite
|
|
818
|
+
|
|
819
|
+
- **Broken symlink** (target inesistente): il GET diretto restituisce 404; nel listing il nome appare senza link
|
|
820
|
+
- **Symlink circolare** (A โ B โ A): trattato come broken symlink, nessun loop infinito
|
|
821
|
+
- **Symlink a directory**: navigabile come una directory regolare, i file al suo interno sono accessibili
|
|
822
|
+
|
|
780
823
|
### MIME Types
|
|
781
824
|
|
|
782
825
|
MIME types riconosciuti automaticamente tramite estensione file:
|
|
@@ -1494,7 +1537,7 @@ app.use(koaClassicServer(__dirname + '/public', {
|
|
|
1494
1537
|
## Informazioni Aggiuntive
|
|
1495
1538
|
|
|
1496
1539
|
### Versione
|
|
1497
|
-
**
|
|
1540
|
+
**2.4.0**
|
|
1498
1541
|
|
|
1499
1542
|
### Autore
|
|
1500
1543
|
**Italo Paesano**
|
|
@@ -1558,8 +1601,13 @@ Contributi benvenuti! Per favore:
|
|
|
1558
1601
|
|
|
1559
1602
|
### Changelog
|
|
1560
1603
|
|
|
1604
|
+
#### v1.2.0
|
|
1605
|
+
- Fix: supporto completo link simbolici in `findIndexFile()` e directory listing
|
|
1606
|
+
- Nuovi helper `isFileOrSymlinkToFile()` / `isDirOrSymlinkToDir()` (zero overhead per file regolari)
|
|
1607
|
+
- Directory listing: indicatori `( Symlink )` e `( Broken Symlink )`, tipo effettivo (MIME/DIR) per symlink
|
|
1608
|
+
- 17 nuovi test per tutti gli scenari symlink (NixOS, Docker, npm link, broken, circular)
|
|
1609
|
+
|
|
1561
1610
|
#### v1.1.0
|
|
1562
|
-
- Versione attuale
|
|
1563
1611
|
- Supporto conditional exports
|
|
1564
1612
|
- Test suite completa
|
|
1565
1613
|
|
|
@@ -1580,6 +1628,6 @@ Repository con esempi completi disponibile nella cartella `customTest/` del prog
|
|
|
1580
1628
|
|
|
1581
1629
|
---
|
|
1582
1630
|
|
|
1583
|
-
**Documentazione generata per koa-classic-server
|
|
1631
|
+
**Documentazione generata per koa-classic-server v2.4.0**
|
|
1584
1632
|
|
|
1585
|
-
*Ultimo aggiornamento:
|
|
1633
|
+
*Ultimo aggiornamento: 2026-02-28*
|
package/index.cjs
CHANGED
|
@@ -44,11 +44,17 @@ module.exports = function koaClassicServer(
|
|
|
44
44
|
render: undefined, // Template rendering function: async (ctx, next, filePath) => {}
|
|
45
45
|
ext: [], // File extensions to process with template.render
|
|
46
46
|
},
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
browserCacheMaxAge: 3600, // Browser Cache-Control max-age in seconds (default: 1 hour)
|
|
48
|
+
browserCacheEnabled: false, // Enable browser HTTP caching headers (ETag, Last-Modified)
|
|
49
|
+
// NOTE: Default is false for development.
|
|
50
|
+
// In production, it's recommended to set browserCacheEnabled: true
|
|
51
|
+
// to reduce bandwidth usage and improve performance.
|
|
52
|
+
useOriginalUrl: true, // Use ctx.originalUrl (default) or ctx.url
|
|
53
|
+
// Set false for URL rewriting middleware (i18n, routing)
|
|
54
|
+
|
|
55
|
+
// DEPRECATED OPTIONS (maintained for backward compatibility):
|
|
56
|
+
// cacheMaxAge: use browserCacheMaxAge instead
|
|
57
|
+
// enableCaching: use browserCacheEnabled instead
|
|
52
58
|
}
|
|
53
59
|
*/
|
|
54
60
|
) {
|
|
@@ -100,11 +106,71 @@ module.exports = function koaClassicServer(
|
|
|
100
106
|
options.template.ext = Array.isArray(options.template.ext) ? options.template.ext : [];
|
|
101
107
|
|
|
102
108
|
// OPTIMIZATION: HTTP Caching options
|
|
103
|
-
// NOTE: Default
|
|
109
|
+
// NOTE: Default browserCacheEnabled is false for development environments.
|
|
104
110
|
// For production deployments, it's strongly recommended to enable caching
|
|
105
|
-
// by setting
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
// by setting browserCacheEnabled: true to benefit from reduced bandwidth and improved performance.
|
|
112
|
+
|
|
113
|
+
// DEPRECATION: Handle legacy option names for backward compatibility
|
|
114
|
+
if ('cacheMaxAge' in opts && !('browserCacheMaxAge' in opts)) {
|
|
115
|
+
console.warn(
|
|
116
|
+
'\x1b[33m%s\x1b[0m',
|
|
117
|
+
'[koa-classic-server] DEPRECATION WARNING: The "cacheMaxAge" option is deprecated and will be removed in future versions.\n' +
|
|
118
|
+
' Current usage: cacheMaxAge: ' + opts.cacheMaxAge + '\n' +
|
|
119
|
+
' Recommended: browserCacheMaxAge: ' + opts.cacheMaxAge + '\n' +
|
|
120
|
+
' Please update your configuration to use the new option name.'
|
|
121
|
+
);
|
|
122
|
+
options.browserCacheMaxAge = opts.cacheMaxAge;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if ('enableCaching' in opts && !('browserCacheEnabled' in opts)) {
|
|
126
|
+
console.warn(
|
|
127
|
+
'\x1b[33m%s\x1b[0m',
|
|
128
|
+
'[koa-classic-server] DEPRECATION WARNING: The "enableCaching" option is deprecated and will be removed in future versions.\n' +
|
|
129
|
+
' Current usage: enableCaching: ' + opts.enableCaching + '\n' +
|
|
130
|
+
' Recommended: browserCacheEnabled: ' + opts.enableCaching + '\n' +
|
|
131
|
+
' Please update your configuration to use the new option name.'
|
|
132
|
+
);
|
|
133
|
+
options.browserCacheEnabled = opts.enableCaching;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Set new option names (with defaults)
|
|
137
|
+
options.browserCacheMaxAge = typeof options.browserCacheMaxAge === 'number' && options.browserCacheMaxAge >= 0 ? options.browserCacheMaxAge : 3600;
|
|
138
|
+
options.browserCacheEnabled = typeof options.browserCacheEnabled === 'boolean' ? options.browserCacheEnabled : false;
|
|
139
|
+
options.useOriginalUrl = typeof options.useOriginalUrl === 'boolean' ? options.useOriginalUrl : true;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns true if dirent is a regular file or a symlink pointing to a regular file.
|
|
143
|
+
* Uses fs.promises.stat (which follows symlinks) only when dirent.isSymbolicLink() is true.
|
|
144
|
+
*/
|
|
145
|
+
async function isFileOrSymlinkToFile(dirent, dirPath) {
|
|
146
|
+
if (dirent.isFile()) return true;
|
|
147
|
+
if (dirent.isSymbolicLink()) {
|
|
148
|
+
try {
|
|
149
|
+
const realStat = await fs.promises.stat(path.join(dirPath, dirent.name));
|
|
150
|
+
return realStat.isFile();
|
|
151
|
+
} catch {
|
|
152
|
+
return false; // Broken or circular symlink
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns true if dirent is a directory or a symlink pointing to a directory.
|
|
160
|
+
* Uses fs.promises.stat (which follows symlinks) only when dirent.isSymbolicLink() is true.
|
|
161
|
+
*/
|
|
162
|
+
async function isDirOrSymlinkToDir(dirent, dirPath) {
|
|
163
|
+
if (dirent.isDirectory()) return true;
|
|
164
|
+
if (dirent.isSymbolicLink()) {
|
|
165
|
+
try {
|
|
166
|
+
const realStat = await fs.promises.stat(path.join(dirPath, dirent.name));
|
|
167
|
+
return realStat.isDirectory();
|
|
168
|
+
} catch {
|
|
169
|
+
return false; // Broken or circular symlink
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
108
174
|
|
|
109
175
|
return async (ctx, next) => {
|
|
110
176
|
// Check if method is allowed
|
|
@@ -113,12 +179,14 @@ module.exports = function koaClassicServer(
|
|
|
113
179
|
return;
|
|
114
180
|
}
|
|
115
181
|
|
|
116
|
-
//
|
|
182
|
+
// Construct full URL based on useOriginalUrl option
|
|
183
|
+
const urlToUse = options.useOriginalUrl ? ctx.originalUrl : ctx.url;
|
|
184
|
+
const fullUrl = ctx.protocol + '://' + ctx.host + urlToUse;
|
|
117
185
|
let pageHref = '';
|
|
118
|
-
if (
|
|
119
|
-
pageHref = new URL(
|
|
186
|
+
if (fullUrl.charAt(fullUrl.length - 1) === '/') {
|
|
187
|
+
pageHref = new URL(fullUrl.slice(0, -1));
|
|
120
188
|
} else {
|
|
121
|
-
pageHref = new URL(
|
|
189
|
+
pageHref = new URL(fullUrl);
|
|
122
190
|
}
|
|
123
191
|
|
|
124
192
|
// Check URL prefix
|
|
@@ -225,10 +293,16 @@ module.exports = function koaClassicServer(
|
|
|
225
293
|
// Read directory contents
|
|
226
294
|
const files = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
227
295
|
|
|
228
|
-
// Filter
|
|
229
|
-
const
|
|
230
|
-
.
|
|
231
|
-
|
|
296
|
+
// Filter files, following symlinks to determine effective type
|
|
297
|
+
const fileCheckResults = await Promise.all(
|
|
298
|
+
files.map(async dirent => ({
|
|
299
|
+
name: dirent.name,
|
|
300
|
+
isFile: await isFileOrSymlinkToFile(dirent, dirPath)
|
|
301
|
+
}))
|
|
302
|
+
);
|
|
303
|
+
const fileNames = fileCheckResults
|
|
304
|
+
.filter(entry => entry.isFile)
|
|
305
|
+
.map(entry => entry.name);
|
|
232
306
|
|
|
233
307
|
// Search with priority order (first pattern wins)
|
|
234
308
|
for (const pattern of indexPatterns) {
|
|
@@ -317,7 +391,7 @@ module.exports = function koaClassicServer(
|
|
|
317
391
|
}
|
|
318
392
|
|
|
319
393
|
// OPTIMIZATION: HTTP Caching Headers
|
|
320
|
-
if (options.
|
|
394
|
+
if (options.browserCacheEnabled) {
|
|
321
395
|
// Generate ETag from mtime timestamp + file size
|
|
322
396
|
// This ensures ETag changes when file is modified or resized
|
|
323
397
|
const etag = `"${fileStat.mtime.getTime()}-${fileStat.size}"`;
|
|
@@ -328,7 +402,7 @@ module.exports = function koaClassicServer(
|
|
|
328
402
|
// Set caching headers
|
|
329
403
|
ctx.set('ETag', etag);
|
|
330
404
|
ctx.set('Last-Modified', lastModified);
|
|
331
|
-
ctx.set('Cache-Control', `public, max-age=${options.
|
|
405
|
+
ctx.set('Cache-Control', `public, max-age=${options.browserCacheMaxAge}, must-revalidate`);
|
|
332
406
|
|
|
333
407
|
// OPTIMIZATION: Handle conditional requests (304 Not Modified)
|
|
334
408
|
|
|
@@ -508,27 +582,45 @@ module.exports = function koaClassicServer(
|
|
|
508
582
|
itemUri = `${baseUrl}/${encodeURIComponent(s_name)}`;
|
|
509
583
|
}
|
|
510
584
|
|
|
585
|
+
// Resolve symlinks to their effective type
|
|
586
|
+
let effectiveType = type;
|
|
587
|
+
let isBrokenSymlink = false;
|
|
588
|
+
if (type === 3) {
|
|
589
|
+
try {
|
|
590
|
+
const realStat = await fs.promises.stat(itemPath);
|
|
591
|
+
if (realStat.isFile()) effectiveType = 1;
|
|
592
|
+
else if (realStat.isDirectory()) effectiveType = 2;
|
|
593
|
+
} catch {
|
|
594
|
+
isBrokenSymlink = true; // Broken or circular symlink
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
511
598
|
// Get file size
|
|
512
599
|
let sizeStr = '-';
|
|
513
600
|
let sizeBytes = 0;
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
601
|
+
if (!isBrokenSymlink) {
|
|
602
|
+
try {
|
|
603
|
+
const itemStat = await fs.promises.stat(itemPath);
|
|
604
|
+
if (effectiveType === 1) {
|
|
605
|
+
sizeBytes = itemStat.size;
|
|
606
|
+
sizeStr = formatSize(sizeBytes);
|
|
607
|
+
} else {
|
|
608
|
+
sizeStr = '-';
|
|
609
|
+
}
|
|
610
|
+
} catch (error) {
|
|
520
611
|
sizeStr = '-';
|
|
521
612
|
}
|
|
522
|
-
} catch (error) {
|
|
523
|
-
sizeStr = '-';
|
|
524
613
|
}
|
|
525
614
|
|
|
526
|
-
const mimeType =
|
|
527
|
-
const isReserved = pageHrefOutPrefix.pathname === '/' && options.urlsReserved.includes('/' + s_name) && (
|
|
615
|
+
const mimeType = effectiveType === 2 ? "DIR" : (mime.lookup(itemPath) || 'unknown');
|
|
616
|
+
const isReserved = pageHrefOutPrefix.pathname === '/' && options.urlsReserved.includes('/' + s_name) && (effectiveType === 2 || type === 3);
|
|
528
617
|
|
|
529
618
|
items.push({
|
|
530
619
|
name: s_name,
|
|
531
620
|
type: type,
|
|
621
|
+
effectiveType: effectiveType,
|
|
622
|
+
isSymlink: type === 3,
|
|
623
|
+
isBrokenSymlink: isBrokenSymlink,
|
|
532
624
|
mimeType: mimeType,
|
|
533
625
|
sizeStr: sizeStr,
|
|
534
626
|
sizeBytes: sizeBytes,
|
|
@@ -544,19 +636,19 @@ module.exports = function koaClassicServer(
|
|
|
544
636
|
if (sortBy === 'name') {
|
|
545
637
|
comparison = a.name.localeCompare(b.name);
|
|
546
638
|
} else if (sortBy === 'type') {
|
|
547
|
-
// Sort directories first, then by mime type
|
|
548
|
-
if (a.
|
|
639
|
+
// Sort directories first, then by mime type (using effectiveType for symlinks)
|
|
640
|
+
if (a.effectiveType === 2 && b.effectiveType !== 2) {
|
|
549
641
|
comparison = -1;
|
|
550
|
-
} else if (a.
|
|
642
|
+
} else if (a.effectiveType !== 2 && b.effectiveType === 2) {
|
|
551
643
|
comparison = 1;
|
|
552
644
|
} else {
|
|
553
645
|
comparison = a.mimeType.localeCompare(b.mimeType);
|
|
554
646
|
}
|
|
555
647
|
} else if (sortBy === 'size') {
|
|
556
|
-
// Directories always at top when sorting by size
|
|
557
|
-
if (a.
|
|
648
|
+
// Directories always at top when sorting by size (using effectiveType for symlinks)
|
|
649
|
+
if (a.effectiveType === 2 && b.effectiveType !== 2) {
|
|
558
650
|
comparison = -1;
|
|
559
|
-
} else if (a.
|
|
651
|
+
} else if (a.effectiveType !== 2 && b.effectiveType === 2) {
|
|
560
652
|
comparison = 1;
|
|
561
653
|
} else {
|
|
562
654
|
comparison = a.sizeBytes - b.sizeBytes;
|
|
@@ -570,16 +662,26 @@ module.exports = function koaClassicServer(
|
|
|
570
662
|
// Generate HTML for sorted items
|
|
571
663
|
for (const item of items) {
|
|
572
664
|
let rowStart = '';
|
|
573
|
-
if (item.
|
|
665
|
+
if (item.effectiveType === 1) {
|
|
574
666
|
rowStart = `<tr><td> FILE `;
|
|
575
667
|
} else {
|
|
576
668
|
rowStart = `<tr><td>`;
|
|
577
669
|
}
|
|
578
670
|
|
|
671
|
+
// Symlink indicator label
|
|
672
|
+
const symlinkLabel = item.isBrokenSymlink
|
|
673
|
+
? ' ( Broken Symlink )'
|
|
674
|
+
: item.isSymlink
|
|
675
|
+
? ' ( Symlink )'
|
|
676
|
+
: '';
|
|
677
|
+
|
|
579
678
|
if (item.isReserved) {
|
|
580
|
-
parts.push(`${rowStart} ${escapeHtml(item.name)}</td> <td> DIR BUT RESERVED</td><td>${item.sizeStr}</td></tr>`);
|
|
679
|
+
parts.push(`${rowStart} ${escapeHtml(item.name)}${symlinkLabel}</td> <td> DIR BUT RESERVED</td><td>${item.sizeStr}</td></tr>`);
|
|
680
|
+
} else if (item.isBrokenSymlink) {
|
|
681
|
+
// Broken symlink: name visible but not clickable
|
|
682
|
+
parts.push(`${rowStart} ${escapeHtml(item.name)}${symlinkLabel}</td> <td> ${escapeHtml(item.mimeType)} </td><td>${item.sizeStr}</td></tr>`);
|
|
581
683
|
} else {
|
|
582
|
-
parts.push(`${rowStart} <a href="${escapeHtml(item.itemUri)}">${escapeHtml(item.name)}</a
|
|
684
|
+
parts.push(`${rowStart} <a href="${escapeHtml(item.itemUri)}">${escapeHtml(item.name)}</a>${symlinkLabel} </td> <td> ${escapeHtml(item.mimeType)} </td><td>${item.sizeStr}</td></tr>`);
|
|
583
685
|
}
|
|
584
686
|
}
|
|
585
687
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koa-classic-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "High-performance Koa middleware for serving static files with Apache-like directory listing, HTTP caching, template engine support, and comprehensive security fixes",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"exports": {
|