简介:在数据分散于手机、电脑、平板等设备的日常中,个人文件管理成为高频需求。网盘技术本质是文件存储与同步的工程化实践,而Python作为高效后端语言,配合Django框架成熟的全栈能力,能够快速构建可靠的文件服务。Django提供的ORM、认证体系与Admin后台,结合MySQL稳定的事务支持,保障了文件元数据的一致性与并发安全。在此基础上,秒传、断点续传、分享链接等功能提炼出文件传输的通用原理,使系统兼具实用性与学习价值。本文完整呈现一个基于Django+MySQL的个人网盘项目,涵盖数据库建模、上传下载、权限控制及线上部署踩坑,适合开发者构建私有云盘或学习Web工程实践。 不知道你有没有遇到过这种场景:手机里存了几百张照片,电脑上有一堆工作文档,平板里还有十几个PDF教程,想互相传一下全靠微信“文件传输助手”,急了就直接开数据线,再狠一点就买个U盘来回插。我反正折腾了很久,最后索性花了一个周末,用 Python 自己搭了一个网盘。项目不大,但五脏俱全,基于 Django + MySQL,前后端加权限、上传下载、分享、秒传全都有。这套源码我放在 GitHub 上,也实跑过真机,下文把完整设计思路、核心实现和踩坑记录全部整理出来,给想做个人网盘、毕设或者练手 Django 的朋友当参考。
先交代清楚这套东西的价值:它不需要你懂分布式,也不用上 Kafka、HDFS 这种重型依赖,就是一套主流的 Django 单体项目,配合 MySQL 做主存储,文件系统存物理文件。它解决的问题很直接——让一个人(或者一个小团队)把散落在各设备的文件收敛到一台自己可控的服务器上,通过网页完成上传、浏览、下载、分享、回收站这些核心操作。适合的人群很明确:熟悉 Python 基础,想用 Django 完整走一遍后端项目的同学;被各类网盘限速搞烦了、想自己捏一个私有云盘的开发者;以及正在选型做个人网站 / NAS 文件模块的人。
- 项目核心需求与整体架构设计
1.1 个人网盘到底要解决哪些核心问题
网盘乍一听无非是上传下载四个字,但真上手做就会发现,很多细节没有想清楚的话,后面每一个功能都会打架。我最初把需求拆成了四块:文件管理、用户系统、分享协作、可靠性保障。文件管理是基本面,包含文件的上传、下载、删除、重命名、移动,以及目录的创建和浏览;用户系统解决的是“你上传的东西不能被别人看到”,所以注册、登录、鉴权、用户间隔离是必须的;分享协作看起来是加分项,但实际用起来概率极高,给同事传个包、给朋友发个相册,链接 + 密码的方式最省事;可靠性保障则是上线后才会意识到多重要的东西,比如回收站、断点续传、秒传、重复文件处理,这些都属于“没有也能跑,有了才敢用”的范畴。
1.2 为什么选择 Django + MySQL 而不是 Flask 或其他方案
选型期我也想过用 Flask FastAPI 自己拼插线板,但最后被 Django 的“全家桶”属性拉回来了。Django 自带 Admin 后台、ORM、Auth 认证体系、表单与校验、中间件机制,这些对网盘项目属于必需品:Auth 帮你搞定注册登录和 session 会话,避免自己写密码加盐和 session 管理;Admin 后台可以直接体检整个文件表记录,调试时不用开数据库客户端;ORM 对 MySQL 的支持很成熟,迁移建表一条命令搞定。换句话说,一个人开发的时候,Django 把大量底层后勤扛了,你专注写文件业务就行。MySQL 选择的原因也很朴素:它稳定、部署普遍、运维资料多,且对事务、索引的支持可以满足文件元数据这种中等规模并发读写的场景,不会像 SQLite 那样写多几路就锁库。
- 数据库设计与模型层实现
2.1 用户模块设计
用户系统直接继承 Django 的 AbstractUser 扩展是最稳的路径。别自己从零写 User 表,原因是 Django 的 auth 模块内置了权限表、session 表跟用户表的关联,硬重写容易踩到坑。我是这样设计的:
from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): nickname = models.CharField(max_length=64, blank=True, verbose_name="昵称") avatar = models.ImageField(upload_to="avatar/", null=True, blank=True) quota = models.BigIntegerField(default=10737418240, verbose_name="配额,默认10GB") used_quota = models.BigIntegerField(default=0, verbose_name="已用容量") is_vip = models.BooleanField(default=False)这里有两个值得注意的点。第一,自定义用户模型必须在第一次 migrate 前设置 AUTH_USER_MODEL,不然后续迁移会提示关联冲突,这个坑几乎人人都会踩。第二,网盘项目必须在用户上记录容量配额,不能真的让单用户无限上传,不然磁盘爆了后悔都来不及。
2.2 文件与目录模型设计
文件和目录我放在同一张表里,而不是分两张表。一开始按“文件夹一张表、文件一张表”设计,很快发现移动目录、重命名、分享子树时逻辑会得极其繁琐。用一张表加 parent 自引用之后,整个文件树非常直观:
class FileNode(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=255) parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, related_name='children') file_type = models.CharField(max_length=32, choices=[("folder", "目录"), ("file", "文件")]) size = models.BigIntegerField(default=0) storage_path = models.CharField(max_length=512, null=True, blank=True) file_hash = models.CharField(max_length=64, db_index=True, null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) is_trash = models.BooleanField(default=False) trash_time = models.DateTimeField(null=True, blank=True)parent 自关联的妙处在于,不管你读多少层目录,本质上都是一条递归查询;配合前端面包屑做路径导航,后端的逻辑会非常干净。file_hash 字段就是给秒传用的,传 SHA1 值,先查库,相同的就不落盘,直接引用旧文件,这样重复文件几乎零成本。
2.3 MySQL 适配与索引、编码的细节
Django 对 MySQL 的适配整体没有太多黑魔法,但有几个配置项必须显式处理。
DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "pan_db", "USER": "pan_user", "PASSWORD": "your_password", "HOST": "127.0.0.1", "PORT": "3306", "OPTIONS": { "charset": "utf8mb4", "init_command": "SET sql_mode='STRICT_TRANS_TABLES'", }, } }utf8mb4 是必须的,因为 MySQL 8.0 默认字符集虽然已经是 utf8mb4,但如果你从老版本迁移过来,表里的 utf8 只能存 3 字节的字符,遇到生僻字、表情符号直接报错。设置 STRICT_TRANS_TABLES 是避免字段超长时 MySQL 静默截断,这种“软错误”排查起来非常折磨人。
索引方面,除了主键和 owner 外,经常用于查询的字段主要有两个:parent 和 is_trash。用户进入首页要列出“某一目录下所有非回收站项”,所以组合索引比单列索引更有价值,我建了一个联合索引:
class Meta: indexes = [ models.Index(fields=["owner", "parent", "is_trash"]), ]这样目录列表页的查询基本走覆盖索引,在几万条数据量下响应仍然在毫秒级。
- 文件上传与下载核心实现
3.1 文件上传的完整技术流程
Django 处理上传文件有两种常用方式:小文件直接读进内存,大文件写入 TemporaryFile。我选择的是让 Django 自己判断。在 settings 里设置FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440,默认是 2.5MB,代表少于这个尺寸的直接放内存,大于的则落到临时文件,避免上传大文件时内存被吃满。上传视图里,拿到的request.FILES['file']是一个 UploadedFile 对象,可以拿到 chunks 方法,分块写入最终位置。
实际生产服务器上,前面通常有一层 Nginx,nginx 的client_max_body_size也要同步加大,比如文件上限是 4GB,就设置成client_max_body_size 4096m;,否则你会发现 Django 层什么都没做错,文件却一直报 413。这是 Nginx 在拦你,不是 Django 的问题。
文件保存的核心逻辑如下:
def handle_uploaded_file(f, user, parent_dir, dest_name): hasher = hashlib.sha1() save_path = os.path.join(settings.MEDIA_ROOT, "files", str(user.id)) os.makedirs(save_path, exist_ok=True) dest = os.path.join(save_path, uuid.uuid4().hex + "_" + dest_name) with open(dest, "wb+") as destination: for chunk in f.chunks(): hasher.update(chunk) destination.write(chunk) file_hash = hasher.hexdigest() # 如果文件已经存在,则删除刚写入的物理文件,并复用已有记录 existing = FileNode.objects.filter(file_hash=file_hash, is_trash=False).first() if existing: os.remove(dest) return existing return dest, file_hash单独提一下 UUID 文件名的作用。真实存储路径如果直接用用户原始文件名,两个不同目录上传同名文件会冲突,而且文件名里夹带特殊字符各种乱入。我采取 UUID + 原名拼接的方式,保留原名的同时避免冲突,展示时再从数据库里读真正的 name 字段来显示。
3.2 分片上传与断点续传方案
网盘不比普通博客,动不动就是几个 G 的压缩包,单次请求超大文件在公网环境下基本不可靠。分片上传是把大文件切成固定大小的块,前端切片,后端一个块一个块接收,最终再合并。我用的分片方案比较轻,不引消息队列,纯粹用一张分片表维护状态:
class UploadChunk(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) file_identifier = models.CharField(max_length=128, db_index=True) # md5(文件名+大小+修改时间) chunk_index = models.IntegerField() total_chunks = models.IntegerField() chunk_path = models.CharField(max_length=512) uploaded_at = models.DateTimeField(auto_now_add=True)前端用 spark-md5 先算出文件整体 md5,再通过 File.slice 按 4MB 切块循环上传。后点击“上传”后实际会先请求一个初始化接口,传 identifier 和总块数,服务端返回这个文件目前缺失了哪些分片,前端只补传缺失的部分,天然支持断点续传。合并时,服务端按 chunk_index 顺序读取所有分片文件,写入同一个文件,再走一次 SHA1 入库逻辑,同时清理分片记录。
这里需要注意:分片上传的合并操作最好放在事务或者唯一约束里。我在合并前会先检查文件是否存在,避免两个请求同时写同一个文件导致互相覆盖。
3.3 下载与断点续传
下载逻辑相对于上传要简单一些,但大文件下载同样有细节。Django 的普通 HttpResponse 会先读入内存再返回,几 G 的文件可以直接把服务器内存吃爆。必须用 StreamingHttpResponse 或者 FileResponse:
from django.http import FileResponse def download_file(request, node_id): node = get_object_or_404(FileNode, pk=node_id, owner=request.user) real_path = os.path.join(settings.MEDIA_ROOT, node.storage_path) filename = urllib.parse.quote(node.name) response = FileResponse(open(real_path, "rb"), as_attachment=True) response["Content-Disposition"] = f"attachment; filename*=UTF-8''{filename}" return response给 Content-Disposition 做 URL 编码是非常关键的一步。如果不处理中文文件名,Chrome 下载时中文会乱码,或者直接被截断,传filename*=UTF-8''{filename}可以兼容现代浏览器。
断点续传下载这里其实是个偏前端的功能,后端不需要特别实现。FileResponse 本身依赖的 wsgi 服务器支持 Range 请求,比如 uWSGI 或 Nginx 层会自动处理 Range,客户端只要发送带 Range 的请求就能接着下。唯一要注意的是,如果开了 Gzip 压缩,Range 会失效,所以给文件接口必须加上Content-Encoding: identity。
3.4 分享链接与提取码实现
分享功能的常见形式是“外链 + 提取码 + 有效期”。我用一张 ShareLink 表来实现,token 使用 secrets.token_urlsafe(16) 生成,保证不可猜测。extract_code 是用户填的 4 位数字,暴露给提取人的。过期时间由用户从前端选择,后端做校验。
class ShareLink(models.Model): node = models.ForeignKey(FileNode, on_delete=models.CASCADE) owner = models.ForeignKey(User, on_delete=models.CASCADE) token = models.CharField(max_length=64, unique=True) extract_code = models.CharField(max_length=8) expires_at = models.DateTimeField(null=True, blank=True) visitor_count = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True)访客访问分享链接时,前端会拿 token 请求一个“是否过期、是否正需要提取码”的校验接口。如果分享的是目录,后端会把整棵子树拉出来,拼成类似/share/<token>/<node_id>的列表,下载时同样套用上面的 FileResponse 逻辑,只不过校验的是分享 token 而不是登录态。
- 路由、视图与权限控制实战
4.1 URL 设计与视图层组织
路由设计上,我按前后台模式拆分成两套 URL:管理的 API 给内部页面 Ajax 使用,分享的 URL 是公开的。内部 API 统一带 /api/ 前缀,方便以后接 Nginx 统一限流。路由表大致如下:
urlpatterns = [ path("", include("core.urls")), path("admin/", admin.site.urls), path("api/auth/", include("apps.user.urls")), path("api/files/", include("apps.file.urls")), path("share/<str:token>/", share_views.share_index), ]视图层我优先使用 CBV(Class-Based View),尤其是列表页和上传页。原因很简单:Django 内置的 ListView、CreateView 已经把分页、表单校验、表单错误回显这套做好了,继承后只要改少量方法,省下一大堆重复代码。比如目录列表的 ListView:
class FileListView(LoginRequiredMixin, ListView): model = FileNode template_name = "file/list.html" paginate_by = 30 def get_queryset(self): parent_id = self.request.GET.get("parent", 0) qs = FileNode.objects.filter( owner=self.request.user, parent_id=None if parent_id == "0" else parent_id, is_trash=False, ).order_by("-file_type", "name") return qs4.2 登录态与会话管理
Django 自带的 login_required 装饰器和 LoginRequiredMixin 可以解决大部分需要登录才能访问的资源。但要做个人网盘,不能只防页面级未登录访问,更重要是防用户越权操作。我的处理方式是在每个视图内再次校验 owner 字段,即使被猜到了 URL,也拿不到别人的文件。比如:
objs = FileNode.objects.filter(owner=request.user, pk=node_id)而不是直接get(pk=node_id)然后手动判断。这种过滤式查询是一条 SQL 走完的,效率和安全性都好。另外,如果使用了自定义 User 模型,建议给 Session 增加过期时间,比如默认 7 天,防盗用风险。
4.3 前端页面交付方式
个人网盘这类管理后台属性强的项目,我不推荐重前端。初学者如果一开始就上 Vue + Axios 跨域,很容易被 CORS 和 JWT 吃掉大量时间,反而偏离了网盘核心逻辑。我最初就用 Django 模板 + Bootstrap 居中弹窗,配少量原生 JS fetch 完成上传交互。模板复用继承一套 base.html 就够了。等核心功能全通后,如果想做更丝滑的交互,再单独抽一层 DRF 接口,前端配 Vue3。前后端分离是渐进式改造出来的,不是一上来就火箭配置。
- 常见问题与排查技巧实录
5.1 MySQL 连接失败的典型场景
我在本地环境装好全部依赖后,启动项目第一个报错就是django.db.utils.OperationalError: (1045, "Access denied for user 'pan_user'@'localhost'")。排查后发现是 MySQL 8 默认用的caching_sha2_password插件,而 PyMySQL 在旧版本对它支持不友好。解决方案有两个:一是把 MySQL 用户改为 mysql_native_password,二是装较新版本的 PyMySQL(0.12 以上)并在 Django 的 OPTIONS 里加上"auth_plugin": "mysql_native_password"。我建议直接升级 PyMySQL,改密码插件的方案留到老系统兼容时再用。
5.2 上传大文件总超时或直接被 413
自测阶段上传 600MB 的压缩包,总是传一会儿就断。排查出三个层次的原因:Nginx client_max_body_size 未设置、Django 的DATA_UPLOAD_MAX_MEMORY_SIZE太小、网络中间代理层超时时间太短。解决方案是把 Nginx 的 client_max_body_size 调到 4g,Django 侧不要动 DATA_UPLOAD_MAX_MEMORY_SIZE 太大(会影响缓冲策略),改用分片上传绕开单次请求体积限制。前端再把分片大小调到 4MB 左右,连续失败重试 3 次,体验就稳定了。
5.3 中文文件名和路径中的隐藏坑
文件下载时 Chrome 偶尔会把中文显示成下划线或乱码,这是 Content-Disposition 编码问题。我前面已经写了 urllib.parse.quote 方案,这里提醒另一个坑:跨平台路径拼接时不要用字符串拼接,必须用os.path.join。我最初图省事写real_path = settings.MEDIA_ROOT + "/" + node.storage_path,在 Linux 上问题不大,在 Windows 上跑测试就把路径组合成了D:\media\path\to\file \whatever,空格和反斜杠全乱。后来全部换成 os.path.join,收工。
5.4 文件总是传完后没有出现在列表里
这类问题十有八九是缓存或事务未提交。如果是用了模板自带的分页器,确认分页的页面参数page是否被前端传入;如果是上传后目录树没有刷新,可以在表单提交后window.location.reload()或者手动刷新当前目录接口。很多次经验告诉我,用户普遍急躁,上传完想立刻看到结果,所以前端在上传完成回调里再加一个小延迟后刷新,体验会比等用户手动刷新好很多。
- 项目扩展方向与二次开发建议
这套源码只把网盘最核心的主干打通,真要长期自用,我建议往这几个方向扩展。第一是接入对象存储,把实体文件从本地磁盘搬到 MinIO 或阿里云 OSS,元数据继续放 MySQL,这样服务器硬盘不用扩容,下载带宽也能吃满。第二是引入后台异步任务,比如文件压缩、视频转码用 Celery + Redis 在后台执行,避免上传耗时操作阻塞 Web 进程。第三是增加目录级分享权限,现在分享是基于单个文件夹或文件,如果需要同事之间协作编辑,就得引入更细粒度的权限模型,比如只读、可写、可管理,这可以套用 Django 自带的 ContentType 做通用权限表。
从我的实际体验讲,这个项目最大的收益不是“搞定文件上传下载”,而是完整走了一遍“从需求拆分 → 数据库建模 → 核心流程 → 权限安全 → 部署运维”的项目全流程。如果你也想拿它练手,建议先把上传和目录树做出来,再把分享和回收站铺上,一步步来,过程中踩的每一个坑都会成为复试或者简历里最有分量的项目经验。
最后再分享一个小技巧——Django 生产环境千万不要用 runserver 硬抗。我在真实部署时用的是 uWSGI + Nginx,uWSGI 里设http-timeout=600和harakiri=600,否则大文件并发上传时 worker 会被拖死。配置一次之后,整个项目跑在低配云服务器上,跑了大半年没出过幺蛾子。这个项目的源码地址在 github.com 上可以找到,搜索“Django 个人网盘项目”或者“django-pan”就能看到,相关的 README 里我也写了完整的部署步骤,直接照着抄就行。
本文还有配套的精品资源,点击获取