123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- use std::{
- fs::{create_dir_all, File},
- io::{ErrorKind, Read, Write},
- path::{Path, PathBuf},
- process::Command,
- };
- use anyhow::Context;
- use flate2::read::GzDecoder;
- use futures_util::StreamExt;
- use tar::Archive;
- use tokio::io::AsyncWriteExt;
- #[derive(Debug, PartialEq, Eq)]
- pub enum Tool {
- Sass,
- Tailwind,
- }
- pub fn app_path() -> PathBuf {
- let data_local = dirs::data_local_dir().unwrap();
- let dioxus_dir = data_local.join("dioxus");
- if !dioxus_dir.is_dir() {
- create_dir_all(&dioxus_dir).unwrap();
- }
- dioxus_dir
- }
- pub fn temp_path() -> PathBuf {
- let app_path = app_path();
- let temp_path = app_path.join("temp");
- if !temp_path.is_dir() {
- create_dir_all(&temp_path).unwrap();
- }
- temp_path
- }
- pub fn clone_repo(dir: &Path, url: &str) -> anyhow::Result<()> {
- let target_dir = dir.parent().unwrap();
- let dir_name = dir.file_name().unwrap();
- let mut cmd = Command::new("git");
- let cmd = cmd.current_dir(target_dir);
- let res = cmd.arg("clone").arg(url).arg(dir_name).output();
- if let Err(err) = res {
- if ErrorKind::NotFound == err.kind() {
- tracing::warn!("Git program not found. Hint: Install git or check $PATH.");
- return Err(err.into());
- }
- }
- Ok(())
- }
- pub fn tools_path() -> PathBuf {
- let app_path = app_path();
- let temp_path = app_path.join("tools");
- if !temp_path.is_dir() {
- create_dir_all(&temp_path).unwrap();
- }
- temp_path
- }
- #[allow(clippy::should_implement_trait)]
- impl Tool {
- /// from str to tool enum
- pub fn from_str(name: &str) -> Option<Self> {
- match name {
- "sass" => Some(Self::Sass),
- "tailwindcss" => Some(Self::Tailwind),
- _ => None,
- }
- }
- /// get current tool name str
- pub fn name(&self) -> &str {
- match self {
- Self::Sass => "sass",
- Self::Tailwind => "tailwindcss",
- }
- }
- /// get tool bin dir path
- pub fn bin_path(&self) -> &str {
- match self {
- Self::Sass => ".",
- Self::Tailwind => ".",
- }
- }
- /// get target platform
- pub fn target_platform(&self) -> &str {
- match self {
- Self::Sass => {
- if cfg!(target_os = "windows") {
- "windows"
- } else if cfg!(target_os = "macos") {
- "macos"
- } else if cfg!(target_os = "linux") {
- "linux"
- } else {
- panic!("unsupported platformm");
- }
- }
- Self::Tailwind => {
- if cfg!(target_os = "windows") {
- "windows"
- } else if cfg!(target_os = "macos") {
- "macos"
- } else if cfg!(target_os = "linux") {
- "linux"
- } else {
- panic!("unsupported platformm");
- }
- }
- }
- }
- /// get tool version
- pub fn tool_version(&self) -> &str {
- match self {
- Self::Sass => "1.51.0",
- Self::Tailwind => "v3.1.6",
- }
- }
- /// get tool package download url
- pub fn download_url(&self) -> String {
- match self {
- Self::Sass => {
- format!(
- "https://github.com/sass/dart-sass/releases/download/{version}/dart-sass-{version}-{target}-x64.{extension}",
- version = self.tool_version(),
- target = self.target_platform(),
- extension = self.extension()
- )
- }
- Self::Tailwind => {
- let windows_extension = match self.target_platform() {
- "windows" => ".exe",
- _ => "",
- };
- format!(
- "https://github.com/tailwindlabs/tailwindcss/releases/download/{version}/tailwindcss-{target}-x64{optional_ext}",
- version = self.tool_version(),
- target = self.target_platform(),
- optional_ext = windows_extension
- )
- }
- }
- }
- /// get package extension name
- pub fn extension(&self) -> &str {
- match self {
- Self::Sass => {
- if cfg!(target_os = "windows") {
- "zip"
- } else {
- "tar.gz"
- }
- }
- Self::Tailwind => "bin",
- }
- }
- /// check tool state
- pub fn is_installed(&self) -> bool {
- tools_path().join(self.name()).is_dir()
- }
- /// get download temp path
- pub fn temp_out_path(&self) -> PathBuf {
- temp_path().join(format!("{}-tool.tmp", self.name()))
- }
- /// start to download package
- pub async fn download_package(&self) -> anyhow::Result<PathBuf> {
- let download_url = self.download_url();
- let temp_out = self.temp_out_path();
- let mut file = tokio::fs::File::create(&temp_out)
- .await
- .context("failed creating temporary output file")?;
- let resp = reqwest::get(download_url).await.unwrap();
- let mut res_bytes = resp.bytes_stream();
- while let Some(chunk_res) = res_bytes.next().await {
- let chunk = chunk_res.context("error reading chunk from download")?;
- let _ = file.write(chunk.as_ref()).await;
- }
- // tracing::info!("temp file path: {:?}", temp_out);
- Ok(temp_out)
- }
- /// start to install package
- pub async fn install_package(&self) -> anyhow::Result<()> {
- let temp_path = self.temp_out_path();
- let tool_path = tools_path();
- let dir_name = match self {
- Self::Sass => "dart-sass".to_string(),
- Self::Tailwind => self.name().to_string(),
- };
- if self.extension() == "tar.gz" {
- let tar_gz = File::open(temp_path)?;
- let tar = GzDecoder::new(tar_gz);
- let mut archive = Archive::new(tar);
- archive.unpack(&tool_path)?;
- std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?;
- } else if self.extension() == "zip" {
- // decompress the `zip` file
- extract_zip(&temp_path, &tool_path)?;
- std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?;
- } else if self.extension() == "bin" {
- let bin_path = match self.target_platform() {
- "windows" => tool_path.join(&dir_name).join(self.name()).join(".exe"),
- _ => tool_path.join(&dir_name).join(self.name()),
- };
- // Manualy creating tool directory because we directly download the binary via Github
- std::fs::create_dir(tool_path.join(dir_name))?;
- let mut final_file = std::fs::File::create(&bin_path)?;
- let mut temp_file = File::open(&temp_path)?;
- let mut content = Vec::new();
- temp_file.read_to_end(&mut content)?;
- final_file.write_all(&content)?;
- if self.target_platform() == "linux" {
- // This code does not update permissions idk why
- // let mut perms = final_file.metadata()?.permissions();
- // perms.set_mode(0o744);
- // Adding to the binary execution rights with "chmod"
- let mut command = Command::new("chmod");
- let _ = command
- .args(vec!["+x", bin_path.to_str().unwrap()])
- .stdout(std::process::Stdio::inherit())
- .stderr(std::process::Stdio::inherit())
- .output()?;
- }
- std::fs::remove_file(&temp_path)?;
- }
- Ok(())
- }
- pub fn call(&self, command: &str, args: Vec<&str>) -> anyhow::Result<Vec<u8>> {
- let bin_path = tools_path().join(self.name()).join(self.bin_path());
- let command_file = match self {
- Tool::Sass => {
- if cfg!(target_os = "windows") {
- format!("{}.bat", command)
- } else {
- command.to_string()
- }
- }
- Tool::Tailwind => {
- if cfg!(target_os = "windows") {
- format!("{}.exe", command)
- } else {
- command.to_string()
- }
- }
- };
- if !bin_path.join(&command_file).is_file() {
- return Err(anyhow::anyhow!("Command file not found."));
- }
- let mut command = Command::new(bin_path.join(&command_file).to_str().unwrap());
- let output = command
- .args(&args[..])
- .stdout(std::process::Stdio::inherit())
- .stderr(std::process::Stdio::inherit())
- .output()?;
- Ok(output.stdout)
- }
- }
- pub fn extract_zip(file: &Path, target: &Path) -> anyhow::Result<()> {
- let zip_file = std::fs::File::open(file)?;
- let mut zip = zip::ZipArchive::new(zip_file)?;
- if !target.exists() {
- std::fs::create_dir_all(target)?;
- }
- for i in 0..zip.len() {
- let mut zip_entry = zip.by_index(i)?;
- // check for dangerous paths
- // see https://docs.rs/zip/latest/zip/read/struct.ZipFile.html#warnings
- let Some(enclosed_name) = zip_entry.enclosed_name() else {
- return Err(anyhow::anyhow!(
- "Refusing to unpack zip entry with potentially dangerous path: zip={} entry={:?}",
- file.display(),
- zip_entry.name()
- ));
- };
- let output_path = target.join(enclosed_name);
- if zip_entry.is_dir() {
- std::fs::create_dir_all(output_path)?;
- } else {
- // create parent dirs if needed
- if let Some(parent) = output_path.parent() {
- std::fs::create_dir_all(parent)?;
- }
- // extract file
- let mut target_file = if !output_path.exists() {
- std::fs::File::create(output_path)?
- } else {
- std::fs::File::open(output_path)?
- };
- let _num = std::io::copy(&mut zip_entry, &mut target_file)?;
- }
- }
- Ok(())
- }
- #[cfg(test)]
- mod test {
- use super::*;
- use tempfile::tempdir;
- #[test]
- fn test_extract_zip() -> anyhow::Result<()> {
- let path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
- .join("tests/fixtures/test.zip");
- let temp_dir = tempdir()?;
- let temp_path = temp_dir.path();
- extract_zip(path.as_path(), temp_path)?;
- let expected_files = vec!["file1.txt", "file2.txt", "dir/file3.txt"];
- for file in expected_files {
- let path = temp_path.join(file);
- assert!(path.exists(), "File not found: {:?}", path);
- }
- Ok(())
- }
- #[test]
- fn test_extract_zip_dangerous_path() -> anyhow::Result<()> {
- let path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
- .join("tests/fixtures/dangerous.zip");
- let temp_dir = tempdir()?;
- let temp_path = temp_dir.path();
- let result = extract_zip(path.as_path(), temp_path);
- let err = result.unwrap_err();
- assert!(err
- .to_string()
- .contains("Refusing to unpack zip entry with potentially dangerous path: zip="));
- assert!(err.to_string().contains("entry=\"/etc/passwd\""));
- Ok(())
- }
- }
|