From da7be5f8c18879d1ca21f76470fc83d2c5cbd1c4 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Sun, 18 Mar 2018 00:31:56 +0300 Subject: Add support for copying file timestamps by cpfile() --- libbutl/filesystem.cxx | 203 ++++++++++++++++++++++++++++++++++++-------- libbutl/filesystem.mxx | 138 +++++++++++++++++++++++++++--- tests/entry-time/buildfile | 8 ++ tests/entry-time/driver.cxx | 151 ++++++++++++++++++++++++++++++++ tests/entry-time/testscript | 63 ++++++++++++++ 5 files changed, 514 insertions(+), 49 deletions(-) create mode 100644 tests/entry-time/buildfile create mode 100644 tests/entry-time/driver.cxx create mode 100644 tests/entry-time/testscript 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 (&s, true) / 1000; - times[1].tv_sec = s.st_mtime; - times[1].tv_usec = mnsec (&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 (to)) != rmdir_status::not_empty && - MoveFileExA (f, t, mfl)) + try_rmdir (path_cast (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 (chrono::nanoseconds (nsec)); + }; + + return {tm (s.st_mtime, mnsec (&s, true)), + tm (s.st_atime, ansec (&s, true))}; - return system_clock::from_time_t (s.st_mtime) + - chrono::duration_cast ( - chrono::nanoseconds (mnsec (&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 (t.dwHighDateTime) << 32) | + t.dwLowDateTime); + + nsec -= 11644473600ULL * 10000000; // Now in UNIX epoch. + nsec *= 100; // Now in nanoseconds. + + return timestamp ( + chrono::duration_cast (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 ( + t.time_since_epoch ()).count ()); + + return {static_cast (msec / 1000000), // Seconds. + static_cast (msec % 1000000)}; // Microseconds. + }; + + timeval times[2]; + times[0] = tm (t.access, s.st_atime, ansec (&s, true)); + times[1] = tm (t.modification, s.st_mtime, mnsec (&s, true)); - uint64_t ns ((static_cast (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 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 ( + 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 ( - 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 + +#ifndef __cpp_lib_modules +#include +#include +#include +#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 +#include +#include +#include +#endif + +using namespace std; +using namespace butl; + +// Usage: argv[0] (-p|-s