欢迎光临
我们一直在努力

基于 FastAPI + React 的轻量级网盘

🔥 肝了一周,我开源了这个基于 FastAPI + React 的企业级网盘 (支持秒传/断点续传)

摘要:市面上的网盘系统要么太臃肿,要么也是闭源的。作为一名全栈开发者,我利用业余时间基于 FastAPI (Python) 和 React 构建了一套高性能、轻量级的私有云盘系统 —— SkyDrive Enterprise。

本文将深度复盘项目的核心架构,从数据库设计、文件秒传原理、分片上传实现到 Docker 容器化部署,毫无保留地分享给大家。

💻 项目开源地址: https://github.com/ChaceQC/SkyDrive (欢迎 Star ⭐️) 👀 在线演示: https://home.chacewebsite.cn:8686/


📋 目录

  • 为什么造这个轮子?
  • 技术栈选型与架构
  • 核心功能深扒
    • 3.1 元数据与存储分离 (架构之魂)
    • 3.2 秒传功能实现 (附流程图)
    • 3.3 分片上传与断点续传
  • 前端安全与交互细节
  • Docker 一键部署指南
  • 未来规划

  • 1. 为什么造这个轮子?

    在数字化办公场景下,数据资产的管理至关重要。传统 FTP 太老旧,公有云盘不仅限速还存在隐私风险。

    SkyDrive Enterprise 的目标非常明确:

    • 极致轻量:没有复杂的微服务全家桶,一个后端服务 + 一个数据库搞定。
    • 极致性能:利用 Python 异步 IO 处理高并发文件流。
    • 数据自主:私有化部署,我的数据我做主。

    2. 技术栈选型与架构

    为了保证开发效率和运行性能,我选择了目前主流的前后端分离架构。

    🛠 后端 (Backend)

    • Framework: FastAPI (性能接近 Go,自动生成 Swagger 文档,开发体验极佳)
    • Database: MySQL 8.0 (存储元数据)
    • ORM: SQLAlchemy (处理复杂的数据库关系)
    • Security: OAuth2 + JWT (标准的身份认证体系)

    🎨 前端 (Frontend)

    • Core: React 18 + TypeScript
    • Build: Vite (秒级启动,开发体验吊打 Webpack)
    • UI: TailwindCSS / Ant Design (根据实际情况调整)

    📂 项目结构概览

    SkyDrive/
    ├── backend/ # 后端核心
    │ ├── app/
    │ │ ├── api/ # 接口路由 (files.py, users.py)
    │ │ ├── core/ # 核心配置 (security.py, config.py)
    │ │ ├── crud/ # 数据库操作 (CRUD)
    │ │ ├── models/ # SQLAlchemy 模型 (数据库表结构)
    │ │ └── utils/ # 工具类 (Hash计算等)
    │ ├── upload_storage/ # 物理文件默认存储位置
    │ └── Dockerfile
    ├── frontend/ # 前端核心
    │ ├── src/
    │ │ ├── components/ # 公共组件
    │ │ ├── utils/ # 工具函数 (crypto.ts, request.ts)
    │ │ └── App.tsx
    │ └── vite.config.ts
    └── docker-compose.yml # 容器编排


    3. 核心功能深扒

    3.1 元数据与存储分离 (架构之魂)

    做网盘系统,最忌讳的就是把“文件信息”和“物理文件”绑定在一起。我采用了逻辑与物理分离的设计模式。

    设计思路:

    • FileStore (物理表):只存储文件的 Hash (MD5/SHA256)、物理路径、大小。它不关心是谁传的,只关心文件本身。
    • FileMeta (逻辑表):存储文件名、父目录 ID、用户 ID,并通过 file_hash 外键关联到 FileStore。

    #mermaid-svg-Gq8K7kJ0vOWG9gWF{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Gq8K7kJ0vOWG9gWF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .error-icon{fill:#552222;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .marker.cross{stroke:#333333;}#mermaid-svg-Gq8K7kJ0vOWG9gWF svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Gq8K7kJ0vOWG9gWF p{margin:0;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-Gq8K7kJ0vOWG9gWF .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-Gq8K7kJ0vOWG9gWF .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .node rect,#mermaid-svg-Gq8K7kJ0vOWG9gWF .node circle,#mermaid-svg-Gq8K7kJ0vOWG9gWF .node ellipse,#mermaid-svg-Gq8K7kJ0vOWG9gWF .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-Gq8K7kJ0vOWG9gWF .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-Gq8K7kJ0vOWG9gWF :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    uploads

    references

    User

    FileMeta

    int

    id

    PK

    string

    file_name

    文件名

    int

    parent_id

    父文件夹ID

    string

    file_hash

    FK

    指向物理Hash

    FileStore

    string

    file_hash

    PK

    MD5 Hash

    string

    real_path

    磁盘绝对路径

    int

    ref_count

    引用计数

    代码实现 (backend/app/models/file.py):

    class FileMeta(Base):
    __tablename__ = "file_meta"
    id = Column(BigInteger, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("sys_user.id"), nullable=False)
    file_name = Column(String(255), nullable=False)
    # 核心:多条 Meta 可以指向同一个 Store
    file_hash = Column(String(64), ForeignKey("file_store.file_hash"), nullable=True)

    class FileStore(Base):
    __tablename__ = "file_store"
    # 核心:Hash 即主键,天然去重
    file_hash = Column(String(64), primary_key=True, index=True)
    real_path = Column(String(512), nullable=False)
    ref_count = Column(Integer, default=1) # 引用计数,为0时才真正删除

    3.2 秒传功能实现

    当用户上传一个 1GB 的电影时,如果服务器上已经有了这个文件,为什么还要浪费时间重传一遍?

    流程逻辑:

  • 前端计算文件 MD5。
  • 请求后端接口 /check_fast_upload。
  • 后端查询 FileStore。
    • 命中:直接生成 FileMeta 记录,引用计数 +1,返回“上传成功”。
    • 未命中:通知前端走普通上传流程。
  • # backend/app/api/api_v1/endpoints/files.py

    @router.post("/check_fast_upload", response_model=schemas.CheckFastUpload)
    def check_fast_upload(
    *,
    db: Session = Depends(deps.get_db),
    current_user: models.User = Depends(deps.get_current_user),
    file_hash: str = Form(...),
    file_name: str = Form(...),
    # …
    ) > Any:
    # 1. 查库
    existing_store = crud.file.get_by_hash(db, file_hash=file_hash)

    if existing_store:
    # 2. 检查配额…

    # 3. 引用计数自增
    crud.file.increment_ref_count(db, file_hash=file_hash)

    # 4. 仅创建元数据,不涉及 IO 操作
    file_meta = crud.file.create_with_user(
    db=db,
    obj_in=schemas.FileMetaCreate(file_name=file_name, file_hash=file_hash, ...),
    user_id=current_user.id
    )
    return {"can_fast_upload": True, "file_meta": file_meta}

    return {"can_fast_upload": False, "file_meta": None}

    3.3 分片上传与断点续传

    对于大文件,Nginx 默认会限制 body 大小,且中间一旦网络抖动就会前功尽弃。解决方案是 Chunked Upload。

    • 前端:使用 Blob.slice() 将文件切割为 4MB/片,并发上传。
    • 后端:
    • /upload/init:初始化上传任务,生成 upload_id。
    • /upload/chunk:接收分片,暂存到临时目录。
    • /upload/merge:所有分片上传完毕,合并为完整文件,并校验最终 Hash。

    避坑指南:合并文件时,务必再次校验 Hash!

    # 后端合并逻辑核心代码
    @router.post("/upload/merge")
    def merge_chunks(...):
    # …获取所有分片路径…
    with open(final_path, "wb") as final_file:
    for chunk in chunks:
    with open(chunk.temp_path, "rb") as chunk_file:
    # 流式写入,防止内存溢出
    while chunk_content := chunk_file.read(1024 * 1024):
    final_file.write(chunk_content)
    hasher.update(chunk_content) # 边写边算 Hash
    os.remove(chunk.temp_path) # 删除分片

    # 校验 Hash
    if hasher.hexdigest() != file_hash:
    os.remove(final_path)
    raise HTTPException(detail="文件完整性校验失败")


    4. 前端安全与交互细节

    🔐 签名校验 (防止篡改)

    为了防止恶意用户绕过前端直接调用 API,我在 frontend/src/utils/crypto.ts 中实现了一套简单的签名机制。

    注意:这里的 SALT 必须与后端 .env 文件中的配置严格一致!

    // frontend/src/utils/crypto.ts
    import CryptoJS from 'crypto-js';

    // ⚠️ 部署时请修改此值,并确保与后端 .env 中的 SALT 一致
    const SALT = "YOUR_SALT_HERE";

    export const calculateHash = (parts: string[], serverNonce: string) => {
    // 简单的签名算法:参数 + 盐 + 随机数
    const raw = parts.join('') + SALT + serverNonce;
    return CryptoJS.SHA256(raw).toString(CryptoJS.enc.Hex);
    };


    5. Docker 一键部署指南

    为了让大家能快速把项目跑起来,我编写了 docker-compose.yml。

    前置条件:安装 Docker 和 Docker Compose。

    步骤 1:克隆项目

    git clone https://github.com/YourUsername/SkyDrive-Enterprise.git
    cd SkyDrive-Enterprise

    步骤 2:配置环境变量 编辑 backend/app/.env,设置数据库密码和密钥。

    MYSQL_PASSWORD=StrongPassword123
    SECRET_KEY=RandomSecretString…
    SALT=YourSalt… # 记得和前端保持一致

    步骤 3:启动服务

    # 进入后端目录构建并启动
    cd backend
    docker-compose up -d –build

    启动后:

    • 后端 API: http://localhost:8899/docs
    • 前端页面: http://localhost:8899 (假设你配置了 Nginx 代理前端静态资源)

    6. 未来规划

    目前 SkyDrive Enterprise 已经具备了核心的网盘功能,未来计划加入:

    • WebDAV 协议支持 (直接挂载为本地磁盘)
    • Office 在线预览 (集成 OnlyOffice)
    • 图片/视频异步转码 (Celery + Redis)
    • 分享链接有效期与密码

    如果你觉得这个项目对你有帮助,欢迎在 GitHub 上点个 Star!你的支持是我更新的动力。

    👉 项目地址:https://github.com/ChaceQC/SkyDrive

    赞(0)
    未经允许不得转载:171主机测评 » 基于 FastAPI + React 的轻量级网盘
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址