You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

114 lines
3.5 KiB

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()