Merge branch 'master' of https://github.com/owncloud/client into oc_master

This commit is contained in:
Roeland Jago Douma 2017-11-20 13:29:59 +01:00
commit 725be9ff54
No known key found for this signature in database
GPG key ID: F941078878347C0C
138 changed files with 11630 additions and 7448 deletions

View file

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

View file

@ -1,75 +1,103 @@
ChangeLog
=========
version 2.4.0 (2017-1X-XX)
* OAuth2 authentication support by opening external browser
* Sync Issues: More functional error view including filters and conflicts (#5516)
version 2.4.0 (2017-11-XX)
* If you're using 2.4.0 alpha1, please upgrade as the alpha1 had an issue with hidden files!
* OAuth2 authentication support by opening external browser (#5668)
* 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: Show warning that links are public
* Sharing: Many UI improvements
* Sharing: Show warning that links are public (#5747)
* Sharing: Sharing dialog redesign: multiple share links support (#5695)
* 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: Don't show last page anymore, go to settings directly (#5726)
* Wizard: Handle url-shortener redirects #5954
* Gui: Display the user server avatar
* Wizard: Handle url-shortener redirects (#5954)
* 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)
* 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: 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)
* Excludes: remove .htaccess form list of excluded files
* Excludes: Remove .htaccess form list of excluded files (#5701)
* Excludes: Hardcode desktop.ini
* Excludes: Allow escaping "#" (#6012)
* Excludes: Use faster matching via QRegularExpression
* Discovery: Increase the MAX_DEPTH and show deep folders as ignored
* Excludes: Use faster matching via QRegularExpression (#6063)
* Discovery: Increase the MAX_DEPTH and show deep folders as ignored (#1067)
* Discovery: General speed improvements
* Downloads: Remove empty temporary if disk space full (#5746)
* Downloads: Re-trigger folder discovery on 404
* Quota: PropagateUpload: Model of remote quota, avoid some uploads (#5537)
* When creating explorer favorite use more specific windows functions (#5690)
* Downloads: Read Content-MD5 header for object store setups
* Checksums: Add global disable environment variable (#5017)
* Quota: PropagateUpload: Model of remote quota, avoid some uploads (#5537)
* 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)
* 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: Detect changes in the shared flag (#6098)
* Windows Overlay Icons: Potential hang fixes
* Linux Overlay Icons: fix branded nemo and caja shell integration (#5966)
* Http credentials: Fix behavior for bad password (#5989)
* Credentials: Use per-account keychain entries (#5830)
* Credentials: Fix behavior for bad password (#5989)
* 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)
* 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)
* 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: Better messaging for 507 Insufficient Storage (#5537)
* 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: 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: Don't crash if the db file is readonly (#6050)
* SyncJournalDB: DB close error is not fatal
* Fix at least one memory leak
* 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
* Test improvements
* Small UI layout fixes
* Maintenance Mode: Detect maintenance mode (#4485)
* Maintenance Mode: Add a 1 to 5 min reconnection delay (#5872)
* 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)
* 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: 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)
* OpenSSL: Don't require directly, only via Qt
* Remove iconv dependency, use Qt for file system locale encoding/decoding (emoji filename support on macOS)
* Compilation: Remove Qt 4 code
* Harmonize source code style with clang-format
* Switch over to Qt 5 function pointer signal/slot syntax
* Updater: Rudimentary support for beta channel
* 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) (#5875)
* Compilation: Remove Qt 4 code (#6025, #5702, #5505)
* Harmonize source code style with clang-format (#5732)
* Switch over to Qt 5 function pointer signal/slot syntax (#6041)
* 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)
* 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_MINOR 4 )
set( MIRALL_VERSION_MINOR 5 )
set( MIRALL_VERSION_PATCH 0 )
set( MIRALL_VERSION_YEAR 2017 )
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_KILLING_PROCESSES_TEXT "Terminerer ${APPLICATION_EXECUTABLE}-prosesser."
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_3 "Ikke avinstaller"
StrCpy $PageReinstall_NEW_MUI_HEADER_TEXT_TITLE "Allerede installert"

View file

@ -1,9 +1,9 @@
# Auto-generated - do not modify
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_KILLING_PROCESSES_TEXT "Deteniendo el/los proceso/s ${APPLICATION_EXECUTABLE}."
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_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_3 "No desinstalar"
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 $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_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_DESKTOP_SECTION "Acceso directo 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_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_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}."

View file

@ -603,6 +603,35 @@ Section Uninstall
Abort $UNINSTALL_ABORT
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.
DeleteRegValue HKLM "Software\${APPLICATION_VENDOR}\${APPLICATION_NAME}" "VersionBuild"
DeleteRegValue HKLM "Software\${APPLICATION_VENDOR}\${APPLICATION_NAME}" "VersionMajor"

View file

@ -15,7 +15,7 @@ Configuration File
.. include:: conffile.rst
Environment Variables
------------------
---------------------
.. index:: env vars
.. 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. |
+---------------------------------+---------------+--------------------------------------------------------------------------------------------------------+
| ``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. |
+---------------------------------+---------------+--------------------------------------------------------------------------------------------------------+

View file

@ -1,7 +1,7 @@
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.
@ -11,15 +11,17 @@ continually changes all files, unless you remove
from the windows registry.
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
fifty sub-directories, to help prevent memory problems.
Unfortunately, it, *currently*, does not report an error when this occurs.
However, a UI notification is planned for a future release of ownCloud.
The sync client has been intentionally limited to sync no deeper than 100
sub-directories. The hard limit exists to guard against bugs with cycles
like symbolic link loops.
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.
@ -29,20 +31,20 @@ Specifically, you have to:
1. Remove the existing connection which syncs to the wrong 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
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.
.. 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
If you're sure, click "**Remove connection**".
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
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
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.
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
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
-------------------
@ -58,3 +72,7 @@ synchronizing your files.
Web GUI, and one to open your local ownCloud folder
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
Comment[oc]=@APPLICATION_NAME@ sincronizacion del client
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
** within its configuration management system. ^The SQLITE_SOURCE_ID
** 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()],
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.20.1"
#define SQLITE_VERSION_NUMBER 3020001
#define SQLITE_SOURCE_ID "2017-08-24 16:21:36 8d3a7ea6c5690d6b7c3767558f4f01b511c55463e3f9e64506801fe9b74dce34"
#define SQLITE_VERSION "3.21.0"
#define SQLITE_VERSION_NUMBER 3021000
#define SQLITE_SOURCE_ID "2017-10-24 18:55:49 1a584e499906b5c87ec7d43d4abce641fdf017c42125b083109bc77c4de48827"
/*
** CAPI3REF: Run-Time Library Version Numbers
@ -139,7 +141,7 @@ extern "C" {
**
** <blockquote><pre>
** 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 );
** </pre></blockquote>)^
**
@ -149,9 +151,11 @@ extern "C" {
** function is provided for use in DLLs since DLL users usually do not have
** direct access to string constants within the DLL. ^The
** 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
** [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()].
*/
@ -432,7 +436,7 @@ SQLITE_API int sqlite3_exec(
#define SQLITE_FULL 13 /* Insertion failed because database is full */
#define SQLITE_CANTOPEN 14 /* Unable to open the database file */
#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_TOOBIG 18 /* String or BLOB exceeds size limit */
#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_VNODE (SQLITE_IOERR | (27<<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_BUSY_RECOVERY (SQLITE_BUSY | (1<<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
** read-only media and cannot be changed even by processes with
** 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_ATOMIC512 0x00000002
@ -595,6 +607,7 @@ SQLITE_API int sqlite3_exec(
#define SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN 0x00000800
#define SQLITE_IOCAP_POWERSAFE_OVERWRITE 0x00001000
#define SQLITE_IOCAP_IMMUTABLE 0x00002000
#define SQLITE_IOCAP_BATCH_ATOMIC 0x00004000
/*
** CAPI3REF: File Locking Levels
@ -729,6 +742,7 @@ struct sqlite3_file {
** <li> [SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN]
** <li> [SQLITE_IOCAP_POWERSAFE_OVERWRITE]
** <li> [SQLITE_IOCAP_IMMUTABLE]
** <li> [SQLITE_IOCAP_BATCH_ATOMIC]
** </ul>
**
** 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 RBU extension only. All other VFS should return SQLITE_NOTFOUND for
** 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>
*/
#define SQLITE_FCNTL_LOCKSTATE 1
@ -1043,6 +1091,9 @@ struct sqlite3_io_methods {
#define SQLITE_FCNTL_JOURNAL_POINTER 28
#define SQLITE_FCNTL_WIN32_GET_HANDLE 29
#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 */
#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
** 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>
** <dd> ^The SQLITE_CONFIG_MEMSTATUS option takes single argument of type int,
** interpreted as a boolean, which enables or disables the collection of
@ -1630,25 +1691,7 @@ struct sqlite3_mem_methods {
** </dd>
**
** [[SQLITE_CONFIG_SCRATCH]] <dt>SQLITE_CONFIG_SCRATCH</dt>
** <dd> ^The SQLITE_CONFIG_SCRATCH option specifies a static memory buffer
** 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> The SQLITE_CONFIG_SCRATCH option is no longer used.
** </dd>
**
** [[SQLITE_CONFIG_PAGECACHE]] <dt>SQLITE_CONFIG_PAGECACHE</dt>
@ -1684,8 +1727,7 @@ struct sqlite3_mem_methods {
** [[SQLITE_CONFIG_HEAP]] <dt>SQLITE_CONFIG_HEAP</dt>
** <dd> ^The SQLITE_CONFIG_HEAP option specifies a static memory buffer
** that SQLite will use for all of its dynamic memory allocation needs
** beyond those provided for by [SQLITE_CONFIG_SCRATCH] and
** [SQLITE_CONFIG_PAGECACHE].
** beyond those provided for by [SQLITE_CONFIG_PAGECACHE].
** ^The SQLITE_CONFIG_HEAP option is only available if SQLite is compiled
** with either [SQLITE_ENABLE_MEMSYS3] or [SQLITE_ENABLE_MEMSYS5] and returns
** [SQLITE_ERROR] if invoked otherwise.
@ -1878,7 +1920,7 @@ struct sqlite3_mem_methods {
#define SQLITE_CONFIG_SERIALIZED 3 /* nil */
#define SQLITE_CONFIG_MALLOC 4 /* 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_HEAP 8 /* void*, int nByte, int min */
#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_PMASZ 25 /* unsigned int szPma */
#define SQLITE_CONFIG_STMTJRNL_SPILL 26 /* int nByte */
#define SQLITE_CONFIG_SMALL_MALLOC 27 /* boolean */
/*
** 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
** begins with "file:", then the filename is interpreted as a URI. ^URI
** 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
** [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
** interpretation by default. See "[URI filenames]" for additional
** information.
@ -3776,8 +3819,9 @@ SQLITE_API int sqlite3_stmt_busy(sqlite3_stmt*);
** implementation of [application-defined SQL functions] are protected.
** ^The sqlite3_value object returned by
** [sqlite3_column_value()] is unprotected.
** Unprotected sqlite3_value objects may only be used with
** [sqlite3_result_value()] and [sqlite3_bind_value()].
** Unprotected sqlite3_value objects may only be used as arguments
** to [sqlite3_result_value()], [sqlite3_bind_value()], and
** [sqlite3_value_dup()].
** The [sqlite3_value_blob | sqlite3_value_type()] family of
** 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
** sqlite3_step(). Failure to reset the prepared statement using
** [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
** calling [sqlite3_reset()] automatically in this circumstance rather
** 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
** a query that uses a [virtual table].
*/
#define SQLITE_INDEX_CONSTRAINT_EQ 2
#define SQLITE_INDEX_CONSTRAINT_GT 4
#define SQLITE_INDEX_CONSTRAINT_LE 8
#define SQLITE_INDEX_CONSTRAINT_LT 16
#define SQLITE_INDEX_CONSTRAINT_GE 32
#define SQLITE_INDEX_CONSTRAINT_MATCH 64
#define SQLITE_INDEX_CONSTRAINT_LIKE 65
#define SQLITE_INDEX_CONSTRAINT_GLOB 66
#define SQLITE_INDEX_CONSTRAINT_REGEXP 67
#define SQLITE_INDEX_CONSTRAINT_EQ 2
#define SQLITE_INDEX_CONSTRAINT_GT 4
#define SQLITE_INDEX_CONSTRAINT_LE 8
#define SQLITE_INDEX_CONSTRAINT_LT 16
#define SQLITE_INDEX_CONSTRAINT_GE 32
#define SQLITE_INDEX_CONSTRAINT_MATCH 64
#define SQLITE_INDEX_CONSTRAINT_LIKE 65
#define SQLITE_INDEX_CONSTRAINT_GLOB 66
#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
@ -6963,7 +7012,7 @@ SQLITE_API int sqlite3_test_control(int op, ...);
#define SQLITE_TESTCTRL_RESERVE 14
#define SQLITE_TESTCTRL_OPTIMIZATIONS 15
#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_EXPLAIN_STMT 19 /* NOT USED */
#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
** using [sqlite3_malloc()], either directly or indirectly. The
** figure includes calls made to [sqlite3_malloc()] by the application
** and internal memory usage by the SQLite library. Scratch memory
** controlled by [SQLITE_CONFIG_SCRATCH] and auxiliary page-cache
** and internal memory usage by the SQLite library. Auxiliary page-cache
** memory controlled by [SQLITE_CONFIG_PAGECACHE] is not included in
** this parameter. The amount returned is the sum of the allocation
** 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.
** The value written into the *pCurrent parameter is undefined.</dd>)^
**
** [[SQLITE_STATUS_SCRATCH_USED]] ^(<dt>SQLITE_STATUS_SCRATCH_USED</dt>
** <dd>This parameter returns the number of allocations used out of the
** [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_USED]] <dt>SQLITE_STATUS_SCRATCH_USED</dt>
** <dd>No longer used.</dd>
**
** [[SQLITE_STATUS_SCRATCH_OVERFLOW]] ^(<dt>SQLITE_STATUS_SCRATCH_OVERFLOW</dt>
** <dd>This parameter returns the number of bytes of scratch memory
** 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>)^
** <dd>No longer used.</dd>
**
** [[SQLITE_STATUS_SCRATCH_SIZE]] ^(<dt>SQLITE_STATUS_SCRATCH_SIZE</dt>
** <dd>This parameter records the largest memory allocation request
** 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_SCRATCH_SIZE]] <dt>SQLITE_STATUS_SCRATCH_SIZE</dt>
** <dd>No longer used.</dd>
**
** [[SQLITE_STATUS_PARSER_STACK]] ^(<dt>SQLITE_STATUS_PARSER_STACK</dt>
** <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_PAGECACHE_USED 1
#define SQLITE_STATUS_PAGECACHE_OVERFLOW 2
#define SQLITE_STATUS_SCRATCH_USED 3
#define SQLITE_STATUS_SCRATCH_OVERFLOW 4
#define SQLITE_STATUS_SCRATCH_USED 3 /* NOT USED */
#define SQLITE_STATUS_SCRATCH_OVERFLOW 4 /* NOT USED */
#define SQLITE_STATUS_MALLOC_SIZE 5
#define SQLITE_STATUS_PARSER_STACK 6
#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
/*
@ -9198,8 +9231,8 @@ SQLITE_API int sqlite3session_diff(
*/
SQLITE_API int sqlite3session_patchset(
sqlite3_session *pSession, /* Session object */
int *pnPatchset, /* OUT: Size of buffer at *ppChangeset */
void **ppPatchset /* OUT: Buffer containing changeset */
int *pnPatchset, /* OUT: Size of buffer at *ppPatchset */
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">
** <tr><th>Streaming function<th>Non-streaming equivalent</th>
** <tr><td>sqlite3changeset_apply_str<td>[sqlite3changeset_apply]
** <tr><td>sqlite3changeset_concat_str<td>[sqlite3changeset_concat]
** <tr><td>sqlite3changeset_invert_str<td>[sqlite3changeset_invert]
** <tr><td>sqlite3changeset_start_str<td>[sqlite3changeset_start]
** <tr><td>sqlite3session_changeset_str<td>[sqlite3session_changeset]
** <tr><td>sqlite3session_patchset_str<td>[sqlite3session_patchset]
** <tr><td>sqlite3changeset_apply_strm<td>[sqlite3changeset_apply]
** <tr><td>sqlite3changeset_concat_strm<td>[sqlite3changeset_concat]
** <tr><td>sqlite3changeset_invert_strm<td>[sqlite3changeset_invert]
** <tr><td>sqlite3changeset_start_strm<td>[sqlite3changeset_start]
** <tr><td>sqlite3session_changeset_strm<td>[sqlite3session_changeset]
** <tr><td>sqlite3session_patchset_strm<td>[sqlite3session_patchset]
** </table>
**
** Non-streaming functions that accept changesets (or patchsets) as input

View file

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

View file

@ -90,6 +90,21 @@ QByteArray makeChecksumHeader(const QByteArray &checksumType, const QByteArray &
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)
{
if (header.isEmpty()) {
@ -120,7 +135,7 @@ QByteArray parseChecksumHeaderType(const QByteArray &header)
bool uploadChecksumEnabled()
{
static bool enabled = qgetenv("OWNCLOUD_DISABLE_CHECKSUM_UPLOAD").isEmpty();
static bool enabled = qEnvironmentVariableIsEmpty("OWNCLOUD_DISABLE_CHECKSUM_UPLOAD");
return enabled;
}
@ -133,6 +148,12 @@ QByteArray contentChecksumType()
return type;
}
static bool checksumComputationEnabled()
{
static bool enabled = qgetenv("OWNCLOUD_DISABLE_CHECKSUM_COMPUTATIONS").isEmpty();
return enabled;
}
ComputeChecksum::ComputeChecksum(QObject *parent)
: QObject(parent)
{
@ -150,6 +171,8 @@ QByteArray ComputeChecksum::checksumType() const
void ComputeChecksum::start(const QString &filePath)
{
qCInfo(lcChecksums) << "Computing" << checksumType() << "checksum of" << filePath << "in a thread";
// Calculate the checksum in a different thread first.
connect(&_watcher, &QFutureWatcherBase::finished,
this, &ComputeChecksum::slotCalculationDone,
@ -159,6 +182,11 @@ void ComputeChecksum::start(const QString &filePath)
QByteArray ComputeChecksum::computeNow(const QString &filePath, const QByteArray &checksumType)
{
if (!checksumComputationEnabled()) {
qCWarning(lcChecksums) << "Checksum computation disabled by environment variable";
return QByteArray();
}
if (checksumType == checkSumMD5C) {
return FileSystem::calcMd5(filePath);
} else if (checksumType == checkSumSHA1C) {
@ -237,6 +265,7 @@ QByteArray CSyncChecksumHook::hook(const QByteArray &path, const QByteArray &oth
if (type.isEmpty())
return NULL;
qCInfo(lcChecksums) << "Computing" << type << "checksum of" << path << "in the csync hook";
QByteArray checksum = ComputeChecksum::computeNow(QString::fromUtf8(path), type);
if (checksum.isNull()) {
qCWarning(lcChecksums) << "Failed to compute checksum" << type << "for" << path;

View file

@ -36,6 +36,16 @@ static const char checkSumAdlerC[] = "Adler32";
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.
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
static QByteArray readToCrypto(const QString &filename, QCryptographicHash::Algorithm algo)
{
QFile file(filename);
const qint64 bufSize = qMin(BUFSIZE, file.size() + 1);
QByteArray buf(bufSize, Qt::Uninitialized);
QByteArray arr;
QCryptographicHash crypto(algo);
static QByteArray readToCrypto( const QString& filename, QCryptographicHash::Algorithm algo )
{
QFile file(filename);
QByteArray arr;
QCryptographicHash crypto( algo );
if (file.open(QIODevice::ReadOnly)) {
qint64 size;
while (!file.atEnd()) {
size = file.read(buf.data(), bufSize);
if (size > 0) {
crypto.addData(buf.data(), size);
}
}
arr = crypto.result().toHex();
}
return arr;
}
if (file.open(QIODevice::ReadOnly)) {
if (crypto.addData(&file)) {
arr = crypto.result().toHex();
}
}
return arr;
}
QByteArray FileSystem::calcMd5(const QString &filename)
{

View file

@ -85,24 +85,32 @@ bool SqlDatabase::openHelper(const QString &filename, int sqliteFlags)
return true;
}
bool SqlDatabase::checkDb()
SqlDatabase::CheckDbResult SqlDatabase::checkDb()
{
// quick_check can fail with a disk IO error when diskspace is low
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()) {
qCWarning(lcSql) << "Error running quick_check on database";
return false;
_errId = quick_check.errorId();
_error = quick_check.error();
return CheckDbResult::CantExec;
}
quick_check.next();
QString result = quick_check.stringValue(0);
if (result != "ok") {
qCWarning(lcSql) << "quick_check returned failure:" << result;
return false;
return CheckDbResult::NotOk;
}
return true;
return CheckDbResult::Ok;
}
bool SqlDatabase::openOrCreateReadWrite(const QString &filename)
@ -115,13 +123,25 @@ bool SqlDatabase::openOrCreateReadWrite(const QString &filename)
return false;
}
if (!checkDb()) {
// When disk space is low, checking the db may fail even though it's fine.
qint64 freeSpace = Utility::freeDiskSpace(QFileInfo(filename).dir().absolutePath());
if (freeSpace != -1 && freeSpace < 1000000) {
qCWarning(lcSql) << "Consistency check failed, disk space is low, aborting" << freeSpace;
close();
return false;
auto checkResult = checkDb();
if (checkResult != CheckDbResult::Ok) {
if (checkResult == CheckDbResult::CantPrepare) {
// When disk space is low, preparing may fail even though the db is fine.
// Typically CANTOPEN or IOERR.
qint64 freeSpace = Utility::freeDiskSpace(QFileInfo(filename).dir().absolutePath());
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;
@ -144,7 +164,7 @@ bool SqlDatabase::openReadOnly(const QString &filename)
return false;
}
if (!checkDb()) {
if (checkDb() != CheckDbResult::Ok) {
qCWarning(lcSql) << "Consistency check failed in readonly mode, giving up" << filename;
close();
return false;
@ -164,8 +184,8 @@ void SqlDatabase::close()
{
if (_db) {
SQLITE_DO(sqlite3_close(_db));
// Fatal because reopening an unclosed db might be problematic.
ENFORCE(_errId == SQLITE_OK, "Error when closing DB");
if (_errId != SQLITE_OK)
qCWarning(lcSql) << "Closing database failed" << _error;
_db = 0;
}
}

View file

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

View file

@ -547,13 +547,28 @@ bool SyncJournalDb::checkConnect()
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));
if (_getFilesBelowPathQuery->prepare(
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);
}
_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));
if (_setFileRecordQuery->prepare("INSERT OR REPLACE INTO metadata "
"(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);
_getFileRecordQueryByFileId.reset(0);
_getFilesBelowPathQuery.reset(0);
_getAllFilesQuery.reset(0);
_setFileRecordQuery.reset(0);
_setFileRecordChecksumQuery.reset(0);
_setFileRecordLocalMetadataQuery.reset(0);
@ -1094,15 +1110,10 @@ bool SyncJournalDb::getFileRecordByInode(quint64 inode, SyncJournalFileRecord *r
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);
// 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)
return true; // no error, yet nothing found (rec->isValid() == false)
@ -1116,8 +1127,10 @@ bool SyncJournalDb::getFileRecordByFileId(const QByteArray &fileId, SyncJournalF
return false;
}
if (_getFileRecordQueryByFileId->next()) {
fillFileRecordFromGetQuery(*rec, *_getFileRecordQueryByFileId);
while (_getFileRecordQueryByFileId->next()) {
SyncJournalFileRecord rec;
fillFileRecordFromGetQuery(rec, *_getFileRecordQueryByFileId);
rowCallback(rec);
}
return true;
@ -1133,16 +1146,23 @@ bool SyncJournalDb::getFilesBelowPath(const QByteArray &path, const std::functio
if (!checkConnect())
return false;
_getFilesBelowPathQuery->reset_and_clear_bindings();
_getFilesBelowPathQuery->bindValue(1, path);
// Since the path column doesn't store the starting /, the getFilesBelowPathQuery
// 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;
}
while (_getFilesBelowPathQuery->next()) {
while (query->next()) {
SyncJournalFileRecord rec;
fillFileRecordFromGetQuery(rec, *_getFilesBelowPathQuery);
fillFileRecordFromGetQuery(rec, *query);
rowCallback(rec);
}

View file

@ -58,7 +58,7 @@ public:
bool getFileRecord(const QString &filename, SyncJournalFileRecord *rec) { return getFileRecord(filename.toUtf8(), rec); }
bool getFileRecord(const QByteArray &filename, 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 setFileRecord(const SyncJournalFileRecord &record);
@ -244,6 +244,7 @@ private:
QScopedPointer<SqlQuery> _getFileRecordQueryByInode;
QScopedPointer<SqlQuery> _getFileRecordQueryByFileId;
QScopedPointer<SqlQuery> _getFilesBelowPathQuery;
QScopedPointer<SqlQuery> _getAllFilesQuery;
QScopedPointer<SqlQuery> _setFileRecordQuery;
QScopedPointer<SqlQuery> _setFileRecordChecksumQuery;
QScopedPointer<SqlQuery> _setFileRecordLocalMetadataQuery;

View file

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

View file

@ -28,8 +28,13 @@
#include <QLoggingCategory>
#include <QMap>
#include <QUrl>
#include <functional>
#include <memory>
#ifdef Q_OS_WIN
#include <windows.h>
#endif
class QSettings;
namespace OCC {
@ -187,6 +192,14 @@ namespace Utility {
* Experimental! Real feature planned for 2.5.
*/
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

View file

@ -16,8 +16,7 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#define _WIN32_WINNT 0x0600
#define WINVER 0x0600
#include "asserts.h"
#include <shlobj.h>
#include <winbase.h>
#include <windows.h>
@ -93,4 +92,168 @@ static inline bool hasDarkSystray_private()
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

View file

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

View file

@ -38,6 +38,7 @@
#include <stdbool.h>
#include <sqlite3.h>
#include <map>
#include <set>
#include "common/syncjournaldb.h"
#include "config_csync.h"
@ -70,6 +71,11 @@ enum csync_replica_e {
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
@ -190,6 +196,16 @@ struct OCSYNC_EXPORT csync_s {
*/
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;
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.
@ -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.
*/
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;
/* we need the opposite tree! */
switch (ctx->current) {
case LOCAL_REPLICA:
our_tree = &ctx->local.files;
other_tree = &ctx->remote.files;
break;
case REMOTE_REPLICA:
our_tree = &ctx->remote.files;
other_tree = &ctx->local.files;
break;
default:
@ -152,40 +143,51 @@ static int _csync_merge_algorithm_visitor(csync_file_stat_t *cur, CSYNC * ctx) {
cur->instruction = CSYNC_INSTRUCTION_REMOVE;
break;
case CSYNC_INSTRUCTION_EVAL_RENAME: {
OCC::SyncJournalFileRecord base;
if(ctx->current == LOCAL_REPLICA ) {
/* use the old name to find the "other" node */
ctx->statedb->getFileRecordByInode(cur->inode, &base);
qCDebug(lcReconcile, "Finding opposite temp through inode %" PRIu64 ": %s",
cur->inode, base.isValid() ? "true":"false");
} else {
ASSERT( ctx->current == REMOTE_REPLICA );
ctx->statedb->getFileRecordByFileId(cur->file_id, &base);
qCDebug(lcReconcile, "Finding opposite temp through file ID %s: %s",
cur->file_id.constData(), base.isValid() ? "true":"false");
}
// By default, the EVAL_RENAME decays into a NEW
cur->instruction = CSYNC_INSTRUCTION_NEW;
bool processedRename = false;
auto renameCandidateProcessing = [&](const OCC::SyncJournalFileRecord &base) {
if (processedRename)
return;
if (!base.isValid())
return;
if( base.isValid() ) {
/* 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)) {
qCDebug(lcReconcile, "Origin found in our tree : %s", base._path.constData());
if (our_tree->findFile(base._path)) {
qCDebug(lcReconcile, "Origin found in our tree : %s", base._path.constData());
} 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
* is not longer existing there, maybe because it was renamed or deleted.
* The journal is cleaned up later after propagation.
*/
other = other_tree->findFile(base._path);
qCDebug(lcReconcile, "Temporary opposite (%s) %s",
base._path.constData() , other ? "found": "not found" );
qCDebug(lcReconcile, "Rename origin in other tree (%s) %s",
base._path.constData(), other ? "found" : "not found");
}
if(!other) {
cur->instruction = CSYNC_INSTRUCTION_NEW;
} else if (other->instruction == CSYNC_INSTRUCTION_NONE
|| other->instruction == CSYNC_INSTRUCTION_UPDATE_METADATA
|| cur->type == CSYNC_FTW_TYPE_DIR) {
// Stick with the NEW
return;
} else if (other->instruction == CSYNC_INSTRUCTION_RENAME) {
// 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->rename_path = cur->path;
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;
cur->instruction = CSYNC_INSTRUCTION_NONE;
} else if (other->instruction == CSYNC_INSTRUCTION_REMOVE) {
other->instruction = CSYNC_INSTRUCTION_RENAME;
other->rename_path = cur->path;
// We have consumed 'other': exit this loop to not consume another one.
processedRename = true;
} 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() ) {
other->file_id = cur->file_id;
}
other->inode = cur->inode;
// Local: The remote reconcile will be able to deal with this.
// Remote: The local replica has already dealt with this.
// See the EVAL_RENAME case when other was found directly.
qCDebug(lcReconcile, "File in a renamed directory, other side's instruction: %d",
other->instruction);
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 {
assert(other->type != CSYNC_FTW_TYPE_DIR);
cur->instruction = CSYNC_INSTRUCTION_NONE;
other->instruction = CSYNC_INSTRUCTION_SYNC;
// This can, for instance, happen when there was a local change in other
// and the instruction in the local tree is NEW while cur has EVAL_RENAME
// 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;
}
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
// 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
// will compare hashes and avoid the download if they are equal.
const char *remoteChecksumHeader =
// will compare hashes and avoid the download if possible.
QByteArray remoteChecksumHeader =
(ctx->current == REMOTE_REPLICA ? cur->checksumHeader : other->checksumHeader);
if (remoteChecksumHeader) {
is_conflict |= _csync_is_collision_safe_hash(remoteChecksumHeader);
if (!remoteChecksumHeader.isEmpty()) {
is_conflict = true;
}
// 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;
case CSYNC_INSTRUCTION_IGNORE:
cur->instruction = CSYNC_INSTRUCTION_IGNORE;
break;
break;
default:
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:
break;
}

View file

@ -297,41 +297,60 @@ static int _csync_detect_update(CSYNC *ctx, std::unique_ptr<csync_file_stat_t> f
} else {
/* Remote Replica Rename check */
OCC::SyncJournalFileRecord base;
if(!ctx->statedb->getFileRecordByFileId(fs->file_id, &base)) {
fs->instruction = CSYNC_INSTRUCTION_NEW;
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;
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 {
/* file not found in statedb */
fs->instruction = CSYNC_INSTRUCTION_NEW;
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)) {
return 1;
}
if (fs->instruction == CSYNC_INSTRUCTION_NEW
&& 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)) {
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;
QByteArray skipbase;
auto rowCallback = [ctx, &count, &skipbase](const OCC::SyncJournalFileRecord &rec) {
/* When selective sync is used, the database may have subtrees with a parent
* whose etag (md5) is _invalid_. These are ignored and shall not appear in the
* remote tree.
* 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
* their correct etags again and we don't run into this case.
*/
if( rec._etag == "_invalid_") {
qCDebug(lcUpdate, "%s selective sync excluded", rec._path.constData());
skipbase = rec._path;
skipbase += '/';
return;
}
auto &files = ctx->current == LOCAL_REPLICA ? ctx->local.files : ctx->remote.files;
auto rowCallback = [ctx, &count, &skipbase, &files](const OCC::SyncJournalFileRecord &rec) {
if (ctx->current == REMOTE_REPLICA) {
/* When selective sync is used, the database may have subtrees with a parent
* whose etag is _invalid_. These are ignored and shall not appear in the
* remote tree.
* 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
* their correct etags again and we don't run into this case.
*/
if (rec._etag == "_invalid_") {
qCDebug(lcUpdate, "%s selective sync excluded", rec._path.constData());
skipbase = rec._path;
skipbase += '/';
return;
}
/* Skip over all entries with the same base path. Note that this depends
* strongly on the ordering of the retrieved items. */
if( !skipbase.isEmpty() && rec._path.startsWith(skipbase) ) {
qCDebug(lcUpdate, "%s selective sync excluded because the parent is", rec._path.constData());
return;
} else {
skipbase.clear();
/* Skip over all entries with the same base path. Note that this depends
* strongly on the ordering of the retrieved items. */
if (!skipbase.isEmpty() && rec._path.startsWith(skipbase)) {
qCDebug(lcUpdate, "%s selective sync excluded because the parent is", rec._path.constData());
return;
} else {
skipbase.clear();
}
}
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. */
ctx->remote.files[rec._path] = std::move(st);
files[rec._path] = std::move(st);
++count;
};
@ -522,6 +544,26 @@ int csync_ftw(CSYNC *ctx, const char *uri, csync_walker_fn fn,
int rc = 0;
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) {
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
// database.
if( do_read_from_db ) {
if( ! fill_tree_from_db(ctx, uri) ) {
if( ! fill_tree_from_db(ctx, db_uri) ) {
errno = ENOENT;
ctx->status_code = CSYNC_STATUS_OPENDIR_ERROR;
goto error;
@ -616,7 +658,7 @@ int csync_ftw(CSYNC *ctx, const char *uri, csync_walker_fn fn,
* local stat function.
*/
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;
}
}

View file

@ -211,3 +211,10 @@ time_t oc_httpdate_parse( const char *date ) {
result = timegm(&gmt);
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);
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 */

View file

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

View file

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

View file

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

View file

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

View file

@ -43,7 +43,7 @@ public:
UserAgentWebPage(QObject *parent)
: QWebPage(parent)
{
if (!qgetenv("OWNCLOUD_SHIBBOLETH_DEBUG").isEmpty()) {
if (!qEnvironmentVariableIsEmpty("OWNCLOUD_SHIBBOLETH_DEBUG")) {
settings()->setAttribute(QWebSettings::DeveloperExtrasEnabled, true);
}
}
@ -83,7 +83,7 @@ ShibbolethWebView::ShibbolethWebView(AccountPtr account, QWidget *parent)
setWindowTitle(tr("%1 - Authenticate").arg(Theme::instance()->appNameGUI()));
// 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
QWebPage *debugPage = new UserAgentWebPage(this);
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)
{
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
// a sync operation, both for actual user files and the database
// and log. Therefore we check notifications against operations
// the sync is doing to filter out our own changes.
#ifdef Q_OS_MAC
Q_UNUSED(path)
// On OSX the folder watcher does not report changes done by our
// own process. Therefore nothing needs to be done here!
#else
@ -465,15 +479,12 @@ void Folder::slotWatchedPathChanged(const QString &path)
#endif
// Check that the mtime actually changed.
if (path.startsWith(this->path())) {
auto relativePath = path.mid(this->path().size());
SyncJournalFileRecord record;
if (_journal.getFileRecord(relativePath, &record)
&& record.isValid()
&& !FileSystem::fileChanged(path, record._fileSize, record._modtime)) {
qCInfo(lcFolder) << "Ignoring spurious notification for file" << relativePath;
return; // probably a spurious notification
}
SyncJournalFileRecord record;
if (_journal.getFileRecord(relativePathBytes, &record)
&& record.isValid()
&& !FileSystem::fileChanged(path, record._fileSize, record._modtime)) {
qCInfo(lcFolder) << "Ignoring spurious notification for file" << relativePath;
return; // probably a spurious notification
}
emit watchedFileChangedExternally(path);
@ -645,6 +656,36 @@ void Folder::startSync(const QStringList &pathList)
setDirtyNetworkLimits();
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);
QMetaObject::invokeMethod(_engine.data(), "startSync", Qt::QueuedConnection);
@ -783,6 +824,24 @@ void Folder::slotSyncFinished(bool success)
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();
// 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
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
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) {
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);
@ -901,6 +986,11 @@ void Folder::slotScheduleThisFolder()
FolderMan::instance()->scheduleFolder(this);
}
void Folder::slotNextSyncFullLocalDiscovery()
{
_timeSinceLastFullLocalDiscovery.invalidate();
}
void Folder::scheduleThisFolderSoon()
{
if (!_scheduleSelfTimer.isActive()) {
@ -913,6 +1003,20 @@ void Folder::setSaveBackwardsCompatible(bool 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)
{
ConfigFile cfgFile;
@ -976,6 +1080,12 @@ void FolderDefinition::save(QSettings &settings, const FolderDefinition &folder)
settings.setValue(QLatin1String("targetPath"), folder.targetPath);
settings.setValue(QLatin1String("paused"), folder.paused);
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();
}
@ -989,6 +1099,7 @@ bool FolderDefinition::load(QSettings &settings, const QString &alias,
folder->targetPath = settings.value(QLatin1String("targetPath")).toString();
folder->paused = settings.value(QLatin1String("paused")).toBool();
folder->ignoreHiddenFiles = settings.value(QLatin1String("ignoreHiddenFiles"), QVariant(true)).toBool();
folder->navigationPaneClsid = settings.value(QLatin1String("navigationPaneClsid")).toUuid();
settings.endGroup();
// Old settings can contain paths with native separators. In the rest of the

View file

@ -27,6 +27,8 @@
#include <QObject>
#include <QStringList>
#include <QUuid>
#include <set>
class QThread;
class QSettings;
@ -36,6 +38,7 @@ namespace OCC {
class SyncEngine;
class AccountState;
class SyncRunFileLog;
class FolderWatcher;
/**
* @brief The FolderDefinition class
@ -62,6 +65,8 @@ public:
bool paused;
/// whether the folder syncs hidden files
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.
static void save(QSettings &settings, const FolderDefinition &folder);
@ -133,6 +138,9 @@ public:
*/
QString remotePath() const;
void setNavigationPaneClsid(const QUuid &clsid) { _definition.navigationPaneClsid = clsid; }
QUuid navigationPaneClsid() const { return _definition.navigationPaneClsid; }
/**
* remote folder path with server url
*/
@ -227,6 +235,13 @@ public:
*/
void setSaveBackwardsCompatible(bool save);
/**
* Sets up this folder's folderWatcher if possible.
*
* May be called several times.
*/
void registerFolderWatcher();
signals:
void syncStateChange();
void syncStarted();
@ -304,6 +319,9 @@ private slots:
*/
void slotScheduleThisFolder();
/** Ensures that the next sync performs a full local discovery. */
void slotNextSyncFullLocalDiscovery();
private:
bool setIgnoredFiles();
@ -338,6 +356,7 @@ private:
QString _lastEtag;
QElapsedTimer _timeSinceLastSyncDone;
QElapsedTimer _timeSinceLastSyncStart;
QElapsedTimer _timeSinceLastFullLocalDiscovery;
qint64 _lastSyncDuration;
/// The number of syncs that failed in a row.
@ -365,6 +384,29 @@ private:
* path.
*/
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)
, _syncEnabled(true)
, _lockWatcher(new LockWatcher)
, _navigationPaneHelper(this)
, _appRestartRequired(false)
{
ASSERT(!_instance);
@ -104,9 +105,6 @@ void FolderMan::unloadFolder(Folder *f)
_socketApi->slotUnregisterPath(f->alias());
if (_folderWatchers.contains(f->alias())) {
_folderWatchers.remove(f->alias());
}
_folderMap.remove(f->alias());
disconnect(f, &Folder::syncStarted,
@ -147,54 +145,18 @@ int FolderMan::unloadAndDeleteAllFolders()
return cnt;
}
// add a monitor to the local file system. If there is a change in the
// file system, the method slotFolderMonitorFired is triggered through
// the SignalMapper
void FolderMan::registerFolderMonitor(Folder *folder)
void FolderMan::registerFolderWithSocketApi(Folder *folder)
{
if (!folder)
return;
if (!QDir(folder->path()).exists())
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
if (folder->canSync())
_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()
{
unloadAndDeleteAllFolders();
@ -750,7 +712,8 @@ void FolderMan::slotStartScheduledFolderSync()
if (folder) {
// Safe to call several times, and necessary to try again if
// the folder path didn't exist previously.
registerFolderMonitor(folder);
folder->registerFolderWatcher();
registerFolderWithSocketApi(folder);
_currentSyncFolder = folder;
folder->startSync(QStringList());
@ -932,6 +895,9 @@ Folder *FolderMan::addFolder(AccountState *accountState, const FolderDefinition
emit folderSyncStateChange(folder);
emit folderListChanged(_folderMap);
}
_navigationPaneHelper.scheduleUpdateCloudStorageRegistry();
return folder;
}
@ -964,7 +930,8 @@ Folder *FolderMan::addFolderInternal(FolderDefinition folderDefinition,
connect(folder, &Folder::watchedFileChangedExternally,
&folder->syncEngine().syncFileStatusTracker(), &SyncFileStatusTracker::slotPathTouched);
registerFolderMonitor(folder);
folder->registerFolderWatcher();
registerFolderWithSocketApi(folder);
return folder;
}
@ -1040,6 +1007,8 @@ void FolderMan::removeFolder(Folder *f)
delete f;
}
_navigationPaneHelper.scheduleUpdateCloudStorageRegistry();
emit folderListChanged(_folderMap);
}

View file

@ -22,6 +22,7 @@
#include "folder.h"
#include "folderwatcher.h"
#include "navigationpanehelper.h"
#include "syncfileitem.h"
class TestFolderMan;
@ -109,15 +110,13 @@ public:
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
// system, thus need to be escaped.
static QString escapeAlias(const QString &);
static QString unescapeAlias(const QString &);
SocketApi *socketApi();
NavigationPaneHelper &navigationPaneHelper() { return _navigationPaneHelper; }
/**
* 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
// and create the folders
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)
void restartApplication();
@ -298,9 +299,6 @@ private:
QPointer<Folder> _lastSyncFolder;
bool _syncEnabled;
/// Watching for file changes in folders
QMap<QString, FolderWatcher *> _folderWatchers;
/// Starts regular etag query jobs
QTimer _etagPollTimer;
/// The currently running etag query
@ -319,6 +317,7 @@ private:
QTimer _startScheduledSyncTimer;
QScopedPointer<SocketApi> _socketApi;
NavigationPaneHelper _navigationPaneHelper;
bool _appRestartRequired;

View file

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

View file

@ -72,13 +72,29 @@ public:
/* Check if the path is ignored. */
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:
/** Emitted when one of the watched directories or one
* of the contained files is changed. */
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:
// called from the implementations to indicate a change in path
@ -93,6 +109,7 @@ private:
QTime _timer;
QSet<QString> _lastPaths;
Folder *_folder;
bool _isReliable = true;
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);
if (wd > -1) {
_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();
if (errorCode == ERROR_NOTIFY_ENUM_DIR) {
qCDebug(lcFolderWatcher) << "The buffer for changes overflowed! Triggering a generic change and resizing";
emit lostChanges();
emit changed(_path);
*increaseBufferSize = true;
} else {
@ -199,6 +200,8 @@ FolderWatcherPrivate::FolderWatcherPrivate(FolderWatcher *p, const QString &path
_thread = new WatcherThread(path);
connect(_thread, SIGNAL(changed(const QString &)),
_parent, SLOT(changeDetected(const QString &)));
connect(_thread, SIGNAL(lostChanges()),
_parent, SIGNAL(lostChanges()));
_thread->start();
}

View file

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

View file

@ -44,6 +44,7 @@ GeneralSettings::GeneralSettings(QWidget *parent)
connect(_ui->desktopNotificationsCheckBox, &QAbstractButton::toggled,
this, &GeneralSettings::slotToggleOptionalDesktopNotifications);
connect(_ui->showInExplorerNavigationPaneCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::slotShowInExplorerNavigationPane);
_ui->autostartCheckBox->setChecked(Utility::hasLaunchOnStartup(Theme::instance()->appName()));
connect(_ui->autostartCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::slotToggleLaunchOnStartup);
@ -73,6 +74,13 @@ GeneralSettings::GeneralSettings(QWidget *parent)
_ui->crashreporterCheckBox->setVisible(false);
#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
* align properly vertically , fixes bug #3758
*/
@ -107,6 +115,7 @@ void GeneralSettings::loadMiscSettings()
ConfigFile cfgFile;
_ui->monoIconsCheckBox->setChecked(cfgFile.monoIcons());
_ui->desktopNotificationsCheckBox->setChecked(cfgFile.optionalDesktopNotifications());
_ui->showInExplorerNavigationPaneCheckBox->setChecked(cfgFile.showInExplorerNavigationPane());
_ui->crashreporterCheckBox->setChecked(cfgFile.crashReporter());
auto newFolderLimit = cfgFile.newBigFolderSizeLimit();
_ui->newFolderLimitCheckBox->setChecked(newFolderLimit.first);
@ -162,6 +171,14 @@ void GeneralSettings::slotToggleOptionalDesktopNotifications(bool 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()
{
if (_ignoreEditor.isNull()) {

View file

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

View file

@ -218,6 +218,17 @@
</item>
</layout>
</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>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>

View file

@ -107,6 +107,16 @@ void IssuesWidget::showEvent(QShowEvent *ev)
{
ConfigFile cfg;
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);
}
@ -119,6 +129,8 @@ void IssuesWidget::hideEvent(QHideEvent *ev)
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
// with ignored files and propagation errors.
int itemCnt = _ui->_treeWidget->topLevelItemCount();
@ -129,6 +141,9 @@ void IssuesWidget::cleanItems(const QString &folder)
delete item;
}
}
_ui->_treeWidget->setSortingEnabled(true);
// update the tabtext
emit(issueCountUpdated(_ui->_treeWidget->topLevelItemCount()));
}
@ -240,7 +255,7 @@ bool IssuesWidget::shouldBeVisible(QTreeWidgetItem *item, AccountState *filterAc
const QString &filterFolderAlias) const
{
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->showWarnings->isChecked()
|| (status != SyncFileItem::SoftError
@ -368,13 +383,14 @@ void IssuesWidget::addError(const QString &folderAlias, const QString &message,
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::UserRole, timestamp);
twitem->setIcon(0, icon);
twitem->setToolTip(0, longTimeStr);
twitem->setToolTip(3, message);
twitem->setData(0, Qt::UserRole, SyncFileItem::NormalError);
twitem->setData(2, Qt::UserRole, folderAlias);
twitem->setToolTip(3, message);
twitem->setData(3, Qt::UserRole, SyncFileItem::NormalError);
addItem(twitem);
addErrorWidget(twitem, message, category);

View file

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

View file

@ -88,7 +88,7 @@ int main(int argc, char **argv)
// check a environment variable for core dumps
#ifdef Q_OS_UNIX
if (!qgetenv("OWNCLOUD_CORE_DUMP").isEmpty()) {
if (!qEnvironmentVariableIsEmpty("OWNCLOUD_CORE_DUMP")) {
struct rlimit core_limit;
core_limit.rlim_cur = 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);
}
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();
connect(action, &QAction::triggered, this, [this, alias] { this->slotFolderOpenAction(alias); });
}

View file

@ -18,6 +18,7 @@
#include <QProcess>
#include <QMessageBox>
#include <QDesktopServices>
#include <QApplication>
#include "wizard/owncloudwizardcommon.h"
#include "wizard/owncloudwizard.h"
@ -45,7 +46,7 @@ OwncloudSetupWizard::OwncloudSetupWizard(QObject *parent)
, _remoteFolder()
{
connect(_ocWizard, &OwncloudWizard::determineAuthType,
this, &OwncloudSetupWizard::slotDetermineAuthType);
this, &OwncloudSetupWizard::slotCheckServer);
connect(_ocWizard, &OwncloudWizard::connectToOCUrl,
this, &OwncloudSetupWizard::slotConnectToOCUrl);
connect(_ocWizard, &OwncloudWizard::createLocalAndRemoteFolders,
@ -69,6 +70,7 @@ static QPointer<OwncloudSetupWizard> wiz = 0;
void OwncloudSetupWizard::runWizard(QObject *obj, const char *amember, QWidget *parent)
{
if (!wiz.isNull()) {
bringWizardToFrontIfVisible();
return;
}
@ -84,6 +86,17 @@ bool OwncloudSetupWizard::bringWizardToFrontIfVisible()
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);
return true;
}
@ -127,7 +140,7 @@ void OwncloudSetupWizard::startWizard()
}
// 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;
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)
account->networkAccessManager()->setProxy(QNetworkProxy(QNetworkProxy::DefaultProxy));
// 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();
account->networkAccessManager()->setProxy(proxy);
slotContinueDetermineAuth();
slotFindServer();
}
void OwncloudSetupWizard::slotContinueDetermineAuth()
void OwncloudSetupWizard::slotFindServer()
{
AccountPtr account = _ocWizard->account();
// Set fake credentials before we check what credential it actually is.
account->setCredentials(CredentialsFactory::create("dummy"));
// Before we check the auth type, resolve any permanent redirect
// chain there might be. We cannot do this only on url/status.php
// in CheckServerJob, because things like url shorteners don't
// redirect subpaths.
// Determining the actual server URL can be a multi-stage process
// 1. Check url/status.php with CheckServerJob
// If that works we're done. In that case we don't check the
// 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());
// 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,
[this, account]() {
CheckServerJob *job = new CheckServerJob(account, this);
job->setIgnoreCredentialFailure(true);
connect(job, &CheckServerJob::instanceFound, this, &OwncloudSetupWizard::slotOwnCloudFoundAuth);
connect(job, &CheckServerJob::instanceNotFound, this, &OwncloudSetupWizard::slotNoOwnCloudFoundAuth);
connect(job, &CheckServerJob::timeout, this, &OwncloudSetupWizard::slotNoOwnCloudFoundAuthTimeout);
connect(job, &CheckServerJob::instanceFound, this, &OwncloudSetupWizard::slotFoundServer);
connect(job, &CheckServerJob::instanceNotFound, this, &OwncloudSetupWizard::slotNoServerFound);
connect(job, &CheckServerJob::timeout, this, &OwncloudSetupWizard::slotNoServerFoundTimeout);
job->setTimeout((account->url().scheme() == "https") ? 30 * 1000 : 10 * 1000);
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);
@ -230,14 +263,10 @@ void OwncloudSetupWizard::slotOwnCloudFoundAuth(const QUrl &url, const QJsonObje
qCInfo(lcWizard) << " was redirected to" << url.toString();
}
DetermineAuthTypeJob *job = new DetermineAuthTypeJob(_ocWizard->account(), this);
job->setIgnoreCredentialFailure(true);
connect(job, &DetermineAuthTypeJob::authType,
_ocWizard, &OwncloudWizard::setAuthType);
job->start();
slotDetermineAuthType();
}
void OwncloudSetupWizard::slotNoOwnCloudFoundAuth(QNetworkReply *reply)
void OwncloudSetupWizard::slotNoServerFound(QNetworkReply *reply)
{
auto job = qobject_cast<CheckServerJob *>(sender());
int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
@ -250,7 +279,7 @@ void OwncloudSetupWizard::slotNoOwnCloudFoundAuth(QNetworkReply *reply)
} else {
msg = tr("Failed to connect to %1 at %2:<br/>%3")
.arg(Utility::escape(Theme::instance()->appNameGUI()),
Utility::escape(reply->url().toString()),
Utility::escape(_ocWizard->account()->url().toString()),
Utility::escape(job->errorString()));
}
bool isDowngradeAdvised = checkDowngradeAdvised(reply);
@ -265,7 +294,8 @@ void OwncloudSetupWizard::slotNoOwnCloudFoundAuth(QNetworkReply *reply)
QString serverError = reply->peek(1024 * 20);
qCDebug(lcWizard) << serverError;
QMessageBox messageBox(_ocWizard);
messageBox.setText(serverError);
messageBox.setText(tr("The server reported the following error:"));
messageBox.setInformativeText(serverError);
messageBox.addButton(QMessageBox::Ok);
messageBox.setTextFormat(Qt::RichText);
messageBox.exec();
@ -279,12 +309,20 @@ void OwncloudSetupWizard::slotNoOwnCloudFoundAuth(QNetworkReply *reply)
_ocWizard->account()->resetRejectedCertificates();
}
void OwncloudSetupWizard::slotNoOwnCloudFoundAuthTimeout(const QUrl &url)
void OwncloudSetupWizard::slotNoServerFoundTimeout(const QUrl &url)
{
_ocWizard->displayError(
tr("Timeout while trying to connect to %1 at %2.")
.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)
@ -579,6 +617,8 @@ void OwncloudSetupWizard::slotAssistantFinished(int result)
folderDefinition.localPath = localFolder;
folderDefinition.targetPath = FolderDefinition::prepareTargetPath(_remoteFolder);
folderDefinition.ignoreHiddenFiles = folderMan->ignoreHiddenFiles();
if (folderMan->navigationPaneHelper().showInExplorerNavigationPane())
folderDefinition.navigationPaneClsid = QUuid::createUuid();
auto f = folderMan->addFolder(account, folderDefinition);
if (f) {

View file

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

View file

@ -32,6 +32,19 @@
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)
: QWidget(parent)
, _ui(new Ui::ProtocolWidget)
@ -86,6 +99,16 @@ void ProtocolWidget::showEvent(QShowEvent *ev)
{
ConfigFile cfg;
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);
}
@ -158,14 +181,15 @@ QTreeWidgetItem *ProtocolWidget::createCompletedTreewidgetItem(const QString &fo
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::UserRole, timestamp);
twitem->setIcon(0, icon);
twitem->setToolTip(0, longTimeStr);
twitem->setToolTip(1, item._file);
twitem->setToolTip(3, message);
twitem->setData(0, Qt::UserRole, item._status);
twitem->setData(2, Qt::UserRole, folder);
twitem->setToolTip(3, message);
twitem->setData(3, Qt::UserRole, item._status);
return twitem;
}

View file

@ -34,6 +34,21 @@ namespace Ui {
}
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
* @ingroup gui

View file

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

View file

@ -157,7 +157,8 @@ void SettingsDialogMac::accountAdded(AccountState *s)
connect(accountSettings, &AccountSettings::openFolderAlias, _gui, &ownCloudGui::slotFolderOpenAction);
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);
}
@ -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 accountRemoved(AccountState *);
void slotAccountAvatarChanged();
void slotAccountDisplayNameChanged();
private:
void closeEvent(QCloseEvent *event);

View file

@ -133,6 +133,7 @@ ShareDialog::ShareDialog(QPointer<AccountState> accountState,
job->setProperties(
QList<QByteArray>()
<< "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");
job->setTimeout(10 * 1000);
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;
}
auto privateLinkUrl = result["privatelink"].toString();
auto numericFileId = result["fileid"].toByteArray();
if (!privateLinkUrl.isEmpty()) {
qCInfo(lcSharing) << "Received private link url for" << _sharePath << 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();

View file

@ -155,14 +155,15 @@ ShareLinkWidget::ShareLinkWidget(AccountPtr account,
// Prepare sharing menu
_shareLinkMenu = new QMenu(this);
connect(_shareLinkMenu, &QMenu::triggered,
this, &ShareLinkWidget::slotShareLinkActionTriggered);
_openLinkAction = _shareLinkMenu->addAction(tr("Open link in browser"));
_copyLinkAction = _shareLinkMenu->addAction(tr("Copy link to clipboard"));
_copyDirectLinkAction = _shareLinkMenu->addAction(tr("Copy link to clipboard (direct download)"));
_emailLinkAction = _shareLinkMenu->addAction(tr("Send link by email"));
_emailDirectLinkAction = _shareLinkMenu->addAction(tr("Send link by email (direct download)"));
_linkContextMenu = new QMenu(this);
connect(_linkContextMenu, &QMenu::triggered,
this, &ShareLinkWidget::slotLinkContextMenuActionTriggered);
_deleteLinkAction = _linkContextMenu->addAction(tr("Delete"));
_openLinkAction = _linkContextMenu->addAction(tr("Open link in browser"));
_copyLinkAction = _linkContextMenu->addAction(tr("Copy link to clipboard"));
_copyDirectLinkAction = _linkContextMenu->addAction(tr("Copy link to clipboard (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
@ -231,28 +232,24 @@ void ShareLinkWidget::slotSharesFetched(const QList<QSharedPointer<Share>> &shar
table->insertRow(row);
auto nameItem = new QTableWidgetItem;
QString name = linkShare->getName();
if (name.isEmpty()) {
if (!_namesSupported) {
name = tr("Public link");
nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable);
} else {
name = linkShare->getToken();
}
auto name = shareName(*linkShare);
if (!_namesSupported) {
nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable);
}
nameItem->setText(name);
nameItem->setData(Qt::UserRole, QVariant::fromValue(linkShare));
table->setItem(row, 0, nameItem);
auto shareButton = new QToolButton;
shareButton->setText("...");
shareButton->setProperty(propertyShareC, QVariant::fromValue(linkShare));
connect(shareButton, &QAbstractButton::clicked, this, &ShareLinkWidget::slotShareLinkButtonClicked);
table->setCellWidget(row, 1, shareButton);
auto dotdotdotButton = new QToolButton;
dotdotdotButton->setText("...");
dotdotdotButton->setProperty(propertyShareC, QVariant::fromValue(linkShare));
connect(dotdotdotButton, &QAbstractButton::clicked, this, &ShareLinkWidget::slotContextMenuButtonClicked);
table->setCellWidget(row, 1, dotdotdotButton);
auto deleteButton = new QToolButton;
deleteButton->setIcon(deleteIcon);
deleteButton->setProperty(propertyShareC, QVariant::fromValue(linkShare));
deleteButton->setToolTip(tr("Delete link share"));
connect(deleteButton, &QAbstractButton::clicked, this, &ShareLinkWidget::slotDeleteShareClicked);
table->setCellWidget(row, 2, deleteButton);
@ -514,22 +511,56 @@ void ShareLinkWidget::openShareLink(const QUrl &url)
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>>();
bool downloadEnabled = share->getShowFileListing();
_copyDirectLinkAction->setVisible(downloadEnabled);
_emailDirectLinkAction->setVisible(downloadEnabled);
_shareLinkMenu->setProperty(propertyShareC, QVariant::fromValue(share));
_shareLinkMenu->exec(QCursor::pos());
_linkContextMenu->setProperty(propertyShareC, QVariant::fromValue(share));
_linkContextMenu->exec(QCursor::pos());
}
void ShareLinkWidget::slotShareLinkActionTriggered(QAction *action)
void ShareLinkWidget::slotLinkContextMenuActionTriggered(QAction *action)
{
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());
} else if (action == _copyDirectLinkAction) {
QApplication::clipboard()->setText(share->getDirectDownloadLink().toString());
@ -545,7 +576,7 @@ void ShareLinkWidget::slotShareLinkActionTriggered(QAction *action)
void ShareLinkWidget::slotDeleteShareClicked()
{
auto share = sender()->property(propertyShareC).value<QSharedPointer<LinkShare>>();
share->deleteShare();
confirmAndDeleteShare(share);
}
void ShareLinkWidget::slotPermissionsCheckboxClicked()

View file

@ -70,8 +70,8 @@ private slots:
void slotPasswordChanged(const QString &newText);
void slotNameEdited(QTableWidgetItem *item);
void slotShareLinkButtonClicked();
void slotShareLinkActionTriggered(QAction *action);
void slotContextMenuButtonClicked();
void slotLinkContextMenuActionTriggered(QAction *action);
void slotDeleteShareFetched();
void slotCreateShareFetched(const QSharedPointer<LinkShare> &share);
@ -93,6 +93,12 @@ private:
void emailShareLink(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.
*/
@ -120,12 +126,13 @@ private:
// the next time getShares() finishes. This stores its id.
QString _newShareOverrideSelectionId;
QMenu *_shareLinkMenu;
QAction *_openLinkAction;
QAction *_copyLinkAction;
QAction *_copyDirectLinkAction;
QAction *_emailLinkAction;
QAction *_emailDirectLinkAction;
QMenu *_linkContextMenu = nullptr;
QAction *_deleteLinkAction = nullptr;
QAction *_openLinkAction = nullptr;
QAction *_copyLinkAction = nullptr;
QAction *_copyDirectLinkAction = nullptr;
QAction *_emailLinkAction = nullptr;
QAction *_emailDirectLinkAction = nullptr;
};
}

View file

@ -15,25 +15,41 @@
#include "sharemanager.h"
#include "ocssharejob.h"
#include "account.h"
#include "folderman.h"
#include "accountstate.h"
#include <QUrl>
#include <QJsonDocument>
#include <QJsonObject>
#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 {
/**
* 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,
const QString &id,
const QString &path,
@ -54,6 +70,11 @@ AccountPtr Share::account() const
return _account;
}
QString Share::path() const
{
return _path;
}
QString Share::getId() const
{
return _id;
@ -99,6 +120,8 @@ void Share::deleteShare()
void Share::slotDeleted()
{
emit shareDeleted();
updateFolder(_account, _path);
}
void Share::slotOcsError(int statusCode, const QString &message)
@ -258,6 +281,8 @@ void ShareManager::slotLinkShareCreated(const QJsonDocument &reply)
QSharedPointer<LinkShare> share(parseLinkShare(data));
emit linkShareCreated(share);
updateFolder(_account, share->path());
}
@ -267,51 +292,34 @@ void ShareManager::createShare(const QString &path,
const Share::Permissions permissions)
{
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, &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();
}
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)
{
@ -320,6 +328,8 @@ void ShareManager::slotShareCreated(const QJsonDocument &reply)
QSharedPointer<Share> share(parseShare(data));
emit shareCreated(share);
updateFolder(_account, share->path());
}
void ShareManager::fetchShares(const QString &path)

View file

@ -64,6 +64,8 @@ public:
*/
AccountPtr account() const;
QString path() const;
/*
* Get the id
*/
@ -293,13 +295,10 @@ private slots:
void slotLinkShareCreated(const QJsonDocument &reply);
void slotShareCreated(const QJsonDocument &reply);
void slotOcsError(int statusCode, const QString &message);
void slotCreateShare(const QJsonDocument &reply);
private:
QSharedPointer<LinkShare> parseLinkShare(const QJsonObject &data);
QSharedPointer<Share> parseShare(const QJsonObject &data);
QMap<QObject *, QVariant> _jobContinuation;
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 file = localFileClean.mid(shareFolder->cleanPath().length() + 1);
AccountPtr account = shareFolder->accountState()->account();
// Generate private link ourselves: used as a fallback
SyncJournalFileRecord rec;
if (!shareFolder->journalDb()->getFileRecord(file, &rec) || !rec.isValid())
return;
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.
if (!shareFolder->accountState()->account()->capabilities().privateLinkPropertyAvailable()) {
(target->*targetFun)(oldUrl);
return;
}
// Retrieve the new link by PROPFIND
PropfindJob *job = new PropfindJob(shareFolder->accountState()->account(), file, target);
job->setProperties(QList<QByteArray>() << "http://owncloud.org/ns:privatelink");
// Retrieve the new link or numeric file id by PROPFIND
PropfindJob *job = new PropfindJob(account, file, target);
job->setProperties(
QList<QByteArray>()
<< "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation
<< "http://owncloud.org/ns:privatelink");
job->setTimeout(10 * 1000);
QObject::connect(job, &PropfindJob::result, target, [=](const QVariantMap &result) {
auto privateLinkUrl = result["privatelink"].toString();
auto numericFileId = result["fileid"].toByteArray();
if (!privateLinkUrl.isEmpty()) {
(target->*targetFun)(privateLinkUrl);
} else if (!numericFileId.isEmpty()) {
(target->*targetFun)(account->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded));
} else {
(target->*targetFun)(oldUrl);
}

View file

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

View file

@ -135,6 +135,7 @@ void AbstractNetworkJob::adoptRequest(QNetworkReply *reply)
addTimer(reply);
setReply(reply);
setupConnections(reply);
newReplyHook(reply);
}
QUrl AbstractNetworkJob::makeAccountUrl(const QString &relativePath) const
@ -172,8 +173,6 @@ void AbstractNetworkJob::slotFinished()
QUrl requestedUrl = reply()->request().url();
QUrl redirectUrl = reply()->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
if (_followRedirects && !redirectUrl.isEmpty()) {
_redirectCount++;
// Redirects may be relative
if (redirectUrl.isRelative())
redirectUrl = requestedUrl.resolved(redirectUrl);
@ -194,27 +193,32 @@ void AbstractNetworkJob::slotFinished()
QByteArray verb = requestVerb(*reply());
if (requestedUrl.scheme() == QLatin1String("https") && redirectUrl.scheme() == QLatin1String("http")) {
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!";
} else if (_requestBody && _requestBody->isSequential()) {
qCWarning(lcNetworkJob) << this << "cannot redirect request with sequential body";
} else if (verb.isEmpty()) {
qCWarning(lcNetworkJob) << this << "cannot redirect request: could not detect original verb";
} else {
emit redirected(_reply, redirectUrl, _redirectCount - 1);
emit redirected(_reply, redirectUrl, _redirectCount);
// Create the redirected request and send it
qCInfo(lcNetworkJob) << "Redirecting" << verb << requestedUrl << redirectUrl;
resetTimeout();
if (_requestBody) {
_requestBody->seek(0);
// The signal emission may have changed this value
if (_followRedirects) {
_redirectCount++;
// 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.
*/
void setFollowRedirects(bool follow);
bool followRedirects() const { return _followRedirects; }
QByteArray responseTimestamp();
@ -107,8 +108,6 @@ signals:
void redirected(QNetworkReply *reply, const QUrl &targetUrl, int redirectCount);
protected:
void setupConnections(QNetworkReply *reply);
/** Initiate a network request, returning a QNetworkReply.
*
* Calls setReply() and setupConnections() on it.
@ -133,6 +132,16 @@ protected:
*/
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
QUrl makeAccountUrl(const QString &relativePath) const;

View file

@ -176,7 +176,7 @@ QUrl Account::davUrl() 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)));
}
@ -311,6 +311,12 @@ void Account::setSslErrorHandler(AbstractSslErrorHandler *handler)
void Account::setUrl(const QUrl &url)
{
_url = url;
_userVisibleUrl = url;
}
void Account::setUserVisibleHost(const QString &host)
{
_userVisibleUrl.setHost(host);
}
QVariant Account::credentialSetting(const QString &key) const

View file

@ -99,6 +99,9 @@ public:
void setUrl(const QUrl &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
* a trailing slash.
@ -266,6 +269,15 @@ private:
QImage _avatarImg;
QMap<QString, QVariant> _settingsMap;
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;
QSslConfiguration _sslConfiguration;
Capabilities _capabilities;

View file

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

View file

@ -105,6 +105,17 @@ public:
*/
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:
QVariantMap _capabilities;
};

View file

@ -36,6 +36,7 @@
#include <QNetworkProxy>
#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
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 remotePollIntervalC[] = "remotePollInterval";
static const char forceSyncIntervalC[] = "forceSyncInterval";
static const char fullLocalDiscoveryIntervalC[] = "fullLocalDiscoveryInterval";
static const char notificationRefreshIntervalC[] = "notificationRefreshInterval";
static const char monoIconsC[] = "monoIcons";
static const char promptDeleteC[] = "promptDeleteAllFiles";
static const char crashReporterC[] = "crashReporter";
static const char optionalDesktopNoficationsC[] = "optionalDesktopNotifications";
static const char showInExplorerNavigationPaneC[] = "showInExplorerNavigationPane";
static const char skipUpdateCheckC[] = "skipUpdateCheck";
static const char updateCheckIntervalC[] = "updateCheckInterval";
static const char geometryC[] = "geometry";
@ -122,6 +125,26 @@ bool ConfigFile::optionalDesktopNotifications() const
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
{
QSettings settings(configFile(), QSettings::IniFormat);
@ -406,6 +429,13 @@ quint64 ConfigFile::forceSyncInterval(const QString &connection) const
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
{
QString con(connection);

View file

@ -72,6 +72,13 @@ public:
/* Force sync interval, in milliseconds */
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;
void setMonoIcons(bool);
@ -116,6 +123,9 @@ public:
bool optionalDesktopNotifications() const;
void setOptionalDesktopNotifications(bool show);
bool showInExplorerNavigationPane() const;
void setShowInExplorerNavigationPane(bool show);
int timeout() const;
quint64 chunkSize() const;
quint64 maxChunkSize() const;

View file

@ -18,6 +18,7 @@
#include <QNetworkReply>
#include <QNetworkProxyFactory>
#include <QPixmap>
#include <QXmlStreamReader>
#include "connectionvalidator.h"
#include "account.h"
@ -247,10 +248,22 @@ void ConnectionValidator::slotAuthSuccess()
void ConnectionValidator::checkServerCapabilities()
{
// The main flow now needs the capabilities
JsonApiJob *job = new JsonApiJob(_account, QLatin1String("ocs/v1.php/cloud/capabilities"), this);
job->setTimeout(timeoutToUseMsec);
QObject::connect(job, &JsonApiJob::jsonReceived, this, &ConnectionValidator::slotCapabilitiesRecieved);
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)
@ -268,6 +281,17 @@ void ConnectionValidator::slotCapabilitiesRecieved(const QJsonDocument &json)
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()
{
JsonApiJob *job = new JsonApiJob(_account, QLatin1String("ocs/v1.php/cloud/user"), this);

View file

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

View file

@ -14,6 +14,7 @@
#include <QLoggingCategory>
#include <QString>
#include <QCoreApplication>
#include "common/asserts.h"
#include "creds/abstractcredentials.h"
@ -53,6 +54,15 @@ QString AbstractCredentials::keychainKey(const QString &url, const QString &user
QString key = user + QLatin1Char(':') + u;
if (!accountId.isEmpty()) {
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;
}

View file

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

View file

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

View file

@ -17,6 +17,7 @@
#include "account.h"
#include "theme.h"
#include "common/asserts.h"
#include "common/checksums.h"
#include <csync_private.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)
{
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)
: AbstractNetworkJob(account, QString(), parent)
, _redirects(0)
: QObject(parent)
, _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()
{
send(account()->davUrl());
AbstractNetworkJob::start();
}
qCInfo(lcDetermineAuthTypeJob) << "Determining auth type for" << _account->davUrl();
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;
// Prevent HttpCredentialsAccessManager from setting an Authorization header.
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)

View file

@ -355,26 +355,29 @@ private:
* @brief Checks with auth type to use for a server
* @ingroup libsync
*/
class OWNCLOUDSYNC_EXPORT DetermineAuthTypeJob : public AbstractNetworkJob
class OWNCLOUDSYNC_EXPORT DetermineAuthTypeJob : public QObject
{
Q_OBJECT
public:
enum AuthType {
Unknown,
Basic,
Basic, // also the catch-all fallback for backwards compatibility reasons
OAuth,
Shibboleth
};
explicit DetermineAuthTypeJob(AccountPtr account, QObject *parent = 0);
void start() Q_DECL_OVERRIDE;
void start();
signals:
void authType(AuthType);
private slots:
bool finished() Q_DECL_OVERRIDE;
private:
void send(const QUrl &url);
int _redirects;
void checkBothDone();
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()
{
return qMax(qgetenv("OWNCLOUD_BLACKLIST_TIME_MIN").toInt(),
return qMax(qEnvironmentVariableIntValue("OWNCLOUD_BLACKLIST_TIME_MIN"),
25); // 25 seconds
}
static qint64 getMaxBlacklistTime()
{
int v = qgetenv("OWNCLOUD_BLACKLIST_TIME_MAX").toInt();
int v = qEnvironmentVariableIntValue("OWNCLOUD_BLACKLIST_TIME_MAX");
if (v > 0)
return v;
return 24 * 60 * 60; // 1 day
@ -742,6 +742,16 @@ PropagatorJob::JobParallelism PropagatorCompositeJob::parallelism()
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()
{
@ -900,7 +910,8 @@ void PropagateDirectory::slotFirstJobFinished(SyncFileItem::Status status)
if (status != SyncFileItem::Success && status != SyncFileItem::Restoration) {
if (_state != Finished) {
abort();
// Synchronously abort
abort(AbortType::Synchronous);
_state = Finished;
emit finished(status);
}

View file

@ -65,6 +65,11 @@ class PropagatorJob : public QObject
public:
explicit PropagatorJob(OwncloudPropagator *propagator);
enum AbortType {
Synchronous,
Asynchronous
};
enum JobState {
NotYetStarted,
Running,
@ -98,7 +103,14 @@ public:
virtual qint64 committedDiskSpace() const { return 0; }
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
* returns true if a job was started.
@ -110,11 +122,14 @@ signals:
*/
void finished(SyncFileItem::Status);
/**
* Emitted when the abort is fully finished
*/
void abortFinished(SyncFileItem::Status status = SyncFileItem::NormalError);
protected:
OwncloudPropagator *propagator() const;
};
/*
* Abstract class to propagate a single item
*/
@ -185,10 +200,11 @@ public:
SyncFileItemVector _tasksToDo;
QVector<PropagatorJob *> _runningJobs;
SyncFileItem::Status _hasError; // NoStatus, or NormalError / SoftError if there was an error
quint64 _abortsCount;
explicit PropagatorCompositeJob(OwncloudPropagator *propagator)
: PropagatorJob(propagator)
, _hasError(SyncFileItem::NoStatus)
, _hasError(SyncFileItem::NoStatus), _abortsCount(0)
{
}
@ -209,15 +225,32 @@ public:
virtual bool scheduleSelfOrChild() 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)
j->abort();
if (!_runningJobs.empty()) {
_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;
private slots:
void slotSubJobAbortFinished();
bool possiblyRunNextJob(PropagatorJob *next)
{
if (next->_state == NotYetStarted) {
@ -258,11 +291,17 @@ public:
virtual bool scheduleSelfOrChild() 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)
_firstJob->abort();
_subJobs.abort();
// Force first job to abort synchronously
// 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()
@ -280,6 +319,7 @@ private slots:
void slotFirstJobFinished(SyncFileItem::Status status);
void slotSubJobsFinished(SyncFileItem::Status status);
};
@ -324,6 +364,7 @@ public:
, _chunkSize(10 * 1000 * 1000) // 10 MB, overridden in setSyncOptions
, _account(account)
{
qRegisterMetaType<PropagatorJob::AbortType>("PropagatorJob::AbortType");
}
~OwncloudPropagator();
@ -404,13 +445,23 @@ public:
void abort()
{
_abortRequested.fetchAndStoreOrdered(true);
bool alreadyAborting = _abortRequested.fetchAndStoreOrdered(true);
if (alreadyAborting)
return;
if (_rootJob) {
// We're possibly already in an item's finished stack
QMetaObject::invokeMethod(_rootJob.data(), "abort", Qt::QueuedConnection);
// Connect to abortFinished which signals that abort has been asynchronously finished
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
@ -431,6 +482,13 @@ public:
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 */
void emitFinished(SyncFileItem::Status status)
{

View file

@ -127,7 +127,6 @@ void GETFileJob::start()
sendRequest("GET", _directDownloadUrl, req);
}
reply()->setReadBufferSize(16 * 1024); // keep low so we can easier limit the bandwidth
qCDebug(lcGetJob) << _bandwidthManager << _bandwidthChoked << _bandwidthLimited;
if (_bandwidthManager) {
_bandwidthManager->registerDownloadJob(this);
@ -137,14 +136,20 @@ void GETFileJob::start()
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);
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()
{
// 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();
// 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.
// For any error: handle it when the job is finished, not here.
if (httpStatus / 100 != 2) {
@ -216,6 +225,8 @@ void GETFileJob::slotMetaDataChanged()
if (!lastModified.isNull()) {
_lastModified = Utility::qDateTimeToTime_t(lastModified.toDateTime());
}
_saveBodyToFile = true;
}
void GETFileJob::setBandwidthManager(BandwidthManager *bwm)
@ -281,7 +292,7 @@ void GETFileJob::slotReadyRead()
return;
}
if (_device->isOpen()) {
if (_device->isOpen() && _saveBodyToFile) {
qint64 w = _device->write(buffer.constData(), r);
if (w != r) {
_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.
// 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
&& _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";
auto computeChecksum = new ComputeChecksum(this);
computeChecksum->setChecksumType(parseChecksumHeaderType(_item->_checksumHeader));
@ -368,8 +382,17 @@ void PropagateDownloadFile::start()
void PropagateDownloadFile::conflictChecksumComputed(const QByteArray &checksumType, const QByteArray &checksum)
{
if (makeChecksumHeader(checksumType, checksum) == _item->_checksumHeader) {
// No download necessary, just update fs and journal metadata
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);
return;
}
@ -624,7 +647,10 @@ void PropagateDownloadFile::slotGetFinished()
this, &PropagateDownloadFile::transmissionChecksumValidated);
connect(validator, &ValidateChecksumHeader::validationFailed,
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);
}
@ -897,9 +923,13 @@ void PropagateDownloadFile::slotDownloadProgress(qint64 received, qint64)
}
void PropagateDownloadFile::abort()
void PropagateDownloadFile::abort(PropagatorJob::AbortType abortType)
{
if (_job && _job->reply())
_job->reply()->abort();
if (abortType == AbortType::Asynchronous) {
emit abortFinished();
}
}
}

View file

@ -43,6 +43,9 @@ class GETFileJob : public AbstractNetworkJob
bool _hasEmittedFinishedSignal;
time_t _lastModified;
/// Will be set to true once we've seen a 2xx response header
bool _saveBodyToFile = false;
public:
// DOES NOT take ownership of the 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 setChoked(bool c);
void setBandwidthLimited(bool b);
@ -185,7 +190,7 @@ private slots:
/// Called when it's time to update the db metadata
void updateMetadata(bool isConflict);
void abort() Q_DECL_OVERRIDE;
void abort(PropagatorJob::AbortType abortType) Q_DECL_OVERRIDE;
void slotDownloadProgress(qint64, qint64);
void slotChecksumFail(const QString &errMsg);

View file

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

View file

@ -52,7 +52,7 @@ public:
{
}
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(); }

View file

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

View file

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

View file

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

View file

@ -56,7 +56,7 @@ public:
{
}
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; }
/**

View file

@ -558,20 +558,24 @@ void PropagateUploadFileCommon::slotJobDestroyed(QObject *job)
_jobs.erase(std::remove(_jobs.begin(), _jobs.end(), job), _jobs.end());
}
void PropagateUploadFileCommon::abort()
void PropagateUploadFileCommon::abort(PropagatorJob::AbortType abortType)
{
foreach (auto *job, _jobs) {
if (job->reply()) {
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
void PropagateUploadFileCommon::abortWithError(SyncFileItem::Status status, const QString &error)
{
_finished = true;
abort();
abort(AbortType::Synchronous);
done(status, error);
}
@ -625,4 +629,33 @@ void PropagateUploadFileCommon::finalize()
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;
}
QIODevice *device()
{
return _device;
}
QString errorString()
{
return _errorString.isEmpty() ? AbstractNetworkJob::errorString() : _errorString;
@ -205,6 +210,7 @@ protected:
QVector<AbstractNetworkJob *> _jobs; /// network jobs that are currently in transit
bool _finished BITFIELD(1); /// Tells that all the jobs have been finished
bool _deleteExisting BITFIELD(1);
quint64 _abortCount; /// Keep track of number of aborted items
// measure the performance of checksum calc and upload
#ifdef WITH_TESTING
@ -218,6 +224,7 @@ public:
: PropagateItemJob(propagator, item)
, _finished(false)
, _deleteExisting(false)
, _abortCount(0)
{
}
@ -248,13 +255,20 @@ public:
void abortWithError(SyncFileItem::Status status, const QString &error);
public slots:
void abort() Q_DECL_OVERRIDE;
void abort(PropagatorJob::AbortType abortType) Q_DECL_OVERRIDE;
void slotJobDestroyed(QObject *job);
private slots:
void slotReplyAbortFinished();
void slotPollFinished();
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
* transfer if it happens too often. If so: Bump UploadInfo::errorCount
@ -287,15 +301,15 @@ private:
* In the non-resuming case it is 0.
* 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
* (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)
*/
int _currentChunk;
int _chunkCount; /// Total number of chunks for this file
int _transferId; /// transfer id (part of the url)
int _currentChunk = 0;
int _chunkCount = 0; /// Total number of chunks for this file
int _transferId = 0; /// transfer id (part of the url)
quint64 chunkSize() const {
// 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;
}
public:
PropagateUploadFileV1(OwncloudPropagator *propagator, const SyncFileItemPtr &item)
: PropagateUploadFileCommon(propagator, item)
@ -311,7 +324,8 @@ public:
}
void doStartUpload() Q_DECL_OVERRIDE;
public slots:
void abort(PropagatorJob::AbortType abortType) Q_DECL_OVERRIDE;
private slots:
void startNextChunk();
void slotPutFinished();
@ -328,11 +342,11 @@ class PropagateUploadFileNG : public PropagateUploadFileCommon
{
Q_OBJECT
private:
quint64 _sent; /// amount of data (bytes) that was already sent
uint _transferId; /// transfer id (part of the url)
int _currentChunk; /// Id of the next chunk that will be sent
quint64 _currentChunkSize; /// current chunk size
bool _removeJobError; /// If not null, there was an error removing the job
quint64 _sent = 0; /// amount of data (bytes) that was already sent
uint _transferId = 0; /// transfer id (part of the url)
int _currentChunk = 0; /// Id of the next chunk that will be sent
quint64 _currentChunkSize = 0; /// current chunk size
bool _removeJobError = false; /// If not null, there was an error removing the job
// Map chunk number with its size from the PROPFIND on resume.
// (Only used from slotPropfindIterate/slotPropfindFinished because the LsColJob use signals to report data.)
@ -352,7 +366,6 @@ private:
public:
PropagateUploadFileNG(OwncloudPropagator *propagator, const SyncFileItemPtr &item)
: PropagateUploadFileCommon(propagator, item)
, _currentChunkSize(0)
{
}
@ -361,6 +374,8 @@ public:
private:
void startNewUpload();
void startNextChunk();
public slots:
void abort(AbortType abortType) Q_DECL_OVERRIDE;
private slots:
void slotPropfindFinished();
void slotPropfindFinishedWithError();

View file

@ -365,7 +365,7 @@ void PropagateUploadFileNG::slotPutFinished()
// target duration for each chunk upload.
double targetDuration = propagator()->syncOptions()._targetChunkUploadDuration;
if (targetDuration > 0) {
double uploadTime = job->msSinceStart();
double uploadTime = job->msSinceStart() + 1; // add one to avoid div-by-zero
auto predictedGoodSize = static_cast<quint64>(
_currentChunkSize / uploadTime * targetDuration);
@ -491,4 +491,26 @@ void PropagateUploadFileNG::slotUploadProgress(qint64 sent, qint64 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);
}
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
*/
static const char checkSumHeaderC[] = "OC-Checksum";
static const char contentMd5HeaderC[] = "Content-MD5";
/**
* @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;
// For directories, metadata-only updates will be done after all their files are propagated.
if (!isDirectory) {
emit syncItemDiscovered(*item);
// Update the database now already: New remote fileid or Etag or RemotePerm
// 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));
// This might have changed the shared flag, so we must notify SyncFileStatusTracker for example
emit itemCompleted(item);
} else {
// The local tree is walked first and doesn't have all the info from the server.
// 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);
emit syncItemDiscovered(*item);
return re;
}
@ -813,6 +813,7 @@ void SyncEngine::startSync()
}
_csync_ctx->read_remote_from_db = true;
_lastLocalDiscoveryStyle = _csync_ctx->local_discovery_style;
bool 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
if (_account->serverVersionInt() < Account::makeServerVersion(8, 1, 0)) {
// Server version older than 8.1 don't support these character in filename.
static const QRegExp invalidCharRx("[\\\\:?*\"<>|]");
QString invalidFilenamePattern = _account->capabilities().invalidFilenameRegex();
if (invalidFilenamePattern.isNull()
&& _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) {
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)->_instruction = CSYNC_INSTRUCTION_IGNORE;
}
@ -1022,7 +1032,7 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult)
_progressInfo->startEstimateUpdates();
// 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
QString script = qgetenv("OWNCLOUD_POST_UPDATE_SCRIPT");
@ -1455,7 +1465,7 @@ void SyncEngine::checkForPermission(SyncFileItemVector &syncItems)
RemotePermissions SyncEngine::getPermissions(const QString &file) const
{
static bool isTest = qgetenv("OWNCLOUD_TEST_PERMISSIONS").toInt();
static bool isTest = qEnvironmentVariableIntValue("OWNCLOUD_TEST_PERMISSIONS");
if (isTest) {
QRegExp rx("_PERM_([^_]*)_[^/]*$");
if (rx.indexIn(file) != -1) {
@ -1555,6 +1565,12 @@ AccountPtr SyncEngine::account() const
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()
{
if (_propagator)

View file

@ -25,6 +25,7 @@
#include <QMap>
#include <QStringList>
#include <QSharedPointer>
#include <set>
#include <csync.h>
@ -92,6 +93,7 @@ public:
AccountPtr account() const;
SyncJournalDb *journal() const { return _journal; }
QString localPath() const { return _localPath; }
/**
* 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
@ -99,14 +101,28 @@ public:
*/
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:
void csyncUnavailable();
// During update, before reconcile
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
void aboutToPropagate(SyncFileItemVector &);
@ -272,6 +288,9 @@ private:
/** List of unique errors that occurred in a sync run. */
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