mock-service-cli 3.6.1 → 3.6.3

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.
@@ -15,41 +15,47 @@
15
15
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
16
16
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
17
  min-height: 100vh;
18
- padding: 20px;
18
+ padding: 12px;
19
+ margin: 0;
20
+ box-sizing: border-box;
19
21
  }
20
22
 
21
23
  .container {
22
24
  max-width: 1200px;
23
25
  margin: 0 auto;
26
+ display: flex;
27
+ flex-direction: column;
28
+ height: calc(100vh - 24px);
24
29
  }
25
30
 
26
31
  .header {
27
32
  background: white;
28
- padding: 24px;
29
- border-radius: 12px;
33
+ padding: 12px 16px;
34
+ border-radius: 8px;
30
35
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
31
- margin-bottom: 20px;
36
+ margin-bottom: 12px;
37
+ flex-shrink: 0;
32
38
  }
33
39
 
34
40
  .header h1 {
35
41
  color: #333;
36
- font-size: 28px;
37
- margin-bottom: 8px;
42
+ font-size: 20px;
43
+ margin-bottom: 4px;
38
44
  }
39
45
 
40
46
  .breadcrumb {
41
47
  display: flex;
42
48
  align-items: center;
43
- gap: 8px;
49
+ gap: 6px;
44
50
  flex-wrap: wrap;
45
- margin-top: 16px;
51
+ margin-top: 8px;
46
52
  }
47
53
 
48
54
  .breadcrumb-item {
49
- padding: 6px 12px;
55
+ padding: 4px 8px;
50
56
  background: #f0f0f0;
51
- border-radius: 6px;
52
- font-size: 14px;
57
+ border-radius: 4px;
58
+ font-size: 12px;
53
59
  color: #666;
54
60
  cursor: pointer;
55
61
  transition: background 0.2s;
@@ -71,31 +77,43 @@
71
77
 
72
78
  .file-container {
73
79
  background: white;
74
- border-radius: 12px;
80
+ border-radius: 8px;
75
81
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
76
82
  overflow: hidden;
83
+ flex: 1;
84
+ display: flex;
85
+ flex-direction: column;
77
86
  }
78
87
 
79
88
  .toolbar {
80
- padding: 16px 20px;
89
+ padding: 10px 12px;
81
90
  border-bottom: 1px solid #eee;
82
91
  display: flex;
83
- gap: 12px;
92
+ gap: 8px;
84
93
  align-items: center;
85
94
  flex-wrap: wrap;
95
+ flex-shrink: 0;
96
+ }
97
+
98
+ .stats-info {
99
+ font-size: 12px;
100
+ color: #666;
101
+ padding: 4px 8px;
102
+ background: #f8f8f8;
103
+ border-radius: 4px;
86
104
  }
87
105
 
88
106
  .search-box {
89
107
  flex: 1;
90
- min-width: 200px;
108
+ min-width: 150px;
91
109
  }
92
110
 
93
111
  .search-box input {
94
112
  width: 100%;
95
- padding: 8px 16px;
113
+ padding: 6px 12px;
96
114
  border: 1px solid #ddd;
97
- border-radius: 6px;
98
- font-size: 14px;
115
+ border-radius: 4px;
116
+ font-size: 12px;
99
117
  outline: none;
100
118
  transition: border-color 0.2s;
101
119
  }
@@ -106,15 +124,15 @@
106
124
 
107
125
  .sort-controls {
108
126
  display: flex;
109
- gap: 8px;
127
+ gap: 6px;
110
128
  align-items: center;
111
129
  }
112
130
 
113
131
  .sort-select {
114
- padding: 8px 12px;
132
+ padding: 6px 10px;
115
133
  border: 1px solid #ddd;
116
- border-radius: 6px;
117
- font-size: 14px;
134
+ border-radius: 4px;
135
+ font-size: 12px;
118
136
  cursor: pointer;
119
137
  outline: none;
120
138
  transition: border-color 0.2s;
@@ -125,11 +143,11 @@
125
143
  }
126
144
 
127
145
  .sort-order-btn {
128
- padding: 8px 12px;
146
+ padding: 6px 10px;
129
147
  background: #f0f0f0;
130
148
  border: none;
131
- border-radius: 6px;
132
- font-size: 14px;
149
+ border-radius: 4px;
150
+ font-size: 12px;
133
151
  cursor: pointer;
134
152
  transition: background 0.2s;
135
153
  }
@@ -139,15 +157,15 @@
139
157
  }
140
158
 
141
159
  .back-btn {
142
- padding: 8px 16px;
160
+ padding: 6px 12px;
143
161
  background: #f0f0f0;
144
162
  border: none;
145
- border-radius: 6px;
146
- font-size: 14px;
163
+ border-radius: 4px;
164
+ font-size: 12px;
147
165
  cursor: pointer;
148
166
  display: flex;
149
167
  align-items: center;
150
- gap: 6px;
168
+ gap: 4px;
151
169
  transition: background 0.2s;
152
170
  }
153
171
 
@@ -163,31 +181,31 @@
163
181
  .view-toggle {
164
182
  display: flex;
165
183
  background: #f0f0f0;
166
- border-radius: 6px;
167
- padding: 4px;
184
+ border-radius: 4px;
185
+ padding: 3px;
168
186
  }
169
187
 
170
188
  .view-btn {
171
- padding: 6px 12px;
189
+ padding: 4px 10px;
172
190
  background: transparent;
173
191
  border: none;
174
- border-radius: 4px;
192
+ border-radius: 3px;
175
193
  cursor: pointer;
176
- font-size: 14px;
194
+ font-size: 12px;
177
195
  transition: all 0.2s;
178
196
  }
179
197
 
180
198
  .view-btn.active {
181
199
  background: white;
182
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
200
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
183
201
  }
184
202
 
185
203
  .refresh-btn {
186
- padding: 8px 12px;
204
+ padding: 6px 10px;
187
205
  background: #f0f0f0;
188
206
  border: none;
189
- border-radius: 6px;
190
- font-size: 16px;
207
+ border-radius: 4px;
208
+ font-size: 14px;
191
209
  cursor: pointer;
192
210
  transition: background 0.2s;
193
211
  }
