feat: basic firmware registry
This commit is contained in:
109
internal/app/app.go
Normal file
109
internal/app/app.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/rs/cors"
|
||||
|
||||
"spore-registry/internal/config"
|
||||
"spore-registry/internal/database"
|
||||
"spore-registry/internal/handlers"
|
||||
"spore-registry/internal/repository"
|
||||
"spore-registry/internal/service"
|
||||
"spore-registry/internal/storage"
|
||||
)
|
||||
|
||||
// App holds the application dependencies
|
||||
type App struct {
|
||||
config *config.Config
|
||||
db *database.DB
|
||||
handler *handlers.FirmwareHandler
|
||||
}
|
||||
|
||||
// NewApp creates a new application instance
|
||||
func NewApp() (*App, error) {
|
||||
// Load configuration
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
// Create registry directory if it doesn't exist
|
||||
if err := os.MkdirAll(cfg.Registry, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create registry directory: %w", err)
|
||||
}
|
||||
|
||||
// Initialize database
|
||||
db, err := database.NewDB(cfg.DBPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize database: %w", err)
|
||||
}
|
||||
|
||||
// Create repository
|
||||
repo := repository.NewFirmwareRepository(db.GetConnection())
|
||||
|
||||
// Create storage
|
||||
fileStorage := storage.NewFileStorage(cfg.Registry)
|
||||
|
||||
// Create service
|
||||
firmwareService := service.NewFirmwareService(repo, fileStorage)
|
||||
|
||||
// Create handler
|
||||
handler := handlers.NewFirmwareHandler(firmwareService)
|
||||
|
||||
return &App{
|
||||
config: cfg,
|
||||
db: db,
|
||||
handler: handler,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (a *App) Close() error {
|
||||
return a.db.Close()
|
||||
}
|
||||
|
||||
// SetupRoutes sets up HTTP routes
|
||||
func (a *App) SetupRoutes() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// API endpoints
|
||||
mux.HandleFunc("POST /firmware", a.handler.UploadFirmware)
|
||||
mux.HandleFunc("PUT /firmware/{name}/{version}", a.handler.UpdateFirmwareMetadata)
|
||||
mux.HandleFunc("GET /firmware", a.handler.ListFirmware)
|
||||
mux.HandleFunc("GET /firmware/{name}/{version}", a.handler.DownloadFirmware)
|
||||
|
||||
// Health check endpoint
|
||||
mux.HandleFunc("GET /health", a.handler.HealthCheck)
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
func (a *App) Start() error {
|
||||
mux := a.SetupRoutes()
|
||||
|
||||
log.Printf("Starting SPORE registry server on port %s", a.config.Port)
|
||||
log.Printf("Server will be accessible from any host on port %s", a.config.Port)
|
||||
log.Printf("CORS enabled for all origins to support mobile access")
|
||||
|
||||
// Add CORS middleware with dynamic origin handling
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"}, // Allow all origins for mobile access
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"*"},
|
||||
AllowCredentials: false, // Set to false when using wildcard origins
|
||||
AllowOriginFunc: func(origin string) bool {
|
||||
// Allow all origins for mobile/remote access
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
||||
// Add request logging middleware
|
||||
loggedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
|
||||
c.Handler(mux).ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
return http.ListenAndServe(":"+a.config.Port, loggedHandler)
|
||||
}
|
||||
47
internal/config/config.go
Normal file
47
internal/config/config.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Config holds the application configuration
|
||||
type Config struct {
|
||||
Port string
|
||||
DBPath string
|
||||
Registry string
|
||||
MaxSize int64
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from environment variables with defaults
|
||||
func LoadConfig() *Config {
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "3002"
|
||||
}
|
||||
|
||||
dbPath := os.Getenv("DB_PATH")
|
||||
if dbPath == "" {
|
||||
dbPath = "./registry.db"
|
||||
}
|
||||
|
||||
registry := os.Getenv("REGISTRY_PATH")
|
||||
if registry == "" {
|
||||
registry = "registry"
|
||||
}
|
||||
|
||||
maxSizeStr := os.Getenv("MAX_UPLOAD_SIZE")
|
||||
maxSize := int64(32 << 20) // 32MB default
|
||||
if maxSizeStr != "" {
|
||||
if parsed, err := strconv.ParseInt(maxSizeStr, 10, 64); err == nil {
|
||||
maxSize = parsed
|
||||
}
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Port: port,
|
||||
DBPath: dbPath,
|
||||
Registry: registry,
|
||||
MaxSize: maxSize,
|
||||
}
|
||||
}
|
||||
61
internal/database/database.go
Normal file
61
internal/database/database.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// DB wraps the database connection and provides methods for database operations
|
||||
type DB struct {
|
||||
conn *sql.DB
|
||||
}
|
||||
|
||||
// NewDB creates a new database connection
|
||||
func NewDB(dbPath string) (*DB, error) {
|
||||
conn, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
db := &DB{conn: conn}
|
||||
|
||||
// Create tables
|
||||
if err := db.createTables(); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to create tables: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (db *DB) Close() error {
|
||||
return db.conn.Close()
|
||||
}
|
||||
|
||||
// GetConnection returns the underlying database connection
|
||||
func (db *DB) GetConnection() *sql.DB {
|
||||
return db.conn
|
||||
}
|
||||
|
||||
// createTables creates the necessary database tables
|
||||
func (db *DB) createTables() error {
|
||||
query := `
|
||||
CREATE TABLE IF NOT EXISTS firmware (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
version TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
labels TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(name, version)
|
||||
);`
|
||||
|
||||
if _, err := db.conn.Exec(query); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
207
internal/handlers/firmware.go
Normal file
207
internal/handlers/firmware.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"spore-registry/internal/models"
|
||||
"spore-registry/internal/service"
|
||||
)
|
||||
|
||||
// FirmwareHandler handles HTTP requests for firmware operations
|
||||
type FirmwareHandler struct {
|
||||
service *service.FirmwareService
|
||||
}
|
||||
|
||||
// NewFirmwareHandler creates a new firmware handler
|
||||
func NewFirmwareHandler(service *service.FirmwareService) *FirmwareHandler {
|
||||
return &FirmwareHandler{service: service}
|
||||
}
|
||||
|
||||
// UploadFirmware handles POST /firmware endpoint
|
||||
func (h *FirmwareHandler) UploadFirmware(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("POST /firmware - %s", r.RemoteAddr)
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
log.Printf("Invalid method %s for /firmware", r.Method)
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse multipart form
|
||||
err := r.ParseMultipartForm(32 << 20) // 32MB max
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse multipart form: %v", err)
|
||||
http.Error(w, "Failed to parse form: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get metadata from form
|
||||
metadataJSON := r.FormValue("metadata")
|
||||
if metadataJSON == "" {
|
||||
log.Printf("Missing metadata field in firmware upload")
|
||||
http.Error(w, "Missing metadata field", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var metadata models.FirmwareMetadata
|
||||
if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil {
|
||||
log.Printf("Invalid metadata JSON: %v", err)
|
||||
http.Error(w, "Invalid metadata JSON: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get firmware file
|
||||
file, _, err := r.FormFile("firmware")
|
||||
if err != nil {
|
||||
log.Printf("Missing firmware file in upload: %v", err)
|
||||
http.Error(w, "Missing firmware file: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
log.Printf("Uploading firmware: %s/%s", metadata.Name, metadata.Version)
|
||||
|
||||
// Upload firmware using service
|
||||
response, err := h.service.UploadFirmware(metadata, file)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upload firmware: %v", err)
|
||||
http.Error(w, "Failed to upload firmware: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Successfully uploaded firmware: %s/%s (size: %d bytes)", metadata.Name, metadata.Version, response.Size)
|
||||
|
||||
// Return success response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// ListFirmware handles GET /firmware endpoint with optional query parameters
|
||||
func (h *FirmwareHandler) ListFirmware(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("GET /firmware - %s (name=%s, version=%s)",
|
||||
r.RemoteAddr, r.URL.Query().Get("name"), r.URL.Query().Get("version"))
|
||||
|
||||
if r.Method != http.MethodGet {
|
||||
log.Printf("Invalid method %s for /firmware", r.Method)
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
name := r.URL.Query().Get("name")
|
||||
version := r.URL.Query().Get("version")
|
||||
|
||||
// Get firmware records using service
|
||||
grouped, err := h.service.ListFirmware(name, version)
|
||||
if err != nil {
|
||||
log.Printf("Failed to retrieve firmware list: %v", err)
|
||||
http.Error(w, "Failed to retrieve firmware list: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Returning %d firmware groups", len(grouped))
|
||||
|
||||
// Return grouped firmware list
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(grouped)
|
||||
}
|
||||
|
||||
// UpdateFirmwareMetadata handles PUT /firmware/{name}/{version} endpoint for metadata-only updates
|
||||
func (h *FirmwareHandler) UpdateFirmwareMetadata(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract path parameters
|
||||
path := strings.TrimPrefix(r.URL.Path, "/firmware/")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) != 2 {
|
||||
log.Printf("Invalid firmware path for update: %s", r.URL.Path)
|
||||
http.Error(w, "Invalid firmware path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
name := parts[0]
|
||||
version := parts[1]
|
||||
|
||||
log.Printf("PUT /firmware/%s/%s - %s", name, version, r.RemoteAddr)
|
||||
|
||||
// Parse JSON body
|
||||
var metadata models.FirmwareMetadata
|
||||
if err := json.NewDecoder(r.Body).Decode(&metadata); err != nil {
|
||||
log.Printf("Invalid JSON in metadata update: %v", err)
|
||||
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Update metadata using service
|
||||
response, err := h.service.UpdateFirmwareMetadata(name, version, metadata)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
log.Printf("Firmware not found for update: %s/%s", name, version)
|
||||
http.Error(w, "Firmware not found", http.StatusNotFound)
|
||||
} else {
|
||||
log.Printf("Failed to update firmware metadata: %v", err)
|
||||
http.Error(w, "Failed to update firmware metadata: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Successfully updated firmware metadata: %s/%s", name, version)
|
||||
|
||||
// Return success response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// DownloadFirmware handles GET /firmware/{name}/{version} endpoint
|
||||
func (h *FirmwareHandler) DownloadFirmware(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract path parameters
|
||||
path := strings.TrimPrefix(r.URL.Path, "/firmware/")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) != 2 {
|
||||
log.Printf("Invalid firmware path: %s", r.URL.Path)
|
||||
http.Error(w, "Invalid firmware path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
name := parts[0]
|
||||
version := parts[1]
|
||||
|
||||
log.Printf("GET /firmware/%s/%s - %s", name, version, r.RemoteAddr)
|
||||
|
||||
// Get firmware file path using service
|
||||
filePath, err := h.service.GetFirmwarePath(name, version)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
log.Printf("Firmware not found: %s/%s", name, version)
|
||||
http.Error(w, "Firmware not found", http.StatusNotFound)
|
||||
} else {
|
||||
log.Printf("Failed to get firmware path: %v", err)
|
||||
http.Error(w, "Failed to get firmware: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Serve the file
|
||||
log.Printf("Serving firmware file: %s", filePath)
|
||||
http.ServeFile(w, r, filePath)
|
||||
}
|
||||
|
||||
// HealthCheck provides a simple health check endpoint
|
||||
func (h *FirmwareHandler) HealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
response := models.HealthResponse{Status: "healthy"}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
37
internal/models/firmware.go
Normal file
37
internal/models/firmware.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package models
|
||||
|
||||
// FirmwareMetadata represents the metadata structure for firmware uploads
|
||||
type FirmwareMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
|
||||
// FirmwareRecord represents a firmware record in the database
|
||||
type FirmwareRecord struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Size int64 `json:"size"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Path string `json:"download_url"`
|
||||
}
|
||||
|
||||
// GroupedFirmware represents firmware grouped by name
|
||||
type GroupedFirmware struct {
|
||||
Name string `json:"name"`
|
||||
Firmware []FirmwareRecord `json:"firmware"`
|
||||
}
|
||||
|
||||
// UploadResponse represents the response for firmware upload
|
||||
type UploadResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Size int64 `json:"size"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// HealthResponse represents the health check response
|
||||
type HealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
82
internal/repository/firmware.go
Normal file
82
internal/repository/firmware.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"spore-registry/internal/models"
|
||||
)
|
||||
|
||||
// FirmwareRepository handles database operations for firmware
|
||||
type FirmwareRepository struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewFirmwareRepository creates a new firmware repository
|
||||
func NewFirmwareRepository(db *sql.DB) *FirmwareRepository {
|
||||
return &FirmwareRepository{db: db}
|
||||
}
|
||||
|
||||
// StoreFirmwareMetadata stores firmware metadata in the database
|
||||
func (r *FirmwareRepository) StoreFirmwareMetadata(metadata models.FirmwareMetadata, size int64) error {
|
||||
labelsJSON, err := json.Marshal(metadata.Labels)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal labels: %w", err)
|
||||
}
|
||||
|
||||
query := `INSERT OR REPLACE INTO firmware (name, version, size, labels) VALUES (?, ?, ?, ?)`
|
||||
_, err = r.db.Exec(query, metadata.Name, metadata.Version, size, string(labelsJSON))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store firmware metadata: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFirmwareRecords retrieves firmware records with optional filtering
|
||||
func (r *FirmwareRepository) GetFirmwareRecords(name, version string) ([]models.FirmwareRecord, error) {
|
||||
var records []models.FirmwareRecord
|
||||
|
||||
// Build query with optional filters
|
||||
query := `
|
||||
SELECT name, version, size, labels
|
||||
FROM firmware
|
||||
WHERE 1=1`
|
||||
|
||||
var args []interface{}
|
||||
if name != "" {
|
||||
query += " AND name = ?"
|
||||
args = append(args, name)
|
||||
}
|
||||
if version != "" {
|
||||
query += " AND version = ?"
|
||||
args = append(args, version)
|
||||
}
|
||||
|
||||
rows, err := r.db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query firmware records: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var record models.FirmwareRecord
|
||||
var labelsJSON string
|
||||
|
||||
err := rows.Scan(&record.Name, &record.Version, &record.Size, &labelsJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan firmware record: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(labelsJSON), &record.Labels); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal labels: %w", err)
|
||||
}
|
||||
|
||||
// Construct download path
|
||||
record.Path = fmt.Sprintf("/firmware/%s/%s", record.Name, record.Version)
|
||||
records = append(records, record)
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
190
internal/service/firmware.go
Normal file
190
internal/service/firmware.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"spore-registry/internal/models"
|
||||
"spore-registry/internal/repository"
|
||||
"spore-registry/internal/storage"
|
||||
)
|
||||
|
||||
// FirmwareService handles business logic for firmware operations
|
||||
type FirmwareService struct {
|
||||
repo *repository.FirmwareRepository
|
||||
storage *storage.FileStorage
|
||||
}
|
||||
|
||||
// NewFirmwareService creates a new firmware service
|
||||
func NewFirmwareService(repo *repository.FirmwareRepository, storage *storage.FileStorage) *FirmwareService {
|
||||
return &FirmwareService{
|
||||
repo: repo,
|
||||
storage: storage,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadFirmware uploads firmware binary and metadata
|
||||
func (s *FirmwareService) UploadFirmware(metadata models.FirmwareMetadata, data io.Reader) (*models.UploadResponse, error) {
|
||||
// Validate required fields
|
||||
if metadata.Name == "" || metadata.Version == "" {
|
||||
return nil, fmt.Errorf("name and version are required")
|
||||
}
|
||||
|
||||
// Store firmware binary
|
||||
filePath, size, err := s.storage.StoreFirmwareBinary(metadata.Name, metadata.Version, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to store firmware binary: %w", err)
|
||||
}
|
||||
|
||||
// Store metadata in database
|
||||
if err := s.repo.StoreFirmwareMetadata(metadata, size); err != nil {
|
||||
// Try to clean up the file if database operation fails
|
||||
os.Remove(filePath)
|
||||
return nil, fmt.Errorf("failed to store firmware metadata: %w", err)
|
||||
}
|
||||
|
||||
return &models.UploadResponse{
|
||||
Success: true,
|
||||
Name: metadata.Name,
|
||||
Version: metadata.Version,
|
||||
Size: size,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListFirmware retrieves firmware records with optional filtering
|
||||
func (s *FirmwareService) ListFirmware(name, version string) ([]models.GroupedFirmware, error) {
|
||||
records, err := s.repo.GetFirmwareRecords(name, version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve firmware records: %w", err)
|
||||
}
|
||||
|
||||
grouped := s.groupFirmwareByName(records)
|
||||
return grouped, nil
|
||||
}
|
||||
|
||||
// UpdateFirmwareMetadata updates firmware metadata
|
||||
func (s *FirmwareService) UpdateFirmwareMetadata(name, version string, metadata models.FirmwareMetadata) (*models.UploadResponse, error) {
|
||||
// Validate that the name and version match the URL
|
||||
if metadata.Name != name || metadata.Version != version {
|
||||
return nil, fmt.Errorf("name and version in URL must match metadata")
|
||||
}
|
||||
|
||||
// Check if firmware exists
|
||||
records, err := s.repo.GetFirmwareRecords(name, version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check if firmware exists: %w", err)
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return nil, fmt.Errorf("firmware not found")
|
||||
}
|
||||
|
||||
// Get existing firmware size (keep the same size since we're not updating the binary)
|
||||
existingRecord := records[0]
|
||||
size := existingRecord.Size
|
||||
|
||||
// Update metadata in database
|
||||
if err := s.repo.StoreFirmwareMetadata(metadata, size); err != nil {
|
||||
return nil, fmt.Errorf("failed to update firmware metadata: %w", err)
|
||||
}
|
||||
|
||||
return &models.UploadResponse{
|
||||
Success: true,
|
||||
Name: metadata.Name,
|
||||
Version: metadata.Version,
|
||||
Size: size,
|
||||
Message: "Metadata updated successfully",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetFirmwarePath returns the file path for downloading firmware
|
||||
func (s *FirmwareService) GetFirmwarePath(name, version string) (string, error) {
|
||||
if !s.storage.FirmwareExists(name, version) {
|
||||
return "", fmt.Errorf("firmware not found")
|
||||
}
|
||||
|
||||
return s.storage.GetFirmwareBinaryPath(name, version), nil
|
||||
}
|
||||
|
||||
// groupFirmwareByName groups firmware records by name
|
||||
func (s *FirmwareService) groupFirmwareByName(records []models.FirmwareRecord) []models.GroupedFirmware {
|
||||
nameMap := make(map[string][]models.FirmwareRecord)
|
||||
|
||||
// Group records by name
|
||||
for _, record := range records {
|
||||
nameMap[record.Name] = append(nameMap[record.Name], record)
|
||||
}
|
||||
|
||||
// Convert to slice
|
||||
var grouped []models.GroupedFirmware
|
||||
for name, firmware := range nameMap {
|
||||
// Sort firmware versions by version number (highest first)
|
||||
sort.Slice(firmware, func(i, j int) bool {
|
||||
return compareVersions(firmware[i].Version, firmware[j].Version) > 0
|
||||
})
|
||||
|
||||
grouped = append(grouped, models.GroupedFirmware{
|
||||
Name: name,
|
||||
Firmware: firmware,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort firmware groups by name (A to Z)
|
||||
sort.Slice(grouped, func(i, j int) bool {
|
||||
return grouped[i].Name < grouped[j].Name
|
||||
})
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
// compareVersions compares two semantic version strings
|
||||
// Returns: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
|
||||
func compareVersions(v1, v2 string) int {
|
||||
// Remove 'v' prefix if present
|
||||
v1 = strings.TrimPrefix(v1, "v")
|
||||
v2 = strings.TrimPrefix(v2, "v")
|
||||
|
||||
parts1 := strings.Split(v1, ".")
|
||||
parts2 := strings.Split(v2, ".")
|
||||
|
||||
// Pad shorter version with zeros
|
||||
maxLen := len(parts1)
|
||||
if len(parts2) > maxLen {
|
||||
maxLen = len(parts2)
|
||||
}
|
||||
|
||||
for len(parts1) < maxLen {
|
||||
parts1 = append(parts1, "0")
|
||||
}
|
||||
for len(parts2) < maxLen {
|
||||
parts2 = append(parts2, "0")
|
||||
}
|
||||
|
||||
// Compare each part
|
||||
for i := 0; i < maxLen; i++ {
|
||||
num1, err1 := strconv.Atoi(parts1[i])
|
||||
num2, err2 := strconv.Atoi(parts2[i])
|
||||
|
||||
// If parsing fails, do string comparison
|
||||
if err1 != nil || err2 != nil {
|
||||
if parts1[i] < parts2[i] {
|
||||
return -1
|
||||
} else if parts1[i] > parts2[i] {
|
||||
return 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if num1 < num2 {
|
||||
return -1
|
||||
} else if num1 > num2 {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
57
internal/storage/file.go
Normal file
57
internal/storage/file.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// FileStorage handles file system operations for firmware binaries
|
||||
type FileStorage struct {
|
||||
registryPath string
|
||||
}
|
||||
|
||||
// NewFileStorage creates a new file storage instance
|
||||
func NewFileStorage(registryPath string) *FileStorage {
|
||||
return &FileStorage{registryPath: registryPath}
|
||||
}
|
||||
|
||||
// StoreFirmwareBinary stores a firmware binary in the hierarchical file structure
|
||||
func (fs *FileStorage) StoreFirmwareBinary(name, version string, data io.Reader) (string, int64, error) {
|
||||
// Create directory path: registry/<name>/<version>/
|
||||
dirPath := filepath.Join(fs.registryPath, name, version)
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
return "", 0, fmt.Errorf("failed to create firmware directory: %w", err)
|
||||
}
|
||||
|
||||
// Create firmware file path
|
||||
filePath := filepath.Join(dirPath, "firmware.bin")
|
||||
|
||||
// Create the file
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("failed to create firmware file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Copy data to file and calculate size
|
||||
size, err := io.Copy(file, data)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("failed to write firmware data: %w", err)
|
||||
}
|
||||
|
||||
return filePath, size, nil
|
||||
}
|
||||
|
||||
// GetFirmwareBinaryPath returns the file path for a firmware binary
|
||||
func (fs *FileStorage) GetFirmwareBinaryPath(name, version string) string {
|
||||
return filepath.Join(fs.registryPath, name, version, "firmware.bin")
|
||||
}
|
||||
|
||||
// FirmwareExists checks if a firmware binary exists
|
||||
func (fs *FileStorage) FirmwareExists(name, version string) bool {
|
||||
filePath := fs.GetFirmwareBinaryPath(name, version)
|
||||
_, err := os.Stat(filePath)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
Reference in New Issue
Block a user