From c54d88462e37e68deaafd051989adbe3a765fde7 Mon Sep 17 00:00:00 2001 From: arctic-hen7 Date: Fri, 11 Nov 2022 09:25:06 +1100 Subject: [PATCH] feat(cli): made cli symlink exported content instead of copying it This should substantially speed up exports for very large apps. Note that deployment will, of course, still copy. Also, if symlinking fails, copying will be used instead, with no warning. If the CLI breaks on Windows in the next release, this will almost certainly be why. (It shouldn't though...) --- packages/perseus-cli/src/deploy.rs | 5 +- packages/perseus-cli/src/errors.rs | 7 ++ packages/perseus-cli/src/export.rs | 150 ++++++++++++++++------------- 3 files changed, 93 insertions(+), 69 deletions(-) diff --git a/packages/perseus-cli/src/deploy.rs b/packages/perseus-cli/src/deploy.rs index a6483612f7..b02d8651ca 100644 --- a/packages/perseus-cli/src/deploy.rs +++ b/packages/perseus-cli/src/deploy.rs @@ -14,6 +14,9 @@ use std::path::PathBuf; /// together in one folder that can be conveniently uploaded to a server, file /// host, etc. This can return any kind of error because deploying involves /// working with other subcommands. +/// +/// Note that this will execute a full copy of all static assets, so apps with +/// large volumes of these may have longer deployment times. pub fn deploy( dir: PathBuf, opts: &DeployOpts, @@ -132,7 +135,7 @@ fn deploy_full( } .into()); } - let from = dir.join("dist/pkg"); + let from = dir.join("dist/pkg"); // Note: this handles snippets and the like if let Err(err) = copy_dir(&from, &output_path.join("dist"), &CopyOptions::new()) { return Err(DeployError::MoveDirFailed { to: output, diff --git a/packages/perseus-cli/src/errors.rs b/packages/perseus-cli/src/errors.rs index ccaf088ef6..4a9cb025ba 100644 --- a/packages/perseus-cli/src/errors.rs +++ b/packages/perseus-cli/src/errors.rs @@ -98,6 +98,13 @@ pub enum ExportError { #[source] source: std::io::Error, }, + #[error("couldn't copy directory from '{from}' to '{to}' for exporting")] + MoveDirFailed { + to: String, + from: String, + #[source] + source: fs_extra::error::Error, + }, // We need to execute in exports #[error(transparent)] ExecutionError(#[from] ExecutionError), diff --git a/packages/perseus-cli/src/export.rs b/packages/perseus-cli/src/export.rs index 1b4d736c19..4d8f17291c 100644 --- a/packages/perseus-cli/src/export.rs +++ b/packages/perseus-cli/src/export.rs @@ -22,17 +22,82 @@ macro_rules! handle_exit_code { }; } -/// An internal macro for copying files into the export package. The `from` and -/// `to` that this accepts should be extensions of the `target`, and they'll be -/// `.join()`ed on. +/// An internal macro for symlinking/copying files into the export package. The +/// `from` and `to` that this accepts should be extensions of the `target`, and +/// they'll be `.join()`ed on. +/// +/// This will attempt to symlink, and, if that fails, it will print a warning +/// and copy normally. +/// +/// For symlinking to work on Windows, developer mode must be enabled. macro_rules! copy_file { ($from:expr, $to:expr, $target:expr) => { - if let Err(err) = fs::copy($target.join($from), $target.join($to)) { - return Err(ExportError::MoveAssetFailed { - to: $to.to_string(), - from: $from.to_string(), - source: err, - }); + // Try symlinking first + #[cfg(unix)] + if std::os::unix::fs::symlink($target.join($from), $target.join($to)).is_err() { + // That failed, try a usual copy + if let Err(err) = fs::copy($target.join($from), $target.join($to)) { + return Err(ExportError::MoveAssetFailed { + to: $to.to_string(), + from: $from.to_string(), + source: err, + }); + } + } + #[cfg(windows)] + if std::os::windows::fs::symlink_file($target.join($from), $target.join($to)).is_err() { + // That failed, try a usual copy + if let Err(err) = fs::copy($target.join($from), $target.join($to)) { + return Err(ExportError::MoveAssetFailed { + to: $to.to_string(), + from: $from.to_string(), + source: err, + }); + } + } + }; +} + +/// An internal macro for symlinking/copying directories into the export +/// package. The `from` and `to` that this accepts should be extensions of the +/// `target`, and they'll be `.join()`ed on. +/// +/// This will attempt to symlink, and, if that fails, it will print a warning +/// and copy normally. +/// +/// For symlinking to work on Windows, developer mode must be enabled. +macro_rules! copy_directory { + ($from:expr, $to:expr, $to_symlink:expr, $target:expr) => { + // Try symlinking first + #[cfg(unix)] + if std::os::unix::fs::symlink($target.join($from), $target.join($to_symlink)).is_err() { + // That failed, try a usual copy + if let Err(err) = fs_extra::dir::copy( + $target.join($from), + $target.join($to), + &fs_extra::dir::CopyOptions::new(), + ) { + return Err(ExportError::MoveDirFailed { + to: $to.to_string(), + from: $from.to_string(), + source: err, + }); + } + } + #[cfg(windows)] + if std::os::unix::fs::symlink_dir($target.join($from), $target.join($to_symlink)).is_err() { + // That failed, try a usual copy + if let Err(err) = fs_extra::dir::copy( + $target.join($from), + $target.join($to), + &fs_extra::dir::CopyOptions::new(), + ) { + return Err(ExportError::MoveDirFailed { + to: $to.to_string(), + from: $from.to_string(), + source: err, + }); + } } }; } @@ -53,66 +118,15 @@ pub fn finalize_export(target: &Path) -> Result<(), ExportError> { target ); // Copy any JS snippets over (if the directory doesn't exist though, don't do - // anything) This takes a target of the `dist/` directory, and then extends - // on that - fn copy_snippets(ext: &str, parent: &Path) -> Result<(), ExportError> { - // We read from the parent directory (`.perseus`), extended with `ext` - if let Ok(snippets) = fs::read_dir(&parent.join(ext)) { - for file in snippets { - let path = match file { - Ok(file) => file.path(), - Err(err) => { - return Err(ExportError::MoveAssetFailed { - from: "js snippet".to_string(), - to: "exportable js snippet".to_string(), - source: err, - }) - } - }; - // Recurse on any directories and copy any files - if path.is_dir() { - // We continue to pass on the parent, but we add the filename of this directory - // to the extension - copy_snippets( - &format!("{}/{}", ext, path.file_name().unwrap().to_str().unwrap()), - parent, - )?; - } else { - // `ext` holds the folder structure of this file, which we'll preserve - // We must remove the prefix though (which is hardcoded in the initial - // invocation of this function) - let dir_tree = ext.strip_prefix("dist/pkg/snippets").unwrap(); - // This is to avoid `//` - let dir_tree = if dir_tree.is_empty() { - String::new() - } else if dir_tree.starts_with('/') { - dir_tree.to_string() - } else { - format!("/{}", dir_tree) - }; - let filename = path.file_name().unwrap().to_str().unwrap(); - let final_dir_tree = - parent.join(format!("dist/exported/.perseus/snippets{}", dir_tree)); - let path_to_copy_to = parent.join(&format!( - "dist/exported/.perseus/snippets{}/{}", - dir_tree, filename - )); - // Create the directory structure needed for this - if let Err(err) = fs::create_dir_all(&final_dir_tree) { - return Err(ExportError::DirStructureCreationFailed { source: err }); - } - copy_file!( - path.to_str().unwrap(), - path_to_copy_to.to_str().unwrap(), - parent - ); - } - } - } - - Ok(()) + // anything) + if fs::metadata(target.join("dist/pkg/snippets")).is_ok() { + copy_directory!( + "dist/pkg/snippets", + "dist/exported/.perseus", // For a usual copy + "dist/exported/.perseus/snippets", // For a symlink + target + ); } - copy_snippets("dist/pkg/snippets", target)?; Ok(()) }