1use std::io;
12use std::path::{Path, PathBuf};
13
14use crate::error::MountError;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum CacheConvention {
19 Xdg,
21 MacOs,
23}
24
25impl CacheConvention {
26 pub fn current() -> Self {
28 if cfg!(target_os = "macos") {
29 Self::MacOs
30 } else {
31 Self::Xdg
32 }
33 }
34}
35
36pub fn resolve_spool_dir(
45 override_dir: Option<&Path>,
46 xdg_cache_home: Option<&str>,
47 home: Option<&str>,
48 convention: CacheConvention,
49) -> Result<PathBuf, MountError> {
50 if let Some(dir) = override_dir {
51 return Ok(dir.to_path_buf());
52 }
53
54 fn non_empty(v: Option<&str>) -> Option<&str> {
56 v.filter(|s| !s.is_empty())
57 }
58 let home = non_empty(home);
59
60 let base = match convention {
61 CacheConvention::MacOs => home
62 .map(|h| Path::new(h).join("Library").join("Caches"))
63 .ok_or_else(|| {
64 MountError::Other(
65 "can't find a cache directory: $HOME is not set. \
66 Pass --spool-dir to say where writes should be spooled."
67 .into(),
68 )
69 })?,
70 CacheConvention::Xdg => match non_empty(xdg_cache_home) {
71 Some(xdg) => PathBuf::from(xdg),
72 None => home.map(|h| Path::new(h).join(".cache")).ok_or_else(|| {
73 MountError::Other(
74 "can't find a cache directory: neither $XDG_CACHE_HOME nor $HOME is set. \
75 Pass --spool-dir to say where writes should be spooled."
76 .into(),
77 )
78 })?,
79 },
80 };
81
82 Ok(base.join("mtp-mount").join("spool"))
83}
84
85pub fn spool_dir_from_env(override_dir: Option<&Path>) -> Result<PathBuf, MountError> {
87 let xdg = std::env::var("XDG_CACHE_HOME").ok();
88 let home = std::env::var("HOME").ok();
89 resolve_spool_dir(
90 override_dir,
91 xdg.as_deref(),
92 home.as_deref(),
93 CacheConvention::current(),
94 )
95}
96
97pub fn prepare_spool_dir(dir: &Path) -> Result<(), MountError> {
102 std::fs::create_dir_all(dir).map_err(|e| spool_error(dir, e))?;
103 tempfile::tempfile_in(dir).map_err(|e| spool_error(dir, e))?;
106 Ok(())
107}
108
109fn spool_error(dir: &Path, source: io::Error) -> MountError {
110 MountError::Other(format!(
111 "can't use the spool directory {}: {source}. \
112 Pass --spool-dir to point it at a writable directory on disk.",
113 dir.display()
114 ))
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn xdg_cache_home_wins_when_set() {
123 let dir = resolve_spool_dir(
124 None,
125 Some("/cache"),
126 Some("/home/dave"),
127 CacheConvention::Xdg,
128 )
129 .unwrap();
130 assert_eq!(dir, PathBuf::from("/cache/mtp-mount/spool"));
131 }
132
133 #[test]
134 fn xdg_unset_falls_back_to_home_cache() {
135 let dir = resolve_spool_dir(None, None, Some("/home/dave"), CacheConvention::Xdg).unwrap();
136 assert_eq!(dir, PathBuf::from("/home/dave/.cache/mtp-mount/spool"));
137 }
138
139 #[test]
140 fn empty_xdg_counts_as_unset() {
141 let dir =
142 resolve_spool_dir(None, Some(""), Some("/home/dave"), CacheConvention::Xdg).unwrap();
143 assert_eq!(dir, PathBuf::from("/home/dave/.cache/mtp-mount/spool"));
144 }
145
146 #[test]
147 fn override_wins_over_env() {
148 let dir = resolve_spool_dir(
149 Some(Path::new("/mnt/scratch")),
150 Some("/cache"),
151 Some("/home/dave"),
152 CacheConvention::Xdg,
153 )
154 .unwrap();
155 assert_eq!(dir, PathBuf::from("/mnt/scratch"));
156 }
157
158 #[test]
159 fn macos_uses_library_caches_and_ignores_xdg() {
160 let dir = resolve_spool_dir(
161 None,
162 Some("/cache"),
163 Some("/Users/dave"),
164 CacheConvention::MacOs,
165 )
166 .unwrap();
167 assert_eq!(
168 dir,
169 PathBuf::from("/Users/dave/Library/Caches/mtp-mount/spool")
170 );
171 }
172
173 #[test]
174 fn no_home_and_no_xdg_errors() {
175 let err = resolve_spool_dir(None, None, None, CacheConvention::Xdg).unwrap_err();
176 assert!(err.to_string().contains("--spool-dir"));
177
178 let err =
179 resolve_spool_dir(None, Some("/cache"), None, CacheConvention::MacOs).unwrap_err();
180 assert!(err.to_string().contains("--spool-dir"));
181 }
182
183 #[test]
184 fn prepare_creates_missing_directory() {
185 let parent = tempfile::tempdir().unwrap();
186 let dir = parent.path().join("mtp-mount").join("spool");
187 prepare_spool_dir(&dir).unwrap();
188 assert!(dir.is_dir());
189 }
190
191 #[test]
192 fn prepare_reports_the_path_it_could_not_use() {
193 let parent = tempfile::tempdir().unwrap();
195 let blocked = parent.path().join("not-a-dir");
196 std::fs::write(&blocked, b"x").unwrap();
197 let err = prepare_spool_dir(&blocked).unwrap_err().to_string();
198 assert!(err.contains(&blocked.display().to_string()), "{err}");
199 }
200}