diff --git a/cmd/ipfs/ipfs.go b/cmd/ipfs/ipfs.go index 9fc7b1e238d..0e94627f961 100644 --- a/cmd/ipfs/ipfs.go +++ b/cmd/ipfs/ipfs.go @@ -84,13 +84,14 @@ func (d *cmdDetails) usesRepo() bool { return !d.doesNotUseRepo } // properties so that other code can make decisions about whether to invoke a // command or return an error to the user. var cmdDetailsMap = map[string]cmdDetails{ - "init": {doesNotUseConfigAsInput: true, cannotRunOnDaemon: true, doesNotUseRepo: true}, - "daemon": {doesNotUseConfigAsInput: true, cannotRunOnDaemon: true}, - "commands": {doesNotUseRepo: true}, - "version": {doesNotUseConfigAsInput: true, doesNotUseRepo: true}, // must be permitted to run before init - "log": {cannotRunOnClient: true}, - "diag/cmds": {cannotRunOnClient: true}, - "repo/fsck": {cannotRunOnDaemon: true}, - "config/edit": {cannotRunOnDaemon: true, doesNotUseRepo: true}, - "cid": {doesNotUseRepo: true}, + "init": {doesNotUseConfigAsInput: true, cannotRunOnDaemon: true, doesNotUseRepo: true}, + "daemon": {doesNotUseConfigAsInput: true, cannotRunOnDaemon: true}, + "commands": {doesNotUseRepo: true}, + "version": {doesNotUseConfigAsInput: true, doesNotUseRepo: true}, // must be permitted to run before init + "log": {cannotRunOnClient: true}, + "diag/cmds": {cannotRunOnClient: true}, + "repo/fsck": {cannotRunOnDaemon: true}, + "repo/rm-files-root": {cannotRunOnDaemon: true}, + "config/edit": {cannotRunOnDaemon: true, doesNotUseRepo: true}, + "cid": {doesNotUseRepo: true}, } diff --git a/core/commands/commands_test.go b/core/commands/commands_test.go index a34de3dfc4f..b15dc7677ef 100644 --- a/core/commands/commands_test.go +++ b/core/commands/commands_test.go @@ -188,6 +188,7 @@ func TestCommands(t *testing.T) { "/repo/stat", "/repo/verify", "/repo/version", + "/repo/rm-files-root", "/resolve", "/shutdown", "/stats", diff --git a/core/commands/repo.go b/core/commands/repo.go index 5f12ec516c6..da8f8a7124e 100644 --- a/core/commands/repo.go +++ b/core/commands/repo.go @@ -12,6 +12,7 @@ import ( "sync" "text/tabwriter" + core "github.com/ipfs/go-ipfs/core" cmdenv "github.com/ipfs/go-ipfs/core/commands/cmdenv" corerepo "github.com/ipfs/go-ipfs/core/corerepo" fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo" @@ -19,6 +20,8 @@ import ( cmds "gx/ipfs/QmQkW9fnCsg9SLHdViiAh6qfBppodsPZVpU92dZLqYtEfs/go-ipfs-cmds" cid "gx/ipfs/QmTbxNB1NwDesLmKTscr4udL2tVP7MaxvXnD1D9yX7g3PN/go-cid" config "gx/ipfs/QmUAuYuiafnJRZxDDX7MuruMNsicYNuyub5vUeAcupUBNs/go-ipfs-config" + ds "gx/ipfs/QmUadX5EcvrBmxAV9sE7wUWtWSqxns5K84qKJBixmcT1w9/go-datastore" + b58 "gx/ipfs/QmWFAMPqsEyUX7gDUsRVmMWz59FxSpJ1b2v6bJ1yYzo7jY/go-base58-fast/base58" bstore "gx/ipfs/QmXjKkjMDTtXAiLBwstVexofB8LeruZmE2eBd85GwGFFLA/go-ipfs-blockstore" cmdkit "gx/ipfs/Qmde5VP1qUkyQXKCfmEUA7bP64V2HAptbJ7phuPp7jXWwg/go-ipfs-cmdkit" ) @@ -36,11 +39,12 @@ var RepoCmd = &cmds.Command{ }, Subcommands: map[string]*cmds.Command{ - "stat": repoStatCmd, - "gc": repoGcCmd, - "fsck": repoFsckCmd, - "version": repoVersionCmd, - "verify": repoVerifyCmd, + "stat": repoStatCmd, + "gc": repoGcCmd, + "fsck": repoFsckCmd, + "version": repoVersionCmd, + "verify": repoVerifyCmd, + "rm-files-root": repoRmFilesRootCmd, }, } @@ -262,6 +266,89 @@ daemons are running. }, } +var repoRmFilesRootCmd = &cmds.Command{ + Helptext: cmdkit.HelpText{ + Tagline: "Unlink the root used by the `ipfs files` comands.", + ShortDescription: ` +'ipfs repo rm-files-root' will unlink the root used by the files API ('ipfs +files' commands) without trying to read the root itself. The root and +its children will be removed the next time the garbage collector runs, +unless pinned. + +This command is designed to recover form the situation when the root +becomes unavailable and recovering it (such as recreating it, or +fetching it from the network) is not possible. This command should +only be used as a last resort as using this command could lead to data +loss if there are unpinned nodes connected to the root. + +This command can only run when the ipfs daemon is not running. +`, + }, + Options: []cmdkit.Option{ + cmdkit.BoolOption("confirm", "Really perform operation."), + cmdkit.BoolOption("remove-existing-root", "Remove even if it root exists locally."), + }, + Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { + confirm, _ := req.Options["confirm"].(bool) + removeExistingRoot, _ := req.Options["remove-existing-root"].(bool) + + if !confirm { + return fmt.Errorf("this is a potentially dangerous operation please pass --confirm to proceed") + } + + configRoot, err := cmdenv.GetConfigRoot(env) + if err != nil { + return err + } + + // Can't use a full node as that interferes with the removal + // of the files root, so open the repo directly + repo, err := fsrepo.Open(configRoot) + if err != nil { + return err + } + defer repo.Close() + bs := bstore.NewBlockstore(repo.Datastore()) + + // Get the old root and display it to the user so that they can + // can do something to prevent from being garbage collected, + // such as pin it + val, err := repo.Datastore().Get(core.FilesRootKey) + if err == ds.ErrNotFound || val == nil { + return cmds.EmitOnce(res, &MessageOutput{"`ipfs files` root not found.\n"}) + } + + var cidStr string + var have bool + c, err := cid.Cast(val) + if err == nil { + cidStr = c.String() + have, _ = bs.Has(c) + } else { + cidStr = b58.Encode(val) + } + + if have && !removeExistingRoot { + return fmt.Errorf("`ipfs files` root %s exists locally. Are you sure you want to unlink this? Pass --remove-existing-root to continue", cidStr) + } + + err = repo.Datastore().Delete(core.FilesRootKey) + if err != nil { + return fmt.Errorf("unable to remove `ipfs files` root: %s. Root hash was %s", err.Error(), cidStr) + } + return cmds.EmitOnce(res, &MessageOutput{ + fmt.Sprintf("Unlinked files API root. Root hash was %s.\n", cidStr), + }) + }, + Type: MessageOutput{}, + Encoders: cmds.EncoderMap{ + cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, msg *MessageOutput) error { + _, err := fmt.Fprintf(w, "%s", msg.Message) + return err + }), + }, +} + type VerifyProgress struct { Msg string Progress int diff --git a/core/core.go b/core/core.go index c5fbfe4c1c2..a045ecbbed3 100644 --- a/core/core.go +++ b/core/core.go @@ -80,6 +80,9 @@ import ( const IpnsValidatorTag = "ipns" +// FilesRootKey is the datastore key for the files root. +var FilesRootKey ds.Key = ds.NewKey("/local/filesroot") + const kReprovideFrequency = time.Hour * 12 const discoveryConnTimeout = time.Second * 30 const DefaultIpnsCacheSize = 128 @@ -856,13 +859,12 @@ func (n *IpfsNode) loadBootstrapPeers() ([]pstore.PeerInfo, error) { } func (n *IpfsNode) loadFilesRoot() error { - dsk := ds.NewKey("/local/filesroot") pf := func(ctx context.Context, c cid.Cid) error { - return n.Repo.Datastore().Put(dsk, c.Bytes()) + return n.Repo.Datastore().Put(FilesRootKey, c.Bytes()) } var nd *merkledag.ProtoNode - val, err := n.Repo.Datastore().Get(dsk) + val, err := n.Repo.Datastore().Get(FilesRootKey) switch { case err == ds.ErrNotFound || val == nil: diff --git a/test/sharness/t0089-repo-rm-files-root.sh b/test/sharness/t0089-repo-rm-files-root.sh new file mode 100755 index 00000000000..ba7b8f19116 --- /dev/null +++ b/test/sharness/t0089-repo-rm-files-root.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2016 Jeromy Johnson +# MIT Licensed; see the LICENSE file in this repository. +# + +test_description="Test ipfs repo fsck" + +. lib/test-lib.sh + +test_init_ipfs + +ROOT_HASH=QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn + +test_expect_success "ipfs repo rm-files-root fails without --confirm" ' + test_must_fail ipfs repo rm-files-root 2> err && + cat err && + fgrep -q "please pass --confirm to proceed" err +' + +test_expect_success "ipfs repo rm-files-root fails to remove existing root without --remove-existing-root" ' + test_must_fail ipfs repo rm-files-root --confirm 2> err && + cat err && + fgrep -q "Are you sure you want to unlink this?" err +' + +test_expect_success "ipfs repo rm-files-root" ' + ipfs repo rm-files-root --confirm --remove-existing-root | tee rm-files-root.actual && + echo "Unlinked files API root. Root hash was $ROOT_HASH." > rm-files-root.expected && + test_cmp rm-files-root.expected rm-files-root.actual +' + +test_expect_success "files api root really removed" ' + ipfs repo rm-files-root --confirm | tee rm-files-root-post.actual && + echo "Files API root not found." > rm-files-root-post.expected && + test_cmp rm-files-root-post.expected rm-files-root-post.actual +' + +test_launch_ipfs_daemon + +test_expect_success "ipfs repo rm-files-root does not run on daemon" ' + test_must_fail ipfs repo rm-files-root --confirm 2> err && + cat err && + fgrep -q "ipfs daemon is running" err +' + +test_kill_ipfs_daemon + +test_done