上传

upload_to参数可以指定文件被上传到的文件夹,如果不存在会被自动创建. 支持time-format.
upload_to参数的根目录是settings文件中的MEDIA_ROOT所指向的目录

  1. MEDIA_ROOT = "/opt/project_data"
  2. MEDIA_URL = "/files/"
  1. class Question(models.Model):
  2. image = models.FileField(blank=True, null=True, upload_to='os_fans_images/%Y/%m/%d', verbose_name='问题图片')
  3. class Meta:
  4. db_table = 'question'

进行上述配置之后上传的图片就会被自动上传到/opt/project_data/os_fans_images/2022/06/08/目录下了.
注意上传的时候的contentType需要为multipart-formdata或者formdata.
此时数据库中image字段在数据库中的值为os_fans_images/2022/05/09/17.png

返回

drf 的FileField的to_representation方法源码如下.

  1. class FileField(Field):
  2. ...
  3. def to_representation(self, value):
  4. if not value:
  5. return None
  6. use_url = getattr(self, 'use_url', api_settings.UPLOADED_FILES_USE_URL)
  7. if use_url:
  8. try:
  9. url = value.url
  10. except AttributeError:
  11. return None
  12. request = self.context.get('request', None)
  13. if request is not None:
  14. return request.build_absolute_uri(url)
  15. return url
  16. return value.name

value的值其实是一个django/db/models/fields/fields.py中的FieldFile类的实例.

  1. class FieldFile(File):
  2. ...
  3. @property
  4. def url(self):
  5. self._require_file()
  6. return self.storage.url(self.name)

self.storage的值时django中一个配置项,DEFAULT_FILE_STORAGE.默认值为FileSystemStorage的实例.

  1. class FileSystemStorage(Storage):
  2. ...
  3. @cached_property
  4. def base_url(self):
  5. if self._base_url is not None and not self._base_url.endswith('/'):
  6. self._base_url += '/'
  7. return self._value_or_setting(self._base_url, settings.MEDIA_URL)
  8. def url(self, name):
  9. if self.base_url is None:
  10. raise ValueError("This file is not accessible via a URL.")
  11. url = filepath_to_uri(name)
  12. if url is not None:
  13. url = url.lstrip('/')
  14. return urljoin(self.base_url, url)

可以看到在没有指定参数的情况下会将settings.MEDIA_URL拼接到数据库数据的前面
因此在to_representation方法的第10行.将os_fans_images/2022/05/09/17.png就变为了/files/os_fans_images/2022/05/09/17.png
然后,to_representation方法的第15行,build_absolute_uri会根据request的domain来将绝对路径拼接完成./files/os_fans_images/2022/05/09/17.png就变为了http://10.150.80.80/files/os_fans_images/2022/05/09/17.png

由于,通常django会跑在nginx的反向代理后面,所以build_absolute_uri方法会传入错误的协议或者domain.可以考虑重写这个序列化器字段.

  1. class BaseFileField(FileField):
  2. def to_representation(self, value):
  3. if not value:
  4. return None
  5. use_url = getattr(self, 'use_url', api_settings.UPLOADED_FILES_USE_URL)
  6. if use_url:
  7. try:
  8. url = value.url
  9. except AttributeError:
  10. return None
  11. # 20220218:相比父类移除了build_absolute_uri的调用以规避域名问题
  12. # request = self.context.get('request', None)
  13. # if request is not None:
  14. # return request.build_absolute_uri(url)
  15. return url
  16. return value.name

那么为什么不直接将user_url指定为false呢?
因为MEDIA_ROOT通常我们会用nginx代理.这样可以通过nginx来下载用户上传的文件.

  1. location /files/ {
  2. autoindex on;
  3. autoindex_exact_size off;
  4. autoindex_localtime on;
  5. charset utf-8,gbk;
  6. alias /opt/project_data/;
  7. }

这样前端收到的值就是/files/os_fans_images/2022/05/09/17.png一个相对路径了.会比较方便.