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
+1
View File
@@ -0,0 +1 @@
add_subdirectory(vfs)
+32
View File
@@ -0,0 +1,32 @@
# Globbing for plugins has a problem with in-source builds
# that create directories for the build.
include(OCAddVfsPlugin)
foreach(vfsPlugin ${VIRTUAL_FILE_SYSTEM_PLUGINS})
set(vfsPluginPath ${vfsPlugin})
set(vfsPluginPathOut ${vfsPlugin})
get_filename_component(vfsPluginName ${vfsPlugin} NAME)
if (NOT IS_ABSOLUTE ${vfsPlugin})
set(vfsPluginPath "${CMAKE_CURRENT_LIST_DIR}/${vfsPlugin}")
else()
set(vfsPluginPathOut ${PROJECT_BINARY_DIR}/plugins/${vfsPluginName})
endif()
if(NOT IS_DIRECTORY ${vfsPluginPath})
continue()
endif()
add_subdirectory(${vfsPluginPath} ${vfsPluginPathOut})
if (TARGET vfs_${vfsPluginName})
if(BUILD_TESTING AND IS_DIRECTORY "${vfsPluginPath}/test")
add_subdirectory("${vfsPluginPath}/test" "${vfsPluginName}_test")
message(STATUS "Added VFSPlugin with tests: ${vfsPluginName}")
else()
message(STATUS "Added VFSPlugin without tests: ${vfsPluginName}")
endif()
else()
message(STATUS "VFSPlugin ${vfsPluginName} was requested but not added to build")
endif()
endforeach()
@@ -0,0 +1,12 @@
if (WIN32)
add_vfs_plugin(NAME cfapi
SRC
cfapiwrapper.cpp
hydrationdevice.cpp
vfs_cfapi.cpp
LIBS
cldapi
WindowsApp
)
endif()
@@ -0,0 +1,662 @@
/*
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "cfapiwrapper.h"
#include "common/filesystembase.h"
#include "common/utility_win.h"
#include "filesystem.h"
#include "hydrationdevice.h"
#include "libsync/account.h"
#include "syncengine.h"
#include "theme.h"
#include "vfs_cfapi.h"
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QLocalSocket>
#include <QLoggingCategory>
#include <QUuid>
#include <comdef.h>
#include <ntstatus.h>
#include <sddl.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Security.Cryptography.h>
#include <winrt/windows.Storage.Streams.h>
#include <winrt/windows.storage.provider.h>
Q_LOGGING_CATEGORY(lcCfApiWrapper, "sync.vfs.cfapi.wrapper", QtDebugMsg)
using namespace Qt::Literals::StringLiterals;
using namespace std::chrono_literals;
namespace winrt {
using namespace Windows::Foundation;
using namespace Windows::Storage;
using namespace Windows::Storage::Streams;
using namespace Windows::Storage::Provider;
using namespace Windows::Foundation::Collections;
using namespace Windows::Security::Cryptography;
}
template <>
QString OCC::Utility::enumToDisplayName(CF_CALLBACK_DEHYDRATION_REASON reason)
{
switch (reason) {
OCC_UTIL_ENUM_DISPLAY_NAME(CF_CALLBACK_DEHYDRATION_REASON_NONE)
OCC_UTIL_ENUM_DISPLAY_NAME(CF_CALLBACK_DEHYDRATION_REASON_USER_MANUAL)
OCC_UTIL_ENUM_DISPLAY_NAME(CF_CALLBACK_DEHYDRATION_REASON_SYSTEM_LOW_SPACE)
OCC_UTIL_ENUM_DISPLAY_NAME(CF_CALLBACK_DEHYDRATION_REASON_SYSTEM_INACTIVITY)
OCC_UTIL_ENUM_DISPLAY_NAME(CF_CALLBACK_DEHYDRATION_REASON_SYSTEM_OS_UPGRADE)
}
Q_UNREACHABLE();
}
namespace {
std::mutex sRegister_mutex;
constexpr auto forbiddenLeadingCharacterInPath = '#'_L1;
QString createErrorMessageForPlaceholderUpdateAndCreate(const QString &path, const QString &originalErrorMessage)
{
const auto pathFromNativeSeparators = QDir::fromNativeSeparators(path);
if (!pathFromNativeSeparators.contains(QStringLiteral("/%1").arg(forbiddenLeadingCharacterInPath))) {
return originalErrorMessage;
}
const auto fileComponents = pathFromNativeSeparators.split('/'_L1);
for (const auto &fileComponent : fileComponents) {
if (fileComponent.startsWith(forbiddenLeadingCharacterInPath)) {
qCInfo(lcCfApiWrapper) << u"Failed to create/update a placeholder for path \"" << pathFromNativeSeparators << u"\" that has a leading '#'.";
return {u"%1: %2"_s.arg(originalErrorMessage, QObject::tr("Paths beginning with '#' character are not supported in VFS mode."))};
}
}
return originalErrorMessage;
}
// retreive the pllaceholder info, by default we don't request the full FileIdentity
OCC::Result<std::vector<char>, int64_t> getPlaceholderInfo(
const OCC::Utility::Handle &handle, CF_PLACEHOLDER_INFO_CLASS infoClass = CF_PLACEHOLDER_INFO_BASIC, bool withFileIdentity = false)
{
std::vector<char> buffer(
withFileIdentity ? 512 : (infoClass == CF_PLACEHOLDER_INFO_BASIC ? sizeof(CF_PLACEHOLDER_BASIC_INFO) : sizeof(CF_PLACEHOLDER_STANDARD_INFO)));
DWORD actualSize = {};
const int64_t result = CfGetPlaceholderInfo(handle.handle(), infoClass, buffer.data(), static_cast<DWORD>(buffer.size()), &actualSize);
if (result == S_OK || (!withFileIdentity && result == HRESULT_FROM_WIN32(ERROR_MORE_DATA))) {
if (withFileIdentity) {
buffer.resize(actualSize);
}
return std::move(buffer);
} else if (result == HRESULT_FROM_WIN32(ERROR_NOT_A_CLOUD_FILE)) {
// native file, not yet converted
return std::vector<char>{};
} else {
qCWarning(lcCfApiWrapper) << u"Failed to retrieve placeholder info:" << OCC::Utility::formatWinError(result);
Q_ASSERT(false);
}
return {result};
}
OCC::CfApiWrapper::CallBackContext logCallback(const char *function, const CF_CALLBACK_INFO *callbackInfo, QMap<QByteArray, QVariant> &&extraArgs = {})
{
OCC::CfApiWrapper::CallBackContext context{.vfs = reinterpret_cast<OCC::VfsCfApi *>(callbackInfo->CallbackContext),
.path = QString(QString::fromWCharArray(callbackInfo->VolumeDosName) + QString::fromWCharArray(callbackInfo->NormalizedPath)),
.transferKey = callbackInfo->TransferKey.QuadPart,
.connectionKey = callbackInfo->ConnectionKey,
.fileId = QByteArray(reinterpret_cast<const char *>(callbackInfo->FileIdentity), callbackInfo->FileIdentityLength),
.extraArgs = std::move(extraArgs)};
Q_ASSERT(context.vfs->metaObject()->className() == QByteArrayLiteral("OCC::VfsCfApi"));
QString app = u"NULL"_s;
QString appId;
uint32_t pid{};
if (callbackInfo->ProcessInfo) {
app = QString::fromWCharArray(callbackInfo->ProcessInfo->ImagePath);
appId = QString::fromWCharArray(callbackInfo->ProcessInfo->ApplicationId);
pid = callbackInfo->ProcessInfo->ProcessId;
}
qCInfo(lcCfApiWrapper) << function << context << u"requested by application:" << app << u"PID:" << pid << u"AppID:" << appId;
return context;
}
void CALLBACK cfApiFetchDataCallback(const CF_CALLBACK_INFO *callbackInfo, const CF_CALLBACK_PARAMETERS *parameters)
{
const auto context = logCallback(Q_FUNC_INFO, callbackInfo, {{"reason", OCC::Utility::enumToDisplayName(parameters->FetchData.LastDehydrationReason)}});
if (callbackInfo->ProcessInfo && QCoreApplication::applicationPid() == callbackInfo->ProcessInfo->ProcessId) {
qCCritical(lcCfApiWrapper) << u"implicit hydration triggered by the client itself. Will lead to a deadlock. Cancel" << context.path
<< context.transferKey;
Q_ASSERT(false);
return;
};
// switch to main thread
QTimer::singleShot(0, context.vfs, [context, fileSize = callbackInfo->FileSize.QuadPart] {
Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread());
if (auto *hydration = OCC::CfApiWrapper::HydrationDevice::requestHydration(context, fileSize, context.vfs)) {
hydration->start();
}
});
}
OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> updatePlaceholderState(
const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath)
{
OCC::CfApiWrapper::PlaceHolderInfo<CF_PLACEHOLDER_BASIC_INFO> info;
if (!replacesPath.isEmpty()) {
info = OCC::CfApiWrapper::findPlaceholderInfo<CF_PLACEHOLDER_BASIC_INFO>(replacesPath);
}
if (!info) {
info = OCC::CfApiWrapper::findPlaceholderInfo<CF_PLACEHOLDER_BASIC_INFO>(path);
}
if (!info) {
Q_ASSERT(false);
return {u"Can't update non existing placeholder info"_s};
}
const auto previousPinState = info.pinState();
CF_FS_METADATA metadata = {};
metadata.FileSize.QuadPart = size;
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &metadata.BasicInfo.CreationTime);
metadata.BasicInfo.LastWriteTime = metadata.BasicInfo.CreationTime;
metadata.BasicInfo.LastAccessTime = metadata.BasicInfo.CreationTime;
metadata.BasicInfo.ChangeTime = metadata.BasicInfo.CreationTime;
qCInfo(lcCfApiWrapper) << u"updatePlaceholderState" << path << modtime << fileId;
const qint64 result = CfUpdatePlaceholder(OCC::Utility::Handle::createHandle(OCC::FileSystem::toFilesystemPath(path)), &metadata, fileId.data(),
static_cast<DWORD>(fileId.size()), nullptr, 0, CF_UPDATE_FLAG_MARK_IN_SYNC, nullptr, nullptr);
if (result != S_OK) {
const QString errorMessage = createErrorMessageForPlaceholderUpdateAndCreate(path, u"Couldn't update placeholder info"_s);
qCWarning(lcCfApiWrapper) << errorMessage << path << u":" << OCC::Utility::formatWinError(result) << replacesPath;
return errorMessage;
}
// Pin state tends to be lost on updates, so restore it every time
if (!setPinState(path, previousPinState, OCC::CfApiWrapper::NoRecurse)) {
return {u"Couldn't restore pin state"_s};
}
return OCC::Vfs::ConvertToPlaceholderResult::Ok;
}
}
void CALLBACK cfApiCancelFetchData(const CF_CALLBACK_INFO *callbackInfo, const CF_CALLBACK_PARAMETERS *)
{
const auto context = logCallback(Q_FUNC_INFO, callbackInfo);
qInfo(lcCfApiWrapper) << u"Cancel fetch data of" << context;
QMetaObject::invokeMethod(context.vfs, [context] { context.vfs->cancelHydration(context); }, Qt::QueuedConnection);
}
void CALLBACK cfApiNotifyFileOpenCompletion(const CF_CALLBACK_INFO *callbackInfo, const CF_CALLBACK_PARAMETERS * /*callbackParameters*/)
{
logCallback(Q_FUNC_INFO, callbackInfo);
}
void CALLBACK cfApiValidateData(const CF_CALLBACK_INFO *callbackInfo, const CF_CALLBACK_PARAMETERS * /*callbackParameters*/)
{
logCallback(Q_FUNC_INFO, callbackInfo);
}
void CALLBACK cfApiCancelFetchPlaceHolders(const CF_CALLBACK_INFO *callbackInfo, const CF_CALLBACK_PARAMETERS * /*callbackParameters*/)
{
logCallback(Q_FUNC_INFO, callbackInfo);
}
void CALLBACK cfApiRename(const CF_CALLBACK_INFO *callbackInfo, const CF_CALLBACK_PARAMETERS *callbackParameters)
{
const QString target = QString::fromWCharArray(callbackInfo->VolumeDosName) + QString::fromWCharArray(callbackParameters->Rename.TargetPath);
// isExcluded can't handle windows paths, as the target does not exist yet, we can't use QFileInfo::canonicalFilePath
const auto qtPath = QString(target).replace('\\'_L1, '/'_L1);
const auto context = logCallback(Q_FUNC_INFO, callbackInfo, {{"flags", QVariant::fromValue(QFlags(callbackParameters->Rename.Flags))}, {"target", target}});
bool reject = false;
// if both are managed by us, we prevent the move of a placeholder to a folder where its ignored, as it would implicitly cause a deletion
if (callbackParameters->Rename.Flags & (CF_CALLBACK_RENAME_FLAG_TARGET_IN_SCOPE | CF_CALLBACK_RENAME_FLAG_SOURCE_IN_SCOPE) &&
// CF_CALLBACK_RENAME_FLAG_TARGET_IN_SCOPE is also set for any other sync root we manage
// but in that case windows will hydrate the file before its moved
OCC::FileSystem::isChildPathOf(target, context.vfs->params().filesystemPath()) && context.vfs->isDehydratedPlaceholder(context.path)
&& context.vfs->params().syncEngine()->isExcluded(qtPath)) {
reject = true;
}
if (reject) {
qCInfo(lcCfApiWrapper) << u"Rejecting rename of" << context;
} else {
qCInfo(lcCfApiWrapper) << u"Accepting rename of" << context;
}
CF_OPERATION_INFO opInfo = {};
opInfo.StructSize = sizeof(opInfo);
opInfo.ConnectionKey = callbackInfo->ConnectionKey;
opInfo.TransferKey = callbackInfo->TransferKey;
opInfo.Type = CF_OPERATION_TYPE_ACK_RENAME;
CF_OPERATION_PARAMETERS params = {};
params.ParamSize = CF_SIZE_OF_OP_PARAM(AckRename);
params.AckRename.CompletionStatus = reject ? STATUS_CLOUD_FILE_NOT_SUPPORTED : STATUS_SUCCESS;
if (auto result = CfExecute(&opInfo, &params); FAILED(result)) {
qCWarning(lcCfApiWrapper) << u"CfExecute failed:" << OCC::Utility::formatWinError(result);
}
}
void CALLBACK cfApiRenameCompletion(const CF_CALLBACK_INFO *callbackInfo, const CF_CALLBACK_PARAMETERS * /*callbackParameters*/)
{
logCallback(Q_FUNC_INFO, callbackInfo);
}
void CALLBACK cfApiNotifyFileCloseCompletion(const CF_CALLBACK_INFO *callbackInfo, const CF_CALLBACK_PARAMETERS * /*callbackParameters*/)
{
logCallback(Q_FUNC_INFO, callbackInfo);
}
CF_PIN_STATE pinStateToCfPinState(OCC::PinState state)
{
switch (state) {
case OCC::PinState::Inherited:
return CF_PIN_STATE_INHERIT;
case OCC::PinState::AlwaysLocal:
return CF_PIN_STATE_PINNED;
case OCC::PinState::OnlineOnly:
return CF_PIN_STATE_UNPINNED;
case OCC::PinState::Unspecified:
return CF_PIN_STATE_UNSPECIFIED;
case OCC::PinState::Excluded:
return CF_PIN_STATE_EXCLUDED;
}
Q_UNREACHABLE();
}
CF_SET_PIN_FLAGS pinRecurseModeToCfSetPinFlags(OCC::CfApiWrapper::SetPinRecurseMode mode)
{
switch (mode) {
case OCC::CfApiWrapper::NoRecurse:
return CF_SET_PIN_FLAG_NONE;
case OCC::CfApiWrapper::Recurse:
return CF_SET_PIN_FLAG_RECURSE;
case OCC::CfApiWrapper::ChildrenOnly:
return CF_SET_PIN_FLAG_RECURSE_ONLY;
}
Q_UNREACHABLE();
}
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) << u"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) << u"GetTokenInformation failed with error" << lastError;
return {};
}
return tokenInfo;
}
QString retrieveWindowsSid()
{
if (const auto tokenInfo = getCurrentTokenInformation()) {
return convertSidToStringSid(tokenInfo->User.Sid);
}
return {};
}
QString createSyncRootID(const QString &providerName, const QUuid &accountUUID, 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) << u"Failed to set Registry keys for shell integration, as windowsSid is empty. Progress bar will not work.";
return {};
}
// syncRootId should be: [storage provider ID]![Windows SID]![Account ID]![PathHash]
// multiple sync folders for the same account) folder registry keys go like:
// QSfera!S-1-5-21-2096452760-2617351404-2281157308-1001!AccountUUID!hash
const auto stableFolderHash =
QString::fromUtf8(QCryptographicHash::hash(QFileInfo(syncRootPath).canonicalFilePath().toUtf8(), QCryptographicHash::Sha256).toHex());
return u"%1!%2!%3!%4"_s.arg(providerName, windowsSid, accountUUID.toString(QUuid::WithoutBraces), stableFolderHash);
}
void OCC::CfApiWrapper::registerSyncRoot(const VfsSetupParams &params, const std::function<void(QString)> &callback)
{
const auto nativePath = QDir::toNativeSeparators(params.filesystemPath());
winrt::StorageFolder::GetFolderFromPathAsync(reinterpret_cast<const wchar_t *>(nativePath.utf16()))
.Completed([params, callback](const winrt::IAsyncOperation<winrt::StorageFolder> &result, winrt::AsyncStatus status) {
if (status != winrt::AsyncStatus::Completed) {
callback(u"Failed to retrieve folder info: %1"_s.arg(Utility::formatWinError(result.ErrorCode())));
return;
}
try {
const auto iconPath = QCoreApplication::applicationFilePath();
const auto id = createSyncRootID(params.providerName, params.account->uuid(), params.filesystemPath());
const auto version = params.providerVersion.toString();
winrt::StorageProviderSyncRootInfo info;
info.Id(reinterpret_cast<const wchar_t *>(id.utf16()));
info.Path(result.GetResults());
// Displayed by Windows in Cloud Files errors and in the trash bin.
const QString displayname = u"%1: %2"_s.arg(params.providerName, params.folderDisplayName());
info.DisplayNameResource(reinterpret_cast<const wchar_t *>(displayname.utf16()));
info.IconResource(reinterpret_cast<const wchar_t *>(iconPath.utf16()));
info.HydrationPolicy(winrt::StorageProviderHydrationPolicy::Full);
info.HydrationPolicyModifier(winrt::StorageProviderHydrationPolicyModifier::AutoDehydrationAllowed);
info.PopulationPolicy(winrt::StorageProviderPopulationPolicy::AlwaysFull);
info.InSyncPolicy(winrt::StorageProviderInSyncPolicy::PreserveInsyncForSyncEngine);
info.HardlinkPolicy(winrt::StorageProviderHardlinkPolicy::None);
info.Version(reinterpret_cast<const wchar_t *>(version.utf16()));
info.AllowPinning(true);
info.ShowSiblingsAsGroup(true);
winrt::Uri uri(reinterpret_cast<const wchar_t *>(
u"%1/files/trash/project?fileId=%2"_s.arg(params.account->url().toString(QUrl::FullyEncoded), params.spaceId()).utf16()));
info.RecycleBinUri(uri);
winrt::Streams::DataWriter streamWriter;
streamWriter.WriteString(params.account->uuid().toString().toStdWString());
info.Context(streamWriter.DetachBuffer());
{
// don't confuse Windows with parallel registrations
std::lock_guard lock(sRegister_mutex);
winrt::StorageProviderSyncRootManager::Register(info);
}
callback({});
} catch (const winrt::hresult_error &ex) {
callback(u"Failed to register sync root %1: %2"_s.arg(params.filesystemPath(), Utility::formatWinError(ex.code())));
}
});
}
#if 0
void unregisterSyncRootShellExtensions(const QString &providerName, const QString &folderAlias, const QString &accountDisplayName)
{
const auto windowsSid = retrieveWindowsSid();
Q_ASSERT(!windowsSid.isEmpty());
if (windowsSid.isEmpty()) {
qCWarning(lcCfApiWrapper) << u"Failed to unregister SyncRoot Shell Extensions!";
return;
}
const auto syncRootId = QStringLiteral("%1!%2!%3!%4").arg(providerName).arg(windowsSid).arg(accountDisplayName).arg(folderAlias);
const QString providerSyncRootIdRegistryKey = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId;
OCC::Utility::registryDeleteKeyValue(HKEY_LOCAL_MACHINE, providerSyncRootIdRegistryKey, QStringLiteral("ThumbnailProvider"));
OCC::Utility::registryDeleteKeyValue(HKEY_LOCAL_MACHINE, providerSyncRootIdRegistryKey, QStringLiteral("CustomStateHandler"));
qCInfo(lcCfApiWrapper) << u"Successfully unregistered SyncRoot Shell Extensions!";
}
#endif
OCC::Result<void, QString> OCC::CfApiWrapper::unregisterSyncRoot(const VfsSetupParams &params)
{
try {
std::lock_guard lock(sRegister_mutex);
winrt::StorageProviderSyncRootManager::Unregister(
reinterpret_cast<const wchar_t *>(createSyncRootID(params.providerName, params.account->uuid(), params.filesystemPath()).utf16()));
} catch (winrt::hresult_error const &ex) {
return u"unregisterSyncRoot failed: %1"_s.arg(Utility::formatWinError(ex.code()));
}
return {};
}
OCC::Result<CF_CONNECTION_KEY, QString> OCC::CfApiWrapper::connectSyncRoot(const QString &path, OCC::VfsCfApi *context)
{
std::lock_guard lock(sRegister_mutex);
CF_CONNECTION_KEY key;
const auto p = QDir::toNativeSeparators(path).toStdWString();
CF_CALLBACK_REGISTRATION cfApiCallbacks[] = {{CF_CALLBACK_TYPE_FETCH_DATA, cfApiFetchDataCallback}, //
{CF_CALLBACK_TYPE_CANCEL_FETCH_DATA, cfApiCancelFetchData}, //
{CF_CALLBACK_TYPE_NOTIFY_FILE_OPEN_COMPLETION, cfApiNotifyFileOpenCompletion}, //
{CF_CALLBACK_TYPE_NOTIFY_FILE_CLOSE_COMPLETION, cfApiNotifyFileCloseCompletion}, //,
{CF_CALLBACK_TYPE_VALIDATE_DATA, cfApiValidateData}, //
{CF_CALLBACK_TYPE_CANCEL_FETCH_PLACEHOLDERS, cfApiCancelFetchPlaceHolders}, //
{CF_CALLBACK_TYPE_NOTIFY_RENAME, cfApiRename}, //
{CF_CALLBACK_TYPE_NOTIFY_RENAME_COMPLETION, cfApiRenameCompletion}, //
CF_CALLBACK_REGISTRATION_END};
const qint64 result =
CfConnectSyncRoot(p.data(), cfApiCallbacks, context, CF_CONNECT_FLAG_REQUIRE_PROCESS_INFO | CF_CONNECT_FLAG_REQUIRE_FULL_FILE_PATH | CF_CONNECT_FLAG_BLOCK_SELF_IMPLICIT_HYDRATION, &key);
Q_ASSERT(result == S_OK);
if (result != S_OK) {
return OCC::Utility::formatWinError(result);
} else {
return {std::move(key)};
}
}
OCC::Result<void, QString> OCC::CfApiWrapper::disconnectSyncRoot(CF_CONNECTION_KEY &&key)
{
std::lock_guard lock(sRegister_mutex);
const qint64 result = CfDisconnectSyncRoot(key);
if (result != S_OK) {
qCWarning(lcCfApiWrapper) << u"Disconnecting sync root failed" << OCC::Utility::formatWinError(result);
Q_ASSERT(result == S_OK);
return OCC::Utility::formatWinError(result);
} else {
return {};
}
}
bool OCC::CfApiWrapper::isDehydratedPlaceholder(const FileSystem::Path &path)
{
const auto handle = OCC::Utility::Handle::createHandle(path);
if (!handle) {
qCWarning(lcCfApiWrapper) << u"Failed to get file handle" << path << handle.errorMessage();
return false;
}
FILE_ATTRIBUTE_TAG_INFO targInfo = {};
if (!GetFileInformationByHandleEx(handle, FileAttributeTagInfo, &targInfo, sizeof(targInfo))) {
const auto error = GetLastError();
qCWarning(lcCfApiWrapper) << u"Failed to get file attribute tag info for" << path << OCC::Utility::formatWinError(error);
return false;
}
const CF_PLACEHOLDER_STATE state = CfGetPlaceholderStateFromAttributeTag(targInfo.FileAttributes, targInfo.ReparseTag);
if (state == CF_PLACEHOLDER_STATE_NO_STATES) {
return false;
}
return state & CF_PLACEHOLDER_STATE_PARTIAL;
}
template <>
OCC::CfApiWrapper::PlaceHolderInfo<CF_PLACEHOLDER_BASIC_INFO> OCC::CfApiWrapper::findPlaceholderInfo(const QString &path, bool withFileIdentity)
{
if (auto handle = OCC::Utility::Handle::createHandle(OCC::FileSystem::toFilesystemPath(path))) {
auto info = getPlaceholderInfo(handle, CF_PLACEHOLDER_INFO_BASIC, withFileIdentity);
if (!info || info->empty()) {
return {std::move(handle), {}};
}
return PlaceHolderInfo<CF_PLACEHOLDER_BASIC_INFO>(std::move(handle), std::move(*info));
}
return {};
}
template <>
OCC::CfApiWrapper::PlaceHolderInfo<CF_PLACEHOLDER_STANDARD_INFO> OCC::CfApiWrapper::findPlaceholderInfo(const QString &path, bool withFileIdentity)
{
if (auto handle = OCC::Utility::Handle::createHandle(OCC::FileSystem::toFilesystemPath(path))) {
auto info = getPlaceholderInfo(handle, CF_PLACEHOLDER_INFO_STANDARD, withFileIdentity);
if (!info || info->empty()) {
return {std::move(handle), {}};
}
return PlaceHolderInfo<CF_PLACEHOLDER_STANDARD_INFO>(std::move(handle), std::move(*info));
}
return {};
}
OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::setPinState(const QString &path, OCC::PinState state, SetPinRecurseMode mode)
{
const auto cfState = pinStateToCfPinState(state);
const auto flags = pinRecurseModeToCfSetPinFlags(mode);
const qint64 result = CfSetPinState(OCC::Utility::Handle::createHandle(OCC::FileSystem::toFilesystemPath(path)), cfState, flags, nullptr);
if (result == S_OK) {
return OCC::Vfs::ConvertToPlaceholderResult::Ok;
} else {
qCWarning(lcCfApiWrapper) << u"Couldn't set pin state" << state << u"for" << path << u"with recurse mode" << mode << u":"
<< OCC::Utility::formatWinError(result);
return {u"Couldn't set pin state"_s};
}
}
OCC::Result<void, QString> OCC::CfApiWrapper::createPlaceholderInfo(const QString &path, time_t modtime, qint64 size, const QByteArray &fileId)
{
const auto fileInfo = QFileInfo(path);
const auto localBasePath = QDir::toNativeSeparators(fileInfo.path()).toStdWString();
const auto relativePath = fileInfo.fileName().toStdWString();
CF_PLACEHOLDER_CREATE_INFO cloudEntry = {};
cloudEntry.FileIdentity = fileId.data();
cloudEntry.FileIdentityLength = static_cast<DWORD>(fileId.length());
cloudEntry.RelativeFileName = relativePath.data();
cloudEntry.Flags = CF_PLACEHOLDER_CREATE_FLAG_MARK_IN_SYNC;
cloudEntry.FsMetadata.FileSize.QuadPart = size;
cloudEntry.FsMetadata.BasicInfo.FileAttributes = FILE_ATTRIBUTE_NORMAL;
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &cloudEntry.FsMetadata.BasicInfo.CreationTime);
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &cloudEntry.FsMetadata.BasicInfo.LastWriteTime);
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &cloudEntry.FsMetadata.BasicInfo.LastAccessTime);
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &cloudEntry.FsMetadata.BasicInfo.ChangeTime);
if (fileInfo.isDir()) {
cloudEntry.Flags |= CF_PLACEHOLDER_CREATE_FLAG_DISABLE_ON_DEMAND_POPULATION;
cloudEntry.FsMetadata.BasicInfo.FileAttributes = FILE_ATTRIBUTE_DIRECTORY;
cloudEntry.FsMetadata.FileSize.QuadPart = 0;
}
qCDebug(lcCfApiWrapper) << u"CfCreatePlaceholders" << path << modtime;
const qint64 result = CfCreatePlaceholders(localBasePath.data(), &cloudEntry, 1, CF_CREATE_FLAG_NONE, nullptr);
if (result != S_OK) {
qCWarning(lcCfApiWrapper) << u"Couldn't create placeholder info for" << path << u":" << Utility::formatWinError(result);
return {u"Couldn't create placeholder info"_s};
}
const auto parentInfo = findPlaceholderInfo<CF_PLACEHOLDER_BASIC_INFO>(QDir::toNativeSeparators(QFileInfo(path).absolutePath()));
const auto state = parentInfo && parentInfo.pinState() == PinState::OnlineOnly ? PinState::OnlineOnly : PinState::Inherited;
if (!setPinState(path, state, NoRecurse)) {
return {u"Couldn't set the default inherit pin state"_s};
}
return {};
}
OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::updatePlaceholderInfo(
const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath)
{
return updatePlaceholderState(path, modtime, size, fileId, replacesPath);
}
OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::dehydratePlaceholder(const QString &path, const QByteArray &fileId)
{
const auto info = findPlaceholderInfo<CF_PLACEHOLDER_BASIC_INFO>(path);
if (info) {
const qint64 result = CfUpdatePlaceholder(Utility::Handle::createHandle(OCC::FileSystem::toFilesystemPath(path)), nullptr, fileId.data(),
static_cast<DWORD>(fileId.size()), nullptr, 0, CF_UPDATE_FLAG_MARK_IN_SYNC | CF_UPDATE_FLAG_DEHYDRATE, nullptr, nullptr);
if (result != S_OK) {
const auto errorMessage = createErrorMessageForPlaceholderUpdateAndCreate(path, u"Couldn't update placeholder info"_s);
qCWarning(lcCfApiWrapper) << errorMessage << path << u":" << OCC::Utility::formatWinError(result);
return errorMessage;
}
} else {
const qint64 result = CfConvertToPlaceholder(Utility::Handle::createHandle(OCC::FileSystem::toFilesystemPath(path)), fileId.data(),
static_cast<DWORD>(fileId.size()), CF_CONVERT_FLAG_MARK_IN_SYNC | CF_CONVERT_FLAG_DEHYDRATE, nullptr, nullptr);
if (result != S_OK) {
const auto errorMessage = createErrorMessageForPlaceholderUpdateAndCreate(path, u"Couldn't convert to placeholder"_s);
qCWarning(lcCfApiWrapper) << errorMessage << path << u":" << OCC::Utility::formatWinError(result);
return errorMessage;
}
setPinState(path, OCC::PinState::OnlineOnly, OCC::CfApiWrapper::NoRecurse);
}
return OCC::Vfs::ConvertToPlaceholderResult::Ok;
}
OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::convertToPlaceholder(
const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath)
{
const qint64 result = CfConvertToPlaceholder(Utility::Handle::createHandle(OCC::FileSystem::toFilesystemPath(path)), fileId.data(),
static_cast<DWORD>(fileId.size()), CF_CONVERT_FLAG_MARK_IN_SYNC, nullptr, nullptr);
Q_ASSERT(result == S_OK);
if (result != S_OK) {
const auto errorMessage = createErrorMessageForPlaceholderUpdateAndCreate(path, u"Couldn't convert to placeholder"_s);
qCWarning(lcCfApiWrapper) << errorMessage << path << u":" << OCC::Utility::formatWinError(result);
return errorMessage;
}
return updatePlaceholderState(path, modtime, size, fileId, replacesPath);
}
OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::updatePlaceholderMarkInSync(const Utility::Handle &handle)
{
if (auto result = CfSetInSyncState(handle, CF_IN_SYNC_STATE_IN_SYNC, CF_SET_IN_SYNC_FLAG_NONE, nullptr); SUCCEEDED(result)) {
return OCC::Vfs::ConvertToPlaceholderResult::Ok;
} else {
if (result == HRESULT_FROM_WIN32(ERROR_NOT_A_CLOUD_FILE)) {
return u"»%1« is not a cloud file"_s.arg(FileSystem::fromFilesystemPath(handle.path()));
}
const auto error = u"updatePlaceholderMarkInSync failed: %1"_s.arg(Utility::formatWinError(result));
qCWarning(lcCfApiWrapper) << error;
return error;
}
}
bool OCC::CfApiWrapper::isPlaceHolderInSync(const QString &filePath)
{
if (const auto originalInfo = findPlaceholderInfo<CF_PLACEHOLDER_BASIC_INFO>(filePath)) {
return originalInfo->InSyncState == CF_IN_SYNC_STATE_IN_SYNC;
}
return true;
}
QDebug operator<<(QDebug debug, const OCC::CfApiWrapper::CallBackContext &context)
{
QDebugStateSaver saver(debug);
debug.setAutoInsertSpaces(false);
debug << u"cfapiCallback(" << context.path << u", " << context.requestHexId();
for (const auto &[k, v] : context.extraArgs.asKeyValueRange()) {
debug << u", ";
debug.noquote() << k << u"=";
debug.quote() << v;
};
debug << u")";
return debug.maybeSpace();
}
@@ -0,0 +1,133 @@
/*
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
// include order is important, this must be included before cfapi
#include <windows.h>
#include <winternl.h>
#include <cfapi.h>
#include "common/pinstate.h"
#include "common/result.h"
#include "common/utility_win.h"
#include "libsync/vfs/vfs.h"
struct CF_PLACEHOLDER_BASIC_INFO;
// see cloud mirror example
#define FIELD_SIZE(type, field) (sizeof(((type *)0)->field))
#define CF_SIZE_OF_OP_PARAM(field) (FIELD_OFFSET(CF_OPERATION_PARAMETERS, field) + FIELD_SIZE(CF_OPERATION_PARAMETERS, field))
namespace OCC {
class VfsCfApi;
namespace CfApiWrapper {
struct CallBackContext
{
OCC::VfsCfApi *vfs;
QString path;
int64_t transferKey;
CF_CONNECTION_KEY connectionKey;
QByteArray fileId;
QMap<QByteArray, QVariant> extraArgs;
inline QString requestHexId() const { return QString::number(transferKey, 16); }
};
template <typename T>
class PlaceHolderInfo
{
public:
PlaceHolderInfo(Utility::Handle &&handle = {}, const std::vector<char> &&buffer = {})
: _handle(std::move(handle))
, _data(std::move(buffer))
{
}
inline auto *get() const noexcept { return reinterpret_cast<T *>(const_cast<char *>(_data.data())); }
inline auto *operator->() const noexcept { return get(); }
inline explicit operator bool() const noexcept { return isValid(); }
inline bool isValid() const noexcept { return !_data.empty(); };
inline auto size() const { return _data.size(); }
inline QByteArray fileId() const
{
if (get()->FileIdentityLength == 0) {
return {};
}
// the file id increases the struct size, ensure we fetched the whole dynamic struct
Q_ASSERT(size() > (sizeof(T) + 20));
return QByteArray(reinterpret_cast<char *>(get()->FileIdentity), get()->FileIdentityLength);
}
PinState pinState() const
{
Q_ASSERT(this);
switch (get()->PinState) {
case CF_PIN_STATE_UNSPECIFIED:
return OCC::PinState::Unspecified;
case CF_PIN_STATE_PINNED:
return OCC::PinState::AlwaysLocal;
case CF_PIN_STATE_UNPINNED:
return OCC::PinState::OnlineOnly;
case CF_PIN_STATE_INHERIT:
return OCC::PinState::Inherited;
case CF_PIN_STATE_EXCLUDED:
return OCC::PinState::Excluded;
}
Q_UNREACHABLE();
}
const Utility::Handle &handle() const { return _handle; }
private:
Utility::Handle _handle;
std::vector<char> _data;
};
void registerSyncRoot(const VfsSetupParams &params, const std::function<void(QString)> &callback);
// void unregisterSyncRootShellExtensions(const QString &providerName, const QString &folderAlias, const QString &accountDisplayName);
Result<void, QString> unregisterSyncRoot(const VfsSetupParams &params);
Result<CF_CONNECTION_KEY, QString> connectSyncRoot(const QString &path, VfsCfApi *context);
Result<void, QString> disconnectSyncRoot(CF_CONNECTION_KEY &&key);
bool isDehydratedPlaceholder(const FileSystem::Path &path);
/**
* The placeholder info can have a dynamic size, by default we don't query FileIdentity
* If FileIdentity is required withFileIdentity must be set to true.
*/
template <typename T>
PlaceHolderInfo<T> findPlaceholderInfo(const QString &path, bool withFileIdentity = false)
{
}
template <>
PlaceHolderInfo<CF_PLACEHOLDER_BASIC_INFO> findPlaceholderInfo(const QString &path, bool withFileIdentity);
template <>
PlaceHolderInfo<CF_PLACEHOLDER_STANDARD_INFO> findPlaceholderInfo(const QString &path, bool withFileIdentity);
enum SetPinRecurseMode { NoRecurse = 0, Recurse, ChildrenOnly };
Result<OCC::Vfs::ConvertToPlaceholderResult, QString> setPinState(const QString &path, PinState state, SetPinRecurseMode mode);
Result<void, QString> createPlaceholderInfo(const QString &path, time_t modtime, qint64 size, const QByteArray &fileId);
Result<OCC::Vfs::ConvertToPlaceholderResult, QString> updatePlaceholderInfo(
const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath = QString());
Result<OCC::Vfs::ConvertToPlaceholderResult, QString> convertToPlaceholder(
const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath);
Result<OCC::Vfs::ConvertToPlaceholderResult, QString> dehydratePlaceholder(const QString &path, const QByteArray &fileId);
Result<OCC::Vfs::ConvertToPlaceholderResult, QString> updatePlaceholderMarkInSync(const Utility::Handle &handle);
bool isPlaceHolderInSync(const QString &filePath);
}
} // namespace OCC
QDebug operator<<(QDebug debug, const OCC::CfApiWrapper::CallBackContext &context);
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#include "hydrationdevice.h"
#include "vfs_cfapi.h"
#include "libsync/common/filesystembase.h"
#include "libsync/filesystem.h"
#include <ntstatus.h>
using namespace OCC;
using namespace OCC::FileSystem::SizeLiterals;
Q_LOGGING_CATEGORY(lcCfApiHydrationDevice, "sync.vfs.cfapi.hydrationdevice", QtDebugMsg)
namespace {
constexpr auto ChunkSize = 4_KiB;
constexpr auto BufferSize = 4_MiB;
}
CfApiHydrationJob *CfApiWrapper::HydrationDevice::requestHydration(const CfApiWrapper::CallBackContext &context, qint64 totalSize, QObject *parent)
{
qCInfo(lcCfApiHydrationDevice) << u"Requesting hydration" << context;
if (context.vfs->_hydrationJobs.contains(context.transferKey)) {
qCWarning(lcCfApiHydrationDevice) << u"Ignoring hydration request for running hydration" << context;
return {};
}
// we assume that transferKey is unique and that we don't receive multiple requests for the same file with the same key
Q_ASSERT(std::find_if(context.vfs->_hydrationJobs.cbegin(), context.vfs->_hydrationJobs.cend(), [&context](const auto &it) {
return it->context().path == context.path;
}) == context.vfs->_hydrationJobs.cend());
auto *hydration =
new CfApiHydrationJob(context.vfs, context.fileId, std::make_unique<OCC::CfApiWrapper::HydrationDevice>(context, totalSize, parent), parent);
hydration->setContext(context);
context.vfs->_hydrationJobs.insert(context.transferKey, hydration);
connect(hydration, &HydrationJob::finished, context.vfs, [hydration] {
hydration->deleteLater();
hydration->context().vfs->_hydrationJobs.remove(hydration->context().transferKey);
if (QFileInfo::exists(hydration->context().path)) {
auto item = OCC::SyncFileItem::fromSyncJournalFileRecord(hydration->record());
// the file is now downloaded
item->_type = ItemTypeFile;
if (auto inode = FileSystem::getInode(FileSystem::toFilesystemPath(hydration->context().path))) {
item->_inode = inode.value();
} else {
qCWarning(lcCfApiHydrationDevice) << u"Failed to get inode for" << hydration->context().path;
}
const auto result = hydration->context().vfs->params().journal->setFileRecord(SyncJournalFileRecord::fromSyncFileItem(*item));
if (!result) {
qCWarning(lcCfApiHydrationDevice) << u"Error when setting the file record to the database" << hydration->context() << result.error();
} else {
qCInfo(lcCfApiHydrationDevice) << u"Hydration succeeded" << hydration->context();
}
} else {
qCWarning(lcCfApiHydrationDevice) << u"Hydration succeeded but the file appears to be moved" << hydration->context();
}
});
connect(hydration, &HydrationJob::error, context.vfs, [hydration](const QString &error) {
hydration->deleteLater();
hydration->context().vfs->_hydrationJobs.remove(hydration->context().transferKey);
qCWarning(lcCfApiHydrationDevice) << u"Hydration failed" << hydration->context() << error;
});
return hydration;
}
CfApiWrapper::HydrationDevice::HydrationDevice(const CfApiWrapper::CallBackContext &context, qint64 totalSize, QObject *parent)
: QIODevice(parent)
, _context(context)
, _totalSize(totalSize)
{
// we reserve a fixed size and don't expect to ever grow the array
_buffer.reserve(BufferSize);
}
qint64 CfApiWrapper::HydrationDevice::readData(char *, qint64)
{
Q_UNREACHABLE();
}
qint64 CfApiWrapper::HydrationDevice::writeData(const char *data, qint64 len)
{
_buffer.append(data, len);
const bool isLastChunk = (_offset + _buffer.size()) >= _totalSize;
// only write if the buffer is decently filled, or we are in the last chunk
if (_buffer.size() >= BufferSize * 0.9 || isLastChunk) {
// the buffer should not grow above BufferSize
Q_ASSERT(_buffer.size() <= BufferSize);
// ensure we chunk the writes to the block size, if we are at then end of the file take all the rest
const auto currentBlockLength = isLastChunk ? _buffer.size() : // we are in the last chunk, use everything
_buffer.size() - _buffer.size() % ChunkSize; // align the current block with ChunkSize
CF_OPERATION_INFO opInfo = {};
opInfo.StructSize = sizeof(opInfo);
opInfo.Type = CF_OPERATION_TYPE_TRANSFER_DATA;
opInfo.ConnectionKey = _context.connectionKey;
opInfo.TransferKey.QuadPart = _context.transferKey;
CF_OPERATION_PARAMETERS opParams = {};
opParams.ParamSize = CF_SIZE_OF_OP_PARAM(TransferData);
opParams.TransferData.CompletionStatus = STATUS_SUCCESS;
opParams.TransferData.Buffer = _buffer.data();
opParams.TransferData.Offset.QuadPart = _offset;
opParams.TransferData.Length.QuadPart = currentBlockLength;
const qint64 cfExecuteResult = CfExecute(&opInfo, &opParams);
if (cfExecuteResult != S_OK) {
qCCritical(lcCfApiHydrationDevice) << u"Couldn't send transfer info" << _context << u":" << cfExecuteResult
<< Utility::formatWinError(cfExecuteResult);
setErrorString(Utility::formatWinError(cfExecuteResult));
return -1;
}
const auto trailing = _buffer.size() - currentBlockLength;
Q_ASSERT(trailing >= 0);
if (trailing) {
// move the memory to the front
std::memcpy(_buffer.data(), _buffer.data() + currentBlockLength, trailing);
}
// this won't reduce the allocated size
_buffer.resize(trailing);
_offset += currentBlockLength;
// refresh Windows Copy Dialog progress
const LARGE_INTEGER progressTotal = {.QuadPart = _totalSize};
const LARGE_INTEGER progressCompleted = {.QuadPart = _offset};
const qint64 cfReportProgressResult =
CfReportProviderProgress(_context.connectionKey, {.QuadPart = _context.transferKey}, progressTotal, progressCompleted);
if (cfReportProgressResult != S_OK) {
qCCritical(lcCfApiHydrationDevice) << u"Couldn't report provider progress" << _context << u":" << cfReportProgressResult
<< Utility::formatWinError(cfReportProgressResult);
}
}
return len;
}
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
#pragma once
#include "cfapiwrapper.h"
#include <QFile>
namespace OCC {
class CfApiHydrationJob;
namespace CfApiWrapper {
class HydrationDevice : public QIODevice
{
Q_OBJECT
public:
static CfApiHydrationJob *requestHydration(const CfApiWrapper::CallBackContext &context, qint64 totalSize, QObject *parent = nullptr);
HydrationDevice(const CfApiWrapper::CallBackContext &context, qint64 totalSize, QObject *parent = nullptr);
qint64 readData(char *data, qint64 maxlen) override;
qint64 writeData(const char *data, qint64 len) override;
private:
CfApiWrapper::CallBackContext _context;
// expected total size
qint64 _totalSize;
// current offset
qint64 _offset = 0;
QByteArray _buffer;
};
}
}
@@ -0,0 +1,221 @@
# SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: GPL-2.0-or-later
include(GenerateIconsUtils)
unset(CMAKE_CXX_CLANG_TIDY)
# generate custom states icons
set(theme_dir ${CMAKE_SOURCE_DIR}/theme)
set(custom_state_icons_path "${theme_dir}/cfapishellext_custom_states")
set(CUSTOM_STATE_ICON_LOCKED_PATH "${custom_state_icons_path}/0-locked.svg")
set(CUSTOM_STATE_ICON_SHARED_PATH "${custom_state_icons_path}/1-shared.svg")
foreach(size IN ITEMS 24;32;40;48;64;128;256;512;1024)
get_filename_component(output_icon_name_custom_state_locked ${CUSTOM_STATE_ICON_LOCKED_PATH} NAME_WLE)
generate_sized_png_from_svg(${CUSTOM_STATE_ICON_LOCKED_PATH} ${size} OUTPUT_ICON_NAME ${output_icon_name_custom_state_locked} OUTPUT_ICON_PATH "${custom_state_icons_path}/")
endforeach()
foreach(size IN ITEMS 24;32;40;48;64;128;256;512;1024)
get_filename_component(output_icon_name_custom_state_shared ${CUSTOM_STATE_ICON_SHARED_PATH} NAME_WLE)
generate_sized_png_from_svg(${CUSTOM_STATE_ICON_SHARED_PATH} ${size} OUTPUT_ICON_NAME ${output_icon_name_custom_state_shared} OUTPUT_ICON_PATH "${custom_state_icons_path}/")
endforeach()
# offset is used for referencing icon within the binary's resources (indexing start with 0, while IDI_ICON{i} 'i' starts with 1)
if(NOT DEFINED CUSTOM_STATE_ICON_INDEX_OFFSET)
set(CUSTOM_STATE_ICON_INDEX_OFFSET 1)
endif()
# indices used for referencing icon within the binary's resources and .rc file's IDI_ICON{i} entries 'i'
if(NOT DEFINED CUSTOM_STATE_ICON_LOCKED_INDEX)
set(CUSTOM_STATE_ICON_LOCKED_INDEX 1)
endif()
if(NOT DEFINED CUSTOM_STATE_ICON_SHARED_INDEX)
set(CUSTOM_STATE_ICON_SHARED_INDEX 2)
endif()
file(GLOB_RECURSE CUSTOM_STATE_ICONS_LOCKED "${custom_state_icons_path}/*-locked.png*")
get_filename_component(CUSTOM_STATE_ICON_LOCKED_NAME ${CUSTOM_STATE_ICON_LOCKED_PATH} NAME_WLE)
ecm_add_app_icon(CUSTOM_STATE_ICON_LOCKED_OUT ICONS "${CUSTOM_STATE_ICONS_LOCKED}" OUTFILE_BASENAME "${CUSTOM_STATE_ICON_LOCKED_NAME}" DO_NOT_GENERATE_RC_FILE TRUE)
file(GLOB_RECURSE CUSTOM_STATE_ICONS_SHARED "${custom_state_icons_path}/*-shared.png*")
get_filename_component(CUSTOM_STATE_ICON_SHARED_NAME ${CUSTOM_STATE_ICON_SHARED_PATH} NAME_WLE)
ecm_add_app_icon(CUSTOM_STATE_ICON_SHARED_OUT ICONS "${CUSTOM_STATE_ICONS_SHARED}" OUTFILE_BASENAME "${CUSTOM_STATE_ICON_SHARED_NAME}" DO_NOT_GENERATE_RC_FILE TRUE)
file(REMOVE "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc.in")
file(APPEND "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc.in" "IDI_ICON${CUSTOM_STATE_ICON_LOCKED_INDEX} ICON DISCARDABLE \"${CMAKE_CURRENT_BINARY_DIR}/${CUSTOM_STATE_ICON_LOCKED_NAME}.ico\"\n")
file(APPEND "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc.in" "IDI_ICON${CUSTOM_STATE_ICON_SHARED_INDEX} ICON DISCARDABLE \"${CMAKE_CURRENT_BINARY_DIR}/${CUSTOM_STATE_ICON_SHARED_NAME}.ico\"\n")
add_custom_command(
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc"
COMMAND ${CMAKE_COMMAND}
ARGS -E copy "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc.in" "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc"
DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${CUSTOM_STATE_ICON_LOCKED_NAME}.ico" "${CMAKE_CURRENT_BINARY_DIR}/${CUSTOM_STATE_ICON_SHARED_NAME}.ico"
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
)
message("CUSTOM_STATE_ICON_LOCKED_OUT: ${CUSTOM_STATE_ICON_LOCKED_OUT}")
message("CUSTOM_STATE_ICON_SHARED_OUT: ${CUSTOM_STATE_ICON_SHARED_OUT}")
#
# Windows SDK command-line tools require native paths
file(TO_NATIVE_PATH "${CMAKE_CURRENT_SOURCE_DIR}" MidleFileFolder)
set(GeneratedFilesPath "${CMAKE_CURRENT_BINARY_DIR}\\Generated")
set(MidlOutputPathHeader "${GeneratedFilesPath}\\CustomStateProvider.g.h")
set(MidlOutputPathTlb "${GeneratedFilesPath}\\CustomStateProvider.tlb")
set(MidlOutputPathWinmd "${GeneratedFilesPath}\\CustomStateProvider.winmd")
add_custom_target(CustomStateProviderImpl
DEPENDS ${MidlOutputPathHeader}
)
if(NOT DEFINED ENV{WindowsSdkDir})
message("Getting WindowsSdkDir from Registry")
get_filename_component(WindowsSdkDir "[HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows Kits\\Installed Roots;KitsRoot10]" ABSOLUTE)
else()
set(WindowsSdkDir $ENV{WindowsSdkDir})
message("Setting WindowsSdkDir from ENV{WindowsSdkDir")
endif()
# we need cmake path to work with subfolders
file(TO_CMAKE_PATH "${WindowsSdkDir}" WindowsSdkDir)
MACRO(SUBDIRLIST result curdir)
FILE(GLOB children RELATIVE ${curdir} ${curdir}/*)
SET(dirlist "")
FOREACH(child ${children})
IF(IS_DIRECTORY ${curdir}/${child})
LIST(APPEND dirlist ${child})
ENDIF()
ENDFOREACH()
SET(${result} ${dirlist})
ENDMACRO()
SUBDIRLIST(WindowsSdkList "${WindowsSdkDir}/bin")
# pick only dirs that start with 10.0
list(FILTER WindowsSdkList INCLUDE REGEX "10.0.")
# sort the list of subdirs and choose the latest
list(SORT WindowsSdkList ORDER ASCENDING)
list(GET WindowsSdkList -1 WindowsSdkLatest)
message("WindowsSdkLatest has been set to: ${WindowsSdkLatest}")
if(NOT WindowsSdkLatest)
message( FATAL_ERROR "Windows SDK not found")
endif()
SUBDIRLIST(listFoundationContracts "${WindowsSdkDir}/References/${WindowsSdkLatest}/Windows.Foundation.FoundationContract")
list(FILTER listFoundationContracts INCLUDE REGEX "[0-9]+\.")
list(SORT listFoundationContracts ORDER ASCENDING)
list(GET listFoundationContracts -1 WindowsFoundationContractVersion)
message("WindowsFoundationContractVersion has been set to: ${WindowsFoundationContractVersion}")
if(NOT WindowsFoundationContractVersion)
message( FATAL_ERROR "Windows Foundation Contract is not found in ${WindowsSdkLatest} SDK.")
endif()
SUBDIRLIST(listCloudFilesContracts "${WindowsSdkDir}/References/${WindowsSdkLatest}/Windows.Storage.Provider.CloudFilesContract")
list(FILTER listCloudFilesContracts INCLUDE REGEX "[0-9]+\.")
list(SORT listCloudFilesContracts ORDER ASCENDING)
list(GET listCloudFilesContracts -1 WindowsStorageProviderCloudFilesContractVersion)
message("WindowsStorageProviderCloudFilesContractVersion has been set to: ${WindowsStorageProviderCloudFilesContractVersion}")
if(NOT WindowsStorageProviderCloudFilesContractVersion)
message( FATAL_ERROR "Windows Storage Provider Cloud Files Contract is not found in ${WindowsSdkLatest} SDK.")
endif()
# we no longer need to work with sub folders, so convert the WindowsSdkDir to native path
file(TO_NATIVE_PATH ${WindowsSdkDir} WindowsSdkDir)
message("WindowsSdkDir has been set to: ${WindowsSdkDir}")
message("WindowsSdkList has been set to: ${WindowsSdkList}")
message("WindowsSdkLatest has been set to: ${WindowsSdkLatest}")
set(TargetPlatform "x64")
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(TargetPlatform "x64")
elseif(CMAKE_SIZEOF_VOID_P EQUAL 4)
set(TargetPlatform "x86")
endif()
set(WindowsSDKReferencesPath "${WindowsSdkDir}\\References\\${WindowsSdkLatest}")
set(WindowsSDKBinPathForTools "${WindowsSdkDir}\\bin\\${WindowsSdkLatest}\\${TargetPlatform}")
set(WindowsSDKMetadataDirectory "${WindowsSdkDir}\\UnionMetadata\\${WindowsSdkLatest}")
IF(NOT EXISTS "${WindowsSDKReferencesPath}" OR NOT IS_DIRECTORY "${WindowsSDKReferencesPath}")
message( FATAL_ERROR "Please install Windows SDK ${WindowsSdkLatest}")
ENDIF()
IF(NOT EXISTS "${WindowsSDKBinPathForTools}" OR NOT IS_DIRECTORY "${WindowsSDKBinPathForTools}")
message( FATAL_ERROR "Please install Windows SDK ${WindowsSdkLatest}")
ENDIF()
IF(NOT EXISTS "${WindowsSDKMetadataDirectory}" OR NOT IS_DIRECTORY "${WindowsSDKMetadataDirectory}")
message( FATAL_ERROR "Please install Windows SDK ${WindowsSdkLatest}")
ENDIF()
set(midlExe "${WindowsSDKBinPathForTools}\\midl.exe")
set(cppWinRtExe "${WindowsSDKBinPathForTools}\\cppwinrt.exe")
message("cppWinRtExe: ${cppWinRtExe}")
message("midlExe: ${midlExe}")
configure_file(CfApiShellIntegrationVersion.h.in ${CMAKE_CURRENT_BINARY_DIR}/CfApiShellIntegrationVersion.h)
# use midl.exe and cppwinrt.exe to generate files for CustomStateProvider (WinRT class)
add_custom_command(OUTPUT ${MidlOutputPathHeader}
COMMAND ${midlExe} /winrt /h nul /tlb ${MidlOutputPathTlb} /winmd ${MidlOutputPathWinmd} /metadata_dir "${WindowsSDKReferencesPath}\\Windows.Foundation.FoundationContract\\${WindowsFoundationContractVersion}" /nomidl /reference "${WindowsSDKReferencesPath}\\Windows.Foundation.FoundationContract\\${WindowsFoundationContractVersion}\\Windows.Foundation.FoundationContract.winmd" /reference "${WindowsSDKReferencesPath}\\Windows.Storage.Provider.CloudFilesContract\\${WindowsStorageProviderCloudFilesContractVersion}\\Windows.Storage.Provider.CloudFilesContract.winmd" /I ${MidleFileFolder} customstateprovider.idl
COMMAND ${cppWinRtExe} -in ${MidlOutputPathWinmd} -comp ${GeneratedFilesPath} -pch pch.h -ref ${WindowsSDKMetadataDirectory} -out ${GeneratedFilesPath} -verbose
COMMENT "Creating generated files from customstateprovider.idl"
)
add_library(CfApiShellExtensions MODULE
dllmain.cpp
cfapishellintegrationclassfactory.cpp
customstateprovideripc.cpp
ipccommon.cpp
thumbnailprovider.cpp
thumbnailprovideripc.cpp
${CMAKE_SOURCE_DIR}/src/common/shellextensionutils.cpp
customstateprovider.cpp
CfApiShellIntegration.def
CfApiShellIntegration.rc
)
message("CUSTOM_STATE_ICON_LOCKED_OUT: ${CUSTOM_STATE_ICON_LOCKED_OUT}")
message("CUSTOM_STATE_ICON_SHARED_OUT: ${CUSTOM_STATE_ICON_SHARED_OUT}")
if (CUSTOM_STATE_ICON_LOCKED_OUT AND CUSTOM_STATE_ICON_SHARED_OUT)
message("Adding ${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc...")
target_sources(CfApiShellExtensions PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc")
else()
message(WARNING "Could not add ${CMAKE_CURRENT_BINARY_DIR}/${CFAPI_SHELL_EXTENSIONS_LIB_NAME}.rc to CfApiShellExtensions. Custom states for Windows Virtual Files won't work.")
endif()
add_dependencies(CfApiShellExtensions CustomStateProviderImpl)
target_link_libraries(CfApiShellExtensions shlwapi Gdiplus onecoreuap Nextcloud::csync Qt::Core Qt::Network)
target_include_directories(CfApiShellExtensions PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(CfApiShellExtensions PRIVATE ${GeneratedFilesPath})
target_include_directories(CfApiShellExtensions PRIVATE ${CMAKE_SOURCE_DIR})
target_compile_features(CfApiShellExtensions PRIVATE cxx_std_17)
set_target_properties(CfApiShellExtensions
PROPERTIES
LIBRARY_OUTPUT_NAME
${CFAPI_SHELL_EXTENSIONS_LIB_NAME}
RUNTIME_OUTPUT_NAME
${CFAPI_SHELL_EXTENSIONS_LIB_NAME}
LIBRARY_OUTPUT_DIRECTORY
${BIN_OUTPUT_DIRECTORY}
RUNTIME_OUTPUT_DIRECTORY
${BIN_OUTPUT_DIRECTORY}
)
install(TARGETS CfApiShellExtensions
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(FILES $<TARGET_PDB_FILE:CfApiShellExtensions> DESTINATION bin OPTIONAL)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configvfscfapishellext.h.in ${CMAKE_CURRENT_BINARY_DIR}/configvfscfapishellext.h)
@@ -0,0 +1,3 @@
EXPORTS
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
@@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: GPL-2.0-or-later
@@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
// Microsoft Visual C++ generated resource script.
//
#include "CfApiShellIntegrationVersion.h"
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "windows.h"
/////////////////////////////////////////////////////////////////////////////
//
// Version
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION NCEXT_VERSION
PRODUCTVERSION NCEXT_VERSION
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
#else
FILEFLAGS 0x0L
#endif
FILEOS 0x40004L
FILETYPE 0x2L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "Nextcloud GmbH"
VALUE "FileDescription", "Nextcloud CfApi shell extension"
VALUE "FileVersion", NCEXT_VERSION_STRING
VALUE "InternalName", "NCOverlays"
VALUE "LegalCopyright", "Copyright (C) 2023 Nextcloud GmbH"
VALUE "ProductName", "Nextcloud shell extension"
VALUE "ProductVersion", NCEXT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED
@@ -0,0 +1,14 @@
#pragma once
// SPDX-FileCopyrightText: 2016 ownCloud GmbH
// SPDX-License-Identifier: LGPL-2.1-or-later
// This is the number that will end up in the version window of the DLLs.
// Increment this version before committing a new build if you are today's shell_integration build master.
#cmakedefine NCEXT_BUILD_NUM @NCEXT_BUILD_NUM@
#define STRINGIZE2(s) #s
#define STRINGIZE(s) STRINGIZE2(s)
#cmakedefine NCEXT_VERSION @NCEXT_VERSION@
#define NCEXT_VERSION_STRING STRINGIZE(NCEXT_VERSION)
@@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
namespace CfApiShellExtensions
{
runtimeclass CustomStateProvider : [default] Windows.Storage.Provider.IStorageProviderItemPropertySource
{
CustomStateProvider();
}
}
@@ -0,0 +1,87 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "cfapishellintegrationclassfactory.h"
#include <new>
extern long dllReferenceCount;
namespace VfsShellExtensions {
HRESULT CfApiShellIntegrationClassFactory::CreateInstance(
REFCLSID clsid, const ClassObjectInit *classObjectInits, size_t classObjectInitsCount, REFIID riid, void **ppv)
{
for (size_t i = 0; i < classObjectInitsCount; ++i) {
if (clsid == *classObjectInits[i].clsid) {
IClassFactory *classFactory = new (std::nothrow) CfApiShellIntegrationClassFactory(classObjectInits[i].pfnCreate);
if (!classFactory) {
return E_OUTOFMEMORY;
}
const auto hresult = classFactory->QueryInterface(riid, ppv);
classFactory->Release();
return hresult;
}
}
return CLASS_E_CLASSNOTAVAILABLE;
}
// IUnknown
IFACEMETHODIMP CfApiShellIntegrationClassFactory::QueryInterface(REFIID riid, void **ppv)
{
*ppv = nullptr;
if (IsEqualIID(IID_IUnknown, riid) || IsEqualIID(IID_IClassFactory, riid)) {
*ppv = static_cast<IUnknown *>(this);
AddRef();
return S_OK;
} else {
return E_NOINTERFACE;
}
}
IFACEMETHODIMP_(ULONG) CfApiShellIntegrationClassFactory::AddRef()
{
return InterlockedIncrement(&_referenceCount);
}
IFACEMETHODIMP_(ULONG) CfApiShellIntegrationClassFactory::Release()
{
const auto refCount = InterlockedDecrement(&_referenceCount);
if (refCount == 0) {
delete this;
}
return refCount;
}
IFACEMETHODIMP CfApiShellIntegrationClassFactory::CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv)
{
if (punkOuter) {
return CLASS_E_NOAGGREGATION;
}
return _pfnCreate(riid, ppv);
}
IFACEMETHODIMP CfApiShellIntegrationClassFactory::LockServer(BOOL fLock)
{
if (fLock) {
InterlockedIncrement(&dllReferenceCount);
} else {
InterlockedDecrement(&dllReferenceCount);
}
return S_OK;
}
CfApiShellIntegrationClassFactory::CfApiShellIntegrationClassFactory(PFNCREATEINSTANCE pfnCreate)
: _referenceCount(1)
, _pfnCreate(pfnCreate)
{
InterlockedIncrement(&dllReferenceCount);
}
CfApiShellIntegrationClassFactory::~CfApiShellIntegrationClassFactory()
{
InterlockedDecrement(&dllReferenceCount);
}
}
@@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <unknwn.h>
namespace VfsShellExtensions {
using PFNCREATEINSTANCE = HRESULT (*)(REFIID riid, void **ppvObject);
struct ClassObjectInit
{
const CLSID *clsid;
PFNCREATEINSTANCE pfnCreate;
};
class CfApiShellIntegrationClassFactory : public IClassFactory
{
public:
CfApiShellIntegrationClassFactory(PFNCREATEINSTANCE pfnCreate);
IFACEMETHODIMP_(ULONG) AddRef();
IFACEMETHODIMP CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv);
static HRESULT CreateInstance(REFCLSID clsid, const ClassObjectInit *classObjectInits, size_t classObjectInitsCount, REFIID riid, void **ppv);
IFACEMETHODIMP LockServer(BOOL fLock);
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv);
IFACEMETHODIMP_(ULONG) Release();
protected:
~CfApiShellIntegrationClassFactory();
private:
long _referenceCount;
PFNCREATEINSTANCE _pfnCreate;
};
}
@@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef CONFIG_VFS_CFAPI_SHELLEXT_H
#define CONFIG_VFS_CFAPI_SHELLEXT_H
#cmakedefine CUSTOM_STATE_ICON_LOCKED_INDEX "@CUSTOM_STATE_ICON_LOCKED_INDEX@"
#cmakedefine CUSTOM_STATE_ICON_SHARED_INDEX "@CUSTOM_STATE_ICON_SHARED_INDEX@"
#cmakedefine CUSTOM_STATE_ICON_INDEX_OFFSET "@CUSTOM_STATE_ICON_INDEX_OFFSET@"
#endif
@@ -0,0 +1,95 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "customstateprovider.h"
#include "customstateprovideripc.h"
#include <Shlguid.h>
extern long dllObjectsCount;
namespace winrt::CfApiShellExtensions::implementation {
CustomStateProvider::CustomStateProvider()
{
InterlockedIncrement(&dllObjectsCount);
}
CustomStateProvider::~CustomStateProvider()
{
InterlockedDecrement(&dllObjectsCount);
}
winrt::Windows::Foundation::Collections::IIterable<winrt::Windows::Storage::Provider::StorageProviderItemProperty> CustomStateProvider::GetItemProperties(
hstring const &itemPath)
{
std::vector<winrt::Windows::Storage::Provider::StorageProviderItemProperty> properties;
if (_dllFilePath.isEmpty()) {
return winrt::single_threaded_vector(std::move(properties));
}
const auto itemPathString = QString::fromStdString(winrt::to_string(itemPath));
const auto isItemPathValid = [&itemPathString]() {
if (itemPathString.isEmpty()) {
return false;
}
const auto itemPathSplit = itemPathString.split(QStringLiteral("\\"), Qt::SkipEmptyParts);
if (itemPathSplit.size() > 0) {
const auto itemName = itemPathSplit.last();
return !itemName.startsWith(QStringLiteral(".sync_")) && !itemName.startsWith(QStringLiteral(".owncloudsync.log"));
}
return true;
}();
if (!isItemPathValid) {
return winrt::single_threaded_vector(std::move(properties));
}
VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
const auto states = customStateProviderIpc.fetchCustomStatesForFile(itemPathString);
for (const auto &state : states) {
const auto stateValue = state.canConvert<int>() ? state.toInt() : -1;
if (stateValue >= 0) {
auto foundAvalability = _stateIconsAvailibility.constFind(stateValue);
if (foundAvalability == std::cend(_stateIconsAvailibility)) {
const auto hIcon = ExtractIcon(NULL, _dllFilePath.toStdWString().c_str(), stateValue);
_stateIconsAvailibility[stateValue] = hIcon != NULL;
if (hIcon) {
DestroyIcon(hIcon);
}
foundAvalability = _stateIconsAvailibility.constFind(stateValue);
}
if (!foundAvalability.value()) {
continue;
}
winrt::Windows::Storage::Provider::StorageProviderItemProperty itemProperty;
itemProperty.Id(stateValue);
itemProperty.Value(QStringLiteral("Value%1").arg(stateValue).toStdWString());
itemProperty.IconResource(QString(_dllFilePath + QStringLiteral(",%1").arg(QString::number(stateValue))).toStdWString());
properties.push_back(std::move(itemProperty));
}
}
return winrt::single_threaded_vector(std::move(properties));
}
void CustomStateProvider::setDllFilePath(LPCTSTR dllFilePath)
{
_dllFilePath = QString::fromWCharArray(dllFilePath);
if (!_dllFilePath.endsWith(QStringLiteral(".dll"))) {
_dllFilePath.clear();
}
}
QString CustomStateProvider::_dllFilePath;
}
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "Generated/CfApiShellExtensions/customstateprovider.g.h"
#include "config.h"
#include <QMap>
#include <QString>
#include <windows.storage.provider.h>
#include <winrt/windows.foundation.collections.h>
namespace winrt::CfApiShellExtensions::implementation {
class __declspec(uuid(CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID)) CustomStateProvider : public CustomStateProviderT<CustomStateProvider>
{
public:
CustomStateProvider();
virtual ~CustomStateProvider();
Windows::Foundation::Collections::IIterable<Windows::Storage::Provider::StorageProviderItemProperty> GetItemProperties(_In_ hstring const &itemPath);
static void setDllFilePath(LPCTSTR dllFilePath);
private:
static QString _dllFilePath;
static HINSTANCE _dllhInstance;
QMap<int, bool> _stateIconsAvailibility;
};
}
namespace winrt::CfApiShellExtensions::factory_implementation {
struct CustomStateProvider : CustomStateProviderT<CustomStateProvider, implementation::CustomStateProvider>
{
};
}
@@ -0,0 +1,90 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "customstateprovideripc.h"
#include "common/shellextensionutils.h"
#include "ipccommon.h"
#include <QJsonDocument>
namespace {
// we don't want to block the Explorer for too long (default is 30K, so we'd keep it at 10K, except QLocalSocket::waitForDisconnected())
constexpr auto socketTimeoutMs = 10000;
}
namespace VfsShellExtensions {
CustomStateProviderIpc::~CustomStateProviderIpc()
{
disconnectSocketFromServer();
}
QVariantList CustomStateProviderIpc::fetchCustomStatesForFile(const QString &filePath)
{
const auto sendMessageAndReadyRead = [this](QVariantMap &message) {
_localSocket.write(VfsShellExtensions::Protocol::createJsonMessage(message));
return _localSocket.waitForBytesWritten(socketTimeoutMs) && _localSocket.waitForReadyRead(socketTimeoutMs);
};
const auto mainServerName = getServerNameForPath(filePath);
if (mainServerName.isEmpty()) {
return {};
}
// #1 Connect to the local server
if (!connectSocketToServer(mainServerName)) {
return {};
}
auto messageRequestCustomStatesForFile =
QVariantMap{{VfsShellExtensions::Protocol::CustomStateProviderRequestKey, QVariantMap{{VfsShellExtensions::Protocol::FilePathKey, filePath}}}};
// #2 Request custom states for a 'filePath'
if (!sendMessageAndReadyRead(messageRequestCustomStatesForFile)) {
return {};
}
// #3 Receive custom states as JSON
const auto message = QJsonDocument::fromJson(_localSocket.readAll()).toVariant().toMap();
if (!VfsShellExtensions::Protocol::validateProtocolVersion(message) || !message.contains(VfsShellExtensions::Protocol::CustomStateDataKey)) {
return {};
}
const auto customStates =
message.value(VfsShellExtensions::Protocol::CustomStateDataKey).toMap().value(VfsShellExtensions::Protocol::CustomStateStatesKey).toList();
disconnectSocketFromServer();
return customStates;
}
bool CustomStateProviderIpc::disconnectSocketFromServer()
{
const auto isConnectedOrConnecting = _localSocket.state() == QLocalSocket::ConnectedState || _localSocket.state() == QLocalSocket::ConnectingState;
if (isConnectedOrConnecting) {
_localSocket.disconnectFromServer();
const auto isNotConnected = _localSocket.state() == QLocalSocket::UnconnectedState || _localSocket.state() == QLocalSocket::ClosingState;
return isNotConnected || _localSocket.waitForDisconnected();
}
return true;
}
QString CustomStateProviderIpc::getServerNameForPath(const QString &filePath)
{
if (!overrideServerName.isEmpty()) {
return overrideServerName;
}
return findServerNameForPath(filePath);
}
bool CustomStateProviderIpc::connectSocketToServer(const QString &serverName)
{
if (!disconnectSocketFromServer()) {
return false;
}
_localSocket.setServerName(serverName);
_localSocket.connectToServer();
return _localSocket.state() == QLocalSocket::ConnectedState || _localSocket.waitForConnected(socketTimeoutMs);
}
QString CustomStateProviderIpc::overrideServerName = {};
}
@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QString>
#include <QVariant>
#include <QtNetwork/QLocalSocket>
namespace VfsShellExtensions {
class CustomStateProviderIpc
{
public:
CustomStateProviderIpc() = default;
~CustomStateProviderIpc();
QVariantList fetchCustomStatesForFile(const QString &filePath);
private:
bool connectSocketToServer(const QString &serverName);
bool disconnectSocketFromServer();
static QString getServerNameForPath(const QString &filePath);
public:
// for unit tests (as Registry does not work on a CI VM)
static QString overrideServerName;
private:
QLocalSocket _localSocket;
};
}
@@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "cfapishellintegrationclassfactory.h"
#include "customstateprovider.h"
#include "thumbnailprovider.h"
#include <comdef.h>
long dllReferenceCount = 0;
long dllObjectsCount = 0;
HINSTANCE instanceHandle = nullptr;
HRESULT CustomStateProvider_CreateInstance(REFIID riid, void **ppv);
HRESULT ThumbnailProvider_CreateInstance(REFIID riid, void **ppv);
const VfsShellExtensions::ClassObjectInit listClassesSupported[] = {
{&__uuidof(winrt::CfApiShellExtensions::implementation::CustomStateProvider), CustomStateProvider_CreateInstance},
{&__uuidof(VfsShellExtensions::ThumbnailProvider), ThumbnailProvider_CreateInstance}};
STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void *)
{
if (dwReason == DLL_PROCESS_ATTACH) {
instanceHandle = hInstance;
wchar_t dllFilePath[_MAX_PATH] = {0};
::GetModuleFileName(instanceHandle, dllFilePath, _MAX_PATH);
winrt::CfApiShellExtensions::implementation::CustomStateProvider::setDllFilePath(dllFilePath);
DisableThreadLibraryCalls(hInstance);
}
return TRUE;
}
STDAPI DllCanUnloadNow()
{
return (dllReferenceCount == 0 && dllObjectsCount == 0) ? S_OK : S_FALSE;
}
STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv)
{
return VfsShellExtensions::CfApiShellIntegrationClassFactory::CreateInstance(clsid, listClassesSupported, ARRAYSIZE(listClassesSupported), riid, ppv);
}
HRESULT CustomStateProvider_CreateInstance(REFIID riid, void **ppv)
{
try {
const auto customStateProvider = winrt::make_self<winrt::CfApiShellExtensions::implementation::CustomStateProvider>();
return customStateProvider->QueryInterface(riid, ppv);
} catch (_com_error exc) {
return exc.Error();
}
}
HRESULT ThumbnailProvider_CreateInstance(REFIID riid, void **ppv)
{
auto *thumbnailProvider = new (std::nothrow) VfsShellExtensions::ThumbnailProvider();
if (!thumbnailProvider) {
return E_OUTOFMEMORY;
}
const auto hresult = thumbnailProvider->QueryInterface(riid, ppv);
thumbnailProvider->Release();
return hresult;
}
@@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "ipccommon.h"
#include "common/shellextensionutils.h"
#include "common/utility.h"
#include <QDir>
namespace VfsShellExtensions {
QString findServerNameForPath(const QString &filePath)
{
// SyncRootManager Registry key contains all registered folders for Cf API. It will give us the correct name of the
// current app based on the folder path
QString serverName;
constexpr auto syncRootManagerRegKey = R"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager)";
if (OCC::Utility::registryKeyExists(HKEY_LOCAL_MACHINE, syncRootManagerRegKey)) {
OCC::Utility::registryWalkSubKeys(HKEY_LOCAL_MACHINE, syncRootManagerRegKey, [&](HKEY, const QString &syncRootId) {
const QString syncRootIdUserSyncRootsRegistryKey = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId + QStringLiteral(R"(\UserSyncRoots\)");
OCC::Utility::registryWalkValues(HKEY_LOCAL_MACHINE, syncRootIdUserSyncRootsRegistryKey, [&](const QString &userSyncRootName, bool *done) {
const auto userSyncRootValue = QDir::fromNativeSeparators(
OCC::Utility::registryGetKeyValue(HKEY_LOCAL_MACHINE, syncRootIdUserSyncRootsRegistryKey, userSyncRootName).toString());
if (QDir::fromNativeSeparators(filePath).startsWith(userSyncRootValue)) {
const auto syncRootIdSplit = syncRootId.split(QLatin1Char('!'), Qt::SkipEmptyParts);
if (!syncRootIdSplit.isEmpty()) {
serverName = VfsShellExtensions::serverNameForApplicationName(syncRootIdSplit.first());
*done = true;
}
}
});
});
}
return serverName;
}
}
@@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QString>
namespace VfsShellExtensions {
QString findServerNameForPath(const QString &filePath);
}
@@ -0,0 +1,158 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
// global compilation flag configuring windows sdk headers
// preventing inclusion of min and max macros clashing with <limits>
#define NOMINMAX 1
// override byte to prevent clashes with <cstddef>
#define byte win_byte_override
#include <Windows.h> // gdi plus requires Windows.h
// ...includes for other windows header that may use byte...
// Define min max macros required by GDI+ headers.
#ifndef max
#define max(a, b) (((a) > (b)) ? (a) : (b))
#else
#error max macro is already defined
#endif
#ifndef min
#define min(a, b) (((a) < (b)) ? (a) : (b))
#else
#error min macro is already defined
#endif
#include <gdiplus.h>
// Undefine min max macros so they won't collide with <limits> header content.
#undef min
#undef max
// Undefine byte macros so it won't collide with <cstddef> header content.
#undef byte
#include "thumbnailprovider.h"
#include <QSize>
#include <shlwapi.h>
#include <vector>
extern long dllObjectsCount;
namespace VfsShellExtensions {
std::pair<HBITMAP, WTS_ALPHATYPE> hBitmapAndAlphaTypeFromData(const QByteArray &thumbnailData)
{
if (thumbnailData.isEmpty()) {
return {NULL, WTSAT_UNKNOWN};
}
Gdiplus::Bitmap *gdiPlusBitmap = nullptr;
ULONG_PTR gdiPlusToken;
Gdiplus::GdiplusStartupInput gdiPlusStartupInput;
if (Gdiplus::GdiplusStartup(&gdiPlusToken, &gdiPlusStartupInput, nullptr) != Gdiplus::Status::Ok) {
return {NULL, WTSAT_UNKNOWN};
}
const auto handleFailure = [gdiPlusToken]() -> std::pair<HBITMAP, WTS_ALPHATYPE> {
Gdiplus::GdiplusShutdown(gdiPlusToken);
return {NULL, WTSAT_UNKNOWN};
};
const std::vector<unsigned char> bitmapData(thumbnailData.begin(), thumbnailData.end());
auto const stream{::SHCreateMemStream(&bitmapData[0], static_cast<UINT>(bitmapData.size()))};
if (!stream) {
return handleFailure();
}
gdiPlusBitmap = Gdiplus::Bitmap::FromStream(stream);
auto hasAlpha = false;
HBITMAP hBitmap = NULL;
if (gdiPlusBitmap) {
hasAlpha = Gdiplus::IsAlphaPixelFormat(gdiPlusBitmap->GetPixelFormat());
if (gdiPlusBitmap->GetHBITMAP(Gdiplus::Color(0, 0, 0), &hBitmap) != Gdiplus::Status::Ok) {
return handleFailure();
}
}
Gdiplus::GdiplusShutdown(gdiPlusToken);
return {hBitmap, hasAlpha ? WTSAT_ARGB : WTSAT_RGB};
}
ThumbnailProvider::ThumbnailProvider()
: _referenceCount(1)
{
InterlockedIncrement(&dllObjectsCount);
}
ThumbnailProvider::~ThumbnailProvider()
{
InterlockedDecrement(&dllObjectsCount);
}
IFACEMETHODIMP ThumbnailProvider::QueryInterface(REFIID riid, void **ppv)
{
static const QITAB qit[] = {
QITABENT(ThumbnailProvider, IInitializeWithItem),
QITABENT(ThumbnailProvider, IThumbnailProvider),
{0},
};
return QISearch(this, qit, riid, ppv);
}
IFACEMETHODIMP_(ULONG) ThumbnailProvider::AddRef()
{
return InterlockedIncrement(&_referenceCount);
}
IFACEMETHODIMP_(ULONG) ThumbnailProvider::Release()
{
const auto refCount = InterlockedDecrement(&_referenceCount);
if (refCount == 0) {
delete this;
}
return refCount;
}
IFACEMETHODIMP ThumbnailProvider::Initialize(_In_ IShellItem *item, _In_ DWORD mode)
{
HRESULT hresult = item->QueryInterface(__uuidof(_shellItem), reinterpret_cast<void **>(&_shellItem));
if (FAILED(hresult)) {
return hresult;
}
LPWSTR pszName = NULL;
hresult = _shellItem->GetDisplayName(SIGDN_FILESYSPATH, &pszName);
if (FAILED(hresult)) {
return hresult;
}
_shellItemPath = QString::fromWCharArray(pszName);
return S_OK;
}
IFACEMETHODIMP ThumbnailProvider::GetThumbnail(_In_ UINT cx, _Out_ HBITMAP *bitmap, _Out_ WTS_ALPHATYPE *alphaType)
{
*bitmap = nullptr;
*alphaType = WTSAT_UNKNOWN;
const auto thumbnailDataReceived = _thumbnailProviderIpc.fetchThumbnailForFile(_shellItemPath, QSize(cx, cx));
if (thumbnailDataReceived.isEmpty()) {
return E_FAIL;
}
const auto bitmapAndAlphaType = hBitmapAndAlphaTypeFromData(thumbnailDataReceived);
if (!bitmapAndAlphaType.first) {
return E_FAIL;
}
*bitmap = bitmapAndAlphaType.first;
*alphaType = bitmapAndAlphaType.second;
return S_OK;
}
}
@@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "thumbnailprovideripc.h"
#include "config.h"
#include "thumbcache.h"
#include <QString>
#include <comdef.h>
#include <ntstatus.h>
namespace VfsShellExtensions {
std::pair<HBITMAP, WTS_ALPHATYPE> hBitmapAndAlphaTypeFromData(const QByteArray &thumbnailData);
_COM_SMARTPTR_TYPEDEF(IShellItem2, IID_IShellItem2);
class __declspec(uuid(CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID)) ThumbnailProvider : public IInitializeWithItem, public IThumbnailProvider
{
public:
ThumbnailProvider();
virtual ~ThumbnailProvider();
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv);
IFACEMETHODIMP_(ULONG) AddRef();
IFACEMETHODIMP_(ULONG) Release();
IFACEMETHODIMP Initialize(_In_ IShellItem *item, _In_ DWORD mode);
IFACEMETHODIMP GetThumbnail(_In_ UINT cx, _Out_ HBITMAP *bitmap, _Out_ WTS_ALPHATYPE *alphaType);
private:
long _referenceCount;
IShellItem2Ptr _shellItem;
QString _shellItemPath;
ThumbnailProviderIpc _thumbnailProviderIpc;
};
}
@@ -0,0 +1,99 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "thumbnailprovideripc.h"
#include "common/shellextensionutils.h"
#include "ipccommon.h"
#include <QJsonDocument>
#include <QSize>
#include <QString>
#include <QtNetwork/QLocalSocket>
namespace {
// we don't want to block the Explorer for too long (default is 30K, so we'd keep it at 10K, except QLocalSocket::waitForDisconnected())
constexpr auto socketTimeoutMs = 10000;
}
namespace VfsShellExtensions {
ThumbnailProviderIpc::ThumbnailProviderIpc()
{
_localSocket.reset(new QLocalSocket());
}
ThumbnailProviderIpc::~ThumbnailProviderIpc()
{
disconnectSocketFromServer();
}
QByteArray ThumbnailProviderIpc::fetchThumbnailForFile(const QString &filePath, const QSize &size)
{
QByteArray result;
const auto sendMessageAndReadyRead = [this](QVariantMap &message) {
_localSocket->write(VfsShellExtensions::Protocol::createJsonMessage(message));
return _localSocket->waitForBytesWritten(socketTimeoutMs) && _localSocket->waitForReadyRead(socketTimeoutMs);
};
const auto mainServerName = getServerNameForPath(filePath);
if (mainServerName.isEmpty()) {
return result;
}
// #1 Connect to the local server
if (!connectSocketToServer(mainServerName)) {
return result;
}
auto messageRequestThumbnailForFile = QVariantMap{{VfsShellExtensions::Protocol::ThumbnailProviderRequestKey,
QVariantMap{{VfsShellExtensions::Protocol::FilePathKey, filePath},
{VfsShellExtensions::Protocol::ThumbnailProviderRequestFileSizeKey,
QVariantMap{{QStringLiteral("width"), size.width()}, {QStringLiteral("height"), size.height()}}}}}};
// #2 Request a thumbnail of a 'size' for a 'filePath'
if (!sendMessageAndReadyRead(messageRequestThumbnailForFile)) {
return result;
}
// #3 Read the thumbnail data (read all as the thumbnail size is usually less than 1MB)
const auto message = QJsonDocument::fromJson(_localSocket->readAll()).toVariant().toMap();
if (!VfsShellExtensions::Protocol::validateProtocolVersion(message)) {
return result;
}
result = QByteArray::fromBase64(message.value(VfsShellExtensions::Protocol::ThumnailProviderDataKey).toByteArray());
disconnectSocketFromServer();
return result;
}
bool ThumbnailProviderIpc::disconnectSocketFromServer()
{
const auto isConnectedOrConnecting = _localSocket->state() == QLocalSocket::ConnectedState || _localSocket->state() == QLocalSocket::ConnectingState;
if (isConnectedOrConnecting) {
_localSocket->disconnectFromServer();
const auto isNotConnected = _localSocket->state() == QLocalSocket::UnconnectedState || _localSocket->state() == QLocalSocket::ClosingState;
return isNotConnected || _localSocket->waitForDisconnected();
}
return true;
}
QString ThumbnailProviderIpc::getServerNameForPath(const QString &filePath)
{
if (!overrideServerName.isEmpty()) {
return overrideServerName;
}
return findServerNameForPath(filePath);
}
bool ThumbnailProviderIpc::connectSocketToServer(const QString &serverName)
{
if (!disconnectSocketFromServer()) {
return false;
}
_localSocket->setServerName(serverName);
_localSocket->connectToServer();
return _localSocket->state() == QLocalSocket::ConnectedState || _localSocket->waitForConnected(socketTimeoutMs);
}
QString ThumbnailProviderIpc::overrideServerName = {};
}
@@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
class QString;
class QSize;
class QLocalSocket;
#include <QByteArray>
#include <QScopedPointer>
namespace VfsShellExtensions {
class ThumbnailProviderIpc
{
public:
ThumbnailProviderIpc();
~ThumbnailProviderIpc();
QByteArray fetchThumbnailForFile(const QString &filePath, const QSize &size);
private:
bool connectSocketToServer(const QString &serverName);
bool disconnectSocketFromServer();
static QString getServerNameForPath(const QString &filePath);
public:
// for unit tests (as Registry does not work on a CI VM)
static QString overrideServerName;
private:
QScopedPointer<QLocalSocket> _localSocket;
};
}
+335
View File
@@ -0,0 +1,335 @@
/*
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "vfs_cfapi.h"
#include <QDir>
#include "cfapiwrapper.h"
#include "common/filesystembase.h"
#include "common/syncjournaldb.h"
#include "filesystem.h"
#include "syncfileitem.h"
#include <QCoreApplication>
#include <QThread>
// include order is important, this must be included before cfapi
#include <windows.h>
#include <winternl.h>
#include <cfapi.h>
#include <comdef.h>
Q_LOGGING_CATEGORY(lcCfApi, "sync.vfs.cfapi", QtInfoMsg)
using namespace Qt::Literals::StringLiterals;
namespace cfapi {
using namespace OCC::CfApiWrapper;
constexpr auto appIdRegKey = R"(Software\Classes\AppID\)";
constexpr auto clsIdRegKey = R"(Software\Classes\CLSID\)";
const auto rootKey = HKEY_CURRENT_USER;
#if 0
bool registerShellExtension()
{
const QList<QPair<QString, QString>> listExtensions = {{CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME, CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG},
{CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_DISPLAY_NAME, CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG}};
// assume CFAPI_SHELL_EXTENSIONS_LIB_NAME is always in the same folder as the main executable
// assume CFAPI_SHELL_EXTENSIONS_LIB_NAME is always in the same folder as the main executable
const auto shellExtensionDllPath = QDir::toNativeSeparators(
QString(QCoreApplication::applicationDirPath() + QStringLiteral("/") + CFAPI_SHELL_EXTENSIONS_LIB_NAME + QStringLiteral(".dll")));
if (!OCC::FileSystem::fileExists(shellExtensionDllPath)) {
Q_ASSERT(false);
qCWarning(lcCfApi) << u"Register CfAPI shell extensions failed. Dll does not exist in " << QCoreApplication::applicationDirPath();
return false;
}
const QString appIdPath = QString() % appIdRegKey % CFAPI_SHELLEXT_APPID_REG;
if (!OCC::Utility::registrySetKeyValue(rootKey, appIdPath, {}, REG_SZ, CFAPI_SHELLEXT_APPID_DISPLAY_NAME)) {
return false;
}
if (!OCC::Utility::registrySetKeyValue(rootKey, appIdPath, QStringLiteral("DllSurrogate"), REG_SZ, {})) {
return false;
}
for (const auto extension : listExtensions) {
const QString clsidPath = QString() % clsIdRegKey % extension.second;
const QString clsidServerPath = clsidPath % R"(\InprocServer32)";
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, QStringLiteral("AppID"), REG_SZ, CFAPI_SHELLEXT_APPID_REG)) {
return false;
}
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, {}, REG_SZ, extension.first)) {
return false;
}
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, {}, REG_SZ, shellExtensionDllPath)) {
return false;
}
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, QStringLiteral("ThreadingModel"), REG_SZ, QStringLiteral("Apartment"))) {
return false;
}
}
return true;
}
void unregisterShellExtensions()
{
const QString appIdPath = QString() % appIdRegKey % CFAPI_SHELLEXT_APPID_REG;
if (OCC::Utility::registryKeyExists(rootKey, appIdPath)) {
OCC::Utility::registryDeleteKeyTree(rootKey, appIdPath);
}
const QStringList listExtensions = {CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG, CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG};
for (const auto extension : listExtensions) {
const QString clsidPath = QString() % clsIdRegKey % extension;
if (OCC::Utility::registryKeyExists(rootKey, clsidPath)) {
OCC::Utility::registryDeleteKeyTree(rootKey, clsidPath);
}
}
}
#endif
}
namespace OCC {
VfsCfApi::VfsCfApi(QObject *parent)
: Vfs(parent)
{
}
VfsCfApi::~VfsCfApi() = default;
Vfs::Mode VfsCfApi::mode() const
{
return Vfs::Mode::WindowsCfApi;
}
void VfsCfApi::startImpl(const VfsSetupParams &params)
{
// cfapi::registerShellExtension();
cfapi::registerSyncRoot(params, [this](const QString &errorMessage) {
if (errorMessage.isEmpty()) {
auto connectResult = cfapi::connectSyncRoot(this->params().filesystemPath(), this);
if (!connectResult) {
qCCritical(lcCfApi) << u"Initialization failed, couldn't connect sync root:" << connectResult.error();
return;
}
_connectionKey = *std::move(connectResult);
Q_EMIT started();
} else {
qCCritical(lcCfApi) << errorMessage;
Q_ASSERT(false);
Q_EMIT error(errorMessage);
}
});
}
Result<void, QString> CfApiVfsPluginFactory::prepare(const QString &path, const QUuid &) const
{
if (QDir(path).isRoot()) {
return tr("The Virtual filesystem feature does not support a drive as sync root");
}
const auto fileSystem = FileSystem::fileSystemForPath(path);
if (!fileSystem.startsWith("NTFS"_L1, Qt::CaseInsensitive)) {
return tr("The Virtual filesystem feature requires a NTFS file system, %1 is using %2").arg(path, fileSystem);
}
const auto type = GetDriveTypeW(reinterpret_cast<const wchar_t *>(path.mid(0, 3).utf16()));
if (type == DRIVE_REMOTE) {
return tr("The Virtual filesystem feature is not supported on network drives");
}
return {};
}
void VfsCfApi::stop()
{
if (_connectionKey.Internal != 0) {
const auto result = cfapi::disconnectSyncRoot(std::move(_connectionKey));
if (!result) {
qCCritical(lcCfApi) << u"Disconnect failed for" << params().filesystemPath() << u":" << result.error();
}
}
}
void VfsCfApi::unregisterFolder()
{
const auto result = cfapi::unregisterSyncRoot(params());
if (!result) {
qCCritical(lcCfApi) << u"Unregistration failed for" << params().filesystemPath() << u":" << result.error();
}
#if 0
if (!cfapi::isAnySyncRoot(params().providerName, params().account->displayName())) {
cfapi::unregisterShellExtensions();
}
#endif
}
bool VfsCfApi::socketApiPinStateActionsShown() const
{
return true;
}
Result<Vfs::ConvertToPlaceholderResult, QString> VfsCfApi::updateMetadata(const SyncFileItem &syncItem, const QString &filePath, const QString &replacesFile)
{
const auto localPath = QDir::toNativeSeparators(filePath);
const auto replacesPath = QDir::toNativeSeparators(replacesFile);
if (syncItem._type == ItemTypeVirtualFileDehydration) {
auto result = cfapi::dehydratePlaceholder(localPath, syncItem._fileId);
// if the dehydration call succeeded, check whether the placeholder is dehydrated
Q_ASSERT(!result || isDehydratedPlaceholder(filePath));
return result;
} else {
if (cfapi::findPlaceholderInfo<CF_PLACEHOLDER_BASIC_INFO>(localPath)) {
return cfapi::updatePlaceholderInfo(localPath, syncItem._modtime, syncItem._size, syncItem._fileId, replacesPath);
} else {
return cfapi::convertToPlaceholder(localPath, syncItem._modtime, syncItem._size, syncItem._fileId, replacesPath);
}
}
}
Result<void, QString> VfsCfApi::createPlaceholder(const SyncFileItem &item)
{
const auto localPath = QDir::toNativeSeparators(params().filesystemPath() + item.localName());
const auto result = cfapi::createPlaceholderInfo(localPath, item._modtime, item._size, item._fileId);
return result;
}
bool VfsCfApi::needsMetadataUpdate(const SyncFileItem &item)
{
const QString path = params().filesystemPath() + item.localName();
if (!QFileInfo::exists(path)) {
return false;
}
return !cfapi::findPlaceholderInfo<CF_PLACEHOLDER_BASIC_INFO>(path).isValid();
}
bool VfsCfApi::isDehydratedPlaceholder(const QString &filePath)
{
return cfapi::isDehydratedPlaceholder(FileSystem::Path(filePath));
}
LocalInfo VfsCfApi::statTypeVirtualFile(const std::filesystem::directory_entry &entry, ItemType type)
{
// only get placeholder info if it's a file
if (type == ItemTypeFile) {
const auto path = FileSystem::Path(entry);
if (auto placeholderInfo = cfapi::findPlaceholderInfo<CF_PLACEHOLDER_BASIC_INFO>(path.toString())) {
Q_ASSERT(placeholderInfo.handle());
FILE_ATTRIBUTE_TAG_INFO attributeInfo = {};
if (!GetFileInformationByHandleEx(placeholderInfo.handle(), FileAttributeTagInfo, &attributeInfo, sizeof(attributeInfo))) {
const auto error = GetLastError();
qCCritical(lcCfApi) << u"GetFileInformationByHandle failed on" << path << OCC::Utility::formatWinError(error);
return {};
}
const CF_PLACEHOLDER_STATE placeholderState = CfGetPlaceholderStateFromAttributeTag(attributeInfo.FileAttributes, attributeInfo.ReparseTag);
if (placeholderState & CF_PLACEHOLDER_STATE_PLACEHOLDER) {
if (placeholderState & CF_PLACEHOLDER_STATE_PARTIAL) {
if (placeholderInfo.pinState() == PinState::AlwaysLocal) {
Q_ASSERT(attributeInfo.FileAttributes & FILE_ATTRIBUTE_PINNED);
type = ItemTypeVirtualFileDownload;
} else {
type = ItemTypeVirtualFile;
}
} else {
if (placeholderInfo.pinState() == PinState::OnlineOnly) {
Q_ASSERT(attributeInfo.FileAttributes & FILE_ATTRIBUTE_UNPINNED);
type = ItemTypeVirtualFileDehydration;
} else {
// nothing to do
Q_ASSERT(type == ItemTypeFile);
}
}
}
}
}
return LocalInfo(entry, type);
}
bool VfsCfApi::setPinState(const QString &folderPath, PinState state)
{
qCDebug(lcCfApi) << u"setPinState" << folderPath << state;
const auto localPath = QDir::toNativeSeparators(params().filesystemPath() + folderPath);
return static_cast<bool>(cfapi::setPinState(localPath, state, cfapi::Recurse));
}
Optional<PinState> VfsCfApi::pinState(const QString &folderPath)
{
const auto localPath = QDir::toNativeSeparators(params().filesystemPath() + folderPath);
const auto info = cfapi::findPlaceholderInfo<CF_PLACEHOLDER_BASIC_INFO>(localPath);
if (!info) {
qCDebug(lcCfApi) << u"Couldn't find pin state for regular non-placeholder file" << localPath;
return {};
}
return info.pinState();
}
Vfs::AvailabilityResult VfsCfApi::availability(const QString &folderPath)
{
const auto basePinState = pinState(folderPath);
if (basePinState) {
switch (*basePinState) {
case OCC::PinState::AlwaysLocal:
return VfsItemAvailability::AlwaysLocal;
break;
case OCC::PinState::Inherited:
break;
case OCC::PinState::OnlineOnly:
return VfsItemAvailability::OnlineOnly;
break;
case OCC::PinState::Unspecified:
break;
case OCC::PinState::Excluded:
break;
};
return VfsItemAvailability::Mixed;
} else {
return AvailabilityError::NoSuchItem;
}
}
void VfsCfApi::cancelHydration(const OCC::CfApiWrapper::CallBackContext &context)
{
Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread());
// Find matching hydration job for request id
auto hydrationJob = _hydrationJobs.find(context.transferKey);
if (hydrationJob != _hydrationJobs.end()) {
qCInfo(lcCfApi) << u"Cancel hydration" << hydrationJob.value()->context();
// the job itself will take care of _hydrationJobs and its deletion
hydrationJob.value()->abort();
}
}
void VfsCfApi::fileStatusChanged(const QString &systemFileName, SyncFileStatus fileStatus)
{
if (!QFileInfo::exists(systemFileName)) {
return;
}
if (fileStatus.tag() == SyncFileStatus::StatusUpToDate) {
if (auto info = CfApiWrapper::findPlaceholderInfo<CF_PLACEHOLDER_BASIC_INFO>(systemFileName)) {
std::ignore = cfapi::updatePlaceholderMarkInSync(info.handle());
if (info.pinState() == PinState::Excluded) {
// clear possible exclude flag
// a file usually does not change from excluded to not excluded, but ...
cfapi::setPinState(systemFileName, PinState::Inherited, CfApiWrapper::Recurse);
}
}
} else if (fileStatus.tag() == SyncFileStatus::StatusExcluded) {
cfapi::setPinState(systemFileName, PinState::Excluded, CfApiWrapper::Recurse);
}
}
} // namespace OCC
+85
View File
@@ -0,0 +1,85 @@
/*
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "common/plugin.h"
#include "libsync/vfs/hydrationjob.h"
#include "libsync/vfs/vfs.h"
#include "plugins/vfs/cfapi/cfapiwrapper.h"
#include <QScopedPointer>
namespace OCC {
class HydrationJob;
class VfsCfApiPrivate;
class SyncJournalFileRecord;
namespace CfApiWrapper {
struct CallBackContext;
class HydrationDevice;
}
class CfApiHydrationJob : public HydrationJob
{
Q_OBJECT
public:
using HydrationJob::HydrationJob;
void setContext(const CfApiWrapper::CallBackContext &context) { _context = context; }
CfApiWrapper::CallBackContext context() const { return _context; }
private:
CfApiWrapper::CallBackContext _context;
};
class VfsCfApi : public Vfs
{
Q_OBJECT
public:
explicit VfsCfApi(QObject *parent = nullptr);
~VfsCfApi();
Mode mode() const override;
void stop() override;
void unregisterFolder() override;
bool socketApiPinStateActionsShown() const override;
Result<void, QString> createPlaceholder(const SyncFileItem &item) override;
bool needsMetadataUpdate(const SyncFileItem &) override;
bool isDehydratedPlaceholder(const QString &filePath) override;
bool setPinState(const QString &folderPath, PinState state) override;
Optional<PinState> pinState(const QString &folderPath) override;
AvailabilityResult availability(const QString &folderPath) override;
void cancelHydration(const OCC::CfApiWrapper::CallBackContext &context);
LocalInfo statTypeVirtualFile(const std::filesystem::directory_entry &entry, ItemType type) override;
public Q_SLOTS:
void fileStatusChanged(const QString &systemFileName, OCC::SyncFileStatus fileStatus) override;
protected:
Result<ConvertToPlaceholderResult, QString> updateMetadata(const SyncFileItem &, const QString &, const QString &) override;
void startImpl(const VfsSetupParams &params) override;
private:
QMap<uint64_t, CfApiHydrationJob *> _hydrationJobs;
CF_CONNECTION_KEY _connectionKey = {};
friend class CfApiWrapper::HydrationDevice;
};
class CfApiVfsPluginFactory : public QObject, public DefaultPluginFactory<VfsCfApi>
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "eu.qsfera.PluginFactory" FILE "libsync/vfs/vfspluginmetadata.json")
Q_INTERFACES(OCC::PluginFactory)
public:
Result<void, QString> prepare(const QString &path, const QUuid &accountUuid) const override;
};
} // namespace OCC
@@ -0,0 +1 @@
add_vfs_plugin(NAME off SRC vfs_off.cpp)
+101
View File
@@ -0,0 +1,101 @@
/*
* 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 "vfs_off.h"
#include "filesystem.h"
#include "syncfileitem.h"
using namespace OCC;
VfsOff::VfsOff(QObject *parent)
: Vfs(parent)
{
}
VfsOff::~VfsOff() = default;
Vfs::Mode VfsOff::mode() const
{
return Vfs::Mode::Off;
}
void VfsOff::stop() { }
void VfsOff::unregisterFolder() { }
bool VfsOff::socketApiPinStateActionsShown() const
{
return false;
}
Result<void, QString> VfsOff::createPlaceholder(const SyncFileItem &)
{
return {};
}
bool VfsOff::needsMetadataUpdate(const SyncFileItem &)
{
return false;
}
bool VfsOff::isDehydratedPlaceholder(const QString &)
{
return false;
}
bool VfsOff::setPinState(const QString &, PinState)
{
return true;
}
Optional<PinState> VfsOff::pinState(const QString &)
{
return PinState::AlwaysLocal;
}
Vfs::AvailabilityResult VfsOff::availability(const QString &)
{
return VfsItemAvailability::AlwaysLocal;
}
LocalInfo VfsOff::statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type)
{
return LocalInfo(path, type);
}
void VfsOff::startImpl(const VfsSetupParams &)
{
Q_EMIT started();
}
Result<void, QString> OffVfsPluginFactory::prepare(const QString &, const QUuid &) const
{
return {};
}
Result<Vfs::ConvertToPlaceholderResult, QString> VfsOff::updateMetadata(const SyncFileItem &item, const QString &filePath, const QString &replacesFile)
{
Q_UNUSED(replacesFile)
if (!item.isDirectory()) {
const bool isReadOnly = !item._remotePerm.isNull() && !item._remotePerm.hasPermission(RemotePermissions::CanWrite);
FileSystem::setFileReadOnlyWeak(filePath, isReadOnly);
}
return { ConvertToPlaceholderResult::Ok };
}
void VfsOff::fileStatusChanged(const QString &, SyncFileStatus)
{
}
+68
View File
@@ -0,0 +1,68 @@
/*
* 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 <QObject>
#include <QScopedPointer>
#include "common/plugin.h"
#include "libsync/vfs/vfs.h"
namespace OCC {
class VfsOff : public Vfs
{
Q_OBJECT
public:
VfsOff(QObject *parent = nullptr);
~VfsOff() override;
Mode mode() const override;
void stop() override;
void unregisterFolder() override;
bool socketApiPinStateActionsShown() const override;
Result<void, QString> createPlaceholder(const SyncFileItem &) override;
bool needsMetadataUpdate(const SyncFileItem &) override;
bool isDehydratedPlaceholder(const QString &) override;
bool setPinState(const QString &, PinState) override;
Optional<PinState> pinState(const QString &) override;
AvailabilityResult availability(const QString &) override;
LocalInfo statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) override;
public Q_SLOTS:
void fileStatusChanged(const QString &, SyncFileStatus) override;
protected:
Result<ConvertToPlaceholderResult, QString> updateMetadata(const SyncFileItem &, const QString &, const QString &) override;
void startImpl(const VfsSetupParams &) override;
};
class OffVfsPluginFactory : public QObject, public DefaultPluginFactory<VfsOff>
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "eu.qsfera.PluginFactory" FILE "libsync/vfs/vfspluginmetadata.json")
Q_INTERFACES(OCC::PluginFactory)
public:
Result<void, QString> prepare(const QString &path, const QUuid &accountUuid) const override;
};
} // namespace OCC
@@ -0,0 +1,19 @@
if (NOT WIN32)
find_package(OpenVFS 0.1.0 QUIET)
set_package_properties(OpenVFS PROPERTIES TYPE OPTIONAL
DESCRIPTION "A Virtual Filesystem Layer for cloud storages for the free desktop"
URL "https://github.com/opencloud-eu/openvfs/")
if(TARGET OpenVFS::OpenVFS)
get_target_property (OPEN_VFS_CONFIG OpenVFS::OpenVFS IMPORTED_CONFIGURATIONS)
get_target_property (OPEN_VFS_PATH OpenVFS::OpenVFS IMPORTED_LOCATION_${OPEN_VFS_CONFIG})
message(STATUS "Using OpenVFS ${OPEN_VFS_CONFIG} (${OPEN_VFS_PATH})")
add_vfs_plugin(NAME openvfs
SRC
vfs_openvfs.cpp
LIBS
OpenVFS::LibOpenVFS
)
target_compile_definitions(vfs_openvfs PRIVATE OPENVFS_EXE=\"${OPEN_VFS_PATH}\")
endif()
endif()
@@ -0,0 +1,579 @@
/*
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2025 OpenCloud GmbH and OpenCloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "vfs_openvfs.h"
#include "account.h"
#include "common/chronoelapsedtimer.h"
#include "common/syncjournaldb.h"
#include "filesystem.h"
#include "libsync/theme.h"
#include "libsync/xattr.h"
#include "syncfileitem.h"
#include "vfs/hydrationjob.h"
#include <openvfs/openvfs.h>
#include <QDir>
#include <QFile>
#include <QLoggingCategory>
#include <QString>
#include <QUuid>
using namespace std::chrono_literals;
using namespace Qt::StringLiterals;
Q_LOGGING_CATEGORY(lcOpenVFS, "sync.vfs.xattr", QtInfoMsg)
namespace {
OCC::FileSystem::Path openVFSExePath()
{
auto openVFS = OCC::FileSystem::Path(std::filesystem::path(OPENVFS_EXE));
auto binary = OCC::FileSystem::Path(qApp->applicationDirPath()) / openVFS.get().filename();
if (binary.exists()) {
return binary;
}
return openVFS;
}
QString xattrOwnerString(const QUuid &accountUuid)
{
return u"%1:%2"_s.arg(OCC::Theme::instance()->appName(), accountUuid.toString(QUuid::WithoutBraces));
}
OCC::FileSystem::Path openVFSConfigFilePath()
{
auto systemPath = OCC::FileSystem::Path(QStandardPaths::locate(QStandardPaths::ConfigLocation, u"openvfs/config.json"_s));
if (!systemPath.get().empty()) {
return systemPath;
}
if (OCC::Utility::runningInAppImage()) {
auto appimagePath = OCC::FileSystem::Path(qApp->applicationDirPath()) / "../etc/xdg/openvfs/config.json";
if (appimagePath.exists()) {
return appimagePath;
}
}
return {};
}
OpenVFS::PlaceHolderAttributes placeHolderAttributes(const std::filesystem::path &path)
{
const auto data = OCC::FileSystem::Xattr::getxattr(path, QString::fromUtf8(OpenVFS::Constants::XAttributeNames::Data));
if (!data) {
qCWarning(lcOpenVFS) << u"No OpenVFS xattr found for" << path.native();
}
return OpenVFS::PlaceHolderAttributes::fromData(path, data ? std::vector<uint8_t>{data->cbegin(), data->cend()} : std::vector<uint8_t>{});
}
OpenVFS::PlaceHolderAttributes placeHolderAttributes(const QString &path)
{
return placeHolderAttributes(OCC::FileSystem::toFilesystemPath(path));
}
OCC::Result<void, QString> setPlaceholderAttributes(const OpenVFS::PlaceHolderAttributes &attributes)
{
Q_ASSERT(attributes.validate());
const auto data = attributes.toData();
return OCC::FileSystem::Xattr::setxattr(attributes.absolutePath, QString::fromUtf8(OpenVFS::Constants::XAttributeNames::Data),
{reinterpret_cast<const char *>(data.data()), static_cast<qsizetype>(data.size())});
}
OCC::Result<void, QString> setPlaceholderAttributes(const OpenVFS::PlaceHolderAttributes &attributes, time_t modtime)
{
if (const auto result = setPlaceholderAttributes(attributes); !result) {
return result;
}
OCC::FileSystem::setModTime(attributes.absolutePath, modtime);
return {};
}
OpenVFS::Constants::PinStates convertPinState(OCC::PinState pState)
{
switch (pState) {
case OCC::PinState::AlwaysLocal:
return OpenVFS::Constants::PinStates::AlwaysLocal;
case OCC::PinState::Inherited:
return OpenVFS::Constants::PinStates::Inherited;
case OCC::PinState::OnlineOnly:
return OpenVFS::Constants::PinStates::OnlineOnly;
case OCC::PinState::Excluded:
return OpenVFS::Constants::PinStates::OnlineOnly;
case OCC::PinState::Unspecified:
return OpenVFS::Constants::PinStates::Unspecified;
};
Q_UNREACHABLE();
}
OCC::PinState convertPinState(OpenVFS::Constants::PinStates pState)
{
switch (pState) {
case OpenVFS::Constants::PinStates::AlwaysLocal:
return OCC::PinState::AlwaysLocal;
case OpenVFS::Constants::PinStates::Inherited:
return OCC::PinState::Inherited;
case OpenVFS::Constants::PinStates::OnlineOnly:
return OCC::PinState::OnlineOnly;
case OpenVFS::Constants::PinStates::Unspecified:
return OCC::PinState::Unspecified;
case OpenVFS::Constants::PinStates::Excluded:
return OCC::PinState::Excluded;
}
Q_UNREACHABLE();
}
#ifdef Q_OS_LINUX
// Helper function to parse paths that the kernel inserts escape sequences
// for.
// https://github.com/qt/qtbase/blob/f47d9bcb45c77183c23e406df415ec2d9f4acbc4/src/corelib/io/qstorageinfo_linux.cpp#L72
QByteArray parseMangledPath(QByteArrayView path)
{
// The kernel escapes with octal the following characters:
// space ' ', tab '\t', backslash '\\', and newline '\n'
// See:
// https://codebrowser.dev/linux/linux/fs/proc_namespace.c.html#show_mountinfo
// https://codebrowser.dev/linux/linux/fs/seq_file.c.html#mangle_path
QByteArray ret(path.size(), '\0');
char *dst = ret.data();
const char *src = path.data();
const char *srcEnd = path.data() + path.size();
while (src != srcEnd) {
switch (*src) {
case ' ': // Shouldn't happen
return {};
case '\\': {
// It always uses exactly three octal characters.
++src;
char c = (*src++ - '0') << 6;
c |= (*src++ - '0') << 3;
c |= (*src++ - '0');
*dst++ = c;
break;
}
default:
*dst++ = *src++;
break;
}
}
// If "path" contains any of the characters this method is demangling,
// "ret" would be oversized with extra '\0' characters at the end.
ret.resize(dst - ret.data());
return ret;
}
#endif
}
namespace OCC {
OpenVFS::OpenVFS(QObject *parent)
: Vfs(parent)
{
}
OpenVFS::~OpenVFS() = default;
Vfs::Mode OpenVFS::mode() const
{
return Mode::OpenVFS;
}
void OpenVFS::startImpl(const VfsSetupParams &params)
{
qCDebug(lcOpenVFS, "Start OpenVFS VFS");
// Lets claim the sync root directory for us
// Set the owner to this client to claim it.
const auto owner = xattrOwnerString(params.account->uuid()).toStdString();
if (const auto info = ::OpenVFS::Registration::registerFilesystem(params.root(), owner); !info) {
if (info.owner() != owner) {
Q_EMIT error(tr("Unable to claim the sync root for files on demand, the folder is already claimed by %1").arg(info.owner()));
return;
}
Q_EMIT error(tr("Unable to retrieve registration info. Error: %1").arg(info.error()));
return;
}
qCDebug(lcOpenVFS) << "Mounting" << openVFSExePath().toString() << params.root().toString();
_openVfsProcess = new QProcess(this);
// merging the channels and piping the output to our log lead to deadlocks
_openVfsProcess->setProcessChannelMode(QProcess::ForwardedChannels);
const auto logPrefix = [path = params.root().toString(), this] { return u"[%1 %2] "_s.arg(QString::number(_openVfsProcess->processId()), path); };
connect(_openVfsProcess, &QProcess::finished, this, [logPrefix, this] { qCFatal(lcOpenVFS) << logPrefix() << "finished" << _openVfsProcess->exitCode(); });
connect(_openVfsProcess, &QProcess::started, this, [logPrefix, this] {
qCInfo(lcOpenVFS) << logPrefix() << u"started";
// TODO:
// give it time to mount
QTimer::singleShot(1s, this, &Vfs::started);
});
connect(_openVfsProcess, &QProcess::errorOccurred, this, [logPrefix, this] { qCWarning(lcOpenVFS) << logPrefix() << _openVfsProcess->errorString(); });
_openVfsProcess->start(openVFSExePath().toString(),
{u"-d"_s, u"-i"_s, openVFSConfigFilePath().toString(), u"-o"_s, xattrOwnerString(params.account->uuid()), params.root().toString()},
QIODevice::ReadOnly);
}
void OpenVFS::stop()
{
if (_openVfsProcess) {
// disconnect qFatal on subprocess exit
disconnect(_openVfsProcess, &QProcess::finished, nullptr, nullptr);
_openVfsProcess->terminate();
_openVfsProcess->waitForFinished();
_openVfsProcess->deleteLater();
}
}
void OpenVFS::unregisterFolder()
{
::OpenVFS::Registration::unregisterFilesystem({params().root(), xattrOwnerString(params().account->uuid()).toStdString()});
}
bool OpenVFS::socketApiPinStateActionsShown() const
{
return true;
}
bool OpenVfsPluginFactory::checkAvailability() const
{
#ifdef Q_OS_LINUX
if (!FileSystem::Path("/dev/fuse").exists()) {
qCWarning(lcOpenVFS) << u"Fuse is not installed or available on the system";
return false;
}
if (QStandardPaths::findExecutable(u"fusermount3"_s).isEmpty()) {
qCWarning(lcOpenVFS) << u"fusermount3 is not installed on the system";
return false;
}
#endif
return true;
}
Result<void, QString> OpenVfsPluginFactory::prepare(const QString &path, const QUuid &accountUuid) const
{
#ifdef Q_OS_LINUX
// we can't use QStorageInfo as it does not list fuse mounts
if (!_cacheTimer.isStarted() || _cacheTimer.duration() > 30s) {
_fuseMountCache.clear();
QFile file(u"/proc/self/mountinfo"_s);
if (file.open(QIODevice::ReadOnly)) {
const auto lines = file.readAll().split('\n');
file.close();
for (auto &line : lines) {
auto fields = line.split(' ');
if (fields.size() >= 9 && fields[8] == "fuse.openvfsfuse") {
_fuseMountCache << QString::fromUtf8(parseMangledPath(fields[4]));
}
}
} else {
qCWarning(lcOpenVFS) << "Failed to read /proc/self/mountinfo" << file.errorString();
return tr("Failed to read /proc/self/mountinfo");
}
}
if (std::ranges::find_if(_fuseMountCache, [&](const QString &p) { return FileSystem::isChildPathOf2(path, p).testFlag(FileSystem::ChildResult::IsEqual); })
!= _fuseMountCache.cend()) {
QProcess process;
process.setProcessChannelMode(QProcess::MergedChannels);
process.start(u"fusermount"_s, {u"-zu"_s, path});
// TODO: don't block?
process.waitForFinished();
if (process.exitCode() != 0) {
const auto output = process.readAll();
qCWarning(lcOpenVFS) << "Failed to unmount the OpenVFS mount" << path << output;
return tr("Failed to unmount the OpenVFS mount %1 Error:%2").arg(path, output);
} else {
qCDebug(lcOpenVFS) << "Unmounted OpenVFS mount" << path;
}
}
#endif
const auto fsPath = FileSystem::toFilesystemPath(path);
if (!FileSystem::Xattr::supportsxattr(fsPath)) {
qCDebug(lcOpenVFS) << path << "does not support xattributes";
return tr("The filesystem for %1 does not support xattributes.").arg(path);
}
if (const auto info = ::OpenVFS::Registration::fromAttributes(fsPath); info && info.owner() != xattrOwnerString(accountUuid).toStdString()) {
return tr("The sync path is already claimed by %1").arg(info.owner());
}
if (!openVFSExePath().exists()) {
qCDebug(lcOpenVFS) << "OpenVFS executable not found at" << openVFSExePath().toString();
return tr("OpenVFS executable not found, please install it");
}
const auto vfsConfig = openVFSConfigFilePath();
if (!vfsConfig.get().empty()) {
qCDebug(lcOpenVFS) << "Using config file" << vfsConfig.toString();
} else {
return tr("Failed to find the OpenVFS config file, please check your installation.");
}
return {};
}
OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OpenVFS::updateMetadata(
const SyncFileItem &syncItem, const QString &filePath, const QString &replacesFile)
{
if (syncItem._type == ItemTypeVirtualFileDehydration) {
// replace the file with a placeholder
if (const auto result = createPlaceholder(syncItem); !result) {
qCCritical(lcOpenVFS) << "Failed to create placeholder for" << filePath << result.error();
return result.error();
}
return ConvertToPlaceholderResult::Ok;
} else {
::OpenVFS::PlaceHolderAttributes attributes = [&] {
// load the previous attributes
if (!replacesFile.isEmpty()) {
if (const auto attr = placeHolderAttributes(replacesFile)) {
return attr;
}
}
if (const auto attr = placeHolderAttributes(filePath)) {
return attr;
}
Q_ASSERT(QFileInfo::exists(filePath));
// generate new meta data for an existing file
auto attr = ::OpenVFS::PlaceHolderAttributes::create(
FileSystem::toFilesystemPath(filePath), syncItem._etag.toStdString(), syncItem._fileId.toStdString(), syncItem._size);
attr.state = ::OpenVFS::Constants::States::Hydrated;
return attr;
}();
Q_ASSERT(attributes);
attributes.size = syncItem._size;
attributes.fileId = syncItem._fileId.toStdString();
attributes.etag = syncItem._etag.toStdString();
qCDebug(lcOpenVFS) << attributes.absolutePath.native() << syncItem._type;
switch (syncItem._type) {
case ItemTypeVirtualFileDownload:
attributes.state = ::OpenVFS::Constants::States::Hydrating;
break;
case ItemTypeVirtualFile:
[[fallthrough]];
case ItemTypeVirtualFileDehydration:
qCDebug(lcOpenVFS) << "updateMetadata for virtual file " << syncItem._type;
attributes.state = ::OpenVFS::Constants::States::DeHydrated;
break;
case ItemTypeFile:
// hydrated files must not have a size attribute != 0
attributes.size = 0;
[[fallthrough]];
case ItemTypeDirectory:
qCDebug(lcOpenVFS) << "updateMetadata for" << syncItem._type;
attributes.state = ::OpenVFS::Constants::States::Hydrated;
break;
case ItemTypeSymLink:
[[fallthrough]];
case ItemTypeUnsupported:
Q_UNREACHABLE();
}
if (const auto result = setPlaceholderAttributes(attributes, syncItem._modtime); !result) {
qCCritical(lcOpenVFS) << "Failed to update placeholder for" << filePath << result.error();
return result.error();
}
return ConvertToPlaceholderResult::Ok;
}
}
void OpenVFS::slotHydrateJobFinished()
{
HydrationJob *hydration = qobject_cast<HydrationJob *>(sender());
const auto targetPath = FileSystem::toFilesystemPath(hydration->targetFileName());
Q_ASSERT(!targetPath.empty());
qCInfo(lcOpenVFS) << u"Hydration Job finished for" << targetPath.native();
if (std::filesystem::exists(targetPath)) {
auto item = OCC::SyncFileItem::fromSyncJournalFileRecord(hydration->record());
// the file is now downloaded
item->_type = ItemTypeFile;
if (auto inode = FileSystem::getInode(targetPath)) {
item->_inode = inode.value();
} else {
qCWarning(lcOpenVFS) << u"Failed to get inode for" << targetPath.native();
}
// Update the client sync journal database if the file modifications have been successful
const auto result = this->params().journal->setFileRecord(SyncJournalFileRecord::fromSyncFileItem(*item));
if (!result) {
qCWarning(lcOpenVFS) << u"Error when setting the file record to the database" << result.error();
} else {
qCInfo(lcOpenVFS) << u"Hydration succeeded" << targetPath.native();
}
} else {
qCWarning(lcOpenVFS) << u"Hydration succeeded but the file appears to be moved" << targetPath.native();
}
hydration->deleteLater();
this->_hydrationJobs.remove(hydration->fileId());
}
Result<void, QString> OpenVFS::createPlaceholder(const SyncFileItem &item)
{
const auto path = params().root() / item.localName();
if (path.exists()) {
Q_ASSERT(item._type == ItemTypeVirtualFileDehydration);
if (item._type == ItemTypeVirtualFileDehydration && FileSystem::fileChanged(path, FileSystem::FileChangedInfo::fromSyncFileItem(&item))) {
return tr("Cannot dehydrate a placeholder because the file changed");
}
}
QFile file(path.get());
if (!file.open(QFile::ReadWrite | QFile::Truncate)) {
return file.errorString();
}
file.write("");
file.close();
const auto attributes = ::OpenVFS::PlaceHolderAttributes::create(path, item._etag.toStdString(), item._fileId.toStdString(), item._size);
return setPlaceholderAttributes(attributes, item._modtime);
}
HydrationJob *OpenVFS::hydrateFile(const QByteArray &fileId, const QString &targetPath)
{
qCInfo(lcOpenVFS) << u"Requesting hydration for" << fileId;
if (_hydrationJobs.contains(fileId)) {
qCWarning(lcOpenVFS) << u"Ignoring hydration request for running hydration for fileId" << fileId;
return {};
}
if (auto attr = placeHolderAttributes(targetPath)) {
attr.state = ::OpenVFS::Constants::States::Hydrating;
if (auto res = setPlaceholderAttributes(attr); !res) {
qCWarning(lcOpenVFS) << u"Failed to set attributes for" << targetPath << res.error();
return nullptr;
}
} else {
qCWarning(lcOpenVFS) << u"Failed to get attributes for" << targetPath;
return nullptr;
}
HydrationJob *hydration = new HydrationJob(this, fileId, std::make_unique<QFile>(targetPath), nullptr);
hydration->setTargetFile(targetPath);
_hydrationJobs.insert(fileId, hydration);
connect(hydration, &HydrationJob::finished, this, &OpenVFS::slotHydrateJobFinished);
connect(hydration, &HydrationJob::error, this, [this, hydration](const QString &error) {
qCWarning(lcOpenVFS) << u"Hydration failed" << error;
this->_hydrationJobs.remove(hydration->fileId());
hydration->deleteLater();
});
return hydration;
}
bool OpenVFS::needsMetadataUpdate(const SyncFileItem &item)
{
const auto path = params().root() / item.localName();
// if the attributes do not exist we need to add them
return QFileInfo::exists(path.toString()) && !placeHolderAttributes(path);
}
bool OpenVFS::isDehydratedPlaceholder(const QString &filePath)
{
if (QFileInfo::exists(filePath)) {
return placeHolderAttributes(filePath).state == ::OpenVFS::Constants::States::DeHydrated;
}
return false;
}
LocalInfo OpenVFS::statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type)
{
if (type == ItemTypeFile) {
const auto attribs = placeHolderAttributes(path.path());
if (attribs.state == ::OpenVFS::Constants::States::DeHydrated) {
type = ItemTypeVirtualFile;
if (attribs.pinState == convertPinState(PinState::AlwaysLocal)) {
type = ItemTypeVirtualFileDownload;
}
} else {
if (attribs.pinState == convertPinState(PinState::OnlineOnly)) {
type = ItemTypeVirtualFileDehydration;
}
}
}
qCDebug(lcOpenVFS) << path.path().native() << Utility::enumToString(type);
return LocalInfo(path, type);
}
bool OpenVFS::setPinState(const QString &folderPath, PinState state)
{
const auto localPath = params().root() / folderPath;
qCDebug(lcOpenVFS) << localPath.toString() << state;
auto attribs = placeHolderAttributes(localPath);
if (!attribs) {
// the file is not yet converted
return false;
}
attribs.pinState = convertPinState(state);
if (!setPlaceholderAttributes(attribs)) {
return false;
}
return true;
}
Optional<PinState> OpenVFS::pinState(const QString &folderPath)
{
for (auto relativePath = FileSystem::Path::relative(folderPath).get();; relativePath = relativePath.parent_path()) {
const auto attributes = placeHolderAttributes(params().root() / relativePath);
if (!attributes) {
qCDebug(lcOpenVFS) << "Couldn't find pin state for placeholder file" << folderPath;
return {};
}
// if the state is inherited and we still have a parent path, retreive that instead.
if (attributes.pinState != ::OpenVFS::Constants::PinStates::Inherited || !relativePath.has_relative_path()) {
return convertPinState(attributes.pinState);
}
}
}
Vfs::AvailabilityResult OpenVFS::availability(const QString &folderPath)
{
const auto attribs = placeHolderAttributes(params().root() / folderPath);
if (attribs) {
switch (convertPinState(attribs.pinState)) {
case OCC::PinState::AlwaysLocal:
return VfsItemAvailability::AlwaysLocal;
case OCC::PinState::OnlineOnly:
return VfsItemAvailability::OnlineOnly;
case OCC::PinState::Inherited: {
switch (attribs.state) {
case ::OpenVFS::Constants::States::Hydrated:
return VfsItemAvailability::AllHydrated;
case ::OpenVFS::Constants::States::DeHydrated:
return VfsItemAvailability::AllDehydrated;
case ::OpenVFS::Constants::States::Hydrating:
return VfsItemAvailability::Mixed;
}
}
Q_UNREACHABLE();
case OCC::PinState::Unspecified:
[[fallthrough]];
case OCC::PinState::Excluded:
return VfsItemAvailability::Mixed;
};
} else {
return AvailabilityError::NoSuchItem;
}
return VfsItemAvailability::Mixed;
}
void OpenVFS::fileStatusChanged(const QString &systemFileName, SyncFileStatus fileStatus)
{
if (fileStatus.tag() == SyncFileStatus::StatusExcluded) {
const FileSystem::Path rel = FileSystem::Path(systemFileName)->lexically_relative(params().root());
setPinState(rel.toString(), PinState::Excluded);
return;
}
qCDebug(lcOpenVFS) << systemFileName << fileStatus;
}
} // namespace OCC
@@ -0,0 +1,85 @@
/*
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2025 OpenCloud GmbH and OpenCloud contributors
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "common/chronoelapsedtimer.h"
#include <QObject>
#include <QScopedPointer>
#include "common/plugin.h"
#include "common/result.h"
#include "vfs/vfs.h"
#include <QProcess>
namespace OCC {
class HydrationJob;
class OpenVFS : public Vfs
{
Q_OBJECT
public:
explicit OpenVFS(QObject *parent = nullptr);
~OpenVFS() override;
[[nodiscard]] Mode mode() const override;
void stop() override;
void unregisterFolder() override;
[[nodiscard]] bool socketApiPinStateActionsShown() const override;
Result<ConvertToPlaceholderResult, QString> updateMetadata(const SyncFileItem &syncItem, const QString &filePath, const QString &replacesFile) override;
// [[nodiscard]] bool isPlaceHolderInSync(const QString &filePath) const override { Q_UNUSED(filePath) return true; }
Result<void, QString> createPlaceholder(const SyncFileItem &item) override;
bool needsMetadataUpdate(const SyncFileItem &item) override;
bool isDehydratedPlaceholder(const QString &filePath) override;
LocalInfo statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) override;
bool setPinState(const QString &folderPath, PinState state) override;
Optional<PinState> pinState(const QString &folderPath) override;
AvailabilityResult availability(const QString &folderPath) override;
HydrationJob *hydrateFile(const QByteArray &fileId, const QString &targetPath) override;
Q_SIGNALS:
void finished(Result<void, QString>);
public Q_SLOTS:
void fileStatusChanged(const QString &systemFileName, OCC::SyncFileStatus fileStatus) override;
void slotHydrateJobFinished();
protected:
void startImpl(const VfsSetupParams &params) override;
private:
QMap<QByteArray, HydrationJob *> _hydrationJobs;
QPointer<QProcess> _openVfsProcess;
};
class OpenVfsPluginFactory : public QObject, public DefaultPluginFactory<OpenVFS>
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "eu.qsfera.PluginFactory" FILE "libsync/vfs/vfspluginmetadata.json")
Q_INTERFACES(OCC::PluginFactory)
public:
[[nodiscard]] bool checkAvailability() const override;
Result<void, QString> prepare(const QString &path, const QUuid &accountUuid) const override;
private:
mutable Utility::ChronoElapsedTimer _cacheTimer = false;
mutable QStringList _fuseMountCache;
};
} // namespace OCC