123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- //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)
- }
|