Files
decent-webui/internal/filesvc/memstore.go
jbergner 291cfa33a9
All checks were successful
release-tag / release-image (push) Successful in 1m27s
Persistenz implementiert : Test-3
2025-09-29 23:55:35 +02:00

323 lines
6.2 KiB
Go
Raw Permalink 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 filesvc
import (
"context"
"encoding/json"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"time"
)
type MemStore struct {
mu sync.Mutex
items map[ID]File
self string
// optionales Eventing
subs []chan ChangeEvent
persistPath string
}
func NewMemStore(self string) *MemStore {
return &MemStore{self: strings.TrimSpace(self), items: make(map[ID]File)}
}
func NewMemStorePersistent(self, path string) *MemStore {
m := NewMemStore(self)
m.persistPath = strings.TrimSpace(path)
// beim Start versuchen zu laden
_ = m.loadFromDisk()
return m
}
// --- Persistenz-Helper (NEU) ---
func (m *MemStore) loadFromDisk() error {
if m.persistPath == "" {
return nil
}
f, err := os.Open(m.persistPath)
if err != nil {
return nil // Datei existiert beim ersten Start nicht ok
}
defer f.Close()
var snap Snapshot
if err := json.NewDecoder(f).Decode(&snap); err != nil {
return err
}
m.mu.Lock()
for _, it := range snap.Items {
m.items[it.ID] = it
}
m.mu.Unlock()
return nil
}
func (m *MemStore) saveLocked() error {
if m.persistPath == "" {
return nil
}
if err := os.MkdirAll(filepath.Dir(m.persistPath), 0o755); err != nil {
return err
}
// Snapshot aus Map bauen
snap := Snapshot{Items: make([]File, 0, len(m.items))}
for _, it := range m.items {
snap.Items = append(snap.Items, it)
}
// atomar schreiben
tmp := m.persistPath + ".tmp"
f, err := os.Create(tmp)
if err != nil {
return err
}
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err := enc.Encode(&snap); err != nil {
f.Close()
_ = os.Remove(tmp)
return err
}
if err := f.Sync(); err != nil {
f.Close()
_ = os.Remove(tmp)
return err
}
if err := f.Close(); err != nil {
_ = os.Remove(tmp)
return err
}
return os.Rename(tmp, m.persistPath)
}
/*** Store ***/
func (m *MemStore) Get(_ context.Context, id ID) (File, error) {
m.mu.Lock()
defer m.mu.Unlock()
it, ok := m.items[id]
if !ok || it.Deleted {
return File{}, ErrNotFound
}
return it, nil
}
func (m *MemStore) List(_ context.Context, next ID, limit int) ([]File, ID, error) {
m.mu.Lock()
defer m.mu.Unlock()
if limit <= 0 || limit > 1000 {
limit = 100
}
// sortiere deterministisch nach UpdatedAt, dann ID
all := make([]File, 0, len(m.items))
for _, v := range m.items {
all = append(all, v)
}
slices.SortFunc(all, func(a, b File) int {
if a.UpdatedAt == b.UpdatedAt {
if a.ID == b.ID {
return 0
}
if a.ID < b.ID {
return -1
}
return 1
}
if a.UpdatedAt < b.UpdatedAt {
return -1
}
return 1
})
start := 0
if next != "" {
for i, it := range all {
if it.ID >= next {
start = i
break
}
}
}
end := start + limit
if end > len(all) {
end = len(all)
}
out := make([]File, 0, end-start)
for _, it := range all[start:end] {
if !it.Deleted {
out = append(out, it)
}
}
var nextOut ID
if end < len(all) {
nextOut = all[end].ID
}
return out, nextOut, nil
}
func (m *MemStore) Create(_ context.Context, name string) (File, error) {
name = strings.TrimSpace(name)
if name == "" {
return File{}, ErrBadInput
}
m.mu.Lock()
defer m.mu.Unlock()
now := time.Now().UnixNano()
uid, err := NewUUIDv4()
if err != nil {
return File{}, err
}
it := File{ID: uid, Name: name, UpdatedAt: now, Owner: m.self}
m.items[it.ID] = it
_ = m.saveLocked()
m.emit(it)
return it, nil
}
func (m *MemStore) Rename(_ context.Context, id ID, newName string) (File, error) {
newName = strings.TrimSpace(newName)
if newName == "" {
return File{}, ErrBadInput
}
m.mu.Lock()
defer m.mu.Unlock()
it, ok := m.items[id]
if !ok || it.Deleted {
return File{}, ErrNotFound
}
if it.Owner != "" && it.Owner != m.self { // ← nur Owner
return File{}, ErrForbidden
}
it.Name = strings.TrimSpace(newName)
it.UpdatedAt = time.Now().UnixNano()
m.items[id] = it
_ = m.saveLocked()
m.emit(it)
return it, nil
}
func (m *MemStore) Delete(_ context.Context, id ID) (File, error) {
m.mu.Lock()
defer m.mu.Unlock()
it, ok := m.items[id]
if !ok {
return File{}, ErrNotFound
}
if it.Owner != "" && it.Owner != m.self { // ← nur Owner
return File{}, ErrForbidden
}
if it.Deleted {
return it, nil
}
it.Deleted = true
it.UpdatedAt = time.Now().UnixNano()
m.items[id] = it
_ = m.saveLocked()
m.emit(it)
return it, nil
}
func (m *MemStore) TakeoverOwner(_ context.Context, id ID, newOwner string) (File, error) {
m.mu.Lock()
defer m.mu.Unlock()
it, ok := m.items[id]
if !ok || it.Deleted {
return File{}, ErrNotFound
}
newOwner = strings.TrimSpace(newOwner)
if newOwner == "" {
return File{}, ErrBadInput
}
// Sicherheit: nur für sich selbst übernehmen
if newOwner != m.self {
return File{}, ErrForbidden
}
if it.Owner == newOwner {
return it, nil
}
it.Owner = newOwner
it.UpdatedAt = time.Now().UnixNano()
m.items[id] = it
_ = m.saveLocked()
m.emitLocked(it)
return it, nil
}
/*** Replicable ***/
func (m *MemStore) Snapshot(_ context.Context) (Snapshot, error) {
m.mu.Lock()
defer m.mu.Unlock()
s := Snapshot{Items: make([]File, 0, len(m.items))}
for _, it := range m.items {
s.Items = append(s.Items, it) // inkl. Tombstones
}
return s, nil
}
func (m *MemStore) ApplyRemote(_ context.Context, s Snapshot) error {
m.mu.Lock()
defer m.mu.Unlock()
changed := false
for _, ri := range s.Items {
li, ok := m.items[ri.ID]
if !ok || ri.UpdatedAt > li.UpdatedAt {
// Owner nie überschreiben, außer er ist leer
if ok && li.Owner != "" && ri.Owner != "" && ri.Owner != li.Owner {
ri.Owner = li.Owner
}
m.items[ri.ID] = ri
changed = true
m.emitLocked(ri)
}
}
if changed {
_ = m.saveLocked() // ← NEU
}
return nil
}
/*** Watchable (optional) ***/
func (m *MemStore) Watch(stop <-chan struct{}) <-chan ChangeEvent {
ch := make(chan ChangeEvent, 32)
m.mu.Lock()
m.subs = append(m.subs, ch)
m.mu.Unlock()
go func() {
<-stop
m.mu.Lock()
// entferne ch aus subs
for i, s := range m.subs {
if s == ch {
m.subs = append(m.subs[:i], m.subs[i+1:]...)
break
}
}
m.mu.Unlock()
close(ch)
}()
return ch
}
func (m *MemStore) emit(it File) {
m.emitLocked(it) // mu wird im Aufrufer gehalten
}
func (m *MemStore) emitLocked(it File) {
ev := ChangeEvent{At: time.Now(), Item: it}
for _, s := range m.subs {
select {
case s <- ev:
default: /* drop wenn voll */
}
}
}