From 8b5e3e0a8f9ec8852cf2f15dab53bfa4436bea87 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Thu, 9 Mar 2017 02:24:54 +0300 Subject: Add mventry(), mvfile() and mvdir() --- butl/filesystem | 81 ++++++++++++++++++++++++++-- butl/filesystem.cxx | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 221 insertions(+), 8 deletions(-) (limited to 'butl') diff --git a/butl/filesystem b/butl/filesystem index 336233c..4e0395c 100644 --- a/butl/filesystem +++ b/butl/filesystem @@ -243,12 +243,87 @@ namespace butl LIBBUTL_EXPORT void cpfile (const path& from, const path& to, cpflags = cpflags::none); - // Copy a regular file to an existing directory. + // Copy a regular file into (inside) an existing directory. // inline void - cpfile (const path& from, const dir_path& to, cpflags fl = cpflags::none) + cpfile_into (const path& from, + const dir_path& into, + cpflags fl = cpflags::none) { - cpfile (from, to / from.leaf (), fl); + cpfile (from, into / from.leaf (), fl); + } + + // Rename a filesystem entry (file, symlink, or directory). Throw + // std::system_error on failure. + // + // If the source path refers to a directory, then the destination path must + // either not exist, or refer to an empty directory. If the source path + // refers to an entry that is not a directory, then the destination path must + // not exist or not refer to a directory. + // + // If the source path refers to a symlink, then the link is renamed. If the + // destination path refers to a symlink, then the link will be overwritten. + // + // If the source and destination paths are on different file systems (or + // different drives on Windows) and the underlying OS does not support move + // for the source entry, then fail unless the source paths refers to a file + // or a file symlink. In this case fall back to copying the source file + // (content, permissions, access and modification times) and removing the + // source entry afterwards. + // + // Note that the operation is atomic only on POSIX, only if source and + // destination paths are on the same file system, and only if the + // overwrite_content flag is specified. + // + LIBBUTL_EXPORT void + mventry (const path& from, + const path& to, + cpflags = cpflags::overwrite_permissions); + + // Move a filesystem entry into (inside) an existing directory. + // + inline void + mventry_into (const path& from, + const dir_path& into, + cpflags f = cpflags::overwrite_permissions) + { + mventry (from, into / from.leaf (), f); + } + + // Raname file or file symlink. + // + inline void + mvfile (const path& from, + const path& to, + cpflags f = cpflags::overwrite_permissions) + { + mventry (from, to, f); + } + + inline void + mvfile_into (const path& from, + const dir_path& into, + cpflags f = cpflags::overwrite_permissions) + { + mventry_into (from, into, f); + } + + // Raname directory or directory symlink. + // + inline void + mvdir (const dir_path& from, + const dir_path& to, + cpflags f = cpflags::overwrite_permissions) + { + mventry (from, to, f); + } + + inline void + mvdir_into (const path& from, + const dir_path& into, + cpflags f = cpflags::overwrite_permissions) + { + mventry_into (from, into, f); } // Return timestamp_nonexistent if the entry at the specified path diff --git a/butl/filesystem.cxx b/butl/filesystem.cxx index bd03d7a..e881417 100644 --- a/butl/filesystem.cxx +++ b/butl/filesystem.cxx @@ -5,8 +5,10 @@ #include #ifndef _WIN32 +# include // rename() # include // struct dirent, *dir() # include // symlink(), link(), stat(), rmdir(), unlink() +# include // utimes() # include // stat # include // stat(), lstat(), S_I*, mkdir(), chmod() #else @@ -386,28 +388,164 @@ namespace butl // template inline constexpr auto - nsec (const S* s, bool) -> decltype(s->st_mtim.tv_nsec) + mnsec (const S* s, bool) -> decltype(s->st_mtim.tv_nsec) { return s->st_mtim.tv_nsec; // POSIX (GNU/Linux, Solaris). } template inline constexpr auto - nsec (const S* s, int) -> decltype(s->st_mtimespec.tv_nsec) + mnsec (const S* s, int) -> decltype(s->st_mtimespec.tv_nsec) { return s->st_mtimespec.tv_nsec; // *BSD, MacOS. } template inline constexpr auto - nsec (const S* s, float) -> decltype(s->st_mtime_n) + mnsec (const S* s, float) -> decltype(s->st_mtime_n) { return s->st_mtime_n; // AIX 5.2 and later. } template inline constexpr int - nsec (...) {return 0;} + mnsec (...) {return 0;} + + template + inline constexpr auto + ansec (const S* s, bool) -> decltype(s->st_atim.tv_nsec) + { + return s->st_atim.tv_nsec; // POSIX (GNU/Linux, Solaris). + } + + template + inline constexpr auto + ansec (const S* s, int) -> decltype(s->st_atimespec.tv_nsec) + { + return s->st_atimespec.tv_nsec; // *BSD, MacOS. + } + + template + inline constexpr auto + ansec (const S* s, float) -> decltype(s->st_atime_n) + { + return s->st_atime_n; // AIX 5.2 and later. + } + + template + inline constexpr int + ansec (...) {return 0;} + + void + mventry (const path& from, const path& to, cpflags fl) + { + assert ((fl & cpflags::overwrite_permissions) == + cpflags::overwrite_permissions); + + bool ovr ((fl & cpflags::overwrite_content) == cpflags::overwrite_content); + + const char* f (from.string ().c_str ()); + const char* t (to.string ().c_str ()); + +#ifndef _WIN32 + + if (!ovr && path_entry (to).first) + throw system_error (EEXIST, system_category ()); + + if (::rename (f, t) == 0) // POSIX implementation. + return; + + // If source and destination paths are on different file systems we need to + // move the file ourselves. + // + if (errno != EXDEV) + throw system_error (errno, system_category ()); + + // Note that cpfile() follows symlinks, so we need to remove destination if + // exists. + // + try_rmfile (to); + + // Note that permissions are copied unconditionally to a new file. + // + cpfile (from, to, cpflags::none); + + // Copy file access and modification times. + // + struct stat s; + if (stat (f, &s) != 0) + throw system_error (errno, system_category ()); + + 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 system_error (errno, system_category ()); + + // Finally, remove the source file. + // + try_rmfile (from); + +#else + + // While ::rename() is present on Windows, it is not POSIX but ISO C + // implementation, that doesn't fit our needs well. + // + auto te (path_entry (to)); + + // Note that it would be nicer to just pass standard error codes to + // system_error ctors below, but their error descriptions horrifies for + // msvcrt. Actually they just don't match the semantics. The resulted error + // description is also not ideal (see below). + // + if (!ovr && te.first) + throw system_error (EEXIST, system_category (), "file exists"); + + bool td (te.first && te.second == entry_type::directory); + + auto fe (path_entry (from)); + bool fd (fe.first && fe.second == entry_type::directory); + + // If source and destination filesystem entries exist, they both must be + // either directories or not directories. + // + if (fe.first && te.first && fd != td) + throw system_error (EIO, system_category (), "not a directory"); + + DWORD mfl (fd ? 0 : (MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING)); + + if (MoveFileExA (f, t, mfl)) + return; + + // If the destination already exists, then MoveFileExA() succeeds only if + // it is a regular file or a symlink. Lets also support an empty directory + // special case to comply with POSIX. If the destination is an empty + // directory we will just remove it and retry the move operation. + // + // Note that under Wine we endup with ERROR_ACCESS_DENIED error code in + // that case, and with ERROR_ALREADY_EXISTS when run natively. + // + 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)) + return; + + // @@ The exception description will look like: + // + // file not found. : Access denied + // + // Probably need to consider such discriptions for sanitizing in + // operator<<(ostream,exception). + // + string e (win32::error_msg (ec)); + throw system_error (EIO, system_category (), e); + +#endif + } timestamp file_mtime (const char* p) @@ -429,7 +567,7 @@ namespace butl return S_ISREG (s.st_mode) ? system_clock::from_time_t (s.st_mtime) + chrono::duration_cast ( - chrono::nanoseconds (nsec (&s, true))) + chrono::nanoseconds (mnsec (&s, true))) : timestamp_nonexistent; } -- cgit v1.1