mirror of
				https://github.com/1Panel-dev/1Panel.git
				synced 2025-10-25 06:56:32 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			822 lines
		
	
	
	
		
			25 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			822 lines
		
	
	
	
		
			25 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package v1
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"path/filepath"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"syscall"
 | |
| 
 | |
| 	"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/request"
 | |
| 	"github.com/1Panel-dev/1Panel/backend/app/dto/response"
 | |
| 	"github.com/1Panel-dev/1Panel/backend/buserr"
 | |
| 	"github.com/1Panel-dev/1Panel/backend/constant"
 | |
| 	"github.com/1Panel-dev/1Panel/backend/global"
 | |
| 	"github.com/1Panel-dev/1Panel/backend/utils/files"
 | |
| 	websocket2 "github.com/1Panel-dev/1Panel/backend/utils/websocket"
 | |
| 	"github.com/gin-gonic/gin"
 | |
| 	"github.com/gorilla/websocket"
 | |
| )
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary List files
 | |
| // @Description 获取文件列表
 | |
| // @Accept json
 | |
| // @Param request body request.FileOption true "request"
 | |
| // @Success 200 {object} response.FileInfo
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/search [post]
 | |
| func (b *BaseApi) ListFiles(c *gin.Context) {
 | |
| 	var req request.FileOption
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	files, err := fileService.GetFileList(req)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, files)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Page file
 | |
| // @Description 分页获取上传文件
 | |
| // @Accept json
 | |
| // @Param request body request.SearchUploadWithPage true "request"
 | |
| // @Success 200 {array} response.FileInfo
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/upload/search [post]
 | |
