1
0

app.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. //go:build !unembed
  2. package app
  3. import (
  4. "archive/tar"
  5. "bytes"
  6. "embed"
  7. _ "embed"
  8. "io"
  9. "io/fs"
  10. "net/http"
  11. "path/filepath"
  12. "strings"
  13. "github.com/spf13/afero"
  14. "github.com/ulikunitz/xz"
  15. )
  16. //go:embed dist.tar.xz
  17. var compressedDist []byte
  18. //go:embed i18n.json
  19. var i18nJSON []byte
  20. //go:embed src/language/* src/language/*/*
  21. var languageFS embed.FS
  22. var (
  23. DistFS afero.Fs
  24. initErr error
  25. )
  26. func init() {
  27. DistFS, initErr = initDistFS()
  28. }
  29. // GetDistFS returns the initialized memory filesystem with decompressed frontend assets
  30. func GetDistFS() (afero.Fs, error) {
  31. return DistFS, initErr
  32. }
  33. // initDistFS initializes the memory filesystem by decompressing the embedded assets
  34. func initDistFS() (afero.Fs, error) {
  35. memFS := afero.NewMemMapFs()
  36. // Extract compressed dist archive
  37. if err := extractDistArchive(memFS); err != nil {
  38. return nil, err
  39. }
  40. // Copy i18n.json
  41. if err := afero.WriteFile(memFS, "i18n.json", i18nJSON, 0644); err != nil {
  42. return nil, err
  43. }
  44. // Copy language files from embed.FS to memory filesystem
  45. if err := copyLanguageFiles(memFS); err != nil {
  46. return nil, err
  47. }
  48. return memFS, nil
  49. }
  50. // extractDistArchive decompresses and extracts the dist.tar.xz archive
  51. func extractDistArchive(memFS afero.Fs) error {
  52. if len(compressedDist) == 0 {
  53. return nil
  54. }
  55. xzReader, err := xz.NewReader(bytes.NewReader(compressedDist))
  56. if err != nil {
  57. return err
  58. }
  59. tarReader := tar.NewReader(xzReader)
  60. for {
  61. header, err := tarReader.Next()
  62. if err == io.EOF {
  63. break
  64. }
  65. if err != nil {
  66. return err
  67. }
  68. // Sanitize the file path to prevent directory traversal
  69. cleanPath := filepath.Clean(header.Name)
  70. // Ensure the path doesn't escape the target directory
  71. if strings.Contains(cleanPath, "..") || filepath.IsAbs(cleanPath) {
  72. // Skip entries with suspicious paths
  73. continue
  74. }
  75. switch header.Typeflag {
  76. case tar.TypeDir:
  77. if err := memFS.MkdirAll(cleanPath, 0755); err != nil {
  78. return err
  79. }
  80. case tar.TypeReg:
  81. dir := filepath.Dir(cleanPath)
  82. if dir != "." {
  83. if err := memFS.MkdirAll(dir, 0755); err != nil {
  84. return err
  85. }
  86. }
  87. file, err := memFS.Create(cleanPath)
  88. if err != nil {
  89. return err
  90. }
  91. if _, err := io.Copy(file, tarReader); err != nil {
  92. file.Close()
  93. return err
  94. }
  95. file.Close()
  96. }
  97. }
  98. return nil
  99. }
  100. // copyLanguageFiles copies language files from embed.FS to memory filesystem
  101. func copyLanguageFiles(memFS afero.Fs) error {
  102. return fs.WalkDir(languageFS, ".", func(path string, d fs.DirEntry, err error) error {
  103. if err != nil {
  104. return err
  105. }
  106. if d.IsDir() {
  107. return memFS.MkdirAll(path, 0755)
  108. }
  109. data, err := languageFS.ReadFile(path)
  110. if err != nil {
  111. return err
  112. }
  113. return afero.WriteFile(memFS, path, data, 0644)
  114. })
  115. }
  116. // HTTPFileSystem returns an http.FileSystem that serves from the memory filesystem
  117. func HTTPFileSystem() (http.FileSystem, error) {
  118. fs, err := GetDistFS()
  119. if err != nil {
  120. return nil, err
  121. }
  122. return afero.NewHttpFs(fs), nil
  123. }
  124. // Open opens a file from the memory filesystem
  125. func Open(name string) (afero.File, error) {
  126. fs, err := GetDistFS()
  127. if err != nil {
  128. return nil, err
  129. }
  130. name = strings.TrimPrefix(name, "/")
  131. return fs.Open(name)
  132. }