mirror of https://github.com/KDE/kasts.git
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:
parent
85798ebd8c
commit
a15e2dbe5d
|
@ -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)
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ public:
|
|||
MeteredNotAllowed,
|
||||
InvalidMedia,
|
||||
DiscoverError,
|
||||
StorageMoveError,
|
||||
};
|
||||
Q_ENUM(Type)
|
||||
|
||||
|
|
|
@ -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;"));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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*
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
class AudioManager;
|
||||
class Entry;
|
||||
class Feed;
|
||||
|
||||
class MediaPlayer2Player : public QDBusAbstractAdaptor
|
||||
{
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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() {}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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());
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
};
|
Loading…
Reference in New Issue