diff --git a/Makefile b/Makefile index 09faf4765..1bc158842 100644 --- a/Makefile +++ b/Makefile @@ -153,7 +153,10 @@ endif ci-tests: lint vet test -ci-integration-tests: integration-tests robustness-tool-tests +ci-integration-tests: + $(MAKE) integration-tests + $(MAKE) integration-tests-index-v2 + $(MAKE) robustness-tool-tests $(MAKE) stress-test ci-publish-coverage: @@ -220,6 +223,9 @@ integration-tests: build-integration-test-binary $(gotestsum) $(TESTING_ACTION_E $(GO_TEST) $(TEST_FLAGS) -count=$(REPEAT_TEST) -parallel $(PARALLEL) -timeout 3600s github.com/kopia/kopia/tests/end_to_end_test -$(gotestsum) tool slowest --jsonfile .tmp.integration-tests.json --threshold 1000ms +integration-tests-index-v2: + KOPIA_CREATE_INDEX_VERSION=2 KOPIA_RUN_ALL_INTEGRATION_TESTS=true $(MAKE) integration-tests + endurance-tests: export KOPIA_EXE ?= $(KOPIA_INTEGRATION_EXE) endurance-tests: export KOPIA_LOGS_DIR=$(CURDIR)/.logs endurance-tests: build-integration-test-binary $(gotestsum) diff --git a/cli/command_content_list.go b/cli/command_content_list.go index 90077d830..3ddb75489 100644 --- a/cli/command_content_list.go +++ b/cli/command_content_list.go @@ -2,11 +2,13 @@ import ( "context" + "fmt" "github.com/pkg/errors" "github.com/kopia/kopia/internal/stats" "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/repo/compression" "github.com/kopia/kopia/repo/content" ) @@ -16,6 +18,7 @@ type commandContentList struct { deletedOnly bool summary bool human bool + compression bool contentRange contentRangeFlags jo jsonOutput @@ -25,6 +28,7 @@ type commandContentList struct { func (c *commandContentList) setup(svc appServices, parent commandParent) { cmd := parent.Command("list", "List contents").Alias("ls") cmd.Flag("long", "Long output").Short('l').BoolVar(&c.long) + cmd.Flag("compression", "Compression").Short('c').BoolVar(&c.compression) cmd.Flag("deleted", "Include deleted content").BoolVar(&c.includeDeleted) cmd.Flag("deleted-only", "Only show deleted content").BoolVar(&c.deletedOnly) cmd.Flag("summary", "Summarize the list").Short('s').BoolVar(&c.summary) @@ -56,24 +60,14 @@ func(b content.Info) error { totalSize.Add(int64(b.GetPackedLength())) - if c.jo.jsonOutput { + switch { + case c.jo.jsonOutput: jl.emit(b) - return nil - } - - if c.long { - optionalDeleted := "" - if b.GetDeleted() { - optionalDeleted = " (deleted)" - } - c.out.printStdout("%v %v %v %v+%v%v\n", - b.GetContentID(), - formatTimestamp(b.Timestamp()), - b.GetPackBlobID(), - b.GetPackOffset(), - maybeHumanReadableBytes(c.human, int64(b.GetPackedLength())), - optionalDeleted) - } else { + case c.compression: + c.outputCompressed(b) + case c.long: + c.outputLong(b) + default: c.out.printStdout("%v\n", b.GetContentID()) } @@ -92,3 +86,52 @@ func(b content.Info) error { return nil } + +func (c *commandContentList) outputLong(b content.Info) { + c.out.printStdout("%v %v %v %v %v+%v%v %v\n", + b.GetContentID(), + b.GetOriginalLength(), + formatTimestamp(b.Timestamp()), + b.GetPackBlobID(), + b.GetPackOffset(), + maybeHumanReadableBytes(c.human, int64(b.GetPackedLength())), + c.deletedInfoString(b), + c.compressionInfoStringString(b), + ) +} + +func (c *commandContentList) outputCompressed(b content.Info) { + c.out.printStdout("%v length %v packed %v %v %v\n", + b.GetContentID(), + maybeHumanReadableBytes(c.human, int64(b.GetOriginalLength())), + maybeHumanReadableBytes(c.human, int64(b.GetPackedLength())), + c.compressionInfoStringString(b), + c.deletedInfoString(b), + ) +} + +func (*commandContentList) deletedInfoString(b content.Info) string { + if b.GetDeleted() { + return " (deleted)" + } + + return "" +} + +func (*commandContentList) compressionInfoStringString(b content.Info) string { + h := b.GetCompressionHeaderID() + if h == content.NoCompression { + return "-" + } + + s := string(compression.HeaderIDToName[h]) + if s == "" { + s = fmt.Sprintf("compression-%x", h) + } + + if b.GetOriginalLength() > 0 { + s += " " + formatCompressionPercentage(int64(b.GetOriginalLength()), int64(b.GetPackedLength())) + } + + return s +} diff --git a/cli/command_content_stats.go b/cli/command_content_stats.go index 1b64cf0d3..28cda4aca 100644 --- a/cli/command_content_stats.go +++ b/cli/command_content_stats.go @@ -8,6 +8,7 @@ "github.com/kopia/kopia/internal/units" "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/repo/compression" "github.com/kopia/kopia/repo/content" ) @@ -25,39 +26,24 @@ func (c *commandContentStats) setup(svc appServices, parent commandParent) { cmd.Action(svc.directRepositoryReadAction(c.run)) } +type contentStatsTotals struct { + originalSize, packedSize, count int64 +} + func (c *commandContentStats) run(ctx context.Context, rep repo.DirectRepository) error { - var sizeThreshold uint32 = 10 - - countMap := map[uint32]int{} - totalSizeOfContentsUnder := map[uint32]int64{} - - var sizeThresholds []uint32 + var ( + sizeThreshold uint32 = 10 + sizeBuckets []uint32 + ) for i := 0; i < 8; i++ { - sizeThresholds = append(sizeThresholds, sizeThreshold) - countMap[sizeThreshold] = 0 + sizeBuckets = append(sizeBuckets, sizeThreshold) sizeThreshold *= 10 } - var totalSize, count int64 - - if err := rep.ContentReader().IterateContents( - ctx, - content.IterateOptions{ - Range: c.contentRange.contentIDRange(), - }, - func(b content.Info) error { - totalSize += int64(b.GetPackedLength()) - count++ - for s := range countMap { - if b.GetPackedLength() < s { - countMap[s]++ - totalSizeOfContentsUnder[s] += int64(b.GetPackedLength()) - } - } - return nil - }); err != nil { - return errors.Wrap(err, "error iterating contents") + grandTotal, byCompressionTotal, countMap, totalSizeOfContentsUnder, err := c.calculateStats(ctx, rep, sizeBuckets) + if err != nil { + return errors.Wrap(err, "error calculating totals") } sizeToString := units.BytesStringBase10 @@ -65,20 +51,47 @@ func(b content.Info) error { sizeToString = func(l int64) string { return strconv.FormatInt(l, 10) } } - c.out.printStdout("Count: %v\n", count) - c.out.printStdout("Total: %v\n", sizeToString(totalSize)) + c.out.printStdout("Count: %v\n", grandTotal.count) + c.out.printStdout("Total Bytes: %v\n", sizeToString(grandTotal.originalSize)) - if count == 0 { + if grandTotal.packedSize < grandTotal.originalSize { + c.out.printStdout( + "Total Packed: %v (compression %v)\n", + sizeToString(grandTotal.packedSize), + formatCompressionPercentage(grandTotal.originalSize, grandTotal.packedSize)) + } + + if len(byCompressionTotal) > 1 { + c.out.printStdout("By Method:\n") + + if bct := byCompressionTotal[content.NoCompression]; bct != nil { + c.out.printStdout(" %-22v count: %v size: %v\n", "(uncompressed)", bct.count, sizeToString(bct.originalSize)) + } + + for hdrID, bct := range byCompressionTotal { + cname := compression.HeaderIDToName[hdrID] + if cname == "" { + continue + } + + c.out.printStdout(" %-22v count: %v size: %v packed: %v compression: %v\n", + cname, bct.count, + sizeToString(bct.originalSize), + sizeToString(bct.packedSize), + formatCompressionPercentage(bct.originalSize, bct.packedSize)) + } + } + + if grandTotal.count == 0 { return nil } - c.out.printStdout("Average: %v\n", sizeToString(totalSize/count)) - + c.out.printStdout("Average: %v\n", sizeToString(grandTotal.originalSize/grandTotal.count)) c.out.printStdout("Histogram:\n\n") var lastSize uint32 - for _, size := range sizeThresholds { + for _, size := range sizeBuckets { c.out.printStdout("%9v between %v and %v (total %v)\n", countMap[size]-countMap[lastSize], sizeToString(int64(lastSize)), @@ -91,3 +104,51 @@ func(b content.Info) error { return nil } + +func (c *commandContentStats) calculateStats(ctx context.Context, rep repo.DirectRepository, sizeBuckets []uint32) ( + grandTotal contentStatsTotals, + byCompressionTotal map[compression.HeaderID]*contentStatsTotals, + countMap map[uint32]int, + totalSizeOfContentsUnder map[uint32]int64, + err error, +) { + byCompressionTotal = make(map[compression.HeaderID]*contentStatsTotals) + totalSizeOfContentsUnder = make(map[uint32]int64) + countMap = make(map[uint32]int) + + for _, s := range sizeBuckets { + countMap[s] = 0 + } + + err = rep.ContentReader().IterateContents( + ctx, + content.IterateOptions{ + Range: c.contentRange.contentIDRange(), + }, + func(b content.Info) error { + grandTotal.packedSize += int64(b.GetPackedLength()) + grandTotal.originalSize += int64(b.GetOriginalLength()) + grandTotal.count++ + + bct := byCompressionTotal[b.GetCompressionHeaderID()] + if bct == nil { + bct = &contentStatsTotals{} + byCompressionTotal[b.GetCompressionHeaderID()] = bct + } + + bct.packedSize += int64(b.GetPackedLength()) + bct.originalSize += int64(b.GetOriginalLength()) + bct.count++ + + for s := range countMap { + if b.GetPackedLength() < s { + countMap[s]++ + totalSizeOfContentsUnder[s] += int64(b.GetPackedLength()) + } + } + return nil + }) + + // nolint:wrapcheck + return grandTotal, byCompressionTotal, countMap, totalSizeOfContentsUnder, err +} diff --git a/cli/command_repository_create.go b/cli/command_repository_create.go index 6853a1cac..9034f909a 100644 --- a/cli/command_repository_create.go +++ b/cli/command_repository_create.go @@ -21,6 +21,7 @@ type commandRepositoryCreate struct { createBlockEncryptionFormat string createSplitter string createOnly bool + createIndexVersion int co connectOptions svc advancedAppServices @@ -34,6 +35,7 @@ func (c *commandRepositoryCreate) setup(svc advancedAppServices, parent commandP cmd.Flag("encryption", "Content encryption algorithm.").PlaceHolder("ALGO").Default(encryption.DefaultAlgorithm).EnumVar(&c.createBlockEncryptionFormat, encryption.SupportedAlgorithms(false)...) cmd.Flag("object-splitter", "The splitter to use for new objects in the repository").Default(splitter.DefaultAlgorithm).EnumVar(&c.createSplitter, splitter.SupportedAlgorithms()...) cmd.Flag("create-only", "Create repository, but don't connect to it.").Short('c').BoolVar(&c.createOnly) + cmd.Flag("index-version", "Force particular index version").Hidden().Envar("KOPIA_CREATE_INDEX_VERSION").IntVar(&c.createIndexVersion) c.co.setup(cmd) c.svc = svc @@ -63,8 +65,9 @@ func (c *commandRepositoryCreate) setup(svc advancedAppServices, parent commandP func (c *commandRepositoryCreate) newRepositoryOptionsFromFlags() *repo.NewRepositoryOptions { return &repo.NewRepositoryOptions{ BlockFormat: content.FormattingOptions{ - Hash: c.createBlockHashFormat, - Encryption: c.createBlockEncryptionFormat, + Hash: c.createBlockHashFormat, + Encryption: c.createBlockEncryptionFormat, + IndexVersion: c.createIndexVersion, }, ObjectFormat: object.Format{ diff --git a/cli/command_repository_status.go b/cli/command_repository_status.go index 35229f8c4..670dd4aea 100644 --- a/cli/command_repository_status.go +++ b/cli/command_repository_status.go @@ -60,6 +60,7 @@ func (c *commandRepositoryStatus) run(ctx context.Context, rep repo.Repository) c.out.printStdout("Encryption: %v\n", dr.ContentReader().ContentFormat().Encryption) c.out.printStdout("Splitter: %v\n", dr.ObjectFormat().Splitter) c.out.printStdout("Format version: %v\n", dr.ContentReader().ContentFormat().Version) + c.out.printStdout("Content compression: %v\n", dr.ContentReader().SupportsContentCompression()) c.out.printStdout("Max pack length: %v\n", units.BytesStringBase2(int64(dr.ContentReader().ContentFormat().MaxPackSize))) if !c.statusReconnectToken { diff --git a/cli/show_utils.go b/cli/show_utils.go index 86c133b78..8d2219a39 100644 --- a/cli/show_utils.go +++ b/cli/show_utils.go @@ -15,6 +15,8 @@ "github.com/kopia/kopia/internal/units" ) +const oneHundredPercent = 100.0 + // TODO - remove this global. var timeZone = "local" @@ -90,3 +92,15 @@ func convertTimezone(ts time.Time) time.Time { return ts } } + +func formatCompressionPercentage(original, compressed int64) string { + if compressed >= original { + return "0%" + } + + if original == 0 { + return "0%" + } + + return fmt.Sprintf("%.1f%%", oneHundredPercent*(1-float64(compressed)/float64(original))) +} diff --git a/htmlui/src/RepoStatus.js b/htmlui/src/RepoStatus.js index 73c30a740..b8ab901d7 100644 --- a/htmlui/src/RepoStatus.js +++ b/htmlui/src/RepoStatus.js @@ -159,6 +159,10 @@ export class RepoStatus extends Component { Splitter Algorithm + + Supports Content Compression + + } diff --git a/htmlui/src/SetupRepository.js b/htmlui/src/SetupRepository.js index 882649b68..4d817263f 100644 --- a/htmlui/src/SetupRepository.js +++ b/htmlui/src/SetupRepository.js @@ -61,6 +61,7 @@ export class SetupRepository extends Component { hash: result.data.defaultHash, encryption: result.data.defaultEncryption, splitter: result.data.defaultSplitter, + indexVersion: "", }); }); axios.get('/api/v1/current-user').then(result => { @@ -107,7 +108,7 @@ export class SetupRepository extends Component { return; } - const request = { + let request = { storage: { type: this.state.provider, config: this.state.providerSettings, @@ -124,6 +125,10 @@ export class SetupRepository extends Component { }, }; + if (this.state.indexVersion) { + request.options.blockFormat.indexVersion = parseInt(this.state.indexVersion) + } + request.clientOptions = this.clientOptions(); axios.post('/api/v1/repo/create', request).then(result => { @@ -360,6 +365,18 @@ export class SetupRepository extends Component { {this.state.algorithms.splitter.map(x => )} + + Index Format + + + + + + {this.overrideUsernameHostnameRow()} diff --git a/internal/grpcapi/repository_server.pb.go b/internal/grpcapi/repository_server.pb.go index 02d3328cb..74b0eafd7 100644 --- a/internal/grpcapi/repository_server.pb.go +++ b/internal/grpcapi/repository_server.pb.go @@ -319,9 +319,10 @@ type RepositoryParameters struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - HashFunction string `protobuf:"bytes,1,opt,name=hash_function,json=hashFunction,proto3" json:"hash_function,omitempty"` - HmacSecret []byte `protobuf:"bytes,2,opt,name=hmac_secret,json=hmacSecret,proto3" json:"hmac_secret,omitempty"` - Splitter string `protobuf:"bytes,3,opt,name=splitter,proto3" json:"splitter,omitempty"` + HashFunction string `protobuf:"bytes,1,opt,name=hash_function,json=hashFunction,proto3" json:"hash_function,omitempty"` + HmacSecret []byte `protobuf:"bytes,2,opt,name=hmac_secret,json=hmacSecret,proto3" json:"hmac_secret,omitempty"` + Splitter string `protobuf:"bytes,3,opt,name=splitter,proto3" json:"splitter,omitempty"` + SupportsContentCompression bool `protobuf:"varint,4,opt,name=supports_content_compression,json=supportsContentCompression,proto3" json:"supports_content_compression,omitempty"` } func (x *RepositoryParameters) Reset() { @@ -377,6 +378,13 @@ func (x *RepositoryParameters) GetSplitter() string { return "" } +func (x *RepositoryParameters) GetSupportsContentCompression() bool { + if x != nil { + return x.SupportsContentCompression + } + return false +} + // InitializeSessionRequest must be sent by the client as the first request in a session. type InitializeSessionRequest struct { state protoimpl.MessageState @@ -750,8 +758,9 @@ type WriteContentRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` - Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + Compression uint32 `protobuf:"varint,3,opt,name=compression,proto3" json:"compression,omitempty"` } func (x *WriteContentRequest) Reset() { @@ -800,6 +809,13 @@ func (x *WriteContentRequest) GetData() []byte { return nil } +func (x *WriteContentRequest) GetCompression() uint32 { + if x != nil { + return x.Compression + } + return 0 +} + type WriteContentResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1674,206 +1690,212 @@ func (*SessionResponse_DeleteManifest) isSessionResponse_Response() {} 0x10, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x04, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x43, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x44, 0x45, 0x4e, 0x49, 0x45, 0x44, 0x10, 0x05, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x54, 0x52, 0x45, 0x41, 0x4d, - 0x5f, 0x42, 0x52, 0x4f, 0x4b, 0x45, 0x4e, 0x10, 0x06, 0x22, 0x78, 0x0a, 0x14, 0x52, 0x65, 0x70, - 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x68, 0x61, 0x73, 0x68, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x46, 0x75, - 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x6d, 0x61, 0x63, 0x5f, 0x73, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x68, 0x6d, 0x61, - 0x63, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x70, 0x6c, 0x69, 0x74, - 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x70, 0x6c, 0x69, 0x74, - 0x74, 0x65, 0x72, 0x22, 0x51, 0x0a, 0x18, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, - 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x18, 0x0a, 0x07, 0x70, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x70, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x61, - 0x64, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, - 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x63, 0x0a, 0x19, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, - 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, - 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x73, - 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x52, - 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x36, 0x0a, 0x15, 0x47, - 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, - 0x74, 0x49, 0x64, 0x22, 0x4b, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, - 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, - 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6b, 0x6f, - 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x43, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x69, 0x6e, 0x66, 0x6f, - 0x22, 0x32, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x49, 0x64, 0x22, 0x28, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x0e, - 0x0a, 0x0c, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0f, - 0x0a, 0x0d, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x41, 0x0a, 0x13, 0x57, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x12, - 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, - 0x74, 0x61, 0x22, 0x35, 0x0a, 0x14, 0x57, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x35, 0x0a, 0x12, 0x47, 0x65, 0x74, - 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x49, 0x64, - 0x22, 0x77, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x6a, 0x73, 0x6f, 0x6e, 0x5f, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6a, 0x73, 0x6f, 0x6e, - 0x44, 0x61, 0x74, 0x61, 0x12, 0x43, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, - 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, - 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, - 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xb6, 0x01, 0x0a, 0x12, 0x50, 0x75, - 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x1b, 0x0a, 0x09, 0x6a, 0x73, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6a, 0x73, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x48, 0x0a, - 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, - 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, - 0x2e, 0x50, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0x36, 0x0a, 0x13, 0x50, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, - 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x49, 0x64, 0x22, 0x38, 0x0a, 0x15, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, - 0x73, 0x74, 0x49, 0x64, 0x22, 0x18, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, - 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9d, - 0x01, 0x0a, 0x14, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4a, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, - 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4d, - 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, - 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, - 0x65, 0x6c, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5c, - 0x0a, 0x15, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, - 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xf5, 0x05, 0x0a, - 0x0e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x5b, - 0x0a, 0x12, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x5f, 0x73, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x6b, 0x6f, 0x70, - 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x49, 0x6e, - 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, - 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x53, 0x0a, 0x10, 0x67, - 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, - 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, - 0x52, 0x0e, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, - 0x12, 0x36, 0x0a, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1e, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, - 0x72, 0x79, 0x2e, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, - 0x00, 0x52, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x12, 0x4c, 0x0a, 0x0d, 0x77, 0x72, 0x69, 0x74, - 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, - 0x72, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x77, 0x72, 0x69, 0x74, 0x65, 0x43, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x46, 0x0a, 0x0b, 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6b, 0x6f, - 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, - 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x48, 0x00, 0x52, 0x0a, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x49, - 0x0a, 0x0c, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x0f, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, - 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, - 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x67, 0x65, - 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x49, 0x0a, 0x0c, 0x70, 0x75, 0x74, - 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x24, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, - 0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x70, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, - 0x66, 0x65, 0x73, 0x74, 0x12, 0x4f, 0x0a, 0x0e, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x6d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6b, + 0x5f, 0x42, 0x52, 0x4f, 0x4b, 0x45, 0x4e, 0x10, 0x06, 0x22, 0xba, 0x01, 0x0a, 0x14, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x68, 0x61, 0x73, 0x68, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x68, 0x61, 0x73, 0x68, 0x46, + 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x68, 0x6d, 0x61, 0x63, 0x5f, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x68, 0x6d, + 0x61, 0x63, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x70, 0x6c, 0x69, + 0x74, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x70, 0x6c, 0x69, + 0x74, 0x74, 0x65, 0x72, 0x12, 0x40, 0x0a, 0x1c, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, + 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, 0x73, 0x75, 0x70, 0x70, + 0x6f, 0x72, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x70, 0x72, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x51, 0x0a, 0x18, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, + 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x75, 0x72, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, + 0x72, 0x65, 0x61, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x08, 0x72, 0x65, 0x61, 0x64, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x63, 0x0a, 0x19, 0x49, 0x6e, 0x69, + 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, + 0x74, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6b, 0x6f, 0x70, + 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x73, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x22, 0x36, + 0x0a, 0x15, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x4b, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x31, 0x0a, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, + 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x69, + 0x6e, 0x66, 0x6f, 0x22, 0x32, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x28, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x43, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, + 0x61, 0x22, 0x0e, 0x0a, 0x0c, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x0f, 0x0a, 0x0d, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x63, 0x0a, 0x13, 0x57, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, + 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, + 0x78, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x70, + 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x35, 0x0a, 0x14, 0x57, 0x72, 0x69, 0x74, 0x65, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x35, + 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x6e, 0x69, 0x66, + 0x65, 0x73, 0x74, 0x49, 0x64, 0x22, 0x77, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, + 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, + 0x6a, 0x73, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x08, 0x6a, 0x73, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, 0x43, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, + 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x4d, + 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0xb6, + 0x01, 0x0a, 0x12, 0x50, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x6a, 0x73, 0x6f, 0x6e, 0x5f, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6a, 0x73, 0x6f, 0x6e, 0x44, 0x61, + 0x74, 0x61, 0x12, 0x48, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x39, 0x0a, 0x0b, + 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x36, 0x0a, 0x13, 0x50, 0x75, 0x74, 0x4d, 0x61, + 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, + 0x0a, 0x0b, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x49, 0x64, 0x22, + 0x38, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x6e, 0x69, + 0x66, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, + 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x49, 0x64, 0x22, 0x18, 0x0a, 0x16, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x9d, 0x01, 0x0a, 0x14, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, + 0x66, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4a, 0x0a, 0x06, + 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, - 0x66, 0x65, 0x73, 0x74, 0x73, 0x12, 0x52, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, - 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, + 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, + 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0x5c, 0x0a, 0x15, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, + 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, - 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x72, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x22, 0xb9, 0x06, 0x0a, 0x0f, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x37, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, - 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x12, 0x5c, 0x0a, 0x12, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x5f, 0x73, - 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x6b, - 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, - 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x11, 0x69, 0x6e, 0x69, - 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x54, - 0x0a, 0x10, 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, - 0x66, 0x6f, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, - 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, - 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x48, 0x00, 0x52, 0x0e, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, - 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x37, 0x0a, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, - 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x12, 0x4d, 0x0a, + 0x79, 0x2e, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x22, 0xf5, 0x05, 0x0a, 0x0e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x49, 0x64, 0x12, 0x5b, 0x0a, 0x12, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, + 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x2a, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, + 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x11, 0x69, + 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x53, 0x0a, 0x10, 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, + 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, + 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, + 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x36, 0x0a, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x6c, 0x75, 0x73, 0x68, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x66, 0x6c, 0x75, 0x73, 0x68, 0x12, 0x4c, 0x0a, 0x0d, 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0d, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, - 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0c, - 0x77, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x47, 0x0a, 0x0b, - 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x24, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, - 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x67, 0x65, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x4a, 0x0a, 0x0c, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x6f, - 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, - 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x48, 0x00, 0x52, 0x0b, 0x67, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, - 0x74, 0x12, 0x4a, 0x0a, 0x0c, 0x70, 0x75, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, - 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, - 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, 0x4d, 0x61, - 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, - 0x52, 0x0b, 0x70, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x50, 0x0a, - 0x0e, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x18, - 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, - 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, - 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, - 0x52, 0x0d, 0x66, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x12, - 0x53, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, - 0x73, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, - 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x48, 0x00, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, - 0x66, 0x65, 0x73, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x32, 0x65, 0x0a, 0x0f, 0x4b, 0x6f, 0x70, 0x69, 0x61, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, - 0x6f, 0x72, 0x79, 0x12, 0x52, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, - 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, - 0x79, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x21, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, - 0x6f, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x2f, 0x6b, 0x6f, 0x70, 0x69, - 0x61, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x61, - 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x77, + 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x46, 0x0a, 0x0b, 0x67, + 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x23, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x12, 0x49, 0x0a, 0x0c, 0x67, 0x65, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, + 0x65, 0x73, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6b, 0x6f, 0x70, 0x69, + 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, + 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, + 0x00, 0x52, 0x0b, 0x67, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x49, + 0x0a, 0x0c, 0x70, 0x75, 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x10, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x50, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, + 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x70, 0x75, + 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x4f, 0x0a, 0x0e, 0x66, 0x69, 0x6e, + 0x64, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x26, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x69, 0x6e, + 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x12, 0x52, 0x0a, 0x0f, 0x64, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x12, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, + 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0e, + 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x42, 0x09, + 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb9, 0x06, 0x0a, 0x0f, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, + 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x37, 0x0a, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6b, 0x6f, + 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x5c, 0x0a, 0x12, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, + 0x69, 0x7a, 0x65, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2b, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, + 0x52, 0x11, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x54, 0x0a, 0x10, 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, + 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0e, 0x67, 0x65, 0x74, 0x43, 0x6f, + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x37, 0x0a, 0x05, 0x66, 0x6c, 0x75, + 0x73, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, + 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x6c, 0x75, 0x73, + 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x05, 0x66, 0x6c, 0x75, + 0x73, 0x68, 0x12, 0x4d, 0x0a, 0x0d, 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6b, 0x6f, 0x70, 0x69, + 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x57, 0x72, 0x69, + 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x48, 0x00, 0x52, 0x0c, 0x77, 0x72, 0x69, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x12, 0x47, 0x0a, 0x0b, 0x67, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, + 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, + 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, + 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0a, + 0x67, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x4a, 0x0a, 0x0c, 0x67, 0x65, + 0x74, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x25, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0b, 0x67, 0x65, 0x74, 0x4d, 0x61, + 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x4a, 0x0a, 0x0c, 0x70, 0x75, 0x74, 0x5f, 0x6d, 0x61, + 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6b, + 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, + 0x50, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0b, 0x70, 0x75, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, + 0x73, 0x74, 0x12, 0x50, 0x0a, 0x0e, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x6d, 0x61, 0x6e, 0x69, 0x66, + 0x65, 0x73, 0x74, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6b, 0x6f, 0x70, + 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x46, 0x69, + 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x69, 0x6e, 0x64, 0x4d, 0x61, 0x6e, 0x69, 0x66, + 0x65, 0x73, 0x74, 0x73, 0x12, 0x53, 0x0a, 0x0f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x5f, 0x6d, + 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, + 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x72, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x65, 0x0a, 0x0f, 0x4b, 0x6f, 0x70, 0x69, 0x61, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x52, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, 0x70, 0x6f, + 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x5f, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x29, 0x5a, 0x27, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6b, 0x6f, 0x70, 0x69, 0x61, + 0x2f, 0x6b, 0x6f, 0x70, 0x69, 0x61, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, + 0x67, 0x72, 0x70, 0x63, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/grpcapi/repository_server.proto b/internal/grpcapi/repository_server.proto index 6c9e8ff48..119f37415 100644 --- a/internal/grpcapi/repository_server.proto +++ b/internal/grpcapi/repository_server.proto @@ -45,6 +45,7 @@ message RepositoryParameters { string hash_function = 1; bytes hmac_secret = 2; string splitter = 3; + bool supports_content_compression = 4; } // InitializeSessionRequest must be sent by the client as the first request in a session. @@ -83,6 +84,7 @@ message FlushResponse { message WriteContentRequest { string prefix = 1; bytes data = 2; + uint32 compression = 3; } message WriteContentResponse { diff --git a/internal/remoterepoapi/remoterepoapi.go b/internal/remoterepoapi/remoterepoapi.go index 8121ee36c..9ecb84251 100644 --- a/internal/remoterepoapi/remoterepoapi.go +++ b/internal/remoterepoapi/remoterepoapi.go @@ -11,8 +11,9 @@ // Parameters encapsulates all parameters for repository. // returned by /api/v1/repo/parameters. type Parameters struct { - HashFunction string `json:"hash"` - HMACSecret []byte `json:"hmacSecret"` + HashFunction string `json:"hash"` + HMACSecret []byte `json:"hmacSecret"` + SupportsContentCompression bool `json:"supportsContentCompression"` object.Format } diff --git a/internal/repotesting/repotesting_test.go b/internal/repotesting/repotesting_test.go index d12a53d4e..5c47cb321 100644 --- a/internal/repotesting/repotesting_test.go +++ b/internal/repotesting/repotesting_test.go @@ -7,6 +7,7 @@ "github.com/kopia/kopia/internal/faketime" "github.com/kopia/kopia/internal/mockfs" "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/repo/content" "github.com/kopia/kopia/snapshot" "github.com/kopia/kopia/snapshot/policy" "github.com/kopia/kopia/snapshot/snapshotfs" @@ -42,7 +43,7 @@ func TestTimeFuncWiring(t *testing.T) { // verify wiring for the content layer nt := ft.Advance(20 * time.Second) - cid, err := env.RepositoryWriter.ContentManager().WriteContent(ctx, []byte("foo"), "") + cid, err := env.RepositoryWriter.ContentManager().WriteContent(ctx, []byte("foo"), "", content.NoCompression) if err != nil { t.Fatal("failed to write content:", err) } diff --git a/internal/server/api_content.go b/internal/server/api_content.go index 60b584178..6d2256939 100644 --- a/internal/server/api_content.go +++ b/internal/server/api_content.go @@ -4,12 +4,14 @@ "context" "errors" "net/http" + "strconv" "strings" "github.com/gorilla/mux" "github.com/kopia/kopia/internal/serverapi" "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/repo/compression" "github.com/kopia/kopia/repo/content" "github.com/kopia/kopia/repo/manifest" ) @@ -66,7 +68,21 @@ func (s *Server) handleContentPut(ctx context.Context, r *http.Request, data []b return nil, accessDeniedError() } - actualCID, err := dr.ContentManager().WriteContent(ctx, data, prefix) + var comp compression.HeaderID + + if c := r.URL.Query().Get("compression"); c != "" { + v, err := strconv.ParseInt(c, 16, 32) + if err != nil { + return nil, requestError(serverapi.ErrorMalformedRequest, "malformed compression ID") + } + + comp = compression.HeaderID(v) + if _, ok := compression.ByHeaderID[comp]; !ok { + return nil, requestError(serverapi.ErrorMalformedRequest, "invalid compression ID") + } + } + + actualCID, err := dr.ContentManager().WriteContent(ctx, data, prefix, comp) if err != nil { return nil, internalServerError(err) } diff --git a/internal/server/api_repo.go b/internal/server/api_repo.go index 4614d1372..f8f58ac66 100644 --- a/internal/server/api_repo.go +++ b/internal/server/api_repo.go @@ -30,9 +30,10 @@ func (s *Server) handleRepoParameters(ctx context.Context, r *http.Request, body } rp := &remoterepoapi.Parameters{ - HashFunction: dr.ContentReader().ContentFormat().Hash, - HMACSecret: dr.ContentReader().ContentFormat().HMACSecret, - Format: dr.ObjectFormat(), + HashFunction: dr.ContentReader().ContentFormat().Hash, + HMACSecret: dr.ContentReader().ContentFormat().HMACSecret, + Format: dr.ObjectFormat(), + SupportsContentCompression: dr.ContentReader().SupportsContentCompression(), } return rp, nil @@ -48,19 +49,21 @@ func (s *Server) handleRepoStatus(ctx context.Context, r *http.Request, body []b dr, ok := s.rep.(repo.DirectRepository) if ok { return &serverapi.StatusResponse{ - Connected: true, - ConfigFile: dr.ConfigFilename(), - Hash: dr.ContentReader().ContentFormat().Hash, - Encryption: dr.ContentReader().ContentFormat().Encryption, - MaxPackSize: dr.ContentReader().ContentFormat().MaxPackSize, - Splitter: dr.ObjectFormat().Splitter, - Storage: dr.BlobReader().ConnectionInfo().Type, - ClientOptions: dr.ClientOptions(), + Connected: true, + ConfigFile: dr.ConfigFilename(), + Hash: dr.ContentReader().ContentFormat().Hash, + Encryption: dr.ContentReader().ContentFormat().Encryption, + MaxPackSize: dr.ContentReader().ContentFormat().MaxPackSize, + Splitter: dr.ObjectFormat().Splitter, + Storage: dr.BlobReader().ConnectionInfo().Type, + ClientOptions: dr.ClientOptions(), + SupportsContentCompression: dr.ContentReader().SupportsContentCompression(), }, nil } type remoteRepository interface { APIServerURL() string + SupportsContentCompression() bool } result := &serverapi.StatusResponse{ @@ -70,6 +73,7 @@ type remoteRepository interface { if rr, ok := s.rep.(remoteRepository); ok { result.APIServerURL = rr.APIServerURL() + result.SupportsContentCompression = rr.SupportsContentCompression() } return result, nil diff --git a/internal/server/grpc_session.go b/internal/server/grpc_session.go index 1c40ad032..66435a47c 100644 --- a/internal/server/grpc_session.go +++ b/internal/server/grpc_session.go @@ -19,6 +19,7 @@ "github.com/kopia/kopia/internal/auth" "github.com/kopia/kopia/internal/grpcapi" "github.com/kopia/kopia/repo" + "github.com/kopia/kopia/repo/compression" "github.com/kopia/kopia/repo/content" "github.com/kopia/kopia/repo/manifest" "github.com/kopia/kopia/repo/object" @@ -230,7 +231,7 @@ func handleWriteContentRequest(ctx context.Context, dw repo.DirectRepositoryWrit return accessDeniedResponse() } - contentID, err := dw.ContentManager().WriteContent(ctx, req.GetData(), content.ID(req.GetPrefix())) + contentID, err := dw.ContentManager().WriteContent(ctx, req.GetData(), content.ID(req.GetPrefix()), compression.HeaderID(req.GetCompression())) if err != nil { return errorResponse(err) } @@ -420,9 +421,10 @@ func (s *Server) handleInitialSessionHandshake(srv grpcapi.KopiaRepository_Sessi Response: &grpcapi.SessionResponse_InitializeSession{ InitializeSession: &grpcapi.InitializeSessionResponse{ Parameters: &grpcapi.RepositoryParameters{ - HashFunction: dr.ContentReader().ContentFormat().Hash, - HmacSecret: dr.ContentReader().ContentFormat().HMACSecret, - Splitter: dr.ObjectFormat().Splitter, + HashFunction: dr.ContentReader().ContentFormat().Hash, + HmacSecret: dr.ContentReader().ContentFormat().HMACSecret, + Splitter: dr.ObjectFormat().Splitter, + SupportsContentCompression: dr.ContentReader().SupportsContentCompression(), }, }, }, diff --git a/internal/serverapi/serverapi.go b/internal/serverapi/serverapi.go index 21bc8e8ef..ae08c6cbf 100644 --- a/internal/serverapi/serverapi.go +++ b/internal/serverapi/serverapi.go @@ -18,14 +18,15 @@ // StatusResponse is the response of 'status' HTTP API command. type StatusResponse struct { - Connected bool `json:"connected"` - ConfigFile string `json:"configFile,omitempty"` - Hash string `json:"hash,omitempty"` - Encryption string `json:"encryption,omitempty"` - Splitter string `json:"splitter,omitempty"` - MaxPackSize int `json:"maxPackSize,omitempty"` - Storage string `json:"storage,omitempty"` - APIServerURL string `json:"apiServerURL,omitempty"` + Connected bool `json:"connected"` + ConfigFile string `json:"configFile,omitempty"` + Hash string `json:"hash,omitempty"` + Encryption string `json:"encryption,omitempty"` + Splitter string `json:"splitter,omitempty"` + MaxPackSize int `json:"maxPackSize,omitempty"` + Storage string `json:"storage,omitempty"` + APIServerURL string `json:"apiServerURL,omitempty"` + SupportsContentCompression bool `json:"supportsContentCompression"` repo.ClientOptions } diff --git a/repo/api_server_repository.go b/repo/api_server_repository.go index db90a4608..8c54a8081 100644 --- a/repo/api_server_repository.go +++ b/repo/api_server_repository.go @@ -14,6 +14,7 @@ "github.com/kopia/kopia/internal/cache" "github.com/kopia/kopia/internal/clock" "github.com/kopia/kopia/internal/remoterepoapi" + "github.com/kopia/kopia/repo/compression" "github.com/kopia/kopia/repo/content" "github.com/kopia/kopia/repo/hashing" "github.com/kopia/kopia/repo/manifest" @@ -30,12 +31,13 @@ type APIServerInfo struct { // remoteRepository is an implementation of Repository that connects to an instance of // API server hosted by `kopia server`, instead of directly manipulating files in the BLOB storage. type apiServerRepository struct { - cli *apiclient.KopiaAPIClient - h hashing.HashFunc - objectFormat object.Format - cliOpts ClientOptions - omgr *object.Manager - wso WriteSessionOptions + cli *apiclient.KopiaAPIClient + h hashing.HashFunc + objectFormat object.Format + serverSupportsContentCompression bool + cliOpts ClientOptions + omgr *object.Manager + wso WriteSessionOptions isSharedReadOnlySession bool contentCache *cache.PersistentCache @@ -136,6 +138,10 @@ func (r *apiServerRepository) Flush(ctx context.Context) error { return errors.Wrap(r.cli.Post(ctx, "flush", nil, nil), "Flush") } +func (r *apiServerRepository) SupportsContentCompression() bool { + return r.serverSupportsContentCompression +} + func (r *apiServerRepository) NewWriter(ctx context.Context, opt WriteSessionOptions) (RepositoryWriter, error) { // apiServerRepository is stateless except object manager. r2 := *r @@ -177,7 +183,7 @@ func (r *apiServerRepository) GetContent(ctx context.Context, contentID content. }) } -func (r *apiServerRepository) WriteContent(ctx context.Context, data []byte, prefix content.ID) (content.ID, error) { +func (r *apiServerRepository) WriteContent(ctx context.Context, data []byte, prefix content.ID, comp compression.HeaderID) (content.ID, error) { if err := content.ValidatePrefix(prefix); err != nil { return "", errors.Wrap(err, "invalid prefix") } @@ -194,7 +200,12 @@ func (r *apiServerRepository) WriteContent(ctx context.Context, data []byte, pre r.wso.OnUpload(int64(len(data))) - if err := r.cli.Put(ctx, "contents/"+string(contentID), data, nil); err != nil { + maybeCompression := "" + if comp != content.NoCompression { + maybeCompression = fmt.Sprintf("?compression=%x", comp) + } + + if err := r.cli.Put(ctx, "contents/"+string(contentID)+maybeCompression, data, nil); err != nil { return "", errors.Wrapf(err, "error writing content %v", contentID) } @@ -266,6 +277,7 @@ func openRestAPIRepository(ctx context.Context, si *APIServerInfo, cliOpts Clien rr.h = hf rr.objectFormat = p.Format + rr.serverSupportsContentCompression = p.SupportsContentCompression // create object manager using rr as contentManager implementation. omgr, err := object.NewObjectManager(ctx, rr, rr.objectFormat) diff --git a/repo/content/committed_read_manager.go b/repo/content/committed_read_manager.go index 68014c8ca..0a3fb44d6 100644 --- a/repo/content/committed_read_manager.go +++ b/repo/content/committed_read_manager.go @@ -1,6 +1,7 @@ package content import ( + "bytes" "context" "os" "sync" @@ -13,6 +14,7 @@ "github.com/kopia/kopia/internal/cache" "github.com/kopia/kopia/internal/clock" "github.com/kopia/kopia/repo/blob" + "github.com/kopia/kopia/repo/compression" "github.com/kopia/kopia/repo/encryption" "github.com/kopia/kopia/repo/hashing" ) @@ -218,11 +220,31 @@ func (sm *SharedManager) decryptContentAndVerify(payload []byte, bi Info) ([]byt return nil, err } + // reserved for future use + if k := bi.GetEncryptionKeyID(); k != 0 { + return nil, errors.Errorf("unsupported encryption key ID: %v", k) + } + decrypted, err := sm.decryptAndVerify(payload, iv) if err != nil { return nil, errors.Wrapf(err, "invalid checksum at %v offset %v length %v", bi.GetPackBlobID(), bi.GetPackOffset(), len(payload)) } + if h := bi.GetCompressionHeaderID(); h != 0 { + c := compression.ByHeaderID[h] + if c == nil { + return nil, errors.Errorf("unsupported compressor %x", h) + } + + out := bytes.NewBuffer(nil) + + if err := c.Decompress(out, decrypted); err != nil { + return nil, errors.Wrap(err, "error decompressing") + } + + return out.Bytes(), nil + } + return decrypted, nil } @@ -361,6 +383,15 @@ func NewSharedManager(ctx context.Context, st blob.Storage, f *FormattingOptions return nil, err } + actualIndexVersion := f.IndexVersion + if actualIndexVersion == 0 { + actualIndexVersion = DefaultIndexVersion + } + + if actualIndexVersion < v1IndexVersion || actualIndexVersion > v2IndexVersion { + return nil, errors.Errorf("index version %v is not supported", actualIndexVersion) + } + sm := &SharedManager{ st: st, encryptor: encryptor, @@ -375,8 +406,8 @@ func NewSharedManager(ctx context.Context, st blob.Storage, f *FormattingOptions repositoryFormatBytes: opts.RepositoryFormatBytes, checkInvariantsOnUnlock: os.Getenv("KOPIA_VERIFY_INVARIANTS") != "", writeFormatVersion: int32(f.Version), - encryptionBufferPool: buf.NewPool(ctx, defaultEncryptionBufferPoolSegmentSize+encryptor.Overhead(), "content-manager-encryption"), - indexVersion: v1IndexVersion, + encryptionBufferPool: buf.NewPool(ctx, defaultEncryptionBufferPoolSegmentSize+encryptor.Overhead()+maxCompressionOverheadPerContent, "content-manager-encryption"), + indexVersion: actualIndexVersion, } caching = caching.CloneOrDefault() diff --git a/repo/content/content_formatter_test.go b/repo/content/content_formatter_test.go index 1e2003973..e0c23f694 100644 --- a/repo/content/content_formatter_test.go +++ b/repo/content/content_formatter_test.go @@ -121,7 +121,7 @@ func verifyEndToEndFormatter(ctx context.Context, t *testing.T, hashAlgo, encryp } for _, b := range cases { - contentID, err := bm.WriteContent(ctx, b, "") + contentID, err := bm.WriteContent(ctx, b, "", NoCompression) if err != nil { t.Errorf("err: %v", err) } diff --git a/repo/content/content_formatting_options.go b/repo/content/content_formatting_options.go index 8e0607bd3..2852c4ecb 100644 --- a/repo/content/content_formatting_options.go +++ b/repo/content/content_formatting_options.go @@ -2,12 +2,13 @@ // FormattingOptions describes the rules for formatting contents in repository. type FormattingOptions struct { - Version int `json:"version,omitempty"` // version number, must be "1" - Hash string `json:"hash,omitempty"` // identifier of the hash algorithm used - Encryption string `json:"encryption,omitempty"` // identifier of the encryption algorithm used - HMACSecret []byte `json:"secret,omitempty"` // HMAC secret used to generate encryption keys - MasterKey []byte `json:"masterKey,omitempty"` // master encryption key (SIV-mode encryption only) - MaxPackSize int `json:"maxPackSize,omitempty"` // maximum size of a pack object + Version int `json:"version,omitempty"` // version number, must be "1" + Hash string `json:"hash,omitempty"` // identifier of the hash algorithm used + Encryption string `json:"encryption,omitempty"` // identifier of the encryption algorithm used + HMACSecret []byte `json:"secret,omitempty"` // HMAC secret used to generate encryption keys + MasterKey []byte `json:"masterKey,omitempty"` // master encryption key (SIV-mode encryption only) + MaxPackSize int `json:"maxPackSize,omitempty"` // maximum size of a pack object + IndexVersion int `json:"indexVersion,omitempty"` // force particular index format version (1,2,..) } // GetEncryptionAlgorithm implements encryption.Parameters. diff --git a/repo/content/content_manager.go b/repo/content/content_manager.go index 6be02e1cb..4e6fc67e0 100644 --- a/repo/content/content_manager.go +++ b/repo/content/content_manager.go @@ -18,6 +18,7 @@ "github.com/kopia/kopia/internal/clock" "github.com/kopia/kopia/internal/gather" "github.com/kopia/kopia/repo/blob" + "github.com/kopia/kopia/repo/compression" "github.com/kopia/kopia/repo/logging" ) @@ -31,10 +32,14 @@ PackBlobIDPrefixRegular blob.ID = "p" PackBlobIDPrefixSpecial blob.ID = "q" + NoCompression compression.HeaderID = 0 + FormatLogModule = "kopia/format" maxHashSize = 64 defaultEncryptionBufferPoolSegmentSize = 8 << 20 // 8 MB + + DefaultIndexVersion = 1 ) // PackBlobIDPrefixes contains all possible prefixes for pack blobs. @@ -208,7 +213,7 @@ func (bm *WriteManager) maybeRetryWritingFailedPacksUnlocked(ctx context.Context return nil } -func (bm *WriteManager) addToPackUnlocked(ctx context.Context, contentID ID, data []byte, isDeleted bool) error { +func (bm *WriteManager) addToPackUnlocked(ctx context.Context, contentID ID, data []byte, isDeleted bool, comp compression.HeaderID) error { // see if the current index is old enough to cause automatic flush. if err := bm.maybeFlushBasedOnTimeUnlocked(ctx); err != nil { return errors.Wrap(err, "unable to flush old pending writes") @@ -257,10 +262,12 @@ func (bm *WriteManager) addToPackUnlocked(ctx context.Context, contentID ID, dat OriginalLength: uint32(len(data)), } - if err := bm.maybeEncryptContentDataForPacking(pp.currentPackData, data, contentID); err != nil { + actualComp, err := bm.maybeCompressAndEncryptDataForPacking(pp.currentPackData, data, contentID, comp) + if err != nil { return errors.Wrapf(err, "unable to encrypt %q", contentID) } + info.CompressionHeaderID = actualComp info.PackedLength = uint32(pp.currentPackData.Length()) - info.PackOffset pp.currentPackItems[contentID] = info @@ -528,6 +535,8 @@ func (bm *WriteManager) Flush(ctx context.Context) error { } // RewriteContent causes reads and re-writes a given content using the most recent format. +// TODO(jkowalski): this will currently always re-encrypt and re-compress data, perhaps consider a +// pass-through mode that preserves encrypted/compressed bits. func (bm *WriteManager) RewriteContent(ctx context.Context, contentID ID) error { formatLog(ctx).Debugf("rewrite-content %v", contentID) @@ -541,7 +550,7 @@ func (bm *WriteManager) RewriteContent(ctx context.Context, contentID ID) error return err } - return bm.addToPackUnlocked(ctx, contentID, data, bi.GetDeleted()) + return bm.addToPackUnlocked(ctx, contentID, data, bi.GetDeleted(), bi.GetCompressionHeaderID()) } // UndeleteContent rewrites the content with the given ID if the content exists @@ -564,7 +573,7 @@ func (bm *WriteManager) UndeleteContent(ctx context.Context, contentID ID) error return err } - return bm.addToPackUnlocked(ctx, contentID, data, false) + return bm.addToPackUnlocked(ctx, contentID, data, false, bi.GetCompressionHeaderID()) } func packPrefixForContentID(contentID ID) blob.ID { @@ -609,9 +618,14 @@ func (bm *WriteManager) getOrCreatePendingPackInfoLocked(ctx context.Context, pr return bm.pendingPacks[prefix], nil } +// SupportsContentCompression returns true if content manager supports content-compression. +func (bm *WriteManager) SupportsContentCompression() bool { + return bm.format.IndexVersion >= v2IndexVersion +} + // WriteContent saves a given content of data to a pack group with a provided name and returns a contentID // that's based on the contents of data written. -func (bm *WriteManager) WriteContent(ctx context.Context, data []byte, prefix ID) (ID, error) { +func (bm *WriteManager) WriteContent(ctx context.Context, data []byte, prefix ID, comp compression.HeaderID) (ID, error) { if err := bm.maybeRetryWritingFailedPacksUnlocked(ctx); err != nil { return "", err } @@ -639,7 +653,7 @@ func (bm *WriteManager) WriteContent(ctx context.Context, data []byte, prefix ID formatLog(ctx).Debugf("write-content %v new", contentID) } - err := bm.addToPackUnlocked(ctx, contentID, data, false) + err := bm.addToPackUnlocked(ctx, contentID, data, false, comp) return contentID, err } diff --git a/repo/content/content_manager_lock_free.go b/repo/content/content_manager_lock_free.go index 1b62691d7..4ea448fc0 100644 --- a/repo/content/content_manager_lock_free.go +++ b/repo/content/content_manager_lock_free.go @@ -1,6 +1,7 @@ package content import ( + "bytes" "context" "crypto/aes" cryptorand "crypto/rand" @@ -11,18 +12,52 @@ "github.com/kopia/kopia/internal/gather" "github.com/kopia/kopia/repo/blob" + "github.com/kopia/kopia/repo/compression" "github.com/kopia/kopia/repo/encryption" "github.com/kopia/kopia/repo/hashing" ) +// maxCompressionOverheadPerContent is the maximum amount of overhead any compressor +// would need for non-compressible data of maximum size. +// The consequences of getting this wrong are not fatal - just an unnecessary memory allocation. +const maxCompressionOverheadPerContent = 16384 + const indexBlobCompactionWarningThreshold = 1000 -func (sm *SharedManager) maybeEncryptContentDataForPacking(output *gather.WriteBuffer, data []byte, contentID ID) error { +func (sm *SharedManager) maybeCompressAndEncryptDataForPacking(output *gather.WriteBuffer, data []byte, contentID ID, comp compression.HeaderID) (compression.HeaderID, error) { var hashOutput [maxHashSize]byte iv, err := getPackedContentIV(hashOutput[:], contentID) if err != nil { - return errors.Wrapf(err, "unable to get packed content IV for %q", contentID) + return NoCompression, errors.Wrapf(err, "unable to get packed content IV for %q", contentID) + } + + // nolint:nestif + if comp != NoCompression { + if sm.format.IndexVersion < v2IndexVersion { + return NoCompression, errors.Errorf("compression is not enabled for this repository.") + } + + // allocate temporary buffer to hold the compressed bytes. + tmp := sm.encryptionBufferPool.Allocate(len(data) + maxCompressionOverheadPerContent) + defer tmp.Release() + + c := compression.ByHeaderID[comp] + if c == nil { + return NoCompression, errors.Errorf("unsupported compressor %x", comp) + } + + cbuf := bytes.NewBuffer(tmp.Data[:0]) + if err = c.Compress(cbuf, data); err != nil { + return NoCompression, errors.Wrap(err, "compression error") + } + + if cd := cbuf.Bytes(); len(cd) >= len(data) { + // data was not compressible enough. + comp = NoCompression + } else { + data = cd + } } b := sm.encryptionBufferPool.Allocate(len(data) + sm.encryptor.Overhead()) @@ -30,14 +65,14 @@ func (sm *SharedManager) maybeEncryptContentDataForPacking(output *gather.WriteB cipherText, err := sm.encryptor.Encrypt(b.Data[:0], data, iv) if err != nil { - return errors.Wrap(err, "unable to encrypt") + return NoCompression, errors.Wrap(err, "unable to encrypt") } sm.Stats.encrypted(len(data)) output.Append(cipherText) - return nil + return comp, nil } func writeRandomBytesToBuffer(b *gather.WriteBuffer, count int) error { diff --git a/repo/content/content_manager_test.go b/repo/content/content_manager_test.go index b336a8bc0..2b88d4f08 100644 --- a/repo/content/content_manager_test.go +++ b/repo/content/content_manager_test.go @@ -26,6 +26,7 @@ "github.com/kopia/kopia/internal/testutil" "github.com/kopia/kopia/repo/blob" "github.com/kopia/kopia/repo/blob/logging" + "github.com/kopia/kopia/repo/compression" ) const ( @@ -253,7 +254,7 @@ func TestContentManagerWriteMultiple(t *testing.T) { for i := 0; i < repeatCount; i++ { b := seededRandomData(i, i%113) - blkID, err := bm.WriteContent(ctx, b, "") + blkID, err := bm.WriteContent(ctx, b, "", NoCompression) if err != nil { t.Errorf("err: %v", err) } @@ -324,12 +325,12 @@ func TestContentManagerFailedToWritePack(t *testing.T) { }, } - _, err = bm.WriteContent(ctx, seededRandomData(1, 10), "") + _, err = bm.WriteContent(ctx, seededRandomData(1, 10), "", NoCompression) if !errors.Is(err, sessionPutErr) { t.Fatalf("can't create first content: %v", err) } - b1, err := bm.WriteContent(ctx, seededRandomData(1, 10), "") + b1, err := bm.WriteContent(ctx, seededRandomData(1, 10), "", NoCompression) if err != nil { t.Fatalf("can't create content: %v", err) } @@ -337,7 +338,7 @@ func TestContentManagerFailedToWritePack(t *testing.T) { // advance time enough to cause auto-flush, which will fail (firstPutErr) ta.Advance(1 * time.Hour) - if _, err := bm.WriteContent(ctx, seededRandomData(2, 10), ""); !errors.Is(err, firstPutErr) { + if _, err := bm.WriteContent(ctx, seededRandomData(2, 10), "", NoCompression); !errors.Is(err, firstPutErr) { t.Fatalf("can't create 2nd content: %v", err) } @@ -1771,7 +1772,7 @@ func verifyVersionCompat(t *testing.T, writeVersion int) { data := make([]byte, i) cryptorand.Read(data) - cid, err := mgr.WriteContent(ctx, data, "") + cid, err := mgr.WriteContent(ctx, data, "", NoCompression) if err != nil { t.Fatalf("unable to write %v bytes: %v", len(data), err) } @@ -1921,6 +1922,94 @@ func verifyContentManagerDataSet(ctx context.Context, t *testing.T, mgr *WriteMa } } +func TestCompression_Disabled(t *testing.T) { + data := blobtesting.DataMap{} + st := blobtesting.NewMapStorage(data, nil, nil) + bm := newTestContentManagerWithTweaks(t, st, &contentManagerTestTweaks{ + indexVersion: v1IndexVersion, + }) + + require.False(t, bm.SupportsContentCompression()) + ctx := testlogging.Context(t) + compressibleData := bytes.Repeat([]byte{1, 2, 3, 4}, 1000) + + // with index v1 the compression is disabled + _, err := bm.WriteContent(ctx, compressibleData, "", compression.ByName["pgzip"].HeaderID()) + require.Error(t, err) +} + +func TestCompression_CompressibleData(t *testing.T) { + data := blobtesting.DataMap{} + st := blobtesting.NewMapStorage(data, nil, nil) + bm := newTestContentManagerWithTweaks(t, st, &contentManagerTestTweaks{ + indexVersion: v2IndexVersion, + }) + + require.True(t, bm.SupportsContentCompression()) + + ctx := testlogging.Context(t) + compressibleData := bytes.Repeat([]byte{1, 2, 3, 4}, 1000) + headerID := compression.ByName["gzip"].HeaderID() + + cid, err := bm.WriteContent(ctx, compressibleData, "", headerID) + require.NoError(t, err) + + ci, err := bm.ContentInfo(ctx, cid) + require.NoError(t, err) + + // gzip-compressed length + require.Equal(t, uint32(79), ci.GetPackedLength()) + require.Equal(t, uint32(len(compressibleData)), ci.GetOriginalLength()) + require.Equal(t, headerID, ci.GetCompressionHeaderID()) + + verifyContent(ctx, t, bm, cid, compressibleData) + + require.NoError(t, bm.Flush(ctx)) + verifyContent(ctx, t, bm, cid, compressibleData) + + bm2 := newTestContentManagerWithTweaks(t, st, &contentManagerTestTweaks{ + indexVersion: v2IndexVersion, + }) + verifyContent(ctx, t, bm2, cid, compressibleData) +} + +func TestCompression_NonCompressibleData(t *testing.T) { + data := blobtesting.DataMap{} + st := blobtesting.NewMapStorage(data, nil, nil) + bm := newTestContentManagerWithTweaks(t, st, &contentManagerTestTweaks{ + indexVersion: v2IndexVersion, + }) + + require.True(t, bm.SupportsContentCompression()) + + ctx := testlogging.Context(t) + nonCompressibleData := make([]byte, 65000) + headerID := compression.ByName["pgzip"].HeaderID() + + rand.Read(nonCompressibleData) + + cid, err := bm.WriteContent(ctx, nonCompressibleData, "", headerID) + require.NoError(t, err) + + verifyContent(ctx, t, bm, cid, nonCompressibleData) + + ci, err := bm.ContentInfo(ctx, cid) + require.NoError(t, err) + + // verify compression did not occur + require.True(t, ci.GetPackedLength() > ci.GetOriginalLength()) + require.Equal(t, uint32(len(nonCompressibleData)), ci.GetOriginalLength()) + require.Equal(t, NoCompression, ci.GetCompressionHeaderID()) + + require.NoError(t, bm.Flush(ctx)) + verifyContent(ctx, t, bm, cid, nonCompressibleData) + + bm2 := newTestContentManagerWithTweaks(t, st, &contentManagerTestTweaks{ + indexVersion: v2IndexVersion, + }) + verifyContent(ctx, t, bm2, cid, nonCompressibleData) +} + func newTestContentManager(t *testing.T, data blobtesting.DataMap) *WriteManager { t.Helper() @@ -1944,6 +2033,8 @@ func newTestContentManagerWithCustomTime(t *testing.T, data blobtesting.DataMap, type contentManagerTestTweaks struct { CachingOptions ManagerOptions + + indexVersion int } func newTestContentManagerWithTweaks(t *testing.T, st blob.Storage, tweaks *contentManagerTestTweaks) *WriteManager { @@ -1964,6 +2055,8 @@ func newTestContentManagerWithTweaks(t *testing.T, st blob.Storage, tweaks *cont HMACSecret: hmacSecret, MaxPackSize: maxPackSize, Version: 1, + + IndexVersion: tweaks.indexVersion, } bm, err := NewManager(ctx, st, fo, &tweaks.CachingOptions, &tweaks.ManagerOptions) @@ -2023,7 +2116,7 @@ func verifyContent(ctx context.Context, t *testing.T, bm *WriteManager, contentI func writeContentAndVerify(ctx context.Context, t *testing.T, bm *WriteManager, b []byte) ID { t.Helper() - contentID, err := bm.WriteContent(ctx, b, "") + contentID, err := bm.WriteContent(ctx, b, "", NoCompression) if err != nil { t.Errorf("err: %v", err) } @@ -2061,13 +2154,13 @@ func writeContentWithRetriesAndVerify(ctx context.Context, t *testing.T, bm *Wri log(ctx).Infof("*** starting writeContentWithRetriesAndVerify") - contentID, err := bm.WriteContent(ctx, b, "") + contentID, err := bm.WriteContent(ctx, b, "", NoCompression) for i := 0; err != nil && i < maxRetries; i++ { retryCount++ log(ctx).Infof("*** try %v", retryCount) - contentID, err = bm.WriteContent(ctx, b, "") + contentID, err = bm.WriteContent(ctx, b, "", NoCompression) } if err != nil { diff --git a/repo/content/content_reader.go b/repo/content/content_reader.go index 406bc6b0f..334e6ed71 100644 --- a/repo/content/content_reader.go +++ b/repo/content/content_reader.go @@ -6,6 +6,7 @@ // Reader defines content read API. type Reader interface { + SupportsContentCompression() bool ContentFormat() FormattingOptions GetContent(ctx context.Context, id ID) ([]byte, error) ContentInfo(ctx context.Context, id ID) (Info, error) diff --git a/repo/grpc_repository_client.go b/repo/grpc_repository_client.go index a9a3d60f8..dab928e76 100644 --- a/repo/grpc_repository_client.go +++ b/repo/grpc_repository_client.go @@ -22,6 +22,7 @@ "github.com/kopia/kopia/internal/retry" "github.com/kopia/kopia/internal/tlsutil" "github.com/kopia/kopia/repo/blob" + "github.com/kopia/kopia/repo/compression" "github.com/kopia/kopia/repo/content" "github.com/kopia/kopia/repo/hashing" "github.com/kopia/kopia/repo/manifest" @@ -55,10 +56,11 @@ type grpcRepositoryClient struct { // how many times we tried to establish inner session innerSessionAttemptCount int - h hashing.HashFunc - objectFormat object.Format - cliOpts ClientOptions - omgr *object.Manager + h hashing.HashFunc + objectFormat object.Format + serverSupportsContentCompression bool + cliOpts ClientOptions + omgr *object.Manager contentCache *cache.PersistentCache } @@ -533,7 +535,11 @@ func (r *grpcInnerSession) GetContent(ctx context.Context, contentID content.ID) return nil, errNoSessionResponse() } -func (r *grpcRepositoryClient) WriteContent(ctx context.Context, data []byte, prefix content.ID) (content.ID, error) { +func (r *grpcRepositoryClient) SupportsContentCompression() bool { + return r.serverSupportsContentCompression +} + +func (r *grpcRepositoryClient) WriteContent(ctx context.Context, data []byte, prefix content.ID, comp compression.HeaderID) (content.ID, error) { if err := content.ValidatePrefix(prefix); err != nil { return "", errors.Wrap(err, "invalid prefix") } @@ -551,7 +557,7 @@ func (r *grpcRepositoryClient) WriteContent(ctx context.Context, data []byte, pr r.opt.OnUpload(int64(len(data))) v, err := r.inSessionWithoutRetry(ctx, func(ctx context.Context, sess *grpcInnerSession) (interface{}, error) { - return sess.WriteContent(ctx, data, prefix) + return sess.WriteContent(ctx, data, prefix, comp) }) if err != nil { return "", err @@ -565,7 +571,7 @@ func (r *grpcRepositoryClient) WriteContent(ctx context.Context, data []byte, pr return v.(content.ID), nil } -func (r *grpcInnerSession) WriteContent(ctx context.Context, data []byte, prefix content.ID) (content.ID, error) { +func (r *grpcInnerSession) WriteContent(ctx context.Context, data []byte, prefix content.ID, comp compression.HeaderID) (content.ID, error) { if err := content.ValidatePrefix(prefix); err != nil { return "", errors.Wrap(err, "invalid prefix") } @@ -573,8 +579,9 @@ func (r *grpcInnerSession) WriteContent(ctx context.Context, data []byte, prefix for resp := range r.sendRequest(ctx, &apipb.SessionRequest{ Request: &apipb.SessionRequest_WriteContent{ WriteContent: &apipb.WriteContentRequest{ - Data: data, - Prefix: string(prefix), + Data: data, + Prefix: string(prefix), + Compression: uint32(comp), }, }, }) { @@ -773,6 +780,8 @@ func newGRPCAPIRepositoryForConnection(ctx context.Context, conn *grpc.ClientCon Splitter: p.Splitter, } + rr.serverSupportsContentCompression = p.SupportsContentCompression + rr.omgr, err = object.NewObjectManager(ctx, rr, rr.objectFormat) if err != nil { return nil, errors.Wrap(err, "unable to initialize object manager") diff --git a/repo/initialize.go b/repo/initialize.go index cf17808d2..7bae01f1b 100644 --- a/repo/initialize.go +++ b/repo/initialize.go @@ -89,12 +89,13 @@ func formatBlobFromOptions(opt *NewRepositoryOptions) *formatBlob { func repositoryObjectFormatFromOptions(opt *NewRepositoryOptions) *repositoryObjectFormat { f := &repositoryObjectFormat{ FormattingOptions: content.FormattingOptions{ - Version: 1, - Hash: applyDefaultString(opt.BlockFormat.Hash, hashing.DefaultAlgorithm), - Encryption: applyDefaultString(opt.BlockFormat.Encryption, encryption.DefaultAlgorithm), - HMACSecret: applyDefaultRandomBytes(opt.BlockFormat.HMACSecret, hmacSecretLength), - MasterKey: applyDefaultRandomBytes(opt.BlockFormat.MasterKey, masterKeyLength), - MaxPackSize: applyDefaultInt(opt.BlockFormat.MaxPackSize, 20<<20), //nolint:gomnd + Version: 1, + Hash: applyDefaultString(opt.BlockFormat.Hash, hashing.DefaultAlgorithm), + Encryption: applyDefaultString(opt.BlockFormat.Encryption, encryption.DefaultAlgorithm), + HMACSecret: applyDefaultRandomBytes(opt.BlockFormat.HMACSecret, hmacSecretLength), + MasterKey: applyDefaultRandomBytes(opt.BlockFormat.MasterKey, masterKeyLength), + MaxPackSize: applyDefaultInt(opt.BlockFormat.MaxPackSize, 20<<20), //nolint:gomnd + IndexVersion: applyDefaultInt(opt.BlockFormat.IndexVersion, content.DefaultIndexVersion), }, Format: object.Format{ Splitter: applyDefaultString(opt.ObjectFormat.Splitter, splitter.DefaultAlgorithm), diff --git a/repo/manifest/committed_manifest_manager.go b/repo/manifest/committed_manifest_manager.go index 4ca4ea424..c99d3fa3d 100644 --- a/repo/manifest/committed_manifest_manager.go +++ b/repo/manifest/committed_manifest_manager.go @@ -76,7 +76,7 @@ func (m *committedManifestManager) writeEntriesLocked(ctx context.Context, entri mustSucceed(gz.Flush()) mustSucceed(gz.Close()) - contentID, err := m.b.WriteContent(ctx, buf.Bytes(), ContentPrefix) + contentID, err := m.b.WriteContent(ctx, buf.Bytes(), ContentPrefix, content.NoCompression) if err != nil { return nil, errors.Wrap(err, "unable to write content") } diff --git a/repo/manifest/manifest_manager.go b/repo/manifest/manifest_manager.go index 4a849d4e5..3c45c6afb 100644 --- a/repo/manifest/manifest_manager.go +++ b/repo/manifest/manifest_manager.go @@ -13,6 +13,7 @@ "github.com/pkg/errors" "github.com/kopia/kopia/internal/clock" + "github.com/kopia/kopia/repo/compression" "github.com/kopia/kopia/repo/content" "github.com/kopia/kopia/repo/logging" ) @@ -36,7 +37,7 @@ type contentManager interface { Revision() int64 GetContent(ctx context.Context, contentID content.ID) ([]byte, error) - WriteContent(ctx context.Context, data []byte, prefix content.ID) (content.ID, error) + WriteContent(ctx context.Context, data []byte, prefix content.ID, comp compression.HeaderID) (content.ID, error) DeleteContent(ctx context.Context, contentID content.ID) error IterateContents(ctx context.Context, options content.IterateOptions, callback content.IterateCallback) error DisableIndexFlush(ctx context.Context) diff --git a/repo/object/object_manager.go b/repo/object/object_manager.go index 380b533c7..5d6793ed2 100644 --- a/repo/object/object_manager.go +++ b/repo/object/object_manager.go @@ -34,7 +34,8 @@ type contentReader interface { type contentManager interface { contentReader - WriteContent(ctx context.Context, data []byte, prefix content.ID) (content.ID, error) + SupportsContentCompression() bool + WriteContent(ctx context.Context, data []byte, prefix content.ID, comp compression.HeaderID) (content.ID, error) } // Format describes the format of objects in a repository. diff --git a/repo/object/object_manager_test.go b/repo/object/object_manager_test.go index 0253f243e..9f7bfa4ed 100644 --- a/repo/object/object_manager_test.go +++ b/repo/object/object_manager_test.go @@ -17,6 +17,7 @@ "testing" "github.com/pkg/errors" + "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" "github.com/kopia/kopia/internal/clock" @@ -29,8 +30,10 @@ ) type fakeContentManager struct { - mu sync.Mutex - data map[content.ID][]byte + mu sync.Mutex + data map[content.ID][]byte + compresionIDs map[content.ID]compression.HeaderID + supportsContentCompression bool } func (f *fakeContentManager) GetContent(ctx context.Context, contentID content.ID) ([]byte, error) { @@ -44,7 +47,7 @@ func (f *fakeContentManager) GetContent(ctx context.Context, contentID content.I return nil, content.ErrContentNotFound } -func (f *fakeContentManager) WriteContent(ctx context.Context, data []byte, prefix content.ID) (content.ID, error) { +func (f *fakeContentManager) WriteContent(ctx context.Context, data []byte, prefix content.ID, comp compression.HeaderID) (content.ID, error) { h := sha256.New() h.Write(data) contentID := prefix + content.ID(hex.EncodeToString(h.Sum(nil))) @@ -53,10 +56,17 @@ func (f *fakeContentManager) WriteContent(ctx context.Context, data []byte, pref defer f.mu.Unlock() f.data[contentID] = append([]byte(nil), data...) + if f.compresionIDs != nil { + f.compresionIDs[contentID] = comp + } return contentID, nil } +func (f *fakeContentManager) SupportsContentCompression() bool { + return f.supportsContentCompression +} + func (f *fakeContentManager) ContentInfo(ctx context.Context, contentID content.ID) (content.Info, error) { f.mu.Lock() defer f.mu.Unlock() @@ -72,16 +82,16 @@ func (f *fakeContentManager) Flush(ctx context.Context) error { return nil } -func setupTest(t *testing.T) (map[content.ID][]byte, *Manager) { +func setupTest(t *testing.T, compressionHeaderID map[content.ID]compression.HeaderID) (map[content.ID][]byte, *Manager) { t.Helper() - return setupTestWithData(t, map[content.ID][]byte{}) -} + data := map[content.ID][]byte{} -func setupTestWithData(t *testing.T, data map[content.ID][]byte) (map[content.ID][]byte, *Manager) { - t.Helper() - - r, err := NewObjectManager(testlogging.Context(t), &fakeContentManager{data: data}, Format{ + r, err := NewObjectManager(testlogging.Context(t), &fakeContentManager{ + data: data, + supportsContentCompression: compressionHeaderID != nil, + compresionIDs: compressionHeaderID, + }, Format{ Splitter: "FIXED-1M", }) if err != nil { @@ -109,7 +119,7 @@ func TestWriters(t *testing.T) { } for _, c := range cases { - data, om := setupTest(t) + data, om := setupTest(t, nil) writer := om.NewWriter(ctx, WriterOptions{}) @@ -144,9 +154,46 @@ func objectIDsEqual(o1, o2 ID) bool { return o1 == o2 } +func TestCompression_ContentCompressionEnabled(t *testing.T) { + ctx := testlogging.Context(t) + + cmap := map[content.ID]compression.HeaderID{} + _, om := setupTest(t, cmap) + + w := om.NewWriter(ctx, WriterOptions{ + Compressor: "gzip", + }) + w.Write(bytes.Repeat([]byte{1, 2, 3, 4}, 1000)) + oid, err := w.Result() + require.NoError(t, err) + + cid, isCompressed, ok := oid.ContentID() + require.True(t, ok) + require.False(t, isCompressed) // oid will not indicate compression + require.Equal(t, compression.ByName["gzip"].HeaderID(), cmap[cid]) +} + +func TestCompression_ContentCompressionDisabled(t *testing.T) { + ctx := testlogging.Context(t) + + // this disables content compression + _, om := setupTest(t, nil) + + w := om.NewWriter(ctx, WriterOptions{ + Compressor: "gzip", + }) + w.Write(bytes.Repeat([]byte{1, 2, 3, 4}, 1000)) + oid, err := w.Result() + require.NoError(t, err) + + _, isCompressed, ok := oid.ContentID() + require.True(t, ok) + require.True(t, isCompressed) // oid will indicate compression +} + func TestWriterCompleteChunkInTwoWrites(t *testing.T) { ctx := testlogging.Context(t) - _, om := setupTest(t) + _, om := setupTest(t, nil) b := make([]byte, 100) writer := om.NewWriter(ctx, WriterOptions{}) @@ -161,7 +208,7 @@ func TestWriterCompleteChunkInTwoWrites(t *testing.T) { func TestCheckpointing(t *testing.T) { ctx := testlogging.Context(t) - _, om := setupTest(t) + _, om := setupTest(t, nil) writer := om.NewWriter(ctx, WriterOptions{}) @@ -348,7 +395,7 @@ func TestIndirection(t *testing.T) { } for _, c := range cases { - data, om := setupTest(t) + data, om := setupTest(t, nil) contentBytes := make([]byte, c.dataLength) @@ -400,7 +447,7 @@ func TestHMAC(t *testing.T) { ctx := testlogging.Context(t) c := bytes.Repeat([]byte{0xcd}, 50) - _, om := setupTest(t) + _, om := setupTest(t, nil) w := om.NewWriter(ctx, WriterOptions{}) w.Write(c) @@ -414,7 +461,7 @@ func TestHMAC(t *testing.T) { // nolint:gocyclo func TestConcatenate(t *testing.T) { ctx := testlogging.Context(t) - _, om := setupTest(t) + _, om := setupTest(t, nil) phrase := []byte("hello world\n") phraseLength := len(phrase) @@ -549,7 +596,7 @@ func mustWriteObject(t *testing.T, om *Manager, data []byte, compressor compress func TestReader(t *testing.T) { ctx := testlogging.Context(t) - data, om := setupTest(t) + data, om := setupTest(t, nil) storedPayload := []byte("foo\nbar") data["a76999788386641a3ec798554f1fe7e6"] = storedPayload @@ -589,7 +636,7 @@ func TestReader(t *testing.T) { func TestReaderStoredBlockNotFound(t *testing.T) { ctx := testlogging.Context(t) - _, om := setupTest(t) + _, om := setupTest(t, nil) objectID, err := ParseID("deadbeef") if err != nil { @@ -610,7 +657,7 @@ func TestEndToEndReadAndSeek(t *testing.T) { t.Parallel() ctx := testlogging.Context(t) - _, om := setupTest(t) + _, om := setupTest(t, nil) for _, size := range []int{1, 199, 200, 201, 9999, 512434, 5012434} { // Create some random data sample of the specified size. @@ -663,7 +710,7 @@ func TestEndToEndReadAndSeekWithCompression(t *testing.T) { totalBytesWritten := 0 - data, om := setupTest(t) + data, om := setupTest(t, nil) for _, size := range sizes { var inputData []byte @@ -762,7 +809,7 @@ func verify(ctx context.Context, t *testing.T, cr contentReader, objectID ID, ex // nolint:gocyclo func TestSeek(t *testing.T) { ctx := testlogging.Context(t) - _, om := setupTest(t) + _, om := setupTest(t, nil) for _, size := range []int{0, 1, 500000, 15000000} { randomData := make([]byte, size) diff --git a/repo/object/object_writer.go b/repo/object/object_writer.go index b95f341a8..192e1ea38 100644 --- a/repo/object/object_writer.go +++ b/repo/object/object_writer.go @@ -189,13 +189,22 @@ func (w *objectWriter) prepareAndWriteContentChunk(chunkID int, data []byte) err b := w.om.bufferPool.Allocate(len(data) + maxCompressionOverheadPerSegment) defer b.Release() + comp := content.NoCompression + objectComp := w.compressor + + // do not compress in this layer, instead pass comp to the content manager. + if w.om.contentMgr.SupportsContentCompression() && w.compressor != nil { + comp = w.compressor.HeaderID() + objectComp = nil + } + // contentBytes is what we're going to write to the content manager, it potentially uses bytes from b - contentBytes, isCompressed, err := maybeCompressedContentBytes(w.compressor, bytes.NewBuffer(b.Data[:0]), data) + contentBytes, isCompressed, err := maybeCompressedContentBytes(objectComp, bytes.NewBuffer(b.Data[:0]), data) if err != nil { return errors.Wrap(err, "unable to prepare content bytes") } - contentID, err := w.om.contentMgr.WriteContent(w.ctx, contentBytes, w.prefix) + contentID, err := w.om.contentMgr.WriteContent(w.ctx, contentBytes, w.prefix, comp) if err != nil { return errors.Wrapf(err, "unable to write content chunk %v of %v: %v", chunkID, w.description, err) } diff --git a/tests/end_to_end_test/compression_test.go b/tests/end_to_end_test/compression_test.go index 9d06298f0..323274a52 100644 --- a/tests/end_to_end_test/compression_test.go +++ b/tests/end_to_end_test/compression_test.go @@ -56,11 +56,43 @@ func TestCompression(t *testing.T) { oid := sources[0].Snapshots[0].ObjectID entries := clitestutil.ListDirectory(t, e, oid) - if !strings.HasPrefix(entries[0].ObjectID, "Z") { - t.Errorf("expected compressed object, got %v", entries[0].ObjectID) + supportsContentLevelCompression := containsLineStartingWith( + e.RunAndExpectSuccess(t, "repo", "status"), + "Content compression: true", + ) + + // without content-level compression, we'll do it at object level and object ID will be prefixed with 'Z' + if !supportsContentLevelCompression { + if !strings.HasPrefix(entries[0].ObjectID, "Z") { + t.Errorf("expected compressed object, got %v", entries[0].ObjectID) + } + } else { + // with content-level compression we're looking for a content with compression. + lines := e.RunAndExpectSuccess(t, "content", "ls", "-c") + found := false + + for _, l := range lines { + if strings.HasPrefix(l, entries[0].ObjectID) { + require.Contains(t, l, "pgzip") + found = true + break + } + } + + require.True(t, found) } if lines := e.RunAndExpectSuccess(t, "show", entries[0].ObjectID); !reflect.DeepEqual(dataLines, lines) { t.Errorf("invalid object contents") } } + +func containsLineStartingWith(lines []string, prefix string) bool { + for _, l := range lines { + if strings.HasPrefix(l, prefix) { + return true + } + } + + return false +} diff --git a/tests/end_to_end_test/content_info_test.go b/tests/end_to_end_test/content_info_test.go new file mode 100644 index 000000000..45ba5925b --- /dev/null +++ b/tests/end_to_end_test/content_info_test.go @@ -0,0 +1,68 @@ +package endtoend_test + +import ( + "bytes" + "io/ioutil" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/kopia/kopia/internal/testutil" + "github.com/kopia/kopia/snapshot" + "github.com/kopia/kopia/tests/testenv" +) + +func TestContentListAndStats_Indexv1(t *testing.T) { + t.Parallel() + testContentListAndStats(t, "1") +} + +func TestContentListAndStats_Indexv2(t *testing.T) { + t.Parallel() + testContentListAndStats(t, "2") +} + +// nolint:thelper +func testContentListAndStats(t *testing.T, indexVersion string) { + runner := testenv.NewInProcRunner(t) + e := testenv.NewCLITest(t, runner) + + e.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", e.RepoDir, "--index-version", indexVersion) + + require.Empty(t, e.RunAndExpectSuccess(t, "content", "list", "--deleted-only")) + e.RunAndExpectSuccess(t, "policy", "set", "--global", "--compression", "pgzip") + + srcDir := testutil.TempDirectory(t) + ioutil.WriteFile(filepath.Join(srcDir, "compressible.txt"), + bytes.Repeat([]byte{1, 2, 3, 4}, 1000), + 0o600, + ) + + var man snapshot.Manifest + + testutil.MustParseJSONLines(t, e.RunAndExpectSuccess(t, "snapshot", "create", srcDir, "--json"), &man) + contentID := string(man.RootObjectID()) + + require.True(t, containsLineStartingWith(e.RunAndExpectSuccess(t, "content", "list"), contentID)) + require.True(t, containsLineStartingWith(e.RunAndExpectSuccess(t, "content", "list", "-l"), contentID)) + require.True(t, containsLineStartingWith(e.RunAndExpectSuccess(t, "content", "list", "-c"), contentID)) + require.True(t, containsLineStartingWith(e.RunAndExpectSuccess(t, "content", "list", "--summary"), "Total: ")) + + e.RunAndExpectSuccess(t, "content", "stats") + + // sleep a bit to ensure at least one second passes, otherwise delete may end up happen on the same + // second as create, in which case creation will prevail. + time.Sleep(time.Second) + + e.RunAndExpectSuccess(t, "content", "delete", contentID) + + require.False(t, containsLineStartingWith(e.RunAndExpectSuccess(t, "content", "list"), contentID)) + require.False(t, containsLineStartingWith(e.RunAndExpectSuccess(t, "content", "list", "-l"), contentID)) + require.False(t, containsLineStartingWith(e.RunAndExpectSuccess(t, "content", "list", "-c"), contentID)) + + require.True(t, containsLineStartingWith(e.RunAndExpectSuccess(t, "content", "list", "--deleted"), contentID)) + require.True(t, containsLineStartingWith(e.RunAndExpectSuccess(t, "content", "list", "--deleted", "-l"), contentID)) + require.True(t, containsLineStartingWith(e.RunAndExpectSuccess(t, "content", "list", "--deleted", "-c"), contentID)) +} diff --git a/tests/repository_stress_test/repository_stress_test.go b/tests/repository_stress_test/repository_stress_test.go index 04f0cc4bf..82bf5194f 100644 --- a/tests/repository_stress_test/repository_stress_test.go +++ b/tests/repository_stress_test/repository_stress_test.go @@ -233,7 +233,7 @@ func writeRandomBlock(ctx context.Context, t *testing.T, r repo.DirectRepository data := make([]byte, 1000) cryptorand.Read(data) - contentID, err := r.ContentManager().WriteContent(ctx, data, "") + contentID, err := r.ContentManager().WriteContent(ctx, data, "", content.NoCompression) if err == nil { knownBlocksMutex.Lock() if len(knownBlocks) >= 1000 { diff --git a/tests/stress_test/stress_test.go b/tests/stress_test/stress_test.go index 5511c5949..65709dbd0 100644 --- a/tests/stress_test/stress_test.go +++ b/tests/stress_test/stress_test.go @@ -95,7 +95,7 @@ type writtenBlock struct { dataCopy := append([]byte{}, data...) - contentID, err := bm.WriteContent(ctx, data, "") + contentID, err := bm.WriteContent(ctx, data, "", content.NoCompression) if err != nil { t.Errorf("err: %v", err) return diff --git a/tests/testenv/cli_inproc_runner.go b/tests/testenv/cli_inproc_runner.go index b122a6c50..d922a5708 100644 --- a/tests/testenv/cli_inproc_runner.go +++ b/tests/testenv/cli_inproc_runner.go @@ -31,7 +31,7 @@ func (e *CLIInProcRunner) Start(t *testing.T, args []string) (stdout, stderr io. func NewInProcRunner(t *testing.T) *CLIInProcRunner { t.Helper() - if os.Getenv("KOPIA_EXE") != "" { + if os.Getenv("KOPIA_EXE") != "" && os.Getenv("KOPIA_RUN_ALL_INTEGRATION_TESTS") == "" { t.Skip("not running test since it's also included in the unit tests") }