add.go 13.8 KB
Newer Older
1 2 3
package commands

import (
4
	"errors"
5
	"fmt"
6
	"io"
Jan Winkelmann's avatar
Jan Winkelmann committed
7
	"os"
8
	"strings"
9

10
	bstore "github.com/ipfs/go-ipfs/blocks/blockstore"
11
	blockservice "github.com/ipfs/go-ipfs/blockservice"
12
	core "github.com/ipfs/go-ipfs/core"
13
	"github.com/ipfs/go-ipfs/core/coreunix"
14 15
	offline "github.com/ipfs/go-ipfs/exchange/offline"
	dag "github.com/ipfs/go-ipfs/merkledag"
Jeromy's avatar
Jeromy committed
16 17
	dagtest "github.com/ipfs/go-ipfs/merkledag/test"
	mfs "github.com/ipfs/go-ipfs/mfs"
18
	ft "github.com/ipfs/go-ipfs/unixfs"
19

Steven Allen's avatar
Steven Allen committed
20
	"gx/ipfs/QmQtQuaQvS5mKJVoCvL5FvrYH7oZPjxsVHf2bKSGgcVmZt/go-ipfs-cmds"
21
	mh "gx/ipfs/QmU9a9NV9RdPNwZQDYd5uKsm6N6LJLSvLbywDDYFbaaC6P/go-multihash"
Steven Allen's avatar
Steven Allen committed
22 23
	"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit"
	"gx/ipfs/QmUyfy4QSr3NXym4etEiRyxBLqqAeKHJuRdi8AACxg63fZ/go-ipfs-cmdkit/files"
24
	"gx/ipfs/QmeWjRodbcZFKe5tMN7poEx3izym6osrLSnTLf9UjJZBbs/pb"
25 26
)

Jan Winkelmann's avatar
Jan Winkelmann committed
27
// ErrDepthLimitExceeded indicates that the max depth has been exceded.
28 29
var ErrDepthLimitExceeded = fmt.Errorf("depth limit exceeded")

30
const (
31
	quietOptionName       = "quiet"
32
	quieterOptionName     = "quieter"
33 34 35 36 37 38 39 40 41 42 43
	silentOptionName      = "silent"
	progressOptionName    = "progress"
	trickleOptionName     = "trickle"
	wrapOptionName        = "wrap-with-directory"
	hiddenOptionName      = "hidden"
	onlyHashOptionName    = "only-hash"
	chunkerOptionName     = "chunker"
	pinOptionName         = "pin"
	rawLeavesOptionName   = "raw-leaves"
	noCopyOptionName      = "nocopy"
	fstoreCacheOptionName = "fscache"
44
	cidVersionOptionName  = "cid-version"
45
	hashOptionName        = "hash"
46
)
47

48 49
const adderOutChanSize = 8

50
var AddCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
51
	Helptext: cmdkit.HelpText{
52
		Tagline: "Add a file or directory to ipfs.",
53
		ShortDescription: `
54
Adds contents of <path> to ipfs. Use -r to add directories (recursively).
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
`,
		LongDescription: `
Adds contents of <path> to ipfs. Use -r to add directories.
Note that directories are added recursively, to form the ipfs
MerkleDAG.

The wrap option, '-w', wraps the file (or files, if using the
recursive option) in a directory. This directory contains only
the files which have been added, and means that the file retains
its filename. For example:

  > ipfs add example.jpg
  added QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH example.jpg
  > ipfs add example.jpg -w
  added QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH example.jpg
  added QmaG4FuMqEBnQNn3C8XJ5bpW8kLs7zq2ZXgHptJHbKDDVx

You can now refer to the added file in a gateway, like so:

  /ipfs/QmaG4FuMqEBnQNn3C8XJ5bpW8kLs7zq2ZXgHptJHbKDDVx/example.jpg
75

76 77
The chunker option, '-s', specifies the chunking strategy that dictates
how to break files into blocks. Blocks with same content can
78
be deduplicated. The default is a fixed block size of
79 80
256 * 1024 bytes, 'size-262144'. Alternatively, you can use the
rabin chunker for content defined chunking by specifying
81
rabin-[min]-[avg]-[max] (where min/avg/max refer to the resulting
82 83
chunk sizes). Using other chunking strategies will produce
different hashes for the same file.
84

85 86 87 88
  > ipfs add --chunker=size-2048 ipfs-logo.svg
  added QmafrLBfzRLV4XSH1XcaMMeaXEUhDJjmtDfsYU95TrWG87 ipfs-logo.svg
  > ipfs add --chunker=rabin-512-1024-2048 ipfs-logo.svg
  added Qmf1hDN65tR55Ubh2RN1FPxr69xq3giVBz1KApsresY8Gn ipfs-logo.svg
89 90 91

You can now check what blocks have been created by:

92 93 94 95 96 97
  > ipfs object links QmafrLBfzRLV4XSH1XcaMMeaXEUhDJjmtDfsYU95TrWG87
  QmY6yj1GsermExDXoosVE3aSPxdMNYr6aKuw3nA8LoWPRS 2059
  Qmf7ZQeSxq2fJVJbCmgTrLLVN9tDR9Wy5k75DxQKuz5Gyt 1195
  > ipfs object links Qmf1hDN65tR55Ubh2RN1FPxr69xq3giVBz1KApsresY8Gn
  QmY6yj1GsermExDXoosVE3aSPxdMNYr6aKuw3nA8LoWPRS 2059
  QmerURi9k4XzKCaaPbsK6BL5pMEjF7PGphjDvkkjDtsVf3 868
98
  QmQB28iwSriSUSMqG2nXDTLtdPHgWb4rebBrU7Q1j4vxPv 338
99 100 101
`,
	},

