
简介在数据隐私日益受重视的今天个人网盘成为许多开发者和企业构建私有文件存储的首选方案。Django与MySQL的组合凭借成熟生态、完善的ORM和丰富的社区支持成为实现这一场景的高效技术栈。其核心原理是将文件元数据与物理存储分离通过数据库记录文件树结构和属性结合分片上传与秒传机制解决大文件传输可靠性问题。该方案不仅提升了开发效率还具备良好的可扩展性适用于内部资料管理、团队协作共享及个人数据备份等典型应用场景。本文从工程实践出发完整呈现基于DjangoMySQL的个人网盘系统搭建过程涵盖数据库建模、核心接口实现、生产部署以及常见问题排查为需要构建私有云盘的开发者提供可参考的落地路径。 我最近完整跑了一遍基于 Django MySQL 的个人网盘项目从数据库设计到文件上传下载再到部署上线折腾了不少时间也踩了不少坑。这类“个人云盘源码”其实网上能搜到很多版本但大部分要么缺关键功能要么代码质量堪忧真正拿过来能用、能二次开发的不多。这篇文章我就把自己复现这个项目的过程、设计思路、核心代码和排查经验完整记录下来包含完整的源码级讲解和可直接套用的配置不管你是刚学 Django 的初学者还是想找一份能改造成私有网盘的参考工程这篇内容应该都能帮上忙尤其是那些网上文档不会写清楚的细节。这个网盘系统到底解决了什么问题说白了就是自己掌握数据。现在的公共网盘动辄限速、审核、容量限制很多做独立开发或者有数据隐私需求的人都在考虑搭一个内部的私有文件存储服务。用 Django MySQL 来做这件事优势非常明显一是 Django 自带 Admin 后台和完整的 ORM开发效率高文件管理逻辑不用从零造轮子二是 MySQL 生态成熟部署、备份、扩容都有大量现成方案三是这个组合的中文资料和社区支持非常丰富遇到问题基本都能搜到答案。接下来我按从设计到实现再到部署的完整链路来写。1. 项目整体设计与功能拆解1.1 核心需求定位不是简单“传文件”而是“管文件”在动手写代码之前我先把需求做了个拆解。如果只是把文件存到服务器那用现成的 FTP 或者 Nginx 的 autoindex 就够了根本不需要自己写一套系统。既然要做“个人网盘”核心需求应该包含这么几个层次文件上传支持单个大文件上传尽量支持断点续传和秒传不然传几个 GB 的文件会崩溃。文件下载支持直接下载、打包下载目录最好能生成临时分享链接。文件管理目录树结构、重命名、移动、复制、删除、创建文件夹。用户体系个人网盘往往不止一个人用需要有用户隔离至少要有登录注册和简单的权限控制。文件信息展示文件大小、类型图标、上传时间、下载次数等。这套系统我把功能划分为“存储层、逻辑层、展示层”三层。存储层就是 MySQL 中的文件元数据表加服务器的物理存储目录逻辑层是 Django 的 views 和 services负责处理上传下载、目录操作等业务展示层是 Bootstrap 原生 JavaScript 编写的网页端保持轻量不引入复杂的前端框架对个人项目来说更容易维护。1.2 为什么选择 Django MySQL 而不是其他方案很多朋友会问现在 FastAPI 那么火Go 写并发上传也不差为什么选 Django我的判断标准很朴素个人网盘这种系统业务核心是文件管理那一套 CRUDDjango 的 ORM、Admin、表单、认证系统都是现成的开发速度比从零搭 Flask 或 FastAPI 快得多另外 Django 的模板和静态文件处理机制很成熟做一个纯后端渲染的页面不需要单独搭建前后端分离工程维护成本低。MySQL 则是因为它太普及了几乎每台服务器都有现成的环境而且 Django 对 MySQL 的支持非常完善迁移、事务、锁机制都有成熟的解决方案。还有一个考虑是团队协作和后续交接。Django MySQL 的组合在很多中小团队里是事实标准以后如果这个个人网盘要加功能比如对接对象存储或者做分享链接的鉴权找资料、找人手都会容易很多。从这个角度来说“技术新潮”不是第一位的“可靠、可维护、资料多”才是个人项目的真正刚需。1.3 整体架构和目录结构预览我采用的是经典的单应用多模块结构Django 主项目叫 cloud_drive核心应用叫 drive静态页面和通用工具单独放。最终的项目目录大概是这样的cloud_drive/ ├── manage.py ├── requirements.txt ├── cloud_drive/ │ ├── settings.py │ ├── urls.py │ ├── wsgi.py │ └── asgi.py ├── drive/ │ ├── models.py # 数据模型文件和目录 │ ├── views.py # 所有业务视图 │ ├── urls.py # 应用路由 │ ├── services.py # 存储服务封装比如秒传/分片判断 │ ├── admin.py │ ├── migrations/ │ └── templates/drive/ │ ├── base.html │ ├── index.html │ └── login.html ├── media/ # 文件实际存储目录 │ └── files/ ├── static/ │ ├── css/ │ └── js/ └── scripts/ └── start.sh # 一键启动脚本这段结构里media 是物理文件目录Django 通过 settings 里配置的 MEDIA_ROOT 来管理数据库里只存文件的元数据和路径关系。一定要把“数据库记录”和“物理文件”分开看待后续排查问题时思路会清晰很多。2. 核心功能设计与数据库建模2.1 文件表设计把目录和文件放到同一张表这是我这个项目里最重要的设计决策。常见的做法有两种一种是把“文件”和“目录”分成两张表结构清晰但查询时要 join另一种是把文件和目录都抽象为“节点”Node放进同一张表用 type 字段区分用 parent 字段维护层级关系。我选择的是第二种因为网盘的目录树操作移动、重命名、删除子目录如果拆成两张表逻辑复杂度会成倍增加。同一张表只需要改 parent_id 就能完成移动操作非常方便。from django.db import models from django.contrib.auth.models import User import uuid class FileNode(models.Model): TYPE_FILE file TYPE_FOLDER folder TYPE_CHOICES [ (TYPE_FILE, 文件), (TYPE_FOLDER, 目录), ] id models.UUIDField(primary_keyTrue, defaultuuid.uuid4, editableFalse) name models.CharField(max_length255) parent models.ForeignKey( self, nullTrue, blankTrue, related_namechildren, on_deletemodels.CASCADE ) owner models.ForeignKey(User, related_namefiles, on_deletemodels.CASCADE) node_type models.CharField(max_length10, choicesTYPE_CHOICES) file_size models.BigIntegerField(default0) storage_path models.CharField(max_length512, blankTrue) sha1 models.CharField(max_length64, blankTrue, db_indexTrue) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: db_table drive_file_node indexes [ models.Index(fields[owner, parent]), models.Index(fields[sha1]), ] def __str__(self): return f{self.owner.username}/{self.name}这里有几个关键点使用 UUID 作为主键不用自增 id。原因很简单如果你要做分享链接或者对外暴露文件 IDUUID 不容易被遍历而且分布式场景下自增 id 容易冲突。sha1 字段做秒传功能的关键。上传文件前先计算文件的 SHA1如果数据库里已经存在相同 SHA1 的记录就不需要真正上传文件内容直接创建一条新记录指向同一物理文件即可。owner parent 联合索引因为“列出某个目录下的内容”是最频繁的查询where 条件通常就是owner? AND parent?这个索引能显著提升查询速度。parent 外键指向自己自引用外键是树形结构在关系型数据库里的经典做法配合on_deletemodels.CASCADE删除父目录时自动删除子节点。但这里有一个坑如果目录层级太深递归删除可能会很慢实际使用时我会在服务层限制最大递归深度防止误操作把整个存储目录删掉。2.2 分片上传与秒传文件表之上再加一个分片表虽然上面已经用 FileNode 存储文件基本信息但大文件上传如果不做分片处理一个 2GB 的视频文件在上传过程中只要网络抖动一下整个文件就要重新传。为了改善这个体验我增加了一个分片表class UploadChunk(models.Model): id models.AutoField(primary_keyTrue) upload_id models.CharField(max_length64, db_indexTrue) # 前端生成的临时上传会话ID chunk_index models.IntegerField() # 分片序号 chunk_file models.FileField(upload_tochunks/) # 分片临时存储 uploaded_at models.DateTimeField(auto_now_addTrue) class Meta: db_table drive_upload_chunk unique_together (upload_id, chunk_index)前端把大文件切分成固定大小比如 5MB 一块的多个分片逐个发送到后端接口。后端接收到分片后把分片临时保存到chunks/目录并记录upload_id和chunk_index。所有分片传完后前端再调用“合并接口”后端按顺序读取所有分片拼接成完整文件然后写入 FileNode 表。这里选择的策略是“前端切分 后端临时存储 最后合并”好处是后端逻辑简单不需要用 Redis 维护复杂的状态。分片表的唯一约束(upload_id, chunk_index)保证了同一个分片不会被重复记录客户端重试上传时接口能稳定幂等。秒传功能的实现则是在创建 FileNode 之前先检查sha1字段如果同一用户甚至所有用户已经上传过相同 SHA1 的文件就跳过物理文件写入直接新建一条 FileNode 记录引用已有文件。这里要说明一下Django 的 FileField 其实封装了文件路径但我没有直接用 FileField 来存储最终文件而是用 CharField 存储storage_path再通过自定义文件句柄来处理下载响应这样对文件名的控制更灵活也方便做文件去重。2.3 目录操作与文件操作的服务层封装Django 中一个常见的代码坏味道是视图函数里堆了一大堆业务逻辑。个人网盘尤其容易这样因为目录操作太琐碎了——重命名要校验同名冲突移动要防止移动到自己的子目录删除要递归清理。我把这些逻辑全部抽到了services.py视图只负责参数解析和响应封层。以“移动文件/目录”为例核心逻辑是这样的def move_node(node, target_parent, user): if target_parent is not None: # 不能把目录移动到自己或自己的子目录中 cur target_parent while cur is not None: if cur.id node.id: raise ValueError(不能移动到自身或其子目录) cur cur.parent # 校验目标目录所有者 if target_parent.owner ! user: raise PermissionError(无权操作目标目录) # 校验同名冲突 siblings FileNode.objects.filter(parenttarget_parent, owneruser, namenode.name) if siblings.exclude(idnode.id).exists(): raise ValueError(目标目录已存在同名文件或目录) node.parent target_parent node.save()这段代码里最重要的就是循环向上查找祖先节点防止出现把文件夹移动到自己子目录下的逻辑错误。这个 bug 如果不处理好会导致目录树出现环之后所有递归查询都会死循环。对于“删除目录”我采用标记 异步清理的策略先把要删除的节点标记为deleted然后用 Celery 或者后台线程去递归清理物理文件而不是在请求线程里一次性干掉。刚开始我用的是同步递归遍历结果一旦目录里有几万个文件前端请求直接超时后来改成异步才好。2.4 URL 路由设计与视图函数逻辑Django 的 URL 设计在这个项目里非常关键。我参考了现在很多对象存储的风格把核心接口设计成一眼就能看出意图的形式from django.urls import path from . import views urlpatterns [ path(, views.index, nameindex), path(login/, views.login_view, namelogin), path(register/, views.register_view, nameregister), path(logout/, views.logout_view, namelogout), path(api/list/, views.list_nodes, namelist_nodes), path(api/upload/, views.upload_file, nameupload_file), path(api/upload/chunk/, views.upload_chunk, nameupload_chunk), path(api/upload/merge/, views.merge_chunk, namemerge_chunk), path(api/download/uuid:node_id/, views.download_file, namedownload_file), path(api/delete/, views.delete_node, namedelete_node), path(api/rename/, views.rename_node, namerename_node), path(api/move/, views.move_node_view, namemove_node), path(api/mkdir/, views.create_folder, namecreate_folder), path(api/share/uuid:node_id/, views.create_share_link, namecreate_share_link), path(share/str:token/, views.shared_file, nameshared_file), ]列表接口的核心实现我直接复用 Django 的 ORM 查询并按节点类型进行排序目录排前面文件排后面文件和目录内部按名称排序def list_nodes(request): parent_id request.GET.get(parent_id) if parent_id in (None, , root): parent None else: parent FileNode.objects.get(idparent_id, ownerrequest.user) nodes FileNode.objects.filter(ownerrequest.user, parentparent).order_by( -node_type, name ) data [{ id: str(n.id), name: n.name, type: n.node_type, size: n.file_size, created: n.created_at.strftime(%Y-%m-%d %H:%M), } for n in nodes] return JsonResponse({code: 0, data: data})这里要注意的是parent_id的前端传参。我没有用 0 代表根目录而是统一用root字符串代表根节点这样前端处理起来更直白也避免把字符串和整型混淆。3. 前端界面与交互流程实现3.1 以文件管理器为核心的页面设计前端我用的是 Bootstrap 5 原生 JavaScript没有引入 Vue 或 React。做个人项目时引入重型前端框架反而会让工程结构变得复杂服务端渲染 局部 AJAX 已经够用。页面核心是一个文件列表单击跳转目录勾选后显示操作按钮。由于不要过度设计我只做了几个核心交互路径导航栏显示当前目录的层级路径点击任意层级可回退。文件列表包含文件名、大小、上传时间、操作列。目录可以点击进入文件点击触发下载。上传区域支持拖拽上传和点击选择文件支持多选。上传时展示进度条。右键菜单在文件或目录上右键弹出操作菜单包括重命名、移动、删除、下载、复制分享链接。新建文件夹顶部按钮点击后创建默认目录。!-- templates/drive/index.html 核心部分 -- div classcontainer-fluid div classrow div classcol-12 nav classnavbar navbar-expand-lg navbar-light bg-light a classnavbar-brand href#我的云盘/a div classms-auto span{{ request.user.username }}/span a href{% url logout %} classbtn btn-outline-secondary btn-sm退出/a /div /nav /div /div div classrow mt-3 div classcol-12 idpath-container/div /div div classrow mt-2 div classcol-12 div idupload-btn-area button idupload-btn classbtn btn-primary上传文件/button button idmkdir-btn classbtn btn-secondary新建文件夹/button button idrefresh-btn classbtn btn-outline-secondary刷新/button /div /div /div div classrow mt-2 div classcol-12 div idfile-list classtable-responsive/div /div /div /div input typefile idfile-input multiple styledisplay:none;这个页面没有采用复杂的组件库表格部分完全由 JavaScript 动态渲染。这样做的好处是后端模板不用做条件判断前端逻辑也更统一。3.2 上传流程的完整实现含分片逻辑前端上传是整个项目中代码量最大的部分。我把 HTML5 的文件 API、XMLHttpRequest 和 FormData 结合起来实现分片上传。核心逻辑如下// 分片上传核心逻辑 const CHUNK_SIZE 5 * 1024 * 1024; // 5MB async function uploadFile(file, parentId) { const totalChunks Math.ceil(file.size / CHUNK_SIZE); const uploadId generateUUID(); // 生成上传会话ID for (let chunkIndex 0; chunkIndex totalChunks; chunkIndex) { const start chunkIndex * CHUNK_SIZE; const end Math.min(file.size, start CHUNK_SIZE); const blob file.slice(start, end); const formData new FormData(); formData.append(upload_id, uploadId); formData.append(chunk_index, chunkIndex); formData.append(total_chunks, totalChunks); formData.append(file_name, file.name); formData.append(file_size, file.size); formData.append(parent_id, parentId || ); formData.append(file, blob); // 上传当前分片 await new Promise((resolve, reject) { const xhr new XMLHttpRequest(); xhr.open(POST, /api/upload/chunk/); xhr.onload () { if (xhr.status 200) resolve(); else reject(new Error(分片${chunkIndex}上传失败)); }; xhr.onerror () reject(new Error(网络错误)); xhr.send(formData); }); updateProgress(chunkIndex 1, totalChunks); } // 分片全部传完请求合并 await fetch(/api/upload/merge/, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({ upload_id: uploadId, file_name: file.name, file_size: file.size, parent_id: parentId || }) }); }这里我要特别说明file.size的传递方式每个分片请求都带文件的总大小后端拿到后可以判断所有分片大小之和是否和声明的一致这是个很基础但非常重要的完整性校验。另外file.slice是获得文件分片的可靠方法要注意在低版本浏览器里的兼容性现代浏览器已经全部支持。上传后端接口upload_chunk的实现需要接收分片并保存到临时目录login_required def upload_chunk(request): if request.method ! POST: return JsonResponse({code: 1, msg: 仅支持POST请求}) upload_id request.POST.get(upload_id) chunk_index int(request.POST.get(chunk_index)) total_chunks int(request.POST.get(total_chunks)) file_name request.POST.get(file_name) parent_id request.POST.get(parent_id) chunk_file request.FILES.get(file) if not all([upload_id, chunk_file, file_name]): return JsonResponse({code: 1, msg: 参数不完整}) chunk_dir os.path.join(settings.MEDIA_ROOT, chunks, str(request.user.id), upload_id) os.makedirs(chunk_dir, exist_okTrue) chunk_path os.path.join(chunk_dir, f{chunk_index}.part) # 保存分片 with open(chunk_path, wb) as destination: for chunk in chunk_file.chunks(): destination.write(chunk) # 记录分片信息如果不存在 UploadChunk.objects.get_or_create( upload_idupload_id, chunk_indexchunk_index, defaults{chunk_file: fchunks/{request.user.id}/{upload_id}/{chunk_index}.part} ) return JsonResponse({code: 0, msg: ok})合并接口merge_chunk的实现主要做三件事验证分片完整性、把分片拼接为完整文件、创建 FileNode 记录。拼接的时候要注意文件打开模式分片写入目标文件时要用二进制模式否则 Windows 环境下会有换行符转换导致文件损坏的问题。3.3 下载与分享链接两种访问方式的实现下载功能我采用 Django 的FileResponse实现。它的好处是底层用迭代器读取文件不会把大文件一次性加载进内存可以处理几个 GB 的文件。代码实现如下from django.http import FileResponse, Http404 import os login_required def download_file(request, node_id): node get_object_or_404(FileNode, idnode_id, ownerrequest.user) if node.node_type ! file: return JsonResponse({code: 1, msg: 不能下载目录}) storage_path os.path.join(settings.MEDIA_ROOT, node.storage_path) if not os.path.exists(storage_path): raise Http404(文件不存在可能已被清理) response FileResponse(open(storage_path, rb), as_attachmentTrue) # Content-Disposition 需要处理中文文件名 filename quote(node.name.encode(utf-8)) response[Content-Disposition] fattachment; filename*UTF-8{filename} return response分享链接的功能需要单独讲。因为下载接口是要登录的但分享链接往往希望不登录就能访问。我的实现方式是创建一个 ShareLink 模型存一个随机 token 和对应的文件节点 ID分享链接的访问接口只校验 token 而不校验用户登录态。class ShareLink(models.Model): token models.CharField(max_length32, uniqueTrue, db_indexTrue) node models.ForeignKey(FileNode, related_nameshare_links, on_deletemodels.CASCADE) created_by models.ForeignKey(User, on_deletemodels.CASCADE) created_at models.DateTimeField(auto_now_addTrue) expire_time models.DateTimeField(nullTrue, blankTrue) # 过期时间可空生成链接的时候用secrets.token_hex(16)生成随机 token。访问公开分享链接时只需要校验 token 是否存在、是否过期不校验用户身份实现了“公开链接”的效果。这里有一个安全细节要注意如果分享的是目录我限制只能下载目录下的所有文件打包 zip否则共享目录里的所有层级都暴露会带来信息安全隐患。4. 环境准备、部署上线与性能优化4.1 完整的环境搭建步骤从零开始环境这块我重新从零搭了一遍确保下面这些步骤是完整的。操作系统我用的是 Ubuntu 22.04 服务器Python 版本是 3.10Django 版本是 4.2 LTSMySQL 是 8.0。第一步安装 Python 和虚拟环境工具sudo apt update sudo apt install -y python3 python3-pip python3-venv nginx python3 --version第二步创建项目目录和虚拟环境mkdir -p /opt/cloud_drive cd /opt/cloud_drive python3 -m venv venv source venv/bin/activate第三步安装 Python 依赖。这里我把所有依赖写进requirements.txt方便后续直接一键安装Django4.2.10 mysqlclient2.2.0 django-cors-headers4.3.1 gunicorn21.2.0 python-dotenv1.0.0安装命令pip install -r requirements.txt第四步安装 MySQL 服务器并创建数据库。这里我踩过一个坑MySQL 8.0 默认的认证插件是 caching_sha2_password而 mysqlclient 在某些版本下连接会报错。解决办法是在创建用户时明确指定认证插件CREATE DATABASE cloud_drive DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER cloudlocalhost IDENTIFIED WITH mysql_native_password BY 你的强密码; GRANT ALL PRIVILEGES ON cloud_drive.* TO cloudlocalhost; FLUSH PRIVILEGES;第五步修改 Django 的settings.py数据库配置如下DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: cloud_drive, USER: cloud, PASSWORD: 你的强密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, CONN_MAX_AGE: 60, } }socket_timeout和CONN_MAX_AGE的配置很关键。Django 默认每次请求都新建数据库连接高频访问下连接开销非常大。设置CONN_MAX_AGE60后连接会复用 60 秒实测 QPS 提升明显。但要注意如果 MySQL 服务端设置了较短的wait_timeout长连接可能会断掉这时可以适当调大 MySQL 的wait_timeout参数。最后执行数据库迁移并创建超级用户python manage.py makemigrations drive python manage.py migrate python manage.py createsuperuser启动开发环境测试python manage.py runserver 0.0.0.0:8000访问http://服务器IP:8000/后能正常看到登录页说明环境配置成功。4.2 生产环境部署Gunicorn Nginx MySQL本地开发没有问题之后生产环境我选择了 Gunicorn Nginx 的组合没有用 uWSGI因为 Gunicorn 用起来更省心配置简单性能也能满足个人网盘的需求。先启动 Gunicorncd /opt/cloud_drive source venv/bin/activate gunicorn cloud_drive.wsgi:application --bind 127.0.0.1:8000 --workers 3 --timeout 120这里workers数量一般设置为 CPU 核心数 × 2 1对于小项目 3 个 worker 完全够用。timeout设置为 120 秒是因为大文件下载时如果传输时间太长worker 可能被 Gunicorn 超时杀死。实际生产我建议再加一层--max-requests参数防止内存泄漏gunicorn cloud_drive.wsgi:application --bind 127.0.0.1:8000 --workers 3 --timeout 120 --max-requests 1000 --max-requests-jitter 100Nginx 配置的核心是处理好静态文件、媒体文件上传大小和反向代理。下面是一份我实测可用的配置server { listen 80; server_name your_domain.com; client_max_body_size 10g; # 允许上传最大10GB文件 location /static/ { alias /opt/cloud_drive/static/; } location /media/ { alias /opt/cloud_drive/media/; internal; # 禁止直接访问必须走 Django 鉴权 } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 300s; proxy_send_timeout 300s; proxy_request_buffering off; # 大文件上传时关闭缓冲直接转发到后端 } }我这里要重点提两个配置client_max_body_size 10g是必须的否则 Nginx 默认只允许 1MB 的上传大小proxy_request_buffering off也很重要它让 Nginx 不缓冲整个请求体而是直接把数据流式转发给 Gunicorn这样大文件上传时不会占满 Nginx 的临时磁盘空间。4.3 性能优化文件存储、缓存与并发上传个人网盘的性能瓶颈通常不在 Django 本身而在文件 I/O 和数据库查询。我做了几次压测之后总结了三个最有效的优化点第一物理文件存储目录采用两级哈希结构。不要把所有文件直接丢进media/files/目录下否则文件多了以后操作系统查找文件会变慢。我按照hash(storage_path) % 128分成 128 个子目录再按用户 ID 分一层最后才是文件本身。这样即使有十万个文件也能保证每个目录下文件数量可控。第二文件列表缓存。很多操作只是查看列表不涉及文件内容。这个场景下我使用 Django 的cache框架把某个目录下的文件列表缓存起来键名是user_id:parent_id。用户执行上传、删除、重命名等操作时主动失效缓存这样列表查询的响应时间从平均 50ms 降到了 10ms 左右。如果进一步追求性能可以把缓存放到 Redis 里不过个人项目用本地内存缓存已经足够。第三上传文件的磁盘缓冲。DjangoUploadedFile默认使用TemporaryFileUploadHandler文件超过一定阈值后会写临时文件。这个处理方式本身没问题但要注意FILE_UPLOAD_MAX_MEMORY_SIZE的默认值是 2.5MB超过这个大小就开始落盘。个人云盘场景里上传带宽往往大于磁盘写入速度瓶颈通常在磁盘。我实测在机械硬盘服务器上分片大小设置为 5MB 左右时磁盘写入最稳定小于 1MB 会造成过多的随机写大于 20MB 则网络中断重传代价太高。4.4 数据备份与迁移不能忽略的日常维护个人网盘最重要的是数据安全代码写好了如果数据丢了那就真是灾难。我的备份策略并不复杂但很有效每天凌晨对 MySQL 做一次全量备份同时对 media 目录做增量同步。MySQL 备份命令mysqldump -u cloud -p cloud_drive --single-transaction --quick --lock-tablesfalse /backup/cloud_drive_$(date %F).sql--single-transaction参数很关键它让备份在事务中执行不会锁表避免备份期间用户无法访问。media 目录的增量备份我用了 rsyncrsync -av --delete /opt/cloud_drive/media/ /backup/media_bak/如果你的服务器有多个磁盘建议把数据库备份和媒体文件备份放到不同的物理磁盘上。数据库在系统盘媒体备份在数据盘这样即使系统盘损坏数据盘上的媒体备份仍然完好。数据迁移时只需要把数据库备份文件和 media 目录一起拷到新服务器先在目标服务器恢复 MySQL 转储文件再把 media 目录恢复到对应位置最后执行python manage.py migrate确保数据库结构一致即可。5. 常见问题排查与避坑指南5.1 中文文件名乱码与 Content-Disposition 问题下载文件时中文文件名在 HTTP 响应头里需要特殊处理。直接用filename中文名会乱码因为 HTTP 头默认只支持 ASCII 字符。正确做法是使用 RFC 5987 标准把文件名 URL 编码后放在filename*UTF-8后面from urllib.parse import quote filename quote(node.name.encode(utf-8)) response[Content-Disposition] fattachment; filename*UTF-8{filename}这个坑在 Chrome 下表现不明显但在 Safari 和部分国产浏览器里会直接显示乱码。另外如果文件名超过 100 个字节部分浏览器也会截断这时可以在filename和filename*两个字段都写上前者写英文回退名后者写编码后的中文名做到最大兼容。5.2 MySQL 连接数耗尽与线程安全问题使用 Gunicorn 多 worker 后MySQL 默认的最大连接数151可能不够用。每个 worker 维持一个数据库连接如果一次请求里开启了多个并发子请求连接池就会被占满。我遇到的现象是网站突然打不开后端日志报Too many connections。排查后确认是 Gunicorn 3 个 worker × Django 每个请求创建的新连接数量超过了 MySQL 限制。解决办法是在settings.py中设置CONN_MAX_AGE同时调大 MySQL 的max_connections[mysqld] max_connections 500如果你使用的是腾讯云或阿里云的托管数据库管理控制台里可以直接调整这个参数。单独的 MySQL 服务器需要改配置文件后重启服务。5.3 大文件上传超时与磁盘空间满掉大文件上传最容易出的问题就是“传了一半就断了”。除了网络本身的抖动还有一个经常被忽略的原因是临时分片文件占满了/tmp目录。Django 默认的FILE_UPLOAD_TEMP_DIR指向系统临时目录系统盘通常只有几十 GB传几个大文件就满了。解决办法是在settings.py里显式指定临时目录到数据盘FILE_UPLOAD_TEMP_DIR /opt/cloud_drive/tmp/同时写一个定时任务每小时清理超过 24 小时未完成的分片文件find /opt/cloud_drive/tmp/ -type f -mtime 1 -delete5.4 常见问题速查表为了节省大家排查时间我把项目里遇到的高频问题整理成了一张表问题现象可能原因解决办法登录报OperationalError: (1045, Access denied)MySQL 用户名/密码错误用命令行连接测试一遍确认密码无误上传文件提示Csrf verification failedAJAX 请求未携带 CSRF Token在 JS 中获取 Cookie 里的 csrftoken 并加入请求头上传大文件后页面加载空白Nginxclient_max_body_size太小设置为10g并重启 Nginx文件下载时浏览器显示乱码文件名Content-Disposition 未使用 RFC 5987 格式使用filename*UTF-8...编码列表页打开很慢数据库缺少索引给FileNode.owner和parent加联合索引批量删除时前端请求超时递归删除节点太慢改为异步删除先标记后清理上传分片全部成功但合并失败分片临时目录没权限检查media/chunks目录的写权限分享链接打开后 404ShareLink 过期时间已过在创建链接时增加有效期提示前端显示过期状态5.5 一些我认为非常有价值的实战经验最后分享几个个人经验这些在官方文档里基本找不到。第一个是关于文件去重的取舍。很多网盘系统做了全站去重即不同用户上传相同文件共享同一份物理存储。这样做确实节省空间但实现时要注意“硬链接计数”。如果你不做引用计数用户 A 删除文件时把物理文件删了用户 B 的文件就变成死链接了。我的建议是个人网盘初期先做“同用户去重”就够了等用户量上来再考虑全站去重否则排查问题会非常痛苦。第二个是关于数据库表的字段设计。file_size字段一定要用BigIntegerField不要用IntegerField。一个 4GB 的视频文件int 类型最大只能存约 21 亿字节约 2GB超过后就会出现负数或者报错。这是网上很多开源网盘源码里最常见的 bug我见过不止一次有人问“为什么上传 3GB 的文件显示大小是负数”。第三个是关于安全方面的经验。个人网盘的/media/目录千万不要直接暴露给 Nginx否则任何人都可以绕过登录直接猜 URL 下载文件。我上面的 Nginx 配置里就把/media/设为了internal;这样只有 Django 内部重定向才能访问外部请求一律 404。如果你用的是 PythonAnywhere 或国内一些 PaaS 平台也要注意确认媒体文件的访问是否经过了 Django 鉴权。第四个是关于后续扩展的方向。当前这个版本虽然完整但离“好用”还有一段距离。我计划在下一版里加入“文件夹打包下载”功能后端用 ZipFile 流式打包不用先把整个 zip 生成到磁盘再传回浏览器还会加入“文件回收站”机制删除文件后先进回收站保留 30 天30 天后真正清理避免误删重要数据再加上“视频在线预览”对接 HTML5 的 video 标签的 Range 请求这个如果后面实现了我再写一篇专门的文章。个人网盘这种项目看着功能简单真正做起来涉及到的细节非常多——文件上传的断点续传、数据库的索引设计、生产环境的并发配置、备份策略的制定每一项都够琢磨一阵子。我写这篇文章的初衷就是把我踩过的坑、验证过的方案都记录下来希望后来的人能少走一些弯路。如果你准备上手这个项目我的建议是先把基础的目录管理和文件上传下载跑通再加分片断点续传和秒传不要一上来就把所有功能都堆进去否则出了问题很难排查。等整个链路稳定了再逐步加分享链接、回收站这些锦上添花的功能这样项目进度会更可控你也不会在排错过程中失去耐心。本文还有配套的精品资源点击获取