diff --git a/src/common/vfs.h b/src/common/vfs.h index bb9ef619274da..ba2c5c7af06c0 100644 --- a/src/common/vfs.h +++ b/src/common/vfs.h @@ -44,6 +44,12 @@ struct OCSYNC_EXPORT VfsSetupParams */ QString filesystemPath; + // Folder display name in Windows Explorer + QString displayName; + + // Folder alias + QString alias; + /** The path to the synced folder on the account * * Always ends with /. diff --git a/src/gui/accountmanager.cpp b/src/gui/accountmanager.cpp index 7ee5ed3acc57a..165be3c97445d 100644 --- a/src/gui/accountmanager.cpp +++ b/src/gui/accountmanager.cpp @@ -380,6 +380,7 @@ void AccountManager::deleteAccount(AccountState *account) // Forget E2E keys account->account()->e2e()->forgetSensitiveData(account->account()); + emit accountSyncConnectionRemoved(account); emit accountRemoved(account); } diff --git a/src/gui/accountmanager.h b/src/gui/accountmanager.h index 356ba0e6f3aa6..9a1b50497162e 100644 --- a/src/gui/accountmanager.h +++ b/src/gui/accountmanager.h @@ -114,6 +114,7 @@ public slots: Q_SIGNALS: void accountAdded(AccountState *account); void accountRemoved(AccountState *account); + void accountSyncConnectionRemoved(AccountState *account); void removeAccountFolders(AccountState *account); }; } diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 4f8c1e1522e4e..34a036bf4c50e 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -300,6 +300,14 @@ void Folder::setSyncPaused(bool paused) emit canSyncChanged(); } +void Folder::onAssociatedAccountRemoved() +{ + if (_vfs) { + _vfs->stop(); + _vfs->unregisterFolder(); + } +} + void Folder::setSyncState(SyncResult::Status state) { _syncResult.setStatus(state); @@ -486,6 +494,8 @@ void Folder::startVfs() VfsSetupParams vfsParams; vfsParams.filesystemPath = path(); + vfsParams.displayName = shortGuiRemotePathOrAppName(); + vfsParams.alias = alias(); vfsParams.remotePath = remotePathTrailingSlash(); vfsParams.account = _accountState->account(); vfsParams.journal = &_journal; diff --git a/src/gui/folder.h b/src/gui/folder.h index 5150d3ac04f51..f5ca8c2e02a23 100644 --- a/src/gui/folder.h +++ b/src/gui/folder.h @@ -206,6 +206,8 @@ class Folder : public QObject */ virtual void wipeForRemoval(); + void onAssociatedAccountRemoved(); + void setSyncState(SyncResult::Status state); void setDirtyNetworkLimits(); diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index c61fafa0b0a20..667bfd1fdae85 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -76,6 +76,9 @@ FolderMan::FolderMan(QObject *parent) connect(AccountManager::instance(), &AccountManager::removeAccountFolders, this, &FolderMan::slotRemoveFoldersForAccount); + connect(AccountManager::instance(), &AccountManager::accountSyncConnectionRemoved, + this, &FolderMan::slotAccountRemoved); + connect(_lockWatcher.data(), &LockWatcher::fileUnlocked, this, &FolderMan::slotWatchedFileUnlocked); @@ -902,6 +905,15 @@ void FolderMan::runEtagJobIfPossible(Folder *folder) QMetaObject::invokeMethod(folder, "slotRunEtagJob", Qt::QueuedConnection); } +void FolderMan::slotAccountRemoved(AccountState *accountState) +{ + for (const auto &folder : qAsConst(_folderMap)) { + if (folder->accountState() == accountState) { + folder->onAssociatedAccountRemoved(); + } + } +} + void FolderMan::slotRemoveFoldersForAccount(AccountState *accountState) { QVarLengthArray foldersToRemove; diff --git a/src/gui/folderman.h b/src/gui/folderman.h index 1a6134fa48398..815c1288f82dd 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -264,6 +264,8 @@ private slots: void slotStartScheduledFolderSync(); void slotEtagPollTimerTimeout(); + void slotAccountRemoved(AccountState *accountState); + void slotRemoveFoldersForAccount(AccountState *accountState); // Wraps the Folder::syncStateChange() signal into the diff --git a/src/libsync/vfs/cfapi/cfapiwrapper.cpp b/src/libsync/vfs/cfapi/cfapiwrapper.cpp index 1d64ec5d56920..dacc61c4b21a0 100644 --- a/src/libsync/vfs/cfapi/cfapiwrapper.cpp +++ b/src/libsync/vfs/cfapi/cfapiwrapper.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -36,7 +37,7 @@ Q_LOGGING_CATEGORY(lcCfApiWrapper, "nextcloud.sync.vfs.cfapi.wrapper", QtInfoMsg FIELD_SIZE( CF_OPERATION_PARAMETERS, field ) ) namespace { -void cfApiSendTransferInfo(const CF_CONNECTION_KEY &connectionKey, const CF_TRANSFER_KEY &transferKey, NTSTATUS status, void *buffer, qint64 offset, qint64 length) +void cfApiSendTransferInfo(const CF_CONNECTION_KEY &connectionKey, const CF_TRANSFER_KEY &transferKey, NTSTATUS status, void *buffer, qint64 offset, qint64 currentBlockLength, qint64 totalLength) { CF_OPERATION_INFO opInfo = { 0 }; @@ -50,11 +51,24 @@ void cfApiSendTransferInfo(const CF_CONNECTION_KEY &connectionKey, const CF_TRAN opParams.TransferData.CompletionStatus = status; opParams.TransferData.Buffer = buffer; opParams.TransferData.Offset.QuadPart = offset; - opParams.TransferData.Length.QuadPart = length; + opParams.TransferData.Length.QuadPart = currentBlockLength; - const qint64 result = CfExecute(&opInfo, &opParams); - if (result != S_OK) { - qCCritical(lcCfApiWrapper) << "Couldn't send transfer info" << QString::number(transferKey.QuadPart, 16) << ":" << result << QString::fromWCharArray(_com_error(result).ErrorMessage()); + const qint64 cfExecuteresult = CfExecute(&opInfo, &opParams); + if (cfExecuteresult != S_OK) { + qCCritical(lcCfApiWrapper) << "Couldn't send transfer info" << QString::number(transferKey.QuadPart, 16) << ":" << cfExecuteresult << QString::fromWCharArray(_com_error(cfExecuteresult).ErrorMessage()); + } + + // refresh Windows Copy Dialog progress + LARGE_INTEGER progressTotal; + progressTotal.QuadPart = totalLength; + + LARGE_INTEGER progressCompleted; + progressCompleted.QuadPart = offset; + + const qint64 cfReportProgressresult = CfReportProviderProgress(connectionKey, transferKey, progressTotal, progressCompleted); + + if (cfReportProgressresult != S_OK) { + qCCritical(lcCfApiWrapper) << "Couldn't report provider progress" << QString::number(transferKey.QuadPart, 16) << ":" << cfReportProgressresult << QString::fromWCharArray(_com_error(cfReportProgressresult).ErrorMessage()); } } @@ -67,7 +81,8 @@ void CALLBACK cfApiFetchDataCallback(const CF_CALLBACK_INFO *callbackInfo, const STATUS_UNSUCCESSFUL, nullptr, callbackParameters->FetchData.RequiredFileOffset.QuadPart, - callbackParameters->FetchData.RequiredLength.QuadPart); + callbackParameters->FetchData.RequiredLength.QuadPart, + callbackInfo->FileSize.QuadPart); }; const auto sendTransferInfo = [=](QByteArray &data, qint64 offset) { @@ -77,7 +92,8 @@ void CALLBACK cfApiFetchDataCallback(const CF_CALLBACK_INFO *callbackInfo, const STATUS_SUCCESS, data.data(), offset, - data.length()); + data.length(), + callbackInfo->FileSize.QuadPart); }; auto vfs = reinterpret_cast(callbackInfo->CallbackContext); @@ -309,8 +325,141 @@ OCC::Optional OCC::CfApiWrapper::PlaceHolderInfo:: return cfPinStateToPinState(_data->PinState); } -OCC::Result OCC::CfApiWrapper::registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion) +QString convertSidToStringSid(void *sid) +{ + wchar_t *stringSid = nullptr; + if (!ConvertSidToStringSid(sid, &stringSid)) { + return {}; + } + + const auto result = QString::fromWCharArray(stringSid); + LocalFree(stringSid); + return result; +} + +std::unique_ptr getCurrentTokenInformation() +{ + const auto tokenHandle = GetCurrentThreadEffectiveToken(); + + auto tokenInfoSize = DWORD{0}; + + const auto tokenSizeCallSucceeded = ::GetTokenInformation(tokenHandle, TokenUser, nullptr, 0, &tokenInfoSize); + const auto lastError = GetLastError(); + Q_ASSERT(!tokenSizeCallSucceeded && lastError == ERROR_INSUFFICIENT_BUFFER); + if (tokenSizeCallSucceeded || lastError != ERROR_INSUFFICIENT_BUFFER) { + qCCritical(lcCfApiWrapper) << "GetTokenInformation for token size has failed with error" << lastError; + return {}; + } + + std::unique_ptr tokenInfo; + + tokenInfo.reset(reinterpret_cast(new char[tokenInfoSize])); + if (!::GetTokenInformation(tokenHandle, TokenUser, tokenInfo.get(), tokenInfoSize, &tokenInfoSize)) { + qCCritical(lcCfApiWrapper) << "GetTokenInformation failed with error" << lastError; + return {}; + } + + return tokenInfo; +} + +QString retrieveWindowsSid() +{ + if (const auto tokenInfo = getCurrentTokenInformation()) { + return convertSidToStringSid(tokenInfo->User.Sid); + } + + return {}; +} + +bool createSyncRootRegistryKeys(const QString &providerName, const QString &folderAlias, const QString &displayName, const QString &accountDisplayName, const QString &syncRootPath) { + // We must set specific Registry keys to make the progress bar refresh correctly and also add status icons into Windows Explorer + // More about this here: https://docs.microsoft.com/en-us/windows/win32/shell/integrate-cloud-storage + const auto windowsSid = retrieveWindowsSid(); + Q_ASSERT(!windowsSid.isEmpty()); + if (windowsSid.isEmpty()) { + qCWarning(lcCfApiWrapper) << "Failed to set Registry keys for shell integration, as windowsSid is empty. Progress bar will not work."; + return false; + } + + // syncRootId should be: [storage provider ID]![Windows SID]![Account ID]![FolderAlias] (FolderAlias is a custom part added here to be able to register multiple sync folders for the same account) + // folder registry keys go like: Nextcloud!S-1-5-21-2096452760-2617351404-2281157308-1001!user@nextcloud.lan:8080!0, Nextcloud!S-1-5-21-2096452760-2617351404-2281157308-1001!user@nextcloud.lan:8080!1, etc. for each sync folder + const auto syncRootId = QString("%1!%2!%3!%4").arg(providerName).arg(windowsSid).arg(accountDisplayName).arg(folderAlias); + + const QString providerSyncRootIdRegistryKey = QStringLiteral(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager\)") + syncRootId; + const QString providerSyncRootIdUserSyncRootsRegistryKey = providerSyncRootIdRegistryKey + QStringLiteral(R"(\UserSyncRoots\)"); + + struct RegistryKeyInfo { + QString subKey; + QString valueName; + int type; + QVariant value; + }; + + const QVector registryKeysToSet = { + { providerSyncRootIdRegistryKey, QStringLiteral("Flags"), REG_DWORD, 34 }, + { providerSyncRootIdRegistryKey, QStringLiteral("DisplayNameResource"), REG_EXPAND_SZ, displayName }, + { providerSyncRootIdRegistryKey, QStringLiteral("IconResource"), REG_EXPAND_SZ, QString(QDir::toNativeSeparators(qApp->applicationFilePath()) + QStringLiteral(",0")) }, + { providerSyncRootIdUserSyncRootsRegistryKey, windowsSid, REG_SZ, syncRootPath } + }; + + for (const auto ®istryKeyToSet : qAsConst(registryKeysToSet)) { + if (!OCC::Utility::registrySetKeyValue(HKEY_LOCAL_MACHINE, registryKeyToSet.subKey, registryKeyToSet.valueName, registryKeyToSet.type, registryKeyToSet.value)) { + qCWarning(lcCfApiWrapper) << "Failed to set Registry keys for shell integration. Progress bar will not work."; + const auto deleteKeyResult = OCC::Utility::registryDeleteKeyTree(HKEY_LOCAL_MACHINE, providerSyncRootIdRegistryKey); + Q_ASSERT(!deleteKeyResult); + return false; + } + } + + qCInfo(lcCfApiWrapper) << "Successfully set Registry keys for shell integration at:" << providerSyncRootIdRegistryKey << ". Progress bar will work."; + + return true; +} + +bool deleteSyncRootRegistryKey(const QString &syncRootPath, const QString &providerName, const QString &accountDisplayName) +{ + const auto syncRootManagerRegistryKey = QStringLiteral(R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager\)"); + + if (OCC::Utility::registryKeyExists(HKEY_LOCAL_MACHINE, syncRootManagerRegistryKey)) { + const auto windowsSid = retrieveWindowsSid(); + Q_ASSERT(!windowsSid.isEmpty()); + if (windowsSid.isEmpty()) { + qCWarning(lcCfApiWrapper) << "Failed to delete Registry key for shell integration on path" << syncRootPath << ". Because windowsSid is empty."; + return false; + } + + const auto currentUserSyncRootIdPattern = QString("%1!%2!%3").arg(providerName).arg(windowsSid).arg(accountDisplayName); + + bool result = true; + + // walk through each registered syncRootId + OCC::Utility::registryWalkSubKeys(HKEY_LOCAL_MACHINE, syncRootManagerRegistryKey, [&](HKEY, const QString &syncRootId) { + // make sure we have matching syncRootId(providerName!windowsSid!accountDisplayName) + if (syncRootId.startsWith(currentUserSyncRootIdPattern)) { + const QString syncRootIdUserSyncRootsRegistryKey = syncRootManagerRegistryKey + syncRootId + QStringLiteral(R"(\UserSyncRoots\)"); + // check if there is a 'windowsSid' Registry value under \UserSyncRoots and it matches the sync folder path we are removing + if (OCC::Utility::registryGetKeyValue(HKEY_LOCAL_MACHINE, syncRootIdUserSyncRootsRegistryKey, windowsSid).toString() == syncRootPath) { + const QString syncRootIdToDelete = syncRootManagerRegistryKey + syncRootId; + result = OCC::Utility::registryDeleteKeyTree(HKEY_LOCAL_MACHINE, syncRootIdToDelete); + } + } + }); + return result; + } + return true; +} + +OCC::Result OCC::CfApiWrapper::registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion, const QString &folderAlias, const QString &displayName, const QString &accountDisplayName) +{ + // even if we fail to register our sync root with shell, we can still proceed with using the VFS + const auto createRegistryKeyResult = createSyncRootRegistryKeys(providerName, folderAlias, displayName, accountDisplayName, path); + Q_ASSERT(createRegistryKeyResult); + + if (!createRegistryKeyResult) { + qCWarning(lcCfApiWrapper) << "Failed to create the registry key for path:" << path; + } + const auto p = path.toStdWString(); const auto name = providerName.toStdWString(); const auto version = providerVersion.toStdWString(); @@ -340,8 +489,15 @@ OCC::Result OCC::CfApiWrapper::registerSyncRoot(const QString &pa } } -OCC::Result OCC::CfApiWrapper::unegisterSyncRoot(const QString &path) +OCC::Result OCC::CfApiWrapper::unregisterSyncRoot(const QString &path, const QString &providerName, const QString &accountDisplayName) { + const auto deleteRegistryKeyResult = deleteSyncRootRegistryKey(path, providerName, accountDisplayName); + Q_ASSERT(deleteRegistryKeyResult); + + if (!deleteRegistryKeyResult) { + qCWarning(lcCfApiWrapper) << "Failed to delete the registry key for path:" << path; + } + const auto p = path.toStdWString(); const qint64 result = CfUnregisterSyncRoot(p.data()); Q_ASSERT(result == S_OK); @@ -365,7 +521,7 @@ OCC::Result OCC::CfApiWrapper::connec if (result != S_OK) { return QString::fromWCharArray(_com_error(result).ErrorMessage()); } else { - return key; + return { std::move(key) }; } } diff --git a/src/libsync/vfs/cfapi/cfapiwrapper.h b/src/libsync/vfs/cfapi/cfapiwrapper.h index 888242bf1959b..2e8810b45e8ea 100644 --- a/src/libsync/vfs/cfapi/cfapiwrapper.h +++ b/src/libsync/vfs/cfapi/cfapiwrapper.h @@ -71,8 +71,8 @@ class OWNCLOUDSYNC_EXPORT PlaceHolderInfo std::unique_ptr _data; }; -OWNCLOUDSYNC_EXPORT Result registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion); -OWNCLOUDSYNC_EXPORT Result unegisterSyncRoot(const QString &path); +OWNCLOUDSYNC_EXPORT Result registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion, const QString &folderAlias, const QString &displayName, const QString &accountDisplayName); +OWNCLOUDSYNC_EXPORT Result unregisterSyncRoot(const QString &path, const QString &providerName, const QString &accountDisplayName); OWNCLOUDSYNC_EXPORT Result connectSyncRoot(const QString &path, VfsCfApi *context); OWNCLOUDSYNC_EXPORT Result disconnectSyncRoot(ConnectionKey &&key); diff --git a/src/libsync/vfs/cfapi/vfs_cfapi.cpp b/src/libsync/vfs/cfapi/vfs_cfapi.cpp index 580c15b992bca..c9d05ce07e174 100644 --- a/src/libsync/vfs/cfapi/vfs_cfapi.cpp +++ b/src/libsync/vfs/cfapi/vfs_cfapi.cpp @@ -63,7 +63,7 @@ void VfsCfApi::startImpl(const VfsSetupParams ¶ms) { const auto localPath = QDir::toNativeSeparators(params.filesystemPath); - const auto registerResult = cfapi::registerSyncRoot(localPath, params.providerName, params.providerVersion); + const auto registerResult = cfapi::registerSyncRoot(localPath, params.providerName, params.providerVersion, params.alias, params.displayName, params.account->displayName()); if (!registerResult) { qCCritical(lcCfApi) << "Initialization failed, couldn't register sync root:" << registerResult.error(); return; @@ -89,7 +89,7 @@ void VfsCfApi::stop() void VfsCfApi::unregisterFolder() { const auto localPath = QDir::toNativeSeparators(params().filesystemPath); - const auto result = cfapi::unegisterSyncRoot(localPath); + const auto result = cfapi::unregisterSyncRoot(localPath, params().providerName, params().account->displayName()); if (!result) { qCCritical(lcCfApi) << "Unregistration failed for" << localPath << ":" << result.error(); } @@ -388,7 +388,6 @@ VfsCfApi::HydratationAndPinStates VfsCfApi::computeRecursiveHydrationAndPinState if (!info.exists()) { return {}; } - const auto effectivePin = pinState(folderPath); const auto pinResult = (!effectivePin && !basePinState) ? Optional() : (!effectivePin || !basePinState) ? PinState::Inherited