diff --git a/backend/app/api/v1/file.go b/backend/app/api/v1/file.go index 2a6276372..05a82d111 100644 --- a/backend/app/api/v1/file.go +++ b/backend/app/api/v1/file.go @@ -186,7 +186,29 @@ func (b *BaseApi) ChangeFileMode(c *gin.Context) { helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return } - helper.SuccessWithData(c, nil) + helper.SuccessWithOutData(c) +} + +// @Tags File +// @Summary Change file owner +// @Description 修改文件用户/组 +// @Accept json +// @Param request body request.FileRoleUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /files/owner [post] +// @x-panel-log {"bodyKeys":["path","user","group"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"修改用户/组 [paths] => [user]/[group]","formatEN":"Change owner [paths] => [user]/[group]"} +func (b *BaseApi) ChangeFileOwner(c *gin.Context) { + var req request.FileRoleUpdate + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := fileService.ChangeOwner(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) } // @Tags File diff --git a/backend/app/api/v1/setting.go b/backend/app/api/v1/setting.go index 3d1714de1..f16aaa200 100644 --- a/backend/app/api/v1/setting.go +++ b/backend/app/api/v1/setting.go @@ -127,7 +127,7 @@ func (b *BaseApi) UpdatePassword(c *gin.Context) { // @Success 200 // @Security ApiKeyAuth // @Router /settings/ssl/update [post] -// @x-panel-log {"bodyKeys":[ssl],"paramKeys":[],"BeforeFuntions":[],"formatZH":"修改系统 ssl => [ssl]","formatEN":"update system ssl => [ssl]"} +// @x-panel-log {"bodyKeys":["ssl"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"修改系统 ssl => [ssl]","formatEN":"update system ssl => [ssl]"} func (b *BaseApi) UpdateSSL(c *gin.Context) { var req dto.SSLUpdate if err := c.ShouldBindJSON(&req); err != nil { diff --git a/backend/app/dto/request/file.go b/backend/app/dto/request/file.go index a7c384ead..f4468fd93 100644 --- a/backend/app/dto/request/file.go +++ b/backend/app/dto/request/file.go @@ -88,3 +88,10 @@ type DirSizeReq struct { type FileProcessReq struct { Key string `json:"key"` } + +type FileRoleUpdate struct { + Path string `json:"path" validate:"required"` + User string `json:"user" validate:"required"` + Group string `json:"group" validate:"required"` + Sub bool `json:"sub" validate:"required"` +} diff --git a/backend/app/service/file.go b/backend/app/service/file.go index e26fe8e23..8bc371aa2 100644 --- a/backend/app/service/file.go +++ b/backend/app/service/file.go @@ -39,6 +39,7 @@ type IFileService interface { ChangeName(req request.FileRename) error Wget(w request.FileWget) (string, error) MvFile(m request.FileMove) error + ChangeOwner(req request.FileRoleUpdate) error } func NewIFileService() IFileService { @@ -162,6 +163,11 @@ func (f *FileService) ChangeMode(op request.FileCreate) error { return fo.Chmod(op.Path, fs.FileMode(op.Mode)) } +func (f *FileService) ChangeOwner(req request.FileRoleUpdate) error { + fo := files.NewFileOp() + return fo.ChownR(req.Path, req.User, req.Group, req.Sub) +} + func (f *FileService) Compress(c request.FileCompress) error { fo := files.NewFileOp() if !c.Replace && fo.Stat(filepath.Join(c.Dst, c.Name)) { diff --git a/backend/router/ro_file.go b/backend/router/ro_file.go index 8a552c844..6b5365dde 100644 --- a/backend/router/ro_file.go +++ b/backend/router/ro_file.go @@ -21,6 +21,7 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) { fileRouter.POST("/del", baseApi.DeleteFile) fileRouter.POST("/batch/del", baseApi.BatchDeleteFile) fileRouter.POST("/mode", baseApi.ChangeFileMode) + fileRouter.POST("/owner", baseApi.ChangeFileOwner) fileRouter.POST("/compress", baseApi.CompressFile) fileRouter.POST("/decompress", baseApi.DeCompressFile) fileRouter.POST("/content", baseApi.GetContent) diff --git a/backend/utils/files/file_op.go b/backend/utils/files/file_op.go index d93c15dcc..2ee8b5b7b 100644 --- a/backend/utils/files/file_op.go +++ b/backend/utils/files/file_op.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/1Panel-dev/1Panel/backend/utils/cmd" "io" "io/fs" "net/http" @@ -106,7 +107,7 @@ func (f FileOp) SaveFile(dst string, content string, mode fs.FileMode) error { } defer file.Close() write := bufio.NewWriter(file) - _, _ = write.WriteString(string(content)) + _, _ = write.WriteString(content) write.Flush() return nil } @@ -119,6 +120,34 @@ func (f FileOp) Chown(dst string, uid int, gid int) error { return f.Fs.Chown(dst, uid, gid) } +func (f FileOp) ChownR(dst string, uid string, gid string, sub bool) error { + cmdStr := fmt.Sprintf("sudo chown %s:%s %s", uid, gid, dst) + if sub { + cmdStr = fmt.Sprintf("sudo chown -R %s:%s %s", uid, gid, dst) + } + if msg, err := cmd.ExecWithTimeOut(cmdStr, 2*time.Second); err != nil { + if msg != "" { + return errors.New(msg) + } + return err + } + return nil +} + +func (f FileOp) ChmodR(dst string, mode fs.FileMode) error { + cmdStr := fmt.Sprintf("chmod -R %v %s", mode, dst) + if cmd.HasNoPasswordSudo() { + cmdStr = fmt.Sprintf("sudo %s", cmdStr) + } + if msg, err := cmd.ExecWithTimeOut(cmdStr, 2*time.Second); err != nil { + if msg != "" { + return errors.New(msg) + } + return err + } + return nil +} + func (f FileOp) Rename(oldName string, newName string) error { return f.Fs.Rename(oldName, newName) } diff --git a/backend/utils/files/fileinfo.go b/backend/utils/files/fileinfo.go index d33558bdb..668b5fcd2 100644 --- a/backend/utils/files/fileinfo.go +++ b/backend/utils/files/fileinfo.go @@ -9,6 +9,7 @@ import ( "path" "path/filepath" "strings" + "syscall" "time" "github.com/spf13/afero" @@ -73,6 +74,8 @@ func NewFileInfo(op FileOption) (*FileInfo, error) { Extension: filepath.Ext(info.Name()), IsHidden: IsHidden(op.Path), Mode: fmt.Sprintf("%04o", info.Mode().Perm()), + User: GetUsername(info.Sys().(*syscall.Stat_t).Uid), + Group: GetGroup(info.Sys().(*syscall.Stat_t).Gid), MimeType: GetMimeType(op.Path), } if file.IsSymlink { @@ -202,6 +205,8 @@ func (f *FileInfo) listChildren(dir, showHidden, containSub bool, search string, Path: fPath, Mode: fmt.Sprintf("%04o", df.Mode().Perm()), MimeType: GetMimeType(fPath), + User: GetUsername(df.Sys().(*syscall.Stat_t).Uid), + Group: GetGroup(df.Sys().(*syscall.Stat_t).Gid), } if isSymlink { diff --git a/backend/utils/files/utils.go b/backend/utils/files/utils.go index 92282b1ac..c75ba980c 100644 --- a/backend/utils/files/utils.go +++ b/backend/utils/files/utils.go @@ -4,7 +4,9 @@ import ( "github.com/gabriel-vasile/mimetype" "github.com/spf13/afero" "os" + "os/user" "path/filepath" + "strconv" "sync" ) @@ -28,6 +30,22 @@ func GetSymlink(path string) string { return linkPath } +func GetUsername(uid uint32) string { + usr, err := user.LookupId(strconv.Itoa(int(uid))) + if err != nil { + return "" + } + return usr.Username +} + +func GetGroup(gid uint32) string { + usr, err := user.LookupGroupId(strconv.Itoa(int(gid))) + if err != nil { + return "" + } + return usr.Name +} + func ScanDir(fs afero.Fs, path string, dirMap *sync.Map, wg *sync.WaitGroup) { afs := &afero.Afero{Fs: fs} files, _ := afs.ReadDir(path) diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index b3fa8c8af..5e85306c5 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -4529,6 +4529,50 @@ const docTemplate = `{ } } }, + "/files/owner": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改文件用户/组", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Change file owner", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileRoleUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFuntions": [], + "bodyKeys": [ + "path", + "user", + "group" + ], + "formatEN": "Change owner [paths] =\u003e [user]/[group]", + "formatZH": "修改用户/组 [paths] =\u003e [user]/[group]", + "paramKeys": [] + } + } + }, "/files/rename": { "post": { "security": [ @@ -7431,6 +7475,70 @@ const docTemplate = `{ } } }, + "/settings/ssl/info": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取证书信息", + "tags": [ + "System Setting" + ], + "summary": "Load system cert info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SettingInfo" + } + } + } + } + }, + "/settings/ssl/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统 ssl 登录", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SSLUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFuntions": [], + "bodyKeys": [ + "ssl" + ], + "formatEN": "update system ssl =\u003e [ssl]", + "formatZH": "修改系统 ssl =\u003e [ssl]", + "paramKeys": [] + } + } + }, "/settings/time/sync": { "post": { "security": [ @@ -11440,10 +11548,16 @@ const docTemplate = `{ "type": "object", "properties": { "containerPort": { - "type": "integer" + "type": "string" + }, + "hostIP": { + "type": "string" }, "hostPort": { - "type": "integer" + "type": "string" + }, + "protocol": { + "type": "string" } } }, @@ -11669,6 +11783,36 @@ const docTemplate = `{ } } }, + "dto.SSLUpdate": { + "type": "object", + "required": [ + "ssl" + ], + "properties": { + "cert": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "key": { + "type": "string" + }, + "ssl": { + "type": "string", + "enum": [ + "enable", + "disable" + ] + }, + "sslID": { + "type": "integer" + }, + "sslType": { + "type": "string" + } + } + }, "dto.SearchForTree": { "type": "object", "properties": { @@ -11851,6 +11995,12 @@ const docTemplate = `{ "sessionTimeout": { "type": "string" }, + "ssl": { + "type": "string" + }, + "sslType": { + "type": "string" + }, "systemVersion": { "type": "string" }, @@ -12796,6 +12946,29 @@ const docTemplate = `{ } } }, + "request.FileRoleUpdate": { + "type": "object", + "required": [ + "group", + "path", + "sub", + "user" + ], + "properties": { + "group": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sub": { + "type": "boolean" + }, + "user": { + "type": "string" + } + } + }, "request.FileWget": { "type": "object", "required": [ diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index 3114e9d78..e4ad62e63 100644 --- a/cmd/server/docs/swagger.json +++ b/cmd/server/docs/swagger.json @@ -4522,6 +4522,50 @@ } } }, + "/files/owner": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改文件用户/组", + "consumes": [ + "application/json" + ], + "tags": [ + "File" + ], + "summary": "Change file owner", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.FileRoleUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFuntions": [], + "bodyKeys": [ + "path", + "user", + "group" + ], + "formatEN": "Change owner [paths] =\u003e [user]/[group]", + "formatZH": "修改用户/组 [paths] =\u003e [user]/[group]", + "paramKeys": [] + } + } + }, "/files/rename": { "post": { "security": [ @@ -7424,6 +7468,70 @@ } } }, + "/settings/ssl/info": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取证书信息", + "tags": [ + "System Setting" + ], + "summary": "Load system cert info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SettingInfo" + } + } + } + } + }, + "/settings/ssl/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "修改系统 ssl 登录", + "consumes": [ + "application/json" + ], + "tags": [ + "System Setting" + ], + "summary": "Update system ssl", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.SSLUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "x-panel-log": { + "BeforeFuntions": [], + "bodyKeys": [ + "ssl" + ], + "formatEN": "update system ssl =\u003e [ssl]", + "formatZH": "修改系统 ssl =\u003e [ssl]", + "paramKeys": [] + } + } + }, "/settings/time/sync": { "post": { "security": [ @@ -11433,10 +11541,16 @@ "type": "object", "properties": { "containerPort": { - "type": "integer" + "type": "string" + }, + "hostIP": { + "type": "string" }, "hostPort": { - "type": "integer" + "type": "string" + }, + "protocol": { + "type": "string" } } }, @@ -11662,6 +11776,36 @@ } } }, + "dto.SSLUpdate": { + "type": "object", + "required": [ + "ssl" + ], + "properties": { + "cert": { + "type": "string" + }, + "domain": { + "type": "string" + }, + "key": { + "type": "string" + }, + "ssl": { + "type": "string", + "enum": [ + "enable", + "disable" + ] + }, + "sslID": { + "type": "integer" + }, + "sslType": { + "type": "string" + } + } + }, "dto.SearchForTree": { "type": "object", "properties": { @@ -11844,6 +11988,12 @@ "sessionTimeout": { "type": "string" }, + "ssl": { + "type": "string" + }, + "sslType": { + "type": "string" + }, "systemVersion": { "type": "string" }, @@ -12789,6 +12939,29 @@ } } }, + "request.FileRoleUpdate": { + "type": "object", + "required": [ + "group", + "path", + "sub", + "user" + ], + "properties": { + "group": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sub": { + "type": "boolean" + }, + "user": { + "type": "string" + } + } + }, "request.FileWget": { "type": "object", "required": [ diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index b1a8cea8c..9412b55dd 100644 --- a/cmd/server/docs/swagger.yaml +++ b/cmd/server/docs/swagger.yaml @@ -1201,9 +1201,13 @@ definitions: dto.PortHelper: properties: containerPort: - type: integer + type: string + hostIP: + type: string hostPort: - type: integer + type: string + protocol: + type: string type: object dto.PortRuleOperate: properties: @@ -1354,6 +1358,26 @@ definitions: used_memory_rss: type: string type: object + dto.SSLUpdate: + properties: + cert: + type: string + domain: + type: string + key: + type: string + ssl: + enum: + - enable + - disable + type: string + sslID: + type: integer + sslType: + type: string + required: + - ssl + type: object dto.SearchForTree: properties: info: @@ -1475,6 +1499,10 @@ definitions: type: string sessionTimeout: type: string + ssl: + type: string + sslType: + type: string systemVersion: type: string theme: @@ -2101,6 +2129,22 @@ definitions: - newName - oldName type: object + request.FileRoleUpdate: + properties: + group: + type: string + path: + type: string + sub: + type: boolean + user: + type: string + required: + - group + - path + - sub + - user + type: object request.FileWget: properties: name: @@ -5899,6 +5943,35 @@ paths: formatEN: Move [oldPaths] => [newPath] formatZH: 移动文件 [oldPaths] => [newPath] paramKeys: [] + /files/owner: + post: + consumes: + - application/json + description: 修改文件用户/组 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/request.FileRoleUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Change file owner + tags: + - File + x-panel-log: + BeforeFuntions: [] + bodyKeys: + - path + - user + - group + formatEN: Change owner [paths] => [user]/[group] + formatZH: 修改用户/组 [paths] => [user]/[group] + paramKeys: [] /files/rename: post: consumes: @@ -7742,6 +7815,46 @@ paths: summary: Page system snapshot tags: - System Setting + /settings/ssl/info: + get: + description: 获取证书信息 + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.SettingInfo' + security: + - ApiKeyAuth: [] + summary: Load system cert info + tags: + - System Setting + /settings/ssl/update: + post: + consumes: + - application/json + description: 修改系统 ssl 登录 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.SSLUpdate' + responses: + "200": + description: OK + security: + - ApiKeyAuth: [] + summary: Update system ssl + tags: + - System Setting + x-panel-log: + BeforeFuntions: [] + bodyKeys: + - ssl + formatEN: update system ssl => [ssl] + formatZH: 修改系统 ssl => [ssl] + paramKeys: [] /settings/time/sync: post: description: 系统时间同步 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d29e06a3b..33fbd0d04 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,7 +17,7 @@ "axios": "^0.27.2", "echarts": "^5.3.0", "echarts-liquidfill": "^3.1.0", - "element-plus": "^2.2.32", + "element-plus": "^2.3.4", "fit2cloud-ui-plus": "^1.0.7", "js-base64": "^3.7.2", "js-md5": "^0.7.3", diff --git a/frontend/package.json b/frontend/package.json index 4d4708369..c405a7924 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,7 @@ "axios": "^0.27.2", "echarts": "^5.3.0", "echarts-liquidfill": "^3.1.0", - "element-plus": "^2.2.32", + "element-plus": "^2.3.4", "fit2cloud-ui-plus": "^1.0.7", "js-base64": "^3.7.2", "js-md5": "^0.7.3", diff --git a/frontend/src/api/interface/file.ts b/frontend/src/api/interface/file.ts index a8e26cdd9..12b28dee5 100644 --- a/frontend/src/api/interface/file.ts +++ b/frontend/src/api/interface/file.ts @@ -90,6 +90,13 @@ export namespace File { newName: string; } + export interface FileOwner { + path: string; + user: string; + group: string; + sub: boolean; + } + export interface FileWget { path: string; name: string; diff --git a/frontend/src/api/modules/files.ts b/frontend/src/api/modules/files.ts index 869590357..3aed29fe7 100644 --- a/frontend/src/api/modules/files.ts +++ b/frontend/src/api/modules/files.ts @@ -67,6 +67,10 @@ export const RenameRile = (params: File.FileRename) => { return http.post('files/rename', params); }; +export const ChangeOwner = (params: File.FileOwner) => { + return http.post('files/owner', params); +}; + export const WgetFile = (params: File.FileWget) => { return http.post('files/wget', params); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 648714a41..2872ca7d7 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -800,6 +800,10 @@ const message = { copyDir: 'Copy Dir', paste: 'Paste', cancel: 'Cancel', + changeOwner: 'Modify user and user group', + containSub: 'Modify sub-file attributes at the same time', + ownerHelper: + 'The default user of the PHP operating environment: the user group is 1000:1000, it is normal that the users inside and outside the container show inconsistencies', }, setting: { all: 'All', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index daa0ca3a7..99091c50f 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -807,6 +807,9 @@ const message = { copyDir: '复制路径', paste: '粘贴', cancel: '取消', + changeOwner: '修改用户和用户组', + containSub: '同时修改子文件属性', + ownerHelper: 'PHP 运行环境默认用户:用户组为 1000:1000, 容器内外用户显示不一致为正常现象', }, setting: { all: '全部', diff --git a/frontend/src/views/host/file-management/chown/index.vue b/frontend/src/views/host/file-management/chown/index.vue new file mode 100644 index 000000000..3b50103cf --- /dev/null +++ b/frontend/src/views/host/file-management/chown/index.vue @@ -0,0 +1,108 @@ + + + diff --git a/frontend/src/views/host/file-management/index.vue b/frontend/src/views/host/file-management/index.vue index 475154bc4..0ddb5f248 100644 --- a/frontend/src/views/host/file-management/index.vue +++ b/frontend/src/views/host/file-management/index.vue @@ -123,8 +123,16 @@ {{ row.mode }} - + + + + + +