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