Image upload and media serving
Accept multipart uploads, save public or private files, and serve private files only through authorized routes.
Work through each step in order. The examples use the generated LenAPI project structure and are designed to be adapted to your app.
Configure media storage
MEDIA_ROOT and MEDIA_URL are for files intended to be public. New projects use MEDIA_URL='/' for short URLs such as /<generated-name>.jpg when a file is saved directly into MEDIA_ROOT. PRIVATE_MEDIA_ROOT stores files without a public URL; serve those only from authenticated routes after checking access to the specific record.
# settings.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/" # generated files saved at the root of MEDIA_ROOT
PRIVATE_MEDIA_ROOT = BASE_DIR / "private_media"Add a profile image to Student
Store the uploaded image path on the existing Student record. Add profile_image to app/models.py and create a migration so the database gets the new column.
# app/models.py — add this field to Student
profile_image = model.ImageField(
upload_to="profiles",
nullable=True,
)
# Apply the schema change
python lenora.py migrationAccept an image in Swagger and save it to Student
Declare the file as UploadFile = File(...). LenAPI generates the multipart file chooser in Swagger. Save without a directory to write directly under MEDIA_ROOT; store the returned relative filename in Student.profile_image. The generated URL is then /<filename>, without /media/ or a folder segment.
from lenapi import File, UploadFile
from app.models import Student
@app.post("/students/{student_id}/profile-image")
async def upload_student_image(
student_id: int,
image: UploadFile = File(...),
):
student = Student.get_or_404(id=student_id)
filename = await image.save() # MEDIA_ROOT/<generated-name>.jpg
student.profile_image = filename
student.save()
return {
"student_id": student.id,
"profile_image": student.profile_image,
"image_url": f"/{filename}",
}Create a Student with an uploaded image
For a new record, save the file first and pass its relative filename into Student.create. With MEDIA_URL='/' and no save directory, its public URL is /<filename>.
filename = await image.save()
student = Student.create(
person_name="Ada",
course="Python",
profile_image=filename,
)
return {
"id": student.id,
"person_name": student.person_name,
"profile_image": student.profile_image,
"image_url": f"/{filename}",
}Serve and reference the image
The database stores only the relative filename on Student.profile_image; the upload bytes live under MEDIA_ROOT. With MEDIA_URL='/', LenAPI serves a matching top-level file at /<filename> and lets normal routes continue when no file exists. Files saved into subdirectories retain those path segments.
# Example database value on Student.profile_image
7bcbf310be6a4ca88dd810807da8a95a.jpg
# Public URL returned by the endpoint
/7bcbf310be6a4ca88dd810807da8a95a.jpgImage saves validate JPEG, PNG, GIF, WebP, and BMP signatures. This checks format consistency; it is not malware scanning. Use save('documents', type='pdf') for PDFs, type='doc' for DOC/DOCX, or type=None for any file. Names are sanitized and existing files are never overwritten. Set MEDIA_URL='/media/' to retain the conventional prefixed route, or use save('profiles') if you want a folder in the URL. Configure a reverse proxy or object storage for production-scale media delivery.
Save a private image
Pass private=True to save beneath PRIVATE_MEDIA_ROOT. Store its relative path in a dedicated model field. Public media URLs cannot serve this file.
# app/models.py — add a separate field for private files
private_profile_image = model.ImageField(
upload_to="profiles",
nullable=True,
)
# main.py
from lenapi import Depends, File, HTTPException, UploadFile
from app.models import Student
@app.post(
"/students/{student_id}/private-image",
auth=app.authenticator,
)
async def upload_private_student_image(
student_id: int,
image: UploadFile = File(...),
identity=Depends(app.authenticator),
):
student = Student.get_or_404(id=student_id)
if not can_edit_student(identity, student):
raise HTTPException(404, "Not found")
filename = await image.save("profiles", private=True)
student.private_profile_image = f"profiles/{filename}"
student.save()
return {"student_id": student.id, "saved": True}Keep private and public file paths in separate fields so the application does not accidentally return a private path as a public media URL. Never take the path passed to private_file directly from a request parameter; load it from the authorized record. Add malware scanning if your threat model requires it.
Serve a private image after authorization
Protect the route with the application's authenticator, then check record ownership or another access rule before calling app.private_file. The helper enforces private-root path containment, disables caching, and sends non-image files as downloads. It does not decide whether a caller may access a record.
from lenapi import Depends, HTTPException
@app.get(
"/students/{student_id}/private-image",
auth=app.authenticator,
)
def private_student_image(
student_id: int,
identity=Depends(app.authenticator),
):
student = Student.get_or_404(id=student_id)
# Implement this check using your identity format and ownership model.
if not can_view_student(identity, student):
raise HTTPException(404, "Not found")
if not student.private_profile_image:
raise HTTPException(404, "Not found")
return app.private_file(student.private_profile_image, inline=True)Use the same ownership check on upload and download routes. Returning 404 for inaccessible records avoids confirming that another user's file exists.