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

Fix AWS Glacier/Deep Archive loading of restored objects #759

Merged
merged 5 commits into from
Feb 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 6 additions & 20 deletions src/main/java/org/broad/igv/aws/S3LoadDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@
import java.util.Collections;
import java.util.List;

import software.amazon.awssdk.services.s3.model.S3Error;
import software.amazon.awssdk.services.s3.model.S3Exception;

import static org.broad.igv.util.AmazonUtils.isObjectAccessible;


public class S3LoadDialog extends JDialog {

Expand Down Expand Up @@ -102,7 +103,8 @@ private void loadButtonActionPerformed(ActionEvent e) {
if (isFilePath(path)) {
Triple<String, String, String> bucketKeyTier = getBucketKeyTierFromTreePath(path);

if(!isObjectAccessible(bucketKeyTier)) return;
AmazonUtils.s3ObjectAccessResult res = isObjectAccessible(bucketKeyTier.getLeft(), bucketKeyTier.getMiddle());
if(!res.getObjAvailable()) { MessageUtils.showErrorMessage(res.getErrorReason(), null); return; }

preLocatorPaths.add(bucketKeyTier);
}
Expand Down Expand Up @@ -200,23 +202,6 @@ private void updateModel(DefaultMutableTreeNode parent) {
}
}

// Determines whether the object is immediately available.
// On AWS this means present in STANDARD, STANDARD_IA, INTELLIGENT_TIERING object access tiers.
// Tiers GLACIER and DEEP_ARCHIVE are not immediately retrievable without action.
private boolean isObjectAccessible(Triple S3Obj) {
String S3ObjectBucket = S3Obj.getLeft().toString();
String S3ObjectKey = S3Obj.getMiddle().toString();
String S3ObjectStorageClass = S3Obj.getRight().toString();

if (S3ObjectStorageClass.contains("DEEP_ARCHIVE") ||
S3ObjectStorageClass.contains("GLACIER")) {
MessageUtils.showErrorMessage("Amazon S3 object is in " + S3ObjectStorageClass + " storage tier, not accessible at this moment. " +
"Please contact your local system administrator about object: s3://" + S3ObjectBucket + "/" + S3ObjectKey, null);
return false;
}

return true;
}

private void initComponents() {
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
Expand Down Expand Up @@ -256,7 +241,8 @@ public void mousePressed(MouseEvent e) {
if (isFilePath(selPath)) {
Triple<String, String, String> bucketKeyTier = getBucketKeyTierFromTreePath(selPath);

if(!isObjectAccessible(bucketKeyTier)) return;
AmazonUtils.s3ObjectAccessResult res = isObjectAccessible(bucketKeyTier.getLeft(), bucketKeyTier.getMiddle());
if(!res.getObjAvailable()) { MessageUtils.showErrorMessage(res.getErrorReason(), null); return;}

ResourceLocator loc = getResourceLocatorFromBucketKey(bucketKeyTier);
IGV.getInstance().loadTracks(Collections.singletonList(loc));
Expand Down
18 changes: 17 additions & 1 deletion src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@
import org.broad.igv.exceptions.HttpResponseException;
import org.broad.igv.feature.genome.GenomeManager;
import org.broad.igv.google.GoogleUtils;
import org.broad.igv.google.OAuthUtils;
import org.broad.igv.prefs.Constants;
import org.broad.igv.prefs.PreferencesManager;
import org.broad.igv.ui.IGV;
import org.broad.igv.ui.IGVMenuBar;
import org.broad.igv.ui.util.LoadFromURLDialog;
import org.broad.igv.ui.util.MessageUtils;
import org.broad.igv.util.AmazonUtils;
import org.broad.igv.util.HttpUtils;
import org.broad.igv.util.ResourceLocator;

Expand All @@ -52,6 +52,8 @@
import java.util.HashMap;
import java.util.Map;

import static org.broad.igv.util.AmazonUtils.isObjectAccessible;

/**
* @author jrobinso
*/
Expand Down Expand Up @@ -96,6 +98,20 @@ public void actionPerformed(ActionEvent e) {
MessageUtils.showMessage("Error loading url: " + url + " (" + ex.toString() + ")");
}
} else {
try {
// If AWS support is active, check if objects are in accessible tiers via Load URL menu...
if (AmazonUtils.isAwsS3Path(url)) {
String bucket = AmazonUtils.getBucketFromS3URL(url);
String key = AmazonUtils.getKeyFromS3URL(url);

AmazonUtils.s3ObjectAccessResult res = isObjectAccessible(bucket, key);
if (!res.getObjAvailable()) { MessageUtils.showErrorMessage(res.getErrorReason(), null); return; }
}
} catch (NullPointerException npe) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a comment, do we know that the user hasn't logged in solely from the npe? Could there be other reasons for an npe?

// User has not yet done Amazon->Login sequence
AmazonUtils.checkLogin();
}

ResourceLocator rl = new ResourceLocator(url.trim());

if (dlg.getIndexURL() != null) {
Expand Down
117 changes: 106 additions & 11 deletions src/main/java/org/broad/igv/util/AmazonUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.gson.JsonObject;
import htsjdk.samtools.util.Tuple;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.log4j.Logger;
import org.broad.igv.Globals;
import org.broad.igv.aws.IGVS3Object;
Expand Down Expand Up @@ -203,6 +204,100 @@ public static List<String> ListBucketsForUser() {
return bucketsFinalList;
}

public static HeadObjectResponse getObjectMetadata(String bucket, String key) {
HeadObjectRequest HeadObjReq = HeadObjectRequest.builder()
.bucket(bucket)
.key(key).build();
HeadObjectResponse HeadObjRes = s3Client.headObject(HeadObjReq);
log.debug("getObjectMetadata(): "+HeadObjRes.toString());
return HeadObjRes;
}


// Holds whether a S3 object is accessible or not and reason/error msg in case it's not.
public static class s3ObjectAccessResult {
private boolean objAvailable;
private String errorReason;

public boolean getObjAvailable() {
return objAvailable;
}

public void setObjAvailable(boolean objAvailable) {
this.objAvailable = objAvailable;
}

public String getErrorReason() {
return errorReason;
}

public void setErrorReason(String errorReason) {
this.errorReason = errorReason;
}
}

// Determines whether the object is immediately available.
// On AWS this means present in STANDARD, STANDARD_IA, INTELLIGENT_TIERING object access tiers.
// Tiers GLACIER and DEEP_ARCHIVE are not immediately retrievable without action.
public static s3ObjectAccessResult isObjectAccessible(String bucket, String key) {
s3ObjectAccessResult res = new s3ObjectAccessResult();
HeadObjectResponse s3Meta;

String s3ObjectStorageStatus = null;
String s3ObjectStorageClass;

s3Meta = AmazonUtils.getObjectMetadata(bucket, key);
s3ObjectStorageClass = s3Meta.storageClass().toString();

// Determine in which state this object really is:
// 1. Archived.
// 2. In the process of being restored.
// 3. Restored
//
// This is important because after restoration the object mantains the Tier (DEEP_ARCHIVE) instead of
// transitioning that attribute to STANDARD, we must look at head_object response for the "Restore"
// attribute.
//
// Possible error reason messages for the users are:

String archived = "Amazon S3 object is in " + s3ObjectStorageClass + " storage tier, not accessible at this moment. " +
"Please contact your local system administrator about object: s3://" + bucket + "/" + key;
String restoreInProgress = "Amazon S3 object is in " + s3ObjectStorageClass + " and being restored right now, please be patient, this can take up to 48h. " +
"For further enquiries about this dataset, please use the following path when communicating with your system administrator: s3://" + bucket + "/" + key;

if (s3ObjectStorageClass.contains("DEEP_ARCHIVE") ||
s3ObjectStorageClass.contains("GLACIER")) {
try {
s3ObjectStorageStatus = s3Meta.sdkHttpResponse().headers().get("x-amz-restore").toString();
//S3ObjectStorageStatus = S3Meta.restore();
} catch(NullPointerException npe) {
res.setObjAvailable(false);
res.setErrorReason(archived);
return res;
}

if(s3ObjectStorageStatus.contains("ongoing-request=\"true\"")) {
res.setObjAvailable(false);
res.setErrorReason(restoreInProgress);

// "If an archive copy is already restored, the header value indicates when Amazon S3 is scheduled to delete the object copy"
} else if(s3ObjectStorageStatus.contains("ongoing-request=\"false\"") && s3ObjectStorageStatus.contains("expiry-date=")) {
res.setObjAvailable(true);
} else {
// The object has never been restored?
res.setObjAvailable(false);
res.setErrorReason(archived);
}
} else {
// The object must be either in STANDARD, INFREQUENT_ACCESS, INTELLIGENT_TIERING or
// any other "immediately available" tier...
res.setErrorReason("Object is in an accessible tier, no errors are expected");
res.setObjAvailable(true);
}

return res;
}

private static List<String> getReadableBuckets(List<String> buckets) {
List<CompletableFuture<String>> futures =
buckets.stream()
Expand Down Expand Up @@ -289,18 +384,19 @@ public static ArrayList<IGVS3Object> ListBucketObjects(String bucketName, String
return objects;
}

public static Tuple<String, String> bucketAndKey(String S3urlString) {
AmazonS3URI s3URI = new AmazonS3URI(S3urlString);
String bucket = s3URI.getBucket();
String key = s3URI.getKey();
public static String getBucketFromS3URL(String s3URL) {
AmazonS3URI s3URI = new AmazonS3URI(s3URL);
return s3URI.getBucket();

log.debug("bucketAndKey(): " + bucket + " , " + key);
return new Tuple(bucket, key);
}

public static String getKeyFromS3URL(String s3URL) {
AmazonS3URI s3URI = new AmazonS3URI(s3URL);
return s3URI.getKey();
}

// Amazon S3 Presign URLs
// Also keeps an internal mapping between ResourceLocator and active/valid signed URLs.

private static String createPresignedURL(String s3Path) throws IOException {
// Make sure access token are valid (refreshes token internally)
OAuthProvider provider = OAuthUtils.getInstance().getProvider("Amazon");
Expand All @@ -318,11 +414,10 @@ private static String createPresignedURL(String s3Path) throws IOException {
.region(getAWSREGION())
.build();

Tuple<String, String> bandk = bucketAndKey(s3Path);
String bucket = bandk.a;
String filename = bandk.b;
String bucket = getBucketFromS3URL(s3Path);
String key = getKeyFromS3URL(s3Path);

URI presigned = s3Presigner.presignS3DownloadLink(bucket, filename);
URI presigned = s3Presigner.presignS3DownloadLink(bucket, key);
log.debug("AWS presigned URL from translateAmazonCloudURL is: " + presigned);
return presigned.toString();
}
Expand Down