dag.go 10 KB
Newer Older
1 2 3
package dagcmd

import (
4
	"errors"
5 6
	"fmt"
	"io"
7
	"math"
8
	"os"
9
	"strings"
10
	"time"
11

Overbool's avatar
Overbool committed
12 13
	"github.com/ipfs/go-ipfs/core/commands/cmdenv"
	"github.com/ipfs/go-ipfs/core/coredag"
14
	mdag "github.com/ipfs/go-merkledag"
15

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

	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"
29 30 31 32 33 34

	"gopkg.in/cheggaaa/pb.v1"
)

const (
	progressOptionName = "progress"
35 36 37
)

var DagCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
38
	Helptext: cmds.HelpText{
Jeromy's avatar
Jeromy committed
39 40
		Tagline: "Interact with ipld dag objects.",
		ShortDescription: `
41
'ipfs dag' is used for creating and manipulating dag objects/hierarchies.
Jeromy's avatar
Jeromy committed
42 43 44 45

This subcommand is currently an experimental feature, but it is intended
to deprecate and replace the existing 'ipfs object' command moving forward.
		`,
46 47
	},
	Subcommands: map[string]*cmds.Command{
Łukasz Magiera's avatar
Łukasz Magiera committed
48 49 50
		"put":     DagPutCmd,
		"get":     DagGetCmd,
		"resolve": DagResolveCmd,
51
		"export":  DagExportCmd,
52 53 54
	},
}

Łukasz Magiera's avatar
Łukasz Magiera committed
55
// OutputObject is the output type of 'dag put' command
56
type OutputObject struct {
57
	Cid cid.Cid
58 59
}

Łukasz Magiera's avatar
Łukasz Magiera committed
60
// ResolveOutput is the output type of 'dag resolve' command
Łukasz Magiera's avatar
Łukasz Magiera committed
61
type ResolveOutput struct {
62
	Cid     cid.Cid
Łukasz Magiera's avatar
Łukasz Magiera committed
63 64 65
	RemPath string
}

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

Overbool's avatar
Overbool committed
89 90 91 92
		ienc, _ := req.Options["input-enc"].(string)
		format, _ := req.Options["format"].(string)
		hash, _ := req.Options["hash"].(string)
		dopin, _ := req.Options["pin"].(bool)
93

94 95 96 97 98 99 100 101
		// 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
102
				return fmt.Errorf("%s in not a valid multihash name", hash)
103 104 105
			}
		}

106
		var adder ipld.NodeAdder = api.Dag()
Steven Allen's avatar
Steven Allen committed
107
		if dopin {
108
			adder = api.Dag().Pinning()
Steven Allen's avatar
Steven Allen committed
109
		}
110
		b := ipld.NewBatch(req.Context, adder)
Steven Allen's avatar
Steven Allen committed
111

112
		it := req.Files.Entries()
113
		for it.Next() {
114 115
			file := files.FileFromEntry(it)
			if file == nil {
116
				return fmt.Errorf("expected a regular file")
Łukasz Magiera's avatar
Łukasz Magiera committed
117
			}
118
			nds, err := coredag.ParseInputs(ienc, format, file, mhType, -1)
Steven Allen's avatar
Steven Allen committed
119
			if err != nil {
Łukasz Magiera's avatar
Łukasz Magiera committed
120 121
				return err
			}
Steven Allen's avatar
Steven Allen committed
122 123 124
			if len(nds) == 0 {
				return fmt.Errorf("no node returned from ParseInputs")
			}
125

Steven Allen's avatar
Steven Allen committed
126
			for _, nd := range nds {
Hector Sanjuan's avatar
Hector Sanjuan committed
127
				err := b.Add(req.Context, nd)
Łukasz Magiera's avatar
Łukasz Magiera committed
128 129 130
				if err != nil {
					return err
				}
Jeromy's avatar
Jeromy committed
131
			}
132

Steven Allen's avatar
Steven Allen committed
133 134 135 136
			cid := nds[0].Cid()
			if err := res.Emit(&OutputObject{Cid: cid}); err != nil {
				return err
			}
137
		}
138
		if it.Err() != nil {
139
			return it.Err()
140
		}
141

Steven Allen's avatar
Steven Allen committed
142
		if err := b.Commit(); err != nil {
Overbool's avatar
Overbool committed
143 144
			return err
		}
