Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading