DRF 文件上传实战 #

一、文件上传概述 #

1.1 功能需求 #

  • 单文件上传
  • 多文件上传
  • 图片处理(缩略图)
  • 文件预览
  • 文件管理

1.2 配置 #

python
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'

INSTALLED_APPS = [
    ...
    'rest_framework',
]

二、模型设计 #

2.1 文件模型 #

python
from django.db import models
from django.contrib.auth.models import User
import os
import uuid

def upload_to(instance, filename):
    ext = filename.split('.')[-1]
    filename = f'{uuid.uuid4()}.{ext}'
    return os.path.join('uploads', str(instance.uploaded_at.year), str(instance.uploaded_at.month), filename)

class File(models.Model):
    FILE_TYPES = [
        ('image', '图片'),
        ('document', '文档'),
        ('video', '视频'),
        ('audio', '音频'),
        ('other', '其他'),
    ]
    
    file = models.FileField(upload_to=upload_to)
    filename = models.CharField(max_length=255)
    file_type = models.CharField(max_length=20, choices=FILE_TYPES)
    file_size = models.PositiveIntegerField()
    mime_type = models.CharField(max_length=100)
    uploader = models.ForeignKey(User, on_delete=models.CASCADE, related_name='files')
    description = models.TextField(blank=True)
    uploaded_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name = '文件'
        verbose_name_plural = '文件'
        ordering = ['-uploaded_at']
    
    def __str__(self):
        return self.filename
    
    def save(self, *args, **kwargs):
        if self.file:
            self.filename = self.file.name
            self.file_size = self.file.size
        super().save(*args, **kwargs)

2.2 图片模型 #

python
from PIL import Image
from io import BytesIO
from django.core.files.base import ContentFile

class Image(models.Model):
    original = models.ImageField(upload_to='images/original/')
    thumbnail = models.ImageField(upload_to='images/thumbnail/', blank=True)
    medium = models.ImageField(upload_to='images/medium/', blank=True)
    uploader = models.ForeignKey(User, on_delete=models.CASCADE, related_name='images')
    alt_text = models.CharField(max_length=255, blank=True)
    uploaded_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name = '图片'
        verbose_name_plural = '图片'
        ordering = ['-uploaded_at']
    
    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        
        if self.original and not self.thumbnail:
            self.create_thumbnails()
    
    def create_thumbnails(self):
        img = Image.open(self.original)
        
        # Thumbnail (150x150)
        thumb = img.copy()
        thumb.thumbnail((150, 150))
        thumb_io = BytesIO()
        thumb.save(thumb_io, format=img.format or 'JPEG')
        self.thumbnail.save(
            f'thumb_{self.original.name}',
            ContentFile(thumb_io.getvalue()),
            save=False
        )
        
        # Medium (800x800)
        medium = img.copy()
        medium.thumbnail((800, 800))
        medium_io = BytesIO()
        medium.save(medium_io, format=img.format or 'JPEG')
        self.medium.save(
            f'medium_{self.original.name}',
            ContentFile(medium_io.getvalue()),
            save=False
        )
        
        super().save()

三、序列化器 #

3.1 文件序列化器 #

python
class FileSerializer(serializers.ModelSerializer):
    file_url = serializers.SerializerMethodField()
    uploader_name = serializers.CharField(source='uploader.username', read_only=True)
    
    class Meta:
        model = File
        fields = ['id', 'file', 'file_url', 'filename', 'file_type', 'file_size', 
                  'mime_type', 'uploader', 'uploader_name', 'description', 'uploaded_at']
        read_only_fields = ['id', 'filename', 'file_type', 'file_size', 'mime_type', 'uploader', 'uploaded_at']
    
    def get_file_url(self, obj):
        request = self.context.get('request')
        if obj.file and request:
            return request.build_absolute_uri(obj.file.url)
        return None
    
    def create(self, validated_data):
        file = validated_data['file']
        
        validated_data['filename'] = file.name
        validated_data['file_size'] = file.size
        validated_data['mime_type'] = file.content_type
        
        import mimetypes
        mime_type = file.content_type
        if mime_type.startswith('image'):
            validated_data['file_type'] = 'image'
        elif mime_type.startswith('video'):
            validated_data['file_type'] = 'video'
        elif mime_type.startswith('audio'):
            validated_data['file_type'] = 'audio'
        elif 'pdf' in mime_type or 'document' in mime_type:
            validated_data['file_type'] = 'document'
        else:
            validated_data['file_type'] = 'other'
        
        return super().create(validated_data)

