simple_fs/
sfile.rs

1use crate::{Error, Result};
2use crate::{SMeta, SPath};
3use camino::{Utf8Path, Utf8PathBuf};
4use core::fmt;
5use std::fs::{self, Metadata};
6use std::path::{Path, PathBuf};
7use std::time::SystemTime;
8
9/// An SFile can be constructed from a Path, io::DirEntry, or walkdir::DirEntry
10/// and guarantees the following:
11///
12/// - The entry is a file (exists).
13/// - It has a file name.
14/// - The full path is UTF-8 valid.
15#[derive(Debug, Clone)]
16pub struct SFile {
17	path: SPath,
18}
19
20/// Constructors that guarantee the SFile contract described in the struct
21impl SFile {
22	/// Constructor for SFile accepting anything that implements Into<Utf8PathBuf>.
23	pub fn new(path: impl Into<Utf8PathBuf>) -> Result<Self> {
24		let path = SPath::new(path);
25		validate_sfile_for_result(&path)?;
26		Ok(Self { path })
27	}
28
29	/// Constructor from standard PathBuf.
30	pub fn from_std_path_buf(path_buf: PathBuf) -> Result<Self> {
31		let path = SPath::from_std_path_buf(path_buf)?;
32		validate_sfile_for_result(&path)?;
33		Ok(Self { path })
34	}
35
36	/// Constructor from standard Path and all impl AsRef<Path>.
37	pub fn from_std_path(path: impl AsRef<Path>) -> Result<Self> {
38		let path = SPath::from_std_path(path)?;
39		validate_sfile_for_result(&path)?;
40		Ok(Self { path })
41	}
42
43	/// Constructor from walkdir::DirEntry
44	pub fn from_walkdir_entry(wd_entry: walkdir::DirEntry) -> Result<Self> {
45		let path = SPath::from_walkdir_entry(wd_entry)?;
46		validate_sfile_for_result(&path)?;
47		Ok(Self { path })
48	}
49
50	/// Constructors for anything that implements AsRef<Path>.
51	///
52	/// Returns Option<SFile>. Useful for filter_map.
53	pub fn from_std_path_ok(path: impl AsRef<Path>) -> Option<Self> {
54		let path = SPath::from_std_path_ok(path)?;
55		validate_sfile_for_option(&path)?;
56		Some(Self { path })
57	}
58
59	/// Constructor from PathBuf returning an Option, none if validation fails.
60	/// Useful for filter_map.
61	pub fn from_std_path_buf_ok(path_buf: PathBuf) -> Option<Self> {
62		let path = SPath::from_std_path_buf_ok(path_buf)?;
63		validate_sfile_for_option(&path)?;
64		Some(Self { path })
65	}
66
67	/// Constructor from fs::DirEntry returning an Option; none if validation fails.
68	/// Useful for filter_map.
69	pub fn from_fs_entry_ok(fs_entry: fs::DirEntry) -> Option<Self> {
70		let path = SPath::from_fs_entry_ok(fs_entry)?;
71		validate_sfile_for_option(&path)?;
72		Some(Self { path })
73	}
74
75	/// Constructor from walkdir::DirEntry returning an Option; none if validation fails.
76	/// Useful for filter_map.
77	pub fn from_walkdir_entry_ok(wd_entry: walkdir::DirEntry) -> Option<Self> {
78		let path = SPath::from_walkdir_entry_ok(wd_entry)?;
79		validate_sfile_for_option(&path)?;
80		Some(Self { path })
81	}
82}
83
84/// Public into path
85impl SFile {
86	/// Consumes the SFile and returns its PathBuf.
87	pub fn into_std_path_buf(self) -> PathBuf {
88		self.path.into_std_path_buf()
89	}
90
91	/// Returns a reference to the internal standard Path.
92	pub fn std_path(&self) -> &Path {
93		self.path.std_path()
94	}
95
96	/// Returns a reference to the internal Utf8Path.
97	pub fn path(&self) -> &SPath {
98		&self.path
99	}
100}
101
102/// Public getters
103impl SFile {
104	/// Returns the &str of the path.
105	///
106	/// NOTE: We know that this must be Some() since the SFile constructor guarantees that
107	///       the path.as_str() is valid.
108	#[deprecated(note = "Use `as_str()` instead")]
109	pub fn to_str(&self) -> &str {
110		self.path.as_str()
111	}
112
113	pub fn as_str(&self) -> &str {
114		self.path.as_str()
115	}
116
117	/// Returns the Option<&str> representation of the `path.file_name()`.
118	pub fn file_name(&self) -> Option<&str> {
119		self.path.file_name()
120	}
121
122	/// Returns the &str representation of the `path.file_name()`.
123	///
124	/// Note: If no file name will be an empty string
125	pub fn name(&self) -> &str {
126		self.path.name()
127	}
128
129	/// Returns the parent name, and empty static &str if no present
130	pub fn parent_name(&self) -> &str {
131		self.path.parent_name()
132	}
133
134	/// Returns the Option<&str> representation of the file_stem().
135	pub fn file_stem(&self) -> Option<&str> {
136		self.path.file_stem()
137	}
138
139	/// Returns the &str representation of the `file_name()`.
140	///
141	/// Note: If no stem, will be an empty string
142	pub fn stem(&self) -> &str {
143		self.path.stem()
144	}
145
146	/// Returns the Option<&str> representation of the extension().
147	///
148	/// NOTE: This should never be a non-UTF-8 string
149	///       as the path was validated during SFile construction.
150	pub fn extension(&self) -> Option<&str> {
151		self.path.extension()
152	}
153
154	/// Same as `.extension()` but returns "" if no extension.
155	pub fn ext(&self) -> &str {
156		self.path.ext()
157	}
158
159	/// Returns true if the internal path is absolute.
160	pub fn is_absolute(&self) -> bool {
161		self.path.is_absolute()
162	}
163
164	/// Returns true if the internal path is relative.
165	pub fn is_relative(&self) -> bool {
166		self.path.is_relative()
167	}
168}
169
170/// Meta
171impl SFile {
172	/// Get a Simple Metadata structure `SMeta` with
173	/// created_epoch_us, modified_epoch_us, and size (all i64)
174	/// (size will be '0' for any none file)
175	pub fn meta(&self) -> Result<SMeta> {
176		self.path.meta()
177	}
178
179	/// Returns the std metadata
180	pub fn metadata(&self) -> Result<Metadata> {
181		self.path.metadata()
182	}
183
184	/// Returns the path.metadata modified as SystemTime.
185	///
186	#[allow(deprecated)]
187	#[deprecated = "use spath.meta() or spath.metadata"]
188	pub fn modified(&self) -> Result<SystemTime> {
189		self.path.modified()
190	}
191
192	/// Returns the epoch duration in microseconds.
193	/// Note: The maximum UTC date would be approximately `2262-04-11`.
194	///       Thus, for all intents and purposes, it is far enough not to worry.
195	#[deprecated = "use spath.meta()"]
196	pub fn modified_us(&self) -> Result<i64> {
197		Ok(self.meta()?.modified_epoch_us)
198	}
199
200	/// Returns the file size in bytes as `u64`.
201	#[deprecated = "use spath.meta()"]
202	pub fn file_size(&self) -> Result<u64> {
203		let path = self.std_path();
204		let metadata = fs::metadata(path).map_err(|ex| Error::CantGetMetadata((path, ex).into()))?;
205		Ok(metadata.len())
206	}
207}
208
209/// Transformers
210impl SFile {
211	pub fn canonicalize(&self) -> Result<SFile> {
212		let path = self.path.canonicalize()?;
213		// Note: here since the previous path was valid, if the spath canonicalization passes,
214		//       we are ok.
215		Ok(SFile { path })
216	}
217
218	// region:    --- Collapse
219
220	/// Collapse a path without performing I/O.
221	///
222	/// All redundant separator and up-level references are collapsed.
223	///
224	/// However, this does not resolve links.
225	pub fn collapse(&self) -> SFile {
226		SFile {
227			path: self.path.collapse(),
228		}
229	}
230
231	/// Same as [`collapse`] but consume and create a new SPath only if needed
232	pub fn into_collapsed(self) -> SFile {
233		if self.is_collapsed() { self } else { self.collapse() }
234	}
235
236	/// Return `true` if the path is collapsed.
237	///
238	/// # Quirk
239	///
240	/// If the path does not start with `./` but contains `./` in the middle,
241	/// then this function might returns `true`.
242	pub fn is_collapsed(&self) -> bool {
243		crate::is_collapsed(self)
244	}
245
246	// endregion: --- Collapse
247
248	// region:    --- Parent & Join
249
250	/// Returns the parent directory as SPath, if available.
251	pub fn parent(&self) -> Option<SPath> {
252		self.path.parent()
253	}
254
255	/// Joins the current path with the specified leaf_path.
256	///
257	/// This method creates a new path by joining the existing path with a specified leaf_path
258	/// and returns the result as an SPath.
259	pub fn join(&self, leaf_path: impl Into<Utf8PathBuf>) -> SPath {
260		self.path.join(leaf_path)
261	}
262
263	/// Joins a standard Path to the path of this SFile.
264	pub fn join_std_path(&self, leaf_path: impl AsRef<Path>) -> Result<SPath> {
265		self.path.join_std_path(leaf_path)
266	}
267
268	/// Creates a new sibling path with the specified leaf_path.
269	///
270	/// Generates a new path in the same parent directory as the current file, appending the leaf_path.
271	pub fn new_sibling(&self, leaf_path: &str) -> SPath {
272		self.path.new_sibling(leaf_path)
273	}
274
275	/// Creates a new sibling path with the specified standard path.
276	pub fn new_sibling_std_path(&self, leaf_path: impl AsRef<Path>) -> Result<SPath> {
277		self.path.new_sibling_std_path(leaf_path)
278	}
279
280	// endregion: --- Parent & Join
281
282	// region:    --- Diff
283
284	pub fn diff(&self, base: impl AsRef<Utf8Path>) -> Option<SPath> {
285		self.path.diff(base)
286	}
287
288	pub fn try_diff(&self, base: impl AsRef<Utf8Path>) -> Result<SPath> {
289		self.path.try_diff(base)
290	}
291
292	// endregion: --- Diff
293
294	// region:    --- Replace
295
296	pub fn replace_prefix(&self, base: impl AsRef<str>, with: impl AsRef<str>) -> SPath {
297		let path = &self.path;
298		path.replace_prefix(base, with)
299	}
300
301	pub fn into_replace_prefix(self, base: impl AsRef<str>, with: impl AsRef<str>) -> SPath {
302		let path = self.path;
303		path.into_replace_prefix(base, with)
304	}
305
306	// endregion: --- Replace
307}
308
309/// Path/UTF8Path/Camino passthrough
310impl SFile {
311	pub fn as_std_path(&self) -> &Path {
312		self.path.std_path()
313	}
314
315	/// Returns a path that, when joined onto `base`, yields `self`.
316	///
317	/// # Errors
318	///
319	/// If `base` is not a prefix of `self`
320	pub fn strip_prefix(&self, prefix: impl AsRef<Path>) -> Result<SPath> {
321		self.path.strip_prefix(prefix)
322	}
323
324	/// Determines whether `base` is a prefix of `self`.
325	///
326	/// Only considers whole path components to match.
327	///
328	/// # Examples
329	///
330	/// ```
331	/// use camino::Utf8Path;
332	///
333	/// let path = Utf8Path::new("/etc/passwd");
334	///
335	/// assert!(path.starts_with("/etc"));
336	/// assert!(path.starts_with("/etc/"));
337	/// assert!(path.starts_with("/etc/passwd"));
338	/// assert!(path.starts_with("/etc/passwd/")); // extra slash is okay
339	/// assert!(path.starts_with("/etc/passwd///")); // multiple extra slashes are okay
340	///
341	/// assert!(!path.starts_with("/e"));
342	/// assert!(!path.starts_with("/etc/passwd.txt"));
343	///
344	/// assert!(!Utf8Path::new("/etc/foo.rs").starts_with("/etc/foo"));
345	/// ```
346	pub fn starts_with(&self, base: impl AsRef<Path>) -> bool {
347		self.path.starts_with(base)
348	}
349}
350
351// region:    --- Std Traits Impls
352
353impl AsRef<Path> for SFile {
354	fn as_ref(&self) -> &Path {
355		self.path.as_ref()
356	}
357}
358
359impl fmt::Display for SFile {
360	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361		write!(f, "{}", self.as_str())
362	}
363}
364
365// endregion: --- Std Traits Impls
366
367// region:    --- AsRefs
368
369impl AsRef<SFile> for SFile {
370	fn as_ref(&self) -> &SFile {
371		self
372	}
373}
374
375impl AsRef<Utf8Path> for SFile {
376	fn as_ref(&self) -> &Utf8Path {
377		self.path.as_ref()
378	}
379}
380
381impl AsRef<str> for SFile {
382	fn as_ref(&self) -> &str {
383		self.as_str()
384	}
385}
386
387impl AsRef<SPath> for SFile {
388	fn as_ref(&self) -> &SPath {
389		&self.path
390	}
391}
392
393// endregion: --- AsRefs
394
395// region:    --- Froms
396
397impl From<SFile> for String {
398	fn from(val: SFile) -> Self {
399		val.as_str().to_string()
400	}
401}
402
403impl From<&SFile> for String {
404	fn from(val: &SFile) -> Self {
405		val.as_str().to_string()
406	}
407}
408
409impl From<SFile> for PathBuf {
410	fn from(val: SFile) -> Self {
411		val.into_std_path_buf()
412	}
413}
414
415impl From<&SFile> for PathBuf {
416	fn from(val: &SFile) -> Self {
417		val.std_path().to_path_buf()
418	}
419}
420
421impl From<SFile> for Utf8PathBuf {
422	fn from(val: SFile) -> Self {
423		val.path.path_buf
424	}
425}
426
427impl From<SFile> for SPath {
428	fn from(val: SFile) -> Self {
429		val.path
430	}
431}
432
433impl From<&SFile> for SPath {
434	fn from(val: &SFile) -> Self {
435		val.path.clone()
436	}
437}
438
439// endregion: --- Froms
440
441// region:    --- TryFroms
442
443impl TryFrom<&str> for SFile {
444	type Error = Error;
445	fn try_from(path: &str) -> Result<SFile> {
446		let path = SPath::from(path);
447		validate_sfile_for_result(&path)?;
448		Ok(Self { path })
449	}
450}
451
452impl TryFrom<String> for SFile {
453	type Error = Error;
454	fn try_from(path: String) -> Result<SFile> {
455		SFile::try_from(path.as_str())
456	}
457}
458
459impl TryFrom<&String> for SFile {
460	type Error = Error;
461	fn try_from(path: &String) -> Result<SFile> {
462		SFile::try_from(path.as_str())
463	}
464}
465
466impl TryFrom<PathBuf> for SFile {
467	type Error = Error;
468	fn try_from(path_buf: PathBuf) -> Result<SFile> {
469		SFile::from_std_path_buf(path_buf)
470	}
471}
472
473impl TryFrom<fs::DirEntry> for SFile {
474	type Error = Error;
475	fn try_from(fs_entry: fs::DirEntry) -> Result<SFile> {
476		let path = SPath::try_from(fs_entry)?;
477		validate_sfile_for_result(&path)?;
478		Ok(Self { path })
479	}
480}
481
482impl TryFrom<walkdir::DirEntry> for SFile {
483	type Error = Error;
484	fn try_from(wd_entry: walkdir::DirEntry) -> Result<SFile> {
485		let path = SPath::try_from(wd_entry)?;
486		validate_sfile_for_result(&path)?;
487		Ok(Self { path })
488	}
489}
490
491impl TryFrom<SPath> for SFile {
492	type Error = Error;
493	fn try_from(path: SPath) -> Result<SFile> {
494		validate_sfile_for_result(&path)?;
495		Ok(Self { path })
496	}
497}
498// endregion: --- TryFroms
499
500// region:    --- File Validation
501
502fn validate_sfile_for_result(path: &SPath) -> Result<()> {
503	if path.is_file() {
504		Ok(())
505	} else {
506		Err(Error::FileNotFound(path.as_str().to_string()))
507	}
508}
509
510/// Validate but without generating an error (good for the _ok constructors)
511fn validate_sfile_for_option(path: &SPath) -> Option<()> {
512	if path.is_file() { Some(()) } else { None }
513}
514
515// endregion: --- File Validation