mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
271 lines
8.1 KiB
Go
271 lines
8.1 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package doctor
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/models/git"
|
|
"github.com/hanzoai/git/models/packages"
|
|
"github.com/hanzoai/git/models/repo"
|
|
"github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/modules/base"
|
|
"github.com/hanzoai/git/modules/log"
|
|
packages_module "github.com/hanzoai/git/modules/packages"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/storage"
|
|
"github.com/hanzoai/git/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(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,
|
|
})
|
|
}
|