laju-server 1.0.1 → 1.0.4
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/index.js +395 -93
- package/package.json +4 -5
package/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
const
|
|
1
|
+
const http = require('http');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const mime = require('mime-types');
|
|
5
|
+
const os = require('os');
|
|
5
6
|
|
|
6
7
|
function startServer(folderPath, port = 3000) {
|
|
7
8
|
// Check if folder exists
|
|
@@ -16,25 +17,9 @@ function startServer(folderPath, port = 3000) {
|
|
|
16
17
|
process.exit(1);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
const server =
|
|
20
|
-
fast_buffers: true,
|
|
21
|
-
max_body_buffer: 64 * 1024, // 64KB buffer for streaming
|
|
22
|
-
streaming: {
|
|
23
|
-
readable: {
|
|
24
|
-
highWaterMark: 64 * 1024, // 64KB chunks
|
|
25
|
-
},
|
|
26
|
-
writable: {
|
|
27
|
-
highWaterMark: 64 * 1024, // 64KB chunks
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Serve static files
|
|
33
|
-
server.get('/*', async (req, res) => {
|
|
34
|
-
let requestPath = req.path;
|
|
35
|
-
|
|
20
|
+
const server = http.createServer((req, res) => {
|
|
36
21
|
// Decode URL
|
|
37
|
-
requestPath = decodeURIComponent(
|
|
22
|
+
let requestPath = decodeURIComponent(req.url.split('?')[0]);
|
|
38
23
|
|
|
39
24
|
// Remove leading slash
|
|
40
25
|
if (requestPath.startsWith('/')) {
|
|
@@ -51,115 +36,431 @@ function startServer(folderPath, port = 3000) {
|
|
|
51
36
|
// Security: prevent directory traversal
|
|
52
37
|
const resolvedPath = path.resolve(filePath);
|
|
53
38
|
if (!resolvedPath.startsWith(path.resolve(folderPath))) {
|
|
54
|
-
res.
|
|
39
|
+
res.writeHead(403);
|
|
40
|
+
res.end('Forbidden');
|
|
55
41
|
return;
|
|
56
42
|
}
|
|
57
43
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
//
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
44
|
+
// Check if file exists
|
|
45
|
+
fs.stat(resolvedPath, (err, stat) => {
|
|
46
|
+
if (err) {
|
|
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;
|
|
59
|
+
}
|
|
60
|
+
res.writeHead(404);
|
|
61
|
+
res.end('File not found');
|
|
62
|
+
});
|
|
67
63
|
return;
|
|
68
64
|
}
|
|
69
65
|
|
|
70
|
-
const stat = fs.statSync(resolvedPath);
|
|
71
|
-
|
|
72
66
|
if (stat.isDirectory()) {
|
|
73
|
-
// Try serving index.html from directory
|
|
74
67
|
const indexPath = path.join(resolvedPath, 'index.html');
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
fs.stat(indexPath, (err2, stat2) => {
|
|
69
|
+
if (!err2 && stat2.isFile()) {
|
|
70
|
+
return serveFile(indexPath, stat2, req, res);
|
|
71
|
+
}
|
|
72
|
+
// No index.html, show directory listing
|
|
73
|
+
return serveDirectoryListing(resolvedPath, req.url, res);
|
|
74
|
+
});
|
|
79
75
|
return;
|
|
80
76
|
}
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
console.error('Error:', err.message);
|
|
85
|
-
res.status(500).send('Internal Server Error');
|
|
86
|
-
}
|
|
78
|
+
serveFile(resolvedPath, stat, req, res);
|
|
79
|
+
});
|
|
87
80
|
});
|
|
88
81
|
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
};
|
|
93
386
|
|
|
94
|
-
const
|
|
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
|
+
}
|
|
95
400
|
|
|
96
|
-
|
|
97
|
-
const
|
|
401
|
+
function serveFile(filePath, stat, req, res) {
|
|
402
|
+
const mimeType = mime.lookup(filePath) || 'application/octet-stream';
|
|
403
|
+
const fileSize = stat.size;
|
|
404
|
+
const range = req.headers.range;
|
|
98
405
|
|
|
99
406
|
if (range) {
|
|
100
|
-
// Handle range requests for video streaming / resume download
|
|
101
407
|
const parts = range.replace(/bytes=/, '').split('-');
|
|
102
408
|
const start = parseInt(parts[0], 10);
|
|
103
|
-
|
|
104
|
-
const maxChunk = 10 * 1024 * 1024;
|
|
105
|
-
let end = parts[1] ? parseInt(parts[1], 10) : Math.min(start + maxChunk, fileSize - 1);
|
|
106
|
-
|
|
107
|
-
// Ensure end doesn't exceed file size
|
|
108
|
-
if (end >= fileSize) end = fileSize - 1;
|
|
109
|
-
|
|
409
|
+
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
|
|
110
410
|
const chunkSize = end - start + 1;
|
|
111
411
|
|
|
112
|
-
res.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
412
|
+
res.writeHead(206, {
|
|
413
|
+
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
|
414
|
+
'Accept-Ranges': 'bytes',
|
|
415
|
+
'Content-Length': chunkSize,
|
|
416
|
+
'Content-Type': mimeType,
|
|
417
|
+
});
|
|
118
418
|
|
|
119
|
-
const stream = fs.createReadStream(filePath, { start, end
|
|
419
|
+
const stream = fs.createReadStream(filePath, { start, end });
|
|
420
|
+
stream.pipe(res);
|
|
120
421
|
|
|
121
422
|
stream.on('error', (err) => {
|
|
122
423
|
console.error('Stream error:', err.message);
|
|
123
|
-
|
|
124
|
-
res.status(500).send('Stream error');
|
|
125
|
-
}
|
|
424
|
+
res.end();
|
|
126
425
|
});
|
|
127
426
|
|
|
128
|
-
|
|
427
|
+
req.on('close', () => {
|
|
428
|
+
stream.destroy();
|
|
429
|
+
});
|
|
129
430
|
} else {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
431
|
+
res.writeHead(200, {
|
|
432
|
+
'Content-Length': fileSize,
|
|
433
|
+
'Content-Type': mimeType,
|
|
434
|
+
'Accept-Ranges': 'bytes',
|
|
435
|
+
});
|
|
135
436
|
|
|
136
|
-
const stream = fs.createReadStream(filePath
|
|
437
|
+
const stream = fs.createReadStream(filePath);
|
|
438
|
+
stream.pipe(res);
|
|
137
439
|
|
|
138
440
|
stream.on('error', (err) => {
|
|
139
441
|
console.error('Stream error:', err.message);
|
|
140
|
-
|
|
141
|
-
res.status(500).send('Stream error');
|
|
142
|
-
}
|
|
442
|
+
res.end();
|
|
143
443
|
});
|
|
144
444
|
|
|
145
|
-
|
|
445
|
+
req.on('close', () => {
|
|
446
|
+
stream.destroy();
|
|
447
|
+
});
|
|
146
448
|
}
|
|
147
449
|
}
|
|
148
450
|
|
|
149
|
-
server.listen(port, '0.0.0.0')
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
for (const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
addresses.push(net.address);
|
|
158
|
-
}
|
|
451
|
+
server.listen(port, '0.0.0.0', () => {
|
|
452
|
+
const networkInterfaces = os.networkInterfaces();
|
|
453
|
+
const addresses = [];
|
|
454
|
+
|
|
455
|
+
for (const name of Object.keys(networkInterfaces)) {
|
|
456
|
+
for (const net of networkInterfaces[name]) {
|
|
457
|
+
if (net.family === 'IPv4' && !net.internal) {
|
|
458
|
+
addresses.push(net.address);
|
|
159
459
|
}
|
|
160
460
|
}
|
|
461
|
+
}
|
|
161
462
|
|
|
162
|
-
|
|
463
|
+
console.log(`
|
|
163
464
|
🚀 Laju Server berjalan!
|
|
164
465
|
|
|
165
466
|
📁 Folder: ${folderPath}
|
|
@@ -168,11 +469,12 @@ function startServer(folderPath, port = 3000) {
|
|
|
168
469
|
|
|
169
470
|
Tekan Ctrl+C untuk berhenti
|
|
170
471
|
`);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
server.on('error', (err) => {
|
|
475
|
+
console.error('❌ Gagal menjalankan server:', err.message);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
});
|
|
176
478
|
}
|
|
177
479
|
|
|
178
480
|
module.exports = { startServer };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "laju-server",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Instant static file server
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "Instant static file server - supports large files up to 100GB+",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"laju-server": "./bin/cli.js"
|
|
@@ -12,14 +12,13 @@
|
|
|
12
12
|
"keywords": [
|
|
13
13
|
"static",
|
|
14
14
|
"server",
|
|
15
|
-
"hyper-express",
|
|
16
15
|
"file-server",
|
|
17
|
-
"cli"
|
|
16
|
+
"cli",
|
|
17
|
+
"large-files"
|
|
18
18
|
],
|
|
19
19
|
"author": "",
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"hyper-express": "^6.17.3",
|
|
23
22
|
"mime-types": "^2.1.35"
|
|
24
23
|
}
|
|
25
24
|
}
|