1
0
mirror of https://github.com/chylex/Nextcloud-Desktop.git synced 2026-04-10 09:57:41 +02:00

Compare commits

..

102 Commits

Author SHA1 Message Date
Markus Goetz
b62b0d0a64 VERSION.cmake: 2.4.0 beta1 2017-11-16 13:57:56 +01:00
Olivier Goffart
2192386d86 test/testsyncengine: test for PR #6168 2017-11-16 13:31:54 +01:00
Christian Kamm
e694ffcd03 PropagateDownload: Adjustments to skipping downloads #6153
Previously we required matching mtimes but that's actually
unnecessary when the question is about whether to skip the
download. We will still update the file's metadata.

Also, adjust behavior when the checksum is weak (Adler32):
in these cases we still depend on equal mtimes.
2017-11-16 13:31:54 +01:00
Christian Kamm
eff401d418 Share links: Confirm deletion explicitly #6163
Also add the "Delete" action to the "..." menu.
2017-11-16 11:16:47 +01:00
Jenkins for ownCloud
7049ccd7ab [tx-robot] updated from transifex 2017-11-16 02:18:53 +01:00
Markus Goetz
8fb2167fca sqlite: Update bundled version to 3.21
For OS X and Windows.
2017-11-15 21:53:53 +01:00
Markus Goetz
d0b8528022 Changelog: 2.4 changes up to now 2017-11-15 21:47:34 +01:00
Samuel Alfageme
13d80beb83 Changelog Updated with latest changes
Web equivalent PR: https://github.com/owncloud/owncloud.org/pull/1318
2017-11-15 13:37:25 +01:00
Christian Kamm
06ce8dd8bd Wizard: Add explanation text when server error is shown #6157
Just showing a box with the message can be surprising.
2017-11-15 11:01:44 +01:00
Olivier Goffart
9a1ea67a35 Theme: Update the wizard size on high dpi screen
Issue #6156
2017-11-15 10:46:08 +01:00
Christian Kamm
e616f904b6 Share link: Add tooltip to delete button #6163 2017-11-15 10:16:33 +01:00
Christian Kamm
bff24ffd2f PropagateDownload: Fix GET with redirects #6159
The GET jobs were redirected, but the custom incremental handling
in readyRead didn't propagate to the follow-up job.
2017-11-15 10:11:15 +01:00
Jenkins for ownCloud
2cdc969cc5 [tx-robot] updated from transifex 2017-11-15 02:18:34 +01:00
Matthew Setter
b13fd786b6 Fix broken download link 2017-11-14 12:25:48 +01:00
Matthew Setter
89d55d0af8 Fix broken images in the FAQ 2017-11-14 12:25:48 +01:00
Matthew Setter
d46335aba1 Minor formatting fixes
Fix broken header and table
2017-11-14 12:25:48 +01:00
Matthew Setter
e417a4b159 Fix title casing to match style guide 2017-11-14 12:25:48 +01:00
Christian Kamm
b0f986c740 Private links: Account for overwritehost #6146
Retrieve ocs/v1.php/config to determine the desired host and build the
private link urls with that instead.
2017-11-14 12:14:11 +01:00
Christian Kamm
18091f99d4 Private links: Retrieve numeric file id property #6146
Some servers have non-compliant instance ids (that start with a number)
and thereby make deducing the numeric file id from the full id
unreliable.

To circumvent this problem we retrieve the fileid property from the
server with a PROPFIND.
2017-11-14 12:14:11 +01:00
Christian Kamm
3ae2071129 DetermineAuth: Remove concept of Unknown #6148
This restores 2.3 behavior. Some servers reply 404 to GETs and PROPFINDs
to the remote.php/webdav/ url and used to work. Being more picky would
break them.
2017-11-14 12:10:35 +01:00
Christian Kamm
820ebf4b6e Sql: db close() failing isn't fatal
The assert was made fatal when looking at asserts for #5429 #5518
without having a particular problem in mind. Recent reports weakly
suggest that this might lead to occasional crashes for users when
sqlite_close fails in ways that look ignorable.
2017-11-14 12:10:19 +01:00
Jenkins for ownCloud
161f6baba6 [tx-robot] updated from transifex 2017-11-14 02:18:32 +01:00
Jenkins for ownCloud
89d7dbf2cf [tx-robot] updated from transifex 2017-11-13 02:18:32 +01:00
Jenkins for ownCloud
b1972e56fa [tx-robot] updated from transifex 2017-11-12 02:18:33 +01:00
Jenkins for ownCloud
7d10f9829e [tx-robot] updated from transifex 2017-11-11 02:18:34 +01:00
Jenkins for ownCloud
d2723bc0ac [tx-robot] updated from transifex 2017-11-10 02:18:50 +01:00
Jenkins for ownCloud
a0d43f8ba6 [tx-robot] updated from transifex 2017-11-09 02:18:50 +01:00
Christian Kamm
37c5612aee Discovery: Treat files starting with '.' as hidden #6145
This bug was introduced when strcmp(a, b) != 0 was accidentally
converted to a == b.
2017-11-08 12:53:46 +01:00
Jenkins for ownCloud
f2e73c0887 [tx-robot] updated from transifex 2017-11-08 02:25:47 +01:00
Jenkins for ownCloud
aca36cc1ac [tx-robot] updated from transifex 2017-11-07 02:21:25 +01:00
Christian Kamm
9af6e29f42 DetermineAuthType: Adjustments for tight firewalls #6135
With some firewalls we can't GET /remote.php/webdav/. Here we keep the
GET request to detect shibboleth through the redirect pattern but then
use PROPFIND to figure out the http auth method.

Currently we prefer OAuth to Shibboleth to Basic auth.

This also restores the fallback behavior of assuming basic auth
when no auth type can be determined.
2017-11-06 13:09:10 +01:00
Jenkins for ownCloud
d6db77cce8 [tx-robot] updated from transifex 2017-11-06 02:25:40 +01:00
Jenkins for ownCloud
71d2e3de74 [tx-robot] updated from transifex 2017-11-05 02:26:45 +01:00
Jenkins for ownCloud
a7e07239e3 [tx-robot] updated from transifex 2017-11-04 02:19:35 +01:00
Christian Kamm
3608114ec1 Journal: Fix low-disk space errors
Fixes a regression from 4dbe9d4113,
low disk space can lead to IOERR not just CANTOPEN.
2017-11-03 10:44:47 +01:00
Christian Kamm
c9d5a9cea2 HttpCreds: Don't create empty client cert keychain entries #5752
This doesn't do anything about deleting the client cert keychain
entries when the whole account is removed though.
2017-11-03 10:28:56 +01:00
Jenkins for ownCloud
354cdfdbc1 [tx-robot] updated from transifex 2017-11-03 02:19:05 +01:00
Christian Kamm
4dbe9d4113 Journal: Don't crash if the db file is readonly #6050
Surprisingly sqlite3_open_v2() returns ok on readonly files with
SQLITE_OPEN_READWRITE. Probably due to the journal mode?
2017-11-02 16:36:51 +01:00
Jenkins for ownCloud
7963a8322d [tx-robot] updated from transifex 2017-11-02 02:18:48 +01:00
Christian Kamm
6ac44f05cd Credentials: Namespace windows cred keys #6125
The application name is prepended to the key. QtKeychain doesn't
do that automatically on the platform.
2017-11-01 17:03:30 +01:00
Christian Kamm
64a84fda38 Wizard: Don't report confusing error message #6116
For historical reasons CheckServerJob doesn't just check url/ but also
url/owncloud/. However, that means if url/status.php is a 404 and
url/owncloud/status.php is a 404, the user will see the latter url
appear in the error message. That's potentially confusing.

Instead, just show the account url which will be closer to what the
user typed into the account wizard, while being adjusted for protocol
and possible redirects.
2017-11-01 15:57:58 +01:00
Jenkins for ownCloud
8eec79680c [tx-robot] updated from transifex 2017-11-01 02:19:02 +01:00
Jenkins for ownCloud
0968d74dad [tx-robot] updated from transifex 2017-10-30 02:27:13 +01:00
Jenkins for ownCloud
876a1c5140 [tx-robot] updated from transifex 2017-10-29 02:18:42 +01:00
Jenkins for ownCloud
1455636169 [tx-robot] updated from transifex 2017-10-29 02:21:41 +02:00
Jenkins for ownCloud
7ea9a5ca4e [tx-robot] updated from transifex 2017-10-28 02:18:51 +02:00
Jenkins for ownCloud
2d2ec2a576 [tx-robot] updated from transifex 2017-10-27 02:20:55 +02:00
Jenkins for ownCloud
e4612ca5d4 [tx-robot] updated from transifex 2017-10-26 02:19:20 +02:00
Jenkins for ownCloud
15af5878b6 [tx-robot] updated from transifex 2017-10-25 02:21:52 +02:00
Olivier Goffart
c36043a175 ShareDialog: trigger a sync for folder affected by a change of sharing
This allow the sync engine to query the new metadata and update the
overlay icons.

Note: we also need to invalidate the etags because the server does not
change the etag of parent directories that see their share-types changed.

Issue #6098
2017-10-24 15:50:14 +02:00
Olivier Goffart
ee63b36ed3 SyncFileStatusTracker: Detect changed in the shared flag
... even if the file is not changed.

We get an UPDATE_METADATA in that case, so make sure we let the
SyncFileStatusTracker know about it.
That means we need to filter out UPDATE_METADATA in the other listeners
of this signal.

