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