qvm/internal/workspace/registry.go

180 lines
4.3 KiB
Go

package workspace
import (
"crypto/sha256"
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/samber/mo"
)
type Workspace struct {
HostPath string `json:"host_path"`
Hash string `json:"hash"`
MountTag string `json:"mount_tag"`
GuestPath string `json:"guest_path"`
}
type Registry struct {
filePath string
workspaces map[string]Workspace
}
// Hash generates an 8-character hash from a path, matching bash behavior:
// echo -n "$path" | sha256sum | cut -c1-8
func Hash(path string) string {
h := sha256.Sum256([]byte(path))
return fmt.Sprintf("%x", h)[:8]
}
// NewRegistry creates a new empty registry
func NewRegistry(filePath string) *Registry {
return &Registry{
filePath: filePath,
workspaces: make(map[string]Workspace),
}
}
// Load reads the registry from a JSON file
func Load(filePath string) mo.Result[*Registry] {
registry := NewRegistry(filePath)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return mo.Ok(registry)
}
data, err := os.ReadFile(filePath)
if err != nil {
return mo.Err[*Registry](fmt.Errorf("failed to read workspaces file: %w", err))
}
if len(data) == 0 {
return mo.Ok(registry)
}
var workspaceList []Workspace
if err := json.Unmarshal(data, &workspaceList); err != nil {
return mo.Err[*Registry](fmt.Errorf("failed to parse workspaces JSON: %w", err))
}
for _, ws := range workspaceList {
registry.workspaces[ws.HostPath] = ws
}
return mo.Ok(registry)
}
// Save writes the registry to the JSON file
func (r *Registry) Save() mo.Result[struct{}] {
dir := filepath.Dir(r.filePath)
if err := os.MkdirAll(dir, 0755); err != nil {
return mo.Err[struct{}](fmt.Errorf("failed to create directory: %w", err))
}
workspaceList := make([]Workspace, 0, len(r.workspaces))
for _, ws := range r.workspaces {
workspaceList = append(workspaceList, ws)
}
data, err := json.MarshalIndent(workspaceList, "", " ")
if err != nil {
return mo.Err[struct{}](fmt.Errorf("failed to marshal JSON: %w", err))
}
if err := os.WriteFile(r.filePath, data, 0644); err != nil {
return mo.Err[struct{}](fmt.Errorf("failed to write workspaces file: %w", err))
}
return mo.Ok(struct{}{})
}
// Register adds a workspace to the registry if it doesn't already exist
func (r *Registry) Register(hostPath string) mo.Result[*Workspace] {
absPath, err := filepath.Abs(hostPath)
if err != nil {
return mo.Err[*Workspace](fmt.Errorf("failed to resolve absolute path: %w", err))
}
if existing, exists := r.workspaces[absPath]; exists {
return mo.Ok(&existing)
}
hash := Hash(absPath)
dirName := filepath.Base(absPath)
ws := Workspace{
HostPath: absPath,
Hash: hash,
MountTag: fmt.Sprintf("ws_%s", hash),
GuestPath: fmt.Sprintf("/workspace/%s-%s", hash, dirName),
}
r.workspaces[absPath] = ws
return mo.Ok(&ws)
}
// List returns all registered workspaces
func (r *Registry) List() []Workspace {
result := make([]Workspace, 0, len(r.workspaces))
for _, ws := range r.workspaces {
result = append(result, ws)
}
return result
}
// Find looks up a workspace by host path
func (r *Registry) Find(hostPath string) mo.Option[Workspace] {
absPath, err := filepath.Abs(hostPath)
if err != nil {
if ws, exists := r.workspaces[hostPath]; exists {
return mo.Some(ws)
}
return mo.None[Workspace]()
}
if ws, exists := r.workspaces[absPath]; exists {
return mo.Some(ws)
}
return mo.None[Workspace]()
}
// FindByHash looks up a workspace by its hash (or hash prefix)
func (r *Registry) FindByHash(hash string) mo.Option[Workspace] {
for _, ws := range r.workspaces {
if ws.Hash == hash {
return mo.Some(ws)
}
// Also support prefix matching for convenience
if len(hash) >= 3 && len(hash) < 8 && ws.Hash[:len(hash)] == hash {
return mo.Some(ws)
}
}
return mo.None[Workspace]()
}
// Unregister removes a workspace from the registry by host path
func (r *Registry) Unregister(hostPath string) bool {
absPath, err := filepath.Abs(hostPath)
if err != nil {
absPath = hostPath
}
if _, exists := r.workspaces[absPath]; exists {
delete(r.workspaces, absPath)
return true
}
return false
}
// UnregisterByHash removes a workspace from the registry by its hash
func (r *Registry) UnregisterByHash(hash string) mo.Option[Workspace] {
wsOpt := r.FindByHash(hash)
if wsOpt.IsAbsent() {
return mo.None[Workspace]()
}
ws := wsOpt.MustGet()
delete(r.workspaces, ws.HostPath)
return mo.Some(ws)
}