-
Notifications
You must be signed in to change notification settings - Fork 39
/
S3URLHandler.scala
520 lines (397 loc) · 21.8 KB
/
S3URLHandler.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
/*
* Copyright 2014 Frugal Mechanic (http://frugalmechanic.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fm.sbt
import java.io.{File, FileInputStream, InputStream}
import java.net.{URI, URL}
import java.util.Properties
import java.util.concurrent.ConcurrentHashMap
import javax.naming.{Context, NamingException}
import javax.naming.directory.{Attribute, Attributes, InitialDirContext}
import com.amazonaws.ClientConfiguration
import com.amazonaws.SDKGlobalConfiguration.{ACCESS_KEY_ENV_VAR, ACCESS_KEY_SYSTEM_PROPERTY, SECRET_KEY_ENV_VAR, SECRET_KEY_SYSTEM_PROPERTY}
import com.amazonaws.auth._
import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration
import com.amazonaws.regions.Regions
import com.amazonaws.services.s3.model._
import com.amazonaws.services.s3.{AmazonS3, AmazonS3Client, AmazonS3ClientBuilder, AmazonS3URI}
import com.amazonaws.services.securitytoken.{AWSSecurityTokenService, AWSSecurityTokenServiceClient}
import com.amazonaws.services.securitytoken.model.{AssumeRoleRequest, AssumeRoleResult}
import org.apache.ivy.util.url.URLHandler
import org.apache.ivy.util.{CopyProgressEvent, CopyProgressListener, Message}
import scala.annotation.tailrec
import scala.collection.JavaConverters._
import scala.util.Try
import scala.util.matching.Regex
object S3URLHandler {
private val DOT_SBT_DIR: File = new File(System.getProperty("user.home"), ".sbt")
// This is for matching region names in URLs or host names
private val RegionMatcher: Regex = Regions.values().map{ _.getName }.sortBy{ -1 * _.length }.mkString("|").r
private var bucketCredentialsProvider: String => AWSCredentialsProvider = makePropertiesFileCredentialsProvider
private var bucketACLMap: Map[String, CannedAccessControlList] = Map()
def registerBucketCredentialsProvider(provider: String => AWSCredentialsProvider): Unit = {
bucketCredentialsProvider = provider
}
def registerBucketACLMap(aclMap: Map[String, CannedAccessControlList]): Unit = {
bucketACLMap = aclMap
}
def getBucketCredentialsProvider: String => AWSCredentialsProvider = bucketCredentialsProvider
private class S3URLInfo(available: Boolean, contentLength: Long, lastModified: Long) extends URLHandler.URLInfo(available, contentLength, lastModified)
private class BucketSpecificSystemPropertiesCredentialsProvider(bucket: String) extends BucketSpecificCredentialsProvider(bucket) {
def AccessKeyName: String = ACCESS_KEY_SYSTEM_PROPERTY
def SecretKeyName: String = SECRET_KEY_SYSTEM_PROPERTY
protected def getProp(names: String*): String = names.map{ System.getProperty }.flatMap{ Option(_) }.head.trim
}
private class BucketSpecificEnvironmentVariableCredentialsProvider(bucket: String) extends BucketSpecificCredentialsProvider(bucket) {
def AccessKeyName: String = ACCESS_KEY_ENV_VAR
def SecretKeyName: String = SECRET_KEY_ENV_VAR
protected def getProp(names: String*): String = names.map{ toEnvironmentVariableName }.map{ System.getenv }.flatMap{ Option(_) }.head.trim
}
private abstract class BucketSpecificCredentialsProvider(bucket: String) extends AWSCredentialsProvider {
def AccessKeyName: String
def SecretKeyName: String
def getCredentials(): AWSCredentials = {
val accessKey: String = getProp(s"${AccessKeyName}.${bucket}", s"${bucket}.${AccessKeyName}")
val secretKey: String = getProp(s"${SecretKeyName}.${bucket}", s"${bucket}.${SecretKeyName}")
new BasicAWSCredentials(accessKey, secretKey)
}
def refresh(): Unit = {}
// This should throw an exception if the value is missing
protected def getProp(names: String*): String
}
private abstract class RoleBasedCredentialsProvider(providerChain: AWSCredentialsProviderChain) extends AWSCredentialsProvider {
def RoleArnKeyNames: Seq[String]
// This should throw an exception if the value is missing
protected def getRoleArn(keys: String*): String
def getCredentials(): AWSCredentials = {
val roleArn: String = getRoleArn(RoleArnKeyNames: _*)
if (roleArn == null || roleArn == "") return null
val securityTokenService: AWSSecurityTokenService = AWSSecurityTokenServiceClient.builder().withCredentials(providerChain).build()
val roleRequest: AssumeRoleRequest = new AssumeRoleRequest()
.withRoleArn(roleArn)
.withRoleSessionName(System.currentTimeMillis.toString)
val result: AssumeRoleResult = securityTokenService.assumeRole(roleRequest)
new BasicSessionCredentials(result.getCredentials.getAccessKeyId, result.getCredentials.getSecretAccessKey, result.getCredentials.getSessionToken)
}
def refresh(): Unit = {}
}
private class RoleBasedSystemPropertiesCredentialsProvider(providerChain: AWSCredentialsProviderChain)
extends RoleBasedCredentialsProvider(providerChain) {
val RoleArnKeyName: String = "aws.roleArn"
val RoleArnKeyNames: Seq[String] = Seq(RoleArnKeyName)
protected def getRoleArn(keys: String*): String = keys.map( System.getProperty ).flatMap( Option(_) ).head.trim
}
private class RoleBasedEnvironmentVariableCredentialsProvider(providerChain: AWSCredentialsProviderChain)
extends RoleBasedCredentialsProvider(providerChain) {
val RoleArnKeyName: String = "AWS_ROLE_ARN"
val RoleArnKeyNames: Seq[String] = Seq("AWS_ROLE_ARN")
protected def getRoleArn(keys: String*): String = keys.map( toEnvironmentVariableName ).map( System.getenv ).flatMap( Option(_) ).head.trim
}
private class RoleBasedPropertiesFileCredentialsProvider(providerChain: AWSCredentialsProviderChain, fileName: String)
extends RoleBasedCredentialsProvider(providerChain) {
val RoleArnKeyName: String = "roleArn"
val RoleArnKeyNames: Seq[String] = Seq(RoleArnKeyName)
protected def getRoleArn(keys: String*): String = {
val file: File = new File(DOT_SBT_DIR, fileName)
// This will throw if the file doesn't exist
val is: InputStream = new FileInputStream(file)
try {
val props: Properties = new Properties()
props.load(is)
// This will throw if there is no matching properties
RoleArnKeyNames.map{ props.getProperty }.flatMap{ Option(_) }.head.trim
} finally is.close()
}
}
private class BucketSpecificRoleBasedSystemPropertiesCredentialsProvider(providerChain: AWSCredentialsProviderChain, bucket: String)
extends RoleBasedSystemPropertiesCredentialsProvider(providerChain) {
override val RoleArnKeyNames: Seq[String] = Seq(s"${RoleArnKeyName}.${bucket}", s"${bucket}.${RoleArnKeyName}")
}
private class BucketSpecificRoleBasedEnvironmentVariableCredentialsProvider(providerChain: AWSCredentialsProviderChain, bucket: String)
extends RoleBasedEnvironmentVariableCredentialsProvider(providerChain) {
override val RoleArnKeyNames: Seq[String] = Seq(s"${RoleArnKeyName}.${bucket}", s"${bucket}.${RoleArnKeyName}")
}
private def toEnvironmentVariableName(s: String): String = s.toUpperCase.replace('-','_').replace('.','_').replaceAll("[^A-Z0-9_]", "")
private def makePropertiesFileCredentialsProvider(fileName: String): PropertiesFileCredentialsProvider = {
val file: File = new File(DOT_SBT_DIR, fileName)
new PropertiesFileCredentialsProvider(file.toString)
}
def defaultCredentialsProviderChain(bucket: String): AWSCredentialsProviderChain = {
val basicProviders: Vector[AWSCredentialsProvider] = Vector(
new BucketSpecificEnvironmentVariableCredentialsProvider(bucket),
new BucketSpecificSystemPropertiesCredentialsProvider(bucket),
makePropertiesFileCredentialsProvider(s".s3credentials_${bucket}"),
makePropertiesFileCredentialsProvider(s".${bucket}_s3credentials"),
DefaultAWSCredentialsProviderChain.getInstance(),
makePropertiesFileCredentialsProvider(".s3credentials"),
InstanceProfileCredentialsProvider.getInstance()
)
val basicProviderChain: AWSCredentialsProviderChain = new AWSCredentialsProviderChain(basicProviders: _*)
val roleBasedProviders: Vector[AWSCredentialsProvider] = Vector(
new BucketSpecificRoleBasedEnvironmentVariableCredentialsProvider(basicProviderChain, bucket),
new BucketSpecificRoleBasedSystemPropertiesCredentialsProvider(basicProviderChain, bucket),
new RoleBasedPropertiesFileCredentialsProvider(basicProviderChain, s".s3credentials_${bucket}"),
new RoleBasedPropertiesFileCredentialsProvider(basicProviderChain, s".${bucket}_s3credentials"),
new RoleBasedEnvironmentVariableCredentialsProvider(basicProviderChain),
new RoleBasedSystemPropertiesCredentialsProvider(basicProviderChain),
new RoleBasedPropertiesFileCredentialsProvider(basicProviderChain, s".s3credentials")
)
new AWSCredentialsProviderChain((roleBasedProviders ++ basicProviders): _*)
}
def getRegionNameFromDNS(bucket: String): Option[String] = {
// maven.custom.s3.amazonaws.com. 21600 IN CNAME s3-1-w.amazonaws.com.
// s3-1-w.amazonaws.com. 39 IN CNAME s3-w.us-east-1.amazonaws.com.
getDNSAliasesForBucket(bucket).flatMap { RegionMatcher.findFirstIn(_) }.headOption
}
private[this] val dnsContext: InitialDirContext = {
val env: Properties = new Properties()
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory")
new InitialDirContext(env)
}
def getDNSAliasesForBucket(bucket: String): Seq[String] = {
getDNSAliasesForHost(bucket + ".s3.amazonaws.com")
}
def getDNSAliasesForHost(host: String): Seq[String] = getDNSAliasesForHost(host, Nil)
@tailrec private def getDNSAliasesForHost(host: String, matches: List[String]): Seq[String] = {
val cname: Option[String] = try {
val attrs: Attributes = dnsContext.getAttributes(host, Array("CNAME"))
Option(attrs.get("CNAME"))
.flatMap{ attr: Attribute => Option(attr.get) }
.collectFirst{ case res: String => res }
} catch {
case _: NamingException => None
}
if (cname.isEmpty || cname.exists{ matches.contains(_) }) matches
else getDNSAliasesForHost(cname.get, cname.get :: matches)
}
}
/**
* This implements the Ivy URLHandler
*/
final class S3URLHandler extends URLHandler {
import fm.sbt.S3URLHandler._
import org.apache.ivy.util.url.URLHandler.{UNAVAILABLE, URLInfo}
// Cache of Bucket Name => AmazonS3 Client Instance
private val amazonS3ClientCache: ConcurrentHashMap[String,AmazonS3] = new ConcurrentHashMap()
// Cache of Bucket Name => true/false (requires Server Side Encryption or not)
private val bucketRequiresSSE: ConcurrentHashMap[String,Boolean] = new ConcurrentHashMap()
def isReachable(url: URL): Boolean = getURLInfo(url).isReachable
def isReachable(url: URL, timeout: Int): Boolean = getURLInfo(url, timeout).isReachable
def getContentLength(url: URL): Long = getURLInfo(url).getContentLength
def getContentLength(url: URL, timeout: Int): Long = getURLInfo(url, timeout).getContentLength
def getLastModified(url: URL): Long = getURLInfo(url).getLastModified
def getLastModified(url: URL, timeout: Int): Long = getURLInfo(url, timeout).getLastModified
def getURLInfo(url: URL): URLInfo = getURLInfo(url, 0)
private def debug(msg: String): Unit = Message.debug("S3URLHandler."+msg)
def getCredentialsProvider(bucket: String): AWSCredentialsProvider = {
Message.info("S3URLHandler - Looking up AWS Credentials for bucket: "+bucket+" ...")
val credentialsProvider: AWSCredentialsProvider = try {
getBucketCredentialsProvider(bucket)
} catch {
case ex: com.amazonaws.AmazonClientException =>
Message.error("Unable to find AWS Credentials.")
throw ex
}
Message.info("S3URLHandler - Using AWS Access Key Id: "+credentialsProvider.getCredentials().getAWSAccessKeyId+" for bucket: "+bucket)
credentialsProvider
}
def getProxyConfiguration: ClientConfiguration = {
val configuration = new ClientConfiguration()
for {
proxyHost <- Option( System.getProperty("https.proxyHost") )
proxyPort <- Option( System.getProperty("https.proxyPort").toInt )
} {
configuration.setProxyHost(proxyHost)
configuration.setProxyPort(proxyPort)
}
configuration
}
def getClientBucketAndKey(url: URL): (AmazonS3, String, String) = {
val (bucket, key) = getBucketAndKey(url)
var client: AmazonS3 = amazonS3ClientCache.get(bucket)
if (null == client) {
// This allows you to change the S3 endpoint and signing region to point to a non-aws S3 implementation (e.g. LocalStack).
val endpointConfiguration: Option[EndpointConfiguration] = for {
serviceEndpoint: String <- Option(System.getenv("S3_SERVICE_ENDPOINT"))
signingRegion: String <- Option(System.getenv("S3_SIGNING_REGION"))
} yield new EndpointConfiguration(serviceEndpoint, signingRegion)
// Path Style Access is deprecated by Amazon S3 but LocalStack seems to want to use it
val pathStyleAccess: Boolean = Option(System.getenv("S3_PATH_STYLE_ACCESS")).exists(_.toBoolean)
// Rerouting can cause replacing the user custom endpoint with the S3 default one (s3.amazonaws.com). Default is true
val forceGlobalBucketAccessEnabled: Boolean = Option(System.getenv("S3_FORCE_GLOBAL_BUCKET_ACCESS")).forall(_.toBoolean)
val tmp: AmazonS3ClientBuilder = AmazonS3Client.builder()
.withCredentials(getCredentialsProvider(bucket))
.withClientConfiguration(getProxyConfiguration)
.withForceGlobalBucketAccessEnabled(forceGlobalBucketAccessEnabled)
.withPathStyleAccessEnabled(pathStyleAccess)
// Only one of the endpointConfiguration or region can be set at a time.
client = (endpointConfiguration match {
case Some(endpoint) => tmp.withEndpointConfiguration(endpoint)
case None => tmp.withRegion(getRegion(url, bucket))
}).build()
amazonS3ClientCache.put(bucket, client)
Message.info("S3URLHandler - Created S3 Client for bucket: "+bucket+" and region: "+client.getRegionName)
}
(client, bucket, key)
}
def getURLInfo(url: URL, timeout: Int): URLInfo = try {
debug(s"getURLInfo($url, $timeout)")
val (client, bucket, key) = getClientBucketAndKey(url)
val meta: ObjectMetadata = client.getObjectMetadata(bucket, key)
val available: Boolean = true
val contentLength: Long = meta.getContentLength
val lastModified: Long = meta.getLastModified.getTime
new S3URLInfo(available, contentLength, lastModified)
} catch {
case ex: AmazonS3Exception if ex.getStatusCode == 404 => UNAVAILABLE
case ex: java.net.URISyntaxException =>
// We can hit this when given a URL that looks like:
// s3://maven.custom/releases/javax/ws/rs/javax.ws.rs-api/2.1/javax.ws.rs-api-2.1.${packaging.type}
//
// In that case we just ignore it and treat it as a 404. It looks like this is really a bug in IVY that has
// recently been fixed (as of 2018-03-12): https://issues.apache.org/jira/browse/IVY-1577
//
// Original Bug: https://github.com/frugalmechanic/fm-sbt-s3-resolver/issues/45
// Original PR: https://github.com/frugalmechanic/fm-sbt-s3-resolver/pull/46
//
Message.warn("S3URLHandler - " + ex.getMessage)
UNAVAILABLE
}
def openStream(url: URL): InputStream = {
debug(s"openStream($url)")
val (client, bucket, key) = getClientBucketAndKey(url)
val obj: S3Object = client.getObject(bucket, key)
obj.getObjectContent()
}
/**
* A directory listing for keys/directories under this prefix
*/
def list(url: URL): Seq[URL] = {
debug(s"list($url)")
val (client, bucket, key /* key is the prefix in this case */) = getClientBucketAndKey(url)
// We want the prefix to have a trailing slash
val prefix: String = key.stripSuffix("/") + "/"
val request: ListObjectsRequest = new ListObjectsRequest().withBucketName(bucket).withPrefix(prefix).withDelimiter("/")
val listing: ObjectListing = client.listObjects(request)
require(!listing.isTruncated, "Truncated ObjectListing! Making additional calls currently isn't implemented!")
val keys: Seq[String] = listing.getCommonPrefixes.asScala ++ listing.getObjectSummaries.asScala.map{ _.getKey }
val res: Seq[URL] = keys.map{ k: String =>
new URL(url.toString.stripSuffix("/") + "/" + k.stripPrefix(prefix))
}
debug(s"list($url) => \n "+res.mkString("\n "))
res
}
def download(src: URL, dest: File, l: CopyProgressListener): Unit = {
debug(s"download($src, $dest)")
val (client, bucket, key) = getClientBucketAndKey(src)
val event: CopyProgressEvent = new CopyProgressEvent()
if (null != l) l.start(event)
val meta: ObjectMetadata = client.getObject(new GetObjectRequest(bucket, key), dest)
dest.setLastModified(meta.getLastModified.getTime)
if (null != l) l.end(event) //l.progress(evt.update(EMPTY_BUFFER, 0, meta.getContentLength))
}
def upload(src: File, dest: URL, l: CopyProgressListener): Unit = {
debug(s"upload($src, $dest)")
val event: CopyProgressEvent = new CopyProgressEvent()
if (null != l) l.start(event)
val (client, bucket, key) = getClientBucketAndKey(dest)
// Nested helper method for performing the actual PUT
def putImpl(serverSideEncryption: Boolean): PutObjectResult = {
val meta: ObjectMetadata = new ObjectMetadata()
if (serverSideEncryption) meta.setSSEAlgorithm(ObjectMetadata.AES_256_SERVER_SIDE_ENCRYPTION)
val customizers = Seq[PutObjectRequest => PutObjectRequest](
// add metadata
x => {x.withMetadata(meta)},
// add bucket ACL
x => {
bucketACLMap.get(bucket) match {
case Some(y) => x.withCannedAcl(y)
case None => x
}
}
)
val req = customizers.foldLeft(new PutObjectRequest(bucket, key, src))((putObjectRequest, customizer) => customizer(putObjectRequest))
client.putObject(req)
}
// Do we know for sure that this bucket requires SSE?
val requiresSSE: Boolean = bucketRequiresSSE.containsKey(bucket)
if (requiresSSE) {
// We know we require SSE
putImpl(true)
} else {
try {
// We either don't require SSE or don't know yet so we try without SSE enabled
putImpl(false)
} catch {
case ex: AmazonS3Exception if ex.getStatusCode() == 403 =>
debug(s"upload($src, $dest) failed with a 403 status code. Retrying with Server Side Encryption Enabled.")
// Retry with SSE
val res: PutObjectResult = putImpl(true)
// If that succeeded then save the fact that we require SSE for future requests
bucketRequiresSSE.put(bucket, true)
Message.info(s"S3URLHandler - Enabled Server Side Encryption (SSE) for bucket: $bucket")
res
}
}
if (null != l) l.end(event)
}
// I don't think we care what this is set to
def setRequestMethod(requestMethod: Int): Unit = debug(s"setRequestMethod($requestMethod)")
// Try to get the region of the S3 URL so we can set it on the S3Client
def getRegion(url: URL, bucket: String/*, client: AmazonS3*/): Regions = {
getRegionNameFromURL(url).toOptionalRegion orElse
getRegionNameFromDNS(bucket).toOptionalRegion orElse
Option(Regions.getCurrentRegion()).map{ _.getName }.toOptionalRegion getOrElse
Regions.DEFAULT_REGION
}
private implicit class RichStringOption(s: Option[String]) {
def toOptionalRegion: Option[Regions] = s.flatMap{ _.toOptionalRegion }
}
private implicit class RichString(s: String) {
def toOptionalRegion: Option[Regions] = Try{ Regions.fromName(s) }.toOption
}
def getRegionNameFromURL(url: URL): Option[String] = {
// We'll try the AmazonS3URI parsing first then fallback to our RegionMatcher
getAmazonS3URI(url).map{ _.getRegion }.flatMap{ Option(_) } orElse RegionMatcher.findFirstIn(url.toString)
}
// Not used anymore since the AmazonS3ClientBuilder requires the region during construction
// def getRegionNameFromService(bucket: String, client: AmazonS3): Option[String] = {
// // This might fail if the current credentials don't have access to the getBucketLocation call
// Try { client.getBucketLocation(bucket) }.toOption
// }
def getBucketAndKey(url: URL): (String, String) = {
// The AmazonS3URI constructor should work for standard S3 urls. But if a custom domain is being used
// (e.g. snapshots.maven.frugalmechanic.com) then we treat the hostname as the bucket and the path as the key
getAmazonS3URI(url).map{ amzn: AmazonS3URI =>
(amzn.getBucket, amzn.getKey)
}.getOrElse {
// Probably a custom domain name - The host should be the bucket and the path the key
(url.getHost, url.getPath.stripPrefix("/"))
}
}
def getAmazonS3URI(uri: String): Option[AmazonS3URI] = getAmazonS3URI(URI.create(uri))
def getAmazonS3URI(url: URL) : Option[AmazonS3URI] = getAmazonS3URI(url.toURI)
def getAmazonS3URI(uri: URI) : Option[AmazonS3URI] = try {
val httpsURI: URI =
// If there is no scheme (e.g. new URI("s3-us-west-2.amazonaws.com/<bucket>"))
// then we need to re-create the URI to add one and to also make sure the host is set
if (uri.getScheme == null) new URI("https://"+uri)
// AmazonS3URI can't parse the region from s3:// URLs so we rewrite the scheme to https://
else new URI("https", uri.getUserInfo, uri.getHost, uri.getPort, uri.getPath, uri.getQuery, uri.getFragment)
Some(new AmazonS3URI(httpsURI))
} catch {
case _: IllegalArgumentException => None
}
}