提升文件上传体验 (#4393)

* 上传文件支持覆盖被占用的文件且上传失败时不影响原文件,增加强制覆盖选项

* 原文件存在时,保持同样的文件权限

* 制表符更新
This commit is contained in:
Node 2024-04-03 14:37:22 +08:00 committed by GitHub
parent 08da7802c6
commit e3eebc938a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -11,7 +11,7 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/dto/request" "github.com/1Panel-dev/1Panel/backend/app/dto/request"
@ -303,6 +303,15 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
} }
files := form.File["file"] files := form.File["file"]
paths := form.Value["path"] paths := form.Value["path"]
overwrite := true
if ow, ok := form.Value["overwrite"]; ok {
if len(ow) != 0 {
parseBool, _ := strconv.ParseBool(ow[0])
overwrite = parseBool
}
}
if len(paths) == 0 || !strings.Contains(paths[0], "/") { if len(paths) == 0 || !strings.Contains(paths[0], "/") {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error paths in request")) helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error paths in request"))
return return
@ -317,12 +326,31 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
success := 0 success := 0
failures := make(buserr.MultiErr) failures := make(buserr.MultiErr)
for _, file := range files { for _, file := range files {
if err := c.SaveUploadedFile(file, path.Join(paths[0], file.Filename)); err != nil { dstFilename := path.Join(paths[0], file.Filename)
tmpFilename := dstFilename + ".tmp"
if err := c.SaveUploadedFile(file, tmpFilename); err != nil {
_ = os.Remove(tmpFilename)
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err) e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
failures[file.Filename] = e failures[file.Filename] = e
global.LOG.Error(e) global.LOG.Error(e)
continue continue
} }
stat, statErr := os.Stat(dstFilename)
if overwrite {
_ = os.Remove(dstFilename)
}
err = os.Rename(tmpFilename, dstFilename)
if err != nil {
_ = os.Remove(tmpFilename)
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
failures[file.Filename] = e
global.LOG.Error(e)
continue
}
if statErr == nil {
_ = os.Chmod(dstFilename, stat.Mode())
}
success++ success++
} }
if success == 0 { if success == 0 {
@ -471,29 +499,29 @@ func (b *BaseApi) DownloadChunkFiles(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrFileDownloadDir, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrFileDownloadDir, err)
return return
} }
c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", req.Name)) c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", req.Name))
c.Writer.Header().Set("Content-Type", "application/octet-stream") c.Writer.Header().Set("Content-Type", "application/octet-stream")
c.Writer.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10)) c.Writer.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10))
c.Writer.Header().Set("Accept-Ranges", "bytes") c.Writer.Header().Set("Accept-Ranges", "bytes")
if c.Request.Header.Get("Range") != "" { if c.Request.Header.Get("Range") != "" {
rangeHeader := c.Request.Header.Get("Range") rangeHeader := c.Request.Header.Get("Range")
rangeArr := strings.Split(rangeHeader, "=")[1] rangeArr := strings.Split(rangeHeader, "=")[1]
rangeParts := strings.Split(rangeArr, "-") rangeParts := strings.Split(rangeArr, "-")
startPos, _ := strconv.ParseInt(rangeParts[0], 10, 64) startPos, _ := strconv.ParseInt(rangeParts[0], 10, 64)
var endPos int64 var endPos int64
if rangeParts[1] == "" { if rangeParts[1] == "" {
endPos = info.Size() - 1 endPos = info.Size() - 1
} else { } else {
endPos, _ = strconv.ParseInt(rangeParts[1], 10, 64) endPos, _ = strconv.ParseInt(rangeParts[1], 10, 64)
} }
c.Writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", startPos, endPos, info.Size())) c.Writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", startPos, endPos, info.Size()))
c.Writer.WriteHeader(http.StatusPartialContent) c.Writer.WriteHeader(http.StatusPartialContent)
buffer := make([]byte, 1024*1024) buffer := make([]byte, 1024*1024)
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
@ -501,7 +529,7 @@ func (b *BaseApi) DownloadChunkFiles(c *gin.Context) {
return return
} }
defer file.Close() defer file.Close()
_, _ = file.Seek(startPos, 0) _, _ = file.Seek(startPos, 0)
reader := io.LimitReader(file, endPos-startPos+1) reader := io.LimitReader(file, endPos-startPos+1)
_, err = io.CopyBuffer(c.Writer, reader, buffer) _, err = io.CopyBuffer(c.Writer, reader, buffer)
@ -549,7 +577,7 @@ func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int)
return err return err
} }
defer targetFile.Close() defer targetFile.Close()
for i := 0; i < chunkCount; i++ { for i := 0; i < chunkCount; i++ {
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", fileName, i)) chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", fileName, i))
chunkData, err := os.ReadFile(chunkPath) chunkData, err := os.ReadFile(chunkPath)
@ -561,7 +589,7 @@ func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int)
return err return err
} }
} }
return files.NewFileOp().DeleteDir(fileDir) return files.NewFileOp().DeleteDir(fileDir)
} }
@ -611,7 +639,7 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
_ = os.MkdirAll(fileDir, 0755) _ = os.MkdirAll(fileDir, 0755)
} }
filePath := filepath.Join(fileDir, filename) filePath := filepath.Join(fileDir, filename)
defer func() { defer func() {
if err != nil { if err != nil {
_ = os.Remove(fileDir) _ = os.Remove(fileDir)
@ -621,27 +649,27 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
emptyFile *os.File emptyFile *os.File
chunkData []byte chunkData []byte
) )
emptyFile, err = os.Create(filePath) emptyFile, err = os.Create(filePath)
if err != nil { if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return return
} }
defer emptyFile.Close() defer emptyFile.Close()
chunkData, err = io.ReadAll(uploadFile) chunkData, err = io.ReadAll(uploadFile)
if err != nil { if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err)) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err))
return return
} }
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex)) chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
err = os.WriteFile(chunkPath, chunkData, 0644) err = os.WriteFile(chunkPath, chunkData, 0644)
if err != nil { if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err)) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err))
return return
} }
if chunkIndex+1 == chunkCount { if chunkIndex+1 == chunkCount {
err = mergeChunks(filename, fileDir, c.PostForm("path"), chunkCount) err = mergeChunks(filename, fileDir, c.PostForm("path"), chunkCount)
if err != nil { if err != nil {