add.go 14.6 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

20
	"gx/ipfs/QmUEB5nT4LG3TkUd5mkHrfRESUSgaUD4r7jSAYvvPeuWT9/go-ipfs-cmds"
Steven Allen's avatar
Steven Allen committed
21
	mh "gx/ipfs/QmYeKnKpubCMRiq3PGZcTREErthbb5Q9cXsCoSkD9bjEBd/go-multihash"
22 23
	"gx/ipfs/QmceUdzxkimdYsgtX733uNgzf1DLHyBKN6ehGSp85ayppM/go-ipfs-cmdkit"
	"gx/ipfs/QmceUdzxkimdYsgtX733uNgzf1DLHyBKN6ehGSp85ayppM/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
	Options: []cmdkit.Option{
106
		cmds.OptionRecursivePath, // a builtin option that allows recursive paths (-r, --recursive)
Jan Winkelmann's avatar
Jan Winkelmann committed
107 108 109 110 111 112 113 114
		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),
117 118
		cmdkit.BoolOption(rawLeavesOptionName, "Use raw blocks for leaf nodes. Implies CIDv1, defaults to on if CIDv1 is enabled. (experimental)"),
		cmdkit.BoolOption(noCopyOptionName, "Add the file using filestore. Implies raw-leaves. (experimental)"),
Jan Winkelmann's avatar
Jan Winkelmann committed
119
		cmdkit.BoolOption(fstoreCacheOptionName, "Check the filestore for pre-existing blocks. (experimental)"),
120 121
		cmdkit.IntOption(cidVersionOptionName, "CID version. Defaults to 0 unless an option that depends on CIDv1 is passed. (experimental)"),
		cmdkit.StringOption(hashOptionName, "Hash function to use. Implies CIDv1 if not sha2-256. (experimental)").WithDefault("sha2-256"),
122
	},
Jeromy's avatar
Jeromy committed
123
	PreRun: func(req *cmds.Request, env cmds.Environment) error {
124 125
		quiet, _ := req.Options[quietOptionName].(bool)
		quieter, _ := req.Options[quieterOptionName].(bool)
126 127
		quiet = quiet || quieter

128
		silent, _ := req.Options[silentOptionName].(bool)
129 130

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

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

140
		return nil
141
	},
Jeromy's avatar
Jeromy committed
142
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) {
143
		n, err := GetNode(env)
144
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
145
			res.SetError(err, cmdkit.ErrNormal)
146
			return
147
		}
148 149 150

		cfg, err := n.Repo.Config()
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
151
			res.SetError(err, cmdkit.ErrNormal)
152 153
			return
		}
rht's avatar
rht committed
154 155 156 157
		// 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
158
		//	res.SetError(err, cmdkit.ErrNormal)
rht's avatar
rht committed
159 160
		//	return
		//}
161

162 163 164 165 166 167 168 169 170 171 172
		progress, _ := req.Options[progressOptionName].(bool)
		trickle, _ := req.Options[trickleOptionName].(bool)
		wrap, _ := req.Options[wrapOptionName].(bool)
		hash, _ := req.Options[onlyHashOptionName].(bool)
		hidden, _ := req.Options[hiddenOptionName].(bool)
		silent, _ := req.Options[silentOptionName].(bool)
		chunker, _ := req.Options[chunkerOptionName].(string)
		dopin, _ := req.Options[pinOptionName].(bool)
		rawblks, rbset := req.Options[rawLeavesOptionName].(bool)
		nocopy, _ := req.Options[noCopyOptionName].(bool)
		fscache, _ := req.Options[fstoreCacheOptionName].(bool)
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
		cidVer, cidVerSet := req.Options[cidVersionOptionName].(int)
		hashFunStr, _ := req.Options[hashOptionName].(string)

		// Given the following constraints:
		//
		// nocopy -> filestoreEnabled
		// nocopy -> rawblocks
		// rawblocks -> cidv1
		// (hash != sha2-256) -> cidv1
		//
		// We solve for the values of rawblocks and cidv1 in the
		// following order of preference:
		//
		// 1. If cidv1 isn't fixed, set it to false and try solving.
		// 2. If rawblocks isn't fixed, set it to true and try solving.
		//
		// If neither solution works, give up (we have a conflict).

		// nocopy -> filestorEnabled
