mirror of
https://github.com/kopia/kopia.git
synced 2026-01-25 06:48:48 -05:00
237 lines
5.8 KiB
Go
237 lines
5.8 KiB
Go
package diff
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
|
|
"github.com/kopia/kopia/fs"
|
|
"github.com/kopia/kopia/internal/kopialogging"
|
|
"github.com/kopia/repo"
|
|
"github.com/kopia/repo/object"
|
|
)
|
|
|
|
var log = kopialogging.Logger("diff")
|
|
|
|
// Comparer outputs diff information between two filesystems.
|
|
type Comparer struct {
|
|
rep *repo.Repository
|
|
out io.Writer
|
|
tmpDir string
|
|
|
|
DiffCommand string
|
|
DiffArguments []string
|
|
}
|
|
|
|
// Compare compares two filesystem entries and emits their diff information.
|
|
func (c *Comparer) Compare(ctx context.Context, e1, e2 fs.Entry) error {
|
|
return c.compareEntry(ctx, e1, e2, ".")
|
|
}
|
|
|
|
// Close removes all temporary files used by the comparer.
|
|
func (c *Comparer) Close() error {
|
|
return os.RemoveAll(c.tmpDir)
|
|
}
|
|
|
|
func (c *Comparer) compareDirectories(ctx context.Context, dir1, dir2 fs.Directory, parent string) error {
|
|
log.Debugf("comparing directories %v", parent)
|
|
var entries1, entries2 fs.Entries
|
|
var err error
|
|
|
|
if dir1 != nil {
|
|
entries1, err = dir1.Readdir(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to read first directory %v: %v", parent, err)
|
|
}
|
|
}
|
|
|
|
if dir2 != nil {
|
|
entries2, err = dir2.Readdir(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to read second directory %v: %v", parent, err)
|
|
}
|
|
}
|
|
|
|
return c.compareDirectoryEntries(ctx, entries1, entries2, parent)
|
|
}
|
|
|
|
// nolint:gocyclo
|
|
func (c *Comparer) compareEntry(ctx context.Context, e1, e2 fs.Entry, path string) error {
|
|
// see if we have the same object IDs, which implies identical objects, thanks to content-addressable-storage
|
|
if h1, ok := e1.(object.HasObjectID); ok {
|
|
if h2, ok := e2.(object.HasObjectID); ok {
|
|
if h1.ObjectID() == h2.ObjectID() {
|
|
log.Debugf("unchanged %v", path)
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
if e1 == nil {
|
|
if dir2, isDir2 := e2.(fs.Directory); isDir2 {
|
|
c.output("added directory %v\n", path)
|
|
return c.compareDirectories(ctx, nil, dir2, path)
|
|
}
|
|
|
|
c.output("added file %v (%v bytes)\n", path, e2.Size())
|
|
if f, ok := e2.(fs.File); ok {
|
|
if err := c.compareFiles(ctx, nil, f, path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if e2 == nil {
|
|
if dir1, isDir1 := e1.(fs.Directory); isDir1 {
|
|
c.output("removed directory %v\n", path)
|
|
return c.compareDirectories(ctx, dir1, nil, path)
|
|
}
|
|
|
|
c.output("removed file %v (%v bytes)\n", path, e1.Size())
|
|
if f, ok := e1.(fs.File); ok {
|
|
if err := c.compareFiles(ctx, f, nil, path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
dir1, isDir1 := e1.(fs.Directory)
|
|
dir2, isDir2 := e2.(fs.Directory)
|
|
if isDir1 {
|
|
if !isDir2 {
|
|
// right is a non-directory, left is a directory
|
|
c.output("changed %v from directory to non-directory\n", path)
|
|
return nil
|
|
}
|
|
|
|
return c.compareDirectories(ctx, dir1, dir2, path)
|
|
}
|
|
|
|
if isDir2 {
|
|
// left is non-directory, right is a directory
|
|
log.Infof("changed %v from non-directory to a directory", path)
|
|
return nil
|
|
}
|
|
|
|
c.output("changed %v at %v (size %v -> %v)\n", path, e2.ModTime().String(), e1.Size(), e2.Size())
|
|
if f1, ok := e1.(fs.File); ok {
|
|
if f2, ok := e2.(fs.File); ok {
|
|
if err := c.compareFiles(ctx, f1, f2, path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Comparer) compareDirectoryEntries(ctx context.Context, entries1, entries2 fs.Entries, dirPath string) error {
|
|
e1byname := map[string]fs.Entry{}
|
|
for _, e1 := range entries1 {
|
|
e1byname[e1.Name()] = e1
|
|
}
|
|
|
|
for _, e2 := range entries2 {
|
|
entryName := e2.Name()
|
|
if err := c.compareEntry(ctx, e1byname[entryName], e2, dirPath+"/"+entryName); err != nil {
|
|
return fmt.Errorf("error comparing %v: %v", entryName, err)
|
|
}
|
|
delete(e1byname, entryName)
|
|
}
|
|
|
|
// at this point e1byname only has entries present in entries1 but not entries2, those are the deleted ones
|
|
for _, e1 := range entries1 {
|
|
entryName := e1.Name()
|
|
if _, ok := e1byname[entryName]; ok {
|
|
if err := c.compareEntry(ctx, e1, nil, dirPath+"/"+entryName); err != nil {
|
|
return fmt.Errorf("error comparing %v: %v", entryName, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Comparer) compareFiles(ctx context.Context, f1, f2 fs.File, fname string) error {
|
|
if c.DiffCommand == "" {
|
|
return nil
|
|
}
|
|
|
|
oldName := "/dev/null"
|
|
newName := "/dev/null"
|
|
|
|
if f1 != nil {
|
|
oldName = filepath.Clean("old/" + fname)
|
|
oldFile := filepath.Join(c.tmpDir, oldName)
|
|
|
|
if err := c.downloadFile(ctx, f1, oldFile); err != nil {
|
|
return fmt.Errorf("error downloading old file: %v", err)
|
|
}
|
|
|
|
defer os.Remove(oldFile) //nolint:errcheck
|
|
}
|
|
|
|
if f2 != nil {
|
|
newName = filepath.Clean("new/" + fname)
|
|
newFile := filepath.Join(c.tmpDir, newName)
|
|
|
|
if err := c.downloadFile(ctx, f2, newFile); err != nil {
|
|
return fmt.Errorf("error downloading new file: %v", err)
|
|
}
|
|
defer os.Remove(newFile) //nolint:errcheck
|
|
}
|
|
|
|
var args []string
|
|
args = append(args, c.DiffArguments...)
|
|
args = append(args, oldName)
|
|
args = append(args, newName)
|
|
|
|
cmd := exec.CommandContext(ctx, c.DiffCommand, args...)
|
|
cmd.Dir = c.tmpDir
|
|
cmd.Stdout = c.out
|
|
cmd.Stderr = c.out
|
|
cmd.Run() //nolint:errcheck
|
|
return nil
|
|
}
|
|
|
|
func (c *Comparer) downloadFile(ctx context.Context, f fs.File, fname string) error {
|
|
if err := os.MkdirAll(filepath.Dir(fname), 0700); err != nil {
|
|
return err
|
|
}
|
|
|
|
src, err := f.Open(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close() //nolint:errcheck
|
|
|
|
dst, err := os.Create(fname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dst.Close() //nolint:errcheck
|
|
|
|
_, err = io.Copy(dst, src)
|
|
return err
|
|
}
|
|
|
|
func (c *Comparer) output(msg string, args ...interface{}) {
|
|
fmt.Fprintf(c.out, msg, args...) //nolint:errcheck
|
|
}
|
|
|
|
// NewComparer creates a comparer for a given repository that will output the results to a given writer.
|
|
func NewComparer(rep *repo.Repository, out io.Writer) (*Comparer, error) {
|
|
tmp, err := ioutil.TempDir("", "kopia")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Comparer{rep: rep, out: out, tmpDir: tmp}, nil
|
|
}
|