get.go 5.4 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"
Jeromy's avatar
Jeromy committed
9
	gopath "path"
10
	"strings"
Matt Bell's avatar
Matt Bell committed
11

12 13
	"github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/cheggaaa/pb"

14 15 16 17
	cmds "github.com/ipfs/go-ipfs/commands"
	core "github.com/ipfs/go-ipfs/core"
	path "github.com/ipfs/go-ipfs/path"
	tar "github.com/ipfs/go-ipfs/thirdparty/tar"
rht's avatar
rht committed
18
	uarchive "github.com/ipfs/go-ipfs/unixfs/archive"
Matt Bell's avatar
Matt Bell committed
19 20
)

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

Matt Bell's avatar
Matt Bell committed
23 24
var GetCmd = &cmds.Command{
	Helptext: cmds.HelpText{
rht's avatar
rht committed
25
		Tagline: "Download IPFS objects.",
Matt Bell's avatar
Matt Bell committed
26
		ShortDescription: `
27
Retrieves the object named by <ipfs-or-ipns-path> and stores the data to disk.
Matt Bell's avatar
Matt Bell committed
28 29 30 31 32

By default, the output will be stored at ./<ipfs-path>, but an alternate path
can be specified with '--output=<path>' or '-o=<path>'.

To output a TAR archive instead of unpacked files, use '--archive' or '-a'.
33 34 35

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
36 37 38 39
`,
	},

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

Jeromy's avatar
Jeromy committed
59
		node, err := req.InvocContext().GetNode()
Matt Bell's avatar
Matt Bell committed
60 61 62 63
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
64
		p := path.Path(req.Arguments()[0])
rht's avatar
rht committed
65 66 67 68 69
		ctx := req.Context()
		dn, err := core.Resolve(ctx, node, p)
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
rht's avatar
rht committed
70
		}
rht's avatar
rht committed
71 72

		archive, _, _ := req.Option("archive").Bool()
rht's avatar
rht committed
73
		reader, err := uarchive.DagArchive(ctx, dn, p.String(), node.DAG, archive, cmplvl)
Matt Bell's avatar
Matt Bell committed
74 75 76 77 78 79
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		res.SetOutput(reader)
	},
80
	PostRun: func(req cmds.Request, res cmds.Response) {
81 82 83
		if res.Output() == nil {
			return
		}
84
		outReader := res.Output().(io.Reader)
85 86
		res.SetOutput(nil)

87
		outPath, _, _ := req.Option("output").String()
88
		if len(outPath) == 0 {
Jeromy's avatar
Jeromy committed
89 90
			_, outPath = gopath.Split(req.Arguments()[0])
			outPath = gopath.Clean(outPath)
91 92
		}

93 94 95 96
		cmplvl, err := getCompressOptions(req)
		if err != nil {
			res.SetError(err, cmds.ErrClient)
			return
97
		}
98

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
99
		archive, _, _ := req.Option("archive").Bool()
100

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
101 102 103 104 105 106 107 108 109
		gw := getWriter{
			Out:         os.Stdout,
			Err:         os.Stderr,
			Archive:     archive,
			Compression: cmplvl,
		}

		if err := gw.Write(outReader, outPath); err != nil {
			res.SetError(err, cmds.ErrNormal)
110 111
			return
		}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
112 113
	},
}
114

115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
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) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
130 131
	// setup bar reader
	// TODO: get total length of files
132
	bar := pb.New64(l).SetUnits(pb.U_BYTES)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
133
	bar.Output = out
134 135 136 137 138 139 140 141

	// 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)
	}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
142
	barR := bar.NewProxyReader(r)
143
	return bar, &clearlineReader{barR, out}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
144
}
145

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
146 147 148
type getWriter struct {
	Out io.Writer // for output to user
	Err io.Writer // for progress bar output
149

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
150 151 152
	Archive     bool
	Compression int
}
153

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
154 155 156 157 158 159 160 161 162 163 164 165
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"
166
		}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
	}

	// 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)
184
	bar, barR := progressBarForReader(gw.Err, r, 0)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
185 186 187 188 189 190 191 192 193
	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)
194
	bar, barR := progressBarForReader(gw.Err, r, 0)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
195 196 197 198 199
	bar.Start()
	defer bar.Finish()

	extractor := &tar.Extractor{fpath}
	return extractor.Extract(barR)
Matt Bell's avatar
Matt Bell committed
200 201
}

202 203 204 205 206 207 208 209 210 211
func getCompressOptions(req cmds.Request) (int, error) {
	cmprs, _, _ := req.Option("compress").Bool()
	cmplvl, cmplvlFound, _ := req.Option("compression-level").Int()
	switch {
	case !cmprs:
		return gzip.NoCompression, nil
	case cmprs && !cmplvlFound:
		return gzip.DefaultCompression, nil
	case cmprs && cmplvlFound && (cmplvl < 1 || cmplvl > 9):
		return gzip.NoCompression, ErrInvalidCompressionLevel
212
	}
213
	return cmplvl, nil
214
}