feat: Remote download supports server file name options and improves error handling (#13808)

* feat: Remote download supports server file name options and improves error handling

* feat: Remote download supports server file name options and improves error handling
This commit is contained in:
2026-09-15 10:04:44 +08:00
committed by GitHub
parent 2485b0aa5e
commit 75da53e374
23 changed files with 277 additions and 26 deletions

View File

@@ -122,6 +122,7 @@ type FileWget struct {
Name string `json:"name" validate:"required"`
IgnoreCertificate bool `json:"ignoreCertificate"`
UseProxy bool `json:"useProxy"`
UseServerFilename bool `json:"useServerFilename"`
}
type FileMove struct {

View File

@@ -896,6 +896,7 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
key := "file-wget-" + common.GetUuid()
options := files.DownloadOptions{
IgnoreCertificate: w.IgnoreCertificate,
UseServerFilename: w.UseServerFilename,
}
if w.UseProxy {
systemProxy, err := NewISettingService().GetSystemProxy()

View File

@@ -16,6 +16,7 @@ import (
"io"
"io/fs"
"math"
"mime"
"net"
"net/http"
"net/url"
@@ -28,6 +29,8 @@ import (
"sync"
"syscall"
"time"
"unicode"
"unicode/utf8"
"github.com/1Panel-dev/1Panel/agent/buserr"
@@ -386,9 +389,23 @@ type DownloadProxyConfig struct {
type DownloadOptions struct {
IgnoreCertificate bool
UseServerFilename bool
Proxy *DownloadProxyConfig
}
func downloadResponseFilename(header string) string {
_, params, err := mime.ParseMediaType(header)
if err != nil {
return ""
}
name := strings.TrimSpace(params["filename"])
if name == "" || name == "." || name == ".." || len(name) > 255 || !utf8.ValidString(name) ||
strings.ContainsAny(name, "/\\:") || strings.IndexFunc(name, unicode.IsControl) >= 0 {
return ""
}
return name
}
func buildDownloadProxyURL(proxy DownloadProxyConfig) (*url.URL, error) {
proxyType := strings.TrimSpace(proxy.Type)
proxyHost := strings.TrimSpace(proxy.URL)
@@ -455,7 +472,7 @@ type downloadPolicy struct {
idleTimeout time.Duration
}
var remoteDownloadPolicy = downloadPolicy{retries: 3, retryDelay: 2 * time.Second, idleTimeout: 90 * time.Second}
var remoteDownloadPolicy = downloadPolicy{retries: 3, retryDelay: 5 * time.Second, idleTimeout: 90 * time.Second}
func saveDownloadProcess(process Process) {
if process.Total > 0 {
@@ -492,20 +509,27 @@ func (f FileOp) DownloadFileWithProcess(rawURL, dst, key string, options Downloa
client.CloseIdleConnections()
return err
}
original, err := os.Lstat(dst)
if err != nil && !os.IsNotExist(err) {
client.CloseIdleConnections()
return err
}
if original != nil && !original.Mode().IsRegular() {
client.CloseIdleConnections()
return fmt.Errorf("download target must be a regular file")
parent = filepath.Dir(dst)
var original os.FileInfo
if !options.UseServerFilename {
original, err = os.Lstat(dst)
if err != nil && !os.IsNotExist(err) {
client.CloseIdleConnections()
return err
}
if original != nil && !original.Mode().IsRegular() {
client.CloseIdleConnections()
return fmt.Errorf("download target must be a regular file")
}
}
ctx, cancel := context.WithCancel(context.Background())
task := &downloadTask{cancel: cancel, done: make(chan struct{}), dst: dst}
if options.UseServerFilename {
task.dst = ""
}
downloadMu.Lock()
for _, active := range downloadTasks {
if active.dst == dst {
if task.dst != "" && active.dst == task.dst {
downloadMu.Unlock()
cancel()
client.CloseIdleConnections()
@@ -532,6 +556,34 @@ func (f FileOp) DownloadFileWithProcess(rawURL, dst, key string, options Downloa
close(task.done)
}()
process := Process{Key: key, Name: filepath.Base(dst), Status: "Downloading"}
nameResolved := !options.UseServerFilename
resolveName := func(resp *http.Response) (string, error) {
if nameResolved {
return dst, nil
}
name := downloadResponseFilename(resp.Header.Get("Content-Disposition"))
if name == "" {
name = filepath.Base(dst)
}
resolved := filepath.Join(parent, name)
process.Name = name
downloadMu.Lock()
defer downloadMu.Unlock()
for otherKey, active := range downloadTasks {
if otherKey != key && active.dst == resolved {
return "", buserr.New("TaskIsExecuting")
}
}
if _, statErr := os.Lstat(resolved); statErr == nil {
return "", fmt.Errorf("download target already exists: %s", name)
} else if !os.IsNotExist(statErr) {
return "", statErr
}
task.dst = resolved
dst = resolved
nameResolved = true
return dst, nil
}
update := func(state downloadState, status string, attempt int) {
process.Written = uint64(state.written)
process.Total = uint64(max(0, state.total))
@@ -553,7 +605,7 @@ func (f FileOp) DownloadFileWithProcess(rawURL, dst, key string, options Downloa
record, runErr = recordDownloadPart(out.Name(), partInfo)
}
if runErr == nil {
runErr = runRemoteDownload(ctx, client, rawURL, dst, out, remoteDownloadPolicy, update)
runErr = runRemoteDownload(ctx, client, rawURL, dst, out, remoteDownloadPolicy, update, resolveName)
}
task.mu.Lock()
if ctx.Err() != nil {
@@ -685,7 +737,7 @@ func retryDownloadError(err error) bool {
}
func runRemoteDownload(ctx context.Context, client *http.Client, rawURL, dst string, out *os.File,
policy downloadPolicy, update func(downloadState, string, int)) error {
policy downloadPolicy, update func(downloadState, string, int), resolveName ...func(*http.Response) (string, error)) error {
state := downloadState{total: -1}
for attempt := 0; ; attempt++ {
if err := ctx.Err(); err != nil {
@@ -693,7 +745,7 @@ func runRemoteDownload(ctx context.Context, client *http.Client, rawURL, dst str
}
update(state, "Downloading", attempt)
retry, retryAfter, err := downloadAttempt(ctx, client, rawURL, dst, out, &state, policy.idleTimeout,
func() { update(state, "Downloading", attempt) })
func() { update(state, "Downloading", attempt) }, resolveName...)
if err == nil {
return nil
}
@@ -719,7 +771,7 @@ func runRemoteDownload(ctx context.Context, client *http.Client, rawURL, dst str
}
func downloadAttempt(ctx context.Context, client *http.Client, rawURL, dst string, out *os.File,
state *downloadState, idleTimeout time.Duration, progress func()) (bool, time.Duration, error) {
state *downloadState, idleTimeout time.Duration, progress func(), resolveName ...func(*http.Response) (string, error)) (bool, time.Duration, error) {
attemptCtx, cancel := context.WithCancel(ctx)
defer cancel()
request, err := http.NewRequestWithContext(attemptCtx, http.MethodGet, rawURL, nil)
@@ -755,12 +807,6 @@ func downloadAttempt(ctx context.Context, client *http.Client, rawURL, dst strin
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
return false, 0, fmt.Errorf("remote download returned HTTP %d", resp.StatusCode)
}
ct := strings.ToLower(resp.Header.Get("Content-Type"))
ext := strings.ToLower(filepath.Ext(dst))
if (strings.Contains(ct, "text/html") || strings.Contains(ct, "text/xml")) &&
ext != ".html" && ext != ".htm" && ext != ".xml" && ext != ".svg" {
return false, 0, fmt.Errorf("unexpected download Content-Type: %s", ct)
}
if encoding := resp.Header.Get("Content-Encoding"); encoding != "" && !strings.EqualFold(encoding, "identity") {
return false, 0, fmt.Errorf("unexpected download Content-Encoding: %s", encoding)
}
@@ -793,6 +839,11 @@ func downloadAttempt(ctx context.Context, client *http.Client, rawURL, dst strin
state.etag = etag
}
}
if len(resolveName) > 0 {
if _, err := resolveName[0](resp); err != nil {
return false, 0, err
}
}
progress()
timer := time.AfterFunc(idleTimeout, cancel)
defer timer.Stop()

View File

@@ -17,10 +17,60 @@ import (
"github.com/1Panel-dev/1Panel/core/buserr"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/1Panel-dev/1Panel/core/global"
"github.com/1Panel-dev/1Panel/core/init/session/psession"
"github.com/1Panel-dev/1Panel/core/utils/common"
"github.com/gin-gonic/gin"
)
// @Tags System Setting
// @Summary Load current user's file download preference
// @Success 200 {object} dto.FileDownloadPreference
// @Router /core/settings/file/download [get]
func (b *BaseApi) GetFileDownloadPreference(c *gin.Context) {
user, ok := fileDownloadPreferenceUser(c)
if !ok {
return
}
preference, err := settingService.GetFileDownloadPreference(user.ID)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, preference)
}
// @Tags System Setting
// @Summary Update current user's file download preference
// @Accept json
// @Param request body dto.FileDownloadPreference true "request"
// @Success 200
// @Router /core/settings/file/download [post]
func (b *BaseApi) UpdateFileDownloadPreference(c *gin.Context) {
user, ok := fileDownloadPreferenceUser(c)
if !ok {
return
}
var req dto.FileDownloadPreference
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := settingService.UpdateFileDownloadPreference(user.ID, req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func fileDownloadPreferenceUser(c *gin.Context) (psession.SessionUser, bool) {
// Preferences always belong to the authenticated session, never a request-supplied user ID.
user, err := global.SESSION.Get(c)
if err != nil || user.ID == "" {
helper.BadAuth(c, "ErrNotLogin", buserr.New("ErrNotLogin"))
return psession.SessionUser{}, false
}
return user, true
}
// @Tags System Setting
// @Summary Load system setting info
// @Success 200 {object} dto.SettingInfo

View File

@@ -78,6 +78,10 @@ type SettingBaseInfo struct {
DashboardSimpleNodeVisible string `json:"dashboardSimpleNodeVisible"`
}
type FileDownloadPreference struct {
UseServerFilename bool `json:"useServerFilename"`
}
type SettingUpdate struct {
Key string `json:"key" validate:"required,base_setting_key"`
Value string `json:"value"`

View File

@@ -9,6 +9,7 @@ import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net"
@@ -38,13 +39,17 @@ import (
"github.com/1Panel-dev/1Panel/core/utils/xpack"
"github.com/gin-gonic/gin"
"golang.org/x/net/proxy"
"gorm.io/gorm"
)
type SettingService struct{}
var panelPortChangeMu sync.Mutex
var fileDownloadPreferenceMu sync.Mutex
type ISettingService interface {
GetFileDownloadPreference(userID string) (dto.FileDownloadPreference, error)
UpdateFileDownloadPreference(userID string, req dto.FileDownloadPreference) error
GetSettingInfo() (*dto.SettingInfo, error)
GetSettingBaseInfo() (*dto.SettingBaseInfo, error)
LoadInterfaceAddr() ([]string, error)
@@ -74,6 +79,37 @@ func NewISettingService() ISettingService {
return &SettingService{}
}
func (u *SettingService) GetFileDownloadPreference(userID string) (dto.FileDownloadPreference, error) {
var preference dto.FileDownloadPreference
if userID == "" {
return preference, buserr.New("ErrNotLogin")
}
fileDownloadPreferenceMu.Lock()
defer fileDownloadPreferenceMu.Unlock()
value, err := settingRepo.GetValueByKey("FileDownloadPreference:" + userID)
if errors.Is(err, gorm.ErrRecordNotFound) {
return preference, nil
}
if err != nil {
return preference, err
}
err = json.Unmarshal([]byte(value), &preference)
return preference, err
}
func (u *SettingService) UpdateFileDownloadPreference(userID string, req dto.FileDownloadPreference) error {
if userID == "" {
return buserr.New("ErrNotLogin")
}
value, err := json.Marshal(req)
if err != nil {
return err
}
fileDownloadPreferenceMu.Lock()
defer fileDownloadPreferenceMu.Unlock()
return settingRepo.UpdateOrCreate("FileDownloadPreference:"+userID, string(value))
}
func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
setting, err := settingRepo.List()
if err != nil {

View File

@@ -22,6 +22,8 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
Use(middleware.PasswordExpired())
{
settingRouter.POST("/search", baseApi.GetSettingInfo)
settingRouter.GET("/file/download", baseApi.GetFileDownloadPreference)
settingRouter.POST("/file/download", baseApi.UpdateFileDownloadPreference)
settingRouter.POST("/terminal/search", baseApi.GetTerminalSettingInfo)
settingRouter.GET("/search/available", baseApi.GetSystemAvailable)
settingRouter.POST("/update", baseApi.UpdateSetting)

View File

@@ -216,6 +216,7 @@ export namespace File {
url: string;
ignoreCertificate?: boolean;
useProxy?: boolean;
useServerFilename?: boolean;
}
export interface FileWgetRes {

View File

@@ -150,6 +150,14 @@ export const wgetFile = (params: File.FileWget) => {
return http.post<File.FileWgetRes>('files/wget', params);
};
export const getFileDownloadPreference = () => {
return http.get<{ useServerFilename: boolean }>('core/settings/file/download');
};
export const updateFileDownloadPreference = (useServerFilename: boolean) => {
return http.post('core/settings/file/download', { useServerFilename });
};
export const stopWgetFile = (key: string, currentNode?: string) => {
return http.post('files/wget/stop', { key }, undefined, currentNode ? { CurrentNode: currentNode } : undefined);
};

View File

@@ -2497,12 +2497,15 @@ const message = {
downloadProcess: 'Download progress',
downloading: 'Downloading...',
stopWgetConfirm: 'Are you sure you want to stop this download task?',
useServerFilename: 'Use server-provided filename',
downloadRecordsNotRemoved: 'Some records were not removed. Refresh and try again.',
infoDetail: 'File properties',
root: 'Root directory',
list: 'File list',
sub: 'Recursive',
downloadSuccess: 'Successfully downloaded',
downloadFailed: 'Download failed',
downloadFailureDetail: 'Download failed: {error}',
theme: 'Theme',
language: 'Language',
eol: 'End of line',

View File

@@ -2538,6 +2538,8 @@ const message = {
list: 'Lista de archivos',
sub: 'Recursivo',
downloadSuccess: 'Descarga completada correctamente',
downloadFailed: 'Descarga fallida',
downloadFailureDetail: 'Descarga fallida: {error}',
theme: 'Tema',
language: 'Idioma',
eol: 'Fin de línea',
@@ -2699,6 +2701,7 @@ const message = {
panelInstallDir: 'El directorio de instalación de 1Panel no puede eliminarse',
wgetTask: 'Tarea de descarga',
stopWgetConfirm: '¿Confirmar que desea detener esta tarea de descarga?',
useServerFilename: 'Usar el nombre de archivo del servidor',
downloadRecordsNotRemoved: 'No se eliminaron algunos registros. Actualice e inténtelo de nuevo.',
existFileTitle: 'Archivo con el mismo nombre',
existFileHelper: 'El archivo cargado contiene un archivo con el mismo nombre, ¿desea sobrescribirlo?',

View File

@@ -2474,12 +2474,15 @@ const message = {
downloadProcess: 'پیشرفت دانلود',
downloading: 'در حال دانلود...',
stopWgetConfirm: 'آیا مطمئن هستید که می‌خواهید این وظیفه دانلود را متوقف کنید؟',
useServerFilename: 'استفاده از نام فایل ارائه‌شده توسط سرور',
downloadRecordsNotRemoved: 'برخی رکوردها حذف نشدند. صفحه را تازه‌سازی کرده و دوباره تلاش کنید.',
infoDetail: 'ویژگی‌های فایل',
root: 'دایرکتوری ریشه',
list: 'لیست فایل',
sub: 'بازگشتی',
downloadSuccess: 'دانلود با موفقیت انجام شد',
downloadFailed: 'دانلود ناموفق',
downloadFailureDetail: 'دانلود ناموفق: {error}',
theme: 'پوسته',
language: 'زبان',
eol: 'پایان خط',

View File

@@ -2487,6 +2487,8 @@ const message = {
list: 'ファイルリスト',
sub: 'サブフォルダ',
downloadSuccess: 'ダウンロードに成功しました',
downloadFailed: 'ダウンロード失敗',
downloadFailureDetail: 'ダウンロード失敗{error}',
theme: 'テーマ',
language: '言語',
eol: '行の終わり',
@@ -2638,6 +2640,7 @@ const message = {
panelInstallDir: '1Panelインストールディレクトリは削除できません',
wgetTask: 'ダウンロードタスク',
stopWgetConfirm: 'このダウンロードタスクを停止しますか',
useServerFilename: 'サーバーが指定したファイル名を使用',
downloadRecordsNotRemoved: '一部の記録を削除できませんでした更新して再試行してください',
existFileTitle: '同名ファイルの警告',
existFileHelper: 'アップロードしたファイルに同じ名前のファイルが含まれています上書きしますか',

View File

@@ -2450,6 +2450,8 @@ const message = {
list: '파일 목록',
sub: '하위 폴더',
downloadSuccess: '다운로드 성공',
downloadFailed: '다운로드 실패',
downloadFailureDetail: '다운로드 실패: {error}',
theme: '테마',
language: '언어',
eol: '줄 끝',
@@ -2601,6 +2603,7 @@ const message = {
panelInstallDir: '1Panel 설치 디렉터리는 삭제할 수 없습니다.',
wgetTask: '다운로드 작업',
stopWgetConfirm: '이 다운로드 작업을 중지하시겠습니까?',
useServerFilename: '서버에서 제공한 파일 이름 사용',
downloadRecordsNotRemoved: '일부 기록을 제거하지 못했습니다. 새로 고침 후 다시 시도하세요.',
existFileTitle: '동일한 이름의 파일 경고',
existFileHelper: '업로드한 파일에 동일한 이름의 파일이 포함되어 있습니다. 덮어쓰시겠습니까?',

View File

@@ -2429,12 +2429,15 @@ const message = {
downloadProcess: 'ຄວາມຄືບໜ້າການດາວໂຫຼດ',
downloading: 'ກຳລັງດາວໂຫຼດ...',
stopWgetConfirm: 'ທ່ານແນ່ໃຈບໍວ່າຕ້ອງການຢຸດງານດາວໂຫຼດນີ້?',
useServerFilename: 'ໃຊ້ຊື່ໄຟລ໌ທີ່ເຊີບເວີລະບຸ',
downloadRecordsNotRemoved: 'ບາງບັນທຶກບໍ່ຖືກລຶບ. ກະລຸນາໂຫຼດໃໝ່ ແລະລອງອີກຄັ້ງ.',
infoDetail: 'ຄຸນສົມບັດໄຟລ໌',
root: 'ໄດເຣັກທໍຣີຮາກ (Root)',
list: 'ລາຍການໄຟລ໌',
sub: 'ລວມໂຟນເດີຍ່ອຍ',
downloadSuccess: 'ດາວໂຫຼດສຳເລັດແລ້ວ',
downloadFailed: 'ດາວໂຫຼດລົ້ມເຫຼວ',
downloadFailureDetail: 'ດາວໂຫຼດລົ້ມເຫຼວ: {error}',
theme: 'ຮູບແບບ',
language: 'ພາສາ',
eol: 'ຈົບແຖວ (EOL)',

View File

@@ -2537,6 +2537,8 @@ const message = {
list: 'Senarai fail',
sub: 'Subfolder',
downloadSuccess: 'Berjaya dimuat turun',
downloadFailed: 'Muat turun gagal',
downloadFailureDetail: 'Muat turun gagal: {error}',
theme: 'Tema',
language: 'Bahasa',
eol: 'Akhir baris',
@@ -2698,6 +2700,7 @@ const message = {
panelInstallDir: 'Direktori pemasangan 1Panel tidak boleh dipadamkan',
wgetTask: 'Tugas Muat Turun',
stopWgetConfirm: 'Adakah anda pasti mahu menghentikan tugas muat turun ini?',
useServerFilename: 'Gunakan nama fail daripada pelayan',
downloadRecordsNotRemoved: 'Sesetengah rekod tidak dibuang. Muat semula dan cuba lagi.',
existFileTitle: 'Amaran fail dengan nama yang sama',
existFileHelper: 'Fail yang dimuat naik mengandungi fail dengan nama yang sama. Adakah anda mahu menimpanya?',

View File

@@ -2537,6 +2537,8 @@ const message = {
list: 'Lista de arquivos',
sub: 'Subpastas',
downloadSuccess: 'Baixado com sucesso',
downloadFailed: 'Falha no download',
downloadFailureDetail: 'Falha no download: {error}',
theme: 'Tema',
language: 'Idioma',
eol: 'Fim de linha',
@@ -2697,6 +2699,7 @@ const message = {
panelInstallDir: 'O diretório de instalação do 1Panel não pode ser excluído',
wgetTask: 'Tarefa de Download',
stopWgetConfirm: 'Tem certeza de que deseja parar esta tarefa de download?',
useServerFilename: 'Usar o nome de arquivo fornecido pelo servidor',
downloadRecordsNotRemoved: 'Alguns registros não foram removidos. Atualize e tente novamente.',
existFileTitle: 'Aviso de arquivo com o mesmo nome',
existFileHelper: 'O arquivo enviado contém um arquivo com o mesmo nome. Deseja substituí-lo?',

View File

@@ -2518,6 +2518,8 @@ const message = {
list: 'Список файлов',
sub: 'Подпапки',
downloadSuccess: 'Успешно скачано',
downloadFailed: 'Ошибка загрузки',
downloadFailureDetail: 'Ошибка загрузки: {error}',
theme: 'Тема',
language: 'Язык',
eol: 'Конец строки',
@@ -2672,6 +2674,7 @@ const message = {
panelInstallDir: 'Директорию установки 1Panel нельзя удалить',
wgetTask: 'Задача загрузки',
stopWgetConfirm: 'Вы уверены, что хотите остановить эту задачу загрузки?',
useServerFilename: 'Использовать имя файла с сервера',
downloadRecordsNotRemoved: 'Некоторые записи не удалены. Обновите страницу и повторите попытку.',
existFileTitle: 'Предупреждение о файле с тем же именем',
existFileHelper: 'Загруженный файл содержит файл с таким же именем. Заменить его?',

View File

@@ -2528,6 +2528,8 @@ const message = {
list: 'Dosya listesi',
sub: 'Alt dizin',
downloadSuccess: 'Başarıyla indirildi',
downloadFailed: 'İndirme başarısız',
downloadFailureDetail: 'İndirme başarısız: {error}',
theme: 'Tema',
language: 'Dil',
eol: 'Satır sonu',
@@ -2687,6 +2689,7 @@ const message = {
panelInstallDir: '1Panel kurulum dizini silinemez',
wgetTask: 'İndirme Görevi',
stopWgetConfirm: 'Bu indirme görevini durdurmak istediğinizden emin misiniz?',
useServerFilename: 'Sunucunun sağladığı dosya adını kullan',
downloadRecordsNotRemoved: 'Bazı kayıtlar kaldırılamadı. Yenileyip tekrar deneyin.',
existFileTitle: 'Aynı ada sahip dosya uyarısı',
existFileHelper: 'Yüklenen dosya, aynı ada sahip bir dosya içeriyor, üzerine yazmak istiyor musunuz?',

View File

@@ -2340,6 +2340,8 @@ const message = {
linkPath: '連結路徑',
selectFile: '選擇檔案',
downloadSuccess: '下載成功',
downloadFailed: '下載失敗',
downloadFailureDetail: '下載失敗:{error}',
downloadUrl: '下載網址',
downloadStart: '下載開始!',
wgetUrlInvalid: '請輸入有效的 http(s) 下載網址',
@@ -2360,6 +2362,7 @@ const message = {
downloading: '正在下載...',
infoDetail: '檔案屬性',
stopWgetConfirm: '確認停止該下載任務?',
useServerFilename: '使用伺服器提供的檔案名稱',
downloadRecordsNotRemoved: '部分紀錄未移除,請重新整理後重試。',
root: '根目錄',
list: '檔案列表',

View File

@@ -2371,6 +2371,8 @@ const message = {
linkPath: '链接路径',
selectFile: '选择文件',
downloadSuccess: '下载成功',
downloadFailed: '下载失败',
downloadFailureDetail: '下载失败:{error}',
downloadUrl: '下载地址',
downloadStart: '下载开始!',
wgetUrlInvalid: '请输入有效的 http(s) 下载地址',
@@ -2390,6 +2392,7 @@ const message = {
downloadProcess: '下载进度',
downloading: '正在下载...',
stopWgetConfirm: '确认停止该下载任务?',
useServerFilename: '使用服务器提供的文件名',
downloadRecordsNotRemoved: '部分记录未移除,请刷新后重试。',
infoDetail: '文件属性',
root: '根目录',

View File

@@ -154,14 +154,22 @@ const onMessage = async (message: any) => {
const failures = res.value.filter((value) => getStatus(value) === 'Failed' && !reportedFailures.has(value.key));
if (failures.length > 0) {
failures.forEach((value) => reportedFailures.add(value.key));
MsgError(failures.map((value) => `${value.name}: ${value.error || getStatusText(value)}`).join('\n'));
MsgError(
failures
.map((value) =>
i18n.global.t(value.error ? 'file.downloadFailureDetail' : 'file.downloadFailed', {
error: value.error,
}),
)
.join('\n'),
);
}
const successes = res.value.filter(
(value) => getStatus(value) === 'Success' && !reportedSuccesses.has(value.key),
);
if (successes.length > 0) {
successes.forEach((value) => reportedSuccesses.add(value.key));
MsgSuccess(successes.map((value) => `${value.name}: ${getStatusText(value)}`).join('\n'));
MsgSuccess(i18n.global.t('file.downloadSuccess'));
}
await onRemove(getAutoRemoveKeys());
}

View File

@@ -21,6 +21,16 @@
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model="addForm.name"></el-input>
</el-form-item>
<el-form-item>
<el-checkbox
v-model="addForm.useServerFilename"
:disabled="!preferenceReady || preferenceLoading || preferenceSaving || loading"
class="!h-auto [&_.el-checkbox__label]:whitespace-normal"
@change="saveServerFilenamePreference"
>
{{ $t('file.useServerFilename') }}
</el-checkbox>
</el-form-item>
<el-form-item>
<el-checkbox v-model="addForm.useProxy">
{{ $t('file.useProxy') }}
@@ -44,7 +54,11 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose()" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(fileForm)" :disabled="loading">
<el-button
type="primary"
@click="submit(fileForm)"
:disabled="loading || preferenceLoading || preferenceSaving"
>
{{ $t('commons.button.confirm') }}
</el-button>
</span>
@@ -54,7 +68,7 @@
</template>
<script lang="ts" setup>
import { wgetFile } from '@/api/modules/files';
import { getFileDownloadPreference, updateFileDownloadPreference, wgetFile } from '@/api/modules/files';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { FormInstance, FormRules } from 'element-plus';
@@ -70,6 +84,11 @@ interface WgetProps {
const fileForm = ref<FormInstance>();
const loading = ref(false);
const preferenceLoading = ref(false);
const preferenceSaving = ref(false);
const preferenceReady = ref(false);
let preferenceToken = 0;
let savedServerFilename = false;
const isAppendOnly = ref(false);
let open = ref(false);
let submitData = ref(false);
@@ -105,11 +124,13 @@ const addForm = reactive({
name: '',
ignoreCertificate: false,
useProxy: false,
useServerFilename: false,
});
const em = defineEmits(['close']);
const handleClose = () => {
preferenceToken++;
if (fileForm.value) {
fileForm.value.resetFields();
}
@@ -122,7 +143,7 @@ const getPath = (path: string) => {
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
if (!formEl || preferenceLoading.value || preferenceSaving.value || loading.value) return;
await formEl.validate((valid) => {
if (!valid) {
return;
@@ -147,13 +168,45 @@ const getFileName = (url: string) => {
addForm.name = getFilenameFromUrl(url);
};
const acceptParams = (props: WgetProps) => {
const saveServerFilenamePreference = async () => {
if (!preferenceReady.value || preferenceSaving.value) return;
const token = preferenceToken;
const value = addForm.useServerFilename;
preferenceSaving.value = true;
try {
await updateFileDownloadPreference(value);
if (token === preferenceToken) savedServerFilename = value;
} catch {
if (token === preferenceToken) addForm.useServerFilename = savedServerFilename;
} finally {
if (token === preferenceToken) preferenceSaving.value = false;
}
};
const acceptParams = async (props: WgetProps) => {
const token = ++preferenceToken;
addForm.path = props.path;
isAppendOnly.value = Boolean(props.isAppendOnly);
open.value = true;
submitData.value = false;
addForm.ignoreCertificate = false;
addForm.useProxy = false;
addForm.useServerFilename = false;
savedServerFilename = false;
preferenceReady.value = false;
preferenceSaving.value = false;
preferenceLoading.value = true;
try {
const result = await getFileDownloadPreference();
if (token !== preferenceToken || !open.value) return;
savedServerFilename = result.data?.useServerFilename === true;
addForm.useServerFilename = savedServerFilename;
preferenceReady.value = true;
} catch {
// Older Core versions can still use the existing manual-name download flow.
} finally {
if (token === preferenceToken) preferenceLoading.value = false;
}
};
defineExpose({ acceptParams });