mock-service-cli 4.3.0 → 4.3.2
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 +0 -1
- package/dist/file-explorer.html +398 -78
- package/dist/runtime.js +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
[](https://github.com/chandq/mock-service-cli/actions/workflows/release.yml)
|
|
2
2
|
[](https://coveralls.io/github/chandq/mock-service-cli?branch=master)
|
|
3
3
|
[](https://www.npmjs.com/package/mock-service-cli)
|
|
4
|
-
[](https://nodejs.org/download/release/v12.0.0/)
|
|
5
4
|
[](https://github.com/chandq/mock-service-cli/blob/master/LICENSE.md)
|
|
6
5
|
|
|
7
6
|
# 🦅 mock-service-cli
|
package/dist/file-explorer.html
CHANGED
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
box-sizing: border-box;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
.icon-sprite {
|
|
15
|
+
position: absolute;
|
|
16
|
+
width: 0;
|
|
17
|
+
height: 0;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
body {
|
|
15
22
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
16
23
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
@@ -335,6 +342,7 @@
|
|
|
335
342
|
}
|
|
336
343
|
|
|
337
344
|
.file-grid {
|
|
345
|
+
--list-row-height: 37px;
|
|
338
346
|
position: relative;
|
|
339
347
|
overflow: auto;
|
|
340
348
|
flex: 1;
|
|
@@ -351,19 +359,21 @@
|
|
|
351
359
|
top: 0;
|
|
352
360
|
left: 0;
|
|
353
361
|
right: 0;
|
|
362
|
+
overflow-anchor: none;
|
|
354
363
|
}
|
|
355
364
|
|
|
356
365
|
.virtual-content.grid-view {
|
|
357
|
-
display:
|
|
358
|
-
gap: 8px;
|
|
359
|
-
grid-auto-rows: 124px;
|
|
360
|
-
padding: 8px;
|
|
366
|
+
display: block;
|
|
361
367
|
}
|
|
362
368
|
|
|
363
369
|
.virtual-content.list-view {
|
|
364
370
|
display: block;
|
|
365
371
|
}
|
|
366
372
|
|
|
373
|
+
.virtual-content .file-item {
|
|
374
|
+
position: absolute;
|
|
375
|
+
}
|
|
376
|
+
|
|
367
377
|
.file-item {
|
|
368
378
|
display: flex;
|
|
369
379
|
flex-direction: column;
|
|
@@ -371,7 +381,7 @@
|
|
|
371
381
|
padding: 10px 8px;
|
|
372
382
|
border-radius: 6px;
|
|
373
383
|
cursor: pointer;
|
|
374
|
-
transition:
|
|
384
|
+
transition: background-color 0.2s;
|
|
375
385
|
}
|
|
376
386
|
|
|
377
387
|
.grid-view .file-item {
|
|
@@ -393,7 +403,7 @@
|
|
|
393
403
|
border-bottom: 1px solid #f0f0f0;
|
|
394
404
|
border-radius: 0;
|
|
395
405
|
min-height: auto;
|
|
396
|
-
height:
|
|
406
|
+
height: var(--list-row-height);
|
|
397
407
|
}
|
|
398
408
|
|
|
399
409
|
.file-icon {
|
|
@@ -460,6 +470,7 @@
|
|
|
460
470
|
margin-bottom: 0;
|
|
461
471
|
flex: 1;
|
|
462
472
|
font-size: 12px;
|
|
473
|
+
min-width: 0;
|
|
463
474
|
}
|
|
464
475
|
|
|
465
476
|
.file-meta {
|
|
@@ -468,6 +479,11 @@
|
|
|
468
479
|
}
|
|
469
480
|
.birthtime {
|
|
470
481
|
padding-right: 4px;
|
|
482
|
+
display: none;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.birthtime.visible {
|
|
486
|
+
display: inline-block;
|
|
471
487
|
}
|
|
472
488
|
|
|
473
489
|
.list-view .file-meta {
|
|
@@ -476,6 +492,11 @@
|
|
|
476
492
|
font-size: 10px;
|
|
477
493
|
}
|
|
478
494
|
|
|
495
|
+
.date-compact,
|
|
496
|
+
.more-actions-btn {
|
|
497
|
+
display: none;
|
|
498
|
+
}
|
|
499
|
+
|
|
479
500
|
.hidden-file {
|
|
480
501
|
opacity: 0.6;
|
|
481
502
|
}
|
|
@@ -486,7 +507,50 @@
|
|
|
486
507
|
}
|
|
487
508
|
|
|
488
509
|
.list-view .action-buttons {
|
|
489
|
-
display:
|
|
510
|
+
display: none;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.list-view .more-actions-btn {
|
|
514
|
+
display: inline-flex;
|
|
515
|
+
flex: 0 0 32px;
|
|
516
|
+
align-items: center;
|
|
517
|
+
justify-content: center;
|
|
518
|
+
align-self: center;
|
|
519
|
+
width: 32px;
|
|
520
|
+
height: 28px;
|
|
521
|
+
margin-left: 8px;
|
|
522
|
+
padding: 0;
|
|
523
|
+
border: 1px solid transparent;
|
|
524
|
+
border-radius: 5px;
|
|
525
|
+
background: #f1f3f5;
|
|
526
|
+
color: #555;
|
|
527
|
+
font-size: 20px;
|
|
528
|
+
line-height: 1;
|
|
529
|
+
cursor: pointer;
|
|
530
|
+
touch-action: manipulation;
|
|
531
|
+
-webkit-tap-highlight-color: transparent;
|
|
532
|
+
transition:
|
|
533
|
+
background-color 120ms ease,
|
|
534
|
+
border-color 120ms ease,
|
|
535
|
+
color 120ms ease,
|
|
536
|
+
transform 120ms ease;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.list-view .more-actions-btn:hover {
|
|
540
|
+
border-color: #d9dce1;
|
|
541
|
+
background: #e5e7eb;
|
|
542
|
+
color: #222;
|
|
543
|
+
transform: scale(1.06);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.list-view .more-actions-btn:active {
|
|
547
|
+
background: #d9dce1;
|
|
548
|
+
transform: scale(0.92);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.list-view .more-actions-btn:focus-visible {
|
|
552
|
+
outline: 2px solid #667eea;
|
|
553
|
+
outline-offset: 1px;
|
|
490
554
|
}
|
|
491
555
|
|
|
492
556
|
.action-btn {
|
|
@@ -523,6 +587,90 @@
|
|
|
523
587
|
color: white;
|
|
524
588
|
}
|
|
525
589
|
|
|
590
|
+
@media (max-width: 767px) {
|
|
591
|
+
.file-grid.list-view {
|
|
592
|
+
--list-row-height: 48px;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.file-grid.list-view.show-birthtime {
|
|
596
|
+
--list-row-height: 65px;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.list-view .file-item {
|
|
600
|
+
align-items: flex-start;
|
|
601
|
+
padding: 7px 10px;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.list-view .file-icon {
|
|
605
|
+
flex: 0 0 20px;
|
|
606
|
+
margin-top: 2px;
|
|
607
|
+
margin-right: 7px;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.list-view .file-select {
|
|
611
|
+
flex: 0 0 auto;
|
|
612
|
+
margin: 3px 7px 0 0;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.list-view .file-info {
|
|
616
|
+
display: flex;
|
|
617
|
+
flex: 1;
|
|
618
|
+
flex-direction: column;
|
|
619
|
+
align-items: stretch;
|
|
620
|
+
gap: 3px;
|
|
621
|
+
min-width: 0;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.list-view .file-name {
|
|
625
|
+
width: 100%;
|
|
626
|
+
overflow: hidden;
|
|
627
|
+
text-overflow: ellipsis;
|
|
628
|
+
white-space: nowrap;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.list-view .file-meta {
|
|
632
|
+
display: flex;
|
|
633
|
+
width: 100%;
|
|
634
|
+
gap: 3px;
|
|
635
|
+
flex-wrap: wrap;
|
|
636
|
+
overflow: hidden;
|
|
637
|
+
white-space: nowrap;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.list-view .file-meta > span {
|
|
641
|
+
overflow: hidden;
|
|
642
|
+
text-overflow: ellipsis;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.date-full {
|
|
646
|
+
display: none;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.date-compact {
|
|
650
|
+
display: inline;
|
|
651
|
+
vertical-align: middle;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.list-view .birthtime.visible {
|
|
655
|
+
flex-basis: 100%;
|
|
656
|
+
display: block;
|
|
657
|
+
padding-right: 0;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.list-view .action-buttons {
|
|
661
|
+
display: none !important;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.list-view .more-actions-btn {
|
|
665
|
+
align-self: flex-start;
|
|
666
|
+
margin: 10px 0 0 6px;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.file-grid.list-view.show-birthtime .more-actions-btn {
|
|
670
|
+
margin-top: 17px;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
526
674
|
.modal {
|
|
527
675
|
display: none;
|
|
528
676
|
position: fixed;
|
|
@@ -828,6 +976,15 @@
|
|
|
828
976
|
</style>
|
|
829
977
|
</head>
|
|
830
978
|
<body>
|
|
979
|
+
<svg class="icon-sprite" aria-hidden="true" focusable="false">
|
|
980
|
+
<symbol id="icon-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
981
|
+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
|
982
|
+
</symbol>
|
|
983
|
+
<symbol id="icon-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
984
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
985
|
+
<polyline points="14 2 14 8 20 8"></polyline>
|
|
986
|
+
</symbol>
|
|
987
|
+
</svg>
|
|
831
988
|
<div class="container">
|
|
832
989
|
<div class="header">
|
|
833
990
|
<h1>📁 文件浏览器</h1>
|
|
@@ -936,7 +1093,7 @@
|
|
|
936
1093
|
let searchQuery = '';
|
|
937
1094
|
let sortBy = 'name';
|
|
938
1095
|
let sortAscending = true;
|
|
939
|
-
let showHiddenFiles =
|
|
1096
|
+
let showHiddenFiles = false;
|
|
940
1097
|
let showBirthtime = false;
|
|
941
1098
|
let currentPreviewContent = '';
|
|
942
1099
|
let currentPreviewObjectUrl = null;
|
|
@@ -1047,6 +1204,12 @@
|
|
|
1047
1204
|
case 'toggleSelect':
|
|
1048
1205
|
toggleSelectedPath(path, actionBtn.checked, actionBtn.closest('.file-item'));
|
|
1049
1206
|
break;
|
|
1207
|
+
case 'more': {
|
|
1208
|
+
const fileItem = actionBtn.closest('.file-item');
|
|
1209
|
+
const rect = actionBtn.getBoundingClientRect();
|
|
1210
|
+
showContextMenu(rect.right, rect.bottom, fileItem.dataset.path, fileItem.dataset.isDirectory === 'true');
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1050
1213
|
}
|
|
1051
1214
|
return;
|
|
1052
1215
|
}
|
|
@@ -1139,7 +1302,7 @@
|
|
|
1139
1302
|
addEventListenerWithCleanup(document, 'keydown', handleDocumentKeydown);
|
|
1140
1303
|
|
|
1141
1304
|
fileGridResizeObserver = new ResizeObserver(() => {
|
|
1142
|
-
if (virtualState
|
|
1305
|
+
if (virtualState) {
|
|
1143
1306
|
renderFiles(currentFiles, false);
|
|
1144
1307
|
}
|
|
1145
1308
|
});
|
|
@@ -1475,14 +1638,18 @@
|
|
|
1475
1638
|
}
|
|
1476
1639
|
}
|
|
1477
1640
|
|
|
1641
|
+
function getDownloadUrl(filePath) {
|
|
1642
|
+
return `/__api/file?path=${encodeURIComponent(filePath)}&download=1`;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1478
1645
|
function downloadFile(filePath, fileName) {
|
|
1479
|
-
const url = `/__api/file?path=${encodeURIComponent(filePath)}`;
|
|
1480
1646
|
const a = document.createElement('a');
|
|
1481
|
-
a.href =
|
|
1482
|
-
a.download = fileName;
|
|
1647
|
+
a.href = getDownloadUrl(filePath);
|
|
1648
|
+
a.download = fileName || '';
|
|
1649
|
+
a.style.display = 'none';
|
|
1483
1650
|
document.body.appendChild(a);
|
|
1484
1651
|
a.click();
|
|
1485
|
-
|
|
1652
|
+
a.remove();
|
|
1486
1653
|
}
|
|
1487
1654
|
|
|
1488
1655
|
async function deletePath(filePath, fileName) {
|
|
@@ -1600,10 +1767,12 @@
|
|
|
1600
1767
|
|
|
1601
1768
|
if (currentView === 'list') {
|
|
1602
1769
|
fileGrid.classList.add('list-view');
|
|
1770
|
+
fileGrid.classList.toggle('show-birthtime', showBirthtime);
|
|
1603
1771
|
toggleBirthtimeBtn.style.display = 'inline-block';
|
|
1604
1772
|
} else {
|
|
1605
1773
|
toggleBirthtimeBtn.style.display = 'none';
|
|
1606
1774
|
fileGrid.classList.remove('list-view');
|
|
1775
|
+
fileGrid.classList.remove('show-birthtime');
|
|
1607
1776
|
}
|
|
1608
1777
|
|
|
1609
1778
|
if (!files || files.length === 0) {
|
|
@@ -1615,19 +1784,33 @@
|
|
|
1615
1784
|
const isListView = currentView === 'list';
|
|
1616
1785
|
const columnCount = isListView
|
|
1617
1786
|
? 1
|
|
1618
|
-
: Math.max(
|
|
1619
|
-
|
|
1787
|
+
: Math.max(
|
|
1788
|
+
1,
|
|
1789
|
+
Math.floor((Math.max(fileGrid.clientWidth, GRID_MIN_ITEM_WIDTH + 16) - 8) / (GRID_MIN_ITEM_WIDTH + 8))
|
|
1790
|
+
);
|
|
1791
|
+
const rowHeight = isListView ? getListRowHeight(fileGrid) : GRID_ROW_HEIGHT;
|
|
1620
1792
|
const totalRows = Math.ceil(files.length / columnCount);
|
|
1793
|
+
const gridItemWidth = isListView ? null : (fileGrid.clientWidth - 16 - (columnCount - 1) * 8) / columnCount;
|
|
1621
1794
|
const spacer = document.createElement('div');
|
|
1622
1795
|
spacer.className = 'virtual-spacer';
|
|
1623
1796
|
spacer.style.height = `${totalRows * rowHeight + (isListView ? 0 : 8)}px`;
|
|
1624
1797
|
const content = document.createElement('div');
|
|
1625
1798
|
content.className = `virtual-content ${isListView ? 'list-view' : 'grid-view'}`;
|
|
1626
|
-
if (!isListView) {
|
|
1627
|
-
content.style.gridTemplateColumns = `repeat(${columnCount}, minmax(${GRID_MIN_ITEM_WIDTH}px, 1fr))`;
|
|
1628
|
-
}
|
|
1629
1799
|
|
|
1630
|
-
virtualState = {
|
|
1800
|
+
virtualState = {
|
|
1801
|
+
files,
|
|
1802
|
+
columnCount,
|
|
1803
|
+
rowHeight,
|
|
1804
|
+
gridItemWidth,
|
|
1805
|
+
isListView,
|
|
1806
|
+
spacer,
|
|
1807
|
+
content,
|
|
1808
|
+
itemPool: [],
|
|
1809
|
+
freeItems: [],
|
|
1810
|
+
renderedItems: new Map(),
|
|
1811
|
+
startRow: -1,
|
|
1812
|
+
endRow: -1
|
|
1813
|
+
};
|
|
1631
1814
|
fileGrid.replaceChildren(spacer, content);
|
|
1632
1815
|
if (resetScroll) fileGrid.scrollTop = 0;
|
|
1633
1816
|
renderVirtualWindow();
|
|
@@ -1641,67 +1824,195 @@
|
|
|
1641
1824
|
});
|
|
1642
1825
|
}
|
|
1643
1826
|
|
|
1827
|
+
function getListRowHeight(fileGrid) {
|
|
1828
|
+
const rowHeight = Number.parseFloat(getComputedStyle(fileGrid).getPropertyValue('--list-row-height'));
|
|
1829
|
+
return Number.isFinite(rowHeight) ? rowHeight : LIST_ROW_HEIGHT;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1644
1832
|
function renderVirtualWindow() {
|
|
1645
1833
|
if (!virtualState) return;
|
|
1646
1834
|
const fileGrid = document.getElementById('fileGrid');
|
|
1647
|
-
const {
|
|
1835
|
+
const {
|
|
1836
|
+
files,
|
|
1837
|
+
columnCount,
|
|
1838
|
+
rowHeight,
|
|
1839
|
+
gridItemWidth,
|
|
1840
|
+
isListView,
|
|
1841
|
+
content,
|
|
1842
|
+
itemPool,
|
|
1843
|
+
freeItems,
|
|
1844
|
+
renderedItems
|
|
1845
|
+
} = virtualState;
|
|
1648
1846
|
const firstVisibleRow = Math.floor(fileGrid.scrollTop / rowHeight);
|
|
1649
1847
|
const visibleRows = Math.ceil(fileGrid.clientHeight / rowHeight);
|
|
1650
1848
|
const startRow = Math.max(0, firstVisibleRow - VIRTUAL_BUFFER_ROWS);
|
|
1651
|
-
const endRow = Math.min(
|
|
1849
|
+
const endRow = Math.min(
|
|
1850
|
+
Math.ceil(files.length / columnCount),
|
|
1851
|
+
firstVisibleRow + visibleRows + VIRTUAL_BUFFER_ROWS
|
|
1852
|
+
);
|
|
1652
1853
|
const startIndex = startRow * columnCount;
|
|
1653
1854
|
const endIndex = Math.min(files.length, endRow * columnCount);
|
|
1654
1855
|
|
|
1655
1856
|
if (virtualState.startRow === startRow && virtualState.endRow === endRow) return;
|
|
1656
1857
|
virtualState.startRow = startRow;
|
|
1657
1858
|
virtualState.endRow = endRow;
|
|
1658
|
-
|
|
1859
|
+
renderedItems.forEach((fileItem, fileIndex) => {
|
|
1860
|
+
if (fileIndex < startIndex || fileIndex >= endIndex) {
|
|
1861
|
+
renderedItems.delete(fileIndex);
|
|
1862
|
+
fileItem.hidden = true;
|
|
1863
|
+
freeItems.push(fileItem);
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1867
|
+
for (let fileIndex = startIndex; fileIndex < endIndex; fileIndex++) {
|
|
1868
|
+
if (renderedItems.has(fileIndex)) continue;
|
|
1869
|
+
|
|
1870
|
+
let fileItem = freeItems.pop();
|
|
1871
|
+
if (!fileItem) {
|
|
1872
|
+
fileItem = createFileItemElement(isListView);
|
|
1873
|
+
itemPool.push(fileItem);
|
|
1874
|
+
content.appendChild(fileItem);
|
|
1875
|
+
}
|
|
1876
|
+
updateFileItem(fileItem, files[fileIndex]);
|
|
1877
|
+
positionVirtualItem(fileItem, fileIndex, columnCount, rowHeight, gridItemWidth, isListView);
|
|
1878
|
+
fileItem.hidden = false;
|
|
1879
|
+
renderedItems.set(fileIndex, fileItem);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1659
1882
|
|
|
1660
|
-
|
|
1661
|
-
|
|
1883
|
+
function positionVirtualItem(fileItem, fileIndex, columnCount, rowHeight, gridItemWidth, isListView) {
|
|
1884
|
+
fileItem.style.transform = '';
|
|
1885
|
+
if (isListView) {
|
|
1886
|
+
fileItem.style.left = '0';
|
|
1887
|
+
fileItem.style.right = '0';
|
|
1888
|
+
fileItem.style.top = `${fileIndex * rowHeight}px`;
|
|
1889
|
+
fileItem.style.width = '';
|
|
1890
|
+
return;
|
|
1662
1891
|
}
|
|
1663
1892
|
|
|
1664
|
-
|
|
1665
|
-
|
|
1893
|
+
const row = Math.floor(fileIndex / columnCount);
|
|
1894
|
+
const column = fileIndex % columnCount;
|
|
1895
|
+
fileItem.style.left = `${8 + column * (gridItemWidth + 8)}px`;
|
|
1896
|
+
fileItem.style.right = 'auto';
|
|
1897
|
+
fileItem.style.top = `${8 + row * rowHeight}px`;
|
|
1898
|
+
fileItem.style.width = `${gridItemWidth}px`;
|
|
1666
1899
|
}
|
|
1667
1900
|
|
|
1668
|
-
function
|
|
1669
|
-
const icon = getFileIcon(file);
|
|
1670
|
-
const size = formatSize(file.size);
|
|
1671
|
-
const mtime = formatDate(file.mtime);
|
|
1672
|
-
const birthtime = formatDate(file.birthtime);
|
|
1673
|
-
const hiddenClass = file.isHidden ? 'hidden-file' : '';
|
|
1674
|
-
const selectedClass = selectedPaths.has(file.path) ? 'selected' : '';
|
|
1675
|
-
const selectedAttr = selectedPaths.has(file.path) ? 'checked' : '';
|
|
1676
|
-
const selectLabel = file.isDirectory ? '选择文件夹' : '选择文件';
|
|
1677
|
-
const pathAttr = escapeAttribute(file.path);
|
|
1678
|
-
const nameAttr = escapeAttribute(file.name);
|
|
1901
|
+
function createFileItemElement(isListView) {
|
|
1679
1902
|
const fileItem = document.createElement('div');
|
|
1680
|
-
fileItem.className =
|
|
1681
|
-
|
|
1682
|
-
|
|
1903
|
+
fileItem.className = 'file-item';
|
|
1904
|
+
const select = document.createElement('input');
|
|
1905
|
+
select.className = 'file-select edit-only edit-inline-block';
|
|
1906
|
+
select.type = 'checkbox';
|
|
1907
|
+
select.dataset.action = 'toggleSelect';
|
|
1908
|
+
fileItem.appendChild(select);
|
|
1909
|
+
|
|
1910
|
+
const icon = createFileIconElement();
|
|
1911
|
+
fileItem.appendChild(icon.wrapper);
|
|
1912
|
+
|
|
1913
|
+
const info = document.createElement('div');
|
|
1914
|
+
info.className = 'file-info';
|
|
1915
|
+
const name = document.createElement('div');
|
|
1916
|
+
name.className = 'file-name';
|
|
1917
|
+
info.appendChild(name);
|
|
1918
|
+
const meta = document.createElement('div');
|
|
1919
|
+
meta.className = 'file-meta';
|
|
1920
|
+
const type = document.createElement('span');
|
|
1921
|
+
meta.appendChild(type);
|
|
1922
|
+
info.appendChild(meta);
|
|
1923
|
+
fileItem.appendChild(info);
|
|
1924
|
+
|
|
1925
|
+
const refs = { select, icon, name, type };
|
|
1683
1926
|
|
|
1684
1927
|
if (isListView) {
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
${!file.isDirectory ? `<button class="action-btn download" data-action="download" data-path="${pathAttr}" data-filename="${nameAttr}" title="下载">⬇️</button>` : ''}
|
|
1693
|
-
<button class="action-btn rename edit-only edit-inline" data-action="rename" data-path="${pathAttr}" data-filename="${nameAttr}" title="重命名">✏️</button>
|
|
1694
|
-
<button class="action-btn delete edit-only edit-inline" data-action="delete" data-path="${pathAttr}" data-filename="${nameAttr}" title="删除">🗑️</button>
|
|
1695
|
-
</div>`;
|
|
1696
|
-
} else {
|
|
1697
|
-
fileItem.innerHTML = `
|
|
1698
|
-
<input class="file-select edit-only edit-inline-block" type="checkbox" data-action="toggleSelect" data-path="${pathAttr}" title="${selectLabel}" ${selectedAttr} />
|
|
1699
|
-
<div class="file-icon">${icon}</div>
|
|
1700
|
-
<div class="file-info"><div class="file-name">${escapeHtml(file.name)}</div><div class="file-meta">${file.isDirectory ? '目录' : size}</div></div>`;
|
|
1928
|
+
const mtime = createDateMeta();
|
|
1929
|
+
const birthtime = createDateMeta('birthtime');
|
|
1930
|
+
meta.append(mtime.container, birthtime.container);
|
|
1931
|
+
const more = createActionButton('more-actions-btn', 'more', '更多操作', '⋮');
|
|
1932
|
+
more.setAttribute('aria-label', '更多操作');
|
|
1933
|
+
fileItem.appendChild(more);
|
|
1934
|
+
Object.assign(refs, { mtime, birthtime });
|
|
1701
1935
|
}
|
|
1936
|
+
|
|
1937
|
+
fileItem._refs = refs;
|
|
1702
1938
|
return fileItem;
|
|
1703
1939
|
}
|
|
1704
1940
|
|
|
1941
|
+
function createFileIconElement() {
|
|
1942
|
+
const namespace = 'http://www.w3.org/2000/svg';
|
|
1943
|
+
const wrapper = document.createElement('div');
|
|
1944
|
+
wrapper.className = 'file-icon';
|
|
1945
|
+
const svg = document.createElementNS(namespace, 'svg');
|
|
1946
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
1947
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
1948
|
+
const use = document.createElementNS(namespace, 'use');
|
|
1949
|
+
svg.appendChild(use);
|
|
1950
|
+
wrapper.appendChild(svg);
|
|
1951
|
+
return { wrapper, svg, use };
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
function createDateMeta(className = '') {
|
|
1955
|
+
const container = document.createElement('span');
|
|
1956
|
+
if (className) container.className = className;
|
|
1957
|
+
const full = document.createElement('span');
|
|
1958
|
+
full.className = 'date-full';
|
|
1959
|
+
const compact = document.createElement('span');
|
|
1960
|
+
compact.className = 'date-compact';
|
|
1961
|
+
container.append(full, compact);
|
|
1962
|
+
return { container, full, compact };
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function createActionButton(className, action, title, label) {
|
|
1966
|
+
const button = document.createElement('button');
|
|
1967
|
+
button.className = className;
|
|
1968
|
+
button.dataset.action = action;
|
|
1969
|
+
button.title = title;
|
|
1970
|
+
button.textContent = label;
|
|
1971
|
+
return button;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
function updateFileItem(fileItem, file) {
|
|
1975
|
+
const refs = fileItem._refs;
|
|
1976
|
+
const view = getFileView(file);
|
|
1977
|
+
const isSelected = selectedPaths.has(file.path);
|
|
1978
|
+
fileItem.className = `file-item${file.isHidden ? ' hidden-file' : ''}${isSelected ? ' selected' : ''}`;
|
|
1979
|
+
fileItem.dataset.path = file.path;
|
|
1980
|
+
fileItem.dataset.isDirectory = file.isDirectory;
|
|
1981
|
+
refs.select.dataset.path = file.path;
|
|
1982
|
+
refs.select.checked = isSelected;
|
|
1983
|
+
refs.select.title = file.isDirectory ? '选择文件夹' : '选择文件';
|
|
1984
|
+
updateFileIcon(refs.icon, view.iconInfo);
|
|
1985
|
+
refs.name.textContent = file.name;
|
|
1986
|
+
refs.type.textContent = view.typeLabel;
|
|
1987
|
+
|
|
1988
|
+
if (!refs.mtime) return;
|
|
1989
|
+
|
|
1990
|
+
refs.mtime.full.textContent = view.mtime;
|
|
1991
|
+
refs.mtime.compact.textContent = view.compactMtime;
|
|
1992
|
+
refs.birthtime.full.textContent = view.birthtime;
|
|
1993
|
+
refs.birthtime.compact.textContent = view.compactBirthtime;
|
|
1994
|
+
refs.birthtime.container.classList.toggle('visible', showBirthtime);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function getFileView(file) {
|
|
1998
|
+
if (!file._view) {
|
|
1999
|
+
file._view = {
|
|
2000
|
+
typeLabel: file.isDirectory ? '目录' : formatSize(file.size),
|
|
2001
|
+
mtime: formatDate(file.mtime),
|
|
2002
|
+
compactMtime: formatDate(file.mtime, false),
|
|
2003
|
+
birthtime: formatDate(file.birthtime),
|
|
2004
|
+
compactBirthtime: `创建 ${formatDate(file.birthtime, false)}`,
|
|
2005
|
+
iconInfo: getFileIconInfo(file)
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
return file._view;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function updateFileIcon(icon, iconInfo) {
|
|
2012
|
+
icon.use.setAttribute('href', iconInfo.symbol);
|
|
2013
|
+
icon.svg.style.color = iconInfo.color;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1705
2016
|
function handleItemClick(path, isDirectory) {
|
|
1706
2017
|
if (isDirectory) {
|
|
1707
2018
|
loadDirectory(path);
|
|
@@ -2026,7 +2337,7 @@
|
|
|
2026
2337
|
</div>
|
|
2027
2338
|
`;
|
|
2028
2339
|
} else if (BINARY_EXTS.includes(ext)) {
|
|
2029
|
-
const downloadUrl =
|
|
2340
|
+
const downloadUrl = getDownloadUrl(path);
|
|
2030
2341
|
const fileSize = fileInfo ? formatSize(fileInfo.size) : '未知大小';
|
|
2031
2342
|
|
|
2032
2343
|
body.innerHTML = `
|
|
@@ -2044,7 +2355,7 @@
|
|
|
2044
2355
|
`;
|
|
2045
2356
|
} else {
|
|
2046
2357
|
if (isLargeFile) {
|
|
2047
|
-
const downloadUrl =
|
|
2358
|
+
const downloadUrl = getDownloadUrl(path);
|
|
2048
2359
|
body.innerHTML = `
|
|
2049
2360
|
<div class="preview-container">
|
|
2050
2361
|
<div class="binary-info">
|
|
@@ -2178,14 +2489,16 @@
|
|
|
2178
2489
|
renderFiles(currentFiles);
|
|
2179
2490
|
}
|
|
2180
2491
|
|
|
2181
|
-
|
|
2492
|
+
const fileIconInfoCache = new Map();
|
|
2493
|
+
|
|
2494
|
+
function getFileIconInfo(file) {
|
|
2182
2495
|
if (file.isDirectory) {
|
|
2183
|
-
return
|
|
2184
|
-
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
|
2185
|
-
</svg>`;
|
|
2496
|
+
return { symbol: '#icon-folder', color: '#667eea' };
|
|
2186
2497
|
}
|
|
2187
2498
|
|
|
2188
2499
|
const ext = file.name.split('.').pop().toLowerCase();
|
|
2500
|
+
const cached = fileIconInfoCache.get(ext);
|
|
2501
|
+
if (cached) return cached;
|
|
2189
2502
|
const iconColors = {
|
|
2190
2503
|
js: '#f7df1e',
|
|
2191
2504
|
json: '#292929',
|
|
@@ -2263,12 +2576,9 @@
|
|
|
2263
2576
|
webm: '#2196f3'
|
|
2264
2577
|
};
|
|
2265
2578
|
|
|
2266
|
-
const
|
|
2267
|
-
|
|
2268
|
-
return
|
|
2269
|
-
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
|
2270
|
-
<polyline points="14 2 14 8 20 8"></polyline>
|
|
2271
|
-
</svg>`;
|
|
2579
|
+
const iconInfo = { symbol: '#icon-file', color: iconColors[ext] || '#95a5a6' };
|
|
2580
|
+
fileIconInfoCache.set(ext, iconInfo);
|
|
2581
|
+
return iconInfo;
|
|
2272
2582
|
}
|
|
2273
2583
|
|
|
2274
2584
|
function formatSize(bytes) {
|
|
@@ -2279,9 +2589,15 @@
|
|
|
2279
2589
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
2280
2590
|
}
|
|
2281
2591
|
|
|
2282
|
-
function formatDate(
|
|
2283
|
-
const date = new Date(
|
|
2284
|
-
|
|
2592
|
+
function formatDate(value, includeSeconds = true) {
|
|
2593
|
+
const date = new Date(value);
|
|
2594
|
+
const year = date.getFullYear();
|
|
2595
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
2596
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
2597
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
2598
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
2599
|
+
const base = `${year}/${month}/${day} ${hours}:${minutes}`;
|
|
2600
|
+
return includeSeconds ? `${base}:${String(date.getSeconds()).padStart(2, '0')}` : base;
|
|
2285
2601
|
}
|
|
2286
2602
|
|
|
2287
2603
|
function escapeHtml(text) {
|
|
@@ -2291,13 +2607,17 @@
|
|
|
2291
2607
|
}
|
|
2292
2608
|
|
|
2293
2609
|
function escapeAttribute(value) {
|
|
2294
|
-
return String(value).replace(
|
|
2295
|
-
'
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2610
|
+
return String(value).replace(
|
|
2611
|
+
/[&<>"']/g,
|
|
2612
|
+
char =>
|
|
2613
|
+
({
|
|
2614
|
+
'&': '&',
|
|
2615
|
+
'<': '<',
|
|
2616
|
+
'>': '>',
|
|
2617
|
+
'"': '"',
|
|
2618
|
+
"'": '''
|
|
2619
|
+
})[char]
|
|
2620
|
+
);
|
|
2301
2621
|
}
|
|
2302
2622
|
</script>
|
|
2303
2623
|
</body>
|
package/dist/runtime.js
CHANGED
|
@@ -135,7 +135,7 @@ Opening API overview page...`));let openCommand;switch(process.platform){case"da
|
|
|
135
135
|
Enable proxy at web server on:`)),Object.keys(proxyTable).forEach(prefix=>{console.info(` ${prefix} -> ${proxyTable[prefix]}`)}))})}process.platform==="win32"&&require("readline").createInterface({input:process.stdin,output:process.stdout}).on("SIGINT",function(){process.emit("SIGINT")});process.on("SIGINT",function(){log.info(colors.red("mock-server process stopped.")),process.exit()});process.on("SIGTERM",function(){log.info(colors.red("mock-server process stopped.")),process.exit()});module2.exports={composeRouteFromJsFile,parseMockFiles}}});var require_staticServer=__commonJS({"src/lib/staticServer.js"(){var express=require_express2(),colors=require_safe(),portfinder=require_portfinder(),{dateFormat,logger,getServerHost,getServerUrls}=require_utils3(),log=logger(process.env.SILENT),argv=JSON.parse(process.env.ARGV),resHeaders={};process.env.PORT?startServer():(portfinder.basePort=8090,portfinder.getPort(function(err,port){if(err)throw err;process.env.PORT=port,startServer()}));function appendResHeaders(){return(req,res,next)=>{for(let key in resHeaders)resHeaders.hasOwnProperty(key)&&res.header(key,resHeaders[key]);next()}}function startServer(){let app=express();typeof argv.A=="string"&&(argv.A.split(/\s*,\s*/).forEach(function(h){let[key,value]=h.split("=");resHeaders[key]=value},this),app.use(appendResHeaders())),app.use(express.static(process.env.STATIC_DIRECTORY)),app.listen(Number.parseInt(process.env.PORT,10),getServerHost(),()=>{console.info([colors.yellow(`
|
|
136
136
|
Starting up Static Server, serving `),colors.cyan(process.env.STATIC_DIRECTORY),colors.yellow(` ${dateFormat("YYYY-mm-dd HH:MM:SS",new Date)}`)].join("")),console.info(colors.yellow(`
|
|
137
137
|
Static Server available on:
|
|
138
|
-
`)),getServerUrls(process.env.PORT).forEach(url=>{console.info(" "+url.replace(String(process.env.PORT),colors.green(process.env.PORT)))})})}process.platform==="win32"&&require("readline").createInterface({input:process.stdin,output:process.stdout}).on("SIGINT",function(){process.emit("SIGINT")});process.on("SIGINT",function(){log.info(colors.red("static-server process stopped.")),process.exit()});process.on("SIGTERM",function(){log.info(colors.red("static-server process stopped.")),process.exit()})}});var require_fileExplorerServer=__commonJS({"src/lib/fileExplorerServer.js"(){var express=require_express2(),{existsSync,readFileSync,statSync,rmSync,mkdirSync,writeFileSync,renameSync,realpathSync,promises:fsPromises}=require("fs"),path=require("path"),colors=require_safe(),portfinder=require_portfinder(),{exec,execFile}=require("child_process"),{dateFormat,logger,getServerHost,getServerUrls}=require_utils3(),{getPackageVersion}=require_packageInfo(),app=express(),log=logger(process.env.SILENT),argv=JSON.parse(process.env.ARGV),explorerRoot=path.resolve(process.env.EXPLORER_DIRECTORY||process.cwd()),explorerRootRealPath=realpathSync(explorerRoot),port=argv.p||argv.port,isEditMode=process.env.EXPLORER_EDIT==="true";function isPathInsideRoot(fullPath,resolvedRoot=explorerRootRealPath){let relativePath=path.relative(resolvedRoot,fullPath);return relativePath===""||!relativePath.startsWith("..")&&!path.isAbsolute(relativePath)}function normalizeExplorerInputPath(inputPath){let decodedPath=decodeURIComponent(String(inputPath||"/")).replace(/\\/g,"/");return!decodedPath||decodedPath==="."?"/":decodedPath.startsWith("/")?decodedPath:`/${decodedPath}`}function resolveExplorerPath(inputPath){let relativePath=normalizeExplorerInputPath(inputPath).replace(/^\/+/,""),fullPath=path.resolve(explorerRoot,relativePath);if(!isPathInsideRoot(fullPath,explorerRoot)){let error=new Error("Access denied");throw error.statusCode=403,error}if(!existsSync(fullPath))return fullPath;let realPath=realpathSync(fullPath);if(!isPathInsideRoot(realPath)){let error=new Error("Access denied");throw error.statusCode=403,error}return realPath}function validateEntryName(name){if(typeof name!="string")return"Name must be a string";let normalizedName=name.trim();if(!normalizedName||normalizedName==="."||normalizedName==="..")return"Invalid name";if(/[/\\\0<>:"|?*]/.test(normalizedName))return"Name contains invalid characters";if(process.platform==="win32"){let upperName=normalizedName.replace(/[. ]+$/g,"").split(".")[0].toUpperCase();if(new Set(["CON","PRN","AUX","NUL","COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9","LPT1","LPT2","LPT3","LPT4","LPT5","LPT6","LPT7","LPT8","LPT9"]).has(upperName)||normalizedName.endsWith(" ")||normalizedName.endsWith("."))return"Name is not supported on Windows"}return null}function getChildPath(parentPath,name){let nameError=validateEntryName(name);if(nameError){let error=new Error(nameError);throw error.statusCode=400,error}let parentFullPath=resolveExplorerPath(parentPath||"/");if(!existsSync(parentFullPath)||!statSync(parentFullPath).isDirectory()){let error=new Error("Parent directory not found");throw error.statusCode=404,error}let childPath=path.resolve(parentFullPath,name.trim());if(!isPathInsideRoot(childPath)){let error=new Error("Access denied");throw error.statusCode=403,error}return childPath}function requireEditMode(req,res,next){if(!isEditMode)return res.status(403).json({error:"File explorer is read-only. Restart with --edit to modify files."});next()}function deleteExplorerPath(targetPath){let fullPath=resolveExplorerPath(targetPath);if(path.resolve(fullPath)===explorerRootRealPath){let error=new Error("Cannot delete explorer root");throw error.statusCode=400,error}if(!existsSync(fullPath)){let error=new Error("Path not found");throw error.statusCode=404,error}rmSync(fullPath,{recursive:!0,force:!1})}process.env.PORT?init():(portfinder.basePort=port||8090,portfinder.getPort(function(err,foundPort){if(err)throw err;process.env.PORT=foundPort,init()}));function init(){app.use(express.json()),app.get("/",(req,res)=>{let htmlPath=path.resolve(__dirname,"./file-explorer.html");existsSync(htmlPath)?res.sendFile(htmlPath):res.status(404).send("File explorer page not found")}),app.get("/__api/config",(req,res)=>{res.json({editMode:isEditMode})}),app.get("/__api/list",async(req,res)=>{let dirPath=normalizeExplorerInputPath(req.query.path||"/"),fullPath;try{fullPath=resolveExplorerPath(dirPath)}catch(error){return res.status(error.statusCode||500).json({error:error.message})}if(!existsSync(fullPath))return res.status(404).json({error:"Path not found"});try{if(!(await fsPromises.lstat(fullPath)).isDirectory())return res.status(400).json({error:"Not a directory"});let files=await fsPromises.readdir(fullPath,{withFileTypes:!0}),result=await Promise.all(files.map(async file=>{let filePath=path.join(fullPath,file.name),fileStats,hasError=!1;try{fileStats=await fsPromises.lstat(filePath)}catch{hasError=!0}let relativePath=path.posix.join(dirPath,file.name),isDirectory=hasError?file.isDirectory():fileStats.isDirectory();return{name:file.name,path:relativePath.replace(/\\/g,"/"),isDirectory,size:hasError?0:fileStats.size,mtime:hasError?new Date:fileStats.mtime,birthtime:hasError?new Date:fileStats.birthtime,isHidden:file.name.startsWith("."),error:hasError?"Cannot access file":null}}));result.sort((a,b)=>a.isDirectory!==b.isDirectory?a.isDirectory?-1:1:a.name.localeCompare(b.name)),res.json({currentPath:dirPath,parentPath:dirPath==="/"?null:path.posix.dirname(dirPath),files:result})}catch(error){res.status(500).json({error:error.message})}}),app.get("/__api/file",(req,res)=>{let filePath=normalizeExplorerInputPath(req.query.path||"/"),fullPath;try{fullPath=resolveExplorerPath(filePath)}catch(error){return res.status(error.statusCode||500).json({error:error.message})}if(!existsSync(fullPath))return res.status(404).json({error:"File not found"});let stats=statSync(fullPath);if(stats.isDirectory())return res.status(400).json({error:"Is a directory"});let ext=path.extname(filePath).toLowerCase(),imageExts=[".jpg",".jpeg",".png",".gif",".bmp",".webp",".svg",".ico"],textExts=[".txt",".json",".js",".css",".html",".xml",".md",".csv",".yaml",".yml",".log"];if(imageExts.includes(ext))res.sendFile(fullPath);else if(textExts.includes(ext)||stats.size<1024*1024)try{let content=readFileSync(fullPath,"utf-8");res.json({name:path.basename(filePath),type:"text",content,size:stats.size})}catch{res.download(fullPath)}else res.download(fullPath)}),app.post("/__api/open-in-explorer",(req,res)=>{let filePath=normalizeExplorerInputPath(req.body.path||"/"),fullPath;try{fullPath=resolveExplorerPath(filePath)}catch(error){return res.status(error.statusCode||500).json({error:error.message})}if(!existsSync(fullPath))return res.status(404).json({error:"Path not found"});let command,args;switch(process.platform){case"darwin":command="open",args=[fullPath];break;case"win32":command="explorer.exe",args=[fullPath];break;case"linux":command="xdg-open",args=[fullPath];break;default:return res.status(400).json({error:"Unsupported platform"})}execFile(command,args,error=>{if(error)return console.error(colors.red(`Failed to open in explorer: ${error.message}`)),res.status(500).json({error:"Failed to open in explorer"});res.json({success:!0,path:fullPath})})}),app.post("/__api/path",requireEditMode,(req,res)=>{let parentPath=req.body&&req.body.parentPath||"/",name=req.body&&req.body.name,type=req.body&&req.body.type||"file";if(type!=="file"&&type!=="directory")return res.status(400).json({error:"Invalid type"});let fullPath;try{fullPath=getChildPath(parentPath,name)}catch(error){return res.status(error.statusCode||500).json({error:error.message})}if(existsSync(fullPath))return res.status(409).json({error:"Path already exists"});try{type==="directory"?mkdirSync(fullPath):writeFileSync(fullPath,""),res.json({success:!0,path:fullPath})}catch(error){console.error(colors.red(`Failed to create path: ${error.message}`)),res.status(500).json({error:"Failed to create path"})}}),app.patch("/__api/path",requireEditMode,(req,res)=>{let sourcePath=req.body&&req.body.path,name=req.body&&req.body.name,fullPath,nextPath;try{if(fullPath=resolveExplorerPath(sourcePath),path.resolve(fullPath)===explorerRootRealPath)return res.status(400).json({error:"Cannot rename explorer root"});if(!existsSync(fullPath))return res.status(404).json({error:"Path not found"});nextPath=getChildPath(path.posix.dirname(normalizeExplorerInputPath(sourcePath||"/")),name)}catch(error){return res.status(error.statusCode||500).json({error:error.message})}if(existsSync(nextPath))return res.status(409).json({error:"Path already exists"});try{renameSync(fullPath,nextPath),res.json({success:!0,path:sourcePath,nextPath})}catch(error){console.error(colors.red(`Failed to rename path: ${error.message}`)),res.status(500).json({error:"Failed to rename path"})}}),app.delete("/__api/path",requireEditMode,(req,res)=>{let targetPath=req.body&&req.body.path||"/";try{deleteExplorerPath(targetPath),res.json({success:!0,path:targetPath})}catch(error){console.error(colors.red(`Failed to delete path: ${error.message}`)),res.status(error.statusCode||500).json({error:error.statusCode?error.message:"Failed to delete path"})}}),app.delete("/__api/paths",requireEditMode,(req,res)=>{let paths=req.body&&req.body.paths||[];if(!Array.isArray(paths)||paths.length===0)return res.status(400).json({error:"Paths must be a non-empty array"});let deleted=[];try{paths.forEach(targetPath=>{deleteExplorerPath(targetPath),deleted.push(targetPath)}),res.json({success:!0,deleted})}catch(error){console.error(colors.red(`Failed to delete paths: ${error.message}`)),res.status(error.statusCode||500).json({error:error.statusCode?error.message:"Failed to delete paths",deleted})}}),startServer()}function startServer(){require("http").createServer(app).listen(Number.parseInt(process.env.PORT,10),getServerHost(),()=>{if(console.info([colors.yellow(`
|
|
138
|
+
`)),getServerUrls(process.env.PORT).forEach(url=>{console.info(" "+url.replace(String(process.env.PORT),colors.green(process.env.PORT)))})})}process.platform==="win32"&&require("readline").createInterface({input:process.stdin,output:process.stdout}).on("SIGINT",function(){process.emit("SIGINT")});process.on("SIGINT",function(){log.info(colors.red("static-server process stopped.")),process.exit()});process.on("SIGTERM",function(){log.info(colors.red("static-server process stopped.")),process.exit()})}});var require_fileExplorerServer=__commonJS({"src/lib/fileExplorerServer.js"(){var express=require_express2(),{existsSync,readFileSync,statSync,rmSync,mkdirSync,writeFileSync,renameSync,realpathSync,promises:fsPromises}=require("fs"),path=require("path"),colors=require_safe(),portfinder=require_portfinder(),{exec,execFile}=require("child_process"),{dateFormat,logger,getServerHost,getServerUrls}=require_utils3(),{getPackageVersion}=require_packageInfo(),app=express(),log=logger(process.env.SILENT),argv=JSON.parse(process.env.ARGV),explorerRoot=path.resolve(process.env.EXPLORER_DIRECTORY||process.cwd()),explorerRootRealPath=realpathSync(explorerRoot),port=argv.p||argv.port,isEditMode=process.env.EXPLORER_EDIT==="true";function isPathInsideRoot(fullPath,resolvedRoot=explorerRootRealPath){let relativePath=path.relative(resolvedRoot,fullPath);return relativePath===""||!relativePath.startsWith("..")&&!path.isAbsolute(relativePath)}function normalizeExplorerInputPath(inputPath){let decodedPath=decodeURIComponent(String(inputPath||"/")).replace(/\\/g,"/");return!decodedPath||decodedPath==="."?"/":decodedPath.startsWith("/")?decodedPath:`/${decodedPath}`}function resolveExplorerPath(inputPath){let relativePath=normalizeExplorerInputPath(inputPath).replace(/^\/+/,""),fullPath=path.resolve(explorerRoot,relativePath);if(!isPathInsideRoot(fullPath,explorerRoot)){let error=new Error("Access denied");throw error.statusCode=403,error}if(!existsSync(fullPath))return fullPath;let realPath=realpathSync(fullPath);if(!isPathInsideRoot(realPath)){let error=new Error("Access denied");throw error.statusCode=403,error}return realPath}function validateEntryName(name){if(typeof name!="string")return"Name must be a string";let normalizedName=name.trim();if(!normalizedName||normalizedName==="."||normalizedName==="..")return"Invalid name";if(/[/\\\0<>:"|?*]/.test(normalizedName))return"Name contains invalid characters";if(process.platform==="win32"){let upperName=normalizedName.replace(/[. ]+$/g,"").split(".")[0].toUpperCase();if(new Set(["CON","PRN","AUX","NUL","COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9","LPT1","LPT2","LPT3","LPT4","LPT5","LPT6","LPT7","LPT8","LPT9"]).has(upperName)||normalizedName.endsWith(" ")||normalizedName.endsWith("."))return"Name is not supported on Windows"}return null}function getChildPath(parentPath,name){let nameError=validateEntryName(name);if(nameError){let error=new Error(nameError);throw error.statusCode=400,error}let parentFullPath=resolveExplorerPath(parentPath||"/");if(!existsSync(parentFullPath)||!statSync(parentFullPath).isDirectory()){let error=new Error("Parent directory not found");throw error.statusCode=404,error}let childPath=path.resolve(parentFullPath,name.trim());if(!isPathInsideRoot(childPath)){let error=new Error("Access denied");throw error.statusCode=403,error}return childPath}function requireEditMode(req,res,next){if(!isEditMode)return res.status(403).json({error:"File explorer is read-only. Restart with --edit to modify files."});next()}function deleteExplorerPath(targetPath){let fullPath=resolveExplorerPath(targetPath);if(path.resolve(fullPath)===explorerRootRealPath){let error=new Error("Cannot delete explorer root");throw error.statusCode=400,error}if(!existsSync(fullPath)){let error=new Error("Path not found");throw error.statusCode=404,error}rmSync(fullPath,{recursive:!0,force:!1})}process.env.PORT?init():(portfinder.basePort=port||8090,portfinder.getPort(function(err,foundPort){if(err)throw err;process.env.PORT=foundPort,init()}));function init(){app.use(express.json()),app.get("/",(req,res)=>{let htmlPath=path.resolve(__dirname,"./file-explorer.html");existsSync(htmlPath)?res.sendFile(htmlPath):res.status(404).send("File explorer page not found")}),app.get("/__api/config",(req,res)=>{res.json({editMode:isEditMode})}),app.get("/__api/list",async(req,res)=>{let dirPath=normalizeExplorerInputPath(req.query.path||"/"),fullPath;try{fullPath=resolveExplorerPath(dirPath)}catch(error){return res.status(error.statusCode||500).json({error:error.message})}if(!existsSync(fullPath))return res.status(404).json({error:"Path not found"});try{if(!(await fsPromises.lstat(fullPath)).isDirectory())return res.status(400).json({error:"Not a directory"});let files=await fsPromises.readdir(fullPath,{withFileTypes:!0}),result=await Promise.all(files.map(async file=>{let filePath=path.join(fullPath,file.name),fileStats,hasError=!1;try{fileStats=await fsPromises.lstat(filePath)}catch{hasError=!0}let relativePath=path.posix.join(dirPath,file.name),isDirectory=hasError?file.isDirectory():fileStats.isDirectory();return{name:file.name,path:relativePath.replace(/\\/g,"/"),isDirectory,size:hasError?0:fileStats.size,mtime:hasError?new Date:fileStats.mtime,birthtime:hasError?new Date:fileStats.birthtime,isHidden:file.name.startsWith("."),error:hasError?"Cannot access file":null}}));result.sort((a,b)=>a.isDirectory!==b.isDirectory?a.isDirectory?-1:1:a.name.localeCompare(b.name)),res.json({currentPath:dirPath,parentPath:dirPath==="/"?null:path.posix.dirname(dirPath),files:result})}catch(error){res.status(500).json({error:error.message})}}),app.get("/__api/file",(req,res)=>{let filePath=normalizeExplorerInputPath(req.query.path||"/"),fullPath;try{fullPath=resolveExplorerPath(filePath)}catch(error){return res.status(error.statusCode||500).json({error:error.message})}if(!existsSync(fullPath))return res.status(404).json({error:"File not found"});let stats=statSync(fullPath);if(stats.isDirectory())return res.status(400).json({error:"Is a directory"});if(req.query.download==="1")return res.download(fullPath,path.basename(filePath));let ext=path.extname(filePath).toLowerCase(),imageExts=[".jpg",".jpeg",".png",".gif",".bmp",".webp",".svg",".ico"],textExts=[".txt",".json",".js",".css",".html",".xml",".md",".csv",".yaml",".yml",".log"];if(imageExts.includes(ext))res.sendFile(fullPath);else if(textExts.includes(ext)||stats.size<1024*1024)try{let content=readFileSync(fullPath,"utf-8");res.json({name:path.basename(filePath),type:"text",content,size:stats.size})}catch{res.download(fullPath)}else res.download(fullPath)}),app.post("/__api/open-in-explorer",(req,res)=>{let filePath=normalizeExplorerInputPath(req.body.path||"/"),fullPath;try{fullPath=resolveExplorerPath(filePath)}catch(error){return res.status(error.statusCode||500).json({error:error.message})}if(!existsSync(fullPath))return res.status(404).json({error:"Path not found"});let command,args;switch(process.platform){case"darwin":command="open",args=[fullPath];break;case"win32":command="explorer.exe",args=[fullPath];break;case"linux":command="xdg-open",args=[fullPath];break;default:return res.status(400).json({error:"Unsupported platform"})}execFile(command,args,error=>{if(error)return console.error(colors.red(`Failed to open in explorer: ${error.message}`)),res.status(500).json({error:"Failed to open in explorer"});res.json({success:!0,path:fullPath})})}),app.post("/__api/path",requireEditMode,(req,res)=>{let parentPath=req.body&&req.body.parentPath||"/",name=req.body&&req.body.name,type=req.body&&req.body.type||"file";if(type!=="file"&&type!=="directory")return res.status(400).json({error:"Invalid type"});let fullPath;try{fullPath=getChildPath(parentPath,name)}catch(error){return res.status(error.statusCode||500).json({error:error.message})}if(existsSync(fullPath))return res.status(409).json({error:"Path already exists"});try{type==="directory"?mkdirSync(fullPath):writeFileSync(fullPath,""),res.json({success:!0,path:fullPath})}catch(error){console.error(colors.red(`Failed to create path: ${error.message}`)),res.status(500).json({error:"Failed to create path"})}}),app.patch("/__api/path",requireEditMode,(req,res)=>{let sourcePath=req.body&&req.body.path,name=req.body&&req.body.name,fullPath,nextPath;try{if(fullPath=resolveExplorerPath(sourcePath),path.resolve(fullPath)===explorerRootRealPath)return res.status(400).json({error:"Cannot rename explorer root"});if(!existsSync(fullPath))return res.status(404).json({error:"Path not found"});nextPath=getChildPath(path.posix.dirname(normalizeExplorerInputPath(sourcePath||"/")),name)}catch(error){return res.status(error.statusCode||500).json({error:error.message})}if(existsSync(nextPath))return res.status(409).json({error:"Path already exists"});try{renameSync(fullPath,nextPath),res.json({success:!0,path:sourcePath,nextPath})}catch(error){console.error(colors.red(`Failed to rename path: ${error.message}`)),res.status(500).json({error:"Failed to rename path"})}}),app.delete("/__api/path",requireEditMode,(req,res)=>{let targetPath=req.body&&req.body.path||"/";try{deleteExplorerPath(targetPath),res.json({success:!0,path:targetPath})}catch(error){console.error(colors.red(`Failed to delete path: ${error.message}`)),res.status(error.statusCode||500).json({error:error.statusCode?error.message:"Failed to delete path"})}}),app.delete("/__api/paths",requireEditMode,(req,res)=>{let paths=req.body&&req.body.paths||[];if(!Array.isArray(paths)||paths.length===0)return res.status(400).json({error:"Paths must be a non-empty array"});let deleted=[];try{paths.forEach(targetPath=>{deleteExplorerPath(targetPath),deleted.push(targetPath)}),res.json({success:!0,deleted})}catch(error){console.error(colors.red(`Failed to delete paths: ${error.message}`)),res.status(error.statusCode||500).json({error:error.statusCode?error.message:"Failed to delete paths",deleted})}}),startServer()}function startServer(){require("http").createServer(app).listen(Number.parseInt(process.env.PORT,10),getServerHost(),()=>{if(console.info([colors.yellow(`
|
|
139
139
|
Starting up file-explorer-server, serving `),colors.cyan(explorerRoot),colors.yellow(` ${dateFormat("YYYY-mm-dd HH:MM:SS",new Date)}`)].join("")),console.info([colors.yellow(`
|
|
140
140
|
\u{1F30D} file-explorer-server version: `),colors.cyan(getPackageVersion()),`
|
|
141
141
|
`].join("")),console.info(colors.yellow(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mock-service-cli",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.2",
|
|
4
4
|
"description": "🦅 Local Mock/Static/SPA server, Http?s request proxy, API overview page, File explorer",
|
|
5
5
|
"main": "./dist/mockServer.js",
|
|
6
6
|
"bin": {
|
|
@@ -81,7 +81,10 @@
|
|
|
81
81
|
"tap": "^15.0.10"
|
|
82
82
|
},
|
|
83
83
|
"tap": {
|
|
84
|
-
"check-coverage": false
|
|
84
|
+
"check-coverage": false,
|
|
85
|
+
"nyc-arg": [
|
|
86
|
+
"--include=src/**/*.js"
|
|
87
|
+
]
|
|
85
88
|
},
|
|
86
89
|
"husky": {
|
|
87
90
|
"hooks": {
|