package adminassets import ( "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "strings" webassets "ymhut-box/server/unified-management/web" ) const ( ModeEmbedded = "embedded" ModeDisk = "disk" ) type Diagnostics struct { Mode string `json:"mode"` Source string `json:"source"` BuildID string `json:"buildId,omitempty"` CompiledBuildID string `json:"compiledBuildId,omitempty"` ManifestStatus string `json:"manifestStatus"` ManifestEntries int `json:"manifestEntries"` Ready bool `json:"ready"` ValidationError string `json:"validationError,omitempty"` } type Service struct { mode string diskRoot string embedRoot string diagnostics Diagnostics } type manifestEntry struct { File string `json:"file"` CSS []string `json:"css"` Assets []string `json:"assets"` Imports []string `json:"imports"` DynamicImports []string `json:"dynamicImports"` } type buildMetadata struct { BuildID string `json:"buildId"` } type fileReader func(string) ([]byte, error) func New(mode, diskRoot, compiledBuildID string) *Service { mode = strings.ToLower(strings.TrimSpace(mode)) if mode != ModeDisk && mode != ModeEmbedded { mode = DefaultMode() } service := &Service{ mode: mode, diskRoot: diskRoot, embedRoot: "admin/dist", } service.diagnostics = service.validate(compiledBuildID) return service } func DefaultMode() string { if webassets.Embedded { return ModeEmbedded } return ModeDisk } func (s *Service) Mode() string { return s.mode } func (s *Service) Diagnostics() Diagnostics { return s.diagnostics } func (s *Service) ReadFile(name string) ([]byte, error) { name, err := cleanAssetPath(name) if err != nil { return nil, err } if !s.diagnostics.Ready { return nil, errors.New("admin asset source failed validation") } if s.mode == ModeEmbedded { return webassets.ReadFile(s.embedRoot + "/" + name) } return os.ReadFile(filepath.Join(s.diskRoot, filepath.FromSlash(name))) } func ValidateDisk(root, compiledBuildID string) Diagnostics { return validateSource(ModeDisk, root, compiledBuildID, func(name string) ([]byte, error) { return os.ReadFile(filepath.Join(root, filepath.FromSlash(name))) }) } func ValidateEmbedded(compiledBuildID string) Diagnostics { const root = "admin/dist" return validateSource(ModeEmbedded, root, compiledBuildID, func(name string) ([]byte, error) { return webassets.ReadFile(root + "/" + name) }) } func (s *Service) validate(compiledBuildID string) Diagnostics { if s.mode == ModeEmbedded { if !webassets.Embedded { return Diagnostics{ Mode: s.mode, Source: s.embedRoot, CompiledBuildID: compiledBuildID, ManifestStatus: "unavailable", ValidationError: "binary was built without embed_web", } } return ValidateEmbedded(compiledBuildID) } return ValidateDisk(s.diskRoot, compiledBuildID) } func validateSource(mode, source, compiledBuildID string, read fileReader) Diagnostics { result := Diagnostics{ Mode: mode, Source: source, CompiledBuildID: strings.TrimSpace(compiledBuildID), ManifestStatus: "invalid", } fail := func(err error) Diagnostics { result.ValidationError = err.Error() return result } if _, err := read("index.html"); err != nil { return fail(fmt.Errorf("read index.html: %w", err)) } metadataBytes, err := read("admin-build.json") if err != nil { return fail(fmt.Errorf("read admin-build.json: %w", err)) } var metadata buildMetadata if err := json.Unmarshal(metadataBytes, &metadata); err != nil { return fail(fmt.Errorf("parse admin-build.json: %w", err)) } result.BuildID = strings.TrimSpace(metadata.BuildID) if result.BuildID == "" { return fail(errors.New("admin-build.json has an empty buildId")) } if result.CompiledBuildID != "" && result.CompiledBuildID != "dev" && result.BuildID != result.CompiledBuildID { return fail(fmt.Errorf("admin build ID %q does not match binary build ID %q", result.BuildID, result.CompiledBuildID)) } manifestBytes, err := read("asset-manifest.json") if err != nil { return fail(fmt.Errorf("read asset-manifest.json: %w", err)) } manifest := map[string]manifestEntry{} if err := json.Unmarshal(manifestBytes, &manifest); err != nil { return fail(fmt.Errorf("parse asset-manifest.json: %w", err)) } if len(manifest) == 0 { return fail(errors.New("asset-manifest.json is empty")) } for key, entry := range manifest { if strings.TrimSpace(entry.File) == "" { return fail(fmt.Errorf("manifest entry %q has no output file", key)) } for _, dependencyKey := range append(append([]string{}, entry.Imports...), entry.DynamicImports...) { if _, ok := manifest[dependencyKey]; !ok { return fail(fmt.Errorf("manifest entry %q references missing entry %q", key, dependencyKey)) } } files := append([]string{entry.File}, entry.CSS...) files = append(files, entry.Assets...) for _, name := range files { name, cleanErr := cleanAssetPath(name) if cleanErr != nil { return fail(fmt.Errorf("manifest entry %q: %w", key, cleanErr)) } data, readErr := read(name) if readErr != nil { return fail(fmt.Errorf("manifest entry %q is missing %s: %w", key, name, readErr)) } if len(data) == 0 { return fail(fmt.Errorf("manifest entry %q references empty file %s", key, name)) } } } result.ManifestEntries = len(manifest) result.ManifestStatus = "valid" result.Ready = true return result } func cleanAssetPath(name string) (string, error) { name = filepath.ToSlash(strings.TrimSpace(name)) name = strings.TrimPrefix(name, "./") if name == "" || strings.HasPrefix(name, "/") || strings.Contains(name, "\\") { return "", fmt.Errorf("invalid admin asset path %q", name) } cleaned := filepath.ToSlash(filepath.Clean(name)) if cleaned == "." || cleaned == ".." || strings.HasPrefix(cleaned, "../") { return "", fmt.Errorf("invalid admin asset path %q", name) } return cleaned, nil } func IsMissing(err error) bool { return errors.Is(err, fs.ErrNotExist) }