Skip to content

Commit

Permalink
feat(Storage): support object retention lock (#6829)
Browse files Browse the repository at this point in the history
  • Loading branch information
vishwarajanand authored Dec 8, 2023
1 parent 8e329e8 commit b92c658
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 0 deletions.
13 changes: 13 additions & 0 deletions Storage/src/Bucket.php
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,19 @@ public function exists(array $options = [])
* Acceptable values include, `"authenticatedRead"`,
* `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`,
* `"projectPrivate"`, and `"publicRead"`.
* @type array $retention The full list of available options are outlined
* at the [JSON API docs](https://cloud.google.com/storage/docs/json_api/v1/objects/insert#request-body).
* @type string $retention.retainUntilTime The earliest time in RFC 3339
* UTC "Zulu" format that the object can be deleted or replaced.
* This is the retention configuration set for this object.
* @type string $retention.mode The mode of the retention configuration,
* which can be either `"Unlocked"` or `"Locked"`.
* @type string $retentionExpirationTime The earliest time in
* RFC 3339 UTC "Zulu" format that the object can be deleted or
* replaced. This depends on any retention configuration set for
* the object as well as retention policy set for the bucket that
* contains the object. This value should normally only be set by
* the back-end API.
* @type array $metadata The full list of available options are outlined
* at the [JSON API docs](https://cloud.google.com/storage/docs/json_api/v1/objects/insert#request-body).
* @type array $metadata.metadata User-provided metadata, in key/value pairs.
Expand Down
6 changes: 6 additions & 0 deletions Storage/src/Connection/Rest.php
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,12 @@ private function resolveUploadOptions(array $args)
}

$args['metadata']['name'] = $args['name'];
if (isset($args['retention'])) {
// during object creation retention properties go into metadata
// but not into request body
$args['metadata']['retention'] = $args['retention'];
unset($args['retention']);
}
unset($args['name']);
$args['contentType'] = $args['metadata']['contentType']
?? MimeType::fromFilename($args['metadata']['name']);
Expand Down
4 changes: 4 additions & 0 deletions Storage/src/StorageClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ function (array $bucket) use ($userProject) {
* `"projectPrivate"`, and `"publicRead"`.
* @type string $predefinedDefaultObjectAcl Apply a predefined set of
* default object access controls to this bucket.
* @type bool $enableObjectRetention Whether object retention should
* be enabled on this bucket. For more information, refer to the
* [Object Retention Lock](https://cloud.google.com/storage/docs/object-lock)
* documentation.
* @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
Expand Down
18 changes: 18 additions & 0 deletions Storage/src/StorageObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,24 @@ public function delete(array $options = [])
* Acceptable values include, `"authenticatedRead"`,
* `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`,
* `"projectPrivate"`, and `"publicRead"`.
* @type array $retention The full list of available options are outlined
* at the [JSON API docs](https://cloud.google.com/storage/docs/json_api/v1/objects/update#request-body).
* @type string $retention.retainUntilTime The earliest time in RFC 3339
* UTC "Zulu" format that the object can be deleted or replaced.
* This is the retention configuration set for this object.
* @type string $retention.mode The mode of the retention configuration,
* which can be either `"Unlocked"` or `"Locked"`.
* @type string $retentionExpirationTime The earliest time in
* RFC 3339 UTC "Zulu" format that the object can be deleted or
* replaced. This depends on any retention configuration set for
* the object as well as retention policy set for the bucket that
* contains the object. This value should normally only be set by
* the back-end API.
* @type bool $overrideUnlockedRetention Applicable for objects that
* have an unlocked retention configuration. Required to be set to
* `true` if the operation includes a retention property that
* changes the mode to `Locked`, reduces the `retainUntilTime`, or
* removes the retention configuration from the object.
* @type string $projection Determines which properties to return. May
* be either 'full' or 'noAcl'.
* @type string $fields Selector which will cause the response to only
Expand Down
131 changes: 131 additions & 0 deletions Storage/tests/System/ManageObjectsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,137 @@ public function testListsObjectsWithMatchGlob()
}
}

public function testObjectRetentionLockedMode()
{
// Bucket with object retention locked mode is not cleaned up immediately
$bucket = self::$client->createBucket(uniqid('object-retention-locked-'), [
'enableObjectRetention' => true
]);

// Test create object with object retention enabled
$objectName = "object-retention-lock";
$time = (new \DateTime)->add(
\DateInterval::createFromDateString('+2 hours')
);
$object = $bucket->upload(self::DATA, [
'name' => $objectName,
'retention' => [
'mode' => 'Locked',
'retainUntilTime' => $time->format(\DateTime::RFC3339)
]
]);
$this->assertEquals('Locked', $object->info()['retention']['mode']);

$laterTime = (new \DateTime)->add(
\DateInterval::createFromDateString('+4 hours')
);

// retainUntilTime of a locked mode object can be increased
$object->update([
'retention' => [
'mode' => 'Locked',
'retainUntilTime' => $laterTime->format(\DateTime::RFC3339)
]
]);
$this->assertEqualsWithDelta(
$laterTime,
new \DateTime($object->info()['retention']['retainUntilTime']),
1
);

// retainUntilTime of a locked mode object can not be decreased
$exception = null;
try {
$object->update([
'retention' => [
'mode' => 'Locked',
'retainUntilTime' => $time->format(\DateTime::RFC3339)
]
]);
} catch (ServiceException $e) {
$exception = $e;
}
$this->assertInstanceOf(ServiceException::class, $exception);
$this->assertStringContainsString(
'retention period cannot be shortened',
$exception->getMessage()
);
$this->assertEqualsWithDelta(
$laterTime,
new \DateTime($object->info()['retention']['retainUntilTime']),
1
);

// Retention mode of a locked mode object can not be changed
$exception = null;
try {
$object->update([
'retention' => [
'mode' => 'Unlocked',
'retainUntilTime' => $laterTime->format(\DateTime::RFC3339)
],
'overrideUnlockedRetention' => true
]);
} catch (ServiceException $e) {
$exception = $e;
}
$this->assertInstanceOf(ServiceException::class, $exception);
$this->assertStringContainsString(
'retention mode cannot be changed',
$exception->getMessage()
);
}

public function testObjectRetentionUnlockedMode()
{
// Test bucket created with object retention enabled
$bucket = self::createBucket(self::$client, uniqid('object-retention-'), [
'enableObjectRetention' => true
]);
$this->assertEquals('Enabled', $bucket->info()['objectRetention']['mode']);

// Test create object with object retention enabled
$objectName = "object-retention-lock";
$expires = (new \DateTime)->add(
\DateInterval::createFromDateString('+2 hours')
);
$uploader = $bucket->getStreamableUploader('initial contents', [
'name' => $objectName,
'retention' => [
'mode' => 'Unlocked',
'retainUntilTime' => $expires->format(\DateTime::RFC3339)
]
]);
$uploader->upload();
$object = $bucket->object($objectName);
$this->assertEquals('Unlocked', $object->info()['retention']['mode']);

// Object delete throws when object has a valid retention policy
$exception = null;
try {
$object->delete();
} catch (ServiceException $e) {
$exception = $e;
}
$this->assertInstanceOf(ServiceException::class, $exception);
$this->assertStringContainsString(
'cannot be deleted or overwritten',
$exception->getMessage()
);
$this->assertTrue($object->exists());

// Disable object retention
$object->update([
'retention' => [],
'overrideUnlockedRetention' => true
]);
$this->assertNotContains('retention', $object->info());

// Object delete succeeds when object retention is disabled
$object->delete();
$this->assertFalse($object->exists());
}

public function testObjectExists()
{
$object = self::$bucket->upload(self::DATA, ['name' => uniqid(self::TESTING_PREFIX)]);
Expand Down

0 comments on commit b92c658

Please sign in to comment.