diff options
Diffstat (limited to 'src/file_util.cpp')
-rw-r--r-- | src/file_util.cpp | 671 |
1 files changed, 451 insertions, 220 deletions
diff --git a/src/file_util.cpp b/src/file_util.cpp index 0707514..9ae19c1 100644 --- a/src/file_util.cpp +++ b/src/file_util.cpp @@ -22,13 +22,16 @@ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +#include <jau/debug.hpp> +#include <jau/file_util.hpp> + #include <cstdint> #include <cstdlib> #include <cinttypes> #include <limits> - -#include <jau/debug.hpp> -#include <jau/file_util.hpp> +#include <cstring> +#include <cstdio> +#include <random> extern "C" { #include <unistd.h> @@ -51,6 +54,8 @@ extern "C" { #define O_NONBLOCK 0 #endif +inline constexpr const int _open_dir_flags = O_RDONLY|O_BINARY|O_NOCTTY|O_DIRECTORY; + using namespace jau; using namespace jau::fs; @@ -256,6 +261,10 @@ dir_item::dir_item(std::unique_ptr<backed_string_view> cleanpath) noexcept } } +dir_item::dir_item(const std::string& dirname__, const std::string& basename__) noexcept +: dirname_(dirname__), basename_(basename__) { +} + dir_item::dir_item() noexcept : dirname_(_dot), basename_(_dot) {} @@ -369,7 +378,7 @@ file_stats::file_stats() noexcept static constexpr bool jau_has_stat(const uint32_t mask, const uint32_t bit) { return bit == ( mask & bit ); } -file_stats::file_stats(const ctor_cookie& cc, const dir_item& item) noexcept +file_stats::file_stats(const ctor_cookie& cc, const int dirfd, const dir_item& item) noexcept : has_fields_(field_t::none), item_(item), link_target_path_(), link_target_(), mode_(fmode_t::none), uid_(0), gid_(0), size_(0), btime_(), atime_(), ctime_(), mtime_(), errno_res_(0) @@ -380,7 +389,7 @@ file_stats::file_stats(const ctor_cookie& cc, const dir_item& item) noexcept if constexpr ( _use_statx ) { struct ::statx s; ::bzero(&s, sizeof(s)); - int stat_res = ::statx(AT_FDCWD, path.c_str(), + int stat_res = ::statx(dirfd, path.c_str(), ( AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW ), ( STATX_BASIC_STATS | STATX_BTIME ), &s); if( 0 != stat_res ) { @@ -482,7 +491,7 @@ if constexpr ( _use_statx ) { // Initial symbolic followed: Test recursive loop-error constexpr const bool _debug = false; ::bzero(&s, sizeof(s)); - stat_res = ::statx(AT_FDCWD, path.c_str(), AT_NO_AUTOMOUNT, STATX_BASIC_STATS, &s); + stat_res = ::statx(dirfd, path.c_str(), AT_NO_AUTOMOUNT, STATX_BASIC_STATS, &s); if( 0 != stat_res ) { if constexpr ( _debug ) { jau::fprintf_td(stderr, "file_stats(%d): Test link ERROR: '%s', %d, errno %d (%s)\n", (int)cc.rec_level, path.c_str(), stat_res, errno, ::strerror(errno)); @@ -509,9 +518,11 @@ if constexpr ( _use_statx ) { } } if( 0 < link_path.size() && '/' == link_path[0] ) { - link_target_ = std::make_shared<file_stats>(ctor_cookie(cc.rec_level+1), dir_item( link_path )); // absolute link_path + link_target_ = std::make_shared<file_stats>(ctor_cookie(cc.rec_level+1), dirfd, dir_item( link_path )); // absolute link_path + } else if( AT_FDCWD == dirfd ) { + link_target_ = std::make_shared<file_stats>(ctor_cookie(cc.rec_level+1), dirfd, dir_item( jau::fs::dirname(path) + _slash + link_path )); } else { - link_target_ = std::make_shared<file_stats>(ctor_cookie(cc.rec_level+1), dir_item( jau::fs::dirname(path) + _slash + link_path )); + link_target_ = std::make_shared<file_stats>(ctor_cookie(cc.rec_level+1), dirfd, dir_item( _dot, link_path )); } if( link_target_->is_file() ) { mode_ |= fmode_t::file; @@ -529,7 +540,7 @@ if constexpr ( _use_statx ) { } else { /* constexpr !_use_statx */ struct ::stat64 s; ::bzero(&s, sizeof(s)); - int stat_res = ::lstat64(path.c_str(), &s); + int stat_res = ::fstatat64(dirfd, path.c_str(), &s, AT_SYMLINK_NOFOLLOW); // lstat64 compatible if( 0 != stat_res ) { switch( errno ) { case EACCES: @@ -596,7 +607,7 @@ if constexpr ( _use_statx ) { if( 0 == cc.rec_level ) { // Initial symbolic followed: Test recursive loop-error ::bzero(&s, sizeof(s)); - stat_res = ::stat64(path.c_str(), &s); + stat_res = ::fstatat64(dirfd, path.c_str(), &s, 0); // stat64 compatible if( 0 != stat_res ) { switch( errno ) { case EACCES: @@ -617,9 +628,11 @@ if constexpr ( _use_statx ) { } } if( 0 < link_path.size() && '/' == link_path[0] ) { - link_target_ = std::make_shared<file_stats>(ctor_cookie(cc.rec_level+1), dir_item( link_path )); // absolute link_path + link_target_ = std::make_shared<file_stats>(ctor_cookie(cc.rec_level+1), dirfd, dir_item( link_path )); // absolute link_path + } else if( AT_FDCWD == dirfd ) { + link_target_ = std::make_shared<file_stats>(ctor_cookie(cc.rec_level+1), dirfd, dir_item( jau::fs::dirname(path) + _slash + link_path )); } else { - link_target_ = std::make_shared<file_stats>(ctor_cookie(cc.rec_level+1), dir_item( jau::fs::dirname(path) + _slash + link_path )); + link_target_ = std::make_shared<file_stats>(ctor_cookie(cc.rec_level+1), dirfd, dir_item( _dot, link_path )); } if( link_target_->is_file() ) { mode_ |= fmode_t::file; @@ -640,11 +653,19 @@ if constexpr ( _use_statx ) { } file_stats::file_stats(const dir_item& item) noexcept -: file_stats(ctor_cookie(0), item) +: file_stats(ctor_cookie(0), AT_FDCWD, item) +{} + +file_stats::file_stats(const int dirfd, const dir_item& item) noexcept +: file_stats(ctor_cookie(0), dirfd, item) {} file_stats::file_stats(const std::string& _path) noexcept -: file_stats(ctor_cookie(0), dir_item(_path)) +: file_stats(ctor_cookie(0), AT_FDCWD, dir_item(_path)) +{} + +file_stats::file_stats(const int dirfd, const std::string& _path) noexcept +: file_stats(ctor_cookie(0), dirfd, dir_item(_path)) {} const file_stats* file_stats::final_target(size_t* link_count) const noexcept { @@ -792,11 +813,12 @@ bool jau::fs::touch(const std::string& path, const fmode_t mode) noexcept { bool jau::fs::get_dir_content(const std::string& path, const consume_dir_item& digest) noexcept { DIR *dir; struct dirent *ent; + if( ( dir = ::opendir( path.c_str() ) ) != nullptr ) { while ( ( ent = ::readdir( dir ) ) != NULL ) { std::string fname( ent->d_name ); if( _dot != fname && _dotdot != fname ) { // avoid '.' and '..' - digest( dir_item( path + _slash + fname ) ); + digest( dir_item( path, fname ) ); } } ::closedir (dir); @@ -891,48 +913,100 @@ bool jau::fs::visit(const std::string& path, const traverse_options topts, const bool jau::fs::remove(const std::string& path, const traverse_options topts) noexcept { file_stats path_stats(path); + if( path_stats.is_file() || + ( path_stats.is_dir() && path_stats.is_link() && !is_set(topts, traverse_options::follow_symlinks) ) + ) + { + int res = ::unlink( path_stats.path().c_str() ); + if( 0 != res ) { + ERR_PRINT("remove failed: %s, res %d", path_stats.to_string().c_str(), res); + return false; + } + return true; + } if( path_stats.is_dir() && !is_set(topts, traverse_options::recursive) ) { if( is_set(topts, traverse_options::verbose) ) { jau::fprintf_td(stderr, "remove: Error: path is dir but !recursive, %s\n", path_stats.to_string().c_str()); } return false; } - const path_visitor pv = jau::bindCaptureRefFunc<bool, const traverse_options, traverse_event, const file_stats&>(&topts, - ( bool(*)(const traverse_options*, traverse_event, const file_stats&) ) /* help template type deduction of function-ptr */ - ( [](const traverse_options* _options, traverse_event tevt, const file_stats& element_stats) -> bool { + struct remove_context_t { + traverse_options topts; + std::vector<int> dirfds; + }; + remove_context_t ctx = { topts, std::vector<int>() }; + { + const int dirfd = ::openat64(AT_FDCWD, path_stats.item().dirname().c_str(), _open_dir_flags); + if ( 0 > dirfd ) { + ERR_PRINT("path dirname couldn't be opened, source %s", path_stats.to_string().c_str()); + return false; + } + ctx.dirfds.push_back(dirfd); + } + + const path_visitor pv = jau::bindCaptureRefFunc<bool, remove_context_t, traverse_event, const file_stats&>(&ctx, + ( bool(*)(remove_context_t*, traverse_event, const file_stats&) ) /* help template type deduction of function-ptr */ + ( [](remove_context_t* ctx_ptr, traverse_event tevt, const file_stats& element_stats) -> bool { (void)tevt; - const int res = ::remove( element_stats.path().c_str() ); - if( 0 != res ) { - if( is_set(*_options, traverse_options::verbose) ) { - jau::fprintf_td(stderr, "remove: Error: remove failed: %s, res %d, errno %d (%s)\n", - element_stats.to_string().c_str(), res, errno, strerror(errno)); + + if( !element_stats.has_access() ) { + if( is_set(ctx_ptr->topts, traverse_options::verbose) ) { + jau::fprintf_td(stderr, "remove: Error: remove failed: no access, %s\n", element_stats.to_string().c_str()); } return false; - } else { - if( is_set(*_options, traverse_options::verbose) ) { + } + if( ctx_ptr->dirfds.size() < 1 ) { + ERR_PRINT("dirfd stack error: count %zu] @ %s", ctx_ptr->dirfds.size(), element_stats.to_string().c_str()); + return false; + } + const int dirfd = ctx_ptr->dirfds.back(); + const std::string& basename_ = element_stats.item().basename(); + if( is_set(tevt, traverse_event::dir_entry) ) { + const int new_dirfd = ::openat64(dirfd, basename_.c_str(), _open_dir_flags); + if ( 0 > new_dirfd ) { + ERR_PRINT("entered path dir couldn't be opened, source %s", element_stats.to_string().c_str()); + return false; + } + ctx_ptr->dirfds.push_back(new_dirfd); + } else if( is_set(tevt, traverse_event::dir_exit) ) { + if( ctx_ptr->dirfds.size() < 2 ) { + ERR_PRINT("dirfd stack error: count %zu] @ %s", ctx_ptr->dirfds.size(), element_stats.to_string().c_str()); + return false; + } + const int dirfd2 = *( ctx_ptr->dirfds.end() - 2 ); + const int res = ::unlinkat( dirfd2, basename_.c_str(), AT_REMOVEDIR ); + if( 0 != res ) { + ERR_PRINT("remove failed: %s, res %d", element_stats.to_string().c_str(), res); + return false; + } + if( is_set(ctx_ptr->topts, traverse_options::verbose) ) { + jau::fprintf_td(stderr, "remove: %s removed\n", element_stats.to_string().c_str()); + } + ::close(dirfd); + ctx_ptr->dirfds.pop_back(); + } else if( is_set(tevt, traverse_event::file) || is_set(tevt, traverse_event::symlink) || is_set(tevt, traverse_event::dir_symlink) ) { + const int res = ::unlinkat( dirfd, basename_.c_str(), 0 ); + if( 0 != res ) { + ERR_PRINT("remove failed: %s, res %d", element_stats.to_string().c_str(), res); + return false; + } + if( is_set(ctx_ptr->topts, traverse_options::verbose) ) { jau::fprintf_td(stderr, "remove: %s removed\n", element_stats.to_string().c_str()); } - return true; } + return true; } ) ); - return jau::fs::visit(path_stats, - ( topts & ~jau::fs::traverse_options::dir_entry ) | jau::fs::traverse_options::dir_exit, pv); -} - -#define COPYOPTIONS_BIT_ENUM(X,M) \ - X(copy_options,recursive,M) \ - X(copy_options,follow_symlinks,M) \ - X(copy_options,ignore_symlink_errors,M) \ - X(copy_options,overwrite,M) \ - X(copy_options,preserve_all,M) \ - X(copy_options,sync,M) - -std::string jau::fs::to_string(const copy_options mask) noexcept { - std::string out("["); - bool comma = false; - COPYOPTIONS_BIT_ENUM(APPEND_BITSTR,mask) - out.append("]"); - return out; + bool res = jau::fs::visit(path_stats, + topts | jau::fs::traverse_options::dir_entry | jau::fs::traverse_options::dir_exit, pv); + if( ctx.dirfds.size() != 1 ) { + ERR_PRINT("dirfd stack error: count %zu", ctx.dirfds.size()); + res = false; + } + while( !ctx.dirfds.empty() ) { + ::close(ctx.dirfds.back()); + ctx.dirfds.pop_back(); + } + return res; } bool jau::fs::compare(const std::string& source1, const std::string& source2, const bool verbose) noexcept { @@ -1031,73 +1105,89 @@ errout: return res; } -static bool copy_file(const file_stats& source_stats, const std::string& dest_path, const copy_options copts) noexcept { - file_stats dest_stats(dest_path); +#define COPYOPTIONS_BIT_ENUM(X,M) \ + X(copy_options,recursive,M) \ + X(copy_options,follow_symlinks,M) \ + X(copy_options,ignore_symlink_errors,M) \ + X(copy_options,overwrite,M) \ + X(copy_options,preserve_all,M) \ + X(copy_options,sync,M) + +std::string jau::fs::to_string(const copy_options mask) noexcept { + std::string out("["); + bool comma = false; + COPYOPTIONS_BIT_ENUM(APPEND_BITSTR,mask) + out.append("]"); + return out; +} + +struct copy_context_t { + copy_options copts; + int skip_dst_dir_mkdir; + std::vector<int> src_dirfds; + std::vector<int> dst_dirfds; +}; + +static bool copy_file(const int src_dirfd, const file_stats& src_stats, + const int dst_dirfd, const std::string& dst_basename, const copy_options copts) noexcept { + file_stats dst_stats(dst_dirfd, dst_basename); // overwrite: remove pre-existing file, if copy_options::overwrite set - if( dest_stats.is_file() ) { + if( dst_stats.is_file() ) { if( !is_set(copts, copy_options::overwrite) ) { if( is_set(copts, copy_options::verbose) ) { jau::fprintf_td(stderr, "copy: Error: dest_path exists but copy_options::overwrite not set: source %s, dest '%s', copts %s\n", - source_stats.to_string().c_str(), dest_stats.to_string().c_str(), to_string( copts ).c_str()); + src_stats.to_string().c_str(), dst_stats.to_string().c_str(), to_string( copts ).c_str()); } return false; } - const int res = ::remove( dest_path.c_str() ); + const int res = ::unlinkat(dst_dirfd, dst_basename.c_str(), 0); if( 0 != res ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: remove existing dest_path for symbolic-link failed: source %s, dest '%s', errno %d (%s)\n", - source_stats.to_string().c_str(), dest_stats.to_string().c_str(), errno, strerror(errno)); - } + ERR_PRINT("remove existing dest_path for symbolic-link failed: source %s, dest '%s'", + src_stats.to_string().c_str(), dst_stats.to_string().c_str()); return false; } } // copy as symbolic link - if( source_stats.is_link() && !is_set(copts, copy_options::follow_symlinks) ) { - const std::shared_ptr<std::string> link_target_path = source_stats.link_target_path(); + if( src_stats.is_link() && !is_set(copts, copy_options::follow_symlinks) ) { + const std::shared_ptr<std::string> link_target_path = src_stats.link_target_path(); if( nullptr == link_target_path || 0 == link_target_path->size() ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: Symbolic link-path is empty %s\n", source_stats.to_string().c_str()); - } + ERR_PRINT("Symbolic link-path is empty %s", src_stats.to_string().c_str()); return false; } // symlink - const int res = ::symlink(link_target_path->c_str(), dest_path.c_str()); + const int res = ::symlinkat(link_target_path->c_str(), dst_dirfd, dst_basename.c_str()); if( 0 > res ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: Creating symlink failed %s -> %s, errno %d, %s\n", - dest_path.c_str(), link_target_path->c_str(), errno, ::strerror(errno)); - } + ERR_PRINT("Creating symlink failed %s -> %s", dst_basename.c_str(), link_target_path->c_str()); return false; } if( is_set(copts, copy_options::preserve_all) ) { // preserve time - struct timespec ts2[2] = { source_stats.atime().to_timespec(), source_stats.mtime().to_timespec() }; - if( 0 != ::utimensat(AT_FDCWD, dest_path.c_str(), ts2, AT_SYMLINK_NOFOLLOW) ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: Couldn't preserve time of symlink, source %s, dest '%s', errno %d (%s)\n", - source_stats.to_string().c_str(), dest_path.c_str(), errno, ::strerror(errno)); - } + struct timespec ts2[2] = { src_stats.atime().to_timespec(), src_stats.mtime().to_timespec() }; + if( 0 != ::utimensat(dst_dirfd, dst_basename.c_str(), ts2, AT_SYMLINK_NOFOLLOW) ) { + ERR_PRINT("Couldn't preserve time of symlink, source %s, dest '%s'", src_stats.to_string().c_str(), dst_basename.c_str()); return false; } // preserve ownership const uid_t caller_uid = ::geteuid(); - const ::uid_t source_uid = 0 == caller_uid ? source_stats.uid() : -1; - if( 0 != ::fchownat(AT_FDCWD, dest_path.c_str(), source_uid, source_stats.gid(), AT_SYMLINK_NOFOLLOW) ) { - if( errno != EPERM && errno != EINVAL ) { // OK to fail due to permissions - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: Couldn't preserve ownership of symlink, source %s, dest '%s', errno %d (%s)\n", - source_stats.to_string().c_str(), dest_path.c_str(), errno, ::strerror(errno)); - } + const ::uid_t source_uid = 0 == caller_uid ? src_stats.uid() : -1; + if( 0 != ::fchownat(dst_dirfd, dst_basename.c_str(), source_uid, src_stats.gid(), AT_SYMLINK_NOFOLLOW) ) { + if( errno != EPERM && errno != EINVAL ) { + ERR_PRINT("Couldn't preserve ownership of symlink, source %s, dest '%s'", src_stats.to_string().c_str(), dst_basename.c_str()); return false; } + // OK to fail due to permissions + if( is_set(copts, copy_options::verbose) ) { + jau::fprintf_td(stderr, "copy: Warn: Couldn't preserve ownership of symlink, source %s, dest '%s', errno %d (%s)\n", + src_stats.to_string().c_str(), dst_basename.c_str(), errno, ::strerror(errno)); + } } } return true; } // copy actual file bytes - const file_stats* target_stats = source_stats.final_target(); // follows symlinks up to definite item + const file_stats* target_stats = src_stats.final_target(); // follows symlinks up to definite item const fmode_t dest_mode = target_stats->prot_mode(); const fmode_t omitted_permissions = dest_mode & ( fmode_t::rwx_grp | fmode_t::rwx_oth ); @@ -1110,28 +1200,28 @@ static bool copy_file(const file_stats& source_stats, const std::string& dest_pa if( caller_uid == target_stats->uid() ) { src_flags |= O_NOATIME; } // else we are not allowed to not use O_NOATIME - src = ::open64(source_stats.path().c_str(), src_flags); + src = ::openat64(src_dirfd, src_stats.item().basename().c_str(), src_flags); if ( 0 > src ) { - if( source_stats.is_link() ) { + if( src_stats.is_link() ) { res = is_set(copts, copy_options::ignore_symlink_errors); } - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: %s: Failed to open source %s, errno %d, %s\n", res?"Ignored":"Error", source_stats.to_string().c_str(), errno, ::strerror(errno)); + if( !res ) { + ERR_PRINT("Failed to open source %s", src_stats.to_string().c_str()); + } else if( is_set(copts, copy_options::verbose) ) { + jau::fprintf_td(stderr, "copy: Ignored: Failed to open source %s, errno %d, %s\n", src_stats.to_string().c_str(), errno, ::strerror(errno)); } goto errout; } - dst = ::open64(dest_path.c_str(), O_CREAT|O_WRONLY|O_BINARY|O_EXCL|O_CLOEXEC|O_NOCTTY, jau::fs::posix_protection_bits( dest_mode & ~omitted_permissions ) ); + dst = ::openat64 (dst_dirfd, dst_basename.c_str(), O_CREAT|O_WRONLY|O_BINARY|O_EXCL|O_NOCTTY, jau::fs::posix_protection_bits( dest_mode & ~omitted_permissions ) ); if ( 0 > dst ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: Failed to open target_path '%s', errno %d, %s\n", dest_path.c_str(), errno, ::strerror(errno)); - } + ERR_PRINT("Failed to open target_path '%s'", dst_basename.c_str()); goto errout; } - while ( offset < source_stats.size()) { + while ( offset < src_stats.size()) { ssize_t rc1, rc2=0; if constexpr ( _use_sendfile ) { off64_t offset_i = (off64_t)offset; // we drop 1 bit of value-range as off64_t is int64_t - const uint64_t count = std::max<uint64_t>(std::numeric_limits<ssize_t>::max(), source_stats.size() - offset); + const uint64_t count = std::max<uint64_t>(std::numeric_limits<ssize_t>::max(), src_stats.size() - offset); if( ( rc1 = ::sendfile64(dst, src, &offset_i, (size_t)count) ) >= 0 ) { offset = (uint64_t)offset_i; } @@ -1151,20 +1241,18 @@ static bool copy_file(const file_stats& source_stats, const std::string& dest_pa } } if ( 0 > rc1 || 0 > rc2 ) { - if( is_set(copts, copy_options::verbose) ) { - if constexpr ( _use_sendfile ) { - jau::fprintf_td(stderr, "copy: Error: Failed to copy bytes @ %s / %s, %s -> '%s', errno %d, %s\n", - jau::to_decstring(offset).c_str(), jau::to_decstring(source_stats.size()).c_str(), - source_stats.to_string().c_str(), dest_path.c_str(), errno, ::strerror(errno)); - } else if ( 0 > rc1 ) { - jau::fprintf_td(stderr, "copy: Error: Failed to read bytes @ %s / %s, %s, errno %d, %s\n", - jau::to_decstring(offset).c_str(), jau::to_decstring(source_stats.size()).c_str(), - source_stats.to_string().c_str(), errno, ::strerror(errno)); - } else if ( 0 > rc2 ) { - jau::fprintf_td(stderr, "copy: Error: Failed to write bytes @ %s / %s, %s, errno %d, %s\n", - jau::to_decstring(offset).c_str(), jau::to_decstring(source_stats.size()).c_str(), - dest_path.c_str(), errno, ::strerror(errno)); - } + if constexpr ( _use_sendfile ) { + ERR_PRINT("Failed to copy bytes @ %s / %s, %s -> '%s'", + jau::to_decstring(offset).c_str(), jau::to_decstring(src_stats.size()).c_str(), + src_stats.to_string().c_str(), dst_basename.c_str()); + } else if ( 0 > rc1 ) { + ERR_PRINT("Failed to read bytes @ %s / %s, %s", + jau::to_decstring(offset).c_str(), jau::to_decstring(src_stats.size()).c_str(), + src_stats.to_string().c_str()); + } else if ( 0 > rc2 ) { + ERR_PRINT("Failed to write bytes @ %s / %s, %s", + jau::to_decstring(offset).c_str(), jau::to_decstring(src_stats.size()).c_str(), + dst_basename.c_str()); } goto errout; } @@ -1172,22 +1260,18 @@ static bool copy_file(const file_stats& source_stats, const std::string& dest_pa break; } } - if( offset < source_stats.size() ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: Incomplete transfer %s / %s, %s -> '%s', errno %d, %s\n", - jau::to_decstring(offset).c_str(), jau::to_decstring(source_stats.size()).c_str(), - source_stats.to_string().c_str(), dest_path.c_str(), errno, ::strerror(errno)); - } + if( offset < src_stats.size() ) { + ERR_PRINT("Incomplete transfer %s / %s, %s -> '%s'", + jau::to_decstring(offset).c_str(), jau::to_decstring(src_stats.size()).c_str(), + src_stats.to_string().c_str(), dst_basename.c_str()); goto errout; } res = true; if( omitted_permissions != fmode_t::none ) { // restore omitted permissions if( 0 != ::fchmod(dst, jau::fs::posix_protection_bits( dest_mode )) ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: Couldn't restore omitted permissions, source %s, dest '%s', errno %d (%s)\n", - source_stats.to_string().c_str(), dest_path.c_str(), errno, ::strerror(errno)); - } + ERR_PRINT("Couldn't restore omitted permissions, source %s, dest '%s'", + src_stats.to_string().c_str(), dst_basename.c_str()); res = false; } } @@ -1195,34 +1279,30 @@ static bool copy_file(const file_stats& source_stats, const std::string& dest_pa // preserve time struct timespec ts2[2] = { target_stats->atime().to_timespec(), target_stats->mtime().to_timespec() }; if( 0 != ::futimens(dst, ts2) ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: Couldn't preserve time of file, source %s, dest '%s', errno %d (%s)\n", - source_stats.to_string().c_str(), dest_path.c_str(), errno, ::strerror(errno)); - } + ERR_PRINT("Couldn't preserve time of file, source %s, dest '%s'", + src_stats.to_string().c_str(), dst_basename.c_str()); res = false; } // preserve ownership ::uid_t source_uid = 0 == caller_uid ? target_stats->uid() : -1; if( 0 != ::fchown(dst, source_uid, target_stats->gid()) ) { - if( errno != EPERM && errno != EINVAL ) { // OK to fail due to permissions + if( errno != EPERM && errno != EINVAL ) { + ERR_PRINT("Couldn't preserve ownership of file, uid(caller %" PRIu32 ", chown %" PRIu32 "), source %s, dest '%s'", + caller_uid, source_uid, src_stats.to_string().c_str(), dst_basename.c_str()); + res = false; + } else { + // OK to fail due to permissions if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: Couldn't preserve ownership of file, uid(caller %" PRIu32 ", chown %" PRIu32 "), source %s, dest '%s', errno %d (%s)\n", - caller_uid, source_uid, source_stats.to_string().c_str(), dest_path.c_str(), errno, ::strerror(errno)); + jau::fprintf_td(stderr, "copy: Ignored: Preserve ownership of file failed, uid(caller %" PRIu32 ", chown %" PRIu32 "), source %s, dest '%s', errno %d (%s)\n", + caller_uid, source_uid, src_stats.to_string().c_str(), dst_stats.to_string().c_str(), errno, ::strerror(errno)); } - res = false; - } - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Ignored: Preserve ownership of file failed, uid(caller %" PRIu32 ", chown %" PRIu32 "), source %s, dest '%s', errno %d (%s)\n", - caller_uid, source_uid, source_stats.to_string().c_str(), dest_stats.to_string().c_str(), errno, ::strerror(errno)); } } } if( is_set(copts, copy_options::sync) ) { if( 0 != ::fsync(dst) ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: Couldn't synchronize destination file, source %s, dest '%s', errno %d (%s)\n", - source_stats.to_string().c_str(), dest_path.c_str(), errno, ::strerror(errno)); - } + ERR_PRINT("Couldn't synchronize destination file, source %s, dest '%s'", + src_stats.to_string().c_str(), dst_basename.c_str()); res = false; } } @@ -1236,78 +1316,121 @@ errout: return res; } -static bool copy_mkdir(const file_stats& source_stats, const file_stats& dest_stats, const copy_options copts) noexcept { - (void)source_stats; - const std::string dest_path = dest_stats.path(); - - if( dest_stats.is_dir() ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: mkdir directory already exist: %s\n", dest_stats.to_string().c_str()); - } - } else if( !dest_stats.exists() ) { - const int dir_err = ::mkdir(dest_path.c_str(), posix_protection_bits(fmode_t::rwx_usr)); - if ( 0 != dir_err ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: mkdir failed: %s, errno %d (%s)\n", dest_stats.to_string().c_str(), errno, strerror(errno)); - } +static bool copy_push_mkdir(const file_stats& dst_stats, copy_context_t& ctx) noexcept +{ + // atomically, using unpredictable '.'+rand temp dir (if target dir non-existent) + // and drops read-permissions for user and all of group and others after fetching its dirfd. + bool new_dir = false; + std::string basename_; + const int dest_dirfd = ctx.dst_dirfds.back(); + if( dst_stats.is_dir() ) { + if( is_set(ctx.copts, copy_options::verbose) ) { + jau::fprintf_td(stderr, "copy: mkdir directory already exist: %s\n", dst_stats.to_string().c_str()); + } + basename_ = dst_stats.item().basename(); + } else if( !dst_stats.exists() ) { + new_dir = true; + constexpr const uint64_t val_min = 888; + constexpr const uint64_t val_max = 999999999999999; // 15 digits + uint64_t mkdir_cntr = 0; + std::mt19937_64 prng; + std::uniform_int_distribution<uint64_t> prng_dist(val_min, val_max); + bool mkdir_ok = false; + do { + ++mkdir_cntr; + const uint64_t val = prng_dist(prng); + basename_ = "."+std::to_string(val); + if( 0 == ::mkdirat(dest_dirfd, basename_.c_str(), jau::fs::posix_protection_bits(fmode_t::rwx_usr)) ) { + mkdir_ok = true; + } else if (errno != EINTR && errno != EEXIST) { + ERR_PRINT("mkdir failed: %s, temp '%s'", dst_stats.to_string().c_str(), basename_.c_str()); + return false; + } // else continue on EINTR or EEXIST + } while( !mkdir_ok && mkdir_cntr < val_max ); + if( !mkdir_ok ) { + ERR_PRINT("mkdir failed: %s", dst_stats.to_string().c_str()); return false; } } else { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: mkdir failed: %s, exists but is no dir\n", dest_stats.to_string().c_str()); + ERR_PRINT("mkdir failed: %s, exists but is no dir", dst_stats.to_string().c_str()); + return false; + } + // open dirfd + const int new_dirfd = ::openat64(dest_dirfd, basename_.c_str(), _open_dir_flags); + if ( 0 > new_dirfd ) { + if( new_dir ) { + ERR_PRINT("Couldn't open new dir %s, temp '%s'", dst_stats.to_string().c_str(), basename_.c_str()); + } else { + ERR_PRINT("Couldn't open new dir %s", dst_stats.to_string().c_str()); + } + if( new_dir ) { + ::unlinkat(dest_dirfd, basename_.c_str(), AT_REMOVEDIR); } return false; } - return true; -} - -static bool copy_dir_preserve(const file_stats& source_stats, const file_stats& dest_stats, const copy_options copts) noexcept { - const std::string dest_path = dest_stats.path(); - if( !dest_stats.is_dir() ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: dir_preserve failed: %s, is no dir\n", dest_stats.to_string().c_str()); + // drop read permissions to render destination directory transaction safe until copy is done + if( 0 != ::fchmod(new_dirfd, jau::fs::posix_protection_bits(fmode_t::write_usr | fmode_t::exec_usr)) ) { + if( new_dir ) { + ::unlinkat(dest_dirfd, basename_.c_str(), AT_REMOVEDIR); + ERR_PRINT("zero permissions on dest %s, temp '%s'", dst_stats.to_string().c_str(), basename_.c_str()); + } else { + ERR_PRINT("zero permissions on dest %s", dst_stats.to_string().c_str()); } + ::close(new_dirfd); return false; } - const file_stats* target_stats = source_stats.is_link() ? source_stats.link_target().get() : &source_stats; + if( new_dir ) { + const int rename_flags = 0; // Not supported on all fs: RENAME_NOREPLACE + if( 0 != ::renameat2(dest_dirfd, basename_.c_str(), dest_dirfd, dst_stats.item().basename().c_str(), rename_flags) ) { + ERR_PRINT("rename temp to dest, temp '%s', dest %s", basename_.c_str(), dst_stats.to_string().c_str()); + ::unlinkat(dest_dirfd, basename_.c_str(), AT_REMOVEDIR); + ::close(new_dirfd); + return false; + } + } + ctx.dst_dirfds.push_back(new_dirfd); + return true; +} + +static bool copy_dir_preserve(const file_stats& src_stats, const int dst_dirfd, const std::string& dst_basename, const copy_options copts) noexcept { + const file_stats* target_stats = src_stats.is_link() ? src_stats.link_target().get() : &src_stats; // restore permissions const fmode_t dest_mode = target_stats->prot_mode(); - if( 0 != ::fchmodat(AT_FDCWD, dest_path.c_str(), jau::fs::posix_protection_bits( dest_mode ), 0) ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: dir_preserve restore permissions, source %s, dest '%s', errno %d (%s)\n", - source_stats.to_string().c_str(), dest_path.c_str(), errno, ::strerror(errno)); - } + if( 0 != ::fchmod(dst_dirfd, jau::fs::posix_protection_bits( dest_mode )) ) { + ERR_PRINT("restore permissions, source %s, dest '%s'", src_stats.to_string().c_str(), dst_basename.c_str()); return false; } if( is_set(copts, copy_options::preserve_all) ) { // preserve time struct timespec ts2[2] = { target_stats->atime().to_timespec(), target_stats->mtime().to_timespec() }; - if( 0 != ::utimensat(AT_FDCWD, dest_path.c_str(), ts2, 0) ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: dir_preserve time of file failed, source %s, dest '%s', errno %d (%s)\n", - source_stats.to_string().c_str(), dest_stats.to_string().c_str(), errno, ::strerror(errno)); - } + if( 0 != ::futimens(dst_dirfd, ts2) ) { + ERR_PRINT("preserve time of file failed, source %s, dest '%s'", src_stats.to_string().c_str(), dst_basename.c_str()); return false; } // preserve ownership const uid_t caller_uid = ::geteuid(); const ::uid_t source_uid = 0 == caller_uid ? target_stats->uid() : -1; - if( 0 != ::chown( dest_path.c_str(), source_uid, target_stats->gid() ) ) { - if( errno != EPERM && errno != EINVAL ) { // OK to fail due to permissions - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: dir_preserve ownership of file failed, uid(caller %" PRIu32 ", chown %" PRIu32 "), source %s, dest '%s', errno %d (%s)\n", - caller_uid, source_uid, source_stats.to_string().c_str(), dest_stats.to_string().c_str(), errno, ::strerror(errno)); - } + if( 0 != ::fchown(dst_dirfd, source_uid, target_stats->gid()) ) { + if( errno != EPERM && errno != EINVAL ) { + ERR_PRINT("dir_preserve ownership of file failed, uid(caller %" PRIu32 ", chown %" PRIu32 "), source %s, dest '%s'", + caller_uid, source_uid, src_stats.to_string().c_str(), dst_basename.c_str()); return false; } + // OK to fail due to permissions if( is_set(copts, copy_options::verbose) ) { jau::fprintf_td(stderr, "copy: Ignored: dir_preserve ownership of file failed, uid(caller %" PRIu32 ", chown %" PRIu32 "), source %s, dest '%s', errno %d (%s)\n", - caller_uid, source_uid, source_stats.to_string().c_str(), dest_stats.to_string().c_str(), errno, ::strerror(errno)); + caller_uid, source_uid, src_stats.to_string().c_str(), dst_basename.c_str(), errno, ::strerror(errno)); } } } + if( is_set(copts, copy_options::sync) ) { + if( 0 != ::fsync(dst_dirfd) ) { + ERR_PRINT("Couldn't synchronize destination file '%s'", dst_basename.c_str()); + return false; + } + } return true; } @@ -1324,26 +1447,11 @@ bool jau::fs::copy(const std::string& source_path, const std::string& target_pat } file_stats source_stats(source_path); file_stats target_stats(target_path); - bool target_path_is_root = false; - if( source_stats.is_dir() ) { - if( !is_set(copts, copy_options::recursive) ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: source_path is dir but !recursive, %s\n", source_stats.to_string().c_str()); - } - return false; - } - if( !target_stats.exists() ) { - target_stats = file_stats(target_stats.path()); // update - target_path_is_root = true; - } else if( !target_stats.is_dir() ) { - if( is_set(copts, copy_options::verbose) ) { - jau::fprintf_td(stderr, "copy: Error: source_path is dir but target_path not, source %s, target %s\n", - source_stats.to_string().c_str(), target_stats.to_string().c_str()); - } - return false; - } - } else if( source_stats.is_file() ) { + if( source_stats.is_file() ) { + // + // single file copy + // if( target_stats.exists() ) { if( target_stats.is_file() ) { if( !is_set(copts, copy_options::overwrite)) { @@ -1353,66 +1461,189 @@ bool jau::fs::copy(const std::string& source_path, const std::string& target_pat } return false; } - target_path_is_root = true; + } // else file2file to directory + } // else file2file to explicit new file + const int src_dirfd = ::openat64(AT_FDCWD, source_stats.item().dirname().c_str(), _open_dir_flags); + if ( 0 > src_dirfd ) { + ERR_PRINT("source_path dir couldn't be opened, source %s", source_stats.to_string().c_str()); + return false; + } + + std::string dst_basename; + int dst_dirfd = -1; + if( target_stats.is_dir() ) { + // file2file to directory + dst_dirfd = ::openat64(AT_FDCWD, target_stats.path().c_str(), _open_dir_flags); + if ( 0 > dst_dirfd ) { + ERR_PRINT("target dir couldn't be opened, target %s", target_stats.to_string().c_str()); + ::close(src_dirfd); + return false; } + dst_basename = source_stats.item().basename(); } else { - // else new file2file - target_path_is_root = true; + // file2file to file + file_stats target_parent_stats(target_stats.item().dirname()); + if( !target_parent_stats.is_dir() ) { + if( is_set(copts, copy_options::verbose) ) { + jau::fprintf_td(stderr, "copy: Error: target parent is not an existing directory, target %s, target_parent %s\n", + target_stats.to_string().c_str(), target_parent_stats.to_string().c_str()); + } + ::close(src_dirfd); + return false; + } + dst_dirfd = ::openat64(AT_FDCWD, target_parent_stats.path().c_str(), _open_dir_flags); + if ( 0 > dst_dirfd ) { + ERR_PRINT("target_parent dir couldn't be opened, target %s, target_parent %s", + target_stats.to_string().c_str(), target_parent_stats.to_string().c_str()); + ::close(src_dirfd); + return false; + } + dst_basename = target_stats.item().basename(); } - } else { + if( !copy_file(src_dirfd, source_stats, dst_dirfd, dst_basename, copts) ) { + return false; + } + ::close(src_dirfd); + ::close(dst_dirfd); + return true; + } + if( !source_stats.is_dir() ) { if( is_set(copts, copy_options::verbose) ) { jau::fprintf_td(stderr, "copy: Error: source_path is neither file nor dir, source %s, target %s\n", source_stats.to_string().c_str(), target_stats.to_string().c_str()); } return false; } - struct copy_context_t { - file_stats source_stats; - size_t source_path_len; - std::string source_basename; - file_stats target_stats; - copy_options copts; - bool target_path_is_root; - }; - copy_context_t copy_context { source_stats, source_path.size(), basename(source_path), target_stats, copts, target_path_is_root }; - const path_visitor pv = jau::bindCaptureRefFunc<bool, copy_context_t, traverse_event, const file_stats&>(©_context, + + // + // directory copy + // + copy_context_t ctx { copts, 0, std::vector<int>(), std::vector<int>() }; + + if( !is_set(copts, copy_options::recursive) ) { + if( is_set(copts, copy_options::verbose) ) { + jau::fprintf_td(stderr, "copy: Error: source_path is dir but !recursive, %s\n", source_stats.to_string().c_str()); + } + return false; + } + if( target_stats.exists() && !target_stats.is_dir() ) { + if( is_set(copts, copy_options::verbose) ) { + jau::fprintf_td(stderr, "copy: Error: source_path is dir but target_path exist and is no dir, source %s, target %s\n", + source_stats.to_string().c_str(), target_stats.to_string().c_str()); + } + return false; + } + { + const int src_dirfd = ::openat64(AT_FDCWD, source_stats.item().dirname().c_str(), _open_dir_flags); + if ( 0 > src_dirfd ) { + ERR_PRINT("source_path dirname couldn't be opened, source %s", source_stats.to_string().c_str()); + return false; + } + ctx.src_dirfds.push_back(src_dirfd); + } + if( target_stats.is_dir() ) { + // Case: If dest_path exists as a directory, source_path dir will be copied below the dest_path directory. + const int dst_dirfd = ::openat64(AT_FDCWD, target_stats.path().c_str(), _open_dir_flags); + if ( 0 > dst_dirfd ) { + ERR_PRINT("target dir couldn't be opened, target %s", target_stats.to_string().c_str()); + ::close(ctx.src_dirfds.back()); + return false; + } + ctx.dst_dirfds.push_back(dst_dirfd); + } else { + // Case: If dest_path doesn't exist, source_path dir content is copied into the newly created dest_path. + file_stats target_parent_stats(target_stats.item().dirname()); + if( !target_parent_stats.is_dir() ) { + if( is_set(copts, copy_options::verbose) ) { + jau::fprintf_td(stderr, "copy: Error: target parent is not an existing directory, target %s, target_parent %s\n", + target_stats.to_string().c_str(), target_parent_stats.to_string().c_str()); + } + ::close(ctx.src_dirfds.back()); + return false; + } + const int dst_dirfd = ::openat64(AT_FDCWD, target_parent_stats.path().c_str(), _open_dir_flags); + if ( 0 > dst_dirfd ) { + ERR_PRINT("target dirname couldn't be opened, target %s, target_parent %s", + target_stats.to_string().c_str(), target_parent_stats.to_string().c_str()); + ::close(ctx.src_dirfds.back()); + return false; + } + ctx.dst_dirfds.push_back(dst_dirfd); + + if( !copy_push_mkdir(target_stats, ctx) ) { + return false; + } + ctx.skip_dst_dir_mkdir = 1; + } + const path_visitor pv = jau::bindCaptureRefFunc<bool, copy_context_t, traverse_event, const file_stats&>(&ctx, ( bool(*)(copy_context_t*, traverse_event, const file_stats&) ) /* help template type deduction of function-ptr */ - ( [](copy_context_t* ctx, traverse_event tevt, const file_stats& element_stats) -> bool { + ( [](copy_context_t* ctx_ptr, traverse_event tevt, const file_stats& element_stats) -> bool { if( !element_stats.has_access() ) { - if( is_set(ctx->copts, copy_options::verbose) ) { + if( is_set(ctx_ptr->copts, copy_options::verbose) ) { jau::fprintf_td(stderr, "copy: Error: remove failed: no access, %s\n", element_stats.to_string().c_str()); } return false; } - std::string element_path = element_stats.path(); - std::string element_path_trail; - if( ctx->source_path_len < element_path.size() ) { - element_path_trail = _slash + element_path.substr(ctx->source_path_len+1); - } - std::string target_path_; - if( ctx->target_path_is_root ) { - target_path_ = ctx->target_stats.path() + element_path_trail; - } else { - target_path_ = ctx->target_stats.path() + _slash + ctx->source_basename + element_path_trail; + if( ctx_ptr->src_dirfds.size() < 1 || ctx_ptr->dst_dirfds.size() < 1 || + ctx_ptr->src_dirfds.size() != ctx_ptr->dst_dirfds.size() - ctx_ptr->skip_dst_dir_mkdir ) + { + ERR_PRINT("dirfd stack error: count[src %zu, dst %zu, dst_skip %d] @ %s", + ctx_ptr->src_dirfds.size(), ctx_ptr->dst_dirfds.size(), ctx_ptr->skip_dst_dir_mkdir, element_stats.to_string().c_str()); + return false; } + const int src_dirfd = ctx_ptr->src_dirfds.back(); + const int dst_dirfd = ctx_ptr->dst_dirfds.back(); + const std::string& basename_ = element_stats.item().basename(); if( is_set(tevt, traverse_event::dir_entry) ) { - const file_stats target_stats_(target_path_); - if( !copy_mkdir( element_stats, target_stats_, ctx->copts ) ) { + const int new_src_dirfd = ::openat64(src_dirfd, basename_.c_str(), _open_dir_flags); + if ( 0 > new_src_dirfd ) { + ERR_PRINT("entered source_path dir couldn't be opened, source %s", element_stats.to_string().c_str()); return false; } + ctx_ptr->src_dirfds.push_back(new_src_dirfd); + + if( 0 < ctx_ptr->skip_dst_dir_mkdir ) { + --ctx_ptr->skip_dst_dir_mkdir; + } else { + const file_stats target_stats_(dst_dirfd, basename_); + if( !copy_push_mkdir(target_stats_, *ctx_ptr) ) { + return false; + } + } } else if( is_set(tevt, traverse_event::dir_exit) ) { - const file_stats target_stats_(target_path_); - if( !copy_dir_preserve( element_stats, target_stats_, ctx->copts ) ) { + if( ctx_ptr->src_dirfds.size() < 2 || ctx_ptr->dst_dirfds.size() < 2 ) { + ERR_PRINT("dirfd stack error: count[src %zu, dst %zu] @ %s", + ctx_ptr->src_dirfds.size(), ctx_ptr->dst_dirfds.size(), element_stats.to_string().c_str()); return false; } + if( !copy_dir_preserve( element_stats, dst_dirfd, basename_, ctx_ptr->copts ) ) { + return false; + } + ::close(dst_dirfd); + ::close(src_dirfd); + ctx_ptr->dst_dirfds.pop_back(); + ctx_ptr->src_dirfds.pop_back(); } else if( is_set(tevt, traverse_event::file) || is_set(tevt, traverse_event::symlink) || is_set(tevt, traverse_event::dir_symlink) ) { - if( !copy_file(element_stats, target_path_, ctx->copts) ) { + if( !copy_file(src_dirfd, element_stats, dst_dirfd, basename_, ctx_ptr->copts) ) { return false; } } return true; } ) ); - return jau::fs::visit(source_stats, topts, pv); + bool res = jau::fs::visit(source_stats, topts, pv); + if( ctx.src_dirfds.size() != 1 || ctx.src_dirfds.size() != ctx.dst_dirfds.size() ) { + ERR_PRINT("dirfd stack error: count[src %zu, dst %zu]", ctx.src_dirfds.size(), ctx.dst_dirfds.size()); + res = false; + } + while( !ctx.dst_dirfds.empty() ) { + ::close(ctx.dst_dirfds.back()); + ctx.dst_dirfds.pop_back(); + } + while( !ctx.src_dirfds.empty() ) { + ::close(ctx.src_dirfds.back()); + ctx.src_dirfds.pop_back(); + } + return res; } static bool set_effective_uid(::uid_t user_id) { |