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 <info@tdvorak.dev>
pull/3580/head
Tomas Dvorak 2 weeks ago
parent fca45cedd4
commit f3ebf1fcea

@ -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.", "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", "aria2BatchSize": "Max batch size of remote download tasks",
"aria2BatchSizeDes": "Max number for submitting batched remote download tasks, fill in 0 to indicate no limit.", "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", "migratePolicy": "Relocate storage policy",
"migratePolicyDes": "Whether the user creates a storage policy relocation task.", "migratePolicyDes": "Whether the user creates a storage policy relocation task.",
"advanceDelete": "Advanced file deletion options", "advanceDelete": "Advanced file deletion options",

@ -1317,6 +1317,10 @@
"aria2OptionsDes": "qBittorrent 或 Aria2 下载器的任务额外配置参数,以 JSON 键值对格式书写,可用参数请查阅官方文档。", "aria2OptionsDes": "qBittorrent 或 Aria2 下载器的任务额外配置参数,以 JSON 键值对格式书写,可用参数请查阅官方文档。",
"aria2BatchSize": "批量离线下载最大数量", "aria2BatchSize": "批量离线下载最大数量",
"aria2BatchSizeDes": "批量创建离线下载时的最大数量,填写为 0 表示不限制。", "aria2BatchSizeDes": "批量创建离线下载时的最大数量,填写为 0 表示不限制。",
"aria2TaskLimit": "最大并发离线下载任务数",
"aria2TaskLimitDes": "每个用户同时处于排队、下载或暂停状态的离线下载任务上限,填 0 表示不限制。",
"aria2MaxFileSize": "单个下载任务最大体积(字节)",
"aria2MaxFileSizeDes": "解析后的下载总大小超过此值的任务将被中止,填 0 表示不限制。",
"migratePolicy": "存储策略转移", "migratePolicy": "存储策略转移",
"migratePolicyDes": "是否用户创建存储策略转移任务。", "migratePolicyDes": "是否用户创建存储策略转移任务。",
"advanceDelete": "高级文件删除选项", "advanceDelete": "高级文件删除选项",

@ -58,6 +58,8 @@ export interface GroupSetting {
remote_download_options?: Record<string, any>; remote_download_options?: Record<string, any>;
source_batch?: number; source_batch?: number;
aria2_batch?: number; aria2_batch?: number;
aria2_task_limit?: number;
aria2_max_file_size?: number;
max_walked_files?: number; max_walked_files?: number;
trash_retention?: number; trash_retention?: number;
redirected_source?: boolean; redirected_source?: boolean;

@ -132,6 +132,27 @@ const FileManagementSection = () => {
[setGroup], [setGroup],
); );
const onAria2TaskLimitChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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( const onAllowAdvanceDeleteChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
setGroup((p: GroupEnt) => ({ setGroup((p: GroupEnt) => ({
@ -322,6 +343,36 @@ const FileManagementSection = () => {
<NoMarginHelperText>{t("group.aria2BatchSizeDes")}</NoMarginHelperText> <NoMarginHelperText>{t("group.aria2BatchSizeDes")}</NoMarginHelperText>
</FormControl> </FormControl>
</SettingForm> </SettingForm>
<SettingForm title={t("group.aria2TaskLimit")} lgWidth={5}>
<FormControl fullWidth>
<DenseFilledTextField
slotProps={{
htmlInput: {
type: "number",
min: 0,
},
}}
value={values.settings?.aria2_task_limit ?? 0}
onChange={onAria2TaskLimitChange}
/>
<NoMarginHelperText>{t("group.aria2TaskLimitDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm title={t("group.aria2MaxFileSize")} lgWidth={5}>
<FormControl fullWidth>
<DenseFilledTextField
slotProps={{
htmlInput: {
type: "number",
min: 0,
},
}}
value={values.settings?.aria2_max_file_size ?? 0}
onChange={onAria2MaxFileSizeChange}
/>
<NoMarginHelperText>{t("group.aria2MaxFileSizeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
</Stack> </Stack>
</Collapse> </Collapse>
<SettingForm lgWidth={5}> <SettingForm lgWidth={5}>

@ -44,6 +44,10 @@ type (
RemoteDownloadOptions map[string]interface{} `json:"remote_download_options,omitempty"` // 离线下载用户组配置 RemoteDownloadOptions map[string]interface{} `json:"remote_download_options,omitempty"` // 离线下载用户组配置
SourceBatchSize int `json:"source_batch,omitempty"` SourceBatchSize int `json:"source_batch,omitempty"`
Aria2BatchSize int `json:"aria2_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"` MaxWalkedFiles int `json:"max_walked_files,omitempty"`
TrashRetention int `json:"trash_retention,omitempty"` TrashRetention int `json:"trash_retention,omitempty"`
RedirectedSource bool `json:"redirected_source,omitempty"` RedirectedSource bool `json:"redirected_source,omitempty"`

@ -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 { if m.state.Status == nil || m.state.Status.Total != status.Total {
m.l.Info("download size changed, re-validate files.") 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 // First time to get status / total size changed, check user capacity
if err := m.validateFiles(ctx, dep, status); err != nil { if err := m.validateFiles(ctx, dep, status); err != nil {
m.state.Status = status m.state.Status = status

@ -124,6 +124,23 @@ func (service *DownloadWorkflowService) CreateDownloadTask(c *gin.Context) ([]*T
return nil, serializer.NewError(serializer.CodeBatchAria2Size, "", nil) 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 // Validate src file
if service.SrcFile != "" { if service.SrcFile != "" {
src, err := fs.NewUriFromString(service.SrcFile) src, err := fs.NewUriFromString(service.SrcFile)

Loading…
Cancel
Save