Issue #6098
2017-10-24 15:50:14 +02:00
Olivier Goffart
35d28294cd SyncEngine: remove SyncEngine::syncItemDiscovered
It is unused.
2017-10-24 15:50:14 +02:00
Olivier Goffart
c6bd3ab31a Sharing: remove the ShareManager::_jobContinuation
It is growing indefinitively in case of error, causing a leak.
Use a labda instead to pass the capture
2017-10-24 15:50:14 +02:00
Christian Kamm
9c7ee6ef85 Reconcile: Rename handling fixes: duplicate file ids
When users share the same tree several times (say A/ and A/B/ are both
shared) the remote tree can have several entries that have the same
file id. This needs to be respected in rename detection.

Also adds several tests and fixes for issues noticed during testing.

See #6096
2017-10-24 10:54:23 +02:00
Christian Kamm
f3ea375083 Wizard: Resolve url/ redirects only if url/status.php not found
Unfortunately checking the base-url for redirects in all cases lead
to incorrect behavior in some SAML/OAuth2 edge cases.

This new iteration checks the base url for redirects only if the
standard CheckServerJob can't reach the server. That way the 2.3
behavior is only changed in cases that would have lead to errors.

See #5954
2017-10-24 09:42:08 +02:00
Jenkins for ownCloud
15b02547e8 [tx-robot] updated from transifex 2017-10-24 02:18:49 +02:00
Jenkins for ownCloud
13b1f8b33e [tx-robot] updated from transifex 2017-10-23 02:19:01 +02:00
Jenkins for ownCloud
ccd32c04a9 [tx-robot] updated from transifex 2017-10-22 02:21:00 +02:00
Jenkins for ownCloud
df19b20d07 [tx-robot] updated from transifex 2017-10-21 02:20:25 +02:00
Olivier Goffart
f41c9fbb7f owncloudsetupwizard: Fix "add new account" if the wizard is already visible
Clicking on the "Add new Account" from the systray menu should raise
the wizard, even if it is already running.

Relates to issue #6105
2017-10-20 12:41:48 +02:00
Olivier Goffart
a0e50670de Shibolleth: raise the browser when clicking on the tray
Issue #6105

Dynamically find the browser trough topLevelWidgets instead of
forwarding the call to the relevant page as it would require to break
many abstration layers (OwncloudSetupWizard -> OwncloudWizard ->
AbstractCredentialsWizardPage -> OwncloudShibbolethCredsPage)
And considering that we want to phase shibboleth down, I tought is
was not worth adding an interface for this.

The OAuth page don't have this problem because it shows a label and
allow the user to re-open the browser.
2017-10-20 10:51:35 +02:00
Jenkins for ownCloud
984631d807 [tx-robot] updated from transifex 2017-10-20 02:21:27 +02:00
Olivier Goffart
a9761a8976 Use qEnvironmentVariable* instead of qgetenv when appropriate
Now that we use Qt5, we should do that. It is slightly more efficient
and declares the intent.
(Modified using clazy)
2017-10-19 13:57:49 +02:00
Jenkins for ownCloud
84d8624e03 [tx-robot] updated from transifex 2017-10-19 02:21:00 +02:00
Markus Goetz
9866010b4c SettingsDialogMac: Fix account display name #6078 2017-10-18 10:41:01 +02:00
Markus Goetz
a3c1052cae owncloudcmd: Fix timestamps, Fix --logdebug
We did not set a log handler so there were no timestamps.
The --debug didn't have an effect, let's use --logdebug like in GUI version.
(Command line always outputs some log)

Found in owncloud/documentation#3436
2017-10-18 09:16:05 +02:00
Jenkins for ownCloud
726cbc160c [tx-robot] updated from transifex 2017-10-18 02:19:58 +02:00
Christian Kamm
bf39343920 Sync: Add capability for invalid filename regexes #6092 2017-10-17 17:15:49 +02:00
Markus Goetz
af24b4132d Packaging: Require ZLIB
For owncloud/enterprise#2295
2017-10-17 16:53:11 +02:00
Markus Goetz
d339b68715 Checksums: Use addData function
Our implementation had bad error handling.
This one now uses a new Qt5 addData function.

owncloud/enterprise#2252
2017-10-17 13:26:54 +02:00
Jenkins for ownCloud
eadc791795 [tx-robot] updated from transifex 2017-10-17 12:43:06 +02:00
Jenkins for ownCloud
408cf11b2b [tx-robot] updated from transifex 2017-10-17 12:40:55 +02:00
Markus Goetz
e3ba8d3209 Merge branch 'master' into 2.4 2017-10-17 12:37:20 +02:00
Christian Kamm
17b1c83ae5 Activity: Allow sorting of issues and protocol #6086
The issues tab uses custom ordering where overall and summary sync
issues are displayed first. This ordering is preserved by creating
special sorting logic for the "time" column.

It needed special handling anyway, since sorting by time-string would
have yielded incorrect results.
2017-10-17 09:47:57 +02:00
Christian Kamm
b2a8ffc577 Abort: Fix crash with early aborts
_chunkCount could be 0, leading to a floating point exception

I also added initializers for several uninitialized integers in the
upload jobs.
2017-10-17 09:44:52 +02:00
Christian Kamm
e2711224ed Propagator: Avoid duplicate async abort 2017-10-17 09:44:52 +02:00
Christian Kamm
2ac7e0200a Test case for #5949 2017-10-17 09:44:52 +02:00
Christian Kamm
e6b971b316 TestUtils: Invalidate etags on PUT or chunk-MOVE 2017-10-17 09:44:52 +02:00
Christian Kamm
111bb485ec UploadNG: Avoid div-by-zero for super fast uploads 2017-10-17 09:44:52 +02:00
Piotr Mrowczynski
e10775d34f Fix paused sync file move issue #5949
Dont abort final chunk immedietally

Use sync and async aborts
2017-10-17 09:44:52 +02:00
Jenkins for ownCloud
30957479a3 [tx-robot] updated from transifex 2017-10-17 02:18:34 +02:00
Jenkins for ownCloud
b4ab53dd32 [tx-robot] updated from transifex 2017-10-16 02:18:33 +02:00
Jenkins for ownCloud
a314eeb892 [tx-robot] updated from transifex 2017-10-15 02:18:35 +02:00
Jenkins for ownCloud
644ddf318c [tx-robot] updated from transifex 2017-10-14 02:18:36 +02:00
Christian Kamm
f598ac89ac HttpCreds: Fix retry after wrong password #5989
This is an ugly solution.
2017-10-13 14:24:37 +02:00
Christian Kamm
3f7b3ca962 Checksums: Improve logging and add global disable #5017 2017-10-13 13:08:20 +02:00
Jenkins for ownCloud
104c6edcde [tx-robot] updated from transifex 2017-10-13 02:18:37 +02:00
Jenkins for ownCloud
94b673dc8e [tx-robot] updated from transifex 2017-10-12 02:18:38 +02:00
Christian Kamm
096cd348f0 Doc: Update FAQ entry on deeply nested directories #1067 2017-10-11 16:01:46 +02:00
Christian Kamm
01c2ffe2ae PropagateDownload: Read Content-md5 header #6088 2017-10-11 09:06:23 +02:00
Jenkins for ownCloud
0a4370236d [tx-robot] updated from transifex 2017-10-11 02:18:35 +02:00
Jenkins for ownCloud
fe8d2b397e [tx-robot] updated from transifex 2017-10-10 02:18:44 +02:00
Christian Kamm
92e90f9c55 Context menu: More detailed status messages
Previously it could only display synchronization progress or "up to
date". Now it also communicates the same overall state that the icon
shows.

See owncloud/enterprise#2134
2017-10-09 14:14:31 +02:00
Jenkins for ownCloud
3eb2642b11 [tx-robot] updated from transifex 2017-10-09 02:18:35 +02:00
Jenkins for ownCloud
99192a6dec [tx-robot] updated from transifex 2017-10-08 02:18:36 +02:00
Jenkins for ownCloud
cffc1fd1c4 [tx-robot] updated from transifex 2017-10-07 02:18:37 +02:00
Olivier Goffart
b8f7c6daae CMakeLists.txt: Put -pie as a linker flag were it belongs
Fixes warning introduced by PR #6040
2017-10-06 14:35:56 +02:00
Markus Goetz
cd07865da6 Settings Dialog: Fix display name compilation for macOS #6078 2017-10-06 13:34:40 +02:00
Thomas Müller
315e38e814 Use display-name from the ocs call in the settings dialog 2017-10-06 10:59:20 +02:00
Jenkins for ownCloud
95b90271b6 [tx-robot] updated from transifex 2017-10-06 02:18:35 +02:00
Christian Kamm
90befac901 Rename detection: File size must be equal
Comparison of file sizes for potential conflicts was added in
0eb9401c62, but did not extend to checking
the file size in case of potential local moves.

This commit adds this check and adds tests for various move+change
scenarios.
2017-10-05 14:17:07 +02:00
Christian Kamm
22f71ce17e t9.pl: Make more reliable
Sometimes the 'touch' didn't change the mtime since too little time had
passed!
2017-10-05 14:13:10 +02:00
124 changed files with 13392 additions and 9671 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)

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

@@ -5,7 +5,7 @@ set( MIRALL_VERSION_YEAR 2017 )
set( MIRALL_SOVERSION 0 )
if ( NOT DEFINED MIRALL_VERSION_SUFFIX )
set( MIRALL_VERSION_SUFFIX "alpha1") #e.g. beta1, beta2, rc1
set( MIRALL_VERSION_SUFFIX "beta1") #e.g. beta1, beta2, rc1
endif( NOT DEFINED MIRALL_VERSION_SUFFIX )
if( NOT DEFINED MIRALL_VERSION_BUILD )

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

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

View File

