Files
agent-evals/internal/storage/storage.go
Evan Reichard 482d8a448a Initial commit: WYSIWYG Markdown Editor - Go backend + React/TypeScript frontend with Tailwind CSS
Backend:
- Cobra CLI with --data-dir, --port, --host flags
- Gin HTTP server with REST API for markdown CRUD operations
- File storage on disk (.md files only)
- Comprehensive logrus logging
- Backend tests with CRUD round-trip verification

Frontend:
- React 18 + TypeScript + Tailwind CSS
- Markdown editor with live GFM preview (react-markdown + remark-gfm)
- File management UI (list, create, open, save, delete)
- Theme switcher with Dark/Light/System modes
- Responsive design
- Frontend tests with vitest

Testing:
- All backend tests pass (go test ./...)
- All frontend tests pass (npm test)
2026-02-05 15:44:06 -05:00

134 lines
3.3 KiB
Go

package storage
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"github.com/sirupsen/logrus"
"markdown-editor/internal/logger"
)
var (
allowedExt = regexp.MustCompile(`(?i)\.md$`)
)
type Storage struct {
dataDir string
log *logrus.Logger
}
func NewStorage(dataDir string) (*Storage, error) {
log := logger.GetLogger()
// Create data directory if it doesn't exist
if err := os.MkdirAll(dataDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create data directory: %w", err)
}
return &Storage{
dataDir: dataDir,
log: log,
}, nil
}
// File represents a markdown file
type File struct {
Name string `json:"name"`
Content string `json:"content"`
}
// ListFiles returns all .md files in the data directory
func (s *Storage) ListFiles() ([]string, error) {
s.log.WithField("operation", "list_files").Info("Listing markdown files")
files := []string{}
err := filepath.WalkDir(s.dataDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && allowedExt.MatchString(d.Name()) {
files = append(files, d.Name())
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to walk directory: %w", err)
}
return files, nil
}
// GetFile reads a markdown file by name
func (s *Storage) GetFile(name string) (*File, error) {
s.log.WithField("operation", "get_file").WithField("filename", name).Info("Getting file")
path := filepath.Join(s.dataDir, name)
if !allowedExt.MatchString(path) {
return nil, fmt.Errorf("invalid file extension: only .md files allowed")
}
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("file not found: %s", name)
}
return nil, fmt.Errorf("failed to read file: %w", err)
}
return &File{
Name: name,
Content: string(data),
}, nil
}
// CreateFile creates a new markdown file
func (s *Storage) CreateFile(name string, content string) error {
s.log.WithField("operation", "create_file").WithField("filename", name).Info("Creating file")
path := filepath.Join(s.dataDir, name)
if !allowedExt.MatchString(path) {
return fmt.Errorf("invalid file extension: only .md files allowed")
}
// Check if file already exists
if _, err := os.Stat(path); err == nil {
return fmt.Errorf("file already exists: %s", name)
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
return nil
}
// UpdateFile updates an existing markdown file
func (s *Storage) UpdateFile(name string, content string) error {
s.log.WithField("operation", "update_file").WithField("filename", name).Info("Updating file")
path := filepath.Join(s.dataDir, name)
if !allowedExt.MatchString(path) {
return fmt.Errorf("invalid file extension: only .md files allowed")
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
return nil
}
// DeleteFile deletes a markdown file
func (s *Storage) DeleteFile(name string) error {
s.log.WithField("operation", "delete_file").WithField("filename", name).Info("Deleting file")
path := filepath.Join(s.dataDir, name)
if !allowedExt.MatchString(path) {
return fmt.Errorf("invalid file extension: only .md files allowed")
}
if err := os.Remove(path); err != nil {
return fmt.Errorf("failed to delete file: %w", err)
}
return nil
}