Merge pull request #4942 from nextcloud/feature/vfs-windows-sharing-and-lock-state

Feature/vfs windows sharing and lock state
This commit is contained in:
allexzander 2022-10-04 22:40:15 +03:00 committed by GitHub
commit de27a2ffd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1431 additions and 187 deletions

View File

@ -16,6 +16,10 @@ set( CFAPI_SHELL_EXTENSIONS_LIB_NAME CfApiShellExtensions )
set( CFAPI_SHELLEXT_APPID_REG "{E314A650-DCA4-416E-974E-18EA37C213EA}") set( CFAPI_SHELLEXT_APPID_REG "{E314A650-DCA4-416E-974E-18EA37C213EA}")
set( CFAPI_SHELLEXT_APPID_DISPLAY_NAME "${APPLICATION_NAME} CfApi Shell Extensions" ) set( CFAPI_SHELLEXT_APPID_DISPLAY_NAME "${APPLICATION_NAME} CfApi Shell Extensions" )
set( CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID "1E62D59A-6EA4-476C-B707-4A32E88ED822" )
set( CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG "{${CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID}}" )
set( CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_DISPLAY_NAME "${APPLICATION_NAME} Custom State Handler" )
set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID "6FF9B5B6-389F-444A-9FDD-A286C36EA079" ) set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID "6FF9B5B6-389F-444A-9FDD-A286C36EA079" )
set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG "{${CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID}}" ) set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG "{${CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID}}" )
set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME "${APPLICATION_NAME} Thumbnail Handler" ) set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME "${APPLICATION_NAME} Thumbnail Handler" )

View File

@ -53,19 +53,25 @@ End Function
Function RegistryCleanupCfApiShellExtensions() Function RegistryCleanupCfApiShellExtensions()
Set objRegistry = GetObject(strObjRegistry) Set objRegistry = GetObject(strObjRegistry)
strShellExtThumbnailHandlerAppId = "Software\Classes\AppID\@CFAPI_SHELLEXT_APPID_REG@" strShellExtAppId = "Software\Classes\AppID\@CFAPI_SHELLEXT_APPID_REG@"
strShellExtThumbnailHandlerClsId = "Software\Classes\CLSID\@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG@" strShellExtThumbnailHandlerClsId = "Software\Classes\CLSID\@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG@"
strShellExtCustomStateHandlerClsId = "Software\Classes\CLSID\@CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG@"
rootKey = HKEY_CURRENT_USER rootKey = HKEY_CURRENT_USER
If objRegistry.EnumKey(rootKey, strShellExtThumbnailHandlerAppId, arrSubKeys) = 0 Then If objRegistry.EnumKey(rootKey, strShellExtAppId, arrSubKeys) = 0 Then
RegistryDeleteKeyRecursive rootKey, strShellExtThumbnailHandlerAppId RegistryDeleteKeyRecursive rootKey, strShellExtAppId
End If End If
If objRegistry.EnumKey(rootKey, strShellExtThumbnailHandlerClsId, arrSubKeys) = 0 Then If objRegistry.EnumKey(rootKey, strShellExtThumbnailHandlerClsId, arrSubKeys) = 0 Then
RegistryDeleteKeyRecursive rootKey, strShellExtThumbnailHandlerClsId RegistryDeleteKeyRecursive rootKey, strShellExtThumbnailHandlerClsId
End If End If
If objRegistry.EnumKey(rootKey, strShellExtCustomStateHandlerClsId, arrSubKeys) = 0 Then
RegistryDeleteKeyRecursive rootKey, strShellExtCustomStateHandlerClsId
End If
End Function End Function
Function RegistryCleanup() Function RegistryCleanup()

View File

@ -102,11 +102,13 @@ include(CMakeParseArguments)
function(ecm_add_app_icon appsources) function(ecm_add_app_icon appsources)
set(options) set(options)
set(oneValueArgs OUTFILE_BASENAME ICON_INDEX) set(oneValueArgs OUTFILE_BASENAME ICON_INDEX DO_NOT_GENERATE_RC_FILE)
set(multiValueArgs ICONS SIDEBAR_ICONS RC_DEPENDENCIES) set(multiValueArgs ICONS SIDEBAR_ICONS RC_DEPENDENCIES)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if (NOT ARG_ICON_INDEX) if (ARG_DO_NOT_GENERATE_RC_FILE)
set(ARG_ICON_INDEX 1) set (_do_not_generate_rc_file TRUE)
else()
set (_do_not_generate_rc_file FALSE)
endif() endif()
if(NOT ARG_ICONS) if(NOT ARG_ICONS)
@ -211,15 +213,17 @@ function(ecm_add_app_icon appsources)
DEPENDS ${deps} DEPENDS ${deps}
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
) )
# this bit's a little hacky to make the dependency stuff work if (NOT _do_not_generate_rc_file)
file(WRITE "${_outfilename}.rc.in" "IDI_ICON${ARG_ICON_INDEX} ICON DISCARDABLE \"${_outfilename}.ico\"\n") # this bit's a little hacky to make the dependency stuff work
add_custom_command( file(WRITE "${_outfilename}.rc.in" "IDI_ICON${ARG_ICON_INDEX} ICON DISCARDABLE \"${_outfilename}.ico\"\n")
OUTPUT "${_outfilename}.rc" add_custom_command(
COMMAND ${CMAKE_COMMAND} OUTPUT "${_outfilename}.rc"
ARGS -E copy "${_outfilename}.rc.in" "${_outfilename}.rc" COMMAND ${CMAKE_COMMAND}
DEPENDS ${ARG_RC_DEPENDENCIES} "${_outfilename}.ico" ARGS -E copy "${_outfilename}.rc.in" "${_outfilename}.rc"
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" DEPENDS ${ARG_RC_DEPENDENCIES} "${_outfilename}.ico"
) WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
)
endif()
endfunction() endfunction()
if (IcoTool_FOUND) if (IcoTool_FOUND)

View File

@ -0,0 +1,58 @@
# UPSTREAM our ECMAddAppIcon.cmake then require that version here
# find_package(ECM 1.7.0 REQUIRED NO_MODULE)
# list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
include(ECMAddAppIcon)
find_program(SVG_CONVERTER
NAMES inkscape inkscape.exe rsvg-convert
REQUIRED
HINTS "C:\\Program Files\\Inkscape\\bin" "/usr/bin" ENV SVG_CONVERTER_DIR)
# REQUIRED keyword is only supported on CMake 3.18 and above
if (NOT SVG_CONVERTER)
message(FATAL_ERROR "Could not find a suitable svg converter. Set SVG_CONVERTER_DIR to the path of either the inkscape or rsvg-convert executable.")
endif()
function(generate_sized_png_from_svg icon_path size)
set(options)
set(oneValueArgs OUTPUT_ICON_NAME OUTPUT_ICON_FULL_NAME_WLE OUTPUT_ICON_PATH)
set(multiValueArgs)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
get_filename_component(icon_name_dir ${icon_path} DIRECTORY)
get_filename_component(icon_name_wle ${icon_path} NAME_WLE)
if (ARG_OUTPUT_ICON_NAME)
set(icon_name_wle ${ARG_OUTPUT_ICON_NAME})
endif ()
if (ARG_OUTPUT_ICON_PATH)
set(icon_name_dir ${ARG_OUTPUT_ICON_PATH})
endif ()
set(output_icon_full_name_wle "${size}-${icon_name_wle}")
if (ARG_OUTPUT_ICON_FULL_NAME_WLE)
set(output_icon_full_name_wle ${ARG_OUTPUT_ICON_FULL_NAME_WLE})
endif ()
if (EXISTS "${icon_name_dir}/${output_icon_full_name_wle}.png")
return()
endif()
set(icon_output_name "${output_icon_full_name_wle}.png")
message(STATUS "Generate ${icon_output_name}")
execute_process(COMMAND
"${SVG_CONVERTER}" -w ${size} -h ${size} "${icon_path}" -o "${icon_output_name}"
WORKING_DIRECTORY "${icon_name_dir}"
RESULT_VARIABLE
SVG_CONVERTER_SIDEBAR_ERROR
OUTPUT_QUIET
ERROR_QUIET)
if (SVG_CONVERTER_SIDEBAR_ERROR)
message(FATAL_ERROR
"${SVG_CONVERTER} could not generate icon: ${SVG_CONVERTER_SIDEBAR_ERROR}")
else()
endif()
endfunction()

View File

@ -48,6 +48,10 @@
#cmakedefine CFAPI_SHELLEXT_APPID_REG "@CFAPI_SHELLEXT_APPID_REG@" #cmakedefine CFAPI_SHELLEXT_APPID_REG "@CFAPI_SHELLEXT_APPID_REG@"
#cmakedefine CFAPI_SHELLEXT_APPID_DISPLAY_NAME "@CFAPI_SHELLEXT_APPID_DISPLAY_NAME@" #cmakedefine CFAPI_SHELLEXT_APPID_DISPLAY_NAME "@CFAPI_SHELLEXT_APPID_DISPLAY_NAME@"
#cmakedefine CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID "@CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID@"
#cmakedefine CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG "@CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG@"
#cmakedefine CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_DISPLAY_NAME "@CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_DISPLAY_NAME@"
#cmakedefine CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID "@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID@" #cmakedefine CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID "@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID@"
#cmakedefine CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG "@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG@" #cmakedefine CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG "@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG@"
#cmakedefine CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME "@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME@" #cmakedefine CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME "@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME@"

View File

@ -29,7 +29,6 @@ namespace Protocol {
if (!valid) { if (!valid) {
qCWarning(lcShellExtensionUtils) << "Invalid shell extensions IPC protocol: " << message.value(QStringLiteral("version")) << " vs " << Version; qCWarning(lcShellExtensionUtils) << "Invalid shell extensions IPC protocol: " << message.value(QStringLiteral("version")) << " vs " << Version;
} }
Q_ASSERT(valid);
return valid; return valid;
} }
} }

View File

