awesome_operates/embed.rs
1#[cfg(unix)]
2use std::os::unix::fs::MetadataExt;
3use std::path::Path;
4
5use rust_embed::RustEmbed;
6use tokio::io::AsyncWriteExt;
7use tower_http::services::ServeDir;
8
9/// used with swagger openapi
10/// eg: I have a swagger.json at path swagger-files/api.json, so I can start a http service for generate swagger
11/// ```rust,no_run
12/// use awesome_operates::embed::{server_dir, EXTRACT_DIR_PATH};
13/// use awesome_operates::swagger::InitSwagger;
14/// use axum::{Router, Extension, routing::get, Json, response::{Response, IntoResponse}};
15/// use tower::ServiceBuilder;
16/// use tower_http::compression::CompressionLayer;
17/// use aide::openapi::OpenApi;
18/// use aide::transform::TransformOpenApi;
19/// use std::sync::Arc;
20///
21/// async fn serve_docs(Extension(api): Extension<Arc<OpenApi>>) -> Response {
22/// Json(serde_json::json!(*api)).into_response()
23/// }
24///
25/// fn api_docs(api: TransformOpenApi) -> TransformOpenApi {
26/// api.title("数据采集")
27/// }
28///
29/// #[tokio::test]
30/// async fn server() -> anyhow::Result<()> {
31/// aide::gen::on_error(|error| {
32/// println!("{error}")
33/// });
34/// aide::gen::extract_schemas(true);
35/// let mut api = OpenApi::default();
36///
37/// awesome_operates::extract_all_files!(awesome_operates::embed::Asset);
38/// InitSwagger::new(EXTRACT_DIR_PATH, "swagger-init.js", "swagger.html", "../api.json").build().await.unwrap();
39/// let app = Router::new()
40/// // .api_route("/example", post_with(handlers::example, handlers::example_docs))
41/// .nest_service("/docs/", server_dir(EXTRACT_DIR_PATH).await.unwrap())
42/// .route("/api.json", get(serve_docs))
43/// .finish_api_with(&mut api, api_docs)
44/// .layer(ServiceBuilder::new()
45/// .layer(CompressionLayer::new())
46/// .layer(Extension(Arc::new(api))));
47///
48/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
49/// # axum::serve(listener, app).await.unwrap();
50/// Ok(())
51/// }
52/// ```
53/// finally, you can visit at browser at http://127.0.0.1:3000/docs/ for your swagger
54#[derive(RustEmbed)]
55#[prefix = "embed_files/"]
56#[folder = "src/embed_files/"]
57pub struct Asset;
58
59pub const EXTRACT_SWAGGER_DIR_PATH: &str = "embed_files/swagger";
60pub const EXTRACT_DIR_PATH: &str = "embed_files";
61
62pub async fn server_dir(dir_path: &str) -> anyhow::Result<ServeDir> {
63 let dir_path_clone = dir_path.to_owned();
64 tokio::task::spawn_blocking(move || {
65 tokio::runtime::Handle::current().block_on(async move {
66 pre_compress_dir(&dir_path_clone).await;
67 });
68 });
69 Ok(ServeDir::new(dir_path)
70 .precompressed_br()
71 .precompressed_deflate()
72 .precompressed_gzip()
73 .precompressed_zstd())
74}
75
76/// only used for `pre_compress_dir`
77macro_rules! compress {
78 ($encoder:ident, $extension:expr, $data:expr, $path:expr) => {
79 let mut encoder = async_compression::tokio::write::$encoder::with_quality(
80 Vec::new(),
81 async_compression::Level::Best,
82 );
83 encoder.write_all(&$data).await?;
84 encoder.shutdown().await?;
85 let compressed = encoder.into_inner();
86 tokio::fs::write(format!("{}.{}", $path.display(), $extension), compressed).await?;
87 };
88}
89
90/// very time consuming operate, maybe even minitues
91/// use `tokio::spawn`
92/// ```rust
93/// use awesome_operates::embed::pre_compress_dir;
94///
95/// #[tokio::test]
96/// async fn compress_all() {
97/// tokio::task::spawn_blocking(move || {
98/// tokio::runtime::Handle::current().block_on(async move {
99/// pre_compress_dir("").await;
100/// });
101/// });
102/// }
103/// ```
104pub async fn pre_compress_dir(dir: &str) {
105 for entry in walkdir::WalkDir::new(dir)
106 .into_iter()
107 .filter_map(|e| e.ok())
108 .filter(|e| e.path().is_file() && !e.path().extension().unwrap_or_default().eq("br"))
109 {
110 multi_compress(entry.path())
111 .await
112 .unwrap_or_else(|e| tracing::warn!("pre compress failed with `{e:?}`"))
113 }
114 tracing::info!("pre brotli compress for {dir} over");
115}
116
117pub async fn multi_compress(path: &Path) -> anyhow::Result<()> {
118 let permissions = tokio::fs::metadata(path).await?;
119 #[cfg(unix)]
120 if permissions.mode() & 0o200 != 0 {
121 tracing::debug!("{} don't has write permission", path.display());
122 return Ok(());
123 }
124 tracing::debug!("pre compress {}", path.display());
125 let data = tokio::fs::read(path).await?;
126 compress!(BrotliEncoder, "br", data, path);
127 compress!(GzipEncoder, "gz", data, path);
128 Ok(())
129}