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
174 lines
4.5 KiB
Go
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
|
|
}
|