Skip to main content

rightkit_tauri_shell/
startup_cache.rs

1//! Startup cache repair: keep the app's cache directory usable before a plugin needs it.
2//!
3//! Donor: ViewRight `startup_cache.rs`. A dangling symlink or a stray file where a cache directory
4//! belongs makes `create_dir_all` fail with `AlreadyExists`, which aborts plugins that expect the
5//! directory (`tauri-plugin-http` cookie cache). Cache content is disposable, so the bad entry is
6//! replaced with a real directory.
7//!
8//! [`ensure_http_cache_dir`] resolves the platform user cache root (`~/Library/Caches`,
9//! `%LOCALAPPDATA%`, or `$XDG_CACHE_HOME` / `~/.cache`) and repairs `<root>/<identifier>`. It needs
10//! no Tauri handle, so it can run before the Tauri builder starts.
11
12use std::fs;
13use std::io;
14use std::path::{Path, PathBuf};
15
16/// What a cache-directory repair did.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
18#[serde(rename_all = "snake_case")]
19pub enum CacheDirOutcome {
20    /// The directory was already usable.
21    Existing,
22    /// Nothing was there; the directory was created.
23    Created,
24    /// A dangling symlink or a file was replaced with a directory.
25    Repaired,
26}
27
28/// Make `path` a usable directory, replacing a dangling symlink or a regular file.
29pub fn ensure_cache_dir(path: &Path) -> io::Result<CacheDirOutcome> {
30    let mut repaired = false;
31    match fs::symlink_metadata(path) {
32        Ok(m) if m.is_dir() => return Ok(CacheDirOutcome::Existing),
33        Ok(m) if m.file_type().is_symlink() => {
34            if fs::metadata(path).is_ok_and(|t| t.is_dir()) {
35                return Ok(CacheDirOutcome::Existing);
36            }
37            fs::remove_file(path)?;
38            repaired = true;
39        }
40        Ok(_) => {
41            fs::remove_file(path)?;
42            repaired = true;
43        }
44        Err(e) if e.kind() == io::ErrorKind::NotFound => {}
45        Err(e) => return Err(e),
46    }
47    fs::create_dir_all(path)?;
48    Ok(if repaired {
49        CacheDirOutcome::Repaired
50    } else {
51        CacheDirOutcome::Created
52    })
53}
54
55/// The platform user cache root that Tauri and `dirs` agree on.
56pub fn user_cache_root() -> Option<PathBuf> {
57    #[cfg(target_os = "macos")]
58    {
59        std::env::var_os("HOME")
60            .filter(|h| !h.is_empty())
61            .map(|h| PathBuf::from(h).join("Library").join("Caches"))
62    }
63    #[cfg(windows)]
64    {
65        std::env::var_os("LOCALAPPDATA")
66            .filter(|h| !h.is_empty())
67            .map(PathBuf::from)
68    }
69    #[cfg(not(any(target_os = "macos", windows)))]
70    {
71        std::env::var_os("XDG_CACHE_HOME")
72            .map(PathBuf::from)
73            .filter(|p| p.is_absolute())
74            .or_else(|| {
75                std::env::var_os("HOME")
76                    .filter(|h| !h.is_empty())
77                    .map(|h| PathBuf::from(h).join(".cache"))
78            })
79    }
80}
81
82/// Repair `<user cache root>/<identifier>` so a plugin can create its cookie or HTTP cache there.
83/// `identifier` is one path component (usually the bundle identifier): empty values, separators,
84/// `.` and `..` are refused. The root is [`user_cache_root`]; an app whose Tauri resolves the cache
85/// directory differently (for example through `dirs::cache_dir()` or the Tauri path resolver) uses
86/// [`ensure_http_cache_dir_with`].
87pub fn ensure_http_cache_dir(identifier: &str) -> io::Result<CacheDirOutcome> {
88    ensure_http_cache_dir_with(identifier, user_cache_root)
89}
90
91/// [`ensure_http_cache_dir`] with a caller-supplied cache root, so the repaired directory is exactly
92/// the one the app's Tauri resolves:
93///
94/// ```ignore
95/// ensure_http_cache_dir_with(identifier, dirs::cache_dir)?;
96/// // or, once an AppHandle exists: || app.path().cache_dir().ok()
97/// ```
98///
99/// `root` returns `None` when it cannot resolve a cache directory (reported as `NotFound`). The
100/// identifier rules are the same, and are checked before `root` is called.
101pub fn ensure_http_cache_dir_with<F>(identifier: &str, root: F) -> io::Result<CacheDirOutcome>
102where
103    F: FnOnce() -> Option<PathBuf>,
104{
105    let valid = !identifier.is_empty()
106        && identifier != "."
107        && identifier != ".."
108        && !identifier.contains(['/', '\\', '\0']);
109    if !valid {
110        return Err(io::Error::new(
111            io::ErrorKind::InvalidInput,
112            "cache identifier must be one path component",
113        ));
114    }
115    let root = root().ok_or_else(|| {
116        io::Error::new(io::ErrorKind::NotFound, "user cache directory unavailable")
117    })?;
118    ensure_cache_dir(&root.join(identifier))
119}