346 lines
15 KiB
C++
346 lines
15 KiB
C++
// *****************************************************************************
|
|
// * This file is part of the FreeFileSync project. It is distributed under *
|
|
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
|
|
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
|
|
// *****************************************************************************
|
|
|
|
#include "file_io.h"
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h> //open
|
|
#include <unistd.h> //close, read, write
|
|
|
|
using namespace zen;
|
|
|
|
|
|
size_t FileBase::getBlockSize() //throw FileError
|
|
{
|
|
if (blockSizeBuf_ == 0)
|
|
{
|
|
/* - statfs::f_bsize - "optimal transfer block size"
|
|
- stat::st_blksize - "blocksize for file system I/O. Writing in smaller chunks may cause an inefficient read-modify-rewrite."
|
|
|
|
e.g. local disk: f_bsize 4096 st_blksize 4096
|
|
USB memory: f_bsize 32768 st_blksize 32768 */
|
|
const auto st_blksize = getStatBuffered().st_blksize; //throw FileError
|
|
if (st_blksize > 0) //st_blksize is signed!
|
|
blockSizeBuf_ = st_blksize; //
|
|
|
|
blockSizeBuf_ = std::max(blockSizeBuf_, defaultBlockSize);
|
|
//ha, convergent evolution! https://github.com/coreutils/coreutils/blob/master/src/ioblksize.h#L74
|
|
}
|
|
return blockSizeBuf_;
|
|
}
|
|
|
|
|
|
const struct stat& FileBase::getStatBuffered() //throw FileError
|
|
{
|
|
if (!statBuf_)
|
|
try
|
|
{
|
|
if (hFile_ == invalidFileHandle)
|
|
throw SysError(L"Contract error: getStatBuffered() called after close().");
|
|
|
|
struct stat fileInfo = {};
|
|
if (::fstat(hFile_, &fileInfo) != 0)
|
|
THROW_LAST_SYS_ERROR("fstat");
|
|
statBuf_ = std::move(fileInfo);
|
|
}
|
|
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(filePath_)), e.toString()); }
|
|
|
|
return *statBuf_;
|
|
}
|
|
|
|
|
|
FileBase::~FileBase()
|
|
{
|
|
if (hFile_ != invalidFileHandle)
|
|
try
|
|
{
|
|
close(); //throw FileError
|
|
}
|
|
catch (const FileError& e) { logExtraError(e.toString()); }
|
|
}
|
|
|
|
|
|
void FileBase::close() //throw FileError
|
|
{
|
|
try
|
|
{
|
|
if (hFile_ == invalidFileHandle)
|
|
throw SysError(L"Contract error: close() called more than once.");
|
|
if (::close(hFile_) != 0)
|
|
THROW_LAST_SYS_ERROR("close");
|
|
hFile_ = invalidFileHandle; //do NOT set on error! => ~FileOutputPlain() still wants to (try to) delete the file!
|
|
}
|
|
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), e.toString()); }
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------------------
|
|
|
|
namespace
|
|
{
|
|
std::pair<FileBase::FileHandle, struct stat>
|
|
openHandleForRead(const Zstring& filePath) //throw FileError, ErrorFileLocked
|
|
{
|
|
try
|
|
{
|
|
//caveat: check for file types that block during open(): character device, block device, named pipe
|
|
struct stat fileInfo = {};
|
|
if (::stat(filePath.c_str(), &fileInfo) != 0) //follows symlinks
|
|
THROW_LAST_SYS_ERROR("stat");
|
|
|
|
if (!S_ISREG(fileInfo.st_mode) &&
|
|
!S_ISDIR(fileInfo.st_mode) && //open() will fail with "EISDIR: Is a directory" => nice
|
|
!S_ISLNK(fileInfo.st_mode)) //?? shouldn't be possible after successful stat()
|
|
{
|
|
const std::wstring typeName = [m = fileInfo.st_mode]
|
|
{
|
|
std::wstring name =
|
|
S_ISCHR (m) ? L"character device" : //e.g. /dev/null
|
|
S_ISBLK (m) ? L"block device" : //e.g. /dev/sda1
|
|
S_ISFIFO(m) ? L"FIFO, named pipe" :
|
|
S_ISSOCK(m) ? L"socket" : L""; //doesn't block but open() error is unclear: "ENXIO: No such device or address"
|
|
if (!name.empty())
|
|
name += L", ";
|
|
return name + printNumber<std::wstring>(L"0%06o", m & S_IFMT);
|
|
}();
|
|
throw SysError(_("Unsupported item type.") + L" [" + typeName + L']');
|
|
}
|
|
|
|
//don't use O_DIRECT: https://yarchive.net/comp/linux/o_direct.html
|
|
const int fdFile = ::open(filePath.c_str(), O_RDONLY | O_CLOEXEC);
|
|
if (fdFile == -1) //don't check "< 0" -> docu seems to allow "-2" to be a valid file handle
|
|
THROW_LAST_SYS_ERROR("open");
|
|
return {fdFile /*pass ownership*/, fileInfo};
|
|
}
|
|
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(filePath)), e.toString()); }
|
|
}
|
|
}
|
|
|
|
|
|
FileInputPlain::FileInputPlain(const Zstring& filePath) :
|
|
FileInputPlain(openHandleForRead(filePath), filePath) {} //throw FileError, ErrorFileLocked
|
|
|
|
|
|
FileInputPlain::FileInputPlain(const std::pair<FileBase::FileHandle, struct stat>& fileDetails, const Zstring& filePath) :
|
|
FileInputPlain(fileDetails.first, filePath)
|
|
{
|
|
setStatBuffered(fileDetails.second);
|
|
}
|
|
|
|
|
|
FileInputPlain::FileInputPlain(FileHandle handle, const Zstring& filePath) :
|
|
FileBase(handle, filePath)
|
|
{
|
|
//optimize read-ahead on input file:
|
|
if (::posix_fadvise(getHandle(), 0 /*offset*/, 0 /*len*/, POSIX_FADV_SEQUENTIAL) != 0) //"len == 0" means "end of the file"
|
|
THROW_LAST_FILE_ERROR(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(filePath)), "posix_fadvise(POSIX_FADV_SEQUENTIAL)");
|
|
|
|
/* - POSIX_FADV_SEQUENTIAL is like POSIX_FADV_NORMAL, but with twice the read-ahead buffer size
|
|
- POSIX_FADV_NOREUSE "since kernel 2.6.18 this flag is a no-op" WTF!?
|
|
- POSIX_FADV_DONTNEED may be used to clear the OS file system cache (offset and len must be page-aligned!)
|
|
=> does nothing, unless data was already written to disk: https://insights.oetiker.ch/linux/fadvise/
|
|
- POSIX_FADV_WILLNEED: issue explicit read-ahead; almost the same as readahead(), but with weaker error checking
|
|
https://unix.stackexchange.com/questions/681188/difference-between-posix-fadvise-and-readahead
|
|
|
|
clear file system cache manually: sync; echo 3 > /proc/sys/vm/drop_caches */
|
|
|
|
}
|
|
|
|
|
|
//may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0!
|
|
size_t FileInputPlain::tryRead(void* buffer, size_t bytesToRead) //throw FileError, ErrorFileLocked
|
|
{
|
|
if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check!
|
|
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
|
|
assert(bytesToRead % getBlockSize() == 0);
|
|
try
|
|
{
|
|
ssize_t bytesRead = 0;
|
|
do
|
|
{
|
|
bytesRead = ::read(getHandle(), buffer, bytesToRead);
|
|
}
|
|
while (bytesRead < 0 && errno == EINTR); //Compare copy_reg() in copy.c: ftp://ftp.gnu.org/gnu/coreutils/coreutils-8.23.tar.xz
|
|
//EINTR is not checked on macOS' copyfile: https://opensource.apple.com/source/copyfile/copyfile-173.40.2/copyfile.c.auto.html
|
|
//read() on macOS: https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man2/read.2.html
|
|
//if ::read is interrupted (EINTR) right in the middle, it will return successfully with "bytesRead < bytesToRead"
|
|
|
|
if (bytesRead < 0)
|
|
THROW_LAST_SYS_ERROR("read");
|
|
|
|
ASSERT_SYSERROR(makeUnsigned(bytesRead) <= bytesToRead); //better safe than sorry
|
|
return bytesRead; //"zero indicates end of file"
|
|
}
|
|
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(getFilePath())), e.toString()); }
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------------------
|
|
|
|
namespace
|
|
{
|
|
FileBase::FileHandle openHandleForWrite(const Zstring& filePath) //throw FileError, ErrorTargetExisting
|
|
{
|
|
try
|
|
{
|
|
//check for named pipe, etc.? not needed, open() + O_WRONLY should fail fast
|
|
|
|
const mode_t lockFileMode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; //0666 => umask will be applied implicitly!
|
|
|
|
//O_EXCL contains a race condition on NFS file systems: https://linux.die.net/man/2/open
|
|
const int fdFile = ::open(filePath.c_str(), //const char* pathname
|
|
O_CREAT | //int flags
|
|
/*access == FileOutput::ACC_OVERWRITE ? O_TRUNC : */ O_EXCL | O_WRONLY | O_CLOEXEC,
|
|
lockFileMode); //mode_t mode
|
|
if (fdFile == -1)
|
|
{
|
|
const int ec = errno; //copy before making other system calls!
|
|
std::wstring errorDescr = formatSystemError("open", ec);
|
|
|
|
if (ec == EEXIST)
|
|
throw ErrorTargetExisting(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(filePath)), errorDescr);
|
|
|
|
if (ec == EINVAL)
|
|
for (const Zchar c : getItemName(filePath))
|
|
if (contains(fileNameForbiddenChars, c))
|
|
{
|
|
errorDescr += L' ' + replaceCpy(_("Unsupported character %x"), L"%x", L"'" + utfTo<std::wstring>(c) + L"'");
|
|
break;
|
|
}
|
|
|
|
throw SysError(errorDescr);
|
|
}
|
|
return fdFile; //pass ownership
|
|
}
|
|
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(filePath)), e.toString()); }
|
|
}
|
|
}
|
|
|
|
|
|
FileOutputPlain::FileOutputPlain(const Zstring& filePath) :
|
|
FileOutputPlain(openHandleForWrite(filePath), filePath) {} //throw FileError, ErrorTargetExisting
|
|
|
|
|
|
FileOutputPlain::FileOutputPlain(FileHandle handle, const Zstring& filePath) :
|
|
FileBase(handle, filePath)
|
|
{
|
|
}
|
|
|
|
|
|
FileOutputPlain::~FileOutputPlain()
|
|
{
|
|
|
|
if (getHandle() != invalidFileHandle) //not finalized => clean up garbage
|
|
try
|
|
{
|
|
//"deleting while handle is open" == FILE_FLAG_DELETE_ON_CLOSE
|
|
if (::unlink(getFilePath().c_str()) != 0)
|
|
THROW_LAST_SYS_ERROR("unlink");
|
|
}
|
|
catch (const SysError& e)
|
|
{
|
|
logExtraError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getFilePath())) + L"\n\n" + e.toString());
|
|
}
|
|
}
|
|
|
|
|
|
void FileOutputPlain::reserveSpace(uint64_t expectedSize) //throw FileError
|
|
{
|
|
//NTFS: "If you set the file allocation info [...] the file contents will be forced into nonresident data, even if it would have fit inside the MFT."
|
|
if (expectedSize < 1024) //https://docs.microsoft.com/en-us/archive/blogs/askcore/the-four-stages-of-ntfs-file-growth
|
|
return;
|
|
|
|
try
|
|
{
|
|
#if 0 /* fallocate(FALLOC_FL_KEEP_SIZE):
|
|
- perf: no real benefit (in a quick and dirty local test)
|
|
- breaks Btrfs compression: https://freefilesync.org/forum/viewtopic.php?t=10356
|
|
- apparently not even used by cp: https://github.com/coreutils/coreutils/blob/17479ef60c8edbd2fe8664e31a7f69704f0cd221/src/copy.c#LL1234C5-L1234C5 */
|
|
|
|
//don't use ::posix_fallocate which uses horribly inefficient fallback if FS doesn't support it (EOPNOTSUPP) and changes files size!
|
|
//FALLOC_FL_KEEP_SIZE => allocate only, file size is NOT changed!
|
|
if (::fallocate(getHandle(), //int fd
|
|
FALLOC_FL_KEEP_SIZE, //int mode
|
|
0, //off_t offset
|
|
expectedSize) != 0) //off_t len
|
|
if (errno != EOPNOTSUPP) //possible, unlike with posix_fallocate()
|
|
THROW_LAST_SYS_ERROR("fallocate");
|
|
#endif
|
|
|
|
}
|
|
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), e.toString()); }
|
|
}
|
|
|
|
|
|
//may return short! CONTRACT: bytesToWrite > 0
|
|
size_t FileOutputPlain::tryWrite(const void* buffer, size_t bytesToWrite) //throw FileError
|
|
{
|
|
if (bytesToWrite == 0)
|
|
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
|
|
assert(bytesToWrite % getBlockSize() == 0 || bytesToWrite < getBlockSize());
|
|
try
|
|
{
|
|
ssize_t bytesWritten = 0;
|
|
do
|
|
{
|
|
bytesWritten = ::write(getHandle(), buffer, bytesToWrite);
|
|
}
|
|
while (bytesWritten < 0 && errno == EINTR);
|
|
//write() on macOS: https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man2/write.2.html
|
|
//if ::write() is interrupted (EINTR) right in the middle, it will return successfully with "bytesWritten < bytesToWrite"!
|
|
|
|
if (bytesWritten <= 0)
|
|
{
|
|
if (bytesWritten == 0) //comment in safe-read.c suggests to treat this as an error due to buggy drivers
|
|
errno = ENOSPC;
|
|
|
|
THROW_LAST_SYS_ERROR("write");
|
|
}
|
|
|
|
ASSERT_SYSERROR(makeUnsigned(bytesWritten) <= bytesToWrite); //better safe than sorry
|
|
return bytesWritten;
|
|
}
|
|
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getFilePath())), e.toString()); }
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------------------
|
|
|
|
std::string zen::getFileContent(const Zstring& filePath, const IoCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, X
|
|
{
|
|
FileInputPlain fileIn(filePath); //throw FileError, ErrorFileLocked
|
|
|
|
return unbufferedLoad<std::string>([&](void* buffer, size_t bytesToRead)
|
|
{
|
|
const size_t bytesRead = fileIn.tryRead(buffer, bytesToRead); //throw FileError; may return short, only 0 means EOF! => CONTRACT: bytesToRead > 0!
|
|
if (notifyUnbufferedIO) notifyUnbufferedIO(bytesRead); //throw X!
|
|
return bytesRead;
|
|
},
|
|
fileIn.getBlockSize()); //throw FileError, X
|
|
}
|
|
|
|
|
|
void zen::setFileContent(const Zstring& filePath, const std::string_view byteStream, const IoCallback& notifyUnbufferedIO /*throw X*/) //throw FileError, X
|
|
{
|
|
const Zstring tmpFilePath = getPathWithTempName(filePath);
|
|
|
|
FileOutputPlain tmpFile(tmpFilePath); //throw FileError, (ErrorTargetExisting)
|
|
|
|
tmpFile.reserveSpace(byteStream.size()); //throw FileError
|
|
|
|
unbufferedSave(byteStream, [&](const void* buffer, size_t bytesToWrite)
|
|
{
|
|
const size_t bytesWritten = tmpFile.tryWrite(buffer, bytesToWrite); //throw FileError; may return short! CONTRACT: bytesToWrite > 0
|
|
if (notifyUnbufferedIO) notifyUnbufferedIO(bytesWritten); //throw X!
|
|
return bytesWritten;
|
|
},
|
|
tmpFile.getBlockSize()); //throw FileError, X
|
|
|
|
tmpFile.close(); //throw FileError
|
|
//take over ownership:
|
|
ZEN_ON_SCOPE_FAIL( try { removeFilePlain(tmpFilePath); }
|
|
catch (const FileError& e) { logExtraError(e.toString()); });
|
|
|
|
//operation finished: move temp file transactionally
|
|
moveAndRenameItem(tmpFilePath, filePath, true /*replaceExisting*/); //throw FileError, (ErrorMoveUnsupported), (ErrorTargetExisting)
|
|
}
|