VFS Windows: Display the sharing state and lock state in the 'Status' column of Windows Explorer
Signed-off-by: allexzander <blackslayer4@gmail.com>
|
@ -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_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_REG "{${CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID}}" )
|
||||
set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME "${APPLICATION_NAME} Thumbnail Handler" )
|
||||
|
|
|
@ -53,19 +53,25 @@ End Function
|
|||
Function RegistryCleanupCfApiShellExtensions()
|
||||
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@"
|
||||
strShellExtCustomStateHandlerClsId = "Software\Classes\CLSID\@CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG@"
|
||||
|
||||
rootKey = HKEY_CURRENT_USER
|
||||
|
||||
If objRegistry.EnumKey(rootKey, strShellExtThumbnailHandlerAppId, arrSubKeys) = 0 Then
|
||||
RegistryDeleteKeyRecursive rootKey, strShellExtThumbnailHandlerAppId
|
||||
If objRegistry.EnumKey(rootKey, strShellExtAppId, arrSubKeys) = 0 Then
|
||||
RegistryDeleteKeyRecursive rootKey, strShellExtAppId
|
||||
End If
|
||||
|
||||
If objRegistry.EnumKey(rootKey, strShellExtThumbnailHandlerClsId, arrSubKeys) = 0 Then
|
||||
RegistryDeleteKeyRecursive rootKey, strShellExtThumbnailHandlerClsId
|
||||
End If
|
||||
|
||||
If objRegistry.EnumKey(rootKey, strShellExtCustomStateHandlerClsId, arrSubKeys) = 0 Then
|
||||
RegistryDeleteKeyRecursive rootKey, strShellExtCustomStateHandlerClsId
|
||||
End If
|
||||
|
||||
End Function
|
||||
|
||||
Function RegistryCleanup()
|
||||
|
|
|
@ -102,11 +102,13 @@ include(CMakeParseArguments)
|
|||
|
||||
function(ecm_add_app_icon appsources)
|
||||
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)
|
||||
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
|
||||
if (NOT ARG_ICON_INDEX)
|
||||
set(ARG_ICON_INDEX 1)
|
||||
if (ARG_DO_NOT_GENERATE_RC_FILE)
|
||||
set (_do_not_generate_rc_file TRUE)
|
||||
else()
|
||||
set (_do_not_generate_rc_file FALSE)
|
||||
endif()
|
||||
|
||||
if(NOT ARG_ICONS)
|
||||
|
@ -211,15 +213,17 @@ function(ecm_add_app_icon appsources)
|
|||
DEPENDS ${deps}
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
)
|
||||
# this bit's a little hacky to make the dependency stuff work
|
||||
file(WRITE "${_outfilename}.rc.in" "IDI_ICON${ARG_ICON_INDEX} ICON DISCARDABLE \"${_outfilename}.ico\"\n")
|
||||
add_custom_command(
|
||||
OUTPUT "${_outfilename}.rc"
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
ARGS -E copy "${_outfilename}.rc.in" "${_outfilename}.rc"
|
||||
DEPENDS ${ARG_RC_DEPENDENCIES} "${_outfilename}.ico"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
)
|
||||
if (NOT _do_not_generate_rc_file)
|
||||
# this bit's a little hacky to make the dependency stuff work
|
||||
file(WRITE "${_outfilename}.rc.in" "IDI_ICON${ARG_ICON_INDEX} ICON DISCARDABLE \"${_outfilename}.ico\"\n")
|
||||
add_custom_command(
|
||||
OUTPUT "${_outfilename}.rc"
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
ARGS -E copy "${_outfilename}.rc.in" "${_outfilename}.rc"
|
||||
DEPENDS ${ARG_RC_DEPENDENCIES} "${_outfilename}.ico"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
if (IcoTool_FOUND)
|
||||
|
|
|
@ -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()
|
|
@ -48,6 +48,10 @@
|
|||
#cmakedefine CFAPI_SHELLEXT_APPID_REG "@CFAPI_SHELLEXT_APPID_REG@"
|
||||
#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_REG "@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG@"
|
||||
#cmakedefine CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME "@CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME@"
|
||||
|
|
|
@ -29,7 +29,6 @@ namespace Protocol {
|
|||
if (!valid) {
|
||||
qCWarning(lcShellExtensionUtils) << "Invalid shell extensions IPC protocol: " << message.value(QStringLiteral("version")) << " vs " << Version;
|
||||
}
|
||||
Q_ASSERT(valid);
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,11 +23,14 @@ QString serverNameForApplicationName(const QString &applicationName);
|
|||
QString serverNameForApplicationNameDefault();
|
||||
|
||||
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 ThumbnailProviderRequestFilePathKey = "filePath";
|
||||
static constexpr auto ThumbnailProviderRequestFileSizeKey = "fileSize";
|
||||
static constexpr auto ThumnailProviderDataKey = "thumbnailData";
|
||||
static constexpr auto Version = "1.0";
|
||||
static constexpr auto Version = "2.0";
|
||||
|
||||
QByteArray createJsonMessage(const QVariantMap &message);
|
||||
bool validateProtocolVersion(const QVariantMap &message);
|
||||
|
|
|
@ -49,7 +49,7 @@ Q_LOGGING_CATEGORY(lcDb, "nextcloud.sync.database", QtInfoMsg)
|
|||
#define GET_FILE_RECORD_QUERY \
|
||||
"SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize," \
|
||||
" ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, " \
|
||||
" lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout " \
|
||||
" lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap " \
|
||||
" FROM metadata" \
|
||||
" 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._lockTime = query.int64Value(17);
|
||||
rec._lockstate._lockTimeout = query.int64Value(18);
|
||||
rec._isShared = query.intValue(19) > 0;
|
||||
rec._lastShareStateFetchedTimestmap = query.int64Value(20);
|
||||
}
|
||||
|
||||
static QByteArray defaultJournalMode(const QString &dbPath)
|
||||
|
@ -727,6 +729,8 @@ bool SyncJournalDb::updateMetadataTableStructure()
|
|||
addColumn(QStringLiteral("contentChecksumTypeId"), QStringLiteral("INTEGER"));
|
||||
addColumn(QStringLiteral("e2eMangledName"), QStringLiteral("TEXT"));
|
||||
addColumn(QStringLiteral("isE2eEncrypted"), QStringLiteral("INTEGER"));
|
||||
addColumn(QStringLiteral("isShared"), QStringLiteral("INTEGER"));
|
||||
addColumn(QStringLiteral("lastShareStateFetchedTimestmap"), QStringLiteral("INTEGER"));
|
||||
|
||||
auto uploadInfoColumns = tableColumns("uploadinfo");
|
||||
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
|
||||
<< "modtime:" << record._modtime << "type:" << record._type
|
||||
<< "etag:" << record._etag << "fileId:" << record._fileId << "remotePerm:" << record._remotePerm.toString()
|
||||
<< "modtime:" << record._modtime << "type:" << record._type << "etag:" << record._etag
|
||||
<< "fileId:" << record._fileId << "remotePerm:" << record._remotePerm.toString()
|
||||
<< "fileSize:" << record._fileSize << "checksum:" << record._checksumHeader
|
||||
<< "e2eMangledName:" << record.e2eMangledName() << "isE2eEncrypted:" << record._isE2eEncrypted
|
||||
<< "lock:" << (record._lockstate._locked ? "true" : "false") << "lock owner type:" << record._lockstate._lockOwnerType
|
||||
<< "lock owner:" << record._lockstate._lockOwnerDisplayName << "lock owner id:" << record._lockstate._lockOwnerId
|
||||
<< "lock editor:" << record._lockstate._lockEditorApp;
|
||||
<< "lock:" << (record._lockstate._locked ? "true" : "false")
|
||||
<< "lock owner type:" << record._lockstate._lockOwnerType
|
||||
<< "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);
|
||||
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 "
|
||||
"(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, "
|
||||
"contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, lock, lockType, lockOwnerDisplayName, lockOwnerId, "
|
||||
"lockOwnerEditor, lockTime, lockTimeout) "
|
||||
"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);"),
|
||||
"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, ?26, ?27);"),
|
||||
_db);
|
||||
if (!query) {
|
||||
return query->error();
|
||||
|
@ -945,6 +953,8 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
|
|||
query->bindValue(23, record._lockstate._lockEditorApp);
|
||||
query->bindValue(24, record._lockstate._lockTime);
|
||||
query->bindValue(25, record._lockstate._lockTimeout);
|
||||
query->bindValue(26, record._isShared);
|
||||
query->bindValue(27, record._lastShareStateFetchedTimestmap);
|
||||
|
||||
if (!query->exec()) {
|
||||
return query->error();
|
||||
|
|
|
@ -81,6 +81,8 @@ public:
|
|||
QByteArray _e2eMangledName;
|
||||
bool _isE2eEncrypted = false;
|
||||
SyncJournalFileLockInfo _lockstate;
|
||||
bool _isShared = false;
|
||||
qint64 _lastShareStateFetchedTimestmap = 0;
|
||||
};
|
||||
|
||||
bool OCSYNC_EXPORT
|
||||
|
|
|
@ -353,11 +353,7 @@ if(Qt5Keychain_FOUND)
|
|||
endif()
|
||||
|
||||
# add executable icon on windows and osx
|
||||
|
||||
# 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)
|
||||
include(GenerateIconsUtils)
|
||||
|
||||
# For historical reasons we can not use the application_shortname
|
||||
# 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)
|
||||
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)
|
||||
|
||||
foreach(state_icons_color ${STATE_ICONS_COLORS})
|
||||
|
|
|
@ -40,7 +40,7 @@ void OcsJob::setVerb(const QByteArray &verb)
|
|||
|
||||
void OcsJob::addParam(const QString &name, const QString &value)
|
||||
{
|
||||
_params.append(qMakePair(name, value));
|
||||
_params.insert(name, value);
|
||||
}
|
||||
|
||||
void OcsJob::addPassStatusCode(int code)
|
||||
|
@ -58,16 +58,21 @@ void OcsJob::addRawHeader(const QByteArray &headerName, const QByteArray &value)
|
|||
_request.setRawHeader(headerName, value);
|
||||
}
|
||||
|
||||
QString OcsJob::getParamValue(const QString &key) const
|
||||
{
|
||||
return _params.value(key);
|
||||
}
|
||||
|
||||
static QUrlQuery percentEncodeQueryItems(
|
||||
const QList<QPair<QString, QString>> &items)
|
||||
const QHash<QString, QString> &items)
|
||||
{
|
||||
QUrlQuery result;
|
||||
// Note: QUrlQuery::setQueryItems() does not fully percent encode
|
||||
// the query items, see #5042
|
||||
foreach (const auto &item, items) {
|
||||
for (auto it = std::cbegin(items); it != std::cend(items); ++it) {
|
||||
result.addQueryItem(
|
||||
QUrl::toPercentEncoding(item.first),
|
||||
QUrl::toPercentEncoding(item.second));
|
||||
QUrl::toPercentEncoding(it.key()),
|
||||
QUrl::toPercentEncoding(it.value()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -85,13 +90,13 @@ void OcsJob::start()
|
|||
} else if (_verb == "POST" || _verb == "PUT") {
|
||||
// Url encode the _postParams and put them in a buffer.
|
||||
QByteArray postData;
|
||||
Q_FOREACH (auto tmp, _params) {
|
||||
for (auto it = std::cbegin(_params); it != std::cend(_params); ++it) {
|
||||
if (!postData.isEmpty()) {
|
||||
postData.append("&");
|
||||
}
|
||||
postData.append(QUrl::toPercentEncoding(tmp.first));
|
||||
postData.append(QUrl::toPercentEncoding(it.key()));
|
||||
postData.append("=");
|
||||
postData.append(QUrl::toPercentEncoding(tmp.second));
|
||||
postData.append(QUrl::toPercentEncoding(it.value()));
|
||||
}
|
||||
buffer->setData(postData);
|
||||
}
|
||||
|
|
|
@ -19,8 +19,7 @@
|
|||
#include "abstractnetworkjob.h"
|
||||
|
||||
#include <QVector>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QHash>
|
||||
#include <QUrl>
|
||||
|
||||
#define OCS_SUCCESS_STATUS_CODE 100
|
||||
|
@ -110,6 +109,8 @@ public:
|
|||
*/
|
||||
void addRawHeader(const QByteArray &headerName, const QByteArray &value);
|
||||
|
||||
[[nodiscard]] QString getParamValue(const QString &key) const;
|
||||
|
||||
|
||||
protected slots:
|
||||
|
||||
|
@ -149,7 +150,7 @@ private slots:
|
|||
|
||||
private:
|
||||
QByteArray _verb;
|
||||
QList<QPair<QString, QString>> _params;
|
||||
QHash<QString, QString> _params;
|
||||
QVector<int> _passStatusCodes;
|
||||
QNetworkRequest _request;
|
||||
};
|
||||
|
|
|
@ -24,16 +24,21 @@ namespace OCC {
|
|||
OcsShareJob::OcsShareJob(AccountPtr account)
|
||||
: OcsJob(account)
|
||||
{
|
||||
setPath("ocs/v2.php/apps/files_sharing/api/v1/shares");
|
||||
setPath(_pathForSharesRequest);
|
||||
connect(this, &OcsJob::jobFinished, this, &OcsShareJob::jobDone);
|
||||
}
|
||||
|
||||
void OcsShareJob::getShares(const QString &path)
|
||||
void OcsShareJob::getShares(const QString &path, const QMap<QString, QString> ¶ms)
|
||||
{
|
||||
setVerb("GET");
|
||||
|
||||
addParam(QString::fromLatin1("path"), path);
|
||||
addParam(QString::fromLatin1("reshares"), QString("true"));
|
||||
|
||||
for (auto it = std::cbegin(params); it != std::cend(params); ++it) {
|
||||
addParam(it.key(), it.value());
|
||||
}
|
||||
|
||||
addPassStatusCode(404);
|
||||
|
||||
start();
|
||||
|
@ -181,4 +186,6 @@ void OcsShareJob::jobDone(QJsonDocument reply)
|
|||
{
|
||||
emit shareJobFinished(reply, _value);
|
||||
}
|
||||
|
||||
QString const OcsShareJob::_pathForSharesRequest = QStringLiteral("ocs/v2.php/apps/files_sharing/api/v1/shares");
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ public:
|
|||
*
|
||||
* @param path Path to request shares for (default all shares)
|
||||
*/
|
||||
void getShares(const QString &path = "");
|
||||
void getShares(const QString &path = "", const QMap<QString, QString> ¶ms = {});
|
||||
|
||||
/**
|
||||
* Delete the current Share
|
||||
|
@ -131,6 +131,8 @@ public:
|
|||
*/
|
||||
void getSharedWithMe();
|
||||
|
||||
static const QString _pathForSharesRequest;
|
||||
|
||||
signals:
|
||||
/**
|
||||
* Result of the OCS request
|
||||
|
|
|
@ -16,29 +16,58 @@
|
|||
#include "account.h"
|
||||
#include "accountstate.h"
|
||||
#include "common/shellextensionutils.h"
|
||||
#include <libsync/vfs/cfapi/shellext/configvfscfapishellext.h>
|
||||
#include "folder.h"
|
||||
#include "folderman.h"
|
||||
#include "ocssharejob.h"
|
||||
#include <QDir>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#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 {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcShellExtServer, "nextcloud.gui.shellextensions.server", QtInfoMsg)
|
||||
|
||||
ShellExtensionsServer::ShellExtensionsServer(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
_isSharedInvalidationInterval = isSharedInvalidationInterval;
|
||||
_localServer.listen(VfsShellExtensions::serverNameForApplicationNameDefault());
|
||||
connect(&_localServer, &QLocalServer::newConnection, this, &ShellExtensionsServer::slotNewConnection);
|
||||
}
|
||||
|
||||
ShellExtensionsServer::~ShellExtensionsServer()
|
||||
{
|
||||
for (const auto &connection : _customStateSocketConnections) {
|
||||
if (connection) {
|
||||
QObject::disconnect(connection);
|
||||
}
|
||||
}
|
||||
_customStateSocketConnections.clear();
|
||||
|
||||
if (!_localServer.isListening()) {
|
||||
return;
|
||||
}
|
||||
_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)
|
||||
{
|
||||
socket->write(VfsShellExtensions::Protocol::createJsonMessage(message));
|
||||
|
@ -60,6 +89,96 @@ void ShellExtensionsServer::closeSession(QLocalSocket *socket)
|
|||
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)
|
||||
{
|
||||
if (!thumbnailRequestInfo.isValid()) {
|
||||
|
@ -87,7 +206,7 @@ void ShellExtensionsServer::processThumbnailRequest(QLocalSocket *socket, const
|
|||
queryItems.addQueryItem(QStringLiteral("fileId"), record._fileId);
|
||||
queryItems.addQueryItem(QStringLiteral("x"), QString::number(thumbnailRequestInfo.size.width()));
|
||||
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());
|
||||
job->startRequest(QByteArrayLiteral("GET"), jobUrl);
|
||||
connect(job, &SimpleNetworkJob::finishedSignal, this, [socket, this](QNetworkReply *reply) {
|
||||
|
@ -121,8 +240,155 @@ void ShellExtensionsServer::slotNewConnection()
|
|||
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 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();
|
||||
|
||||
if (thumbnailFilePath.isEmpty() || thumbnailFileSize.isEmpty()) {
|
||||
|
|
|
@ -16,8 +16,11 @@
|
|||
|
||||
#include <QObject>
|
||||
#include <QLocalServer>
|
||||
#include <QMutex>
|
||||
#include <QSize>
|
||||
#include <QVariant>
|
||||
|
||||
class QJsonDocument;
|
||||
class QLocalSocket;
|
||||
|
||||
namespace OCC {
|
||||
|
@ -32,21 +35,45 @@ class ShellExtensionsServer : public QObject
|
|||
[[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
|
||||
public:
|
||||
ShellExtensionsServer(QObject *parent = nullptr);
|
||||
~ShellExtensionsServer() override;
|
||||
|
||||
static QString getFetchThumbnailPath();
|
||||
|
||||
void setIsSharedInvalidationInterval(qint64 interval);
|
||||
|
||||
signals:
|
||||
void fetchSharesJobFinished(const QString &folderAlias);
|
||||
|
||||
private:
|
||||
void sendJsonMessageWithVersion(QLocalSocket *socket, const QVariantMap &message);
|
||||
void sendEmptyDataAndCloseSession(QLocalSocket *socket);
|
||||
void closeSession(QLocalSocket *socket);
|
||||
void processCustomStateRequest(QLocalSocket *socket, const CustomStateRequestInfo &customStateRequestInfo);
|
||||
void processThumbnailRequest(QLocalSocket *socket, const ThumbnailRequestInfo &thumbnailRequestInfo);
|
||||
|
||||
void parseCustomStateRequest(QLocalSocket *socket, const QVariantMap &message);
|
||||
void parseThumbnailRequest(QLocalSocket *socket, const QVariantMap &message);
|
||||
|
||||
private slots:
|
||||
void slotNewConnection();
|
||||
void slotSharesFetched(const QJsonDocument &reply);
|
||||
void slotSharesFetchError(int statusCode, const QString &message);
|
||||
|
||||
private:
|
||||
QLocalServer _localServer;
|
||||
QStringList _runningFetchShareJobsForPaths;
|
||||
QMap<qintptr, QMetaObject::Connection> _customStateSocketConnections;
|
||||
qint64 _isSharedInvalidationInterval = 0;
|
||||
};
|
||||
} // namespace OCC
|
||||
|
|
|
@ -982,6 +982,9 @@ void SocketApi::setFileLock(const QString &localFile, const SyncFileItem::LockSt
|
|||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -393,6 +393,8 @@ void BulkPropagatorJob::slotPutFinishedOneFile(const BulkUploadItem &singleFile,
|
|||
singleFile._item->_etag = etag;
|
||||
singleFile._item->_fileId = getHeaderFromJsonReply(fileReply, "fileid");
|
||||
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") {
|
||||
// X-OC-MTime is supported since owncloud 5.0. But not when chunking.
|
||||
|
|
|
@ -475,6 +475,8 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
|
|||
item->_checksumHeader = serverEntry.checksumHeader;
|
||||
item->_fileId = serverEntry.fileId;
|
||||
item->_remotePerm = serverEntry.remotePerm;
|
||||
item->_isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared);
|
||||
item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
|
||||
item->_type = serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile;
|
||||
item->_etag = serverEntry.etag;
|
||||
item->_directDownloadUrl = serverEntry.directDownloadUrl;
|
||||
|
@ -633,6 +635,8 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
|
|||
item->_direction = SyncFileItem::Up;
|
||||
item->_fileId = serverEntry.fileId;
|
||||
item->_remotePerm = serverEntry.remotePerm;
|
||||
item->_isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared);
|
||||
item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
|
||||
item->_etag = serverEntry.etag;
|
||||
item->_type = serverEntry.isDirectory ? CSyncEnums::ItemTypeDirectory : CSyncEnums::ItemTypeFile;
|
||||
|
||||
|
@ -919,6 +923,8 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
|
|||
item->_remotePerm = base.isValid() ? base._remotePerm : RemotePermissions{};
|
||||
item->_etag = base.isValid() ? base._etag : QByteArray{};
|
||||
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()) {
|
||||
|
@ -1326,6 +1332,8 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
|
|||
item->_direction = SyncFileItem::Up;
|
||||
item->_fileId = base._fileId;
|
||||
item->_remotePerm = base._remotePerm;
|
||||
item->_isShared = base._isShared;
|
||||
item->_lastShareStateFetchedTimestmap = base._lastShareStateFetchedTimestmap;
|
||||
item->_etag = base._etag;
|
||||
item->_type = base._type;
|
||||
|
||||
|
@ -1451,6 +1459,8 @@ void ProcessDirectoryJob::processFileConflict(const SyncFileItemPtr &item, Proce
|
|||
rec._type = item->_type;
|
||||
rec._fileSize = serverEntry.size;
|
||||
rec._remotePerm = serverEntry.remotePerm;
|
||||
rec._isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared);
|
||||
rec._lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
|
||||
rec._checksumHeader = serverEntry.checksumHeader;
|
||||
const auto result = _discoveryData->_statedb->setFileRecord(rec);
|
||||
if (!result) {
|
||||
|
|
|
@ -144,6 +144,8 @@ void PropagateRemoteMkdir::finalizeMkColJob(QNetworkReply::NetworkError err, con
|
|||
connect(propfindJob, &PropfindJob::result, this, [this, jobPath](const QVariantMap &result){
|
||||
propagator()->_activeJobList.removeOne(this);
|
||||
_item->_remotePerm = RemotePermissions::fromServerString(result.value(QStringLiteral("permissions")).toString());
|
||||
_item->_isShared = _item->_remotePerm.hasPermission(RemotePermissions::IsShared);
|
||||
_item->_lastShareStateFetchedTimestmap = QDateTime::currentMSecsSinceEpoch();
|
||||
|
||||
if (!_uploadEncryptedHelper && !_item->_isEncrypted) {
|
||||
success();
|
||||
|
|
|
@ -41,6 +41,8 @@ SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QStri
|
|||
rec._fileId = _fileId;
|
||||
rec._fileSize = _size;
|
||||
rec._remotePerm = _remotePerm;
|
||||
rec._isShared = _isShared;
|
||||
rec._lastShareStateFetchedTimestmap = _lastShareStateFetchedTimestmap;
|
||||
rec._serverHasIgnoredFiles = _serverHasIgnoredFiles;
|
||||
rec._checksumHeader = _checksumHeader;
|
||||
rec._e2eMangledName = _encryptedFileName.toUtf8();
|
||||
|
@ -89,6 +91,8 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec
|
|||
item->_lockEditorApp = rec._lockstate._lockEditorApp;
|
||||
item->_lockTime = rec._lockstate._lockTime;
|
||||
item->_lockTimeout = rec._lockstate._lockTimeout;
|
||||
item->_isShared = rec._isShared;
|
||||
item->_lastShareStateFetchedTimestmap = rec._lastShareStateFetchedTimestmap;
|
||||
return item;
|
||||
}
|
||||
|
||||
|
|
|
@ -308,6 +308,9 @@ public:
|
|||
QString _lockEditorApp;
|
||||
qint64 _lockTime = 0;
|
||||
qint64 _lockTimeout = 0;
|
||||
|
||||
bool _isShared = false;
|
||||
time_t _lastShareStateFetchedTimestmap = 0;
|
||||
};
|
||||
|
||||
inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2)
|
||||
|
|
|
@ -442,7 +442,8 @@ bool createSyncRootRegistryKeys(const QString &providerName, const QString &fold
|
|||
{ providerSyncRootIdRegistryKey, QStringLiteral("Flags"), REG_DWORD, flags },
|
||||
{ providerSyncRootIdRegistryKey, QStringLiteral("DisplayNameResource"), REG_EXPAND_SZ, displayName },
|
||||
{ providerSyncRootIdRegistryKey, QStringLiteral("IconResource"), REG_EXPAND_SZ, QString(QDir::toNativeSeparators(qApp->applicationFilePath()) + QStringLiteral(",0")) },
|
||||
{ providerSyncRootIdUserSyncRootsRegistryKey, windowsSid, REG_SZ, syncRootPath},
|
||||
{ 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("NamespaceCLSID"), REG_SZ, QString(navigationPaneClsid)}
|
||||
};
|
||||
|
@ -550,6 +551,7 @@ void unregisterSyncRootShellExtensions(const QString &providerName, const QStrin
|
|||
const QString providerSyncRootIdRegistryKey = syncRootManagerRegKey + QStringLiteral("\\") + syncRootId;
|
||||
|
||||
OCC::Utility::registryDeleteKeyValue(HKEY_LOCAL_MACHINE, providerSyncRootIdRegistryKey, QStringLiteral("ThumbnailProvider"));
|
||||
OCC::Utility::registryDeleteKeyValue(HKEY_LOCAL_MACHINE, providerSyncRootIdRegistryKey, QStringLiteral("CustomStateHandler"));
|
||||
|
||||
qCInfo(lcCfApiWrapper) << "Successfully unregistered SyncRoot Shell Extensions!";
|
||||
}
|
||||
|
|
|
@ -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
|
||||
dllmain.cpp
|
||||
cfapishellintegrationclassfactory.cpp
|
||||
customstateprovideripc.cpp
|
||||
ipccommon.cpp
|
||||
thumbnailprovider.cpp
|
||||
thumbnailprovideripc.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/common/shellextensionutils.cpp
|
||||
customstateprovider.cpp
|
||||
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 ${CMAKE_SOURCE_DIR})
|
||||
|
||||
target_compile_features(CfApiShellExtensions PRIVATE cxx_std_17)
|
||||
|
||||
set_target_properties(CfApiShellExtensions
|
||||
PROPERTIES
|
||||
LIBRARY_OUTPUT_NAME
|
||||
|
@ -29,3 +208,5 @@ install(TARGETS CfApiShellExtensions
|
|||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configvfscfapishellext.h.in ${CMAKE_CURRENT_BINARY_DIR}/configvfscfapishellext.h)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
{
|
||||
};
|
||||
}
|
|
@ -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 = {};
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -13,16 +13,20 @@
|
|||
*/
|
||||
|
||||
#include "cfapishellintegrationclassfactory.h"
|
||||
#include "customstateprovider.h"
|
||||
#include "thumbnailprovider.h"
|
||||
#include <comdef.h>
|
||||
|
||||
long dllReferenceCount = 0;
|
||||
long dllObjectsCount = 0;
|
||||
|
||||
HINSTANCE instanceHandle = NULL;
|
||||
|
||||
HRESULT CustomStateProvider_CreateInstance(REFIID riid, void **ppv);
|
||||
HRESULT ThumbnailProvider_CreateInstance(REFIID riid, void **ppv);
|
||||
|
||||
const VfsShellExtensions::ClassObjectInit listClassesSupported[] = {
|
||||
{&__uuidof(winrt::CfApiShellExtensions::implementation::CustomStateProvider), CustomStateProvider_CreateInstance},
|
||||
{&__uuidof(VfsShellExtensions::ThumbnailProvider), ThumbnailProvider_CreateInstance}
|
||||
};
|
||||
|
||||
|
@ -30,6 +34,9 @@ STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void *)
|
|||
{
|
||||
if (dwReason == DLL_PROCESS_ATTACH) {
|
||||
instanceHandle = hInstance;
|
||||
wchar_t dllFilePath[_MAX_PATH] = {0};
|
||||
::GetModuleFileName(instanceHandle, dllFilePath, _MAX_PATH);
|
||||
winrt::CfApiShellExtensions::implementation::CustomStateProvider::setDllFilePath(dllFilePath);
|
||||
DisableThreadLibraryCalls(hInstance);
|
||||
}
|
||||
|
||||
|
@ -38,7 +45,7 @@ STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void *)
|
|||
|
||||
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)
|
||||
|
@ -46,6 +53,16 @@ STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv)
|
|||
return VfsShellExtensions::CfApiShellIntegrationClassFactory::CreateInstance(clsid, listClassesSupported, ARRAYSIZE(listClassesSupported), riid, ppv);
|
||||
}
|
||||
|
||||
HRESULT CustomStateProvider_CreateInstance(REFIID riid, void **ppv)
|
||||
{
|
||||
try {
|
||||
const auto customStateProvider = winrt::make_self<winrt::CfApiShellExtensions::implementation::CustomStateProvider>();
|
||||
return customStateProvider->QueryInterface(riid, ppv);
|
||||
} catch (_com_error exc) {
|
||||
return exc.Error();
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT ThumbnailProvider_CreateInstance(REFIID riid, void **ppv)
|
||||
{
|
||||
auto *thumbnailProvider = new (std::nothrow) VfsShellExtensions::ThumbnailProvider();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -48,6 +48,8 @@
|
|||
#include <shlwapi.h>
|
||||
#include <QSize>
|
||||
|
||||
extern long dllObjectsCount;
|
||||
|
||||
namespace VfsShellExtensions {
|
||||
|
||||
std::pair<HBITMAP, WTS_ALPHATYPE> hBitmapAndAlphaTypeFromData(const QByteArray &thumbnailData)
|
||||
|
@ -93,8 +95,13 @@ std::pair<HBITMAP, WTS_ALPHATYPE> hBitmapAndAlphaTypeFromData(const QByteArray &
|
|||
ThumbnailProvider::ThumbnailProvider()
|
||||
: _referenceCount(1)
|
||||
{
|
||||
InterlockedIncrement(&dllObjectsCount);
|
||||
}
|
||||
|
||||
ThumbnailProvider::~ThumbnailProvider()
|
||||
{
|
||||
InterlockedDecrement(&dllObjectsCount);
|
||||
}
|
||||
IFACEMETHODIMP ThumbnailProvider::QueryInterface(REFIID riid, void **ppv)
|
||||
{
|
||||
static const QITAB qit[] = {
|
||||
|
|
|
@ -30,7 +30,7 @@ class __declspec(uuid(CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID)) ThumbnailProvi
|
|||
public:
|
||||
ThumbnailProvider();
|
||||
|
||||
virtual ~ThumbnailProvider() = default;
|
||||
virtual ~ThumbnailProvider();
|
||||
|
||||
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv);
|
||||
|
||||
|
|
|
@ -14,14 +14,11 @@
|
|||
|
||||
#include "thumbnailprovideripc.h"
|
||||
#include "common/shellextensionutils.h"
|
||||
#include "common/utility.h"
|
||||
#include "ipccommon.h"
|
||||
#include <QString>
|
||||
#include <QSize>
|
||||
#include <QtNetwork/QLocalSocket>
|
||||
#include <QJsonDocument>
|
||||
#include <QObject>
|
||||
#include <QDir>
|
||||
#include <Windows.h>
|
||||
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;
|
||||
|
@ -61,7 +58,7 @@ QByteArray ThumbnailProviderIpc::fetchThumbnailForFile(const QString &filePath,
|
|||
{
|
||||
VfsShellExtensions::Protocol::ThumbnailProviderRequestKey,
|
||||
QVariantMap {
|
||||
{VfsShellExtensions::Protocol::ThumbnailProviderRequestFilePathKey, filePath},
|
||||
{VfsShellExtensions::Protocol::FilePathKey, filePath},
|
||||
{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()) {
|
||||
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)) {
|
||||
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;
|
||||
return findServerNameForPath(filePath);
|
||||
}
|
||||
|
||||
bool ThumbnailProviderIpc::connectSocketToServer(const QString &serverName)
|
||||
|
|
|
@ -40,12 +40,16 @@ const auto rootKey = HKEY_CURRENT_USER;
|
|||
|
||||
bool registerShellExtension()
|
||||
{
|
||||
const QList<QPair<QString, QString>> listExtensions = {
|
||||
{CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME, CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG},
|
||||
{CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_DISPLAY_NAME, CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG}
|
||||
};
|
||||
// assume CFAPI_SHELL_EXTENSIONS_LIB_NAME is always in the same folder as the main executable
|
||||
// assume CFAPI_SHELL_EXTENSIONS_LIB_NAME is always in the same folder as the main executable
|
||||
const auto shellExtensionDllPath = QDir::toNativeSeparators(QString(QCoreApplication::applicationDirPath() + QStringLiteral("/") + CFAPI_SHELL_EXTENSIONS_LIB_NAME + QStringLiteral(".dll")));
|
||||
if (!QFileInfo::exists(shellExtensionDllPath)) {
|
||||
Q_ASSERT(false);
|
||||
qCWarning(lcCfApi) << "Register CfAPI shell extensions failed. Dll does not exist in "
|
||||
<< QCoreApplication::applicationDirPath();
|
||||
qCWarning(lcCfApi) << "Register CfAPI shell extensions failed. Dll does not exist in " << QCoreApplication::applicationDirPath();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -57,20 +61,22 @@ bool registerShellExtension()
|
|||
return false;
|
||||
}
|
||||
|
||||
const QString clsidPath = QString() % clsIdRegKey % CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG;
|
||||
const QString clsidServerPath = clsidPath % R"(\InprocServer32)";
|
||||
for (const auto extension : listExtensions) {
|
||||
const QString clsidPath = QString() % clsIdRegKey % extension.second;
|
||||
const QString clsidServerPath = clsidPath % R"(\InprocServer32)";
|
||||
|
||||
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, QStringLiteral("AppID"), REG_SZ, CFAPI_SHELLEXT_APPID_REG)) {
|
||||
return false;
|
||||
}
|
||||
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, {}, REG_SZ, CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME)) {
|
||||
return false;
|
||||
}
|
||||
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, {}, REG_SZ, shellExtensionDllPath)) {
|
||||
return false;
|
||||
}
|
||||
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, QStringLiteral("ThreadingModel"), REG_SZ, QStringLiteral("Apartment"))) {
|
||||
return false;
|
||||
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, QStringLiteral("AppID"), REG_SZ, CFAPI_SHELLEXT_APPID_REG)) {
|
||||
return false;
|
||||
}
|
||||
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidPath, {}, REG_SZ, extension.first)) {
|
||||
return false;
|
||||
}
|
||||
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, {}, REG_SZ, shellExtensionDllPath)) {
|
||||
return false;
|
||||
}
|
||||
if (!OCC::Utility::registrySetKeyValue(rootKey, clsidServerPath, QStringLiteral("ThreadingModel"), REG_SZ, QStringLiteral("Apartment"))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -83,9 +89,16 @@ void unregisterShellExtensions()
|
|||
OCC::Utility::registryDeleteKeyTree(rootKey, appIdPath);
|
||||
}
|
||||
|
||||
const QString clsidPath = QString() % clsIdRegKey % CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG;
|
||||
if (OCC::Utility::registryKeyExists(rootKey, clsidPath)) {
|
||||
OCC::Utility::registryDeleteKeyTree(rootKey, clsidPath);
|
||||
const QStringList listExtensions = {
|
||||
CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG,
|
||||
CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG
|
||||
};
|
||||
|
||||
for (const auto extension : listExtensions) {
|
||||
const QString clsidPath = QString() % clsIdRegKey % extension;
|
||||
if (OCC::Utility::registryKeyExists(rootKey, clsidPath)) {
|
||||
OCC::Utility::registryDeleteKeyTree(rootKey, clsidPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ if (WIN32)
|
|||
|
||||
nextcloud_add_test(SyncCfApi)
|
||||
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)
|
||||
nextcloud_add_test(SyncXAttr)
|
||||
endif()
|
||||
|
|
|
@ -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 <QImage>
|
||||
#include <QPainter>
|
||||
#include "syncenginetestutils.h"
|
||||
#include "common/vfs.h"
|
||||
#include "common/shellextensionutils.h"
|
||||
#include "config.h"
|
||||
#include <syncengine.h>
|
||||
|
||||
#include "folderman.h"
|
||||
#include "account.h"
|
||||
#include "accountstate.h"
|
||||
#include "accountmanager.h"
|
||||
#include "testhelper.h"
|
||||
#include "vfs/cfapi/shellext/thumbnailprovideripc.h"
|
||||
#include "shellextensionsserver.h"
|
||||
namespace {
|
||||
static constexpr auto roootFolderName = "A";
|
||||
static constexpr auto imagesFolderName = "photos";
|
||||
static constexpr auto filesFolderName = "files";
|
||||
|
||||
static const QByteArray fakeNoSharesResponse = R"({"ocs":{"data":[],"meta":{"message":"OK","status":"ok","statuscode":200}}})";
|
||||
|
||||
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;
|
||||
|
||||
|
@ -38,21 +137,39 @@ class TestCfApiShellExtensionsIPC : public QObject
|
|||
|
||||
QScopedPointer<ShellExtensionsServer> _shellExtensionsServer;
|
||||
|
||||
QStringList dummmyImageNames = {
|
||||
"A/photos/imageJpg.jpg",
|
||||
"A/photos/imagePng.png",
|
||||
"A/photos/imagePng.bmp",
|
||||
const QStringList dummmyImageNames = {
|
||||
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imageJpg.jpg")) },
|
||||
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.png")) },
|
||||
{ QString(QString(roootFolderName) + QLatin1Char('/') + QString(imagesFolderName) + QLatin1Char('/') + QStringLiteral("imagePng.bmp")) }
|
||||
};
|
||||
QMap<QString, QByteArray> dummyImages;
|
||||
|
||||
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:
|
||||
void initTestCase()
|
||||
{
|
||||
VfsShellExtensions::ThumbnailProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
|
||||
VfsShellExtensions::CustomStateProviderIpc::overrideServerName = VfsShellExtensions::serverNameForApplicationNameDefault();
|
||||
|
||||
_shellExtensionsServer.reset(new ShellExtensionsServer);
|
||||
_shellExtensionsServer->setIsSharedInvalidationInterval(shellExtensionServerOverrideIntervalMs);
|
||||
|
||||
for (const auto &dummyImageName : dummmyImageNames) {
|
||||
const auto extension = dummyImageName.split(".").last();
|
||||
|
@ -68,6 +185,16 @@ private slots:
|
|||
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({}));
|
||||
account = OCC::Account::create();
|
||||
account->setCredentials(new FakeCredentials{fakeQnam.data()});
|
||||
|
@ -86,31 +213,43 @@ private slots:
|
|||
Q_UNUSED(device);
|
||||
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();
|
||||
|
||||
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);
|
||||
|
||||
if (path.endsWith(OCC::OcsShareJob::_pathForSharesRequest)) {
|
||||
const auto jsonReply = TestCfApiShellExtensionsIPC::replyWithNoShares ? fakeNoSharesResponse : fakeSharedFilesResponse;
|
||||
TestCfApiShellExtensionsIPC::replyWithNoShares = false;
|
||||
auto fakePayloadReply = new FakePayloadReply(op, req, jsonReply, nullptr);
|
||||
QMap<QNetworkRequest::KnownHeaders, QByteArray> additionalHeaders = {
|
||||
{QNetworkRequest::KnownHeaders::ContentTypeHeader, "image/jpeg"}};
|
||||
{QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"}};
|
||||
fakePayloadReply->_additionalHeaders = additionalHeaders;
|
||||
|
||||
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;
|
||||
|
@ -126,6 +265,7 @@ private slots:
|
|||
|
||||
folder->setVirtualFilesEnabled(true);
|
||||
|
||||
QVERIFY(fakeFolder.syncOnce());
|
||||
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
||||
ItemCompletedSpy completeSpy(fakeFolder);
|
||||
|
||||
|
@ -135,8 +275,6 @@ private slots:
|
|||
cleanup();
|
||||
|
||||
// Create a virtual file for remote files
|
||||
fakeFolder.remoteModifier().mkdir("A");
|
||||
fakeFolder.remoteModifier().mkdir("A/photos");
|
||||
for (const auto &dummyImageName : dummmyImageNames) {
|
||||
fakeFolder.remoteModifier().insert(dummyImageName, 256);
|
||||
}
|
||||
|
@ -198,6 +336,137 @@ private slots:
|
|||
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()
|
||||
{
|
||||
VfsShellExtensions::ThumbnailProviderIpc::overrideServerName.clear();
|
||||
|
@ -212,5 +481,7 @@ private slots:
|
|||
}
|
||||
};
|
||||
|
||||
bool TestCfApiShellExtensionsIPC::replyWithNoShares = false;
|
||||
|
||||
QTEST_GUILESS_MAIN(TestCfApiShellExtensionsIPC)
|
||||
#include "testcfapishellextensionsipc.moc"
|
||||
|
|
|
@ -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>
|
После Ширина: | Высота: | Размер: 268 B |
|
@ -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>
|
После Ширина: | Высота: | Размер: 290 B |
После Ширина: | Высота: | Размер: 12 KiB |
После Ширина: | Высота: | Размер: 20 KiB |
После Ширина: | Высота: | Размер: 1.0 KiB |
После Ширина: | Высота: | Размер: 1.4 KiB |
После Ширина: | Высота: | Размер: 325 B |
После Ширина: | Высота: | Размер: 405 B |
После Ширина: | Высота: | Размер: 2.2 KiB |
После Ширина: | Высота: | Размер: 3.6 KiB |
После Ширина: | Высота: | Размер: 314 B |
После Ширина: | Высота: | Размер: 379 B |
После Ширина: | Высота: | Размер: 439 B |
После Ширина: | Высота: | Размер: 566 B |
После Ширина: | Высота: | Размер: 459 B |
После Ширина: | Высота: | Размер: 585 B |
После Ширина: | Высота: | Размер: 4.9 KiB |
После Ширина: | Высота: | Размер: 8.9 KiB |
После Ширина: | Высота: | Размер: 580 B |
После Ширина: | Высота: | Размер: 656 B |