blob.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. package git
  2. /*
  3. blob.go
  4. Reading a file's content at a given revision.
  5. This backs the rich preview in GitApp: an image or PDF that changed is shown
  6. as a before/after pair, and the "before" side only exists inside the object
  7. database, so it has to be extracted rather than read off disk.
  8. */
  9. import (
  10. "errors"
  11. "regexp"
  12. "strings"
  13. "github.com/go-git/go-git/v5/plumbing"
  14. )
  15. // maxBlobBytes caps what may be pulled into memory for a preview. Anything
  16. // larger is refused so a stray multi-gigabyte asset cannot exhaust the server.
  17. const maxBlobBytes = 8 * 1024 * 1024
  18. // headRevision is the revision name meaning "the current HEAD commit".
  19. const headRevision = "HEAD"
  20. // commitHashPattern matches a full 40 character SHA-1, the only hash form
  21. // accepted from callers.
  22. var commitHashPattern = regexp.MustCompile(`^[0-9a-fA-F]{40}$`)
  23. /*
  24. FileBlob returns the content of a repository-relative path at a revision.
  25. revision is either "HEAD" (or empty, meaning the same) or a full commit hash.
  26. The second return value reports whether the path existed in that revision at
  27. all, which lets the caller distinguish "added in this change" from a genuine
  28. read failure.
  29. */
  30. func (m *Manager) FileBlob(realpath string, file string, revision string) ([]byte, bool, error) {
  31. cleaned, err := cleanRepoPath(file)
  32. if err != nil {
  33. return nil, false, err
  34. }
  35. repo, err := m.open(realpath)
  36. if err != nil {
  37. return nil, false, err
  38. }
  39. revision = strings.TrimSpace(revision)
  40. var hash plumbing.Hash
  41. if revision == "" || strings.EqualFold(revision, headRevision) {
  42. head, herr := repo.Head()
  43. if herr != nil {
  44. if errors.Is(herr, plumbing.ErrReferenceNotFound) {
  45. //Unborn branch: nothing has been committed yet
  46. return nil, false, nil
  47. }
  48. return nil, false, herr
  49. }
  50. hash = head.Hash()
  51. } else {
  52. if !commitHashPattern.MatchString(revision) {
  53. return nil, false, errors.New("not a commit hash: " + revision)
  54. }
  55. hash = plumbing.NewHash(revision)
  56. }
  57. commit, err := repo.CommitObject(hash)
  58. if err != nil {
  59. return nil, false, err
  60. }
  61. treeFile, err := commit.File(cleaned)
  62. if err != nil {
  63. //Absent from this revision, which is a normal answer rather than a fault
  64. return nil, false, nil
  65. }
  66. if treeFile.Size > maxBlobBytes {
  67. return nil, true, errors.New("file is too large to load")
  68. }
  69. content, err := treeFile.Contents()
  70. if err != nil {
  71. return nil, true, err
  72. }
  73. return []byte(content), true, nil
  74. }
  75. /*
  76. PreviewMimeType maps a file name onto the media type a browser needs to render
  77. it, or "" when the type is not something a browser displays.
  78. The table is explicit rather than delegating to mime.TypeByExtension: that
  79. consults the Windows registry and /etc/mime.types, so the same repository could
  80. otherwise preview differently on two machines.
  81. */
  82. func PreviewMimeType(path string) string {
  83. extension := strings.ToLower(path)
  84. if index := strings.LastIndex(extension, "."); index >= 0 {
  85. extension = extension[index:]
  86. } else {
  87. return ""
  88. }
  89. switch extension {
  90. //Images
  91. case ".png":
  92. return "image/png"
  93. case ".jpg", ".jpeg":
  94. return "image/jpeg"
  95. case ".gif":
  96. return "image/gif"
  97. case ".webp":
  98. return "image/webp"
  99. case ".bmp":
  100. return "image/bmp"
  101. case ".ico":
  102. return "image/x-icon"
  103. case ".svg":
  104. return "image/svg+xml"
  105. case ".avif":
  106. return "image/avif"
  107. //Documents
  108. case ".pdf":
  109. return "application/pdf"
  110. //Video
  111. case ".mp4", ".m4v":
  112. return "video/mp4"
  113. case ".webm":
  114. return "video/webm"
  115. case ".ogv":
  116. return "video/ogg"
  117. //Audio
  118. case ".mp3":
  119. return "audio/mpeg"
  120. case ".wav":
  121. return "audio/wav"
  122. case ".ogg", ".oga":
  123. return "audio/ogg"
  124. case ".flac":
  125. return "audio/flac"
  126. case ".m4a":
  127. return "audio/mp4"
  128. default:
  129. return ""
  130. }
  131. }
  132. // PreviewKind groups a media type into the element the front-end should use.
  133. // Returns "image", "pdf", "video", "audio" or "" when there is no preview.
  134. func PreviewKind(path string) string {
  135. mimeType := PreviewMimeType(path)
  136. switch {
  137. case mimeType == "":
  138. return ""
  139. case mimeType == "application/pdf":
  140. return "pdf"
  141. case strings.HasPrefix(mimeType, "image/"):
  142. return "image"
  143. case strings.HasPrefix(mimeType, "video/"):
  144. return "video"
  145. case strings.HasPrefix(mimeType, "audio/"):
  146. return "audio"
  147. default:
  148. return ""
  149. }
  150. }