repo.go 9.56 KB
Newer Older
1 2 3 4
package commands

import (
	"bytes"
keks's avatar
fixes  
keks committed
5
	"errors"
6
	"fmt"
Jeromy's avatar
Jeromy committed
7 8 9
	"io"
	"os"
	"path/filepath"
10
	"strings"
11
	"text/tabwriter"
Jeromy's avatar
Jeromy committed
12

Jan Winkelmann's avatar
Jan Winkelmann committed
13
	oldcmds "github.com/ipfs/go-ipfs/commands"
14
	lgc "github.com/ipfs/go-ipfs/commands/legacy"
15
	cmdenv "github.com/ipfs/go-ipfs/core/commands/cmdenv"
Jan Winkelmann's avatar
Jan Winkelmann committed
16
	e "github.com/ipfs/go-ipfs/core/commands/e"
17
	corerepo "github.com/ipfs/go-ipfs/core/corerepo"
Michael Pfister's avatar
Michael Pfister committed
18
	fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"
Jeromy's avatar
Jeromy committed
19

20
	cid "gx/ipfs/QmPSQnBKM9g7BaUcZCvswUJVscQ1ipjmwxN5PXCjkp9EQ7/go-cid"
21
	cmdkit "gx/ipfs/QmSP88ryZkHSRn1fnngAaV2Vcn63WUJzAavnRM9CVdU1Ky/go-ipfs-cmdkit"
Lars Gierth's avatar
Lars Gierth committed
22
	config "gx/ipfs/QmSoYrBMibm2T3LupaLuez7LPGnyrJwdRxvTfPUyCp691u/go-ipfs-config"
Steven Allen's avatar
Steven Allen committed
23
	cmds "gx/ipfs/QmXTmUCBtDUrzDYVzASogLiNph7EBuYqEgPL7QoHNMzUnz/go-ipfs-cmds"
24
	bstore "gx/ipfs/QmfUhZX9KpvJiuiziUzP2cjhRAyqHJURsPgRKn1cdDZMKa/go-ipfs-blockstore"
25 26
)

Michael Pfister's avatar
Michael Pfister committed
27 28 29 30
type RepoVersion struct {
	Version string
}

31
var RepoCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
32
	Helptext: cmdkit.HelpText{
33
		Tagline: "Manipulate the IPFS repo.",
34 35 36 37 38 39
		ShortDescription: `
'ipfs repo' is a plumbing command used to manipulate the repo.
`,
	},

	Subcommands: map[string]*cmds.Command{
40
		"stat":    repoStatCmd,
keks's avatar
keks committed
41
		"gc":      repoGcCmd,
42 43 44
		"fsck":    lgc.NewCommand(RepoFsckCmd),
		"version": lgc.NewCommand(repoVersionCmd),
		"verify":  lgc.NewCommand(repoVerifyCmd),
45 46 47
	},
}

Kevin Atkinson's avatar
Kevin Atkinson committed
48
// GcResult is the result returned by "repo gc" command.
49
type GcResult struct {
50
	Key   cid.Cid
51 52 53
	Error string `json:",omitempty"`
}

