add.go 9.8 KB
Newer Older
1 2 3 4
package commands

import (
	"fmt"
5
	"io"
6

7
	"github.com/ipfs/go-ipfs/core/coreunix"
Jakub Sztandera's avatar
Jakub Sztandera committed
8
	"gx/ipfs/QmeWjRodbcZFKe5tMN7poEx3izym6osrLSnTLf9UjJZBbs/pb"
Jeromy's avatar
Jeromy committed
9

10
	bstore "github.com/ipfs/go-ipfs/blocks/blockstore"
11
	blockservice "github.com/ipfs/go-ipfs/blockservice"
12 13 14
	cmds "github.com/ipfs/go-ipfs/commands"
	files "github.com/ipfs/go-ipfs/commands/files"
	core "github.com/ipfs/go-ipfs/core"
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
	u "gx/ipfs/QmZuY8aV7zbNXVy6DyN9SmnuH3o9nG852F4aTiSBpts8d1/go-ipfs-util"
21 22 23 24 25
)

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

26
const (
27 28 29 30 31 32 33 34 35 36 37 38
	quietOptionName       = "quiet"
	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"
39
)
40

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

69
	Arguments: []cmds.Argument{
70
		cmds.FileArg("path", true, true, "The path to a file to be added to ipfs.").EnableRecursive().EnableStdin(),
71
	},
72 73
	Options: []cmds.Option{
		cmds.OptionRecursivePath, // a builtin option that allows recursive paths (-r, --recursive)
74 75
		cmds.BoolOption(quietOptionName, "q", "Write minimal output."),
		cmds.BoolOption(silentOptionName, "Write no output."),
76
		cmds.BoolOption(progressOptionName, "p", "Stream progress data."),
77 78 79 80
		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."),
81
		cmds.StringOption(chunkerOptionName, "s", "Chunking algorithm to use."),
82
		cmds.BoolOption(pinOptionName, "Pin this object when adding.").Default(true),
83
		cmds.BoolOption(rawLeavesOptionName, "Use raw blocks for leaf nodes. (experimental)"),
84 85
		cmds.BoolOption(noCopyOptionName, "Add the file using filestore. (experimental)"),
		cmds.BoolOption(fstoreCacheOptionName, "Check the filestore for pre-existing blocks. (experimental)"),
86 87
	},
	PreRun: func(req cmds.Request) error {
88 89 90 91
		quiet, _, _ := req.Option(quietOptionName).Bool()
		silent, _, _ := req.Option(silentOptionName).Bool()

		if quiet || silent {
92 93 94
			return nil
		}

95
		// ipfs cli progress bar defaults to true unless quiet or silent is used
96 97 98 99 100
		_, found, _ := req.Option(progressOptionName).Bool()
		if !found {
			req.SetOption(progressOptionName, true)
		}

101 102 103
		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
Jeromy's avatar
Jeromy committed
104
			log.Warning("cannnot determine size of input file")
105 106 107
			return nil
		}

108 109
		sizeCh := make(chan int64, 1)
		req.Values()["size"] = sizeCh
rht's avatar
rht committed
110

111 112 113
		go func() {
			size, err := sizeFile.Size()
			if err != nil {
Jeromy's avatar
Jeromy committed
114
				log.Warningf("error getting files size: %s", err)
115 116 117 118 119 120 121
				// see comment above
				return
			}

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

		return nil
124
	},
125
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
126
		n, err := req.InvocContext().GetNode()
127
		if err != nil {
128 129
			res.SetError(err, cmds.ErrNormal)
			return
130
		}
rht's avatar
rht committed
131 132 133 134 135 136 137
		// 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
		//}
138

139
		progress, _, _ := req.Option(progressOptionName).Bool()
140
		trickle, _, _ := req.Option(trickleOptionName).Bool()
141
		wrap, _, _ := req.Option(wrapOptionName).Bool()
gatesvp's avatar
gatesvp committed
142 143
		hash, _, _ := req.Option(onlyHashOptionName).Bool()
		hidden, _, _ := req.Option(hiddenOptionName).Bool()
Jeromy's avatar
Jeromy committed
144
		silent, _, _ := req.Option(silentOptionName).Bool()
145
		chunker, _, _ := req.Option(chunkerOptionName).String()
146
		dopin, _, _ := req.Option(pinOptionName).Bool()
Jeromy's avatar
Jeromy committed
147
		rawblks, rbset, _ := req.Option(rawLeavesOptionName).Bool()
148 149 150
		nocopy, _, _ := req.Option(noCopyOptionName).Bool()
		fscache, _, _ := req.Option(fstoreCacheOptionName).Bool()

Jeromy's avatar
Jeromy committed
151 152 153 154
		if nocopy && !rbset {
			rawblks = true
		}

155 156 157 158
		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
159 160

		if hash {
161 162 163
			nilnode, err := core.NewNode(n.Context(), &core.BuildCfg{
				//TODO: need this to be true or all files
				// hashed will be stored in memory!
164
				NilRepo: true,
165
			})
Jeromy's avatar
Jeromy committed
166 167 168 169 170 171
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
			n = nilnode
		}
