laju-server 1.0.3 → 1.0.5
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 +72 -22
- package/index.js +333 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,45 +1,95 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🚀 Laju Server
|
|
2
2
|
|
|
3
|
-
Instant static file server
|
|
3
|
+
> Instant, modern, and zero-configuration static file server.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/laju-server)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
**Laju Server** is a lightweight command-line tool that instantly turns any folder into a web server. It features a beautiful, modern UI for directory listings, automatic network discovery, and support for large file streaming.
|
|
9
|
+
|
|
10
|
+
Perfect for local development, sharing files over LAN, or testing static builds.
|
|
11
|
+
|
|
12
|
+
## ✨ Features
|
|
13
|
+
|
|
14
|
+
- **🎨 Modern UI**: Beautiful, responsive directory listing with a clean interface.
|
|
15
|
+
- **⚡ Zero Config**: Just run the command and your server is ready.
|
|
16
|
+
- **📁 Smart Listing**:
|
|
17
|
+
- SVG icons for different file types.
|
|
18
|
+
- Sorts folders and files automatically.
|
|
19
|
+
- One-click navigation and file downloads.
|
|
20
|
+
- **🌐 Network Ready**: Automatically displays LAN IP addresses for easy sharing.
|
|
21
|
+
- **🚀 High Performance**: Built on native Node.js APIs for optimal speed.
|
|
22
|
+
- **🔒 Secure**: Built-in protection against directory traversal attacks.
|
|
23
|
+
- **📺 Media Friendly**: Supports video/audio streaming (Range requests).
|
|
24
|
+
|
|
25
|
+
## 📦 Installation
|
|
26
|
+
|
|
27
|
+
You can run it directly using `npx` (recommended):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx laju-server [folder]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or install globally:
|
|
6
34
|
|
|
7
35
|
```bash
|
|
8
36
|
npm install -g laju-server
|
|
9
37
|
```
|
|
10
38
|
|
|
11
|
-
##
|
|
39
|
+
## 🛠 Usage
|
|
40
|
+
|
|
41
|
+
### Basic Usage
|
|
12
42
|
|
|
43
|
+
Serve the current directory:
|
|
13
44
|
```bash
|
|
14
|
-
# Serve folder saat ini
|
|
15
45
|
npx laju-server
|
|
46
|
+
```
|
|
16
47
|
|
|
17
|
-
|
|
18
|
-
|
|
48
|
+
Serve a specific folder:
|
|
49
|
+
```bash
|
|
50
|
+
npx laju-server ./my-project
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Options
|
|
19
54
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
npx laju-server
|
|
55
|
+
Serve on a specific port (default: 3000):
|
|
56
|
+
```bash
|
|
57
|
+
npx laju-server . -p 8080
|
|
58
|
+
# or
|
|
59
|
+
npx laju-server . --port 8080
|
|
23
60
|
```
|
|
24
61
|
|
|
25
|
-
##
|
|
62
|
+
## 🖥️ Preview
|
|
63
|
+
|
|
64
|
+
When you open a folder without an `index.html`, Laju Server presents a modern file explorer:
|
|
65
|
+
|
|
66
|
+
- **Folders** are shown with blue icons and navigation chevrons.
|
|
67
|
+
- **Files** have specific icons (Code, Image, Video, Archive, etc.) and direct download buttons.
|
|
68
|
+
- **Header** shows the current path clearly.
|
|
69
|
+
- **Mobile Ready**: Fully responsive design that works great on phones and tablets.
|
|
26
70
|
|
|
27
|
-
|
|
28
|
-
- 📁 Serve folder statis
|
|
29
|
-
- 🔒 Proteksi directory traversal
|
|
30
|
-
- 📄 Auto serve index.html
|
|
31
|
-
- 🎨 Auto detect MIME types
|
|
71
|
+
## 📝 Examples
|
|
32
72
|
|
|
33
|
-
|
|
73
|
+
**Sharing a game folder over LAN:**
|
|
74
|
+
```bash
|
|
75
|
+
npx laju-server /Volumes/Games
|
|
76
|
+
```
|
|
77
|
+
*Result: Your friends can browse and download games from `http://192.168.1.x:3000`*
|
|
34
78
|
|
|
79
|
+
**Testing a React build:**
|
|
35
80
|
```bash
|
|
36
|
-
# Serve folder build React
|
|
37
81
|
npx laju-server ./build
|
|
82
|
+
```
|
|
38
83
|
|
|
39
|
-
|
|
40
|
-
|
|
84
|
+
**Serving a static site on port 4000:**
|
|
85
|
+
```bash
|
|
86
|
+
npx laju-server ./public --port 4000
|
|
41
87
|
```
|
|
42
88
|
|
|
43
|
-
##
|
|
89
|
+
## 🤝 Contributing
|
|
90
|
+
|
|
91
|
+
Contributions are welcome! Feel free to submit a Pull Request.
|
|
92
|
+
|
|
93
|
+
## 📄 License
|
|
44
94
|
|
|
45
|
-
MIT
|
|
95
|
+
MIT © [Maulana Shalihin](https://github.com/maulanashalihin)
|
package/index.js
CHANGED
|
@@ -44,11 +44,18 @@ function startServer(folderPath, port = 3000) {
|
|
|
44
44
|
// Check if file exists
|
|
45
45
|
fs.stat(resolvedPath, (err, stat) => {
|
|
46
46
|
if (err) {
|
|
47
|
-
//
|
|
48
|
-
const
|
|
49
|
-
fs.stat(
|
|
50
|
-
if (!err2 && stat2.
|
|
51
|
-
|
|
47
|
+
// Check if parent directory exists and show listing
|
|
48
|
+
const parentDir = path.dirname(resolvedPath);
|
|
49
|
+
fs.stat(parentDir, (err2, stat2) => {
|
|
50
|
+
if (!err2 && stat2.isDirectory()) {
|
|
51
|
+
const indexPath = path.join(parentDir, 'index.html');
|
|
52
|
+
fs.stat(indexPath, (err3, stat3) => {
|
|
53
|
+
if (!err3 && stat3.isFile()) {
|
|
54
|
+
return serveFile(indexPath, stat3, req, res);
|
|
55
|
+
}
|
|
56
|
+
return serveDirectoryListing(parentDir, path.dirname(req.url), res);
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
52
59
|
}
|
|
53
60
|
res.writeHead(404);
|
|
54
61
|
res.end('File not found');
|
|
@@ -62,8 +69,8 @@ function startServer(folderPath, port = 3000) {
|
|
|
62
69
|
if (!err2 && stat2.isFile()) {
|
|
63
70
|
return serveFile(indexPath, stat2, req, res);
|
|
64
71
|
}
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
// No index.html, show directory listing
|
|
73
|
+
return serveDirectoryListing(resolvedPath, req.url, res);
|
|
67
74
|
});
|
|
68
75
|
return;
|
|
69
76
|
}
|
|
@@ -72,6 +79,325 @@ function startServer(folderPath, port = 3000) {
|
|
|
72
79
|
});
|
|
73
80
|
});
|
|
74
81
|
|
|
82
|
+
function serveDirectoryListing(dirPath, urlPath, res) {
|
|
83
|
+
fs.readdir(dirPath, { withFileTypes: true }, (err, entries) => {
|
|
84
|
+
if (err) {
|
|
85
|
+
res.writeHead(500);
|
|
86
|
+
res.end('Error reading directory');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Separate folders and files
|
|
91
|
+
const folders = [];
|
|
92
|
+
const files = [];
|
|
93
|
+
|
|
94
|
+
entries.forEach(entry => {
|
|
95
|
+
if (entry.name.startsWith('.')) return; // Skip hidden files
|
|
96
|
+
if (entry.isDirectory()) {
|
|
97
|
+
folders.push(entry.name);
|
|
98
|
+
} else {
|
|
99
|
+
files.push(entry.name);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Sort alphabetically
|
|
104
|
+
folders.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
105
|
+
files.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
106
|
+
|
|
107
|
+
// Normalize URL path
|
|
108
|
+
let basePath = urlPath;
|
|
109
|
+
if (!basePath.endsWith('/')) basePath += '/';
|
|
110
|
+
if (!basePath.startsWith('/')) basePath = '/' + basePath;
|
|
111
|
+
|
|
112
|
+
// Generate HTML
|
|
113
|
+
let html = `<!DOCTYPE html>
|
|
114
|
+
<html lang="id">
|
|
115
|
+
<head>
|
|
116
|
+
<meta charset="UTF-8">
|
|
117
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
118
|
+
<title>Laju Server - ${basePath}</title>
|
|
119
|
+
<style>
|
|
120
|
+
:root {
|
|
121
|
+
--primary: #3b82f6;
|
|
122
|
+
--bg: #f8fafc;
|
|
123
|
+
--surface: #ffffff;
|
|
124
|
+
--text: #1e293b;
|
|
125
|
+
--text-secondary: #64748b;
|
|
126
|
+
--border: #e2e8f0;
|
|
127
|
+
--hover: #f1f5f9;
|
|
128
|
+
}
|
|
129
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
130
|
+
body {
|
|
131
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
132
|
+
background: var(--bg);
|
|
133
|
+
color: var(--text);
|
|
134
|
+
line-height: 1.5;
|
|
135
|
+
}
|
|
136
|
+
.container {
|
|
137
|
+
max-width: 900px;
|
|
138
|
+
margin: 40px auto;
|
|
139
|
+
padding: 0 20px;
|
|
140
|
+
}
|
|
141
|
+
header {
|
|
142
|
+
margin-bottom: 24px;
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
gap: 12px;
|
|
146
|
+
}
|
|
147
|
+
h1 {
|
|
148
|
+
font-size: 1.25rem;
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
color: var(--text);
|
|
151
|
+
word-break: break-all;
|
|
152
|
+
}
|
|
153
|
+
.badge {
|
|
154
|
+
background: #dbeafe;
|
|
155
|
+
color: #1e40af;
|
|
156
|
+
font-size: 0.75rem;
|
|
157
|
+
padding: 4px 8px;
|
|
158
|
+
border-radius: 6px;
|
|
159
|
+
font-weight: 500;
|
|
160
|
+
}
|
|
161
|
+
.card {
|
|
162
|
+
background: var(--surface);
|
|
163
|
+
border-radius: 12px;
|
|
164
|
+
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
165
|
+
border: 1px solid var(--border);
|
|
166
|
+
overflow: hidden;
|
|
167
|
+
}
|
|
168
|
+
.list-header {
|
|
169
|
+
padding: 12px 16px;
|
|
170
|
+
border-bottom: 1px solid var(--border);
|
|
171
|
+
background: #f8fafc;
|
|
172
|
+
font-size: 0.875rem;
|
|
173
|
+
color: var(--text-secondary);
|
|
174
|
+
font-weight: 500;
|
|
175
|
+
display: flex;
|
|
176
|
+
justify-content: space-between;
|
|
177
|
+
}
|
|
178
|
+
.item {
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
padding: 12px 16px;
|
|
182
|
+
border-bottom: 1px solid var(--border);
|
|
183
|
+
text-decoration: none;
|
|
184
|
+
color: var(--text);
|
|
185
|
+
transition: all 0.2s ease;
|
|
186
|
+
background: var(--surface);
|
|
187
|
+
}
|
|
188
|
+
.item:last-child {
|
|
189
|
+
border-bottom: none;
|
|
190
|
+
}
|
|
191
|
+
.item:hover {
|
|
192
|
+
background: var(--hover);
|
|
193
|
+
transform: translateX(2px);
|
|
194
|
+
}
|
|
195
|
+
.icon {
|
|
196
|
+
width: 32px;
|
|
197
|
+
height: 32px;
|
|
198
|
+
margin-right: 16px;
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
justify-content: center;
|
|
202
|
+
background: #f1f5f9;
|
|
203
|
+
border-radius: 8px;
|
|
204
|
+
color: var(--text-secondary);
|
|
205
|
+
}
|
|
206
|
+
.icon svg {
|
|
207
|
+
width: 20px;
|
|
208
|
+
height: 20px;
|
|
209
|
+
stroke-width: 2px;
|
|
210
|
+
}
|
|
211
|
+
.folder .icon {
|
|
212
|
+
background: #eff6ff;
|
|
213
|
+
color: #3b82f6;
|
|
214
|
+
}
|
|
215
|
+
.file .icon {
|
|
216
|
+
background: #f1f5f9;
|
|
217
|
+
color: #64748b;
|
|
218
|
+
}
|
|
219
|
+
.info {
|
|
220
|
+
flex: 1;
|
|
221
|
+
min-width: 0;
|
|
222
|
+
}
|
|
223
|
+
.name {
|
|
224
|
+
font-weight: 500;
|
|
225
|
+
white-space: nowrap;
|
|
226
|
+
overflow: hidden;
|
|
227
|
+
text-overflow: ellipsis;
|
|
228
|
+
display: block;
|
|
229
|
+
}
|
|
230
|
+
.meta {
|
|
231
|
+
font-size: 0.75rem;
|
|
232
|
+
color: var(--text-secondary);
|
|
233
|
+
margin-top: 2px;
|
|
234
|
+
}
|
|
235
|
+
.action-icon {
|
|
236
|
+
opacity: 0;
|
|
237
|
+
transition: opacity 0.2s;
|
|
238
|
+
color: var(--text-secondary);
|
|
239
|
+
display: flex;
|
|
240
|
+
align-items: center;
|
|
241
|
+
}
|
|
242
|
+
.action-icon svg {
|
|
243
|
+
width: 18px;
|
|
244
|
+
height: 18px;
|
|
245
|
+
}
|
|
246
|
+
.item:hover .action-icon {
|
|
247
|
+
opacity: 1;
|
|
248
|
+
}
|
|
249
|
+
.parent {
|
|
250
|
+
background: #f8fafc;
|
|
251
|
+
}
|
|
252
|
+
.empty {
|
|
253
|
+
padding: 60px 20px;
|
|
254
|
+
text-align: center;
|
|
255
|
+
color: var(--text-secondary);
|
|
256
|
+
display: flex;
|
|
257
|
+
flex-direction: column;
|
|
258
|
+
align-items: center;
|
|
259
|
+
}
|
|
260
|
+
.empty-icon {
|
|
261
|
+
margin-bottom: 16px;
|
|
262
|
+
color: #cbd5e1;
|
|
263
|
+
}
|
|
264
|
+
.empty-icon svg {
|
|
265
|
+
width: 48px;
|
|
266
|
+
height: 48px;
|
|
267
|
+
}
|
|
268
|
+
@media (max-width: 640px) {
|
|
269
|
+
.container { margin: 20px auto; }
|
|
270
|
+
.icon { width: 28px; height: 28px; margin-right: 12px; }
|
|
271
|
+
.icon svg { width: 16px; height: 16px; }
|
|
272
|
+
}
|
|
273
|
+
</style>
|
|
274
|
+
</head>
|
|
275
|
+
<body>
|
|
276
|
+
<div class="container">
|
|
277
|
+
<header>
|
|
278
|
+
<div class="badge">DIR</div>
|
|
279
|
+
<h1>${basePath}</h1>
|
|
280
|
+
</header>
|
|
281
|
+
|
|
282
|
+
<div class="card">
|
|
283
|
+
<div class="list-header">
|
|
284
|
+
<span>Name</span>
|
|
285
|
+
<span>Action</span>
|
|
286
|
+
</div>`;
|
|
287
|
+
|
|
288
|
+
const Icons = {
|
|
289
|
+
folder: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>',
|
|
290
|
+
back: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5v0a5.5 5.5 0 0 1-5.5 5.5H11"/></svg>',
|
|
291
|
+
file: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>',
|
|
292
|
+
image: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>',
|
|
293
|
+
video: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.777-.416L16 10"/><rect x="2" y="6" width="14" height="12" rx="2"/></svg>',
|
|
294
|
+
music: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>',
|
|
295
|
+
code: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
|
|
296
|
+
archive: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8v13H3V8"/><path d="M1 3h22v5H1z"/><path d="M10 12h4"/></svg>',
|
|
297
|
+
download: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>',
|
|
298
|
+
chevronRight: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>'
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Add parent directory link if not at root
|
|
302
|
+
if (basePath !== '/') {
|
|
303
|
+
const parentPath = path.dirname(basePath.slice(0, -1)) || '/';
|
|
304
|
+
html += `\n <a href="${parentPath}" class="item parent folder">
|
|
305
|
+
<span class="icon">${Icons.back}</span>
|
|
306
|
+
<div class="info">
|
|
307
|
+
<span class="name">..</span>
|
|
308
|
+
<div class="meta">Go back</div>
|
|
309
|
+
</div>
|
|
310
|
+
</a>`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Add folders
|
|
314
|
+
folders.forEach(folder => {
|
|
315
|
+
const href = basePath + encodeURIComponent(folder) + '/';
|
|
316
|
+
html += `\n <a href="${href}" class="item folder">
|
|
317
|
+
<span class="icon">${Icons.folder}</span>
|
|
318
|
+
<div class="info">
|
|
319
|
+
<span class="name">${escapeHtml(folder)}</span>
|
|
320
|
+
<div class="meta">Folder</div>
|
|
321
|
+
</div>
|
|
322
|
+
<span class="action-icon">${Icons.chevronRight}</span>
|
|
323
|
+
</a>`;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Add files with download attribute
|
|
327
|
+
files.forEach(file => {
|
|
328
|
+
const href = basePath + encodeURIComponent(file);
|
|
329
|
+
const icon = getFileIconSvg(file);
|
|
330
|
+
html += `\n <a href="${href}" class="item file" download>
|
|
331
|
+
<span class="icon">${icon}</span>
|
|
332
|
+
<div class="info">
|
|
333
|
+
<span class="name">${escapeHtml(file)}</span>
|
|
334
|
+
<div class="meta">File</div>
|
|
335
|
+
</div>
|
|
336
|
+
<span class="action-icon">${Icons.download}</span>
|
|
337
|
+
</a>`;
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
if (folders.length === 0 && files.length === 0) {
|
|
341
|
+
html += `\n <div class="empty">
|
|
342
|
+
<div class="empty-icon">${Icons.folder}</div>
|
|
343
|
+
<p>Folder ini kosong</p>
|
|
344
|
+
</div>`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
html += `\n </div>
|
|
348
|
+
<div style="margin-top: 20px; text-align: center; color: var(--text-secondary); font-size: 0.875rem;">
|
|
349
|
+
Laju Server v1.0.4
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
</body>
|
|
353
|
+
</html>`;
|
|
354
|
+
|
|
355
|
+
res.writeHead(200, {
|
|
356
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
357
|
+
'Content-Length': Buffer.byteLength(html)
|
|
358
|
+
});
|
|
359
|
+
res.end(html);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function escapeHtml(text) {
|
|
364
|
+
return text
|
|
365
|
+
.replace(/&/g, '&')
|
|
366
|
+
.replace(/</g, '<')
|
|
367
|
+
.replace(/>/g, '>')
|
|
368
|
+
.replace(/"/g, '"');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function getFileIconSvg(filename) {
|
|
372
|
+
const ext = path.extname(filename).toLowerCase();
|
|
373
|
+
// Simple SVG icons
|
|
374
|
+
const svgMap = {
|
|
375
|
+
// Images
|
|
376
|
+
'.jpg': 'image', '.jpeg': 'image', '.png': 'image', '.gif': 'image', '.webp': 'image', '.svg': 'image', '.ico': 'image',
|
|
377
|
+
// Video
|
|
378
|
+
'.mp4': 'video', '.mkv': 'video', '.avi': 'video', '.mov': 'video', '.webm': 'video',
|
|
379
|
+
// Audio
|
|
380
|
+
'.mp3': 'music', '.wav': 'music', '.flac': 'music', '.aac': 'music', '.ogg': 'music',
|
|
381
|
+
// Code
|
|
382
|
+
'.js': 'code', '.ts': 'code', '.py': 'code', '.php': 'code', '.html': 'code', '.css': 'code', '.json': 'code',
|
|
383
|
+
// Archive
|
|
384
|
+
'.zip': 'archive', '.rar': 'archive', '.7z': 'archive', '.tar': 'archive', '.gz': 'archive'
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const iconType = svgMap[ext] || 'file';
|
|
388
|
+
|
|
389
|
+
const icons = {
|
|
390
|
+
file: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></svg>',
|
|
391
|
+
image: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>',
|
|
392
|
+
video: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.777-.416L16 10"/><rect x="2" y="6" width="14" height="12" rx="2"/></svg>',
|
|
393
|
+
music: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>',
|
|
394
|
+
code: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
|
|
395
|
+
archive: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8v13H3V8"/><path d="M1 3h22v5H1z"/><path d="M10 12h4"/></svg>'
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
return icons[iconType];
|
|
399
|
+
}
|
|
400
|
+
|
|
75
401
|
function serveFile(filePath, stat, req, res) {
|
|
76
402
|
const mimeType = mime.lookup(filePath) || 'application/octet-stream';
|
|
77
403
|
const fileSize = stat.size;
|