xdg_thumbnail/
identity.rs1use std::ffi::{OsStr, OsString};
5use std::fmt;
6use std::fs::File;
7use std::path::{Path, PathBuf};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use std::os::unix::ffi::OsStrExt;
11
12use crate::{
13 CacheRootProblem, PersonalOriginalUri, Result, SharedRelativeOriginalUri, ThumbnailError,
14 ThumbnailSize, validate_mime_type,
15};
16
17#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
19pub struct UnixMtimeSeconds {
20 seconds: u64,
21}
22
23impl UnixMtimeSeconds {
24 #[must_use]
26 pub const fn new(seconds: u64) -> Self {
27 Self { seconds }
28 }
29
30 pub const fn try_from_i64(seconds: i64) -> Result<Self> {
36 if seconds < 0 {
37 return Err(ThumbnailError::invalid_metadata(
38 "mtime is before the Unix epoch",
39 ));
40 }
41 Ok(Self {
42 seconds: seconds as u64,
43 })
44 }
45
46 pub fn from_system_time(time: SystemTime) -> Result<Self> {
52 let duration = time
53 .duration_since(UNIX_EPOCH)
54 .map_err(|_| ThumbnailError::invalid_metadata("mtime is before the Unix epoch"))?;
55 Ok(Self {
56 seconds: duration.as_secs(),
57 })
58 }
59
60 #[must_use]
62 pub const fn as_u64(self) -> u64 {
63 self.seconds
64 }
65}
66
67impl TryFrom<i64> for UnixMtimeSeconds {
68 type Error = ThumbnailError;
69
70 fn try_from(seconds: i64) -> Result<Self> {
71 Self::try_from_i64(seconds)
72 }
73}
74
75impl From<u64> for UnixMtimeSeconds {
76 fn from(seconds: u64) -> Self {
77 Self::new(seconds)
78 }
79}
80
81impl From<UnixMtimeSeconds> for u64 {
82 fn from(mtime: UnixMtimeSeconds) -> Self {
83 mtime.as_u64()
84 }
85}
86
87impl TryFrom<SystemTime> for UnixMtimeSeconds {
88 type Error = ThumbnailError;
89
90 fn try_from(time: SystemTime) -> Result<Self> {
91 Self::from_system_time(time)
92 }
93}
94
95impl fmt::Display for UnixMtimeSeconds {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(f, "{}", self.seconds)
98 }
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
107pub struct PersonalOriginalIdentity {
108 uri: PersonalOriginalUri,
109 mtime: UnixMtimeSeconds,
110 original_byte_size: Option<u64>,
111 mime_type: Option<String>,
112}
113
114impl PersonalOriginalIdentity {
115 #[must_use]
117 pub fn new(uri: PersonalOriginalUri, mtime: UnixMtimeSeconds) -> Self {
118 Self {
119 uri,
120 mtime,
121 original_byte_size: None,
122 mime_type: None,
123 }
124 }
125
126 #[must_use]
128 pub fn with_original_byte_size(mut self, size: u64) -> Self {
129 self.original_byte_size = Some(size);
130 self
131 }
132
133 pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Result<Self> {
139 let mime_type = mime_type.into();
140 validate_mime_type(&mime_type)?;
141 self.mime_type = Some(mime_type);
142 Ok(self)
143 }
144
145 #[must_use]
147 pub fn uri(&self) -> &PersonalOriginalUri {
148 &self.uri
149 }
150
151 #[must_use]
153 pub const fn mtime(&self) -> UnixMtimeSeconds {
154 self.mtime
155 }
156
157 #[must_use]
159 pub const fn original_byte_size(&self) -> Option<u64> {
160 self.original_byte_size
161 }
162
163 #[must_use]
165 pub fn mime_type(&self) -> Option<&str> {
166 self.mime_type.as_deref()
167 }
168}
169
170#[derive(Clone, Debug, Eq, PartialEq)]
172pub struct ReadablePersonalOriginalIdentity {
173 identity: PersonalOriginalIdentity,
174}
175
176impl ReadablePersonalOriginalIdentity {
177 #[must_use]
182 pub fn assume_readable(identity: PersonalOriginalIdentity) -> Self {
183 Self { identity }
184 }
185
186 #[must_use]
188 pub fn into_identity(self) -> PersonalOriginalIdentity {
189 self.identity
190 }
191
192 pub fn from_local_path(path: impl AsRef<Path>) -> Result<Self> {
206 Self::from_local_path_inner(path.as_ref(), None)
207 }
208
209 pub fn from_local_path_with_mime_type(
223 path: impl AsRef<Path>,
224 mime_type: impl Into<String>,
225 ) -> Result<Self> {
226 Self::from_local_path_inner(path.as_ref(), Some(mime_type.into()))
227 }
228
229 fn from_local_path_inner(path: &Path, mime_type: Option<String>) -> Result<Self> {
230 if !path.is_absolute() {
231 return Err(ThumbnailError::invalid_uri("local path must be absolute"));
232 }
233 let file = File::open(path).map_err(|source| {
234 ThumbnailError::io("open original for reading", Some(path.to_owned()), source)
235 })?;
236 let metadata = file.metadata().map_err(|source| {
237 ThumbnailError::io("read original metadata", Some(path.to_owned()), source)
238 })?;
239 let uri = PersonalOriginalUri::from_absolute_path_bytes(path.as_os_str().as_bytes())?;
240 let mtime = UnixMtimeSeconds::from_system_time(metadata.modified().map_err(|source| {
241 ThumbnailError::io(
242 "read original modification time",
243 Some(path.to_owned()),
244 source,
245 )
246 })?)?;
247 let identity =
248 PersonalOriginalIdentity::new(uri, mtime).with_original_byte_size(metadata.len());
249 let identity = if let Some(mime_type) = mime_type {
250 identity.with_mime_type(mime_type)?
251 } else {
252 identity
253 };
254 Ok(Self { identity })
255 }
256
257 #[must_use]
259 pub const fn identity(&self) -> &PersonalOriginalIdentity {
260 &self.identity
261 }
262}
263
264#[derive(Clone, Debug, Eq, PartialEq)]
266pub struct SharedRepositoryContext {
267 repository_root: PathBuf,
268 original_child_name: OsString,
269 shared_uri: SharedRelativeOriginalUri,
270}
271
272impl SharedRepositoryContext {
273 pub fn new(
280 repository_root: impl AsRef<Path>,
281 original_child_name: impl AsRef<OsStr>,
282 ) -> Result<Self> {
283 let repository_root = repository_root.as_ref();
284 let original_child_name = original_child_name.as_ref();
285 if !repository_root.is_absolute() {
286 return Err(ThumbnailError::InvalidCacheRoot {
287 path: repository_root.to_owned(),
288 problem: CacheRootProblem::NotAbsolute,
289 });
290 }
291 let shared_uri =
292 SharedRelativeOriginalUri::from_raw_child_name(original_child_name.as_bytes())?;
293 Ok(Self {
294 repository_root: repository_root.to_owned(),
295 original_child_name: original_child_name.to_owned(),
296 shared_uri,
297 })
298 }
299
300 #[must_use]
302 pub fn repository_root(&self) -> &Path {
303 &self.repository_root
304 }
305
306 #[must_use]
308 pub fn original_child_name(&self) -> &OsStr {
309 &self.original_child_name
310 }
311
312 #[must_use]
314 pub const fn shared_uri(&self) -> &SharedRelativeOriginalUri {
315 &self.shared_uri
316 }
317
318 #[must_use]
320 pub fn cache_entry_path(&self, size: ThumbnailSize) -> PathBuf {
321 self.repository_root
322 .join(".sh_thumbnails")
323 .join(size.directory_name())
324 .join(self.shared_uri.thumbnail_file_name())
325 }
326}