dag.go 8.34 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
	mdag "github.com/ipfs/go-merkledag"
12

Jakub Sztandera's avatar
Jakub Sztandera committed
13 14 15 16 17
	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
18
	ipfspath "github.com/ipfs/go-path"
19
	path "github.com/ipfs/interface-go-ipfs-core/path"
Jakub Sztandera's avatar
Jakub Sztandera committed
20
	mh "github.com/multiformats/go-multihash"
21 22 23 24 25

	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"
26 27 28
)

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

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

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

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

57
var DagPutCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
58
	Helptext: cmds.HelpText{
59
		Tagline: "Add a dag node to ipfs.",
Jeromy's avatar
Jeromy committed
60 61 62 63
		ShortDescription: `
'ipfs dag put' accepts input from a file or stdin and parses it
into an object of the specified format.
`,
64
	},
Steven Allen's avatar
Steven Allen committed
65 66
	Arguments: []cmds.Argument{
		cmds.FileArg("object data", true, true, "The object to put").EnableStdin(),
67
	},
Steven Allen's avatar
Steven Allen committed
68 69 70 71 72
	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(""),
73
	},
Overbool's avatar
Overbool committed
74
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
75
		api, err := cmdenv.GetApi(env, req)
76
		if err != nil {
Overbool's avatar
Overbool committed
77
			return err
78 79
		}

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

85 86 87 88 89 90 91 92
		// 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
93
				return fmt.Errorf("%s in not a valid multihash name", hash)
94 95 96
			}
		}

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

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

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

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

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

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

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

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

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

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

Łukasz Magiera's avatar
Łukasz Magiera committed
192
// DagResolveCmd returns address of highest block within a path and a path remainder
Łukasz Magiera's avatar
Łukasz Magiera committed
193
var DagResolveCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
194
	Helptext: cmds.HelpText{
Łukasz Magiera's avatar
Łukasz Magiera committed
195 196 197 198 199
		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
200 201
	Arguments: []cmds.Argument{
		cmds.StringArg("ref", true, false, "The path to resolve").EnableStdin(),
Łukasz Magiera's avatar
Łukasz Magiera committed
202
	},
Overbool's avatar
Overbool committed
203
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
204
		api, err := cmdenv.GetApi(env, req)
Łukasz Magiera's avatar
Łukasz Magiera committed
205
		if err != nil {
Overbool's avatar
Overbool committed
206
			return err
Łukasz Magiera's avatar
Łukasz Magiera committed
207 208
		}

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

214
		return cmds.EmitOnce(res, &ResolveOutput{
215 216
			Cid:     rp.Cid(),
			RemPath: rp.Remainder(),
Łukasz Magiera's avatar
Łukasz Magiera committed
217 218
		})
	},
Overbool's avatar
Overbool committed
219 220
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *ResolveOutput) error {
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
			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
				}
239 240
			}
			p := enc.Encode(out.Cid)
Overbool's avatar
Overbool committed
241
			if out.RemPath != "" {
Łukasz Magiera's avatar
Łukasz Magiera committed
242
				p = ipfspath.Join([]string{p, out.RemPath})
Łukasz Magiera's avatar
Łukasz Magiera committed
243 244
			}

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

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
257
Note that at present only single root selections / .car files are supported.
258 259 260 261
The output of blocks happens in strict DAG-traversal, first-seen, order.
`,
	},
	Arguments: []cmds.Argument{
Peter Rabbitson's avatar
Peter Rabbitson committed
262
		cmds.StringArg("root", true, false, "CID of a root to recursively export").EnableStdin(),
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
	},
	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
280
		// https://github.com/ribasushi/gip-muddle-up
281 282 283 284
		//
		// sb := gipselectorbuilder.NewSelectorSpecBuilder(gipfree.NodeBuilder())
		// car := gocar.NewSelectiveCar(
		// 	req.Context,
285
		// 	<needs to be fixed to take format.NodeGetter as well>,
286 287 288 289 290 291 292 293
		// 	[]gocar.Dag{gocar.Dag{
		// 		Root: c,
		// 		Selector: sb.ExploreRecursive(
		// 			gipselector.RecursionLimitNone(),
		// 			sb.ExploreAll(sb.ExploreRecursiveEdge()),
		// 		).Node(),
		// 	}},
		// )
294 295
		// ...
		// if err := car.Write(pipeW); err != nil {}
296 297 298 299 300 301 302 303 304 305 306 307 308 309

		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 := gocar.WriteCar(
				req.Context,
310 311 312 313
				mdag.NewSession(
					req.Context,
					node.DAG,
				),
314 315 316 317 318 319 320 321
				[]cid.Cid{c},
				pipeW,
			); err != nil {
				errCh <- err
			}
		}()

		if err := res.Emit(pipeR); err != nil {
322
			pipeR.Close() // ignore the error if any
323 324 325
			return err
		}

326 327 328 329 330
		err = <-errCh

		// minimal user friendliness
		if err != nil &&
			!node.IsOnline &&
Peter Rabbitson's avatar
Peter Rabbitson committed
331
			err == ipld.ErrNotFound {
332 333 334 335
			err = fmt.Errorf("%s (currently offline, perhaps retry after attaching to the network)", err)
		}

		return err
336 337
	},
}