oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1package docs
2
3import (
4 "fmt"
5 "sync"
6 "time"
7)
8
9// oneShotTTL is how long a minted public URL waits to be collected. Long
10// enough to survive a 303 and a slow render, short enough that a URL left
11// behind by a browser that never followed the redirect is gone before
12// anybody could come back to the machine and find it.
13const oneShotTTL = 30 * time.Second
14
15// oneShots holds each freshly minted public URL for exactly one read.
16//
17// It exists because of ruling 16 and the redirect, together. The token is
18// shown once and the database cannot produce it again, so the share page
19// has to be handed it — but sharePost answers 303 (a POST that renders its
20// own page is a POST a reload re-submits, and here a re-submit would
21// rotate the link the person is in the middle of copying). So the URL has
22// to survive one redirect and no longer.
23//
24// In memory, and that is consistent rather than lazy: this app is one
25// process on one SQLite file, which is the same assumption hub and
26// previews already make. A restart between the POST and the GET loses the
27// URL; the owner republishes, which is the same recovery as losing it any
28// other way, and no worse than the alternative of writing the plaintext
29// token to the database — which is the whole thing being avoided.
30type oneShots struct {
31 mu sync.Mutex
32 items map[string]oneShot
33}
34
35type oneShot struct {
36 value string
37 expires time.Time
38}
39
40func newOneShots() *oneShots { return &oneShots{items: map[string]oneShot{}} }
41
42// key scopes a pending URL to one person and one document, so a second
43// person on the same document — or the same person on another — can never
44// collect it.
45func oneShotKey(person int64, sqid string) string {
46 return fmt.Sprintf("%d:%s", person, sqid)
47}
48
49// put stores value under key, replacing anything already there. Publishing
50// twice in a row leaves the newer URL, which is the one that works.
51func (o *oneShots) put(key, value string) {
52 o.mu.Lock()
53 defer o.mu.Unlock()
54 o.sweepLocked()
55 o.items[key] = oneShot{value: value, expires: time.Now().Add(oneShotTTL)}
56}
57
58// take returns the value and removes it. A second read returns "", which
59// is what makes this show-once rather than show-until-expiry: a reload of
60// the share page after collecting the URL shows the published state and no
61// URL, exactly as a visit tomorrow would.
62func (o *oneShots) take(key string) string {
63 o.mu.Lock()
64 defer o.mu.Unlock()
65 item, ok := o.items[key]
66 delete(o.items, key)
67 if !ok || time.Now().After(item.expires) {
68 return ""
69 }
70 return item.value
71}
72
73// sweepLocked drops expired entries. Called on put rather than on a timer:
74// the map only grows when somebody publishes, so that is the only moment
75// it can need clearing, and a background goroutine for a map this size
76// would be more machinery than the problem.
77func (o *oneShots) sweepLocked() {
78 now := time.Now()
79 for k, v := range o.items {
80 if now.After(v.expires) {
81 delete(o.items, k)
82 }
83 }
84}
85