Skip to content

Commit

Permalink
管理端支持拉黑上传ip;管理端批量操作支持按照用户选择的顺序进行;random接口优化
Browse files Browse the repository at this point in the history
  • Loading branch information
MarSeventh committed Dec 20, 2024
1 parent 6d0ecd1 commit 303a80f
Show file tree
Hide file tree
Showing 61 changed files with 282 additions and 54 deletions.
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@
<summary>更新日志</summary>


## 2024.12.20

Add Features:

- 管理端支持拉黑上传IP(Dashboard->用户管理->允许上传)
- 管理端批量操作支持按照用户选择的顺序进行([#issue124](https://github.com/MarSeventh/CloudFlare-ImgBed/issues/124)
- `random`接口优化,减少KV操作次数,增加`content`参数,支持返回指定类型的文件

## 2024.12.14

Add Features:
Expand Down Expand Up @@ -246,7 +254,7 @@ Add Features:
- **支持身份认证、防滥用**
- 支持Web和API**上传认证**(感谢[hl128k](https://github.com/hl128k)
- 支持访问域名限制(感谢[hl128k](https://github.com/hl128k)
- 支持上传IP统计
- 支持上传IP统计,支持禁止指定IP上传

- **支持页面自定义**
- **背景自定义**
Expand Down Expand Up @@ -486,7 +494,7 @@ Web端在登录页面输入你的**认证码**即可登录使用;API端需要

环境变量增加`WhiteList_Mode`,设置为`true`即可开启白名单模式,仅设置为白名单的图片可被访问。

#### 3.1.3.6页面自定义(DIY接口)
#### 3.1.3.6自定义配置接口

<details>
<summary>设置方式</summary>
Expand Down Expand Up @@ -532,6 +540,8 @@ Web端在登录页面输入你的**认证码**即可登录使用;API端需要
设置`AllowRandom`环境变量,值为`true`,以从图床中随机获取一张图片,详见[API文档](#4.2.2随机图API)。
**警告**:为了减少KV的操作次数,一旦启用随机图API,**所有存储的图片/视频链接会被CDN缓存**(仅缓存文件的路径,其他信息不会缓存),因此请谨慎启用该功能。
#### 3.1.3.9管理端删除、拉黑等操作优化
正常情况下,因为CloudFlare CDN缓存的存在,在管理端进行删除、拉黑、加白名单等操作不会立即生效,需要等到缓存过期才能生效。
Expand Down Expand Up @@ -634,9 +644,9 @@ Web端在登录页面输入你的**认证码**即可登录使用;API端需要
| 接口名称 | /random |
| ------------ | ------------------------------------------------------------ |
| **接口功能** | 从图床中随机返回一张图片的链接(注意会消耗列出次数) |
| **前置条件** | 设置`AllowRandom`环境变量,值为`true` |
| **前置条件** | 设置`AllowRandom`环境变量,值为`true`<br />**注意**:为了减少KV的操作次数,一旦启用随机图API,**所有存储的图片/视频链接会被CDN缓存**(仅缓存文件的路径,其他信息不会缓存),因此请谨慎启用该功能。 |
| **请求方法** | GET |
| **请求参数** | **Query参数**:<br />`type`: 设为`img`时直接返回图片(此时form不生效);设为`url`时返回完整url链接;否则返回随机图的文件路径。<br />`form`: 设为`text`时直接返回文本,否则返回json格式内容。 |
| **请求参数** | **Query参数**:<br />`content`:返回的文件类型,可选值有`[image, video]`,多个使用`,`分隔,默认为`image`<br />`type`: 设为`img`时直接返回图片(此时form不生效);设为`url`时返回完整url链接;默认返回随机图的文件路径。<br />`form`: 设为`text`时直接返回文本,默认返回json格式内容。 |
| **响应格式** | 1、当`type`为`img`时:<br />返回格式为`image/jpeg`<br />2、当`type`为其他值时:<br />当`form`不是`text`时,返回JSON格式内容,`data.url`为返回的链接/文件路径。<br />否则,直接返回链接/文件路径。 |
> **请求示例**:
Expand Down Expand Up @@ -681,7 +691,7 @@ Web端在登录页面输入你的**认证码**即可登录使用;API端需要
9. :white_check_mark:~~支持大于5MB的图片上传前自动压缩~~(2024.8.26已完成)
10. :white_check_mark:~~上传页面右下角工具栏样式重构,支持上传页自定义压缩(上传前+存储端)~~(2024.9.28已完成)
11. :hourglass_flowing_sand:重构管理端,认证+显示效果优化,增加图片详情页
12. :hourglass_flowing_sand:管理端增加访问量统计,IP记录、IP黑名单、上传IP黑名单等
12. :white_check_mark:~~管理端增加访问量统计,IP记录、IP黑名单、上传IP黑名单等~~(2024.12.20已支持上传ip黑名单,访问记录由于对KV读写消耗太大,暂时搁置)
13. :white_check_mark:~~上传页面点击链接,自动复制到剪切板~~(2024.9.27已完成)
14. :white_check_mark:~~上传设置记忆(上传方式、链接格式等)~~(2024.9.27已完成,**两种上传方式合并**)
15. :white_check_mark:~~若未设置密码,无需跳转进入登录页~~(2024.9.27已完成)
Expand All @@ -699,6 +709,9 @@ Web端在登录页面输入你的**认证码**即可登录使用;API端需要
27. :hourglass_flowing_sand:支持管理员自定义全局默认链接前缀
28. :white_check_mark:~~开放更多文件格式~~(2024.12.9已完成)
29. :white_check_mark:~~进行删除、加入白名单、加入黑名单等操作时,自动清除CF CDN缓存,避免延迟生效~~(2024.12.11已完成)
30. :hourglass_flowing_sand:管理端批量选择时,记录用户选择的顺序
31. :memo:上传图片支持自定义上传路径,支持相册功能(评估中)
32. :hourglass_flowing_sand:支持多个 Telegram Bot Token 负载均衡
</details>
Expand Down
1 change: 1 addition & 0 deletions css/272.bf5dabcf.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added css/272.bf5dabcf.css.gz
Binary file not shown.
1 change: 0 additions & 1 deletion css/529.53cae4f9.css

This file was deleted.

Binary file removed css/529.53cae4f9.css.gz
Binary file not shown.
1 change: 1 addition & 0 deletions css/538.1f895f86.css

Large diffs are not rendered by default.

Binary file added css/538.1f895f86.css.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion css/786.7bb4f38a.css → css/631.048af2d3.css

Large diffs are not rendered by default.

Binary file renamed css/786.7bb4f38a.css.gz → css/631.048af2d3.css.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion css/890.f1386e1b.css → css/659.f084e9fe.css

Large diffs are not rendered by default.

Binary file renamed css/890.f1386e1b.css.gz → css/659.f084e9fe.css.gz
Binary file not shown.
1 change: 0 additions & 1 deletion css/986.0b7ed77b.css

This file was deleted.

Binary file removed css/986.0b7ed77b.css.gz
Binary file not shown.
20 changes: 20 additions & 0 deletions functions/api/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export async function onRequestPost(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;
//从POST请求中获取authCode
const jsonRequest = await request.json();
const authCode = jsonRequest.authCode;
//验证authCode
if (env.AUTH_CODE !== undefined && authCode !== env.AUTH_CODE) {
return new Response('Unauthorized', { status: 401 })
}
//返回登录成功
return new Response('Login success', { status: 200 })
}
38 changes: 38 additions & 0 deletions functions/api/manage/cusConfig/blockip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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;
try {
// 检查是否配置了KV数据库
if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") {
return new Response('Error: Please configure KV database', { status: 500 });
}

const kv = env.img_url;
let list = await kv.get("manage@blockipList");
if (list == null) {
list = [];
} else {
list = list.split(",");
}

//从请求body中获取要block的ip
const ip = await request.text();
if (ip == null || ip == "") {
return new Response('Error: Please input ip', { status: 400 });
}

//将ip添加到list中
list.push(ip);
await kv.put("manage@blockipList", list.join(","));
return new Response('Add ip to block list successfully', { status: 200 });
} catch (e) {
return new Response('Add ip to block list failed', { status: 500 });
}
}
27 changes: 27 additions & 0 deletions functions/api/manage/cusConfig/blockipList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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;
try {
// 检查是否配置了KV数据库
if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") {
return new Response('Error: Please configure KV database', { status: 500 });
}

const kv = env.img_url;
const list = await kv.get("manage@blockipList");
if (list == null) {
return new Response('', { status: 200 });
} else {
return new Response(list, { status: 200 });
}
} catch (e) {
return new Response('fetch block ip list failed', { status: 500 });
}
}
38 changes: 38 additions & 0 deletions functions/api/manage/cusConfig/whiteip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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;
try {
// 检查是否配置了KV数据库
if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") {
return new Response('Error: Please configure KV database', { status: 500 });
}

const kv = env.img_url;
let list = await kv.get("manage@blockipList");
if (list == null) {
list = [];
} else {
list = list.split(",");
}

//从请求body中获取要white的ip
const ip = await request.text();
if (ip == null || ip == "") {
return new Response('Error: Please input ip', { status: 400 });
}

//将ip从list中删除
list = list.filter(item => item !== ip);
await kv.put("manage@blockipList", list.join(","));
return new Response('delete ip from block ip list successfully', { status: 200 });
} catch (e) {
return new Response('delete ip from block ip list failed', { status: 500 });
}
}
5 changes: 3 additions & 2 deletions functions/api/manage/delete/[id].js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ export async function onRequest(context) {
await env.img_url.delete(params.id);
const info = JSON.stringify(params.id);

// 清除CDN缓存
// 清除CDN缓存,包括图片和randomFileList接口的缓存
const randomFileListUrl = `https://${url.hostname}/api/randomFileList`;
const options = {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Auth-Email': `${env.CF_EMAIL}`, 'X-Auth-Key': `${env.CF_API_KEY}`},
body: `{"files":["${ cdnUrl }"]}`
body: `{"files":["${ cdnUrl }", "${ randomFileListUrl }"]}`
};
await fetch(`https://api.cloudflare.com/client/v4/zones/${ env.CF_ZONE_ID }/purge_cache`, options);

Expand Down
2 changes: 2 additions & 0 deletions functions/api/manage/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export async function onRequest(context) {
limit: 1000,
cursor,
});
// 除去records中key以manage@开头的记录
records.keys = records.keys.filter(item => !item.name.startsWith("manage@"));
allRecords.push(...records.keys);
cursor = records.cursor;
} while (cursor);
Expand Down
56 changes: 56 additions & 0 deletions functions/api/randomFileList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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;

