crabbox/internal/cli/claim.go
2026-05-06 09:11:17 +01:00

158 lines
4.3 KiB
Go

package cli
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"time"
)
type leaseClaim struct {
LeaseID string `json:"leaseID"`
Slug string `json:"slug,omitempty"`
Provider string `json:"provider,omitempty"`
RepoRoot string `json:"repoRoot"`
ClaimedAt string `json:"claimedAt"`
LastUsedAt string `json:"lastUsedAt"`
IdleTimeoutSeconds int `json:"idleTimeoutSeconds,omitempty"`
}
func claimLeaseForRepo(leaseID, slug, repoRoot string, idleTimeout time.Duration, reclaim bool) error {
return claimLeaseForRepoProvider(leaseID, slug, "", repoRoot, idleTimeout, reclaim)
}
func claimLeaseForRepoConfig(leaseID, slug string, cfg Config, repoRoot string, idleTimeout time.Duration, reclaim bool) error {
provider := ""
if isStaticProvider(cfg.Provider) {
provider = staticProvider
}
return claimLeaseForRepoProvider(leaseID, slug, provider, repoRoot, idleTimeout, reclaim)
}
func claimLeaseForRepoProvider(leaseID, slug, provider, repoRoot string, idleTimeout time.Duration, reclaim bool) error {
if leaseID == "" || repoRoot == "" {
return nil
}
now := time.Now().UTC().Format(time.RFC3339)
path, err := leaseClaimPath(leaseID)
if err != nil {
return err
}
existing, err := readLeaseClaim(leaseID)
if err != nil {
return err
}
if existing.LeaseID != "" && existing.RepoRoot != "" && existing.RepoRoot != repoRoot && !reclaim {
return exit(2, "lease %s is claimed by repo %s; use --reclaim to claim it for %s", leaseID, existing.RepoRoot, repoRoot)
}
if existing.ClaimedAt == "" || reclaim || existing.RepoRoot != repoRoot {
existing.ClaimedAt = now
}
existing.LeaseID = leaseID
existing.Slug = slug
if provider != "" {
existing.Provider = provider
}
existing.RepoRoot = repoRoot
existing.LastUsedAt = now
if idleTimeout > 0 {
existing.IdleTimeoutSeconds = int(idleTimeout.Seconds())
}
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return exit(2, "create claim directory: %v", err)
}
data, err := json.MarshalIndent(existing, "", " ")
if err != nil {
return err
}
data = append(data, '\n')
if err := os.WriteFile(path, data, 0o600); err != nil {
return exit(2, "write claim %s: %v", path, err)
}
return nil
}
func resolveLeaseClaim(identifier string) (leaseClaim, bool, error) {
if identifier == "" {
return leaseClaim{}, false, nil
}
if claim, err := readLeaseClaim(identifier); err != nil {
return leaseClaim{}, false, err
} else if claim.LeaseID != "" {
return claim, true, nil
}
dir, err := crabboxStateDir()
if err != nil {
return leaseClaim{}, false, err
}
entries, err := os.ReadDir(filepath.Join(dir, "claims"))
if errors.Is(err, os.ErrNotExist) {
return leaseClaim{}, false, nil
}
if err != nil {
return leaseClaim{}, false, exit(2, "read claims directory: %v", err)
}
slug := normalizeLeaseSlug(identifier)
for _, entry := range entries {
if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
continue
}
leaseID := strings.TrimSuffix(entry.Name(), ".json")
claim, err := readLeaseClaim(leaseID)
if err != nil {
return leaseClaim{}, false, err
}
if claim.LeaseID == identifier || (slug != "" && normalizeLeaseSlug(claim.Slug) == slug) {
return claim, true, nil
}
}
return leaseClaim{}, false, nil
}
func removeLeaseClaim(leaseID string) {
path, err := leaseClaimPath(leaseID)
if err == nil {
_ = os.Remove(path)
}
}
func readLeaseClaim(leaseID string) (leaseClaim, error) {
path, err := leaseClaimPath(leaseID)
if err != nil {
return leaseClaim{}, err
}
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return leaseClaim{}, nil
}
if err != nil {
return leaseClaim{}, exit(2, "read claim %s: %v", path, err)
}
var claim leaseClaim
if err := json.Unmarshal(data, &claim); err != nil {
return leaseClaim{}, exit(2, "parse claim %s: %v", path, err)
}
return claim, nil
}
func leaseClaimPath(leaseID string) (string, error) {
dir, err := crabboxStateDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "claims", leaseID+".json"), nil
}
func crabboxStateDir() (string, error) {
if dir := os.Getenv("XDG_STATE_HOME"); dir != "" {
return filepath.Join(dir, "crabbox"), nil
}
dir, err := os.UserConfigDir()
if err != nil {
return "", exit(2, "user state directory is unavailable")
}
return filepath.Join(dir, "crabbox", "state"), nil
}