package config import ( "fmt" "os" "path/filepath" webassets "ymhut-box/server/unified-management/web" ) const defaultUpdateInfoJSON = `{ "app_version": "0.0.0", "download_url": "", "update_notes": {}, "last_update_notes": {}, "release_notes": "", "release_notes_md": "", "last_updated": "" } ` const defaultMediaTypesJSON = `{ "layout_version": "1.0.0", "last_updated": "", "ui_config": {}, "categories": [] } ` type Check struct { Name string `json:"name"` Status string `json:"status"` Path string `json:"path,omitempty"` Message string `json:"message,omitempty"` } func Preflight(cfg *Config) []Check { checks := []Check{ checkDir("storage", cfg.StorageDir, true), checkParent("sqlite", cfg.Database.SQLitePath), checkDir("update public", cfg.UpdatePublicDir, true), checkDir("update notice", cfg.UpdateNoticeDir, true), checkDir("downloads", cfg.DownloadsDir, true), checkSeedFile("legacy update-info", filepath.Join(cfg.UpdatePublicDir, "update-info.json"), []byte(defaultUpdateInfoJSON)), checkSeedFile("legacy media-types", filepath.Join(cfg.UpdatePublicDir, "media-types.json"), []byte(defaultMediaTypesJSON)), checkNoticeIndex("version notice index", filepath.Join(cfg.UpdateNoticeDir, "total.json")), checkWebBuild("admin web dist", cfg.AdminWebDir, "admin/dist"), checkWebBuild("portal web dist", cfg.PortalWebDir, "portal/dist"), checkWebBuild("setup web dist", cfg.SetupWebDir, "setup/dist"), } return checks } func checkDir(name, path string, create bool) Check { if create { if err := os.MkdirAll(path, 0o750); err != nil { return Check{Name: name, Status: "error", Path: path, Message: err.Error()} } } info, err := os.Stat(path) if err != nil { return Check{Name: name, Status: "missing", Path: path, Message: "directory not found"} } if !info.IsDir() { return Check{Name: name, Status: "error", Path: path, Message: "path is not a directory"} } return Check{Name: name, Status: "ok", Path: path} } func checkNoticeIndex(name, path string) Check { if _, err := os.Stat(path); err == nil { return Check{Name: name, Status: "ok", Path: path} } else if !os.IsNotExist(err) { return Check{Name: name, Status: "error", Path: path, Message: err.Error()} } if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { return Check{Name: name, Status: "error", Path: path, Message: err.Error()} } data := []byte("{\n \"schema_version\": 1,\n \"product\": \"YMhut Box\",\n \"versions\": []\n}\n") if err := os.WriteFile(path, data, 0o640); err != nil { return Check{Name: name, Status: "error", Path: path, Message: err.Error()} } return Check{Name: name, Status: "ok", Path: path, Message: "created empty notice index"} } func checkSeedFile(name, path string, data []byte) Check { if _, err := os.Stat(path); err == nil { return Check{Name: name, Status: "ok", Path: path} } else if !os.IsNotExist(err) { return Check{Name: name, Status: "error", Path: path, Message: err.Error()} } if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { return Check{Name: name, Status: "error", Path: path, Message: err.Error()} } if err := os.WriteFile(path, data, 0o640); err != nil { return Check{Name: name, Status: "error", Path: path, Message: err.Error()} } return Check{Name: name, Status: "ok", Path: path, Message: "created default compatibility JSON"} } func checkParent(name, path string) Check { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0o750); err != nil { return Check{Name: name, Status: "error", Path: path, Message: err.Error()} } return Check{Name: name, Status: "ok", Path: path} } func checkFile(name, path string, required bool) Check { info, err := os.Stat(path) if err != nil { status := "missing" if required { status = "error" } return Check{Name: name, Status: status, Path: path, Message: "file not found"} } if info.IsDir() { return Check{Name: name, Status: "error", Path: path, Message: "path is a directory"} } return Check{Name: name, Status: "ok", Path: path} } func checkWebBuild(name, path, embedRoot string) Check { dir := checkDir(name, path, false) if dir.Status != "ok" { if embeddedWebBuildOK(embedRoot) { return Check{Name: name, Status: "ok", Path: path, Message: "using embedded frontend assets"} } dir.Message = "frontend dist missing; run npm install && npm run build, or build release with -tags embed_web" return dir } index := filepath.Join(path, "index.html") if file := checkFile(name+" index", index, true); file.Status != "ok" { if embeddedWebBuildOK(embedRoot) { return Check{Name: name, Status: "ok", Path: path, Message: "disk index missing; using embedded frontend assets"} } return Check{Name: name, Status: "missing", Path: index, Message: "index.html missing; run npm run build"} } assets := filepath.Join(path, "assets") if assetDir := checkDir(name+" assets", assets, false); assetDir.Status != "ok" { if embeddedWebBuildOK(embedRoot) { return Check{Name: name, Status: "ok", Path: path, Message: "disk assets missing; using embedded frontend assets"} } return Check{Name: name, Status: "missing", Path: assets, Message: "assets directory missing; run npm run build"} } return Check{Name: name, Status: "ok", Path: path} } func embeddedWebBuildOK(embedRoot string) bool { if !webassets.Embedded { return false } if _, err := webassets.ReadFile(filepath.ToSlash(filepath.Join(embedRoot, "index.html"))); err != nil { return false } entries, err := webassets.ReadDir(filepath.ToSlash(filepath.Join(embedRoot, "assets"))) if err != nil { return false } for _, entry := range entries { if !entry.IsDir() { return true } } return false } func FormatPreflight(cfg *Config, checks []Check) []string { lines := make([]string, 0, len(checks)) for _, check := range checks { line := fmt.Sprintf("[%s] %s", check.Status, check.Name) if check.Path != "" { line += " -> " + relativePath(cfg.BaseDir, check.Path) } if check.Message != "" { line += " (" + check.Message + ")" } lines = append(lines, line) } return lines }