From 9bc4847a3bc9cf1bc387774547c61bb58b28cc4e Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 5 May 2025 20:08:29 +0200 Subject: [PATCH] init --- go.mod | 3 +++ spa/handler.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ spa/helper.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ spa/mime_type.go | 9 +++++++++ spa/options.go | 9 +++++++++ 5 files changed, 116 insertions(+) create mode 100644 go.mod create mode 100644 spa/handler.go create mode 100644 spa/helper.go create mode 100644 spa/mime_type.go create mode 100644 spa/options.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..820f3de --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.apihub24.de/admin/spa-handler + +go 1.22.0 diff --git a/spa/handler.go b/spa/handler.go new file mode 100644 index 0000000..2feef8b --- /dev/null +++ b/spa/handler.go @@ -0,0 +1,48 @@ +package spa + +import ( + "embed" + "fmt" + "net/http" + "os" +) + +type handler struct { + staticFS embed.FS + indexPath string +} + +func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + reqPath := r.URL.Path + f, err := h.staticFS.Open(fmt.Sprintf("ng-client/browser%s", reqPath)) + if f != nil { + f.Close() + } + + if os.IsNotExist(err) { + indexFile, indexErr := h.staticFS.Open(h.indexPath) + if indexErr != nil { + http.Error(w, "Index file not found", http.StatusInternalServerError) + return + } + defer indexFile.Close() + tryToDeliverFile(indexFile, h.indexPath, w, r) + return + } else if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + tryToDeliverFile(f, h.indexPath, w, r) +} + +func HandleFiles(path string, options HandlerOptions) { + if options.AdditionalMimeTypeMapping != nil { + for key, value := range options.AdditionalMimeTypeMapping { + contentTypeMapping[key] = value + } + } + http.Handle(path, handler{ + staticFS: options.Files, + indexPath: options.IndexPath, + }) +} diff --git a/spa/helper.go b/spa/helper.go new file mode 100644 index 0000000..e735201 --- /dev/null +++ b/spa/helper.go @@ -0,0 +1,47 @@ +package spa + +import ( + "io" + "io/fs" + "net/http" + "strings" +) + +func tryToDeliverFile(file fs.File, path string, w http.ResponseWriter, r *http.Request) { + fileSeeker, ok := file.(io.ReadSeeker) + if !ok { + file.Close() + http.Error(w, "Index file does not implement ReadSeeker", http.StatusInternalServerError) + return + } + stats, statErr := file.Stat() + if statErr != nil { + http.Error(w, "Cannot stat index file", http.StatusInternalServerError) + return + } + contentType, contentTypeErr := detectContentType(file) + if contentTypeErr != nil { + contentType = "text/html" + } + contentType = getContentTypeDetail(contentType, stats) + w.Header().Set("Content-Type", contentType) + http.ServeContent(w, r, path, stats.ModTime(), fileSeeker) +} + +func detectContentType(file fs.File) (string, error) { + buffer := []byte{} + _, err := file.Read(buffer) + if err != nil { + return "", err + } + return http.DetectContentType(buffer), nil +} + +func getContentTypeDetail(contentType string, info fs.FileInfo) string { + for ending, realContentType := range contentTypeMapping { + if strings.HasSuffix(info.Name(), ending) { + return realContentType + } + } + return contentType +} \ No newline at end of file diff --git a/spa/mime_type.go b/spa/mime_type.go new file mode 100644 index 0000000..516a6c8 --- /dev/null +++ b/spa/mime_type.go @@ -0,0 +1,9 @@ +package spa + +var contentTypeMapping = map[string]string{ + ".html": "text/html", + ".htm": "text/html", + ".js": "text/javascript", + ".json": "application/json", + ".css": "text/css", +} \ No newline at end of file diff --git a/spa/options.go b/spa/options.go new file mode 100644 index 0000000..db4a6af --- /dev/null +++ b/spa/options.go @@ -0,0 +1,9 @@ +package spa + +import "embed" + +type HandlerOptions struct { + Files embed.FS + IndexPath string + AdditionalMimeTypeMapping map[string]string +}