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

import (
4
	"errors"
5
	"fmt"
6
	"io"
7
	"strings"
8

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

21
	mh "gx/ipfs/QmVGtdTZdTFaLsaj2RwdVG8jcjNNcp1DE914DKZ2kHmXHw/go-multihash"
22
	u "gx/ipfs/QmWbjfz3u6HkAdPh34dgPchGbQjob6LXLhAeCGii2TX69n/go-ipfs-util"
23
	"gx/ipfs/QmeWjRodbcZFKe5tMN7poEx3izym6osrLSnTLf9UjJZBbs/pb"
24 25 26 27 28
)

// Error indicating the max depth has been exceded.
var ErrDepthLimitExceeded = fmt.Errorf("depth limit exceeded")

29
const (
30
	quietOptionName       = "quiet"
31
	quieterOptionName     = "quieter"
32 33 34 35 36 37 38 39 40 41 42
	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"
43
	cidVersionOptionName  = "cid-version"
44
	hashOptionName        = "hash"
45
)
46

47 48
const adderOutChanSize = 8

49
var AddCmd = &cmds.Command{
50
	Helptext: cmds.HelpText{
51
		Tagline: "Add a file or directory to ipfs.",
52
		ShortDescription: `
53
Adds contents of <path> to ipfs. Use -r to add directories (recursively).
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
`,
		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
74 75 76
`,
	},

77
	Arguments: []cmds.Argument{
78
		cmds.FileArg("path", true, true, "The path to a file to be added to ipfs.").EnableRecursive().EnableStdin(),
79
	},
80 81
	Options: []cmds.Option{
		cmds.OptionRecursivePath, // a builtin option that allows recursive paths (-r, --recursive)
82
		cmds.BoolOption(quietOptionName, "q", "Write minimal output."),
83
		cmds.BoolOption(quieterOptionName, "Q", "Write only final hash."),
84
		cmds.BoolOption(silentOptionName, "Write no output."),
85
		cmds.BoolOption(progressOptionName, "p", "Stream progress data."),
86 87 88 89
		cmds.BoolOption(trickleOptionName, "t", "Use trickle-dag format for dag generation."),
		cmds.BoolOption(onlyHashOptionName, "n", "Only chunk and hash - do not write to disk."),
		cmds.BoolOption(wrapOptionName, "w", "Wrap files with a directory object."),
		cmds.BoolOption(hiddenOptionName, "H", "Include files that are hidden. Only takes effect on recursive add."),
90
		cmds.StringOption(chunkerOptionName, "s", "Chunking algorithm to use."),
91
		cmds.BoolOption(pinOptionName, "Pin this object when adding.").Default(true),
92
		cmds.BoolOption(rawLeavesOptionName, "Use raw blocks for leaf nodes. (experimental)"),
93 94
		cmds.BoolOption(noCopyOptionName, "Add the file using filestore. (experimental)"),
		cmds.BoolOption(fstoreCacheOptionName, "Check the filestore for pre-existing blocks. (experimental)"),
95
		cmds.IntOption(cidVersionOptionName, "Cid version. Non-zero value will change default of 'raw-leaves' to true. (experimental)").Default(0),
96
		cmds.StringOption(hashOptionName, "Hash function to use. Will set Cid version to 1 if used. (experimental)").Default("sha2-256"),
97 98
	},
	PreRun: func(req cmds.Request) error {
99
		quiet, _, _ := req.Option(quietOptionName).Bool()
100 101 102
		quieter, _, _ := req.Option(quieterOptionName).Bool()
		quiet = quiet || quieter

103 104 105
		silent, _, _ := req.Option(silentOptionName).Bool()

		if quiet || silent {
106 107 108
			return nil
		}

109
		// ipfs cli progress bar defaults to true unless quiet or silent is used
110 111 112 113 114
		_, found, _ := req.Option(progressOptionName).Bool()
		if !found {
			req.SetOption(progressOptionName, true)
		}

115 116 117
		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
118
			log.Warning("cannot determine size of input file")
119 120 121
			return nil
		}

122 123
		sizeCh := make(chan int64, 1)
		req.Values()["size"] = sizeCh
rht's avatar
rht committed
124

125 126 127
		go func() {
			size, err := sizeFile.Size()
			if err != nil {
Jeromy's avatar
Jeromy committed
128
				log.Warningf("error getting files size: %s", err)
129 130 131 132 133 134 135
				// see comment above
				return
			}

			log.Debugf("Total size of file being added: %v\n", size)
			sizeCh <- size
		}()
136 137

		return nil
138
	},
139
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
140
		n, err := req.InvocContext().GetNode()
141
		if err != nil {
142 143
			res.SetError(err, cmds.ErrNormal)
			return
144
		}
145 146 147 148 149 150

		cfg, err := n.Repo.Config()
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
rht's avatar
rht committed
151 152 153 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 {
		//	res.SetError(err, cmds.ErrNormal)
		//	return
		//}
158

159
		progress, _, _ := req.Option(progressOptionName).Bool()
160
		trickle, _, _ := req.Option(trickleOptionName).Bool()
161
		wrap, _, _ := req.Option(wrapOptionName).Bool()
gatesvp's avatar
gatesvp committed
162 163
		hash, _, _ := req.Option(onlyHashOptionName).Bool()
		hidden, _, _ := req.Option(hiddenOptionName).Bool()
Jeromy's avatar
Jeromy committed
164
		silent, _, _ := req.Option(silentOptionName).Bool()
165
		chunker, _, _ := req.Option(chunkerOptionName).String()
166
		dopin, _, _ := req.Option(pinOptionName).Bool()
Jeromy's avatar
Jeromy committed
167
		rawblks, rbset, _ := req.Option(rawLeavesOptionName).Bool()
168 169
		nocopy, _, _ := req.Option(noCopyOptionName).Bool()
		fscache, _, _ := req.Option(fstoreCacheOptionName).Bool()
170
		cidVer, _, _ := req.Option(cidVersionOptionName).Int()
171
		hashFunStr, hfset, _ := req.Option(hashOptionName).String()
172

173 174 175 176 177 178
		if nocopy && !cfg.Experimental.FilestoreEnabled {
			res.SetError(errors.New("filestore is not enabled, see https://git.io/vy4XN"),
				cmds.ErrClient)
			return
		}

Jeromy's avatar
Jeromy committed
179 180 181 182
		if nocopy && !rbset {
			rawblks = true
		}

183 184 185 186
		if nocopy && !rawblks {
			res.SetError(fmt.Errorf("nocopy option requires '--raw-leaves' to be enabled as well"), cmds.ErrNormal)
			return
		}
Jeromy's avatar
Jeromy committed
187

188 189 190 191
		if hfset && cidVer == 0 {
			cidVer = 1
		}

192 193 194 195 196 197 198 199 200 201
		if cidVer >= 1 && !rbset {
			rawblks = true
		}

		prefix, err := dag.PrefixForCidVersion(cidVer)
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

202 203 204 205 206 207 208 209 210
		hashFunCode, ok := mh.Names[strings.ToLower(hashFunStr)]
		if !ok {
			res.SetError(fmt.Errorf("unrecognized hash function: %s", strings.ToLower(hashFunStr)), cmds.ErrNormal)
			return
		}

		prefix.MhType = hashFunCode
		prefix.MhLength = -1

Jeromy's avatar
Jeromy committed
211
		if hash {
212 213 214
			nilnode, err := core.NewNode(n.Context(), &core.BuildCfg{
				//TODO: need this to be true or all files
				// hashed will be stored in memory!
215
				NilRepo: true,
216
			})
Jeromy's avatar
Jeromy committed
217 218 219 220 221 222
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
			n = nilnode
		}
223

224
		addblockstore := n.Blockstore
Jeromy's avatar
Jeromy committed
225
		if !(fscache || nocopy) {
226 227 228 229
			addblockstore = bstore.NewGCBlockstore(n.BaseBlocks, n.GCLocker)
		}

		exch := n.Exchange
230 231
		local, _, _ := req.Option("local").Bool()
		if local {
232
			exch = offline.Exchange(addblockstore)
233 234
		}

235 236 237
		bserv := blockservice.New(addblockstore, exch)
		dserv := dag.NewDAGService(bserv)

238
		fileAdder, err := coreunix.NewAdder(req.Context(), n.Pinning, n.Blockstore, dserv)
Jeromy's avatar
Jeromy committed
239 240 241 242
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
243

244
		outChan := make(chan interface{}, adderOutChanSize)
245 246
		res.SetOutput((<-chan interface{})(outChan))

247
		fileAdder.Out = outChan
248 249 250 251 252
		fileAdder.Chunker = chunker
		fileAdder.Progress = progress
		fileAdder.Hidden = hidden
		fileAdder.Trickle = trickle
		fileAdder.Wrap = wrap
253
		fileAdder.Pin = dopin
Jeromy's avatar
Jeromy committed
254
		fileAdder.Silent = silent
255
		fileAdder.RawLeaves = rawblks
256
		fileAdder.NoCopy = nocopy
257
		fileAdder.Prefix = &prefix
258

Jeromy's avatar
Jeromy committed
259 260
		if hash {
			md := dagtest.Mock()
261
			mr, err := mfs.NewRoot(req.Context(), md, ft.EmptyDirNode(), nil)
Jeromy's avatar
Jeromy committed
262 263 264 265 266 267 268 269
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}

			fileAdder.SetMfsRoot(mr)
		}

270
		addAllAndPin := func(f files.File) error {
271 272 273 274 275 276 277 278 279 280 281 282 283 284
			// 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
				}
285
			}
286

287
			// copy intermediary nodes from editor to our actual dagservice
Jeromy's avatar
Jeromy committed
288
			_, err := fileAdder.Finalize()
289 290
			if err != nil {
				return err
291 292
			}

Stephen Whitmore's avatar
Stephen Whitmore committed
293 294 295 296
			if hash {
				return nil
			}

297
			return fileAdder.PinRoot()
298 299 300 301 302
		}

		go func() {
			defer close(outChan)
			if err := addAllAndPin(req.Files()); err != nil {
303 304
				res.SetError(err, cmds.ErrNormal)
				return
305
			}
306

307
		}()
308
	},
309
	PostRun: func(req cmds.Request, res cmds.Response) {
310 311 312
		if res.Error() != nil {
			return
		}
313 314 315 316 317
		outChan, ok := res.Output().(<-chan interface{})
		if !ok {
			res.SetError(u.ErrCast(), cmds.ErrNormal)
			return
		}
318
		res.SetOutput(nil)
319

320 321 322
		quiet, _, _ := req.Option(quietOptionName).Bool()
		quieter, _, _ := req.Option(quieterOptionName).Bool()
		quiet = quiet || quieter
323

324
		progress, _, _ := req.Option(progressOptionName).Bool()
Jeromy's avatar
Jeromy committed
325

326
		var bar *pb.ProgressBar
327
		if progress {
328
			bar = pb.New64(0).SetUnits(pb.U_BYTES)
329
			bar.ManualUpdate = true
Jeromy's avatar
Jeromy committed
330 331 332
			bar.ShowTimeLeft = false
			bar.ShowPercent = false
			bar.Output = res.Stderr()
333 334 335
			bar.Start()
		}

336 337 338 339 340 341
		var sizeChan chan int64
		s, found := req.Values()["size"]
		if found {
			sizeChan = s.(chan int64)
		}

342
		lastFile := ""
343
		lastHash := ""
344
		var totalProgress, prevFiles, lastBytes int64
345

346 347 348 349 350
	LOOP:
		for {
			select {
			case out, ok := <-outChan:
				if !ok {
351 352 353
					if quieter {
						fmt.Fprintln(res.Stdout(), lastHash)
					}
354
					break LOOP
355
				}
356 357
				output := out.(*coreunix.AddedObject)
				if len(output.Hash) > 0 {
358 359 360 361 362
					lastHash = output.Hash
					if quieter {
						continue
					}

363
					if progress {
364 365 366 367 368 369 370 371 372 373 374
						// clear progress bar line before we print "added x" output
						fmt.Fprintf(res.Stderr(), "\033[2K\r")
					}
					if quiet {
						fmt.Fprintf(res.Stdout(), "%s\n", output.Hash)
					} else {
						fmt.Fprintf(res.Stdout(), "added %s %s\n", output.Hash, output.Name)
					}
				} else {
					log.Debugf("add progress: %v %v\n", output.Name, output.Bytes)

375
					if !progress {
376 377 378 379 380 381 382 383 384 385 386 387 388
						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)
389 390
				}

391
				if progress {
392
					bar.Update()
393
				}
394
			case size := <-sizeChan:
395
				if progress {
Jeromy's avatar
Jeromy committed
396 397 398 399 400
					bar.Total = size
					bar.ShowPercent = true
					bar.ShowBar = true
					bar.ShowTimeLeft = true
				}
Jeromy's avatar
Jeromy committed
401 402 403
			case <-req.Context().Done():
				res.SetError(req.Context().Err(), cmds.ErrNormal)
				return
404 405
			}
		}
406
	},
407
	Type: coreunix.AddedObject{},
408
}