mirror of
https://github.com/ZSCGR/CloudFlare-ImgBed.git
synced 2026-08-13 04:03:42 +08:00
Add Features: 支持目录功能
支持目录功能
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# CloudFlare-ImgBed
|
||||
|
||||
开源图片/文件托管解决方案,基于 Cloudflare Pages,支持 Telegram Bot 、 Cloudflare R2 ,S3 API 等多种不同存储渠道。
|
||||
开源文件托管解决方案,基于 Cloudflare Pages,支持 Telegram Bot 、 Cloudflare R2 ,S3 API 等多种不同存储渠道,支持目录功能。
|
||||
|
||||
**前端仓库**:[MarSeventh/Sanyue-ImgHub](https://github.com/MarSeventh/Sanyue-ImgHub)
|
||||
|
||||
@@ -70,10 +70,42 @@
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
## 最近更新 2025.3.7
|
||||
|
||||
Add Features:
|
||||
|
||||
- **目录功能上线啦(感谢[fantasy-ke](https://github.com/fantasy-ke)协助)**,当前支持:
|
||||
- 上传到指定目录
|
||||
- 整目录删除
|
||||
- 文件位置移动
|
||||
- 按目录读取文件
|
||||
|
||||
Fix Bugs:
|
||||
|
||||
- 修复多项影响体验的bug
|
||||
|
||||
|
||||
|
||||
<details>
|
||||
<summary>更新日志</summary>
|
||||
|
||||
|
||||
## 2025.3.7
|
||||
|
||||
Add Features:
|
||||
|
||||
- **目录功能上线啦**,当前支持:
|
||||
- 上传到指定目录
|
||||
- 整目录删除
|
||||
- 文件位置移动( Telegraph 和旧版 Telegram 渠道不支持移动)
|
||||
- 按目录读取文件
|
||||
|
||||
Fix Bugs:
|
||||
|
||||
- 修复多项影响体验的bug
|
||||
|
||||
## 2025.3.1
|
||||
|
||||
Add Features:
|
||||
@@ -807,7 +839,7 @@ Web端在登录页面输入你的**认证码**即可登录使用;API端需要
|
||||
| ------------ | ------------------------------------------------------------ |
|
||||
| **接口功能** | 上传图片或视频 |
|
||||
| **请求方法** | POST |
|
||||
| **请求参数** | **Query参数**:<br />`authCode`: string类型,即为你设置的认证码<br />`serverCompress`: boolean类型,表示是否开启服务端压缩(仅针对图片文件、Telegram上传渠道生效,默认为`true`)<br />`uploadChannel`: string类型,取值为`telegram`和`cfr2`,分别代表telegram bot渠道和Cloudflare R2渠道,默认为`telegram` 渠道<br />`autoRetry`: boolean类型,表示是否开启上传失败自动切换渠道重试,默认开启<br />`uploadNameType`: string类型,表示文件命名方式,可选值为`[default, index, origin, short]`,分别代表默认`前缀_原名`命名、`仅前缀`命名、`仅原名`命名和`短链接`命名法,默认为`default`<br />`returnFormat`:string类型,表示返回链接格式,可选值为`[default, full]`,分别代表默认的`/file/id`格式、完整链接格式<br />**Body参数(application/form-data)**:<br />`file`: file类型,你要上传的文件 |
|
||||
| **请求参数** | **Query参数**:<br />`authCode`: string类型,即为你设置的认证码<br />`serverCompress`: boolean类型,表示是否开启服务端压缩(仅针对图片文件、Telegram上传渠道生效,默认为`true`)<br />`uploadChannel`: string类型,取值为`telegram`和`cfr2`,分别代表telegram bot渠道和Cloudflare R2渠道,默认为`telegram` 渠道<br />`autoRetry`: boolean类型,表示是否开启上传失败自动切换渠道重试,默认开启<br />`uploadNameType`: string类型,表示文件命名方式,可选值为`[default, index, origin, short]`,分别代表默认`前缀_原名`命名、`仅前缀`命名、`仅原名`命名和`短链接`命名法,默认为`default`<br />`returnFormat`:string类型,表示返回链接格式,可选值为`[default, full]`,分别代表默认的`/file/id`格式、完整链接格式<br />`uploadFolder`:string类型,指定上传目录,用相对路径表示,例如上传到img/test目录需填`img/test`<br />**Body参数(application/form-data)**:<br />`file`: file类型,你要上传的文件 |
|
||||
| **返回响应** | `data[0].src`为获得的图片链接(注意不包含域名,需要自己添加) |
|
||||
|
||||
> **请求示例**:
|
||||
@@ -904,7 +936,12 @@ Web端在登录页面输入你的**认证码**即可登录使用;API端需要
|
||||
28. :white_check_mark:~~开放更多文件格式~~(2024.12.9已完成)
|
||||
29. :white_check_mark:~~进行删除、加入白名单、加入黑名单等操作时,自动清除CF CDN缓存,避免延迟生效~~(2024.12.11已完成)
|
||||
30. :white_check_mark:~~管理端批量选择时,记录用户选择的顺序~~(2024.12.20已完成)
|
||||
31. :memo:上传图片支持自定义上传路径,支持相册功能(评估中)
|
||||
31. :memo:上传图片支持自定义上传路径,支持相册功能
|
||||
- ~~文件夹删除功能~~(2025.3.6已完成)
|
||||
- ~~文件位置移动功能~~(2025.3.7已完成)
|
||||
- ~~管理端加载更多数据时鬼打墙问题修复~~(2025.3.6已完成)
|
||||
- ~~管理端批量操作适配文件夹~~(2025.3.6已完成)
|
||||
- ~~管理端分页逻辑调整~~(2025.3.6已完成)
|
||||
32. :white_check_mark:~~支持多个 Telegram Bot Token 负载均衡~~(2025.2.4已完成)
|
||||
33. :white_check_mark:~~管理端提供详细的设置信息和设置方式引导~~(2025.2.5已完成)
|
||||
34. :white_check_mark:~~Logo焕新、登录页面优化、设置提示项等多项展示效果优化~~(2025.2.2已完成)
|
||||
|
||||
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.
Binary file not shown.
@@ -13,17 +13,21 @@ export async function onRequest(context) {
|
||||
|
||||
// 组装 CDN URL
|
||||
const url = new URL(request.url);
|
||||
const cdnUrl = `https://${url.hostname}/file/${params.id}`;
|
||||
|
||||
// 解码params.id
|
||||
params.id = decodeURIComponent(params.id);
|
||||
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);
|
||||
|
||||
//read the metadata
|
||||
const value = await env.img_url.getWithMetadata(params.id);
|
||||
const value = await env.img_url.getWithMetadata(params.path);
|
||||
|
||||
//change the metadata
|
||||
value.metadata.ListType = "Block"
|
||||
await env.img_url.put(params.id,"",{metadata: value.metadata});
|
||||
await env.img_url.put(params.path,"",{metadata: value.metadata});
|
||||
const info = JSON.stringify(value.metadata);
|
||||
|
||||
// 清除CDN缓存
|
||||
@@ -11,20 +11,71 @@ export async function onRequest(context) {
|
||||
next, // used for middleware or to fetch assets
|
||||
data, // arbitrary space for passing data between middlewares
|
||||
} = context;
|
||||
// 组装 CDN URL
|
||||
const url = new URL(request.url);
|
||||
const cdnUrl = `https://${url.hostname}/file/${params.id}`;
|
||||
|
||||
// 解码params.id
|
||||
params.id = decodeURIComponent(params.id);
|
||||
const url = new URL(request.url);
|
||||
|
||||
// 读取folder参数,判断是否为文件夹删除请求
|
||||
const folder = url.searchParams.get('folder');
|
||||
if (folder === 'true') {
|
||||
try {
|
||||
// 调用list API获取指定目录下的所有文件
|
||||
const folderPath = params.path.join('/');
|
||||
|
||||
const listUrl = new URL(`${url.origin}/api/manage/list?count=-1&dir=${folderPath}`);
|
||||
const listRequest = new Request(listUrl, request);
|
||||
|
||||
const listResponse = await fetch(listRequest);
|
||||
const listData = await listResponse.json();
|
||||
|
||||
const files = listData.files;
|
||||
// 调用delete API删除文件夹下的所有文件
|
||||
for (const file of files) {
|
||||
const deleteUrl = new URL(`${url.origin}/api/manage/delete/${file.name}`);
|
||||
const deleteRequest = new Request(deleteUrl, request);
|
||||
|
||||
await fetch(deleteRequest);
|
||||
}
|
||||
|
||||
// 调用delete API删除所有子文件夹
|
||||
const directories = listData.directories;
|
||||
for (const dir of directories) {
|
||||
const deleteUrl = new URL(`${url.origin}/api/manage/delete/${dir}?folder=true`);
|
||||
const deleteRequest = new Request(deleteUrl, request);
|
||||
|
||||
await fetch(deleteRequest);
|
||||
}
|
||||
|
||||
// 返回成功信息
|
||||
return new Response('Folder Deleted');
|
||||
|
||||
} catch (e) {
|
||||
return new Response('Error: Delete Folder Failed', { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
// 组装 CDN URL
|
||||
const cdnPath = url.pathname.replace('/api/manage/delete/', '');
|
||||
const cdnUrl = `https://${url.hostname}/file/${cdnPath}`;
|
||||
|
||||
// 从params中获取图片ID
|
||||
let fileId = '';
|
||||
try {
|
||||
// 解码params.path
|
||||
params.path = decodeURIComponent(params.path);
|
||||
// 从path中提取文件ID
|
||||
fileId = params.path.split(',').join('/');
|
||||
} catch (e) {
|
||||
return new Response('Error: Decode Image ID Failed', { status: 400 });
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 读取图片信息
|
||||
const img = await env.img_url.getWithMetadata(params.id);
|
||||
const img = await env.img_url.getWithMetadata(fileId);
|
||||
|
||||
// 如果是R2渠道的图片,删除R2中对应的图片
|
||||
if (img.metadata?.Channel === 'CloudflareR2') {
|
||||
await env.img_r2.delete(params.id);
|
||||
await env.img_r2.delete(fileId);
|
||||
}
|
||||
|
||||
// S3 渠道的图片,删除S3中对应的图片
|
||||
@@ -55,8 +106,8 @@ export async function onRequest(context) {
|
||||
}
|
||||
|
||||
// 删除KV中的图片信息
|
||||
await env.img_url.delete(params.id);
|
||||
const info = JSON.stringify(params.id);
|
||||
await env.img_url.delete(fileId);
|
||||
const info = JSON.stringify(fileId);
|
||||
|
||||
// 清除CDN缓存
|
||||
await purgeCFCache(env, cdnUrl);
|
||||
@@ -1,78 +1,101 @@
|
||||
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;
|
||||
const { request, env } = context;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// 解析 URL 中的参数
|
||||
let start = parseInt(url.searchParams.get('start'), 10) || 0;
|
||||
let count = parseInt(url.searchParams.get('count'), 10) || 50;
|
||||
let sum = url.searchParams.get('sum') || false;
|
||||
|
||||
// count 为 -1 时,返回所有数据
|
||||
if (count === -1) {
|
||||
const allRecords = await getAllRecords(env);
|
||||
// sum 参数为 true 时,只返回数据总数
|
||||
if (sum === 'true') {
|
||||
return new Response(JSON.stringify(
|
||||
{
|
||||
sum: allRecords.length
|
||||
}
|
||||
), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
} else {
|
||||
return new Response(JSON.stringify(allRecords), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
let dir = url.searchParams.get('dir') || ''; // 目录名
|
||||
// 相对路径
|
||||
if (dir.startsWith('/')) {
|
||||
dir = dir.substring(1);
|
||||
}
|
||||
if (dir !== '' && !dir.endsWith('/')) {
|
||||
dir += '/';
|
||||
}
|
||||
|
||||
// 倒序返回指定数量的数据
|
||||
let allRecords = await getAllRecords(env, dir);
|
||||
|
||||
start = Math.max(0, start); // start 不能小于 0
|
||||
count = Math.max(1, count); // count 不能小于 1
|
||||
|
||||
let allRecords = [];
|
||||
|
||||
allRecords = await getAllRecords(env);
|
||||
|
||||
// 按照 metadata 中的时间戳倒序排序
|
||||
allRecords.sort((a, b) => {
|
||||
return b.metadata.TimeStamp - a.metadata.TimeStamp;
|
||||
});
|
||||
|
||||
const resultRecords = allRecords.slice(start, start + count);
|
||||
// 解析目录下的文件和子目录
|
||||
let filteredRecords = [];
|
||||
let subdirectories = new Set();
|
||||
|
||||
// 只返回 `count` 条数据
|
||||
return new Response(JSON.stringify(resultRecords), {
|
||||
for (let record of allRecords) {
|
||||
let key = record.name;
|
||||
if (key.startsWith(dir)) {
|
||||
let relativePath = key.substring(dir.length);
|
||||
if (relativePath.startsWith('/')) {
|
||||
relativePath = relativePath.substring(1);
|
||||
}
|
||||
|
||||
let parts = relativePath.split('/');
|
||||
if (parts.length === 1) {
|
||||
// 直接位于该目录的文件
|
||||
filteredRecords.push(record);
|
||||
} else {
|
||||
// 该目录下的子文件夹
|
||||
if (dir === '' || dir.endsWith('/')) {
|
||||
subdirectories.add(dir + parts[0]);
|
||||
} else {
|
||||
subdirectories.add(dir + '/' + parts[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// sum 参数为 true 时,只返回数据总数
|
||||
if (count === -1 && sum === 'true') {
|
||||
return new Response(JSON.stringify({ sum: filteredRecords.length }), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
|
||||
async function getAllRecords(env) {
|
||||
let recordsFetched = 0;
|
||||
// count 为 -1 时返回所有数据
|
||||
if (count === -1) {
|
||||
return new Response(JSON.stringify({
|
||||
files: filteredRecords,
|
||||
directories: Array.from(subdirectories)
|
||||
}), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
|
||||
// 进行分页
|
||||
start = Math.max(0, start);
|
||||
count = Math.max(1, count);
|
||||
const resultRecords = filteredRecords.slice(start, start + count);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
files: resultRecords,
|
||||
directories: Array.from(subdirectories)
|
||||
}), {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
|
||||
async function getAllRecords(env, dir) {
|
||||
// 按前缀列出所有文件
|
||||
let allRecords = [];
|
||||
let cursor = null;
|
||||
|
||||
while (true) {
|
||||
const limit = 1000; // 读取所需的最少数量
|
||||
const response = await env.img_url.list({ limit, cursor });
|
||||
|
||||
// 过滤掉以 "manage@" 开头的 key
|
||||
const filteredRecords = response.keys.filter(item => !item.name.startsWith("manage@"));
|
||||
|
||||
allRecords.push(...filteredRecords);
|
||||
recordsFetched += filteredRecords.length;
|
||||
const limit = 1000;
|
||||
const response = await env.img_url.list({
|
||||
prefix: dir,
|
||||
limit: limit,
|
||||
cursor: cursor
|
||||
});
|
||||
cursor = response.cursor;
|
||||
|
||||
if (!cursor) {
|
||||
const filteredRecords = response.keys.filter(item => !item.name.startsWith("manage@"));
|
||||
allRecords.push(...filteredRecords);
|
||||
|
||||
if (!cursor || filteredRecords.length < limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { S3Client, CopyObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { purgeCFCache } from "../../../utils/purgeCache";
|
||||
|
||||
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;
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
// 读取目标文件夹
|
||||
const dist = url.searchParams.get('dist')
|
||||
? url.searchParams.get('dist').replace(/^\/+/, '') // 移除开头的/
|
||||
.replace(/\/{2,}/g, '/') // 替换多个连续的/为单个/
|
||||
.replace(/\/$/, '') // 移除末尾的/
|
||||
: '';
|
||||
|
||||
// 读取folder参数,判断是否为文件夹删除请求
|
||||
const folder = url.searchParams.get('folder');
|
||||
if (folder === 'true') {
|
||||
try {
|
||||
// 获取当前文件夹名称
|
||||
const curFolder = params.path[params.path.length - 1];
|
||||
|
||||
// 调用list API获取指定目录下的所有文件
|
||||
const folderPath = params.path.join('/');
|
||||
|
||||
const listUrl = new URL(`${url.origin}/api/manage/list?count=-1&dir=${folderPath}`);
|
||||
const listRequest = new Request(listUrl, request);
|
||||
|
||||
const listResponse = await fetch(listRequest);
|
||||
const listData = await listResponse.json();
|
||||
|
||||
const files = listData.files;
|
||||
|
||||
const folderDist = dist === '' ? curFolder : `${dist}/${curFolder}`;
|
||||
// 调用move API移动文件夹下的所有文件
|
||||
for (const file of files) {
|
||||
const moveUrl = new URL(`${url.origin}/api/manage/move/${file.name}?dist=${folderDist}`);
|
||||
const moveRequest = new Request(moveUrl, request);
|
||||
|
||||
await fetch(moveRequest);
|
||||
}
|
||||
|
||||
const directories = listData.directories;
|
||||
|
||||
// 调用move API移动所有子文件夹
|
||||
for (const dir of directories) {
|
||||
const moveUrl = new URL(`${url.origin}/api/manage/move/${dir}?dist=${folderDist}&folder=true`);
|
||||
const moveRequest = new Request(moveUrl, request);
|
||||
|
||||
await fetch(moveRequest);
|
||||
}
|
||||
|
||||
// 返回成功信息
|
||||
return new Response('Folder Moved');
|
||||
|
||||
} catch (e) {
|
||||
return new Response('Error: Move Folder Failed', { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
// 组装 CDN URL
|
||||
const cdnPath = url.pathname.replace('/api/manage/move/', '');
|
||||
const cdnUrl = `https://${url.hostname}/file/${cdnPath}`;
|
||||
|
||||
// 从params中获取图片ID
|
||||
let fileId = '';
|
||||
try {
|
||||
// 解码params.path
|
||||
params.path = decodeURIComponent(params.path);
|
||||
// 从path中提取文件ID
|
||||
fileId = params.path.split(',').join('/');
|
||||
} catch (e) {
|
||||
return new Response('Error: Decode Image ID Failed', { status: 400 });
|
||||
}
|
||||
|
||||
// 读取文件名
|
||||
const fileKey = fileId.split('/').pop();
|
||||
const newFileId = dist === '' ? fileKey : `${dist}/${fileKey}`;
|
||||
|
||||
try {
|
||||
// 读取图片信息
|
||||
const img = await env.img_url.getWithMetadata(fileId);
|
||||
|
||||
// 如果是R2渠道的图片,需要移动R2中对应的图片
|
||||
if (img.metadata?.Channel === 'CloudflareR2') {
|
||||
const R2DataBase = env.img_r2;
|
||||
|
||||
// 获取原文件内容
|
||||
const object = await R2DataBase.get(fileId);
|
||||
if (!object) {
|
||||
return new Response('Error: R2 Object Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
// 复制到新位置
|
||||
await R2DataBase.put(newFileId, object.body);
|
||||
|
||||
// 删除旧文件
|
||||
await R2DataBase.delete(fileId);
|
||||
}
|
||||
|
||||
// S3 渠道的图片,需要移动S3中对应的图片
|
||||
if (img.metadata?.Channel === 'S3') {
|
||||
const { success, newKey, error } = await moveS3File(img, newFileId);
|
||||
if (success) {
|
||||
// 更新 metadata
|
||||
img.metadata.S3FileKey = newFileId;
|
||||
|
||||
const s3ServerDomain = img.metadata.S3Endpoint.replace(/https?:\/\//, "");
|
||||
img.metadata.S3Location = `https://${img.metadata.S3BucketName}.${s3ServerDomain}/${newKey}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 旧版 Telegram 渠道和 Telegraph 渠道不支持移动
|
||||
if (img.metadata?.Channel === 'Telegram' || img.metadata?.Channel === undefined) {
|
||||
return new Response('Error: Move Image Failed', { status: 400 });
|
||||
}
|
||||
|
||||
|
||||
// 其他渠道,直接修改KV中的id为newFileId
|
||||
await env.img_url.put(newFileId, img.value, { metadata: img.metadata });
|
||||
|
||||
|
||||
// 删除原有图片
|
||||
await env.img_url.delete(fileId);
|
||||
|
||||
const info = JSON.stringify(fileId);
|
||||
|
||||
// 清除CDN缓存
|
||||
await purgeCFCache(env, cdnUrl);
|
||||
|
||||
// 清除api/randomFileList API缓存
|
||||
try {
|
||||
const cache = caches.default;
|
||||
// 通过写入一个max-age=0的response来清除缓存
|
||||
const nullResponse = new Response(null, {
|
||||
headers: { 'Cache-Control': 'max-age=0' },
|
||||
});
|
||||
await cache.put(`${url.origin}/api/randomFileList`, nullResponse);
|
||||
} catch (error) {
|
||||
console.error('Failed to clear cache:', error);
|
||||
}
|
||||
|
||||
return new Response(info);
|
||||
} catch (e) {
|
||||
return new Response('Error: Move Image Failed', { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
// 移动 S3 渠道的图片
|
||||
async function moveS3File(img, newFileId) {
|
||||
const s3Client = new S3Client({
|
||||
region: img.metadata?.S3Region || "auto",
|
||||
endpoint: img.metadata?.S3Endpoint,
|
||||
credentials: {
|
||||
accessKeyId: img.metadata?.S3AccessKeyId,
|
||||
secretAccessKey: img.metadata?.S3SecretAccessKey
|
||||
},
|
||||
});
|
||||
|
||||
const bucketName = img.metadata?.S3BucketName;
|
||||
const oldKey = img.metadata?.S3FileKey;
|
||||
const newKey = newFileId;
|
||||
|
||||
try {
|
||||
// 复制文件到新位置
|
||||
await s3Client.send(new CopyObjectCommand({
|
||||
Bucket: bucketName,
|
||||
CopySource: `/${bucketName}/${oldKey}`,
|
||||
Key: newKey,
|
||||
}));
|
||||
|
||||
// 复制成功后,删除旧文件
|
||||
await s3Client.send(new DeleteObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: oldKey,
|
||||
}));
|
||||
|
||||
// 返回新的 S3 文件信息
|
||||
return { success: true, newKey };
|
||||
} catch (error) {
|
||||
console.error("S3 Move Failed:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
@@ -10,19 +10,24 @@ export async function onRequest(context) {
|
||||
next, // used for middleware or to fetch assets
|
||||
data, // arbitrary space for passing data between middlewares
|
||||
} = context;
|
||||
|
||||
// 组装 CDN URL
|
||||
const url = new URL(request.url);
|
||||
const cdnUrl = `https://${url.hostname}/file/${params.id}`;
|
||||
|
||||
// 解码params.id
|
||||
params.id = decodeURIComponent(params.id);
|
||||
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);
|
||||
|
||||
//read the metadata
|
||||
const value = await env.img_url.getWithMetadata(params.id);
|
||||
const value = await env.img_url.getWithMetadata(params.path);
|
||||
|
||||
//change the metadata
|
||||
value.metadata.ListType = "White"
|
||||
await env.img_url.put(params.id,"",{metadata: value.metadata});
|
||||
await env.img_url.put(params.path,"",{metadata: value.metadata});
|
||||
const info = JSON.stringify(value.metadata);
|
||||
|
||||
// 清除CDN缓存
|
||||
@@ -16,9 +16,13 @@ export async function onRequest(context) { // Contents of context object
|
||||
data, // arbitrary space for passing data between middlewares
|
||||
} = context;
|
||||
|
||||
let fileId = '';
|
||||
try {
|
||||
// 解码params.id
|
||||
params.id = decodeURIComponent(params.id);
|
||||
// 解码params.path
|
||||
params.path = decodeURIComponent(params.path);
|
||||
// 从path中提取文件ID
|
||||
fileId = params.path.split(',').join('/');
|
||||
|
||||
} catch (e) {
|
||||
return new Response('Error: Decode Image ID Failed', { status: 400 });
|
||||
}
|
||||
@@ -29,13 +33,13 @@ export async function onRequest(context) { // Contents of context object
|
||||
whiteListMode = securityConfig.access.whiteListMode;
|
||||
|
||||
const url = new URL(request.url);
|
||||
let Referer = request.headers.get('Referer');
|
||||
let Referer = request.headers.get('Referer')
|
||||
if (Referer) {
|
||||
try {
|
||||
let refererUrl = new URL(Referer);
|
||||
if (allowedDomains && allowedDomains.trim() !== '') {
|
||||
let allowedDomainsList = allowedDomains.split(',');
|
||||
let isAllowed = allowedDomainsList.some(domain => {
|
||||
const domains = allowedDomains.split(',');
|
||||
let isAllowed = domains.some(domain => {
|
||||
let domainPattern = new RegExp(`(^|\\.)${domain.replace('.', '\\.')}$`); // Escape dot in domain
|
||||
return domainPattern.test(refererUrl.hostname);
|
||||
});
|
||||
@@ -51,10 +55,16 @@ export async function onRequest(context) { // Contents of context object
|
||||
if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") {
|
||||
return new Response('Error: Please configure KV database', { status: 500 });
|
||||
}
|
||||
const imgRecord = await env.img_url.getWithMetadata(params.id);
|
||||
// 如果meatdata不存在,只可能是之前未设置KV,且存储在Telegraph上的图片,那么在后面获取时会返回404错误,此处不用处理
|
||||
const imgRecord = await env.img_url.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 || params.id;
|
||||
const fileName = imgRecord.metadata?.FileName || fileId;
|
||||
const encodedFileName = encodeURIComponent(fileName);
|
||||
const fileType = imgRecord.metadata?.FileType || null;
|
||||
|
||||
@@ -64,8 +74,6 @@ export async function onRequest(context) { // Contents of context object
|
||||
return accessRes; // 如果不可访问,直接返回
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Cloudflare R2渠道
|
||||
if (imgRecord.metadata?.Channel === 'CloudflareR2') {
|
||||
// 检查是否配置了R2
|
||||
@@ -74,7 +82,7 @@ export async function onRequest(context) { // Contents of context object
|
||||
}
|
||||
|
||||
const R2DataBase = env.img_r2;
|
||||
const object = await R2DataBase.get(params.id);
|
||||
const object = await R2DataBase.get(fileId);
|
||||
|
||||
if (object === null) {
|
||||
return new Response('Error: Failed to fetch image', { status: 500 });
|
||||
@@ -103,7 +111,6 @@ export async function onRequest(context) { // Contents of context object
|
||||
return newRes;
|
||||
}
|
||||
|
||||
|
||||
// S3渠道
|
||||
if (imgRecord.metadata?.Channel === "S3") {
|
||||
const s3Client = new S3Client({
|
||||
@@ -151,20 +158,17 @@ export async function onRequest(context) { // Contents of context object
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 外链渠道
|
||||
if (imgRecord.metadata?.Channel === 'External') {
|
||||
// 直接重定向到外链
|
||||
return Response.redirect(imgRecord.metadata?.ExternalLink, 302);
|
||||
}
|
||||
|
||||
|
||||
// Telegram及Telegraph渠道
|
||||
let TgFileID = ''; // Tg的file_id
|
||||
if (imgRecord.metadata?.Channel === 'Telegram') {
|
||||
// id为file_id + ext
|
||||
TgFileID = params.id.split('.')[0];
|
||||
TgFileID = fileId.split('.')[0];
|
||||
} else if (imgRecord.metadata?.Channel === 'TelegramNew') {
|
||||
// id为unique_id + file_name
|
||||
TgFileID = imgRecord.metadata?.TgFileId;
|
||||
@@ -174,7 +178,6 @@ export async function onRequest(context) { // Contents of context object
|
||||
} else {
|
||||
// 旧版telegraph
|
||||
}
|
||||
|
||||
// 构建目标 URL
|
||||
if (isTgChannel(imgRecord)) {
|
||||
// 获取TG图片真实地址
|
||||
@@ -187,14 +190,12 @@ export async function onRequest(context) { // Contents of context object
|
||||
} else {
|
||||
targetUrl = 'https://telegra.ph/' + url.pathname + url.search;
|
||||
}
|
||||
|
||||
const response = await getFileContent(request);
|
||||
if (response === null) {
|
||||
return new Response('Error: Failed to fetch image', { status: 500 });
|
||||
} else if (response.status === 404) {
|
||||
return await return404(url);
|
||||
}
|
||||
|
||||
try {
|
||||
const headers = new Headers(response.headers);
|
||||
headers.set('Content-Disposition', `inline; filename="${encodedFileName}"; filename*=UTF-8''${encodedFileName}`);
|
||||
@@ -208,13 +209,11 @@ export async function onRequest(context) { // Contents of context object
|
||||
} else {
|
||||
headers.set('Cache-Control', 'public, max-age=604800');
|
||||
}
|
||||
|
||||
const newRes = new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return newRes;
|
||||
}
|
||||
+19
-11
@@ -97,6 +97,15 @@ export async function onRequestPost(context) { // Contents of context object
|
||||
|
||||
// 获得上传渠道
|
||||
const urlParamUploadChannel = url.searchParams.get('uploadChannel');
|
||||
// 获取上传文件夹路径
|
||||
const uploadFolder = url.searchParams.get('uploadFolder') || '';
|
||||
// 处理文件夹路径格式,确保没有开头的/
|
||||
const normalizedFolder = uploadFolder
|
||||
? uploadFolder.replace(/^\/+/, '') // 移除开头的/
|
||||
.replace(/\/{2,}/g, '/') // 替换多个连续的/为单个/
|
||||
.replace(/\/$/, '') // 移除末尾的/
|
||||
: '';
|
||||
|
||||
let uploadChannel = 'TelegramNew';
|
||||
switch (urlParamUploadChannel) {
|
||||
case 'telegram':
|
||||
@@ -141,6 +150,7 @@ export async function onRequestPost(context) { // Contents of context object
|
||||
ListType: "None",
|
||||
TimeStamp: time,
|
||||
Label: "None",
|
||||
Folder: normalizedFolder || 'root',
|
||||
}
|
||||
|
||||
|
||||
@@ -164,24 +174,21 @@ export async function onRequestPost(context) { // Contents of context object
|
||||
const unique_index = time + Math.floor(Math.random() * 10000);
|
||||
let fullId = '';
|
||||
if (nameType === 'index') {
|
||||
// 仅前缀
|
||||
fullId = unique_index + '.' + fileExt;
|
||||
// 只在 normalizedFolder 非空时添加路径
|
||||
fullId = normalizedFolder ? `${normalizedFolder}/${unique_index}.${fileExt}` : `${unique_index}.${fileExt}`;
|
||||
} else if (nameType === 'origin') {
|
||||
// 仅文件名
|
||||
fullId = fileName? fileName : unique_index + '.' + fileExt;
|
||||
fullId = normalizedFolder ? `${normalizedFolder}/${fileName}` : fileName;
|
||||
} else if (nameType === 'short') {
|
||||
// 短链接,8位大小写字母+数字的随机字符
|
||||
while (true) {
|
||||
const shortId = generateShortId(8);
|
||||
const testFullId = shortId + '.' + fileExt;
|
||||
const testFullId = normalizedFolder ? `${normalizedFolder}/${shortId}.${fileExt}` : `${shortId}.${fileExt}`;
|
||||
if (await env.img_url.get(testFullId) === null) {
|
||||
fullId = shortId + '.' + fileExt;
|
||||
fullId = testFullId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 默认方式:前缀+文件名
|
||||
fullId = fileName? unique_index + '_' + fileName : unique_index + '.' + fileExt;
|
||||
fullId = normalizedFolder ? `${normalizedFolder}/${unique_index}_${fileName}` : `${unique_index}_${fileName}`;
|
||||
}
|
||||
|
||||
// 获得返回链接格式, default为返回/file/id, full为返回完整链接
|
||||
@@ -213,7 +220,7 @@ export async function onRequestPost(context) { // Contents of context object
|
||||
err = await res.text();
|
||||
}
|
||||
} else if (uploadChannel === 'S3') {
|
||||
// -------------S3 渠道---------------
|
||||
// ---------------------S3 渠道------------------
|
||||
const res = await uploadFileToS3(env, formdata, fullId, metadata, returnLink, url);
|
||||
if (res.status === 200 || !autoRetry) {
|
||||
return res;
|
||||
@@ -221,7 +228,7 @@ export async function onRequestPost(context) { // Contents of context object
|
||||
err = await res.text();
|
||||
}
|
||||
} else if (uploadChannel === 'External') {
|
||||
// -------------外链渠道---------------
|
||||
// --------------------外链渠道----------------------
|
||||
const res = await uploadFileToExternal(env, formdata, fullId, metadata, returnLink, url);
|
||||
return res;
|
||||
} else {
|
||||
@@ -560,6 +567,7 @@ async function uploadFileToExternal(env, formdata, fullId, metadata, returnLink,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 图像审查
|
||||
async function moderateContent(env, url, metadata) {
|
||||
const apikey = moderateContentApiKey;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<!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"><title>Sanyue ImgHub</title><script defer="defer" src="/js/app.38dcb97d.js"></script><link href="/css/app.9a1a6b51.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><style>/* 下拉菜单样式 */
|
||||
<!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"><title>Sanyue ImgHub</title><script defer="defer" src="/js/app.1854c0a3.js"></script><link href="/css/app.9a1a6b51.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><style>/* 下拉菜单样式 */
|
||||
.el-dropdown__popper.el-popper {
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
|
||||
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
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.
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
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user