feat: 文件增加收藏夹功能 (#2612)

Refs https://github.com/1Panel-dev/1Panel/issues/492
This commit is contained in:
zhengkunwang 2023-10-20 04:56:46 -05:00 committed by GitHub
parent 8715cfc343
commit b38abf852d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1194 additions and 258 deletions

View File

@ -55,4 +55,5 @@ var (
hostToolService = service.NewIHostToolService()
recycleBinService = service.NewIRecycleBinService()
favoriteService = service.NewIFavoriteService()
)

View File

@ -0,0 +1,76 @@
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 File
// @Summary List favorites
// @Description 获取收藏列表
// @Accept json
// @Param request body dto.PageInfo true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /files/favorite/search [post]
func (b *BaseApi) SearchFavorite(c *gin.Context) {
var req dto.PageInfo
if err := helper.CheckBind(req, c); err != nil {
return
}
total, list, err := favoriteService.Page(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Total: total,
Items: list,
})
}
// @Tags File
// @Summary Create favorite
// @Description 创建收藏
// @Accept json
// @Param request body request.FavoriteCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /files/favorite [post]
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"收藏文件/文件夹 [path]","formatEN":"收藏文件/文件夹 [path]"}
func (b *BaseApi) CreateFavorite(c *gin.Context) {
var req request.FavoriteCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
favorite, err := favoriteService.Create(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, favorite)
}
// @Tags File
// @Summary Delete favorite
// @Description 删除收藏
// @Accept json
// @Param request body request.FavoriteDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /files/favorite/del [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"favorites","output_column":"path","output_value":"path"}],"formatZH":"删除收藏 [path]","formatEN":"delete avorite [path]"}
func (b *BaseApi) DeleteFavorite(c *gin.Context) {
var req request.FavoriteDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := favoriteService.Delete(req.ID); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}

View File

@ -115,7 +115,7 @@ func GetTxAndContext() (tx *gorm.DB, ctx context.Context) {
}
func CheckBindAndValidate(req interface{}, c *gin.Context) error {
if err := c.ShouldBindJSON(&req); err != nil {
if err := c.ShouldBindJSON(req); err != nil {
ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return err
}
@ -125,3 +125,11 @@ func CheckBindAndValidate(req interface{}, c *gin.Context) error {
}
return nil
}
func CheckBind(req interface{}, c *gin.Context) error {
if err := c.ShouldBindJSON(&req); err != nil {
ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return err
}
return nil
}

View File

@ -8,14 +8,14 @@ import (
"github.com/gin-gonic/gin"
)
// @Tags RecycleBin
// @Tags File
// @Summary List RecycleBin files
// @Description 获取回收站文件列表
// @Accept json
// @Param request body dto.PageInfo true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /recycle/search [post]
// @Router /files/recycle/search [post]
func (b *BaseApi) SearchRecycleBinFile(c *gin.Context) {
var req dto.PageInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@ -32,15 +32,15 @@ func (b *BaseApi) SearchRecycleBinFile(c *gin.Context) {
})
}
// @Tags RecycleBin
// @Tags File
// @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]"}
// @Router /files/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 {
@ -53,13 +53,13 @@ func (b *BaseApi) ReduceRecycleBinFile(c *gin.Context) {
helper.SuccessWithOutData(c)
}
// @Tags RecycleBin
// @Tags File
// @Summary Clear RecycleBin files
// @Description 清空回收站文件
// @Accept json
// @Success 200
// @Security ApiKeyAuth
// @Router /recycle/clear [post]
// @Router /files/recycle/clear [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空回收站","formatEN":"清空回收站"}
func (b *BaseApi) ClearRecycleBinFile(c *gin.Context) {
if err := recycleBinService.Clear(); err != nil {

View File

@ -0,0 +1,9 @@
package request
type FavoriteCreate struct {
Path string `json:"path" validate:"required"`
}
type FavoriteDelete struct {
ID uint `json:"id" validate:"required"`
}

View File

@ -0,0 +1,7 @@
package response
import "github.com/1Panel-dev/1Panel/backend/app/model"
type FavoriteDTO struct {
model.Favorite
}

View File

@ -0,0 +1,10 @@
package model
type Favorite struct {
BaseModel
Name string `gorm:"type:varchar(256);not null;" json:"name" `
Path string `gorm:"type:varchar(256);not null;unique" json:"path"`
Type string `gorm:"type:varchar(64);" json:"type"`
IsDir bool `json:"isDir"`
IsTxt bool `json:"isTxt"`
}

View File

@ -0,0 +1,66 @@
package repo
import (
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/global"
"gorm.io/gorm"
)
type FavoriteRepo struct{}
type IFavoriteRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.Favorite, error)
Create(group *model.Favorite) error
Delete(opts ...DBOption) error
GetFirst(opts ...DBOption) (model.Favorite, error)
All() ([]model.Favorite, error)
WithByPath(path string) DBOption
}
func NewIFavoriteRepo() IFavoriteRepo {
return &FavoriteRepo{}
}
func (f *FavoriteRepo) WithByPath(path string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("path = ?", path)
}
}
func (f *FavoriteRepo) Page(page, size int, opts ...DBOption) (int64, []model.Favorite, error) {
var (
favorites []model.Favorite
count int64
)
count = int64(0)
db := getDb(opts...).Model(&model.Favorite{})
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&favorites).Error
return count, favorites, err
}
func (f *FavoriteRepo) Create(favorite *model.Favorite) error {
return global.DB.Create(favorite).Error
}
func (f *FavoriteRepo) GetFirst(opts ...DBOption) (model.Favorite, error) {
var favorite model.Favorite
db := getDb(opts...).Model(&model.Favorite{})
if err := db.First(&favorite).Error; err != nil {
return favorite, err
}
return favorite, nil
}
func (f *FavoriteRepo) Delete(opts ...DBOption) error {
db := getDb(opts...).Model(&model.Favorite{})
return db.Delete(&model.Favorite{}).Error
}
func (f *FavoriteRepo) All() ([]model.Favorite, error) {
var favorites []model.Favorite
if err := getDb().Find(&favorites).Error; err != nil {
return nil, err
}
return favorites, nil
}

