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) } var files []string 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 }