From a36c9344af3334c78769a143cda990dfbec61eb2 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:42:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=93=8D=E4=BD=9C=20immutable=20?= =?UTF-8?q?=E6=A0=87=E8=AF=86=E7=9A=84=E6=96=87=E4=BB=B6=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E6=8F=90=E9=86=92=20(#1203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * feat: 添加 immutable 字段到文件列表返回结果中 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: 前端添加 immutable 文件提醒弹窗功能 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * refactor: 移除未使用的 renameModel.immutable 字段 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: lint --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> Co-authored-by: 耗子 --- internal/service/file.go | 39 +++++++++++++-------- web/src/views/file/ListTable.vue | 58 ++++++++++++++++++++++++++------ 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/internal/service/file.go b/internal/service/file.go index 341d2a47..5ffd52c6 100644 --- a/internal/service/file.go +++ b/internal/service/file.go @@ -23,6 +23,7 @@ import ( "github.com/acepanel/panel/internal/app" "github.com/acepanel/panel/internal/biz" "github.com/acepanel/panel/internal/http/request" + "github.com/acepanel/panel/pkg/chattr" "github.com/acepanel/panel/pkg/io" "github.com/acepanel/panel/pkg/os" "github.com/acepanel/panel/pkg/shell" @@ -495,21 +496,31 @@ func (s *FileService) formatDir(base string, entries []stdos.DirEntry) []any { if !info.IsDir() { size = tools.FormatBytes(float64(info.Size())) } + + // 检查是否有 immutable 属性 + fullPath := filepath.Join(base, info.Name()) + immutable := false + if f, err := stdos.OpenFile(fullPath, stdos.O_RDONLY, 0); err == nil { + immutable, _ = chattr.IsAttr(f, chattr.FS_IMMUTABLE_FL) + _ = f.Close() + } + paths = append(paths, map[string]any{ - "name": info.Name(), - "full": filepath.Join(base, info.Name()), - "size": size, - "mode_str": info.Mode().String(), - "mode": fmt.Sprintf("%04o", info.Mode().Perm()), - "owner": os.GetUser(stat.Uid), - "group": os.GetGroup(stat.Gid), - "uid": stat.Uid, - "gid": stat.Gid, - "hidden": io.IsHidden(info.Name()), - "symlink": io.IsSymlink(info.Mode()), - "link": io.GetSymlink(filepath.Join(base, info.Name())), - "dir": info.IsDir(), - "modify": info.ModTime().Format(time.DateTime), + "name": info.Name(), + "full": fullPath, + "size": size, + "mode_str": info.Mode().String(), + "mode": fmt.Sprintf("%04o", info.Mode().Perm()), + "owner": os.GetUser(stat.Uid), + "group": os.GetGroup(stat.Gid), + "uid": stat.Uid, + "gid": stat.Gid, + "hidden": io.IsHidden(info.Name()), + "symlink": io.IsSymlink(info.Mode()), + "link": io.GetSymlink(fullPath), + "dir": info.IsDir(), + "modify": info.ModTime().Format(time.DateTime), + "immutable": immutable, }) } diff --git a/web/src/views/file/ListTable.vue b/web/src/views/file/ListTable.vue index cdc04c6f..e390defe 100644 --- a/web/src/views/file/ListTable.vue +++ b/web/src/views/file/ListTable.vue @@ -66,6 +66,24 @@ const unCompressModel = ref({ file: '' }) +// 检查是否有 immutable 属性,如果有则弹出确认对话框 +const confirmImmutableOperation = (row: any, operation: string, callback: () => void) => { + if (row.immutable) { + window.$dialog.warning({ + title: $gettext('Warning'), + content: $gettext( + '%{ name } has immutable attribute. The panel will temporarily remove the immutable attribute, perform the operation, and then restore the immutable attribute. Do you want to continue?', + { name: row.name } + ), + positiveText: $gettext('Continue'), + negativeText: $gettext('Cancel'), + onPositiveClick: callback + }) + } else { + callback() + } +} + const options = computed(() => { if (selectedRow.value == null) return [] const options = [ @@ -145,7 +163,15 @@ const columns: DataTableColumns = [ return row.name } } - }) + }), + // 如果文件有 immutable 属性,显示锁定图标 + row.immutable + ? h(TheIcon, { + icon: 'mdi:lock', + size: 16, + style: { color: '#f0a020', marginLeft: '4px' } + }) + : null ] ) } @@ -297,9 +323,11 @@ const columns: DataTableColumns = [ size: 'small', tertiary: true, onClick: () => { - renameModel.value.source = getFilename(row.name) - renameModel.value.target = getFilename(row.name) - renameModal.value = true + confirmImmutableOperation(row, 'rename', () => { + renameModel.value.source = getFilename(row.name) + renameModel.value.target = getFilename(row.name) + renameModal.value = true + }) } }, { default: () => $gettext('Rename') } @@ -317,6 +345,12 @@ const columns: DataTableColumns = [ }, { default: () => { + if (row.immutable) { + return $gettext( + 'The file %{ name } has immutable attribute. The system will temporarily remove the immutable attribute and delete the file. Do you want to continue?', + { name: row.name } + ) + } return $gettext('Are you sure you want to delete %{ name }?', { name: row.name }) }, trigger: () => { @@ -659,14 +693,18 @@ const handleSelect = (key: string) => { unCompressModal.value = true break case 'rename': - renameModel.value.source = getFilename(selectedRow.value.name) - renameModel.value.target = getFilename(selectedRow.value.name) - renameModal.value = true + confirmImmutableOperation(selectedRow.value, 'rename', () => { + renameModel.value.source = getFilename(selectedRow.value.name) + renameModel.value.target = getFilename(selectedRow.value.name) + renameModal.value = true + }) break case 'delete': - useRequest(file.delete(selectedRow.value.full)).onSuccess(() => { - window.$bus.emit('file:refresh') - window.$message.success($gettext('Deleted successfully')) + confirmImmutableOperation(selectedRow.value, 'delete', () => { + useRequest(file.delete(selectedRow.value.full)).onSuccess(() => { + window.$bus.emit('file:refresh') + window.$message.success($gettext('Deleted successfully')) + }) }) break }