forked from
tangled.org/core
Monorepo for Tangled
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}