mirror of
https://github.com/ZSCGR/CloudFlare-ImgBed.git
synced 2026-08-13 04:03:42 +08:00
558 lines
20 KiB
JavaScript
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 });
|
|
}
|