mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
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:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -216,6 +216,7 @@ export namespace File {
|
||||
url: string;
|
||||
ignoreCertificate?: boolean;
|
||||
useProxy?: boolean;
|
||||
useServerFilename?: boolean;
|
||||
}
|
||||
|
||||
export interface FileWgetRes {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -2474,12 +2474,15 @@ const message = {
|
||||
downloadProcess: 'پیشرفت دانلود',
|
||||
downloading: 'در حال دانلود...',
|
||||
stopWgetConfirm: 'آیا مطمئن هستید که میخواهید این وظیفه دانلود را متوقف کنید؟',
|
||||
useServerFilename: 'استفاده از نام فایل ارائهشده توسط سرور',
|
||||
downloadRecordsNotRemoved: 'برخی رکوردها حذف نشدند. صفحه را تازهسازی کرده و دوباره تلاش کنید.',
|
||||
infoDetail: 'ویژگیهای فایل',
|
||||
root: 'دایرکتوری ریشه',
|
||||
list: 'لیست فایل',
|
||||
sub: 'بازگشتی',
|
||||
downloadSuccess: 'دانلود با موفقیت انجام شد',
|
||||
downloadFailed: 'دانلود ناموفق',
|
||||
downloadFailureDetail: 'دانلود ناموفق: {error}',
|
||||
theme: 'پوسته',
|
||||
language: 'زبان',
|
||||
eol: 'پایان خط',
|
||||
|
||||
@@ -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: 'アップロードしたファイルに同じ名前のファイルが含まれています。上書きしますか?',
|
||||
|
||||
@@ -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: '업로드한 파일에 동일한 이름의 파일이 포함되어 있습니다. 덮어쓰시겠습니까?',
|
||||
|
||||
@@ -2429,12 +2429,15 @@ const message = {
|
||||
downloadProcess: 'ຄວາມຄືບໜ້າການດາວໂຫຼດ',
|
||||
downloading: 'ກຳລັງດາວໂຫຼດ...',
|
||||
stopWgetConfirm: 'ທ່ານແນ່ໃຈບໍວ່າຕ້ອງການຢຸດງານດາວໂຫຼດນີ້?',
|
||||
useServerFilename: 'ໃຊ້ຊື່ໄຟລ໌ທີ່ເຊີບເວີລະບຸ',
|
||||
downloadRecordsNotRemoved: 'ບາງບັນທຶກບໍ່ຖືກລຶບ. ກະລຸນາໂຫຼດໃໝ່ ແລະລອງອີກຄັ້ງ.',
|
||||
infoDetail: 'ຄຸນສົມບັດໄຟລ໌',
|
||||
root: 'ໄດເຣັກທໍຣີຮາກ (Root)',
|
||||
list: 'ລາຍການໄຟລ໌',
|
||||
sub: 'ລວມໂຟນເດີຍ່ອຍ',
|
||||
downloadSuccess: 'ດາວໂຫຼດສຳເລັດແລ້ວ',
|
||||
downloadFailed: 'ດາວໂຫຼດລົ້ມເຫຼວ',
|
||||
downloadFailureDetail: 'ດາວໂຫຼດລົ້ມເຫຼວ: {error}',
|
||||
theme: 'ຮູບແບບ',
|
||||
language: 'ພາສາ',
|
||||
eol: 'ຈົບແຖວ (EOL)',
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -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: 'Загруженный файл содержит файл с таким же именем. Заменить его?',
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -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: '檔案列表',
|
||||
|
||||
@@ -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: '根目录',
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user