dump.go 5.64 KB
Newer Older
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
1 2 3 4
// Copyright (C) 2016-2017  Nexedi SA and Contributors.
//                          Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
5
// it under the terms of the GNU General Public License version 3, or (at your
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
6 7
// option) any later version, as published by the Free Software Foundation.
//
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
8
// You can also Link and Combine this program with other software covered by
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
9 10 11 12
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
13
//
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
14 15 16 17
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
18
// See https://www.nexedi.com/licensing for rationale and options.
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
19 20 21 22

/*
Zodbdump - Tool to dump content of a ZODB database

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
23 24
TODO sync text with zodbdump/py

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
25 26 27
Format
------

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
28
txn <tid> (<status>)	XXX escape status ?
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
29 30 31 32 33 34 35 36 37 38
user <user|quote>
description <description|quote>
extension <extension|quote>
obj <oid> (delete | from <tid> | sha1:<sha1> <size> (LF <content>)?) LF     XXX do we really need back <tid>
---- // ----
LF
txn ...

*/

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
39
package zodbtools
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
40 41

import (
42
	"context"
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
43 44 45 46 47 48
	"crypto/sha1"
	"flag"
	"fmt"
	"io"
	"os"

49
	"lab.nexedi.com/kirr/go123/prog"
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
50
	"lab.nexedi.com/kirr/go123/xfmt"
51
	"lab.nexedi.com/kirr/neo/go/zodb"
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
52 53
)

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
54 55 56 57 58 59 60

// dumper dumps zodb record to a writer
type dumper struct {
	W          io.Writer
	HashOnly   bool		// whether to dump only hashes of data without content

	afterFirst bool // true after first transaction has been dumped
61

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
62
	buf xfmt.Buffer // reusable data buffer for formatting
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
63 64
}

65 66 67
var _LF = []byte{'\n'}


Kirill Smelkov's avatar
.  
Kirill Smelkov committed
68
// DumpData dumps one data record
Kirill Smelkov's avatar
Kirill Smelkov committed
69
// XXX naming -> DumpObj ?
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
70
func (d *dumper) DumpData(datai *zodb.DataInfo) error {
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
71 72
	buf := &d.buf
	buf.Reset()
73

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
74
	buf .S("obj ") .V(&datai.Oid) .Cb(' ')
75

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
76 77 78 79
	writeData := false

	switch {
	case datai.Data == nil:
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
80
		buf .S("delete")
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
81 82

	case datai.Tid != datai.DataTid:
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
83
		buf .S("from ") .V(&datai.DataTid)
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
84 85

	default:
86
		dataSha1 := sha1.Sum(datai.Data)
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
87
		buf .D(len(datai.Data)) .S(" sha1:") .Xb(dataSha1[:])
88

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
89
		writeData = true
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
90 91
	}

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
92
	buf .Cb('\n')
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
93 94

	// TODO use writev(data, "\n") via net.Buffers (it is already available)
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
95
	_, err := d.W.Write(buf.Bytes())
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
96
	if err != nil {
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
97
		goto out
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
98
	}
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
99 100 101 102

	if writeData && !d.HashOnly {
		_, err = d.W.Write(datai.Data)
		if err != nil {
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
103
			goto out
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
104 105
		}

Kirill Smelkov's avatar
Kirill Smelkov committed
106
		// XXX maybe better to merge with next record ?
107
		_, err = d.W.Write(_LF)
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
108
		if err != nil {
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
109
			goto out
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
110 111 112
		}
	}

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
113 114 115 116 117 118 119
out:
	// XXX do we need this context ?
	// see for rationale in similar place in DumpTxn
	if err != nil {
		return fmt.Errorf("%v: %v", datai.Oid, err)
	}

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
120
	return nil
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
121 122
}

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
123
// DumpTxn dumps one transaction record
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
124 125
func (d *dumper) DumpTxn(txni *zodb.TxnInfo, dataIter zodb.IDataIterator) error {
	var datai *zodb.DataInfo
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
126

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
127 128 129 130 131 132 133 134 135 136
	// LF in-between txn records
	vskip := "\n"
	if !d.afterFirst {
		vskip = ""
		d.afterFirst = true
	}

	_, err := fmt.Fprintf(d.W, "%stxn %s (%c)\nuser %q\ndescription %q\nextension %q\n",
			vskip, txni.Tid, txni.Status, txni.User, txni.Description, txni.Extension)
	if err != nil {
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
137
		goto out
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
138 139 140 141
	}

	// data records
	for {
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
142
		datai, err = dataIter.NextData()
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
143 144 145 146 147 148 149 150 151 152 153 154 155
		if err != nil {
			if err == io.EOF {
				err = nil	// XXX -> okEOF ?
			}

			break
		}

		err = d.DumpData(datai)
		if err != nil {
			break
		}
	}
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
156

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
157 158 159 160
out:
	// XXX do we need this context ?
	// rationale: dataIter.NextData() if error in db - will include db context
	// if error is in writer - it will include its own context
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
161 162
	if err != nil {
		return fmt.Errorf("%v: %v", txni.Tid, err)
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
163 164
	}

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
165 166 167 168 169 170
	return nil
}

