mock-service-cli-ultra 4.5.0 → 4.5.1
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/dist/file-explorer-login.html +1 -1
- package/dist/file-explorer.html +33 -7
- package/dist/meta.json +3 -3
- package/dist/runtime.js +2 -2
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>文件浏览器登录</title>
|
|
7
|
-
<link rel="icon" type="image/svg+xml" href="
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="__FILE_EXPLORER_FAVICON_URL__" />
|
|
8
8
|
<style>
|
|
9
9
|
* { box-sizing: border-box; }
|
|
10
10
|
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f4f6f8; color: #202124; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
package/dist/file-explorer.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>文件浏览器 - File Explorer</title>
|
|
7
|
-
<link rel="icon" type="image/svg+xml" href="
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="__FILE_EXPLORER_FAVICON_URL__" />
|
|
8
8
|
<style>
|
|
9
9
|
* {
|
|
10
10
|
margin: 0;
|
|
@@ -1158,6 +1158,7 @@
|
|
|
1158
1158
|
let explorerLifecycleVersion = 0;
|
|
1159
1159
|
let currentArchiveJobId = null;
|
|
1160
1160
|
let currentArchiveJobOperation = null;
|
|
1161
|
+
let explorerRootId = null;
|
|
1161
1162
|
let archiveCapabilities = { createFormats: [], readExtensions: [] };
|
|
1162
1163
|
|
|
1163
1164
|
const EXPLORER_AUTH_STORAGE_KEY = 'mock-service-cli.file-explorer.password';
|
|
@@ -1281,6 +1282,7 @@
|
|
|
1281
1282
|
const config = await response.json();
|
|
1282
1283
|
if (!response.ok || config.error) throw new Error(config.error || 'Failed to load configuration');
|
|
1283
1284
|
editMode = Boolean(config.editMode);
|
|
1285
|
+
explorerRootId = typeof config.rootId === 'string' ? config.rootId : null;
|
|
1284
1286
|
} catch (error) {
|
|
1285
1287
|
console.warn('Failed to load file explorer configuration:', error);
|
|
1286
1288
|
editMode = false;
|
|
@@ -1413,7 +1415,25 @@
|
|
|
1413
1415
|
}
|
|
1414
1416
|
}
|
|
1415
1417
|
|
|
1416
|
-
|
|
1418
|
+
function getStoredExplorerPath() {
|
|
1419
|
+
try {
|
|
1420
|
+
const stored = JSON.parse(sessionStorage.getItem(EXPLORER_PATH_STORAGE_KEY));
|
|
1421
|
+
if (stored && stored.rootId === explorerRootId && typeof stored.path === 'string' && stored.path.startsWith('/')) {
|
|
1422
|
+
return stored.path;
|
|
1423
|
+
}
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
// Earlier releases stored the path directly; discard that unscoped value.
|
|
1426
|
+
}
|
|
1427
|
+
sessionStorage.removeItem(EXPLORER_PATH_STORAGE_KEY);
|
|
1428
|
+
return '/';
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function storeExplorerPath(path) {
|
|
1432
|
+
if (!explorerRootId) return;
|
|
1433
|
+
sessionStorage.setItem(EXPLORER_PATH_STORAGE_KEY, JSON.stringify({ rootId: explorerRootId, path }));
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
async function initializeExplorer() {
|
|
1417
1437
|
if (explorerInitializationPromise) return explorerInitializationPromise;
|
|
1418
1438
|
|
|
1419
1439
|
const lifecycleVersion = explorerLifecycleVersion;
|
|
@@ -1443,7 +1463,7 @@
|
|
|
1443
1463
|
await loadArchiveCapabilities();
|
|
1444
1464
|
if (lifecycleVersion !== explorerLifecycleVersion) return;
|
|
1445
1465
|
startHealthCheck();
|
|
1446
|
-
await loadDirectory(
|
|
1466
|
+
await loadDirectory(getStoredExplorerPath(), { fallbackToRoot: true });
|
|
1447
1467
|
})();
|
|
1448
1468
|
explorerInitializationPromise = initializationPromise;
|
|
1449
1469
|
|
|
@@ -1460,7 +1480,7 @@
|
|
|
1460
1480
|
|
|
1461
1481
|
window.addEventListener('pagehide', handlePageHide);
|
|
1462
1482
|
window.addEventListener('pageshow', event => {
|
|
1463
|
-
if (event.persisted) initializeExplorer(
|
|
1483
|
+
if (event.persisted) initializeExplorer();
|
|
1464
1484
|
});
|
|
1465
1485
|
|
|
1466
1486
|
function showContextMenu(x, y, filePath, isDirectory) {
|
|
@@ -1538,7 +1558,7 @@
|
|
|
1538
1558
|
hideContextMenu();
|
|
1539
1559
|
}
|
|
1540
1560
|
|
|
1541
|
-
async function loadDirectory(path) {
|
|
1561
|
+
async function loadDirectory(path, { fallbackToRoot = false } = {}) {
|
|
1542
1562
|
const fileGrid = document.getElementById('fileGrid');
|
|
1543
1563
|
const oldCurrentPath = currentPath;
|
|
1544
1564
|
const requestId = ++directoryRequestId;
|
|
@@ -1562,12 +1582,14 @@
|
|
|
1562
1582
|
const data = await response.json();
|
|
1563
1583
|
|
|
1564
1584
|
if (!response.ok || data.error) {
|
|
1565
|
-
|
|
1585
|
+
const error = new Error(data.error || 'Failed to load directory');
|
|
1586
|
+
error.status = response.status;
|
|
1587
|
+
throw error;
|
|
1566
1588
|
}
|
|
1567
1589
|
if (requestId !== directoryRequestId) return;
|
|
1568
1590
|
|
|
1569
1591
|
currentPath = data.currentPath;
|
|
1570
|
-
|
|
1592
|
+
storeExplorerPath(currentPath);
|
|
1571
1593
|
allFiles = data.files || [];
|
|
1572
1594
|
|
|
1573
1595
|
if (document.getElementById('searchInput')) {
|
|
@@ -1580,6 +1602,10 @@
|
|
|
1580
1602
|
updateBackButton(data.parentPath);
|
|
1581
1603
|
} catch (error) {
|
|
1582
1604
|
if (error.name === 'AbortError' || requestId !== directoryRequestId) return;
|
|
1605
|
+
if (fallbackToRoot && path !== '/' && error.status === 404) {
|
|
1606
|
+
sessionStorage.removeItem(EXPLORER_PATH_STORAGE_KEY);
|
|
1607
|
+
return loadDirectory('/', { fallbackToRoot: false });
|
|
1608
|
+
}
|
|
1583
1609
|
console.error('Error loading directory:', error);
|
|
1584
1610
|
currentPath = oldCurrentPath;
|
|
1585
1611
|
const parentPath = currentPath === '/' ? null : currentPath.substring(0, currentPath.lastIndexOf('/')) || '/';
|
package/dist/meta.json
CHANGED
|
@@ -7489,7 +7489,7 @@
|
|
|
7489
7489
|
"format": "cjs"
|
|
7490
7490
|
},
|
|
7491
7491
|
"src/lib/fileExplorerServer.js": {
|
|
7492
|
-
"bytes":
|
|
7492
|
+
"bytes": 25537,
|
|
7493
7493
|
"imports": [
|
|
7494
7494
|
{
|
|
7495
7495
|
"path": "node_modules/express/index.js",
|
|
@@ -9989,13 +9989,13 @@
|
|
|
9989
9989
|
"bytesInOutput": 7774
|
|
9990
9990
|
},
|
|
9991
9991
|
"src/lib/fileExplorerServer.js": {
|
|
9992
|
-
"bytesInOutput":
|
|
9992
|
+
"bytesInOutput": 19066
|
|
9993
9993
|
},
|
|
9994
9994
|
"src/runtime.js": {
|
|
9995
9995
|
"bytesInOutput": 507
|
|
9996
9996
|
}
|
|
9997
9997
|
},
|
|
9998
|
-
"bytes":
|
|
9998
|
+
"bytes": 1619956
|
|
9999
9999
|
}
|
|
10000
10000
|
}
|
|
10001
10001
|
}
|
package/dist/runtime.js
CHANGED
|
@@ -146,8 +146,8 @@ Starting up Static Server, serving `),colors.cyan(instance.root),colors.yellow(`
|
|
|
146
146
|
[...extracted.files];
|
|
147
147
|
parentPort.postMessage({ success: true });
|
|
148
148
|
})().catch(error => parentPort.postMessage({ error: error.message }));
|
|
149
|
-
`;function isArchivePath(filePath){return hasArchiveExtension(filePath,extensions)}function isCompressedTar(filePath){return/\.(tar\.gz|tgz|tar\.bz2|tbz|tbz2|tar\.xz|txz)$/i.test(filePath)}function parse7ZipList(output){let separator=output.indexOf("----------");return separator<0?[]:output.slice(separator+10).split(/\r?\n\r?\n/).map(block=>{let values={};return block.split(/\r?\n/).forEach(line=>{let index=line.indexOf(" = ");index>0&&(values[line.slice(0,index)]=line.slice(index+3))}),values}).filter(values=>values.Path).map(values=>({path:values.Path.replace(/\\/g,"/"),isDirectory:values.Folder==="+"||String(values.Attributes||"").startsWith("D"),size:Number(values.Size||0),packedSize:Number(values["Packed Size"]||0),encrypted:values.Encrypted==="+",modified:values.Modified||null,attributes:values.Attributes||""}))}function run7Zip(args,onProcess){return new Promise((resolve,reject)=>{try{process.platform!=="win32"&&chmodSync(path7za,493)}catch(error){return reject(createArchiveError(`Unable to prepare bundled 7-Zip binary: ${error.message}`,500))}let child=spawn(path7za,args,{stdio:["ignore","pipe","pipe"]}),stderr="",stdout="";onProcess&&onProcess(child),child.stdout.on("data",chunk=>{stdout+=chunk.toString()}),child.stderr.on("data",chunk=>{stderr+=chunk.toString()}),child.once("error",reject),child.once("close",code=>{if(code===0)return resolve(stdout);reject(createArchiveError(stderr.trim()||`7-Zip exited with code ${code}`,422))})})}async function list7Zip(fullPath){let output=await run7Zip(["l","-slt","--",fullPath]),entries=parse7ZipList(output);if(!isCompressedTar(fullPath))return entries;let tempRoot=await fsPromises.mkdtemp(path.join(os.tmpdir(),"mock-service-cli-archive-list-"));try{await run7Zip(["e","-y",`-o${tempRoot}`,"--",fullPath]);let tarName=(await fsPromises.readdir(tempRoot)).find(name=>/\.tar$/i.test(name));return tarName?await list7Zip(path.join(tempRoot,tarName)):entries}finally{rmSync(tempRoot,{recursive:!0,force:!0})}}async function listRar(fullPath){let list=(await createExtractorFromFile({filepath:fullPath})).getFileList();if(list.arcHeader.flags&&list.arcHeader.flags.headerEncrypted)throw createArchiveError("Encrypted archives are not supported",422);return[...list.fileHeaders].map(header=>({path:header.name.replace(/\\/g,"/"),isDirectory:!!(header.flags&&header.flags.directory),size:Number(header.unpSize||0),packedSize:Number(header.packSize||0),encrypted:!!(header.flags&&header.flags.encrypted),modified:header.time||null,attributes:""}))}async function listEntries(fullPath){return path.extname(fullPath).toLowerCase()===".rar"?listRar(fullPath):list7Zip(fullPath)}async function extractRar(job,sourcePath,targetPath){await new Promise((resolve,reject)=>{let worker=new Worker(RAR_EXTRACT_WORKER,{eval:!0,workerData:{sourcePath,targetPath}}),settled=!1,finish=(callback,value)=>{settled||(settled=!0,job.child=null,callback(value))};job.child={kill:()=>worker.terminate()},worker.on("message",message=>{message&&message.success?finish(resolve):finish(reject,createArchiveError(message&&message.error||"RAR extraction failed",422))}),worker.on("error",error=>finish(reject,error)),worker.on("exit",code=>{if(!settled&&code!==0){let message=job.cancelled?"Archive job cancelled":"RAR extraction stopped";finish(reject,createArchiveError(message,499))}})})}async function extract(job,sourcePath,targetPath){if(path.extname(sourcePath).toLowerCase()===".rar")return extractRar(job,sourcePath,targetPath);await run7Zip(["x","-y",`-o${targetPath}`,"--",sourcePath],child=>{job.child=child})}module2.exports={capabilities,extract,isArchivePath,listEntries}}});var require_archiveService=__commonJS({"src/lib/archiveService.js"(exports2,module2){var{createWriteStream,existsSync,lstatSync,mkdirSync,promises:fsPromises,realpathSync,renameSync,rmSync}=require("fs"),path=require("path"),crypto=require("crypto"),archiver=require("archiver"),archiveProvider=require_ultra(),{createArchiveError,isInside,isSafeArchiveEntry}=require_archiveShared(),MAX_ARCHIVE_SIZE=2*1024*1024*1024,MAX_ARCHIVE_ENTRIES=1e4,MAX_COMPRESSION_RATIO=100,ArchiveService=class{constructor({rootPath,resolvePath,getChildPath}){this.rootPath=rootPath,this.resolvePath=resolvePath,this.getChildPath=getChildPath,this.jobs=new Map}resolveExistingFile(inputPath){let fullPath=this.resolvePath(inputPath);if(!existsSync(fullPath))throw createArchiveError("Path not found",404);let stats=lstatSync(fullPath);if(!stats.isFile())throw createArchiveError("Archive path must be a file");if(!archiveProvider.isArchivePath(fullPath))throw createArchiveError("Unsupported archive format");if(stats.size>MAX_ARCHIVE_SIZE)throw createArchiveError("Archive exceeds 2GB limit",413);return fullPath}async listArchive(inputPath){let fullPath=this.resolveExistingFile(inputPath),entries=await archiveProvider.listEntries(fullPath);return this.assertEntriesSafe(entries,fullPath),{path:inputPath,name:path.basename(fullPath),entries,totalEntries:entries.length,totalSize:entries.reduce((total,entry)=>total+entry.size,0),totalPackedSize:entries.reduce((total,entry)=>total+entry.packedSize,0)}}assertEntriesSafe(entries,fullPath){if(!entries.length)throw createArchiveError("Archive contains no entries",422);if(entries.length>MAX_ARCHIVE_ENTRIES)throw createArchiveError("Archive contains too many entries",413);if(entries.some(entry=>entry.encrypted))throw createArchiveError("Encrypted archives are not supported",422);if(entries.some(entry=>entry.isSymbolicLink||!isSafeArchiveEntry(entry.path)))throw createArchiveError("Archive contains an unsafe entry path",422);let totalSize=entries.reduce((total,entry)=>total+entry.size,0),packedSize=Math.max(lstatSync(fullPath).size,1);if(totalSize>MAX_ARCHIVE_SIZE)throw createArchiveError("Archive expands beyond 2GB limit",413);if(totalSize/packedSize>MAX_COMPRESSION_RATIO)throw createArchiveError("Archive compression ratio exceeds limit",413)}startJob(type,payload){let id=crypto.randomUUID(),job={id,type,status:"queued",progress:{processedEntries:0,totalEntries:0},createdAt:new Date().toISOString(),result:null,error:null,cancelled:!1,child:null,archive:null};return this.jobs.set(id,job),setImmediate(async()=>{if(!job.cancelled){job.status="running";try{job.result=type==="create"?await this.createArchive(job,payload):await this.extractArchive(job,payload),job.status=job.cancelled?"cancelled":"completed"}catch(error){job.status=job.cancelled?"cancelled":"failed",job.error=error.message}finally{job.child=null,job.archive=null,job.finishedAt=new Date().toISOString()}}}),job}getJob(id){return this.jobs.get(id)}getCapabilities(){return archiveProvider.capabilities}cancelJob(id){let job=this.getJob(id);if(!job)throw createArchiveError("Archive job not found",404);return["completed","failed","cancelled"].includes(job.status)||(job.cancelled=!0,job.status="cancelled",job.child&&job.child.kill("SIGTERM"),job.archive&&job.archive.abort()),job}async createArchive(job,payload){let format=payload.format==="tar.gz"?"tar.gz":payload.format;if(!archiveProvider.capabilities.createFormats.includes(format)){let formats=archiveProvider.capabilities.createFormats.join(" and ");throw createArchiveError(`Supported creation formats are ${formats}`)}if(!Array.isArray(payload.sources)||payload.sources.length===0)throw createArchiveError("Sources must be a non-empty array");let sourcePaths=payload.sources.map(source=>this.resolvePath(source));sourcePaths.forEach(source=>{if(!existsSync(source))throw createArchiveError("Source path not found",404);if(lstatSync(source).isSymbolicLink())throw createArchiveError("Symbolic links cannot be archived");let realPath=realpathSync(source);if(!isInside(this.rootPath,realPath))throw createArchiveError("Access denied",403)}),await Promise.all(sourcePaths.map(source=>this.assertSourceTreeSafe(source)));let destinationPath=payload.destinationPath||path.posix.dirname(payload.sources[0]),archiveName=String(payload.name||"").trim(),requiredExtension=format==="zip"?".zip":".tar.gz",safeName=archiveName.endsWith(requiredExtension)?archiveName:`${archiveName}${requiredExtension}`,outputPath=this.getChildPath(destinationPath,safeName);if(existsSync(outputPath))throw createArchiveError("Archive path already exists",409);if(job.progress.totalEntries=sourcePaths.length,await new Promise((resolve,reject)=>{let output=createWriteStream(outputPath,{flags:"wx"}),archive=format==="zip"?archiver("zip",{zlib:{level:9}}):archiver("tar",{gzip:!0,gzipOptions:{level:9}});job.archive=archive,output.on("close",resolve),output.on("error",reject),archive.on("error",reject),archive.on("progress",progress=>{job.progress.processedEntries=progress.entries.processed,job.progress.totalEntries=progress.entries.total}),archive.pipe(output),sourcePaths.forEach(source=>{lstatSync(source).isDirectory()?archive.directory(source,path.basename(source)):archive.file(source,{name:path.basename(source)})}),archive.finalize()}).catch(error=>{throw rmSync(outputPath,{force:!0}),error}),job.cancelled)throw rmSync(outputPath,{force:!0}),createArchiveError("Archive job cancelled",499);return{path:outputPath,name:path.basename(outputPath)}}async extractArchive(job,payload){let sourcePath=this.resolveExistingFile(payload.path),destinationPath=this.resolvePath(payload.destinationPath||"/");if(!existsSync(destinationPath)||!lstatSync(destinationPath).isDirectory())throw createArchiveError("Extraction destination must be an existing directory",404);let listing=await this.listArchive(payload.path);job.progress.totalEntries=listing.totalEntries;let tempRoot=await fsPromises.mkdtemp(path.join(destinationPath,".mock-service-cli-archive-")),tempOutput=path.join(tempRoot,"output");mkdirSync(tempOutput);try{if(await archiveProvider.extract(job,sourcePath,tempOutput),job.cancelled)throw createArchiveError("Archive job cancelled",499);await this.assertExtractedTreeSafe(tempOutput);let outputEntries=await fsPromises.readdir(tempOutput);if(!outputEntries.length)throw createArchiveError("Archive produced no files",422);outputEntries.forEach(name=>{if(existsSync(path.join(destinationPath,name)))throw createArchiveError(`Destination already contains ${name}`,409)});for(let name of outputEntries)renameSync(path.join(tempOutput,name),path.join(destinationPath,name));return job.progress.processedEntries=job.progress.totalEntries,{destinationPath,entries:outputEntries}}finally{rmSync(tempRoot,{recursive:!0,force:!0})}}async assertExtractedTreeSafe(root){let rootRealPath=realpathSync(root),walk=async current=>{let entries=await fsPromises.readdir(current,{withFileTypes:!0});for(let entry of entries){let child=path.join(current,entry.name),stats=await fsPromises.lstat(child);if(stats.isSymbolicLink())throw createArchiveError("Archive contains symbolic links",422);let realPath=realpathSync(child);if(!isInside(rootRealPath,realPath))throw createArchiveError("Archive extracted outside its destination",422);stats.isDirectory()&&await walk(child)}};await walk(root)}async assertSourceTreeSafe(source){let stats=await fsPromises.lstat(source);if(stats.isSymbolicLink())throw createArchiveError("Symbolic links cannot be archived");if(!stats.isDirectory())return;let entries=await fsPromises.readdir(source,{withFileTypes:!0});await Promise.all(entries.map(entry=>this.assertSourceTreeSafe(path.join(source,entry.name))))}};module2.exports={ArchiveService,isArchivePath:archiveProvider.isArchivePath}}});var require_fileExplorerServer=__commonJS({"src/lib/fileExplorerServer.js"(){var express=require_express2(),{existsSync,readFileSync,statSync,lstatSync,rmSync,mkdirSync,writeFileSync,renameSync,realpathSync,copyFileSync,unlinkSync,constants:fsConstants,promises:fsPromises}=require("fs"),path=require("path"),os=require("os"),crypto=require("crypto"),multer=require_multer(),{UAParser}=require_ua_parser(),colors=require_safe(),portfinder=require_portfinder(),{exec,execFile}=require("child_process"),{dateFormat,logger,getServerHost,getServerUrls,hostAllowlistMiddleware,normalizeRemoteAddress}=require_utils3(),{getPackageVersion}=require_packageInfo(),{ArchiveService}=require_archiveService(),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",explorerPassword=process.env.EXPLORER_AUTH||"",isAuthEnabled=!!explorerPassword,visitorKeys=new Set,MAX_UPLOAD_FILE_SIZE=2*1024*1024*1024,MAX_UPLOAD_TOTAL_SIZE=2*1024*1024*1024,MAX_UPLOAD_FILE_COUNT=100,MAX_MULTIPART_OVERHEAD_SIZE=2*1024*1024,upload=multer({dest:path.join(os.tmpdir(),"mock-service-cli-upload"),preservePath:!0,limits:{fileSize:MAX_UPLOAD_FILE_SIZE,files:MAX_UPLOAD_FILE_COUNT,fields:10}}),archiveService=new ArchiveService({rootPath:explorerRootRealPath,resolvePath:resolveExplorerPath,getChildPath});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 isValidExplorerPassword(value){if(!isAuthEnabled)return!0;let provided=Buffer.from(String(value||"")),expected=Buffer.from(explorerPassword);return provided.length===expected.length&&crypto.timingSafeEqual(provided,expected)}function getExplorerPassword(req){return req.get("x-file-explorer-password")||req.body&&req.body.password}function requireExplorerAuth(req,res,next){if(!isValidExplorerPassword(getExplorerPassword(req)))return res.status(401).json({error:"Authentication required"});next()}function logExplorerVisit(req){let userAgent=req.get("user-agent")||"",ip=normalizeRemoteAddress(req.socket&&req.socket.remoteAddress),visitorKey=`${ip}
|
|
150
|
-
${userAgent}`;if(visitorKeys.has(visitorKey))return;visitorKeys.add(visitorKey);let parsed=new UAParser(userAgent).getResult(),browser=[parsed.browser.name,parsed.browser.version].filter(Boolean).join(" ")||"Unknown",operatingSystem=[parsed.os.name,parsed.os.version].filter(Boolean).join(" ")||"Unknown";log.info(`File explorer visitor: ip=${ip}, os=${operatingSystem}, browser=${browser}, userAgent=${userAgent}`)}function getUploadTargetPath(parentPath,originalName){let normalizedName=String(originalName||"").replace(/\\/g,"/").replace(/^\/+/,""),parts=normalizedName.split("/").filter(Boolean);if(parts.length===0||normalizedName!==parts.join("/")){let error=new Error("Invalid upload path");throw error.statusCode=400,error}parts.forEach(part=>{let nameError=validateEntryName(part);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 targetPath=path.resolve(parentFullPath,...parts);if(!isPathInsideRoot(targetPath)){let error=new Error("Access denied");throw error.statusCode=403,error}return{targetPath,parentFullPath,parts}}function ensureUploadParent(parentFullPath,parts){let current=parentFullPath;parts.slice(0,-1).forEach(part=>{if(current=path.join(current,part),existsSync(current)){let stats=lstatSync(current);if(!stats.isDirectory()||stats.isSymbolicLink()||!isPathInsideRoot(realpathSync(current))){let error=new Error("Upload path is not a safe directory");throw error.statusCode=403,error}}else mkdirSync(current)})}function moveUploadedFile(sourcePath,targetPath){copyFileSync(sourcePath,targetPath,fsConstants.COPYFILE_EXCL),unlinkSync(sourcePath)}function cleanupUploadedTempFiles(files=[]){files.forEach(file=>{file&&file.path&&existsSync(file.path)&&rmSync(file.path,{force:!0})})}function enforceUploadRequestSize(req,res,next){let contentLength=Number(req.get("content-length"));if(Number.isFinite(contentLength)&&contentLength>MAX_UPLOAD_TOTAL_SIZE+MAX_MULTIPART_OVERHEAD_SIZE)return res.status(413).json({error:"Total upload size exceeds 2GB limit"});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(hostAllowlistMiddleware()),app.use(express.json()),app.get("/favicon-file-explorer.svg",(req,res)=>res.sendFile(path.resolve(__dirname,"./favicon-file-explorer.svg"))),app.get("/favicon-file-explorer-login.svg",(req,res)=>res.sendFile(path.resolve(__dirname,"./favicon-file-explorer-login.svg"))),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("/__login",(req,res)=>{let htmlPath=path.resolve(__dirname,"./file-explorer-login.html");existsSync(htmlPath)?res.sendFile(htmlPath):res.status(404).send("File explorer login page not found")}),app.post("/__api/auth/verify",(req,res)=>{if(!isValidExplorerPassword(getExplorerPassword(req)))return res.status(401).json({error:"Invalid password"});res.json({success:!0,authEnabled:isAuthEnabled})}),app.use("/__api",requireExplorerAuth),app.get("/__api/config",(req,res)=>{logExplorerVisit(req),res.json({editMode:isEditMode,authEnabled:isAuthEnabled})}),app.get("/__api/archive/capabilities",requireEditMode,(req,res)=>{res.json(archiveService.getCapabilities())}),app.get("/__api/health",(req,res)=>{res.json({success:!0})}),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})}}),app.post("/__api/upload",requireEditMode,enforceUploadRequestSize,upload.any(),(req,res)=>{let parentPath=req.body&&req.body.parentPath||"/",files=req.files||[];if(!files.length)return res.status(400).json({error:"No files uploaded"});if(files.reduce((sum,file)=>sum+file.size,0)>MAX_UPLOAD_TOTAL_SIZE)return cleanupUploadedTempFiles(files),res.status(413).json({error:"Total upload size exceeds 2GB limit"});let uploaded=[],failed=[];files.forEach(file=>{try{let{targetPath,parentFullPath,parts}=getUploadTargetPath(parentPath,file.originalname);if(existsSync(targetPath)){failed.push({name:file.originalname,error:"Path already exists",statusCode:409});return}if(ensureUploadParent(parentFullPath,parts),existsSync(targetPath)){failed.push({name:file.originalname,error:"Path already exists",statusCode:409});return}moveUploadedFile(file.path,targetPath),uploaded.push({name:file.originalname,path:path.posix.join(normalizeExplorerInputPath(parentPath),...parts)})}catch(error){failed.push({name:file.originalname,error:error.message,statusCode:error.statusCode||500})}finally{existsSync(file.path)&&rmSync(file.path,{force:!0})}}),res.status(failed.length?207:200).json({success:failed.length===0,uploaded,failed})}),app.get("/__api/archive/preview",requireEditMode,async(req,res)=>{try{let preview=await archiveService.listArchive(normalizeExplorerInputPath(req.query.path||"/"));res.json(preview)}catch(error){res.status(error.statusCode||500).json({error:error.message})}}),app.post("/__api/archive/jobs",requireEditMode,(req,res)=>{let payload=req.body||{},operation=payload.operation;if(operation!=="create"&&operation!=="extract")return res.status(400).json({error:"Archive operation must be create or extract"});try{let job=archiveService.startJob(operation,payload);res.status(202).json({id:job.id,status:job.status,type:job.type})}catch(error){res.status(error.statusCode||500).json({error:error.message})}}),app.get("/__api/archive/jobs/:id",requireEditMode,(req,res)=>{let job=archiveService.getJob(req.params.id);if(!job)return res.status(404).json({error:"Archive job not found"});res.json({id:job.id,type:job.type,status:job.status,progress:job.progress,result:job.result,error:job.error,createdAt:job.createdAt,finishedAt:job.finishedAt||null})}),app.delete("/__api/archive/jobs/:id",requireEditMode,(req,res)=>{try{let job=archiveService.cancelJob(req.params.id);res.json({id:job.id,status:job.status})}catch(error){res.status(error.statusCode||500).json({error:error.message})}}),app.use((error,req,res,next)=>{if(error instanceof multer.MulterError){cleanupUploadedTempFiles(req.files);let statusCode=error.code==="LIMIT_FILE_SIZE"||error.code==="LIMIT_FILE_COUNT"?413:400;return res.status(statusCode).json({error:error.message})}if(error)return console.error(colors.red(`File explorer request failed: ${error.message}`)),res.status(error.statusCode||500).json({error:error.message||"Request failed"});next()}),startServer()}function startServer(){let http=require("http").createServer(app);http.requestTimeout=0,http.listen(Number.parseInt(process.env.PORT,10),getServerHost(),()=>{if(console.info([colors.yellow(`
|
|
149
|
+
`;function isArchivePath(filePath){return hasArchiveExtension(filePath,extensions)}function isCompressedTar(filePath){return/\.(tar\.gz|tgz|tar\.bz2|tbz|tbz2|tar\.xz|txz)$/i.test(filePath)}function parse7ZipList(output){let separator=output.indexOf("----------");return separator<0?[]:output.slice(separator+10).split(/\r?\n\r?\n/).map(block=>{let values={};return block.split(/\r?\n/).forEach(line=>{let index=line.indexOf(" = ");index>0&&(values[line.slice(0,index)]=line.slice(index+3))}),values}).filter(values=>values.Path).map(values=>({path:values.Path.replace(/\\/g,"/"),isDirectory:values.Folder==="+"||String(values.Attributes||"").startsWith("D"),size:Number(values.Size||0),packedSize:Number(values["Packed Size"]||0),encrypted:values.Encrypted==="+",modified:values.Modified||null,attributes:values.Attributes||""}))}function run7Zip(args,onProcess){return new Promise((resolve,reject)=>{try{process.platform!=="win32"&&chmodSync(path7za,493)}catch(error){return reject(createArchiveError(`Unable to prepare bundled 7-Zip binary: ${error.message}`,500))}let child=spawn(path7za,args,{stdio:["ignore","pipe","pipe"]}),stderr="",stdout="";onProcess&&onProcess(child),child.stdout.on("data",chunk=>{stdout+=chunk.toString()}),child.stderr.on("data",chunk=>{stderr+=chunk.toString()}),child.once("error",reject),child.once("close",code=>{if(code===0)return resolve(stdout);reject(createArchiveError(stderr.trim()||`7-Zip exited with code ${code}`,422))})})}async function list7Zip(fullPath){let output=await run7Zip(["l","-slt","--",fullPath]),entries=parse7ZipList(output);if(!isCompressedTar(fullPath))return entries;let tempRoot=await fsPromises.mkdtemp(path.join(os.tmpdir(),"mock-service-cli-archive-list-"));try{await run7Zip(["e","-y",`-o${tempRoot}`,"--",fullPath]);let tarName=(await fsPromises.readdir(tempRoot)).find(name=>/\.tar$/i.test(name));return tarName?await list7Zip(path.join(tempRoot,tarName)):entries}finally{rmSync(tempRoot,{recursive:!0,force:!0})}}async function listRar(fullPath){let list=(await createExtractorFromFile({filepath:fullPath})).getFileList();if(list.arcHeader.flags&&list.arcHeader.flags.headerEncrypted)throw createArchiveError("Encrypted archives are not supported",422);return[...list.fileHeaders].map(header=>({path:header.name.replace(/\\/g,"/"),isDirectory:!!(header.flags&&header.flags.directory),size:Number(header.unpSize||0),packedSize:Number(header.packSize||0),encrypted:!!(header.flags&&header.flags.encrypted),modified:header.time||null,attributes:""}))}async function listEntries(fullPath){return path.extname(fullPath).toLowerCase()===".rar"?listRar(fullPath):list7Zip(fullPath)}async function extractRar(job,sourcePath,targetPath){await new Promise((resolve,reject)=>{let worker=new Worker(RAR_EXTRACT_WORKER,{eval:!0,workerData:{sourcePath,targetPath}}),settled=!1,finish=(callback,value)=>{settled||(settled=!0,job.child=null,callback(value))};job.child={kill:()=>worker.terminate()},worker.on("message",message=>{message&&message.success?finish(resolve):finish(reject,createArchiveError(message&&message.error||"RAR extraction failed",422))}),worker.on("error",error=>finish(reject,error)),worker.on("exit",code=>{if(!settled&&code!==0){let message=job.cancelled?"Archive job cancelled":"RAR extraction stopped";finish(reject,createArchiveError(message,499))}})})}async function extract(job,sourcePath,targetPath){if(path.extname(sourcePath).toLowerCase()===".rar")return extractRar(job,sourcePath,targetPath);await run7Zip(["x","-y",`-o${targetPath}`,"--",sourcePath],child=>{job.child=child})}module2.exports={capabilities,extract,isArchivePath,listEntries}}});var require_archiveService=__commonJS({"src/lib/archiveService.js"(exports2,module2){var{createWriteStream,existsSync,lstatSync,mkdirSync,promises:fsPromises,realpathSync,renameSync,rmSync}=require("fs"),path=require("path"),crypto=require("crypto"),archiver=require("archiver"),archiveProvider=require_ultra(),{createArchiveError,isInside,isSafeArchiveEntry}=require_archiveShared(),MAX_ARCHIVE_SIZE=2*1024*1024*1024,MAX_ARCHIVE_ENTRIES=1e4,MAX_COMPRESSION_RATIO=100,ArchiveService=class{constructor({rootPath,resolvePath,getChildPath}){this.rootPath=rootPath,this.resolvePath=resolvePath,this.getChildPath=getChildPath,this.jobs=new Map}resolveExistingFile(inputPath){let fullPath=this.resolvePath(inputPath);if(!existsSync(fullPath))throw createArchiveError("Path not found",404);let stats=lstatSync(fullPath);if(!stats.isFile())throw createArchiveError("Archive path must be a file");if(!archiveProvider.isArchivePath(fullPath))throw createArchiveError("Unsupported archive format");if(stats.size>MAX_ARCHIVE_SIZE)throw createArchiveError("Archive exceeds 2GB limit",413);return fullPath}async listArchive(inputPath){let fullPath=this.resolveExistingFile(inputPath),entries=await archiveProvider.listEntries(fullPath);return this.assertEntriesSafe(entries,fullPath),{path:inputPath,name:path.basename(fullPath),entries,totalEntries:entries.length,totalSize:entries.reduce((total,entry)=>total+entry.size,0),totalPackedSize:entries.reduce((total,entry)=>total+entry.packedSize,0)}}assertEntriesSafe(entries,fullPath){if(!entries.length)throw createArchiveError("Archive contains no entries",422);if(entries.length>MAX_ARCHIVE_ENTRIES)throw createArchiveError("Archive contains too many entries",413);if(entries.some(entry=>entry.encrypted))throw createArchiveError("Encrypted archives are not supported",422);if(entries.some(entry=>entry.isSymbolicLink||!isSafeArchiveEntry(entry.path)))throw createArchiveError("Archive contains an unsafe entry path",422);let totalSize=entries.reduce((total,entry)=>total+entry.size,0),packedSize=Math.max(lstatSync(fullPath).size,1);if(totalSize>MAX_ARCHIVE_SIZE)throw createArchiveError("Archive expands beyond 2GB limit",413);if(totalSize/packedSize>MAX_COMPRESSION_RATIO)throw createArchiveError("Archive compression ratio exceeds limit",413)}startJob(type,payload){let id=crypto.randomUUID(),job={id,type,status:"queued",progress:{processedEntries:0,totalEntries:0},createdAt:new Date().toISOString(),result:null,error:null,cancelled:!1,child:null,archive:null};return this.jobs.set(id,job),setImmediate(async()=>{if(!job.cancelled){job.status="running";try{job.result=type==="create"?await this.createArchive(job,payload):await this.extractArchive(job,payload),job.status=job.cancelled?"cancelled":"completed"}catch(error){job.status=job.cancelled?"cancelled":"failed",job.error=error.message}finally{job.child=null,job.archive=null,job.finishedAt=new Date().toISOString()}}}),job}getJob(id){return this.jobs.get(id)}getCapabilities(){return archiveProvider.capabilities}cancelJob(id){let job=this.getJob(id);if(!job)throw createArchiveError("Archive job not found",404);return["completed","failed","cancelled"].includes(job.status)||(job.cancelled=!0,job.status="cancelled",job.child&&job.child.kill("SIGTERM"),job.archive&&job.archive.abort()),job}async createArchive(job,payload){let format=payload.format==="tar.gz"?"tar.gz":payload.format;if(!archiveProvider.capabilities.createFormats.includes(format)){let formats=archiveProvider.capabilities.createFormats.join(" and ");throw createArchiveError(`Supported creation formats are ${formats}`)}if(!Array.isArray(payload.sources)||payload.sources.length===0)throw createArchiveError("Sources must be a non-empty array");let sourcePaths=payload.sources.map(source=>this.resolvePath(source));sourcePaths.forEach(source=>{if(!existsSync(source))throw createArchiveError("Source path not found",404);if(lstatSync(source).isSymbolicLink())throw createArchiveError("Symbolic links cannot be archived");let realPath=realpathSync(source);if(!isInside(this.rootPath,realPath))throw createArchiveError("Access denied",403)}),await Promise.all(sourcePaths.map(source=>this.assertSourceTreeSafe(source)));let destinationPath=payload.destinationPath||path.posix.dirname(payload.sources[0]),archiveName=String(payload.name||"").trim(),requiredExtension=format==="zip"?".zip":".tar.gz",safeName=archiveName.endsWith(requiredExtension)?archiveName:`${archiveName}${requiredExtension}`,outputPath=this.getChildPath(destinationPath,safeName);if(existsSync(outputPath))throw createArchiveError("Archive path already exists",409);if(job.progress.totalEntries=sourcePaths.length,await new Promise((resolve,reject)=>{let output=createWriteStream(outputPath,{flags:"wx"}),archive=format==="zip"?archiver("zip",{zlib:{level:9}}):archiver("tar",{gzip:!0,gzipOptions:{level:9}});job.archive=archive,output.on("close",resolve),output.on("error",reject),archive.on("error",reject),archive.on("progress",progress=>{job.progress.processedEntries=progress.entries.processed,job.progress.totalEntries=progress.entries.total}),archive.pipe(output),sourcePaths.forEach(source=>{lstatSync(source).isDirectory()?archive.directory(source,path.basename(source)):archive.file(source,{name:path.basename(source)})}),archive.finalize()}).catch(error=>{throw rmSync(outputPath,{force:!0}),error}),job.cancelled)throw rmSync(outputPath,{force:!0}),createArchiveError("Archive job cancelled",499);return{path:outputPath,name:path.basename(outputPath)}}async extractArchive(job,payload){let sourcePath=this.resolveExistingFile(payload.path),destinationPath=this.resolvePath(payload.destinationPath||"/");if(!existsSync(destinationPath)||!lstatSync(destinationPath).isDirectory())throw createArchiveError("Extraction destination must be an existing directory",404);let listing=await this.listArchive(payload.path);job.progress.totalEntries=listing.totalEntries;let tempRoot=await fsPromises.mkdtemp(path.join(destinationPath,".mock-service-cli-archive-")),tempOutput=path.join(tempRoot,"output");mkdirSync(tempOutput);try{if(await archiveProvider.extract(job,sourcePath,tempOutput),job.cancelled)throw createArchiveError("Archive job cancelled",499);await this.assertExtractedTreeSafe(tempOutput);let outputEntries=await fsPromises.readdir(tempOutput);if(!outputEntries.length)throw createArchiveError("Archive produced no files",422);outputEntries.forEach(name=>{if(existsSync(path.join(destinationPath,name)))throw createArchiveError(`Destination already contains ${name}`,409)});for(let name of outputEntries)renameSync(path.join(tempOutput,name),path.join(destinationPath,name));return job.progress.processedEntries=job.progress.totalEntries,{destinationPath,entries:outputEntries}}finally{rmSync(tempRoot,{recursive:!0,force:!0})}}async assertExtractedTreeSafe(root){let rootRealPath=realpathSync(root),walk=async current=>{let entries=await fsPromises.readdir(current,{withFileTypes:!0});for(let entry of entries){let child=path.join(current,entry.name),stats=await fsPromises.lstat(child);if(stats.isSymbolicLink())throw createArchiveError("Archive contains symbolic links",422);let realPath=realpathSync(child);if(!isInside(rootRealPath,realPath))throw createArchiveError("Archive extracted outside its destination",422);stats.isDirectory()&&await walk(child)}};await walk(root)}async assertSourceTreeSafe(source){let stats=await fsPromises.lstat(source);if(stats.isSymbolicLink())throw createArchiveError("Symbolic links cannot be archived");if(!stats.isDirectory())return;let entries=await fsPromises.readdir(source,{withFileTypes:!0});await Promise.all(entries.map(entry=>this.assertSourceTreeSafe(path.join(source,entry.name))))}};module2.exports={ArchiveService,isArchivePath:archiveProvider.isArchivePath}}});var require_fileExplorerServer=__commonJS({"src/lib/fileExplorerServer.js"(){var express=require_express2(),{existsSync,readFileSync,statSync,lstatSync,rmSync,mkdirSync,writeFileSync,renameSync,realpathSync,copyFileSync,unlinkSync,constants:fsConstants,promises:fsPromises}=require("fs"),path=require("path"),os=require("os"),crypto=require("crypto"),multer=require_multer(),{UAParser}=require_ua_parser(),colors=require_safe(),portfinder=require_portfinder(),{exec,execFile}=require("child_process"),{dateFormat,logger,getServerHost,getServerUrls,hostAllowlistMiddleware,normalizeRemoteAddress}=require_utils3(),{getPackageVersion}=require_packageInfo(),{ArchiveService}=require_archiveService(),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),explorerRootId=crypto.createHash("sha256").update(explorerRootRealPath).digest("hex"),port=argv.p||argv.port,isEditMode=process.env.EXPLORER_EDIT==="true",explorerPassword=process.env.EXPLORER_AUTH||"",isAuthEnabled=!!explorerPassword,visitorKeys=new Set,MAX_UPLOAD_FILE_SIZE=2*1024*1024*1024,MAX_UPLOAD_TOTAL_SIZE=2*1024*1024*1024,MAX_UPLOAD_FILE_COUNT=100,MAX_MULTIPART_OVERHEAD_SIZE=2*1024*1024,upload=multer({dest:path.join(os.tmpdir(),"mock-service-cli-upload"),preservePath:!0,limits:{fileSize:MAX_UPLOAD_FILE_SIZE,files:MAX_UPLOAD_FILE_COUNT,fields:10}}),archiveService=new ArchiveService({rootPath:explorerRootRealPath,resolvePath:resolveExplorerPath,getChildPath});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 isValidExplorerPassword(value){if(!isAuthEnabled)return!0;let provided=Buffer.from(String(value||"")),expected=Buffer.from(explorerPassword);return provided.length===expected.length&&crypto.timingSafeEqual(provided,expected)}function getExplorerPassword(req){return req.get("x-file-explorer-password")||req.body&&req.body.password}function requireExplorerAuth(req,res,next){if(!isValidExplorerPassword(getExplorerPassword(req)))return res.status(401).json({error:"Authentication required"});next()}function logExplorerVisit(req){let userAgent=req.get("user-agent")||"",ip=normalizeRemoteAddress(req.socket&&req.socket.remoteAddress),visitorKey=`${ip}
|
|
150
|
+
${userAgent}`;if(visitorKeys.has(visitorKey))return;visitorKeys.add(visitorKey);let parsed=new UAParser(userAgent).getResult(),browser=[parsed.browser.name,parsed.browser.version].filter(Boolean).join(" ")||"Unknown",operatingSystem=[parsed.os.name,parsed.os.version].filter(Boolean).join(" ")||"Unknown";log.info(`File explorer visitor: ip=${ip}, os=${operatingSystem}, browser=${browser}, userAgent=${userAgent}`)}function getUploadTargetPath(parentPath,originalName){let normalizedName=String(originalName||"").replace(/\\/g,"/").replace(/^\/+/,""),parts=normalizedName.split("/").filter(Boolean);if(parts.length===0||normalizedName!==parts.join("/")){let error=new Error("Invalid upload path");throw error.statusCode=400,error}parts.forEach(part=>{let nameError=validateEntryName(part);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 targetPath=path.resolve(parentFullPath,...parts);if(!isPathInsideRoot(targetPath)){let error=new Error("Access denied");throw error.statusCode=403,error}return{targetPath,parentFullPath,parts}}function ensureUploadParent(parentFullPath,parts){let current=parentFullPath;parts.slice(0,-1).forEach(part=>{if(current=path.join(current,part),existsSync(current)){let stats=lstatSync(current);if(!stats.isDirectory()||stats.isSymbolicLink()||!isPathInsideRoot(realpathSync(current))){let error=new Error("Upload path is not a safe directory");throw error.statusCode=403,error}}else mkdirSync(current)})}function moveUploadedFile(sourcePath,targetPath){copyFileSync(sourcePath,targetPath,fsConstants.COPYFILE_EXCL),unlinkSync(sourcePath)}function cleanupUploadedTempFiles(files=[]){files.forEach(file=>{file&&file.path&&existsSync(file.path)&&rmSync(file.path,{force:!0})})}function enforceUploadRequestSize(req,res,next){let contentLength=Number(req.get("content-length"));if(Number.isFinite(contentLength)&&contentLength>MAX_UPLOAD_TOTAL_SIZE+MAX_MULTIPART_OVERHEAD_SIZE)return res.status(413).json({error:"Total upload size exceeds 2GB limit"});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(hostAllowlistMiddleware()),app.use(express.json());let faviconInstanceId=crypto.randomUUID(),explorerFaviconUrl=`/favicon-file-explorer.svg?instance=${faviconInstanceId}`,loginFaviconUrl=`/favicon-file-explorer-login.svg?instance=${faviconInstanceId}`,sendFavicon=(filename,res)=>{res.type("image/svg+xml").set("Cache-Control","no-store").sendFile(path.resolve(__dirname,filename))},sendExplorerPage=(filename,faviconUrl,res)=>{let htmlPath=path.resolve(__dirname,filename);if(!existsSync(htmlPath))return res.status(404).send("File explorer page not found");res.type("html").send(readFileSync(htmlPath,"utf8").replace("__FILE_EXPLORER_FAVICON_URL__",faviconUrl))};app.get("/favicon-file-explorer.svg",(req,res)=>sendFavicon("./favicon-file-explorer.svg",res)),app.get("/favicon-file-explorer-login.svg",(req,res)=>sendFavicon("./favicon-file-explorer-login.svg",res)),app.get("/",(req,res)=>sendExplorerPage("./file-explorer.html",explorerFaviconUrl,res)),app.get("/__login",(req,res)=>sendExplorerPage("./file-explorer-login.html",loginFaviconUrl,res)),app.post("/__api/auth/verify",(req,res)=>{if(!isValidExplorerPassword(getExplorerPassword(req)))return res.status(401).json({error:"Invalid password"});res.json({success:!0,authEnabled:isAuthEnabled})}),app.use("/__api",requireExplorerAuth),app.get("/__api/config",(req,res)=>{logExplorerVisit(req),res.json({editMode:isEditMode,authEnabled:isAuthEnabled,rootId:explorerRootId})}),app.get("/__api/archive/capabilities",requireEditMode,(req,res)=>{res.json(archiveService.getCapabilities())}),app.get("/__api/health",(req,res)=>{res.json({success:!0})}),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})}}),app.post("/__api/upload",requireEditMode,enforceUploadRequestSize,upload.any(),(req,res)=>{let parentPath=req.body&&req.body.parentPath||"/",files=req.files||[];if(!files.length)return res.status(400).json({error:"No files uploaded"});if(files.reduce((sum,file)=>sum+file.size,0)>MAX_UPLOAD_TOTAL_SIZE)return cleanupUploadedTempFiles(files),res.status(413).json({error:"Total upload size exceeds 2GB limit"});let uploaded=[],failed=[];files.forEach(file=>{try{let{targetPath,parentFullPath,parts}=getUploadTargetPath(parentPath,file.originalname);if(existsSync(targetPath)){failed.push({name:file.originalname,error:"Path already exists",statusCode:409});return}if(ensureUploadParent(parentFullPath,parts),existsSync(targetPath)){failed.push({name:file.originalname,error:"Path already exists",statusCode:409});return}moveUploadedFile(file.path,targetPath),uploaded.push({name:file.originalname,path:path.posix.join(normalizeExplorerInputPath(parentPath),...parts)})}catch(error){failed.push({name:file.originalname,error:error.message,statusCode:error.statusCode||500})}finally{existsSync(file.path)&&rmSync(file.path,{force:!0})}}),res.status(failed.length?207:200).json({success:failed.length===0,uploaded,failed})}),app.get("/__api/archive/preview",requireEditMode,async(req,res)=>{try{let preview=await archiveService.listArchive(normalizeExplorerInputPath(req.query.path||"/"));res.json(preview)}catch(error){res.status(error.statusCode||500).json({error:error.message})}}),app.post("/__api/archive/jobs",requireEditMode,(req,res)=>{let payload=req.body||{},operation=payload.operation;if(operation!=="create"&&operation!=="extract")return res.status(400).json({error:"Archive operation must be create or extract"});try{let job=archiveService.startJob(operation,payload);res.status(202).json({id:job.id,status:job.status,type:job.type})}catch(error){res.status(error.statusCode||500).json({error:error.message})}}),app.get("/__api/archive/jobs/:id",requireEditMode,(req,res)=>{let job=archiveService.getJob(req.params.id);if(!job)return res.status(404).json({error:"Archive job not found"});res.json({id:job.id,type:job.type,status:job.status,progress:job.progress,result:job.result,error:job.error,createdAt:job.createdAt,finishedAt:job.finishedAt||null})}),app.delete("/__api/archive/jobs/:id",requireEditMode,(req,res)=>{try{let job=archiveService.cancelJob(req.params.id);res.json({id:job.id,status:job.status})}catch(error){res.status(error.statusCode||500).json({error:error.message})}}),app.use((error,req,res,next)=>{if(error instanceof multer.MulterError){cleanupUploadedTempFiles(req.files);let statusCode=error.code==="LIMIT_FILE_SIZE"||error.code==="LIMIT_FILE_COUNT"?413:400;return res.status(statusCode).json({error:error.message})}if(error)return console.error(colors.red(`File explorer request failed: ${error.message}`)),res.status(error.statusCode||500).json({error:error.message||"Request failed"});next()}),startServer()}function startServer(){let http=require("http").createServer(app);http.requestTimeout=0,http.listen(Number.parseInt(process.env.PORT,10),getServerHost(),()=>{if(console.info([colors.yellow(`
|
|
151
151
|
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(`
|
|
152
152
|
\u{1F30D} file-explorer-server version: `),colors.cyan(getPackageVersion()),`
|
|
153
153
|
`].join("")),console.info(colors.yellow(`
|
package/package.json
CHANGED