#include "core/MergeController.h"
#include <QFile>
#include <QStorageInfo>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/xattr.h>
#include <unistd.h>
#include <cerrno>
#include <vector>
namespace whittle {
namespace {
constexpr qsizetype kBuf = 1 << 20;
constexpr qint64 kEmitEvery = 8 << 20;
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 '|': return true;
}
}
return name.toUtf8().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, QString usbMount,
QObject* parent)
: QThread(parent), m_items(std::move(items)), 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();
}
void MergeController::run() {
const FsClass fs = classifyFs(m_usbMount);
m_fatNames = fs.fatNames;
qint64 bytesDone = 0;
int filesDone = 0;
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;
Result r = rec.isDir ? makeDir(rec, dest)
: copyFile(rec, dest, bytesDone, filesDone,
fs.caseInsensitive);
switch (r) {
case Result::Ok:
if (!rec.isDir) { ++filesDone; emit progress(rec.relPath, bytesDone, filesDone); }
break;
case Result::IllegalName:
bytesDone += rec.size; emit skipped(rec.relPath, int(Skip::IllegalName)); break;
case Result::CaseCollision:
bytesDone += rec.size; emit skipped(rec.relPath, int(Skip::CaseCollision)); break;
case Result::IoError:
bytesDone += rec.size; emit skipped(rec.relPath, int(Skip::IoError)); break;
case Result::DeviceLost:
emit deviceLost(); emit done(true); return; // stop and wait
}
}
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 deviceGone() ? Result::DeviceLost : Result::IoError;
if (::mkdir(d.constData(), 0777) != 0 && errno != EEXIST)
return deviceGone() ? Result::DeviceLost : Result::IoError;
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);
struct statx s;
if (::statx(AT_FDCWD, srcEnc.constData(), 0,
STATX_MODE | STATX_UID | STATX_GID | STATX_MTIME, &s) != 0)
return Result::IoError;
if (!ensureParents(dest)) return deviceGone() ? Result::DeviceLost : Result::IoError;
const int in = ::open(srcEnc.constData(), O_RDONLY);
if (in < 0) return Result::IoError;
const int out = ::open(dstEnc.constData(), O_WRONLY | O_CREAT | O_TRUNC,
s.stx_mode & 07777);
if (out < 0) { ::close(in); return deviceGone() ? Result::DeviceLost : Result::IoError; }
std::vector<char> buf(kBuf);
qint64 sinceEmit = 0;
Result res = Result::Ok;
for (;;) {
if (m_cancel.load(std::memory_order_relaxed)) { res = Result::Ok; break; }
const ssize_t n = ::read(in, buf.data(), buf.size());
if (n == 0) break;
if (n < 0) { res = Result::IoError; break; }
ssize_t off = 0;
while (off < n) {
const ssize_t w = ::write(out, buf.data() + off, n - off);
if (w < 0) { res = deviceGone() ? Result::DeviceLost : Result::IoError; 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) {
::fchown(out, s.stx_uid, s.stx_gid);
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;
::futimens(out, ts);
}
::close(out);
if (res == Result::Ok) copyXattrs(srcEnc, dstEnc);
else ::unlink(dstEnc.constData());
return res;
}
}