A 45‑minute beginner clone of Paperless‑ngx
Welcome to another official UVF IT guide! In this quick build you’ll create a tiny document management system inspired by Paperless‑ngx, using only Python, FastAPI, and a local SQLite database—no paid services required.
We’ll scaffold a FastAPI app, define a simple Document model, add upload & list endpoints, run it locally, and verify you can store and view PDFs—all in under an hour.
Create a virtual environment so our dependencies stay isolated from the system Python. Then install FastAPI for the web framework, Uvicorn as the ASGI server, and SQLModel which gives us an easy ORM on top of SQLite. Finally we add a minimal main.py that starts the app – this file will grow as we add routes later.
setup.sh
#!/usr/bin/env bash
# 1. Create and activate a virtual environment
python3 -m venv .venv
source .venv/bin/activate
# 2. Upgrade pip and install core packages
pip install --upgrade pip
pip install fastapi uvicorn sqlmodel
# 3. Create project skeleton
mkdir -p app/uploads
cat > app/main.py <<'PY'
from fastapi import FastAPI
app = FastAPI()
@app.get('/')
async def root():
return {'msg': 'Paperless‑ngx clone ready'}
PY
echo "Setup complete. Run with: uvicorn app.main:app --reload"A Document needs an identifier, the original filename, and the moment it was uploaded. SQLModel lets us declare this as a Python class; when the app starts it will automatically create the corresponding SQLite table if it doesn't exist yet.
app/models.py
from datetime import datetime
from sqlmodel import Field, SQLModel
class Document(SQLModel, table=True):
id: int = Field(default=None, primary_key=True)
filename: str = Field(index=True)
uploaded_at: datetime = Field(default_factory=datetime.utcnow)
# Helper to create the DB tables – call from main later
def init_db(engine):
SQLModel.metadata.create_all(engine)In this step we add the heart of our clone: a FastAPI endpoint that accepts multipart file uploads. The uploaded file is stored in a local ./uploads folder and a record is inserted into the SQLite database so we can retrieve it later. We also expose a simple GET endpoint that returns a JSON list of all stored documents, showing their IDs and filenames. This mirrors Paperless‑ngx’s basic document ingestion and browsing capabilities.
main.py
import os
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
import sqlite3
UPLOAD_DIR = "uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)
app = FastAPI(title="Paperless‑ngx Clone")
DB_PATH = "documents.db"
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
@app.on_event("startup")
def init_db():
with get_db() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
path TEXT NOT NULL
)
""")
conn.commit()
class Document(BaseModel):
id: int
filename: str
path: str
@app.post("/upload", response_model=Document)
async def upload_file(file: UploadFile = File(...)):
file_location = os.path.join(UPLOAD_DIR, file.filename)
with open(file_location, "wb") as buffer:
content = await file.read()
buffer.write(content)
with get_db() as conn:
cur = conn.execute("INSERT INTO documents (filename, path) VALUES (?, ?)", (file.filename, file_location))
doc_id = cur.lastrowid
conn.commit()
return Document(id=doc_id, filename=file.filename, path=file_location)
@app.get("/documents", response_model=list[Document])
def list_documents():
with get_db() as conn:
rows = conn.execute("SELECT id, filename, path FROM documents").fetchall()
return [Document(id=row["id"], filename=row["filename"], path=row["path"]) for row in rows]Now that the API is defined, we launch the development server so you can interact with it. Uvicorn will serve the FastAPI app and automatically reload on code changes, which is handy while you experiment. Once running, open your browser to the /docs path to see the interactive Swagger UI that FastAPI generates for you.
uvicorn main:app --reloadWith the server up, use the Swagger UI (http://127.0.0.1:8000/docs) to upload a PDF file and then call the GET /documents endpoint to see it listed. You can also perform the same actions from the command line with curl to automate testing. Feel free to explore the ./uploads folder – the raw file you sent should be there, confirming the end‑to‑end flow works.
curl -X POST "http://127.0.0.1:8000/upload" -F "file=@sample.pdf"| Check | Command / Why it matters |
|---|---|
| Server starts without errors | uvicorn main:app --reload shows 'Uvicorn running on http://127.0.0.1:8000' |
| Upload works via Swagger UI or curl | POST /upload returns JSON with id and filename |
| GET /documents returns a JSON array with your uploaded file | Visit http://127.0.0.1:8000/documents in browser |
You now have a functional clone. Add OCR with Tesseract, implement tags, or secure the API with JWT to approach the full Paperless‑ngx feature set.