code.gitea.io/gitea@v1.22.3/services/doctor/dbconsistency.go (about)

     1  // Copyright 2020 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package doctor
     5  
     6  import (
     7  	"context"
     8  
     9  	actions_model "code.gitea.io/gitea/models/actions"
    10  	activities_model "code.gitea.io/gitea/models/activities"
    11  	"code.gitea.io/gitea/models/db"
    12  	issues_model "code.gitea.io/gitea/models/issues"
    13  	"code.gitea.io/gitea/models/migrations"
    14  	repo_model "code.gitea.io/gitea/models/repo"
    15  	"code.gitea.io/gitea/modules/log"
    16  	"code.gitea.io/gitea/modules/setting"
    17  )
    18  
    19  type consistencyCheck struct {
    20  	Name         string
    21  	Counter      func(context.Context) (int64, error)
    22  	Fixer        func(context.Context) (int64, error)
    23  	FixedMessage string
    24  }
    25  
    26  func (c *consistencyCheck) Run(ctx context.Context, logger log.Logger, autofix bool) error {
    27  	count, err := c.Counter(ctx)
    28  	if err != nil {
    29  		logger.Critical("Error: %v whilst counting %s", err, c.Name)
    30  		return err
    31  	}
    32  	if count > 0 {
    33  		if autofix {
    34  			var fixed int64
    35  			if fixed, err = c.Fixer(ctx); err != nil {
    36  				logger.Critical("Error: %v whilst fixing %s", err, c.Name)
    37  				return err
    38  			}
    39  
    40  			prompt := "Deleted"
    41  			if c.FixedMessage != "" {
    42  				prompt = c.FixedMessage
    43  			}
    44  
    45  			if fixed < 0 {
    46  				logger.Info(prompt+" %d %s", count, c.Name)
    47  			} else {
    48  				logger.Info(prompt+" %d/%d %s", fixed, count, c.Name)
    49  			}
    50  		} else {
    51  			logger.Warn("Found %d %s", count, c.Name)
    52  		}
    53  	}
    54  	return nil
    55  }
    56  
    57  func asFixer(fn func(ctx context.Context) error) func(ctx context.Context) (int64, error) {
    58  	return func(ctx context.Context) (int64, error) {
    59  		err := fn(ctx)
    60  		return -1, err
    61  	}
    62  }
    63  
    64  func genericOrphanCheck(name, subject, refObject, joinCond string) consistencyCheck {
    65  	return consistencyCheck{
    66  		Name: name,
    67  		Counter: func(ctx context.Context) (int64, error) {
    68  			return db.CountOrphanedObjects(ctx, subject, refObject, joinCond)
    69  		},
    70  		Fixer: func(ctx context.Context) (int64, error) {
    71  			err := db.DeleteOrphanedObjects(ctx, subject, refObject, joinCond)
    72  			return -1, err
    73  		},
    74  	}
    75  }
    76  
    77  func prepareDBConsistencyChecks() []consistencyCheck {
    78  	consistencyChecks := []consistencyCheck{
    79  		{
    80  			// find labels without existing repo or org
    81  			Name:    "Orphaned Labels without existing repository or organisation",
    82  			Counter: issues_model.CountOrphanedLabels,
    83  			Fixer:   asFixer(issues_model.DeleteOrphanedLabels),
    84  		},
    85  		{
    86  			// find IssueLabels without existing label
    87  			Name:    "Orphaned Issue Labels without existing label",
    88  			Counter: issues_model.CountOrphanedIssueLabels,
    89  			Fixer:   asFixer(issues_model.DeleteOrphanedIssueLabels),
    90  		},
    91  		{
    92  			// find issues without existing repository
    93  			Name:    "Orphaned Issues without existing repository",
    94  			Counter: issues_model.CountOrphanedIssues,
    95  			Fixer:   asFixer(issues_model.DeleteOrphanedIssues),
    96  		},
    97  		// find releases without existing repository
    98  		genericOrphanCheck("Orphaned Releases without existing repository",
    99  			"release", "repository", "`release`.repo_id=repository.id"),
   100  		// find pulls without existing issues
   101  		genericOrphanCheck("Orphaned PullRequests without existing issue",
   102  			"pull_request", "issue", "pull_request.issue_id=issue.id"),
   103  		// find pull requests without base repository
   104  		genericOrphanCheck("Pull request entries without existing base repository",
   105  			"pull_request", "repository", "pull_request.base_repo_id=repository.id"),
   106  		// find tracked times without existing issues/pulls
   107  		genericOrphanCheck("Orphaned TrackedTimes without existing issue",
   108  			"tracked_time", "issue", "tracked_time.issue_id=issue.id"),
   109  		// find attachments without existing issues or releases
   110  		{
   111  			Name:    "Orphaned Attachments without existing issues or releases",
   112  			Counter: repo_model.CountOrphanedAttachments,
   113  			Fixer:   asFixer(repo_model.DeleteOrphanedAttachments),
   114  		},
   115  		// find null archived repositories
   116  		{
   117  			Name:         "Repositories with is_archived IS NULL",
   118  			Counter:      repo_model.CountNullArchivedRepository,
   119  			Fixer:        repo_model.FixNullArchivedRepository,
   120  			FixedMessage: "Fixed",
   121  		},
   122  		// find label comments with empty labels
   123  		{
   124  			Name:         "Label comments with empty labels",
   125  			Counter:      issues_model.CountCommentTypeLabelWithEmptyLabel,
   126  			Fixer:        issues_model.FixCommentTypeLabelWithEmptyLabel,
   127  			FixedMessage: "Fixed",
   128  		},
   129  		// find label comments with labels from outside the repository
   130  		{
   131  			Name:         "Label comments with labels from outside the repository",
   132  			Counter:      issues_model.CountCommentTypeLabelWithOutsideLabels,
   133  			Fixer:        issues_model.FixCommentTypeLabelWithOutsideLabels,
   134  			FixedMessage: "Removed",
   135  		},
   136  		// find issue_label with labels from outside the repository
   137  		{
   138  			Name:         "IssueLabels with Labels from outside the repository",
   139  			Counter:      issues_model.CountIssueLabelWithOutsideLabels,
   140  			Fixer:        issues_model.FixIssueLabelWithOutsideLabels,
   141  			FixedMessage: "Removed",
   142  		},
   143  		{
   144  			Name:         "Action with created_unix set as an empty string",
   145  			Counter:      activities_model.CountActionCreatedUnixString,
   146  			Fixer:        activities_model.FixActionCreatedUnixString,
   147  			FixedMessage: "Set to zero",
   148  		},
   149  		{
   150  			Name:         "Action Runners without existing owner",
   151  			Counter:      actions_model.CountRunnersWithoutBelongingOwner,
   152  			Fixer:        actions_model.FixRunnersWithoutBelongingOwner,
   153  			FixedMessage: "Removed",
   154  		},
   155  		{
   156  			Name:         "Action Runners without existing repository",
   157  			Counter:      actions_model.CountRunnersWithoutBelongingRepo,
   158  			Fixer:        actions_model.FixRunnersWithoutBelongingRepo,
   159  			FixedMessage: "Removed",
   160  		},
   161  		{
   162  			Name:         "Topics with empty repository count",
   163  			Counter:      repo_model.CountOrphanedTopics,
   164  			Fixer:        repo_model.DeleteOrphanedTopics,
   165  			FixedMessage: "Removed",
   166  		},
   167  	}
   168  
   169  	// TODO: function to recalc all counters
   170  
   171  	if setting.Database.Type.IsPostgreSQL() {
   172  		consistencyChecks = append(consistencyChecks, consistencyCheck{
   173  			Name:         "Sequence values",
   174  			Counter:      db.CountBadSequences,
   175  			Fixer:        asFixer(db.FixBadSequences),
   176  			FixedMessage: "Updated",
   177  		})
   178  	}
   179  
   180  	consistencyChecks = append(consistencyChecks,
   181  		// find protected branches without existing repository
   182  		genericOrphanCheck("Protected Branches without existing repository",
   183  			"protected_branch", "repository", "protected_branch.repo_id=repository.id"),
   184  		// find branches without existing repository
   185  		genericOrphanCheck("Branches without existing repository",
   186  			"branch", "repository", "branch.repo_id=repository.id"),
   187  		// find LFS locks without existing repository
   188  		genericOrphanCheck("LFS locks without existing repository",
   189  			"lfs_lock", "repository", "lfs_lock.repo_id=repository.id"),
   190  		// find collaborations without users
   191  		genericOrphanCheck("Collaborations without existing user",
   192  			"collaboration", "user", "collaboration.user_id=`user`.id"),
   193  		// find collaborations without repository
   194  		genericOrphanCheck("Collaborations without existing repository",
   195  			"collaboration", "repository", "collaboration.repo_id=repository.id"),
   196  		// find access without users
   197  		genericOrphanCheck("Access entries without existing user",
   198  			"access", "user", "access.user_id=`user`.id"),
   199  		// find access without repository
   200  		genericOrphanCheck("Access entries without existing repository",
   201  			"access", "repository", "access.repo_id=repository.id"),
   202  		// find action without repository
   203  		genericOrphanCheck("Action entries without existing repository",
   204  			"action", "repository", "action.repo_id=repository.id"),
   205  		// find action without user
   206  		genericOrphanCheck("Action entries without existing user",
   207  			"action", "user", "action.act_user_id=`user`.id"),
   208  		// find OAuth2Grant without existing user
   209  		genericOrphanCheck("Orphaned OAuth2Grant without existing User",
   210  			"oauth2_grant", "user", "oauth2_grant.user_id=`user`.id"),
   211  		// find OAuth2Application without existing user
   212  		genericOrphanCheck("Orphaned OAuth2Application without existing User",
   213  			"oauth2_application", "user", "oauth2_application.uid=0 OR oauth2_application.uid=`user`.id"),
   214  		// find OAuth2AuthorizationCode without existing OAuth2Grant
   215  		genericOrphanCheck("Orphaned OAuth2AuthorizationCode without existing OAuth2Grant",
   216  			"oauth2_authorization_code", "oauth2_grant", "oauth2_authorization_code.grant_id=oauth2_grant.id"),
   217  		// find stopwatches without existing user
   218  		genericOrphanCheck("Orphaned Stopwatches without existing User",
   219  			"stopwatch", "user", "stopwatch.user_id=`user`.id"),
   220  		// find stopwatches without existing issue
   221  		genericOrphanCheck("Orphaned Stopwatches without existing Issue",
   222  			"stopwatch", "issue", "stopwatch.issue_id=`issue`.id"),
   223  		// find redirects without existing user.
   224  		genericOrphanCheck("Orphaned Redirects without existing redirect user",
   225  			"user_redirect", "user", "user_redirect.redirect_user_id=`user`.id"),
   226  	)
   227  	return consistencyChecks
   228  }
   229  
   230  func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) error {
   231  	// make sure DB version is uptodate
   232  	if err := db.InitEngineWithMigration(ctx, migrations.EnsureUpToDate); err != nil {
   233  		logger.Critical("Model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded")
   234  		return err
   235  	}
   236  	consistencyChecks := prepareDBConsistencyChecks()
   237  	for _, c := range consistencyChecks {
   238  		if err := c.Run(ctx, logger, autofix); err != nil {
   239  			return err
   240  		}
   241  	}
   242  
   243  	return nil
   244  }
   245  
   246  func init() {
   247  	Register(&Check{
   248  		Title:     "Check consistency of database",
   249  		Name:      "check-db-consistency",
   250  		IsDefault: false,
   251  		Run:       checkDBConsistency,
   252  		Priority:  3,
   253  	})
   254  }