mirror of
https://github.com/ipfs/kubo.git
synced 2025-06-29 09:34:03 +08:00
Merge pull request #332 from jbenet/cmd-ref-part2-rebase
Commands Refactor Part 2 - rebased
This commit is contained in:
8
Makefile
8
Makefile
@ -10,10 +10,14 @@ godep:
|
||||
vendor: godep
|
||||
godep save -r ./...
|
||||
|
||||
|
||||
install:
|
||||
# TODO revert to `install` once new command refactoring is complete
|
||||
install_1:
|
||||
cd cmd/ipfs && go install
|
||||
|
||||
# TODO remove once new command refactoring is complete
|
||||
install_2:
|
||||
cd cmd/ipfs2 && go install
|
||||
|
||||
test: test_go test_sharness
|
||||
|
||||
test_go:
|
||||
|
3
cmd/ipfs2/.gitignore
vendored
Normal file
3
cmd/ipfs2/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
./ipfs
|
||||
./ipfs.exe
|
||||
./ipfs2
|
7
cmd/ipfs2/Makefile
Normal file
7
cmd/ipfs2/Makefile
Normal file
@ -0,0 +1,7 @@
|
||||
all: install
|
||||
|
||||
build:
|
||||
go build
|
||||
|
||||
install: build
|
||||
go install
|
38
cmd/ipfs2/README.md
Normal file
38
cmd/ipfs2/README.md
Normal file
@ -0,0 +1,38 @@
|
||||
# go-ipfs/cmd/ipfs
|
||||
|
||||
This is the ipfs commandline tool. For now, it's the main entry point to using IPFS. Use it.
|
||||
|
||||
```
|
||||
> go build
|
||||
> go install
|
||||
> ipfs
|
||||
ipfs - global versioned p2p merkledag file system
|
||||
|
||||
Basic commands:
|
||||
|
||||
init Initialize ipfs local configurationx
|
||||
add <path> Add an object to ipfs
|
||||
cat <ref> Show ipfs object data
|
||||
ls <ref> List links from an object
|
||||
|
||||
Tool commands:
|
||||
|
||||
config Manage configuration
|
||||
update Download and apply go-ipfs updates
|
||||
version Show ipfs version information
|
||||
commands List all available commands
|
||||
|
||||
Advanced Commands:
|
||||
|
||||
mount Mount an ipfs read-only mountpoint
|
||||
serve Serve an interface to ipfs
|
||||
diag Print diagnostics
|
||||
|
||||
Plumbing commands:
|
||||
|
||||
block Interact with raw blocks in the datastore
|
||||
object Interact with raw dag nodes
|
||||
|
||||
|
||||
Use "ipfs help <command>" for more information about a command.
|
||||
```
|
80
cmd/ipfs2/daemon.go
Normal file
80
cmd/ipfs2/daemon.go
Normal file
@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
ma "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr"
|
||||
manet "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr/net"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
cmdsHttp "github.com/jbenet/go-ipfs/commands/http"
|
||||
core "github.com/jbenet/go-ipfs/core"
|
||||
commands "github.com/jbenet/go-ipfs/core/commands2"
|
||||
daemon "github.com/jbenet/go-ipfs/daemon2"
|
||||
)
|
||||
|
||||
var daemonCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Run a network-connected IPFS node",
|
||||
ShortDescription: `
|
||||
'ipfs daemon' runs a persistent IPFS daemon that can serve commands
|
||||
over the network. Most applications that use IPFS will do so by
|
||||
communicating with a daemon over the HTTP API. While the daemon is
|
||||
running, calls to 'ipfs' commands will be sent over the network to
|
||||
the daemon.
|
||||
`,
|
||||
},
|
||||
|
||||
Options: []cmds.Option{},
|
||||
Subcommands: map[string]*cmds.Command{},
|
||||
Run: daemonFunc,
|
||||
}
|
||||
|
||||
func daemonFunc(req cmds.Request) (interface{}, error) {
|
||||
lock, err := daemon.Lock(req.Context().ConfigRoot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't obtain lock. Is another daemon already running?")
|
||||
}
|
||||
defer lock.Close()
|
||||
|
||||
cfg, err := req.Context().GetConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// setup function that constructs the context. we have to do it this way
|
||||
// to play along with how the Context works and thus not expose its internals
|
||||
req.Context().ConstructNode = func() (*core.IpfsNode, error) {
|
||||
return core.NewIpfsNode(cfg, true)
|
||||
}
|
||||
node, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr, err := ma.NewMultiaddr(cfg.Addresses.API)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, host, err := manet.DialArgs(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmdHandler := cmdsHttp.NewHandler(*req.Context(), commands.Root)
|
||||
http.Handle(cmdsHttp.ApiPath+"/", cmdHandler)
|
||||
|
||||
ifpsHandler := &ipfsHandler{node}
|
||||
http.Handle("/ipfs/", ifpsHandler)
|
||||
|
||||
fmt.Printf("API server listening on '%s'\n", host)
|
||||
|
||||
err = http.ListenAndServe(host, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
6
cmd/ipfs2/equinox.yaml
Normal file
6
cmd/ipfs2/equinox.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
equinox-account: CHANGEME
|
||||
equinox-secret: CHANGEME
|
||||
equinox-app: CHANGEME
|
||||
channel: stable
|
||||
private-key: equinox-priv
|
175
cmd/ipfs2/init.go
Normal file
175
cmd/ipfs2/init.go
Normal file
@ -0,0 +1,175 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
config "github.com/jbenet/go-ipfs/config"
|
||||
ci "github.com/jbenet/go-ipfs/crypto"
|
||||
peer "github.com/jbenet/go-ipfs/peer"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
var initCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Initializes IPFS config file",
|
||||
ShortDescription: "Initializes IPFS configuration files and generates a new keypair.",
|
||||
},
|
||||
|
||||
Options: []cmds.Option{
|
||||
cmds.IntOption("bits", "b", "Number of bits to use in the generated RSA private key (defaults to 4096)"),
|
||||
cmds.StringOption("passphrase", "p", "Passphrase for encrypting the private key"),
|
||||
cmds.BoolOption("force", "f", "Overwrite existing config (if it exists)"),
|
||||
cmds.StringOption("datastore", "d", "Location for the IPFS data store"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
|
||||
dspathOverride, _, err := req.Option("d").String() // if !found it's okay. Let == ""
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
force, _, err := req.Option("f").Bool() // if !found, it's okay force == false
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nBitsForKeypair, bitsOptFound, err := req.Option("b").Int()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !bitsOptFound {
|
||||
nBitsForKeypair = 4096
|
||||
}
|
||||
|
||||
return nil, doInit(req.Context().ConfigRoot, dspathOverride, force, nBitsForKeypair)
|
||||
},
|
||||
}
|
||||
|
||||
// TODO add default welcome hash: eaa68bedae247ed1e5bd0eb4385a3c0959b976e4
|
||||
// NB: if dspath is not provided, it will be retrieved from the config
|
||||
func doInit(configRoot string, dspathOverride string, force bool, nBitsForKeypair int) error {
|
||||
|
||||
u.POut("initializing ipfs node at %s\n", configRoot)
|
||||
|
||||
configFilename, err := config.Filename(configRoot)
|
||||
if err != nil {
|
||||
return errors.New("Couldn't get home directory path")
|
||||
}
|
||||
|
||||
fi, err := os.Lstat(configFilename)
|
||||
if fi != nil || (err != nil && !os.IsNotExist(err)) {
|
||||
if !force {
|
||||
// TODO multi-line string
|
||||
return errors.New("ipfs configuration file already exists!\nReinitializing would overwrite your keys.\n(use -f to force overwrite)")
|
||||
}
|
||||
}
|
||||
|
||||
ds, err := datastoreConfig(dspathOverride)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
identity, err := identityConfig(nBitsForKeypair)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conf := config.Config{
|
||||
|
||||
// setup the node addresses.
|
||||
Addresses: config.Addresses{
|
||||
Swarm: "/ip4/0.0.0.0/tcp/4001",
|
||||
API: "/ip4/127.0.0.1/tcp/5001",
|
||||
},
|
||||
|
||||
Bootstrap: []*config.BootstrapPeer{
|
||||
&config.BootstrapPeer{ // Use these hardcoded bootstrap peers for now.
|
||||
// mars.i.ipfs.io
|
||||
PeerID: "QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
|
||||
Address: "/ip4/104.131.131.82/tcp/4001",
|
||||
},
|
||||
},
|
||||
|
||||
Datastore: ds,
|
||||
|
||||
Identity: identity,
|
||||
|
||||
// setup the node mount points.
|
||||
Mounts: config.Mounts{
|
||||
IPFS: "/ipfs",
|
||||
IPNS: "/ipns",
|
||||
},
|
||||
|
||||
// tracking ipfs version used to generate the init folder and adding
|
||||
// update checker default setting.
|
||||
Version: config.VersionDefaultValue(),
|
||||
}
|
||||
|
||||
err = config.WriteConfigFile(configFilename, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func datastoreConfig(dspath string) (config.Datastore, error) {
|
||||
ds := config.Datastore{}
|
||||
if len(dspath) == 0 {
|
||||
var err error
|
||||
dspath, err = config.DataStorePath("")
|
||||
if err != nil {
|
||||
return ds, err
|
||||
}
|
||||
}
|
||||
ds.Path = dspath
|
||||
ds.Type = "leveldb"
|
||||
|
||||
// Construct the data store if missing
|
||||
if err := os.MkdirAll(dspath, os.ModePerm); err != nil {
|
||||
return ds, err
|
||||
}
|
||||
|
||||
// Check the directory is writeable
|
||||
if f, err := os.Create(filepath.Join(dspath, "._check_writeable")); err == nil {
|
||||
os.Remove(f.Name())
|
||||
} else {
|
||||
return ds, errors.New("Datastore '" + dspath + "' is not writeable")
|
||||
}
|
||||
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
func identityConfig(nbits int) (config.Identity, error) {
|
||||
// TODO guard higher up
|
||||
ident := config.Identity{}
|
||||
if nbits < 1024 {
|
||||
return ident, errors.New("Bitsize less than 1024 is considered unsafe.")
|
||||
}
|
||||
|
||||
fmt.Println("generating key pair...")
|
||||
sk, pk, err := ci.GenerateKeyPair(ci.RSA, nbits)
|
||||
if err != nil {
|
||||
return ident, err
|
||||
}
|
||||
|
||||
// currently storing key unencrypted. in the future we need to encrypt it.
|
||||
// TODO(security)
|
||||
skbytes, err := sk.Bytes()
|
||||
if err != nil {
|
||||
return ident, err
|
||||
}
|
||||
ident.PrivKey = base64.StdEncoding.EncodeToString(skbytes)
|
||||
|
||||
id, err := peer.IDFromPubKey(pk)
|
||||
if err != nil {
|
||||
return ident, err
|
||||
}
|
||||
ident.PeerID = id.Pretty()
|
||||
|
||||
return ident, nil
|
||||
}
|
84
cmd/ipfs2/ipfs.go
Normal file
84
cmd/ipfs2/ipfs.go
Normal file
@ -0,0 +1,84 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
commands "github.com/jbenet/go-ipfs/core/commands2"
|
||||
)
|
||||
|
||||
// This is the CLI root, used for executing commands accessible to CLI clients.
|
||||
// Some subcommands (like 'ipfs daemon' or 'ipfs init') are only accessible here,
|
||||
// and can't be called through the HTTP API.
|
||||
var Root = &cmds.Command{
|
||||
Options: commands.Root.Options,
|
||||
Helptext: commands.Root.Helptext,
|
||||
}
|
||||
|
||||
// commandsClientCmd is the "ipfs commands" command for local cli
|
||||
var commandsClientCmd = commands.CommandsCmd(Root)
|
||||
|
||||
// Commands in localCommands should always be run locally (even if daemon is running).
|
||||
// They can override subcommands in commands.Root by defining a subcommand with the same name.
|
||||
var localCommands = map[string]*cmds.Command{
|
||||
"daemon": daemonCmd,
|
||||
"init": initCmd,
|
||||
"tour": tourCmd,
|
||||
"commands": commandsClientCmd,
|
||||
}
|
||||
var localMap = make(map[*cmds.Command]bool)
|
||||
|
||||
func init() {
|
||||
// setting here instead of in literal to prevent initialization loop
|
||||
// (some commands make references to Root)
|
||||
Root.Subcommands = localCommands
|
||||
|
||||
// copy all subcommands from commands.Root into this root (if they aren't already present)
|
||||
for k, v := range commands.Root.Subcommands {
|
||||
if _, found := Root.Subcommands[k]; !found {
|
||||
Root.Subcommands[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range localCommands {
|
||||
localMap[v] = true
|
||||
}
|
||||
}
|
||||
|
||||
// isLocal returns true if the command should only be run locally (not sent to daemon), otherwise false
|
||||
func isLocal(cmd *cmds.Command) bool {
|
||||
_, found := localMap[cmd]
|
||||
return found
|
||||
}
|
||||
|
||||
type cmdDetails struct {
|
||||
cannotRunOnClient bool
|
||||
cannotRunOnDaemon bool
|
||||
doesNotUseRepo bool
|
||||
}
|
||||
|
||||
func (d *cmdDetails) String() string {
|
||||
return fmt.Sprintf("on client? %t, on daemon? %t, uses repo? %t",
|
||||
d.canRunOnClient(), d.canRunOnDaemon(), d.usesRepo())
|
||||
}
|
||||
|
||||
func (d *cmdDetails) canRunOnClient() bool { return !d.cannotRunOnClient }
|
||||
func (d *cmdDetails) canRunOnDaemon() bool { return !d.cannotRunOnDaemon }
|
||||
func (d *cmdDetails) usesRepo() bool { return !d.doesNotUseRepo }
|
||||
|
||||
// "What is this madness!?" you ask. Our commands have the unfortunate problem of
|
||||
// not being able to run on all the same contexts. This map describes these
|
||||
// properties so that other code can make decisions about whether to invoke a
|
||||
// command or return an error to the user.
|
||||
var cmdDetailsMap = map[*cmds.Command]cmdDetails{
|
||||
initCmd: cmdDetails{cannotRunOnDaemon: true, doesNotUseRepo: true},
|
||||
daemonCmd: cmdDetails{cannotRunOnDaemon: true},
|
||||
commandsClientCmd: cmdDetails{doesNotUseRepo: true},
|
||||
commands.CommandsDaemonCmd: cmdDetails{doesNotUseRepo: true},
|
||||
commands.DiagCmd: cmdDetails{cannotRunOnClient: true},
|
||||
commands.VersionCmd: cmdDetails{doesNotUseRepo: true},
|
||||
commands.UpdateCmd: cmdDetails{cannotRunOnDaemon: true},
|
||||
commands.UpdateCheckCmd: cmdDetails{},
|
||||
commands.UpdateLogCmd: cmdDetails{},
|
||||
commands.LogCmd: cmdDetails{cannotRunOnClient: true},
|
||||
}
|
97
cmd/ipfs2/ipfsHandler.go
Normal file
97
cmd/ipfs2/ipfsHandler.go
Normal file
@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context"
|
||||
mh "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multihash"
|
||||
|
||||
core "github.com/jbenet/go-ipfs/core"
|
||||
"github.com/jbenet/go-ipfs/importer"
|
||||
dag "github.com/jbenet/go-ipfs/merkledag"
|
||||
"github.com/jbenet/go-ipfs/routing"
|
||||
uio "github.com/jbenet/go-ipfs/unixfs/io"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
type ipfs interface {
|
||||
ResolvePath(string) (*dag.Node, error)
|
||||
NewDagFromReader(io.Reader) (*dag.Node, error)
|
||||
AddNodeToDAG(nd *dag.Node) (u.Key, error)
|
||||
NewDagReader(nd *dag.Node) (io.Reader, error)
|
||||
}
|
||||
|
||||
// ipfsHandler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/<path>)
|
||||
// (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link)
|
||||
type ipfsHandler struct {
|
||||
node *core.IpfsNode
|
||||
}
|
||||
|
||||
func (i *ipfsHandler) ResolvePath(path string) (*dag.Node, error) {
|
||||
return i.node.Resolver.ResolvePath(path)
|
||||
}
|
||||
|
||||
func (i *ipfsHandler) NewDagFromReader(r io.Reader) (*dag.Node, error) {
|
||||
return importer.NewDagFromReader(r)
|
||||
}
|
||||
|
||||
func (i *ipfsHandler) AddNodeToDAG(nd *dag.Node) (u.Key, error) {
|
||||
return i.node.DAG.Add(nd)
|
||||
}
|
||||
|
||||
func (i *ipfsHandler) NewDagReader(nd *dag.Node) (io.Reader, error) {
|
||||
return uio.NewDagReader(nd, i.node.DAG)
|
||||
}
|
||||
|
||||
func (i *ipfsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path[5:]
|
||||
|
||||
nd, err := i.ResolvePath(path)
|
||||
if err != nil {
|
||||
if err == routing.ErrNotFound {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
} else if err == context.DeadlineExceeded {
|
||||
w.WriteHeader(http.StatusRequestTimeout)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
log.Error(err)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
dr, err := i.NewDagReader(nd)
|
||||
if err != nil {
|
||||
// TODO: return json object containing the tree data if it's a directory (err == ErrIsDir)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Error(err)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
io.Copy(w, dr)
|
||||
}
|
||||
|
||||
func (i *ipfsHandler) postHandler(w http.ResponseWriter, r *http.Request) {
|
||||
nd, err := i.NewDagFromReader(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Error(err)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
k, err := i.AddNodeToDAG(nd)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Error(err)
|
||||
w.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
//TODO: return json representation of list instead
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte(mh.Multihash(k).B58String()))
|
||||
}
|
404
cmd/ipfs2/main.go
Normal file
404
cmd/ipfs2/main.go
Normal file
@ -0,0 +1,404 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime/pprof"
|
||||
|
||||
logging "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-logging"
|
||||
ma "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr"
|
||||
manet "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr/net"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
cmdsCli "github.com/jbenet/go-ipfs/commands/cli"
|
||||
cmdsHttp "github.com/jbenet/go-ipfs/commands/http"
|
||||
config "github.com/jbenet/go-ipfs/config"
|
||||
core "github.com/jbenet/go-ipfs/core"
|
||||
daemon "github.com/jbenet/go-ipfs/daemon2"
|
||||
updates "github.com/jbenet/go-ipfs/updates"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
// log is the command logger
|
||||
var log = u.Logger("cmd/ipfs")
|
||||
|
||||
// signal to output help
|
||||
var errHelpRequested = errors.New("Help Requested")
|
||||
|
||||
const (
|
||||
cpuProfile = "ipfs.cpuprof"
|
||||
heapProfile = "ipfs.memprof"
|
||||
errorFormat = "ERROR: %v\n\n"
|
||||
)
|
||||
|
||||
type cmdInvocation struct {
|
||||
path []string
|
||||
cmd *cmds.Command
|
||||
req cmds.Request
|
||||
}
|
||||
|
||||
// main roadmap:
|
||||
// - parse the commandline to get a cmdInvocation
|
||||
// - if user requests, help, print it and exit.
|
||||
// - run the command invocation
|
||||
// - output the response
|
||||
// - if anything fails, print error, maybe with help
|
||||
func main() {
|
||||
var invoc cmdInvocation
|
||||
var err error
|
||||
|
||||
// we'll call this local helper to output errors.
|
||||
// this is so we control how to print errors in one place.
|
||||
printErr := func(err error) {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
|
||||
}
|
||||
|
||||
// this is a local helper to print out help text.
|
||||
// there's some considerations that this makes easier.
|
||||
printHelp := func(long bool) {
|
||||
helpFunc := cmdsCli.ShortHelp
|
||||
if long {
|
||||
helpFunc = cmdsCli.LongHelp
|
||||
}
|
||||
|
||||
helpFunc("ipfs", Root, invoc.path, os.Stderr)
|
||||
}
|
||||
|
||||
// parse the commandline into a command invocation
|
||||
parseErr := invoc.Parse(os.Args[1:])
|
||||
|
||||
// BEFORE handling the parse error, if we have enough information
|
||||
// AND the user requested help, print it out and exit
|
||||
if invoc.req != nil {
|
||||
longH, shortH, err := invoc.requestedHelp()
|
||||
if err != nil {
|
||||
printErr(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if longH || shortH {
|
||||
printHelp(longH)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// here we handle the cases where
|
||||
// - commands with no Run func are invoked directly.
|
||||
// - the main command is invoked.
|
||||
if invoc.cmd == nil || invoc.cmd.Run == nil {
|
||||
printHelp(false)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// ok now handle parse error (which means cli input was wrong,
|
||||
// e.g. incorrect number of args, or nonexistent subcommand)
|
||||
if parseErr != nil {
|
||||
printErr(parseErr)
|
||||
|
||||
// this was a user error, print help.
|
||||
if invoc.cmd != nil {
|
||||
// we need a newline space.
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
printHelp(false)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// ok, finally, run the command invocation.
|
||||
output, err := invoc.Run()
|
||||
if err != nil {
|
||||
printErr(err)
|
||||
|
||||
// if this error was a client error, print short help too.
|
||||
if isClientError(err) {
|
||||
printHelp(false)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// everything went better than expected :)
|
||||
io.Copy(os.Stdout, output)
|
||||
}
|
||||
|
||||
func (i *cmdInvocation) Run() (output io.Reader, err error) {
|
||||
handleInterrupt()
|
||||
|
||||
// check if user wants to debug. option OR env var.
|
||||
debug, _, err := i.req.Option("debug").Bool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if debug || u.GetenvBool("DEBUG") {
|
||||
u.Debug = true
|
||||
u.SetAllLoggers(logging.DEBUG)
|
||||
}
|
||||
|
||||
// if debugging, let's profile.
|
||||
// TODO maybe change this to its own option... profiling makes it slower.
|
||||
if u.Debug {
|
||||
stopProfilingFunc, err := startProfiling()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stopProfilingFunc() // to be executed as late as possible
|
||||
}
|
||||
|
||||
res, err := callCommand(i.req, Root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := res.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.Reader()
|
||||
}
|
||||
|
||||
func (i *cmdInvocation) Parse(args []string) error {
|
||||
var err error
|
||||
|
||||
i.req, i.cmd, i.path, err = cmdsCli.Parse(args, os.Stdin, Root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath, err := getConfigRoot(i.req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// this sets up the function that will initialize the config lazily.
|
||||
ctx := i.req.Context()
|
||||
ctx.ConfigRoot = configPath
|
||||
ctx.LoadConfig = loadConfig
|
||||
|
||||
// if no encoding was specified by user, default to plaintext encoding
|
||||
// (if command doesn't support plaintext, use JSON instead)
|
||||
if !i.req.Option("encoding").Found() {
|
||||
if i.req.Command().Marshalers != nil && i.req.Command().Marshalers[cmds.Text] != nil {
|
||||
i.req.SetOption("encoding", cmds.Text)
|
||||
} else {
|
||||
i.req.SetOption("encoding", cmds.JSON)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *cmdInvocation) requestedHelp() (short bool, long bool, err error) {
|
||||
longHelp, _, err := i.req.Option("help").Bool()
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
shortHelp, _, err := i.req.Option("h").Bool()
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
return longHelp, shortHelp, nil
|
||||
}
|
||||
|
||||
func callCommand(req cmds.Request, root *cmds.Command) (cmds.Response, error) {
|
||||
var res cmds.Response
|
||||
|
||||
useDaemon, err := commandShouldRunOnDaemon(req, root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err := req.Context().GetConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if useDaemon {
|
||||
|
||||
addr, err := ma.NewMultiaddr(cfg.Addresses.API)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("Executing command on daemon running at %s", addr)
|
||||
_, host, err := manet.DialArgs(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := cmdsHttp.NewClient(host)
|
||||
|
||||
res, err = client.Send(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Info("Executing command locally")
|
||||
|
||||
// Check for updates and potentially install one.
|
||||
if err := updates.CliCheckForUpdates(cfg, req.Context().ConfigRoot); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// this sets up the function that will initialize the node
|
||||
// this is so that we can construct the node lazily.
|
||||
ctx := req.Context()
|
||||
ctx.ConstructNode = func() (*core.IpfsNode, error) {
|
||||
cfg, err := ctx.GetConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return core.NewIpfsNode(cfg, false)
|
||||
}
|
||||
|
||||
// Okay!!!!! NOW we can call the command.
|
||||
res = root.Call(req)
|
||||
|
||||
// let's not forget teardown. If a node was initialized, we must close it.
|
||||
// Note that this means the underlying req.Context().Node variable is exposed.
|
||||
// this is gross, and should be changed when we extract out the exec Context.
|
||||
node := req.Context().NodeWithoutConstructing()
|
||||
if node != nil {
|
||||
node.Close()
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func commandShouldRunOnDaemon(req cmds.Request, root *cmds.Command) (bool, error) {
|
||||
path := req.Path()
|
||||
// root command.
|
||||
if len(path) < 1 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var details cmdDetails
|
||||
// find the last command in path that has a cmdDetailsMap entry
|
||||
cmd := root
|
||||
for _, cmp := range path {
|
||||
var found bool
|
||||
cmd, found = cmd.Subcommands[cmp]
|
||||
if !found {
|
||||
return false, fmt.Errorf("subcommand %s should be in root", cmp)
|
||||
}
|
||||
|
||||
if cmdDetails, found := cmdDetailsMap[cmd]; found {
|
||||
details = cmdDetails
|
||||
}
|
||||
}
|
||||
log.Debugf("cmd perms for +%v: %s", path, details.String())
|
||||
|
||||
if details.cannotRunOnClient && details.cannotRunOnDaemon {
|
||||
return false, fmt.Errorf("command disabled: %s", path[0])
|
||||
}
|
||||
|
||||
if details.doesNotUseRepo && details.canRunOnClient() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// at this point need to know whether daemon is running. we defer
|
||||
// to this point so that some commands dont open files unnecessarily.
|
||||
daemonLocked := daemon.Locked(req.Context().ConfigRoot)
|
||||
log.Info("Daemon is running.")
|
||||
|
||||
if daemonLocked {
|
||||
|
||||
if details.cannotRunOnDaemon {
|
||||
e := "ipfs daemon is running. please stop it to run this command"
|
||||
return false, cmds.ClientError(e)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if details.cannotRunOnClient {
|
||||
return false, cmds.ClientError("must run on the ipfs daemon")
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func isClientError(err error) bool {
|
||||
|
||||
// Somewhat suprisingly, the pointer cast fails to recognize commands.Error
|
||||
// passed as values, so we check both.
|
||||
|
||||
// cast to cmds.Error
|
||||
switch e := err.(type) {
|
||||
case *cmds.Error:
|
||||
return e.Code == cmds.ErrClient
|
||||
case cmds.Error:
|
||||
return e.Code == cmds.ErrClient
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getConfigRoot(req cmds.Request) (string, error) {
|
||||
configOpt, found, err := req.Option("config").String()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if found && configOpt != "" {
|
||||
return configOpt, nil
|
||||
}
|
||||
|
||||
configPath, err := config.PathRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return configPath, nil
|
||||
}
|
||||
|
||||
func loadConfig(path string) (*config.Config, error) {
|
||||
configFile, err := config.Filename(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config.Load(configFile)
|
||||
}
|
||||
|
||||
// startProfiling begins CPU profiling and returns a `stop` function to be
|
||||
// executed as late as possible. The stop function captures the memprofile.
|
||||
func startProfiling() (func(), error) {
|
||||
|
||||
// start CPU profiling as early as possible
|
||||
ofi, err := os.Create(cpuProfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pprof.StartCPUProfile(ofi)
|
||||
|
||||
stopProfiling := func() {
|
||||
pprof.StopCPUProfile()
|
||||
defer ofi.Close() // captured by the closure
|
||||
err := writeHeapProfileToFile()
|
||||
if err != nil {
|
||||
log.Critical(err)
|
||||
}
|
||||
}
|
||||
return stopProfiling, nil
|
||||
}
|
||||
|
||||
func writeHeapProfileToFile() error {
|
||||
mprof, err := os.Create(heapProfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer mprof.Close() // _after_ writing the heap profile
|
||||
return pprof.WriteHeapProfile(mprof)
|
||||
}
|
||||
|
||||
// listen for and handle SIGTERM
|
||||
func handleInterrupt() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
for _ = range c {
|
||||
log.Info("Received interrupt signal, terminating...")
|
||||
os.Exit(0)
|
||||
}
|
||||
}()
|
||||
}
|
17
cmd/ipfs2/main_test.go
Normal file
17
cmd/ipfs2/main_test.go
Normal file
@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jbenet/go-ipfs/commands"
|
||||
)
|
||||
|
||||
func TestIsCientErr(t *testing.T) {
|
||||
t.Log("Catch both pointers and values")
|
||||
if !isClientError(commands.Error{Code: commands.ErrClient}) {
|
||||
t.Errorf("misidentified value")
|
||||
}
|
||||
if !isClientError(&commands.Error{Code: commands.ErrClient}) {
|
||||
t.Errorf("misidentified pointer")
|
||||
}
|
||||
}
|
201
cmd/ipfs2/tour.go
Normal file
201
cmd/ipfs2/tour.go
Normal file
@ -0,0 +1,201 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
config "github.com/jbenet/go-ipfs/config"
|
||||
internal "github.com/jbenet/go-ipfs/core/commands2/internal"
|
||||
tour "github.com/jbenet/go-ipfs/tour"
|
||||
)
|
||||
|
||||
var tourCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "An introduction to IPFS",
|
||||
ShortDescription: `
|
||||
This is a tour that takes you through various IPFS concepts,
|
||||
features, and tools to make sure you get up to speed with
|
||||
IPFS very quickly. To start, run:
|
||||
|
||||
ipfs tour
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("id", false, false, "The id of the topic you would like to tour"),
|
||||
},
|
||||
Subcommands: map[string]*cmds.Command{
|
||||
"list": cmdIpfsTourList,
|
||||
"next": cmdIpfsTourNext,
|
||||
"restart": cmdIpfsTourRestart,
|
||||
},
|
||||
Run: tourRunFunc,
|
||||
}
|
||||
|
||||
func tourRunFunc(req cmds.Request) (interface{}, error) {
|
||||
|
||||
cfg, err := req.Context().GetConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
strs, err := internal.CastToStrings(req.Arguments())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id := tour.TopicID(cfg.Tour.Last)
|
||||
if len(strs) > 0 {
|
||||
id = tour.TopicID(strs[0])
|
||||
}
|
||||
|
||||
var w bytes.Buffer
|
||||
defer w.WriteTo(os.Stdout)
|
||||
t, err := tourGet(id)
|
||||
if err != nil {
|
||||
|
||||
// If no topic exists for this id, we handle this error right here.
|
||||
// To help the user achieve the task, we construct a response
|
||||
// comprised of...
|
||||
// 1) a simple error message
|
||||
// 2) the full list of topics
|
||||
|
||||
fmt.Fprintln(&w, "ERROR")
|
||||
fmt.Fprintln(&w, err)
|
||||
fmt.Fprintln(&w, "")
|
||||
fprintTourList(&w, tour.TopicID(cfg.Tour.Last))
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fprintTourShow(&w, t)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var cmdIpfsTourNext = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Show the next IPFS Tour topic",
|
||||
},
|
||||
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
var w bytes.Buffer
|
||||
path := req.Context().ConfigRoot
|
||||
cfg, err := req.Context().GetConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id := tour.NextTopic(tour.TopicID(cfg.Tour.Last))
|
||||
topic, err := tourGet(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := fprintTourShow(&w, topic); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// topic changed, not last. write it out.
|
||||
if string(id) != cfg.Tour.Last {
|
||||
cfg.Tour.Last = string(id)
|
||||
err := writeConfig(path, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteTo(os.Stdout)
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
var cmdIpfsTourRestart = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Restart the IPFS Tour",
|
||||
},
|
||||
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
path := req.Context().ConfigRoot
|
||||
cfg, err := req.Context().GetConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg.Tour.Last = ""
|
||||
err = writeConfig(path, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
var cmdIpfsTourList = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Show a list of IPFS Tour topics",
|
||||
},
|
||||
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
cfg, err := req.Context().GetConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var w bytes.Buffer
|
||||
fprintTourList(&w, tour.TopicID(cfg.Tour.Last))
|
||||
w.WriteTo(os.Stdout)
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
func fprintTourList(w io.Writer, lastid tour.ID) {
|
||||
for _, id := range tour.IDs {
|
||||
c := ' '
|
||||
switch {
|
||||
case id == lastid:
|
||||
c = '*'
|
||||
case id.LessThan(lastid):
|
||||
c = '✓'
|
||||
}
|
||||
|
||||
t := tour.Topics[id]
|
||||
fmt.Fprintf(w, "- %c %-5.5s %s\n", c, id, t.Title)
|
||||
}
|
||||
}
|
||||
|
||||
// fprintTourShow writes a text-formatted topic to the writer
|
||||
func fprintTourShow(w io.Writer, t *tour.Topic) error {
|
||||
tmpl := `
|
||||
Tour {{ .ID }} - {{ .Title }}
|
||||
|
||||
{{ .Text }}
|
||||
|
||||
`
|
||||
ttempl, err := template.New("tour").Parse(tmpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ttempl.Execute(w, t)
|
||||
}
|
||||
|
||||
// tourGet returns the topic given its ID. Returns an error if topic does not
|
||||
// exist.
|
||||
func tourGet(id tour.ID) (*tour.Topic, error) {
|
||||
t, found := tour.Topics[id]
|
||||
if !found {
|
||||
return nil, fmt.Errorf("no topic with id: %s", id)
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
// TODO share func
|
||||
func writeConfig(path string, cfg *config.Config) error {
|
||||
filename, err := config.Filename(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return config.WriteConfigFile(filename, cfg)
|
||||
}
|
27
cmd/ipfs2/tour_test.go
Normal file
27
cmd/ipfs2/tour_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/jbenet/go-ipfs/tour"
|
||||
)
|
||||
|
||||
func TestParseTourTemplate(t *testing.T) {
|
||||
topic := &tour.Topic{
|
||||
ID: "42",
|
||||
Content: tour.Content{
|
||||
Title: "IPFS CLI test files",
|
||||
Text: `
|
||||
Welcome to the IPFS test files
|
||||
This is where we test our beautiful command line interfaces
|
||||
`,
|
||||
},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := fprintTourShow(&buf, topic)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(buf.String())
|
||||
}
|
@ -8,8 +8,35 @@ const (
|
||||
)
|
||||
|
||||
type Argument struct {
|
||||
Name string
|
||||
Type ArgumentType
|
||||
Required bool
|
||||
Variadic bool
|
||||
Name string
|
||||
Type ArgumentType
|
||||
Required bool
|
||||
Variadic bool
|
||||
SupportsStdin bool
|
||||
Description string
|
||||
}
|
||||
|
||||
func StringArg(name string, required, variadic bool, description string) Argument {
|
||||
return Argument{
|
||||
Name: name,
|
||||
Type: ArgString,
|
||||
Required: required,
|
||||
Variadic: variadic,
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
func FileArg(name string, required, variadic bool, description string) Argument {
|
||||
return Argument{
|
||||
Name: name,
|
||||
Type: ArgFile,
|
||||
Required: required,
|
||||
Variadic: variadic,
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
func (a Argument) EnableStdin() Argument {
|
||||
a.SupportsStdin = true
|
||||
return a
|
||||
}
|
||||
|
379
commands/cli/helptext.go
Normal file
379
commands/cli/helptext.go
Normal file
@ -0,0 +1,379 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
)
|
||||
|
||||
const (
|
||||
requiredArg = "<%v>"
|
||||
optionalArg = "[<%v>]"
|
||||
variadicArg = "%v..."
|
||||
optionFlag = "-%v"
|
||||
optionType = "(%v)"
|
||||
|
||||
whitespace = "\r\n\t "
|
||||
|
||||
indentStr = " "
|
||||
)
|
||||
|
||||
type helpFields struct {
|
||||
Indent string
|
||||
Usage string
|
||||
Path string
|
||||
ArgUsage string
|
||||
Tagline string
|
||||
Arguments string
|
||||
Options string
|
||||
Synopsis string
|
||||
Subcommands string
|
||||
Description string
|
||||
}
|
||||
|
||||
// TrimNewlines removes extra newlines from fields. This makes aligning
|
||||
// commands easier. Below, the leading + tralining newlines are removed:
|
||||
// Synopsis: `
|
||||
// ipfs config <key> - Get value of <key>
|
||||
// ipfs config <key> <value> - Set value of <key> to <value>
|
||||
// ipfs config --show - Show config file
|
||||
// ipfs config --edit - Edit config file in $EDITOR
|
||||
// `
|
||||
func (f *helpFields) TrimNewlines() {
|
||||
f.Path = strings.Trim(f.Path, "\n")
|
||||
f.ArgUsage = strings.Trim(f.ArgUsage, "\n")
|
||||
f.Tagline = strings.Trim(f.Tagline, "\n")
|
||||
f.Arguments = strings.Trim(f.Arguments, "\n")
|
||||
f.Options = strings.Trim(f.Options, "\n")
|
||||
f.Synopsis = strings.Trim(f.Synopsis, "\n")
|
||||
f.Subcommands = strings.Trim(f.Subcommands, "\n")
|
||||
f.Description = strings.Trim(f.Description, "\n")
|
||||
}
|
||||
|
||||
// Indent adds whitespace the lines of fields.
|
||||
func (f *helpFields) IndentAll() {
|
||||
indent := func(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
return indentString(s, indentStr)
|
||||
}
|
||||
|
||||
f.Arguments = indent(f.Arguments)
|
||||
f.Options = indent(f.Options)
|
||||
f.Synopsis = indent(f.Synopsis)
|
||||
f.Subcommands = indent(f.Subcommands)
|
||||
f.Description = indent(f.Description)
|
||||
}
|
||||
|
||||
const usageFormat = "{{if .Usage}}{{.Usage}}{{else}}{{.Path}}{{if .ArgUsage}} {{.ArgUsage}}{{end}} - {{.Tagline}}{{end}}"
|
||||
|
||||
const longHelpFormat = `
|
||||
{{.Indent}}{{template "usage" .}}
|
||||
|
||||
{{if .Arguments}}ARGUMENTS:
|
||||
|
||||
{{.Arguments}}
|
||||
|
||||
{{end}}{{if .Options}}OPTIONS:
|
||||
|
||||
{{.Options}}
|
||||
|
||||
{{end}}{{if .Subcommands}}SUBCOMMANDS:
|
||||
|
||||
{{.Subcommands}}
|
||||
|
||||
{{.Indent}}Use '{{.Path}} <subcmd> --help' for more information about each command.
|
||||
|
||||
{{end}}{{if .Description}}DESCRIPTION:
|
||||
|
||||
{{.Description}}
|
||||
|
||||
{{end}}
|
||||
`
|
||||
const shortHelpFormat = `USAGE:
|
||||
|
||||
{{.Indent}}{{template "usage" .}}
|
||||
{{if .Synopsis}}
|
||||
SYNOPSIS
|
||||
|
||||
{{.Synopsis}}
|
||||
{{end}}{{if .Description}}
|
||||
{{.Description}}
|
||||
{{end}}
|
||||
Use '{{.Path}} --help' for more information about this command.
|
||||
`
|
||||
|
||||
var usageTemplate *template.Template
|
||||
var longHelpTemplate *template.Template
|
||||
var shortHelpTemplate *template.Template
|
||||
|
||||
func init() {
|
||||
usageTemplate = template.Must(template.New("usage").Parse(usageFormat))
|
||||
longHelpTemplate = template.Must(usageTemplate.New("longHelp").Parse(longHelpFormat))
|
||||
shortHelpTemplate = template.Must(usageTemplate.New("shortHelp").Parse(shortHelpFormat))
|
||||
}
|
||||
|
||||
// LongHelp returns a formatted CLI helptext string, generated for the given command
|
||||
func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer) error {
|
||||
cmd, err := root.Get(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pathStr := rootName
|
||||
if len(path) > 0 {
|
||||
pathStr += " " + strings.Join(path, " ")
|
||||
}
|
||||
|
||||
fields := helpFields{
|
||||
Indent: indentStr,
|
||||
Path: pathStr,
|
||||
ArgUsage: usageText(cmd),
|
||||
Tagline: cmd.Helptext.Tagline,
|
||||
Arguments: cmd.Helptext.Arguments,
|
||||
Options: cmd.Helptext.Options,
|
||||
Synopsis: cmd.Helptext.Synopsis,
|
||||
Subcommands: cmd.Helptext.Subcommands,
|
||||
Description: cmd.Helptext.ShortDescription,
|
||||
Usage: cmd.Helptext.Usage,
|
||||
}
|
||||
|
||||
if len(cmd.Helptext.LongDescription) > 0 {
|
||||
fields.Description = cmd.Helptext.LongDescription
|
||||
}
|
||||
|
||||
// autogen fields that are empty
|
||||
if len(fields.Arguments) == 0 {
|
||||
fields.Arguments = strings.Join(argumentText(cmd), "\n")
|
||||
}
|
||||
if len(fields.Options) == 0 {
|
||||
fields.Options = strings.Join(optionText(cmd), "\n")
|
||||
}
|
||||
if len(fields.Subcommands) == 0 {
|
||||
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
|
||||
}
|
||||
|
||||
// trim the extra newlines (see TrimNewlines doc)
|
||||
fields.TrimNewlines()
|
||||
|
||||
// indent all fields that have been set
|
||||
fields.IndentAll()
|
||||
|
||||
return longHelpTemplate.Execute(out, fields)
|
||||
}
|
||||
|
||||
// ShortHelp returns a formatted CLI helptext string, generated for the given command
|
||||
func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer) error {
|
||||
cmd, err := root.Get(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// default cmd to root if there is no path
|
||||
if path == nil && cmd == nil {
|
||||
cmd = root
|
||||
}
|
||||
|
||||
pathStr := rootName
|
||||
if len(path) > 0 {
|
||||
pathStr += " " + strings.Join(path, " ")
|
||||
}
|
||||
|
||||
fields := helpFields{
|
||||
Indent: indentStr,
|
||||
Path: pathStr,
|
||||
ArgUsage: usageText(cmd),
|
||||
Tagline: cmd.Helptext.Tagline,
|
||||
Synopsis: cmd.Helptext.Synopsis,
|
||||
Description: cmd.Helptext.ShortDescription,
|
||||
Usage: cmd.Helptext.Usage,
|
||||
}
|
||||
|
||||
// trim the extra newlines (see TrimNewlines doc)
|
||||
fields.TrimNewlines()
|
||||
|
||||
// indent all fields that have been set
|
||||
fields.IndentAll()
|
||||
|
||||
return shortHelpTemplate.Execute(out, fields)
|
||||
}
|
||||
|
||||
func argumentText(cmd *cmds.Command) []string {
|
||||
lines := make([]string, len(cmd.Arguments))
|
||||
|
||||
for i, arg := range cmd.Arguments {
|
||||
lines[i] = argUsageText(arg)
|
||||
}
|
||||
lines = align(lines)
|
||||
for i, arg := range cmd.Arguments {
|
||||
lines[i] += " - " + arg.Description
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func optionText(cmd ...*cmds.Command) []string {
|
||||
// get a slice of the options we want to list out
|
||||
options := make([]cmds.Option, 0)
|
||||
for _, c := range cmd {
|
||||
for _, opt := range c.Options {
|
||||
options = append(options, opt)
|
||||
}
|
||||
}
|
||||
|
||||
// add option names to output (with each name aligned)
|
||||
lines := make([]string, 0)
|
||||
j := 0
|
||||
for {
|
||||
done := true
|
||||
i := 0
|
||||
for _, opt := range options {
|
||||
if len(lines) < i+1 {
|
||||
lines = append(lines, "")
|
||||
}
|
||||
|
||||
names := sortByLength(opt.Names)
|
||||
if len(names) >= j+1 {
|
||||
lines[i] += fmt.Sprintf(optionFlag, names[j])
|
||||
}
|
||||
if len(names) > j+1 {
|
||||
lines[i] += ", "
|
||||
done = false
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
if done {
|
||||
break
|
||||
}
|
||||
|
||||
lines = align(lines)
|
||||
j++
|
||||
}
|
||||
lines = align(lines)
|
||||
|
||||
// add option types to output
|
||||
for i, opt := range options {
|
||||
lines[i] += " " + fmt.Sprintf("%v", opt.Type)
|
||||
}
|
||||
lines = align(lines)
|
||||
|
||||
// add option descriptions to output
|
||||
for i, opt := range options {
|
||||
lines[i] += " - " + opt.Description
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func subcommandText(cmd *cmds.Command, rootName string, path []string) []string {
|
||||
prefix := fmt.Sprintf("%v %v", rootName, strings.Join(path, " "))
|
||||
if len(path) > 0 {
|
||||
prefix += " "
|
||||
}
|
||||
subcmds := make([]*cmds.Command, len(cmd.Subcommands))
|
||||
lines := make([]string, len(cmd.Subcommands))
|
||||
|
||||
i := 0
|
||||
for name, sub := range cmd.Subcommands {
|
||||
usage := usageText(sub)
|
||||
if len(usage) > 0 {
|
||||
usage = " " + usage
|
||||
}
|
||||
lines[i] = prefix + name + usage
|
||||
subcmds[i] = sub
|
||||
i++
|
||||
}
|
||||
|
||||
lines = align(lines)
|
||||
for i, sub := range subcmds {
|
||||
lines[i] += " - " + sub.Helptext.Tagline
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func usageText(cmd *cmds.Command) string {
|
||||
s := ""
|
||||
for i, arg := range cmd.Arguments {
|
||||
if i != 0 {
|
||||
s += " "
|
||||
}
|
||||
s += argUsageText(arg)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func argUsageText(arg cmds.Argument) string {
|
||||
s := arg.Name
|
||||
|
||||
if arg.Required {
|
||||
s = fmt.Sprintf(requiredArg, s)
|
||||
} else {
|
||||
s = fmt.Sprintf(optionalArg, s)
|
||||
}
|
||||
|
||||
if arg.Variadic {
|
||||
s = fmt.Sprintf(variadicArg, s)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func align(lines []string) []string {
|
||||
longest := 0
|
||||
for _, line := range lines {
|
||||
length := len(line)
|
||||
if length > longest {
|
||||
longest = length
|
||||
}
|
||||
}
|
||||
|
||||
for i, line := range lines {
|
||||
length := len(line)
|
||||
if length > 0 {
|
||||
lines[i] += strings.Repeat(" ", longest-length)
|
||||
}
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func indent(lines []string, prefix string) []string {
|
||||
for i, line := range lines {
|
||||
lines[i] = prefix + indentString(line, prefix)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func indentString(line string, prefix string) string {
|
||||
return prefix + strings.Replace(line, "\n", "\n"+prefix, -1)
|
||||
}
|
||||
|
||||
type lengthSlice []string
|
||||
|
||||
func (ls lengthSlice) Len() int {
|
||||
return len(ls)
|
||||
}
|
||||
func (ls lengthSlice) Swap(a, b int) {
|
||||
ls[a], ls[b] = ls[b], ls[a]
|
||||
}
|
||||
func (ls lengthSlice) Less(a, b int) bool {
|
||||
return len(ls[a]) < len(ls[b])
|
||||
}
|
||||
|
||||
func sortByLength(slice []string) []string {
|
||||
output := make(lengthSlice, len(slice))
|
||||
for i, val := range slice {
|
||||
output[i] = val
|
||||
}
|
||||
sort.Sort(output)
|
||||
return []string(output)
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@ -9,50 +10,50 @@ import (
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
)
|
||||
|
||||
// ErrInvalidSubcmd signals when the parse error is not found
|
||||
var ErrInvalidSubcmd = errors.New("subcommand not found")
|
||||
|
||||
// Parse parses the input commandline string (cmd, flags, and args).
|
||||
// returns the corresponding command Request object.
|
||||
func Parse(input []string, roots ...*cmds.Command) (cmds.Request, *cmds.Command, error) {
|
||||
var root, cmd *cmds.Command
|
||||
var path, stringArgs []string
|
||||
var opts map[string]interface{}
|
||||
|
||||
// Parse will search each root to find the one that best matches the requested subcommand.
|
||||
func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *cmds.Command, []string, error) {
|
||||
// use the root that matches the longest path (most accurately matches request)
|
||||
maxLength := 0
|
||||
for _, r := range roots {
|
||||
p, i, c := parsePath(input, r)
|
||||
o, s, err := parseOptions(i)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
length := len(p)
|
||||
if length > maxLength {
|
||||
maxLength = length
|
||||
root = r
|
||||
path = p
|
||||
cmd = c
|
||||
opts = o
|
||||
stringArgs = s
|
||||
}
|
||||
}
|
||||
|
||||
if maxLength == 0 {
|
||||
return nil, nil, errors.New("Not a valid subcommand")
|
||||
}
|
||||
|
||||
args, err := parseArgs(stringArgs, cmd)
|
||||
path, input, cmd := parsePath(input, root)
|
||||
opts, stringArgs, err := parseOptions(input)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, cmd, path, err
|
||||
}
|
||||
|
||||
req := cmds.NewRequest(path, opts, args, cmd)
|
||||
if len(path) == 0 {
|
||||
return nil, nil, path, ErrInvalidSubcmd
|
||||
}
|
||||
|
||||
args, err := parseArgs(stringArgs, stdin, cmd.Arguments)
|
||||
if err != nil {
|
||||
return nil, cmd, path, err
|
||||
}
|
||||
|
||||
optDefs, err := root.GetOptions(path)
|
||||
if err != nil {
|
||||
return nil, cmd, path, err
|
||||
}
|
||||
|
||||
// check to make sure there aren't any undefined options
|
||||
for k := range opts {
|
||||
if _, found := optDefs[k]; !found {
|
||||
err = fmt.Errorf("Unrecognized option: -%s", k)
|
||||
return nil, cmd, path, err
|
||||
}
|
||||
}
|
||||
|
||||
req := cmds.NewRequest(path, opts, args, cmd, optDefs)
|
||||
|
||||
err = cmd.CheckArguments(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return req, cmd, path, err
|
||||
}
|
||||
|
||||
return req, root, nil
|
||||
return req, cmd, path, nil
|
||||
}
|
||||
|
||||
// parsePath separates the command path and the opts and args from a command string
|
||||
@ -116,25 +117,89 @@ func parseOptions(input []string) (map[string]interface{}, []string, error) {
|
||||
return opts, args, nil
|
||||
}
|
||||
|
||||
func parseArgs(stringArgs []string, cmd *cmds.Command) ([]interface{}, error) {
|
||||
var argDef cmds.Argument
|
||||
args := make([]interface{}, len(stringArgs))
|
||||
func parseArgs(stringArgs []string, stdin *os.File, arguments []cmds.Argument) ([]interface{}, error) {
|
||||
// check if stdin is coming from terminal or is being piped in
|
||||
if stdin != nil {
|
||||
stat, err := stdin.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, arg := range stringArgs {
|
||||
if i < len(cmd.Arguments) {
|
||||
argDef = cmd.Arguments[i]
|
||||
// if stdin isn't a CharDevice, set it to nil
|
||||
// (this means it is coming from terminal and we want to ignore it)
|
||||
if (stat.Mode() & os.ModeCharDevice) != 0 {
|
||||
stdin = nil
|
||||
}
|
||||
}
|
||||
|
||||
// count required argument definitions
|
||||
lenRequired := 0
|
||||
for _, argDef := range arguments {
|
||||
if argDef.Required {
|
||||
lenRequired++
|
||||
}
|
||||
}
|
||||
|
||||
valCount := len(stringArgs)
|
||||
if stdin != nil {
|
||||
valCount += 1
|
||||
}
|
||||
|
||||
args := make([]interface{}, 0, valCount)
|
||||
|
||||
argDefIndex := 0 // the index of the current argument definition
|
||||
for i := 0; i < valCount; i++ {
|
||||
// get the argument definiton (should be arguments[argDefIndex],
|
||||
// but if argDefIndex > len(arguments) we use the last argument definition)
|
||||
var argDef cmds.Argument
|
||||
if argDefIndex < len(arguments) {
|
||||
argDef = arguments[argDefIndex]
|
||||
} else if len(arguments) > 0 {
|
||||
argDef = arguments[len(arguments)-1]
|
||||
}
|
||||
|
||||
// skip optional argument definitions if there aren't sufficient remaining values
|
||||
if valCount-i <= lenRequired && !argDef.Required {
|
||||
continue
|
||||
} else if argDef.Required {
|
||||
lenRequired--
|
||||
}
|
||||
|
||||
if argDef.Type == cmds.ArgString {
|
||||
args[i] = arg
|
||||
if stdin == nil {
|
||||
// add string values
|
||||
args = append(args, stringArgs[0])
|
||||
stringArgs = stringArgs[1:]
|
||||
|
||||
} else {
|
||||
in, err := os.Open(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if argDef.SupportsStdin {
|
||||
// if we have a stdin, read it in and use the data as a string value
|
||||
var buf bytes.Buffer
|
||||
_, err := buf.ReadFrom(stdin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, buf.String())
|
||||
stdin = nil
|
||||
}
|
||||
|
||||
} else if argDef.Type == cmds.ArgFile {
|
||||
if stdin == nil {
|
||||
// treat stringArg values as file paths
|
||||
file, err := os.Open(stringArgs[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, file)
|
||||
stringArgs = stringArgs[1:]
|
||||
|
||||
} else if argDef.SupportsStdin {
|
||||
// if we have a stdin, use that as a reader
|
||||
args = append(args, stdin)
|
||||
stdin = nil
|
||||
}
|
||||
args[i] = in
|
||||
}
|
||||
|
||||
argDefIndex++
|
||||
}
|
||||
|
||||
return args, nil
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
@ -13,11 +14,31 @@ var log = u.Logger("command")
|
||||
|
||||
// Function is the type of function that Commands use.
|
||||
// It reads from the Request, and writes results to the Response.
|
||||
type Function func(Response, Request)
|
||||
type Function func(Request) (interface{}, error)
|
||||
|
||||
// Marshaller is a function that takes in a Response, and returns a marshalled []byte
|
||||
// Marshaler is a function that takes in a Response, and returns a marshalled []byte
|
||||
// (or an error on failure)
|
||||
type Marshaller func(Response) ([]byte, error)
|
||||
type Marshaler func(Response) ([]byte, error)
|
||||
|
||||
// MarshalerMap is a map of Marshaler functions, keyed by EncodingType
|
||||
// (or an error on failure)
|
||||
type MarshalerMap map[EncodingType]Marshaler
|
||||
|
||||
// HelpText is a set of strings used to generate command help text. The help
|
||||
// text follows formats similar to man pages, but not exactly the same.
|
||||
type HelpText struct {
|
||||
// required
|
||||
Tagline string // used in <cmd usage>
|
||||
ShortDescription string // used in DESCRIPTION
|
||||
Synopsis string // showcasing the cmd
|
||||
|
||||
// optional - whole section overrides
|
||||
Usage string // overrides USAGE section
|
||||
LongDescription string // overrides DESCRIPTION section
|
||||
Options string // overrides OPTIONS section
|
||||
Arguments string // overrides ARGUMENTS section
|
||||
Subcommands string // overrides SUBCOMMANDS section
|
||||
}
|
||||
|
||||
// TODO: check Argument definitions when creating a Command
|
||||
// (might need to use a Command constructor)
|
||||
@ -28,19 +49,27 @@ type Marshaller func(Response) ([]byte, error)
|
||||
// Command is a runnable command, with input arguments and options (flags).
|
||||
// It can also have Subcommands, to group units of work into sets.
|
||||
type Command struct {
|
||||
Help string
|
||||
Options []Option
|
||||
Arguments []Argument
|
||||
Run Function
|
||||
Marshallers map[EncodingType]Marshaller
|
||||
Options []Option
|
||||
Arguments []Argument
|
||||
Run Function
|
||||
Marshalers map[EncodingType]Marshaler
|
||||
Helptext HelpText
|
||||
|
||||
// Type describes the type of the output of the Command's Run Function.
|
||||
// In precise terms, the value of Type is an instance of the return type of
|
||||
// the Run Function.
|
||||
//
|
||||
// ie. If command Run returns &Block{}, then Command.Type == &Block{}
|
||||
Type interface{}
|
||||
Subcommands map[string]*Command
|
||||
}
|
||||
|
||||
// ErrNotCallable signals a command that cannot be called.
|
||||
var ErrNotCallable = errors.New("This command can't be called directly. Try one of its subcommands.")
|
||||
var ErrNotCallable = ClientError("This command can't be called directly. Try one of its subcommands.")
|
||||
|
||||
var ErrNoFormatter = errors.New("This command cannot be formatted to plain text")
|
||||
var ErrNoFormatter = ClientError("This command cannot be formatted to plain text")
|
||||
|
||||
var ErrIncorrectType = errors.New("The command returned a value with a different type than expected")
|
||||
|
||||
// Call invokes the command for the given Request
|
||||
func (c *Command) Call(req Request) Response {
|
||||
@ -64,20 +93,47 @@ func (c *Command) Call(req Request) Response {
|
||||
return res
|
||||
}
|
||||
|
||||
options, err := c.GetOptions(req.Path())
|
||||
err = req.ConvertOptions()
|
||||
if err != nil {
|
||||
res.SetError(err, ErrClient)
|
||||
return res
|
||||
}
|
||||
|
||||
err = req.ConvertOptions(options)
|
||||
output, err := cmd.Run(req)
|
||||
if err != nil {
|
||||
res.SetError(err, ErrClient)
|
||||
// if returned error is a commands.Error, use its error code
|
||||
// otherwise, just default the code to ErrNormal
|
||||
switch e := err.(type) {
|
||||
case *Error:
|
||||
res.SetError(e, e.Code)
|
||||
case Error:
|
||||
res.SetError(e, e.Code)
|
||||
default:
|
||||
res.SetError(err, ErrNormal)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
cmd.Run(res, req)
|
||||
// If the command specified an output type, ensure the actual value returned is of that type
|
||||
if cmd.Type != nil {
|
||||
definedType := reflect.ValueOf(cmd.Type).Type()
|
||||
actualType := reflect.ValueOf(output).Type()
|
||||
|
||||
if definedType != actualType {
|
||||
res.SetError(ErrIncorrectType, ErrNormal)
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
// clean up the request (close the readers, e.g. fileargs)
|
||||
// NOTE: this means commands can't expect to keep reading after cmd.Run returns (in a goroutine)
|
||||
err = req.Cleanup()
|
||||
if err != nil {
|
||||
res.SetError(err, ErrNormal)
|
||||
return res
|
||||
}
|
||||
|
||||
res.SetOutput(output)
|
||||
return res
|
||||
}
|
||||
|
||||
@ -149,13 +205,27 @@ func (c *Command) CheckArguments(req Request) error {
|
||||
return fmt.Errorf("Expected %v arguments, got %v", len(argDefs), len(args))
|
||||
}
|
||||
|
||||
// count required argument definitions
|
||||
numRequired := 0
|
||||
for _, argDef := range c.Arguments {
|
||||
if argDef.Required {
|
||||
numRequired++
|
||||
}
|
||||
}
|
||||
|
||||
// iterate over the arg definitions
|
||||
for i, argDef := range c.Arguments {
|
||||
valueIndex := 0 // the index of the current value (in `args`)
|
||||
for _, argDef := range c.Arguments {
|
||||
// skip optional argument definitions if there aren't sufficient remaining values
|
||||
if len(args)-valueIndex <= numRequired && !argDef.Required {
|
||||
continue
|
||||
}
|
||||
|
||||
// the value for this argument definition. can be nil if it wasn't provided by the caller
|
||||
var v interface{}
|
||||
if i < len(args) {
|
||||
v = args[i]
|
||||
if valueIndex < len(args) {
|
||||
v = args[valueIndex]
|
||||
valueIndex++
|
||||
}
|
||||
|
||||
err := checkArgValue(v, argDef)
|
||||
@ -164,8 +234,8 @@ func (c *Command) CheckArguments(req Request) error {
|
||||
}
|
||||
|
||||
// any additional values are for the variadic arg definition
|
||||
if argDef.Variadic && i < len(args)-1 {
|
||||
for _, val := range args[i+1:] {
|
||||
if argDef.Variadic && valueIndex < len(args)-1 {
|
||||
for _, val := range args[valueIndex:] {
|
||||
err := checkArgValue(val, argDef)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -207,3 +277,7 @@ func checkArgValue(v interface{}, def Argument) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClientError(msg string) error {
|
||||
return &Error{Code: ErrClient, Message: msg}
|
||||
}
|
||||
|
@ -2,38 +2,36 @@ package commands
|
||||
|
||||
import "testing"
|
||||
|
||||
func noop(req Request) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestOptionValidation(t *testing.T) {
|
||||
cmd := Command{
|
||||
Options: []Option{
|
||||
Option{[]string{"b", "beep"}, Int},
|
||||
Option{[]string{"B", "boop"}, String},
|
||||
Option{[]string{"b", "beep"}, Int, "enables beeper"},
|
||||
Option{[]string{"B", "boop"}, String, "password for booper"},
|
||||
},
|
||||
Run: func(res Response, req Request) {},
|
||||
Run: noop,
|
||||
}
|
||||
|
||||
req := NewEmptyRequest()
|
||||
req.SetOption("beep", 5)
|
||||
req.SetOption("b", 10)
|
||||
opts, _ := cmd.GetOptions(nil)
|
||||
|
||||
req := NewRequest(nil, nil, nil, nil, opts)
|
||||
req.SetOption("beep", true)
|
||||
res := cmd.Call(req)
|
||||
if res.Error() == nil {
|
||||
t.Error("Should have failed (duplicate options)")
|
||||
}
|
||||
|
||||
req = NewEmptyRequest()
|
||||
req.SetOption("beep", "foo")
|
||||
res = cmd.Call(req)
|
||||
if res.Error() == nil {
|
||||
t.Error("Should have failed (incorrect type)")
|
||||
}
|
||||
|
||||
req = NewEmptyRequest()
|
||||
req = NewRequest(nil, nil, nil, nil, opts)
|
||||
req.SetOption("beep", 5)
|
||||
res = cmd.Call(req)
|
||||
if res.Error() != nil {
|
||||
t.Error(res.Error(), "Should have passed")
|
||||
}
|
||||
|
||||
req = NewEmptyRequest()
|
||||
req = NewRequest(nil, nil, nil, nil, opts)
|
||||
req.SetOption("beep", 5)
|
||||
req.SetOption("boop", "test")
|
||||
res = cmd.Call(req)
|
||||
@ -41,7 +39,7 @@ func TestOptionValidation(t *testing.T) {
|
||||
t.Error("Should have passed")
|
||||
}
|
||||
|
||||
req = NewEmptyRequest()
|
||||
req = NewRequest(nil, nil, nil, nil, opts)
|
||||
req.SetOption("b", 5)
|
||||
req.SetOption("B", "test")
|
||||
res = cmd.Call(req)
|
||||
@ -49,48 +47,46 @@ func TestOptionValidation(t *testing.T) {
|
||||
t.Error("Should have passed")
|
||||
}
|
||||
|
||||
req = NewEmptyRequest()
|
||||
req = NewRequest(nil, nil, nil, nil, opts)
|
||||
req.SetOption("foo", 5)
|
||||
res = cmd.Call(req)
|
||||
if res.Error() != nil {
|
||||
t.Error("Should have passed")
|
||||
}
|
||||
|
||||
req = NewEmptyRequest()
|
||||
req = NewRequest(nil, nil, nil, nil, opts)
|
||||
req.SetOption(EncShort, "json")
|
||||
res = cmd.Call(req)
|
||||
if res.Error() != nil {
|
||||
t.Error("Should have passed")
|
||||
}
|
||||
|
||||
req = NewEmptyRequest()
|
||||
req = NewRequest(nil, nil, nil, nil, opts)
|
||||
req.SetOption("b", "100")
|
||||
res = cmd.Call(req)
|
||||
if res.Error() != nil {
|
||||
t.Error("Should have passed")
|
||||
}
|
||||
|
||||
req = NewEmptyRequest()
|
||||
req = NewRequest(nil, nil, nil, nil, opts)
|
||||
req.SetOption("b", ":)")
|
||||
res = cmd.Call(req)
|
||||
if res.Error() == nil {
|
||||
t.Error(res.Error(), "Should have failed (string value not convertible to int)")
|
||||
t.Error("Should have failed (string value not convertible to int)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistration(t *testing.T) {
|
||||
noop := func(res Response, req Request) {}
|
||||
|
||||
cmdA := &Command{
|
||||
Options: []Option{
|
||||
Option{[]string{"beep"}, Int},
|
||||
Option{[]string{"beep"}, Int, "number of beeps"},
|
||||
},
|
||||
Run: noop,
|
||||
}
|
||||
|
||||
cmdB := &Command{
|
||||
Options: []Option{
|
||||
Option{[]string{"beep"}, Int},
|
||||
Option{[]string{"beep"}, Int, "number of beeps"},
|
||||
},
|
||||
Run: noop,
|
||||
Subcommands: map[string]*Command{
|
||||
@ -100,18 +96,19 @@ func TestRegistration(t *testing.T) {
|
||||
|
||||
cmdC := &Command{
|
||||
Options: []Option{
|
||||
Option{[]string{"encoding"}, String},
|
||||
Option{[]string{"encoding"}, String, "data encoding type"},
|
||||
},
|
||||
Run: noop,
|
||||
}
|
||||
|
||||
res := cmdB.Call(NewRequest([]string{"a"}, nil, nil, nil))
|
||||
if res.Error() == nil {
|
||||
path := []string{"a"}
|
||||
_, err := cmdB.GetOptions(path)
|
||||
if err == nil {
|
||||
t.Error("Should have failed (option name collision)")
|
||||
}
|
||||
|
||||
res = cmdC.Call(NewEmptyRequest())
|
||||
if res.Error() == nil {
|
||||
_, err = cmdC.GetOptions(nil)
|
||||
if err == nil {
|
||||
t.Error("Should have failed (option name collision with global options)")
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package http
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -11,10 +10,9 @@ import (
|
||||
"strings"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
var castError = errors.New("cast error")
|
||||
|
||||
const (
|
||||
ApiUrlFormat = "http://%s%s/%s?%s"
|
||||
ApiPath = "/api/v0" // TODO: make configurable
|
||||
@ -34,24 +32,16 @@ func NewClient(address string) Client {
|
||||
}
|
||||
|
||||
func (c *client) Send(req cmds.Request) (cmds.Response, error) {
|
||||
var userEncoding string
|
||||
if enc, found := req.Option(cmds.EncShort); found {
|
||||
var ok bool
|
||||
userEncoding, ok = enc.(string)
|
||||
if !ok {
|
||||
return nil, castError
|
||||
}
|
||||
req.SetOption(cmds.EncShort, cmds.JSON)
|
||||
} else {
|
||||
var ok bool
|
||||
enc, _ := req.Option(cmds.EncLong)
|
||||
userEncoding, ok = enc.(string)
|
||||
if !ok {
|
||||
return nil, castError
|
||||
}
|
||||
req.SetOption(cmds.EncLong, cmds.JSON)
|
||||
|
||||
// save user-provided encoding
|
||||
previousUserProvidedEncoding, found, err := req.Option(cmds.EncShort).String()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// override with json to send to server
|
||||
req.SetOption(cmds.EncShort, cmds.JSON)
|
||||
|
||||
query, inputStream, err := getQuery(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -60,19 +50,23 @@ func (c *client) Send(req cmds.Request) (cmds.Response, error) {
|
||||
path := strings.Join(req.Path(), "/")
|
||||
url := fmt.Sprintf(ApiUrlFormat, c.serverAddress, ApiPath, path, query)
|
||||
|
||||
// TODO extract string const?
|
||||
httpRes, err := http.Post(url, "application/octet-stream", inputStream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// using the overridden JSON encoding in request
|
||||
res, err := getResponse(httpRes, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(userEncoding) > 0 {
|
||||
req.SetOption(cmds.EncShort, userEncoding)
|
||||
req.SetOption(cmds.EncLong, userEncoding)
|
||||
if found && len(previousUserProvidedEncoding) > 0 {
|
||||
// reset to user provided encoding after sending request
|
||||
// NB: if user has provided an encoding but it is the empty string,
|
||||
// still leave it as JSON.
|
||||
req.SetOption(cmds.EncShort, previousUserProvidedEncoding)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
@ -84,10 +78,7 @@ func getQuery(req cmds.Request) (string, io.Reader, error) {
|
||||
|
||||
query := url.Values{}
|
||||
for k, v := range req.Options() {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return "", nil, castError
|
||||
}
|
||||
str := fmt.Sprintf("%v", v)
|
||||
query.Set(k, str)
|
||||
}
|
||||
|
||||
@ -103,7 +94,7 @@ func getQuery(req cmds.Request) (string, io.Reader, error) {
|
||||
if argDef.Type == cmds.ArgString {
|
||||
str, ok := arg.(string)
|
||||
if !ok {
|
||||
return "", nil, castError
|
||||
return "", nil, u.ErrCast()
|
||||
}
|
||||
query.Add("arg", str)
|
||||
|
||||
@ -115,7 +106,7 @@ func getQuery(req cmds.Request) (string, io.Reader, error) {
|
||||
var ok bool
|
||||
inputStream, ok = arg.(io.Reader)
|
||||
if !ok {
|
||||
return "", nil, castError
|
||||
return "", nil, u.ErrCast()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -131,7 +122,7 @@ func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error
|
||||
contentType := httpRes.Header["Content-Type"][0]
|
||||
contentType = strings.Split(contentType, ";")[0]
|
||||
|
||||
if contentType == "application/octet-stream" {
|
||||
if len(httpRes.Header.Get(streamHeader)) > 0 {
|
||||
res.SetOutput(httpRes.Body)
|
||||
return res, nil
|
||||
}
|
||||
@ -166,7 +157,7 @@ func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error
|
||||
} else {
|
||||
v := req.Command().Type
|
||||
err = dec.Decode(&v)
|
||||
if err != nil {
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -6,8 +6,11 @@ import (
|
||||
"net/http"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
var log = u.Logger("commands/http")
|
||||
|
||||
type Handler struct {
|
||||
ctx cmds.Context
|
||||
root *cmds.Command
|
||||
@ -15,6 +18,8 @@ type Handler struct {
|
||||
|
||||
var ErrNotFound = errors.New("404 page not found")
|
||||
|
||||
const streamHeader = "X-Stream-Output"
|
||||
|
||||
var mimeTypes = map[string]string{
|
||||
cmds.JSON: "application/json",
|
||||
cmds.XML: "application/xml",
|
||||
@ -26,6 +31,8 @@ func NewHandler(ctx cmds.Context, root *cmds.Command) *Handler {
|
||||
}
|
||||
|
||||
func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debug("Incoming API request: ", r.URL)
|
||||
|
||||
req, err := Parse(r, i.root)
|
||||
if err != nil {
|
||||
if err == ErrNotFound {
|
||||
@ -43,16 +50,19 @@ func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// set the Content-Type based on res output
|
||||
if _, ok := res.Output().(io.Reader); ok {
|
||||
// TODO: set based on actual Content-Type of file
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
// we don't set the Content-Type for streams, so that browsers can MIME-sniff the type themselves
|
||||
// we set this header so clients have a way to know this is an output stream
|
||||
// (not marshalled command output)
|
||||
// TODO: set a specific Content-Type if the command response needs it to be a certain type
|
||||
w.Header().Set(streamHeader, "1")
|
||||
|
||||
} else {
|
||||
enc, _ := req.Option(cmds.EncShort)
|
||||
encStr, ok := enc.(string)
|
||||
if !ok {
|
||||
enc, found, err := req.Option(cmds.EncShort).String()
|
||||
if err != nil || !found {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
mime := mimeTypes[encStr]
|
||||
mime := mimeTypes[enc]
|
||||
w.Header().Set("Content-Type", mime)
|
||||
}
|
||||
|
||||
|
@ -39,20 +39,44 @@ func Parse(r *http.Request, root *cmds.Command) (cmds.Request, error) {
|
||||
opts, stringArgs2 := parseOptions(r)
|
||||
stringArgs = append(stringArgs, stringArgs2...)
|
||||
|
||||
// Note that the argument handling here is dumb, it does not do any error-checking.
|
||||
// (Arguments are further processed when the request is passed to the command to run)
|
||||
args := make([]interface{}, 0)
|
||||
// count required argument definitions
|
||||
numRequired := 0
|
||||
for _, argDef := range cmd.Arguments {
|
||||
if argDef.Required {
|
||||
numRequired++
|
||||
}
|
||||
}
|
||||
|
||||
for _, arg := range cmd.Arguments {
|
||||
if arg.Type == cmds.ArgString {
|
||||
if arg.Variadic {
|
||||
// count the number of provided argument values
|
||||
valCount := len(stringArgs)
|
||||
// TODO: add total number of parts in request body (instead of just 1 if body is present)
|
||||
if r.Body != nil && r.ContentLength != 0 {
|
||||
valCount += 1
|
||||
}
|
||||
|
||||
args := make([]interface{}, valCount)
|
||||
|
||||
valIndex := 0
|
||||
for _, argDef := range cmd.Arguments {
|
||||
// skip optional argument definitions if there aren't sufficient remaining values
|
||||
if valCount-valIndex <= numRequired && !argDef.Required {
|
||||
continue
|
||||
} else if argDef.Required {
|
||||
numRequired--
|
||||
}
|
||||
|
||||
if argDef.Type == cmds.ArgString {
|
||||
if argDef.Variadic {
|
||||
for _, s := range stringArgs {
|
||||
args = append(args, s)
|
||||
args[valIndex] = s
|
||||
valIndex++
|
||||
}
|
||||
valCount -= len(stringArgs)
|
||||
|
||||
} else if len(stringArgs) > 0 {
|
||||
args = append(args, stringArgs[0])
|
||||
args[valIndex] = stringArgs[0]
|
||||
stringArgs = stringArgs[1:]
|
||||
valIndex++
|
||||
|
||||
} else {
|
||||
break
|
||||
@ -60,11 +84,17 @@ func Parse(r *http.Request, root *cmds.Command) (cmds.Request, error) {
|
||||
|
||||
} else {
|
||||
// TODO: create multipart streams for file args
|
||||
args = append(args, r.Body)
|
||||
args[valIndex] = r.Body
|
||||
valIndex++
|
||||
}
|
||||
}
|
||||
|
||||
req := cmds.NewRequest(path, opts, args, cmd)
|
||||
optDefs, err := root.GetOptions(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := cmds.NewRequest(path, opts, args, cmd, optDefs)
|
||||
|
||||
err = cmd.CheckArguments(req)
|
||||
if err != nil {
|
||||
|
@ -1,6 +1,10 @@
|
||||
package commands
|
||||
|
||||
import "reflect"
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
// Types of Command options
|
||||
const (
|
||||
@ -14,14 +18,120 @@ const (
|
||||
|
||||
// Option is used to specify a field that will be provided by a consumer
|
||||
type Option struct {
|
||||
Names []string // a list of unique names to
|
||||
Type reflect.Kind // value must be this type
|
||||
Names []string // a list of unique names to
|
||||
Type reflect.Kind // value must be this type
|
||||
Description string // a short string to describe this option
|
||||
|
||||
// TODO: add more features(?):
|
||||
// MAYBE_TODO: add more features(?):
|
||||
//Default interface{} // the default value (ignored if `Required` is true)
|
||||
//Required bool // whether or not the option must be provided
|
||||
}
|
||||
|
||||
// constructor helper functions
|
||||
func NewOption(kind reflect.Kind, names ...string) Option {
|
||||
if len(names) < 2 {
|
||||
// FIXME(btc) don't panic (fix_before_merge)
|
||||
panic("Options require at least two string values (name and description)")
|
||||
}
|
||||
|
||||
desc := names[len(names)-1]
|
||||
names = names[:len(names)-1]
|
||||
|
||||
return Option{
|
||||
Names: names,
|
||||
Type: kind,
|
||||
Description: desc,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO handle description separately. this will take care of the panic case in
|
||||
// NewOption
|
||||
|
||||
// For all func {Type}Option(...string) functions, the last variadic argument
|
||||
// is treated as the description field.
|
||||
|
||||
func BoolOption(names ...string) Option {
|
||||
return NewOption(Bool, names...)
|
||||
}
|
||||
func IntOption(names ...string) Option {
|
||||
return NewOption(Int, names...)
|
||||
}
|
||||
func UintOption(names ...string) Option {
|
||||
return NewOption(Uint, names...)
|
||||
}
|
||||
func FloatOption(names ...string) Option {
|
||||
return NewOption(Float, names...)
|
||||
}
|
||||
func StringOption(names ...string) Option {
|
||||
return NewOption(String, names...)
|
||||
}
|
||||
|
||||
type OptionValue struct {
|
||||
value interface{}
|
||||
found bool
|
||||
}
|
||||
|
||||
// Found returns true if the option value was provided by the user (not a default value)
|
||||
func (ov OptionValue) Found() bool {
|
||||
return ov.found
|
||||
}
|
||||
|
||||
// value accessor methods, gets the value as a certain type
|
||||
func (ov OptionValue) Bool() (value bool, found bool, err error) {
|
||||
if !ov.found {
|
||||
return false, false, nil
|
||||
}
|
||||
val, ok := ov.value.(bool)
|
||||
if !ok {
|
||||
err = util.ErrCast()
|
||||
}
|
||||
return val, ov.found, err
|
||||
}
|
||||
|
||||
func (ov OptionValue) Int() (value int, found bool, err error) {
|
||||
if !ov.found {
|
||||
return 0, false, nil
|
||||
}
|
||||
val, ok := ov.value.(int)
|
||||
if !ok {
|
||||
err = util.ErrCast()
|
||||
}
|
||||
return val, ov.found, err
|
||||
}
|
||||
|
||||
func (ov OptionValue) Uint() (value uint, found bool, err error) {
|
||||
if !ov.found {
|
||||
return 0, false, nil
|
||||
}
|
||||
val, ok := ov.value.(uint)
|
||||
if !ok {
|
||||
err = util.ErrCast()
|
||||
}
|
||||
return val, ov.found, err
|
||||
}
|
||||
|
||||
func (ov OptionValue) Float() (value float64, found bool, err error) {
|
||||
if !ov.found {
|
||||
return 0, false, nil
|
||||
}
|
||||
val, ok := ov.value.(float64)
|
||||
if !ok {
|
||||
err = util.ErrCast()
|
||||
}
|
||||
return val, ov.found, err
|
||||
}
|
||||
|
||||
func (ov OptionValue) String() (value string, found bool, err error) {
|
||||
if !ov.found {
|
||||
return "", false, nil
|
||||
}
|
||||
val, ok := ov.value.(string)
|
||||
if !ok {
|
||||
err = util.ErrCast()
|
||||
}
|
||||
return val, ov.found, err
|
||||
}
|
||||
|
||||
// Flag names
|
||||
const (
|
||||
EncShort = "enc"
|
||||
@ -30,7 +140,8 @@ const (
|
||||
|
||||
// options that are used by this package
|
||||
var globalOptions = []Option{
|
||||
Option{[]string{EncShort, EncLong}, String},
|
||||
Option{[]string{EncShort, EncLong}, String,
|
||||
"The encoding type the output should be encoded with (json, xml, or text)"},
|
||||
}
|
||||
|
||||
// the above array of Options, wrapped in a Command
|
||||
|
36
commands/option_test.go
Normal file
36
commands/option_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package commands
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestOptionValueExtractBoolNotFound(t *testing.T) {
|
||||
t.Log("ensure that no error is returned when value is not found")
|
||||
optval := &OptionValue{found: false}
|
||||
_, _, err := optval.Bool()
|
||||
if err != nil {
|
||||
t.Fatal("Found was false. Err should have been nil")
|
||||
}
|
||||
|
||||
t.Log("ensure that no error is returned when value is not found (even if value exists)")
|
||||
optval = &OptionValue{value: "wrong type: a string", found: false}
|
||||
_, _, err = optval.Bool()
|
||||
if err != nil {
|
||||
t.Fatal("Found was false. Err should have been nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionValueExtractWrongType(t *testing.T) {
|
||||
|
||||
t.Log("ensure that error is returned when value if of wrong type")
|
||||
|
||||
optval := &OptionValue{value: "wrong type: a string", found: true}
|
||||
_, _, err := optval.Bool()
|
||||
if err == nil {
|
||||
t.Fatal("No error returned. Failure.")
|
||||
}
|
||||
|
||||
optval = &OptionValue{value: "wrong type: a string", found: true}
|
||||
_, _, err = optval.Int()
|
||||
if err == nil {
|
||||
t.Fatal("No error returned. Failure.")
|
||||
}
|
||||
}
|
@ -3,41 +3,87 @@ package commands
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/jbenet/go-ipfs/config"
|
||||
"github.com/jbenet/go-ipfs/core"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
type optMap map[string]interface{}
|
||||
|
||||
type Context struct {
|
||||
ConfigRoot string
|
||||
Config *config.Config
|
||||
Node *core.IpfsNode
|
||||
|
||||
config *config.Config
|
||||
LoadConfig func(path string) (*config.Config, error)
|
||||
|
||||
node *core.IpfsNode
|
||||
ConstructNode func() (*core.IpfsNode, error)
|
||||
}
|
||||
|
||||
// GetConfig returns the config of the current Command exection
|
||||
// context. It may load it with the providied function.
|
||||
func (c *Context) GetConfig() (*config.Config, error) {
|
||||
var err error
|
||||
if c.config == nil {
|
||||
if c.LoadConfig == nil {
|
||||
return nil, errors.New("nil LoadConfig function")
|
||||
}
|
||||
c.config, err = c.LoadConfig(c.ConfigRoot)
|
||||
}
|
||||
return c.config, err
|
||||
}
|
||||
|
||||
// GetNode returns the node of the current Command exection
|
||||
// context. It may construct it with the providied function.
|
||||
func (c *Context) GetNode() (*core.IpfsNode, error) {
|
||||
var err error
|
||||
if c.node == nil {
|
||||
if c.ConstructNode == nil {
|
||||
return nil, errors.New("nil ConstructNode function")
|
||||
}
|
||||
c.node, err = c.ConstructNode()
|
||||
}
|
||||
return c.node, err
|
||||
}
|
||||
|
||||
// NodeWithoutConstructing returns the underlying node variable
|
||||
// so that clients may close it.
|
||||
func (c *Context) NodeWithoutConstructing() *core.IpfsNode {
|
||||
return c.node
|
||||
}
|
||||
|
||||
// Request represents a call to a command from a consumer
|
||||
type Request interface {
|
||||
Path() []string
|
||||
Option(name string) (interface{}, bool)
|
||||
Options() map[string]interface{}
|
||||
Option(name string) *OptionValue
|
||||
Options() optMap
|
||||
SetOption(name string, val interface{})
|
||||
|
||||
// Arguments() returns user provided arguments as declared on the Command.
|
||||
//
|
||||
// NB: `io.Reader`s returned by Arguments() are owned by the library.
|
||||
// Readers are not guaranteed to remain open after the Command's Run
|
||||
// function returns.
|
||||
Arguments() []interface{} // TODO: make argument value type instead of using interface{}
|
||||
Context() *Context
|
||||
SetContext(Context)
|
||||
Command() *Command
|
||||
Cleanup() error
|
||||
|
||||
ConvertOptions(options map[string]Option) error
|
||||
ConvertOptions() error
|
||||
}
|
||||
|
||||
type request struct {
|
||||
path []string
|
||||
options optMap
|
||||
arguments []interface{}
|
||||
cmd *Command
|
||||
ctx Context
|
||||
path []string
|
||||
options optMap
|
||||
arguments []interface{}
|
||||
cmd *Command
|
||||
ctx Context
|
||||
optionDefs map[string]Option
|
||||
}
|
||||
|
||||
// Path returns the command path of this request
|
||||
@ -46,13 +92,34 @@ func (r *request) Path() []string {
|
||||
}
|
||||
|
||||
// Option returns the value of the option for given name.
|
||||
func (r *request) Option(name string) (interface{}, bool) {
|
||||
val, err := r.options[name]
|
||||
return val, err
|
||||
func (r *request) Option(name string) *OptionValue {
|
||||
val, found := r.options[name]
|
||||
if found {
|
||||
return &OptionValue{val, found}
|
||||
}
|
||||
|
||||
// if a value isn't defined for that name, we will try to look it up by its aliases
|
||||
|
||||
// find the option with the specified name
|
||||
option, found := r.optionDefs[name]
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
// try all the possible names, break if we find a value
|
||||
for _, n := range option.Names {
|
||||
val, found = r.options[n]
|
||||
if found {
|
||||
return &OptionValue{val, found}
|
||||
}
|
||||
}
|
||||
|
||||
// MAYBE_TODO: use default value instead of nil
|
||||
return &OptionValue{nil, false}
|
||||
}
|
||||
|
||||
// Options returns a copy of the option map
|
||||
func (r *request) Options() map[string]interface{} {
|
||||
func (r *request) Options() optMap {
|
||||
output := make(optMap)
|
||||
for k, v := range r.options {
|
||||
output[k] = v
|
||||
@ -62,6 +129,21 @@ func (r *request) Options() map[string]interface{} {
|
||||
|
||||
// SetOption sets the value of the option for given name.
|
||||
func (r *request) SetOption(name string, val interface{}) {
|
||||
// find the option with the specified name
|
||||
option, found := r.optionDefs[name]
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
// try all the possible names, if we already have a value then set over it
|
||||
for _, n := range option.Names {
|
||||
_, found := r.options[n]
|
||||
if found {
|
||||
r.options[n] = val
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
r.options[name] = val
|
||||
}
|
||||
|
||||
@ -82,6 +164,20 @@ func (r *request) Command() *Command {
|
||||
return r.cmd
|
||||
}
|
||||
|
||||
func (r *request) Cleanup() error {
|
||||
for _, arg := range r.arguments {
|
||||
closer, ok := arg.(io.Closer)
|
||||
if ok {
|
||||
err := closer.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type converter func(string) (interface{}, error)
|
||||
|
||||
var converters = map[reflect.Kind]converter{
|
||||
@ -92,48 +188,52 @@ var converters = map[reflect.Kind]converter{
|
||||
return strconv.ParseBool(v)
|
||||
},
|
||||
Int: func(v string) (interface{}, error) {
|
||||
return strconv.ParseInt(v, 0, 32)
|
||||
val, err := strconv.ParseInt(v, 0, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return int(val), err
|
||||
},
|
||||
Uint: func(v string) (interface{}, error) {
|
||||
return strconv.ParseInt(v, 0, 32)
|
||||
val, err := strconv.ParseUint(v, 0, 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return int(val), err
|
||||
},
|
||||
Float: func(v string) (interface{}, error) {
|
||||
return strconv.ParseFloat(v, 64)
|
||||
},
|
||||
}
|
||||
|
||||
func (r *request) ConvertOptions(options map[string]Option) error {
|
||||
converted := make(map[string]interface{})
|
||||
|
||||
func (r *request) ConvertOptions() error {
|
||||
for k, v := range r.options {
|
||||
opt, ok := options[k]
|
||||
opt, ok := r.optionDefs[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
kind := reflect.TypeOf(v).Kind()
|
||||
var value interface{}
|
||||
|
||||
if kind != opt.Type {
|
||||
if kind == String {
|
||||
convert := converters[opt.Type]
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return errors.New("cast error")
|
||||
return u.ErrCast()
|
||||
}
|
||||
val, err := convert(str)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not convert string value '%s' to type '%s'",
|
||||
v, opt.Type.String())
|
||||
}
|
||||
value = val
|
||||
r.options[k] = val
|
||||
|
||||
} else {
|
||||
return fmt.Errorf("Option '%s' should be type '%s', but got type '%s'",
|
||||
k, opt.Type.String(), kind.String())
|
||||
}
|
||||
} else {
|
||||
value = v
|
||||
r.options[k] = v
|
||||
}
|
||||
|
||||
for _, name := range opt.Names {
|
||||
@ -141,22 +241,19 @@ func (r *request) ConvertOptions(options map[string]Option) error {
|
||||
return fmt.Errorf("Duplicate command options were provided ('%s' and '%s')",
|
||||
k, name)
|
||||
}
|
||||
|
||||
converted[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
r.options = converted
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewEmptyRequest initializes an empty request
|
||||
func NewEmptyRequest() Request {
|
||||
return NewRequest(nil, nil, nil, nil)
|
||||
return NewRequest(nil, nil, nil, nil, nil)
|
||||
}
|
||||
|
||||
// NewRequest returns a request initialized with given arguments
|
||||
func NewRequest(path []string, opts optMap, args []interface{}, cmd *Command) Request {
|
||||
func NewRequest(path []string, opts optMap, args []interface{}, cmd *Command, optDefs map[string]Option) Request {
|
||||
if path == nil {
|
||||
path = make([]string, 0)
|
||||
}
|
||||
@ -166,5 +263,12 @@ func NewRequest(path []string, opts optMap, args []interface{}, cmd *Command) Re
|
||||
if args == nil {
|
||||
args = make([]interface{}, 0)
|
||||
}
|
||||
return &request{path, opts, args, cmd, Context{}}
|
||||
if optDefs == nil {
|
||||
optDefs = make(map[string]Option)
|
||||
}
|
||||
|
||||
req := &request{path, opts, args, cmd, Context{}, optDefs}
|
||||
req.ConvertOptions()
|
||||
|
||||
return req
|
||||
}
|
||||
|
@ -40,12 +40,12 @@ const (
|
||||
// TODO: support more encoding types
|
||||
)
|
||||
|
||||
var marshallers = map[EncodingType]Marshaller{
|
||||
var marshallers = map[EncodingType]Marshaler{
|
||||
JSON: func(res Response) ([]byte, error) {
|
||||
if res.Error() != nil {
|
||||
return json.Marshal(res.Error())
|
||||
return json.MarshalIndent(res.Error(), "", " ")
|
||||
}
|
||||
return json.Marshal(res.Output())
|
||||
return json.MarshalIndent(res.Output(), "", " ")
|
||||
},
|
||||
XML: func(res Response) ([]byte, error) {
|
||||
if res.Error() != nil {
|
||||
@ -69,7 +69,7 @@ type Response interface {
|
||||
Output() interface{}
|
||||
|
||||
// Marshal marshals out the response into a buffer. It uses the EncodingType
|
||||
// on the Request to chose a Marshaller (Codec).
|
||||
// on the Request to chose a Marshaler (Codec).
|
||||
Marshal() ([]byte, error)
|
||||
|
||||
// Gets a io.Reader that reads the marshalled output
|
||||
@ -108,18 +108,26 @@ func (r *response) Marshal() ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
enc, found := r.req.Option(EncShort)
|
||||
encStr, ok := enc.(string)
|
||||
if !found || !ok || encStr == "" {
|
||||
enc, found, err := r.req.Option(EncShort).String()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("No encoding type was specified")
|
||||
}
|
||||
encType := EncodingType(strings.ToLower(encStr))
|
||||
encType := EncodingType(strings.ToLower(enc))
|
||||
|
||||
var marshaller Marshaller
|
||||
if r.req.Command() != nil && r.req.Command().Marshallers != nil {
|
||||
marshaller = r.req.Command().Marshallers[encType]
|
||||
// Special case: if text encoding and an error, just print it out.
|
||||
if encType == Text && r.Error() != nil {
|
||||
return []byte(r.Error().Error()), nil
|
||||
}
|
||||
|
||||
var marshaller Marshaler
|
||||
if r.req.Command() != nil && r.req.Command().Marshalers != nil {
|
||||
marshaller = r.req.Command().Marshalers[encType]
|
||||
}
|
||||
if marshaller == nil {
|
||||
var ok bool
|
||||
marshaller, ok = marshallers[encType]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("No marshaller found for encoding type '%s'", enc)
|
||||
|
@ -2,6 +2,7 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -11,42 +12,45 @@ type TestOutput struct {
|
||||
}
|
||||
|
||||
func TestMarshalling(t *testing.T) {
|
||||
req := NewEmptyRequest()
|
||||
cmd := &Command{}
|
||||
opts, _ := cmd.GetOptions(nil)
|
||||
|
||||
req := NewRequest(nil, nil, nil, nil, opts)
|
||||
|
||||
res := NewResponse(req)
|
||||
res.SetOutput(TestOutput{"beep", "boop", 1337})
|
||||
|
||||
// get command global options so we can set the encoding option
|
||||
cmd := Command{}
|
||||
options, err := cmd.GetOptions(nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = res.Marshal()
|
||||
_, err := res.Marshal()
|
||||
if err == nil {
|
||||
t.Error("Should have failed (no encoding type specified in request)")
|
||||
}
|
||||
|
||||
req.SetOption(EncShort, JSON)
|
||||
req.ConvertOptions(options)
|
||||
|
||||
bytes, err := res.Marshal()
|
||||
if err != nil {
|
||||
t.Error(err, "Should have passed")
|
||||
}
|
||||
output := string(bytes)
|
||||
if output != "{\"Foo\":\"beep\",\"Bar\":\"boop\",\"Baz\":1337}" {
|
||||
if removeWhitespace(output) != "{\"Foo\":\"beep\",\"Bar\":\"boop\",\"Baz\":1337}" {
|
||||
t.Error("Incorrect JSON output")
|
||||
}
|
||||
|
||||
res.SetError(fmt.Errorf("You broke something!"), ErrClient)
|
||||
res.SetError(fmt.Errorf("Oops!"), ErrClient)
|
||||
bytes, err = res.Marshal()
|
||||
if err != nil {
|
||||
t.Error("Should have passed")
|
||||
}
|
||||
output = string(bytes)
|
||||
if output != "{\"Message\":\"You broke something!\",\"Code\":1}" {
|
||||
fmt.Println(removeWhitespace(output))
|
||||
if removeWhitespace(output) != "{\"Message\":\"Oops!\",\"Code\":1}" {
|
||||
t.Error("Incorrect JSON output")
|
||||
}
|
||||
}
|
||||
|
||||
func removeWhitespace(input string) string {
|
||||
input = strings.Replace(input, " ", "", -1)
|
||||
input = strings.Replace(input, "\t", "", -1)
|
||||
input = strings.Replace(input, "\n", "", -1)
|
||||
return strings.Replace(input, "\r", "", -1)
|
||||
}
|
||||
|
@ -53,10 +53,25 @@ func WriteFile(filename string, buf []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// HumanOutput gets a config value ready for printing
|
||||
func HumanOutput(value interface{}) ([]byte, error) {
|
||||
s, ok := value.(string)
|
||||
if ok {
|
||||
return []byte(strings.Trim(s, "\n")), nil
|
||||
}
|
||||
return Marshal(value)
|
||||
}
|
||||
|
||||
// Marshal configuration with JSON
|
||||
func Marshal(value interface{}) ([]byte, error) {
|
||||
// need to prettyprint, hence MarshalIndent, instead of Encoder
|
||||
return json.MarshalIndent(value, "", " ")
|
||||
}
|
||||
|
||||
// Encode configuration with JSON
|
||||
func Encode(w io.Writer, value interface{}) error {
|
||||
// need to prettyprint, hence MarshalIndent, instead of Encoder
|
||||
buf, err := json.MarshalIndent(value, "", " ")
|
||||
buf, err := Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// CurrentVersionNumber is the current application's version literal
|
||||
const CurrentVersionNumber = "0.1.6"
|
||||
const CurrentVersionNumber = "0.1.7"
|
||||
|
||||
// Version regulates checking if the most recent version is run
|
||||
type Version struct {
|
||||
|
228
core/commands2/add.go
Normal file
228
core/commands2/add.go
Normal file
@ -0,0 +1,228 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
core "github.com/jbenet/go-ipfs/core"
|
||||
internal "github.com/jbenet/go-ipfs/core/commands2/internal"
|
||||
importer "github.com/jbenet/go-ipfs/importer"
|
||||
"github.com/jbenet/go-ipfs/importer/chunk"
|
||||
dag "github.com/jbenet/go-ipfs/merkledag"
|
||||
pinning "github.com/jbenet/go-ipfs/pin"
|
||||
ft "github.com/jbenet/go-ipfs/unixfs"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
// Error indicating the max depth has been exceded.
|
||||
var ErrDepthLimitExceeded = fmt.Errorf("depth limit exceeded")
|
||||
|
||||
type AddOutput struct {
|
||||
Objects []*Object
|
||||
Names []string
|
||||
}
|
||||
|
||||
var addCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Add an object to ipfs.",
|
||||
ShortDescription: `
|
||||
Adds contents of <path> to ipfs. Use -r to add directories.
|
||||
Note that directories are added recursively, to form the ipfs
|
||||
MerkleDAG. A smarter partial add with a staging area (like git)
|
||||
remains to be implemented.
|
||||
`,
|
||||
},
|
||||
|
||||
Options: []cmds.Option{
|
||||
cmds.BoolOption("recursive", "r", "Must be specified when adding directories"),
|
||||
},
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("path", true, true, "The path to a file to be added to IPFS"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
added := &AddOutput{}
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recursive, _, err := req.Option("r").Bool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// THIS IS A HORRIBLE HACK -- FIXME!!!
|
||||
// see https://github.com/jbenet/go-ipfs/issues/309
|
||||
|
||||
// returns the last one
|
||||
addDagnode := func(name string, dn *dag.Node) error {
|
||||
o, err := getOutput(dn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
added.Objects = append(added.Objects, o)
|
||||
added.Names = append(added.Names, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
addFile := func(name string) (*dag.Node, error) {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
dns, err := add(n, []io.Reader{f})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("adding file: %s", name)
|
||||
if err := addDagnode(name, dns[len(dns)-1]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dns[len(dns)-1], nil // last dag node is the file.
|
||||
}
|
||||
|
||||
var addPath func(name string) (*dag.Node, error)
|
||||
addDir := func(name string) (*dag.Node, error) {
|
||||
tree := &dag.Node{Data: ft.FolderPBData()}
|
||||
|
||||
entries, err := ioutil.ReadDir(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// construct nodes for containing files.
|
||||
for _, e := range entries {
|
||||
fp := filepath.Join(name, e.Name())
|
||||
nd, err := addPath(fp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = tree.AddNodeLink(e.Name(), nd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("adding dir: %s", name)
|
||||
if err := addNode(n, tree); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := addDagnode(name, tree); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
addPath = func(fpath string) (*dag.Node, error) {
|
||||
fi, err := os.Stat(fpath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
if !recursive {
|
||||
return nil, errors.New("use -r to recursively add directories")
|
||||
}
|
||||
return addDir(fpath)
|
||||
}
|
||||
return addFile(fpath)
|
||||
}
|
||||
|
||||
paths, err := internal.CastToStrings(req.Arguments())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, f := range paths {
|
||||
if _, err := addPath(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return added, nil
|
||||
|
||||
// readers, err := internal.CastToReaders(req.Arguments())
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// dagnodes, err := add(n, readers)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// // TODO: include fs paths in output (will need a way to specify paths in underlying filearg system)
|
||||
// added := make([]*Object, 0, len(req.Arguments()))
|
||||
// for _, dagnode := range dagnodes {
|
||||
// object, err := getOutput(dagnode)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// added = append(added, object)
|
||||
// }
|
||||
//
|
||||
// return &AddOutput{added}, nil
|
||||
},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
val, ok := res.Output().(*AddOutput)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
for i, obj := range val.Objects {
|
||||
buf.Write([]byte(fmt.Sprintf("added %s %s\n", obj.Hash, val.Names[i])))
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
},
|
||||
},
|
||||
Type: &AddOutput{},
|
||||
}
|
||||
|
||||
func add(n *core.IpfsNode, readers []io.Reader) ([]*dag.Node, error) {
|
||||
mp, ok := n.Pinning.(pinning.ManualPinner)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid pinner type! expected manual pinner")
|
||||
}
|
||||
|
||||
dagnodes := make([]*dag.Node, 0)
|
||||
|
||||
// TODO: allow adding directories (will need support for multiple files in filearg system)
|
||||
|
||||
for _, reader := range readers {
|
||||
node, err := importer.BuildDagFromReader(reader, n.DAG, mp, chunk.DefaultSplitter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dagnodes = append(dagnodes, node)
|
||||
}
|
||||
|
||||
return dagnodes, nil
|
||||
}
|
||||
|
||||
func addNode(n *core.IpfsNode, node *dag.Node) error {
|
||||
err := n.DAG.AddRecursive(node) // add the file to the graph + local storage
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = n.Pinning.Pin(node, true) // ensure we keep it
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
132
core/commands2/block.go
Normal file
132
core/commands2/block.go
Normal file
@ -0,0 +1,132 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context"
|
||||
|
||||
mh "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multihash"
|
||||
"github.com/jbenet/go-ipfs/blocks"
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
type Block struct {
|
||||
Key string
|
||||
Length int
|
||||
}
|
||||
|
||||
var blockCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Manipulate raw IPFS blocks",
|
||||
ShortDescription: `
|
||||
'ipfs block' is a plumbing command used to manipulate raw ipfs blocks.
|
||||
Reads from stdin or writes to stdout, and <key> is a base58 encoded
|
||||
multihash.
|
||||
`,
|
||||
},
|
||||
|
||||
Subcommands: map[string]*cmds.Command{
|
||||
"get": blockGetCmd,
|
||||
"put": blockPutCmd,
|
||||
},
|
||||
}
|
||||
|
||||
var blockGetCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Get a raw IPFS block",
|
||||
ShortDescription: `
|
||||
'ipfs block get' is a plumbing command for retreiving raw ipfs blocks.
|
||||
It outputs to stdout, and <key> is a base58 encoded multihash.
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("key", true, false, "The base58 multihash of an existing block to get"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, ok := req.Arguments()[0].(string)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
if !u.IsValidHash(key) {
|
||||
return nil, cmds.Error{"Not a valid hash", cmds.ErrClient}
|
||||
}
|
||||
|
||||
h, err := mh.FromB58String(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k := u.Key(h)
|
||||
ctx, _ := context.WithTimeout(context.TODO(), time.Second*5)
|
||||
b, err := n.Blocks.GetBlock(ctx, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("BlockGet key: '%q'", b.Key())
|
||||
|
||||
return bytes.NewReader(b.Data), nil
|
||||
},
|
||||
}
|
||||
|
||||
var blockPutCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Stores input as an IPFS block",
|
||||
ShortDescription: `
|
||||
ipfs block put is a plumbing command for storing raw ipfs blocks.
|
||||
It reads from stdin, and <key> is a base58 encoded multihash.
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.FileArg("data", true, false, "The data to be stored as an IPFS block").EnableStdin(),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
in, ok := req.Arguments()[0].(io.Reader)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b := blocks.NewBlock(data)
|
||||
log.Debugf("BlockPut key: '%q'", b.Key())
|
||||
|
||||
k, err := n.Blocks.AddBlock(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Block{
|
||||
Key: k.String(),
|
||||
Length: len(data),
|
||||
}, nil
|
||||
},
|
||||
Type: &Block{},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
block := res.Output().(*Block)
|
||||
s := fmt.Sprintf("Block added (%v bytes): %s\n", block.Length, block.Key)
|
||||
return []byte(s), nil
|
||||
},
|
||||
},
|
||||
}
|
296
core/commands2/bootstrap.go
Normal file
296
core/commands2/bootstrap.go
Normal file
@ -0,0 +1,296 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
ma "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr"
|
||||
mh "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multihash"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
config "github.com/jbenet/go-ipfs/config"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
type BootstrapOutput struct {
|
||||
Peers []*config.BootstrapPeer
|
||||
}
|
||||
|
||||
var peerOptionDesc = "A peer to add to the bootstrap list (in the format '<multiaddr>/<peerID>')"
|
||||
|
||||
var bootstrapCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Show or edit the list of bootstrap peers",
|
||||
Synopsis: `
|
||||
ipfs bootstrap list - Show peers in the bootstrap list
|
||||
ipfs bootstrap add <peer>... - Add peers to the bootstrap list
|
||||
ipfs bootstrap remove <peer>... - Removes peers from the bootstrap list
|
||||
`,
|
||||
ShortDescription: `
|
||||
Running 'ipfs bootstrap' with no arguments will run 'ipfs bootstrap list'.
|
||||
` + bootstrapSecurityWarning,
|
||||
},
|
||||
|
||||
Run: bootstrapListCmd.Run,
|
||||
Marshalers: bootstrapListCmd.Marshalers,
|
||||
Type: bootstrapListCmd.Type,
|
||||
|
||||
Subcommands: map[string]*cmds.Command{
|
||||
"list": bootstrapListCmd,
|
||||
"add": bootstrapAddCmd,
|
||||
"rm": bootstrapRemoveCmd,
|
||||
},
|
||||
}
|
||||
|
||||
var bootstrapAddCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Add peers to the bootstrap list",
|
||||
ShortDescription: `Outputs a list of peers that were added (that weren't already
|
||||
in the bootstrap list).
|
||||
` + bootstrapSecurityWarning,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("peer", true, true, peerOptionDesc),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
input, err := bootstrapInputToPeers(req.Arguments())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filename, err := config.Filename(req.Context().ConfigRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err := req.Context().GetConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
added, err := bootstrapAdd(filename, cfg, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BootstrapOutput{added}, nil
|
||||
},
|
||||
Type: &BootstrapOutput{},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
v, ok := res.Output().(*BootstrapOutput)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := bootstrapWritePeers(&buf, "added ", v.Peers)
|
||||
return buf.Bytes(), err
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var bootstrapRemoveCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Removes peers from the bootstrap list",
|
||||
ShortDescription: `Outputs the list of peers that were removed.
|
||||
` + bootstrapSecurityWarning,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("peer", true, true, peerOptionDesc),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
input, err := bootstrapInputToPeers(req.Arguments())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filename, err := config.Filename(req.Context().ConfigRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg, err := req.Context().GetConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
removed, err := bootstrapRemove(filename, cfg, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BootstrapOutput{removed}, nil
|
||||
},
|
||||
Type: &BootstrapOutput{},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
v, ok := res.Output().(*BootstrapOutput)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := bootstrapWritePeers(&buf, "removed ", v.Peers)
|
||||
return buf.Bytes(), err
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var bootstrapListCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Show peers in the bootstrap list",
|
||||
ShortDescription: "Peers are output in the format '<multiaddr>/<peerID>'.",
|
||||
},
|
||||
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
cfg, err := req.Context().GetConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
peers := cfg.Bootstrap
|
||||
return &BootstrapOutput{peers}, nil
|
||||
},
|
||||
Type: &BootstrapOutput{},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: bootstrapMarshaler,
|
||||
},
|
||||
}
|
||||
|
||||
func bootstrapMarshaler(res cmds.Response) ([]byte, error) {
|
||||
v, ok := res.Output().(*BootstrapOutput)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := bootstrapWritePeers(&buf, "", v.Peers)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
|
||||
func bootstrapWritePeers(w io.Writer, prefix string, peers []*config.BootstrapPeer) error {
|
||||
|
||||
for _, peer := range peers {
|
||||
s := prefix + peer.Address + "/" + peer.PeerID + "\n"
|
||||
_, err := w.Write([]byte(s))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func bootstrapInputToPeers(input []interface{}) ([]*config.BootstrapPeer, error) {
|
||||
inputAddrs := make([]string, len(input))
|
||||
for i, v := range input {
|
||||
addr, ok := v.(string)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
inputAddrs[i] = addr
|
||||
}
|
||||
|
||||
split := func(addr string) (string, string) {
|
||||
idx := strings.LastIndex(addr, "/")
|
||||
if idx == -1 {
|
||||
return "", addr
|
||||
}
|
||||
return addr[:idx], addr[idx+1:]
|
||||
}
|
||||
|
||||
peers := []*config.BootstrapPeer{}
|
||||
for _, addr := range inputAddrs {
|
||||
addrS, peeridS := split(addr)
|
||||
|
||||
// make sure addrS parses as a multiaddr.
|
||||
if len(addrS) > 0 {
|
||||
maddr, err := ma.NewMultiaddr(addrS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addrS = maddr.String()
|
||||
}
|
||||
|
||||
// make sure idS parses as a peer.ID
|
||||
_, err := mh.FromB58String(peeridS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// construct config entry
|
||||
peers = append(peers, &config.BootstrapPeer{
|
||||
Address: addrS,
|
||||
PeerID: peeridS,
|
||||
})
|
||||
}
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
func bootstrapAdd(filename string, cfg *config.Config, peers []*config.BootstrapPeer) ([]*config.BootstrapPeer, error) {
|
||||
added := make([]*config.BootstrapPeer, 0, len(peers))
|
||||
|
||||
for _, peer := range peers {
|
||||
duplicate := false
|
||||
for _, peer2 := range cfg.Bootstrap {
|
||||
if peer.Address == peer2.Address && peer.PeerID == peer2.PeerID {
|
||||
duplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !duplicate {
|
||||
cfg.Bootstrap = append(cfg.Bootstrap, peer)
|
||||
added = append(added, peer)
|
||||
}
|
||||
}
|
||||
|
||||
err := config.WriteConfigFile(filename, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return added, nil
|
||||
}
|
||||
|
||||
func bootstrapRemove(filename string, cfg *config.Config, toRemove []*config.BootstrapPeer) ([]*config.BootstrapPeer, error) {
|
||||
removed := make([]*config.BootstrapPeer, 0, len(toRemove))
|
||||
keep := make([]*config.BootstrapPeer, 0, len(cfg.Bootstrap))
|
||||
|
||||
for _, peer := range cfg.Bootstrap {
|
||||
found := false
|
||||
for _, peer2 := range toRemove {
|
||||
if peer.Address == peer2.Address && peer.PeerID == peer2.PeerID {
|
||||
found = true
|
||||
removed = append(removed, peer)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
keep = append(keep, peer)
|
||||
}
|
||||
}
|
||||
cfg.Bootstrap = keep
|
||||
|
||||
err := config.WriteConfigFile(filename, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
const bootstrapSecurityWarning = `
|
||||
SECURITY WARNING:
|
||||
|
||||
The bootstrap command manipulates the "bootstrap list", which contains
|
||||
the addresses of bootstrap nodes. These are the *trusted peers* from
|
||||
which to learn about other peers in the network. Only edit this list
|
||||
if you understand the risks of adding or removing nodes from this list.
|
||||
|
||||
`
|
61
core/commands2/cat.go
Normal file
61
core/commands2/cat.go
Normal file
@ -0,0 +1,61 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
core "github.com/jbenet/go-ipfs/core"
|
||||
"github.com/jbenet/go-ipfs/core/commands2/internal"
|
||||
uio "github.com/jbenet/go-ipfs/unixfs/io"
|
||||
)
|
||||
|
||||
var catCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Show IPFS object data",
|
||||
ShortDescription: `
|
||||
Retrieves the object named by <ipfs-path> and outputs the data
|
||||
it contains.
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to be outputted"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
node, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
readers := make([]io.Reader, 0, len(req.Arguments()))
|
||||
|
||||
paths, err := internal.CastToStrings(req.Arguments())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
readers, err = cat(node, paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader := io.MultiReader(readers...)
|
||||
return reader, nil
|
||||
},
|
||||
}
|
||||
|
||||
func cat(node *core.IpfsNode, paths []string) ([]io.Reader, error) {
|
||||
readers := make([]io.Reader, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
dagnode, err := node.Resolver.ResolvePath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
read, err := uio.NewDagReader(dagnode, node.DAG)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
readers = append(readers, read)
|
||||
}
|
||||
return readers, nil
|
||||
}
|
71
core/commands2/commands.go
Normal file
71
core/commands2/commands.go
Normal file
@ -0,0 +1,71 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
Name string
|
||||
Subcommands []Command
|
||||
}
|
||||
|
||||
// CommandsCmd takes in a root command,
|
||||
// and returns a command that lists the subcommands in that root
|
||||
func CommandsCmd(root *cmds.Command) *cmds.Command {
|
||||
return &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "List all available commands.",
|
||||
ShortDescription: `Lists all available commands (and subcommands) and exits.`,
|
||||
},
|
||||
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
root := cmd2outputCmd("ipfs", root)
|
||||
return &root, nil
|
||||
},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
v := res.Output().(*Command)
|
||||
var buf bytes.Buffer
|
||||
for _, s := range cmdPathStrings(v) {
|
||||
buf.Write([]byte(s + "\n"))
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
},
|
||||
},
|
||||
Type: &Command{},
|
||||
}
|
||||
}
|
||||
|
||||
func cmd2outputCmd(name string, cmd *cmds.Command) Command {
|
||||
output := Command{
|
||||
Name: name,
|
||||
Subcommands: make([]Command, len(cmd.Subcommands)),
|
||||
}
|
||||
|
||||
i := 0
|
||||
for name, sub := range cmd.Subcommands {
|
||||
output.Subcommands[i] = cmd2outputCmd(name, sub)
|
||||
i++
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func cmdPathStrings(cmd *Command) []string {
|
||||
var cmds []string
|
||||
|
||||
var recurse func(prefix string, cmd *Command)
|
||||
recurse = func(prefix string, cmd *Command) {
|
||||
cmds = append(cmds, prefix+cmd.Name)
|
||||
for _, sub := range cmd.Subcommands {
|
||||
recurse(prefix+cmd.Name+" ", &sub)
|
||||
}
|
||||
}
|
||||
|
||||
recurse("", cmd)
|
||||
sort.Sort(sort.StringSlice(cmds))
|
||||
return cmds
|
||||
}
|
193
core/commands2/config.go
Normal file
193
core/commands2/config.go
Normal file
@ -0,0 +1,193 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
config "github.com/jbenet/go-ipfs/config"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
type ConfigField struct {
|
||||
Key string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
var configCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "get and set IPFS config values",
|
||||
Synopsis: `
|
||||
ipfs config <key> - Get value of <key>
|
||||
ipfs config <key> <value> - Set value of <key> to <value>
|
||||
ipfs config show - Show config file
|
||||
ipfs config edit - Edit config file in $EDITOR
|
||||
`,
|
||||
ShortDescription: `
|
||||
ipfs config controls configuration variables. It works like 'git config'.
|
||||
The configuration values are stored in a config file inside your IPFS
|
||||
repository.`,
|
||||
LongDescription: `
|
||||
ipfs config controls configuration variables. It works
|
||||
much like 'git config'. The configuration values are stored in a config
|
||||
file inside your IPFS repository.
|
||||
|
||||
EXAMPLES:
|
||||
|
||||
Get the value of the 'datastore.path' key:
|
||||
|
||||
ipfs config datastore.path
|
||||
|
||||
Set the value of the 'datastore.path' key:
|
||||
|
||||
ipfs config datastore.path ~/.go-ipfs/datastore
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("key", true, false, "The key of the config entry (e.g. \"Addresses.API\")"),
|
||||
cmds.StringArg("value", false, false, "The value to set the config entry to"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
args := req.Arguments()
|
||||
|
||||
key, ok := args[0].(string)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
filename, err := config.Filename(req.Context().ConfigRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var value string
|
||||
if len(args) == 2 {
|
||||
var ok bool
|
||||
value, ok = args[1].(string)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
return setConfig(filename, key, value)
|
||||
|
||||
} else {
|
||||
return getConfig(filename, key)
|
||||
}
|
||||
},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
if len(res.Request().Arguments()) == 2 {
|
||||
return nil, nil // dont output anything
|
||||
}
|
||||
|
||||
v := res.Output()
|
||||
if v == nil {
|
||||
k := res.Request().Arguments()[0]
|
||||
return nil, fmt.Errorf("config does not contain key: %s", k)
|
||||
}
|
||||
vf, ok := v.(*ConfigField)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
buf, err := config.HumanOutput(vf.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = append(buf, byte('\n'))
|
||||
return buf, nil
|
||||
},
|
||||
},
|
||||
Type: &ConfigField{},
|
||||
Subcommands: map[string]*cmds.Command{
|
||||
"show": configShowCmd,
|
||||
"edit": configEditCmd,
|
||||
},
|
||||
}
|
||||
|
||||
var configShowCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Outputs the content of the config file",
|
||||
ShortDescription: `
|
||||
WARNING: Your private key is stored in the config file, and it will be
|
||||
included in the output of this command.
|
||||
`,
|
||||
},
|
||||
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
filename, err := config.Filename(req.Context().ConfigRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return showConfig(filename)
|
||||
},
|
||||
}
|
||||
|
||||
var configEditCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Opens the config file for editing in $EDITOR",
|
||||
ShortDescription: `
|
||||
To use 'ipfs config edit', you must have the $EDITOR environment
|
||||
variable set to your preferred text editor.
|
||||
`,
|
||||
},
|
||||
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
filename, err := config.Filename(req.Context().ConfigRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, editConfig(filename)
|
||||
},
|
||||
}
|
||||
|
||||
func getConfig(filename string, key string) (*ConfigField, error) {
|
||||
value, err := config.ReadConfigKey(filename, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to get config value: %s", err)
|
||||
}
|
||||
|
||||
return &ConfigField{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func setConfig(filename string, key, value string) (*ConfigField, error) {
|
||||
err := config.WriteConfigKey(filename, key, value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to set config value: %s", err)
|
||||
}
|
||||
|
||||
return getConfig(filename, key)
|
||||
}
|
||||
|
||||
func showConfig(filename string) (io.Reader, error) {
|
||||
// TODO maybe we should omit privkey so we don't accidentally leak it?
|
||||
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bytes.NewReader(data), nil
|
||||
}
|
||||
|
||||
func editConfig(filename string) error {
|
||||
editor := os.Getenv("EDITOR")
|
||||
if editor == "" {
|
||||
return errors.New("ENV variable $EDITOR not set")
|
||||
}
|
||||
|
||||
cmd := exec.Command("sh", "-c", editor+" "+filename)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
129
core/commands2/diag.go
Normal file
129
core/commands2/diag.go
Normal file
@ -0,0 +1,129 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
util "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
type DiagnosticConnection struct {
|
||||
ID string
|
||||
// TODO use milliseconds or microseconds for human readability
|
||||
NanosecondsLatency uint64
|
||||
}
|
||||
|
||||
type DiagnosticPeer struct {
|
||||
ID string
|
||||
UptimeSeconds uint64
|
||||
BandwidthBytesIn uint64
|
||||
BandwidthBytesOut uint64
|
||||
Connections []DiagnosticConnection
|
||||
}
|
||||
|
||||
type DiagnosticOutput struct {
|
||||
Peers []DiagnosticPeer
|
||||
}
|
||||
|
||||
var DiagCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Generates diagnostic reports",
|
||||
},
|
||||
|
||||
Subcommands: map[string]*cmds.Command{
|
||||
"net": diagNetCmd,
|
||||
},
|
||||
}
|
||||
|
||||
var diagNetCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Generates a network diagnostics report",
|
||||
ShortDescription: `
|
||||
Sends out a message to each node in the network recursively
|
||||
requesting a listing of data about them including number of
|
||||
connected peers and latencies between them.
|
||||
`,
|
||||
},
|
||||
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !n.OnlineMode() {
|
||||
return nil, errNotOnline
|
||||
}
|
||||
|
||||
info, err := n.Diagnostics.GetDiagnostic(time.Second * 20)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output := make([]DiagnosticPeer, len(info))
|
||||
for i, peer := range info {
|
||||
connections := make([]DiagnosticConnection, len(peer.Connections))
|
||||
for j, conn := range peer.Connections {
|
||||
connections[j] = DiagnosticConnection{
|
||||
ID: conn.ID,
|
||||
NanosecondsLatency: uint64(conn.Latency.Nanoseconds()),
|
||||
}
|
||||
}
|
||||
|
||||
output[i] = DiagnosticPeer{
|
||||
ID: peer.ID,
|
||||
UptimeSeconds: uint64(peer.LifeSpan.Seconds()),
|
||||
BandwidthBytesIn: peer.BwIn,
|
||||
BandwidthBytesOut: peer.BwOut,
|
||||
Connections: connections,
|
||||
}
|
||||
}
|
||||
|
||||
return &DiagnosticOutput{output}, nil
|
||||
},
|
||||
Type: &DiagnosticOutput{},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(r cmds.Response) ([]byte, error) {
|
||||
output, ok := r.Output().(*DiagnosticOutput)
|
||||
if !ok {
|
||||
return nil, util.ErrCast()
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := printDiagnostics(&buf, output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func printDiagnostics(out io.Writer, info *DiagnosticOutput) error {
|
||||
|
||||
diagTmpl := `
|
||||
{{ range $peer := .Peers }}
|
||||
ID {{ $peer.ID }}
|
||||
up {{ $peer.UptimeSeconds }} seconds
|
||||
connected to {{ len .Connections }}...
|
||||
{{ range $connection := .Connections }}
|
||||
ID {{ $connection.ID }}
|
||||
latency: {{ $connection.NanosecondsLatency }} ns
|
||||
{{ end }}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
templ, err := template.New("DiagnosticOutput").Parse(diagTmpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = templ.Execute(out, info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
29
core/commands2/diag_test.go
Normal file
29
core/commands2/diag_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPrintDiagnostics(t *testing.T) {
|
||||
output := DiagnosticOutput{
|
||||
Peers: []DiagnosticPeer{
|
||||
DiagnosticPeer{ID: "QmNrjRuUtBNZAigzLRdZGN1YCNUxdF2WY2HnKyEFJqoTeg",
|
||||
UptimeSeconds: 14,
|
||||
Connections: []DiagnosticConnection{
|
||||
DiagnosticConnection{ID: "QmNrjRuUtBNZAigzLRdZGN1YCNUxdF2WY2HnKyEFJqoTeg",
|
||||
NanosecondsLatency: 1347899,
|
||||
},
|
||||
},
|
||||
},
|
||||
DiagnosticPeer{ID: "QmUaUZDp6QWJabBYSKfiNmXLAXD8HNKnWZh9Zoz6Zri9Ti",
|
||||
UptimeSeconds: 14,
|
||||
},
|
||||
},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := printDiagnostics(&buf, &output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(buf.String())
|
||||
}
|
31
core/commands2/internal/slice_util.go
Normal file
31
core/commands2/internal/slice_util.go
Normal file
@ -0,0 +1,31 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
func CastToReaders(slice []interface{}) ([]io.Reader, error) {
|
||||
readers := make([]io.Reader, 0)
|
||||
for _, arg := range slice {
|
||||
reader, ok := arg.(io.Reader)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
readers = append(readers, reader)
|
||||
}
|
||||
return readers, nil
|
||||
}
|
||||
|
||||
func CastToStrings(slice []interface{}) ([]string, error) {
|
||||
strs := make([]string, 0)
|
||||
for _, maybe := range slice {
|
||||
str, ok := maybe.(string)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
strs = append(strs, str)
|
||||
}
|
||||
return strs, nil
|
||||
}
|
56
core/commands2/log.go
Normal file
56
core/commands2/log.go
Normal file
@ -0,0 +1,56 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
// Golang os.Args overrides * and replaces the character argument with
|
||||
// an array which includes every file in the user's CWD. As a
|
||||
// workaround, we use 'all' instead. The util library still uses * so
|
||||
// we convert it at this step.
|
||||
var logAllKeyword = "all"
|
||||
|
||||
var LogCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Change the logging level",
|
||||
ShortDescription: `
|
||||
'ipfs log' is a utility command used to change the logging
|
||||
output of a running daemon.
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
// TODO use a different keyword for 'all' because all can theoretically
|
||||
// clash with a subsystem name
|
||||
cmds.StringArg("subsystem", true, false, fmt.Sprintf("the subsystem logging identifier. Use '%s' for all subsystems.", logAllKeyword)),
|
||||
cmds.StringArg("level", true, false, "one of: debug, info, notice, warning, error, critical"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
|
||||
args := req.Arguments()
|
||||
subsystem, ok1 := args[0].(string)
|
||||
level, ok2 := args[1].(string)
|
||||
if !ok1 || !ok2 {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
if subsystem == logAllKeyword {
|
||||
subsystem = "*"
|
||||
}
|
||||
|
||||
if err := u.SetLogLevel(subsystem, level); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := fmt.Sprintf("Changed log level of '%s' to '%s'", subsystem, level)
|
||||
log.Info(s)
|
||||
return &MessageOutput{s}, nil
|
||||
},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: MessageTextMarshaler,
|
||||
},
|
||||
Type: &MessageOutput{},
|
||||
}
|
102
core/commands2/ls.go
Normal file
102
core/commands2/ls.go
Normal file
@ -0,0 +1,102 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
"github.com/jbenet/go-ipfs/core/commands2/internal"
|
||||
merkledag "github.com/jbenet/go-ipfs/merkledag"
|
||||
)
|
||||
|
||||
type Link struct {
|
||||
Name, Hash string
|
||||
Size uint64
|
||||
}
|
||||
|
||||
type Object struct {
|
||||
Hash string
|
||||
Links []Link
|
||||
}
|
||||
|
||||
type LsOutput struct {
|
||||
Objects []Object
|
||||
}
|
||||
|
||||
var lsCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "List links from an object.",
|
||||
ShortDescription: `
|
||||
Retrieves the object named by <ipfs-path> and displays the links
|
||||
it contains, with the following format:
|
||||
|
||||
<link base58 hash> <link size in bytes> <link name>
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to list links from"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
node, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
paths, err := internal.CastToStrings(req.Arguments())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dagnodes := make([]*merkledag.Node, 0)
|
||||
for _, path := range paths {
|
||||
dagnode, err := node.Resolver.ResolvePath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dagnodes = append(dagnodes, dagnode)
|
||||
}
|
||||
|
||||
output := make([]Object, len(req.Arguments()))
|
||||
for i, dagnode := range dagnodes {
|
||||
output[i] = Object{
|
||||
Hash: paths[i],
|
||||
Links: make([]Link, len(dagnode.Links)),
|
||||
}
|
||||
for j, link := range dagnode.Links {
|
||||
output[i].Links[j] = Link{
|
||||
Name: link.Name,
|
||||
Hash: link.Hash.B58String(),
|
||||
Size: link.Size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &LsOutput{output}, nil
|
||||
},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
s := ""
|
||||
output := res.Output().(*LsOutput).Objects
|
||||
|
||||
for _, object := range output {
|
||||
if len(output) > 1 {
|
||||
s += fmt.Sprintf("%s:\n", object.Hash)
|
||||
}
|
||||
s += marshalLinks(object.Links)
|
||||
if len(output) > 1 {
|
||||
s += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(s), nil
|
||||
},
|
||||
},
|
||||
Type: &LsOutput{},
|
||||
}
|
||||
|
||||
func marshalLinks(links []Link) (s string) {
|
||||
for _, link := range links {
|
||||
s += fmt.Sprintf("%s %v %s\n", link.Hash, link.Size, link.Name)
|
||||
}
|
||||
return s
|
||||
}
|
33
core/commands2/mount_darwin.go
Normal file
33
core/commands2/mount_darwin.go
Normal file
@ -0,0 +1,33 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// this is a hack, but until we need to do it another way, this works.
|
||||
platformFuseChecks = darwinFuseCheckVersion
|
||||
}
|
||||
|
||||
func darwinFuseCheckVersion() error {
|
||||
// on OSX, check FUSE version.
|
||||
if runtime.GOOS != "darwin" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ov, err := syscall.Sysctl("osxfuse.version.number")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(ov, "2.7.") || strings.HasPrefix(ov, "2.8.") {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("osxfuse version %s not supported.\n%s\n%s", ov,
|
||||
"Older versions of osxfuse have kernel panic bugs; please upgrade!",
|
||||
"https://github.com/jbenet/go-ipfs/issues/177")
|
||||
}
|
191
core/commands2/mount_unix.go
Normal file
191
core/commands2/mount_unix.go
Normal file
@ -0,0 +1,191 @@
|
||||
// +build linux darwin freebsd
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
config "github.com/jbenet/go-ipfs/config"
|
||||
core "github.com/jbenet/go-ipfs/core"
|
||||
ipns "github.com/jbenet/go-ipfs/fuse/ipns"
|
||||
rofs "github.com/jbenet/go-ipfs/fuse/readonly"
|
||||
)
|
||||
|
||||
// amount of time to wait for mount errors
|
||||
// TODO is this non-deterministic?
|
||||
const mountTimeout = time.Second
|
||||
|
||||
// fuseNoDirectory used to check the returning fuse error
|
||||
const fuseNoDirectory = "fusermount: failed to access mountpoint"
|
||||
|
||||
var mountCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Mounts IPFS to the filesystem (read-only)",
|
||||
ShortDescription: `
|
||||
Mount ipfs at a read-only mountpoint on the OS (default: /ipfs and /ipns).
|
||||
All ipfs objects will be accessible under that directory. Note that the
|
||||
root will not be listable, as it is virtual. Access known paths directly.
|
||||
|
||||
You may have to create /ipfs and /ipfs before using 'ipfs mount':
|
||||
|
||||
> sudo mkdir /ipfs /ipns
|
||||
> sudo chown ` + "`" + `whoami` + "`" + ` /ipfs /ipns
|
||||
> ipfs mount
|
||||
`,
|
||||
LongDescription: `
|
||||
Mount ipfs at a read-only mountpoint on the OS (default: /ipfs and /ipns).
|
||||
All ipfs objects will be accessible under that directory. Note that the
|
||||
root will not be listable, as it is virtual. Access known paths directly.
|
||||
|
||||
You may have to create /ipfs and /ipfs before using 'ipfs mount':
|
||||
|
||||
> sudo mkdir /ipfs /ipns
|
||||
> sudo chown ` + "`" + `whoami` + "`" + ` /ipfs /ipns
|
||||
> ipfs mount
|
||||
|
||||
EXAMPLE:
|
||||
|
||||
# setup
|
||||
> mkdir foo
|
||||
> echo "baz" > foo/bar
|
||||
> ipfs add -r foo
|
||||
added QmWLdkp93sNxGRjnFHPaYg8tCQ35NBY3XPn6KiETd3Z4WR foo/bar
|
||||
added QmSh5e7S6fdcu75LAbXNZAFY2nGyZUJXyLCJDvn2zRkWyC foo
|
||||
> ipfs ls QmSh5e7S6fdcu75LAbXNZAFY2nGyZUJXyLCJDvn2zRkWyC
|
||||
QmWLdkp93sNxGRjnFHPaYg8tCQ35NBY3XPn6KiETd3Z4WR 12 bar
|
||||
> ipfs cat QmWLdkp93sNxGRjnFHPaYg8tCQ35NBY3XPn6KiETd3Z4WR
|
||||
baz
|
||||
|
||||
# mount
|
||||
> ipfs daemon &
|
||||
> ipfs mount
|
||||
IPFS mounted at: /ipfs
|
||||
IPNS mounted at: /ipns
|
||||
> cd /ipfs/QmSh5e7S6fdcu75LAbXNZAFY2nGyZUJXyLCJDvn2zRkWyC
|
||||
> ls
|
||||
bar
|
||||
> cat bar
|
||||
baz
|
||||
> cat /ipfs/QmSh5e7S6fdcu75LAbXNZAFY2nGyZUJXyLCJDvn2zRkWyC/bar
|
||||
baz
|
||||
> cat /ipfs/QmWLdkp93sNxGRjnFHPaYg8tCQ35NBY3XPn6KiETd3Z4WR
|
||||
baz
|
||||
`,
|
||||
},
|
||||
|
||||
Options: []cmds.Option{
|
||||
// TODO longform
|
||||
cmds.StringOption("f", "The path where IPFS should be mounted"),
|
||||
|
||||
// TODO longform
|
||||
cmds.StringOption("n", "The path where IPNS should be mounted"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
cfg, err := req.Context().GetConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// error if we aren't running node in online mode
|
||||
if node.Network == nil {
|
||||
return nil, errNotOnline
|
||||
}
|
||||
|
||||
if err := platformFuseChecks(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fsdir, found, err := req.Option("f").String()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
fsdir = cfg.Mounts.IPFS // use default value
|
||||
}
|
||||
fsdone := mountIpfs(node, fsdir)
|
||||
|
||||
// get default mount points
|
||||
nsdir, found, err := req.Option("n").String()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
nsdir = cfg.Mounts.IPNS // NB: be sure to not redeclare!
|
||||
}
|
||||
|
||||
nsdone := mountIpns(node, nsdir, fsdir)
|
||||
|
||||
fmtFuseErr := func(err error) error {
|
||||
s := err.Error()
|
||||
if strings.Contains(s, fuseNoDirectory) {
|
||||
s = strings.Replace(s, `fusermount: "fusermount:`, "", -1)
|
||||
s = strings.Replace(s, `\n", exit status 1`, "", -1)
|
||||
return cmds.ClientError(s)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// wait until mounts return an error (or timeout if successful)
|
||||
select {
|
||||
case err := <-fsdone:
|
||||
return nil, fmtFuseErr(err)
|
||||
case err := <-nsdone:
|
||||
return nil, fmtFuseErr(err)
|
||||
|
||||
// mounted successfully, we timed out with no errors
|
||||
case <-time.After(mountTimeout):
|
||||
output := cfg.Mounts
|
||||
return &output, nil
|
||||
}
|
||||
},
|
||||
Type: &config.Mounts{},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
v := res.Output().(*config.Mounts)
|
||||
s := fmt.Sprintf("IPFS mounted at: %s\n", v.IPFS)
|
||||
s += fmt.Sprintf("IPNS mounted at: %s\n", v.IPNS)
|
||||
return []byte(s), nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func mountIpfs(node *core.IpfsNode, fsdir string) <-chan error {
|
||||
done := make(chan error)
|
||||
log.Info("Mounting IPFS at ", fsdir)
|
||||
|
||||
go func() {
|
||||
err := rofs.Mount(node, fsdir)
|
||||
done <- err
|
||||
close(done)
|
||||
}()
|
||||
|
||||
return done
|
||||
}
|
||||
|
||||
func mountIpns(node *core.IpfsNode, nsdir, fsdir string) <-chan error {
|
||||
if nsdir == "" {
|
||||
return nil
|
||||
}
|
||||
done := make(chan error)
|
||||
log.Info("Mounting IPNS at ", nsdir)
|
||||
|
||||
go func() {
|
||||
err := ipns.Mount(node, nsdir, fsdir)
|
||||
done <- err
|
||||
close(done)
|
||||
}()
|
||||
|
||||
return done
|
||||
}
|
||||
|
||||
var platformFuseChecks = func() error {
|
||||
return nil
|
||||
}
|
18
core/commands2/mount_windows.go
Normal file
18
core/commands2/mount_windows.go
Normal file
@ -0,0 +1,18 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
)
|
||||
|
||||
var ipfsMount = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Not yet implemented on Windows",
|
||||
ShortDescription: "Not yet implemented on Windows. :(",
|
||||
},
|
||||
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
return errors.New("Mount isn't compatible with Windows yet"), nil
|
||||
},
|
||||
}
|
57
core/commands2/name.go
Normal file
57
core/commands2/name.go
Normal file
@ -0,0 +1,57 @@
|
||||
package commands
|
||||
|
||||
import cmds "github.com/jbenet/go-ipfs/commands"
|
||||
|
||||
type IpnsEntry struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
var nameCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "IPFS namespace (IPNS) tool",
|
||||
Synopsis: `
|
||||
ipfs name publish [<name>] <ipfs-path> - Publish an object to IPNS
|
||||
ipfs name resolve [<name>] - Gets the value currently published at an IPNS name
|
||||
`,
|
||||
ShortDescription: `
|
||||
IPNS is a PKI namespace, where names are the hashes of public keys, and
|
||||
the private key enables publishing new (signed) values. In both publish
|
||||
and resolve, the default value of <name> is your own identity public key.
|
||||
`,
|
||||
LongDescription: `
|
||||
IPNS is a PKI namespace, where names are the hashes of public keys, and
|
||||
the private key enables publishing new (signed) values. In both publish
|
||||
and resolve, the default value of <name> is your own identity public key.
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
Publish a <ref> to your identity name:
|
||||
|
||||
> ipfs name publish QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
|
||||
published name QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n to QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
|
||||
|
||||
Publish a <ref> to another public key:
|
||||
|
||||
> ipfs name publish QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
|
||||
published name QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n to QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
|
||||
|
||||
Resolve the value of your identity:
|
||||
|
||||
> ipfs name resolve
|
||||
QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
|
||||
|
||||
Resolve the value of another name:
|
||||
|
||||
> ipfs name resolve QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n
|
||||
QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
|
||||
|
||||
`,
|
||||
},
|
||||
|
||||
Subcommands: map[string]*cmds.Command{
|
||||
"publish": publishCmd,
|
||||
"resolve": resolveCmd,
|
||||
},
|
||||
}
|
393
core/commands2/object.go
Normal file
393
core/commands2/object.go
Normal file
@ -0,0 +1,393 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
mh "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multihash"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
core "github.com/jbenet/go-ipfs/core"
|
||||
dag "github.com/jbenet/go-ipfs/merkledag"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
// ErrObjectTooLarge is returned when too much data was read from stdin. current limit 512k
|
||||
var ErrObjectTooLarge = errors.New("input object was too large. limit is 512kbytes")
|
||||
|
||||
const inputLimit = 512 * 1024
|
||||
|
||||
type Node struct {
|
||||
Links []Link
|
||||
Data []byte
|
||||
}
|
||||
|
||||
var objectCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Interact with ipfs objects",
|
||||
ShortDescription: `
|
||||
'ipfs object' is a plumbing command used to manipulate DAG objects
|
||||
directly.`,
|
||||
Synopsis: `
|
||||
ipfs object get <key> - Get the DAG node named by <key>
|
||||
ipfs object put <data> <encoding> - Stores input, outputs its key
|
||||
ipfs object data <key> - Outputs raw bytes in an object
|
||||
ipfs object links <key> - Outputs links pointed to by object
|
||||
`,
|
||||
},
|
||||
|
||||
Subcommands: map[string]*cmds.Command{
|
||||
"data": objectDataCmd,
|
||||
"links": objectLinksCmd,
|
||||
"get": objectGetCmd,
|
||||
"put": objectPutCmd,
|
||||
},
|
||||
}
|
||||
|
||||
var objectDataCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Outputs the raw bytes in an IPFS object",
|
||||
ShortDescription: `
|
||||
ipfs data is a plumbing command for retreiving the raw bytes stored in
|
||||
a DAG node. It outputs to stdout, and <key> is a base58 encoded
|
||||
multihash.
|
||||
`,
|
||||
LongDescription: `
|
||||
ipfs data is a plumbing command for retreiving the raw bytes stored in
|
||||
a DAG node. It outputs to stdout, and <key> is a base58 encoded
|
||||
multihash.
|
||||
|
||||
Note that the "--encoding" option does not affect the output, since the
|
||||
output is the raw data of the object.
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, ok := req.Arguments()[0].(string)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
return objectData(n, key)
|
||||
},
|
||||
}
|
||||
|
||||
var objectLinksCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Outputs the links pointed to by the specified object",
|
||||
ShortDescription: `
|
||||
'ipfs object links' is a plumbing command for retreiving the links from
|
||||
a DAG node. It outputs to stdout, and <key> is a base58 encoded
|
||||
multihash.
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, ok := req.Arguments()[0].(string)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
return objectLinks(n, key)
|
||||
},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
object := res.Output().(*Object)
|
||||
marshalled := marshalLinks(object.Links)
|
||||
return []byte(marshalled), nil
|
||||
},
|
||||
},
|
||||
Type: &Object{},
|
||||
}
|
||||
|
||||
var objectGetCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Get and serialize the DAG node named by <key>",
|
||||
ShortDescription: `
|
||||
'ipfs object get' is a plumbing command for retreiving DAG nodes.
|
||||
It serializes the DAG node to the format specified by the "--encoding"
|
||||
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
|
||||
`,
|
||||
LongDescription: `
|
||||
'ipfs object get' is a plumbing command for retreiving DAG nodes.
|
||||
It serializes the DAG node to the format specified by the "--encoding"
|
||||
flag. It outputs to stdout, and <key> is a base58 encoded multihash.
|
||||
|
||||
This command outputs data in the following encodings:
|
||||
* "protobuf"
|
||||
* "json"
|
||||
* "xml"
|
||||
(Specified by the "--encoding" or "-enc" flag)`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("key", true, false, "Key of the object to retrieve (in base58-encoded multihash format)"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, ok := req.Arguments()[0].(string)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
object, err := objectGet(n, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node := &Node{
|
||||
Links: make([]Link, len(object.Links)),
|
||||
Data: object.Data,
|
||||
}
|
||||
|
||||
for i, link := range object.Links {
|
||||
node.Links[i] = Link{
|
||||
Hash: link.Hash.B58String(),
|
||||
Name: link.Name,
|
||||
Size: link.Size,
|
||||
}
|
||||
}
|
||||
|
||||
return node, nil
|
||||
},
|
||||
Type: &Node{},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.EncodingType("protobuf"): func(res cmds.Response) ([]byte, error) {
|
||||
node := res.Output().(*Node)
|
||||
object, err := deserializeNode(node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return object.Marshal()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var objectPutCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Stores input as a DAG object, outputs its key",
|
||||
ShortDescription: `
|
||||
'ipfs object put' is a plumbing command for storing DAG nodes.
|
||||
It reads from stdin, and the output is a base58 encoded multihash.
|
||||
`,
|
||||
LongDescription: `
|
||||
'ipfs object put' is a plumbing command for storing DAG nodes.
|
||||
It reads from stdin, and the output is a base58 encoded multihash.
|
||||
|
||||
Data should be in the format specified by <encoding>.
|
||||
<encoding> may be one of the following:
|
||||
* "protobuf"
|
||||
* "json"
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.FileArg("data", true, false, "Data to be stored as a DAG object"),
|
||||
cmds.StringArg("encoding", true, false, "Encoding type of <data>, either \"protobuf\" or \"json\""),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
input, ok := req.Arguments()[0].(io.Reader)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
encoding, ok := req.Arguments()[1].(string)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
|
||||
output, err := objectPut(n, input, encoding)
|
||||
if err != nil {
|
||||
errType := cmds.ErrNormal
|
||||
if err == ErrUnknownObjectEnc {
|
||||
errType = cmds.ErrClient
|
||||
}
|
||||
return nil, cmds.Error{err.Error(), errType}
|
||||
}
|
||||
|
||||
return output, nil
|
||||
},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
object := res.Output().(*Object)
|
||||
return []byte("added " + object.Hash), nil
|
||||
},
|
||||
},
|
||||
Type: &Object{},
|
||||
}
|
||||
|
||||
// objectData takes a key string and writes out the raw bytes of that node (if there is one)
|
||||
func objectData(n *core.IpfsNode, key string) (io.Reader, error) {
|
||||
dagnode, err := n.Resolver.ResolvePath(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("objectData: found dagnode %q (# of bytes: %d - # links: %d)", key, len(dagnode.Data), len(dagnode.Links))
|
||||
|
||||
return bytes.NewReader(dagnode.Data), nil
|
||||
}
|
||||
|
||||
// objectLinks takes a key string and lists the links it points to
|
||||
func objectLinks(n *core.IpfsNode, key string) (*Object, error) {
|
||||
dagnode, err := n.Resolver.ResolvePath(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("objectLinks: found dagnode %q (# of bytes: %d - # links: %d)", key, len(dagnode.Data), len(dagnode.Links))
|
||||
|
||||
return getOutput(dagnode)
|
||||
}
|
||||
|
||||
// objectGet takes a key string from args and a format option and serializes the dagnode to that format
|
||||
func objectGet(n *core.IpfsNode, key string) (*dag.Node, error) {
|
||||
dagnode, err := n.Resolver.ResolvePath(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("objectGet: found dagnode %q (# of bytes: %d - # links: %d)", key, len(dagnode.Data), len(dagnode.Links))
|
||||
|
||||
return dagnode, nil
|
||||
}
|
||||
|
||||
// objectPut takes a format option, serializes bytes from stdin and updates the dag with that data
|
||||
func objectPut(n *core.IpfsNode, input io.Reader, encoding string) (*Object, error) {
|
||||
var (
|
||||
dagnode *dag.Node
|
||||
data []byte
|
||||
err error
|
||||
)
|
||||
|
||||
data, err = ioutil.ReadAll(io.LimitReader(input, inputLimit+10))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) >= inputLimit {
|
||||
return nil, ErrObjectTooLarge
|
||||
}
|
||||
|
||||
switch getObjectEnc(encoding) {
|
||||
case objectEncodingJSON:
|
||||
node := new(Node)
|
||||
err = json.Unmarshal(data, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dagnode, err = deserializeNode(node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case objectEncodingProtobuf:
|
||||
dagnode, err = dag.Decoded(data)
|
||||
|
||||
default:
|
||||
return nil, ErrUnknownObjectEnc
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = addNode(n, dagnode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return getOutput(dagnode)
|
||||
}
|
||||
|
||||
// ErrUnknownObjectEnc is returned if a invalid encoding is supplied
|
||||
var ErrUnknownObjectEnc = errors.New("unknown object encoding")
|
||||
|
||||
type objectEncoding string
|
||||
|
||||
const (
|
||||
objectEncodingJSON objectEncoding = "json"
|
||||
objectEncodingProtobuf = "protobuf"
|
||||
)
|
||||
|
||||
func getObjectEnc(o interface{}) objectEncoding {
|
||||
v, ok := o.(string)
|
||||
if !ok {
|
||||
// chosen as default because it's human readable
|
||||
log.Warning("option is not a string - falling back to json")
|
||||
return objectEncodingJSON
|
||||
}
|
||||
|
||||
return objectEncoding(v)
|
||||
}
|
||||
|
||||
func getOutput(dagnode *dag.Node) (*Object, error) {
|
||||
key, err := dagnode.Key()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output := &Object{
|
||||
Hash: key.Pretty(),
|
||||
Links: make([]Link, len(dagnode.Links)),
|
||||
}
|
||||
|
||||
for i, link := range dagnode.Links {
|
||||
output.Links[i] = Link{
|
||||
Name: link.Name,
|
||||
Hash: link.Hash.B58String(),
|
||||
Size: link.Size,
|
||||
}
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// converts the Node object into a real dag.Node
|
||||
func deserializeNode(node *Node) (*dag.Node, error) {
|
||||
dagnode := new(dag.Node)
|
||||
dagnode.Data = node.Data
|
||||
dagnode.Links = make([]*dag.Link, len(node.Links))
|
||||
for i, link := range node.Links {
|
||||
hash, err := mh.FromB58String(link.Hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dagnode.Links[i] = &dag.Link{
|
||||
Name: link.Name,
|
||||
Size: link.Size,
|
||||
Hash: hash,
|
||||
}
|
||||
}
|
||||
|
||||
return dagnode, nil
|
||||
}
|
163
core/commands2/pin.go
Normal file
163
core/commands2/pin.go
Normal file
@ -0,0 +1,163 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
"github.com/jbenet/go-ipfs/core"
|
||||
"github.com/jbenet/go-ipfs/core/commands2/internal"
|
||||
"github.com/jbenet/go-ipfs/merkledag"
|
||||
)
|
||||
|
||||
var pinCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Pin (and unpin) objects to local storage",
|
||||
},
|
||||
|
||||
Subcommands: map[string]*cmds.Command{
|
||||
"add": addPinCmd,
|
||||
"rm": rmPinCmd,
|
||||
},
|
||||
}
|
||||
|
||||
var addPinCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Pins objects to local storage",
|
||||
ShortDescription: `
|
||||
Retrieves the object named by <ipfs-path> and stores it locally
|
||||
on disk.
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("ipfs-path", true, true, "Path to object(s) to be pinned"),
|
||||
},
|
||||
Options: []cmds.Option{
|
||||
cmds.BoolOption("recursive", "r", "Recursively pin the object linked to by the specified object(s)"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set recursive flag
|
||||
recursive, found, err := req.Option("recursive").Bool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
recursive = false
|
||||
}
|
||||
|
||||
paths, err := internal.CastToStrings(req.Arguments())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = pin(n, paths, recursive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: create some output to show what got pinned
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
var rmPinCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Unpin an object from local storage",
|
||||
ShortDescription: `
|
||||
Removes the pin from the given object allowing it to be garbage
|
||||
collected if needed.
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("ipfs-path", true, true, "Path to object(s) to be unpinned"),
|
||||
},
|
||||
Options: []cmds.Option{
|
||||
cmds.BoolOption("recursive", "r", "Recursively unpin the object linked to by the specified object(s)"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set recursive flag
|
||||
recursive, found, err := req.Option("recursive").Bool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
recursive = false // default
|
||||
}
|
||||
|
||||
paths, err := internal.CastToStrings(req.Arguments())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = unpin(n, paths, recursive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: create some output to show what got unpinned
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
func pin(n *core.IpfsNode, paths []string, recursive bool) ([]*merkledag.Node, error) {
|
||||
|
||||
dagnodes := make([]*merkledag.Node, 0)
|
||||
for _, path := range paths {
|
||||
dagnode, err := n.Resolver.ResolvePath(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pin error: %v", err)
|
||||
}
|
||||
dagnodes = append(dagnodes, dagnode)
|
||||
}
|
||||
|
||||
for _, dagnode := range dagnodes {
|
||||
err := n.Pinning.Pin(dagnode, recursive)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pin: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err := n.Pinning.Flush()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dagnodes, nil
|
||||
}
|
||||
|
||||
func unpin(n *core.IpfsNode, paths []string, recursive bool) ([]*merkledag.Node, error) {
|
||||
|
||||
dagnodes := make([]*merkledag.Node, 0)
|
||||
for _, path := range paths {
|
||||
dagnode, err := n.Resolver.ResolvePath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dagnodes = append(dagnodes, dagnode)
|
||||
}
|
||||
|
||||
for _, dagnode := range dagnodes {
|
||||
k, _ := dagnode.Key()
|
||||
err := n.Pinning.Unpin(k, recursive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err := n.Pinning.Flush()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dagnodes, nil
|
||||
}
|
107
core/commands2/publish.go
Normal file
107
core/commands2/publish.go
Normal file
@ -0,0 +1,107 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
core "github.com/jbenet/go-ipfs/core"
|
||||
crypto "github.com/jbenet/go-ipfs/crypto"
|
||||
nsys "github.com/jbenet/go-ipfs/namesys"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
var errNotOnline = errors.New("This command must be run in online mode. Try running 'ipfs daemon' first.")
|
||||
|
||||
var publishCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Publish an object to IPNS",
|
||||
ShortDescription: `
|
||||
IPNS is a PKI namespace, where names are the hashes of public keys, and
|
||||
the private key enables publishing new (signed) values. In publish, the
|
||||
default value of <name> is your own identity public key.
|
||||
`,
|
||||
LongDescription: `
|
||||
IPNS is a PKI namespace, where names are the hashes of public keys, and
|
||||
the private key enables publishing new (signed) values. In publish, the
|
||||
default value of <name> is your own identity public key.
|
||||
|
||||
Examples:
|
||||
|
||||
Publish a <ref> to your identity name:
|
||||
|
||||
> ipfs name publish QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
|
||||
published name QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n to QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
|
||||
|
||||
Publish a <ref> to another public key:
|
||||
|
||||
> ipfs name publish QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
|
||||
published name QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n to QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("name", false, false, "The IPNS name to publish to. Defaults to your node's peerID"),
|
||||
cmds.StringArg("ipfs-path", true, false, "IPFS path of the obejct to be published at <name>"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
log.Debug("Begin Publish")
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args := req.Arguments()
|
||||
|
||||
if n.Network == nil {
|
||||
return nil, errNotOnline
|
||||
}
|
||||
|
||||
if n.Identity == nil {
|
||||
return nil, errors.New("Identity not loaded!")
|
||||
}
|
||||
|
||||
// name := ""
|
||||
ref := ""
|
||||
|
||||
switch len(args) {
|
||||
case 2:
|
||||
// name = args[0]
|
||||
ref = args[1].(string)
|
||||
return nil, errors.New("keychains not yet implemented")
|
||||
case 1:
|
||||
// name = n.Identity.ID.String()
|
||||
ref = args[0].(string)
|
||||
}
|
||||
|
||||
// TODO n.Keychain.Get(name).PrivKey
|
||||
k := n.Identity.PrivKey()
|
||||
return publish(n, k, ref)
|
||||
},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
v := res.Output().(*IpnsEntry)
|
||||
s := fmt.Sprintf("Published name %s to %s\n", v.Name, v.Value)
|
||||
return []byte(s), nil
|
||||
},
|
||||
},
|
||||
Type: &IpnsEntry{},
|
||||
}
|
||||
|
||||
func publish(n *core.IpfsNode, k crypto.PrivKey, ref string) (*IpnsEntry, error) {
|
||||
pub := nsys.NewRoutingPublisher(n.Routing)
|
||||
err := pub.Publish(k, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hash, err := k.GetPublic().Hash()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &IpnsEntry{
|
||||
Name: u.Key(hash).String(),
|
||||
Value: ref,
|
||||
}, nil
|
||||
}
|
135
core/commands2/refs.go
Normal file
135
core/commands2/refs.go
Normal file
@ -0,0 +1,135 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
mh "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multihash"
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
"github.com/jbenet/go-ipfs/core"
|
||||
"github.com/jbenet/go-ipfs/core/commands2/internal"
|
||||
dag "github.com/jbenet/go-ipfs/merkledag"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
type RefsOutput struct {
|
||||
Refs []string
|
||||
}
|
||||
|
||||
var refsCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Lists link hashes from an object",
|
||||
ShortDescription: `
|
||||
Retrieves the object named by <ipfs-path> and displays the link
|
||||
hashes it contains, with the following format:
|
||||
|
||||
<link base58 hash>
|
||||
|
||||
Note: list all refs recursively with -r.
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("ipfs-path", true, true, "Path to the object(s) to list refs from"),
|
||||
},
|
||||
Options: []cmds.Option{
|
||||
cmds.BoolOption("unique", "u", "Omit duplicate refs from output"),
|
||||
cmds.BoolOption("recursive", "r", "Recursively list links of child nodes"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unique, found, err := req.Option("unique").Bool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
unique = false
|
||||
}
|
||||
|
||||
recursive, found, err := req.Option("recursive").Bool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
recursive = false
|
||||
}
|
||||
|
||||
paths, err := internal.CastToStrings(req.Arguments())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return getRefs(n, paths, unique, recursive)
|
||||
},
|
||||
Type: &RefsOutput{},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
output := res.Output().(*RefsOutput)
|
||||
s := ""
|
||||
for _, ref := range output.Refs {
|
||||
s += fmt.Sprintln(ref)
|
||||
}
|
||||
return []byte(s), nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func getRefs(n *core.IpfsNode, paths []string, unique, recursive bool) (*RefsOutput, error) {
|
||||
var refsSeen map[u.Key]bool
|
||||
if unique {
|
||||
refsSeen = make(map[u.Key]bool)
|
||||
}
|
||||
|
||||
refs := make([]string, 0)
|
||||
|
||||
for _, path := range paths {
|
||||
object, err := n.Resolver.ResolvePath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refs, err = addRefs(n, object, refs, refsSeen, recursive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &RefsOutput{refs}, nil
|
||||
}
|
||||
|
||||
func addRefs(n *core.IpfsNode, object *dag.Node, refs []string, refsSeen map[u.Key]bool, recursive bool) ([]string, error) {
|
||||
for _, link := range object.Links {
|
||||
var found bool
|
||||
found, refs = addRef(link.Hash, refs, refsSeen)
|
||||
|
||||
if recursive && !found {
|
||||
child, err := n.DAG.Get(u.Key(link.Hash))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot retrieve %s (%s)", link.Hash.B58String(), err)
|
||||
}
|
||||
|
||||
refs, err = addRefs(n, child, refs, refsSeen, recursive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func addRef(h mh.Multihash, refs []string, refsSeen map[u.Key]bool) (bool, []string) {
|
||||
if refsSeen != nil {
|
||||
_, found := refsSeen[u.Key(h)]
|
||||
if found {
|
||||
return true, refs
|
||||
}
|
||||
refsSeen[u.Key(h)] = true
|
||||
}
|
||||
|
||||
refs = append(refs, h.B58String())
|
||||
return false, refs
|
||||
}
|
84
core/commands2/resolve.go
Normal file
84
core/commands2/resolve.go
Normal file
@ -0,0 +1,84 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
var resolveCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Gets the value currently published at an IPNS name",
|
||||
ShortDescription: `
|
||||
IPNS is a PKI namespace, where names are the hashes of public keys, and
|
||||
the private key enables publishing new (signed) values. In resolve, the
|
||||
default value of <name> is your own identity public key.
|
||||
`,
|
||||
LongDescription: `
|
||||
IPNS is a PKI namespace, where names are the hashes of public keys, and
|
||||
the private key enables publishing new (signed) values. In resolve, the
|
||||
default value of <name> is your own identity public key.
|
||||
|
||||
|
||||
Examples:
|
||||
|
||||
Resolve the value of your identity:
|
||||
|
||||
> ipfs name resolve
|
||||
QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
|
||||
|
||||
Resolve te value of another name:
|
||||
|
||||
> ipfs name resolve QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n
|
||||
QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
|
||||
|
||||
`,
|
||||
},
|
||||
|
||||
Arguments: []cmds.Argument{
|
||||
cmds.StringArg("name", false, false, "The IPNS name to resolve. Defaults to your node's peerID."),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var name string
|
||||
|
||||
if n.Network == nil {
|
||||
return nil, errNotOnline
|
||||
}
|
||||
|
||||
if len(req.Arguments()) == 0 {
|
||||
if n.Identity == nil {
|
||||
return nil, errors.New("Identity not loaded!")
|
||||
}
|
||||
name = n.Identity.ID().String()
|
||||
|
||||
} else {
|
||||
var ok bool
|
||||
name, ok = req.Arguments()[0].(string)
|
||||
if !ok {
|
||||
return nil, u.ErrCast()
|
||||
}
|
||||
}
|
||||
|
||||
output, err := n.Namesys.Resolve(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: better errors (in the case of not finding the name, we get "failed to find any peer in table")
|
||||
|
||||
return output, nil
|
||||
},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
output := res.Output().(string)
|
||||
return []byte(output), nil
|
||||
},
|
||||
},
|
||||
}
|
87
core/commands2/root.go
Normal file
87
core/commands2/root.go
Normal file
@ -0,0 +1,87 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
u "github.com/jbenet/go-ipfs/util"
|
||||
)
|
||||
|
||||
var log = u.Logger("core/commands")
|
||||
|
||||
type TestOutput struct {
|
||||
Foo string
|
||||
Bar int
|
||||
}
|
||||
|
||||
var Root = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Global P2P Merkle-DAG filesystem",
|
||||
ShortDescription: `
|
||||
Basic commands:
|
||||
|
||||
init Initialize ipfs local configurationx
|
||||
add <path> Add an object to ipfs
|
||||
cat <ref> Show ipfs object data
|
||||
ls <ref> List links from an object
|
||||
|
||||
Tool commands:
|
||||
|
||||
config Manage configuration
|
||||
update Download and apply go-ipfs updates
|
||||
version Show ipfs version information
|
||||
commands List all available commands
|
||||
|
||||
Advanced Commands:
|
||||
|
||||
mount Mount an ipfs read-only mountpoint
|
||||
serve Serve an interface to ipfs
|
||||
diag Print diagnostics
|
||||
|
||||
Plumbing commands:
|
||||
|
||||
block Interact with raw blocks in the datastore
|
||||
object Interact with raw dag nodes
|
||||
`,
|
||||
},
|
||||
Options: []cmds.Option{
|
||||
cmds.StringOption("config", "c", "Path to the configuration file to use"),
|
||||
cmds.BoolOption("debug", "D", "Operate in debug mode"),
|
||||
cmds.BoolOption("help", "Show the full command help text"),
|
||||
cmds.BoolOption("h", "Show a short version of the command help text"),
|
||||
cmds.BoolOption("local", "L", "Run the command locally, instead of using the daemon"),
|
||||
},
|
||||
}
|
||||
|
||||
// commandsDaemonCmd is the "ipfs commands" command for daemon
|
||||
var CommandsDaemonCmd = CommandsCmd(Root)
|
||||
|
||||
var rootSubcommands = map[string]*cmds.Command{
|
||||
"cat": catCmd,
|
||||
"ls": lsCmd,
|
||||
"commands": CommandsDaemonCmd,
|
||||
"name": nameCmd,
|
||||
"add": addCmd,
|
||||
"log": LogCmd,
|
||||
"diag": DiagCmd,
|
||||
"pin": pinCmd,
|
||||
"version": VersionCmd,
|
||||
"config": configCmd,
|
||||
"bootstrap": bootstrapCmd,
|
||||
"mount": mountCmd,
|
||||
"block": blockCmd,
|
||||
"update": UpdateCmd,
|
||||
"object": objectCmd,
|
||||
"refs": refsCmd,
|
||||
}
|
||||
|
||||
func init() {
|
||||
Root.Subcommands = rootSubcommands
|
||||
u.SetLogLevel("core/commands", "info")
|
||||
}
|
||||
|
||||
type MessageOutput struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func MessageTextMarshaler(res cmds.Response) ([]byte, error) {
|
||||
return []byte(res.Output().(*MessageOutput).Message), nil
|
||||
}
|
149
core/commands2/update.go
Normal file
149
core/commands2/update.go
Normal file
@ -0,0 +1,149 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
"github.com/jbenet/go-ipfs/core"
|
||||
"github.com/jbenet/go-ipfs/updates"
|
||||
)
|
||||
|
||||
type UpdateOutput struct {
|
||||
OldVersion string
|
||||
NewVersion string
|
||||
}
|
||||
|
||||
var UpdateCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Downloads and installs updates for IPFS",
|
||||
ShortDescription: "ipfs update is a utility command used to check for updates and apply them.",
|
||||
},
|
||||
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return updateApply(n)
|
||||
},
|
||||
Type: &UpdateOutput{},
|
||||
Subcommands: map[string]*cmds.Command{
|
||||
"check": UpdateCheckCmd,
|
||||
"log": UpdateLogCmd,
|
||||
},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
v := res.Output().(*UpdateOutput)
|
||||
s := ""
|
||||
if v.NewVersion != v.OldVersion {
|
||||
s = fmt.Sprintf("Successfully updated to IPFS version '%s' (from '%s')\n",
|
||||
v.NewVersion, v.OldVersion)
|
||||
} else {
|
||||
s = fmt.Sprintf("Already updated to latest version ('%s')\n", v.NewVersion)
|
||||
}
|
||||
return []byte(s), nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateCheckCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Checks if updates are available",
|
||||
ShortDescription: "'ipfs update check' checks if any updates are available for IPFS.\nNothing will be downloaded or installed.",
|
||||
},
|
||||
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return updateCheck(n)
|
||||
},
|
||||
Type: &UpdateOutput{},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
v := res.Output().(*UpdateOutput)
|
||||
s := ""
|
||||
if v.NewVersion != v.OldVersion {
|
||||
s = fmt.Sprintf("A new version of IPFS is available ('%s', currently running '%s')\n",
|
||||
v.NewVersion, v.OldVersion)
|
||||
} else {
|
||||
s = fmt.Sprintf("Already updated to latest version ('%s')\n", v.NewVersion)
|
||||
}
|
||||
return []byte(s), nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var UpdateLogCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "List the changelog for the latest versions of IPFS",
|
||||
ShortDescription: "This command is not yet implemented.",
|
||||
},
|
||||
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
n, err := req.Context().GetNode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return updateLog(n)
|
||||
},
|
||||
}
|
||||
|
||||
// updateApply applies an update of the ipfs binary and shuts down the node if successful
|
||||
func updateApply(n *core.IpfsNode) (*UpdateOutput, error) {
|
||||
// TODO: 'force bool' param that stops the daemon (if running) before update
|
||||
|
||||
output := &UpdateOutput{
|
||||
OldVersion: updates.Version,
|
||||
}
|
||||
|
||||
u, err := updates.CheckForUpdate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
output.NewVersion = updates.Version
|
||||
return output, nil
|
||||
}
|
||||
|
||||
output.NewVersion = u.Version
|
||||
|
||||
if n.OnlineMode() {
|
||||
return nil, errors.New(`You must stop the IPFS daemon before updating.`)
|
||||
}
|
||||
|
||||
if err = updates.Apply(u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// updateCheck checks wether there is an update available
|
||||
func updateCheck(n *core.IpfsNode) (*UpdateOutput, error) {
|
||||
output := &UpdateOutput{
|
||||
OldVersion: updates.Version,
|
||||
}
|
||||
|
||||
u, err := updates.CheckForUpdate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
output.NewVersion = updates.Version
|
||||
return output, nil
|
||||
}
|
||||
|
||||
output.NewVersion = u.Version
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// updateLog lists the version available online
|
||||
func updateLog(n *core.IpfsNode) (interface{}, error) {
|
||||
// TODO
|
||||
return nil, errors.New("Not yet implemented")
|
||||
}
|
43
core/commands2/version.go
Normal file
43
core/commands2/version.go
Normal file
@ -0,0 +1,43 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
cmds "github.com/jbenet/go-ipfs/commands"
|
||||
config "github.com/jbenet/go-ipfs/config"
|
||||
)
|
||||
|
||||
type VersionOutput struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
var VersionCmd = &cmds.Command{
|
||||
Helptext: cmds.HelpText{
|
||||
Tagline: "Shows ipfs version information",
|
||||
ShortDescription: "Returns the current version of ipfs and exits.",
|
||||
},
|
||||
|
||||
Options: []cmds.Option{
|
||||
cmds.BoolOption("number", "n", "Only show the version number"),
|
||||
},
|
||||
Run: func(req cmds.Request) (interface{}, error) {
|
||||
return &VersionOutput{
|
||||
Version: config.CurrentVersionNumber,
|
||||
}, nil
|
||||
},
|
||||
Marshalers: cmds.MarshalerMap{
|
||||
cmds.Text: func(res cmds.Response) ([]byte, error) {
|
||||
v := res.Output().(*VersionOutput)
|
||||
|
||||
number, found, err := res.Request().Option("number").Bool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if found && number {
|
||||
return []byte(fmt.Sprintln(v.Version)), nil
|
||||
}
|
||||
return []byte(fmt.Sprintf("ipfs version %s\n", v.Version)), nil
|
||||
},
|
||||
},
|
||||
Type: &VersionOutput{},
|
||||
}
|
@ -74,6 +74,8 @@ type IpfsNode struct {
|
||||
Pinning pin.Pinner
|
||||
|
||||
ctxc.ContextCloser
|
||||
|
||||
onlineMode bool // alternatively, offline
|
||||
}
|
||||
|
||||
// NewIpfsNode constructs a new IpfsNode based on the given config.
|
||||
@ -92,6 +94,7 @@ func NewIpfsNode(cfg *config.Config, online bool) (n *IpfsNode, err error) {
|
||||
// derive this from a higher context.
|
||||
ctx := context.TODO()
|
||||
n = &IpfsNode{
|
||||
onlineMode: online,
|
||||
Config: cfg,
|
||||
ContextCloser: ctxc.NewContextCloser(ctx, nil),
|
||||
}
|
||||
@ -172,6 +175,10 @@ func NewIpfsNode(cfg *config.Config, online bool) (n *IpfsNode, err error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (n *IpfsNode) OnlineMode() bool {
|
||||
return n.onlineMode
|
||||
}
|
||||
|
||||
func initIdentity(cfg *config.Config, peers peer.Peerstore, online bool) (peer.Peer, error) {
|
||||
if cfg.Identity.PeerID == "" {
|
||||
return nil, errors.New("Identity was not set in config (was ipfs init run?)")
|
||||
|
25
daemon2/daemon.go
Normal file
25
daemon2/daemon.go
Normal file
@ -0,0 +1,25 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path"
|
||||
|
||||
lock "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/camlistore/lock"
|
||||
)
|
||||
|
||||
// LockFile is the filename of the daemon lock, relative to config dir
|
||||
const LockFile = "daemon.lock"
|
||||
|
||||
func Lock(confdir string) (io.Closer, error) {
|
||||
return lock.Lock(path.Join(confdir, LockFile))
|
||||
}
|
||||
|
||||
func Locked(confdir string) bool {
|
||||
if lk, err := Lock(confdir); err != nil {
|
||||
return true
|
||||
|
||||
} else {
|
||||
lk.Close()
|
||||
return false
|
||||
}
|
||||
}
|
@ -67,6 +67,7 @@ type DiagInfo struct {
|
||||
Keys []string
|
||||
|
||||
// How long this node has been running for
|
||||
// TODO rename Uptime
|
||||
LifeSpan time.Duration
|
||||
|
||||
// Incoming Bandwidth Usage
|
||||
|
@ -22,8 +22,10 @@ type handler struct {
|
||||
func Serve(address ma.Multiaddr, node *core.IpfsNode) error {
|
||||
r := mux.NewRouter()
|
||||
handler := &handler{&ipfsHandler{node}}
|
||||
|
||||
r.HandleFunc("/ipfs/", handler.postHandler).Methods("POST")
|
||||
r.PathPrefix("/ipfs/").Handler(handler).Methods("GET")
|
||||
|
||||
http.Handle("/", r)
|
||||
|
||||
_, host, err := manet.DialArgs(address)
|
||||
|
@ -200,7 +200,7 @@ func CliCheckForUpdates(cfg *config.Config, confFile string) error {
|
||||
|
||||
// if config says not to, don't check for updates
|
||||
if !cfg.Version.ShouldCheckForUpdate() {
|
||||
log.Info("update checking disabled.")
|
||||
log.Info("update check skipped.")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
crand "crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/jbenet/go-ipfs/peer"
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -40,6 +41,14 @@ func TildeExpansion(filename string) (string, error) {
|
||||
return homedir.Expand(filename)
|
||||
}
|
||||
|
||||
// ErrCast is returned when a cast fails AND the program should not panic.
|
||||
func ErrCast() error {
|
||||
debug.PrintStack()
|
||||
return errCast
|
||||
}
|
||||
|
||||
var errCast = errors.New("cast error")
|
||||
|
||||
// ExpandPathnames takes a set of paths and turns them into absolute paths
|
||||
func ExpandPathnames(paths []string) ([]string, error) {
|
||||
var out []string
|
||||
|
Reference in New Issue
Block a user