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.7KB

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