package config // functions to import and export the protobuf // data to and from config files import ( "errors" "os" "path/filepath" "strings" "go.wit.com/lib/protobuf/filepb" "go.wit.com/log" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/encoding/prototext" "google.golang.org/protobuf/proto" ) /* // loads a file from ~/.config// func Load(argname string) ([]byte, string) { } */ var ErrEmpty error = log.Errorf("config file was empty") var ErrMarshal error = log.Errorf("protobuf parse error") // returns: // - Full path to the config file. usually: ~/.config/ // - []byte : the contents of the file // - error on read func ConfigLoad(pb proto.Message, argname string, protoname string) error { var fullname string var err error homeDir, err := os.UserHomeDir() if err != nil { log.Printf("ConfigLoad() UserHomeDir() err=%v\n", err) return err } fullname = filepath.Join(homeDir, ".config", argname, protoname+".text") SetFilename(pb, fullname) // if both don't exist or both are empty, return known errors // these can be used to detect if the user is new to the application if err := missingConfig(fullname); err != nil { if errors.Is(err, os.ErrNotExist) { return os.ErrNotExist } if errors.Is(err, ErrEmpty) { return ErrEmpty } log.Info("ConfigLoad() error", fullname, err) return err } if err = loadTEXT(pb, fullname); err == nil { return nil } else { if strings.HasSuffix(fullname, ".text") { fulljson := fullname + ".json" if err := loadJSON(pb, fulljson); err == nil { return nil } else { log.Info("Config file load failed:", fulljson, err) } } } log.Info("Config file load failed:", fullname, err) return ErrMarshal } func Load(pb proto.Message) error { fullname, err := GetFilename(pb) if err != nil { log.Info("'Filename' is not in: =", fullname, err) return err } // text is supposed to be "easy". Don't verify 'version' if strings.HasSuffix(fullname, ".text") { return loadTEXT(pb, fullname) } // verify 'version' for .pb files // application will panic if they don't match var worked bool ver, err := GetString(pb, "version") if err != nil { log.Info("'Version' is not in: =", fullname, err) return err } // maybe don't really verify .json files (?) // doing it for now anyway. maybe just return an error if strings.HasSuffix(fullname, ".json") { if err := loadJSON(pb, fullname); err != nil { return err } worked = true } if strings.HasSuffix(fullname, ".pb") { if err := loadPB(pb, fullname); err != nil { pbuuid, pbver, pberr := filepb.IdentifyPB(fullname) if pberr != nil { return pberr } log.Info("your version :", ver) log.Info("pb version is:", pbver) log.Info("pb uuid is:", pbuuid) return err } worked = true } if !worked { return log.Errorf("unknown filetype %s", fullname) } newver, _ := GetString(pb, "version") if ver != newver { log.Printf("VERSION '%s' != '%s'\n", ver, newver) log.Info("Your protobuf file is old and can not be loaded") log.Info("You must delete or convert the file", fullname) // probably should ALWAYS PANIC HERE panic("protobuf version mismatch") } return nil } func LoadFile(pb proto.Message, fullname string) error { if strings.HasSuffix(fullname, ".text") { return loadTEXT(pb, fullname) } if strings.HasSuffix(fullname, ".json") { return loadJSON(pb, fullname) } if strings.HasSuffix(fullname, ".pb") { return loadPB(pb, fullname) } return log.Errorf("unknown filetype %s", fullname) } func loadPB(pb proto.Message, fullname string) error { data, err := loadFile(fullname) if err != nil { log.Warn("LoadPB()", fullname, err) // set pb.Filename that was attempted return err } if err = proto.Unmarshal(data, pb); err != nil { log.Warn("LoadPB() error Unmarshal() ", fullname, err) return err } return nil } func LoadPB(pb proto.Message, argname string, protoname string) (string, error) { var fullname string if strings.HasPrefix(argname, "/") { fullname = filepath.Join(argname, protoname+".pb") } else { homeDir, err := os.UserHomeDir() if err != nil { log.Warn("ConfigLoad() UserHomeDir() err", err) return "", err } fullname = filepath.Join(homeDir, ".config", argname, protoname+".pb") } data, err := loadFile(fullname) if err != nil { log.Warn("LoadPB()", fullname, err) // set pb.Filename that was attempted return fullname, err } // Unmarshal() if err = proto.Unmarshal(data, pb); err != nil { log.Warn("LoadPB() file", fullname) log.Warn("LoadPB() Unmarshal() err", err) return fullname, err } return fullname, nil } func loadTEXT(pb proto.Message, fullname string) error { var data []byte var err error SetFilename(pb, fullname) if data, err = loadFile(fullname); err != nil { log.Warn("config file failed to load", err) // set pb.Filename that was attempted return err } // don't even bother with Marshal() if data == nil { log.Warn("ConfigLoad() config file was empty", fullname) return ErrEmpty // file is empty } // Unmarshal() if err = prototext.Unmarshal(data, pb); err != nil { log.Warn("ConfigLoad() file", fullname) log.Warn("ConfigLoad() Unmarshal() err", err) return ErrMarshal } if fn, err := GetFilename(pb); err != nil { if fn != fullname { log.Info("config.ConfigLoad() new filename:", fullname) SetFilename(pb, fullname) } } if os.Getenv("CONFIG_VERBOSE") == "true" { log.Infof("ConfigLoad() %s len()=%d\n", fullname, len(data)) } return nil } // json files are backup Marshal() data in case .text Unmarshal() fails // they always should have the ".text" filename in them func loadJSON(pb proto.Message, fullname string) error { var data []byte var err error if data, err = loadFile(fullname); err != nil { log.Warn("config file failed to load", err) return err } // don't even bother with Marshal() if data == nil { log.Warn("ConfigLoad() config file was empty", fullname) return ErrEmpty // file is empty } // Unmarshal() if err = protojson.Unmarshal(data, pb); err != nil { log.Warn("ConfigLoad() file", fullname) log.Warn("ConfigLoad() Unmarshal() err", err) return ErrMarshal } if fn, err := GetFilename(pb); err != nil { if fn != fullname { log.Info("config.ConfigLoad() new filename:", fullname) SetFilename(pb, fullname) } } if os.Getenv("CONFIG_VERBOSE") == "true" { log.Infof("ConfigLoad() %s len()=%d\n", fullname, len(data)) } return nil } // dumb but simple to read logic func missingConfig(fullname string) error { data1, err1 := os.ReadFile(fullname) if !errors.Is(err1, os.ErrNotExist) { return err1 } data2, err2 := os.ReadFile(fullname + ".json") if !errors.Is(err2, os.ErrNotExist) { return err2 } if errors.Is(err1, os.ErrNotExist) && errors.Is(err2, os.ErrNotExist) { return os.ErrNotExist } if (len(data1) == 0) && (len(data2) == 0) { return ErrEmpty } return nil } func loadFile(fullname string) ([]byte, error) { data, err := os.ReadFile(fullname) if errors.Is(err, os.ErrNotExist) { // if file does not exist, just return nil. this return nil, err } if err != nil { // log.Info("open config file :", err) return nil, err } if len(data) == 0 { return data, ErrEmpty } return data, nil }