diff --git a/importer/importer.cpp b/importer/importer.cpp index 38219821..4648704b 100644 --- a/importer/importer.cpp +++ b/importer/importer.cpp @@ -1,247 +1,259 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2009 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "importer.h" // Qt #include #include #include #include // KDE #include #include #include +#include #include #include #include #include // stdc++ #include // Local #include #include #include #include namespace Gwenview { struct ImporterPrivate { Importer* q; QWidget* mAuthWindow; std::unique_ptr mFileNameFormater; QUrl mTempImportDirUrl; QTemporaryDir* mTempImportDir; /* @defgroup reset Should be reset in start() * @{ */ QList mUrlList; QList mImportedUrlList; QList mSkippedUrlList; int mRenamedCount; int mProgress; int mJobProgress; /* @} */ QUrl mCurrentUrl; bool createImportDir(const QUrl& url) { Q_ASSERT(url.isLocalFile()); // FIXME: Support remote urls if (!QDir().mkpath(url.toLocalFile())) { emit q->error(i18n("Could not create destination folder.")); return false; } QString tempDirPath = url.toLocalFile() + "/.gwenview_importer-XXXXXX"; mTempImportDir = new QTemporaryDir(tempDirPath); if (!mTempImportDir->isValid()) { emit q->error(i18n("Could not create temporary upload folder.")); return false; } mTempImportDirUrl = QUrl::fromLocalFile(mTempImportDir->path() + '/'); if (!mTempImportDirUrl.isValid()) { emit q->error(i18n("Could not create temporary upload folder.")); return false; } return true; } void importNext() { if (mUrlList.empty()) { q->finalizeImport(); return; } mCurrentUrl = mUrlList.takeFirst(); QUrl dst = mTempImportDirUrl; dst.setPath(dst.path() + mCurrentUrl.fileName()); KIO::Job* job = KIO::copy(mCurrentUrl, dst, KIO::HideProgressInfo); KJobWidgets::setWindow(job, mAuthWindow); QObject::connect(job, &KJob::result, q, &Importer::slotCopyDone); QObject::connect(job, SIGNAL(percent(KJob*,ulong)), q, SLOT(slotPercent(KJob*,ulong))); } void renameImportedUrl(const QUrl& src) { QUrl dst = src.resolved(QUrl("..")); QString fileName; if (mFileNameFormater.get()) { KFileItem item(src); item.setDelayedMimeTypes(true); // Get the document time, but do not cache the result because the // 'src' url is temporary: if we import "foo/image.jpg" and // "bar/image.jpg", both images will be temporarily saved in the // 'src' url. QDateTime dateTime = TimeUtils::dateTimeForFileItem(item, TimeUtils::SkipCache); fileName = mFileNameFormater->format(src, dateTime); } else { fileName = src.fileName(); } dst.setPath(dst.path() + fileName); - FileUtils::RenameResult result = FileUtils::rename(src, dst, mAuthWindow); + FileUtils::RenameResult result; + // Create additional subfolders if needed (e.g. when extra slashes in FileNameFormater) + QUrl subFolder = dst.adjusted(QUrl::RemoveFilename); + KIO::Job* job = KIO::mkpath(subFolder, QUrl(), KIO::HideProgressInfo); + KJobWidgets::setWindow(job,mAuthWindow); + if (!job->exec()) { // if subfolder creation fails + qWarning() << "Could not create subfolder:" << subFolder; + result = FileUtils::RenameFailed; + } else { // if subfolder creation succeeds + result = FileUtils::rename(src, dst, mAuthWindow); + } + switch (result) { case FileUtils::RenamedOK: mImportedUrlList << mCurrentUrl; break; case FileUtils::RenamedUnderNewName: mRenamedCount++; mImportedUrlList << mCurrentUrl; break; case FileUtils::Skipped: mSkippedUrlList << mCurrentUrl; break; case FileUtils::RenameFailed: qWarning() << "Rename failed for" << mCurrentUrl; } q->advance(); importNext(); } }; Importer::Importer(QWidget* parent) : QObject(parent) , d(new ImporterPrivate) { d->q = this; d->mAuthWindow = parent; } Importer::~Importer() { delete d; } void Importer::setAutoRenameFormat(const QString& format) { if (format.isEmpty()) { d->mFileNameFormater.reset(nullptr); } else { d->mFileNameFormater.reset(new FileNameFormater(format)); } } void Importer::start(const QList& list, const QUrl& destination) { d->mUrlList = list; d->mImportedUrlList.clear(); d->mSkippedUrlList.clear(); d->mRenamedCount = 0; d->mProgress = 0; d->mJobProgress = 0; emitProgressChanged(); emit maximumChanged(d->mUrlList.count() * 100); if (!d->createImportDir(destination)) { qWarning() << "Could not create import dir"; return; } d->importNext(); } void Importer::slotCopyDone(KJob* _job) { KIO::CopyJob* job = static_cast(_job); QUrl url = job->destUrl(); if (job->error()) { qWarning() << "FIXME: What do we do with failed urls?"; advance(); d->importNext(); return; } d->renameImportedUrl(url); } void Importer::finalizeImport() { delete d->mTempImportDir; emit importFinished(); } void Importer::advance() { ++d->mProgress; d->mJobProgress = 0; emitProgressChanged(); } void Importer::slotPercent(KJob*, unsigned long percent) { d->mJobProgress = percent; emitProgressChanged(); } void Importer::emitProgressChanged() { emit progressChanged(d->mProgress * 100 + d->mJobProgress); } QList Importer::importedUrlList() const { return d->mImportedUrlList; } QList Importer::skippedUrlList() const { return d->mSkippedUrlList; } int Importer::renamedCount() const { return d->mRenamedCount; } } // namespace diff --git a/tests/auto/importertest.cpp b/tests/auto/importertest.cpp index 9187b4cf..5d6886c9 100644 --- a/tests/auto/importertest.cpp +++ b/tests/auto/importertest.cpp @@ -1,248 +1,269 @@ /* Gwenview: an image viewer Copyright 2009 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "importertest.h" // stdlib #include // Qt #include // KDE #include #include #include // Local #include "../importer/fileutils.h" #include "../importer/importer.h" #include "../importer/filenameformater.h" #include "testutils.h" QTEST_MAIN(ImporterTest) using namespace Gwenview; void ImporterTest::init() { mDocumentList = QList() << urlForTestFile("import/pict0001.jpg") << urlForTestFile("import/pict0002.jpg") << urlForTestFile("import/pict0003.jpg") ; mTempDir.reset(new QTemporaryDir()); } void ImporterTest::testContentsAreIdentical() { QVERIFY(!FileUtils::contentsAreIdentical(mDocumentList[0], mDocumentList[1])); QVERIFY(FileUtils::contentsAreIdentical(mDocumentList[0], mDocumentList[0])); QUrl url1 = mDocumentList[0]; QUrl url2 = urlForTestOutputFile("foo"); // Test on a copy of a file QFile::remove(url2.toLocalFile()); QFile::copy(url1.toLocalFile(), url2.toLocalFile()); QVERIFY(FileUtils::contentsAreIdentical(url1, url2)); // Alter one byte of the copy and test again QFile file(url2.toLocalFile()); QVERIFY(file.open(QIODevice::ReadOnly)); QByteArray data = file.readAll(); file.close(); data[data.size() / 2] = 255 - data[data.size() / 2]; file.open(QIODevice::WriteOnly); file.write(data); file.close(); QVERIFY(!FileUtils::contentsAreIdentical(url1, url2)); } void ImporterTest::testSuccessfulImport() { QUrl destUrl = QUrl::fromLocalFile(mTempDir->path() + "/foo"); Importer importer(nullptr); QSignalSpy maximumChangedSpy(&importer, SIGNAL(maximumChanged(int))); QSignalSpy errorSpy(&importer, SIGNAL(error(QString))); QList list = mDocumentList; QEventLoop loop; connect(&importer, &Importer::importFinished, &loop, &QEventLoop::quit); importer.start(list, destUrl); loop.exec(); QCOMPARE(maximumChangedSpy.count(), 1); QCOMPARE(maximumChangedSpy.takeFirst().at(0).toInt(), list.count() * 100); QCOMPARE(errorSpy.count(), 0); QCOMPARE(importer.importedUrlList().count(), list.count()); QCOMPARE(importer.importedUrlList(), list); QCOMPARE(importer.skippedUrlList().count(), 0); QCOMPARE(importer.renamedCount(), 0); for (const QUrl & src : qAsConst(list)) { QUrl dst = destUrl; dst.setPath(dst.path() + '/' + src.fileName()); QVERIFY(FileUtils::contentsAreIdentical(src, dst)); } } void ImporterTest::testSkippedUrlList() { QUrl destUrl = QUrl::fromLocalFile(mTempDir->path() + "/foo"); Importer importer(nullptr); QList list = mDocumentList.mid(0, 1); QEventLoop loop; connect(&importer, &Importer::importFinished, &loop, &QEventLoop::quit); importer.start(list, destUrl); loop.exec(); QCOMPARE(importer.importedUrlList().count(), 1); QCOMPARE(importer.importedUrlList(), list); list = mDocumentList; QList expectedImportedList = mDocumentList.mid(1); QList expectedSkippedList = mDocumentList.mid(0, 1); importer.start(list, destUrl); loop.exec(); QCOMPARE(importer.importedUrlList().count(), 2); QCOMPARE(importer.importedUrlList(), expectedImportedList); QCOMPARE(importer.skippedUrlList(), expectedSkippedList); QCOMPARE(importer.renamedCount(), 0); } void ImporterTest::testRenamedCount() { QUrl destUrl = QUrl::fromLocalFile(mTempDir->path() + "/foo"); Importer importer(nullptr); QList list; list << mDocumentList.first(); QEventLoop loop; connect(&importer, &Importer::importFinished, &loop, &QEventLoop::quit); importer.start(list, destUrl); loop.exec(); QCOMPARE(importer.importedUrlList().count(), 1); QCOMPARE(importer.importedUrlList(), list); // Modify imported document so that next import does not skip it { QUrl url = destUrl; url.setPath(url.path() + '/' + mDocumentList.first().fileName()); QFile file(url.toLocalFile()); QVERIFY(file.open(QIODevice::Append)); file.write("foo"); } list = mDocumentList; importer.start(list, destUrl); loop.exec(); QCOMPARE(importer.importedUrlList().count(), 3); QCOMPARE(importer.importedUrlList(), mDocumentList); QCOMPARE(importer.skippedUrlList().count(), 0); QCOMPARE(importer.renamedCount(), 1); } void ImporterTest::testFileNameFormater() { QFETCH(QString, fileName); QFETCH(QString, dateTime); QFETCH(QString, format); QFETCH(QString, expected); QUrl url = QUrl("file://foo/bar/" + fileName); FileNameFormater fileNameFormater(format); QCOMPARE(fileNameFormater.format(url, QDateTime::fromString(dateTime, Qt::ISODate)), expected); } #define NEW_ROW(fileName, dateTime, format, expected) QTest::newRow(fileName) << fileName << dateTime << format << expected void ImporterTest::testFileNameFormater_data() { QTest::addColumn("fileName"); QTest::addColumn("dateTime"); QTest::addColumn("format"); QTest::addColumn("expected"); NEW_ROW("PICT0001.JPG", "2009-10-24T22:50:49", "{date}_{time}.{ext}", "2009-10-24_22-50-49.JPG"); NEW_ROW("PICT0001.JPG", "2009-10-24T22:50:49", "{date}_{time}.{ext.lower}", "2009-10-24_22-50-49.jpg"); NEW_ROW("2009.10.24.JPG", "2009-10-24T22:50:49", "{date}_{time}.{ext.lower}", "2009-10-24_22-50-49.jpg"); NEW_ROW("PICT0001.JPG", "2009-10-24T22:50:49", "{name}.{ext}", "PICT0001.JPG"); NEW_ROW("PICT0001.JPG", "2009-10-24T22:50:49", "{name.lower}.{ext.lower}", "pict0001.jpg"); NEW_ROW("iLikeCurlies", "2009-10-24T22:50:49", "{{{name}}", "{iLikeCurlies}"); NEW_ROW("UnknownKeyword", "2009-10-24T22:50:49", "foo{unknown}bar", "foobar"); NEW_ROW("MissingClosingCurly", "2009-10-24T22:50:49", "foo{date", "foo"); + NEW_ROW("PICT0001.JPG", "2009-10-24T22:50:49", "{date}/{name}.{ext}", "2009-10-24/PICT0001.JPG"); } void ImporterTest::testAutoRenameFormat() { QStringList dates = QStringList() << "1979-02-23_10-20-00" << "2006-04-01_11-30-15" << "2009-10-01_21-15-27"; + QStringList dates2 = QStringList() + << "1979-02-23/10-20-00" + << "2006-04-01/11-30-15" + << "2009-10-01/21-15-27"; QCOMPARE(dates.count(), mDocumentList.count()); + QCOMPARE(dates2.count(), mDocumentList.count()); QUrl destUrl = QUrl::fromLocalFile(mTempDir->path() + "/foo"); Importer importer(nullptr); importer.setAutoRenameFormat("{date}_{time}.{ext}"); QList list = mDocumentList; QEventLoop loop; connect(&importer, &Importer::importFinished, &loop, &QEventLoop::quit); importer.start(list, destUrl); loop.exec(); QCOMPARE(importer.importedUrlList().count(), list.count()); QCOMPARE(importer.importedUrlList(), list); for (int pos = 0; pos < dates.count(); ++pos) { QUrl src = list[pos]; QUrl dst = destUrl; dst.setPath(dst.path() + '/' + dates[pos] + ".jpg"); QVERIFY(FileUtils::contentsAreIdentical(src, dst)); } + + // Test again with slashes in AutoRenameFormat + importer.setAutoRenameFormat("{date}/{time}.{ext}"); + importer.start(list, destUrl); + loop.exec(); + + QCOMPARE(importer.importedUrlList().count(), list.count()); + QCOMPARE(importer.importedUrlList(), list); + + for (int pos = 0; pos < dates2.count(); ++pos) { + QUrl src = list[pos]; + QUrl dst = destUrl; + dst.setPath(dst.path() + '/' + dates2[pos] + ".jpg"); + QVERIFY(FileUtils::contentsAreIdentical(src, dst)); + } } void ImporterTest::testReadOnlyDestination() { QUrl destUrl = QUrl::fromLocalFile(mTempDir->path() + "/foo"); chmod(QFile::encodeName(mTempDir->path()), 0555); Importer importer(nullptr); QSignalSpy errorSpy(&importer, SIGNAL(error(QString))); importer.start(mDocumentList, destUrl); QCOMPARE(errorSpy.count(), 1); QVERIFY(importer.importedUrlList().isEmpty()); }