From 45a5565998a51af614647d49713b0135c577a3d2 Mon Sep 17 00:00:00 2001 From: Henrik Nygren Date: Fri, 4 Oct 2024 16:03:06 +0300 Subject: [PATCH] WIP azure blob storage --- services/headless-lms/Cargo.lock | 308 +++++++++++++++++- services/headless-lms/Cargo.toml | 1 + services/headless-lms/chatbot/Cargo.toml | 4 + .../chatbot/src/azure_blob_storage.rs | 85 +++++ services/headless-lms/chatbot/src/lib.rs | 1 + .../server/src/programs/chatbot_syncer.rs | 11 +- services/headless-lms/utils/src/lib.rs | 32 +- 7 files changed, 424 insertions(+), 18 deletions(-) create mode 100644 services/headless-lms/chatbot/src/azure_blob_storage.rs diff --git a/services/headless-lms/Cargo.lock b/services/headless-lms/Cargo.lock index 05656eb7358..9f610adc112 100644 --- a/services/headless-lms/Cargo.lock +++ b/services/headless-lms/Cargo.lock @@ -8,6 +8,12 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +[[package]] +name = "RustyXML" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5ace29ee3216de37c0546865ad08edef58b0f9e76838ed8959a84a990e58c5" + [[package]] name = "actix" version = "0.13.5" @@ -400,7 +406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -479,6 +485,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + [[package]] name = "async-compression" version = "0.4.12" @@ -493,6 +510,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -547,6 +575,92 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "azure_core" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ce3de4b65b1ee2667c81d1fc692949049502a4cf9c38118d811d6d79a7eaef" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "dyn-clone", + "futures", + "getrandom 0.2.15", + "hmac", + "http-types", + "once_cell", + "paste", + "pin-project", + "quick-xml 0.31.0", + "rand 0.8.5", + "reqwest 0.12.7", + "rustc_version", + "serde", + "serde_json", + "sha2", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "azure_storage" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9713002fc30956a9f4061cdbc2e912ff739c6160e138ad3b6d992b3bcedccc6d" +dependencies = [ + "RustyXML", + "async-lock", + "async-trait", + "azure_core", + "bytes", + "serde", + "serde_derive", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "azure_storage_blobs" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b3a31dd8f920739437b827d0c9f9a4011eb3f06f79a121764aa11af6c51ee2" +dependencies = [ + "RustyXML", + "azure_core", + "azure_storage", + "azure_svc_blobstorage", + "bytes", + "futures", + "serde", + "serde_derive", + "serde_json", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "azure_svc_blobstorage" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef37ba6180df451042f1c277d4d0898e2447f0a5d5072e0ff11ee6ea5e7ef38" +dependencies = [ + "azure_core", + "bytes", + "futures", + "log", + "once_cell", + "serde", + "serde_json", + "time", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -1060,6 +1174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1125,6 +1240,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "either" version = "1.13.0" @@ -1198,6 +1319,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "event-listener" version = "5.3.1" @@ -1209,6 +1336,25 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.1.1" @@ -1392,6 +1538,21 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.30" @@ -1443,6 +1604,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1452,7 +1624,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1567,7 +1739,7 @@ dependencies = [ "icu_provider", "icu_provider_blob", "once_cell", - "quick-xml", + "quick-xml 0.35.0", "resvg", "tracing", "url", @@ -1582,6 +1754,8 @@ version = "0.1.0" dependencies = [ "anyhow", "async-stream", + "azure_storage", + "azure_storage_blobs", "bytes", "chrono", "futures", @@ -1862,6 +2036,26 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel", + "base64 0.13.1", + "futures-lite", + "infer", + "pin-project-lite", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "url", +] + [[package]] name = "httparse" version = "1.9.4" @@ -2475,6 +2669,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + [[package]] name = "inout" version = "0.1.3" @@ -2484,6 +2684,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -2602,7 +2811,7 @@ dependencies = [ "chumsky", "email-encoding", "email_address", - "fastrand", + "fastrand 2.1.1", "futures-util", "hostname", "httpdate", @@ -2834,7 +3043,7 @@ dependencies = [ "hermit-abi", "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -3004,7 +3213,7 @@ checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" dependencies = [ "base64 0.13.1", "chrono", - "getrandom", + "getrandom 0.2.15", "http 0.2.12", "rand 0.8.5", "reqwest 0.11.27", @@ -3299,6 +3508,16 @@ dependencies = [ "cc", ] +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quick-xml" version = "0.35.0" @@ -3384,6 +3603,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -3391,10 +3623,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3420,13 +3662,31 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -3687,7 +3947,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin 0.9.8", "untrusted 0.9.0", @@ -3962,6 +4222,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4182,7 +4453,7 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener", + "event-listener 5.3.1", "futures-channel", "futures-core", "futures-intrusive", @@ -4529,7 +4800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", - "fastrand", + "fastrand 2.1.1", "once_cell", "rustix", "windows-sys 0.59.0", @@ -4582,6 +4853,7 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "js-sys", "num-conv", "powerfmt", "serde", @@ -5028,7 +5300,7 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ - "getrandom", + "getrandom 0.2.15", "serde", "sha1_smol", ] @@ -5057,6 +5329,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "walkdir" version = "2.5.0" @@ -5076,6 +5354,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/services/headless-lms/Cargo.toml b/services/headless-lms/Cargo.toml index d69ada8b220..fc1f9889369 100644 --- a/services/headless-lms/Cargo.toml +++ b/services/headless-lms/Cargo.toml @@ -6,6 +6,7 @@ members = [ "utils", "doc-macro", "certificates", + "chatbot", "langs-api", ] diff --git a/services/headless-lms/chatbot/Cargo.toml b/services/headless-lms/chatbot/Cargo.toml index 9f93af90d18..fb4b2a03631 100644 --- a/services/headless-lms/chatbot/Cargo.toml +++ b/services/headless-lms/chatbot/Cargo.toml @@ -38,3 +38,7 @@ sqlx.workspace = true async-stream = "0.3.5" # Types and traits for working with bytes bytes = "1.5.0" +# Azure Storage crate from the Azure SDK for Rust +azure_storage = "0.20.0" +# Azure Blob Storage crate from the Azure SDK for Rust +azure_storage_blobs = "0.20.0" diff --git a/services/headless-lms/chatbot/src/azure_blob_storage.rs b/services/headless-lms/chatbot/src/azure_blob_storage.rs new file mode 100644 index 00000000000..4df8d1d05f9 --- /dev/null +++ b/services/headless-lms/chatbot/src/azure_blob_storage.rs @@ -0,0 +1,85 @@ +use crate::prelude::*; +use anyhow::Context; +use azure_storage::StorageCredentials; +use azure_storage_blobs::prelude::*; +use headless_lms_utils::{ApplicationConfiguration, AzureBlobStorageConfiguration}; +use std::path::Path; + +/// A client for interacting with Azure Blob Storage. +pub struct AzureBlobClient { + container_client: ContainerClient, +} + +impl AzureBlobClient { + pub async fn new( + app_config: &ApplicationConfiguration, + container_name_prefix: &str, + ) -> anyhow::Result { + let azure_configuration = app_config + .azure_configuration + .as_ref() + .context("Azure configuration is missing")?; + let AzureBlobStorageConfiguration { + storage_account, + access_key, + } = azure_configuration + .blob_storage_config + .clone() + .context("Azure Blob Storage configuration is missing")?; + + let container_name = format!("{}-chatbot", container_name_prefix); + + let storage_credentials = StorageCredentials::access_key(&storage_account, access_key); + let blob_service_client = BlobServiceClient::new(storage_account, storage_credentials); + let container_client = blob_service_client.container_client(container_name); + + Ok(AzureBlobClient { container_client }) + } + + /// Ensures the container used to store the blobs exists. If it does not, the container is created. + pub async fn ensure_container_exists(&self) -> anyhow::Result<()> { + if self.container_client.exists().await? { + return Ok(()); + } + + info!( + "Azure blob storage container '{}' does not exist. Creating...", + self.container_client.container_name() + ); + self.container_client + .create() + .public_access(PublicAccess::None) + .await?; + Ok(()) + } + + /// Uploads a file to the specified container. + pub async fn upload_file(&self, path: &Path, file_bytes: &[u8]) -> anyhow::Result<()> { + let blob_name = Self::convert_path_to_blob_name(path); + let blob_client = self.container_client.blob_client(&blob_name); + + blob_client.put_block_blob(file_bytes.to_vec()).await?; + + info!("Blob '{}' uploaded successfully.", &blob_name); + Ok(()) + } + + /// Deletes a file (blob) from the specified container. + pub async fn delete_file(&self, path: &Path) -> anyhow::Result<()> { + let blob_name = Self::convert_path_to_blob_name(path); + let blob_client = self.container_client.blob_client(&blob_name); + + blob_client.delete().await?; + + info!("Blob '{}' deleted successfully.", &blob_name); + Ok(()) + } + + /// Converts a path to a blob name by joining its components with '/'. + fn convert_path_to_blob_name(path: &Path) -> String { + path.components() + .map(|comp| comp.as_os_str().to_string_lossy()) + .collect::>() + .join("/") + } +} diff --git a/services/headless-lms/chatbot/src/lib.rs b/services/headless-lms/chatbot/src/lib.rs index e2ee499ce83..2425c7ea073 100644 --- a/services/headless-lms/chatbot/src/lib.rs +++ b/services/headless-lms/chatbot/src/lib.rs @@ -1,5 +1,6 @@ //! For chatbot-related features. +pub mod azure_blob_storage; pub mod azure_chatbot; pub mod azure_search_index; pub(crate) mod prelude; diff --git a/services/headless-lms/server/src/programs/chatbot_syncer.rs b/services/headless-lms/server/src/programs/chatbot_syncer.rs index 35916694c72..722aae544f1 100644 --- a/services/headless-lms/server/src/programs/chatbot_syncer.rs +++ b/services/headless-lms/server/src/programs/chatbot_syncer.rs @@ -7,8 +7,9 @@ use std::{ use crate::setup_tracing; use dotenv::dotenv; -use headless_lms_chatbot::azure_search_index::{ - add_documents_to_index, create_search_index, does_search_index_exist, +use headless_lms_chatbot::{ + azure_blob_storage::AzureBlobClient, + azure_search_index::{add_documents_to_index, create_search_index, does_search_index_exist}, }; use headless_lms_models::{page_history::PageHistory, pages::Page}; use headless_lms_utils::{ @@ -28,12 +29,14 @@ pub async fn main() -> anyhow::Result<()> { let base_url = Url::parse(&env::var("BASE_URL").expect("BASE_URL must be defined")) .expect("BASE_URL must be a valid URL"); - let index_name_prefix = base_url + let name_prefix = base_url .host_str() .expect("BASE_URL must have a host") .replace(".", "-"); let app_config = ApplicationConfiguration::try_from_env()?; + let blob_storage = AzureBlobClient::new(&app_config, &name_prefix).await?; + blob_storage.ensure_container_exists().await?; let db_pool = PgPool::connect(&database_url).await?; let mut conn = db_pool.acquire().await?; @@ -48,7 +51,7 @@ pub async fn main() -> anyhow::Result<()> { // Occasionally prints a reminder that the service is still running ticks = 0; tracing::info!("Syncing pages to chatbot backend."); - sync_pages(&mut conn, &index_name_prefix, &app_config).await?; + sync_pages(&mut conn, &name_prefix, &app_config).await?; } } } diff --git a/services/headless-lms/utils/src/lib.rs b/services/headless-lms/utils/src/lib.rs index a2cb7538299..49624737cec 100644 --- a/services/headless-lms/utils/src/lib.rs +++ b/services/headless-lms/utils/src/lib.rs @@ -134,24 +134,52 @@ impl AzureSearchConfiguration { } } +#[derive(Clone, PartialEq)] +pub struct AzureBlobStorageConfiguration { + pub storage_account: String, + pub access_key: String, +} + +impl AzureBlobStorageConfiguration { + /// Attempts to create an AzureBlobStorageConfiguration from environment variables. + /// Returns `Ok(Some(AzureBlobStorageConfiguration))` if both environment variables are set. + /// Returns `Ok(None)` if no environment variables are set for blob storage. + pub fn try_from_env() -> anyhow::Result> { + let storage_account = env::var("AZURE_BLOB_STORAGE_ACCOUNT").ok(); + let access_key = env::var("AZURE_BLOB_STORAGE_ACCESS_KEY").ok(); + + if let (Some(storage_account), Some(access_key)) = (storage_account, access_key) { + Ok(Some(AzureBlobStorageConfiguration { + storage_account, + access_key, + })) + } else { + Ok(None) + } + } +} + #[derive(Clone, PartialEq)] pub struct AzureConfiguration { pub chatbot_config: Option, pub search_config: Option, + pub blob_storage_config: Option, } impl AzureConfiguration { /// Attempts to create an AzureConfiguration by calling the individual try_from_env functions. - /// Returns `Ok(Some(AzureConfiguration))` if either chatbot or search_config configurations are set. + /// Returns `Ok(Some(AzureConfiguration))` if any of the configurations are set. /// Returns `Ok(None)` if no relevant environment variables are set. pub fn try_from_env() -> anyhow::Result> { let chatbot = AzureChatbotConfiguration::try_from_env()?; let search_config = AzureSearchConfiguration::try_from_env()?; + let blob_storage_config = AzureBlobStorageConfiguration::try_from_env()?; - if chatbot.is_some() || search_config.is_some() { + if chatbot.is_some() || search_config.is_some() || blob_storage_config.is_some() { Ok(Some(AzureConfiguration { chatbot_config: chatbot, search_config, + blob_storage_config, })) } else { Ok(None)