Monorepo for Tangled tangled.org
6

Configure Feed

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

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 size, reader, err := x.getFile(r.Context(), repo, ref, path) 41 if err != nil { 42 l.Warn("local mirror failed, trying proxy", "err", err) 43 if x.proxyToKnot(w, r, repo) { 44 return 45 } 46 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get blob"}) 47 return 48 } 49 defer reader.Close() 50 51 // default to octet-stream for large blobs 52 if size > 1000*1000 { // 1MB 53 w.Header().Set("Content-Type", "application/octet-stream") 54 if _, err := io.Copy(w, reader); err != nil { 55 l.Error("failed to serve the blob", "err", err) 56 } 57 return 58 } 59 60 contents, err := io.ReadAll(reader) 61 if err != nil { 62 l.Error("failed to read blob content", "err", err) 63 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to read the blob"}) 64 return 65 } 66 67 mimeType := http.DetectContentType(contents) 68 // override MIME types for formats that http.DetectContentType does not recognize 69 switch filepath.Ext(path) { 70 case ".svg": 71 mimeType = "image/svg+xml" 72 case ".avif": 73 mimeType = "image/avif" 74 case ".jxl": 75 mimeType = "image/jxl" 76 case ".heic", ".heif": 77 mimeType = "image/heif" 78 } 79 80 switch { 81 case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): 82 eTag := fmt.Sprintf("\"%x\"", sha256.Sum256(contents)) 83 if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { 84 w.WriteHeader(http.StatusNotModified) 85 return 86 } 87 w.Header().Set("ETag", eTag) 88 w.Header().Set("Content-Type", mimeType) 89 90 case strings.HasPrefix(mimeType, "text/") || isTextualMimeType(mimeType): 91 w.Header().Set("Cache-Control", "public, no-cache") 92 // serve all text content as text/plain 93 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 94 95 default: 96 l.Error("attempted to serve disallowed file type", "mimetype", mimeType) 97 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InvalidRequest", Message: "only image, video, and text files can be accessed directly"}) 98 return 99 } 100 w.Write(contents) 101} 102 103func (x *Xrpc) getFile(ctx context.Context, repo syntax.DID, ref, path string) (int64, io.ReadCloser, error) { 104 repoPath, err := x.makeRepoPath(ctx, repo) 105 if err != nil { 106 return 0, nil, fmt.Errorf("resolving repo did: %w", err) 107 } 108 109 rev := ref 110 if rev == "" { 111 rev = "HEAD" 112 } 113 114 head, err := gitea.GetCommit(ctx, repoPath, rev) 115 if err != nil { 116 return 0, nil, fmt.Errorf("get head commit: %w", err) 117 } 118 119 treePath := filepath.Dir(path) 120 name := filepath.Base(path) 121 122 // find subTree 123 subRev := head.Hash.String() + "^{tree}" 124 if treePath != "." { 125 subRev = head.Hash.String() + ":" + treePath 126 } 127 subTree, err := gitea.GetTree(ctx, repoPath, subRev) 128 if err != nil { 129 return 0, nil, fmt.Errorf("get subtree %s: %w", subRev, err) 130 } 131 132 // find entry 133 entry, err := func(subTree *object.Tree) (*object.TreeEntry, error) { 134 for _, entry := range subTree.Entries { 135 if entry.Name == name { 136 return &entry, nil 137 } 138 } 139 return nil, fmt.Errorf("object doesn't exist") 140 }(subTree) 141 if err != nil { 142 return 0, nil, fmt.Errorf("get file: %w", err) 143 } 144 145 x.logger.Debug("ReadBlob", "name", entry.Name, "mode", entry.Mode.String(), "hash", entry.Hash.String()) 146 147 // find blob 148 return gitea.ReadBlob(ctx, repoPath, entry.Hash) 149} 150 151var textualMimeTypes = []string{ 152 "application/json", 153 "application/xml", 154 "application/yaml", 155 "application/x-yaml", 156 "application/toml", 157 "application/javascript", 158 "application/ecmascript", 159} 160 161// isTextualMimeType returns true if the MIME type represents textual content 162// that should be served as text/plain for security reasons 163func isTextualMimeType(mimeType string) bool { 164 return slices.Contains(textualMimeTypes, mimeType) 165}