@@ -62,4 +62,4 @@ Some interesting values that can be set on the configuration file are:
| | | ``2`` for No Proxy. |
+ + +--------------------------------------------------------------------------------------------------------+
| | | ``3`` for HTTP(S) Proxy. |
+---------------------------------+---------------+--------------------------------------------------------------------------------------------------------+
+---------------------------------+---------------+--------------------------------------------------------------------------------------------------------+

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
@@ -58,3 +58,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

@@ -183,6 +183,135 @@ X-GNOME-Autostart-Delay=3
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# Translations
# 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

@@ -20,8 +20,9 @@ include_directories(${CMAKE_SOURCE_DIR}/src/csync
include_directories(${CMAKE_SOURCE_DIR}/src/3rdparty/qtokenizer)
if(UNIX AND NOT APPLE)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pie -fPIE")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pie -fPIE")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIE")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIE")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pie")
endif()
if(NOT BUILD_LIBRARIES_ONLY)

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

@@ -120,7 +120,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 +133,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 +156,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 +167,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 +250,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

@@ -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

@@ -1094,15 +1094,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 +1111,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;

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);

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

@@ -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

@@ -265,8 +265,8 @@ static int _csync_detect_update(CSYNC *ctx, std::unique_ptr<csync_file_stat_t> f
fs->instruction = CSYNC_INSTRUCTION_NEW;
bool isRename =
base.isValid() && base._inode == fs->inode && base._type == fs->type
&& (base._modtime == fs->modtime || fs->type == CSYNC_FTW_TYPE_DIR)
base.isValid() && base._type == fs->type
&& ((base._modtime == fs->modtime && base._fileSize == fs->size) || fs->type == CSYNC_FTW_TYPE_DIR)
#ifdef NO_RENAME_EXTENSION
&& _csync_sameextension(base._path, fs->path)
#endif
@@ -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;
}
}
@@ -616,7 +635,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

@@ -129,6 +129,7 @@ set(updater_SRCS
IF( APPLE )
list(APPEND client_SRCS cocoainitializer_mac.mm)
list(APPEND client_SRCS settingsdialogmac.cpp)
list(REMOVE_ITEM client_SRCS settingsdialog.cpp)
list(APPEND client_SRCS socketapisocket_mac.mm)
list(APPEND client_SRCS systray.mm)
@@ -242,8 +243,9 @@ if (NOT NO_SHIBBOLETH)
endif()
if(UNIX AND NOT APPLE)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pie -fPIE")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pie -fPIE")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIE")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIE")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pie")
endif()
if(NOT BUILD_OWNCLOUD_OSX_BUNDLE)

View File

@@ -638,7 +638,11 @@ void AccountSettings::slotAccountStateChanged()
Utility::escape(safeUrl.toString()));
QString serverWithUser = server;
if (AbstractCredentials *cred = account->credentials()) {
serverWithUser = tr("%1 as <i>%2</i>").arg(server, Utility::escape(cred->user()));
QString user = account->davDisplayName();
if (user.isEmpty()) {
user = cred->user();
}
serverWithUser = tr("%1 as <i>%2</i>").arg(server, Utility::escape(user));
}
if (state == AccountState::Connected) {

View File

@@ -361,23 +361,4 @@ std::unique_ptr<QSettings> AccountState::settings()
return s;
}
QString AccountState::shortDisplayNameForSettings(int width) const
{
QString user = account()->credentials()->user();
QString host = account()->url().host();
int port = account()->url().port();
if (port > 0 && port != 80 && port != 443) {
host.append(QLatin1Char(':'));
host.append(QString::number(port));
}
if (width > 0) {
QFont f;
QFontMetrics fm(f);
host = fm.elidedText(host, Qt::ElideMiddle, width);
user = fm.elidedText(user, Qt::ElideRight, width);
}
return user + QLatin1String("\n") + host;
}
} // namespace OCC

View File

@@ -113,11 +113,6 @@ public:
/** Returns a new settings object for this account, already in the right groups. */
std::unique_ptr<QSettings> settings();
/** display name with two lines that is displayed in the settings
* If width is bigger than 0, the string will be ellided so it does not exceed that width
*/
QString shortDisplayNameForSettings(int width = 0) const;
/** Mark the timestamp when the last successful ETag check happened for
* this account.
* The checkConnectivity() method uses the timestamp to save a call to

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

@@ -841,6 +841,11 @@ 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);

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

@@ -207,13 +207,23 @@ void ownCloudGui::slotComputeOverallSyncStatus()
{
bool allSignedOut = true;
bool allPaused = true;
bool allDisconnected = true;
QVector<AccountStatePtr> problemAccounts;
auto setStatusText = [&](const QString &text) {
// Don't overwrite the status if we're currently syncing
if (FolderMan::instance()->currentSyncFolder())
return;
_actionStatus->setText(text);
};
foreach (auto a, AccountManager::instance()->accounts()) {
if (!a->isSignedOut()) {
allSignedOut = false;
}
if (!a->isConnected()) {
problemAccounts.append(a);
} else {
allDisconnected = false;
}
}
foreach (Folder *f, FolderMan::instance()->map()) {
@@ -224,6 +234,11 @@ void ownCloudGui::slotComputeOverallSyncStatus()
if (!problemAccounts.empty()) {
_tray->setIcon(Theme::instance()->folderOfflineIcon(true, contextMenuVisible()));
if (allDisconnected) {
setStatusText(tr("Disconnected"));
} else {
setStatusText(tr("Disconnected from some accounts"));
}
#ifdef Q_OS_WIN
// Windows has a 128-char tray tooltip length limit.
QStringList accountNames;
@@ -250,10 +265,12 @@ void ownCloudGui::slotComputeOverallSyncStatus()
if (allSignedOut) {
_tray->setIcon(Theme::instance()->folderOfflineIcon(true, contextMenuVisible()));
_tray->setToolTip(tr("Please sign in"));
setStatusText(tr("Signed out"));
return;
} else if (allPaused) {
_tray->setIcon(Theme::instance()->syncStateIcon(SyncResult::Paused, true, contextMenuVisible()));
_tray->setToolTip(tr("Account synchronization is disabled"));
setStatusText(tr("Synchronization is paused"));
return;
}
@@ -261,34 +278,40 @@ void ownCloudGui::slotComputeOverallSyncStatus()
QString trayMessage;
FolderMan *folderMan = FolderMan::instance();
Folder::Map map = folderMan->map();
SyncResult overallResult = FolderMan::accountStatus(map.values());
SyncResult::Status overallResult = FolderMan::accountStatus(map.values()).status();
// create the tray blob message, check if we have an defined state
if (overallResult.status() != SyncResult::Undefined) {
if (map.count() > 0) {
if (overallResult != SyncResult::Undefined && map.count() > 0) {
#ifdef Q_OS_WIN
// Windows has a 128-char tray tooltip length limit.
trayMessage = folderMan->statusToString(overallResult.status(), false);
// Windows has a 128-char tray tooltip length limit.
trayMessage = folderMan->statusToString(overallResult, false);
#else
QStringList allStatusStrings;
foreach (Folder *folder, map.values()) {
QString folderMessage = folderMan->statusToString(folder->syncResult().status(), folder->syncPaused());
allStatusStrings += tr("Folder %1: %2").arg(folder->shortGuiLocalPath(), folderMessage);
}
trayMessage = allStatusStrings.join(QLatin1String("\n"));
#endif
} else {
trayMessage = tr("No sync folders configured.");
QStringList allStatusStrings;
foreach (Folder *folder, map.values()) {
QString folderMessage = folderMan->statusToString(folder->syncResult().status(), folder->syncPaused());
allStatusStrings += tr("Folder %1: %2").arg(folder->shortGuiLocalPath(), folderMessage);
}
trayMessage = allStatusStrings.join(QLatin1String("\n"));
#endif
QIcon statusIcon = Theme::instance()->syncStateIcon(overallResult.status(), true, contextMenuVisible());
QIcon statusIcon = Theme::instance()->syncStateIcon(overallResult, true, contextMenuVisible());
_tray->setIcon(statusIcon);
_tray->setToolTip(trayMessage);
if (overallResult == SyncResult::Success || overallResult == SyncResult::Problem) {
setStatusText(tr("Up to date"));
} else if (overallResult == SyncResult::Paused) {
setStatusText(tr("Synchronization is paused"));
} else {
setStatusText(tr("Error during synchronization"));
}
} else {
// undefined because there are no folders.
QIcon icon = Theme::instance()->syncStateIcon(SyncResult::Problem, true, contextMenuVisible());
if (overallResult == SyncResult::Undefined)
overallResult = SyncResult::Problem;
QIcon icon = Theme::instance()->syncStateIcon(overallResult, true, contextMenuVisible());
_tray->setIcon(icon);
_tray->setToolTip(tr("There are no sync folders configured."));
setStatusText(tr("No sync folders configured"));
}
}
@@ -550,11 +573,13 @@ void ownCloudGui::updateContextMenu()
_contextMenu->addSeparator();
_contextMenu->addAction(_actionStatus);
if (isConfigured && atLeastOneConnected) {
_contextMenu->addAction(_actionStatus);
_contextMenu->addMenu(_recentActionsMenu);
_contextMenu->addSeparator();
}
_contextMenu->addSeparator();
if (accountList.isEmpty()) {
_contextMenu->addAction(_actionNewAccountWizard);
}
@@ -742,7 +767,7 @@ void ownCloudGui::slotUpdateProgress(const QString &folder, const ProgressInfo &
.arg(progress._currentDiscoveredFolder));
}
} else if (progress.status() == ProgressInfo::Done) {
QTimer::singleShot(2000, this, &ownCloudGui::slotDisplayIdle);
QTimer::singleShot(2000, this, &ownCloudGui::slotComputeOverallSyncStatus);
}
if (progress.status() != ProgressInfo::Propagation) {
return;
@@ -812,11 +837,6 @@ void ownCloudGui::slotUpdateProgress(const QString &folder, const ProgressInfo &
}
}
void ownCloudGui::slotDisplayIdle()
{
_actionStatus->setText(tr("Up to date"));
}
void ownCloudGui::slotLogin()
{
if (auto account = qvariant_cast<AccountStatePtr>(sender()->property(propertyAccountC))) {

View File

@@ -98,7 +98,6 @@ public slots:
void slotRemoveDestroyedShareDialogs();
private slots:
void slotDisplayIdle();
void slotLogin();
void slotLogout();
void slotUnpauseAllFolders();

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)

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

@@ -55,6 +55,8 @@ static const float buttonSizeRatio = 1.618; // golden ratio
namespace OCC {
#include "settingsdialogcommon.cpp"
static QIcon circleMask(const QImage &avatar)
{
int dim = avatar.width();
@@ -236,7 +238,7 @@ void SettingsDialog::accountAdded(AccountState *s)
if (!brandingSingleAccount) {
accountAction->setToolTip(s->account()->displayName());
accountAction->setIconText(s->shortDisplayNameForSettings(height * buttonSizeRatio));
accountAction->setIconText(SettingsDialogCommon::shortDisplayNameForSettings(s->account().data(), height * buttonSizeRatio));
}
_toolBar->insertAction(_toolBar->actions().at(0), accountAction);
auto accountSettings = new AccountSettings(s, this);
@@ -250,6 +252,7 @@ void SettingsDialog::accountAdded(AccountState *s)
_gui, &ownCloudGui::slotFolderOpenAction);
connect(accountSettings, &AccountSettings::showIssuesList, this, &SettingsDialog::showIssuesList);
connect(s->account().data(), &Account::accountChangedAvatar, this, &SettingsDialog::slotAccountAvatarChanged);
connect(s->account().data(), &Account::accountChangedDisplayName, this, &SettingsDialog::slotAccountDisplayNameChanged);
slotRefreshActivity(s);
}
@@ -268,6 +271,20 @@ void SettingsDialog::slotAccountAvatarChanged()
}
}
void SettingsDialog::slotAccountDisplayNameChanged()
{
Account *account = static_cast<Account *>(sender());
if (account && _actionForAccount.contains(account)) {
QAction *action = _actionForAccount[account];
if (action) {
QString displayName = account->displayName();
action->setText(displayName);
auto height = _toolBar->sizeHint().height();
action->setIconText(SettingsDialogCommon::shortDisplayNameForSettings(account, height * buttonSizeRatio));
}
}
}
void SettingsDialog::accountRemoved(AccountState *s)
{
for (auto it = _actionGroupWidgets.begin(); it != _actionGroupWidgets.end(); ++it) {

View File

@@ -60,6 +60,7 @@ public slots:
void slotSwitchPage(QAction *action);
void slotRefreshActivity(AccountState *accountState);
void slotAccountAvatarChanged();
void slotAccountDisplayNameChanged();
protected:
void reject() Q_DECL_OVERRIDE;

View File

@@ -0,0 +1,28 @@
namespace SettingsDialogCommon
{
/** display name with two lines that is displayed in the settings
* If width is bigger than 0, the string will be ellided so it does not exceed that width
*/
QString shortDisplayNameForSettings(Account* account, int width)
{
QString user = account->davDisplayName();
if (user.isEmpty()) {
user = account->credentials()->user();
}
QString host = account->url().host();
int port = account->url().port();
if (port > 0 && port != 80 && port != 443) {
host.append(QLatin1Char(':'));
host.append(QString::number(port));
}
if (width > 0) {
QFont f;
QFontMetrics fm(f);
host = fm.elidedText(host, Qt::ElideMiddle, width);
user = fm.elidedText(user, Qt::ElideRight, width);
}
return user + QLatin1String("\n") + host;
}
}

