ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django+Mysql个人网盘源码实战:秒传、断点续传与权限隔离

Django+Mysql个人网盘源码实战:秒传、断点续传与权限隔离 简介这是一套基于Django与MySQL开发的个人网盘云盘系统源程序面向具备Python与Web开发基础、希望学习或二次开发网盘项目的开发者与计算机专业学生可用于课程设计、毕业设计或个人练手。压缩包共66个文件约1.21MB以21个py后端模块、15个html页面模板、10个js脚本为主另含4个sql初始化脚本、样式与图片等静态资源覆盖用户认证、文件管理、权限与容量限制等核心模块目录结构清晰便于按功能定位代码。项目已提供依赖清单与配置文件需自行调整邮箱和数据库参数、迁移数据库并导入基础SQL、创建超级用户后即可启动本地服务适合对照源码理解Django的模型、视图、序列化与模板渲染流程。目前已有1495人学习下载可作为入门云盘类项目、梳理后端接口设计与数据库表结构的实用参考。1. 从一份 DjangoMysql 个人网盘源码说起它到底能跑出什么很多人第一次搜「Python个人网盘源码」脑子里想的其实是另一件事能不能不依赖第三方服务自己搭一个能上传、下载、分享、多用户隔离的云盘系统。基于 DjangoMysql 的这套组合恰好是能满足这个诉求的最短路径——Django 自带用户体系、ORM、后台管理Mysql 负责元数据和用户数据持久化文件本体落本地磁盘或对象存储。它适合两类人一类是想拿一套能跑通的 Python 源码练手 Django 项目实战的新手另一类是想把现成网盘系统源程序改造成内部资料库的运维或后端。但这里有个反直觉的结论个人网盘源码的难点从来不在上传下载而在权限、去重和断点续传。上传下载随便一个 Django 视图就能写真正让系统从「玩具」变成「能用」的是用户 A 不能看到用户 B 的文件、同一份文件不重复占空间、大文件传一半断了能接着传。这套源码值不值得投入取决于它有没有把这三件事做扎实。下面我按「先立住原理、再动手复现、最后讲坑」的顺序把基于 DjangoMysql 的个人网盘从零到能用的路径拆开讲中间会给出可直接抄的模型、视图和配置。2. DjangoMysql 网盘的核心数据模型与存储选型2.1 为什么用户、文件、分享要拆成三张表网盘系统的数据关系本质是「一个用户拥有多个文件一个文件可以被多个分享引用」。如果偷懒把所有信息塞进一张表后面做权限校验和分享失效时会非常痛苦。常见做法是拆成三张核心表用户表直接用 Django 内置的auth.User文件表存元数据文件名、大小、哈希、存储路径、所属用户分享表存分享码、过期时间、关联文件。这里有个关键设计点文件表里存的是相对路径不是绝对路径。因为部署环境一变绝对路径就失效了。同时文件表要加一个file_hash字段用文件内容的 SHA256 值这是后面做秒传和去重的基础。# models.py import hashlib from django.db import models from django.contrib.auth.models import User class UserFile(models.Model): owner models.ForeignKey(User, on_deletemodels.CASCADE, related_namefiles) file_name models.CharField(max_length255) # 展示用文件名 file_size models.BigIntegerField(default0) # 字节数 file_hash models.CharField(max_length64, db_indexTrue) # SHA256用于秒传 storage_path models.CharField(max_length500) # 相对存储路径 created_at models.DateTimeField(auto_now_addTrue) class Meta: indexes [models.Index(fields[owner, file_hash])] class ShareLink(models.Model): file models.ForeignKey(UserFile, on_deletemodels.CASCADE) share_code models.CharField(max_length16, uniqueTrue) expire_at models.DateTimeField(nullTrue, blankTrue) download_count models.IntegerField(default0)逻辑说明file_hash加索引是为了秒传查询快storage_path存相对路径保证可迁移ShareLink用独立分享码而不是直接暴露文件 ID避免被遍历。参数上file_size用BigIntegerField而不是IntegerField因为个人网盘里几个 G 的文件很常见IntegerField上限约 21 亿字节也就是 2G会溢出。2.2 存储后端本地磁盘还是对象存储新手最容易纠结的就是文件存哪。我的建议是先本地磁盘跑通再考虑对象存储。本地磁盘用 Django 的MEDIA_ROOT配置即可零依赖调试方便。对象存储如 S3 兼容接口适合多机部署但会引入网络延迟和额外配置成本。存储方式适用场景优点代价本地磁盘单机、内网、练手零依赖、读写快无法多机共享、备份麻烦对象存储多机、公网可扩展、天然备份需配置密钥、有流量成本数据库 BLOB极小文件事务一致性能差不推荐配置本地存储只需要在settings.py里指定两个路径并确保目录存在且进程有写权限# settings.py import os BASE_DIR os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media) FILE_UPLOAD_MAX_MEMORY_SIZE 2621440 # 2.5MB 以上走临时文件 DATA_UPLOAD_MAX_MEMORY_SIZE None # 不限制表单内存FILE_UPLOAD_MAX_MEMORY_SIZE控制的是「小于这个值放内存大于这个值写临时文件」不是上传大小上限。很多人误以为改它就能限制上传大小结果大文件照样传进来真正的限制要在视图里自己判断request.FILES[file].size。3. 上传、秒传与断点续传的落地实现3.1 用分块哈希实现秒传秒传的逻辑是前端先算文件哈希发给后端后端查这个哈希是否已存在存在就直接在用户文件表里插一条记录指向已有存储路径不用真的再传一遍。这里有个坑——大文件整体算哈希很慢常见做法是取文件头、中、尾各一段加上文件大小拼起来算一个「采样哈希」速度快且碰撞概率低。# utils.py import hashlib def sample_hash(file_obj, chunk1024 * 1024): 对大文件采样计算哈希避免全量读取 file_obj.seek(0, 2) size file_obj.tell() hasher hashlib.sha256() hasher.update(str(size).encode()) # 取头、中、尾三个 1MB 块 offsets [0, max(0, size // 2 - chunk // 2), max(0, size - chunk)] for off in offsets: file_obj.seek(off) hasher.update(file_obj.read(chunk)) file_obj.seek(0) return hasher.hexdigest()逻辑说明seek(0, 2)把指针移到文件末尾拿到大小然后分别读头、中、尾三块。参数chunk默认 1MB太小采样不准太大读取慢。这个哈希只用于秒传判断不用于完整性校验所以采样足够。3.2 上传视图与权限校验上传视图要做三件事校验登录、算哈希、判断秒传或落盘。注意 Django 默认的request.FILES在文件大于FILE_UPLOAD_MAX_MEMORY_SIZE时会存到临时文件所以直接读file_obj是安全的。# views.py import os from django.conf import settings from django.http import JsonResponse from django.contrib.auth.decorators import login_required from .models import UserFile from .utils import sample_hash login_required def upload(request): if request.method ! POST: return JsonResponse({code: 400, msg: method not allowed}) f request.FILES.get(file) if not f: return JsonResponse({code: 400, msg: no file}) fhash sample_hash(f) # 秒传已有相同哈希的文件直接建引用 exist UserFile.objects.filter(file_hashfhash).first() if exist: UserFile.objects.create( ownerrequest.user, file_namef.name, file_sizef.size, file_hashfhash, storage_pathexist.storage_path) return JsonResponse({code: 0, msg: instant, hash: fhash}) # 正常落盘 rel_path fuploads/{request.user.id}/{fhash[:2]}/{fhash} abs_path os.path.join(settings.MEDIA_ROOT, rel_path) os.makedirs(os.path.dirname(abs_path), exist_okTrue) with open(abs_path, wb) as dst: for chunk in f.chunks(): dst.write(chunk) UserFile.objects.create( ownerrequest.user, file_namef.name, file_sizef.size, file_hashfhash, storage_pathrel_path) return JsonResponse({code: 0, msg: ok, hash: fhash})逻辑说明f.chunks()是 Django 提供的分块读取方法默认每块 64KB避免一次性把大文件读进内存。存储路径用fhash[:2]做二级目录防止单目录文件过多导致ls卡死。参数上rel_path里带request.user.id是为了物理隔离但秒传时多个用户会指向同一路径这是设计预期——去重的收益大于隔离需求。3.3 断点续传的最小实现断点续传需要前端配合上传前先问后端「这个哈希已经传了多少字节」后端返回已传偏移量前端从该偏移继续。后端要维护一个上传会话表记录哈希、已传大小、临时文件路径。# models.py 追加 class UploadSession(models.Model): file_hash models.CharField(max_length64, db_indexTrue) uploaded_size models.BigIntegerField(default0) temp_path models.CharField(max_length500) updated_at models.DateTimeField(auto_nowTrue)前端每次上传前调/upload/status/?hashxxx后端返回uploaded_size前端用slice(uploaded_size)切文件继续传。这里的关键是临时文件必须按哈希命名这样同一个文件多次续传能定位到同一个临时文件。合并完成后删除会话记录。这套逻辑不复杂但前端切片和后端偏移量对不齐是最高频的翻车点建议先用小文件把偏移量打印出来核对。4. 权限隔离、分享与 Mysql 连接池的避坑清单4.1 权限隔离别只靠前端隐藏现象用户 A 通过改 URL 里的文件 ID能下载到用户 B 的文件。 原因下载视图只判断了登录没判断文件归属。 解决所有涉及文件的查询都必须带ownerrequest.user过滤分享下载则校验分享码有效性。login_required def download(request, file_id): obj UserFile.objects.filter(idfile_id, ownerrequest.user).first() if not obj: return JsonResponse({code: 403, msg: forbidden}) # 返回文件流...这条规则要刻进肌肉记忆任何按 ID 取资源的接口查询条件里必须带当前用户。Django 的get_object_or_404如果不加过滤就是越权漏洞。4.2 分享链接的过期与计数现象分享链接设置了 7 天过期结果第 8 天还能下载。 原因只存了expire_at下载时没做时间比较。 解决下载前判断expire_at是否小于当前时间同时用F()表达式原子递增下载次数避免并发下计数丢失。from django.db.models import F from django.utils import timezone def share_download(request, code): link ShareLink.objects.filter(share_codecode).first() if not link or (link.expire_at and link.expire_at timezone.now()): return JsonResponse({code: 404, msg: expired}) ShareLink.objects.filter(idlink.id).update(download_countF(download_count) 1) # 返回文件流...用F()而不是link.download_count 1; link.save()是因为后者在并发下会读到旧值导致计数偏小。4.3 Mysql 连接与字符集现象启动报Error 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock。 原因Mysql 没启动或者 Django 配置的HOST是localhost但 socket 路径不对。 解决先确认 Mysql 服务在跑配置里把HOST改成127.0.0.1强制走 TCP避开 socket 路径问题。DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: netdisk, USER: netdisk, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, } }utf8mb4是必须的因为文件名里可能有 emoji 或生僻字老的utf8存不下会报错。STRICT_TRANS_TABLES让字段超长直接报错而不是静默截断早暴露问题。4.4 大文件上传超时与 Nginx 限制现象本地测试传 500MB 没问题部署到 Nginx 后传 100MB 就断。 原因Nginx 默认client_max_body_size是 1MB且proxy_read_timeout默认 60 秒。 解决在 Nginx 配置里调大这两个值同时 Django 侧确认没有中间件限制请求体大小。client_max_body_size 2048m; proxy_read_timeout 600s; proxy_send_timeout 600s;这三个值要按实际最大文件大小和网络速度估算别盲目设成无限大否则容易被恶意大请求拖垮。4.5 静态文件 404现象vscode里写img标签引用 Django static 文件页面显示不了。 原因开发模式下没配static路由或者DEBUGFalse后 Django 不再自动服务静态文件。 解决开发时在urls.py里加static(settings.STATIC_URL, document_rootsettings.STATIC_ROOT)生产环境交给 Nginx 处理。from django.conf import settings from django.conf.urls.static import static urlpatterns [ # ... 你的路由 ] static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)注意这段只在DEBUGTrue时生效上线后必须由 Nginx 或 CDN 接管否则性能极差。5. 把网盘跑稳的三个进阶技巧第一个技巧是用数据库连接池扛并发。Django 默认每个请求新建一个 Mysql 连接并发一高就报too many connections。常见做法是引入django-db-connection-pool或直接用CONN_MAX_AGE复用连接。我一般先设CONN_MAX_AGE60让连接在 60 秒内复用成本最低。DATABASES[default][CONN_MAX_AGE] 60第二个技巧是给文件哈希加唯一约束前先想清楚。如果你希望同一用户重复上传同一文件只保留一条记录可以加unique_together (owner, file_hash)但如果你希望保留每次上传的历史就别加。这个决定一旦定了后面改起来要迁移数据属于典型的「后悔药不好买」。第三个技巧是用管理命令定期清理孤儿文件。用户删除记录后磁盘上的物理文件不会自动消失。写一个clean_orphan_files管理命令对比数据库里的storage_path和磁盘实际文件删掉没有引用的。# management/commands/clean_orphan_files.py import os from django.conf import settings from django.core.management.base import BaseCommand from app.models import UserFile class Command(BaseCommand): help 删除数据库无引用的物理文件 def handle(self, *args, **options): used set(UserFile.objects.values_list(storage_path, flatTrue)) root settings.MEDIA_ROOT removed 0 for dirpath, _, filenames in os.walk(root): for name in filenames: abs_path os.path.join(dirpath, name) rel os.path.relpath(abs_path, root).replace(\\, /) if rel not in used: os.remove(abs_path) removed 1 self.stdout.write(fremoved {removed} orphan files)跑之前一定先备份或者先加--dry-run参数只打印不删除。我自己就干过没备份直接跑、结果把还在上传中的临时文件一起删了的事血泪经验。验证这套系统是否真的能用我的习惯是拿三个场景压一遍同一文件传两次看是否秒传、用户 A 改 URL 下载用户 B 的文件看是否 403、传一个 1GB 文件中途断网再续传看偏移量对不对。这三个过了基本就能交付。希望帮到你。本文还有配套的精品资源点击获取
返回列表