Jan Winkelmann's avatar
Jan Winkelmann committed
102 103
	Arguments: []cmdkit.Argument{
		cmdkit.FileArg("path", true, true, "The path to a file to be added to ipfs.").EnableRecursive().EnableStdin(),
104
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
105 106 107 108 109 110 111 112 113 114
	Options: []cmdkit.Option{
		cmdkit.OptionRecursivePath, // a builtin option that allows recursive paths (-r, --recursive)
		cmdkit.BoolOption(quietOptionName, "q", "Write minimal output."),
		cmdkit.BoolOption(quieterOptionName, "Q", "Write only final hash."),
		cmdkit.BoolOption(silentOptionName, "Write no output."),
		cmdkit.BoolOption(progressOptionName, "p", "Stream progress data."),
		cmdkit.BoolOption(trickleOptionName, "t", "Use trickle-dag format for dag generation."),
		cmdkit.BoolOption(onlyHashOptionName, "n", "Only chunk and hash - do not write to disk."),
		cmdkit.BoolOption(wrapOptionName, "w", "Wrap files with a directory object."),
		cmdkit.BoolOption(hiddenOptionName, "H", "Include files that are hidden. Only takes effect on recursive add."),
115 116
		cmdkit.StringOption(chunkerOptionName, "s", "Chunking algorithm, size-[bytes] or rabin-[min]-[avg]-[max]").WithDefault("size-262144"),
		cmdkit.BoolOption(pinOptionName, "Pin this object when adding.").WithDefault(true),
Jan Winkelmann's avatar
Jan Winkelmann committed
117 118 119
		cmdkit.BoolOption(rawLeavesOptionName, "Use raw blocks for leaf nodes. (experimental)"),
		cmdkit.BoolOption(noCopyOptionName, "Add the file using filestore. (experimental)"),
		cmdkit.BoolOption(fstoreCacheOptionName, "Check the filestore for pre-existing blocks. (experimental)"),
120 121
		cmdkit.IntOption(cidVersionOptionName, "Cid version. Non-zero value will change default of 'raw-leaves' to true. (experimental)").WithDefault(0),
		cmdkit.StringOption(hashOptionName, "Hash function to use. Will set Cid version to 1 if used. (experimental)").WithDefault("sha2-256"),
122 123
	},
	PreRun: func(req cmds.Request) error {
124
		quiet, _, _ := req.Option(quietOptionName).Bool()
125 126 127
		quieter, _, _ := req.Option(quieterOptionName).Bool()
		quiet = quiet || quieter

128 129 130
		silent, _, _ := req.Option(silentOptionName).Bool()

		if quiet || silent {
131 132 133
			return nil
		}

134
		// ipfs cli progress bar defaults to true unless quiet or silent is used
135 136 137 138 139
		_, found, _ := req.Option(progressOptionName).Bool()
		if !found {
			req.SetOption(progressOptionName, true)
		}

140 141 142
		sizeFile, ok := req.Files().(files.SizeFile)
		if !ok {
			// we don't need to error, the progress bar just won't know how big the files are
143
			log.Warning("cannot determine size of input file")
144 145 146
			return nil
		}

147 148
		sizeCh := make(chan int64, 1)
		req.Values()["size"] = sizeCh
rht's avatar
rht committed
149

150 151 152
		go func() {
			size, err := sizeFile.Size()
			if err != nil {
Jeromy's avatar
Jeromy committed
153
				log.Warningf("error getting files size: %s", err)
154 155 156 157 158 159
				// see comment above
				return
			}

			sizeCh <- size
		}()
160 161

		return nil
162
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
163
	Run: func(req cmds.Request, res cmds.ResponseEmitter) {
Jeromy's avatar
Jeromy committed
164
		n, err := req.InvocContext().GetNode()
165
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
166
			res.SetError(err, cmdkit.ErrNormal)
167
			return
168
		}
169 170 171

		cfg, err := n.Repo.Config()
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
172
			res.SetError(err, cmdkit.ErrNormal)
173 174
			return
		}
rht's avatar
rht committed
175 176 177 178
		// check if repo will exceed storage limit if added
		// TODO: this doesn't handle the case if the hashed file is already in blocks (deduplicated)
		// TODO: conditional GC is disabled due to it is somehow not possible to pass the size to the daemon
		//if err := corerepo.ConditionalGC(req.Context(), n, uint64(size)); err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
179
		//	res.SetError(err, cmdkit.ErrNormal)
rht's avatar
rht committed
180 181
		//	return
		//}
182

183
		progress, _, _ := req.Option(progressOptionName).Bool()
184
		trickle, _, _ := req.Option(trickleOptionName).Bool()
185
		wrap, _, _ := req.Option(wrapOptionName).Bool()
gatesvp's avatar
gatesvp committed
186 187
		hash, _, _ := req.Option(onlyHashOptionName).Bool()
		hidden, _, _ := req.Option(hiddenOptionName).Bool()
Jeromy's avatar
Jeromy committed
188
		silent, _, _ := req.Option(silentOptionName).Bool()
189
		chunker, _, _ := req.Option(chunkerOptionName).String()
190
		dopin, _, _ := req.Option(pinOptionName).Bool()
Jeromy's avatar
Jeromy committed
191
		rawblks, rbset, _ := req.Option(rawLeavesOptionName).Bool()
192 193
		nocopy, _, _ := req.Option(noCopyOptionName).Bool()
		fscache, _, _ := req.Option(fstoreCacheOptionName).Bool()
194
		cidVer, _, _ := req.Option(cidVersionOptionName).Int()
195
		hashFunStr, hfset, _ := req.Option(hashOptionName).String()
196

197 198
		if nocopy && !cfg.Experimental.FilestoreEnabled {
			res.SetError(errors.New("filestore is not enabled, see https://git.io/vy4XN"),
Jan Winkelmann's avatar
Jan Winkelmann committed
199
				cmdkit.ErrClient)
200 201 202
			return
		}

Jeromy's avatar
Jeromy committed
203 204 205 206
		if nocopy && !rbset {
			rawblks = true
		}

207
		if nocopy && !rawblks {
Jan Winkelmann's avatar
Jan Winkelmann committed
208
			res.SetError(fmt.Errorf("nocopy option requires '--raw-leaves' to be enabled as well"), cmdkit.ErrNormal)
209 210
			return
		}
Jeromy's avatar
Jeromy committed
211

212 213 214 215
		if hfset && cidVer == 0 {
			cidVer = 1
		}

216 217 218 219 220 221
		if cidVer >= 1 && !rbset {
			rawblks = true
		}

		prefix, err := dag.PrefixForCidVersion(cidVer)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
222
			res.SetError(err, cmdkit.ErrNormal)
223 224 225
			return
		}

