Fix files page new-file flow and SFTP parent directory creation.
Nexus CI/CD / test (push) Waiting to run
Nexus CI/CD / e2e (push) Blocked by required conditions
Nexus CI/CD / deploy (push) Blocked by required conditions
Nexus Pre-commit Checks / quick-check (push) Waiting to run

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:
Nexus Agent
2026-06-13 09:08:17 +08:00
parent e95e522d4d
commit 72042598b0
7 changed files with 89 additions and 4 deletions
@@ -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