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 */ } } }