@ -23,11 +23,14 @@ QString serverNameForApplicationName(const QString &applicationName);
QString serverNameForApplicationNameDefault(); QString serverNameForApplicationNameDefault();
namespace Protocol { namespace Protocol {
static constexpr auto CustomStateProviderRequestKey = "customStateProviderRequest";
static constexpr auto CustomStateDataKey = "customStateData";
static constexpr auto CustomStateStatesKey = "states";
static constexpr auto FilePathKey = "filePath";
static constexpr auto ThumbnailProviderRequestKey = "thumbnailProviderRequest"; static constexpr auto ThumbnailProviderRequestKey = "thumbnailProviderRequest";
static constexpr auto ThumbnailProviderRequestFilePathKey = "filePath";
static constexpr auto ThumbnailProviderRequestFileSizeKey = "fileSize"; static constexpr auto ThumbnailProviderRequestFileSizeKey = "fileSize";
static constexpr auto ThumnailProviderDataKey = "thumbnailData"; static constexpr auto ThumnailProviderDataKey = "thumbnailData";
static constexpr auto Version = "1.0"; static constexpr auto Version = "2.0";
QByteArray createJsonMessage(const QVariantMap &message); QByteArray createJsonMessage(const QVariantMap &message);
bool validateProtocolVersion(const QVariantMap &message); bool validateProtocolVersion(const QVariantMap &message);

View File

@ -49,7 +49,7 @@ Q_LOGGING_CATEGORY(lcDb, "nextcloud.sync.database", QtInfoMsg)
#define GET_FILE_RECORD_QUERY \ #define GET_FILE_RECORD_QUERY \
"SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize," \ "SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize," \
" ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, " \ " ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, " \
" lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout " \ " lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap " \
" FROM metadata" \ " FROM metadata" \
" LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id" " LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id"
@ -74,6 +74,8 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que
rec._lockstate._lockEditorApp = query.stringValue(16); rec._lockstate._lockEditorApp = query.stringValue(16);
rec._lockstate._lockTime = query.int64Value(17); rec._lockstate._lockTime = query.int64Value(17);
rec._lockstate._lockTimeout = query.int64Value(18); rec._lockstate._lockTimeout = query.int64Value(18);
rec._isShared = query.intValue(19) > 0;
rec._lastShareStateFetchedTimestmap = query.int64Value(20);
} }
static QByteArray defaultJournalMode(const QString &dbPath) static QByteArray defaultJournalMode(const QString &dbPath)
@ -727,6 +729,8 @@ bool SyncJournalDb::updateMetadataTableStructure()
addColumn(QStringLiteral("contentChecksumTypeId"), QStringLiteral("INTEGER")); addColumn(QStringLiteral("contentChecksumTypeId"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("e2eMangledName"), QStringLiteral("TEXT")); addColumn(QStringLiteral("e2eMangledName"), QStringLiteral("TEXT"));
addColumn(QStringLiteral("isE2eEncrypted"), QStringLiteral("INTEGER")); addColumn(QStringLiteral("isE2eEncrypted"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("isShared"), QStringLiteral("INTEGER"));
addColumn(QStringLiteral("lastShareStateFetchedTimestmap"), QStringLiteral("INTEGER"));
auto uploadInfoColumns = tableColumns("uploadinfo"); auto uploadInfoColumns = tableColumns("uploadinfo");
if (uploadInfoColumns.isEmpty()) if (uploadInfoColumns.isEmpty())
@ -881,13 +885,17 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
} }
qCInfo(lcDb) << "Updating file record for path:" << record.path() << "inode:" << record._inode qCInfo(lcDb) << "Updating file record for path:" << record.path() << "inode:" << record._inode
<< "modtime:" << record._modtime << "type:" << record._type << "modtime:" << record._modtime << "type:" << record._type << "etag:" << record._etag
<< "etag:" << record._etag << "fileId:" << record._fileId << "remotePerm:" << record._remotePerm.toString() << "fileId:" << record._fileId << "remotePerm:" << record._remotePerm.toString()
<< "fileSize:" << record._fileSize << "checksum:" << record._checksumHeader << "fileSize:" << record._fileSize << "checksum:" << record._checksumHeader
<< "e2eMangledName:" << record.e2eMangledName() << "isE2eEncrypted:" << record._isE2eEncrypted << "e2eMangledName:" << record.e2eMangledName() << "isE2eEncrypted:" << record._isE2eEncrypted
<< "lock:" << (record._lockstate._locked ? "true" : "false") << "lock owner type:" << record._lockstate._lockOwnerType << "lock:" << (record._lockstate._locked ? "true" : "false")
<< "lock owner:" << record._lockstate._lockOwnerDisplayName << "lock owner id:" << record._lockstate._lockOwnerId << "lock owner type:" << record._lockstate._lockOwnerType
<< "lock editor:" << record._lockstate._lockEditorApp; << "lock owner:" << record._lockstate._lockOwnerDisplayName
<< "lock owner id:" << record._lockstate._lockOwnerId
<< "lock editor:" << record._lockstate._lockEditorApp
<< "isShared:" << record._isShared
<< "lastShareStateFetchedTimestmap:" << record._lastShareStateFetchedTimestmap;
const qint64 phash = getPHash(record._path); const qint64 phash = getPHash(record._path);
if (!checkConnect()) { if (!checkConnect()) {
@ -913,8 +921,8 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
const auto query = _queryManager.get(PreparedSqlQueryManager::SetFileRecordQuery, QByteArrayLiteral("INSERT OR REPLACE INTO metadata " const auto query = _queryManager.get(PreparedSqlQueryManager::SetFileRecordQuery, QByteArrayLiteral("INSERT OR REPLACE INTO metadata "
"(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, " "(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, "
"contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, lock, lockType, lockOwnerDisplayName, lockOwnerId, " "contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, lock, lockType, lockOwnerDisplayName, lockOwnerId, "
"lockOwnerEditor, lockTime, lockTimeout) " "lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap) "
"VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25);"), "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27);"),
_db); _db);
if (!query) { if (!query) {
return query->error(); return query->error();
@ -945,6 +953,8 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
query->bindValue(23, record._lockstate._lockEditorApp); query->bindValue(23, record._lockstate._lockEditorApp);
query->bindValue(24, record._lockstate._lockTime); query->bindValue(24, record._lockstate._lockTime);
query->bindValue(25, record._lockstate._lockTimeout); query->bindValue(25, record._lockstate._lockTimeout);
query->bindValue(26, record._isShared);
query->bindValue(27, record._lastShareStateFetchedTimestmap);
if (!query->exec()) { if (!query->exec()) {
return query->error(); return query->error();

View File

@ -81,6 +81,8 @@ public:
QByteArray _e2eMangledName; QByteArray _e2eMangledName;
bool _isE2eEncrypted = false; bool _isE2eEncrypted = false;
SyncJournalFileLockInfo _lockstate; SyncJournalFileLockInfo _lockstate;
bool _isShared = false;
qint64 _lastShareStateFetchedTimestmap = 0;
}; };
bool OCSYNC_EXPORT bool OCSYNC_EXPORT

View File

@ -353,11 +353,7 @@ if(Qt5Keychain_FOUND)
endif() endif()
# add executable icon on windows and osx # add executable icon on windows and osx
include(GenerateIconsUtils)
# UPSTREAM our ECMAddAppIcon.cmake then require that version here
# find_package(ECM 1.7.0 REQUIRED NO_MODULE)
# list(APPEND CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
include(ECMAddAppIcon)
# For historical reasons we can not use the application_shortname # For historical reasons we can not use the application_shortname
# for ownCloud but must rather set it manually. # for ownCloud but must rather set it manually.
@ -369,61 +365,6 @@ if(NOT DEFINED APPLICATION_FOLDER_ICON_INDEX)
set(APPLICATION_FOLDER_ICON_INDEX 0) set(APPLICATION_FOLDER_ICON_INDEX 0)
endif() endif()
# Generate png icons from svg
find_program(SVG_CONVERTER
NAMES inkscape inkscape.exe rsvg-convert
REQUIRED
HINTS "C:\\Program Files\\Inkscape\\bin" "/usr/bin" ENV SVG_CONVERTER_DIR)
# REQUIRED keyword is only supported on CMake 3.18 and above
if (NOT SVG_CONVERTER)
message(FATAL_ERROR "Could not find a suitable svg converter. Set SVG_CONVERTER_DIR to the path of either the inkscape or rsvg-convert executable.")
endif()
function(generate_sized_png_from_svg icon_path size)
set(options)
set(oneValueArgs OUTPUT_ICON_NAME OUTPUT_ICON_FULL_NAME_WLE OUTPUT_ICON_PATH)
set(multiValueArgs)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
get_filename_component(icon_name_dir ${icon_path} DIRECTORY)
get_filename_component(icon_name_wle ${icon_path} NAME_WLE)
if (ARG_OUTPUT_ICON_NAME)
set(icon_name_wle ${ARG_OUTPUT_ICON_NAME})
endif ()
if (ARG_OUTPUT_ICON_PATH)
set(icon_name_dir ${ARG_OUTPUT_ICON_PATH})
endif ()
set(output_icon_full_name_wle "${size}-${icon_name_wle}")
if (ARG_OUTPUT_ICON_FULL_NAME_WLE)
set(output_icon_full_name_wle ${ARG_OUTPUT_ICON_FULL_NAME_WLE})
endif ()
if (EXISTS "${icon_name_dir}/${output_icon_full_name_wle}.png")
return()
endif()
set(icon_output_name "${output_icon_full_name_wle}.png")
message(STATUS "Generate ${icon_output_name}")
execute_process(COMMAND
"${SVG_CONVERTER}" -w ${size} -h ${size} "${icon_path}" -o "${icon_output_name}"
WORKING_DIRECTORY "${icon_name_dir}"
RESULT_VARIABLE
SVG_CONVERTER_SIDEBAR_ERROR
OUTPUT_QUIET
ERROR_QUIET)
if (SVG_CONVERTER_SIDEBAR_ERROR)
message(FATAL_ERROR
"${SVG_CONVERTER} could not generate icon: ${SVG_CONVERTER_SIDEBAR_ERROR}")
else()
endif()
endfunction()
set(STATE_ICONS_COLORS colored black white) set(STATE_ICONS_COLORS colored black white)
foreach(state_icons_color ${STATE_ICONS_COLORS}) foreach(state_icons_color ${STATE_ICONS_COLORS})

View File

@ -40,7 +40,7 @@ void OcsJob::setVerb(const QByteArray &verb)
void OcsJob::addParam(const QString &name, const QString &value) void OcsJob::addParam(const QString &name, const QString &value)
{ {
_params.append(qMakePair(name, value)); _params.insert(name, value);
} }
void OcsJob::addPassStatusCode(int code) void OcsJob::addPassStatusCode(int code)
@ -58,16 +58,21 @@ void OcsJob::addRawHeader(const QByteArray &headerName, const QByteArray &value)
_request.setRawHeader(headerName, value); _request.setRawHeader(headerName, value);
} }
QString OcsJob::getParamValue(const QString &key) const
{
return _params.value(key);
}
static QUrlQuery percentEncodeQueryItems( static QUrlQuery percentEncodeQueryItems(
const QList<QPair<QString, QString>> &items) const QHash<QString, QString> &items)
{ {
QUrlQuery result; QUrlQuery result;
// Note: QUrlQuery::setQueryItems() does not fully percent encode // Note: QUrlQuery::setQueryItems() does not fully percent encode
// the query items, see #5042 // the query items, see #5042
foreach (const auto &item, items) { for (auto it = std::cbegin(items); it != std::cend(items); ++it) {
result.addQueryItem( result.addQueryItem(
QUrl::toPercentEncoding(item.first), QUrl::toPercentEncoding(it.key()),
QUrl::toPercentEncoding(item.second)); QUrl::toPercentEncoding(it.value()));
} }
return result; return result;
} }
@ -85,13 +90,13 @@ void OcsJob::start()
} else if (_verb == "POST" || _verb == "PUT") { } else if (_verb == "POST" || _verb == "PUT") {
// Url encode the _postParams and put them in a buffer. // Url encode the _postParams and put them in a buffer.
QByteArray postData; QByteArray postData;
Q_FOREACH (auto tmp, _params) { for (auto it = std::cbegin(_params); it != std::cend(_params); ++it) {
if (!postData.isEmpty()) { if (!postData.isEmpty()) {
postData.append("&"); postData.append("&");
} }
postData.append(QUrl::toPercentEncoding(tmp.first)); postData.append(QUrl::toPercentEncoding(it.key()));
postData.append("="); postData.append("=");
postData.append(QUrl::toPercentEncoding(tmp.second)); postData.append(QUrl::toPercentEncoding(it.value()));
} }
buffer->setData(postData); buffer->setData(postData);
} }

