desktop/src/libsync/vfs/cfapi/vfs_cfapi.cpp

506 lines
18 KiB
C++

/*
* Copyright (C) by Kevin Ottens <kevin.ottens@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "vfs_cfapi.h"
#include <QDir>
#include <QFile>
#include "cfapiwrapper.h"
#include "hydrationjob.h"
#include "syncfileitem.h"
#include "filesystem.h"
#include "common/syncjournaldb.h"
#include "config.h"
#include <cfapi.h>
#include <comdef.h>
#include <QCoreApplication>
Q_LOGGING_CATEGORY(lcCfApi, "nextcloud.sync.vfs.cfapi", QtInfoMsg)
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;
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 (!QFileInfo::exists(shellExtensionDllPath)) {
Q_ASSERT(false);
qCWarning(lcCfApi) << "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);
}
}
}
}
namespace OCC {
class VfsCfApiPrivate
{
public:
QList<HydrationJob *> hydrationJobs;
cfapi::ConnectionKey connectionKey;
};
VfsCfApi::VfsCfApi(QObject *parent)
: Vfs(parent)
, d(new VfsCfApiPrivate)
{
}
VfsCfApi::~VfsCfApi() = default;
Vfs::Mode VfsCfApi::mode() const
{
return WindowsCfApi;
}
QString VfsCfApi::fileSuffix() const
{
return {};
}
void VfsCfApi::startImpl(const VfsSetupParams &params)
{
cfapi::registerShellExtension();
const auto localPath = QDir::toNativeSeparators(params.filesystemPath);
const auto registerResult = cfapi::registerSyncRoot(localPath, params.providerName, params.providerVersion, params.alias, params.navigationPaneClsid, params.displayName, params.account->displayName());
if (!registerResult) {
qCCritical(lcCfApi) << "Initialization failed, couldn't register sync root:" << registerResult.error();
return;
}
auto connectResult = cfapi::connectSyncRoot(localPath, this);
if (!connectResult) {
qCCritical(lcCfApi) << "Initialization failed, couldn't connect sync root:" << connectResult.error();
return;
}
d->connectionKey = *std::move(connectResult);
}
void VfsCfApi::stop()
{
const auto result = cfapi::disconnectSyncRoot(std::move(d->connectionKey));
if (!result) {
qCCritical(lcCfApi) << "Disconnect failed for" << QDir::toNativeSeparators(params().filesystemPath) << ":" << result.error();
}
}
void VfsCfApi::unregisterFolder()
{
const auto localPath = QDir::toNativeSeparators(params().filesystemPath);
const auto result = cfapi::unregisterSyncRoot(localPath, params().providerName, params().account->displayName());
if (!result) {
qCCritical(lcCfApi) << "Unregistration failed for" << localPath << ":" << result.error();
}
if (!cfapi::isAnySyncRoot(params().providerName, params().account->displayName())) {
cfapi::unregisterShellExtensions();
}
}
bool VfsCfApi::socketApiPinStateActionsShown() const
{
return true;
}
bool VfsCfApi::isHydrating() const
{
return !d->hydrationJobs.isEmpty();
}
Result<void, QString> VfsCfApi::updateMetadata(const QString &filePath, time_t modtime, qint64 size, const QByteArray &fileId)
{
const auto localPath = QDir::toNativeSeparators(filePath);
if (cfapi::handleForPath(localPath)) {
auto result = cfapi::updatePlaceholderInfo(localPath, modtime, size, fileId);
if (result) {
return {};
} else {
return result.error();
}
} else {
qCWarning(lcCfApi) << "Couldn't update metadata for non existing file" << localPath;
return {QStringLiteral("Couldn't update metadata")};
}
}
Result<void, QString> VfsCfApi::createPlaceholder(const SyncFileItem &item)
{
Q_ASSERT(params().filesystemPath.endsWith('/'));
const auto localPath = QDir::toNativeSeparators(params().filesystemPath + item._file);
const auto result = cfapi::createPlaceholderInfo(localPath, item._modtime, item._size, item._fileId);
return result;
}
Result<void, QString> VfsCfApi::dehydratePlaceholder(const SyncFileItem &item)
{
const auto localPath = QDir::toNativeSeparators(_setupParams.filesystemPath + item._file);
if (cfapi::handleForPath(localPath)) {
auto result = cfapi::dehydratePlaceholder(localPath, item._modtime, item._size, item._fileId);
if (result) {
return {};
} else {
return result.error();
}
} else {
qCWarning(lcCfApi) << "Couldn't update metadata for non existing file" << localPath;
return {QStringLiteral("Couldn't update metadata")};
}
}
Result<Vfs::ConvertToPlaceholderResult, QString> VfsCfApi::convertToPlaceholder(const QString &filename, const SyncFileItem &item, const QString &replacesFile)
{
if (item._type != ItemTypeDirectory && OCC::FileSystem::isLnkFile(filename)) {
qCInfo(lcCfApi) << "File \"" << filename << "\" is a Windows shortcut. Not converting it to a placeholder.";
return Vfs::ConvertToPlaceholderResult::Ok;
}
const auto localPath = QDir::toNativeSeparators(filename);
const auto replacesPath = QDir::toNativeSeparators(replacesFile);
if (cfapi::findPlaceholderInfo(localPath)) {
return cfapi::updatePlaceholderInfo(localPath, item._modtime, item._size, item._fileId, replacesPath);
} else {
return cfapi::convertToPlaceholder(localPath, item._modtime, item._size, item._fileId, replacesPath);
}
}
bool VfsCfApi::needsMetadataUpdate(const SyncFileItem &item)
{
return false;
}
bool VfsCfApi::isDehydratedPlaceholder(const QString &filePath)
{
const auto path = QDir::toNativeSeparators(filePath);
return cfapi::isSparseFile(path);
}
bool VfsCfApi::statTypeVirtualFile(csync_file_stat_t *stat, void *statData)
{
const auto ffd = static_cast<WIN32_FIND_DATA *>(statData);
const auto isDirectory = (ffd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
const auto isSparseFile = (ffd->dwFileAttributes & FILE_ATTRIBUTE_SPARSE_FILE) != 0;
const auto isPinned = (ffd->dwFileAttributes & FILE_ATTRIBUTE_PINNED) != 0;
const auto isUnpinned = (ffd->dwFileAttributes & FILE_ATTRIBUTE_UNPINNED) != 0;
const auto hasReparsePoint = (ffd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
const auto hasCloudTag = (ffd->dwReserved0 & IO_REPARSE_TAG_CLOUD) != 0;
const auto isWindowsShortcut = !isDirectory && FileSystem::isLnkFile(stat->path);
const auto isExcludeFile = !isDirectory && FileSystem::isExcludeFile(stat->path);
// It's a dir with a reparse point due to the placeholder info (hence the cloud tag)
// if we don't remove the reparse point flag the discovery will end up thinking
// it is a file... let's prevent it
if (isDirectory) {
if (hasReparsePoint && hasCloudTag) {
ffd->dwFileAttributes &= ~FILE_ATTRIBUTE_REPARSE_POINT;
}
return false;
} else if (isSparseFile && isPinned) {
stat->type = ItemTypeVirtualFileDownload;
return true;
} else if (!isSparseFile && isUnpinned && !isWindowsShortcut && !isExcludeFile) {
stat->type = ItemTypeVirtualFileDehydration;
return true;
} else if (isSparseFile) {
stat->type = ItemTypeVirtualFile;
return true;
}
return false;
}
bool VfsCfApi::setPinState(const QString &folderPath, PinState state)
{
const auto localPath = QDir::toNativeSeparators(params().filesystemPath + folderPath);
if (cfapi::setPinState(localPath, state, cfapi::Recurse)) {
return true;
} else {
return false;
}
}
Optional<PinState> VfsCfApi::pinState(const QString &folderPath)
{
const auto localPath = QDir::toNativeSeparators(params().filesystemPath + folderPath);
const auto info = cfapi::findPlaceholderInfo(localPath);
if (!info) {
qCWarning(lcCfApi) << "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);
const auto hydrationAndPinStates = computeRecursiveHydrationAndPinStates(folderPath, basePinState);
const auto pin = hydrationAndPinStates.pinState;
const auto hydrationStatus = hydrationAndPinStates.hydrationStatus;
if (hydrationStatus.hasDehydrated) {
if (hydrationStatus.hasHydrated)
return VfsItemAvailability::Mixed;
if (pin && *pin == PinState::OnlineOnly)
return VfsItemAvailability::OnlineOnly;
else
return VfsItemAvailability::AllDehydrated;
} else if (hydrationStatus.hasHydrated) {
if (pin && *pin == PinState::AlwaysLocal)
return VfsItemAvailability::AlwaysLocal;
else
return VfsItemAvailability::AllHydrated;
}
return AvailabilityError::NoSuchItem;
}
HydrationJob *VfsCfApi::findHydrationJob(const QString &requestId) const
{
// Find matching hydration job for request id
const auto hydrationJobsIter = std::find_if(d->hydrationJobs.cbegin(), d->hydrationJobs.cend(), [&](const HydrationJob *job) {
return job->requestId() == requestId;
});
if (hydrationJobsIter != d->hydrationJobs.cend()) {
return *hydrationJobsIter;
}
return nullptr;
}
void VfsCfApi::cancelHydration(const QString &requestId, const QString & /*path*/)
{
// Find matching hydration job for request id
const auto hydrationJob = findHydrationJob(requestId);
// If found, cancel it
if (hydrationJob) {
qCInfo(lcCfApi) << "Cancel hydration";
hydrationJob->cancel();
}
}
void VfsCfApi::requestHydration(const QString &requestId, const QString &path)
{
qCInfo(lcCfApi) << "Received request to hydrate" << path << requestId;
const auto root = QDir::toNativeSeparators(params().filesystemPath);
Q_ASSERT(path.startsWith(root));
const auto relativePath = QDir::fromNativeSeparators(path.mid(root.length()));
const auto journal = params().journal;
// Set in the database that we should download the file
SyncJournalFileRecord record;
journal->getFileRecord(relativePath, &record);
if (!record.isValid()) {
qCInfo(lcCfApi) << "Couldn't hydrate, did not find file in db";
emit hydrationRequestFailed(requestId);
return;
}
if (!record.isVirtualFile()) {
qCInfo(lcCfApi) << "Couldn't hydrate, the file is not virtual";
emit hydrationRequestFailed(requestId);
return;
}
// All good, let's hydrate now
scheduleHydrationJob(requestId, relativePath, record);
}
void VfsCfApi::fileStatusChanged(const QString &systemFileName, SyncFileStatus fileStatus)
{
Q_UNUSED(systemFileName);
Q_UNUSED(fileStatus);
}
void VfsCfApi::scheduleHydrationJob(const QString &requestId, const QString &folderPath, const SyncJournalFileRecord &record)
{
const auto jobAlreadyScheduled = std::any_of(std::cbegin(d->hydrationJobs), std::cend(d->hydrationJobs), [=](HydrationJob *job) {
return job->requestId() == requestId || job->folderPath() == folderPath;
});
if (jobAlreadyScheduled) {
qCWarning(lcCfApi) << "The OS submitted again a hydration request which is already on-going" << requestId << folderPath;
emit hydrationRequestFailed(requestId);
return;
}
if (d->hydrationJobs.isEmpty()) {
emit beginHydrating();
}
auto job = new HydrationJob(this);
job->setAccount(params().account);
job->setRemotePath(params().remotePath);
job->setLocalPath(params().filesystemPath);
job->setJournal(params().journal);
job->setRequestId(requestId);
job->setFolderPath(folderPath);
job->setIsEncryptedFile(record._isE2eEncrypted);
job->setE2eMangledName(record._e2eMangledName);
connect(job, &HydrationJob::finished, this, &VfsCfApi::onHydrationJobFinished);
d->hydrationJobs << job;
job->start();
emit hydrationRequestReady(requestId);
}
void VfsCfApi::onHydrationJobFinished(HydrationJob *job)
{
Q_ASSERT(d->hydrationJobs.contains(job));
qCInfo(lcCfApi) << "Hydration job finished" << job->requestId() << job->folderPath() << job->status();
emit hydrationRequestFinished(job->requestId());
}
int VfsCfApi::finalizeHydrationJob(const QString &requestId)
{
qCDebug(lcCfApi) << "Finalize hydration job" << requestId;
// Find matching hydration job for request id
const auto hydrationJob = findHydrationJob(requestId);
// If found, finalize it
if (hydrationJob) {
hydrationJob->finalize(this);
d->hydrationJobs.removeAll(hydrationJob);
hydrationJob->deleteLater();
if (d->hydrationJobs.isEmpty()) {
emit doneHydrating();
}
return hydrationJob->status();
}
return HydrationJob::Status::Error;
}
VfsCfApi::HydratationAndPinStates VfsCfApi::computeRecursiveHydrationAndPinStates(const QString &folderPath, const Optional<PinState> &basePinState)
{
Q_ASSERT(!folderPath.endsWith('/'));
QFileInfo info(params().filesystemPath + folderPath);
if (!info.exists()) {
return {};
}
const auto effectivePin = pinState(folderPath);
const auto pinResult = (!effectivePin && !basePinState) ? Optional<PinState>()
: (!effectivePin || !basePinState) ? PinState::Inherited
: (*effectivePin == *basePinState) ? *effectivePin
: PinState::Inherited;
if (info.isDir()) {
const auto dirState = HydratationAndPinStates {
pinResult,
{}
};
const auto dir = QDir(info.absoluteFilePath());
Q_ASSERT(dir.exists());
const auto children = dir.entryList();
return std::accumulate(std::cbegin(children), std::cend(children), dirState, [=](const HydratationAndPinStates &currentState, const QString &name) {
if (name == QStringLiteral("..") || name == QStringLiteral(".")) {
return currentState;
}
// if the folderPath.isEmpty() we don't want to end up having path "/example.file" because this will lead to double slash later, when appending to "SyncFolder/"
const auto path = folderPath.isEmpty() ? name : folderPath + '/' + name;
const auto states = computeRecursiveHydrationAndPinStates(path, currentState.pinState);
return HydratationAndPinStates {
states.pinState,
{
states.hydrationStatus.hasHydrated || currentState.hydrationStatus.hasHydrated,
states.hydrationStatus.hasDehydrated || currentState.hydrationStatus.hasDehydrated,
}
};
});
} else { // file case
const auto isDehydrated = isDehydratedPlaceholder(info.absoluteFilePath());
return {
pinResult,
{
!isDehydrated,
isDehydrated
}
};
}
}
} // namespace OCC