1
0
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:
Juan Batiz-Benet
2014-11-14 03:36:19 -08:00
58 changed files with 5036 additions and 210 deletions

View File

@ -10,10 +10,14 @@ godep:
vendor: godep vendor: godep
godep save -r ./... godep save -r ./...
# TODO revert to `install` once new command refactoring is complete
install: install_1:
cd cmd/ipfs && go install 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: test_go test_sharness
test_go: test_go:

3
cmd/ipfs2/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
./ipfs
./ipfs.exe
./ipfs2

7
cmd/ipfs2/Makefile Normal file
View File

@ -0,0 +1,7 @@
all: install
build:
go build
install: build
go install

38
cmd/ipfs2/README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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())
}

View File

@ -8,8 +8,35 @@ const (
) )
type Argument struct { type Argument struct {
Name string Name string
Type ArgumentType Type ArgumentType
Required bool Required bool
Variadic 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
View 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)
}

View File

@ -1,6 +1,7 @@
package cli package cli
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -9,50 +10,50 @@ import (
cmds "github.com/jbenet/go-ipfs/commands" 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). // Parse parses the input commandline string (cmd, flags, and args).
// returns the corresponding command Request object. // returns the corresponding command Request object.
func Parse(input []string, roots ...*cmds.Command) (cmds.Request, *cmds.Command, error) { // Parse will search each root to find the one that best matches the requested subcommand.
var root, cmd *cmds.Command func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *cmds.Command, []string, error) {
var path, stringArgs []string
var opts map[string]interface{}
// use the root that matches the longest path (most accurately matches request) // use the root that matches the longest path (most accurately matches request)
maxLength := 0 path, input, cmd := parsePath(input, root)
for _, r := range roots { opts, stringArgs, err := parseOptions(input)
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)
if err != nil { 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) err = cmd.CheckArguments(req)
if err != nil { 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 // 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 return opts, args, nil
} }
func parseArgs(stringArgs []string, cmd *cmds.Command) ([]interface{}, error) { func parseArgs(stringArgs []string, stdin *os.File, arguments []cmds.Argument) ([]interface{}, error) {
var argDef cmds.Argument // check if stdin is coming from terminal or is being piped in
args := make([]interface{}, len(stringArgs)) if stdin != nil {
stat, err := stdin.Stat()
if err != nil {
return nil, err
}
for i, arg := range stringArgs { // if stdin isn't a CharDevice, set it to nil
if i < len(cmd.Arguments) { // (this means it is coming from terminal and we want to ignore it)
argDef = cmd.Arguments[i] 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 { if argDef.Type == cmds.ArgString {
args[i] = arg if stdin == nil {
// add string values
args = append(args, stringArgs[0])
stringArgs = stringArgs[1:]
} else { } else if argDef.SupportsStdin {
in, err := os.Open(arg) // if we have a stdin, read it in and use the data as a string value
if err != nil { var buf bytes.Buffer
return nil, err _, 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 return args, nil

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"reflect"
"strings" "strings"
u "github.com/jbenet/go-ipfs/util" 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. // Function is the type of function that Commands use.
// It reads from the Request, and writes results to the Response. // 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) // (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 // TODO: check Argument definitions when creating a Command
// (might need to use a Command constructor) // (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). // Command is a runnable command, with input arguments and options (flags).
// It can also have Subcommands, to group units of work into sets. // It can also have Subcommands, to group units of work into sets.
type Command struct { type Command struct {
Help string Options []Option
Options []Option Arguments []Argument
Arguments []Argument Run Function
Run Function Marshalers map[EncodingType]Marshaler
Marshallers map[EncodingType]Marshaller 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{} Type interface{}
Subcommands map[string]*Command Subcommands map[string]*Command
} }
// ErrNotCallable signals a command that cannot be called. // 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 // Call invokes the command for the given Request
func (c *Command) Call(req Request) Response { func (c *Command) Call(req Request) Response {
@ -64,20 +93,47 @@ func (c *Command) Call(req Request) Response {
return res return res
} }
options, err := c.GetOptions(req.Path()) err = req.ConvertOptions()
if err != nil { if err != nil {
res.SetError(err, ErrClient) res.SetError(err, ErrClient)
return res return res
} }
err = req.ConvertOptions(options) output, err := cmd.Run(req)
if err != nil { 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 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 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)) 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 // 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 // the value for this argument definition. can be nil if it wasn't provided by the caller
var v interface{} var v interface{}
if i < len(args) { if valueIndex < len(args) {
v = args[i] v = args[valueIndex]
valueIndex++
} }
err := checkArgValue(v, argDef) err := checkArgValue(v, argDef)
@ -164,8 +234,8 @@ func (c *Command) CheckArguments(req Request) error {
} }
// any additional values are for the variadic arg definition // any additional values are for the variadic arg definition
if argDef.Variadic && i < len(args)-1 { if argDef.Variadic && valueIndex < len(args)-1 {
for _, val := range args[i+1:] { for _, val := range args[valueIndex:] {
err := checkArgValue(val, argDef) err := checkArgValue(val, argDef)
if err != nil { if err != nil {
return err return err
@ -207,3 +277,7 @@ func checkArgValue(v interface{}, def Argument) error {
return nil return nil
} }
func ClientError(msg string) error {
return &Error{Code: ErrClient, Message: msg}
}

View File

@ -2,38 +2,36 @@ package commands
import "testing" import "testing"
func noop(req Request) (interface{}, error) {
return nil, nil
}
func TestOptionValidation(t *testing.T) { func TestOptionValidation(t *testing.T) {
cmd := Command{ cmd := Command{
Options: []Option{ Options: []Option{
Option{[]string{"b", "beep"}, Int}, Option{[]string{"b", "beep"}, Int, "enables beeper"},
Option{[]string{"B", "boop"}, String}, Option{[]string{"B", "boop"}, String, "password for booper"},
}, },
Run: func(res Response, req Request) {}, Run: noop,
} }
req := NewEmptyRequest() opts, _ := cmd.GetOptions(nil)
req.SetOption("beep", 5)
req.SetOption("b", 10) req := NewRequest(nil, nil, nil, nil, opts)
req.SetOption("beep", true)
res := cmd.Call(req) 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 { if res.Error() == nil {
t.Error("Should have failed (incorrect type)") t.Error("Should have failed (incorrect type)")
} }
req = NewEmptyRequest() req = NewRequest(nil, nil, nil, nil, opts)
req.SetOption("beep", 5) req.SetOption("beep", 5)
res = cmd.Call(req) res = cmd.Call(req)
if res.Error() != nil { if res.Error() != nil {
t.Error(res.Error(), "Should have passed") t.Error(res.Error(), "Should have passed")
} }
req = NewEmptyRequest() req = NewRequest(nil, nil, nil, nil, opts)
req.SetOption("beep", 5) req.SetOption("beep", 5)
req.SetOption("boop", "test") req.SetOption("boop", "test")
res = cmd.Call(req) res = cmd.Call(req)
@ -41,7 +39,7 @@ func TestOptionValidation(t *testing.T) {
t.Error("Should have passed") t.Error("Should have passed")
} }
req = NewEmptyRequest() req = NewRequest(nil, nil, nil, nil, opts)
req.SetOption("b", 5) req.SetOption("b", 5)
req.SetOption("B", "test") req.SetOption("B", "test")
res = cmd.Call(req) res = cmd.Call(req)
@ -49,48 +47,46 @@ func TestOptionValidation(t *testing.T) {
t.Error("Should have passed") t.Error("Should have passed")
} }
req = NewEmptyRequest() req = NewRequest(nil, nil, nil, nil, opts)
req.SetOption("foo", 5) req.SetOption("foo", 5)
res = cmd.Call(req) res = cmd.Call(req)
if res.Error() != nil { if res.Error() != nil {
t.Error("Should have passed") t.Error("Should have passed")
} }
req = NewEmptyRequest() req = NewRequest(nil, nil, nil, nil, opts)
req.SetOption(EncShort, "json") req.SetOption(EncShort, "json")
res = cmd.Call(req) res = cmd.Call(req)
if res.Error() != nil { if res.Error() != nil {
t.Error("Should have passed") t.Error("Should have passed")
} }
req = NewEmptyRequest() req = NewRequest(nil, nil, nil, nil, opts)
req.SetOption("b", "100") req.SetOption("b", "100")
res = cmd.Call(req) res = cmd.Call(req)
if res.Error() != nil { if res.Error() != nil {
t.Error("Should have passed") t.Error("Should have passed")
} }
req = NewEmptyRequest() req = NewRequest(nil, nil, nil, nil, opts)
req.SetOption("b", ":)") req.SetOption("b", ":)")
res = cmd.Call(req) res = cmd.Call(req)
if res.Error() == nil { 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) { func TestRegistration(t *testing.T) {
noop := func(res Response, req Request) {}
cmdA := &Command{ cmdA := &Command{
Options: []Option{ Options: []Option{
Option{[]string{"beep"}, Int}, Option{[]string{"beep"}, Int, "number of beeps"},
}, },
Run: noop, Run: noop,
} }
cmdB := &Command{ cmdB := &Command{
Options: []Option{ Options: []Option{
Option{[]string{"beep"}, Int}, Option{[]string{"beep"}, Int, "number of beeps"},
}, },
Run: noop, Run: noop,
Subcommands: map[string]*Command{ Subcommands: map[string]*Command{
@ -100,18 +96,19 @@ func TestRegistration(t *testing.T) {
cmdC := &Command{ cmdC := &Command{
Options: []Option{ Options: []Option{
Option{[]string{"encoding"}, String}, Option{[]string{"encoding"}, String, "data encoding type"},
}, },
Run: noop, Run: noop,
} }
res := cmdB.Call(NewRequest([]string{"a"}, nil, nil, nil)) path := []string{"a"}
if res.Error() == nil { _, err := cmdB.GetOptions(path)
if err == nil {
t.Error("Should have failed (option name collision)") t.Error("Should have failed (option name collision)")
} }
res = cmdC.Call(NewEmptyRequest()) _, err = cmdC.GetOptions(nil)
if res.Error() == nil { if err == nil {
t.Error("Should have failed (option name collision with global options)") t.Error("Should have failed (option name collision with global options)")
} }
} }

View File

@ -3,7 +3,6 @@ package http
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -11,10 +10,9 @@ import (
"strings" "strings"
cmds "github.com/jbenet/go-ipfs/commands" cmds "github.com/jbenet/go-ipfs/commands"
u "github.com/jbenet/go-ipfs/util"
) )
var castError = errors.New("cast error")
const ( const (
ApiUrlFormat = "http://%s%s/%s?%s" ApiUrlFormat = "http://%s%s/%s?%s"
ApiPath = "/api/v0" // TODO: make configurable 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) { func (c *client) Send(req cmds.Request) (cmds.Response, error) {
var userEncoding string
if enc, found := req.Option(cmds.EncShort); found { // save user-provided encoding
var ok bool previousUserProvidedEncoding, found, err := req.Option(cmds.EncShort).String()
userEncoding, ok = enc.(string) if err != nil {
if !ok { return nil, err
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)
} }
// override with json to send to server
req.SetOption(cmds.EncShort, cmds.JSON)
query, inputStream, err := getQuery(req) query, inputStream, err := getQuery(req)
if err != nil { if err != nil {
return nil, err return nil, err
@ -60,19 +50,23 @@ func (c *client) Send(req cmds.Request) (cmds.Response, error) {
path := strings.Join(req.Path(), "/") path := strings.Join(req.Path(), "/")
url := fmt.Sprintf(ApiUrlFormat, c.serverAddress, ApiPath, path, query) url := fmt.Sprintf(ApiUrlFormat, c.serverAddress, ApiPath, path, query)
// TODO extract string const?
httpRes, err := http.Post(url, "application/octet-stream", inputStream) httpRes, err := http.Post(url, "application/octet-stream", inputStream)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// using the overridden JSON encoding in request
res, err := getResponse(httpRes, req) res, err := getResponse(httpRes, req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(userEncoding) > 0 { if found && len(previousUserProvidedEncoding) > 0 {
req.SetOption(cmds.EncShort, userEncoding) // reset to user provided encoding after sending request
req.SetOption(cmds.EncLong, userEncoding) // 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 return res, nil
@ -84,10 +78,7 @@ func getQuery(req cmds.Request) (string, io.Reader, error) {
query := url.Values{} query := url.Values{}
for k, v := range req.Options() { for k, v := range req.Options() {
str, ok := v.(string) str := fmt.Sprintf("%v", v)
if !ok {
return "", nil, castError
}
query.Set(k, str) query.Set(k, str)
} }
@ -103,7 +94,7 @@ func getQuery(req cmds.Request) (string, io.Reader, error) {
if argDef.Type == cmds.ArgString { if argDef.Type == cmds.ArgString {
str, ok := arg.(string) str, ok := arg.(string)
if !ok { if !ok {
return "", nil, castError return "", nil, u.ErrCast()
} }
query.Add("arg", str) query.Add("arg", str)
@ -115,7 +106,7 @@ func getQuery(req cmds.Request) (string, io.Reader, error) {
var ok bool var ok bool
inputStream, ok = arg.(io.Reader) inputStream, ok = arg.(io.Reader)
if !ok { 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 := httpRes.Header["Content-Type"][0]
contentType = strings.Split(contentType, ";")[0] contentType = strings.Split(contentType, ";")[0]
if contentType == "application/octet-stream" { if len(httpRes.Header.Get(streamHeader)) > 0 {
res.SetOutput(httpRes.Body) res.SetOutput(httpRes.Body)
return res, nil return res, nil
} }
@ -166,7 +157,7 @@ func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error
} else { } else {
v := req.Command().Type v := req.Command().Type
err = dec.Decode(&v) err = dec.Decode(&v)
if err != nil { if err != nil && err != io.EOF {
return nil, err return nil, err
} }

View File

@ -6,8 +6,11 @@ import (
"net/http" "net/http"
cmds "github.com/jbenet/go-ipfs/commands" cmds "github.com/jbenet/go-ipfs/commands"
u "github.com/jbenet/go-ipfs/util"
) )
var log = u.Logger("commands/http")
type Handler struct { type Handler struct {
ctx cmds.Context ctx cmds.Context
root *cmds.Command root *cmds.Command
@ -15,6 +18,8 @@ type Handler struct {
var ErrNotFound = errors.New("404 page not found") var ErrNotFound = errors.New("404 page not found")
const streamHeader = "X-Stream-Output"
var mimeTypes = map[string]string{ var mimeTypes = map[string]string{
cmds.JSON: "application/json", cmds.JSON: "application/json",
cmds.XML: "application/xml", 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) { func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Debug("Incoming API request: ", r.URL)
req, err := Parse(r, i.root) req, err := Parse(r, i.root)
if err != nil { if err != nil {
if err == ErrNotFound { 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 // set the Content-Type based on res output
if _, ok := res.Output().(io.Reader); ok { if _, ok := res.Output().(io.Reader); ok {
// TODO: set based on actual Content-Type of file // we don't set the Content-Type for streams, so that browsers can MIME-sniff the type themselves
w.Header().Set("Content-Type", "application/octet-stream") // 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 { } else {
enc, _ := req.Option(cmds.EncShort) enc, found, err := req.Option(cmds.EncShort).String()
encStr, ok := enc.(string) if err != nil || !found {
if !ok {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return
} }
mime := mimeTypes[encStr] mime := mimeTypes[enc]
w.Header().Set("Content-Type", mime) w.Header().Set("Content-Type", mime)
} }

View File

@ -39,20 +39,44 @@ func Parse(r *http.Request, root *cmds.Command) (cmds.Request, error) {
opts, stringArgs2 := parseOptions(r) opts, stringArgs2 := parseOptions(r)
stringArgs = append(stringArgs, stringArgs2...) stringArgs = append(stringArgs, stringArgs2...)
// Note that the argument handling here is dumb, it does not do any error-checking. // count required argument definitions
// (Arguments are further processed when the request is passed to the command to run) numRequired := 0
args := make([]interface{}, 0) for _, argDef := range cmd.Arguments {
if argDef.Required {
numRequired++
}
}
for _, arg := range cmd.Arguments { // count the number of provided argument values
if arg.Type == cmds.ArgString { valCount := len(stringArgs)
if arg.Variadic { // 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 { for _, s := range stringArgs {
args = append(args, s) args[valIndex] = s
valIndex++
} }
valCount -= len(stringArgs)
} else if len(stringArgs) > 0 { } else if len(stringArgs) > 0 {
args = append(args, stringArgs[0]) args[valIndex] = stringArgs[0]
stringArgs = stringArgs[1:] stringArgs = stringArgs[1:]
valIndex++
} else { } else {
break break
@ -60,11 +84,17 @@ func Parse(r *http.Request, root *cmds.Command) (cmds.Request, error) {
} else { } else {
// TODO: create multipart streams for file args // 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) err = cmd.CheckArguments(req)
if err != nil { if err != nil {

View File

@ -1,6 +1,10 @@
package commands package commands
import "reflect" import (
"reflect"
"github.com/jbenet/go-ipfs/util"
)
// Types of Command options // Types of Command options
const ( const (
@ -14,14 +18,120 @@ const (
// Option is used to specify a field that will be provided by a consumer // Option is used to specify a field that will be provided by a consumer
type Option struct { type Option struct {
Names []string // a list of unique names to Names []string // a list of unique names to
Type reflect.Kind // value must be this type 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) //Default interface{} // the default value (ignored if `Required` is true)
//Required bool // whether or not the option must be provided //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 // Flag names
const ( const (
EncShort = "enc" EncShort = "enc"
@ -30,7 +140,8 @@ const (
// options that are used by this package // options that are used by this package
var globalOptions = []Option{ 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 // the above array of Options, wrapped in a Command

36
commands/option_test.go Normal file
View 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.")
}
}

View File

@ -3,41 +3,87 @@ package commands
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"reflect" "reflect"
"strconv" "strconv"
"github.com/jbenet/go-ipfs/config" "github.com/jbenet/go-ipfs/config"
"github.com/jbenet/go-ipfs/core" "github.com/jbenet/go-ipfs/core"
u "github.com/jbenet/go-ipfs/util"
) )
type optMap map[string]interface{} type optMap map[string]interface{}
type Context struct { type Context struct {
ConfigRoot string 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 // Request represents a call to a command from a consumer
type Request interface { type Request interface {
Path() []string Path() []string
Option(name string) (interface{}, bool) Option(name string) *OptionValue
Options() map[string]interface{} Options() optMap
SetOption(name string, val interface{}) 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{} Arguments() []interface{} // TODO: make argument value type instead of using interface{}
Context() *Context Context() *Context
SetContext(Context) SetContext(Context)
Command() *Command Command() *Command
Cleanup() error
ConvertOptions(options map[string]Option) error ConvertOptions() error
} }
type request struct { type request struct {
path []string path []string
options optMap options optMap
arguments []interface{} arguments []interface{}
cmd *Command cmd *Command
ctx Context ctx Context
optionDefs map[string]Option
} }
// Path returns the command path of this request // 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. // Option returns the value of the option for given name.
func (r *request) Option(name string) (interface{}, bool) { func (r *request) Option(name string) *OptionValue {
val, err := r.options[name] val, found := r.options[name]
return val, err 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 // Options returns a copy of the option map
func (r *request) Options() map[string]interface{} { func (r *request) Options() optMap {
output := make(optMap) output := make(optMap)
for k, v := range r.options { for k, v := range r.options {
output[k] = v output[k] = v
@ -62,6 +129,21 @@ func (r *request) Options() map[string]interface{} {
// SetOption sets the value of the option for given name. // SetOption sets the value of the option for given name.
func (r *request) SetOption(name string, val interface{}) { 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 r.options[name] = val
} }
@ -82,6 +164,20 @@ func (r *request) Command() *Command {
return r.cmd 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) type converter func(string) (interface{}, error)
var converters = map[reflect.Kind]converter{ var converters = map[reflect.Kind]converter{
@ -92,48 +188,52 @@ var converters = map[reflect.Kind]converter{
return strconv.ParseBool(v) return strconv.ParseBool(v)
}, },
Int: func(v string) (interface{}, error) { 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) { 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) { Float: func(v string) (interface{}, error) {
return strconv.ParseFloat(v, 64) return strconv.ParseFloat(v, 64)
}, },
} }
func (r *request) ConvertOptions(options map[string]Option) error { func (r *request) ConvertOptions() error {
converted := make(map[string]interface{})
for k, v := range r.options { for k, v := range r.options {
opt, ok := options[k] opt, ok := r.optionDefs[k]
if !ok { if !ok {
continue continue
} }
kind := reflect.TypeOf(v).Kind() kind := reflect.TypeOf(v).Kind()
var value interface{}
if kind != opt.Type { if kind != opt.Type {
if kind == String { if kind == String {
convert := converters[opt.Type] convert := converters[opt.Type]
str, ok := v.(string) str, ok := v.(string)
if !ok { if !ok {
return errors.New("cast error") return u.ErrCast()
} }
val, err := convert(str) val, err := convert(str)
if err != nil { if err != nil {
return fmt.Errorf("Could not convert string value '%s' to type '%s'", return fmt.Errorf("Could not convert string value '%s' to type '%s'",
v, opt.Type.String()) v, opt.Type.String())
} }
value = val r.options[k] = val
} else { } else {
return fmt.Errorf("Option '%s' should be type '%s', but got type '%s'", return fmt.Errorf("Option '%s' should be type '%s', but got type '%s'",
k, opt.Type.String(), kind.String()) k, opt.Type.String(), kind.String())
} }
} else { } else {
value = v r.options[k] = v
} }
for _, name := range opt.Names { 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')", return fmt.Errorf("Duplicate command options were provided ('%s' and '%s')",
k, name) k, name)
} }
converted[name] = value
} }
} }
r.options = converted
return nil return nil
} }
// NewEmptyRequest initializes an empty request // NewEmptyRequest initializes an empty request
func NewEmptyRequest() 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 // 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 { if path == nil {
path = make([]string, 0) path = make([]string, 0)
} }
@ -166,5 +263,12 @@ func NewRequest(path []string, opts optMap, args []interface{}, cmd *Command) Re
if args == nil { if args == nil {
args = make([]interface{}, 0) 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
} }

View File

@ -40,12 +40,12 @@ const (
// TODO: support more encoding types // TODO: support more encoding types
) )
var marshallers = map[EncodingType]Marshaller{ var marshallers = map[EncodingType]Marshaler{
JSON: func(res Response) ([]byte, error) { JSON: func(res Response) ([]byte, error) {
if res.Error() != nil { 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) { XML: func(res Response) ([]byte, error) {
if res.Error() != nil { if res.Error() != nil {
@ -69,7 +69,7 @@ type Response interface {
Output() interface{} Output() interface{}
// Marshal marshals out the response into a buffer. It uses the EncodingType // 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) Marshal() ([]byte, error)
// Gets a io.Reader that reads the marshalled output // Gets a io.Reader that reads the marshalled output
@ -108,18 +108,26 @@ func (r *response) Marshal() ([]byte, error) {
return []byte{}, nil return []byte{}, nil
} }
enc, found := r.req.Option(EncShort) enc, found, err := r.req.Option(EncShort).String()
encStr, ok := enc.(string) if err != nil {
if !found || !ok || encStr == "" { return nil, err
}
if !found {
return nil, fmt.Errorf("No encoding type was specified") return nil, fmt.Errorf("No encoding type was specified")
} }
encType := EncodingType(strings.ToLower(encStr)) encType := EncodingType(strings.ToLower(enc))
var marshaller Marshaller // Special case: if text encoding and an error, just print it out.
if r.req.Command() != nil && r.req.Command().Marshallers != nil { if encType == Text && r.Error() != nil {
marshaller = r.req.Command().Marshallers[encType] 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 { if marshaller == nil {
var ok bool
marshaller, ok = marshallers[encType] marshaller, ok = marshallers[encType]
if !ok { if !ok {
return nil, fmt.Errorf("No marshaller found for encoding type '%s'", enc) return nil, fmt.Errorf("No marshaller found for encoding type '%s'", enc)

View File

@ -2,6 +2,7 @@ package commands
import ( import (
"fmt" "fmt"
"strings"
"testing" "testing"
) )
@ -11,42 +12,45 @@ type TestOutput struct {
} }
func TestMarshalling(t *testing.T) { func TestMarshalling(t *testing.T) {
req := NewEmptyRequest() cmd := &Command{}
opts, _ := cmd.GetOptions(nil)
req := NewRequest(nil, nil, nil, nil, opts)
res := NewResponse(req) res := NewResponse(req)
res.SetOutput(TestOutput{"beep", "boop", 1337}) res.SetOutput(TestOutput{"beep", "boop", 1337})
// get command global options so we can set the encoding option _, err := res.Marshal()
cmd := Command{}
options, err := cmd.GetOptions(nil)
if err != nil {
t.Error(err)
}
_, err = res.Marshal()
if err == nil { if err == nil {
t.Error("Should have failed (no encoding type specified in request)") t.Error("Should have failed (no encoding type specified in request)")
} }
req.SetOption(EncShort, JSON) req.SetOption(EncShort, JSON)
req.ConvertOptions(options)
bytes, err := res.Marshal() bytes, err := res.Marshal()
if err != nil { if err != nil {
t.Error(err, "Should have passed") t.Error(err, "Should have passed")
} }
output := string(bytes) 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") t.Error("Incorrect JSON output")
} }
res.SetError(fmt.Errorf("You broke something!"), ErrClient) res.SetError(fmt.Errorf("Oops!"), ErrClient)
bytes, err = res.Marshal() bytes, err = res.Marshal()
if err != nil { if err != nil {
t.Error("Should have passed") t.Error("Should have passed")
} }
output = string(bytes) 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") 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)
}

View File

@ -53,10 +53,25 @@ func WriteFile(filename string, buf []byte) error {
return err 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 // Encode configuration with JSON
func Encode(w io.Writer, value interface{}) error { func Encode(w io.Writer, value interface{}) error {
// need to prettyprint, hence MarshalIndent, instead of Encoder // need to prettyprint, hence MarshalIndent, instead of Encoder
buf, err := json.MarshalIndent(value, "", " ") buf, err := Marshal(value)
if err != nil { if err != nil {
return err return err
} }

View File

@ -8,7 +8,7 @@ import (
) )
// CurrentVersionNumber is the current application's version literal // 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 // Version regulates checking if the most recent version is run
type Version struct { type Version struct {

228
core/commands2/add.go Normal file
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
}

View 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())
}

View 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
View 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
View 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
}

View 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")
}

View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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{},
}

View File

@ -74,6 +74,8 @@ type IpfsNode struct {
Pinning pin.Pinner Pinning pin.Pinner
ctxc.ContextCloser ctxc.ContextCloser
onlineMode bool // alternatively, offline
} }
// NewIpfsNode constructs a new IpfsNode based on the given config. // 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. // derive this from a higher context.
ctx := context.TODO() ctx := context.TODO()
n = &IpfsNode{ n = &IpfsNode{
onlineMode: online,
Config: cfg, Config: cfg,
ContextCloser: ctxc.NewContextCloser(ctx, nil), ContextCloser: ctxc.NewContextCloser(ctx, nil),
} }
@ -172,6 +175,10 @@ func NewIpfsNode(cfg *config.Config, online bool) (n *IpfsNode, err error) {
return n, nil return n, nil
} }
func (n *IpfsNode) OnlineMode() bool {
return n.onlineMode
}
func initIdentity(cfg *config.Config, peers peer.Peerstore, online bool) (peer.Peer, error) { func initIdentity(cfg *config.Config, peers peer.Peerstore, online bool) (peer.Peer, error) {
if cfg.Identity.PeerID == "" { if cfg.Identity.PeerID == "" {
return nil, errors.New("Identity was not set in config (was ipfs init run?)") return nil, errors.New("Identity was not set in config (was ipfs init run?)")

25
daemon2/daemon.go Normal file
View 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
}
}

View File

@ -67,6 +67,7 @@ type DiagInfo struct {
Keys []string Keys []string
// How long this node has been running for // How long this node has been running for
// TODO rename Uptime
LifeSpan time.Duration LifeSpan time.Duration
// Incoming Bandwidth Usage // Incoming Bandwidth Usage

View File

@ -22,8 +22,10 @@ type handler struct {
func Serve(address ma.Multiaddr, node *core.IpfsNode) error { func Serve(address ma.Multiaddr, node *core.IpfsNode) error {
r := mux.NewRouter() r := mux.NewRouter()
handler := &handler{&ipfsHandler{node}} handler := &handler{&ipfsHandler{node}}
r.HandleFunc("/ipfs/", handler.postHandler).Methods("POST") r.HandleFunc("/ipfs/", handler.postHandler).Methods("POST")
r.PathPrefix("/ipfs/").Handler(handler).Methods("GET") r.PathPrefix("/ipfs/").Handler(handler).Methods("GET")
http.Handle("/", r) http.Handle("/", r)
_, host, err := manet.DialArgs(address) _, host, err := manet.DialArgs(address)

View File

@ -200,7 +200,7 @@ func CliCheckForUpdates(cfg *config.Config, confFile string) error {
// if config says not to, don't check for updates // if config says not to, don't check for updates
if !cfg.Version.ShouldCheckForUpdate() { if !cfg.Version.ShouldCheckForUpdate() {
log.Info("update checking disabled.") log.Info("update check skipped.")
return nil return nil
} }

View File

@ -1,8 +1,8 @@
package testutil package testutil
import ( import (
"testing"
crand "crypto/rand" crand "crypto/rand"
"testing"
"github.com/jbenet/go-ipfs/peer" "github.com/jbenet/go-ipfs/peer"

View File

@ -8,6 +8,7 @@ import (
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"runtime/debug"
"strings" "strings"
"time" "time"
@ -40,6 +41,14 @@ func TildeExpansion(filename string) (string, error) {
return homedir.Expand(filename) 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 // ExpandPathnames takes a set of paths and turns them into absolute paths
func ExpandPathnames(paths []string) ([]string, error) { func ExpandPathnames(paths []string) ([]string, error) {
var out []string var out []string