Files
CloudFlare-ImgBed/functions/file/[[path]].js
2026-01-23 20:50:26 +08:00

828 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { fetchSecurityConfig } from "../utils/sysConfig";
import { TelegramAPI } from "../utils/telegramAPI";
import { DiscordAPI } from "../utils/discordAPI";
import { HuggingFaceAPI } from "../utils/huggingfaceAPI";
import {
setCommonHeaders, setRangeHeaders, handleHeadRequest, getFileContent, isTgChannel,
returnWithCheck, return404, isDomainAllowed
} from './fileTools';
import { getDatabase } from '../utils/databaseAdapter.js';
export async function onRequest(context) { // Contents of context object
const {
request, // same as existing Worker API
env, // same as existing Worker API
params, // if filename includes [id] or [[path]]
waitUntil, // same as ctx.waitUntil in existing Worker API
next, // used for middleware or to fetch assets
data, // arbitrary space for passing data between middlewares
} = context;
// 解码文件ID
let fileId = '';
try {
params.path = decodeURIComponent(params.path);
fileId = params.path.split(',').join('/');
} catch (e) {
return new Response('Error: Decode Image ID Failed', { status: 400 });
}
// 读取安全配置,解析必要参数
const securityConfig = await fetchSecurityConfig(env);
context.securityConfig = securityConfig;
const url = new URL(request.url);
context.url = url;
const Referer = request.headers.get('Referer')
context.Referer = Referer;
// 检查引用域名是否被允许
if (!isDomainAllowed(context)) {
return await returnBlockImg(url);
}
// 从数据库中获取图片记录
const db = getDatabase(env);
const imgRecord = await db.getWithMetadata(fileId);
if (!imgRecord) {
return new Response('Error: Image Not Found', { status: 404 });
}
// 如果metadata不存在只可能是之前未设置KV且存储在Telegraph上的图片
if (!imgRecord.metadata) {
imgRecord.metadata = {};
}
const fileName = imgRecord.metadata?.FileName || fileId;
const encodedFileName = encodeURIComponent(fileName);
const fileType = imgRecord.metadata?.FileType || null;
// 检查文件可访问状态
let accessRes = await returnWithCheck(context, imgRecord);
if (accessRes.status !== 200) {
return accessRes; // 如果不可访问,直接返回
}
/* Cloudflare R2渠道 */
if (imgRecord.metadata?.Channel === 'CloudflareR2') {
return await handleR2File(context, fileId, encodedFileName, fileType);
}
/* S3渠道 */
if (imgRecord.metadata?.Channel === "S3") {
return await handleS3File(context, imgRecord.metadata, encodedFileName, fileType);
}
/* Discord 渠道 */
if (imgRecord.metadata?.Channel === 'Discord') {
// 检查是否为分片文件
if (imgRecord.metadata?.IsChunked === true) {
return await handleDiscordChunkedFile(context, imgRecord, encodedFileName, fileType);
}
return await handleDiscordFile(context, imgRecord.metadata, encodedFileName, fileType);
}
/* HuggingFace 渠道 */
if (imgRecord.metadata?.Channel === 'HuggingFace') {
return await handleHuggingFaceFile(context, imgRecord.metadata, encodedFileName, fileType);
}
/* 外链渠道 */
if (imgRecord.metadata?.Channel === 'External') {
// 直接重定向到外链
return Response.redirect(imgRecord.metadata?.ExternalLink, 302);
}
/* Telegram及Telegraph渠道 */
// 构建目标 URL
let targetUrl = '';
if (isTgChannel(imgRecord)) {
let TgFileID = ''; // Tg的file_id
if (imgRecord.metadata?.Channel === 'Telegram') {
TgFileID = fileId.split('.')[0]; // id为file_id + ext
} else if (imgRecord.metadata?.Channel === 'TelegramNew') {
// 检查是否为分片文件
if (imgRecord.metadata?.IsChunked === true) {
return await handleTelegramChunkedFile(context, imgRecord, encodedFileName, fileType);
}
TgFileID = imgRecord.metadata?.TgFileId;
if (TgFileID === null) {
return new Response('Error: Failed to fetch image', { status: 500 });
}
}
// 获取TG图片真实地址支持代理域名
const TgBotToken = imgRecord.metadata?.TgBotToken || env.TG_BOT_TOKEN;
const TgProxyUrl = imgRecord.metadata?.TgProxyUrl || '';
const tgApi = new TelegramAPI(TgBotToken, TgProxyUrl);
const filePath = await tgApi.getFilePath(TgFileID);
if (filePath === null) {
return new Response('Error: Failed to fetch image path', { status: 500 });
}
// 使用代理域名或官方域名
const fileDomain = TgProxyUrl ? `https://${TgProxyUrl}` : 'https://api.telegram.org';
targetUrl = `${fileDomain}/file/bot${TgBotToken}/${filePath}`;
} else {
targetUrl = 'https://telegra.ph/' + url.pathname + url.search;
}
try {
const response = await getFileContent(request, targetUrl);
if (response === null) {
return new Response('Error: Failed to fetch image', { status: 500 });
} else if (response.status === 404) {
return await return404(url);
}
const headers = new Headers(response.headers);
setCommonHeaders(headers, encodedFileName, fileType, Referer, url);
const newRes = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
return newRes;
} catch (error) {
return new Response('Error: ' + error, { status: 500 });
}
}
// 处理 Telegram 渠道分片文件读取
async function handleTelegramChunkedFile(context, imgRecord, encodedFileName, fileType) {
const { env, request, url, Referer } = context;
const metadata = imgRecord.metadata;
const TgBotToken = metadata.TgBotToken || env.TG_BOT_TOKEN;
const TgProxyUrl = metadata.TgProxyUrl || '';
// 从KV的value中读取分片信息
let chunks = [];
try {
if (imgRecord.value) {
chunks = JSON.parse(imgRecord.value);
// 确保分片按索引排序
chunks.sort((a, b) => a.index - b.index);
}
} catch (parseError) {
console.error('Failed to parse chunks data:', parseError);
return new Response('Error: Invalid chunks data', { status: 500 });
}
if (chunks.length === 0) {
return new Response('Error: No chunks found for this file', { status: 500 });
}
// 验证分片完整性
const expectedChunks = metadata.TotalChunks || chunks.length;
if (chunks.length !== expectedChunks) {
return new Response(`Error: Missing chunks, expected ${expectedChunks}, got ${chunks.length}`, { status: 500 });
}
// 计算文件总大小
const totalSize = chunks.reduce((total, chunk) => total + (chunk.size || 0), 0);
// 构建响应头
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, Referer, url);
headers.set('Content-Length', totalSize.toString());
// 添加ETag支持
const etag = `"${metadata.TimeStamp || Date.now()}-${totalSize}"`;
headers.set('ETag', etag);
// 检查If-None-Match头304缓存
const ifNoneMatch = request.headers.get('If-None-Match');
if (ifNoneMatch && ifNoneMatch === etag) {
return new Response(null, {
status: 304,
headers: {
'ETag': etag,
'Cache-Control': headers.get('Cache-Control'),
'Accept-Ranges': 'bytes'
}
});
}
// 检查Range请求头
const range = request.headers.get('Range');
let rangeStart = 0;
let rangeEnd = totalSize - 1;
let isRangeRequest = false;
if (range) {
const matches = range.match(/bytes=(\d+)-(\d*)/);
if (matches) {
rangeStart = parseInt(matches[1]);
rangeEnd = matches[2] ? parseInt(matches[2]) : totalSize - 1;
isRangeRequest = true;
// 验证范围有效性
if (rangeStart >= totalSize || rangeEnd >= totalSize || rangeStart > rangeEnd) {
return new Response('Range Not Satisfiable', { status: 416 });
}
}
}
// 处理HEAD请求
if (request.method === 'HEAD') {
return handleHeadRequest(headers, etag);
}
try {
// 创建支持Range请求的流
const stream = new ReadableStream({
async start(controller) {
try {
let currentPosition = 0;
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const chunkSize = chunk.size || 0;
// 如果当前分片完全在请求范围之前,跳过
if (currentPosition + chunkSize <= rangeStart) {
currentPosition += chunkSize;
continue;
}
// 如果当前分片完全在请求范围之后,结束
if (currentPosition > rangeEnd) {
break;
}
// 获取分片数据(支持代理域名)
const chunkData = await fetchTelegramChunkWithRetry(TgBotToken, chunk, TgProxyUrl, 3);
if (!chunkData) {
throw new Error(`Failed to fetch chunk ${chunk.index} after retries`);
}
// 计算在当前分片中的起始和结束位置
const chunkStart = Math.max(0, rangeStart - currentPosition);
const chunkEnd = Math.min(chunkSize, rangeEnd - currentPosition + 1);
// 如果需要部分分片数据
if (chunkStart > 0 || chunkEnd < chunkSize) {
const partialData = chunkData.slice(chunkStart, chunkEnd);
controller.enqueue(partialData);
} else {
controller.enqueue(chunkData);
}
currentPosition += chunkSize;
}
controller.close();
} catch (error) {
controller.error(error);
}
}
});
// 设置Range相关头部
if (isRangeRequest) {
setRangeHeaders(headers, rangeStart, rangeEnd, totalSize);
return new Response(stream, {
status: 206, // Partial Content
headers,
});
} else {
headers.set('Cache-Control', 'private, max-age=86400'); // CDN 不缓存完整文件,避免 CDN 不支持 Range 请求
return new Response(stream, {
status: 200,
headers,
});
}
} catch (error) {
return new Response(`Error: Failed to reconstruct chunked file - ${error.message}`, { status: 500 });
}
}
// 带重试机制的Telegram分片获取函数支持代理域名
async function fetchTelegramChunkWithRetry(botToken, chunk, proxyUrl = '', maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const tgApi = new TelegramAPI(botToken, proxyUrl);
const response = await tgApi.getFileContent(chunk.fileId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// 验证分片大小是否匹配
const chunkData = await response.arrayBuffer();
const actualSize = chunkData.byteLength;
// 如果有期望大小且不匹配,抛出错误
if (chunk.size && actualSize !== chunk.size) {
console.warn(`Chunk ${chunk.index} size mismatch: expected ${chunk.size}, got ${actualSize}`);
}
return new Uint8Array(chunkData);
} catch (error) {
console.warn(`Chunk ${chunk.index} fetch attempt ${attempt + 1} failed:`, error.message);
if (attempt === maxRetries - 1) {
return null; // 最后一次尝试也失败了
}
// 重试前等待一段时间
await new Promise(resolve => setTimeout(resolve, 500 * (attempt + 1)));
}
}
return null;
}
// 处理 Discord 渠道分片文件读取
async function handleDiscordChunkedFile(context, imgRecord, encodedFileName, fileType) {
const { request, url, Referer } = context;
const metadata = imgRecord.metadata;
const botToken = metadata.DiscordBotToken;
const proxyUrl = metadata.DiscordProxyUrl;
// 从KV的value中读取分片信息
let chunks = [];
try {
if (imgRecord.value) {
chunks = JSON.parse(imgRecord.value);
// 确保分片按索引排序
chunks.sort((a, b) => a.index - b.index);
}
} catch (parseError) {
console.error('Failed to parse Discord chunks data:', parseError);
return new Response('Error: Invalid chunks data', { status: 500 });
}
if (chunks.length === 0) {
return new Response('Error: No chunks found for this file', { status: 500 });
}
// 验证分片完整性
const expectedChunks = metadata.TotalChunks || chunks.length;
if (chunks.length !== expectedChunks) {
return new Response(`Error: Missing chunks, expected ${expectedChunks}, got ${chunks.length}`, { status: 500 });
}
// 计算文件总大小
const totalSize = chunks.reduce((total, chunk) => total + (chunk.size || 0), 0);
// 构建响应头
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, Referer, url);
headers.set('Content-Length', totalSize.toString());
// 添加ETag支持
const etag = `"${metadata.TimeStamp || Date.now()}-${totalSize}"`;
headers.set('ETag', etag);
// 检查If-None-Match头304缓存
const ifNoneMatch = request.headers.get('If-None-Match');
if (ifNoneMatch && ifNoneMatch === etag) {
return new Response(null, {
status: 304,
headers: {
'ETag': etag,
'Cache-Control': headers.get('Cache-Control'),
'Accept-Ranges': 'bytes'
}
});
}
// 检查Range请求头
const range = request.headers.get('Range');
let rangeStart = 0;
let rangeEnd = totalSize - 1;
let isRangeRequest = false;
if (range) {
const matches = range.match(/bytes=(\d+)-(\d*)/);
if (matches) {
rangeStart = parseInt(matches[1]);
rangeEnd = matches[2] ? parseInt(matches[2]) : totalSize - 1;
isRangeRequest = true;
// 验证范围有效性
if (rangeStart >= totalSize || rangeEnd >= totalSize || rangeStart > rangeEnd) {
return new Response('Range Not Satisfiable', { status: 416 });
}
}
}
// 处理HEAD请求
if (request.method === 'HEAD') {
return handleHeadRequest(headers, etag);
}
try {
// 创建支持Range请求的流
const stream = new ReadableStream({
async start(controller) {
try {
let currentPosition = 0;
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const chunkSize = chunk.size || 0;
// 如果当前分片完全在请求范围之前,跳过
if (currentPosition + chunkSize <= rangeStart) {
currentPosition += chunkSize;
continue;
}
// 如果当前分片完全在请求范围之后,结束
if (currentPosition > rangeEnd) {
break;
}
// 获取分片数据(每次通过 API 获取新的附件 URL
const chunkData = await fetchDiscordChunkWithRetry(botToken, metadata.DiscordChannelId, chunk, proxyUrl, 3);
if (!chunkData) {
throw new Error(`Failed to fetch Discord chunk ${chunk.index} after retries`);
}
// 计算在当前分片中的起始和结束位置
const chunkStart = Math.max(0, rangeStart - currentPosition);
const chunkEnd = Math.min(chunkSize, rangeEnd - currentPosition + 1);
// 如果需要部分分片数据
if (chunkStart > 0 || chunkEnd < chunkSize) {
const partialData = chunkData.slice(chunkStart, chunkEnd);
controller.enqueue(partialData);
} else {
controller.enqueue(chunkData);
}
currentPosition += chunkSize;
}
controller.close();
} catch (error) {
controller.error(error);
}
}
});
// 设置Range相关头部
if (isRangeRequest) {
setRangeHeaders(headers, rangeStart, rangeEnd, totalSize);
return new Response(stream, {
status: 206, // Partial Content
headers,
});
} else {
headers.set('Cache-Control', 'private, max-age=86400');
return new Response(stream, {
status: 200,
headers,
});
}
} catch (error) {
return new Response(`Error: Failed to reconstruct Discord chunked file - ${error.message}`, { status: 500 });
}
}
// 带重试机制的Discord分片获取函数每次通过 API 获取新的附件 URL
async function fetchDiscordChunkWithRetry(botToken, channelId, chunk, proxyUrl, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// 通过 Discord API 获取新的附件 URL因为 URL 会在约24小时后过期
const discordAPI = new DiscordAPI(botToken);
let fileUrl = await discordAPI.getFileURL(channelId, chunk.messageId);
if (!fileUrl) {
throw new Error('Failed to get attachment URL from Discord API');
}
// 如果配置了代理 URL替换 Discord CDN 域名
if (proxyUrl) {
fileUrl = fileUrl.replace('https://cdn.discordapp.com', `https://${proxyUrl}`);
}
const response = await fetch(fileUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// 验证分片大小是否匹配
const chunkData = await response.arrayBuffer();
const actualSize = chunkData.byteLength;
// 如果有期望大小且不匹配,记录警告
if (chunk.size && actualSize !== chunk.size) {
console.warn(`Discord chunk ${chunk.index} size mismatch: expected ${chunk.size}, got ${actualSize}`);
}
return new Uint8Array(chunkData);
} catch (error) {
console.warn(`Discord chunk ${chunk.index} fetch attempt ${attempt + 1} failed:`, error.message);
if (attempt === maxRetries - 1) {
return null; // 最后一次尝试也失败了
}
// 重试前等待一段时间
await new Promise(resolve => setTimeout(resolve, 500 * (attempt + 1)));
}
}
return null;
}
// 处理R2文件读取
async function handleR2File(context, fileId, encodedFileName, fileType) {
const { env, request, url, Referer } = context;
try {
// 检查是否配置了R2
if (typeof env.img_r2 == "undefined" || env.img_r2 == null || env.img_r2 == "") {
return new Response('Error: Please configure R2 database', { status: 500 });
}
const R2DataBase = env.img_r2;
// 检查Range请求头
const range = request.headers.get('Range');
let object;
if (range) {
// 处理Range请求
const matches = range.match(/bytes=(\d+)-(\d*)/);
if (matches) {
const start = parseInt(matches[1]);
const end = matches[2] ? parseInt(matches[2]) : undefined;
const rangeOptions = {
range: {
offset: start
}
};
if (end !== undefined) {
rangeOptions.range.length = end - start + 1;
}
object = await R2DataBase.get(fileId, rangeOptions);
} else {
object = await R2DataBase.get(fileId);
}
} else {
object = await R2DataBase.get(fileId);
}
if (object === null) {
return new Response('Error: Failed to fetch file', { status: 500 });
}
const headers = new Headers();
object.writeHttpMetadata(headers);
setCommonHeaders(headers, encodedFileName, fileType, Referer, url);
// 处理HEAD请求
if (request.method === 'HEAD') {
return handleHeadRequest(headers);
}
// 如果是Range请求设置相应的状态码和头
if (range && object.range) {
headers.set('Content-Range', `bytes ${object.range.offset}-${object.range.offset + object.range.length - 1}/${object.size}`);
headers.set('Content-Length', object.range.length.toString());
return new Response(object.body, {
status: 206, // Partial Content
headers,
});
}
// 正常请求
return new Response(object.body, {
status: 200,
headers,
});
} catch (error) {
return new Response(`Error: Failed to fetch from R2 - ${error.message}`, { status: 500 });
}
}
// 处理S3文件读取
async function handleS3File(context, metadata, encodedFileName, fileType) {
const { Referer, url, request } = context;
const s3Client = new S3Client({
region: metadata?.S3Region || "auto",
endpoint: metadata?.S3Endpoint,
credentials: {
accessKeyId: metadata?.S3AccessKeyId,
secretAccessKey: metadata?.S3SecretAccessKey
},
forcePathStyle: metadata?.S3PathStyle || false
});
const bucketName = metadata?.S3BucketName;
const key = metadata?.S3FileKey;
try {
// 检查Range请求头
const range = request.headers.get('Range');
const commandParams = {
Bucket: bucketName,
Key: key
};
if (range) {
// 添加Range参数用于部分内容请求
commandParams.Range = range;
}
const command = new GetObjectCommand(commandParams);
const response = await s3Client.send(command);
// 设置响应头
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, Referer, url);
// 设置Content-Length和Content-Range头
if (response.ContentLength) {
headers.set('Content-Length', response.ContentLength.toString());
}
if (response.ContentRange) {
headers.set('Content-Range', response.ContentRange);
}
// 处理HEAD请求
if (request.method === 'HEAD') {
return handleHeadRequest(headers);
}
// 返回响应,支持流式传输
const statusCode = range ? 206 : 200; // Range请求返回206 Partial Content
return new Response(response.Body, {
status: statusCode,
headers
});
} catch (error) {
return new Response(`Error: Failed to fetch from S3 - ${error.message}`, { status: 500 });
}
}
// 处理 Discord 文件读取
async function handleDiscordFile(context, metadata, encodedFileName, fileType) {
const { env, request, url, Referer } = context;
try {
// 每次读取都通过 API 获取新的附件 URL因为 Discord 附件 URL 会在约24小时后过期
let fileUrl = null;
if (metadata.DiscordMessageId && metadata.DiscordChannelId && metadata.DiscordBotToken) {
const discordAPI = new DiscordAPI(metadata.DiscordBotToken);
fileUrl = await discordAPI.getFileURL(metadata.DiscordChannelId, metadata.DiscordMessageId);
}
if (!fileUrl) {
return new Response('Error: Discord file URL not found', { status: 500 });
}
// 如果配置了代理 URL替换 Discord CDN 域名
if (metadata.DiscordProxyUrl) {
fileUrl = fileUrl.replace('https://cdn.discordapp.com', `https://${metadata.DiscordProxyUrl}`);
}
// 处理 HEAD 请求
if (request.method === 'HEAD') {
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, Referer, url);
return handleHeadRequest(headers);
}
// 获取文件内容(支持 Range 请求)
const fetchHeaders = {};
const range = request.headers.get('Range');
if (range) {
fetchHeaders['Range'] = range;
}
const response = await fetch(fileUrl, {
method: 'GET',
headers: fetchHeaders
});
if (!response.ok && response.status !== 206) {
return new Response(`Error: Failed to fetch from Discord - ${response.status}`, { status: response.status });
}
// 构建响应头
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, Referer, url);
// 复制相关头部
if (response.headers.get('Content-Length')) {
headers.set('Content-Length', response.headers.get('Content-Length'));
}
if (response.headers.get('Content-Range')) {
headers.set('Content-Range', response.headers.get('Content-Range'));
}
return new Response(response.body, {
status: response.status,
headers
});
} catch (error) {
return new Response(`Error: Failed to fetch from Discord - ${error.message}`, { status: 500 });
}
}
// 处理 HuggingFace 文件读取
async function handleHuggingFaceFile(context, metadata, encodedFileName, fileType) {
const { request, url, Referer } = context;
try {
const hfRepo = metadata.HfRepo;
const hfFilePath = metadata.HfFilePath;
const hfToken = metadata.HfToken;
const hfIsPrivate = metadata.HfIsPrivate || false;
if (!hfRepo || !hfFilePath) {
return new Response('Error: HuggingFace file info not found', { status: 500 });
}
// 构建文件 URL
const fileUrl = metadata.HfFileUrl || `https://huggingface.co/datasets/${hfRepo}/resolve/main/${hfFilePath}`;
// 处理 HEAD 请求
if (request.method === 'HEAD') {
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, Referer, url);
return handleHeadRequest(headers);
}
// 构建请求头
const fetchHeaders = {};
// 私有仓库需要 Authorization
if (hfIsPrivate && hfToken) {
fetchHeaders['Authorization'] = `Bearer ${hfToken}`;
}
// 支持 Range 请求
const range = request.headers.get('Range');
if (range) {
fetchHeaders['Range'] = range;
}
const response = await fetch(fileUrl, {
method: 'GET',
headers: fetchHeaders
});
if (!response.ok && response.status !== 206) {
return new Response(`Error: Failed to fetch from HuggingFace - ${response.status}`, { status: response.status });
}
// 构建响应头
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, Referer, url);
// 复制相关头部
if (response.headers.get('Content-Length')) {
headers.set('Content-Length', response.headers.get('Content-Length'));
}
if (response.headers.get('Content-Range')) {
headers.set('Content-Range', response.headers.get('Content-Range'));
}
return new Response(response.body, {
status: response.status,
headers
});
} catch (error) {
return new Response(`Error: Failed to fetch from HuggingFace - ${error.message}`, { status: 500 });
}
}