You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1303 lines
49 KiB
C++
1303 lines
49 KiB
C++
#include "directory.h"
|
|
#include "mimesniffer_p.h"
|
|
#include "task.h"
|
|
|
|
#include <QAbstractItemModelTester>
|
|
#include <QCryptographicHash>
|
|
#include <QMimeDatabase>
|
|
#include <QPersistentModelIndex>
|
|
#include <QPixmap>
|
|
#include <QScopeGuard>
|
|
#include <QSignalSpy>
|
|
#include <QStandardPaths>
|
|
#include <QTemporaryDir>
|
|
#include <QTimeZone>
|
|
#include <QtTest>
|
|
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/statvfs.h>
|
|
#include <unistd.h>
|
|
|
|
using namespace Cutefish::IO;
|
|
|
|
static void writeFile(const QString &path, const QByteArray &data = "contents")
|
|
{
|
|
QFile file(path);
|
|
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size())
|
|
throw Failure(EIO, path);
|
|
}
|
|
|
|
static QByteArray readFile(const QString &path)
|
|
{
|
|
QFile file(path);
|
|
if (!file.open(QIODevice::ReadOnly))
|
|
throw Failure(EIO, path);
|
|
|
|
return file.readAll();
|
|
}
|
|
|
|
template <class F> static int errorCode(F action)
|
|
{
|
|
try {
|
|
action();
|
|
} catch (const Failure &failure) {
|
|
return failure.error.code;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static QUrl local(const QString &path)
|
|
{
|
|
return QUrl::fromLocalFile(path);
|
|
}
|
|
|
|
class IOTest : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
QTemporaryDir m_data;
|
|
|
|
private slots:
|
|
void initTestCase()
|
|
{
|
|
// The model tester probes GUI decoration types even for a Core-only model.
|
|
qRegisterMetaType<QPixmap>();
|
|
|
|
QVERIFY(m_data.isValid());
|
|
qputenv("XDG_DATA_HOME", m_data.path().toUtf8());
|
|
QCOMPARE(FileSystemUrl::trashRoot(), m_data.path() + "/Trash");
|
|
}
|
|
|
|
void cleanup()
|
|
{
|
|
OperationContext ctx;
|
|
LocalFileSystem::emptyTrash(ctx);
|
|
}
|
|
|
|
void pathsAndUrls()
|
|
{
|
|
QCOMPARE(FilePath("/one/two").parent().bytes(), QByteArray("/one"));
|
|
QCOMPARE(FilePath("/one").child("a% b").bytes(), QByteArray("/one/a% b"));
|
|
QCOMPARE(FilePath(QByteArray("/a/\xff", 4)).bytes(), QByteArray("/a/\xff", 4));
|
|
|
|
QCOMPARE(errorCode([] { FilePath(QByteArray("x\0y", 3)); }), EINVAL);
|
|
QCOMPARE(errorCode([] { FilePath("/a").child("../x"); }), EINVAL);
|
|
QCOMPARE(errorCode([] { FileSystemUrl(QUrl("https://example.com/file")); }), ENOTSUP);
|
|
QCOMPARE(errorCode([] { FileSystemUrl(QUrl("file://remote/tmp/a")); }), ENOTSUP);
|
|
QCOMPARE(errorCode([] { FileSystemUrl(QUrl("trash:/../a")); }), EINVAL);
|
|
QCOMPARE(errorCode([] { FileSystemUrl(QUrl("trash:/%2E%2E/a")); }), EINVAL);
|
|
|
|
QCOMPARE(FileSystemUrl(local("/tmp/a % # é")).localPath(), QString("/tmp/a % # é"));
|
|
QCOMPARE(upUrl(local("/tmp/a")), local("/tmp"));
|
|
}
|
|
|
|
void enumerationAndMetadata()
|
|
{
|
|
QTemporaryDir dir;
|
|
writeFile(dir.filePath("file.txt"), "hello");
|
|
writeFile(dir.filePath(".hidden"));
|
|
QVERIFY(QDir(dir.path()).mkdir("sub"));
|
|
QVERIFY(::symlink("missing", QFile::encodeName(dir.filePath("broken"))) == 0);
|
|
|
|
const auto items = LocalFileSystem::enumerate(local(dir.path()));
|
|
QCOMPARE(items.size(), 4);
|
|
QCOMPARE(LocalFileSystem::enumerate(local(dir.path()), false).size(), 3);
|
|
|
|
FileItem item(local(dir.filePath("file.txt")));
|
|
QCOMPARE(item.size(), 5);
|
|
QVERIFY(item.isFile());
|
|
QVERIFY(item.time(FileItem::ModificationTime).isValid());
|
|
QCOMPARE(item.mimetype(), QString("text/plain"));
|
|
QVERIFY(FileItem(local(dir.filePath("broken"))).isLink());
|
|
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::enumerate(local(dir.filePath("missing")));
|
|
}),
|
|
ENOENT);
|
|
}
|
|
|
|
void copyRecursiveSymlinksAndPermissions()
|
|
{
|
|
QTemporaryDir dir;
|
|
const auto source = dir.filePath("source");
|
|
QVERIFY(QDir().mkpath(source + "/a/b"));
|
|
writeFile(source + "/a/b/file", QByteArray("hello\0world", 11));
|
|
QVERIFY(::chmod(QFile::encodeName(source + "/a/b/file"), 0751) == 0);
|
|
QVERIFY(::symlink("..", QFile::encodeName(source + "/a/loop")) == 0);
|
|
QVERIFY(::symlink("absent", QFile::encodeName(source + "/broken")) == 0);
|
|
QVERIFY(::symlink("/etc/passwd", QFile::encodeName(source + "/external")) == 0);
|
|
|
|
OperationContext ctx;
|
|
const auto destination = dir.filePath("target");
|
|
QCOMPARE(LocalFileSystem::copy(source, destination, ConflictPolicy::Fail, ctx),
|
|
destination);
|
|
QCOMPARE(readFile(destination + "/a/b/file"), readFile(source + "/a/b/file"));
|
|
|
|
struct stat st;
|
|
QCOMPARE(::lstat(QFile::encodeName(destination + "/a/b/file"), &st), 0);
|
|
QCOMPARE(st.st_mode & 0777, mode_t(0751));
|
|
QCOMPARE(::lstat(QFile::encodeName(destination + "/broken"), &st), 0);
|
|
QVERIFY(S_ISLNK(st.st_mode));
|
|
QVERIFY(QFileInfo(destination + "/a/loop").isSymbolicLink());
|
|
|
|
LocalFileSystem::remove(destination, ctx);
|
|
QVERIFY(!QFileInfo::exists(destination));
|
|
QVERIFY(QFileInfo::exists("/etc/passwd"));
|
|
QVERIFY(QFileInfo::exists(source + "/a/b/file"));
|
|
}
|
|
|
|
void conflicts()
|
|
{
|
|
QTemporaryDir dir;
|
|
auto source = dir.filePath("source.txt"), target = dir.filePath("target.txt");
|
|
writeFile(source, "new");
|
|
writeFile(target, "old");
|
|
|
|
OperationContext ctx;
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::copy(source, target, ConflictPolicy::Fail, ctx);
|
|
}),
|
|
EEXIST);
|
|
QCOMPARE(readFile(target), QByteArray("old"));
|
|
|
|
QVERIFY(LocalFileSystem::copy(source, target, ConflictPolicy::Skip, ctx).isEmpty());
|
|
|
|
QCOMPARE(LocalFileSystem::copy(source, target, ConflictPolicy::KeepBoth, ctx),
|
|
dir.filePath("target (1).txt"));
|
|
QCOMPARE(readFile(dir.filePath("target (1).txt")), QByteArray("new"));
|
|
|
|
QCOMPARE(LocalFileSystem::copy(source, target, ConflictPolicy::Replace, ctx), target);
|
|
QCOMPARE(readFile(target), QByteArray("new"));
|
|
|
|
QVERIFY(!QDir(dir.path())
|
|
.entryList({".cutefish-*"}, QDir::AllEntries | QDir::Hidden)
|
|
.size());
|
|
}
|
|
|
|
void directoryConflictPreservesDestination()
|
|
{
|
|
QTemporaryDir dir;
|
|
QVERIFY(QDir().mkpath(dir.filePath("source/sub")));
|
|
QVERIFY(QDir().mkpath(dir.filePath("target/sub")));
|
|
writeFile(dir.filePath("target/sub/keep"));
|
|
OperationContext ctx;
|
|
writeFile(dir.filePath("source/sub/new"));
|
|
|
|
LocalFileSystem::copy(dir.filePath("source"), dir.filePath("target"),
|
|
ConflictPolicy::Replace, ctx);
|
|
|
|
QVERIFY(QFileInfo::exists(dir.filePath("target/sub/new")));
|
|
QVERIFY(QFileInfo::exists(dir.filePath("target/sub/keep")));
|
|
}
|
|
|
|
void selfAndDescendantProtection()
|
|
{
|
|
QTemporaryDir dir;
|
|
writeFile(dir.filePath("file"));
|
|
QVERIFY(QDir(dir.path()).mkdir("sub"));
|
|
QVERIFY(::symlink(QFile::encodeName(dir.path()), QFile::encodeName(dir.filePath("alias")))
|
|
== 0);
|
|
|
|
OperationContext ctx;
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::copy(dir.filePath("file"), dir.filePath("file"),
|
|
ConflictPolicy::Replace, ctx);
|
|
}),
|
|
EINVAL);
|
|
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::copy(dir.path(), dir.filePath("sub/copy"),
|
|
ConflictPolicy::Fail, ctx);
|
|
}),
|
|
EINVAL);
|
|
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::move(dir.path(), dir.filePath("alias/copy"),
|
|
ConflictPolicy::Fail, ctx);
|
|
}),
|
|
EINVAL);
|
|
|
|
QCOMPARE(errorCode([&] { LocalFileSystem::remove("/", ctx); }), EINVAL);
|
|
}
|
|
|
|
void destinationSymlinkIsNotFollowed()
|
|
{
|
|
QTemporaryDir dir;
|
|
writeFile(dir.filePath("source"), "new");
|
|
writeFile(dir.filePath("victim"), "unchanged");
|
|
QVERIFY(::symlink("victim", QFile::encodeName(dir.filePath("target"))) == 0);
|
|
|
|
OperationContext ctx;
|
|
LocalFileSystem::copy(dir.filePath("source"), dir.filePath("target"),
|
|
ConflictPolicy::Replace, ctx);
|
|
|
|
QCOMPARE(readFile(dir.filePath("victim")), QByteArray("unchanged"));
|
|
QCOMPARE(readFile(dir.filePath("target")), QByteArray("new"));
|
|
QVERIFY(!QFileInfo(dir.filePath("target")).isSymbolicLink());
|
|
}
|
|
|
|
void cancellationCleansStaging()
|
|
{
|
|
QTemporaryDir dir;
|
|
writeFile(dir.filePath("source"), QByteArray(8 * 1024 * 1024, 'x'));
|
|
writeFile(dir.filePath("target"), "original");
|
|
|
|
OperationContext ctx;
|
|
ctx.progress = [&](quint64 bytes, quint64) {
|
|
if (bytes >= 1024 * 1024)
|
|
ctx.canceled = true;
|
|
};
|
|
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::copy(dir.filePath("source"), dir.filePath("target"),
|
|
ConflictPolicy::Replace, ctx);
|
|
}),
|
|
ECANCELED);
|
|
|
|
QCOMPARE(readFile(dir.filePath("target")), QByteArray("original"));
|
|
QCOMPARE(QFileInfo(dir.filePath("source")).size(), 8 * 1024 * 1024);
|
|
QVERIFY(QDir(dir.path())
|
|
.entryList({".cutefish-*"}, QDir::AllEntries | QDir::Hidden)
|
|
.isEmpty());
|
|
}
|
|
|
|
void cancelBeforeMutation()
|
|
{
|
|
QTemporaryDir dir;
|
|
writeFile(dir.filePath("source"));
|
|
|
|
OperationContext ctx;
|
|
ctx.canceled = true;
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::move(dir.filePath("source"), dir.filePath("target"),
|
|
ConflictPolicy::Fail, ctx);
|
|
}),
|
|
ECANCELED);
|
|
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::remove(dir.filePath("source"), ctx);
|
|
}),
|
|
ECANCELED);
|
|
|
|
QVERIFY(QFileInfo::exists(dir.filePath("source")));
|
|
}
|
|
|
|
void sizeDoesNotFollowSymlinks()
|
|
{
|
|
QTemporaryDir dir;
|
|
QVERIFY(QDir(dir.path()).mkdir("sub"));
|
|
writeFile(dir.filePath("sub/file"), QByteArray(50, 'x'));
|
|
QVERIFY(::symlink("..", QFile::encodeName(dir.filePath("sub/loop"))) == 0);
|
|
|
|
OperationContext ctx;
|
|
QCOMPARE(LocalFileSystem::directorySize(dir.path(), ctx), quint64(52));
|
|
QCOMPARE(ctx.bytes, quint64(52));
|
|
|
|
ctx.canceled = true;
|
|
QCOMPARE(errorCode([&] { LocalFileSystem::directorySize(dir.path(), ctx); }), ECANCELED);
|
|
}
|
|
|
|
void sparseLargeFile()
|
|
{
|
|
QTemporaryDir dir;
|
|
QFile source(dir.filePath("source"));
|
|
QVERIFY(source.open(QIODevice::WriteOnly));
|
|
constexpr qint64 length = (qint64(5) << 30) + 17;
|
|
QVERIFY(source.resize(length));
|
|
QVERIFY(source.seek(length - 4));
|
|
QCOMPARE(source.write("tail"), 4);
|
|
source.close();
|
|
|
|
OperationContext ctx;
|
|
LocalFileSystem::copy(source.fileName(), dir.filePath("target"), ConflictPolicy::Fail, ctx);
|
|
QCOMPARE(QFileInfo(dir.filePath("target")).size(), length);
|
|
QCOMPARE(ctx.bytes, quint64(length));
|
|
|
|
QFile target(dir.filePath("target"));
|
|
QVERIFY(target.open(QIODevice::ReadOnly));
|
|
QVERIFY(target.seek(length - 4));
|
|
QCOMPARE(target.read(4), QByteArray("tail"));
|
|
QVERIFY(target.seek(0));
|
|
QCOMPARE(target.read(4), QByteArray(4, '\0'));
|
|
}
|
|
|
|
void dataAroundHolesCopiesExactly()
|
|
{
|
|
QTemporaryDir dir;
|
|
QByteArray head(9 * 1024 * 1024 + 3, Qt::Uninitialized);
|
|
QByteArray tail(5 * 1024 * 1024 + 11, Qt::Uninitialized);
|
|
for (qsizetype i = 0; i < head.size(); ++i)
|
|
head[i] = char(i * 7 + i / 4099);
|
|
for (qsizetype i = 0; i < tail.size(); ++i)
|
|
tail[i] = char(i * 13 + i / 8191);
|
|
|
|
constexpr qint64 gap = 64 * 1024 * 1024;
|
|
QFile source(dir.filePath("source"));
|
|
QVERIFY(source.open(QIODevice::WriteOnly));
|
|
QCOMPARE(source.write(head), head.size());
|
|
QVERIFY(source.seek(head.size() + gap));
|
|
QCOMPARE(source.write(tail), tail.size());
|
|
source.close();
|
|
|
|
const qint64 length = head.size() + gap + tail.size();
|
|
quint64 previous = 0;
|
|
OperationContext ctx;
|
|
ctx.progress = [&](quint64 bytes, quint64) {
|
|
QVERIFY(bytes >= previous);
|
|
previous = bytes;
|
|
};
|
|
|
|
LocalFileSystem::copy(source.fileName(), dir.filePath("target"), ConflictPolicy::Fail, ctx);
|
|
QCOMPARE(ctx.bytes, quint64(length));
|
|
|
|
const QByteArray copied = readFile(dir.filePath("target"));
|
|
QCOMPARE(copied.size(), length);
|
|
QVERIFY(copied.first(head.size()) == head);
|
|
QVERIFY(copied.sliced(head.size(), gap) == QByteArray(gap, '\0'));
|
|
QVERIFY(copied.last(tail.size()) == tail);
|
|
|
|
struct stat st;
|
|
QCOMPARE(::stat(QFile::encodeName(dir.filePath("target")), &st), 0);
|
|
QVERIFY(st.st_blocks * 512 < length - gap / 2);
|
|
}
|
|
|
|
void specialFilesRejected()
|
|
{
|
|
QTemporaryDir dir;
|
|
QVERIFY(::mkfifo(QFile::encodeName(dir.filePath("fifo")), 0600) == 0);
|
|
|
|
OperationContext ctx;
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::copy(dir.filePath("fifo"), dir.filePath("target"),
|
|
ConflictPolicy::Fail, ctx);
|
|
}),
|
|
ENOTSUP);
|
|
|
|
QVERIFY(!QFileInfo::exists(dir.filePath("target")));
|
|
}
|
|
|
|
void permissionErrors()
|
|
{
|
|
if (::geteuid() == 0)
|
|
QSKIP("Permission checks require an unprivileged user");
|
|
|
|
QTemporaryDir dir;
|
|
writeFile(dir.filePath("source"));
|
|
QVERIFY(QDir(dir.path()).mkdir("blocked"));
|
|
QVERIFY(::chmod(QFile::encodeName(dir.filePath("blocked")), 0500) == 0);
|
|
|
|
OperationContext ctx;
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::copy(dir.filePath("source"), dir.filePath("blocked/target"),
|
|
ConflictPolicy::Fail, ctx);
|
|
}),
|
|
EACCES);
|
|
|
|
QVERIFY(::chmod(QFile::encodeName(dir.filePath("source")), 0000) == 0);
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::copy(dir.filePath("source"), dir.filePath("target"),
|
|
ConflictPolicy::Fail, ctx);
|
|
}),
|
|
EACCES);
|
|
|
|
QVERIFY(::chmod(QFile::encodeName(dir.filePath("blocked")), 0700) == 0);
|
|
QVERIFY(::chmod(QFile::encodeName(dir.filePath("source")), 0600) == 0);
|
|
}
|
|
|
|
void trashRestoreAndConflicts()
|
|
{
|
|
QTemporaryDir dir;
|
|
const auto path = dir.filePath("é % #\n.txt");
|
|
writeFile(path, "original");
|
|
|
|
OperationContext ctx;
|
|
const auto trashed = LocalFileSystem::trash(local(path), ctx);
|
|
QVERIFY(!QFileInfo::exists(path));
|
|
QCOMPARE(readFile(FileSystemUrl(trashed).localPath()), QByteArray("original"));
|
|
|
|
writeFile(path, "replacement");
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::restore(trashed, ConflictPolicy::Fail, ctx);
|
|
}),
|
|
EEXIST);
|
|
|
|
QVERIFY(QFileInfo::exists(FileSystemUrl(trashed).localPath()));
|
|
QCOMPARE(LocalFileSystem::restore(trashed, ConflictPolicy::Replace, ctx), local(path));
|
|
QCOMPARE(readFile(path), QByteArray("original"));
|
|
|
|
QVERIFY(LocalFileSystem::enumerate(QUrl("trash:/")).isEmpty());
|
|
QVERIFY(!QFileInfo::exists(FileSystemUrl::trashRoot() + "/info/" + trashed.fileName()
|
|
+ ".trashinfo"));
|
|
}
|
|
|
|
void trashRecursiveDuplicateAndEmpty()
|
|
{
|
|
QTemporaryDir dir;
|
|
QVERIFY(QDir().mkpath(dir.filePath("tree/sub")));
|
|
writeFile(dir.filePath("tree/sub/file"));
|
|
|
|
OperationContext ctx;
|
|
const auto first = LocalFileSystem::trash(local(dir.filePath("tree")), ctx);
|
|
QVERIFY(QDir().mkpath(dir.filePath("tree/sub")));
|
|
const auto second = LocalFileSystem::trash(local(dir.filePath("tree")), ctx);
|
|
QVERIFY(first != second);
|
|
QCOMPARE(LocalFileSystem::enumerate(QUrl("trash:/")).size(), 2);
|
|
|
|
LocalFileSystem::emptyTrash(ctx);
|
|
QVERIFY(LocalFileSystem::enumerate(QUrl("trash:/")).isEmpty());
|
|
QVERIFY(QDir(FileSystemUrl::trashRoot() + "/info").entryList(QDir::Files).isEmpty());
|
|
}
|
|
|
|
void longTrashNameAndOrphanMetadata()
|
|
{
|
|
QTemporaryDir dir;
|
|
const auto path = dir.filePath(QString(250, 'a'));
|
|
writeFile(path);
|
|
|
|
OperationContext ctx;
|
|
const auto trashed = LocalFileSystem::trash(local(path), ctx);
|
|
QCOMPARE(LocalFileSystem::restore(trashed, ConflictPolicy::Fail, ctx), local(path));
|
|
|
|
writeFile(FileSystemUrl::trashRoot() + "/info/orphan.trashinfo",
|
|
"[Trash Info]\nPath=/missing\n");
|
|
|
|
LocalFileSystem::emptyTrash(ctx);
|
|
QVERIFY(QDir(FileSystemUrl::trashRoot() + "/info").entryList(QDir::Files).isEmpty());
|
|
}
|
|
|
|
void malformedTrashInfo()
|
|
{
|
|
QTemporaryDir dir;
|
|
writeFile(dir.filePath("file"));
|
|
|
|
OperationContext ctx;
|
|
const auto url = LocalFileSystem::trash(local(dir.filePath("file")), ctx);
|
|
writeFile(FileSystemUrl::trashRoot() + "/info/" + url.fileName() + ".trashinfo",
|
|
"[Trash Info]\nPath=relative\n");
|
|
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::restore(url, ConflictPolicy::Fail, ctx);
|
|
}),
|
|
EINVAL);
|
|
QVERIFY(QFileInfo::exists(FileSystemUrl(url).localPath()));
|
|
}
|
|
|
|
void watcherAndStableModel()
|
|
{
|
|
QTemporaryDir dir;
|
|
writeFile(dir.filePath("first"));
|
|
DirectoryLister lister;
|
|
DirectoryModel model;
|
|
model.setDirLister(&lister);
|
|
QAbstractItemModelTester tester(&model,
|
|
QAbstractItemModelTester::FailureReportingMode::QtTest);
|
|
|
|
QSignalSpy listed(&lister, &DirectoryLister::completed);
|
|
lister.openUrl(local(dir.path()));
|
|
QTRY_COMPARE(model.rowCount(), 1);
|
|
|
|
QPersistentModelIndex first(model.indexForUrl(local(dir.filePath("first"))));
|
|
writeFile(dir.filePath("second"));
|
|
QTRY_COMPARE(model.rowCount(), 2);
|
|
QVERIFY(first.isValid());
|
|
QCOMPARE(model.itemForIndex(first).name(), QString("first"));
|
|
|
|
writeFile(dir.filePath("first"), "modified contents");
|
|
QTRY_COMPARE(model.itemForIndex(first).size(), quint64(17));
|
|
|
|
QVERIFY(QFile::rename(dir.filePath("second"), dir.filePath("renamed")));
|
|
QTRY_VERIFY(model.indexForUrl(local(dir.filePath("renamed"))).isValid());
|
|
|
|
QVERIFY(QFile::remove(dir.filePath("renamed")));
|
|
QTRY_COMPARE(model.rowCount(), 1);
|
|
|
|
writeFile(dir.filePath(".hidden"));
|
|
lister.setShowHiddenFiles(true);
|
|
lister.emitChanges();
|
|
QTRY_COMPARE(model.rowCount(), 2);
|
|
|
|
QVERIFY(listed.size() >= 4);
|
|
}
|
|
|
|
void searchFindsNamesAtAnyDepth()
|
|
{
|
|
QTemporaryDir dir;
|
|
QVERIFY(QDir(dir.path()).mkpath("Projects/Report Drafts/old"));
|
|
QVERIFY(QDir(dir.path()).mkpath(".cache/report"));
|
|
writeFile(dir.filePath("annual report.txt"));
|
|
writeFile(dir.filePath("Projects/REPORT final.pdf"));
|
|
writeFile(dir.filePath("Projects/Report Drafts/old/report-1.txt"));
|
|
writeFile(dir.filePath("Projects/notes.txt"));
|
|
writeFile(dir.filePath(".cache/report/report.bin"));
|
|
QVERIFY(QFile::link(dir.filePath("Projects"), dir.filePath("Projects/loop")));
|
|
|
|
DirectoryLister lister;
|
|
DirectoryModel model;
|
|
model.setDirLister(&lister);
|
|
QAbstractItemModelTester tester(&model,
|
|
QAbstractItemModelTester::FailureReportingMode::QtTest);
|
|
QSignalSpy listed(&lister, &DirectoryLister::completed);
|
|
|
|
lister.openUrl(local(dir.filePath("Projects")));
|
|
QTRY_COMPARE(listed.count(), 1);
|
|
|
|
const auto names = [&] {
|
|
QStringList result;
|
|
for (const auto &item : lister.items())
|
|
result.append(QDir(dir.path()).relativeFilePath(item.localPath()));
|
|
result.sort();
|
|
return result;
|
|
};
|
|
|
|
// Every word, in any case; hidden folders are left out and links not followed.
|
|
lister.search(local(dir.path()), " report ");
|
|
QVERIFY(lister.isSearching());
|
|
QTRY_COMPARE(listed.count(), 2);
|
|
QCOMPARE(names(), QStringList({"Projects/REPORT final.pdf", "Projects/Report Drafts",
|
|
"Projects/Report Drafts/old/report-1.txt",
|
|
"annual report.txt"}));
|
|
QCOMPARE(lister.url(), local(dir.filePath("Projects")));
|
|
QCOMPARE(model.rowCount(), 4);
|
|
|
|
lister.search(local(dir.path()), "report TXT");
|
|
QTRY_COMPARE(listed.count(), 3);
|
|
QCOMPARE(names(), QStringList({"Projects/Report Drafts/old/report-1.txt",
|
|
"annual report.txt"}));
|
|
|
|
lister.setShowHiddenFiles(true);
|
|
lister.emitChanges();
|
|
QTRY_COMPARE(listed.count(), 4);
|
|
QCOMPARE(names(), QStringList({"Projects/Report Drafts/old/report-1.txt",
|
|
"annual report.txt"}));
|
|
|
|
lister.search(local(dir.path()), "report");
|
|
QTRY_COMPARE(listed.count(), 5);
|
|
QVERIFY(names().contains(".cache/report/report.bin"));
|
|
|
|
// No text lists the directory again.
|
|
lister.search({}, " ");
|
|
QVERIFY(!lister.isSearching());
|
|
QTRY_COMPARE(listed.count(), 6);
|
|
QCOMPARE(names(), QStringList({"Projects/REPORT final.pdf", "Projects/Report Drafts",
|
|
"Projects/loop", "Projects/notes.txt"}));
|
|
}
|
|
|
|
void searchStopsWhenAFolderOpens()
|
|
{
|
|
QTemporaryDir dir;
|
|
QVERIFY(QDir(dir.path()).mkpath("inner"));
|
|
writeFile(dir.filePath("inner/match"));
|
|
|
|
DirectoryLister lister;
|
|
QSignalSpy listed(&lister, &DirectoryLister::completed);
|
|
lister.search(local(dir.path()), "match");
|
|
lister.openUrl(local(dir.filePath("inner")));
|
|
QVERIFY(!lister.isSearching());
|
|
QTRY_COMPARE(listed.count(), 1);
|
|
QCOMPARE(lister.items().size(), 1);
|
|
QCOMPARE(lister.items().constFirst().name(), QString("match"));
|
|
}
|
|
|
|
void refreshDetectsChangedContents()
|
|
{
|
|
QTemporaryDir dir;
|
|
// Without an extension the type comes from the contents.
|
|
writeFile(dir.filePath("changed"), "%PDF-1.4\n");
|
|
writeFile(dir.filePath("kept"), "%PDF-1.4\n");
|
|
|
|
DirectoryLister lister;
|
|
QSignalSpy listed(&lister, &DirectoryLister::completed);
|
|
lister.openUrl(local(dir.path()));
|
|
QTRY_COMPARE(listed.count(), 1);
|
|
|
|
const auto typeOf = [&](const QString &name) {
|
|
for (const auto &item : lister.items()) {
|
|
if (item.name() == name)
|
|
return item.mimetype();
|
|
}
|
|
return QString();
|
|
};
|
|
QCOMPARE(typeOf("changed"), QString("application/pdf"));
|
|
QCOMPARE(typeOf("kept"), QString("application/pdf"));
|
|
|
|
writeFile(dir.filePath("changed"), "#!/bin/sh\necho\n");
|
|
QTRY_COMPARE(typeOf("changed"), QString("application/x-shellscript"));
|
|
QCOMPARE(typeOf("kept"), QString("application/pdf"));
|
|
|
|
const auto item = lister.items().constFirst();
|
|
QCOMPARE(item.time(FileItem::ModificationTime, QTimeZone::UTC),
|
|
item.time(FileItem::ModificationTime));
|
|
}
|
|
|
|
// The sniffer answers what QMimeDatabase does, names and contents alike.
|
|
void sniffedTypesMatchQt()
|
|
{
|
|
QTemporaryDir dir;
|
|
const QByteArray elf = readFile(QCoreApplication::applicationFilePath()).left(20000);
|
|
const QByteArray transportStream =
|
|
QByteArray(1, 0x47) + QByteArray(187, 0) + QByteArray(1, 0x47) + QByteArray(187, 0);
|
|
const QList<QPair<QString, QByteArray>> files = {
|
|
{"program", elf},
|
|
{"libsample.so.6", elf},
|
|
{"page.1", "plain words\n"},
|
|
{"script", "#!/bin/sh\necho\n"},
|
|
{"empty", {}},
|
|
{"empty.ts", {}},
|
|
{"strings.ts", "<?xml version=\"1.0\"?>\n<TS version=\"2.1\"></TS>\n"},
|
|
{"video.ts", transportStream},
|
|
{"function.m", "function y = f(x)\nend\n"},
|
|
{"README", "read me\n"},
|
|
{"Makefile", "all:\n"},
|
|
{"PHOTO.JPG", "\xff\xd8\xff\xe0"},
|
|
{"source.C", "int main() {}\n"},
|
|
{"binary", QByteArray("\x00\x01\x02\x03", 4)},
|
|
{"document", "%PDF-1.4\n"},
|
|
{"notes~", "backup\n"},
|
|
{"123.vdr", "recording"},
|
|
{"callgrind.out.1", "events: Ir\n"},
|
|
};
|
|
|
|
const QMimeDatabase database;
|
|
for (const auto &[name, data] : files) {
|
|
const QString path = dir.filePath(name);
|
|
writeFile(path, data);
|
|
QCOMPARE(name + u' ' + Internal::MimeSniffer::self()->mimeTypeForFile(path).name(),
|
|
name + u' ' + database.mimeTypeForFile(path).name());
|
|
}
|
|
}
|
|
|
|
// A first listing detects types on several threads.
|
|
void listingDetectsEveryType()
|
|
{
|
|
QTemporaryDir dir;
|
|
for (int i = 0; i < 400; ++i)
|
|
writeFile(dir.filePath(QString::number(i)), i % 2 ? "%PDF-1.4\n" : "#!/bin/sh\n");
|
|
|
|
DirectoryLister lister;
|
|
QSignalSpy listed(&lister, &DirectoryLister::completed);
|
|
lister.openUrl(local(dir.path()));
|
|
QTRY_COMPARE(listed.count(), 1);
|
|
QCOMPARE(lister.items().size(), 400);
|
|
|
|
for (const auto &item : lister.items()) {
|
|
QCOMPARE(item.name() + u' ' + item.mimetype(),
|
|
item.name() + (item.name().toInt() % 2 ? " application/pdf"
|
|
: " application/x-shellscript"));
|
|
}
|
|
}
|
|
|
|
// The name an item keeps is the one its URL decodes to.
|
|
void listedNamesMatchTheirUrls()
|
|
{
|
|
QTemporaryDir dir;
|
|
const QByteArray base = QFile::encodeName(dir.path());
|
|
const QList<QByteArray> names = {"a%20b", "100%", "c#d", "e?f", "g h", "[x]", "semi;colon",
|
|
"back\\slash", "\xc3\xbc", "x\xff", "plus+sign"};
|
|
const auto removeNames = qScopeGuard([&] {
|
|
for (const QByteArray &name : names)
|
|
::unlink(base + '/' + name);
|
|
});
|
|
for (const QByteArray &name : names)
|
|
QVERIFY(::close(::open(base + '/' + name, O_CREAT | O_WRONLY | O_CLOEXEC, 0644)) == 0);
|
|
|
|
DirectoryLister lister;
|
|
QSignalSpy listed(&lister, &DirectoryLister::completed);
|
|
lister.openUrl(local(dir.path()));
|
|
QTRY_COMPARE(listed.count(), 1);
|
|
QCOMPARE(lister.items().size(), names.size());
|
|
|
|
for (const auto &item : lister.items()) {
|
|
QCOMPARE(item.name(), item.url().fileName());
|
|
QCOMPARE(QFileInfo(item.localPath()).fileName(), item.name());
|
|
}
|
|
}
|
|
|
|
// A folder this large builds its items on several threads.
|
|
void largeListingHasEveryFileOnce()
|
|
{
|
|
QTemporaryDir dir;
|
|
constexpr int Count = 20500;
|
|
for (int i = 0; i < Count; ++i)
|
|
QVERIFY(::close(::open(QFile::encodeName(dir.filePath(QString::number(i))),
|
|
O_CREAT | O_WRONLY | O_CLOEXEC, 0644)) == 0);
|
|
|
|
DirectoryLister lister;
|
|
QSignalSpy listed(&lister, &DirectoryLister::completed);
|
|
lister.openUrl(local(dir.path()));
|
|
QTRY_COMPARE_WITH_TIMEOUT(listed.count(), 1, 30000);
|
|
|
|
QList<bool> seen(Count, false);
|
|
for (const auto &item : lister.items()) {
|
|
const int index = item.name().toInt();
|
|
QVERIFY(!seen[index]);
|
|
seen[index] = true;
|
|
QCOMPARE(item.url(), local(dir.filePath(item.name())));
|
|
QCOMPARE(item.localPath(), dir.filePath(item.name()));
|
|
}
|
|
QVERIFY(!seen.contains(false));
|
|
}
|
|
|
|
// Files added or removed in one place leave the other rows where they are.
|
|
void modelFollowsChangesBetweenUnchangedRows()
|
|
{
|
|
QTemporaryDir dir;
|
|
for (int i = 0; i < 60; ++i)
|
|
writeFile(dir.filePath(QString::number(i)));
|
|
|
|
DirectoryLister lister;
|
|
DirectoryModel model;
|
|
model.setDirLister(&lister);
|
|
QAbstractItemModelTester tester(&model,
|
|
QAbstractItemModelTester::FailureReportingMode::QtTest);
|
|
QSignalSpy listed(&lister, &DirectoryLister::completed);
|
|
lister.openUrl(local(dir.path()));
|
|
QTRY_COMPARE(listed.count(), 1);
|
|
|
|
const auto sortedUrls = [](const FileItemList &items) {
|
|
QStringList urls;
|
|
for (const auto &item : items)
|
|
urls.append(item.url().toString());
|
|
urls.sort();
|
|
return urls;
|
|
};
|
|
const auto modelItems = [&] {
|
|
FileItemList items;
|
|
for (int row = 0; row < model.rowCount(); ++row)
|
|
items.append(model.itemForIndex(model.index(row, 0)));
|
|
return items;
|
|
};
|
|
|
|
QPersistentModelIndex kept(model.indexForUrl(local(dir.filePath("30"))));
|
|
QVERIFY(kept.isValid());
|
|
|
|
writeFile(dir.filePath("added"));
|
|
QTRY_COMPARE(model.rowCount(), 61);
|
|
QCOMPARE(sortedUrls(modelItems()), sortedUrls(lister.items()));
|
|
|
|
QVERIFY(QFile::remove(dir.filePath("10")));
|
|
QTRY_COMPARE(model.rowCount(), 60);
|
|
QCOMPARE(sortedUrls(modelItems()), sortedUrls(lister.items()));
|
|
|
|
QVERIFY(kept.isValid());
|
|
QCOMPARE(model.itemForIndex(kept).name(), QString("30"));
|
|
QVERIFY(model.indexForUrl(local(dir.filePath("added"))).isValid());
|
|
QVERIFY(!model.indexForUrl(local(dir.filePath("10"))).isValid());
|
|
}
|
|
|
|
// Names that are not UTF-8 can decode to the same text, giving two files one URL.
|
|
void modelKeepsRowsForCollidingNames()
|
|
{
|
|
QTemporaryDir dir;
|
|
const QByteArray base = QFile::encodeName(dir.path());
|
|
const QList<QByteArray> names = {"a\xff", "a\xfe", "b"};
|
|
// QTemporaryDir cannot remove what it cannot name.
|
|
const auto removeNames = qScopeGuard([&] {
|
|
for (const QByteArray &name : names)
|
|
::unlink(base + '/' + name);
|
|
});
|
|
for (const QByteArray &name : names)
|
|
QVERIFY(::close(::open(base + '/' + name, O_CREAT | O_WRONLY | O_CLOEXEC, 0644)) == 0);
|
|
|
|
DirectoryLister lister;
|
|
DirectoryModel model;
|
|
model.setDirLister(&lister);
|
|
QAbstractItemModelTester tester(&model,
|
|
QAbstractItemModelTester::FailureReportingMode::QtTest);
|
|
QSignalSpy listed(&lister, &DirectoryLister::completed);
|
|
lister.openUrl(local(dir.path()));
|
|
QTRY_COMPARE(listed.count(), 1);
|
|
QCOMPARE(model.rowCount(), 3);
|
|
|
|
for (int round = 0; round < 4; ++round) {
|
|
const int before = listed.count();
|
|
writeFile(dir.filePath(QStringLiteral("new%1").arg(round)));
|
|
QTRY_VERIFY(listed.count() > before);
|
|
QTRY_COMPARE(model.rowCount(), int(lister.items().size()));
|
|
QCOMPARE(model.rowCount(), 4 + round);
|
|
}
|
|
}
|
|
|
|
void watcherIgnoresUnrelatedSiblings()
|
|
{
|
|
QTemporaryDir directory;
|
|
const auto watched = directory.filePath("watched");
|
|
QVERIFY(QDir().mkpath(watched));
|
|
|
|
FileWatcher watcher;
|
|
QSignalSpy changed(&watcher, &FileWatcher::changed);
|
|
QVERIFY(watcher.watch(watched));
|
|
|
|
writeFile(directory.filePath("unrelated"));
|
|
QTest::qWait(80);
|
|
QCOMPARE(changed.count(), 0);
|
|
|
|
writeFile(watched + "/inside");
|
|
QTRY_COMPARE(changed.count(), 1);
|
|
}
|
|
|
|
// A kept watch must not outlive its directory: one recreated at the path is watched anew.
|
|
void watcherFollowsRecreatedDirectory()
|
|
{
|
|
QTemporaryDir directory;
|
|
const auto watched = directory.filePath("watched");
|
|
QVERIFY(QDir().mkpath(watched));
|
|
|
|
FileWatcher watcher;
|
|
QSignalSpy changed(&watcher, &FileWatcher::changed);
|
|
QVERIFY(watcher.watch(watched));
|
|
|
|
QVERIFY(QDir(watched).removeRecursively());
|
|
QVERIFY(QDir().mkpath(watched));
|
|
QTRY_VERIFY(changed.count() > 0);
|
|
QVERIFY(watcher.watch(watched));
|
|
|
|
QTest::qWait(80);
|
|
changed.clear();
|
|
writeFile(watched + "/inside");
|
|
QTRY_VERIFY(changed.count() > 0);
|
|
}
|
|
|
|
void watcherDirectoryRemoval()
|
|
{
|
|
QTemporaryDir dir;
|
|
const auto path = dir.filePath("a/b");
|
|
QVERIFY(QDir().mkpath(path));
|
|
writeFile(dir.filePath("sibling"));
|
|
|
|
DirectoryLister lister;
|
|
DirectoryModel model;
|
|
model.setDirLister(&lister);
|
|
QSignalSpy listed(&lister, &DirectoryLister::completed);
|
|
QSignalSpy failed(&lister, &DirectoryLister::error);
|
|
lister.openUrl(local(path));
|
|
QTRY_COMPARE(listed.count(), 1);
|
|
|
|
QVERIFY(QDir(dir.filePath("a")).removeRecursively());
|
|
QTRY_COMPARE(lister.url(), local(dir.path()));
|
|
QTRY_COMPARE(model.rowCount(), 1);
|
|
QCOMPARE(model.itemForIndex(model.index(0, 0)).name(), QString("sibling"));
|
|
QCOMPARE(failed.count(), 0);
|
|
}
|
|
|
|
void missingDirectoryReportsOnce()
|
|
{
|
|
QTemporaryDir dir;
|
|
const auto path = dir.filePath("missing");
|
|
|
|
DirectoryLister lister;
|
|
QSignalSpy failed(&lister, &DirectoryLister::error);
|
|
lister.openUrl(local(path));
|
|
QTRY_COMPARE(failed.count(), 1);
|
|
|
|
lister.emitChanges();
|
|
QTest::qWait(2500);
|
|
QCOMPARE(failed.count(), 1);
|
|
QCOMPARE(lister.url(), local(path));
|
|
}
|
|
|
|
void staleListingAndDestruction()
|
|
{
|
|
QTemporaryDir dir;
|
|
QVERIFY(QDir().mkpath(dir.filePath("a")));
|
|
QVERIFY(QDir().mkpath(dir.filePath("b")));
|
|
for (int i = 0; i < 100; ++i)
|
|
writeFile(dir.filePath("a/" + QString::number(i)));
|
|
writeFile(dir.filePath("b/only"));
|
|
|
|
DirectoryLister lister;
|
|
DirectoryModel model;
|
|
model.setDirLister(&lister);
|
|
|
|
lister.openUrl(local(dir.filePath("a")));
|
|
lister.openUrl(local(dir.filePath("b")));
|
|
QTRY_COMPARE(model.rowCount(), 1);
|
|
QCOMPARE(model.itemForIndex(model.index(0, 0)).name(), QString("only"));
|
|
|
|
auto *pending = new DirectoryLister;
|
|
pending->openUrl(local(dir.filePath("a")));
|
|
delete pending;
|
|
}
|
|
|
|
void asynchronousTasksAndPaste()
|
|
{
|
|
QTemporaryDir dir;
|
|
QVERIFY(QDir().mkpath(dir.filePath("target")));
|
|
writeFile(dir.filePath("source"), "payload");
|
|
QMimeData mime;
|
|
mime.setUrls({local(dir.filePath("source"))});
|
|
|
|
auto *copyTask = paste(&mime, local(dir.filePath("target")));
|
|
QSignalSpy copied(copyTask, &Task::result);
|
|
QTRY_COMPARE(copied.size(), 1);
|
|
QCOMPARE(readFile(dir.filePath("target/source")), QByteArray("payload"));
|
|
QVERIFY(QFileInfo::exists(dir.filePath("source")));
|
|
|
|
mime.setData("application/x-cutefish-cutselection", "1");
|
|
auto *moveTask = paste(&mime, local(dir.filePath("target")));
|
|
QSignalSpy asked(moveTask, &Task::conflictRequested);
|
|
QSignalSpy moved(moveTask, &Task::result);
|
|
QTRY_COMPARE(asked.size(), 1);
|
|
QCOMPARE(asked.first().at(0).toString(), dir.filePath("source"));
|
|
QCOMPARE(asked.first().at(1).toString(), dir.filePath("target/source"));
|
|
moveTask->resolveConflict(int(ConflictPolicy::KeepBoth));
|
|
QTRY_COMPARE(moved.size(), 1);
|
|
QVERIFY(!QFileInfo::exists(dir.filePath("source")));
|
|
QCOMPARE(readFile(dir.filePath("target/source (1)")), QByteArray("payload"));
|
|
|
|
Task rename({Operation::Rename,
|
|
{local(dir.filePath("target/source (1)"))},
|
|
local(dir.filePath("renamed"))});
|
|
QSignalSpy renamed(&rename, &Task::result);
|
|
rename.start();
|
|
rename.start();
|
|
QTRY_COMPARE(renamed.size(), 1);
|
|
QCOMPARE(rename.state(), Task::State::Succeeded);
|
|
|
|
Task remove({Operation::Delete, {local(dir.filePath("target"))}});
|
|
QSignalSpy removed(&remove, &Task::result);
|
|
remove.start();
|
|
QTRY_COMPARE(removed.size(), 1);
|
|
QVERIFY(!QFileInfo::exists(dir.filePath("target")));
|
|
}
|
|
|
|
void conflictResolutionAppliesToAll()
|
|
{
|
|
QTemporaryDir dir;
|
|
QVERIFY(QDir().mkpath(dir.filePath("target")));
|
|
writeFile(dir.filePath("a"), "new a");
|
|
writeFile(dir.filePath("b"), "new b");
|
|
writeFile(dir.filePath("target/a"), "old a");
|
|
writeFile(dir.filePath("target/b"), "old b");
|
|
|
|
Task task({Operation::Copy,
|
|
{local(dir.filePath("a")), local(dir.filePath("b"))},
|
|
local(dir.filePath("target")),
|
|
ConflictPolicy::Ask});
|
|
QSignalSpy conflict(&task, &Task::conflictRequested), finished(&task, &Task::result);
|
|
task.start();
|
|
QTRY_COMPARE(conflict.size(), 1);
|
|
task.resolveConflict(int(ConflictPolicy::Replace), true);
|
|
|
|
QTRY_COMPARE(finished.size(), 1);
|
|
QCOMPARE(conflict.size(), 1);
|
|
QCOMPARE(task.state(), Task::State::Succeeded);
|
|
QCOMPARE(readFile(dir.filePath("target/a")), QByteArray("new a"));
|
|
QCOMPARE(readFile(dir.filePath("target/b")), QByteArray("new b"));
|
|
}
|
|
|
|
void pasteIntoOwnFolderDuplicatesWithoutAsking()
|
|
{
|
|
QTemporaryDir dir;
|
|
writeFile(dir.filePath("source"), "payload");
|
|
|
|
Task task({Operation::Copy,
|
|
{local(dir.filePath("source"))},
|
|
local(dir.path()),
|
|
ConflictPolicy::Ask});
|
|
QSignalSpy conflict(&task, &Task::conflictRequested), finished(&task, &Task::result);
|
|
task.start();
|
|
QTRY_COMPARE(finished.size(), 1);
|
|
|
|
QCOMPARE(conflict.size(), 0);
|
|
QCOMPARE(task.state(), Task::State::Succeeded);
|
|
QCOMPARE(readFile(dir.filePath("source (1)")), QByteArray("payload"));
|
|
}
|
|
|
|
void asynchronousConflictAndCancel()
|
|
{
|
|
QTemporaryDir dir;
|
|
QVERIFY(QDir().mkpath(dir.filePath("target")));
|
|
writeFile(dir.filePath("source"), "new");
|
|
writeFile(dir.filePath("target/source"), "old");
|
|
|
|
Task task({Operation::Copy,
|
|
{local(dir.filePath("source"))},
|
|
local(dir.filePath("target")),
|
|
ConflictPolicy::Ask});
|
|
QSignalSpy conflict(&task, &Task::conflictRequested), finished(&task, &Task::result);
|
|
task.start();
|
|
QTRY_COMPARE(conflict.size(), 1);
|
|
task.resolveConflict(int(ConflictPolicy::KeepBoth));
|
|
QTRY_COMPARE(finished.size(), 1);
|
|
QCOMPARE(task.state(), Task::State::Succeeded);
|
|
QCOMPARE(readFile(dir.filePath("target/source (1)")), QByteArray("new"));
|
|
|
|
Task canceled(task.request());
|
|
QSignalSpy waiting(&canceled, &Task::conflictRequested), done(&canceled, &Task::result);
|
|
canceled.start();
|
|
QTRY_COMPARE(waiting.size(), 1);
|
|
canceled.cancel();
|
|
QTRY_COMPARE(done.size(), 1);
|
|
|
|
QCOMPARE(canceled.state(), Task::State::Canceled);
|
|
QCOMPARE(readFile(dir.filePath("target/source")), QByteArray("old"));
|
|
}
|
|
|
|
void controllerTrashUndo()
|
|
{
|
|
QTemporaryDir dir;
|
|
writeFile(dir.filePath("file"));
|
|
TaskController controller;
|
|
QSignalSpy finished(&controller, &TaskController::taskFinished);
|
|
|
|
controller.submit({Operation::Trash, {local(dir.filePath("file"))}});
|
|
QTRY_COMPARE(finished.size(), 1);
|
|
QVERIFY(controller.isUndoAvailable());
|
|
|
|
QVERIFY(controller.undo());
|
|
QTRY_COMPARE(finished.size(), 2);
|
|
QVERIFY(QFileInfo::exists(dir.filePath("file")));
|
|
QVERIFY(!controller.isUndoAvailable());
|
|
}
|
|
|
|
void controllerTrashRedo()
|
|
{
|
|
QTemporaryDir dir;
|
|
writeFile(dir.filePath("file"));
|
|
TaskController controller;
|
|
QSignalSpy finished(&controller, &TaskController::taskFinished);
|
|
|
|
controller.submit({Operation::Trash, {local(dir.filePath("file"))}});
|
|
QTRY_COMPARE(finished.size(), 1);
|
|
QVERIFY(!controller.isRedoAvailable());
|
|
|
|
QVERIFY(controller.undo());
|
|
QTRY_COMPARE(finished.size(), 2);
|
|
QVERIFY(controller.isRedoAvailable());
|
|
|
|
QVERIFY(controller.redo());
|
|
QTRY_COMPARE(finished.size(), 3);
|
|
QVERIFY(!QFileInfo::exists(dir.filePath("file")));
|
|
QVERIFY(!controller.isRedoAvailable());
|
|
QVERIFY(controller.isUndoAvailable());
|
|
|
|
// A new operation after an undo forgets what could be redone.
|
|
QVERIFY(controller.undo());
|
|
QTRY_COMPARE(finished.size(), 4);
|
|
QVERIFY(controller.isRedoAvailable());
|
|
writeFile(dir.filePath("other"));
|
|
controller.submit({Operation::Trash, {local(dir.filePath("other"))}});
|
|
QTRY_COMPARE(finished.size(), 5);
|
|
QVERIFY(!controller.isRedoAvailable());
|
|
}
|
|
|
|
void crossDeviceMove()
|
|
{
|
|
const auto root = qEnvironmentVariable("CUTEFISH_IO_CROSS_DEVICE_ROOT");
|
|
if (root.isEmpty())
|
|
QSKIP("Set CUTEFISH_IO_CROSS_DEVICE_ROOT to a writable different filesystem");
|
|
|
|
QTemporaryDir source;
|
|
QTemporaryDir target(root + "/cutefish-io-XXXXXX");
|
|
QVERIFY(target.isValid());
|
|
|
|
struct stat from, to;
|
|
QCOMPARE(::stat(QFile::encodeName(source.path()), &from), 0);
|
|
QCOMPARE(::stat(QFile::encodeName(target.path()), &to), 0);
|
|
QVERIFY(from.st_dev != to.st_dev);
|
|
|
|
QVERIFY(QDir().mkpath(source.filePath("tree/sub")));
|
|
writeFile(source.filePath("tree/sub/file"), QByteArray(4 * 1024 * 1024, 'x'));
|
|
QVERIFY(::symlink("sub/file", QFile::encodeName(source.filePath("tree/link"))) == 0);
|
|
|
|
OperationContext ctx;
|
|
LocalFileSystem::move(source.filePath("tree"), target.filePath("tree"),
|
|
ConflictPolicy::Fail, ctx);
|
|
|
|
QVERIFY(!QFileInfo::exists(source.filePath("tree")));
|
|
QCOMPARE(QFileInfo(target.filePath("tree/sub/file")).size(), 4 * 1024 * 1024);
|
|
QVERIFY(QFileInfo(target.filePath("tree/link")).isSymbolicLink());
|
|
|
|
OperationContext cancel;
|
|
cancel.progress = [&](quint64 bytes, quint64) {
|
|
if (bytes)
|
|
cancel.canceled = true;
|
|
};
|
|
|
|
QCOMPARE(errorCode([&] {
|
|
LocalFileSystem::move(target.filePath("tree"), source.filePath("tree"),
|
|
ConflictPolicy::Fail, cancel);
|
|
}),
|
|
ECANCELED);
|
|
|
|
QVERIFY(QFileInfo::exists(target.filePath("tree/sub/file")));
|
|
QVERIFY(!QFileInfo::exists(source.filePath("tree")));
|
|
}
|
|
|
|
void progressIncludesTotalsAndCurrentPath()
|
|
{
|
|
QTemporaryDir dir;
|
|
QVERIFY(QDir().mkpath(dir.filePath("source/nested")));
|
|
QVERIFY(QDir().mkpath(dir.filePath("target")));
|
|
writeFile(dir.filePath("source/nested/one"), QByteArray(4 * 1024 * 1024, 'x'));
|
|
writeFile(dir.filePath("two"), QByteArray(2 * 1024 * 1024, 'y'));
|
|
|
|
Task task({Operation::Copy,
|
|
{local(dir.filePath("source")), local(dir.filePath("two"))},
|
|
local(dir.filePath("target"))});
|
|
QList<TaskProgress> snapshots;
|
|
connect(&task, &Task::progressInfoChanged, this, [&] {
|
|
QCOMPARE(QThread::currentThread(), task.thread());
|
|
snapshots.append(task.progressInfo());
|
|
});
|
|
|
|
QSignalSpy done(&task, &Task::result);
|
|
task.start();
|
|
QTRY_COMPARE(done.size(), 1);
|
|
|
|
QCOMPARE(task.state(), Task::State::Succeeded);
|
|
QVERIFY(!snapshots.isEmpty());
|
|
QVERIFY(snapshots.first().preparing);
|
|
QVERIFY(!snapshots.first().currentPath.isEmpty());
|
|
|
|
const auto last = task.progressInfo();
|
|
QVERIFY(!last.preparing);
|
|
QVERIFY(last.totalsKnown);
|
|
QCOMPARE(last.totalBytes, quint64(6 * 1024 * 1024));
|
|
QCOMPARE(last.processedBytes, last.totalBytes);
|
|
QCOMPARE(last.totalItems, quint64(4));
|
|
QCOMPARE(last.processedItems, last.totalItems);
|
|
|
|
quint64 previous = 0;
|
|
for (const auto &snapshot : snapshots) {
|
|
if (snapshot.preparing)
|
|
continue;
|
|
QVERIFY(snapshot.processedBytes >= previous);
|
|
QVERIFY(snapshot.processedBytes <= snapshot.totalBytes);
|
|
previous = snapshot.processedBytes;
|
|
}
|
|
|
|
QCOMPARE(task.operationName(), QString("copy"));
|
|
QCOMPARE(task.destinationName(), QString("target"));
|
|
}
|
|
|
|
void copyThatCannotFitFailsBeforeWriting()
|
|
{
|
|
QTemporaryDir target(QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation)
|
|
+ "/cutefish-io-XXXXXX");
|
|
QVERIFY(target.isValid());
|
|
|
|
struct statvfs fs;
|
|
QCOMPARE(::statvfs(QFile::encodeName(target.path()), &fs), 0);
|
|
const qint64 available = qint64(fs.f_bavail) * fs.f_frsize;
|
|
if (available > (qint64(8) << 30))
|
|
QSKIP("The runtime directory has too much free space to fill");
|
|
|
|
// Kept on the build tree's disk, so the large source costs no memory.
|
|
QTemporaryDir source(QDir::currentPath() + "/cutefish-io-XXXXXX");
|
|
QVERIFY(source.isValid());
|
|
const qint64 length = available + 64 * 1024 * 1024;
|
|
|
|
const int full =
|
|
::open(QFile::encodeName(source.filePath("full")), O_WRONLY | O_CREAT, 0600);
|
|
QVERIFY(full >= 0);
|
|
const int allocated = ::posix_fallocate(full, 0, length);
|
|
::close(full);
|
|
if (allocated != 0)
|
|
QSKIP("The build tree has no room for a source larger than the target");
|
|
|
|
Task copy({Operation::Copy, {local(source.filePath("full"))}, local(target.path())});
|
|
QSignalSpy failed(©, &Task::result);
|
|
copy.start();
|
|
QTRY_COMPARE(failed.size(), 1);
|
|
QCOMPARE(copy.error(), ENOSPC);
|
|
QVERIFY(QDir(target.path())
|
|
.isEmpty(QDir::AllEntries | QDir::Hidden | QDir::NoDotAndDotDot));
|
|
|
|
QFile sparse(source.filePath("sparse"));
|
|
QVERIFY(sparse.open(QIODevice::WriteOnly));
|
|
QVERIFY(sparse.resize(length));
|
|
QVERIFY(sparse.seek(length - 4));
|
|
QCOMPARE(sparse.write("tail"), 4);
|
|
sparse.close();
|
|
|
|
Task fits({Operation::Copy, {local(sparse.fileName())}, local(target.path())});
|
|
QSignalSpy done(&fits, &Task::result);
|
|
fits.start();
|
|
QTRY_COMPARE(done.size(), 1);
|
|
QCOMPARE(fits.state(), Task::State::Succeeded);
|
|
QCOMPARE(QFileInfo(target.filePath("sparse")).size(), length);
|
|
}
|
|
|
|
void preparationSharesCancellation()
|
|
{
|
|
QTemporaryDir dir;
|
|
QVERIFY(QDir().mkpath(dir.filePath("nested")));
|
|
writeFile(dir.filePath("nested/file"));
|
|
|
|
OperationContext task;
|
|
OperationContext scan;
|
|
scan.cancelFlag = &task.canceled;
|
|
scan.currentItem = [&](const QString &) {
|
|
task.canceled = true;
|
|
};
|
|
|
|
QCOMPARE(errorCode([&] { FileEnumerator::size(dir.path(), scan); }), ECANCELED);
|
|
QVERIFY(QFileInfo::exists(dir.filePath("nested/file")));
|
|
}
|
|
|
|
void taskDestructionWhileWaiting()
|
|
{
|
|
QTemporaryDir dir;
|
|
writeFile(dir.filePath("source"));
|
|
writeFile(dir.filePath("target"));
|
|
auto *task = new Task({Operation::Rename,
|
|
{local(dir.filePath("source"))},
|
|
local(dir.filePath("target")),
|
|
ConflictPolicy::Ask});
|
|
|
|
QSignalSpy conflict(task, &Task::conflictRequested);
|
|
task->start();
|
|
QTRY_COMPARE(conflict.size(), 1);
|
|
|
|
delete task;
|
|
QVERIFY(QFileInfo::exists(dir.filePath("source")));
|
|
}
|
|
|
|
void asynchronousErrorsAndMkdir()
|
|
{
|
|
QTemporaryDir dir;
|
|
Task mkdirTask({Operation::Mkdir, {}, local(dir.filePath("folder"))});
|
|
QSignalSpy made(&mkdirTask, &Task::result);
|
|
mkdirTask.start();
|
|
QTRY_COMPARE(made.size(), 1);
|
|
QCOMPARE(mkdirTask.state(), Task::State::Succeeded);
|
|
|
|
Task unsupported(
|
|
{Operation::Copy, {QUrl("https://example.invalid/file")}, local(dir.path())});
|
|
QSignalSpy failed(&unsupported, &Task::result);
|
|
unsupported.start();
|
|
QTRY_COMPARE(failed.size(), 1);
|
|
QCOMPARE(unsupported.error(), ENOTSUP);
|
|
|
|
Task canceled({Operation::Mkdir, {}, local(dir.filePath("canceled"))});
|
|
QSignalSpy done(&canceled, &Task::result);
|
|
canceled.cancel();
|
|
canceled.start();
|
|
QTRY_COMPARE(done.size(), 1);
|
|
QCOMPARE(canceled.state(), Task::State::Canceled);
|
|
QVERIFY(!QFileInfo::exists(dir.filePath("canceled")));
|
|
}
|
|
|
|
void linkDrop()
|
|
{
|
|
QTemporaryDir dir;
|
|
writeFile(dir.filePath("file"));
|
|
QVERIFY(QDir().mkpath(dir.filePath("target")));
|
|
|
|
QMimeData mime;
|
|
mime.setUrls({local(dir.filePath("file"))});
|
|
|
|
QSignalSpy finished(TaskController::instance(), &TaskController::taskFinished);
|
|
drop(&mime, local(dir.filePath("target")), Qt::LinkAction);
|
|
QTRY_COMPARE(finished.size(), 1);
|
|
QVERIFY(QFileInfo(dir.filePath("target/file")).isSymbolicLink());
|
|
QVERIFY(QFileInfo::exists(dir.filePath("file")));
|
|
}
|
|
};
|
|
|
|
QTEST_GUILESS_MAIN(IOTest)
|
|
#include "tst_io.moc"
|