Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for restore token #2548

Merged
merged 8 commits into from
Oct 29, 2024
Merged
9 changes: 9 additions & 0 deletions src/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export interface GetFileMetadataCallback {
export interface GetFileOptions extends GetConfig {
userProject?: string;
generation?: number;
restoreToken?: string;
softDeleted?: boolean;
}

Expand Down Expand Up @@ -354,6 +355,7 @@ export interface FileOptions {
crc32cGenerator?: CRC32CValidatorGenerator;
encryptionKey?: string | Buffer;
generation?: number | string;
restoreToken?: string;
kmsKeyName?: string;
preconditionOpts?: PreconditionOptions;
userProject?: string;
Expand Down Expand Up @@ -450,6 +452,7 @@ export interface SetStorageClassCallback {

export interface RestoreOptions extends PreconditionOptions {
generation: number;
restoreToken?: string;
projection?: 'full' | 'noAcl';
}

Expand All @@ -471,6 +474,7 @@ export interface FileMetadata extends BaseMetadata {
eventBasedHold?: boolean | null;
readonly eventBasedHoldReleaseTime?: string;
generation?: string | number;
restoreToken?: string;
hardDeleteTime?: string;
kmsKeyName?: string;
md5Hash?: string;
Expand Down Expand Up @@ -547,6 +551,7 @@ class File extends ServiceObject<File, FileMetadata> {
name: string;

generation?: number;
restoreToken?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to have this field as both a class level and metadata level field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noticed that in some scenarios, a refresh token is associated with a specific generation ID. To ensure consistent behavior and avoid potential issues, I've implemented the refresh token handling in a way that aligns with the generation ID concept. This approach will help maintain data integrity and security.

parent!: Bucket;

private encryptionKey?: string | Buffer;
Expand Down Expand Up @@ -844,6 +849,8 @@ class File extends ServiceObject<File, FileMetadata> {
* @param {string} [options.userProject] The ID of the project which will be
* billed for the request.
* @param {number} [options.generation] The generation number to get
* @param {string} [options.restoreToken] If this is a soft-deleted object in an HNS-enabled bucket, returns the restore token which will
* be necessary to restore it if there's a name conflict with another object.
* @param {boolean} [options.softDeleted] If true, returns the soft-deleted object.
Object `generation` is required if `softDeleted` is set to True.
* @param {GetFileCallback} [callback] Callback function.
Expand Down Expand Up @@ -3707,6 +3714,8 @@ class File extends ServiceObject<File, FileMetadata> {
* @param {string} [userProject] The ID of the project which will be
* billed for the request.
* @param {number} [generation] If present, selects a specific revision of this object.
* @param {string} [restoreToken] Returns an option that must be specified when getting a soft-deleted object from an HNS-enabled
* bucket that has a naming and generation conflict with another object in the same bucket.
* @param {string} [projection] Specifies the set of properties to return. If used, must be 'full' or 'noAcl'.
* @param {string | number} [ifGenerationMatch] Request proceeds if the generation of the target resource
* matches the value used in the precondition.
Expand Down
73 changes: 73 additions & 0 deletions system-test/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ describe('storage', function () {

describe('soft-delete', () => {
let bucket: Bucket;
let hnsBucket: Bucket;
const SOFT_DELETE_RETENTION_SECONDS = 7 * 24 * 60 * 60; //7 days in seconds;

beforeEach(async () => {
Expand All @@ -802,11 +803,26 @@ describe('storage', function () {
retentionDurationSeconds: SOFT_DELETE_RETENTION_SECONDS,
},
});

hnsBucket = storage.bucket(generateName());
await storage.createBucket(hnsBucket.name, {
hierarchicalNamespace: {enabled: true},
iamConfiguration: {
uniformBucketLevelAccess: {
enabled: true,
},
},
softDeletePolicy: {
retentionDurationSeconds: SOFT_DELETE_RETENTION_SECONDS,
},
});
});

afterEach(async () => {
await bucket.deleteFiles({force: true, versions: true});
await bucket.delete();
await hnsBucket.deleteFiles({force: true, versions: true});
await hnsBucket.delete();
});

it('should set softDeletePolicy correctly', async () => {
Expand Down Expand Up @@ -862,6 +878,63 @@ describe('storage', function () {
[files] = await bucket.getFiles();
assert.strictEqual(files.length, 1);
});

it('should LIST soft-deleted files with restore token', async () => {
const f1 = hnsBucket.file('file5a');
const f2 = hnsBucket.file('file5b');
await f1.save('file5a');
await f2.save('file5b');
await f1.delete();
await f2.delete();
const [notSoftDeletedFiles] = await hnsBucket.getFiles();
assert.strictEqual(notSoftDeletedFiles.length, 0);
const [softDeletedFiles] = await hnsBucket.getFiles({softDeleted: true});
assert.strictEqual(softDeletedFiles.length, 2);
assert.notStrictEqual(
softDeletedFiles![0].metadata.restoreToken,
undefined
);
});

it('should GET a soft-deleted file with restore token', async () => {
const f1 = hnsBucket.file('file6');
await f1.save('file6');
const [metadata] = await f1.getMetadata();
await f1.delete();
const [softDeletedFile] = await f1.get({
softDeleted: true,
generation: parseInt(metadata.generation?.toString() || '0'),
});
assert(softDeletedFile);
assert.strictEqual(
softDeletedFile.metadata.generation,
metadata.generation
);
assert.notStrictEqual(softDeletedFile.metadata.restoreToken, undefined);
});

it('should restore a soft-deleted file using restoreToken', async () => {
const f1 = hnsBucket.file('file7');
await f1.save('file7');
const [metadata] = await f1.getMetadata();
await f1.delete();
let [files] = await hnsBucket.getFiles();
assert.strictEqual(files.length, 0);
const [softDeletedFile] = await f1.get({
softDeleted: true,
generation: parseInt(metadata.generation?.toString() || '0'),
});
assert(softDeletedFile);
const restoredFile = await f1.restore({
generation: parseInt(
softDeletedFile.metadata.generation?.toString() || '0'
),
restoreToken: softDeletedFile.metadata.restoreToken,
});
assert(restoredFile);
[files] = await hnsBucket.getFiles();
assert.strictEqual(files.length, 1);
});
});

describe('dual-region', () => {
Expand Down
1 change: 0 additions & 1 deletion test/transfer-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
MultiPartUploadError,
MultiPartUploadHelper,
UploadOptions,
UploadManyFilesOptions,
TransferManager,
Storage,
DownloadResponse,
Expand Down
Loading