get.go 6.58 KB
Newer Older
Matt Bell's avatar
Matt Bell committed
1 2 3
package commands

import (
4
	"compress/gzip"
5
	"errors"
6
	"fmt"
Matt Bell's avatar
Matt Bell committed
7
	"io"
8
	"os"
Dominic Della Valle's avatar
Dominic Della Valle committed
9
	"path/filepath"
10
	"strings"
Matt Bell's avatar
Matt Bell committed
11

12
	core "github.com/ipfs/go-ipfs/core"
Jan Winkelmann's avatar
Jan Winkelmann committed
13
	e "github.com/ipfs/go-ipfs/core/commands/e"
14
	dag "github.com/ipfs/go-ipfs/merkledag"
15
	path "github.com/ipfs/go-ipfs/path"
rht's avatar
rht committed
16
	uarchive "github.com/ipfs/go-ipfs/unixfs/archive"
Jan Winkelmann's avatar
Jan Winkelmann committed
17

18
	tar "gx/ipfs/QmQine7gvHncNevKtG9QXxf3nXcwSj6aDDmMm52mHofEEp/tar-utils"
19
	"gx/ipfs/QmceUdzxkimdYsgtX733uNgzf1DLHyBKN6ehGSp85ayppM/go-ipfs-cmdkit"
Jan Winkelmann's avatar
Jan Winkelmann committed
20
	"gx/ipfs/QmeWjRodbcZFKe5tMN7poEx3izym6osrLSnTLf9UjJZBbs/pb"
21
	"gx/ipfs/QmfAkMSt9Fwzk48QDJecPcwCUjnf2uG7MLnmCGTp4C6ouL/go-ipfs-cmds"
Matt Bell's avatar
Matt Bell committed
22 23
)

24 25
var ErrInvalidCompressionLevel = errors.New("Compression level must be between 1 and 9")

Matt Bell's avatar
Matt Bell committed
26
var GetCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
27
	Helptext: cmdkit.HelpText{
rht's avatar
rht committed
28
		Tagline: "Download IPFS objects.",
Matt Bell's avatar
Matt Bell committed
29
		ShortDescription: `
Richard Littauer's avatar
Richard Littauer committed
30
Stores to disk the data contained an IPFS or IPNS object(s) at the given path.
Matt Bell's avatar
Matt Bell committed
31

32 33
By default, the output will be stored at './<ipfs-path>', but an alternate
path can be specified with '--output=<path>' or '-o=<path>'.
Matt Bell's avatar
Matt Bell committed
34 35

To output a TAR archive instead of unpacked files, use '--archive' or '-a'.
36 37 38

To compress the output with GZIP compression, use '--compress' or '-C'. You
may also specify the level of compression by specifying '-l=<1-9>'.
Matt Bell's avatar
Matt Bell committed
39 40 41
`,
	},

Jan Winkelmann's avatar
Jan Winkelmann committed
42 43
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("ipfs-path", true, false, "The path to the IPFS object(s) to be outputted.").EnableStdin(),
Matt Bell's avatar
Matt Bell committed
44
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
45 46
	Options: []cmdkit.Option{
		cmdkit.StringOption("output", "o", "The path where the output should be stored."),
47 48
		cmdkit.BoolOption("archive", "a", "Output a TAR archive."),
		cmdkit.BoolOption("compress", "C", "Compress the output with GZIP compression."),
Steven Allen's avatar
Steven Allen committed
49
		cmdkit.IntOption("compression-level", "l", "The level of compression (1-9)."),
Matt Bell's avatar
Matt Bell committed
50
	},
Jeromy's avatar
Jeromy committed
51
	PreRun: func(req *cmds.Request, env cmds.Environment) error {
52 53 54
		_, err := getCompressOptions(req)
		return err
	},
Jeromy's avatar
Jeromy committed
55
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) {
56
		cmplvl, err := getCompressOptions(req)
57
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
58
			res.SetError(err, cmdkit.ErrNormal)
59 60 61
			return
		}

