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.

lfs.go 4.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repository
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "time"
  9. git_model "code.gitea.io/gitea/models/git"
  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/log"
  14. "code.gitea.io/gitea/modules/setting"
  15. )
  16. // GarbageCollectLFSMetaObjectsOptions provides options for GarbageCollectLFSMetaObjects function
  17. type GarbageCollectLFSMetaObjectsOptions struct {
  18. Logger log.Logger
  19. AutoFix bool
  20. OlderThan time.Time
  21. UpdatedLessRecentlyThan time.Time
  22. NumberToCheckPerRepo int64
  23. ProportionToCheckPerRepo float64
  24. }
  25. // GarbageCollectLFSMetaObjects garbage collects LFS objects for all repositories
  26. func GarbageCollectLFSMetaObjects(ctx context.Context, opts GarbageCollectLFSMetaObjectsOptions) error {
  27. log.Trace("Doing: GarbageCollectLFSMetaObjects")
  28. defer log.Trace("Finished: GarbageCollectLFSMetaObjects")
  29. if !setting.LFS.StartServer {
  30. if opts.Logger != nil {
  31. opts.Logger.Info("LFS support is disabled")
  32. }
  33. return nil
  34. }
  35. return git_model.IterateRepositoryIDsWithLFSMetaObjects(ctx, func(ctx context.Context, repoID, count int64) error {
  36. repo, err := repo_model.GetRepositoryByID(ctx, repoID)
  37. if err != nil {
  38. return err
  39. }
  40. if newMinimum := int64(float64(count) * opts.ProportionToCheckPerRepo); newMinimum > opts.NumberToCheckPerRepo && opts.NumberToCheckPerRepo != 0 {
  41. opts.NumberToCheckPerRepo = newMinimum
  42. }
  43. return GarbageCollectLFSMetaObjectsForRepo(ctx, repo, opts)
  44. })
  45. }
  46. // GarbageCollectLFSMetaObjectsForRepo garbage collects LFS objects for a specific repository
  47. func GarbageCollectLFSMetaObjectsForRepo(ctx context.Context, repo *repo_model.Repository, opts GarbageCollectLFSMetaObjectsOptions) error {
  48. if opts.Logger != nil {
  49. opts.Logger.Info("Checking %-v", repo)
  50. }
  51. total, orphaned, collected, deleted := int64(0), 0, 0, 0
  52. if opts.Logger != nil {
  53. defer func() {
  54. if orphaned == 0 {
  55. opts.Logger.Info("Found %d total LFSMetaObjects in %-v", total, repo)
  56. } else if !opts.AutoFix {
  57. opts.Logger.Info("Found %d/%d orphaned LFSMetaObjects in %-v", orphaned, total, repo)
  58. } else {
  59. opts.Logger.Info("Collected %d/%d orphaned/%d total LFSMetaObjects in %-v. %d removed from storage.", collected, orphaned, total, repo, deleted)
  60. }
  61. }()
  62. }
  63. gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
  64. if err != nil {
  65. log.Error("Unable to open git repository %-v: %v", repo, err)
  66. return err
  67. }
  68. defer gitRepo.Close()
  69. store := lfs.NewContentStore()
  70. errStop := errors.New("STOPERR")
  71. err = git_model.IterateLFSMetaObjectsForRepo(ctx, repo.ID, func(ctx context.Context, metaObject *git_model.LFSMetaObject, count int64) error {
  72. if opts.NumberToCheckPerRepo > 0 && total > opts.NumberToCheckPerRepo {
  73. return errStop
  74. }
  75. total++
  76. pointerSha := git.ComputeBlobHash([]byte(metaObject.Pointer.StringContent()))
  77. if gitRepo.IsObjectExist(pointerSha.String()) {
  78. return git_model.MarkLFSMetaObject(ctx, metaObject.ID)
  79. }
  80. orphaned++
  81. if !opts.AutoFix {
  82. return nil
  83. }
  84. // Non-existent pointer file
  85. _, err = git_model.RemoveLFSMetaObjectByOidFn(ctx, repo.ID, metaObject.Oid, func(count int64) error {
  86. if count > 0 {
  87. return nil
  88. }
  89. if err := store.Delete(metaObject.RelativePath()); err != nil {
  90. log.Error("Unable to remove lfs metaobject %s from store: %v", metaObject.Oid, err)
  91. }
  92. deleted++
  93. return nil
  94. })
  95. if err != nil {
  96. return fmt.Errorf("unable to remove meta-object %s in %s: %w", metaObject.Oid, repo.FullName(), err)
  97. }
  98. collected++
  99. return nil
  100. }, &git_model.IterateLFSMetaObjectsForRepoOptions{
  101. // Only attempt to garbage collect lfs meta objects older than a week as the order of git lfs upload
  102. // and git object upload is not necessarily guaranteed. It's possible to imagine a situation whereby
  103. // an LFS object is uploaded but the git branch is not uploaded immediately, or there are some rapid
  104. // changes in new branches that might lead to lfs objects becoming temporarily unassociated with git
  105. // objects.
  106. //
  107. // It is likely that a week is potentially excessive but it should definitely be enough that any
  108. // unassociated LFS object is genuinely unassociated.
  109. OlderThan: opts.OlderThan,
  110. UpdatedLessRecentlyThan: opts.UpdatedLessRecentlyThan,
  111. OrderByUpdated: true,
  112. LoopFunctionAlwaysUpdates: true,
  113. })
  114. if err == errStop {
  115. if opts.Logger != nil {
  116. opts.Logger.Info("Processing stopped at %d total LFSMetaObjects in %-v", total, repo)
  117. }
  118. return nil
  119. } else if err != nil {
  120. return err
  121. }
  122. return nil
  123. }