diff --git a/README.md b/README.md index 392e02c0..31dbd835 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ and the `ipa` is _api_ reversed. Aaand... `ipa` is also an awesome type of beer - **`rc_schema`**: Add `ToSchema` support for `Arc` and `Rc` types. **Note!** serde `rc` feature flag must be enabled separately to allow serialization and deserialization of `Arc` and `Rc` types. See more about [serde feature flags](https://serde.rs/feature-flags.html). - **`config`** Enables [`utoipa-config`](./utoipa-config/README.md) for the project which allows defining global configuration options for `utoipa`. +- **`cache`** Enables caching of the Swagger UI download in `utoipa-swagger-ui` during the build process. ### Default Library Support diff --git a/utoipa-swagger-ui/CHANGELOG.md b/utoipa-swagger-ui/CHANGELOG.md index cbd839f5..5b02b17f 100644 --- a/utoipa-swagger-ui/CHANGELOG.md +++ b/utoipa-swagger-ui/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +* Add `cache` feature to cache swagger ui zip in build script (https://github.com/juhaku/utoipa/pull/1214) * Allow disabling syntax highlighting (https://github.com/juhaku/utoipa/pull/1188) ## 8.0.3 - Oct 23 2024 diff --git a/utoipa-swagger-ui/Cargo.toml b/utoipa-swagger-ui/Cargo.toml index 13ed4908..b12dd8a4 100644 --- a/utoipa-swagger-ui/Cargo.toml +++ b/utoipa-swagger-ui/Cargo.toml @@ -18,6 +18,8 @@ debug-embed = ["rust-embed/debug-embed"] reqwest = ["dep:reqwest"] url = ["dep:url"] vendored = ["dep:utoipa-swagger-ui-vendored"] +# cache swagger ui zip +cache = ["dep:dirs", "dep:sha2"] [dependencies] rust-embed = { version = "8" } @@ -40,7 +42,7 @@ tokio = { version = "1", features = ["macros"] } utoipa-swagger-ui = { path = ".", features = ["actix-web", "axum", "rocket"] } [package.metadata.docs.rs] -features = ["actix-web", "axum", "rocket", "vendored"] +features = ["actix-web", "axum", "rocket", "vendored", "cache"] no-default-features = true rustdoc-args = ["--cfg", "doc_cfg"] @@ -48,6 +50,10 @@ rustdoc-args = ["--cfg", "doc_cfg"] zip = { version = "2", default-features = false, features = ["deflate"] } regex = "1.7" +# used by cache feature +dirs = { version = "5.0.1", optional = true } +sha2 = { version = "0.10.8", optional = true } + # enabled optionally to allow rust only build with expense of bigger dependency tree and platform # independent build. By default `curl` system package is tried for downloading the Swagger UI. reqwest = { version = "0.12", features = [ diff --git a/utoipa-swagger-ui/build.rs b/utoipa-swagger-ui/build.rs index 3f9a5359..8f88c023 100644 --- a/utoipa-swagger-ui/build.rs +++ b/utoipa-swagger-ui/build.rs @@ -24,6 +24,20 @@ const SWAGGER_UI_DOWNLOAD_URL_DEFAULT: &str = const SWAGGER_UI_DOWNLOAD_URL: &str = "SWAGGER_UI_DOWNLOAD_URL"; const SWAGGER_UI_OVERWRITE_FOLDER: &str = "SWAGGER_UI_OVERWRITE_FOLDER"; +#[cfg(feature = "cache")] +fn sha256(data: &[u8]) -> String { + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(data); + let hash = hasher.finalize(); + format!("{:x}", hash).to_uppercase() +} + +#[cfg(feature = "cache")] +fn get_cache_dir() -> Option { + dirs::cache_dir().map(|p| p.join("utoipa-swagger-ui")) +} + fn main() { let target_dir = env::var("OUT_DIR").unwrap(); println!("OUT_DIR: {target_dir}"); @@ -137,7 +151,8 @@ impl SwaggerZip { fn get_zip_archive(url: &str, target_dir: &str) -> SwaggerZip { let zip_filename = url.split('/').last().unwrap().to_string(); - let zip_path = [target_dir, &zip_filename].iter().collect::(); + #[allow(unused_mut)] + let mut zip_path = [target_dir, &zip_filename].iter().collect::(); if env::var("CARGO_FEATURE_VENDORED").is_ok() { #[cfg(not(feature = "vendored"))] @@ -173,15 +188,37 @@ fn get_zip_archive(url: &str, target_dir: &str) -> SwaggerZip { .expect("failed to open file protocol copied Swagger UI"); SwaggerZip::File(zip) } else if url.starts_with("http://") || url.starts_with("https://") { - println!("start download to : {:?}", zip_path); - // with http protocol we update when the 'SWAGGER_UI_DOWNLOAD_URL' changes println!("cargo:rerun-if-env-changed={SWAGGER_UI_DOWNLOAD_URL}"); - download_file(url, zip_path.clone()) - .unwrap_or_else(|error| panic!("failed to download Swagger UI: {error}")); - let swagger_ui_zip = - File::open([target_dir, &zip_filename].iter().collect::()).unwrap(); + // Update zip_path to point to the resolved cache directory + #[cfg(feature = "cache")] + { + // Compute cache key based hashed URL + crate version + let mut cache_key = String::new(); + cache_key.push_str(url); + cache_key.push_str(&env::var("CARGO_PKG_VERSION").unwrap_or_default()); + let cache_key = sha256(cache_key.as_bytes()); + // Store the cache in the cache_key directory inside the OS's default cache folder + let mut cache_dir = if let Some(dir) = get_cache_dir() { + dir.join("swagger-ui").join(&cache_key) + } else { + println!("cargo:warning=Could not determine cache directory, using OUT_DIR"); + PathBuf::from(env::var("OUT_DIR").unwrap()) + }; + if fs::create_dir_all(&cache_dir).is_err() { + cache_dir = env::var("OUT_DIR").unwrap().into(); + } + zip_path = cache_dir.join(&zip_filename); + } + + if zip_path.exists() { + println!("using cached zip path from : {:?}", zip_path); + } else { + println!("start download to : {:?}", zip_path); + download_file(url, zip_path.clone()).expect("failed to download Swagger UI"); + } + let swagger_ui_zip = File::open(zip_path).unwrap(); let zip = ZipArchive::new(swagger_ui_zip).expect("failed to open downloaded Swagger UI"); SwaggerZip::File(zip) } else {