diff options
Diffstat (limited to 'cli/util/fs.rs')
-rw-r--r-- | cli/util/fs.rs | 661 |
1 files changed, 661 insertions, 0 deletions
diff --git a/cli/util/fs.rs b/cli/util/fs.rs new file mode 100644 index 000000000..35cdae4fa --- /dev/null +++ b/cli/util/fs.rs @@ -0,0 +1,661 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use deno_core::anyhow::Context; +use deno_core::error::AnyError; +pub use deno_core::normalize_path; +use deno_core::ModuleSpecifier; +use deno_runtime::deno_crypto::rand; +use deno_runtime::deno_node::PathClean; +use std::env::current_dir; +use std::fs::OpenOptions; +use std::io::Error; +use std::io::ErrorKind; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; +use std::time::Duration; +use walkdir::WalkDir; + +use super::path::specifier_to_file_path; + +pub fn atomic_write_file<T: AsRef<[u8]>>( + filename: &Path, + data: T, + mode: u32, +) -> std::io::Result<()> { + let rand: String = (0..4) + .map(|_| format!("{:02x}", rand::random::<u8>())) + .collect(); + let extension = format!("{}.tmp", rand); + let tmp_file = filename.with_extension(extension); + write_file(&tmp_file, data, mode)?; + std::fs::rename(tmp_file, filename)?; + Ok(()) +} + +pub fn write_file<T: AsRef<[u8]>>( + filename: &Path, + data: T, + mode: u32, +) -> std::io::Result<()> { + write_file_2(filename, data, true, mode, true, false) +} + +pub fn write_file_2<T: AsRef<[u8]>>( + filename: &Path, + data: T, + update_mode: bool, + mode: u32, + is_create: bool, + is_append: bool, +) -> std::io::Result<()> { + let mut file = OpenOptions::new() + .read(false) + .write(true) + .append(is_append) + .truncate(!is_append) + .create(is_create) + .open(filename)?; + + if update_mode { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = mode & 0o777; + let permissions = PermissionsExt::from_mode(mode); + file.set_permissions(permissions)?; + } + #[cfg(not(unix))] + let _ = mode; + } + + file.write_all(data.as_ref()) +} + +/// Similar to `std::fs::canonicalize()` but strips UNC prefixes on Windows. +pub fn canonicalize_path(path: &Path) -> Result<PathBuf, Error> { + let path = path.canonicalize()?; + #[cfg(windows)] + return Ok(strip_unc_prefix(path)); + #[cfg(not(windows))] + return Ok(path); +} + +/// Canonicalizes a path which might be non-existent by going up the +/// ancestors until it finds a directory that exists, canonicalizes +/// that path, then adds back the remaining path components. +/// +/// Note: When using this, you should be aware that a symlink may +/// subsequently be created along this path by some other code. +pub fn canonicalize_path_maybe_not_exists( + path: &Path, +) -> Result<PathBuf, Error> { + let path = path.to_path_buf().clean(); + let mut path = path.as_path(); + let mut names_stack = Vec::new(); + loop { + match canonicalize_path(path) { + Ok(mut canonicalized_path) => { + for name in names_stack.into_iter().rev() { + canonicalized_path = canonicalized_path.join(name); + } + return Ok(canonicalized_path); + } + Err(err) if err.kind() == ErrorKind::NotFound => { + names_stack.push(path.file_name().unwrap()); + path = path.parent().unwrap(); + } + Err(err) => return Err(err), + } + } +} + +#[cfg(windows)] +fn strip_unc_prefix(path: PathBuf) -> PathBuf { + use std::path::Component; + use std::path::Prefix; + + let mut components = path.components(); + match components.next() { + Some(Component::Prefix(prefix)) => { + match prefix.kind() { + // \\?\device + Prefix::Verbatim(device) => { + let mut path = PathBuf::new(); + path.push(format!(r"\\{}\", device.to_string_lossy())); + path.extend(components.filter(|c| !matches!(c, Component::RootDir))); + path + } + // \\?\c:\path + Prefix::VerbatimDisk(_) => { + let mut path = PathBuf::new(); + path.push(prefix.as_os_str().to_string_lossy().replace(r"\\?\", "")); + path.extend(components); + path + } + // \\?\UNC\hostname\share_name\path + Prefix::VerbatimUNC(hostname, share_name) => { + let mut path = PathBuf::new(); + path.push(format!( + r"\\{}\{}\", + hostname.to_string_lossy(), + share_name.to_string_lossy() + )); + path.extend(components.filter(|c| !matches!(c, Component::RootDir))); + path + } + _ => path, + } + } + _ => path, + } +} + +pub fn resolve_from_cwd(path: &Path) -> Result<PathBuf, AnyError> { + let resolved_path = if path.is_absolute() { + path.to_owned() + } else { + let cwd = + current_dir().context("Failed to get current working directory")?; + cwd.join(path) + }; + + Ok(normalize_path(&resolved_path)) +} + +/// Collects file paths that satisfy the given predicate, by recursively walking `files`. +/// If the walker visits a path that is listed in `ignore`, it skips descending into the directory. +pub fn collect_files<P>( + files: &[PathBuf], + ignore: &[PathBuf], + predicate: P, +) -> Result<Vec<PathBuf>, AnyError> +where + P: Fn(&Path) -> bool, +{ + let mut target_files = Vec::new(); + + // retain only the paths which exist and ignore the rest + let canonicalized_ignore: Vec<PathBuf> = ignore + .iter() + .filter_map(|i| canonicalize_path(i).ok()) + .collect(); + + for file in files { + for entry in WalkDir::new(file) + .into_iter() + .filter_entry(|e| { + canonicalize_path(e.path()).map_or(false, |c| { + !canonicalized_ignore.iter().any(|i| c.starts_with(i)) + }) + }) + .filter_map(|e| match e { + Ok(e) if !e.file_type().is_dir() && predicate(e.path()) => Some(e), + _ => None, + }) + { + target_files.push(canonicalize_path(entry.path())?) + } + } + + Ok(target_files) +} + +/// Collects module specifiers that satisfy the given predicate as a file path, by recursively walking `include`. +/// Specifiers that start with http and https are left intact. +pub fn collect_specifiers<P>( + include: Vec<String>, + ignore: &[PathBuf], + predicate: P, +) -> Result<Vec<ModuleSpecifier>, AnyError> +where + P: Fn(&Path) -> bool, +{ + let mut prepared = vec![]; + + let root_path = current_dir()?; + for path in include { + let lowercase_path = path.to_lowercase(); + if lowercase_path.starts_with("http://") + || lowercase_path.starts_with("https://") + { + let url = ModuleSpecifier::parse(&path)?; + prepared.push(url); + continue; + } + + let p = if lowercase_path.starts_with("file://") { + specifier_to_file_path(&ModuleSpecifier::parse(&path)?)? + } else { + root_path.join(path) + }; + let p = normalize_path(&p); + if p.is_dir() { + let test_files = collect_files(&[p], ignore, &predicate).unwrap(); + let mut test_files_as_urls = test_files + .iter() + .map(|f| ModuleSpecifier::from_file_path(f).unwrap()) + .collect::<Vec<ModuleSpecifier>>(); + + test_files_as_urls.sort(); + prepared.extend(test_files_as_urls); + } else { + let url = ModuleSpecifier::from_file_path(p).unwrap(); + prepared.push(url); + } + } + + Ok(prepared) +} + +/// Asynchronously removes a directory and all its descendants, but does not error +/// when the directory does not exist. +pub async fn remove_dir_all_if_exists(path: &Path) -> std::io::Result<()> { + let result = tokio::fs::remove_dir_all(path).await; + match result { + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), + _ => result, + } +} + +/// Copies a directory to another directory. +/// +/// Note: Does not handle symlinks. +pub fn copy_dir_recursive(from: &Path, to: &Path) -> Result<(), AnyError> { + std::fs::create_dir_all(to) + .with_context(|| format!("Creating {}", to.display()))?; + let read_dir = std::fs::read_dir(from) + .with_context(|| format!("Reading {}", from.display()))?; + + for entry in read_dir { + let entry = entry?; + let file_type = entry.file_type()?; + let new_from = from.join(entry.file_name()); + let new_to = to.join(entry.file_name()); + + if file_type.is_dir() { + copy_dir_recursive(&new_from, &new_to).with_context(|| { + format!("Dir {} to {}", new_from.display(), new_to.display()) + })?; + } else if file_type.is_file() { + std::fs::copy(&new_from, &new_to).with_context(|| { + format!("Copying {} to {}", new_from.display(), new_to.display()) + })?; + } + } + + Ok(()) +} + +/// Hardlinks the files in one directory to another directory. +/// +/// Note: Does not handle symlinks. +pub fn hard_link_dir_recursive(from: &Path, to: &Path) -> Result<(), AnyError> { + std::fs::create_dir_all(to) + .with_context(|| format!("Creating {}", to.display()))?; + let read_dir = std::fs::read_dir(from) + .with_context(|| format!("Reading {}", from.display()))?; + + for entry in read_dir { + let entry = entry?; + let file_type = entry.file_type()?; + let new_from = from.join(entry.file_name()); + let new_to = to.join(entry.file_name()); + + if file_type.is_dir() { + hard_link_dir_recursive(&new_from, &new_to).with_context(|| { + format!("Dir {} to {}", new_from.display(), new_to.display()) + })?; + } else if file_type.is_file() { + // note: chance for race conditions here between attempting to create, + // then removing, then attempting to create. There doesn't seem to be + // a way to hard link with overwriting in Rust, but maybe there is some + // way with platform specific code. The workaround here is to handle + // scenarios where something else might create or remove files. + if let Err(err) = std::fs::hard_link(&new_from, &new_to) { + if err.kind() == ErrorKind::AlreadyExists { + if let Err(err) = std::fs::remove_file(&new_to) { + if err.kind() == ErrorKind::NotFound { + // Assume another process/thread created this hard link to the file we are wanting + // to remove then sleep a little bit to let the other process/thread move ahead + // faster to reduce contention. + std::thread::sleep(Duration::from_millis(10)); + } else { + return Err(err).with_context(|| { + format!( + "Removing file to hard link {} to {}", + new_from.display(), + new_to.display() + ) + }); + } + } + + // Always attempt to recreate the hardlink. In contention scenarios, the other process + // might have been killed or exited after removing the file, but before creating the hardlink + if let Err(err) = std::fs::hard_link(&new_from, &new_to) { + // Assume another process/thread created this hard link to the file we are wanting + // to now create then sleep a little bit to let the other process/thread move ahead + // faster to reduce contention. + if err.kind() == ErrorKind::AlreadyExists { + std::thread::sleep(Duration::from_millis(10)); + } else { + return Err(err).with_context(|| { + format!( + "Hard linking {} to {}", + new_from.display(), + new_to.display() + ) + }); + } + } + } else { + return Err(err).with_context(|| { + format!( + "Hard linking {} to {}", + new_from.display(), + new_to.display() + ) + }); + } + } + } + } + + Ok(()) +} + +pub fn symlink_dir(oldpath: &Path, newpath: &Path) -> Result<(), AnyError> { + let err_mapper = |err: Error| { + Error::new( + err.kind(), + format!( + "{}, symlink '{}' -> '{}'", + err, + oldpath.display(), + newpath.display() + ), + ) + }; + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + symlink(oldpath, newpath).map_err(err_mapper)?; + } + #[cfg(not(unix))] + { + use std::os::windows::fs::symlink_dir; + symlink_dir(oldpath, newpath).map_err(err_mapper)?; + } + Ok(()) +} + +/// Gets the total size (in bytes) of a directory. +pub fn dir_size(path: &Path) -> std::io::Result<u64> { + let entries = std::fs::read_dir(path)?; + let mut total = 0; + for entry in entries { + let entry = entry?; + total += match entry.metadata()? { + data if data.is_dir() => dir_size(&entry.path())?, + data => data.len(), + }; + } + Ok(total) +} + +#[cfg(test)] +mod tests { + use super::*; + use test_util::TempDir; + + #[test] + fn resolve_from_cwd_child() { + let cwd = current_dir().unwrap(); + assert_eq!(resolve_from_cwd(Path::new("a")).unwrap(), cwd.join("a")); + } + + #[test] + fn resolve_from_cwd_dot() { + let cwd = current_dir().unwrap(); + assert_eq!(resolve_from_cwd(Path::new(".")).unwrap(), cwd); + } + + #[test] + fn resolve_from_cwd_parent() { + let cwd = current_dir().unwrap(); + assert_eq!(resolve_from_cwd(Path::new("a/..")).unwrap(), cwd); + } + + #[test] + fn test_normalize_path() { + assert_eq!(normalize_path(Path::new("a/../b")), PathBuf::from("b")); + assert_eq!(normalize_path(Path::new("a/./b/")), PathBuf::from("a/b/")); + assert_eq!( + normalize_path(Path::new("a/./b/../c")), + PathBuf::from("a/c") + ); + + if cfg!(windows) { + assert_eq!( + normalize_path(Path::new("C:\\a\\.\\b\\..\\c")), + PathBuf::from("C:\\a\\c") + ); + } + } + + // TODO: Get a good expected value here for Windows. + #[cfg(not(windows))] + #[test] + fn resolve_from_cwd_absolute() { + let expected = Path::new("/a"); + assert_eq!(resolve_from_cwd(expected).unwrap(), expected); + } + + #[test] + fn test_collect_files() { + fn create_files(dir_path: &Path, files: &[&str]) { + std::fs::create_dir(dir_path).expect("Failed to create directory"); + for f in files { + let path = dir_path.join(f); + std::fs::write(path, "").expect("Failed to create file"); + } + } + + // dir.ts + // ├── a.ts + // ├── b.js + // ├── child + // │ ├── e.mjs + // │ ├── f.mjsx + // │ ├── .foo.TS + // │ └── README.md + // ├── c.tsx + // ├── d.jsx + // └── ignore + // ├── g.d.ts + // └── .gitignore + + let t = TempDir::new(); + + let root_dir_path = t.path().join("dir.ts"); + let root_dir_files = ["a.ts", "b.js", "c.tsx", "d.jsx"]; + create_files(&root_dir_path, &root_dir_files); + + let child_dir_path = root_dir_path.join("child"); + let child_dir_files = ["e.mjs", "f.mjsx", ".foo.TS", "README.md"]; + create_files(&child_dir_path, &child_dir_files); + + let ignore_dir_path = root_dir_path.join("ignore"); + let ignore_dir_files = ["g.d.ts", ".gitignore"]; + create_files(&ignore_dir_path, &ignore_dir_files); + + let result = collect_files(&[root_dir_path], &[ignore_dir_path], |path| { + // exclude dotfiles + path + .file_name() + .and_then(|f| f.to_str()) + .map_or(false, |f| !f.starts_with('.')) + }) + .unwrap(); + let expected = [ + "a.ts", + "b.js", + "e.mjs", + "f.mjsx", + "README.md", + "c.tsx", + "d.jsx", + ]; + for e in expected.iter() { + assert!(result.iter().any(|r| r.ends_with(e))); + } + assert_eq!(result.len(), expected.len()); + } + + #[test] + fn test_collect_specifiers() { + fn create_files(dir_path: &Path, files: &[&str]) { + std::fs::create_dir(dir_path).expect("Failed to create directory"); + for f in files { + let path = dir_path.join(f); + std::fs::write(path, "").expect("Failed to create file"); + } + } + + // dir.ts + // ├── a.ts + // ├── b.js + // ├── child + // │ ├── e.mjs + // │ ├── f.mjsx + // │ ├── .foo.TS + // │ └── README.md + // ├── c.tsx + // ├── d.jsx + // └── ignore + // ├── g.d.ts + // └── .gitignore + + let t = TempDir::new(); + + let root_dir_path = t.path().join("dir.ts"); + let root_dir_files = ["a.ts", "b.js", "c.tsx", "d.jsx"]; + create_files(&root_dir_path, &root_dir_files); + + let child_dir_path = root_dir_path.join("child"); + let child_dir_files = ["e.mjs", "f.mjsx", ".foo.TS", "README.md"]; + create_files(&child_dir_path, &child_dir_files); + + let ignore_dir_path = root_dir_path.join("ignore"); + let ignore_dir_files = ["g.d.ts", ".gitignore"]; + create_files(&ignore_dir_path, &ignore_dir_files); + + let predicate = |path: &Path| { + // exclude dotfiles + path + .file_name() + .and_then(|f| f.to_str()) + .map_or(false, |f| !f.starts_with('.')) + }; + + let result = collect_specifiers( + vec![ + "http://localhost:8080".to_string(), + root_dir_path.to_str().unwrap().to_string(), + "https://localhost:8080".to_string(), + ], + &[ignore_dir_path], + predicate, + ) + .unwrap(); + + let root_dir_url = ModuleSpecifier::from_file_path( + canonicalize_path(&root_dir_path).unwrap(), + ) + .unwrap() + .to_string(); + let expected: Vec<ModuleSpecifier> = [ + "http://localhost:8080", + &format!("{}/a.ts", root_dir_url), + &format!("{}/b.js", root_dir_url), + &format!("{}/c.tsx", root_dir_url), + &format!("{}/child/README.md", root_dir_url), + &format!("{}/child/e.mjs", root_dir_url), + &format!("{}/child/f.mjsx", root_dir_url), + &format!("{}/d.jsx", root_dir_url), + "https://localhost:8080", + ] + .iter() + .map(|f| ModuleSpecifier::parse(f).unwrap()) + .collect::<Vec<_>>(); + + assert_eq!(result, expected); + + let scheme = if cfg!(target_os = "windows") { + "file:///" + } else { + "file://" + }; + let result = collect_specifiers( + vec![format!( + "{}{}", + scheme, + root_dir_path + .join("child") + .to_str() + .unwrap() + .replace('\\', "/") + )], + &[], + predicate, + ) + .unwrap(); + + let expected: Vec<ModuleSpecifier> = [ + &format!("{}/child/README.md", root_dir_url), + &format!("{}/child/e.mjs", root_dir_url), + &format!("{}/child/f.mjsx", root_dir_url), + ] + .iter() + .map(|f| ModuleSpecifier::parse(f).unwrap()) + .collect::<Vec<_>>(); + + assert_eq!(result, expected); + } + + #[cfg(windows)] + #[test] + fn test_strip_unc_prefix() { + run_test(r"C:\", r"C:\"); + run_test(r"C:\test\file.txt", r"C:\test\file.txt"); + + run_test(r"\\?\C:\", r"C:\"); + run_test(r"\\?\C:\test\file.txt", r"C:\test\file.txt"); + + run_test(r"\\.\C:\", r"\\.\C:\"); + run_test(r"\\.\C:\Test\file.txt", r"\\.\C:\Test\file.txt"); + + run_test(r"\\?\UNC\localhost\", r"\\localhost"); + run_test(r"\\?\UNC\localhost\c$\", r"\\localhost\c$"); + run_test( + r"\\?\UNC\localhost\c$\Windows\file.txt", + r"\\localhost\c$\Windows\file.txt", + ); + run_test(r"\\?\UNC\wsl$\deno.json", r"\\wsl$\deno.json"); + + run_test(r"\\?\server1", r"\\server1"); + run_test(r"\\?\server1\e$\", r"\\server1\e$\"); + run_test( + r"\\?\server1\e$\test\file.txt", + r"\\server1\e$\test\file.txt", + ); + + fn run_test(input: &str, expected: &str) { + assert_eq!( + strip_unc_prefix(PathBuf::from(input)), + PathBuf::from(expected) + ); + } + } +} |