Skip to content

Commit 4fbb9df

Browse files
authored
Fix AWS Glacier/Deep Archive loading of restored objects (#759)
* Naive headObject method wrapper for GLACIER_ARCHIVE restore. Observed header combinations while debugging do not seem to align docs, more investigation is needed. * The AWS SDK Java v2 HeadObject.restore() is not reliable as of version 2.8.5, see aws/aws-sdk-java-v2#670 (comment) * Other than fixing the Glacier/Archive tier error handling, also implemented support for File->Load From URL if the user knows the S3 URL, they can paste it there directly. Also aws-java-sdk-v2 version bump from Maven * This field gets populated on newer versions of the aws-java-sdk-v2, fortunately * Apply suggestions/code review from @reisingerf. Rollback aws-sdk version bump since it affects error messaging and "germane-ity" of this PR. Killing Triple/Tuple in next commit.
1 parent b4a0e3d commit 4fbb9df

File tree

3 files changed

+129
-32
lines changed

3 files changed

+129
-32
lines changed

src/main/java/org/broad/igv/aws/S3LoadDialog.java

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,10 @@
4848
import java.util.Collections;
4949
import java.util.List;
5050

51-
import software.amazon.awssdk.services.s3.model.S3Error;
5251
import software.amazon.awssdk.services.s3.model.S3Exception;
5352

53+
import static org.broad.igv.util.AmazonUtils.isObjectAccessible;
54+
5455

5556
public class S3LoadDialog extends JDialog {
5657

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

105-
if(!isObjectAccessible(bucketKeyTier)) return;
106+
AmazonUtils.s3ObjectAccessResult res = isObjectAccessible(bucketKeyTier.getLeft(), bucketKeyTier.getMiddle());
107+
if(!res.getObjAvailable()) { MessageUtils.showErrorMessage(res.getErrorReason(), null); return; }
106108

107109
preLocatorPaths.add(bucketKeyTier);
108110
}
@@ -200,23 +202,6 @@ private void updateModel(DefaultMutableTreeNode parent) {
200202
}
201203
}
202204

203-
// Determines whether the object is immediately available.
204-
// On AWS this means present in STANDARD, STANDARD_IA, INTELLIGENT_TIERING object access tiers.
205-
// Tiers GLACIER and DEEP_ARCHIVE are not immediately retrievable without action.
206-
private boolean isObjectAccessible(Triple S3Obj) {
207-
String S3ObjectBucket = S3Obj.getLeft().toString();
208-
String S3ObjectKey = S3Obj.getMiddle().toString();
209-
String S3ObjectStorageClass = S3Obj.getRight().toString();
210-
211-
if (S3ObjectStorageClass.contains("DEEP_ARCHIVE") ||
212-
S3ObjectStorageClass.contains("GLACIER")) {
213-
MessageUtils.showErrorMessage("Amazon S3 object is in " + S3ObjectStorageClass + " storage tier, not accessible at this moment. " +
214-
"Please contact your local system administrator about object: s3://" + S3ObjectBucket + "/" + S3ObjectKey, null);
215-
return false;
216-
}
217-
218-
return true;
219-
}
220205

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

259-
if(!isObjectAccessible(bucketKeyTier)) return;
244+
AmazonUtils.s3ObjectAccessResult res = isObjectAccessible(bucketKeyTier.getLeft(), bucketKeyTier.getMiddle());
245+
if(!res.getObjAvailable()) { MessageUtils.showErrorMessage(res.getErrorReason(), null); return;}
260246

261247
ResourceLocator loc = getResourceLocatorFromBucketKey(bucketKeyTier);
262248
IGV.getInstance().loadTracks(Collections.singletonList(loc));

src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@
3333
import org.broad.igv.exceptions.HttpResponseException;
3434
import org.broad.igv.feature.genome.GenomeManager;
3535
import org.broad.igv.google.GoogleUtils;
36-
import org.broad.igv.google.OAuthUtils;
3736
import org.broad.igv.prefs.Constants;
3837
import org.broad.igv.prefs.PreferencesManager;
3938
import org.broad.igv.ui.IGV;
4039
import org.broad.igv.ui.IGVMenuBar;
4140
import org.broad.igv.ui.util.LoadFromURLDialog;
4241
import org.broad.igv.ui.util.MessageUtils;
42+
import org.broad.igv.util.AmazonUtils;
4343
import org.broad.igv.util.HttpUtils;
4444
import org.broad.igv.util.ResourceLocator;
4545

