This repository has no description
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}