From 61e5ede650af9aedfba013c34971de1698e5a1ad Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:50:37 +0800 Subject: [PATCH] fix(workspace): tolerate read-only files on delete Co-Authored-By: Claude --- internal/workspace/manager.go | 21 ++++++++++++++++++++- internal/workspace/manager_test.go | 21 +++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/internal/workspace/manager.go b/internal/workspace/manager.go index f91785f..c648591 100644 --- a/internal/workspace/manager.go +++ b/internal/workspace/manager.go @@ -8,6 +8,25 @@ import ( "codespace/internal/util" ) + +// forceRemove chmods every file and directory under root to writable before +// removing the tree. It ignores chmod and walk errors so that partial or +// concurrent removals do not abort the delete. +func forceRemove(root string) error { + _ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { + return nil + } + mode := os.FileMode(0o644) + if d.IsDir() { + mode = 0o755 + } + _ = os.Chmod(path, mode) + return nil + }) + return os.RemoveAll(root) +} + // Manager manages workspace lifecycle. type Manager interface { Create(id string) (*Workspace, error) @@ -68,7 +87,7 @@ func (m *LocalManager) Delete(id string) error { return util.New(util.CodeBadRequest, "invalid workspace id") } root := RootFor(m.root, id) - if err := os.RemoveAll(root); err != nil { + if err := forceRemove(root); err != nil { return util.Wrap(util.CodeInternal, "failed to delete workspace", err) } return nil diff --git a/internal/workspace/manager_test.go b/internal/workspace/manager_test.go index 48765bd..d93611a 100644 --- a/internal/workspace/manager_test.go +++ b/internal/workspace/manager_test.go @@ -6,6 +6,27 @@ import ( "testing" ) +func TestLocalManagerDeleteRemovesReadOnlyFiles(t *testing.T) { + mgr := NewLocalManager(t.TempDir()) + ws, err := mgr.Create("readonly") + if err != nil { + t.Fatal(err) + } + + readonlyFile := filepath.Join(ws.Root, "readonly.txt") + if err := os.WriteFile(readonlyFile, []byte("x"), 0o444); err != nil { + t.Fatal(err) + } + + if err := mgr.Delete("readonly"); err != nil { + t.Fatalf("Delete: %v", err) + } + + if _, err := os.Stat(ws.Root); !os.IsNotExist(err) { + t.Fatalf("workspace root still exists: %v", err) + } +} + func TestLocalManagerListReturnsSortedWorkspaces(t *testing.T) { mgr := NewLocalManager(t.TempDir()) if _, err := mgr.Create("beta"); err != nil {