Skip to content

Commit 228a051

Browse files
committed
feat: serve .deb files from GitHub releases
1 parent 6a7211f commit 228a051

File tree

3 files changed

+152
-19
lines changed

3 files changed

+152
-19
lines changed

go.mod

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,29 @@ go 1.23.4
55
require (
66
calmh.dev/proxy v0.0.0-20241220141533-76ceaea4b336
77
github.com/prometheus/client_golang v1.22.0
8+
github.com/syncthing/syncthing v1.29.7
9+
github.com/thejerf/suture/v4 v4.0.6
810
)
911

1012
require (
1113
github.com/beorn7/perks v1.0.1 // indirect
1214
github.com/cespare/xxhash/v2 v2.3.0 // indirect
15+
github.com/ebitengine/purego v0.8.3 // indirect
16+
github.com/go-ole/go-ole v1.3.0 // indirect
1317
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
18+
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
1419
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
20+
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
1521
github.com/prometheus/client_model v0.6.1 // indirect
1622
github.com/prometheus/common v0.62.0 // indirect
1723
github.com/prometheus/procfs v0.15.1 // indirect
18-
golang.org/x/sync v0.10.0 // indirect
19-
golang.org/x/sys v0.30.0 // indirect
20-
google.golang.org/protobuf v1.36.5 // indirect
24+
github.com/shirou/gopsutil/v4 v4.25.4 // indirect
25+
github.com/tklauser/go-sysconf v0.3.14 // indirect
26+
github.com/tklauser/numcpus v0.9.0 // indirect
27+
github.com/yusufpapurcu/wmi v1.2.4 // indirect
28+
golang.org/x/net v0.40.0 // indirect
29+
golang.org/x/sync v0.14.0 // indirect
30+
golang.org/x/sys v0.33.0 // indirect
31+
golang.org/x/text v0.25.0 // indirect
32+
google.golang.org/protobuf v1.36.6 // indirect
2133
)

go.sum

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,28 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
66
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
77
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
88
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9+
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
10+
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
11+
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
12+
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
13+
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
914
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
1015
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
16+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
1117
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
1218
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
1319
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
1420
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
1521
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
1622
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
23+
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
24+
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
1725
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
1826
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
1927
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2028
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
29+
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
30+
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
2131
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
2232
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
2333
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
@@ -26,13 +36,32 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
2636
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
2737
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
2838
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
39+
github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
40+
github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
2941
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
3042
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
31-
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
32-
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
33-
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
34-
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
35-
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
36-
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
43+
github.com/syncthing/syncthing v1.29.7 h1:Jq+gEDSuNRAwkm+dvH68rwBMLiXJKWPB4Erh8EM2SE4=
44+
github.com/syncthing/syncthing v1.29.7/go.mod h1:fdJbdWqmQsJa7bWrqZJQOAuednqtxCNgNpK8tNQS9fw=
45+
github.com/thejerf/suture/v4 v4.0.6 h1:QsuCEsCqb03xF9tPAsWAj8QOAJBgQI1c0VqJNaingg8=
46+
github.com/thejerf/suture/v4 v4.0.6/go.mod h1:gu9Y4dXNUWFrByqRt30Rm9/UZ0wzRSt9AJS6xu/ZGxU=
47+
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
48+
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
49+
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
50+
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
51+
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
52+
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
53+
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
54+
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
55+
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
56+
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
57+
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
58+
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
59+
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
60+
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
61+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
62+
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
63+
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
64+
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
65+
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
3766
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
3867
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,23 @@ package main
22

33
import (
44
"cmp"
5+
"context"
56
"embed"
7+
"encoding/json"
68
"io/fs"
79
"log/slog"
810
"net/http"
911
"net/http/httputil"
1012
"net/url"
1113
"os"
1214
"path"
15+
"sync"
1316
"time"
1417

1518
"calmh.dev/proxy"
1619
"github.com/prometheus/client_golang/prometheus/promhttp"
20+
"github.com/syncthing/syncthing/lib/upgrade"
21+
"github.com/thejerf/suture/v4"
1722
)
1823

1924
//go:embed site
@@ -26,16 +31,31 @@ var (
2631
)
2732

