feat: 新增批量修改文件权限功能 (#2778)

Refs https://github.com/1Panel-dev/1Panel/issues/1330
pull/2786/head
zhengkunwang 1 year ago committed by GitHub
parent eebef06c77
commit 51cbd7bf9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -707,3 +707,23 @@ func (b *BaseApi) ReadFileByLine(c *gin.Context) {
res.Content = strings.Join(lines, "\n")
helper.SuccessWithData(c, res)
}
// @Tags File
// @Summary Batch change file mode and owner
// @Description 批量修改文件权限和用户/组
// @Accept json
// @Param request body request.FileRoleReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /files/batch/role [post]
// @x-panel-log {"bodyKeys":["paths","mode","user","group"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量修改文件权限和用户/组 [paths] => [mode]/[user]/[group]","formatEN":"Batch change file mode and owner [paths] => [mode]/[user]/[group]"}
func (b *BaseApi) BatchChangeModeAndOwner(c *gin.Context) {
var req request.FileRoleReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := fileService.BatchChangeModeAndOwner(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
}
helper.SuccessWithOutData(c)
}

@ -29,6 +29,14 @@ type FileCreate struct {
Sub bool `json:"sub"`
}
type FileRoleReq struct {
Paths []string `json:"paths" validate:"required"`
Mode int64 `json:"mode" validate:"required"`
User string `json:"user" validate:"required"`
Group string `json:"group" validate:"required"`
Sub bool `json:"sub"`
}
type FileDelete struct {
Path string `json:"path" validate:"required"`
IsDir bool `json:"isDir"`
@ -114,7 +122,7 @@ type FileReadByLineReq struct {
Page int `json:"page" validate:"required"`
PageSize int `json:"pageSize" validate:"required"`
}
type FileExistReq struct {
Name string `json:"name" validate:"required"`
Dir string `json:"dir" validate:"required"`

@ -29,7 +29,6 @@ type IFileService interface {
Create(op request.FileCreate) error
Delete(op request.FileDelete) error
BatchDelete(op request.FileBatchDelete) error
ChangeMode(op request.FileCreate) error
Compress(c request.FileCompress) error
DeCompress(c request.FileDeCompress) error
GetContent(op request.FileContentReq) (response.FileInfo, error)
@ -40,6 +39,8 @@ type IFileService interface {
Wget(w request.FileWget) (string, error)
MvFile(m request.FileMove) error
ChangeOwner(req request.FileRoleUpdate) error
ChangeMode(op request.FileCreate) error
BatchChangeModeAndOwner(op request.FileRoleReq) error
}
func NewIFileService() IFileService {
@ -166,11 +167,24 @@ func (f *FileService) BatchDelete(op request.FileBatchDelete) error {
func (f *FileService) ChangeMode(op request.FileCreate) error {
fo := files.NewFileOp()
if op.Sub {
return fo.ChmodR(op.Path, op.Mode)
} else {
return fo.Chmod(op.Path, fs.FileMode(op.Mode))
return fo.ChmodR(op.Path, op.Mode, op.Sub)
}
func (f *FileService) BatchChangeModeAndOwner(op request.FileRoleReq) error {
fo := files.NewFileOp()
for _, path := range op.Paths {
if !fo.Stat(path) {
return buserr.New(constant.ErrPathNotFound)
}
if err := fo.ChownR(path, op.User, op.Group, op.Sub); err != nil {
return err
}
if err := fo.ChmodR(path, op.Mode, op.Sub); err != nil {
return err
}
}
return nil
}
func (f *FileService) ChangeOwner(req request.FileRoleUpdate) error {

@ -38,6 +38,7 @@ func (f *FileRouter) InitFileRouter(Router *gin.RouterGroup) {
fileRouter.GET("/ws", baseApi.Ws)
fileRouter.GET("/keys", baseApi.Keys)
fileRouter.POST("/read", baseApi.ReadFileByLine)
fileRouter.POST("/batch/role", baseApi.BatchChangeModeAndOwner)
fileRouter.POST("/recycle/search", baseApi.SearchRecycleBinFile)
fileRouter.POST("/recycle/reduce", baseApi.ReduceRecycleBinFile)

@ -149,8 +149,11 @@ func (f FileOp) ChownR(dst string, uid string, gid string, sub bool) error {
return nil
}
func (f FileOp) ChmodR(dst string, mode int64) error {
cmdStr := fmt.Sprintf(`chmod -R %v "%s"`, fmt.Sprintf("%04o", mode), dst)
func (f FileOp) ChmodR(dst string, mode int64, sub bool) error {
cmdStr := fmt.Sprintf(`chmod %v "%s"`, fmt.Sprintf("%04o", mode), dst)
if sub {
cmdStr = fmt.Sprintf(`chmod -R %v "%s"`, fmt.Sprintf("%04o", mode), dst)
}
if cmd.HasNoPasswordSudo() {
cmdStr = fmt.Sprintf("sudo %s", cmdStr)
}

@ -176,4 +176,12 @@ export namespace File {
isTxt: boolean;
name: string;
}
export interface FileRole {
paths: string[];
mode: number;
user: string;
group: string;
sub: boolean;
}
}

@ -116,3 +116,7 @@ export const ReadByLine = (req: File.FileReadByLine) => {
export const RemoveFavorite = (id: number) => {
return http.post<any>('files/favorite/del', { id: id });
};
export const BatchChangeRole = (params: File.FileRole) => {
return http.post<any>('files/batch/role', params);
};

@ -16,7 +16,7 @@
<el-checkbox v-model="form.public.w" :label="$t('file.wRole')" />
<el-checkbox v-model="form.public.x" :label="$t('file.xRole')" />
</el-form-item>
<el-form-item :label="$t('file.role')">
<el-form-item :label="$t('file.role')" required>
<el-input v-model="form.mode" maxlength="4" @input="changeMode"></el-input>
</el-form-item>
</el-form>

@ -0,0 +1,109 @@
<template>
<el-drawer v-model="open" :before-close="handleClose" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader :header="$t('file.setRole')" :back="handleClose" />
</template>
<el-row>
<el-col :span="22" :offset="1">
<FileRole v-loading="loading" :mode="mode" @get-mode="getMode"></FileRole>
<el-form
ref="fileForm"
label-position="left"
:model="addForm"
label-width="100px"
:rules="rules"
v-loading="loading"
>
<el-form-item :label="$t('commons.table.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>
<el-checkbox v-model="addForm.sub">{{ $t('file.containSub') }}</el-checkbox>
</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()">{{ $t('commons.button.confirm') }}</el-button>
</span>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import DrawerHeader from '@/components/drawer-header/index.vue';
import { reactive, ref } from 'vue';
import { File } from '@/api/interface/file';
import { BatchChangeRole } from '@/api/modules/files';
import i18n from '@/lang';
import FileRole from '@/components/file-role/index.vue';
import { MsgSuccess } from '@/utils/message';
import { FormRules } from 'element-plus';
import { Rules } from '@/global/form-rules';
interface BatchRoleProps {
files: File.File[];
}
const open = ref(false);
const loading = ref(false);
const mode = ref('0755');
const files = ref<File.File[]>([]);
const rules = reactive<FormRules>({
user: [Rules.requiredInput],
group: [Rules.requiredInput],
});
const em = defineEmits(['close']);
const handleClose = () => {
open.value = false;
em('close', false);
};
const addForm = reactive({
paths: [],
mode: 755,
user: '',
group: '',
sub: false,
});
const acceptParams = (props: BatchRoleProps) => {
files.value = props.files;
files.value.forEach((file) => {
addForm.paths.push(file.path);
});
addForm.mode = Number.parseInt(String(props.files[0].mode), 8);
addForm.group = props.files[0].group;
addForm.user = props.files[0].user;
addForm.sub = true;
mode.value = String(props.files[0].mode);
open.value = true;
};
const getMode = (val: number) => {
addForm.mode = val;
};
const submit = async () => {
loading.value = true;
BatchChangeRole(addForm)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
};
defineExpose({ acceptParams });
</script>

@ -80,6 +80,9 @@
<el-button plain @click="openCompress(selects)" :disabled="selects.length === 0">
{{ $t('file.compress') }}
</el-button>
<el-button plain @click="openBatchRole(selects)" :disabled="selects.length === 0">
{{ $t('file.role') }}
</el-button>
<el-button plain @click="batchDelFiles" :disabled="selects.length === 0">
{{ $t('commons.button.delete') }}
</el-button>
@ -291,6 +294,7 @@
<DeleteFile ref="deleteRef" @close="search" />
<RecycleBin ref="recycleBinRef" @close="search" />
<Favorite ref="favoriteRef" @close="search" />
<BatchRole ref="batchRoleRef" @close="search" />
</LayoutContent>
</div>
</template>
@ -333,6 +337,7 @@ import Process from './process/index.vue';
import Detail from './detail/index.vue';
import RecycleBin from './recycle-bin/index.vue';
import Favorite from './favorite/index.vue';
import BatchRole from './batch-role/index.vue';
const globalStore = GlobalStore();
@ -394,6 +399,7 @@ const recycleBinRef = ref();
const favoriteRef = ref();
const hoveredRowIndex = ref(-1);
const favorites = ref([]);
const batchRoleRef = ref();
// editablePath
const { searchableStatus, searchablePath, searchableInputRef, searchableInputBlur } = useSearchable(paths);
@ -655,6 +661,10 @@ const openWget = () => {
wgetRef.value.acceptParams(fileWget);
};
const openBatchRole = (items: File.File[]) => {
batchRoleRef.value.acceptParams({ files: items });
};
const closeWget = (submit: Boolean) => {
search();
if (submit) {

Loading…
Cancel
Save