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
]);
}