mirror of https://github.com/1Panel-dev/1Panel
parent
eebef06c77
commit
51cbd7bf9c
|
@ -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…
Reference in New Issue