1use std::collections::HashSet;
4use std::io::ErrorKind;
5use std::path::Path;
6use std::path::PathBuf;
7use std::sync::Arc;
8
9use deno_cache_dir::file_fetcher::CacheSetting;
10use deno_cache_dir::npm::NpmCacheDir;
11use deno_error::JsErrorBox;
12use deno_npm::NpmPackageCacheFolderId;
13use deno_npmrc::RegistryConfig;
14use deno_npmrc::ResolvedNpmRc;
15use deno_path_util::fs::atomic_write_file_with_retries;
16use deno_semver::StackString;
17use deno_semver::Version;
18use deno_semver::package::PackageNv;
19use parking_lot::Mutex;
20use sys_traits::FsCanonicalize;
21use sys_traits::FsCreateDirAll;
22use sys_traits::FsHardLink;
23use sys_traits::FsMetadata;
24use sys_traits::FsOpen;
25use sys_traits::FsRead;
26use sys_traits::FsReadDir;
27use sys_traits::FsRemoveDirAll;
28use sys_traits::FsRemoveFile;
29use sys_traits::FsRename;
30use sys_traits::SystemRandom;
31use sys_traits::ThreadSleep;
32use url::Url;
33
34mod fs_util;
35mod registry_info;
36mod remote;
37mod rt;
38mod tarball;
39mod tarball_extract;
40
41pub use fs_util::hard_link_dir_recursive;
42pub use fs_util::hard_link_file;
43pub use fs_util::is_etxtbsy;
44pub use registry_info::RegistryInfoProvider;
45pub use registry_info::SerializedCachedPackageInfo;
46pub use registry_info::get_package_url;
47pub use remote::maybe_auth_header_value_for_npm_registry;
48pub use tarball::EnsurePackageError;
49pub use tarball::TarballCache;
50pub use tarball::TarballCacheReporter;
51
52use self::rt::spawn_blocking;
53
54#[derive(Debug, deno_error::JsError)]
55#[class(generic)]
56pub struct DownloadError {
57 pub status_code: Option<u16>,
58 pub error: JsErrorBox,
59}
60
61impl std::error::Error for DownloadError {
62 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
63 self.error.source()
64 }
65}
66
67impl std::fmt::Display for DownloadError {
68 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
69 self.error.fmt(f)
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum NpmPackumentFormat {
75 Abbreviated,
77 Full,
79}
80
81pub enum NpmCacheHttpClientResponse {
82 NotFound,
83 NotModified,
84 Bytes(NpmCacheHttpClientBytesResponse),
85}
86
87pub struct NpmCacheHttpClientBytesResponse {
88 pub bytes: Vec<u8>,
89 pub etag: Option<String>,
90}
91
92#[async_trait::async_trait(?Send)]
93pub trait NpmCacheHttpClient: std::fmt::Debug + Send + Sync + 'static {
94 async fn download_with_retries_on_any_tokio_runtime(
95 &self,
96 url: Url,
97 maybe_auth: Option<String>,
98 maybe_etag: Option<String>,
99 maybe_registry_config: Option<&RegistryConfig>,
100 ) -> Result<NpmCacheHttpClientResponse, DownloadError>;
101}
102
103#[derive(Debug, Clone, Eq, PartialEq)]
105pub enum NpmCacheSetting {
106 Only,
109 ReloadAll,
112 ReloadSome { npm_package_names: Vec<String> },
115 Use,
118}
119
120impl NpmCacheSetting {
121 pub fn from_cache_setting(cache_setting: &CacheSetting) -> NpmCacheSetting {
122 match cache_setting {
123 CacheSetting::Only => NpmCacheSetting::Only,
124 CacheSetting::ReloadAll => NpmCacheSetting::ReloadAll,
125 CacheSetting::ReloadSome(values) => {
126 if values.iter().any(|v| v == "npm:") {
127 NpmCacheSetting::ReloadAll
128 } else {
129 NpmCacheSetting::ReloadSome {
130 npm_package_names: values
131 .iter()
132 .filter_map(|v| v.strip_prefix("npm:"))
133 .map(|n| n.to_string())
134 .collect(),
135 }
136 }
137 }
138 CacheSetting::RespectHeaders => panic!("not supported"),
139 CacheSetting::Use => NpmCacheSetting::Use,
140 }
141 }
142 pub fn should_use_for_npm_package(&self, package_name: &str) -> bool {
143 match self {
144 NpmCacheSetting::ReloadAll => false,
145 NpmCacheSetting::ReloadSome { npm_package_names } => {
146 !npm_package_names.iter().any(|n| n == package_name)
147 }
148 _ => true,
149 }
150 }
151}
152
153#[sys_traits::auto_impl]
154pub trait NpmCacheSys:
155 FsCanonicalize
156 + FsCreateDirAll
157 + FsHardLink
158 + FsMetadata
159 + FsOpen
160 + FsRead
161 + FsReadDir
162 + FsRemoveDirAll
163 + FsRemoveFile
164 + FsRename
165 + ThreadSleep
166 + SystemRandom
167 + Send
168 + Sync
169 + Clone
170 + std::fmt::Debug
171 + 'static
172{
173}
174
175#[derive(Debug)]
177pub struct NpmCache<TSys: NpmCacheSys> {
178 cache_dir: Arc<NpmCacheDir>,
179 sys: TSys,
180 cache_setting: NpmCacheSetting,
181 npmrc: Arc<ResolvedNpmRc>,
182 previously_reloaded_packages: Mutex<HashSet<PackageNv>>,
183}
184
185impl<TSys: NpmCacheSys> NpmCache<TSys> {
186 pub fn new(
187 cache_dir: Arc<NpmCacheDir>,
188 sys: TSys,
189 cache_setting: NpmCacheSetting,
190 npmrc: Arc<ResolvedNpmRc>,
191 ) -> Self {
192 Self {
193 cache_dir,
194 sys,
195 cache_setting,
196 npmrc,
197 previously_reloaded_packages: Default::default(),
198 }
199 }
200
201 pub fn cache_setting(&self) -> &NpmCacheSetting {
202 &self.cache_setting
203 }
204
205 pub fn root_dir_path(&self) -> &Path {
206 self.cache_dir.root_dir()
207 }
208
209 pub fn root_dir_url(&self) -> &Url {
210 self.cache_dir.root_dir_url()
211 }
212
213 pub fn should_use_cache_for_package(&self, package: &PackageNv) -> bool {
219 self.cache_setting.should_use_for_npm_package(&package.name)
220 || !self
221 .previously_reloaded_packages
222 .lock()
223 .insert(package.clone())
224 }
225
226 pub fn ensure_copy_package(
231 &self,
232 folder_id: &NpmPackageCacheFolderId,
233 ) -> Result<(), WithFolderSyncLockError> {
234 let registry_url = self.npmrc.get_registry_url(&folder_id.nv.name);
235 assert_ne!(folder_id.copy_index, 0);
236 let package_folder = self.cache_dir.package_folder_for_id(
237 &folder_id.nv.name,
238 &folder_id.nv.version.to_string(),
239 folder_id.copy_index,
240 registry_url,
241 );
242
243 if self.sys.fs_exists_no_err(&package_folder)
244 && !self.sys.fs_exists_no_err(package_folder.join(NPM_PACKAGE_SYNC_LOCK_FILENAME))
247 && self.cache_setting.should_use_for_npm_package(&folder_id.nv.name)
248 {
249 return Ok(());
250 }
251
252 let original_package_folder = self.cache_dir.package_folder_for_id(
253 &folder_id.nv.name,
254 &folder_id.nv.version.to_string(),
255 0, registry_url,
257 );
258
259 with_folder_sync_lock(&self.sys, &folder_id.nv, &package_folder, || {
262 hard_link_dir_recursive(
263 &self.sys,
264 &original_package_folder,
265 &package_folder,
266 )
267 .map_err(JsErrorBox::from_err)
268 })?;
269 Ok(())
270 }
271
272 pub fn package_folder_for_id(&self, id: &NpmPackageCacheFolderId) -> PathBuf {
273 let registry_url = self.npmrc.get_registry_url(&id.nv.name);
274 self.cache_dir.package_folder_for_id(
275 &id.nv.name,
276 &id.nv.version.to_string(),
277 id.copy_index,
278 registry_url,
279 )
280 }
281
282 pub fn package_folder_for_nv(&self, package: &PackageNv) -> PathBuf {
283 let registry_url = self.npmrc.get_registry_url(&package.name);
284 self.package_folder_for_nv_and_url(package, registry_url)
285 }
286
287 pub fn package_folder_for_nv_and_url(
288 &self,
289 package: &PackageNv,
290 registry_url: &Url,
291 ) -> PathBuf {
292 self.cache_dir.package_folder_for_id(
293 &package.name,
294 &package.version.to_string(),
295 0, registry_url,
297 )
298 }
299
300 pub fn package_name_folder(&self, name: &str) -> PathBuf {
301 let registry_url = self.npmrc.get_registry_url(name);
302 self.cache_dir.package_name_folder(name, registry_url)
303 }
304
305 pub fn resolve_package_folder_id_from_specifier(
306 &self,
307 specifier: &Url,
308 ) -> Option<NpmPackageCacheFolderId> {
309 self
310 .cache_dir
311 .resolve_package_folder_id_from_specifier(specifier)
312 .and_then(|cache_id| {
313 Some(NpmPackageCacheFolderId {
314 nv: PackageNv {
315 name: StackString::from_string(cache_id.name),
316 version: Version::parse_from_npm(&cache_id.version).ok()?,
317 },
318 copy_index: cache_id.copy_index,
319 })
320 })
321 }
322
323 pub async fn load_package_info(
324 &self,
325 name: &str,
326 ) -> Result<Option<SerializedCachedPackageInfo>, serde_json::Error> {
327 let file_cache_path = self.get_registry_package_info_file_cache_path(name);
328
329 let file_bytes = match self.sys.fs_read(&file_cache_path) {
330 Ok(file_text) => file_text,
331 Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None),
332 Err(err) => return Err(serde_json::Error::io(err)),
333 };
334
335 spawn_blocking(move || serde_json::from_slice(&file_bytes))
336 .await
337 .unwrap()
338 }
339
340 pub fn save_package_info(
341 &self,
342 name: &str,
343 package_info: &SerializedCachedPackageInfo,
344 ) -> Result<(), JsErrorBox> {
345 let file_cache_path = self.get_registry_package_info_file_cache_path(name);
346 let file_text =
347 serde_json::to_string(&package_info).map_err(JsErrorBox::from_err)?;
348 atomic_write_file_with_retries(
349 &self.sys,
350 &file_cache_path,
351 file_text.as_bytes(),
352 0o644,
353 )
354 .map_err(JsErrorBox::from_err)?;
355 Ok(())
356 }
357
358 fn get_registry_package_info_file_cache_path(&self, name: &str) -> PathBuf {
359 let name_folder_path = self.package_name_folder(name);
360 name_folder_path.join("registry.json")
361 }
362}
363
364const NPM_PACKAGE_SYNC_LOCK_FILENAME: &str = ".deno_sync_lock";
365
366#[derive(Debug, thiserror::Error, deno_error::JsError)]
367pub enum WithFolderSyncLockError {
368 #[class(inherit)]
369 #[error("Error creating '{path}'")]
370 CreateDir {
371 path: PathBuf,
372 #[source]
373 #[inherit]
374 source: std::io::Error,
375 },
376 #[class(inherit)]
377 #[error(
378 "Error creating package sync lock file at '{path}'. Maybe try manually deleting this folder."
379 )]
380 CreateLockFile {
381 path: PathBuf,
382 #[source]
383 #[inherit]
384 source: std::io::Error,
385 },
386 #[class(inherit)]
387 #[error(transparent)]
388 Action(#[from] JsErrorBox),
389 #[class(generic)]
390 #[error(
391 "Failed setting up package cache directory for {package}, then failed cleaning it up.\n\nOriginal error:\n\n{error}\n\nRemove error:\n\n{remove_error}\n\nPlease manually delete this folder or you will run into issues using this package in the future:\n\n{output_folder}"
392 )]
393 SetUpPackageCacheDir {
394 package: Box<PackageNv>,
395 error: Box<WithFolderSyncLockError>,
396 remove_error: std::io::Error,
397 output_folder: PathBuf,
398 },
399}
400
401fn with_folder_sync_lock(
402 sys: &(impl FsCreateDirAll + FsOpen + FsRemoveDirAll + FsRemoveFile),
403 package: &PackageNv,
404 output_folder: &Path,
405 action: impl FnOnce() -> Result<(), JsErrorBox>,
406) -> Result<(), WithFolderSyncLockError> {
407 fn inner(
408 sys: &(impl FsCreateDirAll + FsOpen + FsRemoveFile),
409 output_folder: &Path,
410 action: impl FnOnce() -> Result<(), JsErrorBox>,
411 ) -> Result<(), WithFolderSyncLockError> {
412 sys.fs_create_dir_all(output_folder).map_err(|source| {
413 WithFolderSyncLockError::CreateDir {
414 path: output_folder.to_path_buf(),
415 source,
416 }
417 })?;
418
419 let sync_lock_path = output_folder.join(NPM_PACKAGE_SYNC_LOCK_FILENAME);
428 let mut open_options = sys_traits::OpenOptions::new();
429 open_options.write = true;
430 open_options.create = true;
431 open_options.truncate = false;
432 match sys.fs_open(&sync_lock_path, &open_options) {
433 Ok(_) => {
434 action()?;
435 let _ignore = sys.fs_remove_file(&sync_lock_path);
437 Ok(())
438 }
439 Err(err) => Err(WithFolderSyncLockError::CreateLockFile {
440 path: output_folder.to_path_buf(),
441 source: err,
442 }),
443 }
444 }
445
446 match inner(sys, output_folder, action) {
447 Ok(()) => Ok(()),
448 Err(err) => {
449 if let Err(remove_err) = sys.fs_remove_dir_all(output_folder)
450 && remove_err.kind() != std::io::ErrorKind::NotFound
451 {
452 return Err(WithFolderSyncLockError::SetUpPackageCacheDir {
453 package: Box::new(package.clone()),
454 error: Box::new(err),
455 remove_error: remove_err,
456 output_folder: output_folder.to_path_buf(),
457 });
458 }
459 Err(err)
460 }
461 }
462}