dag.go 8.72 KB
Newer Older
1 2 3 4 5
package dagcmd

import (
	"fmt"
	"io"
6
	"math"
7
	"strings"
8

Overbool's avatar
Overbool committed
9 10
	"github.com/ipfs/go-ipfs/core/commands/cmdenv"
	"github.com/ipfs/go-ipfs/core/coredag"
11

Jakub Sztandera's avatar
Jakub Sztandera committed
12 13 14 15 16
	cid "github.com/ipfs/go-cid"
	cidenc "github.com/ipfs/go-cidutil/cidenc"
	cmds "github.com/ipfs/go-ipfs-cmds"
	files "github.com/ipfs/go-ipfs-files"
	ipld "github.com/ipfs/go-ipld-format"
Łukasz Magiera's avatar
Łukasz Magiera committed
17
	ipfspath "github.com/ipfs/go-path"
18
	path "github.com/ipfs/interface-go-ipfs-core/path"
Jakub Sztandera's avatar
Jakub Sztandera committed
19
	mh "github.com/multiformats/go-multihash"
20 21 22 23 24

	gocar "github.com/ipld/go-car"
	//gipfree "github.com/ipld/go-ipld-prime/impl/free"
	//gipselector "github.com/ipld/go-ipld-prime/traversal/selector"
	//gipselectorbuilder "github.com/ipld/go-ipld-prime/traversal/selector/builder"
25 26 27
)

var DagCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
28
	Helptext: cmds.HelpText{
Jeromy's avatar
Jeromy committed
29 30
		Tagline: "Interact with ipld dag objects.",
		ShortDescription: `
31
'ipfs dag' is used for creating and manipulating dag objects/hierarchies.
Jeromy's avatar
Jeromy committed
32 33 34 35

This subcommand is currently an experimental feature, but it is intended
to deprecate and replace the existing 'ipfs object' command moving forward.
		`,
36 37
	},
	Subcommands: map[string]*cmds.Command{
Łukasz Magiera's avatar
Łukasz Magiera committed
38 39 40
		"put":     DagPutCmd,
		"get":     DagGetCmd,
		"resolve": DagResolveCmd,
41
		"export":  DagExportCmd,
42 43 44
	},
}

Łukasz Magiera's avatar
Łukasz Magiera committed
45
// OutputObject is the output type of 'dag put' command
46
type OutputObject struct {
47
	Cid cid.Cid
48 49
}

Łukasz Magiera's avatar
Łukasz Magiera committed
50
// ResolveOutput is the output type of 'dag resolve' command
Łukasz Magiera's avatar
Łukasz Magiera committed
51
type ResolveOutput struct {
52
	Cid     cid.Cid
Łukasz Magiera's avatar
Łukasz Magiera committed
53 54 55
	RemPath string
}

56
var DagPutCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
57
	Helptext: cmds.HelpText{
58
		Tagline: "Add a dag node to ipfs.",
Jeromy's avatar
Jeromy committed
59 60 61 62
		ShortDescription: `
'ipfs dag put' accepts input from a file or stdin and parses it
into an object of the specified format.
`,
63
	},
Steven Allen's avatar
Steven Allen committed
64 65
	Arguments: []cmds.Argument{
		cmds.FileArg("object data", true, true, "The object to put").EnableStdin(),
66
	},
Steven Allen's avatar
Steven Allen committed
67 68 69 70 71
	Options: []cmds.Option{
		cmds.StringOption("format", "f", "Format that the object will be added as.").WithDefault("cbor"),
		cmds.StringOption("input-enc", "Format that the input object will be.").WithDefault("json"),
		cmds.BoolOption("pin", "Pin this object when adding."),
		cmds.StringOption("hash", "Hash function to use").WithDefault(""),
72
	},
Overbool's avatar
Overbool committed
73
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
74
		api, err := cmdenv.GetApi(env, req)
75
		if err != nil {
Overbool's avatar
Overbool committed
76
			return err
77 78
		}

Overbool's avatar
Overbool committed
79 80 81 82
		ienc, _ := req.Options["input-enc"].(string)
		format, _ := req.Options["format"].(string)
		hash, _ := req.Options["hash"].(string)
		dopin, _ := req.Options["pin"].(bool)
83

84 85 86 87 88 89 90 91
		// mhType tells inputParser which hash should be used. MaxUint64 means 'use
		// default hash' (sha256 for cbor, sha1 for git..)
		mhType := uint64(math.MaxUint64)

		if hash != "" {
			var ok bool
			mhType, ok = mh.Names[hash]
			if !ok {
Overbool's avatar
Overbool committed
92
				return fmt.Errorf("%s in not a valid multihash name", hash)
93 94 95
			}
		}

96
		var adder ipld.NodeAdder = api.Dag()
Steven Allen's avatar
Steven Allen committed
97
		if dopin {
98
			adder = api.Dag().Pinning()
Steven Allen's avatar
Steven Allen committed
99
		}
