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

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

8
	bstore "github.com/ipfs/go-ipfs/blocks/blockstore"
9
	blockservice "github.com/ipfs/go-ipfs/blockservice"
10 11 12
	cmds "github.com/ipfs/go-ipfs/commands"
	files "github.com/ipfs/go-ipfs/commands/files"
	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
	u "gx/ipfs/QmWbjfz3u6HkAdPh34dgPchGbQjob6LXLhAeCGii2TX69n/go-ipfs-util"
21
	"gx/ipfs/QmeWjRodbcZFKe5tMN7poEx3izym6osrLSnTLf9UjJZBbs/pb"
22 23 24 25 26
)

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

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

44 45
const adderOutChanSize = 8

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

74
	Arguments: []cmds.Argument{
75
		cmds.FileArg("path", true, true, "The path to a file to be added to ipfs.").EnableRecursive().EnableStdin(),
76
	},
77 78
	Options: []cmds.Option{
		cmds.OptionRecursivePath, // a builtin option that allows recursive paths (-r, --recursive)
79
		cmds.BoolOption(quietOptionName, "q", "Write minimal output."),
80
		cmds.BoolOption(quieterOptionName, "Q", "Write only final hash."),
81
		cmds.BoolOption(silentOptionName, "Write no output."),
82
		cmds.BoolOption(progressOptionName, "p", "Stream progress data."),
83 84 85 86
		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."),
87
		cmds.StringOption(chunkerOptionName, "s", "Chunking algorithm to use."),
88
		cmds.BoolOption(pinOptionName, "Pin this object when adding.").Default(true),
89
		cmds.BoolOption(rawLeavesOptionName, "Use raw blocks for leaf nodes. (experimental)"),
90 91
		cmds.BoolOption(noCopyOptionName, "Add the file using filestore. (experimental)"),
		cmds.BoolOption(fstoreCacheOptionName, "Check the filestore for pre-existing blocks. (experimental)"),
92
		cmds.IntOption(cidVersionOptionName, "Cid version. Non-zero value will change default of 'raw-leaves' to true. (experimental)").Default(0),
93 94
	},
	PreRun: func(req cmds.Request) error {
95
		quiet, _, _ := req.Option(quietOptionName).Bool()
96 97 98
		quieter, _, _ := req.Option(quieterOptionName).Bool()
		quiet = quiet || quieter

99 100 101
		silent, _, _ := req.Option(silentOptionName).Bool()

		if quiet || silent {
102 103 104
			return nil
		}

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

111 112 113
		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
114
			log.Warning("cannot determine size of input file")
115 116 117
			return nil
		}

118 119
		sizeCh := make(chan int64, 1)
		req.Values()["size"] = sizeCh
rht's avatar
rht committed
120

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

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

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

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

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

168 169 170 171 172 173
		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
174 175 176 177
		if nocopy && !rbset {
			rawblks = true
		}

178 179 180 181
		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
182

183 184 185 186 187 188 189 190 191 192
		if cidVer >= 1 && !rbset {
			rawblks = true
		}

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

Jeromy's avatar
Jeromy committed
193
		if hash {
194 195 196
			nilnode, err := core.NewNode(n.Context(), &core.BuildCfg{
				//TODO: need this to be true or all files
				// hashed will be stored in memory!
197
				NilRepo: true,
198
			})
Jeromy's avatar
Jeromy committed
199 200 201 202 203 204
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
			n = nilnode
		}
205

206
		addblockstore := n.Blockstore
Jeromy's avatar
Jeromy committed
207
		if !(fscache || nocopy) {
208 209 210 211
			addblockstore = bstore.NewGCBlockstore(n.BaseBlocks, n.GCLocker)
		}

		exch := n.Exchange
212 213
		local, _, _ := req.Option("local").Bool()
		if local {
214
			exch = offline.Exchange(addblockstore)
215 216
		}

217 218 219
		bserv := blockservice.New(addblockstore, exch)
		dserv := dag.NewDAGService(bserv)

220
		fileAdder, err := coreunix.NewAdder(req.Context(), n.Pinning, n.Blockstore, dserv)
Jeromy's avatar
Jeromy committed
221 222 223 224
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
225

226
		outChan := make(chan interface{}, adderOutChanSize)