2833
func main() {
34+
main := suture.NewSimple("main")
35+
36+
// The built in FS serves static files from memory
2937
subFS, _ := fs.Sub(fs.FS(siteFS), "site")
3038
site := http.FS(subFS)
3139
http.Handle("/", http.FileServer(site))
3240

41+
// The caching proxy serves files from the backend object store
3342
proxy, err := newCachingProxy(distsHost, 5*time.Minute)
3443
if err != nil {
3544
slog.Error("failed to construct proxy", "error", err)
3645
os.Exit(2)
3746
}
38-
filtered := validateFilename(proxy, []string{
47+
48+
// The GitHub redirector serves assets from GitHub releases
49+
github := &githubRedirector{
50+
releasesURL: "https://api.github.com/repos/syncthing/syncthing/releases?per_page=25",
51+
refreshInterval: 5 * time.Minute,
52+
next: proxy,
53+
}
54+
main.Add(github)
55+
56+
// We slightly filter which files we're willing to even try to serve
57+
filtered := validateFilename(github, []string{
58+
"*.deb",
3959
"InRelease",
4060
"InRelease.gz",
4161
"Release",
@@ -44,19 +64,18 @@ func main() {
4464
"Release.gpg.gz",
4565
"Packages",
4666
"Packages.gz",
47-
"*.deb",
4867
})
4968
http.Handle("/dists/", filtered)
5069

51-
go func() {
52-
if err := http.ListenAndServe(metricsListenAddr, promhttp.Handler()); err != nil {
53-
slog.Error("failed to listen", "server", "metrics", "error", err)
54-
}
55-
}()
70+
main.Add(asService(func(_ context.Context) error {
71+
return http.ListenAndServe(metricsListenAddr, promhttp.Handler())
72+
}))
5673

57-
if err := http.ListenAndServe(listenAddr, nil); err != nil {
58-
slog.Error("failed to listen", "server", "main", "error", err)
59-
}
74+
main.Add(asService(func(_ context.Context) error {
75+
return http.ListenAndServe(listenAddr, nil)
76+
}))
77+
78+
main.Serve(context.Background())
6079
}
6180

6281
func validateFilename(next http.Handler, names []string) http.Handler {
@@ -85,3 +104,76 @@ func newCachingProxy(next string, cacheTime time.Duration) (http.Handler, error)
85104

86105
return proxy.New(cacheTime, 100, rev), nil
87106
}
107+
108+
type githubRedirector struct {
109+
releasesURL string
110+
refreshInterval time.Duration
111+
112+
mut sync.Mutex
113+
assets map[string]string
114+
next http.Handler
115+
}
116+
117+
func (r *githubRedirector) Serve(ctx context.Context) error {
118+
timer := time.NewTimer(0)
119+
defer timer.Stop()
120+
for {
121+
select {
122+
case <-timer.C:
123+
assets, err := r.fetchGithubReleaseAssets(ctx, r.releasesURL)
124+
if err != nil {
125+
return err
126+
}
127+
r.mut.Lock()
128+
r.assets = assets
129+
r.mut.Unlock()
130+
timer.Reset(r.refreshInterval)
131+
case <-ctx.Done():
132+
return ctx.Err()
133+
}
134+
}
135+
}
136+
137+
func (r *githubRedirector) ServeHTTP(w http.ResponseWriter, req *http.Request) {
138+
file := path.Base(req.URL.Path)
139+
r.mut.Lock()
140+
url, ok := r.assets[file]
141+
r.mut.Unlock()
142+
if ok {
143+
http.Redirect(w, req, url, http.StatusTemporaryRedirect)
144+
return
145+
}
146+
r.next.ServeHTTP(w, req)
147+
}
148+
149+
func (r *githubRedirector) fetchGithubReleaseAssets(ctx context.Context, url string) (map[string]string, error) {
150+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
151+
if err != nil {
152+
return nil, err
153+
}
154+
resp, err := http.DefaultClient.Do(req)
155+
if err != nil {
156+
return nil, err
157+
}
158+
defer resp.Body.Close()
159+
var rels []upgrade.Release
160+
if err := json.NewDecoder(resp.Body).Decode(&rels); err != nil {
161+
return nil, err
162+
}
163+
164+
assets := make(map[string]string)
165+
for _, rel := range rels {
166+
for _, asset := range rel.Assets {
167+
if path.Ext(asset.Name) == ".deb" {
168+
assets[asset.Name] = asset.BrowserURL
169+
}
170+
}
171+
}
172+
return assets, nil
173+
}
174+
175+
type asService func(ctx context.Context) error
176+
177+
func (fn asService) Serve(ctx context.Context) error {
178+
return fn(ctx)
179+
}

0 commit comments

Comments
 (0)