3.2 图片序列化器 #

python
class ImageSerializer(serializers.ModelSerializer):
    original_url = serializers.SerializerMethodField()
    thumbnail_url = serializers.SerializerMethodField()
    medium_url = serializers.SerializerMethodField()
    
    class Meta:
        model = Image
        fields = ['id', 'original', 'original_url', 'thumbnail', 'thumbnail_url',
                  'medium', 'medium_url', 'alt_text', 'uploaded_at']
        read_only_fields = ['id', 'thumbnail', 'medium', 'uploaded_at']
    
    def get_original_url(self, obj):
        request = self.context.get('request')
        if obj.original and request:
            return request.build_absolute_uri(obj.original.url)
        return None
    
    def get_thumbnail_url(self, obj):
        request = self.context.get('request')
        if obj.thumbnail and request:
            return request.build_absolute_uri(obj.thumbnail.url)
        return None
    
    def get_medium_url(self, obj):
        request = self.context.get('request')
        if obj.medium and request:
            return request.build_absolute_uri(obj.medium.url)
        return None

四、视图 #

4.1 文件视图 #

python
class FileViewSet(viewsets.ModelViewSet):
    serializer_class = FileSerializer
    permission_classes = [IsAuthenticated]
    parser_classes = [MultiPartParser, FormParser]
    
    def get_queryset(self):
        queryset = File.objects.all()
        
        if not self.request.user.is_staff:
            queryset = queryset.filter(uploader=self.request.user)
        
        file_type = self.request.query_params.get('file_type')
        if file_type:
            queryset = queryset.filter(file_type=file_type)
        
        return queryset
    
    def perform_create(self, serializer):
        serializer.save(uploader=self.request.user)
    
    @action(detail=False, methods=['post'])
    def upload_multiple(self, request):
        files = request.FILES.getlist('files')
        
        if not files:
            return Response({'error': '请选择文件'}, status=400)
        
        results = []
        for file in files:
            file_obj = File.objects.create(
                file=file,
                filename=file.name,
                file_size=file.size,
                mime_type=file.content_type,
                uploader=request.user
            )
            results.append(FileSerializer(file_obj, context={'request': request}).data)
        
        return Response({'files': results}, status=201)

4.2 图片视图 #

python
class ImageViewSet(viewsets.ModelViewSet):
    serializer_class = ImageSerializer
    permission_classes = [IsAuthenticated]
    parser_classes = [MultiPartParser, FormParser]
    
    def get_queryset(self):
        queryset = Image.objects.all()
        
        if not self.request.user.is_staff:
            queryset = queryset.filter(uploader=self.request.user)
        
        return queryset
    
    def perform_create(self, serializer):
        serializer.save(uploader=self.request.user)

五、URL配置 #

python
from django.conf import settings
from django.conf.urls.static import static
from rest_framework.routers import DefaultRouter
from .views import FileViewSet, ImageViewSet

router = DefaultRouter()
router.register(r'files', FileViewSet)
router.register(r'images', ImageViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

六、API接口 #

接口 方法 说明
/api/files/ GET 文件列表
/api/files/ POST 上传文件
/api/files/upload_multiple/ POST 多文件上传
/api/images/ GET 图片列表
/api/images/ POST 上传图片

七、总结 #

本章实现了完整的文件上传功能:

  • 单文件和多文件上传
  • 图片自动缩略图
  • 文件类型识别
  • 文件管理

文件上传是API常见功能!

最后更新:2026-03-28