diff --git a/backend/app/api/v1/entry.go b/backend/app/api/v1/entry.go index 0bac385fc..4060d6645 100644 --- a/backend/app/api/v1/entry.go +++ b/backend/app/api/v1/entry.go @@ -53,4 +53,6 @@ var ( processService = service.NewIProcessService() hostToolService = service.NewIHostToolService() + + recycleBinService = service.NewIRecycleBinService() ) diff --git a/backend/app/api/v1/recycle_bin.go b/backend/app/api/v1/recycle_bin.go new file mode 100644 index 000000000..f41eaeac2 --- /dev/null +++ b/backend/app/api/v1/recycle_bin.go @@ -0,0 +1,70 @@ +package v1 + +import ( + "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/constant" + "github.com/gin-gonic/gin" +) + +// @Tags RecycleBin +// @Summary List RecycleBin files +// @Description 获取回收站文件列表 +// @Accept json +// @Param request body dto.PageInfo true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /recycle/search [post] +func (b *BaseApi) SearchRecycleBinFile(c *gin.Context) { + var req dto.PageInfo + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + total, list, err := recycleBinService.Page(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +// @Tags RecycleBin +// @Summary Reduce RecycleBin files +// @Description 还原回收站文件 +// @Accept json +// @Param request body request.RecycleBinReduce true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /recycle/reduce [post] +// @x-panel-log {"bodyKeys":["name],"paramKeys":[],"BeforeFunctions":[],"formatZH":"还原回收站文件 [name]","formatEN":"Reduce RecycleBin file [name]"} +func (b *BaseApi) ReduceRecycleBinFile(c *gin.Context) { + var req request.RecycleBinReduce + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := recycleBinService.Reduce(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags RecycleBin +// @Summary Clear RecycleBin files +// @Description 清空回收站文件 +// @Accept json +// @Success 200 +// @Security ApiKeyAuth +// @Router /recycle/clear [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空回收站","formatEN":"清空回收站"} +func (b *BaseApi) ClearRecycleBinFile(c *gin.Context) { + if err := recycleBinService.Clear(); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} diff --git a/backend/app/dto/request/file.go b/backend/app/dto/request/file.go index ba744e624..f21f0f415 100644 --- a/backend/app/dto/request/file.go +++ b/backend/app/dto/request/file.go @@ -26,8 +26,9 @@ type FileCreate struct { } type FileDelete struct { - Path string `json:"path" validate:"required"` - IsDir bool `json:"isDir"` + Path string `json:"path" validate:"required"` + IsDir bool `json:"isDir"` + ForceDelete bool `json:"forceDelete"` } type FileBatchDelete struct { diff --git a/backend/app/dto/request/recycle_bin.go b/backend/app/dto/request/recycle_bin.go new file mode 100644 index 000000000..4870a735e --- /dev/null +++ b/backend/app/dto/request/recycle_bin.go @@ -0,0 +1,11 @@ +package request + +type RecycleBinCreate struct { + SourcePath string `json:"sourcePath" validate:"required"` +} + +type RecycleBinReduce struct { + From string `json:"from" validate:"required"` + RName string `json:"rName" validate:"required"` + Name string `json:"name"` +} diff --git a/backend/app/dto/response/recycle_bin.go b/backend/app/dto/response/recycle_bin.go new file mode 100644 index 000000000..b5a040358 --- /dev/null +++ b/backend/app/dto/response/recycle_bin.go @@ -0,0 +1,14 @@ +package response + +import "time" + +type RecycleBinDTO struct { + Name string `json:"name"` + Size int `json:"size"` + Type string `json:"type"` + DeleteTime time.Time `json:"deleteTime"` + RName string `json:"rName"` + SourcePath string `json:"sourcePath"` + IsDir bool `json:"isDir"` + From string `json:"from"` +} diff --git a/backend/app/service/file.go b/backend/app/service/file.go index 4214364a4..2d7b20264 100644 --- a/backend/app/service/file.go +++ b/backend/app/service/file.go @@ -133,11 +133,14 @@ func (f *FileService) Create(op request.FileCreate) error { func (f *FileService) Delete(op request.FileDelete) error { fo := files.NewFileOp() - if op.IsDir { - return fo.DeleteDir(op.Path) - } else { - return fo.DeleteFile(op.Path) + if op.ForceDelete { + if op.IsDir { + return fo.DeleteDir(op.Path) + } else { + return fo.DeleteFile(op.Path) + } } + return NewIRecycleBinService().Create(request.RecycleBinCreate{SourcePath: op.Path}) } func (f *FileService) BatchDelete(op request.FileBatchDelete) error { diff --git a/backend/app/service/recycle_bin.go b/backend/app/service/recycle_bin.go new file mode 100644 index 000000000..0c58807d0 --- /dev/null +++ b/backend/app/service/recycle_bin.go @@ -0,0 +1,208 @@ +package service + +import ( + "fmt" + "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/utils/files" + "github.com/shirou/gopsutil/v3/disk" + "os" + "path" + "regexp" + "strconv" + "strings" + "time" +) + +type RecycleBinService struct { +} + +type IRecycleBinService interface { + Page(search dto.PageInfo) (int64, []response.RecycleBinDTO, error) + Create(create request.RecycleBinCreate) error + Reduce(reduce request.RecycleBinReduce) error + Clear() error +} + +func NewIRecycleBinService() IRecycleBinService { + return &RecycleBinService{} +} + +func (r RecycleBinService) Page(search dto.PageInfo) (int64, []response.RecycleBinDTO, error) { + var ( + result []response.RecycleBinDTO + ) + partitions, err := disk.Partitions(false) + if err != nil { + return 0, nil, err + } + op := files.NewFileOp() + for _, p := range partitions { + dir := path.Join(p.Mountpoint, ".1panel_clash") + if !op.Stat(dir) { + continue + } + clashFiles, err := os.ReadDir(dir) + if err != nil { + return 0, nil, err + } + for _, file := range clashFiles { + if strings.HasPrefix(file.Name(), "_1p_") { + recycleDTO, err := getRecycleBinDTOFromName(file.Name()) + recycleDTO.IsDir = file.IsDir() + recycleDTO.From = dir + if err == nil { + result = append(result, *recycleDTO) + } + } + } + } + startIndex := (search.Page - 1) * search.PageSize + endIndex := startIndex + search.PageSize + + if startIndex > len(result) { + return int64(len(result)), result, nil + } + if endIndex > len(result) { + endIndex = len(result) + } + return int64(len(result)), result[startIndex:endIndex], nil +} + +func (r RecycleBinService) Create(create request.RecycleBinCreate) error { + op := files.NewFileOp() + if !op.Stat(create.SourcePath) { + return buserr.New(constant.ErrLinkPathNotFound) + } + clashDir, err := getClashDir(create.SourcePath) + if err != nil { + return err + } + paths := strings.Split(create.SourcePath, "/") + rNamePre := strings.Join(paths, "_1p_") + deleteTime := time.Now() + openFile, err := op.OpenFile(create.SourcePath) + if err != nil { + return err + } + fileInfo, err := openFile.Stat() + if err != nil { + return err + } + size := 0 + if fileInfo.IsDir() { + sizeF, err := op.GetDirSize(create.SourcePath) + if err != nil { + return err + } + size = int(sizeF) + } else { + size = int(fileInfo.Size()) + } + + rName := fmt.Sprintf("_1p_%s%s_p_%d_%d", "file", rNamePre, size, deleteTime.Unix()) + return op.Rename(create.SourcePath, path.Join(clashDir, rName)) +} + +func (r RecycleBinService) Reduce(reduce request.RecycleBinReduce) error { + filePath := path.Join(reduce.From, reduce.RName) + op := files.NewFileOp() + if !op.Stat(filePath) { + return buserr.New(constant.ErrLinkPathNotFound) + } + recycleBinDTO, err := getRecycleBinDTOFromName(reduce.RName) + if err != nil { + return err + } + if !op.Stat(path.Dir(recycleBinDTO.SourcePath)) { + return buserr.New("ErrSourcePathNotFound") + } + if op.Stat(recycleBinDTO.SourcePath) { + if err = op.RmRf(recycleBinDTO.SourcePath); err != nil { + return err + } + } + return op.Rename(filePath, recycleBinDTO.SourcePath) +} + +func (r RecycleBinService) Clear() error { + partitions, err := disk.Partitions(false) + if err != nil { + return err + } + op := files.NewFileOp() + for _, p := range partitions { + dir := path.Join(p.Mountpoint, ".1panel_clash") + if !op.Stat(dir) { + continue + } + newDir := path.Join(p.Mountpoint, "1panel_clash") + if err := op.Rename(dir, newDir); err != nil { + return err + } + go func() { + _ = op.DeleteDir(newDir) + }() + } + return nil +} + +func getClashDir(realPath string) (string, error) { + trimmedPath := strings.Trim(realPath, "/") + parts := strings.Split(trimmedPath, "/") + dir := "" + if len(parts) > 0 { + dir = parts[0] + partitions, err := disk.Partitions(false) + if err != nil { + return "", err + } + for _, p := range partitions { + if p.Mountpoint == dir { + if err = createClashDir(path.Join(p.Mountpoint, ".1panel_clash")); err != nil { + return "", err + } + return dir, nil + } + } + } + return constant.RecycleBinDir, createClashDir(constant.RecycleBinDir) +} + +func createClashDir(clashDir string) error { + op := files.NewFileOp() + if !op.Stat(clashDir) { + if err := op.CreateDir(clashDir, 0755); err != nil { + return err + } + } + return nil +} + +func getRecycleBinDTOFromName(filename string) (*response.RecycleBinDTO, error) { + r := regexp.MustCompile(`_1p_file_1p_(.+)_p_(\d+)_(\d+)`) + matches := r.FindStringSubmatch(filename) + if len(matches) != 4 { + return nil, fmt.Errorf("invalid filename format") + } + sourcePath := "/" + strings.ReplaceAll(matches[1], "_1p_", "/") + size, err := strconv.ParseInt(matches[2], 10, 64) + if err != nil { + return nil, err + } + deleteTime, err := strconv.ParseInt(matches[3], 10, 64) + if err != nil { + return nil, err + } + return &response.RecycleBinDTO{ + Name: path.Base(sourcePath), + Size: int(size), + Type: "file", + DeleteTime: time.Unix(deleteTime, 0), + SourcePath: sourcePath, + RName: filename, + }, nil +} diff --git a/backend/constant/dir.go b/backend/constant/dir.go index d828ea948..87c348913 100644 --- a/backend/constant/dir.go +++ b/backend/constant/dir.go @@ -15,4 +15,5 @@ var ( LocalAppInstallDir = path.Join(AppInstallDir, "local") RemoteAppResourceDir = path.Join(AppResourceDir, "remote") RuntimeDir = path.Join(DataDir, "runtime") + RecycleBinDir = "/.1panel_clash" ) diff --git a/backend/i18n/lang/en.yaml b/backend/i18n/lang/en.yaml index 6ce197a34..a943e2764 100644 --- a/backend/i18n/lang/en.yaml +++ b/backend/i18n/lang/en.yaml @@ -63,6 +63,7 @@ ErrFileIsExit: "File already exists!" ErrFileUpload: "Failed to upload file {{.name}} {{.detail}}" ErrFileDownloadDir: "Download folder not supported" ErrCmdNotFound: "{{ .name}} command does not exist, please install this command on the host first" +ErrSourcePathNotFound: "Source directory does not exist" #website ErrDomainIsExist: "Domain is already exist" diff --git a/backend/i18n/lang/zh-Hant.yaml b/backend/i18n/lang/zh-Hant.yaml index 0fdc0d6a0..0b3850b53 100644 --- a/backend/i18n/lang/zh-Hant.yaml +++ b/backend/i18n/lang/zh-Hant.yaml @@ -63,6 +63,7 @@ ErrFileIsExit: "文件已存在!" ErrFileUpload: "{{ .name }} 上傳文件失敗 {{ .detail}}" ErrFileDownloadDir: "不支持下載文件夾" ErrCmdNotFound: "{{ .name}} 命令不存在,請先在宿主機安裝此命令" +ErrSourcePathNotFound: "源目錄不存在" #website ErrDomainIsExist: "域名已存在" diff --git a/backend/i18n/lang/zh.yaml b/backend/i18n/lang/zh.yaml index 9a9cdc218..c49d72be4 100644 --- a/backend/i18n/lang/zh.yaml +++ b/backend/i18n/lang/zh.yaml @@ -63,6 +63,7 @@ ErrFileIsExit: "文件已存在!" ErrFileUpload: "{{ .name }} 上传文件失败 {{ .detail}}" ErrFileDownloadDir: "不支持下载文件夹" ErrCmdNotFound: "{{ .name}} 命令不存在,请先在宿主机安装此命令" +ErrSourcePathNotFound: "源目录不存在" #website ErrDomainIsExist: "域名已存在" diff --git a/backend/router/ro_file.go b/backend/router/ro_file.go index a57509851..64bf409b3 100644 --- a/backend/router/ro_file.go +++ b/backend/router/ro_file.go @@ -37,5 +37,9 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) { fileRouter.POST("/size", baseApi.Size) fileRouter.GET("/ws", baseApi.Ws) fileRouter.GET("/keys", baseApi.Keys) + + fileRouter.POST("/recycle/search", baseApi.SearchRecycleBinFile) + fileRouter.POST("/recycle/reduce", baseApi.ReduceRecycleBinFile) + fileRouter.POST("/recycle/clear", baseApi.ClearRecycleBinFile) } } diff --git a/backend/utils/files/file_op.go b/backend/utils/files/file_op.go index 1d78157e3..bbca7b804 100644 --- a/backend/utils/files/file_op.go +++ b/backend/utils/files/file_op.go @@ -84,6 +84,14 @@ func (f FileOp) DeleteFile(dst string) error { return f.Fs.Remove(dst) } +func (f FileOp) Delete(dst string) error { + return os.RemoveAll(dst) +} + +func (f FileOp) RmRf(dst string) error { + return cmd.ExecCmd(fmt.Sprintf("rm -rf %s", dst)) +} + func (f FileOp) WriteFile(dst string, in io.Reader, mode fs.FileMode) error { file, err := f.Fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) if err != nil { diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 87275609c..484b06a01 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -1,5 +1,5 @@ -// Code generated by swaggo/swag. DO NOT EDIT. - +// Package docs GENERATED BY SWAG; DO NOT EDIT +// This file was generated by swaggo/swag package docs import "github.com/swaggo/swag" @@ -7826,6 +7826,94 @@ const docTemplate = `{ } } }, + "/recycle/clear": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清空回收站文件", + "consumes": [ + "application/json" + ], + "tags": [ + "RecycleBin" + ], + "summary": "Clear RecycleBin files", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/recycle/reduce": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "还原回收站文件", + "consumes": [ + "application/json" + ], + "tags": [ + "RecycleBin" + ], + "summary": "Reduce RecycleBin files", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RecycleBinReduce" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/recycle/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取回收站文件列表", + "consumes": [ + "application/json" + ], + "tags": [ + "RecycleBin" + ], + "summary": "List RecycleBin files", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/runtimes": { "post": { "security": [ @@ -16228,6 +16316,9 @@ const docTemplate = `{ "path" ], "properties": { + "forceDelete": { + "type": "boolean" + }, "isDir": { "type": "boolean" }, @@ -16837,6 +16928,21 @@ const docTemplate = `{ } } }, + "request.RecycleBinReduce": { + "type": "object", + "required": [ + "from", + "rName" + ], + "properties": { + "from": { + "type": "string" + }, + "rName": { + "type": "string" + } + } + }, "request.RuntimeCreate": { "type": "object", "properties": { diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index 3e9a4b0ca..4ad1ac140 100644 --- a/cmd/server/docs/swagger.json +++ b/cmd/server/docs/swagger.json @@ -7819,6 +7819,94 @@ } } }, + "/recycle/clear": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "清空回收站文件", + "consumes": [ + "application/json" + ], + "tags": [ + "RecycleBin" + ], + "summary": "Clear RecycleBin files", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/recycle/reduce": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "还原回收站文件", + "consumes": [ + "application/json" + ], + "tags": [ + "RecycleBin" + ], + "summary": "Reduce RecycleBin files", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.RecycleBinReduce" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/recycle/search": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取回收站文件列表", + "consumes": [ + "application/json" + ], + "tags": [ + "RecycleBin" + ], + "summary": "List RecycleBin files", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PageInfo" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/runtimes": { "post": { "security": [ @@ -16221,6 +16309,9 @@ "path" ], "properties": { + "forceDelete": { + "type": "boolean" + }, "isDir": { "type": "boolean" }, @@ -16830,6 +16921,21 @@ } } }, + "request.RecycleBinReduce": { + "type": "object", + "required": [ + "from", + "rName" + ], + "properties": { + "from": { + "type": "string" + }, + "rName": { + "type": "string" + } + } + }, "request.RuntimeCreate": { "type": "object", "properties": { diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index b5cc4e7fc..a1c33dcc1 100644 --- a/cmd/server/docs/swagger.yaml +++ b/cmd/server/docs/swagger.yaml @@ -2778,6 +2778,8 @@ definitions: type: object request.FileDelete: properties: + forceDelete: + type: boolean isDir: type: boolean path: @@ -3190,6 +3192,16 @@ definitions: required: - PID type: object + request.RecycleBinReduce: + properties: + from: + type: string + rName: + type: string + required: + - from + - rName + type: object request.RuntimeCreate: properties: appDetailId: @@ -9147,6 +9159,59 @@ paths: formatEN: 结束进程 [PID] formatZH: 结束进程 [PID] paramKeys: [] + /recycle/clear: + post: + consumes: + - application/json + description: 清空回收站文件 + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Clear RecycleBin files + tags: + - RecycleBin + /recycle/reduce: + post: + consumes: + - application/json + description: 还原回收站文件 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.RecycleBinReduce' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Reduce RecycleBin files + tags: + - RecycleBin + /recycle/search: + post: + consumes: + - application/json + description: 获取回收站文件列表 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.PageInfo' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: List RecycleBin files + tags: + - RecycleBin /runtimes: post: consumes: diff --git a/frontend/src/api/interface/file.ts b/frontend/src/api/interface/file.ts index b26d120f5..701e74e0e 100644 --- a/frontend/src/api/interface/file.ts +++ b/frontend/src/api/interface/file.ts @@ -65,6 +65,7 @@ export namespace File { export interface FileDelete { path: string; isDir: boolean; + forceDelete: boolean; } export interface FileBatchDelete { @@ -145,4 +146,20 @@ export namespace File { export interface FilePath { path: string; } + + export interface RecycleBin { + sourcePath: string; + name: string; + isDir: boolean; + size: number; + deleteTime: string; + rName: string; + from: string; + } + + export interface RecycleBinReduce { + rName: string; + from: string; + name: string; + } } diff --git a/frontend/src/api/modules/files.ts b/frontend/src/api/modules/files.ts index 3f0ea9fdd..1261f087e 100644 --- a/frontend/src/api/modules/files.ts +++ b/frontend/src/api/modules/files.ts @@ -3,6 +3,7 @@ import http from '@/api'; import { AxiosRequestConfig } from 'axios'; import { ResPage } from '../interface'; import { TimeoutEnum } from '@/enums/http-enum'; +import { ReqPage } from '@/api/interface'; export const GetFilesList = (params: File.ReqFile) => { return http.post('files/search', params, TimeoutEnum.T_5M); @@ -87,3 +88,15 @@ export const ComputeDirSize = (params: File.DirSizeReq) => { export const FileKeys = () => { return http.get('files/keys'); }; + +export const getRecycleList = (params: ReqPage) => { + return http.post>('files/recycle/search', params); +}; + +export const reduceFile = (params: File.RecycleBinReduce) => { + return http.post('files/recycle/reduce', params); +}; + +export const clearRecycle = () => { + return http.post('files/recycle/clear'); +}; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 4641f8581..b75ca74ba 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -957,8 +957,18 @@ const message = { fileUploadStart: 'Uploading [{0}]....', currentSelect: 'Current Select: ', unsupportType: 'Unsupported file type', - deleteHelper: 'The following resources will be deleted, this operation cannot be rolled back, continue? ', + deleteHelper: + 'Are you sure you want to delete the following files? By default, it will enter the recycle bin after deletion', fileHeper: 'Note: 1. Sorting is not supported after searching 2. Folders are not supported by size sorting', + forceDeleteHelper: 'Permanently delete the file (without entering the recycle bin, delete it directly)', + recycleBin: 'Recycle bin', + sourcePath: 'Original path', + deleteTime: 'Delete time', + reduce: 'Reduction', + reduceHelper: + 'Restore the file to its original path. If a file or directory with the same name exists at the original address of the file, it will be overwritten. Do you want to continue?', + clearRecycleBin: 'Clear the recycle bin', + clearRecycleBinHelper: 'Do you want to clear the recycle bin? ', }, ssh: { autoStart: 'Auto Start', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index aeda637f1..532aa6d1e 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -920,8 +920,16 @@ const message = { fileUploadStart: '正在上傳【{0}】....', currentSelect: '當前選中: ', unsupportType: '不支持的文件類型', - deleteHelper: '以下資源將被刪除,此操作不可回滾,是否繼續?', + deleteHelper: '確定刪除所選檔案? 預設刪除之後將進入回收站?', fileHeper: '注意:1.搜尋之後不支援排序 2.依大小排序不支援資料夾', + forceDeleteHelper: '永久刪除檔案(不進入回收站,直接刪除)', + recycleBin: '回收站', + sourcePath: '原路徑', + deleteTime: '刪除時間', + reduce: '還原', + reduceHelper: '恢復檔案到原路徑,如果檔案原始位址,存在同名檔案或目錄,將會覆蓋,是否繼續? ', + clearRecycleBin: '清空回收站', + clearRecycleBinHelper: '是否清空回收站? ', }, ssh: { autoStart: '開機自啟', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 9f9e1e6d8..a94ad9d94 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -921,8 +921,16 @@ const message = { fileUploadStart: '正在上传【{0}】....', currentSelect: '当前选中: ', unsupportType: '不支持的文件类型', - deleteHelper: '以下资源将被删除,此操作不可回滚,是否继续?', + deleteHelper: '确定删除所选文件? 默认删除之后将进入回收站', fileHeper: '注意:1.搜索之后不支持排序 2.按大小排序不支持文件夹', + forceDeleteHelper: '永久删除文件(不进入回收站,直接删除)', + recycleBin: '回收站', + sourcePath: '原路径', + deleteTime: '删除时间', + reduce: '还原', + reduceHelper: '恢复文件到原路径,如果文件原地址,存在同名文件或目录,将会覆盖,是否继续?', + clearRecycleBin: '清空回收站', + clearRecycleBinHelper: '是否清空回收站?', }, ssh: { autoStart: '开机自启', diff --git a/frontend/src/views/host/file-management/change-role/index.vue b/frontend/src/views/host/file-management/change-role/index.vue index 46e6c8e38..e8a766403 100644 --- a/frontend/src/views/host/file-management/change-role/index.vue +++ b/frontend/src/views/host/file-management/change-role/index.vue @@ -1,5 +1,5 @@ @@ -209,6 +221,7 @@ import { nextTick, onMounted, reactive, ref, computed } from '@vue/runtime-core'; import { GetFilesList, GetFileContent, ComputeDirSize } from '@/api/modules/files'; import { computeSize, dateFormat, downloadFile, getIcon, getRandomStr } from '@/utils/util'; +import { Delete } from '@element-plus/icons-vue'; import { File } from '@/api/interface/file'; import i18n from '@/lang'; import CreateFile from './create/index.vue'; @@ -222,10 +235,11 @@ import Wget from './wget/index.vue'; import Move from './move/index.vue'; import Download from './download/index.vue'; import Owner from './chown/index.vue'; -import Delete from './delete/index.vue'; +import DeleteFile from './delete/index.vue'; import { Mimetypes, Languages } from '@/global/mimetype'; import Process from './process/index.vue'; import Detail from './detail/index.vue'; +import RecycleBin from './recycle-bin/index.vue'; import { useRouter } from 'vue-router'; import { Back, Refresh } from '@element-plus/icons-vue'; import { MsgSuccess, MsgWarning } from '@/utils/message'; @@ -288,6 +302,7 @@ const breadCrumbRef = ref(); const chownRef = ref(); const moveOpen = ref(false); const deleteRef = ref(); +const recycleBinRef = ref(); // editablePath const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchable(paths); @@ -608,6 +623,10 @@ const openDetail = (row: File.File) => { detailRef.value.acceptParams({ path: row.path }); }; +const openRecycleBin = () => { + recycleBinRef.value.acceptParams(); +}; + const changeSort = ({ prop, order }) => { req.sortBy = prop; req.sortOrder = order; @@ -713,12 +732,6 @@ onMounted(() => { } } -.search { - display: inline; - width: 400px; - float: right; -} - .copy-button { margin-left: 10px; .close { @@ -728,4 +741,25 @@ onMounted(() => { } } } + +.btn-container { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.left-section, +.right-section { + display: flex; + align-items: center; +} + +.left-section > *:not(:first-child) { + margin-left: 10px; +} + +.right-section > *:not(:last-child) { + margin-right: 10px; +} diff --git a/frontend/src/views/host/file-management/recycle-bin/index.vue b/frontend/src/views/host/file-management/recycle-bin/index.vue new file mode 100644 index 000000000..24453e395 --- /dev/null +++ b/frontend/src/views/host/file-management/recycle-bin/index.vue @@ -0,0 +1,155 @@ + + +