File Uploads and Static Files

Introduction

Users upload avatars, documents, and images. Flask reads files from request.files, saves them safely with secure_filename, and enforces size limits. For downloads, send_from_directory serves files without exposing your whole disk. This chapter covers local storage patterns; object storage (S3, OSS) follows the same validation rules with a different backend.

Prerequisites

HTML Upload Form

templates/upload.html:

html
<form method="post" enctype="multipart/form-data">
  {{ form.hidden_tag() }}
  <p>{{ form.photo.label }} {{ form.photo() }}</p>
  <p>{{ form.submit() }}</p>
</form>

Code explanation:

  • enctype="multipart/form-data" is required for file uploads
  • Without it, the browser sends no file bytes

forms.py with Flask-WTF:

Save Uploaded File

app/main/routes.py:

Code explanation:

  • secure_filename strips ../ and dangerous paths from client-provided names
  • instance_path keeps uploads outside the package—gitignore instance/

Manual handling without WTF:

python
from flask import request
 
file = request.files.get("photo")
if file and file.filename and allowed_file(file.filename):
    filename = secure_filename(file.filename)
    file.save(os.path.join(upload_dir, filename))

Limit Upload Size

app/config.py:

python
class Config:
    MAX_CONTENT_LENGTH = 16 * 1024 * 1024  # 16 MB

Oversized request:

python
from werkzeug.exceptions import RequestEntityTooLarge
 
@app.errorhandler(RequestEntityTooLarge)
def file_too_large(e):
    return "File too large (max 16 MB)", 413

Also validate MIME type and dimensions server-side—client checks are not enough.

Warning

Validate Content, Not Just Extension

Rename evil.exe to evil.jpg—inspect file headers or use a library like python-magic in production.

Unique Filenames

Avoid overwriting:

python
import uuid
 
ext = filename.rsplit(".", 1)[1].lower()
unique_name = f"{uuid.uuid4().hex}.{ext}"
save_path = os.path.join(upload_dir, unique_name)

Store unique_name in the database; display original name separately if needed.

Serve and Download Files

python
from flask import send_from_directory, abort
 
@main_bp.route("/uploads/<path:filename>")
def download_upload(filename):
    safe_name = secure_filename(filename)
    if safe_name != filename:
        abort(404)
    directory = os.path.join(current_app.instance_path, "uploads")
    return send_from_directory(directory, safe_name, as_attachment=True)

as_attachment=False displays inline (images in browser).

Code explanation:

  • send_from_directory prevents path traversal when used with sanitized names
  • Prefer authenticated routes—do not expose all uploads publicly

Static Files vs User Uploads

DirectoryPurposeServed by
static/CSS, JS, bundled assetsFlask dev or Nginx
instance/uploads/User-generated contentProtected view or CDN

Production often uploads to S3-compatible storage and returns a URL—Flask never stores large binaries on app servers.

Object Storage (Concept)

text
Browser → Flask (validate) → S3 API put_object → DB stores object key
Browser ← signed URL ← Flask

Same validation rules apply before calling the cloud API.

FAQ

request.files empty?

Missing enctype="multipart/form-data" or field name mismatch.

Permission denied on save?

Create upload_dir with os.makedirs(..., exist_ok=True); check filesystem permissions on Linux.

CSRF on file upload?

Include form.hidden_tag() in multipart forms.

Store files in database BLOB?

Possible for tiny files—usually worse for performance; use filesystem or object storage.

Nginx max body size?

Set client_max_body_size to match MAX_CONTENT_LENGTH.

Virus scan?

Scan in background job after upload for public-facing apps.