145

Steven Allen's avatar
Steven Allen committed
146
		return nil
Overbool's avatar
Overbool committed
147 148 149 150
	},
	Type: OutputObject{},
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *OutputObject) error {
151 152 153 154 155
			enc, err := cmdenv.GetLowLevelCidEncoder(req)
			if err != nil {
				return err
			}
			fmt.Fprintln(w, enc.Encode(out.Cid))
Overbool's avatar
Overbool committed
156 157
			return nil
		}),
158 159 160 161
	},
}

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

178
		rp, err := api.ResolvePath(req.Context, path.New(req.Arguments[0]))
179
		if err != nil {
Overbool's avatar
Overbool committed
180
			return err
181
		}
182 183

		obj, err := api.Dag().Get(req.Context, rp.Cid())
184
		if err != nil {
Overbool's avatar
Overbool committed
185
			return err
186 187
		}

188
		var out interface{} = obj
189 190
		if len(rp.Remainder()) > 0 {
			rem := strings.Split(rp.Remainder(), "/")
191 192
			final, _, err := obj.Resolve(rem)
			if err != nil {
Overbool's avatar
Overbool committed
193
				return err
194 195 196
			}
			out = final
		}
197
		return cmds.EmitOnce(res, &out)
198 199
	},
}
Łukasz Magiera's avatar
Łukasz Magiera committed
200

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

218
		rp, err := api.ResolvePath(req.Context, path.New(req.Arguments[0]))
Łukasz Magiera's avatar
Łukasz Magiera committed
219
		if err != nil {
Overbool's avatar
Overbool committed
220
			return err
Łukasz Magiera's avatar
Łukasz Magiera committed
221 222
		}

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

Overbool's avatar
Overbool committed
254 255 256
			fmt.Fprint(w, p)
			return nil
		}),
Łukasz Magiera's avatar
Łukasz Magiera committed
257 258 259
	},
	Type: ResolveOutput{},
}
260 261 262 263 264 265

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

		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,
322 323 324 325
				mdag.NewSession(
					req.Context,
					node.DAG,
				),
326 327 328 329 330 331 332 333
				[]cid.Cid{c},
				pipeW,
			); err != nil {
				errCh <- err
			}
		}()

		if err := res.Emit(pipeR); err != nil {
334
			pipeR.Close() // ignore the error if any
335 336 337
			return err
		}

338 339 340 341 342
		err = <-errCh

		// minimal user friendliness
		if err != nil &&
			!node.IsOnline &&
Peter Rabbitson's avatar
Peter Rabbitson committed
343
			err == ipld.ErrNotFound {
344 345 346 347
			err = fmt.Errorf("%s (currently offline, perhaps retry after attaching to the network)", err)
		}

		return err
348
	},
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
	PostRun: cmds.PostRunMap{
		cmds.CLI: func(res cmds.Response, re cmds.ResponseEmitter) error {

			var showProgress bool
			val, specified := res.Request().Options[progressOptionName]
			if !specified {
				// default based on TTY availability
				errStat, _ := os.Stderr.Stat()
				if 0 != (errStat.Mode() & os.ModeCharDevice) {
					showProgress = true
				}
			} else if val.(bool) {
				showProgress = true
			}

			// simple passthrough, no progress
			if !showProgress {
				return cmds.Copy(re, res)
			}

			bar := pb.New64(0).SetUnits(pb.U_BYTES)
			bar.Output = os.Stderr
			bar.ShowSpeed = true
			bar.ShowElapsedTime = true
			bar.RefreshRate = 500 * time.Millisecond
			bar.Start()

			var processedOneResponse bool
			for {
				v, err := res.Next()
				if err == io.EOF {

					// We only write the final bar update on success
					// On error it looks too weird
					bar.Finish()

					return re.Close()
				} else if err != nil {
					return re.CloseWithError(err)
				} else if processedOneResponse {
					return re.CloseWithError(errors.New("unexpected multipart response during emit, please file a bugreport"))
				}

				r, ok := v.(io.Reader)
				if !ok {
					// some sort of encoded response, this should not be happening
					return errors.New("unexpected non-stream passed to PostRun: please file a bugreport")
				}

				processedOneResponse = true

				if err := re.Emit(bar.NewProxyReader(r)); err != nil {
					return err
				}
			}
		},
	},
406
}