export default {
async fetch(request, env) {
const { pathname } = new URL(request.url);
const domain = env.DOMAIN;
const DATABASE = env.DATABASE;
const USERNAME = env.USERNAME;
const PASSWORD = env.PASSWORD;
const adminPath = env.ADMIN_PATH;
const enableAuth = env.ENABLE_AUTH === 'true';
const R2BUCKET = env.R2BUCKET;
const maxSizeMB = env.MAXSIZEMB ? parseInt(env.MAXSIZEMB, 10) : 10;
const maxSize = maxSizeMB 1024 1024;
switch (pathname) {
case '/':
return await handleRootRequest(request, USERNAME, PASSWORD, enableAuth);
case /${adminPath}:
return await handleAdminRequest(DATABASE, request, USERNAME, PASSWORD);
case '/upload':
return request.method === 'POST' ? await handleUploadRequest(request, DATABASE, enableAuth, USERNAME, PASSWORD, domain, R2_BUCKET, maxSize) : new Response('Method Not Allowed', { status: 405 });
case '/bing-images':
return handleBingImagesRequest();
case '/delete-images':
return await handleDeleteImagesRequest(request, DATABASE, USERNAME, PASSWORD, R2_BUCKET);
default:
return await handleImageRequest(request, DATABASE, R2_BUCKET);
}
}
};
function authenticate(request, USERNAME, PASSWORD) {
const authHeader = request.headers.get('Authorization');
if (!authHeader) return false;
return isValidCredentials(authHeader, USERNAME, PASSWORD);
}
async function handleRootRequest(request, USERNAME, PASSWORD, enableAuth) {
const cache = caches.default;
const cacheKey = new Request(request.url);
if (enableAuth) {
if (!authenticate(request, USERNAME, PASSWORD)) {
return new Response('Unauthorized', { status: 401, headers: { 'WWW-Authenticate': 'Basic realm="Admin"' } });
}
}
const cachedResponse = await cache.match(cacheKey);
if (cachedResponse) {
return cachedResponse;
}
const response = new Response(`
开源GitHub |
载入天数... | 总访问量 次`, { headers: { 'Content-Type': 'text/html;charset=UTF-8' } });
await cache.put(cacheKey, response.clone());
return response;
}
async function handleAdminRequest(DATABASE, request, USERNAME, PASSWORD) {
if (!authenticate(request, USERNAME, PASSWORD)) {
return new Response('Unauthorized', { status: 401, headers: { 'WWW-Authenticate': 'Basic realm="Admin"' } });
}
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') || '1', 10);
return await generateAdminPage(DATABASE, page);
}
function isValidCredentials(authHeader, USERNAME, PASSWORD) {
const base64Credentials = authHeader.split(' ')[1];
const credentials = atob(base64Credentials).split(':');
const username = credentials[0];
const password = credentials[1];
return username === USERNAME && password === PASSWORD;
}
function generatePaginationButtons(currentPage, totalPages) {
if (totalPages === 0) return '';
const pages = Array.from({ length: totalPages }, (_, i) => i + 1)
.filter(pageNum => {
return pageNum === 1 ||
pageNum === totalPages ||
(pageNum >= currentPage - 2 && pageNum <= currentPage + 2);
});
let buttons = '';
for (let i = 0; i < pages.length; i++) {
const pageNum = pages[i];
if (i > 0 && pageNum - pages[i - 1] > 1) {
buttons += '...';
}
const activeClass = pageNum === currentPage ? 'active' : '';
buttons += ;
}
return buttons;
}
async function generateAdminPage(DATABASE, page = 1) {
const itemsPerPage = 28;
const allMediaData = await fetchMediaData(DATABASE);
const totalItems = allMediaData.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
const currentPage = Math.max(1, Math.min(page, totalPages));
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const mediaData = allMediaData.slice(startIndex, endIndex);
const mediaHtml = mediaData.map(({ url }) => {
const fileExtension = url.split('.').pop().toLowerCase();
const timestamp = url.split('/').pop().split('.')[0];
const mediaType = fileExtension;
let displayUrl = url;
const supportedImageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'svg'];
const supportedVideoExtensions = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm'];
const isSupported = [...supportedImageExtensions, ...supportedVideoExtensions].includes(fileExtension);
const backgroundStyle = isSupported ? '' : style="font-size: 50px; display: flex; justify-content: center; align-items: center;";
const icon = isSupported ? '' : '📁';
return `
${supportedVideoExtensions.includes(fileExtension) ? `
:
${isSupported ? : icon}
`}
`;
}).join('');
const html = `
${mediaHtml}
${generatePaginationButtons(currentPage, totalPages)}
第 ${currentPage} / ${totalPages} 页,共 ${totalItems} 个文件 `;
return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
}
async function fetchMediaData(DATABASE) {
const result = await DATABASE.prepare('SELECT url FROM media').all();
const mediaData = result.results.map(row => {
const timestamp = parseInt(row.url.split('/').pop().split('.')[0]);
return { url: row.url, timestamp: timestamp };
});
mediaData.sort((a, b) => b.timestamp - a.timestamp);
return mediaData.map(({ url }) => ({ url }));
}
async function handleUploadRequest(request, DATABASE, enableAuth, USERNAME, PASSWORD, domain, R2_BUCKET, maxSize) {
try {
const formData = await request.formData();
const file = formData.get('file');
if (!file) throw new Error('缺少文件');
if (file.size > maxSize) {
return new Response(JSON.stringify({ error: 文件大小超过${maxSize / (1024 * 1024)}MB限制 }), { status: 413, headers: { 'Content-Type': 'application/json' } });
}
if (enableAuth && !authenticate(request, USERNAME, PASSWORD)) {
return new Response('Unauthorized', { status: 401, headers: { 'WWW-Authenticate': 'Basic realm="Admin"' } });
}
const r2Key = ${Date.now()};
await R2_BUCKET.put(r2Key, file.stream(), {
httpMetadata: { contentType: file.type }
});
const fileExtension = file.name.split('.').pop();
const imageURL = https://${domain}/${r2Key}.${fileExtension};
await DATABASE.prepare('INSERT INTO media (url) VALUES (?) ON CONFLICT(url) DO NOTHING').bind(imageURL).run();
return new Response(JSON.stringify({ data: imageURL }), { status: 200, headers: { 'Content-Type': 'application/json' } });
} catch (error) {
console.error('R2 上传错误:', error);
return new Response(JSON.stringify({ error: error.message }), { status: 500, headers: { 'Content-Type': 'application/json' } });
}
}
async function handleImageRequest(request, DATABASE, R2_BUCKET) {
const requestedUrl = request.url;
const cache = caches.default;
const cacheKey = new Request(requestedUrl);
const cachedResponse = await cache.match(cacheKey);
if (cachedResponse) return cachedResponse;
const result = await DATABASE.prepare('SELECT url FROM media WHERE url = ?').bind(requestedUrl).first();
if (!result) {
const notFoundResponse = new Response('资源不存在', { status: 404 });
await cache.put(cacheKey, notFoundResponse.clone());
return notFoundResponse;
}
const urlParts = requestedUrl.split('/');
const fileName = urlParts[urlParts.length - 1];
const [r2Key, fileExtension] = fileName.split('.');
const object = await R2_BUCKET.get(r2Key);
if (!object) {
return new Response('获取文件内容失败', { status: 404 });
}
let contentType = 'text/plain';
if (fileExtension === 'jpg' || fileExtension === 'jpeg') contentType = 'image/jpeg';
if (fileExtension === 'png') contentType = 'image/png';
if (fileExtension === 'gif') contentType = 'image/gif';
if (fileExtension === 'webp') contentType = 'image/webp';
if (fileExtension === 'mp4') contentType = 'video/mp4';
const headers = new Headers();
headers.set('Content-Type', contentType);
headers.set('Content-Disposition', 'inline');
const responseToCache = new Response(object.body, { status: 200, headers });
await cache.put(cacheKey, responseToCache.clone());
return responseToCache;
}
async function handleBingImagesRequest(request) {
const cache = caches.default;
const cacheKey = new Request('https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=5');
const cachedResponse = await cache.match(cacheKey);
if (cachedResponse) return cachedResponse;
const res = await fetch(cacheKey);
if (!res.ok) {
return new Response('请求 Bing API 失败', { status: res.status });
}
const bingData = await res.json();
const images = bingData.images.map(image => ({ url: https://cn.bing.com${image.url} }));
const returnData = { status: true, message: "操作成功", data: images };
const response = new Response(JSON.stringify(returnData), { status: 200, headers: { 'Content-Type': 'application/json' } });
await cache.put(cacheKey, response.clone());
return response;
}
async function handleDeleteImagesRequest(request, DATABASE, USERNAME, PASSWORD, R2_BUCKET) {
if (!authenticate(request, USERNAME, PASSWORD)) {
return new Response('Unauthorized', { status: 401, headers: { 'WWW-Authenticate': 'Basic realm="Admin"' } });
}
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
try {
const keysToDelete = await request.json();
if (!Array.isArray(keysToDelete) || keysToDelete.length === 0) {
return new Response(JSON.stringify({ message: '没有要删除的项' }), { status: 400 });
}
const placeholders = keysToDelete.map(() => '?').join(',');
const result = await DATABASE.prepare(DELETE FROM media WHERE url IN (${placeholders})).bind(...keysToDelete).run();
if (result.changes === 0) {
return new Response(JSON.stringify({ message: '未找到要删除的项' }), { status: 404 });
}
const cache = caches.default;
for (const url of keysToDelete) {
const cacheKey = new Request(url);
const cachedResponse = await cache.match(cacheKey);
if (cachedResponse) {
await cache.delete(cacheKey);
}
const urlParts = url.split('/');
const fileName = urlParts[urlParts.length - 1];
const r2Key = fileName.split('.')[0];
await R2_BUCKET.delete(r2Key);
}
return new Response(JSON.stringify({ message: '删除成功' }), { status: 200 });
} catch (error) {
return new Response(JSON.stringify({ error: '删除失败', details: error.message }), { status: 500 });
}
}