feat: 文件管理增加用户/用户组设置 (#803)

pull/804/head
zhengkunwang223 2 years ago committed by GitHub
parent db2aa35b2f
commit 7887bf96de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

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

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

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

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

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

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

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

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

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

@ -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: 系统时间同步

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

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

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

@ -67,6 +67,10 @@ export const RenameRile = (params: File.FileRename) => {
return http.post<File.File>('files/rename', params);
};
export const ChangeOwner = (params: File.FileOwner) => {
return http.post<File.File>('files/owner', params);
};
export const WgetFile = (params: File.FileWget) => {
return http.post<File.FileWgetRes>('files/wget', params);
};

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

@ -807,6 +807,9 @@ const message = {
copyDir: '',
paste: '',
cancel: '',
changeOwner: '',
containSub: '',
ownerHelper: 'PHP : 1000:1000, ',
},
setting: {
all: '',

@ -0,0 +1,108 @@
<template>
<el-drawer v-model="open" size="40%">
<template #header>
<DrawerHeader :header="$t('file.changeOwner')" :back="handleClose" />
</template>
<el-row>
<el-col :span="22" :offset="1">
<el-form
ref="fileForm"
label-position="top"
:model="addForm"
label-width="100px"
:rules="rules"
v-loading="loading"
>
<el-form-item :label="$t('file.user')" prop="user">
<el-input v-model.trim="addForm.user" />
</el-form-item>
<el-form-item :label="$t('file.group')" prop="group">
<el-input v-model.trim="addForm.group" />
</el-form-item>
<el-form-item v-if="isDir">
<el-checkbox v-model="addForm.sub">{{ $t('file.containSub') }}</el-checkbox>
</el-form-item>
<el-form-item>
<el-alert :title="$t('file.ownerHelper')" type="info" :closable="false" />
</el-form-item>
</el-form>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(fileForm)">{{ $t('commons.button.confirm') }}</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { ChangeOwner } from '@/api/modules/files';
import { Rules } from '@/global/form-rules';
import { FormInstance, FormRules } from 'element-plus';
import { reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
interface OwnerProps {
path: string;
user: string;
group: string;
isDir: boolean;
}
const fileForm = ref<FormInstance>();
const loading = ref(false);
const open = ref(false);
const isDir = ref(false);
const addForm = reactive({
path: '',
user: '',
group: '',
sub: false,
});
const rules = reactive<FormRules>({
user: [Rules.requiredInput],
group: [Rules.requiredInput],
});
const em = defineEmits(['close']);
const handleClose = () => {
open.value = false;
if (fileForm.value) {
fileForm.value.resetFields();
}
em('close', false);
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
loading.value = true;
ChangeOwner(addForm)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
});
};
const acceptParams = (props: OwnerProps) => {
addForm.user = props.user;
addForm.path = props.path;
addForm.group = props.group;
isDir.value = props.isDir;
open.value = true;
};
defineExpose({ acceptParams });
</script>

@ -123,8 +123,16 @@
<el-link :underline="false" @click="openMode(row)" type="primary">{{ row.mode }}</el-link>
</template>
</el-table-column>
<!-- <el-table-column :label="$t('file.user')" prop="user" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('file.group')" prop="group"></el-table-column> -->
<el-table-column :label="$t('file.user')" prop="user" show-overflow-tooltip>
<template #default="{ row }">
<el-link :underline="false" @click="openChown(row)" type="primary">{{ row.user }}</el-link>
</template>
</el-table-column>
<el-table-column :label="$t('file.group')" prop="group">
<template #default="{ row }">
<el-link :underline="false" @click="openChown(row)" type="primary">{{ row.group }}</el-link>
</template>
</el-table-column>
<el-table-column :label="$t('file.size')" prop="size" max-width="50">
<template #default="{ row }">
<span v-if="row.isDir">
@ -167,6 +175,7 @@
<Move ref="moveRef" @close="closeMovePage" />
<Download ref="downloadRef" @close="search" />
<Process :open="processPage.open" @close="closeProcess" />
<Owner ref="chownRef" @close="search"></Owner>
<!-- <Detail ref="detailRef" /> -->
</LayoutContent>
</div>
@ -191,6 +200,7 @@ import CodeEditor from './code-editor/index.vue';
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 { Mimetypes, Languages } from '@/global/mimetype';
import Process from './process/index.vue';
// import Detail from './detail/index.vue';
@ -251,7 +261,7 @@ const moveRef = ref();
const downloadRef = ref();
const pathRef = ref();
const breadCrumbRef = ref();
const chownRef = ref();
const moveOpen = ref(false);
// editablePath
@ -443,6 +453,10 @@ const openMode = (item: File.File) => {
roleRef.value.acceptParams(item);
};
const openChown = (item: File.File) => {
chownRef.value.acceptParams(item);
};
const openCompress = (items: File.File[]) => {
const paths = [];
for (const item of items) {

Loading…
Cancel
Save