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 }