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

Refs https://github.com/1Panel-dev/1Panel/issues/1330
This commit is contained in:
zhengkunwang 2023-11-02 18:38:50 +08:00 committed by GitHub
parent eebef06c77
commit 51cbd7bf9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 186 additions and 9 deletions

View file

@ -707,3 +707,23 @@ func (b *BaseApi) ReadFileByLine(c *gin.Context) {
res.Content = strings.Join(lines, "\n") res.Content = strings.Join(lines, "\n")
helper.SuccessWithData(c, res) 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)
}

View file

@ -29,6 +29,14 @@ type FileCreate struct {
Sub bool `json:"sub"` 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 { type FileDelete struct {
Path string `json:"path" validate:"required"` Path string `json:"path" validate:"required"`
IsDir bool `json:"isDir"` IsDir bool `json:"isDir"`

View file

@ -29,7 +29,6 @@ type IFileService interface {
Create(op request.FileCreate) error Create(op request.FileCreate) error
Delete(op request.FileDelete) error Delete(op request.FileDelete) error
BatchDelete(op request.FileBatchDelete) error BatchDelete(op request.FileBatchDelete) error
ChangeMode(op request.FileCreate) error
Compress(c request.FileCompress) error Compress(c request.FileCompress) error
DeCompress(c request.FileDeCompress) error DeCompress(c request.FileDeCompress) error
GetContent(op request.FileContentReq) (response.FileInfo, error) GetContent(op request.FileContentReq) (response.FileInfo, error)
@ -40,6 +39,8 @@ type IFileService interface {
Wget(w request.FileWget) (string, error) Wget(w request.FileWget) (string, error)
MvFile(m request.FileMove) error MvFile(m request.FileMove) error
ChangeOwner(req request.FileRoleUpdate) error ChangeOwner(req request.FileRoleUpdate) error
ChangeMode(op request.FileCreate) error
BatchChangeModeAndOwner(op request.FileRoleReq) error
} }
func NewIFileService() IFileService { func NewIFileService() IFileService {
@ -166,11 +167,24 @@ func (f *FileService) BatchDelete(op request.FileBatchDelete) error {
func (f *FileService) ChangeMode(op request.FileCreate) error { func (f *FileService) ChangeMode(op request.FileCreate) error {
fo := files.NewFileOp() fo := files.NewFileOp()
if op.Sub { return fo.ChmodR(op.Path, op.Mode, op.Sub)
return fo.ChmodR(op.Path, op.Mode)
} else {
return fo.Chmod(op.Path, fs.FileMode(op.Mode))
} }
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 { func (f *FileService) ChangeOwner(req request.FileRoleUpdate) error {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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