aboutsummaryrefslogtreecommitdiff
path: root/butl
diff options
context:
space:
mode:
Diffstat (limited to 'butl')
-rw-r--r--butl/filesystem81
-rw-r--r--butl/filesystem.cxx148
2 files changed, 221 insertions, 8 deletions
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 <butl/filesystem>
#ifndef _WIN32
+# include <stdio.h> // rename()
# include <dirent.h> // struct dirent, *dir()
# include <unistd.h> // symlink(), link(), stat(), rmdir(), unlink()
+# include <sys/time.h> // utimes()
# include <sys/types.h> // stat
# include <sys/stat.h> // stat(), lstat(), S_I*, mkdir(), chmod()
#else
@@ -386,28 +388,164 @@ namespace butl
//
template <typename S>
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 <typename S>
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 <typename S>
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 <typename S>
inline constexpr int
- nsec (...) {return 0;}
+ mnsec (...) {return 0;}
+
+ template <typename S>
+ 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 <typename S>
+ inline constexpr auto
+ ansec (const S* s, int) -> decltype(s->st_atimespec.tv_nsec)
+ {
+ return s->st_atimespec.tv_nsec; // *BSD, MacOS.
+ }
+
+ template <typename S>
+ inline constexpr auto
+ ansec (const S* s, float) -> decltype(s->st_atime_n)
+ {
+ return s->st_atime_n; // AIX 5.2 and later.
+ }
+
+ template <typename S>
+ 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<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 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<dir_path> (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<duration> (
- chrono::nanoseconds (nsec<struct stat> (&s, true)))
+ chrono::nanoseconds (mnsec<struct stat> (&s, true)))
: timestamp_nonexistent;
}