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

Jakub Sztandera's avatar
Jakub Sztandera committed
12
	"gx/ipfs/QmeWjRodbcZFKe5tMN7poEx3izym6osrLSnTLf9UjJZBbs/pb"
13

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

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

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

30 31
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
32 33

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

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

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

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

73 74 75 76 77 78
		pbnd, ok := dn.(*dag.ProtoNode)
		if !ok {
			res.SetError(err, cmds.ErrNormal)
			return
		}

Jeromy's avatar
Jeromy committed
79 80 81 82 83 84 85 86
		size, err := dn.Size()
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}

		res.SetLength(size)

rht's avatar
rht committed
87
		archive, _, _ := req.Option("archive").Bool()
88
		reader, err := uarchive.DagArchive(ctx, pbnd, p.String(), node.DAG, archive, cmplvl)
Matt Bell's avatar
Matt Bell committed
89 90 91 92 93 94
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
		res.SetOutput(reader)
	},
95
	PostRun: func(req cmds.Request, res cmds.Response) {
96 97 98
		if res.Output() == nil {
			return
		}
99
		outReader := res.Output().(io.Reader)
100 101
		res.SetOutput(nil)

102
		outPath, _, _ := req.Option("output").String()
103
		if len(outPath) == 0 {
Jeromy's avatar
Jeromy committed
104 105
			_, outPath = gopath.Split(req.Arguments()[0])
			outPath = gopath.Clean(outPath)
106 107
		}

108 109 110 111
		cmplvl, err := getCompressOptions(req)
		if err != nil {
			res.SetError(err, cmds.ErrClient)
			return
112
		}
113

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

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
116 117 118 119 120
		gw := getWriter{
			Out:         os.Stdout,
			Err:         os.Stderr,
			Archive:     archive,
			Compression: cmplvl,
Jeromy's avatar
Jeromy committed
121
			Size:        int64(res.Length()),
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
122 123 124 125
		}

		if err := gw.Write(outReader, outPath); err != nil {
			res.SetError(err, cmds.ErrNormal)
126 127
			return
		}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
128 129
	},
}
130

131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
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
146 147
	// setup bar reader
	// TODO: get total length of files
148
	bar := pb.New64(l).SetUnits(pb.U_BYTES)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
149
	bar.Output = out
150 151 152 153 154 155 156 157

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

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
162 163 164
type getWriter struct {
	Out io.Writer // for output to user
	Err io.Writer // for progress bar output
165

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
166 167
	Archive     bool
	Compression int
Jeromy's avatar
Jeromy committed
168
	Size        int64
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
169
}
170

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
171 172 173 174 175 176 177 178 179 180 181 182
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"
183
		}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
	}

	// 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
201
	bar, barR := progressBarForReader(gw.Err, r, gw.Size)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
202 203 204 205 206 207 208 209 210
	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
211
	bar, barR := progressBarForReader(gw.Err, r, gw.Size)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
212 213 214 215 216
	bar.Start()
	defer bar.Finish()

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

219 220 221 222 223 224 225 226 227 228
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
229
	}
230
	return cmplvl, nil
231
}