如何在Cloudflare搭建临时文件存储服务
2025-12-03 12:39:10

全程在浏览器操作,无需本地开发环境,支持公开上传 + 自动过期 + 原始文件名下载

第一步:创建 KV 命名空间(用于存储临时文件)

目标:创建一个名为 TEMP_STORE 的 KV 存储空间。
操作路径:
Dashboard 首页 → 左侧边栏 「账户和主页」 → 「存储和数据库」 → 「Workers KV」
操作步骤:

  1. 点击右上角 「Create instance」 按钮
  2. 填写:
    Name: TEMP_STORE
    (其他选项保持默认)
  3. 点击 「Create」
    提示:无需记录 Namespace ID,后续通过变量名绑定即可。

第二步:创建 Worker 并粘贴代码

目标:部署处理上传/下载逻辑的 Worker。
操作路径:
Dashboard 首页 → 左侧边栏 「账户和主页」 → 「计算和 AI」 → 「Workers 和 Pages」
操作步骤:

  1. 点击 「创建应用」 → 选择 「从 Hello World! 开始」
  2. 应用名称输入:tmp-worker(可自定义)
  3. 进入代码编辑器后,全选并删除默认代码
  4. 将下方完整 JS 代码 逐字粘贴 到编辑区
    重要:请先修改以下两处为你自己的信息!
1
2
3
4
5
// 1. HTML 标题(第 4 行)
<title>📁 tmp.yourdomain.com</title>

// 2. 下载链接域名(约第 120 行)
const downloadUrl = https://tmp.yourdomain.com/${fileId};
▶ 点击展开完整 Worker 代码(含 4 位短 ID + 自动去重 + 12 小时过期)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
// ====== HTML 页面 ======
const HTML = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Air1临时文件</title>
<link rel="icon" type="image/png" href="https://air1.cn/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 500px; margin: 40px auto; padding: 20px; }
h1 { text-align: center; }
input, button { width: 100%; padding: 12px; margin: 10px 0; box-sizing: border-box; border: 1px solid #ccc; border-radius: 6px; }
button { background: #007bff; color: white; border: none; cursor: pointer; }
button:hover { background: #0069d9; }
#result { margin-top: 15px; padding: 12px; background: #e8f4ff; border-radius: 6px; word-break: break-all; }
a { color: #007bff; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>📁 临时文件保存</h1>
<input type="file" id="fileInput" />
<button onclick="upload()">上传(≤25MB)</button>
<div id="result"></div>
<p style="text-align:center;color:#666;font-size:14px;">文件 12 小时后自动删除</p>

<script>
async function upload() {
const file = document.getElementById('fileInput').files[0];
if (!file) return alert("请选择文件");
if (file.size > 26112000) return alert("文件不能超过 25MB");

const formData = new FormData();
formData.append("file", file);
const btn = document.querySelector('button');
btn.disabled = true;
btn.textContent = "上传中…";

try {
const res = await fetch("/api/upload-public", { method: "POST", body: formData });
const data = await res.json();
const el = document.getElementById('result');
if (res.ok && data.downloadUrl) {
el.innerHTML = '<strong>✅ 分享链接:</strong><br><a href="' + data.downloadUrl + '" target="_blank">' + data.downloadUrl + '</a>';
} else {
el.innerText = "❌ " + (data.error || "上传失败");
}
} catch (e) {
document.getElementById('result').innerText = "网络错误:" + e.message;
} finally {
btn.disabled = false;
btn.textContent = "上传(≤25MB)";
}
}
</script>
</body>
</html>
`;

// ====== 工具函数 ======
function generateFileId() {
return Math.random().toString(36).substring(2, 6); // 4字符随机ID
}

// ====== 公共上传逻辑(无鉴权) ======
async function handleFileUpload(file, env) {
const MAX_SIZE = 26112000; // 25 MB
if (file.size > MAX_SIZE) {
return new Response(JSON.stringify({ error: "文件不能超过 25MB" }), {
status: 400,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
});
}

const fileId = generateFileId();
const arrayBuffer = await file.arrayBuffer();

await env.TEMP_STORE.put(fileId, arrayBuffer, {
metadata: {
filename: file.name || "file",
contentType: file.type || "application/octet-stream"
},
expirationTtl: 43200 // 12小时 = 43200秒
});

const downloadUrl = `https://tmp.air1.cn/${fileId}`; // 👈 你的实际域名

return new Response(JSON.stringify({ downloadUrl }), {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
}
});
}

// ====== 受保护上传逻辑(需 Token) ======
async function handleProtectedUpload(file, env, request) {
const authHeader = request.headers.get("Authorization");
const expectedToken = env.UPLOAD_TOKEN; // 从 Secrets 读取

if (!expectedToken) {
return new Response(JSON.stringify({ error: "服务器未配置 UPLOAD_TOKEN" }), {
status: 500,
headers: { "Content-Type": "application/json" }
});
}

if (authHeader !== `Bearer ${expectedToken}`) {
return new Response(JSON.stringify({ error: "无效或缺失 Token" }), {
status: 401,
headers: { "Content-Type": "application/json" }
});
}

const MAX_SIZE = 26112000;
if (file.size > MAX_SIZE) {
return new Response(JSON.stringify({ error: "文件不能超过 25MB" }), {
status: 400,
headers: { "Content-Type": "application/json" }
});
}

const fileId = generateFileId();
const arrayBuffer = await file.arrayBuffer();

await env.TEMP_STORE.put(fileId, arrayBuffer, {
metadata: {
filename: file.name || "file",
contentType: file.type || "application/octet-stream"
},
expirationTtl: 43200
});

const downloadUrl = `https://tmp.air1.cn/${fileId}`;
return new Response(JSON.stringify({ downloadUrl }), {
headers: { "Content-Type": "application/json" }
});
}

// ====== 主入口 ======
export default {
async fetch(request, env) {
const url = new URL(request.url);
const { pathname } = url;

// 1. 首页
if (pathname === "/") {
return new Response(HTML, {
headers: { "Content-Type": "text/html; charset=utf-8" }
});
}

// 2. CORS 预检
if (request.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
}
});
}

// 3. 公开上传接口(网页使用)
if (pathname === "/api/upload-public" && request.method === "POST") {
try {
const formData = await request.formData();
const file = formData.get("file");
if (!file || !(file instanceof File)) {
return new Response(JSON.stringify({ error: "未提供有效文件" }), {
status: 400,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
});
}
return await handleFileUpload(file, env);
} catch (e) {
console.error("公开上传出错:", e);
return new Response(JSON.stringify({ error: "服务器内部错误" }), {
status: 500,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }
});
}
}

