下面是一个在 Django 5 中实现文件上传功能的完整指南。我们将从最基础的配置开始,逐步构建一个包含模型、表单、视图和模板的完整示例。
📁 第一步:基础配置 (settings.py)
首先,需要在项目的 settings.py 文件中配置媒体文件的保存路径和访问URL。
python
# settings.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
# 1. 媒体文件存储路径 (上传的文件会存到这里)
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # 项目根目录下的 media 文件夹
# 2. 媒体文件访问URL (通过这个URL前缀可以访问上传的文件)
MEDIA_URL = '/media/'
# 3. (可选) 配置文件上传大小限制
# FILE_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10MB
完成配置后,需要在项目根目录下手动创建一个 media 文件夹,否则上传时会报错。
🌐 第二步:配置路由 (urls.py)
为了让 Django 在开发环境下能提供媒体文件的访问服务,需要配置路由。
python
# 项目的 urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
# ... 你的应用路由
]
# 仅在开发环境 (DEBUG=True) 下启用,生产环境请使用 Nginx 等服务器处理
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
🗄️ 第三步:定义模型 (models.py)
如果你需要将上传文件的信息(如文件名、上传时间)保存到数据库,就需要定义一个模型。核心是使用 FileField 或 ImageField。
python
# your_app/models.py
from django.db import models
class UploadedFile(models.Model):
# upload_to 指定文件存储在 MEDIA_ROOT 下的子目录
# 可以使用 %Y/%m/%d 等 strftime 格式化参数,按日期自动创建子目录
file = models.FileField(upload_to='uploads/%Y/%m/%d/')
uploaded_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.file.name
别忘了执行数据库迁移命令:
bash
python manage.py makemigrations
python manage.py migrate
📝 第四步:创建表单 (forms.py)
使用 Django 表单能自动处理文件数据的绑定和验证,让代码更简洁、安全。这里我们使用 ModelForm 来直接关联刚才定义的模型。
python
# your_app/forms.py
from django import forms
from .models import UploadedFile
class UploadFileForm(forms.ModelForm):
class Meta:
model = UploadedFile
fields = ['file'] # 表单中只显示文件上传字段
👁️ 第五步:编写视图 (views.py)
视图函数负责处理上传逻辑。关键点在于,必须将 request.FILES 传递给表单。
python
# your_app/views.py
from django.shortcuts import render, redirect
from .forms import UploadFileForm
def upload_file(request):
if request.method == 'POST':
# 注意:必须同时传入 request.POST 和 request.FILES
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
# 表单验证通过后,调用 save() 方法,文件会自动保存到 MEDIA_ROOT
form.save()
# 上传成功后重定向到成功页面
return redirect('upload_success')
else:
form = UploadFileForm()
return render(request, 'upload.html', {'form': form})
def upload_success(request):
return render(request, 'success.html')
🖼️ 第六步:创建模板 (upload.html)
模板中的 <form> 标签有两个关键点:
method必须为"post"。enctype必须为"multipart/form-data"。
html
<!-- your_app/templates/upload.html -->
<!DOCTYPE html>
<html>
<head>
<title>文件上传</title>
</head>
<body>
<h2>上传文件</h2>
<form method="post" enctype="multipart/form-data">
{% csrf_token %} <!-- Django 的 CSRF 防护令牌,必须包含 -->
{{ form.as_p }} <!-- 渲染表单,包含 file 字段 -->
<button type="submit">上传</button>
</form>
</body>
</html>
🚀 第七步:添加应用路由 (urls.py)
最后,将应用的视图添加到路由中。
python
# your_app/urls.py
from django.urls import path
from . import views
app_name = 'your_app'
urlpatterns = [
path('', views.upload_file, name='upload_file'),
path('success/', views.upload_success, name='upload_success'),
]
# 别忘了在项目的 urls.py 中 include 这个应用的 urls.py
📚 深入理解:处理上传的文件对象
在视图中,通过 request.FILES 获取的文件对象(如 myFile)提供了以下常用属性和方法:
myFile.name: 获取上传文件的文件名。myFile.size: 获取上传文件的大小(字节)。myFile.content_type: 获取文件的 MIME 类型。myFile.chunks(): 推荐的读取方式。它将文件分成数据块(chunk)进行迭代,适合处理大文件,能有效节省内存。myFile.read(): 一次性读取整个文件,仅适用于小文件。myFile.multiple_chunks(): 判断文件是否大于FILE_UPLOAD_MAX_MEMORY_SIZE(默认2.5MB),可用于决定使用read()还是chunks()。
🔒 重要安全提示
- 文件大小限制 :应在 Web 服务器层面(如 Nginx)配置文件大小限制,而不仅仅依赖 Django 的
FILE_UPLOAD_MAX_MEMORY_SIZE设置。 - 内容验证:不要仅依赖文件扩展名。应验证文件内容(如检查文件的"魔数"字节)来确保其类型与声称的一致。
- 安全标头 :提供用户上传的文件时,应设置安全的响应头,例如
Content-Disposition: attachment和X-Content-Type-Options: nosniff,以防止跨站脚本攻击。 - 保持更新:请关注 Django 官方安全公告,并及时升级到最新版本,以修复已知的安全漏洞。
💎 总结
至此,你就完成了一个健壮的文件上传功能。这个流程涵盖了从项目配置到前端展示的完整链路,你可以根据实际需求(例如上传图片、限制文件类型等)在此基础上进行扩展。
这段代码是一种手动处理文件上传 的方式,使用的是 Django 的底层 API。它直接操作 request.FILES,自行打开文件流,分块写入到服务器指定目录。
🔍 逐行拆解
python
myFile = request.FILES.get("myfile", None)
request.FILES是一个类似字典的对象,包含了所有上传的文件数据,仅当请求方法为POST且表单的enctype="multipart/form-data"时才有内容。.get("myfile", None)尝试获取名为myfile的文件字段,如果不存在则返回None(而不是抛出异常)。
python
if myFile:
f = open(os.path.join("D:\\myFile", myFile.name), "wb+")
- 假设存在该文件,则使用 Python 内置的
open函数以二进制写入模式("wb+")打开一个目标文件,路径硬编码为D:\myFile,文件名取自myFile.name(用户上传的原始文件名)。 - 注意:硬编码路径非常危险,且没有做任何文件名过滤(可能导致路径遍历攻击)。
python
for chunk in myFile.chunks():
f.write(chunk)
f.close()
chunks()是 Django 上传文件对象提供的方法,它会将文件内容分块(默认每块 2.5MB)迭代返回,适合处理大文件,避免一次性加载到内存。- 逐块写入磁盘。
python
return HttpResponse("文件上传成功!")
- 返回一个简单的成功响应。
📜 这是哪种写法?
这是 "原始(raw)文件上传处理" ,在 Django 早期版本或一些需要完全自定义上传逻辑的场景中常见。它不依赖任何 Django 的表单(forms.Form)、模型(models.Model)或内置的存储系统(django.core.files.storage),纯粹手动管理文件流的读写。
⚖️ 与"推荐写法"的对比
| 维度 | 手动写法(如你贴的代码) | 推荐写法(使用 Form / ModelForm) |
|---|---|---|
| 代码量 | 较多,需要处理打开、写入、关闭、异常等 | 极少,只需 form.save() 即可完成存储 |
| 验证 | 无任何验证(文件大小、类型、内容) | 可通过表单字段的 validators 轻松添加 |
| 安全性 | 文件名未处理,易受路径遍历攻击;缺少 MIME 类型校验 | Django 自动清理文件名(get_available_name),并可配置白名单 |
| 存储位置 | 硬编码路径,不易维护 | 通过 MEDIA_ROOT + upload_to 动态生成,可切换存储后端(如云存储) |
| 数据库关联 | 无,文件信息不保存,无法追踪 | 模型字段自动关联,文件信息与记录绑定 |
| 错误处理 | 需自己写 try/except | 表单验证自动返回错误信息到模板 |
| 大文件处理 | 手动使用 chunks() 已经很好 |
同样支持分块,且可配合 FileUploadHandler 自定义 |
⚠️ 潜在风险和问题
- 路径安全 :
os.path.join("D:\\myFile", myFile.name)若用户上传../../etc/passwd这样的文件名,可能会覆盖系统文件(取决于权限)。Django 的upload_to会正确处理路径并保证安全。 - 文件名冲突 :如果两个用户上传同名文件,后一个会覆盖前一个。推荐做法是使用
uuid重命名文件。 - 硬编码路径 :部署到其他服务器时可能
D:盘不存在,导致上传失败。应使用settings.MEDIA_ROOT。 - 缺少 MIME 检查 :攻击者可以上传
.php或.html文件,结合服务器配置可能导致远程代码执行。
🤔 什么时候还会用这种方式?
- 学习/演示目的,想理解底层原理。
- 需要高度定制化的存储逻辑(例如同时写入多个后端、加密存储等),但即便如此,通常也可以通过自定义存储后端来实现,而不是完全放弃 Django 的封装。
💡 如何改进这段代码(逐步向推荐方式靠拢)
- 使用
settings.MEDIA_ROOT替代硬编码路径。 - 使用
django.core.files.storage.default_storage.save()替代手动open。 - 引入表单验证 ,至少用
forms.FileField检查文件大小和类型。 - 将文件路径存入数据库,以便后续检索和管理。
- 返回更友好的页面(而不是纯文本 HttpResponse)。
如果你只是需要一个简单的上传功能,我更推荐你使用我们之前讲解的 Form + Model 方式,既省心又安全。手动写法虽然"能跑",但在实际生产环境中请务必避免。😊
如果还想了解如何安全地处理文件名(防止覆盖、路径遍历),或如何限制文件类型,我可以再展开细说。