UNPKG

hudada-cli

Version:

专为程序员准备的本地文档搜索,快捷开发工具

3 lines (2 loc) 9.92 kB
#!/usr/bin/env node import{createServer as e}from"http";import t,{existsSync as n,mkdirSync as o,statSync as r,createReadStream as i,rmSync as s,readdirSync as a,readFileSync as c,createWriteStream as l}from"fs";import d,{dirname as p,join as f,relative as m,isAbsolute as u,extname as h,basename as g}from"path";import w from"chalk";import y from"open";import{networkInterfaces as v}from"os";import{fileURLToPath as C,parse as H}from"url";import x from"jszip";import{Marked as T}from"marked";import{markedHighlight as b}from"marked-highlight";import S from"highlight.js";const j=C(import.meta.url),E=p(j),O=f(process.cwd()),I={".html":"text/html",".css":"text/css",".js":"text/javascript",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".svg":"image/svg+xml",".ico":"image/x-icon",".json":"application/json",".pdf":"application/pdf",".txt":"text/plain",".mp4":"video/mp4",".mp3":"audio/mpeg"};function F(e,t=""){const n=[];try{const o=a(e);return o.sort(((t,n)=>{const o=f(e,t),i=f(e,n),s=r(o).isDirectory(),a=r(i).isDirectory();return s&&!a?-1:!s&&a?1:t.localeCompare(n)})).forEach((o=>{const i=f(e,o),s=f(t,o),a=r(i),c=a.size,l=a.mtime.toLocaleString();a.isDirectory()?n.push({type:"directory",name:o,path:s,size:0,mtime:l,items:F(i,s)}):n.push({type:"file",name:o,path:s,size:c,mtime:l,ext:h(o).toLowerCase()})})),n}catch(e){return console.error("文件夹读取错误:",e),[]}}async function N(C=667){const j=e((async(e,d)=>{try{const y=H(e.url||"",!0),v=y.pathname||"/";console.log("请求路径:",decodeURIComponent(v));const C=process.cwd();if(d.setHeader("Access-Control-Allow-Origin","*"),d.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS, DELETE"),d.setHeader("Access-Control-Allow-Headers","Content-Type"),"OPTIONS"===e.method)return d.writeHead(200),void d.end();if(v.startsWith("/static/"))return void await async function(e,t,o){try{const e=decodeURIComponent(o.replace(/^\/static/,"")),s=f(E,"template",e);if(console.log(s,"handleStaticFile"),!n(s))return t.writeHead(404),void t.end("File not found");const a=r(s);if(!a.isFile())return t.writeHead(403),void t.end("Forbidden");const c=h(s).toLowerCase(),l=I[c]||"application/octet-stream";t.writeHead(200,{"Content-Type":l,"Content-Length":a.size,"Cache-Control":"public, max-age=3600"}),i(s).pipe(t)}catch(e){console.error("静态文件处理错误:",e),t.writeHead(500),t.end("Internal Server Error")}}(0,d,v);if(v.startsWith("/preview/"))return void await async function(e,o,s){try{const e=decodeURIComponent(s.replace(/^\/preview/,"")),a=f(process.cwd(),e);if(!n(a))return o.writeHead(404),void o.end("File not found");const c=r(a);if(!c.isFile())return o.writeHead(403),void o.end("Forbidden");if(a.endsWith(".md")){try{let e=await t.readFileSync(a,"utf-8");console.log(e,"mdContent"),e=function(e){const t=/\x1b\[[0-9;]*[a-zA-Z]/g;return(e=(e=e.replace(t,"")).replace(/\r\n/g,"\n")).trim()}(e),console.log("清理后的 Markdown 内容:",e);const n=new T(b({emptyLangClass:"hljs",langPrefix:"hljs language-",highlight(e,t,n){const o=S.getLanguage(t)?t:"plaintext";return S.highlight(e,{language:o}).value}})).parse(e);console.log(n,"htmlContent"),o.writeHead(200,{"Content-Type":"text/html;charset=utf-8"}),o.end(`\n <!DOCTYPE html>\n <html>\n <head>\n <meta charset="UTF-8">\n <title>${g(a)}</title>\n <link href="https://cdn.jsdelivr.net/npm/highlight.js@11.7.0/styles/github.min.css" rel="stylesheet">\n <style>\n body {\n font-family: Arial, sans-serif;\n line-height: 1.6;\n max-width: 800px;\n margin: 0 auto;\n padding: 20px;\n }\n pre {\n background-color: #f4f4f4;\n padding: 10px;\n overflow-x: auto;\n border-radius: 4px;\n }\n code {\n font-family: 'Courier New', Courier, monospace;\n background-color: #f1f1f1;\n padding: 2px 4px;\n border-radius: 3px;\n }\n /* 确保 highlight.js 样式生效 */\n pre code.hljs {\n display: block;\n overflow-x: auto;\n padding: 1em;\n }\n </style>\n </head>\n <body>\n ${n}\n </body>\n </html>\n `)}catch(e){o.writeHead(404,{"Content-Type":"text/plain;chatset=utf-8"}),o.end("Markdown 文件未找到")}return}const l=h(a).toLowerCase(),d=I[l]||"application/octet-stream";o.writeHead(200,{"Content-Type":d,"Content-Length":c.size,"Cache-Control":"public, max-age=3600"}),i(a).pipe(o)}catch(e){console.error("静态文件处理错误:",e),o.writeHead(500),o.end("Internal Server Error")}}(0,d,v);if("POST"===e.method&&"/upload"===v)return void await async function(e,t){const n=function(e){const t=e.match(/boundary=(?:"([^"]+)"|([^;]+))/i);return t?t[1]||t[2]:null}(e.headers["content-type"]||"");if(!n)return t.writeHead(400),void t.end("Invalid Content-Type");let r="",i="",a=null,c=Buffer.from("");e.on("data",(e=>{if(c=Buffer.concat([c,e]),!a&&c.includes(Buffer.from('filename="'))){const e=c.toString(),n=e.match(/filename="(.+?)"/),s=e.match(/name="path"\r\n\r\n(.+?)\r\n/);if(n){r=n[1];const e=s?s[1]:"",d=f(O,e,r),u=p(d);try{o(u,{recursive:!0}),console.log(w.green(`✓ 创建目录: ${m(O,u)||"根目录"}`))}catch(e){return console.error(w.red("创建目录失败:"),e),t.writeHead(500),void t.end("Failed to create directory")}i=d,a=l(i);const h=c.indexOf(Buffer.from("\r\n\r\n"))+4,g=c.slice(h);a.write(g),c=Buffer.from("")}}else a&&a.write(e)})),e.on("end",(()=>{a?(a.end(),t.writeHead(200,{"Content-Type":"application/json"}),t.end(JSON.stringify({success:!0,filename:r,path:i.replace(O,"").replace(/\\/g,"/").replace(/^\//,"")}))):(t.writeHead(400),t.end("No file uploaded"))})),e.on("error",(e=>{if(console.error(w.red("上传错误:"),e),a){a.end();try{s(i)}catch(e){console.error(w.red("清理临时文件失败:"),e)}}t.writeHead(500),t.end("Upload failed")}))}(e,d);if("GET"===e.method&&"/download-folder"===v)return void await async function(e,t,o){try{const i=o.path;if(!i)return t.writeHead(400),void t.end("Missing path parameter");const s=f(process.cwd(),i),l=m(f(process.cwd()),s);if(l.startsWith("..")||u(l))return t.writeHead(403),void t.end("Forbidden");if(!n(s)||!r(s).isDirectory())return t.writeHead(404),void t.end("Folder not found");const d=new x;async function p(e,t){const n=a(e);for(const o of n){const n=f(e,o);if(r(n).isDirectory()){const e=t.folder(o);e&&await p(n,e)}else{const e=c(n);t.file(o,e)}}}await p(s,d);const h=await d.generateAsync({type:"nodebuffer",compression:"DEFLATE",compressionOptions:{level:9}}),g=i.split("/").pop()||"download";t.writeHead(200,{"Content-Type":"application/zip","Content-Disposition":`attachment; filename="${encodeURIComponent(g)}.zip"`,"Content-Length":h.length}),t.end(h)}catch(w){console.error("文件夹下载错误:",w),t.writeHead(500),t.end("Internal Server Error")}}(0,d,y.query);if("DELETE"===e.method&&"/delete"===v)return void await async function(e,t,o){try{const e=o.path;if(!e)return t.writeHead(400),void t.end(JSON.stringify({success:!1,message:"缺少路径参数"}));const r=f(process.cwd(),e),i=m(f(process.cwd()),r);if(i.startsWith("..")||u(i))return t.writeHead(403),void t.end(JSON.stringify({success:!1,message:"非法的文件路径"}));if(!n(r))return t.writeHead(404),void t.end(JSON.stringify({success:!1,message:"文件不存在"}));await s(r,{recursive:!0,force:!0}),t.writeHead(200,{"Content-Type":"application/json"}),t.end(JSON.stringify({success:!0,message:"删除成功"}))}catch(e){console.error("删除文件错误:",e),t.writeHead(500),t.end(JSON.stringify({success:!1,message:"删除失败"}))}}(0,d,y.query);if("/"===v)return d.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),void d.end(function(){const e=process.cwd();let t=[];n(e)&&(t=F(e));try{let e=c(f(E,"template","index.js"),"utf-8");e=e.replace("{{ fileStructure }}",JSON.stringify(t,null,2));let n=c(f(E,"template","index.html"),"utf-8");return n=n.replace("{{jsTemplate}}",e),n}catch(e){return console.error(w.red("读取模板文件失败:"),e),'\n <!DOCTYPE html>\n <html>\n <head>\n <title>错误</title>\n <meta charset="utf-8">\n </head>\n <body>\n <h1>加载页面失败</h1>\n <p>请确保 template/index.html 文件存在</p>\n </body>\n </html>\n '}}());let j=f(C,v.slice(1));if(j=decodeURIComponent(j),console.log(j,"filePath"),v.startsWith("/"))return void await async function(e,t,o){console.log(o,"path");const s=f(process.cwd(),decodeURIComponent(o));if(n(s)&&r(s).isFile()){i(s).pipe(t)}else t.writeHead(404),t.end("File not found")}(0,d,v);d.writeHead(404),d.end("Not Found")}catch(e){console.error("请求处理错误:",e),d.writeHead(500),d.end("Internal Server Error")}}));n(O)||o(O,{recursive:!0}),j.listen(C,(()=>{let e=d.normalize(process.cwd());const t=`http://${function(){const e=v();for(const t of Object.keys(e))for(const n of e[t]||[])if(!n.internal&&"IPv4"===n.family)return n.address;return"localhost"}()}:${C}`;console.log(w.green("文件传输服务已启动!")),console.log(w.blue(`本地访问: http://localhost:${C}`)),console.log(w.blue(`局域网访问: ${t}`)),console.log(w.yellow("文件将传输在本地的 "+e+" 目录中")),y(t)})),j.on("error",(e=>{"EADDRINUSE"===e.code?(console.log(w.yellow(`端口 ${C} 已被占用,尝试使用端口 ${C+1}`)),N(C+1)):console.error(w.red("服务器错误:"),e.message)})),process.on("SIGINT",(()=>{console.log(w.yellow("\n正在关闭服务器...")),j.close((()=>{console.log(w.green("服务器已关闭")),process.exit(0)}))}))}export{N as startLocalServer};