b2: add SkipDestructive handling to backend commands - fixes #8194
This commit is contained in:
parent
d8bc542ffc
commit
29b873b62b
2 changed files with 161 additions and 13 deletions
|
@ -30,6 +30,7 @@ import (
|
||||||
"github.com/rclone/rclone/fs/fserrors"
|
"github.com/rclone/rclone/fs/fserrors"
|
||||||
"github.com/rclone/rclone/fs/fshttp"
|
"github.com/rclone/rclone/fs/fshttp"
|
||||||
"github.com/rclone/rclone/fs/hash"
|
"github.com/rclone/rclone/fs/hash"
|
||||||
|
"github.com/rclone/rclone/fs/operations"
|
||||||
"github.com/rclone/rclone/fs/walk"
|
"github.com/rclone/rclone/fs/walk"
|
||||||
"github.com/rclone/rclone/lib/bucket"
|
"github.com/rclone/rclone/lib/bucket"
|
||||||
"github.com/rclone/rclone/lib/encoder"
|
"github.com/rclone/rclone/lib/encoder"
|
||||||
|
@ -1318,17 +1319,23 @@ func (f *Fs) purge(ctx context.Context, dir string, oldOnly bool, deleteHidden b
|
||||||
// Check current version of the file
|
// Check current version of the file
|
||||||
if deleteHidden && object.Action == "hide" {
|
if deleteHidden && object.Action == "hide" {
|
||||||
fs.Debugf(remote, "Deleting current version (id %q) as it is a hide marker", object.ID)
|
fs.Debugf(remote, "Deleting current version (id %q) as it is a hide marker", object.ID)
|
||||||
|
if !operations.SkipDestructive(ctx, object.Name, "remove hide marker") {
|
||||||
toBeDeleted <- object
|
toBeDeleted <- object
|
||||||
|
}
|
||||||
} else if deleteUnfinished && object.Action == "start" && isUnfinishedUploadStale(object.UploadTimestamp) {
|
} else if deleteUnfinished && object.Action == "start" && isUnfinishedUploadStale(object.UploadTimestamp) {
|
||||||
fs.Debugf(remote, "Deleting current version (id %q) as it is a start marker (upload started at %s)", object.ID, time.Time(object.UploadTimestamp).Local())
|
fs.Debugf(remote, "Deleting current version (id %q) as it is a start marker (upload started at %s)", object.ID, time.Time(object.UploadTimestamp).Local())
|
||||||
|
if !operations.SkipDestructive(ctx, object.Name, "remove pending upload") {
|
||||||
toBeDeleted <- object
|
toBeDeleted <- object
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fs.Debugf(remote, "Not deleting current version (id %q) %q dated %v (%v ago)", object.ID, object.Action, time.Time(object.UploadTimestamp).Local(), time.Since(time.Time(object.UploadTimestamp)))
|
fs.Debugf(remote, "Not deleting current version (id %q) %q dated %v (%v ago)", object.ID, object.Action, time.Time(object.UploadTimestamp).Local(), time.Since(time.Time(object.UploadTimestamp)))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fs.Debugf(remote, "Deleting (id %q)", object.ID)
|
fs.Debugf(remote, "Deleting (id %q)", object.ID)
|
||||||
|
if !operations.SkipDestructive(ctx, object.Name, "delete") {
|
||||||
toBeDeleted <- object
|
toBeDeleted <- object
|
||||||
}
|
}
|
||||||
|
}
|
||||||
last = remote
|
last = remote
|
||||||
tr.Done(ctx, nil)
|
tr.Done(ctx, nil)
|
||||||
}
|
}
|
||||||
|
@ -2284,8 +2291,10 @@ func (f *Fs) lifecycleCommand(ctx context.Context, name string, arg []string, op
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
skip := operations.SkipDestructive(ctx, name, "update lifecycle rules")
|
||||||
|
|
||||||
var bucket *api.Bucket
|
var bucket *api.Bucket
|
||||||
if newRule.DaysFromHidingToDeleting != nil || newRule.DaysFromUploadingToHiding != nil {
|
if !skip && (newRule.DaysFromHidingToDeleting != nil || newRule.DaysFromUploadingToHiding != nil) {
|
||||||
bucketID, err := f.getBucketID(ctx, bucketName)
|
bucketID, err := f.getBucketID(ctx, bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -13,6 +14,7 @@ import (
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/cache"
|
"github.com/rclone/rclone/fs/cache"
|
||||||
"github.com/rclone/rclone/fs/hash"
|
"github.com/rclone/rclone/fs/hash"
|
||||||
|
"github.com/rclone/rclone/fs/object"
|
||||||
"github.com/rclone/rclone/fstest"
|
"github.com/rclone/rclone/fstest"
|
||||||
"github.com/rclone/rclone/fstest/fstests"
|
"github.com/rclone/rclone/fstest/fstests"
|
||||||
"github.com/rclone/rclone/lib/bucket"
|
"github.com/rclone/rclone/lib/bucket"
|
||||||
|
@ -463,24 +465,161 @@ func (f *Fs) InternalTestVersions(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Cleanup", func(t *testing.T) {
|
t.Run("Cleanup", func(t *testing.T) {
|
||||||
require.NoError(t, f.cleanUp(ctx, true, false, 0))
|
t.Run("DryRun", func(t *testing.T) {
|
||||||
items := append([]fstest.Item{newItem}, fstests.InternalTestFiles...)
|
|
||||||
fstest.CheckListing(t, f, items)
|
|
||||||
// Set --b2-versions for this test
|
|
||||||
f.opt.Versions = true
|
f.opt.Versions = true
|
||||||
defer func() {
|
defer func() {
|
||||||
f.opt.Versions = false
|
f.opt.Versions = false
|
||||||
}()
|
}()
|
||||||
|
// Listing should be unchanged after dry run
|
||||||
|
before := listAllFiles(ctx, t, f, dirName)
|
||||||
|
ctx, ci := fs.AddConfig(ctx)
|
||||||
|
ci.DryRun = true
|
||||||
|
require.NoError(t, f.cleanUp(ctx, true, false, 0))
|
||||||
|
after := listAllFiles(ctx, t, f, dirName)
|
||||||
|
assert.Equal(t, before, after)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RealThing", func(t *testing.T) {
|
||||||
|
f.opt.Versions = true
|
||||||
|
defer func() {
|
||||||
|
f.opt.Versions = false
|
||||||
|
}()
|
||||||
|
// Listing should reflect current state after cleanup
|
||||||
|
require.NoError(t, f.cleanUp(ctx, true, false, 0))
|
||||||
|
items := append([]fstest.Item{newItem}, fstests.InternalTestFiles...)
|
||||||
fstest.CheckListing(t, f, items)
|
fstest.CheckListing(t, f, items)
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Purge gets tested later
|
// Purge gets tested later
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fs) InternalTestCleanupUnfinished(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// B2CleanupHidden tests cleaning up hidden files
|
||||||
|
t.Run("CleanupUnfinished", func(t *testing.T) {
|
||||||
|
dirName := "unfinished"
|
||||||
|
fileCount := 5
|
||||||
|
expectedFiles := []string{}
|
||||||
|
for i := 1; i < fileCount; i++ {
|
||||||
|
fileName := fmt.Sprintf("%s/unfinished-%d", dirName, i)
|
||||||
|
expectedFiles = append(expectedFiles, fileName)
|
||||||
|
obj := &Object{
|
||||||
|
fs: f,
|
||||||
|
remote: fileName,
|
||||||
|
}
|
||||||
|
objInfo := object.NewStaticObjectInfo(fileName, fstest.Time("2002-02-03T04:05:06.499999999Z"), -1, true, nil, nil)
|
||||||
|
_, err := f.newLargeUpload(ctx, obj, nil, objInfo, f.opt.ChunkSize, false, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
checkListing(ctx, t, f, dirName, expectedFiles)
|
||||||
|
|
||||||
|
t.Run("DryRun", func(t *testing.T) {
|
||||||
|
// Listing should not change after dry run
|
||||||
|
ctx, ci := fs.AddConfig(ctx)
|
||||||
|
ci.DryRun = true
|
||||||
|
require.NoError(t, f.cleanUp(ctx, false, true, 0))
|
||||||
|
checkListing(ctx, t, f, dirName, expectedFiles)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RealThing", func(t *testing.T) {
|
||||||
|
// Listing should be empty after real cleanup
|
||||||
|
require.NoError(t, f.cleanUp(ctx, false, true, 0))
|
||||||
|
checkListing(ctx, t, f, dirName, []string{})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func listAllFiles(ctx context.Context, t *testing.T, f *Fs, dirName string) []string {
|
||||||
|
bucket, directory := f.split(dirName)
|
||||||
|
foundFiles := []string{}
|
||||||
|
require.NoError(t, f.list(ctx, bucket, directory, "", false, true, 0, true, false, func(remote string, object *api.File, isDirectory bool) error {
|
||||||
|
if !isDirectory {
|
||||||
|
foundFiles = append(foundFiles, object.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
sort.Strings(foundFiles)
|
||||||
|
return foundFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkListing(ctx context.Context, t *testing.T, f *Fs, dirName string, expectedFiles []string) {
|
||||||
|
foundFiles := listAllFiles(ctx, t, f, dirName)
|
||||||
|
sort.Strings(expectedFiles)
|
||||||
|
assert.Equal(t, expectedFiles, foundFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fs) InternalTestLifecycleRules(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
opt := map[string]string{}
|
||||||
|
|
||||||
|
t.Run("InitState", func(t *testing.T) {
|
||||||
|
// There should be no lifecycle rules at the outset
|
||||||
|
lifecycleRulesIf, err := f.lifecycleCommand(ctx, "lifecycle", nil, opt)
|
||||||
|
lifecycleRules := lifecycleRulesIf.([]api.LifecycleRule)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, len(lifecycleRules))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DryRun", func(t *testing.T) {
|
||||||
|
// There should still be no lifecycle rules after each dry run operation
|
||||||
|
ctx, ci := fs.AddConfig(ctx)
|
||||||
|
ci.DryRun = true
|
||||||
|
|
||||||
|
opt["daysFromHidingToDeleting"] = "30"
|
||||||
|
lifecycleRulesIf, err := f.lifecycleCommand(ctx, "lifecycle", nil, opt)
|
||||||
|
lifecycleRules := lifecycleRulesIf.([]api.LifecycleRule)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, len(lifecycleRules))
|
||||||
|
|
||||||
|
delete(opt, "daysFromHidingToDeleting")
|
||||||
|
opt["daysFromUploadingToHiding"] = "40"
|
||||||
|
lifecycleRulesIf, err = f.lifecycleCommand(ctx, "lifecycle", nil, opt)
|
||||||
|
lifecycleRules = lifecycleRulesIf.([]api.LifecycleRule)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, len(lifecycleRules))
|
||||||
|
|
||||||
|
opt["daysFromHidingToDeleting"] = "30"
|
||||||
|
lifecycleRulesIf, err = f.lifecycleCommand(ctx, "lifecycle", nil, opt)
|
||||||
|
lifecycleRules = lifecycleRulesIf.([]api.LifecycleRule)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, len(lifecycleRules))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RealThing", func(t *testing.T) {
|
||||||
|
opt["daysFromHidingToDeleting"] = "30"
|
||||||
|
lifecycleRulesIf, err := f.lifecycleCommand(ctx, "lifecycle", nil, opt)
|
||||||
|
lifecycleRules := lifecycleRulesIf.([]api.LifecycleRule)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(lifecycleRules))
|
||||||
|
assert.Equal(t, 30, *lifecycleRules[0].DaysFromHidingToDeleting)
|
||||||
|
|
||||||
|
delete(opt, "daysFromHidingToDeleting")
|
||||||
|
opt["daysFromUploadingToHiding"] = "40"
|
||||||
|
lifecycleRulesIf, err = f.lifecycleCommand(ctx, "lifecycle", nil, opt)
|
||||||
|
lifecycleRules = lifecycleRulesIf.([]api.LifecycleRule)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(lifecycleRules))
|
||||||
|
assert.Equal(t, 40, *lifecycleRules[0].DaysFromUploadingToHiding)
|
||||||
|
|
||||||
|
opt["daysFromHidingToDeleting"] = "30"
|
||||||
|
lifecycleRulesIf, err = f.lifecycleCommand(ctx, "lifecycle", nil, opt)
|
||||||
|
lifecycleRules = lifecycleRulesIf.([]api.LifecycleRule)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(lifecycleRules))
|
||||||
|
assert.Equal(t, 30, *lifecycleRules[0].DaysFromHidingToDeleting)
|
||||||
|
assert.Equal(t, 40, *lifecycleRules[0].DaysFromUploadingToHiding)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// -run TestIntegration/FsMkdir/FsPutFiles/Internal
|
// -run TestIntegration/FsMkdir/FsPutFiles/Internal
|
||||||
func (f *Fs) InternalTest(t *testing.T) {
|
func (f *Fs) InternalTest(t *testing.T) {
|
||||||
t.Run("Metadata", f.InternalTestMetadata)
|
t.Run("Metadata", f.InternalTestMetadata)
|
||||||
t.Run("Versions", f.InternalTestVersions)
|
t.Run("Versions", f.InternalTestVersions)
|
||||||
|
t.Run("CleanupUnfinished", f.InternalTestCleanupUnfinished)
|
||||||
|
t.Run("LifecycleRules", f.InternalTestLifecycleRules)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ fstests.InternalTester = (*Fs)(nil)
|
var _ fstests.InternalTester = (*Fs)(nil)
|
||||||
|
|
Loading…
Reference in a new issue