// 检查是否启用了随机图功能
if (env.AllowRandom != "true") {
return new Response(JSON.stringify({ error: "Random is disabled" }), { status: 403 });
}

// 检查是否配置了KV数据库
if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") {
return new Response('Error: Please configure KV database', { status: 500 });
}

let allRecords = [];
let cursor = null;

do {
const records = await env.img_url.list({
limit: 1000,
cursor,
});
// 除去records中key以manage@开头的记录
records.keys = records.keys.filter(item => !item.name.startsWith("manage@"));
// 保留metadata中fileType为image或video的记录
records.keys = records.keys.filter(item => item.metadata?.FileType?.includes("image") || item.metadata?.FileType?.includes("video"));
allRecords.push(...records.keys);
cursor = records.cursor;
} while (cursor);

// 仅保留记录的name和metadata中的FileType字段
allRecords = allRecords.map(item => {
return {
name: item.name,
FileType: item.metadata?.FileType
}
});

// 返回所有记录,设置缓存时间为1天
const info = JSON.stringify(allRecords);
return new Response(info,
{
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=86400",
}
}
);
}
9 changes: 4 additions & 5 deletions functions/file/[id].js
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,10 @@ function isTgChannel(imgRecord) {
async function return404(url) {
const Img404 = await fetch(url.origin + "/static/404.png");
if (!Img404.ok) {
return new Response(null,
return new Response('Error: Image Not Found',
{
status: 302,
status: 404,
headers: {
"Location": url.origin + "/static/404.png",
"Cache-Control": "public, max-age=86400"
}
}
Expand All @@ -273,7 +272,7 @@ async function returnBlockImg(url) {
return new Response(null, {
status: 302,
headers: {
"Location": url.origin + "/static/BlockImg.png",
"Location": url.origin + "/blockimg",
"Cache-Control": "public, max-age=86400"
}
})
Expand All @@ -295,7 +294,7 @@ async function returnWhiteListImg(url) {
return new Response(null, {
status: 302,
headers: {
"Location": url.origin + "/static/WhiteListOn.png",
"Location": url.origin + "/whiteliston",
"Cache-Control": "public, max-age=86400"
}
})
Expand Down
48 changes: 29 additions & 19 deletions functions/random.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,53 @@ export async function onRequest(context) {
data, // arbitrary space for passing data between middlewares
} = context;
const requestUrl = new URL(request.url);
const protocol = requestUrl.protocol;
const domain = requestUrl.hostname;
const port = requestUrl.port;
let allRecords = [];
let cursor = null;
do {
const records = await env.img_url.list({
limit: 1000,
cursor,
});
allRecords.push(...records.keys);
cursor = records.cursor;
} while (cursor);

// 检查是否启用了随机图功能
if (env.AllowRandom != "true") {
return new Response(JSON.stringify({ error: "Random is disabled" }), { status: 403 });
}

// 检查是否配置了KV数据库
if (typeof env.img_url == "undefined" || env.img_url == null || env.img_url == "") {
return new Response('Error: Please configure KV database', { status: 500 });
}

// 从params中读取返回的文件类型
let fileType = requestUrl.searchParams.get('content');
if (fileType == null) {
fileType = ['image'];
} else {
fileType = fileType.split(',');
}

// 调用randomFileList接口,读取KV数据库中的所有记录
let allRecords = [];
allRecords = JSON.parse(await fetch(requestUrl.origin + '/api/randomFileList').then(res => res.text()));

// 筛选出符合fileType要求的记录
allRecords = allRecords.filter(item => { return fileType.some(type => item.FileType.includes(type)) });


if (allRecords.length == 0) {
return new Response(JSON.stringify({}), { status: 200 });
} else {
const randomIndex = Math.floor(Math.random() * allRecords.length);
const randomKey = allRecords[randomIndex];
const randomPath = '/file/' + randomKey.name;
let randomUrl = randomPath;

const randomType = requestUrl.searchParams.get('type');
const resType = requestUrl.searchParams.get('form');

// if param 'type' is set to 'url', return the full URL
if (randomType == 'url') {
if (port) {
randomUrl = protocol + '//' + domain + ':' + port + randomPath;
} else {
randomUrl = protocol + '//' + domain + randomPath;
}
randomUrl = requestUrl.origin + randomPath;
}

// if param 'type' is set to 'img', return the image
if (randomType == 'img') {
// Return an image response
randomUrl = protocol + '//' + domain + ':' + port + randomPath;
randomUrl = requestUrl.origin + randomPath;
let contentType = 'image/jpeg';
return new Response(await fetch(randomUrl).then(res => {
contentType = res.headers.get('content-type');
Expand Down
Loading

0 comments on commit 303a80f

Please sign in to comment.