This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knotmirror / xrpc / git_get_blob.go
4.9 kB 168 lines
1package xrpc 2 3import ( 4 "context" 5 "crypto/sha256" 6 "fmt" 7 "io" 8 "net/http" 9 "path/filepath" 10 "slices" 11 "strings" 12 13 "github.com/bluesky-social/indigo/atproto/atclient" 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 "github.com/go-git/go-git/v5/plumbing/object" 16 "tangled.org/core/knotmirror/xrpc/gitea" 17) 18 19func (x *Xrpc) GetBlob(w http.ResponseWriter, r *http.Request) { 20 var ( 21 repoQuery = r.URL.Query().Get("repo") 22 ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this) 23 path = r.URL.Query().Get("path") 24 ) 25 26 repo, err := syntax.ParseDID(repoQuery) 27 if err != nil { 28 writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 29 return 30 } 31 32 l := x.logger.With("method", "git.getBlob", "repo", repo, "ref", ref, "path", path) 33 l.Debug("request") 34 35 if path == "" { 36 writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing path parameter"}) 37 return 38 } 39 40 ctx := r.Context() 41 42 repoPath, err := x.makeRepoPath(ctx, repo) 43 if err != nil { 44 writeJson(w, http.StatusNotFound, atclient.ErrorBody{Name: "RepoNotFound", Message: fmt.Sprintf("unknown repository: %s", repo)}) 45 return 46 } 47 48 entry, err := x.getFile(ctx, repoPath, ref, path) 49 if err != nil { 50 l.Warn("local mirror failed", "err", err) 51 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get blob"}) 52 return 53 } 54 size, reader, err := gitea.ReadBlob(ctx, repoPath, entry.Hash) 55 if err != nil { 56 l.Warn("local mirror failed", "err", err) 57 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get blob"}) 58 return 59 } 60 defer reader.Close() 61 62 // default to octet-stream for large blobs 63 if size > 1000*1000 { // 1MB 64 w.Header().Set("Content-Type", "application/octet-stream") 65 if _, err := io.Copy(w, reader); err != nil { 66 l.Error("failed to serve the blob", "err", err) 67 } 68 return 69 } 70 71 contents, err := io.ReadAll(reader) 72 if err != nil { 73 l.Error("failed to read blob content", "err", err) 74 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to read the blob"}) 75 return 76 } 77 78 mimeType := http.DetectContentType(contents) 79 // override MIME types for formats that http.DetectContentType does not recognize 80 switch filepath.Ext(path) { 81 case ".svg": 82 mimeType = "image/svg+xml" 83 case ".avif": 84 mimeType = "image/avif" 85 case ".jxl": 86 mimeType = "image/jxl" 87 case ".heic", ".heif": 88 mimeType = "image/heif" 89 } 90 91 switch { 92 case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): 93 eTag := fmt.Sprintf("\"%x\"", sha256.Sum256(contents)) 94 if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { 95 w.WriteHeader(http.StatusNotModified) 96 return 97 } 98 w.Header().Set("ETag", eTag) 99 w.Header().Set("Content-Type", mimeType) 100 101 case strings.HasPrefix(mimeType, "text/") || isTextualMimeType(mimeType): 102 w.Header().Set("Cache-Control", "public, no-cache") 103 // serve all text content as text/plain 104 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 105 106 default: 107 l.Error("attempted to serve disallowed file type", "mimetype", mimeType) 108 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InvalidRequest", Message: "only image, video, and text files can be accessed directly"}) 109 return 110 } 111 w.Write(contents) 112} 113 114func (x *Xrpc) getFile(ctx context.Context, repoPath, ref, path string) (*object.TreeEntry, error) { 115 rev := ref 116 if rev == "" { 117 rev = "HEAD" 118 } 119 120 head, err := gitea.GetCommit(ctx, repoPath, rev) 121 if err != nil { 122 return nil, fmt.Errorf("get head commit: %w", err) 123 } 124 125 treePath := filepath.Dir(path) 126 name := filepath.Base(path) 127 128 // find subTree 129 subRev := head.Hash.String() + "^{tree}" 130 if treePath != "." { 131 subRev = head.Hash.String() + ":" + treePath 132 } 133 subTree, err := gitea.GetTree(ctx, repoPath, subRev) 134 if err != nil { 135 return nil, fmt.Errorf("get subtree %s: %w", subRev, err) 136 } 137 138 // find entry 139 entry, err := func(subTree *object.Tree) (*object.TreeEntry, error) { 140 for _, entry := range subTree.Entries { 141 if entry.Name == name { 142 return &entry, nil 143 } 144 } 145 return nil, fmt.Errorf("object doesn't exist") 146 }(subTree) 147 if err != nil { 148 return nil, fmt.Errorf("get file: %w", err) 149 } 150 151 return entry, nil 152} 153 154var textualMimeTypes = []string{ 155 "application/json", 156 "application/xml", 157 "application/yaml", 158 "application/x-yaml", 159 "application/toml", 160 "application/javascript", 161 "application/ecmascript", 162} 163 164// isTextualMimeType returns true if the MIME type represents textual content 165// that should be served as text/plain for security reasons 166func isTextualMimeType(mimeType string) bool { 167 return slices.Contains(textualMimeTypes, mimeType) 168}