mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
fix(storage): a move plan proves the nearest existing parent writable instead of creating the target
Planning ran on every keystroke and its write probe called MkdirAll on the target, leaving a folder per typed prefix. The probe now runs in the nearest existing ancestor, free space is read from it, and Move creates the target once the user starts the move.
This commit is contained in:
1 parent
7c388628f2
commit
1923e50a34
4 files changed
+108
-13
No files matched your search
@@ -89,6 +89,9 @@ func Move(ctx context.Context, plan Plan, report func(Progress)) error {
|
||||
if err := writeJournal(journal); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(plan.To, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
owned := presentEntries(plan.From, movedEntries(plan.Root))
|
||||
copied, err := copyOwned(ctx, owned, plan, notify)
|
||||
|
||||
@@ -3,7 +3,9 @@ package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -110,7 +112,8 @@ func PlanMove(ctx context.Context, root config.RootID, target string) Plan {
|
||||
plan.Bytes, plan.Files, _, _, _ = measureRoot(ctx, plan.Root, plan.From, 0)
|
||||
plan.Need = plan.Bytes + headroom
|
||||
|
||||
if err := probeWritable(plan.To); err != nil {
|
||||
probed, err := probeWritable(plan.To)
|
||||
if err != nil {
|
||||
plan.Refusals = append(plan.Refusals, Refusal{
|
||||
Code: "target.unwritable", Detail: "that folder cannot be written to: " + err.Error(),
|
||||
})
|
||||
@@ -129,7 +132,7 @@ func PlanMove(ctx context.Context, root config.RootID, target string) Plan {
|
||||
plan.Refusals = append(plan.Refusals, Refusal{Code: "target.not_empty", Detail: notEmptyDetail(plan.Root, entries)})
|
||||
}
|
||||
}
|
||||
vol := readVolume(plan.To)
|
||||
vol := readVolume(probed)
|
||||
plan.Free = vol.Free
|
||||
if vol.Free > 0 && vol.Free < plan.Need {
|
||||
plan.Refusals = append(plan.Refusals, Refusal{
|
||||
@@ -160,24 +163,43 @@ func resumes(plan Plan) bool {
|
||||
return ok && pending.Root == plan.Root && samePath(pending.To, plan.To)
|
||||
}
|
||||
|
||||
// probeWritable creates the target if it is missing and proves a file can be
|
||||
// written in it. Checking the mode bits instead would pass on a read-only
|
||||
// network share and fail halfway through the copy.
|
||||
func probeWritable(dir string) error {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
probe, err := os.CreateTemp(dir, ".reasonix-write-probe-*")
|
||||
func probeWritable(dir string) (string, error) {
|
||||
existing, err := existingAncestor(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
probe, err := os.CreateTemp(existing, ".reasonix-write-probe-*")
|
||||
if err != nil {
|
||||
return existing, err
|
||||
}
|
||||
name := probe.Name()
|
||||
closeErr := probe.Close()
|
||||
rmErr := os.Remove(name)
|
||||
if closeErr != nil {
|
||||
return closeErr
|
||||
return existing, closeErr
|
||||
}
|
||||
return existing, rmErr
|
||||
}
|
||||
|
||||
func existingAncestor(dir string) (string, error) {
|
||||
at := dir
|
||||
for {
|
||||
info, err := os.Stat(at)
|
||||
if err == nil {
|
||||
if !info.IsDir() {
|
||||
return "", fmt.Errorf("%s is a file, not a folder", at)
|
||||
}
|
||||
return at, nil
|
||||
}
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return "", err
|
||||
}
|
||||
parent := filepath.Dir(at)
|
||||
if parent == at {
|
||||
return "", err
|
||||
}
|
||||
at = parent
|
||||
}
|
||||
return rmErr
|
||||
}
|
||||
|
||||
// within reports whether path sits inside base, comparing cleaned absolute
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/config"
|
||||
)
|
||||
|
||||
func TestPlanCreatesNothingForATargetStillBeingTyped(t *testing.T) {
|
||||
home := stateHome(t)
|
||||
seedState(t, home)
|
||||
base := testenv.TempDir(t)
|
||||
target := filepath.Join(base, "deep", "er", "moved")
|
||||
|
||||
plan := PlanMove(t.Context(), config.RootState, target)
|
||||
|
||||
if !plan.OK() {
|
||||
t.Fatalf("a writable parent was refused: %+v", plan.Refusals)
|
||||
}
|
||||
if entries, err := os.ReadDir(base); err != nil || len(entries) != 0 {
|
||||
t.Fatalf("planning left %d entries under %s (err %v), want none", len(entries), base, err)
|
||||
}
|
||||
if plan.Free <= 0 {
|
||||
t.Fatalf("free space for a missing target was not read from its nearest existing folder: %d", plan.Free)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanRefusesATargetWhoseNearestFolderIsAFile(t *testing.T) {
|
||||
home := stateHome(t)
|
||||
seedState(t, home)
|
||||
base := testenv.TempDir(t)
|
||||
file := filepath.Join(base, "plain")
|
||||
write(t, file, 16)
|
||||
|
||||
plan := PlanMove(t.Context(), config.RootState, filepath.Join(file, "moved"))
|
||||
|
||||
if plan.OK() {
|
||||
t.Fatal("a target under a regular file was accepted")
|
||||
}
|
||||
if entries, _ := os.ReadDir(base); len(entries) != 1 {
|
||||
t.Fatalf("planning changed %s", base)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveCreatesTheTargetItself(t *testing.T) {
|
||||
home := stateHome(t)
|
||||
seedState(t, home)
|
||||
target := filepath.Join(testenv.TempDir(t), "a", "b", "moved")
|
||||
|
||||
plan := PlanMove(t.Context(), config.RootState, target)
|
||||
if err := Move(t.Context(), plan, nil); err != nil {
|
||||
t.Fatalf("move into a folder that did not exist: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(target, "sessions", "a.jsonl")); err != nil {
|
||||
t.Fatalf("copy missing from the created target: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
## 修复
|
||||
|
||||
- 修复在「设置 → 数据 → 存储」里更改位置时,每敲一个字符就在磁盘上留下一个半截目录的问题(比如输入 `D:\test\reasonix` 会留下 `r`、`re`、`rea` 等 8 个空目录)。现在预检只读:它在目标最近的已存在上级目录里验证可写、读取剩余空间,目录要等你点「开始迁移」后才创建(#14998)。
|
||||
|
||||
---
|
||||
<!-- lang:en -->
|
||||
|
||||
## Fixed
|
||||
|
||||
- Typing a new location under Settings > Data > Storage no longer leaves a half-typed folder on disk for every keystroke (typing `D:\test\reasonix` used to leave `r`, `re`, `rea` and so on as eight empty folders). The check is now read-only: it proves the nearest existing parent is writable and reads its free space, and the folder is only created when you start the move (#14998).
|
||||
Reference in new issue
Block a user