File view with blame information shown in the left gutter beside each line.

barerepo / server / internal/httpd/card.go
197 lines · 5.4kb · master
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package httpd
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
_ "embed"
133728e barerepo 1mo
5
"image"
133728e barerepo 1mo
6
"image/color"
133728e barerepo 1mo
7
"image/draw"
133728e barerepo 1mo
8
"image/png"
133728e barerepo 1mo
9
"net/http"
133728e barerepo 1mo
10
"strconv"
133728e barerepo 1mo
11
"strings"
133728e barerepo 1mo
12
"sync"
133728e barerepo 1mo
13
133728e barerepo 1mo
14
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
15
"github.com/barerepo/server/internal/transport"
133728e barerepo 1mo
16
133728e barerepo 1mo
17
"golang.org/x/image/font"
133728e barerepo 1mo
18
"golang.org/x/image/font/opentype"
133728e barerepo 1mo
19
"golang.org/x/image/math/fixed"
133728e barerepo 1mo
20
)
133728e barerepo 1mo
21
133728e barerepo 1mo
22
//go:embed static/fonts/SpaceMono-Regular.ttf
133728e barerepo 1mo
23
var cardRegular []byte
133728e barerepo 1mo
24
133728e barerepo 1mo
25
//go:embed static/fonts/SpaceMono-Bold.ttf
133728e barerepo 1mo
26
var cardBold []byte
133728e barerepo 1mo
27
133728e barerepo 1mo
28
const (
133728e barerepo 1mo
29
cardW, cardH = 1200, 630
133728e barerepo 1mo
30
cardInset = 48
133728e barerepo 1mo
31
cardLeft = 104
133728e barerepo 1mo
32
)
133728e barerepo 1mo
33
133728e barerepo 1mo
34
var (
133728e barerepo 1mo
35
cardOnce sync.Once
133728e barerepo 1mo
36
cardFaces map[int]font.Face
133728e barerepo 1mo
37
cardFaceErr error
133728e barerepo 1mo
38
)
133728e barerepo 1mo
39
133728e barerepo 1mo
40
func cardFace(size int, bold bool) font.Face {
133728e barerepo 1mo
41
cardOnce.Do(func() {
133728e barerepo 1mo
42
cardFaces = map[int]font.Face{}
133728e barerepo 1mo
43
for _, spec := range []struct {
133728e barerepo 1mo
44
key int
133728e barerepo 1mo
45
size float64
133728e barerepo 1mo
46
data []byte
133728e barerepo 1mo
47
}{
133728e barerepo 1mo
48
{112, 112, cardBold}, {76, 76, cardBold}, {26, 26, cardRegular}, {28, 28, cardRegular},
133728e barerepo 1mo
49
} {
133728e barerepo 1mo
50
f, err := opentype.Parse(spec.data)
133728e barerepo 1mo
51
if err != nil {
133728e barerepo 1mo
52
cardFaceErr = err
133728e barerepo 1mo
53
return
133728e barerepo 1mo
54
}
133728e barerepo 1mo
55
face, err := opentype.NewFace(f, &opentype.FaceOptions{Size: spec.size, DPI: 72, Hinting: font.HintingFull})
133728e barerepo 1mo
56
if err != nil {
133728e barerepo 1mo
57
cardFaceErr = err
133728e barerepo 1mo
58
return
133728e barerepo 1mo
59
}
133728e barerepo 1mo
60
cardFaces[spec.key] = face
133728e barerepo 1mo
61
}
133728e barerepo 1mo
62
})
133728e barerepo 1mo
63
return cardFaces[size]
133728e barerepo 1mo
64
}
133728e barerepo 1mo
65
133728e barerepo 1mo
66
// cardText draws one line and reports where the next one starts.
133728e barerepo 1mo
67
func cardText(dst *image.RGBA, x, y, size int, s string, c color.Color) {
133728e barerepo 1mo
68
face := cardFace(size, false)
133728e barerepo 1mo
69
if face == nil {
133728e barerepo 1mo
70
return
133728e barerepo 1mo
71
}
133728e barerepo 1mo
72
d := font.Drawer{Dst: dst, Src: image.NewUniform(c), Face: face,
133728e barerepo 1mo
73
Dot: fixed.P(x, y)}
133728e barerepo 1mo
74
d.DrawString(s)
133728e barerepo 1mo
75
}
133728e barerepo 1mo
76
133728e barerepo 1mo
77
// cardFits shortens a line that would run past the frame, because a card cannot scroll.
133728e barerepo 1mo
78
func cardFits(s string, size, width int) string {
133728e barerepo 1mo
79
face := cardFace(size, false)
133728e barerepo 1mo
80
if face == nil {
133728e barerepo 1mo
81
return s
133728e barerepo 1mo
82
}
133728e barerepo 1mo
83
if font.MeasureString(face, s).Ceil() <= width {
133728e barerepo 1mo
84
return s
133728e barerepo 1mo
85
}
133728e barerepo 1mo
86
for len(s) > 1 {
133728e barerepo 1mo
87
s = s[:len(s)-1]
133728e barerepo 1mo
88
if font.MeasureString(face, s+"...").Ceil() <= width {
133728e barerepo 1mo
89
return s + "..."
133728e barerepo 1mo
90
}
133728e barerepo 1mo
91
}
133728e barerepo 1mo
92
return s
133728e barerepo 1mo
93
}
133728e barerepo 1mo
94
133728e barerepo 1mo
95
// drawCard is the share image for one page, in the same ink as the pages themselves.
133728e barerepo 1mo
96
func drawCard(title, subtitle, foot string) (*image.RGBA, error) {
133728e barerepo 1mo
97
if cardFace(112, true); cardFaceErr != nil {
133728e barerepo 1mo
98
return nil, cardFaceErr
133728e barerepo 1mo
99
}
133728e barerepo 1mo
100
img := image.NewRGBA(image.Rect(0, 0, cardW, cardH))
133728e barerepo 1mo
101
draw.Draw(img, img.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
133728e barerepo 1mo
102
133728e barerepo 1mo
103
border := color.RGBA{0xdd, 0xdd, 0xdd, 0xff}
133728e barerepo 1mo
104
frame := image.Rect(cardInset, cardInset, cardW-cardInset, cardH-cardInset)
133728e barerepo 1mo
105
for x := frame.Min.X; x < frame.Max.X; x++ {
133728e barerepo 1mo
106
img.Set(x, frame.Min.Y, border)
133728e barerepo 1mo
107
img.Set(x, frame.Min.Y+1, border)
133728e barerepo 1mo
108
img.Set(x, frame.Max.Y-1, border)
133728e barerepo 1mo
109
img.Set(x, frame.Max.Y-2, border)
133728e barerepo 1mo
110
}
133728e barerepo 1mo
111
for y := frame.Min.Y; y < frame.Max.Y; y++ {
133728e barerepo 1mo
112
img.Set(frame.Min.X, y, border)
133728e barerepo 1mo
113
img.Set(frame.Min.X+1, y, border)
133728e barerepo 1mo
114
img.Set(frame.Max.X-1, y, border)
133728e barerepo 1mo
115
img.Set(frame.Max.X-2, y, border)
133728e barerepo 1mo
116
}
133728e barerepo 1mo
117
133728e barerepo 1mo
118
width := cardW - cardLeft - cardInset - 56
133728e barerepo 1mo
119
size := 112
133728e barerepo 1mo
120
if len(title) > 12 {
133728e barerepo 1mo
121
size = 76
133728e barerepo 1mo
122
}
133728e barerepo 1mo
123
cardText(img, cardLeft, 232, size, cardFits(title, size, width), color.Black)
133728e barerepo 1mo
124
if subtitle != "" {
133728e barerepo 1mo
125
cardText(img, cardLeft+2, 296, 28, cardFits(subtitle, 28, width), color.RGBA{0x55, 0x55, 0x55, 0xff})
133728e barerepo 1mo
126
}
133728e barerepo 1mo
127
for x := cardLeft; x < cardW-cardInset-56; x++ {
133728e barerepo 1mo
128
img.Set(x, 360, border)
133728e barerepo 1mo
129
img.Set(x, 361, border)
133728e barerepo 1mo
130
}
133728e barerepo 1mo
131
cardText(img, cardLeft+2, 530, 26, cardFits(foot, 26, width), color.Black)
133728e barerepo 1mo
132
return img, nil
133728e barerepo 1mo
133
}
133728e barerepo 1mo
134
133728e barerepo 1mo
135
// serveCard draws the share image for a page, so a link to it says whose it is.
133728e barerepo 1mo
136
func (s *Server) serveCard(w http.ResponseWriter, r *http.Request, title, subtitle, foot string) {
133728e barerepo 1mo
137
img, err := drawCard(title, subtitle, foot)
133728e barerepo 1mo
138
if err != nil {
133728e barerepo 1mo
139
s.oops(w, r, err)
133728e barerepo 1mo
140
return
133728e barerepo 1mo
141
}
133728e barerepo 1mo
142
w.Header().Set("Content-Type", "image/png")
133728e barerepo 1mo
143
w.Header().Set("Cache-Control", "public, max-age=300")
133728e barerepo 1mo
144
png.Encode(w, img)
133728e barerepo 1mo
145
}
133728e barerepo 1mo
146
133728e barerepo 1mo
147
// cardHost is the domain a card signs itself with, without the scheme.
133728e barerepo 1mo
148
func cardHost(site string) string {
133728e barerepo 1mo
149
host := strings.TrimPrefix(strings.TrimPrefix(site, "https://"), "http://")
133728e barerepo 1mo
150
return strings.TrimSuffix(host, "/")
133728e barerepo 1mo
151
}
133728e barerepo 1mo
152
133728e barerepo 1mo
153
// serveNamedCard draws the share image for an account or a repository, and refuses anything else.
133728e barerepo 1mo
154
func (s *Server) serveNamedCard(w http.ResponseWriter, r *http.Request, parts []string) {
133728e barerepo 1mo
155
foot := cardHost(s.Cfg.Server.ExternalURL)
133728e barerepo 1mo
156
switch len(parts) {
133728e barerepo 1mo
157
case 1:
133728e barerepo 1mo
158
owner := parts[0]
133728e barerepo 1mo
159
if !gitx.ValidName(owner) {
133728e barerepo 1mo
160
s.notFound(w, r)
133728e barerepo 1mo
161
return
133728e barerepo 1mo
162
}
133728e barerepo 1mo
163
if _, err := s.DB.Account(r.Context(), owner); err != nil {
133728e barerepo 1mo
164
s.notFound(w, r)
133728e barerepo 1mo
165
return
133728e barerepo 1mo
166
}
133728e barerepo 1mo
167
// Only what this reader may see, so a card cannot count somebody's private work.
133728e barerepo 1mo
168
names, _ := s.DB.ReposOf(r.Context(), owner)
133728e barerepo 1mo
169
n := 0
133728e barerepo 1mo
170
for _, repo := range names {
133728e barerepo 1mo
171
if _, err := s.Transport.Open(r.Context(), owner, repo, s.viewer(r), transport.Read); err == nil {
133728e barerepo 1mo
172
n++
133728e barerepo 1mo
173
}
133728e barerepo 1mo
174
}
133728e barerepo 1mo
175
s.serveCard(w, r, owner, plural(n, "repository", "repositories"), foot+"/"+owner)
133728e barerepo 1mo
176
case 2:
133728e barerepo 1mo
177
owner, name := parts[0], parts[1]
133728e barerepo 1mo
178
if !gitx.ValidName(owner) || !gitx.ValidRepoName(name) {
133728e barerepo 1mo
179
s.notFound(w, r)
133728e barerepo 1mo
180
return
133728e barerepo 1mo
181
}
133728e barerepo 1mo
182
// A card is public, so it must not say a private repository exists.
133728e barerepo 1mo
183
res, err := s.Transport.Open(r.Context(), owner, name, s.viewer(r), transport.Read)
133728e barerepo 1mo
184
if err != nil || res == nil {
133728e barerepo 1mo
185
s.notFound(w, r)
133728e barerepo 1mo
186
return
133728e barerepo 1mo
187
}
133728e barerepo 1mo
188
// The count, not the description, so the line is the same shape as the profile card's.
133728e barerepo 1mo
189
n := 0
133728e barerepo 1mo
190
if out, err := gitx.Run(r.Context(), res.Dir, "rev-list", "--count", "HEAD"); err == nil {
133728e barerepo 1mo
191
n, _ = strconv.Atoi(strings.TrimSpace(out))
133728e barerepo 1mo
192
}
133728e barerepo 1mo
193
s.serveCard(w, r, owner+"/"+name, plural(n, "commit", "commits"), foot+"/"+owner+"/"+name)
133728e barerepo 1mo
194
default:
133728e barerepo 1mo
195
s.notFound(w, r)
133728e barerepo 1mo
196
}
133728e barerepo 1mo
197
}
history · rawbarerepo 0.1.0