diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-23 15:45:33 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-23 15:48:00 +0900 |
| commit | bd6d0186d2c01a3e1d6324ad5a0bcdd71de53098 (patch) | |
| tree | 939eb1dccbfb3341a2f618e734ca23ef84a8e5cc /crates/shirabe/src/downloader/archive_downloader.rs | |
| parent | e068a9d644fde6659a88accd55b3f1d0d9d7cf46 (diff) | |
| download | php-shirabe-bd6d0186d2c01a3e1d6324ad5a0bcdd71de53098.tar.gz php-shirabe-bd6d0186d2c01a3e1d6324ad5a0bcdd71de53098.tar.zst php-shirabe-bd6d0186d2c01a3e1d6324ad5a0bcdd71de53098.zip | |
refactor(promise): drop \React\Promise
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/downloader/archive_downloader.rs')
| -rw-r--r-- | crates/shirabe/src/downloader/archive_downloader.rs | 175 |
1 files changed, 159 insertions, 16 deletions
diff --git a/crates/shirabe/src/downloader/archive_downloader.rs b/crates/shirabe/src/downloader/archive_downloader.rs index ac898d5..6c8a8f9 100644 --- a/crates/shirabe/src/downloader/archive_downloader.rs +++ b/crates/shirabe/src/downloader/archive_downloader.rs @@ -2,15 +2,17 @@ use anyhow::Result; use indexmap::IndexMap; -use shirabe_external_packages::symfony::component::finder::Finder; +use shirabe_external_packages::symfony::component::finder::{Finder, SplFileInfo}; use shirabe_php_shim::{ - DIRECTORY_SEPARATOR, RuntimeException, bin2hex, file_exists, is_dir, random_bytes, realpath, + DIRECTORY_SEPARATOR, PhpMixed, RuntimeException, basename, bin2hex, file_exists, is_dir, + random_bytes, realpath, }; use crate::dependency_resolver::operation::InstallOperation; use crate::downloader::DownloaderInterface; use crate::downloader::FileDownloader; use crate::package::PackageInterface; +use crate::util::Filesystem; use crate::util::Platform; pub trait ArchiveDownloader { @@ -122,22 +124,79 @@ pub trait ArchiveDownloader { .ensure_directory_exists(&temporary_dir); let file_name = self.inner().get_file_name(package, path); - let _ = file_name; + match self.extract(package, &file_name, &temporary_dir).await { + Err(e) => { + install_cleanup(self.inner_mut(), package, path, &temporary_dir)?; + Err(e) + } + Ok(_) => { + if file_exists(&file_name) { + self.inner().filesystem.borrow().unlink(&file_name)?; + } + + let mut rename_as_one = false; + if !file_exists(path) { + rename_as_one = true; + } else if self.inner().filesystem.borrow().is_dir_empty(path) { + let removed = self + .inner() + .filesystem + .borrow_mut() + .remove_directory_php(path); + match removed { + Ok(true) => { + rename_as_one = true; + } + Ok(false) => {} + Err(e) => { + // ignore error, and simply do not renameAsOne + if e.downcast_ref::<RuntimeException>().is_none() { + return Err(e); + } + } + } + } + + let content_dir = get_folder_content(&temporary_dir); + let single_dir_at_top_level = content_dir.len() == 1 + && content_dir + .first() + .map(|file| is_dir(&file.get_pathname())) + .unwrap_or(false); + + 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.first().unwrap().get_pathname() + } else { + temporary_dir.clone() + }; + self.inner() + .filesystem + .borrow_mut() + .rename(&extracted_dir, path)?; + } else { + // only one dir in the archive, extract its contents out of it + let mut from = temporary_dir.clone(); + if single_dir_at_top_level { + from = content_dir.first().unwrap().get_pathname(); + } - // TODO(phase-c-promise): rewrite extract().then(onFulfilled/onRejected) + renameRecursively chain as an await sequence - let promise = self.extract(package, "", &temporary_dir)?; + rename_recursively(&self.inner().filesystem, package, &from, path)?; + } - // TODO(phase-b): the original PHP chains React promise `.then(onFulfilled, onRejected)` - // callbacks that capture `$this`, `$filesystem`, `$package`, `$path`, `$temporaryDir`, - // `$fileName`, and a recursive `$renameRecursively` closure. PromiseInterface::then in - // Rust expects `FnOnce(Option<PhpMixed>) -> Option<PhpMixed>` and the callbacks here - // need both `&mut self` access and to return another promise. This needs a structural - // rework (likely splitting the trait or adding a `then_boxed_result` adapter), plus a - // way to share `&mut self` with the closure (probably `Rc<RefCell<...>>`). - let _ = (&promise, &temporary_dir, package, path); - todo!( - "ArchiveDownloader::install: rewire .then(onFulfilled, onRejected) chain to match PromiseInterface signature" - ) + self.inner() + .filesystem + .borrow_mut() + .remove_directory_async(&temporary_dir) + .await?; + self.inner_mut() + .remove_cleanup_path(package, &temporary_dir); + self.inner_mut().remove_cleanup_path(package, path); + + Ok(None) + } + } } /// @inheritDoc @@ -145,3 +204,87 @@ pub trait ArchiveDownloader { ": Extracting archive" } } + +fn install_cleanup( + inner: &mut FileDownloader, + package: &dyn PackageInterface, + path: &str, + temporary_dir: &str, +) -> Result<()> { + // remove cache if the file was corrupted + inner.clear_last_cache_write(package); + + // clean up + inner + .filesystem + .borrow_mut() + .remove_directory(temporary_dir)?; + if is_dir(path) && realpath(path) != Some(Platform::get_cwd(false).unwrap_or_default()) { + inner.filesystem.borrow_mut().remove_directory(path)?; + } + inner.remove_cleanup_path(package, temporary_dir); + let realpath = realpath(path); + if let Some(realpath) = realpath { + inner.remove_cleanup_path(package, &realpath); + } + + Ok(()) +} + +/// Returns the folder content, excluding .DS_Store +fn get_folder_content(dir: &str) -> Vec<SplFileInfo> { + let mut finder = Finder::create(); + finder + .ignore_vcs(false) + .ignore_dot_files(false) + .not_name(".DS_Store") + .depth(0) + .r#in(dir); + + finder.iter().collect() +} + +/// 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/ +fn rename_recursively( + filesystem: &std::rc::Rc<std::cell::RefCell<Filesystem>>, + package: &dyn PackageInterface, + 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.get_pathname(); + if is_dir(&format!("{}/{}", to, basename(&file))) { + 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, + basename(&file) + ), + code: 0, + } + .into()); + } + rename_recursively( + filesystem, + package, + &file, + &format!("{}/{}", to, basename(&file)), + )?; + } else { + filesystem + .borrow_mut() + .rename(&file, &format!("{}/{}", to, basename(&file)))?; + } + } + + Ok(()) +} |
