1#![allow(clippy::disallowed_methods)]
2#![allow(clippy::disallowed_types)] use std::borrow::Cow;
5use std::ffi::OsStr;
6use std::fs::{File, Metadata, OpenOptions};
7use std::os::fd::AsRawFd;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use anyhow::{Context, Result};
13
14pub use self::mapped_file::{MappedFile, MappedFileMut};
15
16mod mapped_file;
17
18const BASE_DIR: &str = "temp";
19
20#[derive(Clone)]
21pub struct TempFileStorage {
22 storage_dir: Dir,
23}
24
25impl TempFileStorage {
26 const MAX_FILE_TTL: Duration = Duration::from_secs(86400); pub fn new(files_dir: &Dir) -> Result<Self> {
29 Ok(Self {
30 storage_dir: files_dir.create_subdir(BASE_DIR)?,
31 })
32 }
33
34 pub async fn remove_outdated_files(&self) -> Result<()> {
35 let now = std::time::SystemTime::now();
36
37 let this = self.clone();
38 tokio::task::spawn_blocking(move || {
39 this.retain_files(|path, metadata| match metadata.modified() {
40 Ok(modified) => {
41 let since_modified = now.duration_since(modified).unwrap_or(Duration::ZERO);
42 since_modified <= Self::MAX_FILE_TTL
43 }
44 Err(e) => {
45 tracing::warn!(
46 path = %path.display(),
47 "failed to check file metadata: {e:?}"
48 );
49 false
50 }
51 })
52 })
53 .await?
54 }
55
56 pub fn retain_files<F>(&self, mut f: F) -> Result<()>
57 where
58 F: FnMut(&Path, &Metadata) -> bool,
59 {
60 let entries = self.storage_dir.entries()?;
61 for e in entries {
62 let e = e?;
63
64 let path = e.path();
65 let Ok(metadata) = std::fs::metadata(&path) else {
66 tracing::warn!(
67 path = %path.display(),
68 "failed to check downloaded file metadata: {e:?}"
69 );
70 continue;
71 };
72
73 let is_file = metadata.is_file();
74 let keep = is_file && f(&path, &metadata);
75 tracing::debug!(keep, path = %path.display(), "found downloaded file");
76
77 if keep {
78 continue;
79 }
80
81 let e = if is_file {
82 std::fs::remove_file(&path)
83 } else {
84 std::fs::remove_dir_all(&path)
85 };
86 if let Err(e) = e {
87 tracing::warn!(path = %path.display(), "failed to remove downloads entry: {e:?}");
88 }
89 }
90
91 Ok(())
92 }
93
94 pub fn file<P: AsRef<Path>>(&self, rel_path: P) -> FileBuilder {
95 self.storage_dir.file(&rel_path)
96 }
97
98 pub fn unnamed_file(&self) -> UnnamedFileBuilder {
99 self.storage_dir.unnamed_file()
100 }
101}
102
103#[derive(Clone)]
104pub struct Dir(Arc<DirInner>);
105
106impl Dir {
107 pub fn new<P>(root: P) -> Result<Self>
110 where
111 P: AsRef<Path>,
112 {
113 std::fs::create_dir_all(root.as_ref())
114 .with_context(|| format!("failed to create {}", root.as_ref().display()))?;
115 Ok(Self(Arc::new(DirInner {
116 base_dir: root.as_ref().to_path_buf(),
117 })))
118 }
119
120 pub fn new_readonly<P>(root: P) -> Self
122 where
123 P: AsRef<Path>,
124 {
125 Self(Arc::new(DirInner {
126 base_dir: root.as_ref().to_path_buf(),
127 }))
128 }
129
130 pub fn path(&self) -> &Path {
131 &self.0.base_dir
132 }
133
134 pub fn create_if_not_exists(&self) -> std::io::Result<()> {
135 std::fs::create_dir_all(&self.0.base_dir)
136 }
137
138 pub fn create_dir_all<P: AsRef<Path>>(&self, rel_path: P) -> std::io::Result<()> {
139 std::fs::create_dir_all(self.0.base_dir.join(rel_path))
140 }
141
142 pub fn remove_file<P: AsRef<Path>>(&self, rel_path: P) -> std::io::Result<()> {
143 std::fs::remove_file(self.0.base_dir.join(rel_path))
144 }
145
146 pub fn file<P: AsRef<Path>>(&self, rel_path: P) -> FileBuilder {
147 FileBuilder {
148 path: self.0.base_dir.join(rel_path.as_ref()),
149 options: std::fs::OpenOptions::new(),
150 prealloc: None,
151 }
152 }
153
154 pub fn unnamed_file(&self) -> UnnamedFileBuilder {
155 UnnamedFileBuilder {
156 base_dir: self.0.base_dir.clone(),
157 prealloc: None,
158 }
159 }
160
161 pub fn subdir_readonly<P: AsRef<Path>>(&self, rel_path: P) -> Self {
165 Self(Arc::new(DirInner {
166 base_dir: self.0.base_dir.join(rel_path),
167 }))
168 }
169
170 pub fn create_subdir<P: AsRef<Path>>(&self, rel_path: P) -> Result<Self> {
173 Self::new(self.0.base_dir.join(rel_path))
174 }
175
176 pub fn file_exists<P: AsRef<Path>>(&self, rel_path: P) -> bool {
177 self.path().join(rel_path).is_file()
178 }
179
180 pub fn entries(&self) -> std::io::Result<std::fs::ReadDir> {
181 std::fs::read_dir(&self.0.base_dir)
182 }
183}
184
185struct DirInner {
186 base_dir: PathBuf,
187}
188
189#[derive(Clone)]
190pub struct FileBuilder {
191 path: PathBuf,
192 options: OpenOptions,
193 prealloc: Option<usize>,
194}
195
196impl FileBuilder {
197 pub fn with_extension<S: AsRef<OsStr>>(&self, extension: S) -> Self {
198 Self {
199 path: self.path.with_extension(extension),
200 options: self.options.clone(),
201 prealloc: self.prealloc,
202 }
203 }
204
205 pub fn open(&self) -> Result<File> {
206 let file = self
207 .options
208 .open(&self.path)
209 .with_context(|| format!("failed to open {}", self.path.display()))?;
210 if let Some(prealloc) = self.prealloc {
211 alloc_file(&file, prealloc)?;
212 }
213 Ok(file)
214 }
215
216 pub fn rename<P: AsRef<Path>>(&self, new_path: P) -> std::io::Result<()> {
217 let new_path = match self.path.parent() {
218 Some(parent) => Cow::Owned(parent.join(new_path)),
219 None => Cow::Borrowed(new_path.as_ref()),
220 };
221 std::fs::rename(&self.path, new_path)
222 }
223
224 pub fn exists(&self) -> bool {
225 std::fs::metadata(&self.path)
226 .ok()
227 .map(|m| m.is_file())
228 .unwrap_or_default()
229 }
230
231 pub fn append(&mut self, append: bool) -> &mut Self {
232 self.options.append(append);
233 self
234 }
235
236 pub fn create(&mut self, create: bool) -> &mut Self {
237 self.options.create(create);
238 self
239 }
240
241 pub fn create_new(&mut self, create_new: bool) -> &mut Self {
242 self.options.create_new(create_new);
243 self
244 }
245
246 pub fn read(&mut self, read: bool) -> &mut Self {
247 self.options.read(read);
248 self
249 }
250
251 pub fn truncate(&mut self, truncate: bool) -> &mut Self {
252 self.options.truncate(truncate);
253 self
254 }
255
256 pub fn write(&mut self, write: bool) -> &mut Self {
257 self.options.write(write);
258 self
259 }
260
261 pub fn prealloc(&mut self, prealloc: usize) -> &mut Self {
262 self.prealloc = Some(prealloc);
263 self
264 }
265
266 pub fn path(&self) -> &Path {
267 &self.path
268 }
269}
270
271pub struct UnnamedFileBuilder {
272 base_dir: PathBuf,
273 prealloc: Option<usize>,
274}
275
276impl UnnamedFileBuilder {
277 pub fn open(self) -> Result<File> {
278 let file = tempfile::tempfile_in(&self.base_dir)?;
279 if let Some(prealloc) = self.prealloc {
280 file.set_len(prealloc as u64)?;
281 }
282
283 Ok(file)
284 }
285
286 pub fn open_as_mapped_mut(&self) -> Result<MappedFileMut> {
287 let file = tempfile::tempfile_in(&self.base_dir).with_context(|| {
288 format!("failed to create a tempfile in {}", self.base_dir.display())
289 })?;
290
291 if let Some(prealloc) = self.prealloc {
292 #[cfg(target_os = "linux")]
293 alloc_file(&file, prealloc)?;
294
295 file.set_len(prealloc as u64)?;
296 } else {
297 anyhow::bail!("prealloc is required for mapping unnamed files");
298 }
299
300 MappedFileMut::from_existing_file(file).map_err(Into::into)
301 }
302
303 pub fn prealloc(&mut self, prealloc: usize) -> &mut Self {
304 self.prealloc = Some(prealloc);
305 self
306 }
307}
308
309#[cfg(not(target_os = "macos"))]
310fn alloc_file(file: &File, len: usize) -> std::io::Result<()> {
311 let res = unsafe { libc::posix_fallocate(file.as_raw_fd(), 0, len as i64) };
312 if res == 0 {
313 Ok(())
314 } else {
315 Err(std::io::Error::last_os_error())
316 }
317}
318
319#[cfg(target_os = "macos")]
320pub fn alloc_file(file: &File, len: usize) -> std::io::Result<()> {
321 let res = unsafe { libc::ftruncate(file.as_raw_fd(), len as i64) };
322 if res < 0 {
323 Err(std::io::Error::last_os_error())
324 } else {
325 Ok(())
326 }
327}