332 lines
14 KiB
Go
332 lines
14 KiB
Go
package repo
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"browser.local/platform/domain"
|
|
)
|
|
|
|
type StoreSnapshot struct {
|
|
Users []domain.User `json:"users"`
|
|
AuthSessions []domain.AuthSessionRecord `json:"authSessions"`
|
|
RunControlSessions []domain.RunControlSession `json:"runControlSessions"`
|
|
AIProviders []domain.AIProvider `json:"aiProviders"`
|
|
GamePlugins []domain.GamePlugin `json:"gamePlugins"`
|
|
ServerInstances []domain.ServerInstance `json:"serverInstances"`
|
|
RunEndpoints []domain.RunEndpoint `json:"runEndpoints"`
|
|
Jobs []domain.Job `json:"jobs"`
|
|
Artifacts []domain.Artifact `json:"artifacts"`
|
|
RuntimeBindings []domain.RuntimeBinding `json:"runtimeBindings"`
|
|
EncryptedComponentKeys []domain.EncryptedComponentKey `json:"encryptedComponentKeys"`
|
|
RunDistributions []domain.RunDistribution `json:"runDistributions"`
|
|
ClientManagerDistributions []domain.ClientManagerDistribution `json:"clientManagerDistributions"`
|
|
ClientManagerInstallations []domain.ClientManagerInstallation `json:"clientManagerInstallations"`
|
|
ClientManagerSessions []domain.ClientManagerSession `json:"clientManagerSessions"`
|
|
ClientManagerNonces []domain.ClientManagerRegistrationNonce `json:"clientManagerNonces"`
|
|
DependencyStatuses []domain.DependencyStatus `json:"dependencyStatuses"`
|
|
ClientManagerBuildJobs []domain.ClientManagerBuildJob `json:"clientManagerBuildJobs"`
|
|
RunUpdateJobs []domain.RunUpdateJob `json:"runUpdateJobs"`
|
|
LogStreams []domain.LogStream `json:"logStreams"`
|
|
AuditEvents []domain.AuditEvent `json:"auditEvents"`
|
|
MetricSamples []domain.MetricSample `json:"metricSamples"`
|
|
Backups []domain.BackupRecord `json:"backups"`
|
|
}
|
|
|
|
type FileStore struct {
|
|
*MemoryStore
|
|
path string
|
|
persistMu sync.Mutex
|
|
}
|
|
|
|
func NewFileStore(path string) (*FileStore, error) {
|
|
path = strings.TrimSpace(path)
|
|
if path == "" {
|
|
return nil, fmt.Errorf("metadata path is required")
|
|
}
|
|
store := &FileStore{
|
|
MemoryStore: NewMemoryStore(),
|
|
path: path,
|
|
}
|
|
if err := store.load(); err != nil {
|
|
return nil, err
|
|
}
|
|
return store, nil
|
|
}
|
|
|
|
func (store *FileStore) MetadataPath() string {
|
|
return store.path
|
|
}
|
|
|
|
func (store *FileStore) Users() UserRepository {
|
|
return &persistentRepository[domain.User, domain.UserFilter]{repository: store.MemoryStore.users, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) AuthSessions() AuthSessionRepository {
|
|
return &persistentRepository[domain.AuthSessionRecord, domain.AuthSessionFilter]{repository: store.MemoryStore.authSessions, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) RunControlSessions() RunControlSessionRepository {
|
|
return &persistentRepository[domain.RunControlSession, struct{}]{repository: store.MemoryStore.runSessions, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) AIProviders() AIProviderRepository {
|
|
return &persistentRepository[domain.AIProvider, domain.AIProviderFilter]{repository: store.MemoryStore.aiProviders, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) GamePlugins() GamePluginRepository {
|
|
return &persistentRepository[domain.GamePlugin, domain.GamePluginFilter]{repository: store.MemoryStore.gamePlugins, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) ServerInstances() ServerInstanceRepository {
|
|
return &persistentRepository[domain.ServerInstance, domain.ServerInstanceFilter]{repository: store.MemoryStore.serverInstances, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) RunEndpoints() RunEndpointRepository {
|
|
return &persistentRepository[domain.RunEndpoint, domain.RunEndpointFilter]{repository: store.MemoryStore.runEndpoints, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) Jobs() JobRepository {
|
|
return &persistentJobRepository{
|
|
persistentRepository: &persistentRepository[domain.Job, domain.JobFilter]{repository: store.MemoryStore.jobs, persist: store.persist},
|
|
repository: store.MemoryStore.jobs,
|
|
}
|
|
}
|
|
|
|
func (store *FileStore) Artifacts() ArtifactRepository {
|
|
return &persistentRepository[domain.Artifact, domain.ArtifactFilter]{repository: store.MemoryStore.artifacts, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) RuntimeBindings() RuntimeBindingRepository {
|
|
return &persistentRepository[domain.RuntimeBinding, domain.RuntimeBindingFilter]{repository: store.MemoryStore.runtimeBindings, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) EncryptedComponentKeys() EncryptedComponentKeyRepository {
|
|
return &persistentRepository[domain.EncryptedComponentKey, domain.EncryptedComponentKeyFilter]{repository: store.MemoryStore.componentKeys, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) RunDistributions() RunDistributionRepository {
|
|
return &persistentRepository[domain.RunDistribution, domain.RunDistributionFilter]{repository: store.MemoryStore.runDists, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) ClientManagerDistributions() ClientManagerDistributionRepository {
|
|
return &persistentRepository[domain.ClientManagerDistribution, domain.ClientManagerDistributionFilter]{repository: store.MemoryStore.clientDists, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) ClientManagerInstallations() ClientManagerInstallationRepository {
|
|
return &persistentRepository[domain.ClientManagerInstallation, domain.ClientManagerInstallationFilter]{repository: store.MemoryStore.clientInstalls, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) ClientManagerSessions() ClientManagerSessionRepository {
|
|
return &persistentRepository[domain.ClientManagerSession, domain.ClientManagerSessionFilter]{repository: store.MemoryStore.clientSessions, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) ClientManagerNonces() ClientManagerNonceRepository {
|
|
return &persistentRepository[domain.ClientManagerRegistrationNonce, domain.ClientManagerNonceFilter]{repository: store.MemoryStore.clientNonces, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) DependencyStatuses() DependencyStatusRepository {
|
|
return &persistentRepository[domain.DependencyStatus, domain.DependencyStatusFilter]{repository: store.MemoryStore.dependencies, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) ClientManagerBuildJobs() ClientManagerBuildJobRepository {
|
|
return &persistentRepository[domain.ClientManagerBuildJob, domain.ClientManagerBuildJobFilter]{repository: store.MemoryStore.buildJobs, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) RunUpdateJobs() RunUpdateJobRepository {
|
|
return &persistentRepository[domain.RunUpdateJob, domain.RunUpdateJobFilter]{repository: store.MemoryStore.updateJobs, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) LogStreams() LogStreamRepository {
|
|
return &persistentRepository[domain.LogStream, domain.LogStreamFilter]{repository: store.MemoryStore.logStreams, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) AuditEvents() AuditEventRepository {
|
|
return &persistentRepository[domain.AuditEvent, domain.AuditEventFilter]{repository: store.MemoryStore.auditEvents, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) MetricSamples() MetricSampleRepository {
|
|
return &persistentRepository[domain.MetricSample, domain.MetricSampleFilter]{repository: store.MemoryStore.metricSamples, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) Backups() BackupRepository {
|
|
return &persistentRepository[domain.BackupRecord, domain.BackupFilter]{repository: store.MemoryStore.backups, persist: store.persist}
|
|
}
|
|
|
|
func (store *FileStore) load() error {
|
|
data, err := os.ReadFile(store.path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("read metadata snapshot: %w", err)
|
|
}
|
|
if len(strings.TrimSpace(string(data))) == 0 {
|
|
return nil
|
|
}
|
|
var snapshot StoreSnapshot
|
|
if err := json.Unmarshal(data, &snapshot); err != nil {
|
|
return fmt.Errorf("decode metadata snapshot: %w", err)
|
|
}
|
|
store.loadSnapshot(snapshot)
|
|
return nil
|
|
}
|
|
|
|
func (store *FileStore) persist() error {
|
|
store.persistMu.Lock()
|
|
defer store.persistMu.Unlock()
|
|
|
|
snapshot := store.snapshot()
|
|
data, err := json.MarshalIndent(snapshot, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("encode metadata snapshot: %w", err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(store.path), 0o755); err != nil {
|
|
return fmt.Errorf("create metadata directory: %w", err)
|
|
}
|
|
tmpPath := store.path + ".tmp"
|
|
if err := os.WriteFile(tmpPath, data, 0o600); err != nil {
|
|
return fmt.Errorf("write metadata snapshot: %w", err)
|
|
}
|
|
if err := os.Rename(tmpPath, store.path); err != nil {
|
|
return fmt.Errorf("replace metadata snapshot: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (store *FileStore) snapshot() StoreSnapshot {
|
|
return StoreSnapshot{
|
|
Users: snapshotRepository(store.MemoryStore.users),
|
|
AuthSessions: snapshotRepository(store.MemoryStore.authSessions),
|
|
RunControlSessions: snapshotRepository(store.MemoryStore.runSessions),
|
|
AIProviders: snapshotRepository(store.MemoryStore.aiProviders),
|
|
GamePlugins: snapshotRepository(store.MemoryStore.gamePlugins),
|
|
ServerInstances: snapshotRepository(store.MemoryStore.serverInstances),
|
|
RunEndpoints: snapshotRepository(store.MemoryStore.runEndpoints),
|
|
Jobs: snapshotRepository(store.MemoryStore.jobs.memoryRepository),
|
|
Artifacts: snapshotRepository(store.MemoryStore.artifacts),
|
|
RuntimeBindings: snapshotRepository(store.MemoryStore.runtimeBindings),
|
|
EncryptedComponentKeys: snapshotRepository(store.MemoryStore.componentKeys),
|
|
RunDistributions: snapshotRepository(store.MemoryStore.runDists),
|
|
ClientManagerDistributions: snapshotRepository(store.MemoryStore.clientDists),
|
|
ClientManagerInstallations: snapshotRepository(store.MemoryStore.clientInstalls),
|
|
ClientManagerSessions: snapshotRepository(store.MemoryStore.clientSessions),
|
|
ClientManagerNonces: snapshotRepository(store.MemoryStore.clientNonces),
|
|
DependencyStatuses: snapshotRepository(store.MemoryStore.dependencies),
|
|
ClientManagerBuildJobs: snapshotRepository(store.MemoryStore.buildJobs),
|
|
RunUpdateJobs: snapshotRepository(store.MemoryStore.updateJobs),
|
|
LogStreams: snapshotRepository(store.MemoryStore.logStreams),
|
|
AuditEvents: snapshotRepository(store.MemoryStore.auditEvents),
|
|
MetricSamples: snapshotRepository(store.MemoryStore.metricSamples),
|
|
Backups: snapshotRepository(store.MemoryStore.backups),
|
|
}
|
|
}
|
|
|
|
func (store *FileStore) loadSnapshot(snapshot StoreSnapshot) {
|
|
loadRepository(store.MemoryStore.users, snapshot.Users)
|
|
loadRepository(store.MemoryStore.authSessions, snapshot.AuthSessions)
|
|
loadRepository(store.MemoryStore.runSessions, snapshot.RunControlSessions)
|
|
loadRepository(store.MemoryStore.aiProviders, snapshot.AIProviders)
|
|
loadRepository(store.MemoryStore.gamePlugins, snapshot.GamePlugins)
|
|
loadRepository(store.MemoryStore.serverInstances, snapshot.ServerInstances)
|
|
loadRepository(store.MemoryStore.runEndpoints, snapshot.RunEndpoints)
|
|
loadRepository(store.MemoryStore.jobs.memoryRepository, snapshot.Jobs)
|
|
loadRepository(store.MemoryStore.artifacts, snapshot.Artifacts)
|
|
loadRepository(store.MemoryStore.runtimeBindings, snapshot.RuntimeBindings)
|
|
loadRepository(store.MemoryStore.componentKeys, snapshot.EncryptedComponentKeys)
|
|
loadRepository(store.MemoryStore.runDists, snapshot.RunDistributions)
|
|
loadRepository(store.MemoryStore.clientDists, snapshot.ClientManagerDistributions)
|
|
loadRepository(store.MemoryStore.clientInstalls, snapshot.ClientManagerInstallations)
|
|
loadRepository(store.MemoryStore.clientSessions, snapshot.ClientManagerSessions)
|
|
loadRepository(store.MemoryStore.clientNonces, snapshot.ClientManagerNonces)
|
|
loadRepository(store.MemoryStore.dependencies, snapshot.DependencyStatuses)
|
|
loadRepository(store.MemoryStore.buildJobs, snapshot.ClientManagerBuildJobs)
|
|
loadRepository(store.MemoryStore.updateJobs, snapshot.RunUpdateJobs)
|
|
loadRepository(store.MemoryStore.logStreams, snapshot.LogStreams)
|
|
loadRepository(store.MemoryStore.auditEvents, snapshot.AuditEvents)
|
|
loadRepository(store.MemoryStore.metricSamples, snapshot.MetricSamples)
|
|
loadRepository(store.MemoryStore.backups, snapshot.Backups)
|
|
}
|
|
|
|
type mutableRepository[T any, F any] interface {
|
|
Create(T) error
|
|
Get(string) (T, error)
|
|
List(F) ([]T, error)
|
|
Update(T) error
|
|
Delete(string) error
|
|
}
|
|
|
|
type persistentRepository[T any, F any] struct {
|
|
repository mutableRepository[T, F]
|
|
persist func() error
|
|
}
|
|
|
|
func (repository *persistentRepository[T, F]) Create(value T) error {
|
|
if err := repository.repository.Create(value); err != nil {
|
|
return err
|
|
}
|
|
return repository.persist()
|
|
}
|
|
|
|
func (repository *persistentRepository[T, F]) Get(id string) (T, error) {
|
|
return repository.repository.Get(id)
|
|
}
|
|
|
|
func (repository *persistentRepository[T, F]) List(filter F) ([]T, error) {
|
|
return repository.repository.List(filter)
|
|
}
|
|
|
|
func (repository *persistentRepository[T, F]) Update(value T) error {
|
|
if err := repository.repository.Update(value); err != nil {
|
|
return err
|
|
}
|
|
return repository.persist()
|
|
}
|
|
|
|
func (repository *persistentRepository[T, F]) Delete(id string) error {
|
|
if err := repository.repository.Delete(id); err != nil {
|
|
return err
|
|
}
|
|
return repository.persist()
|
|
}
|
|
|
|
type persistentJobRepository struct {
|
|
*persistentRepository[domain.Job, domain.JobFilter]
|
|
repository JobRepository
|
|
}
|
|
|
|
func (repository *persistentJobRepository) GetByIdempotency(runEndpointID string, idempotencyKey string) (domain.Job, error) {
|
|
return repository.repository.GetByIdempotency(runEndpointID, idempotencyKey)
|
|
}
|
|
|
|
func snapshotRepository[T any, F any](repository *memoryRepository[T, F]) []T {
|
|
repository.mu.RLock()
|
|
defer repository.mu.RUnlock()
|
|
|
|
ids := make([]string, 0, len(repository.byID))
|
|
for id := range repository.byID {
|
|
ids = append(ids, id)
|
|
}
|
|
sort.Strings(ids)
|
|
values := make([]T, 0, len(ids))
|
|
for _, id := range ids {
|
|
values = append(values, repository.copyOf(repository.byID[id]))
|
|
}
|
|
return values
|
|
}
|
|
|
|
func loadRepository[T any, F any](repository *memoryRepository[T, F], values []T) {
|
|
repository.mu.Lock()
|
|
defer repository.mu.Unlock()
|
|
|
|
repository.byID = map[string]T{}
|
|
for _, value := range values {
|
|
repository.byID[repository.idOf(value)] = repository.copyOf(value)
|
|
}
|
|
}
|