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

Issue 815 batch restore of objects for GCP #816

Open
wants to merge 11 commits into
base: issue_792_batch_restore_s3
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
import com.epam.pipeline.common.MessageConstants;
import com.epam.pipeline.common.MessageHelper;
import com.epam.pipeline.config.JsonMapper;
import com.epam.pipeline.controller.vo.data.storage.RestoreFolderVO;
import com.epam.pipeline.entity.datastorage.AbstractDataStorageItem;
import com.epam.pipeline.entity.datastorage.ActionStatus;
import com.epam.pipeline.entity.datastorage.DataStorageDownloadFileUrl;
import com.epam.pipeline.entity.datastorage.DataStorageException;
import com.epam.pipeline.entity.datastorage.DataStorageFile;
import com.epam.pipeline.entity.datastorage.DataStorageFolder;
import com.epam.pipeline.entity.datastorage.DataStorageItemContent;
import com.epam.pipeline.entity.datastorage.DataStorageItemType;
import com.epam.pipeline.entity.datastorage.DataStorageListing;
import com.epam.pipeline.entity.datastorage.DataStorageStreamingContent;
import com.epam.pipeline.entity.datastorage.PathDescription;
Expand Down Expand Up @@ -61,6 +63,7 @@
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;

import java.io.IOException;
Expand Down Expand Up @@ -378,6 +381,72 @@ public void restoreFileVersion(final GSBucketStorage storage, final String path,
deleteBlob(blob, client, true);
}

public void restoreFolder(final GSBucketStorage storage, final String path,
final RestoreFolderVO restoreFolderVO) {
final Storage client = gcpClient.buildStorageClient(region);
final String bucketName = storage.getPath();
cleanDeleteMarkers(client, bucketName, path, restoreFolderVO);
}

private void cleanDeleteMarkers(final Storage client,
final String bucketName, final String requestPath,
final RestoreFolderVO restoreFolderVO) {
String folderPath = Optional.ofNullable(requestPath).orElse(EMPTY_PREFIX);
if (StringUtils.isNotBlank(folderPath)) {
folderPath = normalizeFolderPath(requestPath);
}
final Page<Blob> blobs = client.list(bucketName,
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it would be better to move this logic into separate method and reuse it into listItems methd?

Copy link
Author

Choose a reason for hiding this comment

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

Did it, now it looks little bit better

Storage.BlobListOption.versions(true),
Storage.BlobListOption.currentDirectory(),
Storage.BlobListOption.prefix(folderPath),
Storage.BlobListOption.pageToken(EMPTY_PREFIX),
Storage.BlobListOption.pageSize(Integer.MAX_VALUE));
Assert.isTrue(Objects.nonNull(blobs) && blobs.iterateAll().iterator().hasNext(), messageHelper
.getMessage(MessageConstants.ERROR_DATASTORAGE_PATH_NOT_FOUND, folderPath, bucketName));
listItemsWithVersions(blobs)
.forEach(item -> {
recursiveRestoreFolderCall(item, client, bucketName, restoreFolderVO);
if (isFileWithDeleteMarkerAndShouldBeRestore(item, restoreFolderVO)) {
final Blob blob = checkBlobExistsAndGet(bucketName, item.getPath(), client,
removeDeletedMarkerFromVersion(((DataStorageFile) item).getVersion()));
final Storage.CopyRequest request = Storage.CopyRequest.newBuilder()
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it would be better to create a new method (wth copy bob logic) and reuse it into restoreFileVersion?

Copy link
Author

Choose a reason for hiding this comment

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

done

.setSource(blob.getBlobId())
.setSourceOptions(Storage.BlobSourceOption.generationMatch())
.setTarget(BlobId.of(bucketName, item.getPath()))
.build();
client.copy(request).getResult();
}
});
}

private String removeDeletedMarkerFromVersion(final String versionWithDeletedMarker) {
if (latestVersionHasDeletedMarker(versionWithDeletedMarker)) {
return versionWithDeletedMarker.substring(0, versionWithDeletedMarker.length() - 2);
}
throw new DataStorageException(
String.format("Corresponded version: '%s' should has deleted marker: '%s'", versionWithDeletedMarker,
LATEST_VERSION_DELETION_MARKER));
}

private void recursiveRestoreFolderCall(final AbstractDataStorageItem item, final Storage client,
final String bucketName, final RestoreFolderVO restoreFolderVO) {
if (item.getType() == DataStorageItemType.Folder && restoreFolderVO.isRecursively()) {
cleanDeleteMarkers(client, bucketName, item.getPath(), restoreFolderVO);
}
}

