Refresh Windows download dialog progress when hydrating a placeholder
authorallexzander <blackslayer4@gmail.com>
Tue, 9 Mar 2021 09:56:17 +0000 (11:56 +0200)
committerallexzander <blackslayer4@gmail.com>
Wed, 24 Mar 2021 12:09:06 +0000 (14:09 +0200)
Signed-off-by: allexzander <blackslayer4@gmail.com>
src/common/vfs.h
src/gui/accountmanager.cpp
src/gui/accountmanager.h
src/gui/folder.cpp
src/gui/folder.h
src/gui/folderman.cpp
src/gui/folderman.h
src/libsync/vfs/cfapi/cfapiwrapper.cpp
src/libsync/vfs/cfapi/cfapiwrapper.h
src/libsync/vfs/cfapi/vfs_cfapi.cpp

index bb9ef619274daa53220471f699c361d2ee6c574e..ba2c5c7af06c016598bec2465d090fcf129fd05a 100644 (file)
@@ -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 /.
index 7ee5ed3acc57a510df83f8fd6e2604ddda582f58..165be3c97445d2caeca08efdc8116b83408d348d 100644 (file)
@@ -380,6 +380,7 @@ void AccountManager::deleteAccount(AccountState *account)
     // Forget E2E keys
     account->account()->e2e()->forgetSensitiveData(account->account());
 
+    emit accountSyncConnectionRemoved(account);
     emit accountRemoved(account);
 }
 
index 356ba0e6f3aa665494f4a0934a2ad5a0a6134d36..9a1b50497162e96cd0c103d0e2ac7e38d85f2939 100644 (file)
@@ -114,6 +114,7 @@ public slots:
 Q_SIGNALS:
     void accountAdded(AccountState *account);
     void accountRemoved(AccountState *account);
+    void accountSyncConnectionRemoved(AccountState *account);
     void removeAccountFolders(AccountState *account);
 };
 }
index 4f8c1e1522e4efaaf6a8fae12757d852d8492855..34a036bf4c50e7da6d9be22313d4718b9b9ba87d 100644 (file)
@@ -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;
index 5150d3ac04f5182245801d1f406579124d4bc87d..f5ca8c2e02a23e9bdb4804a82195205fb4a7127c 100644 (file)
@@ -206,6 +206,8 @@ public:
       */
     virtual void wipeForRemoval();
 
+    void onAssociatedAccountRemoved();
+
     void setSyncState(SyncResult::Status state);
 
     void setDirtyNetworkLimits();
index c61fafa0b0a20af7e208bb9df1787d22c756b3f5..667bfd1fdae85e403fc5061f88cd9bb0cdf41016 100644 (file)
@@ -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<Folder *, 16> foldersToRemove;
index 1a6134fa48398cb5fbde56da6d3a1c89dbc089c6..815c1288f82dde1a30ad3b04abd49cfccbc56bf2 100644 (file)
@@ -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
index 1d64ec5d569207ea5b44ea7702bc17d1715aa9b4..dacc61c4b21a01e834e41cf5c38e2f8e881d7fa6 100644 (file)
@@ -24,6 +24,7 @@
 #include <QLocalSocket>
 #include <QLoggingCategory>
 
+#include <sddl.h>
 #include <cfapi.h>
 #include <comdef.h>
 #include <ntstatus.h>
@@ -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<OCC::VfsCfApi *>(callbackInfo->CallbackContext);
@@ -309,8 +325,141 @@ OCC::Optional<OCC::PinStateEnums::PinState> OCC::CfApiWrapper::PlaceHolderInfo::
     return cfPinStateToPinState(_data->PinState);
 }
 
-OCC::Result<void, QString> 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<TOKEN_USER> 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<TOKEN_USER> tokenInfo;
+
+    tokenInfo.reset(reinterpret_cast<TOKEN_USER*>(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<RegistryKeyInfo> 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 &registryKeyToSet : 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<void, QString> 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<void, QString> OCC::CfApiWrapper::registerSyncRoot(const QString &pa
     }
 }
 
-OCC::Result<void, QString> OCC::CfApiWrapper::unegisterSyncRoot(const QString &path)
+OCC::Result<void, QString> 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::ConnectionKey, QString> OCC::CfApiWrapper::connec
     if (result != S_OK) {
         return QString::fromWCharArray(_com_error(result).ErrorMessage());
     } else {
-        return key;
+        return { std::move(key) };
     }
 }
 
index 888242bf1959bd218e8fc26b27084ee41df4b461..2e8810b45e8ea273d6eae7a8afffc87f6f452e94 100644 (file)
@@ -71,8 +71,8 @@ private:
     std::unique_ptr<CF_PLACEHOLDER_BASIC_INFO, Deleter> _data;
 };
 
-OWNCLOUDSYNC_EXPORT Result<void, QString> registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion);
-OWNCLOUDSYNC_EXPORT Result<void, QString> unegisterSyncRoot(const QString &path);
+OWNCLOUDSYNC_EXPORT Result<void, QString> registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion, const QString &folderAlias, const QString &displayName, const QString &accountDisplayName);
+OWNCLOUDSYNC_EXPORT Result<void, QString> unregisterSyncRoot(const QString &path, const QString &providerName, const QString &accountDisplayName);
 
 OWNCLOUDSYNC_EXPORT Result<ConnectionKey, QString> connectSyncRoot(const QString &path, VfsCfApi *context);
 OWNCLOUDSYNC_EXPORT Result<void, QString> disconnectSyncRoot(ConnectionKey &&key);
index 580c15b992bca54bfef5e3fca62806e662b4a885..c9d05ce07e174f0220a6cfd1f3fdc3bc42bc0622 100644 (file)
@@ -63,7 +63,7 @@ void VfsCfApi::startImpl(const VfsSetupParams &params)
 {
     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<PinState>()
                          : (!effectivePin || !basePinState) ? PinState::Inherited