@@ -197,10 +215,10 @@
197
215
  }
198
216
 
199
217
  .toggle-hidden-btn {
200
- padding: 8px 12px;
218
+ padding: 6px 10px;
201
219
  background: #f0f0f0;
202
220
  border: none;
203
- border-radius: 6px;
221
+ border-radius: 4px;
204
222
  font-size: 14px;
205
223
  cursor: pointer;
206
224
  transition: background 0.2s;
@@ -215,6 +233,47 @@
215
233
  color: white;
216
234
  }
217
235
 
236
+ .context-menu {
237
+ position: fixed;
238
+ background: white;
239
+ border-radius: 8px;
240
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
241
+ padding: 8px 0;
242
+ z-index: 10000;
243
+ min-width: 160px;
244
+ display: none;
245
+ }
246
+
247
+ .context-menu.active {
248
+ display: block;
249
+ }
250
+
251
+ .context-menu-item {
252
+ padding: 10px 16px;
253
+ cursor: pointer;
254
+ font-size: 14px;
255
+ color: #333;
256
+ display: flex;
257
+ align-items: center;
258
+ gap: 8px;
259
+ transition: background 0.2s;
260
+ }
261
+
262
+ .context-menu-item:hover {
263
+ background: #f0f0f0;
264
+ }
265
+
266
+ .context-menu-item.disabled {
267
+ opacity: 0.5;
268
+ cursor: not-allowed;
269
+ }
270
+
271
+ .context-menu-divider {
272
+ height: 1px;
273
+ background: #eee;
274
+ margin: 4px 0;
275
+ }
276
+
218
277
  .copy-btn {
219
278
  padding: 4px 8px;
220
279
  background: #f0f0f0;
@@ -231,23 +290,26 @@
231
290
  }
232
291
 
233
292
  .file-grid {
234
- padding: 20px;
293
+ padding: 8px;
235
294
  display: grid;
236
- grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
237
- gap: 16px;
295
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
296
+ gap: 8px;
297
+ overflow: auto;
298
+ flex: 1;
238
299
  }
239
300
 
240
301
  .file-grid.list-view {
241
302
  grid-template-columns: 1fr;
242
303
  gap: 0;
304
+ padding: 4px 0;
243
305
  }
244
306
 
245
307
  .file-item {
246
308
  display: flex;
247
309
  flex-direction: column;
248
310
  align-items: center;
249
- padding: 16px;
250
- border-radius: 8px;
311
+ padding: 10px 8px;
312
+ border-radius: 6px;
251
313
  cursor: pointer;
252
314
  transition: all 0.2s;
253
315
  }
@@ -258,25 +320,26 @@
258
320
 
259
321
  .list-view .file-item {
260
322
  flex-direction: row;
261
- padding: 12px 16px;
323
+ padding: 6px 12px;
262
324
  border-bottom: 1px solid #f0f0f0;
263
325
  border-radius: 0;
326
+ min-height: auto;
264
327
  }
265
328
 
266
329
  .file-icon {
267
- width: 64px;
268
- height: 64px;
269
- margin-bottom: 12px;
330
+ width: 40px;
331
+ height: 40px;
332
+ margin-bottom: 6px;
270
333
  display: flex;
271
334
  align-items: center;
272
335
  justify-content: center;
273
336
  }
274
337
 
275
338
  .list-view .file-icon {
276
- width: 32px;
277
- height: 32px;
339
+ width: 20px;
340
+ height: 20px;
278
341
  margin-bottom: 0;
279
- margin-right: 12px;
342
+ margin-right: 8px;
280
343
  }
281
344
 
