mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-11 18:10:32 +08:00
✨ Feature(custom): add upload task system
This commit is contained in:
@@ -3,6 +3,7 @@ import { uploadChoosedFiles, uploadClipboardFiles } from 'apis/app/uploader/apis
|
||||
import { RPCRouter } from '~/events/rpc/router'
|
||||
import { IRPCActionType, IRPCType } from '~/utils/enum'
|
||||
import getPicBeds from '~/utils/getPicBeds'
|
||||
import UploadTaskQueueManager from '~/utils/uploadTaskQueue'
|
||||
|
||||
const uploadRouter = new RPCRouter()
|
||||
|
||||
@@ -26,6 +27,170 @@ const uploadRoutes = [
|
||||
return uploadChoosedFiles(evt.sender, args[0])
|
||||
},
|
||||
},
|
||||
// Upload task queue routes
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_ADD,
|
||||
handler: async (evt: IIPCEvent, args: [files: IFileWithPath[]]) => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
manager.setWebContents(evt.sender)
|
||||
return manager.addTasks(args[0])
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_START,
|
||||
handler: async (evt: IIPCEvent, args: [intervalS?: number]) => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
manager.setWebContents(evt.sender)
|
||||
await manager.startQueue(args[0])
|
||||
return manager.getQueueStatus()
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_PAUSE,
|
||||
handler: async () => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
manager.pauseQueue()
|
||||
return manager.getQueueStatus()
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_RESUME,
|
||||
handler: async (evt: IIPCEvent) => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
manager.setWebContents(evt.sender)
|
||||
await manager.resumeQueue()
|
||||
return manager.getQueueStatus()
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_CANCEL_ALL,
|
||||
handler: async () => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
manager.cancelQueue()
|
||||
return manager.getQueueStatus()
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_CANCEL_ONE,
|
||||
handler: async (_: IIPCEvent, args: [taskId: string]) => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
return manager.cancelTask(args[0])
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_REMOVE_ONE,
|
||||
handler: async (_: IIPCEvent, args: [taskId: string]) => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
return manager.removeTask(args[0])
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_CLEAR_FINISHED,
|
||||
handler: async () => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
manager.clearFinishedTasks()
|
||||
return manager.getQueueStatus()
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_CLEAR_ALL,
|
||||
handler: async () => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
manager.clearAllTasks()
|
||||
return manager.getQueueStatus()
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_GET_STATUS,
|
||||
handler: async () => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
return manager.getQueueStatus()
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_SET_INTERVAL,
|
||||
handler: async (_: IIPCEvent, args: [intervalS: number]) => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
manager.setInterval(args[0])
|
||||
return manager.getInterval()
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_GET_INTERVAL,
|
||||
handler: async () => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
return manager.getInterval()
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_RETRY_ONE,
|
||||
handler: async (_: IIPCEvent, args: [taskId: string]) => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
return manager.retryTask(args[0])
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_RETRY_ALL_FAILED,
|
||||
handler: async () => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
return manager.retryAllFailed()
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_MOVE_UP,
|
||||
handler: async (_: IIPCEvent, args: [taskId: string]) => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
return manager.moveTaskUp(args[0])
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_MOVE_DOWN,
|
||||
handler: async (_: IIPCEvent, args: [taskId: string]) => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
return manager.moveTaskDown(args[0])
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_SET_PRIORITY,
|
||||
handler: async (_: IIPCEvent, args: [taskId: string, priority: number]) => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
return manager.setTaskPriority(args[0], args[1])
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_UPDATE_SETTINGS,
|
||||
handler: async (_: IIPCEvent, args: [settings: any]) => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
manager.updateSettings(args[0])
|
||||
return manager.getSettings()
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
{
|
||||
action: IRPCActionType.UPLOAD_TASK_GET_SETTINGS,
|
||||
handler: async () => {
|
||||
const manager = UploadTaskQueueManager.getInstance()
|
||||
return manager.getSettings()
|
||||
},
|
||||
type: IRPCType.INVOKE,
|
||||
},
|
||||
]
|
||||
|
||||
uploadRouter.addBatch(uploadRoutes)
|
||||
|
||||
@@ -139,6 +139,27 @@ export const IRPCActionType = {
|
||||
UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE: 'UPLOAD_CLIPBOARD_FILES_FROM_UPLOAD_PAGE',
|
||||
UPLOAD_CHOOSED_FILES: 'UPLOAD_CHOOSED_FILES',
|
||||
|
||||
// upload task queue rpc
|
||||
UPLOAD_TASK_ADD: 'UPLOAD_TASK_ADD',
|
||||
UPLOAD_TASK_START: 'UPLOAD_TASK_START',
|
||||
UPLOAD_TASK_PAUSE: 'UPLOAD_TASK_PAUSE',
|
||||
UPLOAD_TASK_RESUME: 'UPLOAD_TASK_RESUME',
|
||||
UPLOAD_TASK_CANCEL_ALL: 'UPLOAD_TASK_CANCEL_ALL',
|
||||
UPLOAD_TASK_CANCEL_ONE: 'UPLOAD_TASK_CANCEL_ONE',
|
||||
UPLOAD_TASK_REMOVE_ONE: 'UPLOAD_TASK_REMOVE_ONE',
|
||||
UPLOAD_TASK_CLEAR_FINISHED: 'UPLOAD_TASK_CLEAR_FINISHED',
|
||||
UPLOAD_TASK_CLEAR_ALL: 'UPLOAD_TASK_CLEAR_ALL',
|
||||
UPLOAD_TASK_GET_STATUS: 'UPLOAD_TASK_GET_STATUS',
|
||||
UPLOAD_TASK_SET_INTERVAL: 'UPLOAD_TASK_SET_INTERVAL',
|
||||
UPLOAD_TASK_GET_INTERVAL: 'UPLOAD_TASK_GET_INTERVAL',
|
||||
UPLOAD_TASK_RETRY_ONE: 'UPLOAD_TASK_RETRY_ONE',
|
||||
UPLOAD_TASK_RETRY_ALL_FAILED: 'UPLOAD_TASK_RETRY_ALL_FAILED',
|
||||
UPLOAD_TASK_MOVE_UP: 'UPLOAD_TASK_MOVE_UP',
|
||||
UPLOAD_TASK_MOVE_DOWN: 'UPLOAD_TASK_MOVE_DOWN',
|
||||
UPLOAD_TASK_SET_PRIORITY: 'UPLOAD_TASK_SET_PRIORITY',
|
||||
UPLOAD_TASK_UPDATE_SETTINGS: 'UPLOAD_TASK_UPDATE_SETTINGS',
|
||||
UPLOAD_TASK_GET_SETTINGS: 'UPLOAD_TASK_GET_SETTINGS',
|
||||
|
||||
// gallery rpc
|
||||
GALLERY_PASTE_TEXT: 'GALLERY_PASTE_TEXT',
|
||||
GALLERY_REMOVE_FILES: 'GALLERY_REMOVE_FILES',
|
||||
|
||||
631
src/main/utils/uploadTaskQueue.ts
Normal file
631
src/main/utils/uploadTaskQueue.ts
Normal file
@@ -0,0 +1,631 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { GalleryDB } from '@core/datastore'
|
||||
import db from '@core/datastore'
|
||||
import picgo from '@core/picgo'
|
||||
import uploader from 'apis/app/uploader'
|
||||
import windowManager from 'apis/app/window/windowManager'
|
||||
import { app, Notification, WebContents } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import { T as $t } from '~/i18n/index'
|
||||
import { handleCopyUrl, handleUrlEncodeWithSetting } from '~/utils/common'
|
||||
import { configPaths } from '~/utils/configPaths'
|
||||
import { IPasteStyle, IWindowList } from '~/utils/enum'
|
||||
import pasteTemplate from '~/utils/pasteTemplate'
|
||||
|
||||
export const UploadTaskStatus = {
|
||||
PENDING: 'pending',
|
||||
UPLOADING: 'uploading',
|
||||
COMPLETED: 'completed',
|
||||
FAILED: 'failed',
|
||||
CANCELLED: 'cancelled',
|
||||
PAUSED: 'paused',
|
||||
}
|
||||
|
||||
export const UploadTaskPriority = {
|
||||
LOW: 0,
|
||||
NORMAL: 1,
|
||||
HIGH: 2,
|
||||
}
|
||||
|
||||
export interface IUploadTaskItem {
|
||||
id: string
|
||||
fileName: string
|
||||
filePath: string
|
||||
fileSize: number
|
||||
status: string
|
||||
progress: number
|
||||
error?: string
|
||||
result?: IStringKeyMap
|
||||
createdAt: number
|
||||
startedAt?: number
|
||||
completedAt?: number
|
||||
retryCount: number
|
||||
priority: number
|
||||
uploadSpeed?: number // bytes per second
|
||||
uploadDuration?: number // seconds
|
||||
}
|
||||
|
||||
export interface IUploadTaskQueueConfig {
|
||||
intervalS: number // Interval between uploads in seconds
|
||||
isRunning: boolean
|
||||
isPaused: boolean
|
||||
autoStart: boolean
|
||||
pauseOnError: boolean
|
||||
maxRetryCount: number
|
||||
}
|
||||
|
||||
class UploadTaskQueueManager {
|
||||
private static instance: UploadTaskQueueManager
|
||||
|
||||
private taskQueue: IUploadTaskItem[] = []
|
||||
private config: IUploadTaskQueueConfig = {
|
||||
intervalS: 1, // Default 1 second interval
|
||||
isRunning: false,
|
||||
isPaused: false,
|
||||
autoStart: false,
|
||||
pauseOnError: false,
|
||||
maxRetryCount: 3,
|
||||
}
|
||||
|
||||
private webContents: WebContents | null = null
|
||||
private persistPath = path.join(app.getPath('userData'), 'taskQueue.json')
|
||||
private taskTimer: NodeJS.Timeout | null = null
|
||||
|
||||
private constructor() {
|
||||
this.restore()
|
||||
}
|
||||
|
||||
static getInstance(): UploadTaskQueueManager {
|
||||
if (!UploadTaskQueueManager.instance) {
|
||||
UploadTaskQueueManager.instance = new UploadTaskQueueManager()
|
||||
}
|
||||
return UploadTaskQueueManager.instance
|
||||
}
|
||||
|
||||
setWebContents(webContents: WebContents): this {
|
||||
this.webContents = webContents
|
||||
return this
|
||||
}
|
||||
|
||||
private getFileSize(filePath: string): number {
|
||||
try {
|
||||
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
|
||||
return 0
|
||||
}
|
||||
const stats = fs.statSync(filePath)
|
||||
return stats.size
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
addTasks(files: IFileWithPath[], priority: number = UploadTaskPriority.NORMAL): IUploadTaskItem[] {
|
||||
const newTasks: IUploadTaskItem[] = files.map(file => ({
|
||||
id: `task_${uuid()}`,
|
||||
fileName: file.name || path.basename(file.path),
|
||||
filePath: file.path,
|
||||
fileSize: this.getFileSize(file.path),
|
||||
status: UploadTaskStatus.PENDING,
|
||||
progress: 0,
|
||||
createdAt: Date.now(),
|
||||
retryCount: 0,
|
||||
priority,
|
||||
}))
|
||||
|
||||
newTasks.forEach(task => {
|
||||
const insertIndex = this.taskQueue.findIndex(
|
||||
t => t.status === UploadTaskStatus.PENDING && t.priority < task.priority,
|
||||
)
|
||||
if (insertIndex === -1) {
|
||||
this.taskQueue.push(task)
|
||||
} else {
|
||||
this.taskQueue.splice(insertIndex, 0, task)
|
||||
}
|
||||
})
|
||||
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
|
||||
if (this.config.autoStart && !this.config.isRunning) {
|
||||
this.startQueue()
|
||||
}
|
||||
|
||||
return newTasks
|
||||
}
|
||||
|
||||
async startQueue(intervalS?: number): Promise<void> {
|
||||
if (intervalS !== undefined) {
|
||||
this.config.intervalS = intervalS
|
||||
}
|
||||
|
||||
if (this.config.isRunning) {
|
||||
return
|
||||
}
|
||||
|
||||
this.config.isRunning = true
|
||||
this.config.isPaused = false
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
|
||||
await this.processNextTask()
|
||||
}
|
||||
|
||||
private async processNextTask(): Promise<void> {
|
||||
if (!this.config.isRunning || this.config.isPaused) {
|
||||
return
|
||||
}
|
||||
|
||||
const pendingTask = this.taskQueue.find(task => task.status === UploadTaskStatus.PENDING)
|
||||
|
||||
if (!pendingTask) {
|
||||
this.config.isRunning = false
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
this.showCompletionNotification()
|
||||
return
|
||||
}
|
||||
|
||||
pendingTask.status = UploadTaskStatus.UPLOADING
|
||||
pendingTask.startedAt = Date.now()
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
|
||||
try {
|
||||
const result = await this.uploadSingleFile(pendingTask)
|
||||
|
||||
pendingTask.status = UploadTaskStatus.COMPLETED
|
||||
pendingTask.progress = 100
|
||||
pendingTask.completedAt = Date.now()
|
||||
pendingTask.result = result
|
||||
|
||||
if (pendingTask.startedAt && pendingTask.fileSize > 0) {
|
||||
pendingTask.uploadDuration = pendingTask.completedAt - pendingTask.startedAt
|
||||
pendingTask.uploadSpeed = Math.round((pendingTask.fileSize / pendingTask.uploadDuration) * 1000)
|
||||
}
|
||||
} catch (error: any) {
|
||||
pendingTask.error = error.message || 'Upload failed'
|
||||
pendingTask.completedAt = Date.now()
|
||||
|
||||
if (pendingTask.retryCount < this.config.maxRetryCount) {
|
||||
pendingTask.retryCount++
|
||||
pendingTask.status = UploadTaskStatus.PENDING
|
||||
pendingTask.startedAt = undefined
|
||||
pendingTask.completedAt = undefined
|
||||
pendingTask.error = undefined
|
||||
} else {
|
||||
pendingTask.status = UploadTaskStatus.FAILED
|
||||
|
||||
if (this.config.pauseOnError) {
|
||||
this.config.isPaused = true
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
|
||||
if (this.config.isRunning && !this.config.isPaused) {
|
||||
const pendingCount = this.taskQueue.filter(t => t.status === UploadTaskStatus.PENDING).length
|
||||
if (pendingCount > 0) {
|
||||
this.taskTimer = setTimeout(() => {
|
||||
this.processNextTask()
|
||||
}, this.config.intervalS * 1000)
|
||||
} else {
|
||||
this.config.isRunning = false
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
this.showCompletionNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadSingleFile(task: IUploadTaskItem): Promise<IStringKeyMap> {
|
||||
const win = windowManager.getAvailableWindow()
|
||||
const webContents = this.webContents || win?.webContents
|
||||
|
||||
if (!webContents) {
|
||||
throw new Error('No webContents available for upload')
|
||||
}
|
||||
|
||||
const input = [task.filePath]
|
||||
const rawInput = cloneDeep(input)
|
||||
|
||||
let imgs: ImgInfo[] | false = false
|
||||
imgs = await uploader.setWebContents(webContents).upload(input)
|
||||
|
||||
if (imgs !== false && imgs.length > 0) {
|
||||
const pasteStyle = db.get(configPaths.settings.pasteStyle) || IPasteStyle.MARKDOWN
|
||||
const deleteLocalFile = db.get(configPaths.settings.deleteLocalFile) || false
|
||||
|
||||
const img = imgs[0]
|
||||
|
||||
if (deleteLocalFile && !task.filePath.startsWith('http')) {
|
||||
fs.remove(rawInput[0])
|
||||
.then(() => {
|
||||
picgo.log.info(`delete local file: ${rawInput[0]}`)
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
picgo.log.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
const [pasteText, shortUrl] = await pasteTemplate(pasteStyle, img, db.get(configPaths.settings.customLink))
|
||||
img.shortUrl = shortUrl
|
||||
|
||||
const inserted = await GalleryDB.getInstance().insert(img)
|
||||
|
||||
windowManager.get(IWindowList.TRAY_WINDOW)?.webContents?.send('uploadFiles', [img])
|
||||
if (windowManager.has(IWindowList.SETTING_WINDOW)) {
|
||||
windowManager.get(IWindowList.SETTING_WINDOW)!.webContents?.send('updateGallery')
|
||||
}
|
||||
|
||||
handleCopyUrl(pasteText)
|
||||
|
||||
return {
|
||||
url: handleUrlEncodeWithSetting(inserted.imgUrl!),
|
||||
fullResult: inserted,
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Upload failed - no result returned')
|
||||
}
|
||||
|
||||
pauseQueue(): void {
|
||||
this.config.isPaused = true
|
||||
if (this.taskTimer) {
|
||||
clearTimeout(this.taskTimer)
|
||||
this.taskTimer = null
|
||||
}
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
}
|
||||
|
||||
async resumeQueue(): Promise<void> {
|
||||
if (!this.config.isRunning) {
|
||||
await this.startQueue()
|
||||
return
|
||||
}
|
||||
|
||||
this.config.isPaused = false
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
await this.processNextTask()
|
||||
}
|
||||
|
||||
cancelQueue(): void {
|
||||
this.config.isRunning = false
|
||||
this.config.isPaused = false
|
||||
|
||||
if (this.taskTimer) {
|
||||
clearTimeout(this.taskTimer)
|
||||
this.taskTimer = null
|
||||
}
|
||||
|
||||
this.taskQueue.forEach(task => {
|
||||
if (task.status === UploadTaskStatus.PENDING || task.status === UploadTaskStatus.UPLOADING) {
|
||||
task.status = UploadTaskStatus.CANCELLED
|
||||
task.completedAt = Date.now()
|
||||
}
|
||||
})
|
||||
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
}
|
||||
|
||||
cancelTask(taskId: string): boolean {
|
||||
const task = this.taskQueue.find(t => t.id === taskId)
|
||||
if (task && (task.status === UploadTaskStatus.PENDING || task.status === UploadTaskStatus.UPLOADING)) {
|
||||
task.status = UploadTaskStatus.CANCELLED
|
||||
task.completedAt = Date.now()
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
removeTask(taskId: string): boolean {
|
||||
const index = this.taskQueue.findIndex(t => t.id === taskId)
|
||||
if (index !== -1) {
|
||||
this.taskQueue.splice(index, 1)
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
clearFinishedTasks(): void {
|
||||
this.taskQueue = this.taskQueue.filter(
|
||||
task =>
|
||||
task.status === UploadTaskStatus.PENDING ||
|
||||
task.status === UploadTaskStatus.UPLOADING ||
|
||||
task.status === UploadTaskStatus.PAUSED,
|
||||
)
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
}
|
||||
|
||||
clearAllTasks(): void {
|
||||
this.cancelQueue()
|
||||
this.taskQueue = []
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
}
|
||||
|
||||
getAllTasks(): IUploadTaskItem[] {
|
||||
return [...this.taskQueue]
|
||||
}
|
||||
|
||||
getQueueStatus(): {
|
||||
tasks: IUploadTaskItem[]
|
||||
config: IUploadTaskQueueConfig
|
||||
stats: {
|
||||
total: number
|
||||
pending: number
|
||||
completed: number
|
||||
failed: number
|
||||
cancelled: number
|
||||
uploading: number
|
||||
totalSize: number
|
||||
completedSize: number
|
||||
avgSpeed: number
|
||||
estimatedTimeMs: number
|
||||
}
|
||||
} {
|
||||
const completedTasks = this.taskQueue.filter(t => t.status === UploadTaskStatus.COMPLETED)
|
||||
const pendingTasks = this.taskQueue.filter(t => t.status === UploadTaskStatus.PENDING)
|
||||
const uploadingTasks = this.taskQueue.filter(t => t.status === UploadTaskStatus.UPLOADING)
|
||||
|
||||
const totalSize = this.taskQueue.reduce((sum, t) => sum + (t.fileSize || 0), 0)
|
||||
const completedSize = completedTasks.reduce((sum, t) => sum + (t.fileSize || 0), 0)
|
||||
|
||||
const tasksWithSpeed = completedTasks.filter(t => t.uploadSpeed && t.uploadSpeed > 0)
|
||||
const avgSpeed =
|
||||
tasksWithSpeed.length > 0
|
||||
? Math.round(tasksWithSpeed.reduce((sum, t) => sum + (t.uploadSpeed || 0), 0) / tasksWithSpeed.length)
|
||||
: 0
|
||||
|
||||
const remainingSize =
|
||||
pendingTasks.reduce((sum, t) => sum + (t.fileSize || 0), 0) +
|
||||
uploadingTasks.reduce((sum, t) => sum + (t.fileSize || 0), 0)
|
||||
const estimatedTimeMs =
|
||||
avgSpeed > 0
|
||||
? Math.round((remainingSize / avgSpeed) * 1000) + pendingTasks.length * this.config.intervalS * 1000
|
||||
: 0
|
||||
|
||||
const stats = {
|
||||
total: this.taskQueue.length,
|
||||
pending: pendingTasks.length,
|
||||
completed: completedTasks.length,
|
||||
failed: this.taskQueue.filter(t => t.status === UploadTaskStatus.FAILED).length,
|
||||
cancelled: this.taskQueue.filter(t => t.status === UploadTaskStatus.CANCELLED).length,
|
||||
uploading: uploadingTasks.length,
|
||||
totalSize,
|
||||
completedSize,
|
||||
avgSpeed,
|
||||
estimatedTimeMs,
|
||||
}
|
||||
|
||||
return {
|
||||
tasks: [...this.taskQueue],
|
||||
config: { ...this.config },
|
||||
stats,
|
||||
}
|
||||
}
|
||||
|
||||
retryTask(taskId: string): boolean {
|
||||
const task = this.taskQueue.find(t => t.id === taskId)
|
||||
if (task && task.status === UploadTaskStatus.FAILED) {
|
||||
task.status = UploadTaskStatus.PENDING
|
||||
task.retryCount = 0
|
||||
task.error = undefined
|
||||
task.startedAt = undefined
|
||||
task.completedAt = undefined
|
||||
task.progress = 0
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
retryAllFailed(): number {
|
||||
let count = 0
|
||||
this.taskQueue.forEach(task => {
|
||||
if (task.status === UploadTaskStatus.FAILED) {
|
||||
task.status = UploadTaskStatus.PENDING
|
||||
task.retryCount = 0
|
||||
task.error = undefined
|
||||
task.startedAt = undefined
|
||||
task.completedAt = undefined
|
||||
task.progress = 0
|
||||
count++
|
||||
}
|
||||
})
|
||||
if (count > 0) {
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
moveTaskUp(taskId: string): boolean {
|
||||
const index = this.taskQueue.findIndex(t => t.id === taskId)
|
||||
if (index > 0 && this.taskQueue[index].status === UploadTaskStatus.PENDING) {
|
||||
let targetIndex = index - 1
|
||||
while (targetIndex >= 0 && this.taskQueue[targetIndex].status !== UploadTaskStatus.PENDING) {
|
||||
targetIndex--
|
||||
}
|
||||
if (targetIndex >= 0) {
|
||||
const temp = this.taskQueue[index]
|
||||
this.taskQueue[index] = this.taskQueue[targetIndex]
|
||||
this.taskQueue[targetIndex] = temp
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
moveTaskDown(taskId: string): boolean {
|
||||
const index = this.taskQueue.findIndex(t => t.id === taskId)
|
||||
if (index < this.taskQueue.length - 1 && this.taskQueue[index].status === UploadTaskStatus.PENDING) {
|
||||
let targetIndex = index + 1
|
||||
while (targetIndex < this.taskQueue.length && this.taskQueue[targetIndex].status !== UploadTaskStatus.PENDING) {
|
||||
targetIndex++
|
||||
}
|
||||
if (targetIndex < this.taskQueue.length) {
|
||||
const temp = this.taskQueue[index]
|
||||
this.taskQueue[index] = this.taskQueue[targetIndex]
|
||||
this.taskQueue[targetIndex] = temp
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
setTaskPriority(taskId: string, priority: number): boolean {
|
||||
const task = this.taskQueue.find(t => t.id === taskId)
|
||||
if (task && task.status === UploadTaskStatus.PENDING) {
|
||||
task.priority = priority
|
||||
this.taskQueue.sort((a, b) => {
|
||||
if (a.status !== UploadTaskStatus.PENDING && b.status !== UploadTaskStatus.PENDING) return 0
|
||||
if (a.status !== UploadTaskStatus.PENDING) return 1
|
||||
if (b.status !== UploadTaskStatus.PENDING) return -1
|
||||
return b.priority - a.priority
|
||||
})
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
updateSettings(settings: Partial<IUploadTaskQueueConfig>): void {
|
||||
if (settings.intervalS !== undefined) {
|
||||
this.config.intervalS = Math.max(0.1, settings.intervalS)
|
||||
}
|
||||
if (settings.autoStart !== undefined) {
|
||||
this.config.autoStart = settings.autoStart
|
||||
}
|
||||
if (settings.pauseOnError !== undefined) {
|
||||
this.config.pauseOnError = settings.pauseOnError
|
||||
}
|
||||
if (settings.maxRetryCount !== undefined) {
|
||||
this.config.maxRetryCount = Math.max(0, Math.min(10, settings.maxRetryCount))
|
||||
}
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
}
|
||||
|
||||
getSettings(): IUploadTaskQueueConfig {
|
||||
return { ...this.config }
|
||||
}
|
||||
|
||||
setInterval(intervalS: number): void {
|
||||
this.config.intervalS = Math.max(0.1, intervalS) // Minimum 0.1 seconds
|
||||
this.persist()
|
||||
this.notifyTaskUpdate()
|
||||
}
|
||||
|
||||
getInterval(): number {
|
||||
return this.config.intervalS
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this.config.isRunning
|
||||
}
|
||||
|
||||
isPaused(): boolean {
|
||||
return this.config.isPaused
|
||||
}
|
||||
|
||||
private showCompletionNotification(): void {
|
||||
const stats = {
|
||||
completed: this.taskQueue.filter(t => t.status === UploadTaskStatus.COMPLETED).length,
|
||||
failed: this.taskQueue.filter(t => t.status === UploadTaskStatus.FAILED).length,
|
||||
}
|
||||
|
||||
if (stats.completed > 0 || stats.failed > 0) {
|
||||
const isShowResultNotification =
|
||||
db.get(configPaths.settings.uploadResultNotification) === undefined
|
||||
? true
|
||||
: !!db.get(configPaths.settings.uploadResultNotification)
|
||||
|
||||
if (isShowResultNotification) {
|
||||
const notification = new Notification({
|
||||
title: $t('UPLOAD_TASK_COMPLETED'),
|
||||
body: $t('UPLOAD_TASK_COMPLETED_BODY', { completed: stats.completed, failed: stats.failed }),
|
||||
})
|
||||
notification.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private notifyTaskUpdate(): void {
|
||||
const status = this.getQueueStatus()
|
||||
windowManager.get(IWindowList.SETTING_WINDOW)?.webContents?.send('uploadTaskQueueUpdate', status)
|
||||
}
|
||||
|
||||
private persist(): void {
|
||||
try {
|
||||
fs.ensureFileSync(this.persistPath)
|
||||
fs.writeFileSync(
|
||||
this.persistPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
taskQueue: this.taskQueue,
|
||||
config: this.config,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('Failed to persist upload task queue:', e)
|
||||
}
|
||||
}
|
||||
|
||||
private restore(): void {
|
||||
try {
|
||||
if (fs.existsSync(this.persistPath)) {
|
||||
const data = JSON.parse(fs.readFileSync(this.persistPath, { encoding: 'utf-8' }))
|
||||
if (data.taskQueue) {
|
||||
this.taskQueue = data.taskQueue.map((task: IUploadTaskItem) => ({
|
||||
...task,
|
||||
fileSize: task.fileSize || 0,
|
||||
retryCount: task.retryCount || 0,
|
||||
priority: task.priority ?? UploadTaskPriority.NORMAL,
|
||||
status: task.status === UploadTaskStatus.UPLOADING ? UploadTaskStatus.PENDING : task.status,
|
||||
}))
|
||||
}
|
||||
if (data.config) {
|
||||
this.config = {
|
||||
...this.config,
|
||||
intervalS: data.config.intervalS || 1,
|
||||
autoStart: data.config.autoStart || false,
|
||||
pauseOnError: data.config.pauseOnError || false,
|
||||
maxRetryCount: data.config.maxRetryCount ?? 3,
|
||||
isRunning: false,
|
||||
isPaused: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to restore upload task queue:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UploadTaskQueueManager
|
||||
Reference in New Issue
Block a user