private boolean isFileWithDeleteMarkerAndShouldBeRestore(final AbstractDataStorageItem item,
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should move this method into ProviderUtils to reuse it into S3Helper ?

Copy link
Author

Choose a reason for hiding this comment

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

good idea, replaced

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should move this method into ProviderUtils class and reuse it into S3Helper?

Copy link
Author

Choose a reason for hiding this comment

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

duplicate)

final RestoreFolderVO restoreFolderVO) {
final AntPathMatcher matcher = new AntPathMatcher();
return item.getType() == DataStorageItemType.File &&
((DataStorageFile) item).getDeleteMarker() &&
((DataStorageFile) item).getVersion() != null &&
Optional.ofNullable(restoreFolderVO.getIncludeList()).map(includeList -> includeList.stream()
.anyMatch(pattern -> matcher.match(pattern, item.getName()))).orElse(true) &&
Optional.ofNullable(restoreFolderVO.getExcludeList()).map(excludeList -> excludeList.stream()
.noneMatch(pattern -> matcher.match(pattern, item.getName()))).orElse(true);
}

public void applyStoragePolicy(final GSBucketStorage storage, final StoragePolicy policy) {
final Storage client = gcpClient.buildStorageClient(region);
final String bucketName = storage.getPath();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void restoreFileVersion(final GSBucketStorage dataStorage, final String p
@Override
public void restoreFolder(final GSBucketStorage dataStorage, final String path,
final RestoreFolderVO restoreFolderVO) throws DataStorageException {
throw new UnsupportedOperationException();
getHelper(dataStorage).restoreFolder(dataStorage, path, restoreFolderVO);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package com.epam.pipeline.manager.datastorage.providers.gcp;

import com.epam.pipeline.common.MessageHelper;
import com.epam.pipeline.controller.vo.data.storage.RestoreFolderVO;
import com.epam.pipeline.entity.datastorage.gcp.GSBucketStorage;
import com.epam.pipeline.entity.region.GCPRegion;
import com.epam.pipeline.manager.cloud.gcp.GCPClient;
import com.google.api.gax.paging.Page;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.CopyWriter;
import com.google.cloud.storage.Storage;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.joda.time.LocalDate;
import org.junit.Before;
import org.junit.Test;

import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@SuppressWarnings({"unchecked", "PMD.TooManyStaticImports"})
public class GSBucketStorageHelperTest {
private static final String EMPTY_PREFIX = "";
private static final String BUCKET = "bucket";
private static final String OLD_PATH = "oldPath/";
private static final String NEW_PATH = "newPath/";
private static final Long VERSION = 123456789L;
private static final String FIRST_FILE_PATH = OLD_PATH + "firstFile.jpg";
private static final String SECOND_FILE_PATH = OLD_PATH + NEW_PATH + "secondFile.png";
private static final String JPG_PATTERN = "*.jpg";
private static final Long DATE_IN_MILLISECONDS = new Date().getTime();

private final MessageHelper messageHelper = mock(MessageHelper.class);
private final GCPRegion region = new GCPRegion();
private final GCPClient gcpClient = mock(GCPClient.class);
private final GSBucketStorage dataStorage = mock(GSBucketStorage.class);

private final GSBucketStorageHelper storageHelper = spy(
new GSBucketStorageHelper(messageHelper, region, gcpClient));
private final Storage client = mock(Storage.class);

@Before
public void setUp() throws Exception {
doReturn(client).when(gcpClient).buildStorageClient(region);
when(dataStorage.getPath()).thenReturn(BUCKET);
}

@Test
public void testRestoreFolderWithExcludeListShouldNotRestoreExcludeFiles() {
storageHelper.restoreFolder(dataStorage, OLD_PATH,
presettingForRestoreFolderMethodTests(true, null,
Collections.singletonList(JPG_PATTERN), null));
verify(client).copy(argThat(hasSourceAndDestination(
BlobId.of(BUCKET, SECOND_FILE_PATH, VERSION), SECOND_FILE_PATH)));
}

@Test
public void testRestoreFolderWithIncludeListAndWithRecursionShouldRestoreOnlyIncludeFiles() {
storageHelper.restoreFolder(dataStorage, OLD_PATH,
presettingForRestoreFolderMethodTests(true,
Collections.singletonList("*.png"), null, null));
verify(client).copy(argThat(hasSourceAndDestination(
BlobId.of(BUCKET, SECOND_FILE_PATH, VERSION), SECOND_FILE_PATH)));
}

@Test
public void testRestoreFolderWithIncludeListAndNoRecursionShouldRestoreOnlyIncludeFiles() {
storageHelper.restoreFolder(dataStorage, OLD_PATH,
presettingForRestoreFolderMethodTests(false,
Collections.singletonList(JPG_PATTERN), null, null));
verify(client).copy(argThat(hasSourceAndDestination(
BlobId.of(BUCKET, FIRST_FILE_PATH, VERSION), FIRST_FILE_PATH)));
}

@Test
public void testRestoreFolderShouldRestoreOnlyFilesWithDeleteMarker() {
final BlobId withoutDeleteMarkerFileID = BlobId.of(BUCKET, FIRST_FILE_PATH, VERSION);
final Blob withoutDeleteMarkerFile = mock(Blob.class);
when(withoutDeleteMarkerFile.getName()).thenReturn(FIRST_FILE_PATH);
when(withoutDeleteMarkerFile.getGeneration()).thenReturn(VERSION);
when(withoutDeleteMarkerFile.getBlobId()).thenReturn(withoutDeleteMarkerFileID);
when(withoutDeleteMarkerFile.getDeleteTime()).thenReturn(null); //exclusion condition
when(client.get(withoutDeleteMarkerFileID)).thenReturn(withoutDeleteMarkerFile);

storageHelper.restoreFolder(dataStorage, OLD_PATH,
presettingForRestoreFolderMethodTests(true, null, null, withoutDeleteMarkerFile));
verify(client).copy(argThat(hasSourceAndDestination(
BlobId.of(BUCKET, FIRST_FILE_PATH, VERSION), FIRST_FILE_PATH)));
verify(client).copy(argThat(hasSourceAndDestination(
BlobId.of(BUCKET, SECOND_FILE_PATH, VERSION), SECOND_FILE_PATH)));
}

@Test
public void testRestoreFolderShouldRestoreOnlyLastFileVersion() {
final BlobId firstFileOldVersionID = BlobId.of(BUCKET, FIRST_FILE_PATH, VERSION);
final Blob firstFileOldVersion = mock(Blob.class);
when(firstFileOldVersion.getName()).thenReturn(FIRST_FILE_PATH);
when(firstFileOldVersion.getGeneration()).thenReturn(VERSION);
when(firstFileOldVersion.getBlobId()).thenReturn(firstFileOldVersionID);
when(firstFileOldVersion.getUpdateTime()).thenReturn(LocalDate.parse("1995-01-24").toDate().getTime());
when(firstFileOldVersion.getDeleteTime()).thenReturn(DATE_IN_MILLISECONDS);
when(client.get(firstFileOldVersionID)).thenReturn(firstFileOldVersion);

storageHelper.restoreFolder(dataStorage, OLD_PATH,
presettingForRestoreFolderMethodTests(true, null, null, firstFileOldVersion));
verify(client).copy(argThat(hasSourceAndDestination(
BlobId.of(BUCKET, FIRST_FILE_PATH, VERSION), FIRST_FILE_PATH)));
verify(client).copy(argThat(hasSourceAndDestination(
BlobId.of(BUCKET, SECOND_FILE_PATH, VERSION), SECOND_FILE_PATH)));
}

@Test
public void testRestoreFolderWithRecursionShouldLoopAllFolders() {
storageHelper.restoreFolder(dataStorage, OLD_PATH,
presettingForRestoreFolderMethodTests(true, null, null, null));
verify(client).copy(argThat(hasSourceAndDestination(
BlobId.of(BUCKET, FIRST_FILE_PATH, VERSION), FIRST_FILE_PATH)));
verify(client).copy(argThat(hasSourceAndDestination(
BlobId.of(BUCKET, SECOND_FILE_PATH, VERSION), SECOND_FILE_PATH)));

}

private RestoreFolderVO presettingForRestoreFolderMethodTests(final boolean recursive,
final List<String> includeList,
final List<String> excludeList,
final Blob optionalBlob) {
final String firstFolderPath = OLD_PATH;
final BlobId firstFolderID = BlobId.of(BUCKET, firstFolderPath, null);
final Blob firstFolder = mock(Blob.class);
when(firstFolder.isDirectory()).thenReturn(true);
when(firstFolder.getName()).thenReturn(firstFolderPath);
when(client.get(firstFolderID)).thenReturn(firstFolder);

final BlobId firstFileID = BlobId.of(BUCKET, FIRST_FILE_PATH, VERSION);
final Blob firstFile = mock(Blob.class);
when(firstFile.getName()).thenReturn(FIRST_FILE_PATH);
when(firstFile.getGeneration()).thenReturn(VERSION);
when(firstFile.getBlobId()).thenReturn(firstFileID);
when(firstFile.getUpdateTime()).thenReturn(DATE_IN_MILLISECONDS);
when(firstFile.getDeleteTime()).thenReturn(DATE_IN_MILLISECONDS);
when(client.get(firstFileID)).thenReturn(firstFile);

final String secondFolderPath = OLD_PATH + NEW_PATH;
final BlobId secondFolderID = BlobId.of(BUCKET, secondFolderPath, null);
final Blob secondFolder = mock(Blob.class);
when(secondFolder.isDirectory()).thenReturn(true);
when(secondFolder.getName()).thenReturn(secondFolderPath);
when(client.get(secondFolderID)).thenReturn(secondFolder);

final BlobId secondFileID = BlobId.of(BUCKET, SECOND_FILE_PATH, VERSION);
final Blob secondFile = mock(Blob.class);
when(secondFile.getName()).thenReturn(SECOND_FILE_PATH);
when(secondFile.getGeneration()).thenReturn(VERSION);
when(secondFile.getBlobId()).thenReturn(secondFileID);
when(secondFile.getUpdateTime()).thenReturn(DATE_IN_MILLISECONDS);
when(secondFile.getDeleteTime()).thenReturn(DATE_IN_MILLISECONDS);
when(client.get(secondFileID)).thenReturn(secondFile);

final Page<Blob> firstFolderBlobs = (Page<Blob>) spy(Page.class);
final List<Blob> firstFolderBlobsList = Optional.ofNullable(optionalBlob)
.map(blob -> Arrays.asList(firstFile, blob, secondFolder))
.orElse(Arrays.asList(firstFile, secondFolder));
when(firstFolderBlobs.getValues()).thenReturn(firstFolderBlobsList);
when(firstFolderBlobs.iterateAll()).thenReturn(firstFolderBlobsList);

final Page<Blob> secondFolderBlobs = (Page<Blob>) spy(Page.class);
when(secondFolderBlobs.getValues()).thenReturn(Collections.singletonList(secondFile));
when(secondFolderBlobs.iterateAll()).thenReturn(Collections.singletonList(secondFile));

when(client.list(BUCKET, Storage.BlobListOption.versions(true),
Storage.BlobListOption.currentDirectory(),
Storage.BlobListOption.prefix(firstFolderPath),
Storage.BlobListOption.pageToken(EMPTY_PREFIX),
Storage.BlobListOption.pageSize(Integer.MAX_VALUE))).thenReturn(firstFolderBlobs);

when(client.list(BUCKET, Storage.BlobListOption.versions(true),
Storage.BlobListOption.currentDirectory(),
Storage.BlobListOption.prefix(secondFolderPath),
Storage.BlobListOption.pageToken(EMPTY_PREFIX),
Storage.BlobListOption.pageSize(Integer.MAX_VALUE))).thenReturn(secondFolderBlobs);

final CopyWriter firstFileCopyWriter = mock(CopyWriter.class);
when(firstFileCopyWriter.getResult()).thenReturn(firstFile);

final CopyWriter secondFileCopyWriter = mock(CopyWriter.class);
when(secondFileCopyWriter.getResult()).thenReturn(secondFile);

when(client.copy(any(Storage.CopyRequest.class))).thenReturn(secondFileCopyWriter, firstFileCopyWriter);

final RestoreFolderVO restoreFolderVO = new RestoreFolderVO();
restoreFolderVO.setRecursively(recursive);
restoreFolderVO.setIncludeList(includeList);
restoreFolderVO.setExcludeList(excludeList);

return restoreFolderVO;
}

private BaseMatcher<Storage.CopyRequest> hasSourceAndDestination(BlobId blobId, String destination) {
return new BaseMatcher<Storage.CopyRequest>() {
@Override
public boolean matches(final Object item) {
final Storage.CopyRequest copyRequest = (Storage.CopyRequest) item;
return Objects.equals(copyRequest.getSource(), blobId)
&& Objects.equals(copyRequest.getTarget().getName(), destination);
}

@Override
public void describeTo(Description description) {
description.appendText(
String.format("Copy blob request doesn't have required blob Id='%s' and blob Info", blobId));
}
};
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

add empty line please

Copy link
Author

Choose a reason for hiding this comment

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

fixed

Copy link
Contributor

Choose a reason for hiding this comment

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

add a new line please

Copy link
Author

Choose a reason for hiding this comment

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

duplicate)