Monorepo for Tangled
0

Configure Feed

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

at master 4.6 kB View raw
1package xrpc 2 3import ( 4 "context" 5 "encoding/base64" 6 "fmt" 7 "io" 8 "net/http" 9 "path/filepath" 10 "strings" 11 "time" 12 13 "github.com/bluesky-social/indigo/atproto/atclient" 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 "tangled.org/core/api/tangled" 16 "tangled.org/core/knotserver/git" 17) 18 19// TODO(boltless): rewrite lexicon in new NSID 20func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) { 21 var ( 22 repoQuery = r.URL.Query().Get("repo") 23 ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this) 24 path = r.URL.Query().Get("path") 25 ) 26 27 repo, err := syntax.ParseATURI(repoQuery) 28 if err != nil || repo.RecordKey() == "" { 29 writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)}) 30 return 31 } 32 33 l := x.logger.With("repo", repo, "ref", ref, "path", path) 34 35 if path == "" { 36 writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: "missing path parameter"}) 37 return 38 } 39 40 gr, err := x.getRepo(r.Context(), repo, ref) 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 50 // first check if this path is a submodule 51 submodule, err := gr.Submodule(path) 52 if err != nil { 53 // this is okay, continue and try to treat it as a regular file 54 } else { 55 writeJson(w, http.StatusOK, tangled.RepoBlob_Output{ 56 Ref: ref, 57 Path: path, 58 Submodule: &tangled.RepoBlob_Submodule{ 59 Name: submodule.Name, 60 Url: submodule.URL, 61 Branch: &submodule.Branch, 62 }, 63 }) 64 return 65 } 66 67 file, err := x.getFile(r.Context(), repo, ref, path) 68 if err != nil { 69 l.Warn("local mirror failed, trying proxy", "err", err) 70 if x.proxyToKnot(w, r, repo) { 71 return 72 } 73 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get blob"}) 74 return 75 } 76 77 if file.Size > 1000*1000 { // 1MB 78 fileTooLarge := true 79 writeJson(w, http.StatusOK, tangled.RepoBlob_Output{ 80 Ref: ref, 81 Path: path, 82 Size: &file.Size, 83 FileTooLarge: &fileTooLarge, 84 }) 85 return 86 } 87 88 reader, err := file.Reader() 89 if err != nil { 90 l.Error("failed to read blob", "err", err) 91 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to read the blob"}) 92 return 93 } 94 contents, err := io.ReadAll(reader) 95 if err != nil { 96 l.Error("failed to read blob content", "err", err) 97 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to read the blob"}) 98 return 99 } 100 101 mimeType := http.DetectContentType(contents) 102 // override MIME types for formats that http.DetectContentType does not recognize 103 switch filepath.Ext(path) { 104 case ".svg": 105 mimeType = "image/svg+xml" 106 case ".avif": 107 mimeType = "image/avif" 108 case ".jxl": 109 mimeType = "image/jxl" 110 case ".heic", ".heif": 111 mimeType = "image/heif" 112 } 113 114 isBinary := !(strings.HasPrefix(mimeType, "text/") || isTextualMimeType(mimeType)) 115 116 // include content for text blob or svg 117 var content *string 118 if !isBinary { 119 content = new(string) 120 *content = string(contents) 121 } else if filepath.Ext(path) == ".svg" { 122 content = new(string) 123 *content = base64.StdEncoding.EncodeToString(contents) 124 } 125 126 response := tangled.RepoBlob_Output{ 127 Ref: ref, 128 Path: path, 129 Size: &file.Size, 130 IsBinary: &isBinary, 131 Content: content, 132 } 133 134 ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) 135 defer cancel() 136 137 lastCommit, err := gr.LastCommitFile(ctx, path) 138 if err == nil && lastCommit != nil { 139 response.LastCommit = &tangled.RepoBlob_LastCommit{ 140 Hash: lastCommit.Hash.String(), 141 Message: lastCommit.Message, 142 When: lastCommit.When.Format(time.RFC3339), 143 } 144 145 // try to get author information 146 commit, err := gr.Commit(lastCommit.Hash) 147 if err == nil { 148 response.LastCommit.Author = &tangled.RepoBlob_Signature{ 149 Name: commit.Author.Name, 150 Email: commit.Author.Email, 151 } 152 } 153 } 154 155 writeJson(w, http.StatusOK, response) 156} 157 158func (x *Xrpc) getRepo(ctx context.Context, repo syntax.ATURI, ref string) (*git.GitRepo, error) { 159 repoPath, err := x.makeRepoPath(ctx, repo) 160 if err != nil { 161 return nil, fmt.Errorf("resolving repo at-uri: %w", err) 162 } 163 164 gr, err := git.Open(repoPath, ref) 165 if err != nil { 166 return nil, fmt.Errorf("opening git repo: %w", err) 167 } 168 169 return gr, nil 170}