]> OzVa Git service - ozva-cloud/commitdiff
feat: add --assets options to override assets (#134)
authorsigoden <sigoden@gmail.com>
Mon, 5 Sep 2022 02:30:45 +0000 (10:30 +0800)
committerGitHub <noreply@github.com>
Mon, 5 Sep 2022 02:30:45 +0000 (10:30 +0800)
* feat: add --assets options to override assets

* update readme

README.md
assets/index.html
src/args.rs
src/server.rs
tests/assets.rs
tests/fixtures.rs
tests/utils.rs

index 34773823a9f386453d2737bd3464e73ee200fb21..236e03b25212abb5dd8a87d0ec26e9b7037dc3f4 100644 (file)
--- a/README.md
+++ b/README.md
@@ -43,10 +43,10 @@ Download from [Github Releases](https://github.com/sigoden/dufs/releases), unzip
 Dufs is a distinctive utility file server - https://github.com/sigoden/dufs
 
 USAGE:
-    dufs [OPTIONS] [--] [path]
+    dufs [OPTIONS] [--] [root]
 
 ARGS:
-    <path>    Specific path to serve [default: .]
+    <root>    Specific path to serve [default: .]
 
 OPTIONS:
     -b, --bind <addr>...         Specify bind address
@@ -64,6 +64,7 @@ OPTIONS:
         --render-index           Serve index.html when requesting a directory, returns 404 if not found index.html
         --render-try-index       Serve index.html when requesting a directory, returns directory listing if not found index.html
         --render-spa             Serve SPA(Single Page Application)
+        --assets <path>          Use custom assets to override builtin assets
         --tls-cert <path>        Path to an SSL/TLS certificate to serve with HTTPS
         --tls-key <path>         Path to the SSL/TLS certificate's private key
         --log-format <format>    Customize http log format
@@ -264,6 +265,22 @@ dufs --log-format '$remote_addr $remote_user "$request" $status' -a /@admin:admi
 
 </details>
 
+
+### Customize UI
+
+Dufs allows users to customize the UI with their own assets.
+
+```
+dufs --assets my-assets
+```
+
+You assets folder must contains a entrypoint `index.html`.
+
+`index.html` can use the following planceholder to access internal data.
+
+- `__INDEX_DATA__`: directory listing data
+- `__ASSERTS_PREFIX__`: assets url prefix
+
 ## License
 
 Copyright (c) 2022 dufs-developers.
index 927d7fcc47ca852bed5bd8b2b20f86b747f638da..7a4cbe111d173347c424b2942a52795195b66eda 100644 (file)
@@ -4,7 +4,12 @@
 <head>
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width" />
-  __SLOT__
+  <link rel="icon" type="image/x-icon" href="__ASSERTS_PREFIX__favicon.ico">
+  <link rel="stylesheet" href="__ASSERTS_PREFIX__index.css">
+  <script>
+  DATA = __INDEX_DATA__
+  </script>
+  <script src="__ASSERTS_PREFIX__index.js"></script>
 </head>
 <body>
   <div class="head">
index 2a2ba74e6d267be0eeefb63e068843457e77a9e8..2eb382ce0291c19e6902070f9d6ab83a9c19d568 100644 (file)
@@ -43,7 +43,7 @@ pub fn build_cli() -> Command<'static> {
                 .value_name("port"),
         )
         .arg(
-            Arg::new("path")
+            Arg::new("root")
                 .default_value(".")
                 .allow_invalid_utf8(true)
                 .help("Specific path to serve"),
@@ -126,6 +126,13 @@ pub fn build_cli() -> Command<'static> {
             Arg::new("render-spa")
                 .long("render-spa")
                 .help("Serve SPA(Single Page Application)"),
+        )
+        .arg(
+            Arg::new("assets")
+                .long("assets")
+                .help("Use custom assets to override builtin assets")
+                .allow_invalid_utf8(true)
+                .value_name("path")
         );
 
     #[cfg(feature = "tls")]
@@ -181,6 +188,7 @@ pub struct Args {
     pub render_spa: bool,
     pub render_try_index: bool,
     pub enable_cors: bool,
+    pub assets_path: Option<PathBuf>,
     pub log_http: LogHttp,
     #[cfg(feature = "tls")]
     pub tls: Option<(Vec<Certificate>, PrivateKey)>,
@@ -200,7 +208,7 @@ impl Args {
             .map(|v| v.collect())
             .unwrap_or_else(|| vec!["0.0.0.0", "::"]);
         let addrs: Vec<IpAddr> = Args::parse_addrs(&addrs)?;
-        let path = Args::parse_path(matches.value_of_os("path").unwrap_or_default())?;
+        let path = Args::parse_path(matches.value_of_os("root").unwrap_or_default())?;
         let path_is_file = path.metadata()?.is_file();
         let path_prefix = matches
             .value_of("path-prefix")
@@ -247,6 +255,10 @@ impl Args {
             .value_of("log-format")
             .unwrap_or(DEFAULT_LOG_FORMAT)
             .parse()?;
+        let assets_path = match matches.value_of_os("assets") {
+            Some(v) => Some(Args::parse_assets_path(v)?),
+            None => None,
+        };
 
         Ok(Args {
             addrs,
@@ -268,6 +280,7 @@ impl Args {
             render_spa,
             tls,
             log_http,
+            assets_path,
         })
     }
 
@@ -303,4 +316,12 @@ impl Args {
             })
             .map_err(|err| format!("Failed to access path `{}`: {}", path.display(), err,).into())
     }
+
+    fn parse_assets_path<P: AsRef<Path>>(path: P) -> BoxResult<PathBuf> {
+        let path = Self::parse_path(path)?;
+        if !path.join("index.html").exists() {
+            return Err(format!("Path `{}` doesn't contains index.html", path.display()).into());
+        }
+        Ok(path)
+    }
 }
index 47afc58c577b0c7f777de59c9a674e347dcda212..6fd16acea474c927af5c2d008fefa62f24bba9c5 100644 (file)
@@ -19,6 +19,7 @@ use hyper::header::{
 };
 use hyper::{Body, Method, StatusCode, Uri};
 use serde::Serialize;
+use std::borrow::Cow;
 use std::collections::HashMap;
 use std::fs::Metadata;
 use std::io::SeekFrom;
@@ -46,6 +47,7 @@ const BUF_SIZE: usize = 65536;
 pub struct Server {
     args: Arc<Args>,
     assets_prefix: String,
+    html: Cow<'static, str>,
     single_file_req_paths: Vec<String>,
     running: Arc<AtomicBool>,
 }
@@ -66,11 +68,16 @@ impl Server {
         } else {
             vec![]
         };
+        let html = match args.assets_path.as_ref() {
+            Some(path) => Cow::Owned(std::fs::read_to_string(path.join("index.html")).unwrap()),
+            None => Cow::Borrowed(INDEX_HTML),
+        };
         Self {
             args,
             running,
             single_file_req_paths,
             assets_prefix,
+            html,
         }
     }
 
