package storage import ( "fmt" "os" "path/filepath" "regexp" "strings" "time" ) // Storage handles local file system operations for FIT files type Storage struct { outputDir string } // New creates a new Storage instance func New(outputDir string) (*Storage, error) { // Create output directory if it doesn't exist if err := os.MkdirAll(outputDir, 0755); err != nil { return nil, fmt.Errorf("failed to create output directory: %w", err) } return &Storage{ outputDir: outputDir, }, nil } // Save saves a FIT file to the output directory func (s *Storage) Save(activityID int64, activityName string, date time.Time, data []byte) (string, error) { // Generate filename filename := s.generateFilename(activityID, activityName, date) filepath := filepath.Join(s.outputDir, filename) // Check if file already exists if _, err := os.Stat(filepath); err == nil { return filepath, nil // File already exists, no need to save } // Write to temporary file first (atomic write) tmpFile := filepath + ".tmp" if err := os.WriteFile(tmpFile, data, 0644); err != nil { return "", fmt.Errorf("failed to write temporary FIT file: %w", err) } // Verify file integrity (non-zero size) if len(data) == 0 { _ = os.Remove(tmpFile) return "", fmt.Errorf("FIT file is empty") } // Rename temporary file to actual file (atomic operation) if err := os.Rename(tmpFile, filepath); err != nil { _ = os.Remove(tmpFile) return "", fmt.Errorf("failed to rename temporary FIT file: %w", err) } return filepath, nil } // Exists checks if a FIT file already exists for an activity func (s *Storage) Exists(activityID int64) bool { // List files in output directory files, err := os.ReadDir(s.outputDir) if err != nil { return false } // Check if any file starts with the activity ID prefix := fmt.Sprintf("%d_", activityID) for _, file := range files { if strings.HasPrefix(file.Name(), prefix) { return true } } return false } // generateFilename generates a filename for a FIT file // Format: {activityID}_{date}_{name}.fit func (s *Storage) generateFilename(activityID int64, activityName string, date time.Time) string { // Format date as YYYY-MM-DD dateStr := date.Format("2006-01-02") // Sanitize activity name (remove special characters) name := sanitizeFilename(activityName) if name == "" { name = "activity" } // Limit name length if len(name) > 50 { name = name[:50] } return fmt.Sprintf("%d_%s_%s.fit", activityID, dateStr, name) } // sanitizeFilename removes special characters from filename func sanitizeFilename(name string) string { // Replace spaces with dashes name = strings.ReplaceAll(name, " ", "-") // Remove special characters (keep alphanumeric, dash, underscore) re := regexp.MustCompile(`[^a-zA-Z0-9_-]`) name = re.ReplaceAllString(name, "") // Remove multiple consecutive dashes re = regexp.MustCompile(`-+`) name = re.ReplaceAllString(name, "-") // Trim dashes from start and end name = strings.Trim(name, "-") return name }