Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ build/
dist/
.venv

# for localdev
/localdata/*
!localdata/

.vscode/
*.swp
*.swo
Expand Down
5 changes: 4 additions & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ db-migrate:
db-upgrade:
alembic upgrade head

start:
uv run uvicorn main:app --reload

spec:
mkdir -p dist/
python3 -m app.scripts.save_openapi

generate-fixtures:
python3 -m app.scripts.fixtures
python3 -m app.scripts.fixtures
4 changes: 3 additions & 1 deletion backend/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from app.db import engine, sync_db_connection_context, db
from fastapi.middleware.cors import CORSMiddleware
from app.inventory.routes import router as inventory_router
from app.files.routes import router as files_router
from app.extensions.all_models import *

app = FastAPI()
Expand All @@ -31,6 +32,7 @@ async def db_session_middleware(request: Request, call_next):
return response

app.include_router(inventory_router)
app.include_router(files_router)


class UserCreateSchema(BaseModel):
Expand All @@ -52,4 +54,4 @@ async def create_user(body: UserCreateSchema) -> UserDumpSchema:
return UserDumpSchema(
id=user.id,
username=user.username,
)
)
13 changes: 8 additions & 5 deletions backend/app/files/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from app.db.base import Base
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import Integer, Text, Numeric
from sqlalchemy import Integer, Text


class File(Base):
__tablename__ = "files"

id: Mapped[int] = mapped_column(
Integer,
primary_key=True
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)

url: Mapped[str] = mapped_column(Text)

filename: Mapped[str] = mapped_column(Text)
content_type: Mapped[str] = mapped_column(Text)
64 changes: 64 additions & 0 deletions backend/app/files/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from app.files.models import File
from app.db import db
from sqlalchemy import insert
from uuid import uuid4
from fastapi import APIRouter, UploadFile, File as FastAPIFile
from app.files.schemas import FileDumpSchema
from app.utils.strings import make_slug
from app.utils.db_helpers import exec_scalar

router = APIRouter(
prefix="/file",
responses={404: {"description": "Not found"}},
)


@router.post("/upload/")
async def upload_single_file(file: UploadFile = FastAPIFile(...)) -> FileDumpSchema:
satis_file = handle_file_upload(
contents=await file.read(),
filename=file.filename,
content_type=file.content_type,
)
return FileDumpSchema(
id=satis_file.id,
url=satis_file.url,
filename=satis_file.filename,
)


def save_file(
contents: bytes,
filename: str,
) -> str:
url = f"localdata/{make_slug()}-{filename}"
with open(url, "wb") as f:
f.write(contents)

return url


def handle_file_upload(
contents: bytes, filename: str | None, content_type: str | None
) -> File:
resolved_filename = filename if filename is not None else f"{make_slug()}-upload"
file_url = save_file(
contents=contents,
filename=resolved_filename,
)

file = exec_scalar(
insert(File)
.values(
[
{
"url": file_url,
"filename": resolved_filename,
"content_type": content_type or "UNKNOWN",
}
]
)
.returning(File)
)

return file
4 changes: 4 additions & 0 deletions backend/app/files/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from pydantic import BaseModel

class FileDumpSchema(BaseModel):
id: int
Empty file added backend/app/utils/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions backend/app/utils/db_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from sqlalchemy.sql.dml import ReturningInsert
from sqlalchemy import Select
from app.db import db


def exec_scalars[T](statement: Select[tuple[T]] | ReturningInsert[tuple[T]]) -> list[T]:
return list(db.execute(statement).scalars().all())


def exec_scalar[T](statement: Select[tuple[T]] | ReturningInsert[tuple[T]]) -> T:
return db.execute(statement).scalars().one()
6 changes: 6 additions & 0 deletions backend/app/utils/strings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from uuid import uuid4


def make_slug(length: int | None = None):
resolved_length = length if length is not None else 6
return uuid4().hex[:resolved_length]
32 changes: 32 additions & 0 deletions backend/migrations/versions/11c780da6347_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""empty message

Revision ID: 11c780da6347
Revises: e284a1681385
Create Date: 2026-07-19 03:53:26.383878

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '11c780da6347'
down_revision: Union[str, Sequence[str], None] = 'e284a1681385'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('files', sa.Column('url', sa.Text(), nullable=False))
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('files', 'url')
# ### end Alembic commands ###
34 changes: 34 additions & 0 deletions backend/migrations/versions/af7ae6368a55_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""empty message

Revision ID: af7ae6368a55
Revises: 11c780da6347
Create Date: 2026-07-19 04:20:09.409024

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'af7ae6368a55'
down_revision: Union[str, Sequence[str], None] = '11c780da6347'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('files', sa.Column('filename', sa.Text(), nullable=False))
op.add_column('files', sa.Column('content_type', sa.Text(), nullable=False))
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('files', 'content_type')
op.drop_column('files', 'filename')
# ### end Alembic commands ###
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "backend"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"
requires-python = ">=3.14"
dependencies = [
"alembic>=1.18.5",
"fastapi[standard]>=0.139.0",
Expand Down
Loading