While I am working on some code to copy files from one place to
another, I was looking at preserving the metadata. Something
equivalent to cp -a
in shell. In Rust, std::fs::copy
does preserve
the attributes, but unfortunately not the file modification dates so I
needed to explore what are the options.
While I will be mentioning Rust, this is not a Rust specific issue. However it is Linux oriented.
On Linux (and to some extent other UNIX-like), there are 4 timestamps:
mtime
is the modification date, atime
is the access date, ctime
the change date, and then btime
the file creation date. The b
stands for birth.
You can view these using the stat
command.
$ stat meson.build
File: meson.build
Size: 4616 Blocks: 16 IO Block: 4096 regular file
Device: 0,40 Inode: 290010300 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ hub) Gid: ( 1000/ hub)
Access: 2023-03-28 23:24:14.871754275 -0400
Modify: 2023-03-14 22:32:36.909471008 -0400
Change: 2023-03-14 22:32:36.909471008 -0400
Birth: 2023-03-14 22:32:36.909471008 -0400
$
Using the command touch
allow changing the modification date mtime
.
$ touch meson.build
$ stat meson.build
File: meson.build
Size: 4616 Blocks: 16 IO Block: 4096 regular file
Device: 0,40 Inode: 290010300 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ hub) Gid: ( 1000/ hub)
Access: 2023-03-29 11:20:20.635884381 -0400
Modify: 2023-03-29 11:20:20.635884381 -0400
Change: 2023-03-29 11:20:20.635884381 -0400
Birth: 2023-03-14 22:32:36.909471008 -0400
$
It also changed access atime
and changed ctime
. The latter is
changed because setting any of these attribue is a change. Timestamp
precision up to the nanosecond is supported since Linux 2.6, on some
filesystems (notably not ext2/ext3 and reiserfs) and this is
reflected here.
What does cp -a
do?
$ cp -a meson.build meson.build-copy
$ stat meson.build-copy
File: meson.build-copy
Size: 4616 Blocks: 16 IO Block: 4096 regular file
Device: 0,40 Inode: 291019850 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ hub) Gid: ( 1000/ hub)
Access: 2023-03-29 11:20:20.635884381 -0400
Modify: 2023-03-29 11:20:20.635884381 -0400
Change: 2023-03-29 12:42:37.084867384 -0400
Birth: 2023-03-29 12:42:37.080867326 -0400
While it does preserve the access and modification times, the birth and change time are new.
This mean that setting the creation date of the file is unecessary to
mimic cp -a
behaviour.
Underneath the stat
commands used stat()
from the libc, or more
likely the newer and Linux specific statx()
(Linux kernel 4.11 /
glibc 2.28).
stat()
allows you to get informations about a file, including
atime
, mtime
, and ctime
, which are the only one we care about
here. If you want to write portable code (ie not specific to modern
Linux), this is what you should use. In Rust, these are exposed
through the std::os::unix::fs::MetadataExt
trait that is, as its
path implies, UNIX specific. This mean (mostly) everything but
Windows. You can also use stat
from the nix
crate, which is a direct safe binding
on the libc function.
You can set the modification and access times using the utimes()
API, but it doesn’t provide nanosecond precision. The newer Linux
utimensat()
allow this precision. In Rust there is nothing on the
standard library to perform this, so you need to use the nix
crate
again, or if you prefer the unsafe libc
crate that exposes the libc
API.
Here is an example in Rust:
use nix::sys::stat::{stat, utimensat, UtimensatFlags};
use nix::sys::time::TimeSpec;
pub fn copy_utimes<P, Q>(from: P, to: Q) -> std::io::Result<()>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
let file_stat = stat(from.as_ref())?;
utimensat(
None,
to.as_ref(),
&TimeSpec::new(file_stat.st_atime, file_stat.st_atime_nsec),
&TimeSpec::new(file_stat.st_mtime, file_stat.st_mtime_nsec),
UtimensatFlags::NoFollowSymlink,
)?;
Ok(())
}
But what about the birth time btime
? You have to use statx()
, on
Linux 4.11 or later to get it. In Rust, statx
is not in the nix
crate. You can eventually get a SystemTime
with
std::fs::Metadata::created()
or call the unsafe libc::statx()
.
If you check the man page for inode
, you will learn that btime
not
returned by stat()
, since it was not historically present on UNIX,
that it is not supported by most Linux filesystems, and, that’s the
important detail, that it is not changed after being set at file
creation. And I couldn’t find any API that would allow to set it.
So to answer the question How to set the file creation date on Linux?: you can’t.
It took a bit of more research as to why this is not possible. Among
the reasons, the lack of support for the attribute in several Linux
filesystems, I found a thread for a patch
submission
to implement setting the birth time in the Linux kernel with
utimensat()
. The few arguments for the rejection are that birth time
is not user creation time, that it is useful for forensic, either for
post-mortem corruption analysis or compromise investigation. While it
is not full proof, its immutability has a certain value when
investigating and making it mutable would complicate things a lot in
the filesystem code to be safely implemented. Foreign virtual
filesystems (ie file sharing like CIFS) can use extended attributes to
do the job; it is what Samba does for its CIFS implementation.
Let’s copy mtime
and atime
and leave the rest alone. I should
investigate the extended attributes, but in that context it might not
be useful, and portability might be a concern.