munin/internal/workspace/workspace.go
Thomas Adamek 466f1e40af Workspaces, Änderungshistorie, Papierkorb, Nutzungsübersicht, Hilfe
Umsetzung der Ausbaustufen 2.1, 3.6, 3.8, 3.9, 4.1, 4.2, 4.4, 4.6,
5.1–5.4 (Details in AUSBAUSTUFEN.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:44:52 +02:00

105 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package workspace verwaltet die offenen Workspace-Dateien eines laufenden
// Prozesses.
//
// Jede Abteilung hat ihre eigene SQLite-Datei (siehe AUSBAUSTUFEN.md 4.4). Ein
// Manager öffnet sie bei Bedarf und hält sie offen, statt bei jedem Request
// neu zu öffnen — das Öffnen (inklusive Migrationsprüfung) kostet spürbar mehr
// als ein Map-Zugriff.
package workspace
import (
"fmt"
"os"
"path/filepath"
"sync"
"munin/internal/search"
"munin/internal/store"
)
// Workspace bündelt, was zu einer offenen Workspace-Datei gehört: die
// Datenbank und ihren Suchindex. Beide sind an dieselbe Datei gebunden und
// werden deshalb gemeinsam verwaltet — ein Registry, der aus Versehen an der
// falschen Datenbank hängt, wäre ein stiller Fehler.
type Workspace struct {
ID string
Store *store.Store
Search *search.Registry
}
// Manager öffnet Workspace-Dateien bei Bedarf und hält sie offen.
//
// Bei 10–40 Workspaces (siehe AUSBAUSTUFEN.md 4.4) ist das unproblematisch —
// Verdrängung selten gebrauchter Dateien würde erst bei einer deutlich
// höheren Zahl nötig, siehe AUSBAUSTUFEN.md 1.2.
type Manager struct {
dir string // Ordner mit einer Datei je Workspace, z.B. "<DATA_DIR>/workspaces"
mu sync.Mutex
open map[string]*Workspace
}
// NewManager legt einen Manager an. dir muss bereits existieren — das
// übernimmt main.go beim Start.
func NewManager(dir string) *Manager {
return &Manager{dir: dir, open: make(map[string]*Workspace)}
}
// Get liefert den Workspace zu einer ID und öffnet ihn beim ersten Zugriff.
func (m *Manager) Get(workspaceID string) (*Workspace, error) {
m.mu.Lock()
defer m.mu.Unlock()
if w, ok := m.open[workspaceID]; ok {
return w, nil
}
s, err := store.Open(filepath.Join(m.dir, workspaceID+".db"))
if err != nil {
return nil, fmt.Errorf("Workspace %s konnte nicht geöffnet werden: %w", workspaceID, err)
}
w := &Workspace{ID: workspaceID, Store: s, Search: search.NewRegistry(s)}
m.open[workspaceID] = w
return w, nil
}
// Delete schließt die Workspace-Datei, falls offen, und entfernt sie
// unwiederbringlich von der Platte — samt WAL/SHM-Nebendateien, die SQLite im
// laufenden Betrieb danebenlegt.
//
// Ruft nur die Datei ab — die Kontroll-Datenbank (workspaces, key_index) muss
// getrennt bereinigt werden, siehe ControlStore.DeleteWorkspace. Reihenfolge
// ist wichtig: Erst dort löschen, dann hier — sonst verweist die
// Kontroll-Datenbank kurzzeitig auf eine Datei, die es nicht mehr gibt.
func (m *Manager) Delete(workspaceID string) error {
m.mu.Lock()
if w, ok := m.open[workspaceID]; ok {
w.Store.Close()
delete(m.open, workspaceID)
}
m.mu.Unlock()
pfad := filepath.Join(m.dir, workspaceID+".db")
for _, endung := range []string{"", "-wal", "-shm"} {
if err := os.Remove(pfad + endung); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("Workspace-Datei konnte nicht gelöscht werden: %w", err)
}
}
return nil
}
// Close schließt alle offenen Workspace-Dateien.
func (m *Manager) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
var erster error
for id, w := range m.open {
if err := w.Store.Close(); err != nil && erster == nil {
erster = err
}
delete(m.open, id)
}
return erster
}