| // Licensed to the Apache Software Foundation (ASF) under one or more |
| // contributor license agreements. See the NOTICE file distributed with |
| // this work for additional information regarding copyright ownership. |
| // The ASF licenses this file to You under the Apache License, Version 2.0 |
| // (the "License"); you may not use this file except in compliance with |
| // the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package storage |
| |
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| "io" |
| "strings" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/command" |
| "github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/diff" |
| ) |
| |
| // DiffOption is an option for Diff. |
| type DiffOption func(*diffOptions) |
| |
| // DiffWithSuppressCommands returns a new DiffOption that suppresses printing of commands. |
| func DiffWithSuppressCommands() DiffOption { |
| return func(diffOptions *diffOptions) { |
| diffOptions.suppressCommands = true |
| } |
| } |
| |
| // DiffWithSuppressCommands returns a new DiffOption that suppresses printing of timestamps. |
| func DiffWithSuppressTimestamps() DiffOption { |
| return func(diffOptions *diffOptions) { |
| diffOptions.suppressTimestamps = true |
| } |
| } |
| |
| // DiffWithExternalPaths returns a new DiffOption that prints diffs with external paths |
| // instead of paths. |
| func DiffWithExternalPaths() DiffOption { |
| return func(diffOptions *diffOptions) { |
| diffOptions.externalPaths = true |
| } |
| } |
| |
| // DiffWithExternalPathPrefixes returns a new DiffOption that sets the external path prefixes for the buckets. |
| // |
| // If a file is in one bucket but not the other, it will be assumed that the file begins |
| // with the given prefix, and this prefix should be substituted for the other prefix. |
| // |
| // For example, if diffing the directories "test/a" and "test/b", use "test/a/" and "test/b/", |
| // and a file that is in one with path "test/a/foo.txt" will be shown as not |
| // existing as "test/b/foo.txt" in two. |
| // |
| // Note that the prefixes are directly concatenated, so "/" should be included generally. |
| // |
| // This option has no effect if DiffWithExternalPaths is not set. |
| // This option is not required if the prefixes are equal. |
| func DiffWithExternalPathPrefixes( |
| oneExternalPathPrefix string, |
| twoExternalPathPrefix string, |
| ) DiffOption { |
| return func(diffOptions *diffOptions) { |
| if oneExternalPathPrefix != twoExternalPathPrefix { |
| // we don't know if external paths are file paths or not |
| // so we just operate on pure string-prefix paths |
| // this comes up with for example s3:// |
| diffOptions.oneExternalPathPrefix = oneExternalPathPrefix |
| diffOptions.twoExternalPathPrefix = twoExternalPathPrefix |
| } |
| } |
| } |
| |
| // DiffWithTransform returns a DiffOption that adds a transform function. The transform function will be run on each |
| // file being compared before it is diffed. transform takes the arguments: |
| // |
| // side: one or two whether it is the first or second item in the diff |
| // filename: the filename including path |
| // content: the file content. |
| // |
| // transform returns a string that is the transformed content of filename. |
| // |
| // TODO: this needs to be refactored or removed, especially the implicit side enum. |
| // Perhaps provide a transform function for a given bucket and apply it there. |
| func DiffWithTransform( |
| transform func(side string, filename string, content []byte) []byte, |
| ) DiffOption { |
| return func(diffOptions *diffOptions) { |
| diffOptions.transforms = append(diffOptions.transforms, transform) |
| } |
| } |
| |
| // DiffBytes does a diff of the ReadBuckets. |
| func DiffBytes( |
| ctx context.Context, |
| runner command.Runner, |
| one ReadBucket, |
| two ReadBucket, |
| options ...DiffOption, |
| ) ([]byte, error) { |
| buffer := bytes.NewBuffer(nil) |
| if err := Diff(ctx, runner, buffer, one, two, options...); err != nil { |
| return nil, err |
| } |
| return buffer.Bytes(), nil |
| } |
| |
| // Diff writes a diff of the ReadBuckets to the Writer. |
| func Diff( |
| ctx context.Context, |
| runner command.Runner, |
| writer io.Writer, |
| one ReadBucket, |
| two ReadBucket, |
| options ...DiffOption, |
| ) error { |
| diffOptions := newDiffOptions() |
| for _, option := range options { |
| option(diffOptions) |
| } |
| externalPaths := diffOptions.externalPaths |
| oneExternalPathPrefix := diffOptions.oneExternalPathPrefix |
| twoExternalPathPrefix := diffOptions.twoExternalPathPrefix |
| |
| oneObjectInfos, err := allObjectInfos(ctx, one, "") |
| if err != nil { |
| return err |
| } |
| twoObjectInfos, err := allObjectInfos(ctx, two, "") |
| if err != nil { |
| return err |
| } |
| sortObjectInfos(oneObjectInfos) |
| sortObjectInfos(twoObjectInfos) |
| onePathToObjectInfo := pathToObjectInfo(oneObjectInfos) |
| twoPathToObjectInfo := pathToObjectInfo(twoObjectInfos) |
| |
| for _, oneObjectInfo := range oneObjectInfos { |
| path := oneObjectInfo.Path() |
| oneDiffPath, err := getDiffPathForObjectInfo( |
| oneObjectInfo, |
| externalPaths, |
| oneExternalPathPrefix, |
| ) |
| if err != nil { |
| return err |
| } |
| oneData, err := ReadPath(ctx, one, path) |
| if err != nil { |
| return err |
| } |
| var twoData []byte |
| var twoDiffPath string |
| if twoObjectInfo, ok := twoPathToObjectInfo[path]; ok { |
| twoData, err = ReadPath(ctx, two, path) |
| if err != nil { |
| return err |
| } |
| twoDiffPath, err = getDiffPathForObjectInfo( |
| twoObjectInfo, |
| externalPaths, |
| twoExternalPathPrefix, |
| ) |
| if err != nil { |
| return err |
| } |
| } else { |
| twoDiffPath, err = getDiffPathForNotFound( |
| oneObjectInfo, |
| externalPaths, |
| oneExternalPathPrefix, |
| twoExternalPathPrefix, |
| ) |
| if err != nil { |
| return err |
| } |
| } |
| for _, transform := range diffOptions.transforms { |
| oneData = transform("one", oneDiffPath, oneData) |
| twoData = transform("two", twoDiffPath, twoData) |
| } |
| diffData, err := diff.Diff( |
| ctx, |
| runner, |
| oneData, |
| twoData, |
| oneDiffPath, |
| twoDiffPath, |
| diffOptions.toDiffPackageOptions()..., |
| ) |
| if err != nil { |
| return err |
| } |
| if len(diffData) > 0 { |
| if _, err := writer.Write(diffData); err != nil { |
| return err |
| } |
| } |
| } |
| for _, twoObjectInfo := range twoObjectInfos { |
| path := twoObjectInfo.Path() |
| if _, ok := onePathToObjectInfo[path]; !ok { |
| twoData, err := ReadPath(ctx, two, path) |
| if err != nil { |
| return err |
| } |
| oneDiffPath, err := getDiffPathForNotFound( |
| twoObjectInfo, |
| externalPaths, |
| twoExternalPathPrefix, |
| oneExternalPathPrefix, |
| ) |
| if err != nil { |
| return err |
| } |
| twoDiffPath, err := getDiffPathForObjectInfo( |
| twoObjectInfo, |
| externalPaths, |
| twoExternalPathPrefix, |
| ) |
| if err != nil { |
| return err |
| } |
| diffData, err := diff.Diff( |
| ctx, |
| runner, |
| nil, |
| twoData, |
| oneDiffPath, |
| twoDiffPath, |
| diffOptions.toDiffPackageOptions()..., |
| ) |
| if err != nil { |
| return err |
| } |
| if len(diffData) > 0 { |
| if _, err := writer.Write(diffData); err != nil { |
| return err |
| } |
| } |
| } |
| } |
| return nil |
| } |
| |
| func getDiffPathForObjectInfo( |
| objectInfo ObjectInfo, |
| externalPaths bool, |
| externalPathPrefix string, |
| ) (string, error) { |
| if !externalPaths { |
| return objectInfo.Path(), nil |
| } |
| externalPath := objectInfo.ExternalPath() |
| if externalPathPrefix == "" { |
| return externalPath, nil |
| } |
| if !strings.HasPrefix(externalPath, externalPathPrefix) { |
| return "", fmt.Errorf("diff: expected %s to have prefix %s", externalPath, externalPathPrefix) |
| } |
| return externalPath, nil |
| } |
| |
| func getDiffPathForNotFound( |
| foundObjectInfo ObjectInfo, |
| externalPaths bool, |
| foundExternalPathPrefix string, |
| notFoundExternalPathPrefix string, |
| ) (string, error) { |
| if !externalPaths { |
| return foundObjectInfo.Path(), nil |
| } |
| externalPath := foundObjectInfo.ExternalPath() |
| switch { |
| case foundExternalPathPrefix == "" && notFoundExternalPathPrefix == "": |
| // no prefix, just return external path |
| return externalPath, nil |
| case foundExternalPathPrefix == "" && notFoundExternalPathPrefix != "": |
| // the not-found side has a prefix, append the external path to this prefix, and we're done |
| return notFoundExternalPathPrefix + externalPath, nil |
| default: |
| // foundExternalPathPrefix != "" && notFoundExternalPathPrefix == "" |
| // foundExternalPathPrefix != "" && notFoundExternalPathPrefix != "" |
| if !strings.HasPrefix(externalPath, foundExternalPathPrefix) { |
| return "", fmt.Errorf("diff: expected %s to have prefix %s", externalPath, foundExternalPathPrefix) |
| } |
| return notFoundExternalPathPrefix + strings.TrimPrefix(externalPath, foundExternalPathPrefix), nil |
| } |
| } |
| |
| type diffOptions struct { |
| suppressCommands bool |
| suppressTimestamps bool |
| externalPaths bool |
| oneExternalPathPrefix string |
| twoExternalPathPrefix string |
| transforms []func(side string, filename string, content []byte) []byte |
| } |
| |
| func newDiffOptions() *diffOptions { |
| return &diffOptions{} |
| } |
| |
| func (d *diffOptions) toDiffPackageOptions() []diff.DiffOption { |
| var diffPackageOptions []diff.DiffOption |
| if d.suppressCommands { |
| diffPackageOptions = append(diffPackageOptions, diff.DiffWithSuppressCommands()) |
| } |
| if d.suppressTimestamps { |
| diffPackageOptions = append(diffPackageOptions, diff.DiffWithSuppressTimestamps()) |
| } |
| return diffPackageOptions |
| } |