大家好,这里是 eianun科技碎片。
最近在技术圈发现了一个非常香的“羊毛”——云服务商(Hi168)注册就免费送 500GB 实名后送1T 的 S3 对象存储空间!对于喜欢折腾服务器、经常需要中转大文件或分享素材的玩家来说,这简直是天降的神仙级后备仓库。
今天,我就带大家实战演示,如何将这 1TB 空间“榨干”,玩出两种截然不同用法:
轻量级玩法:利用 Cloudflare Workers 零成本手搓一个极简的 私人直链网盘,随时随地拖拽上传,极速获取文件直链。
终极完全体:将 S3 空间完美挂载到你服务器的 Alist 中,打造一个可以在线预览视频、管理目录的全功能私人云盘!
方案一:极客专属,Cloudflare Workers 零成本搭建专属极速云盘
如果你手里没有服务器,或者只想弄个轻量级的工具用来快速分享文件,Cloudflare Workers 是最完美的方案。
1.获取 S3 凭证与“避坑指南”
在开始之前,我们需要从服务商后台拿到 S3 的连接配置。
来到注册网站:https://www.hi168.com 注册后登陆进去-储存管理-创建储存桶-获取参数
这里有一个极其容易踩坑的致命点!
通常 S3 需要四个参数,请对照你的后台严格记录:
Endpoint(端点):OSS服务地址
Access Key(访问密钥):后台密钥列表获取。
Secret Key(安全密钥):后台密钥列表获取。
Bucket(存储桶名称):【超级大坑】 千万不要用后台显示的那个为了好看的“短桶名”(比如
webs3)!一定要使用系统分配的一长串 “挂载名称”(例如hi168-30042-xxxxxx)。API 接口只认这个底层真实的挂载名称,否则会一直报NoSuchBucket的错误!
2. 创建 Worker
进入 Cloudflare 后台,新建一个 Worker,将以下代码全部复制进去并部署:
Cloudflare Worker 源码(前后端合一)
// 1. AWS 签名辅助函数 (不变)
async function hmacRaw(key, string) {
const keyData = typeof key === "string" ? new TextEncoder().encode(key) : key;
const cryptoKey = await crypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
return crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(string));
}
function toHex(buffer) {
return Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, '0')).join('');
}
async function sha256Hex(data) {
const digest = await crypto.subtle.digest("SHA-256", typeof data === "string" ? new TextEncoder().encode(data) : data);
return toHex(digest);
}
// 2. 带有密码锁和云端拉取逻辑的前端页面
const htmlPage = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>eianun科技碎片 - 私人云盘</title>
<style>
:root { --primary: #007bff; --bg: #f4f7f6; --text: #333; --border: #e2e8f0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--bg); color: var(--text); max-width: 800px; margin: 40px auto; padding: 0 20px; }
.container { background: white; padding: 30px 40px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.05); }
h2 { text-align: center; color: #1a1a1a; margin-bottom: 20px; font-weight: 600;}
.auth-box { display: flex; gap: 10px; margin-bottom: 25px; justify-content: center; }
.auth-box input { padding: 10px 15px; border: 1px solid var(--border); border-radius: 8px; outline: none; flex-grow: 1; max-width: 300px; font-size: 15px;}
.auth-box input:focus { border-color: var(--primary); }
.btn { background: var(--primary); color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 15px; font-weight: 500; transition: 0.2s; }
.btn:hover { background: #0069d9; }
.btn:disabled { background: #94a3b8; cursor: not-allowed; }
#workspace { display: none; }
.drop-zone { border: 2px dashed #cbd5e1; border-radius: 12px; padding: 40px 20px; text-align: center; cursor: pointer; transition: all 0.3s ease; background: #f8fafc; margin-bottom: 20px; }
.drop-zone:hover { border-color: var(--primary); background: #eff6ff; }
#fileInput { display: none; }
#selectedFileName { color: var(--primary); font-weight: 600; margin-top: 15px; font-size: 14px;}
#status { text-align: center; margin-bottom: 20px; font-size: 14px; min-height: 20px; font-weight: bold;}
.history-section { margin-top: 30px; border-top: 1px solid var(--border); padding-top: 20px;}
.history-title { font-size: 16px; font-weight: 600; color: #475569; margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center;}
.history-list { list-style: none; padding: 0; margin: 0; max-height: 400px; overflow-y: auto;}
.history-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; background: #fff;}
.file-info { display: flex; flex-direction: column; overflow: hidden; margin-right: 15px;}
.file-name { font-weight: 500; color: #1e293b; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;}
.file-time { font-size: 12px; color: #94a3b8; margin-top: 4px;}
.actions { display: flex; gap: 8px; flex-shrink: 0;}
.btn-sm { padding: 6px 12px; font-size: 12px; background: #f1f5f9; color: #475569; border: none; border-radius: 6px; cursor: pointer; text-decoration: none;}
.btn-sm:hover { background: #e2e8f0; color: #0f172a;}
/* 底部作者签名 */
.footer { text-align: center; margin-top: 30px; font-size: 13px; color: #94a3b8; }
.footer a { color: var(--primary); text-decoration: none; font-weight: bold;}
</style>
</head>
<body>
<div class="container">
<h2>☁️ 私人云盘管理系统</h2>
<div class="auth-box" id="authBox">
<input type="password" id="passInput" placeholder="请输入专属访问密码...">
<button class="btn" onclick="verifyAndLoad()">进入云盘</button>
</div>
<div id="authError" style="color: red; text-align: center; margin-bottom: 15px; font-size: 14px;"></div>
<div id="workspace">
<div class="drop-zone" onclick="document.getElementById('fileInput').click()">
<div style="font-size: 40px; margin-bottom: 10px;">📤</div>
<p>点击或拖拽文件到此处,支持批量上传</p>
<div id="selectedFileName"></div>
</div>
<input type="file" id="fileInput" multiple onchange="fileSelected(event)">
<button class="btn" id="uploadBtn" onclick="uploadFiles()" style="width: 100%;">🚀 开始批量极速上传</button>
<div id="status" style="margin-top: 15px;"></div>
<div class="history-section">
<div class="history-title">
<span>☁️ 云端文件记录</span>
<button class="btn-sm" onclick="verifyAndLoad()" style="background:#eff6ff; color:#007bff;">🔄 刷新列表</button>
</div>
<ul class="history-list" id="historyList"></ul>
</div>
</div>
</div>
<div class="footer">
Powered by <a>eianun</a> © 2026
</div>
<script>
let globalToken = localStorage.getItem('s3_cloud_token') || '';
let selectedFiles = [];
window.onload = () => {
if(globalToken) {
document.getElementById('passInput').value = globalToken;
verifyAndLoad();
}
};
function fileSelected(e) {
if (e.target.files.length > 0) {
selectedFiles = Array.from(e.target.files);
document.getElementById('selectedFileName').textContent = \`已选中 \${selectedFiles.length} 个文件,准备就绪\`;
}
}
async function verifyAndLoad() {
const pwd = document.getElementById('passInput').value;
if(!pwd) return;
document.getElementById('authError').innerText = '正在连接云端...';
try {
const res = await fetch('/api/history', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pwd })
});
const data = await res.json();
if (data.success) {
globalToken = pwd;
localStorage.setItem('s3_cloud_token', pwd);
document.getElementById('authBox').style.display = 'none';
document.getElementById('authError').style.display = 'none';
document.getElementById('workspace').style.display = 'block';
renderHistory(data.history);
} else {
document.getElementById('authError').innerText = '❌ 密码错误或无权访问';
localStorage.removeItem('s3_cloud_token');
}
} catch (err) {
document.getElementById('authError').innerText = '❌ 网络连接失败';
}
}
function renderHistory(historyArray) {
const listObj = document.getElementById('historyList');
listObj.innerHTML = '';
if(historyArray.length === 0) {
listObj.innerHTML = '<li style="text-align:center; color:#94a3b8; padding: 20px 0;">云端暂无文件</li>';
return;
}
historyArray.forEach(item => {
listObj.innerHTML += \`
<li class="history-item">
<div class="file-info">
<span class="file-name" title="\${item.name}">\${item.name}</span>
<span class="file-time">\${item.time}</span>
</div>
<div class="actions">
<button class="btn-sm" onclick="copyText('\${item.url}', this)" style="background:#eff6ff; color:#007bff;">复制直链</button>
<a href="\${item.url}" target="_blank" class="btn-sm">查看</a>
</div>
</li>
\`;
});
}
async function copyText(text, btn) {
await navigator.clipboard.writeText(text);
const old = btn.innerText;
btn.innerText = '已复制!';
setTimeout(() => btn.innerText = old, 2000);
}
// --- 核心更新:多文件并发上传逻辑 ---
async function uploadFiles() {
if (selectedFiles.length === 0) return alert('请先选择文件!');
const btn = document.getElementById('uploadBtn');
const statusDiv = document.getElementById('status');
btn.disabled = true;
statusDiv.innerHTML = \`<span style="color:#007bff;">🚀 正在并发上传 \${selectedFiles.length} 个文件...</span>\`;
let successCount = 0;
let failCount = 0;
// 利用 Promise.all 实现浏览器端多线程并发请求,拉满带宽
const uploadPromises = selectedFiles.map(async (file) => {
const formData = new FormData();
formData.append('file', file);
formData.append('password', globalToken);
try {
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const data = await res.json();
if (data.success) {
successCount++;
} else {
failCount++;
}
} catch (err) {
failCount++;
}
});
// 等待所有文件上传动作完成
await Promise.all(uploadPromises);
statusDiv.innerHTML = \`<span style="color: #10b981;">✅ 批量上传完成!成功: \${successCount},失败: \${failCount}</span>\`;
// 清理状态并刷新列表
selectedFiles = [];
document.getElementById('fileInput').value = '';
document.getElementById('selectedFileName').textContent = '';
verifyAndLoad();
btn.disabled = false;
setTimeout(() => { if(statusDiv.innerText.includes('完成')) statusDiv.innerHTML = ''; }, 4000);
}
</script>
</body>
</html>`;
// 3. 核心后端逻辑 (加入路由和 KV 读写)
export default {
async fetch(request, env) {
const url = new URL(request.url);
// 路由 1: 访问主页,返回 HTML
if (request.method === 'GET' && url.pathname === '/') {
return new Response(htmlPage, { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
}
// 路由 2: 获取云端历史记录 API
if (request.method === 'POST' && url.pathname === '/api/history') {
try {
const body = await request.json();
// 鉴权
if (body.password !== env.MY_PASSWORD) {
return new Response(JSON.stringify({ success: false, error: 'Unauthorized' }), { status: 401 });
}
// 从 KV 数据库读取记录
const history = await env.RECORDS.get("s3_history", { type: "json" }) || [];
return new Response(JSON.stringify({ success: true, history: history }), { headers: { 'Content-Type': 'application/json' } });
} catch (e) {
return new Response(JSON.stringify({ success: false, error: e.message }), { status: 500 });
}
}
// 路由 3: 处理文件上传 API
if (request.method === 'POST' && url.pathname === '/api/upload') {
try {
const formData = await request.formData();
// 鉴权:拦截非法上传
if (formData.get('password') !== env.MY_PASSWORD) {
return new Response(JSON.stringify({ success: false, error: '密码错误,拒绝上传' }), { status: 401 });
}
const file = formData.get('file');
if (!file) throw new Error("没有找到文件");
// --- S3 原生上传流程开始 ---
const fileExt = file.name.split('.').pop();
const randomStr = Math.random().toString(36).substring(2, 8);
const filename = `${Date.now()}-${randomStr}.${fileExt}`;
const endpoint = env.S3_ENDPOINT;
const bucket = env.S3_BUCKET;
const s3Url = new URL(`/${bucket}/${filename}`, endpoint);
const host = s3Url.hostname;
const date = new Date();
const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, "");
const dateStamp = amzDate.substring(0, 8);
const fileBuffer = await file.arrayBuffer();
const payloadHash = await sha256Hex(fileBuffer);
const method = "PUT";
const canonicalUri = `/${bucket}/${filename}`;
const canonicalQuerystring = "";
const contentType = file.type || "application/octet-stream";
const canonicalHeaders = `content-type:${contentType}\nhost:${host}\nx-amz-content-sha256:${payloadHash}\nx-amz-date:${amzDate}\n`;
const signedHeaders = "content-type;host;x-amz-content-sha256;x-amz-date";
const canonicalRequest = `${method}\n${canonicalUri}\n${canonicalQuerystring}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
const canonicalRequestHash = await sha256Hex(canonicalRequest);
const algorithm = "AWS4-HMAC-SHA256";
const region = "auto";
const service = "s3";
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${canonicalRequestHash}`;
const kDate = await hmacRaw(`AWS4${env.S3_SECRET_KEY}`, dateStamp);
const kRegion = await hmacRaw(kDate, region);
const kService = await hmacRaw(kRegion, service);
const kSigning = await hmacRaw(kService, "aws4_request");
const signatureBuffer = await hmacRaw(kSigning, stringToSign);
const signature = toHex(signatureBuffer);
const authorizationHeader = `${algorithm} Credential=${env.S3_ACCESS_KEY}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
const response = await fetch(s3Url.toString(), {
method: "PUT",
body: fileBuffer,
headers: {
"Authorization": authorizationHeader,
"Content-Type": contentType,
"x-amz-date": amzDate,
"x-amz-content-sha256": payloadHash,
"Content-Length": file.size.toString()
}
});
if (response.ok) {
// --- 上传成功,将记录保存到 KV 数据库 ---
const finalUrl = s3Url.toString();
const timeStr = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
// 先获取老数据
let history = await env.RECORDS.get("s3_history", { type: "json" }) || [];
// 把新数据插到最前面
history.unshift({ name: file.name, url: finalUrl, time: timeStr });
// 为了防止数据库过大,最多保存最近的 100 条记录
if (history.length > 100) history.pop();
// 写回数据库
await env.RECORDS.put("s3_history", JSON.stringify(history));
return new Response(JSON.stringify({ success: true, url: finalUrl }), { headers: { 'Content-Type': 'application/json' } });
} else {
const errorText = await response.text();
return new Response(JSON.stringify({ success: false, error: errorText }), { status: response.status });
}
} catch (e) {
return new Response(JSON.stringify({ success: false, error: e.message }), { status: 500 });
}
}
return new Response('Method Not Allowed', { status: 405 });
}
};3. 创建数据库
第一步:创建一个免费的 KV 数据库
回到 Cloudflare 的主界面(Dashboard),在左侧菜单找到 Storage & Databases -> KV。
点击右上角的 Create a namespace(创建命名空间)。
名字随便起,比如叫
MY_S3_RECORDS,然后点击 Add 添加。
第二步:把数据库和密码绑定给 Worker
回到你刚才那个 Worker 的详情页。
进入 Settings(设置) -> Variables and Secrets(变量和机密)。
设置访问密码: 在你之前填
S3_BUCKET的地方,再新增一个文本变量:变量名:
MY_PASSWORD值:填一个你自己记得住的密码(比如
eianun666)。
绑定 KV 数据库: 往下滚动,找到 KV Namespace Bindings(KV 命名空间绑定) 这一块。
点击 Add binding。
Variable name(变量名)填:
RECORDS。KV namespace(命名空间)下拉选择你刚才创建的
MY_S3_RECORDS。
点击最下方的 Save and deploy(保存并部署)。
3. 配置环境变量
在 Worker 的 Settings -> Variables and Secrets 中添加以下变量:
S3_ENDPOINT(文本):https://s3.hi168.comS3_BUCKET(文本): 你的挂载名称(不是短桶名!)S3_ACCESS_KEY(密钥): 你的 AKS3_SECRET_KEY(密钥): 你的 SK
保存部署后,访问你的 Worker 专属域名,就能看到一个支持拖拽、支持并发极速上传的极简云盘界面了!
方案二:终极体验,挂载 Alist 打造全功能私人网盘
如果你有一台云服务器,并且部署了 Alist,那体验可以直接拉满!不仅能在线预览视频、图片,还能通过 WebDAV 挂载到电脑本地当硬盘用。
在 Alist 中添加存储,选择 Amazon S3 协议,并按以下参数填写:
挂载路径:
/Hi168云盘(自定义)端点 (Endpoint):
https://s3.hi168.com(结尾不要带/)存储桶 (Bucket):填入那一长串挂载名称(血泪教训,再次强调)。
区域 (Region):填
us-east-1Access Key & Secret Key:正常填入。
致命报错解决:no such host
如果你填完后状态报红,提示 dial tcp: lookup xxx.s3.hi168.com ... no such host,这是因为中小服务商不支持 S3 的“虚拟主机样式(Virtual Hosted-Style)”。
解决办法:在 Alist 配置中往下划,找到 【强制路径样式 (Force Path Style)】,将这个开关 开启 (True),保存后瞬间变绿!
总结
通过以上两种方案,我们成功把白嫖来的 1TB 空间玩出了花。日常高频发图,用 Cloudflare Worker 随时随地拖拽秒传;大文件归档、视频素材管理,用 Alist 统一调度,美滋滋。
安全提醒: 录制视频或分享代码时,千万记得给你的 Access Key 和 Secret Key 打码!这俩钥匙一旦泄露,你的 1TB 空间就会变成别人的跑马场。
如果你在折腾过程中遇到了其他问题,欢迎在评论区交流。我们下期教程再见!👋
本文首发于博客,视频版教程请关注 YouTube 频道:eianun科技碎片
评论