common.rs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. // Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
  2. // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
  3. // SPDX-License-Identifier: Apache-2.0
  4. // SPDX-License-Identifier: MIT
  5. use log::debug;
  6. use std::{
  7. ffi::OsStr,
  8. fs::{self, File},
  9. io::{self, BufReader, BufWriter},
  10. path::Path,
  11. process::{Command, ExitStatus, Output, Stdio},
  12. sync::{Arc, Mutex},
  13. };
  14. /// Returns true if the path has a filename indicating that it is a high-density
  15. /// "retina" icon. Specifically, returns true the file stem ends with
  16. /// "@2x" (a convention specified by the [Apple developer docs](
  17. /// https://developer.apple.com/library/mac/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Optimizing/Optimizing.html)).
  18. #[allow(dead_code)]
  19. pub fn is_retina<P: AsRef<Path>>(path: P) -> bool {
  20. path
  21. .as_ref()
  22. .file_stem()
  23. .and_then(OsStr::to_str)
  24. .map(|stem| stem.ends_with("@2x"))
  25. .unwrap_or(false)
  26. }
  27. /// Creates a new file at the given path, creating any parent directories as
  28. /// needed.
  29. pub fn create_file(path: &Path) -> crate::Result<BufWriter<File>> {
  30. if let Some(parent) = path.parent() {
  31. fs::create_dir_all(parent)?;
  32. }
  33. let file = File::create(path)?;
  34. Ok(BufWriter::new(file))
  35. }
  36. /// Makes a symbolic link to a directory.
  37. #[cfg(unix)]
  38. #[allow(dead_code)]
  39. fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
  40. std::os::unix::fs::symlink(src, dst)
  41. }
  42. /// Makes a symbolic link to a directory.
  43. #[cfg(windows)]
  44. fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
  45. std::os::windows::fs::symlink_dir(src, dst)
  46. }
  47. /// Makes a symbolic link to a file.
  48. #[cfg(unix)]
  49. #[allow(dead_code)]
  50. fn symlink_file(src: &Path, dst: &Path) -> io::Result<()> {
  51. std::os::unix::fs::symlink(src, dst)
  52. }
  53. /// Makes a symbolic link to a file.
  54. #[cfg(windows)]
  55. fn symlink_file(src: &Path, dst: &Path) -> io::Result<()> {
  56. std::os::windows::fs::symlink_file(src, dst)
  57. }
  58. /// Copies a regular file from one path to another, creating any parent
  59. /// directories of the destination path as necessary. Fails if the source path
  60. /// is a directory or doesn't exist.
  61. pub fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> crate::Result<()> {
  62. let from = from.as_ref();
  63. let to = to.as_ref();
  64. if !from.exists() {
  65. return Err(crate::Error::GenericError(format!(
  66. "{:?} does not exist",
  67. from
  68. )));
  69. }
  70. if !from.is_file() {
  71. return Err(crate::Error::GenericError(format!(
  72. "{:?} is not a file",
  73. from
  74. )));
  75. }
  76. let dest_dir = to.parent().expect("No data in parent");
  77. fs::create_dir_all(dest_dir)?;
  78. fs::copy(from, to)?;
  79. Ok(())
  80. }
  81. /// Recursively copies a directory file from one path to another, creating any
  82. /// parent directories of the destination path as necessary. Fails if the
  83. /// source path is not a directory or doesn't exist, or if the destination path
  84. /// already exists.
  85. #[allow(dead_code)]
  86. pub fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> {
  87. if !from.exists() {
  88. return Err(crate::Error::GenericError(format!(
  89. "{:?} does not exist",
  90. from
  91. )));
  92. }
  93. if !from.is_dir() {
  94. return Err(crate::Error::GenericError(format!(
  95. "{:?} is not a Directory",
  96. from
  97. )));
  98. }
  99. if to.exists() {
  100. return Err(crate::Error::GenericError(format!(
  101. "{:?} already exists",
  102. from
  103. )));
  104. }
  105. let parent = to.parent().expect("No data in parent");
  106. fs::create_dir_all(parent)?;
  107. for entry in walkdir::WalkDir::new(from) {
  108. let entry = entry?;
  109. debug_assert!(entry.path().starts_with(from));
  110. let rel_path = entry.path().strip_prefix(from)?;
  111. let dest_path = to.join(rel_path);
  112. if entry.file_type().is_symlink() {
  113. let target = fs::read_link(entry.path())?;
  114. if entry.path().is_dir() {
  115. symlink_dir(&target, &dest_path)?;
  116. } else {
  117. symlink_file(&target, &dest_path)?;
  118. }
  119. } else if entry.file_type().is_dir() {
  120. fs::create_dir(dest_path)?;
  121. } else {
  122. fs::copy(entry.path(), dest_path)?;
  123. }
  124. }
  125. Ok(())
  126. }
  127. pub trait CommandExt {
  128. // The `pipe` function sets the stdout and stderr to properly
  129. // show the command output in the Node.js wrapper.
  130. fn piped(&mut self) -> std::io::Result<ExitStatus>;
  131. fn output_ok(&mut self) -> crate::Result<Output>;
  132. }
  133. impl CommandExt for Command {
  134. fn piped(&mut self) -> std::io::Result<ExitStatus> {
  135. self.stdout(os_pipe::dup_stdout()?);
  136. self.stderr(os_pipe::dup_stderr()?);
  137. let program = self.get_program().to_string_lossy().into_owned();
  138. debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
  139. self.status().map_err(Into::into)
  140. }
  141. fn output_ok(&mut self) -> crate::Result<Output> {
  142. let program = self.get_program().to_string_lossy().into_owned();
  143. debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{} {}", acc, arg)));
  144. self.stdout(Stdio::piped());
  145. self.stderr(Stdio::piped());
  146. let mut child = self.spawn()?;
  147. let mut stdout = child.stdout.take().map(BufReader::new).unwrap();
  148. let stdout_lines = Arc::new(Mutex::new(Vec::new()));
  149. let stdout_lines_ = stdout_lines.clone();
  150. std::thread::spawn(move || {
  151. let mut buf = Vec::new();
  152. let mut lines = stdout_lines_.lock().unwrap();
  153. loop {
  154. buf.clear();
  155. match tauri_utils::io::read_line(&mut stdout, &mut buf) {
  156. Ok(s) if s == 0 => break,
  157. _ => (),
  158. }
  159. debug!(action = "stdout"; "{}", String::from_utf8_lossy(&buf));
  160. lines.extend(buf.clone());
  161. lines.push(b'\n');
  162. }
  163. });
  164. let mut stderr = child.stderr.take().map(BufReader::new).unwrap();
  165. let stderr_lines = Arc::new(Mutex::new(Vec::new()));
  166. let stderr_lines_ = stderr_lines.clone();
  167. std::thread::spawn(move || {
  168. let mut buf = Vec::new();
  169. let mut lines = stderr_lines_.lock().unwrap();
  170. loop {
  171. buf.clear();
  172. match tauri_utils::io::read_line(&mut stderr, &mut buf) {
  173. Ok(s) if s == 0 => break,
  174. _ => (),
  175. }
  176. debug!(action = "stderr"; "{}", String::from_utf8_lossy(&buf));
  177. lines.extend(buf.clone());
  178. lines.push(b'\n');
  179. }
  180. });
  181. let status = child.wait()?;
  182. let output = Output {
  183. status,
  184. stdout: std::mem::take(&mut *stdout_lines.lock().unwrap()),
  185. stderr: std::mem::take(&mut *stderr_lines.lock().unwrap()),
  186. };
  187. if output.status.success() {
  188. Ok(output)
  189. } else {
  190. Err(crate::Error::GenericError(format!(
  191. "failed to run {}",
  192. program
  193. )))
  194. }
  195. }
  196. }
  197. #[cfg(test)]
  198. mod tests {
  199. use super::{create_file, is_retina};
  200. use std::{io::Write, path::PathBuf};
  201. use tauri_utils::resources::resource_relpath;
  202. #[test]
  203. fn create_file_with_parent_dirs() {
  204. let tmp = tempfile::tempdir().expect("Unable to create temp dir");
  205. assert!(!tmp.path().join("parent").exists());
  206. {
  207. let mut file =
  208. create_file(&tmp.path().join("parent/file.txt")).expect("Failed to create file");
  209. writeln!(file, "Hello, world!").expect("unable to write file");
  210. }
  211. assert!(tmp.path().join("parent").is_dir());
  212. assert!(tmp.path().join("parent/file.txt").is_file());
  213. }
  214. #[cfg(not(windows))]
  215. #[test]
  216. fn copy_dir_with_symlinks() {
  217. // Create a directory structure that looks like this:
  218. // ${TMP}/orig/
  219. // sub/
  220. // file.txt
  221. // link -> sub/file.txt
  222. let tmp = tempfile::tempdir().expect("unable to create tempdir");
  223. {
  224. let mut file =
  225. create_file(&tmp.path().join("orig/sub/file.txt")).expect("Unable to create file");
  226. writeln!(file, "Hello, world!").expect("Unable to write to file");
  227. }
  228. super::symlink_file(
  229. &PathBuf::from("sub/file.txt"),
  230. &tmp.path().join("orig/link"),
  231. )
  232. .expect("Failed to create symlink");
  233. assert_eq!(
  234. std::fs::read(tmp.path().join("orig/link"))
  235. .expect("Failed to read file")
  236. .as_slice(),
  237. b"Hello, world!\n"
  238. );
  239. // Copy ${TMP}/orig to ${TMP}/parent/copy, and make sure that the
  240. // directory structure, file, and symlink got copied correctly.
  241. super::copy_dir(&tmp.path().join("orig"), &tmp.path().join("parent/copy"))
  242. .expect("Failed to copy dir");
  243. assert!(tmp.path().join("parent/copy").is_dir());
  244. assert!(tmp.path().join("parent/copy/sub").is_dir());
  245. assert!(tmp.path().join("parent/copy/sub/file.txt").is_file());
  246. assert_eq!(
  247. std::fs::read(tmp.path().join("parent/copy/sub/file.txt"))
  248. .expect("Failed to read file")
  249. .as_slice(),
  250. b"Hello, world!\n"
  251. );
  252. assert!(tmp.path().join("parent/copy/link").exists());
  253. assert_eq!(
  254. std::fs::read_link(tmp.path().join("parent/copy/link")).expect("Failed to read from symlink"),
  255. PathBuf::from("sub/file.txt")
  256. );
  257. assert_eq!(
  258. std::fs::read(tmp.path().join("parent/copy/link"))
  259. .expect("Failed to read from file")
  260. .as_slice(),
  261. b"Hello, world!\n"
  262. );
  263. }
  264. #[test]
  265. fn retina_icon_paths() {
  266. assert!(!is_retina("data/icons/512x512.png"));
  267. assert!(is_retina("data/icons/512x512@2x.png"));
  268. }
  269. #[test]
  270. fn resource_relative_paths() {
  271. assert_eq!(
  272. resource_relpath(&PathBuf::from("./data/images/button.png")),
  273. PathBuf::from("data/images/button.png")
  274. );
  275. assert_eq!(
  276. resource_relpath(&PathBuf::from("../../images/wheel.png")),
  277. PathBuf::from("_up_/_up_/images/wheel.png")
  278. );
  279. assert_eq!(
  280. resource_relpath(&PathBuf::from("/home/ferris/crab.png")),
  281. PathBuf::from("_root_/home/ferris/crab.png")
  282. );
  283. }
  284. }