mirror of
https://github.com/chylex/Nextcloud-Desktop.git
synced 2026-04-03 18:11:32 +02:00
Compare commits
191 Commits
v3.4.0-rc1
...
v3.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0791e51ce | ||
|
|
11a36158ad | ||
|
|
d74024baf4 | ||
|
|
f95ff45c16 | ||
|
|
d6d8132b36 | ||
|
|
b426ca3579 | ||
|
|
4aabf339ea | ||
|
|
f3c5081c38 | ||
|
|
63863d15ad | ||
|
|
e5690d77f6 | ||
|
|
cbc3cfb8ab | ||
|
|
eae2e0075f | ||
|
|
c875ebecda | ||
|
|
ac114950ed | ||
|
|
869b3f5f81 | ||
|
|
60fc03acb5 | ||
|
|
34c782c81d | ||
|
|
a668cb123f | ||
|
|
4b2dbd54d2 | ||
|
|
7947b31d64 | ||
|
|
318bc32a06 | ||
|
|
985eba8fa3 | ||
|
|
ebf41b181b | ||
|
|
eefdb04277 | ||
|
|
ac4435aa86 | ||
|
|
1d85963551 | ||
|
|
269be3cb4f | ||
|
|
866a1da899 | ||
|
|
9f075be5c4 | ||
|
|
c1e5a0a890 | ||
|
|
1035bca4ae | ||
|
|
195218e0a3 | ||
|
|
bc1bd64ece | ||
|
|
586afaacb6 | ||
|
|
b4690f5721 | ||
|
|
5ceb98c266 | ||
|
|
0c92cfc644 | ||
|
|
ba4f53679a | ||
|
|
820f22ba0d | ||
|
|
80df113fd2 | ||
|
|
4abeda01b7 | ||
|
|
42f6606d52 | ||
|
|
b17d03c0ab | ||
|
|
4ace64f1d7 | ||
|
|
5e26d76d2b | ||
|
|
689ecbcb5b | ||
|
|
de6aa7be02 | ||
|
|
d0957fc071 | ||
|
|
fe6d126443 | ||
|
|
645688ae20 | ||
|
|
d3d2500807 | ||
|
|
a4493606e0 | ||
|
|
6d2ef0b410 | ||
|
|
05c731c4c1 | ||
|
|
1915ab7989 | ||
|
|
54d0351b42 | ||
|
|
282e47d266 | ||
|
|
07c81c02da | ||
|
|
2ef6a20edc | ||
|
|
79ffdac989 | ||
|
|
fa32c10014 | ||
|
|
fe67d66d3d | ||
|
|
3a3a6dd6b8 | ||
|
|
328c673c29 | ||
|
|
49afad0474 | ||
|
|
7c3e91202e | ||
|
|
e94b18f97f | ||
|
|
79a0b937f5 | ||
|
|
e5fbc8c2dd | ||
|
|
3632cc659b | ||
|
|
1731bf7c86 | ||
|
|
936d37fd0b | ||
|
|
7259a0bc0d | ||
|
|
06de878b4b | ||
|
|
179ff27ab6 | ||
|
|
44da2f2ce2 | ||
|
|
92b302fb37 | ||
|
|
d2febdf17c | ||
|
|
9c4b4c6183 | ||
|
|
45029e9012 | ||
|
|
969b0e8e2e | ||
|
|
4b46da9370 | ||
|
|
8b5dd53519 | ||
|
|
5ee5b19406 | ||
|
|
8e1c62cc70 | ||
|
|
151e9300cd | ||
|
|
efc3116f30 | ||
|
|
4296a6041a | ||
|
|
0d1e0057b3 | ||
|
|
5aadc7a62d | ||
|
|
3c28e38089 | ||
|
|
dba8fd7c76 | ||
|
|
39c2bb555a | ||
|
|
39fc86cbcf | ||
|
|
8d574c11e8 | ||
|
|
c02d87f283 | ||
|
|
ddb5375c68 | ||
|
|
bd78604468 | ||
|
|
4920b4d4af | ||
|
|
d12d00562f | ||
|
|
b17bbb2b22 | ||
|
|
fc64edba11 | ||
|
|
b6c7581414 | ||
|
|
962850f307 | ||
|
|
88ab5557bd | ||
|
|
e3fb3bbe73 | ||
|
|
a86a1b4c17 | ||
|
|
4326a70ede | ||
|
|
2880bd62ce | ||
|
|
b49633a9f7 | ||
|
|
adfe7ad953 | ||
|
|
e45a01bc03 | ||
|
|
9bcbc15834 | ||
|
|
b4a19bb6d3 | ||
|
|
52db45c2b1 | ||
|
|
b3d8cacf8c | ||
|
|
c004db2070 | ||
|
|
48ada55e77 | ||
|
|
fd60e60541 | ||
|
|
fb833ed311 | ||
|
|
1440c53ed6 | ||
|
|
bd42c35e80 | ||
|
|
a5c82670c9 | ||
|
|
3c966a77df | ||
|
|
1a9aade28e | ||
|
|
34c4c28879 | ||
|
|
a272b34809 | ||
|
|
05b8d1e40d | ||
|
|
18ef471332 | ||
|
|
e14502606c | ||
|
|
59953d857b | ||
|
|
436eced9fb | ||
|
|
f56985938d | ||
|
|
56f4198b28 | ||
|
|
6b22081f61 | ||
|
|
a5fa53c460 | ||
|
|
426e0af8cd | ||
|
|
e2f1854b1e | ||
|
|
c194605c35 | ||
|
|
112be18635 | ||
|
|
802c7ac906 | ||
|
|
f575cc1860 | ||
|
|
b03bf1c1f0 | ||
|
|
9bebda057a | ||
|
|
83a8058b51 | ||
|
|
072e9d44bd | ||
|
|
a3013de6ea | ||
|
|
9eed62a854 | ||
|
|
79282a8df9 | ||
|
|
ec64246dc7 | ||
|
|
c89d2abf5a | ||
|
|
b3914f627d | ||
|
|
998236dcc5 | ||
|
|
d9626bf311 | ||
|
|
684d70985e | ||
|
|
e92842d837 | ||
|
|
12c6d6e3bd | ||
|
|
1d704d9352 | ||
|
|
38ac585e7c | ||
|
|
892d289f38 | ||
|
|
5294c5135c | ||
|
|
8e6896ba03 | ||
|
|
b2e86c2ea3 | ||
|
|
8cc58dd8b0 | ||
|
|
ca1620ef42 | ||
|
|
f1d834df8e | ||
|
|
502ffc62ef | ||
|
|
73db636361 | ||
|
|
b222785dc2 | ||
|
|
5454004ef9 | ||
|
|
ed9671c2a6 | ||
|
|
911e35bc50 | ||
|
|
898949d1bc | ||
|
|
ef8fe58245 | ||
|
|
9e792369b2 | ||
|
|
c76a77e431 | ||
|
|
2308c9da49 | ||
|
|
c59f88ca82 | ||
|
|
3edfcff1a0 | ||
|
|
d84673376d | ||
|
|
69def04ec2 | ||
|
|
3e1a46f2de | ||
|
|
113ba716e6 | ||
|
|
703037cbfb | ||
|
|
07a8e8c91d | ||
|
|
df745ef39c | ||
|
|
2665c8fc16 | ||
|
|
cb34fec596 | ||
|
|
d8560dcb19 | ||
|
|
0e5f1d9a30 | ||
|
|
ad814f175e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -184,6 +184,7 @@ compile_commands.json
|
||||
convert.exe
|
||||
.dir-locals.el
|
||||
*-icon.png
|
||||
*-icon-win-folder.png
|
||||
*-sidebar.png
|
||||
*-w10startmenu.png
|
||||
theme.qrc
|
||||
|
||||
@@ -22,5 +22,6 @@ Icon=@APPLICATION_EXECUTABLE@
|
||||
|
||||
# Translations
|
||||
Icon[cy_GB]=@APPLICATION_ICON_NAME@
|
||||
Name[cy_GB]=@APPLICATION_NAME@ cleient cydweddu bwrdd gwaith
|
||||
Comment[cy_GB]=@APPLICATION_NAME@ cleient cydweddu bwrdd gwaith
|
||||
GenericName[cy_GB]=Cydweddu Ffolder
|
||||
|
||||
24
.tx/nextcloud.client-desktop/id_translation
Normal file
24
.tx/nextcloud.client-desktop/id_translation
Normal file
@@ -0,0 +1,24 @@
|
||||
[Desktop Entry]
|
||||
Categories=Utility;X-SuSE-SyncUtility;
|
||||
Type=Application
|
||||
Exec=@APPLICATION_EXECUTABLE@
|
||||
Name=@APPLICATION_NAME@ Desktop
|
||||
Comment=@APPLICATION_NAME@ desktop synchronization client
|
||||
GenericName=Folder Sync
|
||||
Icon=@APPLICATION_ICON_NAME@
|
||||
Keywords=@APPLICATION_NAME@;syncing;file;sharing;
|
||||
X-GNOME-Autostart-Delay=3
|
||||
MimeType=application/vnd.@APPLICATION_EXECUTABLE@;
|
||||
Actions=Quit;
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
[Desktop Action Quit]
|
||||
Exec=@APPLICATION_EXECUTABLE@ --quit
|
||||
Name=Quit @APPLICATION_NAME@
|
||||
Icon=@APPLICATION_EXECUTABLE@
|
||||
|
||||
|
||||
# Translations
|
||||
GenericName[id]=Sinkronisasi Folder
|
||||
@@ -22,5 +22,6 @@ Icon=@APPLICATION_EXECUTABLE@
|
||||
|
||||
# Translations
|
||||
Icon[ko]=@APPLICATION_ICON_NAME@
|
||||
Name[ko]=@APPLICATION_NAME@ 데스크탑
|
||||
Comment[ko]=@APPLICATION_NAME@ 데스크톱 동기화 클라이언트
|
||||
GenericName[ko]=폴더 동기화
|
||||
|
||||
@@ -22,5 +22,6 @@ Icon=@APPLICATION_EXECUTABLE@
|
||||
|
||||
# Translations
|
||||
Icon[nb_NO]=@APPLICATION_ICON_NAME@
|
||||
Name[nb_NO]=@APPLICATION_NAME@ skrivebord
|
||||
Comment[nb_NO]=@APPLICATION_NAME@ klient for synkroinisering
|
||||
GenericName[nb_NO]=Mappe synkroinisering
|
||||
|
||||
@@ -22,5 +22,6 @@ Icon=@APPLICATION_EXECUTABLE@
|
||||
|
||||
# Translations
|
||||
Icon[oc]=@APPLICATION_ICON_NAME@
|
||||
Name[oc]=@APPLICATION_NAME@ Burèu
|
||||
Comment[oc]=@APPLICATION_NAME@ client de sincronizacion
|
||||
GenericName[oc]=Sincro. dossièr
|
||||
|
||||
@@ -22,5 +22,6 @@ Icon=@APPLICATION_EXECUTABLE@
|
||||
|
||||
# Translations
|
||||
Icon[sv]=@APPLICATION_ICON_NAME@
|
||||
Name[sv]=@APPLICATION_NAME@ Skrivbord
|
||||
Comment[sv]=@APPLICATION_NAME@ desktopssynkroniseringsklient
|
||||
GenericName[sv]=Mappsynkronisering
|
||||
|
||||
@@ -96,26 +96,15 @@ endif()
|
||||
message(STATUS "GIT_SHA1 ${GIT_SHA1}")
|
||||
|
||||
set(SYSCONFDIR ${SYSCONF_INSTALL_DIR})
|
||||
set(SHAREDIR ${CMAKE_INSTALL_DATADIR})
|
||||
set(SHAREDIR ${CMAKE_INSTALL_FULL_DATADIR})
|
||||
|
||||
#####
|
||||
## handle BUILD_OWNCLOUD_OSX_BUNDLE
|
||||
# BUILD_OWNCLOUD_OSX_BUNDLE was not initialized OR set to true on OSX
|
||||
if(APPLE AND (NOT DEFINED BUILD_OWNCLOUD_OSX_BUNDLE OR BUILD_OWNCLOUD_OSX_BUNDLE))
|
||||
set(BUILD_OWNCLOUD_OSX_BUNDLE ON)
|
||||
# Build MacOS app bundle if wished
|
||||
if(APPLE AND BUILD_OWNCLOUD_OSX_BUNDLE)
|
||||
message(STATUS "Build MacOS app bundle")
|
||||
set(OWNCLOUD_OSX_BUNDLE "${APPLICATION_NAME}.app")
|
||||
set(LIB_INSTALL_DIR "${APPLICATION_NAME}.app/Contents/MacOS")
|
||||
set(BIN_INSTALL_DIR "${APPLICATION_NAME}.app/Contents/MacOS")
|
||||
|
||||
# BUILD_OWNCLOUD_OSX_BUNDLE was disabled on OSX
|
||||
elseif(APPLE AND NOT BUILD_OWNCLOUD_OSX_BUNDLE)
|
||||
message(FATAL_ERROR "Building in non-bundle mode on OSX is currently not supported. Comment this error out if you want to work on/test it.")
|
||||
|
||||
# any other platform
|
||||
else()
|
||||
set(BUILD_OWNCLOUD_OSX_BUNDLE OFF)
|
||||
endif()
|
||||
#####
|
||||
|
||||
# this option removes Http authentication, keychain, shibboleth etc and is intended for
|
||||
# external authentication mechanisms
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
set( MIRALL_VERSION_MAJOR 3 )
|
||||
set( MIRALL_VERSION_MINOR 3 )
|
||||
set( MIRALL_VERSION_PATCH 81 )
|
||||
set( MIRALL_VERSION_YEAR 2021 )
|
||||
set( MIRALL_VERSION_MINOR 4 )
|
||||
set( MIRALL_VERSION_PATCH 2 )
|
||||
set( MIRALL_VERSION_YEAR 2022 )
|
||||
set( MIRALL_SOVERSION 0 )
|
||||
|
||||
# Minimum supported server version according to https://docs.nextcloud.com/server/latest/admin_manual/release_schedule.html
|
||||
|
||||
@@ -695,7 +695,12 @@
|
||||
<key>PROJECT_SETTINGS</key>
|
||||
<dict>
|
||||
<key>ADVANCED_OPTIONS</key>
|
||||
<dict/>
|
||||
<dict>
|
||||
<key>installer-script.options:hostArchitectures</key>
|
||||
<array>
|
||||
<string>x86_64,arm64</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>BUILD_FORMAT</key>
|
||||
<integer>0</integer>
|
||||
<key>BUILD_PATH</key>
|
||||
|
||||
@@ -26,6 +26,8 @@ install(FILES
|
||||
${CMAKE_CURRENT_BINARY_DIR}/make-msi.bat
|
||||
Platform.wxi
|
||||
Nextcloud.wxs
|
||||
RegistryCleanup.vbs
|
||||
RegistryCleanupCustomAction.wxs
|
||||
gui/banner.bmp
|
||||
gui/dialog.bmp
|
||||
DESTINATION msi/)
|
||||
|
||||
@@ -76,12 +76,16 @@
|
||||
|
||||
<!-- Uninstall: Remove sync folders from Explorer's Navigation Pane, only effective for the current user (home users) -->
|
||||
<Custom Action="RemoveNavigationPaneEntries" After="RemoveFiles">(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")</Custom>
|
||||
|
||||
<!-- Uninstall: Cleanup the Registry -->
|
||||
<Custom Action="RegistryCleanupCustomAction" After="RemoveFiles">(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")</Custom>
|
||||
|
||||
<!-- Schedule Reboot for the Shell Extensions (in silent installation mode only, or if SCHEDULE_REBOOT argument is set-->
|
||||
<ScheduleReboot After="InstallFinalize">(SCHEDULE_REBOOT=1) OR NOT (UILevel=2)</ScheduleReboot>
|
||||
</InstallExecuteSequence>
|
||||
|
||||
<!-- "Add or Remove" Programs Entries -->
|
||||
<Property Id="APPNAME">$(var.AppName)</Property>
|
||||
<Property Id="ARPPRODUCTICON">$(var.AppIcon)</Property>
|
||||
<Property Id="ARPHELPLINK">$(var.AppHelpLink)</Property>
|
||||
<Property Id="ARPURLINFOABOUT">$(var.AppInfoLink)</Property>
|
||||
|
||||
54
admin/win/msi/RegistryCleanup.vbs
Normal file
54
admin/win/msi/RegistryCleanup.vbs
Normal file
@@ -0,0 +1,54 @@
|
||||
On Error goto 0
|
||||
|
||||
Const HKEY_LOCAL_MACHINE = &H80000002
|
||||
|
||||
Const strObjRegistry = "winmgmts:\\.\root\default:StdRegProv"
|
||||
|
||||
Function RegistryDeleteKeyRecursive(regRoot, strKeyPath)
|
||||
Set objRegistry = GetObject(strObjRegistry)
|
||||
objRegistry.EnumKey regRoot, strKeyPath, arrSubkeys
|
||||
If IsArray(arrSubkeys) Then
|
||||
For Each strSubkey In arrSubkeys
|
||||
RegistryDeleteKeyRecursive regRoot, strKeyPath & "\" & strSubkey
|
||||
Next
|
||||
End If
|
||||
objRegistry.DeleteKey regRoot, strKeyPath
|
||||
End Function
|
||||
|
||||
Function RegistryListSubkeys(regRoot, strKeyPath)
|
||||
Set objRegistry = GetObject(strObjRegistry)
|
||||
objRegistry.EnumKey regRoot, strKeyPath, arrSubkeys
|
||||
RegistryListSubkeys = arrSubkeys
|
||||
End Function
|
||||
|
||||
Function GetUserSID()
|
||||
Dim objWshNetwork, objUserAccount
|
||||
|
||||
Set objWshNetwork = CreateObject("WScript.Network")
|
||||
|
||||
Set objUserAccount = GetObject("winmgmts://" & objWshNetwork.UserDomain & "/root/cimv2").Get("Win32_UserAccount.Domain='" & objWshNetwork.ComputerName & "',Name='" & objWshNetwork.UserName & "'")
|
||||
GetUserSID = objUserAccount.SID
|
||||
End Function
|
||||
|
||||
Function RegistryCleanupSyncRootManager()
|
||||
strSyncRootManagerKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager"
|
||||
|
||||
arrSubKeys = RegistryListSubkeys(HKEY_LOCAL_MACHINE, strSyncRootManagerKeyPath)
|
||||
|
||||
If IsArray(arrSubkeys) Then
|
||||
arrSubkeys=Filter(arrSubkeys, Session.Property("APPNAME"))
|
||||
End If
|
||||
If IsArray(arrSubkeys) Then
|
||||
arrSubkeys=Filter(arrSubkeys, GetUserSID())
|
||||
End If
|
||||
|
||||
If IsArray(arrSubkeys) Then
|
||||
For Each strSubkey In arrSubkeys
|
||||
RegistryDeleteKeyRecursive HKEY_LOCAL_MACHINE, strSyncRootManagerKeyPath & "\" & strSubkey
|
||||
Next
|
||||
End If
|
||||
End Function
|
||||
|
||||
Function RegistryCleanup()
|
||||
RegistryCleanupSyncRootManager()
|
||||
End Function
|
||||
7
admin/win/msi/RegistryCleanupCustomAction.wxs
Normal file
7
admin/win/msi/RegistryCleanupCustomAction.wxs
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Fragment>
|
||||
<Binary Id="RegistryCleanup" SourceFile="RegistryCleanup.vbs"/>
|
||||
<CustomAction Id='RegistryCleanupCustomAction' BinaryKey="RegistryCleanup" VBScriptCall="RegistryCleanup" Return="ignore" Execute="immediate"/>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
@@ -17,10 +17,10 @@ Rem Generate collect.wxs
|
||||
if %ERRORLEVEL% neq 0 exit %ERRORLEVEL%
|
||||
|
||||
Rem Compile en-US (https://www.firegiant.com/wix/tutorial/transforms/morphing-installers/)
|
||||
"%WIX%\bin\candle.exe" -dcodepage=1252 -dPlatform=%BuildArch% -arch %BuildArch% -dHarvestAppDir="%HarvestAppDir%" -ext WixUtilExtension NCMsiHelper.wxs WinShellExt.wxs collect.wxs Nextcloud.wxs
|
||||
"%WIX%\bin\candle.exe" -dcodepage=1252 -dPlatform=%BuildArch% -arch %BuildArch% -dHarvestAppDir="%HarvestAppDir%" -ext WixUtilExtension NCMsiHelper.wxs WinShellExt.wxs collect.wxs Nextcloud.wxs RegistryCleanupCustomAction.wxs
|
||||
if %ERRORLEVEL% neq 0 exit %ERRORLEVEL%
|
||||
|
||||
Rem Link MSI package
|
||||
"%WIX%\bin\light.exe" -sw1076 -ext WixUIExtension -ext WixUtilExtension -cultures:en-us NCMsiHelper.wixobj WinShellExt.wixobj collect.wixobj Nextcloud.wixobj -out "@MSI_INSTALLER_FILENAME@"
|
||||
"%WIX%\bin\light.exe" -sw1076 -ext WixUIExtension -ext WixUtilExtension -cultures:en-us NCMsiHelper.wixobj WinShellExt.wixobj collect.wixobj Nextcloud.wixobj RegistryCleanupCustomAction.wixobj -out "@MSI_INSTALLER_FILENAME@"
|
||||
|
||||
exit %ERRORLEVEL%
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
# target does not have the ``WIN32_EXECUTABLE`` property set.
|
||||
# * One of the tools png2ico (See :find-module:`FindPng2Ico`) or
|
||||
# icotool (see :find-module:`FindIcoTool`) is required.
|
||||
# * Supported sizes: 16, 24, 32, 48, 64, 128, 256, 512 and 1024.
|
||||
# * Supported sizes: 16, 20, 24, 32, 40, 48, 64, 128, 256, 512 and 1024.
|
||||
#
|
||||
# Mac OS X notes
|
||||
# * The executable target must have the ``MACOSX_BUNDLE`` property set.
|
||||
@@ -102,9 +102,12 @@ include(CMakeParseArguments)
|
||||
|
||||
function(ecm_add_app_icon appsources)
|
||||
set(options)
|
||||
set(oneValueArgs OUTFILE_BASENAME)
|
||||
set(multiValueArgs ICONS SIDEBAR_ICONS)
|
||||
set(oneValueArgs OUTFILE_BASENAME ICON_INDEX)
|
||||
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)
|
||||
endif()
|
||||
|
||||
if(NOT ARG_ICONS)
|
||||
message(FATAL_ERROR "No ICONS argument given to ecm_add_app_icon")
|
||||
@@ -138,8 +141,11 @@ function(ecm_add_app_icon appsources)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
|
||||
_ecm_add_app_icon_categorize_icons("${ARG_ICONS}" "icons" "16;24;32;48;64;128;256;512;1024")
|
||||
if (WIN32)
|
||||
_ecm_add_app_icon_categorize_icons("${ARG_ICONS}" "icons" "16;20;24;32;40;48;64;128;256;512;1024")
|
||||
else()
|
||||
_ecm_add_app_icon_categorize_icons("${ARG_ICONS}" "icons" "16;24;32;48;64;128;256;512;1024")
|
||||
endif()
|
||||
if(ARG_SIDEBAR_ICONS)
|
||||
_ecm_add_app_icon_categorize_icons("${ARG_SIDEBAR_ICONS}" "sidebar_icons" "16;32;64;128;256")
|
||||
endif()
|
||||
@@ -168,8 +174,10 @@ function(ecm_add_app_icon appsources)
|
||||
|
||||
|
||||
set(windows_icons ${icons_at_16px}
|
||||
${icons_at_20px}
|
||||
${icons_at_24px}
|
||||
${icons_at_32px}
|
||||
${icons_at_40px}
|
||||
${icons_at_48px}
|
||||
${icons_at_64px}
|
||||
${icons_at_128px}
|
||||
@@ -204,12 +212,12 @@ function(ecm_add_app_icon appsources)
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
)
|
||||
# this bit's a little hacky to make the dependency stuff work
|
||||
file(WRITE "${_outfilename}.rc.in" "IDI_ICON1 ICON DISCARDABLE \"${_outfilename}.ico\"\n")
|
||||
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 "${_outfilename}.ico"
|
||||
DEPENDS ${ARG_RC_DEPENDENCIES} "${_outfilename}.ico"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
||||
)
|
||||
endfunction()
|
||||
@@ -226,7 +234,7 @@ function(ecm_add_app_icon appsources)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(size 16 24 32 48 64 128 ${maxSize})
|
||||
foreach(size 16 20 24 32 40 48 64 128 ${maxSize})
|
||||
if(NOT icons_at_${size}px)
|
||||
continue()
|
||||
endif()
|
||||
|
||||
@@ -185,6 +185,8 @@ Then, in Terminal:
|
||||
.. code-block:: bash
|
||||
|
||||
% echo 'export CMAKE_INSTALL_PREFIX=~/Builds' >> ~/.nextcloud_build_variables
|
||||
# If you want to build a macOS app bundle for distribution
|
||||
% echo 'export BUILD_OWNCLOUD_OSX_BUNDLE=ON' >> ~/.nextcloud_build_variables
|
||||
|
||||
Replace ``~/Builds`` with a different directory if you'd like the build to end up elsewhere.
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@ copyright = u'2013-2021, The Nextcloud developers'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '3.3'
|
||||
version = '3.4'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '3.3.81'
|
||||
release = '3.4.2'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
||||
@@ -31,7 +31,7 @@ download page.
|
||||
System Requirements
|
||||
----------------------------------
|
||||
|
||||
- Windows 10+
|
||||
- Windows 8.1+
|
||||
- macOS 10.12+ (64-bit only)
|
||||
- Linux
|
||||
- FreeBSD
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
if(APPLE)
|
||||
set(OC_OEM_SHARE_ICNS "${CMAKE_BINARY_DIR}/src/gui/${APPLICATION_ICON_NAME}.icns")
|
||||
set(OC_OEM_SHARE_ICNS "${CMAKE_BINARY_DIR}/src/gui/${APPLICATION_ICON_NAME}.icns")
|
||||
|
||||
# The bundle identifier and application group need to have compatible values with the client
|
||||
# to be able to open a Mach port across the extension's sandbox boundary.
|
||||
# Pass the info through the xcodebuild command line and make sure that the project uses
|
||||
# those user-defined settings to build the plist.
|
||||
add_custom_target( mac_overlayplugin ALL
|
||||
xcodebuild -project ${CMAKE_SOURCE_DIR}/shell_integration/MacOSX/OwnCloudFinderSync/OwnCloudFinderSync.xcodeproj
|
||||
-target FinderSyncExt -configuration Release "SYMROOT=${CMAKE_CURRENT_BINARY_DIR}"
|
||||
"OC_OEM_SHARE_ICNS=${OC_OEM_SHARE_ICNS}"
|
||||
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
|
||||
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
|
||||
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
|
||||
# The bundle identifier and application group need to have compatible values with the client
|
||||
# to be able to open a Mach port across the extension's sandbox boundary.
|
||||
# Pass the info through the xcodebuild command line and make sure that the project uses
|
||||
# those user-defined settings to build the plist.
|
||||
add_custom_target( mac_overlayplugin ALL
|
||||
xcodebuild ARCHS=${CMAKE_OSX_ARCHITECTURES} ONLY_ACTIVE_ARCH=NO
|
||||
-project ${CMAKE_SOURCE_DIR}/shell_integration/MacOSX/OwnCloudFinderSync/OwnCloudFinderSync.xcodeproj
|
||||
-target FinderSyncExt -configuration Release "SYMROOT=${CMAKE_CURRENT_BINARY_DIR}"
|
||||
"OC_OEM_SHARE_ICNS=${OC_OEM_SHARE_ICNS}"
|
||||
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
|
||||
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
|
||||
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
|
||||
COMMENT building Mac Overlay icons
|
||||
VERBATIM)
|
||||
add_dependencies(mac_overlayplugin nextcloud) # for the ownCloud.icns to be generated
|
||||
add_dependencies(mac_overlayplugin nextcloud) # for the ownCloud.icns to be generated
|
||||
|
||||
INSTALL(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Release/FinderSyncExt.appex
|
||||
DESTINATION ${OWNCLOUD_OSX_BUNDLE}/Contents/PlugIns
|
||||
USE_SOURCE_PERMISSIONS)
|
||||
endif(APPLE)
|
||||
if (BUILD_OWNCLOUD_OSX_BUNDLE)
|
||||
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Release/FinderSyncExt.appex
|
||||
DESTINATION ${OWNCLOUD_OSX_BUNDLE}/Contents/PlugIns
|
||||
USE_SOURCE_PERMISSIONS)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
280
src/3rdparty/kirigami/wheelhandler.cpp
vendored
Normal file
280
src/3rdparty/kirigami/wheelhandler.cpp
vendored
Normal file
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "wheelhandler.h"
|
||||
#include <QWheelEvent>
|
||||
#include <QQuickItem>
|
||||
#include <QDebug>
|
||||
|
||||
class GlobalWheelFilterSingleton
|
||||
{
|
||||
public:
|
||||
GlobalWheelFilter self;
|
||||
};
|
||||
|
||||
Q_GLOBAL_STATIC(GlobalWheelFilterSingleton, privateGlobalWheelFilterSelf)
|
||||
|
||||
GlobalWheelFilter::GlobalWheelFilter(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
GlobalWheelFilter::~GlobalWheelFilter() = default;
|
||||
|
||||
GlobalWheelFilter *GlobalWheelFilter::self()
|
||||
{
|
||||
return &privateGlobalWheelFilterSelf()->self;
|
||||
}
|
||||
|
||||
void GlobalWheelFilter::setItemHandlerAssociation(QQuickItem *item, WheelHandler *handler)
|
||||
{
|
||||
if (!m_handlersForItem.contains(handler->target())) {
|
||||
handler->target()->installEventFilter(this);
|
||||
}
|
||||
m_handlersForItem.insert(item, handler);
|
||||
|
||||
connect(item, &QObject::destroyed, this, [this](QObject *obj) {
|
||||
auto item = static_cast<QQuickItem *>(obj);
|
||||
m_handlersForItem.remove(item);
|
||||
});
|
||||
|
||||
connect(handler, &QObject::destroyed, this, [this](QObject *obj) {
|
||||
auto handler = static_cast<WheelHandler *>(obj);
|
||||
removeItemHandlerAssociation(handler->target(), handler);
|
||||
});
|
||||
}
|
||||
|
||||
void GlobalWheelFilter::removeItemHandlerAssociation(QQuickItem *item, WheelHandler *handler)
|
||||
{
|
||||
if (!item || !handler) {
|
||||
return;
|
||||
}
|
||||
m_handlersForItem.remove(item, handler);
|
||||
if (!m_handlersForItem.contains(item)) {
|
||||
item->removeEventFilter(this);
|
||||
}
|
||||
}
|
||||
|
||||
bool GlobalWheelFilter::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::Wheel) {
|
||||
auto item = qobject_cast<QQuickItem *>(watched);
|
||||
if (!item || !item->isEnabled()) {
|
||||
return QObject::eventFilter(watched, event);
|
||||
}
|
||||
auto we = static_cast<QWheelEvent *>(event);
|
||||
m_wheelEvent.initializeFromEvent(we);
|
||||
|
||||
bool shouldBlock = false;
|
||||
bool shouldScrollFlickable = false;
|
||||
|
||||
for (auto *handler : m_handlersForItem.values(item)) {
|
||||
if (handler->m_blockTargetWheel) {
|
||||
shouldBlock = true;
|
||||
}
|
||||
if (handler->m_scrollFlickableTarget) {
|
||||
shouldScrollFlickable = true;
|
||||
}
|
||||
emit handler->wheel(&m_wheelEvent);
|
||||
}
|
||||
|
||||
if (shouldScrollFlickable && !m_wheelEvent.isAccepted()) {
|
||||
manageWheel(item, we);
|
||||
}
|
||||
|
||||
if (shouldBlock) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QObject::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void GlobalWheelFilter::manageWheel(QQuickItem *target, QWheelEvent *event)
|
||||
{
|
||||
// Duck typing: accept everyhint that has all the properties we need
|
||||
if (target->metaObject()->indexOfProperty("contentX") == -1
|
||||
|| target->metaObject()->indexOfProperty("contentY") == -1
|
||||
|| target->metaObject()->indexOfProperty("contentWidth") == -1
|
||||
|| target->metaObject()->indexOfProperty("contentHeight") == -1
|
||||
|| target->metaObject()->indexOfProperty("topMargin") == -1
|
||||
|| target->metaObject()->indexOfProperty("bottomMargin") == -1
|
||||
|| target->metaObject()->indexOfProperty("leftMargin") == -1
|
||||
|| target->metaObject()->indexOfProperty("rightMargin") == -1
|
||||
|| target->metaObject()->indexOfProperty("originX") == -1
|
||||
|| target->metaObject()->indexOfProperty("originY") == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
qreal contentWidth = target->property("contentWidth").toReal();
|
||||
qreal contentHeight = target->property("contentHeight").toReal();
|
||||
qreal contentX = target->property("contentX").toReal();
|
||||
qreal contentY = target->property("contentY").toReal();
|
||||
qreal topMargin = target->property("topMargin").toReal();
|
||||
qreal bottomMargin = target->property("bottomMargin").toReal();
|
||||
qreal leftMargin = target->property("leftMaring").toReal();
|
||||
qreal rightMargin = target->property("rightMargin").toReal();
|
||||
qreal originX = target->property("originX").toReal();
|
||||
qreal originY = target->property("originY").toReal();
|
||||
|
||||
// Scroll Y
|
||||
if (contentHeight > target->height()) {
|
||||
|
||||
int y = event->pixelDelta().y() != 0 ? event->pixelDelta().y() : event->angleDelta().y() / 8;
|
||||
|
||||
//if we don't have a pixeldelta, apply the configured mouse wheel lines
|
||||
if (!event->pixelDelta().y()) {
|
||||
y *= 3; // Magic copied value from Kirigami::Settings
|
||||
}
|
||||
|
||||
// Scroll one page regardless of delta:
|
||||
if ((event->modifiers() & Qt::ControlModifier) || (event->modifiers() & Qt::ShiftModifier)) {
|
||||
if (y > 0) {
|
||||
y = target->height();
|
||||
} else if (y < 0) {
|
||||
y = -target->height();
|
||||
}
|
||||
}
|
||||
|
||||
qreal minYExtent = topMargin - originY;
|
||||
qreal maxYExtent = target->height() - (contentHeight + bottomMargin + originY);
|
||||
|
||||
target->setProperty("contentY", qMin(-maxYExtent, qMax(-minYExtent, contentY - y)));
|
||||
}
|
||||
|
||||
//Scroll X
|
||||
if (contentWidth > target->width()) {
|
||||
|
||||
int x = event->pixelDelta().x() != 0 ? event->pixelDelta().x() : event->angleDelta().x() / 8;
|
||||
|
||||
// Special case: when can't scroll vertically, scroll horizontally with vertical wheel as well
|
||||
if (x == 0 && contentHeight <= target->height()) {
|
||||
x = event->pixelDelta().y() != 0 ? event->pixelDelta().y() : event->angleDelta().y() / 8;
|
||||
}
|
||||
|
||||
//if we don't have a pixeldelta, apply the configured mouse wheel lines
|
||||
if (!event->pixelDelta().x()) {
|
||||
x *= 3; // Magic copied value from Kirigami::Settings
|
||||
}
|
||||
|
||||
// Scroll one page regardless of delta:
|
||||
if ((event->modifiers() & Qt::ControlModifier) || (event->modifiers() & Qt::ShiftModifier)) {
|
||||
if (x > 0) {
|
||||
x = target->width();
|
||||
} else if (x < 0) {
|
||||
x = -target->width();
|
||||
}
|
||||
}
|
||||
|
||||
qreal minXExtent = leftMargin - originX;
|
||||
qreal maxXExtent = target->width() - (contentWidth + rightMargin + originX);
|
||||
|
||||
target->setProperty("contentX", qMin(-maxXExtent, qMax(-minXExtent, contentX - x)));
|
||||
}
|
||||
|
||||
//this is just for making the scrollbar
|
||||
target->metaObject()->invokeMethod(target, "flick", Q_ARG(double, 0), Q_ARG(double, 1));
|
||||
target->metaObject()->invokeMethod(target, "cancelFlick");
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////
|
||||
KirigamiWheelEvent::KirigamiWheelEvent(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
KirigamiWheelEvent::~KirigamiWheelEvent() = default;
|
||||
|
||||
void KirigamiWheelEvent::initializeFromEvent(QWheelEvent *event)
|
||||
{
|
||||
m_x = event->position().x();
|
||||
m_y = event->position().y();
|
||||
m_angleDelta = event->angleDelta();
|
||||
m_pixelDelta = event->pixelDelta();
|
||||
m_buttons = event->buttons();
|
||||
m_modifiers = event->modifiers();
|
||||
m_accepted = false;
|
||||
m_inverted = event->inverted();
|
||||
}
|
||||
|
||||
qreal KirigamiWheelEvent::x() const
|
||||
{
|
||||
return m_x;
|
||||
}
|
||||
|
||||
qreal KirigamiWheelEvent::y() const
|
||||
{
|
||||
return m_y;
|
||||
}
|
||||
|
||||
QPointF KirigamiWheelEvent::angleDelta() const
|
||||
{
|
||||
return m_angleDelta;
|
||||
}
|
||||
|
||||
QPointF KirigamiWheelEvent::pixelDelta() const
|
||||
{
|
||||
return m_pixelDelta;
|
||||
}
|
||||
|
||||
int KirigamiWheelEvent::buttons() const
|
||||
{
|
||||
return m_buttons;
|
||||
}
|
||||
|
||||
int KirigamiWheelEvent::modifiers() const
|
||||
{
|
||||
return m_modifiers;
|
||||
}
|
||||
|
||||
bool KirigamiWheelEvent::inverted() const
|
||||
{
|
||||
return m_inverted;
|
||||
}
|
||||
|
||||
bool KirigamiWheelEvent::isAccepted()
|
||||
{
|
||||
return m_accepted;
|
||||
}
|
||||
|
||||
void KirigamiWheelEvent::setAccepted(bool accepted)
|
||||
{
|
||||
m_accepted = accepted;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////
|
||||
|
||||
WheelHandler::WheelHandler(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
WheelHandler::~WheelHandler() = default;
|
||||
|
||||
QQuickItem *WheelHandler::target() const
|
||||
{
|
||||
return m_target;
|
||||
}
|
||||
|
||||
void WheelHandler::setTarget(QQuickItem *target)
|
||||
{
|
||||
if (m_target == target) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_target) {
|
||||
GlobalWheelFilter::self()->removeItemHandlerAssociation(m_target, this);
|
||||
}
|
||||
|
||||
m_target = target;
|
||||
|
||||
GlobalWheelFilter::self()->setItemHandlerAssociation(target, this);
|
||||
|
||||
emit targetChanged();
|
||||
}
|
||||
|
||||
|
||||
#include "moc_wheelhandler.cpp"
|
||||
213
src/3rdparty/kirigami/wheelhandler.h
vendored
Normal file
213
src/3rdparty/kirigami/wheelhandler.h
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtQml>
|
||||
#include <QPoint>
|
||||
#include <QQuickItem>
|
||||
#include <QObject>
|
||||
|
||||
class QWheelEvent;
|
||||
|
||||
class WheelHandler;
|
||||
|
||||
/**
|
||||
* Describes the mouse wheel event
|
||||
*/
|
||||
class KirigamiWheelEvent : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* x: real
|
||||
*
|
||||
* X coordinate of the mouse pointer
|
||||
*/
|
||||
Q_PROPERTY(qreal x READ x CONSTANT)
|
||||
|
||||
/**
|
||||
* y: real
|
||||
*
|
||||
* Y coordinate of the mouse pointer
|
||||
*/
|
||||
Q_PROPERTY(qreal y READ y CONSTANT)
|
||||
|
||||
/**
|
||||
* angleDelta: point
|
||||
*
|
||||
* The distance the wheel is rotated in degrees.
|
||||
* The x and y coordinates indicate the horizontal and vertical wheels respectively.
|
||||
* A positive value indicates it was rotated up/right, negative, bottom/left
|
||||
* This value is more likely to be set in traditional mice.
|
||||
*/
|
||||
Q_PROPERTY(QPointF angleDelta READ angleDelta CONSTANT)
|
||||
|
||||
/**
|
||||
* pixelDelta: point
|
||||
*
|
||||
* provides the delta in screen pixels available on high resolution trackpads
|
||||
*/
|
||||
Q_PROPERTY(QPointF pixelDelta READ pixelDelta CONSTANT)
|
||||
|
||||
/**
|
||||
* buttons: int
|
||||
*
|
||||
* it contains an OR combination of the buttons that were pressed during the wheel, they can be:
|
||||
* Qt.LeftButton, Qt.MiddleButton, Qt.RightButton
|
||||
*/
|
||||
Q_PROPERTY(int buttons READ buttons CONSTANT)
|
||||
|
||||
/**
|
||||
* modifiers: int
|
||||
*
|
||||
* Keyboard mobifiers that were pressed during the wheel event, such as:
|
||||
* Qt.NoModifier (default, no modifiers)
|
||||
* Qt.ControlModifier
|
||||
* Qt.ShiftModifier
|
||||
* ...
|
||||
*/
|
||||
Q_PROPERTY(int modifiers READ modifiers CONSTANT)
|
||||
|
||||
/**
|
||||
* inverted: bool
|
||||
*
|
||||
* Whether the delta values are inverted
|
||||
* On some platformsthe returned delta are inverted, so positive values would mean bottom/left
|
||||
*/
|
||||
Q_PROPERTY(bool inverted READ inverted CONSTANT)
|
||||
|
||||
/**
|
||||
* accepted: bool
|
||||
*
|
||||
* If set, the event shouldn't be managed anymore,
|
||||
* for instance it can be used to block the handler to manage the scroll of a view on some scenarios
|
||||
* @code
|
||||
* // This handler handles automatically the scroll of
|
||||
* // flickableItem, unless Ctrl is pressed, in this case the
|
||||
* // app has custom code to handle Ctrl+wheel zooming
|
||||
* Kirigami.WheelHandler {
|
||||
* target: flickableItem
|
||||
* blockTargetWheel: true
|
||||
* scrollFlickableTarget: true
|
||||
* onWheel: {
|
||||
* if (wheel.modifiers & Qt.ControlModifier) {
|
||||
* wheel.accepted = true;
|
||||
* // Handle scaling of the view
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
*/
|
||||
Q_PROPERTY(bool accepted READ isAccepted WRITE setAccepted)
|
||||
|
||||
public:
|
||||
KirigamiWheelEvent(QObject *parent = nullptr);
|
||||
~KirigamiWheelEvent() override;
|
||||
|
||||
void initializeFromEvent(QWheelEvent *event);
|
||||
|
||||
qreal x() const;
|
||||
qreal y() const;
|
||||
QPointF angleDelta() const;
|
||||
QPointF pixelDelta() const;
|
||||
int buttons() const;
|
||||
int modifiers() const;
|
||||
bool inverted() const;
|
||||
bool isAccepted();
|
||||
void setAccepted(bool accepted);
|
||||
|
||||
private:
|
||||
qreal m_x = 0;
|
||||
qreal m_y = 0;
|
||||
QPointF m_angleDelta;
|
||||
QPointF m_pixelDelta;
|
||||
Qt::MouseButtons m_buttons = Qt::NoButton;
|
||||
Qt::KeyboardModifiers m_modifiers = Qt::NoModifier;
|
||||
bool m_inverted = false;
|
||||
bool m_accepted = false;
|
||||
};
|
||||
|
||||
class GlobalWheelFilter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
GlobalWheelFilter(QObject *parent = nullptr);
|
||||
~GlobalWheelFilter() override;
|
||||
|
||||
static GlobalWheelFilter *self();
|
||||
|
||||
void setItemHandlerAssociation(QQuickItem *item, WheelHandler *handler);
|
||||
void removeItemHandlerAssociation(QQuickItem *item, WheelHandler *handler);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private:
|
||||
void manageWheel(QQuickItem *target, QWheelEvent *wheel);
|
||||
|
||||
QMultiHash<QQuickItem *, WheelHandler *> m_handlersForItem;
|
||||
KirigamiWheelEvent m_wheelEvent;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This class intercepts the mouse wheel events of its target, and gives them to the user code as a signal, which can be used for custom mouse wheel management code.
|
||||
* The handler can block completely the wheel events from its target, and if it's a Flickable, it can automatically handle scrolling on it
|
||||
*/
|
||||
class WheelHandler : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/**
|
||||
* target: Item
|
||||
*
|
||||
* The target we want to manage wheel events.
|
||||
* We will receive wheel() signals every time the user moves
|
||||
* the mouse wheel (or scrolls with the touchpad) on top
|
||||
* of that item.
|
||||
*/
|
||||
Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged)
|
||||
|
||||
/**
|
||||
* blockTargetWheel: bool
|
||||
*
|
||||
* If true, the target won't receive any wheel event at all (default true)
|
||||
*/
|
||||
Q_PROPERTY(bool blockTargetWheel MEMBER m_blockTargetWheel NOTIFY blockTargetWheelChanged)
|
||||
|
||||
/**
|
||||
* scrollFlickableTarget: bool
|
||||
* If this property is true and the target is a Flickable, wheel events will cause the Flickable to scroll (default true)
|
||||
*/
|
||||
Q_PROPERTY(bool scrollFlickableTarget MEMBER m_scrollFlickableTarget NOTIFY scrollFlickableTargetChanged)
|
||||
|
||||
public:
|
||||
explicit WheelHandler(QObject *parent = nullptr);
|
||||
~WheelHandler() override;
|
||||
|
||||
QQuickItem *target() const;
|
||||
void setTarget(QQuickItem *target);
|
||||
|
||||
Q_SIGNALS:
|
||||
void targetChanged();
|
||||
void blockTargetWheelChanged();
|
||||
void scrollFlickableTargetChanged();
|
||||
void wheel(KirigamiWheelEvent *wheel);
|
||||
|
||||
private:
|
||||
QPointer<QQuickItem> m_target;
|
||||
bool m_blockTargetWheel = true;
|
||||
bool m_scrollFlickableTarget = true;
|
||||
KirigamiWheelEvent m_wheelEvent;
|
||||
|
||||
friend class GlobalWheelFilter;
|
||||
};
|
||||
|
||||
|
||||
@@ -1001,43 +1001,22 @@ qint64 SyncJournalDb::keyValueStoreGetInt(const QString &key, qint64 defaultValu
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const auto query = _queryManager.get(PreparedSqlQueryManager::GetKeyValueStoreQuery, QByteArrayLiteral("SELECT value FROM key_value_store WHERE key = ?1;"), _db);
|
||||
const auto query = _queryManager.get(PreparedSqlQueryManager::GetKeyValueStoreQuery, QByteArrayLiteral("SELECT value FROM key_value_store WHERE key=?1"), _db);
|
||||
if (!query) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
query->bindValue(1, key);
|
||||
query->exec();
|
||||
auto result = query->next();
|
||||
|
||||
if (!query->next().hasData) {
|
||||
if (!result.ok || !result.hasData) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return query->int64Value(0);
|
||||
}
|
||||
|
||||
QVariant SyncJournalDb::keyValueStoreGet(const QString &key, QVariant defaultValue)
|
||||
{
|
||||
QMutexLocker locker(&_mutex);
|
||||
if (!checkConnect()) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const auto query = _queryManager.get(PreparedSqlQueryManager::GetKeyValueStoreQuery, QByteArrayLiteral("SELECT value FROM key_value_store WHERE key = ?1;"), _db);
|
||||
if (!query) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
query->bindValue(1, key);
|
||||
query->exec();
|
||||
|
||||
if (!query->next().hasData) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return query->stringValue(0);
|
||||
}
|
||||
|
||||
void SyncJournalDb::keyValueStoreDelete(const QString &key)
|
||||
{
|
||||
const auto query = _queryManager.get(PreparedSqlQueryManager::DeleteKeyValueStoreQuery, QByteArrayLiteral("DELETE FROM key_value_store WHERE key=?1;"), _db);
|
||||
|
||||
@@ -70,7 +70,6 @@ public:
|
||||
|
||||
void keyValueStoreSet(const QString &key, QVariant value);
|
||||
qint64 keyValueStoreGetInt(const QString &key, qint64 defaultValue);
|
||||
QVariant keyValueStoreGet(const QString &key, QVariant defaultValue = {});
|
||||
void keyValueStoreDelete(const QString &key);
|
||||
|
||||
bool deleteFileRecord(const QString &filename, bool recursively = false);
|
||||
|
||||
@@ -109,6 +109,11 @@ void Utility::setupFavLink(const QString &folder)
|
||||
setupFavLink_private(folder);
|
||||
}
|
||||
|
||||
void Utility::removeFavLink(const QString &folder)
|
||||
{
|
||||
removeFavLink_private(folder);
|
||||
}
|
||||
|
||||
QString Utility::octetsToString(qint64 octets)
|
||||
{
|
||||
#define THE_FACTOR 1024
|
||||
|
||||
@@ -55,6 +55,7 @@ namespace Utility {
|
||||
OCSYNC_EXPORT void usleep(int usec);
|
||||
OCSYNC_EXPORT QString formatFingerprint(const QByteArray &, bool colonSeparated = true);
|
||||
OCSYNC_EXPORT void setupFavLink(const QString &folder);
|
||||
OCSYNC_EXPORT void removeFavLink(const QString &folder);
|
||||
OCSYNC_EXPORT bool writeRandomFile(const QString &fname, int size = -1);
|
||||
OCSYNC_EXPORT QString octetsToString(qint64 octets);
|
||||
OCSYNC_EXPORT QByteArray userAgentString();
|
||||
@@ -241,6 +242,11 @@ namespace Utility {
|
||||
*/
|
||||
OCSYNC_EXPORT bool isPathWindowsDrivePartitionRoot(const QString &path);
|
||||
|
||||
/**
|
||||
* @brief Retrieves current logged-in user name from the OS
|
||||
*/
|
||||
OCSYNC_EXPORT QString getCurrentUserName();
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
OCSYNC_EXPORT bool registryKeyExists(HKEY hRootKey, const QString &subKey);
|
||||
OCSYNC_EXPORT QVariant registryGetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName);
|
||||
|
||||
@@ -41,6 +41,11 @@ static void setupFavLink_private(const QString &folder)
|
||||
CFRelease(urlRef);
|
||||
}
|
||||
|
||||
static void removeFavLink_private(const QString &folder)
|
||||
{
|
||||
Q_UNUSED(folder)
|
||||
}
|
||||
|
||||
bool hasLaunchOnStartup_private(const QString &)
|
||||
{
|
||||
// this is quite some duplicate code with setLaunchOnStartup, at some point we should fix this FIXME.
|
||||
@@ -131,4 +136,9 @@ static bool hasDarkSystray_private()
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
QString Utility::getCurrentUserName()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
@@ -37,6 +37,11 @@ static void setupFavLink_private(const QString &folder)
|
||||
}
|
||||
}
|
||||
|
||||
static void removeFavLink_private(const QString &folder)
|
||||
{
|
||||
Q_UNUSED(folder)
|
||||
}
|
||||
|
||||
// returns the autostart directory the linux way
|
||||
// and respects the XDG_CONFIG_HOME env variable
|
||||
QString getUserAutostartDir_private()
|
||||
@@ -103,4 +108,9 @@ static inline bool hasDarkSystray_private()
|
||||
return true;
|
||||
}
|
||||
|
||||
QString Utility::getCurrentUserName()
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
|
||||
#include "asserts.h"
|
||||
#include "utility.h"
|
||||
#include "gui/configgui.h"
|
||||
|
||||
#include <comdef.h>
|
||||
#include <Lmcons.h>
|
||||
#include <shlguid.h>
|
||||
#include <shlobj.h>
|
||||
#include <string>
|
||||
@@ -47,7 +49,14 @@ static void setupFavLink_private(const QString &folder)
|
||||
desktopIni.open(QFile::WriteOnly);
|
||||
desktopIni.write("[.ShellClassInfo]\r\nIconResource=");
|
||||
desktopIni.write(QDir::toNativeSeparators(qApp->applicationFilePath()).toUtf8());
|
||||
desktopIni.write(",0\r\n");
|
||||
#ifdef APPLICATION_FOLDER_ICON_INDEX
|
||||
const auto iconIndex = APPLICATION_FOLDER_ICON_INDEX;
|
||||
#else
|
||||
const auto iconIndex = "0";
|
||||
#endif
|
||||
desktopIni.write(",");
|
||||
desktopIni.write(iconIndex);
|
||||
desktopIni.write("\r\n");
|
||||
desktopIni.close();
|
||||
|
||||
// Set the folder as system and Desktop.ini as hidden+system for explorer to pick it.
|
||||
@@ -74,6 +83,40 @@ static void setupFavLink_private(const QString &folder)
|
||||
qCWarning(lcUtility) << "linking" << folder << "to" << linkName << "failed!";
|
||||
}
|
||||
|
||||
static void removeFavLink_private(const QString &folder)
|
||||
{
|
||||
const QDir folderDir(folder);
|
||||
|
||||
// #1 Remove the Desktop.ini to reset the folder icon
|
||||
if (!QFile::remove(folderDir.absoluteFilePath(QLatin1String("Desktop.ini")))) {
|
||||
qCWarning(lcUtility) << "Remove Desktop.ini from" << folder
|
||||
<< " has failed. Make sure it exists and is not locked by another process.";
|
||||
}
|
||||
|
||||
// #2 Remove the system file attribute
|
||||
const auto folderAttrs = GetFileAttributesW(folder.toStdWString().c_str());
|
||||
if (!SetFileAttributesW(folder.toStdWString().c_str(), folderAttrs & ~FILE_ATTRIBUTE_SYSTEM)) {
|
||||
qCWarning(lcUtility) << "Remove system file attribute failed for:" << folder;
|
||||
}
|
||||
|
||||
// #3 Remove the link to this folder
|
||||
PWSTR path;
|
||||
if (!SHGetKnownFolderPath(FOLDERID_Links, 0, nullptr, &path) == S_OK) {
|
||||
qCWarning(lcUtility) << "SHGetKnownFolderPath for " << folder << "has failed.";
|
||||
return;
|
||||
}
|
||||
|
||||
const QDir links(QString::fromWCharArray(path));
|
||||
CoTaskMemFree(path);
|
||||
|
||||
const auto linkName = QDir(links).absoluteFilePath(folderDir.dirName() + QLatin1String(".lnk"));
|
||||
|
||||
qCInfo(lcUtility) << "Removing favorite link from" << folder << "to" << linkName;
|
||||
if (!QFile::remove(linkName)) {
|
||||
qCWarning(lcUtility) << "Removing a favorite link from" << folder << "to" << linkName << "failed.";
|
||||
}
|
||||
}
|
||||
|
||||
bool hasSystemLaunchOnStartup_private(const QString &appName)
|
||||
{
|
||||
QString runPath = QLatin1String(systemRunPathC);
|
||||
@@ -346,6 +389,17 @@ QString Utility::formatWinError(long errorCode)
|
||||
return QStringLiteral("WindowsError: %1: %2").arg(QString::number(errorCode, 16), QString::fromWCharArray(_com_error(errorCode).ErrorMessage()));
|
||||
}
|
||||
|
||||
QString Utility::getCurrentUserName()
|
||||
{
|
||||
TCHAR username[UNLEN + 1] = {0};
|
||||
DWORD len = sizeof(username) / sizeof(TCHAR);
|
||||
|
||||
if (!GetUserName(username, &len)) {
|
||||
qCWarning(lcUtility) << "Could not retrieve Windows user name." << formatWinError(GetLastError());
|
||||
}
|
||||
|
||||
return QString::fromWCharArray(username);
|
||||
}
|
||||
|
||||
Utility::NtfsPermissionLookupRAII::NtfsPermissionLookupRAII()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
project(gui)
|
||||
find_package(Qt5 REQUIRED COMPONENTS Widgets Svg Qml Quick QuickControls2)
|
||||
find_package(Qt5 REQUIRED COMPONENTS Widgets Svg Qml Quick QuickControls2 Xml Network)
|
||||
if (NOT TARGET Qt5::GuiPrivate)
|
||||
message(FATAL_ERROR "Could not find GuiPrivate component of Qt5. It might be shipped as a separate package, please check that.")
|
||||
endif()
|
||||
|
||||
if(CMAKE_BUILD_TYPE MATCHES Debug)
|
||||
add_definitions(-DQT_QML_DEBUG)
|
||||
@@ -34,6 +37,7 @@ set(client_UI_SRCS
|
||||
shareuserline.ui
|
||||
sslerrordialog.ui
|
||||
addcertificatedialog.ui
|
||||
passwordinputdialog.ui
|
||||
proxyauthdialog.ui
|
||||
mnemonicdialog.ui
|
||||
UserStatusSelector.qml
|
||||
@@ -92,6 +96,7 @@ set(client_SRCS
|
||||
openfilemanager.cpp
|
||||
owncloudgui.cpp
|
||||
owncloudsetupwizard.cpp
|
||||
passwordinputdialog.cpp
|
||||
selectivesyncdialog.cpp
|
||||
settingsdialog.cpp
|
||||
sharedialog.cpp
|
||||
@@ -203,6 +208,7 @@ set(3rdparty_SRC
|
||||
../3rdparty/qtsingleapplication/qtsingleapplication.cpp
|
||||
../3rdparty/qtsingleapplication/qtsinglecoreapplication.cpp
|
||||
../3rdparty/kmessagewidget/kmessagewidget.cpp
|
||||
../3rdparty/kirigami/wheelhandler.cpp
|
||||
)
|
||||
|
||||
if(NOT WIN32)
|
||||
@@ -254,6 +260,10 @@ if (NOT DEFINED APPLICATION_ICON_NAME)
|
||||
set(APPLICATION_ICON_NAME ${APPLICATION_SHORTNAME})
|
||||
endif()
|
||||
|
||||
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
|
||||
@@ -265,9 +275,24 @@ if (NOT SVG_CONVERTER)
|
||||
endif()
|
||||
|
||||
function(generate_sized_png_from_svg icon_path size)
|
||||
set(options)
|
||||
set(oneValueArgs OUTPUT_ICON_NAME 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 ()
|
||||
|
||||
|
||||
if (EXISTS "${icon_name_dir}/${size}-${icon_name_wle}.png")
|
||||
return()
|
||||
endif()
|
||||
@@ -313,22 +338,86 @@ if(WIN32)
|
||||
endif()
|
||||
|
||||
set(APP_ICON_SVG "${theme_dir}/colored/${APPLICATION_ICON_NAME}-icon.svg")
|
||||
generate_sized_png_from_svg(${APP_ICON_SVG} 16)
|
||||
generate_sized_png_from_svg(${APP_ICON_SVG} 24)
|
||||
generate_sized_png_from_svg(${APP_ICON_SVG} 32)
|
||||
generate_sized_png_from_svg(${APP_ICON_SVG} 48)
|
||||
generate_sized_png_from_svg(${APP_ICON_SVG} 64)
|
||||
generate_sized_png_from_svg(${APP_ICON_SVG} 128)
|
||||
generate_sized_png_from_svg(${APP_ICON_SVG} 256)
|
||||
generate_sized_png_from_svg(${APP_ICON_SVG} 512)
|
||||
generate_sized_png_from_svg(${APP_ICON_SVG} 1024)
|
||||
|
||||
# generate secondary icon if available (currently for Windows only)--------------------------------------
|
||||
set(APP_SECONDARY_ICONS "${theme_dir}/colored/icons")
|
||||
set(APP_ICON_WIN_FOLDER_SVG "${APP_SECONDARY_ICONS}/${APPLICATION_ICON_NAME}-icon-win-folder.svg")
|
||||
|
||||
set(RC_DEPENDENCIES "")
|
||||
|
||||
if(WIN32)
|
||||
if (EXISTS ${APP_ICON_WIN_FOLDER_SVG})
|
||||
get_filename_component(output_icon_name_win ${APP_ICON_WIN_FOLDER_SVG} NAME_WLE)
|
||||
# Product icon (for smallest size)
|
||||
foreach(size IN ITEMS 16;20)
|
||||
generate_sized_png_from_svg(${APP_ICON_SVG} ${size} OUTPUT_ICON_NAME ${output_icon_name_win} OUTPUT_ICON_PATH "${APP_SECONDARY_ICONS}/")
|
||||
endforeach()
|
||||
|
||||
# Product icon with Windows folder (for sizes larger than 20)
|
||||
foreach(size IN ITEMS 24;32;40;48;64;128;256;512;1024)
|
||||
generate_sized_png_from_svg(${APP_ICON_WIN_FOLDER_SVG} ${size} OUTPUT_ICON_NAME ${output_icon_name_win} OUTPUT_ICON_PATH "${APP_SECONDARY_ICONS}/")
|
||||
endforeach()
|
||||
|
||||
file(GLOB_RECURSE OWNCLOUD_ICONS_WIN_FOLDER "${APP_SECONDARY_ICONS}/*-${APPLICATION_ICON_NAME}-icon*")
|
||||
set(APP_ICON_WIN_FOLDER_ICO_NAME "${APPLICATION_ICON_NAME}-win-folder")
|
||||
set(RC_DEPENDENCIES "${RC_DEPENDENCIES} ${APP_ICON_WIN_FOLDER_ICO_NAME}.ico")
|
||||
ecm_add_app_icon(APP_ICON_WIN_FOLDER ICONS "${OWNCLOUD_ICONS_WIN_FOLDER}" SIDEBAR_ICONS "${OWNCLOUD_SIDEBAR_ICONS}" OUTFILE_BASENAME "${APP_ICON_WIN_FOLDER_ICO_NAME}" ICON_INDEX 2)
|
||||
endif()
|
||||
endif()
|
||||
# --------------------------------------
|
||||
|
||||
if (NOT ${RC_DEPENDENCIES} STREQUAL "")
|
||||
string(STRIP ${RC_DEPENDENCIES} RC_DEPENDENCIES)
|
||||
endif()
|
||||
|
||||
# generate primary icon from SVG (due to Win .ico vs .rc dependency issues, primary icon must always be generated last)--------------------------------------
|
||||
if(WIN32)
|
||||
foreach(size IN ITEMS 16;20;24;32;40;48;64;128;256;512;1024)
|
||||
generate_sized_png_from_svg(${APP_ICON_SVG} ${size})
|
||||
endforeach()
|
||||
else()
|
||||
foreach(size IN ITEMS 16;24;32;48;64;128;256;512;1024)
|
||||
generate_sized_png_from_svg(${APP_ICON_SVG} ${size})
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
file(GLOB_RECURSE OWNCLOUD_ICONS "${theme_dir}/colored/*-${APPLICATION_ICON_NAME}-icon*")
|
||||
|
||||
if(APPLE)
|
||||
file(GLOB_RECURSE OWNCLOUD_SIDEBAR_ICONS "${theme_dir}/colored/*-${APPLICATION_ICON_NAME}-sidebar*")
|
||||
MESSAGE(STATUS "OWNCLOUD_SIDEBAR_ICONS: ${APPLICATION_ICON_NAME}: ${OWNCLOUD_SIDEBAR_ICONS}")
|
||||
endif()
|
||||
ecm_add_app_icon(APP_ICON ICONS "${OWNCLOUD_ICONS}" SIDEBAR_ICONS "${OWNCLOUD_SIDEBAR_ICONS}" OUTFILE_BASENAME "${APPLICATION_ICON_NAME}")
|
||||
|
||||
ecm_add_app_icon(APP_ICON RC_DEPENDENCIES ${RC_DEPENDENCIES} ICONS "${OWNCLOUD_ICONS}" SIDEBAR_ICONS "${OWNCLOUD_SIDEBAR_ICONS}" OUTFILE_BASENAME "${APPLICATION_ICON_NAME}" ICON_INDEX 1)
|
||||
# --------------------------------------
|
||||
|
||||
if(WIN32)
|
||||
# merge *.rc.in files for Windows (multiple ICON resources must be placed in a single file, otherwise, this won't work de to a bug in Windows compiler https://developercommunity.visualstudio.com/t/visual-studio-2017-prof-1557-cvt1100-duplicate-res/363156)
|
||||
function(merge_files IN_FILE OUT_FILE)
|
||||
file(READ ${IN_FILE} CONTENTS)
|
||||
message("Merging ${IN_FILE} into ${OUT_FILE}")
|
||||
file(APPEND ${OUT_FILE} "${CONTENTS}")
|
||||
endfunction()
|
||||
message("APP_ICON is: ${APP_ICON}")
|
||||
if(APP_ICON)
|
||||
get_filename_component(RC_IN_FOLDER ${APP_ICON}} DIRECTORY)
|
||||
|
||||
file(GLOB_RECURSE RC_IN_FILES "${RC_IN_FOLDER}/*rc.in")
|
||||
|
||||
foreach(rc_in_file IN ITEMS ${RC_IN_FILES})
|
||||
get_filename_component(rc_in_file_name ${rc_in_file} NAME)
|
||||
get_filename_component(app_icon_name "${APP_ICON}.in" NAME)
|
||||
if(NOT "${rc_in_file_name}" STREQUAL "${app_icon_name}")
|
||||
merge_files(${rc_in_file} "${APP_ICON}.in")
|
||||
if (DEFINED APPLICATION_FOLDER_ICON_INDEX)
|
||||
MATH(EXPR APPLICATION_FOLDER_ICON_INDEX "${APPLICATION_FOLDER_ICON_INDEX}+1")
|
||||
message("APPLICATION_FOLDER_ICON_INDEX is now set to: ${APPLICATION_FOLDER_ICON_INDEX}")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
endif()
|
||||
# --------------------------------------
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIE")
|
||||
@@ -372,6 +461,7 @@ target_include_directories(nextcloudCore
|
||||
PUBLIC
|
||||
${CMAKE_SOURCE_DIR}/src/3rdparty/QProgressIndicator
|
||||
${CMAKE_SOURCE_DIR}/src/3rdparty/qtlockedfile
|
||||
${CMAKE_SOURCE_DIR}/src/3rdparty/kirigami
|
||||
${CMAKE_SOURCE_DIR}/src/3rdparty/qtsingleapplication
|
||||
${CMAKE_SOURCE_DIR}/src/3rdparty/kmessagewidget
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
@@ -540,3 +630,5 @@ if(NOT BUILD_OWNCLOUD_OSX_BUNDLE AND NOT WIN32)
|
||||
update_xdg_mimetypes( ${CMAKE_INSTALL_DATADIR}/mime/packages )
|
||||
endif(SharedMimeInfo_FOUND)
|
||||
endif()
|
||||
|
||||
configure_file(configgui.h.in ${CMAKE_CURRENT_BINARY_DIR}/configgui.h)
|
||||
|
||||
@@ -587,6 +587,7 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos)
|
||||
|
||||
ac = availabilityMenu->addAction(Utility::vfsPinActionText());
|
||||
connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::AlwaysLocal); });
|
||||
ac->setDisabled(Theme::instance()->enforceVirtualFilesSyncFolder());
|
||||
|
||||
ac = availabilityMenu->addAction(Utility::vfsFreeSpaceActionText());
|
||||
connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::OnlineOnly); });
|
||||
@@ -761,6 +762,7 @@ void AccountSettings::slotRemoveCurrentFolder()
|
||||
messageBox->addButton(tr("Cancel"), QMessageBox::NoRole);
|
||||
connect(messageBox, &QMessageBox::finished, this, [messageBox, yesButton, folder, row, this]{
|
||||
if (messageBox->clickedButton() == yesButton) {
|
||||
Utility::removeFavLink(folder->path());
|
||||
FolderMan::instance()->removeFolder(folder);
|
||||
_model->removeRow(row);
|
||||
|
||||
|
||||
@@ -221,6 +221,19 @@ void AccountState::setDesktopNotificationsAllowed(bool isAllowed)
|
||||
emit desktopNotificationsAllowedChanged();
|
||||
}
|
||||
|
||||
AccountState::ConnectionStatus AccountState::lastConnectionStatus() const
|
||||
{
|
||||
return _lastConnectionValidatorStatus;
|
||||
}
|
||||
|
||||
void AccountState::trySignIn()
|
||||
{
|
||||
if (isSignedOut() && account()) {
|
||||
account()->resetRejectedCertificates();
|
||||
signIn();
|
||||
}
|
||||
}
|
||||
|
||||
void AccountState::checkConnectivity()
|
||||
{
|
||||
if (isSignedOut() || _waitingForNewCredentials) {
|
||||
@@ -285,6 +298,8 @@ void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status sta
|
||||
return;
|
||||
}
|
||||
|
||||
_lastConnectionValidatorStatus = status;
|
||||
|
||||
// Come online gradually from 503 or maintenance mode
|
||||
if (status == ConnectionValidator::Connected
|
||||
&& (_connectionStatus == ConnectionValidator::ServiceUnavailable
|
||||
|
||||
@@ -171,6 +171,10 @@ public:
|
||||
*/
|
||||
void setDesktopNotificationsAllowed(bool isAllowed);
|
||||
|
||||
ConnectionStatus lastConnectionStatus() const;
|
||||
|
||||
void trySignIn();
|
||||
|
||||
public slots:
|
||||
/// Triggers a ping to the server to update state and
|
||||
/// connection status and errors.
|
||||
@@ -205,6 +209,7 @@ private:
|
||||
AccountPtr _account;
|
||||
State _state;
|
||||
ConnectionStatus _connectionStatus;
|
||||
ConnectionStatus _lastConnectionValidatorStatus = ConnectionStatus::Undefined;
|
||||
QStringList _connectionErrors;
|
||||
bool _waitingForNewCredentials;
|
||||
QDateTime _timeOfLastETagCheck;
|
||||
|
||||
@@ -465,6 +465,9 @@ void Application::slotCheckConnection()
|
||||
if (state != AccountState::SignedOut && state != AccountState::ConfigurationError
|
||||
&& state != AccountState::AskingCredentials && !pushNotificationsAvailable) {
|
||||
accountState->checkConnectivity();
|
||||
} else if (state == AccountState::SignedOut && accountState->lastConnectionStatus() == AccountState::ConnectionStatus::SslError) {
|
||||
qCWarning(lcApplication) << "Account is signed out due to SSL Handshake error. Going to perform a sign-in attempt...";
|
||||
accountState->trySignIn();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
4
src/gui/configgui.h.in
Normal file
4
src/gui/configgui.h.in
Normal file
@@ -0,0 +1,4 @@
|
||||
#ifndef CONFIG_GUI_H
|
||||
#define CONFIG_GUI_H
|
||||
#cmakedefine APPLICATION_FOLDER_ICON_INDEX "@APPLICATION_FOLDER_ICON_INDEX@"
|
||||
#endif
|
||||
@@ -136,7 +136,7 @@ void ConnectionValidator::slotStatusFound(const QUrl &url, const QJsonObject &in
|
||||
void ConnectionValidator::slotNoStatusFound(QNetworkReply *reply)
|
||||
{
|
||||
auto job = qobject_cast<CheckServerJob *>(sender());
|
||||
qCWarning(lcConnectionValidator) << reply->error() << job->errorString() << reply->peek(1024);
|
||||
qCWarning(lcConnectionValidator) << reply->error() << reply->errorString() << job->errorString() << reply->peek(1024);
|
||||
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
|
||||
reportResult(SslError);
|
||||
return;
|
||||
|
||||
@@ -132,6 +132,16 @@ void Flow2Auth::fetchNewToken(const TokenAction action)
|
||||
|
||||
|
||||
_loginUrl = loginUrl;
|
||||
|
||||
if (_account->isUsernamePrefillSupported()) {
|
||||
const auto userName = Utility::getCurrentUserName();
|
||||
if (!userName.isEmpty()) {
|
||||
auto query = QUrlQuery(_loginUrl);
|
||||
query.addQueryItem(QStringLiteral("user"), userName);
|
||||
_loginUrl.setQuery(query);
|
||||
}
|
||||
}
|
||||
|
||||
_pollToken = pollToken;
|
||||
_pollEndpoint = pollEndpoint;
|
||||
|
||||
|
||||
@@ -691,6 +691,15 @@ void Folder::setRootPinState(PinState state)
|
||||
void Folder::switchToVirtualFiles()
|
||||
{
|
||||
SyncEngine::switchToVirtualFiles(path(), _journal, *_vfs);
|
||||
_hasSwitchedToVfs = true;
|
||||
}
|
||||
|
||||
void Folder::processSwitchedToVirtualFiles()
|
||||
{
|
||||
if (_hasSwitchedToVfs) {
|
||||
_hasSwitchedToVfs = false;
|
||||
saveToSettings();
|
||||
}
|
||||
}
|
||||
|
||||
bool Folder::supportsSelectiveSync() const
|
||||
@@ -866,11 +875,27 @@ void Folder::startSync(const QStringList &pathList)
|
||||
|
||||
_engine->setIgnoreHiddenFiles(_definition.ignoreHiddenFiles);
|
||||
|
||||
correctPlaceholderFiles();
|
||||
|
||||
QMetaObject::invokeMethod(_engine.data(), "startSync", Qt::QueuedConnection);
|
||||
|
||||
emit syncStarted();
|
||||
}
|
||||
|
||||
void Folder::correctPlaceholderFiles()
|
||||
{
|
||||
if (_definition.virtualFilesMode == Vfs::Off) {
|
||||
return;
|
||||
}
|
||||
static const auto placeholdersCorrectedKey = QStringLiteral("placeholders_corrected");
|
||||
const auto placeholdersCorrected = _journal.keyValueStoreGetInt(placeholdersCorrectedKey, 0);
|
||||
if (!placeholdersCorrected) {
|
||||
qCDebug(lcFolder) << "Make sure all virtual files are placeholder files";
|
||||
switchToVirtualFiles();
|
||||
_journal.keyValueStoreSet(placeholdersCorrectedKey, true);
|
||||
}
|
||||
}
|
||||
|
||||
void Folder::setSyncOptions()
|
||||
{
|
||||
SyncOptions opt;
|
||||
|
||||
@@ -289,6 +289,8 @@ public:
|
||||
|
||||
void switchToVirtualFiles();
|
||||
|
||||
void processSwitchedToVirtualFiles();
|
||||
|
||||
/** Whether this folder should show selective sync ui */
|
||||
bool supportsSelectiveSync() const;
|
||||
|
||||
@@ -444,6 +446,8 @@ private:
|
||||
|
||||
void startVfs();
|
||||
|
||||
void correctPlaceholderFiles();
|
||||
|
||||
AccountStatePtr _accountState;
|
||||
FolderDefinition _definition;
|
||||
QString _canonicalLocalPath; // As returned with QFileInfo:canonicalFilePath. Always ends with "/"
|
||||
@@ -498,6 +502,10 @@ private:
|
||||
*/
|
||||
bool _vfsOnOffPending = false;
|
||||
|
||||
/** Whether this folder has just switched to VFS or not
|
||||
*/
|
||||
bool _hasSwitchedToVfs = false;
|
||||
|
||||
/**
|
||||
* Watches this folder's local directory for changes.
|
||||
*
|
||||
|
||||
@@ -211,6 +211,10 @@ int FolderMan::setupFolders()
|
||||
|
||||
emit folderListChanged(_folderMap);
|
||||
|
||||
for (const auto folder : _folderMap) {
|
||||
folder->processSwitchedToVirtualFiles();
|
||||
}
|
||||
|
||||
return _folderMap.size();
|
||||
}
|
||||
|
||||
|
||||
41
src/gui/passwordinputdialog.cpp
Normal file
41
src/gui/passwordinputdialog.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 "passwordinputdialog.h"
|
||||
#include "ui_passwordinputdialog.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
PasswordInputDialog::PasswordInputDialog(const QString &description, const QString &error, QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, _ui(new Ui::PasswordInputDialog)
|
||||
{
|
||||
_ui->setupUi(this);
|
||||
|
||||
_ui->passwordLineEditLabel->setText(description);
|
||||
_ui->passwordLineEditLabel->setVisible(!description.isEmpty());
|
||||
|
||||
_ui->labelErrorMessage->setText(error);
|
||||
_ui->labelErrorMessage->setVisible(!error.isEmpty());
|
||||
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
}
|
||||
|
||||
PasswordInputDialog::~PasswordInputDialog() = default;
|
||||
|
||||
QString PasswordInputDialog::password() const
|
||||
{
|
||||
return _ui->passwordLineEdit->text();
|
||||
}
|
||||
}
|
||||
39
src/gui/passwordinputdialog.h
Normal file
39
src/gui/passwordinputdialog.h
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 <QDialog>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
namespace Ui {
|
||||
class PasswordInputDialog;
|
||||
}
|
||||
|
||||
class PasswordInputDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PasswordInputDialog(const QString &description, const QString &error, QWidget *parent = nullptr);
|
||||
~PasswordInputDialog() override;
|
||||
|
||||
QString password() const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Ui::PasswordInputDialog> _ui;
|
||||
};
|
||||
|
||||
}
|
||||
115
src/gui/passwordinputdialog.ui
Normal file
115
src/gui/passwordinputdialog.ui
Normal file
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>OCC::PasswordInputDialog</class>
|
||||
<widget class="QDialog" name="OCC::PasswordInputDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::WindowModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>276</width>
|
||||
<height>125</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Password for share required</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="passwordLineEditLabel">
|
||||
<property name="text">
|
||||
<string>Please enter a password for your share:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="passwordLineEdit">
|
||||
<property name="inputMask">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="labelErrorMessage">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: rgb(118, 118, 118)</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>OCC::PasswordInputDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>OCC::PasswordInputDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "sharee.h"
|
||||
#include "sharelinkwidget.h"
|
||||
#include "shareusergroupwidget.h"
|
||||
#include "passwordinputdialog.h"
|
||||
|
||||
#include "sharemanager.h"
|
||||
|
||||
@@ -135,21 +136,7 @@ ShareDialog::ShareDialog(QPointer<AccountState> accountState,
|
||||
connect(job, &PropfindJob::finishedWithError, this, &ShareDialog::slotPropfindError);
|
||||
job->start();
|
||||
|
||||
bool sharingPossible = true;
|
||||
if (!accountState->account()->capabilities().sharePublicLink()) {
|
||||
qCWarning(lcSharing) << "Link shares have been disabled";
|
||||
sharingPossible = false;
|
||||
} else if (!(maxSharingPermissions & SharePermissionShare)) {
|
||||
qCWarning(lcSharing) << "The file cannot be shared because it does not have sharing permission.";
|
||||
sharingPossible = false;
|
||||
}
|
||||
|
||||
if (sharingPossible) {
|
||||
_manager = new ShareManager(accountState->account(), this);
|
||||
connect(_manager, &ShareManager::sharesFetched, this, &ShareDialog::slotSharesFetched);
|
||||
connect(_manager, &ShareManager::linkShareCreated, this, &ShareDialog::slotAddLinkShareWidget);
|
||||
connect(_manager, &ShareManager::linkShareRequiresPassword, this, &ShareDialog::slotLinkShareRequiresPassword);
|
||||
}
|
||||
initShareManager();
|
||||
}
|
||||
|
||||
ShareLinkWidget *ShareDialog::addLinkShareWidget(const QSharedPointer<LinkShare> &linkShare)
|
||||
@@ -318,6 +305,8 @@ void ShareDialog::showSharingUi()
|
||||
_userGroupWidget->getShares();
|
||||
}
|
||||
|
||||
initShareManager();
|
||||
|
||||
if (theme->linkSharing()) {
|
||||
if(_manager) {
|
||||
_manager->fetchShares(_sharePath);
|
||||
@@ -325,6 +314,25 @@ void ShareDialog::showSharingUi()
|
||||
}
|
||||
}
|
||||
|
||||
void ShareDialog::initShareManager()
|
||||
{
|
||||
bool sharingPossible = true;
|
||||
if (!_accountState->account()->capabilities().sharePublicLink()) {
|
||||
qCWarning(lcSharing) << "Link shares have been disabled";
|
||||
sharingPossible = false;
|
||||
} else if (!(_maxSharingPermissions & SharePermissionShare)) {
|
||||
qCWarning(lcSharing) << "The file cannot be shared because it does not have sharing permission.";
|
||||
sharingPossible = false;
|
||||
}
|
||||
|
||||
if (!_manager && sharingPossible) {
|
||||
_manager = new ShareManager(_accountState->account(), this);
|
||||
connect(_manager, &ShareManager::sharesFetched, this, &ShareDialog::slotSharesFetched);
|
||||
connect(_manager, &ShareManager::linkShareCreated, this, &ShareDialog::slotAddLinkShareWidget);
|
||||
connect(_manager, &ShareManager::linkShareRequiresPassword, this, &ShareDialog::slotLinkShareRequiresPassword);
|
||||
}
|
||||
}
|
||||
|
||||
void ShareDialog::slotCreateLinkShare()
|
||||
{
|
||||
if(_manager) {
|
||||
@@ -359,26 +367,21 @@ void ShareDialog::slotCreatePasswordForLinkShareProcessed()
|
||||
}
|
||||
}
|
||||
|
||||
void ShareDialog::slotLinkShareRequiresPassword()
|
||||
void ShareDialog::slotLinkShareRequiresPassword(const QString &message)
|
||||
{
|
||||
bool ok = false;
|
||||
QString password = QInputDialog::getText(this,
|
||||
tr("Password for share required"),
|
||||
tr("Please enter a password for your link share:"),
|
||||
QLineEdit::Password,
|
||||
QString(),
|
||||
&ok);
|
||||
const auto passwordInputDialog = new PasswordInputDialog(tr("Please enter a password for your link share:"), message, this);
|
||||
passwordInputDialog->setWindowTitle(tr("Password for share required"));
|
||||
passwordInputDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
passwordInputDialog->open();
|
||||
|
||||
if (!ok) {
|
||||
// The dialog was canceled so no need to do anything
|
||||
connect(passwordInputDialog, &QDialog::finished, this, [this, passwordInputDialog](const int result) {
|
||||
if (result == QDialog::Accepted && _manager) {
|
||||
// Try to create the link share again with the newly entered password
|
||||
_manager->createLinkShare(_sharePath, QString(), passwordInputDialog->password());
|
||||
return;
|
||||
}
|
||||
emit toggleShareLinkAnimation(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if(_manager) {
|
||||
// Try to create the link share again with the newly entered password
|
||||
_manager->createLinkShare(_sharePath, QString(), password);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ShareDialog::slotDeleteShare()
|
||||
|
||||
@@ -66,7 +66,7 @@ private slots:
|
||||
void slotCreateLinkShare();
|
||||
void slotCreatePasswordForLinkShare(const QString &password);
|
||||
void slotCreatePasswordForLinkShareProcessed();
|
||||
void slotLinkShareRequiresPassword();
|
||||
void slotLinkShareRequiresPassword(const QString &message);
|
||||
void slotAdjustScrollWidgetSize();
|
||||
|
||||
signals:
|
||||
@@ -78,6 +78,7 @@ protected:
|
||||
|
||||
private:
|
||||
void showSharingUi();
|
||||
void initShareManager();
|
||||
ShareLinkWidget *addLinkShareWidget(const QSharedPointer<LinkShare> &linkShare);
|
||||
void initLinkShareWidget();
|
||||
|
||||
|
||||
@@ -1225,9 +1225,11 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
|
||||
auto makePinContextMenu = [&](bool makeAvailableLocally, bool freeSpace) {
|
||||
listener->sendMessage(QLatin1String("MENU_ITEM:CURRENT_PIN:d:")
|
||||
+ Utility::vfsCurrentAvailabilityText(*combined));
|
||||
listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_AVAILABLE_LOCALLY:")
|
||||
+ (makeAvailableLocally ? QLatin1String(":") : QLatin1String("d:"))
|
||||
+ Utility::vfsPinActionText());
|
||||
if (!Theme::instance()->enforceVirtualFilesSyncFolder()) {
|
||||
listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_AVAILABLE_LOCALLY:")
|
||||
+ (makeAvailableLocally ? QLatin1String(":") : QLatin1String("d:")) + Utility::vfsPinActionText());
|
||||
}
|
||||
|
||||
listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_ONLINE_ONLY:")
|
||||
+ (freeSpace ? QLatin1String(":") : QLatin1String("d:"))
|
||||
+ Utility::vfsFreeSpaceActionText());
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
#include "common/utility.h"
|
||||
#include "tray/svgimageprovider.h"
|
||||
#include "tray/usermodel.h"
|
||||
#include "wheelhandler.h"
|
||||
#include "tray/unifiedsearchresultimageprovider.h"
|
||||
#include "configfile.h"
|
||||
#include "accessmanager.h"
|
||||
|
||||
#include <QCursor>
|
||||
#include <QGuiApplication>
|
||||
@@ -58,6 +60,8 @@ void Systray::setTrayEngine(QQmlApplicationEngine *trayEngine)
|
||||
{
|
||||
_trayEngine = trayEngine;
|
||||
|
||||
_trayEngine->setNetworkAccessManagerFactory(&_accessManagerFactory);
|
||||
|
||||
_trayEngine->addImportPath("qrc:/qml/theme");
|
||||
_trayEngine->addImageProvider("avatars", new ImageProvider);
|
||||
_trayEngine->addImageProvider(QLatin1String("svgimage-custom-color"), new OCC::Ui::SvgImageProvider);
|
||||
@@ -91,6 +95,8 @@ Systray::Systray()
|
||||
}
|
||||
);
|
||||
|
||||
qmlRegisterType<WheelHandler>("com.nextcloud.desktopclient", 1, 0, "WheelHandler");
|
||||
|
||||
#ifndef Q_OS_MAC
|
||||
auto contextMenu = new QMenu();
|
||||
if (AccountManager::instance()->accounts().isEmpty()) {
|
||||
@@ -502,4 +508,14 @@ QPoint Systray::calcTrayIconCenter() const
|
||||
#endif
|
||||
}
|
||||
|
||||
AccessManagerFactory::AccessManagerFactory()
|
||||
: QQmlNetworkAccessManagerFactory()
|
||||
{
|
||||
}
|
||||
|
||||
QNetworkAccessManager* AccessManagerFactory::create(QObject *parent)
|
||||
{
|
||||
return new AccessManager(parent);
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include "accountmanager.h"
|
||||
#include "tray/usermodel.h"
|
||||
|
||||
#include <QQmlNetworkAccessManagerFactory>
|
||||
|
||||
class QScreen;
|
||||
class QQmlApplicationEngine;
|
||||
class QQuickWindow;
|
||||
@@ -28,6 +30,14 @@ class QQuickWindow;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class AccessManagerFactory : public QQmlNetworkAccessManagerFactory
|
||||
{
|
||||
public:
|
||||
AccessManagerFactory();
|
||||
|
||||
QNetworkAccessManager* create(QObject *parent) override;
|
||||
};
|
||||
|
||||
#ifdef Q_OS_OSX
|
||||
bool canOsXSendUserNotification();
|
||||
void sendOsXUserNotification(const QString &title, const QString &message);
|
||||
@@ -105,6 +115,8 @@ private:
|
||||
bool _isOpen = false;
|
||||
bool _syncIsPaused = true;
|
||||
QPointer<QQmlApplicationEngine> _trayEngine;
|
||||
|
||||
AccessManagerFactory _accessManagerFactory;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
BIN
src/gui/tray/.wheelhandler.h.swo
Normal file
BIN
src/gui/tray/.wheelhandler.h.swo
Normal file
Binary file not shown.
@@ -20,6 +20,10 @@ MouseArea {
|
||||
anchors.fill: parent
|
||||
color: (parent.containsMouse ? Style.lightHover : "transparent")
|
||||
}
|
||||
|
||||
ToolTip.visible: containsMouse && displayLocation !== ""
|
||||
ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
|
||||
ToolTip.text: qsTr("In %1").arg(displayLocation)
|
||||
|
||||
RowLayout {
|
||||
id: activityItem
|
||||
@@ -152,7 +156,7 @@ MouseArea {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
flat: true
|
||||
hoverEnabled: true
|
||||
visible: displayActions && (path !== "")
|
||||
visible: isShareable
|
||||
display: AbstractButton.IconOnly
|
||||
icon.source: "qrc:///client/theme/share.svg"
|
||||
icon.color: "transparent"
|
||||
|
||||
@@ -6,15 +6,21 @@ import Style 1.0
|
||||
import com.nextcloud.desktopclient 1.0 as NC
|
||||
|
||||
ScrollView {
|
||||
id: controlRoot
|
||||
property alias model: activityList.model
|
||||
|
||||
signal showFileActivity(string displayPath, string absolutePath)
|
||||
signal activityItemClicked(int index)
|
||||
|
||||
contentWidth: availableWidth
|
||||
padding: 1
|
||||
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
data: NC.WheelHandler {
|
||||
target: controlRoot.contentItem
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: activityList
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Button {
|
||||
Layout.preferredHeight: Style.trayWindowHeaderHeight
|
||||
|
||||
background: Rectangle {
|
||||
color: root.hovered ? "white" : "transparent"
|
||||
color: root.hovered || root.visualFocus ? "white" : "transparent"
|
||||
opacity: 0.2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ MenuItem {
|
||||
|
||||
property variant dialog;
|
||||
property variant comp;
|
||||
activeFocusOnTab: false
|
||||
|
||||
signal showUserStatusSelectorDialog(int id)
|
||||
|
||||
@@ -35,29 +36,19 @@ MenuItem {
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: qsTr("Switch to account") + " " + name
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onContainsMouseChanged: {
|
||||
accountStatusIndicatorBackground.color = (containsMouse ? "#f6f6f6" : "white")
|
||||
}
|
||||
onClicked: {
|
||||
if (!isCurrentUser) {
|
||||
UserModel.switchCurrentUser(id)
|
||||
} else {
|
||||
accountMenu.close()
|
||||
}
|
||||
}
|
||||
onClicked: if (!isCurrentUser) {
|
||||
UserModel.switchCurrentUser(id)
|
||||
} else {
|
||||
accountMenu.close()
|
||||
}
|
||||
|
||||
|
||||
background: Item {
|
||||
height: parent.height
|
||||
width: userLine.menu ? userLine.menu.width : 0
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
color: parent.parent.hovered ? Style.lightHover : "transparent"
|
||||
color: parent.parent.hovered || parent.parent.visualFocus ? Style.lightHover : "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +72,7 @@ MenuItem {
|
||||
height: width
|
||||
anchors.bottom: accountAvatar.bottom
|
||||
anchors.right: accountAvatar.right
|
||||
color: "white"
|
||||
color: accountButton.hovered || accountButton.visualFocus ? "#f6f6f6" : "white"
|
||||
radius: width*0.5
|
||||
}
|
||||
Image {
|
||||
@@ -163,21 +154,16 @@ MenuItem {
|
||||
Accessible.name: qsTr("Account actions")
|
||||
Accessible.onPressAction: userMoreButtonMouseArea.clicked()
|
||||
|
||||
MouseArea {
|
||||
id: userMoreButtonMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
if (userMoreButtonMenu.visible) {
|
||||
userMoreButtonMenu.close()
|
||||
} else {
|
||||
userMoreButtonMenu.popup()
|
||||
}
|
||||
onClicked: {
|
||||
if (userMoreButtonMenu.visible) {
|
||||
userMoreButtonMenu.close()
|
||||
} else {
|
||||
userMoreButtonMenu.popup()
|
||||
}
|
||||
}
|
||||
background:
|
||||
Rectangle {
|
||||
color: userMoreButtonMouseArea.containsMouse ? "grey" : "transparent"
|
||||
color: userMoreButton.hovered || userMoreButton.visualFocus ? "grey" : "transparent"
|
||||
opacity: 0.2
|
||||
height: userMoreButton.height - 2
|
||||
y: userMoreButton.y + 1
|
||||
@@ -196,7 +182,6 @@ MenuItem {
|
||||
MenuItem {
|
||||
visible: model.isConnected && model.serverHasUserStatus
|
||||
height: visible ? implicitHeight : 0
|
||||
|
||||
text: qsTr("Set status")
|
||||
font.pixelSize: Style.topLinePixelSize
|
||||
hoverEnabled: true
|
||||
|
||||
@@ -142,187 +142,180 @@ Window {
|
||||
Accessible.name: qsTr("Current account")
|
||||
Accessible.onPressAction: currentAccountButton.clicked()
|
||||
|
||||
MouseArea {
|
||||
id: accountBtnMouseArea
|
||||
// We call open() instead of popup() because we want to position it
|
||||
// exactly below the dropdown button, not the mouse
|
||||
onClicked: {
|
||||
syncPauseButton.text = Systray.syncIsPaused() ? qsTr("Resume sync for all") : qsTr("Pause sync for all")
|
||||
if (accountMenu.visible) {
|
||||
accountMenu.close()
|
||||
} else {
|
||||
accountMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: Style.hoverEffectsEnabled
|
||||
Loader {
|
||||
id: userStatusSelectorDialogLoader
|
||||
}
|
||||
|
||||
// We call open() instead of popup() because we want to position it
|
||||
// exactly below the dropdown button, not the mouse
|
||||
onClicked: {
|
||||
syncPauseButton.text = Systray.syncIsPaused() ? qsTr("Resume sync for all") : qsTr("Pause sync for all")
|
||||
if (accountMenu.visible) {
|
||||
accountMenu.close()
|
||||
} else {
|
||||
accountMenu.open()
|
||||
}
|
||||
Menu {
|
||||
id: accountMenu
|
||||
|
||||
// x coordinate grows towards the right
|
||||
// y coordinate grows towards the bottom
|
||||
x: (currentAccountButton.x + 2)
|
||||
y: (currentAccountButton.y + Style.trayWindowHeaderHeight + 2)
|
||||
|
||||
width: (Style.currentAccountButtonWidth - 2)
|
||||
height: Math.min(implicitHeight, maxMenuHeight)
|
||||
closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape
|
||||
|
||||
background: Rectangle {
|
||||
border.color: Style.menuBorder
|
||||
radius: Style.currentAccountButtonRadius
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: userStatusSelectorDialogLoader
|
||||
onClosed: {
|
||||
// HACK: reload account Instantiator immediately by restting it - could be done better I guess
|
||||
// see also onVisibleChanged above
|
||||
userLineInstantiator.active = false;
|
||||
userLineInstantiator.active = true;
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: accountMenu
|
||||
|
||||
// x coordinate grows towards the right
|
||||
// y coordinate grows towards the bottom
|
||||
x: (currentAccountButton.x + 2)
|
||||
y: (currentAccountButton.y + Style.trayWindowHeaderHeight + 2)
|
||||
|
||||
width: (Style.currentAccountButtonWidth - 2)
|
||||
height: Math.min(implicitHeight, maxMenuHeight)
|
||||
closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape
|
||||
|
||||
background: Rectangle {
|
||||
border.color: Style.menuBorder
|
||||
radius: Style.currentAccountButtonRadius
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
// HACK: reload account Instantiator immediately by restting it - could be done better I guess
|
||||
// see also onVisibleChanged above
|
||||
userLineInstantiator.active = false;
|
||||
userLineInstantiator.active = true;
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
id: userLineInstantiator
|
||||
model: UserModel
|
||||
delegate: UserLine {
|
||||
onShowUserStatusSelectorDialog: {
|
||||
userStatusSelectorDialogLoader.source = "qrc:/qml/src/gui/UserStatusSelectorDialog.qml"
|
||||
userStatusSelectorDialogLoader.item.title = qsTr("Set user status")
|
||||
userStatusSelectorDialogLoader.item.model.load(index)
|
||||
userStatusSelectorDialogLoader.item.show()
|
||||
}
|
||||
Instantiator {
|
||||
id: userLineInstantiator
|
||||
model: UserModel
|
||||
delegate: UserLine {
|
||||
onShowUserStatusSelectorDialog: {
|
||||
userStatusSelectorDialogLoader.source = "qrc:/qml/src/gui/UserStatusSelectorDialog.qml"
|
||||
userStatusSelectorDialogLoader.item.title = qsTr("Set user status")
|
||||
userStatusSelectorDialogLoader.item.model.load(index)
|
||||
userStatusSelectorDialogLoader.item.show()
|
||||
}
|
||||
onObjectAdded: accountMenu.insertItem(index, object)
|
||||
onObjectRemoved: accountMenu.removeItem(object)
|
||||
}
|
||||
onObjectAdded: accountMenu.insertItem(index, object)
|
||||
onObjectRemoved: accountMenu.removeItem(object)
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
id: addAccountButton
|
||||
height: Style.addAccountButtonHeight
|
||||
hoverEnabled: true
|
||||
MenuItem {
|
||||
id: addAccountButton
|
||||
height: Style.addAccountButtonHeight
|
||||
hoverEnabled: true
|
||||
|
||||
background: Item {
|
||||
height: parent.height
|
||||
width: parent.menu.width
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
color: parent.parent.hovered ? Style.lightHover : "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
background: Item {
|
||||
height: parent.height
|
||||
width: parent.menu.width
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Image {
|
||||
Layout.leftMargin: 12
|
||||
verticalAlignment: Qt.AlignCenter
|
||||
source: "qrc:///client/theme/black/add.svg"
|
||||
sourceSize.width: Style.headerButtonIconSize
|
||||
sourceSize.height: Style.headerButtonIconSize
|
||||
}
|
||||
Label {
|
||||
Layout.leftMargin: 14
|
||||
text: qsTr("Add account")
|
||||
color: "black"
|
||||
font.pixelSize: Style.topLinePixelSize
|
||||
}
|
||||
// Filler on the right
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
onClicked: UserModel.addAccount()
|
||||
|
||||
Accessible.role: Accessible.MenuItem
|
||||
Accessible.name: qsTr("Add new account")
|
||||
Accessible.onPressAction: addAccountButton.clicked()
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
contentItem: Rectangle {
|
||||
implicitHeight: 1
|
||||
color: Style.menuBorder
|
||||
anchors.margins: 1
|
||||
color: parent.parent.hovered || parent.parent.visualFocus ? Style.lightHover : "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
id: syncPauseButton
|
||||
font.pixelSize: Style.topLinePixelSize
|
||||
hoverEnabled: true
|
||||
onClicked: Systray.pauseResumeSync()
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
background: Item {
|
||||
height: parent.height
|
||||
width: parent.menu.width
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
color: parent.parent.hovered ? Style.lightHover : "transparent"
|
||||
}
|
||||
Image {
|
||||
Layout.leftMargin: 12
|
||||
verticalAlignment: Qt.AlignCenter
|
||||
source: "qrc:///client/theme/black/add.svg"
|
||||
sourceSize.width: Style.headerButtonIconSize
|
||||
sourceSize.height: Style.headerButtonIconSize
|
||||
}
|
||||
Label {
|
||||
Layout.leftMargin: 14
|
||||
text: qsTr("Add account")
|
||||
color: "black"
|
||||
font.pixelSize: Style.topLinePixelSize
|
||||
}
|
||||
// Filler on the right
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
onClicked: UserModel.addAccount()
|
||||
|
||||
Accessible.role: Accessible.MenuItem
|
||||
Accessible.name: Systray.syncIsPaused() ? qsTr("Resume sync for all") : qsTr("Pause sync for all")
|
||||
Accessible.onPressAction: syncPauseButton.clicked()
|
||||
Accessible.role: Accessible.MenuItem
|
||||
Accessible.name: qsTr("Add new account")
|
||||
Accessible.onPressAction: addAccountButton.clicked()
|
||||
}
|
||||
|
||||
MenuSeparator {
|
||||
contentItem: Rectangle {
|
||||
implicitHeight: 1
|
||||
color: Style.menuBorder
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
id: syncPauseButton
|
||||
font.pixelSize: Style.topLinePixelSize
|
||||
hoverEnabled: true
|
||||
onClicked: Systray.pauseResumeSync()
|
||||
|
||||
background: Item {
|
||||
height: parent.height
|
||||
width: parent.menu.width
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
color: parent.parent.hovered || parent.parent.visualFocus ? Style.lightHover : "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
id: settingsButton
|
||||
text: qsTr("Settings")
|
||||
font.pixelSize: Style.topLinePixelSize
|
||||
hoverEnabled: true
|
||||
onClicked: Systray.openSettings()
|
||||
Accessible.role: Accessible.MenuItem
|
||||
Accessible.name: Systray.syncIsPaused() ? qsTr("Resume sync for all") : qsTr("Pause sync for all")
|
||||
Accessible.onPressAction: syncPauseButton.clicked()
|
||||
}
|
||||
|
||||
background: Item {
|
||||
height: parent.height
|
||||
width: parent.menu.width
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
color: parent.parent.hovered ? Style.lightHover : "transparent"
|
||||
}
|
||||
MenuItem {
|
||||
id: settingsButton
|
||||
text: qsTr("Settings")
|
||||
font.pixelSize: Style.topLinePixelSize
|
||||
hoverEnabled: true
|
||||
onClicked: Systray.openSettings()
|
||||
|
||||
background: Item {
|
||||
height: parent.height
|
||||
width: parent.menu.width
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
color: parent.parent.hovered || parent.parent.visualFocus ? Style.lightHover : "transparent"
|
||||
}
|
||||
|
||||
Accessible.role: Accessible.MenuItem
|
||||
Accessible.name: text
|
||||
Accessible.onPressAction: settingsButton.clicked()
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
id: exitButton
|
||||
text: qsTr("Exit");
|
||||
font.pixelSize: Style.topLinePixelSize
|
||||
hoverEnabled: true
|
||||
onClicked: Systray.shutdown()
|
||||
Accessible.role: Accessible.MenuItem
|
||||
Accessible.name: text
|
||||
Accessible.onPressAction: settingsButton.clicked()
|
||||
}
|
||||
|
||||
background: Item {
|
||||
height: parent.height
|
||||
width: parent.menu.width
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
color: parent.parent.hovered ? Style.lightHover : "transparent"
|
||||
}
|
||||
MenuItem {
|
||||
id: exitButton
|
||||
text: qsTr("Exit");
|
||||
font.pixelSize: Style.topLinePixelSize
|
||||
hoverEnabled: true
|
||||
onClicked: Systray.shutdown()
|
||||
|
||||
background: Item {
|
||||
height: parent.height
|
||||
width: parent.menu.width
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
color: parent.parent.hovered || parent.parent.visualFocus ? Style.lightHover : "transparent"
|
||||
}
|
||||
|
||||
Accessible.role: Accessible.MenuItem
|
||||
Accessible.name: text
|
||||
Accessible.onPressAction: exitButton.clicked()
|
||||
}
|
||||
|
||||
Accessible.role: Accessible.MenuItem
|
||||
Accessible.name: text
|
||||
Accessible.onPressAction: exitButton.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: accountBtnMouseArea.containsMouse ? "white" : "transparent"
|
||||
color: parent.hovered || parent.visualFocus ? "white" : "transparent"
|
||||
opacity: 0.2
|
||||
}
|
||||
|
||||
@@ -658,51 +651,61 @@ Window {
|
||||
iconColor: "#afafaf"
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: unifiedSearchResultsListView
|
||||
ScrollView {
|
||||
id: controlRoot
|
||||
padding: 1
|
||||
contentWidth: availableWidth
|
||||
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
data: WheelHandler {
|
||||
target: controlRoot.contentItem
|
||||
}
|
||||
visible: unifiedSearchResultsListView.count > 0
|
||||
|
||||
anchors.top: trayWindowUnifiedSearchInputContainer.bottom
|
||||
anchors.left: trayWindowBackground.left
|
||||
anchors.right: trayWindowBackground.right
|
||||
anchors.bottom: trayWindowBackground.bottom
|
||||
spacing: 4
|
||||
visible: count > 0
|
||||
clip: true
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
id: unifiedSearchResultsListViewScrollbar
|
||||
}
|
||||
|
||||
keyNavigationEnabled: true
|
||||
ListView {
|
||||
id: unifiedSearchResultsListView
|
||||
spacing: 4
|
||||
clip: true
|
||||
|
||||
reuseItems: true
|
||||
keyNavigationEnabled: true
|
||||
|
||||
Accessible.role: Accessible.List
|
||||
Accessible.name: qsTr("Unified search results list")
|
||||
reuseItems: true
|
||||
|
||||
model: UserModel.currentUser.unifiedSearchResultsListModel
|
||||
Accessible.role: Accessible.List
|
||||
Accessible.name: qsTr("Unified search results list")
|
||||
|
||||
delegate: UnifiedSearchResultListItem {
|
||||
width: unifiedSearchResultsListView.width
|
||||
height: trayWindowBackground.Style.unifiedSearchItemHeight
|
||||
isSearchInProgress: unifiedSearchResultsListView.model.isSearchInProgress
|
||||
textLeftMargin: trayWindowBackground.Style.unifiedSearchResultTextLeftMargin
|
||||
textRightMargin: trayWindowBackground.Style.unifiedSearchResultTextRightMargin
|
||||
iconWidth: trayWindowBackground.Style.unifiedSearchResulIconWidth
|
||||
iconLeftMargin: trayWindowBackground.Style.unifiedSearchResulIconLeftMargin
|
||||
titleFontSize: trayWindowBackground.Style.unifiedSearchResulTitleFontSize
|
||||
sublineFontSize: trayWindowBackground.Style.unifiedSearchResulSublineFontSize
|
||||
titleColor: trayWindowBackground.Style.unifiedSearchResulTitleColor
|
||||
sublineColor: trayWindowBackground.Style.unifiedSearchResulSublineColor
|
||||
currentFetchMoreInProgressProviderId: unifiedSearchResultsListView.model.currentFetchMoreInProgressProviderId
|
||||
fetchMoreTriggerClicked: unifiedSearchResultsListView.model.fetchMoreTriggerClicked
|
||||
resultClicked: unifiedSearchResultsListView.model.resultClicked
|
||||
ListView.onPooled: isPooled = true
|
||||
ListView.onReused: isPooled = false
|
||||
}
|
||||
model: UserModel.currentUser.unifiedSearchResultsListModel
|
||||
|
||||
section.property: "providerName"
|
||||
section.criteria: ViewSection.FullString
|
||||
section.delegate: UnifiedSearchResultSectionItem {
|
||||
width: unifiedSearchResultsListView.width
|
||||
delegate: UnifiedSearchResultListItem {
|
||||
width: unifiedSearchResultsListView.width
|
||||
height: trayWindowBackground.Style.unifiedSearchItemHeight
|
||||
isSearchInProgress: unifiedSearchResultsListView.model.isSearchInProgress
|
||||
textLeftMargin: trayWindowBackground.Style.unifiedSearchResultTextLeftMargin
|
||||
textRightMargin: trayWindowBackground.Style.unifiedSearchResultTextRightMargin
|
||||
iconWidth: trayWindowBackground.Style.unifiedSearchResulIconWidth
|
||||
iconLeftMargin: trayWindowBackground.Style.unifiedSearchResulIconLeftMargin
|
||||
titleFontSize: trayWindowBackground.Style.unifiedSearchResulTitleFontSize
|
||||
sublineFontSize: trayWindowBackground.Style.unifiedSearchResulSublineFontSize
|
||||
titleColor: trayWindowBackground.Style.unifiedSearchResulTitleColor
|
||||
sublineColor: trayWindowBackground.Style.unifiedSearchResulSublineColor
|
||||
currentFetchMoreInProgressProviderId: unifiedSearchResultsListView.model.currentFetchMoreInProgressProviderId
|
||||
fetchMoreTriggerClicked: unifiedSearchResultsListView.model.fetchMoreTriggerClicked
|
||||
resultClicked: unifiedSearchResultsListView.model.resultClicked
|
||||
ListView.onPooled: isPooled = true
|
||||
ListView.onReused: isPooled = false
|
||||
}
|
||||
|
||||
section.property: "providerName"
|
||||
section.criteria: ViewSection.FullString
|
||||
section.delegate: UnifiedSearchResultSectionItem {
|
||||
width: unifiedSearchResultsListView.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -723,6 +726,7 @@ Window {
|
||||
anchors.right: trayWindowBackground.right
|
||||
anchors.bottom: trayWindowBackground.bottom
|
||||
|
||||
activeFocusOnTab: true
|
||||
model: activityModel
|
||||
onShowFileActivity: {
|
||||
openFileActivityDialog(displayPath, absolutePath)
|
||||
|
||||
@@ -61,11 +61,22 @@ public:
|
||||
SyncFileItemType
|
||||
};
|
||||
|
||||
struct RichSubjectParameter {
|
||||
QString type; // Required
|
||||
QString id; // Required
|
||||
QString name; // Required
|
||||
QString path; // Required (for files only)
|
||||
QUrl link; // Optional (files only)
|
||||
};
|
||||
|
||||
Type _type;
|
||||
qlonglong _id;
|
||||
QString _fileAction;
|
||||
QString _objectType;
|
||||
QString _subject;
|
||||
QString _subjectRich;
|
||||
QHash<QString, RichSubjectParameter> _subjectRichParameters;
|
||||
QString _subjectDisplay;
|
||||
QString _message;
|
||||
QString _folder;
|
||||
QString _file;
|
||||
|
||||
@@ -58,6 +58,7 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
|
||||
roles[DisplayPathRole] = "displayPath";
|
||||
roles[PathRole] = "path";
|
||||
roles[AbsolutePathRole] = "absolutePath";
|
||||
roles[DisplayLocationRole] = "displayLocation";
|
||||
roles[LinkRole] = "link";
|
||||
roles[MessageRole] = "message";
|
||||
roles[ActionRole] = "type";
|
||||
@@ -68,6 +69,7 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
|
||||
roles[ObjectTypeRole] = "objectType";
|
||||
roles[PointInTimeRole] = "dateTime";
|
||||
roles[DisplayActions] = "displayActions";
|
||||
roles[ShareableRole] = "isShareable";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -113,15 +115,40 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
|
||||
if (!ast && _accountState != ast.data())
|
||||
return QVariant();
|
||||
|
||||
switch (role) {
|
||||
case DisplayPathRole:
|
||||
const auto getFilePath = [&]() {
|
||||
if (!a._file.isEmpty()) {
|
||||
auto folder = FolderMan::instance()->folder(a._folder);
|
||||
QString relPath(a._file);
|
||||
if (folder) {
|
||||
relPath.prepend(folder->remotePath());
|
||||
}
|
||||
const auto folder = FolderMan::instance()->folder(a._folder);
|
||||
|
||||
const QString relPath = folder ? folder->remotePath() + a._file : a._file;
|
||||
|
||||
const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
|
||||
|
||||
if (localFiles.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
// If this is an E2EE file or folder, pretend we got no path, hiding the share button which is what we want
|
||||
if (folder) {
|
||||
SyncJournalFileRecord rec;
|
||||
folder->journalDb()->getFileRecord(a._file.mid(1), &rec);
|
||||
if (rec.isValid() && (rec._isE2eEncrypted || !rec._e2eMangledName.isEmpty())) {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
return localFiles.constFirst();
|
||||
}
|
||||
return QString();
|
||||
};
|
||||
|
||||
const auto getDisplayPath = [&a, &ast]() {
|
||||
if (!a._file.isEmpty()) {
|
||||
const auto folder = FolderMan::instance()->folder(a._folder);
|
||||
|
||||
QString relPath = folder ? folder->remotePath() + a._file : a._file;
|
||||
|
||||
const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
|
||||
|
||||
if (localFiles.count() > 0) {
|
||||
if (relPath.startsWith('/') || relPath.startsWith('\\')) {
|
||||
return relPath.remove(0, 1);
|
||||
@@ -131,54 +158,22 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
};
|
||||
|
||||
const auto displayLocation = [&]() {
|
||||
const auto displayPath = QFileInfo(getDisplayPath()).path();
|
||||
return displayPath == "." || displayPath == "/" ? QString() : displayPath;
|
||||
};
|
||||
|
||||
switch (role) {
|
||||
case DisplayPathRole:
|
||||
return getDisplayPath();
|
||||
case PathRole:
|
||||
if (!a._file.isEmpty()) {
|
||||
const auto folder = FolderMan::instance()->folder(a._folder);
|
||||
|
||||
QString relPath(a._file);
|
||||
if (folder) {
|
||||
relPath.prepend(folder->remotePath());
|
||||
}
|
||||
|
||||
// get relative path to the file so we can open it in the file manager
|
||||
const auto localFiles = FolderMan::instance()->findFileInLocalFolders(QFileInfo(relPath).path(), ast->account());
|
||||
|
||||
if (localFiles.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
// If this is an E2EE file or folder, pretend we got no path, this leads to
|
||||
// hiding the share button which is what we want
|
||||
if (folder) {
|
||||
SyncJournalFileRecord rec;
|
||||
folder->journalDb()->getFileRecord(a._file.mid(1), &rec);
|
||||
if (rec.isValid() && (rec._isE2eEncrypted || !rec._e2eMangledName.isEmpty())) {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
return QUrl::fromLocalFile(localFiles.constFirst());
|
||||
}
|
||||
return QString();
|
||||
case AbsolutePathRole: {
|
||||
const auto folder = FolderMan::instance()->folder(a._folder);
|
||||
QString relPath(a._file);
|
||||
if (!a._file.isEmpty()) {
|
||||
if (folder) {
|
||||
relPath.prepend(folder->remotePath());
|
||||
}
|
||||
const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
|
||||
if (!localFiles.empty()) {
|
||||
return localFiles.constFirst();
|
||||
} else {
|
||||
qWarning("File not local folders while processing absolute path request.");
|
||||
return QString();
|
||||
}
|
||||
} else {
|
||||
qWarning("Received an absolute path request for an activity without a file path.");
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
return QUrl::fromLocalFile(QFileInfo(getFilePath()).path());
|
||||
case AbsolutePathRole:
|
||||
return getFilePath();
|
||||
case DisplayLocationRole:
|
||||
return displayLocation();
|
||||
case ActionsLinksRole: {
|
||||
QList<QVariant> customList;
|
||||
foreach (ActivityLink activityLink, a._links) {
|
||||
@@ -241,7 +236,11 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
}
|
||||
case ActionTextRole:
|
||||
return a._subject;
|
||||
if(a._subjectDisplay.isEmpty()) {
|
||||
return a._subject;
|
||||
}
|
||||
|
||||
return a._subjectDisplay;
|
||||
case ActionTextColorRole:
|
||||
return a._id == -1 ? QLatin1String("#808080") : QLatin1String("#222"); // FIXME: This is a temporary workaround for _showMoreActivitiesAvailableEntry
|
||||
case MessageRole:
|
||||
@@ -262,6 +261,8 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
|
||||
return (ast && ast->isConnected());
|
||||
case DisplayActions:
|
||||
return _displayActions;
|
||||
case ShareableRole:
|
||||
return !data(index, PathRole).toString().isEmpty() && _displayActions && a._fileAction != "file_deleted" && a._status != SyncFileItem::FileIgnored;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@@ -329,16 +330,53 @@ void ActivityListModel::activitiesReceived(const QJsonDocument &json, int status
|
||||
|
||||
Activity a;
|
||||
a._type = Activity::ActivityType;
|
||||
a._objectType = json.value("object_type").toString();
|
||||
a._objectType = json.value(QStringLiteral("object_type")).toString();
|
||||
a._accName = ast->account()->displayName();
|
||||
a._id = json.value("activity_id").toInt();
|
||||
a._fileAction = json.value("type").toString();
|
||||
a._subject = json.value("subject").toString();
|
||||
a._message = json.value("message").toString();
|
||||
a._file = json.value("object_name").toString();
|
||||
a._link = QUrl(json.value("link").toString());
|
||||
a._dateTime = QDateTime::fromString(json.value("datetime").toString(), Qt::ISODate);
|
||||
a._icon = json.value("icon").toString();
|
||||
a._id = json.value(QStringLiteral("activity_id")).toInt();
|
||||
a._fileAction = json.value(QStringLiteral("type")).toString();
|
||||
a._subject = json.value(QStringLiteral("subject")).toString();
|
||||
a._message = json.value(QStringLiteral("message")).toString();
|
||||
a._file = json.value(QStringLiteral("object_name")).toString();
|
||||
a._link = QUrl(json.value(QStringLiteral("link")).toString());
|
||||
a._dateTime = QDateTime::fromString(json.value(QStringLiteral("datetime")).toString(), Qt::ISODate);
|
||||
a._icon = json.value(QStringLiteral("icon")).toString();
|
||||
|
||||
auto richSubjectData = json.value(QStringLiteral("subject_rich")).toArray();
|
||||
Q_ASSERT(richSubjectData.size() > 1);
|
||||
|
||||
if(richSubjectData.size() > 1) {
|
||||
a._subjectRich = richSubjectData[0].toString();
|
||||
auto parameters = richSubjectData[1].toObject();
|
||||
const QRegularExpression subjectRichParameterRe(QStringLiteral("({[a-zA-Z0-9]*})"));
|
||||
const QRegularExpression subjectRichParameterBracesRe(QStringLiteral("[{}]"));
|
||||
|
||||
for (auto i = parameters.begin(); i != parameters.end(); ++i) {
|
||||
const auto parameterJsonObject = i.value().toObject();
|
||||
const Activity::RichSubjectParameter parameter = {
|
||||
parameterJsonObject.value(QStringLiteral("type")).toString(),
|
||||
parameterJsonObject.value(QStringLiteral("id")).toString(),
|
||||
parameterJsonObject.value(QStringLiteral("name")).toString(),
|
||||
parameterJsonObject.contains(QStringLiteral("path")) ? parameterJsonObject.value(QStringLiteral("path")).toString() : QString(),
|
||||
parameterJsonObject.contains(QStringLiteral("link")) ? QUrl(parameterJsonObject.value(QStringLiteral("link")).toString()) : QUrl(),
|
||||
};
|
||||
|
||||
a._subjectRichParameters[i.key()] = parameter;
|
||||
}
|
||||
|
||||
auto displayString = a._subjectRich;
|
||||
auto i = subjectRichParameterRe.globalMatch(displayString);
|
||||
|
||||
while (i.hasNext()) {
|
||||
const auto match = i.next();
|
||||
auto word = match.captured(1);
|
||||
word.remove(subjectRichParameterBracesRe);
|
||||
|
||||
Q_ASSERT(a._subjectRichParameters.contains(word));
|
||||
displayString = displayString.replace(match.captured(1), a._subjectRichParameters[word].name);
|
||||
}
|
||||
|
||||
a._subjectDisplay = displayString;
|
||||
}
|
||||
|
||||
list.append(a);
|
||||
_currentItem = list.last()._id;
|
||||
|
||||
@@ -55,11 +55,13 @@ public:
|
||||
DisplayPathRole,
|
||||
PathRole,
|
||||
AbsolutePathRole,
|
||||
DisplayLocationRole, // Provides the display path to a file's parent folder, relative to Nextcloud root
|
||||
LinkRole,
|
||||
PointInTimeRole,
|
||||
AccountConnectedRole,
|
||||
SyncFileStatusRole,
|
||||
DisplayActions,
|
||||
ShareableRole,
|
||||
};
|
||||
Q_ENUM(DataRole)
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ const QString notificationsPath = QLatin1String("ocs/v2.php/apps/notifications/a
|
||||
const char propertyAccountStateC[] = "oc_account_state";
|
||||
const int successStatusCode = 200;
|
||||
const int notModifiedStatusCode = 304;
|
||||
QMap<int, QByteArray> ServerNotificationHandler::iconCache;
|
||||
|
||||
ServerNotificationHandler::ServerNotificationHandler(AccountState *accountState, QObject *parent)
|
||||
: QObject(parent)
|
||||
@@ -72,11 +71,6 @@ void ServerNotificationHandler::slotAllowDesktopNotificationsChanged(bool isAllo
|
||||
}
|
||||
}
|
||||
|
||||
void ServerNotificationHandler::slotIconDownloaded(QByteArray iconData)
|
||||
{
|
||||
iconCache.insert(sender()->property("activityId").toInt(),iconData);
|
||||
}
|
||||
|
||||
void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &json, int statusCode)
|
||||
{
|
||||
if (statusCode != successStatusCode && statusCode != notModifiedStatusCode) {
|
||||
@@ -112,12 +106,6 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
|
||||
a._message = json.value("message").toString();
|
||||
a._icon = json.value("icon").toString();
|
||||
|
||||
if (!a._icon.isEmpty()) {
|
||||
auto *iconJob = new IconJob(_accountState->account(), QUrl(a._icon));
|
||||
iconJob->setProperty("activityId", a._id);
|
||||
connect(iconJob, &IconJob::jobFinished, this, &ServerNotificationHandler::slotIconDownloaded);
|
||||
}
|
||||
|
||||
QUrl link(json.value("link").toString());
|
||||
if (!link.isEmpty()) {
|
||||
if (link.host().isEmpty()) {
|
||||
|
||||
@@ -14,7 +14,6 @@ class ServerNotificationHandler : public QObject
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ServerNotificationHandler(AccountState *accountState, QObject *parent = nullptr);
|
||||
static QMap<int, QByteArray> iconCache;
|
||||
|
||||
signals:
|
||||
void newNotificationList(ActivityList);
|
||||
@@ -25,7 +24,6 @@ public slots:
|
||||
private slots:
|
||||
void slotNotificationsReceived(const QJsonDocument &json, int statusCode);
|
||||
void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode);
|
||||
void slotIconDownloaded(QByteArray iconData);
|
||||
void slotAllowDesktopNotificationsChanged(bool isAllowed);
|
||||
|
||||
private:
|
||||
|
||||
@@ -322,7 +322,8 @@ void UnifiedSearchResultsListModel::resultClicked(const QString &providerId, con
|
||||
FolderMan::instance()->findFileInLocalFolders(QFileInfo(relativePath).path(), _accountState->account());
|
||||
|
||||
if (!localFiles.isEmpty()) {
|
||||
QDesktopServices::openUrl(localFiles.constFirst());
|
||||
qCInfo(lcUnifiedSearch) << "Opening file:" << localFiles.constFirst();
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(localFiles.constFirst()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,6 +506,8 @@ void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr
|
||||
activity._folder = folder->alias();
|
||||
activity._fileAction = "";
|
||||
|
||||
const auto fileName = QFileInfo(item->_originalFile).fileName();
|
||||
|
||||
if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) {
|
||||
activity._fileAction = "file_deleted";
|
||||
} else if (item->_instruction == CSYNC_INSTRUCTION_NEW) {
|
||||
@@ -520,15 +522,15 @@ void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr
|
||||
qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully.";
|
||||
|
||||
if (item->_direction != SyncFileItem::Up) {
|
||||
activity._message = tr("Synced %1").arg(item->_originalFile);
|
||||
activity._message = tr("Synced %1").arg(fileName);
|
||||
} else if (activity._fileAction == "file_renamed") {
|
||||
activity._message = tr("You renamed %1").arg(item->_originalFile);
|
||||
activity._message = tr("You renamed %1").arg(fileName);
|
||||
} else if (activity._fileAction == "file_deleted") {
|
||||
activity._message = tr("You deleted %1").arg(item->_originalFile);
|
||||
activity._message = tr("You deleted %1").arg(fileName);
|
||||
} else if (activity._fileAction == "file_created") {
|
||||
activity._message = tr("You created %1").arg(item->_originalFile);
|
||||
activity._message = tr("You created %1").arg(fileName);
|
||||
} else {
|
||||
activity._message = tr("You changed %1").arg(item->_originalFile);
|
||||
activity._message = tr("You changed %1").arg(fileName);
|
||||
}
|
||||
|
||||
_activityModel->addSyncFileItemToActivityList(activity);
|
||||
@@ -658,7 +660,7 @@ QString User::statusEmoji() const
|
||||
|
||||
bool User::serverHasUserStatus() const
|
||||
{
|
||||
return _account->account()->capabilities().userStatusNotification();
|
||||
return _account->account()->capabilities().userStatus();
|
||||
}
|
||||
|
||||
QImage User::avatar() const
|
||||
|
||||
@@ -62,6 +62,7 @@ OwncloudAdvancedSetupPage::OwncloudAdvancedSetupPage(OwncloudWizard *wizard)
|
||||
if (Theme::instance()->enforceVirtualFilesSyncFolder()) {
|
||||
_ui.rSyncEverything->setDisabled(true);
|
||||
_ui.rSelectiveSync->setDisabled(true);
|
||||
_ui.bSelectiveSync->setDisabled(true);
|
||||
}
|
||||
|
||||
connect(_ui.rSyncEverything, &QAbstractButton::clicked, this, &OwncloudAdvancedSetupPage::slotSyncEverythingClicked);
|
||||
|
||||
@@ -40,6 +40,8 @@ set(libsync_SRCS
|
||||
propagateupload.cpp
|
||||
propagateuploadv1.cpp
|
||||
propagateuploadng.cpp
|
||||
bulkpropagatorjob.cpp
|
||||
putmultifilejob.cpp
|
||||
propagateremotedelete.cpp
|
||||
propagateremotedeleteencrypted.cpp
|
||||
propagateremotedeleteencryptedrootfolder.cpp
|
||||
|
||||
@@ -148,6 +148,17 @@ QNetworkReply *AbstractNetworkJob::sendRequest(const QByteArray &verb, const QUr
|
||||
return reply;
|
||||
}
|
||||
|
||||
QNetworkReply *AbstractNetworkJob::sendRequest(const QByteArray &verb,
|
||||
const QUrl &url,
|
||||
QNetworkRequest req,
|
||||
QHttpMultiPart *requestBody)
|
||||
{
|
||||
auto reply = _account->sendRawRequest(verb, url, req, requestBody);
|
||||
_requestBody = nullptr;
|
||||
adoptRequest(reply);
|
||||
return reply;
|
||||
}
|
||||
|
||||
void AbstractNetworkJob::adoptRequest(QNetworkReply *reply)
|
||||
{
|
||||
addTimer(reply);
|
||||
|
||||
@@ -77,7 +77,7 @@ public:
|
||||
bool timedOut() const { return _timedout; }
|
||||
|
||||
/** Returns an error message, if any. */
|
||||
QString errorString() const;
|
||||
virtual QString errorString() const;
|
||||
|
||||
/** Like errorString, but also checking the reply body for information.
|
||||
*
|
||||
@@ -138,6 +138,9 @@ protected:
|
||||
QNetworkRequest req = QNetworkRequest(),
|
||||
QIODevice *requestBody = nullptr);
|
||||
|
||||
QNetworkReply *sendRequest(const QByteArray &verb, const QUrl &url,
|
||||
QNetworkRequest req, QHttpMultiPart *requestBody);
|
||||
|
||||
/** Makes this job drive a pre-made QNetworkReply
|
||||
*
|
||||
* This reply cannot have a QIODevice request body because we can't get
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QLoggingCategory>
|
||||
#include <QHttpMultiPart>
|
||||
|
||||
#include <qsslconfiguration.h>
|
||||
#include <qt5keychain/keychain.h>
|
||||
@@ -56,6 +57,7 @@ using namespace QKeychain;
|
||||
|
||||
namespace {
|
||||
constexpr int pushNotificationsReconnectInterval = 1000 * 60 * 2;
|
||||
constexpr int usernamePrefillServerVersinMinSupportedMajor = 24;
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
@@ -360,6 +362,18 @@ QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url,
|
||||
return _am->sendCustomRequest(req, verb, data);
|
||||
}
|
||||
|
||||
QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QHttpMultiPart *data)
|
||||
{
|
||||
req.setUrl(url);
|
||||
req.setSslConfiguration(this->getOrCreateSslConfig());
|
||||
if (verb == "PUT") {
|
||||
return _am->put(req, data);
|
||||
} else if (verb == "POST") {
|
||||
return _am->post(req, data);
|
||||
}
|
||||
return _am->sendCustomRequest(req, verb, data);
|
||||
}
|
||||
|
||||
SimpleNetworkJob *Account::sendRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data)
|
||||
{
|
||||
auto job = new SimpleNetworkJob(sharedFromThis());
|
||||
@@ -616,6 +630,11 @@ bool Account::serverVersionUnsupported() const
|
||||
NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR, NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH);
|
||||
}
|
||||
|
||||
bool Account::isUsernamePrefillSupported() const
|
||||
{
|
||||
return serverVersionInt() >= makeServerVersion(usernamePrefillServerVersinMinSupportedMajor, 0, 0);
|
||||
}
|
||||
|
||||
void Account::setServerVersion(const QString &version)
|
||||
{
|
||||
if (version == _serverVersion) {
|
||||
|
||||
@@ -154,6 +154,9 @@ public:
|
||||
QNetworkReply *sendRawRequest(const QByteArray &verb,
|
||||
const QUrl &url, QNetworkRequest req, const QByteArray &data);
|
||||
|
||||
QNetworkReply *sendRawRequest(const QByteArray &verb,
|
||||
const QUrl &url, QNetworkRequest req, QHttpMultiPart *data);
|
||||
|
||||
/** Create and start network job for a simple one-off request.
|
||||
*
|
||||
* More complicated requests typically create their own job types.
|
||||
@@ -227,6 +230,8 @@ public:
|
||||
*/
|
||||
bool serverVersionUnsupported() const;
|
||||
|
||||
bool isUsernamePrefillSupported() const;
|
||||
|
||||
/** True when the server connection is using HTTP2 */
|
||||
bool isHttp2Supported() { return _http2Supported; }
|
||||
void setHttp2Supported(bool value) { _http2Supported = value; }
|
||||
|
||||
719
src/libsync/bulkpropagatorjob.cpp
Normal file
719
src/libsync/bulkpropagatorjob.cpp
Normal file
@@ -0,0 +1,719 @@
|
||||
/*
|
||||
* Copyright 2021 (c) Matthieu Gallien <matthieu.gallien@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 "bulkpropagatorjob.h"
|
||||
|
||||
#include "putmultifilejob.h"
|
||||
#include "owncloudpropagator_p.h"
|
||||
#include "syncfileitem.h"
|
||||
#include "syncengine.h"
|
||||
#include "propagateupload.h"
|
||||
#include "propagatorjobs.h"
|
||||
#include "filesystem.h"
|
||||
#include "account.h"
|
||||
#include "common/utility.h"
|
||||
#include "common/checksums.h"
|
||||
#include "networkjobs.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcBulkPropagatorJob, "nextcloud.sync.propagator.bulkupload", QtInfoMsg)
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
QByteArray getEtagFromJsonReply(const QJsonObject &reply)
|
||||
{
|
||||
const auto ocEtag = OCC::parseEtag(reply.value("OC-ETag").toString().toLatin1());
|
||||
const auto ETag = OCC::parseEtag(reply.value("ETag").toString().toLatin1());
|
||||
const auto etag = OCC::parseEtag(reply.value("etag").toString().toLatin1());
|
||||
QByteArray ret = ocEtag;
|
||||
if (ret.isEmpty()) {
|
||||
ret = ETag;
|
||||
}
|
||||
if (ret.isEmpty()) {
|
||||
ret = etag;
|
||||
}
|
||||
if (ocEtag.length() > 0 && ocEtag != etag && ocEtag != ETag) {
|
||||
qCDebug(OCC::lcBulkPropagatorJob) << "Quite peculiar, we have an etag != OC-Etag [no problem!]" << etag << ETag << ocEtag;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
QByteArray getHeaderFromJsonReply(const QJsonObject &reply, const QByteArray &headerName)
|
||||
{
|
||||
return reply.value(headerName).toString().toLatin1();
|
||||
}
|
||||
|
||||
constexpr auto batchSize = 100;
|
||||
|
||||
constexpr auto parallelJobsMaximumCount = 1;
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
BulkPropagatorJob::BulkPropagatorJob(OwncloudPropagator *propagator,
|
||||
const std::deque<SyncFileItemPtr> &items)
|
||||
: PropagatorJob(propagator)
|
||||
, _items(items)
|
||||
{
|
||||
_filesToUpload.reserve(batchSize);
|
||||
_pendingChecksumFiles.reserve(batchSize);
|
||||
}
|
||||
|
||||
bool BulkPropagatorJob::scheduleSelfOrChild()
|
||||
{
|
||||
if (_items.empty()) {
|
||||
return false;
|
||||
}
|
||||
if (!_pendingChecksumFiles.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_state = Running;
|
||||
for(int i = 0; i < batchSize && !_items.empty(); ++i) {
|
||||
auto currentItem = _items.front();
|
||||
_items.pop_front();
|
||||
_pendingChecksumFiles.insert(currentItem->_file);
|
||||
QMetaObject::invokeMethod(this, [this, currentItem] () {
|
||||
UploadFileInfo fileToUpload;
|
||||
fileToUpload._file = currentItem->_file;
|
||||
fileToUpload._size = currentItem->_size;
|
||||
fileToUpload._path = propagator()->fullLocalPath(fileToUpload._file);
|
||||
startUploadFile(currentItem, fileToUpload);
|
||||
}); // We could be in a different thread (neon jobs)
|
||||
}
|
||||
|
||||
return _items.empty() && _filesToUpload.empty();
|
||||
}
|
||||
|
||||
PropagatorJob::JobParallelism BulkPropagatorJob::parallelism()
|
||||
{
|
||||
return PropagatorJob::JobParallelism::FullParallelism;
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::startUploadFile(SyncFileItemPtr item, UploadFileInfo fileToUpload)
|
||||
{
|
||||
if (propagator()->_abortRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the specific file can be accessed
|
||||
if (propagator()->hasCaseClashAccessibilityProblem(fileToUpload._file)) {
|
||||
done(item, SyncFileItem::NormalError, tr("File %1 cannot be uploaded because another file with the same name, differing only in case, exists").arg(QDir::toNativeSeparators(item->_file)));
|
||||
return;
|
||||
}
|
||||
|
||||
return slotComputeTransmissionChecksum(item, fileToUpload);
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::doStartUpload(SyncFileItemPtr item,
|
||||
UploadFileInfo fileToUpload,
|
||||
QByteArray transmissionChecksumHeader)
|
||||
{
|
||||
if (propagator()->_abortRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
// write the checksum in the database, so if the POST is sent
|
||||
// to the server, but the connection drops before we get the etag, we can check the checksum
|
||||
// in reconcile (issue #5106)
|
||||
SyncJournalDb::UploadInfo pi;
|
||||
pi._valid = true;
|
||||
pi._chunk = 0;
|
||||
pi._transferid = 0; // We set a null transfer id because it is not chunked.
|
||||
pi._modtime = item->_modtime;
|
||||
pi._errorCount = 0;
|
||||
pi._contentChecksum = item->_checksumHeader;
|
||||
pi._size = item->_size;
|
||||
propagator()->_journal->setUploadInfo(item->_file, pi);
|
||||
propagator()->_journal->commit("Upload info");
|
||||
|
||||
auto currentHeaders = headers(item);
|
||||
currentHeaders[QByteArrayLiteral("Content-Length")] = QByteArray::number(fileToUpload._size);
|
||||
|
||||
if (!item->_renameTarget.isEmpty() && item->_file != item->_renameTarget) {
|
||||
// Try to rename the file
|
||||
const auto originalFilePathAbsolute = propagator()->fullLocalPath(item->_file);
|
||||
const auto newFilePathAbsolute = propagator()->fullLocalPath(item->_renameTarget);
|
||||
const auto renameSuccess = QFile::rename(originalFilePathAbsolute, newFilePathAbsolute);
|
||||
if (!renameSuccess) {
|
||||
done(item, SyncFileItem::NormalError, "File contains trailing spaces and couldn't be renamed");
|
||||
return;
|
||||
}
|
||||
qCWarning(lcBulkPropagatorJob()) << item->_file << item->_renameTarget;
|
||||
fileToUpload._file = item->_file = item->_renameTarget;
|
||||
fileToUpload._path = propagator()->fullLocalPath(fileToUpload._file);
|
||||
item->_modtime = FileSystem::getModTime(newFilePathAbsolute);
|
||||
if (item->_modtime <= 0) {
|
||||
_pendingChecksumFiles.remove(item->_file);
|
||||
slotOnErrorStartFolderUnlock(item, SyncFileItem::NormalError, tr("File %1 has invalid modified time. Do not upload to the server.").arg(QDir::toNativeSeparators(item->_file)));
|
||||
checkPropagationIsDone();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const auto remotePath = propagator()->fullRemotePath(fileToUpload._file);
|
||||
|
||||
currentHeaders["X-File-MD5"] = transmissionChecksumHeader;
|
||||
|
||||
BulkUploadItem newUploadFile{propagator()->account(), item, fileToUpload,
|
||||
remotePath, fileToUpload._path,
|
||||
fileToUpload._size, currentHeaders};
|
||||
|
||||
qCInfo(lcBulkPropagatorJob) << remotePath << "transmission checksum" << transmissionChecksumHeader << fileToUpload._path;
|
||||
_filesToUpload.push_back(std::move(newUploadFile));
|
||||
_pendingChecksumFiles.remove(item->_file);
|
||||
|
||||
if (_pendingChecksumFiles.empty()) {
|
||||
triggerUpload();
|
||||
}
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::triggerUpload()
|
||||
{
|
||||
auto uploadParametersData = std::vector<SingleUploadFileData>{};
|
||||
uploadParametersData.reserve(_filesToUpload.size());
|
||||
|
||||
int timeout = 0;
|
||||
for(auto &singleFile : _filesToUpload) {
|
||||
// job takes ownership of device via a QScopedPointer. Job deletes itself when finishing
|
||||
auto device = std::make_unique<UploadDevice>(
|
||||
singleFile._localPath, 0, singleFile._fileSize, &propagator()->_bandwidthManager);
|
||||
if (!device->open(QIODevice::ReadOnly)) {
|
||||
qCWarning(lcBulkPropagatorJob) << "Could not prepare upload device: " << device->errorString();
|
||||
|
||||
// If the file is currently locked, we want to retry the sync
|
||||
// when it becomes available again.
|
||||
if (FileSystem::isFileLocked(singleFile._localPath)) {
|
||||
emit propagator()->seenLockedFile(singleFile._localPath);
|
||||
}
|
||||
|
||||
abortWithError(singleFile._item, SyncFileItem::NormalError, device->errorString());
|
||||
emit finished(SyncFileItem::NormalError);
|
||||
|
||||
return;
|
||||
}
|
||||
singleFile._headers["X-File-Path"] = singleFile._remotePath.toUtf8();
|
||||
uploadParametersData.push_back({std::move(device), singleFile._headers});
|
||||
timeout += singleFile._fileSize;
|
||||
}
|
||||
|
||||
const auto bulkUploadUrl = Utility::concatUrlPath(propagator()->account()->url(), QStringLiteral("/remote.php/dav/bulk"));
|
||||
auto job = std::make_unique<PutMultiFileJob>(propagator()->account(), bulkUploadUrl, std::move(uploadParametersData), this);
|
||||
connect(job.get(), &PutMultiFileJob::finishedSignal, this, &BulkPropagatorJob::slotPutFinished);
|
||||
|
||||
for(auto &singleFile : _filesToUpload) {
|
||||
connect(job.get(), &PutMultiFileJob::uploadProgress,
|
||||
this, [this, singleFile] (qint64 sent, qint64 total) {
|
||||
slotUploadProgress(singleFile._item, sent, total);
|
||||
});
|
||||
}
|
||||
|
||||
adjustLastJobTimeout(job.get(), timeout);
|
||||
_jobs.append(job.get());
|
||||
job.release()->start();
|
||||
if (parallelism() == PropagatorJob::JobParallelism::FullParallelism && _jobs.size() < parallelJobsMaximumCount) {
|
||||
scheduleSelfOrChild();
|
||||
}
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::checkPropagationIsDone()
|
||||
{
|
||||
if (_items.empty()) {
|
||||
if (!_jobs.empty() || !_pendingChecksumFiles.empty()) {
|
||||
// just wait for the other job to finish.
|
||||
return;
|
||||
}
|
||||
|
||||
qCInfo(lcBulkPropagatorJob) << "final status" << _finalStatus;
|
||||
emit finished(_finalStatus);
|
||||
propagator()->scheduleNextJob();
|
||||
} else {
|
||||
scheduleSelfOrChild();
|
||||
}
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::slotComputeTransmissionChecksum(SyncFileItemPtr item,
|
||||
UploadFileInfo fileToUpload)
|
||||
{
|
||||
// Reuse the content checksum as the transmission checksum if possible
|
||||
const auto supportedTransmissionChecksums =
|
||||
propagator()->account()->capabilities().supportedChecksumTypes();
|
||||
|
||||
// Compute the transmission checksum.
|
||||
auto computeChecksum = std::make_unique<ComputeChecksum>(this);
|
||||
if (uploadChecksumEnabled()) {
|
||||
computeChecksum->setChecksumType("MD5" /*propagator()->account()->capabilities().uploadChecksumType()*/);
|
||||
} else {
|
||||
computeChecksum->setChecksumType(QByteArray());
|
||||
}
|
||||
|
||||
connect(computeChecksum.get(), &ComputeChecksum::done,
|
||||
this, [this, item, fileToUpload] (const QByteArray &contentChecksumType, const QByteArray &contentChecksum) {
|
||||
slotStartUpload(item, fileToUpload, contentChecksumType, contentChecksum);
|
||||
});
|
||||
connect(computeChecksum.get(), &ComputeChecksum::done,
|
||||
computeChecksum.get(), &QObject::deleteLater);
|
||||
computeChecksum.release()->start(fileToUpload._path);
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::slotStartUpload(SyncFileItemPtr item,
|
||||
UploadFileInfo fileToUpload,
|
||||
const QByteArray &transmissionChecksumType,
|
||||
const QByteArray &transmissionChecksum)
|
||||
{
|
||||
const auto transmissionChecksumHeader = makeChecksumHeader(transmissionChecksumType, transmissionChecksum);
|
||||
|
||||
item->_checksumHeader = transmissionChecksumHeader;
|
||||
|
||||
const QString fullFilePath = fileToUpload._path;
|
||||
const QString originalFilePath = propagator()->fullLocalPath(item->_file);
|
||||
|
||||
if (!FileSystem::fileExists(fullFilePath)) {
|
||||
return slotOnErrorStartFolderUnlock(item, SyncFileItem::SoftError, tr("File Removed (start upload) %1").arg(fullFilePath));
|
||||
}
|
||||
const time_t prevModtime = item->_modtime; // the _item value was set in PropagateUploadFile::start()
|
||||
// but a potential checksum calculation could have taken some time during which the file could
|
||||
// have been changed again, so better check again here.
|
||||
|
||||
item->_modtime = FileSystem::getModTime(originalFilePath);
|
||||
if (item->_modtime <= 0) {
|
||||
_pendingChecksumFiles.remove(item->_file);
|
||||
slotOnErrorStartFolderUnlock(item, SyncFileItem::NormalError, tr("File %1 has invalid modified time. Do not upload to the server.").arg(QDir::toNativeSeparators(item->_file)));
|
||||
checkPropagationIsDone();
|
||||
return;
|
||||
}
|
||||
if (prevModtime != item->_modtime) {
|
||||
propagator()->_anotherSyncNeeded = true;
|
||||
_pendingChecksumFiles.remove(item->_file);
|
||||
qDebug() << "trigger another sync after checking modified time of item" << item->_file << "prevModtime" << prevModtime << "Curr" << item->_modtime;
|
||||
slotOnErrorStartFolderUnlock(item, SyncFileItem::SoftError, tr("Local file changed during syncing. It will be resumed."));
|
||||
checkPropagationIsDone();
|
||||
return;
|
||||
}
|
||||
|
||||
fileToUpload._size = FileSystem::getSize(fullFilePath);
|
||||
item->_size = FileSystem::getSize(originalFilePath);
|
||||
|
||||
// But skip the file if the mtime is too close to 'now'!
|
||||
// That usually indicates a file that is still being changed
|
||||
// or not yet fully copied to the destination.
|
||||
if (fileIsStillChanging(*item)) {
|
||||
propagator()->_anotherSyncNeeded = true;
|
||||
_pendingChecksumFiles.remove(item->_file);
|
||||
slotOnErrorStartFolderUnlock(item, SyncFileItem::SoftError, tr("Local file changed during sync."));
|
||||
checkPropagationIsDone();
|
||||
return;
|
||||
}
|
||||
|
||||
doStartUpload(item, fileToUpload, transmissionChecksum);
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::slotOnErrorStartFolderUnlock(SyncFileItemPtr item,
|
||||
SyncFileItem::Status status,
|
||||
const QString &errorString)
|
||||
{
|
||||
qCInfo(lcBulkPropagatorJob()) << status << errorString;
|
||||
done(item, status, errorString);
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::slotPutFinishedOneFile(const BulkUploadItem &singleFile,
|
||||
PutMultiFileJob *job,
|
||||
const QJsonObject &fileReply)
|
||||
{
|
||||
bool finished = false;
|
||||
|
||||
qCInfo(lcBulkPropagatorJob()) << singleFile._item->_file << "file headers" << fileReply;
|
||||
|
||||
if (fileReply.contains("error") && !fileReply[QStringLiteral("error")].toBool()) {
|
||||
singleFile._item->_httpErrorCode = static_cast<quint16>(200);
|
||||
} else {
|
||||
singleFile._item->_httpErrorCode = static_cast<quint16>(412);
|
||||
}
|
||||
|
||||
singleFile._item->_responseTimeStamp = job->responseTimestamp();
|
||||
singleFile._item->_requestId = job->requestId();
|
||||
if (singleFile._item->_httpErrorCode != 200) {
|
||||
commonErrorHandling(singleFile._item, fileReply[QStringLiteral("message")].toString());
|
||||
return;
|
||||
}
|
||||
|
||||
singleFile._item->_status = SyncFileItem::Success;
|
||||
|
||||
// Check the file again post upload.
|
||||
// Two cases must be considered separately: If the upload is finished,
|
||||
// the file is on the server and has a changed ETag. In that case,
|
||||
// the etag has to be properly updated in the client journal, and because
|
||||
// of that we can bail out here with an error. But we can reschedule a
|
||||
// sync ASAP.
|
||||
// But if the upload is ongoing, because not all chunks were uploaded
|
||||
// yet, the upload can be stopped and an error can be displayed, because
|
||||
// the server hasn't registered the new file yet.
|
||||
const auto etag = getEtagFromJsonReply(fileReply);
|
||||
finished = etag.length() > 0;
|
||||
|
||||
const auto fullFilePath(propagator()->fullLocalPath(singleFile._item->_file));
|
||||
|
||||
// Check if the file still exists
|
||||
if (!checkFileStillExists(singleFile._item, finished, fullFilePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check whether the file changed since discovery. the file check here is the original and not the temporary.
|
||||
if (!checkFileChanged(singleFile._item, finished, fullFilePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// the file id should only be empty for new files up- or downloaded
|
||||
computeFileId(singleFile._item, fileReply);
|
||||
|
||||
singleFile._item->_etag = etag;
|
||||
|
||||
if (getHeaderFromJsonReply(fileReply, "X-OC-MTime") != "accepted") {
|
||||
// X-OC-MTime is supported since owncloud 5.0. But not when chunking.
|
||||
// Normally Owncloud 6 always puts X-OC-MTime
|
||||
qCWarning(lcBulkPropagatorJob) << "Server does not support X-OC-MTime" << getHeaderFromJsonReply(fileReply, "X-OC-MTime");
|
||||
// Well, the mtime was not set
|
||||
}
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::slotPutFinished()
|
||||
{
|
||||
auto *job = qobject_cast<PutMultiFileJob *>(sender());
|
||||
Q_ASSERT(job);
|
||||
|
||||
slotJobDestroyed(job); // remove it from the _jobs list
|
||||
|
||||
const auto replyData = job->reply()->readAll();
|
||||
const auto replyJson = QJsonDocument::fromJson(replyData);
|
||||
const auto fullReplyObject = replyJson.object();
|
||||
|
||||
for (const auto &singleFile : _filesToUpload) {
|
||||
if (!fullReplyObject.contains(singleFile._remotePath)) {
|
||||
continue;
|
||||
}
|
||||
const auto singleReplyObject = fullReplyObject[singleFile._remotePath].toObject();
|
||||
slotPutFinishedOneFile(singleFile, job, singleReplyObject);
|
||||
}
|
||||
|
||||
finalize(fullReplyObject);
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::slotUploadProgress(SyncFileItemPtr item, qint64 sent, qint64 total)
|
||||
{
|
||||
// Completion is signaled with sent=0, total=0; avoid accidentally
|
||||
// resetting progress due to the sent being zero by ignoring it.
|
||||
// finishedSignal() is bound to be emitted soon anyway.
|
||||
// See https://bugreports.qt.io/browse/QTBUG-44782.
|
||||
if (sent == 0 && total == 0) {
|
||||
return;
|
||||
}
|
||||
propagator()->reportProgress(*item, sent - total);
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::slotJobDestroyed(QObject *job)
|
||||
{
|
||||
_jobs.erase(std::remove(_jobs.begin(), _jobs.end(), job), _jobs.end());
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::adjustLastJobTimeout(AbstractNetworkJob *job, qint64 fileSize) const
|
||||
{
|
||||
constexpr double threeMinutes = 3.0 * 60 * 1000;
|
||||
|
||||
job->setTimeout(qBound(
|
||||
job->timeoutMsec(),
|
||||
// Calculate 3 minutes for each gigabyte of data
|
||||
qRound64(threeMinutes * static_cast<double>(fileSize) / 1e9),
|
||||
// Maximum of 30 minutes
|
||||
static_cast<qint64>(30 * 60 * 1000)));
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::finalizeOneFile(const BulkUploadItem &oneFile)
|
||||
{
|
||||
// Update the database entry
|
||||
const auto result = propagator()->updateMetadata(*oneFile._item);
|
||||
if (!result) {
|
||||
done(oneFile._item, SyncFileItem::FatalError, tr("Error updating metadata: %1").arg(result.error()));
|
||||
return;
|
||||
} else if (*result == Vfs::ConvertToPlaceholderResult::Locked) {
|
||||
done(oneFile._item, SyncFileItem::SoftError, tr("The file %1 is currently in use").arg(oneFile._item->_file));
|
||||
return;
|
||||
}
|
||||
|
||||
// Files that were new on the remote shouldn't have online-only pin state
|
||||
// even if their parent folder is online-only.
|
||||
if (oneFile._item->_instruction == CSYNC_INSTRUCTION_NEW
|
||||
|| oneFile._item->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE) {
|
||||
auto &vfs = propagator()->syncOptions()._vfs;
|
||||
const auto pin = vfs->pinState(oneFile._item->_file);
|
||||
if (pin && *pin == PinState::OnlineOnly && !vfs->setPinState(oneFile._item->_file, PinState::Unspecified)) {
|
||||
qCWarning(lcBulkPropagatorJob) << "Could not set pin state of" << oneFile._item->_file << "to unspecified";
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from the progress database:
|
||||
propagator()->_journal->setUploadInfo(oneFile._item->_file, SyncJournalDb::UploadInfo());
|
||||
propagator()->_journal->commit("upload file start");
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::finalize(const QJsonObject &fullReply)
|
||||
{
|
||||
for(auto singleFileIt = std::begin(_filesToUpload); singleFileIt != std::end(_filesToUpload); ) {
|
||||
const auto &singleFile = *singleFileIt;
|
||||
|
||||
if (!fullReply.contains(singleFile._remotePath)) {
|
||||
++singleFileIt;
|
||||
continue;
|
||||
}
|
||||
if (!singleFile._item->hasErrorStatus()) {
|
||||
finalizeOneFile(singleFile);
|
||||
}
|
||||
|
||||
done(singleFile._item, singleFile._item->_status, {});
|
||||
|
||||
singleFileIt = _filesToUpload.erase(singleFileIt);
|
||||
}
|
||||
|
||||
checkPropagationIsDone();
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::done(SyncFileItemPtr item,
|
||||
SyncFileItem::Status status,
|
||||
const QString &errorString)
|
||||
{
|
||||
item->_status = status;
|
||||
item->_errorString = errorString;
|
||||
|
||||
qCInfo(lcBulkPropagatorJob) << "Item completed" << item->destination() << item->_status << item->_instruction << item->_errorString;
|
||||
|
||||
handleFileRestoration(item, errorString);
|
||||
|
||||
if (propagator()->_abortRequested && (item->_status == SyncFileItem::NormalError
|
||||
|| item->_status == SyncFileItem::FatalError)) {
|
||||
// an abort request is ongoing. Change the status to Soft-Error
|
||||
item->_status = SyncFileItem::SoftError;
|
||||
}
|
||||
|
||||
if (item->_status != SyncFileItem::Success) {
|
||||
// Blacklist handling
|
||||
handleBulkUploadBlackList(item);
|
||||
propagator()->_anotherSyncNeeded = true;
|
||||
}
|
||||
|
||||
handleJobDoneErrors(item, status);
|
||||
|
||||
emit propagator()->itemCompleted(item);
|
||||
}
|
||||
|
||||
QMap<QByteArray, QByteArray> BulkPropagatorJob::headers(SyncFileItemPtr item) const
|
||||
{
|
||||
QMap<QByteArray, QByteArray> headers;
|
||||
headers[QByteArrayLiteral("Content-Type")] = QByteArrayLiteral("application/octet-stream");
|
||||
headers[QByteArrayLiteral("X-File-Mtime")] = QByteArray::number(qint64(item->_modtime));
|
||||
if (qEnvironmentVariableIntValue("OWNCLOUD_LAZYOPS")) {
|
||||
headers[QByteArrayLiteral("OC-LazyOps")] = QByteArrayLiteral("true");
|
||||
}
|
||||
|
||||
if (item->_file.contains(QLatin1String(".sys.admin#recall#"))) {
|
||||
// This is a file recall triggered by the admin. Note: the
|
||||
// recall list file created by the admin and downloaded by the
|
||||
// client (.sys.admin#recall#) also falls into this category
|
||||
// (albeit users are not supposed to mess up with it)
|
||||
|
||||
// We use a special tag header so that the server may decide to store this file away in some admin stage area
|
||||
// And not directly in the user's area (which would trigger redownloads etc).
|
||||
headers["OC-Tag"] = ".sys.admin#recall#";
|
||||
}
|
||||
|
||||
if (!item->_etag.isEmpty() && item->_etag != "empty_etag"
|
||||
&& item->_instruction != CSYNC_INSTRUCTION_NEW // On new files never send a If-Match
|
||||
&& item->_instruction != CSYNC_INSTRUCTION_TYPE_CHANGE) {
|
||||
// We add quotes because the owncloud server always adds quotes around the etag, and
|
||||
// csync_owncloud.c's owncloud_file_id always strips the quotes.
|
||||
headers[QByteArrayLiteral("If-Match")] = '"' + item->_etag + '"';
|
||||
}
|
||||
|
||||
// Set up a conflict file header pointing to the original file
|
||||
auto conflictRecord = propagator()->_journal->conflictRecord(item->_file.toUtf8());
|
||||
if (conflictRecord.isValid()) {
|
||||
headers[QByteArrayLiteral("OC-Conflict")] = "1";
|
||||
if (!conflictRecord.initialBasePath.isEmpty()) {
|
||||
headers[QByteArrayLiteral("OC-ConflictInitialBasePath")] = conflictRecord.initialBasePath;
|
||||
}
|
||||
if (!conflictRecord.baseFileId.isEmpty()) {
|
||||
headers[QByteArrayLiteral("OC-ConflictBaseFileId")] = conflictRecord.baseFileId;
|
||||
}
|
||||
if (conflictRecord.baseModtime != -1) {
|
||||
headers[QByteArrayLiteral("OC-ConflictBaseMtime")] = QByteArray::number(conflictRecord.baseModtime);
|
||||
}
|
||||
if (!conflictRecord.baseEtag.isEmpty()) {
|
||||
headers[QByteArrayLiteral("OC-ConflictBaseEtag")] = conflictRecord.baseEtag;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::abortWithError(SyncFileItemPtr item,
|
||||
SyncFileItem::Status status,
|
||||
const QString &error)
|
||||
{
|
||||
abort(AbortType::Synchronous);
|
||||
done(item, status, error);
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::checkResettingErrors(SyncFileItemPtr item) const
|
||||
{
|
||||
if (item->_httpErrorCode == 412
|
||||
|| propagator()->account()->capabilities().httpErrorCodesThatResetFailingChunkedUploads().contains(item->_httpErrorCode)) {
|
||||
auto uploadInfo = propagator()->_journal->getUploadInfo(item->_file);
|
||||
uploadInfo._errorCount += 1;
|
||||
if (uploadInfo._errorCount > 3) {
|
||||
qCInfo(lcBulkPropagatorJob) << "Reset transfer of" << item->_file
|
||||
<< "due to repeated error" << item->_httpErrorCode;
|
||||
uploadInfo = SyncJournalDb::UploadInfo();
|
||||
} else {
|
||||
qCInfo(lcBulkPropagatorJob) << "Error count for maybe-reset error" << item->_httpErrorCode
|
||||
<< "on file" << item->_file
|
||||
<< "is" << uploadInfo._errorCount;
|
||||
}
|
||||
propagator()->_journal->setUploadInfo(item->_file, uploadInfo);
|
||||
propagator()->_journal->commit("Upload info");
|
||||
}
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::commonErrorHandling(SyncFileItemPtr item,
|
||||
const QString &errorMessage)
|
||||
{
|
||||
// Ensure errors that should eventually reset the chunked upload are tracked.
|
||||
checkResettingErrors(item);
|
||||
|
||||
abortWithError(item, SyncFileItem::NormalError, errorMessage);
|
||||
}
|
||||
|
||||
bool BulkPropagatorJob::checkFileStillExists(SyncFileItemPtr item,
|
||||
const bool finished,
|
||||
const QString &fullFilePath)
|
||||
{
|
||||
if (!FileSystem::fileExists(fullFilePath)) {
|
||||
if (!finished) {
|
||||
abortWithError(item, SyncFileItem::SoftError, tr("The local file was removed during sync."));
|
||||
return false;
|
||||
} else {
|
||||
propagator()->_anotherSyncNeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BulkPropagatorJob::checkFileChanged(SyncFileItemPtr item,
|
||||
const bool finished,
|
||||
const QString &fullFilePath)
|
||||
{
|
||||
if (!FileSystem::verifyFileUnchanged(fullFilePath, item->_size, item->_modtime)) {
|
||||
propagator()->_anotherSyncNeeded = true;
|
||||
if (!finished) {
|
||||
abortWithError(item, SyncFileItem::SoftError, tr("Local file changed during sync."));
|
||||
// FIXME: the legacy code was retrying for a few seconds.
|
||||
// and also checking that after the last chunk, and removed the file in case of INSTRUCTION_NEW
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::computeFileId(SyncFileItemPtr item,
|
||||
const QJsonObject &fileReply) const
|
||||
{
|
||||
const auto fid = getHeaderFromJsonReply(fileReply, "OC-FileID");
|
||||
if (!fid.isEmpty()) {
|
||||
if (!item->_fileId.isEmpty() && item->_fileId != fid) {
|
||||
qCWarning(lcBulkPropagatorJob) << "File ID changed!" << item->_fileId << fid;
|
||||
}
|
||||
item->_fileId = fid;
|
||||
}
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::handleFileRestoration(SyncFileItemPtr item,
|
||||
const QString &errorString) const
|
||||
{
|
||||
if (item->_isRestoration) {
|
||||
if (item->_status == SyncFileItem::Success
|
||||
|| item->_status == SyncFileItem::Conflict) {
|
||||
item->_status = SyncFileItem::Restoration;
|
||||
} else {
|
||||
item->_errorString += tr("; Restoration Failed: %1").arg(errorString);
|
||||
}
|
||||
} else {
|
||||
if (item->_errorString.isEmpty()) {
|
||||
item->_errorString = errorString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::handleBulkUploadBlackList(SyncFileItemPtr item) const
|
||||
{
|
||||
propagator()->addToBulkUploadBlackList(item->_file);
|
||||
}
|
||||
|
||||
void BulkPropagatorJob::handleJobDoneErrors(SyncFileItemPtr item,
|
||||
SyncFileItem::Status status)
|
||||
{
|
||||
if (item->hasErrorStatus()) {
|
||||
qCWarning(lcPropagator) << "Could not complete propagation of" << item->destination() << "by" << this << "with status" << item->_status << "and error:" << item->_errorString;
|
||||
} else {
|
||||
qCInfo(lcPropagator) << "Completed propagation of" << item->destination() << "by" << this << "with status" << item->_status;
|
||||
}
|
||||
|
||||
if (item->_status == SyncFileItem::FatalError) {
|
||||
// Abort all remaining jobs.
|
||||
propagator()->abort();
|
||||
}
|
||||
|
||||
switch (item->_status)
|
||||
{
|
||||
case SyncFileItem::BlacklistedError:
|
||||
case SyncFileItem::Conflict:
|
||||
case SyncFileItem::FatalError:
|
||||
case SyncFileItem::FileIgnored:
|
||||
case SyncFileItem::FileLocked:
|
||||
case SyncFileItem::FileNameInvalid:
|
||||
case SyncFileItem::NoStatus:
|
||||
case SyncFileItem::NormalError:
|
||||
case SyncFileItem::Restoration:
|
||||
case SyncFileItem::SoftError:
|
||||
_finalStatus = SyncFileItem::NormalError;
|
||||
qCInfo(lcBulkPropagatorJob) << "modify final status NormalError" << _finalStatus << status;
|
||||
break;
|
||||
case SyncFileItem::DetailError:
|
||||
_finalStatus = SyncFileItem::DetailError;
|
||||
qCInfo(lcBulkPropagatorJob) << "modify final status DetailError" << _finalStatus << status;
|
||||
break;
|
||||
case SyncFileItem::Success:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
167
src/libsync/bulkpropagatorjob.h
Normal file
167
src/libsync/bulkpropagatorjob.h
Normal file
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2021 (c) Matthieu Gallien <matthieu.gallien@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 "owncloudpropagator.h"
|
||||
#include "abstractnetworkjob.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QVector>
|
||||
#include <QMap>
|
||||
#include <QByteArray>
|
||||
#include <deque>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcBulkPropagatorJob)
|
||||
|
||||
class ComputeChecksum;
|
||||
class PutMultiFileJob;
|
||||
|
||||
class BulkPropagatorJob : public PropagatorJob
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/* This is a minified version of the SyncFileItem,
|
||||
* that holds only the specifics about the file that's
|
||||
* being uploaded.
|
||||
*
|
||||
* This is needed if we wanna apply changes on the file
|
||||
* that's being uploaded while keeping the original on disk.
|
||||
*/
|
||||
struct UploadFileInfo {
|
||||
QString _file; /// I'm still unsure if I should use a SyncFilePtr here.
|
||||
QString _path; /// the full path on disk.
|
||||
qint64 _size;
|
||||
};
|
||||
|
||||
struct BulkUploadItem
|
||||
{
|
||||
AccountPtr _account;
|
||||
SyncFileItemPtr _item;
|
||||
UploadFileInfo _fileToUpload;
|
||||
QString _remotePath;
|
||||
QString _localPath;
|
||||
qint64 _fileSize;
|
||||
QMap<QByteArray, QByteArray> _headers;
|
||||
};
|
||||
|
||||
public:
|
||||
explicit BulkPropagatorJob(OwncloudPropagator *propagator,
|
||||
const std::deque<SyncFileItemPtr> &items);
|
||||
|
||||
bool scheduleSelfOrChild() override;
|
||||
|
||||
JobParallelism parallelism() override;
|
||||
|
||||
private slots:
|
||||
void startUploadFile(SyncFileItemPtr item, UploadFileInfo fileToUpload);
|
||||
|
||||
// Content checksum computed, compute the transmission checksum
|
||||
void slotComputeTransmissionChecksum(SyncFileItemPtr item,
|
||||
UploadFileInfo fileToUpload);
|
||||
|
||||
// transmission checksum computed, prepare the upload
|
||||
void slotStartUpload(SyncFileItemPtr item,
|
||||
UploadFileInfo fileToUpload,
|
||||
const QByteArray &transmissionChecksumType,
|
||||
const QByteArray &transmissionChecksum);
|
||||
|
||||
// invoked on internal error to unlock a folder and faile
|
||||
void slotOnErrorStartFolderUnlock(SyncFileItemPtr item,
|
||||
SyncFileItem::Status status,
|
||||
const QString &errorString);
|
||||
|
||||
void slotPutFinished();
|
||||
|
||||
void slotUploadProgress(SyncFileItemPtr item, qint64 sent, qint64 total);
|
||||
|
||||
void slotJobDestroyed(QObject *job);
|
||||
|
||||
private:
|
||||
void doStartUpload(SyncFileItemPtr item,
|
||||
UploadFileInfo fileToUpload,
|
||||
QByteArray transmissionChecksumHeader);
|
||||
|
||||
void adjustLastJobTimeout(AbstractNetworkJob *job,
|
||||
qint64 fileSize) const;
|
||||
|
||||
void finalize(const QJsonObject &fullReply);
|
||||
|
||||
void finalizeOneFile(const BulkUploadItem &oneFile);
|
||||
|
||||
void slotPutFinishedOneFile(const BulkUploadItem &singleFile,
|
||||
OCC::PutMultiFileJob *job,
|
||||
const QJsonObject &fullReplyObject);
|
||||
|
||||
void done(SyncFileItemPtr item,
|
||||
SyncFileItem::Status status,
|
||||
const QString &errorString);
|
||||
|
||||
/** Bases headers that need to be sent on the PUT, or in the MOVE for chunking-ng */
|
||||
QMap<QByteArray, QByteArray> headers(SyncFileItemPtr item) const;
|
||||
|
||||
void abortWithError(SyncFileItemPtr item,
|
||||
SyncFileItem::Status status,
|
||||
const QString &error);
|
||||
|
||||
/**
|
||||
* Checks whether the current error is one that should reset the whole
|
||||
* transfer if it happens too often. If so: Bump UploadInfo::errorCount
|
||||
* and maybe perform the reset.
|
||||
*/
|
||||
void checkResettingErrors(SyncFileItemPtr item) const;
|
||||
|
||||
/**
|
||||
* Error handling functionality that is shared between jobs.
|
||||
*/
|
||||
void commonErrorHandling(SyncFileItemPtr item,
|
||||
const QString &errorMessage);
|
||||
|
||||
bool checkFileStillExists(SyncFileItemPtr item,
|
||||
const bool finished,
|
||||
const QString &fullFilePath);
|
||||
|
||||
bool checkFileChanged(SyncFileItemPtr item,
|
||||
const bool finished,
|
||||
const QString &fullFilePath);
|
||||
|
||||
void computeFileId(SyncFileItemPtr item,
|
||||
const QJsonObject &fileReply) const;
|
||||
|
||||
void handleFileRestoration(SyncFileItemPtr item,
|
||||
const QString &errorString) const;
|
||||
|
||||
void handleBulkUploadBlackList(SyncFileItemPtr item) const;
|
||||
|
||||
void handleJobDoneErrors(SyncFileItemPtr item,
|
||||
SyncFileItem::Status status);
|
||||
|
||||
void triggerUpload();
|
||||
|
||||
void checkPropagationIsDone();
|
||||
|
||||
std::deque<SyncFileItemPtr> _items;
|
||||
|
||||
QVector<AbstractNetworkJob *> _jobs; /// network jobs that are currently in transit
|
||||
|
||||
QSet<QString> _pendingChecksumFiles;
|
||||
|
||||
std::vector<BulkUploadItem> _filesToUpload;
|
||||
|
||||
SyncFileItem::Status _finalStatus = SyncFileItem::Status::NoStatus;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -216,11 +216,9 @@ bool Capabilities::chunkingNg() const
|
||||
return _capabilities["dav"].toMap()["chunking"].toByteArray() >= "1.0";
|
||||
}
|
||||
|
||||
bool Capabilities::userStatusNotification() const
|
||||
bool Capabilities::bulkUpload() const
|
||||
{
|
||||
return _capabilities.contains("notifications") &&
|
||||
_capabilities["notifications"].toMap().contains("ocs-endpoints") &&
|
||||
_capabilities["notifications"].toMap()["ocs-endpoints"].toStringList().contains("user-status");
|
||||
return _capabilities["dav"].toMap()["bulkupload"].toByteArray() >= "1.0";
|
||||
}
|
||||
|
||||
bool Capabilities::userStatus() const
|
||||
|
||||
@@ -63,7 +63,7 @@ public:
|
||||
bool shareResharing() const;
|
||||
int shareDefaultPermissions() const;
|
||||
bool chunkingNg() const;
|
||||
bool userStatusNotification() const;
|
||||
bool bulkUpload() const;
|
||||
bool userStatus() const;
|
||||
bool userStatusSupportsEmoji() const;
|
||||
|
||||
|
||||
@@ -389,7 +389,13 @@ void ProcessDirectoryJob::processFile(PathTuple path,
|
||||
item->_originalFile = path._original;
|
||||
item->_previousSize = dbEntry._fileSize;
|
||||
item->_previousModtime = dbEntry._modtime;
|
||||
item->_renameTarget = localEntry.renameName;
|
||||
if (!localEntry.renameName.isEmpty()) {
|
||||
if (_dirItem) {
|
||||
item->_renameTarget = _dirItem->_file + "/" + localEntry.renameName;
|
||||
} else {
|
||||
item->_renameTarget = localEntry.renameName;
|
||||
}
|
||||
}
|
||||
|
||||
if (dbEntry._modtime == localEntry.modtime && dbEntry._type == ItemTypeVirtualFile && localEntry.type == ItemTypeFile) {
|
||||
item->_type = ItemTypeFile;
|
||||
@@ -536,6 +542,19 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
|
||||
} else {
|
||||
item->_instruction = CSYNC_INSTRUCTION_SYNC;
|
||||
}
|
||||
} else if (dbEntry._modtime <= 0 && serverEntry.modtime > 0) {
|
||||
item->_direction = SyncFileItem::Down;
|
||||
item->_modtime = serverEntry.modtime;
|
||||
item->_size = sizeOnServer;
|
||||
if (serverEntry.isDirectory) {
|
||||
ENFORCE(dbEntry.isDirectory());
|
||||
item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
|
||||
} else if (!localEntry.isValid() && _queryLocal != ParentNotChanged) {
|
||||
// Deleted locally, changed on server
|
||||
item->_instruction = CSYNC_INSTRUCTION_NEW;
|
||||
} else {
|
||||
item->_instruction = CSYNC_INSTRUCTION_SYNC;
|
||||
}
|
||||
} else if (dbEntry._remotePerm != serverEntry.remotePerm || dbEntry._fileId != serverEntry.fileId || metaDataSizeNeedsUpdateForE2EeFilePlaceholder) {
|
||||
if (metaDataSizeNeedsUpdateForE2EeFilePlaceholder) {
|
||||
// we are updating placeholder sizes after migrating from older versions with VFS + E2EE implicit hydration not supported
|
||||
@@ -934,6 +953,14 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
|
||||
item->_modtime = localEntry.modtime;
|
||||
item->_type = localEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile;
|
||||
_childModified = true;
|
||||
} else if (dbEntry._modtime > 0 && localEntry.modtime <= 0) {
|
||||
item->_instruction = CSYNC_INSTRUCTION_SYNC;
|
||||
item->_direction = SyncFileItem::Down;
|
||||
item->_size = localEntry.size > 0 ? localEntry.size : dbEntry._fileSize;
|
||||
item->_modtime = dbEntry._modtime;
|
||||
item->_previousModtime = dbEntry._modtime;
|
||||
item->_type = localEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile;
|
||||
_childModified = true;
|
||||
} else {
|
||||
// Local file was changed
|
||||
item->_instruction = CSYNC_INSTRUCTION_SYNC;
|
||||
@@ -1000,6 +1027,11 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
|
||||
return;
|
||||
}
|
||||
|
||||
if (localEntry.isDirectory && _discoveryData->_syncOptions._vfs->mode() != Vfs::WindowsCfApi) {
|
||||
// for VFS folders on Windows only
|
||||
return;
|
||||
}
|
||||
|
||||
Q_ASSERT(item->_instruction == CSYNC_INSTRUCTION_NEW);
|
||||
if (item->_instruction != CSYNC_INSTRUCTION_NEW) {
|
||||
qCWarning(lcDisco) << "Trying to wipe a virtual item" << path._local << " with item->_instruction" << item->_instruction;
|
||||
@@ -1028,9 +1060,8 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
|
||||
|
||||
if (!isFilePlaceHolder && !isOnlineOnlyFolder) {
|
||||
if (localEntry.isDirectory && folderPlaceHolderAvailability.isValid() && !isOnlineOnlyFolder) {
|
||||
// a VFS folder but is not online0only (has some files hydrated)
|
||||
// a VFS folder but is not online-only (has some files hydrated)
|
||||
qCInfo(lcDisco) << "Virtual directory without db entry for" << path._local << "but it contains hydrated file(s), so let's keep it and reupload.";
|
||||
emit _discoveryData->addErrorToGui(SyncFileItem::SoftError, tr("Conflict when uploading some files to a folder. Those, conflicted, are going to get cleared!"), path._local);
|
||||
return;
|
||||
}
|
||||
qCWarning(lcDisco) << "Virtual file without db entry for" << path._local
|
||||
@@ -1043,6 +1074,12 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
|
||||
if (isOnlineOnlyFolder) {
|
||||
// if we're wiping a folder, we will only get this function called once and will wipe a folder along with it's files and also display one error in GUI
|
||||
qCInfo(lcDisco) << "Wiping virtual folder without db entry for" << path._local;
|
||||
if (isfolderPlaceHolderAvailabilityOnlineOnly && folderPlaceHolderAvailability.isValid()) {
|
||||
qCInfo(lcDisco) << "*folderPlaceHolderAvailability:" << *folderPlaceHolderAvailability;
|
||||
}
|
||||
if (isFolderPinStateOnlineOnly && folderPinState.isValid()) {
|
||||
qCInfo(lcDisco) << "*folderPinState:" << *folderPinState;
|
||||
}
|
||||
emit _discoveryData->addErrorToGui(SyncFileItem::SoftError, tr("Conflict when uploading a folder. It's going to get cleared!"), path._local);
|
||||
} else {
|
||||
qCInfo(lcDisco) << "Wiping virtual file without db entry for" << path._local;
|
||||
|
||||
@@ -185,7 +185,9 @@ QPair<bool, QByteArray> DiscoveryPhase::findAndCancelDeletedJob(const QString &o
|
||||
qCWarning(lcDiscovery) << "instruction" << instruction;
|
||||
qCWarning(lcDiscovery) << "(*it)->_type" << (*it)->_type;
|
||||
qCWarning(lcDiscovery) << "(*it)->_isRestoration " << (*it)->_isRestoration;
|
||||
ENFORCE(false);
|
||||
Q_ASSERT(false);
|
||||
addErrorToGui(SyncFileItem::Status::FatalError, tr("Error while canceling delete of a file"), originalPath);
|
||||
emit fatalError(tr("Error while canceling delete of %1").arg(originalPath));
|
||||
}
|
||||
(*it)->_instruction = CSYNC_INSTRUCTION_NONE;
|
||||
result = true;
|
||||
|
||||
@@ -98,7 +98,7 @@ bool FileSystem::verifyFileUnchanged(const QString &fileName,
|
||||
{
|
||||
const qint64 actualSize = getSize(fileName);
|
||||
const time_t actualMtime = getModTime(fileName);
|
||||
if (actualSize != previousSize || actualMtime != previousMtime) {
|
||||
if ((actualSize != previousSize && actualMtime > 0) || (actualMtime != previousMtime && previousMtime > 0 && actualMtime > 0)) {
|
||||
qCInfo(lcFileSystem) << "File" << fileName << "has changed:"
|
||||
<< "size: " << previousSize << "<->" << actualSize
|
||||
<< ", mtime: " << previousMtime << "<->" << actualMtime;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "propagateremotedelete.h"
|
||||
#include "propagateremotemove.h"
|
||||
#include "propagateremotemkdir.h"
|
||||
#include "bulkpropagatorjob.h"
|
||||
#include "propagatorjobs.h"
|
||||
#include "filesystem.h"
|
||||
#include "common/utility.h"
|
||||
@@ -173,7 +174,7 @@ static SyncJournalErrorBlacklistRecord createBlacklistEntry(
|
||||
*
|
||||
* May adjust the status or item._errorString.
|
||||
*/
|
||||
static void blacklistUpdate(SyncJournalDb *journal, SyncFileItem &item)
|
||||
void blacklistUpdate(SyncJournalDb *journal, SyncFileItem &item)
|
||||
{
|
||||
SyncJournalErrorBlacklistRecord oldEntry = journal->errorBlacklistEntry(item._file);
|
||||
|
||||
@@ -396,6 +397,8 @@ std::unique_ptr<PropagateUploadFileCommon> OwncloudPropagator::createUploadJob(S
|
||||
|
||||
job->setDeleteExisting(deleteExisting);
|
||||
|
||||
removeFromBulkUploadBlackList(item->_file);
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
@@ -769,6 +772,10 @@ bool OwncloudPropagator::createConflict(const SyncFileItemPtr &item,
|
||||
|
||||
QString renameError;
|
||||
auto conflictModTime = FileSystem::getModTime(fn);
|
||||
if (conflictModTime <= 0) {
|
||||
*error = tr("Impossible to get modification time for file in conflict %1)").arg(fn);
|
||||
return false;
|
||||
}
|
||||
QString conflictUserName;
|
||||
if (account()->capabilities().uploadConflictFiles())
|
||||
conflictUserName = account()->davDisplayName();
|
||||
@@ -861,7 +868,7 @@ Result<Vfs::ConvertToPlaceholderResult, QString> OwncloudPropagator::staticUpdat
|
||||
|
||||
bool OwncloudPropagator::isDelayedUploadItem(const SyncFileItemPtr &item) const
|
||||
{
|
||||
return !_scheduleDelayedTasks && !item->_isEncrypted;
|
||||
return account()->capabilities().bulkUpload() && !_scheduleDelayedTasks && !item->_isEncrypted && _syncOptions._minChunkSize > item->_size && !isInBulkUploadBlackList(item->_file);
|
||||
}
|
||||
|
||||
void OwncloudPropagator::setScheduleDelayedTasks(bool active)
|
||||
@@ -874,6 +881,23 @@ void OwncloudPropagator::clearDelayedTasks()
|
||||
_delayedTasks.clear();
|
||||
}
|
||||
|
||||
void OwncloudPropagator::addToBulkUploadBlackList(const QString &file)
|
||||
{
|
||||
qCDebug(lcPropagator) << "black list for bulk upload" << file;
|
||||
_bulkUploadBlackList.insert(file);
|
||||
}
|
||||
|
||||
void OwncloudPropagator::removeFromBulkUploadBlackList(const QString &file)
|
||||
{
|
||||
qCDebug(lcPropagator) << "black list for bulk upload" << file;
|
||||
_bulkUploadBlackList.remove(file);
|
||||
}
|
||||
|
||||
bool OwncloudPropagator::isInBulkUploadBlackList(const QString &file) const
|
||||
{
|
||||
return _bulkUploadBlackList.contains(file);
|
||||
}
|
||||
|
||||
// ================================================================================
|
||||
|
||||
PropagatorJob::PropagatorJob(OwncloudPropagator *propagator)
|
||||
@@ -1106,6 +1130,13 @@ void PropagateDirectory::slotSubJobsFinished(SyncFileItem::Status status)
|
||||
if (_item->_instruction == CSYNC_INSTRUCTION_NEW && _item->_direction == SyncFileItem::Down) {
|
||||
// special case for local MKDIR, set local directory mtime
|
||||
// (it's not synced later at all, but can be nice to have it set initially)
|
||||
|
||||
if (_item->_modtime <= 0) {
|
||||
status = _item->_status = SyncFileItem::NormalError;
|
||||
_item->_errorString = tr("Error updating metadata due to invalid modified time");
|
||||
qCWarning(lcDirectory) << "Error writing to the database for file" << _item->_file;
|
||||
}
|
||||
|
||||
FileSystem::setModTime(propagator()->fullLocalPath(_item->destination()), _item->_modtime);
|
||||
}
|
||||
|
||||
@@ -1304,13 +1335,4 @@ QString OwncloudPropagator::remotePath() const
|
||||
return _remoteFolder;
|
||||
}
|
||||
|
||||
BulkPropagatorJob::BulkPropagatorJob(OwncloudPropagator *propagator, const QVector<SyncFileItemPtr> &items)
|
||||
: PropagatorCompositeJob(propagator)
|
||||
, _items(items)
|
||||
{
|
||||
for(const auto &oneItemJob : _items) {
|
||||
appendTask(oneItemJob);
|
||||
}
|
||||
_items.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include "accountfwd.h"
|
||||
#include "syncoptions.h"
|
||||
|
||||
#include <deque>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcPropagator)
|
||||
@@ -46,6 +48,8 @@ qint64 criticalFreeSpaceLimit();
|
||||
*/
|
||||
qint64 freeSpaceLimit();
|
||||
|
||||
void blacklistUpdate(SyncJournalDb *journal, SyncFileItem &item);
|
||||
|
||||
class SyncJournalDb;
|
||||
class OwncloudPropagator;
|
||||
class PropagatorCompositeJob;
|
||||
@@ -380,19 +384,6 @@ private:
|
||||
bool scheduleDelayedJobs();
|
||||
};
|
||||
|
||||
class BulkPropagatorJob : public PropagatorCompositeJob
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
||||
explicit BulkPropagatorJob(OwncloudPropagator *propagator,
|
||||
const QVector<SyncFileItemPtr> &items);
|
||||
|
||||
private:
|
||||
|
||||
QVector<SyncFileItemPtr> _items;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Dummy job that just mark it as completed and ignored
|
||||
* @ingroup libsync
|
||||
@@ -431,7 +422,8 @@ public:
|
||||
|
||||
public:
|
||||
OwncloudPropagator(AccountPtr account, const QString &localDir,
|
||||
const QString &remoteFolder, SyncJournalDb *progressDb)
|
||||
const QString &remoteFolder, SyncJournalDb *progressDb,
|
||||
QSet<QString> &bulkUploadBlackList)
|
||||
: _journal(progressDb)
|
||||
, _finishedEmited(false)
|
||||
, _bandwidthManager(this)
|
||||
@@ -440,6 +432,7 @@ public:
|
||||
, _account(account)
|
||||
, _localDir((localDir.endsWith(QChar('/'))) ? localDir : localDir + '/')
|
||||
, _remoteFolder((remoteFolder.endsWith(QChar('/'))) ? remoteFolder : remoteFolder + '/')
|
||||
, _bulkUploadBlackList(bulkUploadBlackList)
|
||||
{
|
||||
qRegisterMetaType<PropagatorJob::AbortType>("PropagatorJob::AbortType");
|
||||
}
|
||||
@@ -611,7 +604,7 @@ public:
|
||||
|
||||
Q_REQUIRED_RESULT bool isDelayedUploadItem(const SyncFileItemPtr &item) const;
|
||||
|
||||
Q_REQUIRED_RESULT const QVector<SyncFileItemPtr>& delayedTasks() const
|
||||
Q_REQUIRED_RESULT const std::deque<SyncFileItemPtr>& delayedTasks() const
|
||||
{
|
||||
return _delayedTasks;
|
||||
}
|
||||
@@ -620,6 +613,12 @@ public:
|
||||
|
||||
void clearDelayedTasks();
|
||||
|
||||
void addToBulkUploadBlackList(const QString &file);
|
||||
|
||||
void removeFromBulkUploadBlackList(const QString &file);
|
||||
|
||||
bool isInBulkUploadBlackList(const QString &file) const;
|
||||
|
||||
private slots:
|
||||
|
||||
void abortTimeout()
|
||||
@@ -674,8 +673,12 @@ private:
|
||||
const QString _localDir; // absolute path to the local directory. ends with '/'
|
||||
const QString _remoteFolder; // remote folder, ends with '/'
|
||||
|
||||
QVector<SyncFileItemPtr> _delayedTasks;
|
||||
std::deque<SyncFileItemPtr> _delayedTasks;
|
||||
bool _scheduleDelayedTasks = false;
|
||||
|
||||
QSet<QString> &_bulkUploadBlackList;
|
||||
|
||||
static bool _allowDelayedUpload;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -18,9 +18,33 @@
|
||||
#include "owncloudpropagator.h"
|
||||
#include "syncfileitem.h"
|
||||
#include "networkjobs.h"
|
||||
#include "syncengine.h"
|
||||
#include <QLoggingCategory>
|
||||
#include <QNetworkReply>
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* We do not want to upload files that are currently being modified.
|
||||
* To avoid that, we don't upload files that have a modification time
|
||||
* that is too close to the current time.
|
||||
*
|
||||
* This interacts with the msBetweenRequestAndSync delay in the folder
|
||||
* manager. If that delay between file-change notification and sync
|
||||
* has passed, we should accept the file for upload here.
|
||||
*/
|
||||
inline bool fileIsStillChanging(const OCC::SyncFileItem &item)
|
||||
{
|
||||
const auto modtime = OCC::Utility::qDateTimeFromTime_t(item._modtime);
|
||||
const qint64 msSinceMod = modtime.msecsTo(QDateTime::currentDateTimeUtc());
|
||||
|
||||
return std::chrono::milliseconds(msSinceMod) < OCC::SyncEngine::minimumFileAgeForUpload
|
||||
// if the mtime is too much in the future we *do* upload the file
|
||||
&& msSinceMod > -10000;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
inline QByteArray getEtagFromReply(QNetworkReply *reply)
|
||||
|
||||
@@ -564,6 +564,10 @@ void PropagateDownloadFile::startAfterIsEncryptedIsChecked()
|
||||
return checksum_header.startsWith("SHA")
|
||||
|| checksum_header.startsWith("MD5:");
|
||||
};
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
if (_item->_instruction == CSYNC_INSTRUCTION_CONFLICT
|
||||
&& _item->_size == _item->_previousSize
|
||||
&& !_item->_checksumHeader.isEmpty()
|
||||
@@ -592,11 +596,22 @@ void PropagateDownloadFile::conflictChecksumComputed(const QByteArray &checksumT
|
||||
// Apply the server mtime locally if necessary, ensuring the journal
|
||||
// and local mtimes end up identical
|
||||
auto fn = propagator()->fullLocalPath(_item->_file);
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
return;
|
||||
}
|
||||
if (_item->_modtime != _item->_previousModtime) {
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
FileSystem::setModTime(fn, _item->_modtime);
|
||||
emit propagator()->touchedFile(fn);
|
||||
}
|
||||
_item->_modtime = FileSystem::getModTime(fn);
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
return;
|
||||
}
|
||||
updateMetadata(/*isConflict=*/false);
|
||||
return;
|
||||
}
|
||||
@@ -820,6 +835,10 @@ void PropagateDownloadFile::slotGetFinished()
|
||||
// It is possible that the file was modified on the server since we did the discovery phase
|
||||
// so make sure we have the up-to-date time
|
||||
_item->_modtime = job->lastModified();
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
}
|
||||
|
||||
_tmpFile.close();
|
||||
@@ -1058,10 +1077,28 @@ void PropagateDownloadFile::downloadFinished()
|
||||
return;
|
||||
}
|
||||
|
||||
if (_item->_modtime <= 0) {
|
||||
FileSystem::remove(_tmpFile.fileName());
|
||||
done(SyncFileItem::NormalError, tr("File %1 has invalid modified time reported by server. Do not save it.").arg(QDir::toNativeSeparators(_item->_file)));
|
||||
return;
|
||||
}
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
FileSystem::setModTime(_tmpFile.fileName(), _item->_modtime);
|
||||
// We need to fetch the time again because some file systems such as FAT have worse than a second
|
||||
// Accuracy, and we really need the time from the file system. (#3103)
|
||||
_item->_modtime = FileSystem::getModTime(_tmpFile.fileName());
|
||||
if (_item->_modtime <= 0) {
|
||||
FileSystem::remove(_tmpFile.fileName());
|
||||
done(SyncFileItem::NormalError, tr("File %1 has invalid modified time reported by server. Do not save it.").arg(QDir::toNativeSeparators(_item->_file)));
|
||||
return;
|
||||
}
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
|
||||
bool previousFileExists = FileSystem::fileExists(fn);
|
||||
if (previousFileExists) {
|
||||
|
||||
@@ -96,7 +96,7 @@ public:
|
||||
void giveBandwidthQuota(qint64 q);
|
||||
qint64 currentDownloadPosition();
|
||||
|
||||
QString errorString() const;
|
||||
QString errorString() const override;
|
||||
void setErrorString(const QString &s) { _errorString = s; }
|
||||
|
||||
SyncFileItem::Status errorStatus() { return _errorStatus; }
|
||||
|
||||
@@ -49,25 +49,6 @@ Q_LOGGING_CATEGORY(lcPropagateUpload, "nextcloud.sync.propagator.upload", QtInfo
|
||||
Q_LOGGING_CATEGORY(lcPropagateUploadV1, "nextcloud.sync.propagator.upload.v1", QtInfoMsg)
|
||||
Q_LOGGING_CATEGORY(lcPropagateUploadNG, "nextcloud.sync.propagator.upload.ng", QtInfoMsg)
|
||||
|
||||
/**
|
||||
* We do not want to upload files that are currently being modified.
|
||||
* To avoid that, we don't upload files that have a modification time
|
||||
* that is too close to the current time.
|
||||
*
|
||||
* This interacts with the msBetweenRequestAndSync delay in the folder
|
||||
* manager. If that delay between file-change notification and sync
|
||||
* has passed, we should accept the file for upload here.
|
||||
*/
|
||||
static bool fileIsStillChanging(const SyncFileItem &item)
|
||||
{
|
||||
const QDateTime modtime = Utility::qDateTimeFromTime_t(item._modtime);
|
||||
const qint64 msSinceMod = modtime.msecsTo(QDateTime::currentDateTimeUtc());
|
||||
|
||||
return std::chrono::milliseconds(msSinceMod) < SyncEngine::minimumFileAgeForUpload
|
||||
// if the mtime is too much in the future we *do* upload the file
|
||||
&& msSinceMod > -10000;
|
||||
}
|
||||
|
||||
PUTFileJob::~PUTFileJob()
|
||||
{
|
||||
// Make sure that we destroy the QNetworkReply before our _device of which it keeps an internal pointer.
|
||||
@@ -229,6 +210,12 @@ void PropagateUploadFileCommon::start()
|
||||
}
|
||||
_item->_file = _item->_renameTarget;
|
||||
_item->_modtime = FileSystem::getModTime(newFilePathAbsolute);
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
slotOnErrorStartFolderUnlock(SyncFileItem::NormalError, tr("File %1 has invalid modified time. Do not upload to the server.").arg(QDir::toNativeSeparators(_item->_file)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SyncJournalFileRecord parentRec;
|
||||
@@ -331,6 +318,10 @@ void PropagateUploadFileCommon::slotComputeContentChecksum()
|
||||
// and not the _fileToUpload because we are checking the original file, not there
|
||||
// probably temporary one.
|
||||
_item->_modtime = FileSystem::getModTime(filePath);
|
||||
if (_item->_modtime <= 0) {
|
||||
slotOnErrorStartFolderUnlock(SyncFileItem::NormalError, tr("File %1 has invalid modified time. Do not upload to the server.").arg(QDir::toNativeSeparators(_item->_file)));
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray checksumType = propagator()->account()->capabilities().preferredUploadChecksumType();
|
||||
|
||||
@@ -402,11 +393,27 @@ void PropagateUploadFileCommon::slotStartUpload(const QByteArray &transmissionCh
|
||||
if (!FileSystem::fileExists(fullFilePath)) {
|
||||
return slotOnErrorStartFolderUnlock(SyncFileItem::SoftError, tr("File Removed (start upload) %1").arg(fullFilePath));
|
||||
}
|
||||
if (_item->_modtime <= 0) {
|
||||
slotOnErrorStartFolderUnlock(SyncFileItem::NormalError, tr("File %1 has invalid modified time. Do not upload to the server.").arg(QDir::toNativeSeparators(_item->_file)));
|
||||
return;
|
||||
}
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
time_t prevModtime = _item->_modtime; // the _item value was set in PropagateUploadFile::start()
|
||||
// but a potential checksum calculation could have taken some time during which the file could
|
||||
// have been changed again, so better check again here.
|
||||
|
||||
_item->_modtime = FileSystem::getModTime(originalFilePath);
|
||||
if (_item->_modtime <= 0) {
|
||||
slotOnErrorStartFolderUnlock(SyncFileItem::NormalError, tr("File %1 has invalid modified time. Do not upload to the server.").arg(QDir::toNativeSeparators(_item->_file)));
|
||||
return;
|
||||
}
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
if (prevModtime != _item->_modtime) {
|
||||
propagator()->_anotherSyncNeeded = true;
|
||||
qDebug() << "prevModtime" << prevModtime << "Curr" << _item->_modtime;
|
||||
@@ -604,6 +611,10 @@ void PropagateUploadFileCommon::startPollJob(const QString &path)
|
||||
info._file = _item->_file;
|
||||
info._url = path;
|
||||
info._modtime = _item->_modtime;
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
info._fileSize = _item->_size;
|
||||
propagator()->_journal->setPollInfo(info);
|
||||
propagator()->_journal->commit("add poll info");
|
||||
@@ -726,6 +737,10 @@ QMap<QByteArray, QByteArray> PropagateUploadFileCommon::headers()
|
||||
{
|
||||
QMap<QByteArray, QByteArray> headers;
|
||||
headers[QByteArrayLiteral("Content-Type")] = QByteArrayLiteral("application/octet-stream");
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
headers[QByteArrayLiteral("X-OC-Mtime")] = QByteArray::number(qint64(_item->_modtime));
|
||||
if (qEnvironmentVariableIntValue("OWNCLOUD_LAZYOPS"))
|
||||
headers[QByteArrayLiteral("OC-LazyOps")] = QByteArrayLiteral("true");
|
||||
|
||||
@@ -131,7 +131,7 @@ public:
|
||||
return _device;
|
||||
}
|
||||
|
||||
QString errorString()
|
||||
QString errorString() const override
|
||||
{
|
||||
return _errorString.isEmpty() ? AbstractNetworkJob::errorString() : _errorString;
|
||||
}
|
||||
|
||||
@@ -83,6 +83,10 @@ void PropagateUploadFileNG::doStartUpload()
|
||||
propagator()->_activeJobList.append(this);
|
||||
|
||||
const SyncJournalDb::UploadInfo progressInfo = propagator()->_journal->getUploadInfo(_item->_file);
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
if (progressInfo._valid && progressInfo.isChunked() && progressInfo._modtime == _item->_modtime
|
||||
&& progressInfo._size == _item->_size) {
|
||||
_transferId = progressInfo._transferid;
|
||||
@@ -229,6 +233,10 @@ void PropagateUploadFileNG::slotDeleteJobFinished()
|
||||
void PropagateUploadFileNG::startNewUpload()
|
||||
{
|
||||
ASSERT(propagator()->_activeJobList.count(this) == 1);
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
_transferId = uint(Utility::rand() ^ uint(_item->_modtime) ^ (uint(_fileToUpload._size) << 16) ^ qHash(_fileToUpload._file));
|
||||
_sent = 0;
|
||||
_currentChunk = 0;
|
||||
@@ -238,6 +246,10 @@ void PropagateUploadFileNG::startNewUpload()
|
||||
SyncJournalDb::UploadInfo pi;
|
||||
pi._valid = true;
|
||||
pi._transferid = _transferId;
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
pi._modtime = _item->_modtime;
|
||||
pi._contentChecksum = _item->_checksumHeader;
|
||||
pi._size = _item->_size;
|
||||
@@ -423,6 +435,10 @@ void PropagateUploadFileNG::slotPutFinished()
|
||||
}
|
||||
|
||||
// Check whether the file changed since discovery - this acts on the original file.
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
if (!FileSystem::verifyFileUnchanged(fullFilePath, _item->_size, _item->_modtime)) {
|
||||
propagator()->_anotherSyncNeeded = true;
|
||||
if (!_finished) {
|
||||
|
||||
@@ -39,10 +39,18 @@ void PropagateUploadFileV1::doStartUpload()
|
||||
{
|
||||
_chunkCount = int(std::ceil(_fileToUpload._size / double(chunkSize())));
|
||||
_startChunk = 0;
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
_transferId = uint(Utility::rand()) ^ uint(_item->_modtime) ^ (uint(_fileToUpload._size) << 16);
|
||||
|
||||
const SyncJournalDb::UploadInfo progressInfo = propagator()->_journal->getUploadInfo(_item->_file);
|
||||
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
if (progressInfo._valid && progressInfo.isChunked() && progressInfo._modtime == _item->_modtime && progressInfo._size == _item->_size
|
||||
&& (progressInfo._contentChecksum == _item->_checksumHeader || progressInfo._contentChecksum.isEmpty() || _item->_checksumHeader.isEmpty())) {
|
||||
_startChunk = progressInfo._chunk;
|
||||
@@ -56,6 +64,10 @@ void PropagateUploadFileV1::doStartUpload()
|
||||
pi._valid = true;
|
||||
pi._chunk = 0;
|
||||
pi._transferid = 0; // We set a null transfer id because it is not chunked.
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
pi._modtime = _item->_modtime;
|
||||
pi._errorCount = 0;
|
||||
pi._contentChecksum = _item->_checksumHeader;
|
||||
@@ -245,6 +257,10 @@ void PropagateUploadFileV1::slotPutFinished()
|
||||
}
|
||||
|
||||
// Check whether the file changed since discovery. the file check here is the original and not the temprary.
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
if (!FileSystem::verifyFileUnchanged(fullFilePath, _item->_size, _item->_modtime)) {
|
||||
propagator()->_anotherSyncNeeded = true;
|
||||
if (!_finished) {
|
||||
@@ -283,6 +299,10 @@ void PropagateUploadFileV1::slotPutFinished()
|
||||
}
|
||||
pi._chunk = (currentChunk + _startChunk + 1) % _chunkCount; // next chunk to start with
|
||||
pi._transferid = _transferId;
|
||||
Q_ASSERT(_item->_modtime > 0);
|
||||
if (_item->_modtime <= 0) {
|
||||
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||
}
|
||||
pi._modtime = _item->_modtime;
|
||||
pi._errorCount = 0; // successful chunk upload resets
|
||||
pi._contentChecksum = _item->_checksumHeader;
|
||||
|
||||
70
src/libsync/putmultifilejob.cpp
Normal file
70
src/libsync/putmultifilejob.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2021 (c) Matthieu Gallien <matthieu.gallien@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 "putmultifilejob.h"
|
||||
|
||||
#include <QHttpPart>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcPutMultiFileJob, "nextcloud.sync.networkjob.put.multi", QtInfoMsg)
|
||||
|
||||
PutMultiFileJob::~PutMultiFileJob() = default;
|
||||
|
||||
void PutMultiFileJob::start()
|
||||
{
|
||||
QNetworkRequest req;
|
||||
|
||||
for(auto &oneDevice : _devices) {
|
||||
auto onePart = QHttpPart{};
|
||||
|
||||
onePart.setBodyDevice(oneDevice._device.get());
|
||||
|
||||
for (QMap<QByteArray, QByteArray>::const_iterator it = oneDevice._headers.begin(); it != oneDevice._headers.end(); ++it) {
|
||||
onePart.setRawHeader(it.key(), it.value());
|
||||
}
|
||||
|
||||
req.setPriority(QNetworkRequest::LowPriority); // Long uploads must not block non-propagation jobs.
|
||||
|
||||
_body.append(onePart);
|
||||
}
|
||||
|
||||
sendRequest("POST", _url, req, &_body);
|
||||
|
||||
if (reply()->error() != QNetworkReply::NoError) {
|
||||
qCWarning(lcPutMultiFileJob) << " Network error: " << reply()->errorString();
|
||||
}
|
||||
|
||||
connect(reply(), &QNetworkReply::uploadProgress, this, &PutMultiFileJob::uploadProgress);
|
||||
connect(this, &AbstractNetworkJob::networkActivity, account().data(), &Account::propagatorNetworkActivity);
|
||||
_requestTimer.start();
|
||||
AbstractNetworkJob::start();
|
||||
}
|
||||
|
||||
bool PutMultiFileJob::finished()
|
||||
{
|
||||
for(const auto &oneDevice : _devices) {
|
||||
oneDevice._device->close();
|
||||
}
|
||||
|
||||
qCInfo(lcPutMultiFileJob) << "POST of" << reply()->request().url().toString() << path() << "FINISHED WITH STATUS"
|
||||
<< replyStatusString()
|
||||
<< reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute)
|
||||
<< reply()->attribute(QNetworkRequest::HttpReasonPhraseAttribute);
|
||||
|
||||
emit finishedSignal();
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
94
src/libsync/putmultifilejob.h
Normal file
94
src/libsync/putmultifilejob.h
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2021 (c) Matthieu Gallien <matthieu.gallien@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 "abstractnetworkjob.h"
|
||||
|
||||
#include "propagateupload.h"
|
||||
#include "account.h"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QByteArray>
|
||||
#include <QUrl>
|
||||
#include <QString>
|
||||
#include <QElapsedTimer>
|
||||
#include <QHttpMultiPart>
|
||||
#include <memory>
|
||||
|
||||
class QIODevice;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcPutMultiFileJob)
|
||||
|
||||
struct SingleUploadFileData
|
||||
{
|
||||
std::unique_ptr<UploadDevice> _device;
|
||||
QMap<QByteArray, QByteArray> _headers;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The PutMultiFileJob class
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class OWNCLOUDSYNC_EXPORT PutMultiFileJob : public AbstractNetworkJob
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PutMultiFileJob(AccountPtr account, const QUrl &url,
|
||||
std::vector<SingleUploadFileData> devices, QObject *parent = nullptr)
|
||||
: AbstractNetworkJob(account, {}, parent)
|
||||
, _devices(std::move(devices))
|
||||
, _url(url)
|
||||
{
|
||||
_body.setContentType(QHttpMultiPart::RelatedType);
|
||||
for(auto &singleDevice : _devices) {
|
||||
singleDevice._device->setParent(this);
|
||||
connect(this, &PutMultiFileJob::uploadProgress,
|
||||
singleDevice._device.get(), &UploadDevice::slotJobUploadProgress);
|
||||
}
|
||||
}
|
||||
|
||||
~PutMultiFileJob() override;
|
||||
|
||||
void start() override;
|
||||
|
||||
bool finished() override;
|
||||
|
||||
QString errorString() const override
|
||||
{
|
||||
return _errorString.isEmpty() ? AbstractNetworkJob::errorString() : _errorString;
|
||||
}
|
||||
|
||||
std::chrono::milliseconds msSinceStart() const
|
||||
{
|
||||
return std::chrono::milliseconds(_requestTimer.elapsed());
|
||||
}
|
||||
|
||||
signals:
|
||||
void finishedSignal();
|
||||
void uploadProgress(qint64, qint64);
|
||||
|
||||
private:
|
||||
QHttpMultiPart _body;
|
||||
std::vector<SingleUploadFileData> _devices;
|
||||
QString _errorString;
|
||||
QUrl _url;
|
||||
QElapsedTimer _requestTimer;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -711,7 +711,7 @@ void SyncEngine::slotDiscoveryFinished()
|
||||
_journal->commit(QStringLiteral("post treewalk"));
|
||||
|
||||
_propagator = QSharedPointer<OwncloudPropagator>(
|
||||
new OwncloudPropagator(_account, _localPath, _remotePath, _journal));
|
||||
new OwncloudPropagator(_account, _localPath, _remotePath, _journal, _bulkUploadBlackList));
|
||||
_propagator->setSyncOptions(_syncOptions);
|
||||
connect(_propagator.data(), &OwncloudPropagator::itemCompleted,
|
||||
this, &SyncEngine::slotItemCompleted);
|
||||
|
||||
@@ -241,6 +241,8 @@ private:
|
||||
QScopedPointer<DiscoveryPhase> _discoveryPhase;
|
||||
QSharedPointer<OwncloudPropagator> _propagator;
|
||||
|
||||
QSet<QString> _bulkUploadBlackList;
|
||||
|
||||
// List of all files with conflicts
|
||||
QSet<QString> _seenConflictFiles;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "common/utility.h"
|
||||
#include "common/filesystembase.h"
|
||||
#include "hydrationjob.h"
|
||||
#include "theme.h"
|
||||
#include "vfs_cfapi.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
@@ -40,6 +41,9 @@ Q_LOGGING_CATEGORY(lcCfApiWrapper, "nextcloud.sync.vfs.cfapi.wrapper", QtInfoMsg
|
||||
FIELD_SIZE( CF_OPERATION_PARAMETERS, field ) )
|
||||
|
||||
namespace {
|
||||
constexpr auto syncRootFlagsFull = 34;
|
||||
constexpr auto syncRootFlagsNoCfApiContextMenu = 2;
|
||||
|
||||
void cfApiSendTransferInfo(const CF_CONNECTION_KEY &connectionKey, const CF_TRANSFER_KEY &transferKey, NTSTATUS status, void *buffer, qint64 offset, qint64 currentBlockLength, qint64 totalLength)
|
||||
{
|
||||
|
||||
@@ -428,8 +432,10 @@ bool createSyncRootRegistryKeys(const QString &providerName, const QString &fold
|
||||
QVariant value;
|
||||
};
|
||||
|
||||
const auto flags = OCC::Theme::instance()->enforceVirtualFilesSyncFolder() ? syncRootFlagsNoCfApiContextMenu : syncRootFlagsFull;
|
||||
|
||||
const QVector<RegistryKeyInfo> registryKeysToSet = {
|
||||
{ providerSyncRootIdRegistryKey, QStringLiteral("Flags"), REG_DWORD, 34 },
|
||||
{ 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 }
|
||||
@@ -586,13 +592,18 @@ OCC::CfApiWrapper::FileHandle OCC::CfApiWrapper::handleForPath(const QString &pa
|
||||
return {};
|
||||
}
|
||||
|
||||
if (QFileInfo(path).isDir()) {
|
||||
QFileInfo pathFileInfo(path);
|
||||
if (!pathFileInfo.exists()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (pathFileInfo.isDir()) {
|
||||
HANDLE handle = nullptr;
|
||||
const qint64 openResult = CfOpenFileWithOplock(path.toStdWString().data(), CF_OPEN_FILE_FLAG_NONE, &handle);
|
||||
if (openResult == S_OK) {
|
||||
return {handle, [](HANDLE h) { CfCloseHandle(h); }};
|
||||
}
|
||||
} else {
|
||||
} else if (pathFileInfo.isFile()) {
|
||||
const auto longpath = OCC::FileSystem::longWinPath(path);
|
||||
const auto handle = CreateFile(longpath.toStdWString().data(), 0, 0, nullptr,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
@@ -618,7 +629,6 @@ OCC::CfApiWrapper::PlaceHolderInfo OCC::CfApiWrapper::findPlaceholderInfo(const
|
||||
if (result == S_OK) {
|
||||
return info;
|
||||
} else {
|
||||
qCWarning(lcCfApiWrapper()) << "Couldn't get placeholder info" << QString::fromWCharArray(_com_error(result).ErrorMessage());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -639,6 +649,10 @@ OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::se
|
||||
|
||||
OCC::Result<void, QString> OCC::CfApiWrapper::createPlaceholderInfo(const QString &path, time_t modtime, qint64 size, const QByteArray &fileId)
|
||||
{
|
||||
if (modtime <= 0) {
|
||||
return {QString{"Could not update metadata due to invalid modified time for %1: %2"}.arg(path).arg(modtime)};
|
||||
}
|
||||
|
||||
const auto fileInfo = QFileInfo(path);
|
||||
const auto localBasePath = QDir::toNativeSeparators(fileInfo.path()).toStdWString();
|
||||
const auto relativePath = fileInfo.fileName().toStdWString();
|
||||
@@ -687,6 +701,10 @@ OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::up
|
||||
{
|
||||
Q_ASSERT(handle);
|
||||
|
||||
if (modtime <= 0) {
|
||||
return {QString{"Could not update metadata due to invalid modified time for %1: %2"}.arg(pathForHandle(handle)).arg(modtime)};
|
||||
}
|
||||
|
||||
const auto info = replacesPath.isEmpty() ? findPlaceholderInfo(handle)
|
||||
: findPlaceholderInfo(handleForPath(replacesPath));
|
||||
if (!info) {
|
||||
@@ -703,13 +721,14 @@ OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::up
|
||||
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &metadata.BasicInfo.LastWriteTime);
|
||||
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &metadata.BasicInfo.LastAccessTime);
|
||||
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &metadata.BasicInfo.ChangeTime);
|
||||
metadata.BasicInfo.FileAttributes = 0;
|
||||
|
||||
const qint64 result = CfUpdatePlaceholder(handle.get(), &metadata,
|
||||
fileIdentity.data(), sizeToDWORD(fileIdentitySize),
|
||||
nullptr, 0, CF_UPDATE_FLAG_MARK_IN_SYNC, nullptr, nullptr);
|
||||
|
||||
if (result != S_OK) {
|
||||
qCWarning(lcCfApiWrapper) << "Couldn't update placeholder info for" << pathForHandle(handle) << ":" << QString::fromWCharArray(_com_error(result).ErrorMessage());
|
||||
qCWarning(lcCfApiWrapper) << "Couldn't update placeholder info for" << pathForHandle(handle) << ":" << QString::fromWCharArray(_com_error(result).ErrorMessage()) << replacesPath;
|
||||
return { "Couldn't update placeholder info" };
|
||||
}
|
||||
|
||||
@@ -721,6 +740,38 @@ OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::up
|
||||
return OCC::Vfs::ConvertToPlaceholderResult::Ok;
|
||||
}
|
||||
|
||||
OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::dehydratePlaceholder(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId)
|
||||
{
|
||||
Q_ASSERT(handle);
|
||||
|
||||
if (modtime <= 0) {
|
||||
return {QString{"Could not update metadata due to invalid modification time for %1: %2"}.arg(pathForHandle(handle)).arg(modtime)};
|
||||
}
|
||||
|
||||
const auto info = findPlaceholderInfo(handle);
|
||||
if (!info) {
|
||||
return { "Can't update non existing placeholder info" };
|
||||
}
|
||||
|
||||
const auto fileIdentity = QString::fromUtf8(fileId).toStdWString();
|
||||
const auto fileIdentitySize = (fileIdentity.length() + 1) * sizeof(wchar_t);
|
||||
|
||||
CF_FILE_RANGE dehydrationRange;
|
||||
dehydrationRange.StartingOffset.QuadPart = 0;
|
||||
dehydrationRange.Length.QuadPart = size;
|
||||
|
||||
const qint64 result = CfUpdatePlaceholder(handle.get(), nullptr,
|
||||
fileIdentity.data(), sizeToDWORD(fileIdentitySize),
|
||||
&dehydrationRange, 1, CF_UPDATE_FLAG_MARK_IN_SYNC, nullptr, nullptr);
|
||||
|
||||
if (result != S_OK) {
|
||||
qCWarning(lcCfApiWrapper) << "Couldn't update placeholder info for" << pathForHandle(handle) << ":" << QString::fromWCharArray(_com_error(result).ErrorMessage());
|
||||
return { "Couldn't update placeholder info" };
|
||||
}
|
||||
|
||||
return OCC::Vfs::ConvertToPlaceholderResult::Ok;
|
||||
}
|
||||
|
||||
OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::convertToPlaceholder(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath)
|
||||
{
|
||||
Q_UNUSED(modtime);
|
||||
|
||||
@@ -94,6 +94,7 @@ NEXTCLOUD_CFAPI_EXPORT Result<OCC::Vfs::ConvertToPlaceholderResult, QString> set
|
||||
NEXTCLOUD_CFAPI_EXPORT Result<void, QString> createPlaceholderInfo(const QString &path, time_t modtime, qint64 size, const QByteArray &fileId);
|
||||
NEXTCLOUD_CFAPI_EXPORT Result<OCC::Vfs::ConvertToPlaceholderResult, QString> updatePlaceholderInfo(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath = QString());
|
||||
NEXTCLOUD_CFAPI_EXPORT Result<OCC::Vfs::ConvertToPlaceholderResult, QString> convertToPlaceholder(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath);
|
||||
NEXTCLOUD_CFAPI_EXPORT Result<OCC::Vfs::ConvertToPlaceholderResult, QString> dehydratePlaceholder(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -132,26 +132,19 @@ Result<void, QString> VfsCfApi::createPlaceholder(const SyncFileItem &item)
|
||||
|
||||
Result<void, QString> VfsCfApi::dehydratePlaceholder(const SyncFileItem &item)
|
||||
{
|
||||
const auto previousPin = pinState(item._file);
|
||||
|
||||
if (!FileSystem::remove(_setupParams.filesystemPath + item._file)) {
|
||||
return QStringLiteral("Couldn't remove %1 to fulfill dehydration").arg(item._file);
|
||||
}
|
||||
|
||||
const auto r = createPlaceholder(item);
|
||||
if (!r) {
|
||||
return r;
|
||||
}
|
||||
|
||||
if (previousPin) {
|
||||
if (*previousPin == PinState::AlwaysLocal) {
|
||||
setPinState(item._file, PinState::Unspecified);
|
||||
const auto localPath = QDir::toNativeSeparators(_setupParams.filesystemPath + item._file);
|
||||
const auto handle = cfapi::handleForPath(localPath);
|
||||
if (handle) {
|
||||
auto result = cfapi::dehydratePlaceholder(handle, item._modtime, item._size, item._fileId);
|
||||
if (result) {
|
||||
return {};
|
||||
} else {
|
||||
setPinState(item._file, *previousPin);
|
||||
return result.error();
|
||||
}
|
||||
} else {
|
||||
qCWarning(lcCfApi) << "Couldn't update metadata for non existing file" << localPath;
|
||||
return {QStringLiteral("Couldn't update metadata")};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<Vfs::ConvertToPlaceholderResult, QString> VfsCfApi::convertToPlaceholder(const QString &filename, const SyncFileItem &item, const QString &replacesFile)
|
||||
@@ -160,6 +153,9 @@ Result<Vfs::ConvertToPlaceholderResult, QString> VfsCfApi::convertToPlaceholder(
|
||||
const auto replacesPath = QDir::toNativeSeparators(replacesFile);
|
||||
|
||||
const auto handle = cfapi::handleForPath(localPath);
|
||||
if (!handle) {
|
||||
return { "Invalid handle for path " + localPath };
|
||||
}
|
||||
if (cfapi::findPlaceholderInfo(handle)) {
|
||||
return cfapi::updatePlaceholderInfo(handle, item._modtime, item._size, item._fileId, replacesPath);
|
||||
} else {
|
||||
|
||||
@@ -68,12 +68,20 @@ bool VfsSuffix::isHydrating() const
|
||||
|
||||
Result<void, QString> VfsSuffix::updateMetadata(const QString &filePath, time_t modtime, qint64, const QByteArray &)
|
||||
{
|
||||
if (modtime <= 0) {
|
||||
return {tr("Error updating metadata due to invalid modified time")};
|
||||
}
|
||||
|
||||
FileSystem::setModTime(filePath, modtime);
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<void, QString> VfsSuffix::createPlaceholder(const SyncFileItem &item)
|
||||
{
|
||||
if (item._modtime <= 0) {
|
||||
return {tr("Error updating metadata due to invalid modified time")};
|
||||
}
|
||||
|
||||
// The concrete shape of the placeholder is also used in isDehydratedPlaceholder() below
|
||||
QString fn = _setupParams.filesystemPath + item._file;
|
||||
if (!fn.endsWith(fileSuffix())) {
|
||||
|
||||
@@ -69,12 +69,20 @@ bool VfsXAttr::isHydrating() const
|
||||
|
||||
Result<void, QString> VfsXAttr::updateMetadata(const QString &filePath, time_t modtime, qint64, const QByteArray &)
|
||||
{
|
||||
if (modtime <= 0) {
|
||||
return {tr("Error updating metadata due to invalid modified time")};
|
||||
}
|
||||
|
||||
FileSystem::setModTime(filePath, modtime);
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<void, QString> VfsXAttr::createPlaceholder(const SyncFileItem &item)
|
||||
{
|
||||
if (item._modtime <= 0) {
|
||||
return {tr("Error updating metadata due to invalid modified time")};
|
||||
}
|
||||
|
||||
const auto path = QString(_setupParams.filesystemPath + item._file);
|
||||
QFile file(path);
|
||||
if (file.exists() && file.size() > 1
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
]Icon\r*
|
||||
].DS_Store
|
||||
].ds_store
|
||||
*.textClipping
|
||||
._*
|
||||
]Thumbs.db
|
||||
]photothumb.db
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#include "httplogger.h"
|
||||
#include "accessmanager.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include <memory>
|
||||
|
||||
@@ -416,6 +420,109 @@ void FakePutReply::abort()
|
||||
emit finished();
|
||||
}
|
||||
|
||||
FakePutMultiFileReply::FakePutMultiFileReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QString &contentType, const QByteArray &putPayload, QObject *parent)
|
||||
: FakeReply { parent }
|
||||
{
|
||||
setRequest(request);
|
||||
setUrl(request.url());
|
||||
setOperation(op);
|
||||
open(QIODevice::ReadOnly);
|
||||
_allFileInfo = performMultiPart(remoteRootFileInfo, request, putPayload, contentType);
|
||||
QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
QVector<FileInfo *> FakePutMultiFileReply::performMultiPart(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload, const QString &contentType)
|
||||
{
|
||||
QVector<FileInfo *> result;
|
||||
|
||||
auto stringPutPayload = QString::fromUtf8(putPayload);
|
||||
constexpr int boundaryPosition = sizeof("multipart/related; boundary=");
|
||||
const QString boundaryValue = QStringLiteral("--") + contentType.mid(boundaryPosition, contentType.length() - boundaryPosition - 1) + QStringLiteral("\r\n");
|
||||
auto stringPutPayloadRef = QString{stringPutPayload}.left(stringPutPayload.size() - 2 - boundaryValue.size());
|
||||
auto allParts = stringPutPayloadRef.split(boundaryValue, Qt::SkipEmptyParts);
|
||||
for (const auto &onePart : allParts) {
|
||||
auto headerEndPosition = onePart.indexOf(QStringLiteral("\r\n\r\n"));
|
||||
auto onePartHeaderPart = onePart.left(headerEndPosition);
|
||||
auto onePartBody = onePart.mid(headerEndPosition + 4, onePart.size() - headerEndPosition - 6);
|
||||
auto onePartHeaders = onePartHeaderPart.split(QStringLiteral("\r\n"));
|
||||
QMap<QString, QString> allHeaders;
|
||||
for(auto oneHeader : onePartHeaders) {
|
||||
auto headerParts = oneHeader.split(QStringLiteral(": "));
|
||||
allHeaders[headerParts.at(0)] = headerParts.at(1);
|
||||
}
|
||||
auto fileName = allHeaders[QStringLiteral("X-File-Path")];
|
||||
Q_ASSERT(!fileName.isEmpty());
|
||||
FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
|
||||
if (fileInfo) {
|
||||
fileInfo->size = onePartBody.size();
|
||||
fileInfo->contentChar = onePartBody.at(0).toLatin1();
|
||||
} else {
|
||||
// Assume that the file is filled with the same character
|
||||
fileInfo = remoteRootFileInfo.create(fileName, onePartBody.size(), onePartBody.at(0).toLatin1());
|
||||
}
|
||||
fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong());
|
||||
remoteRootFileInfo.find(fileName, /*invalidateEtags=*/true);
|
||||
result.push_back(fileInfo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void FakePutMultiFileReply::respond()
|
||||
{
|
||||
QJsonDocument reply;
|
||||
QJsonObject allFileInfoReply;
|
||||
|
||||
qint64 totalSize = 0;
|
||||
std::for_each(_allFileInfo.begin(), _allFileInfo.end(), [&totalSize](const auto &fileInfo) {
|
||||
totalSize += fileInfo->size;
|
||||
});
|
||||
|
||||
for(auto fileInfo : qAsConst(_allFileInfo)) {
|
||||
QJsonObject fileInfoReply;
|
||||
fileInfoReply.insert("error", QStringLiteral("false"));
|
||||
fileInfoReply.insert("OC-OperationStatus", fileInfo->operationStatus);
|
||||
fileInfoReply.insert("X-File-Path", fileInfo->path());
|
||||
fileInfoReply.insert("OC-ETag", QLatin1String{fileInfo->etag});
|
||||
fileInfoReply.insert("ETag", QLatin1String{fileInfo->etag});
|
||||
fileInfoReply.insert("etag", QLatin1String{fileInfo->etag});
|
||||
fileInfoReply.insert("OC-FileID", QLatin1String{fileInfo->fileId});
|
||||
fileInfoReply.insert("X-OC-MTime", "accepted"); // Prevents Q_ASSERT(!_runningNow) since we'll call PropagateItemJob::done twice in that case.
|
||||
emit uploadProgress(fileInfo->size, totalSize);
|
||||
allFileInfoReply.insert(QChar('/') + fileInfo->path(), fileInfoReply);
|
||||
}
|
||||
reply.setObject(allFileInfoReply);
|
||||
_payload = reply.toJson();
|
||||
|
||||
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
|
||||
|
||||
setFinished(true);
|
||||
if (bytesAvailable()) {
|
||||
emit readyRead();
|
||||
}
|
||||
|
||||
emit metaDataChanged();
|
||||
emit finished();
|
||||
}
|
||||
|
||||
void FakePutMultiFileReply::abort()
|
||||
{
|
||||
setError(OperationCanceledError, QStringLiteral("abort"));
|
||||
emit finished();
|
||||
}
|
||||
|
||||
qint64 FakePutMultiFileReply::bytesAvailable() const
|
||||
{
|
||||
return _payload.size() + QIODevice::bytesAvailable();
|
||||
}
|
||||
|
||||
qint64 FakePutMultiFileReply::readData(char *data, qint64 maxlen)
|
||||
{
|
||||
qint64 len = std::min(qint64 { _payload.size() }, maxlen);
|
||||
std::copy(_payload.cbegin(), _payload.cbegin() + len, data);
|
||||
_payload.remove(0, static_cast<int>(len));
|
||||
return len;
|
||||
}
|
||||
|
||||
FakeMkcolReply::FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
|
||||
: FakeReply { parent }
|
||||
{
|
||||
@@ -813,43 +920,77 @@ FakeQNAM::FakeQNAM(FileInfo initialRoot)
|
||||
setCookieJar(new OCC::CookieJar);
|
||||
}
|
||||
|
||||
QJsonObject FakeQNAM::forEachReplyPart(QIODevice *outgoingData,
|
||||
const QString &contentType,
|
||||
std::function<QJsonObject (const QMap<QString, QByteArray> &)> replyFunction)
|
||||
{
|
||||
auto fullReply = QJsonObject{};
|
||||
auto putPayload = outgoingData->peek(outgoingData->bytesAvailable());
|
||||
outgoingData->reset();
|
||||
auto stringPutPayload = QString::fromUtf8(putPayload);
|
||||
constexpr int boundaryPosition = sizeof("multipart/related; boundary=");
|
||||
const QString boundaryValue = QStringLiteral("--") + contentType.mid(boundaryPosition, contentType.length() - boundaryPosition - 1) + QStringLiteral("\r\n");
|
||||
auto stringPutPayloadRef = QString{stringPutPayload}.left(stringPutPayload.size() - 2 - boundaryValue.size());
|
||||
auto allParts = stringPutPayloadRef.split(boundaryValue, Qt::SkipEmptyParts);
|
||||
for (const auto &onePart : qAsConst(allParts)) {
|
||||
auto headerEndPosition = onePart.indexOf(QStringLiteral("\r\n\r\n"));
|
||||
auto onePartHeaderPart = onePart.left(headerEndPosition);
|
||||
auto onePartHeaders = onePartHeaderPart.split(QStringLiteral("\r\n"));
|
||||
QMap<QString, QByteArray> allHeaders;
|
||||
for(const auto &oneHeader : qAsConst(onePartHeaders)) {
|
||||
auto headerParts = oneHeader.split(QStringLiteral(": "));
|
||||
allHeaders[headerParts.at(0)] = headerParts.at(1).toLatin1();
|
||||
}
|
||||
|
||||
auto reply = replyFunction(allHeaders);
|
||||
if (reply.contains(QStringLiteral("error")) &&
|
||||
reply.contains(QStringLiteral("etag"))) {
|
||||
fullReply.insert(allHeaders[QStringLiteral("X-File-Path")], reply);
|
||||
}
|
||||
}
|
||||
|
||||
return fullReply;
|
||||
}
|
||||
|
||||
QNetworkReply *FakeQNAM::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData)
|
||||
{
|
||||
QNetworkReply *reply = nullptr;
|
||||
auto newRequest = request;
|
||||
newRequest.setRawHeader("X-Request-ID", OCC::AccessManager::generateRequestId());
|
||||
auto contentType = request.header(QNetworkRequest::ContentTypeHeader).toString();
|
||||
if (_override) {
|
||||
if (auto _reply = _override(op, newRequest, outgoingData)) {
|
||||
reply = _reply;
|
||||
}
|
||||
}
|
||||
if (!reply) {
|
||||
const QString fileName = getFilePathFromUrl(newRequest.url());
|
||||
Q_ASSERT(!fileName.isNull());
|
||||
if (_errorPaths.contains(fileName)) {
|
||||
reply = new FakeErrorReply { op, newRequest, this, _errorPaths[fileName] };
|
||||
}
|
||||
reply = overrideReplyWithError(getFilePathFromUrl(newRequest.url()), op, newRequest);
|
||||
}
|
||||
if (!reply) { const bool isUpload = newRequest.url().path().startsWith(sUploadUrl.path());
|
||||
if (!reply) {
|
||||
const bool isUpload = newRequest.url().path().startsWith(sUploadUrl.path());
|
||||
FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo;
|
||||
|
||||
auto verb = newRequest.attribute(QNetworkRequest::CustomVerbAttribute);
|
||||
if (verb == QLatin1String("PROPFIND"))
|
||||
if (verb == QLatin1String("PROPFIND")) {
|
||||
// Ignore outgoingData always returning somethign good enough, works for now.
|
||||
reply = new FakePropfindReply { info, op, newRequest, this };
|
||||
else if (verb == QLatin1String("GET") || op == QNetworkAccessManager::GetOperation)
|
||||
} else if (verb == QLatin1String("GET") || op == QNetworkAccessManager::GetOperation) {
|
||||
reply = new FakeGetReply { info, op, newRequest, this };
|
||||
else if (verb == QLatin1String("PUT") || op == QNetworkAccessManager::PutOperation)
|
||||
} else if (verb == QLatin1String("PUT") || op == QNetworkAccessManager::PutOperation) {
|
||||
reply = new FakePutReply { info, op, newRequest, outgoingData->readAll(), this };
|
||||
else if (verb == QLatin1String("MKCOL"))
|
||||
} else if (verb == QLatin1String("MKCOL")) {
|
||||
reply = new FakeMkcolReply { info, op, newRequest, this };
|
||||
else if (verb == QLatin1String("DELETE") || op == QNetworkAccessManager::DeleteOperation)
|
||||
} else if (verb == QLatin1String("DELETE") || op == QNetworkAccessManager::DeleteOperation) {
|
||||
reply = new FakeDeleteReply { info, op, newRequest, this };
|
||||
else if (verb == QLatin1String("MOVE") && !isUpload)
|
||||
} else if (verb == QLatin1String("MOVE") && !isUpload) {
|
||||
reply = new FakeMoveReply { info, op, newRequest, this };
|
||||
else if (verb == QLatin1String("MOVE") && isUpload)
|
||||
} else if (verb == QLatin1String("MOVE") && isUpload) {
|
||||
reply = new FakeChunkMoveReply { info, _remoteRootFileInfo, op, newRequest, this };
|
||||
else {
|
||||
} else if (verb == QLatin1String("POST") || op == QNetworkAccessManager::PostOperation) {
|
||||
if (contentType.startsWith(QStringLiteral("multipart/related; boundary="))) {
|
||||
reply = new FakePutMultiFileReply { info, op, newRequest, contentType, outgoingData->readAll(), this };
|
||||
}
|
||||
} else {
|
||||
qDebug() << verb << outgoingData;
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
@@ -858,6 +999,18 @@ QNetworkReply *FakeQNAM::createRequest(QNetworkAccessManager::Operation op, cons
|
||||
return reply;
|
||||
}
|
||||
|
||||
QNetworkReply * FakeQNAM::overrideReplyWithError(QString fileName, QNetworkAccessManager::Operation op, QNetworkRequest newRequest)
|
||||
{
|
||||
QNetworkReply *reply = nullptr;
|
||||
|
||||
Q_ASSERT(!fileName.isNull());
|
||||
if (_errorPaths.contains(fileName)) {
|
||||
reply = new FakeErrorReply { op, newRequest, this, _errorPaths[fileName] };
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
FakeFolder::FakeFolder(const FileInfo &fileTemplate, const OCC::Optional<FileInfo> &localFileInfo, const QString &remotePath)
|
||||
: _localModifier(_tempDir.path())
|
||||
{
|
||||
@@ -1079,3 +1232,12 @@ FakeReply::FakeReply(QObject *parent)
|
||||
}
|
||||
|
||||
FakeReply::~FakeReply() = default;
|
||||
|
||||
FakeJsonErrorReply::FakeJsonErrorReply(QNetworkAccessManager::Operation op,
|
||||
const QNetworkRequest &request,
|
||||
QObject *parent,
|
||||
int httpErrorCode,
|
||||
const QJsonDocument &reply)
|
||||
: FakeErrorReply{ op, request, parent, httpErrorCode, reply.toJson() }
|
||||
{
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
#include <cookiejar.h>
|
||||
#include <QTimer>
|
||||
|
||||
class QJsonDocument;
|
||||
|
||||
/*
|
||||
* TODO: In theory we should use QVERIFY instead of Q_ASSERT for testing, but this
|
||||
* only works when directly called from a QTest :-(
|
||||
@@ -148,6 +150,7 @@ public:
|
||||
void fixupParentPathRecursively();
|
||||
|
||||
QString name;
|
||||
int operationStatus = 200;
|
||||
bool isDir = true;
|
||||
bool isShared = false;
|
||||
OCC::RemotePermissions permissions; // When uset, defaults to everything
|
||||
@@ -214,6 +217,27 @@ public:
|
||||
qint64 readData(char *, qint64) override { return 0; }
|
||||
};
|
||||
|
||||
class FakePutMultiFileReply : public FakeReply
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
FakePutMultiFileReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QString &contentType, const QByteArray &putPayload, QObject *parent);
|
||||
|
||||
static QVector<FileInfo *> performMultiPart(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload, const QString &contentType);
|
||||
|
||||
Q_INVOKABLE virtual void respond();
|
||||
|
||||
void abort() override;
|
||||
|
||||
qint64 bytesAvailable() const override;
|
||||
qint64 readData(char *data, qint64 maxlen) override;
|
||||
|
||||
private:
|
||||
QVector<FileInfo *> _allFileInfo;
|
||||
|
||||
QByteArray _payload;
|
||||
};
|
||||
|
||||
class FakeMkcolReply : public FakeReply
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -354,6 +378,17 @@ public:
|
||||
QByteArray _body;
|
||||
};
|
||||
|
||||
class FakeJsonErrorReply : public FakeErrorReply
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
FakeJsonErrorReply(QNetworkAccessManager::Operation op,
|
||||
const QNetworkRequest &request,
|
||||
QObject *parent,
|
||||
int httpErrorCode,
|
||||
const QJsonDocument &reply = QJsonDocument());
|
||||
};
|
||||
|
||||
// A reply that never responds
|
||||
class FakeHangingReply : public FakeReply
|
||||
{
|
||||
@@ -409,6 +444,12 @@ public:
|
||||
|
||||
void setOverride(const Override &override) { _override = override; }
|
||||
|
||||
QJsonObject forEachReplyPart(QIODevice *outgoingData,
|
||||
const QString &contentType,
|
||||
std::function<QJsonObject(const QMap<QString, QByteArray> &)> replyFunction);
|
||||
|
||||
QNetworkReply *overrideReplyWithError(QString fileName, Operation op, QNetworkRequest newRequest);
|
||||
|
||||
protected:
|
||||
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
|
||||
QIODevice *outgoingData = nullptr) override;
|
||||
@@ -467,6 +508,11 @@ public:
|
||||
};
|
||||
ErrorList serverErrorPaths() { return {_fakeQnam}; }
|
||||
void setServerOverride(const FakeQNAM::Override &override) { _fakeQnam->setOverride(override); }
|
||||
QJsonObject forEachReplyPart(QIODevice *outgoingData,
|
||||
const QString &contentType,
|
||||
std::function<QJsonObject(const QMap<QString, QByteArray>&)> replyFunction) {
|
||||
return _fakeQnam->forEachReplyPart(outgoingData, contentType, replyFunction);
|
||||
}
|
||||
|
||||
QString localPath() const;
|
||||
|
||||
|
||||
@@ -243,6 +243,20 @@ private slots:
|
||||
|
||||
QCOMPARE(defaultSharePermissionsAvailable, 31);
|
||||
}
|
||||
|
||||
void testBulkUploadAvailable_bulkUploadAvailable_returnTrue()
|
||||
{
|
||||
QVariantMap bulkuploadMap;
|
||||
bulkuploadMap["bulkupload"] = "1.0";
|
||||
|
||||
QVariantMap capabilitiesMap;
|
||||
capabilitiesMap["dav"] = bulkuploadMap;
|
||||
|
||||
const auto &capabilities = OCC::Capabilities(capabilitiesMap);
|
||||
const auto bulkuploadAvailable = capabilities.bulkUpload();
|
||||
|
||||
QCOMPARE(bulkuploadAvailable, true);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(TestCapabilities)
|
||||
|
||||
@@ -212,10 +212,16 @@ private slots:
|
||||
const QString fileWithSpaces1(" foo");
|
||||
const QString fileWithSpaces2(" bar ");
|
||||
const QString fileWithSpaces3("bla ");
|
||||
const QString fileWithSpaces4("A/ foo");
|
||||
const QString fileWithSpaces5("A/ bar ");
|
||||
const QString fileWithSpaces6("A/bla ");
|
||||
|
||||
fakeFolder.localModifier().insert(fileWithSpaces1);
|
||||
fakeFolder.localModifier().insert(fileWithSpaces2);
|
||||
fakeFolder.localModifier().insert(fileWithSpaces3);
|
||||
fakeFolder.localModifier().insert(fileWithSpaces4);
|
||||
fakeFolder.localModifier().insert(fileWithSpaces5);
|
||||
fakeFolder.localModifier().insert(fileWithSpaces6);
|
||||
|
||||
QVERIFY(fakeFolder.syncOnce());
|
||||
|
||||
@@ -233,6 +239,21 @@ private slots:
|
||||
QVERIFY(!fakeFolder.currentRemoteState().find(fileWithSpaces3));
|
||||
QVERIFY(fakeFolder.currentLocalState().find(fileWithSpaces3.trimmed()));
|
||||
QVERIFY(!fakeFolder.currentLocalState().find(fileWithSpaces3));
|
||||
|
||||
QVERIFY(fakeFolder.currentRemoteState().find("A/foo"));
|
||||
QVERIFY(!fakeFolder.currentRemoteState().find(fileWithSpaces4));
|
||||
QVERIFY(fakeFolder.currentLocalState().find("A/foo"));
|
||||
QVERIFY(!fakeFolder.currentLocalState().find(fileWithSpaces4));
|
||||
|
||||
QVERIFY(fakeFolder.currentRemoteState().find("A/bar"));
|
||||
QVERIFY(!fakeFolder.currentRemoteState().find(fileWithSpaces5));
|
||||
QVERIFY(fakeFolder.currentLocalState().find("A/bar"));
|
||||
QVERIFY(!fakeFolder.currentLocalState().find(fileWithSpaces5));
|
||||
|
||||
QVERIFY(fakeFolder.currentRemoteState().find("A/bla"));
|
||||
QVERIFY(!fakeFolder.currentRemoteState().find(fileWithSpaces6));
|
||||
QVERIFY(fakeFolder.currentLocalState().find("A/bla"));
|
||||
QVERIFY(!fakeFolder.currentLocalState().find(fileWithSpaces6));
|
||||
}
|
||||
|
||||
void testCreateFileWithTrailingSpaces_localTrimmedDoesExist_dontRenameAndUploadFile()
|
||||
|
||||
@@ -41,6 +41,18 @@ bool itemDidCompleteSuccessfullyWithExpectedRank(const ItemCompletedSpy &spy, co
|
||||
return false;
|
||||
}
|
||||
|
||||
int itemSuccessfullyCompletedGetRank(const ItemCompletedSpy &spy, const QString &path)
|
||||
{
|
||||
auto itItem = std::find_if(spy.begin(), spy.end(), [&path] (auto currentItem) {
|
||||
auto item = currentItem[0].template value<OCC::SyncFileItemPtr>();
|
||||
return item->destination() == path;
|
||||
});
|
||||
if (itItem != spy.end()) {
|
||||
return itItem - spy.begin();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
class TestSyncEngine : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -92,6 +104,8 @@ private slots:
|
||||
|
||||
void testDirUploadWithDelayedAlgorithm() {
|
||||
FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
|
||||
fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"bulkupload", "1.0"} } } });
|
||||
|
||||
ItemCompletedSpy completeSpy(fakeFolder);
|
||||
fakeFolder.localModifier().mkdir("Y");
|
||||
fakeFolder.localModifier().insert("Y/d0");
|
||||
@@ -104,12 +118,18 @@ private slots:
|
||||
fakeFolder.syncOnce();
|
||||
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Y", 0));
|
||||
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Z", 1));
|
||||
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Y/d0", 2));
|
||||
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Z/d0", 3));
|
||||
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "A/a0", 4));
|
||||
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "B/b0", 5));
|
||||
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "r0", 6));
|
||||
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "r1", 7));
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Y/d0"));
|
||||
QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "Y/d0") > 1);
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z/d0"));
|
||||
QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "Z/d0") > 1);
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0"));
|
||||
QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "A/a0") > 1);
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "B/b0"));
|
||||
QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "B/b0") > 1);
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "r0"));
|
||||
QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "r0") > 1);
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "r1"));
|
||||
QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "r1") > 1);
|
||||
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
||||
}
|
||||
|
||||
@@ -490,7 +510,9 @@ private slots:
|
||||
int remoteQuota = 1000;
|
||||
int n507 = 0, nPUT = 0;
|
||||
QObject parent;
|
||||
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
|
||||
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
|
||||
Q_UNUSED(outgoingData)
|
||||
|
||||
if (op == QNetworkAccessManager::PutOperation) {
|
||||
nPUT++;
|
||||
if (request.rawHeader("OC-Total-Length").toInt() > remoteQuota) {
|
||||
@@ -776,6 +798,95 @@ private slots:
|
||||
|
||||
QCOMPARE(QFileInfo(fakeFolder.localPath() + "foo").lastModified(), datetime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether subsequent large uploads are skipped after a 507 error
|
||||
*/
|
||||
void testErrorsWithBulkUpload()
|
||||
{
|
||||
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
|
||||
fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"bulkupload", "1.0"} } } });
|
||||
|
||||
// Disable parallel uploads
|
||||
SyncOptions syncOptions;
|
||||
syncOptions._parallelNetworkJobs = 0;
|
||||
fakeFolder.syncEngine().setSyncOptions(syncOptions);
|
||||
|
||||
int nPUT = 0;
|
||||
int nPOST = 0;
|
||||
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
|
||||
auto contentType = request.header(QNetworkRequest::ContentTypeHeader).toString();
|
||||
if (op == QNetworkAccessManager::PostOperation) {
|
||||
++nPOST;
|
||||
if (contentType.startsWith(QStringLiteral("multipart/related; boundary="))) {
|
||||
auto jsonReplyObject = fakeFolder.forEachReplyPart(outgoingData, contentType, [] (const QMap<QString, QByteArray> &allHeaders) -> QJsonObject {
|
||||
auto reply = QJsonObject{};
|
||||
const auto fileName = allHeaders[QStringLiteral("X-File-Path")];
|
||||
if (fileName.endsWith("A/big2") ||
|
||||
fileName.endsWith("A/big3") ||
|
||||
fileName.endsWith("A/big4") ||
|
||||
fileName.endsWith("A/big5") ||
|
||||
fileName.endsWith("A/big7") ||
|
||||
fileName.endsWith("B/big8")) {
|
||||
reply.insert(QStringLiteral("error"), true);
|
||||
reply.insert(QStringLiteral("etag"), {});
|
||||
return reply;
|
||||
} else {
|
||||
reply.insert(QStringLiteral("error"), false);
|
||||
reply.insert(QStringLiteral("etag"), {});
|
||||
}
|
||||
return reply;
|
||||
});
|
||||
if (jsonReplyObject.size()) {
|
||||
auto jsonReply = QJsonDocument{};
|
||||
jsonReply.setObject(jsonReplyObject);
|
||||
return new FakeJsonErrorReply{op, request, this, 200, jsonReply};
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
} else if (op == QNetworkAccessManager::PutOperation) {
|
||||
++nPUT;
|
||||
const auto fileName = getFilePathFromUrl(request.url());
|
||||
if (fileName.endsWith("A/big2") ||
|
||||
fileName.endsWith("A/big3") ||
|
||||
fileName.endsWith("A/big4") ||
|
||||
fileName.endsWith("A/big5") ||
|
||||
fileName.endsWith("A/big7") ||
|
||||
fileName.endsWith("B/big8")) {
|
||||
return new FakeErrorReply(op, request, this, 412);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
});
|
||||
|
||||
fakeFolder.localModifier().insert("A/big", 1);
|
||||
QVERIFY(fakeFolder.syncOnce());
|
||||
QCOMPARE(nPUT, 0);
|
||||
QCOMPARE(nPOST, 1);
|
||||
nPUT = 0;
|
||||
nPOST = 0;
|
||||
|
||||
fakeFolder.localModifier().insert("A/big1", 1); // ok
|
||||
fakeFolder.localModifier().insert("A/big2", 1); // ko
|
||||
fakeFolder.localModifier().insert("A/big3", 1); // ko
|
||||
fakeFolder.localModifier().insert("A/big4", 1); // ko
|
||||
fakeFolder.localModifier().insert("A/big5", 1); // ko
|
||||
fakeFolder.localModifier().insert("A/big6", 1); // ok
|
||||
fakeFolder.localModifier().insert("A/big7", 1); // ko
|
||||
fakeFolder.localModifier().insert("A/big8", 1); // ok
|
||||
fakeFolder.localModifier().insert("B/big8", 1); // ko
|
||||
|
||||
QVERIFY(!fakeFolder.syncOnce());
|
||||
QCOMPARE(nPUT, 0);
|
||||
QCOMPARE(nPOST, 1);
|
||||
nPUT = 0;
|
||||
nPOST = 0;
|
||||
|
||||
QVERIFY(!fakeFolder.syncOnce());
|
||||
QCOMPARE(nPUT, 6);
|
||||
QCOMPARE(nPOST, 0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(TestSyncEngine)
|
||||
|
||||
278
theme/colored/icons/Nextcloud-icon-win-folder.svg
Normal file
278
theme/colored/icons/Nextcloud-icon-win-folder.svg
Normal file
@@ -0,0 +1,278 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Ebene_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 300 300"
|
||||
style="enable-background:new 0 0 300 300;"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="Windows_Folder.svg"
|
||||
inkscape:version="1.1.1 (1:1.1+202109281949+c3084ef5ed)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs74">
|
||||
|
||||
|
||||
|
||||
<linearGradient
|
||||
id="Pfad_499_00000161620552563728473090000003454445015337385602_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="3.5778999"
|
||||
y1="299.08331"
|
||||
x2="3.5778999"
|
||||
y2="297.29568"
|
||||
gradientTransform="matrix(72,0,0,-72,49,21673)">
|
||||
<stop
|
||||
offset="0"
|
||||
style="stop-color:#FE319A"
|
||||
id="stop59" />
|
||||
<stop
|
||||
offset="1"
|
||||
style="stop-color:#E20074"
|
||||
id="stop61" />
|
||||
</linearGradient>
|
||||
|
||||
|
||||
|
||||
|
||||
<linearGradient
|
||||
gradientTransform="matrix(0.9909162,0,0,0.99091623,283.08824,126.10294)"
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient857"
|
||||
id="linearGradient1192"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="18.230097"
|
||||
y1="150"
|
||||
x2="150.00002"
|
||||
y2="-7.6293945e-06" /><linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient857"><stop
|
||||
style="stop-color:#0082c9;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop853" /><stop
|
||||
style="stop-color:#1cafff;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop855" /></linearGradient></defs><sodipodi:namedview
|
||||
id="namedview72"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.9233304"
|
||||
inkscape:cx="123.48372"
|
||||
inkscape:cy="174.17704"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1046"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="34"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Ebene_1" />
|
||||
<style
|
||||
type="text/css"
|
||||
id="style2">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
.st1{fill:url(#SVGID_00000073698049827524260440000006626317865822427277_);}
|
||||
.st2{fill:url(#SVGID_00000058549202148666045310000006535695705354523308_);}
|
||||
.st3{fill:url(#SVGID_00000135661076229880923210000014324535628046543800_);}
|
||||
.st4{fill:url(#SVGID_00000090993352524005697930000005123350809455339168_);}
|
||||
.st5{fill:#FDE07F;}
|
||||
.st6{fill:#FFEEB6;}
|
||||
.st7{clip-path:url(#SVGID_00000027581996385575992730000014613207578918336175_);}
|
||||
.st8{fill:url(#Pfad_499_00000154407742523961947240000000935762443450757262_);}
|
||||
.st9{fill:#FFFFFF;}
|
||||
.st10{fill:#F2F2F2;}
|
||||
</style>
|
||||
<g
|
||||
id="g51">
|
||||
<linearGradient
|
||||
id="SVGID_1_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="224.238"
|
||||
y1="251.2621"
|
||||
x2="235.5521"
|
||||
y2="229.8477">
|
||||
<stop
|
||||
offset="0"
|
||||
style="stop-color:#E5E5E5"
|
||||
id="stop4" />
|
||||
<stop
|
||||
offset="1"
|
||||
style="stop-color:#F2F2F2;stop-opacity:0"
|
||||
id="stop6" />
|
||||
</linearGradient>
|
||||
<polygon
|
||||
class="st0"
|
||||
points="221,245 251,236 221,222 "
|
||||
id="polygon9" />
|
||||
|
||||
<linearGradient
|
||||
id="SVGID_00000168800218275837730680000003462325949068373151_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="108.238"
|
||||
y1="292.2621"
|
||||
x2="119.5521"
|
||||
y2="270.8477">
|
||||
<stop
|
||||
offset="0"
|
||||
style="stop-color:#E5E5E5"
|
||||
id="stop11" />
|
||||
<stop
|
||||
offset="1"
|
||||
style="stop-color:#F2F2F2;stop-opacity:0"
|
||||
id="stop13" />
|
||||
</linearGradient>
|
||||
<polygon
|
||||
style="fill:url(#SVGID_00000168800218275837730680000003462325949068373151_);"
|
||||
points="105,286 135,277 105,263 "
|
||||
id="polygon16" />
|
||||
|
||||
<linearGradient
|
||||
id="SVGID_00000003802560936691988940000016049607085827979708_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="51.9582"
|
||||
y1="185.0925"
|
||||
x2="200.6586"
|
||||
y2="104.7857">
|
||||
<stop
|
||||
offset="0.3193"
|
||||
style="stop-color:#EBC863"
|
||||
id="stop18" />
|
||||
<stop
|
||||
offset="1"
|
||||
style="stop-color:#FEE182"
|
||||
id="stop20" />
|
||||
</linearGradient>
|
||||
<path
|
||||
style="fill:url(#SVGID_00000003802560936691988940000016049607085827979708_);"
|
||||
d="M219,246H56c-1.7,0-3-1.3-3-3V33h151 c1.7,0,3,1.3,3,3v125.2c0,0.8,0.3,1.5,0.9,2.1l13.3,13.6c0.5,0.6,0.9,1.3,0.9,2.1V243C222,244.7,220.7,246,219,246z"
|
||||
id="path23" />
|
||||
<g
|
||||
id="g39">
|
||||
|
||||
<linearGradient
|
||||
id="SVGID_00000088830071281327557650000012927193665735863469_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="81.921"
|
||||
y1="116.6131"
|
||||
x2="83.1607"
|
||||
y2="222.7924">
|
||||
<stop
|
||||
offset="0"
|
||||
style="stop-color:#FFEFBC"
|
||||
id="stop25" />
|
||||
<stop
|
||||
offset="1"
|
||||
style="stop-color:#FFE495"
|
||||
id="stop27" />
|
||||
</linearGradient>
|
||||
<path
|
||||
style="fill:url(#SVGID_00000088830071281327557650000012927193665735863469_);"
|
||||
d="M103.5,286L54,246V33l55.9,44.1 c0.7,0.6,1.1,1.4,1.1,2.4V211l-5,7v66.9C106,286.1,104.5,286.8,103.5,286z"
|
||||
id="path30" />
|
||||
|
||||
<linearGradient
|
||||
id="SVGID_00000075125985038499810010000013334681634146325900_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="58.9558"
|
||||
y1="126.3097"
|
||||
x2="117.1869"
|
||||
y2="218.9498">
|
||||
<stop
|
||||
offset="0"
|
||||
style="stop-color:#FFEAA5"
|
||||
id="stop32" />
|
||||
<stop
|
||||
offset="1"
|
||||
style="stop-color:#FFE086"
|
||||
id="stop34" />
|
||||
</linearGradient>
|
||||
<path
|
||||
style="fill:url(#SVGID_00000075125985038499810010000013334681634146325900_);"
|
||||
d="M102.5,285.7l-49.5-40V33l55.8,44 c0.7,0.6,1.1,1.4,1.1,2.4v131.4l-5,7v66.8C104.9,285.8,103.5,286.5,102.5,285.7z"
|
||||
id="path37" />
|
||||
</g>
|
||||
<g
|
||||
id="g49">
|
||||
<rect
|
||||
x="60"
|
||||
y="46"
|
||||
class="st5"
|
||||
width="1"
|
||||
height="200"
|
||||
id="rect41" />
|
||||
<rect
|
||||
x="56.5"
|
||||
y="46"
|
||||
class="st5"
|
||||
width="1"
|
||||
height="200"
|
||||
id="rect43" />
|
||||
<rect
|
||||
x="57.5"
|
||||
y="46"
|
||||
class="st6"
|
||||
width="1"
|
||||
height="200"
|
||||
id="rect45" />
|
||||
<rect
|
||||
x="61"
|
||||
y="46"
|
||||
class="st6"
|
||||
width="1"
|
||||
height="200"
|
||||
id="rect47" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g69"
|
||||
transform="translate(0,-100)">
|
||||
<defs
|
||||
id="defs54">
|
||||
<rect
|
||||
id="SVGID_00000093176395783954265780000012512996052019405743_"
|
||||
x="122.3"
|
||||
y="139"
|
||||
width="128.7"
|
||||
height="128.7" />
|
||||
</defs>
|
||||
<clipPath
|
||||
id="SVGID_00000106127748250350940740000017471206982484828042_">
|
||||
<use
|
||||
xlink:href="#SVGID_00000093176395783954265780000012512996052019405743_"
|
||||
style="overflow:visible"
|
||||
id="use56"
|
||||
x="0"
|
||||
y="0"
|
||||
width="100%"
|
||||
height="100%" />
|
||||
</clipPath>
|
||||
|
||||
<g
|
||||
id="g925"
|
||||
transform="matrix(0.81518987,0,0,0.81518987,-44.170655,200.60216)"><circle
|
||||
r="79"
|
||||
cy="126.10294"
|
||||
cx="283.08823"
|
||||
id="circle1050"
|
||||
style="fill:url(#linearGradient1192);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0990916;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
inkscape:export-filename="nextcloud-icon.png"
|
||||
inkscape:export-xdpi="300"
|
||||
inkscape:export-ydpi="300" /><path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path1052"
|
||||
d="m 283.18965,95.813064 c -13.79313,0 -25.48394,9.350826 -29.10731,22.020146 -3.14909,-6.72 -9.97328,-11.42792 -17.83356,-11.42792 -10.80973,0 -19.69562,8.88589 -19.69562,19.69562 0,10.80972 8.88589,19.69967 19.69562,19.69967 7.86028,0 14.68447,-4.7108 17.83356,-11.43198 3.62337,12.67028 15.31418,22.02422 29.10731,22.02422 13.69111,0 25.32232,-9.21286 29.03837,-21.74023 3.20726,6.56859 9.94698,11.14799 17.69562,11.14799 10.80974,0 19.69967,-8.88995 19.69967,-19.69967 0,-10.80973 -8.88993,-19.69562 -19.69967,-19.69562 -7.74864,0 -14.48836,4.57653 -17.69562,11.14395 -3.71605,-12.5264 -15.34726,-21.736176 -29.03837,-21.736176 z m 0,11.561796 c 10.41225,0 18.73011,8.31383 18.73011,18.72605 0,10.41221 -8.31786,18.73011 -18.73011,18.73011 -10.41219,0 -18.72603,-8.3179 -18.72603,-18.73011 0,-10.41222 8.31384,-18.72605 18.72603,-18.72605 z m -46.94087,10.59222 c 4.56183,0 8.13788,3.57198 8.13788,8.13383 0,4.56184 -3.57605,8.13787 -8.13788,8.13787 -4.56183,0 -8.13385,-3.57603 -8.13385,-8.13787 0,-4.56185 3.57202,-8.13383 8.13385,-8.13383 z m 93.67486,0 c 4.56187,0 8.1379,3.57198 8.1379,8.13383 0,4.56184 -3.57605,8.13787 -8.1379,8.13787 -4.56181,0 -8.13381,-3.57603 -8.13381,-8.13787 0,-4.56185 3.57201,-8.13383 8.13381,-8.13383 z"
|
||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.50314;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
inkscape:export-filename="Nextcloud Hub logo variants.png"
|
||||
inkscape:export-xdpi="300"
|
||||
inkscape:export-ydpi="300" /></g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user