mirror of
https://github.com/ipfs/kubo.git
synced 2025-05-20 00:18:12 +08:00
263 lines
6.6 KiB
Go
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
|
|
}
|