@@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"io"
+ "strings"
"time"
"git.secluded.site/np/internal/db"
@@ -344,3 +345,61 @@ func (s *Store) Archive(ctx context.Context, sid string) (Document, error) {
return doc, err
}
+
+// LatestArchivedByPath returns the most recently archived session for path
+// or any of its parents. The returned boolean reports whether a session was found.
+func (s *Store) LatestArchivedByPath(ctx context.Context, path string) (Document, bool, error) {
+ canonical, err := db.CanonicalizeDir(path)
+ if err != nil {
+ return Document{}, false, fmt.Errorf("session: canonicalise path: %w", err)
+ }
+
+ var doc Document
+ found := false
+
+ err = s.db.View(ctx, func(txn *db.Txn) error {
+ for _, candidate := range db.ParentWalk(canonical) {
+ hash := db.DirHash(candidate)
+ prefix := db.PrefixDirArchived(hash)
+
+ // Iterate in reverse to get most recent first
+ iterErr := txn.Iterate(db.IterateOptions{
+ Prefix: prefix,
+ Reverse: true,
+ }, func(item db.Item) error {
+ // Extract SID from key: dir/{hash}/archived/{ts}/{sid}
+ key := item.KeyString()
+ i := strings.LastIndex(key, "/")
+ if i < 0 || i+1 >= len(key) {
+ return nil
+ }
+ sid := key[i+1:]
+
+ // Try to load the document; if not found, continue to next older
+ loaded, err := loadDocument(txn, sid)
+ if err != nil {
+ if errors.Is(err, ErrNotFound) {
+ return nil // Try next older entry
+ }
+ return err
+ }
+
+ doc = loaded
+ found = true
+ return db.ErrTxnAborted
+ })
+ if iterErr != nil {
+ return iterErr
+ }
+
+ if found {
+ return nil
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return Document{}, false, err
+ }
+ return doc, found, nil
+}
@@ -246,3 +246,154 @@ func TestTxnStoreTouchAt(t *testing.T) {
t.Fatalf("Expected LastUpdatedAt %v, got %v", want, autoUpdated.LastUpdatedAt)
}
}
+
+func TestLatestArchivedByPath(t *testing.T) {
+ ctx := context.Background()
+ database := testutil.OpenDB(t)
+
+ clock := &testutil.SequenceClock{
+ Times: []time.Time{
+ time.Date(2025, time.April, 1, 9, 0, 0, 0, time.UTC),
+ time.Date(2025, time.April, 1, 10, 0, 0, 0, time.UTC),
+ time.Date(2025, time.April, 2, 9, 0, 0, 0, time.UTC),
+ time.Date(2025, time.April, 2, 10, 0, 0, 0, time.UTC),
+ time.Date(2025, time.April, 3, 9, 0, 0, 0, time.UTC),
+ time.Date(2025, time.April, 3, 10, 0, 0, 0, time.UTC),
+ },
+ }
+
+ store := session.NewStore(database, clock)
+ workdir := t.TempDir()
+
+ // Create and archive first session
+ session1, err := store.Start(ctx, workdir)
+ if err != nil {
+ t.Fatalf("Start session1: %v", err)
+ }
+ _, err = store.Archive(ctx, session1.SID)
+ if err != nil {
+ t.Fatalf("Archive session1: %v", err)
+ }
+
+ // Create and archive second session (more recent)
+ session2, err := store.Start(ctx, workdir)
+ if err != nil {
+ t.Fatalf("Start session2: %v", err)
+ }
+ _, err = store.Archive(ctx, session2.SID)
+ if err != nil {
+ t.Fatalf("Archive session2: %v", err)
+ }
+
+ // Create and archive third session (most recent)
+ session3, err := store.Start(ctx, workdir)
+ if err != nil {
+ t.Fatalf("Start session3: %v", err)
+ }
+ archived3, err := store.Archive(ctx, session3.SID)
+ if err != nil {
+ t.Fatalf("Archive session3: %v", err)
+ }
+
+ // LatestArchivedByPath should return the most recent archived session
+ latest, found, err := store.LatestArchivedByPath(ctx, workdir)
+ if err != nil {
+ t.Fatalf("LatestArchivedByPath: %v", err)
+ }
+ if !found {
+ t.Fatalf("expected to find archived session")
+ }
+ if latest.SID != archived3.SID {
+ t.Fatalf("expected latest SID %s, got %s", archived3.SID, latest.SID)
+ }
+ if latest.State != session.StateArchived {
+ t.Fatalf("expected archived state, got %s", latest.State)
+ }
+
+ // Test with child directory (parent walk)
+ childDir := filepath.Join(workdir, "nested", "deep")
+ if err := os.MkdirAll(childDir, 0o755); err != nil {
+ t.Fatalf("mkdir nested: %v", err)
+ }
+
+ latestFromChild, found, err := store.LatestArchivedByPath(ctx, childDir)
+ if err != nil {
+ t.Fatalf("LatestArchivedByPath from child: %v", err)
+ }
+ if !found {
+ t.Fatalf("expected to find archived session from child dir")
+ }
+ if latestFromChild.SID != archived3.SID {
+ t.Fatalf("expected latest SID %s from child, got %s", archived3.SID, latestFromChild.SID)
+ }
+
+ // Test with directory that has no archived sessions
+ emptyDir := t.TempDir()
+ _, found, err = store.LatestArchivedByPath(ctx, emptyDir)
+ if err != nil {
+ t.Fatalf("LatestArchivedByPath on empty dir: %v", err)
+ }
+ if found {
+ t.Fatalf("expected not to find archived session in empty dir")
+ }
+}
+
+func TestLatestArchivedByPathWithMissingDoc(t *testing.T) {
+ ctx := context.Background()
+ database := testutil.OpenDB(t)
+
+ clock := &testutil.SequenceClock{
+ Times: []time.Time{
+ time.Date(2025, time.April, 1, 9, 0, 0, 0, time.UTC),
+ time.Date(2025, time.April, 1, 10, 0, 0, 0, time.UTC),
+ time.Date(2025, time.April, 2, 9, 0, 0, 0, time.UTC),
+ time.Date(2025, time.April, 2, 10, 0, 0, 0, time.UTC),
+ },
+ }
+
+ store := session.NewStore(database, clock)
+ workdir := t.TempDir()
+
+ // Create and archive first session
+ session1, err := store.Start(ctx, workdir)
+ if err != nil {
+ t.Fatalf("Start session1: %v", err)
+ }
+ archived1, err := store.Archive(ctx, session1.SID)
+ if err != nil {
+ t.Fatalf("Archive session1: %v", err)
+ }
+
+ // Create and archive second session (most recent)
+ session2, err := store.Start(ctx, workdir)
+ if err != nil {
+ t.Fatalf("Start session2: %v", err)
+ }
+ archived2, err := store.Archive(ctx, session2.SID)
+ if err != nil {
+ t.Fatalf("Archive session2: %v", err)
+ }
+
+ // Corrupt the most recent session by deleting its meta key
+ // This simulates a missing/corrupted document
+ err = database.Update(ctx, func(txn *db.Txn) error {
+ metaKey := db.KeySessionMeta(archived2.SID)
+ return txn.Delete(metaKey)
+ })
+ if err != nil {
+ t.Fatalf("Delete meta: %v", err)
+ }
+
+ // LatestArchivedByPath should skip the corrupted session and find the older valid one
+ latest, found, err := store.LatestArchivedByPath(ctx, workdir)
+ if err != nil {
+ t.Fatalf("LatestArchivedByPath: %v", err)
+ }
+ if !found {
+ t.Fatalf("expected to find archived session (should skip corrupted and find older)")
+ }
+ if latest.SID != archived1.SID {
+ t.Fatalf("expected to find session1 %s, got %s", archived1.SID, latest.SID)
+ }
+}
+