cat.go 3.95 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

Jakub Sztandera's avatar
Jakub Sztandera committed
11 12 13 14
	"github.com/ipfs/go-ipfs-cmdkit"
	cmds "github.com/ipfs/go-ipfs-cmds"
	"github.com/ipfs/go-ipfs-files"
	"github.com/ipfs/interface-go-ipfs-core"
15
	"github.com/ipfs/interface-go-ipfs-core/path"
16 17
)

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

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

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

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

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

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

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

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

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

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

		// 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
81
		return res.Emit(reader)
Jan Winkelmann's avatar
Jan Winkelmann committed
82
	},
83
	PostRun: cmds.PostRunMap{
keks's avatar
keks committed
84 85 86 87
		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
88

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

keks's avatar
keks committed
98 99 100 101
				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
102

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

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

127 128 129 130 131 132 133
		var file files.File
		switch f := f.(type) {
		case files.File:
			file = f
		case files.Directory:
			return nil, 0, iface.ErrIsDir
		default:
134
			return nil, 0, iface.ErrNotSupported
135 136
		}

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

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

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

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

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