Skip to content

Commit

Permalink
feat: implement object retention lock for bucket / files (#2365)
Browse files Browse the repository at this point in the history
  • Loading branch information
ddelgrosso1 authored Nov 29, 2023
1 parent 65950f3 commit c140868
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 1 deletion.
3 changes: 3 additions & 0 deletions src/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,9 @@ export interface BucketMetadata extends BaseMetadata {
};
metageneration?: string;
name?: string;
objectRetention?: {
mode?: string;
};
owner?: {
entity?: string;
entityId?: string;
Expand Down
12 changes: 11 additions & 1 deletion src/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ export interface FileMetadata extends BaseMetadata {
};
customTime?: string;
eventBasedHold?: boolean | null;
readonly eventBasedHoldReleaseTime?: string;
generation?: string | number;
kmsKeyName?: string;
md5Hash?: string;
Expand All @@ -436,6 +437,10 @@ export interface FileMetadata extends BaseMetadata {
entity?: string;
entityId?: string;
};
retention?: {
retainUntilTime?: string;
mode?: string;
} | null;
retentionExpirationTime?: string;
size?: string | number;
storageClass?: string;
Expand Down Expand Up @@ -3813,7 +3818,8 @@ class File extends ServiceObject<File, FileMetadata> {
optionsOrCallback: SetMetadataOptions | MetadataCallback<FileMetadata>,
cb?: MetadataCallback<FileMetadata>
): Promise<SetMetadataResponse<FileMetadata>> | void {
const options =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options: any =
typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
cb =
typeof optionsOrCallback === 'function'
Expand All @@ -3826,6 +3832,10 @@ class File extends ServiceObject<File, FileMetadata> {
options
);

if (metadata.retention !== undefined) {
options.overrideUnlockedRetention = true;
}

super
.setMetadata(metadata, options)
.then(resp => cb!(null, ...resp))
Expand Down
8 changes: 8 additions & 0 deletions src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface GetServiceAccountCallback {
export interface CreateBucketQuery {
project: string;
userProject: string;
enableObjectRetention: boolean;
}

export enum IdempotencyStrategy {
Expand Down Expand Up @@ -121,6 +122,7 @@ export interface CreateBucketRequest {
cors?: Cors[];
customPlacementConfig?: CustomPlacementConfig;
dra?: boolean;
enableObjectRetention?: boolean;
location?: string;
multiRegional?: boolean;
nearline?: boolean;
Expand Down Expand Up @@ -862,6 +864,7 @@ export class Storage extends Service {
* For more information, see {@link https://cloud.google.com/storage/docs/locations| Bucket Locations}.
* @property {boolean} [dra=false] Specify the storage class as Durable Reduced
* Availability.
* @property {boolean} [enableObjectRetention=false] Specifiy whether or not object retention should be enabled on this bucket.
* @property {string} [location] Specify the bucket's location. If specifying
* a dual-region, the `customPlacementConfig` property should be set in conjunction.
* For more information, see {@link https://cloud.google.com/storage/docs/locations| Bucket Locations}.
Expand Down Expand Up @@ -1023,6 +1026,11 @@ export class Storage extends Service {
delete body.userProject;
}

if (body.enableObjectRetention) {
query.enableObjectRetention = body.enableObjectRetention;
delete body.enableObjectRetention;
}

this.request(
{
method: 'POST',
Expand Down
45 changes: 45 additions & 0 deletions system-test/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,51 @@ describe('storage', () => {
});
});

describe('object retention lock', () => {
const fileName = generateName();
let objectRetentionBucket: Bucket;

before(async () => {
objectRetentionBucket = storage.bucket(generateName());
});

after(async () => {
await objectRetentionBucket.deleteFiles({force: true});
await objectRetentionBucket.delete();
});

it('should create a bucket with object retention enabled', async () => {
const result = await objectRetentionBucket.create({
enableObjectRetention: true,
});

assert.deepStrictEqual(result[0].metadata.objectRetention, {
mode: 'Enabled',
});
});

it('should create a file with object retention enabled', async () => {
const time = new Date();
time.setMinutes(time.getSeconds() + 1);
const retention = {mode: 'Unlocked', retainUntilTime: time.toISOString()};
const file = new File(objectRetentionBucket, fileName);
await objectRetentionBucket.upload(FILES.big.path, {
metadata: {
retention,
},
destination: fileName,
});
const [metadata] = await file.getMetadata();
assert.deepStrictEqual(metadata.retention, retention);
});

it('should disable object retention on the file', async () => {
const file = new File(objectRetentionBucket, fileName);
const [metadata] = await file.setMetadata({retention: null});
assert.strictEqual(metadata.retention, undefined);
});
});

describe('requester pays', () => {
const HAS_2ND_PROJECT =
process.env.GCN_STORAGE_2ND_PROJECT_ID !== undefined;
Expand Down
14 changes: 14 additions & 0 deletions test/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4567,6 +4567,20 @@ describe('File', () => {
});
});

describe('setMetadata', () => {
it('should set query parameter overrideUnlockedRetention', done => {
const newFile = new File(BUCKET, 'new-file');

newFile.parent.request = (reqOpts: DecorateRequestOptions) => {
console.log(reqOpts.qs);
assert.strictEqual(reqOpts.qs.overrideUnlockedRetention, true);
done();
};

newFile.setMetadata({retention: null}, assert.ifError);
});
});

describe('setStorageClass', () => {
const STORAGE_CLASS = 'new_storage_class';

Expand Down
11 changes: 11 additions & 0 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,17 @@ describe('Storage', () => {
}, /Both `coldline` and `storageClass` were provided./);
});

it('should allow enabling object retention', done => {
storage.request = (
reqOpts: DecorateRequestOptions,
callback: Function
) => {
assert.strictEqual(reqOpts.qs.enableObjectRetention, true);
callback();
};
storage.createBucket(BUCKET_NAME, {enableObjectRetention: true}, done);
});

describe('storage classes', () => {
it('should expand metadata.archive', done => {
storage.request = (reqOpts: DecorateRequestOptions) => {
Expand Down

0 comments on commit c140868

Please sign in to comment.