Merge pull request #48 from nextcloud/oc_master

Bump to oc master
This commit is contained in:
Roeland Jago Douma 2017-11-20 13:43:34 +01:00 committed by GitHub
commit b835bdc55b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
138 changed files with 11630 additions and 7448 deletions

View file

@ -199,7 +199,7 @@ if(BUILD_CLIENT)
endif() endif()
endif() endif()
find_package(ZLIB) find_package(ZLIB REQUIRED)
endif() endif()
if (NOT DEFINED APPLICATION_ICON_NAME) if (NOT DEFINED APPLICATION_ICON_NAME)
@ -213,6 +213,9 @@ add_definitions(-D_UNICODE)
if( WIN32 ) if( WIN32 )
add_definitions( -D__USE_MINGW_ANSI_STDIO=1 ) add_definitions( -D__USE_MINGW_ANSI_STDIO=1 )
add_definitions( -DNOMINMAX ) add_definitions( -DNOMINMAX )
# Get APIs from from Vista onwards.
add_definitions( -D_WIN32_WINNT=0x0600)
add_definitions( -DWINVER=0x0600)
endif( WIN32 ) endif( WIN32 )
include(QtVersionAbstraction) include(QtVersionAbstraction)

View file

@ -1,75 +1,103 @@
ChangeLog ChangeLog
========= =========
version 2.4.0 (2017-1X-XX) version 2.4.0 (2017-11-XX)
* OAuth2 authentication support by opening external browser * If you're using 2.4.0 alpha1, please upgrade as the alpha1 had an issue with hidden files!
* Sync Issues: More functional error view including filters and conflicts (#5516) * OAuth2 authentication support by opening external browser (#5668)
* Sharing: Add support for multiple public link shares (#5655) * Sharing: Add support for multiple public link shares (#5655)
* Sharing: Add option to copy/email private links (#5627, #5023) * Sharing: Add option to copy/email private links (#5023, #5627)
* Sharing: Add option "show file listing" (#5837) * Sharing: Add option "show file listing" (#5837)
* Sharing: Show warning that links are public * Sharing: Show warning that links are public (#5747)
* Sharing: Many UI improvements * Sharing: Sharing dialog redesign: multiple share links support (#5695)
* Sharing: Make "can edit" partially checked sometimes (#5642) * Sharing: Make "can edit" partially checked sometimes (#5642)
* Sharing: Trigger a sync for folder affected by a change of sharing (#6098)
* Wizard: Never propose an existing folder for syncing (#5597) * Wizard: Never propose an existing folder for syncing (#5597)
* Wizard: Don't show last page anymore, go to settings directly (#5726) * Wizard: Don't show last page anymore, go to settings directly (#5726)
* Wizard: Handle url-shortener redirects #5954 * Wizard: Handle url-shortener redirects (#5954)
* Gui: Display the user server avatar * Wizard: Resolve url/ redirects only if url/status.php not found (#5954)
* Wizard: Add explanation text when server error is shown (#6157)
* Wizard: Update the window size on high dpi screen (#6156)
* Wizard: Don't report confusing error message (#6116)
* Gui: Display the user server avatar (#5482)
* Gui: Use display name of user, not internal name
* Server URL: Update configuration in case of permanent redirection (#5972) * Server URL: Update configuration in case of permanent redirection (#5972)
* Gui: Allow to add multiple sync folder connection of the same folder * Gui: Allow to add multiple sync folder connection of the same folder (#6032)
* Tray Menu: More detailed status messages
* Tray Menu: Shibboleth: raise the browser when clicking on the tray (#6105)
* Activity: Link errors from the account tab, allow filtering by account/folder (#5861)
* Activity: Present conflicts more prominently (#5894)
* Activity: Allow sorting the columns in issues and protocol tabs (#6093, #6086)
* Selective Sync: Open sub folder context menu (#5596) * Selective Sync: Open sub folder context menu (#5596)
* Selective Sync: Skip excluded folders when reading db * Selective Sync: Skip excluded folders when reading db (#5772)
* Selective Sync: Remove local files of unselected folder despite other modified files (#5783) * Selective Sync: Remove local files of unselected folder despite other modified files (#5783)
* Excludes: remove .htaccess form list of excluded files * Excludes: Remove .htaccess form list of excluded files (#5701)
* Excludes: Hardcode desktop.ini * Excludes: Hardcode desktop.ini
* Excludes: Allow escaping "#" (#6012) * Excludes: Allow escaping "#" (#6012)
* Excludes: Use faster matching via QRegularExpression * Excludes: Use faster matching via QRegularExpression (#6063)
* Discovery: Increase the MAX_DEPTH and show deep folders as ignored * Discovery: Increase the MAX_DEPTH and show deep folders as ignored (#1067)
* Discovery: General speed improvements * Discovery: General speed improvements
* Downloads: Remove empty temporary if disk space full (#5746) * Downloads: Remove empty temporary if disk space full (#5746)
* Downloads: Re-trigger folder discovery on 404 * Downloads: Read Content-MD5 header for object store setups
* Quota: PropagateUpload: Model of remote quota, avoid some uploads (#5537) * Checksums: Add global disable environment variable (#5017)
* When creating explorer favorite use more specific windows functions (#5690) * Quota: PropagateUpload: Model of remote quota, avoid some uploads (#5537)
* Create favorite also in folder wizard (#455) * Create favorite also in folder wizard (#455)
* Windows: Use the application icon for the Windows 8 sidebar favorite (#2446) * Windows: Use the application icon for the Windows 8 sidebar favorite (#2446, #5690)
* macOS: Finder sidebar icon (#296) * macOS: Finder sidebar icon (#296)
* Overlay Icons: Consider also the "shared by me" as shared (#4788) * Overlay Icons: Consider also the "shared by me" as shared (#4788)
* Overlay Icons: Update right after sharing (#6115)
* Overlay Icons: Fix different case paths not matching (#5257) * Overlay Icons: Fix different case paths not matching (#5257)
* Overlay Icons: Detect changes in the shared flag (#6098)
* Windows Overlay Icons: Potential hang fixes * Windows Overlay Icons: Potential hang fixes
* Linux Overlay Icons: fix branded nemo and caja shell integration (#5966) * Linux Overlay Icons: fix branded nemo and caja shell integration (#5966)
* Http credentials: Fix behavior for bad password (#5989) * Credentials: Fix behavior for bad password (#5989)
* Credentials: Use per-account keychain entries (#5830) * Credentials: Don't create empty client cert keychain entries (#5752)
* Credentials: Namespace windows cred keys (#6125)
* Credentials: Use per-account keychain entries (#5830, #6126)
* AccountSettings: Triggering log in re-ask about previously rejected certificates (#5819) * AccountSettings: Triggering log in re-ask about previously rejected certificates (#5819)
* Added owncloudcmd bandwidth limit parameter (#5707) * owncloudcmd: Added bandwidth limit parameter (#5707)
* owncloudcmd: Fix timestamps, Fix --logdebug
* AccountSettings: Sync with clean discovery on Ctrl-F6 (#5666) * AccountSettings: Sync with clean discovery on Ctrl-F6 (#5666)
* Sync: Dynamic sizing of chunks in chunked uploads for improved big file upload performance * Sync: Dynamic sizing of chunks in chunked uploads for improved big file upload performance (#5852)
* Sync: Introduce overall errors that are not tied to a file (#5746) * Sync: Introduce overall errors that are not tied to a file (#5746)
* Sync: Better messaging for 507 Insufficient Storage (#5537) * Sync: Better messaging for 507 Insufficient Storage (#5537)
* Sync: Create conflicts by comparing the hash of files with identical mtime/size (#5589) * Sync: Create conflicts by comparing the hash of files with identical mtime/size (#5589)
* Sync: Upload conflict files if OWNCLOUD_UPLOAD_CONFLICT_FILES environment variable is set (#6038)
* Sync: Blacklist: Don't let errors become warnings (#5516) * Sync: Blacklist: Don't let errors become warnings (#5516)
* Sync: Check etag again after active sync (#4116) * Sync: Check etag again after active sync (#4116)
* Sync: Rename handling fixes: duplicate file ids (#6096)
* Sync: Rename handling fixes: File size must be equal
* Sync: Rename handling: Fix duplicate files on abort/resume sync (#5949)
* Sync: Add capability for invalid filename regexes (#6092)
* SyncJournalDB: Fall back to DELETE journal mode if WAL mode does not seem to work (#5723) * SyncJournalDB: Fall back to DELETE journal mode if WAL mode does not seem to work (#5723)
* SyncJournalDB: Don't crash if the db file is readonly (#6050)
* SyncJournalDB: DB close error is not fatal
* Fix at least one memory leak * Fix at least one memory leak
* Documentation improvements * Documentation improvements
* Logging improvements (with Qt logging categories), new --logdebug parameter * Logging improvements (With Qt logging categories) (#5671)
* Logging filtering per account (#5672)
* Crash fixes * Crash fixes
* Test improvements * Test improvements
* Small UI layout fixes * Small UI layout fixes
* Maintenance Mode: Detect maintenance mode (#4485) * Maintenance Mode: Detect maintenance mode (#4485)
* Maintenance Mode: Add a 1 to 5 min reconnection delay (#5872) * Maintenance Mode: Add a 1 to 5 min reconnection delay (#5872)
* HTTP: Send a unique X-Request-ID with each request (#5853) * HTTP: Send a unique X-Request-ID with each request (#5853)
* HTTP: Support HTTP2 when built and running with Qt 5.9.x (Official packages still on Qt 5.6.x) * HTTP: Support HTTP2 when built and running with Qt 5.9.x (Official packages still on Qt 5.6.x) (#5659)
* owncloudcmd: Don't start if connection or auth fails (#5692) * owncloudcmd: Don't start if connection or auth fails (#5692)
* csync: Switch build from C to C++ * csync: Switch build from C to C++ (#6033)
* csync: Refactor a lot to use common data structures to save memory and memory copying * csync: Refactor a lot to use common data structures to save memory and memory copying
* csync: Switch some data structures to Qt data structures * csync: Switch some data structures to Qt data structures
* csync: Switch to using upper layer SyncJournalDB * csync: Switch to using upper layer SyncJournalDB (#6087)
* Switch 3rdparty/json usage to Qt5's QJson (#5710) * Switch 3rdparty/json usage to Qt5's QJson (#5710)
* OpenSSL: Don't require directly, only via Qt * OpenSSL: Don't require directly, only via Qt (#5833)
* Remove iconv dependency, use Qt for file system locale encoding/decoding (emoji filename support on macOS) * Remove iconv dependency, use Qt for file system locale encoding/decoding (emoji filename support on macOS) (#5875)
* Compilation: Remove Qt 4 code * Compilation: Remove Qt 4 code (#6025, #5702, #5505)
* Harmonize source code style with clang-format * Harmonize source code style with clang-format (#5732)
* Switch over to Qt 5 function pointer signal/slot syntax * Switch over to Qt 5 function pointer signal/slot syntax (#6041)
* Updater: Rudimentary support for beta channel * Updater: Rudimentary support for beta channel (#6048)
version 2.3.4 (2017-11-02)
* Checksums: Use addData function to avoid endless loop CPU load issues with Office files
* Packaging: Require ZLIB
version 2.3.3 (2017-08-29) version 2.3.3 (2017-08-29)
* Chunking NG: Don't use old chunking on new DAV endpoint (#5855) * Chunking NG: Don't use old chunking on new DAV endpoint (#5855)

View file

@ -1,5 +1,5 @@
set( MIRALL_VERSION_MAJOR 2 ) set( MIRALL_VERSION_MAJOR 2 )
set( MIRALL_VERSION_MINOR 4 ) set( MIRALL_VERSION_MINOR 5 )
set( MIRALL_VERSION_PATCH 0 ) set( MIRALL_VERSION_PATCH 0 )
set( MIRALL_VERSION_YEAR 2017 ) set( MIRALL_VERSION_YEAR 2017 )
set( MIRALL_SOVERSION 0 ) set( MIRALL_SOVERSION 0 )

View file

@ -3,7 +3,7 @@ StrCpy $MUI_FINISHPAGE_SHOWREADME_TEXT_STRING "Vis versjonsmerknader"
StrCpy $ConfirmEndProcess_MESSAGEBOX_TEXT "Fant ${APPLICATION_EXECUTABLE}-prosess(er) som må stoppes.$\nVil du at installasjonsprogrammet skal stoppe dem for deg?" StrCpy $ConfirmEndProcess_MESSAGEBOX_TEXT "Fant ${APPLICATION_EXECUTABLE}-prosess(er) som må stoppes.$\nVil du at installasjonsprogrammet skal stoppe dem for deg?"
StrCpy $ConfirmEndProcess_KILLING_PROCESSES_TEXT "Terminerer ${APPLICATION_EXECUTABLE}-prosesser." StrCpy $ConfirmEndProcess_KILLING_PROCESSES_TEXT "Terminerer ${APPLICATION_EXECUTABLE}-prosesser."
StrCpy $ConfirmEndProcess_KILL_NOT_FOUND_TEXT "Fant ikke prosess som skulle termineres!" StrCpy $ConfirmEndProcess_KILL_NOT_FOUND_TEXT "Fant ikke prosess som skulle termineres!"
StrCpy $PageReinstall_NEW_Field_1 "En eldre versjon av ${APPLICATION_NAME} er installert på systemet ditt. Det anbefales at du avnistallerer den versjonen før installering av ny versjon. Velg hva du vil gjøre og klikk Neste for å fortsette." StrCpy $PageReinstall_NEW_Field_1 "En eldre versjon av ${APPLICATION_NAME} er installert på systemet ditt. Det anbefales at du avinstallerer den versjonen før installering av ny versjon. Velg hva du vil gjøre og klikk Neste for å fortsette."
StrCpy $PageReinstall_NEW_Field_2 "Avinstaller før installering" StrCpy $PageReinstall_NEW_Field_2 "Avinstaller før installering"
StrCpy $PageReinstall_NEW_Field_3 "Ikke avinstaller" StrCpy $PageReinstall_NEW_Field_3 "Ikke avinstaller"
StrCpy $PageReinstall_NEW_MUI_HEADER_TEXT_TITLE "Allerede installert" StrCpy $PageReinstall_NEW_MUI_HEADER_TEXT_TITLE "Allerede installert"

View file

@ -1,9 +1,9 @@
# Auto-generated - do not modify # Auto-generated - do not modify
StrCpy $MUI_FINISHPAGE_SHOWREADME_TEXT_STRING "Mostrar las notas de la versión" StrCpy $MUI_FINISHPAGE_SHOWREADME_TEXT_STRING "Mostrar las notas de la versión"
StrCpy $ConfirmEndProcess_MESSAGEBOX_TEXT "El/los proceso/s ${APPLICATION_EXECUTABLE} debe/n ser detenidos.$\n¿Quiere que el instalador lo haga por usted?" StrCpy $ConfirmEndProcess_MESSAGEBOX_TEXT "El/los proceso(s) ${APPLICATION_EXECUTABLE} debe(n) ser detenido(s).$\n¿Quiere que el instalador lo haga por usted?"
StrCpy $ConfirmEndProcess_KILLING_PROCESSES_TEXT "Deteniendo el/los proceso/s ${APPLICATION_EXECUTABLE}." StrCpy $ConfirmEndProcess_KILLING_PROCESSES_TEXT "Deteniendo el/los proceso(s) ${APPLICATION_EXECUTABLE}."
StrCpy $ConfirmEndProcess_KILL_NOT_FOUND_TEXT "¡Proceso a finalizar no encontrado!" StrCpy $ConfirmEndProcess_KILL_NOT_FOUND_TEXT "¡Proceso a finalizar no encontrado!"
StrCpy $PageReinstall_NEW_Field_1 "Una versión anterior de ${APPLICATION_NAME} se encuentra instalada en el sistema. Se recomienda de instalar la versión actual antes de instalar la nueva. Seleccione la operacion deseada y haga click en Siguiente para continuar." StrCpy $PageReinstall_NEW_Field_1 "Una versión anterior de ${APPLICATION_NAME} se encuentra instalada en el sistema. Se recomienda desinstalar la versión actual antes de instalar la nueva. Seleccione la operacion deseada y haga click en Siguiente para continuar."
StrCpy $PageReinstall_NEW_Field_2 "Desinstalar antes de instalar" StrCpy $PageReinstall_NEW_Field_2 "Desinstalar antes de instalar"
StrCpy $PageReinstall_NEW_Field_3 "No desinstalar" StrCpy $PageReinstall_NEW_Field_3 "No desinstalar"
StrCpy $PageReinstall_NEW_MUI_HEADER_TEXT_TITLE "Ya está instalado" StrCpy $PageReinstall_NEW_MUI_HEADER_TEXT_TITLE "Ya está instalado"
@ -17,13 +17,13 @@ StrCpy $PageReinstall_SAME_MUI_HEADER_TEXT_SUBTITLE "Elija la opcion de mantenim
StrCpy $SEC_APPLICATION_DETAILS "Instalando ${APPLICATION_NAME} esenciales." StrCpy $SEC_APPLICATION_DETAILS "Instalando ${APPLICATION_NAME} esenciales."
StrCpy $OPTION_SECTION_SC_SHELL_EXT_SECTION "Integración para Windows Explorer" StrCpy $OPTION_SECTION_SC_SHELL_EXT_SECTION "Integración para Windows Explorer"
StrCpy $OPTION_SECTION_SC_SHELL_EXT_DetailPrint "Instalando la integración para Windows Explorer" StrCpy $OPTION_SECTION_SC_SHELL_EXT_DetailPrint "Instalando la integración para Windows Explorer"
StrCpy $OPTION_SECTION_SC_START_MENU_SECTION "Acceso directo al programa Menú de Inicio" StrCpy $OPTION_SECTION_SC_START_MENU_SECTION "Acceso directo al programa en Menú de Inicio"
StrCpy $OPTION_SECTION_SC_START_MENU_DetailPrint "Añadiendo accesos directos para ${APPLICATION_NAME} en el Menú de Inicio." StrCpy $OPTION_SECTION_SC_START_MENU_DetailPrint "Añadiendo accesos directos para ${APPLICATION_NAME} en el Menú de Inicio."
StrCpy $OPTION_SECTION_SC_DESKTOP_SECTION "Acceso directo de Escritorio" StrCpy $OPTION_SECTION_SC_DESKTOP_SECTION "Acceso directo de Escritorio"
StrCpy $OPTION_SECTION_SC_DESKTOP_DetailPrint "Creando accesos directos de escritorio" StrCpy $OPTION_SECTION_SC_DESKTOP_DetailPrint "Creando accesos directos de escritorio"
StrCpy $OPTION_SECTION_SC_QUICK_LAUNCH_SECTION "Atajo de accceso rápido" StrCpy $OPTION_SECTION_SC_QUICK_LAUNCH_SECTION "Atajo de acceso rápido"
StrCpy $OPTION_SECTION_SC_QUICK_LAUNCH_DetailPrint "Creando un Acceso Directo al Lanzador Rápido" StrCpy $OPTION_SECTION_SC_QUICK_LAUNCH_DetailPrint "Creando un Acceso Directo al Lanzador Rápido"
StrCpy $OPTION_SECTION_SC_APPLICATION_Desc "${APPLICATION_NAME} esencial." StrCpy $OPTION_SECTION_SC_APPLICATION_Desc "${APPLICATION_NAME} esenciales."
StrCpy $OPTION_SECTION_SC_START_MENU_Desc "Acceso Directo de ${APPLICATION_NAME}" StrCpy $OPTION_SECTION_SC_START_MENU_Desc "Acceso Directo de ${APPLICATION_NAME}"
StrCpy $OPTION_SECTION_SC_DESKTOP_Desc "Acceso Directo de Escritorio para ${APPLICATION_NAME}" StrCpy $OPTION_SECTION_SC_DESKTOP_Desc "Acceso Directo de Escritorio para ${APPLICATION_NAME}"
StrCpy $OPTION_SECTION_SC_QUICK_LAUNCH_Desc "Lanzador Rápido de Accesos Director para ${APPLICATION_NAME}." StrCpy $OPTION_SECTION_SC_QUICK_LAUNCH_Desc "Lanzador Rápido de Accesos Director para ${APPLICATION_NAME}."

View file

@ -603,6 +603,35 @@ Section Uninstall
Abort $UNINSTALL_ABORT Abort $UNINSTALL_ABORT
owncloud_installed: owncloud_installed:
; Delete Navigation Pane entries added for Windows 10.
; On 64bit Windows, the client will be writing to the 64bit registry.
${If} ${RunningX64}
SetRegView 64
${EndIf}
StrCpy $0 0
loop:
; Look at every registered explorer namespace for HKCU and check if it was added by our application
; (we write to a custom "ApplicationName" value there).
EnumRegKey $1 HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace" $0
StrCmp $1 "" done
ReadRegStr $R0 HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\$1" "ApplicationName"
StrCmp $R0 "${APPLICATION_NAME}" deleteClsid
; Increment the index when not deleting the enumerated key.
IntOp $0 $0 + 1
goto loop
deleteClsid:
DetailPrint "Removing Navigation Pane CLSID $1"
; Should match FolderMan::updateCloudStorageRegistry
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\$1"
DeleteRegKey HKCU "Software\Classes\CLSID\$1"
DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel" $1
goto loop
done:
; Go back to the 32bit registry.
SetRegView lastused
;Delete registry keys. ;Delete registry keys.
DeleteRegValue HKLM "Software\${APPLICATION_VENDOR}\${APPLICATION_NAME}" "VersionBuild" DeleteRegValue HKLM "Software\${APPLICATION_VENDOR}\${APPLICATION_NAME}" "VersionBuild"
DeleteRegValue HKLM "Software\${APPLICATION_VENDOR}\${APPLICATION_NAME}" "VersionMajor" DeleteRegValue HKLM "Software\${APPLICATION_VENDOR}\${APPLICATION_NAME}" "VersionMajor"

View file

@ -15,7 +15,7 @@ Configuration File
.. include:: conffile.rst .. include:: conffile.rst
Environment Variables Environment Variables
------------------ ---------------------
.. index:: env vars .. index:: env vars
.. include:: envvars.rst .. include:: envvars.rst

View file

@ -27,6 +27,8 @@ Some interesting values that can be set on the configuration file are:
+---------------------------------+---------------+--------------------------------------------------------------------------------------------------------+ +---------------------------------+---------------+--------------------------------------------------------------------------------------------------------+
| ``forceSyncInterval`` | ``7200000`` | The duration of no activity after which a synchronization run shall be triggered automatically. | | ``forceSyncInterval`` | ``7200000`` | The duration of no activity after which a synchronization run shall be triggered automatically. |
+---------------------------------+---------------+--------------------------------------------------------------------------------------------------------+ +---------------------------------+---------------+--------------------------------------------------------------------------------------------------------+
| ``fullLocalDiscoveryInterval`` | ``3600000`` | The interval after which the next synchronization will perform a full local discovery. |
+---------------------------------+---------------+--------------------------------------------------------------------------------------------------------+
| ``notificationRefreshInterval`` | ``300000`` | Specifies the default interval of checking for new server notifications in milliseconds. | | ``notificationRefreshInterval`` | ``300000`` | Specifies the default interval of checking for new server notifications in milliseconds. |
+---------------------------------+---------------+--------------------------------------------------------------------------------------------------------+ +---------------------------------+---------------+--------------------------------------------------------------------------------------------------------+
@ -62,4 +64,4 @@ Some interesting values that can be set on the configuration file are:
| | | ``2`` for No Proxy. | | | | ``2`` for No Proxy. |
+ + +--------------------------------------------------------------------------------------------------------+ + + +--------------------------------------------------------------------------------------------------------+
| | | ``3`` for HTTP(S) Proxy. | | | | ``3`` for HTTP(S) Proxy. |
+---------------------------------+---------------+--------------------------------------------------------------------------------------------------------+ +---------------------------------+---------------+--------------------------------------------------------------------------------------------------------+

View file

@ -1,7 +1,7 @@
FAQ FAQ
=== ===
Some files are continuously uploaded to the server, even when they are not modified. Some Files Are Continuously Uploaded to the Server, Even When They Are Not Modified.
------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------
It is possible that another program is changing the modification date of the file. It is possible that another program is changing the modification date of the file.
@ -11,15 +11,17 @@ continually changes all files, unless you remove
from the windows registry. from the windows registry.
See http://petersteier.wordpress.com/2011/10/22/windows-indexer-changes-modification-dates-of-eml-files/ for more information. See http://petersteier.wordpress.com/2011/10/22/windows-indexer-changes-modification-dates-of-eml-files/ for more information.
Syncing breaks when attempting to sync deeper than 50 sub-directories, but the sync client does not report an error (RC=0) Syncing Stops When Attempting To Sync Deeper Than 100 Sub-directories.
-------------------------------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------
The sync client has been intentionally limited to sync no deeper than The sync client has been intentionally limited to sync no deeper than 100
fifty sub-directories, to help prevent memory problems. sub-directories. The hard limit exists to guard against bugs with cycles
Unfortunately, it, *currently*, does not report an error when this occurs. like symbolic link loops.
However, a UI notification is planned for a future release of ownCloud. When a deeply nested directory is excluded from synchronization it will be
listed with other ignored files and directories in the "Not synced" tab of
the "Activity" pane.
I want to move my local sync folder I Want To Move My Local Sync Folder
----------------------------------- -----------------------------------
The ownCloud desktop client does not provide a way to change the local sync directory. The ownCloud desktop client does not provide a way to change the local sync directory.
@ -29,20 +31,20 @@ Specifically, you have to:
1. Remove the existing connection which syncs to the wrong directory 1. Remove the existing connection which syncs to the wrong directory
2. Add a new connection which syncs to the desired directory 2. Add a new connection which syncs to the desired directory
.. image:: images/setup/ownCloud-remove_existing_connection.png .. figure:: images/setup/ownCloud-remove_existing_connection.png
:alt: Remove an existing connection :alt: Remove an existing connection
To do so, in the client UI, which you can see above, click the "**Account**" drop-down menu and then click "Remove". To do so, in the client UI, which you can see above, click the "**Account**" drop-down menu and then click "Remove".
This will display a "**Confirm Account Removal**" dialog window. This will display a "**Confirm Account Removal**" dialog window.
.. image:: images/setup/ownCloud-remove_existing_connection_confirmation_dialog.png .. figure:: images/setup/ownCloud-remove_existing_connection_confirmation_dialog.png
:alt: Remove existing connection confirmation dialog :alt: Remove existing connection confirmation dialog
If you're sure, click "**Remove connection**". If you're sure, click "**Remove connection**".
Then, click the Account drop-down menu again, and this time click "**Add new**". Then, click the Account drop-down menu again, and this time click "**Add new**".
.. image:: images/setup/ownCloud-replacement_connection_wizard.png .. figure:: images/setup/ownCloud-replacement_connection_wizard.png
:alt: Replacement connection wizard :alt: Replacement connection wizard
This opens the ownCloud Connection Wizard, which you can see above, *but* with an extra option. This opens the ownCloud Connection Wizard, which you can see above, *but* with an extra option.

View file

@ -3,7 +3,7 @@ Installing the Desktop Synchronization Client
============================================= =============================================
You can download the latest version of the ownCloud Desktop Synchronization You can download the latest version of the ownCloud Desktop Synchronization
Client from the `ownCloud download page <https://owncloud.org/install/>`_. Client from the `ownCloud download page`_.
There are clients for Linux, Mac OS X, and Microsoft Windows. There are clients for Linux, Mac OS X, and Microsoft Windows.
Installation on Mac OS X and Windows is the same as for any software Installation on Mac OS X and Windows is the same as for any software
@ -24,6 +24,20 @@ KWallet, so that the sync client can login automatically.
You will also find links to source code archives and older versions on the You will also find links to source code archives and older versions on the
download page. download page.
System Requirements
----------------------------------
- Windows 7+
- Mac OS X 10.7+ (**64-bit only**)
- CentOS 6 & 7 (64-bit only)
- Debian 7.0 & 8.0 & 9.0
- Fedora 24 & 25 & 26
- Ubuntu 16.04 & 16.10 & 17.04
- openSUSE Leap 42.1 & 42.2 & 42.3
.. note::
For Linux distributions, we support, if technically feasible, the latest 2 versions per platform and the previous `LTS`_.
Installation Wizard Installation Wizard
------------------- -------------------
@ -58,3 +72,7 @@ synchronizing your files.
Web GUI, and one to open your local ownCloud folder Web GUI, and one to open your local ownCloud folder
Click the Finish button, and you're all done. Click the Finish button, and you're all done.
.. Links
.. _ownCloud download page: https://owncloud.com/download/#desktop-clients

View file

@ -219,6 +219,111 @@ X-GNOME-Autostart-Delay=3
# Translations # Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations # Translations
Comment[oc]=@APPLICATION_NAME@ sincronizacion del client Comment[oc]=@APPLICATION_NAME@ sincronizacion del client
GenericName[oc]=Dorsièr de Sincronizacion GenericName[oc]=Dorsièr de Sincronizacion

File diff suppressed because it is too large Load diff

View file

@ -115,15 +115,17 @@ extern "C" {
** a string which identifies a particular check-in of SQLite ** a string which identifies a particular check-in of SQLite
** within its configuration management system. ^The SQLITE_SOURCE_ID ** within its configuration management system. ^The SQLITE_SOURCE_ID
** string contains the date and time of the check-in (UTC) and a SHA1 ** string contains the date and time of the check-in (UTC) and a SHA1
** or SHA3-256 hash of the entire source tree. ** or SHA3-256 hash of the entire source tree. If the source code has
** been edited in any way since it was last checked in, then the last
** four hexadecimal digits of the hash may be modified.
** **
** See also: [sqlite3_libversion()], ** See also: [sqlite3_libversion()],
** [sqlite3_libversion_number()], [sqlite3_sourceid()], ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()]. ** [sqlite_version()] and [sqlite_source_id()].
*/ */
#define SQLITE_VERSION "3.20.1" #define SQLITE_VERSION "3.21.0"
#define SQLITE_VERSION_NUMBER 3020001 #define SQLITE_VERSION_NUMBER 3021000
#define SQLITE_SOURCE_ID "2017-08-24 16:21:36 8d3a7ea6c5690d6b7c3767558f4f01b511c55463e3f9e64506801fe9b74dce34" #define SQLITE_SOURCE_ID "2017-10-24 18:55:49 1a584e499906b5c87ec7d43d4abce641fdf017c42125b083109bc77c4de48827"
/* /*
** CAPI3REF: Run-Time Library Version Numbers ** CAPI3REF: Run-Time Library Version Numbers
@ -139,7 +141,7 @@ extern "C" {
** **
** <blockquote><pre> ** <blockquote><pre>
** assert( sqlite3_libversion_number()==SQLITE_VERSION_NUMBER ); ** assert( sqlite3_libversion_number()==SQLITE_VERSION_NUMBER );
** assert( strcmp(sqlite3_sourceid(),SQLITE_SOURCE_ID)==0 ); ** assert( strncmp(sqlite3_sourceid(),SQLITE_SOURCE_ID,80)==0 );
** assert( strcmp(sqlite3_libversion(),SQLITE_VERSION)==0 ); ** assert( strcmp(sqlite3_libversion(),SQLITE_VERSION)==0 );
** </pre></blockquote>)^ ** </pre></blockquote>)^
** **
@ -149,9 +151,11 @@ extern "C" {
** function is provided for use in DLLs since DLL users usually do not have ** function is provided for use in DLLs since DLL users usually do not have
** direct access to string constants within the DLL. ^The ** direct access to string constants within the DLL. ^The
** sqlite3_libversion_number() function returns an integer equal to ** sqlite3_libversion_number() function returns an integer equal to
** [SQLITE_VERSION_NUMBER]. ^The sqlite3_sourceid() function returns ** [SQLITE_VERSION_NUMBER]. ^(The sqlite3_sourceid() function returns
** a pointer to a string constant whose value is the same as the ** a pointer to a string constant whose value is the same as the
** [SQLITE_SOURCE_ID] C preprocessor macro. ** [SQLITE_SOURCE_ID] C preprocessor macro. Except if SQLite is built
** using an edited copy of [the amalgamation], then the last four characters
** of the hash might be different from [SQLITE_SOURCE_ID].)^
** **
** See also: [sqlite_version()] and [sqlite_source_id()]. ** See also: [sqlite_version()] and [sqlite_source_id()].
*/ */
@ -432,7 +436,7 @@ SQLITE_API int sqlite3_exec(
#define SQLITE_FULL 13 /* Insertion failed because database is full */ #define SQLITE_FULL 13 /* Insertion failed because database is full */
#define SQLITE_CANTOPEN 14 /* Unable to open the database file */ #define SQLITE_CANTOPEN 14 /* Unable to open the database file */
#define SQLITE_PROTOCOL 15 /* Database lock protocol error */ #define SQLITE_PROTOCOL 15 /* Database lock protocol error */
#define SQLITE_EMPTY 16 /* Not used */ #define SQLITE_EMPTY 16 /* Internal use only */
#define SQLITE_SCHEMA 17 /* The database schema changed */ #define SQLITE_SCHEMA 17 /* The database schema changed */
#define SQLITE_TOOBIG 18 /* String or BLOB exceeds size limit */ #define SQLITE_TOOBIG 18 /* String or BLOB exceeds size limit */
#define SQLITE_CONSTRAINT 19 /* Abort due to constraint violation */ #define SQLITE_CONSTRAINT 19 /* Abort due to constraint violation */
@ -494,6 +498,9 @@ SQLITE_API int sqlite3_exec(
#define SQLITE_IOERR_CONVPATH (SQLITE_IOERR | (26<<8)) #define SQLITE_IOERR_CONVPATH (SQLITE_IOERR | (26<<8))
#define SQLITE_IOERR_VNODE (SQLITE_IOERR | (27<<8)) #define SQLITE_IOERR_VNODE (SQLITE_IOERR | (27<<8))
#define SQLITE_IOERR_AUTH (SQLITE_IOERR | (28<<8)) #define SQLITE_IOERR_AUTH (SQLITE_IOERR | (28<<8))
#define SQLITE_IOERR_BEGIN_ATOMIC (SQLITE_IOERR | (29<<8))
#define SQLITE_IOERR_COMMIT_ATOMIC (SQLITE_IOERR | (30<<8))
#define SQLITE_IOERR_ROLLBACK_ATOMIC (SQLITE_IOERR | (31<<8))
#define SQLITE_LOCKED_SHAREDCACHE (SQLITE_LOCKED | (1<<8)) #define SQLITE_LOCKED_SHAREDCACHE (SQLITE_LOCKED | (1<<8))
#define SQLITE_BUSY_RECOVERY (SQLITE_BUSY | (1<<8)) #define SQLITE_BUSY_RECOVERY (SQLITE_BUSY | (1<<8))
#define SQLITE_BUSY_SNAPSHOT (SQLITE_BUSY | (2<<8)) #define SQLITE_BUSY_SNAPSHOT (SQLITE_BUSY | (2<<8))
@ -580,6 +587,11 @@ SQLITE_API int sqlite3_exec(
** SQLITE_IOCAP_IMMUTABLE flag indicates that the file is on ** SQLITE_IOCAP_IMMUTABLE flag indicates that the file is on
** read-only media and cannot be changed even by processes with ** read-only media and cannot be changed even by processes with
** elevated privileges. ** elevated privileges.
**
** The SQLITE_IOCAP_BATCH_ATOMIC property means that the underlying
** filesystem supports doing multiple write operations atomically when those
** write operations are bracketed by [SQLITE_FCNTL_BEGIN_ATOMIC_WRITE] and
** [SQLITE_FCNTL_COMMIT_ATOMIC_WRITE].
*/ */
#define SQLITE_IOCAP_ATOMIC 0x00000001 #define SQLITE_IOCAP_ATOMIC 0x00000001
#define SQLITE_IOCAP_ATOMIC512 0x00000002 #define SQLITE_IOCAP_ATOMIC512 0x00000002
@ -595,6 +607,7 @@ SQLITE_API int sqlite3_exec(
#define SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN 0x00000800 #define SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN 0x00000800
#define SQLITE_IOCAP_POWERSAFE_OVERWRITE 0x00001000 #define SQLITE_IOCAP_POWERSAFE_OVERWRITE 0x00001000
#define SQLITE_IOCAP_IMMUTABLE 0x00002000 #define SQLITE_IOCAP_IMMUTABLE 0x00002000
#define SQLITE_IOCAP_BATCH_ATOMIC 0x00004000
/* /*
** CAPI3REF: File Locking Levels ** CAPI3REF: File Locking Levels
@ -729,6 +742,7 @@ struct sqlite3_file {
** <li> [SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN] ** <li> [SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN]
** <li> [SQLITE_IOCAP_POWERSAFE_OVERWRITE] ** <li> [SQLITE_IOCAP_POWERSAFE_OVERWRITE]
** <li> [SQLITE_IOCAP_IMMUTABLE] ** <li> [SQLITE_IOCAP_IMMUTABLE]
** <li> [SQLITE_IOCAP_BATCH_ATOMIC]
** </ul> ** </ul>
** **
** The SQLITE_IOCAP_ATOMIC property means that all writes of ** The SQLITE_IOCAP_ATOMIC property means that all writes of
@ -1012,6 +1026,40 @@ struct sqlite3_io_methods {
** The [SQLITE_FCNTL_RBU] opcode is implemented by the special VFS used by ** The [SQLITE_FCNTL_RBU] opcode is implemented by the special VFS used by
** the RBU extension only. All other VFS should return SQLITE_NOTFOUND for ** the RBU extension only. All other VFS should return SQLITE_NOTFOUND for
** this opcode. ** this opcode.
**
** <li>[[SQLITE_FCNTL_BEGIN_ATOMIC_WRITE]]
** If the [SQLITE_FCNTL_BEGIN_ATOMIC_WRITE] opcode returns SQLITE_OK, then
** the file descriptor is placed in "batch write mode", which
** means all subsequent write operations will be deferred and done
** atomically at the next [SQLITE_FCNTL_COMMIT_ATOMIC_WRITE]. Systems
** that do not support batch atomic writes will return SQLITE_NOTFOUND.
** ^Following a successful SQLITE_FCNTL_BEGIN_ATOMIC_WRITE and prior to
** the closing [SQLITE_FCNTL_COMMIT_ATOMIC_WRITE] or
** [SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE], SQLite will make
** no VFS interface calls on the same [sqlite3_file] file descriptor
** except for calls to the xWrite method and the xFileControl method
** with [SQLITE_FCNTL_SIZE_HINT].
**
** <li>[[SQLITE_FCNTL_COMMIT_ATOMIC_WRITE]]
** The [SQLITE_FCNTL_COMMIT_ATOMIC_WRITE] opcode causes all write
** operations since the previous successful call to
** [SQLITE_FCNTL_BEGIN_ATOMIC_WRITE] to be performed atomically.
** This file control returns [SQLITE_OK] if and only if the writes were
** all performed successfully and have been committed to persistent storage.
** ^Regardless of whether or not it is successful, this file control takes
** the file descriptor out of batch write mode so that all subsequent
** write operations are independent.
** ^SQLite will never invoke SQLITE_FCNTL_COMMIT_ATOMIC_WRITE without
** a prior successful call to [SQLITE_FCNTL_BEGIN_ATOMIC_WRITE].
**
** <li>[[SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE]]
** The [SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE] opcode causes all write
** operations since the previous successful call to
** [SQLITE_FCNTL_BEGIN_ATOMIC_WRITE] to be rolled back.
** ^This file control takes the file descriptor out of batch write mode
** so that all subsequent write operations are independent.
** ^SQLite will never invoke SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE without
** a prior successful call to [SQLITE_FCNTL_BEGIN_ATOMIC_WRITE].
** </ul> ** </ul>
*/ */
#define SQLITE_FCNTL_LOCKSTATE 1 #define SQLITE_FCNTL_LOCKSTATE 1
@ -1043,6 +1091,9 @@ struct sqlite3_io_methods {
#define SQLITE_FCNTL_JOURNAL_POINTER 28 #define SQLITE_FCNTL_JOURNAL_POINTER 28
#define SQLITE_FCNTL_WIN32_GET_HANDLE 29 #define SQLITE_FCNTL_WIN32_GET_HANDLE 29
#define SQLITE_FCNTL_PDB 30 #define SQLITE_FCNTL_PDB 30
#define SQLITE_FCNTL_BEGIN_ATOMIC_WRITE 31
#define SQLITE_FCNTL_COMMIT_ATOMIC_WRITE 32
#define SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE 33
/* deprecated names */ /* deprecated names */
#define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE #define SQLITE_GET_LOCKPROXYFILE SQLITE_FCNTL_GET_LOCKPROXYFILE
@ -1613,6 +1664,16 @@ struct sqlite3_mem_methods {
** routines with a wrapper that simulations memory allocation failure or ** routines with a wrapper that simulations memory allocation failure or
** tracks memory usage, for example. </dd> ** tracks memory usage, for example. </dd>
** **
** [[SQLITE_CONFIG_SMALL_MALLOC]] <dt>SQLITE_CONFIG_SMALL_MALLOC</dt>
** <dd> ^The SQLITE_CONFIG_SMALL_MALLOC option takes single argument of
** type int, interpreted as a boolean, which if true provides a hint to
** SQLite that it should avoid large memory allocations if possible.
** SQLite will run faster if it is free to make large memory allocations,
** but some application might prefer to run slower in exchange for
** guarantees about memory fragmentation that are possible if large
** allocations are avoided. This hint is normally off.
** </dd>
**
** [[SQLITE_CONFIG_MEMSTATUS]] <dt>SQLITE_CONFIG_MEMSTATUS</dt> ** [[SQLITE_CONFIG_MEMSTATUS]] <dt>SQLITE_CONFIG_MEMSTATUS</dt>
** <dd> ^The SQLITE_CONFIG_MEMSTATUS option takes single argument of type int, ** <dd> ^The SQLITE_CONFIG_MEMSTATUS option takes single argument of type int,
** interpreted as a boolean, which enables or disables the collection of ** interpreted as a boolean, which enables or disables the collection of
@ -1630,25 +1691,7 @@ struct sqlite3_mem_methods {
** </dd> ** </dd>
** **
** [[SQLITE_CONFIG_SCRATCH]] <dt>SQLITE_CONFIG_SCRATCH</dt> ** [[SQLITE_CONFIG_SCRATCH]] <dt>SQLITE_CONFIG_SCRATCH</dt>
** <dd> ^The SQLITE_CONFIG_SCRATCH option specifies a static memory buffer ** <dd> The SQLITE_CONFIG_SCRATCH option is no longer used.
** that SQLite can use for scratch memory. ^(There are three arguments
** to SQLITE_CONFIG_SCRATCH: A pointer an 8-byte
** aligned memory buffer from which the scratch allocations will be
** drawn, the size of each scratch allocation (sz),
** and the maximum number of scratch allocations (N).)^
** The first argument must be a pointer to an 8-byte aligned buffer
** of at least sz*N bytes of memory.
** ^SQLite will not use more than one scratch buffers per thread.
** ^SQLite will never request a scratch buffer that is more than 6
** times the database page size.
** ^If SQLite needs needs additional
** scratch memory beyond what is provided by this configuration option, then
** [sqlite3_malloc()] will be used to obtain the memory needed.<p>
** ^When the application provides any amount of scratch memory using
** SQLITE_CONFIG_SCRATCH, SQLite avoids unnecessary large
** [sqlite3_malloc|heap allocations].
** This can help [Robson proof|prevent memory allocation failures] due to heap
** fragmentation in low-memory embedded systems.
** </dd> ** </dd>
** **
** [[SQLITE_CONFIG_PAGECACHE]] <dt>SQLITE_CONFIG_PAGECACHE</dt> ** [[SQLITE_CONFIG_PAGECACHE]] <dt>SQLITE_CONFIG_PAGECACHE</dt>
@ -1684,8 +1727,7 @@ struct sqlite3_mem_methods {
** [[SQLITE_CONFIG_HEAP]] <dt>SQLITE_CONFIG_HEAP</dt> ** [[SQLITE_CONFIG_HEAP]] <dt>SQLITE_CONFIG_HEAP</dt>
** <dd> ^The SQLITE_CONFIG_HEAP option specifies a static memory buffer ** <dd> ^The SQLITE_CONFIG_HEAP option specifies a static memory buffer
** that SQLite will use for all of its dynamic memory allocation needs ** that SQLite will use for all of its dynamic memory allocation needs
** beyond those provided for by [SQLITE_CONFIG_SCRATCH] and ** beyond those provided for by [SQLITE_CONFIG_PAGECACHE].
** [SQLITE_CONFIG_PAGECACHE].
** ^The SQLITE_CONFIG_HEAP option is only available if SQLite is compiled ** ^The SQLITE_CONFIG_HEAP option is only available if SQLite is compiled
** with either [SQLITE_ENABLE_MEMSYS3] or [SQLITE_ENABLE_MEMSYS5] and returns ** with either [SQLITE_ENABLE_MEMSYS3] or [SQLITE_ENABLE_MEMSYS5] and returns
** [SQLITE_ERROR] if invoked otherwise. ** [SQLITE_ERROR] if invoked otherwise.
@ -1878,7 +1920,7 @@ struct sqlite3_mem_methods {
#define SQLITE_CONFIG_SERIALIZED 3 /* nil */ #define SQLITE_CONFIG_SERIALIZED 3 /* nil */
#define SQLITE_CONFIG_MALLOC 4 /* sqlite3_mem_methods* */ #define SQLITE_CONFIG_MALLOC 4 /* sqlite3_mem_methods* */
#define SQLITE_CONFIG_GETMALLOC 5 /* sqlite3_mem_methods* */ #define SQLITE_CONFIG_GETMALLOC 5 /* sqlite3_mem_methods* */
#define SQLITE_CONFIG_SCRATCH 6 /* void*, int sz, int N */ #define SQLITE_CONFIG_SCRATCH 6 /* No longer used */
#define SQLITE_CONFIG_PAGECACHE 7 /* void*, int sz, int N */ #define SQLITE_CONFIG_PAGECACHE 7 /* void*, int sz, int N */
#define SQLITE_CONFIG_HEAP 8 /* void*, int nByte, int min */ #define SQLITE_CONFIG_HEAP 8 /* void*, int nByte, int min */
#define SQLITE_CONFIG_MEMSTATUS 9 /* boolean */ #define SQLITE_CONFIG_MEMSTATUS 9 /* boolean */
@ -1899,6 +1941,7 @@ struct sqlite3_mem_methods {
#define SQLITE_CONFIG_PCACHE_HDRSZ 24 /* int *psz */ #define SQLITE_CONFIG_PCACHE_HDRSZ 24 /* int *psz */
#define SQLITE_CONFIG_PMASZ 25 /* unsigned int szPma */ #define SQLITE_CONFIG_PMASZ 25 /* unsigned int szPma */
#define SQLITE_CONFIG_STMTJRNL_SPILL 26 /* int nByte */ #define SQLITE_CONFIG_STMTJRNL_SPILL 26 /* int nByte */
#define SQLITE_CONFIG_SMALL_MALLOC 27 /* boolean */
/* /*
** CAPI3REF: Database Connection Configuration Options ** CAPI3REF: Database Connection Configuration Options
@ -3099,10 +3142,10 @@ SQLITE_API void sqlite3_progress_handler(sqlite3*, int, int(*)(void*), void*);
** ^If [URI filename] interpretation is enabled, and the filename argument ** ^If [URI filename] interpretation is enabled, and the filename argument
** begins with "file:", then the filename is interpreted as a URI. ^URI ** begins with "file:", then the filename is interpreted as a URI. ^URI
** filename interpretation is enabled if the [SQLITE_OPEN_URI] flag is ** filename interpretation is enabled if the [SQLITE_OPEN_URI] flag is
** set in the fourth argument to sqlite3_open_v2(), or if it has ** set in the third argument to sqlite3_open_v2(), or if it has
** been enabled globally using the [SQLITE_CONFIG_URI] option with the ** been enabled globally using the [SQLITE_CONFIG_URI] option with the
** [sqlite3_config()] method or by the [SQLITE_USE_URI] compile-time option. ** [sqlite3_config()] method or by the [SQLITE_USE_URI] compile-time option.
** As of SQLite version 3.7.7, URI filename interpretation is turned off ** URI filename interpretation is turned off
** by default, but future releases of SQLite might enable URI filename ** by default, but future releases of SQLite might enable URI filename
** interpretation by default. See "[URI filenames]" for additional ** interpretation by default. See "[URI filenames]" for additional
** information. ** information.
@ -3776,8 +3819,9 @@ SQLITE_API int sqlite3_stmt_busy(sqlite3_stmt*);
** implementation of [application-defined SQL functions] are protected. ** implementation of [application-defined SQL functions] are protected.
** ^The sqlite3_value object returned by ** ^The sqlite3_value object returned by
** [sqlite3_column_value()] is unprotected. ** [sqlite3_column_value()] is unprotected.
** Unprotected sqlite3_value objects may only be used with ** Unprotected sqlite3_value objects may only be used as arguments
** [sqlite3_result_value()] and [sqlite3_bind_value()]. ** to [sqlite3_result_value()], [sqlite3_bind_value()], and
** [sqlite3_value_dup()].
** The [sqlite3_value_blob | sqlite3_value_type()] family of ** The [sqlite3_value_blob | sqlite3_value_type()] family of
** interfaces require protected sqlite3_value objects. ** interfaces require protected sqlite3_value objects.
*/ */
@ -4199,7 +4243,7 @@ SQLITE_API const void *sqlite3_column_decltype16(sqlite3_stmt*,int);
** other than [SQLITE_ROW] before any subsequent invocation of ** other than [SQLITE_ROW] before any subsequent invocation of
** sqlite3_step(). Failure to reset the prepared statement using ** sqlite3_step(). Failure to reset the prepared statement using
** [sqlite3_reset()] would result in an [SQLITE_MISUSE] return from ** [sqlite3_reset()] would result in an [SQLITE_MISUSE] return from
** sqlite3_step(). But after [version 3.6.23.1] ([dateof:3.6.23.1]), ** sqlite3_step(). But after [version 3.6.23.1] ([dateof:3.6.23.1],
** sqlite3_step() began ** sqlite3_step() began
** calling [sqlite3_reset()] automatically in this circumstance rather ** calling [sqlite3_reset()] automatically in this circumstance rather
** than returning [SQLITE_MISUSE]. This is not considered a compatibility ** than returning [SQLITE_MISUSE]. This is not considered a compatibility
@ -6203,15 +6247,20 @@ struct sqlite3_index_info {
** an operator that is part of a constraint term in the wHERE clause of ** an operator that is part of a constraint term in the wHERE clause of
** a query that uses a [virtual table]. ** a query that uses a [virtual table].
*/ */
#define SQLITE_INDEX_CONSTRAINT_EQ 2 #define SQLITE_INDEX_CONSTRAINT_EQ 2
#define SQLITE_INDEX_CONSTRAINT_GT 4 #define SQLITE_INDEX_CONSTRAINT_GT 4
#define SQLITE_INDEX_CONSTRAINT_LE 8 #define SQLITE_INDEX_CONSTRAINT_LE 8
#define SQLITE_INDEX_CONSTRAINT_LT 16 #define SQLITE_INDEX_CONSTRAINT_LT 16
#define SQLITE_INDEX_CONSTRAINT_GE 32 #define SQLITE_INDEX_CONSTRAINT_GE 32
#define SQLITE_INDEX_CONSTRAINT_MATCH 64 #define SQLITE_INDEX_CONSTRAINT_MATCH 64
#define SQLITE_INDEX_CONSTRAINT_LIKE 65 #define SQLITE_INDEX_CONSTRAINT_LIKE 65
#define SQLITE_INDEX_CONSTRAINT_GLOB 66 #define SQLITE_INDEX_CONSTRAINT_GLOB 66
#define SQLITE_INDEX_CONSTRAINT_REGEXP 67 #define SQLITE_INDEX_CONSTRAINT_REGEXP 67
#define SQLITE_INDEX_CONSTRAINT_NE 68
#define SQLITE_INDEX_CONSTRAINT_ISNOT 69
#define SQLITE_INDEX_CONSTRAINT_ISNOTNULL 70
#define SQLITE_INDEX_CONSTRAINT_ISNULL 71
#define SQLITE_INDEX_CONSTRAINT_IS 72
/* /*
** CAPI3REF: Register A Virtual Table Implementation ** CAPI3REF: Register A Virtual Table Implementation
@ -6963,7 +7012,7 @@ SQLITE_API int sqlite3_test_control(int op, ...);
#define SQLITE_TESTCTRL_RESERVE 14 #define SQLITE_TESTCTRL_RESERVE 14
#define SQLITE_TESTCTRL_OPTIMIZATIONS 15 #define SQLITE_TESTCTRL_OPTIMIZATIONS 15
#define SQLITE_TESTCTRL_ISKEYWORD 16 #define SQLITE_TESTCTRL_ISKEYWORD 16
#define SQLITE_TESTCTRL_SCRATCHMALLOC 17 #define SQLITE_TESTCTRL_SCRATCHMALLOC 17 /* NOT USED */
#define SQLITE_TESTCTRL_LOCALTIME_FAULT 18 #define SQLITE_TESTCTRL_LOCALTIME_FAULT 18
#define SQLITE_TESTCTRL_EXPLAIN_STMT 19 /* NOT USED */ #define SQLITE_TESTCTRL_EXPLAIN_STMT 19 /* NOT USED */
#define SQLITE_TESTCTRL_ONCE_RESET_THRESHOLD 19 #define SQLITE_TESTCTRL_ONCE_RESET_THRESHOLD 19
@ -7022,8 +7071,7 @@ SQLITE_API int sqlite3_status64(
** <dd>This parameter is the current amount of memory checked out ** <dd>This parameter is the current amount of memory checked out
** using [sqlite3_malloc()], either directly or indirectly. The ** using [sqlite3_malloc()], either directly or indirectly. The
** figure includes calls made to [sqlite3_malloc()] by the application ** figure includes calls made to [sqlite3_malloc()] by the application
** and internal memory usage by the SQLite library. Scratch memory ** and internal memory usage by the SQLite library. Auxiliary page-cache
** controlled by [SQLITE_CONFIG_SCRATCH] and auxiliary page-cache
** memory controlled by [SQLITE_CONFIG_PAGECACHE] is not included in ** memory controlled by [SQLITE_CONFIG_PAGECACHE] is not included in
** this parameter. The amount returned is the sum of the allocation ** this parameter. The amount returned is the sum of the allocation
** sizes as reported by the xSize method in [sqlite3_mem_methods].</dd>)^ ** sizes as reported by the xSize method in [sqlite3_mem_methods].</dd>)^
@ -7061,29 +7109,14 @@ SQLITE_API int sqlite3_status64(
** *pHighwater parameter to [sqlite3_status()] is of interest. ** *pHighwater parameter to [sqlite3_status()] is of interest.
** The value written into the *pCurrent parameter is undefined.</dd>)^ ** The value written into the *pCurrent parameter is undefined.</dd>)^
** **
** [[SQLITE_STATUS_SCRATCH_USED]] ^(<dt>SQLITE_STATUS_SCRATCH_USED</dt> ** [[SQLITE_STATUS_SCRATCH_USED]] <dt>SQLITE_STATUS_SCRATCH_USED</dt>
** <dd>This parameter returns the number of allocations used out of the ** <dd>No longer used.</dd>
** [scratch memory allocator] configured using
** [SQLITE_CONFIG_SCRATCH]. The value returned is in allocations, not
** in bytes. Since a single thread may only have one scratch allocation
** outstanding at time, this parameter also reports the number of threads
** using scratch memory at the same time.</dd>)^
** **
** [[SQLITE_STATUS_SCRATCH_OVERFLOW]] ^(<dt>SQLITE_STATUS_SCRATCH_OVERFLOW</dt> ** [[SQLITE_STATUS_SCRATCH_OVERFLOW]] ^(<dt>SQLITE_STATUS_SCRATCH_OVERFLOW</dt>
** <dd>This parameter returns the number of bytes of scratch memory ** <dd>No longer used.</dd>
** allocation which could not be satisfied by the [SQLITE_CONFIG_SCRATCH]
** buffer and where forced to overflow to [sqlite3_malloc()]. The values
** returned include overflows because the requested allocation was too
** larger (that is, because the requested allocation was larger than the
** "sz" parameter to [SQLITE_CONFIG_SCRATCH]) and because no scratch buffer
** slots were available.
** </dd>)^
** **
** [[SQLITE_STATUS_SCRATCH_SIZE]] ^(<dt>SQLITE_STATUS_SCRATCH_SIZE</dt> ** [[SQLITE_STATUS_SCRATCH_SIZE]] <dt>SQLITE_STATUS_SCRATCH_SIZE</dt>
** <dd>This parameter records the largest memory allocation request ** <dd>No longer used.</dd>
** handed to [scratch memory allocator]. Only the value returned in the
** *pHighwater parameter to [sqlite3_status()] is of interest.
** The value written into the *pCurrent parameter is undefined.</dd>)^
** **
** [[SQLITE_STATUS_PARSER_STACK]] ^(<dt>SQLITE_STATUS_PARSER_STACK</dt> ** [[SQLITE_STATUS_PARSER_STACK]] ^(<dt>SQLITE_STATUS_PARSER_STACK</dt>
** <dd>The *pHighwater parameter records the deepest parser stack. ** <dd>The *pHighwater parameter records the deepest parser stack.
@ -7096,12 +7129,12 @@ SQLITE_API int sqlite3_status64(
#define SQLITE_STATUS_MEMORY_USED 0 #define SQLITE_STATUS_MEMORY_USED 0
#define SQLITE_STATUS_PAGECACHE_USED 1 #define SQLITE_STATUS_PAGECACHE_USED 1
#define SQLITE_STATUS_PAGECACHE_OVERFLOW 2 #define SQLITE_STATUS_PAGECACHE_OVERFLOW 2
#define SQLITE_STATUS_SCRATCH_USED 3 #define SQLITE_STATUS_SCRATCH_USED 3 /* NOT USED */
#define SQLITE_STATUS_SCRATCH_OVERFLOW 4 #define SQLITE_STATUS_SCRATCH_OVERFLOW 4 /* NOT USED */
#define SQLITE_STATUS_MALLOC_SIZE 5 #define SQLITE_STATUS_MALLOC_SIZE 5
#define SQLITE_STATUS_PARSER_STACK 6 #define SQLITE_STATUS_PARSER_STACK 6
#define SQLITE_STATUS_PAGECACHE_SIZE 7 #define SQLITE_STATUS_PAGECACHE_SIZE 7
#define SQLITE_STATUS_SCRATCH_SIZE 8 #define SQLITE_STATUS_SCRATCH_SIZE 8 /* NOT USED */
#define SQLITE_STATUS_MALLOC_COUNT 9 #define SQLITE_STATUS_MALLOC_COUNT 9
/* /*
@ -9198,8 +9231,8 @@ SQLITE_API int sqlite3session_diff(
*/ */
SQLITE_API int sqlite3session_patchset( SQLITE_API int sqlite3session_patchset(
sqlite3_session *pSession, /* Session object */ sqlite3_session *pSession, /* Session object */
int *pnPatchset, /* OUT: Size of buffer at *ppChangeset */ int *pnPatchset, /* OUT: Size of buffer at *ppPatchset */
void **ppPatchset /* OUT: Buffer containing changeset */ void **ppPatchset /* OUT: Buffer containing patchset */
); );
/* /*
@ -9966,12 +9999,12 @@ SQLITE_API int sqlite3changeset_apply(
** **
** <table border=1 style="margin-left:8ex;margin-right:8ex"> ** <table border=1 style="margin-left:8ex;margin-right:8ex">
** <tr><th>Streaming function<th>Non-streaming equivalent</th> ** <tr><th>Streaming function<th>Non-streaming equivalent</th>
** <tr><td>sqlite3changeset_apply_str<td>[sqlite3changeset_apply] ** <tr><td>sqlite3changeset_apply_strm<td>[sqlite3changeset_apply]
** <tr><td>sqlite3changeset_concat_str<td>[sqlite3changeset_concat] ** <tr><td>sqlite3changeset_concat_strm<td>[sqlite3changeset_concat]
** <tr><td>sqlite3changeset_invert_str<td>[sqlite3changeset_invert] ** <tr><td>sqlite3changeset_invert_strm<td>[sqlite3changeset_invert]
** <tr><td>sqlite3changeset_start_str<td>[sqlite3changeset_start] ** <tr><td>sqlite3changeset_start_strm<td>[sqlite3changeset_start]
** <tr><td>sqlite3session_changeset_str<td>[sqlite3session_changeset] ** <tr><td>sqlite3session_changeset_strm<td>[sqlite3session_changeset]
** <tr><td>sqlite3session_patchset_str<td>[sqlite3session_patchset] ** <tr><td>sqlite3session_patchset_strm<td>[sqlite3session_patchset]
** </table> ** </table>
** **
** Non-streaming functions that accept changesets (or patchsets) as input ** Non-streaming functions that accept changesets (or patchsets) as input

View file

@ -39,6 +39,7 @@
#include "theme.h" #include "theme.h"
#include "netrcparser.h" #include "netrcparser.h"
#include "libsync/logger.h"
#include "config.h" #include "config.h"
@ -189,7 +190,7 @@ void help()
std::cout << " --downlimit [n] Limit the download speed of files to n KB/s" << std::endl; std::cout << " --downlimit [n] Limit the download speed of files to n KB/s" << std::endl;
std::cout << " -h Sync hidden files,do not ignore them" << std::endl; std::cout << " -h Sync hidden files,do not ignore them" << std::endl;
std::cout << " --version, -v Display version and exit" << std::endl; std::cout << " --version, -v Display version and exit" << std::endl;
std::cout << " --debug More verbose logging" << std::endl; std::cout << " --logdebug More verbose logging" << std::endl;
std::cout << "" << std::endl; std::cout << "" << std::endl;
exit(0); exit(0);
} }
@ -267,6 +268,9 @@ void parseOptions(const QStringList &app_args, CmdOptions *options)
options->uplimit = it.next().toInt() * 1000; options->uplimit = it.next().toInt() * 1000;
} else if (option == "--downlimit" && !it.peekNext().startsWith("-")) { } else if (option == "--downlimit" && !it.peekNext().startsWith("-")) {
options->downlimit = it.next().toInt() * 1000; options->downlimit = it.next().toInt() * 1000;
} else if (option == "--logdebug") {
Logger::instance()->setLogFile("-");
Logger::instance()->setLogDebug(true);
} else { } else {
help(); help();
} }
@ -330,6 +334,8 @@ int main(int argc, char **argv)
csync_set_log_level(options.silent ? 1 : 11); csync_set_log_level(options.silent ? 1 : 11);
if (options.silent) { if (options.silent) {
qInstallMsgHandler(nullMessageHandler); qInstallMsgHandler(nullMessageHandler);
} else {
qSetMessagePattern("%{time MM-dd hh:mm:ss:zzz} [ %{type} %{category} ]%{if-debug}\t[ %{function} ]%{endif}:\t%{message}");
} }
AccountPtr account = Account::create(); AccountPtr account = Account::create();

View file

@ -90,6 +90,21 @@ QByteArray makeChecksumHeader(const QByteArray &checksumType, const QByteArray &
return header; return header;
} }
QByteArray findBestChecksum(const QByteArray &checksums)
{
int i = 0;
// The order of the searches here defines the preference ordering.
if (-1 != (i = checksums.indexOf("SHA1:"))
|| -1 != (i = checksums.indexOf("MD5:"))
|| -1 != (i = checksums.indexOf("Adler32:"))) {
// Now i is the start of the best checksum
// Grab it until the next space or end of string.
auto checksum = checksums.mid(i);
return checksum.mid(0, checksum.indexOf(" "));
}
return QByteArray();
}
bool parseChecksumHeader(const QByteArray &header, QByteArray *type, QByteArray *checksum) bool parseChecksumHeader(const QByteArray &header, QByteArray *type, QByteArray *checksum)
{ {
if (header.isEmpty()) { if (header.isEmpty()) {
@ -120,7 +135,7 @@ QByteArray parseChecksumHeaderType(const QByteArray &header)
bool uploadChecksumEnabled() bool uploadChecksumEnabled()
{ {
static bool enabled = qgetenv("OWNCLOUD_DISABLE_CHECKSUM_UPLOAD").isEmpty(); static bool enabled = qEnvironmentVariableIsEmpty("OWNCLOUD_DISABLE_CHECKSUM_UPLOAD");
return enabled; return enabled;
} }
@ -133,6 +148,12 @@ QByteArray contentChecksumType()
return type; return type;
} }
static bool checksumComputationEnabled()
{
static bool enabled = qgetenv("OWNCLOUD_DISABLE_CHECKSUM_COMPUTATIONS").isEmpty();
return enabled;
}
ComputeChecksum::ComputeChecksum(QObject *parent) ComputeChecksum::ComputeChecksum(QObject *parent)
: QObject(parent) : QObject(parent)
{ {
@ -150,6 +171,8 @@ QByteArray ComputeChecksum::checksumType() const
void ComputeChecksum::start(const QString &filePath) void ComputeChecksum::start(const QString &filePath)
{ {
qCInfo(lcChecksums) << "Computing" << checksumType() << "checksum of" << filePath << "in a thread";
// Calculate the checksum in a different thread first. // Calculate the checksum in a different thread first.
connect(&_watcher, &QFutureWatcherBase::finished, connect(&_watcher, &QFutureWatcherBase::finished,
this, &ComputeChecksum::slotCalculationDone, this, &ComputeChecksum::slotCalculationDone,
@ -159,6 +182,11 @@ void ComputeChecksum::start(const QString &filePath)
QByteArray ComputeChecksum::computeNow(const QString &filePath, const QByteArray &checksumType) QByteArray ComputeChecksum::computeNow(const QString &filePath, const QByteArray &checksumType)
{ {
if (!checksumComputationEnabled()) {
qCWarning(lcChecksums) << "Checksum computation disabled by environment variable";
return QByteArray();
}
if (checksumType == checkSumMD5C) { if (checksumType == checkSumMD5C) {
return FileSystem::calcMd5(filePath); return FileSystem::calcMd5(filePath);
} else if (checksumType == checkSumSHA1C) { } else if (checksumType == checkSumSHA1C) {
@ -237,6 +265,7 @@ QByteArray CSyncChecksumHook::hook(const QByteArray &path, const QByteArray &oth
if (type.isEmpty()) if (type.isEmpty())
return NULL; return NULL;
qCInfo(lcChecksums) << "Computing" << type << "checksum of" << path << "in the csync hook";
QByteArray checksum = ComputeChecksum::computeNow(QString::fromUtf8(path), type); QByteArray checksum = ComputeChecksum::computeNow(QString::fromUtf8(path), type);
if (checksum.isNull()) { if (checksum.isNull()) {
qCWarning(lcChecksums) << "Failed to compute checksum" << type << "for" << path; qCWarning(lcChecksums) << "Failed to compute checksum" << type << "for" << path;

View file

@ -36,6 +36,16 @@ static const char checkSumAdlerC[] = "Adler32";
class SyncJournalDb; class SyncJournalDb;
/**
* Returns the highest-quality checksum in a 'checksums'
* property retrieved from the server.
*
* Example: "ADLER32:1231 SHA1:ab124124 MD5:2131affa21"
* -> "SHA1:ab124124"
*/
OCSYNC_EXPORT QByteArray findBestChecksum(const QByteArray &checksums);
/// Creates a checksum header from type and value. /// Creates a checksum header from type and value.
OCSYNC_EXPORT QByteArray makeChecksumHeader(const QByteArray &checksumType, const QByteArray &checksum); OCSYNC_EXPORT QByteArray makeChecksumHeader(const QByteArray &checksumType, const QByteArray &checksum);

View file

@ -358,26 +358,19 @@ QString FileSystem::fileSystemForPath(const QString &path)
#define BUFSIZE qint64(500 * 1024) // 500 KiB #define BUFSIZE qint64(500 * 1024) // 500 KiB
static QByteArray readToCrypto(const QString &filename, QCryptographicHash::Algorithm algo) static QByteArray readToCrypto( const QString& filename, QCryptographicHash::Algorithm algo )
{ {
QFile file(filename); QFile file(filename);
const qint64 bufSize = qMin(BUFSIZE, file.size() + 1); QByteArray arr;
QByteArray buf(bufSize, Qt::Uninitialized); QCryptographicHash crypto( algo );
QByteArray arr;
QCryptographicHash crypto(algo);
if (file.open(QIODevice::ReadOnly)) { if (file.open(QIODevice::ReadOnly)) {
qint64 size; if (crypto.addData(&file)) {
while (!file.atEnd()) { arr = crypto.result().toHex();
size = file.read(buf.data(), bufSize); }
if (size > 0) { }
crypto.addData(buf.data(), size); return arr;
} }
}
arr = crypto.result().toHex();
}
return arr;
}
QByteArray FileSystem::calcMd5(const QString &filename) QByteArray FileSystem::calcMd5(const QString &filename)
{ {

View file

@ -85,24 +85,32 @@ bool SqlDatabase::openHelper(const QString &filename, int sqliteFlags)
return true; return true;
} }
bool SqlDatabase::checkDb() SqlDatabase::CheckDbResult SqlDatabase::checkDb()
{ {
// quick_check can fail with a disk IO error when diskspace is low // quick_check can fail with a disk IO error when diskspace is low
SqlQuery quick_check(*this); SqlQuery quick_check(*this);
quick_check.prepare("PRAGMA quick_check;", /*allow_failure=*/true);
if (quick_check.prepare("PRAGMA quick_check;", /*allow_failure=*/true) != SQLITE_OK) {
qCWarning(lcSql) << "Error preparing quick_check on database";
_errId = quick_check.errorId();
_error = quick_check.error();
return CheckDbResult::CantPrepare;
}
if (!quick_check.exec()) { if (!quick_check.exec()) {
qCWarning(lcSql) << "Error running quick_check on database"; qCWarning(lcSql) << "Error running quick_check on database";
return false; _errId = quick_check.errorId();
_error = quick_check.error();
return CheckDbResult::CantExec;
} }
quick_check.next(); quick_check.next();
QString result = quick_check.stringValue(0); QString result = quick_check.stringValue(0);
if (result != "ok") { if (result != "ok") {
qCWarning(lcSql) << "quick_check returned failure:" << result; qCWarning(lcSql) << "quick_check returned failure:" << result;
return false; return CheckDbResult::NotOk;
} }
return true; return CheckDbResult::Ok;
} }
bool SqlDatabase::openOrCreateReadWrite(const QString &filename) bool SqlDatabase::openOrCreateReadWrite(const QString &filename)
@ -115,13 +123,25 @@ bool SqlDatabase::openOrCreateReadWrite(const QString &filename)
return false; return false;
} }
if (!checkDb()) { auto checkResult = checkDb();
// When disk space is low, checking the db may fail even though it's fine. if (checkResult != CheckDbResult::Ok) {
qint64 freeSpace = Utility::freeDiskSpace(QFileInfo(filename).dir().absolutePath()); if (checkResult == CheckDbResult::CantPrepare) {
if (freeSpace != -1 && freeSpace < 1000000) { // When disk space is low, preparing may fail even though the db is fine.
qCWarning(lcSql) << "Consistency check failed, disk space is low, aborting" << freeSpace; // Typically CANTOPEN or IOERR.
close(); qint64 freeSpace = Utility::freeDiskSpace(QFileInfo(filename).dir().absolutePath());
return false; if (freeSpace != -1 && freeSpace < 1000000) {
qCWarning(lcSql) << "Can't prepare consistency check and disk space is low:" << freeSpace;
close();
return false;
}
// Even when there's enough disk space, it might very well be that the
// file is on a read-only filesystem and can't be opened because of that.
if (_errId == SQLITE_CANTOPEN) {
qCWarning(lcSql) << "Can't open db to prepare consistency check, aborting";
close();
return false;
}
} }
qCCritical(lcSql) << "Consistency check failed, removing broken db" << filename; qCCritical(lcSql) << "Consistency check failed, removing broken db" << filename;
@ -144,7 +164,7 @@ bool SqlDatabase::openReadOnly(const QString &filename)
return false; return false;
} }
if (!checkDb()) { if (checkDb() != CheckDbResult::Ok) {
qCWarning(lcSql) << "Consistency check failed in readonly mode, giving up" << filename; qCWarning(lcSql) << "Consistency check failed in readonly mode, giving up" << filename;
close(); close();
return false; return false;
@ -164,8 +184,8 @@ void SqlDatabase::close()
{ {
if (_db) { if (_db) {
SQLITE_DO(sqlite3_close(_db)); SQLITE_DO(sqlite3_close(_db));
// Fatal because reopening an unclosed db might be problematic. if (_errId != SQLITE_OK)
ENFORCE(_errId == SQLITE_OK, "Error when closing DB"); qCWarning(lcSql) << "Closing database failed" << _error;
_db = 0; _db = 0;
} }
} }

View file

@ -48,8 +48,15 @@ public:
sqlite3 *sqliteDb(); sqlite3 *sqliteDb();
private: private:
enum class CheckDbResult {
Ok,
CantPrepare,
CantExec,
NotOk,
};
bool openHelper(const QString &filename, int sqliteFlags); bool openHelper(const QString &filename, int sqliteFlags);
bool checkDb(); CheckDbResult checkDb();
sqlite3 *_db; sqlite3 *_db;
QString _error; // last error string QString _error; // last error string

View file

@ -547,13 +547,28 @@ bool SyncJournalDb::checkConnect()
return sqlFail("prepare _getFileRecordQueryByFileId", *_getFileRecordQueryByFileId); return sqlFail("prepare _getFileRecordQueryByFileId", *_getFileRecordQueryByFileId);
} }
// This query is used to skip discovery and fill the tree from the
// database instead
_getFilesBelowPathQuery.reset(new SqlQuery(_db)); _getFilesBelowPathQuery.reset(new SqlQuery(_db));
if (_getFilesBelowPathQuery->prepare( if (_getFilesBelowPathQuery->prepare(
GET_FILE_RECORD_QUERY GET_FILE_RECORD_QUERY
" WHERE path > (?1||'/') AND path < (?1||'0') ORDER BY path||'/' ASC")) { " WHERE path > (?1||'/') AND path < (?1||'0')"
// We want to ensure that the contents of a directory are sorted
// directly behind the directory itself. Without this ORDER BY
// an ordering like foo, foo-2, foo/file would be returned.
// With the trailing /, we get foo-2, foo, foo/file. This property
// is used in fill_tree_from_db().
" ORDER BY path||'/' ASC")) {
return sqlFail("prepare _getFilesBelowPathQuery", *_getFilesBelowPathQuery); return sqlFail("prepare _getFilesBelowPathQuery", *_getFilesBelowPathQuery);
} }
_getAllFilesQuery.reset(new SqlQuery(_db));
if (_getAllFilesQuery->prepare(
GET_FILE_RECORD_QUERY
" ORDER BY path||'/' ASC")) {
return sqlFail("prepare _getAllFilesQuery", *_getAllFilesQuery);
}
_setFileRecordQuery.reset(new SqlQuery(_db)); _setFileRecordQuery.reset(new SqlQuery(_db));
if (_setFileRecordQuery->prepare("INSERT OR REPLACE INTO metadata " if (_setFileRecordQuery->prepare("INSERT OR REPLACE INTO metadata "
"(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, contentChecksum, contentChecksumTypeId) " "(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, contentChecksum, contentChecksumTypeId) "
@ -704,6 +719,7 @@ void SyncJournalDb::close()
_getFileRecordQueryByInode.reset(0); _getFileRecordQueryByInode.reset(0);
_getFileRecordQueryByFileId.reset(0); _getFileRecordQueryByFileId.reset(0);
_getFilesBelowPathQuery.reset(0); _getFilesBelowPathQuery.reset(0);
_getAllFilesQuery.reset(0);
_setFileRecordQuery.reset(0); _setFileRecordQuery.reset(0);
_setFileRecordChecksumQuery.reset(0); _setFileRecordChecksumQuery.reset(0);
_setFileRecordLocalMetadataQuery.reset(0); _setFileRecordLocalMetadataQuery.reset(0);
@ -1094,15 +1110,10 @@ bool SyncJournalDb::getFileRecordByInode(quint64 inode, SyncJournalFileRecord *r
return true; return true;
} }
bool SyncJournalDb::getFileRecordByFileId(const QByteArray &fileId, SyncJournalFileRecord *rec) bool SyncJournalDb::getFileRecordsByFileId(const QByteArray &fileId, const std::function<void(const SyncJournalFileRecord &)> &rowCallback)
{ {
QMutexLocker locker(&_mutex); QMutexLocker locker(&_mutex);
// Reset the output var in case the caller is reusing it.
Q_ASSERT(rec);
rec->_path.clear();
Q_ASSERT(!rec->isValid());
if (fileId.isEmpty() || _metadataTableIsEmpty) if (fileId.isEmpty() || _metadataTableIsEmpty)
return true; // no error, yet nothing found (rec->isValid() == false) return true; // no error, yet nothing found (rec->isValid() == false)
@ -1116,8 +1127,10 @@ bool SyncJournalDb::getFileRecordByFileId(const QByteArray &fileId, SyncJournalF
return false; return false;
} }
if (_getFileRecordQueryByFileId->next()) { while (_getFileRecordQueryByFileId->next()) {
fillFileRecordFromGetQuery(*rec, *_getFileRecordQueryByFileId); SyncJournalFileRecord rec;
fillFileRecordFromGetQuery(rec, *_getFileRecordQueryByFileId);
rowCallback(rec);
} }
return true; return true;
@ -1133,16 +1146,23 @@ bool SyncJournalDb::getFilesBelowPath(const QByteArray &path, const std::functio
if (!checkConnect()) if (!checkConnect())
return false; return false;
_getFilesBelowPathQuery->reset_and_clear_bindings(); // Since the path column doesn't store the starting /, the getFilesBelowPathQuery
_getFilesBelowPathQuery->bindValue(1, path); // can't be used for the root path "". It would scan for (path > '/' and path < '0')
// and find nothing. So, unfortunately, we have to use a different query for
// retrieving the whole tree.
auto &query = path.isEmpty() ? _getAllFilesQuery : _getFilesBelowPathQuery;
if (!_getFilesBelowPathQuery->exec()) { query->reset_and_clear_bindings();
if (query == _getFilesBelowPathQuery)
query->bindValue(1, path);
if (!query->exec()) {
return false; return false;
} }
while (_getFilesBelowPathQuery->next()) { while (query->next()) {
SyncJournalFileRecord rec; SyncJournalFileRecord rec;
fillFileRecordFromGetQuery(rec, *_getFilesBelowPathQuery); fillFileRecordFromGetQuery(rec, *query);
rowCallback(rec); rowCallback(rec);
} }

View file

@ -58,7 +58,7 @@ public:
bool getFileRecord(const QString &filename, SyncJournalFileRecord *rec) { return getFileRecord(filename.toUtf8(), rec); } bool getFileRecord(const QString &filename, SyncJournalFileRecord *rec) { return getFileRecord(filename.toUtf8(), rec); }
bool getFileRecord(const QByteArray &filename, SyncJournalFileRecord *rec); bool getFileRecord(const QByteArray &filename, SyncJournalFileRecord *rec);
bool getFileRecordByInode(quint64 inode, SyncJournalFileRecord *rec); bool getFileRecordByInode(quint64 inode, SyncJournalFileRecord *rec);
bool getFileRecordByFileId(const QByteArray &fileId, SyncJournalFileRecord *rec); bool getFileRecordsByFileId(const QByteArray &fileId, const std::function<void(const SyncJournalFileRecord &)> &rowCallback);
bool getFilesBelowPath(const QByteArray &path, const std::function<void(const SyncJournalFileRecord&)> &rowCallback); bool getFilesBelowPath(const QByteArray &path, const std::function<void(const SyncJournalFileRecord&)> &rowCallback);
bool setFileRecord(const SyncJournalFileRecord &record); bool setFileRecord(const SyncJournalFileRecord &record);
@ -244,6 +244,7 @@ private:
QScopedPointer<SqlQuery> _getFileRecordQueryByInode; QScopedPointer<SqlQuery> _getFileRecordQueryByInode;
QScopedPointer<SqlQuery> _getFileRecordQueryByFileId; QScopedPointer<SqlQuery> _getFileRecordQueryByFileId;
QScopedPointer<SqlQuery> _getFilesBelowPathQuery; QScopedPointer<SqlQuery> _getFilesBelowPathQuery;
QScopedPointer<SqlQuery> _getAllFilesQuery;
QScopedPointer<SqlQuery> _setFileRecordQuery; QScopedPointer<SqlQuery> _setFileRecordQuery;
QScopedPointer<SqlQuery> _setFileRecordChecksumQuery; QScopedPointer<SqlQuery> _setFileRecordChecksumQuery;
QScopedPointer<SqlQuery> _setFileRecordLocalMetadataQuery; QScopedPointer<SqlQuery> _setFileRecordLocalMetadataQuery;

View file

@ -579,7 +579,7 @@ bool Utility::isConflictFile(const char *name)
bool Utility::shouldUploadConflictFiles() bool Utility::shouldUploadConflictFiles()
{ {
static bool uploadConflictFiles = qgetenv("OWNCLOUD_UPLOAD_CONFLICT_FILES").toInt() != 0; static bool uploadConflictFiles = qEnvironmentVariableIntValue("OWNCLOUD_UPLOAD_CONFLICT_FILES") != 0;
return uploadConflictFiles; return uploadConflictFiles;
} }

View file

@ -28,8 +28,13 @@
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QMap> #include <QMap>
#include <QUrl> #include <QUrl>
#include <functional>
#include <memory> #include <memory>
#ifdef Q_OS_WIN
#include <windows.h>
#endif
class QSettings; class QSettings;
namespace OCC { namespace OCC {
@ -187,6 +192,14 @@ namespace Utility {
* Experimental! Real feature planned for 2.5. * Experimental! Real feature planned for 2.5.
*/ */
OCSYNC_EXPORT bool shouldUploadConflictFiles(); OCSYNC_EXPORT bool shouldUploadConflictFiles();
#ifdef Q_OS_WIN
OCSYNC_EXPORT QVariant registryGetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName);
OCSYNC_EXPORT bool registrySetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName, DWORD type, const QVariant &value);
OCSYNC_EXPORT bool registryDeleteKeyTree(HKEY hRootKey, const QString &subKey);
OCSYNC_EXPORT bool registryDeleteKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName);
OCSYNC_EXPORT bool registryWalkSubKeys(HKEY hRootKey, const QString &subKey, const std::function<void(HKEY, const QString &)> &callback);
#endif
} }
/** @} */ // \addtogroup /** @} */ // \addtogroup

View file

@ -16,8 +16,7 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
#define _WIN32_WINNT 0x0600 #include "asserts.h"
#define WINVER 0x0600
#include <shlobj.h> #include <shlobj.h>
#include <winbase.h> #include <winbase.h>
#include <windows.h> #include <windows.h>
@ -93,4 +92,168 @@ static inline bool hasDarkSystray_private()
return true; return true;
} }
QVariant Utility::registryGetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName)
{
QVariant value;
HKEY hKey;
REGSAM sam = KEY_READ | KEY_WOW64_64KEY;
LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.utf16()), 0, sam, &hKey);
ASSERT(result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND);
if (result != ERROR_SUCCESS)
return value;
DWORD type = 0, sizeInBytes = 0;
result = RegQueryValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.utf16()), 0, &type, nullptr, &sizeInBytes);
ASSERT(result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND);
if (result == ERROR_SUCCESS) {
switch (type) {
case REG_DWORD:
DWORD dword;
Q_ASSERT(sizeInBytes == sizeof(dword));
if (RegQueryValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.utf16()), 0, &type, reinterpret_cast<LPBYTE>(&dword), &sizeInBytes) == ERROR_SUCCESS) {
value = int(dword);
}
break;
case REG_EXPAND_SZ:
case REG_SZ: {
QString string;
string.resize(sizeInBytes / sizeof(QChar));
result = RegQueryValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.utf16()), 0, &type, reinterpret_cast<LPBYTE>(string.data()), &sizeInBytes);
if (result == ERROR_SUCCESS) {
int newCharSize = sizeInBytes / sizeof(QChar);
// From the doc:
// If the data has the REG_SZ, REG_MULTI_SZ or REG_EXPAND_SZ type, the string may not have been stored with
// the proper terminating null characters. Therefore, even if the function returns ERROR_SUCCESS,
// the application should ensure that the string is properly terminated before using it; otherwise, it may overwrite a buffer.
if (string.at(newCharSize - 1) == QChar('\0'))
string.resize(newCharSize - 1);
value = string;
}
break;
}
default:
Q_UNREACHABLE();
}
}
ASSERT(result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND);
RegCloseKey(hKey);
return value;
}
bool Utility::registrySetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName, DWORD type, const QVariant &value)
{
HKEY hKey;
// KEY_WOW64_64KEY is necessary because CLSIDs are "Redirected and reflected only for CLSIDs that do not specify InprocServer32 or InprocHandler32."
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa384253%28v=vs.85%29.aspx#redirected__shared__and_reflected_keys_under_wow64
// This shouldn't be an issue in our case since we use shell32.dll as InprocServer32, so we could write those registry keys for both 32 and 64bit.
// FIXME: Not doing so at the moment means that explorer will show the cloud provider, but 32bit processes' open dialogs (like the ownCloud client itself) won't show it.
REGSAM sam = KEY_WRITE | KEY_WOW64_64KEY;
LONG result = RegCreateKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.utf16()), 0, nullptr, 0, sam, nullptr, &hKey, nullptr);
ASSERT(result == ERROR_SUCCESS);
if (result != ERROR_SUCCESS)
return false;
result = -1;
switch (type) {
case REG_DWORD: {
DWORD dword = value.toInt();
result = RegSetValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.utf16()), 0, type, reinterpret_cast<const BYTE *>(&dword), sizeof(dword));
break;
}
case REG_EXPAND_SZ:
case REG_SZ: {
QString string = value.toString();
result = RegSetValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.utf16()), 0, type, reinterpret_cast<const BYTE *>(string.constData()), (string.size() + 1) * sizeof(QChar));
break;
}
default:
Q_UNREACHABLE();
}
ASSERT(result == ERROR_SUCCESS);
RegCloseKey(hKey);
return result == ERROR_SUCCESS;
}
bool Utility::registryDeleteKeyTree(HKEY hRootKey, const QString &subKey)
{
HKEY hKey;
REGSAM sam = DELETE | KEY_ENUMERATE_SUB_KEYS | KEY_QUERY_VALUE | KEY_SET_VALUE | KEY_WOW64_64KEY;
LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.utf16()), 0, sam, &hKey);
ASSERT(result == ERROR_SUCCESS);
if (result != ERROR_SUCCESS)
return false;
result = RegDeleteTree(hKey, nullptr);
RegCloseKey(hKey);
ASSERT(result == ERROR_SUCCESS);
result |= RegDeleteKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.utf16()), sam, 0);
ASSERT(result == ERROR_SUCCESS);
return result == ERROR_SUCCESS;
}
bool Utility::registryDeleteKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName)
{
HKEY hKey;
REGSAM sam = KEY_WRITE | KEY_WOW64_64KEY;
LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.utf16()), 0, sam, &hKey);
ASSERT(result == ERROR_SUCCESS);
if (result != ERROR_SUCCESS)
return false;
result = RegDeleteValue(hKey, reinterpret_cast<LPCWSTR>(valueName.utf16()));
ASSERT(result == ERROR_SUCCESS);
RegCloseKey(hKey);
return result == ERROR_SUCCESS;
}
bool Utility::registryWalkSubKeys(HKEY hRootKey, const QString &subKey, const std::function<void(HKEY, const QString &)> &callback)
{
HKEY hKey;
REGSAM sam = KEY_READ | KEY_WOW64_64KEY;
LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(subKey.utf16()), 0, sam, &hKey);
ASSERT(result == ERROR_SUCCESS);
if (result != ERROR_SUCCESS)
return false;
DWORD maxSubKeyNameSize;
// Get the largest keyname size once instead of relying each call on ERROR_MORE_DATA.
result = RegQueryInfoKey(hKey, nullptr, nullptr, nullptr, nullptr, &maxSubKeyNameSize, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
ASSERT(result == ERROR_SUCCESS);
if (result != ERROR_SUCCESS) {
RegCloseKey(hKey);
return false;
}
QString subKeyName;
subKeyName.reserve(maxSubKeyNameSize + 1);
DWORD retCode = ERROR_SUCCESS;
for (DWORD i = 0; retCode == ERROR_SUCCESS; ++i) {
Q_ASSERT(unsigned(subKeyName.capacity()) > maxSubKeyNameSize);
// Make the previously reserved capacity official again.
subKeyName.resize(subKeyName.capacity());
DWORD subKeyNameSize = subKeyName.size();
retCode = RegEnumKeyEx(hKey, i, reinterpret_cast<LPWSTR>(subKeyName.data()), &subKeyNameSize, nullptr, nullptr, nullptr, nullptr);
ASSERT(result == ERROR_SUCCESS || retCode == ERROR_NO_MORE_ITEMS);
if (retCode == ERROR_SUCCESS) {
// subKeyNameSize excludes the trailing \0
subKeyName.resize(subKeyNameSize);
// Pass only the sub keyname, not the full path.
callback(hKey, subKeyName);
}
}
RegCloseKey(hKey);
return retCode != ERROR_NO_MORE_ITEMS;
}
} // namespace OCC } // namespace OCC

View file

@ -314,6 +314,9 @@ int csync_s::reinitialize() {
local.files.clear(); local.files.clear();
remote.files.clear(); remote.files.clear();
local_discovery_style = LocalDiscoveryStyle::FilesystemOnly;
locally_touched_dirs.clear();
status = CSYNC_STATUS_INIT; status = CSYNC_STATUS_INIT;
SAFE_FREE(error_string); SAFE_FREE(error_string);

View file

@ -38,6 +38,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <sqlite3.h> #include <sqlite3.h>
#include <map> #include <map>
#include <set>
#include "common/syncjournaldb.h" #include "common/syncjournaldb.h"
#include "config_csync.h" #include "config_csync.h"
@ -70,6 +71,11 @@ enum csync_replica_e {
REMOTE_REPLICA REMOTE_REPLICA
}; };
enum class LocalDiscoveryStyle {
FilesystemOnly, //< read all local data from the filesystem
DatabaseAndFilesystem, //< read from the db, except for listed paths
};
/* /*
* This is a structurere similar to QStringRef * This is a structurere similar to QStringRef
@ -190,6 +196,16 @@ struct OCSYNC_EXPORT csync_s {
*/ */
bool read_remote_from_db = false; bool read_remote_from_db = false;
LocalDiscoveryStyle local_discovery_style = LocalDiscoveryStyle::FilesystemOnly;
/**
* List of folder-relative directory paths that should be scanned on the
* filesystem if the local_discovery_style suggests it.
*
* Their parents will be scanned too. The paths don't start with a /.
*/
std::set<QByteArray> locally_touched_dirs;
bool ignore_hidden_files = true; bool ignore_hidden_files = true;
csync_s(const char *localUri, OCC::SyncJournalDb *statedb); csync_s(const char *localUri, OCC::SyncJournalDb *statedb);

View file

@ -63,18 +63,6 @@ static csync_file_stat_t *_csync_check_ignored(csync_s::FileMap *tree, const Byt
} }
} }
/* Returns true if we're reasonably certain that hash equality
* for the header means content equality.
*
* Cryptographic safety is not required - this is mainly
* intended to rule out checksums like Adler32 that are not intended for
* hashing and have high likelihood of collision with particular inputs.
*/
static bool _csync_is_collision_safe_hash(const char *checksum_header)
{
return strncmp(checksum_header, "SHA1:", 5) == 0
|| strncmp(checksum_header, "MD5:", 4) == 0;
}
/** /**
* The main function in the reconcile pass. * The main function in the reconcile pass.
@ -104,14 +92,17 @@ static bool _csync_is_collision_safe_hash(const char *checksum_header)
* source and the destination, have been changed, the newer file wins. * source and the destination, have been changed, the newer file wins.
*/ */
static int _csync_merge_algorithm_visitor(csync_file_stat_t *cur, CSYNC * ctx) { static int _csync_merge_algorithm_visitor(csync_file_stat_t *cur, CSYNC * ctx) {
csync_s::FileMap *our_tree = nullptr;
csync_s::FileMap *other_tree = nullptr; csync_s::FileMap *other_tree = nullptr;
/* we need the opposite tree! */ /* we need the opposite tree! */
switch (ctx->current) { switch (ctx->current) {
case LOCAL_REPLICA: case LOCAL_REPLICA:
our_tree = &ctx->local.files;
other_tree = &ctx->remote.files; other_tree = &ctx->remote.files;
break; break;
case REMOTE_REPLICA: case REMOTE_REPLICA:
our_tree = &ctx->remote.files;
other_tree = &ctx->local.files; other_tree = &ctx->local.files;
break; break;
default: default:
@ -152,40 +143,51 @@ static int _csync_merge_algorithm_visitor(csync_file_stat_t *cur, CSYNC * ctx) {
cur->instruction = CSYNC_INSTRUCTION_REMOVE; cur->instruction = CSYNC_INSTRUCTION_REMOVE;
break; break;
case CSYNC_INSTRUCTION_EVAL_RENAME: { case CSYNC_INSTRUCTION_EVAL_RENAME: {
OCC::SyncJournalFileRecord base; // By default, the EVAL_RENAME decays into a NEW
if(ctx->current == LOCAL_REPLICA ) { cur->instruction = CSYNC_INSTRUCTION_NEW;
/* use the old name to find the "other" node */
ctx->statedb->getFileRecordByInode(cur->inode, &base); bool processedRename = false;
qCDebug(lcReconcile, "Finding opposite temp through inode %" PRIu64 ": %s", auto renameCandidateProcessing = [&](const OCC::SyncJournalFileRecord &base) {
cur->inode, base.isValid() ? "true":"false"); if (processedRename)
} else { return;
ASSERT( ctx->current == REMOTE_REPLICA ); if (!base.isValid())
ctx->statedb->getFileRecordByFileId(cur->file_id, &base); return;
qCDebug(lcReconcile, "Finding opposite temp through file ID %s: %s",
cur->file_id.constData(), base.isValid() ? "true":"false");
}
if( base.isValid() ) {
/* First, check that the file is NOT in our tree (another file with the same name was added) */ /* First, check that the file is NOT in our tree (another file with the same name was added) */
csync_s::FileMap *our_tree = ctx->current == REMOTE_REPLICA ? &ctx->remote.files : &ctx->local.files; if (our_tree->findFile(base._path)) {
if (our_tree->findFile(base._path)) { qCDebug(lcReconcile, "Origin found in our tree : %s", base._path.constData());
qCDebug(lcReconcile, "Origin found in our tree : %s", base._path.constData());
} else { } else {
/* Find the temporar file in the other tree. /* Find the potential rename source file in the other tree.
* If the renamed file could not be found in the opposite tree, that is because it * If the renamed file could not be found in the opposite tree, that is because it
* is not longer existing there, maybe because it was renamed or deleted. * is not longer existing there, maybe because it was renamed or deleted.
* The journal is cleaned up later after propagation. * The journal is cleaned up later after propagation.
*/ */
other = other_tree->findFile(base._path); other = other_tree->findFile(base._path);
qCDebug(lcReconcile, "Temporary opposite (%s) %s", qCDebug(lcReconcile, "Rename origin in other tree (%s) %s",
base._path.constData() , other ? "found": "not found" ); base._path.constData(), other ? "found" : "not found");
} }
if(!other) { if(!other) {
cur->instruction = CSYNC_INSTRUCTION_NEW; // Stick with the NEW
} else if (other->instruction == CSYNC_INSTRUCTION_NONE return;
|| other->instruction == CSYNC_INSTRUCTION_UPDATE_METADATA } else if (other->instruction == CSYNC_INSTRUCTION_RENAME) {
|| cur->type == CSYNC_FTW_TYPE_DIR) { // Some other EVAL_RENAME already claimed other.
// We do nothing: maybe a different candidate for
// other is found as well?
qCDebug(lcReconcile, "Other has already been renamed to %s",
other->rename_path.constData());
} else if (cur->type == CSYNC_FTW_TYPE_DIR
// The local replica is reconciled first, so the remote tree would
// have either NONE or UPDATE_METADATA if the remote file is safe to
// move.
// In the remote replica, REMOVE is also valid (local has already
// been reconciled). NONE can still happen if the whole parent dir
// was set to REMOVE by the local reconcile.
|| other->instruction == CSYNC_INSTRUCTION_NONE
|| other->instruction == CSYNC_INSTRUCTION_UPDATE_METADATA
|| other->instruction == CSYNC_INSTRUCTION_REMOVE) {
qCDebug(lcReconcile, "Switching %s to RENAME to %s",
other->path.constData(), cur->path.constData());
other->instruction = CSYNC_INSTRUCTION_RENAME; other->instruction = CSYNC_INSTRUCTION_RENAME;
other->rename_path = cur->path; other->rename_path = cur->path;
if( !cur->file_id.isEmpty() ) { if( !cur->file_id.isEmpty() ) {
@ -193,24 +195,44 @@ static int _csync_merge_algorithm_visitor(csync_file_stat_t *cur, CSYNC * ctx) {
} }
other->inode = cur->inode; other->inode = cur->inode;
cur->instruction = CSYNC_INSTRUCTION_NONE; cur->instruction = CSYNC_INSTRUCTION_NONE;
} else if (other->instruction == CSYNC_INSTRUCTION_REMOVE) { // We have consumed 'other': exit this loop to not consume another one.
other->instruction = CSYNC_INSTRUCTION_RENAME; processedRename = true;
other->rename_path = cur->path; } else if (our_tree->findFile(csync_rename_adjust_path(ctx, other->path)) == cur) {
// If we're here, that means that the other side's reconcile will be able
// to work against cur: The filename itself didn't change, only a parent
// directory was renamed! In that case it's safe to ignore the rename
// since the parent directory rename will already deal with it.
if( !cur->file_id.isEmpty() ) { // Local: The remote reconcile will be able to deal with this.
other->file_id = cur->file_id; // Remote: The local replica has already dealt with this.
} // See the EVAL_RENAME case when other was found directly.
other->inode = cur->inode; qCDebug(lcReconcile, "File in a renamed directory, other side's instruction: %d",
other->instruction);
cur->instruction = CSYNC_INSTRUCTION_NONE; cur->instruction = CSYNC_INSTRUCTION_NONE;
} else if (other->instruction == CSYNC_INSTRUCTION_NEW) {
qCDebug(lcReconcile, "OOOO=> NEW detected in other tree!");
cur->instruction = CSYNC_INSTRUCTION_CONFLICT;
} else { } else {
assert(other->type != CSYNC_FTW_TYPE_DIR); // This can, for instance, happen when there was a local change in other
cur->instruction = CSYNC_INSTRUCTION_NONE; // and the instruction in the local tree is NEW while cur has EVAL_RENAME
other->instruction = CSYNC_INSTRUCTION_SYNC; // due to a remote move of the same file. In these scenarios we just
// want the instruction to stay NEW.
qCDebug(lcReconcile, "Other already has instruction %d",
other->instruction);
} }
};
if (ctx->current == LOCAL_REPLICA) {
/* use the old name to find the "other" node */
OCC::SyncJournalFileRecord base;
qCDebug(lcReconcile, "Finding rename origin through inode %" PRIu64 "",
cur->inode);
ctx->statedb->getFileRecordByInode(cur->inode, &base);
renameCandidateProcessing(base);
} else {
ASSERT(ctx->current == REMOTE_REPLICA);
qCDebug(lcReconcile, "Finding rename origin through file ID %s",
cur->file_id.constData());
ctx->statedb->getFileRecordsByFileId(cur->file_id, renameCandidateProcessing);
} }
break; break;
} }
default: default:
@ -263,14 +285,14 @@ static int _csync_merge_algorithm_visitor(csync_file_stat_t *cur, CSYNC * ctx) {
// //
// In older client versions we always treated these cases as a // In older client versions we always treated these cases as a
// non-conflict. This behavior is preserved in case the server // non-conflict. This behavior is preserved in case the server
// doesn't provide a suitable content hash. // doesn't provide a content checksum.
// //
// When it does have one, however, we do create a job, but the job // When it does have one, however, we do create a job, but the job
// will compare hashes and avoid the download if they are equal. // will compare hashes and avoid the download if possible.
const char *remoteChecksumHeader = QByteArray remoteChecksumHeader =
(ctx->current == REMOTE_REPLICA ? cur->checksumHeader : other->checksumHeader); (ctx->current == REMOTE_REPLICA ? cur->checksumHeader : other->checksumHeader);
if (remoteChecksumHeader) { if (!remoteChecksumHeader.isEmpty()) {
is_conflict |= _csync_is_collision_safe_hash(remoteChecksumHeader); is_conflict = true;
} }
// SO: If there is no checksum, we can have !is_conflict here // SO: If there is no checksum, we can have !is_conflict here
@ -310,10 +332,19 @@ static int _csync_merge_algorithm_visitor(csync_file_stat_t *cur, CSYNC * ctx) {
break; break;
case CSYNC_INSTRUCTION_IGNORE: case CSYNC_INSTRUCTION_IGNORE:
cur->instruction = CSYNC_INSTRUCTION_IGNORE; cur->instruction = CSYNC_INSTRUCTION_IGNORE;
break; break;
default: default:
break; break;
} }
// Ensure we're not leaving discovery-only instructions
// in place. This can happen, for instance, when other's
// instruction is EVAL_RENAME because the parent dir was renamed.
// NEW is safer than EVAL because it will end up with
// propagation unless it's changed by something, and EVAL and
// NEW are treated equivalently during reconcile.
if (cur->instruction == CSYNC_INSTRUCTION_EVAL)
cur->instruction = CSYNC_INSTRUCTION_NEW;
break;
default: default:
break; break;
} }

View file

@ -297,41 +297,60 @@ static int _csync_detect_update(CSYNC *ctx, std::unique_ptr<csync_file_stat_t> f
} else { } else {
/* Remote Replica Rename check */ /* Remote Replica Rename check */
OCC::SyncJournalFileRecord base; fs->instruction = CSYNC_INSTRUCTION_NEW;
if(!ctx->statedb->getFileRecordByFileId(fs->file_id, &base)) {
bool done = false;
auto renameCandidateProcessing = [&](const OCC::SyncJournalFileRecord &base) {
if (done)
return;
if (!base.isValid())
return;
// Some things prohibit rename detection entirely.
// Since we don't do the same checks again in reconcile, we can't
// just skip the candidate, but have to give up completely.
if (base._type != fs->type) {
qCWarning(lcUpdate, "file types different, not a rename");
done = true;
return;
}
if (fs->type != CSYNC_FTW_TYPE_DIR && base._etag != fs->etag) {
/* File with different etag, don't do a rename, but download the file again */
qCWarning(lcUpdate, "file etag different, not a rename");
done = true;
return;
}
// Record directory renames
if (fs->type == CSYNC_FTW_TYPE_DIR) {
// If the same folder was already renamed by a different entry,
// skip to the next candidate
if (ctx->renames.folder_renamed_to.count(base._path) > 0) {
qCWarning(lcUpdate, "folder already has a rename entry, skipping");
return;
}
csync_rename_record(ctx, base._path, fs->path);
}
qCDebug(lcUpdate, "remote rename detected based on fileid %s --> %s", base._path.constData(), fs->path.constData());
fs->instruction = CSYNC_INSTRUCTION_EVAL_RENAME;
done = true;
};
if (!ctx->statedb->getFileRecordsByFileId(fs->file_id, renameCandidateProcessing)) {
ctx->status_code = CSYNC_STATUS_UNSUCCESSFUL; ctx->status_code = CSYNC_STATUS_UNSUCCESSFUL;
return -1; return -1;
} }
if (base.isValid()) { /* tmp existing at all */
if (base._type != fs->type) {
qCWarning(lcUpdate, "file types different is not!");
fs->instruction = CSYNC_INSTRUCTION_NEW;
goto out;
}
qCDebug(lcUpdate, "remote rename detected based on fileid %s --> %s", base._path.constData(), fs->path.constData());
fs->instruction = CSYNC_INSTRUCTION_EVAL_RENAME;
if (fs->type == CSYNC_FTW_TYPE_DIR) {
csync_rename_record(ctx, base._path, fs->path);
} else {
if( base._etag != fs->etag ) {
/* CSYNC_LOG(CSYNC_LOG_PRIORITY_DEBUG, "ETags are different!"); */
/* File with different etag, don't do a rename, but download the file again */
fs->instruction = CSYNC_INSTRUCTION_NEW;
}
}
goto out;
} else { if (fs->instruction == CSYNC_INSTRUCTION_NEW
/* file not found in statedb */ && fs->type == CSYNC_FTW_TYPE_DIR
fs->instruction = CSYNC_INSTRUCTION_NEW; && ctx->current == REMOTE_REPLICA
&& ctx->callbacks.checkSelectiveSyncNewFolderHook) {
if (fs->type == CSYNC_FTW_TYPE_DIR && ctx->current == REMOTE_REPLICA && ctx->callbacks.checkSelectiveSyncNewFolderHook) { if (ctx->callbacks.checkSelectiveSyncNewFolderHook(ctx->callbacks.update_callback_userdata, fs->path, fs->remotePerm)) {
if (ctx->callbacks.checkSelectiveSyncNewFolderHook(ctx->callbacks.update_callback_userdata, fs->path, fs->remotePerm)) { return 1;
return 1;
}
} }
goto out;
} }
goto out;
} }
} }
@ -435,28 +454,31 @@ static bool fill_tree_from_db(CSYNC *ctx, const char *uri)
{ {
int64_t count = 0; int64_t count = 0;
QByteArray skipbase; QByteArray skipbase;
auto rowCallback = [ctx, &count, &skipbase](const OCC::SyncJournalFileRecord &rec) { auto &files = ctx->current == LOCAL_REPLICA ? ctx->local.files : ctx->remote.files;
/* When selective sync is used, the database may have subtrees with a parent auto rowCallback = [ctx, &count, &skipbase, &files](const OCC::SyncJournalFileRecord &rec) {
* whose etag (md5) is _invalid_. These are ignored and shall not appear in the if (ctx->current == REMOTE_REPLICA) {
* remote tree. /* When selective sync is used, the database may have subtrees with a parent
* Sometimes folders that are not ignored by selective sync get marked as * whose etag is _invalid_. These are ignored and shall not appear in the
* _invalid_, but that is not a problem as the next discovery will retrieve * remote tree.
* their correct etags again and we don't run into this case. * Sometimes folders that are not ignored by selective sync get marked as
*/ * _invalid_, but that is not a problem as the next discovery will retrieve
if( rec._etag == "_invalid_") { * their correct etags again and we don't run into this case.
qCDebug(lcUpdate, "%s selective sync excluded", rec._path.constData()); */
skipbase = rec._path; if (rec._etag == "_invalid_") {
skipbase += '/'; qCDebug(lcUpdate, "%s selective sync excluded", rec._path.constData());
return; skipbase = rec._path;
} skipbase += '/';
return;
}
/* Skip over all entries with the same base path. Note that this depends /* Skip over all entries with the same base path. Note that this depends
* strongly on the ordering of the retrieved items. */ * strongly on the ordering of the retrieved items. */
if( !skipbase.isEmpty() && rec._path.startsWith(skipbase) ) { if (!skipbase.isEmpty() && rec._path.startsWith(skipbase)) {
qCDebug(lcUpdate, "%s selective sync excluded because the parent is", rec._path.constData()); qCDebug(lcUpdate, "%s selective sync excluded because the parent is", rec._path.constData());
return; return;
} else { } else {
skipbase.clear(); skipbase.clear();
}
} }
std::unique_ptr<csync_file_stat_t> st = csync_file_stat_t::fromSyncJournalFileRecord(rec); std::unique_ptr<csync_file_stat_t> st = csync_file_stat_t::fromSyncJournalFileRecord(rec);
@ -477,7 +499,7 @@ static bool fill_tree_from_db(CSYNC *ctx, const char *uri)
} }
/* store into result list. */ /* store into result list. */
ctx->remote.files[rec._path] = std::move(st); files[rec._path] = std::move(st);
++count; ++count;
}; };
@ -522,6 +544,26 @@ int csync_ftw(CSYNC *ctx, const char *uri, csync_walker_fn fn,
int rc = 0; int rc = 0;
bool do_read_from_db = (ctx->current == REMOTE_REPLICA && ctx->remote.read_from_db); bool do_read_from_db = (ctx->current == REMOTE_REPLICA && ctx->remote.read_from_db);
const char *db_uri = uri;
if (ctx->current == LOCAL_REPLICA
&& ctx->local_discovery_style == LocalDiscoveryStyle::DatabaseAndFilesystem) {
const char *local_uri = uri + strlen(ctx->local.uri);
if (*local_uri == '/')
++local_uri;
db_uri = local_uri;
do_read_from_db = true;
// Minor bug: local_uri doesn't have a trailing /. Example: Assume it's "d/foo"
// and we want to check whether we should read from the db. Assume "d/foo a" is
// in locally_touched_dirs. Then this check will say no, don't read from the db!
// (because "d/foo" < "d/foo a" < "d/foo/bar")
// C++14: Could skip the conversion to QByteArray here.
auto it = ctx->locally_touched_dirs.lower_bound(QByteArray(local_uri));
if (it != ctx->locally_touched_dirs.end() && it->startsWith(local_uri)) {
do_read_from_db = false;
}
}
if (!depth) { if (!depth) {
mark_current_item_ignored(ctx, previous_fs, CSYNC_STATUS_INDIVIDUAL_TOO_DEEP); mark_current_item_ignored(ctx, previous_fs, CSYNC_STATUS_INDIVIDUAL_TOO_DEEP);
@ -533,7 +575,7 @@ int csync_ftw(CSYNC *ctx, const char *uri, csync_walker_fn fn,
// if the etag of this dir is still the same, its content is restored from the // if the etag of this dir is still the same, its content is restored from the
// database. // database.
if( do_read_from_db ) { if( do_read_from_db ) {
if( ! fill_tree_from_db(ctx, uri) ) { if( ! fill_tree_from_db(ctx, db_uri) ) {
errno = ENOENT; errno = ENOENT;
ctx->status_code = CSYNC_STATUS_OPENDIR_ERROR; ctx->status_code = CSYNC_STATUS_OPENDIR_ERROR;
goto error; goto error;
@ -616,7 +658,7 @@ int csync_ftw(CSYNC *ctx, const char *uri, csync_walker_fn fn,
* local stat function. * local stat function.
*/ */
if( filename[0] == '.' ) { if( filename[0] == '.' ) {
if (filename == ".sys.admin#recall#") { /* recall file shall not be ignored (#4420) */ if (filename != ".sys.admin#recall#") { /* recall file shall not be ignored (#4420) */
dirent->is_hidden = true; dirent->is_hidden = true;
} }
} }

View file

@ -211,3 +211,10 @@ time_t oc_httpdate_parse( const char *date ) {
result = timegm(&gmt); result = timegm(&gmt);
return result; return result;
} }
bool csync_is_collision_safe_hash(const QByteArray &checksum_header)
{
return checksum_header.startsWith("SHA1:")
|| checksum_header.startsWith("MD5:");
}

View file

@ -31,4 +31,14 @@ const char OCSYNC_EXPORT *csync_instruction_str(enum csync_instructions_e instr)
void OCSYNC_EXPORT csync_memstat_check(void); void OCSYNC_EXPORT csync_memstat_check(void);
bool OCSYNC_EXPORT csync_file_locked_or_open( const char *dir, const char *fname); bool OCSYNC_EXPORT csync_file_locked_or_open( const char *dir, const char *fname);
/* Returns true if we're reasonably certain that hash equality
* for the header means content equality.
*
* Cryptographic safety is not required - this is mainly
* intended to rule out checksums like Adler32 that are not intended for
* hashing and have high likelihood of collision with particular inputs.
*/
bool OCSYNC_EXPORT csync_is_collision_safe_hash(const QByteArray &checksum_header);
#endif /* _CSYNC_UTIL_H */ #endif /* _CSYNC_UTIL_H */

View file

@ -57,6 +57,7 @@ set(client_SRCS
ignorelisteditor.cpp ignorelisteditor.cpp
lockwatcher.cpp lockwatcher.cpp
logbrowser.cpp logbrowser.cpp
navigationpanehelper.cpp
networksettings.cpp networksettings.cpp
ocsjob.cpp ocsjob.cpp
ocssharejob.cpp ocssharejob.cpp

View file

@ -401,6 +401,9 @@ void AccountSettings::slotFolderWizardAccepted()
*/ */
definition.ignoreHiddenFiles = folderMan->ignoreHiddenFiles(); definition.ignoreHiddenFiles = folderMan->ignoreHiddenFiles();
if (folderMan->navigationPaneHelper().showInExplorerNavigationPane())
definition.navigationPaneClsid = QUuid::createUuid();
auto selectiveSyncBlackList = folderWizard->property("selectiveSyncBlackList").toStringList(); auto selectiveSyncBlackList = folderWizard->property("selectiveSyncBlackList").toStringList();
folderMan->setSyncEnabled(true); folderMan->setSyncEnabled(true);

View file

@ -30,19 +30,21 @@ using namespace QKeychain;
namespace OCC { namespace OCC {
Q_LOGGING_CATEGORY(lcHttpCredentialsGui, "sync.credentials.http.gui", QtInfoMsg)
void HttpCredentialsGui::askFromUser() void HttpCredentialsGui::askFromUser()
{ {
// Unfortunately there's a bug that doesn't allow us to send the "is this // This function can be called from AccountState::slotInvalidCredentials,
// OAuth2 or Basic auth?" GET request directly. Scheduling it for the event // which (indirectly, through HttpCredentials::invalidateToken) schedules
// loop works though. See #5989. // a cache wipe of the qnam. We can only execute a network job again once
QMetaObject::invokeMethod(this, "askFromUserAsync", Qt::QueuedConnection); // the cache has been cleared, otherwise we'd interfere with the job.
QTimer::singleShot(100, this, &HttpCredentialsGui::askFromUserAsync);
} }
void HttpCredentialsGui::askFromUserAsync() void HttpCredentialsGui::askFromUserAsync()
{ {
// First, we will check what kind of auth we need. // First, we will check what kind of auth we need.
auto job = new DetermineAuthTypeJob(_account->sharedFromThis(), this); auto job = new DetermineAuthTypeJob(_account->sharedFromThis(), this);
job->setTimeout(30 * 1000);
QObject::connect(job, &DetermineAuthTypeJob::authType, this, [this](DetermineAuthTypeJob::AuthType type) { QObject::connect(job, &DetermineAuthTypeJob::authType, this, [this](DetermineAuthTypeJob::AuthType type) {
if (type == DetermineAuthTypeJob::OAuth) { if (type == DetermineAuthTypeJob::OAuth) {
_asyncAuth.reset(new OAuth(_account, this)); _asyncAuth.reset(new OAuth(_account, this));
@ -58,7 +60,8 @@ void HttpCredentialsGui::askFromUserAsync()
// We will re-enter the event loop, so better wait the next iteration // We will re-enter the event loop, so better wait the next iteration
QMetaObject::invokeMethod(this, "showDialog", Qt::QueuedConnection); QMetaObject::invokeMethod(this, "showDialog", Qt::QueuedConnection);
} else { } else {
// Network error? Unsupported auth type? // Shibboleth?
qCWarning(lcHttpCredentialsGui) << "Bad http auth type:" << type;
emit asked(); emit asked();
} }
}); });

View file

@ -60,12 +60,12 @@ public:
private slots: private slots:
void asyncAuthResult(OAuth::Result, const QString &user, const QString &accessToken, const QString &refreshToken); void asyncAuthResult(OAuth::Result, const QString &user, const QString &accessToken, const QString &refreshToken);
void showDialog(); void showDialog();
void askFromUserAsync();
signals: signals:
void authorisationLinkChanged(); void authorisationLinkChanged();
private: private:
Q_INVOKABLE void askFromUserAsync();
QScopedPointer<OAuth, QScopedPointerObjectDeleteLater<OAuth>> _asyncAuth; QScopedPointer<OAuth, QScopedPointerObjectDeleteLater<OAuth>> _asyncAuth;
}; };

View file

@ -43,7 +43,7 @@ public:
UserAgentWebPage(QObject *parent) UserAgentWebPage(QObject *parent)
: QWebPage(parent) : QWebPage(parent)
{ {
if (!qgetenv("OWNCLOUD_SHIBBOLETH_DEBUG").isEmpty()) { if (!qEnvironmentVariableIsEmpty("OWNCLOUD_SHIBBOLETH_DEBUG")) {
settings()->setAttribute(QWebSettings::DeveloperExtrasEnabled, true); settings()->setAttribute(QWebSettings::DeveloperExtrasEnabled, true);
} }
} }
@ -83,7 +83,7 @@ ShibbolethWebView::ShibbolethWebView(AccountPtr account, QWidget *parent)
setWindowTitle(tr("%1 - Authenticate").arg(Theme::instance()->appNameGUI())); setWindowTitle(tr("%1 - Authenticate").arg(Theme::instance()->appNameGUI()));
// Debug view to display the cipher suite // Debug view to display the cipher suite
if (!qgetenv("OWNCLOUD_SHIBBOLETH_DEBUG").isEmpty()) { if (!qEnvironmentVariableIsEmpty("OWNCLOUD_SHIBBOLETH_DEBUG")) {
// open an additional window to display some cipher debug info // open an additional window to display some cipher debug info
QWebPage *debugPage = new UserAgentWebPage(this); QWebPage *debugPage = new UserAgentWebPage(this);
debugPage->mainFrame()->load(QUrl("https://cc.dcsec.uni-hannover.de/")); debugPage->mainFrame()->load(QUrl("https://cc.dcsec.uni-hannover.de/"));

View file

@ -448,12 +448,26 @@ int Folder::slotWipeErrorBlacklist()
void Folder::slotWatchedPathChanged(const QString &path) void Folder::slotWatchedPathChanged(const QString &path)
{ {
if (!path.startsWith(this->path())) {
qCDebug(lcFolder) << "Changed path is not contained in folder, ignoring:" << path;
return;
}
auto relativePath = path.midRef(this->path().size());
// Add to list of locally modified paths
//
// We do this before checking for our own sync-related changes to make
// extra sure to not miss relevant changes.
auto relativePathBytes = relativePath.toUtf8();
_localDiscoveryPaths.insert(relativePathBytes);
qCDebug(lcFolder) << "local discovery: inserted" << relativePath << "due to file watcher";
// The folder watcher fires a lot of bogus notifications during // The folder watcher fires a lot of bogus notifications during
// a sync operation, both for actual user files and the database // a sync operation, both for actual user files and the database
// and log. Therefore we check notifications against operations // and log. Therefore we check notifications against operations
// the sync is doing to filter out our own changes. // the sync is doing to filter out our own changes.
#ifdef Q_OS_MAC #ifdef Q_OS_MAC
Q_UNUSED(path)
// On OSX the folder watcher does not report changes done by our // On OSX the folder watcher does not report changes done by our
// own process. Therefore nothing needs to be done here! // own process. Therefore nothing needs to be done here!
#else #else
@ -465,15 +479,12 @@ void Folder::slotWatchedPathChanged(const QString &path)
#endif #endif
// Check that the mtime actually changed. // Check that the mtime actually changed.
if (path.startsWith(this->path())) { SyncJournalFileRecord record;
auto relativePath = path.mid(this->path().size()); if (_journal.getFileRecord(relativePathBytes, &record)
SyncJournalFileRecord record; && record.isValid()
if (_journal.getFileRecord(relativePath, &record) && !FileSystem::fileChanged(path, record._fileSize, record._modtime)) {
&& record.isValid() qCInfo(lcFolder) << "Ignoring spurious notification for file" << relativePath;
&& !FileSystem::fileChanged(path, record._fileSize, record._modtime)) { return; // probably a spurious notification
qCInfo(lcFolder) << "Ignoring spurious notification for file" << relativePath;
return; // probably a spurious notification
}
} }
emit watchedFileChangedExternally(path); emit watchedFileChangedExternally(path);
@ -645,6 +656,36 @@ void Folder::startSync(const QStringList &pathList)
setDirtyNetworkLimits(); setDirtyNetworkLimits();
setSyncOptions(); setSyncOptions();
static qint64 fullLocalDiscoveryInterval = []() {
auto interval = ConfigFile().fullLocalDiscoveryInterval();
QByteArray env = qgetenv("OWNCLOUD_FULL_LOCAL_DISCOVERY_INTERVAL");
if (!env.isEmpty()) {
interval = env.toLongLong();
}
return interval;
}();
if (_folderWatcher && _folderWatcher->isReliable()
&& _timeSinceLastFullLocalDiscovery.isValid()
&& (fullLocalDiscoveryInterval < 0
|| _timeSinceLastFullLocalDiscovery.elapsed() < fullLocalDiscoveryInterval)) {
qCInfo(lcFolder) << "Allowing local discovery to read from the database";
_engine->setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem, _localDiscoveryPaths);
if (lcFolder().isDebugEnabled()) {
QByteArrayList paths;
for (auto &path : _localDiscoveryPaths)
paths.append(path);
qCDebug(lcFolder) << "local discovery paths: " << paths;
}
_previousLocalDiscoveryPaths = std::move(_localDiscoveryPaths);
} else {
qCInfo(lcFolder) << "Forbidding local discovery to read from the database";
_engine->setLocalDiscoveryOptions(LocalDiscoveryStyle::FilesystemOnly);
_previousLocalDiscoveryPaths.clear();
}
_localDiscoveryPaths.clear();
_engine->setIgnoreHiddenFiles(_definition.ignoreHiddenFiles); _engine->setIgnoreHiddenFiles(_definition.ignoreHiddenFiles);
QMetaObject::invokeMethod(_engine.data(), "startSync", Qt::QueuedConnection); QMetaObject::invokeMethod(_engine.data(), "startSync", Qt::QueuedConnection);
@ -783,6 +824,24 @@ void Folder::slotSyncFinished(bool success)
journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, QStringList()); journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, QStringList());
} }
// bug: This function uses many different criteria for "sync was successful" - investigate!
if ((_syncResult.status() == SyncResult::Success
|| _syncResult.status() == SyncResult::Problem)
&& success) {
if (_engine->lastLocalDiscoveryStyle() == LocalDiscoveryStyle::FilesystemOnly) {
_timeSinceLastFullLocalDiscovery.start();
}
qCDebug(lcFolder) << "Sync success, forgetting last sync's local discovery path list";
} else {
// On overall-failure we can't forget about last sync's local discovery
// paths yet, reuse them for the next sync again.
// C++17: Could use std::set::merge().
_localDiscoveryPaths.insert(
_previousLocalDiscoveryPaths.begin(), _previousLocalDiscoveryPaths.end());
qCDebug(lcFolder) << "Sync failed, keeping last sync's local discovery path list";
}
_previousLocalDiscoveryPaths.clear();
emit syncStateChange(); emit syncStateChange();
// The syncFinished result that is to be triggered here makes the folderman // The syncFinished result that is to be triggered here makes the folderman
@ -841,12 +900,38 @@ void Folder::slotTransmissionProgress(const ProgressInfo &pi)
// a item is completed: count the errors and forward to the ProgressDispatcher // a item is completed: count the errors and forward to the ProgressDispatcher
void Folder::slotItemCompleted(const SyncFileItemPtr &item) void Folder::slotItemCompleted(const SyncFileItemPtr &item)
{ {
if (item->_instruction == CSYNC_INSTRUCTION_NONE || item->_instruction == CSYNC_INSTRUCTION_UPDATE_METADATA) {
// We only care about the updates that deserve to be shown in the UI
return;
}
// add new directories or remove gone away dirs to the watcher // add new directories or remove gone away dirs to the watcher
if (item->isDirectory() && item->_instruction == CSYNC_INSTRUCTION_NEW) { if (item->isDirectory() && item->_instruction == CSYNC_INSTRUCTION_NEW) {
FolderMan::instance()->addMonitorPath(alias(), path() + item->_file); if (_folderWatcher)
_folderWatcher->addPath(path() + item->_file);
} }
if (item->isDirectory() && item->_instruction == CSYNC_INSTRUCTION_REMOVE) { if (item->isDirectory() && item->_instruction == CSYNC_INSTRUCTION_REMOVE) {
FolderMan::instance()->removeMonitorPath(alias(), path() + item->_file); if (_folderWatcher)
_folderWatcher->removePath(path() + item->_file);
}
// Success and failure of sync items adjust what the next sync is
// supposed to do.
//
// For successes, we want to wipe the file from the list to ensure we don't
// rediscover it even if this overall sync fails.
//
// For failures, we want to add the file to the list so the next sync
// will be able to retry it.
if (item->_status == SyncFileItem::Success
|| item->_status == SyncFileItem::FileIgnored
|| item->_status == SyncFileItem::Restoration
|| item->_status == SyncFileItem::Conflict) {
if (_previousLocalDiscoveryPaths.erase(item->_file.toUtf8()))
qCDebug(lcFolder) << "local discovery: wiped" << item->_file;
} else {
_localDiscoveryPaths.insert(item->_file.toUtf8());
qCDebug(lcFolder) << "local discovery: inserted" << item->_file << "due to sync failure";
} }
_syncResult.processCompletedItem(item); _syncResult.processCompletedItem(item);
@ -901,6 +986,11 @@ void Folder::slotScheduleThisFolder()
FolderMan::instance()->scheduleFolder(this); FolderMan::instance()->scheduleFolder(this);
} }
void Folder::slotNextSyncFullLocalDiscovery()
{
_timeSinceLastFullLocalDiscovery.invalidate();
}
void Folder::scheduleThisFolderSoon() void Folder::scheduleThisFolderSoon()
{ {
if (!_scheduleSelfTimer.isActive()) { if (!_scheduleSelfTimer.isActive()) {
@ -913,6 +1003,20 @@ void Folder::setSaveBackwardsCompatible(bool save)
_saveBackwardsCompatible = save; _saveBackwardsCompatible = save;
} }
void Folder::registerFolderWatcher()
{
if (_folderWatcher)
return;
if (!QDir(path()).exists())
return;
_folderWatcher.reset(new FolderWatcher(path(), this));
connect(_folderWatcher.data(), &FolderWatcher::pathChanged,
this, &Folder::slotWatchedPathChanged);
connect(_folderWatcher.data(), &FolderWatcher::lostChanges,
this, &Folder::slotNextSyncFullLocalDiscovery);
}
void Folder::slotAboutToRemoveAllFiles(SyncFileItem::Direction dir, bool *cancel) void Folder::slotAboutToRemoveAllFiles(SyncFileItem::Direction dir, bool *cancel)
{ {
ConfigFile cfgFile; ConfigFile cfgFile;
@ -976,6 +1080,12 @@ void FolderDefinition::save(QSettings &settings, const FolderDefinition &folder)
settings.setValue(QLatin1String("targetPath"), folder.targetPath); settings.setValue(QLatin1String("targetPath"), folder.targetPath);
settings.setValue(QLatin1String("paused"), folder.paused); settings.setValue(QLatin1String("paused"), folder.paused);
settings.setValue(QLatin1String("ignoreHiddenFiles"), folder.ignoreHiddenFiles); settings.setValue(QLatin1String("ignoreHiddenFiles"), folder.ignoreHiddenFiles);
// Happens only on Windows when the explorer integration is enabled.
if (!folder.navigationPaneClsid.isNull())
settings.setValue(QLatin1String("navigationPaneClsid"), folder.navigationPaneClsid);
else
settings.remove(QLatin1String("navigationPaneClsid"));
settings.endGroup(); settings.endGroup();
} }
@ -989,6 +1099,7 @@ bool FolderDefinition::load(QSettings &settings, const QString &alias,
folder->targetPath = settings.value(QLatin1String("targetPath")).toString(); folder->targetPath = settings.value(QLatin1String("targetPath")).toString();
folder->paused = settings.value(QLatin1String("paused")).toBool(); folder->paused = settings.value(QLatin1String("paused")).toBool();
folder->ignoreHiddenFiles = settings.value(QLatin1String("ignoreHiddenFiles"), QVariant(true)).toBool(); folder->ignoreHiddenFiles = settings.value(QLatin1String("ignoreHiddenFiles"), QVariant(true)).toBool();
folder->navigationPaneClsid = settings.value(QLatin1String("navigationPaneClsid")).toUuid();
settings.endGroup(); settings.endGroup();
// Old settings can contain paths with native separators. In the rest of the // Old settings can contain paths with native separators. In the rest of the

View file

@ -27,6 +27,8 @@
#include <QObject> #include <QObject>
#include <QStringList> #include <QStringList>
#include <QUuid>
#include <set>
class QThread; class QThread;
class QSettings; class QSettings;
@ -36,6 +38,7 @@ namespace OCC {
class SyncEngine; class SyncEngine;
class AccountState; class AccountState;
class SyncRunFileLog; class SyncRunFileLog;
class FolderWatcher;
/** /**
* @brief The FolderDefinition class * @brief The FolderDefinition class
@ -62,6 +65,8 @@ public:
bool paused; bool paused;
/// whether the folder syncs hidden files /// whether the folder syncs hidden files
bool ignoreHiddenFiles; bool ignoreHiddenFiles;
/// The CLSID where this folder appears in registry for the Explorer navigation pane entry.
QUuid navigationPaneClsid;
/// Saves the folder definition, creating a new settings group. /// Saves the folder definition, creating a new settings group.
static void save(QSettings &settings, const FolderDefinition &folder); static void save(QSettings &settings, const FolderDefinition &folder);
@ -133,6 +138,9 @@ public:
*/ */
QString remotePath() const; QString remotePath() const;
void setNavigationPaneClsid(const QUuid &clsid) { _definition.navigationPaneClsid = clsid; }
QUuid navigationPaneClsid() const { return _definition.navigationPaneClsid; }
/** /**
* remote folder path with server url * remote folder path with server url
*/ */
@ -227,6 +235,13 @@ public:
*/ */
void setSaveBackwardsCompatible(bool save); void setSaveBackwardsCompatible(bool save);
/**
* Sets up this folder's folderWatcher if possible.
*
* May be called several times.
*/
void registerFolderWatcher();
signals: signals:
void syncStateChange(); void syncStateChange();
void syncStarted(); void syncStarted();
@ -304,6 +319,9 @@ private slots:
*/ */
void slotScheduleThisFolder(); void slotScheduleThisFolder();
/** Ensures that the next sync performs a full local discovery. */
void slotNextSyncFullLocalDiscovery();
private: private:
bool setIgnoredFiles(); bool setIgnoredFiles();
@ -338,6 +356,7 @@ private:
QString _lastEtag; QString _lastEtag;
QElapsedTimer _timeSinceLastSyncDone; QElapsedTimer _timeSinceLastSyncDone;
QElapsedTimer _timeSinceLastSyncStart; QElapsedTimer _timeSinceLastSyncStart;
QElapsedTimer _timeSinceLastFullLocalDiscovery;
qint64 _lastSyncDuration; qint64 _lastSyncDuration;
/// The number of syncs that failed in a row. /// The number of syncs that failed in a row.
@ -365,6 +384,29 @@ private:
* path. * path.
*/ */
bool _saveBackwardsCompatible; bool _saveBackwardsCompatible;
/**
* Watches this folder's local directory for changes.
*
* Created by registerFolderWatcher(), triggers slotWatchedPathChanged()
*/
QScopedPointer<FolderWatcher> _folderWatcher;
/**
* The paths that should be checked by the next local discovery.
*
* Mostly a collection of files the filewatchers have reported as touched.
* Also includes files that have had errors in the last sync run.
*/
std::set<QByteArray> _localDiscoveryPaths;
/**
* The paths that the current sync run used for local discovery.
*
* For failing syncs, this list will be merged into _localDiscoveryPaths
* again when the sync is done to make sure everything is retried.
*/
std::set<QByteArray> _previousLocalDiscoveryPaths;
}; };
} }

View file

@ -49,6 +49,7 @@ FolderMan::FolderMan(QObject *parent)
, _currentSyncFolder(0) , _currentSyncFolder(0)
, _syncEnabled(true) , _syncEnabled(true)
, _lockWatcher(new LockWatcher) , _lockWatcher(new LockWatcher)
, _navigationPaneHelper(this)
, _appRestartRequired(false) , _appRestartRequired(false)
{ {
ASSERT(!_instance); ASSERT(!_instance);
@ -104,9 +105,6 @@ void FolderMan::unloadFolder(Folder *f)
_socketApi->slotUnregisterPath(f->alias()); _socketApi->slotUnregisterPath(f->alias());
if (_folderWatchers.contains(f->alias())) {
_folderWatchers.remove(f->alias());
}
_folderMap.remove(f->alias()); _folderMap.remove(f->alias());
disconnect(f, &Folder::syncStarted, disconnect(f, &Folder::syncStarted,
@ -147,54 +145,18 @@ int FolderMan::unloadAndDeleteAllFolders()
return cnt; return cnt;
} }
// add a monitor to the local file system. If there is a change in the void FolderMan::registerFolderWithSocketApi(Folder *folder)
// file system, the method slotFolderMonitorFired is triggered through
// the SignalMapper
void FolderMan::registerFolderMonitor(Folder *folder)
{ {
if (!folder) if (!folder)
return; return;
if (!QDir(folder->path()).exists()) if (!QDir(folder->path()).exists())
return; return;
if (!_folderWatchers.contains(folder->alias())) {
FolderWatcher *fw = new FolderWatcher(folder->path(), folder);
// Connect the pathChanged signal, which comes with the changed path,
// to the signal mapper which maps to the folder alias. The changed path
// is lost this way, but we do not need it for the current implementation.
connect(fw, &FolderWatcher::pathChanged, folder, &Folder::slotWatchedPathChanged);
_folderWatchers.insert(folder->alias(), fw);
}
// register the folder with the socket API // register the folder with the socket API
if (folder->canSync()) if (folder->canSync())
_socketApi->slotRegisterPath(folder->alias()); _socketApi->slotRegisterPath(folder->alias());
} }
void FolderMan::addMonitorPath(const QString &alias, const QString &path)
{
if (!alias.isEmpty() && _folderWatchers.contains(alias)) {
FolderWatcher *fw = _folderWatchers[alias];
if (fw) {
fw->addPath(path);
}
}
}
void FolderMan::removeMonitorPath(const QString &alias, const QString &path)
{
if (!alias.isEmpty() && _folderWatchers.contains(alias)) {
FolderWatcher *fw = _folderWatchers[alias];
if (fw) {
fw->removePath(path);
}
}
}
int FolderMan::setupFolders() int FolderMan::setupFolders()
{ {
unloadAndDeleteAllFolders(); unloadAndDeleteAllFolders();
@ -750,7 +712,8 @@ void FolderMan::slotStartScheduledFolderSync()
if (folder) { if (folder) {
// Safe to call several times, and necessary to try again if // Safe to call several times, and necessary to try again if
// the folder path didn't exist previously. // the folder path didn't exist previously.
registerFolderMonitor(folder); folder->registerFolderWatcher();
registerFolderWithSocketApi(folder);
_currentSyncFolder = folder; _currentSyncFolder = folder;
folder->startSync(QStringList()); folder->startSync(QStringList());
@ -932,6 +895,9 @@ Folder *FolderMan::addFolder(AccountState *accountState, const FolderDefinition
emit folderSyncStateChange(folder); emit folderSyncStateChange(folder);
emit folderListChanged(_folderMap); emit folderListChanged(_folderMap);
} }
_navigationPaneHelper.scheduleUpdateCloudStorageRegistry();
return folder; return folder;
} }
@ -964,7 +930,8 @@ Folder *FolderMan::addFolderInternal(FolderDefinition folderDefinition,
connect(folder, &Folder::watchedFileChangedExternally, connect(folder, &Folder::watchedFileChangedExternally,
&folder->syncEngine().syncFileStatusTracker(), &SyncFileStatusTracker::slotPathTouched); &folder->syncEngine().syncFileStatusTracker(), &SyncFileStatusTracker::slotPathTouched);
registerFolderMonitor(folder); folder->registerFolderWatcher();
registerFolderWithSocketApi(folder);
return folder; return folder;
} }
@ -1040,6 +1007,8 @@ void FolderMan::removeFolder(Folder *f)
delete f; delete f;
} }
_navigationPaneHelper.scheduleUpdateCloudStorageRegistry();
emit folderListChanged(_folderMap); emit folderListChanged(_folderMap);
} }

View file

@ -22,6 +22,7 @@
#include "folder.h" #include "folder.h"
#include "folderwatcher.h" #include "folderwatcher.h"
#include "navigationpanehelper.h"
#include "syncfileitem.h" #include "syncfileitem.h"
class TestFolderMan; class TestFolderMan;
@ -109,15 +110,13 @@ public:
static SyncResult accountStatus(const QList<Folder *> &folders); static SyncResult accountStatus(const QList<Folder *> &folders);
void removeMonitorPath(const QString &alias, const QString &path);
void addMonitorPath(const QString &alias, const QString &path);
// Escaping of the alias which is used in QSettings AND the file // Escaping of the alias which is used in QSettings AND the file
// system, thus need to be escaped. // system, thus need to be escaped.
static QString escapeAlias(const QString &); static QString escapeAlias(const QString &);
static QString unescapeAlias(const QString &); static QString unescapeAlias(const QString &);
SocketApi *socketApi(); SocketApi *socketApi();
NavigationPaneHelper &navigationPaneHelper() { return _navigationPaneHelper; }
/** /**
* Check if @a path is a valid path for a new folder considering the already sync'ed items. * Check if @a path is a valid path for a new folder considering the already sync'ed items.
@ -284,7 +283,9 @@ private:
// finds all folder configuration files // finds all folder configuration files
// and create the folders // and create the folders
QString getBackupName(QString fullPathName) const; QString getBackupName(QString fullPathName) const;
void registerFolderMonitor(Folder *folder);
// makes the folder known to the socket api
void registerFolderWithSocketApi(Folder *folder);
// restarts the application (Linux only) // restarts the application (Linux only)
void restartApplication(); void restartApplication();
@ -298,9 +299,6 @@ private:
QPointer<Folder> _lastSyncFolder; QPointer<Folder> _lastSyncFolder;
bool _syncEnabled; bool _syncEnabled;
/// Watching for file changes in folders
QMap<QString, FolderWatcher *> _folderWatchers;
/// Starts regular etag query jobs /// Starts regular etag query jobs
QTimer _etagPollTimer; QTimer _etagPollTimer;
/// The currently running etag query /// The currently running etag query
@ -319,6 +317,7 @@ private:
QTimer _startScheduledSyncTimer; QTimer _startScheduledSyncTimer;
QScopedPointer<SocketApi> _socketApi; QScopedPointer<SocketApi> _socketApi;
NavigationPaneHelper _navigationPaneHelper;
bool _appRestartRequired; bool _appRestartRequired;

View file

@ -68,6 +68,11 @@ bool FolderWatcher::pathIsIgnored(const QString &path)
return false; return false;
} }
bool FolderWatcher::isReliable() const
{
return _isReliable;
}
void FolderWatcher::changeDetected(const QString &path) void FolderWatcher::changeDetected(const QString &path)
{ {
QStringList paths(path); QStringList paths(path);

View file

@ -72,13 +72,29 @@ public:
/* Check if the path is ignored. */ /* Check if the path is ignored. */
bool pathIsIgnored(const QString &path); bool pathIsIgnored(const QString &path);
/**
* Returns false if the folder watcher can't be trusted to capture all
* notifications.
*
* For example, this can happen on linux if the inotify user limit from
* /proc/sys/fs/inotify/max_user_watches is exceeded.
*/
bool isReliable() const;
signals: signals:
/** Emitted when one of the watched directories or one /** Emitted when one of the watched directories or one
* of the contained files is changed. */ * of the contained files is changed. */
void pathChanged(const QString &path); void pathChanged(const QString &path);
/** Emitted if an error occurs */ /**
void error(const QString &error); * Emitted if some notifications were lost.
*
* Would happen, for example, if the number of pending notifications
* exceeded the allocated buffer size on Windows. Note that the folder
* watcher could still be able to capture all future notifications -
* i.e. isReliable() is orthogonal to losing changes occasionally.
*/
void lostChanges();
protected slots: protected slots:
// called from the implementations to indicate a change in path // called from the implementations to indicate a change in path
@ -93,6 +109,7 @@ private:
QTime _timer; QTime _timer;
QSet<QString> _lastPaths; QSet<QString> _lastPaths;
Folder *_folder; Folder *_folder;
bool _isReliable = true;
friend class FolderWatcherPrivate; friend class FolderWatcherPrivate;
}; };

View file

@ -78,6 +78,12 @@ void FolderWatcherPrivate::inotifyRegisterPath(const QString &path)
IN_CLOSE_WRITE | IN_ATTRIB | IN_MOVE | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF | IN_UNMOUNT | IN_ONLYDIR); IN_CLOSE_WRITE | IN_ATTRIB | IN_MOVE | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF | IN_UNMOUNT | IN_ONLYDIR);
if (wd > -1) { if (wd > -1) {
_watches.insert(wd, path); _watches.insert(wd, path);
} else {
// If we're running out of memory or inotify watches, become
// unreliable.
if (errno == ENOMEM || errno == ENOSPC) {
_parent->_isReliable = false;
}
} }
} }
} }

View file

@ -101,6 +101,7 @@ void WatcherThread::watchChanges(size_t fileNotifyBufferSize,
DWORD errorCode = GetLastError(); DWORD errorCode = GetLastError();
if (errorCode == ERROR_NOTIFY_ENUM_DIR) { if (errorCode == ERROR_NOTIFY_ENUM_DIR) {
qCDebug(lcFolderWatcher) << "The buffer for changes overflowed! Triggering a generic change and resizing"; qCDebug(lcFolderWatcher) << "The buffer for changes overflowed! Triggering a generic change and resizing";
emit lostChanges();
emit changed(_path); emit changed(_path);
*increaseBufferSize = true; *increaseBufferSize = true;
} else { } else {
@ -199,6 +200,8 @@ FolderWatcherPrivate::FolderWatcherPrivate(FolderWatcher *p, const QString &path
_thread = new WatcherThread(path); _thread = new WatcherThread(path);
connect(_thread, SIGNAL(changed(const QString &)), connect(_thread, SIGNAL(changed(const QString &)),
_parent, SLOT(changeDetected(const QString &))); _parent, SLOT(changeDetected(const QString &)));
connect(_thread, SIGNAL(lostChanges()),
_parent, SIGNAL(lostChanges()));
_thread->start(); _thread->start();
} }

View file

@ -53,6 +53,7 @@ protected:
signals: signals:
void changed(const QString &path); void changed(const QString &path);
void lostChanges();
private: private:
QString _path; QString _path;

View file

@ -44,6 +44,7 @@ GeneralSettings::GeneralSettings(QWidget *parent)
connect(_ui->desktopNotificationsCheckBox, &QAbstractButton::toggled, connect(_ui->desktopNotificationsCheckBox, &QAbstractButton::toggled,
this, &GeneralSettings::slotToggleOptionalDesktopNotifications); this, &GeneralSettings::slotToggleOptionalDesktopNotifications);
connect(_ui->showInExplorerNavigationPaneCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::slotShowInExplorerNavigationPane);
_ui->autostartCheckBox->setChecked(Utility::hasLaunchOnStartup(Theme::instance()->appName())); _ui->autostartCheckBox->setChecked(Utility::hasLaunchOnStartup(Theme::instance()->appName()));
connect(_ui->autostartCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::slotToggleLaunchOnStartup); connect(_ui->autostartCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::slotToggleLaunchOnStartup);
@ -73,6 +74,13 @@ GeneralSettings::GeneralSettings(QWidget *parent)
_ui->crashreporterCheckBox->setVisible(false); _ui->crashreporterCheckBox->setVisible(false);
#endif #endif
// Hide on non-Windows, or WindowsVersion < 10.
// The condition should match the default value of ConfigFile::showInExplorerNavigationPane.
#ifdef Q_OS_WIN
if (QSysInfo::windowsVersion() < QSysInfo::WV_WINDOWS10)
#endif
_ui->showInExplorerNavigationPaneCheckBox->setVisible(false);
/* Set the left contents margin of the layout to zero to make the checkboxes /* Set the left contents margin of the layout to zero to make the checkboxes
* align properly vertically , fixes bug #3758 * align properly vertically , fixes bug #3758
*/ */
@ -107,6 +115,7 @@ void GeneralSettings::loadMiscSettings()
ConfigFile cfgFile; ConfigFile cfgFile;
_ui->monoIconsCheckBox->setChecked(cfgFile.monoIcons()); _ui->monoIconsCheckBox->setChecked(cfgFile.monoIcons());
_ui->desktopNotificationsCheckBox->setChecked(cfgFile.optionalDesktopNotifications()); _ui->desktopNotificationsCheckBox->setChecked(cfgFile.optionalDesktopNotifications());
_ui->showInExplorerNavigationPaneCheckBox->setChecked(cfgFile.showInExplorerNavigationPane());
_ui->crashreporterCheckBox->setChecked(cfgFile.crashReporter()); _ui->crashreporterCheckBox->setChecked(cfgFile.crashReporter());
auto newFolderLimit = cfgFile.newBigFolderSizeLimit(); auto newFolderLimit = cfgFile.newBigFolderSizeLimit();
_ui->newFolderLimitCheckBox->setChecked(newFolderLimit.first); _ui->newFolderLimitCheckBox->setChecked(newFolderLimit.first);
@ -162,6 +171,14 @@ void GeneralSettings::slotToggleOptionalDesktopNotifications(bool enable)
cfgFile.setOptionalDesktopNotifications(enable); cfgFile.setOptionalDesktopNotifications(enable);
} }
void GeneralSettings::slotShowInExplorerNavigationPane(bool checked)
{
ConfigFile cfgFile;
cfgFile.setShowInExplorerNavigationPane(checked);
// Now update the registry with the change.
FolderMan::instance()->navigationPaneHelper().setShowInExplorerNavigationPane(checked);
}
void GeneralSettings::slotIgnoreFilesEditor() void GeneralSettings::slotIgnoreFilesEditor()
{ {
if (_ignoreEditor.isNull()) { if (_ignoreEditor.isNull()) {

View file

@ -43,6 +43,7 @@ private slots:
void saveMiscSettings(); void saveMiscSettings();
void slotToggleLaunchOnStartup(bool); void slotToggleLaunchOnStartup(bool);
void slotToggleOptionalDesktopNotifications(bool); void slotToggleOptionalDesktopNotifications(bool);
void slotShowInExplorerNavigationPane(bool);
void slotUpdateInfo(); void slotUpdateInfo();
void slotIgnoreFilesEditor(); void slotIgnoreFilesEditor();
void loadMiscSettings(); void loadMiscSettings();

View file

@ -218,6 +218,17 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QCheckBox" name="showInExplorerNavigationPaneCheckBox">
<property name="text">
<string>Show sync folders in &amp;Explorer's Navigation Pane</string>
</property>
</widget>
</item>
</layout>
</item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_9"> <layout class="QHBoxLayout" name="horizontalLayout_9">
<item> <item>

View file

@ -107,6 +107,16 @@ void IssuesWidget::showEvent(QShowEvent *ev)
{ {
ConfigFile cfg; ConfigFile cfg;
cfg.restoreGeometryHeader(_ui->_treeWidget->header()); cfg.restoreGeometryHeader(_ui->_treeWidget->header());
// Sorting by section was newly enabled. But if we restore the header
// from a state where sorting was disabled, both of these flags will be
// false and sorting will be impossible!
_ui->_treeWidget->header()->setSectionsClickable(true);
_ui->_treeWidget->header()->setSortIndicatorShown(true);
// Switch back to "first important, then by time" ordering
_ui->_treeWidget->sortByColumn(0, Qt::DescendingOrder);
QWidget::showEvent(ev); QWidget::showEvent(ev);
} }
@ -119,6 +129,8 @@ void IssuesWidget::hideEvent(QHideEvent *ev)
void IssuesWidget::cleanItems(const QString &folder) void IssuesWidget::cleanItems(const QString &folder)
{ {
_ui->_treeWidget->setSortingEnabled(false);
// The issue list is a state, clear it and let the next sync fill it // The issue list is a state, clear it and let the next sync fill it
// with ignored files and propagation errors. // with ignored files and propagation errors.
int itemCnt = _ui->_treeWidget->topLevelItemCount(); int itemCnt = _ui->_treeWidget->topLevelItemCount();
@ -129,6 +141,9 @@ void IssuesWidget::cleanItems(const QString &folder)
delete item; delete item;
} }
} }
_ui->_treeWidget->setSortingEnabled(true);
// update the tabtext // update the tabtext
emit(issueCountUpdated(_ui->_treeWidget->topLevelItemCount())); emit(issueCountUpdated(_ui->_treeWidget->topLevelItemCount()));
} }
@ -240,7 +255,7 @@ bool IssuesWidget::shouldBeVisible(QTreeWidgetItem *item, AccountState *filterAc
const QString &filterFolderAlias) const const QString &filterFolderAlias) const
{ {
bool visible = true; bool visible = true;
auto status = item->data(0, Qt::UserRole); auto status = item->data(3, Qt::UserRole);
visible &= (_ui->showIgnores->isChecked() || status != SyncFileItem::FileIgnored); visible &= (_ui->showIgnores->isChecked() || status != SyncFileItem::FileIgnored);
visible &= (_ui->showWarnings->isChecked() visible &= (_ui->showWarnings->isChecked()
|| (status != SyncFileItem::SoftError || (status != SyncFileItem::SoftError
@ -368,13 +383,14 @@ void IssuesWidget::addError(const QString &folderAlias, const QString &message,
QIcon icon = Theme::instance()->syncStateIcon(SyncResult::Error); QIcon icon = Theme::instance()->syncStateIcon(SyncResult::Error);
QTreeWidgetItem *twitem = new QTreeWidgetItem(columns); QTreeWidgetItem *twitem = new SortedTreeWidgetItem(columns);
twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight())); twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight()));
twitem->setData(0, Qt::UserRole, timestamp);
twitem->setIcon(0, icon); twitem->setIcon(0, icon);
twitem->setToolTip(0, longTimeStr); twitem->setToolTip(0, longTimeStr);
twitem->setToolTip(3, message);
twitem->setData(0, Qt::UserRole, SyncFileItem::NormalError);
twitem->setData(2, Qt::UserRole, folderAlias); twitem->setData(2, Qt::UserRole, folderAlias);
twitem->setToolTip(3, message);
twitem->setData(3, Qt::UserRole, SyncFileItem::NormalError);
addItem(twitem); addItem(twitem);
addErrorWidget(twitem, message, category); addErrorWidget(twitem, message, category);

View file

@ -99,6 +99,9 @@
<property name="rootIsDecorated"> <property name="rootIsDecorated">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="columnCount"> <property name="columnCount">
<number>4</number> <number>4</number>
</property> </property>

View file

@ -88,7 +88,7 @@ int main(int argc, char **argv)
// check a environment variable for core dumps // check a environment variable for core dumps
#ifdef Q_OS_UNIX #ifdef Q_OS_UNIX
if (!qgetenv("OWNCLOUD_CORE_DUMP").isEmpty()) { if (!qEnvironmentVariableIsEmpty("OWNCLOUD_CORE_DUMP")) {
struct rlimit core_limit; struct rlimit core_limit;
core_limit.rlim_cur = RLIM_INFINITY; core_limit.rlim_cur = RLIM_INFINITY;
core_limit.rlim_max = RLIM_INFINITY; core_limit.rlim_max = RLIM_INFINITY;

View file

@ -0,0 +1,151 @@
/*
* Copyright (C) by Jocelyn Turcotte <jturcotte@woboq.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 "navigationpanehelper.h"
#include "accountmanager.h"
#include "configfile.h"
#include "folderman.h"
#include <QDir>
#include <QCoreApplication>
namespace OCC {
Q_LOGGING_CATEGORY(lcNavPane, "gui.folder.navigationpane", QtInfoMsg)
NavigationPaneHelper::NavigationPaneHelper(FolderMan *folderMan)
: _folderMan(folderMan)
{
ConfigFile cfg;
_showInExplorerNavigationPane = cfg.showInExplorerNavigationPane();
_updateCloudStorageRegistryTimer.setSingleShot(true);
connect(&_updateCloudStorageRegistryTimer, &QTimer::timeout, this, &NavigationPaneHelper::updateCloudStorageRegistry);
}
void NavigationPaneHelper::setShowInExplorerNavigationPane(bool show)
{
if (_showInExplorerNavigationPane == show)
return;
_showInExplorerNavigationPane = show;
// Re-generate a new CLSID when enabling, possibly throwing away the old one.
// updateCloudStorageRegistry will take care of removing any unknown CLSID our application owns from the registry.
foreach (Folder *folder, _folderMan->map())
folder->setNavigationPaneClsid(show ? QUuid::createUuid() : QUuid());
scheduleUpdateCloudStorageRegistry();
}
void NavigationPaneHelper::scheduleUpdateCloudStorageRegistry()
{
// Schedule the update to happen a bit later to avoid doing the update multiple times in a row.
if (!_updateCloudStorageRegistryTimer.isActive())
_updateCloudStorageRegistryTimer.start(500);
}
void NavigationPaneHelper::updateCloudStorageRegistry()
{
// Start by looking at every registered namespace extension for the sidebar, and look for an "ApplicationName" value
// that matches ours when we saved.
QVector<QUuid> entriesToRemove;
#ifdef Q_OS_WIN
Utility::registryWalkSubKeys(
HKEY_CURRENT_USER,
QStringLiteral("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\NameSpace"),
[&entriesToRemove](HKEY key, const QString &subKey) {
QVariant appName = Utility::registryGetKeyValue(key, subKey, QStringLiteral("ApplicationName"));
if (appName.toString() == QLatin1String(APPLICATION_NAME)) {
QUuid clsid{ subKey };
Q_ASSERT(!clsid.isNull());
entriesToRemove.append(clsid);
}
});
#endif
// Then re-save every folder that has a valid navigationPaneClsid to the registry.
// We currently don't distinguish between new and existing CLSIDs, if it's there we just
// save over it. We at least need to update the tile in case we are suddently using multiple accounts.
foreach (Folder *folder, _folderMan->map()) {
if (!folder->navigationPaneClsid().isNull()) {
// If it already exists, unmark it for removal, this is a valid sync root.
entriesToRemove.removeOne(folder->navigationPaneClsid());
QString clsidStr = folder->navigationPaneClsid().toString();
QString clsidPath = QString() % "Software\\Classes\\CLSID\\" % clsidStr;
QString namespacePath = QString() % "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\NameSpace\\" % clsidStr;
QString title = folder->shortGuiRemotePathOrAppName();
// Write the account name in the sidebar only when using more than one account.
if (AccountManager::instance()->accounts().size() > 1)
title = title % " - " % folder->accountState()->account()->displayName();
QString iconPath = QDir::toNativeSeparators(qApp->applicationFilePath());
QString targetFolderPath = QDir::toNativeSeparators(folder->cleanPath());
qCInfo(lcNavPane) << "Explorer Cloud storage provider: saving path" << targetFolderPath << "to CLSID" << clsidStr;
#ifdef Q_OS_WIN
// Steps taken from: https://msdn.microsoft.com/en-us/library/windows/desktop/dn889934%28v=vs.85%29.aspx
// Step 1: Add your CLSID and name your extension
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath, QString(), REG_SZ, title);
// Step 2: Set the image for your icon
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\DefaultIcon"), QString(), REG_SZ, iconPath);
// Step 3: Add your extension to the Navigation Pane and make it visible
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath, QStringLiteral("System.IsPinnedToNameSpaceTree"), REG_DWORD, 0x1);
// Step 4: Set the location for your extension in the Navigation Pane
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath, QStringLiteral("SortOrderIndex"), REG_DWORD, 0x41);
// Step 5: Provide the dll that hosts your extension.
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\InProcServer32"), QString(), REG_EXPAND_SZ, QStringLiteral("%systemroot%\\system32\\shell32.dll"));
// Step 6: Define the instance object
// Indicate that your namespace extension should function like other file folder structures in File Explorer.
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\Instance"), QStringLiteral("CLSID"), REG_SZ, QStringLiteral("{0E5AAE11-A475-4c5b-AB00-C66DE400274E}"));
// Step 7: Provide the file system attributes of the target folder
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\Instance\\InitPropertyBag"), QStringLiteral("Attributes"), REG_DWORD, 0x11);
// Step 8: Set the path for the sync root
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\Instance\\InitPropertyBag"), QStringLiteral("TargetFolderPath"), REG_SZ, targetFolderPath);
// Step 9: Set appropriate shell flags
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\ShellFolder"), QStringLiteral("FolderValueFlags"), REG_DWORD, 0x28);
// Step 10: Set the appropriate flags to control your shell behavior
Utility::registrySetKeyValue(HKEY_CURRENT_USER, clsidPath + QStringLiteral("\\ShellFolder"), QStringLiteral("Attributes"), REG_DWORD, 0xF080004D);
// Step 11: Register your extension in the namespace root
Utility::registrySetKeyValue(HKEY_CURRENT_USER, namespacePath, QString(), REG_SZ, title);
// Step 12: Hide your extension from the Desktop
Utility::registrySetKeyValue(HKEY_CURRENT_USER, QStringLiteral("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\HideDesktopIcons\\NewStartPanel"), clsidStr, REG_DWORD, 0x1);
// For us, to later be able to iterate and find our own namespace entries and associated CLSID.
// Use the macro instead of the theme to make sure it matches with the uninstaller.
Utility::registrySetKeyValue(HKEY_CURRENT_USER, namespacePath, QStringLiteral("ApplicationName"), REG_SZ, QLatin1String(APPLICATION_NAME));
#else
// This code path should only occur on Windows (the config will be false, and the checkbox invisible on other platforms).
// Add runtime checks rather than #ifdefing out the whole code to help catch breakages when developing on other platforms.
Q_ASSERT(false);
#endif
}
}
// Then remove anything that isn't in our folder list anymore.
foreach (auto &clsid, entriesToRemove) {
QString clsidStr = clsid.toString();
QString clsidPath = QString() % "Software\\Classes\\CLSID\\" % clsidStr;
QString namespacePath = QString() % "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\NameSpace\\" % clsidStr;
qCInfo(lcNavPane) << "Explorer Cloud storage provider: now unused, removing own CLSID" << clsidStr;
#ifdef Q_OS_WIN
Utility::registryDeleteKeyTree(HKEY_CURRENT_USER, clsidPath);
Utility::registryDeleteKeyTree(HKEY_CURRENT_USER, namespacePath);
Utility::registryDeleteKeyValue(HKEY_CURRENT_USER, QStringLiteral("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\HideDesktopIcons\\NewStartPanel"), clsidStr);
#endif
}
}
} // namespace OCC

View file

@ -0,0 +1,46 @@
/*
* Copyright (C) by Jocelyn Turcotte <jturcotte@woboq.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#ifndef NAVIGATIONPANEHELPER_H
#define NAVIGATIONPANEHELPER_H
#include <QObject>
#include <QTimer>
namespace OCC {
class FolderMan;
class NavigationPaneHelper : public QObject
{
Q_OBJECT
public:
NavigationPaneHelper(FolderMan *folderMan);
bool showInExplorerNavigationPane() const { return _showInExplorerNavigationPane; }
void setShowInExplorerNavigationPane(bool show);
void scheduleUpdateCloudStorageRegistry();
private:
void updateCloudStorageRegistry();
FolderMan *_folderMan;
bool _showInExplorerNavigationPane;
QTimer _updateCloudStorageRegistryTimer;
};
} // namespace OCC
#endif // NAVIGATIONPANEHELPER_H

View file

@ -349,7 +349,7 @@ void ownCloudGui::addAccountContextMenu(AccountStatePtr accountState, QMenu *men
menu->addAction(tr("Managed Folders:"))->setDisabled(true); menu->addAction(tr("Managed Folders:"))->setDisabled(true);
} }
QAction *action = new QAction(tr("Open folder '%1'").arg(folder->shortGuiLocalPath()), menu); QAction *action = menu->addAction(tr("Open folder '%1'").arg(folder->shortGuiLocalPath()));
auto alias = folder->alias(); auto alias = folder->alias();
connect(action, &QAction::triggered, this, [this, alias] { this->slotFolderOpenAction(alias); }); connect(action, &QAction::triggered, this, [this, alias] { this->slotFolderOpenAction(alias); });
} }

View file

@ -18,6 +18,7 @@
#include <QProcess> #include <QProcess>
#include <QMessageBox> #include <QMessageBox>
#include <QDesktopServices> #include <QDesktopServices>
#include <QApplication>
#include "wizard/owncloudwizardcommon.h" #include "wizard/owncloudwizardcommon.h"
#include "wizard/owncloudwizard.h" #include "wizard/owncloudwizard.h"
@ -45,7 +46,7 @@ OwncloudSetupWizard::OwncloudSetupWizard(QObject *parent)
, _remoteFolder() , _remoteFolder()
{ {
connect(_ocWizard, &OwncloudWizard::determineAuthType, connect(_ocWizard, &OwncloudWizard::determineAuthType,
this, &OwncloudSetupWizard::slotDetermineAuthType); this, &OwncloudSetupWizard::slotCheckServer);
connect(_ocWizard, &OwncloudWizard::connectToOCUrl, connect(_ocWizard, &OwncloudWizard::connectToOCUrl,
this, &OwncloudSetupWizard::slotConnectToOCUrl); this, &OwncloudSetupWizard::slotConnectToOCUrl);
connect(_ocWizard, &OwncloudWizard::createLocalAndRemoteFolders, connect(_ocWizard, &OwncloudWizard::createLocalAndRemoteFolders,
@ -69,6 +70,7 @@ static QPointer<OwncloudSetupWizard> wiz = 0;
void OwncloudSetupWizard::runWizard(QObject *obj, const char *amember, QWidget *parent) void OwncloudSetupWizard::runWizard(QObject *obj, const char *amember, QWidget *parent)
{ {
if (!wiz.isNull()) { if (!wiz.isNull()) {
bringWizardToFrontIfVisible();
return; return;
} }
@ -84,6 +86,17 @@ bool OwncloudSetupWizard::bringWizardToFrontIfVisible()
return false; return false;
} }
if (wiz->_ocWizard->currentId() == WizardCommon::Page_ShibbolethCreds) {
// Try to find if there is a browser open and raise that instead (Issue #6105)
const auto allWindow = qApp->topLevelWidgets();
auto it = std::find_if(allWindow.cbegin(), allWindow.cend(), [](QWidget *w)
{ return QLatin1String(w->metaObject()->className()) == QLatin1String("OCC::ShibbolethWebView"); });
if (it != allWindow.cend()) {
ownCloudGui::raiseDialog(*it);
return true;
}
}
ownCloudGui::raiseDialog(wiz->_ocWizard); ownCloudGui::raiseDialog(wiz->_ocWizard);
return true; return true;
} }
@ -127,7 +140,7 @@ void OwncloudSetupWizard::startWizard()
} }
// also checks if an installation is valid and determines auth type in a second step // also checks if an installation is valid and determines auth type in a second step
void OwncloudSetupWizard::slotDetermineAuthType(const QString &urlString) void OwncloudSetupWizard::slotCheckServer(const QString &urlString)
{ {
QString fixedUrl = urlString; QString fixedUrl = urlString;
QUrl url = QUrl::fromUserInput(fixedUrl); QUrl url = QUrl::fromUserInput(fixedUrl);
@ -150,7 +163,7 @@ void OwncloudSetupWizard::slotDetermineAuthType(const QString &urlString)
// We want to reset the QNAM proxy so that the global proxy settings are used (via ClientProxy settings) // We want to reset the QNAM proxy so that the global proxy settings are used (via ClientProxy settings)
account->networkAccessManager()->setProxy(QNetworkProxy(QNetworkProxy::DefaultProxy)); account->networkAccessManager()->setProxy(QNetworkProxy(QNetworkProxy::DefaultProxy));
// use a queued invocation so we're as asynchronous as with the other code path // use a queued invocation so we're as asynchronous as with the other code path
QMetaObject::invokeMethod(this, "slotContinueDetermineAuth", Qt::QueuedConnection); QMetaObject::invokeMethod(this, "slotFindServer", Qt::QueuedConnection);
} }
} }
@ -164,20 +177,40 @@ void OwncloudSetupWizard::slotSystemProxyLookupDone(const QNetworkProxy &proxy)
AccountPtr account = _ocWizard->account(); AccountPtr account = _ocWizard->account();
account->networkAccessManager()->setProxy(proxy); account->networkAccessManager()->setProxy(proxy);
slotContinueDetermineAuth(); slotFindServer();
} }
void OwncloudSetupWizard::slotContinueDetermineAuth() void OwncloudSetupWizard::slotFindServer()
{ {
AccountPtr account = _ocWizard->account(); AccountPtr account = _ocWizard->account();
// Set fake credentials before we check what credential it actually is. // Set fake credentials before we check what credential it actually is.
account->setCredentials(CredentialsFactory::create("dummy")); account->setCredentials(CredentialsFactory::create("dummy"));
// Before we check the auth type, resolve any permanent redirect // Determining the actual server URL can be a multi-stage process
// chain there might be. We cannot do this only on url/status.php // 1. Check url/status.php with CheckServerJob
// in CheckServerJob, because things like url shorteners don't // If that works we're done. In that case we don't check the
// redirect subpaths. // url directly for redirects, see #5954.
// 2. Check the url for permanent redirects (like url shorteners)
// 3. Check redirected-url/status.php with CheckServerJob
// Step 1: Check url/status.php
CheckServerJob *job = new CheckServerJob(account, this);
job->setIgnoreCredentialFailure(true);
connect(job, &CheckServerJob::instanceFound, this, &OwncloudSetupWizard::slotFoundServer);
connect(job, &CheckServerJob::instanceNotFound, this, &OwncloudSetupWizard::slotFindServerBehindRedirect);
connect(job, &CheckServerJob::timeout, this, &OwncloudSetupWizard::slotNoServerFoundTimeout);
job->setTimeout((account->url().scheme() == "https") ? 30 * 1000 : 10 * 1000);
job->start();
// Step 2 and 3 are in slotFindServerBehindRedirect()
}
void OwncloudSetupWizard::slotFindServerBehindRedirect()
{
AccountPtr account = _ocWizard->account();
// Step 2: Resolve any permanent redirect chains on the base url
auto redirectCheckJob = account->sendRequest("GET", account->url()); auto redirectCheckJob = account->sendRequest("GET", account->url());
// Use a significantly reduced timeout for this redirect check: // Use a significantly reduced timeout for this redirect check:
@ -197,20 +230,20 @@ void OwncloudSetupWizard::slotContinueDetermineAuth()
} }
}); });
// When done, start checking status.php. // Step 3: When done, start checking status.php.
connect(redirectCheckJob, &SimpleNetworkJob::finishedSignal, this, connect(redirectCheckJob, &SimpleNetworkJob::finishedSignal, this,
[this, account]() { [this, account]() {
CheckServerJob *job = new CheckServerJob(account, this); CheckServerJob *job = new CheckServerJob(account, this);
job->setIgnoreCredentialFailure(true); job->setIgnoreCredentialFailure(true);
connect(job, &CheckServerJob::instanceFound, this, &OwncloudSetupWizard::slotOwnCloudFoundAuth); connect(job, &CheckServerJob::instanceFound, this, &OwncloudSetupWizard::slotFoundServer);
connect(job, &CheckServerJob::instanceNotFound, this, &OwncloudSetupWizard::slotNoOwnCloudFoundAuth); connect(job, &CheckServerJob::instanceNotFound, this, &OwncloudSetupWizard::slotNoServerFound);
connect(job, &CheckServerJob::timeout, this, &OwncloudSetupWizard::slotNoOwnCloudFoundAuthTimeout); connect(job, &CheckServerJob::timeout, this, &OwncloudSetupWizard::slotNoServerFoundTimeout);
job->setTimeout((account->url().scheme() == "https") ? 30 * 1000 : 10 * 1000); job->setTimeout((account->url().scheme() == "https") ? 30 * 1000 : 10 * 1000);
job->start(); job->start();
}); });
} }
void OwncloudSetupWizard::slotOwnCloudFoundAuth(const QUrl &url, const QJsonObject &info) void OwncloudSetupWizard::slotFoundServer(const QUrl &url, const QJsonObject &info)
{ {
auto serverVersion = CheckServerJob::version(info); auto serverVersion = CheckServerJob::version(info);
@ -230,14 +263,10 @@ void OwncloudSetupWizard::slotOwnCloudFoundAuth(const QUrl &url, const QJsonObje
qCInfo(lcWizard) << " was redirected to" << url.toString(); qCInfo(lcWizard) << " was redirected to" << url.toString();
} }
DetermineAuthTypeJob *job = new DetermineAuthTypeJob(_ocWizard->account(), this); slotDetermineAuthType();
job->setIgnoreCredentialFailure(true);
connect(job, &DetermineAuthTypeJob::authType,
_ocWizard, &OwncloudWizard::setAuthType);
job->start();
} }
void OwncloudSetupWizard::slotNoOwnCloudFoundAuth(QNetworkReply *reply) void OwncloudSetupWizard::slotNoServerFound(QNetworkReply *reply)
{ {
auto job = qobject_cast<CheckServerJob *>(sender()); auto job = qobject_cast<CheckServerJob *>(sender());
int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
@ -250,7 +279,7 @@ void OwncloudSetupWizard::slotNoOwnCloudFoundAuth(QNetworkReply *reply)
} else { } else {
msg = tr("Failed to connect to %1 at %2:<br/>%3") msg = tr("Failed to connect to %1 at %2:<br/>%3")
.arg(Utility::escape(Theme::instance()->appNameGUI()), .arg(Utility::escape(Theme::instance()->appNameGUI()),
Utility::escape(reply->url().toString()), Utility::escape(_ocWizard->account()->url().toString()),
Utility::escape(job->errorString())); Utility::escape(job->errorString()));
} }
bool isDowngradeAdvised = checkDowngradeAdvised(reply); bool isDowngradeAdvised = checkDowngradeAdvised(reply);
@ -265,7 +294,8 @@ void OwncloudSetupWizard::slotNoOwnCloudFoundAuth(QNetworkReply *reply)
QString serverError = reply->peek(1024 * 20); QString serverError = reply->peek(1024 * 20);
qCDebug(lcWizard) << serverError; qCDebug(lcWizard) << serverError;
QMessageBox messageBox(_ocWizard); QMessageBox messageBox(_ocWizard);
messageBox.setText(serverError); messageBox.setText(tr("The server reported the following error:"));
messageBox.setInformativeText(serverError);
messageBox.addButton(QMessageBox::Ok); messageBox.addButton(QMessageBox::Ok);
messageBox.setTextFormat(Qt::RichText); messageBox.setTextFormat(Qt::RichText);
messageBox.exec(); messageBox.exec();
@ -279,12 +309,20 @@ void OwncloudSetupWizard::slotNoOwnCloudFoundAuth(QNetworkReply *reply)
_ocWizard->account()->resetRejectedCertificates(); _ocWizard->account()->resetRejectedCertificates();
} }
void OwncloudSetupWizard::slotNoOwnCloudFoundAuthTimeout(const QUrl &url) void OwncloudSetupWizard::slotNoServerFoundTimeout(const QUrl &url)
{ {
_ocWizard->displayError( _ocWizard->displayError(
tr("Timeout while trying to connect to %1 at %2.") tr("Timeout while trying to connect to %1 at %2.")
.arg(Utility::escape(Theme::instance()->appNameGUI()), Utility::escape(url.toString())), .arg(Utility::escape(Theme::instance()->appNameGUI()), Utility::escape(url.toString())),
false); false);
}
void OwncloudSetupWizard::slotDetermineAuthType()
{
DetermineAuthTypeJob *job = new DetermineAuthTypeJob(_ocWizard->account(), this);
connect(job, &DetermineAuthTypeJob::authType,
_ocWizard, &OwncloudWizard::setAuthType);
job->start();
} }
void OwncloudSetupWizard::slotConnectToOCUrl(const QString &url) void OwncloudSetupWizard::slotConnectToOCUrl(const QString &url)
@ -579,6 +617,8 @@ void OwncloudSetupWizard::slotAssistantFinished(int result)
folderDefinition.localPath = localFolder; folderDefinition.localPath = localFolder;
folderDefinition.targetPath = FolderDefinition::prepareTargetPath(_remoteFolder); folderDefinition.targetPath = FolderDefinition::prepareTargetPath(_remoteFolder);
folderDefinition.ignoreHiddenFiles = folderMan->ignoreHiddenFiles(); folderDefinition.ignoreHiddenFiles = folderMan->ignoreHiddenFiles();
if (folderMan->navigationPaneHelper().showInExplorerNavigationPane())
folderDefinition.navigationPaneClsid = QUuid::createUuid();
auto f = folderMan->addFolder(account, folderDefinition); auto f = folderMan->addFolder(account, folderDefinition);
if (f) { if (f) {

View file

@ -49,12 +49,16 @@ signals:
void ownCloudWizardDone(int); void ownCloudWizardDone(int);
private slots: private slots:
void slotDetermineAuthType(const QString &); void slotCheckServer(const QString &);
void slotSystemProxyLookupDone(const QNetworkProxy &proxy); void slotSystemProxyLookupDone(const QNetworkProxy &proxy);
void slotContinueDetermineAuth();
void slotOwnCloudFoundAuth(const QUrl &, const QJsonObject &); void slotFindServer();
void slotNoOwnCloudFoundAuth(QNetworkReply *reply); void slotFindServerBehindRedirect();
void slotNoOwnCloudFoundAuthTimeout(const QUrl &url); void slotFoundServer(const QUrl &, const QJsonObject &);
void slotNoServerFound(QNetworkReply *reply);
void slotNoServerFoundTimeout(const QUrl &url);
void slotDetermineAuthType();
void slotConnectToOCUrl(const QString &); void slotConnectToOCUrl(const QString &);
void slotAuthError(); void slotAuthError();

View file

@ -32,6 +32,19 @@
namespace OCC { namespace OCC {
bool SortedTreeWidgetItem::operator<(const QTreeWidgetItem &other) const
{
int column = treeWidget()->sortColumn();
if (column != 0) {
return QTreeWidgetItem::operator<(other);
}
// Items with empty "File" column are larger than others,
// otherwise sort by time (this uses lexicographic ordering)
return std::forward_as_tuple(text(1).isEmpty(), data(0, Qt::UserRole).toDateTime())
< std::forward_as_tuple(other.text(1).isEmpty(), other.data(0, Qt::UserRole).toDateTime());
}
ProtocolWidget::ProtocolWidget(QWidget *parent) ProtocolWidget::ProtocolWidget(QWidget *parent)
: QWidget(parent) : QWidget(parent)
, _ui(new Ui::ProtocolWidget) , _ui(new Ui::ProtocolWidget)
@ -86,6 +99,16 @@ void ProtocolWidget::showEvent(QShowEvent *ev)
{ {
ConfigFile cfg; ConfigFile cfg;
cfg.restoreGeometryHeader(_ui->_treeWidget->header()); cfg.restoreGeometryHeader(_ui->_treeWidget->header());
// Sorting by section was newly enabled. But if we restore the header
// from a state where sorting was disabled, both of these flags will be
// false and sorting will be impossible!
_ui->_treeWidget->header()->setSectionsClickable(true);
_ui->_treeWidget->header()->setSortIndicatorShown(true);
// Switch back to "by time" ordering
_ui->_treeWidget->sortByColumn(0, Qt::DescendingOrder);
QWidget::showEvent(ev); QWidget::showEvent(ev);
} }
@ -158,14 +181,15 @@ QTreeWidgetItem *ProtocolWidget::createCompletedTreewidgetItem(const QString &fo
columns << Utility::octetsToString(item._size); columns << Utility::octetsToString(item._size);
} }
QTreeWidgetItem *twitem = new QTreeWidgetItem(columns); QTreeWidgetItem *twitem = new SortedTreeWidgetItem(columns);
twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight())); twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight()));
twitem->setData(0, Qt::UserRole, timestamp);
twitem->setIcon(0, icon); twitem->setIcon(0, icon);
twitem->setToolTip(0, longTimeStr); twitem->setToolTip(0, longTimeStr);
twitem->setToolTip(1, item._file); twitem->setToolTip(1, item._file);
twitem->setToolTip(3, message);
twitem->setData(0, Qt::UserRole, item._status);
twitem->setData(2, Qt::UserRole, folder); twitem->setData(2, Qt::UserRole, folder);
twitem->setToolTip(3, message);
twitem->setData(3, Qt::UserRole, item._status);
return twitem; return twitem;
} }

View file

@ -34,6 +34,21 @@ namespace Ui {
} }
class Application; class Application;
/**
* A QTreeWidgetItem with special sorting.
*
* It allows items for global entries to be moved to the top if the
* sorting section is the "Time" column.
*/
class SortedTreeWidgetItem : public QTreeWidgetItem
{
public:
using QTreeWidgetItem::QTreeWidgetItem;
private:
bool operator<(const QTreeWidgetItem &other) const override;
};
/** /**
* @brief The ProtocolWidget class * @brief The ProtocolWidget class
* @ingroup gui * @ingroup gui

View file

@ -35,6 +35,9 @@
<property name="uniformRowHeights"> <property name="uniformRowHeights">
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="columnCount"> <property name="columnCount">
<number>4</number> <number>4</number>
</property> </property>

View file

@ -157,7 +157,8 @@ void SettingsDialogMac::accountAdded(AccountState *s)
connect(accountSettings, &AccountSettings::openFolderAlias, _gui, &ownCloudGui::slotFolderOpenAction); connect(accountSettings, &AccountSettings::openFolderAlias, _gui, &ownCloudGui::slotFolderOpenAction);
connect(accountSettings, &AccountSettings::showIssuesList, this, &SettingsDialogMac::showIssuesList); connect(accountSettings, &AccountSettings::showIssuesList, this, &SettingsDialogMac::showIssuesList);
connect(s->account().data(), SIGNAL(accountChangedAvatar()), this, SLOT(slotAccountAvatarChanged())); connect(s->account().data(), &Account::accountChangedAvatar, this, &SettingsDialogMac::slotAccountAvatarChanged);
connect(s->account().data(), &Account::accountChangedDisplayName, this, &SettingsDialogMac::slotAccountDisplayNameChanged);
slotRefreshActivity(s); slotRefreshActivity(s);
} }
@ -195,4 +196,23 @@ void SettingsDialogMac::slotAccountAvatarChanged()
} }
} }
} }
void SettingsDialogMac::slotAccountDisplayNameChanged()
{
Account *account = static_cast<Account *>(sender());
auto list = findChildren<AccountSettings *>(QString());
foreach (auto p, list) {
if (p->accountsState()->account() == account) {
int idx = indexForPanel(p);
QString displayName = account->displayName();
if (!displayName.isNull()) {
displayName = Theme::instance()->multiAccount()
? SettingsDialogCommon::shortDisplayNameForSettings(account, 0)
: tr("Account");
setPreferencesPanelTitle(idx, displayName);
}
}
}
}
} }

View file

@ -54,6 +54,7 @@ private slots:
void accountAdded(AccountState *); void accountAdded(AccountState *);
void accountRemoved(AccountState *); void accountRemoved(AccountState *);
void slotAccountAvatarChanged(); void slotAccountAvatarChanged();
void slotAccountDisplayNameChanged();
private: private:
void closeEvent(QCloseEvent *event); void closeEvent(QCloseEvent *event);

View file

@ -133,6 +133,7 @@ ShareDialog::ShareDialog(QPointer<AccountState> accountState,
job->setProperties( job->setProperties(
QList<QByteArray>() QList<QByteArray>()
<< "http://open-collaboration-services.org/ns:share-permissions" << "http://open-collaboration-services.org/ns:share-permissions"
<< "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation
<< "http://owncloud.org/ns:privatelink"); << "http://owncloud.org/ns:privatelink");
job->setTimeout(10 * 1000); job->setTimeout(10 * 1000);
connect(job, &PropfindJob::result, this, &ShareDialog::slotPropfindReceived); connect(job, &PropfindJob::result, this, &ShareDialog::slotPropfindReceived);
@ -160,9 +161,13 @@ void ShareDialog::slotPropfindReceived(const QVariantMap &result)
qCInfo(lcSharing) << "Received sharing permissions for" << _sharePath << _maxSharingPermissions; qCInfo(lcSharing) << "Received sharing permissions for" << _sharePath << _maxSharingPermissions;
} }
auto privateLinkUrl = result["privatelink"].toString(); auto privateLinkUrl = result["privatelink"].toString();
auto numericFileId = result["fileid"].toByteArray();
if (!privateLinkUrl.isEmpty()) { if (!privateLinkUrl.isEmpty()) {
qCInfo(lcSharing) << "Received private link url for" << _sharePath << privateLinkUrl; qCInfo(lcSharing) << "Received private link url for" << _sharePath << privateLinkUrl;
_privateLinkUrl = privateLinkUrl; _privateLinkUrl = privateLinkUrl;
} else if (!numericFileId.isEmpty()) {
qCInfo(lcSharing) << "Received numeric file id for" << _sharePath << numericFileId;
_privateLinkUrl = _accountState->account()->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded);
} }
showSharingUi(); showSharingUi();

View file

@ -155,14 +155,15 @@ ShareLinkWidget::ShareLinkWidget(AccountPtr account,
// Prepare sharing menu // Prepare sharing menu
_shareLinkMenu = new QMenu(this); _linkContextMenu = new QMenu(this);
connect(_shareLinkMenu, &QMenu::triggered, connect(_linkContextMenu, &QMenu::triggered,
this, &ShareLinkWidget::slotShareLinkActionTriggered); this, &ShareLinkWidget::slotLinkContextMenuActionTriggered);
_openLinkAction = _shareLinkMenu->addAction(tr("Open link in browser")); _deleteLinkAction = _linkContextMenu->addAction(tr("Delete"));
_copyLinkAction = _shareLinkMenu->addAction(tr("Copy link to clipboard")); _openLinkAction = _linkContextMenu->addAction(tr("Open link in browser"));
_copyDirectLinkAction = _shareLinkMenu->addAction(tr("Copy link to clipboard (direct download)")); _copyLinkAction = _linkContextMenu->addAction(tr("Copy link to clipboard"));
_emailLinkAction = _shareLinkMenu->addAction(tr("Send link by email")); _copyDirectLinkAction = _linkContextMenu->addAction(tr("Copy link to clipboard (direct download)"));
_emailDirectLinkAction = _shareLinkMenu->addAction(tr("Send link by email (direct download)")); _emailLinkAction = _linkContextMenu->addAction(tr("Send link by email"));
_emailDirectLinkAction = _linkContextMenu->addAction(tr("Send link by email (direct download)"));
/* /*
* Create the share manager and connect it properly * Create the share manager and connect it properly
@ -231,28 +232,24 @@ void ShareLinkWidget::slotSharesFetched(const QList<QSharedPointer<Share>> &shar
table->insertRow(row); table->insertRow(row);
auto nameItem = new QTableWidgetItem; auto nameItem = new QTableWidgetItem;
QString name = linkShare->getName(); auto name = shareName(*linkShare);
if (name.isEmpty()) { if (!_namesSupported) {
if (!_namesSupported) { nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable);
name = tr("Public link");
nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable);
} else {
name = linkShare->getToken();
}
} }
nameItem->setText(name); nameItem->setText(name);
nameItem->setData(Qt::UserRole, QVariant::fromValue(linkShare)); nameItem->setData(Qt::UserRole, QVariant::fromValue(linkShare));
table->setItem(row, 0, nameItem); table->setItem(row, 0, nameItem);
auto shareButton = new QToolButton; auto dotdotdotButton = new QToolButton;
shareButton->setText("..."); dotdotdotButton->setText("...");
shareButton->setProperty(propertyShareC, QVariant::fromValue(linkShare)); dotdotdotButton->setProperty(propertyShareC, QVariant::fromValue(linkShare));
connect(shareButton, &QAbstractButton::clicked, this, &ShareLinkWidget::slotShareLinkButtonClicked); connect(dotdotdotButton, &QAbstractButton::clicked, this, &ShareLinkWidget::slotContextMenuButtonClicked);
table->setCellWidget(row, 1, shareButton); table->setCellWidget(row, 1, dotdotdotButton);
auto deleteButton = new QToolButton; auto deleteButton = new QToolButton;
deleteButton->setIcon(deleteIcon); deleteButton->setIcon(deleteIcon);
deleteButton->setProperty(propertyShareC, QVariant::fromValue(linkShare)); deleteButton->setProperty(propertyShareC, QVariant::fromValue(linkShare));
deleteButton->setToolTip(tr("Delete link share"));
connect(deleteButton, &QAbstractButton::clicked, this, &ShareLinkWidget::slotDeleteShareClicked); connect(deleteButton, &QAbstractButton::clicked, this, &ShareLinkWidget::slotDeleteShareClicked);
table->setCellWidget(row, 2, deleteButton); table->setCellWidget(row, 2, deleteButton);
@ -514,22 +511,56 @@ void ShareLinkWidget::openShareLink(const QUrl &url)
Utility::openBrowser(url, this); Utility::openBrowser(url, this);
} }
void ShareLinkWidget::slotShareLinkButtonClicked() void ShareLinkWidget::confirmAndDeleteShare(const QSharedPointer<LinkShare> &share)
{
auto messageBox = new QMessageBox(
QMessageBox::Question,
tr("Confirm Link Share Deletion"),
tr("<p>Do you really want to delete the public link share <i>%1</i>?</p>"
"<p>Note: This action cannot be undone.</p>")
.arg(shareName(*share)),
QMessageBox::NoButton,
this);
QPushButton *yesButton =
messageBox->addButton(tr("Delete"), QMessageBox::YesRole);
messageBox->addButton(tr("Cancel"), QMessageBox::NoRole);
connect(messageBox, &QMessageBox::finished, this,
[messageBox, yesButton, share]() {
if (messageBox->clickedButton() == yesButton)
share->deleteShare();
});
messageBox->open();
}
QString ShareLinkWidget::shareName(const LinkShare &share) const
{
QString name = share.getName();
if (!name.isEmpty())
return name;
if (!_namesSupported)
return tr("Public link");
return share.getToken();
}
void ShareLinkWidget::slotContextMenuButtonClicked()
{ {
auto share = sender()->property(propertyShareC).value<QSharedPointer<LinkShare>>(); auto share = sender()->property(propertyShareC).value<QSharedPointer<LinkShare>>();
bool downloadEnabled = share->getShowFileListing(); bool downloadEnabled = share->getShowFileListing();
_copyDirectLinkAction->setVisible(downloadEnabled); _copyDirectLinkAction->setVisible(downloadEnabled);
_emailDirectLinkAction->setVisible(downloadEnabled); _emailDirectLinkAction->setVisible(downloadEnabled);
_shareLinkMenu->setProperty(propertyShareC, QVariant::fromValue(share)); _linkContextMenu->setProperty(propertyShareC, QVariant::fromValue(share));
_shareLinkMenu->exec(QCursor::pos()); _linkContextMenu->exec(QCursor::pos());
} }
void ShareLinkWidget::slotShareLinkActionTriggered(QAction *action) void ShareLinkWidget::slotLinkContextMenuActionTriggered(QAction *action)
{ {
auto share = sender()->property(propertyShareC).value<QSharedPointer<LinkShare>>(); auto share = sender()->property(propertyShareC).value<QSharedPointer<LinkShare>>();
if (action == _copyLinkAction) { if (action == _deleteLinkAction) {
confirmAndDeleteShare(share);
} else if (action == _copyLinkAction) {
QApplication::clipboard()->setText(share->getLink().toString()); QApplication::clipboard()->setText(share->getLink().toString());
} else if (action == _copyDirectLinkAction) { } else if (action == _copyDirectLinkAction) {
QApplication::clipboard()->setText(share->getDirectDownloadLink().toString()); QApplication::clipboard()->setText(share->getDirectDownloadLink().toString());
@ -545,7 +576,7 @@ void ShareLinkWidget::slotShareLinkActionTriggered(QAction *action)
void ShareLinkWidget::slotDeleteShareClicked() void ShareLinkWidget::slotDeleteShareClicked()
{ {
auto share = sender()->property(propertyShareC).value<QSharedPointer<LinkShare>>(); auto share = sender()->property(propertyShareC).value<QSharedPointer<LinkShare>>();
share->deleteShare(); confirmAndDeleteShare(share);
} }
void ShareLinkWidget::slotPermissionsCheckboxClicked() void ShareLinkWidget::slotPermissionsCheckboxClicked()

View file

@ -70,8 +70,8 @@ private slots:
void slotPasswordChanged(const QString &newText); void slotPasswordChanged(const QString &newText);
void slotNameEdited(QTableWidgetItem *item); void slotNameEdited(QTableWidgetItem *item);
void slotShareLinkButtonClicked(); void slotContextMenuButtonClicked();
void slotShareLinkActionTriggered(QAction *action); void slotLinkContextMenuActionTriggered(QAction *action);
void slotDeleteShareFetched(); void slotDeleteShareFetched();
void slotCreateShareFetched(const QSharedPointer<LinkShare> &share); void slotCreateShareFetched(const QSharedPointer<LinkShare> &share);
@ -93,6 +93,12 @@ private:
void emailShareLink(const QUrl &url); void emailShareLink(const QUrl &url);
void openShareLink(const QUrl &url); void openShareLink(const QUrl &url);
/** Confirm with the user and then delete the share */
void confirmAndDeleteShare(const QSharedPointer<LinkShare> &share);
/** Retrieve a share's name, accounting for _namesSupported */
QString shareName(const LinkShare &share) const;
/** /**
* Retrieve the selected share, returning 0 if none. * Retrieve the selected share, returning 0 if none.
*/ */
@ -120,12 +126,13 @@ private:
// the next time getShares() finishes. This stores its id. // the next time getShares() finishes. This stores its id.
QString _newShareOverrideSelectionId; QString _newShareOverrideSelectionId;
QMenu *_shareLinkMenu; QMenu *_linkContextMenu = nullptr;
QAction *_openLinkAction; QAction *_deleteLinkAction = nullptr;
QAction *_copyLinkAction; QAction *_openLinkAction = nullptr;
QAction *_copyDirectLinkAction; QAction *_copyLinkAction = nullptr;
QAction *_emailLinkAction; QAction *_copyDirectLinkAction = nullptr;
QAction *_emailDirectLinkAction; QAction *_emailLinkAction = nullptr;
QAction *_emailDirectLinkAction = nullptr;
}; };
} }

View file

@ -15,25 +15,41 @@
#include "sharemanager.h" #include "sharemanager.h"
#include "ocssharejob.h" #include "ocssharejob.h"
#include "account.h" #include "account.h"
#include "folderman.h"
#include "accountstate.h"
#include <QUrl> #include <QUrl>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray> #include <QJsonArray>
namespace {
struct CreateShare
{
QString path;
OCC::Share::ShareType shareType;
QString shareWith;
OCC::Share::Permissions permissions;
};
} // anonymous namespace
Q_DECLARE_METATYPE(CreateShare)
namespace OCC { namespace OCC {
/**
* When a share is modified, we need to tell the folders so they can adjust overlay icons
*/
static void updateFolder(const AccountPtr &account, const QString &path)
{
foreach (Folder *f, FolderMan::instance()->map()) {
if (f->accountState()->account() != account)
continue;
auto folderPath = f->remotePath();
if (path.startsWith(folderPath) && (path == folderPath || folderPath.endsWith('/') || path[folderPath.size()] == '/')) {
// Workaround the fact that the server does not invalidate the etags of parent directories
// when something is shared.
auto relative = path.midRef(folderPath.size());
if (relative.startsWith('/'))
relative = relative.mid(1);
f->journalDb()->avoidReadFromDbOnNextSync(relative.toString());
// Schedule a sync so it can update the remote permission flag and let the socket API
// know about the shared icon.
f->scheduleThisFolderSoon();
}
}
}
Share::Share(AccountPtr account, Share::Share(AccountPtr account,
const QString &id, const QString &id,
const QString &path, const QString &path,
@ -54,6 +70,11 @@ AccountPtr Share::account() const
return _account; return _account;
} }
QString Share::path() const
{
return _path;
}
QString Share::getId() const QString Share::getId() const
{ {
return _id; return _id;
@ -99,6 +120,8 @@ void Share::deleteShare()
void Share::slotDeleted() void Share::slotDeleted()
{ {
emit shareDeleted(); emit shareDeleted();
updateFolder(_account, _path);
} }
void Share::slotOcsError(int statusCode, const QString &message) void Share::slotOcsError(int statusCode, const QString &message)
@ -258,6 +281,8 @@ void ShareManager::slotLinkShareCreated(const QJsonDocument &reply)
QSharedPointer<LinkShare> share(parseLinkShare(data)); QSharedPointer<LinkShare> share(parseLinkShare(data));
emit linkShareCreated(share); emit linkShareCreated(share);
updateFolder(_account, share->path());
} }
@ -267,51 +292,34 @@ void ShareManager::createShare(const QString &path,
const Share::Permissions permissions) const Share::Permissions permissions)
{ {
auto job = new OcsShareJob(_account); auto job = new OcsShareJob(_account);
// Store values that we need for creating this share later.
CreateShare continuation;
continuation.path = path;
continuation.shareType = shareType;
continuation.shareWith = shareWith;
continuation.permissions = permissions;
_jobContinuation[job] = QVariant::fromValue(continuation);
connect(job, &OcsShareJob::shareJobFinished, this, &ShareManager::slotCreateShare);
connect(job, &OcsJob::ocsError, this, &ShareManager::slotOcsError); connect(job, &OcsJob::ocsError, this, &ShareManager::slotOcsError);
connect(job, &OcsShareJob::shareJobFinished, this,
[=](const QJsonDocument &reply) {
// Find existing share permissions (if this was shared with us)
Share::Permissions existingPermissions = SharePermissionDefault;
foreach (const QJsonValue &element, reply.object()["ocs"].toObject()["data"].toArray()) {
auto map = element.toObject();
if (map["file_target"] == path)
existingPermissions = Share::Permissions(map["permissions"].toInt());
}
// Limit the permissions we request for a share to the ones the item
// was shared with initially.
auto perm = permissions;
if (permissions == SharePermissionDefault) {
perm = existingPermissions;
} else if (existingPermissions != SharePermissionDefault) {
perm &= existingPermissions;
}
OcsShareJob *job = new OcsShareJob(_account);
connect(job, &OcsShareJob::shareJobFinished, this, &ShareManager::slotShareCreated);
connect(job, &OcsJob::ocsError, this, &ShareManager::slotOcsError);
job->createShare(path, shareType, shareWith, permissions);
});
job->getSharedWithMe(); job->getSharedWithMe();
} }
void ShareManager::slotCreateShare(const QJsonDocument &reply)
{
if (!_jobContinuation.contains(sender()))
return;
CreateShare cont = _jobContinuation[sender()].value<CreateShare>();
if (cont.path.isEmpty())
return;
_jobContinuation.remove(sender());
// Find existing share permissions (if this was shared with us)
Share::Permissions existingPermissions = SharePermissionDefault;
foreach (const QJsonValue &element, reply.object()["ocs"].toObject()["data"].toArray()) {
auto map = element.toObject();
if (map["file_target"] == cont.path)
existingPermissions = Share::Permissions(map["permissions"].toInt());
}
// Limit the permissions we request for a share to the ones the item
// was shared with initially.
if (cont.permissions == SharePermissionDefault) {
cont.permissions = existingPermissions;
} else if (existingPermissions != SharePermissionDefault) {
cont.permissions &= existingPermissions;
}
OcsShareJob *job = new OcsShareJob(_account);
connect(job, &OcsShareJob::shareJobFinished, this, &ShareManager::slotShareCreated);
connect(job, &OcsJob::ocsError, this, &ShareManager::slotOcsError);
job->createShare(cont.path, cont.shareType, cont.shareWith, cont.permissions);
}
void ShareManager::slotShareCreated(const QJsonDocument &reply) void ShareManager::slotShareCreated(const QJsonDocument &reply)
{ {
@ -320,6 +328,8 @@ void ShareManager::slotShareCreated(const QJsonDocument &reply)
QSharedPointer<Share> share(parseShare(data)); QSharedPointer<Share> share(parseShare(data));
emit shareCreated(share); emit shareCreated(share);
updateFolder(_account, share->path());
} }
void ShareManager::fetchShares(const QString &path) void ShareManager::fetchShares(const QString &path)

View file

@ -64,6 +64,8 @@ public:
*/ */
AccountPtr account() const; AccountPtr account() const;
QString path() const;
/* /*
* Get the id * Get the id
*/ */
@ -293,13 +295,10 @@ private slots:
void slotLinkShareCreated(const QJsonDocument &reply); void slotLinkShareCreated(const QJsonDocument &reply);
void slotShareCreated(const QJsonDocument &reply); void slotShareCreated(const QJsonDocument &reply);
void slotOcsError(int statusCode, const QString &message); void slotOcsError(int statusCode, const QString &message);
void slotCreateShare(const QJsonDocument &reply);
private: private:
QSharedPointer<LinkShare> parseLinkShare(const QJsonObject &data); QSharedPointer<LinkShare> parseLinkShare(const QJsonObject &data);
QSharedPointer<Share> parseShare(const QJsonObject &data); QSharedPointer<Share> parseShare(const QJsonObject &data);
QMap<QObject *, QVariant> _jobContinuation;
AccountPtr _account; AccountPtr _account;
}; };
} }

View file

@ -503,27 +503,29 @@ void fetchPrivateLinkUrl(const QString &localFile, SocketApi *target, void (Sock
const QString localFileClean = QDir::cleanPath(localFile); const QString localFileClean = QDir::cleanPath(localFile);
const QString file = localFileClean.mid(shareFolder->cleanPath().length() + 1); const QString file = localFileClean.mid(shareFolder->cleanPath().length() + 1);
AccountPtr account = shareFolder->accountState()->account();
// Generate private link ourselves: used as a fallback // Generate private link ourselves: used as a fallback
SyncJournalFileRecord rec; SyncJournalFileRecord rec;
if (!shareFolder->journalDb()->getFileRecord(file, &rec) || !rec.isValid()) if (!shareFolder->journalDb()->getFileRecord(file, &rec) || !rec.isValid())
return; return;
const QString oldUrl = const QString oldUrl =
shareFolder->accountState()->account()->deprecatedPrivateLinkUrl(rec.numericFileId()).toString(QUrl::FullyEncoded); account->deprecatedPrivateLinkUrl(rec.numericFileId()).toString(QUrl::FullyEncoded);
// If the server doesn't have the property, use the old url directly. // Retrieve the new link or numeric file id by PROPFIND
if (!shareFolder->accountState()->account()->capabilities().privateLinkPropertyAvailable()) { PropfindJob *job = new PropfindJob(account, file, target);
(target->*targetFun)(oldUrl); job->setProperties(
return; QList<QByteArray>()
} << "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation
<< "http://owncloud.org/ns:privatelink");
// Retrieve the new link by PROPFIND
PropfindJob *job = new PropfindJob(shareFolder->accountState()->account(), file, target);
job->setProperties(QList<QByteArray>() << "http://owncloud.org/ns:privatelink");
job->setTimeout(10 * 1000); job->setTimeout(10 * 1000);
QObject::connect(job, &PropfindJob::result, target, [=](const QVariantMap &result) { QObject::connect(job, &PropfindJob::result, target, [=](const QVariantMap &result) {
auto privateLinkUrl = result["privatelink"].toString(); auto privateLinkUrl = result["privatelink"].toString();
auto numericFileId = result["fileid"].toByteArray();
if (!privateLinkUrl.isEmpty()) { if (!privateLinkUrl.isEmpty()) {
(target->*targetFun)(privateLinkUrl); (target->*targetFun)(privateLinkUrl);
} else if (!numericFileId.isEmpty()) {
(target->*targetFun)(account->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded));
} else { } else {
(target->*targetFun)(oldUrl); (target->*targetFun)(oldUrl);
} }

View file

@ -201,13 +201,15 @@ bool OwncloudSetupPage::urlHasChanged()
int OwncloudSetupPage::nextId() const int OwncloudSetupPage::nextId() const
{ {
if (_authType == DetermineAuthTypeJob::Basic) { switch (_authType) {
case DetermineAuthTypeJob::Basic:
return WizardCommon::Page_HttpCreds; return WizardCommon::Page_HttpCreds;
} else if (_authType == DetermineAuthTypeJob::OAuth) { case DetermineAuthTypeJob::OAuth:
return WizardCommon::Page_OAuthCreds; return WizardCommon::Page_OAuthCreds;
} else { case DetermineAuthTypeJob::Shibboleth:
return WizardCommon::Page_ShibbolethCreds; return WizardCommon::Page_ShibbolethCreds;
} }
return WizardCommon::Page_HttpCreds;
} }
QString OwncloudSetupPage::url() const QString OwncloudSetupPage::url() const
@ -334,7 +336,7 @@ void OwncloudSetupPage::slotCertificateAccepted()
addCertDial->reinit(); // FIXME: Why not just have this only created on use? addCertDial->reinit(); // FIXME: Why not just have this only created on use?
validatePage(); validatePage();
} else { } else {
addCertDial->showErrorMessage("Could not load certificate"); addCertDial->showErrorMessage(tr("Could not load certificate. Maybe wrong password?"));
addCertDial->show(); addCertDial->show();
} }
} }

View file

@ -135,6 +135,7 @@ void AbstractNetworkJob::adoptRequest(QNetworkReply *reply)
addTimer(reply); addTimer(reply);
setReply(reply); setReply(reply);
setupConnections(reply); setupConnections(reply);
newReplyHook(reply);
} }
QUrl AbstractNetworkJob::makeAccountUrl(const QString &relativePath) const QUrl AbstractNetworkJob::makeAccountUrl(const QString &relativePath) const
@ -172,8 +173,6 @@ void AbstractNetworkJob::slotFinished()
QUrl requestedUrl = reply()->request().url(); QUrl requestedUrl = reply()->request().url();
QUrl redirectUrl = reply()->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); QUrl redirectUrl = reply()->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
if (_followRedirects && !redirectUrl.isEmpty()) { if (_followRedirects && !redirectUrl.isEmpty()) {
_redirectCount++;
// Redirects may be relative // Redirects may be relative
if (redirectUrl.isRelative()) if (redirectUrl.isRelative())
redirectUrl = requestedUrl.resolved(redirectUrl); redirectUrl = requestedUrl.resolved(redirectUrl);
@ -194,27 +193,32 @@ void AbstractNetworkJob::slotFinished()
QByteArray verb = requestVerb(*reply()); QByteArray verb = requestVerb(*reply());
if (requestedUrl.scheme() == QLatin1String("https") && redirectUrl.scheme() == QLatin1String("http")) { if (requestedUrl.scheme() == QLatin1String("https") && redirectUrl.scheme() == QLatin1String("http")) {
qCWarning(lcNetworkJob) << this << "HTTPS->HTTP downgrade detected!"; qCWarning(lcNetworkJob) << this << "HTTPS->HTTP downgrade detected!";
} else if (requestedUrl == redirectUrl || _redirectCount >= maxRedirects()) { } else if (requestedUrl == redirectUrl || _redirectCount + 1 >= maxRedirects()) {
qCWarning(lcNetworkJob) << this << "Redirect loop detected!"; qCWarning(lcNetworkJob) << this << "Redirect loop detected!";
} else if (_requestBody && _requestBody->isSequential()) { } else if (_requestBody && _requestBody->isSequential()) {
qCWarning(lcNetworkJob) << this << "cannot redirect request with sequential body"; qCWarning(lcNetworkJob) << this << "cannot redirect request with sequential body";
} else if (verb.isEmpty()) { } else if (verb.isEmpty()) {
qCWarning(lcNetworkJob) << this << "cannot redirect request: could not detect original verb"; qCWarning(lcNetworkJob) << this << "cannot redirect request: could not detect original verb";
} else { } else {
emit redirected(_reply, redirectUrl, _redirectCount - 1); emit redirected(_reply, redirectUrl, _redirectCount);
// Create the redirected request and send it // The signal emission may have changed this value
qCInfo(lcNetworkJob) << "Redirecting" << verb << requestedUrl << redirectUrl; if (_followRedirects) {
resetTimeout(); _redirectCount++;
if (_requestBody) {
_requestBody->seek(0); // Create the redirected request and send it
qCInfo(lcNetworkJob) << "Redirecting" << verb << requestedUrl << redirectUrl;
resetTimeout();
if (_requestBody) {
_requestBody->seek(0);
}
sendRequest(
verb,
redirectUrl,
reply()->request(),
_requestBody);
return;
} }
sendRequest(
verb,
redirectUrl,
reply()->request(),
_requestBody);
return;
} }
} }

View file

@ -66,6 +66,7 @@ public:
* requests where custom handling is necessary. * requests where custom handling is necessary.
*/ */
void setFollowRedirects(bool follow); void setFollowRedirects(bool follow);
bool followRedirects() const { return _followRedirects; }
QByteArray responseTimestamp(); QByteArray responseTimestamp();
@ -107,8 +108,6 @@ signals:
void redirected(QNetworkReply *reply, const QUrl &targetUrl, int redirectCount); void redirected(QNetworkReply *reply, const QUrl &targetUrl, int redirectCount);
protected: protected:
void setupConnections(QNetworkReply *reply);
/** Initiate a network request, returning a QNetworkReply. /** Initiate a network request, returning a QNetworkReply.
* *
* Calls setReply() and setupConnections() on it. * Calls setReply() and setupConnections() on it.
@ -133,6 +132,16 @@ protected:
*/ */
void adoptRequest(QNetworkReply *reply); void adoptRequest(QNetworkReply *reply);
void setupConnections(QNetworkReply *reply);
/** Can be used by derived classes to set up the network reply.
*
* Particularly useful when the request is redirected and reply()
* changes. For things like setting up additional signal connections
* on the new reply.
*/
virtual void newReplyHook(QNetworkReply *) {}
/// Creates a url for the account from a relative path /// Creates a url for the account from a relative path
QUrl makeAccountUrl(const QString &relativePath) const; QUrl makeAccountUrl(const QString &relativePath) const;

View file

@ -176,7 +176,7 @@ QUrl Account::davUrl() const
QUrl Account::deprecatedPrivateLinkUrl(const QByteArray &numericFileId) const QUrl Account::deprecatedPrivateLinkUrl(const QByteArray &numericFileId) const
{ {
return Utility::concatUrlPath(url(), return Utility::concatUrlPath(_userVisibleUrl,
QLatin1String("/index.php/f/") + QUrl::toPercentEncoding(QString::fromLatin1(numericFileId))); QLatin1String("/index.php/f/") + QUrl::toPercentEncoding(QString::fromLatin1(numericFileId)));
} }
@ -311,6 +311,12 @@ void Account::setSslErrorHandler(AbstractSslErrorHandler *handler)
void Account::setUrl(const QUrl &url) void Account::setUrl(const QUrl &url)
{ {
_url = url; _url = url;
_userVisibleUrl = url;
}
void Account::setUserVisibleHost(const QString &host)
{
_userVisibleUrl.setHost(host);
} }
QVariant Account::credentialSetting(const QString &key) const QVariant Account::credentialSetting(const QString &key) const

View file

@ -99,6 +99,9 @@ public:
void setUrl(const QUrl &url); void setUrl(const QUrl &url);
QUrl url() const { return _url; } QUrl url() const { return _url; }
/// Adjusts _userVisibleUrl once the host to use is discovered.
void setUserVisibleHost(const QString &host);
/** /**
* @brief The possibly themed dav path for the account. It has * @brief The possibly themed dav path for the account. It has
* a trailing slash. * a trailing slash.
@ -266,6 +269,15 @@ private:
QImage _avatarImg; QImage _avatarImg;
QMap<QString, QVariant> _settingsMap; QMap<QString, QVariant> _settingsMap;
QUrl _url; QUrl _url;
/** If url to use for any user-visible urls.
*
* If the server configures overwritehost this can be different from
* the connection url in _url. We retrieve the visible host through
* the ocs/v1.php/config endpoint in ConnectionValidator.
*/
QUrl _userVisibleUrl;
QList<QSslCertificate> _approvedCerts; QList<QSslCertificate> _approvedCerts;
QSslConfiguration _sslConfiguration; QSslConfiguration _sslConfiguration;
Capabilities _capabilities; Capabilities _capabilities;

View file

@ -145,4 +145,9 @@ QList<int> Capabilities::httpErrorCodesThatResetFailingChunkedUploads() const
} }
return list; return list;
} }
QString Capabilities::invalidFilenameRegex() const
{
return _capabilities["dav"].toMap()["invalidFilenameRegex"].toString();
}
} }

View file

@ -105,6 +105,17 @@ public:
*/ */
QList<int> httpErrorCodesThatResetFailingChunkedUploads() const; QList<int> httpErrorCodesThatResetFailingChunkedUploads() const;
/**
* Regex that, if contained in a filename, will result in it not being uploaded.
*
* For servers older than 8.1.0 it defaults to [\\:?*"<>|]
* For servers >= that version, it defaults to the empty regex (the server
* will indicate invalid characters through an upload error)
*
* Note that it just needs to be contained. The regex [ab] is contained in "car".
*/
QString invalidFilenameRegex() const;
private: private:
QVariantMap _capabilities; QVariantMap _capabilities;
}; };

View file

@ -36,6 +36,7 @@
#include <QNetworkProxy> #include <QNetworkProxy>
#define DEFAULT_REMOTE_POLL_INTERVAL 30000 // default remote poll time in milliseconds #define DEFAULT_REMOTE_POLL_INTERVAL 30000 // default remote poll time in milliseconds
#define DEFAULT_FULL_LOCAL_DISCOVERY_INTERVAL (60 * 60 * 1000) // 1 hour
#define DEFAULT_MAX_LOG_LINES 20000 #define DEFAULT_MAX_LOG_LINES 20000
namespace OCC { namespace OCC {
@ -45,11 +46,13 @@ Q_LOGGING_CATEGORY(lcConfigFile, "sync.configfile", QtInfoMsg)
//static const char caCertsKeyC[] = "CaCertificates"; only used from account.cpp //static const char caCertsKeyC[] = "CaCertificates"; only used from account.cpp
static const char remotePollIntervalC[] = "remotePollInterval"; static const char remotePollIntervalC[] = "remotePollInterval";
static const char forceSyncIntervalC[] = "forceSyncInterval"; static const char forceSyncIntervalC[] = "forceSyncInterval";
static const char fullLocalDiscoveryIntervalC[] = "fullLocalDiscoveryInterval";
static const char notificationRefreshIntervalC[] = "notificationRefreshInterval"; static const char notificationRefreshIntervalC[] = "notificationRefreshInterval";
static const char monoIconsC[] = "monoIcons"; static const char monoIconsC[] = "monoIcons";
static const char promptDeleteC[] = "promptDeleteAllFiles"; static const char promptDeleteC[] = "promptDeleteAllFiles";
static const char crashReporterC[] = "crashReporter"; static const char crashReporterC[] = "crashReporter";
static const char optionalDesktopNoficationsC[] = "optionalDesktopNotifications"; static const char optionalDesktopNoficationsC[] = "optionalDesktopNotifications";
static const char showInExplorerNavigationPaneC[] = "showInExplorerNavigationPane";
static const char skipUpdateCheckC[] = "skipUpdateCheck"; static const char skipUpdateCheckC[] = "skipUpdateCheck";
static const char updateCheckIntervalC[] = "updateCheckInterval"; static const char updateCheckIntervalC[] = "updateCheckInterval";
static const char geometryC[] = "geometry"; static const char geometryC[] = "geometry";
@ -122,6 +125,26 @@ bool ConfigFile::optionalDesktopNotifications() const
return settings.value(QLatin1String(optionalDesktopNoficationsC), true).toBool(); return settings.value(QLatin1String(optionalDesktopNoficationsC), true).toBool();
} }
bool ConfigFile::showInExplorerNavigationPane() const
{
const bool defaultValue =
#ifdef Q_OS_WIN
QSysInfo::windowsVersion() >= QSysInfo::WV_WINDOWS10
#else
false
#endif
;
QSettings settings(configFile(), QSettings::IniFormat);
return settings.value(QLatin1String(showInExplorerNavigationPaneC), defaultValue).toBool();
}
void ConfigFile::setShowInExplorerNavigationPane(bool show)
{
QSettings settings(configFile(), QSettings::IniFormat);
settings.setValue(QLatin1String(showInExplorerNavigationPaneC), show);
settings.sync();
}
int ConfigFile::timeout() const int ConfigFile::timeout() const
{ {
QSettings settings(configFile(), QSettings::IniFormat); QSettings settings(configFile(), QSettings::IniFormat);
@ -406,6 +429,13 @@ quint64 ConfigFile::forceSyncInterval(const QString &connection) const
return interval; return interval;
} }
qint64 ConfigFile::fullLocalDiscoveryInterval() const
{
QSettings settings(configFile(), QSettings::IniFormat);
settings.beginGroup(defaultConnection());
return settings.value(QLatin1String(fullLocalDiscoveryIntervalC), DEFAULT_FULL_LOCAL_DISCOVERY_INTERVAL).toLongLong();
}
quint64 ConfigFile::notificationRefreshInterval(const QString &connection) const quint64 ConfigFile::notificationRefreshInterval(const QString &connection) const
{ {
QString con(connection); QString con(connection);

View file

@ -72,6 +72,13 @@ public:
/* Force sync interval, in milliseconds */ /* Force sync interval, in milliseconds */
quint64 forceSyncInterval(const QString &connection = QString()) const; quint64 forceSyncInterval(const QString &connection = QString()) const;
/**
* Interval in milliseconds within which full local discovery is required
*
* Use -1 to disable regular full local discoveries.
*/
qint64 fullLocalDiscoveryInterval() const;
bool monoIcons() const; bool monoIcons() const;
void setMonoIcons(bool); void setMonoIcons(bool);
@ -116,6 +123,9 @@ public:
bool optionalDesktopNotifications() const; bool optionalDesktopNotifications() const;
void setOptionalDesktopNotifications(bool show); void setOptionalDesktopNotifications(bool show);
bool showInExplorerNavigationPane() const;
void setShowInExplorerNavigationPane(bool show);
int timeout() const; int timeout() const;
quint64 chunkSize() const; quint64 chunkSize() const;
quint64 maxChunkSize() const; quint64 maxChunkSize() const;

View file

@ -18,6 +18,7 @@
#include <QNetworkReply> #include <QNetworkReply>
#include <QNetworkProxyFactory> #include <QNetworkProxyFactory>
#include <QPixmap> #include <QPixmap>
#include <QXmlStreamReader>
#include "connectionvalidator.h" #include "connectionvalidator.h"
#include "account.h" #include "account.h"
@ -247,10 +248,22 @@ void ConnectionValidator::slotAuthSuccess()
void ConnectionValidator::checkServerCapabilities() void ConnectionValidator::checkServerCapabilities()
{ {
// The main flow now needs the capabilities
JsonApiJob *job = new JsonApiJob(_account, QLatin1String("ocs/v1.php/cloud/capabilities"), this); JsonApiJob *job = new JsonApiJob(_account, QLatin1String("ocs/v1.php/cloud/capabilities"), this);
job->setTimeout(timeoutToUseMsec); job->setTimeout(timeoutToUseMsec);
QObject::connect(job, &JsonApiJob::jsonReceived, this, &ConnectionValidator::slotCapabilitiesRecieved); QObject::connect(job, &JsonApiJob::jsonReceived, this, &ConnectionValidator::slotCapabilitiesRecieved);
job->start(); job->start();
// And we'll retrieve the ocs config in parallel
// note that 'this' might be destroyed before the job finishes, so intentionally not parented
auto configJob = new JsonApiJob(_account, QLatin1String("ocs/v1.php/config"));
configJob->setTimeout(timeoutToUseMsec);
auto account = _account; // capturing account by value will make it live long enough
QObject::connect(configJob, &JsonApiJob::jsonReceived, _account.data(),
[=](const QJsonDocument &json) {
ocsConfigReceived(json, account);
});
configJob->start();
} }
void ConnectionValidator::slotCapabilitiesRecieved(const QJsonDocument &json) void ConnectionValidator::slotCapabilitiesRecieved(const QJsonDocument &json)
@ -268,6 +281,17 @@ void ConnectionValidator::slotCapabilitiesRecieved(const QJsonDocument &json)
fetchUser(); fetchUser();
} }
void ConnectionValidator::ocsConfigReceived(const QJsonDocument &json, AccountPtr account)
{
QString host = json.object().value("ocs").toObject().value("data").toObject().value("host").toString();
if (host.isEmpty()) {
qCWarning(lcConnectionValidator) << "Could not extract 'host' from ocs config reply";
return;
}
qCInfo(lcConnectionValidator) << "Determined user-visible host to be" << host;
account->setUserVisibleHost(host);
}
void ConnectionValidator::fetchUser() void ConnectionValidator::fetchUser()
{ {
JsonApiJob *job = new JsonApiJob(_account, QLatin1String("ocs/v1.php/cloud/user"), this); JsonApiJob *job = new JsonApiJob(_account, QLatin1String("ocs/v1.php/cloud/user"), this);

View file

@ -58,9 +58,9 @@ namespace OCC {
| |
+---------------------------+ +---------------------------+
| |
+-> checkServerCapabilities (cloud/capabilities) +-> checkServerCapabilities --------------v (in parallel)
JsonApiJob JsonApiJob (cloud/capabilities) JsonApiJob (ocs/v1.php/config)
| | +-> ocsConfigReceived
+-> slotCapabilitiesRecieved -+ +-> slotCapabilitiesRecieved -+
| |
+-----------------------------------+ +-----------------------------------+
@ -129,6 +129,7 @@ private:
void reportResult(Status status); void reportResult(Status status);
void checkServerCapabilities(); void checkServerCapabilities();
void fetchUser(); void fetchUser();
static void ocsConfigReceived(const QJsonDocument &json, AccountPtr account);
/** Sets the account's server version /** Sets the account's server version
* *

View file

@ -14,6 +14,7 @@
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QString> #include <QString>
#include <QCoreApplication>
#include "common/asserts.h" #include "common/asserts.h"
#include "creds/abstractcredentials.h" #include "creds/abstractcredentials.h"
@ -53,6 +54,15 @@ QString AbstractCredentials::keychainKey(const QString &url, const QString &user
QString key = user + QLatin1Char(':') + u; QString key = user + QLatin1Char(':') + u;
if (!accountId.isEmpty()) { if (!accountId.isEmpty()) {
key += QLatin1Char(':') + accountId; key += QLatin1Char(':') + accountId;
#ifdef Q_OS_WIN
// On Windows the credential keys aren't namespaced properly
// by qtkeychain. To work around that we manually add namespacing
// to the generated keys. See #6125.
// It's safe to do that since the key format is changing for 2.4
// anyway to include the account ids. That means old keys can be
// migrated to new namespaced keys on windows for 2.4.
key.prepend(QCoreApplication::applicationName() + "_");
#endif
} }
return key; return key;
} }

View file

@ -442,32 +442,38 @@ void HttpCredentials::persist()
_account->setCredentialSetting(QLatin1String(isOAuthC), isUsingOAuth()); _account->setCredentialSetting(QLatin1String(isOAuthC), isUsingOAuth());
_account->wantsAccountSaved(_account); _account->wantsAccountSaved(_account);
// write cert // write cert if there is one
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName()); if (!_clientSslCertificate.isNull()) {
addSettingsToJob(_account, job); WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
job->setInsecureFallback(false); addSettingsToJob(_account, job);
connect(job, &Job::finished, this, &HttpCredentials::slotWriteClientCertPEMJobDone); job->setInsecureFallback(false);
job->setKey(keychainKey(_account->url().toString(), _user + clientCertificatePEMC, _account->id())); connect(job, &Job::finished, this, &HttpCredentials::slotWriteClientCertPEMJobDone);
job->setBinaryData(_clientSslCertificate.toPem()); job->setKey(keychainKey(_account->url().toString(), _user + clientCertificatePEMC, _account->id()));
job->start(); job->setBinaryData(_clientSslCertificate.toPem());
job->start();
} else {
slotWriteClientCertPEMJobDone();
}
} }
void HttpCredentials::slotWriteClientCertPEMJobDone(Job *incomingJob) void HttpCredentials::slotWriteClientCertPEMJobDone()
{ {
Q_UNUSED(incomingJob); // write ssl key if there is one
// write ssl key if (!_clientSslKey.isNull()) {
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName()); WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job); addSettingsToJob(_account, job);
job->setInsecureFallback(false); job->setInsecureFallback(false);
connect(job, &Job::finished, this, &HttpCredentials::slotWriteClientKeyPEMJobDone); connect(job, &Job::finished, this, &HttpCredentials::slotWriteClientKeyPEMJobDone);
job->setKey(keychainKey(_account->url().toString(), _user + clientKeyPEMC, _account->id())); job->setKey(keychainKey(_account->url().toString(), _user + clientKeyPEMC, _account->id()));
job->setBinaryData(_clientSslKey.toPem()); job->setBinaryData(_clientSslKey.toPem());
job->start(); job->start();
} else {
slotWriteClientKeyPEMJobDone();
}
} }
void HttpCredentials::slotWriteClientKeyPEMJobDone(Job *incomingJob) void HttpCredentials::slotWriteClientKeyPEMJobDone()
{ {
Q_UNUSED(incomingJob);
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName()); WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
addSettingsToJob(_account, job); addSettingsToJob(_account, job);
job->setInsecureFallback(false); job->setInsecureFallback(false);

View file

@ -114,8 +114,8 @@ private Q_SLOTS:
void slotReadClientKeyPEMJobDone(QKeychain::Job *); void slotReadClientKeyPEMJobDone(QKeychain::Job *);
void slotReadJobDone(QKeychain::Job *); void slotReadJobDone(QKeychain::Job *);
void slotWriteClientCertPEMJobDone(QKeychain::Job *); void slotWriteClientCertPEMJobDone();
void slotWriteClientKeyPEMJobDone(QKeychain::Job *); void slotWriteClientKeyPEMJobDone();
void slotWriteJobDone(QKeychain::Job *); void slotWriteJobDone(QKeychain::Job *);
protected: protected:

View file

@ -17,6 +17,7 @@
#include "account.h" #include "account.h"
#include "theme.h" #include "theme.h"
#include "common/asserts.h" #include "common/asserts.h"
#include "common/checksums.h"
#include <csync_private.h> #include <csync_private.h>
#include <csync_rename.h> #include <csync_rename.h>
@ -300,28 +301,6 @@ void DiscoverySingleDirectoryJob::abort()
} }
} }
/**
* Returns the highest-quality checksum in a 'checksums'
* property retrieved from the server.
*
* Example: "ADLER32:1231 SHA1:ab124124 MD5:2131affa21"
* -> "SHA1:ab124124"
*/
static QByteArray findBestChecksum(const QByteArray &checksums)
{
int i = 0;
// The order of the searches here defines the preference ordering.
if (-1 != (i = checksums.indexOf("SHA1:"))
|| -1 != (i = checksums.indexOf("MD5:"))
|| -1 != (i = checksums.indexOf("Adler32:"))) {
// Now i is the start of the best checksum
// Grab it until the next space or end of string.
auto checksum = checksums.mid(i);
return checksum.mid(0, checksum.indexOf(" "));
}
return QByteArray();
}
static std::unique_ptr<csync_file_stat_t> propertyMapToFileStat(const QMap<QString, QString> &map) static std::unique_ptr<csync_file_stat_t> propertyMapToFileStat(const QMap<QString, QString> &map)
{ {
std::unique_ptr<csync_file_stat_t> file_stat(new csync_file_stat_t); std::unique_ptr<csync_file_stat_t> file_stat(new csync_file_stat_t);

View file

@ -819,71 +819,69 @@ bool JsonApiJob::finished()
} }
DetermineAuthTypeJob::DetermineAuthTypeJob(AccountPtr account, QObject *parent) DetermineAuthTypeJob::DetermineAuthTypeJob(AccountPtr account, QObject *parent)
: AbstractNetworkJob(account, QString(), parent) : QObject(parent)
, _redirects(0) , _account(account)
{ {
// This job implements special redirect handling to detect redirections
// to pages that are indicative of Shibboleth-using servers. Hence we
// disable the standard job redirection handling here.
_followRedirects = false;
} }
void DetermineAuthTypeJob::start() void DetermineAuthTypeJob::start()
{ {
send(account()->davUrl()); qCInfo(lcDetermineAuthTypeJob) << "Determining auth type for" << _account->davUrl();
AbstractNetworkJob::start();
}
bool DetermineAuthTypeJob::finished()
{
QUrl redirection = reply()->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
if (_redirects >= maxRedirects()) {
redirection.clear();
}
auto authChallenge = reply()->rawHeader("WWW-Authenticate").toLower();
if (redirection.isEmpty()) {
if (authChallenge.contains("bearer ")) {
emit authType(OAuth);
} else if (!authChallenge.isEmpty()) {
emit authType(Basic);
} else {
// This is also where we end up in case of network error.
emit authType(Unknown);
}
} else if (redirection.toString().endsWith(account()->davPath())) {
// do a new run
_redirects++;
resetTimeout();
send(redirection);
qCDebug(lcDetermineAuthTypeJob()) << "Redirected to:" << redirection.toString();
return false; // don't discard
} else {
#ifndef NO_SHIBBOLETH
QRegExp shibbolethyWords("SAML|wayf");
shibbolethyWords.setCaseSensitivity(Qt::CaseInsensitive);
if (redirection.toString().contains(shibbolethyWords)) {
emit authType(Shibboleth);
} else
#endif
{
// We got redirected to an address that doesn't look like shib
// and also doesn't have the davPath. Give up.
qCWarning(lcDetermineAuthTypeJob()) << account()->davUrl()
<< "was redirected to the incompatible address"
<< redirection.toString();
emit authType(Unknown);
}
}
return true;
}
void DetermineAuthTypeJob::send(const QUrl &url)
{
QNetworkRequest req; QNetworkRequest req;
// Prevent HttpCredentialsAccessManager from setting an Authorization header. // Prevent HttpCredentialsAccessManager from setting an Authorization header.
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true); req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
sendRequest("GET", url, req); // Don't reuse previous auth credentials
req.setAttribute(QNetworkRequest::AuthenticationReuseAttribute, QNetworkRequest::Manual);
// Don't send cookies, we can't determine the auth type if we're logged in
req.setAttribute(QNetworkRequest::CookieLoadControlAttribute, QNetworkRequest::Manual);
// Start two parallel requests, one determines whether it's a shib server
// and the other checks the HTTP auth method.
auto get = _account->sendRequest("GET", _account->davUrl(), req);
auto propfind = _account->sendRequest("PROPFIND", _account->davUrl(), req);
get->setTimeout(30 * 1000);
propfind->setTimeout(30 * 1000);
get->setIgnoreCredentialFailure(true);
propfind->setIgnoreCredentialFailure(true);
connect(get, &AbstractNetworkJob::redirected, this, [this, get](QNetworkReply *, const QUrl &target, int) {
#ifndef NO_SHIBBOLETH
QRegExp shibbolethyWords("SAML|wayf");
shibbolethyWords.setCaseSensitivity(Qt::CaseInsensitive);
if (target.toString().contains(shibbolethyWords)) {
_resultGet = Shibboleth;
get->setFollowRedirects(false);
}
#endif
});
connect(get, &SimpleNetworkJob::finishedSignal, this, [this]() {
_getDone = true;
checkBothDone();
});
connect(propfind, &SimpleNetworkJob::finishedSignal, this, [this](QNetworkReply *reply) {
auto authChallenge = reply->rawHeader("WWW-Authenticate").toLower();
if (authChallenge.contains("bearer ")) {
_resultPropfind = OAuth;
} else if (authChallenge.isEmpty()) {
qCWarning(lcDetermineAuthTypeJob) << "Did not receive WWW-Authenticate reply to auth-test PROPFIND";
}
_propfindDone = true;
checkBothDone();
});
}
void DetermineAuthTypeJob::checkBothDone()
{
if (!_getDone || !_propfindDone)
return;
auto result = _resultPropfind;
// OAuth > Shib > Basic
if (_resultGet == Shibboleth && result != OAuth)
result = Shibboleth;
qCInfo(lcDetermineAuthTypeJob) << "Auth type for" << _account->davUrl() << "is" << result;
emit authType(result);
deleteLater();
} }
SimpleNetworkJob::SimpleNetworkJob(AccountPtr account, QObject *parent) SimpleNetworkJob::SimpleNetworkJob(AccountPtr account, QObject *parent)

View file

@ -355,26 +355,29 @@ private:
* @brief Checks with auth type to use for a server * @brief Checks with auth type to use for a server
* @ingroup libsync * @ingroup libsync
*/ */
class OWNCLOUDSYNC_EXPORT DetermineAuthTypeJob : public AbstractNetworkJob class OWNCLOUDSYNC_EXPORT DetermineAuthTypeJob : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
enum AuthType { enum AuthType {
Unknown, Basic, // also the catch-all fallback for backwards compatibility reasons
Basic,
OAuth, OAuth,
Shibboleth Shibboleth
}; };
explicit DetermineAuthTypeJob(AccountPtr account, QObject *parent = 0); explicit DetermineAuthTypeJob(AccountPtr account, QObject *parent = 0);
void start() Q_DECL_OVERRIDE; void start();
signals: signals:
void authType(AuthType); void authType(AuthType);
private slots:
bool finished() Q_DECL_OVERRIDE;
private: private:
void send(const QUrl &url); void checkBothDone();
int _redirects;
AccountPtr _account;
AuthType _resultGet = Basic;
AuthType _resultPropfind = Basic;
bool _getDone = false;
bool _propfindDone = false;
}; };
/** /**

View file

@ -114,13 +114,13 @@ PropagateItemJob::~PropagateItemJob()
static qint64 getMinBlacklistTime() static qint64 getMinBlacklistTime()
{ {
return qMax(qgetenv("OWNCLOUD_BLACKLIST_TIME_MIN").toInt(), return qMax(qEnvironmentVariableIntValue("OWNCLOUD_BLACKLIST_TIME_MIN"),
25); // 25 seconds 25); // 25 seconds
} }
static qint64 getMaxBlacklistTime() static qint64 getMaxBlacklistTime()
{ {
int v = qgetenv("OWNCLOUD_BLACKLIST_TIME_MAX").toInt(); int v = qEnvironmentVariableIntValue("OWNCLOUD_BLACKLIST_TIME_MAX");
if (v > 0) if (v > 0)
return v; return v;
return 24 * 60 * 60; // 1 day return 24 * 60 * 60; // 1 day
@ -742,6 +742,16 @@ PropagatorJob::JobParallelism PropagatorCompositeJob::parallelism()
return FullParallelism; return FullParallelism;
} }
void PropagatorCompositeJob::slotSubJobAbortFinished()
{
// Count that job has been finished
_abortsCount--;
// Emit abort if last job has been aborted
if (_abortsCount == 0) {
emit abortFinished();
}
}
bool PropagatorCompositeJob::scheduleSelfOrChild() bool PropagatorCompositeJob::scheduleSelfOrChild()
{ {
@ -900,7 +910,8 @@ void PropagateDirectory::slotFirstJobFinished(SyncFileItem::Status status)
if (status != SyncFileItem::Success && status != SyncFileItem::Restoration) { if (status != SyncFileItem::Success && status != SyncFileItem::Restoration) {
if (_state != Finished) { if (_state != Finished) {
abort(); // Synchronously abort
abort(AbortType::Synchronous);
_state = Finished; _state = Finished;
emit finished(status); emit finished(status);
} }

View file

@ -65,6 +65,11 @@ class PropagatorJob : public QObject
public: public:
explicit PropagatorJob(OwncloudPropagator *propagator); explicit PropagatorJob(OwncloudPropagator *propagator);
enum AbortType {
Synchronous,
Asynchronous
};
enum JobState { enum JobState {
NotYetStarted, NotYetStarted,
Running, Running,
@ -98,7 +103,14 @@ public:
virtual qint64 committedDiskSpace() const { return 0; } virtual qint64 committedDiskSpace() const { return 0; }
public slots: public slots:
virtual void abort() {} /*
* Asynchronous abort requires emit of abortFinished() signal,
* while synchronous is expected to abort immedietaly.
*/
virtual void abort(PropagatorJob::AbortType abortType) {
if (abortType == AbortType::Asynchronous)
emit abortFinished();
}
/** Starts this job, or a new subjob /** Starts this job, or a new subjob
* returns true if a job was started. * returns true if a job was started.
@ -110,11 +122,14 @@ signals:
*/ */
void finished(SyncFileItem::Status); void finished(SyncFileItem::Status);
/**
* Emitted when the abort is fully finished
*/
void abortFinished(SyncFileItem::Status status = SyncFileItem::NormalError);
protected: protected:
OwncloudPropagator *propagator() const; OwncloudPropagator *propagator() const;
}; };
/* /*
* Abstract class to propagate a single item * Abstract class to propagate a single item
*/ */
@ -185,10 +200,11 @@ public:
SyncFileItemVector _tasksToDo; SyncFileItemVector _tasksToDo;
QVector<PropagatorJob *> _runningJobs; QVector<PropagatorJob *> _runningJobs;
SyncFileItem::Status _hasError; // NoStatus, or NormalError / SoftError if there was an error SyncFileItem::Status _hasError; // NoStatus, or NormalError / SoftError if there was an error
quint64 _abortsCount;
explicit PropagatorCompositeJob(OwncloudPropagator *propagator) explicit PropagatorCompositeJob(OwncloudPropagator *propagator)
: PropagatorJob(propagator) : PropagatorJob(propagator)
, _hasError(SyncFileItem::NoStatus) , _hasError(SyncFileItem::NoStatus), _abortsCount(0)
{ {
} }
@ -209,15 +225,32 @@ public:
virtual bool scheduleSelfOrChild() Q_DECL_OVERRIDE; virtual bool scheduleSelfOrChild() Q_DECL_OVERRIDE;
virtual JobParallelism parallelism() Q_DECL_OVERRIDE; virtual JobParallelism parallelism() Q_DECL_OVERRIDE;
virtual void abort() Q_DECL_OVERRIDE
/*
* Abort synchronously or asynchronously - some jobs
* require to be finished without immediete abort (abort on job might
* cause conflicts/duplicated files - owncloud/client/issues/5949)
*/
virtual void abort(PropagatorJob::AbortType abortType) Q_DECL_OVERRIDE
{ {
foreach (PropagatorJob *j, _runningJobs) if (!_runningJobs.empty()) {
j->abort(); _abortsCount = _runningJobs.size();
foreach (PropagatorJob *j, _runningJobs) {
if (abortType == AbortType::Asynchronous) {
connect(j, &PropagatorJob::abortFinished,
this, &PropagatorCompositeJob::slotSubJobAbortFinished);
}
j->abort(abortType);
}
} else if (abortType == AbortType::Asynchronous){
emit abortFinished();
}
} }
qint64 committedDiskSpace() const Q_DECL_OVERRIDE; qint64 committedDiskSpace() const Q_DECL_OVERRIDE;
private slots: private slots:
void slotSubJobAbortFinished();
bool possiblyRunNextJob(PropagatorJob *next) bool possiblyRunNextJob(PropagatorJob *next)
{ {
if (next->_state == NotYetStarted) { if (next->_state == NotYetStarted) {
@ -258,11 +291,17 @@ public:
virtual bool scheduleSelfOrChild() Q_DECL_OVERRIDE; virtual bool scheduleSelfOrChild() Q_DECL_OVERRIDE;
virtual JobParallelism parallelism() Q_DECL_OVERRIDE; virtual JobParallelism parallelism() Q_DECL_OVERRIDE;
virtual void abort() Q_DECL_OVERRIDE virtual void abort(PropagatorJob::AbortType abortType) Q_DECL_OVERRIDE
{ {
if (_firstJob) if (_firstJob)
_firstJob->abort(); // Force first job to abort synchronously
_subJobs.abort(); // even if caller allows async abort (asyncAbort)
_firstJob->abort(AbortType::Synchronous);
if (abortType == AbortType::Asynchronous){
connect(&_subJobs, &PropagatorCompositeJob::abortFinished, this, &PropagateDirectory::abortFinished);
}
_subJobs.abort(abortType);
} }
void increaseAffectedCount() void increaseAffectedCount()
@ -280,6 +319,7 @@ private slots:
void slotFirstJobFinished(SyncFileItem::Status status); void slotFirstJobFinished(SyncFileItem::Status status);
void slotSubJobsFinished(SyncFileItem::Status status); void slotSubJobsFinished(SyncFileItem::Status status);
}; };
@ -324,6 +364,7 @@ public:
, _chunkSize(10 * 1000 * 1000) // 10 MB, overridden in setSyncOptions , _chunkSize(10 * 1000 * 1000) // 10 MB, overridden in setSyncOptions
, _account(account) , _account(account)
{ {
qRegisterMetaType<PropagatorJob::AbortType>("PropagatorJob::AbortType");
} }
~OwncloudPropagator(); ~OwncloudPropagator();
@ -404,13 +445,23 @@ public:
void abort() void abort()
{ {
_abortRequested.fetchAndStoreOrdered(true); bool alreadyAborting = _abortRequested.fetchAndStoreOrdered(true);
if (alreadyAborting)
return;
if (_rootJob) { if (_rootJob) {
// We're possibly already in an item's finished stack // Connect to abortFinished which signals that abort has been asynchronously finished
QMetaObject::invokeMethod(_rootJob.data(), "abort", Qt::QueuedConnection); connect(_rootJob.data(), &PropagateDirectory::abortFinished, this, &OwncloudPropagator::emitFinished);
// Use Queued Connection because we're possibly already in an item's finished stack
QMetaObject::invokeMethod(_rootJob.data(), "abort", Qt::QueuedConnection,
Q_ARG(PropagatorJob::AbortType, PropagatorJob::AbortType::Asynchronous));
// Give asynchronous abort 5000 msec to finish on its own
QTimer::singleShot(5000, this, SLOT(abortTimeout()));
} else {
// No root job, call emitFinished
emitFinished(SyncFileItem::NormalError);
} }
// abort() of all jobs will likely have already resulted in finished being emitted, but just in case.
QMetaObject::invokeMethod(this, "emitFinished", Qt::QueuedConnection, Q_ARG(SyncFileItem::Status, SyncFileItem::NormalError));
} }
// timeout in seconds // timeout in seconds
@ -431,6 +482,13 @@ public:
private slots: private slots:
void abortTimeout()
{
// Abort synchronously and finish
_rootJob.data()->abort(PropagatorJob::AbortType::Synchronous);
emitFinished(SyncFileItem::NormalError);
}
/** Emit the finished signal and make sure it is only emitted once */ /** Emit the finished signal and make sure it is only emitted once */
void emitFinished(SyncFileItem::Status status) void emitFinished(SyncFileItem::Status status)
{ {

View file

@ -127,7 +127,6 @@ void GETFileJob::start()
sendRequest("GET", _directDownloadUrl, req); sendRequest("GET", _directDownloadUrl, req);
} }
reply()->setReadBufferSize(16 * 1024); // keep low so we can easier limit the bandwidth
qCDebug(lcGetJob) << _bandwidthManager << _bandwidthChoked << _bandwidthLimited; qCDebug(lcGetJob) << _bandwidthManager << _bandwidthChoked << _bandwidthLimited;
if (_bandwidthManager) { if (_bandwidthManager) {
_bandwidthManager->registerDownloadJob(this); _bandwidthManager->registerDownloadJob(this);
@ -137,14 +136,20 @@ void GETFileJob::start()
qCWarning(lcGetJob) << " Network error: " << errorString(); qCWarning(lcGetJob) << " Network error: " << errorString();
} }
connect(reply(), &QNetworkReply::metaDataChanged, this, &GETFileJob::slotMetaDataChanged);
connect(reply(), &QIODevice::readyRead, this, &GETFileJob::slotReadyRead);
connect(reply(), &QNetworkReply::downloadProgress, this, &GETFileJob::downloadProgress);
connect(this, &AbstractNetworkJob::networkActivity, account().data(), &Account::propagatorNetworkActivity); connect(this, &AbstractNetworkJob::networkActivity, account().data(), &Account::propagatorNetworkActivity);
AbstractNetworkJob::start(); AbstractNetworkJob::start();
} }
void GETFileJob::newReplyHook(QNetworkReply *reply)
{
reply->setReadBufferSize(16 * 1024); // keep low so we can easier limit the bandwidth
connect(reply, &QNetworkReply::metaDataChanged, this, &GETFileJob::slotMetaDataChanged);
connect(reply, &QIODevice::readyRead, this, &GETFileJob::slotReadyRead);
connect(reply, &QNetworkReply::downloadProgress, this, &GETFileJob::downloadProgress);
}
void GETFileJob::slotMetaDataChanged() void GETFileJob::slotMetaDataChanged()
{ {
// For some reason setting the read buffer in GETFileJob::start doesn't seem to go // For some reason setting the read buffer in GETFileJob::start doesn't seem to go
@ -153,6 +158,10 @@ void GETFileJob::slotMetaDataChanged()
int httpStatus = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); int httpStatus = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// Ignore redirects
if (httpStatus == 301 || httpStatus == 302 || httpStatus == 303 || httpStatus == 307 || httpStatus == 308)
return;
// If the status code isn't 2xx, don't write the reply body to the file. // If the status code isn't 2xx, don't write the reply body to the file.
// For any error: handle it when the job is finished, not here. // For any error: handle it when the job is finished, not here.
if (httpStatus / 100 != 2) { if (httpStatus / 100 != 2) {
@ -216,6 +225,8 @@ void GETFileJob::slotMetaDataChanged()
if (!lastModified.isNull()) { if (!lastModified.isNull()) {
_lastModified = Utility::qDateTimeToTime_t(lastModified.toDateTime()); _lastModified = Utility::qDateTimeToTime_t(lastModified.toDateTime());
} }
_saveBodyToFile = true;
} }
void GETFileJob::setBandwidthManager(BandwidthManager *bwm) void GETFileJob::setBandwidthManager(BandwidthManager *bwm)
@ -281,7 +292,7 @@ void GETFileJob::slotReadyRead()
return; return;
} }
if (_device->isOpen()) { if (_device->isOpen() && _saveBodyToFile) {
qint64 w = _device->write(buffer.constData(), r); qint64 w = _device->write(buffer.constData(), r);
if (w != r) { if (w != r) {
_errorString = _device->errorString(); _errorString = _device->errorString();
@ -346,13 +357,16 @@ void PropagateDownloadFile::start()
} }
} }
// If we have a conflict where size and mtime are identical, // If we have a conflict where size of the file is unchanged,
// compare the remote checksum to the local one. // compare the remote checksum to the local one.
// Maybe it's not a real conflict and no download is necessary! // Maybe it's not a real conflict and no download is necessary!
// If the hashes are collision safe and identical, we assume the content is too.
// For weak checksums, we only do that if the mtimes are also identical.
if (_item->_instruction == CSYNC_INSTRUCTION_CONFLICT if (_item->_instruction == CSYNC_INSTRUCTION_CONFLICT
&& _item->_size == _item->_previousSize && _item->_size == _item->_previousSize
&& _item->_modtime == _item->_previousModtime && !_item->_checksumHeader.isEmpty()
&& !_item->_checksumHeader.isEmpty()) { && (csync_is_collision_safe_hash(_item->_checksumHeader)
|| _item->_modtime == _item->_previousModtime)) {
qCDebug(lcPropagateDownload) << _item->_file << "may not need download, computing checksum"; qCDebug(lcPropagateDownload) << _item->_file << "may not need download, computing checksum";
auto computeChecksum = new ComputeChecksum(this); auto computeChecksum = new ComputeChecksum(this);
computeChecksum->setChecksumType(parseChecksumHeaderType(_item->_checksumHeader)); computeChecksum->setChecksumType(parseChecksumHeaderType(_item->_checksumHeader));
@ -368,8 +382,17 @@ void PropagateDownloadFile::start()
void PropagateDownloadFile::conflictChecksumComputed(const QByteArray &checksumType, const QByteArray &checksum) void PropagateDownloadFile::conflictChecksumComputed(const QByteArray &checksumType, const QByteArray &checksum)
{ {
if (makeChecksumHeader(checksumType, checksum) == _item->_checksumHeader) { if (makeChecksumHeader(checksumType, checksum) == _item->_checksumHeader) {
// No download necessary, just update fs and journal metadata
qCDebug(lcPropagateDownload) << _item->_file << "remote and local checksum match"; qCDebug(lcPropagateDownload) << _item->_file << "remote and local checksum match";
// No download necessary, just update metadata
// Apply the server mtime locally if necessary, ensuring the journal
// and local mtimes end up identical
auto fn = propagator()->getFilePath(_item->_file);
if (_item->_modtime != _item->_previousModtime) {
FileSystem::setModTime(fn, _item->_modtime);
emit propagator()->touchedFile(fn);
}
_item->_modtime = FileSystem::getModTime(fn);
updateMetadata(/*isConflict=*/false); updateMetadata(/*isConflict=*/false);
return; return;
} }
@ -624,7 +647,10 @@ void PropagateDownloadFile::slotGetFinished()
this, &PropagateDownloadFile::transmissionChecksumValidated); this, &PropagateDownloadFile::transmissionChecksumValidated);
connect(validator, &ValidateChecksumHeader::validationFailed, connect(validator, &ValidateChecksumHeader::validationFailed,
this, &PropagateDownloadFile::slotChecksumFail); this, &PropagateDownloadFile::slotChecksumFail);
auto checksumHeader = job->reply()->rawHeader(checkSumHeaderC); auto checksumHeader = findBestChecksum(job->reply()->rawHeader(checkSumHeaderC));
auto contentMd5Header = job->reply()->rawHeader(contentMd5HeaderC);
if (checksumHeader.isEmpty() && !contentMd5Header.isEmpty())
checksumHeader = "MD5:" + contentMd5Header;
validator->start(_tmpFile.fileName(), checksumHeader); validator->start(_tmpFile.fileName(), checksumHeader);
} }
@ -897,9 +923,13 @@ void PropagateDownloadFile::slotDownloadProgress(qint64 received, qint64)
} }
void PropagateDownloadFile::abort() void PropagateDownloadFile::abort(PropagatorJob::AbortType abortType)
{ {
if (_job && _job->reply()) if (_job && _job->reply())
_job->reply()->abort(); _job->reply()->abort();
if (abortType == AbortType::Asynchronous) {
emit abortFinished();
}
} }
} }

View file

@ -43,6 +43,9 @@ class GETFileJob : public AbstractNetworkJob
bool _hasEmittedFinishedSignal; bool _hasEmittedFinishedSignal;
time_t _lastModified; time_t _lastModified;
/// Will be set to true once we've seen a 2xx response header
bool _saveBodyToFile = false;
public: public:
// DOES NOT take ownership of the device. // DOES NOT take ownership of the device.
explicit GETFileJob(AccountPtr account, const QString &path, QFile *device, explicit GETFileJob(AccountPtr account, const QString &path, QFile *device,
@ -76,6 +79,8 @@ public:
} }
} }
void newReplyHook(QNetworkReply *reply) override;
void setBandwidthManager(BandwidthManager *bwm); void setBandwidthManager(BandwidthManager *bwm);
void setChoked(bool c); void setChoked(bool c);
void setBandwidthLimited(bool b); void setBandwidthLimited(bool b);
@ -185,7 +190,7 @@ private slots:
/// Called when it's time to update the db metadata /// Called when it's time to update the db metadata
void updateMetadata(bool isConflict); void updateMetadata(bool isConflict);
void abort() Q_DECL_OVERRIDE; void abort(PropagatorJob::AbortType abortType) Q_DECL_OVERRIDE;
void slotDownloadProgress(qint64, qint64); void slotDownloadProgress(qint64, qint64);
void slotChecksumFail(const QString &errMsg); void slotChecksumFail(const QString &errMsg);

View file

@ -75,10 +75,14 @@ void PropagateRemoteDelete::start()
_job->start(); _job->start();
} }
void PropagateRemoteDelete::abort() void PropagateRemoteDelete::abort(PropagatorJob::AbortType abortType)
{ {
if (_job && _job->reply()) if (_job && _job->reply())
_job->reply()->abort(); _job->reply()->abort();
if (abortType == AbortType::Asynchronous) {
emit abortFinished();
}
} }
void PropagateRemoteDelete::slotDeleteJobFinished() void PropagateRemoteDelete::slotDeleteJobFinished()

View file

@ -52,7 +52,7 @@ public:
{ {
} }
void start() Q_DECL_OVERRIDE; void start() Q_DECL_OVERRIDE;
void abort() Q_DECL_OVERRIDE; void abort(PropagatorJob::AbortType abortType) Q_DECL_OVERRIDE;
bool isLikelyFinishedQuickly() Q_DECL_OVERRIDE { return !_item->isDirectory(); } bool isLikelyFinishedQuickly() Q_DECL_OVERRIDE { return !_item->isDirectory(); }

View file

@ -60,10 +60,14 @@ void PropagateRemoteMkdir::slotStartMkcolJob()
_job->start(); _job->start();
} }
void PropagateRemoteMkdir::abort() void PropagateRemoteMkdir::abort(PropagatorJob::AbortType abortType)
{ {
if (_job && _job->reply()) if (_job && _job->reply())
_job->reply()->abort(); _job->reply()->abort();
if (abortType == AbortType::Asynchronous) {
emit abortFinished();
}
} }
void PropagateRemoteMkdir::setDeleteExisting(bool enabled) void PropagateRemoteMkdir::setDeleteExisting(bool enabled)

View file

@ -35,7 +35,7 @@ public:
{ {
} }
void start() Q_DECL_OVERRIDE; void start() Q_DECL_OVERRIDE;
void abort() Q_DECL_OVERRIDE; void abort(PropagatorJob::AbortType abortType) Q_DECL_OVERRIDE;
// Creating a directory should be fast. // Creating a directory should be fast.
bool isLikelyFinishedQuickly() Q_DECL_OVERRIDE { return true; } bool isLikelyFinishedQuickly() Q_DECL_OVERRIDE { return true; }

View file

@ -117,10 +117,14 @@ void PropagateRemoteMove::start()
_job->start(); _job->start();
} }
void PropagateRemoteMove::abort() void PropagateRemoteMove::abort(PropagatorJob::AbortType abortType)
{ {
if (_job && _job->reply()) if (_job && _job->reply())
_job->reply()->abort(); _job->reply()->abort();
if (abortType == AbortType::Asynchronous) {
emit abortFinished();
}
} }
void PropagateRemoteMove::slotMoveJobFinished() void PropagateRemoteMove::slotMoveJobFinished()

View file

@ -56,7 +56,7 @@ public:
{ {
} }
void start() Q_DECL_OVERRIDE; void start() Q_DECL_OVERRIDE;
void abort() Q_DECL_OVERRIDE; void abort(PropagatorJob::AbortType abortType) Q_DECL_OVERRIDE;
JobParallelism parallelism() Q_DECL_OVERRIDE { return _item->isDirectory() ? WaitForFinished : FullParallelism; } JobParallelism parallelism() Q_DECL_OVERRIDE { return _item->isDirectory() ? WaitForFinished : FullParallelism; }
/** /**

View file

@ -558,20 +558,24 @@ void PropagateUploadFileCommon::slotJobDestroyed(QObject *job)
_jobs.erase(std::remove(_jobs.begin(), _jobs.end(), job), _jobs.end()); _jobs.erase(std::remove(_jobs.begin(), _jobs.end(), job), _jobs.end());
} }
void PropagateUploadFileCommon::abort() void PropagateUploadFileCommon::abort(PropagatorJob::AbortType abortType)
{ {
foreach (auto *job, _jobs) { foreach (auto *job, _jobs) {
if (job->reply()) { if (job->reply()) {
job->reply()->abort(); job->reply()->abort();
} }
} }
if (abortType == AbortType::Asynchronous) {
emit abortFinished();
}
} }
// This function is used whenever there is an error occuring and jobs might be in progress // This function is used whenever there is an error occuring and jobs might be in progress
void PropagateUploadFileCommon::abortWithError(SyncFileItem::Status status, const QString &error) void PropagateUploadFileCommon::abortWithError(SyncFileItem::Status status, const QString &error)
{ {
_finished = true; _finished = true;
abort(); abort(AbortType::Synchronous);
done(status, error); done(status, error);
} }
@ -625,4 +629,33 @@ void PropagateUploadFileCommon::finalize()
done(SyncFileItem::Success); done(SyncFileItem::Success);
} }
void PropagateUploadFileCommon::prepareAbort(PropagatorJob::AbortType abortType) {
if (!_jobs.empty()) {
// Count number of jobs to be aborted asynchronously
_abortCount = _jobs.size();
foreach (AbstractNetworkJob *job, _jobs) {
// Check if async abort is requested
if (job->reply() && abortType == AbortType::Asynchronous) {
// Connect to finished signal of job reply
// to asynchonously finish the abort
connect(job->reply(), &QNetworkReply::finished, this, &PropagateUploadFileCommon::slotReplyAbortFinished);
}
}
} else if (abortType == AbortType::Asynchronous) {
// Empty job list, emit abortFinished immedietaly
emit abortFinished();
}
}
void PropagateUploadFileCommon::slotReplyAbortFinished()
{
_abortCount--;
if (_abortCount == 0) {
emit abortFinished();
}
}
} }

View file

@ -129,6 +129,11 @@ public:
return true; return true;
} }
QIODevice *device()
{
return _device;
}
QString errorString() QString errorString()
{ {
return _errorString.isEmpty() ? AbstractNetworkJob::errorString() : _errorString; return _errorString.isEmpty() ? AbstractNetworkJob::errorString() : _errorString;
@ -205,6 +210,7 @@ protected:
QVector<AbstractNetworkJob *> _jobs; /// network jobs that are currently in transit QVector<AbstractNetworkJob *> _jobs; /// network jobs that are currently in transit
bool _finished BITFIELD(1); /// Tells that all the jobs have been finished bool _finished BITFIELD(1); /// Tells that all the jobs have been finished
bool _deleteExisting BITFIELD(1); bool _deleteExisting BITFIELD(1);
quint64 _abortCount; /// Keep track of number of aborted items
// measure the performance of checksum calc and upload // measure the performance of checksum calc and upload
#ifdef WITH_TESTING #ifdef WITH_TESTING
@ -218,6 +224,7 @@ public:
: PropagateItemJob(propagator, item) : PropagateItemJob(propagator, item)
, _finished(false) , _finished(false)
, _deleteExisting(false) , _deleteExisting(false)
, _abortCount(0)
{ {
} }
@ -248,13 +255,20 @@ public:
void abortWithError(SyncFileItem::Status status, const QString &error); void abortWithError(SyncFileItem::Status status, const QString &error);
public slots: public slots:
void abort() Q_DECL_OVERRIDE; void abort(PropagatorJob::AbortType abortType) Q_DECL_OVERRIDE;
void slotJobDestroyed(QObject *job); void slotJobDestroyed(QObject *job);
private slots: private slots:
void slotReplyAbortFinished();
void slotPollFinished(); void slotPollFinished();
protected: protected:
/**
* Prepares the abort e.g. connects proper signals and slots
* to the subjobs to abort asynchronously
*/
void prepareAbort(PropagatorJob::AbortType abortType);
/** /**
* Checks whether the current error is one that should reset the whole * Checks whether the current error is one that should reset the whole
* transfer if it happens too often. If so: Bump UploadInfo::errorCount * transfer if it happens too often. If so: Bump UploadInfo::errorCount
@ -287,15 +301,15 @@ private:
* In the non-resuming case it is 0. * In the non-resuming case it is 0.
* If we are resuming, this is the first chunk we need to send * If we are resuming, this is the first chunk we need to send
*/ */
int _startChunk; int _startChunk = 0;
/** /**
* This is the next chunk that we need to send. Starting from 0 even if _startChunk != 0 * This is the next chunk that we need to send. Starting from 0 even if _startChunk != 0
* (In other words, _startChunk + _currentChunk is really the number of the chunk we need to send next) * (In other words, _startChunk + _currentChunk is really the number of the chunk we need to send next)
* (In other words, _currentChunk is the number of the chunk that we already sent or started sending) * (In other words, _currentChunk is the number of the chunk that we already sent or started sending)
*/ */
int _currentChunk; int _currentChunk = 0;
int _chunkCount; /// Total number of chunks for this file int _chunkCount = 0; /// Total number of chunks for this file
int _transferId; /// transfer id (part of the url) int _transferId = 0; /// transfer id (part of the url)
quint64 chunkSize() const { quint64 chunkSize() const {
// Old chunking does not use dynamic chunking algorithm, and does not adjusts the chunk size respectively, // Old chunking does not use dynamic chunking algorithm, and does not adjusts the chunk size respectively,
@ -303,7 +317,6 @@ private:
return propagator()->syncOptions()._initialChunkSize; return propagator()->syncOptions()._initialChunkSize;
} }
public: public:
PropagateUploadFileV1(OwncloudPropagator *propagator, const SyncFileItemPtr &item) PropagateUploadFileV1(OwncloudPropagator *propagator, const SyncFileItemPtr &item)
: PropagateUploadFileCommon(propagator, item) : PropagateUploadFileCommon(propagator, item)
@ -311,7 +324,8 @@ public:
} }
void doStartUpload() Q_DECL_OVERRIDE; void doStartUpload() Q_DECL_OVERRIDE;
public slots:
void abort(PropagatorJob::AbortType abortType) Q_DECL_OVERRIDE;
private slots: private slots:
void startNextChunk(); void startNextChunk();
void slotPutFinished(); void slotPutFinished();
@ -328,11 +342,11 @@ class PropagateUploadFileNG : public PropagateUploadFileCommon
{ {
Q_OBJECT Q_OBJECT
private: private:
quint64 _sent; /// amount of data (bytes) that was already sent quint64 _sent = 0; /// amount of data (bytes) that was already sent
uint _transferId; /// transfer id (part of the url) uint _transferId = 0; /// transfer id (part of the url)
int _currentChunk; /// Id of the next chunk that will be sent int _currentChunk = 0; /// Id of the next chunk that will be sent
quint64 _currentChunkSize; /// current chunk size quint64 _currentChunkSize = 0; /// current chunk size
bool _removeJobError; /// If not null, there was an error removing the job bool _removeJobError = false; /// If not null, there was an error removing the job
// Map chunk number with its size from the PROPFIND on resume. // Map chunk number with its size from the PROPFIND on resume.
// (Only used from slotPropfindIterate/slotPropfindFinished because the LsColJob use signals to report data.) // (Only used from slotPropfindIterate/slotPropfindFinished because the LsColJob use signals to report data.)
@ -352,7 +366,6 @@ private:
public: public:
PropagateUploadFileNG(OwncloudPropagator *propagator, const SyncFileItemPtr &item) PropagateUploadFileNG(OwncloudPropagator *propagator, const SyncFileItemPtr &item)
: PropagateUploadFileCommon(propagator, item) : PropagateUploadFileCommon(propagator, item)
, _currentChunkSize(0)
{ {
} }
@ -361,6 +374,8 @@ public:
private: private:
void startNewUpload(); void startNewUpload();
void startNextChunk(); void startNextChunk();
public slots:
void abort(AbortType abortType) Q_DECL_OVERRIDE;
private slots: private slots:
void slotPropfindFinished(); void slotPropfindFinished();
void slotPropfindFinishedWithError(); void slotPropfindFinishedWithError();

View file

@ -365,7 +365,7 @@ void PropagateUploadFileNG::slotPutFinished()
// target duration for each chunk upload. // target duration for each chunk upload.
double targetDuration = propagator()->syncOptions()._targetChunkUploadDuration; double targetDuration = propagator()->syncOptions()._targetChunkUploadDuration;
if (targetDuration > 0) { if (targetDuration > 0) {
double uploadTime = job->msSinceStart(); double uploadTime = job->msSinceStart() + 1; // add one to avoid div-by-zero
auto predictedGoodSize = static_cast<quint64>( auto predictedGoodSize = static_cast<quint64>(
_currentChunkSize / uploadTime * targetDuration); _currentChunkSize / uploadTime * targetDuration);
@ -491,4 +491,26 @@ void PropagateUploadFileNG::slotUploadProgress(qint64 sent, qint64 total)
} }
propagator()->reportProgress(*_item, _sent + sent - total); propagator()->reportProgress(*_item, _sent + sent - total);
} }
void PropagateUploadFileNG::abort(PropagatorJob::AbortType abortType)
{
// Prepare abort
prepareAbort(abortType);
// Abort all jobs (if there are any left), except final PUT
foreach (AbstractNetworkJob *job, _jobs) {
if (job->reply()) {
if (abortType == AbortType::Asynchronous && qobject_cast<MoveJob *>(job)){
// If it is async abort, dont abort
// MoveJob since it might result in conflict,
// only PUT and MKDIR jobs can be safely aborted.
continue;
}
// Abort the job
job->reply()->abort();
}
}
}
} }

View file

@ -350,4 +350,31 @@ void PropagateUploadFileV1::slotUploadProgress(qint64 sent, qint64 total)
} }
propagator()->reportProgress(*_item, amount); propagator()->reportProgress(*_item, amount);
} }
void PropagateUploadFileV1::abort(PropagatorJob::AbortType abortType)
{
// Prepare abort
prepareAbort(abortType);
// Abort all jobs (if there are any left), except final PUT
foreach (AbstractNetworkJob *job, _jobs) {
if (job->reply()) {
// If asynchronous abort allowed,
// dont abort final PUT which uploaded its data,
// since this might result in conflicts
if (PUTFileJob *putJob = qobject_cast<PUTFileJob *>(job)){
if (abortType == AbortType::Asynchronous
&& _chunkCount > 0
&& (((_currentChunk + _startChunk) % _chunkCount) == 0)
&& putJob->device()->atEnd()) {
continue;
}
}
// Abort the job
job->reply()->abort();
}
}
}
} }

View file

@ -25,6 +25,7 @@ namespace OCC {
* It's here for being shared between Upload- and Download Job * It's here for being shared between Upload- and Download Job
*/ */
static const char checkSumHeaderC[] = "OC-Checksum"; static const char checkSumHeaderC[] = "OC-Checksum";
static const char contentMd5HeaderC[] = "Content-MD5";
/** /**
* @brief Declaration of the other propagation jobs * @brief Declaration of the other propagation jobs

View file

@ -571,7 +571,6 @@ int SyncEngine::treewalkFile(csync_file_stat_t *file, csync_file_stat_t *other,
dir = SyncFileItem::None; dir = SyncFileItem::None;
// For directories, metadata-only updates will be done after all their files are propagated. // For directories, metadata-only updates will be done after all their files are propagated.
if (!isDirectory) { if (!isDirectory) {
emit syncItemDiscovered(*item);
// Update the database now already: New remote fileid or Etag or RemotePerm // Update the database now already: New remote fileid or Etag or RemotePerm
// Or for files that were detected as "resolved conflict". // Or for files that were detected as "resolved conflict".
@ -608,6 +607,9 @@ int SyncEngine::treewalkFile(csync_file_stat_t *file, csync_file_stat_t *other,
} }
_journal->setFileRecordMetadata(item->toSyncJournalFileRecordWithInode(filePath)); _journal->setFileRecordMetadata(item->toSyncJournalFileRecordWithInode(filePath));
// This might have changed the shared flag, so we must notify SyncFileStatusTracker for example
emit itemCompleted(item);
} else { } else {
// The local tree is walked first and doesn't have all the info from the server. // The local tree is walked first and doesn't have all the info from the server.
// Update only outdated data from the disk. // Update only outdated data from the disk.
@ -683,8 +685,6 @@ int SyncEngine::treewalkFile(csync_file_stat_t *file, csync_file_stat_t *other,
} }
_syncItemMap.insert(key, item); _syncItemMap.insert(key, item);
emit syncItemDiscovered(*item);
return re; return re;
} }
@ -813,6 +813,7 @@ void SyncEngine::startSync()
} }
_csync_ctx->read_remote_from_db = true; _csync_ctx->read_remote_from_db = true;
_lastLocalDiscoveryStyle = _csync_ctx->local_discovery_style;
bool ok; bool ok;
auto selectiveSyncBlackList = _journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok); auto selectiveSyncBlackList = _journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok);
@ -960,11 +961,20 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult)
} }
// Check for invalid character in old server version // Check for invalid character in old server version
if (_account->serverVersionInt() < Account::makeServerVersion(8, 1, 0)) { QString invalidFilenamePattern = _account->capabilities().invalidFilenameRegex();
// Server version older than 8.1 don't support these character in filename. if (invalidFilenamePattern.isNull()
static const QRegExp invalidCharRx("[\\\\:?*\"<>|]"); && _account->serverVersionInt() < Account::makeServerVersion(8, 1, 0)) {
// Server versions older than 8.1 don't support some characters in filenames.
// If the capability is not set, default to a pattern that avoids uploading
// files with names that contain these.
// It's important to respect the capability also for older servers -- the
// version check doesn't make sense for custom servers.
invalidFilenamePattern = "[\\\\:?*\"<>|]";
}
if (!invalidFilenamePattern.isEmpty()) {
const QRegExp invalidFilenameRx(invalidFilenamePattern);
for (auto it = syncItems.begin(); it != syncItems.end(); ++it) { for (auto it = syncItems.begin(); it != syncItems.end(); ++it) {
if ((*it)->_direction == SyncFileItem::Up && (*it)->destination().contains(invalidCharRx)) { if ((*it)->_direction == SyncFileItem::Up && (*it)->destination().contains(invalidFilenameRx)) {
(*it)->_errorString = tr("File name contains at least one invalid character"); (*it)->_errorString = tr("File name contains at least one invalid character");
(*it)->_instruction = CSYNC_INSTRUCTION_IGNORE; (*it)->_instruction = CSYNC_INSTRUCTION_IGNORE;
} }
@ -1022,7 +1032,7 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult)
_progressInfo->startEstimateUpdates(); _progressInfo->startEstimateUpdates();
// post update phase script: allow to tweak stuff by a custom script in debug mode. // post update phase script: allow to tweak stuff by a custom script in debug mode.
if (!qgetenv("OWNCLOUD_POST_UPDATE_SCRIPT").isEmpty()) { if (!qEnvironmentVariableIsEmpty("OWNCLOUD_POST_UPDATE_SCRIPT")) {
#ifndef NDEBUG #ifndef NDEBUG
QString script = qgetenv("OWNCLOUD_POST_UPDATE_SCRIPT"); QString script = qgetenv("OWNCLOUD_POST_UPDATE_SCRIPT");
@ -1455,7 +1465,7 @@ void SyncEngine::checkForPermission(SyncFileItemVector &syncItems)
RemotePermissions SyncEngine::getPermissions(const QString &file) const RemotePermissions SyncEngine::getPermissions(const QString &file) const
{ {
static bool isTest = qgetenv("OWNCLOUD_TEST_PERMISSIONS").toInt(); static bool isTest = qEnvironmentVariableIntValue("OWNCLOUD_TEST_PERMISSIONS");
if (isTest) { if (isTest) {
QRegExp rx("_PERM_([^_]*)_[^/]*$"); QRegExp rx("_PERM_([^_]*)_[^/]*$");
if (rx.indexIn(file) != -1) { if (rx.indexIn(file) != -1) {
@ -1555,6 +1565,12 @@ AccountPtr SyncEngine::account() const
return _account; return _account;
} }
void SyncEngine::setLocalDiscoveryOptions(LocalDiscoveryStyle style, std::set<QByteArray> dirs)
{
_csync_ctx->local_discovery_style = style;
_csync_ctx->locally_touched_dirs = std::move(dirs);
}
void SyncEngine::abort() void SyncEngine::abort()
{ {
if (_propagator) if (_propagator)

View file

@ -25,6 +25,7 @@
#include <QMap> #include <QMap>
#include <QStringList> #include <QStringList>
#include <QSharedPointer> #include <QSharedPointer>
#include <set>
#include <csync.h> #include <csync.h>
@ -92,6 +93,7 @@ public:
AccountPtr account() const; AccountPtr account() const;
SyncJournalDb *journal() const { return _journal; } SyncJournalDb *journal() const { return _journal; }
QString localPath() const { return _localPath; } QString localPath() const { return _localPath; }
/** /**
* Minimum age, in milisecond, of a file that can be uploaded. * Minimum age, in milisecond, of a file that can be uploaded.
* Files more recent than that are not going to be uploaeded as they are considered * Files more recent than that are not going to be uploaeded as they are considered
@ -99,14 +101,28 @@ public:
*/ */
static qint64 minimumFileAgeForUpload; // in ms static qint64 minimumFileAgeForUpload; // in ms
/**
* Control whether local discovery should read from filesystem or db.
*
* If style is Partial, the paths is a set of file paths relative to
* the synced folder. All the parent directories of these paths will not
* be read from the db and scanned on the filesystem.
*
* Note, the style and paths are only retained for the next sync and
* revert afterwards. Use _lastLocalDiscoveryStyle to discover the last
* sync's style.
*/
void setLocalDiscoveryOptions(LocalDiscoveryStyle style, std::set<QByteArray> dirs = {});
/** Access the last sync run's local discovery style */
LocalDiscoveryStyle lastLocalDiscoveryStyle() const { return _lastLocalDiscoveryStyle; }
signals: signals:
void csyncUnavailable(); void csyncUnavailable();
// During update, before reconcile // During update, before reconcile
void rootEtag(QString); void rootEtag(QString);
// before actual syncing (after update+reconcile) for each item
void syncItemDiscovered(const SyncFileItem &);
// after the above signals. with the items that actually need propagating // after the above signals. with the items that actually need propagating
void aboutToPropagate(SyncFileItemVector &); void aboutToPropagate(SyncFileItemVector &);
@ -272,6 +288,9 @@ private:
/** List of unique errors that occurred in a sync run. */ /** List of unique errors that occurred in a sync run. */
QSet<QString> _uniqueErrors; QSet<QString> _uniqueErrors;
/** The kind of local discovery the last sync run used */
LocalDiscoveryStyle _lastLocalDiscoveryStyle = LocalDiscoveryStyle::DatabaseAndFilesystem;
}; };
} }

Some files were not shown because too many files have changed in this diff Show more