282
345
  .file-icon svg {
@@ -292,31 +355,34 @@
292
355
  .list-view .file-info {
293
356
  display: flex;
294
357
  align-items: center;
295
- gap: 16px;
358
+ gap: 10px;
296
359
  text-align: left;
297
360
  flex: 1;
298
361
  }
299
362
 
300
363
  .file-name {
301
- font-size: 14px;
364
+ font-size: 12px;
302
365
  color: #333;
303
366
  font-weight: 500;
304
- margin-bottom: 4px;
367
+ margin-bottom: 2px;
368
+ line-height: 1.3;
305
369
  }
306
370
 
307
371
  .list-view .file-name {
308
372
  margin-bottom: 0;
309
373
  flex: 1;
374
+ font-size: 12px;
310
375
  }
311
376
 
312
377
  .file-meta {
313
- font-size: 12px;
378
+ font-size: 10px;
314
379
  color: #999;
315
380
  }
316
381
 
317
382
  .list-view .file-meta {
318
383
  display: flex;
319
- gap: 16px;
384
+ gap: 10px;
385
+ font-size: 10px;
320
386
  }
321
387
 
322
388
  .hidden-file {
@@ -378,9 +444,10 @@
378
444
  .modal-content {
379
445
  background: white;
380
446
  border-radius: 12px;
381
- max-width: 900px;
447
+ max-width: 95vw;
448
+ max-height: 95vh;
382
449
  width: 100%;
383
- max-height: 90vh;
450
+ height: 100%;
384
451
  overflow: hidden;
385
452
  display: flex;
386
453
  flex-direction: column;
@@ -431,6 +498,130 @@
431
498
  max-height: 70vh;
432
499
  display: block;
433
500
  margin: 0 auto;
501
+ transition: transform 0.3s ease;
502
+ }
503
+
504
+ .image-preview-container {
505
+ position: relative;
506
+ display: flex;
507
+ flex-direction: column;
508
+ align-items: center;
509
+ justify-content: flex-start;
510
+ height: 100%;
511
+ overflow: auto;
512
+ }
513
+
514
+ .image-preview-controls {
515
+ display: flex;
516
+ gap: 12px;
517
+ margin-bottom: 16px;
518
+ align-items: center;
519
+ position: sticky;
520
+ top: 0;
521
+ background: white;
522
+ padding: 12px;
523
+ border-radius: 8px;
524
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
525
+ z-index: 10;
526
+ flex-wrap: wrap;
527
+ justify-content: center;
528
+ }
529
+
530
+ .nav-btn {
531
+ padding: 12px 20px;
532
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
533
+ color: white;
534
+ border: none;
535
+ border-radius: 8px;
536
+ font-size: 16px;
537
+ cursor: pointer;
538
+ transition: all 0.2s;
539
+ display: flex;
540
+ align-items: center;
541
+ gap: 8px;
542
+ }
543
+
544
+ .nav-btn:hover:not(:disabled) {
545
+ transform: translateY(-2px);
546
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
547
+ }
548
+
549
+ .nav-btn:disabled {
550
+ opacity: 0.5;
551
+ cursor: not-allowed;
552
+ }
553
+
554
+ .nav-container {
555
+ display: flex;
556
+ justify-content: space-between;
557
+ align-items: center;
558
+ width: 100%;
559
+ margin-bottom: 16px;
560
+ }
561
+
562
+ .zoom-btn {
563
+ padding: 8px 16px;
564
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
565
+ color: white;
566
+ border: none;
567
+ border-radius: 6px;
568
+ font-size: 14px;
569
+ cursor: pointer;
570
+ transition: all 0.2s;
571
+ display: flex;
572
+ align-items: center;
573
+ gap: 6px;
574
+ }
575
+
576
+ .zoom-btn:hover {
577
+ transform: translateY(-1px);
578
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
579
+ }
580
+
581
+ .zoom-level {
582
+ font-size: 14px;
583
+ color: #666;
584
+ font-weight: 500;
585
+ min-width: 60px;
586
+ text-align: center;
587
+ }
588
+
589
+ .rotate-btn {
590
+ padding: 8px 16px;
591
+ background: #f0f0f0;
592
+ color: #333;
593
+ border: none;
594
+ border-radius: 6px;
595
+ font-size: 14px;
596
+ cursor: pointer;
597
+ transition: all 0.2s;
598
+ }
599
+
600
+ .rotate-btn:hover {
601
+ background: #e0e0e0;
602
+ }
603
+
604
+ .binary-preview {
605
+ background: #1e1e1e;
606
+ color: #d4d4d4;
607
+ padding: 20px;
608
+ border-radius: 8px;
609
+ max-height: 70vh;
610
+ overflow: auto;
611
+ font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
612
+ font-size: 12px;
613
+ line-height: 1.5;
614
+ white-space: pre-wrap;
615
+ word-wrap: break-word;
616
+ margin: 0;
617
+ }
618
+
619
+ .binary-info {
620
+ margin-bottom: 12px;
621
+ padding: 12px;
622
+ background: #2d2d2d;
623
+ border-radius: 6px;
624
+ color: #9cdcfe;
434
625
  }
435
626
 
436
627
  .text-preview {
@@ -448,6 +639,19 @@
448
639
  margin: 0;
449
640
  }
450
641
 
642
+ .html-rendered {
643
+ background: white;
644
+ padding: 20px;
645
+ border-radius: 8px;
646
+ max-height: 70vh;
647
+ overflow: auto;
648
+ border: 1px solid #eee;
649
+ }
650
+
651
+ .html-source .text-preview {
652
+ margin: 0;
653
+ }
654
+
451
655
  .preview-container {
452
656
  display: flex;
453
657
  flex-direction: column;
@@ -532,7 +736,7 @@
532
736
  <div class="file-container">
533
737
  <div class="toolbar">
534
738
  <button class="back-btn" id="backBtn" onclick="goBack()">
535
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
739
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
536
740
  <polyline points="15 18 9 12 15 6"></polyline>
537
741
  </svg>
538
742
  返回上级
@@ -546,6 +750,7 @@
546
750
  >
547
751
  👁️
548
752
  </button>
753
+ <div class="stats-info" id="statsInfo"></div>
549
754
  <div class="search-box">
550
755
  <input type="text" id="searchInput" placeholder="搜索文件名..." oninput="handleSearch()" />
551
756
  </div>
@@ -578,6 +783,16 @@
578
783
  </div>
579
784
  </div>
580
785
 
786
+ <div class="context-menu" id="contextMenu">
787
+ <div class="context-menu-item" id="ctxOpen" onclick="handleContextMenuAction('open')">📂 打开</div>
788
+ <div class="context-menu-item" id="ctxCopyPath" onclick="handleContextMenuAction('copyPath')">📋 复制路径</div>
789
+ <div class="context-menu-divider"></div>
790
+ <div class="context-menu-item" id="ctxOpenInExplorer" onclick="handleContextMenuAction('openInExplorer')">
791
+ 🖥️ 在文件管理器中打开
792
+ </div>
793
+ <div class="context-menu-item" id="ctxDownload" onclick="handleContextMenuAction('download')">⬇️ 下载</div>
794
+ </div>
795
+
581
796
  <script>
582
797
  let currentPath = '/';
583
798
  let currentView = 'grid';
@@ -588,17 +803,98 @@
588
803
  let sortAscending = true;
589
804
  let showHiddenFiles = true;
590
805
  let currentPreviewContent = '';
806
+ let currentZoom = 1;
807
+ let currentRotation = 0;
808
+ let currentContextMenuFile = null;
809
+ let currentImageIndex = -1;
810
+ let imageFiles = [];
591
811
 
592
812
  document.addEventListener('DOMContentLoaded', () => {
593
813
  loadDirectory('/');
814
+
815
+ const fileGrid = document.getElementById('fileGrid');
816
+ fileGrid.addEventListener('contextmenu', e => {
817
+ const fileItem = e.target.closest('.file-item');
818
+ if (fileItem) {
819
+ e.preventDefault();
820
+ const filePath = fileItem.dataset.path;
821
+ const isDirectory = fileItem.dataset.isDirectory === 'true';
822
+ showContextMenu(e.clientX, e.clientY, filePath, isDirectory);
823
+ }
824
+ });
825
+
826
+ document.addEventListener('click', () => {
827
+ hideContextMenu();
828
+ });
829
+
830
+ document.addEventListener('scroll', () => {
831
+ hideContextMenu();
832
+ });
594
833
  });
595
834
 
835
+ function showContextMenu(x, y, filePath, isDirectory) {
836
+ currentContextMenuFile = { path: filePath, isDirectory };
837
+
838
+ const menu = document.getElementById('contextMenu');
839
+ const ctxDownload = document.getElementById('ctxDownload');
840
+
841
+ if (isDirectory) {
842
+ ctxDownload.classList.add('disabled');
843
+ } else {
844
+ ctxDownload.classList.remove('disabled');
845
+ }
846
+
847
+ menu.style.left = `${x}px`;
848
+ menu.style.top = `${y}px`;
849
+ menu.classList.add('active');
850
+
851
+ const rect = menu.getBoundingClientRect();
852
+ if (rect.right > window.innerWidth) {
853
+ menu.style.left = `${x - rect.width}px`;
854
+ }
855
+ if (rect.bottom > window.innerHeight) {
856
+ menu.style.top = `${y - rect.height}px`;
857
+ }
858
+ }
859
+
860
+ function hideContextMenu() {
861
+ const menu = document.getElementById('contextMenu');
862
+ menu.classList.remove('active');
863
+ }
864
+
865
+ function handleContextMenuAction(action) {
866
+ if (!currentContextMenuFile) return;
867
+
868
+ const { path, isDirectory } = currentContextMenuFile;
869
+
870
+ switch (action) {
871
+ case 'open':
872
+ handleItemClick(path, isDirectory);
873
+ break;
874
+ case 'copyPath':
875
+ copyToClipboard(path);
876
+ break;
877
+ case 'openInExplorer':
878
+ openInExplorer(path);
879
+ break;
880
+ case 'download':
881
+ if (!isDirectory) {
882
+ downloadFile(path, path.split('/').pop());
883
+ }
884
+ break;
885
+ }
886
+
887
+ hideContextMenu();
888
+ }
889
+
596
890
  async function loadDirectory(path) {
597
891
  // console.log('Loading directory:', path);
598
892
  const fileGrid = document.getElementById('fileGrid');
599
893
 
894
+ // 保存旧路径,以便失败时可以回退
895
+ const oldCurrentPath = currentPath;
896
+
600
897
  // 重置所有状态
601
- currentPath = '/';
602
898
  allFiles = [];
603
899
  currentFiles = [];
604
900
 
@@ -637,16 +933,31 @@
637
933
  searchQuery = '';
638
934
  } catch (error) {
639
935
  console.error('Error loading directory:', error);
936
+ // 回退到旧路径
937
+ currentPath = oldCurrentPath;
938
+ // 计算旧路径的父路径
939
+ const parentPath = currentPath === '/' ? null : currentPath.substring(0, currentPath.lastIndexOf('/')) || '/';
940
+ updateBackButton(parentPath);
640
941
  fileGrid.innerHTML = `<div class="empty-state"><p>错误: ${error.message}</p></div>`;
641
942
  }
642
943
  }
643
944
 
945
+ function updateStatsInfo(files) {
946
+ const statsInfo = document.getElementById('statsInfo');
947
+ if (!statsInfo) return;
948
+
949
+ const dirCount = files.filter(f => f.isDirectory).length;
950
+ const fileCount = files.length - dirCount;
951
+ statsInfo.textContent = `${dirCount} 个文件夹, ${fileCount} 个文件`;
952
+ }
953
+
644
954
  function processAndRenderFiles() {
645
955
  // console.log('processAndRenderFiles called, allFiles:', allFiles);
646
956
 
647
957
  if (!allFiles) {
648
958
  console.error('allFiles is null or undefined');
649
959
  renderFiles([]);
960
+ updateStatsInfo([]);
650
961
  return;
651
962
  }
652
963
 
@@ -689,6 +1000,7 @@
689
1000
 
690
1001
  currentFiles = files;
691
1002
  // console.log('Rendering', files.length, 'files');
1003
+ updateStatsInfo(files);
692
1004
  renderFiles(files);
693
1005
  }
694
1006
 
@@ -809,7 +1121,9 @@
809
1121
 
810
1122
  if (currentView === 'list') {
811
1123
  html += `
812
- <div class="file-item ${hiddenClass}">
1124
+ <div class="file-item ${hiddenClass}" data-path="${file.path}" data-is-directory="${
1125
+ file.isDirectory
1126
+ }">
813
1127
  <div class="file-icon" onclick="handleItemClick('${file.path}', ${
814
1128
  file.isDirectory
815
1129
  })">${icon}</div>
@@ -839,9 +1153,9 @@
839
1153
  `;
840
1154
  } else {
841
1155
  html += `
842
- <div class="file-item ${hiddenClass}" onclick="handleItemClick('${file.path}', ${
1156
+ <div class="file-item ${hiddenClass}" data-path="${file.path}" data-is-directory="${
843
1157
  file.isDirectory
844
- })">
1158
+ }" onclick="handleItemClick('${file.path}', ${file.isDirectory})">
845
1159
  <div class="file-icon">${icon}</div>
846
1160
  <div class="file-info">
847
1161
  <div class="file-name">${escapeHtml(file.name)}</div>
@@ -934,6 +1248,193 @@
934
1248
  }, 2000);
935
1249
  }
936
1250
 
1251
+ function getImageFilesList() {
1252
+ const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'];
1253
+ return currentFiles.filter(file => {
1254
+ if (file.isDirectory) return false;
1255
+ const ext = file.name.split('.').pop().toLowerCase();
1256
+ return imageExts.includes(ext);
1257
+ });
1258
+ }
1259
+
1260
+ function findCurrentImageIndex(path) {
1261
+ return imageFiles.findIndex(file => file.path === path);
1262
+ }
1263
+
1264
+ async function navigateImage(direction) {
1265
+ if (imageFiles.length === 0) return;
1266
+
1267
+ let newIndex = currentImageIndex + direction;
1268
+ if (newIndex < 0) newIndex = imageFiles.length - 1;
1269
+ if (newIndex >= imageFiles.length) newIndex = 0;
1270
+
1271
+ if (imageFiles[newIndex]) {
1272
+ previewFile(imageFiles[newIndex].path);
1273
+ }
1274
+ }
1275
+
1276
+ async function getFileInfo(path) {
1277
+ try {
1278
+ const file = allFiles.find(f => f.path === path);
1279
+ if (file) {
1280
+ return {
1281
+ size: file.size,
1282
+ mtime: file.mtime,
1283
+ name: file.name
1284
+ };
1285
+ }
1286
+ } catch (e) {
1287
+ console.error('Error getting file info:', e);
1288
+ }
1289
+ return null;
1290
+ }
1291
+
1292
+ const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024; // 10MB
1293
+ const IMAGE_EXTS = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'];
1294
+ const HTML_EXTS = ['html', 'htm'];
1295
+ const BINARY_EXTS = [
1296
+ 'exe',
1297
+ 'dll',
1298
+ 'so',
1299
+ 'dylib',
1300
+ 'bin',
1301
+ 'dat',
1302
+ 'class',
1303
+ 'pyc',
1304
+ 'pyd',
1305
+ 'obj',
1306
+ 'o',
1307
+ 'a',
1308
+ 'lib',
1309
+ 'la',
1310
+ 'lo',
1311
+ 'elf',
1312
+ 'mach-o',
1313
+ 'coff',
1314
+ 'pe',
1315
+ 'msi',
1316
+ 'sys',
1317
+ 'drv',
1318
+ 'vxd',
1319
+ 'ocx',
1320
+ 'cpl',
1321
+ 'scr',
1322
+ 'com',
1323
+ 'bat',
1324
+ 'cmd',
1325
+ 'ps1',
1326
+ 'psm1',
1327
+ 'psd1',
1328
+ 'jar',
1329
+ 'war',
1330
+ 'ear',
1331
+ 'nar',
1332
+ 'sar',
1333
+ 'rar',
1334
+ 'zip',
1335
+ '7z',
1336
+ 'tar',
1337
+ 'gz',
1338
+ 'bz2',
1339
+ 'xz',
1340
+ 'lz',
1341
+ 'lzma',
1342
+ 'z',
1343
+ 'Z',
1344
+ 'cab',
1345
+ 'iso',
1346
+ 'img',
1347
+ 'dmg',
1348
+ 'pkg',
1349
+ 'deb',
1350
+ 'rpm',
1351
+ 'apk',
1352
+ 'msix',
1353
+ 'appx',
1354
+ 'snap',
1355
+ 'flatpak',
1356
+ 'appimage',
1357
+ 'swf',
1358
+ 'flv',
1359
+ 'pdf',
1360
+ 'doc',
1361
+ 'docx',
1362
+ 'xls',
1363
+ 'xlsx',
1364
+ 'ppt',
1365
+ 'pptx',
1366
+ 'odt',
1367
+ 'ods',
1368
+ 'odp',
1369
+ 'odg',
1370
+ 'odf',
1371
+ 'rtf',
1372
+ 'wps',
1373
+ 'wks',
1374
+ 'wdb',
1375
+ 'mdb',
1376
+ 'accdb',
1377
+ 'pub',
1378
+ 'vsd',
1379
+ 'vsdx',
1380
+ 'mpp',
1381
+ 'mpx',
1382
+ 'one',
1383
+ 'onetoc2',
1384
+ 'msg',
1385
+ 'eml',
1386
+ 'pst',
1387
+ 'ost',
1388
+ 'ics',
1389
+ 'vcf',
1390
+ 'dwg',
1391
+ 'dxf',
1392
+ 'dgn',
1393
+ 'stl',
1394
+ '3ds',
1395
+ 'max',
1396
+ 'ma',
1397
+ 'mb',
1398
+ 'blend'
1399
+ ];
1400
+ let currentPreviewContentData = '';
1401
+ let isHtmlRenderMode = false;
1402
+
1403
+ function toggleHtmlPreview() {
1404
+ isHtmlRenderMode = !isHtmlRenderMode;
1405
+ const contentDiv = document.getElementById('htmlPreviewContent');
1406
+ const toggleBtn = document.getElementById('toggleHtmlBtn');
1407
+
1408
+ if (!contentDiv) return;
1409
+
1410
+ if (isHtmlRenderMode) {
1411
+ contentDiv.innerHTML = currentPreviewContentData;
1412
+ contentDiv.className = 'html-rendered';
1413
+ toggleBtn.textContent = '📝 查看源码';
1414
+ } else {
1415
+ contentDiv.innerHTML = `<pre class="text-preview">${escapeHtml(currentPreviewContentData)}</pre>`;
1416
+ contentDiv.className = 'html-source';
1417
+ toggleBtn.textContent = '🌐 渲染预览';
1418
+ }
1419
+ }
1420
+
1421
+ function renderBinaryPreview(blob, filename) {
1422
+ const url = URL.createObjectURL(blob);
1423
+ return `
1424
+ <div class="preview-container">
1425
+ <div class="binary-info">
1426
+ <strong>文件信息</strong><br>
1427
+ 文件大小: ${formatSize(blob.size)}<br>
1428
+ 文件类型: ${blob.type || 'Unknown'}
1429
+ </div>
1430
+ <div class="empty-state">
1431
+ <p>该文件类型暂不支持预览</p>
1432
+ <p><a href="${url}" download="${filename}">点击下载</a></p>
1433
+ </div>
1434
+ </div>
1435
+ `;
1436
+ }
1437
+
937
1438
  async function previewFile(path) {
938
1439
  const modal = document.getElementById('previewModal');
939
1440
  const title = document.getElementById('previewTitle');
@@ -943,41 +1444,128 @@
943
1444
  body.innerHTML = '<div class="loading"><div class="spinner"></div><p>加载中...</p></div>';
944
1445
  modal.classList.add('active');
945
1446
 
1447
+ currentZoom = 1;
1448
+ currentRotation = 0;
1449
+ currentPreviewContentData = '';
1450
+ isHtmlRenderMode = false;
1451
+
1452
+ imageFiles = getImageFilesList();
1453
+ currentImageIndex = findCurrentImageIndex(path);
1454
+
946
1455
  try {
947
1456
  const ext = path.split('.').pop().toLowerCase();
948
- const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'];
949
1457
 
950
- if (imageExts.includes(ext)) {
951
- body.innerHTML = `<img src="/__api/file?path=${encodeURIComponent(
952
- path
953
- )}" class="image-preview" alt="Preview">`;
1458
+ const fileInfo = await getFileInfo(path);
1459
+ const isLargeFile = fileInfo && fileInfo.size > LARGE_FILE_THRESHOLD;
1460
+
1461
+ if (IMAGE_EXTS.includes(ext)) {
1462
+ const hasPrev = imageFiles.length > 0 && currentImageIndex > 0;
1463
+ const hasNext = imageFiles.length > 0 && currentImageIndex < imageFiles.length - 1;
1464
+
1465
+ body.innerHTML = `
1466
+ <div class="image-preview-container">
1467
+ <div class="nav-container">
1468
+ <button class="nav-btn" onclick="navigateImage(-1)" ${!hasPrev ? 'disabled' : ''}>
1469
+ ◀ 上一张
1470
+ </button>
1471
+ <span style="font-size: 14px; color: #666;">
1472
+ ${imageFiles.length > 0 ? `${currentImageIndex + 1}/${imageFiles.length}` : ''}
1473
+ ${fileInfo ? ` | ${formatSize(fileInfo.size)}` : ''}
1474
+ </span>
1475
+ <button class="nav-btn" onclick="navigateImage(1)" ${!hasNext ? 'disabled' : ''}>
1476
+ 下一张 ▶
1477
+ </button>
1478
+ </div>
1479
+ <div class="image-preview-controls">
1480
+ <button class="zoom-btn" onclick="zoomImage(-0.1)">➖</button>
1481
+ <div class="zoom-level" id="zoomLevel">100%</div>
1482
+ <button class="zoom-btn" onclick="zoomImage(0.1)">➕</button>
1483
+ <button class="rotate-btn" onclick="rotateImage(-90)">↺</button>
1484
+ <button class="rotate-btn" onclick="rotateImage(90)">↻</button>
1485
+ <button class="zoom-btn" onclick="resetImage()">🔄 重置</button>
1486
+ </div>
1487
+ <img
1488
+ src="/__api/file?path=${encodeURIComponent(path)}"
1489
+ class="image-preview"
1490
+ id="previewImage"
1491
+ alt="Preview"
1492
+ style="transform: scale(1) rotate(0deg)"
1493
+ >
1494
+ </div>
1495
+ `;
1496
+ } else if (BINARY_EXTS.includes(ext)) {
1497
+ const downloadUrl = `/__api/file?path=${encodeURIComponent(path)}`;
1498
+ const fileSize = fileInfo ? formatSize(fileInfo.size) : '未知大小';
1499
+
1500
+ body.innerHTML = `
1501
+ <div class="preview-container">
1502
+ <div class="binary-info">
1503
+ <strong>二进制/特殊文件</strong><br>
1504
+ 文件大小: ${fileSize}<br>
1505
+ 文件类型: ${ext.toUpperCase()}
1506
+ </div>
1507
+ <div class="empty-state">
1508
+ <p>此文件类型无法预览内容</p>
1509
+ <p><a href="${downloadUrl}" download="${path.split('/').pop()}">点击下载</a></p>
1510
+ </div>
1511
+ </div>
1512
+ `;
954
1513
  } else {
955
- const response = await fetch(`/__api/file?path=${encodeURIComponent(path)}`);
956
- const contentType = response.headers.get('content-type');
957
-
958
- if (contentType && contentType.includes('application/json')) {
959
- const data = await response.json();
960
- if (data.type === 'text') {
961
- currentPreviewContent = data.content;
962
- body.innerHTML = `
963
- <div class="preview-container">
964
- <div class="preview-header">
965
- <button class="copy-btn" onclick="copyPreviewContent()">📋 复制全部</button>
966
- </div>
967
- <pre class="text-preview">${escapeHtml(data.content)}</pre>
1514
+ if (isLargeFile) {
1515
+ const downloadUrl = `/__api/file?path=${encodeURIComponent(path)}`;
1516
+ body.innerHTML = `
1517
+ <div class="preview-container">
1518
+ <div class="binary-info">
1519
+ <strong>大文件</strong><br>
1520
+ 文件大小: ${formatSize(fileInfo.size)}<br>
1521
+ <span style="color: #ff9800;">⚠️ 文件较大,为避免性能问题,请下载后查看</span>
968
1522
  </div>
969
- `;
1523
+ <div class="empty-state">
1524
+ <p>该文件超过 ${formatSize(LARGE_FILE_THRESHOLD)},不建议在线预览</p>
1525
+ <p><a href="${downloadUrl}" download="${path.split('/').pop()}">点击下载</a></p>
1526
+ </div>
1527
+ </div>
1528
+ `;
1529
+ } else {
1530
+ const response = await fetch(`/__api/file?path=${encodeURIComponent(path)}`);
1531
+ const contentType = response.headers.get('content-type');
1532
+
1533
+ if (contentType && contentType.includes('application/json')) {
1534
+ const data = await response.json();
1535
+ if (data.type === 'text') {
1536
+ currentPreviewContent = data.content;
1537
+ currentPreviewContentData = data.content;
1538
+
1539
+ if (HTML_EXTS.includes(ext)) {
1540
+ body.innerHTML = `
1541
+ <div class="preview-container">
1542
+ <div class="preview-header">
1543
+ <button class="copy-btn" onclick="copyPreviewContent()">📋 复制全部</button>
1544
+ <button class="copy-btn" id="toggleHtmlBtn" onclick="toggleHtmlPreview()" style="margin-left: 8px;">🌐 渲染预览</button>
1545
+ </div>
1546
+ <div id="htmlPreviewContent" class="html-source">
1547
+ <pre class="text-preview">${escapeHtml(data.content)}</pre>
1548
+ </div>
1549
+ </div>
1550
+ `;
1551
+ } else {
1552
+ body.innerHTML = `
1553
+ <div class="preview-container">
1554
+ <div class="preview-header">
1555
+ <button class="copy-btn" onclick="copyPreviewContent()">📋 复制全部</button>
1556
+ </div>
1557
+ <pre class="text-preview">${escapeHtml(data.content)}</pre>
1558
+ </div>
1559
+ `;
1560
+ }
1561
+ } else {
1562
+ const blob = await response.blob();
1563
+ body.innerHTML = renderBinaryPreview(blob, path.split('/').pop());
1564
+ }
970
1565
  } else {
971
- body.innerHTML = `<div class="empty-state"><p>该文件类型暂不支持预览</p><p><a href="/__api/file?path=${encodeURIComponent(
972
- path
973
- )}" download>点击下载</a></p></div>`;
1566
+ const blob = await response.blob();
1567
+ body.innerHTML = renderBinaryPreview(blob, path.split('/').pop());
974
1568
  }
975
- } else {
976
- const blob = await response.blob();
977
- const url = URL.createObjectURL(blob);
978
- body.innerHTML = `<div class="empty-state"><p>该文件类型暂不支持预览</p><p><a href="${url}" download="${path
979
- .split('/')
980
- .pop()}">点击下载</a></p></div>`;
981
1569
  }
982
1570
  }
983
1571
  } catch (error) {
@@ -985,6 +1573,37 @@
985
1573
  }
986
1574
  }