227 228
		res.SetOutput((<-chan interface{})(outChan))

229
		fileAdder.Out = outChan
230 231 232 233 234
		fileAdder.Chunker = chunker
		fileAdder.Progress = progress
		fileAdder.Hidden = hidden
		fileAdder.Trickle = trickle
		fileAdder.Wrap = wrap
235
		fileAdder.Pin = dopin
Jeromy's avatar
Jeromy committed
236
		fileAdder.Silent = silent
237
		fileAdder.RawLeaves = rawblks
238
		fileAdder.NoCopy = nocopy
239
		fileAdder.Prefix = &prefix
240

Jeromy's avatar
Jeromy committed
241 242
		if hash {
			md := dagtest.Mock()
243
			mr, err := mfs.NewRoot(req.Context(), md, ft.EmptyDirNode(), nil)
Jeromy's avatar
Jeromy committed
244 245 246 247 248 249 250 251
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}

			fileAdder.SetMfsRoot(mr)
		}

252
		addAllAndPin := func(f files.File) error {
253 254 255 256 257 258 259 260 261 262 263 264 265 266
			// 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
				}
267
			}
268

269
			// copy intermediary nodes from editor to our actual dagservice
Jeromy's avatar
Jeromy committed
270
			_, err := fileAdder.Finalize()
271 272
			if err != nil {
				return err
273 274
			}

Stephen Whitmore's avatar
Stephen Whitmore committed
275 276 277 278
			if hash {
				return nil
			}

279
			return fileAdder.PinRoot()
280 281 282 283 284
		}

		go func() {
			defer close(outChan)
			if err := addAllAndPin(req.Files()); err != nil {
285 286
				res.SetError(err, cmds.ErrNormal)
				return
287
			}
288

289
		}()
290
	},
291
	PostRun: func(req cmds.Request, res cmds.Response) {
292 293 294
		if res.Error() != nil {
			return
		}
295 296 297 298 299
		outChan, ok := res.Output().(<-chan interface{})
		if !ok {
			res.SetError(u.ErrCast(), cmds.ErrNormal)
			return
		}
300
		res.SetOutput(nil)
301

302 303 304
		quiet, _, _ := req.Option(quietOptionName).Bool()
		quieter, _, _ := req.Option(quieterOptionName).Bool()
		quiet = quiet || quieter
305

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

308
		var bar *pb.ProgressBar
309
		if progress {
310
			bar = pb.New64(0).SetUnits(pb.U_BYTES)
311
			bar.ManualUpdate = true
Jeromy's avatar
Jeromy committed
312 313 314
			bar.ShowTimeLeft = false
			bar.ShowPercent = false
			bar.Output = res.Stderr()
315 316 317
			bar.Start()
		}

318 319 320 321 322 323
		var sizeChan chan int64
		s, found := req.Values()["size"]
		if found {
			sizeChan = s.(chan int64)
		}

324
		lastFile := ""
325
		lastHash := ""
326
		var totalProgress, prevFiles, lastBytes int64
327

328 329 330 331 332
	LOOP:
		for {
			select {
			case out, ok := <-outChan:
				if !ok {
333 334 335
					if quieter {
						fmt.Fprintln(res.Stdout(), lastHash)
					}
336
					break LOOP
337
				}
338 339
				output := out.(*coreunix.AddedObject)
				if len(output.Hash) > 0 {
340 341 342 343 344
					lastHash = output.Hash
					if quieter {
						continue
					}

345
					if progress {
346 347 348 349 350 351 352 353 354 355 356
						// 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)

357
					if !progress {
358 359 360 361 362 363 364 365 366 367 368 369 370
						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)
371 372
				}

373
				if progress {
374
					bar.Update()
375
				}
376
			case size := <-sizeChan:
377
				if progress {
Jeromy's avatar
Jeromy committed
378 379 380 381 382
					bar.Total = size
					bar.ShowPercent = true
					bar.ShowBar = true
					bar.ShowTimeLeft = true
				}
Jeromy's avatar
Jeromy committed
383 384 385
			case <-req.Context().Done():
				res.SetError(req.Context().Err(), cmds.ErrNormal)
				return
386 387
			}
		}
388
	},
389
	Type: coreunix.AddedObject{},
390
}