keks's avatar
keks committed
54
var repoGcCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
55
	Helptext: cmdkit.HelpText{
rht's avatar
rht committed
56
		Tagline: "Perform a garbage collection sweep on the repo.",
57 58 59 60 61 62
		ShortDescription: `
'ipfs repo gc' is a plumbing command that will sweep the local
set of stored objects and remove ones that are not pinned in
order to reclaim hard disk space.
`,
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
63
	Options: []cmdkit.Option{
64 65
		cmdkit.BoolOption("stream-errors", "Stream errors."),
		cmdkit.BoolOption("quiet", "q", "Write minimal output."),
66
	},
keks's avatar
keks committed
67 68
	Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error {
		n, err := cmdenv.GetNode(env)
69
		if err != nil {
keks's avatar
keks committed
70
			return err
71 72
		}

keks's avatar
keks committed
73 74 75 76 77 78 79 80 81 82 83 84
		streamErrors, _ := req.Options["stream-errors"].(bool)

		gcOutChan := corerepo.GarbageCollectAsync(n, req.Context)

		if streamErrors {
			errs := false
			for res := range gcOutChan {
				if res.Error != nil {
					re.Emit(&GcResult{Error: res.Error.Error()})
					errs = true
				} else {
					re.Emit(&GcResult{Key: res.KeyRemoved})
85
				}
Jeromy's avatar
Jeromy committed
86
			}
keks's avatar
keks committed
87 88
			if errs {
				return errors.New("encountered errors during gc run")
89
			}
keks's avatar
keks committed
90 91 92 93
		} else {
			err := corerepo.CollectResult(req.Context, gcOutChan, func(k cid.Cid) {
				re.Emit(&GcResult{Key: k})
			})
94
			if err != nil {
keks's avatar
keks committed
95
				return err
96
			}
keks's avatar
keks committed
97 98 99 100 101 102 103 104
		}

		return nil
	},
	Type: GcResult{},
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeEncoder(func(req *cmds.Request, w io.Writer, v interface{}) error {
			quiet, _ := req.Options["quiet"].(bool)
105

Jan Winkelmann's avatar
Jan Winkelmann committed
106 107
			obj, ok := v.(*GcResult)
			if !ok {
keks's avatar
keks committed
108
				return e.TypeErr(obj, v)
Jan Winkelmann's avatar
Jan Winkelmann committed
109
			}
110

Jan Winkelmann's avatar
Jan Winkelmann committed
111
			if obj.Error != "" {
keks's avatar
keks committed
112 113
				_, err := fmt.Fprintf(w, "Error: %s\n", obj.Error)
				return err
Jan Winkelmann's avatar
Jan Winkelmann committed
114
			}
115

keks's avatar
keks committed
116 117 118
			prefix := "removed "
			if quiet {
				prefix = ""
119 120
			}

keks's avatar
keks committed
121 122 123
			_, err := fmt.Fprintf(w, "%s%s\n", prefix, obj.Key)
			return err
		}),
124 125
	},
}
126 127

var repoStatCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
128
	Helptext: cmdkit.HelpText{
David Dias's avatar
David Dias committed
129 130
		Tagline: "Get stats for the currently used repo.",
		ShortDescription: `
131
'ipfs repo stat' provides information about the local set of
132 133 134 135
stored objects. It outputs:

RepoSize        int Size in bytes that the repo is currently taking.
StorageMax      string Maximum datastore size (from configuration)
136 137
NumObjects      int Number of objects in the local repo.
RepoPath        string The path to the repo being currently used.
Michael Pfister's avatar
Michael Pfister committed
138
Version         string The repo version.
David Dias's avatar
David Dias committed
139
`,
140
	},
141 142 143 144
	Options: []cmdkit.Option{
		cmdkit.BoolOption("size-only", "Only report RepoSize and StorageMax."),
		cmdkit.BoolOption("human", "Output sizes in MiB."),
	},
keks's avatar
keks committed
145
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
146
		n, err := cmdenv.GetNode(env)
147
		if err != nil {
keks's avatar
keks committed
148
			return err
149 150
		}

151 152
		sizeOnly, _ := req.Options["size-only"].(bool)
		if sizeOnly {
153 154
			sizeStat, err := corerepo.RepoSize(req.Context, n)
			if err != nil {
keks's avatar
keks committed
155
				return err
156 157 158 159
			}
			cmds.EmitOnce(res, &corerepo.Stat{
				SizeStat: sizeStat,
			})
keks's avatar
keks committed
160
			return nil
161 162
		}

163
		stat, err := corerepo.RepoStat(req.Context, n)
164
		if err != nil {
keks's avatar
keks committed
165
			return err
166 167
		}

