115 lines
3.5 KiB
Python
115 lines
3.5 KiB
Python
from __future__ import annotations
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile, status
|
|
from fastapi.responses import FileResponse
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.config import DATA_DIR, STORAGE_DIR
|
|
from app.database import Base, engine, get_db
|
|
from app.models import Archive
|
|
from app.schemas import ArchiveRead
|
|
from app.storage import delete_file, ensure_storage_dirs, resolve_path, save_upload
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_: FastAPI):
|
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
STORAGE_DIR.mkdir(parents=True, exist_ok=True)
|
|
Base.metadata.create_all(bind=engine)
|
|
ensure_storage_dirs()
|
|
yield
|
|
|
|
|
|
app = FastAPI(
|
|
title="Bottle Archive Server",
|
|
version="0.1.0",
|
|
description="Local archive server for exported Bottles backups.",
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
|
|
@app.get("/health")
|
|
def health() -> dict[str, str]:
|
|
return {"status": "ok"}
|
|
|
|
|
|
@app.get("/archives", response_model=list[ArchiveRead])
|
|
def list_archives(db: Session = Depends(get_db)) -> list[Archive]:
|
|
return list(db.scalars(select(Archive).order_by(Archive.created_at.desc())))
|
|
|
|
|
|
@app.post("/archives", response_model=ArchiveRead, status_code=status.HTTP_201_CREATED)
|
|
async def create_archive(
|
|
file: UploadFile = File(...),
|
|
name: str = Form(...),
|
|
bottle_name: str | None = Form(default=None),
|
|
description: str | None = Form(default=None),
|
|
tags: str | None = Form(default=None),
|
|
arch: str | None = Form(default=None),
|
|
runner: str | None = Form(default=None),
|
|
windows_version: str | None = Form(default=None),
|
|
db: Session = Depends(get_db),
|
|
) -> Archive:
|
|
if not file.filename:
|
|
raise HTTPException(status_code=400, detail="Uploaded file must have a filename.")
|
|
|
|
stored_name, size_bytes, digest = await save_upload(file)
|
|
|
|
archive = Archive(
|
|
name=name,
|
|
bottle_name=bottle_name,
|
|
description=description,
|
|
tags=tags,
|
|
arch=arch,
|
|
runner=runner,
|
|
windows_version=windows_version,
|
|
file_name=file.filename,
|
|
stored_name=stored_name,
|
|
content_type=file.content_type,
|
|
size_bytes=size_bytes,
|
|
sha256=digest,
|
|
)
|
|
db.add(archive)
|
|
db.commit()
|
|
db.refresh(archive)
|
|
return archive
|
|
|
|
|
|
@app.get("/archives/{archive_id}", response_model=ArchiveRead)
|
|
def get_archive(archive_id: int, db: Session = Depends(get_db)) -> Archive:
|
|
archive = db.get(Archive, archive_id)
|
|
if archive is None:
|
|
raise HTTPException(status_code=404, detail="Archive not found.")
|
|
return archive
|
|
|
|
|
|
@app.get("/archives/{archive_id}/download")
|
|
def download_archive(archive_id: int, db: Session = Depends(get_db)) -> FileResponse:
|
|
archive = db.get(Archive, archive_id)
|
|
if archive is None:
|
|
raise HTTPException(status_code=404, detail="Archive not found.")
|
|
|
|
path = resolve_path(archive.stored_name)
|
|
if not path.exists():
|
|
raise HTTPException(status_code=404, detail="Stored file not found.")
|
|
|
|
return FileResponse(
|
|
path=path,
|
|
media_type=archive.content_type or "application/octet-stream",
|
|
filename=archive.file_name,
|
|
)
|
|
|
|
|
|
@app.delete("/archives/{archive_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_archive(archive_id: int, db: Session = Depends(get_db)) -> None:
|
|
archive = db.get(Archive, archive_id)
|
|
if archive is None:
|
|
raise HTTPException(status_code=404, detail="Archive not found.")
|
|
|
|
delete_file(archive.stored_name)
|
|
db.delete(archive)
|
|
db.commit()
|