| func (b *BaseApi) SearchUploadWithPage(c *gin.Context) {
 | |
| 	var req request.SearchUploadWithPage
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	total, files, err := fileService.SearchUploadWithPage(req)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, dto.PageResult{
 | |
| 		Items: files,
 | |
| 		Total: total,
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Load files tree
 | |
| // @Description 加载文件树
 | |
| // @Accept json
 | |
| // @Param request body request.FileOption true "request"
 | |
| // @Success 200 {array} response.FileTree
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/tree [post]
 | |
| func (b *BaseApi) GetFileTree(c *gin.Context) {
 | |
| 	var req request.FileOption
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	tree, err := fileService.GetFileTree(req)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, tree)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Create file
 | |
| // @Description 创建文件/文件夹
 | |
| // @Accept json
 | |
| // @Param request body request.FileCreate true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files [post]
 | |
| // @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建文件/文件夹 [path]","formatEN":"Create dir or file [path]"}
 | |
| func (b *BaseApi) CreateFile(c *gin.Context) {
 | |
| 	var req request.FileCreate
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	err := fileService.Create(req)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, nil)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Delete file
 | |
| // @Description 删除文件/文件夹
 | |
| // @Accept json
 | |
| // @Param request body request.FileDelete true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/del [post]
 | |
| // @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除文件/文件夹 [path]","formatEN":"Delete dir or file [path]"}
 | |
| func (b *BaseApi) DeleteFile(c *gin.Context) {
 | |
| 	var req request.FileDelete
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	err := fileService.Delete(req)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, nil)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Batch delete file
 | |
| // @Description 批量删除文件/文件夹
 | |
| // @Accept json
 | |
| // @Param request body request.FileBatchDelete true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/batch/del [post]
 | |
| // @x-panel-log {"bodyKeys":["paths"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量删除文件/文件夹 [paths]","formatEN":"Batch delete dir or file [paths]"}
 | |
| func (b *BaseApi) BatchDeleteFile(c *gin.Context) {
 | |
| 	var req request.FileBatchDelete
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	err := fileService.BatchDelete(req)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, nil)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Change file mode
 | |
| // @Description 修改文件权限
 | |
| // @Accept json
 | |
| // @Param request body request.FileCreate true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/mode [post]
 | |
| // @x-panel-log {"bodyKeys":["path","mode"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改权限 [paths] => [mode]","formatEN":"Change mode [paths] => [mode]"}
 | |
| func (b *BaseApi) ChangeFileMode(c *gin.Context) {
 | |
| 	var req request.FileCreate
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	err := fileService.ChangeMode(req)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithOutData(c)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Change file owner
 | |
| // @Description 修改文件用户/组
 | |
| // @Accept json
 | |
| // @Param request body request.FileRoleUpdate true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/owner [post]
 | |
| // @x-panel-log {"bodyKeys":["path","user","group"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改用户/组 [paths] => [user]/[group]","formatEN":"Change owner [paths] => [user]/[group]"}
 | |
| func (b *BaseApi) ChangeFileOwner(c *gin.Context) {
 | |
| 	var req request.FileRoleUpdate
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	if err := fileService.ChangeOwner(req); err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithOutData(c)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Compress file
 | |
| // @Description 压缩文件
 | |
| // @Accept json
 | |
| // @Param request body request.FileCompress true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/compress [post]
 | |
| // @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"压缩文件 [name]","formatEN":"Compress file [name]"}
 | |
| func (b *BaseApi) CompressFile(c *gin.Context) {
 | |
| 	var req request.FileCompress
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	err := fileService.Compress(req)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, nil)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Decompress file
 | |
| // @Description 解压文件
 | |
| // @Accept json
 | |
| // @Param request body request.FileDeCompress true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/decompress [post]
 | |
| // @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"解压 [path]","formatEN":"Decompress file [path]"}
 | |
| func (b *BaseApi) DeCompressFile(c *gin.Context) {
 | |
| 	var req request.FileDeCompress
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	err := fileService.DeCompress(req)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, nil)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Load file content
 | |
| // @Description 获取文件内容
 | |
| // @Accept json
 | |
| // @Param request body request.FileContentReq true "request"
 | |
| // @Success 200 {object} response.FileInfo
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/content [post]
 | |
| // @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"获取文件内容 [path]","formatEN":"Load file content [path]"}
 | |
| func (b *BaseApi) GetContent(c *gin.Context) {
 | |
| 	var req request.FileContentReq
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	info, err := fileService.GetContent(req)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, info)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Update file content
 | |
| // @Description 更新文件内容
 | |
| // @Accept json
 | |
| // @Param request body request.FileEdit true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/save [post]
 | |
| // @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新文件内容 [path]","formatEN":"Update file content [path]"}
 | |
| func (b *BaseApi) SaveContent(c *gin.Context) {
 | |
| 	var req request.FileEdit
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	if err := fileService.SaveContent(req); err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, nil)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Upload file
 | |
| // @Description 上传文件
 | |
| // @Param file formData file true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/upload [post]
 | |
| // @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"上传文件 [path]","formatEN":"Upload file [path]"}
 | |
| func (b *BaseApi) UploadFiles(c *gin.Context) {
 | |
| 	form, err := c.MultipartForm()
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
 | |
| 		return
 | |
| 	}
 | |
| 	uploadFiles := form.File["file"]
 | |
| 	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], "/") {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, errors.New("error paths in request"))
 | |
| 		return
 | |
| 	}
 | |
| 	dir := path.Dir(paths[0])
 | |
| 
 | |
| 	_, err = os.Stat(dir)
 | |
| 	if err != nil && os.IsNotExist(err) {
 | |
| 		mode, err := files.GetParentMode(dir)
 | |
| 		if err != nil {
 | |
| 			helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 			return
 | |
| 		}
 | |
| 		if err = os.MkdirAll(dir, mode); err != nil {
 | |
| 			helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, fmt.Errorf("mkdir %s failed, err: %v", dir, err))
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 	info, err := os.Stat(dir)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	mode := info.Mode()
 | |
| 	fileOp := files.NewFileOp()
 | |
| 	stat, ok := info.Sys().(*syscall.Stat_t)
 | |
| 	uid, gid := -1, -1
 | |
| 	if ok {
 | |
| 		uid, gid = int(stat.Uid), int(stat.Gid)
 | |
| 	}
 | |
| 
 | |
| 	success := 0
 | |
| 	failures := make(buserr.MultiErr)
 | |
| 	for _, file := range uploadFiles {
 | |
| 		dstFilename := path.Join(paths[0], file.Filename)
 | |
| 		dstDir := path.Dir(dstFilename)
 | |
| 		if !fileOp.Stat(dstDir) {
 | |
| 			if err = fileOp.CreateDir(dstDir, mode); err != nil {
 | |
| 				e := fmt.Errorf("create dir [%s] failed, err: %v", path.Dir(dstFilename), err)
 | |
| 				failures[file.Filename] = e
 | |
| 				global.LOG.Error(e)
 | |
| 				continue
 | |
| 			}
 | |
| 			_ = os.Chown(dstDir, uid, gid)
 | |
| 		}
 | |
| 		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)
 | |
| 			failures[file.Filename] = e
 | |
| 			global.LOG.Error(e)
 | |
| 			continue
 | |
| 		}
 | |
| 		dstInfo, 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, dstInfo.Mode())
 | |
