fix(workspace): tolerate read-only files on delete
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,25 @@ import (
|
|||||||
"codespace/internal/util"
|
"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.
|
// Manager manages workspace lifecycle.
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
Create(id string) (*Workspace, error)
|
Create(id string) (*Workspace, error)
|
||||||
@@ -68,7 +87,7 @@ func (m *LocalManager) Delete(id string) error {
|
|||||||
return util.New(util.CodeBadRequest, "invalid workspace id")
|
return util.New(util.CodeBadRequest, "invalid workspace id")
|
||||||
}
|
}
|
||||||
root := RootFor(m.root, 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 util.Wrap(util.CodeInternal, "failed to delete workspace", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -6,6 +6,27 @@ import (
|
|||||||
"testing"
|
"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) {
|
func TestLocalManagerListReturnsSortedWorkspaces(t *testing.T) {
|
||||||
mgr := NewLocalManager(t.TempDir())
|
mgr := NewLocalManager(t.TempDir())
|
||||||
if _, err := mgr.Create("beta"); err != nil {
|
if _, err := mgr.Create("beta"); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user