go-io/workspace/service.go
Virgil 48b777675e
Some checks failed
CI / test (push) Failing after 2s
CI / auto-fix (push) Failing after 0s
CI / auto-merge (push) Failing after 0s
refactor(workspace): fail unsupported workspace messages explicitly
Return explicit fs sentinels for workspace creation, switching, and inactive file access.\n\nUnsupported command and message inputs now return a failed core.Result instead of a silent success, and tests cover the fallback path.\n\nCo-Authored-By: Virgil <virgil@lethean.io>
2026-03-31 07:17:59 +00:00

234 lines
7.8 KiB
Go

package workspace
import (
"crypto/sha256"
"encoding/hex"
"io/fs"
"sync"
core "dappco.re/go/core"
"dappco.re/go/core/io"
)
// Example: service, _ := workspace.New(workspace.Options{KeyPairProvider: keyPairProvider})
type Workspace interface {
CreateWorkspace(identifier, password string) (string, error)
SwitchWorkspace(workspaceID string) error
ReadWorkspaceFile(workspaceFilePath string) (string, error)
WriteWorkspaceFile(workspaceFilePath, content string) error
}
// Example: key, _ := keyPairProvider.CreateKeyPair("alice", "pass123")
type KeyPairProvider interface {
CreateKeyPair(name, passphrase string) (string, error)
}
const (
WorkspaceCreateAction = "workspace.create"
WorkspaceSwitchAction = "workspace.switch"
)
// Example: command := WorkspaceCommand{Action: WorkspaceCreateAction, Identifier: "alice", Password: "pass123"}
type WorkspaceCommand struct {
Action string
Identifier string
Password string
WorkspaceID string
}
// Example: service, _ := workspace.New(workspace.Options{KeyPairProvider: keyPairProvider})
type Options struct {
KeyPairProvider KeyPairProvider
}
// Example: service, _ := workspace.New(workspace.Options{KeyPairProvider: keyPairProvider})
type Service struct {
keyPairProvider KeyPairProvider
activeWorkspaceID string
rootPath string
medium io.Medium
stateLock sync.RWMutex
}
var _ Workspace = (*Service)(nil)
// Example: service, _ := workspace.New(workspace.Options{KeyPairProvider: keyPairProvider})
// Example: workspaceID, _ := service.CreateWorkspace("alice", "pass123")
func New(options Options) (*Service, error) {
home := resolveWorkspaceHomeDirectory()
if home == "" {
return nil, core.E("workspace.New", "failed to determine home directory", fs.ErrNotExist)
}
rootPath := core.Path(home, ".core", "workspaces")
if options.KeyPairProvider == nil {
return nil, core.E("workspace.New", "key pair provider is required", fs.ErrInvalid)
}
service := &Service{
keyPairProvider: options.KeyPairProvider,
rootPath: rootPath,
medium: io.Local,
}
if err := service.medium.EnsureDir(rootPath); err != nil {
return nil, core.E("workspace.New", "failed to ensure root directory", err)
}
return service, nil
}
// Example: workspaceID, _ := service.CreateWorkspace("alice", "pass123")
func (service *Service) CreateWorkspace(identifier, password string) (string, error) {
service.stateLock.Lock()
defer service.stateLock.Unlock()
if service.keyPairProvider == nil {
return "", core.E("workspace.CreateWorkspace", "key pair provider not available", fs.ErrInvalid)
}
hash := sha256.Sum256([]byte(identifier))
workspaceID := hex.EncodeToString(hash[:])
workspaceDirectory, err := service.resolveWorkspaceDirectory("workspace.CreateWorkspace", workspaceID)
if err != nil {
return "", err
}
if service.medium.Exists(workspaceDirectory) {
return "", core.E("workspace.CreateWorkspace", "workspace already exists", fs.ErrExist)
}
for _, directoryName := range []string{"config", "log", "data", "files", "keys"} {
if err := service.medium.EnsureDir(core.Path(workspaceDirectory, directoryName)); err != nil {
return "", core.E("workspace.CreateWorkspace", core.Concat("failed to create directory: ", directoryName), err)
}
}
privKey, err := service.keyPairProvider.CreateKeyPair(identifier, password)
if err != nil {
return "", core.E("workspace.CreateWorkspace", "failed to generate keys", err)
}
if err := service.medium.WriteMode(core.Path(workspaceDirectory, "keys", "private.key"), privKey, 0600); err != nil {
return "", core.E("workspace.CreateWorkspace", "failed to save private key", err)
}
return workspaceID, nil
}
// Example: _ = service.SwitchWorkspace(workspaceID)
func (service *Service) SwitchWorkspace(workspaceID string) error {
service.stateLock.Lock()
defer service.stateLock.Unlock()
workspaceDirectory, err := service.resolveWorkspaceDirectory("workspace.SwitchWorkspace", workspaceID)
if err != nil {
return err
}
if !service.medium.IsDir(workspaceDirectory) {
return core.E("workspace.SwitchWorkspace", core.Concat("workspace not found: ", workspaceID), fs.ErrNotExist)
}
service.activeWorkspaceID = core.PathBase(workspaceDirectory)
return nil
}
func (service *Service) resolveActiveWorkspaceFilePath(operation, workspaceFilePath string) (string, error) {
if service.activeWorkspaceID == "" {
return "", core.E(operation, "no active workspace", fs.ErrNotExist)
}
filesRoot := core.Path(service.rootPath, service.activeWorkspaceID, "files")
filePath, err := joinPathWithinRoot(filesRoot, workspaceFilePath)
if err != nil {
return "", core.E(operation, "file path escapes workspace files", fs.ErrPermission)
}
if filePath == filesRoot {
return "", core.E(operation, "workspace file path is required", fs.ErrInvalid)
}
return filePath, nil
}
// Example: content, _ := service.ReadWorkspaceFile("notes/todo.txt")
func (service *Service) ReadWorkspaceFile(workspaceFilePath string) (string, error) {
service.stateLock.RLock()
defer service.stateLock.RUnlock()
filePath, err := service.resolveActiveWorkspaceFilePath("workspace.ReadWorkspaceFile", workspaceFilePath)
if err != nil {
return "", err
}
return service.medium.Read(filePath)
}
// Example: _ = service.WriteWorkspaceFile("notes/todo.txt", "ship it")
func (service *Service) WriteWorkspaceFile(workspaceFilePath, content string) error {
service.stateLock.Lock()
defer service.stateLock.Unlock()
filePath, err := service.resolveActiveWorkspaceFilePath("workspace.WriteWorkspaceFile", workspaceFilePath)
if err != nil {
return err
}
return service.medium.Write(filePath, content)
}
// Example: result := service.HandleWorkspaceCommand(WorkspaceCommand{Action: WorkspaceCreateAction, Identifier: "alice", Password: "pass123"})
func (service *Service) HandleWorkspaceCommand(command WorkspaceCommand) core.Result {
switch command.Action {
case WorkspaceCreateAction:
workspaceID, err := service.CreateWorkspace(command.Identifier, command.Password)
if err != nil {
return core.Result{}.New(err)
}
return core.Result{Value: workspaceID, OK: true}
case WorkspaceSwitchAction:
if err := service.SwitchWorkspace(command.WorkspaceID); err != nil {
return core.Result{}.New(err)
}
return core.Result{OK: true}
}
return core.Result{}.New(core.E("workspace.HandleWorkspaceCommand", core.Concat("unsupported action: ", command.Action), fs.ErrInvalid))
}
// Example: result := service.HandleWorkspaceMessage(core.New(), WorkspaceCommand{Action: WorkspaceSwitchAction, WorkspaceID: "f3f0d7"})
func (service *Service) HandleWorkspaceMessage(_ *core.Core, message core.Message) core.Result {
switch command := message.(type) {
case WorkspaceCommand:
return service.HandleWorkspaceCommand(command)
}
return core.Result{}.New(core.E("workspace.HandleWorkspaceMessage", "unsupported message type", fs.ErrInvalid))
}
func resolveWorkspaceHomeDirectory() string {
if home := core.Env("CORE_HOME"); home != "" {
return home
}
if home := core.Env("HOME"); home != "" {
return home
}
return core.Env("DIR_HOME")
}
func joinPathWithinRoot(root string, parts ...string) (string, error) {
candidate := core.Path(append([]string{root}, parts...)...)
sep := core.Env("DS")
if candidate == root || core.HasPrefix(candidate, root+sep) {
return candidate, nil
}
return "", fs.ErrPermission
}
func (service *Service) resolveWorkspaceDirectory(operation, workspaceID string) (string, error) {
if workspaceID == "" {
return "", core.E(operation, "workspace id is required", fs.ErrInvalid)
}
workspaceDirectory, err := joinPathWithinRoot(service.rootPath, workspaceID)
if err != nil {
return "", core.E(operation, "workspace path escapes root", err)
}
if core.PathDir(workspaceDirectory) != service.rootPath {
return "", core.E(operation, core.Concat("invalid workspace id: ", workspaceID), fs.ErrPermission)
}
return workspaceDirectory, nil
}