mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-26 23:28:14 +03:00
Merge pull request #2778 from nextcloud/windows_cfapi_backend_for_vfs
Windows cfapi backend for vfs
This commit is contained in:
commit
c501eed365
12 changed files with 2541 additions and 4 deletions
|
@ -1178,8 +1178,11 @@ DirectEditor* SocketApi::getDirectEditorForLocalFile(const QString &localFile)
|
|||
auto capabilities = fileData.folder->accountState()->account()->capabilities();
|
||||
|
||||
if (fileData.folder && fileData.folder->accountState()->isConnected()) {
|
||||
const auto record = fileData.journalRecord();
|
||||
const auto mimeMatchMode = record.isVirtualFile() ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault;
|
||||
|
||||
QMimeDatabase db;
|
||||
QMimeType type = db.mimeTypeForFile(localFile);
|
||||
QMimeType type = db.mimeTypeForFile(localFile, mimeMatchMode);
|
||||
|
||||
DirectEditor* editor = capabilities.getDirectEditorForMimetype(type);
|
||||
if (!editor) {
|
||||
|
|
|
@ -61,6 +61,16 @@ set(libsync_SRCS
|
|||
vfs/suffix/vfs_suffix.cpp
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
set(libsync_SRCS ${libsync_SRCS}
|
||||
vfs/cfapi/cfapiwrapper.cpp
|
||||
vfs/cfapi/hydrationjob.cpp
|
||||
vfs/cfapi/vfs_cfapi.cpp
|
||||
)
|
||||
add_definitions(-D_WIN32_WINNT=_WIN32_WINNT_WIN10)
|
||||
list(APPEND OS_SPECIFIC_LINK_LIBRARIES cldapi)
|
||||
endif()
|
||||
|
||||
if(TOKEN_AUTH_ONLY)
|
||||
set (libsync_SRCS ${libsync_SRCS} creds/tokencredentials.cpp)
|
||||
else()
|
||||
|
|
|
@ -860,8 +860,8 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
|
|||
auto postProcessLocalNew = [item, localEntry, this]() {
|
||||
if (localEntry.isVirtualFile) {
|
||||
// Remove the spurious file if it looks like a placeholder file
|
||||
// (we know placeholder files contain " ")
|
||||
if (localEntry.size <= 1) {
|
||||
// (we know placeholder files contain " ", but only in the suffix case)
|
||||
if (localEntry.size <= 1 || !isVfsWithSuffix()) {
|
||||
qCWarning(lcDisco) << "Wiping virtual file without db entry for" << _currentFolder._local + "/" + localEntry.name;
|
||||
item->_instruction = CSYNC_INSTRUCTION_REMOVE;
|
||||
item->_direction = SyncFileItem::Down;
|
||||
|
|
|
@ -738,7 +738,8 @@ QPixmap Theme::createColorAwarePixmap(const QString &name)
|
|||
|
||||
bool Theme::showVirtualFilesOption() const
|
||||
{
|
||||
return ConfigFile().showExperimentalOptions();
|
||||
const auto vfsMode = bestAvailableVfsMode();
|
||||
return ConfigFile().showExperimentalOptions() || vfsMode == Vfs::WindowsCfApi;
|
||||
}
|
||||
|
||||
} // end namespace client
|
||||
|
|
501
src/libsync/vfs/cfapi/cfapiwrapper.cpp
Normal file
501
src/libsync/vfs/cfapi/cfapiwrapper.cpp
Normal file
|
@ -0,0 +1,501 @@
|
|||
/*
|
||||
* 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 "cfapiwrapper.h"
|
||||
|
||||
#include "common/utility.h"
|
||||
#include "hydrationjob.h"
|
||||
#include "vfs_cfapi.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QLocalSocket>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
#include <cfapi.h>
|
||||
#include <comdef.h>
|
||||
#include <ntstatus.h>
|
||||
|
||||
Q_LOGGING_CATEGORY(lcCfApiWrapper, "nextcloud.sync.vfs.cfapi.wrapper", QtInfoMsg)
|
||||
|
||||
#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 {
|
||||
void cfApiSendTransferInfo(const CF_CONNECTION_KEY &connectionKey, const CF_TRANSFER_KEY &transferKey, NTSTATUS status, void *buffer, qint64 offset, qint64 length)
|
||||
{
|
||||
|
||||
CF_OPERATION_INFO opInfo = { 0 };
|
||||
CF_OPERATION_PARAMETERS opParams = { 0 };
|
||||
|
||||
opInfo.StructSize = sizeof(opInfo);
|
||||
opInfo.Type = CF_OPERATION_TYPE_TRANSFER_DATA;
|
||||
opInfo.ConnectionKey = connectionKey;
|
||||
opInfo.TransferKey = transferKey;
|
||||
opParams.ParamSize = CF_SIZE_OF_OP_PARAM(TransferData);
|
||||
opParams.TransferData.CompletionStatus = status;
|
||||
opParams.TransferData.Buffer = buffer;
|
||||
opParams.TransferData.Offset.QuadPart = offset;
|
||||
opParams.TransferData.Length.QuadPart = length;
|
||||
|
||||
const qint64 result = CfExecute(&opInfo, &opParams);
|
||||
Q_ASSERT(result == S_OK);
|
||||
if (result != S_OK) {
|
||||
qCCritical(lcCfApiWrapper) << "Couldn't send transfer info" << QString::number(transferKey.QuadPart, 16) << ":" << _com_error(result).ErrorMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void CALLBACK cfApiFetchDataCallback(const CF_CALLBACK_INFO *callbackInfo, const CF_CALLBACK_PARAMETERS *callbackParameters)
|
||||
{
|
||||
const auto sendTransferError = [=] {
|
||||
cfApiSendTransferInfo(callbackInfo->ConnectionKey,
|
||||
callbackInfo->TransferKey,
|
||||
STATUS_UNSUCCESSFUL,
|
||||
nullptr,
|
||||
callbackParameters->FetchData.RequiredFileOffset.QuadPart,
|
||||
callbackParameters->FetchData.RequiredLength.QuadPart);
|
||||
};
|
||||
|
||||
const auto sendTransferInfo = [=](QByteArray &data, qint64 offset) {
|
||||
cfApiSendTransferInfo(callbackInfo->ConnectionKey,
|
||||
callbackInfo->TransferKey,
|
||||
STATUS_SUCCESS,
|
||||
data.data(),
|
||||
offset,
|
||||
data.length());
|
||||
};
|
||||
|
||||
auto vfs = reinterpret_cast<OCC::VfsCfApi *>(callbackInfo->CallbackContext);
|
||||
Q_ASSERT(vfs->metaObject()->className() == QByteArrayLiteral("OCC::VfsCfApi"));
|
||||
const auto path = QString(QString::fromWCharArray(callbackInfo->VolumeDosName) + QString::fromWCharArray(callbackInfo->NormalizedPath));
|
||||
const auto requestId = QString::number(callbackInfo->TransferKey.QuadPart, 16);
|
||||
|
||||
const auto invokeResult = QMetaObject::invokeMethod(vfs, [=] { vfs->requestHydration(requestId, path); }, Qt::QueuedConnection);
|
||||
if (!invokeResult) {
|
||||
qCCritical(lcCfApiWrapper) << "Failed to trigger hydration for" << path << requestId;
|
||||
sendTransferError();
|
||||
return;
|
||||
}
|
||||
|
||||
// Block and wait for vfs to signal back the hydration is ready
|
||||
bool hydrationRequestResult = false;
|
||||
QEventLoop loop;
|
||||
QObject::connect(vfs, &OCC::VfsCfApi::hydrationRequestReady, &loop, [&](const QString &id) {
|
||||
if (requestId == id) {
|
||||
hydrationRequestResult = true;
|
||||
loop.quit();
|
||||
}
|
||||
});
|
||||
QObject::connect(vfs, &OCC::VfsCfApi::hydrationRequestFailed, &loop, [&](const QString &id) {
|
||||
if (requestId == id) {
|
||||
hydrationRequestResult = false;
|
||||
loop.quit();
|
||||
}
|
||||
});
|
||||
loop.exec();
|
||||
QObject::disconnect(vfs, nullptr, &loop, nullptr);
|
||||
qCInfo(lcCfApiWrapper) << "VFS replied for hydration of" << path << requestId << "status was:" << hydrationRequestResult;
|
||||
|
||||
if (!hydrationRequestResult) {
|
||||
sendTransferError();
|
||||
return;
|
||||
}
|
||||
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer(requestId);
|
||||
const auto connectResult = socket.waitForConnected();
|
||||
if (!connectResult) {
|
||||
qCWarning(lcCfApiWrapper) << "Couldn't connect the socket" << requestId << socket.error() << socket.errorString();
|
||||
sendTransferError();
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 offset = 0;
|
||||
|
||||
QObject::connect(&socket, &QLocalSocket::readyRead, &loop, [&] {
|
||||
auto data = socket.readAll();
|
||||
if (data.isEmpty()) {
|
||||
qCWarning(lcCfApiWrapper) << "Unexpected empty data received" << requestId;
|
||||
sendTransferError();
|
||||
loop.quit();
|
||||
return;
|
||||
}
|
||||
sendTransferInfo(data, offset);
|
||||
offset += data.length();
|
||||
});
|
||||
|
||||
QObject::connect(vfs, &OCC::VfsCfApi::hydrationRequestFinished, &loop, [&](const QString &id, int s) {
|
||||
if (requestId == id) {
|
||||
const auto status = static_cast<OCC::HydrationJob::Status>(s);
|
||||
qCInfo(lcCfApiWrapper) << "Hydration done for" << path << requestId << status;
|
||||
if (status != OCC::HydrationJob::Success) {
|
||||
sendTransferError();
|
||||
}
|
||||
loop.quit();
|
||||
}
|
||||
});
|
||||
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
CF_CALLBACK_REGISTRATION cfApiCallbacks[] = {
|
||||
{ CF_CALLBACK_TYPE_FETCH_DATA, cfApiFetchDataCallback },
|
||||
CF_CALLBACK_REGISTRATION_END
|
||||
};
|
||||
|
||||
DWORD sizeToDWORD(size_t size)
|
||||
{
|
||||
return OCC::Utility::convertSizeToDWORD(size);
|
||||
}
|
||||
|
||||
void deletePlaceholderInfo(CF_PLACEHOLDER_BASIC_INFO *info)
|
||||
{
|
||||
auto byte = reinterpret_cast<char *>(info);
|
||||
delete[] byte;
|
||||
}
|
||||
|
||||
std::wstring pathForHandle(const OCC::CfApiWrapper::FileHandle &handle)
|
||||
{
|
||||
wchar_t buffer[MAX_PATH];
|
||||
const qint64 result = GetFinalPathNameByHandle(handle.get(), buffer, MAX_PATH, VOLUME_NAME_DOS);
|
||||
Q_ASSERT(result < MAX_PATH);
|
||||
return std::wstring(buffer);
|
||||
}
|
||||
|
||||
OCC::PinState cfPinStateToPinState(CF_PIN_STATE state)
|
||||
{
|
||||
switch (state) {
|
||||
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;
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
return OCC::PinState::Inherited;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
return CF_PIN_STATE_UNSPECIFIED;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
return CF_SET_PIN_FLAG_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
OCC::CfApiWrapper::ConnectionKey::ConnectionKey()
|
||||
: _data(new CF_CONNECTION_KEY, [](void *p) { delete reinterpret_cast<CF_CONNECTION_KEY *>(p); })
|
||||
{
|
||||
}
|
||||
|
||||
OCC::CfApiWrapper::FileHandle::FileHandle()
|
||||
: _data(nullptr, [](void *) {})
|
||||
{
|
||||
}
|
||||
|
||||
OCC::CfApiWrapper::FileHandle::FileHandle(void *data, Deleter deleter)
|
||||
: _data(data, deleter)
|
||||
{
|
||||
}
|
||||
|
||||
OCC::CfApiWrapper::PlaceHolderInfo::PlaceHolderInfo()
|
||||
: _data(nullptr, [](CF_PLACEHOLDER_BASIC_INFO *) {})
|
||||
{
|
||||
}
|
||||
|
||||
OCC::CfApiWrapper::PlaceHolderInfo::PlaceHolderInfo(CF_PLACEHOLDER_BASIC_INFO *data, Deleter deleter)
|
||||
: _data(data, deleter)
|
||||
{
|
||||
}
|
||||
|
||||
OCC::Optional<OCC::PinStateEnums::PinState> OCC::CfApiWrapper::PlaceHolderInfo::pinState() const
|
||||
{
|
||||
Q_ASSERT(_data);
|
||||
if (!_data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return cfPinStateToPinState(_data->PinState);
|
||||
}
|
||||
|
||||
OCC::Result<void, QString> OCC::CfApiWrapper::registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion)
|
||||
{
|
||||
const auto p = path.toStdWString();
|
||||
const auto name = providerName.toStdWString();
|
||||
const auto version = providerVersion.toStdWString();
|
||||
|
||||
CF_SYNC_REGISTRATION info;
|
||||
info.ProviderName = name.data();
|
||||
info.ProviderVersion = version.data();
|
||||
info.SyncRootIdentity = nullptr;
|
||||
info.SyncRootIdentityLength = 0;
|
||||
info.FileIdentity = nullptr;
|
||||
info.FileIdentityLength = 0;
|
||||
|
||||
CF_SYNC_POLICIES policies;
|
||||
policies.Hydration.Primary = CF_HYDRATION_POLICY_FULL;
|
||||
policies.Hydration.Modifier = CF_HYDRATION_POLICY_MODIFIER_NONE;
|
||||
policies.Population.Primary = CF_POPULATION_POLICY_ALWAYS_FULL;
|
||||
policies.Population.Modifier = CF_POPULATION_POLICY_MODIFIER_NONE;
|
||||
policies.InSync = CF_INSYNC_POLICY_PRESERVE_INSYNC_FOR_SYNC_ENGINE;
|
||||
policies.HardLink = CF_HARDLINK_POLICY_NONE;
|
||||
|
||||
const qint64 result = CfRegisterSyncRoot(p.data(), &info, &policies, CF_REGISTER_FLAG_UPDATE);
|
||||
Q_ASSERT(result == S_OK);
|
||||
if (result != S_OK) {
|
||||
return QString::fromWCharArray(_com_error(result).ErrorMessage());
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
OCC::Result<void, QString> OCC::CfApiWrapper::unegisterSyncRoot(const QString &path)
|
||||
{
|
||||
const auto p = path.toStdWString();
|
||||
const qint64 result = CfUnregisterSyncRoot(p.data());
|
||||
Q_ASSERT(result == S_OK);
|
||||
if (result != S_OK) {
|
||||
return QString::fromWCharArray(_com_error(result).ErrorMessage());
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
OCC::Result<OCC::CfApiWrapper::ConnectionKey, QString> OCC::CfApiWrapper::connectSyncRoot(const QString &path, OCC::VfsCfApi *context)
|
||||
{
|
||||
auto key = ConnectionKey();
|
||||
const auto p = path.toStdWString();
|
||||
const qint64 result = CfConnectSyncRoot(p.data(),
|
||||
cfApiCallbacks,
|
||||
context,
|
||||
CF_CONNECT_FLAG_REQUIRE_PROCESS_INFO | CF_CONNECT_FLAG_REQUIRE_FULL_FILE_PATH,
|
||||
static_cast<CF_CONNECTION_KEY *>(key.get()));
|
||||
Q_ASSERT(result == S_OK);
|
||||
if (result != S_OK) {
|
||||
return QString::fromWCharArray(_com_error(result).ErrorMessage());
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
OCC::Result<void, QString> OCC::CfApiWrapper::disconnectSyncRoot(ConnectionKey &&key)
|
||||
{
|
||||
const qint64 result = CfDisconnectSyncRoot(*static_cast<CF_CONNECTION_KEY *>(key.get()));
|
||||
Q_ASSERT(result == S_OK);
|
||||
if (result != S_OK) {
|
||||
return QString::fromWCharArray(_com_error(result).ErrorMessage());
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
bool OCC::CfApiWrapper::isSparseFile(const QString &path)
|
||||
{
|
||||
const auto p = path.toStdWString();
|
||||
const auto attributes = GetFileAttributes(p.data());
|
||||
return (attributes & FILE_ATTRIBUTE_SPARSE_FILE) != 0;
|
||||
}
|
||||
|
||||
OCC::CfApiWrapper::FileHandle OCC::CfApiWrapper::handleForPath(const QString &path)
|
||||
{
|
||||
if (QFileInfo(path).isDir()) {
|
||||
HANDLE handle = nullptr;
|
||||
const qint64 openResult = CfOpenFileWithOplock(path.toStdWString().data(), CF_OPEN_FILE_FLAG_NONE, &handle);
|
||||
if (openResult == S_OK) {
|
||||
return {handle, CfCloseHandle};
|
||||
}
|
||||
} else {
|
||||
const auto handle = CreateFile(path.toStdWString().data(), 0, 0, nullptr,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (handle != INVALID_HANDLE_VALUE) {
|
||||
return {handle, [](HANDLE h) { CloseHandle(h); }};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
OCC::CfApiWrapper::PlaceHolderInfo OCC::CfApiWrapper::findPlaceholderInfo(const FileHandle &handle)
|
||||
{
|
||||
Q_ASSERT(handle);
|
||||
|
||||
constexpr auto fileIdMaxLength = 128;
|
||||
const auto infoSize = sizeof(CF_PLACEHOLDER_BASIC_INFO) + fileIdMaxLength;
|
||||
auto info = PlaceHolderInfo(reinterpret_cast<CF_PLACEHOLDER_BASIC_INFO *>(new char[infoSize]), deletePlaceholderInfo);
|
||||
const qint64 result = CfGetPlaceholderInfo(handle.get(), CF_PLACEHOLDER_INFO_BASIC, info.get(), sizeToDWORD(infoSize), nullptr);
|
||||
|
||||
if (result == S_OK) {
|
||||
return info;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
OCC::Result<void, QString> OCC::CfApiWrapper::setPinState(const FileHandle &handle, PinState state, SetPinRecurseMode mode)
|
||||
{
|
||||
const auto cfState = pinStateToCfPinState(state);
|
||||
const auto flags = pinRecurseModeToCfSetPinFlags(mode);
|
||||
|
||||
const qint64 result = CfSetPinState(handle.get(), cfState, flags, nullptr);
|
||||
if (result == S_OK) {
|
||||
return {};
|
||||
} else {
|
||||
qCWarning(lcCfApiWrapper) << "Couldn't set pin state" << state << "for" << pathForHandle(handle) << "with recurse mode" << mode << ":" << _com_error(result).ErrorMessage();
|
||||
return "Couldn't set pin state";
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
const auto fileIdentity = QString::fromUtf8(fileId).toStdWString();
|
||||
|
||||
CF_PLACEHOLDER_CREATE_INFO cloudEntry;
|
||||
cloudEntry.FileIdentity = fileIdentity.data();
|
||||
const auto fileIdentitySize = (fileIdentity.length() + 1) * sizeof(wchar_t);
|
||||
cloudEntry.FileIdentityLength = sizeToDWORD(fileIdentitySize);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const qint64 result = CfCreatePlaceholders(localBasePath.data(), &cloudEntry, 1, CF_CREATE_FLAG_NONE, nullptr);
|
||||
if (result != S_OK) {
|
||||
qCWarning(lcCfApiWrapper) << "Couldn't create placeholder info for" << path << ":" << _com_error(result).ErrorMessage();
|
||||
return "Couldn't create placeholder info";
|
||||
}
|
||||
|
||||
const auto parentHandle = handleForPath(QDir::toNativeSeparators(QFileInfo(path).absolutePath()));
|
||||
const auto parentInfo = findPlaceholderInfo(parentHandle);
|
||||
const auto state = parentInfo && parentInfo->PinState == CF_PIN_STATE_UNPINNED ? CF_PIN_STATE_UNPINNED : CF_PIN_STATE_INHERIT;
|
||||
|
||||
const auto handle = handleForPath(path);
|
||||
if (!setPinState(handle, cfPinStateToPinState(state), NoRecurse)) {
|
||||
return "Couldn't set the default inherit pin state";
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
OCC::Result<void, QString> OCC::CfApiWrapper::updatePlaceholderInfo(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath)
|
||||
{
|
||||
Q_ASSERT(handle);
|
||||
|
||||
const auto info = replacesPath.isEmpty() ? findPlaceholderInfo(handle)
|
||||
: findPlaceholderInfo(handleForPath(replacesPath));
|
||||
if (!info) {
|
||||
return "Can't update non existing placeholder info";
|
||||
}
|
||||
|
||||
const auto previousPinState = cfPinStateToPinState(info->PinState);
|
||||
const auto fileIdentity = QString::fromUtf8(fileId).toStdWString();
|
||||
const auto fileIdentitySize = (fileIdentity.length() + 1) * sizeof(wchar_t);
|
||||
|
||||
CF_FS_METADATA metadata;
|
||||
metadata.FileSize.QuadPart = size;
|
||||
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &metadata.BasicInfo.CreationTime);
|
||||
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &metadata.BasicInfo.LastWriteTime);
|
||||
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &metadata.BasicInfo.LastAccessTime);
|
||||
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &metadata.BasicInfo.ChangeTime);
|
||||
|
||||
const qint64 result = CfUpdatePlaceholder(handle.get(), &metadata,
|
||||
fileIdentity.data(), sizeToDWORD(fileIdentitySize),
|
||||
nullptr, 0, CF_UPDATE_FLAG_NONE, nullptr, nullptr);
|
||||
|
||||
if (result != S_OK) {
|
||||
qCWarning(lcCfApiWrapper) << "Couldn't update placeholder info for" << pathForHandle(handle) << ":" << _com_error(result).ErrorMessage();
|
||||
return "Couldn't update placeholder info";
|
||||
}
|
||||
|
||||
// Pin state tends to be lost on updates, so restore it every time
|
||||
if (!setPinState(handle, previousPinState, NoRecurse)) {
|
||||
return "Couldn't restore pin state";
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
OCC::Result<void, QString> OCC::CfApiWrapper::convertToPlaceholder(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath)
|
||||
{
|
||||
Q_ASSERT(handle);
|
||||
|
||||
const auto fileIdentity = QString::fromUtf8(fileId).toStdWString();
|
||||
const auto fileIdentitySize = (fileIdentity.length() + 1) * sizeof(wchar_t);
|
||||
const qint64 result = CfConvertToPlaceholder(handle.get(), fileIdentity.data(), sizeToDWORD(fileIdentitySize), CF_CONVERT_FLAG_NONE, nullptr, nullptr);
|
||||
Q_ASSERT(result == S_OK);
|
||||
if (result != S_OK) {
|
||||
qCCritical(lcCfApiWrapper) << "Couldn't convert to placeholder" << pathForHandle(handle) << ":" << _com_error(result).ErrorMessage();
|
||||
return "Couldn't convert to placeholder";
|
||||
}
|
||||
|
||||
const auto originalHandle = handleForPath(replacesPath);
|
||||
const auto originalInfo = originalHandle ? findPlaceholderInfo(originalHandle) : PlaceHolderInfo(nullptr, deletePlaceholderInfo);
|
||||
if (!originalInfo) {
|
||||
const auto stateResult = setPinState(handle, PinState::Inherited, NoRecurse);
|
||||
Q_ASSERT(stateResult);
|
||||
return stateResult;
|
||||
} else {
|
||||
const auto state = cfPinStateToPinState(originalInfo->PinState);
|
||||
const auto stateResult = setPinState(handle, state, NoRecurse);
|
||||
Q_ASSERT(stateResult);
|
||||
return stateResult;
|
||||
}
|
||||
}
|
99
src/libsync/vfs/cfapi/cfapiwrapper.h
Normal file
99
src/libsync/vfs/cfapi/cfapiwrapper.h
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "owncloudlib.h"
|
||||
#include "common/pinstate.h"
|
||||
#include "common/result.h"
|
||||
|
||||
struct CF_PLACEHOLDER_BASIC_INFO;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class VfsCfApi;
|
||||
|
||||
namespace CfApiWrapper
|
||||
{
|
||||
|
||||
class OWNCLOUDSYNC_EXPORT ConnectionKey
|
||||
{
|
||||
public:
|
||||
ConnectionKey();
|
||||
inline void *get() const { return _data.get(); }
|
||||
|
||||
private:
|
||||
std::unique_ptr<void, void(*)(void *)> _data;
|
||||
};
|
||||
|
||||
class OWNCLOUDSYNC_EXPORT FileHandle
|
||||
{
|
||||
public:
|
||||
using Deleter = void (*)(void *);
|
||||
|
||||
FileHandle();
|
||||
FileHandle(void *data, Deleter deleter);
|
||||
|
||||
inline void *get() const { return _data.get(); }
|
||||
inline explicit operator bool() const noexcept { return static_cast<bool>(_data); }
|
||||
|
||||
private:
|
||||
std::unique_ptr<void, void(*)(void *)> _data;
|
||||
};
|
||||
|
||||
class OWNCLOUDSYNC_EXPORT PlaceHolderInfo
|
||||
{
|
||||
public:
|
||||
using Deleter = void (*)(CF_PLACEHOLDER_BASIC_INFO *);
|
||||
|
||||
PlaceHolderInfo();
|
||||
PlaceHolderInfo(CF_PLACEHOLDER_BASIC_INFO *data, Deleter deleter);
|
||||
|
||||
inline CF_PLACEHOLDER_BASIC_INFO *get() const noexcept { return _data.get(); }
|
||||
inline CF_PLACEHOLDER_BASIC_INFO *operator->() const noexcept { return _data.get(); }
|
||||
inline explicit operator bool() const noexcept { return static_cast<bool>(_data); }
|
||||
|
||||
Optional<PinState> pinState() const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<CF_PLACEHOLDER_BASIC_INFO, Deleter> _data;
|
||||
};
|
||||
|
||||
OWNCLOUDSYNC_EXPORT Result<void, QString> registerSyncRoot(const QString &path, const QString &providerName, const QString &providerVersion);
|
||||
OWNCLOUDSYNC_EXPORT Result<void, QString> unegisterSyncRoot(const QString &path);
|
||||
|
||||
OWNCLOUDSYNC_EXPORT Result<ConnectionKey, QString> connectSyncRoot(const QString &path, VfsCfApi *context);
|
||||
OWNCLOUDSYNC_EXPORT Result<void, QString> disconnectSyncRoot(ConnectionKey &&key);
|
||||
|
||||
OWNCLOUDSYNC_EXPORT bool isSparseFile(const QString &path);
|
||||
|
||||
OWNCLOUDSYNC_EXPORT FileHandle handleForPath(const QString &path);
|
||||
|
||||
OWNCLOUDSYNC_EXPORT PlaceHolderInfo findPlaceholderInfo(const FileHandle &handle);
|
||||
|
||||
enum SetPinRecurseMode {
|
||||
NoRecurse = 0,
|
||||
Recurse,
|
||||
ChildrenOnly
|
||||
};
|
||||
|
||||
OWNCLOUDSYNC_EXPORT Result<void, QString> setPinState(const FileHandle &handle, PinState state, SetPinRecurseMode mode);
|
||||
OWNCLOUDSYNC_EXPORT Result<void, QString> createPlaceholderInfo(const QString &path, time_t modtime, qint64 size, const QByteArray &fileId);
|
||||
OWNCLOUDSYNC_EXPORT Result<void, QString> updatePlaceholderInfo(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath = QString());
|
||||
OWNCLOUDSYNC_EXPORT Result<void, QString> convertToPlaceholder(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath);
|
||||
|
||||
}
|
||||
|
||||
} // namespace OCC
|
167
src/libsync/vfs/cfapi/hydrationjob.cpp
Normal file
167
src/libsync/vfs/cfapi/hydrationjob.cpp
Normal file
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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 "hydrationjob.h"
|
||||
|
||||
#include "common/syncjournaldb.h"
|
||||
#include "propagatedownload.h"
|
||||
|
||||
#include <QLocalServer>
|
||||
#include <QLocalSocket>
|
||||
|
||||
Q_LOGGING_CATEGORY(lcHydration, "nextcloud.sync.vfs.hydrationjob", QtInfoMsg)
|
||||
|
||||
OCC::HydrationJob::HydrationJob(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
connect(this, &HydrationJob::finished, this, &HydrationJob::deleteLater);
|
||||
}
|
||||
|
||||
OCC::AccountPtr OCC::HydrationJob::account() const
|
||||
{
|
||||
return _account;
|
||||
}
|
||||
|
||||
void OCC::HydrationJob::setAccount(const AccountPtr &account)
|
||||
{
|
||||
_account = account;
|
||||
}
|
||||
|
||||
QString OCC::HydrationJob::remotePath() const
|
||||
{
|
||||
return _remotePath;
|
||||
}
|
||||
|
||||
void OCC::HydrationJob::setRemotePath(const QString &remotePath)
|
||||
{
|
||||
_remotePath = remotePath;
|
||||
}
|
||||
|
||||
QString OCC::HydrationJob::localPath() const
|
||||
{
|
||||
return _localPath;
|
||||
}
|
||||
|
||||
void OCC::HydrationJob::setLocalPath(const QString &localPath)
|
||||
{
|
||||
_localPath = localPath;
|
||||
}
|
||||
|
||||
OCC::SyncJournalDb *OCC::HydrationJob::journal() const
|
||||
{
|
||||
return _journal;
|
||||
}
|
||||
|
||||
void OCC::HydrationJob::setJournal(SyncJournalDb *journal)
|
||||
{
|
||||
_journal = journal;
|
||||
}
|
||||
|
||||
QString OCC::HydrationJob::requestId() const
|
||||
{
|
||||
return _requestId;
|
||||
}
|
||||
|
||||
void OCC::HydrationJob::setRequestId(const QString &requestId)
|
||||
{
|
||||
_requestId = requestId;
|
||||
}
|
||||
|
||||
QString OCC::HydrationJob::folderPath() const
|
||||
{
|
||||
return _folderPath;
|
||||
}
|
||||
|
||||
void OCC::HydrationJob::setFolderPath(const QString &folderPath)
|
||||
{
|
||||
_folderPath = folderPath;
|
||||
}
|
||||
|
||||
OCC::HydrationJob::Status OCC::HydrationJob::status() const
|
||||
{
|
||||
return _status;
|
||||
}
|
||||
|
||||
void OCC::HydrationJob::start()
|
||||
{
|
||||
Q_ASSERT(_account);
|
||||
Q_ASSERT(_journal);
|
||||
Q_ASSERT(!_remotePath.isEmpty() && !_localPath.isEmpty());
|
||||
Q_ASSERT(!_requestId.isEmpty() && !_folderPath.isEmpty());
|
||||
|
||||
Q_ASSERT(_remotePath.endsWith('/'));
|
||||
Q_ASSERT(_localPath.endsWith('/'));
|
||||
Q_ASSERT(!_folderPath.startsWith('/'));
|
||||
|
||||
_server = new QLocalServer(this);
|
||||
const auto listenResult = _server->listen(_requestId);
|
||||
if (!listenResult) {
|
||||
qCCritical(lcHydration) << "Couldn't get server to listen" << _requestId << _localPath << _folderPath;
|
||||
emitFinished(Error);
|
||||
return;
|
||||
}
|
||||
|
||||
qCInfo(lcHydration) << "Server ready, waiting for connections" << _requestId << _localPath << _folderPath;
|
||||
connect(_server, &QLocalServer::newConnection, this, &HydrationJob::onNewConnection);
|
||||
}
|
||||
|
||||
void OCC::HydrationJob::emitFinished(Status status)
|
||||
{
|
||||
_status = status;
|
||||
if (status == Success) {
|
||||
_socket->disconnectFromServer();
|
||||
connect(_socket, &QLocalSocket::disconnected, this, [=]{
|
||||
_socket->close();
|
||||
emit finished(this);
|
||||
});
|
||||
} else {
|
||||
_socket->close();
|
||||
emit finished(this);
|
||||
}
|
||||
}
|
||||
|
||||
void OCC::HydrationJob::onNewConnection()
|
||||
{
|
||||
Q_ASSERT(!_socket);
|
||||
Q_ASSERT(!_job);
|
||||
|
||||
qCInfo(lcHydration) << "Got new connection starting GETFileJob" << _requestId << _folderPath;
|
||||
_socket = _server->nextPendingConnection();
|
||||
_job = new GETFileJob(_account, _remotePath + _folderPath, _socket, {}, {}, 0, this);
|
||||
connect(_job, &GETFileJob::finishedSignal, this, &HydrationJob::onGetFinished);
|
||||
_job->start();
|
||||
}
|
||||
|
||||
void OCC::HydrationJob::onGetFinished()
|
||||
{
|
||||
qCInfo(lcHydration) << "GETFileJob finished" << _requestId << _folderPath << _job->reply()->error();
|
||||
|
||||
if (_job->reply()->error()) {
|
||||
emitFinished(Error);
|
||||
return;
|
||||
}
|
||||
|
||||
SyncJournalFileRecord record;
|
||||
_journal->getFileRecord(_folderPath, &record);
|
||||
Q_ASSERT(record.isValid());
|
||||
if (!record.isValid()) {
|
||||
qCWarning(lcHydration) << "Couldn't find record to update after hydration" << _requestId << _folderPath;
|
||||
emitFinished(Error);
|
||||
return;
|
||||
}
|
||||
|
||||
record._type = ItemTypeFile;
|
||||
_journal->setFileRecord(record);
|
||||
emitFinished(Success);
|
||||
}
|
84
src/libsync/vfs/cfapi/hydrationjob.h
Normal file
84
src/libsync/vfs/cfapi/hydrationjob.h
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "account.h"
|
||||
|
||||
class QLocalServer;
|
||||
class QLocalSocket;
|
||||
|
||||
namespace OCC {
|
||||
class GETFileJob;
|
||||
class SyncJournalDb;
|
||||
|
||||
class OWNCLOUDSYNC_EXPORT HydrationJob : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Status {
|
||||
Success = 0,
|
||||
Error,
|
||||
};
|
||||
Q_ENUM(Status)
|
||||
|
||||
explicit HydrationJob(QObject *parent = nullptr);
|
||||
|
||||
AccountPtr account() const;
|
||||
void setAccount(const AccountPtr &account);
|
||||
|
||||
QString remotePath() const;
|
||||
void setRemotePath(const QString &remotePath);
|
||||
|
||||
QString localPath() const;
|
||||
void setLocalPath(const QString &localPath);
|
||||
|
||||
SyncJournalDb *journal() const;
|
||||
void setJournal(SyncJournalDb *journal);
|
||||
|
||||
QString requestId() const;
|
||||
void setRequestId(const QString &requestId);
|
||||
|
||||
QString folderPath() const;
|
||||
void setFolderPath(const QString &folderPath);
|
||||
|
||||
Status status() const;
|
||||
|
||||
void start();
|
||||
|
||||
signals:
|
||||
void finished(HydrationJob *job);
|
||||
|
||||
private:
|
||||
void emitFinished(Status status);
|
||||
|
||||
void onNewConnection();
|
||||
void onGetFinished();
|
||||
|
||||
AccountPtr _account;
|
||||
QString _remotePath;
|
||||
QString _localPath;
|
||||
SyncJournalDb *_journal = nullptr;
|
||||
|
||||
QString _requestId;
|
||||
QString _folderPath;
|
||||
|
||||
QLocalServer *_server = nullptr;
|
||||
QLocalSocket *_socket = nullptr;
|
||||
GETFileJob *_job = nullptr;
|
||||
Status _status = Success;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
394
src/libsync/vfs/cfapi/vfs_cfapi.cpp
Normal file
394
src/libsync/vfs/cfapi/vfs_cfapi.cpp
Normal file
|
@ -0,0 +1,394 @@
|
|||
/*
|
||||
* 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 <cfapi.h>
|
||||
#include <comdef.h>
|
||||
|
||||
Q_LOGGING_CATEGORY(lcCfApi, "nextcloud.sync.vfs.cfapi", QtInfoMsg)
|
||||
|
||||
namespace cfapi {
|
||||
using namespace OCC::CfApiWrapper;
|
||||
}
|
||||
|
||||
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 ¶ms)
|
||||
{
|
||||
const auto localPath = QDir::toNativeSeparators(params.filesystemPath);
|
||||
|
||||
const auto registerResult = cfapi::registerSyncRoot(localPath, params.providerName, params.providerVersion);
|
||||
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::unegisterSyncRoot(localPath);
|
||||
if (!result) {
|
||||
qCCritical(lcCfApi) << "Unregistration failed for" << localPath << ":" << result.error();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
const auto handle = cfapi::handleForPath(localPath);
|
||||
if (handle) {
|
||||
return cfapi::updatePlaceholderInfo(handle, modtime, size, fileId);
|
||||
} else {
|
||||
qCWarning(lcCfApi) << "Couldn't update metadata for non existing file" << localPath;
|
||||
return "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 previousPin = pinState(item._file);
|
||||
|
||||
if (!QFile::remove(_setupParams.filesystemPath + item._file)) {
|
||||
return QStringLiteral("Couldn't remove %1 to fulfill dehydration").arg(item._file);
|
||||
}
|
||||
|
||||
const auto r = createPlaceholder(item);
|
||||
if (!r) {
|
||||
return r;
|
||||
}
|
||||
|
||||
if (previousPin) {
|
||||
if (*previousPin == PinState::AlwaysLocal) {
|
||||
setPinState(item._file, PinState::Unspecified);
|
||||
} else {
|
||||
setPinState(item._file, *previousPin);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void VfsCfApi::convertToPlaceholder(const QString &filename, const SyncFileItem &item, const QString &replacesFile)
|
||||
{
|
||||
const auto localPath = QDir::toNativeSeparators(filename);
|
||||
const auto replacesPath = QDir::toNativeSeparators(replacesFile);
|
||||
|
||||
const auto handle = cfapi::handleForPath(localPath);
|
||||
if (cfapi::findPlaceholderInfo(handle)) {
|
||||
cfapi::updatePlaceholderInfo(handle, item._modtime, item._size, item._fileId, replacesPath);
|
||||
} else {
|
||||
cfapi::convertToPlaceholder(handle, 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;
|
||||
|
||||
// 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 && hasReparsePoint && hasCloudTag) {
|
||||
ffd->dwFileAttributes &= ~FILE_ATTRIBUTE_REPARSE_POINT;
|
||||
return false;
|
||||
} else if (isSparseFile && isPinned) {
|
||||
stat->type = ItemTypeVirtualFileDownload;
|
||||
return true;
|
||||
} else if (!isSparseFile && isUnpinned){
|
||||
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);
|
||||
const auto handle = cfapi::handleForPath(localPath);
|
||||
if (handle) {
|
||||
if (cfapi::setPinState(handle, state, cfapi::Recurse)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
qCWarning(lcCfApi) << "Couldn't update pin state for non existing file" << localPath;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Optional<PinState> VfsCfApi::pinState(const QString &folderPath)
|
||||
{
|
||||
const auto localPath = QDir::toNativeSeparators(params().filesystemPath + folderPath);
|
||||
const auto handle = cfapi::handleForPath(localPath);
|
||||
if (!handle) {
|
||||
qCWarning(lcCfApi) << "Couldn't find pin state for non existing file" << localPath;
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto info = cfapi::findPlaceholderInfo(handle);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// This is impossible to handle with CfAPI since the file size is generally different
|
||||
// between the encrypted and the decrypted file which would make CfAPI reject the hydration
|
||||
// of the placeholder with decrypted data
|
||||
if (record._isE2eEncrypted || !record._e2eMangledName.isEmpty()) {
|
||||
qCInfo(lcCfApi) << "Couldn't hydrate, the file is E2EE this is not supported";
|
||||
emit hydrationRequestFailed(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
// All good, let's hydrate now
|
||||
scheduleHydrationJob(requestId, relativePath);
|
||||
}
|
||||
|
||||
void VfsCfApi::fileStatusChanged(const QString &systemFileName, SyncFileStatus fileStatus)
|
||||
{
|
||||
Q_UNUSED(systemFileName);
|
||||
Q_UNUSED(fileStatus);
|
||||
}
|
||||
|
||||
void VfsCfApi::scheduleHydrationJob(const QString &requestId, const QString &folderPath)
|
||||
{
|
||||
Q_ASSERT(std::none_of(std::cbegin(d->hydrationJobs), std::cend(d->hydrationJobs), [=](HydrationJob *job) {
|
||||
return job->requestId() == requestId || job->folderPath() == folderPath;
|
||||
}));
|
||||
|
||||
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);
|
||||
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(), job->status());
|
||||
d->hydrationJobs.removeAll(job);
|
||||
if (d->hydrationJobs.isEmpty()) {
|
||||
emit doneHydrating();
|
||||
}
|
||||
}
|
||||
|
||||
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 ¤tState, const QString &name) {
|
||||
if (name == QStringLiteral("..") || name == QStringLiteral(".")) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
const auto path = 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
|
||||
|
||||
OCC_DEFINE_VFS_FACTORY("win", OCC::VfsCfApi)
|
85
src/libsync/vfs/cfapi/vfs_cfapi.h
Normal file
85
src/libsync/vfs/cfapi/vfs_cfapi.h
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QScopedPointer>
|
||||
|
||||
#include "common/vfs.h"
|
||||
|
||||
namespace OCC {
|
||||
class HydrationJob;
|
||||
class VfsCfApiPrivate;
|
||||
|
||||
class VfsCfApi : public Vfs
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit VfsCfApi(QObject *parent = nullptr);
|
||||
~VfsCfApi();
|
||||
|
||||
Mode mode() const override;
|
||||
QString fileSuffix() const override;
|
||||
|
||||
void stop() override;
|
||||
void unregisterFolder() override;
|
||||
|
||||
bool socketApiPinStateActionsShown() const override;
|
||||
bool isHydrating() const override;
|
||||
|
||||
Result<void, QString> updateMetadata(const QString &filePath, time_t modtime, qint64 size, const QByteArray &fileId) override;
|
||||
|
||||
Result<void, QString> createPlaceholder(const SyncFileItem &item) override;
|
||||
Result<void, QString> dehydratePlaceholder(const SyncFileItem &item) override;
|
||||
void convertToPlaceholder(const QString &filename, const SyncFileItem &item, const QString &replacesFile) override;
|
||||
|
||||
bool needsMetadataUpdate(const SyncFileItem &) override;
|
||||
bool isDehydratedPlaceholder(const QString &filePath) override;
|
||||
bool statTypeVirtualFile(csync_file_stat_t *stat, void *statData) override;
|
||||
|
||||
bool setPinState(const QString &folderPath, PinState state) override;
|
||||
Optional<PinState> pinState(const QString &folderPath) override;
|
||||
AvailabilityResult availability(const QString &folderPath) override;
|
||||
|
||||
public slots:
|
||||
void requestHydration(const QString &requestId, const QString &path);
|
||||
void fileStatusChanged(const QString &systemFileName, SyncFileStatus fileStatus) override;
|
||||
|
||||
signals:
|
||||
void hydrationRequestReady(const QString &requestId);
|
||||
void hydrationRequestFailed(const QString &requestId);
|
||||
void hydrationRequestFinished(const QString &requestId, int status);
|
||||
|
||||
protected:
|
||||
void startImpl(const VfsSetupParams ¶ms) override;
|
||||
|
||||
private:
|
||||
void scheduleHydrationJob(const QString &requestId, const QString &folderPath);
|
||||
void onHydrationJobFinished(HydrationJob *job);
|
||||
|
||||
struct HasHydratedDehydrated {
|
||||
bool hasHydrated = false;
|
||||
bool hasDehydrated = false;
|
||||
};
|
||||
struct HydratationAndPinStates {
|
||||
Optional<PinState> pinState;
|
||||
HasHydratedDehydrated hydrationStatus;
|
||||
};
|
||||
HydratationAndPinStates computeRecursiveHydrationAndPinStates(const QString &path, const Optional<PinState> &basePinState);
|
||||
|
||||
QScopedPointer<VfsCfApiPrivate> d;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
|
@ -73,6 +73,10 @@ if( UNIX AND NOT APPLE )
|
|||
nextcloud_add_test(InotifyWatcher "${FolderWatcher_SRC}")
|
||||
endif(UNIX AND NOT APPLE)
|
||||
|
||||
if (WIN32)
|
||||
nextcloud_add_test(SyncCfApi "")
|
||||
endif()
|
||||
|
||||
nextcloud_add_benchmark(LargeSync "")
|
||||
|
||||
SET(FolderMan_SRC ../src/gui/folderman.cpp)
|
||||
|
|
1189
test/testsynccfapi.cpp
Normal file
1189
test/testsynccfapi.cpp
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue