Add UserInfo class and fetch quota via API instead of PropfindJob

The PropfindJob quota includes the size of shares and thus leads to confusion
in regard of the real space available, as shown in the UI.
This commit aims to streamline the behaviour with the Android and iOS apps,
which also utilize the API.

Details:
- Refactor the QuotaInfo class into UserInfo
- Use JsonApiJob (ocs/v1.php/cloud/user) instead of PropfindJob
- Let ConnectionValidator use the new UserInfo class to fetch
  the user and the avatar image (to avoid code duplication)
- Allow updating the avatar image upon AccountSettings visibility,
  using UserInfo's quota fetching

Signed-off-by: Michael Schuster <michael@schuster.ms>
This commit is contained in:
Michael Schuster 2020-02-14 02:10:01 +01:00
Родитель bbb310a853
Коммит abfebcf291
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 00819E3BF4177B28
9 изменённых файлов: 229 добавлений и 184 удалений

Просмотреть файл

@ -87,7 +87,7 @@ set(client_SRCS
syncrunfilelog.cpp
systray.cpp
thumbnailjob.cpp
quotainfo.cpp
userinfo.cpp
accountstate.cpp
addcertificatedialog.cpp
authenticationdialog.cpp

Просмотреть файл

@ -26,7 +26,7 @@
#include "configfile.h"
#include "account.h"
#include "accountstate.h"
#include "quotainfo.h"
#include "userinfo.h"
#include "accountmanager.h"
#include "owncloudsetupwizard.h"
#include "creds/abstractcredentials.h"
@ -112,7 +112,7 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
, _ui(new Ui::AccountSettings)
, _wasDisabledBefore(false)
, _accountState(accountState)
, _quotaInfo(accountState)
, _userInfo(accountState, false, true)
, _menuShown(false)
{
_ui->setupUi(this);
@ -189,7 +189,7 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent)
connect(_accountState, &AccountState::stateChanged, this, &AccountSettings::slotAccountStateChanged);
slotAccountStateChanged();
connect(&_quotaInfo, &QuotaInfo::quotaUpdated,
connect(&_userInfo, &UserInfo::quotaUpdated,
this, &AccountSettings::slotUpdateQuota);
// Connect E2E stuff
@ -1173,7 +1173,7 @@ void AccountSettings::slotDeleteAccount()
bool AccountSettings::event(QEvent *e)
{
if (e->type() == QEvent::Hide || e->type() == QEvent::Show) {
_quotaInfo.setActive(isVisible());
_userInfo.setActive(isVisible());
}
if (e->type() == QEvent::Show) {
// Expand the folder automatically only if there's only one, see #4283

Просмотреть файл

@ -22,7 +22,7 @@
#include <QTimer>
#include "folder.h"
#include "quotainfo.h"
#include "userinfo.h"
#include "progressdispatcher.h"
#include "owncloudgui.h"
#include "folderstatusmodel.h"
@ -139,7 +139,7 @@ private:
QUrl _OCUrl;
bool _wasDisabledBefore;
AccountState *_accountState;
QuotaInfo _quotaInfo;
UserInfo _userInfo;
QAction *_toggleSignInOutAction;
QAction *_addAccountAction;

Просмотреть файл

@ -236,7 +236,7 @@ void AccountState::checkConnectivity()
return;
}
ConnectionValidator *conValidator = new ConnectionValidator(account());
ConnectionValidator *conValidator = new ConnectionValidator(AccountStatePtr(this));
_connectionValidator = conValidator;
connect(conValidator, &ConnectionValidator::connectionResult,
this, &AccountState::slotConnectionValidatorResult);

Просмотреть файл

@ -22,6 +22,8 @@
#include "connectionvalidator.h"
#include "account.h"
#include "accountstate.h"
#include "userinfo.h"
#include "networkjobs.h"
#include "clientproxy.h"
#include <creds/abstractcredentials.h>
@ -34,9 +36,10 @@ Q_LOGGING_CATEGORY(lcConnectionValidator, "nextcloud.sync.connectionvalidator",
// This makes sure we get tried often enough without "ConnectionValidator already running"
static qint64 timeoutToUseMsec = qMax(1000, ConnectionValidator::DefaultCallingIntervalMsec - 5 * 1000);
ConnectionValidator::ConnectionValidator(AccountPtr account, QObject *parent)
ConnectionValidator::ConnectionValidator(AccountStatePtr accountState, QObject *parent)
: QObject(parent)
, _account(account)
, _accountState(accountState)
, _account(accountState->account())
, _isCheckingServerAndAuth(false)
{
}
@ -270,10 +273,9 @@ void ConnectionValidator::ocsConfigReceived(const QJsonDocument &json, AccountPt
void ConnectionValidator::fetchUser()
{
JsonApiJob *job = new JsonApiJob(_account, QLatin1String("ocs/v1.php/cloud/user"), this);
job->setTimeout(timeoutToUseMsec);
QObject::connect(job, &JsonApiJob::jsonReceived, this, &ConnectionValidator::slotUserFetched);
job->start();
UserInfo *userInfo = new UserInfo(_accountState.data(), true, true, this);
QObject::connect(userInfo, &UserInfo::fetchedLastInfo, this, &ConnectionValidator::slotUserFetched);
userInfo->setActive(true);
}
bool ConnectionValidator::setAndCheckServerVersion(const QString &version)
@ -305,34 +307,22 @@ bool ConnectionValidator::setAndCheckServerVersion(const QString &version)
return true;
}
void ConnectionValidator::slotUserFetched(const QJsonDocument &json)
void ConnectionValidator::slotUserFetched(UserInfo *userInfo)
{
QString user = json.object().value("ocs").toObject().value("data").toObject().value("id").toString();
if (!user.isEmpty()) {
_account->setDavUser(user);
}
QString displayName = json.object().value("ocs").toObject().value("data").toObject().value("display-name").toString();
if (!displayName.isEmpty()) {
_account->setDavDisplayName(displayName);
if(userInfo) {
userInfo->setActive(false);
userInfo->deleteLater();
}
#ifndef TOKEN_AUTH_ONLY
AvatarJob *job = new AvatarJob(_account, _account->davUser(), 128, this);
job->setTimeout(20 * 1000);
QObject::connect(job, &AvatarJob::avatarPixmap, this, &ConnectionValidator::slotAvatarImage);
job->start();
connect(_account->e2e(), &ClientSideEncryption::initializationFinished, this, &ConnectionValidator::reportConnected);
_account->e2e()->initialize();
#else
reportResult(Connected);
#endif
}
#ifndef TOKEN_AUTH_ONLY
void ConnectionValidator::slotAvatarImage(const QImage &img)
{
_account->setAvatar(img);
connect(_account->e2e(), &ClientSideEncryption::initializationFinished, this, &ConnectionValidator::reportConnected);
_account->e2e()->initialize();
}
void ConnectionValidator::reportConnected() {
reportResult(Connected);
}

Просмотреть файл

@ -67,23 +67,20 @@ namespace OCC {
+---------------------------------+
|
fetchUser
PropfindJob
|
+-> slotUserFetched
AvatarJob
|
+-> slotAvatarImage -->
Utilizes the UserInfo class to fetch the user and avatar image
+-----------------------------------+
|
+-> Client Side Encryption Checks --+ --reportResult()
\endcode
*/
class UserInfo;
class ConnectionValidator : public QObject
{
Q_OBJECT
public:
explicit ConnectionValidator(AccountPtr account, QObject *parent = nullptr);
explicit ConnectionValidator(AccountStatePtr accountState, QObject *parent = nullptr);
enum Status {
Undefined,
@ -125,13 +122,12 @@ protected slots:
void slotAuthSuccess();
void slotCapabilitiesRecieved(const QJsonDocument &);
void slotUserFetched(const QJsonDocument &);
#ifndef TOKEN_AUTH_ONLY
void slotAvatarImage(const QImage &img);
#endif
void slotUserFetched(UserInfo *userInfo);
private:
#ifndef TOKEN_AUTH_ONLY
void reportConnected();
#endif
void reportResult(Status status);
void checkServerCapabilities();
void fetchUser();
@ -144,6 +140,7 @@ private:
bool setAndCheckServerVersion(const QString &version);
QStringList _errors;
AccountStatePtr _accountState;
AccountPtr _account;
bool _isCheckingServerAndAuth;
};

Просмотреть файл

@ -1,121 +0,0 @@
/*
* 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 "quotainfo.h"
#include "account.h"
#include "accountstate.h"
#include "networkjobs.h"
#include "folderman.h"
#include "creds/abstractcredentials.h"
#include <theme.h>
#include <QTimer>
namespace OCC {
namespace {
static const int defaultIntervalT = 30 * 1000;
static const int failIntervalT = 5 * 1000;
}
QuotaInfo::QuotaInfo(AccountState *accountState, QObject *parent)
: QObject(parent)
, _accountState(accountState)
, _lastQuotaTotalBytes(0)
, _lastQuotaUsedBytes(0)
, _active(false)
{
connect(accountState, &AccountState::stateChanged,
this, &QuotaInfo::slotAccountStateChanged);
connect(&_jobRestartTimer, &QTimer::timeout, this, &QuotaInfo::slotCheckQuota);
_jobRestartTimer.setSingleShot(true);
}
void QuotaInfo::setActive(bool active)
{
_active = active;
slotAccountStateChanged();
}
void QuotaInfo::slotAccountStateChanged()
{
if (canGetQuota()) {
auto elapsed = _lastQuotaRecieved.msecsTo(QDateTime::currentDateTime());
if (_lastQuotaRecieved.isNull() || elapsed >= defaultIntervalT) {
slotCheckQuota();
} else {
_jobRestartTimer.start(defaultIntervalT - elapsed);
}
} else {
_jobRestartTimer.stop();
}
}
void QuotaInfo::slotRequestFailed()
{
_lastQuotaTotalBytes = 0;
_lastQuotaUsedBytes = 0;
_jobRestartTimer.start(failIntervalT);
}
bool QuotaInfo::canGetQuota() const
{
if (!_accountState || !_active) {
return false;
}
AccountPtr account = _accountState->account();
return _accountState->isConnected()
&& account->credentials()
&& account->credentials()->ready();
}
QString QuotaInfo::quotaBaseFolder() const
{
return Theme::instance()->quotaBaseFolder();
}
void QuotaInfo::slotCheckQuota()
{
if (!canGetQuota()) {
return;
}
if (_job) {
// The previous job was not finished? Then we cancel it!
_job->deleteLater();
}
AccountPtr account = _accountState->account();
_job = new PropfindJob(account, quotaBaseFolder(), this);
_job->setProperties(QList<QByteArray>() << "quota-available-bytes"
<< "quota-used-bytes");
connect(_job.data(), &PropfindJob::result, this, &QuotaInfo::slotUpdateLastQuota);
connect(_job.data(), &AbstractNetworkJob::networkError, this, &QuotaInfo::slotRequestFailed);
_job->start();
}
void QuotaInfo::slotUpdateLastQuota(const QVariantMap &result)
{
// The server can return fractional bytes (#1374)
// <d:quota-available-bytes>1374532061.2</d:quota-available-bytes>
qint64 avail = result["quota-available-bytes"].toDouble();
_lastQuotaUsedBytes = result["quota-used-bytes"].toDouble();
// negative value of the available quota have special meaning (#3940)
_lastQuotaTotalBytes = avail >= 0 ? _lastQuotaUsedBytes + avail : avail;
emit quotaUpdated(_lastQuotaTotalBytes, _lastQuotaUsedBytes);
_jobRestartTimer.start(defaultIntervalT);
_lastQuotaRecieved = QDateTime::currentDateTime();
}
}

156
src/gui/userinfo.cpp Normal file
Просмотреть файл

@ -0,0 +1,156 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
* Copyright (C) by Michael Schuster <michael@nextcloud.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 "userinfo.h"
#include "account.h"
#include "accountstate.h"
#include "networkjobs.h"
#include "folderman.h"
#include "creds/abstractcredentials.h"
#include <theme.h>
#include <QTimer>
#include <QJsonDocument>
#include <QJsonObject>
namespace OCC {
namespace {
static const int defaultIntervalT = 30 * 1000;
static const int failIntervalT = 5 * 1000;
}
UserInfo::UserInfo(AccountState *accountState, bool allowDisconnectedAccountState, bool fetchAvatarImage, QObject *parent)
: QObject(parent)
, _accountState(accountState)
, _allowDisconnectedAccountState(allowDisconnectedAccountState)
, _fetchAvatarImage(fetchAvatarImage)
, _lastQuotaTotalBytes(0)
, _lastQuotaUsedBytes(0)
, _active(false)
{
connect(accountState, &AccountState::stateChanged,
this, &UserInfo::slotAccountStateChanged);
connect(&_jobRestartTimer, &QTimer::timeout, this, &UserInfo::slotFetchInfo);
_jobRestartTimer.setSingleShot(true);
}
void UserInfo::setActive(bool active)
{
_active = active;
slotAccountStateChanged();
}
void UserInfo::slotAccountStateChanged()
{
if (canGetInfo()) {
auto elapsed = _lastInfoReceived.msecsTo(QDateTime::currentDateTime());
if (_lastInfoReceived.isNull() || elapsed >= defaultIntervalT) {
slotFetchInfo();
} else {
_jobRestartTimer.start(defaultIntervalT - elapsed);
}
} else {
_jobRestartTimer.stop();
}
}
void UserInfo::slotRequestFailed()
{
_lastQuotaTotalBytes = 0;
_lastQuotaUsedBytes = 0;
_jobRestartTimer.start(failIntervalT);
}
bool UserInfo::canGetInfo() const
{
if (!_accountState || !_active) {
return false;
}
AccountPtr account = _accountState->account();
return (_accountState->isConnected() || _allowDisconnectedAccountState)
&& account->credentials()
&& account->credentials()->ready();
}
void UserInfo::slotFetchInfo()
{
if (!canGetInfo()) {
return;
}
if (_job) {
// The previous job was not finished? Then we cancel it!
_job->deleteLater();
}
AccountPtr account = _accountState->account();
_job = new JsonApiJob(account, QLatin1String("ocs/v1.php/cloud/user"), this);
_job->setTimeout(20 * 1000);
connect(_job.data(), &JsonApiJob::jsonReceived, this, &UserInfo::slotUpdateLastInfo);
connect(_job.data(), &AbstractNetworkJob::networkError, this, &UserInfo::slotRequestFailed);
_job->start();
}
void UserInfo::slotUpdateLastInfo(const QJsonDocument &json)
{
auto objData = json.object().value("ocs").toObject().value("data").toObject();
AccountPtr account = _accountState->account();
// User Info
QString user = objData.value("id").toString();
if (!user.isEmpty()) {
account->setDavUser(user);
}
QString displayName = objData.value("display-name").toString();
if (!displayName.isEmpty()) {
account->setDavDisplayName(displayName);
}
// Quota
auto objQuota = objData.value("quota").toObject();
qint64 used = objQuota.value("used").toDouble();
qint64 total = objQuota.value("total").toDouble();
if(_lastInfoReceived.isNull() || _lastQuotaUsedBytes != used || _lastQuotaTotalBytes != total) {
_lastQuotaUsedBytes = used;
_lastQuotaTotalBytes = total;
emit quotaUpdated(_lastQuotaTotalBytes, _lastQuotaUsedBytes);
}
_jobRestartTimer.start(defaultIntervalT);
_lastInfoReceived = QDateTime::currentDateTime();
// Avatar Image
if(_fetchAvatarImage) {
AvatarJob *job = new AvatarJob(account, account->davUser(), 128, this);
job->setTimeout(20 * 1000);
QObject::connect(job, &AvatarJob::avatarPixmap, this, &UserInfo::slotAvatarImage);
job->start();
}
else
emit fetchedLastInfo(this);
}
void UserInfo::slotAvatarImage(const QImage &img)
{
_accountState->account()->setAvatar(img);
emit fetchedLastInfo(this);
}
} // namespace OCC

Просмотреть файл

@ -1,5 +1,6 @@
/*
* Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
* Copyright (C) by Michael Schuster <michael@nextcloud.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
@ -12,8 +13,8 @@
* for more details.
*/
#ifndef QUOTAINFO_H
#define QUOTAINFO_H
#ifndef USERINFO_H
#define USERINFO_H
#include <QObject>
#include <QPointer>
@ -23,31 +24,51 @@
namespace OCC {
class AccountState;
class PropfindJob;
class JsonApiJob;
/**
* @brief handles getting the quota to display in the UI
* @brief handles getting the user info and quota to display in the UI
*
* It is typically owned by the AccountSetting page.
*
* The quota is requested if these 3 conditions are met:
* The user info and quota is requested if these 3 conditions are met:
* - This object is active via setActive() (typically if the settings page is visible.)
* - The account is connected.
* - Every 30 seconds (defaultIntervalT) or 5 seconds in case of failure (failIntervalT)
*
* We only request the quota when the UI is visible otherwise this might slow down the server with
* We only request the info when the UI is visible otherwise this might slow down the server with
* too many requests. But we still need to do it every 30 seconds otherwise user complains that the
* quota is not updated fast enough when changed on the server.
*
* If the quota job is not finished within 30 seconds, it is cancelled and another one is started
* If the fetch job is not finished within 30 seconds, it is cancelled and another one is started
*
* Constructor notes:
* - allowDisconnectedAccountState: set to true if you want to ignore AccountState's isConnected() state,
* this is used by ConnectionValidator (prior having a valid AccountState).
* - fetchAvatarImage: set to false if you don't want to fetch the avatar image
*
* @ingroup gui
*/
class QuotaInfo : public QObject
*
* Here follows the state machine
\code{.unparsed}
*---> slotFetchInfo
JsonApiJob (ocs/v1.php/cloud/user)
|
+-> slotUpdateLastInfo
AvatarJob (if _fetchAvatarImage is true)
|
+-> slotAvatarImage -->
+-----------------------------------+
|
+-> Client Side Encryption Checks --+ --reportResult()
\endcode
*/
class UserInfo : public QObject
{
Q_OBJECT
public:
explicit QuotaInfo(OCC::AccountState *accountState, QObject *parent = nullptr);
explicit UserInfo(OCC::AccountState *accountState, bool allowDisconnectedAccountState, bool fetchAvatarImage, QObject *parent = nullptr);
qint64 lastQuotaTotalBytes() const { return _lastQuotaTotalBytes; }
qint64 lastQuotaUsedBytes() const { return _lastQuotaUsedBytes; }
@ -60,32 +81,34 @@ public:
void setActive(bool active);
public Q_SLOTS:
void slotCheckQuota();
void slotFetchInfo();
private Q_SLOTS:
void slotUpdateLastQuota(const QVariantMap &);
void slotUpdateLastInfo(const QJsonDocument &json);
void slotAccountStateChanged();
void slotRequestFailed();
void slotAvatarImage(const QImage &img);
Q_SIGNALS:
void quotaUpdated(qint64 total, qint64 used);
void fetchedLastInfo(UserInfo *userInfo);
private:
bool canGetQuota() const;
/// Returns the folder that quota shall be retrieved for
QString quotaBaseFolder() const;
bool canGetInfo() const;
QPointer<AccountState> _accountState;
bool _allowDisconnectedAccountState;
bool _fetchAvatarImage;
qint64 _lastQuotaTotalBytes;
qint64 _lastQuotaUsedBytes;
QTimer _jobRestartTimer;
QDateTime _lastQuotaRecieved; // the time at which the quota was received last
QDateTime _lastInfoReceived; // the time at which the user info and quota was received last
bool _active; // if we should check at regular interval (when the UI is visible)
QPointer<PropfindJob> _job; // the currently running job
QPointer<JsonApiJob> _job; // the currently running job
};
} // namespace OCC
#endif //QUOTAINFO_H
#endif //USERINFO_H