mirror of
https://github.com/MarSeventh/CloudFlare-ImgBed.git
synced 2026-02-01 14:44:02 +00:00
update v2.5.7
This commit is contained in:
File diff suppressed because one or more lines are too long
BIN
css/157.3f84c0cf.css.gz
Normal file
BIN
css/157.3f84c0cf.css.gz
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
import { purgeCFCache } from "../../../utils/purgeCache";
|
||||
import { purgeCFCache, purgeRandomFileListCache, purgePublicFileListCache } from "../../../utils/purgeCache";
|
||||
import { addFileToIndex } from "../../../utils/indexManager.js";
|
||||
import { getDatabase } from "../../../utils/databaseAdapter.js";
|
||||
|
||||
@@ -15,12 +15,12 @@ export async function onRequest(context) {
|
||||
|
||||
// 组装 CDN URL
|
||||
const url = new URL(request.url);
|
||||
|
||||
|
||||
if (params.path) {
|
||||
params.path = String(params.path).split(',').join('/');
|
||||
}
|
||||
const cdnUrl = `https://${url.hostname}/file/${params.path}`;
|
||||
|
||||
|
||||
// 解码params.path
|
||||
params.path = decodeURIComponent(params.path);
|
||||
|
||||
@@ -36,9 +36,13 @@ export async function onRequest(context) {
|
||||
// 清除CDN缓存
|
||||
await purgeCFCache(env, cdnUrl);
|
||||
|
||||
// 清除 randomFileList 等API缓存
|
||||
const normalizedFolder = params.path.split('/').slice(0, -1).join('/');
|
||||
await purgeRandomFileListCache(url.origin, normalizedFolder);
|
||||
await purgePublicFileListCache(url.origin, normalizedFolder);
|
||||
|
||||
// 更新索引
|
||||
waitUntil(addFileToIndex(context, params.path, value.metadata));
|
||||
|
||||
return new Response(info);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { purgeCFCache } from "../../../utils/purgeCache";
|
||||
import { purgeCFCache, purgeRandomFileListCache, purgePublicFileListCache } from "../../../utils/purgeCache";
|
||||
import { removeFileFromIndex, batchRemoveFilesFromIndex } from "../../../utils/indexManager.js";
|
||||
import { getDatabase } from '../../../utils/databaseAdapter.js';
|
||||
import { DiscordAPI } from '../../../utils/discordAPI.js';
|
||||
@@ -161,18 +161,10 @@ async function deleteFile(env, fileId, cdnUrl, url) {
|
||||
// 清除CDN缓存
|
||||
await purgeCFCache(env, cdnUrl);
|
||||
|
||||
// 清除randomFileList API缓存
|
||||
try {
|
||||
const cache = caches.default;
|
||||
const nullResponse = new Response(null, {
|
||||
headers: { 'Cache-Control': 'max-age=0' },
|
||||
});
|
||||
|
||||
const normalizedFolder = fileId.split('/').slice(0, -1).join('/');
|
||||
await cache.put(`${url.origin}/api/randomFileList?dir=${normalizedFolder}`, nullResponse);
|
||||
} catch (error) {
|
||||
console.error('Failed to clear cache:', error);
|
||||
}
|
||||
// 清除 api/randomFileList 等API缓存
|
||||
const normalizedFolder = fileId.split('/').slice(0, -1).join('/');
|
||||
await purgeRandomFileListCache(url.origin, normalizedFolder);
|
||||
await purgePublicFileListCache(url.origin, normalizedFolder);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { S3Client, CopyObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { purgeCFCache } from "../../../utils/purgeCache";
|
||||
import { purgeCFCache, purgeRandomFileListCache, purgePublicFileListCache } from "../../../utils/purgeCache";
|
||||
import { moveFileInIndex, batchMoveFilesInIndex } from "../../../utils/indexManager.js";
|
||||
import { getDatabase } from '../../../utils/databaseAdapter.js';
|
||||
|
||||
@@ -177,20 +177,11 @@ async function moveFile(env, fileId, newFileId, cdnUrl, url) {
|
||||
// 清除CDN缓存
|
||||
await purgeCFCache(env, cdnUrl);
|
||||
|
||||
// 清除randomFileList API缓存
|
||||
try {
|
||||
const cache = caches.default;
|
||||
const nullResponse = new Response(null, {
|
||||
headers: { 'Cache-Control': 'max-age=0' },
|
||||
});
|
||||
|
||||
const normalizedFolder = fileId.split('/').slice(0, -1).join('/');
|
||||
const normalizedDist = newFileId.split('/').slice(0, -1).join('/');
|
||||
await cache.put(`${url.origin}/api/randomFileList?dir=${normalizedFolder}`, nullResponse);
|
||||
await cache.put(`${url.origin}/api/randomFileList?dir=${normalizedDist}`, nullResponse);
|
||||
} catch (error) {
|
||||
console.error('Failed to clear cache:', error);
|
||||
}
|
||||
// 清除 api/randomFileList 等API缓存
|
||||
const normalizedFolder = fileId.split('/').slice(0, -1).join('/');
|
||||
const normalizedDist = newFileId.split('/').slice(0, -1).join('/');
|
||||
await purgeRandomFileListCache(url.origin, normalizedFolder, normalizedDist);
|
||||
await purgePublicFileListCache(url.origin, normalizedFolder, normalizedDist);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { purgeCFCache } from "../../../utils/purgeCache";
|
||||
import { purgeCFCache, purgeRandomFileListCache, purgePublicFileListCache } from "../../../utils/purgeCache";
|
||||
import { addFileToIndex } from "../../../utils/indexManager.js";
|
||||
import { getDatabase } from "../../../utils/databaseAdapter.js";
|
||||
|
||||
@@ -15,12 +15,12 @@ export async function onRequest(context) {
|
||||
|
||||
// 组装 CDN URL
|
||||
const url = new URL(request.url);
|
||||
|
||||
|
||||
if (params.path) {
|
||||
params.path = String(params.path).split(',').join('/');
|
||||
}
|
||||
const cdnUrl = `https://${url.hostname}/file/${params.path}`;
|
||||
|
||||
|
||||
// 解码params.path
|
||||
params.path = decodeURIComponent(params.path);
|
||||
|
||||
@@ -36,9 +36,13 @@ export async function onRequest(context) {
|
||||
// 清除CDN缓存
|
||||
await purgeCFCache(env, cdnUrl);
|
||||
|
||||
// 清除 randomFileList 等API缓存
|
||||
const normalizedFolder = params.path.split('/').slice(0, -1).join('/');
|
||||
await purgeRandomFileListCache(url.origin, normalizedFolder);
|
||||
await purgePublicFileListCache(url.origin, normalizedFolder);
|
||||
|
||||
// 更新索引
|
||||
waitUntil(addFileToIndex(context, params.path, value.metadata));
|
||||
|
||||
return new Response(info);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,70 @@ function isAllowedDirectory(dir, allowedDirs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公开浏览文件列表(带缓存)
|
||||
* @param {Object} context - 上下文对象
|
||||
* @param {URL} url - 请求URL
|
||||
* @param {string} dir - 目录
|
||||
* @param {boolean} recursive - 是否递归
|
||||
* @returns {Promise<Object>} 文件列表和目录列表,包含 fromCache 字段
|
||||
*/
|
||||
async function getPublicFileList(context, url, dir, recursive) {
|
||||
// 构建缓存键(目录格式去掉末尾的/,与清除缓存时的格式一致)
|
||||
const cacheDir = dir.replace(/\/$/, '');
|
||||
const cacheKey = `${url.origin}/api/publicFileList?dir=${cacheDir}&recursive=${recursive}`;
|
||||
|
||||
// 检查缓存中是否有记录
|
||||
const cache = caches.default;
|
||||
const cacheRes = await cache.match(cacheKey);
|
||||
if (cacheRes) {
|
||||
const data = JSON.parse(await cacheRes.text());
|
||||
data.fromCache = true;
|
||||
return data;
|
||||
}
|
||||
|
||||
// 读取文件列表
|
||||
const result = await readIndex(context, {
|
||||
directory: dir,
|
||||
start: 0,
|
||||
count: -1,
|
||||
includeSubdirFiles: recursive,
|
||||
accessStatus: 'normal', // 只返回正常可访问的内容
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return { files: [], directories: [], totalCount: 0, fromCache: false };
|
||||
}
|
||||
|
||||
// 转换文件格式(只保留必要信息)
|
||||
const files = result.files.map(file => ({
|
||||
id: file.id,
|
||||
metadata: {
|
||||
FileType: file.metadata?.FileType,
|
||||
TimeStamp: file.metadata?.TimeStamp,
|
||||
FileSize: file.metadata?.FileSize,
|
||||
}
|
||||
}));
|
||||
|
||||
const cacheData = {
|
||||
files,
|
||||
directories: result.directories,
|
||||
totalCount: result.totalCount,
|
||||
};
|
||||
|
||||
// 缓存结果,缓存时间为24小时
|
||||
await cache.put(cacheKey, new Response(JSON.stringify(cacheData), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
}), {
|
||||
expirationTtl: 24 * 60 * 60
|
||||
});
|
||||
|
||||
cacheData.fromCache = false;
|
||||
return cacheData;
|
||||
}
|
||||
|
||||
export async function onRequest(context) {
|
||||
const { request, env } = context;
|
||||
const url = new URL(request.url);
|
||||
@@ -88,13 +152,13 @@ export async function onRequest(context) {
|
||||
let dir = url.searchParams.get('dir') || '';
|
||||
let search = url.searchParams.get('search') || '';
|
||||
if (search) {
|
||||
search = decodeURIComponent(search).trim();
|
||||
search = decodeURIComponent(search).trim().toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
// 获取高级搜索参数
|
||||
const recursive = url.searchParams.get('recursive') === 'true';
|
||||
const fileType = url.searchParams.get('type') || ''; // image, video, audio, other
|
||||
|
||||
|
||||
// 检查目录权限
|
||||
if (!isAllowedDirectory(dir, allowedDirs)) {
|
||||
return new Response(JSON.stringify({ error: 'Directory not allowed' }), {
|
||||
@@ -115,24 +179,11 @@ export async function onRequest(context) {
|
||||
const start = parseInt(url.searchParams.get('start'), 10) || 0;
|
||||
const count = parseInt(url.searchParams.get('count'), 10) || 50;
|
||||
|
||||
// 读取文件列表(获取全部,因为需要先过滤 block/adult)
|
||||
const result = await readIndex(context, {
|
||||
directory: dir,
|
||||
search,
|
||||
start: 0,
|
||||
count: -1, // 获取全部
|
||||
includeSubdirFiles: recursive,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return new Response(JSON.stringify({ error: 'Failed to read file list' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
}
|
||||
// 获取文件列表(带缓存)
|
||||
const cachedData = await getPublicFileList(context, url, dir, recursive);
|
||||
|
||||
// 过滤子目录,只返回允许的目录
|
||||
const filteredDirectories = result.directories.filter(subDir => {
|
||||
const filteredDirectories = cachedData.directories.filter(subDir => {
|
||||
return isAllowedDirectory(subDir, allowedDirs);
|
||||
});
|
||||
|
||||
@@ -140,22 +191,20 @@ export async function onRequest(context) {
|
||||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'avif'];
|
||||
const videoExts = ['mp4', 'webm', 'ogg', 'mov', 'm4v', 'mkv', 'avi', '3gp', 'mpeg', 'mpg', 'flv', 'wmv', 'ts', 'rmvb'];
|
||||
const audioExts = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma', 'ape', 'opus'];
|
||||
|
||||
|
||||
const getFileExt = (name) => (name.split('.').pop() || '').toLowerCase();
|
||||
const isImageFile = (name) => imageExts.includes(getFileExt(name));
|
||||
const isVideoFile = (name) => videoExts.includes(getFileExt(name));
|
||||
const isAudioFile = (name) => audioExts.includes(getFileExt(name));
|
||||
|
||||
// 过滤掉 block 和 adult 图片(公开浏览不应显示这些内容)
|
||||
let filteredFiles = result.files.filter(file => {
|
||||
const listType = file.metadata?.ListType;
|
||||
const label = file.metadata?.Label;
|
||||
// 排除被屏蔽的和成人内容
|
||||
if (listType === 'Block' || label === 'adult') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
let filteredFiles = cachedData.files;
|
||||
|
||||
// 搜索过滤
|
||||
if (search) {
|
||||
filteredFiles = filteredFiles.filter(file => {
|
||||
return file.id.toLowerCase().includes(search);
|
||||
});
|
||||
}
|
||||
|
||||
// 按文件类型过滤
|
||||
if (fileType) {
|
||||
@@ -176,23 +225,19 @@ export async function onRequest(context) {
|
||||
// 过滤后再分页
|
||||
filteredFiles = filteredFiles.slice(start, start + count);
|
||||
|
||||
// 转换文件格式(只返回必要信息,隐藏敏感元数据)
|
||||
// 转换文件格式
|
||||
const safeFiles = filteredFiles.map(file => ({
|
||||
name: file.id,
|
||||
metadata: {
|
||||
FileType: file.metadata?.FileType,
|
||||
TimeStamp: file.metadata?.TimeStamp,
|
||||
FileSize: file.metadata?.FileSize,
|
||||
// 不返回 Channel、IP、Label 等敏感信息
|
||||
}
|
||||
metadata: file.metadata
|
||||
}));
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
files: safeFiles,
|
||||
directories: filteredDirectories,
|
||||
totalCount: fileType ? filteredTotalCount : result.totalCount,
|
||||
totalCount: (search || fileType) ? filteredTotalCount : cachedData.totalCount,
|
||||
returnedCount: safeFiles.length,
|
||||
allowedDirs: allowedDirs, // 返回允许的目录列表供前端使用
|
||||
fromCache: cachedData.fromCache,
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||
});
|
||||
|
||||
@@ -40,6 +40,9 @@ export async function onRequest(context) {
|
||||
fileType = fileType.split(',');
|
||||
}
|
||||
|
||||
// 读取图片方向参数:landscape(横图), portrait(竖图), square(方图)
|
||||
const orientation = requestUrl.searchParams.get('orientation') || '';
|
||||
|
||||
// 读取指定文件夹
|
||||
const paramDir = requestUrl.searchParams.get('dir') || '';
|
||||
const dir = paramDir.replace(/^\/+/, '').replace(/\/{2,}/g, '/').replace(/\/$/, '');
|
||||
@@ -62,6 +65,27 @@ export async function onRequest(context) {
|
||||
// 筛选出符合fileType要求的记录
|
||||
allRecords = allRecords.filter(item => { return fileType.some(type => item.FileType?.includes(type)) });
|
||||
|
||||
// 根据图片方向筛选
|
||||
if (orientation && allRecords.length > 0) {
|
||||
const SQUARE_THRESHOLD = 0.1; // 宽高比差异小于10%视为方图
|
||||
allRecords = allRecords.filter(item => {
|
||||
// 如果没有尺寸信息,跳过该记录
|
||||
if (!item.Width || !item.Height) return false;
|
||||
|
||||
const ratio = item.Width / item.Height;
|
||||
switch (orientation) {
|
||||
case 'landscape': // 横图:宽 > 高
|
||||
return ratio > (1 + SQUARE_THRESHOLD);
|
||||
case 'portrait': // 竖图:高 > 宽
|
||||
return ratio < (1 - SQUARE_THRESHOLD);
|
||||
case 'square': // 方图:宽 ≈ 高
|
||||
return ratio >= (1 - SQUARE_THRESHOLD) && ratio <= (1 + SQUARE_THRESHOLD);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (allRecords.length == 0) {
|
||||
return new Response(JSON.stringify({}), { status: 200 });
|
||||
@@ -109,13 +133,15 @@ async function getRandomFileList(context, url, dir) {
|
||||
return JSON.parse(await cacheRes.text());
|
||||
}
|
||||
|
||||
let allRecords = await readIndex(context, { directory: dir, count: -1, includeSubdirFiles: true });
|
||||
let allRecords = await readIndex(context, { directory: dir, count: -1, includeSubdirFiles: true, accessStatus: 'normal' });
|
||||
|
||||
// 仅保留记录的name和metadata中的FileType字段
|
||||
// 仅保留记录的name和metadata中的必要字段
|
||||
allRecords = allRecords.files?.map(item => {
|
||||
return {
|
||||
name: item.id,
|
||||
FileType: item.metadata?.FileType
|
||||
FileType: item.metadata?.FileType,
|
||||
Width: item.metadata?.Width,
|
||||
Height: item.metadata?.Height
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -276,6 +276,7 @@ async function mergeR2ChunksInfo(context, uploadId, completedChunks, metadata) {
|
||||
}
|
||||
metadata.ChannelName = r2ChannelName;
|
||||
metadata.FileSize = (totalSize / 1024 / 1024).toFixed(2);
|
||||
metadata.FileSizeBytes = totalSize;
|
||||
|
||||
// 清理multipart info
|
||||
await db.delete(multipartKey);
|
||||
@@ -372,6 +373,7 @@ async function mergeS3ChunksInfo(context, uploadId, completedChunks, metadata) {
|
||||
metadata.Channel = "S3";
|
||||
metadata.ChannelName = s3Channel.name;
|
||||
metadata.FileSize = (totalSize / 1024 / 1024).toFixed(2);
|
||||
metadata.FileSizeBytes = totalSize;
|
||||
|
||||
const s3ServerDomain = endpoint.replace(/https?:\/\//, "");
|
||||
if (pathStyle) {
|
||||
@@ -467,6 +469,7 @@ async function mergeTelegramChunksInfo(context, uploadId, completedChunks, metad
|
||||
metadata.IsChunked = true;
|
||||
metadata.TotalChunks = completedChunks.length;
|
||||
metadata.FileSize = (totalSize / 1024 / 1024).toFixed(2);
|
||||
metadata.FileSizeBytes = totalSize;
|
||||
|
||||
// 将分片信息存储到value中
|
||||
const chunksData = JSON.stringify(chunks);
|
||||
@@ -546,6 +549,7 @@ async function mergeDiscordChunksInfo(context, uploadId, completedChunks, metada
|
||||
metadata.IsChunked = true;
|
||||
metadata.TotalChunks = completedChunks.length;
|
||||
metadata.FileSize = (totalSize / 1024 / 1024).toFixed(2);
|
||||
metadata.FileSizeBytes = totalSize;
|
||||
|
||||
// 将分片信息存储到value中
|
||||
const chunksData = JSON.stringify(chunks);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { userAuthCheck, UnauthorizedResponse } from "../utils/userAuth";
|
||||
import { fetchUploadConfig, fetchSecurityConfig } from "../utils/sysConfig";
|
||||
import {
|
||||
createResponse, getUploadIp, getIPAddress, isExtValid,
|
||||
moderateContent, purgeCDNCache, isBlockedUploadIp, buildUniqueFileId, endUpload
|
||||
moderateContent, purgeCDNCache, isBlockedUploadIp, buildUniqueFileId, endUpload, getImageDimensions
|
||||
} from "./uploadTools";
|
||||
import { initializeChunkedUpload, handleChunkUpload, uploadLargeFileToTelegram, handleCleanupRequest } from "./chunkUpload";
|
||||
import { handleChunkMerge } from "./chunkMerge";
|
||||
@@ -124,15 +124,32 @@ async function processFileUpload(context, formdata = null) {
|
||||
|
||||
// 获取文件信息
|
||||
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
|
||||
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 {
|
||||
// JPEG 需要更多数据(EXIF 可能很大),其他格式 512 字节足够
|
||||
const lowerType = fileType.toLowerCase();
|
||||
const isJpeg = lowerType === 'image/jpeg' || lowerType === 'image/jpg' || lowerType === 'image/pjpeg';
|
||||
const readSize = isJpeg ? 65536 : 512;
|
||||
const headerBuffer = await file.slice(0, readSize).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('/');
|
||||
@@ -148,6 +165,7 @@ async function processFileUpload(context, formdata = null) {
|
||||
FileName: fileName,
|
||||
FileType: fileType,
|
||||
FileSize: fileSize,
|
||||
FileSizeBytes: fileSizeBytes,
|
||||
UploadIP: uploadIp,
|
||||
UploadAddress: ipAddress,
|
||||
ListType: "None",
|
||||
@@ -157,6 +175,12 @@ async function processFileUpload(context, formdata = null) {
|
||||
Tags: []
|
||||
};
|
||||
|
||||
// 添加图片尺寸信息
|
||||
if (imageDimensions) {
|
||||
metadata.Width = imageDimensions.width;
|
||||
metadata.Height = imageDimensions.height;
|
||||
}
|
||||
|
||||
let fileExt = fileName.split('.').pop(); // 文件扩展名
|
||||
if (!isExtValid(fileExt)) {
|
||||
// 如果文件名中没有扩展名,尝试从文件类型中获取
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fetchSecurityConfig } from "../utils/sysConfig";
|
||||
import { purgeCFCache } from "../utils/purgeCache";
|
||||
import { purgeCFCache, purgeRandomFileListCache, purgePublicFileListCache } from "../utils/purgeCache";
|
||||
import { addFileToIndex } from "../utils/indexManager.js";
|
||||
import { getDatabase } from '../utils/databaseAdapter.js';
|
||||
|
||||
@@ -81,6 +81,98 @@ export function isExtValid(fileExt) {
|
||||
].includes(fileExt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从图片文件头部提取尺寸信息
|
||||
* 支持 JPEG, PNG, GIF, WebP, BMP 格式
|
||||
* 优先通过文件头魔数检测格式,不依赖 MIME 类型
|
||||
* @param {ArrayBuffer} buffer - 文件的 ArrayBuffer
|
||||
* @param {string} fileType - 文件 MIME 类型(仅作参考)
|
||||
* @returns {Object|null} { width, height } 或 null
|
||||
*/
|
||||
export function getImageDimensions(buffer, fileType) {
|
||||
try {
|
||||
const view = new DataView(buffer);
|
||||
const uint8 = new Uint8Array(buffer);
|
||||
|
||||
// 通过文件头魔数检测格式(不依赖 MIME 类型)
|
||||
|
||||
// PNG 签名: 89 50 4E 47
|
||||
if (uint8[0] === 0x89 && uint8[1] === 0x50 && uint8[2] === 0x4E && uint8[3] === 0x47) {
|
||||
const width = view.getUint32(16, false);
|
||||
const height = view.getUint32(20, false);
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
// JPEG 签名: FF D8 FF
|
||||
if (uint8[0] === 0xFF && uint8[1] === 0xD8 && uint8[2] === 0xFF) {
|
||||
let offset = 2;
|
||||
while (offset < buffer.byteLength - 9) {
|
||||
if (uint8[offset] !== 0xFF) break;
|
||||
const marker = uint8[offset + 1];
|
||||
// SOF0, SOF1, SOF2 标记包含尺寸信息
|
||||
if (marker >= 0xC0 && marker <= 0xC3 && marker !== 0xC4) {
|
||||
const height = view.getUint16(offset + 5, false);
|
||||
const width = view.getUint16(offset + 7, false);
|
||||
return { width, height };
|
||||
}
|
||||
const length = view.getUint16(offset + 2, false);
|
||||
offset += 2 + length;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// GIF 签名: 47 49 46 (GIF)
|
||||
if (uint8[0] === 0x47 && uint8[1] === 0x49 && uint8[2] === 0x46) {
|
||||
const width = view.getUint16(6, true); // little-endian
|
||||
const height = view.getUint16(8, true);
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
// WebP 签名: RIFF....WEBP
|
||||
if (uint8[0] === 0x52 && uint8[1] === 0x49 && uint8[2] === 0x46 && uint8[3] === 0x46 &&
|
||||
uint8[8] === 0x57 && uint8[9] === 0x45 && uint8[10] === 0x42 && uint8[11] === 0x50) {
|
||||
// VP8 (lossy): VP8 + 空格
|
||||
if (uint8[12] === 0x56 && uint8[13] === 0x50 && uint8[14] === 0x38 && uint8[15] === 0x20) {
|
||||
if (buffer.byteLength >= 30) {
|
||||
const width = (view.getUint16(26, true) & 0x3FFF);
|
||||
const height = (view.getUint16(28, true) & 0x3FFF);
|
||||
return { width, height };
|
||||
}
|
||||
}
|
||||
// VP8L (lossless): VP8L
|
||||
if (uint8[12] === 0x56 && uint8[13] === 0x50 && uint8[14] === 0x38 && uint8[15] === 0x4C) {
|
||||
if (buffer.byteLength >= 25) {
|
||||
const bits = view.getUint32(21, true);
|
||||
const width = (bits & 0x3FFF) + 1;
|
||||
const height = ((bits >> 14) & 0x3FFF) + 1;
|
||||
return { width, height };
|
||||
}
|
||||
}
|
||||
// VP8X (extended): VP8X
|
||||
if (uint8[12] === 0x56 && uint8[13] === 0x50 && uint8[14] === 0x38 && uint8[15] === 0x58) {
|
||||
if (buffer.byteLength >= 30) {
|
||||
const width = (uint8[24] | (uint8[25] << 8) | (uint8[26] << 16)) + 1;
|
||||
const height = (uint8[27] | (uint8[28] << 8) | (uint8[29] << 16)) + 1;
|
||||
return { width, height };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// BMP 签名: 42 4D (BM)
|
||||
if (uint8[0] === 0x42 && uint8[1] === 0x4D) {
|
||||
const width = view.getInt32(18, true);
|
||||
const height = Math.abs(view.getInt32(22, true)); // height 可能为负数
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error extracting image dimensions:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 图像审查
|
||||
export async function moderateContent(env, url) {
|
||||
const securityConfig = await fetchSecurityConfig(env);
|
||||
@@ -168,18 +260,9 @@ export async function purgeCDNCache(env, cdnUrl, url, normalizedFolder) {
|
||||
console.error('Failed to clear CDN cache:', error);
|
||||
}
|
||||
|
||||
// 清除api/randomFileList API缓存
|
||||
try {
|
||||
const cache = caches.default;
|
||||
// await cache.delete(`${url.origin}/api/randomFileList`); delete有bug,通过写入一个max-age=0的response来清除缓存
|
||||
const nullResponse = new Response(null, {
|
||||
headers: { 'Cache-Control': 'max-age=0' },
|
||||
});
|
||||
|
||||
await cache.put(`${url.origin}/api/randomFileList?dir=${normalizedFolder}`, nullResponse);
|
||||
} catch (error) {
|
||||
console.error('Failed to clear cache:', error);
|
||||
}
|
||||
// 清除 api/randomFileList 等API缓存
|
||||
await purgeRandomFileListCache(url.origin, normalizedFolder);
|
||||
await purgePublicFileListCache(url.origin, normalizedFolder);
|
||||
}
|
||||
|
||||
// 结束上传:清除缓存,维护索引
|
||||
|
||||
@@ -19,4 +19,38 @@ export async function purgeCFCache(env, cdnUrl) {
|
||||
body: `{"files":["${ cdnUrl }"]}`
|
||||
};
|
||||
await fetch(`https://api.cloudflare.com/client/v4/zones/${ cfZoneId }/purge_cache`, options);
|
||||
}
|
||||
|
||||
export async function purgeRandomFileListCache(origin, ...dirs) {
|
||||
try {
|
||||
const cache = caches.default;
|
||||
// cache.delete有bug,通过写入一个max-age=0的response来清除缓存
|
||||
const nullResponse = new Response(null, {
|
||||
headers: { 'Cache-Control': 'max-age=0' },
|
||||
});
|
||||
|
||||
for (const dir of dirs) {
|
||||
await cache.put(`${origin}/api/randomFileList?dir=${dir}`, nullResponse);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to clear randomFileList cache:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function purgePublicFileListCache(origin, ...dirs) {
|
||||
try {
|
||||
const cache = caches.default;
|
||||
// cache.delete有bug,通过写入一个max-age=0的response来清除缓存
|
||||
const nullResponse = new Response(null, {
|
||||
headers: { 'Cache-Control': 'max-age=0' },
|
||||
});
|
||||
|
||||
for (const dir of dirs) {
|
||||
// 清除递归和非递归两种缓存
|
||||
await cache.put(`${origin}/api/publicFileList?dir=${dir}&recursive=false`, nullResponse);
|
||||
await cache.put(`${origin}/api/publicFileList?dir=${dir}&recursive=true`, nullResponse);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to clear publicFileList cache:', error);
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/logo.png"><link rel="apple-touch-icon" href="/logo.png"><link rel="mask-icon" href="/logo.png" color="#f4b400"><meta name="description" content="Sanyue ImgHub - A modern file hosting platform"><meta name="keywords" content="Sanyue, ImgHub, file hosting, image hosting, cloud storage"><meta name="author" content="SanyueQi"><title>Sanyue ImgHub</title><script defer="defer" src="/js/chunk-vendors.92518219.js"></script><script defer="defer" src="/js/app.19ef69ac.js"></script><link href="/css/chunk-vendors.4363ed49.css" rel="stylesheet"><link href="/css/app.3e1508bd.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but sanyue_imghub doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
||||
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/logo.png"><link rel="apple-touch-icon" href="/logo.png"><link rel="mask-icon" href="/logo.png" color="#f4b400"><meta name="description" content="Sanyue ImgHub - A modern file hosting platform"><meta name="keywords" content="Sanyue, ImgHub, file hosting, image hosting, cloud storage"><meta name="author" content="SanyueQi"><title>Sanyue ImgHub</title><script defer="defer" src="/js/chunk-vendors.92518219.js"></script><script defer="defer" src="/js/app.1611e1ad.js"></script><link href="/css/chunk-vendors.4363ed49.css" rel="stylesheet"><link href="/css/app.3e1508bd.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but sanyue_imghub doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
||||
BIN
index.html.gz
BIN
index.html.gz
Binary file not shown.
2
js/157.17e1accc.js
Normal file
2
js/157.17e1accc.js
Normal file
File diff suppressed because one or more lines are too long
BIN
js/157.17e1accc.js.gz
Normal file
BIN
js/157.17e1accc.js.gz
Normal file
Binary file not shown.
1
js/157.17e1accc.js.map
Normal file
1
js/157.17e1accc.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
js/157.17e1accc.js.map.gz
Normal file
BIN
js/157.17e1accc.js.map.gz
Normal file
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
js/690.ff8af593.js.map.gz
Normal file
BIN
js/690.ff8af593.js.map.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
js/app.1611e1ad.js.map.gz
Normal file
BIN
js/app.1611e1ad.js.map.gz
Normal file
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user