feat: 文件增加回收站功能 (#2586)

Refs https://github.com/1Panel-dev/1Panel/issues/1137
Refs https://github.com/1Panel-dev/1Panel/issues/624
This commit is contained in:
zhengkunwang 2023-10-18 04:02:24 -05:00 committed by GitHub
parent eeb80749a2
commit 743e7d0b59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 933 additions and 82 deletions

View File

@ -53,4 +53,6 @@ var (
processService = service.NewIProcessService()
hostToolService = service.NewIHostToolService()
recycleBinService = service.NewIRecycleBinService()
)

View File

@ -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)
}

View File

@ -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 {

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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 {

View File

@ -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
}

View File

@ -15,4 +15,5 @@ var (
LocalAppInstallDir = path.Join(AppInstallDir, "local")
RemoteAppResourceDir = path.Join(AppResourceDir, "remote")
RuntimeDir = path.Join(DataDir, "runtime")
RecycleBinDir = "/.1panel_clash"
)

View File

@ -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"

View File

@ -63,6 +63,7 @@ ErrFileIsExit: "文件已存在!"
ErrFileUpload: "{{ .name }} 上傳文件失敗 {{ .detail}}"
ErrFileDownloadDir: "不支持下載文件夾"
ErrCmdNotFound: "{{ .name}} 命令不存在,請先在宿主機安裝此命令"
ErrSourcePathNotFound: "源目錄不存在"
#website
ErrDomainIsExist: "域名已存在"

View File

@ -63,6 +63,7 @@ ErrFileIsExit: "文件已存在!"
ErrFileUpload: "{{ .name }} 上传文件失败 {{ .detail}}"
ErrFileDownloadDir: "不支持下载文件夹"
ErrCmdNotFound: "{{ .name}} 命令不存在,请先在宿主机安装此命令"
ErrSourcePathNotFound: "源目录不存在"
#website
ErrDomainIsExist: "域名已存在"

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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": {

View File

@ -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": {

View File

@ -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:

View File

@ -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;
}
}

View File

