Files
agent-evals/backend/internal/storage/storage.go
Evan Reichard 10f584f9a8 fix(api): ensure files array is never null in API response
Add null safety checks to prevent TypeError when backend returns null
instead of empty array for files list. Initialize empty slices on backend
and add null coalescing on frontend when accessing files state.

- Backend: Initialize files slice to always return [] instead of null
- Frontend: Add null checks for files state in all map/filter operations
2026-02-06 08:59:11 -05:00

174 lines
4.5 KiB
Go

package storage
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/evanreichard/markdown-editor/internal/logging"
)
var (
ErrFileNotFound = errors.New("file not found")
ErrInvalidName = errors.New("invalid file name")
)
// File represents a markdown file
type File struct {
Name string `json:"name"`
Content string `json:"content"`
Modified int64 `json:"modified"`
}
// Storage handles file operations
type Storage struct {
dataDir string
}
// New creates a new Storage instance
func New(dataDir string) (*Storage, error) {
if err := os.MkdirAll(dataDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create data directory: %w", err)
}
logging.Logger.WithField("data_dir", dataDir).Info("Storage initialized")
return &Storage{dataDir: dataDir}, nil
}
// List returns all markdown files
func (s *Storage) List() ([]*File, error) {
entries, err := os.ReadDir(s.dataDir)
if err != nil {
logging.Logger.WithError(err).Error("Failed to list files")
return nil, fmt.Errorf("failed to read directory: %w", err)
}
files := make([]*File, 0)
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
files = append(files, &File{
Name: entry.Name(),
Modified: info.ModTime().Unix(),
})
}
logging.Logger.WithField("count", len(files)).Info("Listed files")
return files, nil
}
// Get reads a markdown file
func (s *Storage) Get(name string) (*File, error) {
if !s.isValidName(name) {
logging.Logger.WithField("name", name).Warn("Invalid file name")
return nil, ErrInvalidName
}
path := filepath.Join(s.dataDir, name)
content, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
logging.Logger.WithField("name", name).Warn("File not found")
return nil, ErrFileNotFound
}
logging.Logger.WithError(err).Error("Failed to read file")
return nil, fmt.Errorf("failed to read file: %w", err)
}
info, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("failed to get file info: %w", err)
}
logging.Logger.WithField("name", name).Info("File retrieved")
return &File{
Name: name,
Content: string(content),
Modified: info.ModTime().Unix(),
}, nil
}
// Create creates a new markdown file
func (s *Storage) Create(name, content string) (*File, error) {
if !s.isValidName(name) {
logging.Logger.WithField("name", name).Warn("Invalid file name for create")
return nil, ErrInvalidName
}
path := filepath.Join(s.dataDir, name)
if _, err := os.Stat(path); err == nil {
logging.Logger.WithField("name", name).Warn("File already exists")
return nil, fmt.Errorf("file already exists")
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
logging.Logger.WithError(err).Error("Failed to create file")
return nil, fmt.Errorf("failed to write file: %w", err)
}
logging.Logger.WithField("name", name).Info("File created")
return s.Get(name)
}
// Update updates an existing markdown file
func (s *Storage) Update(name, content string) (*File, error) {
if !s.isValidName(name) {
logging.Logger.WithField("name", name).Warn("Invalid file name for update")
return nil, ErrInvalidName
}
path := filepath.Join(s.dataDir, name)
if _, err := os.Stat(path); os.IsNotExist(err) {
logging.Logger.WithField("name", name).Warn("File not found for update")
return nil, ErrFileNotFound
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
logging.Logger.WithError(err).Error("Failed to update file")
return nil, fmt.Errorf("failed to write file: %w", err)
}
logging.Logger.WithField("name", name).Info("File updated")
return s.Get(name)
}
// Delete removes a markdown file
func (s *Storage) Delete(name string) error {
if !s.isValidName(name) {
logging.Logger.WithField("name", name).Warn("Invalid file name for delete")
return ErrInvalidName
}
path := filepath.Join(s.dataDir, name)
if err := os.Remove(path); err != nil {
if os.IsNotExist(err) {
logging.Logger.WithField("name", name).Warn("File not found for delete")
return ErrFileNotFound
}
logging.Logger.WithError(err).Error("Failed to delete file")
return fmt.Errorf("failed to delete file: %w", err)
}
logging.Logger.WithField("name", name).Info("File deleted")
return nil
}
// isValidName checks if the file name is valid
func (s *Storage) isValidName(name string) bool {
if name == "" || strings.Contains(name, "..") || strings.Contains(name, "/") {
return false
}
if !strings.HasSuffix(name, ".md") {
return false
}
return true
}