Django设置Tus大文件分块上传

后端
观看:0
文章标签:#django#tus#upload#patch#分块上传#signal
最后更新:2025年08月08 14:00

对于大文件的上传,分块、断点续传功能是必须的,借助drf-tus工具可以实现

安装必要的包

shell
pip install drf-tus

pip install redis
pip install django-redis

pip install celery
pip install django-celery-beat
pip install django-celery-results

pip install django-cors-headers

直接把对应的redis和celery也安装上

1、设置cors权限

django默认不允许请求头携带的这些内容,需要放行

把下面的内容写入到base.py内(如果没有区分生产、开发,就写在settings.py内)

python
from corsheaders.defaults import default_headers

# 改为True即为可跨域设置Cookie
CORS_ALLOW_CREDENTIALS = True
# 扩展允许tus上传
CORS_ALLOW_HEADERS = list(default_headers) + [
    'Tus-Resumable',
    'Upload-Length',
    'Upload-Offset',
    'Upload-Metadata',
]

CORS_EXPOSE_HEADERS = [
    'Location',
    'Upload-Offset',
    'Upload-Length',
    'Tus-Resumable',
    'Content-Disposition',
    'Content-Length'
]
# 配置允许的请求方式
CORS_ALLOW_METHODS = [
    'DELETE',
    'GET',
    'OPTIONS',
    'PATCH',
    'POST',
    'PUT',
    'HEAD',
]

2、设置APPs

增加需要的包:除了自己注册的app,还需要把corsheaders、celery、rest_framework_tus等加进去,看清楚顺序,不要颠倒,否则会出现问题

python

INSTALLED_APPS = [
    # ——— Django 内置 Apps ———
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # ——— 自定义 App(必须在 drf-tus 之前) ———
    'appchemnote',
    'appuser',
    # ——— 第三方库 ———
    'corsheaders',
    'celery',
    'django_celery_beat',
    'django_celery_results',
    'django.contrib.postgres',
    # ——— DRF & JWT & tus ———
    'rest_framework',
    'rest_framework.authtoken',
    'rest_framework_simplejwt',
    'rest_framework_simplejwt.token_blacklist',
    'rest_framework_tus',  # <— 一定要在此之前已经加载了 appchemnote
]

3、设置redis

其实是后续配置的,感觉tus上传应该是依赖celery和redis的,是有任务ID的,这里一起配置吧:

python
# 设置redi后端缓存
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        }
    }
}

CELERY_BROKER_URL = 'redis://127.0.0.1:6379/0'  # 消息代理地址
CELERY_RESULT_BACKEND = 'django-db'  # 使用 Django 数据库作为结果后端  安装完成后记得迁移一下

CELERY_WORKER_CONCURRENCY = 2  # Worker并发数量,一般默认CPU核数,可以不设置
CELERYD_FORCE_EXECV = True  # 非常重要,有些情况下可以防止死锁
CELERY_WORKER_MAX_TASKS_PER_CHILD = 100  # celery 的 worker 执行多少个任务后进行重启操作
CELERY_TASK_TRACK_STARTED = True  # 设置任务更新状态:启用后,任务开始执行时,状态会从 "PENDING" 更新为 "STARTED"
# CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24  # 任务结果过期时间,秒  设置为0表示不过期
CELERY_TASK_RESULT_EXPIRES = 0  # 任务结果过期时间,秒  设置为0表示不过期

CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True  # 断开重新连接
CELERY_BROKER_CONNECTION_MAX_RETRIES = 10  # 默认重新连接 100 次

CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Asia/Shanghai'               # 可选:设置时区

详细的配置参数可以后续查询资料,先这样

4、配置参数

drf-tus提供了配置,
from rest_framework_tus import settings


这个文件内可以查看其他的配置,记得千万不要在settings文件内导入这个设置,否则会覆盖设置,导致严重问题

python
GB = 1024**3
BIGFILE_UPLOAD_MAX_MEMORY_SIZE = 1 * GB  # 大文件最高允许上传1G文件

REST_FRAMEWORK_TUS = {
    "UPLOAD_MODEL": "appchemnote.LargeUpload",  # 配置模型,写入user信息
    "MAX_FILE_SIZE": BIGFILE_UPLOAD_MAX_MEMORY_SIZE
}

这里的LargeUpload只是为了定义存放的时候,把用户和其他信息单独写入数据库:

这是对应的models.py内容,这里会建立关联信息,

python
class LargeUpload(BaseUpload):
    '重写的tus上传模型,用来关联用户信息'
    user = models.ForeignKey(
        User,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="tus_uploads",
    )
    # 大块上传完毕后,需要把新的url存储,使用handlers返回给前端
    resource_id = models.IntegerField(null=True, blank=True)
    resource_new_url = models.URLField(null=True, blank=True, max_length=1024)

看数据库是使用外键,重新挂了一些其他内容进去,之前尝试过重写模型,但是由于无法修改子类的内容,所以放弃

5、配置中间件

按照位置把TusMiddleware的中间件,配置上

python
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',  # 安全
    'corsheaders.middleware.CorsMiddleware',  # cors
    'django.contrib.sessions.middleware.SessionMiddleware',  # 会话
    'rest_framework_tus.middleware.TusMiddleware',  # 分块上传
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

