Files
nginx-ui/app/app.go
2025-08-20 10:23:56 +08:00

164 lines
3.2 KiB
Go

//go:build !unembed
package app
import (
"archive/tar"
"bytes"
"embed"
_ "embed"
"io"
"io/fs"
"net/http"
"path/filepath"
"strings"
"github.com/spf13/afero"
"github.com/ulikunitz/xz"
)
//go:embed dist.tar.xz
var compressedDist []byte
//go:embed i18n.json
var i18nJSON []byte
//go:embed src/language/* src/language/*/*
var languageFS embed.FS
var (
DistFS afero.Fs
initErr error
)
func init() {
DistFS, initErr = initDistFS()
}
// GetDistFS returns the initialized memory filesystem with decompressed frontend assets
func GetDistFS() (afero.Fs, error) {
return DistFS, initErr
}
// initDistFS initializes the memory filesystem by decompressing the embedded assets
func initDistFS() (afero.Fs, error) {
memFS := afero.NewMemMapFs()
// Extract compressed dist archive
if err := extractDistArchive(memFS); err != nil {
return nil, err
}
// Copy i18n.json
if err := afero.WriteFile(memFS, "i18n.json", i18nJSON, 0644); err != nil {
return nil, err
}
// Copy language files from embed.FS to memory filesystem
if err := copyLanguageFiles(memFS); err != nil {
return nil, err
}
return memFS, nil
}
// extractDistArchive decompresses and extracts the dist.tar.xz archive
func extractDistArchive(memFS afero.Fs) error {
if len(compressedDist) == 0 {
return nil
}
xzReader, err := xz.NewReader(bytes.NewReader(compressedDist))
if err != nil {
return err
}
tarReader := tar.NewReader(xzReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
// Sanitize the file path to prevent directory traversal
cleanPath := filepath.Clean(header.Name)
// Ensure the path doesn't escape the target directory
if strings.Contains(cleanPath, "..") || filepath.IsAbs(cleanPath) {
// Skip entries with suspicious paths
continue
}
switch header.Typeflag {
case tar.TypeDir:
if err := memFS.MkdirAll(cleanPath, 0755); err != nil {
return err
}
case tar.TypeReg:
dir := filepath.Dir(cleanPath)
if dir != "." {
if err := memFS.MkdirAll(dir, 0755); err != nil {
return err
}
}
file, err := memFS.Create(cleanPath)
if err != nil {
return err
}
if _, err := io.Copy(file, tarReader); err != nil {
file.Close()
return err
}
file.Close()
}
}
return nil
}
// copyLanguageFiles copies language files from embed.FS to memory filesystem
func copyLanguageFiles(memFS afero.Fs) error {
return fs.WalkDir(languageFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return memFS.MkdirAll(path, 0755)
}
data, err := languageFS.ReadFile(path)
if err != nil {
return err
}
return afero.WriteFile(memFS, path, data, 0644)
})
}
// HTTPFileSystem returns an http.FileSystem that serves from the memory filesystem
func HTTPFileSystem() (http.FileSystem, error) {
fs, err := GetDistFS()
if err != nil {
return nil, err
}
return afero.NewHttpFs(fs), nil
}
// Open opens a file from the memory filesystem
func Open(name string) (afero.File, error) {
fs, err := GetDistFS()
if err != nil {
return nil, err
}
name = strings.TrimPrefix(name, "/")
return fs.Open(name)
}