mirror of
https://github.com/chylex/Nextcloud-Desktop.git
synced 2026-04-09 06:13:05 +02:00
Compare commits
204 Commits
v3.4.0-rc1
...
stable-3.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dda6f14e7b | ||
|
|
b97ac8c638 | ||
|
|
a289f46b45 | ||
|
|
59a8ff50ef | ||
|
|
a4a13a98d3 | ||
|
|
65c1f5eb88 | ||
|
|
fe5c6f5758 | ||
|
|
898427da27 | ||
|
|
a96a38b214 | ||
|
|
cde199a432 | ||
|
|
463c534ca2 | ||
|
|
7f6523b17b | ||
|
|
9f939ce6ed | ||
|
|
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
|
||||
|
||||
@@ -21,7 +21,7 @@ Icon=@APPLICATION_EXECUTABLE@
|
||||
|
||||
|
||||
# Translations
|
||||
Icon[de_DE]=@APPLICATION_ICON_NAME@
|
||||
Name[de_DE]=@APPLICATION_NAME@ Client zur Desktop-Synchronisierung
|
||||
Comment[de_DE]=@APPLICATION_NAME@ Client zur Desktop-Synchronisierung
|
||||
GenericName[de_DE]=Ordnersynchronisierung
|
||||
Icon[de]=@APPLICATION_ICON_NAME@
|
||||
Name[de]=@APPLICATION_NAME@ Desktop
|
||||
Comment[de]=@APPLICATION_NAME@ Client zur Desktop-Synchronisierung
|
||||
GenericName[de]=Ordner-Synchronisation
|
||||
|
||||
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
|
||||
|
||||
@@ -36,6 +36,8 @@ option( WITH_PROVIDERS "Build with providers list" ON )
|
||||
|
||||
option( ENFORCE_VIRTUAL_FILES_SYNC_FOLDER "Enforce use of virtual files sync folder when available" OFF )
|
||||
|
||||
option( DO_NOT_USE_PROXY "Do not use system wide proxy, instead always do a direct connection to server" OFF )
|
||||
|
||||
## Theming options
|
||||
set(NEXTCLOUD_BACKGROUND_COLOR "#0082c9" CACHE STRING "Default Nextcloud background color")
|
||||
set( APPLICATION_WIZARD_HEADER_BACKGROUND_COLOR ${NEXTCLOUD_BACKGROUND_COLOR} CACHE STRING "Hex color of the wizard header background")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,10 +15,10 @@ OBS_PROJECT_BETA=home:ivaradi:beta
|
||||
OBS_PACKAGE=nextcloud-desktop
|
||||
|
||||
if test "${DRONE_TARGET_BRANCH}" = "stable-2.6"; then
|
||||
UBUNTU_DISTRIBUTIONS="bionic focal hirsute impish"
|
||||
UBUNTU_DISTRIBUTIONS="bionic focal impish jammy"
|
||||
DEBIAN_DISTRIBUTIONS="buster stretch testing"
|
||||
else
|
||||
UBUNTU_DISTRIBUTIONS="focal hirsute impish"
|
||||
UBUNTU_DISTRIBUTIONS="focal impish jammy"
|
||||
DEBIAN_DISTRIBUTIONS="testing"
|
||||
fi
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#cmakedefine APPLICATION_FORBID_BAD_SSL "@APPLICATION_FORBID_BAD_SSL@"
|
||||
#define APPLICATION_DOTVIRTUALFILE_SUFFIX "." APPLICATION_VIRTUALFILE_SUFFIX
|
||||
#cmakedefine01 ENFORCE_VIRTUAL_FILES_SYNC_FOLDER
|
||||
#cmakedefine DO_NOT_USE_PROXY "@DO_NOT_USE_PROXY@"
|
||||
|
||||
#cmakedefine ZLIB_FOUND @ZLIB_FOUND@
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -250,6 +250,10 @@ Application::Application(int &argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
if (_theme->doNotUseProxy()) {
|
||||
ConfigFile().setProxyType(QNetworkProxy::NoProxy);
|
||||
}
|
||||
|
||||
parseOptions(arguments());
|
||||
//no need to waste time;
|
||||
if (_helpOnly || _versionOnly)
|
||||
@@ -465,6 +469,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,42 +34,51 @@ NetworkSettings::NetworkSettings(QWidget *parent)
|
||||
{
|
||||
_ui->setupUi(this);
|
||||
|
||||
_ui->hostLineEdit->setPlaceholderText(tr("Hostname of proxy server"));
|
||||
_ui->userLineEdit->setPlaceholderText(tr("Username for proxy server"));
|
||||
_ui->passwordLineEdit->setPlaceholderText(tr("Password for proxy server"));
|
||||
_ui->proxyGroupBox->setVisible(!Theme::instance()->doNotUseProxy());
|
||||
|
||||
_ui->typeComboBox->addItem(tr("HTTP(S) proxy"), QNetworkProxy::HttpProxy);
|
||||
_ui->typeComboBox->addItem(tr("SOCKS5 proxy"), QNetworkProxy::Socks5Proxy);
|
||||
if (!Theme::instance()->doNotUseProxy()) {
|
||||
_ui->hostLineEdit->setPlaceholderText(tr("Hostname of proxy server"));
|
||||
_ui->userLineEdit->setPlaceholderText(tr("Username for proxy server"));
|
||||
_ui->passwordLineEdit->setPlaceholderText(tr("Password for proxy server"));
|
||||
|
||||
_ui->authRequiredcheckBox->setEnabled(true);
|
||||
_ui->typeComboBox->addItem(tr("HTTP(S) proxy"), QNetworkProxy::HttpProxy);
|
||||
_ui->typeComboBox->addItem(tr("SOCKS5 proxy"), QNetworkProxy::Socks5Proxy);
|
||||
|
||||
// Explicitly set up the enabled status of the proxy auth widgets to ensure
|
||||
// toggling the parent enables/disables the children
|
||||
_ui->userLineEdit->setEnabled(true);
|
||||
_ui->passwordLineEdit->setEnabled(true);
|
||||
_ui->authWidgets->setEnabled(_ui->authRequiredcheckBox->isChecked());
|
||||
connect(_ui->authRequiredcheckBox, &QAbstractButton::toggled,
|
||||
_ui->authWidgets, &QWidget::setEnabled);
|
||||
_ui->authRequiredcheckBox->setEnabled(true);
|
||||
|
||||
connect(_ui->manualProxyRadioButton, &QAbstractButton::toggled,
|
||||
_ui->manualSettings, &QWidget::setEnabled);
|
||||
connect(_ui->manualProxyRadioButton, &QAbstractButton::toggled,
|
||||
_ui->typeComboBox, &QWidget::setEnabled);
|
||||
connect(_ui->manualProxyRadioButton, &QAbstractButton::toggled,
|
||||
this, &NetworkSettings::checkAccountLocalhost);
|
||||
// Explicitly set up the enabled status of the proxy auth widgets to ensure
|
||||
// toggling the parent enables/disables the children
|
||||
_ui->userLineEdit->setEnabled(true);
|
||||
_ui->passwordLineEdit->setEnabled(true);
|
||||
_ui->authWidgets->setEnabled(_ui->authRequiredcheckBox->isChecked());
|
||||
connect(_ui->authRequiredcheckBox, &QAbstractButton::toggled, _ui->authWidgets, &QWidget::setEnabled);
|
||||
|
||||
connect(_ui->manualProxyRadioButton, &QAbstractButton::toggled, _ui->manualSettings, &QWidget::setEnabled);
|
||||
connect(_ui->manualProxyRadioButton, &QAbstractButton::toggled, _ui->typeComboBox, &QWidget::setEnabled);
|
||||
connect(_ui->manualProxyRadioButton, &QAbstractButton::toggled, this, &NetworkSettings::checkAccountLocalhost);
|
||||
|
||||
loadProxySettings();
|
||||
|
||||
connect(_ui->typeComboBox, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
|
||||
&NetworkSettings::saveProxySettings);
|
||||
connect(_ui->proxyButtonGroup, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), this,
|
||||
&NetworkSettings::saveProxySettings);
|
||||
connect(_ui->hostLineEdit, &QLineEdit::editingFinished, this, &NetworkSettings::saveProxySettings);
|
||||
connect(_ui->userLineEdit, &QLineEdit::editingFinished, this, &NetworkSettings::saveProxySettings);
|
||||
connect(_ui->passwordLineEdit, &QLineEdit::editingFinished, this, &NetworkSettings::saveProxySettings);
|
||||
connect(_ui->portSpinBox, &QAbstractSpinBox::editingFinished, this, &NetworkSettings::saveProxySettings);
|
||||
connect(_ui->authRequiredcheckBox, &QAbstractButton::toggled, this, &NetworkSettings::saveProxySettings);
|
||||
|
||||
// Warn about empty proxy host
|
||||
connect(_ui->hostLineEdit, &QLineEdit::textChanged, this, &NetworkSettings::checkEmptyProxyHost);
|
||||
checkEmptyProxyHost();
|
||||
checkAccountLocalhost();
|
||||
} else {
|
||||
_ui->noProxyRadioButton->setChecked(false);
|
||||
}
|
||||
|
||||
loadProxySettings();
|
||||
loadBWLimitSettings();
|
||||
|
||||
// proxy
|
||||
connect(_ui->typeComboBox, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &NetworkSettings::saveProxySettings);
|
||||
connect(_ui->proxyButtonGroup, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::buttonClicked), this, &NetworkSettings::saveProxySettings);
|
||||
connect(_ui->hostLineEdit, &QLineEdit::editingFinished, this, &NetworkSettings::saveProxySettings);
|
||||
connect(_ui->userLineEdit, &QLineEdit::editingFinished, this, &NetworkSettings::saveProxySettings);
|
||||
connect(_ui->passwordLineEdit, &QLineEdit::editingFinished, this, &NetworkSettings::saveProxySettings);
|
||||
connect(_ui->portSpinBox, &QAbstractSpinBox::editingFinished, this, &NetworkSettings::saveProxySettings);
|
||||
connect(_ui->authRequiredcheckBox, &QAbstractButton::toggled, this, &NetworkSettings::saveProxySettings);
|
||||
|
||||
connect(_ui->uploadLimitRadioButton, &QAbstractButton::clicked, this, &NetworkSettings::saveBWLimitSettings);
|
||||
connect(_ui->noUploadLimitRadioButton, &QAbstractButton::clicked, this, &NetworkSettings::saveBWLimitSettings);
|
||||
connect(_ui->autoUploadLimitRadioButton, &QAbstractButton::clicked, this, &NetworkSettings::saveBWLimitSettings);
|
||||
@@ -78,11 +87,6 @@ NetworkSettings::NetworkSettings(QWidget *parent)
|
||||
connect(_ui->autoDownloadLimitRadioButton, &QAbstractButton::clicked, this, &NetworkSettings::saveBWLimitSettings);
|
||||
connect(_ui->downloadSpinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &NetworkSettings::saveBWLimitSettings);
|
||||
connect(_ui->uploadSpinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &NetworkSettings::saveBWLimitSettings);
|
||||
|
||||
// Warn about empty proxy host
|
||||
connect(_ui->hostLineEdit, &QLineEdit::textChanged, this, &NetworkSettings::checkEmptyProxyHost);
|
||||
checkEmptyProxyHost();
|
||||
checkAccountLocalhost();
|
||||
}
|
||||
|
||||
NetworkSettings::~NetworkSettings()
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -417,6 +417,15 @@ bool Theme::forbidBadSSL() const
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Theme::doNotUseProxy() const
|
||||
{
|
||||
#ifdef DO_NOT_USE_PROXY
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
QString Theme::forceConfigAuthType() const
|
||||
{
|
||||
return QString();
|
||||
|
||||
@@ -254,6 +254,13 @@ public:
|
||||
*/
|
||||
virtual bool forbidBadSSL() const;
|
||||
|
||||
/**
|
||||
* Forbid use of proxy
|
||||
*
|
||||
* When true, the app always connects to the server directly
|
||||
*/
|
||||
virtual bool doNotUseProxy() const;
|
||||
|
||||
/**
|
||||
* This is only usefull when previous version had a different overrideServerUrl
|
||||
* with a different auth type in that case You should then specify "http" or "shibboleth".
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user