Files
CloudFlare-ImgBed/functions/file/[[path]].js
T

1100 lines
39 KiB
JavaScript

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { fetchSecurityConfig } from "../utils/sysConfig";
import { TelegramAPI } from "../utils/storage/telegramAPI";
import { DiscordAPI } from "../utils/storage/discordAPI";
import { HuggingFaceAPI } from "../utils/storage/huggingfaceAPI";
import { buildWebDAVUrl, WebDAVAPI } from "../utils/storage/webdavAPI";
import {
setCommonHeaders, setRangeHeaders, handleHeadRequest, getFileContent, isTgChannel,
returnWithCheck, return404, returnBlockImg, isDomainAllowed, FILE_CACHE_CONTROL
} from './fileTools';
import { getDatabase } from '../utils/databaseAdapter.js';
import { authenticate, AUTH_SCOPE } from '../utils/auth/authCore.js';
import {
resolveDiscordCredentials,
resolveHuggingFaceCredentials,
resolveS3Credentials,
resolveTelegramCredentials,
resolveWebDAVCredentials,
} from '../utils/metadata/channelCredentials.js';
import { buildCdnFileUrl } from '../utils/metadata/metadataView.js';
import {
parseImageTransform,
transformImageResponse,
validateImageTransformRequest,
} from './imageTransform.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;
context.imageTransform = parseImageTransform(url, securityConfig.access);
const imageTransformError = validateImageTransformRequest(request, context.imageTransform);
if (imageTransformError) {
return imageTransformError;
}
const Referer = request.headers.get('Referer')
context.Referer = Referer;
context.fileAccess = await buildFileAccessContext(context);
// 检查引用域名是否被允许
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') {
const response = await handleR2File(context, fileId, encodedFileName, fileType);
return await transformImageResponse(context, response);
}
/* S3渠道 */
if (imgRecord.metadata?.Channel === "S3") {
const response = await handleS3File(context, imgRecord.metadata, encodedFileName, fileType);
return await transformImageResponse(context, response);
}
/* Discord 渠道 */
if (imgRecord.metadata?.Channel === 'Discord') {
// 检查是否为分片文件
if (imgRecord.metadata?.IsChunked === true) {
const response = await handleDiscordChunkedFile(context, imgRecord, encodedFileName, fileType);
return await transformImageResponse(context, response);
}
const response = await handleDiscordFile(context, imgRecord.metadata, encodedFileName, fileType);
return await transformImageResponse(context, response);
}
/* HuggingFace 渠道 */
if (imgRecord.metadata?.Channel === 'HuggingFace') {
const response = await handleHuggingFaceFile(context, imgRecord.metadata, encodedFileName, fileType);
return await transformImageResponse(context, response);
}
/* WebDAV 渠道 */
if (imgRecord.metadata?.Channel === 'WebDAV') {
const response = await handleWebDAVFile(context, imgRecord.metadata, encodedFileName, fileType);
return await transformImageResponse(context, response);
}
/* 外链渠道 */
if (imgRecord.metadata?.Channel === 'External') {
if (!context.imageTransform.requested) {
// 未请求图片处理时维持原有的外链重定向逻辑
return Response.redirect(imgRecord.metadata?.ExternalLink, 302);
}
try {
const response = await fetch(imgRecord.metadata?.ExternalLink);
if (!response.ok) return response;
const headers = new Headers(response.headers);
setCommonHeaders(headers, encodedFileName, fileType, getFileCacheControl(context));
return await transformImageResponse(context, new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
}));
} catch (error) {
return new Response(`Error: Failed to fetch external image - ${error.message}`, { status: 500 });
}
}
/* 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) {
const response = await handleTelegramChunkedFile(context, imgRecord, encodedFileName, fileType);
return await transformImageResponse(context, response);
}
TgFileID = imgRecord.metadata?.TgFileId;
if (TgFileID === null) {
return new Response('Error: Failed to fetch image', { status: 500 });
}
}
// 获取TG图片真实地址(支持代理域名)
const tgCredentials = await resolveTelegramCredentials(db, env, imgRecord.metadata);
const TgBotToken = tgCredentials.botToken;
const TgProxyUrl = tgCredentials.proxyUrl || '';
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, getFileCacheControl(context));
const newRes = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
return await transformImageResponse(context, newRes);
} catch (error) {
return new Response('Error: ' + error, { status: 500 });
}
}
async function buildFileAccessContext(context) {
const { request, env, url } = context;
const fromAdmin = url.searchParams.get('from') === 'admin';
const fileAccess = {
isAdminPreview: fromAdmin,
adminAuthResult: { authorized: false, authType: null },
cacheControl: undefined,
};
if (fileAccess.isAdminPreview) {
fileAccess.adminAuthResult = await authenticate({
env,
request,
requiredPermission: 'manage',
authScope: AUTH_SCOPE.ADMIN,
});
}
return fileAccess;
}
function getFileCacheControl(context) {
return context.fileAccess?.cacheControl;
}
function getChunkedFileCacheControl(context) {
return getFileCacheControl(context) === FILE_CACHE_CONTROL.NO_STORE
? FILE_CACHE_CONTROL.NO_STORE
: FILE_CACHE_CONTROL.PRIVATE;
}
// 处理 Telegram 渠道分片文件读取
async function handleTelegramChunkedFile(context, imgRecord, encodedFileName, fileType) {
const { env, request, url, Referer } = context;
const metadata = imgRecord.metadata;
const db = getDatabase(env);
const tgCredentials = await resolveTelegramCredentials(db, env, metadata);
const TgBotToken = tgCredentials.botToken;
const TgProxyUrl = tgCredentials.proxyUrl || '';
// 从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, getChunkedFileCacheControl(context));
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', getChunkedFileCacheControl(context)); // 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 { env, request, url, Referer } = context;
const metadata = imgRecord.metadata;
const db = getDatabase(env);
const discordCredentials = await resolveDiscordCredentials(db, env, metadata);
const botToken = discordCredentials.botToken;
const channelId = discordCredentials.channelId;
const proxyUrl = discordCredentials.proxyUrl;
// 从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, getChunkedFileCacheControl(context));
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, channelId, 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', getChunkedFileCacheControl(context));
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, getFileCacheControl(context));
// 处理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;
// 检查是否配置了 CDN 文件完整路径
const cdnFileUrl = await getS3CdnFileUrl(context.env, metadata);
// 如果配置了 CDN 文件路径,通过 CDN 读取文件
if (cdnFileUrl) {
try {
// 处理 HEAD 请求
if (request.method === 'HEAD') {
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, getFileCacheControl(context));
return handleHeadRequest(headers);
}
// 构建请求头
const fetchHeaders = {};
// 支持 Range 请求
const range = request.headers.get('Range');
if (range) {
fetchHeaders['Range'] = range;
}
// 通过 CDN 获取文件(直接使用完整路径,无需拼接)
const response = await fetch(cdnFileUrl, {
method: 'GET',
headers: fetchHeaders
});
if (!response.ok && response.status !== 206) {
// CDN 读取失败,回退到 S3 API
console.warn(`CDN fetch failed (${response.status}), falling back to S3 API`);
return await handleS3FileViaAPI(context, metadata, encodedFileName, fileType);
}
// 构建响应头
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, getFileCacheControl(context));
// 复制相关头部
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) {
// CDN 读取出错,回退到 S3 API
console.error(`CDN fetch error: ${error.message}, falling back to S3 API`);
return await handleS3FileViaAPI(context, metadata, encodedFileName, fileType);
}
}
// 没有配置 CDN 文件路径,使用 S3 API
return await handleS3FileViaAPI(context, metadata, encodedFileName, fileType);
}
async function getS3CdnFileUrl(env, metadata) {
try {
const db = getDatabase(env);
const s3Credentials = await resolveS3Credentials(db, env, metadata);
return buildCdnFileUrl(s3Credentials.cdnDomain, s3Credentials.key);
} catch (error) {
console.warn('Failed to build S3 CDN file URL:', error.message);
return '';
}
}
// 通过 S3 API 读取文件
async function handleS3FileViaAPI(context, metadata, encodedFileName, fileType) {
const { Referer, url, request, env } = context;
const db = getDatabase(env);
const s3Credentials = await resolveS3Credentials(db, env, metadata);
const s3Client = new S3Client({
region: s3Credentials.region || "auto",
endpoint: s3Credentials.endpoint,
credentials: {
accessKeyId: s3Credentials.accessKeyId,
secretAccessKey: s3Credentials.secretAccessKey
},
forcePathStyle: s3Credentials.pathStyle || false
});
const bucketName = s3Credentials.bucketName;
const key = s3Credentials.key;
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, getFileCacheControl(context));
// 设置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 {
const db = getDatabase(env);
const discordCredentials = await resolveDiscordCredentials(db, env, metadata);
// 每次读取都通过 API 获取新的附件 URL(因为 Discord 附件 URL 会在约24小时后过期)
let fileUrl = null;
if (discordCredentials.messageId && discordCredentials.channelId && discordCredentials.botToken) {
const discordAPI = new DiscordAPI(discordCredentials.botToken);
fileUrl = await discordAPI.getFileURL(discordCredentials.channelId, discordCredentials.messageId);
}
if (!fileUrl) {
return new Response('Error: Discord file URL not found', { status: 500 });
}
// 如果配置了代理 URL,替换 Discord CDN 域名
if (discordCredentials.proxyUrl) {
fileUrl = fileUrl.replace('https://cdn.discordapp.com', `https://${discordCredentials.proxyUrl}`);
}
// 处理 HEAD 请求
if (request.method === 'HEAD') {
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, getFileCacheControl(context));
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, getFileCacheControl(context));
// 复制相关头部
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 { env, request, url, Referer } = context;
try {
const db = getDatabase(env);
const hfCredentials = await resolveHuggingFaceCredentials(db, env, metadata);
const hfRepo = hfCredentials.repo;
const hfFilePath = hfCredentials.filePath;
const hfToken = hfCredentials.token;
const hfIsPrivate = hfCredentials.isPrivate || false;
if (!hfRepo || !hfFilePath) {
return new Response('Error: HuggingFace file info not found', { status: 500 });
}
// 构建文件 URL
const fileUrl = `https://huggingface.co/datasets/${hfRepo}/resolve/main/${hfFilePath}`;
const fileSize = HuggingFaceAPI.getMetadataFileSize(metadata);
// 处理 HEAD 请求
if (request.method === 'HEAD') {
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, getFileCacheControl(context));
if (fileSize) {
headers.set('Content-Length', fileSize.toString());
}
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, getFileCacheControl(context));
// 复制相关头部
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 });
}
}
// 处理 WebDAV 文件读取
async function handleWebDAVFile(context, metadata, encodedFileName, fileType) {
const { request, url, Referer } = context;
try {
const db = getDatabase(context.env);
const webdavCredentials = await resolveWebDAVCredentials(db, context.env, metadata);
const filePath = webdavCredentials.filePath;
const publicUrl = getWebDAVPublicFileUrl(webdavCredentials, filePath);
if (!filePath && !publicUrl) {
return new Response('Error: WebDAV file info not found', { status: 500 });
}
const headers = new Headers();
setCommonHeaders(headers, encodedFileName, fileType, getFileCacheControl(context));
const fetchHeaders = {};
const range = request.headers.get('Range');
if (range) {
fetchHeaders['Range'] = range;
}
let response;
if (publicUrl) {
response = await fetch(publicUrl, {
method: request.method === 'HEAD' ? 'HEAD' : 'GET',
headers: fetchHeaders,
});
if (!response.ok && response.status !== 206 && response.status !== 304 && webdavCredentials.baseUrl && filePath) {
try {
const webdavAPI = new WebDAVAPI(webdavCredentials);
response = await webdavAPI.getFile(filePath, {
method: request.method === 'HEAD' ? 'HEAD' : 'GET',
headers: fetchHeaders,
});
} catch (fallbackError) {
console.warn('WebDAV public URL fallback failed:', fallbackError.message);
}
}
} else {
if (!webdavCredentials.baseUrl || !filePath) {
return new Response('Error: WebDAV channel config not found', { status: 500 });
}
const webdavAPI = new WebDAVAPI(webdavCredentials);
response = await webdavAPI.getFile(filePath, {
method: request.method === 'HEAD' ? 'HEAD' : 'GET',
headers: fetchHeaders,
});
}
if (!response.ok && response.status !== 206 && response.status !== 304) {
return new Response(`Error: Failed to fetch from WebDAV - ${response.status}`, { status: response.status });
}
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'));
}
if (response.headers.get('ETag')) {
headers.set('ETag', response.headers.get('ETag'));
}
if (response.status === 304) {
return new Response(null, { status: 304, headers });
}
if (request.method === 'HEAD') {
return handleHeadRequest(headers, response.headers.get('ETag'));
}
return new Response(response.body, {
status: response.status,
headers
});
} catch (error) {
return new Response(`Error: Failed to fetch from WebDAV - ${error.message}`, { status: 500 });
}
}
function getWebDAVPublicFileUrl(webdavCredentials, filePath) {
if (webdavCredentials.publicUrl && filePath) {
try {
return buildWebDAVUrl(webdavCredentials.publicUrl, filePath);
} catch (error) {
console.warn('Invalid WebDAV public URL config:', error.message);
}
}
return '';
}