Skip to content

media

有别于static的静态文件,media通常用于存储图片、音、视频、压缩文件等各种跟用户相关的文件,所以配置和使用上也不一样。

Django也提供了一套默认的文件存储系统,你可以通过下面的官档快速了解下:

跟模型类结合使用时,主要涉及到两个字段:

  • FileField:一个专门用于文件上传的字段,官档:https://docs.djangoproject.com/zh-hans/5.0/ref/models/fields/#filefield。它的常用属性有:

    • FieldFile.name,上传文件的文件名。
    • FieldFile.size,获取文件大小。
    • FieldFile.path,只读属性,通过调用底层的 path() 方法,访问文件的本地文件系统路径,用的较少。
    • FileField.upload_to,指定上传目录和文件名的方式。
  • ImageField:继承 FileField 的所有属性和方法,但也验证上传的对象是有效的图像。官档:https://docs.djangoproject.com/zh-hans/5.0/ref/models/fields/#imagefield。除了包含FileField 的特殊属性外, ImageField 也有两个常用属性:

    • ImageField.height_field,模型字段的名称,每次保存模型实例时将自动填充图像的高度。

    • ImageField.width_field,模型字段的名称,每次保存模型实例时将自动填充图像的宽度。

    • 注意,ImageField内部依赖pillow模块,所以,你要提前安装该模块pip install Pillow。否则会提醒:

      bash
      (dj5py312) D:\code\dj5>python manage.py makemigrations
      SystemCheckError: System check identified some issues:
      
      ERRORS:
      store.UserInfo.avatar: (fields.E210) Cannot use ImageField because Pillow is not installed.
              HINT: Get Pillow at https://pypi.org/project/Pillow/ or run command "python -m pip install Pillow".

想要使用Django提供的默认的文件存储系统,一些必不可少的配置需要做。

示例

我们来做一个完整的用户信息管理功能来演示相关的配置和用法。

python
"""
注意要启用静态文件相关的app
避不可少的配置
MEDIA_ROOT:默认值为''(空字符串)。用于保存用户上传的文件的绝对路径。另外,MEDIA_ROOT 和 STATIC_ROOT 必须有不同的值。
MEDIA_URL:默认值为''(空字符串),如果设置为非空值,则必须以斜线结束。结合路由中的配置,可以对外提供访问。
"""
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',  # 这个app要启用
    'apps.ctts.apps.CttsConfig',
    'apps.drf.apps.DrfConfig',
    'apps.api.apps.ApiConfig',
    'apps.store.apps.StoreConfig',
    'rest_framework',
]

STATIC_URL = 'static/'
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)

if not os.path.exists(MEDIA_ROOT):
    os.makedirs(MEDIA_ROOT)
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
python
"""
避不可少的配置,注意,如果是多app环境,必须要在主路由(也就是settings.py同级目录下的urls.py)中配置才能生效
"""
from django.contrib import admin
from django.urls import path, include, re_path
from django.contrib.staticfiles.views import serve
from django.views.static import serve
from django.conf import settings
from apps import drf, api, store

urlpatterns = [
    path('admin/', admin.site.urls),
    path('drf/', include(('apps.drf.urls', 'apps.drf'), namespace='drf')),
    path('api/', include(('apps.api.urls', 'apps.api'), namespace='api')),
    path('store/', include(('apps.store.urls', 'apps.store'), namespace='store')),
    re_path('media/(?P<path>.*)$', serve, {"document_root": settings.MEDIA_ROOT}),  # 这个配置不能少
    path('favicon.ico', serve, {'path': 'favicon.ico', 'document_root': '/static/'})
]
python
from django.db import models


def custom_upload_to(instance, filename):
    return 'certs/{filename}'.format(filename=filename)

class UserInfo(models.Model):
    username = models.CharField(max_length=32, verbose_name='用户名')
    avatar = models.ImageField(upload_to='avatars/', verbose_name='用户头像', default='avatars/default.png')
    cert = models.FileField(upload_to='cert/', verbose_name='用户证书', default='certs/default.png')

    def __str__(self):
        return self.username
python
import os
from django.shortcuts import render, redirect
from django.http.response import HttpResponse
from django.views.generic import View
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.forms import ModelForm, Form
from apps.store.models import UserInfo


class UserInfoForm(ModelForm):
    class Meta:
        model = UserInfo
        fields = ['username', 'avatar', 'cert']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 为所有的字段添加相同的属性
        for field in self.fields.values():
            field.widget.attrs.update({"class": "form-control"})
            field.error_messages.update({"required": "该字段不能为空"})

    def clean_cert(self):
        # 校验就省略了
        cert = self.cleaned_data['cert']
        return cert

    def clean_avatar(self):
        # 校验就省略了
        avatar = self.cleaned_data['avatar']
        return avatar


class UserInfoView(ListView):
    model = UserInfo
    template_name = 'user/user_list.html'
    context_object_name = 'user_list'


class UserInfoAddView(CreateView):
    model = UserInfo
    template_name = 'user/user_add.html'
    form_class = UserInfoForm
    success_url = reverse_lazy("store:userlist")

    def form_valid(self, form):
        # 可以在这里进行手动保存,做一些额外的操作
        # print(111, self.request.FILES)
        form.save()
        return redirect(self.success_url)


class UserInfoDelView(DeleteView):
    model = UserInfo
    success_url = reverse_lazy("store:userlist")

    def get(self, request, *args, **kwargs):
        user = UserInfo.objects.get(id=request.GET.get("id"))
        if not user.avatar.name.endswith("default.png"):
            user.avatar.delete()
        if not user.cert.name.endswith("default.png"):
            user.cert.delete()
        user.delete()
        return redirect(self.success_url)