View File

@ -37,4 +37,6 @@ var (
snapshotRepo = repo.NewISnapshotRepo()
runtimeRepo = repo.NewIRunTimeRepo()
favoriteRepo = repo.NewIFavoriteRepo()
)

View File

@ -0,0 +1,83 @@
package service
import (
"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/app/model"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/spf13/afero"
)
type FavoriteService struct {
}
type IFavoriteService interface {
Page(req dto.PageInfo) (int64, []response.FavoriteDTO, error)
Create(req request.FavoriteCreate) (*model.Favorite, error)
Delete(id uint) error
}
func NewIFavoriteService() IFavoriteService {
return &FavoriteService{}
}
func (f *FavoriteService) Page(req dto.PageInfo) (int64, []response.FavoriteDTO, error) {
total, favorites, err := favoriteRepo.Page(req.Page, req.PageSize)
if err != nil {
return 0, nil, err
}
var dtoFavorites []response.FavoriteDTO
for _, favorite := range favorites {
dtoFavorites = append(dtoFavorites, response.FavoriteDTO{
Favorite: favorite,
})
}
return total, dtoFavorites, nil
}
func (f *FavoriteService) Create(req request.FavoriteCreate) (*model.Favorite, error) {
exist, _ := favoriteRepo.GetFirst(favoriteRepo.WithByPath(req.Path))
if exist.ID > 0 {
return nil, buserr.New(constant.ErrFavoriteExist)
}
op := files.NewFileOp()
if !op.Stat(req.Path) {
return nil, buserr.New(constant.ErrLinkPathNotFound)
}
openFile, err := op.OpenFile(req.Path)
if err != nil {
return nil, err
}
fileInfo, err := openFile.Stat()
if err != nil {
return nil, err
}
favorite := &model.Favorite{
Name: fileInfo.Name(),
IsDir: fileInfo.IsDir(),
Path: req.Path,
}
if fileInfo.Size() <= 10*1024*1024 {
afs := &afero.Afero{Fs: op.Fs}
cByte, err := afs.ReadFile(req.Path)
if err == nil {
if len(cByte) > 0 && !files.DetectBinary(cByte) {
favorite.IsTxt = true
}
}
}
if err := favoriteRepo.Create(favorite); err != nil {
return nil, err
}
return favorite, nil
}
func (f *FavoriteService) Delete(id uint) error {
if err := favoriteRepo.Delete(commonRepo.WithByID(id)); err != nil {
return err
}
return nil
}