@@ -118,7 +125,7 @@ impl Server {
         let headers = req.headers();
         let method = req.method().clone();
 
-        if method == Method::GET && self.handle_embed_assets(req_path, &mut res).await? {
+        if method == Method::GET && self.handle_assets(req_path, headers, &mut res).await? {
             return Ok(res);
         }
 
@@ -496,29 +503,40 @@ impl Server {
         Ok(())
     }
 
-    async fn handle_embed_assets(&self, req_path: &str, res: &mut Response) -> BoxResult<bool> {
+    async fn handle_assets(
+        &self,
+        req_path: &str,
+        headers: &HeaderMap<HeaderValue>,
+        res: &mut Response,
+    ) -> BoxResult<bool> {
         if let Some(name) = req_path.strip_prefix(&self.assets_prefix) {
-            match name {
-                "index.js" => {
-                    *res.body_mut() = Body::from(INDEX_JS);
-                    res.headers_mut().insert(
-                        "content-type",
-                        HeaderValue::from_static("application/javascript"),
-                    );
-                }
-                "index.css" => {
-                    *res.body_mut() = Body::from(INDEX_CSS);
-                    res.headers_mut()
-                        .insert("content-type", HeaderValue::from_static("text/css"));
-                }
-                "favicon.ico" => {
-                    *res.body_mut() = Body::from(FAVICON_ICO);
-                    res.headers_mut()
-                        .insert("content-type", HeaderValue::from_static("image/x-icon"));
-                }
-                _ => {
-                    return Ok(false);
+            match self.args.assets_path.as_ref() {
+                Some(assets_path) => {
+                    let path = assets_path.join(name);
+                    self.handle_send_file(&path, headers, false, res).await?;
                 }
+                None => match name {
+                    "index.js" => {
+                        *res.body_mut() = Body::from(INDEX_JS);
+                        res.headers_mut().insert(
+                            "content-type",
+                            HeaderValue::from_static("application/javascript"),
+                        );
+                    }
+                    "index.css" => {
+                        *res.body_mut() = Body::from(INDEX_CSS);
+                        res.headers_mut()
+                            .insert("content-type", HeaderValue::from_static("text/css"));
+                    }
+                    "favicon.ico" => {
+                        *res.body_mut() = Body::from(FAVICON_ICO);
+                        res.headers_mut()
+                            .insert("content-type", HeaderValue::from_static("image/x-icon"));
+                    }
+                    _ => {
+                        status_not_found(res);
+                    }
+                },
             }
             res.headers_mut().insert(
                 "cache-control",
@@ -802,23 +820,10 @@ impl Server {
             dir_exists: exist,
         };
         let data = serde_json::to_string(&data).unwrap();
-        let asset_js = format!("{}index.js", self.assets_prefix);
-        let asset_css = format!("{}index.css", self.assets_prefix);
-        let asset_ico = format!("{}favicon.ico", self.assets_prefix);
-        let output = INDEX_HTML.replace(
-            "__SLOT__",
-            &format!(
-                r#"
-<link rel="icon" type="image/x-icon" href="{}">
-<link rel="stylesheet" href="{}">
-<script>
-DATA = {}
-</script>
-<script src="{}"></script>
-"#,
-                asset_ico, asset_css, data, asset_js
-            ),
-        );
+        let output = self
+            .html
+            .replace("__ASSERTS_PREFIX__", &self.assets_prefix)
+            .replace("__INDEX_DATA__", &data);
         res.headers_mut()
             .typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
         res.headers_mut()
index ecc566262bc2307da5434d10c6a8ae4a70ea5583..aa55f211bc6b89f86884a7e816b96aa48e67e74f 100644 (file)
@@ -1,8 +1,11 @@
 mod fixtures;
 mod utils;
 
-use fixtures::{server, Error, TestServer};
+use assert_cmd::prelude::*;
+use assert_fs::fixture::TempDir;
+use fixtures::{port, server, tmpdir, wait_for_port, Error, TestServer, DIR_ASSETS};
 use rstest::rstest;
+use std::process::{Command, Stdio};
 
 #[rstest]
 fn assets(server: TestServer) -> Result<(), Error> {
@@ -91,3 +94,29 @@ fn asset_js_with_prefix(
     );
     Ok(())
 }
+
+#[rstest]
+fn assets_override(tmpdir: TempDir, port: u16) -> Result<(), Error> {
+    let mut child = Command::cargo_bin("dufs")?
+        .arg(tmpdir.path())
+        .arg("-p")
+        .arg(port.to_string())
+        .arg("--assets")
+        .arg(tmpdir.join(DIR_ASSETS))
+        .stdout(Stdio::piped())
+        .spawn()?;
+
+    wait_for_port(port);
+
+    let url = format!("http://localhost:{}", port);
+    let resp = reqwest::blocking::get(&url)?;
+    assert!(resp.text()?.starts_with(&format!(
+        "/__dufs_v{}_index.js;DATA",
+        env!("CARGO_PKG_VERSION")
+    )));
+    let resp = reqwest::blocking::get(&url)?;
+    assert_resp_paths!(resp);
+
+    child.kill()?;
+    Ok(())
+}
index 669a1ae6924a31b1bc57584a80026363e9f40948..c20c3388b5d6ed7326bd3069370dd108a01e09c1 100644 (file)
@@ -27,9 +27,13 @@ pub static DIR_NO_INDEX: &str = "dir-no-index/";
 #[allow(dead_code)]
 pub static DIR_GIT: &str = ".git/";
 
+/// Directory names for testings assets override
+#[allow(dead_code)]
+pub static DIR_ASSETS: &str = "dir-assets/";
+
 /// Directory names for testing purpose
 #[allow(dead_code)]
-pub static DIRECTORIES: &[&str] = &["dira/", "dirb/", "dirc/", DIR_NO_INDEX, DIR_GIT];
+pub static DIRECTORIES: &[&str] = &["dira/", "dirb/", "dirc/", DIR_NO_INDEX, DIR_GIT, DIR_ASSETS];
 
 /// Test fixture which creates a temporary directory with a few files and directories inside.
 /// The directories also contain files.
@@ -44,14 +48,21 @@ pub fn tmpdir() -> TempDir {
             .expect("Couldn't write to file");
     }
     for directory in DIRECTORIES {
-        for file in FILES {
-            if *directory == DIR_NO_INDEX && *file == "index.html" {
-                continue;
-            }
+        if *directory == DIR_ASSETS {
             tmpdir
-                .child(format!("{}{}", directory, file))
-                .write_str(&format!("This is {}{}", directory, file))
+                .child(format!("{}{}", directory, "index.html"))
+                .write_str("__ASSERTS_PREFIX__index.js;DATA = __INDEX_DATA__")
                 .expect("Couldn't write to file");
+        } else {
+            for file in FILES {
+                if *directory == DIR_NO_INDEX && *file == "index.html" {
+                    continue;
+                }
+                tmpdir
+                    .child(format!("{}{}", directory, file))
+                    .write_str(&format!("This is {}{}", directory, file))
+                    .expect("Couldn't write to file");
+            }
         }
     }
 
@@ -93,34 +104,6 @@ where
     TestServer::new(port, tmpdir, child, is_tls)
 }
 
-/// Same as `server()` but ignore stderr
-#[fixture]
-#[allow(dead_code)]
-pub fn server_no_stderr<I>(#[default(&[] as &[&str])] args: I) -> TestServer
-where
-    I: IntoIterator + Clone,
-    I::Item: AsRef<std::ffi::OsStr>,
-{
-    let port = port();
-    let tmpdir = tmpdir();
-    let child = Command::cargo_bin("dufs")
-        .expect("Couldn't find test binary")
-        .arg(tmpdir.path())
-        .arg("-p")
-        .arg(port.to_string())
-        .args(args.clone())
-        .stdout(Stdio::null())
-        .stderr(Stdio::null())
-        .spawn()
-        .expect("Couldn't run test binary");
-    let is_tls = args
-        .into_iter()
-        .any(|x| x.as_ref().to_str().unwrap().contains("tls"));
-
-    wait_for_port(port);
-    TestServer::new(port, tmpdir, child, is_tls)
-}
-
 /// Wait a max of 1s for the port to become available.
 pub fn wait_for_port(port: u16) {
     let start_wait = Instant::now();
index 449fbbfb02c7bfb914e72ec7e514ce3dabe5d956..0789d334cfd41cdc12f511e000dc298efee2239c 100644 (file)
@@ -38,7 +38,8 @@ pub fn encode_uri(v: &str) -> String {
 fn retrieve_index_paths_impl(index: &str) -> Option<IndexSet<String>> {
     let lines: Vec<&str> = index.lines().collect();
     let line = lines.iter().find(|v| v.contains("DATA ="))?;
-    let value: Value = line[7..].parse().ok()?;
+    let line_col = line.find("DATA =").unwrap() + 6;
+    let value: Value = line[line_col..].parse().ok()?;
     let paths = value
         .get("paths")?
         .as_array()?