feat: scaffold codespace backend

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
tao.chen
2026-07-02 15:07:54 +08:00
co-authored by Claude Fable 5
commit 411ed1f8ba
43 changed files with 4428 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
package workspace
import (
"os"
"path/filepath"
"codespace/internal/util"
)
// Manager manages workspace lifecycle.
type Manager interface {
Create(id string) (*Workspace, error)
Get(id string) (*Workspace, error)
Delete(id string) 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
}
+25
View File
@@ -0,0 +1,25 @@
package workspace
import (
"regexp"
"codespace/internal/util"
)
var idPattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
// ValidID reports whether id is a safe workspace identifier: only letters,
// digits, '-', '_', '.' and non-empty. The bare values "." and ".." are
// rejected because they would resolve to the parent or current directory.
func ValidID(id string) bool {
if id == "" || id == "." || id == ".." {
return false
}
return idPattern.MatchString(id)
}
// RootFor returns the absolute workspace root path for the given base root and
// workspace id. The result is filepath-cleaned.
func RootFor(baseRoot, id string) string {
return util.JoinClean(baseRoot, id)
}
+31
View File
@@ -0,0 +1,31 @@
package workspace
import (
"testing"
)
func TestValidIDAcceptsSafeSegments(t *testing.T) {
valid := []string{"user1", "project-A", "project_A", "project.1"}
for _, id := range valid {
if !ValidID(id) {
t.Errorf("expected ValidID(%q) to be true", id)
}
}
}
func TestValidIDRejectsUnsafeSegments(t *testing.T) {
invalid := []string{"", ".", "..", "/tmp/x", "../x", "a/b", "a b", "中文", "*"}
for _, id := range invalid {
if ValidID(id) {
t.Errorf("expected ValidID(%q) to be false", id)
}
}
}
func TestRootForUsesConfiguredRoot(t *testing.T) {
got := RootFor("/tmp/workspaces", "user1")
want := "/tmp/workspaces/user1"
if got != want {
t.Errorf("RootFor = %q, want %q", got, want)
}
}
+7
View File
@@ -0,0 +1,7 @@
package workspace
// Workspace represents a single workspace backed by a local directory.
type Workspace struct {
ID string `json:"id"`
Root string `json:"-"`
}