v0.30.0 c899179

sessions.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package kvstore
4
5import (
6	"context"
7	"crypto/rand"
8	"errors"
9	"fmt"
10
11	kv "bnema.dev/ermite-kv"
12	"github.com/bnema/zerowrap"
13	"google.golang.org/protobuf/proto"
14
15	storagepb "bnema.dev/ermite/gen/ermite/storage"
16	"bnema.dev/ermite/internal/domain"
17	"bnema.dev/ermite/internal/ports"
18)
19
20const (
21	sessionKeyKey   = "session/key"
22	sessionEpochKey = "session/epoch"
23
24	// sessionKeyLen is the size of the HS256 secret, in bytes.
25	sessionKeyLen = 32
26)
27
28// Sessions is the ermite-kv backed ports.SessionStore. It shares the database
29// of the Store it comes from.
30type Sessions struct {
31	db *kv.DB
32}
33
34var _ ports.SessionStore = (*Sessions)(nil)
35
36// Sessions returns the session store over the same database. It is valid
37// until the Store is closed.
38func (s *Store) Sessions() *Sessions { return &Sessions{db: s.db} }
39
40// Key reads session/key and generates it when absent. The key is never
41// logged.
42func (s *Sessions) Key(ctx context.Context) ([]byte, error) {
43	for range updateAttempts {
44		if item, ok := s.db.Get(ctx, []byte(sessionKeyKey)); ok {
45			var m storagepb.SessionKey
46			if err := proto.Unmarshal(item.Value, &m); err != nil {
47				return nil, fmt.Errorf("decode session key: %w", err)
48			}
49			if len(m.GetKey()) != sessionKeyLen {
50				return nil, fmt.Errorf("decode session key: %d bytes, want %d", len(m.GetKey()), sessionKeyLen)
51			}
52			return append([]byte(nil), m.GetKey()...), nil
53		}
54		key := make([]byte, sessionKeyLen)
55		if _, err := rand.Read(key); err != nil {
56			return nil, fmt.Errorf("generate session key: %w", err)
57		}
58		var b kv.Batch
59		b.RequireAbsent([]byte(sessionKeyKey))
60		b.Put([]byte(sessionKeyKey), mustMarshal(&storagepb.SessionKey{Key: key}))
61		_, err := s.db.Apply(ctx, &b)
62		if errors.Is(err, kv.ErrConflict) {
63			continue // written meanwhile: read it
64		}
65		if err != nil {
66			return nil, fmt.Errorf("store session key: %w", err)
67		}
68		log := zerowrap.FromCtx(ctx)
69		log.Info().Msg("session key generated")
70		return key, nil
71	}
72	return nil, fmt.Errorf("store session key: %w", domain.ErrConflict)
73}
74
75// Epoch reads session/epoch; a missing key is epoch 0.
76func (s *Sessions) Epoch(ctx context.Context) (uint64, error) {
77	e, _, err := s.readEpoch(ctx)
78	return e, err
79}
80
81func (s *Sessions) readEpoch(ctx context.Context) (epoch, version uint64, err error) {
82	item, ok := s.db.Get(ctx, []byte(sessionEpochKey))
83	if !ok {
84		return 0, 0, nil
85	}
86	var m storagepb.SessionEpoch
87	if err := proto.Unmarshal(item.Value, &m); err != nil {
88		return 0, 0, fmt.Errorf("decode session epoch: %w", err)
89	}
90	return m.GetEpoch(), item.Version, nil
91}
92
93// BumpEpoch increments session/epoch with a version check, so that two
94// concurrent bumps give two different values.
95func (s *Sessions) BumpEpoch(ctx context.Context) (uint64, error) {
96	for range updateAttempts {
97		cur, version, err := s.readEpoch(ctx)
98		if err != nil {
99			return 0, err
100		}
101		var b kv.Batch
102		if version == 0 {
103			b.RequireAbsent([]byte(sessionEpochKey))
104		} else {
105			b.RequireVersion([]byte(sessionEpochKey), version)
106		}
107		b.Put([]byte(sessionEpochKey), mustMarshal(&storagepb.SessionEpoch{Epoch: cur + 1}))
108		_, err = s.db.Apply(ctx, &b)
109		if errors.Is(err, kv.ErrConflict) {
110			continue
111		}
112		if err != nil {
113			return 0, fmt.Errorf("bump session epoch: %w", err)
114		}
115		return cur + 1, nil
116	}
117	return 0, fmt.Errorf("bump session epoch: %w", domain.ErrConflict)
118}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close