使用DRF框架时,默认的图片上传因为自由度低,比较鸡肋,业务需求往往需要一个单独的图片上传接口,在此记录一下实现代码及研究时搞清楚的一些问题。
假设写一个门户网站,新闻模块需要上传图片,图片参数名为img。
图片存储在media文件夹下,配置如下:
from pathlib import Path
MEDIA_URL = '/media/'
PUB_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = os.path.join(PUB_DIR, "media")
orm设置存储图片字段,设计如下:
from django.db import models
# 新闻
class News(models.Model):
"""
新闻
"""
...
img = models.ImageField('展示图片', upload_to="news_img/%Y/%m/", max_length=256, blank=True)
...
class Meta:
verbose_name = '新闻'
verbose_name_plural = '新闻'
反序列化参数设置如下:
from rest_framework import serializers
class NewsImgUploadSerializer(serializers.Serializer):
img = serializers.ImageField(
label="图片",
max_length=256, # 图片名最大长度
use_url=True, # 设为True则URL字符串值将用于输出表示。设为False则文件名字符串值将用于输出表示
error_messages={
'invalid': '图片参数错误'
}
)
import logging
from django_filters.rest_framework.backends import DjangoFilterBackend
from django.http import Http404
from rest_framework import mixins, filters, viewsets
from rest_framework import status as rest_status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import exceptions as rest_framework_exceptions
import utils
from . import models, serializers
logger = logging.getLogger(__name__)
# 新闻
class NewsViewSet(viewsets.GenericViewSet):
"""
新闻
获取新闻列表,输入参数解释:
status: 按新闻审核状态筛选
category: 按新闻分类筛选
search: 按关键词搜索
搜索字段包括:('title', 'content')
ordering: 按字段排序(默认为-update_time)
可选列表为:('-update_time', )
默认为正序,如需逆序,在前面加中横杠,例如'-update_time'
"""
filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter,)
filter_fields = ('status', 'category')
search_fields = ('title', 'content')
ordering_fields = ('id', 'update_time')
ordering = ('-update_time', )
def get_queryset(self):
return models.News.objects.all()
def get_serializer_class(self):
if self.action == 'img_upload':
return serializers.NewsImgUploadSerializer
@action(detail=False, methods=['post'], url_path="img_upload")
def img_upload(self, request, pk=None):
"""
上传新闻图片
:param request: img 图片路径
:return:
"""
try:
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
image = serializer.validated_data['img']
img_file = "news_img" # 图片存储的文件夹
img_name = utils.save_img(image, img_file)
img_url = utils.get_img_url(request, img_file, img_name)
return Response(status=rest_status.HTTP_201_CREATED, data=img_url)
# 未知错误,报服务器内部错误
except Exception as error:
logger.error('图片上传失败,错误: %s' % (error))
return Response(status=rest_status.HTTP_500_INTERNAL_SERVER_ERROR, data={"detail": "服务器内部错误"})
这里有一个小知识点,从serializer中获取到的图片属于InMemoryUploadedFile类型,这种类型数据可以视为一个结构体,要获取到其中的属性可以使用如下的方式:
image_data = [image.file, image.field_name, image.name, image.content_type, image.size, image.charset, image.content_type_extra]
获取到类型的图片后,对图片做了转存操作,方法封装在了utils.py中
import random
import os
import datetime
from pathlib import Path
from django.conf import settings
def ranstr(num):
H = 'abcdefghijklmnopqrstuvwxyz0123456789'
H0 = 'abcdefghijklmnopqrstuvwxyz'
salt = ''
salt += random.choice(H0)
for i in range(num-1):
salt += random.choice(H)
return salt
# 接收并保存图片
def save_img(image, dest_father_dir):
# 创建存储路径
img_dir1 = os.path.join(settings.MEDIA_ROOT, dest_father_dir)
if not os.path.exists(img_dir1):
os.mkdir(img_dir1)
img_dir2 = os.path.join(img_dir1, datetime.datetime.now().strftime("%Y"))
if not os.path.exists(img_dir2):
os.mkdir(img_dir2)
img_file = os.path.join(img_dir2, datetime.datetime.now().strftime("%m"))
if not os.path.exists(img_file):
os.mkdir(img_file)
# 防重名
p = Path(image.name)
img_pure_name = p.stem + ranstr(5)
img_extend_name = p.suffix
img_name = img_pure_name + img_extend_name
# 存储图片
destination = open(os.path.join(img_file, img_name), 'wb+')
for chunk in image.chunks():
destination.write(chunk)
destination.close()
return img_name
# 获取图片存储地址
def get_img_url(request, img_file, img_name):
if request.is_secure():
protocol = 'https'
else:
protocol = 'http'
# 传回给后端ImageField要存储的图片路径
backend_relative_path = img_file + '/' + datetime.datetime.now().strftime("%Y") + '/' + datetime.datetime.now().strftime("%m") + '/' + img_name
relative_path = settings.MEDIA_URL + backend_relative_path
# 前端显示需要的图片路径
frontend_url = protocol + '://'+ str(request.META['HTTP_HOST']) + relative_path
return {"url": frontend_url, "backend_path": backend_relative_path}
urls.py就没什么好写的了,略
以上就实现了自定义的图片上传接口,前端上传图片后获得两种图片url,其中url是前端显示用的,可以用于上传后预览图片,backend_path用于传回后端的img字段,将图片存储路径保存起来。
关于django中InMemoryUploadedFile图片对象的使用方法 - 旷古的寂寞的博客
django上传文件 - Mr.风的影子的博客
Django中HttpRequest常用参数介绍 - 如何好听的博客
[pathlib]内置pathlib库的常用属性和方法 - [sigai]的博客
学习django之python中os模块的函数 - 心海星的博客