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}