Make storage path configurable

This adds a new setting to the Settings page.
Existing enclosures and images will be moved to the new location
(first copied, then deleted in the original location).  If any of
the copy actions fail, the operation is aborted and the original
path is restored.
The StorageMoveJob is set up in such a way that it's easy to add other
files or subfolders in the future.

Solves #15
This commit is contained in:
Bart De Vries 2021-07-04 18:35:09 +02:00
parent 85798ebd8c
commit a15e2dbe5d
20 changed files with 587 additions and 77 deletions

View File

@ -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)

View File

@ -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());

View File

@ -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 <QDateTime>
#include <QDir>
#include <QSqlDatabase>
@ -22,6 +15,15 @@
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#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

View File

@ -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

View File

@ -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;
}

View File

@ -22,6 +22,7 @@ public:
MeteredNotAllowed,
InvalidMedia,
DiscoverError,
StorageMoveError,
};
Q_ENUM(Type)

View File

@ -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;"));

View File

@ -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 <KLocalizedString>
#include <QCryptographicHash>
#include <QDateTime>
#include <QDebug>
#include <QDir>
@ -16,17 +17,16 @@
#include <QMultiMap>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QStandardPaths>
#include <QTextDocumentFragment>
#include <Syndication/Syndication>
#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);

View File

@ -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

View File

@ -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<Entry *>("const Entry*"); // "hack" to make qml understand Entry*
qRegisterMetaType<Feed *>("const Feed*"); // "hack" to make qml understand Feed*

View File

@ -11,7 +11,9 @@
#include "audiomanager.h"
#include "datamanager.h"
#include "fetcher.h"
#include "entry.h"
#include "feed.h"
#include "storagemanager.h"
#include <QCryptographicHash>
#include <QDBusConnection>
@ -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;

View File

@ -14,6 +14,7 @@
class AudioManager;
class Entry;
class Feed;
class MediaPlayer2Player : public QDBusAbstractAdaptor
{

View File

@ -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 <amount of bytes> of disk space", "Using %1 of disk space", StorageManager.formattedEnclosureDirSize)
}
RowLayout {
Kirigami.FormData.label: i18n("Image Cache:")
Controls.Label {
text: i18nc("Using <amount of bytes> 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")

View File

@ -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() {}
}

View File

@ -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()
}
}
}

View File

@ -56,6 +56,10 @@
<label>Use default system font</label>
<default>true</default>
</entry>
<entry name="StoragePath" type="Url">
<label>Custom path to store enclosures and images</label>
<default></default>
</entry>
</group>
<group name="Network">
<entry name="allowMeteredFeedUpdates" type="Bool">

174
src/storagemanager.cpp Normal file
View File

@ -0,0 +1,174 @@
/**
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "storagemanager.h"
#include <KLocalizedString>
#include <QCryptographicHash>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QStandardPaths>
#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());
}

75
src/storagemanager.h Normal file
View File

@ -0,0 +1,75 @@
/**
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#pragma once
#include <QFile>
#include <QObject>
#include <QString>
#include <QUrl>
#include <KFormat>
#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;
};

115
src/storagemovejob.cpp Normal file
View File

@ -0,0 +1,115 @@
/**
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include <QDir>
#include <QFile>
#include <QTimer>
#include <KLocalizedString>
#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;
}

26
src/storagemovejob.h Normal file
View File

@ -0,0 +1,26 @@
/**
* SPDX-FileCopyrightText: 2021 Bart De Vries <bart@mogwai.be>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#pragma once
#include <KJob>
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;
};