Django Admin绑定上传文件失败:内存文件无法传递至确认页
问题原因分析
浏览器出于安全限制,不允许通过代码自动填充文件输入框,所以即使你将绑定了request.FILES的表单传递给模板,ClearableFileInput也无法回显已上传的文件。同时HTTP是无状态协议,内存中的文件无法直接在两次请求间传递,这就是你看到文件在渲染后消失的核心原因。
解决方案:临时存储+Session传递
我们可以通过「服务器临时存储文件+Session记录标识」的方式实现确认页的文件传递,具体步骤如下:
1. 调整视图逻辑,处理文件临时存储
修改ModelAdmin中的import_csv方法,在用户第一次上传文件并验证表单后,将文件保存到服务器临时目录,同时把临时路径和表单参数存入Session:
import os import uuid import csv from django.conf import settings from django.urls import reverse from django.contrib import messages from django.http import HttpResponseRedirect def import_csv(self, request, form_url="", extra_context=None): context = self.admin_site.each_context(request) if request.method == "POST": # 处理用户确认导入的请求 if "confirm_import" in request.POST: temp_path = request.session.get("temp_csv_path") overwrite = request.session.get("overwrite", False) if not temp_path or not os.path.exists(temp_path): self.message_user(request, "临时文件已失效,请重新上传", level=messages.ERROR) return HttpResponseRedirect(reverse(f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_changelist")) try: # 读取临时文件执行导入逻辑 with open(temp_path, "r", encoding="utf-8") as f: reader = csv.reader(f) # 替换为你的CSV导入逻辑 for row in reader: # ...处理每行数据 pass self.message_user(request, "CSV文件导入成功") except Exception as e: self.message_user(request, f"导入失败:{str(e)}", level=messages.ERROR) finally: # 清理临时文件和Session os.remove(temp_path) del request.session["temp_csv_path"] del request.session["overwrite"] return HttpResponseRedirect(reverse(f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_changelist")) # 处理第一次上传文件的请求 else: form = CsvImportForm(request.POST, request.FILES) if form.is_valid(): csv_file = request.FILES["csv_file"] # 生成唯一临时文件名,避免冲突 temp_filename = f"temp_csv_{request.user.id}_{uuid.uuid4().hex}.csv" temp_dir = os.path.join(settings.MEDIA_ROOT, "temp_csv") os.makedirs(temp_dir, exist_ok=True) temp_path = os.path.join(temp_dir, temp_filename) # 保存文件到临时目录 with open(temp_path, "wb+") as destination: for chunk in csv_file.chunks(): destination.write(chunk) # 将临时路径和表单参数存入Session request.session["temp_csv_path"] = temp_path request.session["overwrite"] = form.cleaned_data["overwrite"] # 渲染确认页,传递文件信息 context.update({ "csv_filename": csv_file.name, "overwrite": form.cleaned_data["overwrite"], # 可添加CSV预览内容,比如前3行数据 "preview_rows": list(csv.reader(csv_file))[:3] }) return TemplateResponse(request, "admin/import_confirm.html", context) else: context["form"] = form else: # GET请求,显示上传表单 context["form"] = CsvImportForm() return TemplateResponse(request, "admin/import_upload.html", context)
2. 修改确认页模板
确认页不需要再渲染文件输入框,改为显示已上传的文件信息和确认选项:
<form action="{{ form_url }}" method="post">{% csrf_token %} <div class="module"> <p>你确认要导入文件 <strong>{{ csv_filename }}</strong> 吗?</p> {% if overwrite %} <p class="warning">⚠️ 已勾选「覆盖现有数据」,此操作可能导致已有数据丢失</p> {% endif %} {% if preview_rows %} <h4>数据预览(前3行):</h4> <table> {% for row in preview_rows %} <tr>{% for col in row %}<td>{{ col }}</td>{% endfor %}</tr> {% endfor %} </table> {% endif %} </div> <input type="hidden" name="confirm_import" value="1"> <div class="submit-row"> <input type="submit" value="{% translate 'Yes, I’m sure' %}" class="default"> <a href="{% url 'admin:yourapp_yourmodel_changelist' %}" class="button cancel-link">{% translate "No, take me back" %}</a> </div> </form>
3. 补充清理机制
为避免临时文件占用服务器空间,可添加定时任务(比如用django-crontab)定期清理过期的临时文件:
# 示例:每天凌晨清理7天前的临时CSV文件 import glob import time def clean_temp_csv(): temp_dir = os.path.join(settings.MEDIA_ROOT, "temp_csv") cutoff_time = time.time() - 7*24*3600 # 7天前 for file_path in glob.glob(os.path.join(temp_dir, "*.csv")): if os.path.getmtime(file_path) < cutoff_time: os.remove(file_path)
备选方案:小文件直接存Session
如果CSV文件体积很小(比如几KB),可以直接将文件内容存入Session,省去临时文件的操作:
# 上传时保存内容到Session if form.is_valid(): csv_content = request.FILES["csv_file"].read().decode("utf-8") request.session["csv_content"] = csv_content request.session["overwrite"] = form.cleaned_data["overwrite"] request.session["csv_filename"] = request.FILES["csv_file"].name # 确认时读取Session内容 csv_content = request.session.get("csv_content") if csv_content: from io import StringIO with StringIO(csv_content) as f: reader = csv.reader(f) # 执行导入逻辑
内容的提问来源于stack exchange,提问作者Bruno C
相关产品推荐
相关产品推荐

