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

import (
rht's avatar
rht committed
4
	"bufio"
5
	"compress/gzip"
6
	"errors"
7
	"fmt"
Matt Bell's avatar
Matt Bell committed
8
	"io"
9
	"os"
Jeromy's avatar
Jeromy committed
10
	gopath "path"
11
	"strings"
Matt Bell's avatar
Matt Bell committed
12

13 14 15
	"github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/cheggaaa/pb"
	context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"

16 17 18 19
	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
20
	uio "github.com/ipfs/go-ipfs/unixfs/io"
21
	utar "github.com/ipfs/go-ipfs/unixfs/tar"
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 27 28 29
var GetCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "Download IPFS objects",
		ShortDescription: `
30
Retrieves the object named by <ipfs-or-ipns-path> and stores the data to disk.
Matt Bell's avatar
Matt Bell committed
31 32 33 34 35

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'.
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 42
`,
	},

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

Jeromy's avatar
Jeromy committed
62
		node, err := req.InvocContext().GetNode()
Matt Bell's avatar
Matt Bell committed
63 64 65 66 67
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

68
		p := path.Path(req.Arguments()[0])
rht's avatar
rht committed
69 70 71
		var reader io.Reader
		if archive, _, _ := req.Option("archive").Bool(); !archive && cmplvl != gzip.NoCompression {
			// only use this when the flag is '-C' without '-a'
Jeromy's avatar
Jeromy committed
72
			reader, err = getZip(req.Context(), node, p, cmplvl)
rht's avatar
rht committed
73
		} else {
Jeromy's avatar
Jeromy committed
74
			reader, err = get(req.Context(), node, p, cmplvl)
rht's avatar
rht committed
75
		}
Matt Bell's avatar
Matt Bell committed
76 77 78 79 80 81
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		res.SetOutput(reader)
	},
82
	PostRun: func(req cmds.Request, res cmds.Response) {
83 84 85
		if res.Output() == nil {
			return
		}
86
		outReader := res.Output().(io.Reader)
87 88
		res.SetOutput(nil)

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

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

rht's avatar
rht committed
101 102
		if archive, _, _ := req.Option("archive").Bool(); archive || cmplvl != gzip.NoCompression {
			if archive && !strings.HasSuffix(outPath, ".tar") {
103 104
				outPath += ".tar"
			}
105
			if cmplvl != gzip.NoCompression {
106 107
				outPath += ".gz"
			}
108 109 110 111 112 113 114 115 116
			fmt.Printf("Saving archive to %s\n", outPath)

			file, err := os.Create(outPath)
			if err != nil {
				res.SetError(err, cmds.ErrNormal)
				return
			}
			defer file.Close()

117 118
			bar := pb.New(0).SetUnits(pb.U_BYTES)
			bar.Output = os.Stderr
119
			pbReader := bar.NewProxyReader(outReader)
120 121 122
			bar.Start()
			defer bar.Finish()

123
			if _, err := io.Copy(file, pbReader); err != nil {
124 125 126 127 128 129 130 131 132
				res.SetError(err, cmds.ErrNormal)
				return
			}

			return
		}

		fmt.Printf("Saving file(s) to %s\n", outPath)

133 134 135 136
		// TODO: get total length of files
		bar := pb.New(0).SetUnits(pb.U_BYTES)
		bar.Output = os.Stderr

137
		// wrap the reader with the progress bar proxy reader
rht's avatar
rht committed
138
		reader := bar.NewProxyReader(outReader)
139

140 141
		bar.Start()
		defer bar.Finish()
142
		extractor := &tar.Extractor{outPath}
143
		if err := extractor.Extract(reader); err != nil {
144
			res.SetError(err, cmds.ErrNormal)
145 146
		}
	},
Matt Bell's avatar
Matt Bell committed
147 148
}

149 150 151 152 153 154 155 156 157 158
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
159
	}
160
	return gzip.NoCompression, nil
161 162
}

163 164
func get(ctx context.Context, node *core.IpfsNode, p path.Path, compression int) (io.Reader, error) {
	dagnode, err := core.Resolve(ctx, node, p)
165 166 167 168
	if err != nil {
		return nil, err
	}

169
	return utar.NewReader(ctx, p, node.DAG, dagnode, compression)
Matt Bell's avatar
Matt Bell committed
170
}
rht's avatar
rht committed
171 172

// getZip is equivalent to `ipfs getdag $hash | gzip`
173 174
func getZip(ctx context.Context, node *core.IpfsNode, p path.Path, compression int) (io.Reader, error) {
	dagnode, err := core.Resolve(ctx, node, p)
rht's avatar
rht committed
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
	if err != nil {
		return nil, err
	}

	reader, err := uio.NewDagReader(ctx, dagnode, node.DAG)
	if err != nil {
		return nil, err
	}

	pr, pw := io.Pipe()
	gw, err := gzip.NewWriterLevel(pw, compression)
	if err != nil {
		return nil, err
	}
	bufin := bufio.NewReader(reader)
	go func() {
		_, err := bufin.WriteTo(gw)
		if err != nil {
			log.Error("Fail to compress the stream")
		}
		gw.Close()
		pw.Close()
	}()

	return pr, nil
}