This repository has no description
0

Configure Feed

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

core / appview / repo / blob.go
6.7 kB 233 lines
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}