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}"
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.