This repository has no description
0

Configure Feed

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

core / knotmirror / xrpc / repo_blob.go
4.3 kB 165 lines
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.ParseDID(repoQuery) 28 if err != nil { 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("method", "repo.blob", "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 size, reader, 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 defer reader.Close() 77 78 if size > 1000*1000 { // 1MB 79 fileTooLarge := true 80 writeJson(w, http.StatusOK, tangled.RepoBlob_Output{ 81 Ref: ref, 82 Path: path, 83 Size: &size, 84 FileTooLarge: &fileTooLarge, 85 }) 86 return 87 } 88 89 contents, err := io.ReadAll(reader) 90 if err != nil { 91 l.Error("failed to read blob content", "err", err) 92 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to read the blob"}) 93 return 94 } 95 96 mimeType := http.DetectContentType(contents) 97 // override MIME types for formats that http.DetectContentType does not recognize 98 switch filepath.Ext(path) { 99 case ".svg": 100 mimeType = "image/svg+xml" 101 case ".avif": 102 mimeType = "image/avif" 103 case ".jxl": 104 mimeType = "image/jxl" 105 case ".heic", ".heif": 106 mimeType = "image/heif" 107 } 108 109 isBinary := !(strings.HasPrefix(mimeType, "text/") || isTextualMimeType(mimeType)) 110 111 // include content for text blob or svg 112 var content *string 113 if !isBinary { 114 content = new(string) 115 *content = string(contents) 116 } else if filepath.Ext(path) == ".svg" { 117 content = new(string) 118 *content = base64.StdEncoding.EncodeToString(contents) 119 } 120 121 response := tangled.RepoBlob_Output{ 122 Ref: ref, 123 Path: path, 124 Size: &size, 125 IsBinary: &isBinary, 126 Content: content, 127 } 128 129 ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) 130 defer cancel() 131 132 lastCommit, err := gr.LastCommitFile(ctx, path) 133 if err == nil && lastCommit != nil { 134 response.LastCommit = &tangled.RepoBlob_LastCommit{ 135 Hash: lastCommit.Hash.String(), 136 Message: lastCommit.Message, 137 When: lastCommit.When.Format(time.RFC3339), 138 } 139 140 // try to get author information 141 commit, err := gr.Commit(lastCommit.Hash) 142 if err == nil { 143 response.LastCommit.Author = &tangled.RepoBlob_Signature{ 144 Name: commit.Author.Name, 145 Email: commit.Author.Email, 146 } 147 } 148 } 149 150 writeJson(w, http.StatusOK, response) 151} 152 153func (x *Xrpc) getRepo(ctx context.Context, repo syntax.DID, ref string) (*git.GitRepo, error) { 154 repoPath, err := x.makeRepoPath(ctx, repo) 155 if err != nil { 156 return nil, fmt.Errorf("resolving repo did: %w", err) 157 } 158 159 gr, err := git.Open(repoPath, ref) 160 if err != nil { 161 return nil, fmt.Errorf("opening git repo: %w", err) 162 } 163 164 return gr, nil 165}