FIxed copy_file_linux()

This commit is contained in:
Vojtech Bubnik 2021-03-15 13:00:21 +01:00
parent 0c0fb5a83d
commit 784dbfdf2a

View File

@ -18,9 +18,12 @@
#include <sys/resource.h> #include <sys/resource.h>
#ifdef BSD #ifdef BSD
#include <sys/sysctl.h> #include <sys/sysctl.h>
#endif #elif defined(__APPLE__)
#ifdef __APPLE__
#include <mach/mach.h> #include <mach/mach.h>
#elif defined(__linux__)
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#endif #endif
#endif #endif
@ -418,58 +421,55 @@ std::error_code rename_file(const std::string &from, const std::string &to)
} }
#ifdef __linux__ #ifdef __linux__
bool copy_file_linux(const path& from, const path& to, unsigned int options, error_code* ec) // Copied from boost::filesystem, to support copying a file to a weird filesystem, which does not support changing file attributes,
// for example ChromeOS Linux integration or FlashAIR WebDAV.
bool copy_file_linux(const boost::filesystem::path &from, const boost::filesystem::path &to, boost::system::error_code &ec)
{ {
BOOST_ASSERT((((options & static_cast< unsigned int >(copy_options::overwrite_existing)) != 0u) + using namespace boost::filesystem;
((options & static_cast< unsigned int >(copy_options::skip_existing)) != 0u) +
((options & static_cast< unsigned int >(copy_options::update_existing)) != 0u)) <= 1);
if (ec) struct fd_wrapper
ec->clear(); {
int fd { -1 };
fd_wrapper() = default;
explicit fd_wrapper(int fd) throw() : fd(fd) {}
~fd_wrapper() throw() { if (fd >= 0) ::close(fd); }
};
ec.clear();
int err = 0; int err = 0;
// Note: Declare fd_wrappers here so that errno is not clobbered by close() that may be called in fd_wrapper destructors // 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; fd_wrapper infile, outfile;
while (true) while (true) {
{
infile.fd = ::open(from.c_str(), O_RDONLY | O_CLOEXEC); infile.fd = ::open(from.c_str(), O_RDONLY | O_CLOEXEC);
if (BOOST_UNLIKELY(infile.fd < 0)) if (infile.fd < 0) {
{
err = errno; err = errno;
if (err == EINTR) if (err == EINTR)
continue; continue;
fail: fail:
emit_error(err, from, to, ec, "boost::filesystem::copy_file"); ec.assign(err, boost::system::system_category());
return false; return false;
} }
break; break;
} }
unsigned int statx_data_mask = STATX_TYPE | STATX_MODE | STATX_INO | STATX_SIZE; unsigned int statx_data_mask = STATX_TYPE | STATX_MODE | STATX_INO | STATX_SIZE;
if ((options & static_cast< unsigned int >(copy_options::update_existing)) != 0u)
statx_data_mask |= STATX_MTIME;
struct ::statx from_stat; struct ::statx from_stat;
if (BOOST_UNLIKELY(statx(infile.fd, "", AT_EMPTY_PATH | AT_NO_AUTOMOUNT, statx_data_mask, &from_stat) < 0)) if (statx(infile.fd, "", AT_EMPTY_PATH | AT_NO_AUTOMOUNT, statx_data_mask, &from_stat) < 0) {
{
fail_errno: fail_errno:
err = errno; err = errno;
goto fail; goto fail;
} }
if (BOOST_UNLIKELY((from_stat.stx_mask & statx_data_mask) != statx_data_mask)) if ((from_stat.stx_mask & statx_data_mask) != statx_data_mask) {
{
err = ENOSYS; err = ENOSYS;
goto fail; goto fail;
} }
const mode_t from_mode = get_mode(from_stat); const mode_t from_mode = from_stat.stx_mode;
if (BOOST_UNLIKELY(!S_ISREG(from_mode))) if (!S_ISREG(from_mode)) {
{
err = ENOSYS; err = ENOSYS;
goto fail; goto fail;
} }
@ -477,110 +477,75 @@ bool copy_file_linux(const path& from, const path& to, unsigned int options, err
// Enable writing for the newly created files. Having write permission set is important e.g. for NFS, // 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. // which checks the file permission on the server, even if the client's file descriptor supports writing.
mode_t to_mode = from_mode | S_IWUSR; mode_t to_mode = from_mode | S_IWUSR;
int oflag = O_WRONLY | O_CLOEXEC; int oflag = O_WRONLY | O_CLOEXEC | O_CREAT | O_TRUNC;
if ((options & static_cast< unsigned int >(copy_options::update_existing)) != 0u) while (true) {
{
// Try opening the existing file without truncation to test the modification time later
while (true)
{
outfile.fd = ::open(to.c_str(), oflag, to_mode); outfile.fd = ::open(to.c_str(), oflag, to_mode);
if (outfile.fd < 0) if (outfile.fd < 0) {
{
err = errno; err = errno;
if (err == EINTR) if (err == EINTR)
continue; continue;
if (err == ENOENT)
goto create_outfile;
goto fail; goto fail;
} }
break; break;
} }
}
else
{
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 false;
goto fail;
}
break;
}
}
statx_data_mask = STATX_TYPE | STATX_MODE | STATX_INO; statx_data_mask = STATX_TYPE | STATX_MODE | STATX_INO;
if ((oflag & O_TRUNC) == 0)
{
// O_TRUNC is not set if copy_options::update_existing is set and an existing file was opened.
statx_data_mask |= STATX_MTIME;
}
struct ::statx to_stat; struct ::statx to_stat;
if (BOOST_UNLIKELY(statx(outfile.fd, "", AT_EMPTY_PATH | AT_NO_AUTOMOUNT, statx_data_mask, &to_stat) < 0)) if (statx(outfile.fd, "", AT_EMPTY_PATH | AT_NO_AUTOMOUNT, statx_data_mask, &to_stat) < 0)
goto fail_errno; goto fail_errno;
if (BOOST_UNLIKELY((to_stat.stx_mask & statx_data_mask) != statx_data_mask)) if ((to_stat.stx_mask & statx_data_mask) != statx_data_mask) {
{
err = ENOSYS; err = ENOSYS;
goto fail; goto fail;
} }
to_mode = get_mode(to_stat); to_mode = to_stat.stx_mode;
if (BOOST_UNLIKELY(!S_ISREG(to_mode))) if (!S_ISREG(to_mode)) {
{
err = ENOSYS; err = ENOSYS;
goto fail; goto fail;
} }
if (BOOST_UNLIKELY(detail::equivalent_stat(from_stat, to_stat))) if (from_stat.stx_dev_major == to_stat.stx_dev_major && from_stat.stx_dev_minor == to_stat.stx_dev_minor && from_stat.stx_ino == to_stat.stx_ino) {
{
err = EEXIST; err = EEXIST;
goto fail; goto fail;
} }
if ((oflag & O_TRUNC) == 0) //! copy_file implementation that uses sendfile loop. Requires sendfile to support file descriptors.
//FIXME Vojtech: This is a copy loop valid for Linux 2.6.33 and newer.
// copy_file_data_copy_file_range() supports cross-filesystem copying since 5.3, but Vojtech did not want to polute this
// function with that, we don't think the performance gain is worth it for the types of files we are copying.
{ {
// O_TRUNC is not set if copy_options::update_existing is set and an existing file was opened. // sendfile will not send more than this amount of data in one call
// We need to check the last write times. BOOST_CONSTEXPR_OR_CONST std::size_t max_send_size = 0x7ffff000u;
if (from_stat.stx_mtime.tv_sec < to_stat.stx_mtime.tv_sec || (from_stat.stx_mtime.tv_sec == to_stat.stx_mtime.tv_sec && from_stat.stx_mtime.tv_nsec <= to_stat.stx_mtime.tv_nsec)) uintmax_t offset = 0u;
return false; while (offset < from_stat.stx_size) {
uintmax_t size_left = from_stat.stx_size - offset;
if (BOOST_UNLIKELY(::ftruncate(outfile.fd, 0) != 0)) std::size_t size_to_copy = max_send_size;
goto fail_errno; if (size_left < static_cast<uintmax_t>(max_send_size))
} size_to_copy = static_cast<std::size_t>(size_left);
ssize_t sz = ::sendfile(outfile.fd, infile.fd, nullptr, size_to_copy);
err = detail::copy_file_data(infile.fd, outfile.fd, get_size(from_stat)); if (sz < 0) {
if (BOOST_UNLIKELY(err != 0)) err = errno;
if (err == EINTR)
continue;
if (err == 0)
break;
goto fail; // err already contains the error code goto fail; // err already contains the error code
}
offset += sz;
}
}
// If we created a new file with an explicitly added S_IWUSR permission, // If we created a new file with an explicitly added S_IWUSR permission,
// we may need to update its mode bits to match the source file. // we may need to update its mode bits to match the source file.
if (to_mode != from_mode) if (to_mode != from_mode && ::fchmod(outfile.fd, from_mode) != 0) {
{ if (platform_flavor() == PlatformFlavor::LinuxOnChromium) {
if (BOOST_UNLIKELY(::fchmod(outfile.fd, from_mode) != 0)) // Ignore that. 9p filesystem does not allow fmod().
goto fail_errno; } else {
// Generic linux. Write out an error to console. At least we may get some feedback.
BOOST_LOG_TRIVIAL(error) << "copy_file_linux() failed to fchmod() the output file \"" << to.string() << "\" to " << from_mode << ": " << ec.message();
}
} }
// Note: Use fsync/fdatasync followed by close to avoid dealing with the possibility of close failing with EINTR. // Note: Use fsync/fdatasync followed by close to avoid dealing with the possibility of close failing with EINTR.
@ -590,7 +555,7 @@ bool copy_file_linux(const path& from, const path& to, unsigned int options, err
// ensures that all data have been written, and even if close fails for some unfathomable reason, we don't really // ensures that all data have been written, and even if close fails for some unfathomable reason, we don't really
// care at that point. // care at that point.
err = ::fdatasync(outfile.fd); err = ::fdatasync(outfile.fd);
if (BOOST_UNLIKELY(err != 0)) if (err != 0)
goto fail_errno; goto fail_errno;
return true; return true;
@ -617,7 +582,7 @@ CopyFileResult copy_file_inner(const std::string& from, const std::string& to, s
#ifdef __linux__ #ifdef __linux__
// We want to allow copying files on Linux to succeed even if changing the file attributes fails. // We want to allow copying files on Linux to succeed even if changing the file attributes fails.
// That may happen when copying on some exotic file system, for example Linux on Chrome. // That may happen when copying on some exotic file system, for example Linux on Chrome.
copy_file_linux(source, target, boost::filesystem::copy_option::overwrite_if_exists, ec); copy_file_linux(source, target, ec);
#else // __linux__ #else // __linux__
boost::filesystem::copy_file(source, target, boost::filesystem::copy_option::overwrite_if_exists, ec); boost::filesystem::copy_file(source, target, boost::filesystem::copy_option::overwrite_if_exists, ec);
#endif // __linux__ #endif // __linux__