987
1575
 
1576
+ function zoomImage(delta) {
1577
+ const img = document.getElementById('previewImage');
1578
+ if (!img) return;
1579
+
1580
+ currentZoom = Math.max(0.1, Math.min(5, currentZoom + delta));
1581
+ updateImageTransform();
1582
+ document.getElementById('zoomLevel').textContent = Math.round(currentZoom * 100) + '%';
1583
+ }
1584
+
1585
+ function rotateImage(degrees) {
1586
+ const img = document.getElementById('previewImage');
1587
+ if (!img) return;
1588
+
1589
+ currentRotation = (currentRotation + degrees) % 360;
1590
+ updateImageTransform();
1591
+ }
1592
+
1593
+ function resetImage() {
1594
+ currentZoom = 1;
1595
+ currentRotation = 0;
1596
+ updateImageTransform();
1597
+ document.getElementById('zoomLevel').textContent = '100%';
1598
+ }
1599
+
1600
+ function updateImageTransform() {
1601
+ const img = document.getElementById('previewImage');
1602
+ if (!img) return;
1603
+
1604
+ img.style.transform = `scale(${currentZoom}) rotate(${currentRotation}deg)`;
1605
+ }
1606
+
988
1607
  function closeModal() {
989
1608
  document.getElementById('previewModal').classList.remove('active');
990
1609
  }
