Refactor golden image handling in backup upload process</message>
<message>Update the _set_golden_from_path function to improve the handling of existing golden image files. Replace the existing unlink logic with a more robust method that safely removes files or broken symlinks using the missing_ok parameter. This change enhances the reliability of the backup upload process by ensuring that stale references are properly cleared before setting a new golden image path.
This commit is contained in:
211
backup-from-device/gnss-guard/tm-gnss-guard/server/models.py
Normal file
211
backup-from-device/gnss-guard/tm-gnss-guard/server/models.py
Normal file
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SQLAlchemy and Pydantic models for GNSS Guard Server
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional
|
||||
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, ForeignKey, Text, Index
|
||||
from sqlalchemy.orm import relationship, declarative_base
|
||||
from pydantic import BaseModel, Field
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SQLAlchemy Database Models
|
||||
# =============================================================================
|
||||
|
||||
class Asset(Base):
|
||||
"""Asset (client device) registered with the server"""
|
||||
__tablename__ = "assets"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(255), unique=True, nullable=False, index=True)
|
||||
token_hash = Column(String(64), nullable=False) # SHA-256 hash of token
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
is_active = Column(Boolean, default=True)
|
||||
description = Column(String(500), nullable=True)
|
||||
|
||||
# Telegram notification settings (optional override for this asset)
|
||||
telegram_chat_id = Column(String(100), nullable=True) # Override default chat ID
|
||||
telegram_enabled = Column(Boolean, default=True) # Enable/disable notifications for this asset
|
||||
|
||||
# Relationship to validation history
|
||||
validations = relationship("ValidationHistory", back_populates="asset", cascade="all, delete-orphan")
|
||||
|
||||
# Relationship to notification state
|
||||
notification_state = relationship("AssetNotificationState", back_populates="asset", uselist=False, cascade="all, delete-orphan")
|
||||
|
||||
@staticmethod
|
||||
def hash_token(token: str) -> str:
|
||||
"""Hash a token using SHA-256"""
|
||||
return hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def generate_token() -> str:
|
||||
"""Generate a secure random token"""
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
def verify_token(self, token: str) -> bool:
|
||||
"""Verify if provided token matches stored hash"""
|
||||
return self.token_hash == self.hash_token(token)
|
||||
|
||||
|
||||
class AssetNotificationState(Base):
|
||||
"""Tracks the previous notification state for each asset to detect changes"""
|
||||
__tablename__ = "asset_notification_state"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
asset_id = Column(Integer, ForeignKey("assets.id", ondelete="CASCADE"), unique=True, nullable=False)
|
||||
|
||||
# Previous state (JSON arrays stored as text)
|
||||
prev_sources_missing = Column(Text, nullable=True) # JSON array
|
||||
prev_sources_stale = Column(Text, nullable=True) # JSON array
|
||||
prev_threshold_breached = Column(Boolean, default=False)
|
||||
|
||||
# Last notification timestamp
|
||||
last_notification_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Asset online/offline tracking
|
||||
is_online = Column(Boolean, default=True) # Whether asset is currently reporting
|
||||
last_validation_at = Column(DateTime, nullable=True) # Last time we received validation data
|
||||
|
||||
# Relationship
|
||||
asset = relationship("Asset", back_populates="notification_state")
|
||||
|
||||
|
||||
class ValidationHistory(Base):
|
||||
"""Historical validation records from assets"""
|
||||
__tablename__ = "validation_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
asset_id = Column(Integer, ForeignKey("assets.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
# Validation timestamps
|
||||
validation_timestamp = Column(String(50), nullable=False) # ISO format
|
||||
validation_timestamp_unix = Column(Float, nullable=False, index=True)
|
||||
|
||||
# Validation result
|
||||
is_valid = Column(Boolean, nullable=False)
|
||||
|
||||
# JSON fields stored as text
|
||||
sources_missing = Column(Text, nullable=True) # JSON array
|
||||
sources_stale = Column(Text, nullable=True) # JSON array
|
||||
coordinate_differences = Column(Text, nullable=True) # JSON object
|
||||
source_coordinates = Column(Text, nullable=True) # JSON object
|
||||
validation_details = Column(Text, nullable=True) # JSON object
|
||||
|
||||
# Server-side metadata
|
||||
received_at = Column(DateTime, default=datetime.utcnow, index=True)
|
||||
|
||||
# Relationship
|
||||
asset = relationship("Asset", back_populates="validations")
|
||||
|
||||
# Indexes for common queries
|
||||
__table_args__ = (
|
||||
Index('ix_validation_asset_timestamp', 'asset_id', 'validation_timestamp_unix'),
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Pydantic Request/Response Models
|
||||
# =============================================================================
|
||||
|
||||
class AssetCreate(BaseModel):
|
||||
"""Request model for creating a new asset"""
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
telegram_chat_id: Optional[str] = Field(None, max_length=100) # Override default chat ID
|
||||
telegram_enabled: bool = True # Enable notifications for this asset
|
||||
|
||||
|
||||
class AssetResponse(BaseModel):
|
||||
"""Response model for asset data"""
|
||||
id: int
|
||||
name: str
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
description: Optional[str] = None
|
||||
telegram_chat_id: Optional[str] = None
|
||||
telegram_enabled: bool = True
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AssetWithToken(AssetResponse):
|
||||
"""Response model for newly created asset (includes token)"""
|
||||
token: str # Only returned when asset is created
|
||||
|
||||
|
||||
class AssetImport(BaseModel):
|
||||
"""Request model for importing an asset with a specific token"""
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
token: str = Field(..., min_length=32, max_length=128)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
telegram_chat_id: Optional[str] = Field(None, max_length=100)
|
||||
telegram_enabled: bool = True
|
||||
|
||||
|
||||
class AssetBatchImport(BaseModel):
|
||||
"""Request model for batch importing assets"""
|
||||
assets: List[AssetImport]
|
||||
|
||||
|
||||
class ValidationSubmission(BaseModel):
|
||||
"""Request model for submitting validation data"""
|
||||
validation_timestamp: str
|
||||
validation_timestamp_unix: float
|
||||
is_valid: bool
|
||||
sources_missing: List[str] = []
|
||||
sources_stale: List[str] = []
|
||||
coordinate_differences: Dict[str, Any] = {}
|
||||
source_coordinates: Dict[str, Any] = {}
|
||||
validation_details: Dict[str, Any] = {}
|
||||
|
||||
|
||||
class ValidationBatchSubmission(BaseModel):
|
||||
"""Request model for submitting multiple validation records"""
|
||||
records: List[ValidationSubmission]
|
||||
|
||||
|
||||
class ValidationResponse(BaseModel):
|
||||
"""Response model for validation data"""
|
||||
id: int
|
||||
asset_name: str
|
||||
validation_timestamp: str
|
||||
validation_timestamp_unix: float
|
||||
is_valid: bool
|
||||
sources_missing: List[str]
|
||||
sources_stale: List[str]
|
||||
coordinate_differences: Dict[str, Any]
|
||||
source_coordinates: Dict[str, Any]
|
||||
validation_details: Dict[str, Any]
|
||||
received_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AssetStatus(BaseModel):
|
||||
"""Current status of an asset (latest validation)"""
|
||||
asset_name: str
|
||||
is_online: bool # Has reported in last 5 minutes
|
||||
last_seen: Optional[datetime] = None
|
||||
latest_validation: Optional[ValidationResponse] = None
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
"""Request model for user login"""
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
"""Response model for successful login"""
|
||||
message: str
|
||||
username: str
|
||||
|
||||
Reference in New Issue
Block a user