Files
CloudFlare-ImgBed/functions/upload/index.js
T
叁月柒andGitHub 0bc3bc241f Refine GIF handling and update comments
Update comments for clarity and adjust GIF handling logic.
2025-12-06 15:36:40 +08:00

558 lines
20 KiB
JavaScript

import { userAuthCheck, UnauthorizedResponse } from "../utils/userAuth";
import { fetchUploadConfig, fetchSecurityConfig } from "../utils/sysConfig";
import {
createResponse, getUploadIp, getIPAddress, isExtValid,
moderateContent, purgeCDNCache, isBlockedUploadIp, buildUniqueFileId, endUpload
} from "./uploadTools";
import { initializeChunkedUpload, handleChunkUpload, uploadLargeFileToTelegram, handleCleanupRequest } from "./chunkUpload";
import { handleChunkMerge } from "./chunkMerge";
import { TelegramAPI } from "../utils/telegramAPI";
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.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');
// 获取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 'external':
uploadChannel = 'External';
break;
default:
uploadChannel = 'TelegramNew';
break;
}
// 获取文件信息
const time = new Date().getTime();
const fileType = formdata.get('file').type;
let fileName = formdata.get('file').name;
const fileSize = (formdata.get('file').size / 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 });
}
// 如果上传文件夹路径为空,尝试从文件名中获取
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,
UploadIP: uploadIp,
UploadAddress: ipAddress,
ListType: "None",
TimeStamp: time,
Label: "None",
Directory: normalizedFolder === '' ? '' : normalizedFolder + '/',
Tags: []
};
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 === '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 } = 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 });
}
const r2Channel = r2Settings.channels[0];
const R2DataBase = env.img_r2;
// 写入R2数据库
await R2DataBase.put(fullId, formdata.get('file'));
// 更新metadata
metadata.Channel = "CloudflareR2";
metadata.ChannelName = "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 } = context;
const db = getDatabase(env);
const uploadModerate = securityConfig.upload.moderate;
const s3Settings = uploadConfig.s3;
const s3Channels = s3Settings.channels;
const 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 } = 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;
// 图像审查
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 } = context;
const db = getDatabase(env);
// 选择一个 Telegram 渠道上传,若负载均衡开启,则随机选择一个;否则选择第一个
const tgSettings = uploadConfig.telegram;
const tgChannels = tgSettings.channels;
const 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 file = formdata.get('file');
const fileSize = file.size;
const telegramAPI = new TelegramAPI(tgBotToken);
// 20MB 分片阈值
const CHUNK_SIZE = 20 * 1024 * 1024; // 20MB
if (fileSize > CHUNK_SIZE) {
// 大文件分片上传
return await uploadLargeFileToTelegram(env, file, fullId, metadata, fileName, fileType, url, 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 moderateUrl = `https://api.telegram.org/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;
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',
}
}
);
}
// 自动切换渠道重试
async function tryRetry(err, context, uploadChannel, fullId, metadata, fileExt, fileName, fileType, returnLink) {
const { env, url, formdata } = context;
// 渠道列表
const channelList = ['CloudflareR2', 'TelegramNew', 'S3'];
const errMessages = {};
errMessages[uploadChannel] = 'Error: ' + uploadChannel + err;
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);
}
if (res.status === 200) {
return res;
} else {
errMessages[channelList[i]] = 'Error: ' + channelList[i] + await res.text();
}
}
}
return createResponse(JSON.stringify(errMessages), { status: 500 });
}