Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(swagger-ui): cache swagger zip #1214

Merged
merged 11 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions utoipa-swagger-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ debug-embed = ["rust-embed/debug-embed"]
reqwest = ["dep:reqwest"]
url = ["dep:url"]
vendored = ["dep:utoipa-swagger-ui-vendored"]
cache = []

[dependencies]
rust-embed = { version = "8" }
Expand Down
59 changes: 41 additions & 18 deletions utoipa-swagger-ui/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ use std::{
use regex::Regex;
use zip::{result::ZipError, ZipArchive};

#[path = "src/internal/mod.rs"]
#[cfg(feature = "cache")]
mod internal;

#[cfg(feature = "cache")]
use internal::dirs::cache_dir;

/// the following env variables control the build process:
/// 1. SWAGGER_UI_DOWNLOAD_URL:
/// + the url from where to download the swagger-ui zip file if starts with http:// or https://
Expand All @@ -24,6 +31,11 @@ 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";

// wget <url> && sha256sum <file> | tr '[a-z]' '[A-Z]'
#[cfg(feature = "cache")]
const SWAGGER_UI_FILE_HASH: &str =
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But what happens if the user changes the url? I guess the validation will then just fail?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it is only used as a suffix, well this is not very good to be hard coded because users can change the SWAGGER_UI_DOWNLOAD_URL which effectively is changing the version that changes the hash as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it is only used as a suffix, well this is not very good to be hard coded because users can change the SWAGGER_UI_DOWNLOAD_URL which effectively is changing the version that changes the hash as well.

I think we should avoid use cache if user supplied the URL

"481244D0812097B11FBAEEF79F71D942B171617F9C9F9514E63ACBE13E71CCDC";

fn main() {
let target_dir = env::var("OUT_DIR").unwrap();
println!("OUT_DIR: {target_dir}");
Expand Down Expand Up @@ -139,7 +151,7 @@ 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::<PathBuf>();

if env::var("CARGO_FEATURE_VENDORED").is_ok() {
if cfg!(feature = "vendored") {
#[cfg(not(feature = "vendored"))]
unreachable!("Cannot get vendored Swagger UI without `vendored` flag");

Expand Down Expand Up @@ -173,15 +185,26 @@ 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);
let mut cache_dir = cache_dir()
.expect("could not determine cache directory")
.join("swagger-ui")
.join(SWAGGER_UI_FILE_HASH);

// with http protocol we update when the 'SWAGGER_UI_DOWNLOAD_URL' changes
println!("cargo:rerun-if-env-changed={SWAGGER_UI_DOWNLOAD_URL}");
if fs::create_dir_all(&cache_dir).is_err() {
cache_dir = env::var("OUT_DIR").unwrap().into();
}
let zip_cache_path = cache_dir.join(&zip_filename);

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::<PathBuf>()).unwrap();
if zip_cache_path.exists() {
println!("using cached zip path from : {:?}", zip_cache_path);
} else {
println!("start download to : {:?}", zip_cache_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_cache_path).expect("failed to download Swagger UI");
}
let swagger_ui_zip = File::open(&zip_cache_path).unwrap();
let zip = ZipArchive::new(swagger_ui_zip).expect("failed to open downloaded Swagger UI");
SwaggerZip::File(zip)
} else {
Expand Down Expand Up @@ -223,21 +246,20 @@ struct SwaggerUiDist;
fs::write(path, contents).unwrap();
}

fn download_file(url: &str, path: PathBuf) -> Result<(), Box<dyn Error>> {
let reqwest_feature = env::var("CARGO_FEATURE_REQWEST");
println!("reqwest feature: {reqwest_feature:?}");
if reqwest_feature.is_ok() {
#[cfg(feature = "reqwest")]
download_file_reqwest(url, path)?;
Ok(())
} else {
fn download_file(url: &str, path: &Path) -> Result<(), Box<dyn Error>> {
#[cfg(feature = "reqwest")]
{
download_file_reqwest(url, path)
}
#[cfg(not(feature = "reqwest"))]
{
println!("trying to download using `curl` system package");
download_file_curl(url, path.as_path())
download_file_curl(url, path)
}
}

#[cfg(feature = "reqwest")]
fn download_file_reqwest(url: &str, path: PathBuf) -> Result<(), Box<dyn Error>> {
fn download_file_reqwest(url: &str, path: &Path) -> Result<(), Box<dyn Error>> {
let mut client_builder = reqwest::blocking::Client::builder();

if let Ok(cainfo) = env::var("CARGO_HTTP_CAINFO") {
Expand Down Expand Up @@ -266,6 +288,7 @@ fn parse_ca_file(path: &str) -> Result<reqwest::Certificate, Box<dyn Error>> {
Ok(cert)
}

#[cfg(not(feature = "reqwest"))]
fn download_file_curl<T: AsRef<Path>>(url: &str, target_dir: T) -> Result<(), Box<dyn Error>> {
// Not using `CARGO_CFG_TARGET_OS` because of the possibility of cross-compilation.
// When targeting `x86_64-pc-windows-gnu` on Linux for example, `cfg!()` in the
Expand Down
206 changes: 206 additions & 0 deletions utoipa-swagger-ui/src/internal/dirs.rs
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I am not educated enough, do we need all that C crap and pointer juggling? Aren't there a better and perhaps more simply way to the the same result or at least similar result?

As I understand this tries to use the users local cache dir for caching the data. I'd rather place it just the OUTPUT dir of the crate in question. Or target dir since the OUTPUT dir can change?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd expect at least some level of documentation what all that black magic does. Whats more is that I can guarantee that there are few that even dare to touch this code and I don't want to maintain this code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I am not educated enough, do we need all that C crap and pointer juggling? Aren't there a better and perhaps more simply way to the the same result or at least similar result?

I also doesn't like that. though it's that expressive because the original dev wanted to support as many platforms as he can. but it's just for caching and I guess windows/linux/macos support is enough so we can simplify it into few lines

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand this tries to use the users local cache dir for caching the data. I'd rather place it just the OUTPUT dir of the crate in question. Or target dir since the OUTPUT dir can change?

We shouldn't use OUTPUT dir the whole idea is to cache the downlloaded file in known location that will be still cached even if we rebuild the whole project / delete target folder and rebuild

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd expect at least some level of documentation what all that black magic does. Whats more is that I can guarantee that there are few that even dare to touch this code and I don't want to maintain this code.

It just returns the cache directory on different platforms

Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// based on https://github.com/pykeio/ort/blob/main/ort-sys/src/internal/dirs.rs and https://github.com/dirs-dev/dirs-sys-rs/blob/main/src/lib.rs

pub const PACKAGE_NAME: &str = "utoipa-swagger-ui";

#[cfg(all(target_os = "windows", target_arch = "x86"))]
macro_rules! win32_extern {
($library:literal $abi:literal $($link_name:literal)? $(#[$doc:meta])? fn $($function:tt)*) => (
#[link(name = $library, kind = "raw-dylib", modifiers = "+verbatim", import_name_type = "undecorated")]
extern $abi {
$(#[$doc])?
$(#[link_name=$link_name])?
fn $($function)*;
}
)
}
#[cfg(all(target_os = "windows", not(target_arch = "x86")))]
macro_rules! win32_extern {
($library:literal $abi:literal $($link_name:literal)? $(#[$doc:meta])? fn $($function:tt)*) => (
#[link(name = $library, kind = "raw-dylib", modifiers = "+verbatim")]
extern "C" {
$(#[$doc])?
$(#[link_name=$link_name])?
fn $($function)*;
}
)
}

#[cfg(target_os = "windows")]
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
mod windows {
use std::{
ffi::{c_void, OsString},
os::windows::prelude::OsStringExt,
path::PathBuf,
ptr, slice,
};

#[repr(C)]
#[derive(Clone, Copy)]
struct GUID {
data1: u32,
data2: u16,
data3: u16,
data4: [u8; 8],
}

impl GUID {
pub const fn from_u128(uuid: u128) -> Self {
Self {
data1: (uuid >> 96) as u32,
data2: (uuid >> 80 & 0xffff) as u16,
data3: (uuid >> 64 & 0xffff) as u16,
#[allow(clippy::cast_possible_truncation)]
data4: (uuid as u64).to_be_bytes(),
}
}
}

type HRESULT = i32;
type PWSTR = *mut u16;
type PCWSTR = *const u16;
type HANDLE = isize;
type KNOWN_FOLDER_FLAG = i32;

win32_extern!("SHELL32.DLL" "system" fn SHGetKnownFolderPath(rfid: *const GUID, dwflags: KNOWN_FOLDER_FLAG, htoken: HANDLE, ppszpath: *mut PWSTR) -> HRESULT);
win32_extern!("KERNEL32.DLL" "system" fn lstrlenW(lpstring: PCWSTR) -> i32);
win32_extern!("OLE32.DLL" "system" fn CoTaskMemFree(pv: *const ::core::ffi::c_void) -> ());

fn known_folder(folder_id: GUID) -> Option<PathBuf> {
unsafe {
let mut path_ptr: PWSTR = ptr::null_mut();
let result = SHGetKnownFolderPath(&folder_id, 0, HANDLE::default(), &mut path_ptr);
if result == 0 {
let len = lstrlenW(path_ptr) as usize;
let path = slice::from_raw_parts(path_ptr, len);
let ostr: OsString = OsStringExt::from_wide(path);
CoTaskMemFree(path_ptr as *const c_void);
Some(PathBuf::from(ostr))
} else {
CoTaskMemFree(path_ptr as *const c_void);
None
}
}
}

#[allow(clippy::unusual_byte_groupings)]
const FOLDERID_LOCAL_APP_DATA: GUID = GUID::from_u128(0xf1b32785_6fba_4fcf_9d557b8e7f157091);

#[must_use]
pub fn known_folder_local_app_data() -> Option<PathBuf> {
known_folder(FOLDERID_LOCAL_APP_DATA)
}
}
#[cfg(target_os = "windows")]
#[must_use]
pub fn cache_dir() -> Option<std::path::PathBuf> {
self::windows::known_folder_local_app_data().map(|h| h.join(PACKAGE_NAME))
}

#[cfg(unix)]
#[allow(non_camel_case_types)]
mod unix {
use std::{
env,
ffi::{c_char, c_int, c_long, CStr, OsString},
mem,
os::unix::prelude::OsStringExt,
path::PathBuf,
ptr,
};

type uid_t = u32;
type gid_t = u32;
type size_t = usize;
#[repr(C)]
struct passwd {
pub pw_name: *mut c_char,
pub pw_passwd: *mut c_char,
pub pw_uid: uid_t,
pub pw_gid: gid_t,
pub pw_gecos: *mut c_char,
pub pw_dir: *mut c_char,
pub pw_shell: *mut c_char,
}

extern "C" {
fn sysconf(name: c_int) -> c_long;
fn getpwuid_r(
uid: uid_t,
pwd: *mut passwd,
buf: *mut c_char,
buflen: size_t,
result: *mut *mut passwd,
) -> c_int;
fn getuid() -> uid_t;
}

const SC_GETPW_R_SIZE_MAX: c_int = 70;

#[must_use]
#[cfg(target_os = "linux")]
pub fn is_absolute_path(path: OsString) -> Option<PathBuf> {
let path = PathBuf::from(path);
if path.is_absolute() {
Some(path)
} else {
None
}
}

#[cfg(not(target_os = "windows"))]
#[must_use]
pub fn home_dir() -> Option<PathBuf> {
return env::var_os("HOME")
.and_then(|h| if h.is_empty() { None } else { Some(h) })
.or_else(|| unsafe { fallback() })
.map(PathBuf::from);

#[cfg(any(target_os = "android", target_os = "ios", target_os = "emscripten"))]
unsafe fn fallback() -> Option<OsString> {
None
}
#[cfg(not(any(target_os = "android", target_os = "ios", target_os = "emscripten")))]
unsafe fn fallback() -> Option<OsString> {
let amt = match sysconf(SC_GETPW_R_SIZE_MAX) {
n if n < 0 => 512,
n => n as usize,
};
let mut buf = Vec::with_capacity(amt);
let mut passwd: passwd = mem::zeroed();
let mut result = ptr::null_mut();
match getpwuid_r(
getuid(),
&mut passwd,
buf.as_mut_ptr(),
buf.capacity(),
&mut result,
) {
0 if !result.is_null() => {
let ptr = passwd.pw_dir as *const _;
let bytes = CStr::from_ptr(ptr).to_bytes();
if bytes.is_empty() {
None
} else {
Some(OsStringExt::from_vec(bytes.to_vec()))
}
}
_ => None,
}
}
}
}

#[cfg(target_os = "linux")]
#[must_use]
pub fn cache_dir() -> Option<std::path::PathBuf> {
std::env::var_os("XDG_CACHE_HOME")
.and_then(self::unix::is_absolute_path)
.or_else(|| self::unix::home_dir().map(|h| h.join(".cache").join(PACKAGE_NAME)))
}

#[cfg(target_os = "macos")]
#[must_use]
pub fn cache_dir() -> Option<std::path::PathBuf> {
self::unix::home_dir().map(|h| h.join("Library/Caches").join(PACKAGE_NAME))
}
1 change: 1 addition & 0 deletions utoipa-swagger-ui/src/internal/mod.rs
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than using path/to/internal/mod.rs use path/to/intenral.rs This is a better syntax and is the recommended syntax over the mod.rs files. Eventually the code base ends up having a billion mod.rs files and that is not really compelling.

See more here:
https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html#modules-cheat-sheet
https://doc.rust-lang.org/stable/book/ch07-05-separating-modules-into-different-files.html#alternate-file-paths
https://doc.rust-lang.org/edition-guide/rust-2018/path-changes.html#no-more-modrs

The mod.rs syntax is old relic pre 2018 Edition time (before Rust 1.31) just to make the Rust compiler support the modules.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! didn't knew we can do the same thing without using mod.rs files. thanks

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod dirs;
Loading