@ -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<File.File>('files/search', params, TimeoutEnum.T_5M);
@ -87,3 +88,15 @@ export const ComputeDirSize = (params: File.DirSizeReq) => {
export const FileKeys = () => {
return http.get<File.FileKeys>('files/keys');
};
export const getRecycleList = (params: ReqPage) => {
return http.post<ResPage<File.RecycleBin>>('files/recycle/search', params);
};
export const reduceFile = (params: File.RecycleBinReduce) => {
return http.post<any>('files/recycle/reduce', params);
};
export const clearRecycle = () => {
return http.post<any>('files/recycle/clear');
};

View File

@ -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',

View File

@ -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: '開機自啟',

View File

@ -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: '开机自启',

View File

@ -1,5 +1,5 @@
<template>
<el-drawer v-model="open" :before-close="handleClose" :close-on-click-modal="false" width="50%">
<el-drawer v-model="open" :before-close="handleClose" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader :header="$t('file.setRole')" :resource="name" :back="handleClose" />
</template>
@ -32,7 +32,7 @@ import { MsgSuccess } from '@/utils/message';
const open = ref(false);
const form = ref<File.FileCreate>({ path: '', isDir: false, mode: 0o755 });
const loading = ref<Boolean>(false);
const loading = ref(false);
const mode = ref('0755');
const name = ref('');

View File

@ -18,6 +18,9 @@
</tr>
</table>
</div>
<div class="mt-5">
<el-checkbox v-model="forceDelete">{{ $t('file.forceDeleteHelper') }}</el-checkbox>
</div>
</el-col>
</el-row>
@ -45,6 +48,7 @@ const open = ref(false);
const files = ref();
const loading = ref(false);
const em = defineEmits(['close']);
const forceDelete = ref(false);
const acceptParams = (props: File.File[]) => {
files.value = props;
@ -54,7 +58,7 @@ const acceptParams = (props: File.File[]) => {
const onConfirm = () => {
const pros = [];
for (const s of files.value) {
pros.push(DeleteFile({ path: s['path'], isDir: s['isDir'] }));
pros.push(DeleteFile({ path: s['path'], isDir: s['isDir'], forceDelete: forceDelete.value }));
}
loading.value = true;
Promise.all(pros)

View File

@ -48,67 +48,78 @@
</el-alert>
</template>
<template #toolbar>
<el-dropdown @command="handleCreate">
<el-button type="primary">
{{ $t('commons.button.create') }}
<el-icon><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="dir">
<svg-icon iconName="p-file-folder"></svg-icon>
{{ $t('file.dir') }}
</el-dropdown-item>
<el-dropdown-item command="file">
<svg-icon iconName="p-file-normal"></svg-icon>
{{ $t('file.file') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button-group style="margin-left: 10px">
<el-button plain @click="openUpload">{{ $t('file.upload') }}</el-button>
<el-button plain @click="openWget">{{ $t('file.remoteFile') }}</el-button>
<el-button plain @click="openMove('copy')" :disabled="selects.length === 0">
{{ $t('file.copy') }}
</el-button>
<el-button plain @click="openMove('cut')" :disabled="selects.length === 0">
{{ $t('file.move') }}
</el-button>
<el-button plain @click="openCompress(selects)" :disabled="selects.length === 0">
{{ $t('file.compress') }}
</el-button>
<el-button plain @click="batchDelFiles" :disabled="selects.length === 0">
{{ $t('commons.button.delete') }}
</el-button>
</el-button-group>
<el-button-group class="copy-button" v-if="moveOpen">
<el-tooltip class="box-item" effect="dark" :content="$t('file.paste')" placement="bottom">
<el-button plain @click="openPaste">{{ $t('file.paste') }}</el-button>
</el-tooltip>
<el-tooltip class="box-item" effect="dark" :content="$t('file.cancel')" placement="bottom">
<el-button plain class="close" @click="closeMove">
<el-icon class="close-icon"><Close /></el-icon>
<div class="btn-container">
<div class="left-section">
<el-dropdown @command="handleCreate">
<el-button type="primary">
{{ $t('commons.button.create') }}
<el-icon><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="dir">
<svg-icon iconName="p-file-folder"></svg-icon>
{{ $t('file.dir') }}
</el-dropdown-item>
<el-dropdown-item command="file">
<svg-icon iconName="p-file-normal"></svg-icon>
{{ $t('file.file') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button-group>
<el-button plain @click="openUpload">{{ $t('file.upload') }}</el-button>
<el-button plain @click="openWget">{{ $t('file.remoteFile') }}</el-button>
<el-button plain @click="openMove('copy')" :disabled="selects.length === 0">
{{ $t('file.copy') }}
</el-button>
<el-button plain @click="openMove('cut')" :disabled="selects.length === 0">
{{ $t('file.move') }}
</el-button>
<el-button plain @click="openCompress(selects)" :disabled="selects.length === 0">
{{ $t('file.compress') }}
</el-button>
<el-button plain @click="batchDelFiles" :disabled="selects.length === 0">
{{ $t('commons.button.delete') }}
</el-button>
</el-button-group>
<el-button-group class="copy-button" v-if="moveOpen">
<el-tooltip class="box-item" effect="dark" :content="$t('file.paste')" placement="bottom">
<el-button plain @click="openPaste">{{ $t('file.paste') }}</el-button>
</el-tooltip>
<el-tooltip class="box-item" effect="dark" :content="$t('file.cancel')" placement="bottom">
<el-button plain class="close" @click="closeMove">
<el-icon class="close-icon"><Close /></el-icon>
</el-button>
</el-tooltip>
</el-button-group>
</div>
<div class="right-section">
<el-button class="btn" @click="openRecycleBin" :icon="Delete">
{{ $t('file.recycleBin') }}
</el-button>
</el-tooltip>
</el-button-group>
<div class="search search-button">
<el-input
v-model="req.search"
clearable
@clear="search()"
@keydown.enter="search()"
:placeholder="$t('file.search')"
>
<template #prepend>
<el-checkbox v-model="req.containSub">
{{ $t('file.sub') }}
</el-checkbox>
</template>
<template #append>
<el-button icon="Search" @click="search" round />
</template>
</el-input>
<div class="search-button">
<el-input
v-model="req.search"
clearable
@clear="search()"
@keydown.enter="search()"
:placeholder="$t('file.search')"
>
<template #prepend>
<el-checkbox v-model="req.containSub">
{{ $t('file.sub') }}
</el-checkbox>
</template>
<template #append>
<el-button icon="Search" @click="search" round />
</template>
</el-input>
</div>
</div>
</div>
</template>
<template #main>
@ -200,7 +211,8 @@
<Process :open="processPage.open" @close="closeProcess" />
<Owner ref="chownRef" @close="search"></Owner>
<Detail ref="detailRef" />
<Delete ref="deleteRef" @close="search" />
<DeleteFile ref="deleteRef" @close="search" />
<RecycleBin ref="recycleBinRef" @close="search" />
</LayoutContent>
</div>
</template>
@ -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;
}
</style>

View File

@ -0,0 +1,155 @@
<template>
<el-drawer v-model="open" :before-close="handleClose" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader :header="$t('file.recycleBin')" :back="handleClose" />
</template>
<el-button @click="clear" type="primary" :disabled="data == null || data.length == 0">
{{ $t('file.clearRecycleBin') }}
</el-button>
<ComplexTable
:pagination-config="paginationConfig"
v-model:selects="selects"
:data="data"
@search="search"
class="mt-5"
>
<el-table-column type="selection" fix />
<el-table-column
:label="$t('commons.table.name')"
min-width="100"
fix
show-overflow-tooltip
prop="name"
></el-table-column>
<el-table-column :label="$t('file.sourcePath')" show-overflow-tooltip prop="sourcePath"></el-table-column>
<el-table-column :label="$t('file.size')" prop="size" max-width="50">
<template #default="{ row }">
{{ getFileSize(row.size) }}
</template>
</el-table-column>
<el-table-column
:label="$t('file.deleteTime')"
prop="deleteTime"
:formatter="dateFormat"
show-overflow-tooltip
></el-table-column>
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fix />
</ComplexTable>
</el-drawer>
</template>
<script lang="ts" setup>
import { DeleteFile, clearRecycle, getRecycleList, reduceFile } from '@/api/modules/files';
import { reactive, ref } from 'vue';
import { dateFormat, computeSize } from '@/utils/util';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
const open = ref(false);
const req = reactive({
page: 1,
pageSize: 100,
});
const data = ref([]);
const em = defineEmits(['close']);
const selects = ref([]);
const loading = ref(false);
const files = ref([]);
const paginationConfig = reactive({
cacheSizeKey: 'recycle-page-size',
currentPage: 1,
pageSize: 100,
total: 0,
});
const handleClose = () => {
open.value = false;
em('close', false);
};
const getFileSize = (size: number) => {
return computeSize(size);
};
const acceptParams = () => {
search();
};
const search = async () => {
try {
const res = await getRecycleList(req);
data.value = res.data.items;
paginationConfig.total = res.data.total;
open.value = true;
} catch (error) {}
};
const singleDel = (row: any) => {
ElMessageBox.confirm(i18n.global.t('commons.msg.delete'), i18n.global.t('commons.button.delete'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
files.value = [];
files.value.push(row);
deleteFile();
});
};
const deleteFile = async () => {
const pros = [];
for (const s of files.value) {
pros.push(DeleteFile({ path: s.from + '/' + s.rName, isDir: s.isDir, forceDelete: true }));
}
loading.value = true;
Promise.all(pros)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
search();
})
.finally(() => {
loading.value = false;
});
};
const rdFile = async (row: any) => {
ElMessageBox.confirm(i18n.global.t('file.reduceHelper'), i18n.global.t('file.reduce'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
try {
loading.value = true;
await reduceFile({ from: row.from, rName: row.rName, name: row.name });
loading.value = false;
search();
} catch (error) {}
});
};
const clear = async () => {
ElMessageBox.confirm(i18n.global.t('commons.msg.delete'), i18n.global.t('file.clearRecycleBinHelper'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
try {
loading.value = true;
await clearRecycle();
loading.value = false;
search();
} catch (error) {}
});
};
const buttons = [
{
label: i18n.global.t('file.reduce'),
click: rdFile,
},
{
label: i18n.global.t('commons.button.delete'),
click: singleDel,
},
];
defineExpose({ acceptParams });
</script>