Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
Digia Qt LGPL Exception version 1.1
As an additional permission to the GNU Lesser General Public License version
2.1, the object code form of a "work that uses the Library" may incorporate
material from a header file that is part of the Library. You may distribute
such object code under terms of your choice, provided that:
(i) the header files of the Library have not been modified; and
(ii) the incorporated material is limited to numerical parameters, data
structure layouts, accessors, macros, inline functions and
templates; and
(iii) you comply with the terms of Section 6 of the GNU Lesser General
Public License version 2.1.
Moreover, you may apply this exception to a modified version of the Library,
provided that such modification does not involve copying material from the
Library into the modified Library's header files unless such material is
limited to (i) numerical parameters; (ii) data structure layouts;
(iii) accessors; and (iv) small macros, templates and inline functions of
five lines or less in length.
Furthermore, you are not required to apply this additional permission to a
modified version of the Library.
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2011 Morgan Leborgne
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,140 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2011 Morgan Leborgne
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "QProgressIndicator.h"
#include <QPainter>
QProgressIndicator::QProgressIndicator(QWidget* parent)
: QWidget(parent),
m_angle(0),
m_timerId(-1),
m_delay(40),
m_displayedWhenStopped(false),
m_color(Qt::black)
{
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
setFocusPolicy(Qt::NoFocus);
}
bool QProgressIndicator::isAnimated () const
{
return (m_timerId != -1);
}
void QProgressIndicator::setDisplayedWhenStopped(bool state)
{
m_displayedWhenStopped = state;
update();
}
bool QProgressIndicator::isDisplayedWhenStopped() const
{
return m_displayedWhenStopped;
}
void QProgressIndicator::startAnimation()
{
m_angle = 0;
if (m_timerId == -1)
m_timerId = startTimer(m_delay);
}
void QProgressIndicator::stopAnimation()
{
if (m_timerId != -1)
killTimer(m_timerId);
m_timerId = -1;
update();
}
void QProgressIndicator::setAnimationDelay(int delay)
{
if (m_timerId != -1)
killTimer(m_timerId);
m_delay = delay;
if (m_timerId != -1)
m_timerId = startTimer(m_delay);
}
void QProgressIndicator::setColor(const QColor & color)
{
m_color = color;
update();
}
QSize QProgressIndicator::sizeHint() const
{
return QSize(20,20);
}
int QProgressIndicator::heightForWidth(int w) const
{
return w;
}
void QProgressIndicator::timerEvent(QTimerEvent * /*event*/)
{
m_angle = (m_angle+30)%360;
update();
}
void QProgressIndicator::paintEvent(QPaintEvent * /*event*/)
{
if (!m_displayedWhenStopped && !isAnimated())
return;
int width = qMin(this->width(), this->height());
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
int outerRadius = (width-1)*0.5;
int innerRadius = (width-1)*0.5*0.38;
int capsuleHeight = outerRadius - innerRadius;
int capsuleWidth = (width > 32 ) ? capsuleHeight *.23 : capsuleHeight *.35;
int capsuleRadius = capsuleWidth/2;
for (int i=0; i<12; i++)
{
QColor color = m_color;
color.setAlphaF(1.0f - (i/12.0f));
p.setPen(Qt::NoPen);
p.setBrush(color);
p.save();
p.translate(rect().center());
p.rotate(m_angle - i*30.0f);
p.drawRoundedRect(-capsuleWidth*0.5, -(innerRadius+capsuleHeight), capsuleWidth, capsuleHeight, capsuleRadius, capsuleRadius);
p.restore();
}
}
@@ -0,0 +1,111 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2011 Morgan Leborgne
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef QPROGRESSINDICATOR_H
#define QPROGRESSINDICATOR_H
#include <QWidget>
#include <QColor>
/*!
\class QProgressIndicator
\brief The QProgressIndicator class lets an application display a progress indicator to show that a lengthy task is under way.
Progress indicators are indeterminate and do nothing more than spin to show that the application is busy.
\sa QProgressBar
*/
class QProgressIndicator : public QWidget
{
Q_OBJECT
Q_PROPERTY(int delay READ animationDelay WRITE setAnimationDelay)
Q_PROPERTY(bool displayedWhenStopped READ isDisplayedWhenStopped WRITE setDisplayedWhenStopped)
Q_PROPERTY(QColor color READ color WRITE setColor)
public:
QProgressIndicator(QWidget* parent = nullptr);
/*! Returns the delay between animation steps.
\return The number of milliseconds between animation steps. By default, the animation delay is set to 40 milliseconds.
\sa setAnimationDelay
*/
int animationDelay() const { return m_delay; }
/*! Returns a Boolean value indicating whether the component is currently animated.
\return Animation state.
\sa startAnimation stopAnimation
*/
bool isAnimated () const;
/*! Returns a Boolean value indicating whether the receiver shows itself even when it is not animating.
\return Return true if the progress indicator shows itself even when it is not animating. By default, it returns false.
\sa setDisplayedWhenStopped
*/
bool isDisplayedWhenStopped() const;
/*! Returns the color of the component.
\sa setColor
*/
const QColor & color() const { return m_color; }
QSize sizeHint() const override;
int heightForWidth(int w) const override;
public Q_SLOTS:
/*! Starts the spin animation.
\sa stopAnimation isAnimated
*/
void startAnimation();
/*! Stops the spin animation.
\sa startAnimation isAnimated
*/
void stopAnimation();
/*! Sets the delay between animation steps.
Setting the \a delay to a value larger than 40 slows the animation, while setting the \a delay to a smaller value speeds it up.
\param delay The delay, in milliseconds.
\sa animationDelay
*/
void setAnimationDelay(int delay);
/*! Sets whether the component hides itself when it is not animating.
\param state The animation state. Set false to hide the progress indicator when it is not animating; otherwise true.
\sa isDisplayedWhenStopped
*/
void setDisplayedWhenStopped(bool state);
/*! Sets the color of the components to the given color.
\sa color
*/
void setColor(const QColor & color);
protected:
void timerEvent(QTimerEvent * event) override;
void paintEvent(QPaintEvent * event) override;
private:
int m_angle;
int m_timerId;
int m_delay;
bool m_displayedWhenStopped;
QColor m_color;
};
#endif // QPROGRESSINDICATOR_H
+14
View File
@@ -0,0 +1,14 @@
## Description
The QProgressIndicator class lets an application display a progress indicator to show that a lengthy task is under way.
Will work at any size.
<img src="https://raw.github.com/mojocorp/QProgressIndicator/master/screen-capture-1.png" >
<img src="https://raw.github.com/mojocorp/QProgressIndicator/master/screen-capture-2.png" >
## Dependency
Qt 4.4.x.
## License
MIT
+56
View File
@@ -0,0 +1,56 @@
if(WITH_CRASHREPORTER)
find_package(CrashReporterQt REQUIRED)
endif()
find_package(Qt6Keychain 0.13 REQUIRED)
# TODO: Mingw64 7.3 might also need to be excluded here as it seems to not automatically link libssp
if(NOT WIN32)
if(NOT (CMAKE_SYSTEM_PROCESSOR MATCHES "^(alpha|parisc|hppa)") AND NOT CMAKE_CROSSCOMPILING)
if((CMAKE_CXX_COMPILER_ID MATCHES "GNU") AND (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9))
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector --param=ssp-buffer-size=4")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-protector --param=ssp-buffer-size=4")
else()
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector-strong")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-protector-strong")
endif()
endif()
string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER)
if(CMAKE_BUILD_TYPE_LOWER MATCHES "(release|relwithdebinfo|minsizerel)")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_FORTIFY_SOURCE=2")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_FORTIFY_SOURCE=2")
endif()
endif()
if(WIN32)
# Enable DEP & ASLR
if (MINGW)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--nxcompat -Wl,--dynamicbase")
endif()
elseif(UNIX AND NOT APPLE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,relro -Wl,-z,now")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,relro -Wl,-z,now")
endif()
add_subdirectory(resources)
add_subdirectory(libsync)
add_subdirectory(gui)
add_subdirectory(cmd)
if (WITH_CRASHREPORTER)
add_subdirectory(crashreporter)
endif()
add_subdirectory(plugins)
install(EXPORT ${APPLICATION_SHORTNAME}Config DESTINATION "${KDE_INSTALL_CMAKEPACKAGEDIR}/${APPLICATION_SHORTNAME}" NAMESPACE QSfera::)
ecm_setup_version(PROJECT
VARIABLE_PREFIX ${APPLICATION_SHORTNAME}
PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/${APPLICATION_SHORTNAME}ConfigVersion.cmake"
SOVERSION ${MIRALL_SOVERSION}
)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${APPLICATION_SHORTNAME}ConfigVersion.cmake" DESTINATION "${KDE_INSTALL_CMAKEPACKAGEDIR}/${APPLICATION_SHORTNAME}")
+21
View File
@@ -0,0 +1,21 @@
add_executable(cmd
cmd.cpp
tokencredentials.cpp
)
set_target_properties(cmd PROPERTIES OUTPUT_NAME "${APPLICATION_EXECUTABLE}cmd")
ecm_mark_nongui_executable(cmd)
target_link_libraries(cmd libsync Qt::Core Qt::Network ZLIB::ZLIB)
apply_common_target_settings(cmd)
if(APPLE)
set_target_properties(cmd PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<TARGET_FILE_DIR:qsfera>")
else()
install(TARGETS cmd ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
endif()
if(UNIX AND NOT APPLE)
configure_file(${CMAKE_SOURCE_DIR}/qsferacmd.desktop.in ${CMAKE_CURRENT_BINARY_DIR}/${APPLICATION_EXECUTABLE}cmd.desktop)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${APPLICATION_EXECUTABLE}cmd.desktop DESTINATION ${KDE_INSTALL_DATADIR}/applications)
endif()
+553
View File
@@ -0,0 +1,553 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@owncloud.com>
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
* Copyright (C) by Daniel Heule <daniel.heule@gmail.com>
*
* 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.
*/
#include "account.h"
#include "cmd/tokencredentials.h"
#include "common/syncjournaldb.h"
#include "common/version.h"
#include "configfile.h" // ONLY ACCESS THE STATIC FUNCTIONS!
#include "libsync/graphapi/spacesmanager.h"
#include "libsync/logger.h"
#include "libsync/theme.h"
#include "networkjobs/checkserverjobfactory.h"
#include "networkjobs/jsonjob.h"
#include "platform.h"
#include "syncengine.h"
#include <QJsonObject>
#include <QUrl>
#include <iostream>
#include <memory>
#include <random>
#include <zlib.h>
using namespace OCC;
namespace {
// start in quiet mode
bool logQuietMode = true;
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)
{
if (std::strcmp(context.category, "default")) {
if (logQuietMode) {
return;
}
std::cerr << qPrintable(qFormatLogMessage(type, context, message)) << std::endl;
} else {
switch (type) {
case QtInfoMsg:
std::cout << qPrintable(message) << std::endl;
return;
case QtDebugMsg:
// ignore debug message if we are in quiet mode
if (logQuietMode) {
return;
}
std::cerr << "Debug";
break;
case QtWarningMsg:
std::cerr << "Warning";
break;
case QtCriticalMsg:
std::cerr << "Critical";
break;
case QtFatalMsg:
std::cerr << "Fatal: " << qPrintable(message) << std::endl;
exit(EXIT_FAILURE);
}
std::cerr << ": " << qPrintable(message) << std::endl;
}
}
struct CmdOptions
{
QString source_dir;
QString space_id;
QUrl server_url;
QString remoteFolder;
QByteArray username;
QByteArray token = qgetenv("QSFERA_TOKEN");
bool query = false;
bool trustSSL = false;
bool interactive = true;
bool ignoreHiddenFiles = true;
QString exclude;
QString unsyncedfolders;
int restartTimes = 3;
int downlimit = 0;
int uplimit = 0;
};
struct SyncCTX
{
explicit SyncCTX(const CmdOptions &cmdOptions)
: options{cmdOptions}
, promptRemoveAllFiles(cmdOptions.interactive)
{
}
CmdOptions options;
bool promptRemoveAllFiles;
AccountPtr account;
};
/* If the selective sync list is different from before, we need to disable the read from db
(The normal client does it in SelectiveSyncDialog::accept*)
*/
void selectiveSyncFixup(OCC::SyncJournalDb *journal, const QSet<QString> &newListSet)
{
SqlDatabase db;
if (!db.openOrCreateReadWrite(journal->databaseFilePath())) {
return;
}
bool ok;
const auto oldBlackListSet = journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok);
if (ok) {
const auto changes = (oldBlackListSet - newListSet) + (newListSet - oldBlackListSet);
for (const auto &it : changes) {
journal->schedulePathForRemoteDiscovery(it);
}
journal->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, newListSet);
}
}
void sync(const SyncCTX &ctx, const QUrl &spaceUrl)
{
const auto selectiveSyncList = [&]() -> QSet<QString> {
if (!ctx.options.unsyncedfolders.isEmpty()) {
QFile f(ctx.options.unsyncedfolders);
if (!f.open(QFile::ReadOnly)) {
qCritical() << u"Could not open file containing the list of unsynced folders: " << ctx.options.unsyncedfolders;
} else {
// filter out empty lines and comments
auto selectiveSyncList = QString::fromUtf8(f.readAll())
.split(QLatin1Char('\n'))
.filter(QRegularExpression(QStringLiteral("\\S+")))
.filter(QRegularExpression(QStringLiteral("^[^#]")));
for (int i = 0; i < selectiveSyncList.count(); ++i) {
if (!selectiveSyncList.at(i).endsWith(QLatin1Char('/'))) {
selectiveSyncList[i].append(QLatin1Char('/'));
}
}
return {selectiveSyncList.cbegin(), selectiveSyncList.cend()};
}
}
return {};
}();
const QString dbPath = ctx.options.source_dir + SyncJournalDb::makeDbName(ctx.options.source_dir);
auto db = new SyncJournalDb(dbPath, qApp);
if (!selectiveSyncList.empty()) {
selectiveSyncFixup(db, selectiveSyncList);
}
SyncOptions opt{QSharedPointer<Vfs>(VfsPluginManager::instance().createVfsFromPlugin(Vfs::Mode::Off).release())};
auto engine = new SyncEngine(ctx.account, spaceUrl, ctx.options.source_dir, ctx.options.remoteFolder, db);
engine->setSyncOptions(opt);
engine->setParent(db);
QObject::connect(engine, &SyncEngine::finished, engine, [engine, ctx, restartCount = std::make_shared<int>(0)](bool result) {
if (!result) {
qWarning() << u"Failed to sync";
exit(EXIT_FAILURE);
} else {
if (engine->isAnotherSyncNeeded()) {
if (*restartCount < ctx.options.restartTimes) {
(*restartCount)++;
qDebug() << u"Restarting Sync, because another sync is needed" << *restartCount;
engine->startSync();
return;
}
qWarning() << u"Another sync is needed, but not done because restart count is exceeded" << *restartCount;
} else {
qInfo() << u"Sync succeeded";
qApp->quit();
}
}
});
QObject::connect(engine, &SyncEngine::syncError, engine, [](const QString &error) { qWarning() << u"Sync error:" << error; });
QObject::connect(engine, &SyncEngine::itemCompleted, engine, [](const SyncFileItemPtr &item) {
if (item->hasErrorStatus()) {
switch (item->_status) {
case SyncFileItem::Excluded:
qDebug() << u"Sync excluded file:" << item->_errorString;
break;
default:
qWarning() << u"Sync error:" << item->_status << item->_errorString;
}
}
});
engine->setIgnoreHiddenFiles(ctx.options.ignoreHiddenFiles);
engine->setNetworkLimits(ctx.options.uplimit, ctx.options.downlimit);
// Always try to load the user-provided exclude list if one is specified
if (!ctx.options.exclude.isEmpty()) {
engine->addExcludeList(ctx.options.exclude);
} else {
engine->addExcludeList(ConfigFile::defaultExcludeFile());
}
if (!engine->reloadExcludes()) {
qCritical() << u"Cannot load system exclude list or list supplied via --exclude";
exit(EXIT_FAILURE);
}
engine->startSync();
}
void setupCredentials(SyncCTX &ctx)
{
// Order of retrieval attempt (later attempts override earlier ones):
// 1. From URL
// 2. From options
// 3. From prompt (if interactive)
// Pre-flight check: verify that the file specified by --unsyncedfolders can be read by us:
if (!ctx.options.unsyncedfolders.isNull()) { // yes, isNull and not isEmpty because...:
// ... if the user entered "--unsyncedfolders ''" on the command-line, opening that will
// also fail
QFile f(ctx.options.unsyncedfolders);
if (!f.open(QFile::ReadOnly)) {
qCritical() << u"Cannot read unsyncedfolders file '" << ctx.options.unsyncedfolders << u"': " << f.errorString();
exit(EXIT_FAILURE);
}
f.close();
}
ctx.account->setCredentials(new TokenCredentials(std::move(ctx.options.username), std::move(ctx.options.token)));
if (ctx.options.trustSSL) {
QObject::connect(ctx.account->accessManager(), &QNetworkAccessManager::sslErrors, qApp,
[](QNetworkReply *reply, const QList<QSslError> &errors) { reply->ignoreSslErrors(errors); });
} else {
QObject::connect(ctx.account->accessManager(), &QNetworkAccessManager::sslErrors, qApp, [](QNetworkReply *reply, const QList<QSslError> &errors) {
Q_UNUSED(reply)
qCritical() << u"SSL error encountered";
for (const auto &e : errors) {
qCritical() << e.errorString();
}
qCritical() << u"If you trust the certificate and want to ignore the errors, use the --trust option.";
exit(EXIT_FAILURE);
});
}
}
QString hashSaceId(const QString &id)
{
auto adler = adler32_z(0, nullptr, 0);
adler = adler32_z(adler, reinterpret_cast<Bytef *>(id.toUtf8().data()), id.size());
return QStringLiteral("space:%1").arg(QString::number(adler, 16));
}
void printSpaces(const QVector<GraphApi::Space *> &spaces)
{
auto printTable = [](const QString &a, const QString &b, const QString &c) {
qInfo().noquote() << QStringLiteral("%1 | %2 | %3").arg(a, 15).arg(b, 20).arg(c);
};
qInfo() << u"Listing spaces:";
printTable(QStringLiteral("Short ID"), QStringLiteral("DisplayName"), QStringLiteral("ID"));
printTable(QString().fill(QLatin1Char('-'), 15), QString().fill(QLatin1Char('-'), 20), QString().fill(QLatin1Char('-'), 20));
for (auto *s : spaces) {
printTable(hashSaceId(s->id()), s->displayName(), s->id());
}
}
}
CmdOptions parseOptions(const QStringList &app_args)
{
CmdOptions options;
QCommandLineParser parser;
parser.setApplicationDescription(QStringLiteral("%1 version %2 - command line client tool").arg(QCoreApplication::instance()->applicationName(), OCC::Version::displayString()));
// this little snippet saves a few lines below
auto addOption = [&parser](QCommandLineOption &&option, std::optional<QCommandLineOption::Flag> &&flags = {}) {
if (flags.has_value()) {
option.setFlags(flags.value());
}
parser.addOption(option);
return option;
};
auto userOption = addOption({{QStringLiteral("u"), QStringLiteral("user")}, QStringLiteral("Username"), QStringLiteral("name")});
auto tokenOption = addOption(
{{QStringLiteral("t"), QStringLiteral("token")}, QStringLiteral("Authentication token, you can also use $QSFERA_TOKEN"), QStringLiteral("token")});
auto remoteFolder =
addOption({{QStringLiteral("remote-folder")}, QStringLiteral("The subdirectory of the space that is synchronized"), QStringLiteral("remote-folder")});
auto trustOption = addOption({ { QStringLiteral("trust") }, QStringLiteral("Trust the SSL certification") });
auto excludeOption = addOption({ { QStringLiteral("exclude") }, QStringLiteral("Path to an exclude list [file]"), QStringLiteral("file") });
auto unsyncedfoldersOption = addOption(
{{QStringLiteral("unsynced-folders")}, QStringLiteral("File containing the list of unsynced remote folders (selective sync)"), QStringLiteral("file")});
auto nonInterActiveOption = addOption({ { QStringLiteral("non-interactive") }, QStringLiteral("Do not block execution with interaction") });
auto maxRetriesOption = addOption({{QStringLiteral("max-sync-retries")}, QStringLiteral("Retries maximum n times (defaults to 3)"), QStringLiteral("n")});
auto uploadLimitOption = addOption({{QStringLiteral("upload-limit")}, QStringLiteral("Limit the upload speed of files to n KB/s"), QStringLiteral("n")});
auto downloadLimitption =
addOption({{QStringLiteral("download-limit")}, QStringLiteral("Limit the download speed of files to n KB/s"), QStringLiteral("n")});
auto syncHiddenFilesOption = addOption({ { QStringLiteral("sync-hidden-files") }, QStringLiteral("Enables synchronization of hidden files") });
const auto testCrashReporter =
addOption({{QStringLiteral("crash")}, QStringLiteral("Crash the client to test the crash reporter")}, QCommandLineOption::HiddenFromHelp);
auto verbosityOption = addOption({{QStringLiteral("verbose")},
QStringLiteral("Specify the [verbosity]\n0: no logging (default)\n"
"1: general logging\n"
"2: all previous and http logs\n"
"3: all previous and debug information"),
QStringLiteral("verbosity"), QStringLiteral("0")});
parser.addHelpOption();
parser.addVersionOption();
parser.addPositionalArgument(
QStringLiteral("server_url"), QStringLiteral("The URL to the QSfera installation on the server. This is usually the root path"));
parser.addPositionalArgument(QStringLiteral("space_id"),
QStringLiteral("The id, name or short id of the space to synchronize, if no [space_id] is provided or the [space_id] did not match any space, a list "
"of spaces is printed."),
QStringLiteral("[space_id]"));
parser.addPositionalArgument(QStringLiteral("source_dir"), QStringLiteral("The source dir"), QStringLiteral("[source_dir]"));
parser.process(app_args);
const int verbosity = parser.value(verbosityOption).toInt();
if (verbosity >= 0 && verbosity <= 3) {
logQuietMode = verbosity == 0;
if (verbosity > 1) {
Logger::instance()->addLogRule({QStringLiteral("sync.httplogger=true")});
}
if (verbosity > 2) {
Logger::instance()->setLogDebug(true);
}
} else {
qCritical() << u"Verbosity:" << verbosity << u"is not supported, valid verbosity level are 0, 1, 2";
parser.showHelp(EXIT_FAILURE);
}
QStringList args = parser.positionalArguments();
if (!args.isEmpty()) {
options.server_url = QUrl::fromUserInput(args.takeFirst());
if (!options.server_url.isValid()) {
qCritical() << u"Invalid url: " << options.server_url.toString();
parser.showHelp(EXIT_FAILURE);
}
} else {
qCritical() << u"Please specify server_url";
parser.showHelp(EXIT_FAILURE);
}
if (!args.isEmpty()) {
options.space_id = args.takeFirst();
} else {
options.query = true;
}
if (!args.isEmpty()) {
options.source_dir = [&parser, arg = args.takeFirst()] {
const QFileInfo fi(arg);
if (!fi.exists()) {
qCritical() << u"Source dir" << fi.filePath() << u"does not exist.";
parser.showHelp(EXIT_FAILURE);
}
QString sourceDir = fi.absoluteFilePath();
if (!sourceDir.endsWith(QLatin1Char('/'))) {
sourceDir.append(QLatin1Char('/'));
}
return sourceDir;
}();
}
if (!args.isEmpty()) {
qCritical() << u"Unhandled arguments" << args;
parser.showHelp(EXIT_FAILURE);
}
if (parser.isSet(remoteFolder)) {
options.remoteFolder = parser.value(remoteFolder);
}
if (parser.isSet(trustOption)) {
options.trustSSL = true;
}
if (parser.isSet(nonInterActiveOption)) {
options.interactive = false;
}
if (parser.isSet(tokenOption)) {
options.token = parser.value(tokenOption).toUtf8();
} else if (options.token.isEmpty()) {
qCritical() << u"Token not set";
parser.showHelp(EXIT_FAILURE);
}
if (parser.isSet(userOption)) {
options.username = parser.value(userOption).toUtf8();
} else {
qCritical() << u"Username not set";
parser.showHelp(EXIT_FAILURE);
}
if (parser.isSet(excludeOption)) {
options.exclude = parser.value(excludeOption);
}
if (parser.isSet(unsyncedfoldersOption)) {
options.unsyncedfolders = parser.value(unsyncedfoldersOption);
}
if (parser.isSet(maxRetriesOption)) {
options.restartTimes = parser.value(maxRetriesOption).toInt();
}
if (parser.isSet(uploadLimitOption)) {
options.uplimit = parser.value(maxRetriesOption).toInt() * 1000;
}
if (parser.isSet(downloadLimitption)) {
options.downlimit = parser.value(downloadLimitption).toInt() * 1000;
}
if (parser.isSet(syncHiddenFilesOption)) {
options.ignoreHiddenFiles = false;
}
if (parser.isSet(testCrashReporter)) {
// crash onc ethe main loop was started
qCritical() << u"We'll soon crash on purpose";
QTimer::singleShot(0, qApp, &Utility::crash);
}
return options;
}
int main(int argc, char **argv)
{
auto platform = OCC::Platform::create(Platform::Type::Terminal);
qSetMessagePattern(Logger::loggerPattern());
qInstallMessageHandler(messageHandler);
QCoreApplication app(argc, argv);
platform->setApplication(&app);
app.setApplicationVersion(Theme::instance()->versionSwitchOutput());
SyncCTX ctx { parseOptions(app.arguments()) };
// start the main loop before we ask for the username etc
QTimer::singleShot(0, &app, [&] {
ctx.account = Account::create(QUuid::createUuid());
if (!ctx.account) {
qCritical() << u"Could not initialize account!";
exit(EXIT_FAILURE);
}
setupCredentials(ctx);
// don't leak credentials more than needed
ctx.options.server_url = ctx.options.server_url.adjusted(QUrl::RemoveUserInfo);
ctx.account->setUrl(ctx.options.server_url);
QObject::connect(ctx.account->credentials(), &AbstractCredentials::authenticationFailed, qApp,
[] { qFatal() << u"Authentication failed please verify your credentials"; });
auto *checkServerJob = CheckServerJobFactory(ctx.account->accessManager()).startJob(ctx.account->url(), qApp);
QObject::connect(checkServerJob, &CoreJob::finished, qApp, [ctx, checkServerJob] {
if (checkServerJob->success()) {
// Perform a call to get the capabilities.
auto *capabilitiesJob = new JsonApiJob(ctx.account, QStringLiteral("ocs/v1.php/cloud/capabilities"), {}, {}, nullptr);
QObject::connect(capabilitiesJob, &JsonApiJob::finishedSignal, qApp, [capabilitiesJob, ctx] {
if (capabilitiesJob->reply()->error() != QNetworkReply::NoError || capabilitiesJob->httpStatusCode() != 200) {
qCritical() << u"Error connecting to server";
exit(EXIT_FAILURE);
}
auto caps = capabilitiesJob->data()
.value(QStringLiteral("ocs"))
.toObject()
.value(QStringLiteral("data"))
.toObject()
.value(QStringLiteral("capabilities"))
.toObject();
qDebug() << u"Server capabilities" << caps;
ctx.account->setCapabilities({ctx.account->url(), caps.toVariantMap()});
switch (ctx.account->serverSupportLevel()) {
case Account::ServerSupportLevel::Supported:
break;
case Account::ServerSupportLevel::Unknown:
qWarning() << u"Failed to detect server version";
break;
case Account::ServerSupportLevel::Unsupported:
qCritical() << u"Error unsupported server";
exit(EXIT_FAILURE);
}
if (ctx.options.query) {
QObject::connect(ctx.account->spacesManager(), &GraphApi::SpacesManager::ready, qApp, [ctx] {
printSpaces(ctx.account->spacesManager()->spaces());
qApp->quit();
});
} else {
QObject::connect(ctx.account->spacesManager(), &GraphApi::SpacesManager::ready, qApp, [ctx] {
GraphApi::Space *space = nullptr;
if (ctx.options.space_id.count(QLatin1Char('$')) == 1) {
space = ctx.account->spacesManager()->space(ctx.options.space_id);
} else {
const auto spaces = ctx.account->spacesManager()->spaces();
if (ctx.options.space_id.startsWith(QLatin1String("space:"))) {
auto it = std::find_if(
spaces.cbegin(), spaces.cend(), [&](const GraphApi::Space *s) { return hashSaceId(s->id()) == ctx.options.space_id; });
if (it != spaces.cend()) {
space = *it;
}
} else {
auto it = std::find_if(
spaces.cbegin(), spaces.cend(), [&](const GraphApi::Space *s) { return s->displayName() == ctx.options.space_id; });
if (it != spaces.cend()) {
space = *it;
}
}
}
if (!space) {
qCritical() << u"No spaces found matching:" << ctx.options.space_id;
printSpaces(ctx.account->spacesManager()->spaces());
qApp->exit(EXIT_FAILURE);
return;
}
// much lower age than the default since this utility is usually made to be run right after a change in the tests
SyncEngine::minimumFileAgeForUpload = std::chrono::seconds(0);
sync(ctx, space->webdavUrl());
});
}
// announce we are ready
Q_EMIT ctx.account->credentialsFetched();
});
capabilitiesJob->start();
} else {
if (checkServerJob->reply()->error() == QNetworkReply::OperationCanceledError) {
qFatal() << u"Looking up " << ctx.account->url().toString() << u" timed out.";
} else {
qFatal() << u"Failed to resolve " << ctx.account->url().toString() << u" Error: " << checkServerJob->reply()->errorString();
}
}
});
});
return app.exec();
}
+77
View File
@@ -0,0 +1,77 @@
/*
* Copyright (C) by Hannah von Reth <h.vonreth@opencloud.eu>
*
* 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.
*/
#include "tokencredentials.h"
#include "creds/httpcredentials.h"
#include "libsync/accessmanager.h"
using namespace OCC;
class OCC::TokensAccessManager : public AccessManager
{
Q_OBJECT
public:
TokensAccessManager(QByteArray &&username, QByteArray &&token, QObject *parent)
: AccessManager(parent)
, _token("Basic " + QByteArray(username + ':' + token).toBase64())
{
}
protected:
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData) override
{
QNetworkRequest req(request);
if (!req.attribute(HttpCredentials::DontAddCredentialsAttribute).toBool()) {
req.setRawHeader("Authorization", _token);
}
return AccessManager::createRequest(op, req, outgoingData);
}
private:
QByteArray _token;
};
TokenCredentials::TokenCredentials(QByteArray &&username, QByteArray &&token)
: _accessManager(new TokensAccessManager(std::move(username), std::move(token), this))
{
connect(_accessManager, &QNetworkAccessManager::finished, this, [this](QNetworkReply *reply) {
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
Q_EMIT authenticationFailed();
}
});
}
AccessManager *TokenCredentials::createAM() const
{
return _accessManager;
}
bool TokenCredentials::ready() const
{
return true;
}
void TokenCredentials::fetchFromKeychain() { }
void TokenCredentials::restartOauth() { }
void TokenCredentials::persist() { }
void TokenCredentials::invalidateToken() { }
void TokenCredentials::forgetSensitiveData() { }
#include "tokencredentials.moc"
+40
View File
@@ -0,0 +1,40 @@
/*
* Copyright (C) by Hannah von Reth <h.vonreth@opencloud.eu>
*
* 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.
*/
#pragma once
#include "libsync/creds/abstractcredentials.h"
namespace OCC {
class TokensAccessManager;
class TokenCredentials : public AbstractCredentials
{
private:
Q_OBJECT
public:
TokenCredentials(QByteArray &&username, QByteArray &&token);
AccessManager *createAM() const override;
bool ready() const override;
void fetchFromKeychain() override;
void restartOauth() override;
void persist() override;
void invalidateToken() override;
void forgetSensitiveData() override;
private:
TokensAccessManager *_accessManager;
};
}
+61
View File
@@ -0,0 +1,61 @@
# TODO: differentiate release channel
# if(BUILD_RELEASE)
# set(CRASHREPORTER_RELEASE_CHANNEL "release")
# else()
set(CRASHREPORTER_RELEASE_CHANNEL "nightly")
# endif()
# Theme
set(CRASHREPORTER_ICON_DIR "${OEM_THEME_DIR}/theme/colored")
set(CRASHREPORTER_ICON_FILENAME "${APPLICATION_ICON_NAME}-icon.svg")
if (EXISTS "${OEM_THEME_DIR}/theme/colored/${CRASHREPORTER_ICON_FILENAME}")
set(CRASHREPORTER_ICON ":/${CRASHREPORTER_ICON_FILENAME}")
set(CRASHREPORTER_ICON_PATH "${CRASHREPORTER_ICON_DIR}/${CRASHREPORTER_ICON_FILENAME}")
else()
set(CRASHREPORTER_ICON_FILENAME "${APPLICATION_ICON_NAME}-icon.png")
set(CRASHREPORTER_ICON ":/${CRASHREPORTER_ICON_FILENAME}")
set(CRASHREPORTER_ICON_SIZE "128")
set(CRASHREPORTER_ICON_PATH "${CRASHREPORTER_ICON_DIR}/${CRASHREPORTER_ICON_SIZE}-${CRASHREPORTER_ICON_FILENAME}")
if (NOT EXISTS "${CRASHREPORTER_ICON_PATH}")
set(CRASHREPORTER_ICON_PATH "${CRASHREPORTER_ICON_DIR}/${APPLICATION_ICON_NAME}-icon-${CRASHREPORTER_ICON_SIZE}.png")
endif()
endif()
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/resources.qrc.in
${CMAKE_CURRENT_BINARY_DIR}/resources.qrc)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/CrashReporterConfig.h.in
${CMAKE_CURRENT_BINARY_DIR}/CrashReporterConfig.h)
# Sources
list(APPEND crashreporter_SOURCES main.cpp)
list(APPEND crashreporter_RC "${CMAKE_CURRENT_BINARY_DIR}/resources.qrc")
add_executable(${CRASHREPORTER_EXECUTABLE}
${crashreporter_SOURCES}
${crashreporter_HEADERS_MOC}
${crashreporter_UI_HEADERS}
${crashreporter_RC}
)
apply_common_target_settings(${CRASHREPORTER_EXECUTABLE})
# This is a GUI Application without its own bundle
set_target_properties(${CRASHREPORTER_EXECUTABLE}
PROPERTIES
MACOSX_BUNDLE FALSE
)
target_include_directories(${CRASHREPORTER_EXECUTABLE} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(${CRASHREPORTER_EXECUTABLE}
PRIVATE
CrashReporterQt::Gui
Qt::Core
Qt::Widgets
)
if(APPLE)
set_target_properties(${CRASHREPORTER_EXECUTABLE} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<TARGET_FILE_DIR:qsfera>")
else()
install(TARGETS ${CRASHREPORTER_EXECUTABLE} ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
endif()
@@ -0,0 +1,16 @@
#ifndef CRASHREPORTERCONFIG_H
#define CRASHREPORTERCONFIG_H
#define CRASHREPORTER_BUILD_ID "@CMAKE_DATESTAMP_YEAR@@CMAKE_DATESTAMP_MONTH@@CMAKE_DATESTAMP_DAY@000000"
#define CRASHREPORTER_RELEASE_CHANNEL "@CRASHREPORTER_RELEASE_CHANNEL@"
#define CRASHREPORTER_PRODUCT_NAME "@APPLICATION_SHORTNAME@"
#define CRASHREPORTER_VERSION_STRING "@MIRALL_VERSION_STRING@"
#define CRASHREPORTER_SUBMIT_URL "@CRASHREPORTER_SUBMIT_URL@"
#define CRASHREPORTER_ICON "@CRASHREPORTER_ICON@"
#endif // CRASHREPORTERCONFIG_H
+103
View File
@@ -0,0 +1,103 @@
/*
* Copyright (C) by Dominik Schmidt <domme@tomahawk-player.org>
*
* 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.
*/
#include "CrashReporterConfig.h"
#include <libcrashreporter-gui/CrashReporter.h>
#include <QApplication>
#include <QDir>
#include <QDebug>
#include <QFileInfo>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
#ifdef Q_OS_WIN
QApplication::setStyle(QStringLiteral("fusion"));
#endif // Q_OS_WIN
if (app.arguments().size() != 2) {
qDebug() << u"You need to pass the .dmp file path as only argument";
return 1;
}
// TODO: install socorro ....
CrashReporter reporter(QUrl(QStringLiteral(CRASHREPORTER_SUBMIT_URL)), app.arguments());
#ifdef CRASHREPORTER_ICON
reporter.setLogo(QPixmap(QStringLiteral(CRASHREPORTER_ICON)));
#endif
reporter.setWindowTitle(QStringLiteral(CRASHREPORTER_PRODUCT_NAME));
reporter.setText(QStringLiteral("<html><head/><body><p><span style=\" font-weight:600;\">Sorry!</span> " CRASHREPORTER_PRODUCT_NAME
" crashed. Please tell us about it! " CRASHREPORTER_PRODUCT_NAME
" has created an error report for you that can help improve the stability in the future. You can now send this report "
"directly to the " CRASHREPORTER_PRODUCT_NAME " developers.</p></body></html>"));
const QFileInfo crashLog(QDir::tempPath() + QStringLiteral("/" CRASHREPORTER_PRODUCT_NAME "-crash.log"));
if (crashLog.exists()) {
QFile inFile(crashLog.filePath());
if (inFile.open(QFile::ReadOnly)) {
reporter.setComment(QString::fromUtf8(inFile.readAll()));
}
}
reporter.setReportData("BuildID", CRASHREPORTER_BUILD_ID);
reporter.setReportData("ProductName", CRASHREPORTER_PRODUCT_NAME);
reporter.setReportData("Version", CRASHREPORTER_VERSION_STRING);
reporter.setReportData("ReleaseChannel", CRASHREPORTER_RELEASE_CHANNEL);
//reporter.setReportData( "timestamp", QByteArray::number( QDateTime::currentDateTime().toTime_t() ) );
// add parameters
// << Pair("InstallTime", "1357622062")
// << Pair("Theme", "classic/1.0")
// << Pair("Version", "30")
// << Pair("id", "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}")
// << Pair("Vendor", "Mozilla")
// << Pair("EMCheckCompatibility", "true")
// << Pair("Throttleable", "0")
// << Pair("URL", "http://code.google.com/p/crashme/")
// << Pair("version", "20.0a1")
// << Pair("CrashTime", "1357770042")
// << Pair("submitted_timestamp", "2013-01-09T22:21:18.646733+00:00")
// << Pair("buildid", "20130107030932")
// << Pair("timestamp", "1357770078.646789")
// << Pair("Notes", "OpenGL: NVIDIA Corporation -- GeForce 8600M GT/PCIe/SSE2 -- 3.3.0 NVIDIA 313.09 -- texture_from_pixmap\r\n")
// << Pair("StartupTime", "1357769913")
// << Pair("FramePoisonSize", "4096")
// << Pair("FramePoisonBase", "7ffffffff0dea000")
// << Pair("Add-ons", "%7B972ce4c6-7e08-4474-a285-3208198ce6fd%7D:20.0a1,crashme%40ted.mielczarek.org:0.4")
// << Pair("SecondsSinceLastCrash", "1831736")
// << Pair("ProductName", "WaterWolf")
// << Pair("legacy_processing", "0")
// << Pair("ProductID", "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}")
;
// TODO:
// send log
// QFile logFile( INSERT_FILE_PATH_HERE );
// logFile.open( QFile::ReadOnly );
// reporter.setReportData( "upload_file_miralllog", qCompress( logFile.readAll() ), "application/x-gzip", QFileInfo( INSERT_FILE_PATH_HERE ).fileName().toUtf8());
// logFile.close();
reporter.show();
return app.exec();
}
@@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/">
<file alias="@CRASHREPORTER_ICON_FILENAME@">@CRASHREPORTER_ICON_PATH@</file>
</qresource>
</RCC>
+224
View File
@@ -0,0 +1,224 @@
include(ECMAddAppIcon)
find_package(KDSingleApplication-qt6 1.0.0 REQUIRED)
if (WIN32)
find_package(LibSnoreToast)
set_package_properties(LibSnoreToast PROPERTIES
URL https://invent.kde.org/libraries/snoretoast
DESCRIPTION "Command-line application capable of creating Windows Toast notifications"
TYPE OPTIONAL
)
endif()
add_library(QSferaGui SHARED)
target_sources(QSferaGui PRIVATE
aboutdialog.ui
accountsettings.ui
generalsettings.ui
ignorelisteditor.ui
networksettings.ui
protocolwidget.ui
issueswidget.ui
settingsdialog.ui
tlserrordialog.ui
logbrowser.ui
)
target_sources(QSferaGui PRIVATE
aboutdialog.cpp
accountmanager.cpp
accountsettings.cpp
accountmodalwidget.cpp
accountmodalwidget.ui
application.cpp
fetchserversettings.cpp
commonstrings.cpp
connectionvalidator.cpp
folder.cpp
folderdefinition.cpp
folderman.cpp
folderstatusmodel.cpp
folderwatcher.cpp
generalsettings.cpp
ignorelisteditor.cpp
lockwatcher.cpp
logbrowser.cpp
fonticonmessagebox.cpp
networkinformation.cpp
networksettings.cpp
notifications.cpp
openfilemanager.cpp
protocolwidget.cpp
protocolitem.cpp
issueswidget.cpp
activitywidget.cpp
selectivesyncwidget.cpp
settingsdialog.cpp
tlserrordialog.cpp
syncrunfilelog.cpp
systray.cpp
accountstate.cpp
guiutility.cpp
elidedlabel.cpp
translations.cpp
creds/httpcredentialsgui.cpp
creds/qmlcredentials.cpp
updateurldialog.cpp
updatenotifier.cpp
models/expandingheaderview.cpp
models/models.cpp
models/protocolitemmodel.cpp
scheduling/syncscheduler.cpp
scheduling/etagwatcher.cpp
notifications/systemnotification.cpp
notifications/systemnotificationmanager.cpp
notifications/systemnotificationbackend.cpp
qmlutils.cpp
)
# 3rd party code
target_sources(QSferaGui PRIVATE ../3rdparty/QProgressIndicator/QProgressIndicator.cpp)
add_subdirectory(newwizard)
add_subdirectory(folderwizard)
set_target_properties(QSferaGui PROPERTIES
OUTPUT_NAME "QSferaGui"
AUTOUIC ON
AUTORCC ON
)
# for the generated qml module
target_include_directories(QSferaGui PRIVATE models spaces creds)
target_link_libraries(QSferaGui
PUBLIC
Qt::Widgets Qt::Network Qt::Xml Qt::Quick Qt::QuickWidgets Qt::QuickControls2
libsync
Qt6Keychain::Qt6Keychain
)
apply_common_target_settings(QSferaGui)
ecm_add_qml_module(QSferaGui
URI eu.QSfera.gui
VERSION 1.0
NAMESPACE OCC
GENERATE_PLUGIN_SOURCE
QML_FILES
qml/AccountBar.qml
qml/AccountButton.qml
qml/FolderDelegate.qml
qml/FolderError.qml
qml/UpdateUrlDialog.qml
qml/credentials/Credentials.qml
qml/credentials/OAuthCredentials.qml
spaces/qml/SpaceDelegate.qml
spaces/qml/SpacesView.qml
)
generate_export_header(QSferaGui
EXPORT_MACRO_NAME QSFERA_GUI_EXPORT
EXPORT_FILE_NAME qsferaguilib.h
STATIC_DEFINE QSFERA_BUILT_AS_STATIC
)
add_subdirectory(spaces)
add_subdirectory(socketapi)
target_include_directories(QSferaGui PUBLIC
${CMAKE_SOURCE_DIR}/src/3rdparty/QProgressIndicator
${CMAKE_CURRENT_BINARY_DIR}
)
IF( APPLE )
target_sources(QSferaGui PRIVATE
notifications/macnotifications.mm
settingsdialog_mac.mm
guiutility_mac.mm
folderwatcher_mac.cpp)
set_source_files_properties(guiutility_mac.mm PROPERTIES COMPILE_DEFINITIONS SOCKETAPI_TEAM_IDENTIFIER_PREFIX="${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}")
elseif( WIN32 )
target_sources(QSferaGui PRIVATE
guiutility_win.cpp
folderwatcher_win.cpp
navigationpanehelper.cpp
)
if (TARGET SnoreToast::SnoreToastActions)
target_sources(QSferaGui PRIVATE
notifications/snoretoast.cpp)
target_compile_definitions(QSferaGui PRIVATE WITH_SNORE_TOAST)
target_link_libraries(QSferaGui PRIVATE SnoreToast::SnoreToastActions)
endif()
elseif(UNIX AND NOT APPLE)
## handle DBUS for Fdo notifications
target_link_libraries(QSferaGui PUBLIC Qt::DBus)
target_sources(QSferaGui PRIVATE
folderwatcher_linux.cpp
guiutility_unix.cpp
notifications/dbusnotifications.cpp
)
qt_add_dbus_interface(notifications_dbus_SRCS notifications/org.freedesktop.Notifications.xml dbusnotifications_interface)
target_sources(QSferaGui PRIVATE ${notifications_dbus_SRCS})
endif()
if(WITH_AUTO_UPDATER)
add_subdirectory(updater)
target_compile_definitions(QSferaGui PUBLIC $<BUILD_INTERFACE:WITH_AUTO_UPDATER>)
endif()
add_executable(qsfera main.cpp)
set_target_properties(qsfera PROPERTIES
OUTPUT_NAME "${APPLICATION_EXECUTABLE}"
AUTOUIC ON
AUTORCC ON
)
apply_common_target_settings(qsfera)
target_link_libraries(qsfera PUBLIC QSferaGui QSferaResources KDAB::kdsingleapplication )
MESSAGE(STATUS "QSFERA_SIDEBAR_ICONS: ${APPLICATION_ICON_NAME}: ${QSFERA_SIDEBAR_ICONS}")
ecm_add_app_icon(appIcons ICONS "${QSFERA_ICONS}" SIDEBAR_ICONS "${QSFERA_SIDEBAR_ICONS}" OUTFILE_BASENAME "${APPLICATION_ICON_NAME}")
target_sources(qsfera PRIVATE ${appIcons})
if(NOT APPLE)
if(WIN32)
target_sources(qsfera PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/qsfera.exe.manifest)
else()
file(GLOB _icons "${OEM_THEME_DIR}/theme/colored/*-${APPLICATION_ICON_NAME}-icon.png")
foreach(_file ${_icons})
string(REPLACE "${OEM_THEME_DIR}/theme/colored/" "" _res ${_file})
string(REPLACE "-${APPLICATION_ICON_NAME}-icon.png" "" _res ${_res})
install(FILES ${_file} RENAME ${APPLICATION_ICON_NAME}.png DESTINATION ${KDE_INSTALL_DATADIR}/icons/hicolor/${_res}x${_res}/apps)
endforeach(_file)
endif()
else()
target_sources(qsfera PRIVATE ${QSFERA_BUNDLED_RESOURCES})
set_source_files_properties(
${QSFERA_BUNDLED_RESOURCES}
PROPERTIES
MACOSX_PACKAGE_LOCATION Resources
)
set_target_properties(qsfera PROPERTIES OUTPUT_NAME "${APPLICATION_SHORTNAME}" MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/MacOSXBundleInfo.plist)
endif()
install(TARGETS qsfera QSferaGui ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
ecm_finalize_qml_module(QSferaGui DESTINATION ${KDE_INSTALL_QMLDIR})
if(UNIX AND NOT APPLE)
configure_file(${CMAKE_SOURCE_DIR}/qsfera.desktop.in
${CMAKE_CURRENT_BINARY_DIR}/${APPLICATION_EXECUTABLE}.desktop)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${APPLICATION_EXECUTABLE}.desktop DESTINATION ${KDE_INSTALL_DATADIR}/applications )
endif()
+47
View File
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleName</key>
<string>${APPLICATION_NAME}</string>
<key>CFBundleExecutable</key>
<string>${APPLICATION_SHORTNAME}</string>
<key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
<key>CFBundleIdentifier</key>
<string>${APPLICATION_REV_DOMAIN}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLongVersionString</key>
<string>${APPLICATION_NAME} ${MIRALL_VERSION_STRING}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>${MIRALL_VERSION_FULL}</string>
<key>CFBundleShortVersionString</key>
<string>${MIRALL_VERSION_STRING}</string>
<key>NSHumanReadableCopyright</key>
<string>(C) 2025 OpenCloud GmbH
(C) 2014-${MIRALL_VERSION_YEAR} ownCloud GmbH</string>
<key>SUShowReleaseNotes</key>
<false/>
<key>SUPublicDSAKeyFile</key>
<string>dsa_pub.pem</string>
<key>SUPublicEDKey</key>
<string>F9ZGRXJE7XbQb2Kt36hwaBO4rYjkXYHiNS5hd+MkyKY=</string>
<key>SUAllowsAutomaticUpdates</key>
<string>NO</string>
<key>LSMinimumSystemVersion</key>
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>UIDesignRequiresCompatibility</key>
<true/>
</dict>
</plist>
+194
View File
@@ -0,0 +1,194 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#include "aboutdialog.h"
#include "ui_aboutdialog.h"
#include "gui/guiutility.h"
#include "libsync/theme.h"
#ifdef WITH_AUTO_UPDATER
#include "libsync/configfile.h"
#include "updater/ocupdater.h"
#ifdef Q_OS_MAC
// FIXME We should unify those, but Sparkle does everything behind the scene transparently
#include "updater/sparkleupdater.h"
#endif
#endif
namespace {
#ifdef WITH_AUTO_UPDATER
bool isTestPilotCloudTheme()
{
return OCC::Theme::instance()->appName() == QLatin1String("testpilotcloud");
}
#endif
}
namespace OCC {
AboutDialog::AboutDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::AboutDialog)
{
ui->setupUi(this);
ui->aboutText->setText(Theme::instance()->about());
ui->icon->setPixmap(Theme::instance()->aboutIcon().pixmap(256));
ui->versionInfo->setText(Theme::instance()->aboutVersions(Theme::VersionFormat::RichText));
connect(ui->versionInfo, &QTextBrowser::anchorClicked, this, &AboutDialog::openBrowserFromUrl);
connect(ui->aboutText, &QLabel::linkActivated, this, &AboutDialog::openBrowser);
setupUpdaterWidget();
}
AboutDialog::~AboutDialog()
{
delete ui;
}
void AboutDialog::openBrowser(const QString &s)
{
Utility::openBrowser(QUrl(s), this);
}
void AboutDialog::openBrowserFromUrl(const QUrl &s)
{
return openBrowser(s.toString());
}
void AboutDialog::setupUpdaterWidget()
{
#ifdef WITH_AUTO_UPDATER
// non-standard update channels are only supported by the vanilla theme and the testpilotcloud theme
if (!Resources::isVanillaTheme() && !isTestPilotCloudTheme()) {
if (Utility::isMac()) {
// Because we don't have any statusString from the SparkleUpdater anyway we can hide the whole thing
ui->updaterWidget->hide();
} else {
ui->updateChannelLabel->hide();
ui->updateChannel->hide();
if (ConfigFile().updateChannel() != QLatin1String("stable")) {
ConfigFile().setUpdateChannel(QStringLiteral("stable"));
}
}
}
// we want to attach the known english identifiers which are also used within the configuration file as user data inside the data model
// that way, when we intend to reset to the original selection when the dialog, we can look up the config file's stored value in the data model
ui->updateChannel->addItem(QStringLiteral("stable"), QStringLiteral("stable"));
if (!Resources::isVanillaTheme()) {
ui->updateChannel->addItem(tr("beta"), QStringLiteral("beta"));
}
if (!ConfigFile().skipUpdateCheck() && Updater::instance()) {
// Channel selection
ui->updateChannel->setCurrentIndex(ui->updateChannel->findData(ConfigFile().updateChannel()));
connect(ui->updateChannel, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &AboutDialog::slotUpdateChannelChanged);
// Note: the sparkle-updater is not an OCUpdater
if (auto *ocupdater = qobject_cast<OCUpdater *>(Updater::instance())) {
auto updateInfo = [ocupdater, this] {
QString statusString = ocupdater->statusString();
switch (ocupdater->downloadState()) {
case OCUpdater::Unknown:
[[fallthrough]];
case OCUpdater::CheckingServer:
[[fallthrough]];
case OCUpdater::UpToDate:
// No update, leave the status string as is.
break;
case OCUpdater::Downloading:
[[fallthrough]];
case OCUpdater::DownloadComplete:
[[fallthrough]];
case OCUpdater::DownloadFailed:
[[fallthrough]];
case OCUpdater::DownloadTimedOut:
[[fallthrough]];
case OCUpdater::UpdateOnlyAvailableThroughSystem:
statusString = QStringLiteral("Version %1 is available. %2").arg(ocupdater->availableVersionString(), statusString);
break;
}
ui->updateStateLabel->setText(statusString);
ui->restartButton->setVisible(ocupdater->downloadState() == OCUpdater::DownloadComplete);
};
connect(ocupdater, &OCUpdater::downloadStateChanged, this, updateInfo);
connect(ui->restartButton, &QAbstractButton::clicked, ocupdater, &Updater::applyUpdateAndRestart);
updateInfo();
}
#ifdef HAVE_SPARKLE
if (SparkleUpdater *sparkleUpdater = qobject_cast<SparkleUpdater *>(Updater::instance())) {
ui->updateStateLabel->setText(sparkleUpdater->statusString());
ui->restartButton->setVisible(false);
}
#endif
} else {
ui->updaterWidget->hide();
}
#else
ui->updaterWidget->hide();
#endif
}
void AboutDialog::slotUpdateChannelChanged([[maybe_unused]] int index)
{
#ifdef WITH_AUTO_UPDATER
QString channel;
if (index < 0) {
// invalid index, reset to stable
channel = QStringLiteral("stable");
} else {
channel = ui->updateChannel->itemData(index).toString();
}
if (channel == ConfigFile().updateChannel()) {
return;
}
auto msgBox = new QMessageBox(QMessageBox::Warning, tr("Change update channel?"),
tr("<html>The update channel determines which client updates will be offered for installation.<ul>"
"<li>\"stable\" contains only upgrades that are considered reliable</li>"
"%1"
"</ul>"
"<br>⚠️Downgrades are not supported. If you switch to a stable channel this change will only be applied with the next major release.</html>")
.arg(
isTestPilotCloudTheme() ? tr("<li>\"beta\" may contain newer features and bugfixes, but have not yet been tested thoroughly</li>") : QString()),
QMessageBox::NoButton, this);
auto acceptButton = msgBox->addButton(tr("Change update channel"), QMessageBox::AcceptRole);
msgBox->addButton(tr("Cancel"), QMessageBox::RejectRole);
connect(msgBox, &QMessageBox::finished, msgBox, [this, channel, msgBox, acceptButton] {
msgBox->deleteLater();
if (msgBox->clickedButton() == acceptButton) {
ConfigFile().setUpdateChannel(channel);
if (OCUpdater *updater = qobject_cast<OCUpdater *>(Updater::instance())) {
updater->setUpdateUrl(Updater::updateUrl());
updater->checkForUpdate();
}
#if defined(Q_OS_MAC) && defined(HAVE_SPARKLE)
else if (SparkleUpdater *updater = qobject_cast<SparkleUpdater *>(Updater::instance())) {
updater->setUpdateUrl(Updater::updateUrl());
updater->checkForUpdate();
}
#endif
} else {
const auto oldChannel = ui->updateChannel->findData(ConfigFile().updateChannel());
Q_ASSERT(oldChannel >= 0);
Q_ASSERT(oldChannel <= 1);
ui->updateChannel->setCurrentIndex(oldChannel);
}
});
msgBox->open();
#endif
}
} // OCC namespace
+47
View File
@@ -0,0 +1,47 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#ifndef ABOUTDIALOG_H
#define ABOUTDIALOG_H
#include <QDialog>
namespace OCC {
namespace Ui {
class AboutDialog;
}
class AboutDialog : public QDialog
{
Q_OBJECT
public:
explicit AboutDialog(QWidget *parent = nullptr);
~AboutDialog();
private:
void openBrowser(const QString &s);
void openBrowserFromUrl(const QUrl &s);
void setupUpdaterWidget();
private Q_SLOTS:
void slotUpdateChannelChanged(int index);
private:
Ui::AboutDialog *ui;
};
}
#endif // ABOUTDIALOG_H
+218
View File
@@ -0,0 +1,218 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::AboutDialog</class>
<widget class="QDialog" name="OCC::AboutDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>751</width>
<height>391</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">About</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>About</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="icon">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>256</width>
<height>256</height>
</size>
</property>
<property name="text">
<string notr="true"/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="aboutText">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="updaterWidget">
<property name="title">
<string/>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="updateChannelLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&amp;Update Channel</string>
</property>
<property name="buddy">
<cstring>updateChannel</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="updateChannel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="updateStateLabel"/>
</item>
<item>
<widget class="QPushButton" name="restartButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&amp;Restart &amp;&amp; Update</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Versions</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTextBrowser" name="versionInfo">
<property name="openExternalLinks">
<bool>false</bool>
</property>
<property name="openLinks">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>versionInfo</tabstop>
<tabstop>updateChannel</tabstop>
<tabstop>restartButton</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>OCC::AboutDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>OCC::AboutDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+258
View File
@@ -0,0 +1,258 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* 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.
*/
#include "accountmanager.h"
#include "account.h"
#include "configfile.h"
#include "creds/credentialmanager.h"
#include "guiutility.h"
#include <creds/httpcredentialsgui.h>
#include <theme.h>
#ifdef Q_OS_WIN
#include "common/utility_win.h"
#include "gui/navigationpanehelper.h"
#endif
#include <QSettings>
using namespace Qt::Literals::StringLiterals;
namespace {
auto urlC()
{
return QStringLiteral("url");
}
auto defaultSyncRootC()
{
return QStringLiteral("default_sync_root");
}
const QString davUserDisplyNameC()
{
return QStringLiteral("display-name");
}
const QString userUUIDC()
{
return QStringLiteral("uuid");
}
auto caCertsKeyC()
{
return QStringLiteral("CaCertificates");
}
auto accountsC()
{
return QStringLiteral("Accounts");
}
auto capabilitesC()
{
return QStringLiteral("capabilities");
}
}
namespace OCC {
Q_LOGGING_CATEGORY(lcAccountManager, "gui.account.manager", QtInfoMsg)
AccountManager *AccountManager::instance()
{
static AccountManager instance;
return &instance;
}
AccountManager *AccountManager::create(QQmlEngine *qmlEngine, QJSEngine *)
{
Q_ASSERT(qmlEngine->thread() == AccountManager::instance()->thread());
QJSEngine::setObjectOwnership(AccountManager::instance(), QJSEngine::CppOwnership);
return instance();
}
bool AccountManager::restore()
{
auto settings = ConfigFile::makeQSettings();
if (settings.status() != QSettings::NoError) {
qCWarning(lcAccountManager) << u"Could not read settings from" << settings.fileName() << settings.status();
return false;
}
const auto size = settings.beginReadArray(accountsC());
for (auto i = 0; i < size; ++i) {
settings.setArrayIndex(i);
auto urlConfig = settings.value(urlC());
if (!urlConfig.isValid()) {
// No URL probably means a corrupted entry in the account settings
qCWarning(lcAccountManager) << u"No URL for account " << settings.group();
continue;
}
auto acc = createAccount(settings.value(userUUIDC(), QVariant::fromValue(QUuid::createUuid())).toUuid());
acc->setUrl(urlConfig.toUrl());
acc->_displayName = settings.value(davUserDisplyNameC()).toString();
auto capabilities = settings.value(capabilitesC()).value<QVariantMap>();
capabilities.insert(u"cached"_s, true); // mark as cached capabilities, this will trigger a capabilitiesChanged signal once we got real capabilities
acc->setCapabilities({acc->url(), capabilities});
acc->setDefaultSyncRoot(settings.value(defaultSyncRootC()).toString());
acc->setCredentials(new HttpCredentialsGui);
// now the server cert
const auto certs = QSslCertificate::fromData(settings.value(caCertsKeyC()).toByteArray());
qCInfo(lcAccountManager) << u"Restored: " << certs.count() << u" unknown certs.";
acc->setApprovedCerts(certs);
if (auto accState = AccountState::loadFromSettings(acc, settings)) {
addAccountState(std::move(accState));
}
}
settings.endArray();
return true;
}
void AccountManager::save()
{
auto settings = ConfigFile::makeQSettings();
settings.remove(accountsC());
settings.beginWriteArray(accountsC(), _accounts.size());
int i = 0;
for (const auto &accountState : std::as_const(_accounts)) {
settings.setArrayIndex(i++);
auto account = accountState->account();
qCDebug(lcAccountManager) << u"Saving account" << account->url().toString();
settings.setValue(urlC(), account->_url.toString());
settings.setValue(davUserDisplyNameC(), account->_displayName);
settings.setValue(userUUIDC(), account->uuid());
if (account->hasCapabilities()) {
settings.setValue(capabilitesC(), account->capabilities().raw());
}
if (account->hasDefaultSyncRoot()) {
settings.setValue(defaultSyncRootC(), account->defaultSyncRoot());
}
// Save accepted certificates.
qCInfo(lcAccountManager) << u"Saving " << account->approvedCerts().count() << u" unknown certs.";
const auto approvedCerts = account->approvedCerts();
QByteArray certs;
for (const auto &cert : approvedCerts) {
certs += cert.toPem() + '\n';
}
if (!certs.isEmpty()) {
settings.setValue(caCertsKeyC(), certs);
}
// save the account state
accountState->writeToSettings(settings);
}
settings.endArray();
qCInfo(lcAccountManager) << u"Saved all account settings";
}
QStringList AccountManager::accountNames() const
{
QStringList accounts;
accounts.reserve(AccountManager::instance()->accounts().size());
for (const auto &a : AccountManager::instance()->accounts()) {
accounts << a->account()->displayNameWithHost();
}
std::sort(accounts.begin(), accounts.end());
return accounts;
}
QList<AccountState *> AccountManager::accountsRaw() const
{
QList<AccountState *> out;
out.reserve(_accounts.size());
for (auto &x : _accounts.values()) {
out.append(x);
}
return out;
}
AccountStatePtr AccountManager::account(const QUuid uuid)
{
const auto acc = Utility::optionalFind(_accounts, uuid);
if (OC_ENSURE(acc.has_value())) {
return acc->value();
}
return {};
}
AccountStatePtr AccountManager::addAccount(const AccountPtr &newAccount)
{
return addAccountState(AccountState::fromNewAccount(newAccount));
}
void AccountManager::deleteAccount(AccountStatePtr account)
{
auto it = std::find(_accounts.begin(), _accounts.end(), account);
if (it == _accounts.end()) {
return;
}
// The argument keeps a strong reference to the AccountState, so we can safely remove other
// AccountStatePtr occurrences:
_accounts.erase(it);
if (account->account()->hasDefaultSyncRoot()) {
Utility::unmarkDirectoryAsSyncRoot(account->account()->defaultSyncRoot());
}
// Forget account credentials, cookies
account->account()->credentials()->forgetSensitiveData();
account->account()->credentialManager()->clear();
Q_EMIT accountRemoved(account);
Q_EMIT accountsChanged();
account->deleteLater();
save();
}
AccountPtr AccountManager::createAccount(const QUuid &uuid)
{
AccountPtr acc = Account::create(uuid);
return acc;
}
void AccountManager::shutdown()
{
const auto accounts = std::move(_accounts);
for (const auto &acc : accounts) {
Q_EMIT accountRemoved(acc);
}
qDeleteAll(accounts);
}
AccountStatePtr AccountManager::addAccountState(std::unique_ptr<AccountState> &&accountState)
{
auto *rawAccount = accountState->account().data();
connect(rawAccount, &Account::wantsAccountSaved, this, &AccountManager::save);
AccountStatePtr statePtr = accountState.release();
_accounts.insert(statePtr->account()->uuid(), statePtr);
Q_EMIT accountAdded(statePtr);
Q_EMIT accountsChanged();
return statePtr;
}
}
+115
View File
@@ -0,0 +1,115 @@
/*
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* 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.
*/
#pragma once
#include "gui/qsferaguilib.h"
#include "account.h"
#include "accountstate.h"
#include <QtQmlIntegration/QtQmlIntegration>
class QJSEngine;
class QQmlEngine;
namespace OCC {
/**
@brief The AccountManager class
@ingroup gui
*/
class QSFERA_GUI_EXPORT AccountManager : public QObject
{
Q_OBJECT
Q_PROPERTY(QList<AccountState *> accounts READ accountsRaw() NOTIFY accountsChanged)
QML_SINGLETON
QML_ELEMENT
public:
static AccountManager *instance();
static AccountManager *create(QQmlEngine *qmlEngine, QJSEngine *);
~AccountManager() override {}
/**
* Saves the accounts
*/
void save();
/**
* Creates account objects from settings.
*
* Returns false if there was an error reading the settings,
* but note that settings not existing is not an error.
*/
bool restore();
/**
* Add this account in the list of saved accounts.
* Typically called from the wizard
*/
AccountStatePtr addAccount(const AccountPtr &newAccount);
/**
* remove all accounts
*/
void shutdown();
/**
* Return a list of all accounts.
*/
const QList<AccountStatePtr> accounts() { return _accounts.values(); }
/**
* Return the account state pointer for an account identified by its display name
*/
AccountStatePtr account(const QUuid uuid);
/**
* Delete the AccountState
*/
void deleteAccount(AccountStatePtr account);
/**
* Creates an account and sets up some basic handlers.
* Does *not* add the account to the account manager just yet.
*/
static AccountPtr createAccount(const QUuid &uuid);
/**
* Returns a sorted list of displayNames
*/
QStringList accountNames() const;
private:
// expose raw pointers to qml
QList<AccountState *> accountsRaw() const;
// Adds an account to the tracked list, emitting accountAdded()
AccountStatePtr addAccountState(std::unique_ptr<AccountState> &&accountState);
Q_SIGNALS:
void accountAdded(AccountStatePtr account);
void accountRemoved(AccountStatePtr account);
void accountsChanged();
private:
AccountManager() {}
QMap<QUuid, AccountStatePtr> _accounts;
/// Account ids from settings that weren't read
QSet<QString> _additionalBlockedAccountIds;
};
}
+68
View File
@@ -0,0 +1,68 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#include "accountmodalwidget.h"
#include "ui_accountmodalwidget.h"
#include "gui/qmlutils.h"
namespace OCC {
AccountModalWidget::AccountModalWidget(const QString &title, QWidget *widget, QWidget *parent)
: QWidget(parent)
, ui(new Ui::AccountModalWidget)
{
ui->setupUi(this);
ui->groupBox->setTitle(title);
ui->groupBox->layout()->addWidget(widget);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &AccountModalWidget::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &AccountModalWidget::reject);
}
AccountModalWidget::AccountModalWidget(const QString &title, const QUrl &qmlSource, QObject *qmlContext, QWidget *parent)
: AccountModalWidget(
title,
[&] {
auto *out = new QmlUtils::OCQuickWidget;
out->setOCContext(qmlSource, parent, qmlContext, QJSEngine::JavaScriptOwnership);
return out;
}(),
parent)
{
}
void AccountModalWidget::setStandardButtons(QDialogButtonBox::StandardButtons buttons)
{
ui->buttonBox->setStandardButtons(buttons);
}
QPushButton *AccountModalWidget::addButton(const QString &text, QDialogButtonBox::ButtonRole role)
{
return ui->buttonBox->addButton(text, role);
}
void AccountModalWidget::accept()
{
Q_EMIT accepted();
Q_EMIT finished(Result::Accepted);
}
void AccountModalWidget::reject()
{
Q_EMIT rejected();
Q_EMIT finished(Result::Rejected);
}
} // OCC
+53
View File
@@ -0,0 +1,53 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#pragma once
#include "gui/qmlutils.h"
#include <QDialogButtonBox>
namespace OCC {
namespace Ui {
class AccountModalWidget;
}
class AccountModalWidget : public QWidget
{
Q_OBJECT
public:
AccountModalWidget(const QString &title, QWidget *widget, QWidget *parent);
AccountModalWidget(const QString &title, const QUrl &qmlSource, QObject *qmlContext, QWidget *parent);
enum class Result { Rejected, Accepted };
Q_ENUM(Result)
void setStandardButtons(QDialogButtonBox::StandardButtons buttons);
QPushButton *addButton(const QString &text, QDialogButtonBox::ButtonRole role);
public Q_SLOTS:
void accept();
void reject();
Q_SIGNALS:
void accepted();
void rejected();
void finished(Result result);
private:
Ui::AccountModalWidget *ui;
};
} // OCC
+42
View File
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::AccountModalWidget</class>
<widget class="QWidget" name="OCC::AccountModalWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>25</number>
</property>
<property name="rightMargin">
<number>25</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>GroupBox</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2"/>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::NoButton</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+519
View File
@@ -0,0 +1,519 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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.
*/
#include "accountsettings.h"
#include "ui_accountsettings.h"
#include "account.h"
#include "accountmanager.h"
#include "accountstate.h"
#include "application.h"
#include "common/restartmanager.h"
#include "commonstrings.h"
#include "configfile.h"
#include "folderman.h"
#include "folderstatusmodel.h"
#include "folderwizard/folderwizard.h"
#include "gui/accountmodalwidget.h"
#include "gui/fonticonmessagebox.h"
#include "gui/models/models.h"
#include "gui/networkinformation.h"
#include "gui/notifications/systemnotificationmanager.h"
#include "gui/qmlutils.h"
#include "gui/selectivesyncwidget.h"
#include "gui/spaces/spaceimageprovider.h"
#include "gui/updateurldialog.h"
#include "libsync/graphapi/spacesmanager.h"
#include "libsync/syncresult.h"
#include "networkjobs/jsonjob.h"
#include "resources/fonticon.h"
#include "scheduling/syncscheduler.h"
#include "settingsdialog.h"
#include <QSortFilterProxyModel>
#include <QtQuickWidgets/QtQuickWidgets>
using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;
namespace {
constexpr auto modalWidgetStretchedMarginC = 50;
}
namespace OCC {
Q_LOGGING_CATEGORY(lcAccountSettings, "gui.account.settings", QtInfoMsg)
AccountSettings::AccountSettings(const AccountStatePtr &accountState, QWidget *parent)
: QWidget(parent)
, ui(new Ui::AccountSettings)
, _accountState(accountState)
{
ui->setupUi(this);
_model = new FolderStatusModel(this);
_model->setAccountState(_accountState);
auto weightedModel = new QSortFilterProxyModel(this);
weightedModel->setSourceModel(_model);
weightedModel->setSortRole(static_cast<int>(FolderStatusModel::Roles::Priority));
weightedModel->sort(0, Qt::DescendingOrder);
_sortModel = weightedModel;
ui->quickWidget->engine()->addImageProvider(QStringLiteral("space"), new Spaces::SpaceImageProvider(_accountState->account()));
ui->quickWidget->setOCContext(QUrl(QStringLiteral("qrc:/qt/qml/eu/QSfera/gui/qml/FolderDelegate.qml")), this);
connect(FolderMan::instance(), &FolderMan::folderListChanged, _model, &FolderStatusModel::resetFolders);
connect(_accountState->account().data(), &Account::requestUrlUpdate, this, [this](const QUrl &newUrl) {
if (_updateUrlDialog) {
return;
}
auto *updateUrlDialog = UpdateUrlDialog::fromAccount(_accountState->account(), newUrl, ocApp()->settingsDialog());
_updateUrlDialog = updateUrlDialog;
connect(updateUrlDialog, &UpdateUrlDialog::accepted, this, [newUrl, this]() {
_accountState->account()->setUrl(newUrl);
Q_EMIT _accountState->account()->wantsAccountSaved(_accountState->account().data());
// reload the spaces
RestartManager::requestRestart();
});
auto *modalWidget = new AccountModalWidget(updateUrlDialog->windowTitle(), updateUrlDialog, ocApp()->settingsDialog());
connect(updateUrlDialog, &UpdateUrlDialog::accepted, modalWidget, &AccountModalWidget::accept);
connect(updateUrlDialog, &UpdateUrlDialog::rejected, modalWidget, &AccountModalWidget::reject);
addModalWidget(modalWidget);
});
connect(_accountState.data(), &AccountState::stateChanged, this, &AccountSettings::slotAccountStateChanged);
slotAccountStateChanged();
connect(_accountState.get(), &AccountState::isSettingUpChanged, this, [this] {
if (_accountState->isSettingUp()) {
ui->spinner->startAnimation();
ui->stackedWidget->setCurrentWidget(ui->loadingPage);
} else {
ui->spinner->stopAnimation();
ui->stackedWidget->setCurrentWidget(ui->quickWidget);
}
});
ui->stackedWidget->setCurrentWidget(ui->quickWidget);
auto *notificationsPollTimer = new QTimer(_accountState);
notificationsPollTimer->setInterval(1min);
notificationsPollTimer->start();
connect(notificationsPollTimer, &QTimer::timeout, this, &AccountSettings::updateNotifications);
}
void AccountSettings::slotToggleSignInState()
{
if (_accountState->isSignedOut()) {
_accountState->signIn();
} else {
_accountState->signOutByUi();
}
}
void AccountSettings::markNotificationsRead()
{
if (!_notifications.isEmpty()) {
auto *job = Notification::dismissAllNotifications(_accountState->account(), _notifications, this);
connect(job, &JsonApiJob::finishedSignal, this, &AccountSettings::updateNotifications);
job->start();
}
}
void AccountSettings::showSelectiveSyncDialog(Folder *folder)
{
auto *selectiveSync = new SelectiveSyncWidget(_accountState->account(), this);
selectiveSync->setDavUrl(folder->webDavUrl());
bool ok;
selectiveSync->setFolderInfo(folder->displayName(), folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok));
Q_ASSERT(ok);
auto *modalWidget = new AccountModalWidget(tr("Choose what to sync"), selectiveSync, this);
modalWidget->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
connect(modalWidget, &AccountModalWidget::accepted, this, [selectiveSync, folder, this] {
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, selectiveSync->createBlackList());
doForceSyncCurrentFolder(folder);
});
addModalWidget(modalWidget);
}
void AccountSettings::slotAddFolder()
{
FolderMan::instance()->setSyncEnabled(false); // do not start more syncs.
FolderWizard *folderWizard = new FolderWizard(_accountState, this);
folderWizard->setAttribute(Qt::WA_DeleteOnClose);
connect(folderWizard, &QDialog::accepted, this, &AccountSettings::slotFolderWizardAccepted);
connect(folderWizard, &QDialog::rejected, this, [] {
qCInfo(lcAccountSettings) << u"Folder wizard cancelled";
FolderMan::instance()->setSyncEnabled(true);
});
addModalLegacyDialog(folderWizard, AccountSettings::ModalWidgetSizePolicy::Expanding);
}
void AccountSettings::slotFolderWizardAccepted()
{
FolderWizard *folderWizard = qobject_cast<FolderWizard *>(sender());
qCInfo(lcAccountSettings) << u"Folder wizard completed";
const auto config = folderWizard->result();
auto folder = FolderMan::instance()->addFolderFromFolderWizardResult(_accountState, config);
if (!config.selectiveSyncBlackList.isEmpty() && OC_ENSURE(folder && !config.useVirtualFiles)) {
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, config.selectiveSyncBlackList);
// The user already accepted the selective sync dialog. everything is in the white list
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, {QLatin1String("/")});
}
FolderMan::instance()->setSyncEnabled(true);
FolderMan::instance()->scheduleAllFolders();
}
void AccountSettings::slotRemoveCurrentFolder(Folder *folder)
{
// TODO: move to qml
qCInfo(lcAccountSettings) << u"Remove Folder " << folder->path();
auto messageBox = new FontIconMessageBox({Resources::FontIcon::DefaultGlyphes::Question}, tr("Confirm removal of Space"),
tr("<p>Do you really want to stop syncing the Space »%1«?</p>"
"<p><b>Note:</b> This will <b>not</b> delete any files.</p>")
.arg(folder->displayName()),
QMessageBox::NoButton, ocApp()->settingsDialog());
messageBox->setAttribute(Qt::WA_DeleteOnClose);
QPushButton *yesButton = messageBox->addButton(tr("Remove Space"), QMessageBox::YesRole);
messageBox->addButton(tr("Cancel"), QMessageBox::NoRole);
connect(messageBox, &QMessageBox::finished, this, [messageBox, yesButton, folder, this] {
if (messageBox->clickedButton() == yesButton) {
FolderMan::instance()->removeFolder(folder);
QTimer::singleShot(0, this, &AccountSettings::slotSpacesUpdated);
}
});
messageBox->open();
}
void AccountSettings::showConnectionLabel(const QString &message, SyncResult::Status status, QStringList errors)
{
if (errors.isEmpty()) {
_connectionLabel = message;
} else {
errors.prepend(message);
const QString msg = errors.join(QLatin1String("\n"));
qCDebug(lcAccountSettings) << msg;
_connectionLabel = msg;
}
_accountStateIconGlype = SyncResult(status).glype();
Q_EMIT connectionLabelChanged();
}
void AccountSettings::slotEnableCurrentFolder(Folder *folder, bool terminate)
{
Q_ASSERT(folder);
qCInfo(lcAccountSettings) << u"Application: enable folder" << folder->path();
// this sets the folder status to disabled but does not interrupt it.
const bool currentlyPaused = folder->isSyncPaused();
if (!currentlyPaused && !terminate) {
// check if a sync is still running and if so, ask if we should terminate.
if (FolderMan::instance()->scheduler()->currentSync() == folder) { // it's still running
auto msgbox = new FontIconMessageBox(
{u''}, tr("Sync Running"), tr("The sync operation is running.<br/>Do you want to stop it?"), QMessageBox::Yes | QMessageBox::No, this);
msgbox->setAttribute(Qt::WA_DeleteOnClose);
msgbox->setDefaultButton(QMessageBox::Yes);
connect(msgbox, &QMessageBox::accepted, this, [folder = QPointer<Folder>(folder), this] {
if (folder) {
slotEnableCurrentFolder(folder, true);
}
});
msgbox->open();
return;
}
}
if (currentlyPaused) {
// ensure we don't forget about local errors
folder->slotNextSyncFullLocalDiscovery();
folder->setSyncPaused(false);
FolderMan::instance()->scheduler()->enqueueFolder(folder);
} else {
// set paused to prevent reschedule
folder->setSyncPaused(true);
// terminate the run if we previously where syncing
if (FolderMan::instance()->scheduler()->currentSync() == folder && terminate) {
FolderMan::instance()->scheduler()->terminateCurrentSync(tr("Sync paused by user"));
}
}
_model->slotUpdateFolderState(folder);
}
void AccountSettings::slotForceSyncCurrentFolder(Folder *folder)
{
if (NetworkInformation::instance()->isMetered() && ConfigFile().pauseSyncWhenMetered()) {
auto messageBox = new FontIconMessageBox({Resources::FontIcon::DefaultGlyphes::Question}, tr("Internet connection is metered"),
tr("Synchronization is paused because the Internet connection is a metered connection"
"<p>Do you really want to force a Synchronization now?"),
QMessageBox::Yes | QMessageBox::No, ocApp()->settingsDialog());
messageBox->setAttribute(Qt::WA_DeleteOnClose);
connect(messageBox, &QMessageBox::accepted, this, [folder = QPointer<Folder>(folder), this] {
if (folder) {
doForceSyncCurrentFolder(folder);
}
});
ocApp()->showSettings();
messageBox->open();
} else {
doForceSyncCurrentFolder(folder);
}
}
void AccountSettings::doForceSyncCurrentFolder(Folder *selectedFolder)
{
// Prevent new sync starts
FolderMan::instance()->scheduler()->stop();
// Terminate and reschedule any running sync
if (auto *currentSync = FolderMan::instance()->scheduler()->currentSync()) {
FolderMan::instance()->scheduler()->terminateCurrentSync(tr("User triggered force sync"));
FolderMan::instance()->scheduler()->enqueueFolder(currentSync);
}
selectedFolder->slotWipeErrorBlacklist(); // issue #6757
selectedFolder->slotNextSyncFullLocalDiscovery(); // ensure we don't forget about local errors
// Insert the selected folder at the front of the queue
FolderMan::instance()->scheduler()->enqueueFolder(selectedFolder, SyncScheduler::Priority::High);
// Restart scheduler
FolderMan::instance()->scheduler()->start();
}
void AccountSettings::updateNotifications()
{
if (_accountState->isConnected()) {
auto *job = Notification::createNotificationsJob(_accountState->account(), this);
connect(job, &JsonApiJob::finishedSignal, this, [job, this] {
const auto oldNotifications = _notifications;
_notifications = Notification::getNotifications(job);
auto newNotifications = _notifications;
newNotifications.subtract(oldNotifications);
for (const auto &notification : newNotifications) {
SystemNotificationRequest notificationRequest(notification.title, notification.message, Resources::FontIcon(u''));
notificationRequest.setButtons({tr("Mark as read")});
if (auto *n = ocApp()->systemNotificationManager()->notify(std::move(notificationRequest))) {
connect(n, &SystemNotification::buttonClicked, this, [notification, this](const QString &) {
// we only have one button so we don't need to check which was clicked
auto *job = Notification::dismissAllNotifications(_accountState->account(), {notification}, this);
connect(job, &JsonApiJob::finishedSignal, this, &AccountSettings::updateNotifications);
job->start();
});
}
}
Q_EMIT notificationsChanged();
});
job->start();
}
}
void AccountSettings::slotAccountStateChanged()
{
const AccountState::State state = _accountState->state();
const AccountPtr account = _accountState->account();
qCDebug(lcAccountSettings) << u"Account state changed to" << state << u"for account" << account;
FolderMan *folderMan = FolderMan::instance();
for (auto *folder : folderMan->folders()) {
_model->slotUpdateFolderState(folder);
}
switch (state) {
case AccountState::Connected: {
QStringList errors;
auto icon = SyncResult::Success;
if (account->serverSupportLevel() != Account::ServerSupportLevel::Supported) {
errors << tr("The server version %1 is unsupported! Proceed at your own risk.").arg(account->capabilities().status().versionString());
icon = SyncResult::Problem;
}
showConnectionLabel(tr("Connected"), icon, errors);
connect(
accountsState()->account()->spacesManager(), &GraphApi::SpacesManager::updated, this, &AccountSettings::slotSpacesUpdated, Qt::UniqueConnection);
slotSpacesUpdated();
updateNotifications();
break;
}
case AccountState::SignedOut:
showConnectionLabel(tr("Signed out"), SyncResult::Offline);
break;
case AccountState::Connecting:
if (NetworkInformation::instance()->isBehindCaptivePortal()) {
showConnectionLabel(tr("Captive portal prevents connections to the server."), SyncResult::Offline);
} else if (NetworkInformation::instance()->isMetered() && ConfigFile().pauseSyncWhenMetered()) {
showConnectionLabel(tr("Sync is paused due to metered internet connection"), SyncResult::Offline);
} else {
showConnectionLabel(tr("Connecting..."), SyncResult::Undefined);
}
break;
case AccountState::Disconnected:
showConnectionLabel(tr("Disconnected"), SyncResult::Offline);
break;
}
}
void AccountSettings::slotSpacesUpdated()
{
const uint64_t enabledSpaces = std::ranges::count_if(accountsState()->account()->spacesManager()->spaces(), [](const auto *s) { return !s->disabled(); });
const uint64_t syncedSpaces = std::ranges::count_if(
FolderMan::instance()->folders(), [this](const auto *f) { return f->accountState() == accountsState() && f->space() && !f->space()->disabled(); });
const auto unsyncedSpaces = enabledSpaces - syncedSpaces;
if (_unsyncedSpaces != unsyncedSpaces) {
_unsyncedSpaces = unsyncedSpaces;
Q_EMIT unsyncedSpacesChanged();
}
if (_syncedSpaces != syncedSpaces) {
_syncedSpaces = syncedSpaces;
Q_EMIT syncedSpacesChanged();
}
}
AccountSettings::~AccountSettings()
{
_goingDown = true;
delete ui;
}
void AccountSettings::addModalLegacyDialog(QWidget *widget, ModalWidgetSizePolicy sizePolicy)
{
if (!widget->testAttribute(Qt::WA_DeleteOnClose)) { // DEBUG CODE! See https://github.com/owncloud/client/issues/11673
// Early check to see if the attribute gets unset before the second/real check below
qCWarning(lcAccountSettings) << u"Missing WA_DeleteOnClose! (1)" << widget->metaObject() << widget;
}
// create a widget filling the stacked widget
// this widget contains a wrapping group box with widget as content
auto *outerWidget = new QWidget;
auto *groupBox = new QGroupBox;
switch (sizePolicy) {
case ModalWidgetSizePolicy::Expanding: {
auto *outerLayout = new QHBoxLayout(outerWidget);
outerLayout->setContentsMargins(modalWidgetStretchedMarginC, modalWidgetStretchedMarginC, modalWidgetStretchedMarginC, modalWidgetStretchedMarginC);
outerLayout->addWidget(groupBox);
auto *layout = new QHBoxLayout(groupBox);
layout->addWidget(widget);
} break;
case ModalWidgetSizePolicy::Minimum: {
auto *outerLayout = new QGridLayout(outerWidget);
outerLayout->addWidget(groupBox, 0, 0, Qt::AlignCenter);
auto *layout = new QHBoxLayout(groupBox);
layout->addWidget(widget);
} break;
}
groupBox->setTitle(widget->windowTitle());
ui->stackedWidget->addWidget(outerWidget);
ui->stackedWidget->setCurrentWidget(outerWidget);
// the widget is supposed to behave like a dialog and we connect to its destuction
if (!widget->testAttribute(Qt::WA_DeleteOnClose)) { // DEBUG CODE! See https://github.com/owncloud/client/issues/11673
qCWarning(lcAccountSettings) << u"Missing WA_DeleteOnClose! (2)" << widget->metaObject() << widget;
}
Q_ASSERT(widget->testAttribute(Qt::WA_DeleteOnClose));
connect(widget, &QWidget::destroyed, this, [this, outerWidget] {
outerWidget->deleteLater();
if (!_goingDown) {
ocApp()->settingsDialog()->ceaseModality(_accountState->account().get());
}
});
widget->setVisible(true);
ocApp()->settingsDialog()->requestModality(_accountState->account().get());
}
void AccountSettings::addModalWidget(AccountModalWidget *widget)
{
ui->stackedWidget->addWidget(widget);
ui->stackedWidget->setCurrentWidget(widget);
connect(widget, &AccountModalWidget::finished, this, [widget, this] {
widget->deleteLater();
ocApp()->settingsDialog()->ceaseModality(_accountState->account().get());
});
ocApp()->settingsDialog()->requestModality(_accountState->account().get());
}
uint64_t AccountSettings::unsyncedSpaces() const
{
return _unsyncedSpaces;
}
uint64_t AccountSettings::syncedSpaces() const
{
return _syncedSpaces;
}
auto AccountSettings::model() const
{
return _sortModel;
}
QString AccountSettings::connectionLabel()
{
return _connectionLabel;
}
QChar AccountSettings::accountStateIconGlype()
{
return _accountStateIconGlype;
}
const QSet<Notification> &AccountSettings::notifications() const
{
return _notifications;
}
void AccountSettings::slotDeleteAccount()
{
// Deleting the account potentially deletes 'this', so
// the QMessageBox should be destroyed before that happens.
auto messageBox = new FontIconMessageBox({Resources::FontIcon::DefaultGlyphes::Question}, tr("Confirm Account Removal"),
tr("<p>Do you really want to remove the connection to the account %1«?</p>"
"<p><b>Note:</b> This will <b>not</b> delete any files.</p>")
.arg(_accountState->account()->displayNameWithHost()),
QMessageBox::NoButton, this);
auto yesButton = messageBox->addButton(tr("Remove connection"), QMessageBox::YesRole);
messageBox->addButton(tr("Cancel"), QMessageBox::NoRole);
messageBox->setAttribute(Qt::WA_DeleteOnClose);
connect(messageBox, &QMessageBox::finished, this, [this, messageBox, yesButton]{
if (messageBox->clickedButton() == yesButton) {
auto manager = AccountManager::instance();
// sign out to stop new syncs from being scheduled
_accountState->signOutByUi();
if (FolderMan::instance()->scheduler()->currentSync() && FolderMan::instance()->scheduler()->currentSync()->accountState() == _accountState) {
// the message is usually not user visible
FolderMan::instance()->scheduler()->terminateCurrentSync(u"Account about to be removed"_s);
}
manager->deleteAccount(_accountState);
}
});
messageBox->open();
}
} // namespace OCC
+131
View File
@@ -0,0 +1,131 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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.
*/
#pragma once
#include "gui/qsferaguilib.h"
#include "folder.h"
#include "gui/notifications.h"
#include "gui/qmlutils.h"
#include "progressdispatcher.h"
#include <QSortFilterProxyModel>
#include <QWidget>
class QModelIndex;
class QNetworkReply;
class QLabel;
namespace OCC {
class AccountModalWidget;
namespace Ui {
class AccountSettings;
}
class FolderMan;
class Account;
class AccountState;
class FolderStatusModel;
class FolderStatusDelegate;
/**
* @brief The AccountSettings class
* @ingroup gui
*/
class QSFERA_GUI_EXPORT AccountSettings : public QWidget
{
Q_OBJECT
Q_PROPERTY(AccountState *accountState MEMBER _accountState CONSTANT)
Q_PROPERTY(QSortFilterProxyModel *model MEMBER _sortModel CONSTANT)
Q_PROPERTY(uint64_t unsyncedSpaces READ unsyncedSpaces NOTIFY unsyncedSpacesChanged)
Q_PROPERTY(uint64_t syncedSpaces READ syncedSpaces NOTIFY syncedSpacesChanged)
Q_PROPERTY(QString connectionLabel READ connectionLabel NOTIFY connectionLabelChanged)
Q_PROPERTY(QChar accountStateIconGlype READ accountStateIconGlype NOTIFY connectionLabelChanged)
Q_PROPERTY(QSet<Notification> notifications READ notifications NOTIFY notificationsChanged)
OC_DECLARE_WIDGET_FOCUS
QML_ELEMENT
QML_UNCREATABLE("C++ only")
public:
enum class ModalWidgetSizePolicy { Minimum = QSizePolicy::Minimum, Expanding = QSizePolicy::Expanding };
Q_ENUM(ModalWidgetSizePolicy)
explicit AccountSettings(const AccountStatePtr &accountState, QWidget *parent = nullptr);
~AccountSettings() override;
AccountStatePtr accountsState() const { return _accountState; }
void addModalLegacyDialog(QWidget *widget, ModalWidgetSizePolicy sizePolicy);
void addModalWidget(AccountModalWidget *widget);
uint64_t unsyncedSpaces() const;
uint64_t syncedSpaces() const;
auto model() const;
QString connectionLabel();
QChar accountStateIconGlype();
const QSet<Notification> &notifications() const;
Q_SIGNALS:
void showIssuesList();
void unsyncedSpacesChanged();
void syncedSpacesChanged();
void connectionLabelChanged();
void notificationsChanged();
public Q_SLOTS:
void slotAccountStateChanged();
void slotSpacesUpdated();
protected Q_SLOTS:
void slotAddFolder();
void slotEnableCurrentFolder(Folder *folder, bool terminate = false);
void slotForceSyncCurrentFolder(Folder *folder);
void slotRemoveCurrentFolder(Folder *folder);
void showSelectiveSyncDialog(Folder *folder);
void slotFolderWizardAccepted();
void slotDeleteAccount();
void slotToggleSignInState();
void markNotificationsRead();
private:
void showConnectionLabel(const QString &message, SyncResult::Status status, QStringList errors = {});
void doForceSyncCurrentFolder(Folder *selectedFolder);
void updateNotifications();
Ui::AccountSettings *ui;
FolderStatusModel *_model;
QSortFilterProxyModel *_sortModel;
AccountStatePtr _accountState;
// are we already in the destructor
bool _goingDown = false;
uint64_t _syncedSpaces = 0;
uint64_t _unsyncedSpaces = 0;
QString _connectionLabel;
QChar _accountStateIconGlype;
QSet<Notification> _notifications;
QPointer<QWidget> _updateUrlDialog;
};
} // namespace OCC
+109
View File
@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::AccountSettings</class>
<widget class="QWidget" name="OCC::AccountSettings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>678</width>
<height>557</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>5</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="loadingPage">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Preparing the account</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QProgressIndicator" name="spinner" native="true"/>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="OCC::QmlUtils::OCQuickWidget" name="quickWidget">
<property name="accessibleName">
<string>Sync connections</string>
</property>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>OCC::QmlUtils::OCQuickWidget</class>
<extends>QWidget</extends>
<header>gui/qmlutils.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QProgressIndicator</class>
<extends>QWidget</extends>
<header>QProgressIndicator.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
+541
View File
@@ -0,0 +1,541 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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.
*/
#include "accountstate.h"
#include "account.h"
#include "application.h"
#include "configfile.h"
#include "fetchserversettings.h"
#include "fonticon.h"
#include "guiutility.h"
#include "libsync/creds/abstractcredentials.h"
#include "libsync/creds/httpcredentials.h"
#include "gui/folderman.h"
#include "gui/fonticonmessagebox.h"
#include "gui/networkinformation.h"
#include "gui/settingsdialog.h"
#include "gui/tlserrordialog.h"
#include "logger.h"
#include "socketapi/socketapi.h"
#include "theme.h"
#include <QMessageBox>
#include <QRandomGenerator>
#include <QSettings>
#include <QTimer>
using namespace std::chrono;
using namespace std::chrono_literals;
namespace {
// How often we check the server for changed settings
auto fetchSettingsTimeout = 1h;
const QLatin1String userExplicitlySignedOutC()
{
return QLatin1String("userExplicitlySignedOut");
}
} // anonymous namespace
namespace OCC {
Q_LOGGING_CATEGORY(lcAccountState, "gui.account.state", QtInfoMsg)
AccountState::AccountState(AccountPtr account)
: QObject()
, _account(account)
, _queueGuard(_account->jobQueue())
, _state(AccountState::Disconnected)
, _connectionStatus(ConnectionValidator::Undefined)
, _waitingForNewCredentials(false)
{
qRegisterMetaType<AccountState *>("AccountState*");
connect(account.data(), &Account::invalidCredentials,
this, &AccountState::slotInvalidCredentials);
connect(account.data(), &Account::credentialsFetched,
this, &AccountState::slotCredentialsFetched);
connect(account.data(), &Account::credentialsAsked,
this, &AccountState::slotCredentialsAsked);
connect(account.data(), &Account::unknownConnectionState, this, [this] { checkConnectivity(true); });
connect(account.data(), &Account::capabilitiesChanged, this, [this] {
if (_account->capabilities().checkForUpdates() && isOcApp()) {
ocApp()->updateNotifier()->checkForUpdates(_account);
}
});
connect(NetworkInformation::instance(), &NetworkInformation::reachabilityChanged, this, [this](NetworkInformation::Reachability reachability) {
switch (reachability) {
case NetworkInformation::Reachability::Online:
[[fallthrough]];
case NetworkInformation::Reachability::Site:
[[fallthrough]];
case NetworkInformation::Reachability::Unknown:
// the connection might not yet be established
QTimer::singleShot(0, this, [this] { checkConnectivity(false); });
break;
case NetworkInformation::Reachability::Disconnected:
// explicitly set disconnected, this way a successful checkConnectivity call above will trigger a local discover
if (state() != State::SignedOut) {
setState(State::Disconnected);
}
[[fallthrough]];
case NetworkInformation::Reachability::Local:
break;
}
});
connect(NetworkInformation::instance(), &NetworkInformation::isMeteredChanged, this, [this](bool isMetered) {
if (ConfigFile().pauseSyncWhenMetered()) {
if (state() == State::Connected && isMetered) {
qCInfo(lcAccountState) << u"Network switched to a metered connection, setting account state to PausedDueToMetered";
setState(State::Connecting);
} else if (state() == State::Connecting && !isMetered) {
qCInfo(lcAccountState) << u"Network switched to a NON-metered connection, setting account state to Connected";
setState(State::Connected);
}
}
});
connect(NetworkInformation::instance(), &NetworkInformation::isBehindCaptivePortalChanged, this, [this](bool onoff) {
if (onoff) {
// Block jobs from starting: they will fail because of the captive portal.
// Note: this includes the `Drives` jobs started periodically by the `SpacesManager`.
_queueGuard.block();
} else {
// Empty the jobs queue before unblocking it. The client might have been behind a captive
// portal for hours, so a whole bunch of jobs might have queued up. If we wouldn't
// clear the queue, unleashing all those jobs might look like a DoS attack. Most of them
// are also not very useful anymore (e.g. `Drives` jobs), and the important ones (PUT jobs)
// will be rescheduled by a directory scan.
_account->jobQueue()->clear();
_queueGuard.unblock();
}
// A direct connect is not possible, because then the state parameter of `isBehindCaptivePortalChanged`
// would become the `verifyServerState` argument to `checkConnectivity`.
// The call is also made for when we "go behind" a captive portal. That ensures that not
// only the status is set to `Connecting`, but also makes the UI show that syncing is paused.
QTimer::singleShot(0, this, [this] { checkConnectivity(false); });
});
if (NetworkInformation::instance()->isBehindCaptivePortal()) {
_queueGuard.block();
}
// as a fallback and to recover after server issues we also poll
auto timer = new QTimer(this);
timer->setInterval(ConnectionValidator::DefaultCallingInterval);
connect(timer, &QTimer::timeout, this, [this] { checkConnectivity(false); });
timer->start();
connect(account->credentials(), &AbstractCredentials::requestLogout, this, [this] {
setState(State::SignedOut);
});
if (FolderMan::instance()) {
FolderMan::instance()->socketApi()->registerAccount(account);
}
connect(account.data(), &Account::appProviderErrorOccured, this, [](const QString &error) {
QMessageBox *msgBox =
new FontIconMessageBox({Resources::FontIcon::DefaultGlyphes::Warning}, Theme::instance()->appNameGUI(), error, {}, ocApp()->settingsDialog());
msgBox->setAttribute(Qt::WA_DeleteOnClose);
ocApp()->showSettings();
msgBox->open();
});
}
AccountState::~AccountState() { }
std::unique_ptr<AccountState> AccountState::loadFromSettings(AccountPtr account, const QSettings &settings)
{
auto accountState = std::unique_ptr<AccountState>(new AccountState(account));
const bool userExplicitlySignedOut = settings.value(userExplicitlySignedOutC(), false).toBool();
if (userExplicitlySignedOut) {
// see writeToSettings below
accountState->setState(SignedOut);
}
return accountState;
}
std::unique_ptr<AccountState> AccountState::fromNewAccount(AccountPtr account)
{
return std::unique_ptr<AccountState>(new AccountState(account));
}
void AccountState::writeToSettings(QSettings &settings) const
{
// The SignedOut state is the only state where the client should *not* ask for credentials, nor
// try to connect to the server. All other states should transition to Connected by either
// (re-)trying to make a connection, or by authenticating (AskCredentials). So we save the
// SignedOut state to indicate that the client should not try to re-connect the next time it
// is started.
settings.setValue(userExplicitlySignedOutC(), _state == SignedOut);
}
AccountPtr AccountState::account() const
{
return _account;
}
ConnectionValidator::Status AccountState::connectionStatus() const
{
return _connectionStatus;
}
QStringList AccountState::connectionErrors() const
{
return _connectionErrors;
}
AccountState::State AccountState::state() const
{
return _state;
}
void AccountState::setState(State state)
{
const bool stateHasChanged = state != _state;
if (stateHasChanged) {
const State oldState = _state;
qCInfo(lcAccountState) << u"AccountState state change: " << _state << u"->" << state;
_state = state;
if (_state == SignedOut) {
_connectionStatus = ConnectionValidator::Undefined;
_connectionErrors.clear();
} else if (oldState == SignedOut && _state == Disconnected) {
// If we stop being voluntarily signed-out, try to connect and
// auth right now!
checkConnectivity();
} else if (_state == Connected) {
if ((NetworkInformation::instance()->isMetered() && ConfigFile().pauseSyncWhenMetered())
|| NetworkInformation::instance()->isBehindCaptivePortal()) {
_state = Connecting;
}
}
}
// might not have changed but the underlying _connectionErrors might have
if (state == Connected) {
QTimer::singleShot(0, this, [stateHasChanged, this] {
// ensure the connection validator is done
_queueGuard.unblock();
// update capabilites and fetch relevant settings
if (!_fetchServerSettingsJob && _fetchCapabilitiesElapsedTimer.duration() > fetchSettingsTimeout) {
_fetchServerSettingsJob = new FetchServerSettingsJob(account(), this);
connect(_fetchServerSettingsJob, &FetchServerSettingsJob::finishedSignal, this, [stateHasChanged, this] {
_fetchServerSettingsJob->deleteLater();
// clear the guard to make readyForSync return true
_fetchServerSettingsJob.clear();
_fetchCapabilitiesElapsedTimer.reset();
if (stateHasChanged) {
Q_EMIT isConnectedChanged();
}
});
_fetchServerSettingsJob->start();
} else if (stateHasChanged) {
Q_EMIT isConnectedChanged();
}
});
} else if (stateHasChanged) {
Q_EMIT isConnectedChanged();
}
if (stateHasChanged) {
Q_EMIT stateChanged(_state);
}
}
bool AccountState::isSignedOut() const
{
return _state == SignedOut;
}
void AccountState::signOutByUi()
{
account()->credentials()->forgetSensitiveData();
account()->clearCookieJar();
setState(SignedOut);
// persist that we are signed out
Q_EMIT account()->wantsAccountSaved(account().data());
}
void AccountState::signIn()
{
if (_state == SignedOut) {
_waitingForNewCredentials = false;
setState(Disconnected);
// persist that we are no longer signed out
Q_EMIT account()->wantsAccountSaved(account().data());
}
}
bool AccountState::isConnected() const
{
return _state == Connected;
}
void AccountState::tagLastSuccessfullETagRequest(const QDateTime &tp)
{
_timeOfLastETagCheck = tp;
}
void AccountState::checkConnectivity(bool blockJobs)
{
if (isSignedOut() || _waitingForNewCredentials) {
return;
}
qCInfo(lcAccountState) << u"checkConnectivity blocking:" << blockJobs << account()->displayNameWithHost();
if (_state != Connected) {
setState(Connecting);
}
if (_tlsDialog) {
qCDebug(lcAccountState) << u"Skip checkConnectivity, waiting for tls dialog";
return;
}
if (_connectionValidator && blockJobs && !_queueGuard.queue()->isBlocked()) {
// abort already running non blocking validator
_connectionValidator->deleteLater();
_connectionValidator.clear();
}
if (_connectionValidator) {
qCWarning(lcAccountState) << u"ConnectionValidator already running, ignoring" << account()->displayNameWithHost() << u"Queue is blocked:"
<< _queueGuard.queue()->isBlocked();
return;
}
// If we never fetched credentials, do that now - otherwise connection attempts
// make little sense.
if (!account()->credentials()->wasFetched()) {
_waitingForNewCredentials = true;
account()->credentials()->fetchFromKeychain();
}
if (account()->hasCapabilities()) {
// IF the account is connected the connection check can be skipped
// if the last successful etag check job is not so long ago.
// TODO: https://github.com/owncloud/client/issues/10935
const auto pta = account()->capabilities().remotePollInterval();
const auto polltime = duration_cast<seconds>(ConfigFile().remotePollInterval(pta));
const auto elapsed = _timeOfLastETagCheck.secsTo(QDateTime::currentDateTimeUtc());
if (!blockJobs && isConnected() && _timeOfLastETagCheck.isValid()
&& elapsed <= polltime.count()) {
qCDebug(lcAccountState) << account()->displayNameWithHost() << u"The last ETag check succeeded within the last " << polltime.count() << u"s ("
<< elapsed << u"s). No connection check needed!";
return;
}
}
if (blockJobs) {
_queueGuard.block();
}
_connectionValidator = new ConnectionValidator(account());
connect(_connectionValidator, &ConnectionValidator::connectionResult,
this, &AccountState::slotConnectionValidatorResult);
connect(_connectionValidator, &ConnectionValidator::sslErrors, this, [blockJobs, this](const QList<QSslError> &errors) {
if (NetworkInformation::instance()->isBehindCaptivePortal()) {
return;
}
if (!_tlsDialog) {
// ignore errors for already accepted certificates
auto filteredErrors = _account->accessManager()->filterSslErrors(errors);
if (!filteredErrors.isEmpty()) {
_tlsDialog = new TlsErrorDialog(filteredErrors, _account->url().host(), ocApp()->settingsDialog());
_tlsDialog->setAttribute(Qt::WA_DeleteOnClose);
QSet<QSslCertificate> certs;
certs.reserve(filteredErrors.size());
for (const auto &error : std::as_const(filteredErrors)) {
certs << error.certificate();
}
connect(_tlsDialog, &TlsErrorDialog::accepted, _tlsDialog, [certs, blockJobs, this]() {
_account->addApprovedCerts(certs);
_tlsDialog.clear();
// force a new _connectionValidator
if (_connectionValidator) {
_connectionValidator->deleteLater();
_connectionValidator.clear();
}
checkConnectivity(blockJobs);
});
connect(_tlsDialog, &TlsErrorDialog::rejected, this, [certs, this]() {
setState(SignedOut);
});
ocApp()->showSettings();
_tlsDialog->open();
}
}
});
ConnectionValidator::ValidationMode mode = ConnectionValidator::ValidationMode::ValidateAuthAndUpdate;
if (isConnected()) {
// Use a small authed propfind as a minimal ping when we're
// already connected.
if (blockJobs) {
_connectionValidator->setClearCookies(true);
}
mode = ConnectionValidator::ValidationMode::ValidateAuthAndUpdate;
} else {
// Check the server and then the auth.
if (_waitingForNewCredentials) {
mode = ConnectionValidator::ValidationMode::ValidateServer;
} else {
_connectionValidator->setClearCookies(true);
mode = ConnectionValidator::ValidationMode::ValidateAuthAndUpdate;
}
}
_connectionValidator->checkServer(mode);
}
void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status status, const QStringList &errors)
{
if (isSignedOut()) {
qCWarning(lcAccountState) << u"Signed out, ignoring" << status << _account->url().toString();
return;
}
if (status == ConnectionValidator::Connected && !_account->hasCapabilities()) {
// this code should only be needed when upgrading from a < 3.0 release where capabilities where not cached
// The last check was _waitingForNewCredentials = true so we only checked ValidateServer
// now check again and fetch capabilities
_connectionValidator->deleteLater();
_connectionValidator.clear();
checkConnectivity();
return;
}
if (_connectionStatus != status) {
qCInfo(lcAccountState) << u"AccountState connection status change: " << _connectionStatus << u"->" << status;
_connectionStatus = status;
}
_connectionErrors = errors;
switch (status) {
case ConnectionValidator::Connected:
setState(Connected);
break;
case ConnectionValidator::Undefined:
[[fallthrough]];
case ConnectionValidator::ServiceUnavailable:
[[fallthrough]];
case ConnectionValidator::Timeout:
[[fallthrough]];
case ConnectionValidator::StatusNotFound:
// This can happen either because the server does not exist
// or because we are having network issues. The latter one is
// much more likely, so keep trying to connect.
setState(Disconnected);
break;
case ConnectionValidator::CredentialsWrong:
[[fallthrough]];
case ConnectionValidator::CredentialsNotReady:
slotInvalidCredentials();
break;
case ConnectionValidator::SslError:
// handled with the tlsDialog
break;
case ConnectionValidator::CaptivePortal:
setState(Connecting);
break;
}
}
void AccountState::slotInvalidCredentials()
{
if (!_waitingForNewCredentials) {
qCInfo(lcAccountState) << u"Invalid credentials for" << _account->url().toString();
_waitingForNewCredentials = true;
if (account()->credentials()->ready()) {
account()->credentials()->invalidateToken();
}
if (auto creds = qobject_cast<HttpCredentials *>(account()->credentials())) {
qCInfo(lcAccountState) << u"refreshing oauth";
if (creds->refreshAccessToken()) {
return;
}
qCInfo(lcAccountState) << u"refreshing oauth failed";
}
qCInfo(lcAccountState) << u"restart oauth";
account()->credentials()->restartOauth();
setState(Connecting);
}
}
void AccountState::slotCredentialsFetched()
{
// Make a connection attempt, no matter whether the credentials are
// ready or not - we want to check whether we can get an SSL connection
// going before bothering the user for a password.
qCInfo(lcAccountState) << u"Fetched credentials for" << _account->url().toString() << u"attempting to connect";
_waitingForNewCredentials = false;
checkConnectivity();
}
void AccountState::slotCredentialsAsked()
{
qCInfo(lcAccountState) << u"Credentials asked for" << _account->url().toString() << u"are they ready?" << _account->credentials()->ready();
_waitingForNewCredentials = false;
if (!_account->credentials()->ready()) {
// User canceled the connection or did not give a password
setState(SignedOut);
return;
}
if (_connectionValidator) {
// When new credentials become available we always want to restart the
// connection validation, even if it's currently running.
_connectionValidator->deleteLater();
_connectionValidator.clear();
}
checkConnectivity();
}
Account *AccountState::accountForQml() const
{
return _account.data();
}
bool AccountState::isSettingUp() const
{
return _settingUp;
}
void AccountState::setSettingUp(bool settingUp)
{
if (_settingUp != settingUp) {
_settingUp = settingUp;
Q_EMIT isSettingUpChanged();
}
}
bool AccountState::readyForSync() const
{
return !_fetchServerSettingsJob && isConnected();
}
} // namespace OCC
+160
View File
@@ -0,0 +1,160 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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.
*/
#pragma once
#include "gui/qsferaguilib.h"
#include "connectionvalidator.h"
#include "creds/abstractcredentials.h"
#include "jobqueue.h"
#include <QByteArray>
#include <QPointer>
#include <QtQmlIntegration/QtQmlIntegration>
#include <memory>
class QDialog;
class QMessageBox;
class QSettings;
namespace OCC {
class Account;
class TlsErrorDialog;
class FetchServerSettingsJob;
class QSFERA_GUI_EXPORT AccountState : public QObject
{
Q_OBJECT
Q_PROPERTY(Account *account READ accountForQml CONSTANT)
Q_PROPERTY(bool isConnected READ isConnected NOTIFY isConnectedChanged)
Q_PROPERTY(AccountState::State state READ state NOTIFY stateChanged)
QML_ELEMENT
QML_UNCREATABLE("Only created by AccountManager")
public:
enum State : uint8_t {
/// Not even attempting to connect, most likely because the
/// user explicitly signed out or cancelled a credential dialog.
SignedOut,
/// Account would like to be connected but hasn't heard back yet.
Disconnected,
/// The account is successfully talking to the server.
Connected,
Connecting
};
Q_ENUM(State)
~AccountState() override;
/** Creates an account state from settings and an Account object.
*
* Use from AccountManager with a prepared QSettings object only.
*/
static std::unique_ptr<AccountState> loadFromSettings(AccountPtr account, const QSettings &settings);
static std::unique_ptr<AccountState> fromNewAccount(AccountPtr account);
/** Writes account state information to settings.
*
* It does not write the Account data.
*/
void writeToSettings(QSettings &settings) const;
AccountPtr account() const;
ConnectionValidator::Status connectionStatus() const;
QStringList connectionErrors() const;
State state() const;
bool isSignedOut() const;
[[nodiscard]] bool readyForSync() const;
/** A user-triggered sign out which disconnects, stops syncs
* for the account and forgets the password. */
void signOutByUi();
/// Move from SignedOut state to Disconnected (attempting to connect)
void signIn();
bool isConnected() const;
/** Mark the timestamp when the last successful ETag check happened for
* this account.
* The checkConnectivity() method uses the timestamp to save a call to
* the server to validate the connection if the last successful etag job
* was not so long ago.
*/
void tagLastSuccessfullETagRequest(const QDateTime &tp);
/***
* The account is setup for the first time, this may take some time
*/
bool isSettingUp() const;
void setSettingUp(bool settingUp);
public Q_SLOTS:
/// Triggers a ping to the server to update state and
/// connection status and errors.
/// verifyServerState indicates that we must check the server
void checkConnectivity(bool verifyServerState = false);
private:
/// Use the account as parent
explicit AccountState(AccountPtr account);
void setState(State state);
Q_SIGNALS:
void stateChanged(State state);
void isConnectedChanged();
void isSettingUpChanged();
protected Q_SLOTS:
void slotConnectionValidatorResult(ConnectionValidator::Status status, const QStringList &errors);
void slotInvalidCredentials();
void slotCredentialsFetched();
void slotCredentialsAsked();
private:
Account *accountForQml() const;
AccountPtr _account;
JobQueueGuard _queueGuard;
State _state;
ConnectionValidator::Status _connectionStatus;
QStringList _connectionErrors;
bool _waitingForNewCredentials;
QDateTime _timeOfLastETagCheck;
QPointer<ConnectionValidator> _connectionValidator;
QPointer<TlsErrorDialog> _tlsDialog;
bool _settingUp = false;
Utility::ChronoElapsedTimer _fetchCapabilitiesElapsedTimer = {false};
// guard against multiple fetches
QPointer<FetchServerSettingsJob> _fetchServerSettingsJob;
};
}
Q_DECLARE_METATYPE(OCC::AccountState *)
Q_DECLARE_METATYPE(OCC::AccountStatePtr)
+66
View File
@@ -0,0 +1,66 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#include "activitywidget.h"
#include "issueswidget.h"
#include "protocolwidget.h"
#include "resources/fonticon.h"
#include "theme.h"
#include <QtGui>
#include <QtWidgets>
using namespace std::chrono;
using namespace std::chrono_literals;
namespace OCC {
ActivitySettings::ActivitySettings(QWidget *parent)
: QWidget(parent)
{
QHBoxLayout *hbox = new QHBoxLayout(this);
setLayout(hbox);
// create a tab widget for the three activity views
_tab = new QTabWidget(this);
hbox->addWidget(_tab);
_protocolWidget = new ProtocolWidget(this);
_tab->addTab(_protocolWidget, Resources::FontIcon(u''), tr("Local Activity"));
_issuesWidget = new IssuesWidget(this);
const int issueTabId = _tab->addTab(_issuesWidget, Resources::FontIcon(u''), tr("Not Synced"));
connect(_issuesWidget, &IssuesWidget::issueCountUpdated, this, [issueTabId, this](int issueCount) {
QString cntText = tr("Not Synced");
if (issueCount) {
//: %1 is the number of not synced files.
cntText = tr("Not Synced (%1)").arg(issueCount);
}
_tab->setTabText(issueTabId, cntText);
});
}
void ActivitySettings::slotShowIssuesTab()
{
_tab->setCurrentWidget(_issuesWidget);
}
ActivitySettings::~ActivitySettings()
{
}
}
+51
View File
@@ -0,0 +1,51 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#pragma once
#include "account.h"
#include <QWidget>
class QTabWidget;
namespace OCC {
class ProtocolWidget;
class IssuesWidget;
/**
* @brief The ActivitySettings class
* @ingroup gui
*
* Implements a tab for the settings dialog, displaying the three activity
* lists.
*/
class ActivitySettings : public QWidget
{
Q_OBJECT
public:
explicit ActivitySettings(QWidget *parent = nullptr);
~ActivitySettings() override;
public Q_SLOTS:
void slotShowIssuesTab();
private:
QTabWidget *_tab;
ProtocolWidget *_protocolWidget;
IssuesWidget *_issuesWidget;
};
}
+408
View File
@@ -0,0 +1,408 @@
/*
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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.
*/
#include "application.h"
#include "account.h"
#include "accountmanager.h"
#include "accountstate.h"
#include "common/version.h"
#include "configfile.h"
#include "folder.h"
#include "folderman.h"
#include "gui/aboutdialog.h"
#include "gui/accountsettings.h"
#include "gui/fetchserversettings.h"
#include "gui/folderwizard/folderwizard.h"
#include "gui/newwizard/setupwizardcontroller.h"
#include "gui/notifications/systemnotification.h"
#include "gui/notifications/systemnotificationmanager.h"
#include "gui/systray.h"
#include "libsync/graphapi/spacesmanager.h"
#include "libsync/vfs/vfs.h"
#include "resources/fonticon.h"
#include "settingsdialog.h"
#include "socketapi/socketapi.h"
#include "theme.h"
#ifdef WITH_AUTO_UPDATER
#include "updater/ocupdater.h"
#endif
#if defined(Q_OS_WIN)
#include "gui/navigationpanehelper.h"
#include <qt_windows.h>
#endif
#include <QApplication>
#include <QDesktopServices>
#include <QMenuBar>
using namespace Qt::Literals::StringLiterals;
using namespace OCC;
Q_LOGGING_CATEGORY(lcApplication, "gui.application", QtInfoMsg)
namespace {
void setUpInitialSyncFolder(AccountStatePtr accountStatePtr, bool useVfs)
{
// saves a bit of duplicate code
auto addFolder = [accountStatePtr, useVfs](const QString &localFolder, const QUrl &davUrl, const QString &spaceId = {}, const QString &displayName = {}) {
auto def = FolderDefinition{accountStatePtr->account()->uuid(), davUrl, spaceId, displayName};
def.setLocalPath(localFolder);
return FolderMan::instance()->addFolderFromWizard(accountStatePtr, std::move(def), useVfs);
};
auto finalize = [accountStatePtr] {
accountStatePtr->checkConnectivity();
FolderMan::instance()->setSyncEnabled(true);
FolderMan::instance()->scheduleAllFolders();
};
QObject::connect(
accountStatePtr->account()->spacesManager(), &GraphApi::SpacesManager::ready, accountStatePtr,
[accountStatePtr, addFolder, finalize] {
auto spaces = accountStatePtr->account()->spacesManager()->spaces();
// we do not want to set up folder sync connections for disabled spaces (#10173)
spaces.erase(std::remove_if(spaces.begin(), spaces.end(), [](auto *space) { return space->disabled(); }), spaces.end());
if (!spaces.isEmpty()) {
const QString localDir(accountStatePtr->account()->defaultSyncRoot());
FileSystem::setFolderMinimumPermissions(localDir);
Utility::setupFavLink(localDir);
for (const auto *space : spaces) {
const QString name = space->displayName();
const QString folderName = FolderMan::instance()->findGoodPathForNewSyncFolder(
localDir, name, FolderMan::NewFolderType::SpacesFolder, accountStatePtr->account()->uuid());
auto folder = addFolder(folderName, QUrl(space->drive().getRoot().getWebDavUrl()), space->drive().getRoot().getId(), name);
folder->setPriority(space->priority());
}
finalize();
}
},
Qt::SingleShotConnection);
accountStatePtr->account()->spacesManager()->checkReady();
}
}
QString Application::displayLanguage() const
{
return _displayLanguage;
}
Application *Application::_instance = nullptr;
Application::Application(const QString &displayLanguage, bool debugMode)
: _debugMode(debugMode)
, _displayLanguage(displayLanguage)
, _updateNotifier(new UpdateNotifier(this))
{
// ensure the singleton works
{
_instance = this;
_settingsDialog = new SettingsDialog();
_systray = new Systray(this);
_systemNotificationManager = new SystemNotificationManager(this);
}
qCInfo(lcApplication) << u"Plugin search paths:" << qApp->libraryPaths();
// Check vfs plugins
if (VfsPluginManager::instance().bestAvailableVfsMode() == Vfs::Mode::Off) {
qCWarning(lcApplication) << u"Theme wants to show vfs mode, but no vfs plugins are available";
}
qCInfo(lcApplication) << VfsPluginManager::instance().bestAvailableVfsMode() << u"plugin is available";
ConfigFile cfg;
// this should be called once during application startup to make sure we don't miss any messages
cfg.configureHttpLogging();
// The timeout is initialized with an environment variable, if not, override with the value from the config
if (AbstractNetworkJob::httpTimeout == AbstractNetworkJob::DefaultHttpTimeout) {
AbstractNetworkJob::httpTimeout = cfg.timeout();
}
qApp->setQuitOnLastWindowClosed(false);
connect(AccountManager::instance(), &AccountManager::accountAdded, this, &Application::slotAccountStateAdded);
for (const auto &ai : AccountManager::instance()->accounts()) {
slotAccountStateAdded(ai);
}
connect(_systemNotificationManager, &SystemNotificationManager::unknownNotificationClicked, this, &Application::showSettings);
connect(
_systemNotificationManager, &SystemNotificationManager::notificationFinished, this, [this](SystemNotification *, SystemNotification::Result result) {
if (result == SystemNotification::Result::Clicked) {
showSettings();
}
});
#ifdef WITH_AUTO_UPDATER
// Update checks
UpdaterScheduler *updaterScheduler = new UpdaterScheduler(this, this);
// the updater scheduler takes care of connecting its GUI bits to other components
(void)updaterScheduler;
#endif
// Cleanup at Quit.
connect(qApp, &QCoreApplication::aboutToQuit, this, &Application::slotCleanup);
#ifdef Q_OS_MAC
// add About to the global menu
QMenuBar *menuBar = new QMenuBar(nullptr);
// the menu name is not displayed
auto *menu = menuBar->addMenu(QString());
// the actual name is provided by mac
menu->addAction(QStringLiteral("About"), this, &Application::showAbout)->setMenuRole(QAction::AboutRole);
#endif
#ifdef Q_OS_WIN
// update the existing sidebar entries
NavigationPaneHelper::removeLegacyCloudStorageRegistry();
#endif
}
Application::~Application()
{
// Make sure all folders are gone, otherwise removing the
// accounts will remove the associated folders from the settings.
FolderMan::instance()->unloadAndDeleteAllFolders();
}
void Application::slotAccountStateAdded(AccountStatePtr accountState) const
{
connect(accountState->account().data(), &Account::serverVersionChanged, ocApp(), [account = accountState->account().data()] {
if (account->serverSupportLevel() != Account::ServerSupportLevel::Supported) {
ocApp()->systemNotificationManager()->notify({tr("Unsupported Server Version"),
tr("The server on account »%1« runs an unsupported version %2. "
"Using this client with unsupported server versions is untested and "
"potentially dangerous. Proceed at your own risk.")
.arg(account->displayNameWithHost(), account->capabilities().status().versionString()),
Resources::FontIcon(u'')});
}
});
// Hook up the folder manager slots to the account state's Q_SIGNALS:
connect(accountState.data(), &AccountState::isConnectedChanged, FolderMan::instance(), &FolderMan::slotIsConnectedChanged);
connect(accountState->account().data(), &Account::serverVersionChanged, FolderMan::instance(),
[account = accountState->account().data()] { FolderMan::instance()->slotServerVersionChanged(account); });
accountState->checkConnectivity();
}
void Application::slotCleanup()
{
ConfigFile().saveGeometry(_settingsDialog);
delete _settingsDialog;
// by now the credentials are supposed to be persisted
// don't start async credentials jobs during shutdown
AccountManager::instance()->save();
FolderMan::instance()->scheduler()->stop();
FolderMan::instance()->scheduler()->terminateCurrentSync(tr("Application is shutting down"));
FolderMan::instance()->unloadAndDeleteAllFolders();
// Remove the account from the account manager so it can be deleted.
AccountManager::instance()->shutdown();
}
AccountStatePtr Application::addNewAccount(AccountPtr newAccount)
{
auto *accountMan = AccountManager::instance();
// first things first: we need to add the new account
auto accountStatePtr = accountMan->addAccount(newAccount);
// if one account is configured: enable autostart
bool shouldSetAutoStart = (accountMan->accounts().size() == 1);
#ifdef Q_OS_MAC
// Don't auto start when not being 'installed'
shouldSetAutoStart = shouldSetAutoStart && QCoreApplication::applicationDirPath().startsWith(QLatin1String("/Applications/"));
#endif
if (shouldSetAutoStart) {
Utility::setLaunchOnStartup(Theme::instance()->appName(), Theme::instance()->appNameGUI(), true);
}
// showing the UI to show the user that the account has been added successfully
showSettings();
return accountStatePtr;
}
void Application::showSettings()
{
auto window = ocApp()->settingsDialog();
window->show();
window->raise();
window->activateWindow();
#if defined(Q_OS_WIN)
// Windows disallows raising a Window when you're not the active application.
// Use a common hack to attach to the active application
const auto activeProcessId = GetWindowThreadProcessId(GetForegroundWindow(), nullptr);
if (activeProcessId != qApp->applicationPid()) {
const auto threadId = GetCurrentThreadId();
// don't step here with a debugger...
if (AttachThreadInput(threadId, activeProcessId, true)) {
const auto hwnd = reinterpret_cast<HWND>(window->winId());
SetForegroundWindow(hwnd);
SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
AttachThreadInput(threadId, activeProcessId, false);
}
}
#endif
}
SettingsDialog *Application::settingsDialog() const
{
return _settingsDialog;
}
void Application::showAbout()
{
if (!_aboutDialog) {
_aboutDialog = new AboutDialog(_settingsDialog);
_aboutDialog->setAttribute(Qt::WA_DeleteOnClose);
_settingsDialog->addModalWidget(_aboutDialog);
}
}
SystemNotificationManager *Application::systemNotificationManager() const
{
return _systemNotificationManager;
}
void Application::runNewAccountWizard()
{
// passing the settings dialog as parent makes sure the wizard will be shown above it
// as the settingsDialog's lifetime spans across the entire application but the dialog will live much shorter,
// we have to clean it up manually when finished() is emitted
auto *wizardController = new Wizard::SetupWizardController(this->settingsDialog());
// while the wizard is shown, new syncs are disabled
FolderMan::instance()->setSyncEnabled(false);
connect(
wizardController, &Wizard::SetupWizardController::finished, this, [wizardController, this](const AccountPtr &newAccount, Wizard::SyncMode syncMode) {
// note: while the wizard is shown, we disable the folder synchronization
// previously we could perform this just here, but now we have to postpone this depending on whether selective sync was chosen
// see also #9497
// when the dialog is closed before it has finished, there won't be a new account to set up
// the wizard controller signalizes this by passing a null pointer
if (!newAccount.isNull()) {
// finally, call the slot that finalizes the setup
auto accountStatePtr = ocApp()->addNewAccount(newAccount);
accountStatePtr->setSettingUp(true);
_settingsDialog->setCurrentAccount(accountStatePtr->account().data());
// fetch server settings
auto fetchServerSettings = new FetchServerSettingsJob(accountStatePtr->account(), accountStatePtr->account().data());
connect(fetchServerSettings, &FetchServerSettingsJob::finishedSignal, accountStatePtr.data(), [accountStatePtr, syncMode, this] {
// saving once after adding makes sure the account is stored in the config in a working state
// this is needed to ensure a consistent state in the config file upon unexpected terminations of the client
// (for instance, when running from a debugger and stopping the process from there)
AccountManager::instance()->save();
// the account is now ready, emulate a normal account loading and Q_EMIT that the credentials are ready
Q_EMIT accountStatePtr->account()->credentialsFetched();
switch (syncMode) {
case Wizard::SyncMode::SyncEverything:
case Wizard::SyncMode::UseVfs: {
bool useVfs = syncMode == Wizard::SyncMode::UseVfs;
setUpInitialSyncFolder(accountStatePtr, useVfs);
accountStatePtr->setSettingUp(false);
break;
}
case Wizard::SyncMode::ConfigureUsingFolderWizard: {
Q_ASSERT(!accountStatePtr->account()->hasDefaultSyncRoot());
auto *folderWizard = new FolderWizard(accountStatePtr, ocApp()->settingsDialog());
folderWizard->setAttribute(Qt::WA_DeleteOnClose);
// TODO: duplication of AccountSettings
// adapted from AccountSettings::slotFolderWizardAccepted()
connect(folderWizard, &QDialog::accepted, accountStatePtr.data(), [accountStatePtr, folderWizard]() {
FolderMan *folderMan = FolderMan::instance();
qCInfo(lcApplication) << u"Folder wizard completed";
const auto config = folderWizard->result();
auto folder = folderMan->addFolderFromFolderWizardResult(accountStatePtr, config);
if (!config.selectiveSyncBlackList.isEmpty() && OC_ENSURE(folder && !config.useVirtualFiles)) {
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, config.selectiveSyncBlackList);
// The user already accepted the selective sync dialog. everything is in the white list
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, {QLatin1String("/")});
}
folderMan->setSyncEnabled(true);
folderMan->scheduleAllFolders();
accountStatePtr->setSettingUp(false);
});
connect(folderWizard, &QDialog::rejected, accountStatePtr.data(), [accountStatePtr]() {
qCInfo(lcApplication) << u"Folder wizard cancelled";
FolderMan::instance()->setSyncEnabled(true);
accountStatePtr->setSettingUp(false);
});
_settingsDialog->accountSettings(accountStatePtr->account().get())
->addModalLegacyDialog(folderWizard, AccountSettings::ModalWidgetSizePolicy::Expanding);
break;
}
case OCC::Wizard::SyncMode::Invalid:
Q_UNREACHABLE();
}
});
fetchServerSettings->start();
} else {
FolderMan::instance()->setSyncEnabled(true);
}
// make sure the wizard is cleaned up eventually
wizardController->deleteLater();
});
// all we have to do is show the dialog...
settingsDialog()->addModalWidget(wizardController->window());
}
QSystemTrayIcon *Application::systemTrayIcon() const
{
return _systray;
}
UpdateNotifier *Application::updateNotifier() const
{
return _updateNotifier;
}
bool Application::debugMode()
{
return _debugMode;
}
std::unique_ptr<Application> Application::createInstance(const QString &displayLanguage, bool debugMode)
{
Q_ASSERT(!_instance);
// _instance will be set in the constructor
new Application(displayLanguage, debugMode);
return std::unique_ptr<Application>(_instance);
}
+103
View File
@@ -0,0 +1,103 @@
/*
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
*
* 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.
*/
#pragma once
#include "gui/qsferaguilib.h"
#include "gui/updatenotifier.h"
#include "common/asserts.h"
#include "libsync/accountfwd.h"
#include <QPointer>
#include <QSystemTrayIcon>
namespace CrashReporter {
class Handler;
}
namespace OCC {
class SettingsDialog;
class AboutDialog;
class Systray;
class SystemNotificationManager;
class QSFERA_GUI_EXPORT Application : public QObject
{
Q_OBJECT
public:
static std::unique_ptr<Application> createInstance(const QString &displayLanguage, bool debugMode);
~Application();
bool debugMode();
QString displayLanguage() const;
AccountStatePtr addNewAccount(AccountPtr newAccount);
void showSettings();
SettingsDialog *settingsDialog() const;
void showAbout();
SystemNotificationManager *systemNotificationManager() const;
void runNewAccountWizard();
QSystemTrayIcon *systemTrayIcon() const;
UpdateNotifier *updateNotifier() const;
protected Q_SLOTS:
void slotCleanup();
void slotAccountStateAdded(AccountStatePtr accountState) const;
private:
explicit Application(const QString &displayLanguage, bool debugMode);
const bool _debugMode = false;
SettingsDialog *_settingsDialog = nullptr;
QString _displayLanguage;
Systray *_systray;
UpdateNotifier *_updateNotifier;
SystemNotificationManager *_systemNotificationManager = nullptr;
// keeping a pointer on those dialogs allows us to make sure they will be shown only once
QPointer<AboutDialog> _aboutDialog;
static Application *_instance;
friend Application *ocApp();
friend bool isOcApp();
};
/**
*
* @return whether the Application singleton is available, this must always be true unless we are a unit test...
*/
inline bool isOcApp()
{
return Application::_instance;
}
inline Application *ocApp()
{
Q_ASSERT(isOcApp());
return Application::_instance;
}
} // namespace OCC
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#include "commonstrings.h"
#include <QCoreApplication>
using namespace OCC;
QString CommonStrings::fileBrowser()
{
#ifdef Q_OS_WIN
return QStringLiteral("Explorer");
#elif defined(Q_OS_MAC)
return QStringLiteral("Finder");
#else
return tr("file manager");
#endif
}
QString CommonStrings::showInFileBrowser(const QString &path)
{
if (path.isEmpty()) {
return tr("Show in %1").arg(fileBrowser());
}
return tr("Show »%1« in %2").arg(path, fileBrowser());
}
QString CommonStrings::showInWebBrowser()
{
return tr("Show in web browser");
}
QString CommonStrings::copyToClipBoard()
{
return tr("Copy");
}
QString CommonStrings::filterButtonText(int filterCount)
{
return tr("%n Filter(s)", nullptr, filterCount);
}
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#pragma once
#include <QObject>
#include <QtQml/QQmlEngine>
namespace OCC {
class CommonStrings : public QObject
{
Q_OBJECT
QML_SINGLETON
QML_ELEMENT
public:
Q_INVOKABLE static QString fileBrowser();
Q_INVOKABLE static QString showInFileBrowser(const QString &path = {});
Q_INVOKABLE static QString showInWebBrowser();
Q_INVOKABLE static QString copyToClipBoard();
Q_INVOKABLE static QString filterButtonText(int filterCount);
};
}
+171
View File
@@ -0,0 +1,171 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#include "gui/connectionvalidator.h"
#include "gui/fetchserversettings.h"
#include "gui/networkinformation.h"
#include "libsync/account.h"
#include "libsync/creds/abstractcredentials.h"
#include "libsync/networkjobs.h"
#include "libsync/networkjobs/checkserverjobfactory.h"
#include "libsync/networkjobs/jsonjob.h"
#include "libsync/theme.h"
#include <QJsonObject>
#include <QLoggingCategory>
#include <QNetworkProxyFactory>
#include <QNetworkReply>
#include <QtConcurrent/QtConcurrentRun>
using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;
namespace {
auto fetchSettingsTimeout()
{
return std::min<std::chrono::milliseconds>(20s, OCC::AbstractNetworkJob::httpTimeout);
}
}
namespace OCC {
Q_LOGGING_CATEGORY(lcConnectionValidator, "sync.connectionvalidator", QtInfoMsg)
ConnectionValidator::ConnectionValidator(AccountPtr account, QObject *parent)
: QObject(parent)
, _account(account)
{
// TODO: 6.0 abort validator on 5min timeout
auto timer = new QTimer(this);
timer->setInterval(30s);
connect(timer, &QTimer::timeout, this,
[this] { qCInfo(lcConnectionValidator) << u"ConnectionValidator" << _account->displayNameWithHost() << u"still running after" << _duration; });
timer->start();
}
void ConnectionValidator::setClearCookies(bool clearCookies)
{
_clearCookies = clearCookies;
}
void ConnectionValidator::checkServer(ConnectionValidator::ValidationMode mode)
{
_mode = mode;
qCDebug(lcConnectionValidator) << u"Checking server and authentication";
auto checkServerFactory = CheckServerJobFactory::createFromAccount(_account, _clearCookies, this);
auto checkServerJob = checkServerFactory.startJob(_account->url(), this);
connect(checkServerJob->reply()->manager(), &AccessManager::sslErrors, this, [this](QNetworkReply *reply, const QList<QSslError> &errors) {
Q_UNUSED(reply)
Q_EMIT sslErrors(errors);
});
connect(checkServerJob, &CoreJob::finished, this, [checkServerJob, this]() {
if (checkServerJob->success()) {
const auto result = checkServerJob->result().value<CheckServerJobResult>();
// adopt the new cookies
_account->accessManager()->setCookieJar(checkServerJob->reply()->manager()->cookieJar());
slotStatusFound(result.serverUrl(), result.statusObject());
} else {
switch (checkServerJob->reply()->error()) {
case QNetworkReply::OperationCanceledError:
[[fallthrough]];
case QNetworkReply::TimeoutError:
qCWarning(lcConnectionValidator) << checkServerJob;
_errors.append(tr("timeout"));
reportResult(Timeout);
return;
case QNetworkReply::SslHandshakeFailedError:
reportResult(NetworkInformation::instance()->isBehindCaptivePortal() ? CaptivePortal : SslError);
return;
case QNetworkReply::TooManyRedirectsError:
reportResult(StatusNotFound);
return;
default:
break;
}
_account->credentials()->checkCredentials(checkServerJob->reply());
_errors.append(checkServerJob->errorMessage());
reportResult(StatusNotFound);
}
});
}
void ConnectionValidator::slotStatusFound(const QUrl &url, const QJsonObject &info)
{
// status.php was found.
qCInfo(lcConnectionValidator) << u"** Application found: " << url << u" with version " << info.value(QLatin1String("productversion")).toString();
// Update server URL in case of redirection
if (_account->url() != url) {
if (Utility::urlEqual(_account->url(), url)) {
qCInfo(lcConnectionValidator()) << u"status.php was redirected to" << url.toString() << u"updating the account url";
_account->setUrl(url);
} else {
qCInfo(lcConnectionValidator()) << u"status.php was redirected to" << url.toString() << u"asking user to accept and abort for now";
Q_EMIT _account->requestUrlUpdate(url);
reportResult(StatusNotFound);
return;
}
}
AbstractCredentials *creds = _account->credentials();
if (!creds->ready()) {
reportResult(CredentialsNotReady);
return;
}
// now check the authentication
if (_mode != ConnectionValidator::ValidationMode::ValidateServer) {
// the endpoint requires authentication
auto *userJob = new JsonJob(_account, _account->url(), u"graph/v1.0/me"_s, "GET", nullptr);
userJob->setAuthenticationJob(true);
userJob->setTimeout(fetchSettingsTimeout());
connect(userJob, &JsonApiJob::finishedSignal, this, [userJob, this] {
if (userJob->timedOut()) {
reportResult(ConnectionValidator::Timeout);
} else if (userJob->httpStatusCode() == 200) {
reportResult(ConnectionValidator::Connected);
} else if (userJob->httpStatusCode() == 401) {
reportResult(ConnectionValidator::CredentialsWrong);
} else if (userJob->httpStatusCode() == 503) {
reportResult(ConnectionValidator::ServiceUnavailable);
} else if (userJob->reply()->error() == QNetworkReply::SslHandshakeFailedError) {
reportResult(NetworkInformation::instance()->isBehindCaptivePortal() ? ConnectionValidator::CaptivePortal : ConnectionValidator::SslError);
} else {
reportResult(ConnectionValidator::Undefined);
}
});
userJob->start();
return;
} else {
reportResult(Connected);
}
}
void ConnectionValidator::reportResult(Status status)
{
if (OC_ENSURE(!_finished)) {
_finished = true;
qCDebug(lcConnectionValidator) << status << _duration;
Q_EMIT connectionResult(status, _errors);
deleteLater();
}
}
} // namespace OCC
+87
View File
@@ -0,0 +1,87 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#pragma once
#include "gui/qsferaguilib.h"
#include "common/chronoelapsedtimer.h"
#include "gui/guiutility.h"
#include "libsync/accountfwd.h"
#include <QNetworkReply>
#include <QObject>
#include <QStringList>
#include <QVariantMap>
#include <chrono>
namespace OCC {
class QSFERA_GUI_EXPORT ConnectionValidator : public QObject
{
Q_OBJECT
public:
explicit ConnectionValidator(AccountPtr account, QObject *parent = nullptr);
enum class ValidationMode { ValidateServer, ValidateAuthAndUpdate };
Q_ENUM(ValidationMode)
enum Status : uint8_t {
Undefined,
Connected,
CredentialsNotReady, // Credentials aren't ready
CredentialsWrong, // AuthenticationRequiredError
SslError, // SSL handshake error, certificate rejected by user?
StatusNotFound, // Error retrieving status.php
ServiceUnavailable, // 503 on authed request
Timeout, // actually also used for other errors on the authed request
CaptivePortal, // We're stuck behind a captive portal and (will) get SSL certificate problems
};
Q_ENUM(Status)
// How often should the Application ask this object to check for the connection?
static constexpr auto DefaultCallingInterval = std::chrono::seconds(62);
/** Whether to clear the cookies before we start the CheckServerJob job
* This option also depends on Theme::instance()->connectionValidatorClearCookies()
*/
void setClearCookies(bool clearCookies);
public Q_SLOTS:
/// Checks the server and the authentication.
void checkServer(ConnectionValidator::ValidationMode mode = ConnectionValidator::ValidationMode::ValidateAuthAndUpdate);
Q_SIGNALS:
void connectionResult(ConnectionValidator::Status status, const QStringList &errors);
void sslErrors(const QList<QSslError> &errors);
protected Q_SLOTS:
void slotStatusFound(const QUrl &url, const QJsonObject &info);
private:
void reportResult(Status status);
QStringList _errors;
AccountPtr _account;
bool _clearCookies = false;
Utility::ChronoElapsedTimer _duration;
bool _finished = false;
ConnectionValidator::ValidationMode _mode = ConnectionValidator::ValidationMode::ValidateAuthAndUpdate;
};
}
@@ -0,0 +1,103 @@
/*
* Copyright (C) by Klaas Freitag <freitag@kde.org>
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* 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.
*/
#include "creds/httpcredentialsgui.h"
#include "account.h"
#include "accountmodalwidget.h"
#include "application.h"
#include "common/asserts.h"
#include "creds/qmlcredentials.h"
#include "gui/accountsettings.h"
#include "networkjobs.h"
#include "settingsdialog.h"
#include <QClipboard>
#include <QDesktopServices>
#include <QTimer>
namespace OCC {
Q_LOGGING_CATEGORY(lcHttpCredentialsGui, "sync.credentials.http.gui", QtInfoMsg)
HttpCredentialsGui::HttpCredentialsGui(const QString &accessToken, const QString &refreshToken)
: HttpCredentials(accessToken)
{
_refreshToken = refreshToken;
}
void HttpCredentialsGui::restartOauth()
{
qCDebug(lcHttpCredentialsGui) << u"showing modal dialog asking user to log in again via OAuth2";
if (_asyncAuth) {
return;
}
if (!OC_ENSURE_NOT(_modalWidget)) {
_modalWidget->deleteLater();
}
_asyncAuth.reset(new AccountBasedOAuth(_account->sharedFromThis(), this));
connect(_asyncAuth.data(), &OAuth::result, this, &HttpCredentialsGui::asyncAuthResult);
auto *oauthCredentials = new QmlOAuthCredentials(_asyncAuth.data(), _account->url(), _account->davDisplayName());
_modalWidget = new AccountModalWidget(tr("Login required"), QUrl(QStringLiteral("qrc:/qt/qml/eu/QSfera/gui/qml/credentials/OAuthCredentials.qml")),
oauthCredentials, ocApp()->settingsDialog());
connect(oauthCredentials, &QmlOAuthCredentials::logOutRequested, _modalWidget, [this] {
_modalWidget->reject();
_modalWidget.clear();
_asyncAuth.reset();
requestLogout();
});
connect(oauthCredentials, &QmlOAuthCredentials::requestRestart, this, [this] {
_modalWidget->reject();
_modalWidget.clear();
_asyncAuth.reset();
restartOauth();
});
connect(this, &HttpCredentialsGui::oAuthLoginAccepted, _modalWidget, &AccountModalWidget::accept);
connect(this, &HttpCredentialsGui::oAuthErrorOccurred, oauthCredentials, [this]() {
Q_ASSERT(!ready());
ocApp()->showSettings();
});
ocApp()->settingsDialog()->accountSettings(_account)->addModalWidget(_modalWidget);
_asyncAuth->startAuthentication();
}
void HttpCredentialsGui::asyncAuthResult(OAuth::Result r, const QString &token, const QString &refreshToken)
{
_asyncAuth.reset();
switch (r) {
case OAuth::ErrorInsecureUrl:
// should not happen after the initial setup
Q_ASSERT(false);
[[fallthrough]];
case OAuth::Error:
Q_EMIT oAuthErrorOccurred();
return;
case OAuth::LoggedIn:
Q_EMIT oAuthLoginAccepted();
break;
}
_accessToken = token;
_refreshToken = refreshToken;
_ready = true;
persist();
Q_EMIT fetched();
}
} // namespace OCC
@@ -0,0 +1,53 @@
/*
* Copyright (C) by Klaas Freitag <freitag@kde.org>
* Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
*
* 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.
*/
#pragma once
#include "creds/httpcredentials.h"
#include "creds/oauth.h"
#include <QPointer>
namespace OCC {
class AccountModalWidget;
/**
* @brief The HttpCredentialsGui class
* @ingroup gui
*/
class HttpCredentialsGui : public HttpCredentials
{
Q_OBJECT
public:
HttpCredentialsGui() = default;
HttpCredentialsGui(const QString &accessToken, const QString &refreshToken);
void restartOauth() override;
private Q_SLOTS:
void asyncAuthResult(OAuth::Result, const QString &accessToken, const QString &refreshToken);
Q_SIGNALS:
void oAuthLoginAccepted();
void oAuthErrorOccurred();
private:
QScopedPointer<AccountBasedOAuth, QScopedPointerObjectDeleteLater<AccountBasedOAuth>> _asyncAuth;
QPointer<AccountModalWidget> _modalWidget;
};
} // namespace OCC
+74
View File
@@ -0,0 +1,74 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#include "qmlcredentials.h"
#include "libsync/theme.h"
#include <QClipboard>
#include <QGuiApplication>
using namespace OCC;
QmlCredentials::QmlCredentials(const QUrl &host, const QString &displayName, QObject *parent)
: QObject(parent)
, _host(host)
, _displayName(displayName)
{
}
bool QmlCredentials::isRefresh() const
{
return _isRefresh;
}
void QmlCredentials::setIsRefresh(bool newIsRefresh)
{
if (_isRefresh != newIsRefresh) {
_isRefresh = newIsRefresh;
Q_EMIT isRefreshChanged();
}
}
QmlOAuthCredentials::QmlOAuthCredentials(OAuth *oauth, const QUrl &host, const QString &displayName, QObject *parent)
: QmlCredentials(host, displayName, parent)
, _oauth(oauth)
{
connect(_oauth, &OAuth::authorisationLinkChanged, this, [this] {
_ready = true;
Q_EMIT readyChanged();
});
connect(_oauth, &QObject::destroyed, this, &QmlOAuthCredentials::readyChanged);
}
void QmlOAuthCredentials::copyAuthenticationUrlToClipboard()
{
qApp->clipboard()->setText(_oauth->authorisationLink().toString(QUrl::FullyEncoded));
}
void QmlOAuthCredentials::openAuthenticationUrlInBrowser()
{
_oauth->openBrowser();
}
bool QmlOAuthCredentials::isReady() const
{
return isValid() && _ready;
}
bool QmlOAuthCredentials::isValid() const
{
return _oauth;
}
+84
View File
@@ -0,0 +1,84 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#pragma once
#include "libsync/creds/oauth.h"
#include <QtQmlIntegration/QtQmlIntegration>
namespace OCC {
class QmlCredentials : public QObject
{
Q_OBJECT
Q_PROPERTY(QUrl host MEMBER _host CONSTANT FINAL)
Q_PROPERTY(QString displayName MEMBER _displayName CONSTANT FINAL)
Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged FINAL)
Q_PROPERTY(bool isRefresh READ isRefresh WRITE setIsRefresh NOTIFY isRefreshChanged FINAL)
QML_ELEMENT
QML_UNCREATABLE("Abstract class")
public:
QmlCredentials(const QUrl &host, const QString &displayName, QObject *parent = nullptr);
virtual bool isReady() const = 0;
/**
* Whether we refresh the credentials or are in the wizard
* @default true
*/
bool isRefresh() const;
void setIsRefresh(bool newIsRefresh);
Q_SIGNALS:
void readyChanged() const;
void logOutRequested() const;
void isRefreshChanged();
private:
QUrl _host;
QString _displayName;
bool _isRefresh = true;
};
class QmlOAuthCredentials : public QmlCredentials
{
Q_OBJECT
Q_PROPERTY(bool isValid READ isValid NOTIFY readyChanged FINAL)
QML_ELEMENT
QML_UNCREATABLE("C++ only")
public:
QmlOAuthCredentials(OAuth *oauth, const QUrl &host, const QString &displayName, QObject *parent = nullptr);
Q_INVOKABLE void copyAuthenticationUrlToClipboard();
Q_INVOKABLE void openAuthenticationUrlInBrowser();
bool isReady() const override;
bool isValid() const;
Q_SIGNALS:
void requestRestart();
private:
QPointer<OAuth> _oauth = nullptr;
bool _ready = false;
};
}
+49
View File
@@ -0,0 +1,49 @@
/*
* Copyright (C) by Christian Kamm <mail@ckamm.de>
*
* 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.
*/
#include "elidedlabel.h"
#include <QResizeEvent>
namespace OCC {
ElidedLabel::ElidedLabel(const QString &text, QWidget *parent)
: QLabel(text, parent)
, _text(text)
, _elideMode(Qt::ElideNone)
{
}
void ElidedLabel::setText(const QString &text)
{
_text = text;
QLabel::setText(text);
update();
}
void ElidedLabel::setElideMode(Qt::TextElideMode elideMode)
{
_elideMode = elideMode;
update();
}
void ElidedLabel::resizeEvent(QResizeEvent *event)
{
QLabel::resizeEvent(event);
QFontMetrics fm = fontMetrics();
QString elided = fm.elidedText(_text, _elideMode, event->size().width());
QLabel::setText(elided);
}
}
+44
View File
@@ -0,0 +1,44 @@
/*
* Copyright (C) by Christian Kamm <mail@ckamm.de>
*
* 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.
*/
#ifndef ELIDEDLABEL_H
#define ELIDEDLABEL_H
#include <QLabel>
namespace OCC {
/// Label that can elide its text
class ElidedLabel : public QLabel
{
Q_OBJECT
public:
explicit ElidedLabel(const QString &text, QWidget *parent = nullptr);
void setText(const QString &text);
const QString &text() const { return _text; }
void setElideMode(Qt::TextElideMode elideMode);
Qt::TextElideMode elideMode() const { return _elideMode; }
protected:
void resizeEvent(QResizeEvent *event) override;
private:
QString _text;
Qt::TextElideMode _elideMode;
};
}
#endif
+120
View File
@@ -0,0 +1,120 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#include "fetchserversettings.h"
#include "gui/accountstate.h"
#include "gui/connectionvalidator.h"
#include "gui/networkinformation.h"
#include "libsync/account.h"
#include "libsync/networkjobs/jsonjob.h"
#include <OAIUser.h>
#include <QImageReader>
using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;
using namespace OCC;
Q_LOGGING_CATEGORY(lcfetchserversettings, "sync.fetchserversettings", QtInfoMsg)
namespace {
auto fetchSettingsTimeout()
{
return std::min<std::chrono::milliseconds>(20s, AbstractNetworkJob::httpTimeout);
}
}
// TODO: move to libsync?
FetchServerSettingsJob::FetchServerSettingsJob(const OCC::AccountPtr &account, QObject *parent)
: QObject(parent)
, _account(account)
{
}
void FetchServerSettingsJob::start()
{
// The main flow now needs the capabilities
auto *job = new JsonApiJob(_account, QStringLiteral("ocs/v2.php/cloud/capabilities"), {}, {}, this);
job->setTimeout(fetchSettingsTimeout());
connect(job, &JsonApiJob::finishedSignal, this, [job, this] {
auto caps =
job->data().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("data")).toObject().value(QStringLiteral("capabilities")).toObject();
qCInfo(lcfetchserversettings) << u"Server capabilities" << caps;
if (job->ocsSuccess()) {
// Record that the server supports HTTP/2
// Actual decision if we should use HTTP/2 is done in AccessManager::createRequest
if (auto reply = job->reply()) {
_account->setHttp2Supported(reply->attribute(QNetworkRequest::Http2WasUsedAttribute).toBool());
}
_account->setCapabilities({_account->url(), caps.toVariantMap()});
runAsyncUpdates();
}
Q_EMIT finishedSignal();
});
job->start();
}
void FetchServerSettingsJob::runAsyncUpdates()
{
// those jobs are:
// - never auth jobs
// - might get queued
// - have the default timeout
// - must not be parented by this object
// ideally we would parent them to the account, but as things are messed up by the shared pointer stuff we can't at the moment
// so we just set them free
// this must not be passed to the lambda
[account = _account] {
auto *userJob = new JsonJob(account, account->url(), u"graph/v1.0/me"_s, "GET", nullptr);
userJob->setTimeout(fetchSettingsTimeout());
connect(userJob, &JsonApiJob::finishedSignal, account.data(), [userJob, account] {
if (userJob->httpStatusCode() == 200) {
OpenAPI::OAIUser me;
me.fromJsonObject(userJob->data());
account->setDavDisplayName(me.getDisplayName());
}
});
userJob->start();
if (account->capabilities().appProviders().enabled) {
auto *jsonJob = new JsonJob(account, account->capabilities().appProviders().appsUrl, {}, "GET", nullptr);
connect(jsonJob, &JsonJob::finishedSignal, account.data(), [jsonJob, account] { account->setAppProvider(AppProvider{jsonJob->data()}); });
jsonJob->start();
}
auto *avatarJob = new SimpleNetworkJob(account, account->url(), u"graph/v1.0/me/photo/$value"_s, "GET", nullptr);
connect(avatarJob, &SimpleNetworkJob::finishedSignal, account.data(), [avatarJob, account] {
if (avatarJob->httpStatusCode() == 200) {
QImageReader reader(avatarJob->reply());
const auto image = reader.read();
if (!image.isNull()) {
account->setAvatar(QPixmap::fromImage(image));
} else {
qCWarning(lcfetchserversettings) << u"Failed to read avatar image:" << reader.errorString();
}
}
});
avatarJob->start();
}();
}
+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#pragma once
#include "gui/connectionvalidator.h"
#include "libsync/accountfwd.h"
#include <QObject>
namespace OCC {
class Capabilities;
class FetchServerSettingsJob : public QObject
{
Q_OBJECT
public:
FetchServerSettingsJob(const AccountPtr &account, QObject *parent);
void start();
Q_SIGNALS:
void finishedSignal();
private:
void runAsyncUpdates();
const AccountPtr _account;
};
}
File diff suppressed because it is too large Load Diff
+378
View File
@@ -0,0 +1,378 @@
/*
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#pragma once
#include "gui/qsferaguilib.h"
#include "accountstate.h"
#include "common/syncjournaldb.h"
#include "gui/folderdefinition.h"
#include "libsync/graphapi/space.h"
#include "networkjobs.h"
#include "progressdispatcher.h"
#include "syncoptions.h"
#include "syncresult.h"
#include <QDateTime>
#include <QObject>
#include <QtQml/QQmlEngine>
#include <chrono>
#include <set>
class QThread;
class QSettings;
namespace OCC {
class Vfs;
class SyncEngine;
class SyncRunFileLog;
class FolderWatcher;
class LocalDiscoveryTracker;
/**
* @brief The Folder class
* @ingroup gui
*/
class QSFERA_GUI_EXPORT Folder : public QObject
{
Q_OBJECT
Q_PROPERTY(GraphApi::Space *space READ space NOTIFY spaceChanged)
Q_PROPERTY(QString path READ path CONSTANT)
Q_PROPERTY(QUrl webDavUrl READ webDavUrl CONSTANT)
Q_PROPERTY(bool isReady READ isReady NOTIFY isReadyChanged)
Q_PROPERTY(bool isSyncPaused READ isSyncPaused NOTIFY syncPausedChanged)
Q_PROPERTY(bool isSyncRunning READ isSyncRunning NOTIFY isSyncRunningChanged)
Q_PROPERTY(bool isDeployed READ isDeployed CONSTANT)
Q_PROPERTY(Vfs::Mode vfsMode READ vfsMode CONSTANT)
QML_ELEMENT
QML_UNCREATABLE("Folders can only be created by the FolderManager")
public:
enum class ChangeReason {
Other,
UnLock
};
Q_ENUM(ChangeReason)
static void prepareFolder(const QString &path, const QString &displayName, const QString &description, bool override);
~Folder() override;
/**
* The account the folder is configured on.
*/
AccountStatePtr accountState() const { return _accountState; }
QString displayName() const;
/**
* short local path to display on the GUI (native separators)
*/
QString shortGuiLocalPath() const;
/**
* canonical local folder path, always ends with /
*/
QString path() const;
/**
* cleaned canonical folder path, like path() but never ends with a /
*
* Wrapper for QDir::cleanPath(path()) except for "Z:/",
* where it returns "Z:" instead of "Z:/".
*/
QString cleanPath() const;
/**
* The full remote WebDAV URL
*/
QUrl webDavUrl() const;
/**
* switch sync on or off
*/
void setSyncPaused(bool);
bool isSyncPaused() const;
/**
* Returns true when the folder may sync.
*/
bool canSync() const;
/**
* Whether the folder is ready
*/
bool isReady() const;
bool hasSetupError() const
{
return _syncResult.status() == SyncResult::SetupError;
}
/** True if the folder is currently synchronizing */
bool isSyncRunning() const;
/**
* return the last sync result with error message and status
*/
SyncResult syncResult() const;
/**
* This is called when the sync folder definition is removed. Do cleanups here.
*
* It removes the database, among other things.
*
* The folder is not in a valid state afterwards!
*/
virtual void wipeForRemoval();
void setSyncState(SyncResult::Status state);
SyncResult::Status syncState() const;
void setDirtyNetworkLimits();
void reloadSyncOptions();
/**
* Ignore syncing of hidden files or not. This is defined in the
* folder definition
*/
bool ignoreHiddenFiles();
void setIgnoreHiddenFiles(bool ignore);
// TODO: don't expose
SyncJournalDb *journalDb()
{
return &_journal;
}
// TODO: don't expose
SyncEngine &syncEngine()
{
return *_engine;
}
Vfs &vfs()
{
OC_ENFORCE(_vfs);
return *_vfs;
}
auto lastSyncTime() const { return QDateTime::currentDateTime().addMSecs(-msecSinceLastSync().count()); }
std::chrono::milliseconds msecSinceLastSync() const { return std::chrono::milliseconds(_timeSinceLastSyncDone.elapsed()); }
std::chrono::milliseconds msecLastSyncDuration() const { return _lastSyncDuration; }
/**
* Returns whether a file inside this folder should be excluded.
*/
bool isFileExcludedAbsolute(const QString &fullPath) const;
/**
* Returns whether a file inside this folder should be excluded.
*/
bool isFileExcludedRelative(const QString &relativePath) const;
/** virtual files of some kind are enabled
*
* This is independent of whether new files will be virtual. It's possible to have this enabled
* and never have an automatic virtual file. But when it's on, the shell context menu will allow
* users to make existing files virtual.
*/
bool virtualFilesEnabled() const;
void setVirtualFilesEnabled(bool enabled);
/** Whether this folder should show selective sync ui */
bool supportsSelectiveSync() const;
/**
* The folder is deployed by an admin
* We will hide the remove option and the disable/enable vfs option.
*/
bool isDeployed() const;
Vfs::Mode vfsMode() const;
uint32_t priority();
void setPriority(uint32_t p);
static Result<void, QString> checkPathLength(const QString &path);
/**
*
* @return The corresponding space object or null
*/
GraphApi::Space *space() const;
Q_SIGNALS:
void syncStateChange();
void syncFinished(const SyncResult &result);
void syncPausedChanged(Folder *, bool paused);
void canSyncChanged();
void spaceChanged();
void isReadyChanged();
void isSyncRunningChanged();
/**
* Fires for each change inside this folder that wasn't caused
* by sync activity.
*/
void watchedFileChangedExternally(const QString &path);
public Q_SLOTS:
void openInWebBrowser();
/**
* Starts a sync operation
*
* If the list of changed files is known, it is passed.
*/
void startSync();
void slotDiscardDownloadProgress();
int slotWipeErrorBlacklist();
/**
* Triggered by the folder watcher when a file/dir in this folder
* changes. Needs to check whether this change should trigger a new
* sync run to be scheduled.
*/
void slotWatchedPathsChanged(const QSet<QString> &paths, ChangeReason reason);
/** Ensures that the next sync performs a full local discovery. */
void slotNextSyncFullLocalDiscovery();
/** Adds the path to the local discovery list
*
* A weaker version of slotNextSyncFullLocalDiscovery() that just
* schedules all parent and child items of the path for local
* discovery.
*/
void schedulePathForLocalDiscovery(const QString &relativePath);
/// Reloads the excludes, used when changing the user-defined excludes after saving them to disk.
bool reloadExcludes();
private Q_SLOTS:
void slotSyncFinished(bool);
/** Adds a error message that's not tied to a specific item.
*/
void slotSyncError(const QString &message, ErrorCategory category = ErrorCategory::Normal);
void slotItemCompleted(const SyncFileItemPtr &);
/** Adjust sync result based on conflict data from IssuesWidget.
*
* This is pretty awkward, but IssuesWidget just keeps better track
* of conflicts across partial local discovery.
*/
void slotFolderConflicts(Folder *folder, const QStringList &conflictPaths);
/** Warn users if they create a file or folder that is selective-sync excluded */
void warnOnNewExcludedItem(const SyncJournalFileRecord &record, QStringView path);
/** Warn users about an unreliable folder watcher */
void slotWatcherUnreliable(const QString &message);
private:
/** Create a new Folder
*/
Folder(const FolderDefinition &definition, const AccountStatePtr &accountState, std::unique_ptr<Vfs> &&vfs, QObject *parent = nullptr);
void showSyncResultPopup();
bool checkLocalPath();
SyncOptions loadSyncOptions();
void setIsReady(bool b);
/**
* Sets up this folder's folderWatcher if possible.
*
* May be called several times.
*/
void registerFolderWatcher();
enum LogStatus {
LogStatusRemove,
LogStatusRename,
LogStatusMove,
LogStatusNew,
LogStatusError,
LogStatusConflict,
LogStatusUpdated
};
void createGuiLog(const QString &filename, LogStatus status, int count,
const QString &renameTarget = QString());
void startVfs();
AccountStatePtr _accountState;
FolderDefinition _definition;
QString _canonicalLocalPath; // As returned with QFileInfo:canonicalFilePath. Always ends with "/"
SyncResult _syncResult;
QScopedPointer<SyncEngine> _engine;
QElapsedTimer _timeSinceLastSyncDone;
QElapsedTimer _timeSinceLastSyncStart;
QElapsedTimer _timeSinceLastFullLocalDiscovery;
std::chrono::milliseconds _lastSyncDuration = {};
/// The number of syncs that failed in a row.
/// Reset when a sync is successful.
int _consecutiveFailingSyncs = 0;
/// The number of requested follow-up syncs.
/// Reset when no follow-up is requested.
int _consecutiveFollowUpSyncs = 0;
mutable SyncJournalDb _journal;
QScopedPointer<SyncRunFileLog> _fileLog;
/**
* Setting up vfs is a async operation
*/
bool _vfsIsReady = false;
/**
* Watches this folder's local directory for changes.
*
* Created by registerFolderWatcher(), triggers slotWatchedPathsChanged()
*/
QScopedPointer<FolderWatcher> _folderWatcher;
/**
* Keeps track of locally dirty files so we can skip local discovery sometimes.
*/
QScopedPointer<LocalDiscoveryTracker> _localDiscoveryTracker;
/**
* The vfs mode instance (created by plugin) to use. Never null.
*/
QSharedPointer<Vfs> _vfs;
friend class FolderMan;
};
}
+169
View File
@@ -0,0 +1,169 @@
/*
* Copyright (C) by Hannah von Reth <h.vonreth@opencloud.eu>
*
* 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.
*/
#include "folderdefinition.h"
#include <QDir>
#include <QSettings>
using namespace OCC;
Q_LOGGING_CATEGORY(lcFolder, "gui.folder.definition", QtInfoMsg)
namespace {
auto davUrlC()
{
return "davUrl";
}
auto spaceIdC()
{
return "spaceId";
}
auto displayNameC()
{
return "displayString";
}
auto deployedC()
{
return "deployed";
}
auto priorityC()
{
return "priority";
}
}
FolderDefinition::FolderDefinition(const QUuid &accountUuid, const QUrl &davUrl, const QString &spaceId, const QString &displayName)
: _webDavUrl(davUrl)
, _spaceId(spaceId)
, _displayName(displayName)
, _accountUUID(accountUuid)
{
Q_ASSERT(!_accountUUID.isNull());
}
void FolderDefinition::setPriority(uint32_t newPriority)
{
_priority = newPriority;
}
QUuid FolderDefinition::accountUUID() const
{
return _accountUUID;
}
uint32_t FolderDefinition::priority() const
{
return _priority;
}
void FolderDefinition::save(QSettings &settings, const FolderDefinition &folder)
{
settings.setValue("accountUUID", folder.accountUUID());
settings.setValue("localPath", folder.localPath());
settings.setValue("journalPath", folder.journalPath);
settings.setValue(spaceIdC(), folder.spaceId());
settings.setValue(davUrlC(), folder.webDavUrl());
settings.setValue(displayNameC(), folder.displayName());
settings.setValue("paused", folder.paused);
settings.setValue("ignoreHiddenFiles", folder.ignoreHiddenFiles);
settings.setValue(deployedC(), folder.isDeployed());
settings.setValue(priorityC(), folder.priority());
settings.setValue("virtualFilesMode", Utility::enumToString(folder.virtualFilesMode));
}
FolderDefinition FolderDefinition::load(QSettings &settings)
{
FolderDefinition folder{settings.value("accountUUID").toUuid(), settings.value(davUrlC()).toUrl(), settings.value(spaceIdC()).toString(),
settings.value(displayNameC()).toString()};
folder.setLocalPath(settings.value("localPath").toString());
folder.journalPath = settings.value("journalPath").toString();
folder.paused = settings.value("paused").toBool();
folder.ignoreHiddenFiles = settings.value("ignoreHiddenFiles", QVariant(true)).toBool();
folder._deployed = settings.value(deployedC(), false).toBool();
folder._priority = settings.value(priorityC(), 0).toUInt();
folder.virtualFilesMode = Vfs::Mode::Off;
QString vfsModeString = settings.value("virtualFilesMode").toString();
const auto vfs = Utility::isWindows() ? Vfs::Mode::WindowsCfApi : Vfs::Mode::OpenVFS;
if (auto result = VfsPluginManager::instance().prepare(folder.localPath(), folder.accountUUID(), vfs); result) {
vfsModeString = Utility::enumToString(vfs);
} else {
qCWarning(lcFolder) << u"Failed to upgrade" << folder.localPath() << u"to" << vfs << result.error();
}
if (!vfsModeString.isEmpty()) {
if (auto mode = Vfs::modeFromString(vfsModeString)) {
folder.virtualFilesMode = *mode;
} else {
qCWarning(lcFolder) << u"Unknown virtualFilesMode:" << vfsModeString << u"assuming 'off'";
}
}
return folder;
}
void FolderDefinition::setLocalPath(const QString &path)
{
_localPath = QDir::fromNativeSeparators(path);
if (!_localPath.endsWith(QLatin1Char('/'))) {
_localPath.append(QLatin1Char('/'));
}
}
QString FolderDefinition::absoluteJournalPath() const
{
return QDir(localPath()).filePath(journalPath);
}
QString FolderDefinition::displayName() const
{
return _displayName;
}
void FolderDefinition::setDisplayName(const QString &s)
{
_displayName = s;
}
bool FolderDefinition::isDeployed() const
{
return _deployed;
}
QUrl FolderDefinition::webDavUrl() const
{
Q_ASSERT(_webDavUrl.isValid());
return _webDavUrl;
}
QString FolderDefinition::localPath() const
{
return _localPath;
}
QString FolderDefinition::spaceId() const
{
// we might call the function to check for the id
// anyhow one of the conditions needs to be true
Q_ASSERT(_webDavUrl.isValid() || !_spaceId.isEmpty());
return _spaceId;
}
+103
View File
@@ -0,0 +1,103 @@
/*
* Copyright (C) by Hannah von Reth <h.vonreth@opencloud.eu>
*
* 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.
*/
#pragma once
#include "gui/qsferaguilib.h"
#include "libsync/vfs/vfs.h"
#include <QString>
#include <QUrl>
#include <QUuid>
namespace OCC {
/**
* @brief The FolderDefinition class
* @ingroup gui
*/
class QSFERA_GUI_EXPORT FolderDefinition
{
public:
FolderDefinition(const QUuid &accountUuid, const QUrl &davUrl, const QString &spaceId, const QString &displayName);
/// path to the journal, usually relative to localPath
QString journalPath;
/// whether the folder is paused
bool paused = false;
/// whether the folder syncs hidden files
bool ignoreHiddenFiles = true;
/// Which virtual files setting the folder uses
Vfs::Mode virtualFilesMode = Vfs::Mode::Off;
/// Saves the folder definition into the current settings.
static void save(QSettings &settings, const FolderDefinition &folder);
/// Reads a folder definition from the current settings.
static FolderDefinition load(QSettings &settings);
/// Ensure / as separator and trailing /.
void setLocalPath(const QString &path);
/// journalPath relative to localPath.
QString absoluteJournalPath() const;
QString localPath() const;
QUrl webDavUrl() const;
// could change in the case of spaces
void setWebDavUrl(const QUrl &url) { _webDavUrl = url; }
// when using spaces we don't store the dav URL but the space id
// this id is then used to look up the dav URL
QString spaceId() const;
void setSpaceId(const QString &spaceId) { _spaceId = spaceId; }
QString displayName() const;
void setDisplayName(const QString &s);
/**
* The folder is deployed by an admin
* We will hide the remove option and the disable/enable vfs option.
*/
bool isDeployed() const;
/**
* Higher values mean more imortant
* Used for sorting
*/
uint32_t priority() const;
void setPriority(uint32_t newPriority);
QUuid accountUUID() const;
private:
QUrl _webDavUrl;
QString _spaceId;
QString _displayName;
/// path on local machine (always trailing /)
QString _localPath;
bool _deployed = false;
uint32_t _priority = 0;
QUuid _accountUUID;
};
}
+792
View File
@@ -0,0 +1,792 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#include "folderman.h"
#include "account.h"
#include "accountmanager.h"
#include "accountstate.h"
#include "common/asserts.h"
#include "configfile.h"
#include "gui/folder.h"
#include "gui/folderdefinition.h"
#include "gui/networkinformation.h"
#include "guiutility.h"
#include "libsync/syncengine.h"
#include "lockwatcher.h"
#include "scheduling/syncscheduler.h"
#include "socketapi/socketapi.h"
#include "syncresult.h"
#include "theme.h"
#ifdef Q_OS_WIN
#include "common/utility_win.h"
#endif
#include <QMessageBox>
#include <QtCore>
using namespace Qt::Literals::StringLiterals;
using namespace std::chrono;
using namespace std::chrono_literals;
namespace {
QAnyStringView foldersC()
{
return QAnyStringView("Folders");
}
qsizetype numberOfSyncJournals(const QString &path)
{
return QDir(path).entryList({ QStringLiteral(".sync_*.db"), QStringLiteral("._sync_*.db") }, QDir::Hidden | QDir::Files).size();
}
}
namespace OCC {
Q_LOGGING_CATEGORY(lcFolderMan, "gui.folder.manager", QtInfoMsg)
void TrayOverallStatusResult::addResult(Folder *f)
{
_overallStatus._numNewConflictItems += f->syncResult()._numNewConflictItems;
_overallStatus._numErrorItems += f->syncResult()._numErrorItems;
_overallStatus._numBlacklistErrors += f->syncResult()._numBlacklistErrors;
auto time = f->lastSyncTime();
if (time > lastSyncDone) {
lastSyncDone = time;
}
auto status = f->isSyncPaused() || NetworkInformation::instance()->isBehindCaptivePortal() || NetworkInformation::instance()->isMetered()
? SyncResult::Paused
: f->syncResult().status();
if (status == SyncResult::Undefined) {
status = SyncResult::Problem;
}
if (status > _overallStatus.status()) {
_overallStatus.setStatus(status);
}
}
const SyncResult &TrayOverallStatusResult::overallStatus() const
{
return _overallStatus;
}
FolderMan *FolderMan::_instance = nullptr;
FolderMan::FolderMan()
: _lockWatcher(new LockWatcher)
, _scheduler(new SyncScheduler(this))
, _socketApi(new SocketApi)
{
#ifdef Q_OS_WIN
auto updateSyncRoots = [] {
// set the metadata for a sync root, depending on the number of accounts
for (const auto &accountStatePtr : AccountManager::instance()->accounts()) {
if (accountStatePtr->account()->hasDefaultSyncRoot()) {
Folder::prepareFolder(accountStatePtr->account()->defaultSyncRoot(),
AccountManager::instance()->accounts().size() == 1
? Theme::instance()->appNameGUI()
: u"%1 - %2"_s.arg(Theme::instance()->appNameGUI(), accountStatePtr->account()->davDisplayName()),
accountStatePtr->account()->davDisplayName(), true);
}
}
};
updateSyncRoots();
connect(AccountManager::instance(), &AccountManager::accountAdded, this, updateSyncRoots);
#endif
connect(AccountManager::instance(), &AccountManager::accountRemoved,
this, &FolderMan::slotRemoveFoldersForAccount);
connect(_lockWatcher.data(), &LockWatcher::fileUnlocked, this, [this](const QString &path, FileSystem::LockMode) {
if (Folder *f = folderForPath(path)) {
// Treat this equivalently to the file being reported by the file watcher
f->slotWatchedPathsChanged({path}, Folder::ChangeReason::UnLock);
}
});
}
FolderMan *FolderMan::instance()
{
Q_ASSERT(_instance);
return _instance;
}
FolderMan::~FolderMan()
{
unloadAndDeleteAllFolders();
qDeleteAll(_folders);
_instance = nullptr;
}
const QVector<Folder *> &FolderMan::folders() const
{
return _folders;
}
void FolderMan::unloadFolder(Folder *f)
{
Q_ASSERT(f);
_folders.removeAll(f);
_socketApi->slotUnregisterPath(f);
if (!f->hasSetupError()) {
disconnect(f, nullptr, _socketApi.get(), nullptr);
disconnect(f, nullptr, this, nullptr);
disconnect(f, nullptr, &f->syncEngine().syncFileStatusTracker(), nullptr);
disconnect(&f->syncEngine(), nullptr, f, nullptr);
disconnect(
&f->syncEngine().syncFileStatusTracker(), &SyncFileStatusTracker::fileStatusChanged, _socketApi.get(), &SocketApi::broadcastStatusPushMessage);
}
}
void FolderMan::unloadAndDeleteAllFolders()
{
if (!_folders.isEmpty()) {
// clear the list of existing folders.
saveFolders();
const auto folders = std::move(_folders);
for (auto *folder : folders) {
// unload and disconnect all signals
unloadFolder(folder);
folder->deleteLater();
}
}
}
void FolderMan::registerFolderWithSocketApi(Folder *folder)
{
if (!folder)
return;
if (!QDir(folder->path()).exists())
return;
// register the folder with the socket API
if (folder->canSync())
_socketApi->slotRegisterPath(folder);
}
std::optional<qsizetype> FolderMan::loadFolders()
{
qCInfo(lcFolderMan) << u"Setup folders from settings file";
auto settings = ConfigFile::makeQSettings();
const auto size = settings.beginReadArray(foldersC());
for (auto i = 0; i < size; ++i) {
settings.setArrayIndex(i);
FolderDefinition folderDefinition = FolderDefinition::load(settings);
if (SyncJournalDb::dbIsTooNewForClient(folderDefinition.absoluteJournalPath())) {
continue;
}
auto vfs = VfsPluginManager::instance().createVfsFromPlugin(folderDefinition.virtualFilesMode);
if (!vfs) {
// TODO: Must do better error handling
qFatal("Could not load plugin");
}
auto account = AccountManager::instance()->account(folderDefinition.accountUUID());
if (account.isNull()) {
qFatal("Could not load account");
}
addFolderInternal(std::move(folderDefinition), account, std::move(vfs));
}
settings.endArray();
Q_EMIT folderListChanged();
return _folders.size();
}
void FolderMan::saveFolders()
{
auto settings = ConfigFile::makeQSettings();
settings.remove(foldersC());
settings.beginWriteArray(foldersC(), _folders.size());
int i = 0;
for (const auto folder : std::as_const(_folders)) {
settings.setArrayIndex(i++);
auto definitionToSave = folder->_definition;
// with spaces we rely on the space id
// we save the dav URL nevertheless to have it available during startup
definitionToSave.setWebDavUrl(folder->webDavUrl());
definitionToSave.setDisplayName(folder->displayName());
FolderDefinition::save(settings, definitionToSave);
}
settings.endArray();
}
bool FolderMan::ensureJournalGone(const QString &journalDbFile)
{
// remove the old journal file
while (QFile::exists(journalDbFile) && !QFile::remove(journalDbFile)) {
qCWarning(lcFolderMan) << u"Could not remove old db file at" << journalDbFile;
int ret = QMessageBox::warning(nullptr, tr("Could not reset folder state"),
tr("An old sync journal %1 was found, "
"but could not be removed. Please make sure "
"that no application is currently using it.")
.arg(QDir::fromNativeSeparators(QDir::cleanPath(journalDbFile))),
QMessageBox::Retry | QMessageBox::Abort);
if (ret == QMessageBox::Abort) {
return false;
}
}
return true;
}
SocketApi *FolderMan::socketApi()
{
return _socketApi.get();
}
void FolderMan::slotFolderCanSyncChanged()
{
Folder *f = qobject_cast<Folder *>(sender());
OC_ASSERT(f);
if (f->canSync()) {
_socketApi->slotRegisterPath(f);
} else {
_socketApi->slotUnregisterPath(f);
}
}
void FolderMan::scheduleAllFolders()
{
for (auto *f : std::as_const(_folders)) {
if (f && f->canSync()) {
scheduler()->enqueueFolder(f);
}
}
}
void FolderMan::slotSyncOnceFileUnlocks(const QString &path, FileSystem::LockMode mode)
{
_lockWatcher->addFile(path, mode);
}
void FolderMan::slotIsConnectedChanged()
{
AccountStatePtr accountState(qobject_cast<AccountState *>(sender()));
if (!accountState) {
return;
}
QString accountName = accountState->account()->displayNameWithHost();
if (accountState->isConnected()) {
qCInfo(lcFolderMan) << u"Account" << accountName << u"connected, scheduling its folders";
for (auto *f : std::as_const(_folders)) {
if (f->accountState() == accountState && f->canSync()) {
scheduler()->enqueueFolder(f);
}
}
} else if (accountState->state() == AccountState::State::Disconnected || accountState->state() == AccountState::State::SignedOut) {
qCInfo(lcFolderMan) << u"Account" << accountName << u"disconnected or paused, terminating or descheduling sync folders";
if (scheduler()->currentSync() && scheduler()->currentSync()->accountState() == accountState) {
scheduler()->terminateCurrentSync(tr("Account disconnected or paused"));
}
}
}
// only enable or disable foldermans will schedule and do syncs.
// this is not the same as Pause and Resume of folders.
void FolderMan::setSyncEnabled(bool enabled)
{
if (enabled) {
// We have things in our queue that were waiting for the connection to come back on.
scheduler()->start();
} else {
scheduler()->stop();
}
qCInfo(lcFolderMan) << Q_FUNC_INFO << enabled;
// force a redraw in case the network connect status changed
Q_EMIT folderSyncStateChange(nullptr);
}
void FolderMan::slotRemoveFoldersForAccount(const AccountStatePtr &accountState)
{
QList<Folder *> foldersToRemove;
// reserve a magic number
foldersToRemove.reserve(16);
for (auto *folder : std::as_const(_folders)) {
if (folder->accountState() == accountState) {
foldersToRemove.append(folder);
}
}
for (const auto &f : foldersToRemove) {
removeFolder(f);
}
}
void FolderMan::slotServerVersionChanged(Account *account)
{
// Pause folders if the server version is unsupported
if (account->serverSupportLevel() == Account::ServerSupportLevel::Unsupported) {
qCWarning(lcFolderMan) << u"The server version is unsupported:" << account->capabilities().status().versionString()
<< u"pausing all folders on the account";
for (auto &f : std::as_const(_folders)) {
if (f->accountState()->account().data() == account) {
f->setSyncPaused(true);
}
}
}
}
bool FolderMan::isAnySyncRunning() const
{
if (_scheduler->hasCurrentRunningSyncRunning()) {
return true;
}
for (auto f : _folders) {
if (f->isSyncRunning())
return true;
}
return false;
}
Folder *FolderMan::addFolder(const AccountStatePtr &accountState, const FolderDefinition &folderDefinition)
{
// Choose a db filename
auto definition = folderDefinition;
definition.journalPath = SyncJournalDb::makeDbName(folderDefinition.localPath());
if (!ensureJournalGone(definition.absoluteJournalPath())) {
return nullptr;
}
auto vfs = VfsPluginManager::instance().createVfsFromPlugin(folderDefinition.virtualFilesMode);
if (!vfs) {
qCWarning(lcFolderMan) << u"Could not load plugin for mode" << folderDefinition.virtualFilesMode;
return nullptr;
}
auto folder = addFolderInternal(definition, accountState, std::move(vfs));
if (folder) {
Q_EMIT folderSyncStateChange(folder);
Q_EMIT folderListChanged();
}
saveFolders();
return folder;
}
Folder *FolderMan::addFolderInternal(
FolderDefinition folderDefinition,
const AccountStatePtr &accountState,
std::unique_ptr<Vfs> vfs)
{
auto folder = new Folder(folderDefinition, accountState, std::move(vfs), this);
qCInfo(lcFolderMan) << u"Adding folder to Folder Map " << folder << folder->path();
_folders.push_back(folder);
// See matching disconnects in unloadFolder().
if (!folder->hasSetupError()) {
connect(folder, &Folder::syncStateChange, _socketApi.get(), [folder, this] { _socketApi->slotUpdateFolderView(folder); });
connect(folder, &Folder::syncStateChange, this, [folder, this] { Q_EMIT folderSyncStateChange(folder); });
connect(folder, &Folder::canSyncChanged, this, &FolderMan::slotFolderCanSyncChanged);
connect(
&folder->syncEngine().syncFileStatusTracker(), &SyncFileStatusTracker::fileStatusChanged, _socketApi.get(), &SocketApi::broadcastStatusPushMessage);
connect(folder, &Folder::watchedFileChangedExternally, &folder->syncEngine().syncFileStatusTracker(), &SyncFileStatusTracker::slotPathTouched);
registerFolderWithSocketApi(folder);
}
return folder;
}
Folder *FolderMan::folderForPath(const QString &path, QString *relativePath)
{
QString absolutePath = QDir::cleanPath(path) + QLatin1Char('/');
for (auto *folder : std::as_const(_folders)) {
const QString folderPath = folder->cleanPath() + QLatin1Char('/');
if (FileSystem::isChildPathOf2(absolutePath, folderPath).testAnyFlag(FileSystem::ChildResult::IsChild)) {
if (relativePath) {
*relativePath = absolutePath.mid(folderPath.length());
relativePath->chop(1); // we added a '/' above
}
return folder;
}
}
if (relativePath)
relativePath->clear();
return nullptr;
}
void FolderMan::removeFolder(Folder *f)
{
if (!OC_ENSURE(f)) {
return;
}
qCInfo(lcFolderMan) << u"Removing " << f->path();
if (scheduler()->currentSync() == f) {
// abort the sync now
scheduler()->terminateCurrentSync(tr("Folder is about to be removed"));
}
f->setSyncPaused(true);
f->wipeForRemoval();
unloadFolder(f);
Q_EMIT folderRemoved(f);
Q_EMIT folderListChanged();
f->deleteLater();
saveFolders();
}
QString FolderMan::getBackupName(QString fullPathName) const
{
if (fullPathName.endsWith(QLatin1String("/")))
fullPathName.chop(1);
if (fullPathName.isEmpty())
return QString();
QString newName = fullPathName + tr(" (backup)");
QFileInfo fi(newName);
int cnt = 2;
do {
if (fi.exists()) {
newName = fullPathName + tr(" (backup %1)").arg(cnt++);
fi.setFile(newName);
}
} while (fi.exists());
return newName;
}
void FolderMan::setDirtyNetworkLimits()
{
for (auto *f : std::as_const(_folders)) {
// set only in busy folders. Otherwise they read the config anyway.
if (f && f->isSyncRunning()) {
f->setDirtyNetworkLimits();
}
}
}
TrayOverallStatusResult FolderMan::trayOverallStatus(const QVector<Folder *> &folders)
{
TrayOverallStatusResult result;
// if one of them has an error -> show error
// if one is paused, but others ok, show ok
//
for (auto *folder : folders) {
result.addResult(folder);
}
return result;
}
QString FolderMan::trayTooltipStatusString(
const SyncResult &result, bool paused)
{
QString folderMessage;
switch (result.status()) {
case SyncResult::Success:
[[fallthrough]];
case SyncResult::Problem:
if (result.hasUnresolvedConflicts()) {
folderMessage = tr("Sync was successful, unresolved conflicts.");
break;
}
[[fallthrough]];
default:
return Utility::enumToDisplayName(result.status());
}
if (paused) {
// sync is disabled.
folderMessage = tr("%1 (Sync is paused)").arg(folderMessage);
}
return folderMessage;
}
static QString checkPathForSyncRootMarkingRecursive(const QString &path, FolderMan::NewFolderType folderType, const QUuid &accountUuid)
{
std::pair<QString, QUuid> existingTags = Utility::getDirectorySyncRootMarkings(path);
if (!existingTags.first.isEmpty()) {
if (existingTags.first != Theme::instance()->orgDomainName()) {
// another application uses this as spaces root folder
return FolderMan::tr("The folder »%1« is already in use by application %2!").arg(path, existingTags.first);
}
// Looks good, it's our app, let's check the account tag:
switch (folderType) {
case FolderMan::NewFolderType::SpacesFolder:
if (existingTags.second == accountUuid) {
// Nice, that's what we like, the sync root for our account in our app. No error.
return {};
}
[[fallthrough]];
case FolderMan::NewFolderType::SpacesSyncRoot:
// It's our application but we don't want to create a spaces folder, so it must be another space root
return FolderMan::tr("The folder »%1« is already in use by another account.").arg(path);
}
}
QString parent = QFileInfo(path).path();
if (parent == path) { // root dir, stop recursing
return {};
}
return checkPathForSyncRootMarkingRecursive(parent, folderType, accountUuid);
}
QString FolderMan::checkPathValidityRecursive(const QString &path, FolderMan::NewFolderType folderType, const QUuid &accountUuid)
{
if (path.isEmpty()) {
return FolderMan::tr("No valid folder selected!");
}
#ifdef Q_OS_WIN
QNtfsPermissionCheckGuard ntfs_perm;
#endif
auto pathLenghtCheck = Folder::checkPathLength(path);
if (!pathLenghtCheck) {
return pathLenghtCheck.error();
}
const QFileInfo selectedPathInfo(path);
if (!selectedPathInfo.exists()) {
const QString parentPath = selectedPathInfo.path();
if (parentPath != path) {
return checkPathValidityRecursive(parentPath, folderType, accountUuid);
}
return FolderMan::tr("The selected path does not exist!");
}
if (numberOfSyncJournals(selectedPathInfo.filePath()) != 0) {
return FolderMan::tr("The folder »%1« is used in a folder sync connection!").arg(QDir::toNativeSeparators(selectedPathInfo.filePath()));
}
// At this point we know there is no syncdb in the parent hyrarchy, check for spaces sync root.
if (!selectedPathInfo.isDir()) {
return FolderMan::tr("The selected path is not a folder!");
}
if (!selectedPathInfo.isWritable()) {
return FolderMan::tr("You have no permission to write to the selected folder!");
}
return checkPathForSyncRootMarkingRecursive(path, folderType, accountUuid);
}
/*
* - spaces sync root not in syncdb folder
* - spaces sync root not in another spaces sync root
*
* - space not in syncdb folder
* - space *can* be in sync root
* - space not in spaces sync root of other account (check with account uuid)
*/
QString FolderMan::checkPathValidityForNewFolder(const QString &path, NewFolderType folderType, const QUuid &accountUuid) const
{
// check if the local directory isn't used yet in another sync
if (path.isEmpty()) {
return u"Passingg an empty path is not supported"_s;
}
const QString userDir = FileSystem::canonicalPath(path) + QLatin1Char('/');
for (auto f : _folders) {
const QString folderDir = FileSystem::canonicalPath(f->path()) + QLatin1Char('/');
const auto isChild = FileSystem::isChildPathOf2(folderDir, userDir);
if (isChild.testFlag(FileSystem::ChildResult::IsEqual)) {
return tr("There is already a sync from the server to this local folder. "
"Please pick another local folder!");
} else if (isChild.testFlag(FileSystem::ChildResult::IsChild)) {
return tr("The local folder »%1« already contains a folder used in a folder sync connection. "
"Please pick another local folder!")
.arg(QDir::toNativeSeparators(path));
}
if (FileSystem::isChildPathOf(userDir, folderDir)) {
return tr("The local folder »%1« is already contained in a folder used in a folder sync connection. "
"Please pick another local folder!")
.arg(QDir::toNativeSeparators(path));
}
}
const auto result = checkPathValidityRecursive(path, folderType, accountUuid);
if (!result.isEmpty()) {
return tr("Please pick another local folder for »%1«.").arg(result);
}
return {};
}
QString FolderMan::findGoodPathForNewSyncFolder(
const QString &basePath, const QString &newFolder, FolderMan::NewFolderType folderType, const QUuid &accountUuid)
{
OC_ASSERT(!accountUuid.isNull() || folderType == FolderMan::NewFolderType::SpacesSyncRoot);
// reserve extra characters to allow appending of a number
const QString normalisedPath = FileSystem::createPortableFileName(basePath, newFolder, std::string_view(" (100)").size());
// If the parent folder is a sync folder or contained in one, we can't
// possibly find a valid sync folder inside it.
// Example: Someone syncs their home directory. Then ~/foobar is not
// going to be an acceptable sync folder path for any value of foobar.
// If relativePath is empty, the path is equal to newFolder, and we will find a name in the following loop
QString relativePath;
if (FolderMan::instance()->folderForPath(FileSystem::canonicalPath(normalisedPath), &relativePath) && !relativePath.isEmpty()) {
// Any path with that parent is going to be unacceptable,
// so just keep it as-is.
return FileSystem::canonicalPath(normalisedPath);
}
// Count attempts and give up eventually
{
QString folder = normalisedPath;
for (int attempt = 2; attempt <= 100; ++attempt) {
if (!QFileInfo::exists(folder) && FolderMan::instance()->checkPathValidityForNewFolder(folder, folderType, accountUuid).isEmpty()) {
return FileSystem::canonicalPath(folder);
}
folder = normalisedPath + QStringLiteral(" (%1)").arg(attempt);
}
}
// we failed to find a non existing path
Q_ASSERT(false);
return FileSystem::canonicalPath(normalisedPath);
}
bool FolderMan::ignoreHiddenFiles() const
{
if (_folders.empty()) {
return true;
}
return _folders.first()->ignoreHiddenFiles();
}
void FolderMan::setIgnoreHiddenFiles(bool ignore)
{
// Note that the setting will revert to 'true' if all folders
// are deleted...
for (auto *folder : std::as_const(_folders)) {
folder->setIgnoreHiddenFiles(ignore);
}
saveFolders();
}
Result<void, QString> FolderMan::unsupportedConfiguration(const QString &path) const
{
auto it = _unsupportedConfigurationError.find(path);
if (it == _unsupportedConfigurationError.end()) {
it = _unsupportedConfigurationError.insert(path, [&]() -> Result<void, QString> {
if (numberOfSyncJournals(path) > 1) {
const QString error = tr("Multiple accounts are sharing the folder »%1«.\n"
"This configuration is know to lead to dataloss and is no longer supported.\n"
"Please consider removing this folder from the account and adding it again.")
.arg(path);
if (Theme::instance()->warnOnMultipleDb()) {
qCWarning(lcFolderMan) << error;
return error;
} else {
qCWarning(lcFolderMan) << error << u"this error is not displayed to the user as this is a branded"
<< u"client and the error itself might be a false positive caused by a previous broken migration";
}
}
return {};
}());
}
return *it;
}
bool FolderMan::isSpaceSynced(GraphApi::Space *space) const
{
auto it = std::find_if(_folders.cbegin(), _folders.cend(), [space](auto f) { return f->space() == space; });
return it != _folders.cend();
}
void FolderMan::slotReloadSyncOptions()
{
for (auto *f : std::as_const(_folders)) {
if (f) {
f->reloadSyncOptions();
}
}
}
Folder *FolderMan::addFolderFromWizard(const AccountStatePtr &accountStatePtr, FolderDefinition &&folderDefinition, bool useVfs)
{
if (!FolderMan::prepareFolder(folderDefinition.localPath())) {
return {};
}
folderDefinition.ignoreHiddenFiles = ignoreHiddenFiles();
if (useVfs) {
folderDefinition.virtualFilesMode = VfsPluginManager::instance().bestAvailableVfsMode();
}
auto newFolder = addFolder(accountStatePtr, folderDefinition);
if (newFolder) {
qCDebug(lcFolderMan) << u"Local sync folder" << folderDefinition.localPath() << u"successfully created!";
} else {
qCWarning(lcFolderMan) << u"Failed to create local sync folder!";
}
return newFolder;
}
Folder *FolderMan::addFolderFromFolderWizardResult(const AccountStatePtr &accountStatePtr, const SyncConnectionDescription &description)
{
FolderDefinition definition{accountStatePtr->account()->uuid(), description.davUrl, description.spaceId, description.displayName};
definition.setLocalPath(description.localPath);
auto f = addFolderFromWizard(accountStatePtr, std::move(definition), description.useVirtualFiles);
if (f) {
f->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, description.selectiveSyncBlackList);
f->setPriority(description.priority);
}
return f;
}
QString FolderMan::suggestSyncFolder(NewFolderType folderType, const QUuid &accountUuid)
{
return FolderMan::instance()->findGoodPathForNewSyncFolder(QDir::homePath(), Theme::instance()->appName(), folderType, accountUuid);
}
bool FolderMan::prepareFolder(const QString &folder)
{
if (!QFileInfo::exists(folder)) {
if (!OC_ENSURE(QDir().mkpath(folder))) {
return false;
}
FileSystem::setFolderMinimumPermissions(folder);
Folder::prepareFolder(folder, {}, {}, false);
}
return true;
}
std::unique_ptr<FolderMan> FolderMan::createInstance()
{
OC_ASSERT(!_instance);
_instance = new FolderMan();
return std::unique_ptr<FolderMan>(_instance);
}
} // namespace OCC
+304
View File
@@ -0,0 +1,304 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#pragma once
#include "gui/qsferaguilib.h"
#include "folder.h"
#include "scheduling/syncscheduler.h"
#include <QList>
#include <QObject>
#include <QQueue>
class TestFolderMigration;
namespace OCC {
class FolderMan;
namespace TestUtils {
// prototype for test friend
FolderMan *folderMan();
}
class Application;
class SyncResult;
class SocketApi;
class LockWatcher;
/**
* @brief Return object for Folder::trayOverallStatus.
* @ingroup gui
*/
class TrayOverallStatusResult
{
public:
QDateTime lastSyncDone;
void addResult(Folder *f);
const SyncResult &overallStatus() const;
private:
SyncResult _overallStatus;
};
class QSFERA_GUI_EXPORT FolderMan : public QObject
{
Q_OBJECT
public:
/**
* For a new folder, the type guides what kind of checks are done to ensure the new folder is not embedded in an existing one.
* Or in case of a space folder, that if the new folder is in a Space sync root, it is the sync root of the same account.
*/
enum class NewFolderType {
SpacesSyncRoot,
SpacesFolder,
};
struct SyncConnectionDescription
{
/***
- * The WebDAV URL for the sync connection.
*/
QUrl davUrl;
/***
* The id of the space
*/
QString spaceId;
/***
* The local folder used for the sync.
*/
QString localPath;
/***
* The Space name to display in the list of folders or an empty string.
*/
QString displayName;
/***
* Wether to use virtual files.
*/
bool useVirtualFiles;
uint32_t priority;
QSet<QString> selectiveSyncBlackList;
};
static QString suggestSyncFolder(NewFolderType folderType, const QUuid &accountUuid);
static QString checkPathValidityRecursive(const QString &path, FolderMan::NewFolderType folderType, const QUuid &accountUuid);
static std::unique_ptr<FolderMan> createInstance();
~FolderMan() override;
/**
* Helper to access the FolderMan instance
* Warning: may be null in unit tests
*/
// TODO: use acces throug ocApp and remove that instance pointer
static FolderMan *instance();
/// \returns empty if a downgrade of a folder was detected, otherwise it will return the number
/// of folders that were set up (note: this can be zero when no folders were configured).
std::optional<qsizetype> loadFolders();
const QVector<Folder *> &folders() const;
/** Adds a folder for an account, ensures the journal is gone and saves it in the settings.
*/
Folder *addFolder(const AccountStatePtr &accountState, const FolderDefinition &folderDefinition);
/**
* Adds a folder for an account. Used to be part of the wizard code base. Constructs the folder definition from the parameters.
* In case Wizard::SyncMode::SelectiveSync is used, nullptr is returned.
*/
Folder *addFolderFromWizard(const AccountStatePtr &accountStatePtr, FolderDefinition &&definition, bool useVfs);
Folder *addFolderFromFolderWizardResult(const AccountStatePtr &accountStatePtr, const SyncConnectionDescription &config);
/** Removes a folder */
void removeFolder(Folder *);
/**
* Returns the folder which the file or directory stored in path is in
*
* Optionally, the path relative to the found folder is returned in
* relativePath.
*/
Folder *folderForPath(const QString &path, QString *relativePath = nullptr);
/**
* Ensures that a given directory does not contain a sync journal file.
*
* @returns false if the journal could not be removed, true otherwise.
*/
static bool ensureJournalGone(const QString &journalDbFile);
/// Produce text for use in the tray tooltip
static QString trayTooltipStatusString(const SyncResult &result, bool paused);
/**
* Compute status summarizing multiple folders
* @return tuple containing folders, status, unresolvedConflicts and lastSyncDone
*/
static TrayOverallStatusResult trayOverallStatus(const QVector<Folder *> &folders);
SocketApi *socketApi();
/**
* Check if @a path is a valid path for a new folder considering the already sync'ed items.
* Make sure that this folder, or any subfolder is not sync'ed already.
*
* @returns an empty string if it is allowed, or an error if it is not allowed
*/
QString checkPathValidityForNewFolder(const QString &path, NewFolderType folderType, const QUuid &accountUuid) const;
/**
* Attempts to find a non-existing, acceptable path for creating a new sync folder.
*
* Uses \a basePath as the baseline. It'll return this path if it's acceptable.
*
* Note that this can fail. If someone syncs ~ and \a basePath is ~/QSfera, no
* subfolder of ~ would be a good candidate. When that happens \a basePath
* is returned.
*/
static QString findGoodPathForNewSyncFolder(const QString &basePath, const QString &newFolder, NewFolderType folderType, const QUuid &accountUuid);
/**
* While ignoring hidden files can theoretically be switched per folder,
* it's currently a global setting that users can only change for all folders
* at once.
* These helper functions can be removed once it's properly per-folder.
*/
bool ignoreHiddenFiles() const;
void setIgnoreHiddenFiles(bool ignore);
/**
* Returns true if any folder is currently syncing.
*
* This might be a FolderMan-scheduled sync, or a externally
* managed sync like a placeholder hydration.
*/
bool isAnySyncRunning() const;
/** Removes all folders */
void unloadAndDeleteAllFolders();
/**
* If enabled is set to false, no new folders will start to sync.
* The current one will finish.
*/
void setSyncEnabled(bool);
SyncScheduler *scheduler() { return _scheduler; }
/** Queues all folders for syncing. */
void scheduleAllFolders();
void setDirtyNetworkLimits();
/** If the folder configuration is no longer supported this will return an error string */
Result<void, QString> unsupportedConfiguration(const QString &path) const;
[[nodiscard]] bool isSpaceSynced(GraphApi::Space *space) const;
Q_SIGNALS:
/**
* signal to indicate a folder has changed its sync state.
*
* Attention: The folder may be zero. Do a general update of the state then.
*/
void folderSyncStateChange(Folder *);
/**
* Emitted whenever the list of configured folders changes.
*/
void folderListChanged();
void folderRemoved(Folder *folder);
public Q_SLOTS:
/**
* Schedules folders of newly connected accounts, terminates and
* de-schedules folders of disconnected accounts.
*/
void slotIsConnectedChanged();
/**
* Triggers a sync run once the lock on the given file is removed.
*
* Automatically detemines the folder that's responsible for the file.
* See slotWatchedFileUnlocked().
*/
void slotSyncOnceFileUnlocks(const QString &path, FileSystem::LockMode mode);
/// This slot will tell all sync engines to reload the sync options.
void slotReloadSyncOptions();
private Q_SLOTS:
void slotFolderCanSyncChanged();
void slotRemoveFoldersForAccount(const AccountStatePtr &accountState);
void slotServerVersionChanged(Account *account);
private:
explicit FolderMan();
[[nodiscard]] static bool prepareFolder(const QString &folder);
/** Adds a new folder, does not add it to the account settings and
* does not set an account on the new folder.
*/
Folder *addFolderInternal(FolderDefinition folderDefinition,
const AccountStatePtr &accountState, std::unique_ptr<Vfs> vfs);
/* unloads a folder object, does not delete it */
void unloadFolder(Folder *);
void saveFolders();
// finds all folder configuration files
// and create the folders
QString getBackupName(QString fullPathName) const;
// makes the folder known to the socket api
void registerFolderWithSocketApi(Folder *folder);
QVector<Folder *> _folders;
QString _folderConfigPath;
/// Folder aliases from the settings that weren't read
QSet<QString> _additionalBlockedFolderAliases;
/// Watches files that couldn't be synced due to locks
QScopedPointer<LockWatcher> _lockWatcher;
/// Scheduled folders that should be synced as soon as possible
SyncScheduler *_scheduler;
std::unique_ptr<SocketApi> _socketApi;
mutable QMap<QString, Result<void, QString>> _unsupportedConfigurationError;
static FolderMan *_instance;
friend class OCC::Application;
friend class ::TestFolderMigration;
};
} // namespace OCC
+469
View File
@@ -0,0 +1,469 @@
/*
* Copyright (C) by Klaas Freitag <freitag@kde.org>
*
* 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.
*/
#include "folderstatusmodel.h"
#include "account.h"
#include "accountstate.h"
#include "folderman.h"
#include "gui/networkinformation.h"
#include "theme.h"
#include "libsync/graphapi/space.h"
#include "libsync/graphapi/spacesmanager.h"
#include <QApplication>
#include <QFileIconProvider>
#include <QRandomGenerator>
#include <set>
using namespace std::chrono_literals;
Q_DECLARE_METATYPE(QPersistentModelIndex)
namespace OCC {
Q_LOGGING_CATEGORY(lcFolderStatus, "gui.folder.model", QtInfoMsg)
namespace {
// minimum delay between progress updates
constexpr auto progressUpdateTimeOutC = 1s;
QChar statusIconName(Folder *f)
{
auto status = f->syncResult();
if (!f->accountState()->isConnected()) {
status.setStatus(SyncResult::Status::Offline);
} else if (f->isSyncPaused() || NetworkInformation::instance()->isBehindCaptivePortal() || NetworkInformation::instance()->isMetered()) {
status.setStatus(SyncResult::Status::Paused);
}
return status.glype();
}
class SubFolderInfo
{
public:
class Progress
{
public:
Progress() { }
QString _progressString;
QString _overallSyncString;
float _overallPercent = 0;
};
SubFolderInfo(Folder *folder)
: _folder(folder)
{
}
Folder *const _folder;
Progress _progress;
std::chrono::steady_clock::time_point _lastProgressUpdated = std::chrono::steady_clock::now();
ProgressInfo::Status _lastProgressUpdateStatus = ProgressInfo::None;
};
void computeProgress(const ProgressInfo &progress, SubFolderInfo::Progress *pi)
{
// find the single item to display: This is going to be the bigger item, or the last completed
// item if no items are in progress.
SyncFileItem curItem = progress._lastCompletedItem;
qint64 curItemProgress = -1; // -1 means finished
qint64 biggerItemSize = 0;
quint64 estimatedUpBw = 0;
quint64 estimatedDownBw = 0;
QStringList allFilenames;
for (const auto &citm : progress._currentItems) {
if (curItemProgress == -1 || (ProgressInfo::isSizeDependent(citm._item) && biggerItemSize < citm._item._size)) {
curItemProgress = citm._progress.completed();
curItem = citm._item;
biggerItemSize = citm._item._size;
}
if (citm._item._direction != SyncFileItem::Up) {
estimatedDownBw += progress.fileProgress(citm._item).estimatedBandwidth;
} else {
estimatedUpBw += progress.fileProgress(citm._item).estimatedBandwidth;
}
allFilenames.append(QApplication::translate("FolderStatus", "'%1'").arg(citm._item.localName()));
}
if (curItemProgress == -1) {
curItemProgress = curItem._size;
}
const QString itemFileName = curItem.localName();
const QString kindString = Progress::asActionString(curItem);
QString fileProgressString;
if (ProgressInfo::isSizeDependent(curItem)) {
// quint64 estimatedBw = progress.fileProgress(curItem).estimatedBandwidth;
if (estimatedUpBw || estimatedDownBw) {
/*
//: Example text: "uploading foobar.png (1MB of 2MB) time left 2 minutes at a rate of 24Kb/s"
fileProgressString = tr("%1 %2 (%3 of %4) %5 left at a rate of %6/s")
.arg(kindString, itemFileName, s1, s2,
Utility::durationToDescriptiveString(progress.fileProgress(curItem).estimatedEta),
Utility::octetsToString(estimatedBw) );
*/
//: Example text: "Syncing 'foo.txt', 'bar.txt'"
fileProgressString = QApplication::translate("FolderStatus", "Syncing %1").arg(allFilenames.join(QStringLiteral(", ")));
if (estimatedDownBw > 0) {
fileProgressString.append(QApplication::translate("FolderStatus", ", ⬇️ %1/s").arg(Utility::octetsToString(estimatedDownBw)));
}
if (estimatedUpBw > 0) {
fileProgressString.append(QApplication::translate("FolderStatus", ", ⬆️ %1/s").arg(Utility::octetsToString(estimatedUpBw)));
}
} else {
//: Example text: "uploading foobar.png (2MB of 2MB)"
fileProgressString = QApplication::translate("FolderStatus", "%1 %2 (%3 of %4)")
.arg(kindString, itemFileName, Utility::octetsToString(curItemProgress), Utility::octetsToString(curItem._size));
}
} else if (!kindString.isEmpty()) {
//: Example text: "uploading foobar.png"
fileProgressString = QApplication::translate("FolderStatus", "%1 %2").arg(kindString, itemFileName);
}
pi->_progressString = fileProgressString;
// overall progress
qint64 completedSize = progress.completedSize();
qint64 completedFile = progress.completedFiles();
qint64 currentFile = progress.currentFile();
qint64 totalSize = qMax(completedSize, progress.totalSize());
qint64 totalFileCount = qMax(currentFile, progress.totalFiles());
QString overallSyncString;
if (totalSize > 0) {
const QString s1 = Utility::octetsToString(completedSize);
const QString s2 = Utility::octetsToString(totalSize);
if (progress.trustEta()) {
//: Example text: "5 minutes left, 12 MB of 345 MB, file 6 of 7"
overallSyncString = QApplication::translate("FolderStatus", "%5 left, %1 of %2, file %3 of %4")
.arg(s1, s2)
.arg(currentFile)
.arg(totalFileCount)
.arg(Utility::durationToDescriptiveString1(std::chrono::milliseconds(progress.totalProgress().estimatedEta)));
} else {
//: Example text: "12 MB of 345 MB, file 6 of 7"
overallSyncString = QApplication::translate("FolderStatus", "%1 of %2, file %3 of %4").arg(s1, s2).arg(currentFile).arg(totalFileCount);
}
} else if (totalFileCount > 0) {
// Don't attempt to estimate the time left if there is no kb to transfer.
overallSyncString = QApplication::translate("FolderStatus", "file %1 of %2").arg(currentFile).arg(totalFileCount);
}
pi->_overallSyncString = overallSyncString;
int overallPercent = 0;
if (totalFileCount > 0) {
// Add one 'byte' for each file so the percentage is moving when deleting or renaming files
overallPercent = qRound(double(completedSize + completedFile) / double(totalSize + totalFileCount) * 100.0);
}
pi->_overallPercent = qBound(0, overallPercent, 100);
}
}
class FolderStatusModelPrivate
{
public:
AccountStatePtr _accountState;
std::vector<std::unique_ptr<SubFolderInfo>> _folders;
};
FolderStatusModel::FolderStatusModel(QObject *parent)
: QAbstractListModel(parent)
, d_ptr(new FolderStatusModelPrivate)
{
}
FolderStatusModel::~FolderStatusModel()
{
Q_D(FolderStatusModel);
delete d;
}
void FolderStatusModel::setAccountState(const AccountStatePtr &accountState)
{
Q_D(FolderStatusModel);
beginResetModel();
d->_folders.clear();
if (d->_accountState != accountState) {
Q_ASSERT(!d->_accountState);
d->_accountState = accountState;
connect(FolderMan::instance(), &FolderMan::folderSyncStateChange, this, &FolderStatusModel::slotFolderSyncStateChange);
connect(accountState->account()->spacesManager(), &GraphApi::SpacesManager::updated, this,
[this] { Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0)); });
connect(accountState->account()->spacesManager(), &GraphApi::SpacesManager::spaceChanged, this, [this](auto *space) {
Q_D(FolderStatusModel);
for (int i = 0; i < rowCount(); ++i) {
if (d->_folders[i]->_folder->space() == space) {
Q_EMIT dataChanged(index(i, 0), index(i, 0));
break;
}
}
});
}
for (const auto &f : FolderMan::instance()->folders()) {
if (!accountState)
break;
if (f->accountState() != accountState)
continue;
d->_folders.push_back(std::make_unique<SubFolderInfo>(f));
connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo, this, [f, this](Folder *folder, const ProgressInfo &progress) {
if (folder == f) {
slotSetProgress(progress, f);
}
});
}
endResetModel();
}
QVariant FolderStatusModel::data(const QModelIndex &index, int role) const
{
Q_D(const FolderStatusModel);
if (!index.isValid())
return QVariant();
if (role == Qt::EditRole)
return QVariant();
const auto &folderInfo = d->_folders.at(index.row());
auto f = folderInfo->_folder;
if (!f)
return QVariant();
auto getErrors = [f] {
auto errors = f->syncResult().errorStrings();
const auto legacyError = FolderMan::instance()->unsupportedConfiguration(f->path());
if (!legacyError) {
errors.append(legacyError.error());
}
if (f->syncResult().hasUnresolvedConflicts()) {
errors.append(tr("There are unresolved conflicts."));
}
return errors;
};
auto getDescription = [f] {
if (auto *space = f->space()) {
if (!space->drive().getDescription().isEmpty()) {
return space->drive().getDescription();
}
}
return tr("Local folder: %1").arg(f->shortGuiLocalPath());
};
switch (static_cast<Roles>(role)) {
case Roles::Subtitle:
return getDescription();
case Roles::FolderErrorMsg:
return getErrors();
case Roles::DisplayName:
return f->displayName();
case Roles::FolderStatusIcon:
return statusIconName(f);
case Roles::SyncProgressItemString:
return folderInfo->_progress._progressString;
case Roles::SyncProgressOverallPercent:
return folderInfo->_progress._overallPercent / 100.0;
case Roles::SyncProgressOverallString:
return folderInfo->_progress._overallSyncString;
case Roles::Priority:
// everything will be sorted in descending order, multiply the priority by 100 and prefer A over Z by appling a negative factor
return QVariant::fromValue(f->priority() * 100 - (f->displayName().isEmpty() ? 0 : static_cast<int64_t>(f->displayName().at(0).toLower().unicode())));
case Roles::Quota: {
qint64 used{};
qint64 total{};
if (auto *space = f->space()) {
const auto quota = space->drive().getQuota();
if (quota.isValid()) {
used = quota.getUsed();
total = quota.getTotal();
}
}
if (total <= 0) {
return {};
}
return tr("%1 of %2 used").arg(Utility::octetsToString(used), Utility::octetsToString(total));
}
case Roles::Folder:
return QVariant::fromValue(f);
case Roles::AccessibleDescriptionRole: {
QStringList desc = {f->displayName(), Utility::enumToDisplayName(f->syncResult().status())};
desc << getErrors();
if (f->syncResult().status() == SyncResult::SyncRunning) {
desc << folderInfo->_progress._overallSyncString << QStringLiteral("%1%").arg(QString::number(folderInfo->_progress._overallPercent));
}
desc << getDescription();
return desc.join(QLatin1Char(','));
}
}
return {};
}
Folder *FolderStatusModel::folder(const QModelIndex &index) const
{
Q_D(const FolderStatusModel);
Q_ASSERT(checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid));
return d->_folders.at(index.row())->_folder;
}
int FolderStatusModel::rowCount(const QModelIndex &parent) const
{
Q_D(const FolderStatusModel);
Q_ASSERT(!parent.isValid());
return static_cast<int>(d->_folders.size());
}
QHash<int, QByteArray> FolderStatusModel::roleNames() const
{
return {
{static_cast<int>(Roles::DisplayName), "displayName"},
{static_cast<int>(Roles::Subtitle), "subtitle"},
{static_cast<int>(Roles::FolderStatusIcon), "statusIcon"},
{static_cast<int>(Roles::SyncProgressOverallPercent), "progress"},
{static_cast<int>(Roles::SyncProgressOverallString), "overallText"},
{static_cast<int>(Roles::SyncProgressItemString), "itemText"},
{static_cast<int>(Roles::FolderErrorMsg), "errorMsg"},
{static_cast<int>(Roles::Quota), "quota"},
{static_cast<int>(Roles::Folder), "folder"},
{static_cast<int>(Roles::AccessibleDescriptionRole), "accessibleDescription"},
};
}
void FolderStatusModel::slotUpdateFolderState(Folder *folder)
{
if (!folder) {
return;
}
const auto folderIndex = indexOf(folder);
if (folderIndex != -1) {
Q_EMIT dataChanged(index(folderIndex, 0), index(folderIndex, 0));
}
}
void FolderStatusModel::slotSetProgress(const ProgressInfo &progress, Folder *f)
{
Q_D(FolderStatusModel);
if (!qobject_cast<QWidget *>(QObject::parent())->isVisible()) {
return; // for https://github.com/owncloud/client/issues/2648#issuecomment-71377909
}
const int folderIndex = indexOf(f);
if (folderIndex < 0) {
return;
}
const auto &folder = d->_folders.at(folderIndex);
auto *pi = &folder->_progress;
// depending on the use of virtual files or small files this slot might be called very often.
// throttle the model updates to prevent an needlessly high cpu usage used on ui updates.
if (folder->_lastProgressUpdateStatus != progress.status() || (std::chrono::steady_clock::now() - folder->_lastProgressUpdated > progressUpdateTimeOutC)) {
folder->_lastProgressUpdateStatus = progress.status();
switch (progress.status()) {
case ProgressInfo::None:
Q_UNREACHABLE();
break;
case ProgressInfo::Discovery:
if (!progress._currentDiscoveredRemoteFolder.isEmpty()) {
pi->_overallSyncString = tr("Checking for changes in remote »%1«").arg(progress._currentDiscoveredRemoteFolder);
} else if (!progress._currentDiscoveredLocalFolder.isEmpty()) {
pi->_overallSyncString = tr("Checking for changes in local »%1«").arg(progress._currentDiscoveredLocalFolder);
}
break;
case ProgressInfo::Reconcile:
pi->_overallSyncString = tr("Reconciling changes");
break;
case ProgressInfo::Propagation:
Q_FALLTHROUGH();
case ProgressInfo::Done:
computeProgress(progress, pi);
}
Q_EMIT dataChanged(index(folderIndex, 0), index(folderIndex, 0));
folder->_lastProgressUpdated = std::chrono::steady_clock::now();
}
}
int FolderStatusModel::indexOf(Folder *f) const
{
Q_D(const FolderStatusModel);
const auto found = std::find_if(d->_folders.cbegin(), d->_folders.cend(), [f](const auto &it) { return it->_folder == f; });
if (found == d->_folders.cend()) {
return -1;
}
return std::distance(d->_folders.cbegin(), found);
}
void FolderStatusModel::slotFolderSyncStateChange(Folder *f)
{
Q_D(FolderStatusModel);
if (!f) {
return;
}
const int folderIndex = indexOf(f);
if (folderIndex < 0) {
return;
}
auto &pi = d->_folders.at(folderIndex)->_progress;
SyncResult::Status state = f->syncResult().status();
if (!f->canSync()) {
// Reset progress info.
pi = {};
} else {
switch (state) {
case SyncResult::Queued:
pi = {};
pi._overallSyncString = Utility::enumToDisplayName(state);
break;
case SyncResult::Problem:
[[fallthrough]];
case SyncResult::Success:
[[fallthrough]];
case SyncResult::Error:
// Reset the progress info after a sync.
pi = {};
break;
case SyncResult::Undefined:
[[fallthrough]];
case SyncResult::SyncRunning:
[[fallthrough]];
case SyncResult::Paused:
[[fallthrough]];
case SyncResult::Offline:
[[fallthrough]];
case SyncResult::SetupError:
break;
}
}
// update the icon etc. now
slotUpdateFolderState(f);
}
void FolderStatusModel::resetFolders()
{
Q_D(FolderStatusModel);
setAccountState(d->_accountState);
}
} // namespace OCC
+89
View File
@@ -0,0 +1,89 @@
/*
* Copyright (C) by Klaas Freitag <freitag@kde.org>
*
* 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.
*/
#ifndef FOLDERSTATUSMODEL_H
#define FOLDERSTATUSMODEL_H
#include "accountfwd.h"
#include "progressdispatcher.h"
#include <QAbstractItemModel>
#include <QElapsedTimer>
#include <QLoggingCategory>
#include <QQuickImageProvider>
#include <QtQml/QQmlEngine>
class QNetworkReply;
namespace OCC {
Q_DECLARE_LOGGING_CATEGORY(lcFolderStatus)
class Folder;
class PropfindJob;
class FolderStatusModelPrivate;
/**
* @brief The FolderStatusModel class
* @ingroup gui
*/
class FolderStatusModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
public:
enum class Roles {
AccessibleDescriptionRole = Qt::AccessibleDescriptionRole,
DisplayName = Qt::UserRole + 1, // must be 0 as it is also used from the default delegate
Subtitle,
FolderErrorMsg,
SyncProgressOverallPercent,
SyncProgressOverallString,
SyncProgressItemString,
Priority,
Quota,
FolderStatusIcon,
Folder
};
Q_ENUMS(Roles)
FolderStatusModel(QObject *parent = nullptr);
~FolderStatusModel() override;
void setAccountState(const AccountStatePtr &accountState);
Folder *folder(const QModelIndex &index) const;
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
public Q_SLOTS:
void slotUpdateFolderState(Folder *);
void resetFolders();
void slotSetProgress(const ProgressInfo &progress, Folder *f);
private Q_SLOTS:
void slotFolderSyncStateChange(Folder *f);
private:
int indexOf(Folder *f) const;
Q_DECLARE_PRIVATE(FolderStatusModel)
FolderStatusModelPrivate *d_ptr;
};
} // namespace OCC
#endif // FOLDERSTATUSMODEL_H
+194
View File
@@ -0,0 +1,194 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
// event masks
#include "folderwatcher.h"
#include <cstdint>
#include <QFlags>
#include <QTimer>
#if defined(Q_OS_WIN)
#include "folderwatcher_win.h"
#elif defined(Q_OS_MAC)
#include "folderwatcher_mac.h"
#elif defined(Q_OS_UNIX)
#include "folderwatcher_linux.h"
#endif
#include "folder.h"
#include "filesystem.h"
using namespace std::chrono_literals;
namespace {
constexpr auto notificationTimeoutC = 10s;
}
namespace OCC {
Q_LOGGING_CATEGORY(lcFolderWatcher, "gui.folderwatcher", QtInfoMsg)
FolderWatcher::FolderWatcher(Folder *folder)
: QObject(folder)
, _folder(folder)
{
_timer.setInterval(notificationTimeoutC);
_timer.setSingleShot(true);
connect(&_timer, &QTimer::timeout, this, [this] {
auto paths = popChangeSet();
Q_ASSERT(!paths.empty());
if (!paths.isEmpty()) {
qCInfo(lcFolderWatcher) << u"Detected changes in paths:" << paths;
Q_EMIT pathChanged(paths);
}
});
}
FolderWatcher::~FolderWatcher()
{
}
void FolderWatcher::init(const QString &root)
{
_d.reset(new FolderWatcherPrivate(this, root));
}
bool FolderWatcher::pathIsIgnored(const QString &path) const
{
Q_ASSERT(!path.isEmpty());
Q_ASSERT(_folder);
if (!QFileInfo::exists(path)) {
std::error_code ec;
const auto relPath =
FileSystem::fromFilesystemPath(std::filesystem::proximate(FileSystem::toFilesystemPath(path), FileSystem::toFilesystemPath(_folder->path()), ec));
Q_ASSERT(!ec);
if (ec) {
qCDebug(lcFolderWatcher) << u"* Failed to lookup record for path:" << path << u"error:" << ec.message();
return false;
}
if (auto record = _folder->journalDb()->getFileRecord(relPath); record.isValid()) {
// we know about the file, it got removed, we should not ignore that
qCDebug(lcFolderWatcher) << u"* Not ignoring removed file" << path;
return false;
}
// probably a temporary file that no longer exists
qCDebug(lcFolderWatcher) << u"* Ignoring file" << path << u"It no longer exists and we don't have it in the database.";
return true;
}
if (_folder->isFileExcludedAbsolute(path) && !Utility::isConflictFile(path)) {
qCDebug(lcFolderWatcher) << u"* Ignoring file" << path;
return true;
}
return false;
}
bool FolderWatcher::isReliable() const
{
return _isReliable;
}
void FolderWatcher::startNotificatonTest(const QString &path)
{
#ifdef Q_OS_MAC
// Testing the folder watcher on OSX is harder because the watcher
// automatically discards changes that were performed by our process.
// It would still be useful to test but the OSX implementation
// is deferred until later.
return;
#endif
Q_ASSERT(_testNotificationPath.isEmpty());
Q_ASSERT(!path.isEmpty());
_testNotificationPath = path;
// Don't do the local file modification immediately:
// wait for FolderWatchPrivate::_ready
startNotificationTestWhenReady();
}
void FolderWatcher::startNotificationTestWhenReady()
{
if (!_testNotificationPath.isEmpty()) {
// we already received the notification
return;
}
if (!_d->isReady()) {
QTimer::singleShot(1s, this, &FolderWatcher::startNotificationTestWhenReady);
return;
}
if (OC_ENSURE(QFile::exists(_testNotificationPath))) {
const auto mtime = FileSystem::getModTime(_testNotificationPath);
FileSystem::setModTime(_testNotificationPath, mtime + 1);
} else {
QFile f(_testNotificationPath);
f.open(QIODevice::WriteOnly | QIODevice::Append);
}
QTimer::singleShot(notificationTimeoutC + 5s, this, [this]() {
if (!_testNotificationPath.isEmpty())
Q_EMIT becameUnreliable(tr("The watcher did not receive a test notification."));
_testNotificationPath.clear();
});
}
int FolderWatcher::testLinuxWatchCount() const
{
#ifdef Q_OS_LINUX
return _d->testWatchCount();
#else
return -1;
#endif
}
void FolderWatcher::addChanges(QSet<QString> &&paths)
{
Q_ASSERT(thread() == QThread::currentThread());
// the timer must be inactive if we haven't received changes yet
Q_ASSERT((!_timer.isActive() && _changeSet.isEmpty()) || !_changeSet.isEmpty());
Q_ASSERT(!paths.isEmpty());
// ------- handle ignores:
auto it = paths.cbegin();
while (it != paths.cend()) {
// we cause a file change from time to time to check whether the folder watcher works as expected
if (!_testNotificationPath.isEmpty() && Utility::fileNamesEqual(*it, _testNotificationPath)) {
_testNotificationPath.clear();
}
if (pathIsIgnored(*it)) {
it = paths.erase(it);
} else {
++it;
}
}
if (!paths.isEmpty()) {
_changeSet.unite(paths);
if (!_timer.isActive()) {
_timer.start();
// promote that we will report changes once _timer times out
Q_EMIT changesDetected();
}
}
}
QSet<QString> FolderWatcher::popChangeSet()
{
// stop the timer as we pop all queued changes
_timer.stop();
return std::move(_changeSet);
}
} // namespace OCC
+131
View File
@@ -0,0 +1,131 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#pragma once
#include "gui/qsferaguilib.h"
#include <QElapsedTimer>
#include <QList>
#include <QLoggingCategory>
#include <QScopedPointer>
#include <QSet>
#include <QString>
#include <QStringList>
#include <QTimer>
namespace OCC {
Q_DECLARE_LOGGING_CATEGORY(lcFolderWatcher)
class FolderWatcherPrivate;
class Folder;
/**
* @brief Monitors a directory recursively for changes
*
* Folder Watcher monitors a directory and its sub directories
* for changes in the local file system. Changes are signalled
* through the pathChanged() signal.
*
* @ingroup gui
*/
class QSFERA_GUI_EXPORT FolderWatcher : public QObject
{
Q_OBJECT
public:
// Construct, connect signals, call init()
explicit FolderWatcher(Folder *folder = nullptr);
~FolderWatcher() override;
/**
* @param root Path of the root of the folder
*/
void init(const QString &root);
/* Check if the path is ignored. */
virtual bool pathIsIgnored(const QString &path) const;
/**
* Returns false if the folder watcher can't be trusted to capture all
* notifications.
*
* For example, this can happen on linux if the inotify user limit from
* /proc/sys/fs/inotify/max_user_watches is exceeded.
*/
bool isReliable() const;
/**
* Triggers a change in the path and verifies a notification arrives.
*
* If no notification is seen, the folderwatcher marks itself as unreliable.
* The path must be ignored by the watcher.
*/
void startNotificatonTest(const QString &path);
/// For testing linux behavior only
int testLinuxWatchCount() const;
// pop the accumulated changes
QSet<QString> popChangeSet();
Q_SIGNALS:
/** Emitted when one of the watched directories or one
* of the contained files is changed. */
void pathChanged(const QSet<QString> &path);
/**
* We detected a file change, this signal can be used to trigger the prepareSync state
*/
void changesDetected();
/**
* Emitted if some notifications were lost.
*
* Would happen, for example, if the number of pending notifications
* exceeded the allocated buffer size on Windows. Note that the folder
* watcher could still be able to capture all future notifications -
* i.e. isReliable() is orthogonal to losing changes occasionally.
*/
void lostChanges();
/**
* Signals when the watcher became unreliable. The string is a translated
* message that can be shown to users.
*/
void becameUnreliable(const QString &message);
private Q_SLOTS:
void startNotificationTestWhenReady();
protected:
// called from the implementations to indicate a change in path
void addChanges(QSet<QString> &&paths);
private:
QScopedPointer<FolderWatcherPrivate> _d;
QTimer _timer;
QSet<QString> _changeSet;
Folder *_folder;
bool _isReliable = true;
/** Path of the expected test notification */
QString _testNotificationPath;
friend class FolderWatcherPrivate;
};
}
+224
View File
@@ -0,0 +1,224 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#include <cerrno>
#include <sys/inotify.h>
#include <unistd.h>
#include "folder.h"
#include "folderwatcher_linux.h"
#include <QObject>
#include <QStringList>
#include <QVarLengthArray>
namespace OCC {
FolderWatcherPrivate::FolderWatcherPrivate(FolderWatcher *p, const QString &path)
: QObject()
, _parent(p)
, _folder(path)
{
_fd = inotify_init();
if (_fd != -1) {
_socket.reset(new QSocketNotifier(_fd, QSocketNotifier::Read));
connect(_socket.data(), &QSocketNotifier::activated, this, &FolderWatcherPrivate::slotReceivedNotification);
} else {
qCWarning(lcFolderWatcher) << u"notify_init() failed: " << strerror(errno);
}
QMetaObject::invokeMethod(this, [path, this] { slotAddFolderRecursive(path); });
}
// attention: result list passed by reference!
bool FolderWatcherPrivate::findFoldersBelow(const QDir &dir, QStringList &fullList)
{
if (!dir.exists()) {
qCDebug(lcFolderWatcher) << u" - non existing path coming in: " << dir.absolutePath();
return false;
} else if (!dir.isReadable()) {
qCDebug(lcFolderWatcher) << u" - path without read permissions coming in: " << dir.absolutePath();
return false;
}
const QDir::Filters filter = QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks | QDir::Hidden;
bool ok = true;
for (const QString &path : dir.entryList({ QStringLiteral("*") }, filter)) {
const QString fullPath(dir.path() + QLatin1String("/") + path);
fullList.append(fullPath);
ok &= findFoldersBelow(QDir(fullPath), fullList);
}
return ok;
}
void FolderWatcherPrivate::inotifyRegisterPath(const QString &path)
{
if (path.isEmpty()) {
return;
}
int wd = inotify_add_watch(_fd, path.toUtf8().constData(),
IN_CLOSE_WRITE | IN_ATTRIB | IN_MOVE | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF | IN_UNMOUNT | IN_ONLYDIR);
if (wd != -1) {
_watchToPath.insert(wd, path);
_pathToWatch.insert(path, wd);
} else {
// If we're running out of memory or inotify watches, become unreliable.
if (_parent->_isReliable && (errno == ENOMEM || errno == ENOSPC)) {
_parent->_isReliable = false;
Q_EMIT _parent->becameUnreliable(tr("This problem usually happens when the inotify watches are exhausted. "
"Check the FAQ for details."));
}
}
}
void FolderWatcherPrivate::slotAddFolderRecursive(const QString &path)
{
if (_pathToWatch.contains(path)) {
return;
}
int subdirCount = 0;
qCDebug(lcFolderWatcher) << u"(+) Watcher:" << path;
QDir inPath(path);
inotifyRegisterPath(inPath.absolutePath());
QStringList allSubfolders;
if (!findFoldersBelow(QDir(path), allSubfolders)) {
qCWarning(lcFolderWatcher).nospace() << u"Could not traverse all sub folders of '" << path << u"'";
}
for (const auto &subfolder : std::as_const(allSubfolders)) {
QDir folder(subfolder);
if (folder.exists() && !_pathToWatch.contains(folder.absolutePath())) {
++subdirCount;
if (_parent->pathIsIgnored(subfolder)) {
qCDebug(lcFolderWatcher) << u"* Not adding" << folder.path();
continue;
}
inotifyRegisterPath(folder.absolutePath());
} else {
qCDebug(lcFolderWatcher) << u" `-> discarded:" << folder.path();
}
}
if (subdirCount > 0) {
qCDebug(lcFolderWatcher) << u" `-> and" << subdirCount << u"subdirectories";
}
qCDebug(lcFolderWatcher) << u" --- Finished scanning" << path;
}
void FolderWatcherPrivate::slotReceivedNotification(int fd)
{
int len;
QVarLengthArray<char, 2048> buffer(2048);
while (true) {
len = read(fd, buffer.data(), buffer.size());
auto error = errno;
/**
* From inotify documentation:
*
* The behavior when the buffer given to read(2) is too
* small to return information about the next event
* depends on the kernel version: in kernels before 2.6.21,
* read(2) returns 0; since kernel 2.6.21, read(2) fails with
* the error EINVAL.
*/
if (len < 0 && error == EINVAL) {
// double the buffer size
buffer.resize(buffer.size() * 2);
/* and try again ... */
} else {
// successful read
break;
}
}
QSet<QString> paths;
// iterate over events in buffer
struct inotify_event *event = nullptr;
for (size_t bytePosition = 0; // start at the beginning of the buffer
bytePosition + sizeof(inotify_event) < static_cast<unsigned>(len); // check that we still have at least sizeof(inotify_event) left in the buffer
bytePosition += sizeof(inotify_event) + (event ? event->len : 0)) { // skip over the header and event-payload
// cast into an inotify_event
event = reinterpret_cast<struct inotify_event *>(&buffer[bytePosition]);
if (event == nullptr) {
qCDebug(lcFolderWatcher) << u"NULL event";
continue;
}
if (event->len == 0 || event->wd <= -1) {
continue;
}
const QByteArray fileName(event->name);
// Filter out journal changes - redundant with filtering in FolderWatcher::pathIsIgnored.
if (fileName.startsWith("._sync_")
|| fileName.startsWith(".csync_journal.db")
|| fileName.startsWith(".sync_")) {
continue;
}
const QString p = _watchToPath[event->wd] + QLatin1Char('/') + QString::fromUtf8(fileName);
paths.insert(p);
if ((event->mask & (IN_MOVED_TO | IN_CREATE))
&& QFileInfo(p).isDir()
&& !_parent->pathIsIgnored(p)) {
slotAddFolderRecursive(p);
}
if (event->mask & (IN_MOVED_FROM | IN_DELETE)) {
removeFoldersBelow(p);
}
}
if (!paths.isEmpty()) {
_parent->addChanges(std::move(paths));
}
}
void FolderWatcherPrivate::removeFoldersBelow(const QString &path)
{
auto it = _pathToWatch.find(path);
if (it == _pathToWatch.end())
return;
const QString pathSlash = path + QLatin1Char('/');
// Remove the entry and all subentries
while (it != _pathToWatch.end()) {
auto itPath = it.key();
if (!itPath.startsWith(path))
break;
if (itPath != path && !itPath.startsWith(pathSlash)) {
// order is 'foo', 'foo bar', 'foo/bar'
++it;
continue;
}
auto wid = it.value();
inotify_rm_watch(_fd, wid);
_watchToPath.remove(wid);
it = _pathToWatch.erase(it);
qCDebug(lcFolderWatcher) << u"Removed watch for" << itPath;
}
}
} // ns mirall
+63
View File
@@ -0,0 +1,63 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#pragma once
#include <QObject>
#include <QString>
#include <QSocketNotifier>
#include <QHash>
#include <QDir>
#include "folderwatcher.h"
class QTimer;
namespace OCC {
/**
* @brief Linux (inotify) API implementation of FolderWatcher
* @ingroup gui
*/
class QSFERA_GUI_EXPORT FolderWatcherPrivate : public QObject
{
Q_OBJECT
public:
FolderWatcherPrivate() {}
FolderWatcherPrivate(FolderWatcher *p, const QString &path);
int testWatchCount() const { return _pathToWatch.size(); }
/// On linux the watcher is ready when the ctor finished.
constexpr bool isReady() const { return true; }
protected Q_SLOTS:
void slotReceivedNotification(int fd);
void slotAddFolderRecursive(const QString &path);
protected:
bool findFoldersBelow(const QDir &dir, QStringList &fullList);
void inotifyRegisterPath(const QString &path);
void removeFoldersBelow(const QString &path);
private:
FolderWatcher *_parent;
QString _folder;
QHash<int, QString> _watchToPath;
QMap<QString, int> _pathToWatch;
QScopedPointer<QSocketNotifier> _socket;
int _fd;
};
}
+112
View File
@@ -0,0 +1,112 @@
/*
* Copyright (C) by Markus Goetz <markus@woboq.com>
*
* 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.
*/
#include "folderwatcher.h"
#include "folderwatcher_mac.h"
#include <cerrno>
#include <QScopeGuard>
#include <QStringList>
namespace OCC {
FolderWatcherPrivate::FolderWatcherPrivate(FolderWatcher *p, const QString &path)
: _parent(p)
, _folder(path)
{
this->startWatching();
}
FolderWatcherPrivate::~FolderWatcherPrivate()
{
FSEventStreamStop(_stream);
FSEventStreamInvalidate(_stream);
FSEventStreamRelease(_stream);
}
static void callback(
ConstFSEventStreamRef streamRef,
void *clientCallBackInfo,
size_t numEvents,
void *eventPathsVoid,
const FSEventStreamEventFlags eventFlags[],
const FSEventStreamEventId eventIds[])
{
Q_UNUSED(streamRef)
Q_UNUSED(eventFlags)
Q_UNUSED(eventIds)
const FSEventStreamEventFlags c_interestingFlags =
kFSEventStreamEventFlagItemCreated // for new folder/file
| kFSEventStreamEventFlagItemRemoved // for rm
| kFSEventStreamEventFlagItemInodeMetaMod // for mtime change
| kFSEventStreamEventFlagItemRenamed // also coming for moves to trash in finder
| kFSEventStreamEventFlagItemModified; // for content change
// We ignore other flags, e.g. for owner change, xattr change, Finder label change etc
QSet<QString> paths;
CFArrayRef eventPaths = static_cast<CFArrayRef>(eventPathsVoid);
for (CFIndex i = 0; i < static_cast<CFIndex>(numEvents); ++i) {
auto cfPath = reinterpret_cast<CFStringRef>(CFArrayGetValueAtIndex(eventPaths, i));
const auto qPath = QString::fromCFString(cfPath).normalized(QString::NormalizationForm_C);
if (!(eventFlags[i] & c_interestingFlags)) {
qCDebug(lcFolderWatcher) << u"Ignoring non-content changes for" << qPath;
continue;
}
paths.insert(qPath);
}
if (!paths.isEmpty()) {
reinterpret_cast<FolderWatcherPrivate *>(clientCallBackInfo)->doNotifyParent(std::move(paths));
}
}
void FolderWatcherPrivate::startWatching()
{
qCDebug(lcFolderWatcher) << u"FolderWatcherPrivate::startWatching()" << _folder;
CFStringRef cfFolder = _folder.toCFString();
QScopeGuard freeFolder([cfFolder]() { CFRelease(cfFolder); });
CFArrayRef pathsToWatch = CFStringCreateArrayBySeparatingStrings(nullptr, cfFolder, CFSTR(":"));
QScopeGuard freePaths([pathsToWatch]() { CFRelease(pathsToWatch); });
FSEventStreamContext ctx = { 0, this, nullptr, nullptr, nullptr };
// TODO: Add kFSEventStreamCreateFlagFileEvents ?
_stream = FSEventStreamCreate(nullptr,
&callback,
&ctx,
pathsToWatch,
kFSEventStreamEventIdSinceNow,
0, // latency
kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagIgnoreSelf);
FSEventStreamScheduleWithRunLoop(_stream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
FSEventStreamStart(_stream);
}
void FolderWatcherPrivate::doNotifyParent(QSet<QString> &&paths)
{
_parent->addChanges(std::move(paths));
}
} // ns mirall
+54
View File
@@ -0,0 +1,54 @@
/*
* Copyright (C) by Markus Goetz <markus@woboq.com>
*
* 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.
*/
#ifndef MIRALL_FOLDERWATCHER_MAC_H
#define MIRALL_FOLDERWATCHER_MAC_H
#include <QObject>
#include <QString>
#include <CoreServices/CoreServices.h>
namespace OCC {
class FolderWatcher;
/**
* @brief Mac OS X API implementation of FolderWatcher
* @ingroup gui
*/
class FolderWatcherPrivate
{
public:
FolderWatcherPrivate(FolderWatcher *p, const QString &path);
~FolderWatcherPrivate();
void startWatching();
void doNotifyParent(QSet<QString> &&);
/// On OSX the watcher is ready when the ctor finished.
constexpr bool isReady() const { return true; }
private:
FolderWatcher *_parent;
QString _folder;
FSEventStreamRef _stream;
};
} // namespace OCC
#endif
+230
View File
@@ -0,0 +1,230 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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.
*/
#include <QDir>
#include <QScopeGuard>
#include <QThread>
#include "common/asserts.h"
#include "common/utility.h"
#include "common/utility_win.h"
#include "filesystem.h"
#include "folderwatcher.h"
#include "folderwatcher_win.h"
#include <stdlib.h>
#include <stdio.h>
#include <tchar.h>
namespace OCC {
WatcherThread::WatchChanges WatcherThread::watchChanges(size_t fileNotifyBufferSize)
{
_directory = Utility::Handle::createHandle(FileSystem::toFilesystemPath(_longPath), {.accessMode = FILE_LIST_DIRECTORY, .async = true});
if (!_directory) {
qCWarning(lcFolderWatcher) << u"Failed to create handle for" << _path << u", error:" << _directory.errorMessage();
return WatchChanges::Error;
}
QScopeGuard todoBeforeReturn([this]() {
CancelIo(_directory);
closeHandle();
});
OVERLAPPED overlapped = {};
overlapped.hEvent = _resultEvent;
// QVarLengthArray ensures the stack-buffer is aligned like double and qint64.
QVarLengthArray<char, 4096 * 10> fileNotifyBuffer;
fileNotifyBuffer.resize(fileNotifyBufferSize);
while (true) {
ResetEvent(_resultEvent);
FILE_NOTIFY_INFORMATION *pFileNotifyBuffer =
reinterpret_cast<FILE_NOTIFY_INFORMATION *>(fileNotifyBuffer.data());
DWORD dwBytesReturned = 0;
if (!ReadDirectoryChangesW(_directory, pFileNotifyBuffer,
static_cast<DWORD>(fileNotifyBufferSize), true,
FILE_NOTIFY_CHANGE_FILE_NAME
| FILE_NOTIFY_CHANGE_DIR_NAME
| FILE_NOTIFY_CHANGE_LAST_WRITE
| FILE_NOTIFY_CHANGE_ATTRIBUTES, // attributes are for vfs pin state changes
&dwBytesReturned, &overlapped, nullptr)) {
const DWORD errorCode = GetLastError();
if (errorCode == ERROR_NOTIFY_ENUM_DIR) {
qCDebug(lcFolderWatcher) << u"The buffer for changes overflowed! Triggering a generic change and resizing";
Q_EMIT changed({_path});
return WatchChanges::NeedBiggerBuffer;
} else {
qCWarning(lcFolderWatcher) << u"ReadDirectoryChangesW error" << Utility::formatWinError(errorCode);
return WatchChanges::Error;
}
}
_parent->_ready = true;
HANDLE handles[] = { _resultEvent, _stopEvent };
DWORD result = WaitForMultipleObjects(
2, handles,
false, // awake once one of them arrives
INFINITE);
const auto error = GetLastError();
if (result == 1) {
qCDebug(lcFolderWatcher) << u"Received stop event, aborting folder watcher thread";
return WatchChanges::Done;
}
if (result != 0) {
qCWarning(lcFolderWatcher) << u"WaitForMultipleObjects failed" << result << Utility::formatWinError(error);
return WatchChanges::Error;
}
bool ok = GetOverlappedResult(_directory, &overlapped, &dwBytesReturned, false);
if (!ok) {
const DWORD errorCode = GetLastError();
if (errorCode == ERROR_NOTIFY_ENUM_DIR) {
qCDebug(lcFolderWatcher) << u"The buffer for changes overflowed! Triggering a generic change and resizing";
Q_EMIT lostChanges();
Q_EMIT changed({_path});
return WatchChanges::NeedBiggerBuffer;
} else {
qCWarning(lcFolderWatcher) << u"GetOverlappedResult error" << Utility::formatWinError(errorCode);
return WatchChanges::Error;
}
}
processEntries(pFileNotifyBuffer);
}
}
void WatcherThread::processEntries(FILE_NOTIFY_INFORMATION *curEntry)
{
if (!curEntry) {
return;
}
QSet<QString> paths;
const size_t fileNameBufferSize = 4096;
wchar_t fileNameBuffer[fileNameBufferSize];
do {
const auto action = static_cast<FolderWatcherPrivate::ChangeAction>(curEntry->Action);
QString longfile = _longPath + QString::fromWCharArray(curEntry->FileName, curEntry->FileNameLength / sizeof(wchar_t));
// Unless the file was removed or renamed, get its full long name
// TODO: We could still try expanding the path in the tricky cases...
if (action != FolderWatcherPrivate::ChangeAction::ACTION_REMOVED && action != FolderWatcherPrivate::ChangeAction::ACTION_RENAMED_OLD_NAME) {
const int longNameSize = GetLongPathNameW(reinterpret_cast<const wchar_t*>(longfile.utf16()), fileNameBuffer, fileNameBufferSize);
const auto error = GetLastError();
if (longNameSize > 0) {
longfile = QString::fromWCharArray(fileNameBuffer, longNameSize);
} else {
if (error == ERROR_FILE_NOT_FOUND) {
qCInfo(lcFolderWatcher) << u"Ignoring change in" << longfile << u"the file no longer exists, probably a temporary file." << action;
continue;
} else {
qCWarning(lcFolderWatcher) << u"Error converting file name" << longfile << u"to full length, keeping original name." << action
<< Utility::formatWinError(error);
}
}
}
longfile = QDir::cleanPath(longfile);
// Skip modifications of folders: One of these is triggered for changes
// and new files in a folder, probably because of the folder's mtime
// changing. We don't need them.
if (action == FolderWatcherPrivate::ChangeAction::ACTION_MODIFIED && QFileInfo(longfile).isDir()) {
continue;
}
paths.insert(longfile);
} while (curEntry->NextEntryOffset != 0
// FILE_NOTIFY_INFORMATION has no fixed size and the offset is in bytes therefor we first need to cast to char
&& (curEntry = reinterpret_cast<FILE_NOTIFY_INFORMATION *>(reinterpret_cast<char *>(curEntry) + curEntry->NextEntryOffset)));
if (!paths.isEmpty()) {
Q_EMIT changed(paths);
}
}
void WatcherThread::closeHandle()
{
if (_directory) {
_directory.close();
}
}
void WatcherThread::run()
{
_resultEvent = CreateEvent(nullptr, true, false, nullptr);
_stopEvent = CreateEvent(nullptr, true, false, nullptr);
// If this buffer fills up before we've extracted its data we will lose
// change information. Therefore start big.
size_t bufferSize = 4096 * 10;
const size_t maxBuffer = 64 * 1024;
while (true) {
switch (watchChanges(bufferSize)) {
case WatchChanges::NeedBiggerBuffer:
bufferSize = qMin(bufferSize * 2, maxBuffer);
continue;
case WatchChanges::Done:
return;
case WatchChanges::Error:
// Other errors shouldn't actually happen,
// so sleep a bit to avoid running into the same error case in a
// tight loop.
sleep(2);
default:
Q_UNREACHABLE();
}
}
}
WatcherThread::WatcherThread(FolderWatcherPrivate *parent, const QString &path)
: QThread()
, _parent(parent)
, _path(path + (path.endsWith(QLatin1Char('/')) ? QString() : QStringLiteral("/")))
, _longPath(FileSystem::longWinPath(_path))
, _directory(nullptr)
, _resultEvent(nullptr)
, _stopEvent(nullptr)
{
}
WatcherThread::~WatcherThread()
{
closeHandle();
}
void WatcherThread::stop()
{
SetEvent(_stopEvent);
}
FolderWatcherPrivate::FolderWatcherPrivate(FolderWatcher *p, const QString &path)
: _parent(p)
{
_thread.reset(new WatcherThread(this, path));
// we are using connects instead of directly emitting on p as we need to cross thread borders, the signal must receive a copy
connect(_thread.get(), &WatcherThread::changed, _parent, [this](auto paths) { _parent->addChanges(std::move(paths)); }, Qt::QueuedConnection);
connect(_thread.get(), &WatcherThread::lostChanges, _parent, &FolderWatcher::lostChanges, Qt::QueuedConnection);
_thread->start();
}
FolderWatcherPrivate::~FolderWatcherPrivate()
{
_thread->stop();
_thread->wait();
}
} // namespace OCC
+98
View File
@@ -0,0 +1,98 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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.
*/
#ifndef MIRALL_FOLDERWATCHER_WIN_H
#define MIRALL_FOLDERWATCHER_WIN_H
#include "libsync/common/utility_win.h"
#include <QThread>
namespace OCC {
class FolderWatcher;
class FolderWatcherPrivate;
/**
* @brief The WatcherThread class
* @ingroup gui
*/
class WatcherThread : public QThread
{
Q_OBJECT
public:
WatcherThread(FolderWatcherPrivate *parent, const QString &path);
~WatcherThread() override;
void stop();
protected:
enum class WatchChanges {
Done,
NeedBiggerBuffer,
Error,
};
void run() override;
WatchChanges watchChanges(size_t fileNotifyBufferSize);
void processEntries(FILE_NOTIFY_INFORMATION *curEntry);
void closeHandle();
Q_SIGNALS:
void changed(QSet<QString> path);
void lostChanges();
private:
FolderWatcherPrivate *_parent;
const QString _path;
const QString _longPath;
Utility::Handle _directory;
HANDLE _resultEvent;
HANDLE _stopEvent;
};
/**
* @brief Windows implementation of FolderWatcher
* @ingroup gui
*/
class FolderWatcherPrivate : public QObject
{
Q_OBJECT
public:
enum class ChangeAction : uint8_t {
ACTION_ADDED = FILE_ACTION_ADDED,
ACTION_REMOVED = FILE_ACTION_REMOVED,
ACTION_MODIFIED = FILE_ACTION_MODIFIED,
ACTION_RENAMED_OLD_NAME = FILE_ACTION_RENAMED_OLD_NAME,
ACTION_RENAMED_NEW_NAME = FILE_ACTION_RENAMED_NEW_NAME
};
Q_ENUM(ChangeAction)
FolderWatcherPrivate(FolderWatcher *p, const QString &path);
~FolderWatcherPrivate() override;
/// Set to non-zero once the WatcherThread is capturing events.
bool isReady() const
{
return _ready;
}
private:
FolderWatcher *_parent;
QScopedPointer<WatcherThread> _thread;
bool _ready = false;
friend class WatcherThread;
};
}
#endif // MIRALL_FOLDERWATCHER_WIN_H
@@ -0,0 +1,12 @@
target_sources(QSferaGui PRIVATE
folderwizard.cpp
folderwizardlocalpath.cpp
folderwizardsourcepage.ui
folderwizardtargetpage.ui
folderwizardselectivesync.cpp
spacespage.cpp
spacespage.ui
)
@@ -0,0 +1,162 @@
/*
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
*
* 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.
*/
#include "folderwizard.h"
#include "folderwizard_p.h"
#include "folderwizardlocalpath.h"
#include "folderwizardselectivesync.h"
#include "spacespage.h"
#include "account.h"
#include "gui/application.h"
#include "gui/settingsdialog.h"
#include "theme.h"
#include "gui/accountstate.h"
#include "gui/folderman.h"
#include "libsync/graphapi/space.h"
#include <QDesktopServices>
#include <QMessageBox>
#include <QUrl>
#include <stdlib.h>
namespace OCC {
Q_LOGGING_CATEGORY(lcFolderWizard, "gui.folderwizard", QtInfoMsg)
QString FolderWizardPrivate::formatWarnings(const QStringList &warnings, bool isError)
{
QString ret;
if (warnings.count() == 1) {
ret = isError ? QCoreApplication::translate("FolderWizard", "<b>Error:</b> %1").arg(warnings.first()) : QCoreApplication::translate("FolderWizard", "<b>Warning:</b> %1").arg(warnings.first());
} else if (warnings.count() > 1) {
QStringList w2;
for (const auto &warning : warnings) {
w2.append(QStringLiteral("<li>%1</li>").arg(warning));
}
ret = isError ? QCoreApplication::translate("FolderWizard", "<b>Error:</b><ul>%1</ul>").arg(w2.join(QString()))
: QCoreApplication::translate("FolderWizard", "<b>Warning:</b><ul>%1</ul>").arg(w2.join(QString()));
}
return ret;
}
QString FolderWizardPrivate::defaultSyncRoot() const
{
if (!_account->account()->hasDefaultSyncRoot()) {
return FolderMan::suggestSyncFolder(FolderMan::NewFolderType::SpacesSyncRoot, _account->account()->uuid());
} else {
return _account->account()->defaultSyncRoot();
}
}
FolderWizardPrivate::FolderWizardPrivate(FolderWizard *q, const AccountStatePtr &account)
: q_ptr(q)
, _account(account)
, _spacesPage(new SpacesPage(account->account(), q))
{
if (!_account->account()->hasDefaultSyncRoot()) {
_folderWizardSourcePage = new FolderWizardLocalPath(this);
q->setPage(FolderWizard::Page_Source, _folderWizardSourcePage);
}
q->setPage(FolderWizard::Page_Space, _spacesPage);
if (VfsPluginManager::instance().bestAvailableVfsMode() == Vfs::Mode::Off) {
_folderWizardSelectiveSyncPage = new FolderWizardSelectiveSync(this);
q->setPage(FolderWizard::Page_SelectiveSync, _folderWizardSelectiveSyncPage);
}
}
QString FolderWizardPrivate::localPath() const
{
return FolderMan::findGoodPathForNewSyncFolder(
defaultSyncRoot(), _spacesPage->currentSpace()->displayName(), FolderMan::NewFolderType::SpacesFolder, _account->account()->uuid());
}
uint32_t FolderWizardPrivate::priority() const
{
return _spacesPage->currentSpace()->priority();
}
QUrl FolderWizardPrivate::davUrl() const
{
auto url = _spacesPage->currentSpace()->webdavUrl();
if (!url.path().endsWith(QLatin1Char('/'))) {
url.setPath(url.path() + QLatin1Char('/'));
}
return url;
}
QString FolderWizardPrivate::spaceId() const
{
return _spacesPage->currentSpace()->id();
}
QString FolderWizardPrivate::displayName() const
{
return _spacesPage->currentSpace()->displayName();
}
const AccountStatePtr &FolderWizardPrivate::accountState()
{
return _account;
}
bool FolderWizardPrivate::useVirtualFiles() const
{
return VfsPluginManager::instance().bestAvailableVfsMode() != Vfs::Mode::Off;
}
FolderWizard::FolderWizard(const AccountStatePtr &account, QWidget *parent)
: QWizard(parent)
, d_ptr(new FolderWizardPrivate(this, account))
{
setWindowTitle(tr("Add Space"));
setOptions(QWizard::CancelButtonOnLeft);
setButtonText(QWizard::FinishButton, tr("Add Space"));
setWizardStyle(QWizard::ModernStyle);
}
FolderWizard::~FolderWizard()
{
}
FolderMan::SyncConnectionDescription FolderWizard::result()
{
Q_D(FolderWizard);
if (d->_folderWizardSourcePage) {
d->_account->account()->setDefaultSyncRoot(d->_folderWizardSourcePage->localPath());
}
const QString localPath = d->localPath();
// the local path must be a child of defaultSyncRoot
Q_ASSERT(FileSystem::isChildPathOf(localPath, d->defaultSyncRoot()));
return {
d->davUrl(), //
d->spaceId(), //
localPath, //
d->displayName(), //
d->useVirtualFiles(), //
d->priority(), //
d->_folderWizardSelectiveSyncPage ? d->_folderWizardSelectiveSyncPage->selectiveSyncBlackList() : QSet<QString>{} //
};
}
} // end namespace
@@ -0,0 +1,54 @@
/*
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
*
* 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.
*/
#pragma once
#include <QUrl>
#include <QWizard>
#include "accountfwd.h"
#include "gui/folderman.h"
class QCheckBox;
class QTreeWidgetItem;
class Ui_FolderWizardTargetPage;
namespace OCC {
class FolderWizardPrivate;
/**
* @brief The FolderWizard class
* @ingroup gui
*/
class FolderWizard : public QWizard
{
Q_OBJECT
public:
enum PageType { Page_Source, Page_Space, Page_SelectiveSync };
Q_ENUM(PageType)
explicit FolderWizard(const AccountStatePtr &account, QWidget *parent = nullptr);
~FolderWizard() override;
FolderMan::SyncConnectionDescription result();
Q_DECLARE_PRIVATE(FolderWizard)
private:
QScopedPointer<FolderWizardPrivate> d_ptr;
};
} // namespace OCC
@@ -0,0 +1,84 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "folderwizard.h"
#include "libsync/accountfwd.h"
#include <QCoreApplication>
#include <QLoggingCategory>
#include <QStringList>
namespace OCC {
Q_DECLARE_LOGGING_CATEGORY(lcFolderWizard);
class FolderWizardPrivate
{
public:
FolderWizardPrivate(FolderWizard *q, const AccountStatePtr &account);
static QString formatWarnings(const QStringList &warnings, bool isError = false);
QString localPath() const;
QString remotePath() const;
uint32_t priority() const;
QString defaultSyncRoot() const;
QUrl davUrl() const;
QString spaceId() const;
bool useVirtualFiles() const;
QString displayName() const;
const AccountStatePtr &accountState();
private:
Q_DECLARE_PUBLIC(FolderWizard)
FolderWizard *q_ptr;
AccountStatePtr _account;
class SpacesPage *_spacesPage;
class FolderWizardLocalPath *_folderWizardSourcePage = nullptr;
class FolderWizardRemotePath *_folderWizardTargetPage = nullptr;
class FolderWizardSelectiveSync *_folderWizardSelectiveSyncPage = nullptr;
};
class FolderWizardPage : public QWizardPage
{
Q_OBJECT
public:
FolderWizardPage(FolderWizardPrivate *parent)
: QWizardPage(nullptr)
, _parent(parent)
{
}
protected:
inline FolderWizardPrivate *folderWizardPrivate() const
{
return _parent;
}
private:
FolderWizardPrivate *_parent;
};
}
@@ -0,0 +1,109 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "folderwizardlocalpath.h"
#include "ui_folderwizardsourcepage.h"
#include "folderwizard.h"
#include "folderwizard_p.h"
#include "gui/folderman.h"
#include "libsync/account.h"
#include <QDir>
#include <QFileDialog>
#include <QStandardPaths>
using namespace OCC;
FolderWizardLocalPath::FolderWizardLocalPath(FolderWizardPrivate *parent)
: FolderWizardPage(parent)
, _ui(new Ui_FolderWizardSourcePage)
{
_ui->setupUi(this);
connect(_ui->localFolderChooseBtn, &QAbstractButton::clicked, this, &FolderWizardLocalPath::slotChooseLocalFolder);
connect(_ui->localFolderLineEdit, &QLineEdit::textChanged, this, &FolderWizardLocalPath::completeChanged);
_ui->warnLabel->setTextFormat(Qt::RichText);
_ui->warnLabel->hide();
}
FolderWizardLocalPath::~FolderWizardLocalPath()
{
delete _ui;
}
void FolderWizardLocalPath::initializePage()
{
_ui->warnLabel->hide();
_ui->localFolderLineEdit->setText(QDir::toNativeSeparators(folderWizardPrivate()->defaultSyncRoot()));
}
QString FolderWizardLocalPath::localPath() const
{
return QDir::fromNativeSeparators(_ui->localFolderLineEdit->text());
}
bool FolderWizardLocalPath::isComplete() const
{
auto accountUuid = folderWizardPrivate()->accountState()->account()->uuid();
QString errorStr = FolderMan::instance()->checkPathValidityForNewFolder(localPath(), FolderMan::NewFolderType::SpacesSyncRoot, accountUuid);
if (errorStr.isEmpty()) {
if (auto result = VfsPluginManager::instance().prepare(localPath(), accountUuid, VfsPluginManager::instance().bestAvailableVfsMode()); !result) {
errorStr = result.error();
}
}
bool isOk = errorStr.isEmpty();
QStringList warnStrings;
if (!isOk) {
warnStrings << errorStr;
}
_ui->warnLabel->setWordWrap(true);
if (isOk) {
_ui->warnLabel->hide();
_ui->warnLabel->clear();
} else {
_ui->warnLabel->show();
QString warnings = FolderWizardPrivate::formatWarnings(warnStrings);
_ui->warnLabel->setText(warnings);
}
return isOk;
}
void FolderWizardLocalPath::slotChooseLocalFolder()
{
QString sf = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
QDir d(sf);
// open the first entry of the home dir. Otherwise the dir picker comes
// up with the closed home dir icon, stupid Qt default...
QStringList dirs = d.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks,
QDir::DirsFirst | QDir::Name);
if (dirs.count() > 0)
sf += QLatin1Char('/') + dirs.at(0); // Take the first dir in home dir.
QString dir = QFileDialog::getExistingDirectory(this, tr("Select the Spaces root folder"), sf);
if (!dir.isEmpty()) {
// set the last directory component name as alias
_ui->localFolderLineEdit->setText(QDir::toNativeSeparators(dir));
}
Q_EMIT completeChanged();
}
@@ -0,0 +1,51 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "gui/folder.h"
#include "gui/folderwizard/folderwizard_p.h"
class Ui_FolderWizardSourcePage;
namespace OCC {
/**
* @brief Page to ask for the local source folder
* @ingroup gui
*/
class FolderWizardLocalPath : public FolderWizardPage
{
Q_OBJECT
public:
explicit FolderWizardLocalPath(FolderWizardPrivate *parent);
~FolderWizardLocalPath() override;
bool isComplete() const override;
void initializePage() override;
QString localPath() const;
protected Q_SLOTS:
void slotChooseLocalFolder();
private:
Ui_FolderWizardSourcePage *_ui;
QMap<QString, Folder *> _folderMap;
};
}
@@ -0,0 +1,68 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "folderwizardselectivesync.h"
#include "folderwizard.h"
#include "folderwizard_p.h"
#include "gui/application.h"
#include "gui/selectivesyncwidget.h"
#include "libsync/theme.h"
#include "gui/settingsdialog.h"
#include "libsync/vfs/vfs.h"
#include <QCheckBox>
#include <QVBoxLayout>
using namespace OCC;
FolderWizardSelectiveSync::FolderWizardSelectiveSync(FolderWizardPrivate *parent)
: FolderWizardPage(parent)
{
QVBoxLayout *layout = new QVBoxLayout(this);
_selectiveSync = new SelectiveSyncWidget(folderWizardPrivate()->accountState()->account(), this);
layout->addWidget(_selectiveSync);
}
FolderWizardSelectiveSync::~FolderWizardSelectiveSync()
{
}
void FolderWizardSelectiveSync::initializePage()
{
const auto *wizardPrivate = dynamic_cast<FolderWizard *>(wizard())->d_func();
_selectiveSync->setDavUrl(wizardPrivate->davUrl());
_selectiveSync->setFolderInfo(wizardPrivate->displayName());
QWizardPage::initializePage();
}
bool FolderWizardSelectiveSync::validatePage()
{
_selectiveSyncBlackList = _selectiveSync->createBlackList();
return true;
}
const QSet<QString> &FolderWizardSelectiveSync::selectiveSyncBlackList() const
{
return _selectiveSyncBlackList;
}
@@ -0,0 +1,51 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include "gui/folderwizard/folderwizard_p.h"
#include "libsync/accountfwd.h"
namespace OCC {
class SelectiveSyncWidget;
/**
* @brief The FolderWizardSelectiveSync class
* @ingroup gui
*/
class FolderWizardSelectiveSync : public FolderWizardPage
{
Q_OBJECT
public:
explicit FolderWizardSelectiveSync(FolderWizardPrivate *parent);
~FolderWizardSelectiveSync() override;
bool validatePage() override;
void initializePage() override;
const QSet<QString> &selectiveSyncBlackList() const;
private:
SelectiveSyncWidget *_selectiveSync;
QSet<QString> _selectiveSyncBlackList;
};
}
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FolderWizardSourcePage</class>
<widget class="QWidget" name="FolderWizardSourcePage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>423</width>
<height>174</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Select a local folder to synchronize your Spaces to:</string>
</property>
<property name="buddy">
<cstring>localFolderLineEdit</cstring>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="localFolderLineEdit">
<property name="toolTip">
<string>Enter the path to the Spaces root folder. This folder will contain all your synchronized Spaces.</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="localFolderChooseBtn">
<property name="toolTip">
<string>Click to select the Spaces root folder.</string>
</property>
<property name="text">
<string>&amp;Choose...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="warnLabel">
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::TextFormat::RichText</enum>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>349</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
@@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FolderWizardTargetPage</class>
<widget class="QWidget" name="FolderWizardTargetPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>520</width>
<height>350</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Select a remote destination folder</string>
</property>
<property name="flat">
<bool>true</bool>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTreeWidget" name="folderTreeWidget">
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
<column>
<property name="text">
<string>Folders</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="addFolderButton">
<property name="text">
<string>Create Folder</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="refreshButton">
<property name="text">
<string>Refresh</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLineEdit" name="folderEntry"/>
</item>
<item>
<widget class="QFrame" name="warningFrame">
<property name="palette">
<palette>
<active/>
<inactive/>
<disabled/>
</palette>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="warningIcon">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="textFormat">
<enum>Qt::AutoText</enum>
</property>
<property name="pixmap">
<pixmap resource="../../resources/client.qrc">:/client/resources/light/warning.svg</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
<property name="margin">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="warningLabel">
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../resources/client.qrc"/>
</resources>
<connections/>
</ui>
@@ -0,0 +1,43 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#include "spacespage.h"
#include "ui_spacespage.h"
using namespace OCC;
SpacesPage::SpacesPage(AccountPtr acc, QWidget *parent)
: QWizardPage(parent)
, ui(new Ui::SpacesPage)
{
ui->setupUi(this);
ui->widget->setAccount(acc);
connect(ui->widget, &Spaces::SpacesBrowser::currentSpaceChanged, this, &QWizardPage::completeChanged);
}
SpacesPage::~SpacesPage()
{
delete ui;
}
bool OCC::SpacesPage::isComplete() const
{
return ui->widget->currentSpace();
}
GraphApi::Space *OCC::SpacesPage::currentSpace() const
{
return ui->widget->currentSpace();
}
+45
View File
@@ -0,0 +1,45 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#pragma once
#include "gui/spaces/spacesmodel.h"
#include "accountfwd.h"
#include <QWizardPage>
namespace Ui {
class SpacesPage;
}
namespace OCC {
class SpacesPage : public QWizardPage
{
Q_OBJECT
public:
explicit SpacesPage(AccountPtr acc, QWidget *parent = nullptr);
~SpacesPage();
bool isComplete() const override;
GraphApi::Space *currentSpace() const;
private:
::Ui::SpacesPage *ui;
};
}
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SpacesPage</class>
<widget class="QWizardPage" name="SpacesPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>362</width>
<height>300</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Choose a Space to sync</string>
</property>
</widget>
</item>
<item>
<widget class="OCC::Spaces::SpacesBrowser" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::FocusPolicy::StrongFocus</enum>
</property>
<property name="accessibleName">
<string>Spaces list</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>OCC::Spaces::SpacesBrowser</class>
<extends>QWidget</extends>
<header>gui/spaces/spacesbrowser.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
+14
View File
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#include "fonticonmessagebox.h"
#include <QStyle>
OCC::FontIconMessageBox::FontIconMessageBox(
Resources::FontIcon icon, const QString &title, const QString &text, StandardButtons buttons, QWidget *parent, Qt::WindowFlags flags)
: QMessageBox(NoIcon, title, text, buttons, parent, flags)
{
const int iconSize = style()->pixelMetric(QStyle::PM_MessageBoxIconSize, nullptr, this);
setIconPixmap(icon.pixmap({iconSize, iconSize}, devicePixelRatio()));
}
+19
View File
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#pragma once
#include "gui/qsferaguilib.h"
#include "resources/fonticon.h"
#include <QMessageBox>
namespace OCC {
class QSFERA_GUI_EXPORT FontIconMessageBox : public QMessageBox
{
public:
FontIconMessageBox(Resources::FontIcon icon, const QString &title, const QString &text, StandardButtons buttons = NoButton, QWidget *parent = nullptr,
Qt::WindowFlags flags = Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint);
};
}
+186
View File
@@ -0,0 +1,186 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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.
*/
#include "generalsettings.h"
#include "ui_generalsettings.h"
#include "common/restartmanager.h"
#include "gui/application.h"
#include "gui/folderman.h"
#include "gui/ignorelisteditor.h"
#include "gui/logbrowser.h"
#include "gui/settingsdialog.h"
#include "gui/translations.h"
#include "libsync/configfile.h"
#include "libsync/theme.h"
#include <QMessageBox>
#include <QOperatingSystemVersion>
#include <QScopedValueRollback>
Q_LOGGING_CATEGORY(lcGeneralSettings, "gui.generalsettings", QtInfoMsg)
namespace OCC {
GeneralSettings::GeneralSettings(QWidget *parent)
: QWidget(parent)
, _ui(new Ui::GeneralSettings)
, _currentlyLoading(false)
{
_ui->setupUi(this);
reloadConfig();
loadMiscSettings();
// misc
connect(_ui->crashreporterCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings);
connect(_ui->languageDropdown, QOverload<int>::of(&QComboBox::activated), this, [this]() {
// first, store selected language in config file
saveMiscSettings();
// warn user that a language change requires a restart to take effect
QMessageBox::warning(this, tr("Warning"), tr("Language changes require a restart of this application to take effect."), QMessageBox::Ok);
});
/* handle the hidden file checkbox */
/* the ignoreHiddenFiles flag is a folder specific setting, but for now, it is
* handled globally. Save it to every folder that is defined.
*/
connect(_ui->syncHiddenFilesCheckBox, &QCheckBox::toggled, this, [](bool checked) { FolderMan::instance()->setIgnoreHiddenFiles(!checked); });
_ui->crashreporterCheckBox->setVisible(Theme::instance()->withCrashReporter());
_ui->moveToTrashCheckBox->setVisible(Theme::instance()->enableMoveToTrash());
connect(_ui->moveToTrashCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
ConfigFile().setMoveToTrash(checked);
Q_EMIT syncOptionsChanged();
});
connect(_ui->ignoredFilesButton, &QAbstractButton::clicked, this, &GeneralSettings::slotIgnoreFilesEditor);
connect(_ui->logSettingsButton, &QPushButton::clicked, this, [] {
// only access occApp after things are set up
auto logBrowser = new LogBrowser(ocApp()->settingsDialog());
logBrowser->setAttribute(Qt::WA_DeleteOnClose);
ocApp()->showSettings();
logBrowser->open();
});
connect(_ui->about_pushButton, &QPushButton::clicked, ocApp(), &Application::showAbout);
}
GeneralSettings::~GeneralSettings()
{
delete _ui;
}
void GeneralSettings::loadMiscSettings()
{
QScopedValueRollback<bool> scope(_currentlyLoading, true);
ConfigFile cfgFile;
_ui->crashreporterCheckBox->setChecked(cfgFile.crashReporter());
// the dropdown has to be populated before we can can pick an entry below based on the stored setting
loadLanguageNamesIntoDropdown();
const auto &locale = cfgFile.uiLanguage();
const auto index = _ui->languageDropdown->findData(locale);
_ui->languageDropdown->setCurrentIndex(index < 0 ? 0 : index);
}
void GeneralSettings::showEvent(QShowEvent *)
{
reloadConfig();
}
void GeneralSettings::saveMiscSettings()
{
if (_currentlyLoading)
return;
ConfigFile cfgFile;
cfgFile.setCrashReporter(_ui->crashreporterCheckBox->isChecked());
// the first entry, identified by index 0, means "use default", which is a special case handled below
const QString pickedLocale = _ui->languageDropdown->currentData().toString();
cfgFile.setUiLanguage(pickedLocale);
}
void GeneralSettings::slotToggleLaunchOnStartup(bool enable)
{
Theme *theme = Theme::instance();
Utility::setLaunchOnStartup(theme->appName(), theme->appNameGUI(), enable);
}
void GeneralSettings::slotIgnoreFilesEditor()
{
if (_ignoreEditor.isNull()) {
_ignoreEditor = new IgnoreListEditor(ocApp()->settingsDialog());
_ignoreEditor->setAttribute(Qt::WA_DeleteOnClose, true);
ocApp()->showSettings();
_ignoreEditor->open();
}
}
void GeneralSettings::reloadConfig()
{
_ui->syncHiddenFilesCheckBox->setChecked(!FolderMan::instance()->ignoreHiddenFiles());
_ui->moveToTrashCheckBox->setChecked(ConfigFile().moveToTrash());
if (Utility::isWindows() && Utility::isInstalledByStore()) {
_ui->autostartCheckBox->setVisible(false);
} else {
if (Utility::hasSystemLaunchOnStartup(Theme::instance()->appName())) {
_ui->autostartCheckBox->setChecked(true);
_ui->autostartCheckBox->setDisabled(true);
_ui->autostartCheckBox->setToolTip(tr("You cannot disable autostart because system-wide autostart is enabled."));
} else {
const bool hasAutoStart = Utility::hasLaunchOnStartup(Theme::instance()->appName());
// make sure the binary location is correctly set
slotToggleLaunchOnStartup(hasAutoStart);
_ui->autostartCheckBox->setChecked(hasAutoStart);
connect(_ui->autostartCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::slotToggleLaunchOnStartup);
}
}
}
void GeneralSettings::loadLanguageNamesIntoDropdown()
{
// allow method to be called more than once
_ui->languageDropdown->clear();
// if no option has been chosen explicitly by the user, the first entry shall be used
_ui->languageDropdown->addItem(tr("(use default)"));
// initialize map of locales to language names
const auto availableLocales = []() {
auto rv = Translations::listAvailableTranslations().values();
rv.sort(Qt::CaseInsensitive);
return rv;
}();
for (const auto &availableLocale : availableLocales) {
auto nativeLanguageName = QLocale(availableLocale).nativeLanguageName();
// fallback if there's a locale whose name Qt doesn't know
// this indicates a broken filename
if (nativeLanguageName.isEmpty()) {
qCDebug(lcGeneralSettings) << u"Warning: could not find native language name for locale" << availableLocale;
nativeLanguageName = tr("unknown (%1)").arg(availableLocale);
}
QString entryText = QStringLiteral("%1 (%2)").arg(nativeLanguageName, availableLocale);
_ui->languageDropdown->addItem(entryText, availableLocale);
}
}
} // namespace OCC
+65
View File
@@ -0,0 +1,65 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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.
*/
#ifndef MIRALL_GENERALSETTINGS_H
#define MIRALL_GENERALSETTINGS_H
#include <QMap>
#include <QWidget>
#include <QPointer>
namespace OCC {
class IgnoreListEditor;
class SyncLogDialog;
namespace Ui {
class GeneralSettings;
}
/**
* @brief The GeneralSettings class
* @ingroup gui
*/
class GeneralSettings : public QWidget
{
Q_OBJECT
public:
explicit GeneralSettings(QWidget *parent = nullptr);
~GeneralSettings() override;
Q_SIGNALS:
void syncOptionsChanged();
private Q_SLOTS:
void saveMiscSettings();
void slotToggleLaunchOnStartup(bool);
void slotIgnoreFilesEditor();
void loadMiscSettings();
protected:
void showEvent(QShowEvent *event) override;
private:
void reloadConfig();
void loadLanguageNamesIntoDropdown();
Ui::GeneralSettings *_ui;
QPointer<IgnoreListEditor> _ignoreEditor;
bool _currentlyLoading;
};
} // namespace OCC
#endif // MIRALL_GENERALSETTINGS_H
+253
View File
@@ -0,0 +1,253 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::GeneralSettings</class>
<widget class="QWidget" name="OCC::GeneralSettings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>791</width>
<height>686</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_2">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>765</width>
<height>618</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QGroupBox" name="generalGroupBox">
<property name="title">
<string>General Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="languageLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="locale">
<locale language="English" country="UnitedStates"/>
</property>
<property name="text">
<string>Language</string>
</property>
<property name="buddy">
<cstring>languageDropdown</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="languageDropdown">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="accessibleName">
<string>Language selector</string>
</property>
<property name="currentText">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="autostartCheckBox">
<property name="text">
<string>Start on Login</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="advancedGroupBox">
<property name="title">
<string>Advanced</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QCheckBox" name="syncHiddenFilesCheckBox">
<property name="text">
<string>Sync hidden files</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="crashreporterCheckBox">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Show crash reporter</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="moveToTrashCheckBox">
<property name="text">
<string>Move remotely deleted files to the local trash bin instead of deleting them</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="ignoredFilesButton">
<property name="text">
<string>Edit Ignored Files</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="logSettingsButton">
<property name="text">
<string>Log Settings</string>
</property>
</widget>
</item>
<item>
<spacer name="spacer_3">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>555</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Network</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="NetworkSettings" name="widget" native="true">
<property name="focusPolicy">
<enum>Qt::FocusPolicy::StrongFocus</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QWidget" name="versionInfoWidget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="spacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="about_pushButton">
<property name="text">
<string>About</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>NetworkSettings</class>
<extends>QWidget</extends>
<header>networksettings.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>autostartCheckBox</tabstop>
<tabstop>languageDropdown</tabstop>
<tabstop>syncHiddenFilesCheckBox</tabstop>
<tabstop>crashreporterCheckBox</tabstop>
<tabstop>moveToTrashCheckBox</tabstop>
<tabstop>ignoredFilesButton</tabstop>
<tabstop>logSettingsButton</tabstop>
<tabstop>widget</tabstop>
<tabstop>scrollArea</tabstop>
<tabstop>about_pushButton</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>
+148
View File
@@ -0,0 +1,148 @@
/*
* Copyright (C) by Christian Kamm <mail@ckamm.de>
*
* 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.
*/
#include "guiutility.h"
#include "gui/application.h"
#include "gui/settingsdialog.h"
#include "libsync/filesystem.h"
#include "libsync/theme.h"
#include <QApplication>
#include <QDesktopServices>
#include <QLoggingCategory>
#include <QMessageBox>
#include <QQuickWidget>
#include <QUrlQuery>
namespace OCC {
Q_LOGGING_CATEGORY(lcGuiUtility, "gui.utility", QtInfoMsg)
}
namespace {
const QString dirTag()
{
return QStringLiteral("eu.qsfera.spaces.app");
}
const QString uuidTag()
{
return QStringLiteral("eu.qsfera.spaces.account-uuid");
}
} // anonymous namespace
using namespace OCC;
bool Utility::openBrowser(const QUrl &url, QWidget *errorWidgetParent)
{
if (!QDesktopServices::openUrl(url)) {
if (errorWidgetParent) {
QMessageBox::warning(
errorWidgetParent,
QCoreApplication::translate("utility", "Could not open browser"),
QCoreApplication::translate("utility",
"There was an error when launching the browser to go to "
"URL %1. Maybe no default browser is configured?")
.arg(url.toString()));
}
qCWarning(lcGuiUtility) << u"QDesktopServices::openUrl failed for" << url;
return false;
}
return true;
}
bool Utility::openEmailComposer(const QString &subject, const QString &body, QWidget *errorWidgetParent)
{
QUrl url(QStringLiteral("mailto:"));
QUrlQuery query;
query.setQueryItems({ { QLatin1String("subject"), subject },
{ QLatin1String("body"), body } });
url.setQuery(query);
if (!QDesktopServices::openUrl(url)) {
if (errorWidgetParent) {
QMessageBox::warning(
errorWidgetParent,
QCoreApplication::translate("utility", "Could not open email client"),
QCoreApplication::translate("utility",
"There was an error when launching the email client to "
"create a new message. Maybe no default email client is "
"configured?"));
}
qCWarning(lcGuiUtility) << u"QDesktopServices::openUrl failed for" << url;
return false;
}
return true;
}
QString Utility::vfsPinActionText()
{
return QCoreApplication::translate("utility", "Make always available locally");
}
QString Utility::vfsFreeSpaceActionText()
{
return QCoreApplication::translate("utility", "Free up local space");
}
void Utility::markDirectoryAsSyncRoot(const QString &path, const QUuid &accountUuid)
{
Q_ASSERT(getDirectorySyncRootMarkings(path).first.isEmpty());
Q_ASSERT(getDirectorySyncRootMarkings(path).second.isNull());
auto result1 = FileSystem::Tags::set(path, dirTag(), Theme::instance()->orgDomainName());
if (!result1) {
qCWarning(lcGuiUtility) << QStringLiteral("Failed to set tag on »%1«: %2").arg(path, result1.error())
#ifdef Q_OS_WIN
<< QStringLiteral("(filesystem %1)").arg(FileSystem::fileSystemForPath(path))
#endif // Q_OS_WIN
;
return;
}
auto result2 = FileSystem::Tags::set(path, uuidTag(), accountUuid.toString());
if (!result2) {
qCWarning(lcGuiUtility) << QStringLiteral("Failed to set tag on »%1«: %2").arg(path, result2.error())
#ifdef Q_OS_WIN
<< QStringLiteral("(filesystem %1)").arg(FileSystem::fileSystemForPath(path))
#endif // Q_OS_WIN
;
return;
}
}
std::pair<QString, QUuid> Utility::getDirectorySyncRootMarkings(const QString &path)
{
auto existingDirTag = FileSystem::Tags::get(path, dirTag());
auto existingUuidTag = FileSystem::Tags::get(path, uuidTag());
if (existingDirTag.has_value() && existingUuidTag.has_value()) {
return {existingDirTag.value(), QUuid::fromString(existingUuidTag.value())};
}
return {};
}
void Utility::unmarkDirectoryAsSyncRoot(const QString &path)
{
if (QFileInfo::exists(path)) {
if (!FileSystem::Tags::remove(path, dirTag())) {
qCWarning(lcGuiUtility) << u"Failed to remove tag on" << path;
Q_ASSERT(false);
}
if (!FileSystem::Tags::remove(path, uuidTag())) {
qCWarning(lcGuiUtility) << u"Failed to remove uuid tag on" << path;
Q_ASSERT(false);
}
}
}
+61
View File
@@ -0,0 +1,61 @@
/*
* Copyright (C) by Christian Kamm <mail@ckamm.de>
*
* 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.
*/
#pragma once
#include "gui/qsferaguilib.h"
#include "common/pinstate.h"
#include <QLoggingCategory>
#include <QString>
#include <QUrl>
#include <QWidget>
namespace OCC {
Q_DECLARE_LOGGING_CATEGORY(lcGuiUtility)
namespace Utility {
/** Open a URL in the browser.
*
* If launching the browser fails, display a message.
*/
bool openBrowser(const QUrl &url, QWidget *errorWidgetParent);
/** Start composing a new email message.
*
* If launching the email program fails, display a message.
*/
bool openEmailComposer(const QString &subject, const QString &body,
QWidget *errorWidgetParent);
/** Translated text for "making items always available locally" */
QString vfsPinActionText();
/** Translated text for "free up local space" (and unpinning the item) */
QString vfsFreeSpaceActionText();
void startShellIntegration();
QString socketApiSocketPath();
bool isInstalledByStore();
QSFERA_GUI_EXPORT void markDirectoryAsSyncRoot(const QString &path, const QUuid &accountUuid);
std::pair<QString, QUuid> getDirectorySyncRootMarkings(const QString &path);
void unmarkDirectoryAsSyncRoot(const QString &path);
} // namespace Utility
} // namespace OCC
+66
View File
@@ -0,0 +1,66 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
* Copyright (C) by Erik Verbruggen <erik@verbruggen.consulting>
*
* 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.
*/
#include "application.h"
#include "guiutility.h"
#include "libsync/theme.h"
#include <QProcess>
#import <Foundation/NSBundle.h>
namespace OCC {
void Utility::startShellIntegration()
{
QString bundlePath = QUrl::fromNSURL([NSBundle mainBundle].bundleURL).path();
auto _system = [](const QString &cmd, const QStringList &args) {
QProcess process;
process.setProcessChannelMode(QProcess::MergedChannels);
process.start(cmd, args);
if (!process.waitForFinished()) {
qCWarning(lcGuiUtility) << "Failed to load shell extension:" << cmd
<< args.join(QLatin1Char(' ')) << process.errorString();
} else {
qCInfo(lcGuiUtility) << (process.exitCode() != 0 ? "Failed to load" : "Loaded")
<< "shell extension:" << cmd << args.join(QLatin1Char(' '))
<< process.readAll();
}
};
// Add it again. This was needed for Mojave to trigger a load.
_system(QStringLiteral("pluginkit"), { QStringLiteral("-a"), QStringLiteral("%1Contents/PlugIns/FinderSyncExt.appex/").arg(bundlePath) });
// Tell Finder to use the Extension (checking it from System Preferences -> Extensions)
_system(QStringLiteral("pluginkit"),
{QStringLiteral("-e"), QStringLiteral("use"), QStringLiteral("-i"), Theme::instance()->orgDomainName() + QStringLiteral(".FinderSyncExt")});
}
QString Utility::socketApiSocketPath()
{
// This must match the code signing Team setting of the extension
// Example for developer builds (with ad-hoc signing identity): "" "eu.qsfera.desktop" ".socketApi"
// Example for official signed packages: "9B5WD74GWJ." "eu.qsfera.desktop" ".socketApi"
return QStringLiteral("%1%2.socketApi").arg(QStringLiteral(SOCKETAPI_TEAM_IDENTIFIER_PREFIX), Theme::instance()->orgDomainName());
}
bool Utility::isInstalledByStore()
{
return false;
}
} // namespace OCC
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright (C) by Erik Verbruggen <erik@verbruggen.consulting>
*
* 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.
*/
#include "guiutility.h"
#include "theme.h"
#include <QStandardPaths>
namespace OCC {
void Utility::startShellIntegration()
{
}
QString Utility::socketApiSocketPath()
{
return QStringLiteral("%1/QSfera/socket").arg(QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation));
}
bool Utility::isInstalledByStore()
{
return false;
}
} // namespace OCC
+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright (C) by Erik Verbruggen <erik@verbruggen.consulting>
*
* 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.
*/
#include "application.h"
#include "guiutility.h"
#include <QCoreApplication>
// windows.h mus be included before appmodel.h
#include <windows.h>
#include <appmodel.h>
namespace OCC {
void Utility::startShellIntegration()
{
}
QString Utility::socketApiSocketPath()
{
return QStringLiteral(R"(\\.\pipe\QSfera-%1)").arg(qEnvironmentVariable("USERNAME"));
}
bool Utility::isInstalledByStore()
{
uint32_t length = 0;
return GetCurrentPackageFamilyName(&length, nullptr) != APPMODEL_ERROR_NO_PACKAGE;
}
} // namespace OCC
+204
View File
@@ -0,0 +1,204 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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.
*/
#include "ignorelisteditor.h"
#include "ui_ignorelisteditor.h"
#include "configfile.h"
#include "gui/folderman.h"
#include <QDir>
#include <QFile>
#include <QInputDialog>
#include <QListWidgetItem>
#include <QMessageBox>
namespace OCC {
static const int patternCol = 0;
static const int deletableCol = 1;
static const int skippedLinesRole = Qt::UserRole;
static const int isGlobalRole = Qt::UserRole + 1;
IgnoreListEditor::IgnoreListEditor(QWidget *parent)
: QDialog(parent)
, ui(new Ui::IgnoreListEditor)
{
ui->setupUi(this);
ConfigFile cfgFile;
readOnlyTooltip = tr("This entry is provided by the system at %1 "
"and cannot be modified in this view.")
.arg(QDir::toNativeSeparators(cfgFile.excludeFile(ConfigFile::SystemScope)));
addPattern(QStringLiteral(".csync_journal.db*"), /*deletable=*/false, /*readonly=*/true, /*global=*/true);
addPattern(QStringLiteral("._sync_*.db*"), /*deletable=*/false, /*readonly=*/true, /*global=*/true);
addPattern(QStringLiteral(".sync_*.db*"), /*deletable=*/false, /*readonly=*/true, /*global=*/true);
readIgnoreFile(cfgFile.excludeFile(ConfigFile::SystemScope), /*global=*/true);
readIgnoreFile(cfgFile.excludeFile(ConfigFile::UserScope), /*global=*/false);
connect(this, &QDialog::accepted, this, &IgnoreListEditor::slotUpdateLocalIgnoreList);
ui->removePushButton->setEnabled(false);
connect(ui->tableWidget, &QTableWidget::itemSelectionChanged, this, &IgnoreListEditor::slotItemSelectionChanged);
connect(ui->removePushButton, &QAbstractButton::clicked, this, &IgnoreListEditor::slotRemoveCurrentItem);
connect(ui->addPushButton, &QAbstractButton::clicked, this, &IgnoreListEditor::slotAddPattern);
ui->tableWidget->resizeColumnsToContents();
ui->tableWidget->horizontalHeader()->setSectionResizeMode(patternCol, QHeaderView::Stretch);
ui->tableWidget->verticalHeader()->setVisible(false);
}
IgnoreListEditor::~IgnoreListEditor()
{
delete ui;
}
void IgnoreListEditor::slotItemSelectionChanged()
{
QTableWidgetItem *item = ui->tableWidget->currentItem();
if (!item) {
ui->removePushButton->setEnabled(false);
return;
}
bool enable = item->flags() & Qt::ItemIsEnabled;
ui->removePushButton->setEnabled(enable);
}
void IgnoreListEditor::slotRemoveCurrentItem()
{
ui->tableWidget->removeRow(ui->tableWidget->currentRow());
}
void IgnoreListEditor::slotUpdateLocalIgnoreList()
{
ConfigFile cfgFile;
QString ignoreFile = cfgFile.excludeFile(ConfigFile::UserScope);
QFile ignores(ignoreFile);
if (ignores.open(QIODevice::WriteOnly)) {
for (int row = 0; row < ui->tableWidget->rowCount(); ++row) {
QTableWidgetItem *patternItem = ui->tableWidget->item(row, patternCol);
QTableWidgetItem *deletableItem = ui->tableWidget->item(row, deletableCol);
if (patternItem->data(isGlobalRole).toBool())
continue;
const auto &skippedLines = patternItem->data(skippedLinesRole).toStringList();
for (const auto &line : skippedLines)
ignores.write(line.toUtf8() + '\n');
QByteArray prepend;
if (deletableItem->checkState() == Qt::Checked) {
prepend = "]";
} else if (patternItem->text().startsWith(QLatin1Char('#'))) {
prepend = "\\";
}
ignores.write(prepend + patternItem->text().toUtf8() + '\n');
}
} else {
QMessageBox::warning(this, tr("Could not open file"),
tr("Cannot write changes to »%1«.").arg(ignoreFile));
}
ignores.close(); //close the file before reloading stuff.
FolderMan *folderMan = FolderMan::instance();
// We need to force a remote discovery after a change of the ignore list.
// Otherwise we would not download the files/directories that are no longer
// ignored (because the remote etag did not change) (issue #3172)
for (auto *folder : folderMan->folders()) {
if (folder->isReady()) {
folder->journalDb()->forceRemoteDiscoveryNextSync();
folder->reloadExcludes();
folder->slotNextSyncFullLocalDiscovery();
folderMan->scheduler()->enqueueFolder(folder);
}
}
}
void IgnoreListEditor::slotAddPattern()
{
bool okClicked;
QString pattern = QInputDialog::getText(this, tr("Add Ignore Pattern"),
tr("Add a new ignore pattern:"),
QLineEdit::Normal, QString(), &okClicked);
if (!okClicked || pattern.isEmpty())
return;
addPattern(pattern, /*deletable=*/false, /*readonly=*/false, /*global=*/false);
ui->tableWidget->scrollToBottom();
}
void IgnoreListEditor::readIgnoreFile(const QString &file, bool global)
{
QFile ignores(file);
if (!ignores.open(QIODevice::ReadOnly))
return;
QStringList skippedLines;
bool readonly = global; // global ignores default to read-only
while (!ignores.atEnd()) {
QString line = QString::fromUtf8(ignores.readLine());
line.chop(1);
// Collect empty lines and comments, we want to preserve them
if (line.isEmpty() || line.startsWith(QLatin1String("#"))) {
skippedLines.append(line);
// A directive that prohibits editing in the ui
if (line == QLatin1String("#!readonly"))
readonly = true;
continue;
}
bool deletable = false;
if (line.startsWith(QLatin1Char(']'))) {
deletable = true;
line = line.mid(1);
}
// Add and reset
addPattern(line, deletable, readonly, global, skippedLines);
skippedLines.clear();
readonly = global;
}
}
int IgnoreListEditor::addPattern(const QString &pattern, bool deletable, bool readOnly, bool global, const QStringList &skippedLines)
{
int newRow = ui->tableWidget->rowCount();
ui->tableWidget->setRowCount(newRow + 1);
QTableWidgetItem *patternItem = new QTableWidgetItem;
patternItem->setText(pattern);
patternItem->setData(skippedLinesRole, skippedLines);
patternItem->setData(isGlobalRole, global);
ui->tableWidget->setItem(newRow, patternCol, patternItem);
QTableWidgetItem *deletableItem = new QTableWidgetItem;
deletableItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
deletableItem->setCheckState(deletable ? Qt::Checked : Qt::Unchecked);
ui->tableWidget->setItem(newRow, deletableCol, deletableItem);
if (readOnly) {
patternItem->setFlags(patternItem->flags() ^ Qt::ItemIsEnabled);
patternItem->setToolTip(readOnlyTooltip);
deletableItem->setFlags(deletableItem->flags() ^ Qt::ItemIsEnabled);
}
return newRow;
}
} // namespace OCC
+56
View File
@@ -0,0 +1,56 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
*
* 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.
*/
#ifndef IGNORELISTEDITOR_H
#define IGNORELISTEDITOR_H
#include <QDialog>
class QListWidgetItem;
namespace OCC {
namespace Ui {
class IgnoreListEditor;
}
/**
* @brief The IgnoreListEditor class
* @ingroup gui
*/
class IgnoreListEditor : public QDialog
{
Q_OBJECT
public:
explicit IgnoreListEditor(QWidget *parent = nullptr);
~IgnoreListEditor() override;
private Q_SLOTS:
void slotItemSelectionChanged();
void slotRemoveCurrentItem();
void slotUpdateLocalIgnoreList();
void slotAddPattern();
private:
void readIgnoreFile(const QString &file, bool global);
int addPattern(const QString &pattern, bool deletable, bool readOnly, bool global,
const QStringList &skippedLines = QStringList());
QString readOnlyTooltip;
Ui::IgnoreListEditor *ui;
};
} // namespace OCC
#endif // IGNORELISTEDITOR_H
+164
View File
@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::IgnoreListEditor</class>
<widget class="QDialog" name="OCC::IgnoreListEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>555</width>
<height>609</height>
</rect>
</property>
<property name="windowTitle">
<string>Ignored Files Editor</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Files Ignored by Patterns</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QTableWidget" name="tableWidget">
<property name="enabled">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="columnCount">
<number>2</number>
</property>
<column>
<property name="text">
<string>Pattern</string>
</property>
</column>
<column>
<property name="text">
<string>Allow Deletion</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="addPushButton">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removePushButton">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="enabled">
<bool>true</bool>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>213</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="descriptionLabel">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Files or folders matching a pattern will not be synchronized. Changes take effect the next time folders are synchronized.
Items where deletion is allowed will be deleted if they prevent a directory from being removed. This is useful for meta data.</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>OCC::IgnoreListEditor</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>348</x>
<y>333</y>
</hint>
<hint type="destinationlabel">
<x>361</x>
<y>355</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>OCC::IgnoreListEditor</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>210</x>
<y>333</y>
</hint>
<hint type="destinationlabel">
<x>385</x>
<y>297</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+425
View File
@@ -0,0 +1,425 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#include <QtGui>
#include <QtWidgets>
#include "account.h"
#include "accountmanager.h"
#include "commonstrings.h"
#include "folder.h"
#include "folderman.h"
#include "gui/models/expandingheaderview.h"
#include "issueswidget.h"
#include "libsync/configfile.h"
#include "models/models.h"
#include "protocolwidget.h"
#include "syncengine.h"
#include "syncfileitem.h"
#include "theme.h"
#include "ui_issueswidget.h"
namespace {
bool persistsUntilLocalDiscovery(const OCC::ProtocolItem &data)
{
return data.status() == OCC::SyncFileItem::Conflict
|| (data.status() == OCC::SyncFileItem::FileIgnored && data.direction() == OCC::SyncFileItem::Up)
|| data.status() == OCC::SyncFileItem::Excluded;
}
}
namespace OCC {
class SyncFileItemStatusSetSortFilterProxyModel : public Models::SignalledQSortFilterProxyModel
{
public:
using StatusSet = std::array<bool, SyncFileItem::StatusCount>;
explicit SyncFileItemStatusSetSortFilterProxyModel(QObject *parent = nullptr)
: Models::SignalledQSortFilterProxyModel(parent)
{
restoreFilter();
}
~SyncFileItemStatusSetSortFilterProxyModel() override
{
}
StatusSet filter() const
{
return _filter;
}
void setFilter(const StatusSet &newFilter, bool save = true)
{
if (_filter != newFilter) {
_filter = newFilter;
if (save) {
saveFilter();
}
invalidateFilter();
Q_EMIT filterChanged();
}
}
void resetFilter()
{
setFilter(defaultFilter());
}
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
{
QModelIndex idx = sourceModel()->index(sourceRow, filterKeyColumn(), sourceParent);
bool ok = false;
int sourceData = sourceModel()->data(idx, filterRole()).toInt(&ok);
if (!ok) {
return false;
}
return _filter[static_cast<SyncFileItem::Status>(sourceData)];
}
int filterCount()
{
StatusSet defaultSet = defaultFilter();
OC_ASSERT(defaultSet.size() == _filter.size());
int count = 0;
// The number of filters is the number of items in the current filter that differ from the default set.
for (size_t i = 0, ei = defaultSet.size(); i != ei; ++i) {
// All errors are shown as a single filter, and they are all turned on or off together.
// So to show them as 1 filter, ignore the first three errors...
switch (i) {
case SyncFileItem::Status::FatalError:
Q_FALLTHROUGH();
case SyncFileItem::Status::NormalError:
Q_FALLTHROUGH();
case SyncFileItem::Status::SoftError:
break;
// ... but *do* count the last one ...
case SyncFileItem::Status::DetailError:
Q_FALLTHROUGH();
default:
// ... just like the other status items:
if (defaultSet[i] != _filter[i]) {
count += 1;
}
}
}
return count;
}
private:
static StatusSet defaultFilter()
{
StatusSet defaultSet;
defaultSet.fill(true);
defaultSet[SyncFileItem::NoStatus] = false;
defaultSet[SyncFileItem::Success] = false;
return defaultSet;
}
void saveFilter()
{
QStringList checked;
for (std::underlying_type_t<SyncFileItem::Status> s = SyncFileItem::NoStatus; s < SyncFileItem::StatusCount; ++s) {
if (_filter[s]) {
checked.append(Utility::enumToString(static_cast<SyncFileItem::Status>(s)));
}
}
ConfigFile().setIssuesWidgetFilter(checked);
}
void restoreFilter()
{
StatusSet filter = {};
bool filterNeedsReset = true; // If there is no filter, the `true` value will cause a reset.
std::optional<QStringList> checked = ConfigFile().issuesWidgetFilter();
if (checked.has_value()) {
// There is a filter, but it can be empty (user unchecked all checkboxes), and in that case we do not want to reset the filter.
filterNeedsReset = false;
for (const QString &s : checked.value()) {
const auto status = Utility::stringToEnum<SyncFileItem::Status>(s);
if (status == static_cast<SyncFileItem::Status>(-1)) {
// The string value is not a valid enum value, so stop processing, and queue a reset.
filterNeedsReset = true;
break;
} else {
filter[status] = true;
}
}
}
if (filterNeedsReset) {
// If there was no filter in the config file, or if one of the values is invalid, reset the filter.
resetFilter();
} else {
// There is a valid filter, so apply it. Also don't save it, because we just loaded it successfully
setFilter(filter, false);
}
}
private:
StatusSet _filter = {};
};
/**
* If more issues are reported than this they will not show up
* to avoid performance issues around sorting this many issues.
*/
IssuesWidget::IssuesWidget(QWidget *parent)
: QWidget(parent)
, _ui(new Ui::IssuesWidget)
{
_ui->setupUi(this);
connect(ProgressDispatcher::instance(), &ProgressDispatcher::progressInfo,
this, &IssuesWidget::slotProgressInfo);
connect(ProgressDispatcher::instance(), &ProgressDispatcher::itemCompleted,
this, &IssuesWidget::slotItemCompleted);
connect(ProgressDispatcher::instance(), &ProgressDispatcher::syncError,
this, [this](Folder *folder, const QString &message, ErrorCategory) {
auto item = SyncFileItemPtr::create();
item->_status = SyncFileItem::NormalError;
item->_errorString = message;
_model->addProtocolItem(ProtocolItem { folder, item });
});
connect(ProgressDispatcher::instance(), &ProgressDispatcher::excluded, this, [this](Folder *f, const QString &file) {
auto item = SyncFileItemPtr::create(file);
item->_status = SyncFileItem::FilenameReserved;
item->_errorString = tr("The file »%1« was ignored as its name is reserved by %2").arg(file, Theme::instance()->appNameGUI());
_model->addProtocolItem(ProtocolItem { f, item });
});
_model = new ProtocolItemModel(20000, true, this);
_sortModel = new Models::SignalledQSortFilterProxyModel(this);
connect(_sortModel, &Models::SignalledQSortFilterProxyModel::filterChanged, this, &IssuesWidget::filterDidChange);
_sortModel->setSourceModel(_model);
_statusSortModel = new SyncFileItemStatusSetSortFilterProxyModel(this); // Note: this will restore a previously set filter, if there was one.
connect(_statusSortModel, &Models::SignalledQSortFilterProxyModel::filterChanged, this, &IssuesWidget::filterDidChange);
_statusSortModel->setSourceModel(_sortModel);
_statusSortModel->setSortRole(Qt::DisplayRole); // Sorting should be done based on the text in the column cells, but...
_statusSortModel->setFilterRole(Models::UnderlyingDataRole); // ... filtering should be done on the underlying enum value.
_statusSortModel->setFilterKeyColumn(static_cast<int>(ProtocolItemModel::ProtocolItemRole::Status));
_ui->_tableView->setModel(_statusSortModel);
auto header = new ExpandingHeaderView(QStringLiteral("ActivityErrorListHeaderV2"), _ui->_tableView);
_ui->_tableView->setHorizontalHeader(header);
header->setSectionResizeMode(QHeaderView::Interactive);
header->setExpandingColumn(static_cast<int>(ProtocolItemModel::ProtocolItemRole::Action));
header->setSortIndicator(static_cast<int>(ProtocolItemModel::ProtocolItemRole::Time), Qt::DescendingOrder);
connect(_ui->_tableView, &QTreeView::customContextMenuRequested, this, &IssuesWidget::slotItemContextMenu);
_ui->_tableView->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
connect(header, &QHeaderView::customContextMenuRequested, this, [this, header]() {
auto menu = showFilterMenu(header);
menu->addAction(tr("Reset column sizes"), header, [header] { header->resizeColumns(true); });
});
connect(_ui->_filterButton, &QAbstractButton::clicked, this, [this] {
showFilterMenu(_ui->_filterButton);
});
filterDidChange(); // Set the appropriate label.
_ui->_tooManyIssuesWarning->hide();
connect(_model, &ProtocolItemModel::rowsInserted, this, [this] {
Q_EMIT issueCountUpdated(_model->rowCount());
_ui->_tooManyIssuesWarning->setVisible(_model->isModelFull());
});
connect(_model, &ProtocolItemModel::modelReset, this, [this] {
Q_EMIT issueCountUpdated(_model->rowCount());
_ui->_tooManyIssuesWarning->setVisible(_model->isModelFull());
});
connect(FolderMan::instance(), &FolderMan::folderRemoved, this, [this](Folder *f) {
_model->remove_if([f](const ProtocolItem &item) {
return item.folder() == f;
});
});
}
IssuesWidget::~IssuesWidget()
{
delete _ui;
}
QMenu *IssuesWidget::showFilterMenu(QWidget *parent)
{
auto menu = new QMenu(parent);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->setAccessibleName(tr("Filter menu"));
auto accountFilterReset = Models::addFilterMenuItems(menu, AccountManager::instance()->accountNames(), _sortModel, static_cast<int>(ProtocolItemModel::ProtocolItemRole::Account), tr("Account"), Qt::DisplayRole);
menu->addSeparator();
auto statusFilterReset = addStatusFilter(menu);
menu->addSeparator();
addResetFiltersAction(menu, { accountFilterReset, statusFilterReset });
QTimer::singleShot(0, menu, [menu] {
// FIXME: when activated by the keyboard, this position can be anywhere.
menu->popup(QCursor::pos());
// accassebility
menu->setFocus();
});
return menu;
}
void IssuesWidget::addResetFiltersAction(QMenu *menu, const QList<std::function<void()>> &resetFunctions)
{
menu->addAction(QCoreApplication::translate("OCC::Models", "Reset Filters"), menu, [resetFunctions]() {
for (const auto &reset : resetFunctions) {
reset();
}
});
}
void IssuesWidget::slotProgressInfo(Folder *folder, const ProgressInfo &progress)
{
if (progress.status() == ProgressInfo::Reconcile) {
// Wipe all non-persistent entries - as well as the persistent ones
// in cases where a local discovery was done.
const auto &engine = folder->syncEngine();
const auto style = engine.lastLocalDiscoveryStyle();
_model->remove_if([&](const ProtocolItem &item) {
if (item.folder() != folder) {
return false;
}
if (item.direction() == SyncFileItem::None && item.status() == SyncFileItem::FilenameReserved) {
// TODO: don't clear syncErrors and excludes for now.
// make them either unique or remove them on the next sync?
return false;
}
if (style == LocalDiscoveryStyle::FilesystemOnly) {
return true;
}
if (!persistsUntilLocalDiscovery(item)) {
return true;
}
// Definitely wipe the entry if the file no longer exists
if (!QFileInfo::exists(folder->path() + item.path())) {
return true;
}
auto path = QFileInfo(item.path()).dir().path();
if (path == QLatin1Char('.'))
path.clear();
return engine.shouldDiscoverLocally(path);
});
}
if (progress.status() == ProgressInfo::Done) {
// We keep track very well of pending conflicts.
// Inform other components about them.
QStringList conflicts;
for (const auto &rawData : _model->rawData()) {
if (rawData.folder() == folder && rawData.status() == SyncFileItem::Conflict) {
conflicts.append(rawData.path());
}
}
Q_EMIT ProgressDispatcher::instance() -> folderConflicts(folder, conflicts);
}
}
void IssuesWidget::slotItemCompleted(Folder *folder, const SyncFileItemPtr &item)
{
if (!item->showInIssuesTab())
return;
_model->addProtocolItem(ProtocolItem { folder, item });
}
void IssuesWidget::filterDidChange()
{
// We have two filters here: the filter by status (which can have multiple items checked *off*...
int filterCount = _statusSortModel->filterCount();
// .. and the filter on the account name, which can only be 1 item checked:
if (!_sortModel->filterRegularExpression().pattern().isEmpty()) {
filterCount += 1;
}
_ui->_filterButton->setText(filterCount > 0 ? CommonStrings::filterButtonText(filterCount) : tr("Filter"));
}
void IssuesWidget::slotItemContextMenu(const QPoint &pos)
{
auto rows = _ui->_tableView->selectionModel()->selectedRows();
for (int i = 0; i < rows.size(); ++i) {
rows[i] = _statusSortModel->mapToSource(rows[i]);
rows[i] = _sortModel->mapToSource(rows[i]);
}
ProtocolWidget::showContextMenu(this, _ui->_tableView, _sortModel, _model, rows, pos);
}
std::function<void(void)> IssuesWidget::addStatusFilter(QMenu *menu)
{
menu->addAction(QCoreApplication::translate("OCC::Models", "Status Filter:"))->setEnabled(false);
// Use a QActionGroup to contain all status filter items, so we can find them back easily to reset.
auto statusFilterGroup = new QActionGroup(menu);
statusFilterGroup->setExclusive(false);
const auto initialFilter = _statusSortModel->filter();
{ // Add all errors under 1 action:
auto action = menu->addAction(Utility::enumToDisplayName(SyncFileItem::NormalError), this, [this](bool checked) {
auto currentFilter = _statusSortModel->filter();
for (const auto &item : SyncFileItem::ErrorStatusItems) {
currentFilter[item] = checked;
}
_statusSortModel->setFilter(currentFilter);
});
action->setCheckable(true);
action->setChecked(initialFilter[SyncFileItem::ErrorStatusItems[0]]);
statusFilterGroup->addAction(action);
}
menu->addSeparator();
// list of OtherDisplayableStatusItems with the localised name
std::vector<std::pair<QString, SyncFileItem::Status>> otherStatusItems;
otherStatusItems.reserve(SyncFileItem::OtherDisplayableStatusItems.size());
for (const auto &item : SyncFileItem::OtherDisplayableStatusItems) {
otherStatusItems.emplace_back(Utility::enumToDisplayName(item), item);
}
std::sort(otherStatusItems.begin(), otherStatusItems.end(), [](const auto &a, const auto &b) {
return a.first < b.first;
});
for (const auto &item : otherStatusItems) {
auto action = menu->addAction(item.first, menu, [this, item](bool checked) {
auto currentFilter = _statusSortModel->filter();
currentFilter[item.second] = checked;
_statusSortModel->setFilter(currentFilter);
});
action->setCheckable(true);
action->setChecked(initialFilter[item.second]);
statusFilterGroup->addAction(action);
}
menu->addSeparator();
// Add action to reset all filters at once:
return [statusFilterGroup, this]() {
for (QAction *action : statusFilterGroup->actions()) {
action->setChecked(true);
}
_statusSortModel->resetFilter();
};
}
}
+68
View File
@@ -0,0 +1,68 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#ifndef ISSUESWIDGET_H
#define ISSUESWIDGET_H
#include "models/models.h"
#include "models/protocolitemmodel.h"
#include "progressdispatcher.h"
class QSortFilterProxyModel;
namespace OCC {
class SyncResult;
class SyncFileItemStatusSetSortFilterProxyModel;
namespace Ui {
class IssuesWidget;
}
class Application;
/**
* @brief The ProtocolWidget class
* @ingroup gui
*/
class IssuesWidget : public QWidget
{
Q_OBJECT
public:
explicit IssuesWidget(QWidget *parent = nullptr);
~IssuesWidget() override;
public Q_SLOTS:
void slotProgressInfo(Folder *folder, const ProgressInfo &progress);
void slotItemCompleted(Folder *folder, const SyncFileItemPtr &item);
void filterDidChange();
Q_SIGNALS:
void issueCountUpdated(int);
private Q_SLOTS:
QMenu *showFilterMenu(QWidget *parent);
void slotItemContextMenu(const QPoint &pos);
private:
static void addResetFiltersAction(QMenu *menu, const QList<std::function<void()>> &resetFunctions);
std::function<void()> addStatusFilter(QMenu *menu);
ProtocolItemModel *_model;
Models::SignalledQSortFilterProxyModel *_sortModel;
SyncFileItemStatusSetSortFilterProxyModel *_statusSortModel;
Ui::IssuesWidget *_ui;
};
}
#endif
+91
View File
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::IssuesWidget</class>
<widget class="QWidget" name="OCC::IssuesWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>580</width>
<height>578</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="_filterButton"/>
</item>
</layout>
</item>
<item>
<widget class="QTableView" name="_tableView">
<property name="contextMenuPolicy">
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
</property>
<property name="accessibleName">
<string>Issues table</string>
</property>
<property name="tabKeyNavigation">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<property name="showGrid">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="_tooManyIssuesWarning">
<property name="text">
<string>There were too many issues. Not all will be visible here.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>_filterButton</tabstop>
<tabstop>_tableView</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>
+72
View File
@@ -0,0 +1,72 @@
/*
* Copyright (C) by Christian Kamm <mail@ckamm.de>
*
* 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.
*/
#include "lockwatcher.h"
#include "filesystem.h"
#include <QLoggingCategory>
#include <QTimer>
#include <chrono>
using namespace std::chrono_literals;
using namespace OCC;
Q_LOGGING_CATEGORY(lcLockWatcher, "gui.lockwatcher", QtInfoMsg)
namespace {
const auto check_frequency = 20s;
}
LockWatcher::LockWatcher(QObject *parent)
: QObject(parent)
{
connect(&_timer, &QTimer::timeout,
this, &LockWatcher::checkFiles);
_timer.start(check_frequency);
}
void LockWatcher::addFile(const QString &path, FileSystem::LockMode mode)
{
qCInfo(lcLockWatcher) << u"Watching for lock of" << path << mode << u"being released";
_watchedPaths.insert({ path, mode });
}
void LockWatcher::setCheckInterval(std::chrono::milliseconds interval)
{
_timer.start(interval.count());
}
bool LockWatcher::contains(const QString &path, OCC::FileSystem::LockMode mode) const
{
return _watchedPaths.find({ path, mode }) != _watchedPaths.cend();
}
void LockWatcher::checkFiles()
{
// copy as Q_EMIT fileUnlocked might trigger a new insert
const auto watchedPathsCopy = _watchedPaths;
decltype(_watchedPaths) unlocked;
for (const auto &p : watchedPathsCopy) {
if (!FileSystem::isFileLocked(p.first, p.second)) {
qCInfo(lcLockWatcher) << u"Lock of" << p.first << p.second << u"was released";
Q_EMIT fileUnlocked(p.first, p.second);
unlocked.insert(p);
}
}
for (const auto &removed : std::as_const(unlocked)) {
_watchedPaths.erase(removed);
}
}
+87
View File
@@ -0,0 +1,87 @@
/*
* Copyright (C) by Christian Kamm <mail@ckamm.de>
*
* 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.
*/
#pragma once
#include "gui/qsferaguilib.h"
#include "filesystem.h"
#include <QList>
#include <QObject>
#include <QString>
#include <QSet>
#include <QTimer>
#include <chrono>
#include <unordered_set>
namespace OCC {
/**
* @brief Monitors files that are locked, signaling when they become unlocked
*
* Only relevant on Windows. Some high-profile applications like Microsoft
* Word lock the document that is currently being edited. The synchronization
* client will be unable to update them while they are locked.
*
* In this situation we do want to start a sync run as soon as the file
* becomes available again. To do that, we need to regularly check whether
* the file is still being locked.
*
* @ingroup gui
*/
class QSFERA_GUI_EXPORT LockWatcher : public QObject
{
Q_OBJECT
public:
explicit LockWatcher(QObject *parent = nullptr);
/** Start watching a file.
*
* If the file is not locked later on, the fileUnlocked signal will be
* emitted once.
*/
void addFile(const QString &path, OCC::FileSystem::LockMode mode);
/** Adjusts the default interval for checking whether the lock is still present */
void setCheckInterval(std::chrono::milliseconds interval);
/** Whether the path is being watched for lock-changes */
bool contains(const QString &path, OCC::FileSystem::LockMode mode) const;
Q_SIGNALS:
/** Emitted when one of the watched files is no longer
* being locked. */
void fileUnlocked(const QString &path, OCC::FileSystem::LockMode mode);
private Q_SLOTS:
void checkFiles();
private:
using LockKey = std::pair<QString, FileSystem::LockMode>;
struct HashLockKey
{
size_t operator()(const LockKey &k) const
{
return std::hash<QString> {}(k.first) ^ std::hash<int> {}(static_cast<int>(k.second));
}
};
std::unordered_set<LockKey, HashLockKey> _watchedPaths;
QTimer _timer;
};
}
+100
View File
@@ -0,0 +1,100 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#include "logbrowser.h"
#include <iostream>
#include <QDesktopServices>
#include <QDir>
#include <QLayout>
#include <QMessageBox>
#include <optional>
#include "configfile.h"
#include "guiutility.h"
#include "logger.h"
#include "resources/fonticon.h"
#include "ui_logbrowser.h"
#include "resources/resources.h"
namespace OCC {
// ==============================================================================
LogBrowser::LogBrowser(QWidget *parent)
: QDialog(parent)
, ui(new Ui::LogBrowser)
{
ui->setupUi(this);
ui->warningIcon->setPixmap(Resources::FontIcon(u'').pixmap(ui->warningIcon->size()));
ui->locationLabel->setText(Logger::instance()->temporaryFolderLogDirPath());
ui->enableLoggingButton->setChecked(ConfigFile().automaticLogDir());
connect(ui->enableLoggingButton, &QCheckBox::toggled, this, &LogBrowser::togglePermanentLogging);
ui->httpLogButton->setChecked(ConfigFile().logHttp());
connect(ui->httpLogButton, &QCheckBox::toggled, this, [](bool enable) {
ConfigFile().configureHttpLogging(std::make_optional(enable));
});
ui->spinBox_numberOflogsToKeep->setValue(ConfigFile().automaticDeleteOldLogs());
connect(ui->spinBox_numberOflogsToKeep, qOverload<int>(&QSpinBox::valueChanged), this, [](int i) {
ConfigFile().setAutomaticDeleteOldLogs(i);
Logger::instance()->setMaxLogFiles(i);
});
connect(ui->openFolderButton, &QPushButton::clicked, this, []() {
QString path = Logger::instance()->temporaryFolderLogDirPath();
QDir().mkpath(path);
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
});
connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &QWidget::close);
ConfigFile cfg;
cfg.restoreGeometry(this);
}
LogBrowser::~LogBrowser()
{
}
void LogBrowser::setupLoggingFromConfig()
{
ConfigFile config;
auto logger = Logger::instance();
if (config.automaticLogDir()) {
// Don't override other configured logging
if (logger->isLoggingToFile())
return;
logger->setupTemporaryFolderLogDir();
Logger::instance()->setMaxLogFiles(config.automaticDeleteOldLogs());
} else {
logger->disableTemporaryFolderLogDir();
}
}
void LogBrowser::togglePermanentLogging(bool enabled)
{
ConfigFile config;
config.setAutomaticLogDir(enabled);
setupLoggingFromConfig();
}
} // namespace
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
*
* 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.
*/
#pragma once
#include "gui/qsferaguilib.h"
#include <QDialog>
namespace OCC {
namespace Ui {
class LogBrowser;
};
/**
* @brief The LogBrowser class
* @ingroup gui
*/
class QSFERA_GUI_EXPORT LogBrowser : public QDialog
{
Q_OBJECT
public:
explicit LogBrowser(QWidget *parent);
~LogBrowser() override;
/** Sets Logger settings depending on ConfigFile values.
*
* Currently used for establishing logging to a temporary directory.
* Will only enable logging if it isn't enabled already.
*/
static void setupLoggingFromConfig();
protected Q_SLOTS:
void togglePermanentLogging(bool enabled);
private:
QScopedPointer<Ui::LogBrowser> ui;
};
} // namespace
+198
View File
@@ -0,0 +1,198 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::LogBrowser</class>
<widget class="QDialog" name="OCC::LogBrowser">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>399</height>
</rect>
</property>
<property name="windowTitle">
<string>Log Output</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="warningIcon">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../resources/client.qrc">:/client/resources/light/warning.svg</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;b&gt;The logs contain sensitive information which you should not make publicly available&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>The client can write debug logs to a temporary folder. These logs are very helpful for diagnosing problems.
Since log files can get large, the client will start a new one for each sync run and compress older ones.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>If enabled, logs will be written to:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="locationLabel">
<property name="text">
<string>C:/log</string>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="enableLoggingButton">
<property name="text">
<string>Enable logging to temporary folder</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="httpLogButton">
<property name="text">
<string>Log Http traffic </string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Log files to keep:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBox_numberOflogsToKeep">
<property name="minimum">
<number>5</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>These settings persist across client restarts.
Note that using any logging command line options will override the settings.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="openFolderButton">
<property name="text">
<string>Open folder</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../resources/client.qrc"/>
</resources>
<connections/>
</ui>
+531
View File
@@ -0,0 +1,531 @@
/*
*
* Copyright (C) by Duncan Mac-Vicar P. <duncan@kde.org>
*
* 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.
*/
#include <QtGlobal>
#include "accountmanager.h"
#include "common/restartmanager.h"
#include "gui/application.h"
#include "gui/folderman.h"
#include "gui/logbrowser.h"
#include "gui/networkinformation.h"
#include "libsync/configfile.h"
#include "libsync/platform.h"
#include "libsync/theme.h"
#include "resources/loadresources.h"
#include "common/version.h"
#include "gui/translations.h"
#include "libsync/logger.h"
#include "socketapi/socketapi.h"
#include <kdsingleapplication.h>
#ifdef WITH_AUTO_UPDATER
#include "updater/updater.h"
#endif
#include <QApplication>
#include <QCommandLineParser>
#include <QLibraryInfo>
#include <QMessageBox>
#include <QProcess>
#include <QTimer>
#include <QTranslator>
#ifdef Q_OS_WIN
#include <qt_windows.h>
#endif
#include <iostream>
using namespace std::chrono_literals;
using namespace OCC;
Q_LOGGING_CATEGORY(lcMain, "gui.main", QtInfoMsg)
namespace {
inline auto msgParseOptionsC()
{
return QStringLiteral("MSG_PARSEOPTIONS:");
}
// Helpers for displaying messages. Note that there is probably no console on Windows.
void displayHelpText(const QString &t)
{
Logger::instance()->attacheToConsole();
std::cout << qUtf8Printable(t) << std::endl;
#ifdef Q_OS_WIN
// No console on Windows.
QString spaces(80, QLatin1Char(' ')); // Add a line of non-wrapped space to make the messagebox wide enough.
QString text =
QStringLiteral("<qt><pre style='white-space:pre-wrap'>") + t.toHtmlEscaped() + QStringLiteral("</pre><pre>") + spaces + QStringLiteral("</pre></qt>");
QMessageBox::information(nullptr, Theme::instance()->appNameGUI(), text);
#endif
}
struct CommandLineOptions
{
bool show = false;
bool quitInstance = false;
QString logDir;
QString logFile;
bool logFlush = false;
bool logDebug = false;
bool debugMode = false;
};
CommandLineOptions parseOptions(const QStringList &arguments)
{
QCommandLineParser parser;
QString descriptionText;
QTextStream descriptionTextStream(&descriptionText);
descriptionTextStream << QApplication::translate("CommandLine", "%1 version %2\r\nFile synchronization desktop utility.")
.arg(Theme::instance()->appName(), OCC::Version::displayString())
<< Qt::endl;
if (Resources::isVanillaTheme() && !Theme::instance()->helpUrl().isEmpty()) {
descriptionTextStream
<< Qt::endl
<< Qt::endl
<< QApplication::translate("CommandLine", "For more information, see %1", "link to homepage").arg(Theme::instance()->helpUrl().toString());
}
parser.setApplicationDescription(descriptionText);
auto helpOption = parser.addHelpOption();
auto versionOption = parser.addVersionOption();
// this little snippet saves a few lines below
auto addOption = [&parser](const QCommandLineOption &option) {
parser.addOption(option);
return option;
};
auto showSettingsLegacyOption = QCommandLineOption{{QStringLiteral("showsettings")}, QStringLiteral("Hidden legacy option")};
showSettingsLegacyOption.setFlags(QCommandLineOption::HiddenFromHelp);
parser.addOption(showSettingsLegacyOption);
auto showOption = addOption({{QStringLiteral("s"), QStringLiteral("show")},
QApplication::translate("CommandLine",
"Start with the main window visible, or if it is already running, bring it to the front. By default, the client launches in the background.")});
auto quitInstanceOption = addOption({{QStringLiteral("q"), QStringLiteral("quit")}, QApplication::translate("CommandLine", "Quit the running instance.")});
auto logFileOption = addOption(
{QStringLiteral("logfile"), QApplication::translate("CommandLine", "Write log to file (use - to write to stdout)."), QStringLiteral("filename")});
auto logDirOption = addOption(
{QStringLiteral("logdir"), QApplication::translate("CommandLine", "Write each sync log output in a new file in folder."), QStringLiteral("name")});
auto logFlushOption = addOption({QStringLiteral("logflush"), QApplication::translate("CommandLine", "Flush the log file after every write.")});
auto logDebugOption = addOption({QStringLiteral("logdebug"), QApplication::translate("CommandLine", "Output debug-level messages in the log.")});
auto debugOption = addOption({QStringLiteral("debug"), QApplication::translate("CommandLine", "Enable debug mode.")});
addOption({QStringLiteral("cmd"), QApplication::translate("CommandLine", "Forward all arguments to the cmd client. This argument must be the first.")});
parser.process(arguments);
CommandLineOptions out;
if (parser.isSet(showOption) || parser.isSet(showSettingsLegacyOption)) {
out.show = true;
}
if (parser.isSet(quitInstanceOption)) {
out.quitInstance = true;
}
if (parser.isSet(logFileOption)) {
out.logFile = parser.value(logFileOption);
}
if (parser.isSet(logDirOption)) {
if (parser.isSet(logFileOption)) {
displayHelpText(QApplication::translate("CommandLine", "--logfile and --logdir are mutually exclusive"));
std::exit(1);
}
out.logDir = parser.value(logDirOption);
}
if (parser.isSet(logFlushOption)) {
out.logFlush = true;
}
if (parser.isSet(logDebugOption)) {
out.logDebug = true;
}
if (parser.isSet(debugOption)) {
out.logDebug = true;
out.debugMode = true;
}
return out;
}
void showDowngradeDialog()
{
QMessageBox box(QMessageBox::Warning, Theme::instance()->appNameGUI(),
QCoreApplication::translate("version check",
"Some settings were configured in newer versions of this client "
"and use features that are not available in this version"));
box.addButton(OCC::Application::tr("Quit"), QMessageBox::AcceptRole);
box.exec();
QTimer::singleShot(0, qApp, &QApplication::quit);
}
/**
* Check if the last version used to write the config file differs from the current version.
* If the current version is newer, update the config file with our current version. If the
* current version is older, refuse to do anything: this is a downgrade, and it is too risky to
* assume that things might work "just fine".
*/
bool checkClientVersion()
{
ConfigFile configFile;
// Did the client version change?
// (The client version is adjusted further down)
auto configVersion = QVersionNumber::fromString(configFile.clientVersionWithBuildNumberString());
auto clientVersion = OCC::Version::versionWithBuildNumber();
if (configVersion == clientVersion) {
// no config backup needed
return true;
}
// We allow downgrades as long as the major version stays the same.
if (clientVersion.majorVersion() < configVersion.majorVersion()) {
// We refuse to downgrade, too much can go wrong.
showDowngradeDialog();
return false;
}
// We're okay to continue. The settings will be updated in other parts, but here we bump the
// version we store in the config file.
configFile.backup();
configFile.setClientVersionWithBuildNumberString(OCC::Version::versionWithBuildNumber().toString());
return true;
}
void setupLogging(const CommandLineOptions &options)
{
// might be called from second instance
auto logger = Logger::instance();
logger->install();
// call setLogFlush first, other log settings might already imply flushing
// so setting it false in the end will have undesired results.
logger->setLogFlush(options.logFlush);
if (!options.logDir.isEmpty()) {
logger->setLogDir(options.logDir);
}
if (!options.logFile.isEmpty()) {
Q_ASSERT(options.logDir.isEmpty());
logger->setLogFile(options.logFile);
}
logger->setLogDebug(options.logDebug);
// Possibly configure logging from config file
LogBrowser::setupLoggingFromConfig();
qCInfo(lcMain) << u"##################" << Theme::instance()->appName() << u"locale:" << QLocale::system().name() << u"version:"
<< Theme::instance()->aboutVersions(Theme::VersionFormat::OneLiner);
qCInfo(lcMain) << u"Arguments:" << qApp->arguments();
}
QString setupTranslations(QApplication *app)
{
const auto trPath = Translations::translationsDirectoryPath();
qCDebug(lcMain) << u"Translations directory path:" << trPath;
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
QStringList uiLanguages = QLocale::system().uiLanguages(QLocale::TagSeparator::Underscore);
#else
QStringList uiLanguages;
for (auto lang : QLocale::system().uiLanguages()) {
uiLanguages << lang.replace(QLatin1Char('-'), QLatin1Char('_'));
}
#endif
qCDebug(lcMain) << u"UI languages:" << uiLanguages;
// the user can also set a locale in the settings, so we need to load the config file
const ConfigFile cfg;
// we need to track the enforced language separately, since we need to distinguish between locale-provided
// and user-enforced one below
const QString enforcedLocale = cfg.uiLanguage();
qCDebug(lcMain) << u"Enforced language:" << enforcedLocale;
// note that user-enforced language are prioritized over the theme enforced one
// to make testing easier.
if (!enforcedLocale.isEmpty()) {
uiLanguages.prepend(enforcedLocale);
}
QString displayLanguage;
auto substLang = [](const QString &lang) {
// Map the more appropriate script codes
// to country codes as used by Qt and
// transifex translation conventions.
if (lang == QLatin1String("zh_Hans")) {
// Simplified Chinese
return QStringLiteral("zh_CN");
} else if (lang == QLatin1String("zh_Hant")) {
// Traditional Chinese
return QStringLiteral("zh_TW");
}
return lang;
};
for (QString lang : std::as_const(uiLanguages)) {
lang = substLang(lang);
const QString trFile = Translations::translationsFilePrefix() + lang;
if (QTranslator *translator = new QTranslator(app); translator->load(trFile, trPath) || lang.startsWith(QLatin1String("en"))) {
// Permissive approach: Qt and keychain translations
// may be missing, but Qt translations must be there in order
// for us to accept the language. Otherwise, we try with the next.
// "en" is an exception as it is the default language and may not
// have a translation file provided.
qCInfo(lcMain) << u"Using" << lang << u"translation" << translator->language();
displayLanguage = lang;
const QString qtTrPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
qCDebug(lcMain) << u"qtTrPath:" << qtTrPath;
const QString qtTrFile = QLatin1String("qt_") + lang;
qCDebug(lcMain) << u"qtTrFile:" << qtTrFile;
const QString qtBaseTrFile = QLatin1String("qtbase_") + lang;
qCDebug(lcMain) << u"qtBaseTrFile:" << qtBaseTrFile;
QTranslator *qtTranslator = new QTranslator(app);
QTranslator *qtkeychainTranslator = new QTranslator(app);
if (!qtTranslator->load(qtTrFile, qtTrPath)) {
if (!qtTranslator->load(qtTrFile, trPath)) {
if (!qtTranslator->load(qtBaseTrFile, qtTrPath)) {
if (!qtTranslator->load(qtBaseTrFile, trPath)) {
qCCritical(lcMain) << u"Could not load Qt translations";
}
}
}
}
const QString qtkeychainTrFile = QLatin1String("qtkeychain_") + lang;
if (!qtkeychainTranslator->load(qtkeychainTrFile, qtTrPath)) {
if (!qtkeychainTranslator->load(qtkeychainTrFile, trPath)) {
qCCritical(lcMain) << u"Could not load qtkeychain translations";
}
}
if (!translator->isEmpty() && !qApp->installTranslator(translator)) {
qCCritical(lcMain) << u"Failed to install translator";
translator->deleteLater();
}
if (!qtTranslator->isEmpty() && !qApp->installTranslator(qtTranslator)) {
qCCritical(lcMain) << u"Failed to install Qt translator";
qtTranslator->deleteLater();
}
if (!qtkeychainTranslator->isEmpty() && !qApp->installTranslator(qtkeychainTranslator)) {
qCCritical(lcMain) << u"Failed to install qtkeychain translator";
qtkeychainTranslator->deleteLater();
}
// makes sure widgets with locale-dependent formatting, e.g., QDateEdit, display the correct formatting
// if the language is provided by the system locale anyway (i.e., coming from QLocale::system().uiLanguages()), we should
// not mess with the system locale, though
// if we did, we would enforce a locale for no apparent reason
// see https://github.com/owncloud/client/issues/8608 for more information
if (enforcedLocale == lang) {
QLocale newLocale(lang);
qCDebug(lcMain) << u"language" << lang << u"was enforced, changing default locale to" << newLocale;
QLocale::setDefault(newLocale);
}
break;
} else {
translator->deleteLater();
}
}
return displayLanguage;
}
} // Anonymous namespace
int main(int argc, char **argv)
{
return RestartManager([](int argc, char **argv) {
// when called with --cmd we run the cmd client in a sub process and forward everything
if (argc > 1 && argv[1] == QByteArrayLiteral("--cmd")) {
#ifdef Q_OS_WIN
// On Windows ui applications don't have console access by default
// We can't use our normal workaround to attach to the parent console as it breaks the stdin handling.
// Therefore, we create a new console and redirect our streams.
AllocConsole();
freopen("CONIN$", "r", stdin);
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
#endif
QCoreApplication cmdApp(argc, argv);
QProcess cmd;
cmd.setProcessChannelMode(QProcess::ForwardedChannels);
cmd.setInputChannelMode(QProcess::ForwardedInputChannel);
const QString app = []() -> QString {
#ifdef Q_OS_WIN
return QCoreApplication::applicationFilePath().chopped(4) + QStringLiteral("cmd.exe");
#else
return QCoreApplication::applicationFilePath() + QStringLiteral("cmd");
#endif
}();
cmd.start(app, cmdApp.arguments().mid(2));
if (!cmd.waitForFinished(-1)) {
std::cout << "Failed to start" << qPrintable(cmd.program()) << std::endl;
}
#ifdef Q_OS_WIN
// readline to keep the console window open until closed by the user
std::string dummy;
std::cout << "Press enter to close";
std::getline(std::cin, dummy);
#endif
return cmd.exitCode();
}
// load the resources
const OCC::ResourcesLoader resource;
// Create a `Platform` instance so it can set-up/tear-down stuff for us, and do any
// initialisation that needs to be done before creating a QApplication
const auto platform = Platform::create(Platform::Type::Gui);
// Create the (Q)Application instance:
QApplication app(argc, argv);
app.setOrganizationDomain(Theme::instance()->orgDomainName());
app.setApplicationName(Theme::instance()->appName());
app.setWindowIcon(Theme::instance()->applicationIcon());
app.setApplicationVersion(Theme::instance()->versionSwitchOutput());
#ifdef Q_OS_LINUX
// HACK:
// With X11 arg0.name is used to map WM_CLASS to the desktop file.
// With Wayland it uses app.desktopFileName() or app.organizationDomain()
// As we preferably use AppImages the real deskop file name is unknown, we just ensure it mathces the StartupWMClass entry in our desktop file
if (qgetenv("XDG_SESSION_TYPE") == "wayland") {
// https://bugreports.qt.io/browse/QTBUG-77182 Qt 6.9 brings setAppId() but its private api
app.setDesktopFileName(QFileInfo(app.applicationFilePath()).baseName());
}
#endif
// Load the translations before option parsing, so we can localize help text and error messages.
const QString displayLanguage = setupTranslations(&app);
// parse the arguments before we handle singleApplication
// errors and help/version need to be handled in this instance
const auto options = parseOptions(app.arguments());
KDSingleApplication singleApplication;
if (!singleApplication.isPrimaryInstance()) {
// if the application is already running, notify it.
qCInfo(lcMain) << u"Already running, exiting...";
if (app.isSessionRestored()) {
// This call is mirrored with the one in Application::slotParseMessage
qCInfo(lcMain) << u"Session was restored, don't notify app!";
return -1;
}
QStringList args = app.arguments();
if (args.size() > 1) {
QString msg = args.join(QLatin1String("|"));
if (!singleApplication.sendMessage((msgParseOptionsC() + msg).toUtf8()))
return -1;
}
return 0;
}
// Check if the user upgraded or downgraded. We do this as early as possible, to detect
// a possible downgrade.
if (!checkClientVersion()) {
return -1;
}
setupLogging(options);
NetworkInformation::instance(); //
platform->setApplication(&app);
auto folderManager = FolderMan::createInstance();
if (!AccountManager::instance()->restore()) {
qCCritical(lcMain) << u"Could not read the account settings, quitting";
QMessageBox::critical(nullptr, QCoreApplication::translate("account loading", "Error accessing the configuration file"),
QCoreApplication::translate("account loading", "There was an error while accessing the configuration file at %1.")
.arg(ConfigFile::configFile()),
QMessageBox::Close);
return -1;
}
// Setup the folders. This includes a downgrade-detection, in which case the return value
// is empty. Note that the value 0 (zero) is a valid return value (non-empty), in which case
// the dialog is not shown.
if (!FolderMan::instance()->loadFolders().has_value()) {
// Empty return value: there was a downgrade detected on one of the databases
showDowngradeDialog();
return -1;
}
auto ocApp = Application::createInstance(displayLanguage, options.debugMode);
QObject::connect(platform.get(), &Platform::requestAttention, ocApp.get(), &Application::showSettings);
QObject::connect(&singleApplication, &KDSingleApplication::messageReceived, ocApp.get(), [&](const QByteArray &message) {
const QString msg = QString::fromUtf8(message);
qCInfo(lcMain) << Q_FUNC_INFO << msg;
if (msg.startsWith(msgParseOptionsC())) {
const QStringList optionsStrings = msg.mid(msgParseOptionsC().size()).split(QLatin1Char('|'));
CommandLineOptions options = parseOptions(optionsStrings);
if (options.show) {
ocApp->showSettings();
}
if (options.quitInstance) {
qApp->quit();
}
}
});
platform->startServices();
#ifdef WITH_AUTO_UPDATER
if (Updater::instance()) {
// validate whether the last update was successful.
Updater::instance()->validateUpdate();
}
#endif
// Enable syncing. We cannot do this earlier, because the UI needs to be set up in order to
// show sync errors. We also want to wait for the auto-updater to finish, in case it needs
// to install an update.
folderManager->setSyncEnabled(true);
if (options.show) {
ocApp->showSettings();
// The user explicitly requested the settings dialog, so don't start the new-account wizard.
}
// Display the wizard if we don't have an account yet, and no other UI is showing.
if (AccountManager::instance()->accounts().isEmpty()) {
QTimer::singleShot(0, ocApp.get(), &Application::runNewAccountWizard);
}
// Now that everything is up and running, start accepting connections/requests from the shell integration.
folderManager->socketApi()->startShellIntegration();
return app.exec();
}).exec(argc, argv);
}
@@ -0,0 +1,105 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#include "expandingheaderview.h"
#include "models.h"
#include "configfile.h"
#include <QApplication>
#include <QMenu>
using namespace OCC;
ExpandingHeaderView::ExpandingHeaderView(const QString &objectName, QWidget *parent)
: QHeaderView(Qt::Horizontal, parent)
{
setSectionsClickable(true);
setHighlightSections(true);
connect(this, &QHeaderView::sectionCountChanged, this, [this] { resizeColumns(false); });
setObjectName(objectName);
ConfigFile cfg;
if (!cfg.restoreGeometryHeader(this)) {
_requiresReset = true;
}
}
ExpandingHeaderView::~ExpandingHeaderView()
{
ConfigFile cfg;
cfg.saveGeometryHeader(this);
}
int ExpandingHeaderView::expandingColumn() const
{
return _expandingColumn;
}
void ExpandingHeaderView::setExpandingColumn(int newExpandingColumn)
{
_expandingColumn = newExpandingColumn;
}
void ExpandingHeaderView::resizeEvent(QResizeEvent *event)
{
QHeaderView::resizeEvent(event);
resizeColumns();
}
bool ExpandingHeaderView::resizeToContent() const
{
return _resizeToContent;
}
void ExpandingHeaderView::setResizeToContent(bool newResizeToContent)
{
_resizeToContent = newResizeToContent;
}
void ExpandingHeaderView::resizeColumns(bool reset)
{
int availableWidth = width();
const auto defaultSize = defaultSectionSize();
if (_requiresReset && _resizeToContent) {
// wee need some rows to adjust to the content
if (model()->rowCount() == 0) {
return;
}
reset = true;
}
if (reset) {
_requiresReset = false;
if (_resizeToContent) {
resizeSections(QHeaderView::ResizeToContents);
}
}
for (int i = 0; i < count(); ++i) {
if (i == _expandingColumn || isSectionHidden(i)) {
continue;
}
if (reset) {
resizeSection(i, _resizeToContent ? sectionSize(i) : defaultSize);
}
availableWidth -= sectionSize(i);
}
resizeSection(_expandingColumn, availableWidth);
}
void ExpandingHeaderView::addResetActionToMenu(QMenu *menu)
{
menu->addAction(tr("Reset column sizes"), this, [this] {
resizeColumns(true);
});
}
@@ -0,0 +1,46 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#pragma once
#include <QHeaderView>
namespace OCC {
class ExpandingHeaderView : public QHeaderView
{
Q_OBJECT
public:
ExpandingHeaderView(const QString &objectName, QWidget *parent = nullptr);
~ExpandingHeaderView();
int expandingColumn() const;
void setExpandingColumn(int newExpandingColumn);
void resizeColumns(bool reset = false);
void addResetActionToMenu(QMenu *menu);
bool resizeToContent() const;
void setResizeToContent(bool newResizeToContent);
protected:
void resizeEvent(QResizeEvent *event) override;
private:
bool _requiresReset = false;
bool _resizeToContent = false;
int _expandingColumn = 0;
};
}
+126
View File
@@ -0,0 +1,126 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#include "models.h"
#include <QActionGroup>
#include <QApplication>
#include <QItemSelectionRange>
#include <QMenu>
#include <QSortFilterProxyModel>
#include <QTextStream>
#include <QTimer>
#include <functional>
void OCC::Models::SignalledQSortFilterProxyModel::setFilterFixedStringSignalled(const QString &pattern)
{
setFilterFixedString(pattern);
Q_EMIT filterChanged();
}
QString OCC::Models::formatSelection(const QModelIndexList &items, int dataRole)
{
if (items.isEmpty()) {
return {};
}
const auto columns = items.first().model()->columnCount();
QString out;
QTextStream stream(&out);
QString begin;
QString end;
const auto iterate = [columns](const std::function<void(int)> &f) {
if (qApp->layoutDirection() != Qt::RightToLeft) {
for (int c = 0; c < columns; ++c) {
f(c);
}
} else {
for (int c = columns - 1; c >= 0; --c) {
f(c);
}
}
};
if (qApp->layoutDirection() == Qt::RightToLeft) {
stream << Qt::right;
begin = QLatin1Char(',');
} else {
stream << Qt::left;
end = QLatin1Char(',');
}
iterate([&](int c) {
const auto width = items.first().model()->headerData(c, Qt::Horizontal, StringFormatWidthRole).toInt();
Q_ASSERT(width);
stream << begin
<< qSetFieldWidth(width)
<< items.first().model()->headerData(c, Qt::Horizontal).toString()
<< qSetFieldWidth(0)
<< end;
});
stream << Qt::endl;
for (const auto &index : items) {
iterate([&](int c) {
const auto &child = index.siblingAtColumn(c);
stream << begin
<< qSetFieldWidth(child.model()->headerData(c, Qt::Horizontal, StringFormatWidthRole).toInt())
<< child.data(dataRole).toString()
<< qSetFieldWidth(0)
<< end;
});
stream << Qt::endl;
}
return out;
}
std::function<void()> OCC::Models::addFilterMenuItems(QMenu *menu, const QStringList &candidates, SignalledQSortFilterProxyModel *model, int column, const QString &columnName, int role)
{
menu->addAction(QApplication::translate("OCC::Models", "%1 Filter:").arg(columnName))->setEnabled(false);
auto filterGroup = new QActionGroup(menu);
filterGroup->setExclusive(true);
const auto currentFilter = model->filterRegularExpression().pattern();
auto addAction = [=](const QString &s, const QString &filter) {
auto action = menu->addAction(s, menu, [=]() {
model->setFilterRole(role);
model->setFilterKeyColumn(column);
model->setFilterFixedStringSignalled(filter);
});
action->setCheckable(true);
action->setChecked(currentFilter == QRegularExpression::escape(filter));
filterGroup->addAction(action);
return action;
};
auto noFilter = addAction(QApplication::translate("OCC::Models", "All"), QString());
for (const auto &c : candidates) {
addAction(c, c);
}
auto resetFunction = [noFilter]() {
noFilter->setChecked(true);
noFilter->trigger();
};
return resetFunction;
}
bool OCC::Models::FilteringProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
// column doesn't matter, since we do not filter based on a specific column but on a specific item role
const auto index = sourceModel()->index(sourceRow, filterKeyColumn(), sourceParent);
return sourceModel()->data(index, static_cast<int>(filterRole())).toBool();
}
+88
View File
@@ -0,0 +1,88 @@
/*
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
*
* 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.
*/
#pragma once
#include <QModelIndexList>
#include <QSortFilterProxyModel>
#include <QString>
#include <QtGlobal>
class QSortFilterProxyModel;
class QMenu;
namespace OCC {
namespace Models {
Q_NAMESPACE
enum DataRoles {
UnderlyingDataRole = Qt::UserRole + 100,
StringFormatWidthRole, // The width for a cvs formatted column
// data() should return boolean values for this role to work in conjunction with FilteringProxyModel
FilterRole,
};
Q_ENUM_NS(DataRoles)
class SignalledQSortFilterProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
using QSortFilterProxyModel::QSortFilterProxyModel;
void setFilterFixedStringSignalled(const QString &pattern);
Q_SIGNALS:
void filterChanged();
};
/**
* Returns a cvs representation of a table
*/
QString formatSelection(const QModelIndexList &items, int dataRole = Qt::DisplayRole);
std::function<void()> addFilterMenuItems(QMenu *menu, const QStringList &candidates, SignalledQSortFilterProxyModel *model, int column, const QString &columnName, int role);
/**
* Returns a vector with indices
* This is handy to iterate over the columns
*/
template <typename T>
auto range(T start, T end)
{
std::vector<T> out;
out.reserve(end - start);
for (auto i = start; i < end; ++i) {
out.push_back(i);
}
return out;
}
template <typename T>
auto range(T end)
{
return range<T>(0, end);
}
class FilteringProxyModel : public QSortFilterProxyModel
{
using QSortFilterProxyModel::QSortFilterProxyModel;
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
};
} // OCC::Models namespace
} // OCC namespace

Some files were not shown because too many files have changed in this diff Show More