View File

@@ -38,6 +38,9 @@
namespace OCC {
#include "settingsdialogcommon.cpp"
// Duplicate in settingsdialog.cpp
static QIcon circleMask(const QImage &avatar)
{
@@ -146,7 +149,7 @@ void SettingsDialogMac::accountAdded(AccountState *s)
QIcon accountIcon = MacStandardIcon::icon(MacStandardIcon::UserAccounts);
auto accountSettings = new AccountSettings(s, this);
QString displayName = Theme::instance()->multiAccount() ? s->shortDisplayNameForSettings() : tr("Account");
QString displayName = Theme::instance()->multiAccount() ? SettingsDialogCommon::shortDisplayNameForSettings(s->account().data(), 0) : tr("Account");
insertPreferencesPanel(0, accountIcon, displayName, accountSettings);
@@ -154,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);
}
@@ -192,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

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

@@ -105,7 +105,7 @@ void Account::setAvatar(const QImage &img)
QString Account::displayName() const
{
QString dn = QString("%1@%2").arg(_credentials->user(), _url.host());
QString dn = QString("%1@%2").arg(davUser(), _url.host());
int port = url().port();
if (port > 0 && port != 80 && port != 443) {
dn.append(QLatin1Char(':'));
@@ -114,6 +114,17 @@ QString Account::displayName() const
return dn;
}
QString Account::davDisplayName() const
{
return _displayName;
}
void Account::setDavDisplayName(const QString &newDisplayName)
{
_displayName = newDisplayName;
emit accountChangedDisplayName();
}
QString Account::id() const
{
return _id;
@@ -165,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)));
}
@@ -300,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

@@ -83,6 +83,9 @@ public:
QString davUser() const;
void setDavUser(const QString &newDavUser);
QString davDisplayName() const;
void setDavDisplayName(const QString &newDisplayName);
QImage avatar() const;
void setAvatar(const QImage &img);
@@ -96,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.
@@ -209,7 +215,7 @@ public:
/** True when the server supports HTTP2 */
bool isHttp2Supported() { return _http2Supported; }
void setHttp2Supported(bool value) { _http2Supported = value; };
void setHttp2Supported(bool value) { _http2Supported = value; }
void clearCookieJar();
void lendCookieJarTo(QNetworkAccessManager *guest);
@@ -246,6 +252,7 @@ signals:
void serverVersionChanged(Account *account, const QString &newVersion, const QString &oldVersion);
void accountChangedAvatar();
void accountChangedDisplayName();
protected Q_SLOTS:
void slotCredentialsFetched();
@@ -258,9 +265,19 @@ private:
QWeakPointer<Account> _sharedThis;
QString _id;
QString _davUser;
QString _displayName;
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

@@ -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);
@@ -316,6 +340,10 @@ void ConnectionValidator::slotUserFetched(const QJsonDocument &json)
job->start();
}
QString displayName = json.object().value("ocs").toObject().value("data").toObject().value("display-name").toString();
if (!displayName.isEmpty()) {
_account->setDavDisplayName(displayName);
}
}
void ConnectionValidator::slotAvatarImage(const QImage &img)

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

@@ -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;
}
@@ -625,6 +648,9 @@ void PropagateDownloadFile::slotGetFinished()
connect(validator, &ValidateChecksumHeader::validationFailed,
this, &PropagateDownloadFile::slotChecksumFail);
auto checksumHeader = 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;
}
@@ -960,11 +960,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 +1031,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 +1464,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) {

View File

@@ -105,8 +105,6 @@ signals:
// 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 &);

View File

@@ -428,7 +428,15 @@ QPixmap Theme::wizardHeaderBanner() const
if (!c.isValid())
return QPixmap();
QPixmap pix(QSize(750, 78));
QSize size(750, 78);
if (auto screen = qApp->primaryScreen()) {
// Adjust the the size if there is a different DPI. (Issue #6156)
// Indeed, this size need to be big enough to for the banner height, and the wizard's width
auto ratio = screen->logicalDotsPerInch() / 96.;
if (ratio > 1.)
size *= ratio;
}
QPixmap pix(size);
pix.fill(wizardHeaderBackgroundColor());
return pix;
}

View File

@@ -46,6 +46,7 @@ owncloud_add_test(ExcludedFiles "")
owncloud_add_test(FileSystem "")
owncloud_add_test(Utility "")
owncloud_add_test(SyncEngine "syncenginetestutils.h")
owncloud_add_test(SyncMove "syncenginetestutils.h")
owncloud_add_test(SyncFileStatusTracker "syncenginetestutils.h")
owncloud_add_test(ChunkingNg "syncenginetestutils.h")
owncloud_add_test(UploadReset "syncenginetestutils.h")

View File

@@ -48,6 +48,7 @@ assert($txtpropbefore);
assert($emlpropbefore);
printInfo( "Touch local files");
system( "sleep 1" );
system( "touch $locDir/test.txt" );
system( "touch $locDir/test.eml" );

View File

