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"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-file-plus"
|
||||
:disabled="!p.selectedServer"
|
||||
@click="p.showNewFile = true"
|
||||
@click="p.openNewFileDialog()"
|
||||
>
|
||||
新建文件
|
||||
</v-btn>
|
||||
|
||||
@@ -26,6 +26,7 @@ export function useFilesActions(options: {
|
||||
previewFile: (item: FileEntry) => Promise<void>
|
||||
openInTerminal: () => void
|
||||
editorWorkbench: Ref<{ openFile: (p: OpenFilePayload) => void } | null>
|
||||
waitForEditorWorkbench?: () => Promise<{ openFile: (p: OpenFilePayload) => void } | null>
|
||||
actionLoading?: Ref<boolean>
|
||||
}) {
|
||||
const snackbar = useSnackbar()
|
||||
@@ -289,7 +290,11 @@ export function useFilesActions(options: {
|
||||
}
|
||||
|
||||
async function doNewFile() {
|
||||
if (!selectedServer.value || !newFileName.value) return
|
||||
if (!selectedServer.value) {
|
||||
snackbar('请先选择服务器', 'warning')
|
||||
return
|
||||
}
|
||||
if (!newFileName.value) return
|
||||
const check = validatePathSegment(newFileName.value)
|
||||
if (check !== true) {
|
||||
snackbar(check, 'warning')
|
||||
@@ -311,7 +316,13 @@ export function useFilesActions(options: {
|
||||
await options.browse({ force: true })
|
||||
snackbar('文件已创建')
|
||||
await nextTick()
|
||||
options.editorWorkbench.value?.openFile({
|
||||
const wb =
|
||||
(await options.waitForEditorWorkbench?.()) ?? options.editorWorkbench.value
|
||||
if (!wb?.openFile) {
|
||||
snackbar('文件已创建;编辑器未就绪,请从列表双击打开', 'warning')
|
||||
return
|
||||
}
|
||||
wb.openFile({
|
||||
path,
|
||||
name,
|
||||
content: '',
|
||||
@@ -541,6 +552,14 @@ export function useFilesActions(options: {
|
||||
showContextMenu.value = true
|
||||
}
|
||||
|
||||
function openNewFileDialog() {
|
||||
if (!selectedServer.value) {
|
||||
snackbar('请先选择服务器', 'warning')
|
||||
return
|
||||
}
|
||||
showNewFile.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
dropActive,
|
||||
showUpload,
|
||||
@@ -580,6 +599,7 @@ export function useFilesActions(options: {
|
||||
doCompress,
|
||||
doDecompress,
|
||||
doNewFile,
|
||||
openNewFileDialog,
|
||||
startRename,
|
||||
doRename,
|
||||
startChmod,
|
||||
|
||||
@@ -155,6 +155,7 @@ export function useFilesEditor(options: {
|
||||
previewContent,
|
||||
previewLoading,
|
||||
editorWorkbench,
|
||||
waitForEditorWorkbench,
|
||||
previewFile,
|
||||
editFile,
|
||||
onEditorSaved,
|
||||
|
||||
@@ -89,6 +89,7 @@ export function useFilesPage() {
|
||||
previewFile: editor.previewFile,
|
||||
openInTerminal: nav.openInTerminal,
|
||||
editorWorkbench: editor.editorWorkbench,
|
||||
waitForEditorWorkbench: editor.waitForEditorWorkbench,
|
||||
actionLoading,
|
||||
})
|
||||
|
||||
@@ -228,6 +229,7 @@ export function useFilesPage() {
|
||||
showNewFile: actions.showNewFile,
|
||||
newFileName: actions.newFileName,
|
||||
doNewFile: actions.doNewFile,
|
||||
openNewFileDialog: actions.openNewFileDialog,
|
||||
showMkdir: actions.showMkdir,
|
||||
mkdirName: actions.mkdirName,
|
||||
doMkdir: actions.doMkdir,
|
||||
|
||||
@@ -419,6 +419,9 @@ async def remote_write_bytes(
|
||||
|
||||
if not skip_sftp:
|
||||
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:
|
||||
await sftp_write_bytes(sftp, remote_path, data)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user