diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fadc2977..b91c30f2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,6 +21,8 @@ set(SRCS_base errorlogmodel.cpp error.cpp podcastsearchmodel.cpp + storagemanager.cpp + storagemovejob.cpp mpris2/mpris2.cpp resources.qrc ) @@ -53,6 +55,13 @@ ecm_qt_declare_logging_category(SRCS_base DEFAULT_SEVERITY Info ) +ecm_qt_declare_logging_category(SRCS_base + HEADER "storagemovejoblogging.h" + IDENTIFIER "kastsStorageMoveJob" + CATEGORY_NAME "org.kde.kasts.storagemovejob" + DEFAULT_SEVERITY Info +) + ecm_qt_declare_logging_category(SRCS_base HEADER "feedlogging.h" IDENTIFIER "kastsFeed" @@ -81,6 +90,13 @@ ecm_qt_declare_logging_category(SRCS_base DEFAULT_SEVERITY Info ) +ecm_qt_declare_logging_category(SRCS_base + HEADER "storagemanagerlogging.h" + IDENTIFIER "kastsStorageManager" + CATEGORY_NAME "org.kde.kasts.storagemanager" + DEFAULT_SEVERITY Info +) + if(ANDROID) set (SRCS ${SRCS_base} androidlogging.h) diff --git a/src/audiomanager.cpp b/src/audiomanager.cpp index 41ec5cf8..b1241789 100644 --- a/src/audiomanager.cpp +++ b/src/audiomanager.cpp @@ -234,7 +234,7 @@ void AudioManager::setEntry(Entry *entry) + QStringLiteral(" audio_sink=\"scaletempo ! audioconvert ! audioresample ! autoaudiosink\" video_sink=\"fakevideosink\""))); #else qCDebug(kastsAudio) << "regular audio backend"; - d->m_player.setMedia(QUrl(QStringLiteral("file://") + d->m_entry->enclosure()->path())); + d->m_player.setMedia(QUrl::fromLocalFile(d->m_entry->enclosure()->path())); #endif // save the current playing track in the settingsfile for restoring on startup DataManager::instance().setLastPlayingEntry(d->m_entry->id()); diff --git a/src/datamanager.cpp b/src/datamanager.cpp index 05bfa2e2..fbc99384 100644 --- a/src/datamanager.cpp +++ b/src/datamanager.cpp @@ -6,13 +6,6 @@ #include "datamanager.h" -#include "audiomanager.h" -#include "database.h" -#include "datamanagerlogging.h" -#include "entry.h" -#include "feed.h" -#include "fetcher.h" -#include "settingsmanager.h" #include #include #include @@ -22,6 +15,15 @@ #include #include +#include "audiomanager.h" +#include "database.h" +#include "datamanagerlogging.h" +#include "entry.h" +#include "feed.h" +#include "fetcher.h" +#include "settingsmanager.h" +#include "storagemanager.h" + DataManager::DataManager() { connect( @@ -292,7 +294,7 @@ void DataManager::removeFeed(const int index) if (getEntry(id)->hasEnclosure()) getEntry(id)->enclosure()->deleteFile(); // delete enclosure (if it exists) if (!getEntry(id)->image().isEmpty()) - Fetcher::instance().removeImage(getEntry(id)->image()); // delete entry images + StorageManager::instance().removeImage(getEntry(id)->image()); // delete entry images delete m_entries[id]; // delete pointer m_entries.remove(id); // delete the hash key } @@ -300,7 +302,7 @@ void DataManager::removeFeed(const int index) qCDebug(kastsDataManager) << "Remove feed image" << feed->image() << "for feed" << feedurl; if (!feed->image().isEmpty()) - Fetcher::instance().removeImage(feed->image()); + StorageManager::instance().removeImage(feed->image()); m_feeds.remove(m_feedmap[index]); // remove from m_feeds m_feedmap.removeAt(index); // remove from m_feedmap delete feed; // remove the pointer diff --git a/src/enclosure.cpp b/src/enclosure.cpp index 5a3530f9..d771d1f7 100644 --- a/src/enclosure.cpp +++ b/src/enclosure.cpp @@ -23,6 +23,7 @@ #include "errorlogmodel.h" #include "fetcher.h" #include "settingsmanager.h" +#include "storagemanager.h" Enclosure::Enclosure(Entry *entry) : QObject(entry) @@ -216,7 +217,7 @@ void Enclosure::deleteFile() QString Enclosure::path() const { - return Fetcher::instance().enclosurePath(m_url); + return StorageManager::instance().enclosurePath(m_url); } Enclosure::Status Enclosure::status() const diff --git a/src/error.cpp b/src/error.cpp index d333acb0..afb4b833 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -54,6 +54,8 @@ QString Error::description() const return i18n("Invalid Media File"); case Error::Type::DiscoverError: return i18n("Nothing Found"); + case Error::Type::StorageMoveError: + return i18n("Error moving storage path"); default: return QString(); } @@ -72,6 +74,8 @@ int Error::typeToDb(Error::Type type) return 3; case Error::Type::DiscoverError: return 4; + case Error::Type::StorageMoveError: + return 5; default: return -1; } @@ -90,6 +94,8 @@ Error::Type Error::dbToType(int value) return Error::Type::InvalidMedia; case 4: return Error::Type::DiscoverError; + case 5: + return Error::Type::StorageMoveError; default: return Error::Type::Unknown; } diff --git a/src/error.h b/src/error.h index 4f1553d5..13f283d2 100644 --- a/src/error.h +++ b/src/error.h @@ -22,6 +22,7 @@ public: MeteredNotAllowed, InvalidMedia, DiscoverError, + StorageMoveError, }; Q_ENUM(Type) diff --git a/src/errorlogmodel.cpp b/src/errorlogmodel.cpp index 7eb6fd5f..e4b4e0b2 100644 --- a/src/errorlogmodel.cpp +++ b/src/errorlogmodel.cpp @@ -11,11 +11,13 @@ #include "database.h" #include "datamanager.h" #include "fetcher.h" +#include "storagemanager.h" ErrorLogModel::ErrorLogModel() : QAbstractListModel(nullptr) { connect(&Fetcher::instance(), &Fetcher::error, this, &ErrorLogModel::monitorErrorMessages); + connect(&StorageManager::instance(), &StorageManager::error, this, &ErrorLogModel::monitorErrorMessages); QSqlQuery query; query.prepare(QStringLiteral("SELECT * FROM Errors ORDER BY date DESC;")); diff --git a/src/fetcher.cpp b/src/fetcher.cpp index 730732a4..a00e28fa 100644 --- a/src/fetcher.cpp +++ b/src/fetcher.cpp @@ -5,8 +5,9 @@ * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ +#include "fetcher.h" + #include -#include #include #include #include @@ -16,17 +17,16 @@ #include #include #include -#include #include #include #include "database.h" #include "enclosure.h" -#include "fetcher.h" #include "fetcherlogging.h" #include "kasts-version.h" #include "settingsmanager.h" +#include "storagemanager.h" Fetcher::Fetcher() { @@ -367,10 +367,10 @@ QString Fetcher::image(const QString &url) const } // if image is already cached, then return the path - QString path = imagePath(url); + QString path = StorageManager::instance().imagePath(url); if (QFileInfo::exists(path)) { if (QFileInfo(path).size() != 0) { - return QStringLiteral("file://") + path; + return QUrl::fromLocalFile(path).toString(); } } @@ -450,28 +450,6 @@ QNetworkReply *Fetcher::download(const QString &url, const QString &filePath) co return reply; } -void Fetcher::removeImage(const QString &url) -{ - qCDebug(kastsFetcher) << "Removing image" << imagePath(url); - QFile(imagePath(url)).remove(); -} - -QString Fetcher::imagePath(const QString &url) const -{ - QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/images/"); - // Create path in cache if it doesn't exist yet - QFileInfo().absoluteDir().mkpath(path); - return path + QString::fromStdString(QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5).toHex().toStdString()); -} - -QString Fetcher::enclosurePath(const QString &url) const -{ - QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QStringLiteral("/enclosures/"); - // Create path in cache if it doesn't exist yet - QFileInfo().absoluteDir().mkpath(path); - return path + QString::fromStdString(QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5).toHex().toStdString()); -} - QNetworkReply *Fetcher::get(QNetworkRequest &request) const { setHeader(request); diff --git a/src/fetcher.h b/src/fetcher.h index 6e60a3e6..43e4300e 100644 --- a/src/fetcher.h +++ b/src/fetcher.h @@ -39,11 +39,8 @@ public: Q_INVOKABLE void fetch(const QStringList &urls); Q_INVOKABLE void fetchAll(); Q_INVOKABLE QString image(const QString &url) const; - void removeImage(const QString &url); Q_INVOKABLE QNetworkReply *download(const QString &url, const QString &fileName) const; - QString imagePath(const QString &url) const; - QString enclosurePath(const QString &url) const; QNetworkReply *get(QNetworkRequest &request) const; // Network status related methods diff --git a/src/main.cpp b/src/main.cpp index fbfbf429..bb3fa117 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -48,6 +48,7 @@ #include "podcastsearchmodel.h" #include "queuemodel.h" #include "settingsmanager.h" +#include "storagemanager.h" #ifdef Q_OS_ANDROID Q_DECL_EXPORT @@ -133,6 +134,7 @@ int main(int argc, char *argv[]) qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "DownloadModel", &DownloadModel::instance()); qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "ErrorLogModel", &ErrorLogModel::instance()); qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "AudioManager", &AudioManager::instance()); + qmlRegisterSingletonInstance("org.kde.kasts", 1, 0, "StorageManager", &StorageManager::instance()); qRegisterMetaType("const Entry*"); // "hack" to make qml understand Entry* qRegisterMetaType("const Feed*"); // "hack" to make qml understand Feed* diff --git a/src/mpris2/mediaplayer2player.cpp b/src/mpris2/mediaplayer2player.cpp index 8c9839c7..b62a602d 100644 --- a/src/mpris2/mediaplayer2player.cpp +++ b/src/mpris2/mediaplayer2player.cpp @@ -11,7 +11,9 @@ #include "audiomanager.h" #include "datamanager.h" -#include "fetcher.h" +#include "entry.h" +#include "feed.h" +#include "storagemanager.h" #include #include @@ -388,7 +390,7 @@ QVariantMap MediaPlayer2Player::getMetadataOfCurrentTrack() result[QStringLiteral("xesam:artist")] = authors; } if (!entry->image().isEmpty()) { - result[QStringLiteral("mpris:artUrl")] = Fetcher::instance().imagePath(entry->image()); + result[QStringLiteral("mpris:artUrl")] = StorageManager::instance().imagePath(entry->image()); } return result; diff --git a/src/mpris2/mediaplayer2player.h b/src/mpris2/mediaplayer2player.h index 209f84e8..91153619 100644 --- a/src/mpris2/mediaplayer2player.h +++ b/src/mpris2/mediaplayer2player.h @@ -14,6 +14,7 @@ class AudioManager; class Entry; +class Feed; class MediaPlayer2Player : public QDBusAbstractAdaptor { diff --git a/src/qml/SettingsPage.qml b/src/qml/SettingsPage.qml index ef5bb26b..7bc6bf29 100644 --- a/src/qml/SettingsPage.qml +++ b/src/qml/SettingsPage.qml @@ -5,8 +5,9 @@ * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ -import QtQuick 2.14 -import QtQuick.Controls 2.14 as Controls +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls +import Qt.labs.platform 1.1 import QtQuick.Layouts 1.14 import org.kde.kirigami 2.12 as Kirigami @@ -153,6 +154,68 @@ Kirigami.ScrollablePage { onToggled: SettingsManager.articleFontUseSystem = checked } + Kirigami.Heading { + Kirigami.FormData.isSection: true + text: i18n("Storage") + } + + RowLayout { + visible: Qt.platform.os !== "android" // not functional on android + Kirigami.FormData.label: i18n("Storage path:") + + Layout.fillWidth: true + Controls.TextField { + Layout.fillWidth: true + readOnly: true + text: StorageManager.storagePath + enabled: !defaultStoragePath.checked + } + Controls.Button { + icon.name: "document-open-folder" + text: i18n("Select folder...") + enabled: !defaultStoragePath.checked + onClicked: storagePathDialog.open() + } + FolderDialog { + id: storagePathDialog + title: i18n("Select Storage Path") + currentFolder: "file://" + StorageManager.storagePath + options: FolderDialog.ShowDirsOnly + onAccepted: { + StorageManager.setStoragePath(folder); + } + } + } + + Controls.CheckBox { + id: defaultStoragePath + visible: Qt.platform.os !== "android" // not functional on android + checked: SettingsManager.storagePath == "" + text: i18n("Use default path") + onToggled: { + if (checked) { + StorageManager.setStoragePath(""); + } + } + } + + Controls.Label { + Kirigami.FormData.label: i18n("Podcast Downloads:") + text: i18nc("Using of disk space", "Using %1 of disk space", StorageManager.formattedEnclosureDirSize) + } + + RowLayout { + Kirigami.FormData.label: i18n("Image Cache:") + Controls.Label { + text: i18nc("Using of disk space", "Using %1 of disk space", StorageManager.formattedImageDirSize) + } + Controls.Button { + icon.name: "edit-clear-all" + text: i18n("Clear Cache") + onClicked: StorageManager.clearImageCache(); + } + } + Kirigami.Heading { Kirigami.FormData.isSection: true text: i18n("Errors") diff --git a/src/qml/UpdateNotification.qml b/src/qml/UpdateNotification.qml index 48601f26..55a9ac78 100644 --- a/src/qml/UpdateNotification.qml +++ b/src/qml/UpdateNotification.qml @@ -20,10 +20,21 @@ import org.kde.kasts 1.0 Rectangle { id: rootComponent + required property string text + property bool showAbortButton: false + + z: 2 + + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: bottomMessageSpacing + ( errorNotification.visible ? errorNotification.height : 0 ) + } + color: Kirigami.Theme.activeTextColor - width: (labelWidth.boundingRect.width - labelWidth.boundingRect.x) + 3 * Kirigami.Units.largeSpacing + - indicator.width + width: feedUpdateCountLabel.width + 3 * Kirigami.Units.largeSpacing + + indicator.width + (showAbortButton ? abortButton.implicitWidth + Kirigami.Units.largeSpacing : 0) height: indicator.height visible: opacity > 0 @@ -60,27 +71,25 @@ Rectangle { Controls.Label { id: feedUpdateCountLabel - text: i18ncp("Number of Updated Podcasts", - "Updated %2 of %1 Podcast", - "Updated %2 of %1 Podcasts", - Fetcher.updateTotal, - Fetcher.updateProgress) + text: rootComponent.text color: Kirigami.Theme.textColor Layout.fillWidth: true - //Layout.fillHeight: true Layout.alignment: Qt.AlignVCenter } - } - TextMetrics { - id: labelWidth - - text: i18ncp("Number of Updated Podcasts", - "Updated %2 of %1 Podcast", - "Updated %2 of %1 Podcasts", - 999, - 999) + Controls.Button { + id: abortButton + Layout.alignment: Qt.AlignVCenter + Layout.rightMargin: Kirigami.Units.largeSpacing + visible: showAbortButton + Controls.ToolTip.visible: hovered + Controls.ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval + Controls.ToolTip.text: i18n("Abort") + text: i18n("Abort") + icon.name: "edit-delete-remove" + onClicked: abortAction(); + } } Timer { @@ -102,15 +111,17 @@ Rectangle { } } - Connections { - target: Fetcher - function onUpdatingChanged() { - if (Fetcher.updating) { - hideTimer.stop() - opacity = 1 - } else { - hideTimer.start() - } - } + function open() { + hideTimer.stop(); + opacity = 1; } + + function close() { + hideTimer.start(); + } + + // if the abort button is enabled (showAbortButton = true), this function + // needs to be implemented/overriden to call the correct underlying + // method/function + function abortAction() {} } diff --git a/src/qml/main.qml b/src/qml/main.qml index 92c27a82..89683fbf 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -239,13 +239,47 @@ Kirigami.ApplicationWindow { // It mimicks the behaviour of an InlineMessage, because InlineMessage does // not allow to add a BusyIndicator UpdateNotification { - z: 2 id: updateNotification + text: i18ncp("Number of Updated Podcasts", + "Updated %2 of %1 Podcast", + "Updated %2 of %1 Podcasts", + Fetcher.updateTotal, + Fetcher.updateProgress) - anchors { - horizontalCenter: parent.horizontalCenter - bottom: parent.bottom - bottomMargin: bottomMessageSpacing + ( errorNotification.visible ? errorNotification.height + Kirigami.Units.largeSpacing : 0 ) + Connections { + target: Fetcher + function onUpdatingChanged() { + if (Fetcher.updating) { + updateNotification.open() + } else { + updateNotification.close() + } + } + } + } + + // Notification to show progress of copying enclosure and images to new location + UpdateNotification { + id: moveStorageNotification + text: i18ncp("Number of Moved Files", + "Moved %2 of %1 File", + "Moved %2 of %1 Files", + StorageManager.storageMoveTotal, + StorageManager.storageMoveProgress) + showAbortButton: true + + function abortAction() { + StorageManager.cancelStorageMove(); + } + + Connections { + target: StorageManager + function onStorageMoveStarted() { + moveStorageNotification.open() + } + function onStorageMoveFinished() { + moveStorageNotification.close() + } } } diff --git a/src/settingsmanager.kcfg b/src/settingsmanager.kcfg index 49a2873d..b3826e28 100644 --- a/src/settingsmanager.kcfg +++ b/src/settingsmanager.kcfg @@ -56,6 +56,10 @@ true + + + + diff --git a/src/storagemanager.cpp b/src/storagemanager.cpp new file mode 100644 index 00000000..b0ecd7e0 --- /dev/null +++ b/src/storagemanager.cpp @@ -0,0 +1,174 @@ +/** + * SPDX-FileCopyrightText: 2021 Bart De Vries + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "storagemanager.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "enclosure.h" +#include "settingsmanager.h" +#include "storagemanagerlogging.h" +#include "storagemovejob.h" + +StorageManager::StorageManager() +{ +} + +QString StorageManager::storagePath() const +{ + QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + + if (!SettingsManager::self()->storagePath().isEmpty()) { + path = SettingsManager::self()->storagePath().toLocalFile(); + } + + // Create path if it doesn't exist yet + QFileInfo().absoluteDir().mkpath(path); + + qCDebug(kastsStorageManager) << "Current storage path is" << path; + + return path; +} + +void StorageManager::setStoragePath(QUrl url) +{ + qCDebug(kastsStorageManager) << "New storage path url:" << url; + QUrl oldUrl = SettingsManager::self()->storagePath(); + QString oldPath = storagePath(); + QString newPath = oldPath; + + if (url.isEmpty()) { + qCDebug(kastsStorageManager) << "(Re)set storage path to default location"; + SettingsManager::self()->setStoragePath(url); + newPath = storagePath(); // retrieve default storage path, since url is empty + } else if (url.isLocalFile()) { + SettingsManager::self()->setStoragePath(url); + newPath = url.toLocalFile(); + } else { + qCDebug(kastsStorageManager) << "Cannot set storage path; path is not on local filesystem:" << url; + return; + } + + qCDebug(kastsStorageManager) << "Current storage path in settings:" << SettingsManager::self()->storagePath(); + qCDebug(kastsStorageManager) << "New storage path will be:" << newPath; + + if (oldPath != newPath) { + QStringList list = {QStringLiteral("enclosures"), QStringLiteral("images")}; + StorageMoveJob *moveJob = new StorageMoveJob(oldPath, newPath, list); + connect(moveJob, &KJob::processedAmountChanged, this, [this, moveJob]() { + m_storageMoveProgress = moveJob->processedAmount(KJob::Files); + Q_EMIT storageMoveProgressChanged(m_storageMoveProgress); + }); + connect(moveJob, &KJob::totalAmountChanged, this, [this, moveJob]() { + m_storageMoveTotal = moveJob->totalAmount(KJob::Files); + Q_EMIT storageMoveTotalChanged(m_storageMoveTotal); + }); + connect(moveJob, &KJob::result, this, [=]() { + if (moveJob->error() != 0) { + // Go back to previous old path + SettingsManager::self()->setStoragePath(oldUrl); + QString title = + i18n("Old location:") + QStringLiteral(" ") + oldPath + QStringLiteral("; ") + i18n("New location:") + QStringLiteral(" ") + newPath; + Q_EMIT error(Error::Type::StorageMoveError, QString(), QString(), moveJob->error(), moveJob->errorString(), title); + } + Q_EMIT storageMoveFinished(); + Q_EMIT storagePathChanged(newPath); + + // save settings now to avoid getting into an inconsistent app state + SettingsManager::self()->save(); + disconnect(this, &StorageManager::cancelStorageMove, this, nullptr); + }); + connect(this, &StorageManager::cancelStorageMove, this, [this, moveJob]() { + moveJob->doKill(); + }); + Q_EMIT storageMoveStarted(); + moveJob->start(); + } +} + +QString StorageManager::imageDirPath() const +{ + QString path = storagePath() + QStringLiteral("/images/"); + // Create path if it doesn't exist yet + QFileInfo().absoluteDir().mkpath(path); + return path; +} + +QString StorageManager::imagePath(const QString &url) const +{ + return imageDirPath() + QString::fromStdString(QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5).toHex().toStdString()); +} + +QString StorageManager::enclosureDirPath() const +{ + QString path = storagePath() + QStringLiteral("/enclosures/"); + // Create path if it doesn't exist yet + QFileInfo().absoluteDir().mkpath(path); + return path; +} + +QString StorageManager::enclosurePath(const QString &url) const +{ + return enclosureDirPath() + QString::fromStdString(QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5).toHex().toStdString()); +} + +qint64 StorageManager::dirSize(const QString &path) const +{ + qint64 size = 0; + QFileInfoList files = QDir(path).entryInfoList(QDir::Files); + + for (QFileInfo info : files) { + size += info.size(); + } + + return size; +} + +void StorageManager::removeImage(const QString &url) +{ + qCDebug(kastsStorageManager) << "Removing image" << imagePath(url); + QFile(imagePath(url)).remove(); + Q_EMIT imageDirSizeChanged(); +} + +void StorageManager::clearImageCache() +{ + qDebug() << imageDirPath(); + QStringList images = QDir(imageDirPath()).entryList(QDir::Files); + qDebug() << images; + for (QString image : images) { + qDebug() << image; + QFile(QDir(imageDirPath()).absoluteFilePath(image)).remove(); + } + Q_EMIT imageDirSizeChanged(); +} + +qint64 StorageManager::enclosureDirSize() const +{ + return dirSize(enclosureDirPath()); +} + +qint64 StorageManager::imageDirSize() const +{ + return dirSize(imageDirPath()); +} + +QString StorageManager::formattedEnclosureDirSize() const +{ + return m_kformat.formatByteSize(enclosureDirSize()); +} + +QString StorageManager::formattedImageDirSize() const +{ + return m_kformat.formatByteSize(imageDirSize()); +} diff --git a/src/storagemanager.h b/src/storagemanager.h new file mode 100644 index 00000000..c6880982 --- /dev/null +++ b/src/storagemanager.h @@ -0,0 +1,75 @@ +/** + * SPDX-FileCopyrightText: 2021 Bart De Vries + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include +#include + +#include + +#include "error.h" + +class StorageManager : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int storageMoveProgress MEMBER m_storageMoveProgress NOTIFY storageMoveProgressChanged) + Q_PROPERTY(int storageMoveTotal MEMBER m_storageMoveTotal NOTIFY storageMoveTotalChanged) + Q_PROPERTY(QString storagePath READ storagePath NOTIFY storagePathChanged) + Q_PROPERTY(qint64 enclosureDirSize READ enclosureDirSize NOTIFY enclosureDirSizeChanged) + Q_PROPERTY(qint64 imageDirSize READ imageDirSize NOTIFY imageDirSizeChanged) + Q_PROPERTY(QString formattedEnclosureDirSize READ formattedEnclosureDirSize NOTIFY enclosureDirSizeChanged) + Q_PROPERTY(QString formattedImageDirSize READ formattedImageDirSize NOTIFY imageDirSizeChanged) + +public: + static StorageManager &instance() + { + static StorageManager _instance; + return _instance; + } + + QString storagePath() const; + Q_INVOKABLE void setStoragePath(QUrl url); + + QString imageDirPath() const; + QString imagePath(const QString &url) const; + + QString enclosureDirPath() const; + QString enclosurePath(const QString &url) const; + + qint64 enclosureDirSize() const; + qint64 imageDirSize() const; + QString formattedEnclosureDirSize() const; + QString formattedImageDirSize() const; + + void removeImage(const QString &url); + Q_INVOKABLE void clearImageCache(); + +Q_SIGNALS: + void error(Error::Type type, const QString &url, const QString &id, const int errorId, const QString &errorString, const QString &title); + + void storagePathChanged(QString path); + void storageMoveStarted(); + void storageMoveFinished(); + void storageMoveProgressChanged(int progress); + void storageMoveTotalChanged(int nrOfFeeds); + void cancelStorageMove(); + + void enclosureDirSizeChanged(); + void imageDirSizeChanged(); + +private: + StorageManager(); + + qint64 dirSize(const QString &path) const; + + int m_storageMoveProgress; + int m_storageMoveTotal; + KFormat m_kformat; +}; diff --git a/src/storagemovejob.cpp b/src/storagemovejob.cpp new file mode 100644 index 00000000..1e76be52 --- /dev/null +++ b/src/storagemovejob.cpp @@ -0,0 +1,115 @@ +/** + * SPDX-FileCopyrightText: 2021 Bart De Vries + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include +#include +#include + +#include + +#include "storagemovejob.h" +#include "storagemovejoblogging.h" + +StorageMoveJob::StorageMoveJob(const QString &from, const QString &to, QStringList &list, QObject *parent) + : KJob(parent) + , m_from(from) + , m_to(to) + , m_list(list) +{ +} + +void StorageMoveJob::start() +{ + QTimer::singleShot(0, this, &StorageMoveJob::moveFiles); +} + +void StorageMoveJob::moveFiles() +{ + qCDebug(kastsStorageMoveJob) << "Begin moving" << m_list << "from" << m_from << "to" << m_to; + + bool success = true; + + QStringList fileList; // this list will contain all files that need to be moved + + for (QString item : m_list) { + // make a list of files to be moved; path is relative to m_from + if (QFileInfo(m_from + QStringLiteral("/") + item).isDir()) { + // this item is a dir; now add all files in that subdir + QStringList tempList = QDir(m_from + QStringLiteral("/") + item + QStringLiteral("/")).entryList(QDir::Files); + for (QString file : tempList) { + fileList += item + QStringLiteral("/") + file; + } + + // if the item is a subdir, let's try to create it in the new location + // if this fails, then the destination is not writeable, and the move + // should be aborted + success = QFileInfo().absoluteDir().mkpath(m_to + QStringLiteral("/") + item) && success; + } else if (QFileInfo(m_from + QStringLiteral("/") + item).isFile()) { + // this item is a file; simply add it to the list + fileList += item; + } + } + + if (!success) { + setError(2); + setErrorText(i18n("Destination path not writable")); + emitResult(); + return; + } + + setTotalAmount(Files, fileList.size()); + setProcessedAmount(Files, 0); + + for (int i = 0; i < fileList.size(); i++) { + // First check if we need to abort this job + if (m_abort) { + // Remove files that were already copied + for (int j = 0; j < i; j++) { + qCDebug(kastsStorageMoveJob) << "Removing file" << QDir(m_to).absoluteFilePath(fileList[j]); + QFile(QDir(m_to).absoluteFilePath(fileList[j])).remove(); + } + setError(1); + setErrorText(i18n("Operation aborted by user")); + emitResult(); + return; + } + + // Now we can start copying + QString fromPath = QDir(m_from).absoluteFilePath(fileList[i]); + QString toPath = QDir(m_to).absoluteFilePath(fileList[i]); + if (QFileInfo::exists(toPath) && (QFileInfo(fromPath).size() == QFileInfo(toPath).size())) { + qCDebug(kastsStorageMoveJob) << "Identical file already exists in destination; skipping" << toPath; + } else { + qCDebug(kastsStorageMoveJob) << "Copy" << fromPath << "to" << toPath; + success = QFile(fromPath).copy(toPath) && success; + } + if (!success) + break; + setProcessedAmount(Files, i + 1); + } + + if (m_abort) { + setError(1); + setErrorText(i18n("Operation aborted by user")); + } else if (success) { + // now it's safe to delete all the files from the original location + for (QString file : fileList) { + QFile(QDir(m_from).absoluteFilePath(file)).remove(); + qCDebug(kastsStorageMoveJob) << "Removing file" << QDir(m_from).absoluteFilePath(file); + } + } else { + setError(2); + setErrorText(i18n("An error occured while copying data")); + } + + emitResult(); +} + +bool StorageMoveJob::doKill() +{ + m_abort = true; + return true; +} diff --git a/src/storagemovejob.h b/src/storagemovejob.h new file mode 100644 index 00000000..a6d07d94 --- /dev/null +++ b/src/storagemovejob.h @@ -0,0 +1,26 @@ +/** + * SPDX-FileCopyrightText: 2021 Bart De Vries + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include + +class StorageMoveJob : public KJob +{ +public: + explicit StorageMoveJob(const QString &from, const QString &to, QStringList &list, QObject *parent = nullptr); + + void start() override; + bool doKill() override; + +private: + void moveFiles(); + + QString m_from; + QString m_to; + QStringList m_list; + bool m_abort = false; +};