117
internal/storage/storage.go
Normal file
117
internal/storage/storage.go
Normal file
@@ -0,0 +1,117 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user