| 		} else {
 | |
| 			_ = os.Chmod(dstFilename, mode)
 | |
| 		}
 | |
| 		if uid != -1 && gid != -1 {
 | |
| 			_ = os.Chown(dstFilename, uid, gid)
 | |
| 		}
 | |
| 		success++
 | |
| 	}
 | |
| 	if success == 0 {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, failures)
 | |
| 	} else {
 | |
| 		helper.SuccessWithMsg(c, fmt.Sprintf("%d files upload success", success))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Check file exist
 | |
| // @Description 检测文件是否存在
 | |
| // @Accept json
 | |
| // @Param request body request.FilePathCheck true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/check [post]
 | |
| func (b *BaseApi) CheckFile(c *gin.Context) {
 | |
| 	var req request.FilePathCheck
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	if _, err := os.Stat(req.Path); err != nil {
 | |
| 		helper.SuccessWithData(c, false)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, true)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Change file name
 | |
| // @Description 修改文件名称
 | |
| // @Accept json
 | |
| // @Param request body request.FileRename true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/rename [post]
 | |
| // @x-panel-log {"bodyKeys":["oldName","newName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"重命名 [oldName] => [newName]","formatEN":"Rename [oldName] => [newName]"}
 | |
| func (b *BaseApi) ChangeFileName(c *gin.Context) {
 | |
| 	var req request.FileRename
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	if err := fileService.ChangeName(req); err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, nil)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Wget file
 | |
| // @Description 下载远端文件
 | |
| // @Accept json
 | |
| // @Param request body request.FileWget true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/wget [post]
 | |
| // @x-panel-log {"bodyKeys":["url","path","name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载 url => [path]/[name]","formatEN":"Download url => [path]/[name]"}
 | |
| func (b *BaseApi) WgetFile(c *gin.Context) {
 | |
| 	var req request.FileWget
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	key, err := fileService.Wget(req)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, response.FileWgetRes{
 | |
| 		Key: key,
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Move file
 | |
| // @Description 移动文件
 | |
| // @Accept json
 | |
| // @Param request body request.FileMove true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/move [post]
 | |
| // @x-panel-log {"bodyKeys":["oldPaths","newPath"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"移动文件 [oldPaths] => [newPath]","formatEN":"Move [oldPaths] => [newPath]"}
 | |
| func (b *BaseApi) MoveFile(c *gin.Context) {
 | |
| 	var req request.FileMove
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	if err := fileService.MvFile(req); err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, nil)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Download file
 | |
| // @Description 下载文件
 | |
| // @Accept json
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/download [get]
 | |
| func (b *BaseApi) Download(c *gin.Context) {
 | |
| 	filePath := c.Query("path")
 | |
| 	file, err := os.Open(filePath)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 	}
 | |
| 	defer file.Close()
 | |
| 	info, _ := file.Stat()
 | |
| 	c.Header("Content-Length", strconv.FormatInt(info.Size(), 10))
 | |
| 	c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
 | |
| 	http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Chunk Download file
 | |
| // @Description 分片下载下载文件
 | |
| // @Accept json
 | |
| // @Param request body request.FileDownload true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/chunkdownload [post]
 | |
| // @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"下载文件 [name]","formatEN":"Download file [name]"}
 | |
| func (b *BaseApi) DownloadChunkFiles(c *gin.Context) {
 | |
| 	var req request.FileChunkDownload
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	fileOp := files.NewFileOp()
 | |
| 	if !fileOp.Stat(req.Path) {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrPathNotFound, nil)
 | |
| 		return
 | |
| 	}
 | |
| 	filePath := req.Path
 | |
| 	fstFile, err := fileOp.OpenFile(filePath)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	info, err := fstFile.Stat()
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	if info.IsDir() {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrFileDownloadDir, err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	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-Length", strconv.FormatInt(info.Size(), 10))
 | |
| 	c.Writer.Header().Set("Accept-Ranges", "bytes")
 | |
| 
 | |
| 	if c.Request.Header.Get("Range") != "" {
 | |
| 		rangeHeader := c.Request.Header.Get("Range")
 | |
| 		rangeArr := strings.Split(rangeHeader, "=")[1]
 | |
| 		rangeParts := strings.Split(rangeArr, "-")
 | |
| 
 | |
| 		startPos, _ := strconv.ParseInt(rangeParts[0], 10, 64)
 | |
| 
 | |
| 		var endPos int64
 | |
| 		if rangeParts[1] == "" {
 | |
| 			endPos = info.Size() - 1
 | |
| 		} else {
 | |
| 			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.WriteHeader(http.StatusPartialContent)
 | |
| 
 | |
| 		buffer := make([]byte, 1024*1024)
 | |
| 		file, err := os.Open(filePath)
 | |
| 		if err != nil {
 | |
| 			helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 			return
 | |
| 		}
 | |
| 		defer file.Close()
 | |
| 
 | |
| 		_, _ = file.Seek(startPos, 0)
 | |
| 		reader := io.LimitReader(file, endPos-startPos+1)
 | |
| 		_, err = io.CopyBuffer(c.Writer, reader, buffer)
 | |
| 		if err != nil {
 | |
| 			helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 			return
 | |
| 		}
 | |
| 	} else {
 | |
| 		c.File(filePath)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Load file size
 | |
| // @Description 获取文件夹大小
 | |
| // @Accept json
 | |
| // @Param request body request.DirSizeReq true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/size [post]
 | |
| // @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"获取文件夹大小 [path]","formatEN":"Load file size [path]"}
 | |
| func (b *BaseApi) Size(c *gin.Context) {
 | |
| 	var req request.DirSizeReq
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	res, err := fileService.DirSize(req)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, res)
 | |
| }
 | |
| 
 | |
| func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error {
 | |
| 	defer func() {
 | |
| 		_ = os.RemoveAll(fileDir)
 | |
| 	}()
 | |
| 
 | |
| 	op := files.NewFileOp()
 | |
| 	dstDir = strings.TrimSpace(dstDir)
 | |
| 	mode, _ := files.GetParentMode(dstDir)
 | |
| 	if mode == 0 {
 | |
| 		mode = 0755
 | |
| 	}
 | |
| 	uid, gid := -1, -1
 | |
| 	if info, err := os.Stat(dstDir); err != nil {
 | |
| 		if os.IsNotExist(err) {
 | |
| 			if err = op.CreateDir(dstDir, mode); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		stat, ok := info.Sys().(*syscall.Stat_t)
 | |
| 		if ok {
 | |
| 			uid, gid = int(stat.Uid), int(stat.Gid)
 | |
| 		}
 | |
| 	}
 | |
| 	dstFileName := filepath.Join(dstDir, fileName)
 | |
| 	dstInfo, statErr := os.Stat(dstFileName)
 | |
| 	if statErr == nil {
 | |
| 		mode = dstInfo.Mode()
 | |
| 	} else {
 | |
| 		mode = 0644
 | |
| 	}
 | |
| 
 | |
| 	if overwrite {
 | |
| 		_ = os.Remove(dstFileName)
 | |
| 	}
 | |
| 	targetFile, err := os.OpenFile(dstFileName, os.O_RDWR|os.O_CREATE, mode)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	defer targetFile.Close()
 | |
| 	for i := 0; i < chunkCount; i++ {
 | |
| 		chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", fileName, i))
 | |
| 		chunkData, err := os.ReadFile(chunkPath)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		_, err = targetFile.Write(chunkData)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		_ = os.Remove(chunkPath)
 | |
| 	}
 | |
| 	if uid != -1 && gid != -1 {
 | |
| 		_ = os.Chown(dstFileName, uid, gid)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary ChunkUpload file
 | |
| // @Description 分片上传文件
 | |
| // @Param file formData file true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/chunkupload [post]
 | |
| func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
 | |
| 	var err error
 | |
| 	fileForm, err := c.FormFile("chunk")
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
 | |
| 		return
 | |
| 	}
 | |
| 	uploadFile, err := fileForm.Open()
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
 | |
| 		return
 | |
| 	}
 | |
| 	defer uploadFile.Close()
 | |
| 	chunkIndex, err := strconv.Atoi(c.PostForm("chunkIndex"))
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
 | |
| 		return
 | |
| 	}
 | |
| 	chunkCount, err := strconv.Atoi(c.PostForm("chunkCount"))
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
 | |
| 		return
 | |
| 	}
 | |
| 	fileOp := files.NewFileOp()
 | |
| 	tmpDir := path.Join(global.CONF.System.TmpDir, "upload")
 | |
| 	if !fileOp.Stat(tmpDir) {
 | |
| 		if err := fileOp.CreateDir(tmpDir, 0755); err != nil {
 | |
| 			helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 	filename := c.PostForm("filename")
 | |
| 	fileDir := filepath.Join(tmpDir, filename)
 | |
| 	if chunkIndex == 0 {
 | |
| 		if fileOp.Stat(fileDir) {
 | |
| 			_ = fileOp.DeleteDir(fileDir)
 | |
| 		}
 | |
| 		_ = os.MkdirAll(fileDir, 0755)
 | |
| 	}
 | |
| 	filePath := filepath.Join(fileDir, filename)
 | |
| 
 | |
| 	defer func() {
 | |
| 		if err != nil {
 | |
| 			_ = os.Remove(fileDir)
 | |
| 		}
 | |
| 	}()
 | |
| 	var (
 | |
| 		emptyFile *os.File
 | |
| 		chunkData []byte
 | |
| 	)
 | |
| 
 | |
| 	emptyFile, err = os.Create(filePath)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
 | |
| 		return
 | |
| 	}
 | |
| 	defer emptyFile.Close()
 | |
| 
 | |
| 	chunkData, err = io.ReadAll(uploadFile)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
 | |
| 	err = os.WriteFile(chunkPath, chunkData, 0644)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if chunkIndex+1 == chunkCount {
 | |
| 		overwrite := true
 | |
| 		if ow := c.PostForm("overwrite"); ow != "" {
 | |
| 			overwrite, _ = strconv.ParseBool(ow)
 | |
| 		}
 | |
| 		err = mergeChunks(filename, fileDir, c.PostForm("path"), chunkCount, overwrite)
 | |
| 		if err != nil {
 | |
| 			helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, buserr.WithMap(constant.ErrFileUpload, map[string]interface{}{"name": filename, "detail": err.Error()}, err))
 | |
| 			return
 | |
| 		}
 | |
| 		helper.SuccessWithData(c, true)
 | |
| 	} else {
 | |
| 		return
 | |
| 	}
 | |
| }
 | |
| 
 | |
| var wsUpgrade = websocket.Upgrader{
 | |
| 	CheckOrigin: func(r *http.Request) bool {
 | |
| 		return true
 | |
| 	},
 | |
| }
 | |
| 
 | |
| func (b *BaseApi) Ws(c *gin.Context) {
 | |
| 	ws, err := wsUpgrade.Upgrade(c.Writer, c.Request, nil)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	wsClient := websocket2.NewWsClient("fileClient", ws)
 | |
| 	go wsClient.Read()
 | |
| 	go wsClient.Write()
 | |
| }
 | |
| 
 | |
| func (b *BaseApi) Keys(c *gin.Context) {
 | |
| 	res := &response.FileProcessKeys{}
 | |
| 	keys, err := global.CACHE.PrefixScanKey("file-wget-")
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	res.Keys = keys
 | |
| 	helper.SuccessWithData(c, res)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Read file by Line
 | |
| // @Description 按行读取日志文件
 | |
| // @Param request body request.FileReadByLineReq true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/read [post]
 | |
| func (b *BaseApi) ReadFileByLine(c *gin.Context) {
 | |
| 	var req request.FileReadByLineReq
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	res, err := fileService.ReadLogByLine(req)
 | |
| 	if err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 		return
 | |
| 	}
 | |
| 	helper.SuccessWithData(c, res)
 | |
| }
 | |
| 
 | |
| // @Tags File
 | |
| // @Summary Batch change file mode and owner
 | |
| // @Description 批量修改文件权限和用户/组
 | |
| // @Accept json
 | |
| // @Param request body request.FileRoleReq true "request"
 | |
| // @Success 200
 | |
| // @Security ApiKeyAuth
 | |
| // @Router /files/batch/role [post]
 | |
| // @x-panel-log {"bodyKeys":["paths","mode","user","group"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]","formatEN":"Batch change file mode and owner [paths] => [mode]/[user]/[group]"}
 | |
| func (b *BaseApi) BatchChangeModeAndOwner(c *gin.Context) {
 | |
| 	var req request.FileRoleReq
 | |
| 	if err := helper.CheckBindAndValidate(&req, c); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	if err := fileService.BatchChangeModeAndOwner(req); err != nil {
 | |
| 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 | |
| 	}
 | |
| 	helper.SuccessWithOutData(c)
 | |
| }
 |