keks's avatar
keks committed
168
		return cmds.EmitOnce(res, &stat)
169
	},
170
	Type: &corerepo.Stat{},
Jan Winkelmann's avatar
Jan Winkelmann committed
171
	Encoders: cmds.EncoderMap{
172
		cmds.Text: cmds.MakeEncoder(func(req *cmds.Request, w io.Writer, v interface{}) error {
Jan Winkelmann's avatar
Jan Winkelmann committed
173
			stat, ok := v.(*corerepo.Stat)
174
			if !ok {
Jan Winkelmann's avatar
Jan Winkelmann committed
175
				return e.TypeErr(stat, v)
176 177
			}

Jan Winkelmann's avatar
Jan Winkelmann committed
178
			wtr := tabwriter.NewWriter(w, 0, 0, 1, ' ', 0)
179 180 181 182
			defer wtr.Flush()

			human, _ := req.Options["human"].(bool)
			sizeOnly, _ := req.Options["size-only"].(bool)
Jan Winkelmann's avatar
Jan Winkelmann committed
183

184 185 186 187
			printSize := func(name string, size uint64) {
				sizeInMiB := size / (1024 * 1024)
				if human && sizeInMiB > 0 {
					fmt.Fprintf(wtr, "%s (MiB):\t%d\n", name, sizeInMiB)
188
				} else {
189
					fmt.Fprintf(wtr, "%s:\t%d\n", name, size)
190
				}
191
			}
192

193 194
			if !sizeOnly {
				fmt.Fprintf(wtr, "NumObjects:\t%d\n", stat.NumObjects)
195 196
			}

197 198
			printSize("RepoSize", stat.RepoSize)
			printSize("StorageMax", stat.StorageMax)
Thomas Gardner's avatar
Thomas Gardner committed
199

200 201 202 203
			if !sizeOnly {
				fmt.Fprintf(wtr, "RepoPath:\t%s\n", stat.RepoPath)
				fmt.Fprintf(wtr, "Version:\t%s\n", stat.Version)
			}
Jan Winkelmann's avatar
Jan Winkelmann committed
204

205
			return nil
Jan Winkelmann's avatar
Jan Winkelmann committed
206
		}),
207 208
	},
}
michael's avatar
michael committed
209

Jan Winkelmann's avatar
Jan Winkelmann committed
210 211
var RepoFsckCmd = &oldcmds.Command{
	Helptext: cmdkit.HelpText{
212
		Tagline: "Remove repo lockfiles.",
michael's avatar
michael committed
213 214 215 216 217 218
		ShortDescription: `
'ipfs repo fsck' is a plumbing command that will remove repo and level db
lockfiles, as well as the api file. This command can only run when no ipfs
daemons are running.
`,
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
219
	Run: func(req oldcmds.Request, res oldcmds.Response) {
michael's avatar
michael committed
220 221 222 223
		configRoot := req.InvocContext().ConfigRoot

		dsPath, err := config.DataStorePath(configRoot)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
224
			res.SetError(err, cmdkit.ErrNormal)
michael's avatar
michael committed
225 226 227 228
			return
		}

		dsLockFile := filepath.Join(dsPath, "LOCK") // TODO: get this lockfile programmatically
229
		repoLockFile := filepath.Join(configRoot, fsrepo.LockFile)
michael's avatar
michael committed
230 231 232 233 234 235 236 237
		apiFile := filepath.Join(configRoot, "api") // TODO: get this programmatically

		log.Infof("Removing repo lockfile: %s", repoLockFile)
		log.Infof("Removing datastore lockfile: %s", dsLockFile)
		log.Infof("Removing api file: %s", apiFile)

		err = os.Remove(repoLockFile)
		if err != nil && !os.IsNotExist(err) {
Jan Winkelmann's avatar
Jan Winkelmann committed
238
			res.SetError(err, cmdkit.ErrNormal)
michael's avatar
michael committed
239 240 241 242
			return
		}
		err = os.Remove(dsLockFile)
		if err != nil && !os.IsNotExist(err) {
Jan Winkelmann's avatar
Jan Winkelmann committed
243
			res.SetError(err, cmdkit.ErrNormal)
michael's avatar
michael committed
244 245 246 247
			return
		}
		err = os.Remove(apiFile)
		if err != nil && !os.IsNotExist(err) {
Jan Winkelmann's avatar
Jan Winkelmann committed
248
			res.SetError(err, cmdkit.ErrNormal)
michael's avatar
michael committed
249 250 251
			return
		}

Jeromy's avatar
Jeromy committed
252
		res.SetOutput(&MessageOutput{"Lockfiles have been removed.\n"})
michael's avatar
michael committed
253 254
	},
	Type: MessageOutput{},
Jan Winkelmann's avatar
Jan Winkelmann committed
255 256
	Marshalers: oldcmds.MarshalerMap{
		oldcmds.Text: MessageTextMarshaler,
Michael Pfister's avatar
Michael Pfister committed
257 258 259
	},
}

