mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-22 00:30:33 +08:00
✨ Feature(custom): add upload max concurrency and upload interval setting for api
ISSUES CLOSED: #487
This commit is contained in:
@@ -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,124 +79,126 @@ router.post(
|
|||||||
urlparams?: URLSearchParams
|
urlparams?: URLSearchParams
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const allConfig = picgo.getConfig<any>() || {}
|
await withUploadRateLimit(async () => {
|
||||||
const picbed = urlparams?.get('picbed')
|
const allConfig = picgo.getConfig<any>() || {}
|
||||||
const passedKey = urlparams?.get('key')
|
const picbed = urlparams?.get('picbed')
|
||||||
const serverKey = allConfig.settings?.serverKey || ''
|
const passedKey = urlparams?.get('key')
|
||||||
const useShortUrl = allConfig.settings?.useShortUrl
|
const serverKey = allConfig.settings?.serverKey || ''
|
||||||
if (serverKey && passedKey !== serverKey) {
|
const useShortUrl = allConfig.settings?.useShortUrl
|
||||||
handleResponse({
|
if (serverKey && passedKey !== serverKey) {
|
||||||
response,
|
handleResponse({
|
||||||
body: {
|
response,
|
||||||
success: false,
|
body: {
|
||||||
message: 'server key is uncorrect',
|
success: false,
|
||||||
},
|
message: 'server key is uncorrect',
|
||||||
})
|
},
|
||||||
return
|
})
|
||||||
}
|
return
|
||||||
let currentPicBedType = ''
|
}
|
||||||
let currentPicBedConfig = {} as IStringKeyMap
|
let currentPicBedType = ''
|
||||||
let currentPicBedConfigId = ''
|
let currentPicBedConfig = {} as IStringKeyMap
|
||||||
let needRestore = false
|
let currentPicBedConfigId = ''
|
||||||
if (picbed) {
|
let needRestore = false
|
||||||
const currentPicBed = allConfig.picBed || ({} as IStringKeyMap)
|
if (picbed) {
|
||||||
currentPicBedType = currentPicBed.uploader || currentPicBed.current || 'smms'
|
const currentPicBed = allConfig.picBed || ({} as IStringKeyMap)
|
||||||
currentPicBedConfig = currentPicBed[currentPicBedType] || ({} as IStringKeyMap)
|
currentPicBedType = currentPicBed.uploader || currentPicBed.current || 'smms'
|
||||||
currentPicBedConfigId = currentPicBedConfig._id
|
currentPicBedConfig = currentPicBed[currentPicBedType] || ({} as IStringKeyMap)
|
||||||
const configName = urlparams?.get('configName') || currentPicBed[picbed]?._configName
|
currentPicBedConfigId = currentPicBedConfig._id
|
||||||
if (picbed === currentPicBedType && configName === currentPicBedConfig._configName) {
|
const configName = urlparams?.get('configName') || currentPicBed[picbed]?._configName
|
||||||
// do nothing
|
if (picbed === currentPicBedType && configName === currentPicBedConfig._configName) {
|
||||||
} else {
|
// do nothing
|
||||||
needRestore = true
|
} else {
|
||||||
const picBeds = allConfig.uploader
|
needRestore = true
|
||||||
const currentPicBedList = picBeds?.[picbed]?.configList
|
const picBeds = allConfig.uploader
|
||||||
if (currentPicBedList) {
|
const currentPicBedList = picBeds?.[picbed]?.configList
|
||||||
const currentConfig = currentPicBedList?.find((item: any) => item._configName === configName)
|
if (currentPicBedList) {
|
||||||
if (currentConfig) {
|
const currentConfig = currentPicBedList?.find((item: any) => item._configName === configName)
|
||||||
changeCurrentUploader(picbed, currentConfig, currentConfig._id)
|
if (currentConfig) {
|
||||||
|
changeCurrentUploader(picbed, currentConfig, currentConfig._id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (list.length === 0) {
|
||||||
if (list.length === 0) {
|
// upload with clipboard
|
||||||
// upload with clipboard
|
logger.info('[PicList Server] upload clipboard file')
|
||||||
logger.info('[PicList Server] upload clipboard file')
|
const result = await uploadClipboardFiles()
|
||||||
const result = await uploadClipboardFiles()
|
const res = useShortUrl ? result.fullResult.shortUrl || result.url : result.url
|
||||||
const res = useShortUrl ? result.fullResult.shortUrl || result.url : result.url
|
const fullResult = result.fullResult
|
||||||
const fullResult = result.fullResult
|
fullResult.imgUrl = useShortUrl ? fullResult.shortUrl || fullResult.imgUrl : fullResult.imgUrl
|
||||||
fullResult.imgUrl = useShortUrl ? fullResult.shortUrl || fullResult.imgUrl : fullResult.imgUrl
|
logger.info('[PicList Server] upload result:', res)
|
||||||
logger.info('[PicList Server] upload result:', res)
|
if (res) {
|
||||||
if (res) {
|
const treatedFullResult = {
|
||||||
const treatedFullResult = {
|
isEncrypted: 1,
|
||||||
isEncrypted: 1,
|
EncryptedData: new AESHelper().encrypt(JSON.stringify(fullResult)),
|
||||||
EncryptedData: new AESHelper().encrypt(JSON.stringify(fullResult)),
|
...fullResult,
|
||||||
...fullResult,
|
}
|
||||||
|
delete treatedFullResult.config
|
||||||
|
handleResponse({
|
||||||
|
response,
|
||||||
|
body: {
|
||||||
|
success: true,
|
||||||
|
result: [res],
|
||||||
|
fullResult: [treatedFullResult],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
handleResponse({
|
||||||
|
response,
|
||||||
|
body: {
|
||||||
|
success: false,
|
||||||
|
message: errorMessage,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
delete treatedFullResult.config
|
|
||||||
handleResponse({
|
|
||||||
response,
|
|
||||||
body: {
|
|
||||||
success: true,
|
|
||||||
result: [res],
|
|
||||||
fullResult: [treatedFullResult],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
handleResponse({
|
logger.info('[PicList Server] upload files in list')
|
||||||
response,
|
// upload with files
|
||||||
body: {
|
const pathList = list.map(item => {
|
||||||
success: false,
|
return {
|
||||||
message: errorMessage,
|
path: item,
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
}
|
const win = windowManager.getAvailableWindow()
|
||||||
} else {
|
const result = await uploadChoosedFiles(win?.webContents, pathList)
|
||||||
logger.info('[PicList Server] upload files in list')
|
const res = result.map(item => {
|
||||||
// upload with files
|
return useShortUrl ? item.fullResult.shortUrl || item.url : item.url
|
||||||
const pathList = list.map(item => {
|
})
|
||||||
return {
|
const fullResult = result.map((item: any) => {
|
||||||
path: item,
|
const treatedItem = {
|
||||||
|
isEncrypted: 1,
|
||||||
|
EncryptedData: new AESHelper().encrypt(JSON.stringify(item.fullResult)),
|
||||||
|
...item.fullResult,
|
||||||
|
}
|
||||||
|
delete treatedItem.config
|
||||||
|
treatedItem.imgUrl = useShortUrl ? treatedItem.shortUrl || treatedItem.imgUrl : treatedItem.imgUrl
|
||||||
|
return treatedItem
|
||||||
|
})
|
||||||
|
logger.info('[PicList Server] upload result', res.join(' ; '))
|
||||||
|
if (res.length) {
|
||||||
|
handleResponse({
|
||||||
|
response,
|
||||||
|
body: {
|
||||||
|
success: true,
|
||||||
|
result: res,
|
||||||
|
fullResult,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
handleResponse({
|
||||||
|
response,
|
||||||
|
body: {
|
||||||
|
success: false,
|
||||||
|
message: errorMessage,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
|
||||||
const win = windowManager.getAvailableWindow()
|
|
||||||
const result = await uploadChoosedFiles(win?.webContents, pathList)
|
|
||||||
const res = result.map(item => {
|
|
||||||
return useShortUrl ? item.fullResult.shortUrl || item.url : item.url
|
|
||||||
})
|
|
||||||
const fullResult = result.map((item: any) => {
|
|
||||||
const treatedItem = {
|
|
||||||
isEncrypted: 1,
|
|
||||||
EncryptedData: new AESHelper().encrypt(JSON.stringify(item.fullResult)),
|
|
||||||
...item.fullResult,
|
|
||||||
}
|
|
||||||
delete treatedItem.config
|
|
||||||
treatedItem.imgUrl = useShortUrl ? treatedItem.shortUrl || treatedItem.imgUrl : treatedItem.imgUrl
|
|
||||||
return treatedItem
|
|
||||||
})
|
|
||||||
logger.info('[PicList Server] upload result', res.join(' ; '))
|
|
||||||
if (res.length) {
|
|
||||||
handleResponse({
|
|
||||||
response,
|
|
||||||
body: {
|
|
||||||
success: true,
|
|
||||||
result: res,
|
|
||||||
fullResult,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
handleResponse({
|
|
||||||
response,
|
|
||||||
body: {
|
|
||||||
success: false,
|
|
||||||
message: errorMessage,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
fs.emptyDirSync(serverTempDir)
|
||||||
fs.emptyDirSync(serverTempDir)
|
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({
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "设置代理和镜像地址",
|
||||||
|
|||||||
@@ -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": "設置代理和鏡像地址",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
2
src/universal/types/view.d.ts
vendored
2
src/universal/types/view.d.ts
vendored
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user