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}