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 "context"
5 "errors"
6 "net/http"
7 "strconv"
8 "time"
9
10 "amadan.net/oficina/home/roster"
11 "amadan.net/rastrillo/rastrillo"
12 "gorm.io/gorm"
13)
14
15func (s *Store) rememberResponse(ctx context.Context, p permission, docs ...*Document) {
16 if s.team == nil {
17 return
18 }
19 a, ok := ctx.Value(accessContextKey{}).(*requestAccess)
20 if !ok || a == nil || a.runtime != s.team {
21 return
22 }
23 if a.responseDocs == nil {
24 a.responseDocs = map[int64]permission{}
25 }
26 a.responseStore = s
27 for _, doc := range docs {
28 if prior, ok := a.responseDocs[doc.ID]; !ok || p > prior {
29 a.responseDocs[doc.ID] = p
30 }
31 }
32}
33func (a *requestAccess) checkResponse(ctx context.Context) error {
34 if a.responseStore == nil {
35 return errTeamUnavailable
36 }
37 if len(a.responseDocs) == 0 && a.state() == roster.Unknown {
38 return errTeamUnavailable
39 }
40 for id, p := range a.responseDocs {
41 if e := a.responseStore.checkPermission(ctx, a.responseStore.db.WithContext(ctx), a.actor, id, p); e != nil {
42 return e
43 }
44 }
45 return nil
46}
47
48// writeGuardedBody checks each bounded chunk after rendering has completed.
49// Once headers are sent, refusal must abort the transport rather than produce
50// a successful short body with a sequence number for bytes never received.
51func writeGuardedBody(w http.ResponseWriter, r *http.Request, status int, parts ...[]byte) error {
52 a, team := r.Context().Value(accessContextKey{}).(*requestAccess)
53 if !team || a == nil {
54 if status != 0 {
55 w.WriteHeader(status)
56 }
57 for _, part := range parts {
58 if _, e := w.Write(part); e != nil {
59 return e
60 }
61 }
62 return nil
63 }
64 fail := func(e error) {
65 w.Header().Del("Docs-Seq")
66 w.Header().Del("Content-Disposition")
67 w.Header().Del("Content-Length")
68 code := http.StatusServiceUnavailable
69 key := "docs.error.generic"
70 if errors.Is(e, gorm.ErrRecordNotFound) {
71 code = http.StatusNotFound
72 key = "docs.error.notfound"
73 }
74 http.Error(w, rastrillo.T(r, key), code)
75 }
76 controller := http.NewResponseController(w)
77 prepare := func() error {
78 remaining := defaultStreamWriteDeadline
79 if a.state() == roster.Member {
80 lease := a.decision.Lease.Remaining()
81 if lease <= 0 {
82 return errTeamUnavailable
83 }
84 if lease < remaining {
85 remaining = lease
86 }
87 }
88 deadline := time.Now().Add(remaining)
89 ctx, cancel := context.WithDeadline(r.Context(), deadline)
90 defer cancel()
91 if e := a.checkResponse(ctx); e != nil {
92 return e
93 }
94 if ctx.Err() != nil {
95 return errTeamUnavailable
96 }
97 if e := controller.SetWriteDeadline(deadline); e != nil {
98 return errTeamUnavailable
99 }
100 return nil
101 }
102 if e := prepare(); e != nil {
103 fail(e)
104 return nil
105 }
106 total := 0
107 for _, part := range parts {
108 total += len(part)
109 }
110 w.Header().Set("Content-Length", strconv.Itoa(total))
111 if status == 0 {
112 status = http.StatusOK
113 }
114 w.WriteHeader(status)
115 defer controller.SetWriteDeadline(time.Time{})
116 for _, part := range parts {
117 for len(part) > 0 {
118 if e := prepare(); e != nil {
119 panic(http.ErrAbortHandler)
120 }
121 n := min(len(part), 64<<10)
122 wrote, e := w.Write(part[:n])
123 if e != nil || wrote != n {
124 panic(http.ErrAbortHandler)
125 }
126 part = part[n:]
127 }
128 }
129 return nil
130}
131