@@ -52,6 +52,8 @@
5252
import java.util.HashMap;
5353
import java.util.Map;
5454

55+
import static org.broad.igv.util.AmazonUtils.isObjectAccessible;
56+
5557
/**
5658
* @author jrobinso
5759
*/
@@ -96,6 +98,20 @@ public void actionPerformed(ActionEvent e) {
9698
MessageUtils.showMessage("Error loading url: " + url + " (" + ex.toString() + ")");
9799
}
98100
} else {
101+
try {
102+
// If AWS support is active, check if objects are in accessible tiers via Load URL menu...
103+
if (AmazonUtils.isAwsS3Path(url)) {
104+
String bucket = AmazonUtils.getBucketFromS3URL(url);
105+
String key = AmazonUtils.getKeyFromS3URL(url);
106+
107+
AmazonUtils.s3ObjectAccessResult res = isObjectAccessible(bucket, key);
108+
if (!res.getObjAvailable()) { MessageUtils.showErrorMessage(res.getErrorReason(), null); return; }
109+
}
110+
} catch (NullPointerException npe) {
111+
// User has not yet done Amazon->Login sequence
112+
AmazonUtils.checkLogin();
113+
}
114+
99115
ResourceLocator rl = new ResourceLocator(url.trim());
100116

101117
if (dlg.getIndexURL() != null) {

src/main/java/org/broad/igv/util/AmazonUtils.java

Lines changed: 106 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.google.gson.JsonObject;
44
import htsjdk.samtools.util.Tuple;
5+
import org.apache.commons.lang3.tuple.Triple;
56
import org.apache.log4j.Logger;
67
import org.broad.igv.Globals;
78
import org.broad.igv.aws.IGVS3Object;
@@ -203,6 +204,100 @@ public static List<String> ListBucketsForUser() {
203204
return bucketsFinalList;
204205
}
205206

207+
public static HeadObjectResponse getObjectMetadata(String bucket, String key) {
208+
HeadObjectRequest HeadObjReq = HeadObjectRequest.builder()
209+
.bucket(bucket)
210+
.key(key).build();
211+
HeadObjectResponse HeadObjRes = s3Client.headObject(HeadObjReq);
212+
log.debug("getObjectMetadata(): "+HeadObjRes.toString());
213+
return HeadObjRes;
214+
}
215+
216+
217+
// Holds whether a S3 object is accessible or not and reason/error msg in case it's not.
218+
public static class s3ObjectAccessResult {
219+
private boolean objAvailable;
220+
private String errorReason;
221+
222+
public boolean getObjAvailable() {
223+
return objAvailable;
224+
}
225+
226+
public void setObjAvailable(boolean objAvailable) {
227+
this.objAvailable = objAvailable;
228+
}
229+
230+
public String getErrorReason() {
231+
return errorReason;
232+
}
233+
234+
public void setErrorReason(String errorReason) {
235+
this.errorReason = errorReason;
236+
}
237+
}
238+
239+
// Determines whether the object is immediately available.
240+
// On AWS this means present in STANDARD, STANDARD_IA, INTELLIGENT_TIERING object access tiers.
241+
// Tiers GLACIER and DEEP_ARCHIVE are not immediately retrievable without action.
242+
public static s3ObjectAccessResult isObjectAccessible(String bucket, String key) {
243+
s3ObjectAccessResult res = new s3ObjectAccessResult();
244+
HeadObjectResponse s3Meta;
245+
246+
String s3ObjectStorageStatus = null;
247+
String s3ObjectStorageClass;
248+
249+
s3Meta = AmazonUtils.getObjectMetadata(bucket, key);
250+
s3ObjectStorageClass = s3Meta.storageClass().toString();
251+
252+
// Determine in which state this object really is:
253+
// 1. Archived.
254+
// 2. In the process of being restored.
255+
// 3. Restored
256+
//
257+
// This is important because after restoration the object mantains the Tier (DEEP_ARCHIVE) instead of
258+
// transitioning that attribute to STANDARD, we must look at head_object response for the "Restore"
259+
// attribute.
260+
//
261+
// Possible error reason messages for the users are:
262+
263+
String archived = "Amazon S3 object is in " + s3ObjectStorageClass + " storage tier, not accessible at this moment. " +
264+
"Please contact your local system administrator about object: s3://" + bucket + "/" + key;
265+
String restoreInProgress = "Amazon S3 object is in " + s3ObjectStorageClass + " and being restored right now, please be patient, this can take up to 48h. " +
266+
"For further enquiries about this dataset, please use the following path when communicating with your system administrator: s3://" + bucket + "/" + key;
267+
268+
if (s3ObjectStorageClass.contains("DEEP_ARCHIVE") ||
269+
s3ObjectStorageClass.contains("GLACIER")) {
270+
try {
271+
s3ObjectStorageStatus = s3Meta.sdkHttpResponse().headers().get("x-amz-restore").toString();
272+
//S3ObjectStorageStatus = S3Meta.restore();
273+
} catch(NullPointerException npe) {
274+
res.setObjAvailable(false);
275+
res.setErrorReason(archived);
276+
return res;
277+
}
278+
279+
if(s3ObjectStorageStatus.contains("ongoing-request=\"true\"")) {
280+
res.setObjAvailable(false);
281+
res.setErrorReason(restoreInProgress);
282+
283+
// "If an archive copy is already restored, the header value indicates when Amazon S3 is scheduled to delete the object copy"
284+
} else if(s3ObjectStorageStatus.contains("ongoing-request=\"false\"") && s3ObjectStorageStatus.contains("expiry-date=")) {
285+
res.setObjAvailable(true);
286+
} else {
287+
// The object has never been restored?
288+
res.setObjAvailable(false);
289+
res.setErrorReason(archived);
290+
}
291+
} else {
292+
// The object must be either in STANDARD, INFREQUENT_ACCESS, INTELLIGENT_TIERING or
293+
// any other "immediately available" tier...
294+
res.setErrorReason("Object is in an accessible tier, no errors are expected");
295+
res.setObjAvailable(true);
296+
}
297+
298+
return res;
299+
}
300+
206301
private static List<String> getReadableBuckets(List<String> buckets) {
207302
List<CompletableFuture<String>> futures =
208303
buckets.stream()
@@ -289,18 +384,19 @@ public static ArrayList<IGVS3Object> ListBucketObjects(String bucketName, String
289384
return objects;
290385
}
291386

292-
public static Tuple<String, String> bucketAndKey(String S3urlString) {
293-
AmazonS3URI s3URI = new AmazonS3URI(S3urlString);
294-
String bucket = s3URI.getBucket();
295-
String key = s3URI.getKey();
387+
public static String getBucketFromS3URL(String s3URL) {
388+
AmazonS3URI s3URI = new AmazonS3URI(s3URL);
389+
return s3URI.getBucket();
296390

297-
log.debug("bucketAndKey(): " + bucket + " , " + key);
298-
return new Tuple(bucket, key);
391+
}
392+
393+
public static String getKeyFromS3URL(String s3URL) {
394+
AmazonS3URI s3URI = new AmazonS3URI(s3URL);
395+
return s3URI.getKey();
299396
}
300397

301398
// Amazon S3 Presign URLs
302399
// Also keeps an internal mapping between ResourceLocator and active/valid signed URLs.
303-
304400
private static String createPresignedURL(String s3Path) throws IOException {
305401
// Make sure access token are valid (refreshes token internally)
306402
OAuthProvider provider = OAuthUtils.getInstance().getProvider("Amazon");
@@ -318,11 +414,10 @@ private static String createPresignedURL(String s3Path) throws IOException {
318414
.region(getAWSREGION())
319415
.build();
320416

321-
Tuple<String, String> bandk = bucketAndKey(s3Path);
322-
String bucket = bandk.a;
323-
String filename = bandk.b;
417+
String bucket = getBucketFromS3URL(s3Path);
418+
String key = getKeyFromS3URL(s3Path);
324419

325-
URI presigned = s3Presigner.presignS3DownloadLink(bucket, filename);
420+
URI presigned = s3Presigner.presignS3DownloadLink(bucket, key);
326421
log.debug("AWS presigned URL from translateAmazonCloudURL is: " + presigned);
327422
return presigned.toString();
328423
}

0 commit comments

Comments
 (0)