package archetype import ( "fmt" "os" "path/filepath" "runtime" "strconv" "strings" ) type Config struct { Adapter Adapter // adapter for this instance Root string // root of the site data StaticRoot string // root of static files for StaticFileManager StaticShowHidden bool // whether to show hidden files in the FileManager StaticShowHTML bool // whether to show html files in the FileManager StaticMaxUploadMB int64 // max size in MB of files uploaded via FileManager AssetRoot string // root of Nirvash dist files (CSS, images) ListenAddress string // address the webserver listens on Plugins map[string]interface{} } func GetConfigLocation() string { home := os.Getenv("HOME") appdata := os.Getenv("APPDATA") switch runtime.GOOS { case "windows": return filepath.Join(appdata, "nirvash") case "darwin": return filepath.Join(home, "Library", "Application Support", "nirvash") case "plan9": return filepath.Join(home, "lib", "nirvash") default: return filepath.Join(home, ".config", "nirvash") } } func ensureConfigLocationExists() { fileInfo, err := os.Stat(GetConfigLocation()) if os.IsNotExist(err) { os.MkdirAll(GetConfigLocation(), os.ModePerm) } else if !fileInfo.IsDir() { panic("Config location is not a directory!") } } func ReadConfig() *Config { ensureConfigLocationExists() return parseConfig(filepath.Join(GetConfigLocation(), "nirvash.conf")) } func (self *Config) Write() error { ensureConfigLocationExists() return writeConfig(self, filepath.Join(GetConfigLocation(), "nirvash.conf")) } func (self *Config) SetAdapter(adapter string) { switch adapter { case "eureka": self.Adapter = &EurekaAdapter{} default: panic("Unsupported adapter! Try one of [ eureka ]") } } func (self *Config) IsNull() bool { // zero-values for StaticShowHTML, StaticShowHidden, and StaticMaxUploadMB are valid // zero-value for ListenAddress is also OK, falls back on 127.0.0.1:8080 return self.Adapter == nil || len(self.Root) == 0 || len(self.StaticRoot) == 0 || len(self.AssetRoot) == 0 } func (self *Config) RunWizard() { fmt.Printf("All options are required.\n") defer func(cfg *Config) { if r := recover(); r != nil { fmt.Printf("Invalid selection, starting over...") cfg.RunWizard() } }(self) inputBuf := "" fmt.Printf("adapter? (eureka) [eureka] ") fmt.Scanln(&inputBuf) if len(strings.TrimSpace(inputBuf)) == 0 { inputBuf = "eureka" } self.SetAdapter(inputBuf) inputBuf = "" fmt.Printf("site data root? ") ensureNonEmptyOption(&inputBuf) self.Root = inputBuf inputBuf = "" fmt.Printf("static file root? ") ensureNonEmptyOption(&inputBuf) self.StaticRoot = inputBuf inputBuf = "" fmt.Printf("show HTML files in file manager? ") self.StaticShowHTML = ensureBooleanOption(&inputBuf) inputBuf = "" fmt.Printf("show hidden files in file manager? ") self.StaticShowHidden = ensureBooleanOption(&inputBuf) inputBuf = "" fmt.Printf("max upload size (MB)? ") self.StaticMaxUploadMB = ensureNumberOption(&inputBuf) inputBuf = "" fmt.Printf("nirvash asset root? ") ensureNonEmptyOption(&inputBuf) self.AssetRoot = inputBuf inputBuf = "" fmt.Printf("nirvash listen address? ") ensureNonEmptyOption(&inputBuf) self.ListenAddress = inputBuf inputBuf = "" fmt.Printf("plugins? (not implemented yet) ") ensureNonEmptyOption(&inputBuf) //self.Plugins = processPlugins(inputBuf) fmt.Printf("Configuration complete!\n") self.Write() } func ensureNonEmptyOption(buffer *string) { for { fmt.Scanln(buffer) if len(strings.TrimSpace(*buffer)) != 0 { break } fmt.Println("Please enter a nonempty value") } } func ensureBooleanOption(buffer *string) bool { for { fmt.Scanln(buffer) trimmedBuf := strings.TrimSpace(*buffer) v, err := strconv.ParseBool(trimmedBuf) if err == nil { return v } fmt.Println("Please enter a true or false value") } } func ensureNumberOption(buffer *string) int64 { for { fmt.Scanln(buffer) trimmedBuf := strings.TrimSpace(*buffer) v, err := strconv.ParseInt(trimmedBuf, 10, 64) if err == nil && v > 0 { return v } fmt.Println("Please enter a positive integer") } } func writeConfig(cfg *Config, configFile string) error { f, err := os.Create(configFile) if err != nil { return err } defer f.Close() f.WriteString("root=" + cfg.Root + "\n") f.WriteString("staticRoot=" + cfg.StaticRoot + "\n") f.WriteString("staticShowHTML=" + strconv.FormatBool(cfg.StaticShowHTML) + "\n") f.WriteString("staticShowHidden=" + strconv.FormatBool(cfg.StaticShowHidden) + "\n") f.WriteString("staticMaxUploadMB=" + strconv.FormatInt(cfg.StaticMaxUploadMB, 10) + "\n") f.WriteString("assetRoot=" + cfg.AssetRoot + "\n") f.WriteString("listenAddress=" + cfg.ListenAddress + "\n") f.WriteString("adapter=" + cfg.Adapter.Name() + "\n") f.WriteString("plugins=\n") return nil } func parseConfig(configFile string) *Config { f, err := os.ReadFile(configFile) cfg := &Config{} if err != nil { return cfg } fileData := string(f[:]) lines := strings.Split(fileData, "\n") for _, l := range lines { if len(l) == 0 { continue } if !strings.Contains(l, "=") { panic("Malformed config not in INI format") } kvp := strings.Split(l, "=") k := strings.TrimSpace(kvp[0]) v := strings.TrimSpace(kvp[1]) switch k { case "root": cfg.Root = v case "staticRoot": cfg.StaticRoot = v case "staticShowHTML": cfg.StaticShowHTML, _ = strconv.ParseBool(v) case "staticShowHidden": cfg.StaticShowHidden, _ = strconv.ParseBool(v) case "staticMaxUploadMB": cfg.StaticMaxUploadMB, _ = strconv.ParseInt(v, 10, 64) case "assetRoot": cfg.AssetRoot = v case "listenAddress": cfg.ListenAddress = v case "plugins": // not implemented case "adapter": cfg.SetAdapter(v) default: panic("Unrecognized config option: " + k) } } return cfg }