| 181 | /// Also returns if the response is stale and should be revalidated in the background. |
| 182 | #[expect(clippy::arithmetic_side_effects)] |
| 183 | async fn http_cache_get(context: &Context, url: &str) -> Result<Option<(Response, bool)>> { |
| 184 | let now = time(); |
| 185 | let Some((blob_name, mimetype, encoding, stale_timestamp)) = context |
| 186 | .sql |
| 187 | .query_row_optional( |
| 188 | "SELECT blobname, mimetype, encoding, stale |
| 189 | FROM http_cache WHERE url=? AND expires > ?", |
| 190 | (url, now), |
| 191 | |row| { |
| 192 | let blob_name: String = row.get(0)?; |
| 193 | let mimetype: Option<String> = Some(row.get(1)?).filter(|s: &String| !s.is_empty()); |
| 194 | let encoding: Option<String> = Some(row.get(2)?).filter(|s: &String| !s.is_empty()); |
| 195 | let stale_timestamp: i64 = row.get(3)?; |
| 196 | Ok((blob_name, mimetype, encoding, stale_timestamp)) |
| 197 | }, |
| 198 | ) |
| 199 | .await? |
| 200 | else { |
| 201 | return Ok(None); |
| 202 | }; |
| 203 | let is_stale = now > stale_timestamp; |
| 204 | |
| 205 | let blob_object = BlobObject::from_name(context, &blob_name)?; |
| 206 | let blob_abs_path = blob_object.to_abs_path(); |
| 207 | let blob = match fs::read(blob_abs_path) |
| 208 | .await |
| 209 | .with_context(|| format!("Failed to read blob for {url:?} cache entry.")) |
| 210 | { |
| 211 | Ok(blob) => blob, |
| 212 | Err(err) => { |
| 213 | // This should not happen, but user may go into the blobdir and remove files, |
| 214 | // antivirus may delete the file or there may be a bug in housekeeping. |
| 215 | warn!(context, "{err:?}."); |
| 216 | return Ok(None); |
| 217 | } |
| 218 | }; |
| 219 | |
| 220 | let (expires, _stale) = http_url_cache_timestamps(url, mimetype.as_deref()); |
| 221 | let response = Response { |
| 222 | blob, |
| 223 | mimetype, |
| 224 | encoding, |
| 225 | }; |
| 226 | |
| 227 | // Update expiration timestamp |
| 228 | // to prevent deletion of the file still in use. |
| 229 | // |
| 230 | // If the response is stale, the caller should revalidate it in the background, so update |
| 231 | // `stale` timestamp to avoid revalidating too frequently (and have many parallel revalidation |
| 232 | // tasks) if revalidation fails or the HTTP request takes some time. The stale period >= 1 hour, |
| 233 | // so 1 more minute won't be a problem. |
| 234 | let stale_timestamp = if is_stale { now + 60 } else { stale_timestamp }; |
| 235 | context |
| 236 | .sql |
| 237 | .execute( |
| 238 | "UPDATE http_cache SET expires=?, stale=? WHERE url=?", |
| 239 | (expires, stale_timestamp, url), |
| 240 | ) |