Jeromy's avatar
Jeromy committed
260
type VerifyProgress struct {
Jan Winkelmann's avatar
Jan Winkelmann committed
261
	Msg      string
Jeromy's avatar
Jeromy committed
262 263 264
	Progress int
}

Jan Winkelmann's avatar
Jan Winkelmann committed
265 266
var repoVerifyCmd = &oldcmds.Command{
	Helptext: cmdkit.HelpText{
Jeromy's avatar
Jeromy committed
267 268
		Tagline: "Verify all blocks in repo are not corrupted.",
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
269
	Run: func(req oldcmds.Request, res oldcmds.Response) {
Jeromy's avatar
Jeromy committed
270 271
		nd, err := req.InvocContext().GetNode()
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
272
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
273 274 275 276
			return
		}

		out := make(chan interface{})
Jan Winkelmann's avatar
Jan Winkelmann committed
277 278
		res.SetOutput((<-chan interface{})(out))
		defer close(out)
Jeromy's avatar
Jeromy committed
279

Jan Winkelmann's avatar
Jan Winkelmann committed
280 281
		bs := bstore.NewBlockstore(nd.Repo.Datastore())
		bs.HashOnRead(true)
Jeromy's avatar
Jeromy committed
282

Jan Winkelmann's avatar
Jan Winkelmann committed
283 284 285 286 287 288 289 290 291 292
		keys, err := bs.AllKeysChan(req.Context())
		if err != nil {
			log.Error(err)
			return
		}

		var fails int
		var i int
		for k := range keys {
			_, err := bs.Get(k)
Jeromy's avatar
Jeromy committed
293
			if err != nil {
294 295
				select {
				case out <- &VerifyProgress{
Jan Winkelmann's avatar
Jan Winkelmann committed
296
					Msg: fmt.Sprintf("block %s was corrupt (%s)", k, err),
297 298 299
				}:
				case <-req.Context().Done():
					return
Jan Winkelmann's avatar
Jan Winkelmann committed
300 301
				}
				fails++
Jeromy's avatar
Jeromy committed
302
			}
Jan Winkelmann's avatar
Jan Winkelmann committed
303
			i++
304 305 306 307 308
			select {
			case out <- &VerifyProgress{Progress: i}:
			case <-req.Context().Done():
				return
			}
Jan Winkelmann's avatar
Jan Winkelmann committed
309
		}
Jeromy's avatar
Jeromy committed
310

Jan Winkelmann's avatar
Jan Winkelmann committed
311
		if fails == 0 {
312 313 314 315 316
			select {
			case out <- &VerifyProgress{Msg: "verify complete, all blocks validated."}:
			case <-req.Context().Done():
				return
			}
Jan Winkelmann's avatar
Jan Winkelmann committed
317 318 319 320 321 322 323 324 325 326
		} else {
			res.SetError(fmt.Errorf("verify complete, some blocks were corrupt"), cmdkit.ErrNormal)
		}
	},
	Type: &VerifyProgress{},
	Marshalers: oldcmds.MarshalerMap{
		oldcmds.Text: func(res oldcmds.Response) (io.Reader, error) {
			v, err := unwrapOutput(res.Output())
			if err != nil {
				return nil, err
Jeromy's avatar
Jeromy committed
327
			}
Jan Winkelmann's avatar
Jan Winkelmann committed
328 329 330 331

			obj, ok := v.(*VerifyProgress)
			if !ok {
				return nil, e.TypeErr(obj, v)
Jeromy's avatar
Jeromy committed
332 333
			}

Jan Winkelmann's avatar
Jan Winkelmann committed
334 335 336 337 338
			buf := new(bytes.Buffer)
			if strings.Contains(obj.Msg, "was corrupt") {
				fmt.Fprintln(os.Stdout, obj.Msg)
				return buf, nil
			}
Jeromy's avatar
Jeromy committed
339

Jan Winkelmann's avatar
Jan Winkelmann committed
340 341 342
			if obj.Msg != "" {
				if len(obj.Msg) < 20 {
					obj.Msg += "             "
Jeromy's avatar
Jeromy committed
343
				}
Jan Winkelmann's avatar
Jan Winkelmann committed
344
				fmt.Fprintln(buf, obj.Msg)
Jeromy's avatar
Jeromy committed
345 346 347
				return buf, nil
			}

Jan Winkelmann's avatar
Jan Winkelmann committed
348 349
			fmt.Fprintf(buf, "%d blocks processed.\r", obj.Progress)
			return buf, nil
Jeromy's avatar
Jeromy committed
350 351 352 353
		},
	},
}

