const CUSTOM_CSS = `
*,:after,:before{margin:0;padding:0;box-sizing:border-box}
html{font-size:16px;scroll-behavior:smooth;-webkit-text-size-adjust:100%}
body{font-family:Noto Sans SC,Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;background:#F6F4EF;color:#2C2A26;line-height:1.75;letter-spacing:.02em;min-height:100vh;display:flex;flex-direction:column}
a{color:#A88F6F;text-decoration:none;border-bottom:none;transition:color .2s ease}
a:hover{color:#8A7655}
h1,h2,h3,h4,h5,h6{font-family:"Noto Serif SC",Playfair Display,Georgia,Songti SC,serif;font-weight:600;line-height:1.4;color:#2C2A26;letter-spacing:.02em}
h1{font-size:2rem; margin-bottom: 1rem;}
h2{font-size:1.5rem}
table{width:100%;border-collapse:collapse;margin:1rem 0;}
td,th{text-align:left;vertical-align:middle;padding:.75rem 1rem}
thead th{font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.1em;color:#8A867D;border-bottom:1px solid #e3ded5;background:transparent}
tbody tr:nth-child(2n){background:#FAFAF7}
tbody tr:hover{background:#EFECE5}
tbody tr td{border-bottom:1px solid #e3ded5}
code{font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:.875em;background:#EFECE5;padding:.125em .375em;border-radius:3px;color:#2C2A26}
textarea{width:100%;font-family:inherit;font-size:.875rem;padding:.625rem .875rem;border:1px solid #e3ded5;border-radius:6px;background:#FAFAF8;color:#2C2A26;outline:none;resize:vertical;}
textarea:focus{border-color:#A88F6F;box-shadow:0 0 0 3px rgba(168,143,111,.1);background:#fff}
hr{border:none;border-top:1px solid #e3ded5;margin:2rem 0}
.site-header{border-bottom:1px solid #e3ded5;position:sticky;top:0;background:rgba(246,244,239,.95);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);z-index:100}
.header-inner{max-width:72rem;margin:0 auto;padding:0 2rem;display:flex;align-items:center;justify-content:space-between;height:4rem}
.site-title{font-family:"Noto Serif SC",Playfair Display,Georgia,serif;font-weight:700;font-size:1.125rem;letter-spacing:.04em;color:#2C2A26}
.site-main{flex:1 1;max-width:72rem;margin:2rem auto;padding:0 2rem;width:100%}
.admin-page{max-width:26rem;margin:6rem auto;padding:2.5rem;border:1px solid #e3ded5;border-radius:4px;background:#FAFAF7;box-shadow: 0 4px 12px rgba(44,42,38,.03)}
.admin-page h2{text-align:center;margin-bottom:1.5rem}
.admin-page input[type=password]{width:100%;padding:.75rem 1rem;font-size:.9375rem;border:1px solid #e3ded5;border-radius:4px;background:#F6F4EF;color:#2C2A26;outline:none;transition:border-color .2s ease}
.admin-page input[type=password]:focus{border-color:#A88F6F}
.btn{display:inline-block;padding:.625rem 1.5rem;background:#A88F6F;color:#F6F4EF;border:1px solid #a88f6f;border-radius:4px;font-size:.8125rem;font-weight:500;letter-spacing:.04em;cursor:pointer;transition:all .2s ease}
.btn:hover{background:#F6F4EF;color:#A88F6F}
.btn-delete{font-size:.6875rem;font-weight:500;padding:.25rem .625rem;border:1px solid rgba(168,143,111,.3);border-radius:4px;background:transparent;color:#8A867D;cursor:pointer;transition:all .2s ease}
.btn-delete:hover{background:#A88F6F;color:#F6F4EF;border-color:#A88F6F}
.error{color:#c0392b;font-size:.8125rem;text-align:center;margin-top:.75rem}
.upload-zone{background:#FAFAF7;border:1px solid #e3ded5;border-radius:4px;padding:1.5rem;margin-top:2rem}
.file-input-wrapper{margin-bottom:1rem}
@media (max-width:768px){.header-inner,.site-main{padding:0 1.25rem}table{display:block;overflow-x:auto}.admin-page{margin:2rem 1rem;padding:1.5rem}}
`;
// 登录页面 HTML
const LOGIN_TEMPLATE = `
<html>
<head>
<title>R2 Storage - 验证</title>
<style>${CUSTOM_CSS}</style>
</head>
<body>
<div class="admin-page">
<h2>R2 File Manager</h2>
$ERROR$
<form action="/login" method="post">
<input type="password" name="password" placeholder="请输入安全访问密码" required autofocus>
<button type="submit" class="btn" style="width:100%; margin-top:1rem;">验证登录</button>
</form>
</div>
</body>
</html>`;
// 主页面框架 HTML
const HTML_TEMPLATE = `
<html>
<head>
<title>R2 Cloud Storage</title>
<style>${CUSTOM_CSS}</style>
</head>
<body>
<header class="site-header">
<div class="header-inner">
<div class="site-title">R2 File Manager</div>
<div class="site-nav"><a href="/logout" class="nav-link" style="font-size:0.8125rem;color:#8A867D;">退出登录</a></div>
</div>
</header>
<main class="site-main">
$content$
</main>
</body>
</html>`;
// 简易 Cookie 解析
function getCookie(request, name) {
const cookieString = request.headers.get('Cookie');
if (!cookieString) return null;
const cookies = cookieString.split(';').map(c => c.trim().split('='));
const cookie = cookies.find(c => c[0] === name);
return cookie ? decodeURIComponent(cookie[1]) : null;
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
const CORRECT_PASSWORD = env.AUTH_TOKEN || 'admin';
// 1. 处理登录
if (url.pathname === '/login' && request.method === 'POST') {
const formData = await request.formData();
const password = formData.get('password');
if (password === CORRECT_PASSWORD) {
return new Response('Redirecting...', {
status: 302,
headers: {
'Location': '/',
'Set-Cookie': `r2_session=${encodeURIComponent(CORRECT_PASSWORD)}; Path=/; Max-Age=86400; HttpOnly; SameSite=Strict`
}
});
} else {
const html = LOGIN_TEMPLATE.replace('$ERROR$', '<p class="error">密码验证失败,请重试</p>');
return new Response(html, { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
}
}
// 2. 处理登出
if (url.pathname === '/logout') {
return new Response('Redirecting...', {
status: 302,
headers: { 'Location': '/', 'Set-Cookie': 'r2_session=; Path=/; Max-Age=0; HttpOnly; SameSite=Strict' }
});
}
// 3. 鉴权检验
const session = getCookie(request, 'r2_session');
const isFileDownload = key && !key.startsWith('delete/') && request.method === 'GET';
if (!isFileDownload && session !== CORRECT_PASSWORD) {
const html = LOGIN_TEMPLATE.replace('$ERROR$', '');
return new Response(html, { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
}
// 4. 核心逻辑处理
switch (request.method) {
case 'GET': {
// 主页:文件列表展示
if (!key) {
const fileListObject = await env.MY_BUCKET.get('file-list.txt');
let fileRowsHtml = '<tr><td colspan="2" style="color:#8A867D; text-align:center; padding:2rem;">暂无上传的文件</td></tr>';
if (fileListObject !== null) {
const fileList = await fileListObject.text();
const files = fileList.split('\n').filter(Boolean);
if (files.length > 0) {
fileRowsHtml = files.map(file => {
return `<tr>
<td><a href="${url.origin}/${file}" target="_blank"><code>/${file}</code></a></td>
<td style="text-align:right;"><a href="${url.origin}/delete/${encodeURIComponent(file)}" class="btn-delete" onclick="return confirm('确定要删除此文件吗?')">删除</a></td>
</tr>`;
}).join('');
}
}
const content = `
<h1>文件管理</h1>
<table>
<thead>
<tr>
<th>文件存储路径 (R2 Key)</th>
<th style="text-align:right; width:100px;">操作</th>
</tr>
</thead>
<tbody>
${fileRowsHtml}
</tbody>
</table>
<div class="upload-zone">
<h3 style="margin-bottom:1rem;">上传新文件</h3>
<form action="/" method="post" enctype="multipart/form-data">
<div class="file-input-wrapper">
<input type="file" name="file" required>
</div>
<button type="submit" class="btn">开始上传</button>
</form>
<p style="font-size:12px;color:#8A867D;margin-top:0.5rem;">温馨提示:系统将自动将文件重命名为时间戳,并归档至当月目录中。</p>
</div>
`;
return new Response(HTML_TEMPLATE.replace('$content$', content), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
}
// 删除逻辑
if (key.startsWith('delete/')) {
const fileName = decodeURIComponent(key.slice(7));
await env.MY_BUCKET.delete(fileName);
// 更新索引记录
const fileListObject = await env.MY_BUCKET.get('file-list.txt');
let fileList = '';
if (fileListObject !== null) {
fileList = await fileListObject.text();
}
fileList = fileList.split('\n').filter(file => file !== fileName).join('\n');
await env.MY_BUCKET.put('file-list.txt', new TextEncoder().encode(fileList));
const content = `
<div style="text-align:center; padding: 3rem 0;">
<p style="margin-bottom:1.5rem;">文件 <code>${fileName}</code> 已成功移出存储桶。</p>
<button onclick="window.location.href='${url.origin}'" class="btn">返回工作台</button>
</div>`;
return new Response(HTML_TEMPLATE.replace('$content$', content), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
}
// 文件读取下载
const object = await env.MY_BUCKET.get(key);
if (object === null) {
return new Response('Object Not Found', { status: 404 });
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
return new Response(object.body, { headers });
}
case 'POST': {
// 上传与核心重命名逻辑
const formData = await request.formData();
const file = formData.get('file');
const originalName = file.name;
// 提取文件后缀名 (例如 .png)
const dotIndex = originalName.lastIndexOf('.');
const extension = dotIndex !== -1 ? originalName.slice(dotIndex) : '';
// 获取当前北京/东八区时间 (或者直接使用 UTC/系统本地时间戳)
const now = new Date();
const year = now.getFullYear();
// 补足两位月份,如 06
const month = String(now.getMonth() + 1).padStart(2, '0');
const timestamp = now.getTime(); // 毫秒级时间戳
// 拼接成新路径: 2026/06/1773123456789.png
const newFileName = `${year}/${month}/${timestamp}${extension}`;
// 保存文件到 R2
await env.MY_BUCKET.put(newFileName, file.stream());
// 写入索引记录
const fileListObject = await env.MY_BUCKET.get('file-list.txt');
let fileList = '';
if (fileListObject !== null) {
fileList = await fileListObject.text();
}
fileList += newFileName + '\n';
await env.MY_BUCKET.put('file-list.txt', new TextEncoder().encode(fileList));
const content = `
<div style="padding: 1rem 0;">
<h2 style="color:#A88F6F; margin-bottom:1rem;">🎉 文件上传成功!</h2>
<p>原文件名:<code>${originalName}</code></p>
<p>已重命名归档至:<code>${newFileName}</code></p>
<hr>
<p style="margin-bottom:0.5rem;">访问与直链地址:</p>
<textarea readonly rows="2">${url.origin}/${newFileName}</textarea>
<div style="margin-top:1.5rem;">
<a href="${url.origin}/${newFileName}" target="_blank" class="btn" style="margin-right:1rem; color:#F6F4EF;">预览文件</a>
<button onclick="window.location.href='${url.origin}'" class="btn" style="background:transparent; color:#A88F6F;">返回列表</button>
</div>
</div>`;
return new Response(HTML_TEMPLATE.replace('$content$', content), { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
}
default:
return new Response('Method not supported', { status: 405 });
}
},
};上一篇都市任务 #4