62
		node, err := GetNode(env)
Matt Bell's avatar
Matt Bell committed
63
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
64
			res.SetError(err, cmdkit.ErrNormal)
Matt Bell's avatar
Matt Bell committed
65 66
			return
		}
67 68
		p := path.Path(req.Arguments[0])
		ctx := req.Context
69
		dn, err := core.Resolve(ctx, node.Namesys, node.Resolver, p)
rht's avatar
rht committed
70
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
71
			res.SetError(err, cmdkit.ErrNormal)
rht's avatar
rht committed
72
			return
rht's avatar
rht committed
73
		}
rht's avatar
rht committed
74

Jeromy's avatar
Jeromy committed
75 76 77 78
		switch dn := dn.(type) {
		case *dag.ProtoNode:
			size, err := dn.Size()
			if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
79
				res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
80 81
				return
			}
82

Jeromy's avatar
Jeromy committed
83 84 85 86
			res.SetLength(size)
		case *dag.RawNode:
			res.SetLength(uint64(len(dn.RawData())))
		default:
Jan Winkelmann's avatar
Jan Winkelmann committed
87
			res.SetError(err, cmdkit.ErrNormal)
88 89 90
			return
		}

91
		archive, _ := req.Options["archive"].(bool)
Jeromy's avatar
Jeromy committed
92
		reader, err := uarchive.DagArchive(ctx, dn, p.String(), node.DAG, archive, cmplvl)
Matt Bell's avatar
Matt Bell committed
93
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
94
			res.SetError(err, cmdkit.ErrNormal)
95
			return
96
		}
97

Jan Winkelmann's avatar
Jan Winkelmann committed
98 99
		res.Emit(reader)
	},
100
	PostRun: cmds.PostRunMap{
101
		cmds.CLI: func(req *cmds.Request, re cmds.ResponseEmitter) cmds.ResponseEmitter {
Jan Winkelmann's avatar
Jan Winkelmann committed
102 103 104 105 106 107
			reNext, res := cmds.NewChanResponsePair(req)

			go func() {
				defer re.Close()

				v, err := res.Next()
108
				if !cmds.HandleError(err, res, re) {
Jan Winkelmann's avatar
Jan Winkelmann committed
109 110 111 112 113 114 115 116 117
					return
				}

				outReader, ok := v.(io.Reader)
				if !ok {
					log.Error(e.New(e.TypeErr(outReader, v)))
					return
				}

118
				outPath := getOutPath(req)
Jan Winkelmann's avatar
Jan Winkelmann committed
119 120 121 122 123 124 125

				cmplvl, err := getCompressOptions(req)
				if err != nil {
					re.SetError(err, cmdkit.ErrNormal)
					return
				}

126
				archive, _ := req.Options["archive"].(bool)
Jan Winkelmann's avatar
Jan Winkelmann committed
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142

				gw := getWriter{
					Out:         os.Stdout,
					Err:         os.Stderr,
					Archive:     archive,
					Compression: cmplvl,
					Size:        int64(res.Length()),
				}

				if err := gw.Write(outReader, outPath); err != nil {
					re.SetError(err, cmdkit.ErrNormal)
				}
			}()

			return reNext
		},
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
143 144
	},
}
145

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
type clearlineReader struct {
	io.Reader
	out io.Writer
}

func (r *clearlineReader) Read(p []byte) (n int, err error) {
	n, err = r.Reader.Read(p)
	if err == io.EOF {
		// callback
		fmt.Fprintf(r.out, "\033[2K\r") // clear progress bar line on EOF
	}
	return
}

func progressBarForReader(out io.Writer, r io.Reader, l int64) (*pb.ProgressBar, io.Reader) {
Jeromy's avatar
Jeromy committed
161 162 163 164 165 166
	bar := makeProgressBar(out, l)
	barR := bar.NewProxyReader(r)
	return bar, &clearlineReader{barR, out}
}

