feat: Initial commit for OnEver Drive centralized backup system with Windows PyQt6 agent and Proxmox LXC deployment
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
class Settings(BaseSettings):
|
||||
PROJECT_NAME: str = "OnEver Drive"
|
||||
VERSION: str = "1.0.0"
|
||||
API_V1_PREFIX: str = "/api"
|
||||
|
||||
# Environment
|
||||
ENVIRONMENT: str = "development"
|
||||
DEBUG: bool = True
|
||||
|
||||
# Database: Default to SQLite for seamless local dev & test, configurable to PostgreSQL in production
|
||||
DATABASE_URL: str = f"sqlite+aiosqlite:///{BASE_DIR}/onever_drive.db"
|
||||
|
||||
# Storage settings
|
||||
STORAGE_ROOT: str = str(BASE_DIR / "storage" / "backups")
|
||||
STORAGE_TEMP_ROOT: str = str(BASE_DIR / "storage" / "temp")
|
||||
|
||||
# Security
|
||||
SECRET_KEY: str = "onever-drive-super-secret-key-change-in-production-2026"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 days
|
||||
|
||||
# Upload & Transfer settings
|
||||
DEFAULT_CHUNK_SIZE: int = 4 * 1024 * 1024 # 4 MB
|
||||
MAX_CHUNK_SIZE: int = 16 * 1024 * 1024 # 16 MB
|
||||
MIN_STABLE_TIME_SECONDS: int = 60 # 60s stability window for locked files
|
||||
|
||||
# Retention Defaults
|
||||
DEFAULT_RETENTION_DAILY: int = 7
|
||||
DEFAULT_RETENTION_WEEKLY: int = 4
|
||||
DEFAULT_RETENTION_MONTHLY: int = 12
|
||||
|
||||
model_config = {"env_file": ".env", "extra": "allow"}
|
||||
|
||||
settings = Settings()
|
||||
|
||||
# Ensure storage directories exist
|
||||
os.makedirs(settings.STORAGE_ROOT, exist_ok=True)
|
||||
os.makedirs(settings.STORAGE_TEMP_ROOT, exist_ok=True)
|
||||
@@ -0,0 +1,34 @@
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from app.core.config import settings
|
||||
|
||||
# Configure async engine
|
||||
engine = create_async_engine(
|
||||
settings.DATABASE_URL,
|
||||
echo=False,
|
||||
future=True,
|
||||
# SQLite-specific optimization if running SQLite
|
||||
connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {}
|
||||
)
|
||||
|
||||
AsyncSessionLocal = async_sessionmaker(
|
||||
bind=engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autoflush=False
|
||||
)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
async def get_db():
|
||||
"""FastAPI dependency for obtaining async database sessions."""
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
async def init_db():
|
||||
"""Initializes database tables if they do not already exist."""
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
@@ -0,0 +1,57 @@
|
||||
import secrets
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Any
|
||||
import jwt
|
||||
import bcrypt
|
||||
from app.core.config import settings
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""Hashes a password with bcrypt."""
|
||||
salt = bcrypt.gensalt()
|
||||
return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""Verifies a plain password against a bcrypt hash."""
|
||||
try:
|
||||
return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8"))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def create_access_token(subject: Any, role: str = "ADMIN", expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""Creates a JWT access token for authentication."""
|
||||
if expires_delta:
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
else:
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
|
||||
to_encode = {
|
||||
"exp": expire,
|
||||
"sub": str(subject),
|
||||
"role": role,
|
||||
"iat": datetime.now(timezone.utc)
|
||||
}
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
def decode_access_token(token: str) -> Optional[dict]:
|
||||
"""Decodes and validates a JWT token."""
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
return payload
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def generate_registration_code() -> str:
|
||||
"""Generates an agent registration code formatted as OED-XXXX-XXXX."""
|
||||
part1 = secrets.token_hex(2).upper()
|
||||
part2 = secrets.token_hex(2).upper()
|
||||
return f"OED-{part1}-{part2}"
|
||||
|
||||
def generate_device_token() -> str:
|
||||
"""Generates a high-entropy secret token for an agent device."""
|
||||
return f"oed_sec_{secrets.token_urlsafe(32)}"
|
||||
|
||||
def hash_token(token: str) -> str:
|
||||
"""Returns the SHA-256 hex digest of a token for secure database lookup."""
|
||||
return hashlib.sha256(token.encode("utf-8")).hexdigest()
|
||||
Reference in New Issue
Block a user