add.go 10.3 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
)
42

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

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

95 96 97
		silent, _, _ := req.Option(silentOptionName).Bool()

		if quiet || silent {
98 99 100
			return nil
		}

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

107 108 109
		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
110
			log.Warning("cannnot determine size of input file")
111 112 113
			return nil
		}

114 115
		sizeCh := make(chan int64, 1)
		req.Values()["size"] = sizeCh
rht's avatar
rht committed
116

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

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

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

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

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

163 164 165 166 167 168
		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
169 170 171 172
		if nocopy && !rbset {
			rawblks = true
		}

173 174 175 176
		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
177 178

		if hash {
179 180 181
			nilnode, err := core.NewNode(n.Context(), &core.BuildCfg{
				//TODO: need this to be true or all files
				// hashed will be stored in memory!
182
				NilRepo: true,
183
			})
Jeromy's avatar
Jeromy committed
184 185 186 187 188 189
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
			n = nilnode
		}
190

191
		addblockstore := n.Blockstore
Jeromy's avatar
Jeromy committed
192
		if !(fscache || nocopy) {
193 194 195 196
			addblockstore = bstore.NewGCBlockstore(n.BaseBlocks, n.GCLocker)
		}

		exch := n.Exchange
197 198
		local, _, _ := req.Option("local").Bool()
		if local {
199
			exch = offline.Exchange(addblockstore)
200 201
		}

202 203 204
		bserv := blockservice.New(addblockstore, exch)
		dserv := dag.NewDAGService(bserv)

205
		outChan := make(chan interface{}, 8)
206
		res.SetOutput((<-chan interface{})(outChan))
207

208
		fileAdder, err := coreunix.NewAdder(req.Context(), n.Pinning, n.Blockstore, dserv)
Jeromy's avatar
Jeromy committed
209 210 211 212
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
213 214

		fileAdder.Out = outChan
215 216 217 218 219
		fileAdder.Chunker = chunker
		fileAdder.Progress = progress
		fileAdder.Hidden = hidden
		fileAdder.Trickle = trickle
		fileAdder.Wrap = wrap
220
		fileAdder.Pin = dopin
Jeromy's avatar
Jeromy committed
221
		fileAdder.Silent = silent
222
		fileAdder.RawLeaves = rawblks
223
		fileAdder.NoCopy = nocopy
224

Jeromy's avatar
Jeromy committed
225 226
		if hash {
			md := dagtest.Mock()
227
			mr, err := mfs.NewRoot(req.Context(), md, ft.EmptyDirNode(), nil)
Jeromy's avatar
Jeromy committed
228 229 230 231 232 233 234 235
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}

			fileAdder.SetMfsRoot(mr)
		}

236
		addAllAndPin := func(f files.File) error {
237 238 239 240 241 242 243 244 245 246 247 248 249 250
			// 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
				}
251
			}
252

253
			// copy intermediary nodes from editor to our actual dagservice
Jeromy's avatar
Jeromy committed
254
			_, err := fileAdder.Finalize()
255 256
			if err != nil {
				return err
257 258
			}

Stephen Whitmore's avatar
Stephen Whitmore committed
259 260 261 262
			if hash {
				return nil
			}

263
			return fileAdder.PinRoot()
264 265 266 267 268
		}

		go func() {
			defer close(outChan)
			if err := addAllAndPin(req.Files()); err != nil {
269 270
				res.SetError(err, cmds.ErrNormal)
				return
271
			}
272

273
		}()
274
	},
275
	PostRun: func(req cmds.Request, res cmds.Response) {
276 277 278
		if res.Error() != nil {
			return
		}
279 280 281 282 283
		outChan, ok := res.Output().(<-chan interface{})
		if !ok {
			res.SetError(u.ErrCast(), cmds.ErrNormal)
			return
		}
284
		res.SetOutput(nil)
285

286 287 288
		quiet, _, _ := req.Option(quietOptionName).Bool()
		quieter, _, _ := req.Option(quieterOptionName).Bool()
		quiet = quiet || quieter
289

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

292
		var bar *pb.ProgressBar
293
		if progress {
294
			bar = pb.New64(0).SetUnits(pb.U_BYTES)
295
			bar.ManualUpdate = true
Jeromy's avatar
Jeromy committed
296 297 298
			bar.ShowTimeLeft = false
			bar.ShowPercent = false
			bar.Output = res.Stderr()
299 300 301
			bar.Start()
		}

302 303 304 305 306 307
		var sizeChan chan int64
		s, found := req.Values()["size"]
		if found {
			sizeChan = s.(chan int64)
		}

308
		lastFile := ""
309
		lastHash := ""
310
		var totalProgress, prevFiles, lastBytes int64
311

312 313 314 315 316
	LOOP:
		for {
			select {
			case out, ok := <-outChan:
				if !ok {
317 318 319
					if quieter {
						fmt.Fprintln(res.Stdout(), lastHash)
					}
320
					break LOOP
321
				}
322 323
				output := out.(*coreunix.AddedObject)
				if len(output.Hash) > 0 {
324 325 326 327 328
					lastHash = output.Hash
					if quieter {
						continue
					}

329
					if progress {
330 331 332 333 334 335 336 337 338 339 340
						// 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)

341
					if !progress {
342 343 344 345 346 347 348 349 350 351 352 353 354
						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)
355 356
				}

357
				if progress {
358
					bar.Update()
359
				}
360
			case size := <-sizeChan:
361
				if progress {
Jeromy's avatar
Jeromy committed
362 363 364 365 366
					bar.Total = size
					bar.ShowPercent = true
					bar.ShowBar = true
					bar.ShowTimeLeft = true
				}
Jeromy's avatar
Jeromy committed
367 368 369
			case <-req.Context().Done():
				res.SetError(req.Context().Err(), cmds.ErrNormal)
				return
370 371
			}
		}
372
	},
373
	Type: coreunix.AddedObject{},
374
}