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  }