Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions agent/app/dto/request/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions agent/app/service/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
91 changes: 71 additions & 20 deletions agent/utils/files/file_op.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"io"
"io/fs"
"math"
"mime"
"net"
"net/http"
"net/url"
Expand All @@ -28,6 +29,8 @@ import (
"sync"
"syscall"
"time"
"unicode"
"unicode/utf8"

"github.com/1Panel-dev/1Panel/agent/buserr"

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand All @@ -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))
Expand All @@ -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 {
Expand Down Expand Up @@ -685,15 +737,15 @@ 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 {
return err
}
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
}
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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()
Expand Down
50 changes: 50 additions & 0 deletions core/app/api/v2/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions core/app/dto/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
36 changes: 36 additions & 0 deletions core/app/service/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions core/router/ro_setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api/interface/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export namespace File {
url: string;
ignoreCertificate?: boolean;
useProxy?: boolean;
useServerFilename?: boolean;
}

export interface FileWgetRes {
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/api/modules/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/lang/modules/es-es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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?',
Expand Down
Loading
Loading