diff --git a/data/style.scss b/data/style.scss index b901ef6da..0b87265d5 100644 --- a/data/style.scss +++ b/data/style.scss @@ -82,11 +82,6 @@ a.file:visited, color: var(--file_link_color); } -a.symlink, -a.symlink:visited { - color: var(--symlink_link_color); -} - a.directory:hover { color: var(--directory_link_color); } @@ -95,13 +90,10 @@ a.file:hover { color: var(--file_link_color); } -a.symlink:hover { - color: var(--symlink_link_color); -} - -.symlink-symbol { +.symlink-symbol::after { + content: "⇢"; display: inline-block; - border: 1px solid var(--symlink_link_color); + border: 1px solid; margin-left: 0.5rem; border-radius: 0.2rem; padding: 0 0.1rem; @@ -488,17 +480,12 @@ th span.active span { } a.root, - a.file, - a.symlink { + a.file { display: inline-block; flex: 1; padding: 0.5625rem 0; } - a.symlink { - width: 100%; - } - .back { display: flex; } @@ -539,7 +526,6 @@ th span.active span { --text_color: #323232; --directory_link_color: #d02474; --file_link_color: #0086b3; - --symlink_link_color: #ed6a43; --table_background: #ffffff; --table_text_color: #323232; --table_header_background: #323232; @@ -584,7 +570,6 @@ th span.active span { --text_color: #fefefe; --directory_link_color: #03a9f4; --file_link_color: #ea95ff; - --symlink_link_color: #ff9800; --table_background: #353946; --table_text_color: #eeeeee; --table_header_background: #5294e2; @@ -629,7 +614,6 @@ th span.active span { --text_color: #efefef; --directory_link_color: #f0dfaf; --file_link_color: #87d6d5; - --symlink_link_color: #ffccee; --table_background: #4a4949; --table_text_color: #efefef; --table_header_background: #7f9f7f; @@ -674,7 +658,6 @@ th span.active span { --text_color: #f8f8f2; --directory_link_color: #f92672; --file_link_color: #a6e22e; - --symlink_link_color: #fd971f; --table_background: #3b3a32; --table_text_color: #f8f8f0; --table_header_background: #75715e; diff --git a/src/listing.rs b/src/listing.rs index 013265c88..43cfb0ea7 100644 --- a/src/listing.rs +++ b/src/listing.rs @@ -74,9 +74,6 @@ pub enum EntryType { /// Entry is a file File, - - /// Entry is a symlink - Symlink, } /// Entry @@ -87,6 +84,9 @@ pub struct Entry { /// Type of the entry pub entry_type: EntryType, + /// Entry is symlink. Not mutually exclusive with entry_type + pub is_symlink: bool, + /// URL of the entry pub link: String, @@ -101,6 +101,7 @@ impl Entry { fn new( name: String, entry_type: EntryType, + is_symlink: bool, link: String, size: Option, last_modification_date: Option, @@ -108,6 +109,7 @@ impl Entry { Entry { name, entry_type, + is_symlink, link, size, last_modification_date, @@ -123,11 +125,6 @@ impl Entry { pub fn is_file(&self) -> bool { self.entry_type == EntryType::File } - - /// Returns wether the entry is a symlink - pub fn is_symlink(&self) -> bool { - self.entry_type == EntryType::Symlink - } } /// One entry in the path to the listed directory @@ -263,14 +260,21 @@ pub fn directory_listing( let entry = entry?; // show file url as relative to static path let file_name = entry.file_name().to_string_lossy().to_string(); + let (is_symlink, metadata) = match entry.metadata() { + Ok(metadata) if metadata.file_type().is_symlink() => { + // for symlinks, get the metadata of the original file + (true, std::fs::metadata(entry.path())) + } + res => (false, res), + }; let file_url = base .join(&utf8_percent_encode(&file_name, PATH_SEGMENT).to_string()) .to_string_lossy() .to_string(); // if file is a directory, add '/' to the end of the name - if let Ok(metadata) = entry.metadata() { - if skip_symlinks && metadata.file_type().is_symlink() { + if let Ok(metadata) = metadata { + if skip_symlinks && is_symlink { continue; } let last_modification_date = match metadata.modified() { @@ -278,26 +282,20 @@ pub fn directory_listing( Err(_) => None, }; - if metadata.file_type().is_symlink() { - entries.push(Entry::new( - file_name, - EntryType::Symlink, - file_url, - None, - last_modification_date, - )); - } else if metadata.is_dir() { + if metadata.is_dir() { entries.push(Entry::new( file_name, EntryType::Directory, + is_symlink, file_url, None, last_modification_date, )); - } else { + } else if metadata.is_file() { entries.push(Entry::new( file_name, EntryType::File, + is_symlink, file_url, Some(ByteSize::b(metadata.len())), last_modification_date, diff --git a/src/renderer.rs b/src/renderer.rs index fc6897ca1..d2beda30d 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -330,11 +330,17 @@ fn entry_row( @if entry.is_dir() { a.directory href=(parametrized_link(&entry.link, sort_method, sort_order)) { (entry.name) "/" + @if entry.is_symlink { + span.symlink-symbol { } + } } } @else if entry.is_file() { div.file-entry { a.file href=(&entry.link) { (entry.name) + @if entry.is_symlink { + span.symlink-symbol { } + } } @if let Some(size) = entry.size { span.mobile-info.size { @@ -342,10 +348,6 @@ fn entry_row( } } } - } @else if entry.is_symlink() { - a.symlink href=(parametrized_link(&entry.link, sort_method, sort_order)) { - (entry.name) span.symlink-symbol { "⇢" } - } } } } diff --git a/tests/serve_request.rs b/tests/serve_request.rs index 25c5574de..e259b9e31 100644 --- a/tests/serve_request.rs +++ b/tests/serve_request.rs @@ -8,10 +8,16 @@ use regex::Regex; use rstest::rstest; use select::document::Document; use select::node::Node; +use std::path::Path; use std::process::{Command, Stdio}; use std::thread::sleep; use std::time::Duration; +#[cfg(unix)] +use std::os::unix::fs::{symlink as symlink_dir, symlink as symlink_file}; +#[cfg(windows)] +use std::os::windows::fs::{symlink_dir, symlink_file}; + #[rstest] fn serves_requests_with_no_options(tmpdir: TempDir) -> Result<(), Error> { let mut child = Command::cargo_bin("miniserve")? @@ -159,6 +165,74 @@ fn serves_requests_no_hidden_files_without_flag(tmpdir: TempDir, port: u16) -> R Ok(()) } +#[rstest(no_symlinks, case(true), case(false))] +fn serves_requests_symlinks(tmpdir: TempDir, port: u16, no_symlinks: bool) -> Result<(), Error> { + let mut comm = Command::cargo_bin("miniserve")?; + comm.arg(tmpdir.path()) + .arg("-p") + .arg(port.to_string()) + .stdout(Stdio::null()); + if no_symlinks { + comm.arg("--no-symlinks"); + } + + let mut child = comm.spawn()?; + sleep(Duration::from_secs(1)); + + let files = &["symlink-file.html"]; + let dirs = &["symlink-dir/"]; + let broken = &["symlink broken"]; + + for &directory in dirs { + let orig = Path::new(DIRECTORIES[0].strip_suffix("/").unwrap()); + let link = tmpdir + .path() + .join(Path::new(directory.strip_suffix("/").unwrap())); + symlink_dir(orig, link).expect("Couldn't create symlink"); + } + for &file in files { + let orig = Path::new(FILES[0]); + let link = tmpdir.path().join(Path::new(file)); + symlink_file(orig, link).expect("Couldn't create symlink"); + } + for &file in broken { + let orig = Path::new("should-not-exist.xxx"); + let link = tmpdir.path().join(Path::new(file)); + symlink_file(orig, link).expect("Couldn't create symlink"); + } + + let body = reqwest::blocking::get(format!("http://localhost:{}", port).as_str())? + .error_for_status()?; + let parsed = Document::from_read(body)?; + + for &entry in files.into_iter().chain(dirs) { + let node = parsed + .find(|x: &Node| x.name().unwrap_or_default() == "a" && x.text() == entry) + .next(); + assert_eq!(node.is_none(), no_symlinks); + if no_symlinks { + continue; + } + + let node = node.unwrap(); + assert_eq!(node.attr("href").unwrap().strip_prefix("/").unwrap(), entry); + reqwest::blocking::get(format!("http://localhost:{}/{}", port, entry))? + .error_for_status()?; + if entry.ends_with("/") { + assert_eq!(node.attr("class").unwrap(), "directory"); + } else { + assert_eq!(node.attr("class").unwrap(), "file"); + } + } + for &entry in broken { + assert!(parsed.find(|x: &Node| x.text() == entry).next().is_none()); + } + + child.kill()?; + + Ok(()) +} + #[rstest] fn serves_requests_with_randomly_assigned_port(tmpdir: TempDir) -> Result<(), Error> { let mut child = Command::cargo_bin("miniserve")?