add.go 8.98 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
	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 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
	u "gx/ipfs/Qmb912gdngC1UWwTkhuW8knyRbcWeu5kqkxBpveLmW8bSr/go-ipfs-util"
20 21 22 23 24
)

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

25
const (
gatesvp's avatar
gatesvp committed
26
	quietOptionName    = "quiet"
Jeromy's avatar
Jeromy committed
27
	silentOptionName   = "silent"
28
	progressOptionName = "progress"
29
	trickleOptionName  = "trickle"
30
	wrapOptionName     = "wrap-with-directory"
gatesvp's avatar
gatesvp committed
31 32
	hiddenOptionName   = "hidden"
	onlyHashOptionName = "only-hash"
33
	chunkerOptionName  = "chunker"
34
	pinOptionName      = "pin"
35
)
36

37
var AddCmd = &cmds.Command{
38
	Helptext: cmds.HelpText{
Richard Littauer's avatar
Richard Littauer committed
39
		Tagline: "Add a file to ipfs.",
40
		ShortDescription: `
Richard Littauer's avatar
Richard Littauer committed
41 42 43
Adds contents of <path> to ipfs. Use -r to add directories.
Note that directories are added recursively, to form the ipfs
MerkleDAG.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
`,
		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
64 65 66
`,
	},

67
	Arguments: []cmds.Argument{
68
		cmds.FileArg("path", true, true, "The path to a file to be added to IPFS.").EnableRecursive().EnableStdin(),
69
	},
70 71
	Options: []cmds.Option{
		cmds.OptionRecursivePath, // a builtin option that allows recursive paths (-r, --recursive)
Richard Littauer's avatar
Richard Littauer committed
72 73
		cmds.BoolOption(quietOptionName, "q", "Write minimal output.").Default(false),
		cmds.BoolOption(silentOptionName, "Write no output.").Default(false),
74
		cmds.BoolOption(progressOptionName, "p", "Stream progress data."),
Richard Littauer's avatar
Richard Littauer committed
75 76 77 78
		cmds.BoolOption(trickleOptionName, "t", "Use trickle-dag format for dag generation.").Default(false),
		cmds.BoolOption(onlyHashOptionName, "n", "Only chunk and hash - do not write to disk.").Default(false),
		cmds.BoolOption(wrapOptionName, "w", "Wrap files with a directory object.").Default(false),
		cmds.BoolOption(hiddenOptionName, "H", "Include files that are hidden. Only takes effect on recursive add.").Default(false),
79
		cmds.StringOption(chunkerOptionName, "s", "Chunking algorithm to use."),
Richard Littauer's avatar
Richard Littauer committed
80
		cmds.BoolOption(pinOptionName, "Pin this object when adding.").Default(true),
81 82
	},
	PreRun: func(req cmds.Request) error {
gatesvp's avatar
gatesvp committed
83
		if quiet, _, _ := req.Option(quietOptionName).Bool(); quiet {
84 85 86
			return nil
		}

87 88 89 90 91
		_, found, _ := req.Option(progressOptionName).Bool()
		if !found {
			req.SetOption(progressOptionName, true)
		}

92 93 94
		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
95
			log.Warning("cannnot determine size of input file")
96 97 98
			return nil
		}

99 100
		sizeCh := make(chan int64, 1)
		req.Values()["size"] = sizeCh
rht's avatar
rht committed
101

102 103 104
		go func() {
			size, err := sizeFile.Size()
			if err != nil {
Jeromy's avatar
Jeromy committed
105
				log.Warningf("error getting files size: %s", err)
106 107 108 109 110 111 112
				// see comment above
				return
			}

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

		return nil
115
	},
116
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
117
		n, err := req.InvocContext().GetNode()
118
		if err != nil {
119 120
			res.SetError(err, cmds.ErrNormal)
			return
121
		}
rht's avatar
rht committed
122 123 124 125 126 127 128
		// 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
		//}
129

130
		progress, _, _ := req.Option(progressOptionName).Bool()
131
		trickle, _, _ := req.Option(trickleOptionName).Bool()
132
		wrap, _, _ := req.Option(wrapOptionName).Bool()
gatesvp's avatar
gatesvp committed
133 134
		hash, _, _ := req.Option(onlyHashOptionName).Bool()
		hidden, _, _ := req.Option(hiddenOptionName).Bool()
Jeromy's avatar
Jeromy committed
135
		silent, _, _ := req.Option(silentOptionName).Bool()
136
		chunker, _, _ := req.Option(chunkerOptionName).String()
Richard Littauer's avatar
Richard Littauer committed
137
		dopin, _, _ := req.Option(pinOptionName).Bool()
Jeromy's avatar
Jeromy committed
138 139

		if hash {
140 141 142
			nilnode, err := core.NewNode(n.Context(), &core.BuildCfg{
				//TODO: need this to be true or all files
				// hashed will be stored in memory!
143
				NilRepo: true,
144
			})
Jeromy's avatar
Jeromy committed
145 146 147 148 149 150
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
			n = nilnode
		}
151

152 153 154 155 156 157 158 159
		dserv := n.DAG
		local, _, _ := req.Option("local").Bool()
		if local {
			offlineexch := offline.Exchange(n.Blockstore)
			bserv := blockservice.New(n.Blockstore, offlineexch)
			dserv = dag.NewDAGService(bserv)
		}

160
		outChan := make(chan interface{}, 8)
161
		res.SetOutput((<-chan interface{})(outChan))
162

163
		fileAdder, err := coreunix.NewAdder(req.Context(), n.Pinning, n.Blockstore, dserv)
Jeromy's avatar
Jeromy committed
164 165 166 167
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
168 169

		fileAdder.Out = outChan
170 171 172 173 174
		fileAdder.Chunker = chunker
		fileAdder.Progress = progress
		fileAdder.Hidden = hidden
		fileAdder.Trickle = trickle
		fileAdder.Wrap = wrap
175
		fileAdder.Pin = dopin
Jeromy's avatar
Jeromy committed
176
		fileAdder.Silent = silent
177

Jeromy's avatar
Jeromy committed
178 179
		if hash {
			md := dagtest.Mock()
180
			mr, err := mfs.NewRoot(req.Context(), md, ft.EmptyDirNode(), nil)
Jeromy's avatar
Jeromy committed
181 182 183 184 185 186 187 188
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}

			fileAdder.SetMfsRoot(mr)
		}

189
		addAllAndPin := func(f files.File) error {
190 191 192 193 194 195 196 197 198 199 200 201 202 203
			// 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
				}
204
			}
205

206
			// copy intermediary nodes from editor to our actual dagservice
Jeromy's avatar
Jeromy committed
207
			_, err := fileAdder.Finalize()
208 209
			if err != nil {
				return err
210 211
			}

Stephen Whitmore's avatar
Stephen Whitmore committed
212 213 214 215
			if hash {
				return nil
			}

216
			return fileAdder.PinRoot()
217 218 219 220 221
		}

		go func() {
			defer close(outChan)
			if err := addAllAndPin(req.Files()); err != nil {
222 223
				res.SetError(err, cmds.ErrNormal)
				return
224
			}
225

226
		}()
227
	},
228
	PostRun: func(req cmds.Request, res cmds.Response) {
229 230 231
		if res.Error() != nil {
			return
		}
232 233 234 235 236
		outChan, ok := res.Output().(<-chan interface{})
		if !ok {
			res.SetError(u.ErrCast(), cmds.ErrNormal)
			return
		}
237
		res.SetOutput(nil)
238

239
		quiet, _, err := req.Option("quiet").Bool()
240 241 242 243
		if err != nil {
			res.SetError(u.ErrCast(), cmds.ErrNormal)
			return
		}
244

Richard Littauer's avatar
Richard Littauer committed
245
		progress, _, err := req.Option(progressOptionName).Bool()
Jeromy's avatar
Jeromy committed
246 247 248 249 250
		if err != nil {
			res.SetError(u.ErrCast(), cmds.ErrNormal)
			return
		}

Jeromy's avatar
Jeromy committed
251 252 253 254 255 256
		silent, _, err := req.Option(silentOptionName).Bool()
		if err != nil {
			res.SetError(u.ErrCast(), cmds.ErrNormal)
			return
		}

257 258
		if !quiet && !silent {
			progress = true
Jeromy's avatar
Jeromy committed
259
		}
260 261

		var bar *pb.ProgressBar
262
		if progress {
263
			bar = pb.New64(0).SetUnits(pb.U_BYTES)
264
			bar.ManualUpdate = true
Jeromy's avatar
Jeromy committed
265 266 267
			bar.ShowTimeLeft = false
			bar.ShowPercent = false
			bar.Output = res.Stderr()
268 269 270
			bar.Start()
		}

271 272 273 274 275 276
		var sizeChan chan int64
		s, found := req.Values()["size"]
		if found {
			sizeChan = s.(chan int64)
		}

277 278
		lastFile := ""
		var totalProgress, prevFiles, lastBytes int64
279

280 281 282 283 284 285
	LOOP:
		for {
			select {
			case out, ok := <-outChan:
				if !ok {
					break LOOP
286
				}
287 288
				output := out.(*coreunix.AddedObject)
				if len(output.Hash) > 0 {
289
					if progress {
290 291 292 293 294 295 296 297
						// 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)
					}
298

299 300 301
				} else {
					log.Debugf("add progress: %v %v\n", output.Name, output.Bytes)

302
					if !progress {
303 304 305 306 307 308 309 310 311 312 313 314 315
						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)
316 317
				}

318
				if progress {
319
					bar.Update()
320
				}
321
			case size := <-sizeChan:
322
				if progress {
Jeromy's avatar
Jeromy committed
323 324 325 326 327
					bar.Total = size
					bar.ShowPercent = true
					bar.ShowBar = true
					bar.ShowTimeLeft = true
				}
Jeromy's avatar
Jeromy committed
328 329 330
			case <-req.Context().Done():
				res.SetError(req.Context().Err(), cmds.ErrNormal)
				return
331 332
			}
		}
333
	},
334
	Type: coreunix.AddedObject{},
335
}