Skip to content

Commit

Permalink
feat(Storage): support for soft-delete and restore buckets (#7966)
Browse files Browse the repository at this point in the history
  • Loading branch information
thiyaguk09 authored Jan 6, 2025
1 parent bd706b1 commit 944287d
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 2 deletions.
10 changes: 10 additions & 0 deletions Storage/src/Bucket.php
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,9 @@ public function compose(array $sourceObjects, $name, array $options = [])
* @param array $options [optional] {
* Configuration options.
*
* @type string $generation If present, selects a specific soft-deleted
* version of this bucket instead of the live version.
* This parameter is required if softDeleted is set to true.
* @type string $ifMetagenerationMatch Makes the return of the bucket
* metadata conditional on whether the bucket's current
* metageneration matches the given value.
Expand All @@ -1185,6 +1188,8 @@ public function compose(array $sourceObjects, $name, array $options = [])
* metageneration does not match the given value.
* @type string $projection Determines which properties to return. May
* be either `"full"` or `"noAcl"`.
* @type bool $softDeleted If true, returns the soft-deleted bucket.
* This parameter is required if generation is specified.
* }
* @return array
*/
Expand All @@ -1208,6 +1213,9 @@ public function info(array $options = [])
* @param array $options [optional] {
* Configuration options.
*
* @type string $generation If present, selects a specific soft-deleted
* version of this bucket instead of the live version.
* This parameter is required if softDeleted is set to true.
* @type string $ifMetagenerationMatch Makes the return of the bucket
* metadata conditional on whether the bucket's current
* metageneration matches the given value.
Expand All @@ -1216,6 +1224,8 @@ public function info(array $options = [])
* metageneration does not match the given value.
* @type string $projection Determines which properties to return. May
* be either `"full"` or `"noAcl"`.
* @type bool $softDeleted If true, returns the soft-deleted bucket.
* This parameter is required if generation is specified.
* }
* @return array
*/
Expand Down
5 changes: 5 additions & 0 deletions Storage/src/Connection/ConnectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public function patchAcl(array $args = []);
*/
public function deleteBucket(array $args = []);

/**
* @param array $args
*/
public function restoreBucket(array $args = []);

/**
* @param array $args
*/
Expand Down
8 changes: 8 additions & 0 deletions Storage/src/Connection/Rest.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,14 @@ public function deleteBucket(array $args = [])
return $this->send('buckets', 'delete', $args);
}

/**
* @param array $args
*/
public function restoreBucket(array $args = [])
{
return $this->send('buckets', 'restore', $args);
}