View File

@ -90,6 +90,7 @@ var (
ErrFileUpload = "ErrFileUpload"
ErrFileDownloadDir = "ErrFileDownloadDir"
ErrCmdNotFound = "ErrCmdNotFound"
ErrFavoriteExist = "ErrFavoriteExist"
)
// mysql

View File

@ -64,6 +64,7 @@ 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"
ErrFavoriteExist: "This path has been collected"
#website
ErrDomainIsExist: "Domain is already exist"

View File

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

View File

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

View File

@ -47,6 +47,7 @@ func Init() {
migrations.AddDefaultNetwork,
migrations.UpdateRuntime,
migrations.UpdateTag,
migrations.AddFavorite,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@ -0,0 +1,17 @@
package migrations
import (
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
var AddFavorite = &gormigrate.Migration{
ID: "20231020-add-favorite",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Favorite{}); err != nil {
return err
}
return nil
},
}

View File

@ -41,5 +41,10 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) {
fileRouter.POST("/recycle/search", baseApi.SearchRecycleBinFile)
fileRouter.POST("/recycle/reduce", baseApi.ReduceRecycleBinFile)
fileRouter.POST("/recycle/clear", baseApi.ClearRecycleBinFile)
fileRouter.POST("/favorite/search", baseApi.SearchFavorite)
fileRouter.POST("/favorite", baseApi.CreateFavorite)
fileRouter.POST("/favorite/del", baseApi.DeleteFavorite)
}
}

View File

