121 lines
3.2 KiB
Go
121 lines
3.2 KiB
Go
package workspace
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"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)
|
|
Get(id string) (*Workspace, error)
|
|
Delete(id string) error
|
|
List() ([]Workspace, error)
|
|
}
|
|
|
|
// LocalManager implements Manager using the local filesystem.
|
|
type LocalManager struct {
|
|
root string
|
|
}
|
|
|
|
// NewLocalManager creates a LocalManager rooted at root. The root is made
|
|
// absolute and cleaned if possible.
|
|
func NewLocalManager(root string) *LocalManager {
|
|
abs, err := filepath.Abs(root)
|
|
if err != nil {
|
|
abs = filepath.Clean(root)
|
|
}
|
|
return &LocalManager{root: abs}
|
|
}
|
|
|
|
// Create creates a workspace directory and returns the workspace.
|
|
func (m *LocalManager) Create(id string) (*Workspace, error) {
|
|
if !ValidID(id) {
|
|
return nil, util.New(util.CodeBadRequest, "invalid workspace id")
|
|
}
|
|
root := RootFor(m.root, id)
|
|
if err := os.MkdirAll(root, 0o755); err != nil {
|
|
return nil, util.Wrap(util.CodeInternal, "failed to create workspace", err)
|
|
}
|
|
return &Workspace{ID: id, Root: root}, nil
|
|
}
|
|
|
|
// Get returns the workspace if its directory exists.
|
|
func (m *LocalManager) Get(id string) (*Workspace, error) {
|
|
if !ValidID(id) {
|
|
return nil, util.New(util.CodeBadRequest, "invalid workspace id")
|
|
}
|
|
root := RootFor(m.root, id)
|
|
info, err := os.Stat(root)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, util.New(util.CodeNotFound, "workspace not found")
|
|
}
|
|
return nil, util.Wrap(util.CodeInternal, "failed to stat workspace", err)
|
|
}
|
|
if !info.IsDir() {
|
|
return nil, util.New(util.CodeNotFound, "workspace not found")
|
|
}
|
|
return &Workspace{ID: id, Root: root}, nil
|
|
}
|
|
|
|
// Delete removes the workspace directory recursively.
|
|
func (m *LocalManager) Delete(id string) error {
|
|
if !ValidID(id) {
|
|
return util.New(util.CodeBadRequest, "invalid workspace id")
|
|
}
|
|
root := RootFor(m.root, id)
|
|
if err := forceRemove(root); err != nil {
|
|
return util.Wrap(util.CodeInternal, "failed to delete workspace", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// List returns all workspaces under the manager root, sorted by ID.
|
|
// Only directories whose names pass ValidID are included.
|
|
func (m *LocalManager) List() ([]Workspace, error) {
|
|
if err := os.MkdirAll(m.root, 0o755); err != nil {
|
|
return nil, util.Wrap(util.CodeInternal, "failed to ensure workspace root", err)
|
|
}
|
|
entries, err := os.ReadDir(m.root)
|
|
if err != nil {
|
|
return nil, util.Wrap(util.CodeInternal, "failed to read workspace root", err)
|
|
}
|
|
var result []Workspace
|
|
for _, e := range entries {
|
|
if !e.IsDir() {
|
|
continue
|
|
}
|
|
if !ValidID(e.Name()) {
|
|
continue
|
|
}
|
|
result = append(result, Workspace{ID: e.Name(), Root: RootFor(m.root, e.Name())})
|
|
}
|
|
sort.Slice(result, func(i, j int) bool {
|
|
return result[i].ID < result[j].ID
|
|
})
|
|
return result, nil
|
|
}
|