#pragma once
#include "model/FileRecord.h"
#include <QThread>
#include <QString>
#include <QList>
#include <QSet>
#include <atomic>
namespace whittle {
// Direct-write, one-way copier run on its own thread. Copies each record to
// <usbMount>/<relPath>, creating the full path (mkdir -p) so /a/photos is
// reproduced. Preserves mode/owner/mtime/xattrs where the target FS allows.
// Skips (and reports) files with FAT-illegal names or case-collisions and
// individual IO/permission errors. If the USB disappears mid-run it STOPS
// and waits, rather than skipping -- that is a device event, not a file error.
class MergeController : public QThread {
Q_OBJECT
public:
enum class Skip { IllegalName, CaseCollision, IoError };
MergeController(QList<FileRecord> items, QString usbMount,
QObject* parent = nullptr);
void cancel() { m_cancel.store(true, std::memory_order_relaxed); }
qint64 totalBytes() const { return m_totalBytes; }
int fileCount() const { return m_fileCount; }
signals:
void progress(QString currentRel, qint64 bytesDone, int filesDone);
void skipped(QString relPath, int reason); // reason = Skip
void deviceLost();
void done(bool canceled);
protected:
void run() override;
private:
enum class Result { Ok, IllegalName, CaseCollision, IoError, DeviceLost };
Result copyFile(const FileRecord& rec, const QString& dest,
qint64& bytesDone, int filesDone, bool caseInsensitive);
Result makeDir (const FileRecord& rec, const QString& dest);
bool deviceGone() const;
QList<FileRecord> m_items;
QString m_usbMount;
qint64 m_totalBytes = 0;
int m_fileCount = 0;
bool m_fatNames = false;
QSet<QString> m_writtenLower; // in-run case-collision tracking
std::atomic<bool> m_cancel{false};
};
} // namespace whittle