Added support for copy_options::update_existing to copy_file.

This commit is contained in:
Andrey Semashev 2020-05-09 19:19:33 +03:00
parent 8c3ae354a0
commit ac02dbed2e
5 changed files with 167 additions and 26 deletions

View File

@ -775,7 +775,8 @@ nothing else.&quot;</p>
{
none = 0u,
skip_existing,
overwrite_existing
overwrite_existing,
update_existing
};
// Deprecated, use <a href="#copy_options">copy_options</a> instead
@ -3146,7 +3147,7 @@ void <a name="copy_file2">copy_file</a>(const path&amp; from, const path&amp; to
void <a name="copy_file3">copy_file</a>(const path&amp; from, const path&amp; to, <a href="#copy_option">copy_option</a> options);
void <a name="copy_file4">copy_file</a>(const path&amp; from, const path&amp; to, <a href="#copy_option">copy_option</a> options, system::error_code&amp; ec);</pre>
<blockquote>
<p><i>Precondition:</i> At most one of <code>copy_options::skip_existing</code> or <code>copy_options::overwrite_existing</code> must be specified in <code>options</code>.</p>
<p><i>Precondition:</i> At most one of <code>copy_options::skip_existing</code>, <code>copy_options::overwrite_existing</code> or <code>copy_options::update_existing</code> must be specified in <code>options</code>.</p>
<p><i>Effects:</i> Report an error if:
<ul>
<li><code>!is_regular_file(from)</code>, or</li>
@ -3154,10 +3155,15 @@ void <a name="copy_file4">copy_file</a>(const path&amp; from, const path&amp; to
<li><code>exists(to) &amp;&amp; equivalent(from, to)</code>, or</li>
<li><code>exists(to) &amp;&amp; (options & (copy_options::skip_existing | copy_options::overwrite_existing)) == copy_options::none</code>.</li>
</ul>
Otherwise, if <code>exists(to) &amp;&amp; (options & copy_options::skip_existing) != copy_options::none</code>, return successfully with no effect.
Otherwise, return successfully with no effect if:
<ul>
<li><code>exists(to) &amp;&amp; (options & copy_options::skip_existing) != copy_options::none</code>, or</li>
<li><code>exists(to) &amp;&amp; (options & copy_options::update_existing) != copy_options::none</code> and last write time of <code>from</code> is more recent than that of <code>to</code>.</li>
</ul>
Otherwise, the contents and attributes of the file <code>from</code> resolves to are copied to the file <code>to</code> resolves to.</p>
<p><i>Throws:</i> As specified in <a href="#Error-reporting">Error reporting</a>.</p>
<p>[<i>Note:</i> The overloads taking <a href="#copy_option"><code>copy_option</code></a> are deprecated. Their effect is equivalent to the corresponding overloads taking <a href="#copy_options"><code>copy_options</code></a> after casting the <code>options</code> argument to <a href="#copy_options"><code>copy_options</code></a>.]</p>
<p>[<i>Note:</i> When <code>copy_options::update_existing</code> is specified, checking the write times of <code>from</code> and <code>to</code> may not be atomic with the copy operation. Another process may create or modify the file identified by <code>to</code> after the file modification times have been checked but before copying starts. In this case the target file will be overwritten.]</p>
</blockquote>
<pre>void <a name="copy_symlink">copy_symlink</a>(const path&amp; existing_symlink, const path&amp; new_symlink);
void copy_symlink(const path&amp; existing_symlink, const path&amp; new_symlink, system::error_code&amp; ec);</pre>

View File

@ -48,6 +48,7 @@
<li><b>New:</b> Added <code>copy_file</code> implementations based on <code>sendfile</code> and <code>copy_file_range</code> system calls on Linux, which may improve file copying performance, especially on network filesystems.</li>
<li><b>Deprecated:</b> The <code>copy_option</code> enumeration that is used with the <code>copy_file</code> operation is deprecated. As a replacement, the new enum <code>copy_options</code> (note the trailing 's') has been added. The new enum contains values similar to the <code>copy_options</code> enum from C++20. The old enum values are mapped onto the new enum. The old enum will be removed in a future release.</li>
<li><b>New:</b> Added <code>copy_options::skip_existing</code> option, which allows <code>copy_file</code> operation to succeed without overwriting the target file, if it exists.</li>
<li><b>New:</b> Added <code>copy_options::update_existing</code> option, which allows <code>copy_file</code> operation to conditionally overwrite the target file, if it exists, if its last write time is older than that of the replacement file.</li>
<li><code>equivalent</code> on POSIX systems now returns the actual error code from the OS if one of the paths does not resolve to a file. Previously the function would return an error code of 1. (<a href="https://github.com/boostorg/filesystem/issues/141">#141</a>)</li>
<li><code>equivalent</code> no longer considers file size and last modification time in order to test whether the two paths refer to the same file. These checks could result in a false negative if the file was modified during the <code>equivalent</code> call.</li>
<li><code>space</code> now initializes the <code>space_info</code> structure members to -1 values on error, as required by C++20 ([fs.op.space]/1).</li>

View File

@ -57,7 +57,8 @@ BOOST_SCOPED_ENUM_UT_DECLARE_BEGIN(copy_options, unsigned int)
{
none = 0u, // Default, error if the target file exists
skip_existing = 1u, // Don't overwrite the existing target file, don't report an error
overwrite_existing = 1u << 1 // Overwrite existing file
overwrite_existing = 1u << 1, // Overwrite existing file
update_existing = 1u << 2 // Overwrite existing file if its last write time is older than the replacement file
}
BOOST_SCOPED_ENUM_DECLARE_END(copy_options)

View File

@ -1010,7 +1010,8 @@ BOOST_FILESYSTEM_DECL
void copy_file(const path& from, const path& to, unsigned int options, error_code* ec)
{
BOOST_ASSERT((((options & static_cast< unsigned int >(copy_options::overwrite_existing)) != 0u) +
((options & static_cast< unsigned int >(copy_options::skip_existing)) != 0u)) <= 1u);
((options & static_cast< unsigned int >(copy_options::skip_existing)) != 0u) +
((options & static_cast< unsigned int >(copy_options::update_existing)) != 0u)) <= 1u);
if (ec)
ec->clear();
@ -1022,19 +1023,30 @@ void copy_file(const path& from, const path& to, unsigned int options, error_cod
// Note: Declare fd_wrappers here so that errno is not clobbered by close() that may be called in fd_wrapper destructors
fd_wrapper infile, outfile;
infile.fd = ::open(from.c_str(), O_RDONLY | O_CLOEXEC);
if (BOOST_UNLIKELY(infile.fd < 0))
while (true)
{
fail_errno:
err = errno;
fail:
error(err, from, to, ec, "boost::filesystem::copy_file");
return;
infile.fd = ::open(from.c_str(), O_RDONLY | O_CLOEXEC);
if (BOOST_UNLIKELY(infile.fd < 0))
{
err = errno;
if (err == EINTR)
continue;
fail:
error(err, from, to, ec, "boost::filesystem::copy_file");
return;
}
break;
}
struct ::stat from_stat = {};
if (BOOST_UNLIKELY(::fstat(infile.fd, &from_stat) != 0))
goto fail_errno;
{
fail_errno:
err = errno;
goto fail;
}
if (BOOST_UNLIKELY(!S_ISREG(from_stat.st_mode)))
{
@ -1045,20 +1057,57 @@ void copy_file(const path& from, const path& to, unsigned int options, error_cod
// Enable writing for the newly created files. Having write permission set is important e.g. for NFS,
// which checks the file permission on the server, even if the client's file descriptor supports writing.
mode_t to_mode = from_stat.st_mode | S_IWUSR;
int oflag = O_WRONLY | O_CLOEXEC;
int oflag = O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC;
if ((options & static_cast< unsigned int >(copy_options::overwrite_existing)) == 0u ||
(options & static_cast< unsigned int >(copy_options::skip_existing)) != 0u)
if ((options & static_cast< unsigned int >(copy_options::update_existing)) != 0u)
{
oflag |= O_EXCL;
// Try opening the existing file without truncation to test the modification time later
while (true)
{
outfile.fd = ::open(to.c_str(), oflag, to_mode);
if (outfile.fd < 0)
{
err = errno;
if (err == EINTR)
continue;
if (err == ENOENT)
goto create_outfile;
goto fail;
}
break;
}
}
outfile.fd = ::open(to.c_str(), oflag, to_mode);
if (outfile.fd < 0)
else
{
err = errno;
if (err == EEXIST && (options & static_cast< unsigned int >(copy_options::skip_existing)) != 0u)
return;
goto fail;
create_outfile:
oflag |= O_CREAT | O_TRUNC;
if (((options & static_cast< unsigned int >(copy_options::overwrite_existing)) == 0u ||
(options & static_cast< unsigned int >(copy_options::skip_existing)) != 0u) &&
(options & static_cast< unsigned int >(copy_options::update_existing)) == 0u)
{
oflag |= O_EXCL;
}
while (true)
{
outfile.fd = ::open(to.c_str(), oflag, to_mode);
if (outfile.fd < 0)
{
err = errno;
if (err == EINTR)
continue;
if (err == EEXIST && (options & static_cast< unsigned int >(copy_options::skip_existing)) != 0u)
return;
goto fail;
}
break;
}
}
struct ::stat to_stat = {};
@ -1077,6 +1126,23 @@ void copy_file(const path& from, const path& to, unsigned int options, error_cod
goto fail;
}
if ((oflag & O_TRUNC) == 0)
{
// O_TRUNC is not set if copy_options::update_existing is set and an existing file was opened.
// We need to check the last write times.
#if defined(st_mtime)
// Modify time is available with nanosecond precision.
if (from_stat.st_mtime < to_stat.st_mtime || (from_stat.st_mtime == to_stat.st_mtime && from_stat.st_mtim.tv_nsec <= to_stat.st_mtim.tv_nsec))
return;
#else
if (from_stat.st_mtime <= to_stat.st_mtime)
return;
#endif
if (BOOST_UNLIKELY(::ftruncate(outfile.fd, 0) != 0))
goto fail_errno;
}
err = detail::copy_file_data(infile.fd, from_stat, outfile.fd, to_stat);
if (BOOST_UNLIKELY(err != 0))
goto fail; // err already contains the error code
@ -1105,8 +1171,49 @@ void copy_file(const path& from, const path& to, unsigned int options, error_cod
#else // defined(BOOST_POSIX_API)
const bool fail_if_exists = (options & static_cast< unsigned int >(copy_options::overwrite_existing)) == 0u ||
bool fail_if_exists = (options & static_cast< unsigned int >(copy_options::overwrite_existing)) == 0u ||
(options & static_cast< unsigned int >(copy_options::skip_existing)) != 0u;
if ((options & static_cast< unsigned int >(copy_options::update_existing)) != 0u)
{
// Create handle_wrappers here so that CloseHandle calls don't clobber error code returned by GetLastError
handle_wrapper hw_from, hw_to;
hw_from.handle = create_file_handle(from.c_str(), 0,
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, 0,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
FILETIME lwt_from;
if (hw_from.handle == INVALID_HANDLE_VALUE)
{
fail_last_error:
DWORD err = ::GetLastError();
error(err, from, to, ec, "boost::filesystem::copy_file");
return;
}
if (!::GetFileTime(hw_from.handle, 0, 0, &lwt_from))
goto fail_last_error;
hw_to.handle = create_file_handle(to.c_str(), 0,
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, 0,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
if (hw_to.handle != INVALID_HANDLE_VALUE)
{
FILETIME lwt_to;
if (!::GetFileTime(hw_to.handle, 0, 0, &lwt_to))
goto fail_last_error;
ULONGLONG tfrom = (static_cast< ULONGLONG >(lwt_from.dwHighDateTime) << 32) | static_cast< ULONGLONG >(lwt_from.dwLowDateTime);
ULONGLONG tto = (static_cast< ULONGLONG >(lwt_to.dwHighDateTime) << 32) | static_cast< ULONGLONG >(lwt_to.dwLowDateTime);
if (tfrom <= tto)
return;
}
fail_if_exists = false;
}
BOOL res = ::CopyFileW(from.c_str(), to.c_str(), fail_if_exists);
if (!res)
{

View File

@ -82,6 +82,7 @@ inline void unsetenv_(const char* name)
#else
#include <unistd.h> // sleep
#include <stdlib.h> // allow unqualifed calls to env funcs on SunOS
inline void setenv_(const char* name, const char* val, int ovw)
@ -1618,8 +1619,33 @@ namespace
BOOST_TEST_EQ(fs::file_size(d1x / "f2"), 10U);
verify_file(d1x / "f2", "1234567890");
// create_file(d1x / "f2", "1234567890");
// BOOST_TEST_EQ(fs::file_size(d1x / "f2"), 10U);
copy_ex_ok = true;
try { fs::copy_file(f1x, d1x / "f2", fs::copy_options::update_existing); }
catch (const fs::filesystem_error &) { copy_ex_ok = false; }
BOOST_TEST(copy_ex_ok);
BOOST_TEST_EQ(fs::file_size(d1x / "f2"), 10U);
verify_file(d1x / "f2", "1234567890");
// Sleep for a while so that the last modify time is more recent for new files
#if defined(BOOST_POSIX_API)
sleep(2);
#else
Sleep(2000);
#endif
create_file(d1x / "f2-more-recent", "x");
BOOST_TEST_EQ(fs::file_size(d1x / "f2-more-recent"), 1U);
copy_ex_ok = true;
try { fs::copy_file(d1x / "f2-more-recent", d1x / "f2", fs::copy_options::update_existing); }
catch (const fs::filesystem_error &) { copy_ex_ok = false; }
BOOST_TEST(copy_ex_ok);
BOOST_TEST_EQ(fs::file_size(d1x / "f2"), 1U);
verify_file(d1x / "f2", "x");
fs::remove(d1x / "f2-more-recent");
fs::remove(d1x / "f2");
create_file(d1x / "f2", "1234567890");
BOOST_TEST_EQ(fs::file_size(d1x / "f2"), 10U);
copy_ex_ok = true;
try { fs::copy_file(f1x, d1x / "f2", fs::copy_options::overwrite_existing); }
catch (const fs::filesystem_error &) { copy_ex_ok = false; }