diff --git a/autotests/jobtest.h b/autotests/jobtest.h --- a/autotests/jobtest.h +++ b/autotests/jobtest.h @@ -127,7 +127,11 @@ void enterLoop(); enum { AlreadyExists = 1 }; void copyLocalFile(const QString &src, const QString &dest); + void setXattr(const QString &src, const QString &command); + void compareXattr(const QString &src, const QString &dest, QString &command); + void copyLocalFileWithXattr(const QString &src, const QString &dest); void copyLocalDirectory(const QString &src, const QString &dest, int flags = 0); + void copyLocalDirectoryWithXattr(const QString &src, const QString &dest, int flags = 0); void moveLocalFile(const QString &src, const QString &dest); void moveLocalDirectory(const QString &src, const QString &dest); //void copyFileToSystem( bool resolve_local_urls ); diff --git a/autotests/jobtest.cpp b/autotests/jobtest.cpp --- a/autotests/jobtest.cpp +++ b/autotests/jobtest.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -525,6 +526,166 @@ QFile::remove(dest); } +void JobTest::setXattr(const QString &src, const QString &command) +{ + QProcess xattrwriter; + + QStringList arguments = {"-n", "user.fnoValue", src}; + xattrwriter.start(command, arguments); + xattrwriter.waitForFinished(-1); + + QHash attrs; + attrs["user.name with space"] = "value with spaces"; + attrs["user.baloo.rating"] = "1"; + attrs["user.fnewLine"] = "line1\\nline2"; + attrs["user.flistNull"] = "item1\\0item2"; + attrs["user.fattr.with.a.lot.of.namespaces"] = "true"; + attrs["root.fnot.allowed"] = "forbidden"; + attrs["user.name with space"] = "value with spaces"; + + arguments.insert(2, "-v"); + arguments.insert(3, "value"); + // arguments 0:"-n" 1:"name" 2:"-v", 3:"value" 4:src + QHashIterator i(attrs); + while (i.hasNext()) { + i.next(); + arguments.replace(1, i.key()); + arguments.replace(3, i.value()); + xattrwriter.start(command, arguments); + xattrwriter.waitForStarted(); + QVERIFY(xattrwriter.state() == QProcess::Running); + xattrwriter.waitForFinished(-1); + QVERIFY(xattrwriter.exitStatus() == QProcess::NormalExit); + } +} + +void JobTest::compareXattr(const QString &src, const QString &dest, QString &command) +{ + if (command == "setfattr"){ + command = "getfattr"; + } else if (command == "setextattr") { + command = "getextatr"; + } + + QProcess xattrreader; + xattrreader.setProcessChannelMode(QProcess::MergedChannels); + + //test destination to see if filesystem supports xattr + xattrreader.start(command, QStringList{"-n", "user.test", dest}); + xattrreader.waitForFinished(-1); + QString testfs = xattrreader.readAllStandardOutput(); + if (testfs.section(':', 2, 2) == " Operation not supported\n") { + return; + } else { + //clean up + xattrreader.start(command, QStringList{"-x", "user.test", dest}); + xattrreader.waitForFinished(-1); + } + + QHash attrs; + attrs["user.fnoValue"] = ""; + attrs["user.name with space"] = "value with spaces"; + attrs["user.baloo.rating"] = "1"; + attrs["user.fnewLine"] = "line1\\nline2"; + attrs["user.flistNull"] = "item1\\0item2"; + attrs["user.fattr.with.a.lot.of.namespaces"] = "true"; + attrs["root.fnot.allowed"] = "forbidden"; + attrs["user.name with space"] = "value with spaces"; + + QStringList arguments = {"-n", "name", src}; + QHashIterator i(attrs); + while (i.hasNext()) { + i.next(); + arguments.replace(1, i.key()); + xattrreader.start(command, arguments); + xattrreader.waitForStarted(); + QVERIFY(xattrreader.state() == QProcess::Running); + xattrreader.waitForFinished(-1); + QVERIFY(xattrreader.exitStatus() == QProcess::NormalExit); + QString resultsrc = xattrreader.readAllStandardOutput(); + /****** + * We need to chop the filename from command output to compare + * 0:"getfattr: Removing leading '/' from absolute path names\n + * 1:# file: home/user/.qttest/share/kio/jobtest/kiotests/fileFromHome\n + * 2:user.baloo.rating=\"1\"\n + * 3:\n" + * ****/ + resultsrc = resultsrc.section('\n', 2, 2); + + arguments.replace(2, dest); + xattrreader.start(command, arguments); + xattrreader.waitForStarted(); + QVERIFY(xattrreader.state() == QProcess::Running); + xattrreader.waitForFinished(-1); + QVERIFY(xattrreader.exitStatus() == QProcess::NormalExit); + QString resultdest = xattrreader.readAllStandardOutput(); + resultdest = resultdest.section('\n', 2, 2); + + QCOMPARE(resultdest, resultsrc); + } +} + +void JobTest::copyLocalFileWithXattr(const QString &src, const QString &dest) +{ + /***** + * Find if the platform have support for xattr. + * Linux commands: setfattr, getfattr + * BSD commands: setextattr, getextattr + * MacOS commands: xattr -w, xattr -p + ****/ + QString command = QStandardPaths::findExecutable("setfattr"); + if (command.isEmpty()) { + command = QStandardPaths::findExecutable("setextattr"); + if (command.isEmpty()) { + command = QStandardPaths::findExecutable("xattr"); + } + } + command = command.split("/").last(); + + // TODO: The tests are linux only for now. + if (command.isEmpty() || command != "setfattr") { + return; + } + + setXattr(src, command); + + // Simplified version of tests on copyLocalFile with a added compareXattr call + const QUrl u = QUrl::fromLocalFile(src); + const QUrl d = QUrl::fromLocalFile(dest); + + // copy the file with file_copy + const int perms = 0666; + KIO::Job *job = KIO::file_copy(u, d, perms, KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + job->exec(); + compareXattr(src, dest, command); // Our test + + // cleanup and retry with KIO::copy() + QFile::remove(dest); + job = KIO::copy(u, d, KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + job->exec(); + compareXattr(src, dest, command); // Our test + + // cleanup and retry with KIO::copyAs() + QFile::remove(dest); + job = KIO::copyAs(u, d, KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + job->exec(); + + // Do it again, with Overwrite. + job = KIO::copyAs(u, d, KIO::Overwrite | KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + job->exec(); + compareXattr(src, dest, command); // Our test + + // Clean up + QFile::remove(dest); +} + void JobTest::copyLocalDirectory(const QString &src, const QString &_dest, int flags) { QVERIFY(QFileInfo(src).isDir()); @@ -577,6 +738,62 @@ job->setUiDelegateExtension(nullptr); ok = job->exec(); QVERIFY(!ok); + + // clean up + QDir(dest).removeRecursively(); +} + +void JobTest::copyLocalDirectoryWithXattr(const QString &src, const QString &dest, int flags) +{ + /***** + * Find if the platform have support for xattr. + * Linux commands: setfattr, getfattr + * BSD commands: setextattr, getextattr + * MacOS commands: xattr -w, xattr -p + ****/ + QString command = QStandardPaths::findExecutable("setfattr"); + if (command.isEmpty()) { + command = QStandardPaths::findExecutable("setextattr"); + if (command.isEmpty()) { + command = QStandardPaths::findExecutable("xattr"); + } + } + command = command.split("/").last(); + QUrl u = QUrl::fromLocalFile(src); + QUrl d = QUrl::fromLocalFile(dest); + + // TODO: The tests are linux only for now. + if (command.isEmpty() || command != "setfattr") { + // recreate dest to not fail futher tests + KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + job->exec(); + return; + } + + setXattr(src, command); + + // Simplified version of tests on copyLocalDirectory with a added compareXattr call + + KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + job->exec(); + compareXattr(src, dest, command); // Our test + + if (flags & AlreadyExists) { + dest += '/' + u.fileName(); + //qDebug() << "Expecting dest=" << dest; + } + + // Do it again, with Overwrite. + // Use copyAs, we don't want a subdir inside d. + job = KIO::copyAs(u, d, KIO::HideProgressInfo | KIO::Overwrite); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + job->exec(); + compareXattr(src, dest, command); // Our test } #ifndef Q_OS_WIN @@ -617,6 +834,7 @@ const QString dest = homeTmpDir() + "fileFromHome_copied"; createTestFile(filePath); copyLocalFile(filePath, dest); + copyLocalFileWithXattr(filePath, dest); } void JobTest::copyDirectoryToSamePartition() @@ -626,6 +844,7 @@ const QString dest = homeTmpDir() + "dirFromHome_copied"; createTestDirectory(src); copyLocalDirectory(src, dest); + copyLocalDirectoryWithXattr(src, dest); } void JobTest::copyDirectoryToExistingDirectory() @@ -638,6 +857,7 @@ createTestDirectory(src); createTestDirectory(dest); copyLocalDirectory(src, dest, AlreadyExists); + copyLocalDirectoryWithXattr(src, dest, AlreadyExists); } void JobTest::copyFileToOtherPartition() @@ -647,6 +867,7 @@ const QString dest = otherTmpDir() + "fileFromHome_copied"; createTestFile(filePath); copyLocalFile(filePath, dest); + copyLocalFileWithXattr(filePath, dest); } void JobTest::copyDirectoryToOtherPartition() @@ -656,6 +877,7 @@ const QString dest = otherTmpDir() + "dirFromHome_copied"; createTestDirectory(src); copyLocalDirectory(src, dest); + copyLocalDirectoryWithXattr(src, dest); } void JobTest::copyRelativeSymlinkToSamePartition() // #352927 diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -39,6 +39,7 @@ davjob.cpp deletejob.cpp copyjob.cpp + copyxattrjob.cpp filejob.cpp mkdirjob.cpp mkpathjob.cpp diff --git a/src/core/ConfigureChecks.cmake b/src/core/ConfigureChecks.cmake --- a/src/core/ConfigureChecks.cmake +++ b/src/core/ConfigureChecks.cmake @@ -26,6 +26,8 @@ check_include_files(sys/types.h HAVE_SYS_TYPES_H) check_include_files(fstab.h HAVE_FSTAB_H) check_include_files(sys/param.h HAVE_SYS_PARAM_H) +check_include_files(sys/xattr.h HAVE_SYS_XATTR_H) +check_include_files(sys/extattr.h HAVE_SYS_EXTATTR_H) check_library_exists(volmgt volmgt_running "" HAVE_VOLMGT) diff --git a/src/core/config-kiocore.h.cmake b/src/core/config-kiocore.h.cmake --- a/src/core/config-kiocore.h.cmake +++ b/src/core/config-kiocore.h.cmake @@ -4,6 +4,9 @@ #cmakedefine01 HAVE_POSIX_ACL /* Defined if acl/libacl.h exists */ #cmakedefine01 HAVE_ACL_LIBACL_H +/* Defined if system has extended file attributes support. */ +#cmakedefine01 HAVE_SYS_XATTR_H +#cmakedefine01 HAVE_SYS_EXTATTR_H #define CMAKE_INSTALL_FULL_LIBEXECDIR_KF5 "${CMAKE_INSTALL_FULL_LIBEXECDIR_KF5}" diff --git a/src/core/copyjob.cpp b/src/core/copyjob.cpp --- a/src/core/copyjob.cpp +++ b/src/core/copyjob.cpp @@ -21,7 +21,6 @@ #include "copyjob.h" #include "kiocoredebug.h" -#include #include "kcoredirlister.h" #include "kfileitem.h" #include "job.h" // buildErrorString @@ -40,6 +39,7 @@ #include "scheduler.h" #include "kdirwatch.h" #include "kprotocolmanager.h" +#include "copyxattrjob.h" #include #include @@ -1114,6 +1114,9 @@ return; } } else { // no error : remove from list, to move on to next dir + // after copy is finished, copy xattrs before closing + KJob *job = KIO::copy_xattr((*it).uSource, (*it).uDest); + job->exec(); //this is required for the undo feature emit q->copyingDone(q, (*it).uSource, finalDestUrl((*it).uSource, (*it).uDest), (*it).mtime, true, false); m_directoriesCopied.append(*it); diff --git a/src/core/filecopyjob.cpp b/src/core/filecopyjob.cpp --- a/src/core/filecopyjob.cpp +++ b/src/core/filecopyjob.cpp @@ -18,6 +18,7 @@ Boston, MA 02110-1301, USA. */ +#include "copyxattrjob.h" #include "filecopyjob.h" #include "job_p.h" #include @@ -513,6 +514,9 @@ if (job == d->m_copyJob) { d->m_copyJob = nullptr; + // after copy is finished, copy xattrs before deleting, if move + KJob *job = KIO::copy_xattr(d->m_src, d->m_dest); + job->exec(); if (d->m_move) { d->m_delJob = file_delete(d->m_src, HideProgressInfo/*no GUI*/); // Delete source addSubjob(d->m_delJob);