Files
decent-webui/internal/filesvc/memstore.go
jbergner baedff9e9d
All checks were successful
release-tag / release-image (push) Successful in 1m32s
Umstellung auf uuid
2025-09-27 21:42:32 +02:00

209 lines
3.7 KiB
Go

package filesvc
import (
"context"
"slices"
"strings"
"sync"
"time"
)
type MemStore struct {
mu sync.Mutex
items map[ID]File
// optionales Eventing
subs []chan ChangeEvent
}
func NewMemStore() *MemStore {
return &MemStore{
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}
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
}
it.Name = 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.Deleted {
return it, nil
}
it.Deleted = true
it.UpdatedAt = time.Now().UnixNano()
m.items[id] = it
m.emit(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 {
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 */
}
}
}