chatpb/config.go

184 lines
5.6 KiB
Go

package chatpb
// functions to import and export the protobuf
// data to and from config files
import (
"errors"
"fmt"
"os"
"path/filepath"
"time"
"go.wit.com/lib/protobuf/bugpb"
"go.wit.com/log"
)
// write to ~/.config/gemini/ unless ENV{GEMINI_HOME} is set
func (all *Chats) ConfigSave() error {
if os.Getenv("GEMINI_HOME") == "" {
homeDir, _ := os.UserHomeDir()
fullpath := filepath.Join(homeDir, ".config/gemini")
os.Setenv("GEMINI_HOME", fullpath)
}
if all == nil {
log.Warn("chatpb all == nil")
return errors.New("chatpb.ConfigSave() all == nil")
}
// --- Start of Fix ---
// Create a new, clean Chats object to avoid marshaling a slice with nil entries.
cleanChats := NewChats() // Assuming NewChats() initializes the struct correctly.
cleanChats.Uuid = all.Uuid
cleanChats.Version = all.Version
// Loop through the original chats and append only the non-nil ones.
for _, chat := range all.GetChats() {
if chat != nil {
cleanChats.Chats = append(cleanChats.Chats, chat)
} else {
log.Warn("Found and skipped a nil chat entry during ConfigSave")
}
}
// --- End of Fix ---
data, err := cleanChats.Marshal() // Marshal the clean object, not 'all'
if err != nil {
log.Info("chatpb proto.Marshal() failed len", len(data), err)
// The tryValidate logic might be less necessary now but kept for safety.
if err := all.tryValidate(); err != nil {
return err
} else {
data, err = cleanChats.Marshal() // Retry with the clean object
if err == nil {
log.Info("chatpb.ConfigSave() pb.Marshal() worked after validation len", len(cleanChats.Chats), "chats")
configWrite("gemini.pb", data)
return nil
}
}
return err
}
// --- Backup Logic ---
filename := filepath.Join(os.Getenv("GEMINI_HOME"), "gemini.pb")
if _, err := os.Stat(filename); err == nil {
// File exists, so back it up.
dir := filepath.Dir(filename)
timestamp := time.Now().Format("20060102-150405")
backupFilename := fmt.Sprintf("gemini.%s.pb", timestamp)
backupPath := filepath.Join(dir, backupFilename)
if err := os.Rename(filename, backupPath); err != nil {
log.Warn("Could not backup config file:", err)
}
}
// --- End Backup Logic ---
if err := configWrite("gemini.pb", data); err != nil {
log.Infof("chatpb.ConfigSave() failed len(Chats)=%d bytes=%d", len(cleanChats.Chats), len(data))
return err
}
configWrite("gemini.text", []byte(cleanChats.FormatTEXT()))
log.Infof("chatpb.ConfigSave() worked len(Chats)=%d bytes=%d", len(cleanChats.Chats), len(data))
return nil
}
func (all *Chats) tryValidate() error {
err := bugpb.ValidateProtoUTF8(all)
if err != nil {
log.Printf("Protobuf UTF-8 validation failed: %v\n", err)
}
if err := bugpb.SanitizeProtoUTF8(all); err != nil {
log.Warn("Sanitation failed:", err)
// log.Fatalf("Sanitization failed: %v", err)
return err
}
return nil
}
// load the gemini.pb file. I shouldn't really matter if this
// fails. the file should be autogenerated. This is used
// locally just for speed
func (all *Chats) ConfigLoad() error {
if os.Getenv("GEMINI_HOME") == "" {
homeDir, _ := os.UserHomeDir()
fullpath := filepath.Join(homeDir, ".config/gemini")
os.Setenv("GEMINI_HOME", fullpath)
}
var data []byte
var err error
cfgname := filepath.Join(os.Getenv("GEMINI_HOME"), "gemini.pb")
if data, err = loadFile(cfgname); err != nil {
// something went wrong loading the file
// all.sampleConfig() // causes nil panic
return err
}
// this means the gemini.pb file exists and was read
if len(data) == 0 {
// todo: add default data here since it's blank? // might cause nil panic?
all.AddGeminiComment("I like astronomy")
log.Info(errors.New("chatpb.ConfigLoad() gemini.pb is empty"))
return nil
}
err = all.Unmarshal(data)
test := NewChats()
if test.Uuid != all.Uuid {
log.Warn("uuids do not match", test.Uuid, all.Uuid)
deleteProtobufFile(cfgname)
}
if test.Version != all.Version {
log.Warn("versions do not match", test.Version, all.Version)
deleteProtobufFile(cfgname)
}
log.Info(cfgname, "protobuf versions and uuid match", all.Uuid, all.Version)
return err
}
func deleteProtobufFile(filename string) {
log.Warn("The protobuf file format has changed for", filename)
log.Warn("Deleting old file:", filename)
log.Warn("This file will be recreated on the next run.")
err := os.Remove(filename)
if err != nil {
log.Warn("failed to remove old protobuf file", "err", err)
}
}
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
// will cause ConfigLoad() to try the next config file like "gemini.text"
// because the user might want to edit the .config by hand
return nil, nil
}
if err != nil {
// log.Info("open config file :", err)
return nil, err
}
return data, nil
}
func configWrite(filename string, data []byte) error {
fullname := filepath.Join(os.Getenv("GEMINI_HOME"), filename)
cfgfile, err := os.OpenFile(fullname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
defer cfgfile.Close()
if err != nil {
log.Warn("open config file :", err)
return err
}
if filename == "gemini.text" {
// add header
cfgfile.Write([]byte("# this file is automatically re-generated from gemini.pb, however,\n"))
cfgfile.Write([]byte("# if you want to edit it by hand, you can:\n"))
cfgfile.Write([]byte("# stop gemini; remove gemini.pb; edit gemini.text; start gemini\n"))
cfgfile.Write([]byte("# this will cause the default behavior to fallback to parsing this file for the config\n"))
cfgfile.Write([]byte("\n"))
cfgfile.Write([]byte("# this file is intended to be used to customize settings on what\n"))
}
cfgfile.Write(data)
return nil
}