Skip to content

Commit 4049dca

Browse files
Azure Storage: Azure storage support #3253 (#3254)
* Setting up module for Azure storage #3253 --------- Co-authored-by: Shubham Girdhar <girdharshubham@hotmail.com>
1 parent 6ef874c commit 4049dca

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+5548
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
alpakka {
2+
azure-storage {
3+
api-version = "2024-11-04"
4+
api-version = ${?AZURE_STORAGE_API_VERSION}
5+
signing-algorithm = "HmacSHA256"
6+
7+
# for local testing via emulator
8+
# endpoint-url = ""
9+
10+
#azure-credentials
11+
credentials {
12+
# valid values are anon (annonymous), SharedKey, and sas
13+
authorization-type = anon
14+
authorization-type = ${?AZURE_STORAGE_AUTHORIZATION_TYPE}
15+
16+
# required for all authorization types
17+
account-name = ""
18+
account-name = ${?AZURE_STORAGE_ACCOUNT_NAME}
19+
20+
# Account key is required for SharedKey or SharedKeyLite authorization
21+
account-key = none
22+
account-key = ${?AZURE_STORAGE_ACCOUNT_KEY}
23+
24+
# SAS token for sas authorization
25+
sas-token = ""
26+
sas-token = ${?AZURE_STORAGE_SAS_TOKEN}
27+
}
28+
#azure-credentials
29+
30+
# Default settings corresponding to automatic retry of requests in an Azure Blob Storage stream.
31+
retry-settings {
32+
# The maximum number of additional attempts (following transient errors) that will be made to process a given
33+
# request before giving up.
34+
max-retries = 3
35+
36+
# The minimum delay between request retries.
37+
min-backoff = 200ms
38+
39+
# The maximum delay between request retries.
40+
max-backoff = 10s
41+
42+
# Random jitter factor applied to retry delay calculation.
43+
random-factor = 0.0
44+
}
45+
}
46+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright (C) since 2016 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
package akka.stream.alpakka
6+
package azure
7+
package storage
8+
9+
import akka.stream.Attributes
10+
import akka.stream.Attributes.Attribute
11+
12+
/**
13+
* Akka Stream attributes that are used when materializing AzureStorage stream blueprints.
14+
*/
15+
object StorageAttributes {
16+
17+
/**
18+
* Settings to use for the Azure Blob Storage stream
19+
*/
20+
def settings(settings: StorageSettings): Attributes = Attributes(StorageSettingsValue(settings))
21+
22+
/**
23+
* Config path which will be used to resolve required AzureStorage settings
24+
*/
25+
def settingsPath(path: String): Attributes = Attributes(StorageSettingsPath(path))
26+
27+
/**
28+
* Default settings
29+
*/
30+
def defaultSettings: Attributes = Attributes(StorageSettingsPath.Default)
31+
}
32+
33+
final class StorageSettingsPath private (val path: String) extends Attribute
34+
35+
object StorageSettingsPath {
36+
val Default: StorageSettingsPath = StorageSettingsPath(StorageSettings.ConfigPath)
37+
38+
def apply(path: String) = new StorageSettingsPath(path)
39+
}
40+
41+
final class StorageSettingsValue private (val settings: StorageSettings) extends Attribute
42+
43+
object StorageSettingsValue {
44+
def apply(settings: StorageSettings) = new StorageSettingsValue(settings)
45+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright (C) since 2016 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
package akka.stream.alpakka
6+
package azure
7+
package storage
8+
9+
import akka.http.scaladsl.model.StatusCode
10+
11+
import scala.util.{Failure, Success, Try}
12+
import scala.xml.{Elem, NodeSeq, XML}
13+
14+
final case class StorageException(statusCode: StatusCode,
15+
errorCode: String,
16+
errorMessage: String,
17+
resourceName: Option[String],
18+
resourceValue: Option[String],
19+
reason: Option[String])
20+
extends RuntimeException(errorMessage) {
21+
22+
override def toString: String =
23+
s"""StorageException(
24+
|statusCode=$statusCode,
25+
| errorCode=$errorCode,
26+
| errorMessage=$errorMessage,
27+
| resourceName=$resourceName,
28+
| resourceValue=$resourceValue,
29+
| reason=$reason
30+
|)""".stripMargin.replaceAll(System.lineSeparator(), "")
31+
}
32+
33+
object StorageException {
34+
def apply(statusCode: StatusCode,
35+
errorCode: String,
36+
errorMessage: String,
37+
resourceName: Option[String],
38+
resourceValue: Option[String],
39+
reason: Option[String]): StorageException =
40+
new StorageException(statusCode, errorCode, errorMessage, resourceName, resourceValue, reason)
41+
42+
def apply(response: String, statusCode: StatusCode): StorageException = {
43+
def getOptionalValue(xmlResponse: Elem, elementName: String, fallBackElementName: Option[String]) = {
44+
val element = xmlResponse \ elementName
45+
val node =
46+
if (element.nonEmpty) element
47+
else if (fallBackElementName.nonEmpty) xmlResponse \ fallBackElementName.get
48+
else NodeSeq.Empty
49+
50+
emptyStringToOption(node.text)
51+
}
52+
53+
Try {
54+
val utf8_bom = "\uFEFF"
55+
val sanitizedResponse = if (response.startsWith(utf8_bom)) response.substring(1) else response
56+
val xmlResponse = XML.loadString(sanitizedResponse)
57+
StorageException(
58+
statusCode = statusCode,
59+
errorCode = (xmlResponse \ "Code").text,
60+
errorMessage = (xmlResponse \ "Message").text,
61+
resourceName = getOptionalValue(xmlResponse, "QueryParameterName", Some("HeaderName")),
62+
resourceValue = getOptionalValue(xmlResponse, "QueryParameterValue", Some("HeaderValue")),
63+
reason = getOptionalValue(xmlResponse, "Reason", Some("AuthenticationErrorDetail"))
64+
)
65+
} match {
66+
case Failure(ex) =>
67+
val errorMessage = emptyStringToOption(ex.getMessage)
68+
StorageException(
69+
statusCode = statusCode,
70+
errorCode = errorMessage.getOrElse("null"),
71+
errorMessage = emptyStringToOption(response).orElse(errorMessage).getOrElse("null"),
72+
resourceName = None,
73+
resourceValue = None,
74+
reason = None
75+
)
76+
case Success(value) => value
77+
}
78+
79+
}
80+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright (C) since 2016 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
package akka.stream.alpakka
6+
package azure
7+
package storage
8+
9+
import akka.actor.{
10+
ActorSystem,
11+
ClassicActorSystemProvider,
12+
ExtendedActorSystem,
13+
Extension,
14+
ExtensionId,
15+
ExtensionIdProvider
16+
}
17+
18+
/**
19+
* Manages one [[StorageSettings]] per `ActorSystem`.
20+
*/
21+
final class StorageExt private (sys: ExtendedActorSystem) extends Extension {
22+
val settings: StorageSettings = settings(StorageSettings.ConfigPath)
23+
24+
def settings(prefix: String): StorageSettings = StorageSettings(sys.settings.config.getConfig(prefix))
25+
}
26+
27+
object StorageExt extends ExtensionId[StorageExt] with ExtensionIdProvider {
28+
override def lookup: StorageExt.type = StorageExt
29+
override def createExtension(system: ExtendedActorSystem) = new StorageExt(system)
30+
31+
/**
32+
* Java API.
33+
* Get the Storage extension with the classic actors API.
34+
*/
35+
override def get(system: ActorSystem): StorageExt = super.apply(system)
36+
37+
/**
38+
* Java API.
39+
* Get the Storage extension with the new actors API.
40+
*/
41+
override def get(system: ClassicActorSystemProvider): StorageExt = super.apply(system)
42+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (C) since 2016 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
package akka.stream.alpakka
6+
package azure
7+
package storage
8+
package headers
9+
10+
import akka.annotation.InternalApi
11+
import akka.http.scaladsl.model.HttpHeader
12+
import akka.http.scaladsl.model.headers.RawHeader
13+
14+
import java.security.MessageDigest
15+
import java.util.{Base64, Objects}
16+
17+
sealed abstract class ServerSideEncryption {
18+
@InternalApi private[storage] def headers: Seq[HttpHeader]
19+
}
20+
21+
object ServerSideEncryption {
22+
def customerKey(key: String, hash: Option[String]): ServerSideEncryption = new CustomerKey(key, hash)
23+
def customerKey(key: String): ServerSideEncryption = customerKey(key, None)
24+
}
25+
26+
final class CustomerKey private[headers] (val key: String, val hash: Option[String] = None)
27+
extends ServerSideEncryption {
28+
override private[storage] def headers: Seq[HttpHeader] = Seq(
29+
RawHeader("x-ms-encryption-algorithm", "AES256"),
30+
RawHeader("x-ms-encryption-key", key),
31+
RawHeader("x-ms-encryption-key-sha256", hash.getOrElse(createHash))
32+
)
33+
34+
override def equals(obj: Any): Boolean =
35+
obj match {
36+
case other: CustomerKey => key == other.key && hash == other.hash
37+
case _ => false
38+
}
39+
40+
override def hashCode(): Int = Objects.hash(key, hash)
41+
42+
override def toString: String =
43+
s"""ServerSideEncryption.CustomerKeys(
44+
|key=$key,
45+
| hash=$hash
46+
|)
47+
|""".stripMargin.replaceAll(System.lineSeparator(), "")
48+
49+
private def createHash = {
50+
val messageDigest = MessageDigest.getInstance("SHA-256")
51+
val decodedKey = messageDigest.digest(Base64.getDecoder.decode(key))
52+
Base64.getEncoder.encodeToString(decodedKey)
53+
}
54+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (C) since 2016 Lightbend Inc. <https://www.lightbend.com>
3+
*/
4+
5+
package akka.stream.alpakka
6+
package azure
7+
package storage
8+
package headers
9+
10+
import akka.annotation.InternalApi
11+
import akka.http.scaladsl.model.{ContentType, HttpHeader}
12+
import akka.http.scaladsl.model.headers.{CustomHeader, RawHeader}
13+
14+
private[storage] case class CustomContentTypeHeader(contentType: ContentType) extends CustomHeader {
15+
override def name(): String = "Content-Type"
16+
17+
override def value(): String = contentType.value
18+
19+
override def renderInRequests(): Boolean = true
20+
21+
override def renderInResponses(): Boolean = true
22+
}
23+
24+
private[storage] case class CustomContentLengthHeader(contentLength: Long) extends CustomHeader {
25+
override def name(): String = "Content-Length"
26+
27+
override def value(): String = contentLength.toString
28+
29+
override def renderInRequests(): Boolean = true
30+
31+
override def renderInResponses(): Boolean = true
32+
}
33+
34+
private[storage] case class BlobTypeHeader(blobType: String) {
35+
@InternalApi private[storage] def header: HttpHeader = RawHeader(BlobTypeHeaderKey, blobType)
36+
}
37+
38+
object BlobTypeHeader {
39+
private[storage] val BlockBlobHeader = new BlobTypeHeader(BlockBlobType)
40+
private[storage] val PageBlobHeader = new BlobTypeHeader(PageBlobType)
41+
private[storage] val AppendBlobHeader = new BlobTypeHeader(AppendBlobType)
42+
}
43+
44+
private[storage] case class RangeWriteTypeHeader(headerName: String, writeType: String) {
45+
@InternalApi private[storage] def header: HttpHeader = RawHeader(headerName, writeType)
46+
}
47+
48+
object RangeWriteTypeHeader {
49+
private[storage] val UpdateFileHeader = new RangeWriteTypeHeader(FileWriteTypeHeaderKey, "update")
50+
private[storage] val ClearFileHeader = new RangeWriteTypeHeader(FileWriteTypeHeaderKey, "clear")
51+
private[storage] val UpdatePageHeader = new RangeWriteTypeHeader(PageWriteTypeHeaderKey, "update")
52+
private[storage] val ClearPageHeader = new RangeWriteTypeHeader(PageWriteTypeHeaderKey, "clear")
53+
}

0 commit comments

Comments
 (0)