Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
4.9 kB · 193 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194package xrpc
import ( "context" "crypto/sha256" "encoding/base64" "fmt" "net/http" "path/filepath" "slices" "strings" "time"
"tangled.org/core/api/tangled" "tangled.org/core/knotserver/git" xrpcerr "tangled.org/core/xrpc/errors")
func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) { repo := r.URL.Query().Get("repo") repoPath, err := x.parseRepoParam(repo) if err != nil { writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) return }
ref := r.URL.Query().Get("ref") // ref can be empty (git.Open handles this)
treePath := r.URL.Query().Get("path") if treePath == "" { writeError(w, xrpcerr.NewXrpcError( xrpcerr.WithTag("InvalidRequest"), xrpcerr.WithMessage("missing path parameter"), ), http.StatusBadRequest) return }
raw := r.URL.Query().Get("raw") == "true"
gr, err := git.Open(repoPath, ref) if err != nil { writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) return }
// first check if this path is a submodule submodule, err := gr.Submodule(treePath) if err != nil { // this is okay, continue and try to treat it as a regular file } else { response := tangled.RepoBlob_Output{ Ref: ref, Path: treePath, Submodule: &tangled.RepoBlob_Submodule{ Name: submodule.Name, Url: submodule.URL, Branch: &submodule.Branch, }, } x.writeJson(w, response) return }
contents, err := gr.RawContent(treePath) if err != nil { x.Logger.Error("file content", "error", err.Error(), "treePath", treePath) writeError(w, xrpcerr.NewXrpcError( xrpcerr.WithTag("FileNotFound"), xrpcerr.WithMessage("file not found at the specified path"), ), http.StatusNotFound) return }
mimeType := http.DetectContentType(contents)
// override MIME types for formats that http.DetectContentType does not recognize switch filepath.Ext(treePath) { case ".svg": mimeType = "image/svg+xml" case ".avif": mimeType = "image/avif" case ".jxl": mimeType = "image/jxl" case ".heic", ".heif": mimeType = "image/heif" }
if raw { contentHash := sha256.Sum256(contents) eTag := fmt.Sprintf("\"%x\"", contentHash)
switch { case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { w.WriteHeader(http.StatusNotModified) return } w.Header().Set("ETag", eTag) w.Header().Set("Content-Type", mimeType)
case strings.HasPrefix(mimeType, "text/"): w.Header().Set("Cache-Control", "public, no-cache") // serve all text content as text/plain w.Header().Set("Content-Type", "text/plain; charset=utf-8")
case isTextualMimeType(mimeType): // handle textual application types (json, xml, etc.) as text/plain w.Header().Set("Cache-Control", "public, no-cache") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
default: x.Logger.Error("attempted to serve disallowed file type", "mimetype", mimeType) writeError(w, xrpcerr.NewXrpcError( xrpcerr.WithTag("InvalidRequest"), xrpcerr.WithMessage("only image, video, and text files can be accessed directly"), ), http.StatusForbidden) return } w.Write(contents) return }
isTextual := func(mt string) bool { return strings.HasPrefix(mt, "text/") || isTextualMimeType(mt) }
var content string var encoding string
isBinary := !isTextual(mimeType) size := int64(len(contents))
if isBinary { content = base64.StdEncoding.EncodeToString(contents) encoding = "base64" } else { content = string(contents) encoding = "utf-8" }
response := tangled.RepoBlob_Output{ Ref: ref, Path: treePath, Content: &content, Encoding: &encoding, Size: &size, IsBinary: &isBinary, }
if mimeType != "" { response.MimeType = &mimeType }
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel()
lastCommit, err := gr.LastCommitFile(ctx, treePath) if err == nil && lastCommit != nil { response.LastCommit = &tangled.RepoBlob_LastCommit{ Hash: lastCommit.Hash.String(), Message: lastCommit.Message, When: lastCommit.When.Format(time.RFC3339), }
// try to get author information commit, err := gr.Commit(lastCommit.Hash) if err == nil { response.LastCommit.Author = &tangled.RepoBlob_Signature{ Name: commit.Author.Name, Email: commit.Author.Email, } } }
x.writeJson(w, response)}
// isTextualMimeType returns true if the MIME type represents textual content// that should be served as text/plain for security reasonsfunc isTextualMimeType(mimeType string) bool { textualTypes := []string{ "application/json", "application/xml", "application/yaml", "application/x-yaml", "application/toml", "application/javascript", "application/ecmascript", }
return slices.Contains(textualTypes, mimeType)}