cat.go 3.99 KB
Newer Older
1 2 3
package commands

import (
Jan Winkelmann's avatar
Jan Winkelmann committed
4
	"context"
5
	"fmt"
6
	"io"
Jan Winkelmann's avatar
Jan Winkelmann committed
7
	"os"
8

9
	"github.com/ipfs/go-ipfs/core/commands/cmdenv"
10
	"github.com/ipfs/go-ipfs/core/coreapi/interface"
11

12
	"gx/ipfs/QmXWZCd8jfaHmt4UDSnjKmGcrQMw95bDGWqEeVLVJjoANX/go-ipfs-files"
13
	cmds "gx/ipfs/QmaAP56JAwdjwisPTu4yx17whcjTr6y5JCSCF77Y1rahWV/go-ipfs-cmds"
14
	"gx/ipfs/Qmde5VP1qUkyQXKCfmEUA7bP64V2HAptbJ7phuPp7jXWwg/go-ipfs-cmdkit"
15 16
)

Kejie Zhang's avatar
Kejie Zhang committed
17 18 19 20 21
const (
	progressBarMinSize = 1024 * 1024 * 8 // show progress bar for outputs > 8MiB
	offsetOptionName   = "offset"
	lengthOptionName   = "length"
)
22

23
var CatCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
24
	Helptext: cmdkit.HelpText{
Jeromy's avatar
Jeromy committed
25
		Tagline:          "Show IPFS object data.",
Richard Littauer's avatar
Richard Littauer committed
26
		ShortDescription: "Displays the data contained by an IPFS or IPNS object(s) at the given path.",
27
	},
28

Jan Winkelmann's avatar
Jan Winkelmann committed
29 30
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to be outputted.").EnableStdin(),
31
	},
32
	Options: []cmdkit.Option{
33 34
		cmdkit.Int64Option(offsetOptionName, "o", "Byte offset to begin reading from."),
		cmdkit.Int64Option(lengthOptionName, "l", "Maximum number of bytes to read."),
35
	},
keks's avatar
keks committed
36
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
37 38 39 40 41
		api, err := cmdenv.GetApi(env)
		if err != nil {
			return err
		}

42
		offset, _ := req.Options[offsetOptionName].(int64)
43
		if offset < 0 {
keks's avatar
keks committed
44
			return fmt.Errorf("cannot specify negative offset")
45
		}
rht's avatar
rht committed
46

47
		max, found := req.Options[lengthOptionName].(int64)
48

49
		if max < 0 {
keks's avatar
keks committed
50
			return fmt.Errorf("cannot specify negative length")
51 52 53 54 55
		}
		if !found {
			max = -1
		}

56
		err = req.ParseBodyArgs()
57
		if err != nil {
keks's avatar
keks committed
58
			return err
59 60
		}

61
		readers, length, err := cat(req.Context, api, req.Arguments, int64(offset), int64(max))
62
		if err != nil {
keks's avatar
keks committed
63
			return err
64
		}
Matt Bell's avatar
Matt Bell committed
65

Jeromy's avatar
Jeromy committed
66
		/*
67
			if err := corerepo.ConditionalGC(req.Context, node, length); err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
68
				re.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
69 70 71 72
				return
			}
		*/

73
		res.SetLength(length)
Matt Bell's avatar
Matt Bell committed
74
		reader := io.MultiReader(readers...)
Jan Winkelmann's avatar
Jan Winkelmann committed
75 76 77 78 79

		// Since the reader returns the error that a block is missing, and that error is
		// returned from io.Copy inside Emit, we need to take Emit errors and send
		// them to the client. Usually we don't do that because it means the connection
		// is broken or we supplied an illegal argument etc.
keks's avatar
keks committed
80
		return res.Emit(reader)
Jan Winkelmann's avatar
Jan Winkelmann committed
81
	},
82
	PostRun: cmds.PostRunMap{
keks's avatar
keks committed
83 84 85 86
		cmds.CLI: func(res cmds.Response, re cmds.ResponseEmitter) error {
			if res.Length() > 0 && res.Length() < progressBarMinSize {
				return cmds.Copy(re, res)
			}
Jan Winkelmann's avatar
Jan Winkelmann committed
87

keks's avatar
keks committed
88 89 90 91 92
			for {
				v, err := res.Next()
				if err != nil {
					if err == io.EOF {
						return nil
Jan Winkelmann's avatar
Jan Winkelmann committed
93
					}
keks's avatar
keks committed
94
					return err
Jan Winkelmann's avatar
Jan Winkelmann committed
95 96
				}

keks's avatar
keks committed
97 98 99 100
				switch val := v.(type) {
				case io.Reader:
					bar, reader := progressBarForReader(os.Stderr, val, int64(res.Length()))
					bar.Start()
Jan Winkelmann's avatar
Jan Winkelmann committed
101

keks's avatar
keks committed
102 103 104
					err = re.Emit(reader)
					if err != nil {
						return err
Jan Winkelmann's avatar
Jan Winkelmann committed
105
					}
keks's avatar
keks committed
106 107
				default:
					log.Warningf("cat postrun: received unexpected type %T", val)
Jan Winkelmann's avatar
Jan Winkelmann committed
108
				}
keks's avatar
keks committed
109
			}
Jan Winkelmann's avatar
Jan Winkelmann committed
110
		},
111
	},
112
}
113

114
func cat(ctx context.Context, api iface.CoreAPI, paths []string, offset int64, max int64) ([]io.Reader, uint64, error) {
115
	readers := make([]io.Reader, 0, len(paths))
116
	length := uint64(0)
117 118 119
	if max == 0 {
		return nil, 0, nil
	}
120 121 122 123 124 125
	for _, p := range paths {
		fpath, err := iface.ParsePath(p)
		if err != nil {
			return nil, 0, err
		}

126
		f, err := api.Unixfs().Get(ctx, fpath)
127 128 129 130
		if err != nil {
			return nil, 0, err
		}

131 132
		file, ok := f.(files.File)
		if !ok {
Łukasz Magiera's avatar
Łukasz Magiera committed
133
			return nil, 0, iface.ErrNotFile
134 135
		}

136
		fsize, err := file.Size()
137
		if err != nil {
138
			return nil, 0, err
139
		}
140 141 142

		if offset > fsize {
			offset = offset - fsize
143 144
			continue
		}
145 146

		count, err := file.Seek(offset, io.SeekStart)
147 148 149 150 151
		if err != nil {
			return nil, 0, err
		}
		offset = 0

152 153 154 155 156 157
		fsize, err = file.Size()
		if err != nil {
			return nil, 0, err
		}

		size := uint64(fsize - count)
158 159
		length += size
		if max > 0 && length >= uint64(max) {
160
			var r io.Reader = file
161
			if overshoot := int64(length - uint64(max)); overshoot != 0 {
162
				r = io.LimitReader(file, int64(size)-overshoot)
163 164 165 166 167
				length = uint64(max)
			}
			readers = append(readers, r)
			break
		}
168
		readers = append(readers, file)
169
	}
170
	return readers, length, nil
171
}