192 193
		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
194
				cmdkit.ErrClient)
195 196 197
			return
		}

198 199 200 201 202 203 204 205 206 207 208
		// nocopy -> rawblocks
		if nocopy && !rawblks {
			// fixed?
			if rbset {
				res.SetError(
					fmt.Errorf("nocopy option requires '--raw-leaves' to be enabled as well"),
					cmdkit.ErrNormal,
				)
				return
			}
			// No, satisfy mandatory constraint.
keks's avatar
keks committed
209 210 211
			rawblks = true
		}

212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
		if !cidVerSet {
			// Default to CIDv0 if possible.
			// Conditions: no raw blocks, sha2-256
			if hashFunStr == "sha2-256" && !rawblks {
				cidVer = 0
			} else {
				cidVer = 1
			}
		} else if cidVer == 0 {
			// CIDv0 *was* set...
			if hashFunStr != "sha2-256" {
				res.SetError(errors.New("CIDv0 only supports sha2-256"), cmdkit.ErrClient)
				return
			} else if rawblks {
				res.SetError(
					errors.New("CIDv0 incompatible with raw-leaves and/or nocopy"),
					cmdkit.ErrClient,
				)
				return
			}
Jeromy's avatar
Jeromy committed
232 233
		}

234 235 236
		// cidV1 -> raw blocks (by default)
		if cidVer > 0 && !rbset {
			rawblks = true
237
		}
Jeromy's avatar
Jeromy committed
238

239 240
		prefix, err := dag.PrefixForCidVersion(cidVer)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
241
			res.SetError(err, cmdkit.ErrNormal)
242 243 244
			return
		}

245 246
		hashFunCode, ok := mh.Names[strings.ToLower(hashFunStr)]
		if !ok {
Jan Winkelmann's avatar
Jan Winkelmann committed
247
			res.SetError(fmt.Errorf("unrecognized hash function: %s", strings.ToLower(hashFunStr)), cmdkit.ErrNormal)
248 249 250 251 252 253
			return
		}

		prefix.MhType = hashFunCode
		prefix.MhLength = -1

Jeromy's avatar
Jeromy committed
254
		if hash {
255 256 257
			nilnode, err := core.NewNode(n.Context(), &core.BuildCfg{
				//TODO: need this to be true or all files
				// hashed will be stored in memory!
258
				NilRepo: true,
259
			})
Jeromy's avatar
Jeromy committed
260
			if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
261
				res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
262 263 264 265
				return
			}
			n = nilnode
		}
266

267
		addblockstore := n.Blockstore
Jeromy's avatar
Jeromy committed
268
		if !(fscache || nocopy) {
269 270 271 272
			addblockstore = bstore.NewGCBlockstore(n.BaseBlocks, n.GCLocker)
		}

		exch := n.Exchange
273
		local, _ := req.Options["local"].(bool)
274
		if local {
275
			exch = offline.Exchange(addblockstore)
276 277
		}

278 279 280
		bserv := blockservice.New(addblockstore, exch)
		dserv := dag.NewDAGService(bserv)

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

283
		fileAdder, err := coreunix.NewAdder(req.Context, n.Pinning, n.Blockstore, dserv)
Jeromy's avatar
Jeromy committed
284
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
285
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
286 287
			return
		}
288 289

		fileAdder.Out = outChan
290 291 292 293 294
		fileAdder.Chunker = chunker
		fileAdder.Progress = progress
		fileAdder.Hidden = hidden
		fileAdder.Trickle = trickle
		fileAdder.Wrap = wrap
295
		fileAdder.Pin = dopin
Jeromy's avatar
Jeromy committed
296
		fileAdder.Silent = silent
297
		fileAdder.RawLeaves = rawblks
298
		fileAdder.NoCopy = nocopy
299
		fileAdder.Prefix = &prefix
300

Jeromy's avatar
Jeromy committed
301 302
		if hash {
			md := dagtest.Mock()
303
			mr, err := mfs.NewRoot(req.Context, md, ft.EmptyDirNode(), nil)
Jeromy's avatar
Jeromy committed
304
			if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
305
				res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
306 307 308 309 310 311
				return
			}

			fileAdder.SetMfsRoot(mr)
		}

