Pith - whittle
#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;
}

}