csg-dash-demo / backend /database.py
richtext's picture
Upload folder using huggingface_hub
3674b4b verified
"""
Database module with SQLCipher encryption support.
When DB_ENCRYPTION_KEY is set in the environment (or .env file), the database
will be encrypted using SQLCipher. If no key is provided, a standard SQLite
database is used (for development only).
"""
import os
from sqlalchemy import create_engine, event, text
from sqlalchemy.orm import sessionmaker, DeclarativeBase
from backend.config import settings
# Check if SQLCipher is available
SQLCIPHER_AVAILABLE = False
try:
import sqlcipher3
SQLCIPHER_AVAILABLE = True
except ImportError:
pass
def _get_db_path() -> str:
"""Extract the database file path from the database URL."""
import os
from pathlib import Path
# Check if running in HF Spaces (writable /data directory)
is_hf_spaces = os.path.exists("/data") and os.access("/data", os.W_OK)
if is_hf_spaces:
# Use HF Spaces persistent storage
db_path = "/data/crop_dashboard.db"
else:
# Parse from settings for local development
url = settings.database_url
if url.startswith("sqlite:///"):
db_path = url[10:] # Remove "sqlite:///"
if db_path.startswith("./"):
db_path = db_path[2:]
else:
db_path = url
# Ensure directory exists
db_dir = Path(db_path).parent
try:
db_dir.mkdir(parents=True, exist_ok=True)
except PermissionError:
pass # Directory may already exist
return db_path
def _create_encrypted_engine():
"""Create an engine using SQLCipher for encrypted database access."""
if not SQLCIPHER_AVAILABLE:
raise RuntimeError(
"SQLCipher encryption requested but sqlcipher3 is not installed. "
"Install with: pip install sqlcipher3"
)
db_path = _get_db_path()
key = settings.db_encryption_key
def create_connection():
"""Create a new SQLCipher connection."""
conn = sqlcipher3.connect(db_path, check_same_thread=False)
# Set the encryption key
conn.execute(f"PRAGMA key = '{key}'")
# Use SQLCipher 4 defaults for strong encryption
conn.execute("PRAGMA cipher_compatibility = 4")
conn.execute("PRAGMA kdf_iter = 256000")
conn.execute("PRAGMA cipher_memory_security = ON")
return conn
# Create engine with creator function
# Note: SQLite with creator uses StaticPool instead of QueuePool
from sqlalchemy.pool import StaticPool
engine = create_engine(
"sqlite://", # Dummy URL, we override with creator
creator=create_connection,
poolclass=StaticPool
)
return engine
def _create_standard_engine():
"""Create a standard SQLite engine (no encryption)."""
engine = create_engine(
settings.database_url,
connect_args={"check_same_thread": False}
)
return engine
def create_db_engine():
"""
Create the appropriate database engine based on configuration.
If db_encryption_key is set, uses SQLCipher for encryption.
Otherwise, uses standard SQLite.
"""
if settings.db_encryption_key:
print("Database encryption enabled (SQLCipher)")
return _create_encrypted_engine()
else:
print("WARNING: Database is NOT encrypted (no DB_ENCRYPTION_KEY set)")
return _create_standard_engine()
# Create the engine
engine = create_db_engine()
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Base(DeclarativeBase):
pass
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def init_db():
from backend.models import user, group, site, sensor_data, pipeline, box_connection
Base.metadata.create_all(bind=engine)
def is_database_encrypted(db_path: str | None = None) -> bool:
"""Check if the database file is encrypted."""
path = db_path or _get_db_path()
if not os.path.exists(path):
return False
with open(path, 'rb') as f:
header = f.read(16)
# SQLite files start with "SQLite format 3\0"
return header != b'SQLite format 3\x00'