⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

12.5.2 网断了接着传——断点续传:上传进度保存与恢复

一句话破题

断点续传的核心是"记住已传的"——客户端记录已上传的分片,服务端记录已接收的分片,重新上传时跳过已完成的部分。

本质还原

文件指纹生成

用文件内容生成唯一标识,相同文件会得到相同指纹:

typescript
async function getFileFingerprint(file: File): Promise<string> {
  // 取文件头尾各 2MB + 中间 2MB 计算 hash
  const sliceSize = 2 * 1024 * 1024;
  const chunks = [
    file.slice(0, sliceSize),
    file.slice(Math.floor(file.size / 2) - sliceSize / 2, Math.floor(file.size / 2) + sliceSize / 2),
    file.slice(-sliceSize),
  ];

  const data = await Promise.all(chunks.map((c) => c.arrayBuffer()));
  const combined = new Uint8Array(data.reduce((acc, d) => acc + d.byteLength, 0));
  let offset = 0;
  data.forEach((d) => {
    combined.set(new Uint8Array(d), offset);
    offset += d.byteLength;
  });

  const hashBuffer = await crypto.subtle.digest('SHA-256', combined);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}

客户端实现

typescript
interface UploadState {
  fileId: string;
  uploadedChunks: number[];
  totalChunks: number;
}

class ResumableUploader {
  private storageKey(fingerprint: string) {
    return `upload_${fingerprint}`;
  }

  private saveState(fingerprint: string, state: UploadState) {
    localStorage.setItem(this.storageKey(fingerprint), JSON.stringify(state));
  }

  private loadState(fingerprint: string): UploadState | null {
    const data = localStorage.getItem(this.storageKey(fingerprint));
    return data ? JSON.parse(data) : null;
  }

  async upload(file: File, onProgress: (percent: number) => void) {
    const fingerprint = await getFileFingerprint(file);
    let state = this.loadState(fingerprint);

    // 检查服务端状态
    if (state) {
      const serverState = await this.checkServerState(state.fileId);
      state.uploadedChunks = serverState.uploadedChunks;
    } else {
      // 新上传
      const { fileId } = await this.initUpload(file, fingerprint);
      state = { fileId, uploadedChunks: [], totalChunks: 0 };
    }

    const chunks = createChunks(file);
    state.totalChunks = chunks.length;

    // 过滤已上传的分片
    const pendingChunks = chunks.filter((c) => !state!.uploadedChunks.includes(c.index));

    for (const chunk of pendingChunks) {
      await this.uploadChunk(chunk, state.fileId);
      state.uploadedChunks.push(chunk.index);
      this.saveState(fingerprint, state);
      onProgress((state.uploadedChunks.length / chunks.length) * 100);
    }

    // 完成后清理本地状态
    localStorage.removeItem(this.storageKey(fingerprint));
    await this.mergeChunks(state.fileId, chunks.length);
  }

  // ... 其他方法
}

秒传实现

如果文件指纹已存在,直接返回已上传的文件:

typescript
// 服务端检查
export async function POST(req: Request) {
  const { fingerprint, fileName, fileSize } = await req.json();

  // 检查是否已存在相同文件
  const existingFile = await db.file.findUnique({
    where: { fingerprint },
  });

  if (existingFile) {
    return Response.json({
      status: 'exists',
      fileUrl: existingFile.url,
    });
  }

  // 创建新上传任务
  const fileId = crypto.randomUUID();
  await db.uploadTask.create({
    data: { fileId, fingerprint, fileName, fileSize },
  });

  return Response.json({ status: 'new', fileId });
}

AI 协作指南

  • 核心意图:让 AI 帮你实现断点续传功能。
  • 需求定义公式"请帮我实现一个支持断点续传和秒传的文件上传组件,使用文件指纹识别重复文件。"
  • 关键术语断点续传 (resumable upload)文件指纹 (fingerprint)秒传SHA-256

避坑指南

  • 指纹计算性能:不要对整个大文件计算 hash,取样计算即可。
  • 存储清理:定期清理过期的上传任务和临时文件。
  • 并发控制:同一文件不要允许多个客户端同时上传。