@@ -1038,18 +1657,70 @@
1038
1657
  png: '#ff6b6b',
1039
1658
  gif: '#ff6b6b',
1040
1659
  svg: '#ff6b6b',
1660
+ webp: '#ff6b6b',
1661
+ bmp: '#ff6b6b',
1662
+ ico: '#ff6b6b',
1041
1663
  zip: '#4ecdc4',
1042
1664
  '7z': '#4ecdc4',
1043
1665
  rar: '#4ecdc4',
1044
1666
  tar: '#4ecdc4',
1045
1667
  gz: '#4ecdc4',
1668
+ tgz: '#4ecdc4',
1669
+ xz: '#4ecdc4',
1046
1670
  txt: '#95a5a6',
1047
1671
  log: '#95a5a6',
1048
1672
  py: '#3776ab',
1049
1673
  java: '#007396',
1050
1674
  go: '#00add8',
1051
1675
  rs: '#dea584',
1052
- ts: '#3178c6'
1676
+ ts: '#3178c6',
1677
+ vue: '#42b883',
1678
+ react: '#61dafb',
1679
+ php: '#777bb4',
1680
+ rb: '#cc342d',
1681
+ c: '#555555',
1682
+ cpp: '#00599c',
1683
+ h: '#555555',
1684
+ hpp: '#00599c',
1685
+ cs: '#239120',
1686
+ swift: '#fa7343',
1687
+ kotlin: '#a97bff',
1688
+ dart: '#0175c2',
1689
+ sql: '#003b57',
1690
+ sh: '#4eaa25',
1691
+ bash: '#4eaa25',
1692
+ ps1: '#012456',
1693
+ bat: '#4caf50',
1694
+ cmd: '#4caf50',
1695
+ exe: '#ff4444',
1696
+ dll: '#ff4444',
1697
+ so: '#ff4444',
1698
+ dylib: '#ff4444',
1699
+ bin: '#ff4444',
1700
+ dat: '#ff4444',
1701
+ class: '#ea2d2e',
1702
+ jar: '#ea2d2e',
1703
+ war: '#ea2d2e',
1704
+ pyc: '#3776ab',
1705
+ pyd: '#3776ab',
1706
+ obj: '#555555',
1707
+ o: '#555555',
1708
+ pdf: '#f40f02',
1709
+ doc: '#2b579a',
1710
+ docx: '#2b579a',
1711
+ xls: '#217346',
1712
+ xlsx: '#217346',
1713
+ ppt: '#d24726',
1714
+ pptx: '#d24726',
1715
+ mp3: '#ff5722',
1716
+ wav: '#ff5722',
1717
+ flac: '#ff5722',
1718
+ aac: '#ff5722',
1719
+ mp4: '#2196f3',
1720
+ avi: '#2196f3',
1721
+ mov: '#2196f3',
1722
+ mkv: '#2196f3',
1723
+ webm: '#2196f3'
1053
1724
  };
