@ -6,71 +6,255 @@ package doctor
import (
"context"
"errors"
"io/fs"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"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"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/util"
)
func checkAttachmentStorageFiles ( logger log . Logger , autofix bool ) error {
var total , garbageNum int
var deletePaths [ ] string
if err := storage . Attachments . IterateObjects ( func ( p string , obj storage . Object ) error {
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
if err := opts . storer . IterateObjects ( func ( p string , obj storage . Object ) error {
defer obj . Close ( )
total ++
totalCount ++
stat , err := obj . Stat ( )
if err != nil {
return err
}
exist , err := repo_model . ExistAttachmentsByUUID ( stat . Name ( ) )
totalSize += stat . Size ( )
orphaned , err := opts . isOrphaned ( p , obj , stat )
if err != nil {
return err
}
if ! exist {
garbageNum ++
if orphaned {
orphanedCount ++
orphanedSize += stat . Size ( )
if autofix {
deletePaths = append ( deletePaths , p )
pathsToDelete = append ( pathsToDelete , p )
}
}
return nil
} ) ; err != nil {
logger . Error ( "storage.Attachments.IterateObjects failed: %v" , err )
logger . Error ( "Error whilst iterating %s storage: %v" , opts . name , err )
return err
}
if garbageNum > 0 {
if orphanedCount > 0 {
if autofix {
var deletedNum int
for _ , p := range deletePaths {
if err := storage . Attachments . Delete ( p ) ; err != nil {
log . Error ( "Delete attachment %s failed : %v" , p , err )
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 )
} else {
deletedNum ++
}
}
logger . Info ( "%d missed information attachment detected, %d deleted." , garbageNum , deletedNum )
logger . Info ( "Deleted %d/%d orphaned %s(s)" , deletedNum , orphanedCount , opts . name )
} else {
logger . Warn ( "Checked %d attachment, %d missed information." , total , garbageNum )
logger . Warn ( "Found %d/%d (%s/%s) orphaned %s(s)" , orphanedCount , totalCount , base . FileSize ( orphanedSize ) , base . FileSize ( totalSize ) , opts . name )
}
} else {
logger . Info ( "Found %d (%s) %s(s)" , totalCount , base . FileSize ( totalSize ) , opts . name )
}
return nil
}
func checkStorageFiles ( ctx context . Context , logger log . Logger , autofix bool ) error {
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
}
return checkAttachmentStorageFiles ( logger , autofix )
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 {
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 {
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
}
}
func init ( ) {
Register ( & Check {
Title : "Check if there is garbage storage files" ,
Title : "Check if there are orphaned storage files" ,
Name : "storages" ,
IsDefault : false ,
Run : checkStorageFiles ,
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 } ) ,
AbortIfFailed : false ,
SkipDatabaseInitialization : false ,
Priority : 1 ,