Files
decent-webui/internal/filesvc/memstore.go
2025-09-29 23:04:46 +02:00

242 lines
4.6 KiB
Go

package filesvc
import (
"context"
"slices"
"strings"
"sync"
"time"
)
type MemStore struct {
mu sync.Mutex
items map[ID]File
self string
// optionales Eventing
subs []chan ChangeEvent
}
func NewMemStore(self string) *MemStore {
return &MemStore{self: strings.TrimSpace(self), items: make(map[ID]File)}
}
/*** 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.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.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.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.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()
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
m.emitLocked(ri)
}
}
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 */
}
}
}