All checks were successful
release-tag / release-image (push) Successful in 1m32s
209 lines
3.7 KiB
Go
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 */
|
|
}
|
|
}
|
|
}
|