devbox_build/
fs.rs

1use std::io::Write;
2use std::ffi::OsStr;
3use std::marker::PhantomData;
4use std::ops::Add;
5use std::path::{Component, Path, PathBuf};
6use std::time::SystemTime;
7
8use globset::{ GlobBuilder, GlobMatcher };
9
10use super::Resource;
11use super::Set;
12
13//-- Unit ------------------------------------------------------------------------------------------
14
15/// Enum based Resource that is either a File or a Dir
16///
17#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
18pub enum Unit {
19    Dir(Dir),
20    File(File),
21}
22
23impl Unit {
24
25    /// Delegates to File or Dir path() mathod
26    //TODO: test
27    pub fn path(&self) -> &Path {
28        match self {
29           Unit::Dir(ref res) => res.path(),
30           Unit::File(ref res) => res.path(),
31        }
32    }
33
34    /// Delegates to File or Dir link_from_inside() mathod
35    //TODO: test
36    pub fn link_from_inside(&self, dir: &Dir) {
37        match self {
38           Unit::Dir(ref res) => res.link_from_inside(dir),
39           Unit::File(ref res) => res.link_from_inside(dir),
40        }
41    }
42}
43
44impl Resource for Unit {
45    //TODO: test
46    fn timestamp(&self) -> Option<SystemTime> {
47        match self {
48           Unit::Dir(ref res) => res.timestamp(),
49           Unit::File(ref res) => res.timestamp(),
50        }
51    }
52}
53
54//-- File ------------------------------------------------------------------------------------------
55
56/// Resource representing file system file
57///
58#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
59pub struct File {
60    path: PathBuf
61}
62
63impl File {
64
65    /// Create new File pointing to absolute file system `path`
66    pub fn new<P:AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
67        match normalize(path.as_ref()) {
68            Some(path) if path.is_absolute() => Ok(File { path }),
69            _ => Err(format!("Path {0} is not absolute", path.as_ref().display()).into())
70        }
71    }
72
73    /// Path reference to file system file
74    pub fn path(&self) -> &Path {
75        &self.path
76    }
77
78    /// Creates the file using [`create`](#method.create) and returns itself or stops the build with
79    /// informative error message.
80    pub fn created(self) -> Self {
81        self.create();
82        self
83    }
84
85    /// Creates the file using [`create_result`](#method.create_result) or stops the build with
86    /// informative error message.
87    pub fn create(&self) -> std::fs::File {
88        self.create_result().expect(format!("Creating file {} FAILED", self).as_str())
89    }
90
91    /// Creates (or truncates) the file and any missing directories on it's path in write only mode.
92    pub fn create_result(&self) -> std::io::Result<std::fs::File> {
93        println!("Creating file: {}", self);
94
95        if let Some(parent) = self.parent() {
96            parent.create_result()?;
97        }
98
99        std::fs::File::create(&self.path)
100    }
101
102    /// Creating a link to this file from another directory with this file's name returning self
103    /// or stopping the build with informative error message.
104    ///
105    /// If the directory already contains an entry with this name, linking fails.
106    pub fn linked_from_inside(self, dir: &Dir) -> Self {
107        dir.file(self.path().file_name().unwrap()).link_to(&self);
108        self
109    }
110
111    /// Creating a link to this file from another directory with this file's name or stops the build
112    /// with informative error message.
113    ///
114    /// If the directory already contains an entry with this name, linking fails.
115    pub fn link_from_inside(&self, dir: &Dir) {
116        dir.file(self.path().file_name().unwrap()).link_to(self);
117    }
118
119    /// Creating a link to this file from another directory with this file's name.
120    ///
121    /// If the directory already contains a file or directory by this name, linking fails.
122    /// To allow overwriting existing link with different target set `force` to `true` or linking to
123    /// this file will also fail.
124    pub fn link_from_inside_result(&self, dir: &Dir, force: bool) -> std::io::Result<()> {
125        dir.file(self.path().file_name().unwrap()).link_to_result(self, force)
126    }
127
128    /// Create a symbolic link at this file path to given target file `to` creating any needed
129    /// directories in the process returning self or stops the build with informative error message.
130    ///
131    /// If a file or directory by that name already exists, linking will fail.
132    pub fn linked_to(self, to: &File) -> Self {
133        self.link_to(to);
134        self
135    }
136
137    /// Create a symbolic link at this file path to given target file `to` creating any needed
138    /// directories in the process or stops the build with informative error message.
139    ///
140    /// If a file or directory by that name already exists, linking will fail.
141    pub fn link_to(&self, to: &File) {
142        self.link_to_result(to, false)
143            .expect(format!("Creating link {} -> {} FAILED", self, to).as_str())
144    }
145
146    /// Create a symbolic link at this file path to given target file `to` creating any needed
147    /// directories in the process.
148    ///
149    /// If a file or directory by that name already exists, linking will fail.
150    /// To allow overwriting existing link to a different file set `force` to `true` or linking to
151    /// a different file will also fail.
152    pub fn link_to_result(&self, to: &File, force: bool) -> std::io::Result<()> {
153        println!("Creating link {} -> {}", self, to);
154
155        if let Some(parent) = self.parent() {
156            parent.create_result()?;
157        }
158
159        if self.path.exists() {
160            match std::fs::read_link(&self.path) {
161                Ok(target) if target != to.path && force => std::fs::remove_file(self.path())?,
162                Ok(target) if target == to.path => return Ok(()),
163                _ => return Err(std::io::ErrorKind::AlreadyExists.into()),
164            }
165        }
166
167        File::platform_make_link(&to.path, &self.path)
168    }
169
170    /// Opens file's metadata using [`metadata_result`](#method.metadata_result) or stops the build
171    /// with informative error message.
172    pub fn metadata(&self) -> std::fs::Metadata {
173        self.metadata_result().expect(format!("Metatdata query {} FAILED", self).as_str())
174    }
175
176    /// Opens file metadata
177    pub fn metadata_result(&self) -> std::io::Result<std::fs::Metadata> {
178        std::fs::metadata(&self.path)
179    }
180
181    /// Opens the file using [`open_result`](#method.open_result) or stops the build with
182    /// informative error message
183    pub fn open(&self) -> std::fs::File {
184        self.open_result().expect(format!("Opening file {} FAILED", self).as_str())
185    }
186
187    /// Opens the file in read only mode
188    pub fn open_result(&self) -> std::io::Result<std::fs::File> {
189        std::fs::File::open(&self.path)
190    }
191
192    /// Writes the entire content to the file using [`rewrite_result`](#method.rewrite_result) or
193    /// stops the build with informative error message
194    //TODO: test
195    pub fn rewrite<P: AsRef<[u8]>>(&self, bytes: P) {
196        self.rewrite_result(bytes).expect(format!("Writing text {} FAILED", self).as_str())
197    }
198
199    /// Writes the entire content to the file if it is different then the current one
200    /// creating the file if needed.
201    //TODO: test
202    pub fn rewrite_result<P: AsRef<[u8]>>(&self, bytes: P) -> std::io::Result<()> {
203        let bytes = bytes.as_ref();
204        if let Ok(old) = std::fs::read(&self.path) {
205            if old == bytes {
206                return Ok(())
207            }
208        }
209
210        self.create().write_all(bytes)
211    }
212
213    /// Touches the file using [`touch`](#method.touch) and returns itself or stops the build with
214    /// informative error message
215    pub fn touched(self) -> Self {
216        self.touch();
217        self
218    }
219
220    /// Touches the file using [`touch`](#method.touch) and returns itself or stops the build with
221    /// informative error message.
222    pub fn touch(&self) {
223        self.touch_result().expect(format!("Touching file {} FAILED", self).as_str())
224    }
225
226    /// Touches the file by updating it's modification time or creating an empty one if it does not
227    /// exists yet including any needed directories.
228    pub fn touch_result(&self) -> std::io::Result<()> {
229        println!("Touching file: {}", self);
230
231        if !self.path.exists() {
232            return self.create_result().map(|_|());
233        }
234
235        let now = filetime::FileTime::from_system_time(SystemTime::now());
236        filetime::set_file_mtime(self.path.clone(), now)
237    }
238
239    /// Returns parent directory
240    fn parent(&self) -> Option<Dir> {
241        self.path.parent().map(|parent| Dir { path: parent.to_owned() })
242    }
243
244    #[cfg(not(windows))]
245    fn platform_make_link<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> std::io::Result<()> {
246        std::os::unix::fs::symlink(src, dst)
247    }
248
249    #[cfg(windows)]
250    fn platform_make_link<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> std::io::Result<()> {
251        std::os::windows::fs::symlink_file(src, dst)
252    }
253}
254
255impl Resource for File {
256
257   fn timestamp(&self) -> Option<SystemTime> {
258        if let Ok(metadata) = self.metadata_result() {
259            return metadata.modified().ok();
260        }
261
262        None
263    }
264}
265
266impl AsRef<File> for File {
267    fn as_ref(&self) -> &File {
268        self
269    }
270}
271
272impl AsRef<OsStr> for File {
273    fn as_ref(&self) -> &OsStr {
274        &self.path.as_ref()
275    }
276}
277
278impl AsRef<Path> for File {
279    fn as_ref(&self) -> &Path {
280        &self.path.as_ref()
281    }
282}
283
284impl Add<&File> for &File {
285    type Output = Set<File>;
286
287    fn add(self, rhs: &File) -> Self::Output {
288        vec![self.clone(), rhs.clone()].into()
289    }
290}
291
292impl Add<&Dir> for &File {
293    type Output = Set<Unit>;
294
295    fn add(self, rhs: &Dir) -> Self::Output {
296        vec![Unit::File(self.clone()), Unit::Dir(rhs.clone())].into()
297    }
298}
299
300impl std::fmt::Display for File {
301    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
302        self.path.display().fmt(formatter)
303    }
304}
305
306//-- Dir -------------------------------------------------------------------------------------------
307
308/// Resource representing file system directory
309///
310#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
311pub struct Dir {
312    path: PathBuf
313}
314
315impl Dir {
316
317    /// Create new Dir pointing to absolute file system `path` panicking if failed
318    pub fn new<P:AsRef<Path>>(path: P) -> Self {
319        Dir::new_safe(path).unwrap()
320    }
321
322    /// Create new Dir pointing to absolute file system `path`
323    pub fn new_safe<P:AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
324        match normalize(path.as_ref()) {
325            Some(path) if path.is_absolute() => Ok(Dir { path }),
326            _ => Err(format!("Path {0} is not absolute", path.as_ref().display()).into())
327        }
328    }
329
330    /// Path reference to file system directory
331    pub fn path(&self) -> &Path {
332        self.path.as_ref()
333    }
334
335    /// Creates the directory using [`create`](#method.create) and returns itself or stops the build
336    /// with informative error message.
337    pub fn created(self) -> Self {
338        self.create();
339        self
340    }
341
342    /// Creates the directory using [`create_result`](#method.create_result) or stops the build with
343    /// informative error message.
344    pub fn create(&self) {
345        self.create_result().expect(format!("Creating directory {} FAILED", self).as_str());
346    }
347
348    /// Creates the directory and any missing parent directories on it's path.
349    pub fn create_result(&self) -> std::io::Result<()> {
350        std::fs::create_dir_all(&self.path)
351    }
352
353    /// All directory content (files, directories and links) matching given `glob` file name pattern
354    pub fn content<G:AsRef<str>>(&self, glob: G) -> DirContent<Unit> {
355        DirContent::new(self.path.clone(), glob)
356    }
357
358    /// All subdirectories and directory links matching given `glob` file name pattern
359    pub fn dirs<G:AsRef<str>>(&self, glob: G) -> DirContent<Dir> {
360        DirContent::new(self.path.clone(), glob)
361    }
362
363    /// All files and file links matching given `glob` file name pattern
364    pub fn files<G:AsRef<str>>(&self, glob: G) -> DirContent<File> {
365        DirContent::new(self.path.clone(), glob)
366    }
367
368    /// Subdirectory at given relative `path`
369    ///
370    /// Will stop the build with informative error message if path is not relative.
371    pub fn dir<P:AsRef<Path>>(&self, path: P) -> Dir {
372        self.dir_result(path).unwrap()
373    }
374
375    /// Subdirectory at given relative `path`
376    pub fn dir_result<P:AsRef<Path>>(&self, path: P) -> Result<Self, Box<dyn std::error::Error>> {
377        match normalize(path.as_ref()) {
378            Some(path) if path.is_relative() => Ok(Dir { path: self.path.join(path) }),
379            _ => Err(format!("Path {0} is not relative", path.as_ref().display()).into())
380        }
381    }
382
383    /// A file at given relative `path`
384    ///
385    /// Will stop the build with informative error message if path is not relative.
386    pub fn file<P:AsRef<Path>>(&self, path: P) -> File {
387        self.file_result(path).unwrap()
388    }
389
390    /// A file at given relative `path`
391    pub fn file_result<P:AsRef<Path>>(&self, path: P) -> Result<File, Box<dyn std::error::Error>> {
392        match normalize(path.as_ref()) {
393            Some(path) if path.is_relative() => Ok(File { path: self.path.join(path) }),
394            _ => Err(format!("Path '{0}' is not relative", path.as_ref().display()).into())
395        }
396    }
397
398    /// Creating a link to this directory from another directory with this directory's name
399    /// returning self or stopping the build with informative error message.
400    ///
401    /// If the directory already contains an entry with this name, linking fails.
402    pub fn linked_from_inside(self, dir: &Dir) -> Self {
403        dir.dir(self.path().file_name().unwrap()).link_to(&self);
404        self
405    }
406
407    /// Creating a link to this directory from another directory with this directory's name
408    /// or stopping the build with informative error message.
409    ///
410    /// If the directory already contains an entry with this name, linking fails.
411    pub fn link_from_inside(&self, dir: &Dir) {
412        dir.dir(self.path().file_name().unwrap()).link_to(self);
413    }
414
415    /// Creating a link to this directory from another directory with this directory's name.
416    ///
417    /// If the directory already contains a file or directory by this name, linking fails.
418    /// To allow overwriting existing link with different target set `force` to `true` or linking to
419    /// this directory will also fail.
420    pub fn link_from_inside_result(&self, dir: &Dir, force: bool) -> std::io::Result<()> {
421        dir.dir(self.path().file_name().unwrap()).link_to_result(self, force)
422    }
423
424    /// Create a symbolic link at this directory path to given target directory `to` creating any
425    /// needed directories in the process returning self or stopping the build with informative
426    /// error message.
427    ///
428    /// If a file or directory by that name already exists, linking will fail.
429    pub fn linked_to(self, to: &Dir) -> Self {
430        self.link_to(to);
431        self
432    }
433
434    /// Create a symbolic link at this directory path to given target directory `to` creating any
435    /// needed directories in the process or stopping the build with informative error message.
436    ///
437    /// If a file or directory by that name already exists, linking will fail.
438    pub fn link_to(&self, to: &Dir) {
439        self.link_to_result(to, false)
440            .expect(format!("Creating link {} -> {} FAILED", self, to).as_str())
441    }
442
443    /// Create a symbolic link at this directory path to given target directory `to` creating any
444    /// needed directories in the process.
445    ///
446    /// If a file or directory by that name already exists, linking will fail.
447    /// To allow overwriting existing link to a different directory set `force` to `true` or linking
448    /// to a different directory will also fail.
449    pub fn link_to_result(&self, to: &Dir, force: bool) -> std::io::Result<()> {
450        println!("Creating link {} -> {}", self, to);
451
452        if let Some(parent) = self.parent() {
453            parent.create_result()?;
454        }
455
456        if self.path.exists() {
457
458            match std::fs::read_link(&self.path) {
459                Ok(target) if target != to.path && force => std::fs::remove_file(self.path())?,
460                Ok(target) if target == to.path => return Ok(()),
461                _ => return Err(std::io::ErrorKind::AlreadyExists.into()),
462            }
463        }
464
465        Dir::platform_make_link(&to.path, &self.path)
466    }
467
468    /// Touches the directory using [`touch`](#method.touch) and returns itself or stops the build
469    /// with informative error message
470    pub fn touched(self) -> Self {
471        self.touch();
472        self
473    }
474
475    /// Touches the directory using [`touch`](#method.touch) and returns itself or stops the build
476    /// with informative error message.
477    pub fn touch(&self) {
478        self.touch_result().expect(format!("Touching dir {} FAILED", self).as_str())
479    }
480
481    /// Touches the directory by updating it's modification time or creating a new one if it does
482    /// not exists yet including any needed directories.
483    pub fn touch_result(&self) -> std::io::Result<()> {
484        println!("Touching dir: {}", self);
485
486        if !self.path.exists() {
487            return self.create_result();
488        }
489
490        let now = filetime::FileTime::from_system_time(SystemTime::now());
491        filetime::set_file_mtime(self.path.clone(), now)
492    }
493
494    /// Returns parent directory
495    fn parent(&self) -> Option<Dir> {
496        self.path.parent().map(|parent| Dir { path: parent.to_owned() })
497    }
498
499    #[cfg(not(windows))]
500    fn platform_make_link<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> std::io::Result<()> {
501        std::os::unix::fs::symlink(src, dst)
502    }
503
504    #[cfg(windows)]
505    fn platform_make_link<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> std::io::Result<()> {
506        std::os::windows::fs::symlink_dir(src, dst)
507    }
508}
509
510impl AsRef<Dir> for Dir {
511    fn as_ref(&self) -> &Dir {
512        self
513    }
514}
515
516impl AsRef<OsStr> for Dir {
517    fn as_ref(&self) -> &OsStr {
518        &self.path.as_ref()
519    }
520}
521
522impl AsRef<Path> for Dir {
523    fn as_ref(&self) -> &Path {
524        &self.path.as_ref()
525    }
526}
527
528impl Resource for Dir {
529    fn timestamp(&self) -> Option<SystemTime> {
530        if let Ok(metadata) = std::fs::metadata(&self.path) {
531            return metadata.modified().ok();
532        }
533
534        None
535    }
536}
537
538impl Add<&Dir> for &Dir {
539    type Output = Set<Dir>;
540
541    fn add(self, rhs: &Dir) -> Self::Output {
542        vec![self.clone(), rhs.clone()].into()
543    }
544}
545
546impl Add<&File> for &Dir {
547    type Output = Set<Unit>;
548
549    fn add(self, rhs: &File) -> Self::Output {
550        vec![Unit::Dir(self.clone()), Unit::File(rhs.clone())].into()
551    }
552}
553
554impl std::fmt::Display for Dir {
555    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
556        self.path.display().fmt(formatter)
557    }
558}
559
560//-- Path normalization ----------------------------------------------------------------------------
561
562fn normalize<P:AsRef<Path>>(subpath: P) -> Option<PathBuf> {
563    let result = PathBuf::from(subpath.as_ref()).components().fold(Some((PathBuf::new(), 0)), |r, c| {
564        match r {
565            None => None,
566            Some((mut path, depth)) => match c {
567                Component::Normal(value) => {
568                    path.push(value);
569                    Some((path, depth + 1))
570                }
571                Component::CurDir => {
572                    Some((path, depth))
573                }
574                Component::ParentDir => {
575                    if depth == 0 {
576                        return  None
577                    }
578                    path.pop();
579                    Some((path, depth - 1))
580                }
581                Component::RootDir => {
582                    path.push("/");
583                    Some((path, 0))
584                }
585                Component::Prefix(prefix) => {
586                    path.push(prefix.as_os_str());
587                    Some((path, -1))
588                }
589            }
590        }
591    });
592
593    if let Some((path,depth)) = result {
594        if depth > 0 {
595            return Some(path)
596        }
597    }
598
599    return None;
600}
601
602//-- DirContent ------------------------------------------------------------------------------------
603
604/// Represents directory entries matching certain criteria like GLOB name pattern and type (files,
605/// directories or units).
606///
607/// Matching is done on two sets of patterns:
608///  - entry matches if any of the inclusion patterns matches and
609///  - none of the exclusion pattern matches
610#[derive(Clone, Debug)]
611pub struct DirContent<T> {
612    path: PathBuf,
613    matchers: Vec<(GlobMatcher, bool)>,
614    phantom: PhantomData<T>,
615}
616
617impl<T> DirContent<T> {
618
619    fn new<G:AsRef<str>>(path: PathBuf, glob: G) -> Self {
620        DirContent {
621            phantom: PhantomData,
622            path,
623            matchers: vec![compile(true, glob)],
624        }
625    }
626
627    /// Add exlusion pattern reducing the number of matching entries
628    pub fn exclude<G:AsRef<str>>(mut self, glob: G) -> Self {
629        self.matchers.push(compile(false, glob));
630        self
631    }
632
633    /// Add inclusion pattern increasing the number of matching entries
634    pub fn include<G:AsRef<str>>(mut self, glob: G) -> Self {
635        self.matchers.push(compile(true, glob));
636        self
637    }
638
639    fn walkdir(&self) -> impl Iterator<Item=walkdir::DirEntry> {
640        let root = self.path.clone();
641        let matchers = self.matchers.clone();
642        walkdir::WalkDir::new(&self.path)
643            .follow_links(true)
644            .into_iter()
645            .filter_map(|e| e.ok())
646            .filter(move |e| e.depth() > 0 && {
647                let relative = e.path().strip_prefix(&root).unwrap();
648                let mut matched = false;
649                for matcher in &matchers {
650                    if matcher.0.is_match(relative) {
651                        matched = matcher.1 || return false;
652                    }
653                }
654                matched
655            })
656    }
657}
658
659fn compile<G:AsRef<str>>(incl: bool, glob: G) -> (GlobMatcher, bool) {
660    (
661        GlobBuilder::new(glob.as_ref()).literal_separator(true).build().unwrap().compile_matcher(),
662        incl
663    )
664}
665
666impl DirContent<Unit> {
667    fn iter(&self) -> Box<dyn Iterator<Item=Unit>> {
668        Box::new(self.walkdir().map(|e|
669            if e.file_type().is_dir() {
670                Unit::Dir( Dir { path: e.path().to_owned() })
671            } else {
672                Unit::File( File { path: e.path().to_owned() })
673            }
674        ))
675    }
676}
677
678impl DirContent<Dir> {
679    fn iter(&self) -> Box<dyn Iterator<Item=Dir>> {
680        Box::new(self.walkdir().filter_map(|e|
681            if e.file_type().is_dir() {
682                Some(Dir { path: e.path().to_owned() })
683            } else {
684                None
685            }
686        ))
687    }
688}
689
690impl DirContent<File> {
691    fn iter(&self) -> Box<dyn Iterator<Item=File>> {
692        Box::new(self.walkdir().filter_map(|e|
693            if e.file_type().is_file() {
694                Some(File { path: e.path().to_owned() })
695            } else {
696                None
697            }
698        ))
699    }
700}
701
702impl<T> AsRef<DirContent<T>> for DirContent<T> {
703    fn as_ref(&self) -> &DirContent<T> {
704        self
705    }
706}
707
708impl IntoIterator for DirContent<Unit> {
709    type Item = Unit;
710    type IntoIter = Box<dyn Iterator<Item=Self::Item>>;
711
712    fn into_iter(self) -> Self::IntoIter {
713        self.iter()
714    }
715}
716
717impl IntoIterator for DirContent<Dir> {
718    type Item = Dir;
719    type IntoIter = Box<dyn Iterator<Item=Self::Item>>;
720
721    fn into_iter(self) -> Self::IntoIter {
722        self.iter()
723    }
724}
725
726impl IntoIterator for DirContent<File> {
727    type Item = File;
728    type IntoIter = Box<dyn Iterator<Item=Self::Item>>;
729
730    fn into_iter(self) -> Self::IntoIter {
731        self.iter()
732    }
733}
734
735impl Resource for DirContent<Dir> {
736    fn timestamp(&self) -> Option<SystemTime> {
737        super::res::timestamp(self.iter())
738    }
739}
740
741impl Resource for DirContent<File> {
742    fn timestamp(&self) -> Option<SystemTime> {
743        super::res::timestamp(self.iter())
744    }
745}
746
747impl Resource for DirContent<Unit> {
748    fn timestamp(&self) -> Option<SystemTime> {
749        super::res::timestamp(self.iter())
750    }
751}