172

173
		addblockstore := n.Blockstore
Jeromy's avatar
Jeromy committed
174
		if !(fscache || nocopy) {
175 176 177 178
			addblockstore = bstore.NewGCBlockstore(n.BaseBlocks, n.GCLocker)
		}

		exch := n.Exchange
179 180
		local, _, _ := req.Option("local").Bool()
		if local {
181
			exch = offline.Exchange(addblockstore)
182 183
		}

184 185 186
		bserv := blockservice.New(addblockstore, exch)
		dserv := dag.NewDAGService(bserv)

187
		outChan := make(chan interface{}, 8)
188
		res.SetOutput((<-chan interface{})(outChan))
189

190
		fileAdder, err := coreunix.NewAdder(req.Context(), n.Pinning, n.Blockstore, dserv)
Jeromy's avatar
Jeromy committed
191 192 193 194
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
195 196

		fileAdder.Out = outChan
197 198 199 200 201
		fileAdder.Chunker = chunker
		fileAdder.Progress = progress
		fileAdder.Hidden = hidden
		fileAdder.Trickle = trickle
		fileAdder.Wrap = wrap
202
		fileAdder.Pin = dopin
Jeromy's avatar
Jeromy committed
203
		fileAdder.Silent = silent
204
		fileAdder.RawLeaves = rawblks
205
		fileAdder.NoCopy = nocopy
206

Jeromy's avatar
Jeromy committed
207 208
		if hash {
			md := dagtest.Mock()
209
			mr, err := mfs.NewRoot(req.Context(), md, ft.EmptyDirNode(), nil)
Jeromy's avatar
Jeromy committed
210 211 212 213 214 215 216 217
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}

			fileAdder.SetMfsRoot(mr)
		}

218
		addAllAndPin := func(f files.File) error {
219 220 221 222 223 224 225 226 227 228 229 230 231 232
			// 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
				}
233
			}
234

235
			// copy intermediary nodes from editor to our actual dagservice
Jeromy's avatar
Jeromy committed
236
			_, err := fileAdder.Finalize()
237 238
			if err != nil {
				return err
239 240
			}

Stephen Whitmore's avatar
Stephen Whitmore committed
241 242 243 244
			if hash {
				return nil
			}

245
			return fileAdder.PinRoot()
246 247 248 249 250
		}

		go func() {
			defer close(outChan)
			if err := addAllAndPin(req.Files()); err != nil {
251 252
				res.SetError(err, cmds.ErrNormal)
				return
253
			}
254

255
		}()
256
	},
257
	PostRun: func(req cmds.Request, res cmds.Response) {
258 259 260
		if res.Error() != nil {
			return
		}
261 262 263 264 265
		outChan, ok := res.Output().(<-chan interface{})
		if !ok {
			res.SetError(u.ErrCast(), cmds.ErrNormal)
			return
		}
266
		res.SetOutput(nil)
267

268
		quiet, _, err := req.Option("quiet").Bool()
269 270 271 272
		if err != nil {
			res.SetError(u.ErrCast(), cmds.ErrNormal)
			return
		}
273

274
		progress, _, err := req.Option(progressOptionName).Bool()
Jeromy's avatar
Jeromy committed
275 276 277 278 279
		if err != nil {
			res.SetError(u.ErrCast(), cmds.ErrNormal)
			return
		}

280
		var bar *pb.ProgressBar
281
		if progress {
282
			bar = pb.New64(0).SetUnits(pb.U_BYTES)
283
			bar.ManualUpdate = true
Jeromy's avatar
Jeromy committed
284 285 286
			bar.ShowTimeLeft = false
			bar.ShowPercent = false
			bar.Output = res.Stderr()
287 288 289
			bar.Start()
		}

290 291 292 293 294 295
		var sizeChan chan int64
		s, found := req.Values()["size"]
		if found {
			sizeChan = s.(chan int64)
		}

296 297
		lastFile := ""
		var totalProgress, prevFiles, lastBytes int64
298

299 300 301 302 303 304
	LOOP:
		for {
			select {
			case out, ok := <-outChan:
				if !ok {
					break LOOP
305
				}
306 307
				output := out.(*coreunix.AddedObject)
				if len(output.Hash) > 0 {
308
					if progress {
309 310 311 312 313 314 315 316
						// 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)
					}
317

318 319 320
				} else {
					log.Debugf("add progress: %v %v\n", output.Name, output.Bytes)

321
					if !progress {
322 323 324 325 326 327 328 329 330 331 332 333 334
						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)
335 336
				}

337
				if progress {
338
					bar.Update()
339
				}
340
			case size := <-sizeChan:
341
				if progress {
Jeromy's avatar
Jeromy committed
342 343 344 345 346
					bar.Total = size
					bar.ShowPercent = true
					bar.ShowBar = true
					bar.ShowTimeLeft = true
				}
Jeromy's avatar
Jeromy committed
347 348 349
			case <-req.Context().Done():
				res.SetError(req.Context().Err(), cmds.ErrNormal)
				return
350 351
			}
		}
352
	},
353
	Type: coreunix.AddedObject{},
354
}