mirror of
https://github.com/ipfs/kubo.git
synced 2025-06-04 22:29:23 +08:00
215 lines
5.4 KiB
Go
215 lines
5.4 KiB
Go
package commands
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
gopath "path"
|
|
"strings"
|
|
|
|
"github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/cheggaaa/pb"
|
|
|
|
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"
|
|
uarchive "github.com/ipfs/go-ipfs/unixfs/archive"
|
|
)
|
|
|
|
var ErrInvalidCompressionLevel = errors.New("Compression level must be between 1 and 9")
|
|
|
|
var GetCmd = &cmds.Command{
|
|
Helptext: cmds.HelpText{
|
|
Tagline: "Download IPFS objects",
|
|
ShortDescription: `
|
|
Retrieves the object named by <ipfs-or-ipns-path> and stores the data to disk.
|
|
|
|
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: []cmds.Argument{
|
|
cmds.StringArg("ipfs-path", true, false, "The path to the IPFS object(s) to be outputted").EnableStdin(),
|
|
},
|
|
Options: []cmds.Option{
|
|
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)"),
|
|
},
|
|
PreRun: func(req cmds.Request) error {
|
|
_, err := getCompressOptions(req)
|
|
return err
|
|
},
|
|
Run: func(req cmds.Request, res cmds.Response) {
|
|
cmplvl, err := getCompressOptions(req)
|
|
if err != nil {
|
|
res.SetError(err, cmds.ErrClient)
|
|
return
|
|
}
|
|
|
|
node, err := req.InvocContext().GetNode()
|
|
if err != nil {
|
|
res.SetError(err, cmds.ErrNormal)
|
|
return
|
|
}
|
|
p := path.Path(req.Arguments()[0])
|
|
ctx := req.Context()
|
|
dn, err := core.Resolve(ctx, node, p)
|
|
if err != nil {
|
|
res.SetError(err, cmds.ErrNormal)
|
|
return
|
|
}
|
|
|
|
archive, _, _ := req.Option("archive").Bool()
|
|
reader, err := uarchive.DagArchive(ctx, dn, p.String(), node.DAG, archive, cmplvl)
|
|
if err != nil {
|
|
res.SetError(err, cmds.ErrNormal)
|
|
return
|
|
}
|
|
res.SetOutput(reader)
|
|
},
|
|
PostRun: func(req cmds.Request, res cmds.Response) {
|
|
if res.Output() == nil {
|
|
return
|
|
}
|
|
outReader := res.Output().(io.Reader)
|
|
res.SetOutput(nil)
|
|
|
|
outPath, _, _ := req.Option("output").String()
|
|
if len(outPath) == 0 {
|
|
_, outPath = gopath.Split(req.Arguments()[0])
|
|
outPath = gopath.Clean(outPath)
|
|
}
|
|
|
|
cmplvl, err := getCompressOptions(req)
|
|
if err != nil {
|
|
res.SetError(err, cmds.ErrClient)
|
|
return
|
|
}
|
|
|
|
archive, _, _ := req.Option("archive").Bool()
|
|
|
|
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)
|
|
return
|
|
}
|
|
},
|
|
}
|
|
|
|
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) {
|
|
// 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)
|
|
}
|
|
barR := bar.NewProxyReader(r)
|
|
return bar, &clearlineReader{barR, out}
|
|
}
|
|
|
|
type getWriter struct {
|
|
Out io.Writer // for output to user
|
|
Err io.Writer // for progress bar output
|
|
|
|
Archive bool
|
|
Compression int
|
|
}
|
|
|
|
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, 0)
|
|
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, 0)
|
|
bar.Start()
|
|
defer bar.Finish()
|
|
|
|
extractor := &tar.Extractor{fpath}
|
|
return extractor.Extract(barR)
|
|
}
|
|
|
|
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
|
|
}
|
|
return cmplvl, nil
|
|
}
|