Muốn cho người dùng tải lên avatar, ảnh sản phẩm hay file đính kèm trong Django, bạn chỉ cần ba mảnh ghép: FileField (hoặc ImageField) trên model, form gửi kèm enctype=”multipart/form-data”, và hai cấu hình MEDIA_ROOT/MEDIA_URL trong settings. Thiếu bất kỳ mảnh nào, file sẽ không lưu được, hoặc lưu rồi mà không xem được. Bài này ráp đủ cả ba trong 5 phút.
Vấn đề
Tình huống quen thuộc: bạn thêm FileField vào model, viết form, bấm submit — nhưng file biến mất. Nguyên nhân thường nằm ở hai chỗ. Thứ nhất, form HTML thiếu enctype nên trình duyệt chỉ gửi tên file chứ không gửi nội dung file. Thứ hai, view chỉ đọc request.POST mà quên request.FILES — nơi Django đặt dữ liệu file upload. Ngoài ra, nếu chưa cấu hình MEDIA_URL, file có lưu xuống ổ đĩa cũng không có URL nào để truy cập.
# views.py — SAI: quên request.FILESdef upload_avatar(request): form = AvatarForm(request.POST) # thiếu request.FILES if form.is_valid(): form.save()
<!-- template — SAI: thiếu enctype --><form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit">Tải lên</button></form>
Giải pháp
Bước 1 — model: khai báo ImageField cho ảnh (cần cài Pillow) hoặc FileField cho file bất kỳ. Tham số upload_to quyết định thư mục con bên trong MEDIA_ROOT; truyền callable để gom file theo user hoặc theo ngày, tránh trùng tên và tránh một thư mục chứa hàng nghìn file.
# models.pyfrom django.db import modelsdef avatar_path(instance, filename): return f"avatars/user_{instance.user_id}/{filename}"class Profile(models.Model): user = models.OneToOneField("auth.User", on_delete=models.CASCADE) avatar = models.ImageField(upload_to=avatar_path, blank=True) resume = models.FileField(upload_to="resumes/%Y/%m/", blank=True)
Bước 2 — settings và URL: trỏ MEDIA_ROOT tới thư mục lưu file thật, MEDIA_URL là tiền tố URL. Ở môi trường dev, thêm static() vào urls.py để Django phục vụ file media; production thì để Nginx hoặc dịch vụ lưu trữ lo.
# settings.pyMEDIA_URL = "/media/"MEDIA_ROOT = BASE_DIR / "media"# urls.py (chỉ khi DEBUG=True)from django.conf import settingsfrom django.conf.urls.static import staticurlpatterns = [ # ...] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Bước 3 — form và view: dùng ModelForm để khỏi map tay từng field. Trong view, truyền cả request.POST và request.FILES cho form. Template bắt buộc có enctype=”multipart/form-data”, nếu không trình duyệt sẽ không gửi nội dung file.
# forms.pyfrom django import formsfrom .models import Profileclass AvatarForm(forms.ModelForm): class Meta: model = Profile fields = ["avatar"]# views.pydef upload_avatar(request): form = AvatarForm(request.POST or None, request.FILES or None) if request.method == "POST" and form.is_valid(): profile = form.save(commit=False) profile.user = request.user profile.save() return redirect("profile") return render(request, "upload.html", {"form": form})
<form method="post" enctype="multipart/form-data"> {% csrf_token %} {{ form.as_p }} <button type="submit">Tải lên</button></form>
Muốn chặn file sai định dạng hoặc quá lớn, gắn validator vào field: FileExtensionValidator([“jpg”, “png”]) và một hàm kiểm tra file.size trong clean_avatar(). Validate ở tầng form hoặc model thay vì tin vào đuôi file phía client.
# models.py — validate ở tầng modelfrom django.core.validators import FileExtensionValidatoravatar = models.ImageField( upload_to=avatar_path, validators=[FileExtensionValidator(["jpg", "jpeg", "png"])], blank=True,)
Áp dụng vào dự án của bạn
Gắn avatar vào trang hồ sơ người dùng, cho phép đính kèm file vào ticket hỗ trợ, hoặc làm kho tài liệu nội bộ. Nhớ ba nguyên tắc: luôn dùng upload_to dạng callable hoặc có %Y/%m để file không dồn vào một thư mục; giới hạn dung lượng và đuôi file ngay từ đầu; và đừng bao giờ phục vụ thư mục media bằng Django ở production — dùng Nginx, S3 hoặc dịch vụ tương đương.
Bài viết liên quan
Ba bài liên quan trên blog, cùng chủ đề form và upload file:
- Django: viết form nhanh gấp đôi bằng ModelForm — khỏi map tay từng field
- FastAPI: nhận file upload bằng UploadFile — chặn file quá lớn và sai định dạng trước khi lưu trong 5 phút
- Flask: upload file an toàn — validate loại file và giới hạn dung lượng trong 5 phút
Câu hỏi thường gặp
FileField và ImageField khác nhau thế nào?
ImageField là FileField cộng thêm bước kiểm tra file có phải ảnh hợp lệ không, và nó cần Pillow. Dùng ImageField khi chỉ nhận ảnh; dùng FileField cho PDF, CSV, ZIP hay bất kỳ file nào khác.
Có cần cài thêm gì để dùng ImageField không?
Cần Pillow: pip install Pillow. Thiếu Pillow, Django báo lỗi ở bước kiểm tra hệ thống (system check) và form upload ảnh sẽ không chạy.
File upload lưu ở đâu trên ổ đĩa, và lấy link xem file bằng cách nào?
File nằm ở MEDIA_ROOT cộng với đường dẫn trong upload_to. Trong template, dùng {{ profile.avatar.url }} để lấy link đầy đủ bắt đầu bằng MEDIA_URL.
Vì sao truy cập /media/… bị 404 ở môi trường dev?
Thường do quên thêm static() vào urls.py, hoặc DEBUG=False mà vẫn trông chờ Django phục vụ file media. static() chỉ có tác dụng khi DEBUG=True.
Làm sao tránh hai người upload cùng tên file?
Dùng upload_to dạng callable để đổi tên file theo user, thời gian hoặc uuid. Ngoài ra, khi tên file đã tồn tại trong cùng thư mục, Django tự thêm hậu tố ngẫu nhiên để không ghi đè.
💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.
