初始化仓库
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const sharedWorkspaceBindingsKey = "shared"
|
||||
|
||||
// FlexTime wraps time.Time with lenient JSON unmarshaling.
|
||||
type FlexTime struct{ time.Time }
|
||||
|
||||
func (ft *FlexTime) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
ft.Time = time.Time{}
|
||||
return nil
|
||||
}
|
||||
if s == "" {
|
||||
ft.Time = time.Time{}
|
||||
return nil
|
||||
}
|
||||
for _, layout := range []string{
|
||||
time.RFC3339Nano,
|
||||
time.RFC3339,
|
||||
"2006-01-02T15:04:05.999999999",
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02 15:04:05",
|
||||
} {
|
||||
if t, err := time.ParseInLocation(layout, s, time.Local); err == nil {
|
||||
ft.Time = t
|
||||
return nil
|
||||
}
|
||||
}
|
||||
slog.Warn("workspace bindings: unparseable bound_at, treating as zero", "value", s)
|
||||
ft.Time = time.Time{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WorkspaceBinding maps a channel to a workspace directory.
|
||||
type WorkspaceBinding struct {
|
||||
ChannelName string `json:"channel_name"`
|
||||
Workspace string `json:"workspace"`
|
||||
BoundAt FlexTime `json:"bound_at"`
|
||||
}
|
||||
|
||||
// WorkspaceBindingManager persists channel->workspace mappings.
|
||||
// Top-level key is "project:<name>", second-level key is a workspace channel key.
|
||||
type WorkspaceBindingManager struct {
|
||||
mu sync.RWMutex
|
||||
bindings map[string]map[string]*WorkspaceBinding
|
||||
storePath string
|
||||
lastLoadedModTime time.Time
|
||||
lastLoadedSize int64
|
||||
}
|
||||
|
||||
func NewWorkspaceBindingManager(storePath string) *WorkspaceBindingManager {
|
||||
m := &WorkspaceBindingManager{
|
||||
bindings: make(map[string]map[string]*WorkspaceBinding),
|
||||
storePath: storePath,
|
||||
}
|
||||
if storePath != "" {
|
||||
m.load()
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func legacyWorkspaceChannelKey(channelKey string) string {
|
||||
if i := strings.IndexByte(channelKey, ':'); i >= 0 {
|
||||
return channelKey[i+1:]
|
||||
}
|
||||
return channelKey
|
||||
}
|
||||
|
||||
func workspaceChannelKeyCandidates(channelKey string) []string {
|
||||
if channelKey == "" {
|
||||
return nil
|
||||
}
|
||||
legacyKey := legacyWorkspaceChannelKey(channelKey)
|
||||
if legacyKey == channelKey {
|
||||
return []string{channelKey}
|
||||
}
|
||||
return []string{channelKey, legacyKey}
|
||||
}
|
||||
|
||||
func (m *WorkspaceBindingManager) lookupLocked(projectKey, channelKey string) *WorkspaceBinding {
|
||||
proj := m.bindings[projectKey]
|
||||
if proj == nil {
|
||||
return nil
|
||||
}
|
||||
for _, candidate := range workspaceChannelKeyCandidates(channelKey) {
|
||||
if b := proj[candidate]; b != nil {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *WorkspaceBindingManager) Bind(projectKey, channelKey, channelName, workspace string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.refreshLocked()
|
||||
if m.bindings[projectKey] == nil {
|
||||
m.bindings[projectKey] = make(map[string]*WorkspaceBinding)
|
||||
}
|
||||
m.bindings[projectKey][channelKey] = &WorkspaceBinding{
|
||||
ChannelName: channelName,
|
||||
Workspace: workspace,
|
||||
BoundAt: FlexTime{time.Now()},
|
||||
}
|
||||
m.saveLocked()
|
||||
}
|
||||
|
||||
func (m *WorkspaceBindingManager) Unbind(projectKey, channelKey string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.refreshLocked()
|
||||
if proj := m.bindings[projectKey]; proj != nil {
|
||||
for _, candidate := range workspaceChannelKeyCandidates(channelKey) {
|
||||
delete(proj, candidate)
|
||||
}
|
||||
if len(proj) == 0 {
|
||||
delete(m.bindings, projectKey)
|
||||
}
|
||||
}
|
||||
m.saveLocked()
|
||||
}
|
||||
|
||||
func (m *WorkspaceBindingManager) Lookup(projectKey, channelKey string) *WorkspaceBinding {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.refreshLocked()
|
||||
return m.lookupLocked(projectKey, channelKey)
|
||||
}
|
||||
|
||||
// LookupEffective returns the effective binding for a channel, checking the
|
||||
// current project first and then the shared routing layer.
|
||||
func (m *WorkspaceBindingManager) LookupEffective(projectKey, channelKey string) (*WorkspaceBinding, string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.refreshLocked()
|
||||
if b := m.lookupLocked(projectKey, channelKey); b != nil {
|
||||
return b, projectKey
|
||||
}
|
||||
if b := m.lookupLocked(sharedWorkspaceBindingsKey, channelKey); b != nil {
|
||||
return b, sharedWorkspaceBindingsKey
|
||||
}
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
func (m *WorkspaceBindingManager) ListByProject(projectKey string) map[string]*WorkspaceBinding {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.refreshLocked()
|
||||
result := make(map[string]*WorkspaceBinding)
|
||||
if proj := m.bindings[projectKey]; proj != nil {
|
||||
for k, v := range proj {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *WorkspaceBindingManager) saveLocked() {
|
||||
if m.storePath == "" {
|
||||
return
|
||||
}
|
||||
data, err := json.MarshalIndent(m.bindings, "", " ")
|
||||
if err != nil {
|
||||
slog.Error("workspace bindings: marshal error", "err", err)
|
||||
return
|
||||
}
|
||||
if err := AtomicWriteFile(m.storePath, data, 0o644); err != nil {
|
||||
slog.Error("workspace bindings: save error", "err", err)
|
||||
return
|
||||
}
|
||||
if info, err := os.Stat(m.storePath); err == nil {
|
||||
m.lastLoadedModTime = info.ModTime()
|
||||
m.lastLoadedSize = info.Size()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *WorkspaceBindingManager) load() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.refreshLocked()
|
||||
}
|
||||
|
||||
func (m *WorkspaceBindingManager) refreshLocked() {
|
||||
if m.storePath == "" {
|
||||
return
|
||||
}
|
||||
info, err := os.Stat(m.storePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
m.bindings = make(map[string]map[string]*WorkspaceBinding)
|
||||
m.lastLoadedModTime = time.Time{}
|
||||
m.lastLoadedSize = 0
|
||||
return
|
||||
}
|
||||
slog.Error("workspace bindings: stat error", "err", err)
|
||||
return
|
||||
}
|
||||
if !m.lastLoadedModTime.IsZero() && info.ModTime().Equal(m.lastLoadedModTime) && info.Size() == m.lastLoadedSize {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(m.storePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
slog.Error("workspace bindings: load error", "err", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
loaded := make(map[string]map[string]*WorkspaceBinding)
|
||||
if len(data) > 0 {
|
||||
if err := json.Unmarshal(data, &loaded); err != nil {
|
||||
slog.Error("workspace bindings: unmarshal error", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
m.bindings = loaded
|
||||
m.lastLoadedModTime = info.ModTime()
|
||||
m.lastLoadedSize = info.Size()
|
||||
}
|
||||
Reference in New Issue
Block a user