100
		b := ipld.NewBatch(req.Context, adder)
Steven Allen's avatar
Steven Allen committed
101

102
		it := req.Files.Entries()
103
		for it.Next() {
104 105
			file := files.FileFromEntry(it)
			if file == nil {
106
				return fmt.Errorf("expected a regular file")
Łukasz Magiera's avatar
Łukasz Magiera committed
107
			}
108
			nds, err := coredag.ParseInputs(ienc, format, file, mhType, -1)
Steven Allen's avatar
Steven Allen committed
109
			if err != nil {
Łukasz Magiera's avatar
Łukasz Magiera committed
110 111
				return err
			}
Steven Allen's avatar
Steven Allen committed
112 113 114
			if len(nds) == 0 {
				return fmt.Errorf("no node returned from ParseInputs")
			}
115

Steven Allen's avatar
Steven Allen committed
116
			for _, nd := range nds {
Hector Sanjuan's avatar
Hector Sanjuan committed
117
				err := b.Add(req.Context, nd)
Łukasz Magiera's avatar
Łukasz Magiera committed
118 119 120
				if err != nil {
					return err
				}
Jeromy's avatar
Jeromy committed
121
			}
122

Steven Allen's avatar
Steven Allen committed
123 124 125 126
			cid := nds[0].Cid()
			if err := res.Emit(&OutputObject{Cid: cid}); err != nil {
				return err
			}
127
		}
128
		if it.Err() != nil {
129
			return it.Err()
130
		}
131

Steven Allen's avatar
Steven Allen committed
132
		if err := b.Commit(); err != nil {
Overbool's avatar
Overbool committed
133 134
			return err
		}
135

Steven Allen's avatar
Steven Allen committed
136
		return nil
Overbool's avatar
Overbool committed
137 138 139 140
	},
	Type: OutputObject{},
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *OutputObject) error {
141 142 143 144 145
			enc, err := cmdenv.GetLowLevelCidEncoder(req)
			if err != nil {
				return err
			}
			fmt.Fprintln(w, enc.Encode(out.Cid))
Overbool's avatar
Overbool committed
146 147
			return nil
		}),
148 149 150 151
	},
}

var DagGetCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
152
	Helptext: cmds.HelpText{
153
		Tagline: "Get a dag node from ipfs.",
Jeromy's avatar
Jeromy committed
154
		ShortDescription: `
Łukasz Magiera's avatar
Łukasz Magiera committed
155
'ipfs dag get' fetches a dag node from ipfs and prints it out in the specified
156
format.
Jeromy's avatar
Jeromy committed
157
`,
158
	},
Steven Allen's avatar
Steven Allen committed
159 160
	Arguments: []cmds.Argument{
		cmds.StringArg("ref", true, false, "The object to get").EnableStdin(),
161
	},
Overbool's avatar
Overbool committed
162
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
163
		api, err := cmdenv.GetApi(env, req)
164
		if err != nil {
Overbool's avatar
Overbool committed
165
			return err
166 167
		}

168
		rp, err := api.ResolvePath(req.Context, path.New(req.Arguments[0]))
169
		if err != nil {
Overbool's avatar
Overbool committed
170
			return err
171
		}
172 173

		obj, err := api.Dag().Get(req.Context, rp.Cid())
174
		if err != nil {
Overbool's avatar
Overbool committed
175
			return err
176 177
		}

178
		var out interface{} = obj
179 180
		if len(rp.Remainder()) > 0 {
			rem := strings.Split(rp.Remainder(), "/")
181 182
			final, _, err := obj.Resolve(rem)
			if err != nil {
Overbool's avatar
Overbool committed
183
				return err
184 185 186
			}
			out = final
		}
187
		return cmds.EmitOnce(res, &out)
188 189
	},
}
Łukasz Magiera's avatar
Łukasz Magiera committed
190

Łukasz Magiera's avatar
Łukasz Magiera committed
191
// DagResolveCmd returns address of highest block within a path and a path remainder
Łukasz Magiera's avatar
Łukasz Magiera committed
192
var DagResolveCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
193
	Helptext: cmds.HelpText{
Łukasz Magiera's avatar
Łukasz Magiera committed
194 195 196 197 198
		Tagline: "Resolve ipld block",
		ShortDescription: `
'ipfs dag resolve' fetches a dag node from ipfs, prints it's address and remaining path.
`,
	},
Steven Allen's avatar
Steven Allen committed
199 200
	Arguments: []cmds.Argument{
		cmds.StringArg("ref", true, false, "The path to resolve").EnableStdin(),
Łukasz Magiera's avatar
Łukasz Magiera committed
201
	},
Overbool's avatar
Overbool committed
202
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
203
		api, err := cmdenv.GetApi(env, req)
Łukasz Magiera's avatar
Łukasz Magiera committed
204
		if err != nil {
Overbool's avatar
Overbool committed
205
			return err
Łukasz Magiera's avatar
Łukasz Magiera committed
206 207
		}

