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 +++++++++++++++++++++++++++- tests/cpfile/driver.cxx | 2 +- tests/mventry/buildfile | 7 ++ tests/mventry/driver.cxx | 49 ++++++++++ tests/mventry/testscript | 245 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 523 insertions(+), 9 deletions(-) create mode 100644 tests/mventry/buildfile create mode 100644 tests/mventry/driver.cxx create mode 100644 tests/mventry/testscript 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; } diff --git a/tests/cpfile/driver.cxx b/tests/cpfile/driver.cxx index 732b364..5b8f366 100644 --- a/tests/cpfile/driver.cxx +++ b/tests/cpfile/driver.cxx @@ -110,7 +110,7 @@ main () dir_path sd (td / dir_path ("sub")); assert (try_mkdir (sd) == mkdir_status::success); - cpfile (from, sd, cpflags::none); + cpfile_into (from, sd, cpflags::none); assert (from_file (sd / path ("from")) == text1); diff --git a/tests/mventry/buildfile b/tests/mventry/buildfile new file mode 100644 index 0000000..868a2f6 --- /dev/null +++ b/tests/mventry/buildfile @@ -0,0 +1,7 @@ +# file : tests/mventry/buildfile +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +exe{driver}: cxx{driver} ../../butl/lib{butl} test{testscript} + +include ../../butl/ diff --git a/tests/mventry/driver.cxx b/tests/mventry/driver.cxx new file mode 100644 index 0000000..cf6099c --- /dev/null +++ b/tests/mventry/driver.cxx @@ -0,0 +1,49 @@ +// file : tests/mventry/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include +#include + +#include +#include // operator<<(ostream, exception) +#include + +using namespace std; +using namespace butl; + +// Usage: argv[0] +// +// Rename a file, directory or symlink or move it to the specified directory. +// For the later case the destination path must have a trailing directory +// separator. If succeed then exits with the zero code, otherwise prints the +// error descriptions and exits with the one code. +// +int +main (int argc, const char* argv[]) +try +{ + assert (argc == 3); + + path from (argv[1]); + path to (argv[2]); + + cpflags fl (cpflags::overwrite_permissions | cpflags::overwrite_content); + + if (to.to_directory ()) + mventry_into (from, path_cast (move (to)), fl); + else + mventry (from, to, fl); + + return 0; +} +catch (const invalid_path& e) +{ + cerr << e << ": " << e.path << endl; + return 1; +} +catch (const system_error& e) +{ + cerr << e << endl; + return 1; +} diff --git a/tests/mventry/testscript b/tests/mventry/testscript new file mode 100644 index 0000000..0a0aaa0 --- /dev/null +++ b/tests/mventry/testscript @@ -0,0 +1,245 @@ +# file : tests/mventry/testscript +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +: file +: +{ + : non-existing + : + $* a b 2>- == 1 + + : over + : + { + : non-existing + : + echo 'foo' >=a &!a; + $* a b &b; + cat b >'foo'; + test -f a == 1 + + : existing-file + : + echo 'foo' >=a &!a; + echo 'bar' >=b; + $* a b; + cat b >'foo'; + test -f a == 1 + + : existing-dir + : + echo 'foo' >=a; + mkdir b; + $* a b 2>- == 1 + } + + : to-dir + : + echo 'foo' >=a &!a; + mkdir b; + $* a b/ &b/a; + cat b/a >'foo'; + test -f a == 1 +} + +: dir +: +{ + : over + { + : non-existing + : + mkdir -p a/b &!a/b/ &!a/; + echo 'foo' >=a/c &!a/c; + $* a b &b/ &b/c &b/b/; + cat b/c >'foo'; + test -d b/b; + test -d a == 1 + + : empty-dir + : + mkdir -p a/b &!a/b/ &!a/; + echo 'foo' >=a/c &!a/c; + mkdir b; + $* a b &b/c &b/b/; + cat b/c >'foo'; + test -d b/b; + test -d a == 1 + + : non-empty-dir + : + mkdir -p a/b; + mkdir -p b/d; + $* a b 2>- == 1 + + : existing-file + : + mkdir a; + touch b; + $* a b 2>- == 1 + } + + : to-dir + : + mkdir a &!a/; + mkdir b; + $* a b/ &b/a/; + test -d b/a; + test -f a == 1 +} + +: symlink +: +: If we are not cross-testing let's test renaming symlynks from and over. On +: Windows that involves mklink command usability test. If we fail to create a +: trial link (say because we are running non-administrative console), then the +: test group will be silently skipped. +: +if ($test.target == $build.host) +{ + +if ($cxx.target.class != 'windows') + lns = ln -s a b + else + echo 'yes' >=a + if cmd /C 'mklink b a' >- 2>- &?b && cat b >'yes' + lns = cmd /C 'mklink b a' >- + end + end + + if! $empty($lns) + { + : file + : + { + : from + : + : Make sure that if source is a symlink it refers the same target after + : rename. + : + echo 'foo' >=a; + $lns; + $* b c &c; + test -f a; + test -f b == 1; + echo 'bar' >=a; + cat c >'bar' + + : to + : + : Make sure that if destination is a symlink it is get overwritten and it's + : target stays intact. + : + echo 'foo' >=a; + $lns &b; + echo 'bar' >=c &!c; + $* c b; + cat a >'foo'; + test -f c == 1; + echo 'baz' >=a; + cat b >'bar' + + : over-existing-dir + : + echo 'foo' >=a; + $lns &b; + mkdir c; + $* b c 2>- == 1 + } + + : dir + : + { + : from + : + : Make sure that if source is a symlink it refers the same target after + : rename. + : + mkdir -p a; + $lns; + $* b c &c; + touch a/b; + test -f c/b; + test -d b == 1 + + : to + : + : Make sure that if destination is a symlink it is get overwritten and + : it's target stays intact. + : + mkdir -p a; + $lns; + echo 'foo' >=c &!c; + $* c b &b; + cat b >'foo'; + test -d a; + test -f c == 1 + + : over-existing-dir + : + mkdir a; + $lns &b; + mkdir c; + $* b c 2>- == 1 + } + } +} + +: different-fs +: +: Note that nested tests may fail for cross-testing as the directory path will +: unlikelly be usable on both host (build2 driver) and target (test driver) +: platforms. +: +if! $empty($config.libbutl.test.rename.dir) +{ + wd = $config.libbutl.test.rename.dir/libbutl-rename + +rm -r -f $wd + +mkdir $wd + + : file + : + { + : over-non-existing + : + { + wd = "$wd/$@"; + mkdir -p $wd; + b = $wd/b; + + echo 'foo' >=a &!a; + $* a $b; + cat $b >'foo'; + test -f a == 1 + } + + : over-file + : + { + wd = "$wd/$@"; + mkdir -p $wd; + b = $wd/b; + + touch $b; + echo 'foo' >=a &!a; + $* a $b; + cat $b >'foo'; + test -f a == 1 + } + } + + : dir + : + : Test that renaming directory to different fs/drive expectedly fails. + : + { + wd = "$wd/$@"; + mkdir -p $wd; + b = $wd/b; + + mkdir a; + $* a $b 2>- == 1 + } + + -rm -r -f $wd +} -- cgit v1.1