[PHP] 文件二进制分片切割加密传输

希米  (UID: 5950) [复制链接]
帖子链接已复制到剪贴板
帖子已经有人评论啦,不支持删除!

36 1
说明:
 
1.  前端将文件分片加密后上传,后端存储加密数据和索引
 
2. 下载时前端获取索引,逐片下载解密并合并成原文件
 
3. 加密方式是 AES-256 ,调用Crypto库前端执行的;
 
4.就一个演示案例,没有写后台,就两个文件,前端html,后端php;
 
5.文件上传在“uploads”目录内,可以打开php文件自己修改;
 
6.主要用途就是减少网络波动导致文件传输不稳定,与服务器上传文件大小限制;
 
7.传输实时显示进度条,使用的是4线程并发传输,局域网测试可以跑满带宽;
 
 
代码:
index.html
<!-- 
		Title:文件二进制分片加密传输
    Blog:https://www.ximi.me
    Author:希米
    Time:2036-06-19
    说明:
      - 前端将文件分片加密后上传,后端存储加密数据和索引
      - 下载时前端获取索引,逐片下载解密并合并成原文件
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>文件二进制分片加密传输</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
</head>

<body class="bg-gray-100 p-6">

<div class="max-w-2xl mx-auto space-y-6">

<h1 class="text-2xl font-bold text-center"> 文件二进制分片加密传输演示</h1>

<!-- 密钥 -->
<div class="bg-white p-4 rounded-xl">
  <input id="key" type="password" placeholder="加密密钥"
    class="w-full p-3 border rounded">
</div>

<!-- 上传 -->
<div class="bg-white p-4 rounded-xl">
  <h2 class="font-bold mb-2">上传文件</h2>

  <input type="file" id="file" class="mb-3">

  <button onclick="upload()" class="bg-blue-500 text-white px-4 py-2 rounded">
    开始上传
  </button>

  <div id="upStatus" class="text-sm mt-2 text-gray-600"></div>
  <progress id="upProgress" value="0" max="100" class="w-full mt-2"></progress>
</div>

<!-- 下载 -->
<div class="bg-white p-4 rounded-xl">
  <h2 class="font-bold mb-2">下载文件</h2>

  <input id="fid" placeholder="文件ID" class="w-full p-2 border mb-3">

  <button onclick="downloadFile()" class="bg-purple-500 text-white px-4 py-2 rounded">
    开始下载
  </button>

  <div id="downStatus" class="text-sm mt-2 text-gray-600"></div>
  <progress id="downProgress" value="0" max="100" class="w-full mt-2"></progress>
</div>

</div>

<script>


// ================= 工具 =================
function sha256(str){
  return CryptoJS.SHA256(str).toString();
}

function safeJSON(t){
  try{
    return JSON.parse(t);
  }catch(e){
    console.log("❌ 后端返回:", t);
    throw new Error("JSON解析失败");
  }
}

// ================= 超时 fetch =================
function timeoutFetch(url, options, t = 20000){
  return Promise.race([
    fetch(url, options),
    new Promise((_, rej)=>setTimeout(()=>rej("timeout"), t))
  ])
}

// ================= 并发池(稳定版) =================
async function asyncPool(limit, arr, fn){

  const ret = []
  const executing = new Set()

  for(const item of arr){

    const p = Promise.resolve().then(()=>fn(item))

    ret.push(p)
    executing.add(p)

    const clean = () => executing.delete(p)
    p.then(clean).catch(clean)

    if(executing.size >= limit){
      await Promise.race(executing)
    }
  }

  return Promise.all(ret)
}

// =====================================================
// 🚀 上传(稳定版:防卡死)
// =====================================================
async function upload(){

  const file = document.getElementById('file').files[0]
  const key = document.getElementById('key').value

  if(!file || !key) return alert("缺文件或密钥")

  const status = document.getElementById('upStatus')
  const bar = document.getElementById('upProgress')

  const buf = await file.arrayBuffer()

  const chunkSize = 256 * 1024
  const chunks = []

  const originalName = file.name
  const fileId = "f_" + Date.now()

  status.innerText = "切片加密中..."

  let processed = 0

  // ================= 切片 + 加密 =================
  for(let i=0;i<buf.byteLength;i+=chunkSize){

    const slice = buf.slice(i, i+chunkSize)

    const word = CryptoJS.lib.WordArray.create(slice)

    const encrypted = CryptoJS.AES.encrypt(word, key).toString()

    chunks.push({
      index: i,
      name: Math.random().toString(36).slice(2) + ".bin",
      data: encrypted,
      hash: sha256(encrypted)
    })

    processed += slice.byteLength

    // ⭐ UI刷新(防假死)
    if(i % (chunkSize * 2) === 0){
      bar.value = (processed / buf.byteLength) * 40
      status.innerText = `切片加密 ${(processed/buf.byteLength*100).toFixed(1)}%`

      await new Promise(r => setTimeout(r, 0))
    }
  }

  let done = 0

  status.innerText = "上传中..."

  // ================= ⭐ 关键:2线程上传(稳定) =================
  await asyncPool(2, chunks, async (c)=>{

    const fd = new FormData()
    fd.append("id", fileId)
    fd.append("name", c.name)
    fd.append("chunk", c.data)

    const res = await timeoutFetch("file.php?action=upload",{
      method:"POST",
      body:fd
    }, 20000)

    const text = await res.text()
    const data = safeJSON(text)

    if(data.status !== "ok"){
      throw new Error("upload fail")
    }

    done++
    bar.value = 40 + (done / chunks.length) * 60

    status.innerText = `上传 ${done}/${chunks.length}`
  })

  // ================= manifest =================
  const manifest = {
    originalName,
    chunks: chunks.map(c=>({
      name:c.name,
      hash:c.hash
    }))
  }

  const fd2 = new FormData()
  fd2.append("id", fileId)
  fd2.append("manifest", JSON.stringify(manifest))

  await fetch("file.php?action=upload",{
    method:"POST",
    body:fd2
  })

  bar.value = 100
  status.innerText = "上传完成 ID: " + fileId
}

