Django REST framework 自定义图片上传接口

前言

使用DRF框架时,默认的图片上传因为自由度低,比较鸡肋,业务需求往往需要一个单独的图片上传接口,在此记录一下实现代码及研究时搞清楚的一些问题。

业务场景

假设写一个门户网站,新闻模块需要上传图片,图片参数名为img。

代码实现

settings.py

图片存储在media文件夹下,配置如下:

from pathlib import Path

MEDIA_URL = '/media/'

PUB_DIR = Path(__file__).resolve().parent.parent

MEDIA_ROOT = os.path.join(PUB_DIR, "media")

models.py

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 = '新闻'

serializers.py

反序列化参数设置如下:

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': '图片参数错误'
        }
    )

views.py

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中

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模块的函数 - 心海星的博客

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值