View File

@ -19,8 +19,7 @@
#include "abstractnetworkjob.h" #include "abstractnetworkjob.h"
#include <QVector> #include <QVector>
#include <QList> #include <QHash>
#include <QPair>
#include <QUrl> #include <QUrl>
#define OCS_SUCCESS_STATUS_CODE 100 #define OCS_SUCCESS_STATUS_CODE 100
@ -110,6 +109,8 @@ public:
*/ */
void addRawHeader(const QByteArray &headerName, const QByteArray &value); void addRawHeader(const QByteArray &headerName, const QByteArray &value);
[[nodiscard]] QString getParamValue(const QString &key) const;
protected slots: protected slots:
@ -149,7 +150,7 @@ private slots:
private: private:
QByteArray _verb; QByteArray _verb;
QList<QPair<QString, QString>> _params; QHash<QString, QString> _params;
QVector<int> _passStatusCodes; QVector<int> _passStatusCodes;
QNetworkRequest _request; QNetworkRequest _request;
}; };

View File

@ -24,16 +24,21 @@ namespace OCC {
OcsShareJob::OcsShareJob(AccountPtr account) OcsShareJob::OcsShareJob(AccountPtr account)
: OcsJob(account) : OcsJob(account)
{ {
setPath("ocs/v2.php/apps/files_sharing/api/v1/shares"); setPath(_pathForSharesRequest);
connect(this, &OcsJob::jobFinished, this, &OcsShareJob::jobDone); connect(this, &OcsJob::jobFinished, this, &OcsShareJob::jobDone);
} }
void OcsShareJob::getShares(const QString &path) void OcsShareJob::getShares(const QString &path, const QMap<QString, QString> &params)
{ {
setVerb("GET"); setVerb("GET");
addParam(QString::fromLatin1("path"), path); addParam(QString::fromLatin1("path"), path);
addParam(QString::fromLatin1("reshares"), QString("true")); addParam(QString::fromLatin1("reshares"), QString("true"));
for (auto it = std::cbegin(params); it != std::cend(params); ++it) {
addParam(it.key(), it.value());
}
addPassStatusCode(404); addPassStatusCode(404);
start(); start();
@ -181,4 +186,6 @@ void OcsShareJob::jobDone(QJsonDocument reply)
{ {
emit shareJobFinished(reply, _value); emit shareJobFinished(reply, _value);
} }
QString const OcsShareJob::_pathForSharesRequest = QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares");
} }

View File

@ -46,7 +46,7 @@ public:
* *
* @param path Path to request shares for (default all shares) * @param path Path to request shares for (default all shares)
*/ */
void getShares(const QString &path = ""); void getShares(const QString &path = "", const QMap<QString, QString> &params = {});
/** /**
* Delete the current Share * Delete the current Share
@ -131,6 +131,8 @@ public:
*/ */
void getSharedWithMe(); void getSharedWithMe();
static const QString _pathForSharesRequest;
signals: signals:
/** /**
* Result of the OCS request * Result of the OCS request

View File

@ -16,29 +16,58 @@
#include "account.h" #include "account.h"
#include "accountstate.h" #include "accountstate.h"
#include "common/shellextensionutils.h" #include "common/shellextensionutils.h"
#include <libsync/vfs/cfapi/shellext/configvfscfapishellext.h>
#include "folder.h" #include "folder.h"
#include "folderman.h" #include "folderman.h"
#include "ocssharejob.h"
#include <QDir> #include <QDir>
#include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject>
#include <QLocalSocket> #include <QLocalSocket>
namespace {
constexpr auto isSharedInvalidationInterval = 2 * 60 * 1000; // 2 minutes, so we don't make fetch sharees requests too often
constexpr auto folderAliasPropertyKey = "folderAlias";
}
namespace OCC { namespace OCC {
Q_LOGGING_CATEGORY(lcShellExtServer, "nextcloud.gui.shellextensions.server", QtInfoMsg)
ShellExtensionsServer::ShellExtensionsServer(QObject *parent) ShellExtensionsServer::ShellExtensionsServer(QObject *parent)
: QObject(parent) : QObject(parent)
{ {
_isSharedInvalidationInterval = isSharedInvalidationInterval;
_localServer.listen(VfsShellExtensions::serverNameForApplicationNameDefault()); _localServer.listen(VfsShellExtensions::serverNameForApplicationNameDefault());
connect(&_localServer, &QLocalServer::newConnection, this, &ShellExtensionsServer::slotNewConnection); connect(&_localServer, &QLocalServer::newConnection, this, &ShellExtensionsServer::slotNewConnection);
} }
ShellExtensionsServer::~ShellExtensionsServer() ShellExtensionsServer::~ShellExtensionsServer()
{ {
for (const auto &connection : _customStateSocketConnections) {
if (connection) {
QObject::disconnect(connection);
}
}
_customStateSocketConnections.clear();
if (!_localServer.isListening()) { if (!_localServer.isListening()) {
return; return;
} }
_localServer.close(); _localServer.close();
} }
QString ShellExtensionsServer::getFetchThumbnailPath()
{
return QStringLiteral("/index.php/core/preview");
}
void ShellExtensionsServer::setIsSharedInvalidationInterval(qint64 interval)
{
_isSharedInvalidationInterval = interval;
}
void ShellExtensionsServer::sendJsonMessageWithVersion(QLocalSocket *socket, const QVariantMap &message) void ShellExtensionsServer::sendJsonMessageWithVersion(QLocalSocket *socket, const QVariantMap &message)
{ {
socket->write(VfsShellExtensions::Protocol::createJsonMessage(message)); socket->write(VfsShellExtensions::Protocol::createJsonMessage(message));
@ -60,6 +89,96 @@ void ShellExtensionsServer::closeSession(QLocalSocket *socket)
socket->disconnectFromServer(); socket->disconnectFromServer();
} }
void ShellExtensionsServer::processCustomStateRequest(QLocalSocket *socket, const CustomStateRequestInfo &customStateRequestInfo)
{
if (!customStateRequestInfo.isValid()) {
sendEmptyDataAndCloseSession(socket);
return;
}
const auto folder = FolderMan::instance()->folder(customStateRequestInfo.folderAlias);
if (!folder) {
sendEmptyDataAndCloseSession(socket);
return;
}
const auto filePathRelative = QString(customStateRequestInfo.path).remove(folder->path());
SyncJournalFileRecord record;
if (!folder->journalDb()->getFileRecord(filePathRelative, &record) || !record.isValid() || record.path().isEmpty()) {
qCWarning(lcShellExtServer) << "Record not found in SyncJournal for: " << filePathRelative;
sendEmptyDataAndCloseSession(socket);
return;
}
const auto composeMessageReplyFromRecord = [](const SyncJournalFileRecord &record) {
QVariantList states;
if (record._lockstate._locked) {
states.push_back(QString(CUSTOM_STATE_ICON_LOCKED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt());
}
if (record._isShared) {
states.push_back(QString(CUSTOM_STATE_ICON_SHARED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt());
}
return QVariantMap{{VfsShellExtensions::Protocol::CustomStateDataKey,
QVariantMap{{VfsShellExtensions::Protocol::CustomStateStatesKey, states}}}};
};
if (QDateTime::currentMSecsSinceEpoch() - record._lastShareStateFetchedTimestmap < _isSharedInvalidationInterval) {
qCInfo(lcShellExtServer) << record.path() << " record._lastShareStateFetchedTimestmap has less than " << _isSharedInvalidationInterval << " ms difference with QDateTime::currentMSecsSinceEpoch(). Returning data from SyncJournal.";
sendJsonMessageWithVersion(socket, composeMessageReplyFromRecord(record));
closeSession(socket);
return;
}
const auto job = new OcsShareJob(folder->accountState()->account());
job->setProperty(folderAliasPropertyKey, customStateRequestInfo.folderAlias);
connect(job, &OcsShareJob::shareJobFinished, this, &ShellExtensionsServer::slotSharesFetched);
connect(job, &OcsJob::ocsError, this, &ShellExtensionsServer::slotSharesFetchError);
{
_customStateSocketConnections.insert(socket->socketDescriptor(), QObject::connect(this, &ShellExtensionsServer::fetchSharesJobFinished, [this, socket, filePathRelative, composeMessageReplyFromRecord](const QString &folderAlias) {
{
const auto connection = _customStateSocketConnections[socket->socketDescriptor()];
if (connection) {
QObject::disconnect(connection);
}
_customStateSocketConnections.remove(socket->socketDescriptor());
}
const auto folder = FolderMan::instance()->folder(folderAlias);
SyncJournalFileRecord record;
if (!folder || !folder->journalDb()->getFileRecord(filePathRelative, &record) || !record.isValid()) {
qCWarning(lcShellExtServer) << "Record not found in SyncJournal for: " << filePathRelative;
sendEmptyDataAndCloseSession(socket);
return;
}
qCInfo(lcShellExtServer) << "Sending reply from OcsShareJob for socket: " << socket->socketDescriptor() << " and record: " << record.path();
sendJsonMessageWithVersion(socket, composeMessageReplyFromRecord(record));
closeSession(socket);
}));
}
const auto sharesPath = [&record, folder, &filePathRelative]() {
const auto filePathRelativeRemote = QDir(folder->remotePath()).filePath(filePathRelative);
// either get parent's path, or, return '/' if we are in the root folder
auto recordPathSplit = filePathRelativeRemote.split(QLatin1Char('/'), Qt::SkipEmptyParts);
if (recordPathSplit.size() > 1) {
recordPathSplit.removeLast();
return recordPathSplit.join(QLatin1Char('/'));
}
return QStringLiteral("/");
}();
if (!_runningFetchShareJobsForPaths.contains(sharesPath)) {
_runningFetchShareJobsForPaths.push_back(sharesPath);
qCInfo(lcShellExtServer) << "Started OcsShareJob for path: " << sharesPath;
job->getShares(sharesPath, {{QStringLiteral("subfiles"), QStringLiteral("true")}});
} else {
qCInfo(lcShellExtServer) << "OcsShareJob is already running for path: " << sharesPath;
}
}
void ShellExtensionsServer::processThumbnailRequest(QLocalSocket *socket, const ThumbnailRequestInfo &thumbnailRequestInfo) void ShellExtensionsServer::processThumbnailRequest(QLocalSocket *socket, const ThumbnailRequestInfo &thumbnailRequestInfo)
{ {
if (!thumbnailRequestInfo.isValid()) { if (!thumbnailRequestInfo.isValid()) {
@ -87,7 +206,7 @@ void ShellExtensionsServer::processThumbnailRequest(QLocalSocket *socket, const
queryItems.addQueryItem(QStringLiteral("fileId"), record._fileId); queryItems.addQueryItem(QStringLiteral("fileId"), record._fileId);
queryItems.addQueryItem(QStringLiteral("x"), QString::number(thumbnailRequestInfo.size.width())); queryItems.addQueryItem(QStringLiteral("x"), QString::number(thumbnailRequestInfo.size.width()));
queryItems.addQueryItem(QStringLiteral("y"), QString::number(thumbnailRequestInfo.size.height())); queryItems.addQueryItem(QStringLiteral("y"), QString::number(thumbnailRequestInfo.size.height()));
const QUrl jobUrl = Utility::concatUrlPath(folder->accountState()->account()->url(), QStringLiteral("/index.php/core/preview"), queryItems); const QUrl jobUrl = Utility::concatUrlPath(folder->accountState()->account()->url(), getFetchThumbnailPath(), queryItems);
const auto job = new SimpleNetworkJob(folder->accountState()->account()); const auto job = new SimpleNetworkJob(folder->accountState()->account());
job->startRequest(QByteArrayLiteral("GET"), jobUrl); job->startRequest(QByteArrayLiteral("GET"), jobUrl);
connect(job, &SimpleNetworkJob::finishedSignal, this, [socket, this](QNetworkReply *reply) { connect(job, &SimpleNetworkJob::finishedSignal, this, [socket, this](QNetworkReply *reply) {
@ -121,8 +240,155 @@ void ShellExtensionsServer::slotNewConnection()
return; return;
} }
if (message.contains(VfsShellExtensions::Protocol::ThumbnailProviderRequestKey)) {
parseThumbnailRequest(socket, message);
return;
} else if (message.contains(VfsShellExtensions::Protocol::CustomStateProviderRequestKey)) {
parseCustomStateRequest(socket, message);
return;
}
qCWarning(lcShellExtServer) << "Invalid message received from shell extension: " << message;
sendEmptyDataAndCloseSession(socket);
return;
}
void ShellExtensionsServer::slotSharesFetched(const QJsonDocument &reply)
{
const auto job = qobject_cast<OcsShareJob *>(sender());
Q_ASSERT(job);
if (!job) {
qCWarning(lcShellExtServer) << "ShellExtensionsServer::slotSharesFetched is not called by OcsShareJob's signal!";
return;
}
const auto sharesPath = job->getParamValue(QStringLiteral("path"));
_runningFetchShareJobsForPaths.removeAll(sharesPath);
const auto folderAlias = job->property(folderAliasPropertyKey).toString();
Q_ASSERT(!folderAlias.isEmpty());
if (folderAlias.isEmpty()) {
qCWarning(lcShellExtServer) << "No 'folderAlias' set for OcsShareJob's instance!";
return;
}
const auto folder = FolderMan::instance()->folder(folderAlias);
Q_ASSERT(folder);
if (!folder) {
qCWarning(lcShellExtServer) << "folder not found for folderAlias: " << folderAlias;
return;
}
const auto timeStamp = QDateTime::currentMSecsSinceEpoch();
QStringList recortPathsToResetIsSharedFlag;
const QByteArray pathOfSharesToResetIsSharedFlag = sharesPath == QStringLiteral("/") ? QByteArrayLiteral("") : sharesPath.toUtf8();
if (folder->journalDb()->listFilesInPath(pathOfSharesToResetIsSharedFlag, [&](const SyncJournalFileRecord &rec) {
recortPathsToResetIsSharedFlag.push_back(rec.path());
})) {
for (const auto &recordPath : recortPathsToResetIsSharedFlag) {
SyncJournalFileRecord record;
if (!folder->journalDb()->getFileRecord(recordPath, &record) || !record.isValid()) {
continue;
}
record._isShared = false;
record._lastShareStateFetchedTimestmap = timeStamp;
if (!folder->journalDb()->setFileRecord(record)) {
qCWarning(lcShellExtServer) << "Could not set file record for path: " << record._path;
}
}
}
const auto sharesFetched = reply.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("data")).toArray();
for (const auto &share : sharesFetched) {
const auto shareData = share.toObject();
const auto sharePath = [&shareData, folder]() {
const auto sharePathRemote = shareData.value(QStringLiteral("path")).toString();
const auto folderPath = folder->remotePath();
if (folderPath != QLatin1Char('/') && sharePathRemote.startsWith(folderPath)) {
// shares are ruturned with absolute remote path, so, if we have our remote root set to subfolder, we need to adjust share's remote path to relative local path
const auto sharePathLocalRelative = sharePathRemote.midRef(folder->remotePathTrailingSlash().length());
return sharePathLocalRelative.toString();
}
return sharePathRemote.size() > 1 && sharePathRemote.startsWith(QLatin1Char('/'))
? QString(sharePathRemote).remove(0, 1)
: sharePathRemote;
}();
SyncJournalFileRecord record;
if (!folder || !folder->journalDb()->getFileRecord(sharePath, &record) || !record.isValid()) {
continue;
}
record._isShared = true;
record._lastShareStateFetchedTimestmap = timeStamp;
if (!folder->journalDb()->setFileRecord(record)) {
qCWarning(lcShellExtServer) << "Could not set file record for path: " << record._path;
}
}
qCInfo(lcShellExtServer) << "Succeeded OcsShareJob for path: " << sharesPath;
emit fetchSharesJobFinished(folderAlias);
}
void ShellExtensionsServer::slotSharesFetchError(int statusCode, const QString &message)
{
const auto job = qobject_cast<OcsShareJob *>(sender());
Q_ASSERT(job);
if (!job) {
qCWarning(lcShellExtServer) << "ShellExtensionsServer::slotSharesFetched is not called by OcsShareJob's signal!";
return;
}
const auto sharesPath = job->getParamValue(QStringLiteral("path"));
_runningFetchShareJobsForPaths.removeAll(sharesPath);
emit fetchSharesJobFinished(sharesPath);
qCWarning(lcShellExtServer) << "Failed OcsShareJob for path: " << sharesPath;
}
void ShellExtensionsServer::parseCustomStateRequest(QLocalSocket *socket, const QVariantMap &message)
{
const auto customStateRequestMessage = message.value(VfsShellExtensions::Protocol::CustomStateProviderRequestKey).toMap();
const auto itemFilePath = QDir::fromNativeSeparators(customStateRequestMessage.value(VfsShellExtensions::Protocol::FilePathKey).toString());
if (itemFilePath.isEmpty()) {
sendEmptyDataAndCloseSession(socket);
return;
}
QString foundFolderAlias;
for (const auto folder : FolderMan::instance()->map()) {
if (itemFilePath.startsWith(folder->path())) {
foundFolderAlias = folder->alias();
break;
}
}
if (foundFolderAlias.isEmpty()) {
sendEmptyDataAndCloseSession(socket);
return;
}
const auto customStateRequestInfo = CustomStateRequestInfo {
itemFilePath,
foundFolderAlias
};
processCustomStateRequest(socket, customStateRequestInfo);
}
void ShellExtensionsServer::parseThumbnailRequest(QLocalSocket *socket, const QVariantMap &message)
{
const auto thumbnailRequestMessage = message.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestKey).toMap(); const auto thumbnailRequestMessage = message.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestKey).toMap();
const auto thumbnailFilePath = QDir::fromNativeSeparators(thumbnailRequestMessage.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestFilePathKey).toString()); const auto thumbnailFilePath = QDir::fromNativeSeparators(thumbnailRequestMessage.value(VfsShellExtensions::Protocol::FilePathKey).toString());
const auto thumbnailFileSize = thumbnailRequestMessage.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestFileSizeKey).toMap(); const auto thumbnailFileSize = thumbnailRequestMessage.value(VfsShellExtensions::Protocol::ThumbnailProviderRequestFileSizeKey).toMap();
if (thumbnailFilePath.isEmpty() || thumbnailFileSize.isEmpty()) { if (thumbnailFilePath.isEmpty() || thumbnailFileSize.isEmpty()) {

View File

@ -16,8 +16,11 @@
#include <QObject> #include <QObject>
#include <QLocalServer> #include <QLocalServer>
#include <QMutex>
#include <QSize> #include <QSize>
#include <QVariant>
class QJsonDocument;
class QLocalSocket; class QLocalSocket;
namespace OCC { namespace OCC {
@ -32,21 +35,45 @@ class ShellExtensionsServer : public QObject
[[nodiscard]] bool isValid() const { return !path.isEmpty() && !size.isEmpty() && !folderAlias.isEmpty(); } [[nodiscard]] bool isValid() const { return !path.isEmpty() && !size.isEmpty() && !folderAlias.isEmpty(); }
}; };
struct CustomStateRequestInfo
{
QString path;
QString folderAlias;
bool isValid() const { return !path.isEmpty() && !folderAlias.isEmpty(); }
};
Q_OBJECT Q_OBJECT
public: public:
ShellExtensionsServer(QObject *parent = nullptr); ShellExtensionsServer(QObject *parent = nullptr);
~ShellExtensionsServer() override; ~ShellExtensionsServer() override;
static QString getFetchThumbnailPath();
void setIsSharedInvalidationInterval(qint64 interval);
signals:
void fetchSharesJobFinished(const QString &folderAlias);
private: private:
void sendJsonMessageWithVersion(QLocalSocket *socket, const QVariantMap &message); void sendJsonMessageWithVersion(QLocalSocket *socket, const QVariantMap &message);
void sendEmptyDataAndCloseSession(QLocalSocket *socket); void sendEmptyDataAndCloseSession(QLocalSocket *socket);
void closeSession(QLocalSocket *socket); void closeSession(QLocalSocket *socket);
void processCustomStateRequest(QLocalSocket *socket, const CustomStateRequestInfo &customStateRequestInfo);
void processThumbnailRequest(QLocalSocket *socket, const ThumbnailRequestInfo &thumbnailRequestInfo); void processThumbnailRequest(QLocalSocket *socket, const ThumbnailRequestInfo &thumbnailRequestInfo);
void parseCustomStateRequest(QLocalSocket *socket, const QVariantMap &message);
void parseThumbnailRequest(QLocalSocket *socket, const QVariantMap &message);
private slots: private slots:
void slotNewConnection(); void slotNewConnection();
void slotSharesFetched(const QJsonDocument &reply);
void slotSharesFetchError(int statusCode, const QString &message);
private: private:
QLocalServer _localServer; QLocalServer _localServer;
QStringList _runningFetchShareJobsForPaths;
QMap<qintptr, QMetaObject::Connection> _customStateSocketConnections;
qint64 _isSharedInvalidationInterval = 0;
}; };
} // namespace OCC } // namespace OCC

View File

@ -982,6 +982,9 @@ void SocketApi::setFileLock(const QString &localFile, const SyncFileItem::LockSt
} }
shareFolder->accountState()->account()->setLockFileState(fileData.serverRelativePath, shareFolder->journalDb(), lockState); shareFolder->accountState()->account()->setLockFileState(fileData.serverRelativePath, shareFolder->journalDb(), lockState);
shareFolder->journalDb()->schedulePathForRemoteDiscovery(fileData.serverRelativePath);
shareFolder->scheduleThisFolderSoon();
} }
void SocketApi::command_V2_LIST_ACCOUNTS(const QSharedPointer<SocketApiJobV2> &job) const void SocketApi::command_V2_LIST_ACCOUNTS(const QSharedPointer<SocketApiJobV2> &job) const

View File

@ -393,6 +393,8 @@ void BulkPropagatorJob::slotPutFinishedOneFile(const BulkUploadItem &singleFile,
singleFile._item->_etag = etag; singleFile._item->_etag = etag;
singleFile._item->_fileId = getHeaderFromJsonReply(fileReply, "fileid"); singleFile._item->_fileId = getHeaderFromJsonReply(fileReply, "fileid");
singleFile._item->_remotePerm = RemotePermissions::fromServerString(getHeaderFromJsonReply(fileReply, "permissions")); singleFile._item->_remotePerm = RemotePermissions::fromServerString(getHeaderFromJsonReply(fileReply, "permissions"));
singleFile._item->_isShared = singleFile._item->_remotePerm.hasPermission(RemotePermissions::IsShared);
singleFile._item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
if (getHeaderFromJsonReply(fileReply, "X-OC-MTime") != "accepted") { if (getHeaderFromJsonReply(fileReply, "X-OC-MTime") != "accepted") {
// X-OC-MTime is supported since owncloud 5.0. But not when chunking. // X-OC-MTime is supported since owncloud 5.0. But not when chunking.

View File

@ -475,6 +475,8 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
item->_checksumHeader = serverEntry.checksumHeader; item->_checksumHeader = serverEntry.checksumHeader;
item->_fileId = serverEntry.fileId; item->_fileId = serverEntry.fileId;
item->_remotePerm = serverEntry.remotePerm; item->_remotePerm = serverEntry.remotePerm;
item->_isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared);
item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
item->_type = serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile; item->_type = serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile;
item->_etag = serverEntry.etag; item->_etag = serverEntry.etag;
item->_directDownloadUrl = serverEntry.directDownloadUrl; item->_directDownloadUrl = serverEntry.directDownloadUrl;
@ -633,6 +635,8 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
item->_direction = SyncFileItem::Up; item->_direction = SyncFileItem::Up;
item->_fileId = serverEntry.fileId; item->_fileId = serverEntry.fileId;
item->_remotePerm = serverEntry.remotePerm; item->_remotePerm = serverEntry.remotePerm;
item->_isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared);
item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
item->_etag = serverEntry.etag; item->_etag = serverEntry.etag;
item->_type = serverEntry.isDirectory ? CSyncEnums::ItemTypeDirectory : CSyncEnums::ItemTypeFile; item->_type = serverEntry.isDirectory ? CSyncEnums::ItemTypeDirectory : CSyncEnums::ItemTypeFile;
@ -919,6 +923,8 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
item->_remotePerm = base.isValid() ? base._remotePerm : RemotePermissions{}; item->_remotePerm = base.isValid() ? base._remotePerm : RemotePermissions{};
item->_etag = base.isValid() ? base._etag : QByteArray{}; item->_etag = base.isValid() ? base._etag : QByteArray{};
item->_type = base.isValid() ? base._type : localEntry.type; item->_type = base.isValid() ? base._type : localEntry.type;
item->_isShared = base.isValid() ? base._isShared : false;
item->_lastShareStateFetchedTimestmap = base.isValid() ? base._lastShareStateFetchedTimestmap : 0;
}; };
if (!localEntry.isValid()) { if (!localEntry.isValid()) {
@ -1326,6 +1332,8 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
item->_direction = SyncFileItem::Up; item->_direction = SyncFileItem::Up;
item->_fileId = base._fileId; item->_fileId = base._fileId;
item->_remotePerm = base._remotePerm; item->_remotePerm = base._remotePerm;
item->_isShared = base._isShared;
item->_lastShareStateFetchedTimestmap = base._lastShareStateFetchedTimestmap;
item->_etag = base._etag; item->_etag = base._etag;
item->_type = base._type; item->_type = base._type;
@ -1451,6 +1459,8 @@ void ProcessDirectoryJob::processFileConflict(const SyncFileItemPtr &item, Proce
rec._type = item->_type; rec._type = item->_type;
rec._fileSize = serverEntry.size; rec._fileSize = serverEntry.size;
rec._remotePerm = serverEntry.remotePerm; rec._remotePerm = serverEntry.remotePerm;
rec._isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared);
rec._lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
rec._checksumHeader = serverEntry.checksumHeader; rec._checksumHeader = serverEntry.checksumHeader;
const auto result = _discoveryData->_statedb->setFileRecord(rec); const auto result = _discoveryData->_statedb->setFileRecord(rec);
if (!result) { if (!result) {

View File

@ -144,6 +144,8 @@ void PropagateRemoteMkdir::finalizeMkColJob(QNetworkReply::NetworkError err, con
connect(propfindJob, &PropfindJob::result, this, [this, jobPath](const QVariantMap &result){ connect(propfindJob, &PropfindJob::result, this, [this, jobPath](const QVariantMap &result){
propagator()->_activeJobList.removeOne(this); propagator()->_activeJobList.removeOne(this);
_item->_remotePerm = RemotePermissions::fromServerString(result.value(QStringLiteral("permissions")).toString()); _item->_remotePerm = RemotePermissions::fromServerString(result.value(QStringLiteral("permissions")).toString());
_item->_isShared = _item->_remotePerm.hasPermission(RemotePermissions::IsShared);
_item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
if (!_uploadEncryptedHelper && !_item->_isEncrypted) { if (!_uploadEncryptedHelper && !_item->_isEncrypted) {
success(); success();

View File

@ -41,6 +41,8 @@ SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QStri
rec._fileId = _fileId; rec._fileId = _fileId;
rec._fileSize = _size; rec._fileSize = _size;
rec._remotePerm = _remotePerm; rec._remotePerm = _remotePerm;
rec._isShared = _isShared;
rec._lastShareStateFetchedTimestmap = _lastShareStateFetchedTimestmap;
rec._serverHasIgnoredFiles = _serverHasIgnoredFiles; rec._serverHasIgnoredFiles = _serverHasIgnoredFiles;
rec._checksumHeader = _checksumHeader; rec._checksumHeader = _checksumHeader;
rec._e2eMangledName = _encryptedFileName.toUtf8(); rec._e2eMangledName = _encryptedFileName.toUtf8();
@ -89,6 +91,8 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec
item->_lockEditorApp = rec._lockstate._lockEditorApp; item->_lockEditorApp = rec._lockstate._lockEditorApp;
item->_lockTime = rec._lockstate._lockTime; item->_lockTime = rec._lockstate._lockTime;
item->_lockTimeout = rec._lockstate._lockTimeout; item->_lockTimeout = rec._lockstate._lockTimeout;
item->_isShared = rec._isShared;
item->_lastShareStateFetchedTimestmap = rec._lastShareStateFetchedTimestmap;
return item; return item;
} }

View File

@ -308,6 +308,9 @@ public:
QString _lockEditorApp; QString _lockEditorApp;
qint64 _lockTime = 0; qint64 _lockTime = 0;
qint64 _lockTimeout = 0; qint64 _lockTimeout = 0;
bool _isShared = false;
time_t _lastShareStateFetchedTimestmap = 0;
}; };
inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2) inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2)

View File

@ -442,7 +442,8 @@ bool createSyncRootRegistryKeys(const QString &providerName, const QString &fold
{ providerSyncRootIdRegistryKey, QStringLiteral("Flags"), REG_DWORD, flags }, { providerSyncRootIdRegistryKey, QStringLiteral("Flags"), REG_DWORD, flags },
{ providerSyncRootIdRegistryKey, QStringLiteral("DisplayNameResource"), REG_EXPAND_SZ, displayName }, { providerSyncRootIdRegistryKey, QStringLiteral("DisplayNameResource"), REG_EXPAND_SZ, displayName },
{ providerSyncRootIdRegistryKey, QStringLiteral("IconResource"), REG_EXPAND_SZ, QString(QDir::toNativeSeparators(qApp->applicationFilePath()) + QStringLiteral(",0")) }, { providerSyncRootIdRegistryKey, QStringLiteral("IconResource"), REG_EXPAND_SZ, QString(QDir::toNativeSeparators(qApp->applicationFilePath()) + QStringLiteral(",0")) },
{ providerSyncRootIdUserSyncRootsRegistryKey, windowsSid, REG_SZ, syncRootPath}, { providerSyncRootIdUserSyncRootsRegistryKey, windowsSid, REG_SZ, syncRootPath},
{ providerSyncRootIdRegistryKey, QStringLiteral("CustomStateHandler"), REG_SZ, CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG},
{ providerSyncRootIdRegistryKey, QStringLiteral("ThumbnailProvider"), REG_SZ, CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG}, { providerSyncRootIdRegistryKey, QStringLiteral("ThumbnailProvider"), REG_SZ, CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG},
{ providerSyncRootIdRegistryKey, QStringLiteral("NamespaceCLSID"), REG_SZ, QString(navigationPaneClsid)} { providerSyncRootIdRegistryKey, QStringLiteral("NamespaceCLSID"), REG_SZ, QString(navigationPaneClsid)}
}; };
@ -550,6 +551,7 @@ void unregisterSyncRootShellExtensions(const QString &providerName, const QStrin
const QString providerSyncRootIdRegistryKey = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId; const QString providerSyncRootIdRegistryKey = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId;
OCC::Utility::registryDeleteKeyValue(HKEY_LOCAL_MACHINE, providerSyncRootIdRegistryKey, QStringLiteral("ThumbnailProvider")); OCC::Utility::registryDeleteKeyValue(HKEY_LOCAL_MACHINE, providerSyncRootIdRegistryKey, QStringLiteral("ThumbnailProvider"));
OCC::Utility::registryDeleteKeyValue(HKEY_LOCAL_MACHINE, providerSyncRootIdRegistryKey, QStringLiteral("CustomStateHandler"));
qCInfo(lcCfApiWrapper) << "Successfully unregistered SyncRoot Shell Extensions!"; qCInfo(lcCfApiWrapper) << "Successfully unregistered SyncRoot Shell Extensions!";
} }

View File

@ -1,18 +1,197 @@
include(GenerateIconsUtils)
# 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()
# indeces 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}")
# 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 add_library(CfApiShellExtensions MODULE
dllmain.cpp dllmain.cpp
cfapishellintegrationclassfactory.cpp cfapishellintegrationclassfactory.cpp
customstateprovideripc.cpp
ipccommon.cpp
thumbnailprovider.cpp thumbnailprovider.cpp
thumbnailprovideripc.cpp thumbnailprovideripc.cpp
${CMAKE_SOURCE_DIR}/src/common/shellextensionutils.cpp ${CMAKE_SOURCE_DIR}/src/common/shellextensionutils.cpp
customstateprovider.cpp
CfApiShellIntegration.def CfApiShellIntegration.def
) )
target_link_libraries(CfApiShellExtensions shlwapi Gdiplus Nextcloud::csync Qt5::Core Qt5::Network) 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 Qt5::Core Qt5::Network)
target_include_directories(CfApiShellExtensions PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(CfApiShellExtensions PRIVATE ${GeneratedFilesPath}) target_include_directories(CfApiShellExtensions PRIVATE ${GeneratedFilesPath})
target_include_directories(CfApiShellExtensions PRIVATE ${CMAKE_SOURCE_DIR}) target_include_directories(CfApiShellExtensions PRIVATE ${CMAKE_SOURCE_DIR})
target_compile_features(CfApiShellExtensions PRIVATE cxx_std_17)
set_target_properties(CfApiShellExtensions set_target_properties(CfApiShellExtensions
PROPERTIES PROPERTIES
LIBRARY_OUTPUT_NAME LIBRARY_OUTPUT_NAME
@ -29,3 +208,5 @@ install(TARGETS CfApiShellExtensions
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}
) )
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configvfscfapishellext.h.in ${CMAKE_CURRENT_BINARY_DIR}/configvfscfapishellext.h)

View File

@ -0,0 +1,21 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@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.
*/
namespace CfApiShellExtensions
{
runtimeclass CustomStateProvider : [default] Windows.Storage.Provider.IStorageProviderItemPropertySource
{
CustomStateProvider();
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@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.
*/
#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

View File

@ -0,0 +1,104 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@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 "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(QString("Value%1").arg(stateValue).toStdWString());
itemProperty.IconResource(QString(_dllFilePath + QString(",%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;
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@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 "Generated/CfApiShellExtensions/customstateprovider.g.h"
#include "config.h"
#include <winrt/windows.foundation.collections.h>
#include <windows.storage.provider.h>
#include <QString>
#include <QMap>
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>
{
};
}

View File

@ -0,0 +1,104 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@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 "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 = {};
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@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 <QtNetwork/QLocalSocket>
#include <QString>
#include <QVariant>
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;
};
}

View File

@ -13,16 +13,20 @@
*/ */
#include "cfapishellintegrationclassfactory.h" #include "cfapishellintegrationclassfactory.h"
#include "customstateprovider.h"
#include "thumbnailprovider.h" #include "thumbnailprovider.h"
#include <comdef.h> #include <comdef.h>
long dllReferenceCount = 0; long dllReferenceCount = 0;
long dllObjectsCount = 0;
HINSTANCE instanceHandle = NULL; HINSTANCE instanceHandle = NULL;
HRESULT CustomStateProvider_CreateInstance(REFIID riid, void **ppv);
HRESULT ThumbnailProvider_CreateInstance(REFIID riid, void **ppv); HRESULT ThumbnailProvider_CreateInstance(REFIID riid, void **ppv);
const VfsShellExtensions::ClassObjectInit listClassesSupported[] = { const VfsShellExtensions::ClassObjectInit listClassesSupported[] = {
{&__uuidof(winrt::CfApiShellExtensions::implementation::CustomStateProvider), CustomStateProvider_CreateInstance},
{&__uuidof(VfsShellExtensions::ThumbnailProvider), ThumbnailProvider_CreateInstance} {&__uuidof(VfsShellExtensions::ThumbnailProvider), ThumbnailProvider_CreateInstance}
}; };
@ -30,6 +34,9 @@ STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void *)
{ {
if (dwReason == DLL_PROCESS_ATTACH) { if (dwReason == DLL_PROCESS_ATTACH) {
instanceHandle = hInstance; instanceHandle = hInstance;
wchar_t dllFilePath[_MAX_PATH] = {0};
::GetModuleFileName(instanceHandle, dllFilePath, _MAX_PATH);
winrt::CfApiShellExtensions::implementation::CustomStateProvider::setDllFilePath(dllFilePath);
DisableThreadLibraryCalls(hInstance); DisableThreadLibraryCalls(hInstance);
} }
@ -38,7 +45,7 @@ STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void *)
STDAPI DllCanUnloadNow() STDAPI DllCanUnloadNow()
{ {
return dllReferenceCount == 0 ? S_OK : S_FALSE; return (dllReferenceCount == 0 && dllObjectsCount == 0) ? S_OK : S_FALSE;
} }
STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv) STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv)
@ -46,6 +53,16 @@ STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv)
return VfsShellExtensions::CfApiShellIntegrationClassFactory::CreateInstance(clsid, listClassesSupported, ARRAYSIZE(listClassesSupported), riid, 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) HRESULT ThumbnailProvider_CreateInstance(REFIID riid, void **ppv)
{ {
auto *thumbnailProvider = new (std::nothrow) VfsShellExtensions::ThumbnailProvider(); auto *thumbnailProvider = new (std::nothrow) VfsShellExtensions::ThumbnailProvider();

View File

@ -0,0 +1,50 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@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 "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;
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@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 <QString>
namespace VfsShellExtensions {
QString findServerNameForPath(const QString &filePath);
}

View File

@ -48,6 +48,8 @@
#include <shlwapi.h> #include <shlwapi.h>
#include <QSize> #include <QSize>
extern long dllObjectsCount;
namespace VfsShellExtensions { namespace VfsShellExtensions {
std::pair<HBITMAP, WTS_ALPHATYPE> hBitmapAndAlphaTypeFromData(const QByteArray &thumbnailData) std::pair<HBITMAP, WTS_ALPHATYPE> hBitmapAndAlphaTypeFromData(const QByteArray &thumbnailData)
@ -93,8 +95,13 @@ std::pair<HBITMAP, WTS_ALPHATYPE> hBitmapAndAlphaTypeFromData(const QByteArray &
ThumbnailProvider::ThumbnailProvider() ThumbnailProvider::ThumbnailProvider()
: _referenceCount(1) : _referenceCount(1)
{ {
InterlockedIncrement(&dllObjectsCount);
} }
ThumbnailProvider::~ThumbnailProvider()
{
InterlockedDecrement(&dllObjectsCount);
}
IFACEMETHODIMP ThumbnailProvider::QueryInterface(REFIID riid, void **ppv) IFACEMETHODIMP ThumbnailProvider::QueryInterface(REFIID riid, void **ppv)
{ {
static const QITAB qit[] = { static const QITAB qit[] = {

View File

@ -30,7 +30,7 @@ class __declspec(uuid(CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID)) ThumbnailProvi
public: public:
ThumbnailProvider(); ThumbnailProvider();
virtual ~ThumbnailProvider() = default; virtual ~ThumbnailProvider();
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv); IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv);

View File

@ -14,14 +14,11 @@
#include "thumbnailprovideripc.h" #include "thumbnailprovideripc.h"
#include "common/shellextensionutils.h" #include "common/shellextensionutils.h"
#include "common/utility.h" #include "ipccommon.h"
#include <QString> #include <QString>
#include <QSize> #include <QSize>
#include <QtNetwork/QLocalSocket> #include <QtNetwork/QLocalSocket>
#include <QJsonDocument> #include <QJsonDocument>
#include <QObject>
#include <QDir>
#include <Windows.h>
namespace { 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()) // 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; constexpr auto socketTimeoutMs = 10000;
@ -61,7 +58,7 @@ QByteArray ThumbnailProviderIpc::fetchThumbnailForFile(const QString &filePath,
{ {
VfsShellExtensions::Protocol::ThumbnailProviderRequestKey, VfsShellExtensions::Protocol::ThumbnailProviderRequestKey,
QVariantMap { QVariantMap {
{VfsShellExtensions::Protocol::ThumbnailProviderRequestFilePathKey, filePath}, {VfsShellExtensions::Protocol::FilePathKey, filePath},
{VfsShellExtensions::Protocol::ThumbnailProviderRequestFileSizeKey, QVariantMap{{QStringLiteral("width"), size.width()}, {QStringLiteral("height"), size.height()}}} {VfsShellExtensions::Protocol::ThumbnailProviderRequestFileSizeKey, QVariantMap{{QStringLiteral("width"), size.width()}, {QStringLiteral("height"), size.height()}}}
} }
} }
@ -99,26 +96,8 @@ QString ThumbnailProviderIpc::getServerNameForPath(const QString &filePath)
if (!overrideServerName.isEmpty()) { if (!overrideServerName.isEmpty()) {
return overrideServerName; return overrideServerName;
} }
// 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)) { return findServerNameForPath(filePath);
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;
} }
bool ThumbnailProviderIpc::connectSocketToServer(const QString &serverName) bool ThumbnailProviderIpc::connectSocketToServer(const QString &serverName)

View File

@ -40,12 +40,16 @@ const auto rootKey = HKEY_CURRENT_USER;
bool registerShellExtension() 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 // 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"))); const auto shellExtensionDllPath = QDir::toNativeSeparators(QString(QCoreApplication::applicationDirPath() + QStringLiteral("/") + CFAPI_SHELL_EXTENSIONS_LIB_NAME + QStringLiteral(".dll")));
if (!QFileInfo::exists(shellExtensionDllPath)) { if (!QFileInfo::exists(shellExtensionDllPath)) {
Q_ASSERT(false); Q_ASSERT(false);
qCWarning(lcCfApi) << "Register CfAPI shell extensions failed. Dll does not exist in " qCWarning(lcCfApi) << "Register CfAPI shell extensions failed. Dll does not exist in " << QCoreApplication::applicationDirPath();
<< QCoreApplication::applicationDirPath();
return false; return false;
} }
@ -57,20 +61,22 @@ bool registerShellExtension()
return false; return false;
} }
const QString clsidPath = QString() % clsIdRegKey % CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG; for (const auto extension : listExtensions) {
const QString clsidServerPath = clsidPath % R"(\InprocServer32)"; 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)) { if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, QStringLiteral("AppID"), REG_SZ, CFAPI_SHELLEXT_APPID_REG)) {
return false; return false;
} }
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, {}, REG_SZ, CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME)) { if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, {}, REG_SZ, extension.first)) {
return false; return false;
} }
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, {}, REG_SZ, shellExtensionDllPath)) { if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, {}, REG_SZ, shellExtensionDllPath)) {
return false; return false;
} }
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, QStringLiteral("ThreadingModel"), REG_SZ, QStringLiteral("Apartment"))) { if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, QStringLiteral("ThreadingModel"), REG_SZ, QStringLiteral("Apartment"))) {
return false; return false;
}
} }
return true; return true;
@ -83,9 +89,16 @@ void unregisterShellExtensions()
OCC::Utility::registryDeleteKeyTree(rootKey, appIdPath); OCC::Utility::registryDeleteKeyTree(rootKey, appIdPath);
} }
const QString clsidPath = QString() % clsIdRegKey % CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG; const QStringList listExtensions = {
if (OCC::Utility::registryKeyExists(rootKey, clsidPath)) { CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG,
OCC::Utility::registryDeleteKeyTree(rootKey, clsidPath); 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);
}
} }
} }

