Skip to content

Commit

Permalink
feat(Storage): softDelete object in gcs buckets (#7154)
Browse files Browse the repository at this point in the history
  • Loading branch information
vishwarajanand committed Mar 18, 2024
1 parent ea1913d commit 09ea3af
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 5 deletions.
42 changes: 42 additions & 0 deletions Storage/src/Bucket.php
Expand Up @@ -614,6 +614,48 @@ public function object($name, array $options = [])
);
}

/**
* Restores an object.
*
* Example:
* ```
* $object = $bucket->restore('file.txt');
* ```
*
* @param string $name The name of the object to restore.
* @param string $generation Request a specific generation of the object.
* @param array $options [optional] {
* Configuration Options.
*
* @type string $ifGenerationMatch Makes the operation conditional on whether
* the object's current generation matches the given value.
* @type string $ifGenerationNotMatch Makes the operation conditional on whether
* the object's current generation matches the given value.
* @type string $ifMetagenerationMatch If set, only restores
* if its metageneration matches this value.
* @type string $ifMetagenerationNotMatch If set, only restores
* if its metageneration does not match this value.
* }
* @return StorageObject
*/
public function restore($name, $generation, array $options = [])
{
$res = $this->connection->restoreObject([
'bucket' => $this->identity['bucket'],
'generation' => $generation,
'object' => $name,
] + $options);
return new StorageObject(
$this->connection,
$name,
$this->identity['bucket'],
$res['generation'], // restored object will have a new generation
$res + array_filter([
'requesterProjectId' => $this->identity['userProject']
])
);
}

/**
* Fetches all objects in the bucket.
*
Expand Down
5 changes: 5 additions & 0 deletions Storage/src/Connection/ConnectionInterface.php
Expand Up @@ -95,6 +95,11 @@ public function patchBucket(array $args = []);
*/
public function deleteObject(array $args = []);

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

/**
* @param array $args
*/
Expand Down
26 changes: 21 additions & 5 deletions Storage/src/Connection/Rest.php
Expand Up @@ -228,6 +228,14 @@ public function deleteObject(array $args = [])
return $this->send('objects', 'delete', $args);
}

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

/**
* @param array $args
*/
Expand Down Expand Up @@ -614,14 +622,22 @@ private function buildDownloadObjectParams(array $args)
'restDelayFunction' => null
]);

$queryOptions = [
'generation' => $args['generation'],
'alt' => 'media',
'userProject' => $args['userProject'],
];
if (isset($args['softDeleted'])) {
// alt param cannot be specified with softDeleted param. See:
// https://cloud.google.com/storage/docs/json_api/v1/objects/get
unset($args['alt']);
$queryOptions['softDeleted'] = $args['softDeleted'];
}

$uri = $this->expandUri($this->apiEndpoint . self::DOWNLOAD_PATH, [
'bucket' => $args['bucket'],
'object' => $args['object'],
'query' => [
'generation' => $args['generation'],
'alt' => 'media',
'userProject' => $args['userProject']
]
'query' => $queryOptions,
]);

return [
Expand Down
18 changes: 18 additions & 0 deletions Storage/tests/Snippet/BucketTest.php
Expand Up @@ -519,6 +519,24 @@ public function testDelete()
$snippet->invoke();
}

public function testRestore()
{
$snippet = $this->snippetFromMethod(Bucket::class, 'restore');
$snippet->addLocal('bucket', $this->bucket);

$this->connection->restoreObject(Argument::any())
->willReturn([
'name' => 'file.txt',
'generation' => 'abc'
]);

$restoredObject = $this->bucket->restore('file.txt', 'abc');

$this->assertInstanceOf(StorageObject::class, $restoredObject);
$this->assertEquals('file.txt', $restoredObject->name());
$this->assertEquals('abc', $restoredObject->info()['generation']);
}

public function testUpdate()
{
$snippet = $this->snippetFromMethod(Bucket::class, 'update');
Expand Down
26 changes: 26 additions & 0 deletions Storage/tests/System/ManageBucketsTest.php
Expand Up @@ -117,6 +117,32 @@ public function testUpdateBucket()
$this->assertEquals($options['website'], $info['website']);
}

public function testSoftDeletePolicy()
{
$durationSecond = 8*24*60*60;
// set soft delete policy
self::$bucket->update([
'softDeletePolicy' => [
'retentionDurationSeconds' => $durationSecond
]
]);
$this->assertArrayHasKey('softDeletePolicy', self::$bucket->info());
$this->assertEquals(
$durationSecond,
self::$bucket->info()['softDeletePolicy']['retentionDurationSeconds']
);

// remove soft delete policy
self::$bucket->update([
'softDeletePolicy' => []
]);
$this->assertArrayHasKey('softDeletePolicy', self::$bucket->info());
$this->assertEquals(
0,
self::$bucket->info()['softDeletePolicy']['retentionDurationSeconds']
);
}

/**
* @group storage-bucket-lifecycle
* @dataProvider lifecycleRules
Expand Down
47 changes: 47 additions & 0 deletions Storage/tests/System/ManageObjectsTest.php
Expand Up @@ -284,6 +284,36 @@ public function testComposeObjects($object)

$this->assertEquals($name, $composedObject->name());
$this->assertEquals($expectedContent, $composedObject->downloadAsString());
return $composedObject;
}

/**
* @depends testComposeObjects
*/
public function testSoftDeleteObject($object)
{
// Set soft delete policy
self::$bucket->update([
'softDeletePolicy' => [
'retentionDurationSeconds' => 8*24*60*60
]
]);

$this->assertStorageObjectExists($object);
$generation = $object->info()['generation'];

$object->delete();

$this->assertStorageObjectNotExists($object);
$this->assertStorageObjectExists($object, [
'softDeleted' => true,
'generation' => $generation
]);

$restoredObject = self::$bucket->restore($object->name(), $generation);
$this->assertNotEquals($generation, $restoredObject->info()['generation']);

$this->assertStorageObjectExists($restoredObject);
}

public function testRotatesCustomerSuppliedEncrpytion()
Expand Down Expand Up @@ -414,4 +444,21 @@ public function testStringNormalization()
$this->assertSame($expectedContent, $actualContent);
}
}

private function assertStorageObjectExists($object, $options = [], $isPresent = true)
{
$this->assertEquals($isPresent, $object->exists($options));
$object = self::$bucket->object($object->name(), $options);
$this->assertEquals($isPresent, $object->exists($options));
$objects = self::$bucket->objects($options);
$objects = array_map(function ($o) {
return $o->name();
}, iterator_to_array($objects));
$this->assertEquals($isPresent, in_array($object->name(), $objects));
}

private function assertStorageObjectNotExists($object, $options = [])
{
$this->assertStorageObjectExists($object, $options, false);
}
}
16 changes: 16 additions & 0 deletions Storage/tests/Unit/BucketTest.php
Expand Up @@ -275,6 +275,22 @@ public function testDelete()
$this->assertNull($bucket->delete());
}

public function testRestore()
{
$this->connection->restoreObject(Argument::any())
->willReturn([
'name' => 'file.txt',
'generation' => 'abc'
]);

$bucket = $this->getBucket();
$restoredObject = $bucket->restore('file.txt', 'abc');

$this->assertInstanceOf(StorageObject::class, $restoredObject);
$this->assertEquals('file.txt', $restoredObject->name());
$this->assertEquals('abc', $restoredObject->info()['generation']);
}

public function testComposeThrowsExceptionWithLessThanTwoSources()
{
$this->expectException(InvalidArgumentException::class);
Expand Down

0 comments on commit 09ea3af

Please sign in to comment.