226 227
		hashFunCode, ok := mh.Names[strings.ToLower(hashFunStr)]
		if !ok {
Jan Winkelmann's avatar
Jan Winkelmann committed
228
			res.SetError(fmt.Errorf("unrecognized hash function: %s", strings.ToLower(hashFunStr)), cmdkit.ErrNormal)
229 230 231 232 233 234
			return
		}

		prefix.MhType = hashFunCode
		prefix.MhLength = -1

Jeromy's avatar
Jeromy committed
235
		if hash {
236 237 238
			nilnode, err := core.NewNode(n.Context(), &core.BuildCfg{
				//TODO: need this to be true or all files
				// hashed will be stored in memory!
239
				NilRepo: true,
240
			})
Jeromy's avatar
Jeromy committed
241
			if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
242
				res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
243 244 245 246
				return
			}
			n = nilnode
		}
247

248
		addblockstore := n.Blockstore
Jeromy's avatar
Jeromy committed
249
		if !(fscache || nocopy) {
250 251 252 253
			addblockstore = bstore.NewGCBlockstore(n.BaseBlocks, n.GCLocker)
		}

		exch := n.Exchange
254 255
		local, _, _ := req.Option("local").Bool()
		if local {
256
			exch = offline.Exchange(addblockstore)
257 258
		}

