mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-10 17:42:45 +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 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 }) {
|
||||
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
||||
const htmlContent = marked(markdownContent)
|
||||
@@ -44,124 +79,126 @@ router.post(
|
||||
urlparams?: URLSearchParams
|
||||
}): Promise<void> => {
|
||||
try {
|
||||
const allConfig = picgo.getConfig<any>() || {}
|
||||
const picbed = urlparams?.get('picbed')
|
||||
const passedKey = urlparams?.get('key')
|
||||
const serverKey = allConfig.settings?.serverKey || ''
|
||||
const useShortUrl = allConfig.settings?.useShortUrl
|
||||
if (serverKey && passedKey !== serverKey) {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'server key is uncorrect',
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
let currentPicBedType = ''
|
||||
let currentPicBedConfig = {} as IStringKeyMap
|
||||
let currentPicBedConfigId = ''
|
||||
let needRestore = false
|
||||
if (picbed) {
|
||||
const currentPicBed = allConfig.picBed || ({} as IStringKeyMap)
|
||||
currentPicBedType = currentPicBed.uploader || currentPicBed.current || 'smms'
|
||||
currentPicBedConfig = currentPicBed[currentPicBedType] || ({} as IStringKeyMap)
|
||||
currentPicBedConfigId = currentPicBedConfig._id
|
||||
const configName = urlparams?.get('configName') || currentPicBed[picbed]?._configName
|
||||
if (picbed === currentPicBedType && configName === currentPicBedConfig._configName) {
|
||||
// do nothing
|
||||
} else {
|
||||
needRestore = true
|
||||
const picBeds = allConfig.uploader
|
||||
const currentPicBedList = picBeds?.[picbed]?.configList
|
||||
if (currentPicBedList) {
|
||||
const currentConfig = currentPicBedList?.find((item: any) => item._configName === configName)
|
||||
if (currentConfig) {
|
||||
changeCurrentUploader(picbed, currentConfig, currentConfig._id)
|
||||
await withUploadRateLimit(async () => {
|
||||
const allConfig = picgo.getConfig<any>() || {}
|
||||
const picbed = urlparams?.get('picbed')
|
||||
const passedKey = urlparams?.get('key')
|
||||
const serverKey = allConfig.settings?.serverKey || ''
|
||||
const useShortUrl = allConfig.settings?.useShortUrl
|
||||
if (serverKey && passedKey !== serverKey) {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'server key is uncorrect',
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
let currentPicBedType = ''
|
||||
let currentPicBedConfig = {} as IStringKeyMap
|
||||
let currentPicBedConfigId = ''
|
||||
let needRestore = false
|
||||
if (picbed) {
|
||||
const currentPicBed = allConfig.picBed || ({} as IStringKeyMap)
|
||||
currentPicBedType = currentPicBed.uploader || currentPicBed.current || 'smms'
|
||||
currentPicBedConfig = currentPicBed[currentPicBedType] || ({} as IStringKeyMap)
|
||||
currentPicBedConfigId = currentPicBedConfig._id
|
||||
const configName = urlparams?.get('configName') || currentPicBed[picbed]?._configName
|
||||
if (picbed === currentPicBedType && configName === currentPicBedConfig._configName) {
|
||||
// do nothing
|
||||
} else {
|
||||
needRestore = true
|
||||
const picBeds = allConfig.uploader
|
||||
const currentPicBedList = picBeds?.[picbed]?.configList
|
||||
if (currentPicBedList) {
|
||||
const currentConfig = currentPicBedList?.find((item: any) => item._configName === configName)
|
||||
if (currentConfig) {
|
||||
changeCurrentUploader(picbed, currentConfig, currentConfig._id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (list.length === 0) {
|
||||
// upload with clipboard
|
||||
logger.info('[PicList Server] upload clipboard file')
|
||||
const result = await uploadClipboardFiles()
|
||||
const res = useShortUrl ? result.fullResult.shortUrl || result.url : result.url
|
||||
const fullResult = result.fullResult
|
||||
fullResult.imgUrl = useShortUrl ? fullResult.shortUrl || fullResult.imgUrl : fullResult.imgUrl
|
||||
logger.info('[PicList Server] upload result:', res)
|
||||
if (res) {
|
||||
const treatedFullResult = {
|
||||
isEncrypted: 1,
|
||||
EncryptedData: new AESHelper().encrypt(JSON.stringify(fullResult)),
|
||||
...fullResult,
|
||||
if (list.length === 0) {
|
||||
// upload with clipboard
|
||||
logger.info('[PicList Server] upload clipboard file')
|
||||
const result = await uploadClipboardFiles()
|
||||
const res = useShortUrl ? result.fullResult.shortUrl || result.url : result.url
|
||||
const fullResult = result.fullResult
|
||||
fullResult.imgUrl = useShortUrl ? fullResult.shortUrl || fullResult.imgUrl : fullResult.imgUrl
|
||||
logger.info('[PicList Server] upload result:', res)
|
||||
if (res) {
|
||||
const treatedFullResult = {
|
||||
isEncrypted: 1,
|
||||
EncryptedData: new AESHelper().encrypt(JSON.stringify(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 {
|
||||
handleResponse({
|
||||
response,
|
||||
body: {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
},
|
||||
logger.info('[PicList Server] upload files in list')
|
||||
// upload with files
|
||||
const pathList = list.map(item => {
|
||||
return {
|
||||
path: item,
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logger.info('[PicList Server] upload files in list')
|
||||
// upload with files
|
||||
const pathList = list.map(item => {
|
||||
return {
|
||||
path: item,
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
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)
|
||||
if (needRestore) {
|
||||
changeCurrentUploader(currentPicBedType, currentPicBedConfig, currentPicBedConfigId)
|
||||
}
|
||||
fs.emptyDirSync(serverTempDir)
|
||||
if (needRestore) {
|
||||
changeCurrentUploader(currentPicBedType, currentPicBedConfig, currentPicBedConfigId)
|
||||
}
|
||||
})
|
||||
} catch (err: any) {
|
||||
logger.error(err)
|
||||
handleResponse({
|
||||
|
||||
@@ -51,6 +51,8 @@ export interface IConfigStruct {
|
||||
deleteCloudFile: boolean
|
||||
server: IServerConfig
|
||||
serverKey: string
|
||||
serverMaxConcurrency: number
|
||||
serverUploadInterval: number
|
||||
pasteStyle: string
|
||||
aesPassword: string
|
||||
rename: boolean
|
||||
@@ -146,6 +148,8 @@ export const configPaths = {
|
||||
deleteCloudFile: 'settings.deleteCloudFile',
|
||||
server: 'settings.server',
|
||||
serverKey: 'settings.serverKey',
|
||||
serverMaxConcurrency: 'settings.serverMaxConcurrency',
|
||||
serverUploadInterval: 'settings.serverUploadInterval',
|
||||
pasteStyle: 'settings.pasteStyle',
|
||||
aesPassword: 'settings.aesPassword',
|
||||
rename: 'settings.rename',
|
||||
|
||||
@@ -854,9 +854,13 @@
|
||||
"serverHost": "Listen Address",
|
||||
"serverKey": "Authentication Key",
|
||||
"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",
|
||||
"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.",
|
||||
"serverUploadInterval": "Upload Interval (ms)",
|
||||
"serverUploadIntervalPlaceholder": "Delay in ms between uploads via API (0 = no delay)",
|
||||
"setLog": "Set Log",
|
||||
"setLogDesc": "Configure log level and file size",
|
||||
"setProxyAndMirror": "Set Proxy and Mirror Address",
|
||||
|
||||
@@ -854,9 +854,13 @@
|
||||
"serverHost": "监听地址",
|
||||
"serverKey": "鉴权密钥",
|
||||
"serverKeyPlaceholder": "请输入鉴权密钥,用于防止接口滥用",
|
||||
"serverMaxConcurrency": "最大上传并发数",
|
||||
"serverMaxConcurrencyPlaceholder": "通过API同时上传的最大数量(0表示不限制)",
|
||||
"serverPort": "监听端口",
|
||||
"serverSettings": "服务器设置",
|
||||
"serverSettingsNotice": "如果你不知道上传API服务的作用,请阅读文档,或者不用修改配置。",
|
||||
"serverUploadInterval": "上传间隔(毫秒)",
|
||||
"serverUploadIntervalPlaceholder": "通过API上传时每次上传之间的延迟(0表示无延迟)",
|
||||
"setLog": "设置日志",
|
||||
"setLogDesc": "配置日志等级和文件大小",
|
||||
"setProxyAndMirror": "设置代理和镜像地址",
|
||||
|
||||
@@ -854,9 +854,13 @@
|
||||
"serverHost": "監聽地址",
|
||||
"serverKey": "鑑權密鑰",
|
||||
"serverKeyPlaceholder": "請輸入鑑權密鑰,用於防止接口濫用",
|
||||
"serverMaxConcurrency": "最大上傳並發數",
|
||||
"serverMaxConcurrencyPlaceholder": "通過API同時上傳的最大數量(0表示不限制)",
|
||||
"serverPort": "監聽端口",
|
||||
"serverSettings": "伺服器設置",
|
||||
"serverSettingsNotice": "如果你不知道上傳API服務的作用,請閱讀文檔,或者不用修改配置。",
|
||||
"serverUploadInterval": "上傳間隔(毫秒)",
|
||||
"serverUploadIntervalPlaceholder": "通過API上傳時每次上傳之間的延遲(0表示無延遲)",
|
||||
"setLog": "設置日誌",
|
||||
"setLogDesc": "配置日誌等級和文件大小",
|
||||
"setProxyAndMirror": "設置代理和鏡像地址",
|
||||
|
||||
@@ -1044,6 +1044,26 @@
|
||||
:placeholder="t('pages.settings.advanced.serverKeyPlaceholder')"
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
<template #footer>
|
||||
@@ -1441,6 +1461,8 @@ const formOfSetting = ref<ISettingForm>({
|
||||
sinkToken: '',
|
||||
deleteLocalFile: false,
|
||||
serverKey: '',
|
||||
serverMaxConcurrency: 0,
|
||||
serverUploadInterval: 0,
|
||||
aesPassword: 'PicList-aesPassword',
|
||||
enableWebServer: false,
|
||||
webServerHost: '0.0.0.0',
|
||||
@@ -1552,6 +1574,8 @@ const autoWatchKeys = [
|
||||
'webServerPort',
|
||||
'webServerPath',
|
||||
'serverKey',
|
||||
'serverMaxConcurrency',
|
||||
'serverUploadInterval',
|
||||
'uploadNotification',
|
||||
'uploadResultNotification',
|
||||
'autoCloseMainWindow',
|
||||
|
||||
@@ -51,6 +51,8 @@ export interface IConfigStruct {
|
||||
deleteCloudFile: boolean
|
||||
server: IServerConfig
|
||||
serverKey: string
|
||||
serverMaxConcurrency: number
|
||||
serverUploadInterval: number
|
||||
pasteStyle: string
|
||||
aesPassword: string
|
||||
rename: boolean
|
||||
@@ -151,6 +153,8 @@ export const configPaths = {
|
||||
deleteCloudFile: 'settings.deleteCloudFile',
|
||||
server: 'settings.server',
|
||||
serverKey: 'settings.serverKey',
|
||||
serverMaxConcurrency: 'settings.serverMaxConcurrency',
|
||||
serverUploadInterval: 'settings.serverUploadInterval',
|
||||
pasteStyle: 'settings.pasteStyle',
|
||||
aesPassword: 'settings.aesPassword',
|
||||
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
|
||||
deleteLocalFile: boolean
|
||||
serverKey: string
|
||||
serverMaxConcurrency: number
|
||||
serverUploadInterval: number
|
||||
aesPassword: string
|
||||
enableWebServer: boolean
|
||||
webServerHost: string
|
||||
|
||||
Reference in New Issue
Block a user