大家好,这里是 eianun科技碎片

最近在技术圈发现了一个非常香的“羊毛”——云服务商(Hi168)注册就免费送 500GB 实名后送1T 的 S3 对象存储空间!对于喜欢折腾服务器、经常需要中转大文件或分享素材的玩家来说,这简直是天降的神仙级后备仓库。

今天,我就带大家实战演示,如何将这 1TB 空间“榨干”,玩出两种截然不同用法:

  1. 轻量级玩法:利用 Cloudflare Workers 零成本手搓一个极简的  私人直链网盘,随时随地拖拽上传,极速获取文件直链。

  2. 终极完全体:将 S3 空间完美挂载到你服务器的 Alist 中,打造一个可以在线预览视频、管理目录的全功能私人云盘!


 方案一:极客专属,Cloudflare Workers 零成本搭建专属极速云盘

如果你手里没有服务器,或者只想弄个轻量级的工具用来快速分享文件,Cloudflare Workers 是最完美的方案。

1.获取 S3 凭证与“避坑指南”

在开始之前,我们需要从服务商后台拿到 S3 的连接配置。

来到注册网站:https://www.hi168.com 注册后登陆进去-储存管理-创建储存桶-获取参数

这里有一个极其容易踩坑的致命点

通常 S3 需要四个参数,请对照你的后台严格记录:

  1. Endpoint(端点)OSS服务地址

  2. Access Key(访问密钥):后台密钥列表获取。

  3. Secret Key(安全密钥):后台密钥列表获取。

  4. 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 数据库

  1. 回到 Cloudflare 的主界面(Dashboard),在左侧菜单找到 Storage & Databases -> KV

  2. 点击右上角的 Create a namespace(创建命名空间)。

  3. 名字随便起,比如叫 MY_S3_RECORDS,然后点击 Add 添加。

第二步:把数据库和密码绑定给 Worker

  1. 回到你刚才那个 Worker 的详情页。

  2. 进入 Settings(设置) -> Variables and Secrets(变量和机密)

  3. 设置访问密码: 在你之前填 S3_BUCKET 的地方,再新增一个文本变量:

    • 变量名:MY_PASSWORD

    • 值:填一个你自己记得住的密码(比如 eianun666)。

  4. 绑定 KV 数据库: 往下滚动,找到 KV Namespace Bindings(KV 命名空间绑定) 这一块。

    • 点击 Add binding。

    • Variable name(变量名)填:RECORDS

    • KV namespace(命名空间)下拉选择你刚才创建的 MY_S3_RECORDS

  5. 点击最下方的 Save and deploy(保存并部署)。

3. 配置环境变量

在 Worker 的 Settings -> Variables and Secrets 中添加以下变量:

  • S3_ENDPOINT (文本): https://s3.hi168.com

  • S3_BUCKET (文本): 你的挂载名称(不是短桶名!)

  • S3_ACCESS_KEY (密钥): 你的 AK

  • S3_SECRET_KEY (密钥): 你的 SK

保存部署后,访问你的 Worker 专属域名,就能看到一个支持拖拽、支持并发极速上传的极简云盘界面了!


方案二:终极体验,挂载 Alist 打造全功能私人网盘

如果你有一台云服务器,并且部署了 Alist,那体验可以直接拉满!不仅能在线预览视频、图片,还能通过 WebDAV 挂载到电脑本地当硬盘用。

在 Alist 中添加存储,选择 Amazon S3 协议,并按以下参数填写:

  • 挂载路径/Hi168云盘(自定义)

  • 端点 (Endpoint)https://s3.hi168.com(结尾不要带 /

  • 存储桶 (Bucket):填入那一长串挂载名称(血泪教训,再次强调)。

  • 区域 (Region):填 us-east-1

  • Access 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科技碎片