package store import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "github.com/google/uuid" "reichard.io/aethera/pkg/slices" ) var _ Store = (*FileStore)(nil) // Settings represents the application settings type Settings struct { APIEndpoint string `json:"api_endpoint,omitempty"` ImageEditSelector string `json:"image_edit_selector,omitempty"` ImageGenerationSelector string `json:"image_generation_selector,omitempty"` TextGenerationSelector string `json:"text_generation_selector,omitempty"` } // FileStore implements the Store interface using a file-based storage type FileStore struct { filePath string chatDir string } // NewFileStore creates a new FileStore with the specified file path func NewFileStore(filePath string) (*FileStore, error) { // Derive Chat Directory chatDir := filepath.Join(filepath.Dir(filePath), "chats") // Ensure Chat Directory Exists if err := os.MkdirAll(chatDir, 0755); err != nil { return nil, fmt.Errorf("failed to create directory: %w", err) } return &FileStore{ filePath: filePath, chatDir: chatDir, }, nil } // GetSettings reads and returns the settings from the file func (fs *FileStore) GetSettings() (*Settings, error) { data, err := os.ReadFile(fs.filePath) if err != nil { return &Settings{}, nil } var settings Settings err = json.Unmarshal(data, &settings) if err != nil { return nil, err } return &settings, nil } // SaveSettings saves the settings to the file func (fs *FileStore) SaveSettings(settings *Settings) error { data, err := json.MarshalIndent(settings, "", " ") if err != nil { return err } return os.WriteFile(fs.filePath, data, 0644) } // GetChat retrieves a chat from disk func (fs *FileStore) GetChat(chatID uuid.UUID) (*Chat, error) { filePath := filepath.Join(fs.chatDir, chatID.String()+".json") data, err := os.ReadFile(filePath) if err != nil { if os.IsNotExist(err) { return nil, ErrChatNotFound } return nil, fmt.Errorf("failed to read chat: %w", err) } var chat Chat if err := json.Unmarshal(data, &chat); err != nil { return nil, fmt.Errorf("failed to unmarshal chat: %w", err) } return &chat, nil } // SaveChat creates or updates a chat and persists it to disk func (fs *FileStore) SaveChat(c *Chat) error { c.ensureDefaults() return fs.saveChatSession(c) } // DeleteChat removes a chat from disk func (fs *FileStore) DeleteChat(chatID uuid.UUID) error { filePath := filepath.Join(fs.chatDir, chatID.String()+".json") if _, err := os.Stat(filePath); os.IsNotExist(err) { return ErrChatNotFound } if err := os.Remove(filePath); err != nil { return fmt.Errorf("failed to delete chat: %w", err) } return nil } // ListChats returns all persisted chats func (fs *FileStore) ListChats() ([]*Chat, error) { // Read Files entries, err := os.ReadDir(fs.chatDir) if err != nil { return nil, fmt.Errorf("failed to read chat directory: %w", err) } var chats []*Chat for _, entry := range entries { // Ensure JSON if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") { continue } // Extract Chat ID rawChatID := strings.TrimSuffix(entry.Name(), ".json") chatID, err := uuid.Parse(rawChatID) if err != nil { return nil, fmt.Errorf("%w: invalid chat id %s", err, rawChatID) } // Read & Parse Chat chat, err := fs.GetChat(chatID) if err != nil { return nil, fmt.Errorf("%w: failed to read chat id %s", err, rawChatID) } chats = append(chats, chat) } return chats, nil } // SaveChatMessage creates or updates a chat message to a chat and persists it to disk func (fs *FileStore) SaveChatMessage(m *Message) error { if m.ChatID == uuid.Nil { return ErrNilChatID } m.ensureDefaults() // Get Chat chat, err := fs.GetChat(m.ChatID) if err != nil { return err } // Find Existing existingMsg, found := slices.FindFirst(chat.Messages, func(item *Message) bool { return item.ID == m.ID }) // Upsert if found { *existingMsg = *m } else { chat.Messages = append(chat.Messages, m) } // Save return fs.saveChatSession(chat) } // saveChatSession is a helper method to save a chat to disk func (fs *FileStore) saveChatSession(session *Chat) error { filePath := filepath.Join(fs.chatDir, session.ID.String()+".json") data, err := json.MarshalIndent(session, "", " ") if err != nil { return fmt.Errorf("failed to marshal chat: %w", err) } if err := os.WriteFile(filePath, data, 0644); err != nil { return fmt.Errorf("failed to write chat file: %w", err) } return nil }