259 260 261
		bserv := blockservice.New(addblockstore, exch)
		dserv := dag.NewDAGService(bserv)

Jan Winkelmann's avatar
Jan Winkelmann committed
262 263
		outChan := make(chan interface{}, adderOutChanSize)

264
		fileAdder, err := coreunix.NewAdder(req.Context(), n.Pinning, n.Blockstore, dserv)
Jeromy's avatar
Jeromy committed
265
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
266
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
267 268
			return
		}
269 270

		fileAdder.Out = outChan
271 272 273 274 275
		fileAdder.Chunker = chunker
		fileAdder.Progress = progress
		fileAdder.Hidden = hidden
		fileAdder.Trickle = trickle
		fileAdder.Wrap = wrap
276
		fileAdder.Pin = dopin
Jeromy's avatar
Jeromy committed
277
		fileAdder.Silent = silent
278
		fileAdder.RawLeaves = rawblks
279
		fileAdder.NoCopy = nocopy
280
		fileAdder.Prefix = &prefix
281

Jeromy's avatar
Jeromy committed
282 283
		if hash {
			md := dagtest.Mock()
284
			mr, err := mfs.NewRoot(req.Context(), md, ft.EmptyDirNode(), nil)
Jeromy's avatar
Jeromy committed
285
			if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
286
				res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
287 288 289 290 291 292
				return
			}

			fileAdder.SetMfsRoot(mr)
		}

293
		addAllAndPin := func(f files.File) error {
294 295 296 297 298 299 300 301 302 303 304 305 306 307
			// Iterate over each top-level file and add individually. Otherwise the
			// single files.File f is treated as a directory, affecting hidden file
			// semantics.
			for {
				file, err := f.NextFile()
				if err == io.EOF {
					// Finished the list of files.
					break
				} else if err != nil {
					return err
				}
				if err := fileAdder.AddFile(file); err != nil {
					return err
				}
308
			}
309

310
			// copy intermediary nodes from editor to our actual dagservice
Jeromy's avatar
Jeromy committed
311
			_, err := fileAdder.Finalize()
312 313
			if err != nil {
				return err
314 315
			}

Stephen Whitmore's avatar
Stephen Whitmore committed
316 317 318 319
			if hash {
				return nil
			}

320
			return fileAdder.PinRoot()
321 322
		}

Jan Winkelmann's avatar
Jan Winkelmann committed
323
		errCh := make(chan error)
324
		go func() {
Jan Winkelmann's avatar
Jan Winkelmann committed
325 326
			var err error
			defer func() { errCh <- err }()
327
			defer close(outChan)
Jan Winkelmann's avatar
Jan Winkelmann committed
328
			err = addAllAndPin(req.Files())
329
		}()
