mirror of https://github.com/nextcloud/desktop
495 lines
20 KiB
Plaintext
495 lines
20 KiB
Plaintext
/*
|
|
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@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 "fileprovidersettingscontroller.h"
|
|
|
|
#include <QFileDialog>
|
|
#include <QQmlApplicationEngine>
|
|
|
|
#include "gui/systray.h"
|
|
#include "gui/userinfo.h"
|
|
#include "gui/macOS/fileprovider.h"
|
|
#include "gui/macOS/fileprovideritemmetadata.h"
|
|
#include "gui/macOS/fileprovidermaterialiseditemsmodel.h"
|
|
#include "gui/macOS/fileproviderutils.h"
|
|
|
|
// Objective-C imports
|
|
#import <Foundation/Foundation.h>
|
|
|
|
#import "fileproviderstorageuseenumerationobserver.h"
|
|
// End of Objective-C imports
|
|
|
|
namespace {
|
|
constexpr auto fpSettingsQmlPath = "qrc:/qml/src/gui/macOS/ui/FileProviderSettings.qml";
|
|
constexpr auto fpEvictionDialogQmlPath = "qrc:/qml/src/gui/macOS/ui/FileProviderEvictionDialog.qml";
|
|
|
|
// QML properties -- make sure they match up in QML file!
|
|
constexpr auto fpAccountUserIdAtHostProp = "accountUserIdAtHost";
|
|
constexpr auto fpMaterialisedItemsModelProp = "materialisedItemsModel";
|
|
|
|
// NSUserDefaults entries
|
|
constexpr auto enabledAccountsSettingsKey = "enabledAccounts";
|
|
|
|
float gbFromBytesWithOneDecimal(const unsigned long long bytes)
|
|
{
|
|
constexpr auto bytesIn100Mb = 1ULL * 1000ULL * 1000ULL * 100ULL;
|
|
return ((bytes * 1.0) / bytesIn100Mb) / 10.0;
|
|
}
|
|
} // namespace
|
|
|
|
namespace OCC {
|
|
|
|
namespace Mac {
|
|
|
|
Q_LOGGING_CATEGORY(lcFileProviderSettingsController, "nextcloud.gui.mac.fileprovider.settingscontroller")
|
|
|
|
class FileProviderSettingsController::MacImplementation : public QObject
|
|
{
|
|
public:
|
|
enum class VfsAccountsAction {
|
|
VfsAccountsNoAction,
|
|
VfsAccountsEnabledChanged,
|
|
};
|
|
|
|
explicit MacImplementation(FileProviderSettingsController *const parent)
|
|
{
|
|
q = parent;
|
|
initialCheck();
|
|
fetchMaterialisedFilesStorageUsage();
|
|
};
|
|
|
|
~MacImplementation() override = default;
|
|
|
|
[[nodiscard]] QStringList enabledAccounts() const
|
|
{
|
|
QStringList qEnabledAccounts;
|
|
NSArray<NSString *> *const enabledAccounts = nsEnabledAccounts();
|
|
for (NSString *const userIdAtHostString in enabledAccounts) {
|
|
qEnabledAccounts.append(QString::fromNSString(userIdAtHostString));
|
|
}
|
|
return qEnabledAccounts;
|
|
}
|
|
|
|
[[nodiscard]] bool vfsEnabledForAccount(const QString &userIdAtHost) const
|
|
{
|
|
NSArray<NSString *> *const vfsEnabledAccounts = nsEnabledAccounts();
|
|
return [vfsEnabledAccounts containsObject:userIdAtHost.toNSString()];
|
|
}
|
|
|
|
[[nodiscard]] VfsAccountsAction setVfsEnabledForAccount(const QString &userIdAtHost, const bool setEnabled) const
|
|
{
|
|
NSArray<NSString *> *vfsEnabledAccounts = nsEnabledAccounts();
|
|
|
|
qCInfo(lcFileProviderSettingsController) << "Setting file provider-based vfs of account"
|
|
<< userIdAtHost
|
|
<< "to"
|
|
<< setEnabled;
|
|
|
|
if (vfsEnabledAccounts == nil) {
|
|
qCDebug(lcFileProviderSettingsController) << "Received nil array for accounts, creating new array";
|
|
vfsEnabledAccounts = NSArray.array;
|
|
}
|
|
|
|
NSString *const nsUserIdAtHost = userIdAtHost.toNSString();
|
|
const BOOL accountEnabled = [vfsEnabledAccounts containsObject:nsUserIdAtHost];
|
|
|
|
if (accountEnabled == setEnabled) {
|
|
qCDebug(lcFileProviderSettingsController) << "VFS enablement status for"
|
|
<< userIdAtHost
|
|
<< "matches config.";
|
|
return VfsAccountsAction::VfsAccountsNoAction;
|
|
}
|
|
|
|
NSMutableArray<NSString *> *const mutableVfsAccounts = vfsEnabledAccounts.mutableCopy;
|
|
|
|
if (setEnabled) {
|
|
[mutableVfsAccounts addObject:nsUserIdAtHost];
|
|
} else {
|
|
[mutableVfsAccounts removeObject:nsUserIdAtHost];
|
|
}
|
|
|
|
NSArray<NSString *> *const modifiedVfsAccounts = mutableVfsAccounts.copy;
|
|
[_userDefaults setObject:modifiedVfsAccounts forKey:_accountsKey];
|
|
|
|
Q_ASSERT(vfsEnabledForAccount(userIdAtHost) == userIdAtHost);
|
|
|
|
return VfsAccountsAction::VfsAccountsEnabledChanged;
|
|
}
|
|
|
|
[[nodiscard]] VfsAccountsAction enableVfsForAllAccounts() const
|
|
{
|
|
const auto accManager = AccountManager::instance();
|
|
const auto accountsList = accManager->accounts();
|
|
|
|
if (accountsList.count() == 0) {
|
|
return VfsAccountsAction::VfsAccountsNoAction;
|
|
}
|
|
|
|
auto overallActResult = VfsAccountsAction::VfsAccountsNoAction;
|
|
|
|
for (const auto &account : accountsList) {
|
|
const auto qAccountUserIdAtHost = account->account()->userIdAtHostWithPort();
|
|
const auto accountActResult = setVfsEnabledForAccount(qAccountUserIdAtHost, true);
|
|
|
|
if (accountActResult == VfsAccountsAction::VfsAccountsEnabledChanged) {
|
|
overallActResult = accountActResult;
|
|
}
|
|
}
|
|
|
|
return overallActResult;
|
|
}
|
|
|
|
[[nodiscard]] unsigned long long localStorageUsageForAccount(const QString &userIdAtHost) const
|
|
{
|
|
// Return cached value as we fetch asynchronously on initialisation of this class.
|
|
// We will then emit a signal when the new value is found.
|
|
return _storageUsage.value(userIdAtHost);
|
|
}
|
|
|
|
[[nodiscard]] QVector<FileProviderItemMetadata> materialisedItemsForAccount(const QString &userIdAtHost) const
|
|
{
|
|
return _materialisedFiles.value(userIdAtHost);
|
|
}
|
|
|
|
void signalFileProviderDomain(const QString &userIdAtHost) const
|
|
{
|
|
qCInfo(lcFileProviderSettingsController) << "Signalling file provider domain" << userIdAtHost;
|
|
NSFileProviderDomain * const domain = FileProviderUtils::domainForIdentifier(userIdAtHost);
|
|
NSFileProviderManager * const manager = [NSFileProviderManager managerForDomain:domain];
|
|
[manager signalEnumeratorForContainerItemIdentifier:NSFileProviderRootContainerItemIdentifier
|
|
completionHandler:^(NSError *const error) {
|
|
if (error != nil) {
|
|
qCWarning(lcFileProviderSettingsController) << "Could not signal file provider domain, error"
|
|
<< error.localizedDescription;
|
|
return;
|
|
}
|
|
|
|
qCInfo(lcFileProviderSettingsController) << "Successfully signalled file provider domain";
|
|
// TODO: Provide some feedback in the UI
|
|
}];
|
|
}
|
|
|
|
[[nodiscard]] FileProviderDomainSyncStatus *domainSyncStatusForAccount(const QString &userIdAtHost) const
|
|
{
|
|
return _fileProviderDomainSyncStatuses.value(userIdAtHost);
|
|
}
|
|
|
|
private slots:
|
|
void updateDomainSyncStatuses()
|
|
{
|
|
qCInfo(lcFileProviderSettingsController) << "Updating domain sync statuses";
|
|
_fileProviderDomainSyncStatuses.clear();
|
|
const auto enabledAccounts = nsEnabledAccounts();
|
|
for (NSString *const domainIdentifier in enabledAccounts) {
|
|
const auto qDomainIdentifier = QString::fromNSString(domainIdentifier);
|
|
const auto syncStatus = new FileProviderDomainSyncStatus(qDomainIdentifier, q);
|
|
_fileProviderDomainSyncStatuses.insert(qDomainIdentifier, syncStatus);
|
|
}
|
|
}
|
|
|
|
private:
|
|
[[nodiscard]] NSArray<NSString *> *nsEnabledAccounts() const
|
|
{
|
|
return (NSArray<NSString *> *)[_userDefaults objectForKey:_accountsKey];
|
|
}
|
|
|
|
void fetchMaterialisedFilesStorageUsage()
|
|
{
|
|
qCInfo(lcFileProviderSettingsController) << "Fetching materialised files storage usage";
|
|
|
|
[NSFileProviderManager getDomainsWithCompletionHandler: ^(NSArray<NSFileProviderDomain *> *const domains, NSError *const error) {
|
|
if (error != nil) {
|
|
qCWarning(lcFileProviderSettingsController) << "Could not get file provider domains:"
|
|
<< error.localizedDescription
|
|
<< "Will try again in 2 secs";
|
|
|
|
// HACK: Sometimes the system is not in a state where it wants to give us access to
|
|
// the file provider domains. We will try again in 2 seconds and hope it works
|
|
const auto thisQobject = (QObject*)this;
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[NSTimer scheduledTimerWithTimeInterval:2 repeats:NO block:^(NSTimer *const timer) {
|
|
Q_UNUSED(timer)
|
|
QMetaObject::invokeMethod(thisQobject, [this] { fetchMaterialisedFilesStorageUsage(); });
|
|
}];
|
|
});
|
|
return;
|
|
}
|
|
|
|
for (NSFileProviderDomain *const domain in domains) {
|
|
qCInfo(lcFileProviderSettingsController) << "Checking storage use for domain:" << domain.identifier;
|
|
|
|
NSFileProviderManager *const managerForDomain = [NSFileProviderManager managerForDomain:domain];
|
|
if (managerForDomain == nil) {
|
|
qCWarning(lcFileProviderSettingsController) << "Got a nil file provider manager for domain"
|
|
<< domain.identifier
|
|
<< ", returning early.";
|
|
return;
|
|
}
|
|
|
|
const id<NSFileProviderEnumerator> enumerator = [managerForDomain enumeratorForMaterializedItems];
|
|
Q_ASSERT(enumerator != nil);
|
|
[enumerator retain];
|
|
|
|
FileProviderStorageUseEnumerationObserver *const storageUseObserver = [[FileProviderStorageUseEnumerationObserver alloc] init];
|
|
[storageUseObserver retain];
|
|
storageUseObserver.enumerationFinishedHandler = ^(NSError *const error) {
|
|
qCInfo(lcFileProviderSettingsController) << "Enumeration finished for" << domain.identifier;
|
|
if (error != nil) {
|
|
qCWarning(lcFileProviderSettingsController) << "Error while enumerating storage use" << error.localizedDescription;
|
|
[storageUseObserver release];
|
|
[enumerator release];
|
|
return;
|
|
}
|
|
|
|
const auto items = storageUseObserver.materialisedItems;
|
|
Q_ASSERT(items != nil);
|
|
|
|
// Remember that OCC::Account::userIdAtHost == domain.identifier for us
|
|
const auto qDomainIdentifier = QString::fromNSString(domain.identifier);
|
|
QVector<FileProviderItemMetadata> qMaterialisedItems;
|
|
qMaterialisedItems.reserve(items.count);
|
|
for (const id<NSFileProviderItem> item in items) {
|
|
const auto itemMetadata = FileProviderItemMetadata::fromNSFileProviderItem(item, qDomainIdentifier);
|
|
const auto storageUsage = _storageUsage.value(qDomainIdentifier) + itemMetadata.documentSize();
|
|
qCDebug(lcFileProviderSettingsController) << "Adding item" << itemMetadata.identifier()
|
|
<< "with size" << itemMetadata.documentSize()
|
|
<< "to storage usage for account" << qDomainIdentifier
|
|
<< "with total size" << storageUsage;
|
|
qMaterialisedItems.append(itemMetadata);
|
|
_storageUsage.insert(qDomainIdentifier, storageUsage);
|
|
}
|
|
_materialisedFiles.insert(qDomainIdentifier, qMaterialisedItems);
|
|
|
|
emit q->localStorageUsageForAccountChanged(qDomainIdentifier);
|
|
emit q->materialisedItemsForAccountChanged(qDomainIdentifier);
|
|
|
|
[storageUseObserver release];
|
|
[enumerator release];
|
|
};
|
|
[enumerator enumerateItemsForObserver:storageUseObserver startingAtPage:NSFileProviderInitialPageSortedByName];
|
|
}
|
|
}];
|
|
}
|
|
|
|
void initialCheck()
|
|
{
|
|
qCInfo(lcFileProviderSettingsController) << "Running initial checks for file provider settings controller.";
|
|
|
|
NSArray<NSString *> *const vfsEnabledAccounts = nsEnabledAccounts();
|
|
if (vfsEnabledAccounts != nil) {
|
|
updateDomainSyncStatuses();
|
|
connect(q, &FileProviderSettingsController::vfsEnabledAccountsChanged, this, &MacImplementation::updateDomainSyncStatuses);
|
|
return;
|
|
}
|
|
|
|
qCInfo(lcFileProviderSettingsController) << "Initial check for file provider settings found nil enabled vfs accounts array."
|
|
<< "Enabling all accounts on initial setup.";
|
|
|
|
[[maybe_unused]] const auto result = enableVfsForAllAccounts();
|
|
}
|
|
|
|
FileProviderSettingsController *q = nullptr;
|
|
NSUserDefaults *_userDefaults = NSUserDefaults.standardUserDefaults;
|
|
NSString *_accountsKey = [NSString stringWithUTF8String:enabledAccountsSettingsKey];
|
|
QHash<QString, QVector<FileProviderItemMetadata>> _materialisedFiles;
|
|
QHash<QString, unsigned long long> _storageUsage;
|
|
QHash<QString, FileProviderDomainSyncStatus*> _fileProviderDomainSyncStatuses;
|
|
};
|
|
|
|
FileProviderSettingsController *FileProviderSettingsController::instance()
|
|
{
|
|
static FileProviderSettingsController controller;
|
|
return &controller;
|
|
}
|
|
|
|
FileProviderSettingsController::FileProviderSettingsController(QObject *parent)
|
|
: QObject{parent}
|
|
, d(new FileProviderSettingsController::MacImplementation(this))
|
|
{
|
|
const auto accManager = AccountManager::instance();
|
|
const auto accountsList = accManager->accounts();
|
|
|
|
for (const auto &accountState : accountsList) {
|
|
const auto userInfo = new UserInfo(accountState.data(), false, false, this);
|
|
const auto account = accountState->account();
|
|
const auto accountUserIdAtHost = account->userIdAtHostWithPort();
|
|
|
|
_userInfos.insert(accountUserIdAtHost, userInfo);
|
|
connect(userInfo, &UserInfo::fetchedLastInfo, this, [this, accountUserIdAtHost] {
|
|
emit remoteStorageUsageForAccountChanged(accountUserIdAtHost);
|
|
});
|
|
userInfo->setActive(true);
|
|
}
|
|
}
|
|
|
|
QQuickWidget *FileProviderSettingsController::settingsViewWidget(const QString &accountUserIdAtHost,
|
|
QWidget *const parent,
|
|
const QQuickWidget::ResizeMode resizeMode)
|
|
{
|
|
const auto settingsViewWidget = new QQuickWidget(Systray::instance()->trayEngine(), parent);
|
|
settingsViewWidget->setResizeMode(resizeMode);
|
|
settingsViewWidget->setSource(QUrl(fpSettingsQmlPath));
|
|
settingsViewWidget->rootObject()->setProperty(fpAccountUserIdAtHostProp, accountUserIdAtHost);
|
|
return settingsViewWidget;
|
|
}
|
|
|
|
QStringList FileProviderSettingsController::vfsEnabledAccounts() const
|
|
{
|
|
return d->enabledAccounts();
|
|
}
|
|
|
|
bool FileProviderSettingsController::vfsEnabledForAccount(const QString &userIdAtHost) const
|
|
{
|
|
return d->vfsEnabledForAccount(userIdAtHost);
|
|
}
|
|
|
|
void FileProviderSettingsController::setVfsEnabledForAccount(const QString &userIdAtHost, const bool setEnabled)
|
|
{
|
|
const auto enabledAccountsAction = d->setVfsEnabledForAccount(userIdAtHost, setEnabled);
|
|
if (enabledAccountsAction == MacImplementation::VfsAccountsAction::VfsAccountsEnabledChanged) {
|
|
emit vfsEnabledAccountsChanged();
|
|
}
|
|
}
|
|
|
|
bool FileProviderSettingsController::fastEnumerationSetForAccount(const QString &userIdAtHost) const
|
|
{
|
|
const auto xpc = FileProvider::instance()->xpc();
|
|
if (!xpc) {
|
|
return false;
|
|
}
|
|
if (const auto state = xpc->fastEnumerationStateForExtension(userIdAtHost)) {
|
|
return state->second;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FileProviderSettingsController::fastEnumerationEnabledForAccount(const QString &userIdAtHost) const
|
|
{
|
|
const auto xpc = FileProvider::instance()->xpc();
|
|
if (!xpc) {
|
|
return true;
|
|
}
|
|
if (const auto fastEnumerationState = xpc->fastEnumerationStateForExtension(userIdAtHost)) {
|
|
return fastEnumerationState->first;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void FileProviderSettingsController::setFastEnumerationEnabledForAccount(const QString &userIdAtHost, const bool setEnabled)
|
|
{
|
|
const auto xpc = FileProvider::instance()->xpc();
|
|
if (!xpc) {
|
|
// Reset state of UI elements
|
|
emit fastEnumerationEnabledForAccountChanged(userIdAtHost);
|
|
emit fastEnumerationSetForAccountChanged(userIdAtHost);
|
|
return;
|
|
}
|
|
xpc->setFastEnumerationEnabledForExtension(userIdAtHost, setEnabled);
|
|
emit fastEnumerationEnabledForAccountChanged(userIdAtHost);
|
|
emit fastEnumerationSetForAccountChanged(userIdAtHost);
|
|
}
|
|
|
|
unsigned long long FileProviderSettingsController::localStorageUsageForAccount(const QString &userIdAtHost) const
|
|
{
|
|
return d->localStorageUsageForAccount(userIdAtHost);
|
|
}
|
|
|
|
float FileProviderSettingsController::localStorageUsageGbForAccount(const QString &userIdAtHost) const
|
|
{
|
|
return gbFromBytesWithOneDecimal(localStorageUsageForAccount(userIdAtHost));
|
|
}
|
|
|
|
unsigned long long FileProviderSettingsController::remoteStorageUsageForAccount(const QString &userIdAtHost) const
|
|
{
|
|
const auto userInfoForAccount = _userInfos.value(userIdAtHost);
|
|
if (!userInfoForAccount) {
|
|
return 0;
|
|
}
|
|
|
|
return userInfoForAccount->lastQuotaUsedBytes();
|
|
}
|
|
|
|
float FileProviderSettingsController::remoteStorageUsageGbForAccount(const QString &userIdAtHost) const
|
|
{
|
|
return gbFromBytesWithOneDecimal(remoteStorageUsageForAccount(userIdAtHost));
|
|
}
|
|
|
|
QAbstractListModel *FileProviderSettingsController::materialisedItemsModelForAccount(const QString &userIdAtHost)
|
|
{
|
|
const auto items = d->materialisedItemsForAccount(userIdAtHost);
|
|
if (items.isEmpty()) {
|
|
return nullptr;
|
|
}
|
|
|
|
const auto model = new FileProviderMaterialisedItemsModel(this);
|
|
model->setItems(items);
|
|
|
|
connect(this, &FileProviderSettingsController::materialisedItemsForAccountChanged, model, [this, model, userIdAtHost](const QString &accountUserIdAtHost) {
|
|
if (accountUserIdAtHost != userIdAtHost) {
|
|
return;
|
|
}
|
|
|
|
const auto items = d->materialisedItemsForAccount(userIdAtHost);
|
|
model->setItems(items);
|
|
});
|
|
|
|
return model;
|
|
}
|
|
|
|
void FileProviderSettingsController::createEvictionWindowForAccount(const QString &userIdAtHost)
|
|
{
|
|
const auto engine = Systray::instance()->trayEngine();
|
|
QQmlComponent component(engine, QUrl(fpEvictionDialogQmlPath));
|
|
const auto model = materialisedItemsModelForAccount(userIdAtHost);
|
|
const auto genericDialog = component.createWithInitialProperties({
|
|
{fpAccountUserIdAtHostProp, userIdAtHost},
|
|
{fpMaterialisedItemsModelProp, QVariant::fromValue(model)},
|
|
});
|
|
const auto dialog = qobject_cast<QQuickWindow *>(genericDialog);
|
|
Q_ASSERT(dialog);
|
|
dialog->show();
|
|
}
|
|
|
|
void FileProviderSettingsController::signalFileProviderDomain(const QString &userIdAtHost)
|
|
{
|
|
d->signalFileProviderDomain(userIdAtHost);
|
|
}
|
|
|
|
void FileProviderSettingsController::createDebugArchive(const QString &userIdAtHost)
|
|
{
|
|
const auto filename = QFileDialog::getSaveFileName(nullptr,
|
|
tr("Create Debug Archive"),
|
|
QStandardPaths::writableLocation(QStandardPaths::StandardLocation::DocumentsLocation),
|
|
tr("Text files") + " (*.txt)");
|
|
if (filename.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
const auto xpc = FileProvider::instance()->xpc();
|
|
if (!xpc) {
|
|
qCWarning(lcFileProviderSettingsController) << "Could not create debug archive, FileProviderXPC is not available.";
|
|
return;
|
|
}
|
|
xpc->createDebugArchiveForExtension(userIdAtHost, filename);
|
|
}
|
|
|
|
FileProviderDomainSyncStatus *FileProviderSettingsController::domainSyncStatusForAccount(const QString &userIdAtHost) const
|
|
{
|
|
return d->domainSyncStatusForAccount(userIdAtHost);
|
|
}
|
|
|
|
} // namespace Mac
|
|
|
|
} // namespace OCC
|