// Package config loads the file-system roots that the application exposes. package config import ( "encoding/json" "fmt" "os" "path/filepath" "strings" ) const ( rootsEnvVar = "WEB_FILE_BROWSER_ROOTS" rootsFileEnvVar = "WEB_FILE_BROWSER_ROOTS_FILE" ) // Root is a named, absolute path inside the application container. type Root struct { Name string `json:"name"` Path string `json:"path"` } // Config contains all configured browse roots. type Config struct { Roots []Root } type fileConfig struct { Roots []Root `json:"roots"` } // Load reads root configuration from WEB_FILE_BROWSER_ROOTS, when set, or // from the JSON file named by WEB_FILE_BROWSER_ROOTS_FILE. The environment // value is a JSON array of roots and has precedence over the file. func Load() (Config, error) { if rootsJSON := strings.TrimSpace(os.Getenv(rootsEnvVar)); rootsJSON != "" { roots, err := decodeRoots([]byte(rootsJSON)) if err != nil { return Config{}, fmt.Errorf("parse %s: %w", rootsEnvVar, err) } return Config{Roots: roots}, nil } configPath := strings.TrimSpace(os.Getenv(rootsFileEnvVar)) if configPath == "" { return Config{}, fmt.Errorf("set %s or %s", rootsEnvVar, rootsFileEnvVar) } contents, err := os.ReadFile(configPath) if err != nil { return Config{}, fmt.Errorf("read %s: %w", configPath, err) } var fromFile fileConfig if err := json.Unmarshal(contents, &fromFile); err != nil { return Config{}, fmt.Errorf("parse %s: %w", configPath, err) } if err := validateRoots(fromFile.Roots); err != nil { return Config{}, err } return Config{Roots: fromFile.Roots}, nil } func decodeRoots(contents []byte) ([]Root, error) { var roots []Root if err := json.Unmarshal(contents, &roots); err != nil { return nil, err } if err := validateRoots(roots); err != nil { return nil, err } return roots, nil } func validateRoots(roots []Root) error { if len(roots) == 0 { return fmt.Errorf("at least one root is required") } names := make(map[string]struct{}, len(roots)) for index := range roots { roots[index].Name = strings.TrimSpace(roots[index].Name) roots[index].Path = strings.TrimSpace(roots[index].Path) if roots[index].Name == "" { return fmt.Errorf("root %d has an empty name", index+1) } if !filepath.IsAbs(roots[index].Path) { return fmt.Errorf("root %q path must be absolute", roots[index].Name) } nameKey := strings.ToLower(roots[index].Name) if _, exists := names[nameKey]; exists { return fmt.Errorf("root name %q is duplicated", roots[index].Name) } names[nameKey] = struct{}{} } return nil }