A vibe coded tangled fork which supports pijul.
at 7108735bbddad2e27d788dac0264818aeebd8664 193 lines 4.8 kB view raw
1package xrpc 2 3import ( 4 "context" 5 "crypto/sha256" 6 "encoding/base64" 7 "fmt" 8 "net/http" 9 "path/filepath" 10 "slices" 11 "strings" 12 "time" 13 14 "tangled.org/core/api/tangled" 15 "tangled.org/core/knotserver/git" 16 xrpcerr "tangled.org/core/xrpc/errors" 17) 18 19func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) { 20 repo := r.URL.Query().Get("repo") 21 repoPath, err := x.parseRepoParam(repo) 22 if err != nil { 23 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 24 return 25 } 26 27 ref := r.URL.Query().Get("ref") 28 // ref can be empty (git.Open handles this) 29 30 treePath := r.URL.Query().Get("path") 31 if treePath == "" { 32 writeError(w, xrpcerr.NewXrpcError( 33 xrpcerr.WithTag("InvalidRequest"), 34 xrpcerr.WithMessage("missing path parameter"), 35 ), http.StatusBadRequest) 36 return 37 } 38 39 raw := r.URL.Query().Get("raw") == "true" 40 41 gr, err := git.Open(repoPath, ref) 42 if err != nil { 43 writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 44 return 45 } 46 47 // first check if this path is a submodule 48 submodule, err := gr.Submodule(treePath) 49 if err != nil { 50 // this is okay, continue and try to treat it as a regular file 51 } else { 52 response := tangled.RepoBlob_Output{ 53 Ref: ref, 54 Path: treePath, 55 Submodule: &tangled.RepoBlob_Submodule{ 56 Name: submodule.Name, 57 Url: submodule.URL, 58 Branch: &submodule.Branch, 59 }, 60 } 61 writeJson(w, response) 62 return 63 } 64 65 contents, err := gr.RawContent(treePath) 66 if err != nil { 67 x.Logger.Error("file content", "error", err.Error(), "treePath", treePath) 68 writeError(w, xrpcerr.NewXrpcError( 69 xrpcerr.WithTag("FileNotFound"), 70 xrpcerr.WithMessage("file not found at the specified path"), 71 ), http.StatusNotFound) 72 return 73 } 74 75 mimeType := http.DetectContentType(contents) 76 77 // override MIME types for formats that http.DetectContentType does not recognize 78 switch filepath.Ext(treePath) { 79 case ".svg": 80 mimeType = "image/svg+xml" 81 case ".avif": 82 mimeType = "image/avif" 83 case ".jxl": 84 mimeType = "image/jxl" 85 case ".heic", ".heif": 86 mimeType = "image/heif" 87 } 88 89 if raw { 90 contentHash := sha256.Sum256(contents) 91 eTag := fmt.Sprintf("\"%x\"", contentHash) 92 93 switch { 94 case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): 95 if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { 96 w.WriteHeader(http.StatusNotModified) 97 return 98 } 99 w.Header().Set("ETag", eTag) 100 w.Header().Set("Content-Type", mimeType) 101 102 case strings.HasPrefix(mimeType, "text/"): 103 w.Header().Set("Cache-Control", "public, no-cache") 104 // serve all text content as text/plain 105 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 106 107 case isTextualMimeType(mimeType): 108 // handle textual application types (json, xml, etc.) as text/plain 109 w.Header().Set("Cache-Control", "public, no-cache") 110 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 111 112 default: 113 x.Logger.Error("attempted to serve disallowed file type", "mimetype", mimeType) 114 writeError(w, xrpcerr.NewXrpcError( 115 xrpcerr.WithTag("InvalidRequest"), 116 xrpcerr.WithMessage("only image, video, and text files can be accessed directly"), 117 ), http.StatusForbidden) 118 return 119 } 120 w.Write(contents) 121 return 122 } 123 124 isTextual := func(mt string) bool { 125 return strings.HasPrefix(mt, "text/") || isTextualMimeType(mt) 126 } 127 128 var content string 129 var encoding string 130 131 isBinary := !isTextual(mimeType) 132 size := int64(len(contents)) 133 134 if isBinary { 135 content = base64.StdEncoding.EncodeToString(contents) 136 encoding = "base64" 137 } else { 138 content = string(contents) 139 encoding = "utf-8" 140 } 141 142 response := tangled.RepoBlob_Output{ 143 Ref: ref, 144 Path: treePath, 145 Content: &content, 146 Encoding: &encoding, 147 Size: &size, 148 IsBinary: &isBinary, 149 } 150 151 if mimeType != "" { 152 response.MimeType = &mimeType 153 } 154 155 ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) 156 defer cancel() 157 158 lastCommit, err := gr.LastCommitFile(ctx, treePath) 159 if err == nil && lastCommit != nil { 160 response.LastCommit = &tangled.RepoBlob_LastCommit{ 161 Hash: lastCommit.Hash.String(), 162 Message: lastCommit.Message, 163 When: lastCommit.When.Format(time.RFC3339), 164 } 165 166 // try to get author information 167 commit, err := gr.Commit(lastCommit.Hash) 168 if err == nil { 169 response.LastCommit.Author = &tangled.RepoBlob_Signature{ 170 Name: commit.Author.Name, 171 Email: commit.Author.Email, 172 } 173 } 174 } 175 176 writeJson(w, response) 177} 178 179// isTextualMimeType returns true if the MIME type represents textual content 180// that should be served as text/plain for security reasons 181func isTextualMimeType(mimeType string) bool { 182 textualTypes := []string{ 183 "application/json", 184 "application/xml", 185 "application/yaml", 186 "application/x-yaml", 187 "application/toml", 188 "application/javascript", 189 "application/ecmascript", 190 } 191 192 return slices.Contains(textualTypes, mimeType) 193}