diff options
-rw-r--r-- | libbutl/filesystem.cxx | 203 | ||||
-rw-r--r-- | libbutl/filesystem.mxx | 138 | ||||
-rw-r--r-- | tests/entry-time/buildfile | 8 | ||||
-rw-r--r-- | tests/entry-time/driver.cxx | 151 | ||||
-rw-r--r-- | tests/entry-time/testscript | 63 |
5 files changed, 514 insertions, 49 deletions
diff --git a/libbutl/filesystem.cxx b/libbutl/filesystem.cxx index bd91c07..a6c7ae8 100644 --- a/libbutl/filesystem.cxx +++ b/libbutl/filesystem.cxx @@ -484,6 +484,9 @@ namespace butl cpflags::overwrite_permissions) path_permissions (to, perm); + if ((fl & cpflags::copy_timestamps) == cpflags::copy_timestamps) + file_time (to, file_time (from)); + rm.cancel (); } @@ -580,18 +583,7 @@ namespace butl // Copy file access and modification times. // - struct stat s; - if (stat (f, &s) != 0) - throw_generic_error (errno); - - timeval times[2]; - times[0].tv_sec = s.st_atime; - times[0].tv_usec = ansec<struct stat> (&s, true) / 1000; - times[1].tv_sec = s.st_mtime; - times[1].tv_usec = mnsec<struct stat> (&s, true) / 1000; - - if (utimes (t, times) != 0) - throw_generic_error (errno); + file_time (t, file_time (f)); // Finally, remove the source file. // @@ -633,8 +625,8 @@ namespace butl // DWORD ec (GetLastError ()); if ((ec == ERROR_ALREADY_EXISTS || ec == ERROR_ACCESS_DENIED) && td && - try_rmdir (path_cast<dir_path> (to)) != rmdir_status::not_empty && - MoveFileExA (f, t, mfl)) + try_rmdir (path_cast<dir_path> (to)) != rmdir_status::not_empty && + MoveFileExA (f, t, mfl)) return; throw_system_error (ec); @@ -642,25 +634,34 @@ namespace butl #endif } - timestamp - file_mtime (const char* p) + // Return the modification and access times of a regular file or directory. + // + static entry_time + entry_tm (const char* p, bool dir) { #ifndef _WIN32 + struct stat s; if (stat (p, &s) != 0) { if (errno == ENOENT || errno == ENOTDIR) - return timestamp_nonexistent; + return {timestamp_nonexistent, timestamp_nonexistent}; else throw_generic_error (errno); } - if (!S_ISREG (s.st_mode)) - return timestamp_nonexistent; + if (dir ? !S_ISDIR (s.st_mode) : !S_ISREG (s.st_mode)) + return {timestamp_nonexistent, timestamp_nonexistent}; + + auto tm = [] (time_t sec, uint64_t nsec) -> timestamp + { + return system_clock::from_time_t (sec) + + chrono::duration_cast<duration> (chrono::nanoseconds (nsec)); + }; + + return {tm (s.st_mtime, mnsec<struct stat> (&s, true)), + tm (s.st_atime, ansec<struct stat> (&s, true))}; - return system_clock::from_time_t (s.st_mtime) + - chrono::duration_cast<duration> ( - chrono::nanoseconds (mnsec<struct stat> (&s, true))); #else WIN32_FILE_ATTRIBUTE_DATA s; @@ -675,32 +676,162 @@ namespace butl ec == ERROR_INVALID_DRIVE || ec == ERROR_BAD_PATHNAME || ec == ERROR_BAD_NETPATH) - return timestamp_nonexistent; + return {timestamp_nonexistent, timestamp_nonexistent}; throw_system_error (ec); } - if ((s.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) - return timestamp_nonexistent; + if ((s.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != + (dir ? FILE_ATTRIBUTE_DIRECTORY : 0)) + return {timestamp_nonexistent, timestamp_nonexistent}; + + auto tm = [] (const FILETIME& t) -> timestamp + { + // Time in FILETIME is in 100 nanosecond "ticks" since "Windows epoch" + // (1601-01-01T00:00:00Z). To convert it to "UNIX epoch" + // (1970-01-01T00:00:00Z) we need to subtract 11644473600 seconds. + // + uint64_t nsec ((static_cast<uint64_t> (t.dwHighDateTime) << 32) | + t.dwLowDateTime); + + nsec -= 11644473600ULL * 10000000; // Now in UNIX epoch. + nsec *= 100; // Now in nanoseconds. + + return timestamp ( + chrono::duration_cast<duration> (chrono::nanoseconds (nsec))); + }; + + return {tm (s.ftLastWriteTime), tm (s.ftLastAccessTime)}; + +#endif + } + + entry_time + file_time (const char* p) + { + return entry_tm (p, false); + } + + entry_time + dir_time (const char* p) + { + return entry_tm (p, true); + } - // Time in FILETIME is in 100 nanosecond "ticks" since "Windows epoch" - // (1601-01-01T00:00:00Z). To convert it to "UNIX epoch" - // (1970-01-01T00:00:00Z) we need to subtract 11644473600 seconds. + // Set the modification and access times for a regular file or directory. + // + static void + entry_tm (const char* p, const entry_time& t, bool dir) + { +#ifndef _WIN32 + + struct stat s; + if (stat (p, &s) != 0) + throw_generic_error (errno); + + // If the entry is of the wrong type, then let's pretend that it doesn't + // exists. In other words, the entry of the required type doesn't exist. + // + if (dir ? !S_ISDIR (s.st_mode) : !S_ISREG (s.st_mode)) + return throw_generic_error (ENOENT); + + // Note: timeval has the microsecond resolution. // - const FILETIME& t (s.ftLastWriteTime); + auto tm = [] (timestamp t, long sec, long nsec) -> timeval + { + if (t == timestamp_nonexistent) + return {sec, nsec / 1000}; // Filesystem entry current time. + + uint64_t msec (chrono::duration_cast<chrono::microseconds> ( + t.time_since_epoch ()).count ()); + + return {static_cast<long> (msec / 1000000), // Seconds. + static_cast<long> (msec % 1000000)}; // Microseconds. + }; + + timeval times[2]; + times[0] = tm (t.access, s.st_atime, ansec<struct stat> (&s, true)); + times[1] = tm (t.modification, s.st_mtime, mnsec<struct stat> (&s, true)); - uint64_t ns ((static_cast<uint64_t> (t.dwHighDateTime) << 32) | - t.dwLowDateTime); + if (utimes (p, times) != 0) + throw_generic_error (errno); + +#else - ns -= 11644473600ULL * 10000000; // Now in UNIX epoch. - ns *= 100; // Now in nanoseconds. + DWORD attr (GetFileAttributes (p)); + + if (attr == INVALID_FILE_ATTRIBUTES) + throw_system_error (GetLastError ()); + + // If the entry is of the wrong type, then let's pretend that it doesn't + // exists. + // + if ((attr & FILE_ATTRIBUTE_DIRECTORY) != + (dir ? FILE_ATTRIBUTE_DIRECTORY : 0)) + return throw_generic_error (ENOENT); + + HANDLE h (CreateFile (p, + FILE_WRITE_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + OPEN_EXISTING, + dir ? FILE_FLAG_BACKUP_SEMANTICS : 0, + NULL)); + + if (h == INVALID_HANDLE_VALUE) + throw_system_error (GetLastError ()); + + unique_ptr<HANDLE, void (*)(HANDLE*)> deleter ( + &h, [] (HANDLE* h) + { + bool r (CloseHandle (*h)); + + // The valid file handle that has no IO operations being performed on + // it should close successfully, unless something is severely damaged. + // + assert (r); + }); + + auto tm = [] (timestamp t, FILETIME& ft) -> const FILETIME* + { + if (t == timestamp_nonexistent) + return NULL; + + // Time in FILETIME is in 100 nanosecond "ticks" since "Windows epoch" + // (1601-01-01T00:00:00Z). To convert "UNIX epoch" + // (1970-01-01T00:00:00Z) to it we need to add 11644473600 seconds. + // + uint64_t ticks (chrono::duration_cast<chrono::nanoseconds> ( + t.time_since_epoch ()).count ()); + + ticks /= 100; // Now in 100 nanosecond "ticks". + ticks += 11644473600ULL * 10000000; // Now in "Windows epoch". + + ft.dwHighDateTime = (ticks >> 32) & 0xFFFFFFFF; + ft.dwLowDateTime = ticks & 0xFFFFFFFF; + return &ft; + }; + + FILETIME at; + FILETIME mt; + if (!SetFileTime (h, NULL, tm (t.access, at), tm (t.modification, mt))) + throw_system_error (GetLastError ()); - return timestamp ( - chrono::duration_cast<duration> ( - chrono::nanoseconds (ns))); #endif } + void + file_time (const char* p, const entry_time& t) + { + entry_tm (p, t, false); + } + + void + dir_time (const char* p, const entry_time& t) + { + entry_tm (p, t, true); + } + permissions path_permissions (const path& p) { diff --git a/libbutl/filesystem.mxx b/libbutl/filesystem.mxx index a87e57b..7f93739 100644 --- a/libbutl/filesystem.mxx +++ b/libbutl/filesystem.mxx @@ -272,8 +272,10 @@ LIBBUTL_MODEXPORT namespace butl // enum class cpflags: std::uint16_t { - overwrite_content = 0x1, - overwrite_permissions = 0x2, + overwrite_content = 0x1, // Overwrite content of destination. + overwrite_permissions = 0x2, // Overwrite permissions of destination. + + copy_timestamps = 0x4, // Copy timestamps from source. none = 0 }; @@ -283,11 +285,11 @@ LIBBUTL_MODEXPORT namespace butl inline cpflags operator&= (cpflags&, cpflags); inline cpflags operator|= (cpflags&, cpflags); - // Copy a regular file, including its permissions. Throw std::system_error - // on failure. Fail if the destination file exists and the overwrite_content - // flag is not set. Leave permissions of an existing destination file intact - // unless the overwrite_permissions flag is set. Delete incomplete copies - // before throwing. + // Copy a regular file, including its permissions, and optionally timestamps. + // Throw std::system_error on failure. Fail if the destination file exists + // and the overwrite_content flag is not set. Leave permissions of an + // existing destination file intact unless the overwrite_permissions flag is + // set. Delete incomplete copies before throwing. // // Note that in case of overwriting, the existing destination file gets // truncated (not deleted) prior to being overwritten. As a side-effect, @@ -384,17 +386,127 @@ LIBBUTL_MODEXPORT namespace butl mventry_into (from, into, f); } - // Return timestamp_nonexistent if the entry at the specified path - // does not exist or is not a path. All other errors are reported - // by throwing std::system_error. Note that this function resolves - // symlinks. + struct entry_time + { + timestamp modification; + timestamp access; + }; + + // Return timestamp_nonexistent for the modification and access times if the + // entry at the specified path does not exist or is not a regular file. All + // other errors are reported by throwing std::system_error. Note that these + // functions resolves symlinks. // - LIBBUTL_SYMEXPORT timestamp - file_mtime (const char*); + LIBBUTL_SYMEXPORT entry_time + file_time (const char*); + + inline entry_time + file_time (const path& p) {return file_time (p.string ().c_str ());} + + inline timestamp + file_mtime (const char* p) {return file_time (p).modification;} inline timestamp file_mtime (const path& p) {return file_mtime (p.string ().c_str ());} + inline timestamp + file_atime (const char* p) {return file_time (p).access;} + + inline timestamp + file_atime (const path& p) {return file_atime (p.string ().c_str ());} + + // As above but return the directory times. + // + LIBBUTL_SYMEXPORT entry_time + dir_time (const char*); + + inline entry_time + dir_time (const dir_path& p) {return dir_time (p.string ().c_str ());} + + inline timestamp + dir_mtime (const char* p) {return dir_time (p).modification;} + + inline timestamp + dir_mtime (const dir_path& p) {return dir_mtime (p.string ().c_str ());} + + inline timestamp + dir_atime (const char* p) {return dir_time (p).access;} + + inline timestamp + dir_atime (const dir_path& p) {return dir_atime (p.string ().c_str ());} + + // Set a regular file modification and access times. If a time value is + // timestamp_nonexistent then it is left unchanged. All errors are reported + // by throwing std::system_error. + // + LIBBUTL_SYMEXPORT void + file_time (const char*, const entry_time&); + + inline void + file_time (const path& p, const entry_time& t) + { + return file_time (p.string ().c_str (), t); + } + + inline void + file_mtime (const char* p, timestamp t) + { + return file_time (p, {t, timestamp_nonexistent}); + } + + inline void + file_mtime (const path& p, timestamp t) + { + return file_mtime (p.string ().c_str (), t); + } + + inline void + file_atime (const char* p, timestamp t) + { + return file_time (p, {timestamp_nonexistent, t}); + } + + inline void + file_atime (const path& p, timestamp t) + { + return file_atime (p.string ().c_str (), t); + } + + // As above but set the directory times. + // + LIBBUTL_SYMEXPORT void + dir_time (const char*, const entry_time&); + + inline void + dir_time (const dir_path& p, const entry_time& t) + { + return dir_time (p.string ().c_str (), t); + } + + inline void + dir_mtime (const char* p, timestamp t) + { + return dir_time (p, {t, timestamp_nonexistent}); + } + + inline void + dir_mtime (const dir_path& p, timestamp t) + { + return dir_mtime (p.string ().c_str (), t); + } + + inline void + dir_atime (const char* p, timestamp t) + { + return dir_time (p, {timestamp_nonexistent, t}); + } + + inline void + dir_atime (const dir_path& p, timestamp t) + { + return dir_atime (p.string ().c_str (), t); + } + // Path permissions. // enum class permissions: std::uint16_t diff --git a/tests/entry-time/buildfile b/tests/entry-time/buildfile new file mode 100644 index 0000000..7219149 --- /dev/null +++ b/tests/entry-time/buildfile @@ -0,0 +1,8 @@ +# file : tests/entry-time/buildfile +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +import libs = libbutl%lib{butl} +libs += $stdmod_lib + +exe{driver}: {hxx cxx}{*} $libs test{testscript} diff --git a/tests/entry-time/driver.cxx b/tests/entry-time/driver.cxx new file mode 100644 index 0000000..91e9bbc --- /dev/null +++ b/tests/entry-time/driver.cxx @@ -0,0 +1,151 @@ +// file : tests/entry-time/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <cassert> + +#ifndef __cpp_lib_modules +#include <string> +#include <chrono> +#include <iostream> +#endif + +// Other includes. + +#ifdef __cpp_modules +#ifdef __cpp_lib_modules +import std.core; +import std.io; +#endif +import butl.path; +import butl.filesystem; + +import butl.optional; // @@ MOD Clang should not be necessary. +#else +#include <libbutl/path.mxx> +#include <libbutl/optional.mxx> +#include <libbutl/timestamp.mxx> +#include <libbutl/filesystem.mxx> +#endif + +using namespace std; +using namespace butl; + +// Usage: argv[0] (-p|-s <time>) (-f|-d) (-m|-a) <path> +// +// Prints or sets the modification or access time for the specified filesystem +// entry. +// +// -p +// Print the filesystem entry time as as a number of milliseconds passed +// since UNIX epoch. +// +// -s +// Set the filesystem entry time that is represented as a number of +// milliseconds passed since UNIX epoch. +// +// -f +// The specified path is a file. +// +// -d +// The specified path is a directory. +// +// -m +// Print or set the filesystem entry modification time. +// +// -a +// Print or set the filesystem entry access time. +// +int +main (int argc, const char* argv[]) +{ + using butl::optional; + + optional<bool> set; + optional<bool> dir; + optional<bool> mod; + optional<path> p; + timestamp tm; + + assert (argc > 0); + + for (int i (1); i != argc; ++i) + { + string v (argv[i]); + + if (v == "-p") + { + assert (!set); + set = false; + } + else if (v == "-s") + { + assert (!set); + set = true; + + assert (i + 1 != argc); + tm = timestamp (chrono::duration_cast<duration> ( + chrono::milliseconds (stoull (argv[++i])))); + } + else if (v == "-f") + { + assert (!dir); + dir = false; + } + else if (v == "-d") + { + assert (!dir); + dir = true; + } + else if (v == "-m") + { + assert (!mod); + mod = true; + } + else if (v == "-a") + { + assert (!mod); + mod = false; + } + else + { + assert (set && dir && mod && !p); + p = path (move (v)); + } + } + + assert (set); + assert (dir); + assert (mod); + assert (p); + + if (*set) + { + if (*dir) + { + if (*mod) + dir_mtime (path_cast<dir_path> (*p), tm); + else + dir_atime (path_cast<dir_path> (*p), tm); + } + else + { + if (*mod) + file_mtime (*p, tm); + else + file_atime (*p, tm); + } + } + else + { + if (*dir) + tm = *mod + ? dir_mtime (path_cast<dir_path> (*p)) + : dir_atime (path_cast<dir_path> (*p)); + else + tm = *mod ? file_mtime (*p) : file_atime (*p); + + cout << chrono::duration_cast<chrono::milliseconds> ( + tm.time_since_epoch ()).count () << endl; + } +} diff --git a/tests/entry-time/testscript b/tests/entry-time/testscript new file mode 100644 index 0000000..933ae3a --- /dev/null +++ b/tests/entry-time/testscript @@ -0,0 +1,63 @@ +# file : tests/entry-time/testscript +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +# Filesystem entry times resolution may vary across OSes and underlying APIs, +# so the timestamp set for the enty may differ a bit from the one we get for +# it afterwards. Thus we will compare with the milliseconds resolution which +# seems to be reasonable to expect being supported on the platforms we are +# testing on. +# +t = 1521456316789 # Some time point since epoch expressed in milliseconds. + +: modification +: +{ + test.options += -m + + : dir + : + { + test.options += -d; + + mkdir d; + $* -s $t d; + $* -p d >"$t" + } + + : file + : + { + test.options += -f; + + touch f; + $* -s $t f; + $* -p f >"$t" + } +} + +: access +: +{ + test.options += -a + + : dir + : + { + test.options += -d; + + mkdir d; + $* -s $t d; + $* -p d >"$t" + } + + : file + : + { + test.options += -f; + + touch f; + $* -s $t f; + $* -p f >"$t" + } +} |