feature (embed): embed frontend assets from the binary

This commit is contained in:
Mickael Kerjean
2022-11-22 23:03:33 +11:00
parent cb7f1693bd
commit 1eea60cfb0
7 changed files with 87 additions and 112 deletions

View File

@ -26,11 +26,10 @@ steps:
- apk add make git > /dev/null - apk add make git > /dev/null
- npm install --silent - npm install --silent
- make build_frontend - make build_frontend
- cp -R ./dist/data/public ./filestash/data/public
- name: build_backend - name: build_backend
image: golang:1.16-stretch image: golang:1.16-stretch
depends_on: [ build_prepare ] depends_on: [ build_prepare, build_frontend ]
environment: environment:
CGO_LDFLAGS_ALLOW: "-fopenmp" CGO_LDFLAGS_ALLOW: "-fopenmp"
GO111MODULE: "on" GO111MODULE: "on"

1
.gitignore vendored
View File

@ -18,3 +18,4 @@ package-lock.json
.tern-project.js .tern-project.js
*_test.go *_test.go
cover.* cover.*
www

View File

@ -5,7 +5,7 @@
"repository": "https://github.com/mickael-kerjean/filestash", "repository": "https://github.com/mickael-kerjean/filestash",
"main": "server/index.js", "main": "server/index.js",
"scripts": { "scripts": {
"dev": "webpack --watch", "start": "webpack --watch",
"build": "webpack", "build": "webpack",
"test": "jest ./client/", "test": "jest ./client/",
"lint": "eslint ./client/" "lint": "eslint ./client/"

View File

@ -19,8 +19,6 @@ const (
COOKIE_NAME_ADMIN = "admin" COOKIE_NAME_ADMIN = "admin"
COOKIE_PATH_ADMIN = "/admin/api/" COOKIE_PATH_ADMIN = "/admin/api/"
COOKIE_PATH = "/api/" COOKIE_PATH = "/api/"
FILE_INDEX = "./data/public/index.html"
FILE_ASSETS = "./data/public/"
URL_SETUP = "/admin/setup" URL_SETUP = "/admin/setup"
) )

View File

@ -1,7 +1,7 @@
package ctrl package ctrl
import ( import (
_ "embed" "embed"
"fmt" "fmt"
. "github.com/mickael-kerjean/filestash/server/common" . "github.com/mickael-kerjean/filestash/server/common"
"io" "io"
@ -14,46 +14,49 @@ import (
"text/template" "text/template"
) )
//go:embed static/404.html var (
var HtmlPage404 []byte //go:embed static/www
WWWEmbed embed.FS
//go:embed static/404.html
HtmlPage404 []byte
)
func StaticHandler(_path string) func(*App, http.ResponseWriter, *http.Request) { func StaticHandler(_path string) func(*App, http.ResponseWriter, *http.Request) {
return func(ctx *App, res http.ResponseWriter, req *http.Request) { return func(ctx *App, res http.ResponseWriter, req *http.Request) {
var base string = GetAbsolutePath(_path) var chroot string = GetAbsolutePath(_path)
var srcPath string if srcPath := JoinPath(chroot, req.URL.Path); strings.HasPrefix(srcPath, chroot) == false {
if srcPath = JoinPath(base, req.URL.Path); srcPath == base {
http.NotFound(res, req) http.NotFound(res, req)
return return
} }
ServeFile(res, req, srcPath) ServeFile(res, req, JoinPath(_path, req.URL.Path))
} }
} }
func IndexHandler(_path string) func(*App, http.ResponseWriter, *http.Request) { func IndexHandler(ctx *App, res http.ResponseWriter, req *http.Request) {
return func(ctx *App, res http.ResponseWriter, req *http.Request) { urlObj, err := URL.Parse(req.URL.String())
urlObj, err := URL.Parse(req.URL.String()) if err != nil {
if err != nil { NotFoundHandler(ctx, res, req)
NotFoundHandler(ctx, res, req) return
return }
} url := urlObj.Path
url := urlObj.Path
if url != URL_SETUP && Config.Get("auth.admin").String() == "" { if url != URL_SETUP && Config.Get("auth.admin").String() == "" {
http.Redirect(res, req, URL_SETUP, http.StatusTemporaryRedirect) http.Redirect(res, req, URL_SETUP, http.StatusTemporaryRedirect)
return return
} else if url != "/" && strings.HasPrefix(url, "/s/") == false && } else if url != "/" && strings.HasPrefix(url, "/s/") == false &&
strings.HasPrefix(url, "/view/") == false && strings.HasPrefix(url, "/files/") == false && strings.HasPrefix(url, "/view/") == false && strings.HasPrefix(url, "/files/") == false &&
url != "/login" && url != "/logout" && strings.HasPrefix(url, "/admin") == false && strings.HasPrefix(url, "/tags") == false { url != "/login" && url != "/logout" && strings.HasPrefix(url, "/admin") == false && strings.HasPrefix(url, "/tags") == false {
NotFoundHandler(ctx, res, req) NotFoundHandler(ctx, res, req)
return return
} }
ua := req.Header.Get("User-Agent") ua := req.Header.Get("User-Agent")
if strings.Contains(ua, "MSIE ") || strings.Contains(ua, "Trident/") || strings.Contains(ua, "Edge/") { if strings.Contains(ua, "MSIE ") || strings.Contains(ua, "Trident/") || strings.Contains(ua, "Edge/") {
// Microsoft is behaving on many occasion differently than Firefox / Chrome. // Microsoft is behaving on many occasion differently than Firefox / Chrome.
// I have neither the time / motivation for it to work properly // I have neither the time / motivation for it to work properly
res.WriteHeader(http.StatusBadRequest) res.WriteHeader(http.StatusBadRequest)
res.Write([]byte( res.Write([]byte(
Page(` Page(`
<h1>Internet explorer is not supported</h1> <h1>Internet explorer is not supported</h1>
<p> <p>
We don't support IE / Edge at this time We don't support IE / Edge at this time
@ -61,11 +64,9 @@ func IndexHandler(_path string) func(*App, http.ResponseWriter, *http.Request) {
Please use either Chromium, Firefox or Chrome Please use either Chromium, Firefox or Chrome
</p> </p>
`))) `)))
return return
}
srcPath := GetAbsolutePath(_path)
ServeFile(res, req, srcPath)
} }
ServeFile(res, req, "/index.html")
} }
func NotFoundHandler(ctx *App, res http.ResponseWriter, req *http.Request) { func NotFoundHandler(ctx *App, res http.ResponseWriter, req *http.Request) {
@ -130,6 +131,14 @@ func AboutHandler(ctx *App, res http.ResponseWriter, req *http.Request) {
table a { color: inherit; text-decoration: none; } table a { color: inherit; text-decoration: none; }
</style> </style>
`)) `))
hashFileContent := func(path string, n int) string {
f, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return ""
}
defer f.Close()
return HashStream(f, n)
}
t.Execute(res, struct { t.Execute(res, struct {
Version string Version string
CommitHash string CommitHash string
@ -219,75 +228,43 @@ func CustomCssHandler(ctx *App, res http.ResponseWriter, req *http.Request) {
} }
func ServeFile(res http.ResponseWriter, req *http.Request, filePath string) { func ServeFile(res http.ResponseWriter, req *http.Request, filePath string) {
zFilePath := filePath + ".gz" staticConfig := []struct {
bFilePath := filePath + ".br" ContentType string
FileExt string
etagNormal := hashFile(filePath, 10) }{
etagGzip := hashFile(zFilePath, 10) {"br", ".br"},
etagBr := hashFile(bFilePath, 10) {"gzip", ".gz"},
{"", ""},
if req.Header.Get("If-None-Match") != "" {
browserTag := req.Header.Get("If-None-Match")
if browserTag == etagNormal {
res.WriteHeader(http.StatusNotModified)
return
} else if browserTag == etagBr {
res.WriteHeader(http.StatusNotModified)
return
} else if browserTag == etagGzip {
res.WriteHeader(http.StatusNotModified)
return
}
} }
head := res.Header() head := res.Header()
acceptEncoding := req.Header.Get("Accept-Encoding") acceptEncoding := req.Header.Get("Accept-Encoding")
if strings.Contains(acceptEncoding, "br") { for _, cfg := range staticConfig {
if file, err := os.OpenFile(bFilePath, os.O_RDONLY, os.ModePerm); err == nil { if strings.Contains(acceptEncoding, cfg.ContentType) == false {
head.Set("Content-Encoding", "br") continue
head.Set("Etag", etagBr)
io.Copy(res, file)
file.Close()
return
} }
} else if strings.Contains(acceptEncoding, "gzip") { curPath := filePath + cfg.FileExt
if file, err := os.OpenFile(zFilePath, os.O_RDONLY, os.ModePerm); err == nil { file, err := WWWEmbed.Open("static/www" + curPath)
head.Set("Content-Encoding", "gzip") if err != nil {
head.Set("Etag", etagGzip) continue
io.Copy(res, file)
file.Close()
return
} }
} if stat, err := file.Stat(); err == nil {
etag := QuickHash(fmt.Sprintf(
file, err := os.OpenFile(filePath, os.O_RDONLY, os.ModePerm) "%s %d %d %s",
if err != nil { curPath, stat.Size(), stat.Mode(), stat.ModTime()), 10,
http.NotFound(res, req) )
if etag == req.Header.Get("If-None-Match") {
res.WriteHeader(http.StatusNotModified)
return
}
head.Set("Etag", etag)
}
if cfg.ContentType != "" {
head.Set("Content-Encoding", cfg.ContentType)
}
io.Copy(res, file)
file.Close()
return return
} }
head.Set("Etag", etagNormal) http.NotFound(res, req)
io.Copy(res, file)
file.Close()
}
func hashFile(path string, n int) string {
f, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return ""
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
return ""
}
return QuickHash(fmt.Sprintf("%s %d %d %s", path, stat.Size(), stat.Mode(), stat.ModTime()), n)
}
func hashFileContent(path string, n int) string {
f, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
return ""
}
defer f.Close()
return HashStream(f, n)
} }

View File

@ -85,7 +85,7 @@ func Init(a App) {
// Webdav server / Shared Link // Webdav server / Shared Link
middlewares = []Middleware{IndexHeaders, SecureHeaders} middlewares = []Middleware{IndexHeaders, SecureHeaders}
r.HandleFunc("/s/{share}", NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, a)).Methods("GET") r.HandleFunc("/s/{share}", NewMiddlewareChain(IndexHandler, middlewares, a)).Methods("GET")
middlewares = []Middleware{WebdavBlacklist, SessionStart} middlewares = []Middleware{WebdavBlacklist, SessionStart}
r.PathPrefix("/s/{share}").Handler(NewMiddlewareChain(WebdavHandler, middlewares, a)) r.PathPrefix("/s/{share}").Handler(NewMiddlewareChain(WebdavHandler, middlewares, a))
middlewares = []Middleware{ApiHeaders, SecureHeaders, RedirectSharedLoginIfNeeded, SessionStart, LoggedInOnly} middlewares = []Middleware{ApiHeaders, SecureHeaders, RedirectSharedLoginIfNeeded, SessionStart, LoggedInOnly}
@ -97,9 +97,9 @@ func Init(a App) {
r.HandleFunc("/api/backend", NewMiddlewareChain(AdminBackend, middlewares, a)).Methods("GET") r.HandleFunc("/api/backend", NewMiddlewareChain(AdminBackend, middlewares, a)).Methods("GET")
r.HandleFunc("/api/middlewares/authentication", NewMiddlewareChain(AdminAuthenticationMiddleware, middlewares, a)).Methods("GET") r.HandleFunc("/api/middlewares/authentication", NewMiddlewareChain(AdminAuthenticationMiddleware, middlewares, a)).Methods("GET")
middlewares = []Middleware{StaticHeaders, SecureHeaders} middlewares = []Middleware{StaticHeaders, SecureHeaders}
r.PathPrefix("/assets").Handler(http.HandlerFunc(NewMiddlewareChain(StaticHandler(FILE_ASSETS), middlewares, a))).Methods("GET") r.PathPrefix("/assets").Handler(http.HandlerFunc(NewMiddlewareChain(StaticHandler("/"), middlewares, a))).Methods("GET")
r.HandleFunc("/favicon.ico", NewMiddlewareChain(StaticHandler(FILE_ASSETS+"/assets/logo/"), middlewares, a)).Methods("GET") r.HandleFunc("/favicon.ico", NewMiddlewareChain(StaticHandler("/assets/logo/"), middlewares, a)).Methods("GET")
r.HandleFunc("/sw_cache.js", NewMiddlewareChain(StaticHandler(FILE_ASSETS+"/assets/worker/"), middlewares, a)).Methods("GET") r.HandleFunc("/sw_cache.js", NewMiddlewareChain(StaticHandler("/assets/worker/"), middlewares, a)).Methods("GET")
// Other endpoints // Other endpoints
middlewares = []Middleware{ApiHeaders} middlewares = []Middleware{ApiHeaders}
@ -118,8 +118,8 @@ func Init(a App) {
} }
initPluginsRoutes(r, &a) initPluginsRoutes(r, &a)
r.PathPrefix("/admin").Handler(http.HandlerFunc(NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, a))).Methods("GET") r.PathPrefix("/admin").Handler(http.HandlerFunc(NewMiddlewareChain(IndexHandler, middlewares, a))).Methods("GET")
r.PathPrefix("/").Handler(http.HandlerFunc(NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, a))).Methods("GET", "POST") r.PathPrefix("/").Handler(http.HandlerFunc(NewMiddlewareChain(IndexHandler, middlewares, a))).Methods("GET", "POST")
// Routes are served via plugins to avoid getting stuck with plain HTTP. The idea is to // Routes are served via plugins to avoid getting stuck with plain HTTP. The idea is to
// support many more protocols in the future: HTTPS, HTTP2, TOR or whatever that sounds // support many more protocols in the future: HTTPS, HTTP2, TOR or whatever that sounds

View File

@ -11,7 +11,7 @@ const config = {
app: path.join(__dirname, "client", "index.js"), app: path.join(__dirname, "client", "index.js"),
}, },
output: { output: {
path: path.join(__dirname, "dist", "data", "public"), path: path.join(__dirname, "server", "ctrl", "static", "www"),
publicPath: "/", publicPath: "/",
filename: "assets/js/[name]_[chunkhash].js", filename: "assets/js/[name]_[chunkhash].js",
chunkFilename: "assets/js/chunk_[name]_[id]_[chunkhash].js", chunkFilename: "assets/js/chunk_[name]_[id]_[chunkhash].js",