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.FILES
def 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.py
from django.db import models
def 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.py
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
# urls.py (chỉ khi DEBUG=True)
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
# ...
] + 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.py
from django import forms
from .models import Profile
class AvatarForm(forms.ModelForm):
class Meta:
model = Profile
fields = ["avatar"]
# views.py
def 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 model
from django.core.validators import FileExtensionValidator
avatar = 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:

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.

Nguồn tham khảo