feat: scaffold FastAPI app, config, and SQLite models

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
肉末
2026-07-14 12:40:26 +08:00
parent 5b018bf4f2
commit 4d3cd2ac8b
6 changed files with 123 additions and 0 deletions
View File
+33
View File
@@ -0,0 +1,33 @@
from pathlib import Path
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
populate_by_name=True,
extra="ignore",
)
data_dir: Path = Field(default=Path("/data"), alias="DATA_DIR")
admin_password: str = Field(default="changeme", alias="ADMIN_PASSWORD")
secret_key: str = Field(default="dev-secret-change-me", alias="SECRET_KEY")
max_upload_mb: int = Field(default=200, alias="MAX_UPLOAD_MB")
rate_limit_per_second: int = Field(default=2, alias="RATE_LIMIT_PER_SECOND")
jwt_expire_hours: int = Field(default=12, alias="JWT_EXPIRE_HOURS")
host: str = Field(default="0.0.0.0", alias="HOST")
port: int = Field(default=8080, alias="PORT")
@property
def db_url(self) -> str:
return f"sqlite:///{self.data_dir / 'mincedpad.db'}"
@property
def uploads_dir(self) -> Path:
return self.data_dir / "uploads"
settings = Settings()
+24
View File
@@ -0,0 +1,24 @@
from collections.abc import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
from app.config import settings
engine = create_engine(
settings.db_url,
connect_args={"check_same_thread": False},
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Base(DeclarativeBase):
pass
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()
+23
View File
@@ -0,0 +1,23 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.config import settings
from app.db import Base, engine
from app import models # noqa: F401 — register models with Base.metadata
@asynccontextmanager
async def lifespan(_app: FastAPI):
settings.data_dir.mkdir(parents=True, exist_ok=True)
settings.uploads_dir.mkdir(parents=True, exist_ok=True)
Base.metadata.create_all(bind=engine)
yield
app = FastAPI(title="MincedPad", lifespan=lifespan)
@app.get("/api/health")
def health():
return {"ok": True, "name": "MincedPad"}
+35
View File
@@ -0,0 +1,35 @@
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.db import Base
class Item(Base):
__tablename__ = "items"
id: Mapped[int] = mapped_column(primary_key=True)
slug: Mapped[str] = mapped_column(String(32), unique=True, index=True)
kind: Mapped[str] = mapped_column(String(16)) # text|file
title: Mapped[str] = mapped_column(String(255), default="")
body: Mapped[str | None] = mapped_column(Text, nullable=True)
file_name: Mapped[str | None] = mapped_column(String(512), nullable=True)
file_path: Mapped[str | None] = mapped_column(String(512), nullable=True)
mime: Mapped[str | None] = mapped_column(String(128), nullable=True)
size_bytes: Mapped[int | None] = mapped_column(Integer, nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, default=True)
burn_after_read: Mapped[bool] = mapped_column(Boolean, default=False)
expires_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_ip: Mapped[str] = mapped_column(String(64), default="")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
view_count: Mapped[int] = mapped_column(Integer, default=0)
burned: Mapped[bool] = mapped_column(Boolean, default=False)
class BannedIP(Base):
__tablename__ = "banned_ips"
ip: Mapped[str] = mapped_column(String(64), primary_key=True)
reason: Mapped[str] = mapped_column(String(255), default="")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)