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,170 @@ QFile::remove(dest); } +QHash getSampleAttrs() +{ + 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"; + return attrs; +} + +void JobTest::setXattr(const QString &src, const QString &command) +{ + QProcess xattrwriter; + + QStringList arguments = {"-n", "user.fnoValue", src}; + xattrwriter.start(command, arguments); + QVERIFY(xattrwriter.waitForFinished(-1)); + + QHash attrs = getSampleAttrs(); + + 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); + QVERIFY(xattrwriter.waitForStarted()); + QCOMPARE(xattrwriter.state(), QProcess::Running); + QVERIFY(xattrwriter.waitForFinished(-1)); + QCOMPARE(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}); + QVERIFY(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}); + QVERIFY(xattrreader.waitForFinished(-1)); + } + + QHash attrs = getSampleAttrs(); + + QStringList arguments = {"-n", "name", src}; + QHashIterator i(attrs); + while (i.hasNext()) { + i.next(); + arguments.replace(1, i.key()); + xattrreader.start(command, arguments); + QVERIFY(xattrreader.waitForStarted()); + QCOMPARE(xattrreader.state(), QProcess::Running); + QVERIFY(xattrreader.waitForFinished(-1)); + QCOMPARE(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); + QVERIFY(xattrreader.waitForStarted()); + QCOMPARE(xattrreader.state(), QProcess::Running); + QVERIFY(xattrreader.waitForFinished(-1)); + QCOMPARE(xattrreader.exitStatus(), QProcess::NormalExit); + QString resultdest = xattrreader.readAllStandardOutput(); + resultdest = resultdest.section('\n', 2, 2); + + QCOMPARE(resultdest, resultsrc); + } +} + +QString getXattrCommand() +{ + /***** + * 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"); + } + } + return command.split("/").last(); +} + +void JobTest::copyLocalFileWithXattr(const QString &src, const QString &dest) +{ + QString command = getXattrCommand(); + + // 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); + QVERIFY2(job->exec(), qPrintable(job->errorString())); + 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); + QVERIFY2(job->exec(), qPrintable(job->errorString())); + 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); + QVERIFY2(job->exec(), qPrintable(job->errorString())); + compareXattr(src, dest, command); // Our test + + // Do it again, with Overwrite. + job = KIO::copyAs(u, d, KIO::Overwrite | KIO::HideProgressInfo); + job->setUiDelegate(nullptr); + job->setUiDelegateExtension(nullptr); + QVERIFY2(job->exec(), qPrintable(job->errorString())); + 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()); @@ -579,6 +744,45 @@ QVERIFY(!ok); } +void JobTest::copyLocalDirectoryWithXattr(const QString &src, const QString &_dest, int flags) +{ + QString command = getXattrCommand(); + + // TODO: The tests are linux only for now. + if (command.isEmpty() || command != "setfattr") { + return; + } + + const QUrl u = QUrl::fromLocalFile(src); + QString dest(_dest); + const QUrl d = QUrl::fromLocalFile(dest); + + setXattr(src, command); + + if (flags & AlreadyExists) { + dest += '/' + u.fileName(); + //qDebug() << "Expecting dest=" << dest; + } else { + // clean result of previous copyLocalDirectory call + QDir(dest).removeRecursively(); + } + + // 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); + QVERIFY2(job->exec(), qPrintable(job->errorString())); + compareXattr(src, dest, command); // Our test + + // 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); + QVERIFY2(job->exec(), qPrintable(job->errorString())); + compareXattr(src, dest, command); // Our test +} + #ifndef Q_OS_WIN static QString linkTarget(const QString &path) { @@ -617,6 +821,7 @@ const QString dest = homeTmpDir() + "fileFromHome_copied"; createTestFile(filePath); copyLocalFile(filePath, dest); + copyLocalFileWithXattr(filePath, dest); } void JobTest::copyDirectoryToSamePartition() @@ -626,6 +831,7 @@ const QString dest = homeTmpDir() + "dirFromHome_copied"; createTestDirectory(src); copyLocalDirectory(src, dest); + copyLocalDirectoryWithXattr(src, dest); } void JobTest::copyDirectoryToExistingDirectory() @@ -638,6 +844,7 @@ createTestDirectory(src); createTestDirectory(dest); copyLocalDirectory(src, dest, AlreadyExists); + copyLocalDirectoryWithXattr(src, dest, AlreadyExists); } void JobTest::copyFileToOtherPartition() @@ -647,6 +854,7 @@ const QString dest = otherTmpDir() + "fileFromHome_copied"; createTestFile(filePath); copyLocalFile(filePath, dest); + copyLocalFileWithXattr(filePath, dest); } void JobTest::copyDirectoryToOtherPartition() @@ -656,6 +864,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 *xattrjob = KIO::copy_xattr((*it).uSource, (*it).uDest); + xattrjob->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/copyxattrjob.h b/src/core/copyxattrjob.h new file mode 100644 --- /dev/null +++ b/src/core/copyxattrjob.h @@ -0,0 +1,63 @@ +/* This file is part of the KDE libraries + Copyright (C) 2019 Cochise César + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef COPYXATTRJOB_H +#define COPYXATTRJOB_H + +#include "job_base.h" + +namespace KIO +{ + +class CopyXattrJobPrivate; +/** + * @class KIO::CopyXattrJob copyxattrjob.h + * + * The CopyXattrJob copies extended attributes from a file or dir to another + * @see KIO::copy_xattr() + */ +class KIOCORE_EXPORT CopyXattrJob : public Job +{ + Q_OBJECT + +public: + ~CopyXattrJob(); + +protected Q_SLOTS: + void slotResult(KJob *job) override; + +protected: + CopyXattrJob(CopyXattrJobPrivate &dd); + +private: + Q_DECLARE_PRIVATE(CopyXattrJob) +}; + +/** + * Copy the extended attributes from a file/directory to other + * + * Used by @see KIO::file_copy and KIO::copy + * @param src The file from where we get the xattrs. + * @param dest The file where we put the xattrs. + * @return the job handling the operation. + */ +KIOCORE_EXPORT CopyXattrJob *copy_xattr(const QUrl &src, const QUrl &dest); + +} +#endif // COPYXATTRJOB_H diff --git a/src/core/copyxattrjob.cpp b/src/core/copyxattrjob.cpp new file mode 100644 --- /dev/null +++ b/src/core/copyxattrjob.cpp @@ -0,0 +1,188 @@ +/* This file is part of the KDE libraries + Copyright (C) 2000 Stephan Kulow + 2000-2009 David Faure + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include + +#include "copyxattrjob.h" +#include "job_p.h" +#include +#include +#include +#include +#if HAVE_SYS_XATTR_H +#include +#elif HAVE_SYS_EXTATTR_H +#include +#endif + +using namespace KIO; + +class KIO::CopyXattrJobPrivate: public KIO::JobPrivate +{ +public: + CopyXattrJobPrivate(const QUrl &src, const QUrl &dest) + : m_src(src), m_dest(dest) + { } + QUrl m_src; + QUrl m_dest; + const char *m_bsrc; + const char *m_bdest; + QList m_keyList; + + void copyXattr(); + + void slotStart(); + + Q_DECLARE_PUBLIC(CopyXattrJob) + + static inline CopyXattrJob *newJob(const QUrl &src, const QUrl &dest) + { + CopyXattrJob *job = new CopyXattrJob(*new CopyXattrJobPrivate(src, dest)); + return job; + } +}; + +CopyXattrJob::~CopyXattrJob() +{ +} + +/* + * The CopyXattrJob is a very simple job. Reads the list of keys + * and copies each attribute. As a fast operation, don't need to + * be able to suspend. + */ +CopyXattrJob::CopyXattrJob(CopyXattrJobPrivate &dd) + : Job(dd) +{ + Q_D(CopyXattrJob); + QTimer::singleShot(0, this, [d]() { d->slotStart(); }); +} + +void CopyXattrJobPrivate::slotStart() +{ + copyXattr(); +} + +void CopyXattrJobPrivate::copyXattr() +{ + Q_Q(CopyXattrJob); + // Abort if system don't have support to xattrs +#if !HAVE_SYS_XATTR_H && !HAVE_SYS_EXTATTR_H + q->emitResult(); +#endif + // Dont try to copy if cant stat source and dest + // Happens sometimes, as KIO can copy from data, lost access, etc. + if (!QFile::exists(m_src.toLocalFile()) && !QFile::exists(m_dest.toLocalFile())){ + q->emitResult(); + } + + const QByteArray source = QFile::encodeName(m_src.toLocalFile()); + m_bsrc = source.constData(); + const int bsrc_fd = open(m_bsrc, 0); + if (bsrc_fd < 0) + { + q->setErrorText(QLatin1String("failed to obtain file descriptor of source during xattr copy")); + q->emitResult(); + } + const QByteArray destination = QFile::encodeName(m_dest.toLocalFile()); + m_bdest = destination.constData(); + const int bdest_fd = open(m_bdest, 0); + if (bdest_fd < 0) + { + q->setErrorText(QLatin1String("failed to obtain file descriptor of dest during xattr copy")); + q->emitResult(); + } + // Get the size of the key list +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + ssize_t listlen = flistxattr(bsrc_fd, nullptr, 0); +#elif defined(Q_OS_MAC) + ssize_t listlen = flistxattr(bsrc_fd, nullptr, 0, 0); +#elif HAVE_SYS_EXTATTR + ssize_t listlen = extattr_list_file(m_bsrc, EXTATTR_NAMESPACE_USER, nullptr, 0); +#endif + if (listlen == -1) { + q->setErrorText(QLatin1String("libc failed to extract list of xattr from file")); + q->emitResult(); + } else if (listlen == 0) { + qCDebug(KIO_CORE) << "file " << m_bsrc << " don't have any xattr"; + q->emitResult(); + } + QByteArray keylist(listlen, Qt::Uninitialized); + // get the key list +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + listlen = flistxattr(bsrc_fd, keylist.data(), listlen); +#elif defined(Q_OS_MAC) + listlen = flistxattr(bsrc_fd, keylist.data(), listlen, 0); +#elif HAVE_SYS_EXTATTR + listlen = extattr_list_file(m_bsrc, EXTATTR_NAMESPACE_USER, keylist.data(), listlen); +#endif + m_keyList = keylist.split('\0'); + if (m_keyList.last().isEmpty()) m_keyList.removeLast(); // the last item may be empty + for (const auto & key : qAsConst(m_keyList)) { + // get the size of key value +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + ssize_t valuelen = fgetxattr(bsrc_fd, key.constData(), nullptr, 0); +#elif defined(Q_OS_MAC) + ssize_t valuelen = fgetxattr(bsrc_fd, key.constData(), nullptr, 0, 0, 0); +#elif HAVE_SYS_EXTATTR + ssize_t valuelen = extattr_get_file(m_bsrc, EXTATTR_NAMESPACE_USER, key.constData(), nullptr, 0); +#endif + if (valuelen == -1) { + q->setErrorText(QLatin1String("libc failed to extract a xattr value from file")); + continue; + } + QByteArray value(valuelen + 1, Qt::Uninitialized); + //get the value of the key +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + valuelen = fgetxattr(bsrc_fd, key.constData(), value.data(), valuelen); +#elif defined(Q_OS_MAC) + vallen = fgetxattr(bsrc_fd, key.constData(), value.data(), valuelen, 0, 0); +#elif HAVE_SYS_EXTATTR + vallen = extattr_get_file(m_bsrc, EXTATTR_NAMESPACE_USER, key.constData(), value.data(), valuelen); +#endif + //write key:value pair on dest file +#if HAVE_SYS_XATTR_H && !defined(__stub_getxattr) + ssize_t destlen = fsetxattr(bdest_fd, key.constData(), value.data(), valuelen, 0); +#elif defined(Q_OS_MAC) + ssize_t destlen = fsetxattr(bdest_fd, key.constData(), value.data(), valuelen, 0, 0); +#elif defined(Q_OS_FREEBSD) || defined(Q_OS_NETBSD) + ssize_t destlen = extattr_set_file(m_bdest, EXTATTR_NAMESPACE_USER, key.constData(), value.data(), valuelen); +#endif + if (destlen == -1) { + if (errno == ENOTSUP) { + q->setErrorText(QLatin1String("destination filesystem don't support xattrs")); + q->emitResult(); + } else { + q->setErrorText(QLatin1String("failed to write a xattr on destination file")); + } + } + } + q->emitResult(); +} + +void CopyXattrJob::slotResult(KJob *job) +{ + emitResult(); +} + +CopyXattrJob *KIO::copy_xattr(const QUrl &src, const QUrl &dest) +{ + return CopyXattrJobPrivate::newJob(src, dest); +} 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,10 @@ if (job == d->m_copyJob) { d->m_copyJob = nullptr; + // after copy is finished, copy xattrs before deleting, if move + KJob *xattrjob = KIO::copy_xattr(d->m_src, d->m_dest); + addSubjob(xattrjob); + if (d->m_move) { d->m_delJob = file_delete(d->m_src, HideProgressInfo/*no GUI*/); // Delete source addSubjob(d->m_delJob);