118 lines
3.0 KiB
Go
118 lines
3.0 KiB
Go
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
|
|
}
|