diff --git a/crates/package-manager/src/install_package_by_snapshot.rs b/crates/package-manager/src/install_package_by_snapshot.rs index 0adb755a..75ae69c4 100644 --- a/crates/package-manager/src/install_package_by_snapshot.rs +++ b/crates/package-manager/src/install_package_by_snapshot.rs @@ -6,7 +6,7 @@ use pacquet_npmrc::Npmrc; use pacquet_tarball::{DownloadTarballToStore, TarballError}; use pipe_trait::Pipe; use reqwest::Client; -use std::borrow::Cow; +use std::{borrow::Cow, ffi::OsString, io::ErrorKind}; /// This subroutine downloads a package tarball, extracts it, installs it to a virtual dir, /// then creates the symlink layout for the package. @@ -56,17 +56,34 @@ impl<'a> InstallPackageBySnapshot<'a> { } }; - // TODO: skip when already exists in store? - let cas_paths = DownloadTarballToStore { - http_client, - store_dir: &config.store_dir, - package_integrity: integrity, - package_unpacked_size: None, - package_url: &tarball_url, - } - .run_without_mem_cache() - .await - .map_err(InstallPackageBySnapshotError::DownloadTarball)?; + let store_dir = &config.store_dir; + let cas_paths = match store_dir.read_index_file(integrity) { + Ok(index) => store_dir + .cas_file_paths_by_index(&index) + .map(|(entry_path, store_path)| (OsString::from(entry_path), store_path)) + .collect(), + Err(error) => { + if error.io_error_kind() != Some(ErrorKind::NotFound) { + let path = error.file_path(); + tracing::warn!( + target: "pacquet::read_store", + ?error, + ?path, + "Failed to read index from store", + ); + } + DownloadTarballToStore { + http_client, + store_dir, + package_integrity: integrity, + package_unpacked_size: None, + package_url: &tarball_url, + } + .run_without_mem_cache() + .await + .map_err(InstallPackageBySnapshotError::DownloadTarball)? + } + }; CreateVirtualDirBySnapshot { virtual_store_dir: &config.virtual_store_dir, diff --git a/crates/store-dir/src/cas_file.rs b/crates/store-dir/src/cas_file.rs index f53d4bae..93cd0170 100644 --- a/crates/store-dir/src/cas_file.rs +++ b/crates/store-dir/src/cas_file.rs @@ -1,8 +1,9 @@ -use crate::{FileHash, StoreDir}; +use crate::{FileHash, PackageFilesIndex, StoreDir}; use derive_more::{Display, Error}; use miette::Diagnostic; -use pacquet_fs::{ensure_file, file_mode::EXEC_MODE, EnsureFileError}; +use pacquet_fs::{ensure_file, file_mode, EnsureFileError}; use sha2::{Digest, Sha512}; +use ssri::{Algorithm, Integrity}; use std::path::PathBuf; impl StoreDir { @@ -12,6 +13,28 @@ impl StoreDir { let suffix = if executable { "-exec" } else { "" }; self.file_path_by_hex_str(&hex, suffix) } + + /// List maps from index entry to real or would-be file path in the store directory. + pub fn cas_file_paths_by_index<'a>( + &'a self, + index: &'a PackageFilesIndex, + ) -> impl Iterator + 'a { + index.files.iter().map(|(entry_path, info)| { + let entry_path = entry_path.as_str(); + let (algorithm, hex) = info + .integrity + .parse::() + .expect("parse integrity") // TODO: parse integrity before this + .to_hex(); + assert!( + matches!(algorithm, Algorithm::Sha512), + "Only Sha512 is supported. {algorithm} isn't", + ); // TODO: write a custom parser and remove this + let suffix = if file_mode::is_all_exec(info.mode) { "-exec" } else { "" }; + let cas_path = self.file_path_by_hex_str(&hex, suffix); + (entry_path, cas_path) + }) + } } /// Error type of [`StoreDir::write_cas_file`]. @@ -29,7 +52,7 @@ impl StoreDir { ) -> Result<(PathBuf, FileHash), WriteCasFileError> { let file_hash = Sha512::digest(buffer); let file_path = self.cas_file_path(file_hash, executable); - let mode = executable.then_some(EXEC_MODE); + let mode = executable.then_some(file_mode::EXEC_MODE); ensure_file(&file_path, buffer, mode).map_err(WriteCasFileError::WriteFile)?; Ok((file_path, file_hash)) } diff --git a/crates/store-dir/src/index_file.rs b/crates/store-dir/src/index_file.rs index 3fed0b22..1734cb5f 100644 --- a/crates/store-dir/src/index_file.rs +++ b/crates/store-dir/src/index_file.rs @@ -4,7 +4,12 @@ use miette::Diagnostic; use pacquet_fs::{ensure_file, EnsureFileError}; use serde::{Deserialize, Serialize}; use ssri::{Algorithm, Integrity}; -use std::{collections::HashMap, path::PathBuf}; +use std::{ + collections::HashMap, + fs::File, + io::{self, ErrorKind}, + path::{Path, PathBuf}, +}; impl StoreDir { /// Path to an index file of a tarball. @@ -58,6 +63,57 @@ impl StoreDir { } } +/// Error type of [`StoreDir::read_index_file`]. +#[derive(Debug, Display, Error, Diagnostic)] +pub enum ReadIndexFileError { + #[display("Failed to open {file_path:?}: {error}")] + OpenFile { + file_path: PathBuf, + #[error(source)] + error: io::Error, + }, + #[display("Failed to parse content of {file_path:?}: {error}")] + ParseFile { + file_path: PathBuf, + #[error(source)] + error: serde_json::Error, + }, +} + +impl StoreDir { + /// Read an index file from the store directory. + pub fn read_index_file( + &self, + integrity: &Integrity, + ) -> Result { + let file_path = self.index_file_path(integrity); + let file = match File::open(&file_path) { + Ok(file) => file, + Err(error) => return Err(ReadIndexFileError::OpenFile { file_path, error }), + }; + match serde_json::from_reader(file) { + Ok(content) => Ok(content), + Err(error) => Err(ReadIndexFileError::ParseFile { file_path, error }), + } + } +} + +impl ReadIndexFileError { + pub fn file_path(&self) -> &Path { + match self { + ReadIndexFileError::OpenFile { file_path, .. } => file_path, + ReadIndexFileError::ParseFile { file_path, .. } => file_path, + } + } + + pub fn io_error_kind(&self) -> Option { + match self { + ReadIndexFileError::OpenFile { error, .. } => Some(error.kind()), + ReadIndexFileError::ParseFile { error, .. } => error.io_error_kind(), + } + } +} + #[cfg(test)] mod tests { use super::*;