| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194 |
- // ============================================================================
- // webservice.go — 工具页 Web 服务
- //
- // 选择任意目录开启本地静态文件浏览服务;readme.md 自动渲染为 HTML;
- // 支持端口自动探测与运行状态查询。
- // ============================================================================
- package main
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "net"
- "net/http"
- "os"
- "path/filepath"
- "strings"
- "sync"
- "time"
- "github.com/gomarkdown/markdown"
- )
- // WebServiceInfo 描述一次 Web 服务运行实例的状态,供前端展示。
- type WebServiceInfo struct {
- Running bool `json:"running"` // 是否运行中
- Dir string `json:"dir"` // 服务目录
- URL string `json:"url"` // 访问地址 http://127.0.0.1:port/
- IndexName string `json:"indexName"` // 实际用作默认页的文件(index.html / readme.md / 空)
- StartedAt string `json:"startedAt"` // 启动时间
- Error string `json:"error"` // 最近一次错误
- Connections int64 `json:"connections"` // 累计请求次数
- }
- // webService 持有静态文件服务的运行状态(全局单例,工具页调用)。
- type webService struct {
- mu sync.Mutex
- server *http.Server
- dir string
- url string
- index string // 默认页文件名
- started time.Time
- conns int64
- }
- var webSrv = &webService{}
- // pickWebDir 弹出目录选择对话框,返回用户选中的目录(取消则返回空串)。
- // 使用 Wails 全局 app 的 Dialog(绑定到主窗口)。
- func pickWebDir() (string, error) {
- if app == nil {
- return "", errors.New("应用未初始化")
- }
- dialog := app.Dialog.OpenFile().
- CanChooseDirectories(true).
- CanChooseFiles(false).
- SetTitle("选择要浏览的目录")
- return dialog.PromptForSingleSelection()
- }
- // resolveIndexName 判断目录默认页:优先 index.html,其次 readme.md(或 README.md)。
- func resolveIndexName(dir string) string {
- if fileExists(filepath.Join(dir, "index.html")) {
- return "index.html"
- }
- for _, name := range []string{"readme.md", "README.md", "Readme.md"} {
- if fileExists(filepath.Join(dir, name)) {
- return name
- }
- }
- return ""
- }
- func fileExists(path string) bool {
- st, err := os.Stat(path)
- return err == nil && !st.IsDir()
- }
- // webHandler 提供静态文件服务,并对「默认页」做兜底:
- // 请求根路径时,若无 index.html 但有 readme.md,则渲染 README 为 HTML。
- func webHandler(dir, indexName string) http.Handler {
- // 仅当 README 是默认页且无 index.html 时,才用自定义处理器(渲染 Markdown)
- if indexName != "" && !strings.EqualFold(indexName, "index.html") {
- readmePath := filepath.Join(dir, indexName)
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if r.URL.Path == "/" || r.URL.Path == "" {
- data, err := os.ReadFile(readmePath)
- if err != nil {
- http.Error(w, "readme.md 读取失败: "+err.Error(), http.StatusInternalServerError)
- return
- }
- html := markdown.ToHTML(data, nil, nil)
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- w.Write(html)
- return
- }
- // 非根路径:交给默认文件服务器(提供子目录/静态资源)
- http.FileServer(http.Dir(dir)).ServeHTTP(w, r)
- })
- }
- // 有 index.html(或两者都无):用标准文件服务器,浏览器自动服务 index.html 或目录列表
- return http.FileServer(http.Dir(dir))
- }
- // StartWebService 启动静态文件服务,监听 127.0.0.1:0 随机端口。
- // 若已运行则先停止旧实例。indexName 为空时自动探测默认页。
- func StartWebService(dir string) (WebServiceInfo, error) {
- if strings.TrimSpace(dir) == "" {
- return WebServiceInfo{}, errors.New("目录不能为空")
- }
- st, err := os.Stat(dir)
- if err != nil || !st.IsDir() {
- return WebServiceInfo{}, fmt.Errorf("目录不存在或不是文件夹: %s", dir)
- }
- indexName := resolveIndexName(dir)
- // 先停止已有实例(幂等)
- stopWebService()
- ln, err := net.Listen("tcp", "127.0.0.1:0")
- if err != nil {
- return WebServiceInfo{}, fmt.Errorf("监听端口失败: %w", err)
- }
- port := ln.Addr().(*net.TCPAddr).Port
- mux := http.NewServeMux()
- handler := webHandler(dir, indexName)
- mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- webSrv.mu.Lock()
- webSrv.conns++
- webSrv.mu.Unlock()
- handler.ServeHTTP(w, r)
- }))
- srv := &http.Server{Handler: mux}
- webSrv.mu.Lock()
- webSrv.server = srv
- webSrv.dir = dir
- webSrv.url = fmt.Sprintf("http://127.0.0.1:%d/", port)
- webSrv.index = indexName
- webSrv.started = time.Now()
- webSrv.conns = 0
- webSrv.mu.Unlock()
- go func() {
- _ = srv.Serve(ln) // 阻塞直到 Shutdown;返回 http.ErrServerClosed 属正常
- }()
- return WebServiceInfo{
- Running: true, Dir: dir, URL: webSrv.url, IndexName: indexName,
- StartedAt: webSrv.started.Format("2006-01-02 15:04:05"),
- }, nil
- }
- // StopWebService 停止当前静态文件服务(未运行则无副作用)。
- func StopWebService() {
- stopWebService()
- }
- func stopWebService() {
- webSrv.mu.Lock()
- defer webSrv.mu.Unlock()
- if webSrv.server != nil {
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- _ = webSrv.server.Shutdown(ctx)
- cancel()
- webSrv.server = nil
- webSrv.url = ""
- webSrv.dir = ""
- webSrv.index = ""
- }
- }
- // WebServiceStatus 返回当前 Web 服务状态。
- func WebServiceStatus() WebServiceInfo {
- webSrv.mu.Lock()
- defer webSrv.mu.Unlock()
- info := WebServiceInfo{
- Running: webSrv.server != nil, Dir: webSrv.dir, URL: webSrv.url,
- IndexName: webSrv.index, Connections: webSrv.conns,
- }
- if !webSrv.started.IsZero() {
- info.StartedAt = webSrv.started.Format("2006-01-02 15:04:05")
- }
- return info
- }
- // WebServiceJSON 为前端提供一次调用获取完整状态的辅助(避免多次 IPC)。
- func WebServiceJSON() string {
- b, _ := json.Marshal(WebServiceStatus())
- return string(b)
- }
|