From 4aafa15a34645394b693155cc8db07e3581320c0 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 16 May 2026 00:21:58 +0900 Subject: feat(port): port ArchiveDownloader.php --- .../shirabe/src/downloader/archive_downloader.rs | 196 +++++++++++++++++++++ 1 file changed, 196 insertions(+) (limited to 'crates/shirabe/src/downloader') diff --git a/crates/shirabe/src/downloader/archive_downloader.rs b/crates/shirabe/src/downloader/archive_downloader.rs index 3b6f7d7..9220410 100644 --- a/crates/shirabe/src/downloader/archive_downloader.rs +++ b/crates/shirabe/src/downloader/archive_downloader.rs @@ -1 +1,197 @@ //! ref: composer/src/Composer/Downloader/ArchiveDownloader.php + +use anyhow::Result; +use indexmap::IndexMap; +use shirabe_php_shim::{bin2hex, file_exists, is_dir, random_bytes, realpath, RuntimeException, DIRECTORY_SEPARATOR}; +use shirabe_external_packages::react::promise::promise_interface::PromiseInterface; +use shirabe_external_packages::symfony::component::finder::finder::Finder; +use crate::dependency_resolver::operation::install_operation::InstallOperation; +use crate::downloader::file_downloader::FileDownloader; +use crate::package::package_interface::PackageInterface; +use crate::util::platform::Platform; + +#[derive(Debug)] +pub struct ArchiveDownloader { + pub(crate) inner: FileDownloader, + pub(crate) cleanup_executed: IndexMap, +} + +impl ArchiveDownloader { + pub fn prepare(&mut self, r#type: &str, package: &dyn PackageInterface, path: &str, prev_package: Option<&dyn PackageInterface>) -> Result> { + self.cleanup_executed.remove(package.get_name()); + + self.inner.prepare(r#type, package, path, prev_package) + } + + pub fn cleanup(&mut self, r#type: &str, package: &dyn PackageInterface, path: &str, prev_package: Option<&dyn PackageInterface>) -> Result> { + self.cleanup_executed.insert(package.get_name().to_string(), true); + + self.inner.cleanup(r#type, package, path, prev_package) + } + + pub fn install(&mut self, package: &dyn PackageInterface, path: &str, output: bool) -> Result> { + if output { + self.inner.io.write_error(&format!(" - {}{}", InstallOperation::format(package, false), self.get_install_operation_appendix(package, path))); + } + + let vendor_dir = self.inner.config.get("vendor-dir"); + + // clean up the target directory, unless it contains the vendor dir, as the vendor dir contains + // the archive to be extracted. This is the case when installing with create-project in the current directory + // but in that case we ensure the directory is empty already in ProjectInstaller so no need to empty it here. + if !self.inner.filesystem.normalize_path(&vendor_dir).contains(&self.inner.filesystem.normalize_path(&format!("{}{}", path, DIRECTORY_SEPARATOR))) { + self.inner.filesystem.empty_directory(path); + } + + let temporary_dir; + loop { + temporary_dir = format!("{}/composer/{}", vendor_dir, bin2hex(&random_bytes(4))); + if !is_dir(&temporary_dir) { + break; + } + } + + self.inner.add_cleanup_path(package, &temporary_dir); + // avoid cleaning up $path if installing in "." for eg create-project as we can not + // delete the directory we are currently in on windows + if !is_dir(path) || realpath(path) != Platform::get_cwd() { + self.inner.add_cleanup_path(package, path); + } + + self.inner.filesystem.ensure_directory_exists(&temporary_dir); + let file_name = self.inner.get_file_name(package, path); + + let filesystem = &self.inner.filesystem; + + let cleanup = move || { + // remove cache if the file was corrupted + self.inner.clear_last_cache_write(package); + + // clean up + filesystem.remove_directory(&temporary_dir); + if is_dir(path) && realpath(path) != Platform::get_cwd() { + filesystem.remove_directory(path); + } + self.inner.remove_cleanup_path(package, &temporary_dir); + let realpath_result = realpath(path); + if let Some(realpath_val) = realpath_result { + self.inner.remove_cleanup_path(package, &realpath_val); + } + }; + + let promise = match self.extract(package, &file_name, &temporary_dir) { + Ok(p) => p, + Err(e) => { + cleanup(); + return Err(e); + } + }; + + Ok(promise.then( + Box::new(move || -> Result> { + if file_exists(&file_name) { + filesystem.unlink(&file_name); + } + + let get_folder_content = |dir: &str| -> Vec { + let finder = Finder::create() + .ignore_vcs(false) + .ignore_dot_files(false) + .not_name(".DS_Store") + .depth(0) + .in_(dir); + + finder.into_iter().collect() + }; + + let mut rename_recursively: Option Result<()>>> = None; + // Renames (and recursively merges if needed) a folder into another one + // + // For custom installers, where packages may share paths, and given Composer 2's parallelism, we need to make sure + // that the source directory gets merged into the target one if the target exists. Otherwise rename() by default would + // put the source into the target e.g. src/ => target/src/ (assuming target exists) instead of src/ => target/ + rename_recursively = Some(Box::new(move |from: &str, to: &str| -> Result<()> { + let content_dir = get_folder_content(from); + + // move files back out of the temp dir + for file in &content_dir { + let file = file.to_string_lossy().to_string(); + let file_basename = shirabe_php_shim::basename(&file); + if is_dir(&format!("{}/{}", to, file_basename)) { + if !is_dir(&file) { + return Err(RuntimeException { + message: format!("Installing {} would lead to overwriting the {}/{} directory with a file from the package, invalid operation.", package, to, file_basename), + code: 0, + }.into()); + } + rename_recursively.as_ref().unwrap()(&file, &format!("{}/{}", to, file_basename))?; + } else { + filesystem.rename(&file, &format!("{}/{}", to, file_basename)); + } + } + + Ok(()) + })); + + let mut rename_as_one = false; + if !file_exists(path) { + rename_as_one = true; + } else if filesystem.is_dir_empty(path) { + match filesystem.remove_directory_php(path) { + Ok(true) => { + rename_as_one = true; + } + _ => { + // ignore error, and simply do not renameAsOne + } + } + } + + let content_dir = get_folder_content(&temporary_dir); + let single_dir_at_top_level = content_dir.len() == 1 && is_dir(&content_dir[0].to_string_lossy().to_string()); + + if rename_as_one { + // if the target $path is clear, we can rename the whole package in one go instead of looping over the contents + let extracted_dir = if single_dir_at_top_level { + content_dir[0].to_string_lossy().to_string() + } else { + temporary_dir.clone() + }; + filesystem.rename(&extracted_dir, path); + } else { + // only one dir in the archive, extract its contents out of it + let from = if single_dir_at_top_level { + content_dir[0].to_string_lossy().to_string() + } else { + temporary_dir.clone() + }; + + rename_recursively.as_ref().unwrap()(&from, path)?; + } + + let promise = filesystem.remove_directory_async(&temporary_dir); + + Ok(promise.then( + Box::new(move || -> Result<()> { + self.inner.remove_cleanup_path(package, &temporary_dir); + self.inner.remove_cleanup_path(package, path); + Ok(()) + }), + None, + )) + }), + Box::new(move |e: anyhow::Error| -> Result<()> { + cleanup(); + Err(e) + }), + )) + } + + pub fn get_install_operation_appendix(&self, _package: &dyn PackageInterface, _path: &str) -> &str { + ": Extracting archive" + } + + pub(crate) fn extract(&self, _package: &dyn PackageInterface, _file: &str, _path: &str) -> Result> { + todo!() + } +} -- cgit v1.3.1