1use std::path::{Path, PathBuf};
10
11use bytes::Bytes;
12use uuid::Uuid;
13
14use crate::error::Error;
15
16const RENDERS_SUBDIR: &str = "renders";
18
19const LOGOS_SUBDIR: &str = "logos";
21
22pub const IMAGE_SUFFIX: &str = "image";
24
25pub const IMAGE_WITHOUT_ROUTE_SUFFIX: &str = "image-without-route";
27
28#[must_use]
32pub fn ext_for_content_type(content_type: &str) -> &'static str {
33 match content_type {
34 "image/png" => "png",
35 "image/jpeg" => "jpg",
36 "image/webp" => "webp",
37 _ => "bin",
38 }
39}
40
41#[must_use]
44pub fn render_filename(render_id: Uuid, suffix: &str, ext: &str) -> String {
45 format!("{render_id}-{suffix}.{ext}")
46}
47
48#[must_use]
50pub fn render_path(storage_dir: &Path, filename: &str) -> PathBuf {
51 storage_dir.join(RENDERS_SUBDIR).join(filename)
52}
53
54pub fn ensure_layout(storage_dir: &Path) -> Result<(), Error> {
60 fs_err::create_dir_all(storage_dir.join(RENDERS_SUBDIR))?;
61 fs_err::create_dir_all(storage_dir.join(LOGOS_SUBDIR))?;
62 Ok(())
63}
64
65pub async fn write_render_file(
72 storage_dir: &Path,
73 render_id: Uuid,
74 suffix: &str,
75 ext: &str,
76 bytes: Bytes,
77) -> Result<String, Error> {
78 let filename = render_filename(render_id, suffix, ext);
79 let path = render_path(storage_dir, &filename);
80 tokio::task::spawn_blocking(move || fs_err::write(&path, &bytes))
81 .await
82 .map_err(|err| Error::Io(std::io::Error::other(err)))??;
83 Ok(filename)
84}
85
86pub async fn read_render_file(storage_dir: &Path, filename: &str) -> Result<Bytes, Error> {
93 let path = render_path(storage_dir, filename);
94 let filename_for_err = filename.to_owned();
95 let bytes = tokio::task::spawn_blocking(move || fs_err::read(&path))
96 .await
97 .map_err(|err| Error::Io(std::io::Error::other(err)))?;
98 match bytes {
99 Ok(b) => Ok(Bytes::from(b)),
100 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
101 Err(Error::NotFound(format!("render file `{filename_for_err}`")))
102 }
103 Err(err) => Err(Error::Io(err)),
104 }
105}
106
107pub fn try_delete_render_file(storage_dir: &Path, filename: &str) -> Result<(), Error> {
116 let path = render_path(storage_dir, filename);
117 match fs_err::remove_file(&path) {
118 Ok(()) => Ok(()),
119 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
120 Err(err) => Err(Error::Io(err)),
121 }
122}
123
124pub fn list_render_files(storage_dir: &Path) -> Result<Vec<String>, Error> {
131 let dir = storage_dir.join(RENDERS_SUBDIR);
132 let mut out = Vec::new();
133 let entries = fs_err::read_dir(&dir)?;
134 for entry in entries {
135 let entry = entry?;
136 if !entry.file_type()?.is_file() {
137 continue;
138 }
139 if let Some(name) = entry.file_name().to_str() {
140 out.push(name.to_owned());
141 }
142 }
143 Ok(out)
144}
145
146#[must_use]
152pub fn parse_render_id_from_filename(filename: &str) -> Option<Uuid> {
153 const UUID_LEN: usize = 36;
155 let prefix: String = filename.chars().take(UUID_LEN).collect();
156 if prefix.chars().count() != UUID_LEN {
157 return None;
158 }
159 Uuid::parse_str(&prefix).ok()
160}
161
162#[must_use]
173pub fn logo_filename(logo_id: Uuid, ext: &str) -> String {
174 format!("{logo_id}.{ext}")
175}
176
177#[must_use]
179pub fn logo_path(storage_dir: &Path, filename: &str) -> PathBuf {
180 storage_dir.join(LOGOS_SUBDIR).join(filename)
181}
182
183pub async fn write_logo_file(
190 storage_dir: &Path,
191 logo_id: Uuid,
192 ext: &str,
193 bytes: Bytes,
194) -> Result<String, Error> {
195 let filename = logo_filename(logo_id, ext);
196 let path = logo_path(storage_dir, &filename);
197 tokio::task::spawn_blocking(move || fs_err::write(&path, &bytes))
198 .await
199 .map_err(|err| Error::Io(std::io::Error::other(err)))??;
200 Ok(filename)
201}
202
203pub async fn read_logo_file(storage_dir: &Path, filename: &str) -> Result<Bytes, Error> {
210 let path = logo_path(storage_dir, filename);
211 let filename_for_err = filename.to_owned();
212 let bytes = tokio::task::spawn_blocking(move || fs_err::read(&path))
213 .await
214 .map_err(|err| Error::Io(std::io::Error::other(err)))?;
215 match bytes {
216 Ok(b) => Ok(Bytes::from(b)),
217 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
218 Err(Error::NotFound(format!("logo file `{filename_for_err}`")))
219 }
220 Err(err) => Err(Error::Io(err)),
221 }
222}
223
224pub fn try_delete_logo_file(storage_dir: &Path, filename: &str) -> Result<(), Error> {
231 let path = logo_path(storage_dir, filename);
232 match fs_err::remove_file(&path) {
233 Ok(()) => Ok(()),
234 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
235 Err(err) => Err(Error::Io(err)),
236 }
237}
238
239pub fn list_logo_files(storage_dir: &Path) -> Result<Vec<String>, Error> {
246 let dir = storage_dir.join(LOGOS_SUBDIR);
247 let mut out = Vec::new();
248 let entries = fs_err::read_dir(&dir)?;
249 for entry in entries {
250 let entry = entry?;
251 if !entry.file_type()?.is_file() {
252 continue;
253 }
254 if let Some(name) = entry.file_name().to_str() {
255 out.push(name.to_owned());
256 }
257 }
258 Ok(out)
259}
260
261#[must_use]
265pub fn parse_logo_id_from_filename(filename: &str) -> Option<Uuid> {
266 const UUID_LEN: usize = 36;
268 let prefix: String = filename.chars().take(UUID_LEN).collect();
269 if prefix.chars().count() != UUID_LEN {
270 return None;
271 }
272 Uuid::parse_str(&prefix).ok()
273}