se agregaron nuevas caracteristicas
This commit is contained in:
+133
-20
@@ -10,7 +10,8 @@ from app.core.security import generate_registration_code, generate_device_token,
|
||||
from app.models.models import User, Client, ClientCredential, RegistrationCode
|
||||
from app.schemas.schemas import (
|
||||
ClientResponse, ClientRegisterRequest, ClientRegisterResponse,
|
||||
RegistrationCodeCreate, RegistrationCodeResponse, ClientHeartbeatRequest
|
||||
RegistrationCodeCreate, RegistrationCodeResponse, ClientHeartbeatRequest,
|
||||
ClientUpdateRequest
|
||||
)
|
||||
from app.api.deps import get_current_user, require_admin, get_current_client
|
||||
from app.services.event_service import log_event
|
||||
@@ -78,27 +79,55 @@ async def register_client(
|
||||
detail="Invalid, expired, or already used registration code."
|
||||
)
|
||||
|
||||
# Determine next client code (e.g., CLIENT-0001)
|
||||
count_res = await db.execute(select(func.count(Client.id)))
|
||||
client_count = count_res.scalar() or 0
|
||||
client_code = f"CLIENT-{client_count + 1:04d}"
|
||||
|
||||
client_ip = request.client.host if request.client else None
|
||||
|
||||
# Create Client
|
||||
client = Client(
|
||||
client_code=client_code,
|
||||
name=payload.name or reg_code.client_name_hint or payload.hostname,
|
||||
hostname=payload.hostname,
|
||||
os_info=payload.os_info,
|
||||
ip_address=client_ip,
|
||||
agent_version=payload.agent_version,
|
||||
status="ONLINE",
|
||||
last_seen_at=now,
|
||||
is_active=True
|
||||
)
|
||||
db.add(client)
|
||||
await db.flush()
|
||||
if reg_code.client_id:
|
||||
# Re-registering an existing client!
|
||||
client_res = await db.execute(select(Client).where(Client.id == reg_code.client_id))
|
||||
client = client_res.scalar_one_or_none()
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Client associated with this registration code not found."
|
||||
)
|
||||
# Update connection info and mark as active
|
||||
client.hostname = payload.hostname
|
||||
client.os_info = payload.os_info
|
||||
client.ip_address = client_ip
|
||||
client.agent_version = payload.agent_version
|
||||
client.status = "ONLINE"
|
||||
client.last_seen_at = now
|
||||
client.is_active = True
|
||||
else:
|
||||
# Create a new Client
|
||||
max_id_res = await db.execute(select(func.max(Client.id)))
|
||||
max_id = max_id_res.scalar() or 0
|
||||
client_code = f"CLIENT-{max_id + 1:04d}"
|
||||
|
||||
# Load default client quota from settings
|
||||
from app.services.settings_service import get_setting
|
||||
default_quota_gb_str = await get_setting(db, "default_client_quota_gb")
|
||||
default_quota_bytes = 100 * 1024 * 1024 * 1024
|
||||
if default_quota_gb_str:
|
||||
try:
|
||||
default_quota_bytes = int(default_quota_gb_str) * 1024 * 1024 * 1024
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
client = Client(
|
||||
client_code=client_code,
|
||||
name=payload.name or reg_code.client_name_hint or payload.hostname,
|
||||
hostname=payload.hostname,
|
||||
os_info=payload.os_info,
|
||||
ip_address=client_ip,
|
||||
agent_version=payload.agent_version,
|
||||
status="ONLINE",
|
||||
last_seen_at=now,
|
||||
is_active=True,
|
||||
storage_quota_bytes=default_quota_bytes
|
||||
)
|
||||
db.add(client)
|
||||
await db.flush()
|
||||
|
||||
# Generate device unique ID and secret token
|
||||
device_id = str(uuid.uuid4())
|
||||
@@ -220,6 +249,90 @@ async def revoke_client_credentials(
|
||||
|
||||
return {"message": "Client credentials successfully revoked"}
|
||||
|
||||
@router.post("/{client_id}/re-register", response_model=RegistrationCodeResponse)
|
||||
async def generate_client_re_registration_code(
|
||||
client_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Generates a registration code to re-link an existing client's agent."""
|
||||
client_res = await db.execute(select(Client).where(Client.id == client_id))
|
||||
client = client_res.scalar_one_or_none()
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Client not found"
|
||||
)
|
||||
|
||||
# Revoke old credentials to prepare for the new agent connection
|
||||
cred_res = await db.execute(select(ClientCredential).where(ClientCredential.client_id == client_id))
|
||||
creds = cred_res.scalars().all()
|
||||
for cred in creds:
|
||||
cred.is_revoked = True
|
||||
|
||||
code_str = generate_registration_code()
|
||||
expires = datetime.now(timezone.utc) + timedelta(hours=24) # 24 hours to re-register
|
||||
|
||||
reg_code = RegistrationCode(
|
||||
code=code_str,
|
||||
client_id=client.id,
|
||||
client_name_hint=client.name,
|
||||
expires_at=expires,
|
||||
is_used=False
|
||||
)
|
||||
db.add(reg_code)
|
||||
await db.commit()
|
||||
await db.refresh(reg_code)
|
||||
|
||||
await log_event(
|
||||
db=db,
|
||||
event_type="REGISTRATION_CODE_GENERATED",
|
||||
message=f"Generated re-registration code {code_str} for client {client.name} ({client.client_code}).",
|
||||
severity="INFO",
|
||||
client_id=client_id,
|
||||
user_email=admin_user.email
|
||||
)
|
||||
|
||||
return reg_code
|
||||
|
||||
@router.patch("/{client_id}", response_model=ClientResponse)
|
||||
async def update_client(
|
||||
client_id: int,
|
||||
payload: ClientUpdateRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Updates client details such as alias."""
|
||||
res = await db.execute(select(Client).where(Client.id == client_id))
|
||||
client = res.scalar_one_or_none()
|
||||
if not client:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Client not found")
|
||||
|
||||
if payload.alias is not None:
|
||||
client.alias = payload.alias
|
||||
if payload.storage_quota_bytes is not None:
|
||||
client.storage_quota_bytes = payload.storage_quota_bytes
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(client)
|
||||
|
||||
message_parts = []
|
||||
if payload.alias is not None:
|
||||
message_parts.append(f"alias to '{client.alias}'")
|
||||
if payload.storage_quota_bytes is not None:
|
||||
message_parts.append(f"quota to {client.storage_quota_bytes} bytes")
|
||||
|
||||
await log_event(
|
||||
db=db,
|
||||
event_type="CLIENT_UPDATED",
|
||||
message=f"Updated client {client.client_code}: {', '.join(message_parts)}.",
|
||||
severity="INFO",
|
||||
client_id=client.id,
|
||||
user_email=admin_user.email
|
||||
)
|
||||
|
||||
return client
|
||||
|
||||
@router.delete("/{client_id}")
|
||||
async def delete_client(
|
||||
client_id: int,
|
||||
|
||||
+69
-1
@@ -6,7 +6,7 @@ from sqlalchemy import select, func, desc
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.models import User, Client, BackupJob
|
||||
from app.schemas.schemas import JobCreate, JobUpdate, JobResponse
|
||||
from app.schemas.schemas import JobCreate, JobUpdate, JobResponse, AgentJobRegister
|
||||
from app.api.deps import get_current_user, require_admin, get_current_client
|
||||
from app.services.event_service import log_event
|
||||
from app.ws.manager import ws_manager
|
||||
@@ -36,6 +36,74 @@ async def get_agent_jobs(
|
||||
result = await db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@router.post("/agent/register", response_model=JobResponse)
|
||||
async def agent_register_job(
|
||||
payload: AgentJobRegister,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_client: Client = Depends(get_current_client)
|
||||
):
|
||||
"""Called by the Windows Agent to register a new local job/folder on the server."""
|
||||
count_res = await db.execute(select(func.count(BackupJob.id)))
|
||||
job_count = count_res.scalar() or 0
|
||||
job_code = f"JOB-{job_count + 1:03d}"
|
||||
|
||||
job = BackupJob(
|
||||
job_code=job_code,
|
||||
client_id=current_client.id,
|
||||
name=payload.name,
|
||||
source_path=payload.source_path,
|
||||
file_patterns=payload.file_patterns,
|
||||
schedule_cron=payload.schedule_cron,
|
||||
keep_daily=payload.keep_daily,
|
||||
keep_weekly=payload.keep_weekly,
|
||||
keep_monthly=payload.keep_monthly,
|
||||
min_stable_time_seconds=payload.min_stable_time_seconds,
|
||||
status="IDLE",
|
||||
is_active=True
|
||||
)
|
||||
db.add(job)
|
||||
await db.commit()
|
||||
await db.refresh(job)
|
||||
|
||||
await log_event(
|
||||
db=db,
|
||||
event_type="JOB_CREATED",
|
||||
message=f"Agent registered backup job '{job.name}' ({job.job_code}) from device.",
|
||||
severity="INFO",
|
||||
client_id=current_client.id,
|
||||
job_id=job.id
|
||||
)
|
||||
|
||||
return job
|
||||
|
||||
@router.delete("/agent/{job_id}")
|
||||
async def agent_delete_job(
|
||||
job_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_client: Client = Depends(get_current_client)
|
||||
):
|
||||
"""Called by the Windows Agent to delete a backup job it owns."""
|
||||
res = await db.execute(select(BackupJob).where(BackupJob.id == job_id, BackupJob.client_id == current_client.id))
|
||||
job = res.scalar_one_or_none()
|
||||
if not job:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Job not found or not owned by this client"
|
||||
)
|
||||
|
||||
await db.delete(job)
|
||||
await db.commit()
|
||||
|
||||
await log_event(
|
||||
db=db,
|
||||
event_type="JOB_DELETED",
|
||||
message=f"Agent deleted backup job {job.job_code} from device.",
|
||||
severity="WARNING",
|
||||
client_id=current_client.id
|
||||
)
|
||||
|
||||
return {"message": "Job successfully deleted by agent"}
|
||||
|
||||
@router.post("", response_model=JobResponse)
|
||||
async def create_job(
|
||||
payload: JobCreate,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.database import get_db
|
||||
from app.api.deps import require_admin
|
||||
from app.models.models import User
|
||||
from app.schemas.schemas import SystemSettingsResponse, SystemSettingsUpdate
|
||||
from app.services.settings_service import get_all_settings, update_settings_service
|
||||
|
||||
router = APIRouter(prefix="/settings", tags=["System Settings"])
|
||||
|
||||
@router.get("", response_model=SystemSettingsResponse)
|
||||
async def get_settings(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Retrieve global system settings."""
|
||||
settings_dict = await get_all_settings(db)
|
||||
return {
|
||||
"storage_root": settings_dict.get("storage_root", ""),
|
||||
"global_quota_gb": int(settings_dict.get("global_quota_gb", 1000)),
|
||||
"default_client_quota_gb": int(settings_dict.get("default_client_quota_gb", 100)),
|
||||
"default_keep_daily": int(settings_dict.get("default_keep_daily", 7)),
|
||||
"default_keep_weekly": int(settings_dict.get("default_keep_weekly", 4)),
|
||||
"default_keep_monthly": int(settings_dict.get("default_keep_monthly", 12)),
|
||||
}
|
||||
|
||||
@router.put("", response_model=SystemSettingsResponse)
|
||||
async def update_settings(
|
||||
payload: SystemSettingsUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin_user: User = Depends(require_admin)
|
||||
):
|
||||
"""Update global system settings."""
|
||||
# Convert Pydantic model to a dict of values (filtering out None)
|
||||
updates = {k: v for k, v in payload.model_dump().items() if v is not None}
|
||||
|
||||
try:
|
||||
settings_dict = await update_settings_service(db, updates)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
return {
|
||||
"storage_root": settings_dict.get("storage_root", ""),
|
||||
"global_quota_gb": int(settings_dict.get("global_quota_gb", 1000)),
|
||||
"default_client_quota_gb": int(settings_dict.get("default_client_quota_gb", 100)),
|
||||
"default_keep_daily": int(settings_dict.get("default_keep_daily", 7)),
|
||||
"default_keep_weekly": int(settings_dict.get("default_keep_weekly", 4)),
|
||||
"default_keep_monthly": int(settings_dict.get("default_keep_monthly", 12)),
|
||||
}
|
||||
@@ -67,6 +67,23 @@ async def get_dashboard_stats(
|
||||
|
||||
# Storage metrics
|
||||
storage_stats = await storage_provider.get_storage_stats()
|
||||
|
||||
# Override with global virtual quota if set
|
||||
from app.services.settings_service import get_setting
|
||||
global_quota_gb_str = await get_setting(db, "global_quota_gb")
|
||||
if global_quota_gb_str:
|
||||
try:
|
||||
global_quota_bytes = int(global_quota_gb_str) * 1024 * 1024 * 1024
|
||||
# Sum up storage used by all clients
|
||||
used_res = await db.execute(select(func.sum(Client.storage_used_bytes)))
|
||||
total_used_bytes = used_res.scalar() or 0
|
||||
|
||||
storage_stats["total_bytes"] = global_quota_bytes
|
||||
storage_stats["used_bytes"] = total_used_bytes
|
||||
storage_stats["free_bytes"] = max(0, global_quota_bytes - total_used_bytes)
|
||||
storage_stats["usage_percent"] = round((total_used_bytes / global_quota_bytes) * 100, 2) if global_quota_bytes > 0 else 0
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"total_clients": total_clients,
|
||||
|
||||
+17
-1
@@ -14,6 +14,7 @@ from app.api.upload import router as upload_router
|
||||
from app.api.backups import router as backups_router
|
||||
from app.api.events import router as events_router
|
||||
from app.api.stats import router as stats_router
|
||||
from app.api.settings import router as settings_router
|
||||
from app.ws.manager import ws_manager
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -21,8 +22,22 @@ async def lifespan(app: FastAPI):
|
||||
# Initialize database tables
|
||||
await init_db()
|
||||
|
||||
# Seed default administrator if not present
|
||||
# Seed default administrator and initialize settings
|
||||
async with AsyncSessionLocal() as session:
|
||||
# Load and apply settings to storage_provider
|
||||
from app.services.settings_service import get_all_settings
|
||||
settings_dict = await get_all_settings(session)
|
||||
|
||||
# Apply storage_root
|
||||
from app.storage.local import storage_provider
|
||||
from pathlib import Path
|
||||
import os
|
||||
storage_root = settings_dict.get("storage_root")
|
||||
if storage_root:
|
||||
path = Path(storage_root).resolve()
|
||||
os.makedirs(path, exist_ok=True)
|
||||
storage_provider.root_dir = path
|
||||
|
||||
result = await session.execute(select(User))
|
||||
admin = result.scalar_one_or_none()
|
||||
if not admin:
|
||||
@@ -63,6 +78,7 @@ app.include_router(upload_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(backups_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(events_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(stats_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(settings_router, prefix=settings.API_V1_PREFIX)
|
||||
|
||||
@app.websocket("/ws/telemetry")
|
||||
async def websocket_telemetry(websocket: WebSocket):
|
||||
|
||||
@@ -27,6 +27,7 @@ class Client(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
client_code = Column(String(50), unique=True, index=True, nullable=False) # e.g., CLIENT-0001
|
||||
name = Column(String(255), nullable=False)
|
||||
alias = Column(String(255), nullable=True)
|
||||
hostname = Column(String(255), nullable=True)
|
||||
os_info = Column(String(255), nullable=True)
|
||||
ip_address = Column(String(100), nullable=True)
|
||||
@@ -65,6 +66,7 @@ class RegistrationCode(Base):
|
||||
code = Column(String(50), unique=True, index=True, nullable=False) # e.g., OED-A1B2-C3D4
|
||||
is_used = Column(Boolean, default=False, nullable=False)
|
||||
client_name_hint = Column(String(255), nullable=True)
|
||||
client_id = Column(Integer, ForeignKey("clients.id"), nullable=True)
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), default=utc_now, nullable=False)
|
||||
|
||||
@@ -172,3 +174,9 @@ class EventLog(Base):
|
||||
ip_address = Column(String(100), nullable=True)
|
||||
message = Column(Text, nullable=False)
|
||||
details_json = Column(Text, nullable=True)
|
||||
|
||||
class SystemSetting(Base):
|
||||
__tablename__ = "system_settings"
|
||||
|
||||
key = Column(String(100), primary_key=True, index=True)
|
||||
value = Column(String(1024), nullable=False)
|
||||
|
||||
@@ -28,9 +28,11 @@ class RegistrationCodeCreate(BaseModel):
|
||||
expires_in_hours: int = 48
|
||||
|
||||
class RegistrationCodeResponse(BaseModel):
|
||||
id: int
|
||||
code: str
|
||||
expires_at: datetime
|
||||
client_name_hint: Optional[str]
|
||||
client_id: Optional[int] = None
|
||||
|
||||
class ClientRegisterRequest(BaseModel):
|
||||
registration_code: str
|
||||
@@ -51,10 +53,15 @@ class ClientHeartbeatRequest(BaseModel):
|
||||
agent_version: Optional[str] = None
|
||||
ip_address: Optional[str] = None
|
||||
|
||||
class ClientUpdateRequest(BaseModel):
|
||||
alias: Optional[str] = None
|
||||
storage_quota_bytes: Optional[int] = None
|
||||
|
||||
class ClientResponse(BaseModel):
|
||||
id: int
|
||||
client_code: str
|
||||
name: str
|
||||
alias: Optional[str]
|
||||
hostname: Optional[str]
|
||||
os_info: Optional[str]
|
||||
ip_address: Optional[str]
|
||||
@@ -205,3 +212,30 @@ class DashboardStatsResponse(BaseModel):
|
||||
backups_today_failed: int
|
||||
active_uploads_count: int
|
||||
storage: StorageStatsResponse
|
||||
|
||||
# --- System Settings Schemas ---
|
||||
class SystemSettingsResponse(BaseModel):
|
||||
storage_root: str
|
||||
global_quota_gb: int
|
||||
default_client_quota_gb: int
|
||||
default_keep_daily: int
|
||||
default_keep_weekly: int
|
||||
default_keep_monthly: int
|
||||
|
||||
class SystemSettingsUpdate(BaseModel):
|
||||
storage_root: Optional[str] = None
|
||||
global_quota_gb: Optional[int] = None
|
||||
default_client_quota_gb: Optional[int] = None
|
||||
default_keep_daily: Optional[int] = None
|
||||
default_keep_weekly: Optional[int] = None
|
||||
default_keep_monthly: Optional[int] = None
|
||||
|
||||
class AgentJobRegister(BaseModel):
|
||||
name: str
|
||||
source_path: str
|
||||
file_patterns: str = "*.bak,*.mdf"
|
||||
schedule_cron: str = "daily"
|
||||
keep_daily: int = 7
|
||||
keep_weekly: int = 4
|
||||
keep_monthly: int = 12
|
||||
min_stable_time_seconds: int = 60
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.models.models import SystemSetting
|
||||
from app.core.config import settings
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
DEFAULT_SETTINGS = {
|
||||
"storage_root": settings.STORAGE_ROOT,
|
||||
"global_quota_gb": "1000",
|
||||
"default_client_quota_gb": "100",
|
||||
"default_keep_daily": str(settings.DEFAULT_RETENTION_DAILY),
|
||||
"default_keep_weekly": str(settings.DEFAULT_RETENTION_WEEKLY),
|
||||
"default_keep_monthly": str(settings.DEFAULT_RETENTION_MONTHLY),
|
||||
}
|
||||
|
||||
async def get_setting(db: AsyncSession, key: str) -> str:
|
||||
result = await db.execute(select(SystemSetting).where(SystemSetting.key == key))
|
||||
setting = result.scalar_one_or_none()
|
||||
if setting:
|
||||
return setting.value
|
||||
return DEFAULT_SETTINGS.get(key, "")
|
||||
|
||||
async def get_all_settings(db: AsyncSession) -> dict:
|
||||
result = await db.execute(select(SystemSetting))
|
||||
db_settings = {s.key: s.value for s in result.scalars().all()}
|
||||
|
||||
# Merge defaults and add them if not present in db
|
||||
updated = False
|
||||
for k, v in DEFAULT_SETTINGS.items():
|
||||
if k not in db_settings:
|
||||
db_settings[k] = v
|
||||
db.add(SystemSetting(key=k, value=v))
|
||||
updated = True
|
||||
if updated:
|
||||
await db.commit()
|
||||
return db_settings
|
||||
|
||||
async def update_settings_service(db: AsyncSession, new_settings: dict) -> dict:
|
||||
from app.storage.local import storage_provider
|
||||
|
||||
for k, v in new_settings.items():
|
||||
if k in DEFAULT_SETTINGS and v is not None:
|
||||
# If changing storage root, validate and apply
|
||||
if k == "storage_root":
|
||||
path = Path(v).resolve()
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
# Test write access
|
||||
test_file = path / ".write_test"
|
||||
test_file.touch()
|
||||
test_file.unlink()
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid or unwritable storage root path: {str(e)}")
|
||||
|
||||
# Apply to in-memory storage provider
|
||||
storage_provider.root_dir = path
|
||||
|
||||
result = await db.execute(select(SystemSetting).where(SystemSetting.key == k))
|
||||
setting = result.scalar_one_or_none()
|
||||
if setting:
|
||||
setting.value = str(v)
|
||||
else:
|
||||
db.add(SystemSetting(key=k, value=str(v)))
|
||||
|
||||
await db.commit()
|
||||
return await get_all_settings(db)
|
||||
@@ -24,6 +24,28 @@ async def create_or_resume_session(
|
||||
"""
|
||||
total_chunks = max(1, math.ceil(file_size / chunk_size))
|
||||
|
||||
# 1. Check client quota
|
||||
if client.storage_used_bytes + file_size > client.storage_quota_bytes:
|
||||
raise ValueError(
|
||||
f"Client storage quota exceeded. Limit: {client.storage_quota_bytes} bytes. Attempted to upload: {file_size} bytes."
|
||||
)
|
||||
|
||||
# 2. Check global quota
|
||||
from app.services.settings_service import get_setting
|
||||
from sqlalchemy import func
|
||||
global_quota_gb_str = await get_setting(db, "global_quota_gb")
|
||||
if global_quota_gb_str:
|
||||
try:
|
||||
global_quota_bytes = int(global_quota_gb_str) * 1024 * 1024 * 1024
|
||||
used_res = await db.execute(select(func.sum(Client.storage_used_bytes)))
|
||||
total_used_bytes = used_res.scalar() or 0
|
||||
if total_used_bytes + file_size > global_quota_bytes:
|
||||
raise ValueError(
|
||||
f"Global storage quota exceeded. Limit: {global_quota_bytes} bytes. Current used: {total_used_bytes} bytes."
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Check for existing incomplete session for this client and file hash
|
||||
query = (
|
||||
select(BackupSession)
|
||||
@@ -193,6 +215,8 @@ async def complete_session(
|
||||
job = job_res.scalar_one_or_none()
|
||||
if job:
|
||||
job_code = job.job_code
|
||||
job.status = "RUNNING"
|
||||
job.last_run_at = datetime.now(timezone.utc)
|
||||
|
||||
session.status = "ASSEMBLING"
|
||||
await db.commit()
|
||||
@@ -230,6 +254,12 @@ async def complete_session(
|
||||
client.storage_used_bytes += total_bytes
|
||||
client.last_backup_at = datetime.now(timezone.utc)
|
||||
|
||||
if session.job_id:
|
||||
job_res = await db.execute(select(BackupJob).where(BackupJob.id == session.job_id))
|
||||
job = job_res.scalar_one_or_none()
|
||||
if job:
|
||||
job.status = "SUCCESS"
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(backup_file)
|
||||
|
||||
@@ -263,6 +293,16 @@ async def complete_session(
|
||||
except Exception as ex:
|
||||
session.status = "FAILED"
|
||||
session.error_message = str(ex)
|
||||
|
||||
if session.job_id:
|
||||
try:
|
||||
job_res = await db.execute(select(BackupJob).where(BackupJob.id == session.job_id))
|
||||
job = job_res.scalar_one_or_none()
|
||||
if job:
|
||||
job.status = "FAILED"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await db.commit()
|
||||
|
||||
await log_event(
|
||||
|
||||
Reference in New Issue
Block a user