Feature(custom): add upload max concurrency and upload interval setting for api

ISSUES CLOSED: #487
This commit is contained in:
Kuingsmile
2026-05-04 19:39:47 +08:00
parent 130a3904e6
commit 2441360cc6
8 changed files with 193 additions and 110 deletions

View File

@@ -22,6 +22,41 @@ const LOG_PATH = appLogPath()
const errorMessage = `upload error. see ${LOG_PATH} for more detail.` const errorMessage = `upload error. see ${LOG_PATH} for more detail.`
const deleteErrorMessage = `delete error. see ${LOG_PATH} for more detail.` const deleteErrorMessage = `delete error. see ${LOG_PATH} for more detail.`
// Upload rate-limiting state
let runningUploads = 0
const uploadWaitQueue: (() => void)[] = []
let lastUploadFinishTime = 0
async function withUploadRateLimit<T>(fn: () => Promise<T>): Promise<T> {
const allConfig = picgo.getConfig<any>() || {}
const maxConcurrency: number = allConfig.settings?.serverMaxConcurrency || 0
const uploadInterval: number = allConfig.settings?.serverUploadInterval || 0
if (maxConcurrency > 0) {
if (runningUploads >= maxConcurrency) {
await new Promise<void>(resolve => uploadWaitQueue.push(resolve))
}
runningUploads++
}
if (uploadInterval > 0) {
const wait = uploadInterval - (Date.now() - lastUploadFinishTime)
if (wait > 0) {
await new Promise(resolve => setTimeout(resolve, wait))
}
}
try {
return await fn()
} finally {
if (maxConcurrency > 0) {
runningUploads--
const next = uploadWaitQueue.shift()
if (next) next()
}
lastUploadFinishTime = Date.now()
}
}
async function responseForGet({ response }: { response: http.ServerResponse }) { async function responseForGet({ response }: { response: http.ServerResponse }) {
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }) response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
const htmlContent = marked(markdownContent) const htmlContent = marked(markdownContent)
@@ -44,6 +79,7 @@ router.post(
urlparams?: URLSearchParams urlparams?: URLSearchParams
}): Promise<void> => { }): Promise<void> => {
try { try {
await withUploadRateLimit(async () => {
const allConfig = picgo.getConfig<any>() || {} const allConfig = picgo.getConfig<any>() || {}
const picbed = urlparams?.get('picbed') const picbed = urlparams?.get('picbed')
const passedKey = urlparams?.get('key') const passedKey = urlparams?.get('key')
@@ -162,6 +198,7 @@ router.post(
if (needRestore) { if (needRestore) {
changeCurrentUploader(currentPicBedType, currentPicBedConfig, currentPicBedConfigId) changeCurrentUploader(currentPicBedType, currentPicBedConfig, currentPicBedConfigId)
} }
})
} catch (err: any) { } catch (err: any) {
logger.error(err) logger.error(err)
handleResponse({ handleResponse({

View File

@@ -51,6 +51,8 @@ export interface IConfigStruct {
deleteCloudFile: boolean deleteCloudFile: boolean
server: IServerConfig server: IServerConfig
serverKey: string serverKey: string
serverMaxConcurrency: number
serverUploadInterval: number
pasteStyle: string pasteStyle: string
aesPassword: string aesPassword: string
rename: boolean rename: boolean
@@ -146,6 +148,8 @@ export const configPaths = {
deleteCloudFile: 'settings.deleteCloudFile', deleteCloudFile: 'settings.deleteCloudFile',
server: 'settings.server', server: 'settings.server',
serverKey: 'settings.serverKey', serverKey: 'settings.serverKey',
serverMaxConcurrency: 'settings.serverMaxConcurrency',
serverUploadInterval: 'settings.serverUploadInterval',
pasteStyle: 'settings.pasteStyle', pasteStyle: 'settings.pasteStyle',
aesPassword: 'settings.aesPassword', aesPassword: 'settings.aesPassword',
rename: 'settings.rename', rename: 'settings.rename',

View File

@@ -854,9 +854,13 @@
"serverHost": "Listen Address", "serverHost": "Listen Address",
"serverKey": "Authentication Key", "serverKey": "Authentication Key",
"serverKeyPlaceholder": "Please enter the authentication key to prevent API abuse", "serverKeyPlaceholder": "Please enter the authentication key to prevent API abuse",
"serverMaxConcurrency": "Max Upload Concurrency",
"serverMaxConcurrencyPlaceholder": "Max simultaneous uploads via API (0 = unlimited)",
"serverPort": "Listen Port", "serverPort": "Listen Port",
"serverSettings": "Server Settings", "serverSettings": "Server Settings",
"serverSettingsNotice": "If you don't know the purpose of the Upload API Service, please read the documentation or do not modify the configuration.", "serverSettingsNotice": "If you don't know the purpose of the Upload API Service, please read the documentation or do not modify the configuration.",
"serverUploadInterval": "Upload Interval (ms)",
"serverUploadIntervalPlaceholder": "Delay in ms between uploads via API (0 = no delay)",
"setLog": "Set Log", "setLog": "Set Log",
"setLogDesc": "Configure log level and file size", "setLogDesc": "Configure log level and file size",
"setProxyAndMirror": "Set Proxy and Mirror Address", "setProxyAndMirror": "Set Proxy and Mirror Address",

View File

@@ -854,9 +854,13 @@
"serverHost": "监听地址", "serverHost": "监听地址",
"serverKey": "鉴权密钥", "serverKey": "鉴权密钥",
"serverKeyPlaceholder": "请输入鉴权密钥,用于防止接口滥用", "serverKeyPlaceholder": "请输入鉴权密钥,用于防止接口滥用",
"serverMaxConcurrency": "最大上传并发数",
"serverMaxConcurrencyPlaceholder": "通过API同时上传的最大数量0表示不限制",
"serverPort": "监听端口", "serverPort": "监听端口",
"serverSettings": "服务器设置", "serverSettings": "服务器设置",
"serverSettingsNotice": "如果你不知道上传API服务的作用请阅读文档或者不用修改配置。", "serverSettingsNotice": "如果你不知道上传API服务的作用请阅读文档或者不用修改配置。",
"serverUploadInterval": "上传间隔(毫秒)",
"serverUploadIntervalPlaceholder": "通过API上传时每次上传之间的延迟0表示无延迟",
"setLog": "设置日志", "setLog": "设置日志",
"setLogDesc": "配置日志等级和文件大小", "setLogDesc": "配置日志等级和文件大小",
"setProxyAndMirror": "设置代理和镜像地址", "setProxyAndMirror": "设置代理和镜像地址",

View File

@@ -854,9 +854,13 @@
"serverHost": "監聽地址", "serverHost": "監聽地址",
"serverKey": "鑑權密鑰", "serverKey": "鑑權密鑰",
"serverKeyPlaceholder": "請輸入鑑權密鑰,用於防止接口濫用", "serverKeyPlaceholder": "請輸入鑑權密鑰,用於防止接口濫用",
"serverMaxConcurrency": "最大上傳並發數",
"serverMaxConcurrencyPlaceholder": "通過API同時上傳的最大數量0表示不限制",
"serverPort": "監聽端口", "serverPort": "監聽端口",
"serverSettings": "伺服器設置", "serverSettings": "伺服器設置",
"serverSettingsNotice": "如果你不知道上傳API服務的作用請閱讀文檔或者不用修改配置。", "serverSettingsNotice": "如果你不知道上傳API服務的作用請閱讀文檔或者不用修改配置。",
"serverUploadInterval": "上傳間隔(毫秒)",
"serverUploadIntervalPlaceholder": "通過API上傳時每次上傳之間的延遲0表示無延遲",
"setLog": "設置日誌", "setLog": "設置日誌",
"setLogDesc": "配置日誌等級和文件大小", "setLogDesc": "配置日誌等級和文件大小",
"setProxyAndMirror": "設置代理和鏡像地址", "setProxyAndMirror": "設置代理和鏡像地址",

View File

@@ -1044,6 +1044,26 @@
:placeholder="t('pages.settings.advanced.serverKeyPlaceholder')" :placeholder="t('pages.settings.advanced.serverKeyPlaceholder')"
/> />
</SettingCard> </SettingCard>
<SettingCard>
<CustomInput
v-model="formOfSetting.serverMaxConcurrency"
type="number"
:min="0"
:step="1"
:title="t('pages.settings.advanced.serverMaxConcurrency')"
:placeholder="t('pages.settings.advanced.serverMaxConcurrencyPlaceholder')"
/>
</SettingCard>
<SettingCard>
<CustomInput
v-model="formOfSetting.serverUploadInterval"
type="number"
:min="0"
:step="100"
:title="t('pages.settings.advanced.serverUploadInterval')"
:placeholder="t('pages.settings.advanced.serverUploadIntervalPlaceholder')"
/>
</SettingCard>
</SettingSection> </SettingSection>
</div> </div>
<template #footer> <template #footer>
@@ -1441,6 +1461,8 @@ const formOfSetting = ref<ISettingForm>({
sinkToken: '', sinkToken: '',
deleteLocalFile: false, deleteLocalFile: false,
serverKey: '', serverKey: '',
serverMaxConcurrency: 0,
serverUploadInterval: 0,
aesPassword: 'PicList-aesPassword', aesPassword: 'PicList-aesPassword',
enableWebServer: false, enableWebServer: false,
webServerHost: '0.0.0.0', webServerHost: '0.0.0.0',
@@ -1552,6 +1574,8 @@ const autoWatchKeys = [
'webServerPort', 'webServerPort',
'webServerPath', 'webServerPath',
'serverKey', 'serverKey',
'serverMaxConcurrency',
'serverUploadInterval',
'uploadNotification', 'uploadNotification',
'uploadResultNotification', 'uploadResultNotification',
'autoCloseMainWindow', 'autoCloseMainWindow',

View File

@@ -51,6 +51,8 @@ export interface IConfigStruct {
deleteCloudFile: boolean deleteCloudFile: boolean
server: IServerConfig server: IServerConfig
serverKey: string serverKey: string
serverMaxConcurrency: number
serverUploadInterval: number
pasteStyle: string pasteStyle: string
aesPassword: string aesPassword: string
rename: boolean rename: boolean
@@ -151,6 +153,8 @@ export const configPaths = {
deleteCloudFile: 'settings.deleteCloudFile', deleteCloudFile: 'settings.deleteCloudFile',
server: 'settings.server', server: 'settings.server',
serverKey: 'settings.serverKey', serverKey: 'settings.serverKey',
serverMaxConcurrency: 'settings.serverMaxConcurrency',
serverUploadInterval: 'settings.serverUploadInterval',
pasteStyle: 'settings.pasteStyle', pasteStyle: 'settings.pasteStyle',
aesPassword: 'settings.aesPassword', aesPassword: 'settings.aesPassword',
rename: 'settings.rename', rename: 'settings.rename',

View File

@@ -30,6 +30,8 @@ interface ISettingForm {
sinkToken: string sinkToken: string
deleteLocalFile: boolean deleteLocalFile: boolean
serverKey: string serverKey: string
serverMaxConcurrency: number
serverUploadInterval: number
aesPassword: string aesPassword: string
enableWebServer: boolean enableWebServer: boolean
webServerHost: string webServerHost: string