Skip to content

Commit 926dd1f

Browse files
committed
fix: gocache http server demo
1 parent 4104b6a commit 926dd1f

File tree

8 files changed

+281
-14
lines changed

8 files changed

+281
-14
lines changed

.vscode/launch.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@
127127
"sftp://${input:sftphost}/${input:sftpdir}"
128128
]
129129
},
130+
{
131+
"name": "gocache http demo server",
132+
"type": "go",
133+
"request": "launch",
134+
"program": "${workspaceFolder}/addons/gocache/http/server",
135+
},
130136
{
131137
"name": "uninstall k3s",
132138
"type": "go",

addons/gocache/addon.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"time"
78

89
"github.com/spf13/cobra"
910

@@ -76,6 +77,7 @@ func initialize() error {
7677
func makeCmd() *cobra.Command {
7778
writeThrough := true
7879
withDefaults := true
80+
waitForDebugger := false
7981

8082
cmd := &cobra.Command{
8183
Use: "gocache [remote...]",
@@ -144,7 +146,13 @@ func makeCmd() *cobra.Command {
144146
}
145147
frontend := NewFrontend(backend)
146148
s := NewServer(frontend, os.Stdin, os.Stdout)
147-
// time.Sleep(5 * time.Second)
149+
if waitForDebugger {
150+
fmt.Fprintln(os.Stderr, "Waiting for debugger to attach...")
151+
// stick a breakpoint here and use the debugger to change the value
152+
for waitForDebugger {
153+
time.Sleep(100 * time.Millisecond)
154+
}
155+
}
148156
// TODO: signal handlers
149157
return s.Run(cmd.Context())
150158
},
@@ -156,5 +164,8 @@ func makeCmd() *cobra.Command {
156164
fmt.Sprintf("include default remotes (%d) in the list of remotes to use",
157165
len(addon.Config.defaultRemotes)),
158166
)
167+
f.BoolVar(&waitForDebugger, "debug", waitForDebugger,
168+
"wait for a debugger to attach before starting the server")
169+
f.Lookup("debug").Hidden = true
159170
return cmd
160171
}

addons/gocache/http/addon.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ var addon = addons.Addon[config]{
1616
}
1717

1818
type config struct {
19-
auth Authorizer
20-
putMethod string
19+
auth Authorizer
2120
}
2221
type option func(*config)
2322

@@ -27,6 +26,18 @@ type Authorizer interface {
2726
Authorize(req *http.Request) error
2827
}
2928

29+
func WithAuthorizer(a Authorizer) option {
30+
if a == nil {
31+
panic("authorizer must not be nil")
32+
}
33+
return func(c *config) {
34+
if c.auth != nil {
35+
panic("authorizer already set")
36+
}
37+
c.auth = a
38+
}
39+
}
40+
3041
func Configure(opts ...option) {
3142
addon.CheckNotInitialized()
3243
for _, o := range opts {

addons/gocache/http/backend.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ import (
1414
)
1515

1616
type backend struct {
17-
c *http.Client
18-
a Authorizer
19-
base *url.URL
20-
putMethod string
17+
c *http.Client
18+
a Authorizer
19+
base *url.URL
2120
}
2221

2322
func newBackend(
@@ -43,10 +42,9 @@ func newBackend(
4342
a = nopAuthorizer{}
4443
}
4544
return &backend{
46-
c: c,
47-
a: a,
48-
base: u,
49-
putMethod: http.MethodPut,
45+
c: c,
46+
a: a,
47+
base: u,
5048
}, nil
5149
}
5250

addons/gocache/http/factory.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ func (f factory) New(uri string) (gocache.ReadonlyStorageBackend, error) {
2424
if err != nil {
2525
return nil, err
2626
}
27-
if addon.Config.putMethod != "" {
28-
be.putMethod = addon.Config.putMethod
29-
}
3027
return gocache.DiskDirFromFS(be), nil
3128
}
3229

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Demo HTTP Cache Server
2+
3+
This is just a demo. It listens only on localhost and uses a temporary directory
4+
for storage, which it tries to delete before exiting. Do not run it except for
5+
testing.
6+
7+
By default it listens on port 51918 (`0xcace`), but you can change this with the
8+
`PORT` environment variable. You cannot change it from listening only on
9+
localhost.

addons/gocache/http/server/main.go

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"log/slog"
9+
"net/http"
10+
"net/url"
11+
"os"
12+
"os/signal"
13+
"path"
14+
"strconv"
15+
"syscall"
16+
)
17+
18+
func main() {
19+
port := 0xcace // cache without the h
20+
if ps := os.Getenv("PORT"); ps != "" {
21+
if p, err := strconv.ParseInt(ps, 0, 16); err != nil {
22+
panic(err)
23+
} else {
24+
port = int(p)
25+
}
26+
}
27+
lAddr := "localhost:" + strconv.FormatInt(int64(port), 10)
28+
td, err := os.MkdirTemp("", "gdev-gocache-http-")
29+
if err != nil {
30+
panic(err)
31+
}
32+
defer os.RemoveAll(td)
33+
fmt.Println("Using temporary directory:", td)
34+
tdr, err := os.OpenRoot(td)
35+
if err != nil {
36+
panic(err)
37+
}
38+
defer tdr.Close()
39+
40+
sigCh := make(chan os.Signal, 1)
41+
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
42+
defer signal.Stop(sigCh)
43+
44+
slog.SetDefault(slog.New(
45+
slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
46+
Level: slog.LevelInfo,
47+
}),
48+
))
49+
50+
h := &handler{
51+
root: tdr,
52+
log: slog.Default(),
53+
}
54+
55+
fmt.Println("Starting HTTP server at", lAddr)
56+
fmt.Println("Ctrl-C to stop and cleanup")
57+
58+
s := &http.Server{
59+
Addr: lAddr,
60+
Handler: h,
61+
}
62+
63+
go func() {
64+
sig := <-sigCh
65+
fmt.Printf("Received %v, shutting down\n", sig)
66+
s.Shutdown(context.TODO())
67+
}()
68+
69+
if err := s.ListenAndServe(); err != nil {
70+
if !errors.Is(err, http.ErrServerClosed) {
71+
panic(err)
72+
}
73+
}
74+
}
75+
76+
type handler struct {
77+
root *os.Root
78+
log *slog.Logger
79+
}
80+
81+
func (h *handler) prepPath(u *url.URL) string {
82+
// clean the path, make it absolute, and ensure it is relative to the root
83+
p := path.Clean(u.Path)
84+
if !path.IsAbs(p) {
85+
// this should be impossible
86+
panic(fmt.Errorf("path %q is not absolute", p))
87+
}
88+
// make everything relative paths within the root, i.e. /x => ./x
89+
p = "." + p
90+
return p
91+
}
92+
93+
// ServeHTTP implements http.Handler.
94+
func (h *handler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
95+
p := h.prepPath(req.URL)
96+
switch req.Method {
97+
case http.MethodGet, http.MethodHead:
98+
h.getOrHead(res, req, p)
99+
case http.MethodPut:
100+
h.put(res, req, p)
101+
case http.MethodDelete:
102+
h.delete(res, req, p)
103+
case "MOVE":
104+
h.move(res, req, p)
105+
default:
106+
h.err(http.StatusMethodNotAllowed, req, fmt.Errorf(
107+
"unsupported method %q", req.Method,
108+
))
109+
res.WriteHeader(http.StatusMethodNotAllowed)
110+
res.Header().Set("Allow", "GET, HEAD, PUT, DELETE, MOVE")
111+
}
112+
}
113+
114+
func (h *handler) errToHTTP(req *http.Request, res http.ResponseWriter, err error) {
115+
if errors.Is(err, os.ErrNotExist) {
116+
res.WriteHeader(http.StatusNotFound)
117+
h.err(http.StatusNotFound, req, err)
118+
} else if errors.Is(err, os.ErrPermission) {
119+
h.err(http.StatusForbidden, req, err)
120+
res.WriteHeader(http.StatusForbidden)
121+
} else if errors.Is(err, os.ErrExist) || errors.Is(err, syscall.EISDIR) {
122+
h.err(http.StatusConflict, req, err)
123+
res.WriteHeader(http.StatusConflict)
124+
} else {
125+
h.err(http.StatusInternalServerError, req, err)
126+
res.WriteHeader(http.StatusInternalServerError)
127+
}
128+
}
129+
130+
func (h *handler) getOrHead(res http.ResponseWriter, req *http.Request, p string) {
131+
f, err := h.root.Open(p)
132+
if err != nil {
133+
h.errToHTTP(req, res, err)
134+
return
135+
}
136+
defer f.Close()
137+
st, err := f.Stat()
138+
if err != nil {
139+
h.err(http.StatusInternalServerError, req, err)
140+
res.WriteHeader(http.StatusInternalServerError)
141+
return
142+
}
143+
if st.IsDir() {
144+
h.err(http.StatusForbidden, req, fmt.Errorf("cannot serve directory"))
145+
res.WriteHeader(http.StatusForbidden)
146+
return
147+
}
148+
// serve everything as binary
149+
res.Header().Set("Content-Type", "application/octet-stream")
150+
// NOTE: this doesn't compute an etag, since we don't really need it
151+
http.ServeContent(res, req, f.Name(), st.ModTime(), f)
152+
// assume success?
153+
h.ok(http.StatusOK, req)
154+
}
155+
156+
func (h *handler) put(res http.ResponseWriter, req *http.Request, p string) {
157+
// implicit mkdir to keep the api simple
158+
if err := h.root.MkdirAll(path.Dir(p), 0o700); err != nil {
159+
h.errToHTTP(req, res, err)
160+
return
161+
}
162+
f, err := h.root.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
163+
if err != nil {
164+
h.errToHTTP(req, res, err)
165+
return
166+
}
167+
defer f.Close()
168+
if _, err := io.Copy(f, req.Body); err != nil {
169+
h.err(http.StatusInternalServerError, req, err)
170+
res.WriteHeader(http.StatusInternalServerError)
171+
return
172+
}
173+
res.WriteHeader(http.StatusNoContent)
174+
h.ok(http.StatusNoContent, req)
175+
}
176+
177+
func (h *handler) delete(res http.ResponseWriter, req *http.Request, p string) {
178+
if err := h.root.Remove(p); err != nil {
179+
h.errToHTTP(req, res, err)
180+
return
181+
}
182+
res.WriteHeader(http.StatusNoContent)
183+
h.ok(http.StatusNoContent, req)
184+
}
185+
186+
func (h *handler) move(res http.ResponseWriter, req *http.Request, p string) {
187+
dest := req.Header.Get("Destination")
188+
if dest == "" {
189+
res.WriteHeader(http.StatusBadRequest)
190+
return
191+
}
192+
destURL, err := url.Parse(dest)
193+
if err != nil {
194+
h.err(http.StatusBadRequest, req, fmt.Errorf(
195+
"invalid Destination header %q: %w",
196+
dest, err,
197+
))
198+
res.WriteHeader(http.StatusBadRequest)
199+
return
200+
}
201+
if destURL.IsAbs() {
202+
h.err(http.StatusBadRequest, req, fmt.Errorf(
203+
"invalid Destination header %q: must not be an absolute URL",
204+
destURL,
205+
))
206+
res.WriteHeader(http.StatusBadRequest)
207+
return
208+
}
209+
dest = h.prepPath(destURL)
210+
if err := h.root.Rename(p, dest); err != nil {
211+
h.errToHTTP(req, res, err)
212+
return
213+
}
214+
res.WriteHeader(http.StatusNoContent)
215+
h.ok(http.StatusNoContent, req)
216+
}
217+
218+
func (h *handler) err(status int, req *http.Request, err error) {
219+
h.log.Warn("Error",
220+
"status", status,
221+
"method", req.Method,
222+
"path", req.URL.Path,
223+
"err", err,
224+
)
225+
}
226+
227+
func (h *handler) ok(status int, req *http.Request) {
228+
h.log.Info("OK",
229+
"status", status,
230+
"method", req.Method,
231+
"path", req.URL.Path,
232+
)
233+
}

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
gcs_k8s "fastcat.org/go/gdev/addons/gcs/k8s"
1010
"fastcat.org/go/gdev/addons/gocache"
1111
gocache_gcs "fastcat.org/go/gdev/addons/gocache/gcs"
12+
gocache_http "fastcat.org/go/gdev/addons/gocache/http"
1213
gocache_sftp "fastcat.org/go/gdev/addons/gocache/sftp"
1314
"fastcat.org/go/gdev/addons/golang"
1415
"fastcat.org/go/gdev/addons/k3s"
@@ -58,6 +59,7 @@ func main() {
5859
golang.Configure()
5960
gocache_sftp.Configure()
6061
gocache_gcs.Configure()
62+
gocache_http.Configure()
6163
gocache.Configure(
6264
// NOTE: you will not have access to this bucket, it is just here as an
6365
// example and for author testing

0 commit comments

Comments
 (0)