@ -3,6 +3,7 @@ package files
import (
"bufio"
"fmt"
"github.com/1Panel-dev/1Panel/backend/app/repo"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"io/fs"
@ -42,6 +43,7 @@ type FileInfo struct {
FileMode os.FileMode `json:"-"`
Items []*FileInfo `json:"items"`
ItemTotal int `json:"itemTotal"`
FavoriteID uint `json:"favoriteID"`
}
type FileOption struct {
@ -88,6 +90,12 @@ func NewFileInfo(op FileOption) (*FileInfo, error) {
Group: GetGroup(info.Sys().(*syscall.Stat_t).Gid),
MimeType: GetMimeType(op.Path),
}
favoriteRepo := repo.NewIFavoriteRepo()
favorite, _ := favoriteRepo.GetFirst(favoriteRepo.WithByPath(op.Path))
if favorite.ID > 0 {
file.FavoriteID = favorite.ID
}
if file.IsSymlink {
file.LinkPath = GetSymlink(op.Path)
}
@ -266,7 +274,11 @@ func (f *FileInfo) listChildren(option FileOption) error {
Uid: strconv.FormatUint(uint64(df.Sys().(*syscall.Stat_t).Uid), 10),
Gid: strconv.FormatUint(uint64(df.Sys().(*syscall.Stat_t).Gid), 10),
}
favoriteRepo := repo.NewIFavoriteRepo()
favorite, _ := favoriteRepo.GetFirst(favoriteRepo.WithByPath(fPath))
if favorite.ID > 0 {
file.FavoriteID = favorite.ID
}
if isSymlink {
file.LinkPath = GetSymlink(fPath)
}
@ -305,7 +317,7 @@ func (f *FileInfo) getContent() error {
if err != nil {
return nil
}
if len(cByte) > 0 && detectBinary(cByte) {
if len(cByte) > 0 && DetectBinary(cByte) {
return buserr.New(constant.ErrFileCanNotRead)
}
f.Content = string(cByte)
@ -315,7 +327,7 @@ func (f *FileInfo) getContent() error {
}
}
func detectBinary(buf []byte) bool {
func DetectBinary(buf []byte) bool {
whiteByte := 0
n := min(1024, len(buf))
for i := 0; i < n; i++ {

View File

@ -5368,6 +5368,132 @@ const docTemplate = `{
}
}
},
"/files/favorite": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "创建收藏",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "Create favorite",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.FavoriteCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"path"
],
"formatEN": "收藏文件/文件夹 [path]",
"formatZH": "收藏文件/文件夹 [path]",
"paramKeys": []
}
}
},
"/files/favorite/del": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "删除收藏",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "Delete favorite",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.FavoriteDelete"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "favorites",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "path",
"output_value": "path"
}
],
"bodyKeys": [
"id"
],
"formatEN": "delete avorite [path]",
"formatZH": "删除收藏 [path]",
"paramKeys": []
}
}
},
"/files/favorite/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取收藏列表",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "List favorites",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.PageInfo"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/files/mode": {
"post": {
"security": [
@ -5498,6 +5624,110 @@ const docTemplate = `{
}
}
},
"/files/recycle/clear": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "清空回收站文件",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "Clear RecycleBin files",
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "清空回收站",
"formatZH": "清空回收站",
"paramKeys": []
}
}
},
"/files/recycle/reduce": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "还原回收站文件",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "Reduce RecycleBin files",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.RecycleBinReduce"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name"
],
"formatEN": "Reduce RecycleBin file [name]",
"formatZH": "还原回收站文件 [name]",
"paramKeys": []
}
}
},
"/files/recycle/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取回收站文件列表",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "List RecycleBin files",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.PageInfo"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/files/rename": {
"post": {
"security": [
@ -7826,94 +8056,6 @@ 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": [
@ -15563,6 +15705,9 @@ const docTemplate = `{
"extension": {
"type": "string"
},
"favoriteID": {
"type": "integer"
},
"gid": {
"type": "string"
},
@ -16212,6 +16357,28 @@ const docTemplate = `{
}
}
},
"request.FavoriteCreate": {
"type": "object",
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
}
}
},
"request.FavoriteDelete": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "integer"
}
}
},
"request.FileBatchDelete": {
"type": "object",
"required": [
@ -16938,6 +17105,9 @@ const docTemplate = `{
"from": {
"type": "string"
},
"name": {
"type": "string"
},
"rName": {
"type": "string"
}
@ -18062,6 +18232,9 @@ const docTemplate = `{
"extension": {
"type": "string"
},
"favoriteID": {
"type": "integer"
},
"gid": {
"type": "string"
},

View File

@ -5361,6 +5361,132 @@
}
}
},
"/files/favorite": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "创建收藏",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "Create favorite",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.FavoriteCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"path"
],
"formatEN": "收藏文件/文件夹 [path]",
"formatZH": "收藏文件/文件夹 [path]",
"paramKeys": []
}
}
},
"/files/favorite/del": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "删除收藏",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "Delete favorite",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.FavoriteDelete"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "favorites",
"input_column": "id",
"input_value": "id",
"isList": false,
"output_column": "path",
"output_value": "path"
}
],
"bodyKeys": [
"id"
],
"formatEN": "delete avorite [path]",
"formatZH": "删除收藏 [path]",
"paramKeys": []
}
}
},
"/files/favorite/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取收藏列表",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "List favorites",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.PageInfo"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/files/mode": {
"post": {
"security": [
@ -5491,6 +5617,110 @@
}
}
},
"/files/recycle/clear": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "清空回收站文件",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "Clear RecycleBin files",
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "清空回收站",
"formatZH": "清空回收站",
"paramKeys": []
}
}
},
"/files/recycle/reduce": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "还原回收站文件",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "Reduce RecycleBin files",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.RecycleBinReduce"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"name"
],
"formatEN": "Reduce RecycleBin file [name]",
"formatZH": "还原回收站文件 [name]",
"paramKeys": []
}
}
},
"/files/recycle/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取回收站文件列表",
"consumes": [
"application/json"
],
"tags": [
"File"
],
"summary": "List RecycleBin files",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.PageInfo"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/files/rename": {
"post": {
"security": [
@ -7819,94 +8049,6 @@
}
}
},
"/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": [
@ -15556,6 +15698,9 @@
"extension": {
"type": "string"
},
"favoriteID": {
"type": "integer"
},
"gid": {
"type": "string"
},
@ -16205,6 +16350,28 @@
}
}
},
"request.FavoriteCreate": {
"type": "object",
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
}
}
},
"request.FavoriteDelete": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "integer"
}
}
},
"request.FileBatchDelete": {
"type": "object",
"required": [
@ -16931,6 +17098,9 @@
"from": {
"type": "string"
},
"name": {
"type": "string"
},
"rName": {
"type": "string"
}
@ -18055,6 +18225,9 @@
"extension": {
"type": "string"
},
"favoriteID": {
"type": "integer"
},
"gid": {
"type": "string"
},

View File

@ -2280,6 +2280,8 @@ definitions:
type: string
extension:
type: string
favoriteID:
type: integer
gid:
type: string
group:
@ -2710,6 +2712,20 @@ definitions:
required:
- path
type: object
request.FavoriteCreate:
properties:
path:
type: string
required:
- path
type: object
request.FavoriteDelete:
properties:
id:
type: integer
required:
- id
type: object
request.FileBatchDelete:
properties:
isDir:
@ -3196,6 +3212,8 @@ definitions:
properties:
from:
type: string
name:
type: string
rName:
type: string
required:
@ -3955,6 +3973,8 @@ definitions:
type: string
extension:
type: string
favoriteID:
type: integer
gid:
type: string
group:
@ -7604,6 +7624,86 @@ paths:
formatEN: Download file [name]
formatZH: 下载文件 [name]
paramKeys: []
/files/favorite:
post:
consumes:
- application/json
description: 创建收藏
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.FavoriteCreate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Create favorite
tags:
- File
x-panel-log:
BeforeFunctions: []
bodyKeys:
- path
formatEN: 收藏文件/文件夹 [path]
formatZH: 收藏文件/文件夹 [path]
paramKeys: []
/files/favorite/del:
post:
consumes:
- application/json
description: 删除收藏
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.FavoriteDelete'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Delete favorite
tags:
- File
x-panel-log:
BeforeFunctions:
- db: favorites
input_column: id
input_value: id
isList: false
output_column: path
output_value: path
bodyKeys:
- id
formatEN: delete avorite [path]
formatZH: 删除收藏 [path]
paramKeys: []
/files/favorite/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 favorites
tags:
- File
/files/mode:
post:
consumes:
@ -7689,6 +7789,72 @@ paths:
formatEN: Change owner [paths] => [user]/[group]
formatZH: 修改用户/组 [paths] => [user]/[group]
paramKeys: []
/files/recycle/clear:
post:
consumes:
- application/json
description: 清空回收站文件
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Clear RecycleBin files
tags:
- File
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: 清空回收站
formatZH: 清空回收站
paramKeys: []
/files/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:
- File
x-panel-log:
BeforeFunctions: []
bodyKeys:
- name
formatEN: Reduce RecycleBin file [name]
formatZH: 还原回收站文件 [name]
paramKeys: []
/files/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:
- File
/files/rename:
post:
consumes:
@ -9159,59 +9325,6 @@ 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

@ -21,6 +21,7 @@ export namespace File {
items: File[];
extension: string;
itemTotal: number;
favoriteID: number;
}
export interface ReqFile extends ReqPage {
@ -162,4 +163,10 @@ export namespace File {
from: string;
name: string;
}
export interface Favorite extends CommonModel {
path: string;
isDir: boolean;
isTxt: boolean;
}
}

View File

@ -100,3 +100,15 @@ export const reduceFile = (params: File.RecycleBinReduce) => {
export const clearRecycle = () => {
return http.post<any>('files/recycle/clear');
};
export const SearchFavorite = (params: ReqPage) => {
return http.post<ResPage<File.Favorite>>('files/favorite/search', params);
};
export const AddFavorite = (path: string) => {
return http.post<any>('files/favorite', { path: path });
};
export const RemoveFavorite = (id: number) => {
return http.post<any>('files/favorite/del', { id: id });
};

View File

@ -122,6 +122,7 @@ const message = {
'You can upload only files whose name contains 1 to 256 characters, including English, Chinese, digits, or periods (.-_)',
confirmNoNull: 'Make sure the value {0} is not empty',
errPort: 'Incorrect port information, please confirm!',
remove: 'Remove',
},
login: {
username: 'UserName',
@ -970,7 +971,10 @@ const message = {
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? ',
clearRecycleBinHelper: 'Do you want to clear the recycle bin?',
favorite: 'favorites',
removeFavorite: 'Remove from favorites?',
addFavorite: 'Add to favorites',
},
ssh: {
autoStart: 'Auto Start',

View File

@ -123,6 +123,7 @@ const message = {
fileNameErr: '僅支持上傳名稱包含英文中文數字或者 .-_ ,長度 1-256 位的文件',
confirmNoNull: '請確認 {0} 值不為空',
errPort: '錯誤的端口信息請確認',
remove: '移出',
},
login: {
username: '用戶名',
@ -931,7 +932,10 @@ const message = {
reduce: '還原',
reduceHelper: '恢復檔案到原路徑如果檔案原始位址存在同名檔案或目錄將會覆蓋是否繼續 ',
clearRecycleBin: '清空回收站',
clearRecycleBinHelper: '是否清空回收站 ',
clearRecycleBinHelper: '是否清空回收站',
favorite: '收藏夾',
removeFavorite: '是否從收藏夾移出',
addFavorite: '加入收藏夾',
},
ssh: {
autoStart: '開機自啟',

View File

@ -123,6 +123,7 @@ const message = {
fileNameErr: '仅支持上传名称包含英文中文数字或者 .-_ ,长度 1-256 位的文件',
confirmNoNull: '请确认 {0} 值不为空',
errPort: '错误的端口信息请确认',
remove: '移出',
},
login: {
username: '用户名',
@ -933,6 +934,9 @@ const message = {
reduceHelper: '恢复文件到原路径如果文件原地址存在同名文件或目录将会覆盖是否继续',
clearRecycleBin: '清空回收站',
clearRecycleBinHelper: '是否清空回收站',
favorite: '收藏夹',
removeFavorite: '是否从收藏夹移出',
addFavorite: '加入收藏夹子',
},
ssh: {
autoStart: '开机自启',

View File

@ -0,0 +1,72 @@
<template>
<el-drawer v-model="open" :before-close="handleClose" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader :header="$t('file.favorite')" :back="handleClose" />
</template>
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search">
<el-table-column :label="$t('file.path')" show-overflow-tooltip prop="path"></el-table-column>
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fix />
</ComplexTable>
</el-drawer>
</template>
<script setup lang="ts">
import { SearchFavorite, RemoveFavorite } from '@/api/modules/files';
import i18n from '@/lang';
import { reactive, ref } from 'vue';
const paginationConfig = reactive({
cacheSizeKey: 'favorite-page-size',
currentPage: 1,
pageSize: 100,
total: 0,
});
const req = reactive({
page: 1,
pageSize: 100,
});
const open = ref(false);
const data = ref([]);
const em = defineEmits(['close']);
const handleClose = () => {
open.value = false;
em('close', false);
};
const acceptParams = () => {
search();
};
const search = async () => {
try {
const res = await SearchFavorite(req);
data.value = res.data.items;
paginationConfig.total = res.data.total;
open.value = true;
} catch (error) {}
};
const singleDel = async (id: number) => {
ElMessageBox.confirm(i18n.global.t('file.removeFavorite'), i18n.global.t('commons.msg.remove'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
try {
await RemoveFavorite(id);
search();
} catch (error) {}
});
};
const buttons = [
{
label: i18n.global.t('commons.button.delete'),
click: (row: any) => {
singleDel(row.id);
},
},
];
defineExpose({ acceptParams });
</script>

View File

@ -98,6 +98,9 @@
</div>
<div class="right-section">
<el-button @click="openFavorite" :icon="Star">
{{ $t('file.favorite') }}
</el-button>
<el-button class="btn" @click="openRecycleBin" :icon="Delete">
{{ $t('file.recycleBin') }}
</el-button>
@ -140,11 +143,42 @@
sortable
prop="name"
>
<template #default="{ row }">
<svg-icon v-if="row.isDir" className="table-icon" iconName="p-file-folder"></svg-icon>
<svg-icon v-else className="table-icon" :iconName="getIconName(row.extension)"></svg-icon>
<span class="table-link" @click="open(row)" type="primary">{{ row.name }}</span>
<span v-if="row.isSymlink">-> {{ row.linkPath }}</span>
<template #default="{ row, $index }">
<div @mouseenter="showFavorite($index)" @mouseleave="hideFavorite">
<el-row>
<el-col :span="23">
<svg-icon
v-if="row.isDir"
className="table-icon"
iconName="p-file-folder"
></svg-icon>
<svg-icon
v-else
className="table-icon"
:iconName="getIconName(row.extension)"
></svg-icon>
<span class="table-link" @click="open(row)" type="primary">{{ row.name }}</span>
<span v-if="row.isSymlink">-> {{ row.linkPath }}</span>
</el-col>
<el-col :span="1">
<el-button
v-if="row.favoriteID > 0"
link
type="warning"
:icon="Star"
@click="removeFavorite(row.favoriteID)"
></el-button>
<div v-else>
<el-button
v-if="hoveredRowIndex === $index"
link
:icon="Star"
@click="addFavorite(row)"
></el-button>
</div>
</el-col>
</el-row>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('file.mode')" prop="mode" max-width="50">
@ -213,16 +247,25 @@
<Detail ref="detailRef" />
<DeleteFile ref="deleteRef" @close="search" />
<RecycleBin ref="recycleBinRef" @close="search" />
<Favorite ref="favoriteRef" @close="search" />
</LayoutContent>
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, reactive, ref, computed } from '@vue/runtime-core';
import { GetFilesList, GetFileContent, ComputeDirSize } from '@/api/modules/files';
import { GetFilesList, GetFileContent, ComputeDirSize, AddFavorite, RemoveFavorite } from '@/api/modules/files';
import { computeSize, dateFormat, downloadFile, getIcon, getRandomStr } from '@/utils/util';
import { Delete } from '@element-plus/icons-vue';
import { Delete, Star } from '@element-plus/icons-vue';
import { File } from '@/api/interface/file';
import { Mimetypes, Languages } from '@/global/mimetype';
import { useRouter } from 'vue-router';
import { Back, Refresh } from '@element-plus/icons-vue';
import { MsgSuccess, MsgWarning } from '@/utils/message';
import { useSearchable } from './hooks/searchable';
import { ResultData } from '@/api/interface';
import { GlobalStore } from '@/store';
import i18n from '@/lang';
import CreateFile from './create/index.vue';
import ChangeRole from './change-role/index.vue';
@ -236,16 +279,11 @@ import Move from './move/index.vue';
import Download from './download/index.vue';
import Owner from './chown/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';
import { useSearchable } from './hooks/searchable';
import { ResultData } from '@/api/interface';
import { GlobalStore } from '@/store';
import Favorite from './favorite/index.vue';
const globalStore = GlobalStore();
interface FilePaths {
@ -303,6 +341,8 @@ const chownRef = ref();
const moveOpen = ref(false);
const deleteRef = ref();
const recycleBinRef = ref();
const favoriteRef = ref();
const hoveredRowIndex = ref(-1);
// editablePath
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchable(paths);
@ -627,6 +667,10 @@ const openRecycleBin = () => {
recycleBinRef.value.acceptParams();
};
const openFavorite = () => {
favoriteRef.value.acceptParams();
};
const changeSort = ({ prop, order }) => {
req.sortBy = prop;
req.sortOrder = order;
@ -637,6 +681,33 @@ const changeSort = ({ prop, order }) => {
search();
};
const showFavorite = (index: any) => {
hoveredRowIndex.value = index;
};
const hideFavorite = () => {
hoveredRowIndex.value = -1;
};
const addFavorite = async (row: File.File) => {
try {
await AddFavorite(row.path);
search();
} catch (error) {}
};
const removeFavorite = async (id: number) => {
ElMessageBox.confirm(i18n.global.t('file.removeFavorite'), i18n.global.t('commons.msg.remove'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
try {
await RemoveFavorite(id);
search();
} catch (error) {}
});
};
const buttons = [
{
label: i18n.global.t('file.open'),
@ -759,7 +830,9 @@ onMounted(() => {
margin-left: 10px;
}
.right-section > *:not(:last-child) {
margin-right: 10px;
.right-section {
.btn {
margin-right: 10px;
}
}
</style>