Jan Winkelmann's avatar
Jan Winkelmann committed
354 355
var repoVersionCmd = &oldcmds.Command{
	Helptext: cmdkit.HelpText{
Michael Pfister's avatar
Michael Pfister committed
356 357 358 359 360 361
		Tagline: "Show the repo version.",
		ShortDescription: `
'ipfs repo version' returns the current repo version.
`,
	},

Jan Winkelmann's avatar
Jan Winkelmann committed
362 363
	Options: []cmdkit.Option{
		cmdkit.BoolOption("quiet", "q", "Write minimal output."),
Michael Pfister's avatar
Michael Pfister committed
364
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
365
	Run: func(req oldcmds.Request, res oldcmds.Response) {
Michael Pfister's avatar
Michael Pfister committed
366
		res.SetOutput(&RepoVersion{
367
			Version: fmt.Sprint(fsrepo.RepoVersion),
Michael Pfister's avatar
Michael Pfister committed
368 369 370
		})
	},
	Type: RepoVersion{},
Jan Winkelmann's avatar
Jan Winkelmann committed
371 372 373 374 375 376 377 378 379 380
	Marshalers: oldcmds.MarshalerMap{
		oldcmds.Text: func(res oldcmds.Response) (io.Reader, error) {
			v, err := unwrapOutput(res.Output())
			if err != nil {
				return nil, err
			}
			response, ok := v.(*RepoVersion)
			if !ok {
				return nil, e.TypeErr(response, v)
			}
Michael Pfister's avatar
Michael Pfister committed
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395

			quiet, _, err := res.Request().Option("quiet").Bool()
			if err != nil {
				return nil, err
			}

			buf := new(bytes.Buffer)
			if quiet {
				buf = bytes.NewBufferString(fmt.Sprintf("fs-repo@%s\n", response.Version))
			} else {
				buf = bytes.NewBufferString(fmt.Sprintf("ipfs repo version fs-repo@%s\n", response.Version))
			}
			return buf, nil

		},
michael's avatar
michael committed
396 397
	},
}