mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 06:47:20 +08:00
feat: 操作 immutable 标识的文件进行提醒 (#1203)
* 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: 耗子 <haozi@loli.email>
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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<DropdownOption[]>(() => {
|
||||
if (selectedRow.value == null) return []
|
||||
const options = [
|
||||
@@ -145,7 +163,15 @@ const columns: DataTableColumns<RowData> = [
|
||||
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<RowData> = [
|
||||
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<RowData> = [
|
||||
},
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user