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
- npm install --silent
- make build_frontend
- cp -R ./dist/data/public ./filestash/data/public
- name: build_backend
image: golang:1.16-stretch
depends_on: [ build_prepare ]
depends_on: [ build_prepare, build_frontend ]
environment:
CGO_LDFLAGS_ALLOW: "-fopenmp"
GO111MODULE: "on"

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
package ctrl
import (
_ "embed"
"embed"
"fmt"
. "github.com/mickael-kerjean/filestash/server/common"
"io"
@ -14,46 +14,49 @@ import (
"text/template"
)
//go:embed static/404.html
var HtmlPage404 []byte
var (
//go:embed static/www
WWWEmbed embed.FS
//go:embed static/404.html
HtmlPage404 []byte
)
func StaticHandler(_path string) func(*App, http.ResponseWriter, *http.Request) {
return func(ctx *App, res http.ResponseWriter, req *http.Request) {
var base string = GetAbsolutePath(_path)
var srcPath string
if srcPath = JoinPath(base, req.URL.Path); srcPath == base {
var chroot string = GetAbsolutePath(_path)
if srcPath := JoinPath(chroot, req.URL.Path); strings.HasPrefix(srcPath, chroot) == false {
http.NotFound(res, req)
return
}
ServeFile(res, req, srcPath)
ServeFile(res, req, JoinPath(_path, req.URL.Path))
}
}
func IndexHandler(_path string) func(*App, http.ResponseWriter, *http.Request) {
return func(ctx *App, res http.ResponseWriter, req *http.Request) {
urlObj, err := URL.Parse(req.URL.String())
if err != nil {
NotFoundHandler(ctx, res, req)
return
}
url := urlObj.Path
func IndexHandler(ctx *App, res http.ResponseWriter, req *http.Request) {
urlObj, err := URL.Parse(req.URL.String())
if err != nil {
NotFoundHandler(ctx, res, req)
return
}
url := urlObj.Path
if url != URL_SETUP && Config.Get("auth.admin").String() == "" {
http.Redirect(res, req, URL_SETUP, http.StatusTemporaryRedirect)
return
} else if url != "/" && strings.HasPrefix(url, "/s/") == 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 {
NotFoundHandler(ctx, res, req)
return
}
ua := req.Header.Get("User-Agent")
if strings.Contains(ua, "MSIE ") || strings.Contains(ua, "Trident/") || strings.Contains(ua, "Edge/") {
// Microsoft is behaving on many occasion differently than Firefox / Chrome.
// I have neither the time / motivation for it to work properly
res.WriteHeader(http.StatusBadRequest)
res.Write([]byte(
Page(`
if url != URL_SETUP && Config.Get("auth.admin").String() == "" {
http.Redirect(res, req, URL_SETUP, http.StatusTemporaryRedirect)
return
} else if url != "/" && strings.HasPrefix(url, "/s/") == 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 {
NotFoundHandler(ctx, res, req)
return
}
ua := req.Header.Get("User-Agent")
if strings.Contains(ua, "MSIE ") || strings.Contains(ua, "Trident/") || strings.Contains(ua, "Edge/") {
// Microsoft is behaving on many occasion differently than Firefox / Chrome.
// I have neither the time / motivation for it to work properly
res.WriteHeader(http.StatusBadRequest)
res.Write([]byte(
Page(`
<h1>Internet explorer is not supported</h1>
<p>
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
</p>
`)))
return
}
srcPath := GetAbsolutePath(_path)
ServeFile(res, req, srcPath)
return
}
ServeFile(res, req, "/index.html")
}
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; }
</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 {
Version 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) {
zFilePath := filePath + ".gz"
bFilePath := filePath + ".br"
etagNormal := hashFile(filePath, 10)
etagGzip := hashFile(zFilePath, 10)
etagBr := hashFile(bFilePath, 10)
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
}
staticConfig := []struct {
ContentType string
FileExt string
}{
{"br", ".br"},
{"gzip", ".gz"},
{"", ""},
}
head := res.Header()
acceptEncoding := req.Header.Get("Accept-Encoding")
if strings.Contains(acceptEncoding, "br") {
if file, err := os.OpenFile(bFilePath, os.O_RDONLY, os.ModePerm); err == nil {
head.Set("Content-Encoding", "br")
head.Set("Etag", etagBr)
io.Copy(res, file)
file.Close()
return
for _, cfg := range staticConfig {
if strings.Contains(acceptEncoding, cfg.ContentType) == false {
continue
}
} else if strings.Contains(acceptEncoding, "gzip") {
if file, err := os.OpenFile(zFilePath, os.O_RDONLY, os.ModePerm); err == nil {
head.Set("Content-Encoding", "gzip")
head.Set("Etag", etagGzip)
io.Copy(res, file)
file.Close()
return
curPath := filePath + cfg.FileExt
file, err := WWWEmbed.Open("static/www" + curPath)
if err != nil {
continue
}
}
file, err := os.OpenFile(filePath, os.O_RDONLY, os.ModePerm)
if err != nil {
http.NotFound(res, req)
if stat, err := file.Stat(); err == nil {
etag := QuickHash(fmt.Sprintf(
"%s %d %d %s",
curPath, stat.Size(), stat.Mode(), stat.ModTime()), 10,
)
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
}
head.Set("Etag", etagNormal)
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)
http.NotFound(res, req)
}

View File

@ -85,7 +85,7 @@ func Init(a App) {
// Webdav server / Shared Link
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}
r.PathPrefix("/s/{share}").Handler(NewMiddlewareChain(WebdavHandler, middlewares, a))
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/middlewares/authentication", NewMiddlewareChain(AdminAuthenticationMiddleware, middlewares, a)).Methods("GET")
middlewares = []Middleware{StaticHeaders, SecureHeaders}
r.PathPrefix("/assets").Handler(http.HandlerFunc(NewMiddlewareChain(StaticHandler(FILE_ASSETS), middlewares, a))).Methods("GET")
r.HandleFunc("/favicon.ico", NewMiddlewareChain(StaticHandler(FILE_ASSETS+"/assets/logo/"), middlewares, a)).Methods("GET")
r.HandleFunc("/sw_cache.js", NewMiddlewareChain(StaticHandler(FILE_ASSETS+"/assets/worker/"), middlewares, a)).Methods("GET")
r.PathPrefix("/assets").Handler(http.HandlerFunc(NewMiddlewareChain(StaticHandler("/"), middlewares, a))).Methods("GET")
r.HandleFunc("/favicon.ico", NewMiddlewareChain(StaticHandler("/assets/logo/"), middlewares, a)).Methods("GET")
r.HandleFunc("/sw_cache.js", NewMiddlewareChain(StaticHandler("/assets/worker/"), middlewares, a)).Methods("GET")
// Other endpoints
middlewares = []Middleware{ApiHeaders}
@ -118,8 +118,8 @@ func Init(a App) {
}
initPluginsRoutes(r, &a)
r.PathPrefix("/admin").Handler(http.HandlerFunc(NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, a))).Methods("GET")
r.PathPrefix("/").Handler(http.HandlerFunc(NewMiddlewareChain(IndexHandler(FILE_INDEX), middlewares, a))).Methods("GET", "POST")
r.PathPrefix("/admin").Handler(http.HandlerFunc(NewMiddlewareChain(IndexHandler, middlewares, a))).Methods("GET")
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
// 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"),
},
output: {
path: path.join(__dirname, "dist", "data", "public"),
path: path.join(__dirname, "server", "ctrl", "static", "www"),
publicPath: "/",
filename: "assets/js/[name]_[chunkhash].js",
chunkFilename: "assets/js/chunk_[name]_[id]_[chunkhash].js",