diff --git a/agent/app/dto/request/file.go b/agent/app/dto/request/file.go index 36fe6883a..719f0a55a 100644 --- a/agent/app/dto/request/file.go +++ b/agent/app/dto/request/file.go @@ -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 { diff --git a/agent/app/service/file.go b/agent/app/service/file.go index 004406ec3..a4c1bd058 100644 --- a/agent/app/service/file.go +++ b/agent/app/service/file.go @@ -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() diff --git a/agent/utils/files/file_op.go b/agent/utils/files/file_op.go index 0dad332cf..3caf3a6ae 100644 --- a/agent/utils/files/file_op.go +++ b/agent/utils/files/file_op.go @@ -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() diff --git a/core/app/api/v2/setting.go b/core/app/api/v2/setting.go index bf1b66c69..6dacd5664 100644 --- a/core/app/api/v2/setting.go +++ b/core/app/api/v2/setting.go @@ -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 diff --git a/core/app/dto/setting.go b/core/app/dto/setting.go index c007e7c15..a8d68b13f 100644 --- a/core/app/dto/setting.go +++ b/core/app/dto/setting.go @@ -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"` diff --git a/core/app/service/setting.go b/core/app/service/setting.go index 2f62cd614..9604675f7 100644 --- a/core/app/service/setting.go +++ b/core/app/service/setting.go @@ -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 { diff --git a/core/router/ro_setting.go b/core/router/ro_setting.go index e7cf8c3e3..95568c4b7 100644 --- a/core/router/ro_setting.go +++ b/core/router/ro_setting.go @@ -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) diff --git a/frontend/src/api/interface/file.ts b/frontend/src/api/interface/file.ts index 55507fc1c..9dffba498 100644 --- a/frontend/src/api/interface/file.ts +++ b/frontend/src/api/interface/file.ts @@ -216,6 +216,7 @@ export namespace File { url: string; ignoreCertificate?: boolean; useProxy?: boolean; + useServerFilename?: boolean; } export interface FileWgetRes { diff --git a/frontend/src/api/modules/files.ts b/frontend/src/api/modules/files.ts index 70b83e0f5..0225b4b13 100644 --- a/frontend/src/api/modules/files.ts +++ b/frontend/src/api/modules/files.ts @@ -150,6 +150,14 @@ export const wgetFile = (params: File.FileWget) => { return http.post('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); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index a7bc2184e..5d72b46f0 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -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', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 2da3a3470..b75b3e2c3 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -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?', diff --git a/frontend/src/lang/modules/fa.ts b/frontend/src/lang/modules/fa.ts index 2ee0a3824..9b1eb0908 100644 --- a/frontend/src/lang/modules/fa.ts +++ b/frontend/src/lang/modules/fa.ts @@ -2474,12 +2474,15 @@ const message = { downloadProcess: 'پیشرفت دانلود', downloading: 'در حال دانلود...', stopWgetConfirm: 'آیا مطمئن هستید که می‌خواهید این وظیفه دانلود را متوقف کنید؟', + useServerFilename: 'استفاده از نام فایل ارائه‌شده توسط سرور', downloadRecordsNotRemoved: 'برخی رکوردها حذف نشدند. صفحه را تازه‌سازی کرده و دوباره تلاش کنید.', infoDetail: 'ویژگی‌های فایل', root: 'دایرکتوری ریشه', list: 'لیست فایل', sub: 'بازگشتی', downloadSuccess: 'دانلود با موفقیت انجام شد', + downloadFailed: 'دانلود ناموفق', + downloadFailureDetail: 'دانلود ناموفق: {error}', theme: 'پوسته', language: 'زبان', eol: 'پایان خط', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 24a4ebe41..351b56d3a 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -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: 'アップロードしたファイルに同じ名前のファイルが含まれています。上書きしますか?', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index fac5857de..847e8b1c8 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -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: '업로드한 파일에 동일한 이름의 파일이 포함되어 있습니다. 덮어쓰시겠습니까?', diff --git a/frontend/src/lang/modules/lo.ts b/frontend/src/lang/modules/lo.ts index 229bf7762..993a37948 100644 --- a/frontend/src/lang/modules/lo.ts +++ b/frontend/src/lang/modules/lo.ts @@ -2429,12 +2429,15 @@ const message = { downloadProcess: 'ຄວາມຄືບໜ້າການດາວໂຫຼດ', downloading: 'ກຳລັງດາວໂຫຼດ...', stopWgetConfirm: 'ທ່ານແນ່ໃຈບໍວ່າຕ້ອງການຢຸດງານດາວໂຫຼດນີ້?', + useServerFilename: 'ໃຊ້ຊື່ໄຟລ໌ທີ່ເຊີບເວີລະບຸ', downloadRecordsNotRemoved: 'ບາງບັນທຶກບໍ່ຖືກລຶບ. ກະລຸນາໂຫຼດໃໝ່ ແລະລອງອີກຄັ້ງ.', infoDetail: 'ຄຸນສົມບັດໄຟລ໌', root: 'ໄດເຣັກທໍຣີຮາກ (Root)', list: 'ລາຍການໄຟລ໌', sub: 'ລວມໂຟນເດີຍ່ອຍ', downloadSuccess: 'ດາວໂຫຼດສຳເລັດແລ້ວ', + downloadFailed: 'ດາວໂຫຼດລົ້ມເຫຼວ', + downloadFailureDetail: 'ດາວໂຫຼດລົ້ມເຫຼວ: {error}', theme: 'ຮູບແບບ', language: 'ພາສາ', eol: 'ຈົບແຖວ (EOL)', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 8ca1ef477..3892ca083 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -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?', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 2cd1140bd..dbb488838 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -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?', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index ef635bd1d..00b636cf1 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -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: 'Загруженный файл содержит файл с таким же именем. Заменить его?', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index ca9212b1f..00d3f53fc 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -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?', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index d25c3fbba..969af9940 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -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: '檔案列表', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 860c4f056..1791c94ef 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -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: '根目录', diff --git a/frontend/src/views/host/file-management/process/index.vue b/frontend/src/views/host/file-management/process/index.vue index f0bd7a2e8..bbc504292 100644 --- a/frontend/src/views/host/file-management/process/index.vue +++ b/frontend/src/views/host/file-management/process/index.vue @@ -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()); } diff --git a/frontend/src/views/host/file-management/wget/index.vue b/frontend/src/views/host/file-management/wget/index.vue index 4e1c46787..c72fcd8e7 100644 --- a/frontend/src/views/host/file-management/wget/index.vue +++ b/frontend/src/views/host/file-management/wget/index.vue @@ -21,6 +21,16 @@ + + + {{ $t('file.useServerFilename') }} + + {{ $t('file.useProxy') }} @@ -44,7 +54,11 @@