feat: Initial commit for OnEver Drive centralized backup system with Windows PyQt6 agent and Proxmox LXC deployment

This commit is contained in:
2026-08-13 19:33:44 -03:00
commit 1bfb808c79
77 changed files with 10675 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Header, Request, Query, UploadFile, File
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core.database import get_db
from app.models.models import Client, BackupSession
from app.schemas.schemas import (
UploadSessionInitRequest, UploadSessionInitResponse,
UploadSessionStatusResponse, ChunkUploadResponse,
UploadSessionCompleteResponse
)
from app.api.deps import get_current_client
from app.services.upload_service import (
create_or_resume_session, process_chunk_upload,
get_session_status_info, complete_session
)
router = APIRouter(prefix="/upload", tags=["Chunk Upload Engine"])
@router.post("/session", response_model=UploadSessionInitResponse)
async def init_session(
payload: UploadSessionInitRequest,
db: AsyncSession = Depends(get_db),
current_client: Client = Depends(get_current_client)
):
"""
Initializes a new upload session or resumes an existing incomplete session
for the specified file. Returns list of previously received chunks so the agent
only transmits the remaining blocks.
"""
session, received_chunks = await create_or_resume_session(
db=db,
client=current_client,
filename=payload.filename,
file_size=payload.file_size,
sha256_full=payload.sha256,
chunk_size=payload.chunk_size,
job_id=payload.job_id
)
return {
"session_code": session.session_code,
"filename": session.filename,
"file_size": session.file_size,
"chunk_size": session.chunk_size,
"total_chunks": session.total_chunks,
"received_chunks": received_chunks,
"status": session.status
}
@router.post("/{session_code}/chunk", response_model=ChunkUploadResponse)
async def upload_chunk(
session_code: str,
request: Request,
chunk_index: int = Query(..., description="0-indexed chunk number"),
chunk_sha256: Optional[str] = Query(None, description="SHA-256 hash of this specific chunk"),
x_chunk_index: Optional[int] = Header(None, alias="X-Chunk-Index"),
x_chunk_sha256: Optional[str] = Header(None, alias="X-Chunk-SHA256"),
db: AsyncSession = Depends(get_db),
current_client: Client = Depends(get_current_client)
):
"""
Receives and stores a single chunk of data for an active upload session.
Accepts raw binary body directly via streaming.
"""
idx = x_chunk_index if x_chunk_index is not None else chunk_index
sha = x_chunk_sha256 or chunk_sha256
res = await db.execute(
select(BackupSession).where(
BackupSession.session_code == session_code,
BackupSession.client_id == current_client.id
)
)
session = res.scalar_one_or_none()
if not session:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Upload session not found")
chunk_data = await request.body()
if not chunk_data:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Empty chunk payload")
try:
result = await process_chunk_upload(
db=db,
session=session,
chunk_index=idx,
chunk_data=chunk_data,
chunk_sha256=sha
)
return result
except ValueError as ex:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(ex))
@router.get("/{session_code}/status", response_model=UploadSessionStatusResponse)
async def get_session_status(
session_code: str,
db: AsyncSession = Depends(get_db),
current_client: Client = Depends(get_current_client)
):
"""
Returns the current status of an upload session, including the list of received
chunks and missing chunks for easy re-connection and resumption.
"""
res = await db.execute(
select(BackupSession).where(
BackupSession.session_code == session_code,
BackupSession.client_id == current_client.id
)
)
session = res.scalar_one_or_none()
if not session:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Upload session not found")
return await get_session_status_info(db, session)
@router.post("/{session_code}/complete", response_model=UploadSessionCompleteResponse)
async def complete_upload(
session_code: str,
db: AsyncSession = Depends(get_db),
current_client: Client = Depends(get_current_client)
):
"""
Triggers sequential file assembly and final SHA-256 integrity verification.
If integrity passes, the backup file is registered and retention policies are applied.
"""
res = await db.execute(
select(BackupSession).where(
BackupSession.session_code == session_code,
BackupSession.client_id == current_client.id
)
)
session = res.scalar_one_or_none()
if not session:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Upload session not found")
try:
backup_file = await complete_session(db, session)
return {
"session_code": session.session_code,
"filename": backup_file.filename,
"relative_path": backup_file.relative_path,
"file_size": backup_file.file_size,
"sha256": backup_file.sha256,
"status": "SUCCESS",
"completed_at": backup_file.created_at
}
except Exception as ex:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Integrity check or assembly failed: {str(ex)}"
)