Skip to content

Commit

Permalink
feat: Support object soft delete
Browse files Browse the repository at this point in the history
  • Loading branch information
amanda-tarafa committed Mar 25, 2024
1 parent a3f0f6a commit ae28cb4
Show file tree
Hide file tree
Showing 11 changed files with 456 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using Google.Cloud.ClientTesting;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace Google.Cloud.Storage.V1.IntegrationTests
Expand Down Expand Up @@ -68,5 +71,21 @@ public void SpecifyGeneration()
Assert.Equal((ulong) _fixture.SmallContent.Length, o1.Size);
Assert.Equal((ulong) _fixture.LargeContent.Length, o2.Size);
}

[Fact]
public async Task GetSoftDeleted()
{
// We upload and delete an object on the soft delete bucket.
var uploaded = await _fixture.Client.UploadObjectAsync(_fixture.SoftDeleteBucket, IdGenerator.FromGuid(prefix: "get-soft-delete"), "text/plain", new MemoryStream(_fixture.SmallContent));
await _fixture.Client.DeleteObjectAsync(uploaded);

// And now we get it, only soft deleted
var softDeleted = await _fixture.Client.GetObjectAsync(_fixture.SoftDeleteBucket, uploaded.Name, new GetObjectOptions { SoftDeletedOnly = true, Generation = uploaded.Generation });

Assert.Equal(_fixture.SoftDeleteBucket, softDeleted.Bucket);
Assert.Equal(uploaded.Name, softDeleted.Name);
Assert.Equal((ulong) _fixture.SmallContent.Length, softDeleted.Size);
Assert.NotNull(softDeleted.SoftDeleteTimeDateTimeOffset);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2024 Google Inc. All Rights Reserved.
//
// Licensed 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.

using Google.Cloud.ClientTesting;
using System.IO;
using System.Threading.Tasks;
using Xunit;

namespace Google.Cloud.Storage.V1.IntegrationTests
{
[Collection(nameof(StorageFixture))]
public class RestoreObjectTest
{
private readonly StorageFixture _fixture;

public RestoreObjectTest(StorageFixture fixture)
{
_fixture = fixture;
}

[Fact]
public async Task RestoresObject()
{
// We upload and delete an object on the soft delete bucket.
var uploaded = await _fixture.Client.UploadObjectAsync(_fixture.SoftDeleteBucket, IdGenerator.FromGuid(prefix: "restore-soft-delete"), "text/plain", new MemoryStream(_fixture.SmallContent));
await _fixture.Client.DeleteObjectAsync(uploaded);

// And now we can restore it.
await _fixture.Client.RestoreObjectAsync(_fixture.SoftDeleteBucket, uploaded.Name, uploaded.Generation.Value);

var restored = await _fixture.Client.GetObjectAsync(_fixture.SoftDeleteBucket, uploaded.Name);
Assert.Equal(_fixture.SoftDeleteBucket, restored.Bucket);
Assert.Equal(uploaded.Name, restored.Name);
Assert.Equal((ulong) _fixture.SmallContent.Length, restored.Size);
Assert.Null(restored.SoftDeleteTimeDateTimeOffset);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ public sealed class StorageFixture : CloudProjectFixtureBase, ICollectionFixture
/// </summary>
public string InitiallyEmptyBucket => BucketPrefix + "-empty";

/// <summary>
/// Name of a bucket which already exists, has no canned data, but has soft delete protection
/// for 24 hours.
/// </summary>
public string SoftDeleteBucket => BucketPrefix + "-soft-delete";

/// <summary>
/// A small amount of content. Do not mutate the array.
/// </summary>
Expand Down Expand Up @@ -154,12 +160,13 @@ public StorageFixture()
Client = StorageClient.Create();
BucketPrefix = IdGenerator.FromDateTime(prefix: "tests-", suffix: "-");
LargeContent = Encoding.UTF8.GetBytes(string.Join("\n", Enumerable.Repeat("All work and no play makes Jack a dull boy.", 500)));
CreateBucket(SingleVersionBucket, false);
CreateBucket(MultiVersionBucket, true);
CreateBucket(SingleVersionBucket, multiVersion: false);
CreateBucket(MultiVersionBucket, multiVersion: true);
CreateAndPopulateReadBucket();
CreateBucket(BucketBeginningWithZ, false);
CreateBucket(LabelsTestBucket, false);
CreateBucket(InitiallyEmptyBucket, false);
CreateBucket(BucketBeginningWithZ, multiVersion: false);
CreateBucket(LabelsTestBucket, multiVersion: false);
CreateBucket(InitiallyEmptyBucket, multiVersion: false);
CreateBucket(SoftDeleteBucket, multiVersion: false, softDelete: true);

RequesterPaysClient = CreateRequesterPaysClient();
if (RequesterPaysClient != null)
Expand Down Expand Up @@ -242,9 +249,16 @@ void CreateObject()

}

internal Bucket CreateBucket(string name, bool multiVersion)
internal Bucket CreateBucket(string name, bool multiVersion, bool softDelete = false)
{
var bucket = Client.CreateBucket(ProjectId, new Bucket { Name = name, Versioning = new Bucket.VersioningData { Enabled = multiVersion } });
var bucket = Client.CreateBucket(ProjectId,
new Bucket
{
Name = name,
Versioning = new Bucket.VersioningData { Enabled = multiVersion },
// The minimum allowed for soft delete is 7 days.
SoftDeletePolicy = softDelete ? new Bucket.SoftDeletePolicyData { RetentionDurationSeconds = (int) TimeSpan.FromDays(7).TotalSeconds } : null,
});
SleepAfterBucketCreateDelete();
RegisterBucketToDelete(name);
return bucket;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public void ModifyRequest_DefaultOptions()
Assert.Null(request.IfGenerationNotMatch);
Assert.Null(request.IfMetagenerationMatch);
Assert.Null(request.IfMetagenerationNotMatch);
Assert.Null(request.SoftDeleted);
Assert.Null(request.Projection);
Assert.Null(request.UserProject);
}
Expand All @@ -45,6 +46,7 @@ public void ModifyRequest_PositiveMatchOptions()
IfGenerationMatch = 1L,
IfMetagenerationMatch = 2L,
Generation = 3L,
SoftDeletedOnly = true,
Projection = Projection.Full,
UserProject = "proj"
};
Expand All @@ -54,6 +56,7 @@ public void ModifyRequest_PositiveMatchOptions()
Assert.Equal(2L, request.IfMetagenerationMatch);
Assert.Null(request.IfMetagenerationNotMatch);
Assert.Equal(3L, request.Generation);
Assert.True(request.SoftDeleted);
Assert.Equal(ProjectionEnum.Full, request.Projection);
Assert.Equal("proj", request.UserProject);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public void ModifyRequest_DefaultOptions()
Assert.Null(request.Projection);
Assert.Null(request.MaxResults);
Assert.Null(request.Versions);
Assert.Null(request.SoftDeleted);
Assert.Null(request.UserProject);
Assert.Null(request.PageToken);
Assert.Null(request.StartOffset);
Expand All @@ -51,6 +52,7 @@ public void ModifyRequest_AllOptions()
IncludeFoldersAsPrefixes = true,
Projection = Projection.Full,
Versions = true,
SoftDeletedOnly = true,
UserProject = "proj",
PageToken = "nextpage",
Fields = "items(name),nextPageToken",
Expand All @@ -65,6 +67,7 @@ public void ModifyRequest_AllOptions()
Assert.True(request.IncludeFoldersAsPrefixes);
Assert.Equal(ProjectionEnum.Full, request.Projection);
Assert.True(request.Versions);
Assert.True(request.SoftDeleted);
Assert.Equal("proj", request.UserProject);
Assert.Equal("nextpage", request.PageToken);
Assert.Equal("items(name),nextPageToken", request.Fields);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2024 Google Inc. All Rights Reserved.
//
// Licensed 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.

using System;
using Xunit;
using static Google.Apis.Storage.v1.ObjectsResource;
using static Google.Apis.Storage.v1.ObjectsResource.RestoreRequest;
using Object = Google.Apis.Storage.v1.Data.Object;

namespace Google.Cloud.Storage.V1.Tests;

public class RestoreObjectOptionsTest
{
[Fact]
public void ModifyRequest_DefaultOptions()
{
var request = new RestoreRequest(null, "bucket", "object", 2L);
var options = new RestoreObjectOptions();
options.ModifyRequest(request);
Assert.Null(request.IfGenerationMatch);
Assert.Null(request.IfGenerationNotMatch);
Assert.Null(request.IfMetagenerationMatch);
Assert.Null(request.IfMetagenerationNotMatch);
Assert.Null(request.CopySourceAcl);
Assert.Null(request.Projection);
Assert.Null(request.UserProject);
}

[Fact]
public void ModifyRequest_PositiveMatchOptions()
{
var request = new RestoreRequest(null, "bucket", "object", 2L);
var options = new RestoreObjectOptions
{
IfGenerationMatch = 1L,
IfMetagenerationMatch = 2L,
CopySourceAcl = true,
Projection = Projection.Full,
UserProject = "proj"
};
options.ModifyRequest(request);
Assert.Equal(1L, request.IfGenerationMatch);
Assert.Null(request.IfGenerationNotMatch);
Assert.Equal(2L, request.IfMetagenerationMatch);
Assert.Null(request.IfMetagenerationNotMatch);
Assert.True(request.CopySourceAcl);
Assert.Equal(ProjectionEnum.Full, request.Projection);
Assert.Equal("proj", request.UserProject);
}

[Fact]
public void ModifyRequest_NegativeMatchOptions()
{
var request = new RestoreRequest(null, "bucket", "object", 2L);
var options = new RestoreObjectOptions
{
IfGenerationNotMatch = 1L,
IfMetagenerationNotMatch = 2L,
Projection = Projection.Full
};
options.ModifyRequest(request);
Assert.Null(request.IfGenerationMatch);
Assert.Equal(1L, request.IfGenerationNotMatch);
Assert.Null(request.IfMetagenerationMatch);
Assert.Equal(2L, request.IfMetagenerationNotMatch);
Assert.Equal(ProjectionEnum.Full, request.Projection);
}

[Fact]
public void ModifyRequest_MatchNotMatchConflicts()
{
var request = new RestoreRequest(null, "bucket", "object", 2L);
Assert.Throws<ArgumentException>(() =>
{
var options = new RestoreObjectOptions { IfGenerationMatch = 1L, IfGenerationNotMatch = 2L };
options.ModifyRequest(request);
});
Assert.Throws<ArgumentException>(() =>
{
var options = new RestoreObjectOptions { IfMetagenerationMatch = 1L, IfMetagenerationNotMatch = 2L };
options.ModifyRequest(request);
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public sealed class GetObjectOptions
/// </summary>
public long? Generation { get; set; }

/// <summary>
/// If true, only soft-deleted object versions will be retrieved. The default is false.
/// </summary>
public bool? SoftDeletedOnly { get; set; }

/// <summary>
/// The projection to retrieve.
/// </summary>
Expand Down Expand Up @@ -109,6 +114,10 @@ internal void ModifyRequest(GetRequest request)
{
request.IfMetagenerationNotMatch = IfMetagenerationNotMatch;
}
if (SoftDeletedOnly != null)
{
request.SoftDeleted = SoftDeletedOnly;
}
if (Projection != null)
{
request.Projection = GaxPreconditions.CheckEnumValue((ProjectionEnum) Projection, nameof(Projection));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public sealed class ListObjectsOptions
/// </summary>
public bool? Versions { get; set; }

/// <summary>
/// If true, only soft-deleted object versions will be listed. The default is false.
/// </summary>
public bool? SoftDeletedOnly { get; set; }

/// <summary>
/// The projection to retrieve.
/// </summary>
Expand Down Expand Up @@ -132,6 +137,10 @@ internal void ModifyRequest(ListRequest request)
{
request.Versions = Versions;
}
if (SoftDeletedOnly != null)
{
request.SoftDeleted = SoftDeletedOnly;
}
if (Projection != null)
{
request.Projection = GaxPreconditions.CheckEnumValue((ProjectionEnum) Projection, nameof(Projection));
Expand Down

0 comments on commit ae28cb4

Please sign in to comment.