gitea源码

content.go 9.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package files
  4. import (
  5. "context"
  6. "io"
  7. "net/url"
  8. "path"
  9. "strings"
  10. repo_model "code.gitea.io/gitea/models/repo"
  11. "code.gitea.io/gitea/modules/git"
  12. "code.gitea.io/gitea/modules/lfs"
  13. "code.gitea.io/gitea/modules/setting"
  14. api "code.gitea.io/gitea/modules/structs"
  15. "code.gitea.io/gitea/modules/util"
  16. "code.gitea.io/gitea/routers/api/v1/utils"
  17. )
  18. // ContentType repo content type
  19. type ContentType string
  20. // The string representations of different content types
  21. const (
  22. ContentTypeRegular ContentType = "file" // regular content type (file)
  23. ContentTypeDir ContentType = "dir" // dir content type (dir)
  24. ContentTypeLink ContentType = "symlink" // link content type (symlink)
  25. ContentTypeSubmodule ContentType = "submodule" // submodule content type (submodule)
  26. )
  27. // String gets the string of ContentType
  28. func (ct *ContentType) String() string {
  29. return string(*ct)
  30. }
  31. type GetContentsOrListOptions struct {
  32. TreePath string
  33. IncludeSingleFileContent bool // include the file's content when the tree path is a file
  34. IncludeLfsMetadata bool
  35. IncludeCommitMetadata bool
  36. IncludeCommitMessage bool
  37. }
  38. // GetContentsOrList gets the metadata of a file's contents (*ContentsResponse) if treePath not a tree
  39. // directory, otherwise a listing of file contents ([]*ContentsResponse). Ref can be a branch, commit or tag
  40. func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, opts GetContentsOrListOptions) (ret api.ContentsExtResponse, _ error) {
  41. entry, err := prepareGetContentsEntry(refCommit, &opts.TreePath)
  42. if repo.IsEmpty && opts.TreePath == "" {
  43. return api.ContentsExtResponse{DirContents: make([]*api.ContentsResponse, 0)}, nil
  44. }
  45. if err != nil {
  46. return ret, err
  47. }
  48. // get file contents
  49. if entry.Type() != "tree" {
  50. ret.FileContents, err = getFileContentsByEntryInternal(ctx, repo, gitRepo, refCommit, entry, opts)
  51. return ret, err
  52. }
  53. // list directory contents
  54. gitTree, err := refCommit.Commit.SubTree(opts.TreePath)
  55. if err != nil {
  56. return ret, err
  57. }
  58. entries, err := gitTree.ListEntries()
  59. if err != nil {
  60. return ret, err
  61. }
  62. ret.DirContents = make([]*api.ContentsResponse, 0, len(entries))
  63. for _, e := range entries {
  64. subOpts := opts
  65. subOpts.TreePath = path.Join(opts.TreePath, e.Name())
  66. subOpts.IncludeSingleFileContent = false // never include file content when listing a directory
  67. fileContentResponse, err := GetFileContents(ctx, repo, gitRepo, refCommit, subOpts)
  68. if err != nil {
  69. return ret, err
  70. }
  71. ret.DirContents = append(ret.DirContents, fileContentResponse)
  72. }
  73. return ret, nil
  74. }
  75. // GetObjectTypeFromTreeEntry check what content is behind it
  76. func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType {
  77. switch {
  78. case entry.IsDir():
  79. return ContentTypeDir
  80. case entry.IsSubModule():
  81. return ContentTypeSubmodule
  82. case entry.IsExecutable(), entry.IsRegular():
  83. return ContentTypeRegular
  84. case entry.IsLink():
  85. return ContentTypeLink
  86. default:
  87. return ""
  88. }
  89. }
  90. func prepareGetContentsEntry(refCommit *utils.RefCommit, treePath *string) (*git.TreeEntry, error) {
  91. // Check that the path given in opts.treePath is valid (not a git path)
  92. cleanTreePath := CleanGitTreePath(*treePath)
  93. if cleanTreePath == "" && *treePath != "" {
  94. return nil, ErrFilenameInvalid{Path: *treePath}
  95. }
  96. *treePath = cleanTreePath
  97. // Only allow safe ref types
  98. refType := refCommit.RefName.RefType()
  99. if refType != git.RefTypeBranch && refType != git.RefTypeTag && refType != git.RefTypeCommit {
  100. return nil, util.NewNotExistErrorf("no commit found for the ref [ref: %s]", refCommit.RefName)
  101. }
  102. return refCommit.Commit.GetTreeEntryByPath(*treePath)
  103. }
  104. // GetFileContents gets the metadata on a file's contents. Ref can be a branch, commit or tag
  105. func GetFileContents(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, opts GetContentsOrListOptions) (*api.ContentsResponse, error) {
  106. entry, err := prepareGetContentsEntry(refCommit, &opts.TreePath)
  107. if err != nil {
  108. return nil, err
  109. }
  110. return getFileContentsByEntryInternal(ctx, repo, gitRepo, refCommit, entry, opts)
  111. }
  112. func getFileContentsByEntryInternal(_ context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, entry *git.TreeEntry, opts GetContentsOrListOptions) (*api.ContentsResponse, error) {
  113. refType := refCommit.RefName.RefType()
  114. commit := refCommit.Commit
  115. selfURL, err := url.Parse(repo.APIURL() + "/contents/" + util.PathEscapeSegments(opts.TreePath) + "?ref=" + url.QueryEscape(refCommit.InputRef))
  116. if err != nil {
  117. return nil, err
  118. }
  119. selfURLString := selfURL.String()
  120. // All content types have these fields in populated
  121. contentsResponse := &api.ContentsResponse{
  122. Name: entry.Name(),
  123. Path: opts.TreePath,
  124. SHA: entry.ID.String(),
  125. Size: entry.Size(),
  126. URL: &selfURLString,
  127. Links: &api.FileLinksResponse{
  128. Self: &selfURLString,
  129. },
  130. }
  131. if opts.IncludeCommitMetadata || opts.IncludeCommitMessage {
  132. err = gitRepo.AddLastCommitCache(repo.GetCommitsCountCacheKey(refCommit.InputRef, refType != git.RefTypeCommit), repo.FullName(), refCommit.CommitID)
  133. if err != nil {
  134. return nil, err
  135. }
  136. lastCommit, err := refCommit.Commit.GetCommitByPath(opts.TreePath)
  137. if err != nil {
  138. return nil, err
  139. }
  140. if opts.IncludeCommitMetadata {
  141. contentsResponse.LastCommitSHA = util.ToPointer(lastCommit.ID.String())
  142. // GitHub doesn't have these fields in the response, but we could follow other similar APIs to name them
  143. // https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-commits
  144. if lastCommit.Committer != nil {
  145. contentsResponse.LastCommitterDate = util.ToPointer(lastCommit.Committer.When)
  146. }
  147. if lastCommit.Author != nil {
  148. contentsResponse.LastAuthorDate = util.ToPointer(lastCommit.Author.When)
  149. }
  150. }
  151. if opts.IncludeCommitMessage {
  152. contentsResponse.LastCommitMessage = util.ToPointer(lastCommit.Message())
  153. }
  154. }
  155. // Now populate the rest of the ContentsResponse based on the entry type
  156. if entry.IsRegular() || entry.IsExecutable() {
  157. contentsResponse.Type = string(ContentTypeRegular)
  158. // if it is listing the repo root dir, don't waste system resources on reading content
  159. if opts.IncludeSingleFileContent {
  160. blobResponse, err := GetBlobBySHA(repo, gitRepo, entry.ID.String())
  161. if err != nil {
  162. return nil, err
  163. }
  164. contentsResponse.Encoding, contentsResponse.Content = blobResponse.Encoding, blobResponse.Content
  165. contentsResponse.LfsOid, contentsResponse.LfsSize = blobResponse.LfsOid, blobResponse.LfsSize
  166. } else if opts.IncludeLfsMetadata {
  167. contentsResponse.LfsOid, contentsResponse.LfsSize, err = parsePossibleLfsPointerBlob(gitRepo, entry.ID.String())
  168. if err != nil {
  169. return nil, err
  170. }
  171. }
  172. } else if entry.IsDir() {
  173. contentsResponse.Type = string(ContentTypeDir)
  174. } else if entry.IsLink() {
  175. contentsResponse.Type = string(ContentTypeLink)
  176. // The target of a symlink file is the content of the file
  177. targetFromContent, err := entry.Blob().GetBlobContent(1024)
  178. if err != nil {
  179. return nil, err
  180. }
  181. contentsResponse.Target = &targetFromContent
  182. } else if entry.IsSubModule() {
  183. contentsResponse.Type = string(ContentTypeSubmodule)
  184. submodule, err := commit.GetSubModule(opts.TreePath)
  185. if err != nil {
  186. return nil, err
  187. }
  188. if submodule != nil && submodule.URL != "" {
  189. contentsResponse.SubmoduleGitURL = &submodule.URL
  190. }
  191. }
  192. // Handle links
  193. if entry.IsRegular() || entry.IsLink() || entry.IsExecutable() {
  194. downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(opts.TreePath))
  195. if err != nil {
  196. return nil, err
  197. }
  198. downloadURLString := downloadURL.String()
  199. contentsResponse.DownloadURL = &downloadURLString
  200. }
  201. if !entry.IsSubModule() {
  202. htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(opts.TreePath))
  203. if err != nil {
  204. return nil, err
  205. }
  206. htmlURLString := htmlURL.String()
  207. contentsResponse.HTMLURL = &htmlURLString
  208. contentsResponse.Links.HTMLURL = &htmlURLString
  209. gitURL, err := url.Parse(repo.APIURL() + "/git/blobs/" + url.PathEscape(entry.ID.String()))
  210. if err != nil {
  211. return nil, err
  212. }
  213. gitURLString := gitURL.String()
  214. contentsResponse.GitURL = &gitURLString
  215. contentsResponse.Links.GitURL = &gitURLString
  216. }
  217. return contentsResponse, nil
  218. }
  219. func GetBlobBySHA(repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) {
  220. gitBlob, err := gitRepo.GetBlob(sha)
  221. if err != nil {
  222. return nil, err
  223. }
  224. ret := &api.GitBlobResponse{
  225. SHA: gitBlob.ID.String(),
  226. URL: repo.APIURL() + "/git/blobs/" + url.PathEscape(gitBlob.ID.String()),
  227. Size: gitBlob.Size(),
  228. }
  229. blobSize := gitBlob.Size()
  230. if blobSize > setting.API.DefaultMaxBlobSize {
  231. return ret, nil
  232. }
  233. var originContent *strings.Builder
  234. if 0 < blobSize && blobSize < lfs.MetaFileMaxSize {
  235. originContent = &strings.Builder{}
  236. }
  237. content, err := gitBlob.GetBlobContentBase64(originContent)
  238. if err != nil {
  239. return nil, err
  240. }
  241. ret.Encoding, ret.Content = util.ToPointer("base64"), &content
  242. if originContent != nil {
  243. ret.LfsOid, ret.LfsSize = parsePossibleLfsPointerBuffer(strings.NewReader(originContent.String()))
  244. }
  245. return ret, nil
  246. }
  247. func parsePossibleLfsPointerBuffer(r io.Reader) (*string, *int64) {
  248. p, _ := lfs.ReadPointer(r)
  249. if p.IsValid() {
  250. return &p.Oid, &p.Size
  251. }
  252. return nil, nil
  253. }
  254. func parsePossibleLfsPointerBlob(gitRepo *git.Repository, sha string) (*string, *int64, error) {
  255. gitBlob, err := gitRepo.GetBlob(sha)
  256. if err != nil {
  257. return nil, nil, err
  258. }
  259. if gitBlob.Size() > lfs.MetaFileMaxSize {
  260. return nil, nil, nil // not a LFS pointer
  261. }
  262. buf, err := gitBlob.GetBlobContent(lfs.MetaFileMaxSize)
  263. if err != nil {
  264. return nil, nil, err
  265. }
  266. oid, size := parsePossibleLfsPointerBuffer(strings.NewReader(buf))
  267. return oid, size, nil
  268. }