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.

storage.go 8.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. // Copyright 2021 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package doctor
  4. import (
  5. "context"
  6. "errors"
  7. "io/fs"
  8. "strings"
  9. "code.gitea.io/gitea/models/git"
  10. "code.gitea.io/gitea/models/packages"
  11. "code.gitea.io/gitea/models/repo"
  12. "code.gitea.io/gitea/models/user"
  13. "code.gitea.io/gitea/modules/base"
  14. "code.gitea.io/gitea/modules/log"
  15. packages_module "code.gitea.io/gitea/modules/packages"
  16. "code.gitea.io/gitea/modules/setting"
  17. "code.gitea.io/gitea/modules/storage"
  18. "code.gitea.io/gitea/modules/util"
  19. )
  20. type commonStorageCheckOptions struct {
  21. storer storage.ObjectStorage
  22. isOrphaned func(path string, obj storage.Object, stat fs.FileInfo) (bool, error)
  23. name string
  24. }
  25. func commonCheckStorage(ctx context.Context, logger log.Logger, autofix bool, opts *commonStorageCheckOptions) error {
  26. totalCount, orphanedCount := 0, 0
  27. totalSize, orphanedSize := int64(0), int64(0)
  28. var pathsToDelete []string
  29. if err := opts.storer.IterateObjects("", func(p string, obj storage.Object) error {
  30. defer obj.Close()
  31. totalCount++
  32. stat, err := obj.Stat()
  33. if err != nil {
  34. return err
  35. }
  36. totalSize += stat.Size()
  37. orphaned, err := opts.isOrphaned(p, obj, stat)
  38. if err != nil {
  39. return err
  40. }
  41. if orphaned {
  42. orphanedCount++
  43. orphanedSize += stat.Size()
  44. if autofix {
  45. pathsToDelete = append(pathsToDelete, p)
  46. }
  47. }
  48. return nil
  49. }); err != nil {
  50. logger.Error("Error whilst iterating %s storage: %v", opts.name, err)
  51. return err
  52. }
  53. if orphanedCount > 0 {
  54. if autofix {
  55. var deletedNum int
  56. for _, p := range pathsToDelete {
  57. if err := opts.storer.Delete(p); err != nil {
  58. log.Error("Error whilst deleting %s from %s storage: %v", p, opts.name, err)
  59. } else {
  60. deletedNum++
  61. }
  62. }
  63. logger.Info("Deleted %d/%d orphaned %s(s)", deletedNum, orphanedCount, opts.name)
  64. } else {
  65. logger.Warn("Found %d/%d (%s/%s) orphaned %s(s)", orphanedCount, totalCount, base.FileSize(orphanedSize), base.FileSize(totalSize), opts.name)
  66. }
  67. } else {
  68. logger.Info("Found %d (%s) %s(s)", totalCount, base.FileSize(totalSize), opts.name)
  69. }
  70. return nil
  71. }
  72. type checkStorageOptions struct {
  73. All bool
  74. Attachments bool
  75. LFS bool
  76. Avatars bool
  77. RepoAvatars bool
  78. RepoArchives bool
  79. Packages bool
  80. }
  81. // checkStorage will return a doctor check function to check the requested storage types for "orphaned" stored object/files and optionally delete them
  82. func checkStorage(opts *checkStorageOptions) func(ctx context.Context, logger log.Logger, autofix bool) error {
  83. return func(ctx context.Context, logger log.Logger, autofix bool) error {
  84. if err := storage.Init(); err != nil {
  85. logger.Error("storage.Init failed: %v", err)
  86. return err
  87. }
  88. if opts.Attachments || opts.All {
  89. if err := commonCheckStorage(ctx, logger, autofix,
  90. &commonStorageCheckOptions{
  91. storer: storage.Attachments,
  92. isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
  93. exists, err := repo.ExistAttachmentsByUUID(ctx, stat.Name())
  94. return !exists, err
  95. },
  96. name: "attachment",
  97. }); err != nil {
  98. return err
  99. }
  100. }
  101. if opts.LFS || opts.All {
  102. if !setting.LFS.StartServer {
  103. logger.Info("LFS isn't enabled (skipped)")
  104. return nil
  105. }
  106. if err := commonCheckStorage(ctx, logger, autofix,
  107. &commonStorageCheckOptions{
  108. storer: storage.LFS,
  109. isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
  110. // The oid of an LFS stored object is the name but with all the path.Separators removed
  111. oid := strings.ReplaceAll(path, "/", "")
  112. exists, err := git.ExistsLFSObject(ctx, oid)
  113. return !exists, err
  114. },
  115. name: "LFS file",
  116. }); err != nil {
  117. return err
  118. }
  119. }
  120. if opts.Avatars || opts.All {
  121. if err := commonCheckStorage(ctx, logger, autofix,
  122. &commonStorageCheckOptions{
  123. storer: storage.Avatars,
  124. isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
  125. exists, err := user.ExistsWithAvatarAtStoragePath(ctx, path)
  126. return !exists, err
  127. },
  128. name: "avatar",
  129. }); err != nil {
  130. return err
  131. }
  132. }
  133. if opts.RepoAvatars || opts.All {
  134. if err := commonCheckStorage(ctx, logger, autofix,
  135. &commonStorageCheckOptions{
  136. storer: storage.RepoAvatars,
  137. isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
  138. exists, err := repo.ExistsWithAvatarAtStoragePath(ctx, path)
  139. return !exists, err
  140. },
  141. name: "repo avatar",
  142. }); err != nil {
  143. return err
  144. }
  145. }
  146. if opts.RepoArchives || opts.All {
  147. if err := commonCheckStorage(ctx, logger, autofix,
  148. &commonStorageCheckOptions{
  149. storer: storage.RepoArchives,
  150. isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
  151. exists, err := repo.ExistsRepoArchiverWithStoragePath(ctx, path)
  152. if err == nil || errors.Is(err, util.ErrInvalidArgument) {
  153. // invalid arguments mean that the object is not a valid repo archiver and it should be removed
  154. return !exists, nil
  155. }
  156. return !exists, err
  157. },
  158. name: "repo archive",
  159. }); err != nil {
  160. return err
  161. }
  162. }
  163. if opts.Packages || opts.All {
  164. if !setting.Packages.Enabled {
  165. logger.Info("Packages isn't enabled (skipped)")
  166. return nil
  167. }
  168. if err := commonCheckStorage(ctx, logger, autofix,
  169. &commonStorageCheckOptions{
  170. storer: storage.Packages,
  171. isOrphaned: func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) {
  172. key, err := packages_module.RelativePathToKey(path)
  173. if err != nil {
  174. // If there is an error here then the relative path does not match a valid package
  175. // Therefore it is orphaned by default
  176. return true, nil
  177. }
  178. exists, err := packages.ExistPackageBlobWithSHA(ctx, string(key))
  179. return !exists, err
  180. },
  181. name: "package blob",
  182. }); err != nil {
  183. return err
  184. }
  185. }
  186. return nil
  187. }
  188. }
  189. func init() {
  190. Register(&Check{
  191. Title: "Check if there are orphaned storage files",
  192. Name: "storages",
  193. IsDefault: false,
  194. Run: checkStorage(&checkStorageOptions{All: true}),
  195. AbortIfFailed: false,
  196. SkipDatabaseInitialization: false,
  197. Priority: 1,
  198. })
  199. Register(&Check{
  200. Title: "Check if there are orphaned attachments in storage",
  201. Name: "storage-attachments",
  202. IsDefault: false,
  203. Run: checkStorage(&checkStorageOptions{Attachments: true}),
  204. AbortIfFailed: false,
  205. SkipDatabaseInitialization: false,
  206. Priority: 1,
  207. })
  208. Register(&Check{
  209. Title: "Check if there are orphaned lfs files in storage",
  210. Name: "storage-lfs",
  211. IsDefault: false,
  212. Run: checkStorage(&checkStorageOptions{LFS: true}),
  213. AbortIfFailed: false,
  214. SkipDatabaseInitialization: false,
  215. Priority: 1,
  216. })
  217. Register(&Check{
  218. Title: "Check if there are orphaned avatars in storage",
  219. Name: "storage-avatars",
  220. IsDefault: false,
  221. Run: checkStorage(&checkStorageOptions{Avatars: true, RepoAvatars: true}),
  222. AbortIfFailed: false,
  223. SkipDatabaseInitialization: false,
  224. Priority: 1,
  225. })
  226. Register(&Check{
  227. Title: "Check if there are orphaned archives in storage",
  228. Name: "storage-archives",
  229. IsDefault: false,
  230. Run: checkStorage(&checkStorageOptions{RepoArchives: true}),
  231. AbortIfFailed: false,
  232. SkipDatabaseInitialization: false,
  233. Priority: 1,
  234. })
  235. Register(&Check{
  236. Title: "Check if there are orphaned package blobs in storage",
  237. Name: "storage-packages",
  238. IsDefault: false,
  239. Run: checkStorage(&checkStorageOptions{Packages: true}),
  240. AbortIfFailed: false,
  241. SkipDatabaseInitialization: false,
  242. Priority: 1,
  243. })
  244. }