// 4. 受保护上传接口(需 Token)
if (pathname === "/api/upload" && request.method === "POST") {
try {
const formData = await request.formData();
const file = formData.get("file");
if (!file || !(file instanceof File)) {
return new Response(JSON.stringify({ error: "未提供有效文件" }), {
status: 400,
headers: { "Content-Type": "application/json" }
});
}
return await handleProtectedUpload(file, env, request);
} catch (e) {
console.error("受保护上传出错:", e);
return new Response(JSON.stringify({ error: "服务器内部错误" }), {
status: 500,
headers: { "Content-Type": "application/json" }
});
}
}

// 5. 文件下载:通过 /{id} 访问(要求至少6字符)
const segments = pathname.split('/').filter(Boolean);
if (segments.length === 1 && segments[0].length >= 4) {
const id = segments[0];
const reservedPaths = new Set([
'api', 'upload', 'f', 'favicon.ico', 'robots.txt', 'about', 's'
]);
if (!reservedPaths.has(id)) {
const entry = await env.TEMP_STORE.getWithMetadata(id, "arrayBuffer");
if (entry.value) {
return new Response(entry.value, {
headers: {
"Content-Type": entry.metadata?.contentType || "application/octet-stream",
"Content-Disposition": "attachment; filename=\"" +
encodeURIComponent(entry.metadata?.filename || 'file') + "\"",
"Cache-Control": "no-store"
}
});
}
}
}

// 6. 404
return new Response("Not Found", { status: 404 });
}
};
  1. 点击右上角 「Save and Deploy」

第三步:绑定 KV 命名空间到 Worker

目标:让 Worker 能读写你刚创建的 TEMP_STORE。
操作路径:
在 Worker 编辑页面 → 顶部标签栏选择 「绑定」
操作步骤:

  1. 点击 「添加绑定」 → 选择 「KV 命名空间」
  2. 弹窗中填写:
    变量名称(Variable name): TEMP_STORE ← 必须与代码中 env.TEMP_STORE 一致
    KV 命名空间(KV namespace): 选择你刚创建的 TEMP_STORE
  3. 点击 「添加」
    此时无需 Secret,因为服务是公开上传。

第四步:绑定自定义域名路由

前提:你的域名(如 tmp.yourdomain.com)已在 Cloudflare DNS 托管,且状态为 Proxied(橙色云图标)。
操作路径:
在 Worker 详情页 → 顶部标签栏选择 「设置」 → 滚动到 「Routes」 区域
操作步骤:

  1. 点击 「Add Route」
  2. 输入:
    Route: tmp.yourdomain.com/
  3. 点击 「保存」
    📌 注意:
    必须带 /,否则根路径 / 无法匹配
    域名必须已在 Cloudflare DNS 中,且代理开启(橙色云)

第五步:验证功能

测试项 操作 预期结果


首页访问 浏览器打开 https://tmp.yourdomain.com 显示文件上传页面
上传文件 选择 ≤25MB 文件点击上传 返回短链接,如 https://tmp.yourdomain.com/abcd
下载文件 访问该短链接 浏览器自动下载,保留原始文件名
过期测试 12 小时后再次访问 返回 404 Not Found
API 测试(可选):

1
2
curl -X POST https://tmp.yourdomain.com/api/upload-public \
-F "[email protected]"

成功响应:

1
{"downloadUrl":"https://tmp.yourdomain.com/abcd"}

注意事项 & 最佳实践

  1. ID 长度与容量
    当前使用 4 位 ID(如 abcd),安全上限:≈1,600 文件 / 12 小时
    若需更高容量,改为 5 位:
1
return Math.random().toString(36).substring(2, 7); // 5字符

并将路由判断改为 segments[0].length >= 5
2. 文件限制
单文件 ≤ 25 MB(Cloudflare Workers 限制)
自动 12 小时过期(通过 expirationTtl: 43200 实现)
3. 路径冲突防护
已预留以下路径,不会被当作文件 ID:

1
2
3
const reservedPaths = new Set([
'api', 'upload', 'f', 'favicon.ico', 'robots.txt', 'about'
]);
  1. HTTPS 与安全性
    Cloudflare 自动提供 HTTPS,无需配置证书
    上传接口为公开,如需鉴权可参考短链接服务增加 API_TOKEN

至此,你的临时文件存储服务已上线!链接简洁、自动清理、保留文件名,适合分享日志、截图、临时文档等场景。