mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-29 00:55:51 +08:00
feature (embed): embed frontend assets from the binary
This commit is contained in:
@ -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
1
.gitignore
vendored
@ -18,3 +18,4 @@ package-lock.json
|
||||
.tern-project.js
|
||||
*_test.go
|
||||
cover.*
|
||||
www
|
||||
@ -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/"
|
||||
|
||||
@ -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"
|
||||
)
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
Reference in New Issue
Block a user