View File

@ -76,7 +76,7 @@ if (WIN32)
nextcloud_add_test(SyncCfApi) nextcloud_add_test(SyncCfApi)
nextcloud_add_test(CfApiShellExtensionsIPC) nextcloud_add_test(CfApiShellExtensionsIPC)
target_sources(CfApiShellExtensionsIPCTest PRIVATE "${CMAKE_SOURCE_DIR}/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp") target_sources(CfApiShellExtensionsIPCTest PRIVATE "${CMAKE_SOURCE_DIR}/src/libsync/vfs/cfapi/shellext/thumbnailprovideripc.cpp" "${CMAKE_SOURCE_DIR}/src/libsync/vfs/cfapi/shellext/customstateprovideripc.cpp" "${CMAKE_SOURCE_DIR}/src/libsync/vfs/cfapi/shellext/ipccommon.cpp")
elseif(LINUX) # elseif(LINUX OR APPLE) elseif(LINUX) # elseif(LINUX OR APPLE)
nextcloud_add_test(SyncXAttr) nextcloud_add_test(SyncXAttr)
endif() endif()

View File

@ -5,22 +5,121 @@
* *
*/ */
#include <account.h>
#include <accountstate.h>
#include <accountmanager.h>
#include <common/vfs.h>
#include <common/shellextensionutils.h>
#include "config.h"
#include <folderman.h>
#include <libsync/vfs/cfapi/shellext/configvfscfapishellext.h>
#include <ocssharejob.h>
#include <shellextensionsserver.h>
#include <syncengine.h>
#include "syncenginetestutils.h"
#include "testhelper.h"
#include <vfs/cfapi/shellext/customstateprovideripc.h>
#include <vfs/cfapi/shellext/thumbnailprovideripc.h>
#include <QtTest> #include <QtTest>
#include <QImage> #include <QImage>
#include <QPainter> #include <QPainter>
#include "syncenginetestutils.h"
#include "common/vfs.h"
#include "common/shellextensionutils.h"
#include "config.h"
#include <syncengine.h>
#include "folderman.h" namespace {
#include "account.h" static constexpr auto roootFolderName = "A";
#include "accountstate.h" static constexpr auto imagesFolderName = "photos";
#include "accountmanager.h" static constexpr auto filesFolderName = "files";
#include "testhelper.h"
#include "vfs/cfapi/shellext/thumbnailprovideripc.h" static const QByteArray fakeNoSharesResponse = R"({"ocs":{"data":[],"meta":{"message":"OK","status":"ok","statuscode":200}}})";
#include "shellextensionsserver.h"
static const QByteArray fakeSharedFilesResponse = R"({"ocs":{"data":[{
"attributes": null,
"can_delete": true,
"can_edit": true,
"displayname_file_owner": "admin",
"displayname_owner": "admin",
"expiration": null,
"file_parent": 2981,
"file_source": 3538,
"file_target": "/test_shared_file.txt",
"has_preview": true,
"hide_download": 0,
"id": "36",
"item_source": 3538,
"item_type": "file",
"label": null,
"mail_send": 0,
"mimetype": "text/plain",
"note": "",
"parent": null,
"path": "A/files/test_shared_file.txt",
"permissions": 19,
"share_type": 0,
"share_with": "newstandard",
"share_with_displayname": "newstandard",
"share_with_displayname_unique": "newstandard",
"status": {
"clearAt": null,
"icon": null,
"message": null,
"status": "offline"
},
"stime": 1662995777,
"storage": 2,
"storage_id": "home::admin",
"token": null,
"uid_file_owner": "admin",
"uid_owner": "admin"
},
{
"attributes": null,
"can_delete": true,
"can_edit": true,
"displayname_file_owner": "admin",
"displayname_owner": "admin",
"expiration": null,
"file_parent": 2981,
"file_source": 3538,
"file_target": "/test_shared_and_locked_file.txt",
"has_preview": true,
"hide_download": 0,
"id": "36",
"item_source": 3538,
"item_type": "file",
"label": null,
"mail_send": 0,
"mimetype": "text/plain",
"note": "",
"parent": null,
"path": "A/files/test_shared_and_locked_file.txt",
"permissions": 19,
"share_type": 0,
"share_with": "newstandard",
"share_with_displayname": "newstandard",
"share_with_displayname_unique": "newstandard",
"status": {
"clearAt": null,
"icon": null,
"message": null,
"status": "offline"
},
"stime": 1662995777,
"storage": 2,
"storage_id": "home::admin",
"token": null,
"uid_file_owner": "admin",
"uid_owner": "admin"
}
],
"meta": {
"message": "OK",
"status": "ok",
"statuscode": 200
}
}
})";
static constexpr auto shellExtensionServerOverrideIntervalMs = 1000LL * 2LL;
}
using namespace OCC; using namespace OCC;
@ -38,21 +137,39 @@ class TestCfApiShellExtensionsIPC : public QObject
QScopedPointer<ShellExtensionsServer> _shellExtensionsServer; QScopedPointer<ShellExtensionsServer> _shellExtensionsServer;
QStringList dummmyImageNames = { const QStringList dummmyImageNames = {
"A/photos/imageJpg.jpg", { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imageJpg.jpg")) },
"A/photos/imagePng.png", { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.png")) },
"A/photos/imagePng.bmp", { QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.bmp")) }
}; };
QMap<QString, QByteArray> dummyImages; QMap<QString, QByteArray> dummyImages;
QString currentImage; QString currentImage;
struct FileStates
{
bool _isShared = false;
bool _isLocked = false;
};
const QMap<QString, FileStates> dummyFileStates = {
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_locked_file.txt")), { false, true } },
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_shared_file.txt")), { true, false } },
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_shared_and_locked_file.txt")), { true, true }},
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName) + QLatin1Char('/') + QStringLiteral("test_non_shared_and_non_locked_file.txt")), { false, false }}
};
public:
static bool replyWithNoShares;
private slots: private slots:
void initTestCase() void initTestCase()
{ {
VfsShellExtensions::ThumbnailProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault(); VfsShellExtensions::ThumbnailProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
VfsShellExtensions::CustomStateProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
_shellExtensionsServer.reset(new ShellExtensionsServer); _shellExtensionsServer.reset(new ShellExtensionsServer);
_shellExtensionsServer->setIsSharedInvalidationInterval(shellExtensionServerOverrideIntervalMs);
for (const auto &dummyImageName : dummmyImageNames) { for (const auto &dummyImageName : dummmyImageNames) {
const auto extension = dummyImageName.split(".").last(); const auto extension = dummyImageName.split(".").last();
@ -68,6 +185,16 @@ private slots:
dummyImages.insert(dummyImageName, byteArray); dummyImages.insert(dummyImageName, byteArray);
} }
fakeFolder.remoteModifier().mkdir(roootFolderName);
fakeFolder.remoteModifier().mkdir(QString(roootFolderName) + QLatin1Char('/') + QString(filesFolderName));
fakeFolder.remoteModifier().mkdir(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName));
for (const auto &fileStateKey : dummyFileStates.keys()) {
fakeFolder.remoteModifier().insert(fileStateKey, 256);
}
fakeQnam.reset(new FakeQNAM({})); fakeQnam.reset(new FakeQNAM({}));
account = OCC::Account::create(); account = OCC::Account::create();
account->setCredentials(new FakeCredentials{fakeQnam.data()}); account->setCredentials(new FakeCredentials{fakeQnam.data()});
@ -86,31 +213,43 @@ private slots:
Q_UNUSED(device); Q_UNUSED(device);
QNetworkReply *reply = nullptr; QNetworkReply *reply = nullptr;
const auto urlQuery = QUrlQuery(req.url());
const auto fileId = urlQuery.queryItemValue(QStringLiteral("fileId"));
const auto x = urlQuery.queryItemValue(QStringLiteral("x")).toInt();
const auto y = urlQuery.queryItemValue(QStringLiteral("y")).toInt();
const auto path = req.url().path(); const auto path = req.url().path();
if (fileId.isEmpty() || x <= 0 || y <= 0) { if (path.endsWith(OCC::OcsShareJob::_pathForSharesRequest)) {
reply = new FakePayloadReply(op, req, {}, nullptr); const auto jsonReply = TestCfApiShellExtensionsIPC::replyWithNoShares ? fakeNoSharesResponse : fakeSharedFilesResponse;
} else { TestCfApiShellExtensionsIPC::replyWithNoShares = false;
const auto foundImageIt = dummyImages.find(currentImage); auto fakePayloadReply = new FakePayloadReply(op, req, jsonReply, nullptr);
QByteArray byteArray;
if (foundImageIt != dummyImages.end()) {
byteArray = foundImageIt.value();
}
currentImage.clear();
auto fakePayloadReply = new FakePayloadReply(op, req, byteArray, nullptr);
QMap<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = { QMap<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = {
{QNetworkRequest::KnownHeaders::ContentTypeHeader, "image/jpeg"}}; {QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"}};
fakePayloadReply->_additionalHeaders = additionalHeaders; fakePayloadReply->_additionalHeaders = additionalHeaders;
reply = fakePayloadReply; reply = fakePayloadReply;
} else if (path.endsWith(ShellExtensionsServer::getFetchThumbnailPath())) {
const auto urlQuery = QUrlQuery(req.url());
const auto fileId = urlQuery.queryItemValue(QStringLiteral("fileId"));
const auto x = urlQuery.queryItemValue(QStringLiteral("x")).toInt();
const auto y = urlQuery.queryItemValue(QStringLiteral("y")).toInt();
if (fileId.isEmpty() || x <= 0 || y <= 0) {
reply = new FakePayloadReply(op, req, {}, nullptr);
} else {
const auto foundImageIt = dummyImages.find(currentImage);
QByteArray byteArray;
if (foundImageIt != dummyImages.end()) {
byteArray = foundImageIt.value();
}
currentImage.clear();
auto fakePayloadReply = new FakePayloadReply(op, req, byteArray, nullptr);
QMap<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = {
{QNetworkRequest::KnownHeaders::ContentTypeHeader, "image/jpeg"}};
fakePayloadReply->_additionalHeaders = additionalHeaders;
reply = fakePayloadReply;
}
} else {
reply = new FakePayloadReply(op, req, {}, nullptr);
} }
return reply; return reply;
@ -126,6 +265,7 @@ private slots:
folder->setVirtualFilesEnabled(true); folder->setVirtualFilesEnabled(true);
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
ItemCompletedSpy completeSpy(fakeFolder); ItemCompletedSpy completeSpy(fakeFolder);
@ -135,8 +275,6 @@ private slots:
cleanup(); cleanup();
// Create a virtual file for remote files // Create a virtual file for remote files
fakeFolder.remoteModifier().mkdir("A");
fakeFolder.remoteModifier().mkdir("A/photos");
for (const auto &dummyImageName : dummmyImageNames) { for (const auto &dummyImageName : dummmyImageNames) {
fakeFolder.remoteModifier().insert(dummyImageName, 256); fakeFolder.remoteModifier().insert(dummyImageName, 256);
} }
@ -198,6 +336,137 @@ private slots:
QVERIFY(thumbnailReplyData.isEmpty()); QVERIFY(thumbnailReplyData.isEmpty());
} }
void testRequestCustomStates()
{
FolderMan *folderman = FolderMan::instance();
QVERIFY(folderman);
auto folder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
QVERIFY(folder);
folder->setVirtualFilesEnabled(true);
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// just add records from fake folder's journal to real one's to make test work
SyncJournalFileRecord record;
auto realFolder = FolderMan::instance()->folderForPath(fakeFolder.localPath());
QVERIFY(realFolder);
for (auto it = std::begin(dummyFileStates); it != std::end(dummyFileStates); ++it) {
if (fakeFolder.syncJournal().getFileRecord(it.key(), &record)) {
record._isShared = it.value()._isShared;
if (record._isShared) {
record._remotePerm.setPermission(OCC::RemotePermissions::Permissions::IsShared);
}
record._lockstate._locked = it.value()._isLocked;
if (record._lockstate._locked) {
record._lockstate._lockOwnerId = "admin@example.cloud.com";
record._lockstate._lockOwnerDisplayName = "Admin";
record._lockstate._lockOwnerType = static_cast<int>(SyncFileItem::LockOwnerType::UserLock);
record._lockstate._lockTime = QDateTime::currentMSecsSinceEpoch();
record._lockstate._lockTimeout = 1000 * 60 * 60;
}
QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
QVERIFY(realFolder->journalDb()->setFileRecord(record));
}
}
// #1 Test every file's states fetching. Everything must succeed.
for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
QEventLoop loop;
QVariantList customStates;
std::thread t([&] {
VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
});
loop.exec();
t.detach();
QVERIFY(!customStates.isEmpty() || (!it.value()._isLocked && !it.value()._isShared));
}
// #2 Test wrong file's states fetching. It must fail.
QEventLoop loop;
QVariantList customStates;
std::thread t1([&] {
VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/wrong.jpg"));
QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
});
loop.exec();
t1.detach();
QVERIFY(customStates.isEmpty());
// #3 Test wrong file states fetching. It must fail.
customStates.clear();
std::thread t2([&] {
VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/test_non_shared_and_non_locked_file.txt"));
QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
});
loop.exec();
t2.detach();
QVERIFY(customStates.isEmpty());
// reset all share states to make sure we'll get new states when fetching
for (auto it = std::begin(dummyFileStates); it != std::end(dummyFileStates); ++it) {
if (fakeFolder.syncJournal().getFileRecord(it.key(), &record)) {
record._remotePerm.unsetPermission(OCC::RemotePermissions::Permissions::IsShared);
record._isShared = false;
QVERIFY(fakeFolder.syncJournal().setFileRecord(record));
QVERIFY(realFolder->journalDb()->setFileRecord(record));
}
}
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
//
// wait enough time to make shares' state invalid
QTest::qWait(shellExtensionServerOverrideIntervalMs + 1000);
// #4 Test every file's states fetching. Everything must succeed.
for (auto it = std::cbegin(dummyFileStates); it != std::cend(dummyFileStates); ++it) {
QEventLoop loop;
QVariantList customStates;
std::thread t([&] {
VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + it.key());
QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
});
loop.exec();
t.detach();
QVERIFY(!customStates.isEmpty() || (!it.value()._isLocked && !it.value()._isShared));
if (!customStates.isEmpty()) {
const auto lockedIndex = QString(CUSTOM_STATE_ICON_LOCKED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
const auto sharedIndex = QString(CUSTOM_STATE_ICON_SHARED_INDEX).toInt() - QString(CUSTOM_STATE_ICON_INDEX_OFFSET).toInt();
if (customStates.contains(lockedIndex) && customStates.contains(sharedIndex)) {
QVERIFY(it.value()._isLocked && it.value()._isShared);
}
if (customStates.contains(lockedIndex)) {
QVERIFY(it.value()._isLocked);
}
if (customStates.contains(sharedIndex)) {
QVERIFY(it.value()._isShared);
}
}
}
// #5 Test no shares response for a file
QTest::qWait(shellExtensionServerOverrideIntervalMs + 1000);
TestCfApiShellExtensionsIPC::replyWithNoShares = true;
customStates.clear();
std::thread t3([&] {
VfsShellExtensions::CustomStateProviderIpc customStateProviderIpc;
customStates = customStateProviderIpc.fetchCustomStatesForFile(fakeFolder.localPath() + QStringLiteral("A/files/test_non_shared_and_non_locked_file.txt"));
QMetaObject::invokeMethod(&loop, &QEventLoop::quit, Qt::QueuedConnection);
});
loop.exec();
t3.detach();
QVERIFY(customStates.isEmpty());
}
void cleanupTestCase() void cleanupTestCase()
{ {
VfsShellExtensions::ThumbnailProviderIpc::overrideServerName.clear(); VfsShellExtensions::ThumbnailProviderIpc::overrideServerName.clear();
@ -212,5 +481,7 @@ private slots:
} }
}; };
bool TestCfApiShellExtensionsIPC::replyWithNoShares = false;
QTEST_GUILESS_MAIN(TestCfApiShellExtensionsIPC) QTEST_GUILESS_MAIN(TestCfApiShellExtensionsIPC)
#include "testcfapishellextensionsipc.moc" #include "testcfapishellextensionsipc.moc"

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 16 16" width="16" height="16"><path d="m8 1c-2.319 0-3.967 1.8644-4 4v2.5h-1.5v7.5h11v-7.5h-1.5v-2.5c0-2.27-1.8-3.9735-4-4zm0 2c1.25 0 2 0.963 2 2v2.5h-4v-2.5c0-1.174 0.747-2 2-2z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 268 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.1" viewBox="0 0 16 16"><circle cx="3.5" cy="8" r="2.5"/><circle cy="12.5" cx="12.5" r="2.5"/><circle cx="12.5" cy="3.5" r="2.5"/><path d="m3.5 8 9 4.5m-9-4.5 9-4.5" stroke="#000" stroke-width="2" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B