From 81de7d271ef95dd53feedc0bdc5784d7ff746fea Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sun, 24 Jul 2022 20:02:37 +0200 Subject: [PATCH] add support for embedding templates and other static resources This feature is disabled by default and can be enabled using the "bundle" build tag Fixes #823 Signed-off-by: Nicola Murino --- docs/build-from-source.md | 3 +- internal/bundle/bundle.go | 65 ++++++++++++++++++++++ internal/httpd/resources.go | 28 ++++++++++ internal/httpd/resources_embedded.go | 33 ++++++++++++ internal/httpd/server.go | 4 +- internal/util/resources.go | 81 ++++++++++++++++++++++++++++ internal/util/resources_embedded.go | 57 ++++++++++++++++++++ internal/util/util.go | 55 ------------------- 8 files changed, 268 insertions(+), 58 deletions(-) create mode 100644 internal/bundle/bundle.go create mode 100644 internal/httpd/resources.go create mode 100644 internal/httpd/resources_embedded.go create mode 100644 internal/util/resources.go create mode 100644 internal/util/resources_embedded.go diff --git a/docs/build-from-source.md b/docs/build-from-source.md index 218ad0ed..ee70c1f5 100644 --- a/docs/build-from-source.md +++ b/docs/build-from-source.md @@ -13,6 +13,7 @@ The following build tags are available: - `nosqlite`, disable SQLite data provider, default enabled - `noportable`, disable portable mode, default enabled - `nometrics`, disable Prometheus metrics, default enabled +- `bundle`, embed static files and templates. Before building with this tag enabled you have to copy `openapi`, `static` and `templates` dirs to `internal/bundle` directory. Default disabled If no build tag is specified the build will include the default features. @@ -36,5 +37,5 @@ You should get a version that includes git commit, build date and available feat ```bash $ ./sftpgo -v -SFTPGo 0.9.6-dev-b30614e-dirty-2020-06-19T11:04:56Z +metrics -gcs -s3 +bolt +mysql +pgsql -sqlite +portable +SFTPGo 2.3.1-dev-c8158e1-2022-07-24T17:25:45Z +metrics +azblob +gcs +s3 +bolt +mysql +pgsql +sqlite +portable ``` diff --git a/internal/bundle/bundle.go b/internal/bundle/bundle.go new file mode 100644 index 00000000..627160e6 --- /dev/null +++ b/internal/bundle/bundle.go @@ -0,0 +1,65 @@ +// Copyright (C) 2019-2022 Nicola Murino +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//go:build bundle +// +build bundle + +package bundle + +import ( + "embed" + "fmt" + "io/fs" + "net/http" + + "github.com/drakkan/sftpgo/v2/internal/version" +) + +func init() { + version.AddFeature("+bundle") +} + +//go:embed templates/* +var templatesFs embed.FS + +//go:embed static/* +var staticFs embed.FS + +//go:embed openapi/* +var openapiFs embed.FS + +// GetTemplatesFs returns the embedded filesystem with the SFTPGo templates +func GetTemplatesFs() embed.FS { + return templatesFs +} + +// GetStaticFs return the http Filesystem with the embedded static files +func GetStaticFs() http.FileSystem { + fsys, err := fs.Sub(staticFs, "static") + if err != nil { + err = fmt.Errorf("unable to get embedded filesystem for static files: %w", err) + panic(err) + } + return http.FS(fsys) +} + +// GetOpenAPIFs return the http Filesystem with the embedded static files +func GetOpenAPIFs() http.FileSystem { + fsys, err := fs.Sub(openapiFs, "openapi") + if err != nil { + err = fmt.Errorf("unable to get embedded filesystem for OpenAPI files: %w", err) + panic(err) + } + return http.FS(fsys) +} diff --git a/internal/httpd/resources.go b/internal/httpd/resources.go new file mode 100644 index 00000000..eece6004 --- /dev/null +++ b/internal/httpd/resources.go @@ -0,0 +1,28 @@ +// Copyright (C) 2019-2022 Nicola Murino +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//go:build !bundle +// +build !bundle + +package httpd + +import ( + "net/http" + + "github.com/go-chi/chi/v5" +) + +func serveStaticDir(router chi.Router, path, fsDirPath string) { + fileServer(router, path, http.Dir(fsDirPath)) +} diff --git a/internal/httpd/resources_embedded.go b/internal/httpd/resources_embedded.go new file mode 100644 index 00000000..380d5c81 --- /dev/null +++ b/internal/httpd/resources_embedded.go @@ -0,0 +1,33 @@ +// Copyright (C) 2019-2022 Nicola Murino +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//go:build bundle +// +build bundle + +package httpd + +import ( + "github.com/go-chi/chi/v5" + + "github.com/drakkan/sftpgo/v2/internal/bundle" +) + +func serveStaticDir(router chi.Router, path, _ string) { + switch path { + case webStaticFilesPath: + fileServer(router, path, bundle.GetStaticFs()) + case webOpenAPIPath: + fileServer(router, path, bundle.GetOpenAPIFs()) + } +} diff --git a/internal/httpd/server.go b/internal/httpd/server.go index 102e81a2..f1345e23 100644 --- a/internal/httpd/server.go +++ b/internal/httpd/server.go @@ -1352,14 +1352,14 @@ func (s *httpdServer) initializeRouter() { if s.renderOpenAPI { s.router.Group(func(router chi.Router) { router.Use(compressor.Handler) - fileServer(router, webOpenAPIPath, http.Dir(s.openAPIPath)) + serveStaticDir(router, webOpenAPIPath, s.openAPIPath) }) } if s.enableWebAdmin || s.enableWebClient { s.router.Group(func(router chi.Router) { router.Use(compressor.Handler) - fileServer(router, webStaticFilesPath, http.Dir(s.staticFilesPath)) + serveStaticDir(router, webStaticFilesPath, s.staticFilesPath) }) if s.binding.OIDC.isEnabled() { s.router.Get(webOIDCRedirectPath, s.handleOIDCRedirect) diff --git a/internal/util/resources.go b/internal/util/resources.go new file mode 100644 index 00000000..ee8b0719 --- /dev/null +++ b/internal/util/resources.go @@ -0,0 +1,81 @@ +// Copyright (C) 2019-2022 Nicola Murino +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//go:build !bundle +// +build !bundle + +package util + +import ( + "html/template" + "os" + "path/filepath" + "runtime" + + "github.com/drakkan/sftpgo/v2/internal/logger" +) + +// FindSharedDataPath searches for the specified directory name in searchDir +// and in system-wide shared data directories. +// If name is an absolute path it is returned unmodified. +func FindSharedDataPath(name, searchDir string) string { + if !IsFileInputValid(name) { + return "" + } + if name != "" && !filepath.IsAbs(name) { + searchList := []string{searchDir} + if additionalSharedDataSearchPath != "" { + searchList = append(searchList, additionalSharedDataSearchPath) + } + if runtime.GOOS != osWindows { + searchList = append(searchList, "/usr/share/sftpgo") + searchList = append(searchList, "/usr/local/share/sftpgo") + } + searchList = RemoveDuplicates(searchList, false) + for _, basePath := range searchList { + res := filepath.Join(basePath, name) + _, err := os.Stat(res) + if err == nil { + logger.Debug(logSender, "", "found share data path for name %#v: %#v", name, res) + return res + } + } + return filepath.Join(searchDir, name) + } + return name +} + +// LoadTemplate parses the given template paths. +// It behaves like template.Must but it writes a log before exiting. +// You can optionally provide a base template (e.g. to define some custom functions) +func LoadTemplate(base *template.Template, paths ...string) *template.Template { + var t *template.Template + var err error + + if base != nil { + base, err = base.Clone() + if err == nil { + t, err = base.ParseFiles(paths...) + } + } else { + t, err = template.ParseFiles(paths...) + } + + if err != nil { + logger.ErrorToConsole("error loading required template: %v", err) + logger.Error(logSender, "", "error loading required template: %v", err) + panic(err) + } + return t +} diff --git a/internal/util/resources_embedded.go b/internal/util/resources_embedded.go new file mode 100644 index 00000000..f77dfb5c --- /dev/null +++ b/internal/util/resources_embedded.go @@ -0,0 +1,57 @@ +// Copyright (C) 2019-2022 Nicola Murino +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//go:build bundle +// +build bundle + +package util + +import ( + "html/template" + + "github.com/drakkan/sftpgo/v2/internal/bundle" + "github.com/drakkan/sftpgo/v2/internal/logger" +) + +// FindSharedDataPath searches for the specified directory name in searchDir +// and in system-wide shared data directories. +// If name is an absolute path it is returned unmodified. +func FindSharedDataPath(name, _ string) string { + return name +} + +// LoadTemplate parses the given template paths. +// It behaves like template.Must but it writes a log before exiting. +// You can optionally provide a base template (e.g. to define some custom functions) +func LoadTemplate(base *template.Template, paths ...string) *template.Template { + var t *template.Template + var err error + + templateFs := bundle.GetTemplatesFs() + if base != nil { + base, err = base.Clone() + if err == nil { + t, err = base.ParseFS(templateFs, paths...) + } + } else { + t, err = template.ParseFS(templateFs, paths...) + } + + if err != nil { + logger.ErrorToConsole("error loading required template: %v", err) + logger.Error(logSender, "", "error loading required template: %v", err) + panic(err) + } + return t +} diff --git a/internal/util/util.go b/internal/util/util.go index 78a3b692..8a273940 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -27,7 +27,6 @@ import ( "encoding/pem" "errors" "fmt" - "html/template" "io" "io/fs" "net" @@ -335,30 +334,6 @@ func CleanPathWithBase(base, p string) string { return path.Clean(p) } -// LoadTemplate parses the given template paths. -// It behaves like template.Must but it writes a log before exiting. -// You can optionally provide a base template (e.g. to define some custom functions) -func LoadTemplate(base *template.Template, paths ...string) *template.Template { - var t *template.Template - var err error - - if base != nil { - base, err = base.Clone() - if err == nil { - t, err = base.ParseFiles(paths...) - } - } else { - t, err = template.ParseFiles(paths...) - } - - if err != nil { - logger.ErrorToConsole("error loading required template: %v", err) - logger.Error(logSender, "", "error loading required template: %v", err) - panic(err) - } - return t -} - // IsFileInputValid returns true this is a valid file name. // This method must be used before joining a file name, generally provided as // user input, with a directory @@ -370,36 +345,6 @@ func IsFileInputValid(fileInput string) bool { return true } -// FindSharedDataPath searches for the specified directory name in searchDir -// and in system-wide shared data directories. -// If name is an absolute path it is returned unmodified. -func FindSharedDataPath(name, searchDir string) string { - if !IsFileInputValid(name) { - return "" - } - if name != "" && !filepath.IsAbs(name) { - searchList := []string{searchDir} - if additionalSharedDataSearchPath != "" { - searchList = append(searchList, additionalSharedDataSearchPath) - } - if runtime.GOOS != osWindows { - searchList = append(searchList, "/usr/share/sftpgo") - searchList = append(searchList, "/usr/local/share/sftpgo") - } - searchList = RemoveDuplicates(searchList, false) - for _, basePath := range searchList { - res := filepath.Join(basePath, name) - _, err := os.Stat(res) - if err == nil { - logger.Debug(logSender, "", "found share data path for name %#v: %#v", name, res) - return res - } - } - return filepath.Join(searchDir, name) - } - return name -} - // CleanDirInput sanitizes user input for directories. // On Windows it removes any trailing `"`. // We try to help windows users that set an invalid path such as "C:\ProgramData\SFTPGO\".