mirror of
https://github.com/octoleo/restic.git
synced 2024-10-31 19:02:32 +00:00
copy: Mark and skip previously copied snapshots
Use the `Original` field of the copied snapshot to store a persistent snapshot ID. This can either be the ID of the source snapshot if `Original` was not yet set or the previous value stored in the `Original` field. In order to still copy snapshots modified using the tags command the source snapshot is compared to all snapshots in the destination repository which have the same persistent ID. Snapshots are only considered equal if all fields except `Original` and `Parent` match. That way modified snapshots are still copied while avoiding duplicate copies at the same time.
This commit is contained in:
parent
b0a8c4ad6c
commit
ec9a53b7e8
@ -109,9 +109,36 @@ func runCopy(opts CopyOptions, gopts GlobalOptions, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
visitedTrees := restic.NewIDSet()
|
visitedTrees := restic.NewIDSet()
|
||||||
|
dstSnapshotByOriginal := make(map[restic.ID][]*restic.Snapshot)
|
||||||
|
for sn := range FindFilteredSnapshots(ctx, dstRepo, opts.Hosts, opts.Tags, opts.Paths, nil) {
|
||||||
|
if sn.Original != nil && !sn.Original.IsNull() {
|
||||||
|
dstSnapshotByOriginal[*sn.Original] = append(dstSnapshotByOriginal[*sn.Original], sn)
|
||||||
|
}
|
||||||
|
// also consider identical snapshot copies
|
||||||
|
dstSnapshotByOriginal[*sn.ID()] = append(dstSnapshotByOriginal[*sn.ID()], sn)
|
||||||
|
}
|
||||||
|
|
||||||
for sn := range FindFilteredSnapshots(ctx, srcRepo, opts.Hosts, opts.Tags, opts.Paths, args) {
|
for sn := range FindFilteredSnapshots(ctx, srcRepo, opts.Hosts, opts.Tags, opts.Paths, args) {
|
||||||
Verbosef("snapshot %s of %v at %s)\n", sn.ID().Str(), sn.Paths, sn.Time)
|
Verbosef("\nsnapshot %s of %v at %s)\n", sn.ID().Str(), sn.Paths, sn.Time)
|
||||||
|
|
||||||
|
// check whether the destination has a snapshot with the same persistent ID which has similar snapshot fields
|
||||||
|
srcOriginal := *sn.ID()
|
||||||
|
if sn.Original != nil {
|
||||||
|
srcOriginal = *sn.Original
|
||||||
|
}
|
||||||
|
if originalSns, ok := dstSnapshotByOriginal[srcOriginal]; ok {
|
||||||
|
isCopy := false
|
||||||
|
for _, originalSn := range originalSns {
|
||||||
|
if similarSnapshots(originalSn, sn) {
|
||||||
|
Verbosef("skipping source snapshot %s, was already copied to snapshot %s\n", sn.ID().Str(), originalSn.ID().Str())
|
||||||
|
isCopy = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isCopy {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
Verbosef(" copy started, this may take a while...\n")
|
Verbosef(" copy started, this may take a while...\n")
|
||||||
|
|
||||||
if err := copyTree(ctx, srcRepo, dstRepo, *sn.Tree, visitedTrees); err != nil {
|
if err := copyTree(ctx, srcRepo, dstRepo, *sn.Tree, visitedTrees); err != nil {
|
||||||
@ -125,8 +152,11 @@ func runCopy(opts CopyOptions, gopts GlobalOptions, args []string) error {
|
|||||||
debug.Log("flushed packs and saved index")
|
debug.Log("flushed packs and saved index")
|
||||||
|
|
||||||
// save snapshot
|
// save snapshot
|
||||||
sn.Parent = nil // Parent does not have relevance in the new repo.
|
sn.Parent = nil // Parent does not have relevance in the new repo.
|
||||||
sn.Original = nil // Original does not have relevance in the new repo.
|
// Use Original as a persistent snapshot ID
|
||||||
|
if sn.Original == nil {
|
||||||
|
sn.Original = sn.ID()
|
||||||
|
}
|
||||||
newID, err := dstRepo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn)
|
newID, err := dstRepo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -136,6 +166,25 @@ func runCopy(opts CopyOptions, gopts GlobalOptions, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func similarSnapshots(sna *restic.Snapshot, snb *restic.Snapshot) bool {
|
||||||
|
// everything except Parent and Original must match
|
||||||
|
if !sna.Time.Equal(snb.Time) || !sna.Tree.Equal(*snb.Tree) || sna.Hostname != snb.Hostname ||
|
||||||
|
sna.Username != snb.Username || sna.UID != snb.UID || sna.GID != snb.GID ||
|
||||||
|
len(sna.Paths) != len(snb.Paths) || len(sna.Excludes) != len(snb.Excludes) ||
|
||||||
|
len(sna.Tags) != len(snb.Tags) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !sna.HasPaths(snb.Paths) || !sna.HasTags(snb.Tags) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, a := range sna.Excludes {
|
||||||
|
if a != snb.Excludes[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func copyTree(ctx context.Context, srcRepo, dstRepo restic.Repository, treeID restic.ID, visitedTrees restic.IDSet) error {
|
func copyTree(ctx context.Context, srcRepo, dstRepo restic.Repository, treeID restic.ID, visitedTrees restic.IDSet) error {
|
||||||
// We have already processed this tree
|
// We have already processed this tree
|
||||||
if visitedTrees.Has(treeID) {
|
if visitedTrees.Has(treeID) {
|
||||||
|
Loading…
Reference in New Issue
Block a user