Files
CloudFlare-ImgBed/functions/upload/index.js
2026-01-30 17:15:22 +08:00

889 lines
33 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 { userAuthCheck, UnauthorizedResponse } from "../utils/userAuth";
import { fetchUploadConfig, fetchSecurityConfig } from "../utils/sysConfig";
import {
createResponse, getUploadIp, getIPAddress, isExtValid,
moderateContent, purgeCDNCache, isBlockedUploadIp, buildUniqueFileId, endUpload, getImageDimensions
} from "./uploadTools";
import { initializeChunkedUpload, handleChunkUpload, uploadLargeFileToTelegram, handleCleanupRequest } from "./chunkUpload";
import { handleChunkMerge } from "./chunkMerge";
import { TelegramAPI } from "../utils/telegramAPI";
import { DiscordAPI } from "../utils/discordAPI";
import { HuggingFaceAPI } from "../utils/huggingfaceAPI";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getDatabase } from '../utils/databaseAdapter.js';
export async function onRequest(context) { // Contents of context object
const { request, env, params, waitUntil, next, data } = context;
// 解析请求的URL存入 context
const url = new URL(request.url);
context.url = url;
// 读取各项配置,存入 context
const securityConfig = await fetchSecurityConfig(env);
const uploadConfig = await fetchUploadConfig(env, context);
context.securityConfig = securityConfig;
context.uploadConfig = uploadConfig;
// 鉴权
const requiredPermission = 'upload';
if (!await userAuthCheck(env, url, request, requiredPermission)) {
return UnauthorizedResponse('Unauthorized');
}
// 获得上传IP
const uploadIp = getUploadIp(request);
// 判断上传ip是否被封禁
const isBlockedIp = await isBlockedUploadIp(env, uploadIp);
if (isBlockedIp) {
return createResponse('Error: Your IP is blocked', { status: 403 });
}
// 检查是否为清理请求
const cleanupRequest = url.searchParams.get('cleanup') === 'true';
if (cleanupRequest) {
const uploadId = url.searchParams.get('uploadId');
const totalChunks = parseInt(url.searchParams.get('totalChunks')) || 0;
return await handleCleanupRequest(context, uploadId, totalChunks);
}
// 检查是否为初始化分块上传请求
const initChunked = url.searchParams.get('initChunked') === 'true';
if (initChunked) {
return await initializeChunkedUpload(context);
}
// 检查是否为分块上传
const isChunked = url.searchParams.get('chunked') === 'true';
const isMerge = url.searchParams.get('merge') === 'true';
if (isChunked) {
if (isMerge) {
return await handleChunkMerge(context);
} else {
return await handleChunkUpload(context);
}
}
// 处理非分块文件上传
return await processFileUpload(context);
}
// 通用文件上传处理函数
async function processFileUpload(context, formdata = null) {
const { request, url } = context;
// 解析表单数据
formdata = formdata || await request.formData();
// 将 formdata 存储在 context 中
context.formdata = formdata;
// 获得上传渠道类型
const urlParamUploadChannel = url.searchParams.get('uploadChannel');
// 获得指定的渠道名称(可选)
const urlParamChannelName = url.searchParams.get('channelName');
// 获取IP地址
const uploadIp = getUploadIp(request);
const ipAddress = await getIPAddress(uploadIp);
// 获取上传文件夹路径
let uploadFolder = url.searchParams.get('uploadFolder') || '';
let uploadChannel = 'TelegramNew';
switch (urlParamUploadChannel) {
case 'telegram':
uploadChannel = 'TelegramNew';
break;
case 'cfr2':
uploadChannel = 'CloudflareR2';
break;
case 's3':
uploadChannel = 'S3';
break;
case 'discord':
uploadChannel = 'Discord';
break;
case 'huggingface':
uploadChannel = 'HuggingFace';
break;
case 'external':
uploadChannel = 'External';
break;
default:
uploadChannel = 'TelegramNew';
break;
}
// 将指定的渠道名称存入 context供后续上传函数使用
context.specifiedChannelName = urlParamChannelName || null;
// 获取文件信息
const time = new Date().getTime();
const file = formdata.get('file');
const fileType = file.type;
let fileName = file.name;
const fileSizeBytes = file.size; // 文件大小,单位字节
const fileSize = (fileSizeBytes / 1024 / 1024).toFixed(2); // 文件大小单位MB
// 检查fileType和fileName是否存在
if (fileType === null || fileType === undefined || fileName === null || fileName === undefined) {
return createResponse('Error: fileType or fileName is wrong, check the integrity of this file!', { status: 400 });
}
// 提取图片尺寸
let imageDimensions = null;
if (fileType.startsWith('image/')) {
try {
// 统一读取 64KB足以覆盖 JPEG 的 EXIF 数据和其他格式
const headerBuffer = await file.slice(0, 65536).arrayBuffer();
imageDimensions = getImageDimensions(headerBuffer, fileType);
} catch (error) {
console.error('Error reading image dimensions:', error);
}
}
// 如果上传文件夹路径为空,尝试从文件名中获取
if (uploadFolder === '' || uploadFolder === null || uploadFolder === undefined) {
uploadFolder = fileName.split('/').slice(0, -1).join('/');
}
// 处理文件夹路径格式,确保没有开头的/
const normalizedFolder = uploadFolder
? uploadFolder.replace(/^\/+/, '') // 移除开头的/
.replace(/\/{2,}/g, '/') // 替换多个连续的/为单个/
.replace(/\/$/, '') // 移除末尾的/
: '';
const metadata = {
FileName: fileName,
FileType: fileType,
FileSize: fileSize,
FileSizeBytes: fileSizeBytes,
UploadIP: uploadIp,
UploadAddress: ipAddress,
ListType: "None",
TimeStamp: time,
Label: "None",
Directory: normalizedFolder === '' ? '' : normalizedFolder + '/',
Tags: []
};
// 添加图片尺寸信息
if (imageDimensions) {
metadata.Width = imageDimensions.width;
metadata.Height = imageDimensions.height;
}
let fileExt = fileName.split('.').pop(); // 文件扩展名
if (!isExtValid(fileExt)) {
// 如果文件名中没有扩展名,尝试从文件类型中获取
fileExt = fileType.split('/').pop();
if (fileExt === fileType || fileExt === '' || fileExt === null || fileExt === undefined) {
// Type中无法获取扩展名
fileExt = 'unknown' // 默认扩展名
}
}
// 构建文件ID
const fullId = await buildUniqueFileId(context, fileName, fileType);
// 获得返回链接格式, default为返回/file/id, full为返回完整链接
const returnFormat = url.searchParams.get('returnFormat') || 'default';
let returnLink = '';
if (returnFormat === 'full') {
returnLink = `${url.origin}/file/${fullId}`;
} else {
returnLink = `/file/${fullId}`;
}
/* ====================================不同渠道上传======================================= */
// 出错是否切换渠道自动重试,默认开启
const autoRetry = url.searchParams.get('autoRetry') === 'false' ? false : true;
let err = '';
// 上传到不同渠道
if (uploadChannel === 'CloudflareR2') {
// -------------CloudFlare R2 渠道---------------
const res = await uploadFileToCloudflareR2(context, fullId, metadata, returnLink);
if (res.status === 200 || !autoRetry) {
return res;
} else {
err = await res.text();
}
} else if (uploadChannel === 'S3') {
// ---------------------S3 渠道------------------
const res = await uploadFileToS3(context, fullId, metadata, returnLink);
if (res.status === 200 || !autoRetry) {
return res;
} else {
err = await res.text();
}
} else if (uploadChannel === 'Discord') {
// ---------------------Discord 渠道------------------
const res = await uploadFileToDiscord(context, fullId, metadata, returnLink);
if (res.status === 200 || !autoRetry) {
return res;
} else {
err = await res.text();
}
} else if (uploadChannel === 'HuggingFace') {
// ---------------------HuggingFace 渠道------------------
const res = await uploadFileToHuggingFace(context, fullId, metadata, returnLink);
if (res.status === 200 || !autoRetry) {
return res;
} else {
err = await res.text();
}
} else if (uploadChannel === 'External') {
// --------------------外链渠道----------------------
const res = await uploadFileToExternal(context, fullId, metadata, returnLink);
return res;
} else {
// ----------------Telegram New 渠道-------------------
const res = await uploadFileToTelegram(context, fullId, metadata, fileExt, fileName, fileType, returnLink);
if (res.status === 200 || !autoRetry) {
return res;
} else {
err = await res.text();
}
}
// 上传失败,开始自动切换渠道重试
const res = await tryRetry(err, context, uploadChannel, fullId, metadata, fileExt, fileName, fileType, returnLink);
return res;
}
// 上传到Cloudflare R2
async function uploadFileToCloudflareR2(context, fullId, metadata, returnLink) {
const { env, waitUntil, uploadConfig, formdata, specifiedChannelName } = context;
const db = getDatabase(env);
// 检查R2数据库是否配置
if (typeof env.img_r2 == "undefined" || env.img_r2 == null || env.img_r2 == "") {
return createResponse('Error: Please configure R2 database', { status: 500 });
}
// 检查 R2 渠道是否启用
const r2Settings = uploadConfig.cfr2;
if (!r2Settings.channels || r2Settings.channels.length === 0) {
return createResponse('Error: No R2 channel provided', { status: 400 });
}
// 选择渠道:优先使用指定的渠道名称
let r2Channel;
if (specifiedChannelName) {
r2Channel = r2Settings.channels.find(ch => ch.name === specifiedChannelName);
}
if (!r2Channel) {
r2Channel = r2Settings.channels[0];
}
const R2DataBase = env.img_r2;
// 写入R2数据库
await R2DataBase.put(fullId, formdata.get('file'));
// 更新metadata
metadata.Channel = "CloudflareR2";
metadata.ChannelName = r2Channel.name || "R2_env";
// 图像审查采用R2的publicUrl
const R2PublicUrl = r2Channel.publicUrl;
let moderateUrl = `${R2PublicUrl}/${fullId}`;
metadata.Label = await moderateContent(env, moderateUrl);
// 写入数据库
try {
await db.put(fullId, "", {
metadata: metadata,
});
} catch (error) {
return createResponse('Error: Failed to write to database', { status: 500 });
}
// 结束上传
waitUntil(endUpload(context, fullId, metadata));
// 成功上传将文件ID返回给客户端
return createResponse(
JSON.stringify([{ 'src': `${returnLink}` }]),
{
status: 200,
headers: {
'Content-Type': 'application/json',
}
}
);
}
// 上传到 S3支持自定义端点
async function uploadFileToS3(context, fullId, metadata, returnLink) {
const { env, waitUntil, uploadConfig, securityConfig, url, formdata, specifiedChannelName } = context;
const db = getDatabase(env);
const uploadModerate = securityConfig.upload.moderate;
const s3Settings = uploadConfig.s3;
const s3Channels = s3Settings.channels;
// 选择渠道:优先使用指定的渠道名称
let s3Channel;
if (specifiedChannelName) {
s3Channel = s3Channels.find(ch => ch.name === specifiedChannelName);
}
if (!s3Channel) {
s3Channel = s3Settings.loadBalance.enabled
? s3Channels[Math.floor(Math.random() * s3Channels.length)]
: s3Channels[0];
}
if (!s3Channel) {
return createResponse('Error: No S3 channel provided', { status: 400 });
}
const { endpoint, pathStyle, accessKeyId, secretAccessKey, bucketName, region, cdnDomain } = s3Channel;
// 创建 S3 客户端
const s3Client = new S3Client({
region: region || "auto", // R2 可用 "auto"
endpoint, // 自定义 S3 端点
credentials: {
accessKeyId,
secretAccessKey
},
forcePathStyle: pathStyle // 是否启用路径风格
});
// 获取文件
const file = formdata.get("file");
if (!file) return createResponse("Error: No file provided", { status: 400 });
// 转换 Blob 为 Uint8Array
const arrayBuffer = await file.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const s3FileName = fullId;
try {
// S3 上传参数
const putObjectParams = {
Bucket: bucketName,
Key: s3FileName,
Body: uint8Array, // 直接使用 Blob
ContentType: file.type
};
// 执行上传
await s3Client.send(new PutObjectCommand(putObjectParams));
// 更新 metadata
metadata.Channel = "S3";
metadata.ChannelName = s3Channel.name;
const s3ServerDomain = endpoint.replace(/https?:\/\//, "");
if (pathStyle) {
metadata.S3Location = `https://${s3ServerDomain}/${bucketName}/${s3FileName}`; // 采用路径风格的 URL
} else {
metadata.S3Location = `https://${bucketName}.${s3ServerDomain}/${s3FileName}`; // 采用虚拟主机风格的 URL
}
metadata.S3Endpoint = endpoint;
metadata.S3PathStyle = pathStyle;
metadata.S3AccessKeyId = accessKeyId;
metadata.S3SecretAccessKey = secretAccessKey;
metadata.S3Region = region || "auto";
metadata.S3BucketName = bucketName;
metadata.S3FileKey = s3FileName;
// 保存 CDN 文件完整路径(如果配置了 CDN 域名)
if (cdnDomain) {
// 存储完整的 CDN 文件路径,而不是仅存储域名
metadata.S3CdnFileUrl = `${cdnDomain.replace(/\/$/, '')}/${s3FileName}`;
}
// 图像审查
if (uploadModerate && uploadModerate.enabled) {
try {
await db.put(fullId, "", { metadata });
} catch {
return createResponse("Error: Failed to write to KV database", { status: 500 });
}
const moderateUrl = `https://${url.hostname}/file/${fullId}`;
await purgeCDNCache(env, moderateUrl, url);
metadata.Label = await moderateContent(env, moderateUrl);
}
// 写入数据库
try {
await db.put(fullId, "", { metadata });
} catch {
return createResponse("Error: Failed to write to database", { status: 500 });
}
// 结束上传
waitUntil(endUpload(context, fullId, metadata));
return createResponse(JSON.stringify([{ src: returnLink }]), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
} catch (error) {
return createResponse(`Error: Failed to upload to S3 - ${error.message}`, { status: 500 });
}
}
// 上传到Telegram
async function uploadFileToTelegram(context, fullId, metadata, fileExt, fileName, fileType, returnLink) {
const { env, waitUntil, uploadConfig, url, formdata, specifiedChannelName } = context;
const db = getDatabase(env);
// 选择一个 Telegram 渠道上传
const tgSettings = uploadConfig.telegram;
const tgChannels = tgSettings.channels;
let tgChannel;
// 如果指定了渠道名称,优先使用指定的渠道
if (specifiedChannelName) {
tgChannel = tgChannels.find(ch => ch.name === specifiedChannelName);
}
// 未指定或未找到指定渠道,使用负载均衡或第一个
if (!tgChannel) {
tgChannel = tgSettings.loadBalance.enabled ? tgChannels[Math.floor(Math.random() * tgChannels.length)] : tgChannels[0];
}
if (!tgChannel) {
return createResponse('Error: No Telegram channel provided', { status: 400 });
}
const tgBotToken = tgChannel.botToken;
const tgChatId = tgChannel.chatId;
const tgProxyUrl = tgChannel.proxyUrl || '';
const file = formdata.get('file');
const fileSize = file.size;
const telegramAPI = new TelegramAPI(tgBotToken, tgProxyUrl);
// 16MB 分片阈值 (TG Bot getFile download limit: 20MB, leave 4MB safety margin)
const CHUNK_SIZE = 16 * 1024 * 1024; // 16MB
if (fileSize > CHUNK_SIZE) {
// 大文件分片上传
return await uploadLargeFileToTelegram(context, file, fullId, metadata, fileName, fileType, returnLink, tgBotToken, tgChatId, tgChannel);
}
// 由于TG会把gif后缀的文件转为视频所以需要修改后缀名绕过限制
if (fileExt === 'gif') {
const newFileName = fileName.replace(/\.gif$/, '.jpeg');
const newFile = new File([formdata.get('file')], newFileName, { type: fileType });
formdata.set('file', newFile);
} else if (fileExt === 'webp') {
const newFileName = fileName.replace(/\.webp$/, '.jpeg');
const newFile = new File([formdata.get('file')], newFileName, { type: fileType });
formdata.set('file', newFile);
}
// 选择对应的发送接口
const fileTypeMap = {
'image/': { 'url': 'sendPhoto', 'type': 'photo' },
'video/': { 'url': 'sendVideo', 'type': 'video' },
'audio/': { 'url': 'sendAudio', 'type': 'audio' },
'application/pdf': { 'url': 'sendDocument', 'type': 'document' },
};
const defaultType = { 'url': 'sendDocument', 'type': 'document' };
let sendFunction = Object.keys(fileTypeMap).find(key => fileType.startsWith(key))
? fileTypeMap[Object.keys(fileTypeMap).find(key => fileType.startsWith(key))]
: defaultType;
// GIF ICO 等发送接口特殊处理
if (fileType === 'image/gif' || fileType === 'image/webp' || fileExt === 'gif' || fileExt === 'webp') {
sendFunction = { 'url': 'sendAnimation', 'type': 'animation' };
} else if (fileType === 'image/svg+xml' || fileType === 'image/x-icon') {
sendFunction = { 'url': 'sendDocument', 'type': 'document' };
}
// 根据服务端压缩设置处理接口从参数中获取serverCompress如果为false则使用sendDocument接口
if (url.searchParams.get('serverCompress') === 'false') {
sendFunction = { 'url': 'sendDocument', 'type': 'document' };
}
// 上传文件到 Telegram
let res = createResponse('upload error, check your environment params about telegram channel!', { status: 400 });
try {
const response = await telegramAPI.sendFile(formdata.get('file'), tgChatId, sendFunction.url, sendFunction.type);
const fileInfo = telegramAPI.getFileInfo(response);
const filePath = await telegramAPI.getFilePath(fileInfo.file_id);
const id = fileInfo.file_id;
// 更新FileSize
metadata.FileSize = (fileInfo.file_size / 1024 / 1024).toFixed(2);
// 将响应返回给客户端
res = createResponse(
JSON.stringify([{ 'src': `${returnLink}` }]),
{
status: 200,
headers: {
'Content-Type': 'application/json',
}
}
);
// 图像审查(使用代理域名或官方域名)
const moderateDomain = tgProxyUrl ? `https://${tgProxyUrl}` : 'https://api.telegram.org';
const moderateUrl = `${moderateDomain}/file/bot${tgBotToken}/${filePath}`;
metadata.Label = await moderateContent(env, moderateUrl);
// 更新metadata写入KV数据库
try {
metadata.Channel = "TelegramNew";
metadata.ChannelName = tgChannel.name;
metadata.TgFileId = id;
metadata.TgChatId = tgChatId;
metadata.TgBotToken = tgBotToken;
// 保存代理域名配置
if (tgProxyUrl) {
metadata.TgProxyUrl = tgProxyUrl;
}
await db.put(fullId, "", {
metadata: metadata,
});
} catch (error) {
res = createResponse('Error: Failed to write to KV database', { status: 500 });
}
// 结束上传
waitUntil(endUpload(context, fullId, metadata));
} catch (error) {
console.log('Telegram upload error:', error.message);
res = createResponse('upload error, check your environment params about telegram channel!', { status: 400 });
} finally {
return res;
}
}
// 外链渠道
async function uploadFileToExternal(context, fullId, metadata, returnLink) {
const { env, waitUntil, formdata } = context;
const db = getDatabase(env);
// 直接将外链写入metadata
metadata.Channel = "External";
metadata.ChannelName = "External";
// 从 formdata 中获取外链
const extUrl = formdata.get('url');
if (extUrl === null || extUrl === undefined) {
return createResponse('Error: No url provided', { status: 400 });
}
metadata.ExternalLink = extUrl;
// 写入KV数据库
try {
await db.put(fullId, "", {
metadata: metadata,
});
} catch (error) {
return createResponse('Error: Failed to write to KV database', { status: 500 });
}
// 结束上传
waitUntil(endUpload(context, fullId, metadata));
// 返回结果
return createResponse(
JSON.stringify([{ 'src': `${returnLink}` }]),
{
status: 200,
headers: {
'Content-Type': 'application/json',
}
}
);
}
// 上传到 Discord
async function uploadFileToDiscord(context, fullId, metadata, returnLink) {
const { env, waitUntil, uploadConfig, formdata, specifiedChannelName } = context;
const db = getDatabase(env);
// 获取 Discord 渠道配置
const discordSettings = uploadConfig.discord;
if (!discordSettings || !discordSettings.channels || discordSettings.channels.length === 0) {
return createResponse('Error: No Discord channel configured', { status: 400 });
}
// 选择渠道:优先使用指定的渠道名称
const discordChannels = discordSettings.channels;
let discordChannel;
if (specifiedChannelName) {
discordChannel = discordChannels.find(ch => ch.name === specifiedChannelName);
}
if (!discordChannel) {
discordChannel = discordSettings.loadBalance?.enabled
? discordChannels[Math.floor(Math.random() * discordChannels.length)]
: discordChannels[0];
}
if (!discordChannel || !discordChannel.botToken || !discordChannel.channelId) {
return createResponse('Error: Discord channel not properly configured', { status: 400 });
}
const file = formdata.get('file');
const fileSize = file.size;
const fileName = metadata.FileName;
// Discord 文件大小限制Nitro 会员 25MB免费用户 10MB
const isNitro = discordChannel.isNitro || false;
const DISCORD_MAX_SIZE = isNitro ? 25 * 1024 * 1024 : 10 * 1024 * 1024;
if (fileSize > DISCORD_MAX_SIZE) {
const limitMB = isNitro ? 25 : 10;
return createResponse(`Error: File size exceeds Discord limit (${limitMB}MB), please use another channel`, { status: 413 });
}
const discordAPI = new DiscordAPI(discordChannel.botToken);
try {
// 上传文件到 Discord
const response = await discordAPI.sendFile(file, discordChannel.channelId, fileName);
const fileInfo = discordAPI.getFileInfo(response);
if (!fileInfo) {
throw new Error('Failed to get file info from Discord response');
}
// 更新 metadata
metadata.Channel = "Discord";
metadata.ChannelName = discordChannel.name || "Discord_env";
metadata.FileSize = (fileInfo.file_size / 1024 / 1024).toFixed(2);
metadata.DiscordMessageId = fileInfo.message_id;
metadata.DiscordChannelId = discordChannel.channelId;
metadata.DiscordBotToken = discordChannel.botToken;
// 注意:不存储 DiscordAttachmentUrl因为 Discord 附件 URL 会在约24小时后过期
// 读取时会通过 API 获取新的 URL
// 如果配置了代理 URL保存代理信息
if (discordChannel.proxyUrl) {
metadata.DiscordProxyUrl = discordChannel.proxyUrl;
}
// 图像审查(使用 Discord CDN URL 或代理 URL
let moderateUrl = fileInfo.url;
if (discordChannel.proxyUrl) {
moderateUrl = fileInfo.url.replace('https://cdn.discordapp.com', `https://${discordChannel.proxyUrl}`);
}
metadata.Label = await moderateContent(env, moderateUrl);
// 写入 KV 数据库
try {
await db.put(fullId, "", { metadata });
} catch (error) {
return createResponse('Error: Failed to write to KV database', { status: 500 });
}
// 结束上传
waitUntil(endUpload(context, fullId, metadata));
// 返回成功响应
return createResponse(
JSON.stringify([{ 'src': returnLink }]),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
} catch (error) {
console.error('Discord upload error:', error.message);
return createResponse(`Error: Discord upload failed - ${error.message}`, { status: 500 });
}
}
// 上传到 HuggingFace
async function uploadFileToHuggingFace(context, fullId, metadata, returnLink) {
const { env, waitUntil, uploadConfig, formdata, specifiedChannelName } = context;
const db = getDatabase(env);
console.log('=== HuggingFace Upload Start ===');
// 获取 HuggingFace 渠道配置
const hfSettings = uploadConfig.huggingface;
console.log('HuggingFace settings:', hfSettings ? 'found' : 'not found');
if (!hfSettings || !hfSettings.channels || hfSettings.channels.length === 0) {
console.log('Error: No HuggingFace channel configured');
return createResponse('Error: No HuggingFace channel configured', { status: 400 });
}
// 选择渠道:优先使用指定的渠道名称
const hfChannels = hfSettings.channels;
console.log('HuggingFace channels count:', hfChannels.length);
let hfChannel;
if (specifiedChannelName) {
hfChannel = hfChannels.find(ch => ch.name === specifiedChannelName);
}
if (!hfChannel) {
hfChannel = hfSettings.loadBalance?.enabled
? hfChannels[Math.floor(Math.random() * hfChannels.length)]
: hfChannels[0];
}
console.log('Selected channel:', hfChannel?.name, 'repo:', hfChannel?.repo);
if (!hfChannel || !hfChannel.token || !hfChannel.repo) {
console.log('Error: HuggingFace channel not properly configured', {
hasChannel: !!hfChannel,
hasToken: !!hfChannel?.token,
hasRepo: !!hfChannel?.repo
});
return createResponse('Error: HuggingFace channel not properly configured', { status: 400 });
}
const file = formdata.get('file');
const fileName = metadata.FileName;
// 获取前端预计算的 SHA256如果有
const precomputedSha256 = formdata.get('sha256') || null;
console.log('File to upload:', fileName, 'size:', file?.size, 'precomputed SHA256:', precomputedSha256 ? 'yes' : 'no');
// 构建文件路径:直接使用 fullId与其他渠道保持一致
const hfFilePath = fullId;
console.log('HuggingFace file path:', hfFilePath);
const huggingfaceAPI = new HuggingFaceAPI(hfChannel.token, hfChannel.repo, hfChannel.isPrivate || false);
try {
// 上传文件到 HuggingFace传入预计算的 SHA256
console.log('Starting HuggingFace upload...');
const result = await huggingfaceAPI.uploadFile(file, hfFilePath, `Upload ${fileName}`, precomputedSha256);
console.log('HuggingFace upload result:', result);
if (!result.success) {
throw new Error('Failed to upload file to HuggingFace');
}
// 更新 metadata
metadata.Channel = "HuggingFace";
metadata.ChannelName = hfChannel.name || "HuggingFace_env";
metadata.HfRepo = hfChannel.repo;
metadata.HfFilePath = hfFilePath;
metadata.HfToken = hfChannel.token;
metadata.HfIsPrivate = hfChannel.isPrivate || false;
metadata.HfFileUrl = result.fileUrl;
// 图像审查
const securityConfig = context.securityConfig;
const uploadModerate = securityConfig.upload?.moderate;
if (uploadModerate && uploadModerate.enabled) {
if (!hfChannel.isPrivate) {
// 公开仓库直接通过公开URL访问进行审查只写入1次KV
metadata.Label = await moderateContent(env, result.fileUrl);
} else {
// 私有仓库先写入KV再通过自己的域名访问进行审查
try {
await db.put(fullId, "", { metadata });
} catch (error) {
return createResponse('Error: Failed to write to KV database', { status: 500 });
}
const moderateUrl = `https://${context.url.hostname}/file/${fullId}`;
await purgeCDNCache(env, moderateUrl, context.url);
metadata.Label = await moderateContent(env, moderateUrl);
}
}
// 写入 KV 数据库
try {
await db.put(fullId, "", { metadata });
} catch (error) {
return createResponse('Error: Failed to write to KV database', { status: 500 });
}
// 结束上传
waitUntil(endUpload(context, fullId, metadata));
// 返回成功响应
return createResponse(
JSON.stringify([{ 'src': returnLink }]),
{
status: 200,
headers: { 'Content-Type': 'application/json' }
}
);
} catch (error) {
console.error('HuggingFace upload error:', error.message);
return createResponse(`Error: HuggingFace upload failed - ${error.message}`, { status: 500 });
}
}
// 自动切换渠道重试
async function tryRetry(err, context, uploadChannel, fullId, metadata, fileExt, fileName, fileType, returnLink) {
const { env, url, formdata } = context;
// 渠道列表Discord 因为有 10MB 限制,放在最后尝试)
const channelList = ['CloudflareR2', 'TelegramNew', 'S3', 'HuggingFace', 'Discord'];
const errMessages = {};
errMessages[uploadChannel] = 'Error: ' + uploadChannel + err;
// 先用原渠道再试一次(关闭服务端压缩)
url.searchParams.set('serverCompress', 'false');
let retryRes = null;
if (uploadChannel === 'CloudflareR2') {
retryRes = await uploadFileToCloudflareR2(context, fullId, metadata, returnLink);
} else if (uploadChannel === 'TelegramNew') {
retryRes = await uploadFileToTelegram(context, fullId, metadata, fileExt, fileName, fileType, returnLink);
} else if (uploadChannel === 'S3') {
retryRes = await uploadFileToS3(context, fullId, metadata, returnLink);
} else if (uploadChannel === 'HuggingFace') {
retryRes = await uploadFileToHuggingFace(context, fullId, metadata, returnLink);
} else if (uploadChannel === 'Discord') {
retryRes = await uploadFileToDiscord(context, fullId, metadata, returnLink);
}
// 原渠道重试成功,直接返回
if (retryRes && retryRes.status === 200) {
return retryRes;
} else if (retryRes) {
errMessages[uploadChannel + '_retry'] = 'Error: ' + uploadChannel + ' retry - ' + await retryRes.text();
}
// 原渠道重试失败,切换到其他渠道
for (let i = 0; i < channelList.length; i++) {
if (channelList[i] !== uploadChannel) {
let res = null;
if (channelList[i] === 'CloudflareR2') {
res = await uploadFileToCloudflareR2(context, fullId, metadata, returnLink);
} else if (channelList[i] === 'TelegramNew') {
res = await uploadFileToTelegram(context, fullId, metadata, fileExt, fileName, fileType, returnLink);
} else if (channelList[i] === 'S3') {
res = await uploadFileToS3(context, fullId, metadata, returnLink);
} else if (channelList[i] === 'HuggingFace') {
res = await uploadFileToHuggingFace(context, fullId, metadata, returnLink);
} else if (channelList[i] === 'Discord') {
res = await uploadFileToDiscord(context, fullId, metadata, returnLink);
}
if (res && res.status === 200) {
return res;
} else if (res) {
errMessages[channelList[i]] = 'Error: ' + channelList[i] + await res.text();
}
}
}
return createResponse(JSON.stringify(errMessages), { status: 500 });
}