diff --git a/Makefile b/Makefile index dcb369941..69bb553c3 100644 --- a/Makefile +++ b/Makefile @@ -10,10 +10,14 @@ godep: vendor: godep godep save -r ./... - -install: +# TODO revert to `install` once new command refactoring is complete +install_1: cd cmd/ipfs && go install +# TODO remove once new command refactoring is complete +install_2: + cd cmd/ipfs2 && go install + test: test_go test_sharness test_go: diff --git a/cmd/ipfs2/.gitignore b/cmd/ipfs2/.gitignore new file mode 100644 index 000000000..263031f7f --- /dev/null +++ b/cmd/ipfs2/.gitignore @@ -0,0 +1,3 @@ +./ipfs +./ipfs.exe +./ipfs2 diff --git a/cmd/ipfs2/Makefile b/cmd/ipfs2/Makefile new file mode 100644 index 000000000..fccb80330 --- /dev/null +++ b/cmd/ipfs2/Makefile @@ -0,0 +1,7 @@ +all: install + +build: + go build + +install: build + go install diff --git a/cmd/ipfs2/README.md b/cmd/ipfs2/README.md new file mode 100644 index 000000000..5b47554b3 --- /dev/null +++ b/cmd/ipfs2/README.md @@ -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 Add an object to ipfs + cat Show ipfs object data + ls 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 " for more information about a command. +``` diff --git a/cmd/ipfs2/daemon.go b/cmd/ipfs2/daemon.go new file mode 100644 index 000000000..51c1cde49 --- /dev/null +++ b/cmd/ipfs2/daemon.go @@ -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 +} diff --git a/cmd/ipfs2/equinox.yaml b/cmd/ipfs2/equinox.yaml new file mode 100644 index 000000000..9e764e05c --- /dev/null +++ b/cmd/ipfs2/equinox.yaml @@ -0,0 +1,6 @@ +--- +equinox-account: CHANGEME +equinox-secret: CHANGEME +equinox-app: CHANGEME +channel: stable +private-key: equinox-priv diff --git a/cmd/ipfs2/init.go b/cmd/ipfs2/init.go new file mode 100644 index 000000000..6b04a4418 --- /dev/null +++ b/cmd/ipfs2/init.go @@ -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 +} diff --git a/cmd/ipfs2/ipfs.go b/cmd/ipfs2/ipfs.go new file mode 100644 index 000000000..5f349c19c --- /dev/null +++ b/cmd/ipfs2/ipfs.go @@ -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}, +} diff --git a/cmd/ipfs2/ipfsHandler.go b/cmd/ipfs2/ipfsHandler.go new file mode 100644 index 000000000..07a64724a --- /dev/null +++ b/cmd/ipfs2/ipfsHandler.go @@ -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/) +// (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())) +} diff --git a/cmd/ipfs2/main.go b/cmd/ipfs2/main.go new file mode 100644 index 000000000..4bbe1da92 --- /dev/null +++ b/cmd/ipfs2/main.go @@ -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) + } + }() +} diff --git a/cmd/ipfs2/main_test.go b/cmd/ipfs2/main_test.go new file mode 100644 index 000000000..efbb1b620 --- /dev/null +++ b/cmd/ipfs2/main_test.go @@ -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") + } +} diff --git a/cmd/ipfs2/tour.go b/cmd/ipfs2/tour.go new file mode 100644 index 000000000..94cd9aaf6 --- /dev/null +++ b/cmd/ipfs2/tour.go @@ -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) +} diff --git a/cmd/ipfs2/tour_test.go b/cmd/ipfs2/tour_test.go new file mode 100644 index 000000000..35e802278 --- /dev/null +++ b/cmd/ipfs2/tour_test.go @@ -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()) +} diff --git a/commands/argument.go b/commands/argument.go index d8b1605e9..fb4b74b74 100644 --- a/commands/argument.go +++ b/commands/argument.go @@ -8,8 +8,35 @@ const ( ) type Argument struct { - Name string - Type ArgumentType - Required bool - Variadic bool + Name string + Type ArgumentType + Required bool + Variadic bool + SupportsStdin bool + Description string +} + +func StringArg(name string, required, variadic bool, description string) Argument { + return Argument{ + Name: name, + Type: ArgString, + Required: required, + Variadic: variadic, + Description: description, + } +} + +func FileArg(name string, required, variadic bool, description string) Argument { + return Argument{ + Name: name, + Type: ArgFile, + Required: required, + Variadic: variadic, + Description: description, + } +} + +func (a Argument) EnableStdin() Argument { + a.SupportsStdin = true + return a } diff --git a/commands/cli/helptext.go b/commands/cli/helptext.go new file mode 100644 index 000000000..9ac20c820 --- /dev/null +++ b/commands/cli/helptext.go @@ -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 - Get value of +// ipfs config - Set value of to +// 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}} --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) +} diff --git a/commands/cli/parse.go b/commands/cli/parse.go index 1c2b11b86..17b5dab16 100644 --- a/commands/cli/parse.go +++ b/commands/cli/parse.go @@ -1,6 +1,7 @@ package cli import ( + "bytes" "errors" "fmt" "os" @@ -9,50 +10,50 @@ import ( cmds "github.com/jbenet/go-ipfs/commands" ) +// ErrInvalidSubcmd signals when the parse error is not found +var ErrInvalidSubcmd = errors.New("subcommand not found") + // Parse parses the input commandline string (cmd, flags, and args). // returns the corresponding command Request object. -func Parse(input []string, roots ...*cmds.Command) (cmds.Request, *cmds.Command, error) { - var root, cmd *cmds.Command - var path, stringArgs []string - var opts map[string]interface{} - +// Parse will search each root to find the one that best matches the requested subcommand. +func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *cmds.Command, []string, error) { // use the root that matches the longest path (most accurately matches request) - maxLength := 0 - for _, r := range roots { - p, i, c := parsePath(input, r) - o, s, err := parseOptions(i) - if err != nil { - return nil, nil, err - } - - length := len(p) - if length > maxLength { - maxLength = length - root = r - path = p - cmd = c - opts = o - stringArgs = s - } - } - - if maxLength == 0 { - return nil, nil, errors.New("Not a valid subcommand") - } - - args, err := parseArgs(stringArgs, cmd) + path, input, cmd := parsePath(input, root) + opts, stringArgs, err := parseOptions(input) if err != nil { - return nil, nil, err + return nil, cmd, path, err } - req := cmds.NewRequest(path, opts, args, cmd) + if len(path) == 0 { + return nil, nil, path, ErrInvalidSubcmd + } + + args, err := parseArgs(stringArgs, stdin, cmd.Arguments) + if err != nil { + return nil, cmd, path, err + } + + optDefs, err := root.GetOptions(path) + if err != nil { + return nil, cmd, path, err + } + + // check to make sure there aren't any undefined options + for k := range opts { + if _, found := optDefs[k]; !found { + err = fmt.Errorf("Unrecognized option: -%s", k) + return nil, cmd, path, err + } + } + + req := cmds.NewRequest(path, opts, args, cmd, optDefs) err = cmd.CheckArguments(req) if err != nil { - return nil, nil, err + return req, cmd, path, err } - return req, root, nil + return req, cmd, path, nil } // parsePath separates the command path and the opts and args from a command string @@ -116,25 +117,89 @@ func parseOptions(input []string) (map[string]interface{}, []string, error) { return opts, args, nil } -func parseArgs(stringArgs []string, cmd *cmds.Command) ([]interface{}, error) { - var argDef cmds.Argument - args := make([]interface{}, len(stringArgs)) +func parseArgs(stringArgs []string, stdin *os.File, arguments []cmds.Argument) ([]interface{}, error) { + // check if stdin is coming from terminal or is being piped in + if stdin != nil { + stat, err := stdin.Stat() + if err != nil { + return nil, err + } - for i, arg := range stringArgs { - if i < len(cmd.Arguments) { - argDef = cmd.Arguments[i] + // if stdin isn't a CharDevice, set it to nil + // (this means it is coming from terminal and we want to ignore it) + if (stat.Mode() & os.ModeCharDevice) != 0 { + stdin = nil + } + } + + // count required argument definitions + lenRequired := 0 + for _, argDef := range arguments { + if argDef.Required { + lenRequired++ + } + } + + valCount := len(stringArgs) + if stdin != nil { + valCount += 1 + } + + args := make([]interface{}, 0, valCount) + + argDefIndex := 0 // the index of the current argument definition + for i := 0; i < valCount; i++ { + // get the argument definiton (should be arguments[argDefIndex], + // but if argDefIndex > len(arguments) we use the last argument definition) + var argDef cmds.Argument + if argDefIndex < len(arguments) { + argDef = arguments[argDefIndex] + } else if len(arguments) > 0 { + argDef = arguments[len(arguments)-1] + } + + // skip optional argument definitions if there aren't sufficient remaining values + if valCount-i <= lenRequired && !argDef.Required { + continue + } else if argDef.Required { + lenRequired-- } if argDef.Type == cmds.ArgString { - args[i] = arg + if stdin == nil { + // add string values + args = append(args, stringArgs[0]) + stringArgs = stringArgs[1:] - } else { - in, err := os.Open(arg) - if err != nil { - return nil, err + } else if argDef.SupportsStdin { + // if we have a stdin, read it in and use the data as a string value + var buf bytes.Buffer + _, err := buf.ReadFrom(stdin) + if err != nil { + return nil, err + } + args = append(args, buf.String()) + stdin = nil + } + + } else if argDef.Type == cmds.ArgFile { + if stdin == nil { + // treat stringArg values as file paths + file, err := os.Open(stringArgs[0]) + if err != nil { + return nil, err + } + args = append(args, file) + stringArgs = stringArgs[1:] + + } else if argDef.SupportsStdin { + // if we have a stdin, use that as a reader + args = append(args, stdin) + stdin = nil } - args[i] = in } + + argDefIndex++ } return args, nil diff --git a/commands/command.go b/commands/command.go index cac29918e..383e9f217 100644 --- a/commands/command.go +++ b/commands/command.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io" + "reflect" "strings" u "github.com/jbenet/go-ipfs/util" @@ -13,11 +14,31 @@ var log = u.Logger("command") // Function is the type of function that Commands use. // It reads from the Request, and writes results to the Response. -type Function func(Response, Request) +type Function func(Request) (interface{}, error) -// Marshaller is a function that takes in a Response, and returns a marshalled []byte +// Marshaler is a function that takes in a Response, and returns a marshalled []byte // (or an error on failure) -type Marshaller func(Response) ([]byte, error) +type Marshaler func(Response) ([]byte, error) + +// MarshalerMap is a map of Marshaler functions, keyed by EncodingType +// (or an error on failure) +type MarshalerMap map[EncodingType]Marshaler + +// HelpText is a set of strings used to generate command help text. The help +// text follows formats similar to man pages, but not exactly the same. +type HelpText struct { + // required + Tagline string // used in + ShortDescription string // used in DESCRIPTION + Synopsis string // showcasing the cmd + + // optional - whole section overrides + Usage string // overrides USAGE section + LongDescription string // overrides DESCRIPTION section + Options string // overrides OPTIONS section + Arguments string // overrides ARGUMENTS section + Subcommands string // overrides SUBCOMMANDS section +} // TODO: check Argument definitions when creating a Command // (might need to use a Command constructor) @@ -28,19 +49,27 @@ type Marshaller func(Response) ([]byte, error) // Command is a runnable command, with input arguments and options (flags). // It can also have Subcommands, to group units of work into sets. type Command struct { - Help string - Options []Option - Arguments []Argument - Run Function - Marshallers map[EncodingType]Marshaller + Options []Option + Arguments []Argument + Run Function + Marshalers map[EncodingType]Marshaler + Helptext HelpText + + // Type describes the type of the output of the Command's Run Function. + // In precise terms, the value of Type is an instance of the return type of + // the Run Function. + // + // ie. If command Run returns &Block{}, then Command.Type == &Block{} Type interface{} Subcommands map[string]*Command } // ErrNotCallable signals a command that cannot be called. -var ErrNotCallable = errors.New("This command can't be called directly. Try one of its subcommands.") +var ErrNotCallable = ClientError("This command can't be called directly. Try one of its subcommands.") -var ErrNoFormatter = errors.New("This command cannot be formatted to plain text") +var ErrNoFormatter = ClientError("This command cannot be formatted to plain text") + +var ErrIncorrectType = errors.New("The command returned a value with a different type than expected") // Call invokes the command for the given Request func (c *Command) Call(req Request) Response { @@ -64,20 +93,47 @@ func (c *Command) Call(req Request) Response { return res } - options, err := c.GetOptions(req.Path()) + err = req.ConvertOptions() if err != nil { res.SetError(err, ErrClient) return res } - err = req.ConvertOptions(options) + output, err := cmd.Run(req) if err != nil { - res.SetError(err, ErrClient) + // if returned error is a commands.Error, use its error code + // otherwise, just default the code to ErrNormal + switch e := err.(type) { + case *Error: + res.SetError(e, e.Code) + case Error: + res.SetError(e, e.Code) + default: + res.SetError(err, ErrNormal) + } return res } - cmd.Run(res, req) + // If the command specified an output type, ensure the actual value returned is of that type + if cmd.Type != nil { + definedType := reflect.ValueOf(cmd.Type).Type() + actualType := reflect.ValueOf(output).Type() + if definedType != actualType { + res.SetError(ErrIncorrectType, ErrNormal) + return res + } + } + + // clean up the request (close the readers, e.g. fileargs) + // NOTE: this means commands can't expect to keep reading after cmd.Run returns (in a goroutine) + err = req.Cleanup() + if err != nil { + res.SetError(err, ErrNormal) + return res + } + + res.SetOutput(output) return res } @@ -149,13 +205,27 @@ func (c *Command) CheckArguments(req Request) error { return fmt.Errorf("Expected %v arguments, got %v", len(argDefs), len(args)) } + // count required argument definitions + numRequired := 0 + for _, argDef := range c.Arguments { + if argDef.Required { + numRequired++ + } + } + // iterate over the arg definitions - for i, argDef := range c.Arguments { + valueIndex := 0 // the index of the current value (in `args`) + for _, argDef := range c.Arguments { + // skip optional argument definitions if there aren't sufficient remaining values + if len(args)-valueIndex <= numRequired && !argDef.Required { + continue + } // the value for this argument definition. can be nil if it wasn't provided by the caller var v interface{} - if i < len(args) { - v = args[i] + if valueIndex < len(args) { + v = args[valueIndex] + valueIndex++ } err := checkArgValue(v, argDef) @@ -164,8 +234,8 @@ func (c *Command) CheckArguments(req Request) error { } // any additional values are for the variadic arg definition - if argDef.Variadic && i < len(args)-1 { - for _, val := range args[i+1:] { + if argDef.Variadic && valueIndex < len(args)-1 { + for _, val := range args[valueIndex:] { err := checkArgValue(val, argDef) if err != nil { return err @@ -207,3 +277,7 @@ func checkArgValue(v interface{}, def Argument) error { return nil } + +func ClientError(msg string) error { + return &Error{Code: ErrClient, Message: msg} +} diff --git a/commands/command_test.go b/commands/command_test.go index c41f1ce96..44a39d0c1 100644 --- a/commands/command_test.go +++ b/commands/command_test.go @@ -2,38 +2,36 @@ package commands import "testing" +func noop(req Request) (interface{}, error) { + return nil, nil +} + func TestOptionValidation(t *testing.T) { cmd := Command{ Options: []Option{ - Option{[]string{"b", "beep"}, Int}, - Option{[]string{"B", "boop"}, String}, + Option{[]string{"b", "beep"}, Int, "enables beeper"}, + Option{[]string{"B", "boop"}, String, "password for booper"}, }, - Run: func(res Response, req Request) {}, + Run: noop, } - req := NewEmptyRequest() - req.SetOption("beep", 5) - req.SetOption("b", 10) + opts, _ := cmd.GetOptions(nil) + + req := NewRequest(nil, nil, nil, nil, opts) + req.SetOption("beep", true) res := cmd.Call(req) - if res.Error() == nil { - t.Error("Should have failed (duplicate options)") - } - - req = NewEmptyRequest() - req.SetOption("beep", "foo") - res = cmd.Call(req) if res.Error() == nil { t.Error("Should have failed (incorrect type)") } - req = NewEmptyRequest() + req = NewRequest(nil, nil, nil, nil, opts) req.SetOption("beep", 5) res = cmd.Call(req) if res.Error() != nil { t.Error(res.Error(), "Should have passed") } - req = NewEmptyRequest() + req = NewRequest(nil, nil, nil, nil, opts) req.SetOption("beep", 5) req.SetOption("boop", "test") res = cmd.Call(req) @@ -41,7 +39,7 @@ func TestOptionValidation(t *testing.T) { t.Error("Should have passed") } - req = NewEmptyRequest() + req = NewRequest(nil, nil, nil, nil, opts) req.SetOption("b", 5) req.SetOption("B", "test") res = cmd.Call(req) @@ -49,48 +47,46 @@ func TestOptionValidation(t *testing.T) { t.Error("Should have passed") } - req = NewEmptyRequest() + req = NewRequest(nil, nil, nil, nil, opts) req.SetOption("foo", 5) res = cmd.Call(req) if res.Error() != nil { t.Error("Should have passed") } - req = NewEmptyRequest() + req = NewRequest(nil, nil, nil, nil, opts) req.SetOption(EncShort, "json") res = cmd.Call(req) if res.Error() != nil { t.Error("Should have passed") } - req = NewEmptyRequest() + req = NewRequest(nil, nil, nil, nil, opts) req.SetOption("b", "100") res = cmd.Call(req) if res.Error() != nil { t.Error("Should have passed") } - req = NewEmptyRequest() + req = NewRequest(nil, nil, nil, nil, opts) req.SetOption("b", ":)") res = cmd.Call(req) if res.Error() == nil { - t.Error(res.Error(), "Should have failed (string value not convertible to int)") + t.Error("Should have failed (string value not convertible to int)") } } func TestRegistration(t *testing.T) { - noop := func(res Response, req Request) {} - cmdA := &Command{ Options: []Option{ - Option{[]string{"beep"}, Int}, + Option{[]string{"beep"}, Int, "number of beeps"}, }, Run: noop, } cmdB := &Command{ Options: []Option{ - Option{[]string{"beep"}, Int}, + Option{[]string{"beep"}, Int, "number of beeps"}, }, Run: noop, Subcommands: map[string]*Command{ @@ -100,18 +96,19 @@ func TestRegistration(t *testing.T) { cmdC := &Command{ Options: []Option{ - Option{[]string{"encoding"}, String}, + Option{[]string{"encoding"}, String, "data encoding type"}, }, Run: noop, } - res := cmdB.Call(NewRequest([]string{"a"}, nil, nil, nil)) - if res.Error() == nil { + path := []string{"a"} + _, err := cmdB.GetOptions(path) + if err == nil { t.Error("Should have failed (option name collision)") } - res = cmdC.Call(NewEmptyRequest()) - if res.Error() == nil { + _, err = cmdC.GetOptions(nil) + if err == nil { t.Error("Should have failed (option name collision with global options)") } } diff --git a/commands/http/client.go b/commands/http/client.go index 748f50365..ddf4e5d80 100644 --- a/commands/http/client.go +++ b/commands/http/client.go @@ -3,7 +3,6 @@ package http import ( "bytes" "encoding/json" - "errors" "fmt" "io" "net/http" @@ -11,10 +10,9 @@ import ( "strings" cmds "github.com/jbenet/go-ipfs/commands" + u "github.com/jbenet/go-ipfs/util" ) -var castError = errors.New("cast error") - const ( ApiUrlFormat = "http://%s%s/%s?%s" ApiPath = "/api/v0" // TODO: make configurable @@ -34,24 +32,16 @@ func NewClient(address string) Client { } func (c *client) Send(req cmds.Request) (cmds.Response, error) { - var userEncoding string - if enc, found := req.Option(cmds.EncShort); found { - var ok bool - userEncoding, ok = enc.(string) - if !ok { - return nil, castError - } - req.SetOption(cmds.EncShort, cmds.JSON) - } else { - var ok bool - enc, _ := req.Option(cmds.EncLong) - userEncoding, ok = enc.(string) - if !ok { - return nil, castError - } - req.SetOption(cmds.EncLong, cmds.JSON) + + // save user-provided encoding + previousUserProvidedEncoding, found, err := req.Option(cmds.EncShort).String() + if err != nil { + return nil, err } + // override with json to send to server + req.SetOption(cmds.EncShort, cmds.JSON) + query, inputStream, err := getQuery(req) if err != nil { return nil, err @@ -60,19 +50,23 @@ func (c *client) Send(req cmds.Request) (cmds.Response, error) { path := strings.Join(req.Path(), "/") url := fmt.Sprintf(ApiUrlFormat, c.serverAddress, ApiPath, path, query) + // TODO extract string const? httpRes, err := http.Post(url, "application/octet-stream", inputStream) if err != nil { return nil, err } + // using the overridden JSON encoding in request res, err := getResponse(httpRes, req) if err != nil { return nil, err } - if len(userEncoding) > 0 { - req.SetOption(cmds.EncShort, userEncoding) - req.SetOption(cmds.EncLong, userEncoding) + if found && len(previousUserProvidedEncoding) > 0 { + // reset to user provided encoding after sending request + // NB: if user has provided an encoding but it is the empty string, + // still leave it as JSON. + req.SetOption(cmds.EncShort, previousUserProvidedEncoding) } return res, nil @@ -84,10 +78,7 @@ func getQuery(req cmds.Request) (string, io.Reader, error) { query := url.Values{} for k, v := range req.Options() { - str, ok := v.(string) - if !ok { - return "", nil, castError - } + str := fmt.Sprintf("%v", v) query.Set(k, str) } @@ -103,7 +94,7 @@ func getQuery(req cmds.Request) (string, io.Reader, error) { if argDef.Type == cmds.ArgString { str, ok := arg.(string) if !ok { - return "", nil, castError + return "", nil, u.ErrCast() } query.Add("arg", str) @@ -115,7 +106,7 @@ func getQuery(req cmds.Request) (string, io.Reader, error) { var ok bool inputStream, ok = arg.(io.Reader) if !ok { - return "", nil, castError + return "", nil, u.ErrCast() } } } @@ -131,7 +122,7 @@ func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error contentType := httpRes.Header["Content-Type"][0] contentType = strings.Split(contentType, ";")[0] - if contentType == "application/octet-stream" { + if len(httpRes.Header.Get(streamHeader)) > 0 { res.SetOutput(httpRes.Body) return res, nil } @@ -166,7 +157,7 @@ func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error } else { v := req.Command().Type err = dec.Decode(&v) - if err != nil { + if err != nil && err != io.EOF { return nil, err } diff --git a/commands/http/handler.go b/commands/http/handler.go index 8ac3e987f..2a2e3ff8c 100644 --- a/commands/http/handler.go +++ b/commands/http/handler.go @@ -6,8 +6,11 @@ import ( "net/http" cmds "github.com/jbenet/go-ipfs/commands" + u "github.com/jbenet/go-ipfs/util" ) +var log = u.Logger("commands/http") + type Handler struct { ctx cmds.Context root *cmds.Command @@ -15,6 +18,8 @@ type Handler struct { var ErrNotFound = errors.New("404 page not found") +const streamHeader = "X-Stream-Output" + var mimeTypes = map[string]string{ cmds.JSON: "application/json", cmds.XML: "application/xml", @@ -26,6 +31,8 @@ func NewHandler(ctx cmds.Context, root *cmds.Command) *Handler { } func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Debug("Incoming API request: ", r.URL) + req, err := Parse(r, i.root) if err != nil { if err == ErrNotFound { @@ -43,16 +50,19 @@ func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // set the Content-Type based on res output if _, ok := res.Output().(io.Reader); ok { - // TODO: set based on actual Content-Type of file - w.Header().Set("Content-Type", "application/octet-stream") + // we don't set the Content-Type for streams, so that browsers can MIME-sniff the type themselves + // we set this header so clients have a way to know this is an output stream + // (not marshalled command output) + // TODO: set a specific Content-Type if the command response needs it to be a certain type + w.Header().Set(streamHeader, "1") + } else { - enc, _ := req.Option(cmds.EncShort) - encStr, ok := enc.(string) - if !ok { + enc, found, err := req.Option(cmds.EncShort).String() + if err != nil || !found { w.WriteHeader(http.StatusInternalServerError) return } - mime := mimeTypes[encStr] + mime := mimeTypes[enc] w.Header().Set("Content-Type", mime) } diff --git a/commands/http/parse.go b/commands/http/parse.go index c9168f3e3..a59bb3b78 100644 --- a/commands/http/parse.go +++ b/commands/http/parse.go @@ -39,20 +39,44 @@ func Parse(r *http.Request, root *cmds.Command) (cmds.Request, error) { opts, stringArgs2 := parseOptions(r) stringArgs = append(stringArgs, stringArgs2...) - // Note that the argument handling here is dumb, it does not do any error-checking. - // (Arguments are further processed when the request is passed to the command to run) - args := make([]interface{}, 0) + // count required argument definitions + numRequired := 0 + for _, argDef := range cmd.Arguments { + if argDef.Required { + numRequired++ + } + } - for _, arg := range cmd.Arguments { - if arg.Type == cmds.ArgString { - if arg.Variadic { + // count the number of provided argument values + valCount := len(stringArgs) + // TODO: add total number of parts in request body (instead of just 1 if body is present) + if r.Body != nil && r.ContentLength != 0 { + valCount += 1 + } + + args := make([]interface{}, valCount) + + valIndex := 0 + for _, argDef := range cmd.Arguments { + // skip optional argument definitions if there aren't sufficient remaining values + if valCount-valIndex <= numRequired && !argDef.Required { + continue + } else if argDef.Required { + numRequired-- + } + + if argDef.Type == cmds.ArgString { + if argDef.Variadic { for _, s := range stringArgs { - args = append(args, s) + args[valIndex] = s + valIndex++ } + valCount -= len(stringArgs) } else if len(stringArgs) > 0 { - args = append(args, stringArgs[0]) + args[valIndex] = stringArgs[0] stringArgs = stringArgs[1:] + valIndex++ } else { break @@ -60,11 +84,17 @@ func Parse(r *http.Request, root *cmds.Command) (cmds.Request, error) { } else { // TODO: create multipart streams for file args - args = append(args, r.Body) + args[valIndex] = r.Body + valIndex++ } } - req := cmds.NewRequest(path, opts, args, cmd) + optDefs, err := root.GetOptions(path) + if err != nil { + return nil, err + } + + req := cmds.NewRequest(path, opts, args, cmd, optDefs) err = cmd.CheckArguments(req) if err != nil { diff --git a/commands/option.go b/commands/option.go index 8dbf3c1bd..3a679d9f9 100644 --- a/commands/option.go +++ b/commands/option.go @@ -1,6 +1,10 @@ package commands -import "reflect" +import ( + "reflect" + + "github.com/jbenet/go-ipfs/util" +) // Types of Command options const ( @@ -14,14 +18,120 @@ const ( // Option is used to specify a field that will be provided by a consumer type Option struct { - Names []string // a list of unique names to - Type reflect.Kind // value must be this type + Names []string // a list of unique names to + Type reflect.Kind // value must be this type + Description string // a short string to describe this option - // TODO: add more features(?): + // MAYBE_TODO: add more features(?): //Default interface{} // the default value (ignored if `Required` is true) //Required bool // whether or not the option must be provided } +// constructor helper functions +func NewOption(kind reflect.Kind, names ...string) Option { + if len(names) < 2 { + // FIXME(btc) don't panic (fix_before_merge) + panic("Options require at least two string values (name and description)") + } + + desc := names[len(names)-1] + names = names[:len(names)-1] + + return Option{ + Names: names, + Type: kind, + Description: desc, + } +} + +// TODO handle description separately. this will take care of the panic case in +// NewOption + +// For all func {Type}Option(...string) functions, the last variadic argument +// is treated as the description field. + +func BoolOption(names ...string) Option { + return NewOption(Bool, names...) +} +func IntOption(names ...string) Option { + return NewOption(Int, names...) +} +func UintOption(names ...string) Option { + return NewOption(Uint, names...) +} +func FloatOption(names ...string) Option { + return NewOption(Float, names...) +} +func StringOption(names ...string) Option { + return NewOption(String, names...) +} + +type OptionValue struct { + value interface{} + found bool +} + +// Found returns true if the option value was provided by the user (not a default value) +func (ov OptionValue) Found() bool { + return ov.found +} + +// value accessor methods, gets the value as a certain type +func (ov OptionValue) Bool() (value bool, found bool, err error) { + if !ov.found { + return false, false, nil + } + val, ok := ov.value.(bool) + if !ok { + err = util.ErrCast() + } + return val, ov.found, err +} + +func (ov OptionValue) Int() (value int, found bool, err error) { + if !ov.found { + return 0, false, nil + } + val, ok := ov.value.(int) + if !ok { + err = util.ErrCast() + } + return val, ov.found, err +} + +func (ov OptionValue) Uint() (value uint, found bool, err error) { + if !ov.found { + return 0, false, nil + } + val, ok := ov.value.(uint) + if !ok { + err = util.ErrCast() + } + return val, ov.found, err +} + +func (ov OptionValue) Float() (value float64, found bool, err error) { + if !ov.found { + return 0, false, nil + } + val, ok := ov.value.(float64) + if !ok { + err = util.ErrCast() + } + return val, ov.found, err +} + +func (ov OptionValue) String() (value string, found bool, err error) { + if !ov.found { + return "", false, nil + } + val, ok := ov.value.(string) + if !ok { + err = util.ErrCast() + } + return val, ov.found, err +} + // Flag names const ( EncShort = "enc" @@ -30,7 +140,8 @@ const ( // options that are used by this package var globalOptions = []Option{ - Option{[]string{EncShort, EncLong}, String}, + Option{[]string{EncShort, EncLong}, String, + "The encoding type the output should be encoded with (json, xml, or text)"}, } // the above array of Options, wrapped in a Command diff --git a/commands/option_test.go b/commands/option_test.go new file mode 100644 index 000000000..1ca612ee0 --- /dev/null +++ b/commands/option_test.go @@ -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.") + } +} diff --git a/commands/request.go b/commands/request.go index 40eda9fc6..5bdfe7ade 100644 --- a/commands/request.go +++ b/commands/request.go @@ -3,41 +3,87 @@ package commands import ( "errors" "fmt" + "io" "reflect" "strconv" "github.com/jbenet/go-ipfs/config" "github.com/jbenet/go-ipfs/core" + u "github.com/jbenet/go-ipfs/util" ) type optMap map[string]interface{} type Context struct { ConfigRoot string - Config *config.Config - Node *core.IpfsNode + + config *config.Config + LoadConfig func(path string) (*config.Config, error) + + node *core.IpfsNode + ConstructNode func() (*core.IpfsNode, error) +} + +// GetConfig returns the config of the current Command exection +// context. It may load it with the providied function. +func (c *Context) GetConfig() (*config.Config, error) { + var err error + if c.config == nil { + if c.LoadConfig == nil { + return nil, errors.New("nil LoadConfig function") + } + c.config, err = c.LoadConfig(c.ConfigRoot) + } + return c.config, err +} + +// GetNode returns the node of the current Command exection +// context. It may construct it with the providied function. +func (c *Context) GetNode() (*core.IpfsNode, error) { + var err error + if c.node == nil { + if c.ConstructNode == nil { + return nil, errors.New("nil ConstructNode function") + } + c.node, err = c.ConstructNode() + } + return c.node, err +} + +// NodeWithoutConstructing returns the underlying node variable +// so that clients may close it. +func (c *Context) NodeWithoutConstructing() *core.IpfsNode { + return c.node } // Request represents a call to a command from a consumer type Request interface { Path() []string - Option(name string) (interface{}, bool) - Options() map[string]interface{} + Option(name string) *OptionValue + Options() optMap SetOption(name string, val interface{}) + + // Arguments() returns user provided arguments as declared on the Command. + // + // NB: `io.Reader`s returned by Arguments() are owned by the library. + // Readers are not guaranteed to remain open after the Command's Run + // function returns. Arguments() []interface{} // TODO: make argument value type instead of using interface{} Context() *Context SetContext(Context) Command() *Command + Cleanup() error - ConvertOptions(options map[string]Option) error + ConvertOptions() error } type request struct { - path []string - options optMap - arguments []interface{} - cmd *Command - ctx Context + path []string + options optMap + arguments []interface{} + cmd *Command + ctx Context + optionDefs map[string]Option } // Path returns the command path of this request @@ -46,13 +92,34 @@ func (r *request) Path() []string { } // Option returns the value of the option for given name. -func (r *request) Option(name string) (interface{}, bool) { - val, err := r.options[name] - return val, err +func (r *request) Option(name string) *OptionValue { + val, found := r.options[name] + if found { + return &OptionValue{val, found} + } + + // if a value isn't defined for that name, we will try to look it up by its aliases + + // find the option with the specified name + option, found := r.optionDefs[name] + if !found { + return nil + } + + // try all the possible names, break if we find a value + for _, n := range option.Names { + val, found = r.options[n] + if found { + return &OptionValue{val, found} + } + } + + // MAYBE_TODO: use default value instead of nil + return &OptionValue{nil, false} } // Options returns a copy of the option map -func (r *request) Options() map[string]interface{} { +func (r *request) Options() optMap { output := make(optMap) for k, v := range r.options { output[k] = v @@ -62,6 +129,21 @@ func (r *request) Options() map[string]interface{} { // SetOption sets the value of the option for given name. func (r *request) SetOption(name string, val interface{}) { + // find the option with the specified name + option, found := r.optionDefs[name] + if !found { + return + } + + // try all the possible names, if we already have a value then set over it + for _, n := range option.Names { + _, found := r.options[n] + if found { + r.options[n] = val + return + } + } + r.options[name] = val } @@ -82,6 +164,20 @@ func (r *request) Command() *Command { return r.cmd } +func (r *request) Cleanup() error { + for _, arg := range r.arguments { + closer, ok := arg.(io.Closer) + if ok { + err := closer.Close() + if err != nil { + return err + } + } + } + + return nil +} + type converter func(string) (interface{}, error) var converters = map[reflect.Kind]converter{ @@ -92,48 +188,52 @@ var converters = map[reflect.Kind]converter{ return strconv.ParseBool(v) }, Int: func(v string) (interface{}, error) { - return strconv.ParseInt(v, 0, 32) + val, err := strconv.ParseInt(v, 0, 32) + if err != nil { + return nil, err + } + return int(val), err }, Uint: func(v string) (interface{}, error) { - return strconv.ParseInt(v, 0, 32) + val, err := strconv.ParseUint(v, 0, 32) + if err != nil { + return nil, err + } + return int(val), err }, Float: func(v string) (interface{}, error) { return strconv.ParseFloat(v, 64) }, } -func (r *request) ConvertOptions(options map[string]Option) error { - converted := make(map[string]interface{}) - +func (r *request) ConvertOptions() error { for k, v := range r.options { - opt, ok := options[k] + opt, ok := r.optionDefs[k] if !ok { continue } kind := reflect.TypeOf(v).Kind() - var value interface{} - if kind != opt.Type { if kind == String { convert := converters[opt.Type] str, ok := v.(string) if !ok { - return errors.New("cast error") + return u.ErrCast() } val, err := convert(str) if err != nil { return fmt.Errorf("Could not convert string value '%s' to type '%s'", v, opt.Type.String()) } - value = val + r.options[k] = val } else { return fmt.Errorf("Option '%s' should be type '%s', but got type '%s'", k, opt.Type.String(), kind.String()) } } else { - value = v + r.options[k] = v } for _, name := range opt.Names { @@ -141,22 +241,19 @@ func (r *request) ConvertOptions(options map[string]Option) error { return fmt.Errorf("Duplicate command options were provided ('%s' and '%s')", k, name) } - - converted[name] = value } } - r.options = converted return nil } // NewEmptyRequest initializes an empty request func NewEmptyRequest() Request { - return NewRequest(nil, nil, nil, nil) + return NewRequest(nil, nil, nil, nil, nil) } // NewRequest returns a request initialized with given arguments -func NewRequest(path []string, opts optMap, args []interface{}, cmd *Command) Request { +func NewRequest(path []string, opts optMap, args []interface{}, cmd *Command, optDefs map[string]Option) Request { if path == nil { path = make([]string, 0) } @@ -166,5 +263,12 @@ func NewRequest(path []string, opts optMap, args []interface{}, cmd *Command) Re if args == nil { args = make([]interface{}, 0) } - return &request{path, opts, args, cmd, Context{}} + if optDefs == nil { + optDefs = make(map[string]Option) + } + + req := &request{path, opts, args, cmd, Context{}, optDefs} + req.ConvertOptions() + + return req } diff --git a/commands/response.go b/commands/response.go index c79734528..7e12e5922 100644 --- a/commands/response.go +++ b/commands/response.go @@ -40,12 +40,12 @@ const ( // TODO: support more encoding types ) -var marshallers = map[EncodingType]Marshaller{ +var marshallers = map[EncodingType]Marshaler{ JSON: func(res Response) ([]byte, error) { if res.Error() != nil { - return json.Marshal(res.Error()) + return json.MarshalIndent(res.Error(), "", " ") } - return json.Marshal(res.Output()) + return json.MarshalIndent(res.Output(), "", " ") }, XML: func(res Response) ([]byte, error) { if res.Error() != nil { @@ -69,7 +69,7 @@ type Response interface { Output() interface{} // Marshal marshals out the response into a buffer. It uses the EncodingType - // on the Request to chose a Marshaller (Codec). + // on the Request to chose a Marshaler (Codec). Marshal() ([]byte, error) // Gets a io.Reader that reads the marshalled output @@ -108,18 +108,26 @@ func (r *response) Marshal() ([]byte, error) { return []byte{}, nil } - enc, found := r.req.Option(EncShort) - encStr, ok := enc.(string) - if !found || !ok || encStr == "" { + enc, found, err := r.req.Option(EncShort).String() + if err != nil { + return nil, err + } + if !found { return nil, fmt.Errorf("No encoding type was specified") } - encType := EncodingType(strings.ToLower(encStr)) + encType := EncodingType(strings.ToLower(enc)) - var marshaller Marshaller - if r.req.Command() != nil && r.req.Command().Marshallers != nil { - marshaller = r.req.Command().Marshallers[encType] + // Special case: if text encoding and an error, just print it out. + if encType == Text && r.Error() != nil { + return []byte(r.Error().Error()), nil + } + + var marshaller Marshaler + if r.req.Command() != nil && r.req.Command().Marshalers != nil { + marshaller = r.req.Command().Marshalers[encType] } if marshaller == nil { + var ok bool marshaller, ok = marshallers[encType] if !ok { return nil, fmt.Errorf("No marshaller found for encoding type '%s'", enc) diff --git a/commands/response_test.go b/commands/response_test.go index ef6240673..6bc7417ea 100644 --- a/commands/response_test.go +++ b/commands/response_test.go @@ -2,6 +2,7 @@ package commands import ( "fmt" + "strings" "testing" ) @@ -11,42 +12,45 @@ type TestOutput struct { } func TestMarshalling(t *testing.T) { - req := NewEmptyRequest() + cmd := &Command{} + opts, _ := cmd.GetOptions(nil) + + req := NewRequest(nil, nil, nil, nil, opts) res := NewResponse(req) res.SetOutput(TestOutput{"beep", "boop", 1337}) - // get command global options so we can set the encoding option - cmd := Command{} - options, err := cmd.GetOptions(nil) - if err != nil { - t.Error(err) - } - - _, err = res.Marshal() + _, err := res.Marshal() if err == nil { t.Error("Should have failed (no encoding type specified in request)") } req.SetOption(EncShort, JSON) - req.ConvertOptions(options) bytes, err := res.Marshal() if err != nil { t.Error(err, "Should have passed") } output := string(bytes) - if output != "{\"Foo\":\"beep\",\"Bar\":\"boop\",\"Baz\":1337}" { + if removeWhitespace(output) != "{\"Foo\":\"beep\",\"Bar\":\"boop\",\"Baz\":1337}" { t.Error("Incorrect JSON output") } - res.SetError(fmt.Errorf("You broke something!"), ErrClient) + res.SetError(fmt.Errorf("Oops!"), ErrClient) bytes, err = res.Marshal() if err != nil { t.Error("Should have passed") } output = string(bytes) - if output != "{\"Message\":\"You broke something!\",\"Code\":1}" { + fmt.Println(removeWhitespace(output)) + if removeWhitespace(output) != "{\"Message\":\"Oops!\",\"Code\":1}" { t.Error("Incorrect JSON output") } } + +func removeWhitespace(input string) string { + input = strings.Replace(input, " ", "", -1) + input = strings.Replace(input, "\t", "", -1) + input = strings.Replace(input, "\n", "", -1) + return strings.Replace(input, "\r", "", -1) +} diff --git a/config/serialize.go b/config/serialize.go index 647e19e33..ba17686cb 100644 --- a/config/serialize.go +++ b/config/serialize.go @@ -53,10 +53,25 @@ func WriteFile(filename string, buf []byte) error { return err } +// HumanOutput gets a config value ready for printing +func HumanOutput(value interface{}) ([]byte, error) { + s, ok := value.(string) + if ok { + return []byte(strings.Trim(s, "\n")), nil + } + return Marshal(value) +} + +// Marshal configuration with JSON +func Marshal(value interface{}) ([]byte, error) { + // need to prettyprint, hence MarshalIndent, instead of Encoder + return json.MarshalIndent(value, "", " ") +} + // Encode configuration with JSON func Encode(w io.Writer, value interface{}) error { // need to prettyprint, hence MarshalIndent, instead of Encoder - buf, err := json.MarshalIndent(value, "", " ") + buf, err := Marshal(value) if err != nil { return err } diff --git a/config/version.go b/config/version.go index 8a659cd68..62cacfa49 100644 --- a/config/version.go +++ b/config/version.go @@ -8,7 +8,7 @@ import ( ) // CurrentVersionNumber is the current application's version literal -const CurrentVersionNumber = "0.1.6" +const CurrentVersionNumber = "0.1.7" // Version regulates checking if the most recent version is run type Version struct { diff --git a/core/commands2/add.go b/core/commands2/add.go new file mode 100644 index 000000000..85386a87a --- /dev/null +++ b/core/commands2/add.go @@ -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 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 +} diff --git a/core/commands2/block.go b/core/commands2/block.go new file mode 100644 index 000000000..2582fc55e --- /dev/null +++ b/core/commands2/block.go @@ -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 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 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 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 + }, + }, +} diff --git a/core/commands2/bootstrap.go b/core/commands2/bootstrap.go new file mode 100644 index 000000000..825310c8e --- /dev/null +++ b/core/commands2/bootstrap.go @@ -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 '/')" + +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 ... - Add peers to the bootstrap list +ipfs bootstrap remove ... - 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 '/'.", + }, + + 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. + +` diff --git a/core/commands2/cat.go b/core/commands2/cat.go new file mode 100644 index 000000000..3bd760269 --- /dev/null +++ b/core/commands2/cat.go @@ -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 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 +} diff --git a/core/commands2/commands.go b/core/commands2/commands.go new file mode 100644 index 000000000..63124861d --- /dev/null +++ b/core/commands2/commands.go @@ -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 +} diff --git a/core/commands2/config.go b/core/commands2/config.go new file mode 100644 index 000000000..8a4b1bdb6 --- /dev/null +++ b/core/commands2/config.go @@ -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 - Get value of +ipfs config - Set value of to +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() +} diff --git a/core/commands2/diag.go b/core/commands2/diag.go new file mode 100644 index 000000000..764ac3f8b --- /dev/null +++ b/core/commands2/diag.go @@ -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 +} diff --git a/core/commands2/diag_test.go b/core/commands2/diag_test.go new file mode 100644 index 000000000..5e6c383e2 --- /dev/null +++ b/core/commands2/diag_test.go @@ -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()) +} diff --git a/core/commands2/internal/slice_util.go b/core/commands2/internal/slice_util.go new file mode 100644 index 000000000..75a71fd78 --- /dev/null +++ b/core/commands2/internal/slice_util.go @@ -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 +} diff --git a/core/commands2/log.go b/core/commands2/log.go new file mode 100644 index 000000000..1c821dbdc --- /dev/null +++ b/core/commands2/log.go @@ -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{}, +} diff --git a/core/commands2/ls.go b/core/commands2/ls.go new file mode 100644 index 000000000..c849825de --- /dev/null +++ b/core/commands2/ls.go @@ -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 and displays the links +it contains, with the following format: + + +`, + }, + + 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 +} diff --git a/core/commands2/mount_darwin.go b/core/commands2/mount_darwin.go new file mode 100644 index 000000000..1b50f3173 --- /dev/null +++ b/core/commands2/mount_darwin.go @@ -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") +} diff --git a/core/commands2/mount_unix.go b/core/commands2/mount_unix.go new file mode 100644 index 000000000..f55b87aa9 --- /dev/null +++ b/core/commands2/mount_unix.go @@ -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 +} diff --git a/core/commands2/mount_windows.go b/core/commands2/mount_windows.go new file mode 100644 index 000000000..6f5132e3f --- /dev/null +++ b/core/commands2/mount_windows.go @@ -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 + }, +} diff --git a/core/commands2/name.go b/core/commands2/name.go new file mode 100644 index 000000000..a50d02d43 --- /dev/null +++ b/core/commands2/name.go @@ -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 [] - Publish an object to IPNS +ipfs name resolve [] - 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 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 is your own identity public key. + + +Examples: + +Publish a to your identity name: + + > ipfs name publish QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy + published name QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n to QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy + +Publish a 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, + }, +} diff --git a/core/commands2/object.go b/core/commands2/object.go new file mode 100644 index 000000000..fe3925de4 --- /dev/null +++ b/core/commands2/object.go @@ -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 - Get the DAG node named by +ipfs object put - Stores input, outputs its key +ipfs object data - Outputs raw bytes in an object +ipfs object links - 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 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 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 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 ", + 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 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 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 . + 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 , 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 +} diff --git a/core/commands2/pin.go b/core/commands2/pin.go new file mode 100644 index 000000000..3790f5167 --- /dev/null +++ b/core/commands2/pin.go @@ -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 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 +} diff --git a/core/commands2/publish.go b/core/commands2/publish.go new file mode 100644 index 000000000..f7e76ba0a --- /dev/null +++ b/core/commands2/publish.go @@ -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 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 is your own identity public key. + +Examples: + +Publish a to your identity name: + + > ipfs name publish QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy + published name QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n to QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy + +Publish a 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 "), + }, + 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 +} diff --git a/core/commands2/refs.go b/core/commands2/refs.go new file mode 100644 index 000000000..d3c243405 --- /dev/null +++ b/core/commands2/refs.go @@ -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 and displays the link +hashes it contains, with the following format: + + + +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 +} diff --git a/core/commands2/resolve.go b/core/commands2/resolve.go new file mode 100644 index 000000000..db67cfc86 --- /dev/null +++ b/core/commands2/resolve.go @@ -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 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 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 + }, + }, +} diff --git a/core/commands2/root.go b/core/commands2/root.go new file mode 100644 index 000000000..bcf58f565 --- /dev/null +++ b/core/commands2/root.go @@ -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 Add an object to ipfs + cat Show ipfs object data + ls 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 +} diff --git a/core/commands2/update.go b/core/commands2/update.go new file mode 100644 index 000000000..81aad84e8 --- /dev/null +++ b/core/commands2/update.go @@ -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") +} diff --git a/core/commands2/version.go b/core/commands2/version.go new file mode 100644 index 000000000..047c36d56 --- /dev/null +++ b/core/commands2/version.go @@ -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{}, +} diff --git a/core/core.go b/core/core.go index 5ceacab92..75fd7d2c8 100644 --- a/core/core.go +++ b/core/core.go @@ -74,6 +74,8 @@ type IpfsNode struct { Pinning pin.Pinner ctxc.ContextCloser + + onlineMode bool // alternatively, offline } // NewIpfsNode constructs a new IpfsNode based on the given config. @@ -92,6 +94,7 @@ func NewIpfsNode(cfg *config.Config, online bool) (n *IpfsNode, err error) { // derive this from a higher context. ctx := context.TODO() n = &IpfsNode{ + onlineMode: online, Config: cfg, ContextCloser: ctxc.NewContextCloser(ctx, nil), } @@ -172,6 +175,10 @@ func NewIpfsNode(cfg *config.Config, online bool) (n *IpfsNode, err error) { return n, nil } +func (n *IpfsNode) OnlineMode() bool { + return n.onlineMode +} + func initIdentity(cfg *config.Config, peers peer.Peerstore, online bool) (peer.Peer, error) { if cfg.Identity.PeerID == "" { return nil, errors.New("Identity was not set in config (was ipfs init run?)") diff --git a/daemon2/daemon.go b/daemon2/daemon.go new file mode 100644 index 000000000..4e5bd28d5 --- /dev/null +++ b/daemon2/daemon.go @@ -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 + } +} diff --git a/diagnostics/diag.go b/diagnostics/diag.go index f0f0678b2..f31fd03d4 100644 --- a/diagnostics/diag.go +++ b/diagnostics/diag.go @@ -67,6 +67,7 @@ type DiagInfo struct { Keys []string // How long this node has been running for + // TODO rename Uptime LifeSpan time.Duration // Incoming Bandwidth Usage diff --git a/server/http/http.go b/server/http/http.go index 3c5992c6e..1fcfc1686 100644 --- a/server/http/http.go +++ b/server/http/http.go @@ -22,8 +22,10 @@ type handler struct { func Serve(address ma.Multiaddr, node *core.IpfsNode) error { r := mux.NewRouter() handler := &handler{&ipfsHandler{node}} + r.HandleFunc("/ipfs/", handler.postHandler).Methods("POST") r.PathPrefix("/ipfs/").Handler(handler).Methods("GET") + http.Handle("/", r) _, host, err := manet.DialArgs(address) diff --git a/updates/updates.go b/updates/updates.go index e7b6054d9..604a38d81 100644 --- a/updates/updates.go +++ b/updates/updates.go @@ -200,7 +200,7 @@ func CliCheckForUpdates(cfg *config.Config, confFile string) error { // if config says not to, don't check for updates if !cfg.Version.ShouldCheckForUpdate() { - log.Info("update checking disabled.") + log.Info("update check skipped.") return nil } diff --git a/util/testutil/gen.go b/util/testutil/gen.go index 111cbe728..be2fe5988 100644 --- a/util/testutil/gen.go +++ b/util/testutil/gen.go @@ -1,8 +1,8 @@ package testutil import ( - "testing" crand "crypto/rand" + "testing" "github.com/jbenet/go-ipfs/peer" diff --git a/util/util.go b/util/util.go index d4ce940d0..141cd8cba 100644 --- a/util/util.go +++ b/util/util.go @@ -8,6 +8,7 @@ import ( "math/rand" "os" "path/filepath" + "runtime/debug" "strings" "time" @@ -40,6 +41,14 @@ func TildeExpansion(filename string) (string, error) { return homedir.Expand(filename) } +// ErrCast is returned when a cast fails AND the program should not panic. +func ErrCast() error { + debug.PrintStack() + return errCast +} + +var errCast = errors.New("cast error") + // ExpandPathnames takes a set of paths and turns them into absolute paths func ExpandPathnames(paths []string) ([]string, error) { var out []string