Fix files page new-file flow and SFTP parent directory creation.
Show a clear prompt when no server is selected, wait for the editor workbench before openFile, and mkdir -p parent paths before SFTP writes.
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
# 审计 — 文件管理「新建文件」修复
|
||||||
|
|
||||||
|
**Changelog**: `docs/changelog/2026-06-10-files-new-file-fix.md`
|
||||||
|
|
||||||
|
## 变更文件
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `frontend/src/composables/files/useFilesActions.ts` | `openNewFileDialog`、等待编辑器就绪 |
|
||||||
|
| `frontend/src/composables/files/useFilesEditor.ts` | 导出 `waitForEditorWorkbench` |
|
||||||
|
| `frontend/src/composables/files/useFilesPage.ts` | 透传 helper |
|
||||||
|
| `frontend/src/components/files/FilesToolbar.vue` | 未选服务器时提示 |
|
||||||
|
| `server/infrastructure/ssh/asyncssh_pool.py` | SFTP 写前确保父目录 |
|
||||||
|
|
||||||
|
## Step 3(规则扫描)
|
||||||
|
|
||||||
|
| 规则 | 结论 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 鉴权 | PASS | 仍走 `/sync/write-file` + admin JWT |
|
||||||
|
| 路径 | PASS | `validatePathSegment` 不变 |
|
||||||
|
| 静默失败 | PASS | 编辑器未就绪有 warning snackbar |
|
||||||
|
| SSH | PASS | `mkdir -p` 仅父目录,不扩大写范围 |
|
||||||
|
|
||||||
|
## Closure
|
||||||
|
|
||||||
|
| 文件 | 结论 |
|
||||||
|
|------|------|
|
||||||
|
| `useFilesActions.ts` | SAFE — 无 v-html;错误经 snackbar |
|
||||||
|
| `asyncssh_pool.py` | SAFE — 复用已有 `_ensure_remote_parent_dir` |
|
||||||
|
|
||||||
|
## DoD
|
||||||
|
|
||||||
|
- [x] changelog + 本审计
|
||||||
|
- [x] frontend type-check
|
||||||
|
- [ ] 生产:文件管理新建文件 → 编辑器打开
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 2026-06-10 — 文件管理「新建文件」修复
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
|
||||||
|
修复文件页「新建文件」点击无反馈或创建后编辑器不弹出的问题。
|
||||||
|
|
||||||
|
## 动机
|
||||||
|
|
||||||
|
- 未选服务器时按钮被禁用,用户以为功能坏了
|
||||||
|
- 创建成功后未等待 `FileEditorWorkbench` 就绪即 `openFile`,编辑器静默不打开
|
||||||
|
- SFTP 写入前未确保父目录存在(部分路径下 write-file 失败)
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
- `frontend/src/composables/files/useFilesActions.ts` — `openNewFileDialog`、`waitForEditorWorkbench`
|
||||||
|
- `frontend/src/composables/files/useFilesEditor.ts` — 导出 `waitForEditorWorkbench`
|
||||||
|
- `frontend/src/components/files/FilesToolbar.vue` — 未选服务器时提示而非禁用
|
||||||
|
- `server/infrastructure/ssh/asyncssh_pool.py` — SFTP 写前 `mkdir -p` 父目录
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend && npm run type-check
|
||||||
|
# 文件管理 → 选服务器 → 新建文件 → 应创建并打开编辑器
|
||||||
|
```
|
||||||
@@ -97,8 +97,7 @@ const p = useFilesPageContext()
|
|||||||
size="small"
|
size="small"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
prepend-icon="mdi-file-plus"
|
prepend-icon="mdi-file-plus"
|
||||||
:disabled="!p.selectedServer"
|
@click="p.openNewFileDialog()"
|
||||||
@click="p.showNewFile = true"
|
|
||||||
>
|
>
|
||||||
新建文件
|
新建文件
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function useFilesActions(options: {
|
|||||||
previewFile: (item: FileEntry) => Promise<void>
|
previewFile: (item: FileEntry) => Promise<void>
|
||||||
openInTerminal: () => void
|
openInTerminal: () => void
|
||||||
editorWorkbench: Ref<{ openFile: (p: OpenFilePayload) => void } | null>
|
editorWorkbench: Ref<{ openFile: (p: OpenFilePayload) => void } | null>
|
||||||
|
waitForEditorWorkbench?: () => Promise<{ openFile: (p: OpenFilePayload) => void } | null>
|
||||||
actionLoading?: Ref<boolean>
|
actionLoading?: Ref<boolean>
|
||||||
}) {
|
}) {
|
||||||
const snackbar = useSnackbar()
|
const snackbar = useSnackbar()
|
||||||
@@ -289,7 +290,11 @@ export function useFilesActions(options: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function doNewFile() {
|
async function doNewFile() {
|
||||||
if (!selectedServer.value || !newFileName.value) return
|
if (!selectedServer.value) {
|
||||||
|
snackbar('请先选择服务器', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!newFileName.value) return
|
||||||
const check = validatePathSegment(newFileName.value)
|
const check = validatePathSegment(newFileName.value)
|
||||||
if (check !== true) {
|
if (check !== true) {
|
||||||
snackbar(check, 'warning')
|
snackbar(check, 'warning')
|
||||||
@@ -311,7 +316,13 @@ export function useFilesActions(options: {
|
|||||||
await options.browse({ force: true })
|
await options.browse({ force: true })
|
||||||
snackbar('文件已创建')
|
snackbar('文件已创建')
|
||||||
await nextTick()
|
await nextTick()
|
||||||
options.editorWorkbench.value?.openFile({
|
const wb =
|
||||||
|
(await options.waitForEditorWorkbench?.()) ?? options.editorWorkbench.value
|
||||||
|
if (!wb?.openFile) {
|
||||||
|
snackbar('文件已创建;编辑器未就绪,请从列表双击打开', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wb.openFile({
|
||||||
path,
|
path,
|
||||||
name,
|
name,
|
||||||
content: '',
|
content: '',
|
||||||
@@ -541,6 +552,14 @@ export function useFilesActions(options: {
|
|||||||
showContextMenu.value = true
|
showContextMenu.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openNewFileDialog() {
|
||||||
|
if (!selectedServer.value) {
|
||||||
|
snackbar('请先选择服务器', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showNewFile.value = true
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dropActive,
|
dropActive,
|
||||||
showUpload,
|
showUpload,
|
||||||
@@ -580,6 +599,7 @@ export function useFilesActions(options: {
|
|||||||
doCompress,
|
doCompress,
|
||||||
doDecompress,
|
doDecompress,
|
||||||
doNewFile,
|
doNewFile,
|
||||||
|
openNewFileDialog,
|
||||||
startRename,
|
startRename,
|
||||||
doRename,
|
doRename,
|
||||||
startChmod,
|
startChmod,
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export function useFilesEditor(options: {
|
|||||||
previewContent,
|
previewContent,
|
||||||
previewLoading,
|
previewLoading,
|
||||||
editorWorkbench,
|
editorWorkbench,
|
||||||
|
waitForEditorWorkbench,
|
||||||
previewFile,
|
previewFile,
|
||||||
editFile,
|
editFile,
|
||||||
onEditorSaved,
|
onEditorSaved,
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export function useFilesPage() {
|
|||||||
previewFile: editor.previewFile,
|
previewFile: editor.previewFile,
|
||||||
openInTerminal: nav.openInTerminal,
|
openInTerminal: nav.openInTerminal,
|
||||||
editorWorkbench: editor.editorWorkbench,
|
editorWorkbench: editor.editorWorkbench,
|
||||||
|
waitForEditorWorkbench: editor.waitForEditorWorkbench,
|
||||||
actionLoading,
|
actionLoading,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -228,6 +229,7 @@ export function useFilesPage() {
|
|||||||
showNewFile: actions.showNewFile,
|
showNewFile: actions.showNewFile,
|
||||||
newFileName: actions.newFileName,
|
newFileName: actions.newFileName,
|
||||||
doNewFile: actions.doNewFile,
|
doNewFile: actions.doNewFile,
|
||||||
|
openNewFileDialog: actions.openNewFileDialog,
|
||||||
showMkdir: actions.showMkdir,
|
showMkdir: actions.showMkdir,
|
||||||
mkdirName: actions.mkdirName,
|
mkdirName: actions.mkdirName,
|
||||||
doMkdir: actions.doMkdir,
|
doMkdir: actions.doMkdir,
|
||||||
|
|||||||
@@ -419,6 +419,9 @@ async def remote_write_bytes(
|
|||||||
|
|
||||||
if not skip_sftp:
|
if not skip_sftp:
|
||||||
try:
|
try:
|
||||||
|
parent = posix_dirname(remote_path)
|
||||||
|
if parent and parent != "/":
|
||||||
|
await _ensure_remote_parent_dir(conn, remote_path, use_sudo=False)
|
||||||
async with await conn.start_sftp_client() as sftp:
|
async with await conn.start_sftp_client() as sftp:
|
||||||
await sftp_write_bytes(sftp, remote_path, data)
|
await sftp_write_bytes(sftp, remote_path, data)
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user