Pith - whittle
whittle/src/core/MergeController.cpp [7.1 kb]
Modified: 19:51:41 130 026 (27 Jul 026)
0 Days Ago
#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;
}

} 
Updates
Whittle - Linux 130.026
Wedge - Android 126.026
Wedge - Linux 124.026
Shim - Android 117.026
Miter - 114.026
Dev
TVShow (227) 'CSA'
TVShow (228) 'APT'
TVProgram (83) 'BXT'
Miter Update(s)
Peen (Messaging)

Menu
Calendar
Project Tin (024/029)
Miter
RSS Feed
User Avatar
@vgmlr
=SUM(parts)