This repository has no description
1package repo
2
3import (
4 "encoding/base64"
5 "fmt"
6 "net/http"
7 "net/url"
8 "path/filepath"
9 "strings"
10 "time"
11
12 "tangled.org/core/api/tangled"
13 "tangled.org/core/appview/config"
14 "tangled.org/core/appview/db"
15 "tangled.org/core/appview/models"
16 "tangled.org/core/appview/pages"
17 "tangled.org/core/appview/pages/markup"
18 "tangled.org/core/appview/reporesolver"
19 xrpcclient "tangled.org/core/appview/xrpcclient"
20 "tangled.org/core/types"
21
22 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
23 "github.com/go-chi/chi/v5"
24 "github.com/go-git/go-git/v5/plumbing"
25)
26
27// the content can be one of the following:
28//
29// - code : text | | raw
30// - markup : text | rendered | raw
31// - svg : text | rendered | raw
32// - png : | rendered | raw
33// - video : | rendered | raw
34// - submodule : | rendered |
35// - rest : | |
36func (rp *Repo) Blob(w http.ResponseWriter, r *http.Request) {
37 l := rp.logger.With("handler", "RepoBlob")
38
39 f, err := rp.repoResolver.Resolve(r)
40 if err != nil {
41 l.Error("failed to get repo and knot", "err", err)
42 return
43 }
44
45 ref := chi.URLParam(r, "ref")
46 ref, _ = url.PathUnescape(ref)
47
48 filePath := chi.URLParam(r, "*")
49 filePath, _ = url.PathUnescape(filePath)
50
51 xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url}
52 resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, f.RepoDid)
53 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
54 l.Error("failed to call XRPC repo.blob", "xrpcerr", xrpcerr, "err", err)
55 rp.pages.Error503(w)
56 return
57 }
58
59 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
60
61 // Use XRPC response directly instead of converting to internal types
62 var breadcrumbs [][]string
63 breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(ref))})
64 if filePath != "" {
65 for idx, elem := range strings.Split(filePath, "/") {
66 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
67 }
68 }
69
70 // Create the blob view
71 blobView := NewBlobView(resp, rp.config, f, ref, filePath, r.URL.Query())
72
73 user := rp.oauth.GetMultiAccountUser(r)
74
75 // Get email to DID mapping for commit author
76 var emails []string
77 if resp.LastCommit != nil && resp.LastCommit.Author != nil {
78 emails = append(emails, resp.LastCommit.Author.Email)
79 }
80 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true)
81 if err != nil {
82 l.Error("failed to get email to did mapping", "err", err)
83 emailToDidMap = make(map[string]string)
84 }
85
86 var lastCommitInfo *types.LastCommitInfo
87 if resp.LastCommit != nil {
88 when, _ := time.Parse(time.RFC3339, resp.LastCommit.When)
89 lastCommitInfo = &types.LastCommitInfo{
90 Hash: plumbing.NewHash(resp.LastCommit.Hash),
91 Message: resp.LastCommit.Message,
92 When: when,
93 }
94 if resp.LastCommit.Author != nil {
95 lastCommitInfo.Author.Name = resp.LastCommit.Author.Name
96 lastCommitInfo.Author.Email = resp.LastCommit.Author.Email
97 lastCommitInfo.Author.When, _ = time.Parse(time.RFC3339, resp.LastCommit.Author.When)
98 }
99 }
100
101 rp.pages.RepoBlob(w, pages.RepoBlobParams{
102 LoggedInUser: user,
103 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
104 BreadCrumbs: breadcrumbs,
105 BlobView: blobView,
106 EmailToDid: emailToDidMap,
107 LastCommitInfo: lastCommitInfo,
108 Ref: resp.Ref,
109 Path: resp.Path,
110 })
111}
112
113func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
114 l := rp.logger.With("handler", "RepoBlobRaw")
115
116 f, err := rp.repoResolver.Resolve(r)
117 if err != nil {
118 l.Error("failed to get repo and knot", "err", err)
119 w.WriteHeader(http.StatusBadRequest)
120 return
121 }
122
123 ref := chi.URLParam(r, "ref")
124 ref, _ = url.PathUnescape(ref)
125
126 filePath := chi.URLParam(r, "*")
127 filePath, _ = url.PathUnescape(filePath)
128
129 blobURL := generateBlobURL(rp.config, f, ref, filePath)
130
131 w.Header().Set("Cache-Control", "public, no-cache")
132 http.Redirect(w, r, blobURL, http.StatusFound)
133}
134
135// NewBlobView creates a BlobView from the XRPC response
136func NewBlobView(resp *tangled.RepoBlob_Output, config *config.Config, repo *models.Repo, ref, filePath string, queryParams url.Values) models.BlobView {
137 view := models.BlobView{
138 Contents: "",
139 Lines: 0,
140 }
141
142 // Set size
143 if resp.Size != nil {
144 view.SizeHint = uint64(*resp.Size)
145 } else if resp.Content != nil {
146 view.SizeHint = uint64(len(*resp.Content))
147 }
148
149 if resp.Submodule != nil {
150 view.ContentType = models.BlobContentTypeSubmodule
151 view.HasRenderedView = true
152 view.ContentSrc = resp.Submodule.Url
153 return view
154 }
155
156 // Determine if binary
157 if (resp.IsBinary != nil && *resp.IsBinary) || (resp.FileTooLarge != nil && *resp.FileTooLarge) {
158 view.ContentSrc = generateBlobURL(config, repo, ref, filePath)
159 ext := strings.ToLower(filepath.Ext(resp.Path))
160
161 switch ext {
162 case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif", ".jxl", ".heic", ".heif":
163 view.ContentType = models.BlobContentTypeImage
164 view.HasRawView = true
165 view.HasRenderedView = true
166 view.ShowingRendered = true
167
168 case ".svg":
169 view.ContentType = models.BlobContentTypeSvg
170 view.HasRawView = true
171 view.HasTextView = true
172 view.HasRenderedView = true
173 view.ShowingRendered = queryParams.Get("code") != "true"
174 if resp.Content != nil {
175 bytes, _ := base64.StdEncoding.DecodeString(*resp.Content)
176 view.Contents = string(bytes)
177 view.Lines = countLines(view.Contents)
178 }
179
180 case ".mp4", ".webm", ".ogg", ".mov", ".avi":
181 view.ContentType = models.BlobContentTypeVideo
182 view.HasRawView = true
183 view.HasRenderedView = true
184 view.ShowingRendered = true
185 }
186
187 return view
188 }
189
190 // otherwise, we are dealing with text content
191 view.HasRawView = true
192 view.HasTextView = true
193
194 if resp.Content != nil {
195 view.Contents = *resp.Content
196 view.Lines = countLines(view.Contents)
197 }
198
199 // with text, we may be dealing with markdown
200 format := markup.GetFormat(resp.Path)
201 if format == markup.FormatMarkdown {
202 view.ContentType = models.BlobContentTypeMarkup
203 view.HasRenderedView = true
204 view.ShowingRendered = queryParams.Get("code") != "true"
205 }
206
207 return view
208}
209
210func generateBlobURL(config *config.Config, repo *models.Repo, ref, filePath string) string {
211 query := url.Values{}
212 query.Set("repo", repo.RepoDid)
213 query.Set("ref", ref)
214 query.Set("path", filePath)
215
216 blobURL := fmt.Sprintf("%s/xrpc/%s?%s", config.KnotMirror.Url, tangled.GitTempGetBlobNSID, query.Encode())
217 return blobURL
218}
219
220// TODO: dedup with strings
221func countLines(content string) int {
222 if content == "" {
223 return 0
224 }
225
226 count := strings.Count(content, "\n")
227
228 if !strings.HasSuffix(content, "\n") {
229 count++
230 }
231
232 return count
233}