// =====================================================
// 🚀 下载(稳定版)
// =====================================================
async function downloadFile(){

  const id = document.getElementById('fid').value
  const key = document.getElementById('key').value

  const status = document.getElementById('downStatus')
  const bar = document.getElementById('downProgress')

  const res = await fetch("file.php?action=send&id="+id)
  const text = await res.text()

  const data = safeJSON(text)

  if(data.status !== "ok"){
    throw new Error(data.msg)
  }

  const manifest = data.manifest
  const list = manifest.chunks
  const originalName = manifest.originalName

  let done = 0

  status.innerText = "下载中..."

  // ================= ⭐ 2线程下载(稳定) =================
  const results = await asyncPool(2, list, async (item)=>{

    const r = await timeoutFetch(`uploads/${id}/${item.name}`,{},20000)
    const enc = await r.text()

    const decrypted = CryptoJS.AES.decrypt(enc, key)

    const base64 = decrypted.toString(CryptoJS.enc.Base64)

    const binary = atob(base64)

    const arr = new Uint8Array(binary.length)

    for(let i=0;i<binary.length;i++){
      arr[i] = binary.charCodeAt(i)
    }

    done++
    bar.value = (done / list.length) * 100
    status.innerText = `下载 ${done}/${list.length}`

    return {
      index: list.indexOf(item),
      data: arr
    }
  })

  // ================= 合并 =================
  results.sort((a,b)=>a.index-b.index)

  let total = 0
  results.forEach(r=> total += r.data.length)

  const merged = new Uint8Array(total)

  let offset = 0

  for(const r of results){
    merged.set(r.data, offset)
    offset += r.data.length
  }

  const blob = new Blob([merged])

  const a = document.createElement("a")
  a.href = URL.createObjectURL(blob)
  a.download = originalName
  a.click()

  status.innerText = "下载完成"
  bar.value = 100
}

</script>

</body>
</html>
file.php
<?php

$uploadDir = 'uploads/';
$action = $_GET['action'] ?? '';

header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');

function out($data){
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

/**
 * ======================
 * upload
 * ======================
 */
if($action === 'upload'){

    $id = $_POST['id'] ?? null;
    if(!$id){
        $id = uniqid("f_");
    }

    $dir = $uploadDir . $id;
    if(!is_dir($dir)){
        mkdir($dir, 0755, true);
    }

    // chunk
    if(isset($_POST['chunk']) && isset($_POST['name'])){

        file_put_contents($dir . "/" . $_POST['name'], $_POST['chunk']);

        out([
            "status"=>"ok",
            "type"=>"chunk",
            "id"=>$id
        ]);
    }

    // manifest
    if(isset($_POST['manifest'])){

        file_put_contents($dir . "/manifest.json", $_POST['manifest']);

        out([
            "status"=>"ok",
            "type"=>"manifest",
            "id"=>$id
        ]);
    }

    out(["status"=>"error","msg"=>"invalid"]);
}


/**
 * ======================
 * download
 * ======================
 */
if($action === 'send'){

    $id = $_GET['id'] ?? '';
    $dir = $uploadDir . $id;
    $file = $dir . "/manifest.json";

    if(!file_exists($file)){
        out(["status"=>"error","msg"=>"not found"]);
    }

    $manifest = json_decode(file_get_contents($file), true);

    out([
        "status"=>"ok",
        "manifest"=>$manifest,
        "id"=>$id
    ]);
}
博客:www.ximi.me
已有评论 ( 1 )
提示:您必须 登录 才能查看此内容。
域名市场
   域名载入中...
创建新帖
自助推广 (点击空位或 这里 添加)
确认删除
确定要删除这篇帖子吗?删除后将无法恢复。
删除成功
帖子已成功删除,页面将自动刷新。
删除失败
删除帖子时发生错误,请稍后再试。