diff --git a/Cargo.lock b/Cargo.lock index 8d7453c9..b3c4baa1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -379,6 +379,7 @@ dependencies = [ "futures", "indexmap 2.0.0", "libc", + "mime_guess", "natord", "notify", "parking_lot", @@ -1051,6 +1052,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2004,6 +2021,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/core/Cargo.toml b/core/Cargo.toml index a29d996a..c5652535 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -29,6 +29,7 @@ tracing = "^0" trash = "^3" unicode-width = "^0" yazi-prebuild = "^0" +mime_guess = "2.0.4" [target.'cfg(target_os = "windows")'.dependencies] clipboard-win = "^4" diff --git a/core/src/external/file.rs b/core/src/external/file.rs index c48891f5..13c3bfd3 100644 --- a/core/src/external/file.rs +++ b/core/src/external/file.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use anyhow::{bail, Result}; use futures::TryFutureExt; use shared::{MimeKind, Url}; -use tokio::process::Command; +use tokio::{fs, io::AsyncReadExt, process::Command}; use tracing::error; async fn _file(files: &[&Url]) -> Result> { @@ -36,5 +36,41 @@ async fn _file(files: &[&Url]) -> Result> { } pub async fn file(files: &[impl AsRef]) -> Result> { - _file(&files.iter().map(AsRef::as_ref).collect::>()).await + let _files = &files.iter().map(AsRef::as_ref).collect::>(); + _file(_files).or_else(move |_| fallback(_files)).await +} + +async fn fallback(files: &[&Url]) -> Result> { + let mut mimes = BTreeMap::new(); + for &file in files { + if let Ok(mime) = guess_mime(file).await { + mimes.insert(file.clone(), mime); + } + } + + if mimes.is_empty() { + error!("failed to get mime types: {:?}", files); + bail!("failed to get mime types"); + } + + Ok(mimes) +} + +async fn guess_mime(file: impl AsRef) -> Result { + // First, try to guess mime type from file extenstion. + // If fail, either return "text/plain" or "application/octet-stream" by + // trying to convert 1kb of data to utf8 string + let file = file.as_ref(); + match mime_guess::from_path(file).first() { + Some(mime) => Ok(mime.to_string()), + None => { + let mut buf = [0; 1024]; + let num_bytes = fs::File::open(file).await?.read(&mut buf).await?; + if String::from_utf8(buf[..num_bytes].to_vec()).is_ok() { + Ok(String::from("text/plain")) + } else { + Ok(String::from("application/octet-stream")) + } + } + } } diff --git a/shared/src/mime.rs b/shared/src/mime.rs index a3e5d184..16ee9fbf 100644 --- a/shared/src/mime.rs +++ b/shared/src/mime.rs @@ -18,7 +18,7 @@ pub enum MimeKind { impl MimeKind { pub fn new(s: &str) -> Self { - if s.starts_with("text/") || s.ends_with("/xml") || s.ends_with("/javascript") { + if s.starts_with("text/") || s.ends_with("/xml") || s.ends_with("/javascript") || s.ends_with("/x-sh") { Self::Text } else if s.starts_with("image/") { Self::Image