// Dump dumps transaction records in between tidMin..tidMax
func (d *dumper) Dump(stor zodb.IStorage, tidMin, tidMax zodb.Tid) error {
	var txni     *zodb.TxnInfo
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
171
	var dataIter zodb.IDataIterator
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
	var err      error

	iter := stor.Iterate(tidMin, tidMax)

	// transactions
	for {
		txni, dataIter, err = iter.NextTxn()
		if err != nil {
			if err == io.EOF {
				err = nil	// XXX -> okEOF ?
			}

			break
		}

		err = d.DumpTxn(txni, dataIter)
		if err != nil {
			break
		}
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
191 192
	}

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
193 194 195
	if err != nil {
		return fmt.Errorf("%s: dumping %v..%v: %v", stor, tidMin, tidMax, err)
	}
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
196

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
197
	return nil
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
198 199
}

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
200
// Dump dumps contents of a storage in between tidMin..tidMax range to a writer.
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
201
// see top-level documentation for the dump format.
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
202
func Dump(w io.Writer, stor zodb.IStorage, tidMin, tidMax zodb.Tid, hashOnly bool) error {
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
203 204 205 206
	d := dumper{W: w, HashOnly: hashOnly}
	return d.Dump(stor, tidMin, tidMax)
}

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
207 208 209
// ----------------------------------------

const dumpSummary = "dump content of a ZODB database"
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
210

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
211 212
func dumpUsage(w io.Writer) {
	fmt.Fprintf(w,
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
213
`Usage: zodb dump [OPTIONS] <storage> [tidmin..tidmax]
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
214 215
Dump content of a ZODB database.

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
216
<storage> is an URL (see 'zodb help zurl') of a ZODB-storage.
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
217

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
218
Options:
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
219 220 221 222 223 224

	-h --help       this help text.
	-hashonly	dump only hashes of objects without content.
`)
}

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
225
func dumpMain(argv []string) {
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
226
	hashOnly := false
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
227
	tidRange := ".." // [0, +inf]
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
228

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
229 230 231
	flags := flag.FlagSet{Usage: func() { dumpUsage(os.Stderr) }}
	flags.Init("", flag.ExitOnError)
	flags.BoolVar(&hashOnly, "hashonly", hashOnly, "dump only hashes of objects")
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
232
	flags.Parse(argv[1:])
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
233

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
234
	argv = flags.Args()
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
235
	if len(argv) < 1 {
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
236
		flags.Usage()
237
		prog.Exit(2)
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
238 239 240 241
	}
	storUrl := argv[0]

	if len(argv) > 1 {
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
242 243 244 245 246
		tidRange = argv[1]
	}

	tidMin, tidMax, err := zodb.ParseTidRange(tidRange)
	if err != nil {
Kirill Smelkov's avatar
Kirill Smelkov committed
247
		prog.Fatal(err)
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
248 249
	}

250
	stor, err := zodb.OpenStorageURL(context.Background(), storUrl)	// TODO read-only
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
251
	if err != nil {
252
		prog.Fatal(err)
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
253
	}
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
254
	// TODO defer stor.Close()
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
255

Kirill Smelkov's avatar
.  
Kirill Smelkov committed
256
	err = Dump(os.Stdout, stor, tidMin, tidMax, hashOnly)
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
257
	if err != nil {
258
		prog.Fatal(err)
Kirill Smelkov's avatar
.  
Kirill Smelkov committed
259 260
	}
}