mirror of
https://github.com/ipfs/kubo.git
synced 2025-08-23 18:11:27 +08:00

https://github.com/ipfs/kubo/pull/10883 https://github.com/ipshipyard/config.ipfs-mainnet.org/issues/3 --------- Co-authored-by: gammazero <gammazero@users.noreply.github.com>
454 lines
12 KiB
Go
454 lines
12 KiB
Go
package commands
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"text/tabwriter"
|
|
|
|
oldcmds "github.com/ipfs/kubo/commands"
|
|
cmdenv "github.com/ipfs/kubo/core/commands/cmdenv"
|
|
corerepo "github.com/ipfs/kubo/core/corerepo"
|
|
fsrepo "github.com/ipfs/kubo/repo/fsrepo"
|
|
"github.com/ipfs/kubo/repo/fsrepo/migrations"
|
|
|
|
humanize "github.com/dustin/go-humanize"
|
|
bstore "github.com/ipfs/boxo/blockstore"
|
|
cid "github.com/ipfs/go-cid"
|
|
cmds "github.com/ipfs/go-ipfs-cmds"
|
|
)
|
|
|
|
type RepoVersion struct {
|
|
Version string
|
|
}
|
|
|
|
var RepoCmd = &cmds.Command{
|
|
Helptext: cmds.HelpText{
|
|
Tagline: "Manipulate the IPFS repo.",
|
|
ShortDescription: `
|
|
'ipfs repo' is a plumbing command used to manipulate the repo.
|
|
`,
|
|
},
|
|
|
|
Subcommands: map[string]*cmds.Command{
|
|
"stat": repoStatCmd,
|
|
"gc": repoGcCmd,
|
|
"version": repoVersionCmd,
|
|
"verify": repoVerifyCmd,
|
|
"migrate": repoMigrateCmd,
|
|
"ls": RefsLocalCmd,
|
|
},
|
|
}
|
|
|
|
// GcResult is the result returned by "repo gc" command.
|
|
type GcResult struct {
|
|
Key cid.Cid
|
|
Error string `json:",omitempty"`
|
|
}
|
|
|
|
const (
|
|
repoStreamErrorsOptionName = "stream-errors"
|
|
repoQuietOptionName = "quiet"
|
|
repoSilentOptionName = "silent"
|
|
repoAllowDowngradeOptionName = "allow-downgrade"
|
|
repoToVersionOptionName = "to"
|
|
)
|
|
|
|
var repoGcCmd = &cmds.Command{
|
|
Helptext: cmds.HelpText{
|
|
Tagline: "Perform a garbage collection sweep on the repo.",
|
|
ShortDescription: `
|
|
'ipfs repo gc' is a plumbing command that will sweep the local
|
|
set of stored objects and remove ones that are not pinned in
|
|
order to reclaim hard disk space.
|
|
`,
|
|
},
|
|
Options: []cmds.Option{
|
|
cmds.BoolOption(repoStreamErrorsOptionName, "Stream errors."),
|
|
cmds.BoolOption(repoQuietOptionName, "q", "Write minimal output."),
|
|
cmds.BoolOption(repoSilentOptionName, "Write no output."),
|
|
},
|
|
Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error {
|
|
n, err := cmdenv.GetNode(env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
silent, _ := req.Options[repoSilentOptionName].(bool)
|
|
streamErrors, _ := req.Options[repoStreamErrorsOptionName].(bool)
|
|
|
|
gcOutChan := corerepo.GarbageCollectAsync(n, req.Context)
|
|
|
|
if streamErrors {
|
|
errs := false
|
|
for res := range gcOutChan {
|
|
if res.Error != nil {
|
|
if err := re.Emit(&GcResult{Error: res.Error.Error()}); err != nil {
|
|
return err
|
|
}
|
|
errs = true
|
|
} else {
|
|
if err := re.Emit(&GcResult{Key: res.KeyRemoved}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if errs {
|
|
return errors.New("encountered errors during gc run")
|
|
}
|
|
} else {
|
|
err := corerepo.CollectResult(req.Context, gcOutChan, func(k cid.Cid) {
|
|
if silent {
|
|
return
|
|
}
|
|
// Nothing to do with this error, really. This
|
|
// most likely means that the client is gone but
|
|
// we still need to let the GC finish.
|
|
_ = re.Emit(&GcResult{Key: k})
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
Type: GcResult{},
|
|
Encoders: cmds.EncoderMap{
|
|
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, gcr *GcResult) error {
|
|
quiet, _ := req.Options[repoQuietOptionName].(bool)
|
|
silent, _ := req.Options[repoSilentOptionName].(bool)
|
|
|
|
if silent {
|
|
return nil
|
|
}
|
|
|
|
if gcr.Error != "" {
|
|
_, err := fmt.Fprintf(w, "Error: %s\n", gcr.Error)
|
|
return err
|
|
}
|
|
|
|
prefix := "removed "
|
|
if quiet {
|
|
prefix = ""
|
|
}
|
|
|
|
_, err := fmt.Fprintf(w, "%s%s\n", prefix, gcr.Key)
|
|
return err
|
|
}),
|
|
},
|
|
}
|
|
|
|
const (
|
|
repoSizeOnlyOptionName = "size-only"
|
|
repoHumanOptionName = "human"
|
|
)
|
|
|
|
var repoStatCmd = &cmds.Command{
|
|
Helptext: cmds.HelpText{
|
|
Tagline: "Get stats for the currently used repo.",
|
|
ShortDescription: `
|
|
'ipfs repo stat' provides information about the local set of
|
|
stored objects. It outputs:
|
|
|
|
RepoSize int Size in bytes that the repo is currently taking.
|
|
StorageMax string Maximum datastore size (from configuration)
|
|
NumObjects int Number of objects in the local repo.
|
|
RepoPath string The path to the repo being currently used.
|
|
Version string The repo version.
|
|
`,
|
|
},
|
|
Options: []cmds.Option{
|
|
cmds.BoolOption(repoSizeOnlyOptionName, "s", "Only report RepoSize and StorageMax."),
|
|
cmds.BoolOption(repoHumanOptionName, "H", "Print sizes in human readable format (e.g., 1K 234M 2G)"),
|
|
},
|
|
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
|
n, err := cmdenv.GetNode(env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sizeOnly, _ := req.Options[repoSizeOnlyOptionName].(bool)
|
|
if sizeOnly {
|
|
sizeStat, err := corerepo.RepoSize(req.Context, n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return cmds.EmitOnce(res, &corerepo.Stat{
|
|
SizeStat: sizeStat,
|
|
})
|
|
}
|
|
|
|
stat, err := corerepo.RepoStat(req.Context, n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return cmds.EmitOnce(res, &stat)
|
|
},
|
|
Type: &corerepo.Stat{},
|
|
Encoders: cmds.EncoderMap{
|
|
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, stat *corerepo.Stat) error {
|
|
wtr := tabwriter.NewWriter(w, 0, 0, 1, ' ', 0)
|
|
defer wtr.Flush()
|
|
|
|
human, _ := req.Options[repoHumanOptionName].(bool)
|
|
sizeOnly, _ := req.Options[repoSizeOnlyOptionName].(bool)
|
|
|
|
printSize := func(name string, size uint64) {
|
|
sizeStr := fmt.Sprintf("%d", size)
|
|
if human {
|
|
sizeStr = humanize.Bytes(size)
|
|
}
|
|
|
|
fmt.Fprintf(wtr, "%s:\t%s\n", name, sizeStr)
|
|
}
|
|
|
|
if !sizeOnly {
|
|
fmt.Fprintf(wtr, "NumObjects:\t%d\n", stat.NumObjects)
|
|
}
|
|
|
|
printSize("RepoSize", stat.RepoSize)
|
|
printSize("StorageMax", stat.StorageMax)
|
|
|
|
if !sizeOnly {
|
|
fmt.Fprintf(wtr, "RepoPath:\t%s\n", stat.RepoPath)
|
|
fmt.Fprintf(wtr, "Version:\t%s\n", stat.Version)
|
|
}
|
|
|
|
return nil
|
|
}),
|
|
},
|
|
}
|
|
|
|
type VerifyProgress struct {
|
|
Msg string
|
|
Progress int
|
|
}
|
|
|
|
func verifyWorkerRun(ctx context.Context, wg *sync.WaitGroup, keys <-chan cid.Cid, results chan<- string, bs bstore.Blockstore) {
|
|
defer wg.Done()
|
|
|
|
for k := range keys {
|
|
_, err := bs.Get(ctx, k)
|
|
if err != nil {
|
|
select {
|
|
case results <- fmt.Sprintf("block %s was corrupt (%s)", k, err):
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
select {
|
|
case results <- "":
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func verifyResultChan(ctx context.Context, keys <-chan cid.Cid, bs bstore.Blockstore) <-chan string {
|
|
results := make(chan string)
|
|
|
|
go func() {
|
|
defer close(results)
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < runtime.NumCPU()*2; i++ {
|
|
wg.Add(1)
|
|
go verifyWorkerRun(ctx, &wg, keys, results, bs)
|
|
}
|
|
|
|
wg.Wait()
|
|
}()
|
|
|
|
return results
|
|
}
|
|
|
|
var repoVerifyCmd = &cmds.Command{
|
|
Helptext: cmds.HelpText{
|
|
Tagline: "Verify all blocks in repo are not corrupted.",
|
|
},
|
|
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
|
nd, err := cmdenv.GetNode(env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bs := &bstore.ValidatingBlockstore{Blockstore: bstore.NewBlockstore(nd.Repo.Datastore())}
|
|
|
|
keys, err := bs.AllKeysChan(req.Context)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
|
|
results := verifyResultChan(req.Context, keys, bs)
|
|
|
|
var fails int
|
|
var i int
|
|
for msg := range results {
|
|
if msg != "" {
|
|
if err := res.Emit(&VerifyProgress{Msg: msg}); err != nil {
|
|
return err
|
|
}
|
|
fails++
|
|
}
|
|
i++
|
|
if err := res.Emit(&VerifyProgress{Progress: i}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := req.Context.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if fails != 0 {
|
|
return errors.New("verify complete, some blocks were corrupt")
|
|
}
|
|
|
|
return res.Emit(&VerifyProgress{Msg: "verify complete, all blocks validated."})
|
|
},
|
|
Type: &VerifyProgress{},
|
|
Encoders: cmds.EncoderMap{
|
|
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, obj *VerifyProgress) error {
|
|
if strings.Contains(obj.Msg, "was corrupt") {
|
|
fmt.Fprintln(os.Stdout, obj.Msg)
|
|
return nil
|
|
}
|
|
|
|
if obj.Msg != "" {
|
|
if len(obj.Msg) < 20 {
|
|
obj.Msg += " "
|
|
}
|
|
fmt.Fprintln(w, obj.Msg)
|
|
return nil
|
|
}
|
|
|
|
fmt.Fprintf(w, "%d blocks processed.\r", obj.Progress)
|
|
return nil
|
|
}),
|
|
},
|
|
}
|
|
|
|
var repoVersionCmd = &cmds.Command{
|
|
Helptext: cmds.HelpText{
|
|
Tagline: "Show the repo version.",
|
|
ShortDescription: `
|
|
'ipfs repo version' returns the current repo version.
|
|
`,
|
|
},
|
|
|
|
Options: []cmds.Option{
|
|
cmds.BoolOption(repoQuietOptionName, "q", "Write minimal output."),
|
|
},
|
|
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
|
return cmds.EmitOnce(res, &RepoVersion{
|
|
Version: fmt.Sprint(fsrepo.RepoVersion),
|
|
})
|
|
},
|
|
Type: RepoVersion{},
|
|
Encoders: cmds.EncoderMap{
|
|
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *RepoVersion) error {
|
|
quiet, _ := req.Options[repoQuietOptionName].(bool)
|
|
|
|
if quiet {
|
|
fmt.Fprintf(w, "fs-repo@%s\n", out.Version)
|
|
} else {
|
|
fmt.Fprintf(w, "ipfs repo version fs-repo@%s\n", out.Version)
|
|
}
|
|
return nil
|
|
}),
|
|
},
|
|
}
|
|
|
|
var repoMigrateCmd = &cmds.Command{
|
|
Helptext: cmds.HelpText{
|
|
Tagline: "Apply repository migrations to a specific version.",
|
|
ShortDescription: `
|
|
'ipfs repo migrate' applies repository migrations to bring the repository
|
|
to a specific version. By default, migrates to the latest version supported
|
|
by this IPFS binary.
|
|
|
|
Examples:
|
|
ipfs repo migrate # Migrate to latest version
|
|
ipfs repo migrate --to=17 # Migrate to version 17
|
|
ipfs repo migrate --to=16 --allow-downgrade # Downgrade to version 16
|
|
|
|
WARNING: Downgrading a repository may cause data loss and requires using
|
|
an older IPFS binary that supports the target version. After downgrading,
|
|
you must use an IPFS implementation compatible with that repository version.
|
|
|
|
Repository versions 16+ use embedded migrations for faster, more reliable
|
|
migration. Versions below 16 require external migration tools.
|
|
`,
|
|
},
|
|
Options: []cmds.Option{
|
|
cmds.IntOption(repoToVersionOptionName, "Target repository version").WithDefault(fsrepo.RepoVersion),
|
|
cmds.BoolOption(repoAllowDowngradeOptionName, "Allow downgrading to a lower repo version"),
|
|
},
|
|
NoRemote: true,
|
|
// SetDoesNotUseRepo(true) might seem counter-intuitive since migrations
|
|
// do access the repo, but it's correct - we need direct filesystem access
|
|
// without going through the daemon. Migrations handle their own locking.
|
|
Extra: CreateCmdExtras(SetDoesNotUseRepo(true)),
|
|
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
|
cctx := env.(*oldcmds.Context)
|
|
allowDowngrade, _ := req.Options[repoAllowDowngradeOptionName].(bool)
|
|
targetVersion, _ := req.Options[repoToVersionOptionName].(int)
|
|
|
|
// Get current repo version
|
|
currentVersion, err := migrations.RepoVersion(cctx.ConfigRoot)
|
|
if err != nil {
|
|
return fmt.Errorf("could not get current repo version: %w", err)
|
|
}
|
|
|
|
// Check if migration is needed
|
|
if currentVersion == targetVersion {
|
|
fmt.Printf("Repository is already at version %d.\n", targetVersion)
|
|
return nil
|
|
}
|
|
|
|
// Validate downgrade request
|
|
if targetVersion < currentVersion && !allowDowngrade {
|
|
return fmt.Errorf("downgrade from version %d to %d requires --allow-downgrade flag", currentVersion, targetVersion)
|
|
}
|
|
|
|
// Check if repo is locked by daemon before running migration
|
|
locked, err := fsrepo.LockedByOtherProcess(cctx.ConfigRoot)
|
|
if err != nil {
|
|
return fmt.Errorf("could not check repo lock: %w", err)
|
|
}
|
|
if locked {
|
|
return fmt.Errorf("cannot run migration while daemon is running (repo.lock exists)")
|
|
}
|
|
|
|
fmt.Printf("Migrating repository from version %d to %d...\n", currentVersion, targetVersion)
|
|
|
|
// Use hybrid migration strategy that intelligently combines external and embedded migrations
|
|
err = migrations.RunHybridMigrations(cctx.Context(), targetVersion, cctx.ConfigRoot, allowDowngrade)
|
|
if err != nil {
|
|
fmt.Println("Repository migration failed:")
|
|
fmt.Printf(" %s\n", err)
|
|
fmt.Println("If you think this is a bug, please file an issue and include this whole log output.")
|
|
fmt.Println(" https://github.com/ipfs/kubo")
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("Repository successfully migrated to version %d.\n", targetVersion)
|
|
if targetVersion < fsrepo.RepoVersion {
|
|
fmt.Println("WARNING: After downgrading, you must use an IPFS binary compatible with this repository version.")
|
|
}
|
|
return nil
|
|
},
|
|
}
|