File Upload + Pickle — Upload a Model
Accept a .pkl file via multipart form, save it to disk with a UUID name, store metadata in the DB. This is how users submit trained models.
# The basic concept: pickle saves a Python object to a file
import pickle
from sklearn.linear_model import LinearRegression
import numpy as np
# Train a model
model = LinearRegression()
X = np.array([[1], [2], [3]])
y = np.array([2, 4, 6])
model.fit(X, y)
# Save it to a file
with open('model.pkl', 'wb') as f:
pickle.dump(model, f)
# Later (even in a different process): load it back
with open('model.pkl', 'rb') as f:
loaded = pickle.load(f)
print(loaded.predict([[5]])) # [10.]
# The .pkl file contains the trained model's weights + structure.
# You can upload it, download it, and reuse it anywhere.
# Step 5: Add /upload + /my-models on top of steps 1-4
import uuid, shutil, pickle
from pathlib import Path
from fastapi import UploadFile, File, Form, Depends
from sqlalchemy.orm import Session
from sqlalchemy import Column, Integer, String, ForeignKey
# Add to database.py:
# class Model(Base):
# __tablename__ = 'models'
# id = Column(Integer, primary_key=True)
# name = Column(String)
# filename = Column(String) # UUID.pkl on disk
# owner_id = Column(Integer, ForeignKey('users.id'))
UPLOAD_DIR = Path('uploads')
UPLOAD_DIR.mkdir(exist_ok=True)
@app.get('/upload', response_class=HTMLResponse)
async def upload_form(request: Request):
return templates.TemplateResponse('upload.html', {'request': request})
@app.post('/upload')
async def upload(
request: Request,
name: str = Form(...),
file: UploadFile = File(...),
db: Session = Depends(get_db),
):
# 1. Validate the file is a .pkl
if not file.filename.endswith('.pkl'):
return {'error': 'must be a .pkl file'}
# 2. Verify it's a valid pickle by loading it
try:
data = await file.read()
model = pickle.loads(data) # load from bytes
assert hasattr(model, 'predict')
except Exception:
return {'error': 'invalid pickle or not a model'}
# 3. Save with a UUID name (never trust user's filename)
safe_name = f'{uuid.uuid4().hex}.pkl'
with open(UPLOAD_DIR / safe_name, 'wb') as f:
f.write(data)
# 4. Store metadata in DB
from database import Model, User
token = request.cookies.get('access_token')
username = decode_token(token)
user = db.query(User).filter(User.username == username).first()
m = Model(name=name, filename=safe_name, owner_id=user.id)
db.add(m); db.commit()
return RedirectResponse(url='/my-models', status_code=303)
@app.get('/my-models', response_class=HTMLResponse)
async def my_models(request: Request, db: Session = Depends(get_db)):
from database import Model, User
token = request.cookies.get('access_token')
username = decode_token(token)
if not username:
return RedirectResponse(url='/login', status_code=303)
user = db.query(User).filter(User.username == username).first()
models = db.query(Model).filter(Model.owner_id == user.id).all()
return templates.TemplateResponse('my_models.html', {
'request': request, 'models': models,
})
# Train a model and save it as a .pkl — run this to create a file to upload
import pickle
from sklearn.linear_model import LinearRegression
import numpy as np
model = LinearRegression()
X = np.array([[i] for i in range(100)])
y = np.array([2 * i + 1 + np.random.normal(0, 0.5) for i in range(100)])
model.fit(X, y)
print(f'R^2 = {model.score(X, y):.3f}')
with open('my_model.pkl', 'wb') as f:
pickle.dump(model, f)
print('saved my_model.pkl — upload this via the form')
{% extends 'base.html' %}
{% block title %}Upload Model{% endblock %}
{% block content %}
<h1>Upload a Model</h1>
<form method='post' action='/upload' enctype='multipart/form-data'>
<input type='text' name='name' placeholder='Model name' required>
<input type='file' name='file' accept='.pkl' required>
<button type='submit'>Upload</button>
</form>
{% endblock %}
How it works
Pickle is Python's serialization format — it turns any Python object (including a trained sklearn model) into a byte stream you can save to disk or send over a network. The upload flow: (1) The form has enctype='multipart/form-data' — WITHOUT it, the file isn't sent. (2) FastAPI's UploadFile streams the file; we read() it into bytes. (3) pickle.loads(data) verifies it's a real model with a .predict() method — never trust user uploads blindly. (4) We save with a UUID name (uuid.uuid4().hex) instead of the user's filename — this prevents path traversal attacks (../../etc/passwd) and filename collisions. (5) Metadata (name, filename, owner_id) goes in the DB; the actual .pkl goes on disk. Storing big binaries in the DB is a performance disaster.
- Missing `enctype='multipart/form-data'` on the form — the file silently isn't sent. The `file: UploadFile` parameter will be None.
- Trusting the user's filename — `file.filename` could be `../../etc/passwd`. ALWAYS generate a UUID name.
- Loading the whole file into memory — for large files, use `shutil.copyfileobj(file.file, dest)` to stream to disk. We read() here only because we need to verify the pickle.
- Not validating the pickle — `pickle.loads` can execute arbitrary code. In production, only accept pickles from trusted users, or use a safer format like ONNX or joblib with a hash check.