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 CHANGED
@@ -1,7 +1,6 @@
1
1
  [![release status](https://github.com/chandq/mock-service-cli/actions/workflows/release.yml/badge.svg)](https://github.com/chandq/mock-service-cli/actions/workflows/release.yml)
2
2
  [![Coverage Status](https://coveralls.io/repos/github/chandq/mock-service-cli/badge.svg?branch=master)](https://coveralls.io/github/chandq/mock-service-cli?branch=master)
3
3
  [![mock-service-cli](https://img.shields.io/github/package-json/v/chandq/mock-service-cli?style=flat-square)](https://www.npmjs.com/package/mock-service-cli)
4
- [![node](https://img.shields.io/badge/language-node-orange.svg)](https://nodejs.org/download/release/v12.0.0/)
5
4
  [![license:MIT](https://img.shields.io/npm/l/vue.svg?sanitize=true)](https://github.com/chandq/mock-service-cli/blob/master/LICENSE.md)
6
5
 
7
6
  # 🦅 mock-service-cli
@@ -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: grid;
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: all 0.2s;
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: 37px;
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: flex;
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 = true;
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 && currentView === 'grid') {
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 = url;
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
- document.body.removeChild(a);
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(1, Math.floor((Math.max(fileGrid.clientWidth, GRID_MIN_ITEM_WIDTH + 16) - 8) / (GRID_MIN_ITEM_WIDTH + 8)));
1619
- const rowHeight = isListView ? LIST_ROW_HEIGHT : GRID_ROW_HEIGHT;
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 = { files, columnCount, rowHeight, isListView, spacer, content, startRow: -1, endRow: -1 };
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 { files, columnCount, rowHeight, isListView, content } = virtualState;
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(Math.ceil(files.length / columnCount), firstVisibleRow + visibleRows + VIRTUAL_BUFFER_ROWS);
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
- const fragment = document.createDocumentFragment();
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
- for (let index = startIndex; index < endIndex; index++) {
1661
- fragment.appendChild(createFileItem(files[index], isListView));
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
- content.style.transform = `translateY(${startRow * rowHeight}px)`;
1665
- content.replaceChildren(fragment);
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 createFileItem(file, isListView) {
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 = `file-item ${hiddenClass} ${selectedClass}`;
1681
- fileItem.dataset.path = file.path;
1682
- fileItem.dataset.isDirectory = file.isDirectory;
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
- fileItem.innerHTML = `
1686
- <input class="file-select edit-only edit-inline-block" type="checkbox" data-action="toggleSelect" data-path="${pathAttr}" title="${selectLabel}" ${selectedAttr} />
1687
- <div class="file-icon">${icon}</div>
1688
- <div class="file-info"><div class="file-name">${escapeHtml(file.name)}</div><div class="file-meta">${file.isDirectory ? '<span>目录</span>' : `<span>${size}</span>`}<span>${mtime}</span><span class="birthtime" style="display: ${showBirthtime ? 'inline-block' : 'none'};">${birthtime}</span></div></div>
1689
- <div class="action-buttons">
1690
- <button class="copy-btn" data-action="copyPath" data-path="${pathAttr}" title="复制路径">📋</button>
1691
- <button class="action-btn open" data-action="openInExplorer" data-path="${pathAttr}" title="在文件管理器中打开">📂</button>
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 = `/__api/file?path=${encodeURIComponent(path)}`;
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 = `/__api/file?path=${encodeURIComponent(path)}`;
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
- function getFileIcon(file) {
2492
+ const fileIconInfoCache = new Map();
2493
+
2494
+ function getFileIconInfo(file) {
2182
2495
  if (file.isDirectory) {
2183
- return `<svg viewBox="0 0 24 24" fill="none" stroke="#667eea" stroke-width="2">
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 color = iconColors[ext] || '#95a5a6';
2267
-
2268
- return `<svg viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2">
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(dateStr) {
2283
- const date = new Date(dateStr);
2284
- return date.toLocaleString('zh-CN');
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(/[&<>"']/g, char => ({
2295
- '&': '&amp;',
2296
- '<': '&lt;',
2297
- '>': '&gt;',
2298
- '"': '&quot;',
2299
- "'": '&#39;'
2300
- })[char]);
2610
+ return String(value).replace(
2611
+ /[&<>"']/g,
2612
+ char =>
2613
+ ({
2614
+ '&': '&amp;',
2615
+ '<': '&lt;',
2616
+ '>': '&gt;',
2617
+ '"': '&quot;',
2618
+ "'": '&#39;'
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.0",
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": {