package workspace import ( "os" "path/filepath" "sort" "codespace/internal/util" ) // 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 := os.RemoveAll(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 }