#include "core/MergeController.h"
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QStorageInfo>
#include <sys/stat.h>
#include <sys/xattr.h>
#include <unistd.h>
#include <vector>
namespace whittle {
namespace {
constexpr qsizetype kBuf = 1 << 20;
constexpr qint64 kEmitEvery = 8 << 20;
void logErrno(const char* op, const QByteArray& path) {
const int err = errno;
qWarning("whittle: %s %s: %s", op, path.constData(), ::strerror(err));
}
bool stopsMerge(int err) {
return err == EROFS || err == ENODEV || err == ENXIO;
}
QString tempPathFor(const QString& dest) {
return dest.left(dest.lastIndexOf('/')) + QStringLiteral("/.whittle.tmp");
}
struct FsClass { bool fatNames; bool caseInsensitive; };
FsClass classifyFs(const QString& mount) {
const QByteArray t = QStorageInfo(mount).fileSystemType().toLower();
const bool fat = t.contains("vfat") || t.contains("exfat") ||
t.contains("msdos") || t.contains("fat");
const bool ci = fat || t.contains("ntfs") || t.contains("fuseblk");
return { fat || t.contains("ntfs") || t.contains("fuseblk"), ci };
}
bool hasIllegalFatChar(const QString& name) {
for (QChar c : name) {
const ushort u = c.unicode();
if (u < 0x20) return true;
switch (u) {
case ':': case '?': case '*': case '"':
case '<': case '>': case '|': case '\\': return true;
}
}
return name.size() > 255;
}
bool ensureParents(const QString& path) {
const int slash = path.lastIndexOf('/');
if (slash <= 0) return true;
const QString parent = path.left(slash);
QByteArray acc;
for (const QByteArray seg : QFile::encodeName(parent).split('/')) {
acc += '/'; acc += seg;
if (seg.isEmpty()) continue;
if (::mkdir(acc.constData(), 0777) != 0 && errno != EEXIST) return false;
}
return true;
}
void copyXattrs(const QByteArray& src, const QByteArray& dst) {
ssize_t len = ::listxattr(src.constData(), nullptr, 0);
if (len <= 0) return;
std::vector<char> names(len);
len = ::listxattr(src.constData(), names.data(), names.size());
if (len <= 0) return;
for (ssize_t i = 0; i < len; ) {
const char* name = &names[i];
ssize_t vlen = ::getxattr(src.constData(), name, nullptr, 0);
if (vlen > 0) {
std::vector<char> val(vlen);
vlen = ::getxattr(src.constData(), name, val.data(), val.size());
if (vlen > 0) ::setxattr(dst.constData(), name, val.data(), vlen, 0);
}
i += ::strlen(name) + 1;
}
}
}
MergeController::MergeController(QList<FileRecord> items,
QList<FileRecord> deletes, QString usbMount, QObject* parent)
: QThread(parent), m_items(std::move(items)), m_deletes(std::move(deletes)),
m_usbMount(std::move(usbMount)) {
for (const FileRecord& r : m_items)
if (!r.isDir) { m_totalBytes += r.size; ++m_fileCount; }
}
bool MergeController::deviceGone() const {
return ::access(QFile::encodeName(m_usbMount).constData(), F_OK) != 0
|| !QStorageInfo(m_usbMount).isReady();
}
MergeController::Result MergeController::destError(const char* op,
const QByteArray& path) const {
const int err = errno;
logErrno(op, path);
return (stopsMerge(err) || deviceGone()) ? Result::DeviceLost
: Result::IoError;
}
void MergeController::run() {
const FsClass fs = classifyFs(m_usbMount);
m_fatNames = fs.fatNames;
qint64 bytesDone = 0;
int filesDone = 0;
for (const FileRecord& rec : m_deletes) {
if (m_cancel.load(std::memory_order_relaxed)) { emit done(true); return; }
const bool ok = rec.isDir ? QDir(rec.absPath).removeRecursively()
: QFile::remove(rec.absPath);
if (!ok && deviceGone()) { emit deviceLost(); emit done(true); return; }
emit removed(rec.relPath, ok);
}
for (const FileRecord& rec : m_items) {
if (m_cancel.load(std::memory_order_relaxed)) { emit done(true); return; }
const QString dest = m_usbMount + '/' + rec.relPath;
const qint64 start = bytesDone;
Result r = rec.isDir ? makeDir(rec, dest)
: copyFile(rec, dest, bytesDone, filesDone,
fs.caseInsensitive);
switch (r) {
case Result::Ok:
if (!rec.isDir) { ++filesDone; emit copied(rec.relPath);
emit progress(rec.relPath, bytesDone, filesDone); }
break;
case Result::IllegalName:
bytesDone = start + rec.size; emit skipped(rec.relPath, int(Skip::IllegalName)); break;
case Result::CaseCollision:
bytesDone = start + rec.size; emit skipped(rec.relPath, int(Skip::CaseCollision)); break;
case Result::IoError:
bytesDone = start + rec.size; emit skipped(rec.relPath, int(Skip::IoError)); break;
case Result::DeviceLost:
emit deviceLost(); emit done(true); return;
case Result::Canceled:
emit done(true); return;
}
}
emit done(false);
}
MergeController::Result MergeController::makeDir(const FileRecord& rec,
const QString& dest) {
const QByteArray d = QFile::encodeName(dest);
if (m_fatNames && hasIllegalFatChar(rec.relPath.section('/', -1)))
return Result::IllegalName;
if (!ensureParents(dest)) return destError("mkdir", d);
if (::mkdir(d.constData(), 0777) != 0 && errno != EEXIST)
return destError("mkdir", d);
struct statx s;
if (::statx(AT_FDCWD, QFile::encodeName(rec.absPath).constData(), 0,
STATX_MODE | STATX_MTIME, &s) == 0) {
::chmod(d.constData(), s.stx_mode & 07777);
struct timespec ts[2];
ts[0].tv_sec = ts[1].tv_sec = s.stx_mtime.tv_sec;
ts[0].tv_nsec = ts[1].tv_nsec = s.stx_mtime.tv_nsec;
::utimensat(AT_FDCWD, d.constData(), ts, 0);
}
return Result::Ok;
}
MergeController::Result MergeController::copyFile(const FileRecord& rec,
const QString& dest, qint64& bytesDone, int filesDone,
bool caseInsensitive) {
if (m_fatNames && hasIllegalFatChar(rec.relPath.section('/', -1)))
return Result::IllegalName;
if (caseInsensitive) {
const QString lower = dest.toLower();
if (m_writtenLower.contains(lower)) return Result::CaseCollision;
m_writtenLower.insert(lower);
}
const QByteArray srcEnc = QFile::encodeName(rec.absPath);
const QByteArray dstEnc = QFile::encodeName(dest);
const QByteArray tmpEnc = QFile::encodeName(tempPathFor(dest));
const auto destErr = [&](const char* op) { return destError(op, tmpEnc); };
struct statx s;
if (::statx(AT_FDCWD, srcEnc.constData(), 0,
STATX_MODE | STATX_UID | STATX_GID | STATX_MTIME, &s) != 0) {
logErrno("statx", srcEnc);
return Result::IoError;
}
if (!ensureParents(dest)) return destErr("mkdir");
const int in = ::open(srcEnc.constData(), O_RDONLY | O_CLOEXEC);
if (in < 0) { logErrno("open", srcEnc); return Result::IoError; }
const int out = ::open(tmpEnc.constData(),
O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600);
if (out < 0) { const Result r = destErr("open"); ::close(in); return r; }
std::vector<char> buf(kBuf);
qint64 sinceEmit = 0;
Result res = Result::Ok;
for (;;) {
if (m_cancel.load(std::memory_order_relaxed)) { res = Result::Canceled; break; }
const ssize_t n = ::read(in, buf.data(), buf.size());
if (n < 0 && errno == EINTR) continue;
if (n < 0) { logErrno("read", srcEnc); res = Result::IoError; break; }
if (n == 0) break;
ssize_t off = 0;
while (off < n) {
const ssize_t w = ::write(out, buf.data() + off, size_t(n - off));
if (w < 0 && errno == EINTR) continue;
if (w <= 0) {
if (w == 0) errno = EIO;
res = destErr("write");
break;
}
off += w; bytesDone += w; sinceEmit += w;
}
if (res != Result::Ok) break;
if (sinceEmit >= kEmitEvery) {
sinceEmit = 0; emit progress(rec.relPath, bytesDone, filesDone);
}
}
::close(in);
if (res == Result::Ok && ::fsync(out) != 0) res = destErr("fsync");
if (res == Result::Ok) ::fchown(out, s.stx_uid, s.stx_gid);
if (::close(out) != 0 && res == Result::Ok) res = destErr("close");
if (res == Result::Ok) {
copyXattrs(srcEnc, tmpEnc);
::chmod(tmpEnc.constData(), s.stx_mode & 07777);
struct timespec ts[2];
ts[0].tv_sec = ts[1].tv_sec = s.stx_mtime.tv_sec;
ts[0].tv_nsec = ts[1].tv_nsec = s.stx_mtime.tv_nsec;
::utimensat(AT_FDCWD, tmpEnc.constData(), ts, 0);
if (::rename(tmpEnc.constData(), dstEnc.constData()) != 0)
res = destErr("rename");
}
if (res != Result::Ok) ::unlink(tmpEnc.constData());
return res;
}
}