1054
1725
 
1055
1726
  const color = iconColors[ext] || '#95a5a6';
@@ -1085,6 +1756,19 @@
1085
1756
  }
1086
1757
  });
1087
1758
 
1759
+ document.getElementById('previewModal').addEventListener(
1760
+ 'wheel',
1761
+ e => {
1762
+ const img = document.getElementById('previewImage');
1763
+ if (!img) return;
1764
+
1765
+ e.preventDefault();
1766
+ const delta = e.deltaY > 0 ? -0.1 : 0.1;
1767
+ zoomImage(delta);
1768
+ },
1769
+ { passive: false }
1770
+ );
1771
+
1088
1772
  document.addEventListener('keydown', e => {
1089
1773
  if (e.key === 'Escape') {
1090
1774
  closeModal();
@@ -1093,6 +1777,24 @@
1093
1777
  e.preventDefault();
1094
1778
  goBack();
1095
1779
  }
1780
+ const img = document.getElementById('previewImage');
1781
+ if (img) {
1782
+ if (e.key === '+' || e.key === '=') {
1783
+ zoomImage(0.1);
1784
+ } else if (e.key === '-') {
1785
+ zoomImage(-0.1);
1786
+ } else if (e.key === '0') {
1787
+ resetImage();
1788
+ } else if (e.key === '[' || e.key === '{') {
1789
+ rotateImage(-90);
1790
+ } else if (e.key === ']' || e.key === '}') {
1791
+ rotateImage(90);
1792
+ } else if (e.key === 'ArrowLeft') {
1793
+ navigateImage(-1);
1794
+ } else if (e.key === 'ArrowRight') {
1795
+ navigateImage(1);
1796
+ }
1797
+ }
1096
1798
  });