func makeProgressBar(out io.Writer, l int64) *pb.ProgressBar {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
167 168
	// setup bar reader
	// TODO: get total length of files
169
	bar := pb.New64(l).SetUnits(pb.U_BYTES)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
170
	bar.Output = out
171 172 173 174 175 176 177 178

	// the progress bar lib doesn't give us a way to get the width of the output,
	// so as a hack we just use a callback to measure the output, then git rid of it
	bar.Callback = func(line string) {
		terminalWidth := len(line)
		bar.Callback = nil
		log.Infof("terminal width: %v\n", terminalWidth)
	}
Jeromy's avatar
Jeromy committed
179
	return bar
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
180
}
181

182 183
func getOutPath(req *cmds.Request) string {
	outPath, _ := req.Options["output"].(string)
184
	if outPath == "" {
185
		trimmed := strings.TrimRight(req.Arguments[0], "/")
Dominic Della Valle's avatar
Dominic Della Valle committed
186 187
		_, outPath = filepath.Split(trimmed)
		outPath = filepath.Clean(outPath)
188 189 190 191
	}
	return outPath
}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
192 193 194
type getWriter struct {
	Out io.Writer // for output to user
	Err io.Writer // for progress bar output
195

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
196 197
	Archive     bool
	Compression int
Jeromy's avatar
Jeromy committed
198
	Size        int64
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
199
}
200

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
201 202 203 204 205 206 207 208 209 210 211 212
func (gw *getWriter) Write(r io.Reader, fpath string) error {
	if gw.Archive || gw.Compression != gzip.NoCompression {
		return gw.writeArchive(r, fpath)
	}
	return gw.writeExtracted(r, fpath)
}

func (gw *getWriter) writeArchive(r io.Reader, fpath string) error {
	// adjust file name if tar
	if gw.Archive {
		if !strings.HasSuffix(fpath, ".tar") && !strings.HasSuffix(fpath, ".tar.gz") {
			fpath += ".tar"
213
		}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
	}

	// adjust file name if gz
	if gw.Compression != gzip.NoCompression {
		if !strings.HasSuffix(fpath, ".gz") {
			fpath += ".gz"
		}
	}

	// create file
	file, err := os.Create(fpath)
	if err != nil {
		return err
	}
	defer file.Close()

	fmt.Fprintf(gw.Out, "Saving archive to %s\n", fpath)
Jeromy's avatar
Jeromy committed
231
	bar, barR := progressBarForReader(gw.Err, r, gw.Size)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
232 233 234 235 236 237 238 239 240
	bar.Start()
	defer bar.Finish()

	_, err = io.Copy(file, barR)
	return err
}

func (gw *getWriter) writeExtracted(r io.Reader, fpath string) error {
	fmt.Fprintf(gw.Out, "Saving file(s) to %s\n", fpath)
Jeromy's avatar
Jeromy committed
241
	bar := makeProgressBar(gw.Err, gw.Size)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
242 243
	bar.Start()
	defer bar.Finish()
Jeromy's avatar
Jeromy committed
244
	defer bar.Set64(gw.Size)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
245

Steven Allen's avatar
Steven Allen committed
246
	extractor := &tar.Extractor{Path: fpath, Progress: bar.Add64}
Jeromy's avatar
Jeromy committed
247
	return extractor.Extract(r)
Matt Bell's avatar
Matt Bell committed
248 249
}

250 251
func getCompressOptions(req *cmds.Request) (int, error) {
	cmprs, _ := req.Options["compress"].(bool)
Steven Allen's avatar
Steven Allen committed
252
	cmplvl, cmplvlFound := req.Options["compression-level"].(int)
253 254 255
	switch {
	case !cmprs:
		return gzip.NoCompression, nil
Steven Allen's avatar
Steven Allen committed
256
	case cmprs && !cmplvlFound:
257
		return gzip.DefaultCompression, nil
keks's avatar
keks committed
258
	case cmprs && (cmplvl < 1 || cmplvl > 9):
259
		return gzip.NoCompression, ErrInvalidCompressionLevel
260
	}
261
	return cmplvl, nil
262
}