From f3ebf1fcea7b9ba6cf9e1aba0a7bf148a5496381 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Sat, 19 Sep 2026 09:17:29 +0200 Subject: [PATCH] feat(download): remote-download quotas per count and per volume Two new group settings: aria2_task_limit caps a user's concurrent active download tasks (counted at submission via the task list), and aria2_max_file_size aborts a task once aria2 resolves a total size over the cap. Both 0 = unlimited, exposed in the admin group editor. Resolves #16 Authored By: TDvorak --- frontend/public/locales/en-US/dashboard.json | 4 ++ frontend/public/locales/zh-CN/dashboard.json | 4 ++ frontend/src/api/dashboard.ts | 2 + .../Group/EditGroup/FileManagementSection.tsx | 51 +++++++++++++++++++ inventory/types/types.go | 4 ++ pkg/filemanager/workflows/remote_download.go | 9 ++++ service/explorer/workflows.go | 17 +++++++ 7 files changed, 91 insertions(+) diff --git a/frontend/public/locales/en-US/dashboard.json b/frontend/public/locales/en-US/dashboard.json index 07eca696..c353f1af 100644 --- a/frontend/public/locales/en-US/dashboard.json +++ b/frontend/public/locales/en-US/dashboard.json @@ -1317,6 +1317,10 @@ "aria2OptionsDes": "Extra parameters for downloaders (qBittorrent or Aria2), written in JSON key-value format, see the downloader official documentation for available parameters.", "aria2BatchSize": "Max batch size of remote download tasks", "aria2BatchSizeDes": "Max number for submitting batched remote download tasks, fill in 0 to indicate no limit.", + "aria2TaskLimit": "Max concurrent remote download tasks", + "aria2TaskLimitDes": "Max number of active remote download tasks per user (queued, running, or suspended). 0 = no limit.", + "aria2MaxFileSize": "Max size per download task (bytes)", + "aria2MaxFileSizeDes": "Remote download tasks whose resolved total size exceeds this are aborted. 0 = no limit.", "migratePolicy": "Relocate storage policy", "migratePolicyDes": "Whether the user creates a storage policy relocation task.", "advanceDelete": "Advanced file deletion options", diff --git a/frontend/public/locales/zh-CN/dashboard.json b/frontend/public/locales/zh-CN/dashboard.json index 45e78430..ef4db013 100644 --- a/frontend/public/locales/zh-CN/dashboard.json +++ b/frontend/public/locales/zh-CN/dashboard.json @@ -1317,6 +1317,10 @@ "aria2OptionsDes": "qBittorrent 或 Aria2 下载器的任务额外配置参数,以 JSON 键值对格式书写,可用参数请查阅官方文档。", "aria2BatchSize": "批量离线下载最大数量", "aria2BatchSizeDes": "批量创建离线下载时的最大数量,填写为 0 表示不限制。", + "aria2TaskLimit": "最大并发离线下载任务数", + "aria2TaskLimitDes": "每个用户同时处于排队、下载或暂停状态的离线下载任务上限,填 0 表示不限制。", + "aria2MaxFileSize": "单个下载任务最大体积(字节)", + "aria2MaxFileSizeDes": "解析后的下载总大小超过此值的任务将被中止,填 0 表示不限制。", "migratePolicy": "存储策略转移", "migratePolicyDes": "是否用户创建存储策略转移任务。", "advanceDelete": "高级文件删除选项", diff --git a/frontend/src/api/dashboard.ts b/frontend/src/api/dashboard.ts index d2905823..6f8965fb 100644 --- a/frontend/src/api/dashboard.ts +++ b/frontend/src/api/dashboard.ts @@ -58,6 +58,8 @@ export interface GroupSetting { remote_download_options?: Record; source_batch?: number; aria2_batch?: number; + aria2_task_limit?: number; + aria2_max_file_size?: number; max_walked_files?: number; trash_retention?: number; redirected_source?: boolean; diff --git a/frontend/src/component/Admin/Group/EditGroup/FileManagementSection.tsx b/frontend/src/component/Admin/Group/EditGroup/FileManagementSection.tsx index 5c6664bc..102c5488 100644 --- a/frontend/src/component/Admin/Group/EditGroup/FileManagementSection.tsx +++ b/frontend/src/component/Admin/Group/EditGroup/FileManagementSection.tsx @@ -132,6 +132,27 @@ const FileManagementSection = () => { [setGroup], ); + const onAria2TaskLimitChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + settings: { ...p.settings, aria2_task_limit: parseInt(e.target.value) ? parseInt(e.target.value) : undefined }, + })); + }, + [setGroup], + ); + + const onAria2MaxFileSizeChange = useCallback( + (e: React.ChangeEvent) => { + const bytes = parseInt(e.target.value); + setGroup((p: GroupEnt) => ({ + ...p, + settings: { ...p.settings, aria2_max_file_size: bytes > 0 ? bytes : undefined }, + })); + }, + [setGroup], + ); + const onAllowAdvanceDeleteChange = useCallback( (e: React.ChangeEvent) => { setGroup((p: GroupEnt) => ({ @@ -322,6 +343,36 @@ const FileManagementSection = () => { {t("group.aria2BatchSizeDes")} + + + + {t("group.aria2TaskLimitDes")} + + + + + + {t("group.aria2MaxFileSizeDes")} + + diff --git a/inventory/types/types.go b/inventory/types/types.go index 3787b739..be430e85 100644 --- a/inventory/types/types.go +++ b/inventory/types/types.go @@ -44,6 +44,10 @@ type ( RemoteDownloadOptions map[string]interface{} `json:"remote_download_options,omitempty"` // 离线下载用户组配置 SourceBatchSize int `json:"source_batch,omitempty"` Aria2BatchSize int `json:"aria2_batch,omitempty"` + // Aria2TaskLimit caps concurrent active remote-download tasks per user. + Aria2TaskLimit int `json:"aria2_task_limit,omitempty"` + // Aria2MaxFileSize caps the total byte size of a single download task. + Aria2MaxFileSize int64 `json:"aria2_max_file_size,omitempty"` MaxWalkedFiles int `json:"max_walked_files,omitempty"` TrashRetention int `json:"trash_retention,omitempty"` RedirectedSource bool `json:"redirected_source,omitempty"` diff --git a/pkg/filemanager/workflows/remote_download.go b/pkg/filemanager/workflows/remote_download.go index c02ae566..2f49d381 100644 --- a/pkg/filemanager/workflows/remote_download.go +++ b/pkg/filemanager/workflows/remote_download.go @@ -366,6 +366,15 @@ func (m *RemoteDownloadTask) monitor(ctx context.Context, dep dependency.Dep) (t if m.state.Status == nil || m.state.Status.Total != status.Total { m.l.Info("download size changed, re-validate files.") + // Group per-task volume cap: abort once the resolved size exceeds it. + var maxSize int64 + if u := inventory.UserFromContext(ctx); u != nil && u.Edges.Group != nil { + maxSize = u.Edges.Group.Settings.Aria2MaxFileSize + } + if maxSize > 0 && status.Total > maxSize { + m.state.Status = status + return task.StatusError, fmt.Errorf("download size %d exceeds group limit %d (%w)", status.Total, maxSize, queue.CriticalErr) + } // First time to get status / total size changed, check user capacity if err := m.validateFiles(ctx, dep, status); err != nil { m.state.Status = status diff --git a/service/explorer/workflows.go b/service/explorer/workflows.go index fb2496ed..2e11c4eb 100644 --- a/service/explorer/workflows.go +++ b/service/explorer/workflows.go @@ -124,6 +124,23 @@ func (service *DownloadWorkflowService) CreateDownloadTask(c *gin.Context) ([]*T return nil, serializer.NewError(serializer.CodeBatchAria2Size, "", nil) } + // Concurrent active-task quota for this user's group. + if taskLimit := user.Edges.Group.Settings.Aria2TaskLimit; taskLimit > 0 { + active, err := dep.TaskClient().List(c, &inventory.ListTaskArgs{ + PaginationArgs: &inventory.PaginationArgs{PageSize: 1}, + UserID: user.ID, + Types: []string{queue.RemoteDownloadTaskType}, + Status: []task.Status{task.StatusQueued, task.StatusProcessing, task.StatusSuspending}, + }) + if err != nil { + return nil, serializer.NewError(serializer.CodeDBError, "Failed to count download tasks", err) + } + if active.PaginationResults.TotalItems >= taskLimit { + return nil, serializer.NewError(serializer.CodeBatchAria2Size, + fmt.Sprintf("Concurrent download task limit reached (%d)", taskLimit), nil) + } + } + // Validate src file if service.SrcFile != "" { src, err := fs.NewUriFromString(service.SrcFile)