You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

551 lines
15 KiB

  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package repo
  5. import (
  6. "bufio"
  7. "bytes"
  8. "fmt"
  9. gotemplate "html/template"
  10. "io"
  11. "io/ioutil"
  12. "os"
  13. "path/filepath"
  14. "sort"
  15. "strconv"
  16. "strings"
  17. "sync"
  18. "time"
  19. "code.gitea.io/gitea/models"
  20. "code.gitea.io/gitea/modules/base"
  21. "code.gitea.io/gitea/modules/charset"
  22. "code.gitea.io/gitea/modules/context"
  23. "code.gitea.io/gitea/modules/git"
  24. "code.gitea.io/gitea/modules/git/pipeline"
  25. "code.gitea.io/gitea/modules/lfs"
  26. "code.gitea.io/gitea/modules/log"
  27. "code.gitea.io/gitea/modules/setting"
  28. "github.com/mcuadros/go-version"
  29. "github.com/unknwon/com"
  30. gogit "gopkg.in/src-d/go-git.v4"
  31. "gopkg.in/src-d/go-git.v4/plumbing"
  32. "gopkg.in/src-d/go-git.v4/plumbing/object"
  33. )
  34. const (
  35. tplSettingsLFS base.TplName = "repo/settings/lfs"
  36. tplSettingsLFSFile base.TplName = "repo/settings/lfs_file"
  37. tplSettingsLFSFileFind base.TplName = "repo/settings/lfs_file_find"
  38. tplSettingsLFSPointers base.TplName = "repo/settings/lfs_pointers"
  39. )
  40. // LFSFiles shows a repository's LFS files
  41. func LFSFiles(ctx *context.Context) {
  42. if !setting.LFS.StartServer {
  43. ctx.NotFound("LFSFiles", nil)
  44. return
  45. }
  46. page := ctx.QueryInt("page")
  47. if page <= 1 {
  48. page = 1
  49. }
  50. total, err := ctx.Repo.Repository.CountLFSMetaObjects()
  51. if err != nil {
  52. ctx.ServerError("LFSFiles", err)
  53. return
  54. }
  55. pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
  56. ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
  57. ctx.Data["PageIsSettingsLFS"] = true
  58. lfsMetaObjects, err := ctx.Repo.Repository.GetLFSMetaObjects(pager.Paginater.Current(), setting.UI.ExplorePagingNum)
  59. if err != nil {
  60. ctx.ServerError("LFSFiles", err)
  61. return
  62. }
  63. ctx.Data["LFSFiles"] = lfsMetaObjects
  64. ctx.Data["Page"] = pager
  65. ctx.HTML(200, tplSettingsLFS)
  66. }
  67. // LFSFileGet serves a single LFS file
  68. func LFSFileGet(ctx *context.Context) {
  69. if !setting.LFS.StartServer {
  70. ctx.NotFound("LFSFileGet", nil)
  71. return
  72. }
  73. ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
  74. oid := ctx.Params("oid")
  75. ctx.Data["Title"] = oid
  76. ctx.Data["PageIsSettingsLFS"] = true
  77. meta, err := ctx.Repo.Repository.GetLFSMetaObjectByOid(oid)
  78. if err != nil {
  79. if err == models.ErrLFSObjectNotExist {
  80. ctx.NotFound("LFSFileGet", nil)
  81. return
  82. }
  83. ctx.ServerError("LFSFileGet", err)
  84. return
  85. }
  86. ctx.Data["LFSFile"] = meta
  87. dataRc, err := lfs.ReadMetaObject(meta)
  88. if err != nil {
  89. ctx.ServerError("LFSFileGet", err)
  90. return
  91. }
  92. defer dataRc.Close()
  93. buf := make([]byte, 1024)
  94. n, err := dataRc.Read(buf)
  95. if err != nil {
  96. ctx.ServerError("Data", err)
  97. return
  98. }
  99. buf = buf[:n]
  100. isTextFile := base.IsTextFile(buf)
  101. ctx.Data["IsTextFile"] = isTextFile
  102. fileSize := meta.Size
  103. ctx.Data["FileSize"] = meta.Size
  104. ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, "direct")
  105. switch {
  106. case isTextFile:
  107. if fileSize >= setting.UI.MaxDisplayFileSize {
  108. ctx.Data["IsFileTooLarge"] = true
  109. break
  110. }
  111. d, _ := ioutil.ReadAll(dataRc)
  112. buf = charset.ToUTF8WithFallback(append(buf, d...))
  113. // Building code view blocks with line number on server side.
  114. var fileContent string
  115. if content, err := charset.ToUTF8WithErr(buf); err != nil {
  116. log.Error("ToUTF8WithErr: %v", err)
  117. fileContent = string(buf)
  118. } else {
  119. fileContent = content
  120. }
  121. var output bytes.Buffer
  122. lines := strings.Split(fileContent, "\n")
  123. //Remove blank line at the end of file
  124. if len(lines) > 0 && lines[len(lines)-1] == "" {
  125. lines = lines[:len(lines)-1]
  126. }
  127. for index, line := range lines {
  128. line = gotemplate.HTMLEscapeString(line)
  129. if index != len(lines)-1 {
  130. line += "\n"
  131. }
  132. output.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, line))
  133. }
  134. ctx.Data["FileContent"] = gotemplate.HTML(output.String())
  135. output.Reset()
  136. for i := 0; i < len(lines); i++ {
  137. output.WriteString(fmt.Sprintf(`<span id="L%d">%d</span>`, i+1, i+1))
  138. }
  139. ctx.Data["LineNums"] = gotemplate.HTML(output.String())
  140. case base.IsPDFFile(buf):
  141. ctx.Data["IsPDFFile"] = true
  142. case base.IsVideoFile(buf):
  143. ctx.Data["IsVideoFile"] = true
  144. case base.IsAudioFile(buf):
  145. ctx.Data["IsAudioFile"] = true
  146. case base.IsImageFile(buf):
  147. ctx.Data["IsImageFile"] = true
  148. }
  149. ctx.HTML(200, tplSettingsLFSFile)
  150. }
  151. // LFSDelete disassociates the provided oid from the repository and if the lfs file is no longer associated with any repositories - deletes it
  152. func LFSDelete(ctx *context.Context) {
  153. if !setting.LFS.StartServer {
  154. ctx.NotFound("LFSDelete", nil)
  155. return
  156. }
  157. oid := ctx.Params("oid")
  158. count, err := ctx.Repo.Repository.RemoveLFSMetaObjectByOid(oid)
  159. if err != nil {
  160. ctx.ServerError("LFSDelete", err)
  161. return
  162. }
  163. // FIXME: Warning: the LFS store is not locked - and can't be locked - there could be a race condition here
  164. // Please note a similar condition happens in models/repo.go DeleteRepository
  165. if count == 0 {
  166. oidPath := filepath.Join(oid[0:2], oid[2:4], oid[4:])
  167. err = os.Remove(filepath.Join(setting.LFS.ContentPath, oidPath))
  168. if err != nil {
  169. ctx.ServerError("LFSDelete", err)
  170. return
  171. }
  172. }
  173. ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
  174. }
  175. type lfsResult struct {
  176. Name string
  177. SHA string
  178. Summary string
  179. When time.Time
  180. ParentHashes []plumbing.Hash
  181. BranchName string
  182. FullCommitName string
  183. }
  184. type lfsResultSlice []*lfsResult
  185. func (a lfsResultSlice) Len() int { return len(a) }
  186. func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
  187. func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
  188. // LFSFileFind guesses a sha for the provided oid (or uses the provided sha) and then finds the commits that contain this sha
  189. func LFSFileFind(ctx *context.Context) {
  190. if !setting.LFS.StartServer {
  191. ctx.NotFound("LFSFind", nil)
  192. return
  193. }
  194. oid := ctx.Query("oid")
  195. size := ctx.QueryInt64("size")
  196. if len(oid) == 0 || size == 0 {
  197. ctx.NotFound("LFSFind", nil)
  198. return
  199. }
  200. sha := ctx.Query("sha")
  201. ctx.Data["Title"] = oid
  202. ctx.Data["PageIsSettingsLFS"] = true
  203. var hash plumbing.Hash
  204. if len(sha) == 0 {
  205. meta := models.LFSMetaObject{Oid: oid, Size: size}
  206. pointer := meta.Pointer()
  207. hash = plumbing.ComputeHash(plumbing.BlobObject, []byte(pointer))
  208. sha = hash.String()
  209. } else {
  210. hash = plumbing.NewHash(sha)
  211. }
  212. ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
  213. ctx.Data["Oid"] = oid
  214. ctx.Data["Size"] = size
  215. ctx.Data["SHA"] = sha
  216. resultsMap := map[string]*lfsResult{}
  217. results := make([]*lfsResult, 0)
  218. basePath := ctx.Repo.Repository.RepoPath()
  219. gogitRepo := ctx.Repo.GitRepo.GoGitRepo()
  220. commitsIter, err := gogitRepo.Log(&gogit.LogOptions{
  221. Order: gogit.LogOrderCommitterTime,
  222. All: true,
  223. })
  224. if err != nil {
  225. log.Error("Failed to get GoGit CommitsIter: %v", err)
  226. ctx.ServerError("LFSFind: Iterate Commits", err)
  227. return
  228. }
  229. err = commitsIter.ForEach(func(gitCommit *object.Commit) error {
  230. tree, err := gitCommit.Tree()
  231. if err != nil {
  232. return err
  233. }
  234. treeWalker := object.NewTreeWalker(tree, true, nil)
  235. defer treeWalker.Close()
  236. for {
  237. name, entry, err := treeWalker.Next()
  238. if err == io.EOF {
  239. break
  240. }
  241. if entry.Hash == hash {
  242. result := lfsResult{
  243. Name: name,
  244. SHA: gitCommit.Hash.String(),
  245. Summary: strings.Split(strings.TrimSpace(gitCommit.Message), "\n")[0],
  246. When: gitCommit.Author.When,
  247. ParentHashes: gitCommit.ParentHashes,
  248. }
  249. resultsMap[gitCommit.Hash.String()+":"+name] = &result
  250. }
  251. }
  252. return nil
  253. })
  254. if err != nil && err != io.EOF {
  255. log.Error("Failure in CommitIter.ForEach: %v", err)
  256. ctx.ServerError("LFSFind: IterateCommits ForEach", err)
  257. return
  258. }
  259. for _, result := range resultsMap {
  260. hasParent := false
  261. for _, parentHash := range result.ParentHashes {
  262. if _, hasParent = resultsMap[parentHash.String()+":"+result.Name]; hasParent {
  263. break
  264. }
  265. }
  266. if !hasParent {
  267. results = append(results, result)
  268. }
  269. }
  270. sort.Sort(lfsResultSlice(results))
  271. // Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple
  272. shasToNameReader, shasToNameWriter := io.Pipe()
  273. nameRevStdinReader, nameRevStdinWriter := io.Pipe()
  274. errChan := make(chan error, 1)
  275. wg := sync.WaitGroup{}
  276. wg.Add(3)
  277. go func() {
  278. defer wg.Done()
  279. scanner := bufio.NewScanner(nameRevStdinReader)
  280. i := 0
  281. for scanner.Scan() {
  282. line := scanner.Text()
  283. if len(line) == 0 {
  284. continue
  285. }
  286. result := results[i]
  287. result.FullCommitName = line
  288. result.BranchName = strings.Split(line, "~")[0]
  289. i++
  290. }
  291. }()
  292. go pipeline.NameRevStdin(shasToNameReader, nameRevStdinWriter, &wg, basePath)
  293. go func() {
  294. defer wg.Done()
  295. defer shasToNameWriter.Close()
  296. for _, result := range results {
  297. i := 0
  298. if i < len(result.SHA) {
  299. n, err := shasToNameWriter.Write([]byte(result.SHA)[i:])
  300. if err != nil {
  301. errChan <- err
  302. break
  303. }
  304. i += n
  305. }
  306. n := 0
  307. for n < 1 {
  308. n, err = shasToNameWriter.Write([]byte{'\n'})
  309. if err != nil {
  310. errChan <- err
  311. break
  312. }
  313. }
  314. }
  315. }()
  316. wg.Wait()
  317. select {
  318. case err, has := <-errChan:
  319. if has {
  320. ctx.ServerError("LFSPointerFiles", err)
  321. }
  322. default:
  323. }
  324. ctx.Data["Results"] = results
  325. ctx.HTML(200, tplSettingsLFSFileFind)
  326. }
  327. // LFSPointerFiles will search the repository for pointer files and report which are missing LFS files in the content store
  328. func LFSPointerFiles(ctx *context.Context) {
  329. if !setting.LFS.StartServer {
  330. ctx.NotFound("LFSFileGet", nil)
  331. return
  332. }
  333. ctx.Data["PageIsSettingsLFS"] = true
  334. binVersion, err := git.BinVersion()
  335. if err != nil {
  336. log.Fatal("Error retrieving git version: %v", err)
  337. }
  338. ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
  339. basePath := ctx.Repo.Repository.RepoPath()
  340. pointerChan := make(chan pointerResult)
  341. catFileCheckReader, catFileCheckWriter := io.Pipe()
  342. shasToBatchReader, shasToBatchWriter := io.Pipe()
  343. catFileBatchReader, catFileBatchWriter := io.Pipe()
  344. errChan := make(chan error, 1)
  345. wg := sync.WaitGroup{}
  346. wg.Add(5)
  347. var numPointers, numAssociated, numNoExist, numAssociatable int
  348. go func() {
  349. defer wg.Done()
  350. pointers := make([]pointerResult, 0, 50)
  351. for pointer := range pointerChan {
  352. pointers = append(pointers, pointer)
  353. if pointer.InRepo {
  354. numAssociated++
  355. }
  356. if !pointer.Exists {
  357. numNoExist++
  358. }
  359. if !pointer.InRepo && pointer.Accessible {
  360. numAssociatable++
  361. }
  362. }
  363. numPointers = len(pointers)
  364. ctx.Data["Pointers"] = pointers
  365. ctx.Data["NumPointers"] = numPointers
  366. ctx.Data["NumAssociated"] = numAssociated
  367. ctx.Data["NumAssociatable"] = numAssociatable
  368. ctx.Data["NumNoExist"] = numNoExist
  369. ctx.Data["NumNotAssociated"] = numPointers - numAssociated
  370. }()
  371. go createPointerResultsFromCatFileBatch(catFileBatchReader, &wg, pointerChan, ctx.Repo.Repository, ctx.User)
  372. go pipeline.CatFileBatch(shasToBatchReader, catFileBatchWriter, &wg, basePath)
  373. go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg)
  374. if !version.Compare(binVersion, "2.6.0", ">=") {
  375. revListReader, revListWriter := io.Pipe()
  376. shasToCheckReader, shasToCheckWriter := io.Pipe()
  377. wg.Add(2)
  378. go pipeline.CatFileBatchCheck(shasToCheckReader, catFileCheckWriter, &wg, basePath)
  379. go pipeline.BlobsFromRevListObjects(revListReader, shasToCheckWriter, &wg)
  380. go pipeline.RevListAllObjects(revListWriter, &wg, basePath, errChan)
  381. } else {
  382. go pipeline.CatFileBatchCheckAllObjects(catFileCheckWriter, &wg, basePath, errChan)
  383. }
  384. wg.Wait()
  385. select {
  386. case err, has := <-errChan:
  387. if has {
  388. ctx.ServerError("LFSPointerFiles", err)
  389. }
  390. default:
  391. }
  392. ctx.HTML(200, tplSettingsLFSPointers)
  393. }
  394. type pointerResult struct {
  395. SHA string
  396. Oid string
  397. Size int64
  398. InRepo bool
  399. Exists bool
  400. Accessible bool
  401. }
  402. func createPointerResultsFromCatFileBatch(catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pointerChan chan<- pointerResult, repo *models.Repository, user *models.User) {
  403. defer wg.Done()
  404. defer catFileBatchReader.Close()
  405. contentStore := lfs.ContentStore{BasePath: setting.LFS.ContentPath}
  406. bufferedReader := bufio.NewReader(catFileBatchReader)
  407. buf := make([]byte, 1025)
  408. for {
  409. // File descriptor line: sha
  410. sha, err := bufferedReader.ReadString(' ')
  411. if err != nil {
  412. _ = catFileBatchReader.CloseWithError(err)
  413. break
  414. }
  415. // Throw away the blob
  416. if _, err := bufferedReader.ReadString(' '); err != nil {
  417. _ = catFileBatchReader.CloseWithError(err)
  418. break
  419. }
  420. sizeStr, err := bufferedReader.ReadString('\n')
  421. if err != nil {
  422. _ = catFileBatchReader.CloseWithError(err)
  423. break
  424. }
  425. size, err := strconv.Atoi(sizeStr[:len(sizeStr)-1])
  426. if err != nil {
  427. _ = catFileBatchReader.CloseWithError(err)
  428. break
  429. }
  430. pointerBuf := buf[:size+1]
  431. if _, err := io.ReadFull(bufferedReader, pointerBuf); err != nil {
  432. _ = catFileBatchReader.CloseWithError(err)
  433. break
  434. }
  435. pointerBuf = pointerBuf[:size]
  436. // Now we need to check if the pointerBuf is an LFS pointer
  437. pointer := lfs.IsPointerFile(&pointerBuf)
  438. if pointer == nil {
  439. continue
  440. }
  441. result := pointerResult{
  442. SHA: strings.TrimSpace(sha),
  443. Oid: pointer.Oid,
  444. Size: pointer.Size,
  445. }
  446. // Then we need to check that this pointer is in the db
  447. if _, err := repo.GetLFSMetaObjectByOid(pointer.Oid); err != nil {
  448. if err != models.ErrLFSObjectNotExist {
  449. _ = catFileBatchReader.CloseWithError(err)
  450. break
  451. }
  452. } else {
  453. result.InRepo = true
  454. }
  455. result.Exists = contentStore.Exists(pointer)
  456. if result.Exists {
  457. if !result.InRepo {
  458. // Can we fix?
  459. // OK well that's "simple"
  460. // - we need to check whether current user has access to a repo that has access to the file
  461. result.Accessible, err = models.LFSObjectAccessible(user, result.Oid)
  462. if err != nil {
  463. _ = catFileBatchReader.CloseWithError(err)
  464. break
  465. }
  466. } else {
  467. result.Accessible = true
  468. }
  469. }
  470. pointerChan <- result
  471. }
  472. close(pointerChan)
  473. }
  474. // LFSAutoAssociate auto associates accessible lfs files
  475. func LFSAutoAssociate(ctx *context.Context) {
  476. if !setting.LFS.StartServer {
  477. ctx.NotFound("LFSAutoAssociate", nil)
  478. return
  479. }
  480. oids := ctx.QueryStrings("oid")
  481. metas := make([]*models.LFSMetaObject, len(oids))
  482. for i, oid := range oids {
  483. idx := strings.IndexRune(oid, ' ')
  484. if idx < 0 || idx+1 > len(oid) {
  485. ctx.ServerError("LFSAutoAssociate", fmt.Errorf("Illegal oid input: %s", oid))
  486. return
  487. }
  488. var err error
  489. metas[i] = &models.LFSMetaObject{}
  490. metas[i].Size, err = com.StrTo(oid[idx+1:]).Int64()
  491. if err != nil {
  492. ctx.ServerError("LFSAutoAssociate", fmt.Errorf("Illegal oid input: %s %v", oid, err))
  493. return
  494. }
  495. metas[i].Oid = oid[:idx]
  496. //metas[i].RepositoryID = ctx.Repo.Repository.ID
  497. }
  498. if err := models.LFSAutoAssociate(metas, ctx.User, ctx.Repo.Repository.ID); err != nil {
  499. ctx.ServerError("LFSAutoAssociate", err)
  500. return
  501. }
  502. ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs")
  503. }