mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-10 15:36:45 +08:00
feat: 新增批量修改文件权限功能 (#2778)
Refs https://github.com/1Panel-dev/1Panel/issues/1330
This commit is contained in:
parent
eebef06c77
commit
51cbd7bf9c
10 changed files with 186 additions and 9 deletions
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
109
frontend/src/views/host/file-management/batch-role/index.vue
Normal file
109
frontend/src/views/host/file-management/batch-role/index.vue
Normal 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>
|
|
@ -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) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue