get.go 5.84 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

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

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
103 104 105 106 107 108 109 110 111
		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)
112 113
			return
		}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
114 115
	},
}
116

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
117 118 119 120 121 122 123 124
func progressBarForReader(out io.Writer, r io.Reader) (*pb.ProgressBar, *pb.Reader) {
	// setup bar reader
	// TODO: get total length of files
	bar := pb.New(0).SetUnits(pb.U_BYTES)
	bar.Output = out
	barR := bar.NewProxyReader(r)
	return bar, barR
}
125

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
126 127 128
type getWriter struct {
	Out io.Writer // for output to user
	Err io.Writer // for progress bar output
129

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
130 131 132
	Archive     bool
	Compression int
}
133

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
134 135 136 137 138 139 140 141 142 143 144 145
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"
146
		}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
	}

	// 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)
	bar, barR := progressBarForReader(gw.Err, r)
	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)
	bar, barR := progressBarForReader(gw.Err, r)
	bar.Start()
	defer bar.Finish()

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

182 183 184 185 186 187 188 189 190 191
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
192
	}
193
	return gzip.NoCompression, nil
194 195
}

196
func get(ctx context.Context, node *core.IpfsNode, p path.Path, compression int) (io.Reader, error) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
197
	dn, err := core.Resolve(ctx, node, p)
198 199 200 201
	if err != nil {
		return nil, err
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
202
	return utar.DagArchive(ctx, dn, p.String(), node.DAG, compression)
Matt Bell's avatar
Matt Bell committed
203
}
rht's avatar
rht committed
204 205

// getZip is equivalent to `ipfs getdag $hash | gzip`
206 207
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
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
	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
}