package storage import ( "fmt" "os" "path/filepath" "regexp" "strings" "time" ) // FileMetadata holds metadata about a markdown file type FileMetadata struct { Filename string `json:"filename"` Title string `json:"title"` Modified time.Time `json:"modified"` Size int64 `json:"size"` } // FileContent holds both metadata and content type FileContent struct { Filename string `json:"filename"` Title string `json:"title"` Content string `json:"content"` Modified time.Time `json:"modified"` Size int64 `json:"size"` } // Storage handles file operations for markdown files type Storage struct { dataDir string } // NewStorage creates a new storage instance func NewStorage(dataDir string) *Storage { return &Storage{dataDir: dataDir} } // validateFilename checks if the filename is valid func (s *Storage) validateFilename(filename string) error { // Must end with .md if !strings.HasSuffix(filename, ".md") { return fmt.Errorf("only .md files are allowed") } // Check for path traversal if strings.Contains(filename, "..") || strings.Contains(filename, "/") || strings.Contains(filename, "\\") { return fmt.Errorf("invalid filename") } // Check for empty filename if filename == "" { return fmt.Errorf("filename cannot be empty") } return nil } // ListFiles returns a list of all markdown files func (s *Storage) ListFiles() ([]FileMetadata, error) { files, err := os.ReadDir(s.dataDir) if err != nil { return nil, fmt.Errorf("failed to read directory: %w", err) } metadata := []FileMetadata{} re := regexp.MustCompile(`^(.+)\.md$`) for _, file := range files { if file.IsDir() { continue } if !strings.HasSuffix(file.Name(), ".md") { continue } info, err := file.Info() if err != nil { continue } name := re.ReplaceAllString(file.Name(), "$1") metadata = append(metadata, FileMetadata{ Filename: file.Name(), Title: name, Modified: info.ModTime(), Size: info.Size(), }) } return metadata, nil } // GetFile reads a markdown file by filename func (s *Storage) GetFile(filename string) (*FileContent, error) { if err := s.validateFilename(filename); err != nil { return nil, fmt.Errorf("invalid filename: %w", err) } path := filepath.Join(s.dataDir, filename) data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return nil, fmt.Errorf("file not found") } return nil, fmt.Errorf("failed to read file: %w", err) } return &FileContent{ Filename: filename, Content: string(data), Title: strings.TrimSuffix(filename, ".md"), }, nil } // CreateFile creates a new markdown file func (s *Storage) CreateFile(filename string, content string) (*FileContent, error) { if err := s.validateFilename(filename); err != nil { return nil, fmt.Errorf("invalid filename: %w", err) } path := filepath.Join(s.dataDir, filename) if _, err := os.Stat(path); !os.IsNotExist(err) { return nil, fmt.Errorf("file already exists") } if err := os.WriteFile(path, []byte(content), 0644); err != nil { return nil, fmt.Errorf("failed to write file: %w", err) } info, err := os.Stat(path) if err != nil { return nil, fmt.Errorf("failed to get file info: %w", err) } return &FileContent{ Filename: filename, Content: content, Title: strings.TrimSuffix(filename, ".md"), Modified: info.ModTime(), Size: info.Size(), }, nil } // UpdateFile updates an existing markdown file func (s *Storage) UpdateFile(filename string, content string) (*FileContent, error) { if err := s.validateFilename(filename); err != nil { return nil, fmt.Errorf("invalid filename: %w", err) } path := filepath.Join(s.dataDir, filename) if _, err := os.Stat(path); os.IsNotExist(err) { return nil, fmt.Errorf("file not found") } if err := os.WriteFile(path, []byte(content), 0644); err != nil { return nil, fmt.Errorf("failed to write file: %w", err) } info, err := os.Stat(path) if err != nil { return nil, fmt.Errorf("failed to get file info: %w", err) } return &FileContent{ Filename: filename, Content: content, Title: strings.TrimSuffix(filename, ".md"), Modified: info.ModTime(), Size: info.Size(), }, nil } // DeleteFile deletes a markdown file func (s *Storage) DeleteFile(filename string) error { if err := s.validateFilename(filename); err != nil { return fmt.Errorf("invalid filename: %w", err) } path := filepath.Join(s.dataDir, filename) if err := os.Remove(path); err != nil { if os.IsNotExist(err) { return fmt.Errorf("file not found") } return fmt.Errorf("failed to delete file: %w", err) } return nil } // ValidateFilename is a public wrapper for testing func (s *Storage) ValidateFilename(filename string) error { return s.validateFilename(filename) }