1
0
mirror of https://github.com/ipfs/kubo.git synced 2025-05-20 00:18:12 +08:00
Files
kubo/core/commands/get.go
Whyrusleeping 1fb90ad62c Merge pull request #4857 from ipfs/extract/thirdparty-tar
Extract: thirdparty/tar
2018-03-23 09:41:31 -07:00

263 lines
6.6 KiB
Go

package commands
import (
"compress/gzip"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
core "github.com/ipfs/go-ipfs/core"
e "github.com/ipfs/go-ipfs/core/commands/e"
dag "github.com/ipfs/go-ipfs/merkledag"
path "github.com/ipfs/go-ipfs/path"
uarchive "github.com/ipfs/go-ipfs/unixfs/archive"
tar "gx/ipfs/QmYk64JEF4QWPB9Kqib63g8vfYfv78AmSUyeFaMaX6F9vQ/tar-utils"
"gx/ipfs/QmceUdzxkimdYsgtX733uNgzf1DLHyBKN6ehGSp85ayppM/go-ipfs-cmdkit"
"gx/ipfs/QmeWjRodbcZFKe5tMN7poEx3izym6osrLSnTLf9UjJZBbs/pb"
"gx/ipfs/QmfAkMSt9Fwzk48QDJecPcwCUjnf2uG7MLnmCGTp4C6ouL/go-ipfs-cmds"
)
var ErrInvalidCompressionLevel = errors.New("Compression level must be between 1 and 9")
var GetCmd = &cmds.Command{
Helptext: cmdkit.HelpText{
Tagline: "Download IPFS objects.",
ShortDescription: `
Stores to disk the data contained an IPFS or IPNS object(s) at the given path.
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'.
To compress the output with GZIP compression, use '--compress' or '-C'. You
may also specify the level of compression by specifying '-l=<1-9>'.
`,
},
Arguments: []cmdkit.Argument{
cmdkit.StringArg("ipfs-path", true, false, "The path to the IPFS object(s) to be outputted.").EnableStdin(),
},
Options: []cmdkit.Option{
cmdkit.StringOption("output", "o", "The path where the output should be stored."),
cmdkit.BoolOption("archive", "a", "Output a TAR archive."),
cmdkit.BoolOption("compress", "C", "Compress the output with GZIP compression."),
cmdkit.IntOption("compression-level", "l", "The level of compression (1-9)."),
},
PreRun: func(req *cmds.Request, env cmds.Environment) error {
_, err := getCompressOptions(req)
return err
},
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) {
cmplvl, err := getCompressOptions(req)
if err != nil {
res.SetError(err, cmdkit.ErrNormal)
return
}
node, err := GetNode(env)
if err != nil {
res.SetError(err, cmdkit.ErrNormal)
return
}
p := path.Path(req.Arguments[0])
ctx := req.Context
dn, err := core.Resolve(ctx, node.Namesys, node.Resolver, p)
if err != nil {
res.SetError(err, cmdkit.ErrNormal)
return
}
switch dn := dn.(type) {
case *dag.ProtoNode:
size, err := dn.Size()
if err != nil {
res.SetError(err, cmdkit.ErrNormal)
return
}
res.SetLength(size)
case *dag.RawNode:
res.SetLength(uint64(len(dn.RawData())))
default:
res.SetError(err, cmdkit.ErrNormal)
return
}
archive, _ := req.Options["archive"].(bool)
reader, err := uarchive.DagArchive(ctx, dn, p.String(), node.DAG, archive, cmplvl)
if err != nil {
res.SetError(err, cmdkit.ErrNormal)
return
}
res.Emit(reader)
},
PostRun: cmds.PostRunMap{
cmds.CLI: func(req *cmds.Request, re cmds.ResponseEmitter) cmds.ResponseEmitter {
reNext, res := cmds.NewChanResponsePair(req)
go func() {
defer re.Close()
v, err := res.Next()
if !cmds.HandleError(err, res, re) {
return
}
outReader, ok := v.(io.Reader)
if !ok {
log.Error(e.New(e.TypeErr(outReader, v)))
return
}
outPath := getOutPath(req)
cmplvl, err := getCompressOptions(req)
if err != nil {
re.SetError(err, cmdkit.ErrNormal)
return
}
archive, _ := req.Options["archive"].(bool)
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
},
},
}
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) {
bar := makeProgressBar(out, l)
barR := bar.NewProxyReader(r)
return bar, &clearlineReader{barR, out}
}
func makeProgressBar(out io.Writer, l int64) *pb.ProgressBar {
// setup bar reader
// TODO: get total length of files
bar := pb.New64(l).SetUnits(pb.U_BYTES)
bar.Output = out
// 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)
}
return bar
}
func getOutPath(req *cmds.Request) string {
outPath, _ := req.Options["output"].(string)
if outPath == "" {
trimmed := strings.TrimRight(req.Arguments[0], "/")
_, outPath = filepath.Split(trimmed)
outPath = filepath.Clean(outPath)
}
return outPath
}
type getWriter struct {
Out io.Writer // for output to user
Err io.Writer // for progress bar output
Archive bool
Compression int
Size int64
}
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"
}
}
// 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, gw.Size)
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 := makeProgressBar(gw.Err, gw.Size)
bar.Start()
defer bar.Finish()
defer bar.Set64(gw.Size)
extractor := &tar.Extractor{Path: fpath, Progress: bar.Add64}
return extractor.Extract(r)
}
func getCompressOptions(req *cmds.Request) (int, error) {
cmprs, _ := req.Options["compress"].(bool)
cmplvl, cmplvlFound := req.Options["compression-level"].(int)
switch {
case !cmprs:
return gzip.NoCompression, nil
case cmprs && !cmplvlFound:
return gzip.DefaultCompression, nil
case cmprs && (cmplvl < 1 || cmplvl > 9):
return gzip.NoCompression, ErrInvalidCompressionLevel
}
return cmplvl, nil
}