Initialize files slice with make() to ensure JSON serialization returns [] instead of null when no files exist. Prevents frontend TypeError when calling .map() on null.
151 lines
3.7 KiB
Go
151 lines
3.7 KiB
Go
package storage
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Storage handles file operations for markdown files
|
|
type Storage struct {
|
|
dataDir string
|
|
mu sync.RWMutex
|
|
log *logrus.Logger
|
|
}
|
|
|
|
// New creates a new Storage instance
|
|
func New(dataDir string, log *logrus.Logger) *Storage {
|
|
return &Storage{
|
|
dataDir: dataDir,
|
|
log: log,
|
|
}
|
|
}
|
|
|
|
// Init ensures the data directory exists
|
|
func (s *Storage) Init() error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.log.WithField("dataDir", s.dataDir).Info("Initializing storage")
|
|
|
|
if err := os.MkdirAll(s.dataDir, 0755); err != nil {
|
|
s.log.WithError(err).Error("Failed to create data directory")
|
|
return fmt.Errorf("failed to create data directory: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// List returns all markdown files in the data directory
|
|
func (s *Storage) List() ([]string, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
s.log.Debug("Listing all files")
|
|
|
|
entries, err := os.ReadDir(s.dataDir)
|
|
if err != nil {
|
|
s.log.WithError(err).Error("Failed to read data directory")
|
|
return nil, fmt.Errorf("failed to read data directory: %w", err)
|
|
}
|
|
|
|
files := make([]string, 0)
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".md") {
|
|
files = append(files, strings.TrimSuffix(entry.Name(), ".md"))
|
|
}
|
|
}
|
|
|
|
s.log.WithField("count", len(files)).Debug("Listed files")
|
|
return files, nil
|
|
}
|
|
|
|
// Get reads a markdown file by name
|
|
func (s *Storage) Get(name string) (string, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
s.log.WithField("name", name).Debug("Getting file")
|
|
|
|
if !isValidFilename(name) {
|
|
s.log.WithField("name", name).Warn("Invalid filename")
|
|
return "", fmt.Errorf("invalid filename")
|
|
}
|
|
|
|
path := filepath.Join(s.dataDir, name+".md")
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
s.log.WithField("name", name).Warn("File not found")
|
|
return "", fmt.Errorf("file not found")
|
|
}
|
|
s.log.WithError(err).WithField("name", name).Error("Failed to read file")
|
|
return "", fmt.Errorf("failed to read file: %w", err)
|
|
}
|
|
|
|
return string(content), nil
|
|
}
|
|
|
|
// Save writes content to a markdown file
|
|
func (s *Storage) Save(name, content string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.log.WithField("name", name).Debug("Saving file")
|
|
|
|
if !isValidFilename(name) {
|
|
s.log.WithField("name", name).Warn("Invalid filename")
|
|
return fmt.Errorf("invalid filename")
|
|
}
|
|
|
|
path := filepath.Join(s.dataDir, name+".md")
|
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
s.log.WithError(err).WithField("name", name).Error("Failed to write file")
|
|
return fmt.Errorf("failed to write file: %w", err)
|
|
}
|
|
|
|
s.log.WithField("name", name).Info("File saved")
|
|
return nil
|
|
}
|
|
|
|
// Delete removes a markdown file
|
|
func (s *Storage) Delete(name string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.log.WithField("name", name).Debug("Deleting file")
|
|
|
|
if !isValidFilename(name) {
|
|
s.log.WithField("name", name).Warn("Invalid filename")
|
|
return fmt.Errorf("invalid filename")
|
|
}
|
|
|
|
path := filepath.Join(s.dataDir, name+".md")
|
|
if err := os.Remove(path); err != nil {
|
|
if os.IsNotExist(err) {
|
|
s.log.WithField("name", name).Warn("File not found for deletion")
|
|
return fmt.Errorf("file not found")
|
|
}
|
|
s.log.WithError(err).WithField("name", name).Error("Failed to delete file")
|
|
return fmt.Errorf("failed to delete file: %w", err)
|
|
}
|
|
|
|
s.log.WithField("name", name).Info("File deleted")
|
|
return nil
|
|
}
|
|
|
|
// isValidFilename checks if a filename is safe to use
|
|
func isValidFilename(name string) bool {
|
|
if name == "" {
|
|
return false
|
|
}
|
|
// Prevent path traversal
|
|
if strings.Contains(name, "/") || strings.Contains(name, "\\") || strings.Contains(name, "..") {
|
|
return false
|
|
}
|
|
return true
|
|
}
|