1
0
mirror of https://github.com/chylex/Nextcloud-Desktop.git synced 2026-04-03 18:11:32 +02:00

Compare commits

...

191 Commits

Author SHA1 Message Date
allexzander
b0791e51ce Merge pull request #4208 from nextcloud/bump-version-3.4.2
Bump version 3.4.2
2022-01-27 10:55:36 +02:00
alex-z
11a36158ad Bump version to 3.4.2
Signed-off-by: alex-z <blackslayer4@gmail.com>
2022-01-27 10:53:36 +02:00
allexzander
d74024baf4 Merge pull request #4203 from nextcloud/backport/4167/stable-3.4
[stable-3.4] Add macOS *.textClipping files to ignore list
2022-01-27 10:51:17 +02:00
Nextcloud bot
f95ff45c16 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-27 03:53:22 +00:00
Claudio Cambra
d6d8132b36 Add macOS *.textClipping files to ignore list
Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>
2022-01-26 13:46:28 +00:00
Nextcloud bot
b426ca3579 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-26 03:53:20 +00:00
Nextcloud bot
4aabf339ea [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-25 03:53:42 +00:00
allexzander
f3c5081c38 Merge pull request #4197 from nextcloud/backport/4191/stable-3.4
[stable-3.4] use proper API to dehydrate a placeholder file
2022-01-24 17:19:02 +02:00
Matthieu Gallien
63863d15ad use proper API to dehydrate a placeholder file
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2022-01-24 14:51:30 +00:00
Nextcloud bot
e5690d77f6 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-24 03:50:40 +00:00
Nextcloud bot
cbc3cfb8ab [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-21 03:49:51 +00:00
Nextcloud bot
eae2e0075f [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-20 03:52:12 +00:00
Nextcloud bot
c875ebecda [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-19 03:50:57 +00:00
Nextcloud bot
ac114950ed [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-18 04:09:38 +00:00
Nextcloud bot
869b3f5f81 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-17 03:47:43 +00:00
Nextcloud bot
60fc03acb5 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-16 03:51:26 +00:00
Nextcloud bot
34c782c81d [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-15 03:49:56 +00:00
Matthieu Gallien
a668cb123f Merge pull request #4179 from nextcloud/backport/4102/stable-3.4
[stable-3.4] Show only filenames in tray activity items, with full path in tooltip
2022-01-14 15:00:04 +01:00
Claudio Cambra
4b2dbd54d2 Show only filenames in tray activity items, with full path in tooltip
Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>
2022-01-14 10:46:38 +00:00
Nextcloud bot
7947b31d64 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-14 03:52:49 +00:00
allexzander
318bc32a06 Merge pull request #4174 from nextcloud/backport/4171/stable-3.4
[stable-3.4] Bugfix. Re-init sharing manager to enable link sharing UI when receivng sharing permissions.
2022-01-13 11:40:34 +02:00
alex-z
985eba8fa3 Bugfix. Re-init sharing manager to enable link sharing UI when receiving sharing permissions.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2022-01-13 09:37:52 +00:00
Nextcloud bot
ebf41b181b [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-13 03:51:12 +00:00
Matthieu Gallien
eefdb04277 Merge pull request #4170 from nextcloud/backport/4137/stable-3.4
[stable-3.4] Display error message when creating a link share with compromised password.
2022-01-12 14:57:49 +01:00
alex-z
ac4435aa86 Fix review comments.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2022-01-12 12:25:43 +00:00
alex-z
1d85963551 Fix review comments.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2022-01-12 12:25:43 +00:00
alex-z
269be3cb4f Fix review comments.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2022-01-12 12:25:43 +00:00
alex-z
866a1da899 Display error message when creating a link share with compromised password.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2022-01-12 12:25:43 +00:00
Matthieu Gallien
9f075be5c4 Merge pull request #4169 from nextcloud/backport/4090/stable-3.4
[stable-3.4] Hide share button for deleted files and ignored files in tray activity
2022-01-12 13:24:30 +01:00
Claudio Cambra
c1e5a0a890 Hide share button for deleted and ignored files in tray activity
Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>
2022-01-12 10:39:38 +00:00
Nextcloud bot
1035bca4ae [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-12 03:50:48 +00:00
Nextcloud bot
195218e0a3 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-11 03:51:28 +00:00
Nextcloud bot
bc1bd64ece [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-10 03:53:46 +00:00
Matthieu Gallien
586afaacb6 Merge pull request #4156 from nextcloud/backport/4022/stable-3.4
[stable-3.4] Unbreak loading translations
2022-01-08 19:37:49 +01:00
Antonio Rojas
b4690f5721 Unbreak loading translations
Commit 18ddb9df4a changed SHAREDIR to point to CMAKE_INSTALL_DATADIR, which is a relative path by default. This prevents the C++ code from finding the translations at runtime.

Signed-off-by: Antonio Rojas <arojas@archlinux.org>
2022-01-08 17:32:03 +00:00
Nextcloud bot
5ceb98c266 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-08 03:48:31 +00:00
Matthieu Gallien
0c92cfc644 Merge pull request #4151 from nextcloud/backport/4115/stable-3.4
[stable-3.4] Windows. MSI. Unregister Nextcloud folders in SyncRootManager on uninstall.
2022-01-07 23:32:25 +01:00
alex-z
ba4f53679a Windows. MSI. Unregister Nextcloud folders in SyncRootManager on uninstall.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2022-01-07 17:46:36 +00:00
Matthieu Gallien
820f22ba0d Merge pull request #4150 from nextcloud/backport/4111/stable-3.4
[stable-3.4] Do not display 'Conflict when uploading some files to a folder
2022-01-07 18:43:10 +01:00
alex-z
80df113fd2 Do not display 'Conflict when uploading some files to a folder
Signed-off-by: alex-z <blackslayer4@gmail.com>
2022-01-07 17:02:05 +00:00
Nextcloud bot
4abeda01b7 [tx-robot] updated from transifex 2022-01-07 14:02:58 +00:00
Nextcloud bot
42f6606d52 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-07 03:51:24 +00:00
Nextcloud bot
b17d03c0ab [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-06 03:51:38 +00:00
allexzander
4ace64f1d7 Merge pull request #4140 from nextcloud/backport/4116/stable-3.4
[stable-3.4] Bugfix/force re-login on SSL Handshake error
2022-01-05 16:04:55 +02:00
alex-z
5e26d76d2b Try to sign-in after being signed-out due to SslHandshakeFailedError
Signed-off-by: alex-z <blackslayer4@gmail.com>
2022-01-05 13:26:05 +00:00
Nextcloud bot
689ecbcb5b [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-05 03:51:19 +00:00
Nextcloud bot
de6aa7be02 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-04 03:50:40 +00:00
Nextcloud bot
d0957fc071 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-03 03:50:31 +00:00
Nextcloud bot
fe6d126443 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2022-01-02 03:50:19 +00:00
Nextcloud bot
645688ae20 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-31 03:51:16 +00:00
Nextcloud bot
d3d2500807 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-30 03:51:14 +00:00
Nextcloud bot
a4493606e0 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-29 03:51:39 +00:00
Nextcloud bot
6d2ef0b410 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-28 03:51:10 +00:00
Nextcloud bot
05c731c4c1 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-24 03:53:12 +00:00
Nextcloud bot
1915ab7989 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-23 03:51:27 +00:00
Nextcloud bot
54d0351b42 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-22 03:53:10 +00:00
Nextcloud bot
282e47d266 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-21 03:52:06 +00:00
Nextcloud bot
07c81c02da [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-20 03:54:16 +00:00
Nextcloud bot
2ef6a20edc [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-19 03:57:03 +00:00
Nextcloud bot
79ffdac989 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-18 03:51:37 +00:00
Matthieu Gallien
fa32c10014 Merge pull request #4097 from nextcloud/bumpVersion
Bump version
2021-12-17 16:52:51 +01:00
Matthieu Gallien
fe67d66d3d Release 3.4.1
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-17 16:46:27 +01:00
Nextcloud bot
3a3a6dd6b8 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-17 03:55:43 +00:00
Matthieu Gallien
328c673c29 Merge pull request #4094 from nextcloud/backport/4092/stable-3.4
[stable-3.4] ensure any errors after calling FileSystem::getModTime are handled
2021-12-16 18:52:03 +01:00
Matthieu Gallien
49afad0474 ensure any errors after calling FileSystem::getModTime are handled
be sure that even in release mode no errors when calling getModTime
could be ignored

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-16 15:01:20 +00:00
allexzander
7c3e91202e Merge pull request #4083 from nextcloud/backport/4058/stable-3.4
[stable-3.4] Do not crash on findAndCancelDeletedJob
2021-12-16 15:37:07 +02:00
alex-z
e94b18f97f Added sync stop when failed to cancel delete jobs.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2021-12-16 13:09:41 +00:00
alex-z
79a0b937f5 Do not crash on findAndCancelDeletedJob.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2021-12-16 13:09:41 +00:00
Nextcloud bot
e5fbc8c2dd [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-16 03:51:22 +00:00
Matthieu Gallien
3632cc659b Merge pull request #4080 from nextcloud/backport/4076/stable-3.4
[stable-3.4] Bugfix/avoid sync getting stuck
2021-12-15 11:51:55 +01:00
Matthieu Gallien
1731bf7c86 fix review comment
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-15 10:01:02 +00:00
Matthieu Gallien
936d37fd0b ensure bulk upload jobs finished after an error
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-15 10:01:02 +00:00
Nextcloud bot
7259a0bc0d [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-15 03:50:48 +00:00
allexzander
06de878b4b Merge pull request #4081 from nextcloud/backport/4079/stable-3.4
[stable-3.4] Fix CMake error in ECMAddAppIcon for mac
2021-12-14 22:59:39 +02:00
alex-z
179ff27ab6 Fix CMake error in ECMAddAppIcon for mac.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2021-12-14 18:18:01 +00:00
allexzander
44da2f2ce2 Merge pull request #4078 from nextcloud/backport/4073/stable-3.4
[stable-3.4] Enforce VFS. Disable 'Make always available locally'.
2021-12-14 18:20:56 +02:00
alex-z
92b302fb37 Save folder settings to config when force-switching VFS.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2021-12-14 16:07:19 +00:00
alex-z
d2febdf17c Enforce VFS. Disable 'Make always available locally'.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2021-12-14 16:07:19 +00:00
Matthieu Gallien
9c4b4c6183 Merge pull request #4075 from nextcloud/backport/4074/stable-3.4
[stable-3.4] Bugfix/force download local invalid files
2021-12-14 16:27:31 +01:00
Matthieu Gallien
45029e9012 force download from server for local files that have invalid dates
will trigger if local state is incoherent
like the file itself haveing 0 or negative modtime and the database not

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-14 10:15:55 +00:00
Nextcloud bot
969b0e8e2e [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-14 03:50:21 +00:00
Nextcloud bot
4b46da9370 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-13 03:50:36 +00:00
Nextcloud bot
8b5dd53519 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-12 03:52:44 +00:00
Nextcloud bot
5ee5b19406 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-11 03:51:31 +00:00
Matthieu Gallien
8e1c62cc70 Merge pull request #4066 from nextcloud/backport/4064/stable-3.4
[stable-3.4] Bugfix/sync stuck on error
2021-12-10 15:01:00 +01:00
Matthieu Gallien
151e9300cd do not get stuck forever in sync in case of errors
when a local file has invalid date and we try to upload it, properly
handle the error such that we are not stuck forever in sync state

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-10 12:37:54 +00:00
Nextcloud bot
efc3116f30 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-10 03:54:52 +00:00
Matthieu Gallien
4296a6041a Merge pull request #4057 from nextcloud/backport/4055/stable-3.4
[stable-3.4] Bugfix/3.4.1 rc1
2021-12-09 12:13:11 +01:00
Matthieu Gallien
0d1e0057b3 remove files which gets downloaded with an invalid modified time
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-09 09:09:22 +00:00
Nextcloud bot
5aadc7a62d [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-09 03:54:43 +00:00
allexzander
3c28e38089 Merge pull request #4050 from nextcloud/backport/4014/stable-3.4
[stable-3.4] Feature/folder logo variations
2021-12-08 14:41:12 +02:00
alex-z
dba8fd7c76 Use different icon for a sync folder on Windows depending on zoom level.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2021-12-08 11:36:40 +00:00
Camila
39c2bb555a Merge pull request #4051 from nextcloud/backport/4031/stable-3.4
[stable-3.4] Always prefill username from Windows login name based on server version
2021-12-08 11:15:39 +01:00
alex-z
39fc86cbcf Always prefill username from Windows login name based on server version
Signed-off-by: alex-z <blackslayer4@gmail.com>
2021-12-08 09:13:33 +00:00
Matthieu Gallien
8d574c11e8 Merge pull request #4049 from nextcloud/backport/4045/stable-3.4
[stable-3.4] Bugfix/assert invalid modtime
2021-12-08 09:10:25 +01:00
Matthieu Gallien
c02d87f283 add log statements for each new assert about invalid modified time
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-08 08:01:27 +00:00
Matthieu Gallien
ddb5375c68 recover from local invalid modifie time: force download from server
force file download if local modified time is invalid and server has
valid modified time

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-08 08:01:27 +00:00
Matthieu Gallien
bd78604468 prevent cases where desktop client would store invalid modified time
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-08 08:01:27 +00:00
Matthieu Gallien
4920b4d4af prevent injecting invalid modified time through CfApi calls
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-08 08:01:27 +00:00
Matthieu Gallien
d12d00562f do not consider that a file has changed if its mtime is invalid
a mtime should never be 0 or negative

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-08 08:01:27 +00:00
Matthieu Gallien
b17bbb2b22 avoid downloading a file from server when modified time is invalid
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-08 08:01:27 +00:00
Matthieu Gallien
fc64edba11 prevent invalid modified time from being propagated
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-08 08:01:27 +00:00
Matthieu Gallien
b6c7581414 assert on invalid modtime
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-08 08:01:27 +00:00
Nextcloud bot
962850f307 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-08 04:03:04 +00:00
Matthieu Gallien
88ab5557bd Merge pull request #4046 from nextcloud/backport/4033/stable-3.4
[stable-3.4] do not forget the path when renaming files with invalid names
2021-12-07 16:28:07 +01:00
Matthieu Gallien
e3fb3bbe73 do not forget the path when renaming files with invalid names
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-07 12:42:44 +00:00
Matthieu Gallien
a86a1b4c17 test files that should be renamed in sub-directory
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-12-07 12:42:44 +00:00
Nextcloud bot
4326a70ede [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-07 05:07:03 +00:00
Nextcloud bot
2880bd62ce [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-06 03:59:01 +00:00
Nextcloud bot
b49633a9f7 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-05 04:06:08 +00:00
Nextcloud bot
adfe7ad953 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-04 04:01:04 +00:00
Nextcloud bot
e45a01bc03 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-03 03:56:48 +00:00
Nextcloud bot
9bcbc15834 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-02 03:56:35 +00:00
Nextcloud bot
b4a19bb6d3 [tx-robot] updated from transifex 2021-12-01 18:45:48 +00:00
Nextcloud bot
52db45c2b1 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-12-01 03:59:02 +00:00
Nextcloud bot
b3d8cacf8c [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-30 03:56:41 +00:00
Camila
c004db2070 Merge pull request #4013 from nextcloud/backport/4012/stable-3.4
[stable-3.4] fix random error when updating CfApi metadata
2021-11-29 18:26:00 +01:00
Matthieu Gallien
48ada55e77 fix random error when updating CfApi metadata
initialiazing all fields in a structure is required to not have random
behavior

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-29 17:24:46 +00:00
Camila
fd60e60541 Merge pull request #4010 from nextcloud/bump-version
Bump VERSION.cmake to 3.4.0.
2021-11-29 14:46:29 +01:00
Camila
fb833ed311 Bump VERSION.cmake to 3.4.0.
Signed-off-by: Camila <hello@camila.codes>
2021-11-29 14:33:29 +01:00
Nextcloud bot
1440c53ed6 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-29 03:52:09 +00:00
Nextcloud bot
bd42c35e80 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-28 03:50:40 +00:00
Nextcloud bot
a5c82670c9 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-27 03:53:51 +00:00
Matthieu Gallien
3c966a77df Merge pull request #4006 from nextcloud/bugfix/speedUpBulkUpload
Bugfix/speed up bulk upload
2021-11-26 17:50:01 +01:00
Matthieu Gallien
1a9aade28e use the error message sent by the server for bulk upload
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-26 16:32:08 +01:00
Matthieu Gallien
34c4c28879 allow sending parallel batch of files: curretly disabled
can allow to send a new batch before the reply to a previous one is
received

due to concerns with the reliability on the server side this is disabled

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-26 16:32:08 +01:00
Matthieu Gallien
a272b34809 really check that this is a valid answer when receiving batch upload
we could somehow miss that the reply is missing proper fields

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-26 15:49:25 +01:00
Matthieu Gallien
05b8d1e40d batch upload: only handle file that are in the reply
do not handle all files sent but only received ones

should allow to submit more than one request in parallel

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-26 15:49:25 +01:00
Matthieu Gallien
18ef471332 let auto tests of batch upload return proper file paths
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-26 15:49:25 +01:00
Matthieu Gallien
e14502606c make sure we do not start a new batch when the previous one is not sent
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-26 15:49:25 +01:00
Matthieu Gallien
59953d857b use a proper constant for the size of batch
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-26 15:49:25 +01:00
Matthieu Gallien
436eced9fb Merge pull request #4003 from nextcloud/bugfix/errorBulkUpload
if BulkPropagatorJob abort after an error emit finished signal
2021-11-26 15:31:15 +01:00
Matthieu Gallien
f56985938d if BulkPropagatorJob abort after an error emit finished signal
prevent sync engine being stuck because of an error when preparing bulk
upload

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-26 12:48:38 +01:00
Nextcloud bot
56f4198b28 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-26 04:14:39 +00:00
Nextcloud bot
6b22081f61 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-25 03:52:56 +00:00
Matthieu Gallien
a5fa53c460 Merge pull request #4001 from nextcloud/bumpVersion
release 3.4.0 RC2
2021-11-24 18:04:18 +01:00
Matthieu Gallien
426e0af8cd release 3.4.0 RC2
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-24 17:46:10 +01:00
Matthieu Gallien
e2f1854b1e Merge pull request #3887 from nextcloud/feature/bulkUpload
Feature/bulk upload
2021-11-24 17:42:25 +01:00
Matthieu Gallien
c194605c35 implement bulk upload
add PutMultiFileJob to send many files at once

use it in BulkPropagatorJob to implement bulk upload feature

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-24 16:17:47 +01:00
Matthieu Gallien
112be18635 read capabilities for bulk upload from server
use it in dedicated tests

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-24 16:17:47 +01:00
Matthieu Gallien
802c7ac906 make AbstractNetworkJob::errorString virtual: it is already overriden
in practice AbstractNetworkJob::errorString is already overriden but the
overrided code is probably never called while the intention looked like
the opposite

fix that by making the method virtual in base class

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-24 16:17:47 +01:00
allexzander
f575cc1860 Merge pull request #3930 from nextcloud/feature/additional-features-for-initial-setup-config
Pass username from Windows to login page.
2021-11-24 13:26:29 +02:00
alex-z
b03bf1c1f0 Pass username from Windows to login page.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2021-11-24 10:04:22 +00:00
Matthieu Gallien
9bebda057a Merge pull request #3993 from nextcloud/bugfix/variousVfsFixes
Bugfix/various vfs fixes
2021-11-23 17:11:07 +01:00
Matthieu Gallien
83a8058b51 improve logging for CfApi
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-23 13:41:29 +00:00
Matthieu Gallien
072e9d44bd gracefully handle one case of invalid handles
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-23 13:41:29 +00:00
Matthieu Gallien
a3013de6ea fix OCC::CfApiWrapper::handleForPath when path does not exist
sometime it can be called with a path that is already deleted

ensure we always go to the correct code path

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-23 13:41:29 +00:00
Matthieu Gallien
9eed62a854 remove too noisy log print
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-23 13:41:29 +00:00
Camila
79282a8df9 Merge pull request #3983 from nextcloud/bugfix/only-build-app-bundle-if-needed
Only build app bundle if requested
2021-11-23 14:13:45 +01:00
Felix Weilbach
ec64246dc7 Only build app bundle if requested
For development a app bundle is not needed. The app bundle is only
needed for distribution and macdeployqt takes a lot of time.

Signed-off-by: Felix Weilbach <felix.weilbach@nextcloud.com>
2021-11-23 11:41:29 +00:00
Matthieu Gallien
c89d2abf5a Merge pull request #3994 from nextcloud/bugfix/reset-syncfolder-icon
Cleanup system bindings from Windows when removing a local sync folder
2021-11-23 10:58:06 +01:00
alex-z
b3914f627d Cleanup system bindings from Windows when removing a local sync folder
Signed-off-by: alex-z <blackslayer4@gmail.com>
2021-11-23 08:32:55 +00:00
Nextcloud bot
998236dcc5 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-23 03:55:50 +00:00
Matthieu Gallien
d9626bf311 Merge pull request #3989 from nextcloud/bugfix/forceVFS
fix button that should be disabled when force VFS
2021-11-22 15:52:19 +01:00
Matthieu Gallien
684d70985e fix button that should be disabled when force VFS
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-22 14:22:59 +01:00
Matthieu Gallien
e92842d837 Merge pull request #3982 from nextcloud/addUserAgentQml
add a network access factory to qml engine
2021-11-22 13:51:55 +01:00
Matthieu Gallien
12c6d6e3bd add a network access factory to qml engine
ensure network access made via qml are using our user agent

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-22 12:21:58 +00:00
Matthieu Gallien
1d704d9352 Merge pull request #3971 from nextcloud/fix/scrolling-activiy-list
Make scrolling with a touchpad in activity list and search result more natural
2021-11-22 13:20:27 +01:00
Carl Schwan
38ac585e7c Add WheelHandler to the Search result list too
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2021-11-22 11:18:18 +00:00
Carl Schwan
892d289f38 [tray] Makes scrolling with a touchpad in activiy list more natural
This basically use the same method that is used in Kirigami and Plasma
Components3.

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2021-11-22 11:18:18 +00:00
allexzander
5294c5135c Merge pull request #3987 from nextcloud/bugfix/upload-new-folder-with-vfs
Quick fix! Disable VFS folders removal for non-Windows VFS.
2021-11-22 12:39:01 +02:00
alex-z
8e6896ba03 Quick fix! Disable VFS folders removal for non-Windows VFS.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2021-11-22 12:37:18 +02:00
Nextcloud bot
b2e86c2ea3 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-21 03:52:24 +00:00
Nextcloud bot
8cc58dd8b0 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-20 04:12:29 +00:00
Matthieu Gallien
ca1620ef42 Merge pull request #3988 from nextcloud/bugfix/runVfsFixOnlyOnce
properly query sync journal DB to know when to run fix for VFS
2021-11-19 14:38:03 +01:00
Matthieu Gallien
f1d834df8e properly query sync journal DB to know when to run fix for VFS
the new method added to query the db is not working and so the fix for
vfs is executed at each sync run

the new method for bool was not really needed so let's just remove it
(and that will make the usage of SqlQuery be correct

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-19 14:19:04 +01:00
allexzander
502ffc62ef Merge pull request #3984 from nextcloud/bugfix/vfs-folder-upload-conflict
Added more logs to 'postProcessLocalNew'.
2021-11-19 11:48:05 +02:00
alex-z
73db636361 Added more logs to 'postProcessLocalNew'.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2021-11-19 09:10:20 +00:00
Nextcloud bot
b222785dc2 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-19 04:35:47 +00:00
Nextcloud bot
5454004ef9 [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-18 13:37:40 +00:00
Matthieu Gallien
ed9671c2a6 Merge pull request #3985 from nextcloud/bugfix/unified-search-open-local-file
Use QUrl::fromLocalFile to open local files in Unified Search results
2021-11-18 10:58:50 +01:00
alex-z
911e35bc50 Use QUrl::fromLocalFile to open local files in Unified Search results.
Signed-off-by: alex-z <blackslayer4@gmail.com>
2021-11-18 09:34:54 +00:00
Nextcloud bot
898949d1bc [tx-robot] updated from transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2021-11-18 09:08:38 +00:00
Matthieu Gallien
ef8fe58245 Merge pull request #3978 from nextcloud/bugfix/correct-placeholder-files
Correct virtual files placeholder files if needed
2021-11-17 11:31:29 +01:00
Felix Weilbach
9e792369b2 Refactor key-value store query code
Signed-off-by: Felix Weilbach <felix.weilbach@nextcloud.com>
2021-11-17 09:53:26 +00:00
Felix Weilbach
c76a77e431 Correct virtual files placeholder files if needed
In the past not all files were converted to placeholder files when
converting an existing sync folder to a virtual files folder. Because
some files were not converted to placeholder files, the status would
be wrong on the files. This code makes sure that every file in a
virtual files folder is a placeholder file. It could be removed in the
future.

Signed-off-by: Felix Weilbach <felix.weilbach@nextcloud.com>
2021-11-17 09:53:26 +00:00
Felix Weilbach
2308c9da49 Merge pull request #3979 from nextcloud/bugfix/avoidUselessIconDownloads
avoid adding icon data in a cache we never use
2021-11-17 10:17:27 +01:00
Matthieu Gallien
c59f88ca82 avoid adding icon data in a cache we never use
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-16 16:11:44 +01:00
Matthieu Gallien
3edfcff1a0 Merge pull request #3972 from nextcloud/fix/accessibility-keyboard
Fix focus indicator
2021-11-16 14:53:35 +01:00
Carl Schwan
d84673376d More fixes to the menu implementation
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2021-11-16 09:27:10 +00:00
Carl Schwan
69def04ec2 Fix focus indicator
This improve considerably the keyboard navigation in the SystemTray.
But this is still not as good as the golden standard that is recommended
by this article: https://www.sarasoueidan.com/blog/focus-indicators/

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2021-11-16 09:27:10 +00:00
Matthieu Gallien
3e1a46f2de Merge pull request #3969 from nextcloud/bugfix/finder-arch
Compile Finder extensions for arm and x86
2021-11-16 10:25:58 +01:00
Felix Weilbach
113ba716e6 Compile finder extensions for arm and x86
Fix: https://github.com/nextcloud/desktop/issues/3967

Signed-off-by: Felix Weilbach <felix.weilbach@nextcloud.com>
2021-11-16 08:54:48 +00:00
Matthieu Gallien
703037cbfb Merge pull request #3968 from nextcloud/bugfix/user-status-disabled
Check if the server has user status app enabled
2021-11-16 09:53:37 +01:00
Felix Weilbach
07a8e8c91d Check if the server has user status app enabled
According to
https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-status-api.html#user-status-retrieve-statuses
we should check the user status capability, not the the end points.

Signed-off-by: Felix Weilbach <felix.weilbach@nextcloud.com>
2021-11-15 16:20:58 +00:00
Matthieu Gallien
df745ef39c Merge pull request #3970 from nextcloud/updateDoc
we run on Windows 8.1+
2021-11-15 17:19:24 +01:00
Matthieu Gallien
2665c8fc16 we run on Windows 8.1+
Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
2021-11-15 13:40:51 +00:00
Matthieu Gallien
cb34fec596 Merge pull request #3959 from Minoru/bugifx/detect-missing-guiprivate
CMake: fail if `Qt5::GuiPrivate` is not found
2021-11-15 13:18:30 +01:00
Alexander Batischev
d8560dcb19 CMake: fail if Qt5::GuiPrivate is not found
`nextcloud` and `nextcloudCore` depend on three Qt5 components which
aren't mentioned in `find_library`: `Xml`, `Network`, and `GuiPrivate`.
The first two are omitted by mistake, apparently, so this commit just
adds them.

`GuiPrivate` is a special case: it doesn't have its own CMake config, so
adding it to "required components" in `find_package` will always fail
the build. Thus, we implement our own check instead.

Signed-off-by: Alexander Batischev <eual.jp@gmail.com>
2021-11-15 13:57:18 +03:00
Felix Weilbach
0e5f1d9a30 Merge pull request #3966 from nextcloud/bugfix/macos-installer-universal
Let the macOS installer know that the application can run on Arm64
2021-11-15 09:43:55 +01:00
Felix Weilbach
ad814f175e Let the macOS installer know that the application can run on Arm64
Signed-off-by: Felix Weilbach <felix.weilbach@nextcloud.com>
2021-11-12 01:42:37 +01:00
155 changed files with 45195 additions and 31307 deletions

1
.gitignore vendored
View File

@@ -184,6 +184,7 @@ compile_commands.json
convert.exe
.dir-locals.el
*-icon.png
*-icon-win-folder.png
*-sidebar.png
*-w10startmenu.png
theme.qrc

View File

@@ -22,5 +22,6 @@ Icon=@APPLICATION_EXECUTABLE@
# Translations
Icon[cy_GB]=@APPLICATION_ICON_NAME@
Name[cy_GB]=@APPLICATION_NAME@ cleient cydweddu bwrdd gwaith
Comment[cy_GB]=@APPLICATION_NAME@ cleient cydweddu bwrdd gwaith
GenericName[cy_GB]=Cydweddu Ffolder

View File

@@ -0,0 +1,24 @@
[Desktop Entry]
Categories=Utility;X-SuSE-SyncUtility;
Type=Application
Exec=@APPLICATION_EXECUTABLE@
Name=@APPLICATION_NAME@ Desktop
Comment=@APPLICATION_NAME@ desktop synchronization client
GenericName=Folder Sync
Icon=@APPLICATION_ICON_NAME@
Keywords=@APPLICATION_NAME@;syncing;file;sharing;
X-GNOME-Autostart-Delay=3
MimeType=application/vnd.@APPLICATION_EXECUTABLE@;
Actions=Quit;
# Translations
[Desktop Action Quit]
Exec=@APPLICATION_EXECUTABLE@ --quit
Name=Quit @APPLICATION_NAME@
Icon=@APPLICATION_EXECUTABLE@
# Translations
GenericName[id]=Sinkronisasi Folder

View File

@@ -22,5 +22,6 @@ Icon=@APPLICATION_EXECUTABLE@
# Translations
Icon[ko]=@APPLICATION_ICON_NAME@
Name[ko]=@APPLICATION_NAME@ 데스크탑
Comment[ko]=@APPLICATION_NAME@ 데스크톱 동기화 클라이언트
GenericName[ko]=폴더 동기화

View File

@@ -22,5 +22,6 @@ Icon=@APPLICATION_EXECUTABLE@
# Translations
Icon[nb_NO]=@APPLICATION_ICON_NAME@
Name[nb_NO]=@APPLICATION_NAME@ skrivebord
Comment[nb_NO]=@APPLICATION_NAME@ klient for synkroinisering
GenericName[nb_NO]=Mappe synkroinisering

View File

@@ -22,5 +22,6 @@ Icon=@APPLICATION_EXECUTABLE@
# Translations
Icon[oc]=@APPLICATION_ICON_NAME@
Name[oc]=@APPLICATION_NAME@ Burèu
Comment[oc]=@APPLICATION_NAME@ client de sincronizacion
GenericName[oc]=Sincro. dossièr

View File

@@ -22,5 +22,6 @@ Icon=@APPLICATION_EXECUTABLE@
# Translations
Icon[sv]=@APPLICATION_ICON_NAME@
Name[sv]=@APPLICATION_NAME@ Skrivbord
Comment[sv]=@APPLICATION_NAME@ desktopssynkroniseringsklient
GenericName[sv]=Mappsynkronisering

View File

@@ -96,26 +96,15 @@ endif()
message(STATUS "GIT_SHA1 ${GIT_SHA1}")
set(SYSCONFDIR ${SYSCONF_INSTALL_DIR})
set(SHAREDIR ${CMAKE_INSTALL_DATADIR})
set(SHAREDIR ${CMAKE_INSTALL_FULL_DATADIR})
#####
## handle BUILD_OWNCLOUD_OSX_BUNDLE
# BUILD_OWNCLOUD_OSX_BUNDLE was not initialized OR set to true on OSX
if(APPLE AND (NOT DEFINED BUILD_OWNCLOUD_OSX_BUNDLE OR BUILD_OWNCLOUD_OSX_BUNDLE))
set(BUILD_OWNCLOUD_OSX_BUNDLE ON)
# Build MacOS app bundle if wished
if(APPLE AND BUILD_OWNCLOUD_OSX_BUNDLE)
message(STATUS "Build MacOS app bundle")
set(OWNCLOUD_OSX_BUNDLE "${APPLICATION_NAME}.app")
set(LIB_INSTALL_DIR "${APPLICATION_NAME}.app/Contents/MacOS")
set(BIN_INSTALL_DIR "${APPLICATION_NAME}.app/Contents/MacOS")
# BUILD_OWNCLOUD_OSX_BUNDLE was disabled on OSX
elseif(APPLE AND NOT BUILD_OWNCLOUD_OSX_BUNDLE)
message(FATAL_ERROR "Building in non-bundle mode on OSX is currently not supported. Comment this error out if you want to work on/test it.")
# any other platform
else()
set(BUILD_OWNCLOUD_OSX_BUNDLE OFF)
endif()
#####
# this option removes Http authentication, keychain, shibboleth etc and is intended for
# external authentication mechanisms

View File

@@ -1,7 +1,7 @@
set( MIRALL_VERSION_MAJOR 3 )
set( MIRALL_VERSION_MINOR 3 )
set( MIRALL_VERSION_PATCH 81 )
set( MIRALL_VERSION_YEAR 2021 )
set( MIRALL_VERSION_MINOR 4 )
set( MIRALL_VERSION_PATCH 2 )
set( MIRALL_VERSION_YEAR 2022 )
set( MIRALL_SOVERSION 0 )
# Minimum supported server version according to https://docs.nextcloud.com/server/latest/admin_manual/release_schedule.html

View File

@@ -695,7 +695,12 @@
<key>PROJECT_SETTINGS</key>
<dict>
<key>ADVANCED_OPTIONS</key>
<dict/>
<dict>
<key>installer-script.options:hostArchitectures</key>
<array>
<string>x86_64,arm64</string>
</array>
</dict>
<key>BUILD_FORMAT</key>
<integer>0</integer>
<key>BUILD_PATH</key>

View File

@@ -26,6 +26,8 @@ install(FILES
${CMAKE_CURRENT_BINARY_DIR}/make-msi.bat
Platform.wxi
Nextcloud.wxs
RegistryCleanup.vbs
RegistryCleanupCustomAction.wxs
gui/banner.bmp
gui/dialog.bmp
DESTINATION msi/)

View File

@@ -76,12 +76,16 @@
<!-- Uninstall: Remove sync folders from Explorer's Navigation Pane, only effective for the current user (home users) -->
<Custom Action="RemoveNavigationPaneEntries" After="RemoveFiles">(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")</Custom>
<!-- Uninstall: Cleanup the Registry -->
<Custom Action="RegistryCleanupCustomAction" After="RemoveFiles">(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")</Custom>
<!-- Schedule Reboot for the Shell Extensions (in silent installation mode only, or if SCHEDULE_REBOOT argument is set-->
<ScheduleReboot After="InstallFinalize">(SCHEDULE_REBOOT=1) OR NOT (UILevel=2)</ScheduleReboot>
</InstallExecuteSequence>
<!-- "Add or Remove" Programs Entries -->
<Property Id="APPNAME">$(var.AppName)</Property>
<Property Id="ARPPRODUCTICON">$(var.AppIcon)</Property>
<Property Id="ARPHELPLINK">$(var.AppHelpLink)</Property>
<Property Id="ARPURLINFOABOUT">$(var.AppInfoLink)</Property>

View File

@@ -0,0 +1,54 @@
On Error goto 0
Const HKEY_LOCAL_MACHINE = &H80000002
Const strObjRegistry = "winmgmts:\\.\root\default:StdRegProv"
Function RegistryDeleteKeyRecursive(regRoot, strKeyPath)
Set objRegistry = GetObject(strObjRegistry)
objRegistry.EnumKey regRoot, strKeyPath, arrSubkeys
If IsArray(arrSubkeys) Then
For Each strSubkey In arrSubkeys
RegistryDeleteKeyRecursive regRoot, strKeyPath & "\" & strSubkey
Next
End If
objRegistry.DeleteKey regRoot, strKeyPath
End Function
Function RegistryListSubkeys(regRoot, strKeyPath)
Set objRegistry = GetObject(strObjRegistry)
objRegistry.EnumKey regRoot, strKeyPath, arrSubkeys
RegistryListSubkeys = arrSubkeys
End Function
Function GetUserSID()
Dim objWshNetwork, objUserAccount
Set objWshNetwork = CreateObject("WScript.Network")
Set objUserAccount = GetObject("winmgmts://" & objWshNetwork.UserDomain & "/root/cimv2").Get("Win32_UserAccount.Domain='" & objWshNetwork.ComputerName & "',Name='" & objWshNetwork.UserName & "'")
GetUserSID = objUserAccount.SID
End Function
Function RegistryCleanupSyncRootManager()
strSyncRootManagerKeyPath = "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SyncRootManager"
arrSubKeys = RegistryListSubkeys(HKEY_LOCAL_MACHINE, strSyncRootManagerKeyPath)
If IsArray(arrSubkeys) Then
arrSubkeys=Filter(arrSubkeys, Session.Property("APPNAME"))
End If
If IsArray(arrSubkeys) Then
arrSubkeys=Filter(arrSubkeys, GetUserSID())
End If
If IsArray(arrSubkeys) Then
For Each strSubkey In arrSubkeys
RegistryDeleteKeyRecursive HKEY_LOCAL_MACHINE, strSyncRootManagerKeyPath & "\" & strSubkey
Next
End If
End Function
Function RegistryCleanup()
RegistryCleanupSyncRootManager()
End Function

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<Binary Id="RegistryCleanup" SourceFile="RegistryCleanup.vbs"/>
<CustomAction Id='RegistryCleanupCustomAction' BinaryKey="RegistryCleanup" VBScriptCall="RegistryCleanup" Return="ignore" Execute="immediate"/>
</Fragment>
</Wix>

View File

@@ -17,10 +17,10 @@ Rem Generate collect.wxs
if %ERRORLEVEL% neq 0 exit %ERRORLEVEL%
Rem Compile en-US (https://www.firegiant.com/wix/tutorial/transforms/morphing-installers/)
"%WIX%\bin\candle.exe" -dcodepage=1252 -dPlatform=%BuildArch% -arch %BuildArch% -dHarvestAppDir="%HarvestAppDir%" -ext WixUtilExtension NCMsiHelper.wxs WinShellExt.wxs collect.wxs Nextcloud.wxs
"%WIX%\bin\candle.exe" -dcodepage=1252 -dPlatform=%BuildArch% -arch %BuildArch% -dHarvestAppDir="%HarvestAppDir%" -ext WixUtilExtension NCMsiHelper.wxs WinShellExt.wxs collect.wxs Nextcloud.wxs RegistryCleanupCustomAction.wxs
if %ERRORLEVEL% neq 0 exit %ERRORLEVEL%
Rem Link MSI package
"%WIX%\bin\light.exe" -sw1076 -ext WixUIExtension -ext WixUtilExtension -cultures:en-us NCMsiHelper.wixobj WinShellExt.wixobj collect.wixobj Nextcloud.wixobj -out "@MSI_INSTALLER_FILENAME@"
"%WIX%\bin\light.exe" -sw1076 -ext WixUIExtension -ext WixUtilExtension -cultures:en-us NCMsiHelper.wixobj WinShellExt.wixobj collect.wixobj Nextcloud.wixobj RegistryCleanupCustomAction.wixobj -out "@MSI_INSTALLER_FILENAME@"
exit %ERRORLEVEL%

View File

@@ -41,7 +41,7 @@
# target does not have the ``WIN32_EXECUTABLE`` property set.
# * One of the tools png2ico (See :find-module:`FindPng2Ico`) or
# icotool (see :find-module:`FindIcoTool`) is required.
# * Supported sizes: 16, 24, 32, 48, 64, 128, 256, 512 and 1024.
# * Supported sizes: 16, 20, 24, 32, 40, 48, 64, 128, 256, 512 and 1024.
#
# Mac OS X notes
# * The executable target must have the ``MACOSX_BUNDLE`` property set.
@@ -102,9 +102,12 @@ include(CMakeParseArguments)
function(ecm_add_app_icon appsources)
set(options)
set(oneValueArgs OUTFILE_BASENAME)
set(multiValueArgs ICONS SIDEBAR_ICONS)
set(oneValueArgs OUTFILE_BASENAME ICON_INDEX)
set(multiValueArgs ICONS SIDEBAR_ICONS RC_DEPENDENCIES)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if (NOT ARG_ICON_INDEX)
set(ARG_ICON_INDEX 1)
endif()
if(NOT ARG_ICONS)
message(FATAL_ERROR "No ICONS argument given to ecm_add_app_icon")
@@ -138,8 +141,11 @@ function(ecm_add_app_icon appsources)
endforeach()
endif()
_ecm_add_app_icon_categorize_icons("${ARG_ICONS}" "icons" "16;24;32;48;64;128;256;512;1024")
if (WIN32)
_ecm_add_app_icon_categorize_icons("${ARG_ICONS}" "icons" "16;20;24;32;40;48;64;128;256;512;1024")
else()
_ecm_add_app_icon_categorize_icons("${ARG_ICONS}" "icons" "16;24;32;48;64;128;256;512;1024")
endif()
if(ARG_SIDEBAR_ICONS)
_ecm_add_app_icon_categorize_icons("${ARG_SIDEBAR_ICONS}" "sidebar_icons" "16;32;64;128;256")
endif()
@@ -168,8 +174,10 @@ function(ecm_add_app_icon appsources)
set(windows_icons ${icons_at_16px}
${icons_at_20px}
${icons_at_24px}
${icons_at_32px}
${icons_at_40px}
${icons_at_48px}
${icons_at_64px}
${icons_at_128px}
@@ -204,12 +212,12 @@ function(ecm_add_app_icon appsources)
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
)
# this bit's a little hacky to make the dependency stuff work
file(WRITE "${_outfilename}.rc.in" "IDI_ICON1 ICON DISCARDABLE \"${_outfilename}.ico\"\n")
file(WRITE "${_outfilename}.rc.in" "IDI_ICON${ARG_ICON_INDEX} ICON DISCARDABLE \"${_outfilename}.ico\"\n")
add_custom_command(
OUTPUT "${_outfilename}.rc"
COMMAND ${CMAKE_COMMAND}
ARGS -E copy "${_outfilename}.rc.in" "${_outfilename}.rc"
DEPENDS "${_outfilename}.ico"
DEPENDS ${ARG_RC_DEPENDENCIES} "${_outfilename}.ico"
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
)
endfunction()
@@ -226,7 +234,7 @@ function(ecm_add_app_icon appsources)
endif()
endforeach()
foreach(size 16 24 32 48 64 128 ${maxSize})
foreach(size 16 20 24 32 40 48 64 128 ${maxSize})
if(NOT icons_at_${size}px)
continue()
endif()

View File

@@ -185,6 +185,8 @@ Then, in Terminal:
.. code-block:: bash
% echo 'export CMAKE_INSTALL_PREFIX=~/Builds' >> ~/.nextcloud_build_variables
# If you want to build a macOS app bundle for distribution
% echo 'export BUILD_OWNCLOUD_OSX_BUNDLE=ON' >> ~/.nextcloud_build_variables
Replace ``~/Builds`` with a different directory if you'd like the build to end up elsewhere.

View File

@@ -48,9 +48,9 @@ copyright = u'2013-2021, The Nextcloud developers'
# built documents.
#
# The short X.Y version.
version = '3.3'
version = '3.4'
# The full version, including alpha/beta/rc tags.
release = '3.3.81'
release = '3.4.2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

@@ -31,7 +31,7 @@ download page.
System Requirements
----------------------------------
- Windows 10+
- Windows 8.1+
- macOS 10.12+ (64-bit only)
- Linux
- FreeBSD

View File

@@ -1,23 +1,26 @@
if(APPLE)
set(OC_OEM_SHARE_ICNS "${CMAKE_BINARY_DIR}/src/gui/${APPLICATION_ICON_NAME}.icns")
set(OC_OEM_SHARE_ICNS "${CMAKE_BINARY_DIR}/src/gui/${APPLICATION_ICON_NAME}.icns")
# The bundle identifier and application group need to have compatible values with the client
# to be able to open a Mach port across the extension's sandbox boundary.
# Pass the info through the xcodebuild command line and make sure that the project uses
# those user-defined settings to build the plist.
add_custom_target( mac_overlayplugin ALL
xcodebuild -project ${CMAKE_SOURCE_DIR}/shell_integration/MacOSX/OwnCloudFinderSync/OwnCloudFinderSync.xcodeproj
-target FinderSyncExt -configuration Release "SYMROOT=${CMAKE_CURRENT_BINARY_DIR}"
"OC_OEM_SHARE_ICNS=${OC_OEM_SHARE_ICNS}"
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
# The bundle identifier and application group need to have compatible values with the client
# to be able to open a Mach port across the extension's sandbox boundary.
# Pass the info through the xcodebuild command line and make sure that the project uses
# those user-defined settings to build the plist.
add_custom_target( mac_overlayplugin ALL
xcodebuild ARCHS=${CMAKE_OSX_ARCHITECTURES} ONLY_ACTIVE_ARCH=NO
-project ${CMAKE_SOURCE_DIR}/shell_integration/MacOSX/OwnCloudFinderSync/OwnCloudFinderSync.xcodeproj
-target FinderSyncExt -configuration Release "SYMROOT=${CMAKE_CURRENT_BINARY_DIR}"
"OC_OEM_SHARE_ICNS=${OC_OEM_SHARE_ICNS}"
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
COMMENT building Mac Overlay icons
VERBATIM)
add_dependencies(mac_overlayplugin nextcloud) # for the ownCloud.icns to be generated
add_dependencies(mac_overlayplugin nextcloud) # for the ownCloud.icns to be generated
INSTALL(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Release/FinderSyncExt.appex
DESTINATION ${OWNCLOUD_OSX_BUNDLE}/Contents/PlugIns
USE_SOURCE_PERMISSIONS)
endif(APPLE)
if (BUILD_OWNCLOUD_OSX_BUNDLE)
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Release/FinderSyncExt.appex
DESTINATION ${OWNCLOUD_OSX_BUNDLE}/Contents/PlugIns
USE_SOURCE_PERMISSIONS)
endif()
endif()

280
src/3rdparty/kirigami/wheelhandler.cpp vendored Normal file
View File

@@ -0,0 +1,280 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "wheelhandler.h"
#include <QWheelEvent>
#include <QQuickItem>
#include <QDebug>
class GlobalWheelFilterSingleton
{
public:
GlobalWheelFilter self;
};
Q_GLOBAL_STATIC(GlobalWheelFilterSingleton, privateGlobalWheelFilterSelf)
GlobalWheelFilter::GlobalWheelFilter(QObject *parent)
: QObject(parent)
{
}
GlobalWheelFilter::~GlobalWheelFilter() = default;
GlobalWheelFilter *GlobalWheelFilter::self()
{
return &privateGlobalWheelFilterSelf()->self;
}
void GlobalWheelFilter::setItemHandlerAssociation(QQuickItem *item, WheelHandler *handler)
{
if (!m_handlersForItem.contains(handler->target())) {
handler->target()->installEventFilter(this);
}
m_handlersForItem.insert(item, handler);
connect(item, &QObject::destroyed, this, [this](QObject *obj) {
auto item = static_cast<QQuickItem *>(obj);
m_handlersForItem.remove(item);
});
connect(handler, &QObject::destroyed, this, [this](QObject *obj) {
auto handler = static_cast<WheelHandler *>(obj);
removeItemHandlerAssociation(handler->target(), handler);
});
}
void GlobalWheelFilter::removeItemHandlerAssociation(QQuickItem *item, WheelHandler *handler)
{
if (!item || !handler) {
return;
}
m_handlersForItem.remove(item, handler);
if (!m_handlersForItem.contains(item)) {
item->removeEventFilter(this);
}
}
bool GlobalWheelFilter::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::Wheel) {
auto item = qobject_cast<QQuickItem *>(watched);
if (!item || !item->isEnabled()) {
return QObject::eventFilter(watched, event);
}
auto we = static_cast<QWheelEvent *>(event);
m_wheelEvent.initializeFromEvent(we);
bool shouldBlock = false;
bool shouldScrollFlickable = false;
for (auto *handler : m_handlersForItem.values(item)) {
if (handler->m_blockTargetWheel) {
shouldBlock = true;
}
if (handler->m_scrollFlickableTarget) {
shouldScrollFlickable = true;
}
emit handler->wheel(&m_wheelEvent);
}
if (shouldScrollFlickable && !m_wheelEvent.isAccepted()) {
manageWheel(item, we);
}
if (shouldBlock) {
return true;
}
}
return QObject::eventFilter(watched, event);
}
void GlobalWheelFilter::manageWheel(QQuickItem *target, QWheelEvent *event)
{
// Duck typing: accept everyhint that has all the properties we need
if (target->metaObject()->indexOfProperty("contentX") == -1
|| target->metaObject()->indexOfProperty("contentY") == -1
|| target->metaObject()->indexOfProperty("contentWidth") == -1
|| target->metaObject()->indexOfProperty("contentHeight") == -1
|| target->metaObject()->indexOfProperty("topMargin") == -1
|| target->metaObject()->indexOfProperty("bottomMargin") == -1
|| target->metaObject()->indexOfProperty("leftMargin") == -1
|| target->metaObject()->indexOfProperty("rightMargin") == -1
|| target->metaObject()->indexOfProperty("originX") == -1
|| target->metaObject()->indexOfProperty("originY") == -1) {
return;
}
qreal contentWidth = target->property("contentWidth").toReal();
qreal contentHeight = target->property("contentHeight").toReal();
qreal contentX = target->property("contentX").toReal();
qreal contentY = target->property("contentY").toReal();
qreal topMargin = target->property("topMargin").toReal();
qreal bottomMargin = target->property("bottomMargin").toReal();
qreal leftMargin = target->property("leftMaring").toReal();
qreal rightMargin = target->property("rightMargin").toReal();
qreal originX = target->property("originX").toReal();
qreal originY = target->property("originY").toReal();
// Scroll Y
if (contentHeight > target->height()) {
int y = event->pixelDelta().y() != 0 ? event->pixelDelta().y() : event->angleDelta().y() / 8;
//if we don't have a pixeldelta, apply the configured mouse wheel lines
if (!event->pixelDelta().y()) {
y *= 3; // Magic copied value from Kirigami::Settings
}
// Scroll one page regardless of delta:
if ((event->modifiers() & Qt::ControlModifier) || (event->modifiers() & Qt::ShiftModifier)) {
if (y > 0) {
y = target->height();
} else if (y < 0) {
y = -target->height();
}
}
qreal minYExtent = topMargin - originY;
qreal maxYExtent = target->height() - (contentHeight + bottomMargin + originY);
target->setProperty("contentY", qMin(-maxYExtent, qMax(-minYExtent, contentY - y)));
}
//Scroll X
if (contentWidth > target->width()) {
int x = event->pixelDelta().x() != 0 ? event->pixelDelta().x() : event->angleDelta().x() / 8;
// Special case: when can't scroll vertically, scroll horizontally with vertical wheel as well
if (x == 0 && contentHeight <= target->height()) {
x = event->pixelDelta().y() != 0 ? event->pixelDelta().y() : event->angleDelta().y() / 8;
}
//if we don't have a pixeldelta, apply the configured mouse wheel lines
if (!event->pixelDelta().x()) {
x *= 3; // Magic copied value from Kirigami::Settings
}
// Scroll one page regardless of delta:
if ((event->modifiers() & Qt::ControlModifier) || (event->modifiers() & Qt::ShiftModifier)) {
if (x > 0) {
x = target->width();
} else if (x < 0) {
x = -target->width();
}
}
qreal minXExtent = leftMargin - originX;
qreal maxXExtent = target->width() - (contentWidth + rightMargin + originX);
target->setProperty("contentX", qMin(-maxXExtent, qMax(-minXExtent, contentX - x)));
}
//this is just for making the scrollbar
target->metaObject()->invokeMethod(target, "flick", Q_ARG(double, 0), Q_ARG(double, 1));
target->metaObject()->invokeMethod(target, "cancelFlick");
}
////////////////////////////
KirigamiWheelEvent::KirigamiWheelEvent(QObject *parent)
: QObject(parent)
{}
KirigamiWheelEvent::~KirigamiWheelEvent() = default;
void KirigamiWheelEvent::initializeFromEvent(QWheelEvent *event)
{
m_x = event->position().x();
m_y = event->position().y();
m_angleDelta = event->angleDelta();
m_pixelDelta = event->pixelDelta();
m_buttons = event->buttons();
m_modifiers = event->modifiers();
m_accepted = false;
m_inverted = event->inverted();
}
qreal KirigamiWheelEvent::x() const
{
return m_x;
}
qreal KirigamiWheelEvent::y() const
{
return m_y;
}
QPointF KirigamiWheelEvent::angleDelta() const
{
return m_angleDelta;
}
QPointF KirigamiWheelEvent::pixelDelta() const
{
return m_pixelDelta;
}
int KirigamiWheelEvent::buttons() const
{
return m_buttons;
}
int KirigamiWheelEvent::modifiers() const
{
return m_modifiers;
}
bool KirigamiWheelEvent::inverted() const
{
return m_inverted;
}
bool KirigamiWheelEvent::isAccepted()
{
return m_accepted;
}
void KirigamiWheelEvent::setAccepted(bool accepted)
{
m_accepted = accepted;
}
///////////////////////////////
WheelHandler::WheelHandler(QObject *parent)
: QObject(parent)
{
}
WheelHandler::~WheelHandler() = default;
QQuickItem *WheelHandler::target() const
{
return m_target;
}
void WheelHandler::setTarget(QQuickItem *target)
{
if (m_target == target) {
return;
}
if (m_target) {
GlobalWheelFilter::self()->removeItemHandlerAssociation(m_target, this);
}
m_target = target;
GlobalWheelFilter::self()->setItemHandlerAssociation(target, this);
emit targetChanged();
}
#include "moc_wheelhandler.cpp"

213
src/3rdparty/kirigami/wheelhandler.h vendored Normal file
View File

@@ -0,0 +1,213 @@
/*
* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
#include <QtQml>
#include <QPoint>
#include <QQuickItem>
#include <QObject>
class QWheelEvent;
class WheelHandler;
/**
* Describes the mouse wheel event
*/
class KirigamiWheelEvent : public QObject
{
Q_OBJECT
/**
* x: real
*
* X coordinate of the mouse pointer
*/
Q_PROPERTY(qreal x READ x CONSTANT)
/**
* y: real
*
* Y coordinate of the mouse pointer
*/
Q_PROPERTY(qreal y READ y CONSTANT)
/**
* angleDelta: point
*
* The distance the wheel is rotated in degrees.
* The x and y coordinates indicate the horizontal and vertical wheels respectively.
* A positive value indicates it was rotated up/right, negative, bottom/left
* This value is more likely to be set in traditional mice.
*/
Q_PROPERTY(QPointF angleDelta READ angleDelta CONSTANT)
/**
* pixelDelta: point
*
* provides the delta in screen pixels available on high resolution trackpads
*/
Q_PROPERTY(QPointF pixelDelta READ pixelDelta CONSTANT)
/**
* buttons: int
*
* it contains an OR combination of the buttons that were pressed during the wheel, they can be:
* Qt.LeftButton, Qt.MiddleButton, Qt.RightButton
*/
Q_PROPERTY(int buttons READ buttons CONSTANT)
/**
* modifiers: int
*
* Keyboard mobifiers that were pressed during the wheel event, such as:
* Qt.NoModifier (default, no modifiers)
* Qt.ControlModifier
* Qt.ShiftModifier
* ...
*/
Q_PROPERTY(int modifiers READ modifiers CONSTANT)
/**
* inverted: bool
*
* Whether the delta values are inverted
* On some platformsthe returned delta are inverted, so positive values would mean bottom/left
*/
Q_PROPERTY(bool inverted READ inverted CONSTANT)
/**
* accepted: bool
*
* If set, the event shouldn't be managed anymore,
* for instance it can be used to block the handler to manage the scroll of a view on some scenarios
* @code
* // This handler handles automatically the scroll of
* // flickableItem, unless Ctrl is pressed, in this case the
* // app has custom code to handle Ctrl+wheel zooming
* Kirigami.WheelHandler {
* target: flickableItem
* blockTargetWheel: true
* scrollFlickableTarget: true
* onWheel: {
* if (wheel.modifiers & Qt.ControlModifier) {
* wheel.accepted = true;
* // Handle scaling of the view
* }
* }
* }
* @endcode
*
*/
Q_PROPERTY(bool accepted READ isAccepted WRITE setAccepted)
public:
KirigamiWheelEvent(QObject *parent = nullptr);
~KirigamiWheelEvent() override;
void initializeFromEvent(QWheelEvent *event);
qreal x() const;
qreal y() const;
QPointF angleDelta() const;
QPointF pixelDelta() const;
int buttons() const;
int modifiers() const;
bool inverted() const;
bool isAccepted();
void setAccepted(bool accepted);
private:
qreal m_x = 0;
qreal m_y = 0;
QPointF m_angleDelta;
QPointF m_pixelDelta;
Qt::MouseButtons m_buttons = Qt::NoButton;
Qt::KeyboardModifiers m_modifiers = Qt::NoModifier;
bool m_inverted = false;
bool m_accepted = false;
};
class GlobalWheelFilter : public QObject
{
Q_OBJECT
public:
GlobalWheelFilter(QObject *parent = nullptr);
~GlobalWheelFilter() override;
static GlobalWheelFilter *self();
void setItemHandlerAssociation(QQuickItem *item, WheelHandler *handler);
void removeItemHandlerAssociation(QQuickItem *item, WheelHandler *handler);
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
void manageWheel(QQuickItem *target, QWheelEvent *wheel);
QMultiHash<QQuickItem *, WheelHandler *> m_handlersForItem;
KirigamiWheelEvent m_wheelEvent;
};
/**
* This class intercepts the mouse wheel events of its target, and gives them to the user code as a signal, which can be used for custom mouse wheel management code.
* The handler can block completely the wheel events from its target, and if it's a Flickable, it can automatically handle scrolling on it
*/
class WheelHandler : public QObject
{
Q_OBJECT
/**
* target: Item
*
* The target we want to manage wheel events.
* We will receive wheel() signals every time the user moves
* the mouse wheel (or scrolls with the touchpad) on top
* of that item.
*/
Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged)
/**
* blockTargetWheel: bool
*
* If true, the target won't receive any wheel event at all (default true)
*/
Q_PROPERTY(bool blockTargetWheel MEMBER m_blockTargetWheel NOTIFY blockTargetWheelChanged)
/**
* scrollFlickableTarget: bool
* If this property is true and the target is a Flickable, wheel events will cause the Flickable to scroll (default true)
*/
Q_PROPERTY(bool scrollFlickableTarget MEMBER m_scrollFlickableTarget NOTIFY scrollFlickableTargetChanged)
public:
explicit WheelHandler(QObject *parent = nullptr);
~WheelHandler() override;
QQuickItem *target() const;
void setTarget(QQuickItem *target);
Q_SIGNALS:
void targetChanged();
void blockTargetWheelChanged();
void scrollFlickableTargetChanged();
void wheel(KirigamiWheelEvent *wheel);
private:
QPointer<QQuickItem> m_target;
bool m_blockTargetWheel = true;
bool m_scrollFlickableTarget = true;
KirigamiWheelEvent m_wheelEvent;
friend class GlobalWheelFilter;
};

View File

@@ -1001,43 +1001,22 @@ qint64 SyncJournalDb::keyValueStoreGetInt(const QString &key, qint64 defaultValu
return defaultValue;
}
const auto query = _queryManager.get(PreparedSqlQueryManager::GetKeyValueStoreQuery, QByteArrayLiteral("SELECT value FROM key_value_store WHERE key = ?1;"), _db);
const auto query = _queryManager.get(PreparedSqlQueryManager::GetKeyValueStoreQuery, QByteArrayLiteral("SELECT value FROM key_value_store WHERE key=?1"), _db);
if (!query) {
return defaultValue;
}
query->bindValue(1, key);
query->exec();
auto result = query->next();
if (!query->next().hasData) {
if (!result.ok || !result.hasData) {
return defaultValue;
}
return query->int64Value(0);
}
QVariant SyncJournalDb::keyValueStoreGet(const QString &key, QVariant defaultValue)
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return defaultValue;
}
const auto query = _queryManager.get(PreparedSqlQueryManager::GetKeyValueStoreQuery, QByteArrayLiteral("SELECT value FROM key_value_store WHERE key = ?1;"), _db);
if (!query) {
return defaultValue;
}
query->bindValue(1, key);
query->exec();
if (!query->next().hasData) {
return defaultValue;
}
return query->stringValue(0);
}
void SyncJournalDb::keyValueStoreDelete(const QString &key)
{
const auto query = _queryManager.get(PreparedSqlQueryManager::DeleteKeyValueStoreQuery, QByteArrayLiteral("DELETE FROM key_value_store WHERE key=?1;"), _db);

View File

@@ -70,7 +70,6 @@ public:
void keyValueStoreSet(const QString &key, QVariant value);
qint64 keyValueStoreGetInt(const QString &key, qint64 defaultValue);
QVariant keyValueStoreGet(const QString &key, QVariant defaultValue = {});
void keyValueStoreDelete(const QString &key);
bool deleteFileRecord(const QString &filename, bool recursively = false);

View File

@@ -109,6 +109,11 @@ void Utility::setupFavLink(const QString &folder)
setupFavLink_private(folder);
}
void Utility::removeFavLink(const QString &folder)
{
removeFavLink_private(folder);
}
QString Utility::octetsToString(qint64 octets)
{
#define THE_FACTOR 1024

View File

@@ -55,6 +55,7 @@ namespace Utility {
OCSYNC_EXPORT void usleep(int usec);
OCSYNC_EXPORT QString formatFingerprint(const QByteArray &, bool colonSeparated = true);
OCSYNC_EXPORT void setupFavLink(const QString &folder);
OCSYNC_EXPORT void removeFavLink(const QString &folder);
OCSYNC_EXPORT bool writeRandomFile(const QString &fname, int size = -1);
OCSYNC_EXPORT QString octetsToString(qint64 octets);
OCSYNC_EXPORT QByteArray userAgentString();
@@ -241,6 +242,11 @@ namespace Utility {
*/
OCSYNC_EXPORT bool isPathWindowsDrivePartitionRoot(const QString &path);
/**
* @brief Retrieves current logged-in user name from the OS
*/
OCSYNC_EXPORT QString getCurrentUserName();
#ifdef Q_OS_WIN
OCSYNC_EXPORT bool registryKeyExists(HKEY hRootKey, const QString &subKey);
OCSYNC_EXPORT QVariant registryGetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName);

View File

@@ -41,6 +41,11 @@ static void setupFavLink_private(const QString &folder)
CFRelease(urlRef);
}
static void removeFavLink_private(const QString &folder)
{
Q_UNUSED(folder)
}
bool hasLaunchOnStartup_private(const QString &)
{
// this is quite some duplicate code with setLaunchOnStartup, at some point we should fix this FIXME.
@@ -131,4 +136,9 @@ static bool hasDarkSystray_private()
return returnValue;
}
QString Utility::getCurrentUserName()
{
return {};
}
} // namespace OCC

View File

@@ -37,6 +37,11 @@ static void setupFavLink_private(const QString &folder)
}
}
static void removeFavLink_private(const QString &folder)
{
Q_UNUSED(folder)
}
// returns the autostart directory the linux way
// and respects the XDG_CONFIG_HOME env variable
QString getUserAutostartDir_private()
@@ -103,4 +108,9 @@ static inline bool hasDarkSystray_private()
return true;
}
QString Utility::getCurrentUserName()
{
return {};
}
} // namespace OCC

View File

@@ -18,8 +18,10 @@
#include "asserts.h"
#include "utility.h"
#include "gui/configgui.h"
#include <comdef.h>
#include <Lmcons.h>
#include <shlguid.h>
#include <shlobj.h>
#include <string>
@@ -47,7 +49,14 @@ static void setupFavLink_private(const QString &folder)
desktopIni.open(QFile::WriteOnly);
desktopIni.write("[.ShellClassInfo]\r\nIconResource=");
desktopIni.write(QDir::toNativeSeparators(qApp->applicationFilePath()).toUtf8());
desktopIni.write(",0\r\n");
#ifdef APPLICATION_FOLDER_ICON_INDEX
const auto iconIndex = APPLICATION_FOLDER_ICON_INDEX;
#else
const auto iconIndex = "0";
#endif
desktopIni.write(",");
desktopIni.write(iconIndex);
desktopIni.write("\r\n");
desktopIni.close();
// Set the folder as system and Desktop.ini as hidden+system for explorer to pick it.
@@ -74,6 +83,40 @@ static void setupFavLink_private(const QString &folder)
qCWarning(lcUtility) << "linking" << folder << "to" << linkName << "failed!";
}
static void removeFavLink_private(const QString &folder)
{
const QDir folderDir(folder);
// #1 Remove the Desktop.ini to reset the folder icon
if (!QFile::remove(folderDir.absoluteFilePath(QLatin1String("Desktop.ini")))) {
qCWarning(lcUtility) << "Remove Desktop.ini from" << folder
<< " has failed. Make sure it exists and is not locked by another process.";
}
// #2 Remove the system file attribute
const auto folderAttrs = GetFileAttributesW(folder.toStdWString().c_str());
if (!SetFileAttributesW(folder.toStdWString().c_str(), folderAttrs & ~FILE_ATTRIBUTE_SYSTEM)) {
qCWarning(lcUtility) << "Remove system file attribute failed for:" << folder;
}
// #3 Remove the link to this folder
PWSTR path;
if (!SHGetKnownFolderPath(FOLDERID_Links, 0, nullptr, &path) == S_OK) {
qCWarning(lcUtility) << "SHGetKnownFolderPath for " << folder << "has failed.";
return;
}
const QDir links(QString::fromWCharArray(path));
CoTaskMemFree(path);
const auto linkName = QDir(links).absoluteFilePath(folderDir.dirName() + QLatin1String(".lnk"));
qCInfo(lcUtility) << "Removing favorite link from" << folder << "to" << linkName;
if (!QFile::remove(linkName)) {
qCWarning(lcUtility) << "Removing a favorite link from" << folder << "to" << linkName << "failed.";
}
}
bool hasSystemLaunchOnStartup_private(const QString &appName)
{
QString runPath = QLatin1String(systemRunPathC);
@@ -346,6 +389,17 @@ QString Utility::formatWinError(long errorCode)
return QStringLiteral("WindowsError: %1: %2").arg(QString::number(errorCode, 16), QString::fromWCharArray(_com_error(errorCode).ErrorMessage()));
}
QString Utility::getCurrentUserName()
{
TCHAR username[UNLEN + 1] = {0};
DWORD len = sizeof(username) / sizeof(TCHAR);
if (!GetUserName(username, &len)) {
qCWarning(lcUtility) << "Could not retrieve Windows user name." << formatWinError(GetLastError());
}
return QString::fromWCharArray(username);
}
Utility::NtfsPermissionLookupRAII::NtfsPermissionLookupRAII()
{

View File

@@ -1,5 +1,8 @@
project(gui)
find_package(Qt5 REQUIRED COMPONENTS Widgets Svg Qml Quick QuickControls2)
find_package(Qt5 REQUIRED COMPONENTS Widgets Svg Qml Quick QuickControls2 Xml Network)
if (NOT TARGET Qt5::GuiPrivate)
message(FATAL_ERROR "Could not find GuiPrivate component of Qt5. It might be shipped as a separate package, please check that.")
endif()
if(CMAKE_BUILD_TYPE MATCHES Debug)
add_definitions(-DQT_QML_DEBUG)
@@ -34,6 +37,7 @@ set(client_UI_SRCS
shareuserline.ui
sslerrordialog.ui
addcertificatedialog.ui
passwordinputdialog.ui
proxyauthdialog.ui
mnemonicdialog.ui
UserStatusSelector.qml
@@ -92,6 +96,7 @@ set(client_SRCS
openfilemanager.cpp
owncloudgui.cpp
owncloudsetupwizard.cpp
passwordinputdialog.cpp
selectivesyncdialog.cpp
settingsdialog.cpp
sharedialog.cpp
@@ -203,6 +208,7 @@ set(3rdparty_SRC
../3rdparty/qtsingleapplication/qtsingleapplication.cpp
../3rdparty/qtsingleapplication/qtsinglecoreapplication.cpp
../3rdparty/kmessagewidget/kmessagewidget.cpp
../3rdparty/kirigami/wheelhandler.cpp
)
if(NOT WIN32)
@@ -254,6 +260,10 @@ if (NOT DEFINED APPLICATION_ICON_NAME)
set(APPLICATION_ICON_NAME ${APPLICATION_SHORTNAME})
endif()
if(NOT DEFINED APPLICATION_FOLDER_ICON_INDEX)
set(APPLICATION_FOLDER_ICON_INDEX 0)
endif()
# Generate png icons from svg
find_program(SVG_CONVERTER
NAMES inkscape inkscape.exe rsvg-convert
@@ -265,9 +275,24 @@ if (NOT SVG_CONVERTER)
endif()
function(generate_sized_png_from_svg icon_path size)
set(options)
set(oneValueArgs OUTPUT_ICON_NAME OUTPUT_ICON_PATH)
set(multiValueArgs)
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
get_filename_component(icon_name_dir ${icon_path} DIRECTORY)
get_filename_component(icon_name_wle ${icon_path} NAME_WLE)
if (ARG_OUTPUT_ICON_NAME)
set(icon_name_wle ${ARG_OUTPUT_ICON_NAME})
endif ()
if (ARG_OUTPUT_ICON_PATH)
set(icon_name_dir ${ARG_OUTPUT_ICON_PATH})
endif ()
if (EXISTS "${icon_name_dir}/${size}-${icon_name_wle}.png")
return()
endif()
@@ -313,22 +338,86 @@ if(WIN32)
endif()
set(APP_ICON_SVG "${theme_dir}/colored/${APPLICATION_ICON_NAME}-icon.svg")
generate_sized_png_from_svg(${APP_ICON_SVG} 16)
generate_sized_png_from_svg(${APP_ICON_SVG} 24)
generate_sized_png_from_svg(${APP_ICON_SVG} 32)
generate_sized_png_from_svg(${APP_ICON_SVG} 48)
generate_sized_png_from_svg(${APP_ICON_SVG} 64)
generate_sized_png_from_svg(${APP_ICON_SVG} 128)
generate_sized_png_from_svg(${APP_ICON_SVG} 256)
generate_sized_png_from_svg(${APP_ICON_SVG} 512)
generate_sized_png_from_svg(${APP_ICON_SVG} 1024)
# generate secondary icon if available (currently for Windows only)--------------------------------------
set(APP_SECONDARY_ICONS "${theme_dir}/colored/icons")
set(APP_ICON_WIN_FOLDER_SVG "${APP_SECONDARY_ICONS}/${APPLICATION_ICON_NAME}-icon-win-folder.svg")
set(RC_DEPENDENCIES "")
if(WIN32)
if (EXISTS ${APP_ICON_WIN_FOLDER_SVG})
get_filename_component(output_icon_name_win ${APP_ICON_WIN_FOLDER_SVG} NAME_WLE)
# Product icon (for smallest size)
foreach(size IN ITEMS 16;20)
generate_sized_png_from_svg(${APP_ICON_SVG} ${size} OUTPUT_ICON_NAME ${output_icon_name_win} OUTPUT_ICON_PATH "${APP_SECONDARY_ICONS}/")
endforeach()
# Product icon with Windows folder (for sizes larger than 20)
foreach(size IN ITEMS 24;32;40;48;64;128;256;512;1024)
generate_sized_png_from_svg(${APP_ICON_WIN_FOLDER_SVG} ${size} OUTPUT_ICON_NAME ${output_icon_name_win} OUTPUT_ICON_PATH "${APP_SECONDARY_ICONS}/")
endforeach()
file(GLOB_RECURSE OWNCLOUD_ICONS_WIN_FOLDER "${APP_SECONDARY_ICONS}/*-${APPLICATION_ICON_NAME}-icon*")
set(APP_ICON_WIN_FOLDER_ICO_NAME "${APPLICATION_ICON_NAME}-win-folder")
set(RC_DEPENDENCIES "${RC_DEPENDENCIES} ${APP_ICON_WIN_FOLDER_ICO_NAME}.ico")
ecm_add_app_icon(APP_ICON_WIN_FOLDER ICONS "${OWNCLOUD_ICONS_WIN_FOLDER}" SIDEBAR_ICONS "${OWNCLOUD_SIDEBAR_ICONS}" OUTFILE_BASENAME "${APP_ICON_WIN_FOLDER_ICO_NAME}" ICON_INDEX 2)
endif()
endif()
# --------------------------------------
if (NOT ${RC_DEPENDENCIES} STREQUAL "")
string(STRIP ${RC_DEPENDENCIES} RC_DEPENDENCIES)
endif()
# generate primary icon from SVG (due to Win .ico vs .rc dependency issues, primary icon must always be generated last)--------------------------------------
if(WIN32)
foreach(size IN ITEMS 16;20;24;32;40;48;64;128;256;512;1024)
generate_sized_png_from_svg(${APP_ICON_SVG} ${size})
endforeach()
else()
foreach(size IN ITEMS 16;24;32;48;64;128;256;512;1024)
generate_sized_png_from_svg(${APP_ICON_SVG} ${size})
endforeach()
endif()
file(GLOB_RECURSE OWNCLOUD_ICONS "${theme_dir}/colored/*-${APPLICATION_ICON_NAME}-icon*")
if(APPLE)
file(GLOB_RECURSE OWNCLOUD_SIDEBAR_ICONS "${theme_dir}/colored/*-${APPLICATION_ICON_NAME}-sidebar*")
MESSAGE(STATUS "OWNCLOUD_SIDEBAR_ICONS: ${APPLICATION_ICON_NAME}: ${OWNCLOUD_SIDEBAR_ICONS}")
endif()
ecm_add_app_icon(APP_ICON ICONS "${OWNCLOUD_ICONS}" SIDEBAR_ICONS "${OWNCLOUD_SIDEBAR_ICONS}" OUTFILE_BASENAME "${APPLICATION_ICON_NAME}")
ecm_add_app_icon(APP_ICON RC_DEPENDENCIES ${RC_DEPENDENCIES} ICONS "${OWNCLOUD_ICONS}" SIDEBAR_ICONS "${OWNCLOUD_SIDEBAR_ICONS}" OUTFILE_BASENAME "${APPLICATION_ICON_NAME}" ICON_INDEX 1)
# --------------------------------------
if(WIN32)
# merge *.rc.in files for Windows (multiple ICON resources must be placed in a single file, otherwise, this won't work de to a bug in Windows compiler https://developercommunity.visualstudio.com/t/visual-studio-2017-prof-1557-cvt1100-duplicate-res/363156)
function(merge_files IN_FILE OUT_FILE)
file(READ ${IN_FILE} CONTENTS)
message("Merging ${IN_FILE} into ${OUT_FILE}")
file(APPEND ${OUT_FILE} "${CONTENTS}")
endfunction()
message("APP_ICON is: ${APP_ICON}")
if(APP_ICON)
get_filename_component(RC_IN_FOLDER ${APP_ICON}} DIRECTORY)
file(GLOB_RECURSE RC_IN_FILES "${RC_IN_FOLDER}/*rc.in")
foreach(rc_in_file IN ITEMS ${RC_IN_FILES})
get_filename_component(rc_in_file_name ${rc_in_file} NAME)
get_filename_component(app_icon_name "${APP_ICON}.in" NAME)
if(NOT "${rc_in_file_name}" STREQUAL "${app_icon_name}")
merge_files(${rc_in_file} "${APP_ICON}.in")
if (DEFINED APPLICATION_FOLDER_ICON_INDEX)
MATH(EXPR APPLICATION_FOLDER_ICON_INDEX "${APPLICATION_FOLDER_ICON_INDEX}+1")
message("APPLICATION_FOLDER_ICON_INDEX is now set to: ${APPLICATION_FOLDER_ICON_INDEX}")
endif()
endif()
endforeach()
endif()
endif()
# --------------------------------------
if(UNIX AND NOT APPLE)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIE")
@@ -372,6 +461,7 @@ target_include_directories(nextcloudCore
PUBLIC
${CMAKE_SOURCE_DIR}/src/3rdparty/QProgressIndicator
${CMAKE_SOURCE_DIR}/src/3rdparty/qtlockedfile
${CMAKE_SOURCE_DIR}/src/3rdparty/kirigami
${CMAKE_SOURCE_DIR}/src/3rdparty/qtsingleapplication
${CMAKE_SOURCE_DIR}/src/3rdparty/kmessagewidget
${CMAKE_CURRENT_BINARY_DIR}
@@ -540,3 +630,5 @@ if(NOT BUILD_OWNCLOUD_OSX_BUNDLE AND NOT WIN32)
update_xdg_mimetypes( ${CMAKE_INSTALL_DATADIR}/mime/packages )
endif(SharedMimeInfo_FOUND)
endif()
configure_file(configgui.h.in ${CMAKE_CURRENT_BINARY_DIR}/configgui.h)

View File

@@ -587,6 +587,7 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos)
ac = availabilityMenu->addAction(Utility::vfsPinActionText());
connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::AlwaysLocal); });
ac->setDisabled(Theme::instance()->enforceVirtualFilesSyncFolder());
ac = availabilityMenu->addAction(Utility::vfsFreeSpaceActionText());
connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::OnlineOnly); });
@@ -761,6 +762,7 @@ void AccountSettings::slotRemoveCurrentFolder()
messageBox->addButton(tr("Cancel"), QMessageBox::NoRole);
connect(messageBox, &QMessageBox::finished, this, [messageBox, yesButton, folder, row, this]{
if (messageBox->clickedButton() == yesButton) {
Utility::removeFavLink(folder->path());
FolderMan::instance()->removeFolder(folder);
_model->removeRow(row);

View File

@@ -221,6 +221,19 @@ void AccountState::setDesktopNotificationsAllowed(bool isAllowed)
emit desktopNotificationsAllowedChanged();
}
AccountState::ConnectionStatus AccountState::lastConnectionStatus() const
{
return _lastConnectionValidatorStatus;
}
void AccountState::trySignIn()
{
if (isSignedOut() && account()) {
account()->resetRejectedCertificates();
signIn();
}
}
void AccountState::checkConnectivity()
{
if (isSignedOut() || _waitingForNewCredentials) {
@@ -285,6 +298,8 @@ void AccountState::slotConnectionValidatorResult(ConnectionValidator::Status sta
return;
}
_lastConnectionValidatorStatus = status;
// Come online gradually from 503 or maintenance mode
if (status == ConnectionValidator::Connected
&& (_connectionStatus == ConnectionValidator::ServiceUnavailable

View File

@@ -171,6 +171,10 @@ public:
*/
void setDesktopNotificationsAllowed(bool isAllowed);
ConnectionStatus lastConnectionStatus() const;
void trySignIn();
public slots:
/// Triggers a ping to the server to update state and
/// connection status and errors.
@@ -205,6 +209,7 @@ private:
AccountPtr _account;
State _state;
ConnectionStatus _connectionStatus;
ConnectionStatus _lastConnectionValidatorStatus = ConnectionStatus::Undefined;
QStringList _connectionErrors;
bool _waitingForNewCredentials;
QDateTime _timeOfLastETagCheck;

View File

@@ -465,6 +465,9 @@ void Application::slotCheckConnection()
if (state != AccountState::SignedOut && state != AccountState::ConfigurationError
&& state != AccountState::AskingCredentials && !pushNotificationsAvailable) {
accountState->checkConnectivity();
} else if (state == AccountState::SignedOut && accountState->lastConnectionStatus() == AccountState::ConnectionStatus::SslError) {
qCWarning(lcApplication) << "Account is signed out due to SSL Handshake error. Going to perform a sign-in attempt...";
accountState->trySignIn();
}
}

4
src/gui/configgui.h.in Normal file
View File

@@ -0,0 +1,4 @@
#ifndef CONFIG_GUI_H
#define CONFIG_GUI_H
#cmakedefine APPLICATION_FOLDER_ICON_INDEX "@APPLICATION_FOLDER_ICON_INDEX@"
#endif

View File

@@ -136,7 +136,7 @@ void ConnectionValidator::slotStatusFound(const QUrl &url, const QJsonObject &in
void ConnectionValidator::slotNoStatusFound(QNetworkReply *reply)
{
auto job = qobject_cast<CheckServerJob *>(sender());
qCWarning(lcConnectionValidator) << reply->error() << job->errorString() << reply->peek(1024);
qCWarning(lcConnectionValidator) << reply->error() << reply->errorString() << job->errorString() << reply->peek(1024);
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
reportResult(SslError);
return;

View File

@@ -132,6 +132,16 @@ void Flow2Auth::fetchNewToken(const TokenAction action)
_loginUrl = loginUrl;
if (_account->isUsernamePrefillSupported()) {
const auto userName = Utility::getCurrentUserName();
if (!userName.isEmpty()) {
auto query = QUrlQuery(_loginUrl);
query.addQueryItem(QStringLiteral("user"), userName);
_loginUrl.setQuery(query);
}
}
_pollToken = pollToken;
_pollEndpoint = pollEndpoint;

View File

@@ -691,6 +691,15 @@ void Folder::setRootPinState(PinState state)
void Folder::switchToVirtualFiles()
{
SyncEngine::switchToVirtualFiles(path(), _journal, *_vfs);
_hasSwitchedToVfs = true;
}
void Folder::processSwitchedToVirtualFiles()
{
if (_hasSwitchedToVfs) {
_hasSwitchedToVfs = false;
saveToSettings();
}
}
bool Folder::supportsSelectiveSync() const
@@ -866,11 +875,27 @@ void Folder::startSync(const QStringList &pathList)
_engine->setIgnoreHiddenFiles(_definition.ignoreHiddenFiles);
correctPlaceholderFiles();
QMetaObject::invokeMethod(_engine.data(), "startSync", Qt::QueuedConnection);
emit syncStarted();
}
void Folder::correctPlaceholderFiles()
{
if (_definition.virtualFilesMode == Vfs::Off) {
return;
}
static const auto placeholdersCorrectedKey = QStringLiteral("placeholders_corrected");
const auto placeholdersCorrected = _journal.keyValueStoreGetInt(placeholdersCorrectedKey, 0);
if (!placeholdersCorrected) {
qCDebug(lcFolder) << "Make sure all virtual files are placeholder files";
switchToVirtualFiles();
_journal.keyValueStoreSet(placeholdersCorrectedKey, true);
}
}
void Folder::setSyncOptions()
{
SyncOptions opt;

View File

@@ -289,6 +289,8 @@ public:
void switchToVirtualFiles();
void processSwitchedToVirtualFiles();
/** Whether this folder should show selective sync ui */
bool supportsSelectiveSync() const;
@@ -444,6 +446,8 @@ private:
void startVfs();
void correctPlaceholderFiles();
AccountStatePtr _accountState;
FolderDefinition _definition;
QString _canonicalLocalPath; // As returned with QFileInfo:canonicalFilePath. Always ends with "/"
@@ -498,6 +502,10 @@ private:
*/
bool _vfsOnOffPending = false;
/** Whether this folder has just switched to VFS or not
*/
bool _hasSwitchedToVfs = false;
/**
* Watches this folder's local directory for changes.
*

View File

@@ -211,6 +211,10 @@ int FolderMan::setupFolders()
emit folderListChanged(_folderMap);
for (const auto folder : _folderMap) {
folder->processSwitchedToVirtualFiles();
}
return _folderMap.size();
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "passwordinputdialog.h"
#include "ui_passwordinputdialog.h"
namespace OCC {
PasswordInputDialog::PasswordInputDialog(const QString &description, const QString &error, QWidget *parent)
: QDialog(parent)
, _ui(new Ui::PasswordInputDialog)
{
_ui->setupUi(this);
_ui->passwordLineEditLabel->setText(description);
_ui->passwordLineEditLabel->setVisible(!description.isEmpty());
_ui->labelErrorMessage->setText(error);
_ui->labelErrorMessage->setVisible(!error.isEmpty());
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
}
PasswordInputDialog::~PasswordInputDialog() = default;
QString PasswordInputDialog::password() const
{
return _ui->passwordLineEdit->text();
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) by Oleksandr Zolotov <alex@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#pragma once
#include <QDialog>
namespace OCC {
namespace Ui {
class PasswordInputDialog;
}
class PasswordInputDialog : public QDialog
{
Q_OBJECT
public:
explicit PasswordInputDialog(const QString &description, const QString &error, QWidget *parent = nullptr);
~PasswordInputDialog() override;
QString password() const;
private:
std::unique_ptr<Ui::PasswordInputDialog> _ui;
};
}

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OCC::PasswordInputDialog</class>
<widget class="QDialog" name="OCC::PasswordInputDialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>276</width>
<height>125</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Password for share required</string>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="passwordLineEditLabel">
<property name="text">
<string>Please enter a password for your share:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="passwordLineEdit">
<property name="inputMask">
<string notr="true"/>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelErrorMessage">
<property name="enabled">
<bool>true</bool>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(118, 118, 118)</string>
</property>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>OCC::PasswordInputDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>OCC::PasswordInputDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -17,6 +17,7 @@
#include "sharee.h"
#include "sharelinkwidget.h"
#include "shareusergroupwidget.h"
#include "passwordinputdialog.h"
#include "sharemanager.h"
@@ -135,21 +136,7 @@ ShareDialog::ShareDialog(QPointer<AccountState> accountState,
connect(job, &PropfindJob::finishedWithError, this, &ShareDialog::slotPropfindError);
job->start();
bool sharingPossible = true;
if (!accountState->account()->capabilities().sharePublicLink()) {
qCWarning(lcSharing) << "Link shares have been disabled";
sharingPossible = false;
} else if (!(maxSharingPermissions & SharePermissionShare)) {
qCWarning(lcSharing) << "The file cannot be shared because it does not have sharing permission.";
sharingPossible = false;
}
if (sharingPossible) {
_manager = new ShareManager(accountState->account(), this);
connect(_manager, &ShareManager::sharesFetched, this, &ShareDialog::slotSharesFetched);
connect(_manager, &ShareManager::linkShareCreated, this, &ShareDialog::slotAddLinkShareWidget);
connect(_manager, &ShareManager::linkShareRequiresPassword, this, &ShareDialog::slotLinkShareRequiresPassword);
}
initShareManager();
}
ShareLinkWidget *ShareDialog::addLinkShareWidget(const QSharedPointer<LinkShare> &linkShare)
@@ -318,6 +305,8 @@ void ShareDialog::showSharingUi()
_userGroupWidget->getShares();
}
initShareManager();
if (theme->linkSharing()) {
if(_manager) {
_manager->fetchShares(_sharePath);
@@ -325,6 +314,25 @@ void ShareDialog::showSharingUi()
}
}
void ShareDialog::initShareManager()
{
bool sharingPossible = true;
if (!_accountState->account()->capabilities().sharePublicLink()) {
qCWarning(lcSharing) << "Link shares have been disabled";
sharingPossible = false;
} else if (!(_maxSharingPermissions & SharePermissionShare)) {
qCWarning(lcSharing) << "The file cannot be shared because it does not have sharing permission.";
sharingPossible = false;
}
if (!_manager && sharingPossible) {
_manager = new ShareManager(_accountState->account(), this);
connect(_manager, &ShareManager::sharesFetched, this, &ShareDialog::slotSharesFetched);
connect(_manager, &ShareManager::linkShareCreated, this, &ShareDialog::slotAddLinkShareWidget);
connect(_manager, &ShareManager::linkShareRequiresPassword, this, &ShareDialog::slotLinkShareRequiresPassword);
}
}
void ShareDialog::slotCreateLinkShare()
{
if(_manager) {
@@ -359,26 +367,21 @@ void ShareDialog::slotCreatePasswordForLinkShareProcessed()
}
}
void ShareDialog::slotLinkShareRequiresPassword()
void ShareDialog::slotLinkShareRequiresPassword(const QString &message)
{
bool ok = false;
QString password = QInputDialog::getText(this,
tr("Password for share required"),
tr("Please enter a password for your link share:"),
QLineEdit::Password,
QString(),
&ok);
const auto passwordInputDialog = new PasswordInputDialog(tr("Please enter a password for your link share:"), message, this);
passwordInputDialog->setWindowTitle(tr("Password for share required"));
passwordInputDialog->setAttribute(Qt::WA_DeleteOnClose);
passwordInputDialog->open();
if (!ok) {
// The dialog was canceled so no need to do anything
connect(passwordInputDialog, &QDialog::finished, this, [this, passwordInputDialog](const int result) {
if (result == QDialog::Accepted && _manager) {
// Try to create the link share again with the newly entered password
_manager->createLinkShare(_sharePath, QString(), passwordInputDialog->password());
return;
}
emit toggleShareLinkAnimation(false);
return;
}
if(_manager) {
// Try to create the link share again with the newly entered password
_manager->createLinkShare(_sharePath, QString(), password);
}
});
}
void ShareDialog::slotDeleteShare()

View File

@@ -66,7 +66,7 @@ private slots:
void slotCreateLinkShare();
void slotCreatePasswordForLinkShare(const QString &password);
void slotCreatePasswordForLinkShareProcessed();
void slotLinkShareRequiresPassword();
void slotLinkShareRequiresPassword(const QString &message);
void slotAdjustScrollWidgetSize();
signals:
@@ -78,6 +78,7 @@ protected:
private:
void showSharingUi();
void initShareManager();
ShareLinkWidget *addLinkShareWidget(const QSharedPointer<LinkShare> &linkShare);
void initLinkShareWidget();

View File

@@ -1225,9 +1225,11 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
auto makePinContextMenu = [&](bool makeAvailableLocally, bool freeSpace) {
listener->sendMessage(QLatin1String("MENU_ITEM:CURRENT_PIN:d:")
+ Utility::vfsCurrentAvailabilityText(*combined));
listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_AVAILABLE_LOCALLY:")
+ (makeAvailableLocally ? QLatin1String(":") : QLatin1String("d:"))
+ Utility::vfsPinActionText());
if (!Theme::instance()->enforceVirtualFilesSyncFolder()) {
listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_AVAILABLE_LOCALLY:")
+ (makeAvailableLocally ? QLatin1String(":") : QLatin1String("d:")) + Utility::vfsPinActionText());
}
listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_ONLINE_ONLY:")
+ (freeSpace ? QLatin1String(":") : QLatin1String("d:"))
+ Utility::vfsFreeSpaceActionText());

View File

@@ -19,8 +19,10 @@
#include "common/utility.h"
#include "tray/svgimageprovider.h"
#include "tray/usermodel.h"
#include "wheelhandler.h"
#include "tray/unifiedsearchresultimageprovider.h"
#include "configfile.h"
#include "accessmanager.h"
#include <QCursor>
#include <QGuiApplication>
@@ -58,6 +60,8 @@ void Systray::setTrayEngine(QQmlApplicationEngine *trayEngine)
{
_trayEngine = trayEngine;
_trayEngine->setNetworkAccessManagerFactory(&_accessManagerFactory);
_trayEngine->addImportPath("qrc:/qml/theme");
_trayEngine->addImageProvider("avatars", new ImageProvider);
_trayEngine->addImageProvider(QLatin1String("svgimage-custom-color"), new OCC::Ui::SvgImageProvider);
@@ -91,6 +95,8 @@ Systray::Systray()
}
);
qmlRegisterType<WheelHandler>("com.nextcloud.desktopclient", 1, 0, "WheelHandler");
#ifndef Q_OS_MAC
auto contextMenu = new QMenu();
if (AccountManager::instance()->accounts().isEmpty()) {
@@ -502,4 +508,14 @@ QPoint Systray::calcTrayIconCenter() const
#endif
}
AccessManagerFactory::AccessManagerFactory()
: QQmlNetworkAccessManagerFactory()
{
}
QNetworkAccessManager* AccessManagerFactory::create(QObject *parent)
{
return new AccessManager(parent);
}
} // namespace OCC

View File

@@ -20,6 +20,8 @@
#include "accountmanager.h"
#include "tray/usermodel.h"
#include <QQmlNetworkAccessManagerFactory>
class QScreen;
class QQmlApplicationEngine;
class QQuickWindow;
@@ -28,6 +30,14 @@ class QQuickWindow;
namespace OCC {
class AccessManagerFactory : public QQmlNetworkAccessManagerFactory
{
public:
AccessManagerFactory();
QNetworkAccessManager* create(QObject *parent) override;
};
#ifdef Q_OS_OSX
bool canOsXSendUserNotification();
void sendOsXUserNotification(const QString &title, const QString &message);
@@ -105,6 +115,8 @@ private:
bool _isOpen = false;
bool _syncIsPaused = true;
QPointer<QQmlApplicationEngine> _trayEngine;
AccessManagerFactory _accessManagerFactory;
};
} // namespace OCC

Binary file not shown.

View File

@@ -20,6 +20,10 @@ MouseArea {
anchors.fill: parent
color: (parent.containsMouse ? Style.lightHover : "transparent")
}
ToolTip.visible: containsMouse && displayLocation !== ""
ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
ToolTip.text: qsTr("In %1").arg(displayLocation)
RowLayout {
id: activityItem
@@ -152,7 +156,7 @@ MouseArea {
Layout.alignment: Qt.AlignRight
flat: true
hoverEnabled: true
visible: displayActions && (path !== "")
visible: isShareable
display: AbstractButton.IconOnly
icon.source: "qrc:///client/theme/share.svg"
icon.color: "transparent"

View File

@@ -6,15 +6,21 @@ import Style 1.0
import com.nextcloud.desktopclient 1.0 as NC
ScrollView {
id: controlRoot
property alias model: activityList.model
signal showFileActivity(string displayPath, string absolutePath)
signal activityItemClicked(int index)
contentWidth: availableWidth
padding: 1
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
data: NC.WheelHandler {
target: controlRoot.contentItem
}
ListView {
id: activityList

View File

@@ -25,7 +25,7 @@ Button {
Layout.preferredHeight: Style.trayWindowHeaderHeight
background: Rectangle {
color: root.hovered ? "white" : "transparent"
color: root.hovered || root.visualFocus ? "white" : "transparent"
opacity: 0.2
}
}

View File

@@ -16,6 +16,7 @@ MenuItem {
property variant dialog;
property variant comp;
activeFocusOnTab: false
signal showUserStatusSelectorDialog(int id)
@@ -35,29 +36,19 @@ MenuItem {
Accessible.role: Accessible.Button
Accessible.name: qsTr("Switch to account") + " " + name
MouseArea {
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: {
accountStatusIndicatorBackground.color = (containsMouse ? "#f6f6f6" : "white")
}
onClicked: {
if (!isCurrentUser) {
UserModel.switchCurrentUser(id)
} else {
accountMenu.close()
}
}
onClicked: if (!isCurrentUser) {
UserModel.switchCurrentUser(id)
} else {
accountMenu.close()
}
background: Item {
height: parent.height
width: userLine.menu ? userLine.menu.width : 0
Rectangle {
anchors.fill: parent
anchors.margins: 1
color: parent.parent.hovered ? Style.lightHover : "transparent"
color: parent.parent.hovered || parent.parent.visualFocus ? Style.lightHover : "transparent"
}
}
@@ -81,7 +72,7 @@ MenuItem {
height: width
anchors.bottom: accountAvatar.bottom
anchors.right: accountAvatar.right
color: "white"
color: accountButton.hovered || accountButton.visualFocus ? "#f6f6f6" : "white"
radius: width*0.5
}
Image {
@@ -163,21 +154,16 @@ MenuItem {
Accessible.name: qsTr("Account actions")
Accessible.onPressAction: userMoreButtonMouseArea.clicked()
MouseArea {
id: userMoreButtonMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (userMoreButtonMenu.visible) {
userMoreButtonMenu.close()
} else {
userMoreButtonMenu.popup()
}
onClicked: {
if (userMoreButtonMenu.visible) {
userMoreButtonMenu.close()
} else {
userMoreButtonMenu.popup()
}
}
background:
Rectangle {
color: userMoreButtonMouseArea.containsMouse ? "grey" : "transparent"
color: userMoreButton.hovered || userMoreButton.visualFocus ? "grey" : "transparent"
opacity: 0.2
height: userMoreButton.height - 2
y: userMoreButton.y + 1
@@ -196,7 +182,6 @@ MenuItem {
MenuItem {
visible: model.isConnected && model.serverHasUserStatus
height: visible ? implicitHeight : 0
text: qsTr("Set status")
font.pixelSize: Style.topLinePixelSize
hoverEnabled: true

View File

@@ -142,187 +142,180 @@ Window {
Accessible.name: qsTr("Current account")
Accessible.onPressAction: currentAccountButton.clicked()
MouseArea {
id: accountBtnMouseArea
// We call open() instead of popup() because we want to position it
// exactly below the dropdown button, not the mouse
onClicked: {
syncPauseButton.text = Systray.syncIsPaused() ? qsTr("Resume sync for all") : qsTr("Pause sync for all")
if (accountMenu.visible) {
accountMenu.close()
} else {
accountMenu.open()
}
}
anchors.fill: parent
hoverEnabled: Style.hoverEffectsEnabled
Loader {
id: userStatusSelectorDialogLoader
}
// We call open() instead of popup() because we want to position it
// exactly below the dropdown button, not the mouse
onClicked: {
syncPauseButton.text = Systray.syncIsPaused() ? qsTr("Resume sync for all") : qsTr("Pause sync for all")
if (accountMenu.visible) {
accountMenu.close()
} else {
accountMenu.open()
}
Menu {
id: accountMenu
// x coordinate grows towards the right
// y coordinate grows towards the bottom
x: (currentAccountButton.x + 2)
y: (currentAccountButton.y + Style.trayWindowHeaderHeight + 2)
width: (Style.currentAccountButtonWidth - 2)
height: Math.min(implicitHeight, maxMenuHeight)
closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape
background: Rectangle {
border.color: Style.menuBorder
radius: Style.currentAccountButtonRadius
}
Loader {
id: userStatusSelectorDialogLoader
onClosed: {
// HACK: reload account Instantiator immediately by restting it - could be done better I guess
// see also onVisibleChanged above
userLineInstantiator.active = false;
userLineInstantiator.active = true;
}
Menu {
id: accountMenu
// x coordinate grows towards the right
// y coordinate grows towards the bottom
x: (currentAccountButton.x + 2)
y: (currentAccountButton.y + Style.trayWindowHeaderHeight + 2)
width: (Style.currentAccountButtonWidth - 2)
height: Math.min(implicitHeight, maxMenuHeight)
closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape
background: Rectangle {
border.color: Style.menuBorder
radius: Style.currentAccountButtonRadius
}
onClosed: {
// HACK: reload account Instantiator immediately by restting it - could be done better I guess
// see also onVisibleChanged above
userLineInstantiator.active = false;
userLineInstantiator.active = true;
}
Instantiator {
id: userLineInstantiator
model: UserModel
delegate: UserLine {
onShowUserStatusSelectorDialog: {
userStatusSelectorDialogLoader.source = "qrc:/qml/src/gui/UserStatusSelectorDialog.qml"
userStatusSelectorDialogLoader.item.title = qsTr("Set user status")
userStatusSelectorDialogLoader.item.model.load(index)
userStatusSelectorDialogLoader.item.show()
}
Instantiator {
id: userLineInstantiator
model: UserModel
delegate: UserLine {
onShowUserStatusSelectorDialog: {
userStatusSelectorDialogLoader.source = "qrc:/qml/src/gui/UserStatusSelectorDialog.qml"
userStatusSelectorDialogLoader.item.title = qsTr("Set user status")
userStatusSelectorDialogLoader.item.model.load(index)
userStatusSelectorDialogLoader.item.show()
}
onObjectAdded: accountMenu.insertItem(index, object)
onObjectRemoved: accountMenu.removeItem(object)
}
onObjectAdded: accountMenu.insertItem(index, object)
onObjectRemoved: accountMenu.removeItem(object)
}
MenuItem {
id: addAccountButton
height: Style.addAccountButtonHeight
hoverEnabled: true
MenuItem {
id: addAccountButton
height: Style.addAccountButtonHeight
hoverEnabled: true
background: Item {
height: parent.height
width: parent.menu.width
Rectangle {
anchors.fill: parent
anchors.margins: 1
color: parent.parent.hovered ? Style.lightHover : "transparent"
}
}
RowLayout {
background: Item {
height: parent.height
width: parent.menu.width
Rectangle {
anchors.fill: parent
spacing: 0
Image {
Layout.leftMargin: 12
verticalAlignment: Qt.AlignCenter
source: "qrc:///client/theme/black/add.svg"
sourceSize.width: Style.headerButtonIconSize
sourceSize.height: Style.headerButtonIconSize
}
Label {
Layout.leftMargin: 14
text: qsTr("Add account")
color: "black"
font.pixelSize: Style.topLinePixelSize
}
// Filler on the right
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
}
onClicked: UserModel.addAccount()
Accessible.role: Accessible.MenuItem
Accessible.name: qsTr("Add new account")
Accessible.onPressAction: addAccountButton.clicked()
}
MenuSeparator {
contentItem: Rectangle {
implicitHeight: 1
color: Style.menuBorder
anchors.margins: 1
color: parent.parent.hovered || parent.parent.visualFocus ? Style.lightHover : "transparent"
}
}
MenuItem {
id: syncPauseButton
font.pixelSize: Style.topLinePixelSize
hoverEnabled: true
onClicked: Systray.pauseResumeSync()
RowLayout {
anchors.fill: parent
spacing: 0
background: Item {
height: parent.height
width: parent.menu.width
Rectangle {
anchors.fill: parent
anchors.margins: 1
color: parent.parent.hovered ? Style.lightHover : "transparent"
}
Image {
Layout.leftMargin: 12
verticalAlignment: Qt.AlignCenter
source: "qrc:///client/theme/black/add.svg"
sourceSize.width: Style.headerButtonIconSize
sourceSize.height: Style.headerButtonIconSize
}
Label {
Layout.leftMargin: 14
text: qsTr("Add account")
color: "black"
font.pixelSize: Style.topLinePixelSize
}
// Filler on the right
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
}
onClicked: UserModel.addAccount()
Accessible.role: Accessible.MenuItem
Accessible.name: Systray.syncIsPaused() ? qsTr("Resume sync for all") : qsTr("Pause sync for all")
Accessible.onPressAction: syncPauseButton.clicked()
Accessible.role: Accessible.MenuItem
Accessible.name: qsTr("Add new account")
Accessible.onPressAction: addAccountButton.clicked()
}
MenuSeparator {
contentItem: Rectangle {
implicitHeight: 1
color: Style.menuBorder
}
}
MenuItem {
id: syncPauseButton
font.pixelSize: Style.topLinePixelSize
hoverEnabled: true
onClicked: Systray.pauseResumeSync()
background: Item {
height: parent.height
width: parent.menu.width
Rectangle {
anchors.fill: parent
anchors.margins: 1
color: parent.parent.hovered || parent.parent.visualFocus ? Style.lightHover : "transparent"
}
}
MenuItem {
id: settingsButton
text: qsTr("Settings")
font.pixelSize: Style.topLinePixelSize
hoverEnabled: true
onClicked: Systray.openSettings()
Accessible.role: Accessible.MenuItem
Accessible.name: Systray.syncIsPaused() ? qsTr("Resume sync for all") : qsTr("Pause sync for all")
Accessible.onPressAction: syncPauseButton.clicked()
}
background: Item {
height: parent.height
width: parent.menu.width
Rectangle {
anchors.fill: parent
anchors.margins: 1
color: parent.parent.hovered ? Style.lightHover : "transparent"
}
MenuItem {
id: settingsButton
text: qsTr("Settings")
font.pixelSize: Style.topLinePixelSize
hoverEnabled: true
onClicked: Systray.openSettings()
background: Item {
height: parent.height
width: parent.menu.width
Rectangle {
anchors.fill: parent
anchors.margins: 1
color: parent.parent.hovered || parent.parent.visualFocus ? Style.lightHover : "transparent"
}
Accessible.role: Accessible.MenuItem
Accessible.name: text
Accessible.onPressAction: settingsButton.clicked()
}
MenuItem {
id: exitButton
text: qsTr("Exit");
font.pixelSize: Style.topLinePixelSize
hoverEnabled: true
onClicked: Systray.shutdown()
Accessible.role: Accessible.MenuItem
Accessible.name: text
Accessible.onPressAction: settingsButton.clicked()
}
background: Item {
height: parent.height
width: parent.menu.width
Rectangle {
anchors.fill: parent
anchors.margins: 1
color: parent.parent.hovered ? Style.lightHover : "transparent"
}
MenuItem {
id: exitButton
text: qsTr("Exit");
font.pixelSize: Style.topLinePixelSize
hoverEnabled: true
onClicked: Systray.shutdown()
background: Item {
height: parent.height
width: parent.menu.width
Rectangle {
anchors.fill: parent
anchors.margins: 1
color: parent.parent.hovered || parent.parent.visualFocus ? Style.lightHover : "transparent"
}
Accessible.role: Accessible.MenuItem
Accessible.name: text
Accessible.onPressAction: exitButton.clicked()
}
Accessible.role: Accessible.MenuItem
Accessible.name: text
Accessible.onPressAction: exitButton.clicked()
}
}
background: Rectangle {
color: accountBtnMouseArea.containsMouse ? "white" : "transparent"
color: parent.hovered || parent.visualFocus ? "white" : "transparent"
opacity: 0.2
}
@@ -658,51 +651,61 @@ Window {
iconColor: "#afafaf"
}
ListView {
id: unifiedSearchResultsListView
ScrollView {
id: controlRoot
padding: 1
contentWidth: availableWidth
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
data: WheelHandler {
target: controlRoot.contentItem
}
visible: unifiedSearchResultsListView.count > 0
anchors.top: trayWindowUnifiedSearchInputContainer.bottom
anchors.left: trayWindowBackground.left
anchors.right: trayWindowBackground.right
anchors.bottom: trayWindowBackground.bottom
spacing: 4
visible: count > 0
clip: true
ScrollBar.vertical: ScrollBar {
id: unifiedSearchResultsListViewScrollbar
}
keyNavigationEnabled: true
ListView {
id: unifiedSearchResultsListView
spacing: 4
clip: true
reuseItems: true
keyNavigationEnabled: true
Accessible.role: Accessible.List
Accessible.name: qsTr("Unified search results list")
reuseItems: true
model: UserModel.currentUser.unifiedSearchResultsListModel
Accessible.role: Accessible.List
Accessible.name: qsTr("Unified search results list")
delegate: UnifiedSearchResultListItem {
width: unifiedSearchResultsListView.width
height: trayWindowBackground.Style.unifiedSearchItemHeight
isSearchInProgress: unifiedSearchResultsListView.model.isSearchInProgress
textLeftMargin: trayWindowBackground.Style.unifiedSearchResultTextLeftMargin
textRightMargin: trayWindowBackground.Style.unifiedSearchResultTextRightMargin
iconWidth: trayWindowBackground.Style.unifiedSearchResulIconWidth
iconLeftMargin: trayWindowBackground.Style.unifiedSearchResulIconLeftMargin
titleFontSize: trayWindowBackground.Style.unifiedSearchResulTitleFontSize
sublineFontSize: trayWindowBackground.Style.unifiedSearchResulSublineFontSize
titleColor: trayWindowBackground.Style.unifiedSearchResulTitleColor
sublineColor: trayWindowBackground.Style.unifiedSearchResulSublineColor
currentFetchMoreInProgressProviderId: unifiedSearchResultsListView.model.currentFetchMoreInProgressProviderId
fetchMoreTriggerClicked: unifiedSearchResultsListView.model.fetchMoreTriggerClicked
resultClicked: unifiedSearchResultsListView.model.resultClicked
ListView.onPooled: isPooled = true
ListView.onReused: isPooled = false
}
model: UserModel.currentUser.unifiedSearchResultsListModel
section.property: "providerName"
section.criteria: ViewSection.FullString
section.delegate: UnifiedSearchResultSectionItem {
width: unifiedSearchResultsListView.width
delegate: UnifiedSearchResultListItem {
width: unifiedSearchResultsListView.width
height: trayWindowBackground.Style.unifiedSearchItemHeight
isSearchInProgress: unifiedSearchResultsListView.model.isSearchInProgress
textLeftMargin: trayWindowBackground.Style.unifiedSearchResultTextLeftMargin
textRightMargin: trayWindowBackground.Style.unifiedSearchResultTextRightMargin
iconWidth: trayWindowBackground.Style.unifiedSearchResulIconWidth
iconLeftMargin: trayWindowBackground.Style.unifiedSearchResulIconLeftMargin
titleFontSize: trayWindowBackground.Style.unifiedSearchResulTitleFontSize
sublineFontSize: trayWindowBackground.Style.unifiedSearchResulSublineFontSize
titleColor: trayWindowBackground.Style.unifiedSearchResulTitleColor
sublineColor: trayWindowBackground.Style.unifiedSearchResulSublineColor
currentFetchMoreInProgressProviderId: unifiedSearchResultsListView.model.currentFetchMoreInProgressProviderId
fetchMoreTriggerClicked: unifiedSearchResultsListView.model.fetchMoreTriggerClicked
resultClicked: unifiedSearchResultsListView.model.resultClicked
ListView.onPooled: isPooled = true
ListView.onReused: isPooled = false
}
section.property: "providerName"
section.criteria: ViewSection.FullString
section.delegate: UnifiedSearchResultSectionItem {
width: unifiedSearchResultsListView.width
}
}
}
@@ -723,6 +726,7 @@ Window {
anchors.right: trayWindowBackground.right
anchors.bottom: trayWindowBackground.bottom
activeFocusOnTab: true
model: activityModel
onShowFileActivity: {
openFileActivityDialog(displayPath, absolutePath)

View File

@@ -61,11 +61,22 @@ public:
SyncFileItemType
};
struct RichSubjectParameter {
QString type; // Required
QString id; // Required
QString name; // Required
QString path; // Required (for files only)
QUrl link; // Optional (files only)
};
Type _type;
qlonglong _id;
QString _fileAction;
QString _objectType;
QString _subject;
QString _subjectRich;
QHash<QString, RichSubjectParameter> _subjectRichParameters;
QString _subjectDisplay;
QString _message;
QString _folder;
QString _file;

View File

@@ -58,6 +58,7 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
roles[DisplayPathRole] = "displayPath";
roles[PathRole] = "path";
roles[AbsolutePathRole] = "absolutePath";
roles[DisplayLocationRole] = "displayLocation";
roles[LinkRole] = "link";
roles[MessageRole] = "message";
roles[ActionRole] = "type";
@@ -68,6 +69,7 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
roles[ObjectTypeRole] = "objectType";
roles[PointInTimeRole] = "dateTime";
roles[DisplayActions] = "displayActions";
roles[ShareableRole] = "isShareable";
return roles;
}
@@ -113,15 +115,40 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
if (!ast && _accountState != ast.data())
return QVariant();
switch (role) {
case DisplayPathRole:
const auto getFilePath = [&]() {
if (!a._file.isEmpty()) {
auto folder = FolderMan::instance()->folder(a._folder);
QString relPath(a._file);
if (folder) {
relPath.prepend(folder->remotePath());
}
const auto folder = FolderMan::instance()->folder(a._folder);
const QString relPath = folder ? folder->remotePath() + a._file : a._file;
const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
if (localFiles.isEmpty()) {
return QString();
}
// If this is an E2EE file or folder, pretend we got no path, hiding the share button which is what we want
if (folder) {
SyncJournalFileRecord rec;
folder->journalDb()->getFileRecord(a._file.mid(1), &rec);
if (rec.isValid() && (rec._isE2eEncrypted || !rec._e2eMangledName.isEmpty())) {
return QString();
}
}
return localFiles.constFirst();
}
return QString();
};
const auto getDisplayPath = [&a, &ast]() {
if (!a._file.isEmpty()) {
const auto folder = FolderMan::instance()->folder(a._folder);
QString relPath = folder ? folder->remotePath() + a._file : a._file;
const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
if (localFiles.count() > 0) {
if (relPath.startsWith('/') || relPath.startsWith('\\')) {
return relPath.remove(0, 1);
@@ -131,54 +158,22 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
}
}
return QString();
};
const auto displayLocation = [&]() {
const auto displayPath = QFileInfo(getDisplayPath()).path();
return displayPath == "." || displayPath == "/" ? QString() : displayPath;
};
switch (role) {
case DisplayPathRole:
return getDisplayPath();
case PathRole:
if (!a._file.isEmpty()) {
const auto folder = FolderMan::instance()->folder(a._folder);
QString relPath(a._file);
if (folder) {
relPath.prepend(folder->remotePath());
}
// get relative path to the file so we can open it in the file manager
const auto localFiles = FolderMan::instance()->findFileInLocalFolders(QFileInfo(relPath).path(), ast->account());
if (localFiles.isEmpty()) {
return QString();
}
// If this is an E2EE file or folder, pretend we got no path, this leads to
// hiding the share button which is what we want
if (folder) {
SyncJournalFileRecord rec;
folder->journalDb()->getFileRecord(a._file.mid(1), &rec);
if (rec.isValid() && (rec._isE2eEncrypted || !rec._e2eMangledName.isEmpty())) {
return QString();
}
}
return QUrl::fromLocalFile(localFiles.constFirst());
}
return QString();
case AbsolutePathRole: {
const auto folder = FolderMan::instance()->folder(a._folder);
QString relPath(a._file);
if (!a._file.isEmpty()) {
if (folder) {
relPath.prepend(folder->remotePath());
}
const auto localFiles = FolderMan::instance()->findFileInLocalFolders(relPath, ast->account());
if (!localFiles.empty()) {
return localFiles.constFirst();
} else {
qWarning("File not local folders while processing absolute path request.");
return QString();
}
} else {
qWarning("Received an absolute path request for an activity without a file path.");
return QString();
}
}
return QUrl::fromLocalFile(QFileInfo(getFilePath()).path());
case AbsolutePathRole:
return getFilePath();
case DisplayLocationRole:
return displayLocation();
case ActionsLinksRole: {
QList<QVariant> customList;
foreach (ActivityLink activityLink, a._links) {
@@ -241,7 +236,11 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
}
}
case ActionTextRole:
return a._subject;
if(a._subjectDisplay.isEmpty()) {
return a._subject;
}
return a._subjectDisplay;
case ActionTextColorRole:
return a._id == -1 ? QLatin1String("#808080") : QLatin1String("#222"); // FIXME: This is a temporary workaround for _showMoreActivitiesAvailableEntry
case MessageRole:
@@ -262,6 +261,8 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
return (ast && ast->isConnected());
case DisplayActions:
return _displayActions;
case ShareableRole:
return !data(index, PathRole).toString().isEmpty() && _displayActions && a._fileAction != "file_deleted" && a._status != SyncFileItem::FileIgnored;
default:
return QVariant();
}
@@ -329,16 +330,53 @@ void ActivityListModel::activitiesReceived(const QJsonDocument &json, int status
Activity a;
a._type = Activity::ActivityType;
a._objectType = json.value("object_type").toString();
a._objectType = json.value(QStringLiteral("object_type")).toString();
a._accName = ast->account()->displayName();
a._id = json.value("activity_id").toInt();
a._fileAction = json.value("type").toString();
a._subject = json.value("subject").toString();
a._message = json.value("message").toString();
a._file = json.value("object_name").toString();
a._link = QUrl(json.value("link").toString());
a._dateTime = QDateTime::fromString(json.value("datetime").toString(), Qt::ISODate);
a._icon = json.value("icon").toString();
a._id = json.value(QStringLiteral("activity_id")).toInt();
a._fileAction = json.value(QStringLiteral("type")).toString();
a._subject = json.value(QStringLiteral("subject")).toString();
a._message = json.value(QStringLiteral("message")).toString();
a._file = json.value(QStringLiteral("object_name")).toString();
a._link = QUrl(json.value(QStringLiteral("link")).toString());
a._dateTime = QDateTime::fromString(json.value(QStringLiteral("datetime")).toString(), Qt::ISODate);
a._icon = json.value(QStringLiteral("icon")).toString();
auto richSubjectData = json.value(QStringLiteral("subject_rich")).toArray();
Q_ASSERT(richSubjectData.size() > 1);
if(richSubjectData.size() > 1) {
a._subjectRich = richSubjectData[0].toString();
auto parameters = richSubjectData[1].toObject();
const QRegularExpression subjectRichParameterRe(QStringLiteral("({[a-zA-Z0-9]*})"));
const QRegularExpression subjectRichParameterBracesRe(QStringLiteral("[{}]"));
for (auto i = parameters.begin(); i != parameters.end(); ++i) {
const auto parameterJsonObject = i.value().toObject();
const Activity::RichSubjectParameter parameter = {
parameterJsonObject.value(QStringLiteral("type")).toString(),
parameterJsonObject.value(QStringLiteral("id")).toString(),
parameterJsonObject.value(QStringLiteral("name")).toString(),
parameterJsonObject.contains(QStringLiteral("path")) ? parameterJsonObject.value(QStringLiteral("path")).toString() : QString(),
parameterJsonObject.contains(QStringLiteral("link")) ? QUrl(parameterJsonObject.value(QStringLiteral("link")).toString()) : QUrl(),
};
a._subjectRichParameters[i.key()] = parameter;
}
auto displayString = a._subjectRich;
auto i = subjectRichParameterRe.globalMatch(displayString);
while (i.hasNext()) {
const auto match = i.next();
auto word = match.captured(1);
word.remove(subjectRichParameterBracesRe);
Q_ASSERT(a._subjectRichParameters.contains(word));
displayString = displayString.replace(match.captured(1), a._subjectRichParameters[word].name);
}
a._subjectDisplay = displayString;
}
list.append(a);
_currentItem = list.last()._id;

View File

@@ -55,11 +55,13 @@ public:
DisplayPathRole,
PathRole,
AbsolutePathRole,
DisplayLocationRole, // Provides the display path to a file's parent folder, relative to Nextcloud root
LinkRole,
PointInTimeRole,
AccountConnectedRole,
SyncFileStatusRole,
DisplayActions,
ShareableRole,
};
Q_ENUM(DataRole)

View File

@@ -17,7 +17,6 @@ const QString notificationsPath = QLatin1String("ocs/v2.php/apps/notifications/a
const char propertyAccountStateC[] = "oc_account_state";
const int successStatusCode = 200;
const int notModifiedStatusCode = 304;
QMap<int, QByteArray> ServerNotificationHandler::iconCache;
ServerNotificationHandler::ServerNotificationHandler(AccountState *accountState, QObject *parent)
: QObject(parent)
@@ -72,11 +71,6 @@ void ServerNotificationHandler::slotAllowDesktopNotificationsChanged(bool isAllo
}
}
void ServerNotificationHandler::slotIconDownloaded(QByteArray iconData)
{
iconCache.insert(sender()->property("activityId").toInt(),iconData);
}
void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &json, int statusCode)
{
if (statusCode != successStatusCode && statusCode != notModifiedStatusCode) {
@@ -112,12 +106,6 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j
a._message = json.value("message").toString();
a._icon = json.value("icon").toString();
if (!a._icon.isEmpty()) {
auto *iconJob = new IconJob(_accountState->account(), QUrl(a._icon));
iconJob->setProperty("activityId", a._id);
connect(iconJob, &IconJob::jobFinished, this, &ServerNotificationHandler::slotIconDownloaded);
}
QUrl link(json.value("link").toString());
if (!link.isEmpty()) {
if (link.host().isEmpty()) {

View File

@@ -14,7 +14,6 @@ class ServerNotificationHandler : public QObject
Q_OBJECT
public:
explicit ServerNotificationHandler(AccountState *accountState, QObject *parent = nullptr);
static QMap<int, QByteArray> iconCache;
signals:
void newNotificationList(ActivityList);
@@ -25,7 +24,6 @@ public slots:
private slots:
void slotNotificationsReceived(const QJsonDocument &json, int statusCode);
void slotEtagResponseHeaderReceived(const QByteArray &value, int statusCode);
void slotIconDownloaded(QByteArray iconData);
void slotAllowDesktopNotificationsChanged(bool isAllowed);
private:

View File

@@ -322,7 +322,8 @@ void UnifiedSearchResultsListModel::resultClicked(const QString &providerId, con
FolderMan::instance()->findFileInLocalFolders(QFileInfo(relativePath).path(), _accountState->account());
if (!localFiles.isEmpty()) {
QDesktopServices::openUrl(localFiles.constFirst());
qCInfo(lcUnifiedSearch) << "Opening file:" << localFiles.constFirst();
QDesktopServices::openUrl(QUrl::fromLocalFile(localFiles.constFirst()));
return;
}
}

View File

@@ -506,6 +506,8 @@ void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr
activity._folder = folder->alias();
activity._fileAction = "";
const auto fileName = QFileInfo(item->_originalFile).fileName();
if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) {
activity._fileAction = "file_deleted";
} else if (item->_instruction == CSYNC_INSTRUCTION_NEW) {
@@ -520,15 +522,15 @@ void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr
qCWarning(lcActivity) << "Item " << item->_file << " retrieved successfully.";
if (item->_direction != SyncFileItem::Up) {
activity._message = tr("Synced %1").arg(item->_originalFile);
activity._message = tr("Synced %1").arg(fileName);
} else if (activity._fileAction == "file_renamed") {
activity._message = tr("You renamed %1").arg(item->_originalFile);
activity._message = tr("You renamed %1").arg(fileName);
} else if (activity._fileAction == "file_deleted") {
activity._message = tr("You deleted %1").arg(item->_originalFile);
activity._message = tr("You deleted %1").arg(fileName);
} else if (activity._fileAction == "file_created") {
activity._message = tr("You created %1").arg(item->_originalFile);
activity._message = tr("You created %1").arg(fileName);
} else {
activity._message = tr("You changed %1").arg(item->_originalFile);
activity._message = tr("You changed %1").arg(fileName);
}
_activityModel->addSyncFileItemToActivityList(activity);
@@ -658,7 +660,7 @@ QString User::statusEmoji() const
bool User::serverHasUserStatus() const
{
return _account->account()->capabilities().userStatusNotification();
return _account->account()->capabilities().userStatus();
}
QImage User::avatar() const

View File

@@ -62,6 +62,7 @@ OwncloudAdvancedSetupPage::OwncloudAdvancedSetupPage(OwncloudWizard *wizard)
if (Theme::instance()->enforceVirtualFilesSyncFolder()) {
_ui.rSyncEverything->setDisabled(true);
_ui.rSelectiveSync->setDisabled(true);
_ui.bSelectiveSync->setDisabled(true);
}
connect(_ui.rSyncEverything, &QAbstractButton::clicked, this, &OwncloudAdvancedSetupPage::slotSyncEverythingClicked);

View File

@@ -40,6 +40,8 @@ set(libsync_SRCS
propagateupload.cpp
propagateuploadv1.cpp
propagateuploadng.cpp
bulkpropagatorjob.cpp
putmultifilejob.cpp
propagateremotedelete.cpp
propagateremotedeleteencrypted.cpp
propagateremotedeleteencryptedrootfolder.cpp

View File

@@ -148,6 +148,17 @@ QNetworkReply *AbstractNetworkJob::sendRequest(const QByteArray &verb, const QUr
return reply;
}
QNetworkReply *AbstractNetworkJob::sendRequest(const QByteArray &verb,
const QUrl &url,
QNetworkRequest req,
QHttpMultiPart *requestBody)
{
auto reply = _account->sendRawRequest(verb, url, req, requestBody);
_requestBody = nullptr;
adoptRequest(reply);
return reply;
}
void AbstractNetworkJob::adoptRequest(QNetworkReply *reply)
{
addTimer(reply);

View File

@@ -77,7 +77,7 @@ public:
bool timedOut() const { return _timedout; }
/** Returns an error message, if any. */
QString errorString() const;
virtual QString errorString() const;
/** Like errorString, but also checking the reply body for information.
*
@@ -138,6 +138,9 @@ protected:
QNetworkRequest req = QNetworkRequest(),
QIODevice *requestBody = nullptr);
QNetworkReply *sendRequest(const QByteArray &verb, const QUrl &url,
QNetworkRequest req, QHttpMultiPart *requestBody);
/** Makes this job drive a pre-made QNetworkReply
*
* This reply cannot have a QIODevice request body because we can't get

View File

@@ -47,6 +47,7 @@
#include <QJsonObject>
#include <QJsonArray>
#include <QLoggingCategory>
#include <QHttpMultiPart>
#include <qsslconfiguration.h>
#include <qt5keychain/keychain.h>
@@ -56,6 +57,7 @@ using namespace QKeychain;
namespace {
constexpr int pushNotificationsReconnectInterval = 1000 * 60 * 2;
constexpr int usernamePrefillServerVersinMinSupportedMajor = 24;
}
namespace OCC {
@@ -360,6 +362,18 @@ QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url,
return _am->sendCustomRequest(req, verb, data);
}
QNetworkReply *Account::sendRawRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QHttpMultiPart *data)
{
req.setUrl(url);
req.setSslConfiguration(this->getOrCreateSslConfig());
if (verb == "PUT") {
return _am->put(req, data);
} else if (verb == "POST") {
return _am->post(req, data);
}
return _am->sendCustomRequest(req, verb, data);
}
SimpleNetworkJob *Account::sendRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data)
{
auto job = new SimpleNetworkJob(sharedFromThis());
@@ -616,6 +630,11 @@ bool Account::serverVersionUnsupported() const
NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR, NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH);
}
bool Account::isUsernamePrefillSupported() const
{
return serverVersionInt() >= makeServerVersion(usernamePrefillServerVersinMinSupportedMajor, 0, 0);
}
void Account::setServerVersion(const QString &version)
{
if (version == _serverVersion) {

View File

@@ -154,6 +154,9 @@ public:
QNetworkReply *sendRawRequest(const QByteArray &verb,
const QUrl &url, QNetworkRequest req, const QByteArray &data);
QNetworkReply *sendRawRequest(const QByteArray &verb,
const QUrl &url, QNetworkRequest req, QHttpMultiPart *data);
/** Create and start network job for a simple one-off request.
*
* More complicated requests typically create their own job types.
@@ -227,6 +230,8 @@ public:
*/
bool serverVersionUnsupported() const;
bool isUsernamePrefillSupported() const;
/** True when the server connection is using HTTP2 */
bool isHttp2Supported() { return _http2Supported; }
void setHttp2Supported(bool value) { _http2Supported = value; }

View File

@@ -0,0 +1,719 @@
/*
* Copyright 2021 (c) Matthieu Gallien <matthieu.gallien@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "bulkpropagatorjob.h"
#include "putmultifilejob.h"
#include "owncloudpropagator_p.h"
#include "syncfileitem.h"
#include "syncengine.h"
#include "propagateupload.h"
#include "propagatorjobs.h"
#include "filesystem.h"
#include "account.h"
#include "common/utility.h"
#include "common/checksums.h"
#include "networkjobs.h"
#include <QFileInfo>
#include <QDir>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
namespace OCC {
Q_LOGGING_CATEGORY(lcBulkPropagatorJob, "nextcloud.sync.propagator.bulkupload", QtInfoMsg)
}
namespace {
QByteArray getEtagFromJsonReply(const QJsonObject &reply)
{
const auto ocEtag = OCC::parseEtag(reply.value("OC-ETag").toString().toLatin1());
const auto ETag = OCC::parseEtag(reply.value("ETag").toString().toLatin1());
const auto etag = OCC::parseEtag(reply.value("etag").toString().toLatin1());
QByteArray ret = ocEtag;
if (ret.isEmpty()) {
ret = ETag;
}
if (ret.isEmpty()) {
ret = etag;
}
if (ocEtag.length() > 0 && ocEtag != etag && ocEtag != ETag) {
qCDebug(OCC::lcBulkPropagatorJob) << "Quite peculiar, we have an etag != OC-Etag [no problem!]" << etag << ETag << ocEtag;
}
return ret;
}
QByteArray getHeaderFromJsonReply(const QJsonObject &reply, const QByteArray &headerName)
{
return reply.value(headerName).toString().toLatin1();
}
constexpr auto batchSize = 100;
constexpr auto parallelJobsMaximumCount = 1;
}
namespace OCC {
BulkPropagatorJob::BulkPropagatorJob(OwncloudPropagator *propagator,
const std::deque<SyncFileItemPtr> &items)
: PropagatorJob(propagator)
, _items(items)
{
_filesToUpload.reserve(batchSize);
_pendingChecksumFiles.reserve(batchSize);
}
bool BulkPropagatorJob::scheduleSelfOrChild()
{
if (_items.empty()) {
return false;
}
if (!_pendingChecksumFiles.empty()) {
return false;
}
_state = Running;
for(int i = 0; i < batchSize && !_items.empty(); ++i) {
auto currentItem = _items.front();
_items.pop_front();
_pendingChecksumFiles.insert(currentItem->_file);
QMetaObject::invokeMethod(this, [this, currentItem] () {
UploadFileInfo fileToUpload;
fileToUpload._file = currentItem->_file;
fileToUpload._size = currentItem->_size;
fileToUpload._path = propagator()->fullLocalPath(fileToUpload._file);
startUploadFile(currentItem, fileToUpload);
}); // We could be in a different thread (neon jobs)
}
return _items.empty() && _filesToUpload.empty();
}
PropagatorJob::JobParallelism BulkPropagatorJob::parallelism()
{
return PropagatorJob::JobParallelism::FullParallelism;
}
void BulkPropagatorJob::startUploadFile(SyncFileItemPtr item, UploadFileInfo fileToUpload)
{
if (propagator()->_abortRequested) {
return;
}
// Check if the specific file can be accessed
if (propagator()->hasCaseClashAccessibilityProblem(fileToUpload._file)) {
done(item, SyncFileItem::NormalError, tr("File %1 cannot be uploaded because another file with the same name, differing only in case, exists").arg(QDir::toNativeSeparators(item->_file)));
return;
}
return slotComputeTransmissionChecksum(item, fileToUpload);
}
void BulkPropagatorJob::doStartUpload(SyncFileItemPtr item,
UploadFileInfo fileToUpload,
QByteArray transmissionChecksumHeader)
{
if (propagator()->_abortRequested) {
return;
}
// write the checksum in the database, so if the POST is sent
// to the server, but the connection drops before we get the etag, we can check the checksum
// in reconcile (issue #5106)
SyncJournalDb::UploadInfo pi;
pi._valid = true;
pi._chunk = 0;
pi._transferid = 0; // We set a null transfer id because it is not chunked.
pi._modtime = item->_modtime;
pi._errorCount = 0;
pi._contentChecksum = item->_checksumHeader;
pi._size = item->_size;
propagator()->_journal->setUploadInfo(item->_file, pi);
propagator()->_journal->commit("Upload info");
auto currentHeaders = headers(item);
currentHeaders[QByteArrayLiteral("Content-Length")] = QByteArray::number(fileToUpload._size);
if (!item->_renameTarget.isEmpty() && item->_file != item->_renameTarget) {
// Try to rename the file
const auto originalFilePathAbsolute = propagator()->fullLocalPath(item->_file);
const auto newFilePathAbsolute = propagator()->fullLocalPath(item->_renameTarget);
const auto renameSuccess = QFile::rename(originalFilePathAbsolute, newFilePathAbsolute);
if (!renameSuccess) {
done(item, SyncFileItem::NormalError, "File contains trailing spaces and couldn't be renamed");
return;
}
qCWarning(lcBulkPropagatorJob()) << item->_file << item->_renameTarget;
fileToUpload._file = item->_file = item->_renameTarget;
fileToUpload._path = propagator()->fullLocalPath(fileToUpload._file);
item->_modtime = FileSystem::getModTime(newFilePathAbsolute);
if (item->_modtime <= 0) {
_pendingChecksumFiles.remove(item->_file);
slotOnErrorStartFolderUnlock(item, SyncFileItem::NormalError, tr("File %1 has invalid modified time. Do not upload to the server.").arg(QDir::toNativeSeparators(item->_file)));
checkPropagationIsDone();
return;
}
}
const auto remotePath = propagator()->fullRemotePath(fileToUpload._file);
currentHeaders["X-File-MD5"] = transmissionChecksumHeader;
BulkUploadItem newUploadFile{propagator()->account(), item, fileToUpload,
remotePath, fileToUpload._path,
fileToUpload._size, currentHeaders};
qCInfo(lcBulkPropagatorJob) << remotePath << "transmission checksum" << transmissionChecksumHeader << fileToUpload._path;
_filesToUpload.push_back(std::move(newUploadFile));
_pendingChecksumFiles.remove(item->_file);
if (_pendingChecksumFiles.empty()) {
triggerUpload();
}
}
void BulkPropagatorJob::triggerUpload()
{
auto uploadParametersData = std::vector<SingleUploadFileData>{};
uploadParametersData.reserve(_filesToUpload.size());
int timeout = 0;
for(auto &singleFile : _filesToUpload) {
// job takes ownership of device via a QScopedPointer. Job deletes itself when finishing
auto device = std::make_unique<UploadDevice>(
singleFile._localPath, 0, singleFile._fileSize, &propagator()->_bandwidthManager);
if (!device->open(QIODevice::ReadOnly)) {
qCWarning(lcBulkPropagatorJob) << "Could not prepare upload device: " << device->errorString();
// If the file is currently locked, we want to retry the sync
// when it becomes available again.
if (FileSystem::isFileLocked(singleFile._localPath)) {
emit propagator()->seenLockedFile(singleFile._localPath);
}
abortWithError(singleFile._item, SyncFileItem::NormalError, device->errorString());
emit finished(SyncFileItem::NormalError);
return;
}
singleFile._headers["X-File-Path"] = singleFile._remotePath.toUtf8();
uploadParametersData.push_back({std::move(device), singleFile._headers});
timeout += singleFile._fileSize;
}
const auto bulkUploadUrl = Utility::concatUrlPath(propagator()->account()->url(), QStringLiteral("/remote.php/dav/bulk"));
auto job = std::make_unique<PutMultiFileJob>(propagator()->account(), bulkUploadUrl, std::move(uploadParametersData), this);
connect(job.get(), &PutMultiFileJob::finishedSignal, this, &BulkPropagatorJob::slotPutFinished);
for(auto &singleFile : _filesToUpload) {
connect(job.get(), &PutMultiFileJob::uploadProgress,
this, [this, singleFile] (qint64 sent, qint64 total) {
slotUploadProgress(singleFile._item, sent, total);
});
}
adjustLastJobTimeout(job.get(), timeout);
_jobs.append(job.get());
job.release()->start();
if (parallelism() == PropagatorJob::JobParallelism::FullParallelism && _jobs.size() < parallelJobsMaximumCount) {
scheduleSelfOrChild();
}
}
void BulkPropagatorJob::checkPropagationIsDone()
{
if (_items.empty()) {
if (!_jobs.empty() || !_pendingChecksumFiles.empty()) {
// just wait for the other job to finish.
return;
}
qCInfo(lcBulkPropagatorJob) << "final status" << _finalStatus;
emit finished(_finalStatus);
propagator()->scheduleNextJob();
} else {
scheduleSelfOrChild();
}
}
void BulkPropagatorJob::slotComputeTransmissionChecksum(SyncFileItemPtr item,
UploadFileInfo fileToUpload)
{
// Reuse the content checksum as the transmission checksum if possible
const auto supportedTransmissionChecksums =
propagator()->account()->capabilities().supportedChecksumTypes();
// Compute the transmission checksum.
auto computeChecksum = std::make_unique<ComputeChecksum>(this);
if (uploadChecksumEnabled()) {
computeChecksum->setChecksumType("MD5" /*propagator()->account()->capabilities().uploadChecksumType()*/);
} else {
computeChecksum->setChecksumType(QByteArray());
}
connect(computeChecksum.get(), &ComputeChecksum::done,
this, [this, item, fileToUpload] (const QByteArray &contentChecksumType, const QByteArray &contentChecksum) {
slotStartUpload(item, fileToUpload, contentChecksumType, contentChecksum);
});
connect(computeChecksum.get(), &ComputeChecksum::done,
computeChecksum.get(), &QObject::deleteLater);
computeChecksum.release()->start(fileToUpload._path);
}
void BulkPropagatorJob::slotStartUpload(SyncFileItemPtr item,
UploadFileInfo fileToUpload,
const QByteArray &transmissionChecksumType,
const QByteArray &transmissionChecksum)
{
const auto transmissionChecksumHeader = makeChecksumHeader(transmissionChecksumType, transmissionChecksum);
item->_checksumHeader = transmissionChecksumHeader;
const QString fullFilePath = fileToUpload._path;
const QString originalFilePath = propagator()->fullLocalPath(item->_file);
if (!FileSystem::fileExists(fullFilePath)) {
return slotOnErrorStartFolderUnlock(item, SyncFileItem::SoftError, tr("File Removed (start upload) %1").arg(fullFilePath));
}
const time_t prevModtime = item->_modtime; // the _item value was set in PropagateUploadFile::start()
// but a potential checksum calculation could have taken some time during which the file could
// have been changed again, so better check again here.
item->_modtime = FileSystem::getModTime(originalFilePath);
if (item->_modtime <= 0) {
_pendingChecksumFiles.remove(item->_file);
slotOnErrorStartFolderUnlock(item, SyncFileItem::NormalError, tr("File %1 has invalid modified time. Do not upload to the server.").arg(QDir::toNativeSeparators(item->_file)));
checkPropagationIsDone();
return;
}
if (prevModtime != item->_modtime) {
propagator()->_anotherSyncNeeded = true;
_pendingChecksumFiles.remove(item->_file);
qDebug() << "trigger another sync after checking modified time of item" << item->_file << "prevModtime" << prevModtime << "Curr" << item->_modtime;
slotOnErrorStartFolderUnlock(item, SyncFileItem::SoftError, tr("Local file changed during syncing. It will be resumed."));
checkPropagationIsDone();
return;
}
fileToUpload._size = FileSystem::getSize(fullFilePath);
item->_size = FileSystem::getSize(originalFilePath);
// But skip the file if the mtime is too close to 'now'!
// That usually indicates a file that is still being changed
// or not yet fully copied to the destination.
if (fileIsStillChanging(*item)) {
propagator()->_anotherSyncNeeded = true;
_pendingChecksumFiles.remove(item->_file);
slotOnErrorStartFolderUnlock(item, SyncFileItem::SoftError, tr("Local file changed during sync."));
checkPropagationIsDone();
return;
}
doStartUpload(item, fileToUpload, transmissionChecksum);
}
void BulkPropagatorJob::slotOnErrorStartFolderUnlock(SyncFileItemPtr item,
SyncFileItem::Status status,
const QString &errorString)
{
qCInfo(lcBulkPropagatorJob()) << status << errorString;
done(item, status, errorString);
}
void BulkPropagatorJob::slotPutFinishedOneFile(const BulkUploadItem &singleFile,
PutMultiFileJob *job,
const QJsonObject &fileReply)
{
bool finished = false;
qCInfo(lcBulkPropagatorJob()) << singleFile._item->_file << "file headers" << fileReply;
if (fileReply.contains("error") && !fileReply[QStringLiteral("error")].toBool()) {
singleFile._item->_httpErrorCode = static_cast<quint16>(200);
} else {
singleFile._item->_httpErrorCode = static_cast<quint16>(412);
}
singleFile._item->_responseTimeStamp = job->responseTimestamp();
singleFile._item->_requestId = job->requestId();
if (singleFile._item->_httpErrorCode != 200) {
commonErrorHandling(singleFile._item, fileReply[QStringLiteral("message")].toString());
return;
}
singleFile._item->_status = SyncFileItem::Success;
// Check the file again post upload.
// Two cases must be considered separately: If the upload is finished,
// the file is on the server and has a changed ETag. In that case,
// the etag has to be properly updated in the client journal, and because
// of that we can bail out here with an error. But we can reschedule a
// sync ASAP.
// But if the upload is ongoing, because not all chunks were uploaded
// yet, the upload can be stopped and an error can be displayed, because
// the server hasn't registered the new file yet.
const auto etag = getEtagFromJsonReply(fileReply);
finished = etag.length() > 0;
const auto fullFilePath(propagator()->fullLocalPath(singleFile._item->_file));
// Check if the file still exists
if (!checkFileStillExists(singleFile._item, finished, fullFilePath)) {
return;
}
// Check whether the file changed since discovery. the file check here is the original and not the temporary.
if (!checkFileChanged(singleFile._item, finished, fullFilePath)) {
return;
}
// the file id should only be empty for new files up- or downloaded
computeFileId(singleFile._item, fileReply);
singleFile._item->_etag = etag;
if (getHeaderFromJsonReply(fileReply, "X-OC-MTime") != "accepted") {
// X-OC-MTime is supported since owncloud 5.0. But not when chunking.
// Normally Owncloud 6 always puts X-OC-MTime
qCWarning(lcBulkPropagatorJob) << "Server does not support X-OC-MTime" << getHeaderFromJsonReply(fileReply, "X-OC-MTime");
// Well, the mtime was not set
}
}
void BulkPropagatorJob::slotPutFinished()
{
auto *job = qobject_cast<PutMultiFileJob *>(sender());
Q_ASSERT(job);
slotJobDestroyed(job); // remove it from the _jobs list
const auto replyData = job->reply()->readAll();
const auto replyJson = QJsonDocument::fromJson(replyData);
const auto fullReplyObject = replyJson.object();
for (const auto &singleFile : _filesToUpload) {
if (!fullReplyObject.contains(singleFile._remotePath)) {
continue;
}
const auto singleReplyObject = fullReplyObject[singleFile._remotePath].toObject();
slotPutFinishedOneFile(singleFile, job, singleReplyObject);
}
finalize(fullReplyObject);
}
void BulkPropagatorJob::slotUploadProgress(SyncFileItemPtr item, qint64 sent, qint64 total)
{
// Completion is signaled with sent=0, total=0; avoid accidentally
// resetting progress due to the sent being zero by ignoring it.
// finishedSignal() is bound to be emitted soon anyway.
// See https://bugreports.qt.io/browse/QTBUG-44782.
if (sent == 0 && total == 0) {
return;
}
propagator()->reportProgress(*item, sent - total);
}
void BulkPropagatorJob::slotJobDestroyed(QObject *job)
{
_jobs.erase(std::remove(_jobs.begin(), _jobs.end(), job), _jobs.end());
}
void BulkPropagatorJob::adjustLastJobTimeout(AbstractNetworkJob *job, qint64 fileSize) const
{
constexpr double threeMinutes = 3.0 * 60 * 1000;
job->setTimeout(qBound(
job->timeoutMsec(),
// Calculate 3 minutes for each gigabyte of data
qRound64(threeMinutes * static_cast<double>(fileSize) / 1e9),
// Maximum of 30 minutes
static_cast<qint64>(30 * 60 * 1000)));
}
void BulkPropagatorJob::finalizeOneFile(const BulkUploadItem &oneFile)
{
// Update the database entry
const auto result = propagator()->updateMetadata(*oneFile._item);
if (!result) {
done(oneFile._item, SyncFileItem::FatalError, tr("Error updating metadata: %1").arg(result.error()));
return;
} else if (*result == Vfs::ConvertToPlaceholderResult::Locked) {
done(oneFile._item, SyncFileItem::SoftError, tr("The file %1 is currently in use").arg(oneFile._item->_file));
return;
}
// Files that were new on the remote shouldn't have online-only pin state
// even if their parent folder is online-only.
if (oneFile._item->_instruction == CSYNC_INSTRUCTION_NEW
|| oneFile._item->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE) {
auto &vfs = propagator()->syncOptions()._vfs;
const auto pin = vfs->pinState(oneFile._item->_file);
if (pin && *pin == PinState::OnlineOnly && !vfs->setPinState(oneFile._item->_file, PinState::Unspecified)) {
qCWarning(lcBulkPropagatorJob) << "Could not set pin state of" << oneFile._item->_file << "to unspecified";
}
}
// Remove from the progress database:
propagator()->_journal->setUploadInfo(oneFile._item->_file, SyncJournalDb::UploadInfo());
propagator()->_journal->commit("upload file start");
}
void BulkPropagatorJob::finalize(const QJsonObject &fullReply)
{
for(auto singleFileIt = std::begin(_filesToUpload); singleFileIt != std::end(_filesToUpload); ) {
const auto &singleFile = *singleFileIt;
if (!fullReply.contains(singleFile._remotePath)) {
++singleFileIt;
continue;
}
if (!singleFile._item->hasErrorStatus()) {
finalizeOneFile(singleFile);
}
done(singleFile._item, singleFile._item->_status, {});
singleFileIt = _filesToUpload.erase(singleFileIt);
}
checkPropagationIsDone();
}
void BulkPropagatorJob::done(SyncFileItemPtr item,
SyncFileItem::Status status,
const QString &errorString)
{
item->_status = status;
item->_errorString = errorString;
qCInfo(lcBulkPropagatorJob) << "Item completed" << item->destination() << item->_status << item->_instruction << item->_errorString;
handleFileRestoration(item, errorString);
if (propagator()->_abortRequested && (item->_status == SyncFileItem::NormalError
|| item->_status == SyncFileItem::FatalError)) {
// an abort request is ongoing. Change the status to Soft-Error
item->_status = SyncFileItem::SoftError;
}
if (item->_status != SyncFileItem::Success) {
// Blacklist handling
handleBulkUploadBlackList(item);
propagator()->_anotherSyncNeeded = true;
}
handleJobDoneErrors(item, status);
emit propagator()->itemCompleted(item);
}
QMap<QByteArray, QByteArray> BulkPropagatorJob::headers(SyncFileItemPtr item) const
{
QMap<QByteArray, QByteArray> headers;
headers[QByteArrayLiteral("Content-Type")] = QByteArrayLiteral("application/octet-stream");
headers[QByteArrayLiteral("X-File-Mtime")] = QByteArray::number(qint64(item->_modtime));
if (qEnvironmentVariableIntValue("OWNCLOUD_LAZYOPS")) {
headers[QByteArrayLiteral("OC-LazyOps")] = QByteArrayLiteral("true");
}
if (item->_file.contains(QLatin1String(".sys.admin#recall#"))) {
// This is a file recall triggered by the admin. Note: the
// recall list file created by the admin and downloaded by the
// client (.sys.admin#recall#) also falls into this category
// (albeit users are not supposed to mess up with it)
// We use a special tag header so that the server may decide to store this file away in some admin stage area
// And not directly in the user's area (which would trigger redownloads etc).
headers["OC-Tag"] = ".sys.admin#recall#";
}
if (!item->_etag.isEmpty() && item->_etag != "empty_etag"
&& item->_instruction != CSYNC_INSTRUCTION_NEW // On new files never send a If-Match
&& item->_instruction != CSYNC_INSTRUCTION_TYPE_CHANGE) {
// We add quotes because the owncloud server always adds quotes around the etag, and
// csync_owncloud.c's owncloud_file_id always strips the quotes.
headers[QByteArrayLiteral("If-Match")] = '"' + item->_etag + '"';
}
// Set up a conflict file header pointing to the original file
auto conflictRecord = propagator()->_journal->conflictRecord(item->_file.toUtf8());
if (conflictRecord.isValid()) {
headers[QByteArrayLiteral("OC-Conflict")] = "1";
if (!conflictRecord.initialBasePath.isEmpty()) {
headers[QByteArrayLiteral("OC-ConflictInitialBasePath")] = conflictRecord.initialBasePath;
}
if (!conflictRecord.baseFileId.isEmpty()) {
headers[QByteArrayLiteral("OC-ConflictBaseFileId")] = conflictRecord.baseFileId;
}
if (conflictRecord.baseModtime != -1) {
headers[QByteArrayLiteral("OC-ConflictBaseMtime")] = QByteArray::number(conflictRecord.baseModtime);
}
if (!conflictRecord.baseEtag.isEmpty()) {
headers[QByteArrayLiteral("OC-ConflictBaseEtag")] = conflictRecord.baseEtag;
}
}
return headers;
}
void BulkPropagatorJob::abortWithError(SyncFileItemPtr item,
SyncFileItem::Status status,
const QString &error)
{
abort(AbortType::Synchronous);
done(item, status, error);
}
void BulkPropagatorJob::checkResettingErrors(SyncFileItemPtr item) const
{
if (item->_httpErrorCode == 412
|| propagator()->account()->capabilities().httpErrorCodesThatResetFailingChunkedUploads().contains(item->_httpErrorCode)) {
auto uploadInfo = propagator()->_journal->getUploadInfo(item->_file);
uploadInfo._errorCount += 1;
if (uploadInfo._errorCount > 3) {
qCInfo(lcBulkPropagatorJob) << "Reset transfer of" << item->_file
<< "due to repeated error" << item->_httpErrorCode;
uploadInfo = SyncJournalDb::UploadInfo();
} else {
qCInfo(lcBulkPropagatorJob) << "Error count for maybe-reset error" << item->_httpErrorCode
<< "on file" << item->_file
<< "is" << uploadInfo._errorCount;
}
propagator()->_journal->setUploadInfo(item->_file, uploadInfo);
propagator()->_journal->commit("Upload info");
}
}
void BulkPropagatorJob::commonErrorHandling(SyncFileItemPtr item,
const QString &errorMessage)
{
// Ensure errors that should eventually reset the chunked upload are tracked.
checkResettingErrors(item);
abortWithError(item, SyncFileItem::NormalError, errorMessage);
}
bool BulkPropagatorJob::checkFileStillExists(SyncFileItemPtr item,
const bool finished,
const QString &fullFilePath)
{
if (!FileSystem::fileExists(fullFilePath)) {
if (!finished) {
abortWithError(item, SyncFileItem::SoftError, tr("The local file was removed during sync."));
return false;
} else {
propagator()->_anotherSyncNeeded = true;
}
}
return true;
}
bool BulkPropagatorJob::checkFileChanged(SyncFileItemPtr item,
const bool finished,
const QString &fullFilePath)
{
if (!FileSystem::verifyFileUnchanged(fullFilePath, item->_size, item->_modtime)) {
propagator()->_anotherSyncNeeded = true;
if (!finished) {
abortWithError(item, SyncFileItem::SoftError, tr("Local file changed during sync."));
// FIXME: the legacy code was retrying for a few seconds.
// and also checking that after the last chunk, and removed the file in case of INSTRUCTION_NEW
return false;
}
}
return true;
}
void BulkPropagatorJob::computeFileId(SyncFileItemPtr item,
const QJsonObject &fileReply) const
{
const auto fid = getHeaderFromJsonReply(fileReply, "OC-FileID");
if (!fid.isEmpty()) {
if (!item->_fileId.isEmpty() && item->_fileId != fid) {
qCWarning(lcBulkPropagatorJob) << "File ID changed!" << item->_fileId << fid;
}
item->_fileId = fid;
}
}
void BulkPropagatorJob::handleFileRestoration(SyncFileItemPtr item,
const QString &errorString) const
{
if (item->_isRestoration) {
if (item->_status == SyncFileItem::Success
|| item->_status == SyncFileItem::Conflict) {
item->_status = SyncFileItem::Restoration;
} else {
item->_errorString += tr("; Restoration Failed: %1").arg(errorString);
}
} else {
if (item->_errorString.isEmpty()) {
item->_errorString = errorString;
}
}
}
void BulkPropagatorJob::handleBulkUploadBlackList(SyncFileItemPtr item) const
{
propagator()->addToBulkUploadBlackList(item->_file);
}
void BulkPropagatorJob::handleJobDoneErrors(SyncFileItemPtr item,
SyncFileItem::Status status)
{
if (item->hasErrorStatus()) {
qCWarning(lcPropagator) << "Could not complete propagation of" << item->destination() << "by" << this << "with status" << item->_status << "and error:" << item->_errorString;
} else {
qCInfo(lcPropagator) << "Completed propagation of" << item->destination() << "by" << this << "with status" << item->_status;
}
if (item->_status == SyncFileItem::FatalError) {
// Abort all remaining jobs.
propagator()->abort();
}
switch (item->_status)
{
case SyncFileItem::BlacklistedError:
case SyncFileItem::Conflict:
case SyncFileItem::FatalError:
case SyncFileItem::FileIgnored:
case SyncFileItem::FileLocked:
case SyncFileItem::FileNameInvalid:
case SyncFileItem::NoStatus:
case SyncFileItem::NormalError:
case SyncFileItem::Restoration:
case SyncFileItem::SoftError:
_finalStatus = SyncFileItem::NormalError;
qCInfo(lcBulkPropagatorJob) << "modify final status NormalError" << _finalStatus << status;
break;
case SyncFileItem::DetailError:
_finalStatus = SyncFileItem::DetailError;
qCInfo(lcBulkPropagatorJob) << "modify final status DetailError" << _finalStatus << status;
break;
case SyncFileItem::Success:
break;
}
}
}

View File

@@ -0,0 +1,167 @@
/*
* Copyright 2021 (c) Matthieu Gallien <matthieu.gallien@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#pragma once
#include "owncloudpropagator.h"
#include "abstractnetworkjob.h"
#include <QLoggingCategory>
#include <QVector>
#include <QMap>
#include <QByteArray>
#include <deque>
namespace OCC {
Q_DECLARE_LOGGING_CATEGORY(lcBulkPropagatorJob)
class ComputeChecksum;
class PutMultiFileJob;
class BulkPropagatorJob : public PropagatorJob
{
Q_OBJECT
/* This is a minified version of the SyncFileItem,
* that holds only the specifics about the file that's
* being uploaded.
*
* This is needed if we wanna apply changes on the file
* that's being uploaded while keeping the original on disk.
*/
struct UploadFileInfo {
QString _file; /// I'm still unsure if I should use a SyncFilePtr here.
QString _path; /// the full path on disk.
qint64 _size;
};
struct BulkUploadItem
{
AccountPtr _account;
SyncFileItemPtr _item;
UploadFileInfo _fileToUpload;
QString _remotePath;
QString _localPath;
qint64 _fileSize;
QMap<QByteArray, QByteArray> _headers;
};
public:
explicit BulkPropagatorJob(OwncloudPropagator *propagator,
const std::deque<SyncFileItemPtr> &items);
bool scheduleSelfOrChild() override;
JobParallelism parallelism() override;
private slots:
void startUploadFile(SyncFileItemPtr item, UploadFileInfo fileToUpload);
// Content checksum computed, compute the transmission checksum
void slotComputeTransmissionChecksum(SyncFileItemPtr item,
UploadFileInfo fileToUpload);
// transmission checksum computed, prepare the upload
void slotStartUpload(SyncFileItemPtr item,
UploadFileInfo fileToUpload,
const QByteArray &transmissionChecksumType,
const QByteArray &transmissionChecksum);
// invoked on internal error to unlock a folder and faile
void slotOnErrorStartFolderUnlock(SyncFileItemPtr item,
SyncFileItem::Status status,
const QString &errorString);
void slotPutFinished();
void slotUploadProgress(SyncFileItemPtr item, qint64 sent, qint64 total);
void slotJobDestroyed(QObject *job);
private:
void doStartUpload(SyncFileItemPtr item,
UploadFileInfo fileToUpload,
QByteArray transmissionChecksumHeader);
void adjustLastJobTimeout(AbstractNetworkJob *job,
qint64 fileSize) const;
void finalize(const QJsonObject &fullReply);
void finalizeOneFile(const BulkUploadItem &oneFile);
void slotPutFinishedOneFile(const BulkUploadItem &singleFile,
OCC::PutMultiFileJob *job,
const QJsonObject &fullReplyObject);
void done(SyncFileItemPtr item,
SyncFileItem::Status status,
const QString &errorString);
/** Bases headers that need to be sent on the PUT, or in the MOVE for chunking-ng */
QMap<QByteArray, QByteArray> headers(SyncFileItemPtr item) const;
void abortWithError(SyncFileItemPtr item,
SyncFileItem::Status status,
const QString &error);
/**
* Checks whether the current error is one that should reset the whole
* transfer if it happens too often. If so: Bump UploadInfo::errorCount
* and maybe perform the reset.
*/
void checkResettingErrors(SyncFileItemPtr item) const;
/**
* Error handling functionality that is shared between jobs.
*/
void commonErrorHandling(SyncFileItemPtr item,
const QString &errorMessage);
bool checkFileStillExists(SyncFileItemPtr item,
const bool finished,
const QString &fullFilePath);
bool checkFileChanged(SyncFileItemPtr item,
const bool finished,
const QString &fullFilePath);
void computeFileId(SyncFileItemPtr item,
const QJsonObject &fileReply) const;
void handleFileRestoration(SyncFileItemPtr item,
const QString &errorString) const;
void handleBulkUploadBlackList(SyncFileItemPtr item) const;
void handleJobDoneErrors(SyncFileItemPtr item,
SyncFileItem::Status status);
void triggerUpload();
void checkPropagationIsDone();
std::deque<SyncFileItemPtr> _items;
QVector<AbstractNetworkJob *> _jobs; /// network jobs that are currently in transit
QSet<QString> _pendingChecksumFiles;
std::vector<BulkUploadItem> _filesToUpload;
SyncFileItem::Status _finalStatus = SyncFileItem::Status::NoStatus;
};
}

View File

@@ -216,11 +216,9 @@ bool Capabilities::chunkingNg() const
return _capabilities["dav"].toMap()["chunking"].toByteArray() >= "1.0";
}
bool Capabilities::userStatusNotification() const
bool Capabilities::bulkUpload() const
{
return _capabilities.contains("notifications") &&
_capabilities["notifications"].toMap().contains("ocs-endpoints") &&
_capabilities["notifications"].toMap()["ocs-endpoints"].toStringList().contains("user-status");
return _capabilities["dav"].toMap()["bulkupload"].toByteArray() >= "1.0";
}
bool Capabilities::userStatus() const

View File

@@ -63,7 +63,7 @@ public:
bool shareResharing() const;
int shareDefaultPermissions() const;
bool chunkingNg() const;
bool userStatusNotification() const;
bool bulkUpload() const;
bool userStatus() const;
bool userStatusSupportsEmoji() const;

View File

@@ -389,7 +389,13 @@ void ProcessDirectoryJob::processFile(PathTuple path,
item->_originalFile = path._original;
item->_previousSize = dbEntry._fileSize;
item->_previousModtime = dbEntry._modtime;
item->_renameTarget = localEntry.renameName;
if (!localEntry.renameName.isEmpty()) {
if (_dirItem) {
item->_renameTarget = _dirItem->_file + "/" + localEntry.renameName;
} else {
item->_renameTarget = localEntry.renameName;
}
}
if (dbEntry._modtime == localEntry.modtime && dbEntry._type == ItemTypeVirtualFile && localEntry.type == ItemTypeFile) {
item->_type = ItemTypeFile;
@@ -536,6 +542,19 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
} else {
item->_instruction = CSYNC_INSTRUCTION_SYNC;
}
} else if (dbEntry._modtime <= 0 && serverEntry.modtime > 0) {
item->_direction = SyncFileItem::Down;
item->_modtime = serverEntry.modtime;
item->_size = sizeOnServer;
if (serverEntry.isDirectory) {
ENFORCE(dbEntry.isDirectory());
item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
} else if (!localEntry.isValid() && _queryLocal != ParentNotChanged) {
// Deleted locally, changed on server
item->_instruction = CSYNC_INSTRUCTION_NEW;
} else {
item->_instruction = CSYNC_INSTRUCTION_SYNC;
}
} else if (dbEntry._remotePerm != serverEntry.remotePerm || dbEntry._fileId != serverEntry.fileId || metaDataSizeNeedsUpdateForE2EeFilePlaceholder) {
if (metaDataSizeNeedsUpdateForE2EeFilePlaceholder) {
// we are updating placeholder sizes after migrating from older versions with VFS + E2EE implicit hydration not supported
@@ -934,6 +953,14 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
item->_modtime = localEntry.modtime;
item->_type = localEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile;
_childModified = true;
} else if (dbEntry._modtime > 0 && localEntry.modtime <= 0) {
item->_instruction = CSYNC_INSTRUCTION_SYNC;
item->_direction = SyncFileItem::Down;
item->_size = localEntry.size > 0 ? localEntry.size : dbEntry._fileSize;
item->_modtime = dbEntry._modtime;
item->_previousModtime = dbEntry._modtime;
item->_type = localEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile;
_childModified = true;
} else {
// Local file was changed
item->_instruction = CSYNC_INSTRUCTION_SYNC;
@@ -1000,6 +1027,11 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
return;
}
if (localEntry.isDirectory && _discoveryData->_syncOptions._vfs->mode() != Vfs::WindowsCfApi) {
// for VFS folders on Windows only
return;
}
Q_ASSERT(item->_instruction == CSYNC_INSTRUCTION_NEW);
if (item->_instruction != CSYNC_INSTRUCTION_NEW) {
qCWarning(lcDisco) << "Trying to wipe a virtual item" << path._local << " with item->_instruction" << item->_instruction;
@@ -1028,9 +1060,8 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
if (!isFilePlaceHolder && !isOnlineOnlyFolder) {
if (localEntry.isDirectory && folderPlaceHolderAvailability.isValid() && !isOnlineOnlyFolder) {
// a VFS folder but is not online0only (has some files hydrated)
// a VFS folder but is not online-only (has some files hydrated)
qCInfo(lcDisco) << "Virtual directory without db entry for" << path._local << "but it contains hydrated file(s), so let's keep it and reupload.";
emit _discoveryData->addErrorToGui(SyncFileItem::SoftError, tr("Conflict when uploading some files to a folder. Those, conflicted, are going to get cleared!"), path._local);
return;
}
qCWarning(lcDisco) << "Virtual file without db entry for" << path._local
@@ -1043,6 +1074,12 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
if (isOnlineOnlyFolder) {
// if we're wiping a folder, we will only get this function called once and will wipe a folder along with it's files and also display one error in GUI
qCInfo(lcDisco) << "Wiping virtual folder without db entry for" << path._local;
if (isfolderPlaceHolderAvailabilityOnlineOnly && folderPlaceHolderAvailability.isValid()) {
qCInfo(lcDisco) << "*folderPlaceHolderAvailability:" << *folderPlaceHolderAvailability;
}
if (isFolderPinStateOnlineOnly && folderPinState.isValid()) {
qCInfo(lcDisco) << "*folderPinState:" << *folderPinState;
}
emit _discoveryData->addErrorToGui(SyncFileItem::SoftError, tr("Conflict when uploading a folder. It's going to get cleared!"), path._local);
} else {
qCInfo(lcDisco) << "Wiping virtual file without db entry for" << path._local;

View File

@@ -185,7 +185,9 @@ QPair<bool, QByteArray> DiscoveryPhase::findAndCancelDeletedJob(const QString &o
qCWarning(lcDiscovery) << "instruction" << instruction;
qCWarning(lcDiscovery) << "(*it)->_type" << (*it)->_type;
qCWarning(lcDiscovery) << "(*it)->_isRestoration " << (*it)->_isRestoration;
ENFORCE(false);
Q_ASSERT(false);
addErrorToGui(SyncFileItem::Status::FatalError, tr("Error while canceling delete of a file"), originalPath);
emit fatalError(tr("Error while canceling delete of %1").arg(originalPath));
}
(*it)->_instruction = CSYNC_INSTRUCTION_NONE;
result = true;

View File

@@ -98,7 +98,7 @@ bool FileSystem::verifyFileUnchanged(const QString &fileName,
{
const qint64 actualSize = getSize(fileName);
const time_t actualMtime = getModTime(fileName);
if (actualSize != previousSize || actualMtime != previousMtime) {
if ((actualSize != previousSize && actualMtime > 0) || (actualMtime != previousMtime && previousMtime > 0 && actualMtime > 0)) {
qCInfo(lcFileSystem) << "File" << fileName << "has changed:"
<< "size: " << previousSize << "<->" << actualSize
<< ", mtime: " << previousMtime << "<->" << actualMtime;

View File

@@ -21,6 +21,7 @@
#include "propagateremotedelete.h"
#include "propagateremotemove.h"
#include "propagateremotemkdir.h"
#include "bulkpropagatorjob.h"
#include "propagatorjobs.h"
#include "filesystem.h"
#include "common/utility.h"
@@ -173,7 +174,7 @@ static SyncJournalErrorBlacklistRecord createBlacklistEntry(
*
* May adjust the status or item._errorString.
*/
static void blacklistUpdate(SyncJournalDb *journal, SyncFileItem &item)
void blacklistUpdate(SyncJournalDb *journal, SyncFileItem &item)
{
SyncJournalErrorBlacklistRecord oldEntry = journal->errorBlacklistEntry(item._file);
@@ -396,6 +397,8 @@ std::unique_ptr<PropagateUploadFileCommon> OwncloudPropagator::createUploadJob(S
job->setDeleteExisting(deleteExisting);
removeFromBulkUploadBlackList(item->_file);
return job;
}
@@ -769,6 +772,10 @@ bool OwncloudPropagator::createConflict(const SyncFileItemPtr &item,
QString renameError;
auto conflictModTime = FileSystem::getModTime(fn);
if (conflictModTime <= 0) {
*error = tr("Impossible to get modification time for file in conflict %1)").arg(fn);
return false;
}
QString conflictUserName;
if (account()->capabilities().uploadConflictFiles())
conflictUserName = account()->davDisplayName();
@@ -861,7 +868,7 @@ Result<Vfs::ConvertToPlaceholderResult, QString> OwncloudPropagator::staticUpdat
bool OwncloudPropagator::isDelayedUploadItem(const SyncFileItemPtr &item) const
{
return !_scheduleDelayedTasks && !item->_isEncrypted;
return account()->capabilities().bulkUpload() && !_scheduleDelayedTasks && !item->_isEncrypted && _syncOptions._minChunkSize > item->_size && !isInBulkUploadBlackList(item->_file);
}
void OwncloudPropagator::setScheduleDelayedTasks(bool active)
@@ -874,6 +881,23 @@ void OwncloudPropagator::clearDelayedTasks()
_delayedTasks.clear();
}
void OwncloudPropagator::addToBulkUploadBlackList(const QString &file)
{
qCDebug(lcPropagator) << "black list for bulk upload" << file;
_bulkUploadBlackList.insert(file);
}
void OwncloudPropagator::removeFromBulkUploadBlackList(const QString &file)
{
qCDebug(lcPropagator) << "black list for bulk upload" << file;
_bulkUploadBlackList.remove(file);
}
bool OwncloudPropagator::isInBulkUploadBlackList(const QString &file) const
{
return _bulkUploadBlackList.contains(file);
}
// ================================================================================
PropagatorJob::PropagatorJob(OwncloudPropagator *propagator)
@@ -1106,6 +1130,13 @@ void PropagateDirectory::slotSubJobsFinished(SyncFileItem::Status status)
if (_item->_instruction == CSYNC_INSTRUCTION_NEW && _item->_direction == SyncFileItem::Down) {
// special case for local MKDIR, set local directory mtime
// (it's not synced later at all, but can be nice to have it set initially)
if (_item->_modtime <= 0) {
status = _item->_status = SyncFileItem::NormalError;
_item->_errorString = tr("Error updating metadata due to invalid modified time");
qCWarning(lcDirectory) << "Error writing to the database for file" << _item->_file;
}
FileSystem::setModTime(propagator()->fullLocalPath(_item->destination()), _item->_modtime);
}
@@ -1304,13 +1335,4 @@ QString OwncloudPropagator::remotePath() const
return _remoteFolder;
}
BulkPropagatorJob::BulkPropagatorJob(OwncloudPropagator *propagator, const QVector<SyncFileItemPtr> &items)
: PropagatorCompositeJob(propagator)
, _items(items)
{
for(const auto &oneItemJob : _items) {
appendTask(oneItemJob);
}
_items.clear();
}
}

View File

@@ -31,6 +31,8 @@
#include "accountfwd.h"
#include "syncoptions.h"
#include <deque>
namespace OCC {
Q_DECLARE_LOGGING_CATEGORY(lcPropagator)
@@ -46,6 +48,8 @@ qint64 criticalFreeSpaceLimit();
*/
qint64 freeSpaceLimit();
void blacklistUpdate(SyncJournalDb *journal, SyncFileItem &item);
class SyncJournalDb;
class OwncloudPropagator;
class PropagatorCompositeJob;
@@ -380,19 +384,6 @@ private:
bool scheduleDelayedJobs();
};
class BulkPropagatorJob : public PropagatorCompositeJob
{
Q_OBJECT
public:
explicit BulkPropagatorJob(OwncloudPropagator *propagator,
const QVector<SyncFileItemPtr> &items);
private:
QVector<SyncFileItemPtr> _items;
};
/**
* @brief Dummy job that just mark it as completed and ignored
* @ingroup libsync
@@ -431,7 +422,8 @@ public:
public:
OwncloudPropagator(AccountPtr account, const QString &localDir,
const QString &remoteFolder, SyncJournalDb *progressDb)
const QString &remoteFolder, SyncJournalDb *progressDb,
QSet<QString> &bulkUploadBlackList)
: _journal(progressDb)
, _finishedEmited(false)
, _bandwidthManager(this)
@@ -440,6 +432,7 @@ public:
, _account(account)
, _localDir((localDir.endsWith(QChar('/'))) ? localDir : localDir + '/')
, _remoteFolder((remoteFolder.endsWith(QChar('/'))) ? remoteFolder : remoteFolder + '/')
, _bulkUploadBlackList(bulkUploadBlackList)
{
qRegisterMetaType<PropagatorJob::AbortType>("PropagatorJob::AbortType");
}
@@ -611,7 +604,7 @@ public:
Q_REQUIRED_RESULT bool isDelayedUploadItem(const SyncFileItemPtr &item) const;
Q_REQUIRED_RESULT const QVector<SyncFileItemPtr>& delayedTasks() const
Q_REQUIRED_RESULT const std::deque<SyncFileItemPtr>& delayedTasks() const
{
return _delayedTasks;
}
@@ -620,6 +613,12 @@ public:
void clearDelayedTasks();
void addToBulkUploadBlackList(const QString &file);
void removeFromBulkUploadBlackList(const QString &file);
bool isInBulkUploadBlackList(const QString &file) const;
private slots:
void abortTimeout()
@@ -674,8 +673,12 @@ private:
const QString _localDir; // absolute path to the local directory. ends with '/'
const QString _remoteFolder; // remote folder, ends with '/'
QVector<SyncFileItemPtr> _delayedTasks;
std::deque<SyncFileItemPtr> _delayedTasks;
bool _scheduleDelayedTasks = false;
QSet<QString> &_bulkUploadBlackList;
static bool _allowDelayedUpload;
};

View File

@@ -18,9 +18,33 @@
#include "owncloudpropagator.h"
#include "syncfileitem.h"
#include "networkjobs.h"
#include "syncengine.h"
#include <QLoggingCategory>
#include <QNetworkReply>
namespace {
/**
* We do not want to upload files that are currently being modified.
* To avoid that, we don't upload files that have a modification time
* that is too close to the current time.
*
* This interacts with the msBetweenRequestAndSync delay in the folder
* manager. If that delay between file-change notification and sync
* has passed, we should accept the file for upload here.
*/
inline bool fileIsStillChanging(const OCC::SyncFileItem &item)
{
const auto modtime = OCC::Utility::qDateTimeFromTime_t(item._modtime);
const qint64 msSinceMod = modtime.msecsTo(QDateTime::currentDateTimeUtc());
return std::chrono::milliseconds(msSinceMod) < OCC::SyncEngine::minimumFileAgeForUpload
// if the mtime is too much in the future we *do* upload the file
&& msSinceMod > -10000;
}
}
namespace OCC {
inline QByteArray getEtagFromReply(QNetworkReply *reply)

View File

@@ -564,6 +564,10 @@ void PropagateDownloadFile::startAfterIsEncryptedIsChecked()
return checksum_header.startsWith("SHA")
|| checksum_header.startsWith("MD5:");
};
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
if (_item->_instruction == CSYNC_INSTRUCTION_CONFLICT
&& _item->_size == _item->_previousSize
&& !_item->_checksumHeader.isEmpty()
@@ -592,11 +596,22 @@ void PropagateDownloadFile::conflictChecksumComputed(const QByteArray &checksumT
// Apply the server mtime locally if necessary, ensuring the journal
// and local mtimes end up identical
auto fn = propagator()->fullLocalPath(_item->_file);
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
return;
}
if (_item->_modtime != _item->_previousModtime) {
Q_ASSERT(_item->_modtime > 0);
FileSystem::setModTime(fn, _item->_modtime);
emit propagator()->touchedFile(fn);
}
_item->_modtime = FileSystem::getModTime(fn);
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
return;
}
updateMetadata(/*isConflict=*/false);
return;
}
@@ -820,6 +835,10 @@ void PropagateDownloadFile::slotGetFinished()
// It is possible that the file was modified on the server since we did the discovery phase
// so make sure we have the up-to-date time
_item->_modtime = job->lastModified();
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
}
_tmpFile.close();
@@ -1058,10 +1077,28 @@ void PropagateDownloadFile::downloadFinished()
return;
}
if (_item->_modtime <= 0) {
FileSystem::remove(_tmpFile.fileName());
done(SyncFileItem::NormalError, tr("File %1 has invalid modified time reported by server. Do not save it.").arg(QDir::toNativeSeparators(_item->_file)));
return;
}
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
FileSystem::setModTime(_tmpFile.fileName(), _item->_modtime);
// We need to fetch the time again because some file systems such as FAT have worse than a second
// Accuracy, and we really need the time from the file system. (#3103)
_item->_modtime = FileSystem::getModTime(_tmpFile.fileName());
if (_item->_modtime <= 0) {
FileSystem::remove(_tmpFile.fileName());
done(SyncFileItem::NormalError, tr("File %1 has invalid modified time reported by server. Do not save it.").arg(QDir::toNativeSeparators(_item->_file)));
return;
}
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
bool previousFileExists = FileSystem::fileExists(fn);
if (previousFileExists) {

View File

@@ -96,7 +96,7 @@ public:
void giveBandwidthQuota(qint64 q);
qint64 currentDownloadPosition();
QString errorString() const;
QString errorString() const override;
void setErrorString(const QString &s) { _errorString = s; }
SyncFileItem::Status errorStatus() { return _errorStatus; }

View File

@@ -49,25 +49,6 @@ Q_LOGGING_CATEGORY(lcPropagateUpload, "nextcloud.sync.propagator.upload", QtInfo
Q_LOGGING_CATEGORY(lcPropagateUploadV1, "nextcloud.sync.propagator.upload.v1", QtInfoMsg)
Q_LOGGING_CATEGORY(lcPropagateUploadNG, "nextcloud.sync.propagator.upload.ng", QtInfoMsg)
/**
* We do not want to upload files that are currently being modified.
* To avoid that, we don't upload files that have a modification time
* that is too close to the current time.
*
* This interacts with the msBetweenRequestAndSync delay in the folder
* manager. If that delay between file-change notification and sync
* has passed, we should accept the file for upload here.
*/
static bool fileIsStillChanging(const SyncFileItem &item)
{
const QDateTime modtime = Utility::qDateTimeFromTime_t(item._modtime);
const qint64 msSinceMod = modtime.msecsTo(QDateTime::currentDateTimeUtc());
return std::chrono::milliseconds(msSinceMod) < SyncEngine::minimumFileAgeForUpload
// if the mtime is too much in the future we *do* upload the file
&& msSinceMod > -10000;
}
PUTFileJob::~PUTFileJob()
{
// Make sure that we destroy the QNetworkReply before our _device of which it keeps an internal pointer.
@@ -229,6 +210,12 @@ void PropagateUploadFileCommon::start()
}
_item->_file = _item->_renameTarget;
_item->_modtime = FileSystem::getModTime(newFilePathAbsolute);
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
slotOnErrorStartFolderUnlock(SyncFileItem::NormalError, tr("File %1 has invalid modified time. Do not upload to the server.").arg(QDir::toNativeSeparators(_item->_file)));
return;
}
}
SyncJournalFileRecord parentRec;
@@ -331,6 +318,10 @@ void PropagateUploadFileCommon::slotComputeContentChecksum()
// and not the _fileToUpload because we are checking the original file, not there
// probably temporary one.
_item->_modtime = FileSystem::getModTime(filePath);
if (_item->_modtime <= 0) {
slotOnErrorStartFolderUnlock(SyncFileItem::NormalError, tr("File %1 has invalid modified time. Do not upload to the server.").arg(QDir::toNativeSeparators(_item->_file)));
return;
}
const QByteArray checksumType = propagator()->account()->capabilities().preferredUploadChecksumType();
@@ -402,11 +393,27 @@ void PropagateUploadFileCommon::slotStartUpload(const QByteArray &transmissionCh
if (!FileSystem::fileExists(fullFilePath)) {
return slotOnErrorStartFolderUnlock(SyncFileItem::SoftError, tr("File Removed (start upload) %1").arg(fullFilePath));
}
if (_item->_modtime <= 0) {
slotOnErrorStartFolderUnlock(SyncFileItem::NormalError, tr("File %1 has invalid modified time. Do not upload to the server.").arg(QDir::toNativeSeparators(_item->_file)));
return;
}
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
time_t prevModtime = _item->_modtime; // the _item value was set in PropagateUploadFile::start()
// but a potential checksum calculation could have taken some time during which the file could
// have been changed again, so better check again here.
_item->_modtime = FileSystem::getModTime(originalFilePath);
if (_item->_modtime <= 0) {
slotOnErrorStartFolderUnlock(SyncFileItem::NormalError, tr("File %1 has invalid modified time. Do not upload to the server.").arg(QDir::toNativeSeparators(_item->_file)));
return;
}
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
if (prevModtime != _item->_modtime) {
propagator()->_anotherSyncNeeded = true;
qDebug() << "prevModtime" << prevModtime << "Curr" << _item->_modtime;
@@ -604,6 +611,10 @@ void PropagateUploadFileCommon::startPollJob(const QString &path)
info._file = _item->_file;
info._url = path;
info._modtime = _item->_modtime;
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
info._fileSize = _item->_size;
propagator()->_journal->setPollInfo(info);
propagator()->_journal->commit("add poll info");
@@ -726,6 +737,10 @@ QMap<QByteArray, QByteArray> PropagateUploadFileCommon::headers()
{
QMap<QByteArray, QByteArray> headers;
headers[QByteArrayLiteral("Content-Type")] = QByteArrayLiteral("application/octet-stream");
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
headers[QByteArrayLiteral("X-OC-Mtime")] = QByteArray::number(qint64(_item->_modtime));
if (qEnvironmentVariableIntValue("OWNCLOUD_LAZYOPS"))
headers[QByteArrayLiteral("OC-LazyOps")] = QByteArrayLiteral("true");

View File

@@ -131,7 +131,7 @@ public:
return _device;
}
QString errorString()
QString errorString() const override
{
return _errorString.isEmpty() ? AbstractNetworkJob::errorString() : _errorString;
}

View File

@@ -83,6 +83,10 @@ void PropagateUploadFileNG::doStartUpload()
propagator()->_activeJobList.append(this);
const SyncJournalDb::UploadInfo progressInfo = propagator()->_journal->getUploadInfo(_item->_file);
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
if (progressInfo._valid && progressInfo.isChunked() && progressInfo._modtime == _item->_modtime
&& progressInfo._size == _item->_size) {
_transferId = progressInfo._transferid;
@@ -229,6 +233,10 @@ void PropagateUploadFileNG::slotDeleteJobFinished()
void PropagateUploadFileNG::startNewUpload()
{
ASSERT(propagator()->_activeJobList.count(this) == 1);
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
_transferId = uint(Utility::rand() ^ uint(_item->_modtime) ^ (uint(_fileToUpload._size) << 16) ^ qHash(_fileToUpload._file));
_sent = 0;
_currentChunk = 0;
@@ -238,6 +246,10 @@ void PropagateUploadFileNG::startNewUpload()
SyncJournalDb::UploadInfo pi;
pi._valid = true;
pi._transferid = _transferId;
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
pi._modtime = _item->_modtime;
pi._contentChecksum = _item->_checksumHeader;
pi._size = _item->_size;
@@ -423,6 +435,10 @@ void PropagateUploadFileNG::slotPutFinished()
}
// Check whether the file changed since discovery - this acts on the original file.
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
if (!FileSystem::verifyFileUnchanged(fullFilePath, _item->_size, _item->_modtime)) {
propagator()->_anotherSyncNeeded = true;
if (!_finished) {

View File

@@ -39,10 +39,18 @@ void PropagateUploadFileV1::doStartUpload()
{
_chunkCount = int(std::ceil(_fileToUpload._size / double(chunkSize())));
_startChunk = 0;
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
_transferId = uint(Utility::rand()) ^ uint(_item->_modtime) ^ (uint(_fileToUpload._size) << 16);
const SyncJournalDb::UploadInfo progressInfo = propagator()->_journal->getUploadInfo(_item->_file);
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
if (progressInfo._valid && progressInfo.isChunked() && progressInfo._modtime == _item->_modtime && progressInfo._size == _item->_size
&& (progressInfo._contentChecksum == _item->_checksumHeader || progressInfo._contentChecksum.isEmpty() || _item->_checksumHeader.isEmpty())) {
_startChunk = progressInfo._chunk;
@@ -56,6 +64,10 @@ void PropagateUploadFileV1::doStartUpload()
pi._valid = true;
pi._chunk = 0;
pi._transferid = 0; // We set a null transfer id because it is not chunked.
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
pi._modtime = _item->_modtime;
pi._errorCount = 0;
pi._contentChecksum = _item->_checksumHeader;
@@ -245,6 +257,10 @@ void PropagateUploadFileV1::slotPutFinished()
}
// Check whether the file changed since discovery. the file check here is the original and not the temprary.
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
if (!FileSystem::verifyFileUnchanged(fullFilePath, _item->_size, _item->_modtime)) {
propagator()->_anotherSyncNeeded = true;
if (!_finished) {
@@ -283,6 +299,10 @@ void PropagateUploadFileV1::slotPutFinished()
}
pi._chunk = (currentChunk + _startChunk + 1) % _chunkCount; // next chunk to start with
pi._transferid = _transferId;
Q_ASSERT(_item->_modtime > 0);
if (_item->_modtime <= 0) {
qCWarning(lcPropagateUpload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
pi._modtime = _item->_modtime;
pi._errorCount = 0; // successful chunk upload resets
pi._contentChecksum = _item->_checksumHeader;

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2021 (c) Matthieu Gallien <matthieu.gallien@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#include "putmultifilejob.h"
#include <QHttpPart>
namespace OCC {
Q_LOGGING_CATEGORY(lcPutMultiFileJob, "nextcloud.sync.networkjob.put.multi", QtInfoMsg)
PutMultiFileJob::~PutMultiFileJob() = default;
void PutMultiFileJob::start()
{
QNetworkRequest req;
for(auto &oneDevice : _devices) {
auto onePart = QHttpPart{};
onePart.setBodyDevice(oneDevice._device.get());
for (QMap<QByteArray, QByteArray>::const_iterator it = oneDevice._headers.begin(); it != oneDevice._headers.end(); ++it) {
onePart.setRawHeader(it.key(), it.value());
}
req.setPriority(QNetworkRequest::LowPriority); // Long uploads must not block non-propagation jobs.
_body.append(onePart);
}
sendRequest("POST", _url, req, &_body);
if (reply()->error() != QNetworkReply::NoError) {
qCWarning(lcPutMultiFileJob) << " Network error: " << reply()->errorString();
}
connect(reply(), &QNetworkReply::uploadProgress, this, &PutMultiFileJob::uploadProgress);
connect(this, &AbstractNetworkJob::networkActivity, account().data(), &Account::propagatorNetworkActivity);
_requestTimer.start();
AbstractNetworkJob::start();
}
bool PutMultiFileJob::finished()
{
for(const auto &oneDevice : _devices) {
oneDevice._device->close();
}
qCInfo(lcPutMultiFileJob) << "POST of" << reply()->request().url().toString() << path() << "FINISHED WITH STATUS"
<< replyStatusString()
<< reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute)
<< reply()->attribute(QNetworkRequest::HttpReasonPhraseAttribute);
emit finishedSignal();
return true;
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2021 (c) Matthieu Gallien <matthieu.gallien@nextcloud.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* for more details.
*/
#pragma once
#include "abstractnetworkjob.h"
#include "propagateupload.h"
#include "account.h"
#include <QLoggingCategory>
#include <QMap>
#include <QByteArray>
#include <QUrl>
#include <QString>
#include <QElapsedTimer>
#include <QHttpMultiPart>
#include <memory>
class QIODevice;
namespace OCC {
Q_DECLARE_LOGGING_CATEGORY(lcPutMultiFileJob)
struct SingleUploadFileData
{
std::unique_ptr<UploadDevice> _device;
QMap<QByteArray, QByteArray> _headers;
};
/**
* @brief The PutMultiFileJob class
* @ingroup libsync
*/
class OWNCLOUDSYNC_EXPORT PutMultiFileJob : public AbstractNetworkJob
{
Q_OBJECT
public:
explicit PutMultiFileJob(AccountPtr account, const QUrl &url,
std::vector<SingleUploadFileData> devices, QObject *parent = nullptr)
: AbstractNetworkJob(account, {}, parent)
, _devices(std::move(devices))
, _url(url)
{
_body.setContentType(QHttpMultiPart::RelatedType);
for(auto &singleDevice : _devices) {
singleDevice._device->setParent(this);
connect(this, &PutMultiFileJob::uploadProgress,
singleDevice._device.get(), &UploadDevice::slotJobUploadProgress);
}
}
~PutMultiFileJob() override;
void start() override;
bool finished() override;
QString errorString() const override
{
return _errorString.isEmpty() ? AbstractNetworkJob::errorString() : _errorString;
}
std::chrono::milliseconds msSinceStart() const
{
return std::chrono::milliseconds(_requestTimer.elapsed());
}
signals:
void finishedSignal();
void uploadProgress(qint64, qint64);
private:
QHttpMultiPart _body;
std::vector<SingleUploadFileData> _devices;
QString _errorString;
QUrl _url;
QElapsedTimer _requestTimer;
};
}

View File

@@ -711,7 +711,7 @@ void SyncEngine::slotDiscoveryFinished()
_journal->commit(QStringLiteral("post treewalk"));
_propagator = QSharedPointer<OwncloudPropagator>(
new OwncloudPropagator(_account, _localPath, _remotePath, _journal));
new OwncloudPropagator(_account, _localPath, _remotePath, _journal, _bulkUploadBlackList));
_propagator->setSyncOptions(_syncOptions);
connect(_propagator.data(), &OwncloudPropagator::itemCompleted,
this, &SyncEngine::slotItemCompleted);

View File

@@ -241,6 +241,8 @@ private:
QScopedPointer<DiscoveryPhase> _discoveryPhase;
QSharedPointer<OwncloudPropagator> _propagator;
QSet<QString> _bulkUploadBlackList;
// List of all files with conflicts
QSet<QString> _seenConflictFiles;

View File

@@ -17,6 +17,7 @@
#include "common/utility.h"
#include "common/filesystembase.h"
#include "hydrationjob.h"
#include "theme.h"
#include "vfs_cfapi.h"
#include <QCoreApplication>
@@ -40,6 +41,9 @@ Q_LOGGING_CATEGORY(lcCfApiWrapper, "nextcloud.sync.vfs.cfapi.wrapper", QtInfoMsg
FIELD_SIZE( CF_OPERATION_PARAMETERS, field ) )
namespace {
constexpr auto syncRootFlagsFull = 34;
constexpr auto syncRootFlagsNoCfApiContextMenu = 2;
void cfApiSendTransferInfo(const CF_CONNECTION_KEY &connectionKey, const CF_TRANSFER_KEY &transferKey, NTSTATUS status, void *buffer, qint64 offset, qint64 currentBlockLength, qint64 totalLength)
{
@@ -428,8 +432,10 @@ bool createSyncRootRegistryKeys(const QString &providerName, const QString &fold
QVariant value;
};
const auto flags = OCC::Theme::instance()->enforceVirtualFilesSyncFolder() ? syncRootFlagsNoCfApiContextMenu : syncRootFlagsFull;
const QVector<RegistryKeyInfo> registryKeysToSet = {
{ providerSyncRootIdRegistryKey, QStringLiteral("Flags"), REG_DWORD, 34 },
{ providerSyncRootIdRegistryKey, QStringLiteral("Flags"), REG_DWORD, flags },
{ providerSyncRootIdRegistryKey, QStringLiteral("DisplayNameResource"), REG_EXPAND_SZ, displayName },
{ providerSyncRootIdRegistryKey, QStringLiteral("IconResource"), REG_EXPAND_SZ, QString(QDir::toNativeSeparators(qApp->applicationFilePath()) + QStringLiteral(",0")) },
{ providerSyncRootIdUserSyncRootsRegistryKey, windowsSid, REG_SZ, syncRootPath }
@@ -586,13 +592,18 @@ OCC::CfApiWrapper::FileHandle OCC::CfApiWrapper::handleForPath(const QString &pa
return {};
}
if (QFileInfo(path).isDir()) {
QFileInfo pathFileInfo(path);
if (!pathFileInfo.exists()) {
return {};
}
if (pathFileInfo.isDir()) {
HANDLE handle = nullptr;
const qint64 openResult = CfOpenFileWithOplock(path.toStdWString().data(), CF_OPEN_FILE_FLAG_NONE, &handle);
if (openResult == S_OK) {
return {handle, [](HANDLE h) { CfCloseHandle(h); }};
}
} else {
} else if (pathFileInfo.isFile()) {
const auto longpath = OCC::FileSystem::longWinPath(path);
const auto handle = CreateFile(longpath.toStdWString().data(), 0, 0, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
@@ -618,7 +629,6 @@ OCC::CfApiWrapper::PlaceHolderInfo OCC::CfApiWrapper::findPlaceholderInfo(const
if (result == S_OK) {
return info;
} else {
qCWarning(lcCfApiWrapper()) << "Couldn't get placeholder info" << QString::fromWCharArray(_com_error(result).ErrorMessage());
return {};
}
}
@@ -639,6 +649,10 @@ OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::se
OCC::Result<void, QString> OCC::CfApiWrapper::createPlaceholderInfo(const QString &path, time_t modtime, qint64 size, const QByteArray &fileId)
{
if (modtime <= 0) {
return {QString{"Could not update metadata due to invalid modified time for %1: %2"}.arg(path).arg(modtime)};
}
const auto fileInfo = QFileInfo(path);
const auto localBasePath = QDir::toNativeSeparators(fileInfo.path()).toStdWString();
const auto relativePath = fileInfo.fileName().toStdWString();
@@ -687,6 +701,10 @@ OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::up
{
Q_ASSERT(handle);
if (modtime <= 0) {
return {QString{"Could not update metadata due to invalid modified time for %1: %2"}.arg(pathForHandle(handle)).arg(modtime)};
}
const auto info = replacesPath.isEmpty() ? findPlaceholderInfo(handle)
: findPlaceholderInfo(handleForPath(replacesPath));
if (!info) {
@@ -703,13 +721,14 @@ OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::up
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &metadata.BasicInfo.LastWriteTime);
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &metadata.BasicInfo.LastAccessTime);
OCC::Utility::UnixTimeToLargeIntegerFiletime(modtime, &metadata.BasicInfo.ChangeTime);
metadata.BasicInfo.FileAttributes = 0;
const qint64 result = CfUpdatePlaceholder(handle.get(), &metadata,
fileIdentity.data(), sizeToDWORD(fileIdentitySize),
nullptr, 0, CF_UPDATE_FLAG_MARK_IN_SYNC, nullptr, nullptr);
if (result != S_OK) {
qCWarning(lcCfApiWrapper) << "Couldn't update placeholder info for" << pathForHandle(handle) << ":" << QString::fromWCharArray(_com_error(result).ErrorMessage());
qCWarning(lcCfApiWrapper) << "Couldn't update placeholder info for" << pathForHandle(handle) << ":" << QString::fromWCharArray(_com_error(result).ErrorMessage()) << replacesPath;
return { "Couldn't update placeholder info" };
}
@@ -721,6 +740,38 @@ OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::up
return OCC::Vfs::ConvertToPlaceholderResult::Ok;
}
OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::dehydratePlaceholder(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId)
{
Q_ASSERT(handle);
if (modtime <= 0) {
return {QString{"Could not update metadata due to invalid modification time for %1: %2"}.arg(pathForHandle(handle)).arg(modtime)};
}
const auto info = findPlaceholderInfo(handle);
if (!info) {
return { "Can't update non existing placeholder info" };
}
const auto fileIdentity = QString::fromUtf8(fileId).toStdWString();
const auto fileIdentitySize = (fileIdentity.length() + 1) * sizeof(wchar_t);
CF_FILE_RANGE dehydrationRange;
dehydrationRange.StartingOffset.QuadPart = 0;
dehydrationRange.Length.QuadPart = size;
const qint64 result = CfUpdatePlaceholder(handle.get(), nullptr,
fileIdentity.data(), sizeToDWORD(fileIdentitySize),
&dehydrationRange, 1, CF_UPDATE_FLAG_MARK_IN_SYNC, nullptr, nullptr);
if (result != S_OK) {
qCWarning(lcCfApiWrapper) << "Couldn't update placeholder info for" << pathForHandle(handle) << ":" << QString::fromWCharArray(_com_error(result).ErrorMessage());
return { "Couldn't update placeholder info" };
}
return OCC::Vfs::ConvertToPlaceholderResult::Ok;
}
OCC::Result<OCC::Vfs::ConvertToPlaceholderResult, QString> OCC::CfApiWrapper::convertToPlaceholder(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath)
{
Q_UNUSED(modtime);

View File

@@ -94,6 +94,7 @@ NEXTCLOUD_CFAPI_EXPORT Result<OCC::Vfs::ConvertToPlaceholderResult, QString> set
NEXTCLOUD_CFAPI_EXPORT Result<void, QString> createPlaceholderInfo(const QString &path, time_t modtime, qint64 size, const QByteArray &fileId);
NEXTCLOUD_CFAPI_EXPORT Result<OCC::Vfs::ConvertToPlaceholderResult, QString> updatePlaceholderInfo(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath = QString());
NEXTCLOUD_CFAPI_EXPORT Result<OCC::Vfs::ConvertToPlaceholderResult, QString> convertToPlaceholder(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath);
NEXTCLOUD_CFAPI_EXPORT Result<OCC::Vfs::ConvertToPlaceholderResult, QString> dehydratePlaceholder(const FileHandle &handle, time_t modtime, qint64 size, const QByteArray &fileId);
}

View File

@@ -132,26 +132,19 @@ Result<void, QString> VfsCfApi::createPlaceholder(const SyncFileItem &item)
Result<void, QString> VfsCfApi::dehydratePlaceholder(const SyncFileItem &item)
{
const auto previousPin = pinState(item._file);
if (!FileSystem::remove(_setupParams.filesystemPath + item._file)) {
return QStringLiteral("Couldn't remove %1 to fulfill dehydration").arg(item._file);
}
const auto r = createPlaceholder(item);
if (!r) {
return r;
}
if (previousPin) {
if (*previousPin == PinState::AlwaysLocal) {
setPinState(item._file, PinState::Unspecified);
const auto localPath = QDir::toNativeSeparators(_setupParams.filesystemPath + item._file);
const auto handle = cfapi::handleForPath(localPath);
if (handle) {
auto result = cfapi::dehydratePlaceholder(handle, item._modtime, item._size, item._fileId);
if (result) {
return {};
} else {
setPinState(item._file, *previousPin);
return result.error();
}
} else {
qCWarning(lcCfApi) << "Couldn't update metadata for non existing file" << localPath;
return {QStringLiteral("Couldn't update metadata")};
}
return {};
}
Result<Vfs::ConvertToPlaceholderResult, QString> VfsCfApi::convertToPlaceholder(const QString &filename, const SyncFileItem &item, const QString &replacesFile)
@@ -160,6 +153,9 @@ Result<Vfs::ConvertToPlaceholderResult, QString> VfsCfApi::convertToPlaceholder(
const auto replacesPath = QDir::toNativeSeparators(replacesFile);
const auto handle = cfapi::handleForPath(localPath);
if (!handle) {
return { "Invalid handle for path " + localPath };
}
if (cfapi::findPlaceholderInfo(handle)) {
return cfapi::updatePlaceholderInfo(handle, item._modtime, item._size, item._fileId, replacesPath);
} else {

View File

@@ -68,12 +68,20 @@ bool VfsSuffix::isHydrating() const
Result<void, QString> VfsSuffix::updateMetadata(const QString &filePath, time_t modtime, qint64, const QByteArray &)
{
if (modtime <= 0) {
return {tr("Error updating metadata due to invalid modified time")};
}
FileSystem::setModTime(filePath, modtime);
return {};
}
Result<void, QString> VfsSuffix::createPlaceholder(const SyncFileItem &item)
{
if (item._modtime <= 0) {
return {tr("Error updating metadata due to invalid modified time")};
}
// The concrete shape of the placeholder is also used in isDehydratedPlaceholder() below
QString fn = _setupParams.filesystemPath + item._file;
if (!fn.endsWith(fileSuffix())) {

View File

@@ -69,12 +69,20 @@ bool VfsXAttr::isHydrating() const
Result<void, QString> VfsXAttr::updateMetadata(const QString &filePath, time_t modtime, qint64, const QByteArray &)
{
if (modtime <= 0) {
return {tr("Error updating metadata due to invalid modified time")};
}
FileSystem::setModTime(filePath, modtime);
return {};
}
Result<void, QString> VfsXAttr::createPlaceholder(const SyncFileItem &item)
{
if (item._modtime <= 0) {
return {tr("Error updating metadata due to invalid modified time")};
}
const auto path = QString(_setupParams.filesystemPath + item._file);
QFile file(path);
if (file.exists() && file.size() > 1

View File

@@ -8,6 +8,7 @@
]Icon\r*
].DS_Store
].ds_store
*.textClipping
._*
]Thumbs.db
]photothumb.db

View File

@@ -9,6 +9,10 @@
#include "httplogger.h"
#include "accessmanager.h"
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <memory>
@@ -416,6 +420,109 @@ void FakePutReply::abort()
emit finished();
}
FakePutMultiFileReply::FakePutMultiFileReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QString &contentType, const QByteArray &putPayload, QObject *parent)
: FakeReply { parent }
{
setRequest(request);
setUrl(request.url());
setOperation(op);
open(QIODevice::ReadOnly);
_allFileInfo = performMultiPart(remoteRootFileInfo, request, putPayload, contentType);
QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
}
QVector<FileInfo *> FakePutMultiFileReply::performMultiPart(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload, const QString &contentType)
{
QVector<FileInfo *> result;
auto stringPutPayload = QString::fromUtf8(putPayload);
constexpr int boundaryPosition = sizeof("multipart/related; boundary=");
const QString boundaryValue = QStringLiteral("--") + contentType.mid(boundaryPosition, contentType.length() - boundaryPosition - 1) + QStringLiteral("\r\n");
auto stringPutPayloadRef = QString{stringPutPayload}.left(stringPutPayload.size() - 2 - boundaryValue.size());
auto allParts = stringPutPayloadRef.split(boundaryValue, Qt::SkipEmptyParts);
for (const auto &onePart : allParts) {
auto headerEndPosition = onePart.indexOf(QStringLiteral("\r\n\r\n"));
auto onePartHeaderPart = onePart.left(headerEndPosition);
auto onePartBody = onePart.mid(headerEndPosition + 4, onePart.size() - headerEndPosition - 6);
auto onePartHeaders = onePartHeaderPart.split(QStringLiteral("\r\n"));
QMap<QString, QString> allHeaders;
for(auto oneHeader : onePartHeaders) {
auto headerParts = oneHeader.split(QStringLiteral(": "));
allHeaders[headerParts.at(0)] = headerParts.at(1);
}
auto fileName = allHeaders[QStringLiteral("X-File-Path")];
Q_ASSERT(!fileName.isEmpty());
FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
if (fileInfo) {
fileInfo->size = onePartBody.size();
fileInfo->contentChar = onePartBody.at(0).toLatin1();
} else {
// Assume that the file is filled with the same character
fileInfo = remoteRootFileInfo.create(fileName, onePartBody.size(), onePartBody.at(0).toLatin1());
}
fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong());
remoteRootFileInfo.find(fileName, /*invalidateEtags=*/true);
result.push_back(fileInfo);
}
return result;
}
void FakePutMultiFileReply::respond()
{
QJsonDocument reply;
QJsonObject allFileInfoReply;
qint64 totalSize = 0;
std::for_each(_allFileInfo.begin(), _allFileInfo.end(), [&totalSize](const auto &fileInfo) {
totalSize += fileInfo->size;
});
for(auto fileInfo : qAsConst(_allFileInfo)) {
QJsonObject fileInfoReply;
fileInfoReply.insert("error", QStringLiteral("false"));
fileInfoReply.insert("OC-OperationStatus", fileInfo->operationStatus);
fileInfoReply.insert("X-File-Path", fileInfo->path());
fileInfoReply.insert("OC-ETag", QLatin1String{fileInfo->etag});
fileInfoReply.insert("ETag", QLatin1String{fileInfo->etag});
fileInfoReply.insert("etag", QLatin1String{fileInfo->etag});
fileInfoReply.insert("OC-FileID", QLatin1String{fileInfo->fileId});
fileInfoReply.insert("X-OC-MTime", "accepted"); // Prevents Q_ASSERT(!_runningNow) since we'll call PropagateItemJob::done twice in that case.
emit uploadProgress(fileInfo->size, totalSize);
allFileInfoReply.insert(QChar('/') + fileInfo->path(), fileInfoReply);
}
reply.setObject(allFileInfoReply);
_payload = reply.toJson();
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
setFinished(true);
if (bytesAvailable()) {
emit readyRead();
}
emit metaDataChanged();
emit finished();
}
void FakePutMultiFileReply::abort()
{
setError(OperationCanceledError, QStringLiteral("abort"));
emit finished();
}
qint64 FakePutMultiFileReply::bytesAvailable() const
{
return _payload.size() + QIODevice::bytesAvailable();
}
qint64 FakePutMultiFileReply::readData(char *data, qint64 maxlen)
{
qint64 len = std::min(qint64 { _payload.size() }, maxlen);
std::copy(_payload.cbegin(), _payload.cbegin() + len, data);
_payload.remove(0, static_cast<int>(len));
return len;
}
FakeMkcolReply::FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
: FakeReply { parent }
{
@@ -813,43 +920,77 @@ FakeQNAM::FakeQNAM(FileInfo initialRoot)
setCookieJar(new OCC::CookieJar);
}
QJsonObject FakeQNAM::forEachReplyPart(QIODevice *outgoingData,
const QString &contentType,
std::function<QJsonObject (const QMap<QString, QByteArray> &)> replyFunction)
{
auto fullReply = QJsonObject{};
auto putPayload = outgoingData->peek(outgoingData->bytesAvailable());
outgoingData->reset();
auto stringPutPayload = QString::fromUtf8(putPayload);
constexpr int boundaryPosition = sizeof("multipart/related; boundary=");
const QString boundaryValue = QStringLiteral("--") + contentType.mid(boundaryPosition, contentType.length() - boundaryPosition - 1) + QStringLiteral("\r\n");
auto stringPutPayloadRef = QString{stringPutPayload}.left(stringPutPayload.size() - 2 - boundaryValue.size());
auto allParts = stringPutPayloadRef.split(boundaryValue, Qt::SkipEmptyParts);
for (const auto &onePart : qAsConst(allParts)) {
auto headerEndPosition = onePart.indexOf(QStringLiteral("\r\n\r\n"));
auto onePartHeaderPart = onePart.left(headerEndPosition);
auto onePartHeaders = onePartHeaderPart.split(QStringLiteral("\r\n"));
QMap<QString, QByteArray> allHeaders;
for(const auto &oneHeader : qAsConst(onePartHeaders)) {
auto headerParts = oneHeader.split(QStringLiteral(": "));
allHeaders[headerParts.at(0)] = headerParts.at(1).toLatin1();
}
auto reply = replyFunction(allHeaders);
if (reply.contains(QStringLiteral("error")) &&
reply.contains(QStringLiteral("etag"))) {
fullReply.insert(allHeaders[QStringLiteral("X-File-Path")], reply);
}
}
return fullReply;
}
QNetworkReply *FakeQNAM::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData)
{
QNetworkReply *reply = nullptr;
auto newRequest = request;
newRequest.setRawHeader("X-Request-ID", OCC::AccessManager::generateRequestId());
auto contentType = request.header(QNetworkRequest::ContentTypeHeader).toString();
if (_override) {
if (auto _reply = _override(op, newRequest, outgoingData)) {
reply = _reply;
}
}
if (!reply) {
const QString fileName = getFilePathFromUrl(newRequest.url());
Q_ASSERT(!fileName.isNull());
if (_errorPaths.contains(fileName)) {
reply = new FakeErrorReply { op, newRequest, this, _errorPaths[fileName] };
}
reply = overrideReplyWithError(getFilePathFromUrl(newRequest.url()), op, newRequest);
}
if (!reply) { const bool isUpload = newRequest.url().path().startsWith(sUploadUrl.path());
if (!reply) {
const bool isUpload = newRequest.url().path().startsWith(sUploadUrl.path());
FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo;
auto verb = newRequest.attribute(QNetworkRequest::CustomVerbAttribute);
if (verb == QLatin1String("PROPFIND"))
if (verb == QLatin1String("PROPFIND")) {
// Ignore outgoingData always returning somethign good enough, works for now.
reply = new FakePropfindReply { info, op, newRequest, this };
else if (verb == QLatin1String("GET") || op == QNetworkAccessManager::GetOperation)
} else if (verb == QLatin1String("GET") || op == QNetworkAccessManager::GetOperation) {
reply = new FakeGetReply { info, op, newRequest, this };
else if (verb == QLatin1String("PUT") || op == QNetworkAccessManager::PutOperation)
} else if (verb == QLatin1String("PUT") || op == QNetworkAccessManager::PutOperation) {
reply = new FakePutReply { info, op, newRequest, outgoingData->readAll(), this };
else if (verb == QLatin1String("MKCOL"))
} else if (verb == QLatin1String("MKCOL")) {
reply = new FakeMkcolReply { info, op, newRequest, this };
else if (verb == QLatin1String("DELETE") || op == QNetworkAccessManager::DeleteOperation)
} else if (verb == QLatin1String("DELETE") || op == QNetworkAccessManager::DeleteOperation) {
reply = new FakeDeleteReply { info, op, newRequest, this };
else if (verb == QLatin1String("MOVE") && !isUpload)
} else if (verb == QLatin1String("MOVE") && !isUpload) {
reply = new FakeMoveReply { info, op, newRequest, this };
else if (verb == QLatin1String("MOVE") && isUpload)
} else if (verb == QLatin1String("MOVE") && isUpload) {
reply = new FakeChunkMoveReply { info, _remoteRootFileInfo, op, newRequest, this };
else {
} else if (verb == QLatin1String("POST") || op == QNetworkAccessManager::PostOperation) {
if (contentType.startsWith(QStringLiteral("multipart/related; boundary="))) {
reply = new FakePutMultiFileReply { info, op, newRequest, contentType, outgoingData->readAll(), this };
}
} else {
qDebug() << verb << outgoingData;
Q_UNREACHABLE();
}
@@ -858,6 +999,18 @@ QNetworkReply *FakeQNAM::createRequest(QNetworkAccessManager::Operation op, cons
return reply;
}
QNetworkReply * FakeQNAM::overrideReplyWithError(QString fileName, QNetworkAccessManager::Operation op, QNetworkRequest newRequest)
{
QNetworkReply *reply = nullptr;
Q_ASSERT(!fileName.isNull());
if (_errorPaths.contains(fileName)) {
reply = new FakeErrorReply { op, newRequest, this, _errorPaths[fileName] };
}
return reply;
}
FakeFolder::FakeFolder(const FileInfo &fileTemplate, const OCC::Optional<FileInfo> &localFileInfo, const QString &remotePath)
: _localModifier(_tempDir.path())
{
@@ -1079,3 +1232,12 @@ FakeReply::FakeReply(QObject *parent)
}
FakeReply::~FakeReply() = default;
FakeJsonErrorReply::FakeJsonErrorReply(QNetworkAccessManager::Operation op,
const QNetworkRequest &request,
QObject *parent,
int httpErrorCode,
const QJsonDocument &reply)
: FakeErrorReply{ op, request, parent, httpErrorCode, reply.toJson() }
{
}

View File

@@ -28,6 +28,8 @@
#include <cookiejar.h>
#include <QTimer>
class QJsonDocument;
/*
* TODO: In theory we should use QVERIFY instead of Q_ASSERT for testing, but this
* only works when directly called from a QTest :-(
@@ -148,6 +150,7 @@ public:
void fixupParentPathRecursively();
QString name;
int operationStatus = 200;
bool isDir = true;
bool isShared = false;
OCC::RemotePermissions permissions; // When uset, defaults to everything
@@ -214,6 +217,27 @@ public:
qint64 readData(char *, qint64) override { return 0; }
};
class FakePutMultiFileReply : public FakeReply
{
Q_OBJECT
public:
FakePutMultiFileReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QString &contentType, const QByteArray &putPayload, QObject *parent);
static QVector<FileInfo *> performMultiPart(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload, const QString &contentType);
Q_INVOKABLE virtual void respond();
void abort() override;
qint64 bytesAvailable() const override;
qint64 readData(char *data, qint64 maxlen) override;
private:
QVector<FileInfo *> _allFileInfo;
QByteArray _payload;
};
class FakeMkcolReply : public FakeReply
{
Q_OBJECT
@@ -354,6 +378,17 @@ public:
QByteArray _body;
};
class FakeJsonErrorReply : public FakeErrorReply
{
Q_OBJECT
public:
FakeJsonErrorReply(QNetworkAccessManager::Operation op,
const QNetworkRequest &request,
QObject *parent,
int httpErrorCode,
const QJsonDocument &reply = QJsonDocument());
};
// A reply that never responds
class FakeHangingReply : public FakeReply
{
@@ -409,6 +444,12 @@ public:
void setOverride(const Override &override) { _override = override; }
QJsonObject forEachReplyPart(QIODevice *outgoingData,
const QString &contentType,
std::function<QJsonObject(const QMap<QString, QByteArray> &)> replyFunction);
QNetworkReply *overrideReplyWithError(QString fileName, Operation op, QNetworkRequest newRequest);
protected:
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
QIODevice *outgoingData = nullptr) override;
@@ -467,6 +508,11 @@ public:
};
ErrorList serverErrorPaths() { return {_fakeQnam}; }
void setServerOverride(const FakeQNAM::Override &override) { _fakeQnam->setOverride(override); }
QJsonObject forEachReplyPart(QIODevice *outgoingData,
const QString &contentType,
std::function<QJsonObject(const QMap<QString, QByteArray>&)> replyFunction) {
return _fakeQnam->forEachReplyPart(outgoingData, contentType, replyFunction);
}
QString localPath() const;

View File

@@ -243,6 +243,20 @@ private slots:
QCOMPARE(defaultSharePermissionsAvailable, 31);
}
void testBulkUploadAvailable_bulkUploadAvailable_returnTrue()
{
QVariantMap bulkuploadMap;
bulkuploadMap["bulkupload"] = "1.0";
QVariantMap capabilitiesMap;
capabilitiesMap["dav"] = bulkuploadMap;
const auto &capabilities = OCC::Capabilities(capabilitiesMap);
const auto bulkuploadAvailable = capabilities.bulkUpload();
QCOMPARE(bulkuploadAvailable, true);
}
};
QTEST_GUILESS_MAIN(TestCapabilities)

View File

@@ -212,10 +212,16 @@ private slots:
const QString fileWithSpaces1(" foo");
const QString fileWithSpaces2(" bar ");
const QString fileWithSpaces3("bla ");
const QString fileWithSpaces4("A/ foo");
const QString fileWithSpaces5("A/ bar ");
const QString fileWithSpaces6("A/bla ");
fakeFolder.localModifier().insert(fileWithSpaces1);
fakeFolder.localModifier().insert(fileWithSpaces2);
fakeFolder.localModifier().insert(fileWithSpaces3);
fakeFolder.localModifier().insert(fileWithSpaces4);
fakeFolder.localModifier().insert(fileWithSpaces5);
fakeFolder.localModifier().insert(fileWithSpaces6);
QVERIFY(fakeFolder.syncOnce());
@@ -233,6 +239,21 @@ private slots:
QVERIFY(!fakeFolder.currentRemoteState().find(fileWithSpaces3));
QVERIFY(fakeFolder.currentLocalState().find(fileWithSpaces3.trimmed()));
QVERIFY(!fakeFolder.currentLocalState().find(fileWithSpaces3));
QVERIFY(fakeFolder.currentRemoteState().find("A/foo"));
QVERIFY(!fakeFolder.currentRemoteState().find(fileWithSpaces4));
QVERIFY(fakeFolder.currentLocalState().find("A/foo"));
QVERIFY(!fakeFolder.currentLocalState().find(fileWithSpaces4));
QVERIFY(fakeFolder.currentRemoteState().find("A/bar"));
QVERIFY(!fakeFolder.currentRemoteState().find(fileWithSpaces5));
QVERIFY(fakeFolder.currentLocalState().find("A/bar"));
QVERIFY(!fakeFolder.currentLocalState().find(fileWithSpaces5));
QVERIFY(fakeFolder.currentRemoteState().find("A/bla"));
QVERIFY(!fakeFolder.currentRemoteState().find(fileWithSpaces6));
QVERIFY(fakeFolder.currentLocalState().find("A/bla"));
QVERIFY(!fakeFolder.currentLocalState().find(fileWithSpaces6));
}
void testCreateFileWithTrailingSpaces_localTrimmedDoesExist_dontRenameAndUploadFile()

View File

@@ -41,6 +41,18 @@ bool itemDidCompleteSuccessfullyWithExpectedRank(const ItemCompletedSpy &spy, co
return false;
}
int itemSuccessfullyCompletedGetRank(const ItemCompletedSpy &spy, const QString &path)
{
auto itItem = std::find_if(spy.begin(), spy.end(), [&path] (auto currentItem) {
auto item = currentItem[0].template value<OCC::SyncFileItemPtr>();
return item->destination() == path;
});
if (itItem != spy.end()) {
return itItem - spy.begin();
}
return -1;
}
class TestSyncEngine : public QObject
{
Q_OBJECT
@@ -92,6 +104,8 @@ private slots:
void testDirUploadWithDelayedAlgorithm() {
FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"bulkupload", "1.0"} } } });
ItemCompletedSpy completeSpy(fakeFolder);
fakeFolder.localModifier().mkdir("Y");
fakeFolder.localModifier().insert("Y/d0");
@@ -104,12 +118,18 @@ private slots:
fakeFolder.syncOnce();
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Y", 0));
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Z", 1));
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Y/d0", 2));
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "Z/d0", 3));
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "A/a0", 4));
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "B/b0", 5));
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "r0", 6));
QVERIFY(itemDidCompleteSuccessfullyWithExpectedRank(completeSpy, "r1", 7));
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Y/d0"));
QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "Y/d0") > 1);
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z/d0"));
QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "Z/d0") > 1);
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0"));
QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "A/a0") > 1);
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "B/b0"));
QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "B/b0") > 1);
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "r0"));
QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "r0") > 1);
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "r1"));
QVERIFY(itemSuccessfullyCompletedGetRank(completeSpy, "r1") > 1);
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
@@ -490,7 +510,9 @@ private slots:
int remoteQuota = 1000;
int n507 = 0, nPUT = 0;
QObject parent;
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
Q_UNUSED(outgoingData)
if (op == QNetworkAccessManager::PutOperation) {
nPUT++;
if (request.rawHeader("OC-Total-Length").toInt() > remoteQuota) {
@@ -776,6 +798,95 @@ private slots:
QCOMPARE(QFileInfo(fakeFolder.localPath() + "foo").lastModified(), datetime);
}
/**
* Checks whether subsequent large uploads are skipped after a 507 error
*/
void testErrorsWithBulkUpload()
{
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"bulkupload", "1.0"} } } });
// Disable parallel uploads
SyncOptions syncOptions;
syncOptions._parallelNetworkJobs = 0;
fakeFolder.syncEngine().setSyncOptions(syncOptions);
int nPUT = 0;
int nPOST = 0;
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
auto contentType = request.header(QNetworkRequest::ContentTypeHeader).toString();
if (op == QNetworkAccessManager::PostOperation) {
++nPOST;
if (contentType.startsWith(QStringLiteral("multipart/related; boundary="))) {
auto jsonReplyObject = fakeFolder.forEachReplyPart(outgoingData, contentType, [] (const QMap<QString, QByteArray> &allHeaders) -> QJsonObject {
auto reply = QJsonObject{};
const auto fileName = allHeaders[QStringLiteral("X-File-Path")];
if (fileName.endsWith("A/big2") ||
fileName.endsWith("A/big3") ||
fileName.endsWith("A/big4") ||
fileName.endsWith("A/big5") ||
fileName.endsWith("A/big7") ||
fileName.endsWith("B/big8")) {
reply.insert(QStringLiteral("error"), true);
reply.insert(QStringLiteral("etag"), {});
return reply;
} else {
reply.insert(QStringLiteral("error"), false);
reply.insert(QStringLiteral("etag"), {});
}
return reply;
});
if (jsonReplyObject.size()) {
auto jsonReply = QJsonDocument{};
jsonReply.setObject(jsonReplyObject);
return new FakeJsonErrorReply{op, request, this, 200, jsonReply};
}
return nullptr;
}
} else if (op == QNetworkAccessManager::PutOperation) {
++nPUT;
const auto fileName = getFilePathFromUrl(request.url());
if (fileName.endsWith("A/big2") ||
fileName.endsWith("A/big3") ||
fileName.endsWith("A/big4") ||
fileName.endsWith("A/big5") ||
fileName.endsWith("A/big7") ||
fileName.endsWith("B/big8")) {
return new FakeErrorReply(op, request, this, 412);
}
return nullptr;
}
return nullptr;
});
fakeFolder.localModifier().insert("A/big", 1);
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(nPUT, 0);
QCOMPARE(nPOST, 1);
nPUT = 0;
nPOST = 0;
fakeFolder.localModifier().insert("A/big1", 1); // ok
fakeFolder.localModifier().insert("A/big2", 1); // ko
fakeFolder.localModifier().insert("A/big3", 1); // ko
fakeFolder.localModifier().insert("A/big4", 1); // ko
fakeFolder.localModifier().insert("A/big5", 1); // ko
fakeFolder.localModifier().insert("A/big6", 1); // ok
fakeFolder.localModifier().insert("A/big7", 1); // ko
fakeFolder.localModifier().insert("A/big8", 1); // ok
fakeFolder.localModifier().insert("B/big8", 1); // ko
QVERIFY(!fakeFolder.syncOnce());
QCOMPARE(nPUT, 0);
QCOMPARE(nPOST, 1);
nPUT = 0;
nPOST = 0;
QVERIFY(!fakeFolder.syncOnce());
QCOMPARE(nPUT, 6);
QCOMPARE(nPOST, 0);
}
};
QTEST_GUILESS_MAIN(TestSyncEngine)

View File

@@ -0,0 +1,278 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Ebene_1"
x="0px"
y="0px"
viewBox="0 0 300 300"
style="enable-background:new 0 0 300 300;"
xml:space="preserve"
sodipodi:docname="Windows_Folder.svg"
inkscape:version="1.1.1 (1:1.1+202109281949+c3084ef5ed)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs74">
<linearGradient
id="Pfad_499_00000161620552563728473090000003454445015337385602_"
gradientUnits="userSpaceOnUse"
x1="3.5778999"
y1="299.08331"
x2="3.5778999"
y2="297.29568"
gradientTransform="matrix(72,0,0,-72,49,21673)">
<stop
offset="0"
style="stop-color:#FE319A"
id="stop59" />
<stop
offset="1"
style="stop-color:#E20074"
id="stop61" />
</linearGradient>
<linearGradient
gradientTransform="matrix(0.9909162,0,0,0.99091623,283.08824,126.10294)"
inkscape:collect="always"
xlink:href="#linearGradient857"
id="linearGradient1192"
gradientUnits="userSpaceOnUse"
x1="18.230097"
y1="150"
x2="150.00002"
y2="-7.6293945e-06" /><linearGradient
inkscape:collect="always"
id="linearGradient857"><stop
style="stop-color:#0082c9;stop-opacity:1;"
offset="0"
id="stop853" /><stop
style="stop-color:#1cafff;stop-opacity:1"
offset="1"
id="stop855" /></linearGradient></defs><sodipodi:namedview
id="namedview72"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="1.9233304"
inkscape:cx="123.48372"
inkscape:cy="174.17704"
inkscape:window-width="1920"
inkscape:window-height="1046"
inkscape:window-x="0"
inkscape:window-y="34"
inkscape:window-maximized="1"
inkscape:current-layer="Ebene_1" />
<style
type="text/css"
id="style2">
.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_00000073698049827524260440000006626317865822427277_);}
.st2{fill:url(#SVGID_00000058549202148666045310000006535695705354523308_);}
.st3{fill:url(#SVGID_00000135661076229880923210000014324535628046543800_);}
.st4{fill:url(#SVGID_00000090993352524005697930000005123350809455339168_);}
.st5{fill:#FDE07F;}
.st6{fill:#FFEEB6;}
.st7{clip-path:url(#SVGID_00000027581996385575992730000014613207578918336175_);}
.st8{fill:url(#Pfad_499_00000154407742523961947240000000935762443450757262_);}
.st9{fill:#FFFFFF;}
.st10{fill:#F2F2F2;}
</style>
<g
id="g51">
<linearGradient
id="SVGID_1_"
gradientUnits="userSpaceOnUse"
x1="224.238"
y1="251.2621"
x2="235.5521"
y2="229.8477">
<stop
offset="0"
style="stop-color:#E5E5E5"
id="stop4" />
<stop
offset="1"
style="stop-color:#F2F2F2;stop-opacity:0"
id="stop6" />
</linearGradient>
<polygon
class="st0"
points="221,245 251,236 221,222 "
id="polygon9" />
<linearGradient
id="SVGID_00000168800218275837730680000003462325949068373151_"
gradientUnits="userSpaceOnUse"
x1="108.238"
y1="292.2621"
x2="119.5521"
y2="270.8477">
<stop
offset="0"
style="stop-color:#E5E5E5"
id="stop11" />
<stop
offset="1"
style="stop-color:#F2F2F2;stop-opacity:0"
id="stop13" />
</linearGradient>
<polygon
style="fill:url(#SVGID_00000168800218275837730680000003462325949068373151_);"
points="105,286 135,277 105,263 "
id="polygon16" />
<linearGradient
id="SVGID_00000003802560936691988940000016049607085827979708_"
gradientUnits="userSpaceOnUse"
x1="51.9582"
y1="185.0925"
x2="200.6586"
y2="104.7857">
<stop
offset="0.3193"
style="stop-color:#EBC863"
id="stop18" />
<stop
offset="1"
style="stop-color:#FEE182"
id="stop20" />
</linearGradient>
<path
style="fill:url(#SVGID_00000003802560936691988940000016049607085827979708_);"
d="M219,246H56c-1.7,0-3-1.3-3-3V33h151 c1.7,0,3,1.3,3,3v125.2c0,0.8,0.3,1.5,0.9,2.1l13.3,13.6c0.5,0.6,0.9,1.3,0.9,2.1V243C222,244.7,220.7,246,219,246z"
id="path23" />
<g
id="g39">
<linearGradient
id="SVGID_00000088830071281327557650000012927193665735863469_"
gradientUnits="userSpaceOnUse"
x1="81.921"
y1="116.6131"
x2="83.1607"
y2="222.7924">
<stop
offset="0"
style="stop-color:#FFEFBC"
id="stop25" />
<stop
offset="1"
style="stop-color:#FFE495"
id="stop27" />
</linearGradient>
<path
style="fill:url(#SVGID_00000088830071281327557650000012927193665735863469_);"
d="M103.5,286L54,246V33l55.9,44.1 c0.7,0.6,1.1,1.4,1.1,2.4V211l-5,7v66.9C106,286.1,104.5,286.8,103.5,286z"
id="path30" />
<linearGradient
id="SVGID_00000075125985038499810010000013334681634146325900_"
gradientUnits="userSpaceOnUse"
x1="58.9558"
y1="126.3097"
x2="117.1869"
y2="218.9498">
<stop
offset="0"
style="stop-color:#FFEAA5"
id="stop32" />
<stop
offset="1"
style="stop-color:#FFE086"
id="stop34" />
</linearGradient>
<path
style="fill:url(#SVGID_00000075125985038499810010000013334681634146325900_);"
d="M102.5,285.7l-49.5-40V33l55.8,44 c0.7,0.6,1.1,1.4,1.1,2.4v131.4l-5,7v66.8C104.9,285.8,103.5,286.5,102.5,285.7z"
id="path37" />
</g>
<g
id="g49">
<rect
x="60"
y="46"
class="st5"
width="1"
height="200"
id="rect41" />
<rect
x="56.5"
y="46"
class="st5"
width="1"
height="200"
id="rect43" />
<rect
x="57.5"
y="46"
class="st6"
width="1"
height="200"
id="rect45" />
<rect
x="61"
y="46"
class="st6"
width="1"
height="200"
id="rect47" />
</g>
</g>
<g
id="g69"
transform="translate(0,-100)">
<defs
id="defs54">
<rect
id="SVGID_00000093176395783954265780000012512996052019405743_"
x="122.3"
y="139"
width="128.7"
height="128.7" />
</defs>
<clipPath
id="SVGID_00000106127748250350940740000017471206982484828042_">
<use
xlink:href="#SVGID_00000093176395783954265780000012512996052019405743_"
style="overflow:visible"
id="use56"
x="0"
y="0"
width="100%"
height="100%" />
</clipPath>
<g
id="g925"
transform="matrix(0.81518987,0,0,0.81518987,-44.170655,200.60216)"><circle
r="79"
cy="126.10294"
cx="283.08823"
id="circle1050"
style="fill:url(#linearGradient1192);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.0990916;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-filename="nextcloud-icon.png"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300" /><path
inkscape:connector-curvature="0"
id="path1052"
d="m 283.18965,95.813064 c -13.79313,0 -25.48394,9.350826 -29.10731,22.020146 -3.14909,-6.72 -9.97328,-11.42792 -17.83356,-11.42792 -10.80973,0 -19.69562,8.88589 -19.69562,19.69562 0,10.80972 8.88589,19.69967 19.69562,19.69967 7.86028,0 14.68447,-4.7108 17.83356,-11.43198 3.62337,12.67028 15.31418,22.02422 29.10731,22.02422 13.69111,0 25.32232,-9.21286 29.03837,-21.74023 3.20726,6.56859 9.94698,11.14799 17.69562,11.14799 10.80974,0 19.69967,-8.88995 19.69967,-19.69967 0,-10.80973 -8.88993,-19.69562 -19.69967,-19.69562 -7.74864,0 -14.48836,4.57653 -17.69562,11.14395 -3.71605,-12.5264 -15.34726,-21.736176 -29.03837,-21.736176 z m 0,11.561796 c 10.41225,0 18.73011,8.31383 18.73011,18.72605 0,10.41221 -8.31786,18.73011 -18.73011,18.73011 -10.41219,0 -18.72603,-8.3179 -18.72603,-18.73011 0,-10.41222 8.31384,-18.72605 18.72603,-18.72605 z m -46.94087,10.59222 c 4.56183,0 8.13788,3.57198 8.13788,8.13383 0,4.56184 -3.57605,8.13787 -8.13788,8.13787 -4.56183,0 -8.13385,-3.57603 -8.13385,-8.13787 0,-4.56185 3.57202,-8.13383 8.13385,-8.13383 z m 93.67486,0 c 4.56187,0 8.1379,3.57198 8.1379,8.13383 0,4.56184 -3.57605,8.13787 -8.1379,8.13787 -4.56181,0 -8.13381,-3.57603 -8.13381,-8.13787 0,-4.56185 3.57201,-8.13383 8.13381,-8.13383 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6.50314;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
inkscape:export-filename="Nextcloud Hub logo variants.png"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300" /></g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

File diff suppressed because it is too large Load Diff

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