6、配置自定义上传操作

在上传完成后,需要把最后一次的patch请求从204改为200,告诉前端,文件上传完成了,因此需要自定义

在app内新建tus_handlers.py文件:

python
from rest_framework_tus.views import UploadViewSet
from rest_framework.response import Response
from rest_framework import status


class CustomUploadViewSet(UploadViewSet):
    http_method_names = ['post', 'patch', 'head', 'options']

    # 自定义 head 方法
    def head(self, request, *args, **kwargs):
        try:
            # 获取上传对象
            upload = self.get_object()
            # 创建 204 No Content 响应
            response = Response(status=status.HTTP_204_NO_CONTENT)
            # 设置 tus 协议所需的头部信息
            response['Upload-Offset'] = upload.upload_offset
            response['Upload-Length'] = upload.upload_length
            response['Tus-Resumable'] = '1.0.0'  # tus 协议版本
            return response
        except Exception:
            # 如果上传对象不存在,返回 404
            return Response(status=status.HTTP_404_NOT_FOUND)

    def finalize_response(self, request, response, *args, **kwargs):
        response = super().finalize_response(request, response, *args, **kwargs)

        if request.method.upper() == "PATCH" and response.status_code == 204:
            upload = self.get_object()
            print(upload.resource_new_url)
            if upload.upload_offset == upload.upload_length:
                response.data = {
                    "message": "大文件上传完成!",
                    "result":{
                        "filename": upload.filename,
                        "media_id": upload.resource_id,
                        "media_url": upload.resource_new_url,
                    },
                }
                response.status_code = 200
        return response

7、配置urls

在主路由文件内。配置如下:

这里无论如何配置,都会导致patch的请求发生变化,

测试完成后,发现只能通过这种方式配置大文件上传的url路径:

python
from appchemnote.tus_handlers import CustomUploadViewSet
from rest_framework.routers import DefaultRouter

# 挂载注册api的router
api_router = DefaultRouter()
api_router.register("files", CustomUploadViewSet, basename="upload")
tus_urlpatterns = [
    path("", include((api_router.urls, "api"), namespace="api")),
]

urlpatterns = [
    path('dgapi/admin/', admin.site.urls),
    # 分块上传
    path("dgapi/tus/", include((tus_urlpatterns, "rest_framework_tus"), namespace="rest_framework_tus")),
    # 其他路由文件
    path('dgapi/chemnote/', include('appchemnote.urls')),
]

# 仅在开发环境中,才允许直接指向静态资源
if settings.DEBUG:
    # 仅在 DEBUG=True(开发)时,用 Django serve /media/ 和 /static/
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    urlpatterns += staticfiles_urlpatterns()

后面的设置是为了防止用户,直接根据路径,拿到服务器的文件资源,这里做一下判断就行

MEDIA_ROOT表示上传的资源等,不是头像、图片,这类一般放在static内,是网站的静态资源

这里其实对于后端来说,tus的接收逻辑就配置完了

在传递完成后,会把最后一次请求返回的状态更改为200,把文件路径也存储到数据库内了

8、使用django的signal对上传过程进行限制

在需要使用信号的app内创建signal.py文件,用来写限制逻辑

举例:

我现在的app名称为:appchemnote,我需要在这个文件夹内新建signal.py文件

然后在appchemnote/app.py内,把这个信号注册:

python
from django.apps import AppConfig

class AppchemnoteConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'appchemnote'

    def ready(self):
        # 导入 signals.py,让 @receiver 注册钩子
        import appchemnote.signal

需求1:只有登录的用户才允许上传大文件,而且有最大使用空间的限制

python
from rest_framework_tus import signals
from rest_framework.exceptions import PermissionDenied

# 使用装饰器signals.receiving表示在接收文件的时候

@receiver(signals.receiving)
def tus_upload_receiving(sender, instance, **kwargs):
    upload = instance
    user = getattr(upload, "user")
    print('user-----', user)
    if not user:
        raise PermissionDenied("获取用户信息失败")
    # if not (user.is_vip or user.is_admin):
    #     raise PermissionDenied("只有 VIP 用户或管理员才能上传文件资源。")
    # 查询还剩下多少,如果超出就不让上传了
    qs_resource = NoteResource.objects.filter(
        user=user,
        is_visible=True,
        is_temp=False,
    )
    # ✅ 资源总大小(单位:字节)
    resource_memory = qs_resource.aggregate(
        total=Coalesce(Sum('file_size'), 0)
    )['total']

    max_memory = user.max_memory
    print('已经使用:', resource_memory)
    print('一共:', max_memory)
    if resource_memory > max_memory:
        raise PermissionDenied("超出存储限制了!")

查看了一下源码,有这4个阶段可以控制:

python
from django.dispatch import Signal

receiving = Signal()
received = Signal()
saving = Signal()
saved = Signal()
finished = Signal()

可以根据需要,自行研究

比如你想在上传完成后,移动到新的文件夹内,或者把文件大小等信息写入、压缩等,就可以使用finished进行操作

请登录后再发表评论
🔍 快速搜索
文章推荐
基于文本相似性

没有推荐的文章...

文章推荐
化学结构同出现

没有对应的文章...

AioChem © 2025

晋ICP备2025060790号-1