Compare commits

...

5 Commits
v0.1.1 ... main

Author SHA1 Message Date
c6ab3e2466 fix not working stuff 2025-06-06 21:06:38 +02:00
66086b1a91 try add live reloading 2025-06-06 20:41:50 +02:00
7e0d7a1625 bugfix variable lookup path 2025-05-05 20:36:12 +02:00
45629a7bff - 2025-05-05 20:24:05 +02:00
bb660bc2fe try another name 2025-05-05 20:23:04 +02:00
9 changed files with 181 additions and 58 deletions

View File

@ -1,3 +1,10 @@
{
"cSpell.words": ["apihub", "spahandler"]
"cSpell.words": [
"apihub",
"appender",
"livereloader",
"onclose",
"spahandler",
"Upgrader"
]
}

2
go.mod
View File

@ -1,3 +1,5 @@
module git.apihub24.de/admin/spa-handler
go 1.22.0
require github.com/gorilla/websocket v1.5.3

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

76
handler.go Normal file
View File

@ -0,0 +1,76 @@
package spa_handler
import (
"embed"
"fmt"
"net/http"
"os"
"path"
)
type handler struct {
staticFS embed.FS
indexPath string
addLiveReloadScript bool
clientVersion string
}
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
reqPath := r.URL.Path
lookupPath := path.Dir(h.indexPath)
f, err := h.staticFS.Open(fmt.Sprintf("%s%s", lookupPath, 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()
if h.addLiveReloadScript {
tryToDeliverFile(indexFile, h.indexPath, w, r, clientScript(h.clientVersion))
} else {
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, mux *http.ServeMux) {
if options.AdditionalMimeTypeMapping != nil {
for key, value := range options.AdditionalMimeTypeMapping {
contentTypeMapping[key] = value
}
}
if mux == nil {
http.Handle(path, handler{
staticFS: options.Files,
indexPath: options.IndexPath,
addLiveReloadScript: options.ActivateLiveReloading,
clientVersion: options.ClientVersion,
})
if options.ActivateLiveReloading {
http.Handle("/_livereloader_", reloader{
clientVersion: options.ClientVersion,
})
}
return
} else {
mux.Handle(path, handler{
staticFS: options.Files,
indexPath: options.IndexPath,
})
if options.ActivateLiveReloading {
mux.Handle("/_livereloader_", reloader{
clientVersion: options.ClientVersion,
})
}
}
}

View File

@ -1,19 +1,28 @@
package spahandler
package spa_handler
import (
"bytes"
"fmt"
"io"
"io/fs"
"net/http"
"strings"
"time"
)
func tryToDeliverFile(file fs.File, path string, w http.ResponseWriter, r *http.Request) {
fileSeeker, ok := file.(io.ReadSeeker)
if !ok {
func tryToDeliverFile(file fs.File, path string, w http.ResponseWriter, r *http.Request, appender ...string) {
content, err := io.ReadAll(file)
if err != nil {
file.Close()
http.Error(w, "Index file does not implement ReadSeeker", http.StatusInternalServerError)
http.Error(w, "can not read file", http.StatusInternalServerError)
return
}
merge := bytes.NewBuffer([]byte{})
for _, a := range appender {
merge.WriteString(a)
}
newContent := strings.Replace(string(content), "</html>", fmt.Sprintf("%s\n</html>", merge.String()), 1)
stats, statErr := file.Stat()
if statErr != nil {
http.Error(w, "Cannot stat index file", http.StatusInternalServerError)
@ -24,8 +33,10 @@ func tryToDeliverFile(file fs.File, path string, w http.ResponseWriter, r *http.
contentType = "text/html"
}
contentType = getContentTypeDetail(contentType, stats)
fileSeeker := bytes.NewReader([]byte(newContent))
w.Header().Set("Content-Type", contentType)
http.ServeContent(w, r, path, stats.ModTime(), fileSeeker)
http.ServeContent(w, r, path, time.Now(), fileSeeker)
}
func detectContentType(file fs.File) (string, error) {

View File

@ -1,4 +1,4 @@
package spahandler
package spa_handler
var contentTypeMapping = map[string]string{
".html": "text/html",

View File

@ -1,9 +1,11 @@
package spahandler
package spa_handler
import "embed"
type HandlerOptions struct {
Files embed.FS
IndexPath string
ClientVersion string
ActivateLiveReloading bool
AdditionalMimeTypeMapping map[string]string
}

71
reloader.go Normal file
View File

@ -0,0 +1,71 @@
package spa_handler
import (
"fmt"
"net/http"
"github.com/gorilla/websocket"
)
func clientScript(clientVersion string) string {
return fmt.Sprintf(`
<script type="application/javascript">
// [spa_handler] live reload script
function spaHandlerLiveReloaderConnect() {
const ws = new WebSocket("/_livereloader_");
ws.onopen = function() {
console.info('[spa_handler]: live reloader connected!');
};
ws.onmessage = function(event) {
try {
const message = JSON.parse(event.data);
if (message && message.clientVersion && message.clientVersion !== '%s') {
console.info('[spa_handler]: receive reload');
location.reload(true);
}
} catch (err) {
console.warning('[spa_handler]: something was wrong! ', err)
}
};
ws.onclose = function() {
console.info('[spa_handler]: connection closed');
setTimeout(function() {
console.info('[spa_handler]: try reconnect');
spaHandlerLiveReloaderConnect();
}, 5000);
};
ws.onerror = function(err) {
console.warning('[spa_handler]: something was wrong with websocket connection! ', err)
};
}
spaHandlerLiveReloaderConnect();
</script>
`, clientVersion)
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type reloader struct {
clientVersion string
}
func (rel reloader) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
fmt.Println("Error upgrading:", err)
return
}
defer conn.Close()
_ = conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("{\"clientVersion\":\"%s\"}", rel.clientVersion)))
for {
_, _, err := conn.ReadMessage()
if err != nil {
fmt.Println("Error reading message:", err)
break
}
}
}

View File

@ -1,48 +0,0 @@
package spahandler
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,
})
}