208
		rp, err := api.ResolvePath(req.Context, path.New(req.Arguments[0]))
Łukasz Magiera's avatar
Łukasz Magiera committed
209
		if err != nil {
Overbool's avatar
Overbool committed
210
			return err
Łukasz Magiera's avatar
Łukasz Magiera committed
211 212
		}

213
		return cmds.EmitOnce(res, &ResolveOutput{
214 215
			Cid:     rp.Cid(),
			RemPath: rp.Remainder(),
Łukasz Magiera's avatar
Łukasz Magiera committed
216 217
		})
	},
Overbool's avatar
Overbool committed
218 219
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *ResolveOutput) error {
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
			var (
				enc cidenc.Encoder
				err error
			)
			switch {
			case !cmdenv.CidBaseDefined(req):
				// Not specified, check the path.
				enc, err = cmdenv.CidEncoderFromPath(req.Arguments[0])
				if err == nil {
					break
				}
				// Nope, fallback on the default.
				fallthrough
			default:
				enc, err = cmdenv.GetLowLevelCidEncoder(req)
				if err != nil {
					return err
				}
238 239
			}
			p := enc.Encode(out.Cid)
Overbool's avatar
Overbool committed
240
			if out.RemPath != "" {
Łukasz Magiera's avatar
Łukasz Magiera committed
241
				p = ipfspath.Join([]string{p, out.RemPath})
Łukasz Magiera's avatar
Łukasz Magiera committed
242 243
			}

Overbool's avatar
Overbool committed
244 245 246
			fmt.Fprint(w, p)
			return nil
		}),
Łukasz Magiera's avatar
Łukasz Magiera committed
247 248 249
	},
	Type: ResolveOutput{},
}
250 251 252 253 254 255

var DagExportCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "Streams the selected DAG as a .car stream on stdout.",
		ShortDescription: `
'ipfs dag export' fetches a dag and streams it out as a well-formed .car file.
Peter Rabbitson's avatar
Peter Rabbitson committed
256
Note that at present only single root selections / .car files are supported.
257 258 259 260
The output of blocks happens in strict DAG-traversal, first-seen, order.
`,
	},
	Arguments: []cmds.Argument{
Peter Rabbitson's avatar
Peter Rabbitson committed
261
		cmds.StringArg("root", true, false, "CID of a root to recursively export").EnableStdin(),
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
	},
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {

		c, err := cid.Decode(req.Arguments[0])
		if err != nil {
			return fmt.Errorf(
				"unable to parse root specification (currently only bare CIDs are supported): %s",
				err,
			)
		}

		node, err := cmdenv.GetNode(env)
		if err != nil {
			return err
		}

		// Code disabled until descent-issue in go-ipld-prime is fixed
		//
		// The second part of the above - make a super-thin wrapper around
		// a blockservice session, translating Session.GetBlock() to Blockstore.Get()
		//
Peter Rabbitson's avatar
Peter Rabbitson committed
283 284 285 286 287 288
		// The current interface of go-car is rather suboptimal as it
		// only takes a blockstore, instead of accepting a dagservice,
		// and leveraging parallel-fetch capabilities
		// https://github.com/ipld/go-car/issues/27
		//
		//
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
		// sess := blockservice.NewSession(
		// 	req.Context,
		// 	node.Blocks,
		// )
		// var wrapper getBlockFromSessionWrapper = func(c cid.Cid) (blk.Block, error) {
		// 	return sess.GetBlock(req.Context, c)
		// }
		// sb := gipselectorbuilder.NewSelectorSpecBuilder(gipfree.NodeBuilder())
		// car := gocar.NewSelectiveCar(
		// 	req.Context,
		// 	&wrapper,
		// 	[]gocar.Dag{gocar.Dag{
		// 		Root: c,
		// 		Selector: sb.ExploreRecursive(
		// 			gipselector.RecursionLimitNone(),
		// 			sb.ExploreAll(sb.ExploreRecursiveEdge()),
		// 		).Node(),
		// 	}},
		// )

		pipeR, pipeW := io.Pipe()

		errCh := make(chan error, 2) // we only report the 1st error
		go func() {
			defer func() {
				if err := pipeW.Close(); err != nil {
					errCh <- fmt.Errorf("stream flush failed: %s", err)
				}
				close(errCh)
			}()

			//if err := car.Write(pipeW); err != nil {
			if err := gocar.WriteCar(
				req.Context,
				node.DAG,
				[]cid.Cid{c},
				pipeW,
			); err != nil {
				errCh <- err
			}
		}()

		if err := res.Emit(pipeR); err != nil {
			return err
		}

335 336 337 338 339 340 341 342 343 344
		err = <-errCh

		// minimal user friendliness
		if err != nil &&
			!node.IsOnline &&
			err.Error() == "merkledag: not found" {
			err = fmt.Errorf("%s (currently offline, perhaps retry after attaching to the network)", err)
		}

		return err
345 346
	},
}