Files
decent-webui/internal/filesvc/memstore.go
2025-09-27 09:33:40 +02:00

196 lines
3.4 KiB
Go

package filesvc
import (
"context"
"slices"
"strings"
"sync"
"time"
)
type MemStore struct {
mu sync.Mutex
items map[ID]File
next ID
// optionales Eventing
subs []chan ChangeEvent
}
func NewMemStore() *MemStore {
return &MemStore{
items: make(map[ID]File),
next: 1,
}
}
/*** 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
}
ids := make([]ID, 0, len(m.items))
for id := range m.items {
ids = append(ids, id)
}
slices.Sort(ids)
start := 0
if next > 0 {
for i, id := range ids {
if id >= next {
start = i
break
}
}
}
end := start + limit
if end > len(ids) {
end = len(ids)
}
out := make([]File, 0, end-start)
for _, id := range ids[start:end] {
it := m.items[id]
if !it.Deleted {
out = append(out, it)
}
}
var nextOut ID
if end < len(ids) {
nextOut = ids[end]
}
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()
it := File{ID: m.next, Name: name, UpdatedAt: now}
m.items[it.ID] = it
m.next++
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
if ri.ID >= m.next {
m.next = ri.ID + 1
}
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 */
}
}
}