/**
* @param array $args
*/
Expand Down
76 changes: 76 additions & 0 deletions Storage/src/Connection/ServiceDefinition/storage-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,11 @@
]
}
},
"generation": {
"type": "string",
"description": "The version of the bucket.",
"format": "int64"
},
"owner": {
"type": "object",
"description": "The owner of the bucket. This is always the project team's owner group.",
Expand Down Expand Up @@ -501,6 +506,16 @@
}
}
},
"softDeleteTime": {
"type": "string",
"description": "The time at which the bucket was soft-deleted.",
"format": "date-time"
},
"hardDeleteTime": {
"type": "string",
"description": "The time when a soft-deleted bucket is permanently deleted and can no longer be restored.",
"format": "date-time"
},
"storageClass": {
"type": "string",
"description": "The bucket's default storage class, used whenever no storageClass is specified for a newly-created object. This defines how objects in the bucket are stored and determines the SLA and the cost of storage. Values include MULTI_REGIONAL, REGIONAL, STANDARD, NEARLINE, COLDLINE, ARCHIVE, and DURABLE_REDUCED_AVAILABILITY. If this value is not specified when the bucket is created, it will default to STANDARD. For more information, see storage classes."
Expand Down Expand Up @@ -2277,6 +2292,12 @@
"required": true,
"location": "path"
},
"generation": {
"type": "string",
"description": "If present, selects a specific soft-deleted version of this bucket instead of the live version. This parameter is required if softDeleted is set to true.",
"format": "int64",
"location": "query"
},
"ifMetagenerationMatch": {
"type": "string",
"description": "Makes the return of the bucket metadata conditional on whether the bucket's current metageneration matches the given value.",
Expand Down Expand Up @@ -2306,6 +2327,11 @@
"type": "string",
"description": "The project to be billed for this request. Required for Requester Pays buckets.",
"location": "query"
},
"softDeleted": {
"type": "boolean",
"description": "If true, returns the soft-deleted bucket. This parameter is required if generation is specified.",
"location": "query"
}
},
"parameterOrder": [
Expand Down Expand Up @@ -2497,6 +2523,11 @@
"type": "string",
"description": "The project to be billed for this request.",
"location": "query"
},
"softDeleted": {
"type": "boolean",
"description": "If set to true, only soft-deleted bucket versions are listed as distinct results in order of bucket name and generation number. The default value is false.",
"location": "query"
}
},
"parameterOrder": [
Expand Down Expand Up @@ -2816,6 +2847,51 @@
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/devstorage.full_control"
]
},
"restore": {
"id": "storage.buckets.restore",
"path": "b/{bucket}/restore",
"httpMethod": "POST",
"description": "Restores a soft-deleted bucket.",
"parameters": {
"bucket": {
"type": "string",
"description": "Name of the bucket to be restored.",
"required": true,
"location": "path"
},
"generation": {
"type": "string",
"description": "The specific version of the bucket to be restored.",
"required": true,
"format": "int64",
"location": "query"
},
"projection": {
"type": "string",
"description": "Set of properties to return. Defaults to full.",
"enum": [
"full",
"noAcl"
],
"enumDescriptions": [
"Include all properties.",
"Omit the owner, acl property."
],
"location": "query"
}
},
"parameterOrder": [
"bucket",
"generation"
],
"response": {
"$ref": "Bucket"
},
"scopes": [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/devstorage.full_control"
]
}
}
},
Expand Down
49 changes: 47 additions & 2 deletions Storage/src/StorageClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,27 @@ public function __construct(array $config = [])
* will be used. If a string, that string will be used as the
* userProject argument, and that project will be billed for the
* request. **Defaults to** `false`.
* @param array $options [optional] {
* Configuration Options.
*
* @type bool $softDeleted If set to true, only soft-deleted bucket versions
* are listed as distinct results in order of bucket name and generation
* number. The default value is false.
* @type string $generation If present, selects a specific soft-deleted version
* of this bucket instead of the live version. This parameter is required if
* softDeleted is set to true.
* }
* @return Bucket
*/
public function bucket($name, $userProject = false)
public function bucket($name, $userProject = false, array $options = [])
{
if (!$userProject) {
$userProject = null;
} elseif (!is_string($userProject)) {
$userProject = $this->projectId;
}

return new Bucket($this->connection, $name, [
return new Bucket($this->connection, $name, $options + [
'requesterProjectId' => $userProject
]);
}
Expand Down Expand Up @@ -200,6 +210,9 @@ public function bucket($name, $userProject = false)
* return the specified fields.
* @type string $userProject If set, this is the ID of the project which
* will be billed for the request.
* @type bool $softDeleted If set to true, only soft-deleted bucket versions
* are listed as distinct results in order of bucket name and generation
* number. The default value is false.
* @type bool $bucketUserProject If true, each returned instance will
* have `$userProject` set to the value of `$options.userProject`.
* If false, `$options.userProject` will be used ONLY for the
Expand Down Expand Up @@ -238,6 +251,38 @@ function (array $bucket) use ($userProject) {
);
}

/**
* Restores a soft-deleted bucket.
*
* Example:
* ```
* $bucket = $storage->bucket->restore('my-bucket');
* ```
*
* @param string $name The name of the bucket to restore.
* @param string $generation The specific version of the bucket to be restored.
* @param array $options [optional] {
* Configuration Options.
*
* @type string $projection Determines which properties to return. May
* be either `"full"` or `"noAcl"`. **Defaults to** `"noAcl"`,
* unless the bucket resource specifies acl or defaultObjectAcl
* properties, when it defaults to `"full"`.
* }
* @return Bucket
*/
public function restore(string $name, string $generation, array $options = [])
{
$res = $this->connection->restoreBucket([
'bucket' => $name,
'generation' => $generation,
] + $options);
return new Bucket(
$this->connection,
$name
);
}

/**
* Create a bucket. Bucket names must be unique as Cloud Storage uses a flat
* namespace. For more information please see
Expand Down
33 changes: 33 additions & 0 deletions Storage/tests/System/ManageBucketsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -491,4 +491,37 @@ public function hnsConfigs()
], true],
];
}

public function testSoftDeleteBucket()
{
$name = "soft-delete-bucket-" . uniqid();
$softDeleteBucket = self::createBucket(
self::$client,
$name,
[
'softDeletePolicy' => ['retentionDurationSeconds' => 8 * 24 * 60 * 60]
]
);

// Assert that the bucket was created correctly.
$this->assertEquals($name, $softDeleteBucket->name());
$generation = $softDeleteBucket->info()['generation'];

// Delete the bucket.
$softDeleteBucket->delete();
$this->assertFalse(self::$client->bucket($name)->exists());

// Retrieve the soft-deleted bucket by generation.
$softDeleteBucket->reload(['softDeleted' => true, 'generation' => $generation]);

// Assert that the retrieved bucket is the soft-deleted version.
$this->assertEquals($name, $softDeleteBucket->name());
$this->assertEquals($generation, $softDeleteBucket->info()['generation']);
$this->assertArrayHasKey('softDeleteTime', $softDeleteBucket->info());
$this->assertArrayHasKey('hardDeleteTime', $softDeleteBucket->info());

// Restore the soft-deleted bucket.
self::$client->restore($name, $generation);
$this->assertTrue(self::$client->bucket($name)->exists());
}
}
56 changes: 56 additions & 0 deletions Storage/tests/Unit/StorageClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,45 @@ public function testGetBucketRequesterPaysDefaultProjectId()
$bucket->reload();
}

public function testGetSoftDeletedBucket()
{
$this->connection->projectId()->willReturn(self::PROJECT);
$this->connection->getBucket(Argument::any())->shouldBeCalled()
->willReturn([
'name' => 'bucket1',
'generation' => 123456789,
'softDeleteTime' => '2024-09-10T01:01:01.045123456Z',
'hardDeleteTime' => '2024-09-17T01:01:01.045123456Z'
]);
$this->client->___setProperty('connection', $this->connection->reveal());
$bucket = $this->client->bucket('bucket1', true, ['softDeleted' => true, 'generation' => 123456789]);

$bucket->reload(['softDeleted' => true, 'generation' => 123456789]);

$this->assertEquals('bucket1', $bucket->name());
$this->assertEquals(123456789, $bucket->info()['generation']);
$this->assertArrayHasKey('softDeleteTime', $bucket->info());
$this->assertArrayHasKey('hardDeleteTime', $bucket->info());
}

public function testGetsSoftDeletedBuckets()
{
$this->connection->listBuckets(
Argument::withEntry('softDeleted', true)
)->willReturn([
'items' => [
['name' => 'bucket1']
]
]);
$this->connection->projectId()
->willReturn(self::PROJECT);

$this->client->___setProperty('connection', $this->connection->reveal());
$buckets = iterator_to_array($this->client->buckets(['softDeleted' => true]));

$this->assertEquals('bucket1', $buckets[0]->name());
}

public function testGetsBucketsWithoutToken()
{
$this->connection->listBuckets(Argument::any())->willReturn([
Expand Down Expand Up @@ -108,6 +147,23 @@ public function testGetsBucketsWithToken()
$this->assertEquals('bucket2', $bucket[1]->name());
}

public function testRestore()
{
$this->connection->restoreBucket(Argument::any())
->willReturn([
'bucket' => 'bucket1',
'info' => [
'generation' => 12345678
]
]);

$this->connection->projectId()
->willReturn(self::PROJECT);
$this->client->___setProperty('connection', $this->connection->reveal());

$this->assertInstanceOf(Bucket::class, $this->client->restore('bucket1', 123456789));
}

public function testCreatesBucket()
{
$this->connection->insertBucket(Argument::any())->willReturn(['name' => 'bucket']);
Expand Down

0 comments on commit 944287d

Please sign in to comment.