mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-29 17:18:43 +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
|
- 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
1
.gitignore
vendored
@ -18,3 +18,4 @@ package-lock.json
|
|||||||
.tern-project.js
|
.tern-project.js
|
||||||
*_test.go
|
*_test.go
|
||||||
cover.*
|
cover.*
|
||||||
|
www
|
||||||
@ -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/"
|
||||||
|
|||||||
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user