1097
1799
  </script>
1098
1800
  </body>
@@ -48,11 +48,24 @@ function init() {
48
48
  let dirPath = req.query.path || '/';
49
49
  dirPath = decodeURIComponent(dirPath);
50
50
 
51
- const fullPath = path.resolve(explorerRoot, '.' + dirPath);
51
+ let fullPath;
52
+ const resolvedRoot = path.resolve(explorerRoot);
52
53
 
53
- // 防止路径遍历攻击
54
- if (!fullPath.startsWith(path.resolve(explorerRoot))) {
55
- return res.status(403).json({ error: 'Access denied' });
54
+ if (explorerRoot === '/') {
55
+ // 根目录,可以直接使用绝对路径
56
+ fullPath = dirPath;
57
+ } else {
58
+ // 非根目录,应该将 dirPath 视为相对于 explorerRoot 的路径
59
+ // 如果 dirPath 以 / 开头,去掉前面的 /
60
+ const relativePath = dirPath.startsWith('/') ? dirPath.substring(1) : dirPath;
61
+ fullPath = path.resolve(explorerRoot, relativePath);
62
+ }
63
+
64
+ // 防止路径遍历攻击 - 确保路径不会越界
65
+ if (explorerRoot !== '/') {
66
+ if (!fullPath.startsWith(resolvedRoot)) {
67
+ return res.status(403).json({ error: 'Access denied' });
68
+ }
56
69
  }
57
70
 
58
71
  if (!existsSync(fullPath)) {
@@ -70,16 +83,28 @@ function init() {
70
83
 
71
84
  files.forEach(file => {
72
85
  const filePath = path.join(fullPath, file.name);
73
- const fileStats = statSync(filePath);
86
+ let fileStats;
87
+ let hasError = false;
88
+
89
+ try {
90
+ fileStats = statSync(filePath);
91
+ } catch (statError) {
92
+ hasError = true;
93
+ }
94
+
74
95
  const relativePath = path.join(dirPath, file.name);
75
96
 
97
+ // 使用 statSync 的结果,因为 readdirSync 在根目录对某些特殊目录识别不准确
98
+ const isDirectory = hasError ? file.isDirectory() : fileStats.isDirectory();
99
+
76
100
  result.push({
77
101
  name: file.name,
78
102
  path: relativePath.replace(/\\/g, '/'),
79
- isDirectory: file.isDirectory(),
80
- size: fileStats.size,
81
- mtime: fileStats.mtime,
82
- isHidden: file.name.startsWith('.')
103
+ isDirectory: isDirectory,
104
+ size: hasError ? 0 : fileStats.size,
105
+ mtime: hasError ? new Date() : fileStats.mtime,
106
+ isHidden: file.name.startsWith('.'),
107
+ error: hasError ? 'Cannot access file' : null
83
108
  });
84
109
  });
85
110
 
@@ -106,11 +131,24 @@ function init() {
106
131
  let filePath = req.query.path || '/';
107
132
  filePath = decodeURIComponent(filePath);
108
133
 
109
- const fullPath = path.resolve(explorerRoot, '.' + filePath);
134
+ let fullPath;
135
+ const resolvedRoot = path.resolve(explorerRoot);
110
136
 
111
- // 防止路径遍历攻击
112
- if (!fullPath.startsWith(path.resolve(explorerRoot))) {
113
- return res.status(403).json({ error: 'Access denied' });
137
+ if (explorerRoot === '/') {
138
+ // 根目录,可以直接使用绝对路径
139
+ fullPath = filePath;
140
+ } else {
141
+ // 非根目录,应该将 filePath 视为相对于 explorerRoot 的路径
142
+ // 如果 filePath 以 / 开头,去掉前面的 /
143
+ const relativePath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
144
+ fullPath = path.resolve(explorerRoot, relativePath);
145
+ }
146
+
147
+ // 防止路径遍历攻击 - 确保路径不会越界
148
+ if (explorerRoot !== '/') {
149
+ if (!fullPath.startsWith(resolvedRoot)) {
150
+ return res.status(403).json({ error: 'Access denied' });
151
+ }
114
152
  }
115
153
 
116
154
  if (!existsSync(fullPath)) {
@@ -152,11 +190,24 @@ function init() {
152
190
  let filePath = req.body.path || '/';
153
191
  filePath = decodeURIComponent(filePath);
154
192
 
155
- const fullPath = path.resolve(explorerRoot, '.' + filePath);
193
+ let fullPath;
194
+ const resolvedRoot = path.resolve(explorerRoot);
156
195
 
157
- // 防止路径遍历攻击
158
- if (!fullPath.startsWith(path.resolve(explorerRoot))) {
159
- return res.status(403).json({ error: 'Access denied' });
196
+ if (explorerRoot === '/') {
197
+ // 根目录,可以直接使用绝对路径
198
+ fullPath = filePath;
199
+ } else {
200
+ // 非根目录,应该将 filePath 视为相对于 explorerRoot 的路径
201
+ // 如果 filePath 以 / 开头,去掉前面的 /
202
+ const relativePath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
203
+ fullPath = path.resolve(explorerRoot, relativePath);
204
+ }
205
+
206
+ // 防止路径遍历攻击 - 确保路径不会越界
207
+ if (explorerRoot !== '/') {
208
+ if (!fullPath.startsWith(resolvedRoot)) {
209
+ return res.status(403).json({ error: 'Access denied' });
210
+ }
160
211
  }
161
212
 
162
213
  if (!existsSync(fullPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mock-service-cli",
3
- "version": "3.6.1",
3
+ "version": "3.6.3",
4
4
  "description": "🦅 Local Mock/Static/SPA server, Http?s request proxy, API overview page, File explorer",
5
5
  "main": "./lib/mockServer.js",
6
6
  "bin": {