Django & Python ← Back to blog

File Uploads with Per-User Quotas in Django

29 April 2026 · Matt

How the media_system app enforces a 5 GB per-user storage limit and routes files into organised subdirectories.

The Media Model

Uploaded files are tracked in a Media model with the user as a foreign key, the file itself, its size in bytes, and a JSON tags field.

class Media(models.Model):
    user        = models.ForeignKey(User, on_delete=models.CASCADE)
    file        = models.FileField(upload_to=media_upload_path)
    size        = models.BigIntegerField()
    tags        = models.JSONField(default=list)
    uploaded_at = models.DateTimeField(auto_now_add=True)

Enforcing the Quota

Before accepting a file, the upload view calculates total usage for the user and rejects the request if adding the new file would exceed 5 GB.

QUOTA_BYTES = 5 * 1024 ** 3   # 5 GB

def upload(request):
    if request.method != "POST":
        return redirect("media-index")

    f = request.FILES.get("file")
    if not f:
        return redirect("media-index")

    used = Media.objects.filter(user=request.user).aggregate(
        total=models.Sum("size")
    )["total"] or 0

    if used + f.size > QUOTA_BYTES:
        messages.error(request, "Storage quota exceeded (5 GB limit).")
        return redirect("media-index")

    media = Media(user=request.user, size=f.size)
    media.file.save(f.name, f)
    media.save()
    return redirect("media-index")

Automatic Directory Sorting

The upload_to callable inspects the file extension and routes the file into the right subdirectory under MEDIA_ROOT.

def media_upload_path(instance, filename):
    ext = filename.rsplit(".", 1)[-1].lower()
    folder_map = {
        frozenset(["jpg","jpeg","png","gif","webp","svg"]): "images",
        frozenset(["mp4","mov","avi","mkv","webm"]):        "videos",
        frozenset(["gif"]):                                  "gifs",
        frozenset(["pdf","doc","docx","txt","csv"]):        "documents",
    }
    for extensions, folder in folder_map.items():
        if ext in extensions:
            return f"{folder}/{filename}"
    return f"other/{filename}"
Tip: Using upload_to as a callable (rather than a plain string) lets you add per-user subdirectories, timestamps, or extension-based routing without touching the view layer.

The Approval Workflow

Staff-uploaded files are marked APPROVED immediately. Uploads from non-staff users start as PENDING and only become visible to others after a staff member approves them — enforced by a status field on the model and a queryset filter in the index view.