312
		addAllAndPin := func(f files.File) error {
313 314 315 316 317 318 319 320 321 322 323 324 325 326
			// 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
				}
327
			}
328

329
			// copy intermediary nodes from editor to our actual dagservice
Jeromy's avatar
Jeromy committed
330
			_, err := fileAdder.Finalize()
331 332
			if err != nil {
				return err
333 334
			}

Stephen Whitmore's avatar
Stephen Whitmore committed
335 336 337 338
			if hash {
				return nil
			}

339
			return fileAdder.PinRoot()
340 341
		}

Jan Winkelmann's avatar
Jan Winkelmann committed
342
		errCh := make(chan error)
343
		go func() {
Jan Winkelmann's avatar
Jan Winkelmann committed
344 345
			var err error
			defer func() { errCh <- err }()
346
			defer close(outChan)
347
			err = addAllAndPin(req.Files)
348
		}()
Jan Winkelmann's avatar
Jan Winkelmann committed
349 350 351 352 353 354

		defer res.Close()

		err = res.Emit(outChan)
		if err != nil {
			log.Error(err)
355 356
			return
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
357 358 359
		err = <-errCh
		if err != nil {
			res.SetError(err, cmdkit.ErrNormal)
360
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
361
	},
362 363
	PostRun: cmds.PostRunMap{
		cmds.CLI: func(req *cmds.Request, re cmds.ResponseEmitter) cmds.ResponseEmitter {
Jan Winkelmann's avatar
Jan Winkelmann committed
364 365
			reNext, res := cmds.NewChanResponsePair(req)
			outChan := make(chan interface{})
366

Steven Allen's avatar
Steven Allen committed
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
			sizeChan := make(chan int64, 1)

			sizeFile, ok := req.Files.(files.SizeFile)
			if ok {
				// Could be slow.
				go func() {
					size, err := sizeFile.Size()
					if err != nil {
						log.Warningf("error getting files size: %s", err)
						// see comment above
						return
					}

					sizeChan <- size
				}()
			} else {
				// we don't need to error, the progress bar just
				// won't know how big the files are
				log.Warning("cannot determine size of input file")
			}

Jan Winkelmann's avatar
Jan Winkelmann committed
388 389
			progressBar := func(wait chan struct{}) {
				defer close(wait)
390

391 392
				quiet, _ := req.Options[quietOptionName].(bool)
				quieter, _ := req.Options[quieterOptionName].(bool)
Jan Winkelmann's avatar
Jan Winkelmann committed
393
				quiet = quiet || quieter
Jeromy's avatar
Jeromy committed
394

395
				progress, _ := req.Options[progressOptionName].(bool)
396

Jan Winkelmann's avatar
Jan Winkelmann committed
397 398 399 400 401 402 403 404 405
				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()
				}
406

Jan Winkelmann's avatar
Jan Winkelmann committed
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 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465
				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
						}
466
					case <-req.Context.Done():
keks's avatar
cleanup  
keks committed
467
						// don't set or print error here, that happens in the goroutine below
468
						return
469
					}
Jan Winkelmann's avatar
Jan Winkelmann committed
470 471
				}
			}
472

Jan Winkelmann's avatar
Jan Winkelmann committed
473 474 475
			go func() {
				// defer order important! First close outChan, then wait for output to finish, then close re
				defer re.Close()
476

Jan Winkelmann's avatar
Jan Winkelmann committed
477 478 479 480
				if e := res.Error(); e != nil {
					defer close(outChan)
					re.SetError(e.Message, e.Code)
					return
481 482
				}

Jan Winkelmann's avatar
Jan Winkelmann committed
483 484 485 486 487 488 489 490
				wait := make(chan struct{})
				go progressBar(wait)

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

				for {
					v, err := res.Next()
491 492
					if !cmds.HandleError(err, res, re) {
						break
Jan Winkelmann's avatar
Jan Winkelmann committed
493 494
					}

495 496
					select {
					case outChan <- v:
497 498
					case <-req.Context.Done():
						re.SetError(req.Context.Err(), cmdkit.ErrNormal)
499 500
						return
					}
Jeromy's avatar
Jeromy committed
501
				}
Jan Winkelmann's avatar
Jan Winkelmann committed
502 503 504 505
			}()

			return reNext
		},
506
	},
507
	Type: coreunix.AddedObject{},
508
}