@@ -161,12 +161,15 @@ public:
FileInfo(const QString &name, qint64 size) : name{name}, isDir{false}, size{size} { }
FileInfo(const QString &name, qint64 size, char contentChar) : name{name}, isDir{false}, size{size}, contentChar{contentChar} { }
FileInfo(const QString &name, const std::initializer_list<FileInfo> &children) : name{name} {
QString p = path();
for (const auto &source : children) {
auto &dest = this->children[source.name] = source;
dest.parentPath = p;
dest.fixupParentPathRecursively();
}
for (const auto &source : children)
addChild(source);
}
void addChild(const FileInfo &info)
{
auto &dest = this->children[info.name] = info;
dest.parentPath = path();
dest.fixupParentPathRecursively();
}
void remove(const QString &relativePath) override {
@@ -433,6 +436,8 @@ public:
abort();
return;
}
fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong());
remoteRootFileInfo.find(fileName, /*invalidate_etags=*/true);
QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
}
@@ -597,6 +602,9 @@ public:
size -= len;
return len;
}
// useful to be public for testing
using QNetworkReply::setRawHeader;
};
@@ -606,8 +614,10 @@ class FakeChunkMoveReply : public QNetworkReply
FileInfo *fileInfo;
public:
FakeChunkMoveReply(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo,
QNetworkAccessManager::Operation op, const QNetworkRequest &request,
QObject *parent) : QNetworkReply{parent} {
QNetworkAccessManager::Operation op, const QNetworkRequest &request,
quint64 delayMs, QObject *parent)
: QNetworkReply{ parent }
{
setRequest(request);
setUrl(request.url());
setOperation(op);
@@ -662,7 +672,10 @@ public:
abort();
return;
}
QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong());
remoteRootFileInfo.find(fileName, /*invalidate_etags=*/true);
QTimer::singleShot(delayMs, this, &FakeChunkMoveReply::respond);
}
Q_INVOKABLE void respond() {
@@ -713,6 +726,24 @@ public:
int _httpErrorCode;
};
// A reply that never responds
class FakeHangingReply : public QNetworkReply
{
Q_OBJECT
public:
FakeHangingReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
: QNetworkReply(parent)
{
setRequest(request);
setUrl(request.url());
setOperation(op);
open(QIODevice::ReadOnly);
}
void abort() override {}
qint64 readData(char *, qint64) override { return 0; }
};
class FakeQNAM : public QNetworkAccessManager
{
public:
@@ -765,7 +796,7 @@ protected:
else if (verb == QLatin1String("MOVE") && !isUpload)
return new FakeMoveReply{info, op, request, this};
else if (verb == QLatin1String("MOVE") && isUpload)
return new FakeChunkMoveReply{info, _remoteRootFileInfo, op, request, this};
return new FakeChunkMoveReply{ info, _remoteRootFileInfo, op, request, 0, this };
else {
qDebug() << verb << outgoingData;
Q_UNREACHABLE();
@@ -924,6 +955,11 @@ private:
} else {
QFile f{diskChild.filePath()};
f.open(QFile::ReadOnly);
auto content = f.read(1);
if (content.size() == 0) {
qWarning() << "Empty file at:" << diskChild.filePath();
continue;
}
char contentChar = f.read(1).at(0);
templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName(), diskChild.size(), contentChar});
}

View File