Jan Winkelmann's avatar
Jan Winkelmann committed
330 331 332 333 334 335

		defer res.Close()

		err = res.Emit(outChan)
		if err != nil {
			log.Error(err)
336 337
			return
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
338 339 340
		err = <-errCh
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
341
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
342 343 344
	},
	PostRun: map[cmds.EncodingType]func(cmds.Request, cmds.ResponseEmitter) cmds.ResponseEmitter{
		cmds.CLI: func(req cmds.Request, re cmds.ResponseEmitter) cmds.ResponseEmitter {
345 346
			ctx := req.Context()

Jan Winkelmann's avatar
Jan Winkelmann committed
347 348
			reNext, res := cmds.NewChanResponsePair(req)
			outChan := make(chan interface{})
349

Jan Winkelmann's avatar
Jan Winkelmann committed
350 351
			progressBar := func(wait chan struct{}) {
				defer close(wait)
352

Jan Winkelmann's avatar
Jan Winkelmann committed
353 354 355
				quiet, _, _ := req.Option(quietOptionName).Bool()
				quieter, _, _ := req.Option(quieterOptionName).Bool()
				quiet = quiet || quieter
Jeromy's avatar
Jeromy committed
356

Jan Winkelmann's avatar
Jan Winkelmann committed
357
				progress, _, _ := req.Option(progressOptionName).Bool()
358

Jan Winkelmann's avatar
Jan Winkelmann committed
359 360 361 362 363 364 365 366 367
				var bar *pb.ProgressBar
				if progress {
					bar = pb.New64(0).SetUnits(pb.U_BYTES)
					bar.ManualUpdate = true
					bar.ShowTimeLeft = false
					bar.ShowPercent = false
					bar.Output = os.Stderr
					bar.Start()
				}
368

Jan Winkelmann's avatar
Jan Winkelmann committed
369 370 371 372
				var sizeChan chan int64
				s, found := req.Values()["size"]
				if found {
					sizeChan = s.(chan int64)
373
				}
374

Jan Winkelmann's avatar
Jan Winkelmann committed
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 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
				lastFile := ""
				lastHash := ""
				var totalProgress, prevFiles, lastBytes int64

			LOOP:
				for {
					select {
					case out, ok := <-outChan:
						if !ok {
							if quieter {
								fmt.Fprintln(os.Stdout, lastHash)
							}

							break LOOP
						}
						output := out.(*coreunix.AddedObject)
						if len(output.Hash) > 0 {
							lastHash = output.Hash
							if quieter {
								continue
							}

							if progress {
								// clear progress bar line before we print "added x" output
								fmt.Fprintf(os.Stderr, "\033[2K\r")
							}
							if quiet {
								fmt.Fprintf(os.Stdout, "%s\n", output.Hash)
							} else {
								fmt.Fprintf(os.Stdout, "added %s %s\n", output.Hash, output.Name)
							}

						} else {
							if !progress {
								continue
							}

							if len(lastFile) == 0 {
								lastFile = output.Name
							}
							if output.Name != lastFile || output.Bytes < lastBytes {
								prevFiles += lastBytes
								lastFile = output.Name
							}
							lastBytes = output.Bytes
							delta := prevFiles + lastBytes - totalProgress
							totalProgress = bar.Add64(delta)
						}

						if progress {
							bar.Update()
						}
					case size := <-sizeChan:
						if progress {
							bar.Total = size
							bar.ShowPercent = true
							bar.ShowBar = true
							bar.ShowTimeLeft = true
						}
434
					}
Jan Winkelmann's avatar
Jan Winkelmann committed
435 436
				}
			}
437

Jan Winkelmann's avatar
Jan Winkelmann committed
438 439 440
			go func() {
				// defer order important! First close outChan, then wait for output to finish, then close re
				defer re.Close()
441

Jan Winkelmann's avatar
Jan Winkelmann committed
442 443 444 445
				if e := res.Error(); e != nil {
					defer close(outChan)
					re.SetError(e.Message, e.Code)
					return
446 447
				}

Jan Winkelmann's avatar
Jan Winkelmann committed
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
				wait := make(chan struct{})
				go progressBar(wait)

				defer func() { <-wait }()
				defer close(outChan)

				for {
					v, err := res.Next()
					if err != nil {
						// replace error by actual error - will be looked at by next if-statement
						if err == cmds.ErrRcvdError {
							err = res.Error()
						}

						if e, ok := err.(*cmdkit.Error); ok {
							re.Emit(e)
						} else if err != io.EOF {
							re.SetError(err, cmdkit.ErrNormal)
						}

						return
					}

471 472 473 474 475 476
					select {
					case outChan <- v:
					case <-ctx.Done():
						re.SetError(ctx.Err(), cmdkit.ErrNormal)
						return
					}
Jeromy's avatar
Jeromy committed
477
				}
Jan Winkelmann's avatar
Jan Winkelmann committed
478 479 480 481
			}()

			return reNext
		},
482
	},
483
	Type: coreunix.AddedObject{},
484
}