Initial commit

Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
This commit is contained in:
Thomas Klaehn
2026-02-10 11:47:49 +01:00
commit c54fa58c35
17 changed files with 1834 additions and 0 deletions

117
internal/storage/storage.go Normal file
View 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
}