mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
270 lines
8.1 KiB
270 lines
8.1 KiB
// Copyright 2021 The Gitea Authors. All rights reserved. |
|
// SPDX-License-Identifier: MIT |
|
|
|
package doctor |
|
|
|
import ( |
|
"context" |
|
"errors" |
|
"io/fs" |
|
"strings" |
|
|
|
"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/setting" |
|
"code.gitea.io/gitea/modules/storage" |
|
"code.gitea.io/gitea/modules/util" |
|
) |
|
|
|
type commonStorageCheckOptions struct { |
|
storer storage.ObjectStorage |
|
isOrphaned func(path string, obj storage.Object, stat fs.FileInfo) (bool, error) |
|
name string |
|
} |
|
|
|
func commonCheckStorage(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() |
|
|
|
totalCount++ |
|
stat, err := obj.Stat() |
|
if err != nil { |
|
return err |
|
} |
|
totalSize += stat.Size() |
|
|
|
orphaned, err := opts.isOrphaned(p, obj, stat) |
|
if err != nil { |
|
return err |
|
} |
|
if orphaned { |
|
orphanedCount++ |
|
orphanedSize += stat.Size() |
|
if autofix { |
|
pathsToDelete = append(pathsToDelete, p) |
|
} |
|
} |
|
return nil |
|
}); err != nil { |
|
logger.Error("Error whilst iterating %s storage: %v", opts.name, err) |
|
return err |
|
} |
|
|
|
if orphanedCount > 0 { |
|
if autofix { |
|
var deletedNum int |
|
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("Deleted %d/%d orphaned %s(s)", deletedNum, orphanedCount, opts.name) |
|
} else { |
|
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 |
|
} |
|
|
|
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(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 !setting.LFS.StartServer { |
|
logger.Info("LFS isn't enabled (skipped)") |
|
return nil |
|
} |
|
if err := commonCheckStorage(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(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(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(logger, autofix, |
|
&commonStorageCheckOptions{ |
|
storer: storage.RepoArchives, |
|
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 !setting.Packages.Enabled { |
|
logger.Info("Packages isn't enabled (skipped)") |
|
return nil |
|
} |
|
if err := commonCheckStorage(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 are orphaned storage files", |
|
Name: "storages", |
|
IsDefault: false, |
|
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, |
|
}) |
|
}
|
|
|