Skip to main content

deno_cache_dir/global/
mod.rs

1// Copyright 2018-2026 the Deno authors. MIT license.
2
3use std::path::PathBuf;
4use std::time::Duration;
5use std::time::SystemTime;
6use std::time::UNIX_EPOCH;
7
8use deno_maybe_sync::MaybeSend;
9use deno_maybe_sync::MaybeSync;
10use serde::Deserialize;
11use sys_traits::FsCreateDirAll;
12use sys_traits::FsMetadata;
13use sys_traits::FsMetadataValue;
14use sys_traits::FsOpen;
15use sys_traits::FsRead;
16use sys_traits::FsRemoveFile;
17use sys_traits::FsRename;
18use sys_traits::SystemRandom;
19use sys_traits::SystemTimeNow;
20use sys_traits::ThreadSleep;
21use url::Url;
22
23use super::cache::HttpCache;
24use super::cache::HttpCacheItemKey;
25use crate::cache::CacheEntry;
26use crate::cache::CacheReadFileError;
27use crate::cache::Checksum;
28use crate::cache::SerializedCachedUrlMetadata;
29use crate::cache::url_to_filename;
30use crate::common::HeadersMap;
31
32mod cache_file;
33
34#[sys_traits::auto_impl]
35pub trait GlobalHttpCacheSys:
36  FsCreateDirAll
37  + FsMetadata
38  + FsOpen
39  + FsRead
40  + FsRemoveFile
41  + FsRename
42  + ThreadSleep
43  + SystemRandom
44  + SystemTimeNow
45  + std::fmt::Debug
46  + MaybeSend
47  + MaybeSync
48  + Clone
49{
50}
51
52#[allow(clippy::disallowed_types, reason = "arc wrapper type")]
53pub type GlobalHttpCacheRc<TSys> =
54  deno_maybe_sync::MaybeArc<GlobalHttpCache<TSys>>;
55
56#[derive(Debug)]
57pub struct GlobalHttpCache<Sys: GlobalHttpCacheSys> {
58  path: PathBuf,
59  pub(crate) sys: Sys,
60}
61
62impl<Sys: GlobalHttpCacheSys> GlobalHttpCache<Sys> {
63  pub fn new(sys: Sys, path: PathBuf) -> Self {
64    #[cfg(not(target_arch = "wasm32"))]
65    assert!(path.is_absolute());
66    Self { path, sys }
67  }
68
69  pub fn dir_path(&self) -> &PathBuf {
70    &self.path
71  }
72
73  pub fn local_path_for_url(&self, url: &Url) -> std::io::Result<PathBuf> {
74    Ok(self.path.join(url_to_filename(url)?))
75  }
76
77  #[inline]
78  fn key_file_path<'a>(&self, key: &'a HttpCacheItemKey) -> &'a PathBuf {
79    // The key file path is always set for the global cache because
80    // the file will always exist, unlike the local cache, which won't
81    // have this for redirects.
82    key.file_path.as_ref().unwrap()
83  }
84}
85
86impl<TSys: GlobalHttpCacheSys> HttpCache for GlobalHttpCache<TSys> {
87  fn cache_item_key<'a>(
88    &self,
89    url: &'a Url,
90  ) -> std::io::Result<HttpCacheItemKey<'a>> {
91    Ok(HttpCacheItemKey {
92      #[cfg(debug_assertions)]
93      is_local_key: false,
94      url,
95      file_path: Some(self.local_path_for_url(url)?),
96    })
97  }
98
99  fn contains(&self, url: &Url) -> bool {
100    let Ok(cache_filepath) = self.local_path_for_url(url) else {
101      return false;
102    };
103    self.sys.fs_is_file(&cache_filepath).unwrap_or(false)
104  }
105
106  fn read_modified_time(
107    &self,
108    key: &HttpCacheItemKey,
109  ) -> std::io::Result<Option<SystemTime>> {
110    #[cfg(debug_assertions)]
111    debug_assert!(!key.is_local_key);
112
113    match self.sys.fs_metadata(self.key_file_path(key)) {
114      Ok(metadata) => match metadata.modified() {
115        Ok(time) => Ok(Some(time)),
116        Err(_) => Ok(Some(self.sys.sys_time_now())),
117      },
118      Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
119      Err(err) => Err(err),
120    }
121  }
122
123  fn set(
124    &self,
125    url: &Url,
126    headers: HeadersMap,
127    content: &[u8],
128  ) -> std::io::Result<()> {
129    let cache_filepath = self.local_path_for_url(url)?;
130    cache_file::write(
131      &self.sys,
132      &cache_filepath,
133      content,
134      &SerializedCachedUrlMetadata {
135        time: Some(
136          self
137            .sys
138            .sys_time_now()
139            .duration_since(UNIX_EPOCH)
140            .unwrap()
141            .as_secs(),
142        ),
143        url: url.to_string(),
144        headers,
145      },
146    )?;
147
148    Ok(())
149  }
150
151  fn get(
152    &self,
153    key: &HttpCacheItemKey,
154    maybe_checksum: Option<Checksum>,
155  ) -> Result<Option<CacheEntry>, CacheReadFileError> {
156    #[cfg(debug_assertions)]
157    debug_assert!(!key.is_local_key);
158
159    let maybe_file = cache_file::read(&self.sys, self.key_file_path(key))?;
160
161    if let Some(file) = &maybe_file
162      && let Some(expected_checksum) = maybe_checksum
163    {
164      expected_checksum
165        .check(key.url, &file.content)
166        .map_err(CacheReadFileError::ChecksumIntegrity)?;
167    }
168
169    Ok(maybe_file)
170  }
171
172  fn read_headers(
173    &self,
174    key: &HttpCacheItemKey,
175  ) -> std::io::Result<Option<HeadersMap>> {
176    // targeted deserialize
177    #[derive(Deserialize)]
178    struct SerializedHeaders {
179      pub headers: HeadersMap,
180    }
181
182    #[cfg(debug_assertions)]
183    debug_assert!(!key.is_local_key);
184
185    let maybe_metadata = cache_file::read_metadata::<SerializedHeaders>(
186      &self.sys,
187      self.key_file_path(key),
188    )?;
189    Ok(maybe_metadata.map(|m| m.headers))
190  }
191
192  fn read_download_time(
193    &self,
194    key: &HttpCacheItemKey,
195  ) -> std::io::Result<Option<SystemTime>> {
196    // targeted deserialize
197    #[derive(Deserialize)]
198    struct SerializedTime {
199      pub time: Option<u64>,
200    }
201
202    #[cfg(debug_assertions)]
203    debug_assert!(!key.is_local_key);
204    let maybe_metadata = cache_file::read_metadata::<SerializedTime>(
205      &self.sys,
206      self.key_file_path(key),
207    )?;
208    Ok(maybe_metadata.and_then(|m| {
209      Some(SystemTime::UNIX_EPOCH + Duration::from_secs(m.time?))
210    }))
211  }
212}
213
214#[cfg(test)]
215mod test {
216  use super::*;
217
218  #[test]
219  fn test_url_to_filename() {
220    let test_cases = [
221      (
222        "https://deno.land/x/foo.ts",
223        "https/deno.land/2c0a064891b9e3fbe386f5d4a833bce5076543f5404613656042107213a7bbc8",
224      ),
225      (
226        "https://deno.land:8080/x/foo.ts",
227        "https/deno.land_PORT8080/2c0a064891b9e3fbe386f5d4a833bce5076543f5404613656042107213a7bbc8",
228      ),
229      (
230        "https://deno.land/",
231        "https/deno.land/8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1",
232      ),
233      (
234        "https://deno.land/?asdf=qwer",
235        "https/deno.land/e4edd1f433165141015db6a823094e6bd8f24dd16fe33f2abd99d34a0a21a3c0",
236      ),
237      // should be the same as case above, fragment (#qwer) is ignored
238      // when hashing
239      (
240        "https://deno.land/?asdf=qwer#qwer",
241        "https/deno.land/e4edd1f433165141015db6a823094e6bd8f24dd16fe33f2abd99d34a0a21a3c0",
242      ),
243      (
244        "data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=",
245        "data/c21c7fc382b2b0553dc0864aa81a3acacfb7b3d1285ab5ae76da6abec213fb37",
246      ),
247      (
248        "data:text/plain,Hello%2C%20Deno!",
249        "data/967374e3561d6741234131e342bf5c6848b70b13758adfe23ee1a813a8131818",
250      ),
251    ];
252
253    for (url, expected) in test_cases.iter() {
254      let u = Url::parse(url).unwrap();
255      let p = url_to_filename(&u).unwrap();
256      assert_eq!(p, PathBuf::from(expected));
257    }
258  }
259}