2021-09-06 17:46:20 +03:00
// Copyright 2021 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2021-09-06 17:46:20 +03:00
package doctor
import (
2022-01-20 02:26:57 +03:00
"context"
2022-11-15 11:08:59 +03:00
"errors"
"io/fs"
"strings"
2022-01-20 02:26:57 +03:00
2022-11-15 11:08:59 +03:00
"code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
2021-09-06 17:46:20 +03:00
"code.gitea.io/gitea/modules/log"
2022-11-15 11:08:59 +03:00
packages_module "code.gitea.io/gitea/modules/packages"
2023-03-23 15:30:28 +03:00
"code.gitea.io/gitea/modules/setting"
2021-09-06 17:46:20 +03:00
"code.gitea.io/gitea/modules/storage"
2022-11-15 11:08:59 +03:00
"code.gitea.io/gitea/modules/util"
2021-09-06 17:46:20 +03:00
)
2022-11-15 11:08:59 +03:00
type commonStorageCheckOptions struct {
storer storage . ObjectStorage
isOrphaned func ( path string , obj storage . Object , stat fs . FileInfo ) ( bool , error )
name string
}
func commonCheckStorage ( ctx context . Context , logger log . Logger , autofix bool , opts * commonStorageCheckOptions ) error {
totalCount , orphanedCount := 0 , 0
totalSize , orphanedSize := int64 ( 0 ) , int64 ( 0 )
var pathsToDelete [ ] string
2023-03-13 13:23:51 +03:00
if err := opts . storer . IterateObjects ( "" , func ( p string , obj storage . Object ) error {
2021-09-06 17:46:20 +03:00
defer obj . Close ( )
2022-11-15 11:08:59 +03:00
totalCount ++
2021-09-06 17:46:20 +03:00
stat , err := obj . Stat ( )
if err != nil {
return err
}
2022-11-15 11:08:59 +03:00
totalSize += stat . Size ( )
orphaned , err := opts . isOrphaned ( p , obj , stat )
2021-09-06 17:46:20 +03:00
if err != nil {
return err
}
2022-11-15 11:08:59 +03:00
if orphaned {
orphanedCount ++
orphanedSize += stat . Size ( )
2021-09-06 17:46:20 +03:00
if autofix {
2022-11-15 11:08:59 +03:00
pathsToDelete = append ( pathsToDelete , p )
2021-09-06 17:46:20 +03:00
}
}
return nil
} ) ; err != nil {
2022-11-15 11:08:59 +03:00
logger . Error ( "Error whilst iterating %s storage: %v" , opts . name , err )
2021-09-06 17:46:20 +03:00
return err
}
2022-11-15 11:08:59 +03:00
if orphanedCount > 0 {
2021-09-06 17:46:20 +03:00
if autofix {
var deletedNum int
2022-11-15 11:08:59 +03:00
for _ , p := range pathsToDelete {
if err := opts . storer . Delete ( p ) ; err != nil {
log . Error ( "Error whilst deleting %s from %s storage: %v" , p , opts . name , err )
2021-09-06 17:46:20 +03:00
} else {
deletedNum ++
}
}
2022-11-15 11:08:59 +03:00
logger . Info ( "Deleted %d/%d orphaned %s(s)" , deletedNum , orphanedCount , opts . name )
2021-09-06 17:46:20 +03:00
} else {
2022-11-15 11:08:59 +03:00
logger . Warn ( "Found %d/%d (%s/%s) orphaned %s(s)" , orphanedCount , totalCount , base . FileSize ( orphanedSize ) , base . FileSize ( totalSize ) , opts . name )
2021-09-06 17:46:20 +03:00
}
2022-11-15 11:08:59 +03:00
} else {
logger . Info ( "Found %d (%s) %s(s)" , totalCount , base . FileSize ( totalSize ) , opts . name )
2021-09-06 17:46:20 +03:00
}
return nil
}
2022-11-15 11:08:59 +03:00
type checkStorageOptions struct {
All bool
Attachments bool
LFS bool
Avatars bool
RepoAvatars bool
RepoArchives bool
Packages bool
}
// checkStorage will return a doctor check function to check the requested storage types for "orphaned" stored object/files and optionally delete them
func checkStorage ( opts * checkStorageOptions ) func ( ctx context . Context , logger log . Logger , autofix bool ) error {
return func ( ctx context . Context , logger log . Logger , autofix bool ) error {
if err := storage . Init ( ) ; err != nil {
logger . Error ( "storage.Init failed: %v" , err )
return err
}
if opts . Attachments || opts . All {
if err := commonCheckStorage ( ctx , logger , autofix ,
& commonStorageCheckOptions {
storer : storage . Attachments ,
isOrphaned : func ( path string , obj storage . Object , stat fs . FileInfo ) ( bool , error ) {
exists , err := repo . ExistAttachmentsByUUID ( ctx , stat . Name ( ) )
return ! exists , err
} ,
name : "attachment" ,
} ) ; err != nil {
return err
}
}
if opts . LFS || opts . All {
2023-03-23 15:30:28 +03:00
if ! setting . LFS . StartServer {
logger . Info ( "LFS isn't enabled (skipped)" )
return nil
}
2022-11-15 11:08:59 +03:00
if err := commonCheckStorage ( ctx , logger , autofix ,
& commonStorageCheckOptions {
storer : storage . LFS ,
isOrphaned : func ( path string , obj storage . Object , stat fs . FileInfo ) ( bool , error ) {
// The oid of an LFS stored object is the name but with all the path.Separators removed
oid := strings . ReplaceAll ( path , "/" , "" )
exists , err := git . ExistsLFSObject ( ctx , oid )
return ! exists , err
} ,
name : "LFS file" ,
} ) ; err != nil {
return err
}
}
if opts . Avatars || opts . All {
if err := commonCheckStorage ( ctx , logger , autofix ,
& commonStorageCheckOptions {
storer : storage . Avatars ,
isOrphaned : func ( path string , obj storage . Object , stat fs . FileInfo ) ( bool , error ) {
exists , err := user . ExistsWithAvatarAtStoragePath ( ctx , path )
return ! exists , err
} ,
name : "avatar" ,
} ) ; err != nil {
return err
}
}
if opts . RepoAvatars || opts . All {
if err := commonCheckStorage ( ctx , logger , autofix ,
& commonStorageCheckOptions {
storer : storage . RepoAvatars ,
isOrphaned : func ( path string , obj storage . Object , stat fs . FileInfo ) ( bool , error ) {
exists , err := repo . ExistsWithAvatarAtStoragePath ( ctx , path )
return ! exists , err
} ,
name : "repo avatar" ,
} ) ; err != nil {
return err
}
}
if opts . RepoArchives || opts . All {
if err := commonCheckStorage ( ctx , logger , autofix ,
& commonStorageCheckOptions {
storer : storage . RepoAvatars ,
isOrphaned : func ( path string , obj storage . Object , stat fs . FileInfo ) ( bool , error ) {
exists , err := repo . ExistsRepoArchiverWithStoragePath ( ctx , path )
if err == nil || errors . Is ( err , util . ErrInvalidArgument ) {
// invalid arguments mean that the object is not a valid repo archiver and it should be removed
return ! exists , nil
}
return ! exists , err
} ,
name : "repo archive" ,
} ) ; err != nil {
return err
}
}
if opts . Packages || opts . All {
2023-03-23 15:30:28 +03:00
if ! setting . Packages . Enabled {
logger . Info ( "Packages isn't enabled (skipped)" )
return nil
}
2022-11-15 11:08:59 +03:00
if err := commonCheckStorage ( ctx , logger , autofix ,
& commonStorageCheckOptions {
storer : storage . Packages ,
isOrphaned : func ( path string , obj storage . Object , stat fs . FileInfo ) ( bool , error ) {
key , err := packages_module . RelativePathToKey ( path )
if err != nil {
// If there is an error here then the relative path does not match a valid package
// Therefore it is orphaned by default
return true , nil
}
exists , err := packages . ExistPackageBlobWithSHA ( ctx , string ( key ) )
return ! exists , err
} ,
name : "package blob" ,
} ) ; err != nil {
return err
}
}
return nil
2021-09-06 17:46:20 +03:00
}
}
func init ( ) {
Register ( & Check {
2022-11-15 11:08:59 +03:00
Title : "Check if there are orphaned storage files" ,
2021-09-06 17:46:20 +03:00
Name : "storages" ,
IsDefault : false ,
2022-11-15 11:08:59 +03:00
Run : checkStorage ( & checkStorageOptions { All : true } ) ,
AbortIfFailed : false ,
SkipDatabaseInitialization : false ,
Priority : 1 ,
} )
Register ( & Check {
Title : "Check if there are orphaned attachments in storage" ,
Name : "storage-attachments" ,
IsDefault : false ,
Run : checkStorage ( & checkStorageOptions { Attachments : true } ) ,
AbortIfFailed : false ,
SkipDatabaseInitialization : false ,
Priority : 1 ,
} )
Register ( & Check {
Title : "Check if there are orphaned lfs files in storage" ,
Name : "storage-lfs" ,
IsDefault : false ,
Run : checkStorage ( & checkStorageOptions { LFS : true } ) ,
AbortIfFailed : false ,
SkipDatabaseInitialization : false ,
Priority : 1 ,
} )
Register ( & Check {
Title : "Check if there are orphaned avatars in storage" ,
Name : "storage-avatars" ,
IsDefault : false ,
Run : checkStorage ( & checkStorageOptions { Avatars : true , RepoAvatars : true } ) ,
AbortIfFailed : false ,
SkipDatabaseInitialization : false ,
Priority : 1 ,
} )
Register ( & Check {
Title : "Check if there are orphaned archives in storage" ,
Name : "storage-archives" ,
IsDefault : false ,
Run : checkStorage ( & checkStorageOptions { RepoArchives : true } ) ,
AbortIfFailed : false ,
SkipDatabaseInitialization : false ,
Priority : 1 ,
} )
Register ( & Check {
Title : "Check if there are orphaned package blobs in storage" ,
Name : "storage-packages" ,
IsDefault : false ,
Run : checkStorage ( & checkStorageOptions { Packages : true } ) ,
2021-09-06 17:46:20 +03:00
AbortIfFailed : false ,
SkipDatabaseInitialization : false ,
Priority : 1 ,
} )
}