@@ -85,6 +85,136 @@ private slots:
QCOMPARE(fakeFolder.uploadState().children.first().name, chunkingId);
}
// Check what happens when we abort during the final MOVE and the
// the final MOVE takes longer than the abort-delay
void testLateAbortHard()
{
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } }, { "checksums", QVariantMap{ { "supportedTypes", QStringList() << "SHA1" } } } });
const int size = 150 * 1000 * 1000;
// Make the MOVE never reply, but trigger a client-abort and apply the change remotely
auto parent = new QObject;
QByteArray moveChecksumHeader;
int nGET = 0;
int responseDelay = 10000; // bigger than abort-wait timeout
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request) -> QNetworkReply * {
if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") {
QTimer::singleShot(50, parent, [&]() { fakeFolder.syncEngine().abort(); });
moveChecksumHeader = request.rawHeader("OC-Checksum");
return new FakeChunkMoveReply(fakeFolder.uploadState(), fakeFolder.remoteModifier(), op, request, responseDelay, parent);
} else if (op == QNetworkAccessManager::GetOperation) {
nGET++;
}
return nullptr;
});
// Test 1: NEW file aborted
fakeFolder.localModifier().insert("A/a0", size);
QVERIFY(!fakeFolder.syncOnce()); // error: abort!
// Now the next sync gets a NEW/NEW conflict and since there's no checksum
// it just becomes a UPDATE_METADATA
auto checkEtagUpdated = [&](SyncFileItemVector &items) {
QCOMPARE(items.size(), 1);
QCOMPARE(items[0]->_file, QLatin1String("A"));
SyncJournalFileRecord record;
QVERIFY(fakeFolder.syncJournal().getFileRecord(QByteArray("A/a0"), &record));
QCOMPARE(record._etag, fakeFolder.remoteModifier().find("A/a0")->etag.toUtf8());
};
auto connection = connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, checkEtagUpdated);
QVERIFY(fakeFolder.syncOnce());
disconnect(connection);
QCOMPARE(nGET, 0);
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// Test 2: modified file upload aborted
fakeFolder.localModifier().appendByte("A/a0");
QVERIFY(!fakeFolder.syncOnce()); // error: abort!
// An EVAL/EVAL conflict is also UPDATE_METADATA when there's no checksums
connection = connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, checkEtagUpdated);
QVERIFY(fakeFolder.syncOnce());
disconnect(connection);
QCOMPARE(nGET, 0);
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// Test 3: modified file upload aborted, with good checksums
fakeFolder.localModifier().appendByte("A/a0");
QVERIFY(!fakeFolder.syncOnce()); // error: abort!
// Set the remote checksum -- the test setup doesn't do it automatically
QVERIFY(!moveChecksumHeader.isEmpty());
fakeFolder.remoteModifier().find("A/a0")->checksums = moveChecksumHeader;
// This time it's a real conflict, we have a remote checksum!
connection = connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, [&](SyncFileItemVector &items) {
SyncFileItemPtr a0;
for (auto &item : items) {
if (item->_file == "A/a0")
a0 = item;
}
QVERIFY(a0);
QCOMPARE(a0->_instruction, CSYNC_INSTRUCTION_CONFLICT);
});
QVERIFY(fakeFolder.syncOnce());
disconnect(connection);
QCOMPARE(nGET, 0); // no new download, just a metadata update!
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// Test 4: New file, that gets deleted locally before the next sync
fakeFolder.localModifier().insert("A/a3", size);
QVERIFY(!fakeFolder.syncOnce()); // error: abort!
fakeFolder.localModifier().remove("A/a3");
// bug: in this case we must expect a re-download of A/A3
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nGET, 1);
QVERIFY(fakeFolder.currentLocalState().find("A/a3"));
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
// Check what happens when we abort during the final MOVE and the
// the final MOVE is short enough for the abort-delay to help
void testLateAbortRecoverable()
{
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } }, { "checksums", QVariantMap{ { "supportedTypes", QStringList() << "SHA1" } } } });
const int size = 150 * 1000 * 1000;
// Make the MOVE never reply, but trigger a client-abort and apply the change remotely
auto parent = new QObject;
QByteArray moveChecksumHeader;
int nGET = 0;
int responseDelay = 2000; // smaller than abort-wait timeout
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request) -> QNetworkReply * {
if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") {
QTimer::singleShot(50, parent, [&]() { fakeFolder.syncEngine().abort(); });
moveChecksumHeader = request.rawHeader("OC-Checksum");
return new FakeChunkMoveReply(fakeFolder.uploadState(), fakeFolder.remoteModifier(), op, request, responseDelay, parent);
} else if (op == QNetworkAccessManager::GetOperation) {
nGET++;
}
return nullptr;
});
// Test 1: NEW file aborted
fakeFolder.localModifier().insert("A/a0", size);
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// Test 2: modified file upload aborted
fakeFolder.localModifier().appendByte("A/a0");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
// We modify the file locally after it has been partially uploaded
void testRemoveStale1() {

View File

@@ -16,7 +16,7 @@ bool itemDidComplete(const QSignalSpy &spy, const QString &path)
for(const QList<QVariant> &args : spy) {
auto item = args[0].value<SyncFileItemPtr>();
if (item->destination() == path)
return true;
return item->_instruction != CSYNC_INSTRUCTION_NONE && item->_instruction != CSYNC_INSTRUCTION_UPDATE_METADATA;
}
return false;
}
@@ -141,98 +141,6 @@ private slots:
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
void testRemoteChangeInMovedFolder() {
// issue #5192
FakeFolder fakeFolder{FileInfo{ QString(), {
FileInfo { QStringLiteral("folder"), {
FileInfo{ QStringLiteral("folderA"), { { QStringLiteral("file.txt"), 400 } } },
QStringLiteral("folderB")
}
}}}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// Edit a file in a moved directory.
fakeFolder.remoteModifier().setContents("folder/folderA/file.txt", 'a');
fakeFolder.remoteModifier().rename("folder/folderA", "folder/folderB/folderA");
fakeFolder.syncOnce();
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
auto oldState = fakeFolder.currentLocalState();
QVERIFY(oldState.find("folder/folderB/folderA/file.txt"));
QVERIFY(!oldState.find("folder/folderA/file.txt"));
// This sync should not remove the file
fakeFolder.syncOnce();
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(fakeFolder.currentLocalState(), oldState);
}
void testSelectiveSyncModevFolder() {
// issue #5224
FakeFolder fakeFolder{FileInfo{ QString(), {
FileInfo { QStringLiteral("parentFolder"), {
FileInfo{ QStringLiteral("subFolderA"), { { QStringLiteral("fileA.txt"), 400 } } },
FileInfo{ QStringLiteral("subFolderB"), { { QStringLiteral("fileB.txt"), 400 } } }
}
}}}};
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
auto expectedServerState = fakeFolder.currentRemoteState();
// Remove subFolderA with selectiveSync:
fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList,
{"parentFolder/subFolderA/"});
fakeFolder.syncEngine().journal()->avoidReadFromDbOnNextSync(QByteArrayLiteral("parentFolder/subFolderA/"));
fakeFolder.syncOnce();
{
// Nothing changed on the server
QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
// The local state should not have subFolderA
auto remoteState = fakeFolder.currentRemoteState();
remoteState.remove("parentFolder/subFolderA");
QCOMPARE(fakeFolder.currentLocalState(), remoteState);
}
// Rename parentFolder on the server
fakeFolder.remoteModifier().rename("parentFolder", "parentFolderRenamed");
expectedServerState = fakeFolder.currentRemoteState();
fakeFolder.syncOnce();
{
QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
auto remoteState = fakeFolder.currentRemoteState();
// The subFolderA should still be there on the server.
QVERIFY(remoteState.find("parentFolderRenamed/subFolderA/fileA.txt"));
// But not on the client because of the selective sync
remoteState.remove("parentFolderRenamed/subFolderA");
QCOMPARE(fakeFolder.currentLocalState(), remoteState);
}
// Rename it again, locally this time.
fakeFolder.localModifier().rename("parentFolderRenamed", "parentThirdName");
fakeFolder.syncOnce();
{
auto remoteState = fakeFolder.currentRemoteState();
// The subFolderA should still be there on the server.
QVERIFY(remoteState.find("parentThirdName/subFolderA/fileA.txt"));
// But not on the client because of the selective sync
remoteState.remove("parentThirdName/subFolderA");
QCOMPARE(fakeFolder.currentLocalState(), remoteState);
expectedServerState = fakeFolder.currentRemoteState();
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
fakeFolder.syncOnce(); // This sync should do nothing
QCOMPARE(completeSpy.count(), 0);
QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
QCOMPARE(fakeFolder.currentLocalState(), remoteState);
}
}
void testSelectiveSyncBug() {
// issue owncloud/enterprise#1965: files from selective-sync ignored
// folders are uploaded anyway is some circumstances.
@@ -343,8 +251,53 @@ private slots:
}
}
void testFakeConflict_data()
{
QTest::addColumn<bool>("sameMtime");
QTest::addColumn<QByteArray>("checksums");
QTest::addColumn<int>("expectedGET");
QTest::newRow("Same mtime, but no server checksum -> ignored in reconcile")
<< true << QByteArray()
<< 0;
QTest::newRow("Same mtime, weak server checksum differ -> downloaded")
<< true << QByteArray("Adler32:bad")
<< 1;
QTest::newRow("Same mtime, matching weak checksum -> skipped")
<< true << QByteArray("Adler32:2a2010d")
<< 0;
QTest::newRow("Same mtime, strong server checksum differ -> downloaded")
<< true << QByteArray("SHA1:bad")
<< 1;
QTest::newRow("Same mtime, matching strong checksum -> skipped")
<< true << QByteArray("SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427")
<< 0;
QTest::newRow("mtime changed, but no server checksum -> download")
<< false << QByteArray()
<< 1;
QTest::newRow("mtime changed, weak checksum match -> download anyway")
<< false << QByteArray("Adler32:2a2010d")
<< 1;
QTest::newRow("mtime changed, strong checksum match -> skip")
<< false << QByteArray("SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427")
<< 0;
}
void testFakeConflict()
{
QFETCH(bool, sameMtime);
QFETCH(QByteArray, checksums);
QFETCH(int, expectedGET);
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
int nGET = 0;
@@ -361,51 +314,25 @@ private slots:
auto mtime = QDateTime::currentDateTimeUtc().addDays(-4);
mtime.setMSecsSinceEpoch(mtime.toMSecsSinceEpoch() / 1000 * 1000);
// Conflict: Same content, mtime, but no server checksum
// -> ignored in reconcile
fakeFolder.localModifier().setContents("A/a1", 'C');
fakeFolder.localModifier().setModTime("A/a1", mtime);
fakeFolder.remoteModifier().setContents("A/a1", 'C');
if (!sameMtime)
mtime = mtime.addDays(1);
fakeFolder.remoteModifier().setModTime("A/a1", mtime);
remoteInfo.find("A/a1")->checksums = checksums;
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nGET, 0);
QCOMPARE(nGET, expectedGET);
// Conflict: Same content, mtime, but weak server checksum
// -> ignored in reconcile
mtime = mtime.addDays(1);
fakeFolder.localModifier().setContents("A/a1", 'D');
fakeFolder.localModifier().setModTime("A/a1", mtime);
fakeFolder.remoteModifier().setContents("A/a1", 'D');
fakeFolder.remoteModifier().setModTime("A/a1", mtime);
remoteInfo.find("A/a1")->checksums = "Adler32:bad";
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nGET, 0);
// Conflict: Same content, mtime, but server checksum differs
// -> downloaded
mtime = mtime.addDays(1);
fakeFolder.localModifier().setContents("A/a1", 'W');
fakeFolder.localModifier().setModTime("A/a1", mtime);
fakeFolder.remoteModifier().setContents("A/a1", 'W');
fakeFolder.remoteModifier().setModTime("A/a1", mtime);
remoteInfo.find("A/a1")->checksums = "SHA1:bad";
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nGET, 1);
// Conflict: Same content, mtime, matching checksums
// -> PropagateDownload, but it skips the download
mtime = mtime.addDays(1);
fakeFolder.localModifier().setContents("A/a1", 'C');
fakeFolder.localModifier().setModTime("A/a1", mtime);
fakeFolder.remoteModifier().setContents("A/a1", 'C');
fakeFolder.remoteModifier().setModTime("A/a1", mtime);
remoteInfo.find("A/a1")->checksums = "SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427";
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nGET, 1);
// check that mtime in journal and filesystem agree
QString a1path = fakeFolder.localPath() + "A/a1";
SyncJournalFileRecord a1record;
fakeFolder.syncJournal().getFileRecord(QByteArray("A/a1"), &a1record);
QCOMPARE(a1record._modtime, (qint64)FileSystem::getModTime(a1path));
// Extra sync reads from db, no difference
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nGET, 1);
QCOMPARE(nGET, expectedGET);
}
/**
@@ -535,6 +462,116 @@ private slots:
QCOMPARE(nPUT, 6);
QCOMPARE(n507, 3);
}
// Checks whether downloads with bad checksums are accepted
void testChecksumValidation()
{
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
QObject parent;
QByteArray checksumValue;
QByteArray contentMd5Value;
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request) -> QNetworkReply * {
if (op == QNetworkAccessManager::GetOperation) {
auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent);
if (!checksumValue.isNull())
reply->setRawHeader("OC-Checksum", checksumValue);
if (!contentMd5Value.isNull())
reply->setRawHeader("Content-MD5", contentMd5Value);
return reply;
}
return nullptr;
});
// Basic case
fakeFolder.remoteModifier().create("A/a3", 16, 'A');
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// Bad OC-Checksum
checksumValue = "SHA1:bad";
fakeFolder.remoteModifier().create("A/a4", 16, 'A');
QVERIFY(!fakeFolder.syncOnce());
// Good OC-Checksum
checksumValue = "SHA1:19b1928d58a2030d08023f3d7054516dbc186f20"; // printf 'A%.0s' {1..16} | sha1sum -
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
checksumValue = QByteArray();
// Bad Content-MD5
contentMd5Value = "bad";
fakeFolder.remoteModifier().create("A/a5", 16, 'A');
QVERIFY(!fakeFolder.syncOnce());
// Good Content-MD5
contentMd5Value = "d8a73157ce10cd94a91c2079fc9a92c8"; // printf 'A%.0s' {1..16} | md5sum -
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// OC-Checksum has preference
checksumValue = "garbage";
// contentMd5Value is still good
fakeFolder.remoteModifier().create("A/a6", 16, 'A');
QVERIFY(!fakeFolder.syncOnce());
}
// Tests the behavior of invalid filename detection
void testInvalidFilenameRegex()
{
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
// For current servers, no characters are forbidden
fakeFolder.syncEngine().account()->setServerVersion("10.0.0");
fakeFolder.localModifier().insert("A/\\:?*\"<>|.txt");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// For legacy servers, some characters were forbidden by the client
fakeFolder.syncEngine().account()->setServerVersion("8.0.0");
fakeFolder.localModifier().insert("B/\\:?*\"<>|.txt");
QVERIFY(fakeFolder.syncOnce());
QVERIFY(!fakeFolder.currentRemoteState().find("B/\\:?*\"<>|.txt"));
// We can override that by setting the capability
fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "" } } } });
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// Check that new servers also accept the capability
fakeFolder.syncEngine().account()->setServerVersion("10.0.0");
fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "my[fgh]ile" } } } });
fakeFolder.localModifier().insert("C/myfile.txt");
QVERIFY(fakeFolder.syncOnce());
QVERIFY(!fakeFolder.currentRemoteState().find("C/myfile.txt"));
}
void testDiscoveryHiddenFile()
{
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// We can't depend on currentLocalState for hidden files since
// it should rightfully skip things like download temporaries
auto localFileExists = [&](QString name) {
return QFileInfo(fakeFolder.localPath() + name).exists();
};
fakeFolder.syncEngine().setIgnoreHiddenFiles(true);
fakeFolder.remoteModifier().insert("A/.hidden");
fakeFolder.localModifier().insert("B/.hidden");
QVERIFY(fakeFolder.syncOnce());
QVERIFY(!localFileExists("A/.hidden"));
QVERIFY(!fakeFolder.currentRemoteState().find("B/.hidden"));
fakeFolder.syncEngine().setIgnoreHiddenFiles(false);
fakeFolder.syncJournal().forceRemoteDiscoveryNextSync();
QVERIFY(fakeFolder.syncOnce());
QVERIFY(localFileExists("A/.hidden"));
QVERIFY(fakeFolder.currentRemoteState().find("B/.hidden"));
}
};
QTEST_GUILESS_MAIN(TestSyncEngine)

View File

@@ -436,6 +436,8 @@ private slots:
fakeFolder.remoteModifier().appendByte("S/s1");
fakeFolder.remoteModifier().insert("B/b3");
fakeFolder.remoteModifier().find("B/b3")->extraDavProperties = "<oc:share-types><oc:share-type>0</oc:share-type></oc:share-types>";
fakeFolder.remoteModifier().find("A/a1")->isShared = true; // becomes shared
fakeFolder.remoteModifier().find("A", true); // change the etags of the parent
StatusPushSpy statusSpy(fakeFolder.syncEngine());
@@ -458,6 +460,7 @@ private slots:
QCOMPARE(statusSpy.statusOf("S/s1"), sharedUpToDateStatus);
QCOMPARE(statusSpy.statusOf("B/b1").shared(), false);
QCOMPARE(statusSpy.statusOf("B/b3"), sharedUpToDateStatus);
QCOMPARE(statusSpy.statusOf("A/a1"), sharedUpToDateStatus);
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}

569
test/testsyncmove.cpp Normal file
View File

@@ -0,0 +1,569 @@
/*
* This software is in the public domain, furnished "as is", without technical
* support, and with no warranty, express or implied, as to its usefulness for
* any purpose.
*
*/
#include <QtTest>
#include "syncenginetestutils.h"
#include <syncengine.h>
using namespace OCC;
SyncFileItemPtr findItem(const QSignalSpy &spy, const QString &path)
{
for (const QList<QVariant> &args : spy) {
auto item = args[0].value<SyncFileItemPtr>();
if (item->destination() == path)
return item;
}
return SyncFileItemPtr(new SyncFileItem);
}
bool itemSuccessful(const QSignalSpy &spy, const QString &path, const csync_instructions_e instr)
{
auto item = findItem(spy, path);
return item->_status == SyncFileItem::Success && item->_instruction == instr;
}
bool itemConflict(const QSignalSpy &spy, const QString &path)
{
auto item = findItem(spy, path);
return item->_status == SyncFileItem::Conflict && item->_instruction == CSYNC_INSTRUCTION_CONFLICT;
}
bool itemSuccessfulMove(const QSignalSpy &spy, const QString &path)
{
return itemSuccessful(spy, path, CSYNC_INSTRUCTION_RENAME);
}
QStringList findConflicts(const FileInfo &dir)
{
QStringList conflicts;
for (const auto &item : dir.children) {
if (item.name.contains("conflict")) {
conflicts.append(item.path());
}
}
return conflicts;
}
bool expectAndWipeConflict(FileModifier &local, FileInfo state, const QString path)
{
PathComponents pathComponents(path);
auto base = state.find(pathComponents.parentDirComponents());
if (!base)
return false;
for (const auto &item : base->children) {
if (item.name.startsWith(pathComponents.fileName()) && item.name.contains("_conflict")) {
local.remove(item.path());
return true;
}
}
return false;
}
class TestSyncMove : public QObject
{
Q_OBJECT
private slots:
void testRemoteChangeInMovedFolder()
{
// issue #5192
FakeFolder fakeFolder{ FileInfo{ QString(), { FileInfo{ QStringLiteral("folder"), { FileInfo{ QStringLiteral("folderA"), { { QStringLiteral("file.txt"), 400 } } }, QStringLiteral("folderB") } } } } };
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
// Edit a file in a moved directory.
fakeFolder.remoteModifier().setContents("folder/folderA/file.txt", 'a');
fakeFolder.remoteModifier().rename("folder/folderA", "folder/folderB/folderA");
fakeFolder.syncOnce();
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
auto oldState = fakeFolder.currentLocalState();
QVERIFY(oldState.find("folder/folderB/folderA/file.txt"));
QVERIFY(!oldState.find("folder/folderA/file.txt"));
// This sync should not remove the file
fakeFolder.syncOnce();
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(fakeFolder.currentLocalState(), oldState);
}
void testSelectiveSyncMovedFolder()
{
// issue #5224
FakeFolder fakeFolder{ FileInfo{ QString(), { FileInfo{ QStringLiteral("parentFolder"), { FileInfo{ QStringLiteral("subFolderA"), { { QStringLiteral("fileA.txt"), 400 } } }, FileInfo{ QStringLiteral("subFolderB"), { { QStringLiteral("fileB.txt"), 400 } } } } } } } };
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
auto expectedServerState = fakeFolder.currentRemoteState();
// Remove subFolderA with selectiveSync:
fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList,
{ "parentFolder/subFolderA/" });
fakeFolder.syncEngine().journal()->avoidReadFromDbOnNextSync(QByteArrayLiteral("parentFolder/subFolderA/"));
fakeFolder.syncOnce();
{
// Nothing changed on the server
QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
// The local state should not have subFolderA
auto remoteState = fakeFolder.currentRemoteState();
remoteState.remove("parentFolder/subFolderA");
QCOMPARE(fakeFolder.currentLocalState(), remoteState);
}
// Rename parentFolder on the server
fakeFolder.remoteModifier().rename("parentFolder", "parentFolderRenamed");
expectedServerState = fakeFolder.currentRemoteState();
fakeFolder.syncOnce();
{
QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
auto remoteState = fakeFolder.currentRemoteState();
// The subFolderA should still be there on the server.
QVERIFY(remoteState.find("parentFolderRenamed/subFolderA/fileA.txt"));
// But not on the client because of the selective sync
remoteState.remove("parentFolderRenamed/subFolderA");
QCOMPARE(fakeFolder.currentLocalState(), remoteState);
}
// Rename it again, locally this time.
fakeFolder.localModifier().rename("parentFolderRenamed", "parentThirdName");
fakeFolder.syncOnce();
{
auto remoteState = fakeFolder.currentRemoteState();
// The subFolderA should still be there on the server.
QVERIFY(remoteState.find("parentThirdName/subFolderA/fileA.txt"));
// But not on the client because of the selective sync
remoteState.remove("parentThirdName/subFolderA");
QCOMPARE(fakeFolder.currentLocalState(), remoteState);
expectedServerState = fakeFolder.currentRemoteState();
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
fakeFolder.syncOnce(); // This sync should do nothing
QCOMPARE(completeSpy.count(), 0);
QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
QCOMPARE(fakeFolder.currentLocalState(), remoteState);
}
}
void testLocalMoveDetection()
{
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
int nPUT = 0;
int nDELETE = 0;
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &) {
if (op == QNetworkAccessManager::PutOperation)
++nPUT;
if (op == QNetworkAccessManager::DeleteOperation)
++nDELETE;
return nullptr;
});
// For directly editing the remote checksum
FileInfo &remoteInfo = fakeFolder.remoteModifier();
// Simple move causing a remote rename
fakeFolder.localModifier().rename("A/a1", "A/a1m");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
QCOMPARE(nPUT, 0);
// Move-and-change, causing a upload and delete
fakeFolder.localModifier().rename("A/a2", "A/a2m");
fakeFolder.localModifier().appendByte("A/a2m");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
QCOMPARE(nPUT, 1);
QCOMPARE(nDELETE, 1);
// Move-and-change, mtime+content only
fakeFolder.localModifier().rename("B/b1", "B/b1m");
fakeFolder.localModifier().setContents("B/b1m", 'C');
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
QCOMPARE(nPUT, 2);
QCOMPARE(nDELETE, 2);
// Move-and-change, size+content only
auto mtime = fakeFolder.remoteModifier().find("B/b2")->lastModified;
fakeFolder.localModifier().rename("B/b2", "B/b2m");
fakeFolder.localModifier().appendByte("B/b2m");
fakeFolder.localModifier().setModTime("B/b2m", mtime);
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
QCOMPARE(nPUT, 3);
QCOMPARE(nDELETE, 3);
// Move-and-change, content only -- c1 has no checksum, so we fail to detect this!
// NOTE: This is an expected failure.
mtime = fakeFolder.remoteModifier().find("C/c1")->lastModified;
fakeFolder.localModifier().rename("C/c1", "C/c1m");
fakeFolder.localModifier().setContents("C/c1m", 'C');
fakeFolder.localModifier().setModTime("C/c1m", mtime);
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nPUT, 3);
QCOMPARE(nDELETE, 3);
QVERIFY(!(fakeFolder.currentLocalState() == remoteInfo));
// cleanup, and upload a file that will have a checksum in the db
fakeFolder.localModifier().remove("C/c1m");
fakeFolder.localModifier().insert("C/c3");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
QCOMPARE(nPUT, 4);
QCOMPARE(nDELETE, 4);
// Move-and-change, content only, this time while having a checksum
mtime = fakeFolder.remoteModifier().find("C/c3")->lastModified;
fakeFolder.localModifier().rename("C/c3", "C/c3m");
fakeFolder.localModifier().setContents("C/c3m", 'C');
fakeFolder.localModifier().setModTime("C/c3m", mtime);
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nPUT, 5);
QCOMPARE(nDELETE, 5);
QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
}
// If the same folder is shared in two different ways with the same
// user, the target user will see duplicate file ids. We need to make
// sure the move detection and sync still do the right thing in that
// case.
void testDuplicateFileId()
{
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
auto &remote = fakeFolder.remoteModifier();
remote.mkdir("A/W");
remote.insert("A/W/w1");
remote.mkdir("A/Q");
// Duplicate every entry in A under O/A
remote.mkdir("O");
remote.children["O"].addChild(remote.children["A"]);
// This already checks that the rename detection doesn't get
// horribly confused if we add new files that have the same
// fileid as existing ones
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
int nGET = 0;
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &) {
if (op == QNetworkAccessManager::GetOperation)
++nGET;
return nullptr;
});
// Try a remote file move
remote.rename("A/a1", "A/W/a1m");
remote.rename("O/A/a1", "O/A/W/a1m");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 0);
// And a remote directory move
remote.rename("A/W", "A/Q/W");
remote.rename("O/A/W", "O/A/Q/W");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 0);
// Partial file removal (in practice, A/a2 may be moved to O/a2, but we don't care)
remote.rename("O/A/a2", "O/a2");
remote.remove("A/a2");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 0);
// Local change plus remote move at the same time
fakeFolder.localModifier().appendByte("O/a2");
remote.rename("O/a2", "O/a3");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 1);
}
void testMovePropagation()
{
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
auto &local = fakeFolder.localModifier();
auto &remote = fakeFolder.remoteModifier();
int nGET = 0;
int nPUT = 0;
int nMOVE = 0;
int nDELETE = 0;
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &req) {
if (op == QNetworkAccessManager::GetOperation)
++nGET;
if (op == QNetworkAccessManager::PutOperation)
++nPUT;
if (op == QNetworkAccessManager::DeleteOperation)
++nDELETE;
if (req.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE")
++nMOVE;
return nullptr;
});
auto resetCounters = [&]() {
nGET = nPUT = nMOVE = nDELETE = 0;
};
// Move
{
resetCounters();
local.rename("A/a1", "A/a1m");
remote.rename("B/b1", "B/b1m");
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 0);
QCOMPARE(nPUT, 0);
QCOMPARE(nMOVE, 1);
QCOMPARE(nDELETE, 0);
QVERIFY(itemSuccessfulMove(completeSpy, "A/a1m"));
QVERIFY(itemSuccessfulMove(completeSpy, "B/b1m"));
}
// Touch+Move on same side
resetCounters();
local.rename("A/a2", "A/a2m");
local.setContents("A/a2m", 'A');
remote.rename("B/b2", "B/b2m");
remote.setContents("B/b2m", 'A');
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 1);
QCOMPARE(nPUT, 1);
QCOMPARE(nMOVE, 0);
QCOMPARE(nDELETE, 1);
QCOMPARE(remote.find("A/a2m")->contentChar, 'A');
QCOMPARE(remote.find("B/b2m")->contentChar, 'A');
// Touch+Move on opposite sides
resetCounters();
local.rename("A/a1m", "A/a1m2");
remote.setContents("A/a1m", 'B');
remote.rename("B/b1m", "B/b1m2");
local.setContents("B/b1m", 'B');
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 2);
QCOMPARE(nPUT, 2);
QCOMPARE(nMOVE, 0);
QCOMPARE(nDELETE, 0);
// All these files existing afterwards is debatable. Should we propagate
// the rename in one direction and grab the new contents in the other?
// Currently there's no propagation job that would do that, and this does
// at least not lose data.
QCOMPARE(remote.find("A/a1m")->contentChar, 'B');
QCOMPARE(remote.find("B/b1m")->contentChar, 'B');
QCOMPARE(remote.find("A/a1m2")->contentChar, 'W');
QCOMPARE(remote.find("B/b1m2")->contentChar, 'W');
// Touch+create on one side, move on the other
{
resetCounters();
local.appendByte("A/a1m");
local.insert("A/a1mt");
remote.rename("A/a1m", "A/a1mt");
remote.appendByte("B/b1m");
remote.insert("B/b1mt");
local.rename("B/b1m", "B/b1mt");
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
QVERIFY(fakeFolder.syncOnce());
QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "A/a1mt"));
QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "B/b1mt"));
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 3);
QCOMPARE(nPUT, 1);
QCOMPARE(nMOVE, 0);
QCOMPARE(nDELETE, 0);
QVERIFY(itemSuccessful(completeSpy, "A/a1m", CSYNC_INSTRUCTION_NEW));
QVERIFY(itemSuccessful(completeSpy, "B/b1m", CSYNC_INSTRUCTION_NEW));
QVERIFY(itemConflict(completeSpy, "A/a1mt"));
QVERIFY(itemConflict(completeSpy, "B/b1mt"));
}
// Create new on one side, move to new on the other
{
resetCounters();
local.insert("A/a1N", 13);
remote.rename("A/a1mt", "A/a1N");
remote.insert("B/b1N", 13);
local.rename("B/b1mt", "B/b1N");
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
QVERIFY(fakeFolder.syncOnce());
QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "A/a1N"));
QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "B/b1N"));
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 2);
QCOMPARE(nPUT, 0);
QCOMPARE(nMOVE, 0);
QCOMPARE(nDELETE, 1);
QVERIFY(itemSuccessful(completeSpy, "A/a1mt", CSYNC_INSTRUCTION_REMOVE));
QVERIFY(itemSuccessful(completeSpy, "B/b1mt", CSYNC_INSTRUCTION_REMOVE));
QVERIFY(itemConflict(completeSpy, "A/a1N"));
QVERIFY(itemConflict(completeSpy, "B/b1N"));
}
// Local move, remote move
resetCounters();
local.rename("C/c1", "C/c1mL");
remote.rename("C/c1", "C/c1mR");
QVERIFY(fakeFolder.syncOnce());
// end up with both files
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 1);
QCOMPARE(nPUT, 1);
QCOMPARE(nMOVE, 0);
QCOMPARE(nDELETE, 0);
// Rename/rename conflict on a folder
resetCounters();
remote.rename("C", "CMR");
local.rename("C", "CML");
QVERIFY(fakeFolder.syncOnce());
// End up with both folders
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 3); // 3 files in C
QCOMPARE(nPUT, 3);
QCOMPARE(nMOVE, 0);
QCOMPARE(nDELETE, 0);
// Folder move
{
resetCounters();
local.rename("A", "AM");
remote.rename("B", "BM");
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 0);
QCOMPARE(nPUT, 0);
QCOMPARE(nMOVE, 1);
QCOMPARE(nDELETE, 0);
QVERIFY(itemSuccessfulMove(completeSpy, "AM"));
QVERIFY(itemSuccessfulMove(completeSpy, "BM"));
}
// Folder move with contents touched on the same side
{
resetCounters();
local.setContents("AM/a2m", 'C');
local.rename("AM", "A2");
remote.setContents("BM/b2m", 'C');
remote.rename("BM", "B2");
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 1);
QCOMPARE(nPUT, 1);
QCOMPARE(nMOVE, 1);
QCOMPARE(nDELETE, 0);
QCOMPARE(remote.find("A2/a2m")->contentChar, 'C');
QCOMPARE(remote.find("B2/b2m")->contentChar, 'C');
QVERIFY(itemSuccessfulMove(completeSpy, "A2"));
QVERIFY(itemSuccessfulMove(completeSpy, "B2"));
}
// Folder rename with contents touched on the other tree
resetCounters();
remote.setContents("A2/a2m", 'D');
// setContents alone may not produce updated mtime if the test is fast
// and since we don't use checksums here, that matters.
remote.appendByte("A2/a2m");
local.rename("A2", "A3");
local.setContents("B2/b2m", 'D');
local.appendByte("B2/b2m");
remote.rename("B2", "B3");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 1);
QCOMPARE(nPUT, 1);
QCOMPARE(nMOVE, 1);
QCOMPARE(nDELETE, 0);
QCOMPARE(remote.find("A3/a2m")->contentChar, 'D');
QCOMPARE(remote.find("B3/b2m")->contentChar, 'D');
// Folder rename with contents touched on both ends
resetCounters();
remote.setContents("A3/a2m", 'R');
remote.appendByte("A3/a2m");
local.setContents("A3/a2m", 'L');
local.appendByte("A3/a2m");
local.appendByte("A3/a2m");
local.rename("A3", "A4");
remote.setContents("B3/b2m", 'R');
remote.appendByte("B3/b2m");
local.setContents("B3/b2m", 'L');
local.appendByte("B3/b2m");
local.appendByte("B3/b2m");
remote.rename("B3", "B4");
QVERIFY(fakeFolder.syncOnce());
auto currentLocal = fakeFolder.currentLocalState();
auto conflicts = findConflicts(currentLocal.children["A4"]);
QCOMPARE(conflicts.size(), 1);
for (auto c : conflicts) {
QCOMPARE(currentLocal.find(c)->contentChar, 'L');
local.remove(c);
}
conflicts = findConflicts(currentLocal.children["B4"]);
QCOMPARE(conflicts.size(), 1);
for (auto c : conflicts) {
QCOMPARE(currentLocal.find(c)->contentChar, 'L');
local.remove(c);
}
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 2);
QCOMPARE(nPUT, 0);
QCOMPARE(nMOVE, 1);
QCOMPARE(nDELETE, 0);
QCOMPARE(remote.find("A4/a2m")->contentChar, 'R');
QCOMPARE(remote.find("B4/b2m")->contentChar, 'R');
// Rename a folder and rename the contents at the same time
resetCounters();
local.rename("A4/a2m", "A4/a2m2");
local.rename("A4", "A5");
remote.rename("B4/b2m", "B4/b2m2");
remote.rename("B4", "B5");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QCOMPARE(nGET, 0);
QCOMPARE(nPUT, 0);
QCOMPARE(nMOVE, 2);
QCOMPARE(nDELETE, 0);
}
// Check interaction of moves with file type changes
void testMoveAndTypeChange()
{
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
auto &local = fakeFolder.localModifier();
auto &remote = fakeFolder.remoteModifier();
// Touch on one side, rename and mkdir on the other
{
local.appendByte("A/a1");
remote.rename("A/a1", "A/a1mq");
remote.mkdir("A/a1");
remote.appendByte("B/b1");
local.rename("B/b1", "B/b1mq");
local.mkdir("B/b1");
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
QVERIFY(fakeFolder.syncOnce());
// BUG: This doesn't behave right
//QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
}
};
QTEST_GUILESS_MAIN(TestSyncMove)
#include "testsyncmove.moc"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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