class UserInfoUpdateView(UpdateView):
    model = UserInfo
    success_url = reverse_lazy("store:userlist")
    template_name = 'user/user_add.html'
    context_object_name = 'user_detail'
    form_class = UserInfoForm

    def form_valid(self, form):
        # 可以在这里进行手动保存,做一些额外的操作
        # print(111, self.object.avatar.name, )
        if os.path.exists(self.object.avatar.path):
            os.remove(self.object.avatar.path)
        if os.path.exists(self.object.cert.path):
            os.remove(self.object.cert.path)
        form.save()
        return redirect(self.success_url)
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>用户信息处理</title>
    <link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap-theme.min.css" rel="stylesheet">
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
    <script src="http://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
    <style>
        .errorMsg {
            color: red;
            margin-left: 10px;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            {% block content %}
            {% endblock %}
        </div>
    </div>
</div>
</body>
</html>
html
{% extends 'user/base.html' %}
{% block content %}
    <h3>用户列表</h3>
    <a class="btn btn-success" href="{% url 'store:useradd' %}">添加</a>
    <table class="table table-hover table-striped">
        <thead>
        <tr>
            <th>姓名</th>
            <th>头像</th>
            <th>认证文件</th>
            <th>操作</th>
        </tr>
        </thead>
        <tbody>
        {% for user_info in user_list %}
            <tr>
                <td>{{ user_info.username }}</td>
                <td>{{ user_info.avatar }}</td>
                <td>{{ user_info.cert }}</td>
            <td>
                <a href="{% url 'store:userdetail' user_info.id %}?id={{ user_info.id }}" class="btn bg-info">编辑</a>
                <a href="{% url 'store:userdel'  %}?id={{ user_info.id }}" class="btn bg-warning">删除</a>
            </td>
            </tr>
        {% endfor %}
        </tbody>
    </table>
{% endblock %}
html
{% extends 'user/base.html' %}
{% block content %}
    <h3>添加用户</h3>
    <form action="" method="post" novalidate enctype="multipart/form-data">
        {% csrf_token %}
        {% for field in form %}
            <div class="form-group">
                <label for="">{{ field.label }}</label>
                <!-- 渲染input标签,并在有错误提示时渲染该错误信息 -->
                {{ field }} <span class="errorMsg">{{ field.errors.0 }}</span>
            </div>
        {% endfor %}
        <p><input type="submit" class="btn btn-success pull-left"></p>
    </form>
{% endblock %}

关于自定义media路径

这部分就是说,我们在上传文件或者图片时,可能会遇到重名的问题,就有了自定义的路径的需求,django提供了自定义上传文件名的方法,只需要修改models.py文件即可:

python
import os
import uuid  # # 通过uuid或者其他方式搞个随机的字符串解决重名
from django.db import models


def custom_file_path(instance, filename):
    print(333, uuid.uuid4().hex, type(uuid.uuid4().hex), filename, type(filename))
    # 通过uuid搞出来一个子目录,如 media\avatars\f49c652c622640bdad2abffa8a29471e\abc.jpg
    # 但有个操蛋的问题,删除时,只会删除图片,而那个uuid的子目录就保留了,需要我们手动处理,这.....我懒得处理,故排除这个写法
    # return os.path.join('avatars', uuid.uuid4().hex, filename)

    # 上面的懒得搞,就折衷把文件名搞一下解决处理,这样删除的时候就可以了,也不用管那个子目录的事儿了
    return os.path.join('avatars', '{}-{}'.format(uuid.uuid4().hex, filename))


class Asset(models.Model):
    user = models.CharField(max_length=32, verbose_name='用户名')
    avatar = models.FileField(upload_to=custom_file_path, verbose_name='用户头像', default='avatars/default.png')

    def __str__(self):
        return self.user

常见问题

django静态文件配置没问题,浏览器请求静态文件成功,但页面无法渲染静态文件中的样式

win10 + python3.9 + django4.2

这个问题非常难碰到啊!大概情况是这样的,django4.2的项目,在正确引入静态文件后,浏览器访问Django项目成功,访问其css文件成功,但页面中无法应用上css文件中的样式。

排查:

  • django关于静态文件的配置,OK的。
  • django开启了debug=True,OK的。
  • 配置中INSTALLED_APPS也有django.contrib.staticfiles
  • 项目html文件中,load static也没问题,相关引入静态文件的写法也都正确。
  • 浏览器访问通过network栏发现请求到了css文件,也是200,看响应也拿到了css文件内容。
  • 浏览器的console栏中也没有报错或者相关提示信息输出,当然有的会提示Resource interpreted as Stylesheet but transferred with MIME type application/x-css错误信息。

一切迹象表明,咱这静态文件引入是没问题的,但,就是页面不加载css样式。

继续浏览器的network栏观察,静态文件的请求,发现请求头和响应头是否有问题,有一行响应头引起了注意,那就是:

1832669383323811840.png

上面这个截图真是要素过多啊!!!!这个content-type就是这里就是突破口,经查,来自这个帖子:https://www.cnblogs.com/lance2088/p/4173868.html解释,我们套到Django中,其原因应该就是django在返回时,设置mimetypes时,如果是Windows环境下,就会读取本地的注册表,而此时如果注册表中的相关设置偶问题,这样就造成浏览器不能正常显示页面,css完全不起作用。

而我复现问题时,也发现的确是win10系统复现出来了问题,但在win11中没有复现出来,无所谓了,顺这个思路知道咋解决就好了。

来看解决流程吧!

win + R键打开运行,输入regedit打开注册表,然后按照下图操作就完了。

1832669384816984064.png

完事之后,重启电脑,再浏览器尝试访问,应该就没问题了。

再不行,就看下注册表中这个选项是否也需要修改:计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Classes\.css

参考: