292 lines
8.7 KiB
Go
292 lines
8.7 KiB
Go
/*
|
||
* The simplest web service
|
||
* Created by MoeCinnamo on 2025/11/26.
|
||
*
|
||
* This is a simple web service that serves static files from the "docs" directory.
|
||
*/
|
||
|
||
package main
|
||
|
||
import (
|
||
"embed"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"os/signal"
|
||
"strings"
|
||
"sync"
|
||
"syscall"
|
||
"text/template"
|
||
)
|
||
|
||
func main() {
|
||
stop()
|
||
start()
|
||
}
|
||
|
||
//go:embed docs
|
||
var webFiles embed.FS
|
||
|
||
//go:embed docs/index.html
|
||
var indexTemplateContent string
|
||
|
||
//go:embed docs/404.html
|
||
var notFoundTemplateContent string
|
||
|
||
var (
|
||
indexTemplate *template.Template
|
||
notFoundTemplate *template.Template
|
||
indexOnce sync.Once
|
||
notFoundOnce sync.Once
|
||
)
|
||
|
||
func start() {
|
||
log.Printf("[Info] Starting MeowBox Docs...\r\n")
|
||
log.Printf("[Info] Web service started on 0.0.0.0:2230")
|
||
route()
|
||
err := http.ListenAndServe("0.0.0.0:2230", nil)
|
||
if err != nil {
|
||
log.Fatalf("[Error] Failed to start server: %v", err)
|
||
}
|
||
}
|
||
|
||
func stop() {
|
||
sigCh := make(chan os.Signal, 1)
|
||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||
|
||
go func() {
|
||
sig := <-sigCh
|
||
log.Printf("[Info] Received signal: %v,stopping...\r\n", sig)
|
||
os.Exit(0)
|
||
}()
|
||
}
|
||
|
||
// StaticFileHandler Embedded static file service
|
||
func staticFileHandler(w http.ResponseWriter, r *http.Request) {
|
||
path := r.URL.Path
|
||
|
||
// Clean path, make sure it starts with "docs/"
|
||
path = strings.TrimPrefix(path, "/")
|
||
if path == "" {
|
||
if err := notFoundTemplate.Execute(w, ""); err != nil {
|
||
log.Printf("[Error] Failed to render error page: %v", err)
|
||
}
|
||
return
|
||
}
|
||
|
||
// Concatenate embed.FS path
|
||
filePath := "docs/" + path
|
||
data, err := webFiles.ReadFile(filePath)
|
||
if err != nil {
|
||
log.Printf("[Error] Static file not found: %s", filePath)
|
||
if err := notFoundTemplate.Execute(w, ""); err != nil {
|
||
log.Printf("[Error] Failed to render error page: %v", err)
|
||
}
|
||
return
|
||
}
|
||
|
||
// Set Content-Type
|
||
contentType := getContentType(path)
|
||
w.Header().Set("Content-Type", contentType)
|
||
|
||
_, _ = w.Write(data)
|
||
}
|
||
|
||
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||
if r.URL.Path != "/" {
|
||
notFoundHandler(w, r)
|
||
return
|
||
}
|
||
loadIndexTemplate()
|
||
log.Printf("[Info] [Web Access] Handler index page, on path: %s", r.URL.Path)
|
||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||
if err := indexTemplate.Execute(w, nil); err != nil {
|
||
log.Printf("[Error] Failed to render index page: %v", err)
|
||
}
|
||
}
|
||
|
||
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
|
||
loadNotFoundTemplate()
|
||
log.Printf("[Info] [Web Access] Handler http error page: 404, on path: %s", r.URL.Path)
|
||
w.WriteHeader(404)
|
||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||
if err := notFoundTemplate.Execute(w, nil); err != nil {
|
||
log.Printf("[Error] Failed to render index page: %v", err)
|
||
}
|
||
}
|
||
|
||
func loadIndexTemplate() {
|
||
indexOnce.Do(func() {
|
||
var err error
|
||
indexTemplate, err = template.New("index").Parse(indexTemplateContent)
|
||
if err != nil {
|
||
log.Fatalf("[Error] Failed to parse index template: %v", err)
|
||
}
|
||
})
|
||
}
|
||
|
||
func loadNotFoundTemplate() {
|
||
notFoundOnce.Do(func() {
|
||
var err error
|
||
notFoundTemplate, err = template.New("error").Parse(notFoundTemplateContent)
|
||
if err != nil {
|
||
log.Fatalf("[Error] Failed to parse 404 template: %v", err)
|
||
}
|
||
})
|
||
}
|
||
|
||
func route() {
|
||
http.HandleFunc("/", indexHandler)
|
||
http.HandleFunc("/.well-known/", func(w http.ResponseWriter, r *http.Request) {
|
||
w.WriteHeader(http.StatusNoContent)
|
||
})
|
||
http.HandleFunc("/favicon.ico", staticFileHandler)
|
||
http.HandleFunc("/css/", staticFileHandler)
|
||
http.HandleFunc("/font-awesome/", staticFileHandler)
|
||
http.HandleFunc("/js/", staticFileHandler)
|
||
http.HandleFunc("/img/", staticFileHandler)
|
||
http.HandleFunc("/markdown/", staticFileHandler)
|
||
}
|
||
|
||
func getContentType(filename string) string {
|
||
// Convert file names to lowercase for case insensitive comparison
|
||
lowerFilename := strings.ToLower(filename)
|
||
|
||
switch {
|
||
// Text files
|
||
case strings.HasSuffix(lowerFilename, ".css"):
|
||
return "text/css; charset=utf-8"
|
||
case strings.HasSuffix(lowerFilename, ".js"):
|
||
return "application/javascript; charset=utf-8"
|
||
case strings.HasSuffix(lowerFilename, ".json"):
|
||
return "application/json; charset=utf-8"
|
||
case strings.HasSuffix(lowerFilename, ".xml"):
|
||
return "application/xml; charset=utf-8"
|
||
case strings.HasSuffix(lowerFilename, ".html"), strings.HasSuffix(lowerFilename, ".htm"):
|
||
return "text/html; charset=utf-8"
|
||
case strings.HasSuffix(lowerFilename, ".txt"):
|
||
return "text/plain; charset=utf-8"
|
||
case strings.HasSuffix(lowerFilename, ".md"):
|
||
return "text/markdown; charset=utf-8"
|
||
case strings.HasSuffix(lowerFilename, ".csv"):
|
||
return "text/csv; charset=utf-8"
|
||
|
||
// Image files
|
||
case strings.HasSuffix(lowerFilename, ".webp"):
|
||
return "image/webp"
|
||
case strings.HasSuffix(lowerFilename, ".png"):
|
||
return "image/png"
|
||
case strings.HasSuffix(lowerFilename, ".jpg"), strings.HasSuffix(lowerFilename, ".jpeg"):
|
||
return "image/jpeg"
|
||
case strings.HasSuffix(lowerFilename, ".gif"):
|
||
return "image/gif"
|
||
case strings.HasSuffix(lowerFilename, ".bmp"):
|
||
return "image/bmp"
|
||
case strings.HasSuffix(lowerFilename, ".ico"):
|
||
return "image/x-icon"
|
||
case strings.HasSuffix(lowerFilename, ".svg"), strings.HasSuffix(lowerFilename, ".svgz"):
|
||
return "image/svg+xml"
|
||
case strings.HasSuffix(lowerFilename, ".tiff"), strings.HasSuffix(lowerFilename, ".tif"):
|
||
return "image/tiff"
|
||
case strings.HasSuffix(lowerFilename, ".avif"):
|
||
return "image/avif"
|
||
|
||
// Audio files
|
||
case strings.HasSuffix(lowerFilename, ".mp3"):
|
||
return "audio/mpeg"
|
||
case strings.HasSuffix(lowerFilename, ".wav"):
|
||
return "audio/wav"
|
||
case strings.HasSuffix(lowerFilename, ".ogg"):
|
||
return "audio/ogg"
|
||
case strings.HasSuffix(lowerFilename, ".flac"):
|
||
return "audio/flac"
|
||
case strings.HasSuffix(lowerFilename, ".aac"):
|
||
return "audio/aac"
|
||
case strings.HasSuffix(lowerFilename, ".m4a"):
|
||
return "audio/mp4"
|
||
|
||
// Video files
|
||
case strings.HasSuffix(lowerFilename, ".mp4"):
|
||
return "video/mp4"
|
||
case strings.HasSuffix(lowerFilename, ".webm"):
|
||
return "video/webm"
|
||
case strings.HasSuffix(lowerFilename, ".ogg"), strings.HasSuffix(lowerFilename, ".ogv"):
|
||
return "video/ogg"
|
||
case strings.HasSuffix(lowerFilename, ".mov"):
|
||
return "video/quicktime"
|
||
case strings.HasSuffix(lowerFilename, ".avi"):
|
||
return "video/x-msvideo"
|
||
case strings.HasSuffix(lowerFilename, ".wmv"):
|
||
return "video/x-ms-wmv"
|
||
case strings.HasSuffix(lowerFilename, ".flv"):
|
||
return "video/x-flv"
|
||
case strings.HasSuffix(lowerFilename, ".mkv"):
|
||
return "video/x-matroska"
|
||
|
||
// Font files
|
||
case strings.HasSuffix(lowerFilename, ".woff"):
|
||
return "font/woff"
|
||
case strings.HasSuffix(lowerFilename, ".woff2"):
|
||
return "font/woff2"
|
||
case strings.HasSuffix(lowerFilename, ".ttf"):
|
||
return "font/ttf"
|
||
case strings.HasSuffix(lowerFilename, ".otf"):
|
||
return "font/otf"
|
||
|
||
// Archive files
|
||
case strings.HasSuffix(lowerFilename, ".zip"):
|
||
return "application/zip"
|
||
case strings.HasSuffix(lowerFilename, ".rar"):
|
||
return "application/x-rar-compressed"
|
||
case strings.HasSuffix(lowerFilename, ".gz"):
|
||
return "application/gzip"
|
||
case strings.HasSuffix(lowerFilename, ".tar"):
|
||
return "application/x-tar"
|
||
case strings.HasSuffix(lowerFilename, ".7z"):
|
||
return "application/x-7z-compressed"
|
||
case strings.HasSuffix(lowerFilename, ".bz2"):
|
||
return "application/x-bzip2"
|
||
case strings.HasSuffix(lowerFilename, ".xz"):
|
||
return "application/x-xz"
|
||
|
||
// Document files
|
||
case strings.HasSuffix(lowerFilename, ".pdf"):
|
||
return "application/pdf"
|
||
case strings.HasSuffix(lowerFilename, ".doc"):
|
||
return "application/msword"
|
||
case strings.HasSuffix(lowerFilename, ".docx"):
|
||
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||
case strings.HasSuffix(lowerFilename, ".xls"):
|
||
return "application/vnd.ms-excel"
|
||
case strings.HasSuffix(lowerFilename, ".xlsx"):
|
||
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||
case strings.HasSuffix(lowerFilename, ".ppt"):
|
||
return "application/vnd.ms-powerpoint"
|
||
case strings.HasSuffix(lowerFilename, ".pptx"):
|
||
return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||
case strings.HasSuffix(lowerFilename, ".odt"):
|
||
return "application/vnd.oasis.opendocument.text"
|
||
case strings.HasSuffix(lowerFilename, ".ods"):
|
||
return "application/vnd.oasis.opendocument.spreadsheet"
|
||
case strings.HasSuffix(lowerFilename, ".odp"):
|
||
return "application/vnd.oasis.opendocument.presentation"
|
||
|
||
// Other files
|
||
case strings.HasSuffix(lowerFilename, ".rtf"):
|
||
return "application/rtf"
|
||
case strings.HasSuffix(lowerFilename, ".epub"):
|
||
return "application/epub+zip"
|
||
case strings.HasSuffix(lowerFilename, ".apk"):
|
||
return "application/vnd.android.package-archive"
|
||
case strings.HasSuffix(lowerFilename, ".exe"):
|
||
return "application/x-msdownload"
|
||
case strings.HasSuffix(lowerFilename, ".dmg"):
|
||
return "application/x-apple-diskimage"
|
||
case strings.HasSuffix(lowerFilename, ".iso"):
|
||
return "application/x-iso9660-image"
|
||
|
||
default:
|
||
return "application/octet-stream"
|
||
}
|
||
}
|