Monorepo for Tangled tangled.org

knotserver: git: serve raw binary blobs

authored by anirudh.fi and committed by Tangled 53a53a97 04e2bc40

Changed files
+68 -2
knotserver
+31 -1
knotserver/git/git.go
··· 37 37 } 38 38 39 39 var ( 40 - ErrBinaryFile = fmt.Errorf("binary file") 40 + ErrBinaryFile = fmt.Errorf("binary file") 41 + ErrNotBinaryFile = fmt.Errorf("not binary file") 41 42 ) 42 43 43 44 type GitRepo struct { ··· 191 192 } else { 192 193 return "", ErrBinaryFile 193 194 } 195 + } 196 + 197 + func (g *GitRepo) BinContent(path string) ([]byte, error) { 198 + c, err := g.r.CommitObject(g.h) 199 + if err != nil { 200 + return nil, fmt.Errorf("commit object: %w", err) 201 + } 202 + 203 + tree, err := c.Tree() 204 + if err != nil { 205 + return nil, fmt.Errorf("file tree: %w", err) 206 + } 207 + 208 + file, err := tree.File(path) 209 + if err != nil { 210 + return nil, err 211 + } 212 + 213 + isbin, _ := file.IsBinary() 214 + if isbin { 215 + reader, err := file.Reader() 216 + if err != nil { 217 + return nil, fmt.Errorf("opening file reader: %w", err) 218 + } 219 + defer reader.Close() 220 + 221 + return io.ReadAll(reader) 222 + } 223 + return nil, ErrNotBinaryFile 194 224 } 195 225 196 226 func (g *GitRepo) Tags() ([]*TagReference, error) {
+1
knotserver/handler.go
··· 100 100 101 101 r.Route("/blob/{ref}", func(r chi.Router) { 102 102 r.Get("/*", h.Blob) 103 + r.Get("/raw/*", h.BlobRaw) 103 104 }) 104 105 105 106 r.Get("/log/{ref}", h.Log)
+36 -1
knotserver/routes.go
··· 194 194 return 195 195 } 196 196 197 + func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) { 198 + treePath := chi.URLParam(r, "*") 199 + ref := chi.URLParam(r, "ref") 200 + ref, _ = url.PathUnescape(ref) 201 + 202 + l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath) 203 + 204 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 205 + gr, err := git.Open(path, ref) 206 + if err != nil { 207 + notFound(w) 208 + return 209 + } 210 + 211 + contents, err := gr.BinContent(treePath) 212 + if err != nil { 213 + writeError(w, err.Error(), http.StatusBadRequest) 214 + l.Error("file content", "error", err.Error()) 215 + return 216 + } 217 + 218 + mimeType := http.DetectContentType(contents) 219 + 220 + if !strings.HasPrefix(mimeType, "image/") && !strings.HasPrefix(mimeType, "video/") { 221 + l.Error("attempted to serve non-image/video file", "mimetype", mimeType) 222 + writeError(w, "only image and video files can be accessed directly", http.StatusForbidden) 223 + return 224 + } 225 + 226 + w.Header().Set("Cache-Control", "public, max-age=86400") // cache for 24 hours 227 + w.Header().Set("ETag", fmt.Sprintf("%x", sha256.Sum256(contents))) 228 + w.Header().Set("Content-Type", mimeType) 229 + w.Write(contents) 230 + } 231 + 197 232 func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) { 198 233 treePath := chi.URLParam(r, "*") 199 234 ref := chi.URLParam(r, "ref") 200 235 ref, _ = url.PathUnescape(ref) 201 236 202 - l := h.l.With("handler", "FileContent", "ref", ref, "treePath", treePath) 237 + l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath) 203 238 204 239 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 205 240 gr, err := git.Open(path, ref)