mirror of https://github.com/nextcloud/desktop
Compare commits
986 Commits
Author | SHA1 | Date |
---|---|---|
allexzander | f1c37d7671 | |
Matthieu Gallien | f15810ae0d | |
Matthieu Gallien | df7527c310 | |
Nextcloud bot | 064e134569 | |
Claudio Cambra | 22f2fe1218 | |
Claudio Cambra | 57ced98f98 | |
Matthieu Gallien | a06fe10f65 | |
Matthieu Gallien | 9ed87bf2ad | |
Matthieu Gallien | d228ec445f | |
Matthieu Gallien | 3626e4c0c0 | |
Matthieu Gallien | 36c2c618ec | |
Matthieu Gallien | 3342a13f44 | |
Camila Ayres | 6a6138c6b4 | |
Camila Ayres | 011dd6c4cb | |
Matthieu Gallien | 42ed8b454c | |
Matthieu Gallien | a3083f8be4 | |
Matthieu Gallien | e2ed718030 | |
Matthieu Gallien | 7c9f652c3e | |
Matthieu Gallien | 91bea6f305 | |
Matthieu Gallien | a1efabc3b6 | |
Matthieu Gallien | 098f4ef164 | |
Matthieu Gallien | 7cde0b16e1 | |
Matthieu Gallien | 96d1fc0720 | |
Claudio Cambra | df93608477 | |
Matthieu Gallien | d4986e15f8 | |
Matthieu Gallien | aa38a0180d | |
Matthieu Gallien | 1b9fbb03bb | |
Matthieu Gallien | ba00c50022 | |
Claudio Cambra | 62661b7a0b | |
Claudio Cambra | 9eb8645b95 | |
Nextcloud bot | 5e70aa3634 | |
Claudio Cambra | c8d756ee65 | |
Camila Ayres | 71098127fc | |
Camila Ayres | 88ad7e3b02 | |
Camila Ayres | 4fe7cdd5b0 | |
Camila Ayres | 63fdce3fbe | |
Camila Ayres | 6d3335bd60 | |
Camila Ayres | 7d575d9bf0 | |
Claudio Cambra | 3dd3082a66 | |
dependabot[bot] | a6d48ef5db | |
Nextcloud bot | 52e6b8216f | |
Claudio Cambra | 390d6a58a4 | |
Claudio Cambra | 4bd0974696 | |
Claudio Cambra | afbd2ebc02 | |
allexzander | 7a49312ddd | |
Nextcloud bot | 7bd2b8aeff | |
alex-z | 04c0125bd1 | |
alex-z | c7591f6332 | |
Nextcloud bot | cf888027df | |
alex-z | 00022a3f9c | |
Nextcloud bot | 0d7ab96330 | |
Matthieu Gallien | 73c2f17c59 | |
Matthieu Gallien | d0b4af6ccc | |
Nextcloud bot | 75d0e9aebe | |
Nextcloud bot | 593d133a20 | |
Nextcloud bot | 8cbb7c3cd3 | |
Matthieu Gallien | 5ee1afe3dd | |
Matthieu Gallien | 64d54a17b6 | |
Matthieu Gallien | b58723fe6f | |
Matthieu Gallien | 2ba242527c | |
Nextcloud bot | f4d9fbc2e7 | |
Claudio Cambra | 6a6d92d89b | |
Matthieu Gallien | 7262d11f47 | |
Claudio Cambra | 95c7debfb5 | |
rakekniven | 9ec5712b21 | |
Nextcloud bot | 06c4ea8e25 | |
Matthieu Gallien | 810d8f15ee | |
Matthieu Gallien | 3770eec050 | |
Matthieu Gallien | 07c2554eb3 | |
Matthieu Gallien | 347285b5b3 | |
Matthieu Gallien | 5bed41a670 | |
Matthieu Gallien | 794db304f9 | |
Matthieu Gallien | 2d5753c17d | |
Matthieu Gallien | a357570633 | |
Matthieu Gallien | bebb8e1954 | |
Matthieu Gallien | 20db6b6d86 | |
Matthieu Gallien | 1533670e77 | |
Matthieu Gallien | a05ac621bf | |
Matthieu Gallien | 9b9ff4f471 | |
Matthieu Gallien | 3099628770 | |
Matthieu Gallien | 82a0c1d054 | |
Matthieu Gallien | 7d132029a2 | |
Matthieu Gallien | 060181f83d | |
Matthieu Gallien | aae9e84438 | |
tobiasKaminsky | 8ad2a82ea9 | |
Matthieu Gallien | 4f178fa9be | |
Matthieu Gallien | 3ad7ac922a | |
Matthieu Gallien | 910d7f03e8 | |
Matthieu Gallien | 7432fb4980 | |
Matthieu Gallien | 430d56e72e | |
Matthieu Gallien | ced6d3274c | |
Matthieu Gallien | 332f069491 | |
Matthieu Gallien | b7bba50672 | |
Matthieu Gallien | 44f6d514ff | |
Matthieu Gallien | 032af80d4d | |
Matthieu Gallien | 09e60744c2 | |
Matthieu Gallien | fa766c02ac | |
Matthieu Gallien | 670b2ce42f | |
Matthieu Gallien | 274d866c19 | |
Matthieu Gallien | ced85ac287 | |
Matthieu Gallien | e3456847d8 | |
Claudio Cambra | 70931fb0af | |
Claudio Cambra | 5d765dd017 | |
Claudio Cambra | 3765df627b | |
Claudio Cambra | 3597766fb0 | |
Claudio Cambra | 52758a00b8 | |
Claudio Cambra | 5087d5142a | |
Claudio Cambra | 7e62368eb2 | |
Claudio Cambra | c31e65c111 | |
Claudio Cambra | a0e90cf56b | |
Claudio Cambra | a8e7e340aa | |
Claudio Cambra | aaea45110f | |
Claudio Cambra | 7a17a51a25 | |
Claudio Cambra | 7e1448bcf2 | |
Claudio Cambra | 84414ce5dc | |
Claudio Cambra | 6210490109 | |
Claudio Cambra | 1a2db488ab | |
Claudio Cambra | 7954695783 | |
Claudio Cambra | ac1206a0c1 | |
Claudio Cambra | 366f5f0303 | |
Claudio Cambra | 6a497cf21c | |
Claudio Cambra | 4cad9ebdac | |
Claudio Cambra | 9932956686 | |
Claudio Cambra | 29b0d2b8ad | |
Claudio Cambra | 79a150baf4 | |
Claudio Cambra | fe7c00a7bf | |
Matthieu Gallien | 4566400ee6 | |
Matthieu Gallien | aa76de9b68 | |
Matthieu Gallien | d3442d137a | |
Matthieu Gallien | 1f0279e1c1 | |
Matthieu Gallien | b63c88e492 | |
Matthieu Gallien | ee245f26c8 | |
Matthieu Gallien | 249e0eb87d | |
Matthieu Gallien | 20ee506b71 | |
Matthieu Gallien | d035c26be5 | |
Matthieu Gallien | b712108229 | |
Matthieu Gallien | 80b25d36fa | |
Matthieu Gallien | 21464063b6 | |
Nextcloud bot | aa175036e9 | |
allexzander | 7e801d6c9a | |
Matthieu Gallien | 59fc619c35 | |
alex-z | 2ea6d3fd38 | |
Nextcloud bot | 98e1d71f70 | |
alex-z | 9fac497e6e | |
alex-z | fc31ac4a1f | |
Matthieu Gallien | caa4d8943c | |
alex-z | 47a605c654 | |
Nextcloud bot | e18ab96882 | |
Camila Ayres | d415b08101 | |
Camila Ayres | 9f968f6821 | |
Claudio Cambra | ef08c5eb4e | |
Claudio Cambra | ef2423da53 | |
Claudio Cambra | 2323b843f0 | |
Claudio Cambra | 64b7282bd4 | |
Claudio Cambra | d3aa7f8f51 | |
Claudio Cambra | ccf6b5abe1 | |
Claudio Cambra | d497e265df | |
Claudio Cambra | 92f6de9ca9 | |
Claudio Cambra | 1c84b832fe | |
Claudio Cambra | 591d5eebd2 | |
Claudio Cambra | 8f6c19e029 | |
Matthieu Gallien | 510b3edc3c | |
Matthieu Gallien | 2141ccdb7d | |
Matthieu Gallien | 5450552339 | |
Matthieu Gallien | 1522d01d5b | |
Claudio Cambra | 03fe6494e4 | |
Claudio Cambra | 80afd8e737 | |
Matthieu Gallien | c0af76ca5f | |
Matthieu Gallien | 06c2fecbe1 | |
Matthieu Gallien | 23772f18e0 | |
Matthieu Gallien | cd924de9d3 | |
Matthieu Gallien | dd8a16f9d6 | |
allexzander | f40b8ae198 | |
alex-z | b52906a8a6 | |
alex-z | f490989a1a | |
alex-z | 9ae60258e1 | |
alex-z | 57f6c7cda2 | |
alex-z | d2bfb59d6a | |
alex-z | d240ed9d50 | |
Nextcloud bot | f4acb5099f | |
Matthieu Gallien | b7dd6ff748 | |
Thomas Witt | a5a31321f8 | |
Thomas Witt | d0097ce25c | |
Thomas Witt | 8a9de185a9 | |
Nextcloud bot | 1cb798b6c7 | |
Nextcloud bot | 0aea31d877 | |
Nextcloud bot | 0203b5423f | |
Claudio Cambra | fcb5380437 | |
Claudio Cambra | b80afca177 | |
Claudio Cambra | 59928a6c33 | |
Claudio Cambra | 96f1ba656f | |
Claudio Cambra | 19cf69ccd3 | |
Claudio Cambra | ac1b11708f | |
Claudio Cambra | a899fe19fa | |
dependabot[bot] | 75020c03ce | |
Claudio Cambra | aeca31af97 | |
Claudio Cambra | f0f995c260 | |
Claudio Cambra | dff6428a75 | |
Claudio Cambra | 77f9096538 | |
Claudio Cambra | 5977a7c92d | |
Claudio Cambra | 68370ade88 | |
Claudio Cambra | 6a64248ff6 | |
Claudio Cambra | 33e2c084a5 | |
Claudio Cambra | 7664509e22 | |
Claudio Cambra | 9e7ce1640d | |
Claudio Cambra | c082c446c1 | |
Claudio Cambra | dab28f20f4 | |
Claudio Cambra | 485b07a805 | |
Claudio Cambra | c9a131736a | |
Claudio Cambra | 2373cd4dde | |
Claudio Cambra | 3ec18ba1a6 | |
Claudio Cambra | 788fd7f363 | |
Claudio Cambra | 6200cab957 | |
Claudio Cambra | d74d23cedb | |
Claudio Cambra | 1f78b9f685 | |
Claudio Cambra | e8d1afa3df | |
Claudio Cambra | 913d724254 | |
Claudio Cambra | 2c0688f82b | |
Claudio Cambra | 5a774756b6 | |
Claudio Cambra | b9483f0c55 | |
Claudio Cambra | e7616e0e54 | |
Claudio Cambra | 3c3e3aa353 | |
Claudio Cambra | 3066f58673 | |
Claudio Cambra | 2caa43a76d | |
Claudio Cambra | af9a271662 | |
Claudio Cambra | 3afa861f91 | |
Claudio Cambra | d066536de0 | |
Claudio Cambra | 206d7cf3f4 | |
Claudio Cambra | 5e80827c1f | |
Claudio Cambra | 1beb04371c | |
Claudio Cambra | 3bdb1ca1cb | |
Claudio Cambra | 5070c370a6 | |
Claudio Cambra | beedbbb471 | |
Claudio Cambra | cfda22c107 | |
Claudio Cambra | 1285b02770 | |
Claudio Cambra | af2b2e7aa1 | |
Claudio Cambra | 395cf9649c | |
Claudio Cambra | 4464f7e460 | |
Claudio Cambra | 66f77233a8 | |
Claudio Cambra | db20e44850 | |
Claudio Cambra | 07b6391688 | |
Claudio Cambra | 798e060032 | |
Claudio Cambra | c91d5827ab | |
Claudio Cambra | ad43c13882 | |
Claudio Cambra | 3012976d9b | |
Claudio Cambra | 74cea5e57a | |
Claudio Cambra | d6e67a1882 | |
Claudio Cambra | ca94b452f1 | |
Claudio Cambra | e54652b690 | |
Claudio Cambra | 1f3d636a92 | |
Claudio Cambra | bab3b4181c | |
Claudio Cambra | d2242ea9a4 | |
Claudio Cambra | 807371e1b7 | |
Claudio Cambra | 9621dede1f | |
Claudio Cambra | 65db197a62 | |
Claudio Cambra | 4d3e63009c | |
Claudio Cambra | f7dba3e4c6 | |
Claudio Cambra | 3e7cde632e | |
Claudio Cambra | 49303045b7 | |
Claudio Cambra | 7be5541cde | |
Claudio Cambra | 3bb6f43bdf | |
Claudio Cambra | ce1bf89a99 | |
Claudio Cambra | b7541fc783 | |
Claudio Cambra | b074d19e6e | |
Claudio Cambra | 3bfbb38e0f | |
Claudio Cambra | cfd8c00e94 | |
Claudio Cambra | 8039fcd951 | |
Claudio Cambra | 3ffcd6de42 | |
Claudio Cambra | 1e811fead5 | |
Claudio Cambra | 15651f6a0c | |
Claudio Cambra | 2d0cdb3716 | |
Claudio Cambra | b23706633e | |
Claudio Cambra | f1adfcf8b7 | |
Claudio Cambra | 4be8bace48 | |
Claudio Cambra | d12cdebf48 | |
Claudio Cambra | d0195e67c3 | |
Claudio Cambra | 31a9db9e25 | |
Claudio Cambra | c5d57cde1f | |
Claudio Cambra | 54f3822b3c | |
Claudio Cambra | 83ed6ea35f | |
Claudio Cambra | a7637257d5 | |
Claudio Cambra | 70cb2bd4c1 | |
Claudio Cambra | 4029458eff | |
Claudio Cambra | 131973b935 | |
Claudio Cambra | e1997bd1fd | |
Claudio Cambra | 0ceb6a9481 | |
Claudio Cambra | 000fe02bb0 | |
Claudio Cambra | 8d3b101569 | |
Claudio Cambra | e25bebcd69 | |
Claudio Cambra | 3a58fbeef6 | |
Claudio Cambra | 15f03d6417 | |
Claudio Cambra | 2085a1214e | |
Claudio Cambra | ff305ec9fc | |
Claudio Cambra | a4551b25c6 | |
Claudio Cambra | 8b285a7ea9 | |
Claudio Cambra | e52d6dfb23 | |
Claudio Cambra | 49450d52f6 | |
Claudio Cambra | 41f229c7c3 | |
Claudio Cambra | 7e3769cc45 | |
Claudio Cambra | 4aaaf5b15b | |
Claudio Cambra | 69dfe596e2 | |
Claudio Cambra | f1d3798396 | |
Claudio Cambra | 24ce9a1e1e | |
Claudio Cambra | 9d6db4fa45 | |
Claudio Cambra | 30b4509457 | |
Claudio Cambra | a7de8edd16 | |
Claudio Cambra | 5702b163c8 | |
Claudio Cambra | 249dd02e75 | |
Claudio Cambra | f1363040fa | |
Claudio Cambra | 08917a9559 | |
Claudio Cambra | e2b62e492f | |
Claudio Cambra | 3ef88137ad | |
Claudio Cambra | 19316f4d3a | |
Claudio Cambra | 422a6c7962 | |
Claudio Cambra | 755897be55 | |
Claudio Cambra | f654a8ca83 | |
Claudio Cambra | 29262345cd | |
Claudio Cambra | 318661748d | |
Claudio Cambra | aa57ba10f3 | |
Claudio Cambra | 5e0d990308 | |
Claudio Cambra | ec145d8ca2 | |
Claudio Cambra | 52dbfc3109 | |
Claudio Cambra | 322ae529b7 | |
Claudio Cambra | 7a40f6f728 | |
Claudio Cambra | 26d635b6fa | |
Claudio Cambra | e113a09ecf | |
Claudio Cambra | 55cdbe860b | |
Claudio Cambra | 13fa2bca1d | |
Claudio Cambra | c74575cd32 | |
Claudio Cambra | 8956fcebe9 | |
Claudio Cambra | 9b1ad4d6d5 | |
Claudio Cambra | d23f30ae4e | |
Claudio Cambra | 1b64e29050 | |
Claudio Cambra | 70a565d60d | |
Claudio Cambra | d3d75e85f6 | |
Claudio Cambra | ecf4efd481 | |
Claudio Cambra | cad56f1e2a | |
Claudio Cambra | da8ae80544 | |
Claudio Cambra | 18862bed35 | |
Claudio Cambra | 30673976be | |
Claudio Cambra | 540eb11bf2 | |
Claudio Cambra | 4f0e25a4e6 | |
Claudio Cambra | 18de0d0b3f | |
Claudio Cambra | 46e2ca5887 | |
Claudio Cambra | 41f93371a2 | |
Claudio Cambra | 930106e7ac | |
Claudio Cambra | 4ba2bc8a3b | |
Claudio Cambra | dda5ec295e | |
Claudio Cambra | b6c247ba8d | |
Claudio Cambra | ee21715860 | |
Claudio Cambra | c2ec72c132 | |
Claudio Cambra | 731a5e1f8f | |
Claudio Cambra | f11807815a | |
Claudio Cambra | ff9e344810 | |
Claudio Cambra | 331a76195b | |
Claudio Cambra | 8add57a048 | |
Claudio Cambra | 765f33b45c | |
Claudio Cambra | d53680e583 | |
Claudio Cambra | 145d92e5cb | |
Claudio Cambra | e6f20e9498 | |
Claudio Cambra | 81dc8ce708 | |
Claudio Cambra | a6992cf38b | |
Claudio Cambra | 259a28e33e | |
Claudio Cambra | 02b5a31eae | |
Claudio Cambra | adb4028a6b | |
Claudio Cambra | 5b1b87b788 | |
Claudio Cambra | 9ce3af3de7 | |
Claudio Cambra | e15db9b938 | |
Claudio Cambra | 46ad6f0bfb | |
Claudio Cambra | 1288448336 | |
Claudio Cambra | 6dad778498 | |
Claudio Cambra | af93122445 | |
Claudio Cambra | 8dc9807eb2 | |
Claudio Cambra | 820c7e4bd1 | |
Claudio Cambra | b30f8c30d7 | |
Claudio Cambra | 90f7e3a2e3 | |
Claudio Cambra | 023e1453ba | |
Claudio Cambra | a586767f00 | |
Claudio Cambra | 07473ac5d9 | |
Claudio Cambra | 0feb5da08f | |
Claudio Cambra | 8982857833 | |
Claudio Cambra | 36a829849b | |
Claudio Cambra | dbe8a5f8f2 | |
Claudio Cambra | 5fbcec1400 | |
Claudio Cambra | 09042d701d | |
Claudio Cambra | 1e34affc01 | |
Claudio Cambra | 9bb67ee68d | |
Claudio Cambra | 7a9ca59734 | |
Claudio Cambra | ce026dfd94 | |
Claudio Cambra | 13aced88ce | |
Claudio Cambra | 05e7f1b992 | |
Claudio Cambra | 45b4fee7ba | |
Claudio Cambra | 76045fd989 | |
Claudio Cambra | f44fec2ff1 | |
Claudio Cambra | f35148b1bf | |
Claudio Cambra | d0baa23b5d | |
Claudio Cambra | 8128697a70 | |
Claudio Cambra | 1522c22576 | |
Claudio Cambra | fe6f47e3b9 | |
Claudio Cambra | 3dcef75c57 | |
Nextcloud bot | 3ab692c26e | |
allexzander | adc7a22491 | |
alex-z | dbde9e3a2b | |
Matthieu Gallien | d6ed67806e | |
Camila Ayres | 63b0a9f94d | |
Camila Ayres | d9d4101ca7 | |
Matthieu Gallien | 3a7ae094d3 | |
Matthieu Gallien | c588fdc049 | |
Matthieu Gallien | cf97b439d0 | |
Matthieu Gallien | ee28ef818f | |
Matthieu Gallien | 49650f2f7a | |
Matthieu Gallien | 36aadaaa18 | |
Matthieu Gallien | a84c72248f | |
Matthieu Gallien | df8115d1be | |
Claudio Cambra | fc9e69e884 | |
Felix Stupp | 04461c758b | |
Claudio Cambra | 406ff17a29 | |
dependabot[bot] | f725851792 | |
Nextcloud bot | 7fb2e975cd | |
Nextcloud bot | 4a5526879a | |
Nextcloud bot | 5d5a92b8d5 | |
Nextcloud bot | e3dc48dcad | |
allexzander | 6cc4c026a5 | |
alex-z | 1145d0613b | |
Nextcloud bot | 45bb243c38 | |
Nextcloud bot | f8477069ff | |
Nextcloud bot | 139ac3f44d | |
Nextcloud bot | 47a414e1e9 | |
Nextcloud bot | f66d3a3396 | |
Claudio Cambra | f13a0fc732 | |
Claudio Cambra | 42504d0a0f | |
Nextcloud bot | 9c5b41abda | |
Matthieu Gallien | 6c03f7600d | |
Matthieu Gallien | 55034f7e43 | |
Matthieu Gallien | b6ff0c5abb | |
Matthieu Gallien | 608a435d38 | |
Matthieu Gallien | 9715cb01a0 | |
Matthieu Gallien | 4e643166af | |
Matthieu Gallien | d81c20b116 | |
Matthieu Gallien | 9585d994e0 | |
Matthieu Gallien | ba78942209 | |
Nextcloud bot | 709deb794c | |
Nextcloud bot | 5c2bd3c193 | |
Nextcloud bot | 206b3179d3 | |
Nextcloud bot | 170896fdda | |
Matthieu Gallien | 38812ce6b7 | |
Matthieu Gallien | 634f007074 | |
Nextcloud bot | 0312a1f398 | |
Nextcloud bot | 54fdf85879 | |
Matthieu Gallien | 23c812e224 | |
Corentin Noël | 6e1e8a8bdc | |
Matthieu Gallien | baa19e485c | |
Josh | f66fb97b30 | |
Matthieu Gallien | e707d685fd | |
Camila Ayres | 84c08419ff | |
Matthieu Gallien | 0da5e4a089 | |
Matthieu Gallien | 4bf1b8604c | |
Matthieu Gallien | 9a8392c1a3 | |
dependabot[bot] | 1af85564ac | |
Nextcloud bot | 39b145f3c8 | |
allexzander | 53eef98033 | |
alex-z | b749cf9748 | |
allexzander | ff65c3c1f8 | |
alex-z | c5dd2e89a1 | |
Matthieu Gallien | 747efd4711 | |
Matthieu Gallien | 2c95775edf | |
allexzander | a5b65d1cb3 | |
alex-z | c1719bc817 | |
Nextcloud bot | 7262696846 | |
allexzander | 82da32e3d7 | |
Matthieu Gallien | 3ea60ee8a9 | |
Matthieu Gallien | 2346ec226d | |
Matthieu Gallien | 8422496bec | |
Matthieu Gallien | e33832fa1d | |
Matthieu Gallien | 6d1f028ed8 | |
alex-z | 4a21b290d2 | |
Nextcloud bot | b45376d579 | |
Nextcloud bot | 462c35aa02 | |
Nextcloud bot | 54a25d8abe | |
allexzander | a399f4dbf6 | |
alex-z | 7376616375 | |
Nextcloud bot | 7a9cafd589 | |
Matthieu Gallien | 61251cde5d | |
Matthieu Gallien | ff9953b36b | |
Matthieu Gallien | 4844f326c1 | |
Matthieu Gallien | bbc976c920 | |
Matthieu Gallien | bf7f87492a | |
Matthieu Gallien | 706d9697d4 | |
Matthieu Gallien | 5dfeb55e0f | |
Matthieu Gallien | b0a2d5ff81 | |
Matthieu Gallien | 969a873a65 | |
Matthieu Gallien | e98c25af8a | |
Matthieu Gallien | 9d6d28971e | |
Matthieu Gallien | ea9c19e7b4 | |
Matthieu Gallien | e607e9d1fb | |
Nextcloud bot | 899b147d59 | |
Camila Ayres | 7fb8eb3b79 | |
Claudio Cambra | f805596418 | |
Claudio Cambra | bfdbecc416 | |
Claudio Cambra | bbf280e12e | |
Claudio Cambra | 8e39d8ef97 | |
Claudio Cambra | 2f6ae0c4dd | |
Camila | dd2ca78884 | |
Nextcloud bot | 0071daad61 | |
Nextcloud bot | c6d4771e26 | |
Nextcloud bot | 799cfc3252 | |
Nextcloud bot | 2584fdc2da | |
Matthieu Gallien | ffffc89414 | |
Kevin Ottens | 526ab056d6 | |
Kevin Ottens | 312da848bc | |
Claudio Cambra | 0e301e75d9 | |
Claudio Cambra | fe53933b04 | |
Claudio Cambra | 5f243a86d4 | |
Claudio Cambra | 19d1971612 | |
Claudio Cambra | 1e96c0f1f9 | |
Claudio Cambra | 69a3f286c0 | |
Claudio Cambra | bf4fc01b39 | |
Claudio Cambra | 9f4e87dc1f | |
Claudio Cambra | 60f116cdd4 | |
Claudio Cambra | 3e5766be47 | |
Nextcloud bot | 2e5aa59757 | |
Matthieu Gallien | 7ed0b9afb3 | |
Kevin Ottens | 38b59a82bf | |
Kevin Ottens | c03837ee0d | |
Kevin Ottens | 6dbe8cc075 | |
Kevin Ottens | d98d2cead5 | |
Kevin Ottens | 17b913c3a7 | |
Kevin Ottens | ea101adbf4 | |
Claudio Cambra | 6845e24e91 | |
Claudio Cambra | d64d959b51 | |
Claudio Cambra | 1a576a15d6 | |
Claudio Cambra | 6e55c84d18 | |
Claudio Cambra | 23ef72c472 | |
Claudio Cambra | 1441a5e3a2 | |
Claudio Cambra | 23c1e554d0 | |
Claudio Cambra | 5eb333cb8d | |
Claudio Cambra | 6db990183e | |
Claudio Cambra | a02efd65f8 | |
Claudio Cambra | db9b9a64b4 | |
Claudio Cambra | 913dcd70ae | |
Claudio Cambra | 90c26c3eff | |
Claudio Cambra | ae524c0346 | |
Claudio Cambra | bbe48954be | |
Claudio Cambra | 8d3b676558 | |
Claudio Cambra | 9a8af68ddc | |
Claudio Cambra | a3e1d66707 | |
Claudio Cambra | 534b3a60d7 | |
Claudio Cambra | f987bcd97a | |
Claudio Cambra | 1fc6014230 | |
Claudio Cambra | cac263f174 | |
Claudio Cambra | 179a368f9f | |
Nextcloud bot | eb86b9141b | |
Matthieu Gallien | 903e313707 | |
Matthieu Gallien | b7c1a95d1c | |
Matthieu Gallien | b35a26091b | |
Matthieu Gallien | 976dbd6df6 | |
Matthieu Gallien | 7a0b03aabc | |
dependabot[bot] | 07d415f348 | |
Matthieu Gallien | 6bd4539e56 | |
alex-z | d29e5bee7a | |
alex-z | 837f9a4913 | |
Matthieu Gallien | b88dfb1dbd | |
alex-z | 17b0dda300 | |
alex-z | 0a4452ef3d | |
Claudio Cambra | 0826df7916 | |
Claudio Cambra | e15252a88c | |
Claudio Cambra | c7c34b9f14 | |
Claudio Cambra | 0daec2071e | |
Claudio Cambra | 1a2afd4576 | |
Claudio Cambra | 2556681997 | |
Claudio Cambra | 93a6da6c19 | |
Claudio Cambra | e7a99a8e9a | |
Claudio Cambra | c070ed2b6d | |
Claudio Cambra | d66827c91f | |
Claudio Cambra | c8c7bdbf40 | |
Claudio Cambra | 4b1f502be7 | |
Nextcloud bot | 4edb8a254e | |
Nextcloud bot | a80ff33fbb | |
Nextcloud bot | ee3734cbb9 | |
Nextcloud bot | 6ff6638171 | |
Nextcloud bot | 7e1f56bb6d | |
allexzander | 53dd8ea5b7 | |
Nextcloud bot | 249fbbc8f7 | |
allexzander | 12e9ebadef | |
alex-z | c0e0b53ee5 | |
allexzander | 38f23827ec | |
alex-z | ccf99125b5 | |
Nextcloud bot | 553be4287e | |
Nextcloud bot | 0cd31941c0 | |
Nextcloud bot | 0d9ebc1184 | |
Nextcloud bot | 224b6cc249 | |
alex-z | 694a5f4242 | |
Nextcloud bot | 11900b7080 | |
allexzander | e5ac6d2935 | |
alex-z | 7160c05033 | |
Nextcloud bot | 67c5479793 | |
allexzander | 36fc73eab2 | |
alex-z | 484426bae2 | |
allexzander | 094947bfb1 | |
alex-z | 74bef928a3 | |
allexzander | 0364cd29c7 | |
alex-z | 8aaa0d4c13 | |
Nextcloud bot | 47d1800404 | |
Nextcloud bot | 373d6d088d | |
Nextcloud bot | 8893569d7f | |
Claudio Cambra | 478d65c17b | |
Claudio Cambra | 92a1d808ef | |
Claudio Cambra | 28cfd8fe70 | |
Claudio Cambra | b4e1338bc8 | |
Claudio Cambra | a428256e4f | |
Claudio Cambra | 81d4aa7446 | |
Claudio Cambra | 1f70c85e22 | |
Claudio Cambra | 918e411b10 | |
Claudio Cambra | 472bba8149 | |
Claudio Cambra | a469d44123 | |
Claudio Cambra | 7d8778d12a | |
Claudio Cambra | 7b99a06eb9 | |
Claudio Cambra | 034822fcc6 | |
Claudio Cambra | ed12541e15 | |
Claudio Cambra | b9ae82ce3e | |
Claudio Cambra | 2f752a6c5c | |
Claudio Cambra | 3060ff6a71 | |
Claudio Cambra | 9e235f99fa | |
Claudio Cambra | f8ebbe8c71 | |
Claudio Cambra | 36001aeff7 | |
Claudio Cambra | 9ca418c922 | |
Claudio Cambra | 9774ac6dde | |
Claudio Cambra | 21a317cff3 | |
Claudio Cambra | 78732bfeb0 | |
Claudio Cambra | 6d9c44526d | |
Claudio Cambra | f881c1d483 | |
Claudio Cambra | d387eb96ab | |
Claudio Cambra | 13b0244ce6 | |
Claudio Cambra | e314fd05e7 | |
Claudio Cambra | 6fa969d8f2 | |
Claudio Cambra | fe6d03a16b | |
Claudio Cambra | 62c85de35b | |
Claudio Cambra | 7a9a17a2f3 | |
Claudio Cambra | 369c1d737a | |
Claudio Cambra | f1c7811962 | |
Claudio Cambra | 1d01f67790 | |
Claudio Cambra | 3b179cc1a1 | |
Claudio Cambra | fd95ab7171 | |
Claudio Cambra | 6a0f76de7e | |
Claudio Cambra | 62bbf6a2ad | |
Claudio Cambra | 9946495edb | |
Claudio Cambra | 571b1ca238 | |
Claudio Cambra | b2e5659c43 | |
Claudio Cambra | 70521e95bc | |
Claudio Cambra | 9733c11f7f | |
Claudio Cambra | 24dda9b437 | |
Claudio Cambra | 292ff9b3bc | |
Claudio Cambra | 0a5df3ba66 | |
Claudio Cambra | 1dcfee4087 | |
Claudio Cambra | 0ddd22ddbb | |
Claudio Cambra | b9cf135080 | |
Claudio Cambra | cfbdf3a773 | |
Claudio Cambra | a204f114d2 | |
Claudio Cambra | 5c3cd69252 | |
Claudio Cambra | f22bf9e527 | |
Claudio Cambra | 643d30a3d5 | |
Claudio Cambra | 84d8561a8e | |
Claudio Cambra | b23e6bc4ee | |
Claudio Cambra | 44a6f4673e | |
Claudio Cambra | b47665efba | |
Claudio Cambra | ba2bef2495 | |
Claudio Cambra | 64e1166066 | |
Claudio Cambra | d905f13d83 | |
Claudio Cambra | c763a9a227 | |
Claudio Cambra | 0b961e9e17 | |
Claudio Cambra | da20e7e179 | |
Claudio Cambra | b3c75750ab | |
Claudio Cambra | 371257a6de | |
Claudio Cambra | f060f0c271 | |
Claudio Cambra | beb889c2f6 | |
Claudio Cambra | e03edc131d | |
Claudio Cambra | ad2baeaba0 | |
Claudio Cambra | f2547140c4 | |
Claudio Cambra | bfaba671f3 | |
Claudio Cambra | 89bb008bd7 | |
Claudio Cambra | d51c2a486b | |
Claudio Cambra | af98e48805 | |
Claudio Cambra | 2ce4e91383 | |
Claudio Cambra | c1a5e788f8 | |
Claudio Cambra | 85b3a135b6 | |
Claudio Cambra | f90f86d696 | |
Claudio Cambra | 726503f3c7 | |
Claudio Cambra | 269fb03351 | |
Claudio Cambra | 389e663219 | |
Claudio Cambra | 134eae63dd | |
Claudio Cambra | e3a0dabb07 | |
Claudio Cambra | a34a390790 | |
Claudio Cambra | 57d1dc84aa | |
Claudio Cambra | fec9902a25 | |
Claudio Cambra | c3490db271 | |
Claudio Cambra | 0b8a2315a0 | |
Claudio Cambra | 02b47b47f8 | |
Claudio Cambra | 679177eae2 | |
Claudio Cambra | f68965a241 | |
Claudio Cambra | 7d229b569f | |
Claudio Cambra | 4e8eab6cd9 | |
Claudio Cambra | 22a521d2a2 | |
Claudio Cambra | 9105a4584e | |
Claudio Cambra | 18ed46ef77 | |
Claudio Cambra | 2105596643 | |
Claudio Cambra | 8fdc69b3cc | |
Claudio Cambra | 435e25d75c | |
Claudio Cambra | b83f0a51a9 | |
Claudio Cambra | 97c963dcab | |
Claudio Cambra | 5aa7138643 | |
Claudio Cambra | e4aeb5b481 | |
Claudio Cambra | 15525bca62 | |
Claudio Cambra | ee6c081603 | |
Claudio Cambra | 1db59fecc1 | |
Claudio Cambra | 0c40ceff9c | |
Claudio Cambra | 58262a8d94 | |
Claudio Cambra | f5b80e9ec1 | |
Claudio Cambra | 5f059a23ce | |
Claudio Cambra | 42b72f9d9a | |
Claudio Cambra | 556bce2672 | |
Claudio Cambra | e9451e9281 | |
Claudio Cambra | a57cb3df9d | |
Claudio Cambra | ba3baa406b | |
Claudio Cambra | 3c26e25a3c | |
Claudio Cambra | ac608a661d | |
Claudio Cambra | 9a880f4199 | |
Claudio Cambra | c69bb4375c | |
Claudio Cambra | 94ce3da9c5 | |
Claudio Cambra | 20f89c6999 | |
Claudio Cambra | 0e02a64c5e | |
Claudio Cambra | d80962b634 | |
Claudio Cambra | 27c803a3d8 | |
Claudio Cambra | 7e4d643ade | |
Claudio Cambra | 2863415428 | |
Claudio Cambra | f607bfaca1 | |
Claudio Cambra | 8070dbd9f6 | |
Claudio Cambra | 174d3ec9e0 | |
Claudio Cambra | d4fb1e7dad | |
Claudio Cambra | c57a5820d0 | |
Claudio Cambra | 5af363ad9c | |
Claudio Cambra | f158e275fc | |
Claudio Cambra | 58e6385093 | |
Claudio Cambra | 346a07643c | |
Claudio Cambra | 4b8338ce91 | |
Claudio Cambra | dae1643542 | |
Claudio Cambra | 40389a2197 | |
Claudio Cambra | a6f15ea700 | |
Claudio Cambra | 8643acfdaa | |
Claudio Cambra | e547ae22b7 | |
Claudio Cambra | 35659bbda2 | |
Claudio Cambra | 8ab52b8683 | |
Claudio Cambra | 3334b4e49c | |
Claudio Cambra | 71119fe65a | |
Claudio Cambra | 9611e47a3b | |
Claudio Cambra | b22f463ad5 | |
Claudio Cambra | 41133e4cd8 | |
Claudio Cambra | a44454daf8 | |
Claudio Cambra | a01e55272a | |
Claudio Cambra | 45b123130f | |
Claudio Cambra | 423891230e | |
Claudio Cambra | 58dc42a521 | |
Claudio Cambra | 798f77fa2b | |
Claudio Cambra | 43b7ae55df | |
Claudio Cambra | 6d4e785ebb | |
Claudio Cambra | be3bd7bb3b | |
Claudio Cambra | 784cd129fb | |
Claudio Cambra | fca6b37804 | |
Claudio Cambra | 0856138b2e | |
Claudio Cambra | c1fa6621ae | |
Claudio Cambra | 94bfa035c3 | |
Claudio Cambra | f728ec1b75 | |
Claudio Cambra | 502d73814e | |
Claudio Cambra | 23a3a3e554 | |
Claudio Cambra | 8a8d3b3ef5 | |
Claudio Cambra | c9997ce5f9 | |
Claudio Cambra | 595c23cf76 | |
Claudio Cambra | 5ed456f30c | |
Claudio Cambra | 6f5b07c1ae | |
Claudio Cambra | 95bebc5214 | |
Claudio Cambra | 5ee2cfa749 | |
Claudio Cambra | a0376a2dab | |
Claudio Cambra | 4a8d50144a | |
Claudio Cambra | 8eaf4e6324 | |
Claudio Cambra | 59b8b8ef21 | |
Claudio Cambra | 7716860bc9 | |
Claudio Cambra | 6cb71694fd | |
Claudio Cambra | eb774adc95 | |
Claudio Cambra | 2d1e5ba197 | |
Claudio Cambra | ee82968c69 | |
Claudio Cambra | 5f6f7f302a | |
Claudio Cambra | a6e3f18168 | |
Claudio Cambra | d4d0cf550e | |
Claudio Cambra | 0b505a9c2c | |
Claudio Cambra | 25201116a0 | |
Claudio Cambra | 9dddaf4f9f | |
Claudio Cambra | e8c1bbb953 | |
Claudio Cambra | 5cc8b6e7c2 | |
Claudio Cambra | ac43369bed | |
Claudio Cambra | 026f082253 | |
Claudio Cambra | dd39991f1c | |
Claudio Cambra | e763a9d29b | |
Claudio Cambra | a2d69fcf7c | |
Claudio Cambra | 22c176af8a | |
Matthieu Gallien | 9e75ca85bf | |
Matthieu Gallien | c254bb01a3 | |
Matthieu Gallien | 17256d3902 | |
allexzander | 334e032d5b | |
alex-z | aab9066654 | |
Nextcloud bot | 68a5fc64ec | |
Nextcloud bot | dd0baaa0d0 | |
Nextcloud bot | 1129cb9595 | |
Nextcloud bot | cfbcebe216 | |
Nextcloud bot | 5511d692ed | |
allexzander | e2410ff4c5 | |
alex-z | b2aca219fc | |
Nextcloud bot | 39611b9728 | |
Matthieu Gallien | 606b1ef137 | |
Matthieu Gallien | 4f2988a7d5 | |
Matthieu Gallien | 997ff6535f | |
Matthieu Gallien | e24da81f49 | |
Matthieu Gallien | 2a5e273963 | |
Matthieu Gallien | 6797a5736a | |
Claudio Cambra | d52ad2d27b | |
Claudio Cambra | 28874ec25c | |
Claudio Cambra | 165bf97b48 | |
Claudio Cambra | 76ff41f3e0 | |
Matthieu Gallien | 909147bd6c | |
Josh Richards | 90cd5b6d30 | |
Nextcloud bot | 37ac0f21ab | |
Nextcloud bot | ec4adb2469 | |
Nextcloud bot | 6d9c601acb | |
Nextcloud bot | 784e794ec3 | |
Matthieu Gallien | 9d78e97988 | |
Camila Ayres | 533ef0b260 | |
Nextcloud bot | 182d5bc8e6 | |
Camila Ayres | 1b305e4c6a | |
Camila Ayres | 9dbf43c6fd | |
Camila Ayres | b3b48dc163 | |
Camila Ayres | 6d50b87587 | |
Nextcloud bot | 252b84cc96 | |
Matthieu Gallien | ead399895d | |
Claudio Cambra | 7ac5f38178 | |
Claudio Cambra | d707ccc5b3 | |
Claudio Cambra | 639c6f9120 | |
Claudio Cambra | 3928573ff4 | |
Matthieu Gallien | f1ed494b31 | |
Matthieu Gallien | 87522bf24c | |
Matthieu Gallien | 1133e3112b | |
dependabot[bot] | 57b60486c4 | |
Matthieu Gallien | a55ff84d62 | |
John Molakvoæ | fcc2d4e62a | |
Matthieu Gallien | b11648e8a6 | |
rakekniven | f6137200fe | |
rakekniven | 0799af8ed3 | |
Matthieu Gallien | 76f19ee0d8 | |
Matthieu Gallien | 51654bcc90 | |
Nextcloud bot | b0183cef6b | |
Nextcloud bot | 25d13a3372 | |
Matthieu Gallien | 6ad34fa942 | |
dependabot[bot] | dd899d1978 | |
Matthieu Gallien | a6e683f7ed | |
alex-z | 5cdeba8061 | |
Matthieu Gallien | 068979a74d | |
Matthieu Gallien | d8a52266b6 | |
Nextcloud bot | 1fef6e7074 | |
allexzander | 89d2ec4f51 | |
alex-z | e9a0dbd16c | |
Matthieu Gallien | 654b5fe38d | |
Matthieu Gallien | b977472aea | |
Claudio Cambra | b1a50f42be | |
Claudio Cambra | 3dc5a6cc9e | |
Claudio Cambra | 6c71c38ca8 | |
Claudio Cambra | bcc1cea9b4 | |
Matthieu Gallien | 120d67cc3e | |
Matthieu Gallien | b8ee9db377 | |
Matthieu Gallien | e02223108a | |
dependabot[bot] | e84616cb34 | |
Matthieu Gallien | 1ad86aad83 | |
dependabot[bot] | 0ece126cad | |
Matthieu Gallien | 604ad3871f | |
István Váradi | 0433bd7f25 | |
Matthieu Gallien | 8cf4dee510 | |
alex-z | af612525c4 | |
Nextcloud bot | 615d592c3e | |
Nextcloud bot | a113fa7828 | |
Matthieu Gallien | 1e7654be14 | |
Camila Ayres | 0ae7695d5c | |
Matthieu Gallien | 81847f9020 | |
Matthieu Gallien | ad14e10fbb | |
Nextcloud bot | de878649c5 | |
Nextcloud bot | a60b339e22 | |
Matthieu Gallien | 9f9c9fc0f3 | |
Camila Ayres | 2089fb168b | |
Camila Ayres | 30883785e4 | |
Camila Ayres | d677664f9d | |
Erik Verbruggen | 07063c9c9d | |
Erik Verbruggen | 31559faa35 | |
Erik Verbruggen | 74c7487314 | |
Erik Verbruggen | 1f046ccd5e | |
Erik Verbruggen | 76cf707934 | |
Erik Verbruggen | fb13eb7203 | |
Erik Verbruggen | a616fbf189 | |
Erik Verbruggen | 8da94b6509 | |
Hannah von Reth | 3762e410f8 | |
Camila | c4095d698a | |
allexzander | 3bcc17b942 | |
Nextcloud bot | 8a02625ea4 | |
Nextcloud bot | 7269efe235 | |
Nextcloud bot | e94db5d6ed | |
Matthieu Gallien | 15a771e9bb | |
Corentin Noël | 2238dab3f9 | |
Matthieu Gallien | 97ac36e614 | |
Micke Nordin | 6ca4ace09f | |
Micke Nordin | 42012a0efb | |
Micke Nordin | 772d44c66d | |
Matthieu Gallien | c4f2537418 | |
Camila | 8f628f808c | |
Camila | a06b88c1b4 | |
Matthieu Gallien | 4b10038dcc | |
dependabot[bot] | cdac0cf90f | |
Matthieu Gallien | 3523875159 | |
Tobias Kaminsky | 71c5327c98 | |
Nextcloud bot | 7da66f0aee | |
allexzander | 8bb36566c7 | |
alex-z | 2eaecb6400 | |
alex-z | 6791cc6365 | |
Nextcloud bot | 049d3e9df3 | |
Nextcloud bot | 416bf8a109 | |
Nextcloud bot | 419b015279 | |
Nextcloud bot | df5094d299 | |
Nextcloud bot | f797d22f04 | |
Nextcloud bot | d45ad02430 | |
Nextcloud bot | c7dd7dc841 | |
Matthieu Gallien | 8e37f434f7 | |
dependabot[bot] | 761bdb09ee | |
Matthieu Gallien | 008afa8061 | |
John Molakvoæ | ae8eb229ec | |
Matthieu Gallien | 053202e881 | |
Matthieu Gallien | 7bff44daa1 | |
Matthieu Gallien | 452cf435fc | |
Nextcloud bot | 4714e01477 | |
Nextcloud bot | 983f960be0 | |
Nextcloud bot | 4f418e807d | |
Nextcloud bot | ee982a10b9 | |
Nextcloud bot | a51719777e | |
Nextcloud bot | 1e4d8891fb | |
Nextcloud bot | 79afa4d856 | |
Matthieu Gallien | 9b269e9f5e | |
Matthieu Gallien | 0b869247ef | |
Matthieu Gallien | 911120caa0 | |
Nextcloud bot | 9f17381652 | |
allexzander | b4fcc94c36 | |
alex-z | 4672acb5a6 | |
alex-z | ed2ad42502 | |
Nextcloud bot | 7f37cf866d | |
allexzander | 64ce74ebae | |
alex-z | 17fbfe4e06 | |
Matthieu Gallien | 2a58a0a034 | |
dependabot[bot] | 3ef3c9a751 | |
Matthieu Gallien | e9c144785b | |
dependabot[bot] | 67edae2621 | |
Matthieu Gallien | 6415ef3687 | |
dependabot[bot] | 416002cdb0 | |
alex-z | 50d168759f | |
alex-z | c30e526bc8 | |
alex-z | 66b0383fee | |
alex-z | 4b0e1b57eb | |
alex-z | 8e38739d94 | |
alex-z | 5b54b13a97 | |
alex-z | 4b7ccf8647 | |
alex-z | 5e78b83ca7 | |
alex-z | ff8db2674a | |
Matthieu Gallien | d3562e6205 | |
Camila | 02e4f05358 | |
Claudio Cambra | c9b4419070 | |
Camila | 3c51c9e1f8 | |
Nextcloud bot | 74a9717e3f | |
Nextcloud bot | baa1e55db4 | |
alex-z | 9298305093 | |
Claudio Cambra | 6b7b655649 | |
Claudio Cambra | edaf49cc9a | |
Matthieu Gallien | 2c5281cea9 | |
Matthieu Gallien | bf78f008bd | |
Matthieu Gallien | b77fc9d4ff | |
Matthieu Gallien | 369296d6ed | |
Matthieu Gallien | 14c03bf76f |
58
.drone.yml
58
.drone.yml
|
@ -1,19 +1,19 @@
|
|||
---
|
||||
kind: pipeline
|
||||
name: qt-5.15
|
||||
name: drone desktop client
|
||||
|
||||
steps:
|
||||
- name: cmake
|
||||
image: ghcr.io/nextcloud/continuous-integration-client:client-5.15-14
|
||||
image: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
|
||||
volumes:
|
||||
- name: build
|
||||
path: /drone/build
|
||||
commands:
|
||||
- cd /drone/build
|
||||
- cmake -G Ninja -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_CXX_COMPILER=g++-11 -DCMAKE_BUILD_TYPE=Debug -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DADD_E2E_TESTS=ON -DECM_ENABLE_SANITIZERS=address -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64 ../src
|
||||
- cmake /drone/src -G Ninja -DCMAKE_PREFIX_PATH=/opt/qt6.6.3 -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_CXX_COMPILER=g++-11 -DCMAKE_BUILD_TYPE=Debug -DQT_MAJOR_VERSION=6 -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64 -DADD_E2E_TESTS=ON
|
||||
|
||||
- name: compile
|
||||
image: ghcr.io/nextcloud/continuous-integration-client:client-5.15-14
|
||||
image: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
|
||||
volumes:
|
||||
- name: build
|
||||
path: /drone/build
|
||||
|
@ -22,25 +22,27 @@ steps:
|
|||
- ninja
|
||||
|
||||
- name: test
|
||||
image: ghcr.io/nextcloud/continuous-integration-client:client-5.15-14
|
||||
image: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
|
||||
volumes:
|
||||
- name: build
|
||||
path: /drone/build
|
||||
commands:
|
||||
- cd /drone/build
|
||||
- ../src/admin/test/wait_for_server.sh "server"
|
||||
- useradd -m -s /bin/bash test
|
||||
- chown -R test:test .
|
||||
- su -c 'ASAN_OPTIONS=detect_odr_violation=0,detect_leaks=0 xvfb-run ctest --output-on-failure' test
|
||||
- su -c 'xvfb-run ctest --output-on-failure' test
|
||||
|
||||
services:
|
||||
- name: server
|
||||
image: ghcr.io/nextcloud/continuous-integration-server:latest # also change in updateScreenshots.sh
|
||||
image: ghcr.io/nextcloud/continuous-integration-shallow-server:latest # also change in updateScreenshots.sh
|
||||
environment:
|
||||
EVAL: true
|
||||
SERVER_VERSION: 'stable24'
|
||||
SERVER_VERSION: 'stable28'
|
||||
commands:
|
||||
- BRANCH="$SERVER_VERSION" /usr/local/bin/initnc.sh
|
||||
- echo 127.0.0.1 server >> /etc/hosts
|
||||
- apt-get update && apt-get install -y composer
|
||||
- su www-data -c "OC_PASS=user1 php /var/www/html/occ user:add --password-from-env --display-name='User One' user1"
|
||||
- su www-data -c "OC_PASS=user2 php /var/www/html/occ user:add --password-from-env --display-name='User Two' user2"
|
||||
- su www-data -c "OC_PASS=user3 php /var/www/html/occ user:add --password-from-env --display-name='User Three' user3"
|
||||
|
@ -48,12 +50,15 @@ services:
|
|||
- su www-data -c "php /var/www/html/occ group:add users"
|
||||
- su www-data -c "php /var/www/html/occ group:adduser users user1"
|
||||
- su www-data -c "php /var/www/html/occ group:adduser users user2"
|
||||
- su www-data -c "git clone --depth=1 -b $SERVER_VERSION https://github.com/nextcloud/activity.git /var/www/html/apps/activity/"
|
||||
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/activity.git /var/www/html/apps/activity/"
|
||||
- su www-data -c "php /var/www/html/occ app:enable activity"
|
||||
- su www-data -c "git clone --depth=1 -b $SERVER_VERSION https://github.com/nextcloud/text.git /var/www/html/apps/text/"
|
||||
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/text.git /var/www/html/apps/text/"
|
||||
- su www-data -c "php /var/www/html/occ app:enable text"
|
||||
- su www-data -c "git clone --depth=1 -b $SERVER_VERSION https://github.com/nextcloud/end_to_end_encryption.git /var/www/html/apps/end_to_end_encryption/"
|
||||
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/end_to_end_encryption.git /var/www/html/apps/end_to_end_encryption/"
|
||||
- su www-data -c "php /var/www/html/occ app:enable end_to_end_encryption"
|
||||
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/photos.git /var/www/html/apps/photos/"
|
||||
- su www-data -c "cd /var/www/html/apps/photos; composer install"
|
||||
- su www-data -c "php /var/www/html/occ app:enable -f photos"
|
||||
- /usr/local/bin/run.sh
|
||||
|
||||
volumes:
|
||||
|
@ -74,15 +79,15 @@ name: qt-5.15-clang
|
|||
|
||||
steps:
|
||||
- name: cmake
|
||||
image: ghcr.io/nextcloud/continuous-integration-client:client-5.15-14
|
||||
image: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
|
||||
volumes:
|
||||
- name: build
|
||||
path: /drone/build
|
||||
commands:
|
||||
- cd /drone/build
|
||||
- cmake -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_C_COMPILER=clang-14 -DCMAKE_CXX_COMPILER=clang++-14 -DCMAKE_BUILD_TYPE=Debug -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DADD_E2E_TESTS=ON -DECM_ENABLE_SANITIZERS=address -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64 ../src
|
||||
- cmake /drone/src -G Ninja -DCMAKE_PREFIX_PATH=/opt/qt6.6.3 -DCMAKE_C_COMPILER=clang-14 -DCMAKE_CXX_COMPILER=clang++-14 -DCMAKE_BUILD_TYPE=Debug -DQT_MAJOR_VERSION=6 -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64 -DADD_E2E_TESTS=ON
|
||||
- name: compile
|
||||
image: ghcr.io/nextcloud/continuous-integration-client:client-5.15-14
|
||||
image: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
|
||||
volumes:
|
||||
- name: build
|
||||
path: /drone/build
|
||||
|
@ -90,25 +95,27 @@ steps:
|
|||
- cd /drone/build
|
||||
- ninja
|
||||
- name: test
|
||||
image: ghcr.io/nextcloud/continuous-integration-client:client-5.15-14
|
||||
image: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
|
||||
volumes:
|
||||
- name: build
|
||||
path: /drone/build
|
||||
commands:
|
||||
- cd /drone/build
|
||||
- ../src/admin/test/wait_for_server.sh "server"
|
||||
- useradd -m -s /bin/bash test
|
||||
- chown -R test:test .
|
||||
- su -c 'ASAN_OPTIONS=detect_odr_violation=0,detect_leaks=0 xvfb-run ctest --output-on-failure' test
|
||||
- su -c 'xvfb-run ctest --output-on-failure' test
|
||||
|
||||
services:
|
||||
- name: server
|
||||
image: ghcr.io/nextcloud/continuous-integration-server:latest # also change in updateScreenshots.sh
|
||||
image: ghcr.io/nextcloud/continuous-integration-shallow-server:latest # also change in updateScreenshots.sh
|
||||
environment:
|
||||
EVAL: true
|
||||
SERVER_VERSION: 'stable24'
|
||||
SERVER_VERSION: 'stable28'
|
||||
commands:
|
||||
- BRANCH="$SERVER_VERSION" /usr/local/bin/initnc.sh
|
||||
- echo 127.0.0.1 server >> /etc/hosts
|
||||
- apt-get update && apt-get install -y composer
|
||||
- su www-data -c "OC_PASS=user1 php /var/www/html/occ user:add --password-from-env --display-name='User One' user1"
|
||||
- su www-data -c "OC_PASS=user2 php /var/www/html/occ user:add --password-from-env --display-name='User Two' user2"
|
||||
- su www-data -c "OC_PASS=user3 php /var/www/html/occ user:add --password-from-env --display-name='User Three' user3"
|
||||
|
@ -116,12 +123,15 @@ services:
|
|||
- su www-data -c "php /var/www/html/occ group:add users"
|
||||
- su www-data -c "php /var/www/html/occ group:adduser users user1"
|
||||
- su www-data -c "php /var/www/html/occ group:adduser users user2"
|
||||
- su www-data -c "git clone --depth=1 -b $SERVER_VERSION https://github.com/nextcloud/activity.git /var/www/html/apps/activity/"
|
||||
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/activity.git /var/www/html/apps/activity/"
|
||||
- su www-data -c "php /var/www/html/occ app:enable activity"
|
||||
- su www-data -c "git clone --depth=1 -b $SERVER_VERSION https://github.com/nextcloud/text.git /var/www/html/apps/text/"
|
||||
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/text.git /var/www/html/apps/text/"
|
||||
- su www-data -c "php /var/www/html/occ app:enable text"
|
||||
- su www-data -c "git clone --depth=1 -b $SERVER_VERSION https://github.com/nextcloud/end_to_end_encryption.git /var/www/html/apps/end_to_end_encryption/"
|
||||
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/end_to_end_encryption.git /var/www/html/apps/end_to_end_encryption/"
|
||||
- su www-data -c "php /var/www/html/occ app:enable end_to_end_encryption"
|
||||
- su www-data -c "git clone -b $SERVER_VERSION https://github.com/nextcloud/photos.git /var/www/html/apps/photos/"
|
||||
- su www-data -c "cd /var/www/html/apps/photos; composer install"
|
||||
- su www-data -c "php /var/www/html/occ app:enable -f photos"
|
||||
- /usr/local/bin/run.sh
|
||||
|
||||
volumes:
|
||||
|
@ -142,14 +152,14 @@ name: AppImage
|
|||
|
||||
steps:
|
||||
- name: build
|
||||
image: ghcr.io/nextcloud/continuous-integration-client-appimage:client-appimage-9
|
||||
image: ghcr.io/nextcloud/continuous-integration-client-appimage-qt6:client-appimage-6.6.3-2
|
||||
environment:
|
||||
CI_UPLOAD_GIT_TOKEN:
|
||||
from_secret: CI_UPLOAD_GIT_TOKEN
|
||||
CI_UPLOAD_GIT_USERNAME:
|
||||
from_secret: CI_UPLOAD_GIT_USERNAME
|
||||
commands:
|
||||
- BUILDNR=$DRONE_BUILD_NUMBER VERSION_SUFFIX=$DRONE_PULL_REQUEST BUILD_UPDATER=ON DESKTOP_CLIENT_ROOT=$DRONE_WORKSPACE /bin/bash -c "./admin/linux/build-appimage.sh"
|
||||
- BUILDNR=$DRONE_BUILD_NUMBER VERSION_SUFFIX=$DRONE_PULL_REQUEST BUILD_UPDATER=ON DESKTOP_CLIENT_ROOT=$DRONE_WORKSPACE EXECUTABLE_NAME=nextcloud QT_BASE_DIR=/opt/qt6.6.3 OPENSSL_ROOT_DIR=/usr/local/lib64 /bin/bash -c "./admin/linux/build-appimage.sh"
|
||||
- BUILDNR=$DRONE_BUILD_NUMBER VERSION_SUFFIX=$DRONE_PULL_REQUEST DESKTOP_CLIENT_ROOT=$DRONE_WORKSPACE /bin/bash -c "./admin/linux/upload-appimage.sh" || echo "Upload failed, however this is an optional step."
|
||||
trigger:
|
||||
branch:
|
||||
|
@ -196,6 +206,6 @@ trigger:
|
|||
- push
|
||||
---
|
||||
kind: signature
|
||||
hmac: d72110d7f9cba086ca21f9f4f4032ae87f3d9555ab4c5f55d3aeb3df99cab540
|
||||
hmac: fbdc01c6461fcc32d9ebff4be97340cbb6da5566643b60289504ed86b2a67583
|
||||
|
||||
...
|
||||
|
|
|
@ -7,7 +7,7 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cpp-linter/cpp-linter-action@v2.7.1
|
||||
- uses: cpp-linter/cpp-linter-action@v2.11.0
|
||||
id: linter
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Add reaction on start
|
||||
uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
with:
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
repository: ${{ github.event.repository.full_name }}
|
||||
|
@ -42,7 +42,7 @@ jobs:
|
|||
GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
||||
- name: Add reaction on failure
|
||||
uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
if: failure()
|
||||
with:
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
|
|
@ -7,7 +7,7 @@ name: Block fixup and squash commits
|
|||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
types: [opened, ready_for_review, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
@ -24,10 +24,10 @@ jobs:
|
|||
pull-requests: write
|
||||
name: Block fixup and squash commits
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-latest-low
|
||||
|
||||
steps:
|
||||
- name: Run check
|
||||
uses: skjnldsv/block-fixup-merge-action@42d26e1b536ce61e5cf467d65fb76caf4aa85acf # v1
|
||||
uses: skjnldsv/block-fixup-merge-action@c138ea99e45e186567b64cf065ce90f7158c236a # v2
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
name: Linux Appimage Package
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
jobs:
|
||||
build:
|
||||
name: Linux Appimage Package
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/nextcloud/continuous-integration-client-appimage-qt6:client-appimage-6.6.3-2
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Configure, compile and package
|
||||
run: |
|
||||
BUILDNR=${GITHUB_RUN_ID} VERSION_SUFFIX=${GITHUB_HEAD_REF} BUILD_UPDATER=ON DESKTOP_CLIENT_ROOT=`pwd` EXECUTABLE_NAME=nextcloud QT_BASE_DIR=/opt/qt6.6.3 OPENSSL_ROOT_DIR=/usr/local/lib64 /bin/bash -c "./admin/linux/build-appimage.sh"
|
||||
BUILDNR=${GITHUB_RUN_ID} VERSION_SUFFIX=${GITHUB_HEAD_REF} DESKTOP_CLIENT_ROOT=`pwd` /bin/bash -c "./admin/linux/upload-appimage.sh" || echo "Upload failed, however this is an optional step."
|
|
@ -0,0 +1,25 @@
|
|||
name: Linux Clang compilation and tests
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
jobs:
|
||||
build:
|
||||
name: Linux Clang compilation and tests
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Configure and compile
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -G Ninja -DCMAKE_PREFIX_PATH=/opt/qt6.6.3 -DCMAKE_C_COMPILER=clang-14 -DCMAKE_CXX_COMPILER=clang++-14 -DCMAKE_BUILD_TYPE=Debug -DQT_MAJOR_VERSION=6 -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64
|
||||
ninja
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd build
|
||||
useradd -m -s /bin/bash test
|
||||
chown -R test:test .
|
||||
su -c 'xvfb-run ctest --output-on-failure' test
|
|
@ -0,0 +1,25 @@
|
|||
name: Linux GCC compilation and tests
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
jobs:
|
||||
build:
|
||||
name: Linux GCC compilation and tests
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Configure and compile
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -G Ninja -DCMAKE_PREFIX_PATH=/opt/qt6.6.3 -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_CXX_COMPILER=g++-11 -DCMAKE_BUILD_TYPE=Debug -DQT_MAJOR_VERSION=6 -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64
|
||||
ninja
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd build
|
||||
useradd -m -s /bin/bash test
|
||||
chown -R test:test .
|
||||
su -c 'xvfb-run ctest --output-on-failure' test
|
|
@ -5,9 +5,9 @@ on:
|
|||
jobs:
|
||||
build:
|
||||
name: macOS Build and Test
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-14
|
||||
env:
|
||||
CRAFT_TARGET: macos-64-clang
|
||||
CRAFT_TARGET: macos-clang-arm64
|
||||
CRAFT_MASTER_LOCATION: ${{ github.workspace }}/CraftMaster
|
||||
CRAFT_MASTER_CONFIG: ${{ github.workspace }}/craftmaster.ini
|
||||
steps:
|
||||
|
@ -15,16 +15,22 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: List Xcode installations
|
||||
run: sudo ls -1 /Applications | grep "Xcode"
|
||||
|
||||
- name: Select Xcode 15.3
|
||||
run: sudo xcode-select -s /Applications/Xcode_15.3.app/Contents/Developer
|
||||
|
||||
- name: Restore cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/cache
|
||||
key: macos-latest-${{ env.CRAFT_TARGET }}
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: 3.12
|
||||
|
||||
- name: Install Homebrew dependencies
|
||||
run: |
|
||||
|
|
|
@ -7,7 +7,7 @@ jobs:
|
|||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
operations-per-run: 1500
|
||||
days-before-stale: 28
|
||||
|
|
|
@ -5,8 +5,8 @@ on:
|
|||
jobs:
|
||||
build:
|
||||
name: SonarCloud analysis
|
||||
runs-on: ubuntu-22.04
|
||||
container: ghcr.io/nextcloud/continuous-integration-client:client-5.15-14
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/nextcloud/continuous-integration-client-qt6:client-6.6.3-2
|
||||
env:
|
||||
SONAR_SERVER_URL: "https://sonarcloud.io"
|
||||
BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed
|
||||
|
@ -16,7 +16,7 @@ jobs:
|
|||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
- name: Restore cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /cache
|
||||
key: ${{ runner.os }}
|
||||
|
@ -25,7 +25,7 @@ jobs:
|
|||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_CXX_COMPILER=g++-11 -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DBUILD_COVERAGE=ON -DOPENSSL_ROOT_DIR=/usr/local/lib64
|
||||
cmake .. -G Ninja -DCMAKE_PREFIX_PATH=/opt/qt6.6.3 -DCMAKE_C_COMPILER=gcc-11 -DCMAKE_CXX_COMPILER=g++-11 -DCMAKE_BUILD_TYPE=Debug -DQUICK_COMPILER=ON -DBUILD_UPDATER=ON -DBUILD_TESTING=1 -DQT_MAJOR_VERSION=6 -DCMAKE_CXX_FLAGS=-Werror -DOPENSSL_ROOT_DIR=/usr/local/lib64 -DBUILD_COVERAGE=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
|
||||
build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} ninja
|
||||
- name: Run tests
|
||||
run: |
|
||||
|
|
|
@ -7,7 +7,7 @@ jobs:
|
|||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
operations-per-run: 1500
|
||||
days-before-stale: 28
|
||||
|
|
|
@ -5,9 +5,9 @@ on:
|
|||
jobs:
|
||||
build:
|
||||
name: Windows Build and Test
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-2022
|
||||
env:
|
||||
CRAFT_TARGET: windows-msvc2019_64-cl
|
||||
CRAFT_TARGET: windows-msvc2022_64-cl
|
||||
COBERTURA_COVERAGE_FILE: ${{ github.workspace }}\cobertura_coverage\coverage.xml
|
||||
CRAFT_MASTER_LOCATION: ${{ github.workspace }}\CraftMaster
|
||||
CRAFT_MASTER_CONFIG: ${{ github.workspace }}\craftmaster.ini
|
||||
|
@ -16,9 +16,9 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.8'
|
||||
python-version: '3.12'
|
||||
- name: Install Craft Master with Nextcloud Client Deps
|
||||
shell: pwsh
|
||||
run: |
|
||||
|
@ -35,7 +35,7 @@ jobs:
|
|||
|
||||
- name: Cache Install OpenCppCoverage
|
||||
id: cache-install-opencppcoverage
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: C:\Program Files\OpenCppCoverage
|
||||
key: ${{ runner.os }}-cache-install-opencppcoverage
|
||||
|
@ -46,6 +46,19 @@ jobs:
|
|||
run: |
|
||||
choco install opencppcoverage
|
||||
|
||||
- name: Cache Install inkscape
|
||||
id: cache-install-inkscape
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: C:\Program Files\inkscape
|
||||
key: ${{ runner.os }}-cache-install-inkscape
|
||||
|
||||
- name: Install inkscape
|
||||
if: steps.cache-install-inkscape.outputs.cache-hit != 'true'
|
||||
shell: pwsh
|
||||
run: |
|
||||
choco install inkscape
|
||||
|
||||
- name: Setup PATH
|
||||
shell: pwsh
|
||||
run: |
|
||||
|
@ -62,24 +75,17 @@ jobs:
|
|||
|
||||
craft --src-dir ${{ github.workspace }} nextcloud-client
|
||||
|
||||
- name: Run tests
|
||||
- name: Run tests with coverage
|
||||
shell: pwsh
|
||||
run: |
|
||||
function runTestsAndCreateCoverage() {
|
||||
function runTests() {
|
||||
$buildFolder = "${{ github.workspace }}\${{ env.CRAFT_TARGET }}\build\nextcloud-client\work\build"
|
||||
|
||||
cd $buildFolder
|
||||
|
||||
$binFolder = "$buildFolder\bin"
|
||||
|
||||
& OpenCppCoverage.exe --optimized_build --quiet --sources ${{ github.workspace }} --modules $binFolder\*.dll* --export_type cobertura:${{ env.COBERTURA_COVERAGE_FILE }} --cover_children -- ctest --output-on-failure --timeout 300
|
||||
& ctest --output-on-failure --timeout 300
|
||||
}
|
||||
|
||||
runTestsAndCreateCoverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
directory: ${{ github.workspace }}\cobertura_coverage
|
||||
fail_ci_if_error: true
|
||||
runTests
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"fileScopedDeclarationPrivacy" : {
|
||||
"accessLevel" : "private"
|
||||
},
|
||||
"indentation" : {
|
||||
"spaces" : 4
|
||||
},
|
||||
"indentConditionalCompilationBlocks" : true,
|
||||
"indentSwitchCaseLabels" : false,
|
||||
"lineBreakAroundMultilineExpressionChainComponents" : false,
|
||||
"lineBreakBeforeControlFlowKeywords" : false,
|
||||
"lineBreakBeforeEachArgument" : false,
|
||||
"lineBreakBeforeEachGenericRequirement" : false,
|
||||
"lineLength" : 100,
|
||||
"maximumBlankLines" : 1,
|
||||
"multiElementCollectionTrailingCommas" : true,
|
||||
"noAssignmentInExpressions" : {
|
||||
"allowedFunctions" : [
|
||||
"XCTAssertNoThrow"
|
||||
]
|
||||
},
|
||||
"prioritizeKeepingFunctionOutputTogether" : false,
|
||||
"respectsExistingLineBreaks" : true,
|
||||
"rules" : {
|
||||
"AllPublicDeclarationsHaveDocumentation" : false,
|
||||
"AlwaysUseLiteralForEmptyCollectionInit" : false,
|
||||
"AlwaysUseLowerCamelCase" : true,
|
||||
"AmbiguousTrailingClosureOverload" : true,
|
||||
"BeginDocumentationCommentWithOneLineSummary" : false,
|
||||
"DoNotUseSemicolons" : true,
|
||||
"DontRepeatTypeInStaticProperties" : true,
|
||||
"FileScopedDeclarationPrivacy" : true,
|
||||
"FullyIndirectEnum" : true,
|
||||
"GroupNumericLiterals" : true,
|
||||
"IdentifiersMustBeASCII" : true,
|
||||
"NeverForceUnwrap" : false,
|
||||
"NeverUseForceTry" : false,
|
||||
"NeverUseImplicitlyUnwrappedOptionals" : false,
|
||||
"NoAccessLevelOnExtensionDeclaration" : true,
|
||||
"NoAssignmentInExpressions" : true,
|
||||
"NoBlockComments" : true,
|
||||
"NoCasesWithOnlyFallthrough" : true,
|
||||
"NoEmptyTrailingClosureParentheses" : true,
|
||||
"NoLabelsInCasePatterns" : true,
|
||||
"NoLeadingUnderscores" : false,
|
||||
"NoParensAroundConditions" : true,
|
||||
"NoPlaygroundLiterals" : true,
|
||||
"NoVoidReturnOnFunctionSignature" : true,
|
||||
"OmitExplicitReturns" : false,
|
||||
"OneCasePerLine" : true,
|
||||
"OneVariableDeclarationPerLine" : true,
|
||||
"OnlyOneTrailingClosureArgument" : true,
|
||||
"OrderedImports" : true,
|
||||
"ReplaceForEachWithForLoop" : true,
|
||||
"ReturnVoidInsteadOfEmptyTuple" : true,
|
||||
"TypeNamesShouldBeCapitalized" : true,
|
||||
"UseEarlyExits" : false,
|
||||
"UseLetInEveryBoundCaseVariable" : true,
|
||||
"UseShorthandTypeNames" : true,
|
||||
"UseSingleLinePropertyGetter" : true,
|
||||
"UseSynthesizedInitializer" : true,
|
||||
"UseTripleSlashForDocumentationComments" : true,
|
||||
"UseWhereClausesInForLoops" : false,
|
||||
"ValidateDocumentationComments" : false
|
||||
},
|
||||
"spacesAroundRangeFormationOperators" : false,
|
||||
"tabWidth" : 8,
|
||||
"version" : 1
|
||||
}
|
|
@ -1,20 +1,31 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
cmake_policy(SET CMP0071 NEW) # Enable use of QtQuick compiler/generated code
|
||||
|
||||
project(client)
|
||||
|
||||
if(APPLE)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OSX deployment version")
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED 17)
|
||||
|
||||
include(FeatureSummary)
|
||||
|
||||
find_program(CLANG_TIDY_EXE NAMES "clang-tidy")
|
||||
if (CLANG_TIDY_EXE)
|
||||
set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_EXE} -checks=-*,modernize-use-auto,modernize-use-using,modernize-use-nodiscard,modernize-use-nullptr,modernize-use-override,cppcoreguidelines-pro-type-static-cast-downcast,modernize-use-default-member-init,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-init-variables)
|
||||
endif()
|
||||
|
||||
project(client)
|
||||
|
||||
include(FeatureSummary)
|
||||
set(CMAKE_XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES)
|
||||
|
||||
set(BIN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||
|
||||
include(${CMAKE_SOURCE_DIR}/NEXTCLOUD.cmake)
|
||||
|
||||
set(QT_VERSION_MAJOR "6")
|
||||
set(REQUIRED_QT_VERSION "6.0.0")
|
||||
|
||||
# CfAPI Shell Extensions
|
||||
set( CFAPI_SHELL_EXTENSIONS_LIB_NAME CfApiShellExtensions )
|
||||
|
||||
|
@ -100,7 +111,8 @@ include(GetGitRevisionDescription)
|
|||
get_git_head_revision(GIT_REFSPEC GIT_SHA1)
|
||||
|
||||
add_definitions(
|
||||
-DQT_DISABLE_DEPRECATED_BEFORE=0x000000
|
||||
-DQT_DISABLE_DEPRECATED_BEFORE=0x051200
|
||||
-DQT_DEPRECATED_WARNINGS
|
||||
-DQT_USE_QSTRINGBUILDER
|
||||
-DQT_MESSAGELOGCONTEXT #enable function name and line number in debug output
|
||||
)
|
||||
|
@ -130,7 +142,7 @@ if(APPLE AND BUILD_OWNCLOUD_OSX_BUNDLE)
|
|||
add_definitions(-DBUILD_OWNCLOUD_OSX_BUNDLE)
|
||||
endif()
|
||||
|
||||
|
||||
find_package(Qt${QT_MAJOR_VERSION} COMPONENTS Core)
|
||||
option(QUICK_COMPILER "Use QtQuick compiler to improve performance" OFF)
|
||||
|
||||
# this option removes Http authentication, keychain, shibboleth etc and is intended for
|
||||
|
@ -176,6 +188,9 @@ option(BUILD_GUI "BUILD_GUI" ON)
|
|||
# build the tests
|
||||
option(BUILD_TESTING "BUILD_TESTING" ON)
|
||||
|
||||
# allows to run nextclouddev in parallel to nextcloud + logs
|
||||
option(NEXTCLOUD_DEV "NEXTCLOUD_DEV" OFF)
|
||||
|
||||
option(ENABLE_CLANG_TIDY "ENABLE_CLANG_TIDY" OFF)
|
||||
if(ENABLE_CLANG_TIDY)
|
||||
find_program(CLANG_TIDY_EXE NAMES "clang-tidy")
|
||||
|
@ -214,6 +229,7 @@ if(BUILD_CLIENT)
|
|||
find_package(OpenSSL 1.1 REQUIRED )
|
||||
|
||||
find_package(ZLIB REQUIRED)
|
||||
find_package(SQLite3 3.9.0 REQUIRED)
|
||||
|
||||
if(NOT WIN32 AND NOT APPLE)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
set( APPLICATION_NAME "Nextcloud" )
|
||||
set( APPLICATION_SHORTNAME "Nextcloud" )
|
||||
set( APPLICATION_EXECUTABLE "nextcloud" )
|
||||
if(NEXTCLOUD_DEV)
|
||||
set( APPLICATION_NAME "NextcloudDev" )
|
||||
set( APPLICATION_SHORTNAME "NextDev" )
|
||||
set( APPLICATION_EXECUTABLE "nextclouddev" )
|
||||
set( APPLICATION_ICON_NAME "Nextcloud" )
|
||||
else()
|
||||
set( APPLICATION_NAME "Nextcloud" )
|
||||
set( APPLICATION_SHORTNAME "Nextcloud" )
|
||||
set( APPLICATION_EXECUTABLE "nextcloud" )
|
||||
set( APPLICATION_ICON_NAME "${APPLICATION_SHORTNAME}" )
|
||||
endif()
|
||||
|
||||
set( APPLICATION_CONFIG_NAME "${APPLICATION_EXECUTABLE}" )
|
||||
set( APPLICATION_DOMAIN "nextcloud.com" )
|
||||
set( APPLICATION_VENDOR "Nextcloud GmbH" )
|
||||
|
@ -10,8 +19,6 @@ set( APPLICATION_HELP_URL "" CACHE STRING "URL for the help menu" )
|
|||
if(APPLE AND APPLICATION_NAME STREQUAL "Nextcloud" AND EXISTS "${CMAKE_SOURCE_DIR}/theme/colored/Nextcloud-macOS-icon.svg")
|
||||
set( APPLICATION_ICON_NAME "Nextcloud-macOS" )
|
||||
message("Using macOS-specific application icon: ${APPLICATION_ICON_NAME}")
|
||||
else()
|
||||
set( APPLICATION_ICON_NAME "${APPLICATION_SHORTNAME}" )
|
||||
endif()
|
||||
|
||||
set( APPLICATION_ICON_SET "SVG" )
|
||||
|
@ -77,6 +84,6 @@ if(WIN32)
|
|||
option( BUILD_WIN_TOOLS "Build Win32 migration tools" OFF )
|
||||
endif()
|
||||
|
||||
if (APPLE)
|
||||
if (APPLE AND CMAKE_OSX_DEPLOYMENT_TARGET VERSION_GREATER_EQUAL 11.0)
|
||||
option( BUILD_FILE_PROVIDER_MODULE "Build the macOS virtual files File Provider module" OFF )
|
||||
endif()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
set( MIRALL_VERSION_MAJOR 3 )
|
||||
set( MIRALL_VERSION_MINOR 10 )
|
||||
set( MIRALL_VERSION_MINOR 13 )
|
||||
set( MIRALL_VERSION_PATCH 50 )
|
||||
set( MIRALL_VERSION_YEAR 2023 )
|
||||
set( MIRALL_VERSION_YEAR 2024 )
|
||||
set( MIRALL_SOVERSION 0 )
|
||||
|
||||
# Minimum supported server version according to https://docs.nextcloud.com/server/latest/admin_manual/release_schedule.html
|
||||
|
@ -13,6 +13,10 @@ set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR 26)
|
|||
set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MINOR 0)
|
||||
set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH 0)
|
||||
|
||||
set(NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MAJOR 28)
|
||||
set(NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MINOR 0)
|
||||
set(NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_PATCH 3)
|
||||
|
||||
if ( NOT DEFINED MIRALL_VERSION_SUFFIX )
|
||||
set( MIRALL_VERSION_SUFFIX "git") #e.g. beta1, beta2, rc1
|
||||
endif( NOT DEFINED MIRALL_VERSION_SUFFIX )
|
||||
|
@ -33,3 +37,5 @@ set( MIRALL_VERSION_STRING "${MIRALL_VERSION}${MIRALL_VERSION_SUFFIX}" )
|
|||
if( MIRALL_VERSION_BUILD )
|
||||
set( MIRALL_VERSION_STRING "${MIRALL_VERSION_STRING} (build ${MIRALL_VERSION_BUILD})" )
|
||||
endif( MIRALL_VERSION_BUILD )
|
||||
|
||||
set(QT_MAJOR_VERSION 6)
|
||||
|
|
|
@ -2,16 +2,13 @@
|
|||
|
||||
set -xe
|
||||
|
||||
export APPNAME=${APPNAME:-nextcloud}
|
||||
export APPNAME=${APPNAME:-Nextcloud}
|
||||
export EXECUTABLE_NAME=${EXECUTABLE_NAME:-nextcloud}
|
||||
export BUILD_UPDATER=${BUILD_UPDATER:-OFF}
|
||||
export BUILDNR=${BUILDNR:-0000}
|
||||
export DESKTOP_CLIENT_ROOT=${DESKTOP_CLIENT_ROOT:-/home/user}
|
||||
|
||||
#Set Qt-5.15
|
||||
export QT_BASE_DIR=/opt/kdeqt5.15
|
||||
|
||||
export QTDIR=$QT_BASE_DIR
|
||||
export PATH=$QT_BASE_DIR/bin:$PATH
|
||||
export QT_BASE_DIR=${QT_BASE_DIR:-/usr}
|
||||
export OPENSSL_ROOT_DIR=${OPENSSL_ROOT_DIR:-/usr/lib/x86_64-linux-gnu}
|
||||
|
||||
# Set defaults
|
||||
export SUFFIX=${DRONE_PULL_REQUEST:=master}
|
||||
|
@ -29,12 +26,14 @@ mkdir build-client
|
|||
cd build-client
|
||||
cmake \
|
||||
-G Ninja \
|
||||
-D CMAKE_INSTALL_PREFIX=/usr \
|
||||
-D BUILD_TESTING=OFF \
|
||||
-D BUILD_UPDATER=$BUILD_UPDATER \
|
||||
-D MIRALL_VERSION_BUILD=$BUILDNR \
|
||||
-D MIRALL_VERSION_SUFFIX="$VERSION_SUFFIX" \
|
||||
-D CMAKE_UNITY_BUILD=ON \
|
||||
-DCMAKE_PREFIX_PATH=${QT_BASE_DIR} \
|
||||
-DOPENSSL_ROOT_DIR=${OPENSSL_ROOT_DIR} \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DBUILD_TESTING=OFF \
|
||||
-DBUILD_UPDATER=$BUILD_UPDATER \
|
||||
-DMIRALL_VERSION_BUILD=$BUILDNR \
|
||||
-DMIRALL_VERSION_SUFFIX="$VERSION_SUFFIX" \
|
||||
-DCMAKE_UNITY_BUILD=ON \
|
||||
${DESKTOP_CLIENT_ROOT}
|
||||
cmake --build . --target all
|
||||
DESTDIR=/app cmake --install .
|
||||
|
@ -64,48 +63,37 @@ rm -rf etc
|
|||
|
||||
# com.nextcloud.desktopclient.nextcloud.desktop
|
||||
DESKTOP_FILE=$(ls /app/usr/share/applications/*.desktop)
|
||||
sed -i -e 's|Icon=nextcloud|Icon=Nextcloud|g' ${DESKTOP_FILE} # Bug in desktop file?
|
||||
cp ./usr/share/icons/hicolor/512x512/apps/*.png . # Workaround for linuxeployqt bug, FIXME
|
||||
|
||||
# Because distros need to get their shit together
|
||||
cp -R /usr/lib/x86_64-linux-gnu/libssl.so* ./usr/lib/
|
||||
cp -R /usr/lib/x86_64-linux-gnu/libcrypto.so* ./usr/lib/
|
||||
cp -P /usr/local/lib*/libssl.so* ./usr/lib/
|
||||
cp -P /usr/local/lib*/libcrypto.so* ./usr/lib/
|
||||
cp -P /usr/local/lib*/libsqlite*.so* ./usr/lib/
|
||||
# Use linuxdeploy to deploy
|
||||
export APPIMAGE_NAME=linuxdeploy-x86_64.AppImage
|
||||
wget -O ${APPIMAGE_NAME} --ca-directory=/etc/ssl/certs -c "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
|
||||
chmod a+x ${APPIMAGE_NAME}
|
||||
./${APPIMAGE_NAME} --appimage-extract
|
||||
rm ./${APPIMAGE_NAME}
|
||||
cp -r ./squashfs-root ./linuxdeploy-squashfs-root
|
||||
|
||||
# NSS fun
|
||||
cp -P -r /usr/lib/x86_64-linux-gnu/nss ./usr/lib/
|
||||
export LD_LIBRARY_PATH=/app/usr/lib:${QT_BASE_DIR}/lib:/usr/local/lib/x86_64-linux-gnu:/usr/local/lib:/usr/local/lib64
|
||||
./linuxdeploy-squashfs-root/AppRun --desktop-file=${DESKTOP_FILE} --icon-file=usr/share/icons/hicolor/512x512/apps/Nextcloud.png --executable=usr/bin/${EXECUTABLE_NAME} --appdir=AppDir
|
||||
|
||||
# Use linuxdeployqt to deploy
|
||||
LINUXDEPLOYQT_VERSION="continuous"
|
||||
wget -O linuxdeployqt.AppImage --ca-directory=/etc/ssl/certs -c "https://github.com/probonopd/linuxdeployqt/releases/download/${LINUXDEPLOYQT_VERSION}/linuxdeployqt-continuous-x86_64.AppImage"
|
||||
chmod a+x linuxdeployqt.AppImage
|
||||
./linuxdeployqt.AppImage --appimage-extract
|
||||
rm ./linuxdeployqt.AppImage
|
||||
cp -r ./squashfs-root ./linuxdeployqt-squashfs-root
|
||||
unset QTDIR; unset QT_PLUGIN_PATH ; unset LD_LIBRARY_PATH
|
||||
export LD_LIBRARY_PATH=/usr/local/lib/x86_64-linux-gnu
|
||||
./squashfs-root/AppRun ${DESKTOP_FILE} -bundle-non-qt-libs -qmldir=${DESKTOP_CLIENT_ROOT}/src/gui
|
||||
# Use linuxdeploy-plugin-qt to deploy qt dependencies
|
||||
export APPIMAGE_NAME=linuxdeploy-plugin-qt-x86_64.AppImage
|
||||
wget -O ${APPIMAGE_NAME} --ca-directory=/etc/ssl/certs -c "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage"
|
||||
chmod a+x ${APPIMAGE_NAME}
|
||||
./${APPIMAGE_NAME} --appimage-extract
|
||||
rm ./${APPIMAGE_NAME}
|
||||
cp -r ./squashfs-root ./linuxdeploy-plugin-qt-squashfs-root
|
||||
|
||||
# Set origin
|
||||
./squashfs-root/usr/bin/patchelf --set-rpath '$ORIGIN/' /app/usr/lib/lib*sync.so.0
|
||||
export PATH=${QT_BASE_DIR}/bin:${PATH}
|
||||
export QML_SOURCES_PATHS=${DESKTOP_CLIENT_ROOT}/src/gui
|
||||
./linuxdeploy-plugin-qt-squashfs-root/AppRun --appdir=AppDir
|
||||
|
||||
# Build AppImage
|
||||
./squashfs-root/AppRun ${DESKTOP_FILE} -appimage -updateinformation="gh-releases-zsync|nextcloud-releases|desktop|latest|Nextcloud-*-x86_64.AppImage.zsync"
|
||||
|
||||
# Workaround issue #103
|
||||
rm -rf ./squashfs-root
|
||||
APPIMAGE=$(ls *.AppImage)
|
||||
"./${APPIMAGE}" --appimage-extract
|
||||
rm "./${APPIMAGE}"
|
||||
rm ./squashfs-root/usr/lib/libglib-2.0.so.0
|
||||
rm ./squashfs-root/usr/lib/libgobject-2.0.so.0
|
||||
PATH=./linuxdeployqt-squashfs-root/usr/bin:$PATH appimagetool -n ./squashfs-root "$APPIMAGE"
|
||||
./linuxdeploy-squashfs-root/AppRun --desktop-file=${DESKTOP_FILE} --icon-file=usr/share/icons/hicolor/512x512/apps/Nextcloud.png --executable=usr/bin/${EXECUTABLE_NAME} --appdir=AppDir --output appimage
|
||||
|
||||
#move AppImage
|
||||
if [ ! -z "$DRONE_COMMIT" ]
|
||||
then
|
||||
mv *.AppImage ${APPNAME}-${SUFFIX}-${DRONE_COMMIT}-x86_64.AppImage
|
||||
mv *.AppImage ${EXECUTABLE_NAME}-${SUFFIX}-${DRONE_COMMIT}-x86_64.AppImage
|
||||
else
|
||||
mv *.AppImage ${EXECUTABLE_NAME}-${SUFFIX}-x86_64.AppImage
|
||||
fi
|
||||
mv *.AppImage ${DESKTOP_CLIENT_ROOT}/
|
||||
|
|
|
@ -18,7 +18,7 @@ if test "${DRONE_TARGET_BRANCH}" = "stable-2.6"; then
|
|||
UBUNTU_DISTRIBUTIONS="bionic focal jammy kinetic"
|
||||
DEBIAN_DISTRIBUTIONS="buster stretch testing"
|
||||
else
|
||||
UBUNTU_DISTRIBUTIONS="jammy lunar mantic"
|
||||
UBUNTU_DISTRIBUTIONS="jammy mantic noble"
|
||||
DEBIAN_DISTRIBUTIONS="bullseye bookworm testing"
|
||||
fi
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ else()
|
|||
set(MAC_INSTALLER_DO_CUSTOM_BACKGROUND "0")
|
||||
endif()
|
||||
|
||||
find_package(Qt5 5.15 COMPONENTS Core REQUIRED)
|
||||
find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} COMPONENTS Core REQUIRED)
|
||||
configure_file(create_mac.sh.cmake ${CMAKE_CURRENT_BINARY_DIR}/create_mac.sh)
|
||||
configure_file(macosx.pkgproj.cmake ${CMAKE_CURRENT_BINARY_DIR}/macosx.pkgproj)
|
||||
configure_file(pre_install.sh.cmake ${CMAKE_CURRENT_BINARY_DIR}/pre_install.sh)
|
||||
|
|
|
@ -10,6 +10,4 @@ if [ -x "$(command -v pluginkit)" ]; then
|
|||
pluginkit -e use -i @APPLICATION_REV_DOMAIN@.FinderSyncExt
|
||||
fi
|
||||
|
||||
open -a @APPLICATION_NAME@.app
|
||||
|
||||
exit 0
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-FileCopyrightText: 2019-2020 Tobias Kaminsky <tobias@kaminsky.me>
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
counter=0
|
||||
status=""
|
||||
|
||||
until [[ $status = "false" ]]; do
|
||||
status=$(curl 2>/dev/null "http://$1/status.php" | jq .maintenance)
|
||||
|
||||
echo "($counter) $status"
|
||||
|
||||
if [[ "$status" =~ "false" || "$status" = "" ]]; then
|
||||
let "counter += 1"
|
||||
if [[ $counter -gt 90 ]]; then
|
||||
echo "Failed to wait for server"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
sleep 10
|
||||
done
|
||||
|
|
@ -194,14 +194,14 @@
|
|||
</Component>
|
||||
<!-- Register URI handler -->
|
||||
<Component Id="RegistryUriHandler" Guid="*" Win64="$(var.PlatformWin64)">
|
||||
<RegistryKey Root="HKCU" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
|
||||
<RegistryKey Root="HKLM" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
|
||||
<RegistryValue Type="string" Value="URL:$(var.AppName) Protocol" />
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value="" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="HKCU" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)\DefaultIcon" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
|
||||
<RegistryKey Root="HKLM" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)\DefaultIcon" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
|
||||
<RegistryValue Type="string" Value="[INSTALLDIR]$(var.AppExe)" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="HKCU" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)\shell\open\command" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
|
||||
<RegistryKey Root="HKLM" Key="Software\Classes\$(var.AppCommandOpenUrlScheme)\shell\open\command" ForceCreateOnInstall="yes" ForceDeleteOnUninstall="yes">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.AppExe)" "%1"" />
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
|
|
|
@ -1,60 +1,68 @@
|
|||
# - Try to find SQLite3
|
||||
# Once done this will define
|
||||
#
|
||||
# SQLITE3_FOUND - system has SQLite3
|
||||
# SQLITE3_INCLUDE_DIRS - the SQLite3 include directory
|
||||
# SQLITE3_LIBRARIES - Link these to use SQLite3
|
||||
# SQLITE3_DEFINITIONS - Compiler switches required for using SQLite3
|
||||
#
|
||||
# Copyright (c) 2009-2013 Andreas Schneider <asn@cryptomilk.org>
|
||||
#
|
||||
# Redistribution and use is allowed according to the terms of the New
|
||||
# BSD license.
|
||||
# For details see the accompanying COPYING-CMAKE-SCRIPTS file.
|
||||
#
|
||||
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
||||
# file Copyright.txt or https://cmake.org/licensing for details.
|
||||
|
||||
#[=======================================================================[.rst:
|
||||
FindSQLite3
|
||||
-----------
|
||||
|
||||
if (UNIX)
|
||||
find_package(PkgConfig)
|
||||
if (PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(_SQLITE3 sqlite3)
|
||||
endif (PKG_CONFIG_FOUND)
|
||||
endif (UNIX)
|
||||
.. versionadded:: 3.14
|
||||
|
||||
find_path(SQLITE3_INCLUDE_DIR
|
||||
NAMES
|
||||
sqlite3.h
|
||||
PATHS
|
||||
${_SQLITE3_INCLUDEDIR}
|
||||
${SQLITE3_INCLUDE_DIRS}
|
||||
)
|
||||
Find the SQLite libraries, v3
|
||||
|
||||
find_library(SQLITE3_LIBRARY
|
||||
NAMES
|
||||
sqlite3 sqlite3-0
|
||||
PATHS
|
||||
${_SQLITE3_LIBDIR}
|
||||
${SQLITE3_LIBRARIES}
|
||||
)
|
||||
IMPORTED targets
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
set(SQLITE3_INCLUDE_DIRS
|
||||
${SQLITE3_INCLUDE_DIR}
|
||||
)
|
||||
This module defines the following :prop_tgt:`IMPORTED` target:
|
||||
|
||||
if (SQLITE3_LIBRARY)
|
||||
set(SQLITE3_LIBRARIES
|
||||
${SQLITE3_LIBRARIES}
|
||||
${SQLITE3_LIBRARY}
|
||||
)
|
||||
endif (SQLITE3_LIBRARY)
|
||||
``SQLite::SQLite3``
|
||||
|
||||
if (SQLite3_FIND_VERSION AND _SQLITE3_VERSION)
|
||||
set(SQLite3_VERSION _SQLITE3_VERSION)
|
||||
endif (SQLite3_FIND_VERSION AND _SQLITE3_VERSION)
|
||||
Result variables
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
This module will set the following variables if found:
|
||||
|
||||
``SQLite3_INCLUDE_DIRS``
|
||||
where to find sqlite3.h, etc.
|
||||
``SQLite3_LIBRARIES``
|
||||
the libraries to link against to use SQLite3.
|
||||
``SQLite3_VERSION``
|
||||
version of the SQLite3 library found
|
||||
``SQLite3_FOUND``
|
||||
TRUE if found
|
||||
|
||||
#]=======================================================================]
|
||||
|
||||
# Look for the necessary header
|
||||
find_path(SQLite3_INCLUDE_DIR NAMES sqlite3.h)
|
||||
mark_as_advanced(SQLite3_INCLUDE_DIR)
|
||||
|
||||
# Look for the necessary library
|
||||
find_library(SQLite3_LIBRARY NAMES sqlite3 sqlite)
|
||||
mark_as_advanced(SQLite3_LIBRARY)
|
||||
|
||||
# Extract version information from the header file
|
||||
if(SQLite3_INCLUDE_DIR)
|
||||
file(STRINGS ${SQLite3_INCLUDE_DIR}/sqlite3.h _ver_line
|
||||
REGEX "^#define SQLITE_VERSION *\"[0-9]+\\.[0-9]+\\.[0-9]+\""
|
||||
LIMIT_COUNT 1)
|
||||
string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+"
|
||||
SQLite3_VERSION "${_ver_line}")
|
||||
unset(_ver_line)
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(SQLite3 DEFAULT_MSG SQLITE3_LIBRARIES SQLITE3_INCLUDE_DIRS)
|
||||
|
||||
# show the SQLITE3_INCLUDE_DIRS and SQLITE3_LIBRARIES variables only in the advanced view
|
||||
mark_as_advanced(SQLITE3_INCLUDE_DIRS SQLITE3_LIBRARIES)
|
||||
find_package_handle_standard_args(SQLite3
|
||||
REQUIRED_VARS SQLite3_INCLUDE_DIR SQLite3_LIBRARY
|
||||
VERSION_VAR SQLite3_VERSION)
|
||||
|
||||
# Create the imported target
|
||||
if(SQLite3_FOUND)
|
||||
set(SQLite3_INCLUDE_DIRS ${SQLite3_INCLUDE_DIR})
|
||||
set(SQLite3_LIBRARIES ${SQLite3_LIBRARY})
|
||||
if(NOT TARGET SQLite::SQLite3)
|
||||
add_library(SQLite::SQLite3 UNKNOWN IMPORTED)
|
||||
set_target_properties(SQLite::SQLite3 PROPERTIES
|
||||
IMPORTED_LOCATION "${SQLite3_LIBRARY}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${SQLite3_INCLUDE_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0.0</string>
|
||||
<string>10.13.0</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
|
|
|
@ -61,4 +61,6 @@
|
|||
|
||||
#cmakedefine CFAPI_SHELL_EXTENSIONS_LIB_NAME "@CFAPI_SHELL_EXTENSIONS_LIB_NAME@"
|
||||
|
||||
#cmakedefine01 NEXTCLOUD_DEV
|
||||
|
||||
#endif
|
||||
|
|
|
@ -15,12 +15,7 @@ CreateCache = False
|
|||
# Category is case sensitive
|
||||
[GeneralSettings]
|
||||
|
||||
General/MacDeploymentTarget = 10.15
|
||||
|
||||
## This is the location of your python installation.
|
||||
## This value must be set.
|
||||
Paths/Python = C:\Python39-x64
|
||||
Paths/Python27 = C:\Python27-x64
|
||||
General/MacDeploymentTarget = 12.0
|
||||
|
||||
Compile/BuildType = RelWithDebInfo
|
||||
|
||||
|
@ -30,9 +25,8 @@ Paths/downloaddir = ${Variables:Root}\downloads
|
|||
ShortPath/Enabled = False
|
||||
ShortPath/EnableJunctions = False
|
||||
|
||||
; Packager/RepositoryUrl = https://files.kde.org/craft/
|
||||
Packager/RepositoryUrl = https://files.kde.org/craft/Qt6
|
||||
Packager/PackageType = NullsoftInstallerPackager
|
||||
Packager/RepositoryUrl = https://files.kde.org/craft/master/
|
||||
|
||||
ContinuousIntegration/Enabled = True
|
||||
|
||||
|
@ -44,40 +38,22 @@ Packager/UseCache = ${Variables:UseCache}
|
|||
Packager/CreateCache = ${Variables:CreateCache}
|
||||
Packager/CacheDir = ${Variables:Root}\cache
|
||||
|
||||
#CodeSigning/Enabled = ${Env:SIGN_PACKAGE}
|
||||
#CodeSigning/Protected = True
|
||||
#CodeSigning/Certificate = ${Env:CRAFT_CODESIGN_CERTIFICATE}
|
||||
#CodeSigning/CommonName =
|
||||
#CodeSigning/MacDeveloperId = ownCloud GmbH (4AP2STM4H5)
|
||||
#CodeSigning/MacKeychainPath = sign-${Env:DRONE_BUILD_NUMBER}.keychain
|
||||
|
||||
[BlueprintSettings]
|
||||
# don't try to pip install on the ci
|
||||
python-modules.ignored = True
|
||||
dev-utils/python2.ignored = True
|
||||
dev-utils/python3.ignored = True
|
||||
nextcloud-client.buildTests = True
|
||||
binary/mysql.useMariaDB = False
|
||||
|
||||
[windows-msvc2019_64-cl]
|
||||
QtSDK/Compiler = msvc2019_64
|
||||
General/ABI = windows-msvc2019_64-cl
|
||||
[windows-msvc2022_64-cl]
|
||||
QtSDK/Compiler = msvc2022_64
|
||||
General/ABI = windows-msvc2022_64-cl
|
||||
Paths/Python = C:\Python312-x64
|
||||
|
||||
[macos-64-clang]
|
||||
General/ABI = macos-64-clang
|
||||
# Packager/PackageType = MacPkgPackager
|
||||
|
||||
[macos-64-clang-debug]
|
||||
General/ABI = macos-64-clang
|
||||
Compile/BuildType = Debug
|
||||
|
||||
[macos-clang-arm64]
|
||||
General/ABI = macos-clang-arm64
|
||||
|
||||
[macos-clang-arm64-debug]
|
||||
General/ABI = macos-clang-arm64
|
||||
Compile/BuildType = Debug
|
||||
Paths/Python = /Users/runner/hostedtoolcache/Python/3.12.3/arm64
|
||||
|
||||
[Env]
|
||||
CRAFT_CODESIGN_CERTIFICATE =
|
||||
SIGN_PACKAGE = False
|
||||
SIGN_PACKAGE = False
|
||||
|
|
114
doc/building.rst
114
doc/building.rst
|
@ -128,7 +128,7 @@ Then, in Terminal:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
% brew install git qt qtkeychain cmake openssl glib cmocka
|
||||
% brew install git qt qtkeychain cmake openssl glib cmocka karchive
|
||||
|
||||
5. Certain Homebrew packages are not automatically linked in places where
|
||||
the build scripts can find them, so you can create a shell-profile script
|
||||
|
@ -136,9 +136,8 @@ Then, in Terminal:
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
% echo 'export OPENSSL_ROOT_DIR=$(brew --prefix openssl)' >> ~/.nextcloud_build_variables
|
||||
% echo 'export QT_PATH=$(brew --prefix qt5)/bin' >> ~/.nextcloud_build_variables
|
||||
% echo 'export Qt5LinguistTools_DIR=$(brew --prefix qt5)/lib/cmake/Qt5LinguistTools/' >> ~/.nextcloud_build_variables
|
||||
% echo 'export QT_PATH=$(brew --prefix qt6)/bin' >> ~/.nextcloud_build_variables
|
||||
% echo 'export CMAKE_PREFIX_PATH=$(brew --prefix qt6);$(brew --prefix karchive)' >> ~/.nextcloud_build_variables
|
||||
|
||||
.. note:: The name ``~/.nextcloud_build_variables`` is just a suggestion for
|
||||
convenience. You can use a different file or create an entire shell
|
||||
|
@ -207,58 +206,88 @@ Then, in Terminal:
|
|||
Windows Development Build
|
||||
-------------------------
|
||||
|
||||
If you want to test some changes and deploy them locally, you can build natively
|
||||
on Windows using MinGW. If you want to generate an installer for deployment, please
|
||||
follow `Windows Installer Build (Cross-Compile)`_ instead.
|
||||
System requirements
|
||||
-------------------
|
||||
- Windows 10 or Windows 11
|
||||
- `The desktop client code <https://github.com/nextcloud/desktop>`_
|
||||
- Python 3
|
||||
- PowerShell
|
||||
- Microsoft Visual Studio 2022 and tools to compile C++
|
||||
- `KDE Craft <https://community.kde.org/Craft>`_
|
||||
|
||||
1. Get the required dependencies:
|
||||
Setting up Microsoft Visual Studio
|
||||
----------------------------------
|
||||
|
||||
* Make sure that you have CMake_ and Git_.
|
||||
* Download the Qt_ MinGW package. You will use the MinGW version bundled with it.
|
||||
* Download an `OpenSSL Windows Build`_ (the non-"Light" version)
|
||||
1. Click on 'Modify' in the Visual Studio Installer:
|
||||
|
||||
2. Get the QtKeychain_ sources as well as the latest versions of the Nextcloud client
|
||||
from Git as follows
|
||||
.. image:: ./images/building/visual-studio-installer.png
|
||||
:alt: Visual Studio Installer
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone https://github.com/frankosterfeld/qtkeychain.git
|
||||
git clone git://github.com/nextcloud/client.git
|
||||
2. Select 'Desktop development with C++'
|
||||
|
||||
3. Open the Qt MinGW shortcut console from the Start Menu
|
||||
.. image:: ./images/building/desktop-development-with-cpp.png
|
||||
:alt: Desktop development with C++
|
||||
|
||||
4. Make sure that OpenSSL's ``bin`` directory as well as your qtkeychain source
|
||||
directories are in your PATH. This will allow CMake to find the library and
|
||||
headers, as well as allow the Nextcloud client to find the DLLs at runtime::
|
||||
Handling the dependencies
|
||||
-------------------------
|
||||
We handle the dependencies using `KDE Craft <https://community.kde.org/Craft>`_ because it is easy to set it up and it makes the maintenance much more reliable in all platforms.
|
||||
|
||||
set PATH=C:\<OpenSSL Install Dir>\bin;%PATH%
|
||||
set PATH=C:\<qtkeychain Clone Dir>;%PATH%
|
||||
1. Set up KDE Craft as instructed in `Get Involved/development/Windows - KDE Community Wiki <https://community.kde.org/Get_Involved/development/Windows>`_ - it requires Python 3 and PowerShell.
|
||||
2. After running:
|
||||
|
||||
5. Build qtkeychain **directly in the source directory** so that the DLL is built
|
||||
in the same directory as the headers to let CMake find them together through PATH::
|
||||
.. code-block:: winbatch
|
||||
|
||||
cd <qtkeychain Clone Dir>
|
||||
cmake -G "MinGW Makefiles" .
|
||||
mingw32-make
|
||||
cd ..
|
||||
C:\CraftRoot\craft\craftenv.ps1
|
||||
|
||||
6. Create the build directory::
|
||||
3. Add the `desktop client blueprints <https://github.com/nextcloud/desktop-client-blueprints>`_ - the instructions to handle the client dependencies:
|
||||
|
||||
mkdir client-build
|
||||
cd client-build
|
||||
.. code-block:: winbatch
|
||||
|
||||
7. Build the client::
|
||||
craft --add-blueprint-repository [git]https://github.com/nextcloud/desktop-client-blueprints.git
|
||||
craft craft
|
||||
|
||||
cmake -G "MinGW Makefiles" ../client
|
||||
mingw32-make
|
||||
4. Install all client dependencies:
|
||||
|
||||
.. note:: You can try using ninja to build in parallel using
|
||||
``cmake -G Ninja ../client`` and ``ninja`` instead.
|
||||
.. note:: Refer to the :ref:`generic-build-instructions` section for additional options.
|
||||
.. code-block:: winbatch
|
||||
|
||||
The Nextcloud binary will appear in the ``bin`` directory.
|
||||
craft --install-deps nextcloud-client
|
||||
|
||||
.. _`Windows Installer Build (Cross-Compile)`:
|
||||
Compiling
|
||||
---------
|
||||
|
||||
1. Make sure your environment variable %PATH% has no conflicting information to the environment you will use to compile the client. For instance, if you have installed OpenSSL previously and have added it to %PATH%, the OpenSSL installed might be a different version than what was installed via KDE Craft.
|
||||
2. Open the Command Prompt (cmd.exe)
|
||||
3. Run:
|
||||
|
||||
.. code-block:: winbatch
|
||||
|
||||
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x64
|
||||
|
||||
4. To use the tools installed with Visual Studio, you need the following in your %PATH%:
|
||||
|
||||
.. image:: ./images/building/path.png
|
||||
:alt: Windows environment variables
|
||||
|
||||
5. Alternatively you can use the tools installed with KDE Craft by adding them to %PATH%:
|
||||
|
||||
.. code-block:: winbatch
|
||||
|
||||
set "PATH=C:\CraftRoot\bin;C:\CraftRoot\dev-utils\bin;%PATH%"
|
||||
|
||||
.. note::
|
||||
C:\CraftRoot is the path used by default by KDE Craft. When you are setting it up you may choose a different folder.
|
||||
|
||||
6. Create build folder, run cmake, compile and install:
|
||||
|
||||
.. code-block:: winbatch
|
||||
|
||||
cd <desktop-repo-path>
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -G Ninja -DCMAKE_INSTALL_PREFIX=. -DCMAKE_PREFIX_PATH=C:\CraftRoot -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
cmake --build . --target install
|
||||
|
||||
7. Now you can use `Qt Creator <https://doc.qt.io/qtcreator>`_ to import the build folder with its configurations to be able to work with the code.
|
||||
|
||||
Windows Installer (i.e. Deployment) Build (Cross-Compile)
|
||||
---------------------------------------------------------
|
||||
|
@ -355,7 +384,7 @@ To build the most up-to-date version of the client:
|
|||
|
||||
.. note:: qtkeychain must be compiled with the same prefix e.g ``CMAKE_INSTALL_PREFIX=/Users/path/to/client/install/ .``
|
||||
|
||||
.. note:: Example:: ``cmake -DCMAKE_PREFIX_PATH=/usr/local/opt/qt5 -DCMAKE_INSTALL_PREFIX=/Users/path/to/client/install/``
|
||||
.. note:: Example:: ``cmake -DCMAKE_PREFIX_PATH=/usr/local/opt/qt6 -DCMAKE_INSTALL_PREFIX=/Users/path/to/client/install/``
|
||||
|
||||
4. Call ``make``.
|
||||
|
||||
|
@ -371,8 +400,7 @@ The following are known cmake parameters:
|
|||
You need to compile QtKeychain with the same Qt version.
|
||||
* ``WITH_DOC=TRUE``: Creates doc and manpages through running ``make``; also adds install statements,
|
||||
providing the ability to install using ``make install``.
|
||||
* ``CMAKE_PREFIX_PATH=/path/to/Qt5.2.0/5.2.0/yourarch/lib/cmake/``: Builds using Qt5.
|
||||
* ``BUILD_WITH_QT4=ON``: Builds using Qt4 (even if Qt5 is found).
|
||||
* ``CMAKE_PREFIX_PATH=/path/to/Qt6/6.7.0/yourarch/lib/cmake/``: Builds using Qt6.
|
||||
* ``CMAKE_INSTALL_PREFIX=path``: Set an install prefix. This is mandatory on Mac OS
|
||||
|
||||
Address Sanitizer
|
||||
|
|
|
@ -41,16 +41,16 @@ master_doc = 'index'
|
|||
|
||||
# General information about the project.
|
||||
project = u'Nextcloud Client Manual'
|
||||
copyright = u'2013-2023, The Nextcloud developers'
|
||||
copyright = u'2013-2024, The Nextcloud developers'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '3.11'
|
||||
version = '3.13'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '3.10.50'
|
||||
release = '3.12.50'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
|
|
@ -41,15 +41,17 @@ Some interesting values that can be set on the configuration file are:
|
|||
| ``chunkSize`` | ``10000000`` (10 MB) | Specifies the chunk size of uploaded files in bytes. |
|
||||
| | | The client will dynamically adjust this size within the maximum and minimum bounds (see below). |
|
||||
+----------------------------------+--------------------------+--------------------------------------------------------------------------------------------------------+
|
||||
| ``minChunkSize`` | ``1000000`` (1 MB) | Specifies the minimum chunk size of uploaded files in bytes. |
|
||||
| ``forceLoginV2`` | ``false`` | If the client should force the new login flow, eventhough some circumstances might need the old flow. |
|
||||
+----------------------------------+--------------------------+--------------------------------------------------------------------------------------------------------+
|
||||
| ``maxChunkSize`` | ``1000000000`` (1000 MB) | Specifies the maximum chunk size of uploaded files in bytes. |
|
||||
| ``minChunkSize`` | ``5000000`` (5 MB) | Specifies the minimum chunk size of uploaded files in bytes. |
|
||||
+----------------------------------+--------------------------+--------------------------------------------------------------------------------------------------------+
|
||||
| ``maxChunkSize`` | ``5000000000`` (5000 MB) | Specifies the maximum chunk size of uploaded files in bytes. |
|
||||
+----------------------------------+--------------------------+--------------------------------------------------------------------------------------------------------+
|
||||
| ``targetChunkUploadDuration`` | ``60000`` (1 minute) | Target duration in milliseconds for chunk uploads. |
|
||||
| | | The client adjusts the chunk size until each chunk upload takes approximately this long. |
|
||||
| | | Set to 0 to disable dynamic chunk sizing. |
|
||||
+----------------------------------+--------------------------+--------------------------------------------------------------------------------------------------------+
|
||||
| ``promptDeleteAllFiles`` | ``true`` | If a UI prompt should ask for confirmation if it was detected that all files and folders were deleted. |
|
||||
| ``promptDeleteAllFiles`` | ``false`` | If a UI prompt should ask for confirmation if it was detected that all files and folders were deleted. |
|
||||
+----------------------------------+--------------------------+--------------------------------------------------------------------------------------------------------+
|
||||
| ``timeout`` | ``300`` | The timeout for network connections in seconds. |
|
||||
+----------------------------------+--------------------------+--------------------------------------------------------------------------------------------------------+
|
||||
|
|
37
doc/faq.rst
37
doc/faq.rst
|
@ -1,15 +1,40 @@
|
|||
FAQ
|
||||
===
|
||||
|
||||
How the "Edit locally" functionality works when running the desktop client AppImage
|
||||
-----------------------------------------------------------------------------------
|
||||
How the "Edit locally" functionality works
|
||||
------------------------------------------
|
||||
This functionality depends on the desktop client ability to register the mime to handle the nc:// scheme. That is the handler used by the server to open a file locally. This will allow the desktop client to open a document with the local editor when you click on the option "Edit locally" in your Nextcloud instance.
|
||||
|
||||
This functionality depends on the desktop client ability to register the mime to handle the nc:// scheme. That is the handler used by the server to open a file locally.
|
||||
.. note::
|
||||
Without properly registering the mime, independent of the browser and distro being used, the desktop client will fail to open a document with the local editor when you click on the option "Edit locally" in your Nextcloud instance.
|
||||
|
||||
The browser will warn you of the failure: "Failed to launch 'nc://...' because the scheme does not have a registered handler."
|
||||
|
||||
We use AppImage due to its universal compatibility but to take full advantage of the desktop client features you will need a third part software to integrate the AppImage in your system.
|
||||
We have tested `AppImageLauncher <https://github.com/TheAssassin/AppImageLauncher>`_ and alternatively there is `Go AppImage <https://github.com/probonopd/go-appimage>`_.
|
||||
How to enable it
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Without it, independent of the browser and distro being used, the desktop client will fail to open a document with the local editor when you click on the option "Edit locally" in your Nextcloud instance.
|
||||
In order to do that, you need to install the desktop client with the MSI installer on Windows or use a third party software to integrate the AppImage in your system on Linux.
|
||||
|
||||
On Linux
|
||||
^^^^^^^^
|
||||
|
||||
We use AppImage due to its universal compatibility but to take full advantage of the desktop client features you will need a third part software to integrate the AppImage in your system: we have tested `AppImageLauncher <https://github.com/TheAssassin/AppImageLauncher>`_ and alternatively there is `Go AppImage <https://github.com/probonopd/go-appimage>`_.
|
||||
|
||||
On Windows
|
||||
^^^^^^^^^^
|
||||
|
||||
The MSI installer will alter your system registry to register the mime to handle the nc:// scheme.
|
||||
|
||||
Alternatively, you can manually register the mime to handle the nc:// scheme:
|
||||
|
||||
1. Save the following content to a .reg file:
|
||||
```
|
||||
Windows Registry Editor Version 5.00
|
||||
|
||||
[HKEY_CLASSES_ROOT\nc\shell\open\command]
|
||||
@="\"C:\\Program Files\\Nextcloud\\nextcloud.exe\" \"%1\""
|
||||
```
|
||||
2. Double click on the .reg file to import it into the registry.
|
||||
|
||||
See https://nextcloud.com/blog/nextcloud-office-release-solves-document-compatibility-overhauls-knowledge-management/ for more information.
|
||||
|
||||
|
|
|
@ -33,13 +33,13 @@ download page.
|
|||
System Requirements
|
||||
----------------------------------
|
||||
|
||||
- Windows 8.1+
|
||||
- macOS 12+ (64-bit only)
|
||||
- Linux
|
||||
- FreeBSD
|
||||
- Windows 10+ (64-bits only)
|
||||
- macOS 11.4+ (64-bits only)
|
||||
- Linux (ubuntu 22.04 or openSUSE 15.5 or ...) (64-bits only)
|
||||
|
||||
.. note::
|
||||
For Linux distributions, we support, if technically feasible, the latest 2 versions per platform and the previous LTS.
|
||||
For Linux distributions, we support, if technically feasible, the current LTS releases.
|
||||
For BSD, we support them if technically feasible but we do not test
|
||||
|
||||
Customizing the Windows Installation
|
||||
------------------------------------
|
||||
|
|
|
@ -23,5 +23,5 @@ Icon=@APPLICATION_EXECUTABLE@
|
|||
# Translations
|
||||
Name[da]=Skrivebordsklient til @APPLICATION_NAME@
|
||||
Comment[da]=Klient til @APPLICATION_NAME@-skrivebordssynkronisering
|
||||
GenericName[da]=Mappesynkronisering
|
||||
GenericName[da]=Mappe Synkronisering
|
||||
Icon[da]=@APPLICATION_ICON_NAME@
|
||||
|
|
|
@ -23,5 +23,5 @@ Icon=@APPLICATION_EXECUTABLE@
|
|||
# Translations
|
||||
Name[ro]=@Numele_aplicației@ Client de sincronizare pentru PC
|
||||
Comment[ro]=@APPLICATION_NAME@ client de sincronizare pentru desktop
|
||||
GenericName[ro]=Sincronizare director
|
||||
GenericName[ro]=Sincronizare folder
|
||||
Icon[ro]=@APPLICATION_ICON_NAME@
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<file>src/gui/filedetails/FileDetailsView.qml</file>
|
||||
<file>src/gui/filedetails/FileDetailsWindow.qml</file>
|
||||
<file>src/gui/filedetails/FileTag.qml</file>
|
||||
<file>src/gui/filedetails/NCInputDateField.qml</file>
|
||||
<file>src/gui/filedetails/NCInputTextEdit.qml</file>
|
||||
<file>src/gui/filedetails/NCInputTextField.qml</file>
|
||||
<file>src/gui/filedetails/NCTabButton.qml</file>
|
||||
|
@ -61,6 +62,11 @@
|
|||
<file>src/gui/ResolveConflictsDialog.qml</file>
|
||||
<file>src/gui/ConflictDelegate.qml</file>
|
||||
<file>src/gui/ConflictItemFileInfo.qml</file>
|
||||
<file>src/gui/filedetails/NCInputDateField.qml</file>
|
||||
<file>src/gui/macOS/ui/FileProviderSettings.qml</file>
|
||||
<file>src/gui/macOS/ui/FileProviderFileDelegate.qml</file>
|
||||
<file>src/gui/macOS/ui/FileProviderEvictionDialog.qml</file>
|
||||
<file>src/gui/macOS/ui/FileProviderSyncStatus.qml</file>
|
||||
<file>src/gui/macOS/ui/FileProviderStorageInfo.qml</file>
|
||||
<file>src/gui/macOS/ui/FileProviderFastEnumerationSettings.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
@ -11,11 +11,12 @@ if( UNIX AND NOT APPLE )
|
|||
endif()
|
||||
|
||||
if(BUILD_SHELL_INTEGRATION_DOLPHIN)
|
||||
find_package(KF5KIO "5.16")
|
||||
if(KF5KIO_FOUND)
|
||||
find_package(KF5KIO "5.16" CONFIG QUIET)
|
||||
find_package(KF6KIO "5.240" CONFIG QUIET)
|
||||
if(KF5KIO_FOUND OR KF6KIO_FOUND)
|
||||
add_subdirectory(dolphin)
|
||||
else()
|
||||
message("Dolphin plugin disabled: KDE Frameworks 5.16 not found")
|
||||
message("Dolphin plugin disabled: KDE Frameworks 5 and 6 not found")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
@ -35,7 +35,20 @@ if(APPLE)
|
|||
COMMENT building macOS File Provider extension
|
||||
VERBATIM)
|
||||
|
||||
add_dependencies(mac_overlayplugin mac_fileproviderplugin nextcloud) # for the ownCloud.icns to be generated
|
||||
add_custom_target( mac_fileprovideruiplugin ALL
|
||||
xcodebuild ARCHS=${CMAKE_OSX_ARCHITECTURES} ONLY_ACTIVE_ARCH=NO
|
||||
-project ${CMAKE_SOURCE_DIR}/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj
|
||||
-target FileProviderUIExt -configuration ${XCODE_TARGET_CONFIGURATION} "SYMROOT=${CMAKE_CURRENT_BINARY_DIR}"
|
||||
"OC_APPLICATION_EXECUTABLE_NAME=${APPLICATION_EXECUTABLE}"
|
||||
"OC_APPLICATION_VENDOR=${APPLICATION_VENDOR}"
|
||||
"OC_APPLICATION_NAME=${APPLICATION_NAME}"
|
||||
"OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}"
|
||||
"OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}"
|
||||
DEPENDS mac_fileproviderplugin
|
||||
COMMENT building macOS File Provider UI extension
|
||||
VERBATIM)
|
||||
|
||||
add_dependencies(mac_overlayplugin mac_fileproviderplugin mac_fileprovideruiplugin nextcloud) # for the ownCloud.icns to be generated
|
||||
else()
|
||||
add_dependencies(mac_overlayplugin nextcloud) # for the ownCloud.icns to be generated
|
||||
endif()
|
||||
|
@ -55,6 +68,10 @@ if(APPLE)
|
|||
install(DIRECTORY ${OSX_PLUGINS_BINARY_DIR}/FileProviderExt.appex
|
||||
DESTINATION ${OSX_PLUGINS_INSTALL_DIR}
|
||||
USE_SOURCE_PERMISSIONS)
|
||||
|
||||
install(DIRECTORY ${OSX_PLUGINS_BINARY_DIR}/FileProviderUIExt.appex
|
||||
DESTINATION ${OSX_PLUGINS_INSTALL_DIR}
|
||||
USE_SOURCE_PERMISSIONS)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
extension NextcloudFilesDatabaseManager {
|
||||
func directoryMetadata(account: String, serverUrl: String) -> NextcloudItemMetadataTable? {
|
||||
// We want to split by "/" (e.g. cloud.nc.com/files/a/b) but we need to be mindful of "https://c.nc.com"
|
||||
let problematicSeparator = "://"
|
||||
let placeholderSeparator = "__TEMP_REPLACE__"
|
||||
let serverUrlWithoutPrefix = serverUrl.replacingOccurrences(of: problematicSeparator, with: placeholderSeparator)
|
||||
var splitServerUrl = serverUrlWithoutPrefix.split(separator: "/")
|
||||
let directoryItemFileName = String(splitServerUrl.removeLast())
|
||||
let directoryItemServerUrl = splitServerUrl.joined(separator: "/").replacingOccurrences(of: placeholderSeparator, with: problematicSeparator)
|
||||
|
||||
if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND directory == true", account, directoryItemServerUrl, directoryItemFileName).first {
|
||||
return NextcloudItemMetadataTable(value: metadata)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func childItemsForDirectory(_ directoryMetadata: NextcloudItemMetadataTable) -> [NextcloudItemMetadataTable] {
|
||||
let directoryServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
|
||||
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("serverUrl BEGINSWITH %@", directoryServerUrl)
|
||||
return sortedItemMetadatas(metadatas)
|
||||
}
|
||||
|
||||
func childDirectoriesForDirectory(_ directoryMetadata: NextcloudItemMetadataTable) -> [NextcloudItemMetadataTable] {
|
||||
let directoryServerUrl = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
|
||||
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("serverUrl BEGINSWITH %@ AND directory == true", directoryServerUrl)
|
||||
return sortedItemMetadatas(metadatas)
|
||||
}
|
||||
|
||||
func parentDirectoryMetadataForItem(_ itemMetadata: NextcloudItemMetadataTable) -> NextcloudItemMetadataTable? {
|
||||
return directoryMetadata(account: itemMetadata.account, serverUrl: itemMetadata.serverUrl)
|
||||
}
|
||||
|
||||
func directoryMetadata(ocId: String) -> NextcloudItemMetadataTable? {
|
||||
if let metadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first {
|
||||
return NextcloudItemMetadataTable(value: metadata)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func directoryMetadatas(account: String) -> [NextcloudItemMetadataTable] {
|
||||
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND directory == true", account)
|
||||
return sortedItemMetadatas(metadatas)
|
||||
}
|
||||
|
||||
func directoryMetadatas(account: String, parentDirectoryServerUrl: String) -> [NextcloudItemMetadataTable] {
|
||||
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND parentDirectoryServerUrl == %@ AND directory == true", account, parentDirectoryServerUrl)
|
||||
return sortedItemMetadatas(metadatas)
|
||||
}
|
||||
|
||||
// Deletes all metadatas related to the info of the directory provided
|
||||
func deleteDirectoryAndSubdirectoriesMetadata(ocId: String) -> [NextcloudItemMetadataTable]? {
|
||||
let database = ncDatabase()
|
||||
guard let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first else {
|
||||
Logger.ncFilesDatabase.error("Could not find directory metadata for ocId \(ocId, privacy: .public). Not proceeding with deletion")
|
||||
return nil
|
||||
}
|
||||
|
||||
let directoryMetadataCopy = NextcloudItemMetadataTable(value: directoryMetadata)
|
||||
let directoryUrlPath = directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
|
||||
let directoryAccount = directoryMetadata.account
|
||||
let directoryEtag = directoryMetadata.etag
|
||||
|
||||
Logger.ncFilesDatabase.debug("Deleting root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: .public)")
|
||||
|
||||
guard deleteItemMetadata(ocId: directoryMetadata.ocId) else {
|
||||
Logger.ncFilesDatabase.debug("Failure to delete root directory metadata in recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: .public)")
|
||||
return nil
|
||||
}
|
||||
|
||||
var deletedMetadatas: [NextcloudItemMetadataTable] = [directoryMetadataCopy]
|
||||
|
||||
let results = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryAccount, directoryUrlPath)
|
||||
|
||||
for result in results {
|
||||
let successfulItemMetadataDelete = deleteItemMetadata(ocId: result.ocId)
|
||||
if (successfulItemMetadataDelete) {
|
||||
deletedMetadatas.append(NextcloudItemMetadataTable(value: result))
|
||||
}
|
||||
|
||||
if localFileMetadataFromOcId(result.ocId) != nil {
|
||||
deleteLocalFileMetadata(ocId: result.ocId)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.ncFilesDatabase.debug("Completed deletions in directory recursive delete. ocID: \(directoryMetadata.ocId, privacy: .public), etag: \(directoryEtag, privacy: .public), serverUrl: \(directoryUrlPath, privacy: .public)")
|
||||
|
||||
return deletedMetadatas
|
||||
}
|
||||
|
||||
func renameDirectoryAndPropagateToChildren(ocId: String, newServerUrl: String, newFileName: String) -> [NextcloudItemMetadataTable]? {
|
||||
|
||||
let database = ncDatabase()
|
||||
|
||||
guard let directoryMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@ AND directory == true", ocId).first else {
|
||||
Logger.ncFilesDatabase.error("Could not find a directory with ocID \(ocId, privacy: .public), cannot proceed with recursive renaming")
|
||||
return nil
|
||||
}
|
||||
|
||||
let oldItemServerUrl = directoryMetadata.serverUrl
|
||||
let oldDirectoryServerUrl = oldItemServerUrl + "/" + directoryMetadata.fileName
|
||||
let newDirectoryServerUrl = newServerUrl + "/" + newFileName
|
||||
let childItemResults = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, oldDirectoryServerUrl)
|
||||
|
||||
renameItemMetadata(ocId: ocId, newServerUrl: newServerUrl, newFileName: newFileName)
|
||||
Logger.ncFilesDatabase.debug("Renamed root renaming directory")
|
||||
|
||||
do {
|
||||
try database.write {
|
||||
for childItem in childItemResults {
|
||||
let oldServerUrl = childItem.serverUrl
|
||||
let movedServerUrl = oldServerUrl.replacingOccurrences(of: oldDirectoryServerUrl, with: newDirectoryServerUrl)
|
||||
childItem.serverUrl = movedServerUrl
|
||||
database.add(childItem, update: .all)
|
||||
Logger.ncFilesDatabase.debug("Moved childItem at \(oldServerUrl) to \(movedServerUrl)")
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
Logger.ncFilesDatabase.error("Could not rename directory metadata with ocId: \(ocId, privacy: .public) to new serverUrl: \(newServerUrl), received error: \(error.localizedDescription, privacy: .public)")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let updatedChildItemResults = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl BEGINSWITH %@", directoryMetadata.account, newDirectoryServerUrl)
|
||||
return sortedItemMetadatas(updatedChildItemResults)
|
||||
}
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import RealmSwift
|
||||
import OSLog
|
||||
|
||||
extension NextcloudFilesDatabaseManager {
|
||||
func localFileMetadataFromOcId(_ ocId: String) -> NextcloudLocalFileMetadataTable? {
|
||||
if let metadata = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId).first {
|
||||
return NextcloudLocalFileMetadataTable(value: metadata)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addLocalFileMetadataFromItemMetadata(_ itemMetadata: NextcloudItemMetadataTable) {
|
||||
let database = ncDatabase()
|
||||
|
||||
do {
|
||||
try database.write {
|
||||
let newLocalFileMetadata = NextcloudLocalFileMetadataTable()
|
||||
|
||||
newLocalFileMetadata.ocId = itemMetadata.ocId
|
||||
newLocalFileMetadata.fileName = itemMetadata.fileName
|
||||
newLocalFileMetadata.account = itemMetadata.account
|
||||
newLocalFileMetadata.etag = itemMetadata.etag
|
||||
newLocalFileMetadata.exifDate = Date()
|
||||
newLocalFileMetadata.exifLatitude = "-1"
|
||||
newLocalFileMetadata.exifLongitude = "-1"
|
||||
|
||||
database.add(newLocalFileMetadata, update: .all)
|
||||
Logger.ncFilesDatabase.debug("Added local file metadata from item metadata. ocID: \(itemMetadata.ocId, privacy: .public), etag: \(itemMetadata.etag, privacy: .public), fileName: \(itemMetadata.fileName, privacy: .public)")
|
||||
}
|
||||
} catch let error {
|
||||
Logger.ncFilesDatabase.error("Could not add local file metadata from item metadata. ocID: \(itemMetadata.ocId, privacy: .public), etag: \(itemMetadata.etag, privacy: .public), fileName: \(itemMetadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
func deleteLocalFileMetadata(ocId: String) {
|
||||
let database = ncDatabase()
|
||||
|
||||
do {
|
||||
try database.write {
|
||||
let results = database.objects(NextcloudLocalFileMetadataTable.self).filter("ocId == %@", ocId)
|
||||
database.delete(results)
|
||||
}
|
||||
} catch let error {
|
||||
Logger.ncFilesDatabase.error("Could not delete local file metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
private func sortedLocalFileMetadatas(_ metadatas: Results<NextcloudLocalFileMetadataTable>) -> [NextcloudLocalFileMetadataTable] {
|
||||
let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
|
||||
return Array(sortedMetadatas.map { NextcloudLocalFileMetadataTable(value: $0) })
|
||||
}
|
||||
|
||||
func localFileMetadatas(account: String) -> [NextcloudLocalFileMetadataTable] {
|
||||
let results = ncDatabase().objects(NextcloudLocalFileMetadataTable.self).filter("account == %@", account)
|
||||
return sortedLocalFileMetadatas(results)
|
||||
}
|
||||
|
||||
func localFileItemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
|
||||
let localFileMetadatas = localFileMetadatas(account: account)
|
||||
let localFileMetadatasOcIds = Array(localFileMetadatas.map { $0.ocId })
|
||||
|
||||
var itemMetadatas: [NextcloudItemMetadataTable] = []
|
||||
|
||||
for ocId in localFileMetadatasOcIds {
|
||||
guard let itemMetadata = itemMetadataFromOcId(ocId) else {
|
||||
Logger.ncFilesDatabase.error("Could not find matching item metadata for local file metadata with ocId: \(ocId, privacy: .public) with request from account: \(account)")
|
||||
continue;
|
||||
}
|
||||
|
||||
itemMetadatas.append(NextcloudItemMetadataTable(value: itemMetadata))
|
||||
}
|
||||
|
||||
return itemMetadatas
|
||||
}
|
||||
}
|
|
@ -1,326 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import RealmSwift
|
||||
import FileProvider
|
||||
import NextcloudKit
|
||||
import OSLog
|
||||
|
||||
class NextcloudFilesDatabaseManager : NSObject {
|
||||
static let shared = {
|
||||
return NextcloudFilesDatabaseManager();
|
||||
}()
|
||||
|
||||
let relativeDatabaseFolderPath = "Database/"
|
||||
let databaseFilename = "fileproviderextdatabase.realm"
|
||||
let relativeDatabaseFilePath: String
|
||||
var databasePath: URL?
|
||||
|
||||
let schemaVersion: UInt64 = 100
|
||||
|
||||
override init() {
|
||||
self.relativeDatabaseFilePath = self.relativeDatabaseFolderPath + self.databaseFilename
|
||||
|
||||
guard let fileProviderDataDirUrl = pathForFileProviderExtData() else {
|
||||
super.init()
|
||||
return
|
||||
}
|
||||
|
||||
self.databasePath = fileProviderDataDirUrl.appendingPathComponent(self.relativeDatabaseFilePath)
|
||||
|
||||
// Disable file protection for directory DB
|
||||
// https://docs.mongodb.com/realm/sdk/ios/examples/configure-and-open-a-realm/#std-label-ios-open-a-local-realm
|
||||
let dbFolder = fileProviderDataDirUrl.appendingPathComponent(self.relativeDatabaseFolderPath)
|
||||
let dbFolderPath = dbFolder.path
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: dbFolder, withIntermediateDirectories: true)
|
||||
try FileManager.default.setAttributes([FileAttributeKey.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], ofItemAtPath: dbFolderPath)
|
||||
} catch let error {
|
||||
Logger.ncFilesDatabase.error("Could not set permission level for File Provider database folder, received error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
||||
let config = Realm.Configuration(
|
||||
fileURL: self.databasePath,
|
||||
schemaVersion: self.schemaVersion,
|
||||
objectTypes: [NextcloudItemMetadataTable.self, NextcloudLocalFileMetadataTable.self]
|
||||
)
|
||||
|
||||
Realm.Configuration.defaultConfiguration = config
|
||||
|
||||
do {
|
||||
_ = try Realm()
|
||||
Logger.ncFilesDatabase.info("Successfully started Realm db for FileProviderExt")
|
||||
} catch let error as NSError {
|
||||
Logger.ncFilesDatabase.error("Error opening Realm db: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func ncDatabase() -> Realm {
|
||||
let realm = try! Realm()
|
||||
realm.refresh()
|
||||
return realm
|
||||
}
|
||||
|
||||
func anyItemMetadatasForAccount(_ account: String) -> Bool {
|
||||
return !ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account).isEmpty
|
||||
}
|
||||
|
||||
func itemMetadataFromOcId(_ ocId: String) -> NextcloudItemMetadataTable? {
|
||||
// Realm objects are live-fire, i.e. they will be changed and invalidated according to changes in the db
|
||||
// Let's therefore create a copy
|
||||
if let itemMetadata = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId).first {
|
||||
return NextcloudItemMetadataTable(value: itemMetadata)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sortedItemMetadatas(_ metadatas: Results<NextcloudItemMetadataTable>) -> [NextcloudItemMetadataTable] {
|
||||
let sortedMetadatas = metadatas.sorted(byKeyPath: "fileName", ascending: true)
|
||||
return Array(sortedMetadatas.map { NextcloudItemMetadataTable(value: $0) })
|
||||
}
|
||||
|
||||
func itemMetadatas(account: String) -> [NextcloudItemMetadataTable] {
|
||||
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@", account)
|
||||
return sortedItemMetadatas(metadatas)
|
||||
}
|
||||
|
||||
func itemMetadatas(account: String, serverUrl: String) -> [NextcloudItemMetadataTable] {
|
||||
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@", account, serverUrl)
|
||||
return sortedItemMetadatas(metadatas)
|
||||
}
|
||||
|
||||
func itemMetadatas(account: String, serverUrl: String, status: NextcloudItemMetadataTable.Status) -> [NextcloudItemMetadataTable] {
|
||||
let metadatas = ncDatabase().objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND status == %@", account, serverUrl, status.rawValue)
|
||||
return sortedItemMetadatas(metadatas)
|
||||
}
|
||||
|
||||
func itemMetadataFromFileProviderItemIdentifier(_ identifier: NSFileProviderItemIdentifier) -> NextcloudItemMetadataTable? {
|
||||
let ocId = identifier.rawValue
|
||||
return itemMetadataFromOcId(ocId)
|
||||
}
|
||||
|
||||
private func processItemMetadatasToDelete(existingMetadatas: Results<NextcloudItemMetadataTable>,
|
||||
updatedMetadatas: [NextcloudItemMetadataTable]) -> [NextcloudItemMetadataTable] {
|
||||
|
||||
var deletedMetadatas: [NextcloudItemMetadataTable] = []
|
||||
|
||||
for existingMetadata in existingMetadatas {
|
||||
guard !updatedMetadatas.contains(where: { $0.ocId == existingMetadata.ocId }),
|
||||
let metadataToDelete = itemMetadataFromOcId(existingMetadata.ocId) else { continue }
|
||||
|
||||
deletedMetadatas.append(metadataToDelete)
|
||||
|
||||
Logger.ncFilesDatabase.debug("Deleting item metadata during update. ocID: \(existingMetadata.ocId, privacy: .public), etag: \(existingMetadata.etag, privacy: .public), fileName: \(existingMetadata.fileName, privacy: .public)")
|
||||
}
|
||||
|
||||
return deletedMetadatas
|
||||
}
|
||||
|
||||
private func processItemMetadatasToUpdate(existingMetadatas: Results<NextcloudItemMetadataTable>,
|
||||
updatedMetadatas: [NextcloudItemMetadataTable],
|
||||
updateDirectoryEtags: Bool) -> (newMetadatas: [NextcloudItemMetadataTable], updatedMetadatas: [NextcloudItemMetadataTable], directoriesNeedingRename: [NextcloudItemMetadataTable]) {
|
||||
|
||||
var returningNewMetadatas: [NextcloudItemMetadataTable] = []
|
||||
var returningUpdatedMetadatas: [NextcloudItemMetadataTable] = []
|
||||
var directoriesNeedingRename: [NextcloudItemMetadataTable] = []
|
||||
|
||||
for updatedMetadata in updatedMetadatas {
|
||||
if let existingMetadata = existingMetadatas.first(where: { $0.ocId == updatedMetadata.ocId }) {
|
||||
|
||||
if existingMetadata.status == NextcloudItemMetadataTable.Status.normal.rawValue &&
|
||||
!existingMetadata.isInSameDatabaseStoreableRemoteState(updatedMetadata) {
|
||||
|
||||
if updatedMetadata.directory {
|
||||
|
||||
if updatedMetadata.serverUrl != existingMetadata.serverUrl || updatedMetadata.fileName != existingMetadata.fileName {
|
||||
|
||||
directoriesNeedingRename.append(NextcloudItemMetadataTable(value: updatedMetadata))
|
||||
updatedMetadata.etag = "" // Renaming doesn't change the etag so reset manually
|
||||
|
||||
} else if !updateDirectoryEtags {
|
||||
updatedMetadata.etag = existingMetadata.etag
|
||||
}
|
||||
}
|
||||
|
||||
returningUpdatedMetadatas.append(updatedMetadata)
|
||||
|
||||
|
||||
Logger.ncFilesDatabase.debug("Updated existing item metadata. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)")
|
||||
} else {
|
||||
Logger.ncFilesDatabase.debug("Skipping item metadata update; same as existing, or still downloading/uploading. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)")
|
||||
}
|
||||
|
||||
} else { // This is a new metadata
|
||||
if !updateDirectoryEtags && updatedMetadata.directory {
|
||||
updatedMetadata.etag = ""
|
||||
}
|
||||
|
||||
returningNewMetadatas.append(updatedMetadata)
|
||||
|
||||
Logger.ncFilesDatabase.debug("Created new item metadata during update. ocID: \(updatedMetadata.ocId, privacy: .public), etag: \(updatedMetadata.etag, privacy: .public), fileName: \(updatedMetadata.fileName, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
return (returningNewMetadatas, returningUpdatedMetadatas, directoriesNeedingRename)
|
||||
}
|
||||
|
||||
func updateItemMetadatas(account: String, serverUrl: String, updatedMetadatas: [NextcloudItemMetadataTable], updateDirectoryEtags: Bool) -> (newMetadatas: [NextcloudItemMetadataTable]?, updatedMetadatas: [NextcloudItemMetadataTable]?, deletedMetadatas: [NextcloudItemMetadataTable]?) {
|
||||
let database = ncDatabase()
|
||||
|
||||
do {
|
||||
let existingMetadatas = database.objects(NextcloudItemMetadataTable.self).filter("account == %@ AND serverUrl == %@ AND status == %@", account, serverUrl, NextcloudItemMetadataTable.Status.normal.rawValue)
|
||||
|
||||
let metadatasToDelete = processItemMetadatasToDelete(existingMetadatas: existingMetadatas,
|
||||
updatedMetadatas: updatedMetadatas)
|
||||
|
||||
let metadatasToChange = processItemMetadatasToUpdate(existingMetadatas: existingMetadatas,
|
||||
updatedMetadatas: updatedMetadatas,
|
||||
updateDirectoryEtags: updateDirectoryEtags)
|
||||
|
||||
var metadatasToUpdate = metadatasToChange.updatedMetadatas
|
||||
let metadatasToCreate = metadatasToChange.newMetadatas
|
||||
let directoriesNeedingRename = metadatasToChange.directoriesNeedingRename
|
||||
|
||||
let metadatasToAdd = Array(metadatasToUpdate.map { NextcloudItemMetadataTable(value: $0) }) +
|
||||
Array(metadatasToCreate.map { NextcloudItemMetadataTable(value: $0) })
|
||||
|
||||
for metadata in directoriesNeedingRename {
|
||||
|
||||
if let updatedDirectoryChildren = renameDirectoryAndPropagateToChildren(ocId: metadata.ocId, newServerUrl: metadata.serverUrl, newFileName: metadata.fileName) {
|
||||
metadatasToUpdate += updatedDirectoryChildren
|
||||
}
|
||||
}
|
||||
|
||||
try database.write {
|
||||
for metadata in metadatasToDelete {
|
||||
// Can't pass copies, we need the originals from the database
|
||||
database.delete(ncDatabase().objects(NextcloudItemMetadataTable.self).filter("ocId == %@", metadata.ocId))
|
||||
}
|
||||
|
||||
for metadata in metadatasToAdd {
|
||||
database.add(metadata, update: .all)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return (newMetadatas: metadatasToCreate, updatedMetadatas: metadatasToUpdate, deletedMetadatas: metadatasToDelete)
|
||||
} catch let error {
|
||||
Logger.ncFilesDatabase.error("Could not update any item metadatas, received error: \(error.localizedDescription, privacy: .public)")
|
||||
return (nil, nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func setStatusForItemMetadata(_ metadata: NextcloudItemMetadataTable, status: NextcloudItemMetadataTable.Status, completionHandler: @escaping(_ updatedMetadata: NextcloudItemMetadataTable?) -> Void) {
|
||||
let database = ncDatabase()
|
||||
|
||||
do {
|
||||
try database.write {
|
||||
guard let result = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", metadata.ocId).first else {
|
||||
Logger.ncFilesDatabase.debug("Did not update status for item metadata as it was not found. ocID: \(metadata.ocId, privacy: .public)")
|
||||
return
|
||||
}
|
||||
|
||||
result.status = status.rawValue
|
||||
database.add(result, update: .all)
|
||||
Logger.ncFilesDatabase.debug("Updated status for item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)")
|
||||
|
||||
completionHandler(NextcloudItemMetadataTable(value: result))
|
||||
}
|
||||
} catch let error {
|
||||
Logger.ncFilesDatabase.error("Could not update status for item metadata with ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func addItemMetadata(_ metadata: NextcloudItemMetadataTable) {
|
||||
let database = ncDatabase()
|
||||
|
||||
do {
|
||||
try database.write {
|
||||
database.add(metadata, update: .all)
|
||||
Logger.ncFilesDatabase.debug("Added item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)")
|
||||
}
|
||||
} catch let error {
|
||||
Logger.ncFilesDatabase.error("Could not add item metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult func deleteItemMetadata(ocId: String) -> Bool {
|
||||
let database = ncDatabase()
|
||||
|
||||
do {
|
||||
try database.write {
|
||||
let results = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId)
|
||||
|
||||
Logger.ncFilesDatabase.debug("Deleting item metadata. \(ocId, privacy: .public)")
|
||||
database.delete(results)
|
||||
}
|
||||
|
||||
return true
|
||||
} catch let error {
|
||||
Logger.ncFilesDatabase.error("Could not delete item metadata with ocId: \(ocId, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func renameItemMetadata(ocId: String, newServerUrl: String, newFileName: String) {
|
||||
let database = ncDatabase()
|
||||
|
||||
do {
|
||||
try database.write {
|
||||
guard let itemMetadata = database.objects(NextcloudItemMetadataTable.self).filter("ocId == %@", ocId).first else {
|
||||
Logger.ncFilesDatabase.debug("Could not find an item with ocID \(ocId, privacy: .public) to rename to \(newFileName, privacy: .public)")
|
||||
return
|
||||
}
|
||||
|
||||
let oldFileName = itemMetadata.fileName
|
||||
let oldServerUrl = itemMetadata.serverUrl
|
||||
|
||||
itemMetadata.fileName = newFileName
|
||||
itemMetadata.fileNameView = newFileName
|
||||
itemMetadata.serverUrl = newServerUrl
|
||||
|
||||
database.add(itemMetadata, update: .all)
|
||||
|
||||
Logger.ncFilesDatabase.debug("Renamed item \(oldFileName, privacy: .public) to \(newFileName, privacy: .public), moved from serverUrl: \(oldServerUrl, privacy: .public) to serverUrl: \(newServerUrl, privacy: .public)")
|
||||
}
|
||||
} catch let error {
|
||||
Logger.ncFilesDatabase.error("Could not rename filename of item metadata with ocID: \(ocId, privacy: .public) to proposed name \(newFileName, privacy: .public) at proposed serverUrl \(newServerUrl, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
func parentItemIdentifierFromMetadata(_ metadata: NextcloudItemMetadataTable) -> NSFileProviderItemIdentifier? {
|
||||
let homeServerFilesUrl = metadata.urlBase + "/remote.php/dav/files/" + metadata.userId
|
||||
|
||||
if metadata.serverUrl == homeServerFilesUrl {
|
||||
return .rootContainer
|
||||
}
|
||||
|
||||
guard let itemParentDirectory = parentDirectoryMetadataForItem(metadata) else {
|
||||
Logger.ncFilesDatabase.error("Could not get item parent directory metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)")
|
||||
return nil
|
||||
}
|
||||
|
||||
if let parentDirectoryMetadata = itemMetadataFromOcId(itemParentDirectory.ocId) {
|
||||
return NSFileProviderItemIdentifier(parentDirectoryMetadata.ocId)
|
||||
}
|
||||
|
||||
Logger.ncFilesDatabase.error("Could not get item parent directory item metadata for metadata. ocID: \(metadata.ocId, privacy: .public), etag: \(metadata.etag, privacy: .public), fileName: \(metadata.fileName, privacy: .public)")
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import NextcloudKit
|
||||
|
||||
extension NextcloudItemMetadataTable {
|
||||
static func fromNKFile(_ file: NKFile, account: String) -> NextcloudItemMetadataTable {
|
||||
let metadata = NextcloudItemMetadataTable()
|
||||
|
||||
metadata.account = account
|
||||
metadata.checksums = file.checksums
|
||||
metadata.commentsUnread = file.commentsUnread
|
||||
metadata.contentType = file.contentType
|
||||
if let date = file.creationDate {
|
||||
metadata.creationDate = date as Date
|
||||
} else {
|
||||
metadata.creationDate = file.date as Date
|
||||
}
|
||||
metadata.dataFingerprint = file.dataFingerprint
|
||||
metadata.date = file.date as Date
|
||||
metadata.directory = file.directory
|
||||
metadata.downloadURL = file.downloadURL
|
||||
metadata.e2eEncrypted = file.e2eEncrypted
|
||||
metadata.etag = file.etag
|
||||
metadata.favorite = file.favorite
|
||||
metadata.fileId = file.fileId
|
||||
metadata.fileName = file.fileName
|
||||
metadata.fileNameView = file.fileName
|
||||
metadata.hasPreview = file.hasPreview
|
||||
metadata.iconName = file.iconName
|
||||
metadata.mountType = file.mountType
|
||||
metadata.name = file.name
|
||||
metadata.note = file.note
|
||||
metadata.ocId = file.ocId
|
||||
metadata.ownerId = file.ownerId
|
||||
metadata.ownerDisplayName = file.ownerDisplayName
|
||||
metadata.lock = file.lock
|
||||
metadata.lockOwner = file.lockOwner
|
||||
metadata.lockOwnerEditor = file.lockOwnerEditor
|
||||
metadata.lockOwnerType = file.lockOwnerType
|
||||
metadata.lockOwnerDisplayName = file.lockOwnerDisplayName
|
||||
metadata.lockTime = file.lockTime
|
||||
metadata.lockTimeOut = file.lockTimeOut
|
||||
metadata.path = file.path
|
||||
metadata.permissions = file.permissions
|
||||
metadata.quotaUsedBytes = file.quotaUsedBytes
|
||||
metadata.quotaAvailableBytes = file.quotaAvailableBytes
|
||||
metadata.richWorkspace = file.richWorkspace
|
||||
metadata.resourceType = file.resourceType
|
||||
metadata.serverUrl = file.serverUrl
|
||||
metadata.sharePermissionsCollaborationServices = file.sharePermissionsCollaborationServices
|
||||
for element in file.sharePermissionsCloudMesh {
|
||||
metadata.sharePermissionsCloudMesh.append(element)
|
||||
}
|
||||
for element in file.shareType {
|
||||
metadata.shareType.append(element)
|
||||
}
|
||||
metadata.size = file.size
|
||||
metadata.classFile = file.classFile
|
||||
//FIXME: iOS 12.0,* don't detect UTI text/markdown, text/x-markdown
|
||||
if (metadata.contentType == "text/markdown" || metadata.contentType == "text/x-markdown") && metadata.classFile == NKCommon.TypeClassFile.unknow.rawValue {
|
||||
metadata.classFile = NKCommon.TypeClassFile.document.rawValue
|
||||
}
|
||||
if let date = file.uploadDate {
|
||||
metadata.uploadDate = date as Date
|
||||
} else {
|
||||
metadata.uploadDate = file.date as Date
|
||||
}
|
||||
metadata.urlBase = file.urlBase
|
||||
metadata.user = file.user
|
||||
metadata.userId = file.userId
|
||||
|
||||
// Support for finding the correct filename for e2ee files should go here
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
static func metadatasFromDirectoryReadNKFiles(_ files: [NKFile],
|
||||
account: String,
|
||||
completionHandler: @escaping (_ directoryMetadata: NextcloudItemMetadataTable,
|
||||
_ childDirectoriesMetadatas: [NextcloudItemMetadataTable],
|
||||
_ metadatas: [NextcloudItemMetadataTable]) -> Void) {
|
||||
|
||||
var directoryMetadataSet = false
|
||||
var directoryMetadata = NextcloudItemMetadataTable()
|
||||
var childDirectoriesMetadatas: [NextcloudItemMetadataTable] = []
|
||||
var metadatas: [NextcloudItemMetadataTable] = []
|
||||
|
||||
let conversionQueue = DispatchQueue(label: "nkFileToMetadataConversionQueue", qos: .userInitiated, attributes: .concurrent)
|
||||
let appendQueue = DispatchQueue(label: "metadataAppendQueue", qos: .userInitiated) // Serial queue
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for file in files {
|
||||
if metadatas.isEmpty && !directoryMetadataSet {
|
||||
let metadata = NextcloudItemMetadataTable.fromNKFile(file, account: account)
|
||||
directoryMetadata = metadata;
|
||||
directoryMetadataSet = true;
|
||||
} else {
|
||||
conversionQueue.async(group: dispatchGroup) {
|
||||
let metadata = NextcloudItemMetadataTable.fromNKFile(file, account: account)
|
||||
|
||||
appendQueue.async(group: dispatchGroup) {
|
||||
metadatas.append(metadata)
|
||||
if metadata.directory {
|
||||
childDirectoriesMetadatas.append(metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: DispatchQueue.main) {
|
||||
completionHandler(directoryMetadata, childDirectoriesMetadatas, metadatas)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,213 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import RealmSwift
|
||||
import FileProvider
|
||||
import NextcloudKit
|
||||
|
||||
class NextcloudItemMetadataTable: Object {
|
||||
enum Status: Int {
|
||||
case downloadError = -4
|
||||
case downloading = -3
|
||||
case inDownload = -2
|
||||
case waitDownload = -1
|
||||
|
||||
case normal = 0
|
||||
|
||||
case waitUpload = 1
|
||||
case inUpload = 2
|
||||
case uploading = 3
|
||||
case uploadError = 4
|
||||
}
|
||||
|
||||
enum SharePermissions: Int {
|
||||
case readShare = 1
|
||||
case updateShare = 2
|
||||
case createShare = 4
|
||||
case deleteShare = 8
|
||||
case shareShare = 16
|
||||
|
||||
case maxFileShare = 19
|
||||
case maxFolderShare = 31
|
||||
}
|
||||
|
||||
@Persisted(primaryKey: true) var ocId: String
|
||||
@Persisted var account = ""
|
||||
@Persisted var assetLocalIdentifier = ""
|
||||
@Persisted var checksums = ""
|
||||
@Persisted var chunk: Bool = false
|
||||
@Persisted var classFile = ""
|
||||
@Persisted var commentsUnread: Bool = false
|
||||
@Persisted var contentType = ""
|
||||
@Persisted var creationDate = Date()
|
||||
@Persisted var dataFingerprint = ""
|
||||
@Persisted var date = Date()
|
||||
@Persisted var directory: Bool = false
|
||||
@Persisted var deleteAssetLocalIdentifier: Bool = false
|
||||
@Persisted var downloadURL = ""
|
||||
@Persisted var e2eEncrypted: Bool = false
|
||||
@Persisted var edited: Bool = false
|
||||
@Persisted var etag = ""
|
||||
@Persisted var etagResource = ""
|
||||
@Persisted var favorite: Bool = false
|
||||
@Persisted var fileId = ""
|
||||
@Persisted var fileName = ""
|
||||
@Persisted var fileNameView = ""
|
||||
@Persisted var hasPreview: Bool = false
|
||||
@Persisted var iconName = ""
|
||||
@Persisted var iconUrl = ""
|
||||
@Persisted var isExtractFile: Bool = false
|
||||
@Persisted var livePhoto: Bool = false
|
||||
@Persisted var mountType = ""
|
||||
@Persisted var name = "" // for unifiedSearch is the provider.id
|
||||
@Persisted var note = ""
|
||||
@Persisted var ownerId = ""
|
||||
@Persisted var ownerDisplayName = ""
|
||||
@Persisted var lock = false
|
||||
@Persisted var lockOwner = ""
|
||||
@Persisted var lockOwnerEditor = ""
|
||||
@Persisted var lockOwnerType = 0
|
||||
@Persisted var lockOwnerDisplayName = ""
|
||||
@Persisted var lockTime: Date?
|
||||
@Persisted var lockTimeOut: Date?
|
||||
@Persisted var path = ""
|
||||
@Persisted var permissions = ""
|
||||
@Persisted var quotaUsedBytes: Int64 = 0
|
||||
@Persisted var quotaAvailableBytes: Int64 = 0
|
||||
@Persisted var resourceType = ""
|
||||
@Persisted var richWorkspace: String?
|
||||
@Persisted var serverUrl = "" // For parent directory!!
|
||||
@Persisted var session = ""
|
||||
@Persisted var sessionError = ""
|
||||
@Persisted var sessionSelector = ""
|
||||
@Persisted var sessionTaskIdentifier: Int = 0
|
||||
@Persisted var sharePermissionsCollaborationServices: Int = 0
|
||||
let sharePermissionsCloudMesh = List<String>() // TODO: Find a way to compare these in remote state check
|
||||
let shareType = List<Int>()
|
||||
@Persisted var size: Int64 = 0
|
||||
@Persisted var status: Int = 0
|
||||
@Persisted var subline: String?
|
||||
@Persisted var trashbinFileName = ""
|
||||
@Persisted var trashbinOriginalLocation = ""
|
||||
@Persisted var trashbinDeletionTime = Date()
|
||||
@Persisted var uploadDate = Date()
|
||||
@Persisted var url = ""
|
||||
@Persisted var urlBase = ""
|
||||
@Persisted var user = ""
|
||||
@Persisted var userId = ""
|
||||
|
||||
var fileExtension: String {
|
||||
(fileNameView as NSString).pathExtension
|
||||
}
|
||||
|
||||
var fileNoExtension: String {
|
||||
(fileNameView as NSString).deletingPathExtension
|
||||
}
|
||||
|
||||
var isRenameable: Bool {
|
||||
return lock
|
||||
}
|
||||
|
||||
var isPrintable: Bool {
|
||||
if isDocumentViewableOnly {
|
||||
return false
|
||||
}
|
||||
if ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/") || classFile == NKCommon.TypeClassFile.image.rawValue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var isDocumentViewableOnly: Bool {
|
||||
return sharePermissionsCollaborationServices == SharePermissions.readShare.rawValue &&
|
||||
classFile == NKCommon.TypeClassFile.document.rawValue
|
||||
}
|
||||
|
||||
var isCopyableInPasteboard: Bool {
|
||||
!isDocumentViewableOnly && !directory
|
||||
}
|
||||
|
||||
var isModifiableWithQuickLook: Bool {
|
||||
if directory || isDocumentViewableOnly {
|
||||
return false
|
||||
}
|
||||
return contentType == "com.adobe.pdf" || contentType == "application/pdf" || classFile == NKCommon.TypeClassFile.image.rawValue
|
||||
}
|
||||
|
||||
var isSettableOnOffline: Bool {
|
||||
return session.isEmpty && !isDocumentViewableOnly
|
||||
}
|
||||
|
||||
var canOpenIn: Bool {
|
||||
return session.isEmpty && !isDocumentViewableOnly && !directory
|
||||
}
|
||||
|
||||
var isDownloadUpload: Bool {
|
||||
return status == Status.inDownload.rawValue ||
|
||||
status == Status.downloading.rawValue ||
|
||||
status == Status.inUpload.rawValue ||
|
||||
status == Status.uploading.rawValue
|
||||
}
|
||||
|
||||
var isDownload: Bool {
|
||||
status == Status.inDownload.rawValue || status == Status.downloading.rawValue
|
||||
}
|
||||
|
||||
var isUpload: Bool {
|
||||
status == Status.inUpload.rawValue || status == Status.uploading.rawValue
|
||||
}
|
||||
|
||||
override func isEqual(_ object: Any?) -> Bool {
|
||||
if let object = object as? NextcloudItemMetadataTable {
|
||||
return self.fileId == object.fileId &&
|
||||
self.account == object.account &&
|
||||
self.path == object.path &&
|
||||
self.fileName == object.fileName
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func isInSameDatabaseStoreableRemoteState(_ comparingMetadata: NextcloudItemMetadataTable) -> Bool {
|
||||
return comparingMetadata.etag == self.etag &&
|
||||
comparingMetadata.fileNameView == self.fileNameView &&
|
||||
comparingMetadata.date == self.date &&
|
||||
comparingMetadata.permissions == self.permissions &&
|
||||
comparingMetadata.hasPreview == self.hasPreview &&
|
||||
comparingMetadata.note == self.note &&
|
||||
comparingMetadata.lock == self.lock &&
|
||||
comparingMetadata.sharePermissionsCollaborationServices == self.sharePermissionsCollaborationServices &&
|
||||
comparingMetadata.favorite == self.favorite
|
||||
}
|
||||
|
||||
/// Returns false if the user is lokced out of the file. I.e. The file is locked but by someone else
|
||||
func canUnlock(as user: String) -> Bool {
|
||||
return !lock || (lockOwner == user && lockOwnerType == 0)
|
||||
}
|
||||
|
||||
func thumbnailUrl(size: CGSize) -> URL? {
|
||||
guard hasPreview else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let urlBase = urlBase.urlEncoded!
|
||||
let webdavUrl = urlBase + NextcloudAccount.webDavFilesUrlSuffix + user // Leave the leading slash
|
||||
let serverFileRelativeUrl = serverUrl.replacingOccurrences(of: webdavUrl, with: "") + "/" + fileName
|
||||
|
||||
let urlString = "\(urlBase)/index.php/core/preview.png?file=\(serverFileRelativeUrl)&x=\(size.width)&y=\(size.height)&a=1&mode=cover"
|
||||
|
||||
return URL(string: urlString)
|
||||
}
|
||||
}
|
|
@ -17,11 +17,31 @@ import OSLog
|
|||
extension Logger {
|
||||
private static var subsystem = Bundle.main.bundleIdentifier!
|
||||
|
||||
static let desktopClientConnection = Logger(subsystem: subsystem, category: "desktopclientconnection")
|
||||
static let enumeration = Logger(subsystem: subsystem, category: "enumeration")
|
||||
static let fileProviderExtension = Logger(subsystem: subsystem, category: "fileproviderextension")
|
||||
static let fileTransfer = Logger(subsystem: subsystem, category: "filetransfer")
|
||||
static let localFileOps = Logger(subsystem: subsystem, category: "localfileoperations")
|
||||
static let ncFilesDatabase = Logger(subsystem: subsystem, category: "nextcloudfilesdatabase")
|
||||
static let materialisedFileHandling = Logger(subsystem: subsystem, category: "materialisedfilehandling")
|
||||
static let desktopClientConnection = Logger(
|
||||
subsystem: subsystem, category: "desktopclientconnection")
|
||||
static let fpUiExtensionService = Logger(subsystem: subsystem, category: "fpUiExtensionService")
|
||||
static let fileProviderExtension = Logger(
|
||||
subsystem: subsystem, category: "fileproviderextension")
|
||||
static let shares = Logger(subsystem: subsystem, category: "shares")
|
||||
static let logger = Logger(subsystem: subsystem, category: "logger")
|
||||
|
||||
@available(macOSApplicationExtension 12.0, *)
|
||||
static func logEntries(interval: TimeInterval = -3600) -> (Array<String>?, Error?) {
|
||||
do {
|
||||
let logStore = try OSLogStore(scope: .currentProcessIdentifier)
|
||||
let timeDate = Date().addingTimeInterval(interval)
|
||||
let logPosition = logStore.position(date: timeDate)
|
||||
let entries = try logStore.getEntries(at: logPosition)
|
||||
|
||||
return (entries
|
||||
.compactMap { $0 as? OSLogEntryLog }
|
||||
.filter { $0.subsystem == Logger.subsystem }
|
||||
.map { $0.composedMessage }, nil)
|
||||
|
||||
} catch let error {
|
||||
Logger.logger.error("Could not acquire os log store: \(error)");
|
||||
return (nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import FileProvider
|
||||
import NextcloudKit
|
||||
|
||||
extension NKError {
|
||||
static var noChangesErrorCode: Int {
|
||||
return -200
|
||||
}
|
||||
|
||||
var isCouldntConnectError: Bool {
|
||||
return errorCode == -9999 ||
|
||||
errorCode == -1001 ||
|
||||
errorCode == -1004 ||
|
||||
errorCode == -1005 ||
|
||||
errorCode == -1009 ||
|
||||
errorCode == -1012 ||
|
||||
errorCode == -1200 ||
|
||||
errorCode == -1202 ||
|
||||
errorCode == 500 ||
|
||||
errorCode == 503 ||
|
||||
errorCode == 200
|
||||
}
|
||||
|
||||
var isUnauthenticatedError: Bool {
|
||||
return errorCode == -1013
|
||||
}
|
||||
|
||||
var isGoingOverQuotaError: Bool {
|
||||
return errorCode == 507
|
||||
}
|
||||
|
||||
var isNotFoundError: Bool {
|
||||
return errorCode == 404
|
||||
}
|
||||
|
||||
var isNoChangesError: Bool {
|
||||
return errorCode == NKError.noChangesErrorCode
|
||||
}
|
||||
|
||||
var fileProviderError: NSFileProviderError {
|
||||
if isNotFoundError {
|
||||
return NSFileProviderError(.noSuchItem)
|
||||
} else if isCouldntConnectError {
|
||||
// Provide something the file provider can do something with
|
||||
return NSFileProviderError(.serverUnreachable)
|
||||
} else if isUnauthenticatedError {
|
||||
return NSFileProviderError(.notAuthenticated)
|
||||
} else if isGoingOverQuotaError {
|
||||
return NSFileProviderError(.insufficientQuota)
|
||||
} else {
|
||||
return NSFileProviderError(.cannotSynchronize)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
|
||||
extension Progress {
|
||||
func setHandlersFromAfRequest(_ request: Request) {
|
||||
self.cancellationHandler = { request.cancel() }
|
||||
self.pausingHandler = { request.suspend() }
|
||||
self.resumingHandler = { request.resume() }
|
||||
}
|
||||
|
||||
func copyCurrentStateToProgress(_ otherProgress: Progress, includeHandlers: Bool = false) {
|
||||
if includeHandlers {
|
||||
otherProgress.cancellationHandler = self.cancellationHandler
|
||||
otherProgress.pausingHandler = self.pausingHandler
|
||||
otherProgress.resumingHandler = self.resumingHandler
|
||||
}
|
||||
|
||||
otherProgress.totalUnitCount = self.totalUnitCount
|
||||
otherProgress.completedUnitCount = self.completedUnitCount
|
||||
otherProgress.estimatedTimeRemaining = self.estimatedTimeRemaining
|
||||
otherProgress.localizedDescription = self.localizedAdditionalDescription
|
||||
otherProgress.localizedAdditionalDescription = self.localizedAdditionalDescription
|
||||
otherProgress.isCancellable = self.isCancellable
|
||||
otherProgress.isPausable = self.isPausable
|
||||
otherProgress.fileCompletedCount = self.fileCompletedCount
|
||||
otherProgress.fileURL = self.fileURL
|
||||
otherProgress.fileTotalCount = self.fileTotalCount
|
||||
otherProgress.fileCompletedCount = self.fileCompletedCount
|
||||
otherProgress.fileOperationKind = self.fileOperationKind
|
||||
otherProgress.kind = self.kind
|
||||
otherProgress.throughput = self.throughput
|
||||
|
||||
for (key, object) in self.userInfo {
|
||||
otherProgress.setUserInfoObject(object, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func copyOfCurrentState(includeHandlers: Bool = false) -> Progress {
|
||||
let newProgress = Progress()
|
||||
copyCurrentStateToProgress(newProgress, includeHandlers: includeHandlers)
|
||||
return newProgress
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// FileProviderConfig.swift
|
||||
// FileProviderExt
|
||||
//
|
||||
// Created by Claudio Cambra on 5/2/24.
|
||||
//
|
||||
|
||||
import FileProvider
|
||||
import Foundation
|
||||
|
||||
struct FileProviderConfig {
|
||||
private enum ConfigKey: String {
|
||||
case fastEnumerationEnabled = "fastEnumerationEnabled"
|
||||
}
|
||||
|
||||
let domainIdentifier: NSFileProviderDomainIdentifier
|
||||
|
||||
private var internalConfig: [String: Any] {
|
||||
get {
|
||||
let defaults = UserDefaults.standard
|
||||
if let settings = defaults.dictionary(forKey: domainIdentifier.rawValue) {
|
||||
return settings
|
||||
}
|
||||
let dictionary: [String: Any] = [:]
|
||||
defaults.setValue(dictionary, forKey: domainIdentifier.rawValue)
|
||||
return dictionary
|
||||
}
|
||||
set {
|
||||
let defaults = UserDefaults.standard
|
||||
defaults.setValue(newValue, forKey: domainIdentifier.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
var fastEnumerationEnabled: Bool {
|
||||
get { internalConfig[ConfigKey.fastEnumerationEnabled.rawValue] as? Bool ?? true }
|
||||
set { internalConfig[ConfigKey.fastEnumerationEnabled.rawValue] = newValue }
|
||||
}
|
||||
|
||||
lazy var fastEnumerationSet = internalConfig[ConfigKey.fastEnumerationEnabled.rawValue] != nil
|
||||
}
|
|
@ -1,313 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
import FileProvider
|
||||
import NextcloudKit
|
||||
import OSLog
|
||||
|
||||
extension FileProviderEnumerator {
|
||||
func fullRecursiveScan(ncAccount: NextcloudAccount,
|
||||
ncKit: NextcloudKit,
|
||||
scanChangesOnly: Bool,
|
||||
completionHandler: @escaping(_ metadatas: [NextcloudItemMetadataTable],
|
||||
_ newMetadatas: [NextcloudItemMetadataTable],
|
||||
_ updatedMetadatas: [NextcloudItemMetadataTable],
|
||||
_ deletedMetadatas: [NextcloudItemMetadataTable],
|
||||
_ error: NKError?) -> Void) {
|
||||
|
||||
let rootContainerDirectoryMetadata = NextcloudItemMetadataTable()
|
||||
rootContainerDirectoryMetadata.directory = true
|
||||
rootContainerDirectoryMetadata.ocId = NSFileProviderItemIdentifier.rootContainer.rawValue
|
||||
|
||||
// Create a serial dispatch queue
|
||||
let dispatchQueue = DispatchQueue(label: "recursiveChangeEnumerationQueue", qos: .userInitiated)
|
||||
|
||||
dispatchQueue.async {
|
||||
let results = self.scanRecursively(rootContainerDirectoryMetadata,
|
||||
ncAccount: ncAccount,
|
||||
ncKit: ncKit,
|
||||
scanChangesOnly: scanChangesOnly)
|
||||
|
||||
// Run a check to ensure files deleted in one location are not updated in another (e.g. when moved)
|
||||
// The recursive scan provides us with updated/deleted metadatas only on a folder by folder basis;
|
||||
// so we need to check we are not simultaneously marking a moved file as deleted and updated
|
||||
var checkedDeletedMetadatas = results.deletedMetadatas
|
||||
|
||||
for updatedMetadata in results.updatedMetadatas {
|
||||
guard let matchingDeletedMetadataIdx = checkedDeletedMetadatas.firstIndex(where: { $0.ocId == updatedMetadata.ocId } ) else {
|
||||
continue;
|
||||
}
|
||||
|
||||
checkedDeletedMetadatas.remove(at: matchingDeletedMetadataIdx)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(results.metadatas, results.newMetadatas, results.updatedMetadatas, checkedDeletedMetadatas, results.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func scanRecursively(_ directoryMetadata: NextcloudItemMetadataTable,
|
||||
ncAccount: NextcloudAccount,
|
||||
ncKit: NextcloudKit,
|
||||
scanChangesOnly: Bool) -> (metadatas: [NextcloudItemMetadataTable],
|
||||
newMetadatas: [NextcloudItemMetadataTable],
|
||||
updatedMetadatas: [NextcloudItemMetadataTable],
|
||||
deletedMetadatas: [NextcloudItemMetadataTable],
|
||||
error: NKError?) {
|
||||
|
||||
if self.isInvalidated {
|
||||
return ([], [], [], [], nil)
|
||||
}
|
||||
|
||||
assert(directoryMetadata.directory, "Can only recursively scan a directory.")
|
||||
|
||||
// Will include results of recursive calls
|
||||
var allMetadatas: [NextcloudItemMetadataTable] = []
|
||||
var allNewMetadatas: [NextcloudItemMetadataTable] = []
|
||||
var allUpdatedMetadatas: [NextcloudItemMetadataTable] = []
|
||||
var allDeletedMetadatas: [NextcloudItemMetadataTable] = []
|
||||
|
||||
let dbManager = NextcloudFilesDatabaseManager.shared
|
||||
let dispatchGroup = DispatchGroup() // TODO: Maybe own thread?
|
||||
|
||||
dispatchGroup.enter()
|
||||
|
||||
var criticalError: NKError?
|
||||
let itemServerUrl = directoryMetadata.ocId == NSFileProviderItemIdentifier.rootContainer.rawValue ?
|
||||
ncAccount.davFilesUrl : directoryMetadata.serverUrl + "/" + directoryMetadata.fileName
|
||||
|
||||
Logger.enumeration.debug("About to read: \(itemServerUrl, privacy: .public)")
|
||||
|
||||
FileProviderEnumerator.readServerUrl(itemServerUrl, ncAccount: ncAccount, ncKit: ncKit, stopAtMatchingEtags: scanChangesOnly) { metadatas, newMetadatas, updatedMetadatas, deletedMetadatas, readError in
|
||||
|
||||
if readError != nil {
|
||||
let nkReadError = NKError(error: readError!)
|
||||
|
||||
// Is the error is that we have found matching etags on this item, then ignore it
|
||||
// if we are doing a full rescan
|
||||
guard nkReadError.isNoChangesError && scanChangesOnly else {
|
||||
Logger.enumeration.error("Finishing enumeration of changes at \(itemServerUrl, privacy: .public) with \(readError!.localizedDescription, privacy: .public)")
|
||||
|
||||
if nkReadError.isNotFoundError {
|
||||
Logger.enumeration.info("404 error means item no longer exists. Deleting metadata and reporting as deletion without error")
|
||||
|
||||
if let deletedMetadatas = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: directoryMetadata.ocId) {
|
||||
allDeletedMetadatas += deletedMetadatas
|
||||
} else {
|
||||
Logger.enumeration.error("An error occurred while trying to delete directory and children not found in recursive scan")
|
||||
}
|
||||
|
||||
} else if nkReadError.isNoChangesError { // All is well, just no changed etags
|
||||
Logger.enumeration.info("Error was to say no changed files -- not bad error. No need to check children.")
|
||||
|
||||
} else if nkReadError.isUnauthenticatedError || nkReadError.isCouldntConnectError {
|
||||
// If it is a critical error then stop, if not then continue
|
||||
Logger.enumeration.error("Error will affect next enumerated items, so stopping enumeration.")
|
||||
criticalError = nkReadError
|
||||
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Logger.enumeration.info("Finished reading serverUrl: \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)")
|
||||
|
||||
if let metadatas = metadatas {
|
||||
allMetadatas += metadatas
|
||||
} else {
|
||||
Logger.enumeration.warning("WARNING: Nil metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)")
|
||||
}
|
||||
|
||||
if let newMetadatas = newMetadatas {
|
||||
allNewMetadatas += newMetadatas
|
||||
} else {
|
||||
Logger.enumeration.warning("WARNING: Nil new metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)")
|
||||
}
|
||||
|
||||
if let updatedMetadatas = updatedMetadatas {
|
||||
allUpdatedMetadatas += updatedMetadatas
|
||||
} else {
|
||||
Logger.enumeration.warning("WARNING: Nil updated metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)")
|
||||
}
|
||||
|
||||
if let deletedMetadatas = deletedMetadatas {
|
||||
allDeletedMetadatas += deletedMetadatas
|
||||
} else {
|
||||
Logger.enumeration.warning("WARNING: Nil deleted metadatas received for reading of changes at \(itemServerUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)")
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
dispatchGroup.wait()
|
||||
|
||||
guard criticalError == nil else {
|
||||
return ([], [], [], [], error: criticalError)
|
||||
}
|
||||
|
||||
var childDirectoriesToScan: [NextcloudItemMetadataTable] = []
|
||||
var candidateMetadatas: [NextcloudItemMetadataTable]
|
||||
|
||||
if scanChangesOnly {
|
||||
candidateMetadatas = allUpdatedMetadatas + allNewMetadatas
|
||||
} else {
|
||||
candidateMetadatas = allMetadatas
|
||||
}
|
||||
|
||||
for candidateMetadata in candidateMetadatas {
|
||||
if candidateMetadata.directory {
|
||||
childDirectoriesToScan.append(candidateMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
if childDirectoriesToScan.isEmpty {
|
||||
return (metadatas: allMetadatas, newMetadatas: allNewMetadatas, updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil)
|
||||
}
|
||||
|
||||
for childDirectory in childDirectoriesToScan {
|
||||
let childScanResult = scanRecursively(childDirectory,
|
||||
ncAccount: ncAccount,
|
||||
ncKit: ncKit,
|
||||
scanChangesOnly: scanChangesOnly)
|
||||
|
||||
allMetadatas += childScanResult.metadatas
|
||||
allNewMetadatas += childScanResult.newMetadatas
|
||||
allUpdatedMetadatas += childScanResult.updatedMetadatas
|
||||
allDeletedMetadatas += childScanResult.deletedMetadatas
|
||||
}
|
||||
|
||||
return (metadatas: allMetadatas, newMetadatas: allNewMetadatas, updatedMetadatas: allUpdatedMetadatas, deletedMetadatas: allDeletedMetadatas, nil)
|
||||
}
|
||||
|
||||
static func handleDepth1ReadFileOrFolder(serverUrl: String,
|
||||
ncAccount: NextcloudAccount,
|
||||
files: [NKFile],
|
||||
error: NKError,
|
||||
completionHandler: @escaping (_ metadatas: [NextcloudItemMetadataTable]?,
|
||||
_ newMetadatas: [NextcloudItemMetadataTable]?,
|
||||
_ updatedMetadatas: [NextcloudItemMetadataTable]?,
|
||||
_ deletedMetadatas: [NextcloudItemMetadataTable]?,
|
||||
_ readError: Error?) -> Void) {
|
||||
|
||||
guard error == .success else {
|
||||
Logger.enumeration.error("1 depth readFileOrFolder of url: \(serverUrl, privacy: .public) did not complete successfully, received error: \(error.errorDescription, privacy: .public)")
|
||||
completionHandler(nil, nil, nil, nil, error.error)
|
||||
return
|
||||
}
|
||||
|
||||
Logger.enumeration.debug("Starting async conversion of NKFiles for serverUrl: \(serverUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)")
|
||||
|
||||
let dbManager = NextcloudFilesDatabaseManager.shared
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
NextcloudItemMetadataTable.metadatasFromDirectoryReadNKFiles(files, account: ncAccount.ncKitAccount) { directoryMetadata, childDirectoriesMetadata, metadatas in
|
||||
|
||||
// STORE DATA FOR CURRENTLY SCANNED DIRECTORY
|
||||
// We have now scanned this directory's contents, so update with etag in order to not check again if not needed
|
||||
// unless it's the root container
|
||||
if serverUrl != ncAccount.davFilesUrl {
|
||||
dbManager.addItemMetadata(directoryMetadata)
|
||||
}
|
||||
|
||||
// Don't update the etags for folders as we haven't checked their contents.
|
||||
// When we do a recursive check, if we update the etags now, we will think
|
||||
// that our local copies are up to date -- instead, leave them as the old.
|
||||
// They will get updated when they are the subject of a readServerUrl call.
|
||||
// (See above)
|
||||
let changedMetadatas = dbManager.updateItemMetadatas(account: ncAccount.ncKitAccount, serverUrl: serverUrl, updatedMetadatas: metadatas, updateDirectoryEtags: false)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completionHandler(metadatas, changedMetadatas.newMetadatas, changedMetadatas.updatedMetadatas, changedMetadatas.deletedMetadatas, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func readServerUrl(_ serverUrl: String,
|
||||
ncAccount: NextcloudAccount,
|
||||
ncKit: NextcloudKit,
|
||||
stopAtMatchingEtags: Bool = false,
|
||||
depth: String = "1",
|
||||
completionHandler: @escaping (_ metadatas: [NextcloudItemMetadataTable]?,
|
||||
_ newMetadatas: [NextcloudItemMetadataTable]?,
|
||||
_ updatedMetadatas: [NextcloudItemMetadataTable]?,
|
||||
_ deletedMetadatas: [NextcloudItemMetadataTable]?,
|
||||
_ readError: Error?) -> Void) {
|
||||
|
||||
let dbManager = NextcloudFilesDatabaseManager.shared
|
||||
let ncKitAccount = ncAccount.ncKitAccount
|
||||
|
||||
Logger.enumeration.debug("Starting to read serverUrl: \(serverUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public) at depth \(depth, privacy: .public). NCKit info: userId: \(ncKit.nkCommonInstance.user, privacy: .public), password is empty: \(ncKit.nkCommonInstance.password == "" ? "EMPTY PASSWORD" : "NOT EMPTY PASSWORD"), urlBase: \(ncKit.nkCommonInstance.urlBase, privacy: .public), ncVersion: \(ncKit.nkCommonInstance.nextcloudVersion, privacy: .public)")
|
||||
|
||||
ncKit.readFileOrFolder(serverUrlFileName: serverUrl, depth: depth, showHiddenFiles: true) { _, files, _, error in
|
||||
guard error == .success else {
|
||||
Logger.enumeration.error("\(depth, privacy: .public) depth readFileOrFolder of url: \(serverUrl, privacy: .public) did not complete successfully, received error: \(error.errorDescription, privacy: .public)")
|
||||
completionHandler(nil, nil, nil, nil, error.error)
|
||||
return
|
||||
}
|
||||
|
||||
guard let receivedFile = files.first else {
|
||||
Logger.enumeration.error("Received no items from readFileOrFolder of \(serverUrl, privacy: .public), not much we can do...")
|
||||
completionHandler(nil, nil, nil, nil, error.error)
|
||||
return
|
||||
}
|
||||
|
||||
guard receivedFile.directory else {
|
||||
Logger.enumeration.debug("Read item is a file. Converting NKfile for serverUrl: \(serverUrl, privacy: .public) for user: \(ncAccount.ncKitAccount, privacy: .public)")
|
||||
let itemMetadata = NextcloudItemMetadataTable.fromNKFile(receivedFile, account: ncKitAccount)
|
||||
dbManager.addItemMetadata(itemMetadata) // TODO: Return some value when it is an update
|
||||
completionHandler([itemMetadata], nil, nil, nil, error.error)
|
||||
return
|
||||
}
|
||||
|
||||
if stopAtMatchingEtags,
|
||||
let directoryMetadata = dbManager.directoryMetadata(account: ncKitAccount, serverUrl: serverUrl) {
|
||||
|
||||
let directoryEtag = directoryMetadata.etag
|
||||
|
||||
guard directoryEtag == "" || directoryEtag != receivedFile.etag else {
|
||||
Logger.enumeration.debug("Read server url called with flag to stop enumerating at matching etags. Returning and providing soft error.")
|
||||
|
||||
let description = "Fetched directory etag is same as that stored locally. Not fetching child items."
|
||||
let nkError = NKError(errorCode: NKError.noChangesErrorCode, errorDescription: description)
|
||||
|
||||
let metadatas = dbManager.itemMetadatas(account: ncKitAccount, serverUrl: serverUrl)
|
||||
|
||||
completionHandler(metadatas, nil, nil, nil, nkError.error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if depth == "0" {
|
||||
if serverUrl != ncAccount.davFilesUrl {
|
||||
let metadata = NextcloudItemMetadataTable.fromNKFile(receivedFile, account: ncKitAccount)
|
||||
let isNew = dbManager.itemMetadataFromOcId(metadata.ocId) == nil
|
||||
let updatedMetadatas = isNew ? [] : [metadata]
|
||||
let newMetadatas = isNew ? [metadata] : []
|
||||
|
||||
dbManager.addItemMetadata(metadata)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
completionHandler([metadata], newMetadatas, updatedMetadatas, nil, nil)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleDepth1ReadFileOrFolder(serverUrl: serverUrl, ncAccount: ncAccount, files: files, error: error, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,352 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
import FileProvider
|
||||
import NextcloudKit
|
||||
import OSLog
|
||||
|
||||
class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
|
||||
|
||||
private let enumeratedItemIdentifier: NSFileProviderItemIdentifier
|
||||
private var enumeratedItemMetadata: NextcloudItemMetadataTable?
|
||||
private var enumeratingSystemIdentifier: Bool {
|
||||
return FileProviderEnumerator.isSystemIdentifier(enumeratedItemIdentifier)
|
||||
}
|
||||
private let anchor = NSFileProviderSyncAnchor(Date().description.data(using: .utf8)!) // TODO: actually use this in NCKit and server requests
|
||||
private static let maxItemsPerFileProviderPage = 100
|
||||
let ncAccount: NextcloudAccount
|
||||
let ncKit: NextcloudKit
|
||||
var serverUrl: String = ""
|
||||
var isInvalidated = false
|
||||
|
||||
private static func isSystemIdentifier(_ identifier: NSFileProviderItemIdentifier) -> Bool {
|
||||
return identifier == .rootContainer ||
|
||||
identifier == .trashContainer ||
|
||||
identifier == .workingSet
|
||||
}
|
||||
|
||||
init(enumeratedItemIdentifier: NSFileProviderItemIdentifier, ncAccount: NextcloudAccount, ncKit: NextcloudKit) {
|
||||
self.enumeratedItemIdentifier = enumeratedItemIdentifier
|
||||
self.ncAccount = ncAccount
|
||||
self.ncKit = ncKit
|
||||
|
||||
if FileProviderEnumerator.isSystemIdentifier(enumeratedItemIdentifier) {
|
||||
Logger.enumeration.debug("Providing enumerator for a system defined container: \(enumeratedItemIdentifier.rawValue, privacy: .public)")
|
||||
self.serverUrl = ncAccount.davFilesUrl
|
||||
} else {
|
||||
Logger.enumeration.debug("Providing enumerator for item with identifier: \(enumeratedItemIdentifier.rawValue, privacy: .public)")
|
||||
let dbManager = NextcloudFilesDatabaseManager.shared
|
||||
|
||||
enumeratedItemMetadata = dbManager.itemMetadataFromFileProviderItemIdentifier(enumeratedItemIdentifier)
|
||||
if enumeratedItemMetadata != nil {
|
||||
self.serverUrl = enumeratedItemMetadata!.serverUrl + "/" + enumeratedItemMetadata!.fileName
|
||||
} else {
|
||||
Logger.enumeration.error("Could not find itemMetadata for file with identifier: \(enumeratedItemIdentifier.rawValue, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
Logger.enumeration.info("Set up enumerator for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)")
|
||||
super.init()
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
Logger.enumeration.debug("Enumerator is being invalidated for item with identifier: \(self.enumeratedItemIdentifier.rawValue, privacy: .public)")
|
||||
self.isInvalidated = true
|
||||
}
|
||||
|
||||
// MARK: - Protocol methods
|
||||
|
||||
func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) {
|
||||
Logger.enumeration.debug("Received enumerate items request for enumerator with user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)")
|
||||
/*
|
||||
- inspect the page to determine whether this is an initial or a follow-up request (TODO)
|
||||
|
||||
If this is an enumerator for a directory, the root container or all directories:
|
||||
- perform a server request to fetch directory contents
|
||||
If this is an enumerator for the working set:
|
||||
- perform a server request to update your local database
|
||||
- fetch the working set from your local database
|
||||
|
||||
- inform the observer about the items returned by the server (possibly multiple times)
|
||||
- inform the observer that you are finished with this page
|
||||
*/
|
||||
|
||||
if enumeratedItemIdentifier == .trashContainer {
|
||||
Logger.enumeration.debug("Enumerating trash set for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)")
|
||||
// TODO!
|
||||
|
||||
observer.finishEnumerating(upTo: nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle the working set as if it were the root container
|
||||
// If we do a full server scan per the recommendations of the File Provider documentation,
|
||||
// we will be stuck for a huge period of time without being able to access files as the
|
||||
// entire server gets scanned. Instead, treat the working set as the root container here.
|
||||
// Then, when we enumerate changes, we'll go through everything -- while we can still
|
||||
// navigate a little bit in Finder, file picker, etc
|
||||
|
||||
guard serverUrl != "" else {
|
||||
Logger.enumeration.error("Enumerator has empty serverUrl -- can't enumerate that! For identifier: \(self.enumeratedItemIdentifier.rawValue, privacy: .public)")
|
||||
observer.finishEnumeratingWithError(NSFileProviderError(.noSuchItem))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Make better use of pagination and handle paging properly
|
||||
if page == NSFileProviderPage.initialPageSortedByDate as NSFileProviderPage ||
|
||||
page == NSFileProviderPage.initialPageSortedByName as NSFileProviderPage {
|
||||
|
||||
Logger.enumeration.debug("Enumerating initial page for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)")
|
||||
|
||||
FileProviderEnumerator.readServerUrl(serverUrl, ncAccount: ncAccount, ncKit: ncKit) { metadatas, _, _, _, readError in
|
||||
|
||||
guard readError == nil else {
|
||||
Logger.enumeration.error("Finishing enumeration for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public) with error \(readError!.localizedDescription, privacy: .public)")
|
||||
|
||||
let nkReadError = NKError(error: readError!)
|
||||
observer.finishEnumeratingWithError(nkReadError.fileProviderError)
|
||||
return
|
||||
}
|
||||
|
||||
guard let metadatas = metadatas else {
|
||||
Logger.enumeration.error("Finishing enumeration for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public) with invalid metadatas.")
|
||||
observer.finishEnumeratingWithError(NSFileProviderError(.cannotSynchronize))
|
||||
return
|
||||
}
|
||||
|
||||
Logger.enumeration.info("Finished reading serverUrl: \(self.serverUrl, privacy: .public) for user: \(self.ncAccount.ncKitAccount, privacy: .public). Processed \(metadatas.count) metadatas")
|
||||
|
||||
FileProviderEnumerator.completeEnumerationObserver(observer, ncKit: self.ncKit, numPage: 1, itemMetadatas: metadatas)
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let numPage = Int(String(data: page.rawValue, encoding: .utf8)!)!
|
||||
Logger.enumeration.debug("Enumerating page \(numPage, privacy: .public) for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)")
|
||||
// TODO: Handle paging properly
|
||||
// FileProviderEnumerator.completeObserver(observer, ncKit: ncKit, numPage: numPage, itemMetadatas: nil)
|
||||
observer.finishEnumerating(upTo: nil)
|
||||
}
|
||||
|
||||
func enumerateChanges(for observer: NSFileProviderChangeObserver, from anchor: NSFileProviderSyncAnchor) {
|
||||
Logger.enumeration.debug("Received enumerate changes request for enumerator for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)")
|
||||
/*
|
||||
- query the server for updates since the passed-in sync anchor (TODO)
|
||||
|
||||
If this is an enumerator for the working set:
|
||||
- note the changes in your local database
|
||||
|
||||
- inform the observer about item deletions and updates (modifications + insertions)
|
||||
- inform the observer when you have finished enumerating up to a subsequent sync anchor
|
||||
*/
|
||||
|
||||
if enumeratedItemIdentifier == .workingSet {
|
||||
Logger.enumeration.debug("Enumerating changes in working set for user: \(self.ncAccount.ncKitAccount, privacy: .public)")
|
||||
|
||||
// Unlike when enumerating items we can't progressively enumerate items as we need to wait to resolve which items are truly deleted and which
|
||||
// have just been moved elsewhere.
|
||||
fullRecursiveScan(ncAccount: self.ncAccount,
|
||||
ncKit: self.ncKit,
|
||||
scanChangesOnly: true) { _, newMetadatas, updatedMetadatas, deletedMetadatas, error in
|
||||
|
||||
if self.isInvalidated {
|
||||
Logger.enumeration.info("Enumerator invalidated during working set change scan. For user: \(self.ncAccount.ncKitAccount, privacy: .public)")
|
||||
observer.finishEnumeratingWithError(NSFileProviderError(.cannotSynchronize))
|
||||
return
|
||||
}
|
||||
|
||||
guard error == nil else {
|
||||
Logger.enumeration.info("Finished recursive change enumeration of working set for user: \(self.ncAccount.ncKitAccount, privacy: .public) with error: \(error!.errorDescription, privacy: .public)")
|
||||
observer.finishEnumeratingWithError(error!.fileProviderError)
|
||||
return
|
||||
}
|
||||
|
||||
Logger.enumeration.info("Finished recursive change enumeration of working set for user: \(self.ncAccount.ncKitAccount, privacy: .public). Enumerating items.")
|
||||
|
||||
FileProviderEnumerator.completeChangesObserver(observer,
|
||||
anchor: anchor,
|
||||
ncKit: self.ncKit,
|
||||
newMetadatas: newMetadatas,
|
||||
updatedMetadatas: updatedMetadatas,
|
||||
deletedMetadatas: deletedMetadatas)
|
||||
}
|
||||
return
|
||||
} else if enumeratedItemIdentifier == .trashContainer {
|
||||
Logger.enumeration.debug("Enumerating changes in trash set for user: \(self.ncAccount.ncKitAccount, privacy: .public)")
|
||||
// TODO!
|
||||
|
||||
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
|
||||
return
|
||||
}
|
||||
|
||||
Logger.enumeration.info("Enumerating changes for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public)")
|
||||
|
||||
// No matter what happens here we finish enumeration in some way, either from the error
|
||||
// handling below or from the completeChangesObserver
|
||||
// TODO: Move to the sync engine extension
|
||||
FileProviderEnumerator.readServerUrl(serverUrl, ncAccount: ncAccount, ncKit: ncKit, stopAtMatchingEtags: true) { _, newMetadatas, updatedMetadatas, deletedMetadatas, readError in
|
||||
|
||||
// If we get a 404 we might add more deleted metadatas
|
||||
var currentDeletedMetadatas: [NextcloudItemMetadataTable] = []
|
||||
if let notNilDeletedMetadatas = deletedMetadatas {
|
||||
currentDeletedMetadatas = notNilDeletedMetadatas
|
||||
}
|
||||
|
||||
guard readError == nil else {
|
||||
Logger.enumeration.error("Finishing enumeration of changes for user: \(self.ncAccount.ncKitAccount, privacy: .public) with serverUrl: \(self.serverUrl, privacy: .public) with error: \(readError!.localizedDescription, privacy: .public)")
|
||||
|
||||
let nkReadError = NKError(error: readError!)
|
||||
let fpError = nkReadError.fileProviderError
|
||||
|
||||
if nkReadError.isNotFoundError {
|
||||
Logger.enumeration.info("404 error means item no longer exists. Deleting metadata and reporting \(self.serverUrl, privacy: .public) as deletion without error")
|
||||
|
||||
guard let itemMetadata = self.enumeratedItemMetadata else {
|
||||
Logger.enumeration.error("Invalid enumeratedItemMetadata, could not delete metadata nor report deletion")
|
||||
observer.finishEnumeratingWithError(fpError)
|
||||
return
|
||||
}
|
||||
|
||||
let dbManager = NextcloudFilesDatabaseManager.shared
|
||||
if itemMetadata.directory {
|
||||
if let deletedDirectoryMetadatas = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: itemMetadata.ocId) {
|
||||
currentDeletedMetadatas += deletedDirectoryMetadatas
|
||||
} else {
|
||||
Logger.enumeration.error("Something went wrong when recursively deleting directory not found.")
|
||||
}
|
||||
} else {
|
||||
dbManager.deleteItemMetadata(ocId: itemMetadata.ocId)
|
||||
}
|
||||
|
||||
FileProviderEnumerator.completeChangesObserver(observer, anchor: anchor, ncKit: self.ncKit, newMetadatas: nil, updatedMetadatas: nil, deletedMetadatas: [itemMetadata])
|
||||
return
|
||||
} else if nkReadError.isNoChangesError { // All is well, just no changed etags
|
||||
Logger.enumeration.info("Error was to say no changed files -- not bad error. Finishing change enumeration.")
|
||||
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
|
||||
return;
|
||||
}
|
||||
|
||||
observer.finishEnumeratingWithError(fpError)
|
||||
return
|
||||
}
|
||||
|
||||
Logger.enumeration.info("Finished reading serverUrl: \(self.serverUrl, privacy: .public) for user: \(self.ncAccount.ncKitAccount, privacy: .public)")
|
||||
|
||||
FileProviderEnumerator.completeChangesObserver(observer, anchor: anchor, ncKit: self.ncKit, newMetadatas: newMetadatas, updatedMetadatas: updatedMetadatas, deletedMetadatas: deletedMetadatas)
|
||||
}
|
||||
}
|
||||
|
||||
func currentSyncAnchor(completionHandler: @escaping (NSFileProviderSyncAnchor?) -> Void) {
|
||||
completionHandler(anchor)
|
||||
}
|
||||
|
||||
// MARK: - Helper methods
|
||||
|
||||
private static func metadatasToFileProviderItems(_ itemMetadatas: [NextcloudItemMetadataTable], ncKit: NextcloudKit, completionHandler: @escaping(_ items: [NSFileProviderItem]) -> Void) {
|
||||
var items: [NSFileProviderItem] = []
|
||||
|
||||
let conversionQueue = DispatchQueue(label: "metadataToItemConversionQueue", qos: .userInitiated, attributes: .concurrent)
|
||||
let appendQueue = DispatchQueue(label: "enumeratorItemAppendQueue", qos: .userInitiated) // Serial queue
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
for itemMetadata in itemMetadatas {
|
||||
conversionQueue.async(group: dispatchGroup) {
|
||||
if itemMetadata.e2eEncrypted {
|
||||
Logger.enumeration.info("Skipping encrypted metadata in enumeration: \(itemMetadata.ocId, privacy: .public) \(itemMetadata.fileName, privacy: .public)")
|
||||
return
|
||||
}
|
||||
|
||||
if let parentItemIdentifier = NextcloudFilesDatabaseManager.shared.parentItemIdentifierFromMetadata(itemMetadata) {
|
||||
let item = FileProviderItem(metadata: itemMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: ncKit)
|
||||
Logger.enumeration.debug("Will enumerate item with ocId: \(itemMetadata.ocId, privacy: .public) and name: \(itemMetadata.fileName, privacy: .public)")
|
||||
|
||||
appendQueue.async(group: dispatchGroup) {
|
||||
items.append(item)
|
||||
}
|
||||
} else {
|
||||
Logger.enumeration.error("Could not get valid parentItemIdentifier for item with ocId: \(itemMetadata.ocId, privacy: .public) and name: \(itemMetadata.fileName, privacy: .public), skipping enumeration")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: DispatchQueue.main) {
|
||||
completionHandler(items)
|
||||
}
|
||||
}
|
||||
|
||||
private static func fileProviderPageforNumPage(_ numPage: Int) -> NSFileProviderPage {
|
||||
return NSFileProviderPage("\(numPage)".data(using: .utf8)!)
|
||||
}
|
||||
|
||||
private static func completeEnumerationObserver(_ observer: NSFileProviderEnumerationObserver, ncKit: NextcloudKit, numPage: Int, itemMetadatas: [NextcloudItemMetadataTable]) {
|
||||
|
||||
metadatasToFileProviderItems(itemMetadatas, ncKit: ncKit) { items in
|
||||
observer.didEnumerate(items)
|
||||
Logger.enumeration.info("Did enumerate \(items.count) items")
|
||||
|
||||
// TODO: Handle paging properly
|
||||
/*
|
||||
if items.count == maxItemsPerFileProviderPage {
|
||||
let nextPage = numPage + 1
|
||||
let providerPage = NSFileProviderPage("\(nextPage)".data(using: .utf8)!)
|
||||
observer.finishEnumerating(upTo: providerPage)
|
||||
} else {
|
||||
observer.finishEnumerating(upTo: nil)
|
||||
}
|
||||
*/
|
||||
observer.finishEnumerating(upTo: fileProviderPageforNumPage(numPage))
|
||||
}
|
||||
}
|
||||
|
||||
private static func completeChangesObserver(_ observer: NSFileProviderChangeObserver, anchor: NSFileProviderSyncAnchor, ncKit: NextcloudKit, newMetadatas: [NextcloudItemMetadataTable]?, updatedMetadatas: [NextcloudItemMetadataTable]?, deletedMetadatas: [NextcloudItemMetadataTable]?) {
|
||||
|
||||
guard newMetadatas != nil || updatedMetadatas != nil || deletedMetadatas != nil else {
|
||||
Logger.enumeration.error("Received invalid newMetadatas, updatedMetadatas or deletedMetadatas. Finished enumeration of changes with error.")
|
||||
observer.finishEnumeratingWithError(NSFileProviderError(.noSuchItem))
|
||||
return
|
||||
}
|
||||
|
||||
// Observer does not care about new vs updated, so join
|
||||
var allUpdatedMetadatas: [NextcloudItemMetadataTable] = []
|
||||
var allDeletedMetadatas: [NextcloudItemMetadataTable] = []
|
||||
|
||||
if let newMetadatas = newMetadatas {
|
||||
allUpdatedMetadatas += newMetadatas
|
||||
}
|
||||
|
||||
if let updatedMetadatas = updatedMetadatas {
|
||||
allUpdatedMetadatas += updatedMetadatas
|
||||
}
|
||||
|
||||
if let deletedMetadatas = deletedMetadatas {
|
||||
allDeletedMetadatas = deletedMetadatas
|
||||
}
|
||||
|
||||
let allFpItemDeletionsIdentifiers = Array(allDeletedMetadatas.map { NSFileProviderItemIdentifier($0.ocId) })
|
||||
if !allFpItemDeletionsIdentifiers.isEmpty {
|
||||
observer.didDeleteItems(withIdentifiers: allFpItemDeletionsIdentifiers)
|
||||
}
|
||||
|
||||
metadatasToFileProviderItems(allUpdatedMetadatas, ncKit: ncKit) { updatedItems in
|
||||
|
||||
if !updatedItems.isEmpty {
|
||||
observer.didUpdate(updatedItems)
|
||||
}
|
||||
|
||||
Logger.enumeration.info("Processed \(updatedItems.count) new or updated metadatas, \(allDeletedMetadatas.count) deleted metadatas.")
|
||||
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,18 +12,9 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import RealmSwift
|
||||
#ifndef FileProviderExt_Bridging_Header_h
|
||||
#define FileProviderExt_Bridging_Header_h
|
||||
|
||||
class NextcloudLocalFileMetadataTable: Object {
|
||||
@Persisted(primaryKey: true) var ocId: String
|
||||
@Persisted var account = ""
|
||||
@Persisted var etag = ""
|
||||
@Persisted var exifDate: Date?
|
||||
@Persisted var exifLatitude = ""
|
||||
@Persisted var exifLongitude = ""
|
||||
@Persisted var exifLensModel: String?
|
||||
@Persisted var favorite: Bool = false
|
||||
@Persisted var fileName = ""
|
||||
@Persisted var offline: Bool = false
|
||||
}
|
||||
#import "Services/ClientCommunicationProtocol.h"
|
||||
|
||||
#endif /* FileProviderExt_Bridging_Header_h */
|
|
@ -12,14 +12,46 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import FileProvider
|
||||
import OSLog
|
||||
import Foundation
|
||||
import NCDesktopClientSocketKit
|
||||
import NextcloudKit
|
||||
import NextcloudFileProviderKit
|
||||
import OSLog
|
||||
|
||||
extension FileProviderExtension {
|
||||
func sendFileProviderDomainIdentifier() {
|
||||
extension FileProviderExtension: NSFileProviderServicing {
|
||||
/*
|
||||
This FileProviderExtension extension contains everything needed to communicate with the client.
|
||||
We have two systems for communicating between the extensions and the client.
|
||||
|
||||
Apple's XPC based File Provider APIs let us easily communicate client -> extension.
|
||||
This is what ClientCommunicationService is for.
|
||||
|
||||
We also use sockets, because the File Provider XPC system does not let us easily talk from
|
||||
extension->client.
|
||||
We need this because the extension needs to be able to request account details. We can't
|
||||
reliably do this via XPC because the extensions get torn down by the system, out of the control
|
||||
of the app, and we can receive nil/no services from NSFileProviderManager. Once this is done
|
||||
then XPC works ok.
|
||||
*/
|
||||
func supportedServiceSources(
|
||||
for itemIdentifier: NSFileProviderItemIdentifier,
|
||||
completionHandler: @escaping ([NSFileProviderServiceSource]?, Error?) -> Void
|
||||
) -> Progress {
|
||||
Logger.desktopClientConnection.debug("Serving supported service sources")
|
||||
let clientCommService = ClientCommunicationService(fpExtension: self)
|
||||
let fpuiExtService = FPUIExtensionServiceSource(fpExtension: self)
|
||||
let services: [NSFileProviderServiceSource] = [clientCommService, fpuiExtService]
|
||||
completionHandler(services, nil)
|
||||
let progress = Progress()
|
||||
progress.cancellationHandler = {
|
||||
let error = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError)
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
return progress
|
||||
}
|
||||
|
||||
@objc func sendFileProviderDomainIdentifier() {
|
||||
let command = "FILE_PROVIDER_DOMAIN_IDENTIFIER_REQUEST_REPLY"
|
||||
let argument = domain.identifier.rawValue
|
||||
let message = command + ":" + argument + "\n"
|
||||
|
@ -28,7 +60,9 @@ extension FileProviderExtension {
|
|||
|
||||
private func signalEnumeratorAfterAccountSetup() {
|
||||
guard let fpManager = NSFileProviderManager(for: domain) else {
|
||||
Logger.fileProviderExtension.error("Could not get file provider manager for domain \(self.domain.displayName, privacy: .public), cannot notify after account setup")
|
||||
Logger.fileProviderExtension.error(
|
||||
"Could not get file provider manager for domain \(self.domain.displayName, privacy: .public), cannot notify after account setup"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -36,36 +70,53 @@ extension FileProviderExtension {
|
|||
|
||||
fpManager.signalErrorResolved(NSFileProviderError(.notAuthenticated)) { error in
|
||||
if error != nil {
|
||||
Logger.fileProviderExtension.error("Error resolving not authenticated, received error: \(error!.localizedDescription)")
|
||||
Logger.fileProviderExtension.error(
|
||||
"Error resolving not authenticated, received error: \(error!.localizedDescription)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.fileProviderExtension.debug("Signalling enumerators for user \(self.ncAccount!.username) at server \(self.ncAccount!.serverUrl, privacy: .public)")
|
||||
Logger.fileProviderExtension.debug(
|
||||
"Signalling enumerators for user \(self.ncAccount!.username) at server \(self.ncAccount!.serverUrl, privacy: .public)"
|
||||
)
|
||||
|
||||
fpManager.signalEnumerator(for: .workingSet) { error in
|
||||
if error != nil {
|
||||
Logger.fileProviderExtension.error("Error signalling enumerator for working set, received error: \(error!.localizedDescription, privacy: .public)")
|
||||
Logger.fileProviderExtension.error(
|
||||
"Error signalling enumerator for working set, received error: \(error!.localizedDescription, privacy: .public)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupDomainAccount(user: String, serverUrl: String, password: String) {
|
||||
ncAccount = NextcloudAccount(user: user, serverUrl: serverUrl, password: password)
|
||||
ncKit.setup(user: ncAccount!.username,
|
||||
userId: ncAccount!.username,
|
||||
password: ncAccount!.password,
|
||||
urlBase: ncAccount!.serverUrl,
|
||||
userAgent: "Nextcloud-macOS/FileProviderExt",
|
||||
nextcloudVersion: 25,
|
||||
delegate: nil) // TODO: add delegate methods for self
|
||||
@objc func setupDomainAccount(user: String, serverUrl: String, password: String) {
|
||||
let newNcAccount = Account(user: user, serverUrl: serverUrl, password: password)
|
||||
guard newNcAccount != ncAccount else { return }
|
||||
ncAccount = newNcAccount
|
||||
ncKit.setup(
|
||||
account: newNcAccount.ncKitAccount,
|
||||
user: newNcAccount.username,
|
||||
userId: newNcAccount.username,
|
||||
password: newNcAccount.password,
|
||||
urlBase: newNcAccount.serverUrl,
|
||||
userAgent: "Nextcloud-macOS/FileProviderExt",
|
||||
nextcloudVersion: 25,
|
||||
delegate: nil) // TODO: add delegate methods for self
|
||||
|
||||
Logger.fileProviderExtension.info("Nextcloud account set up in File Provider extension for user: \(user, privacy: .public) at server: \(serverUrl, privacy: .public)")
|
||||
changeObserver = RemoteChangeObserver(ncKit: ncKit, domain: domain)
|
||||
ncKit.setup(delegate: changeObserver)
|
||||
|
||||
Logger.fileProviderExtension.info(
|
||||
"Nextcloud account set up in File Provider extension for user: \(user, privacy: .public) at server: \(serverUrl, privacy: .public)"
|
||||
)
|
||||
|
||||
signalEnumeratorAfterAccountSetup()
|
||||
}
|
||||
|
||||
func removeAccountConfig() {
|
||||
Logger.fileProviderExtension.info("Received instruction to remove account data for user \(self.ncAccount!.username, privacy: .public) at server \(self.ncAccount!.serverUrl, privacy: .public)")
|
||||
@objc func removeAccountConfig() {
|
||||
Logger.fileProviderExtension.info(
|
||||
"Received instruction to remove account data for user \(self.ncAccount!.username, privacy: .public) at server \(self.ncAccount!.serverUrl, privacy: .public)"
|
||||
)
|
||||
ncAccount = nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,51 +12,29 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import FileProvider
|
||||
import Foundation
|
||||
import NextcloudKit
|
||||
import NextcloudFileProviderKit
|
||||
import OSLog
|
||||
|
||||
extension FileProviderExtension: NSFileProviderThumbnailing {
|
||||
func fetchThumbnails(for itemIdentifiers: [NSFileProviderItemIdentifier],
|
||||
requestedSize size: CGSize,
|
||||
perThumbnailCompletionHandler: @escaping (NSFileProviderItemIdentifier,
|
||||
Data?,
|
||||
Error?) -> Void,
|
||||
completionHandler: @escaping (Error?) -> Void) -> Progress {
|
||||
|
||||
let progress = Progress(totalUnitCount: Int64(itemIdentifiers.count))
|
||||
var progressCounter: Int64 = 0
|
||||
|
||||
func finishCurrent() {
|
||||
progressCounter += 1
|
||||
|
||||
if progressCounter == progress.totalUnitCount {
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
for itemIdentifier in itemIdentifiers {
|
||||
Logger.fileProviderExtension.debug("Fetching thumbnail for item with identifier:\(itemIdentifier.rawValue, privacy: .public)")
|
||||
guard let metadata = NextcloudFilesDatabaseManager.shared.itemMetadataFromFileProviderItemIdentifier(itemIdentifier),
|
||||
let thumbnailUrl = metadata.thumbnailUrl(size: size) else {
|
||||
Logger.fileProviderExtension.debug("Did not fetch thumbnail URL")
|
||||
finishCurrent()
|
||||
continue
|
||||
}
|
||||
|
||||
Logger.fileProviderExtension.debug("Fetching thumbnail for file:\(metadata.fileName) at:\(thumbnailUrl.absoluteString, privacy: .public)")
|
||||
|
||||
self.ncKit.getPreview(url: thumbnailUrl) { _, data, error in
|
||||
if error == .success && data != nil {
|
||||
perThumbnailCompletionHandler(itemIdentifier, data, nil)
|
||||
} else {
|
||||
perThumbnailCompletionHandler(itemIdentifier, nil, NSFileProviderError(.serverUnreachable))
|
||||
}
|
||||
finishCurrent()
|
||||
}
|
||||
}
|
||||
|
||||
return progress
|
||||
func fetchThumbnails(
|
||||
for itemIdentifiers: [NSFileProviderItemIdentifier],
|
||||
requestedSize size: CGSize,
|
||||
perThumbnailCompletionHandler: @escaping (
|
||||
NSFileProviderItemIdentifier,
|
||||
Data?,
|
||||
Error?
|
||||
) -> Void,
|
||||
completionHandler: @escaping (Error?) -> Void
|
||||
) -> Progress {
|
||||
return NextcloudFileProviderKit.fetchThumbnails(
|
||||
for: itemIdentifiers,
|
||||
requestedSize: size,
|
||||
usingKit: self.ncKit,
|
||||
perThumbnailCompletionHandler: perThumbnailCompletionHandler,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,33 +13,31 @@
|
|||
*/
|
||||
|
||||
import FileProvider
|
||||
import OSLog
|
||||
import NCDesktopClientSocketKit
|
||||
import NextcloudKit
|
||||
import NextcloudFileProviderKit
|
||||
import OSLog
|
||||
|
||||
class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKCommonDelegate {
|
||||
@objc class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKCommonDelegate {
|
||||
let domain: NSFileProviderDomain
|
||||
let ncKit = NextcloudKit()
|
||||
lazy var ncKitBackground: NKBackground = {
|
||||
let nckb = NKBackground(nkCommonInstance: ncKit.nkCommonInstance)
|
||||
return nckb
|
||||
}()
|
||||
|
||||
let appGroupIdentifier: String? = Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix") as? String
|
||||
var ncAccount: NextcloudAccount?
|
||||
let appGroupIdentifier = Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix") as? String
|
||||
var ncAccount: Account?
|
||||
var changeObserver: RemoteChangeObserver?
|
||||
lazy var ncKitBackground = NKBackground(nkCommonInstance: ncKit.nkCommonInstance)
|
||||
lazy var socketClient: LocalSocketClient? = {
|
||||
guard let containerUrl = pathForAppGroupContainer() else {
|
||||
Logger.fileProviderExtension.critical("Could not start file provider socket client properly as could not get container url")
|
||||
Logger.fileProviderExtension.critical("Won't start socket client, no container url")
|
||||
return nil;
|
||||
}
|
||||
|
||||
let socketPath = containerUrl.appendingPathComponent(".fileprovidersocket", conformingTo: .archive)
|
||||
let socketPath = containerUrl.appendingPathComponent(
|
||||
".fileprovidersocket", conformingTo: .archive)
|
||||
let lineProcessor = FileProviderSocketLineProcessor(delegate: self)
|
||||
|
||||
return LocalSocketClient(socketPath: socketPath.path, lineProcessor: lineProcessor)
|
||||
}()
|
||||
|
||||
let urlSessionIdentifier: String = "com.nextcloud.session.upload.fileproviderext"
|
||||
let urlSessionIdentifier = "com.nextcloud.session.upload.fileproviderext"
|
||||
let urlSessionMaximumConnectionsPerHost = 5
|
||||
lazy var urlSession: URLSession = {
|
||||
let configuration = URLSessionConfiguration.background(withIdentifier: urlSessionIdentifier)
|
||||
|
@ -50,571 +48,313 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
|
|||
configuration.requestCachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalCacheData
|
||||
configuration.sharedContainerIdentifier = appGroupIdentifier
|
||||
|
||||
let session = URLSession(configuration: configuration, delegate: ncKitBackground, delegateQueue: OperationQueue.main)
|
||||
let session = URLSession(
|
||||
configuration: configuration,
|
||||
delegate: ncKitBackground,
|
||||
delegateQueue: OperationQueue.main
|
||||
)
|
||||
return session
|
||||
}()
|
||||
|
||||
required init(domain: NSFileProviderDomain) {
|
||||
self.domain = domain
|
||||
// The containing application must create a domain using `NSFileProviderManager.add(_:, completionHandler:)`. The system will then launch the application extension process, call `FileProviderExtension.init(domain:)` to instantiate the extension for that domain, and call methods on the instance.
|
||||
// Whether or not we are going to recursively scan new folders when they are discovered.
|
||||
// Apple's recommendation is that we should always scan the file hierarchy fully.
|
||||
// This does lead to long load times when a file provider domain is initially configured.
|
||||
// We can instead do a fast enumeration where we only scan folders as the user navigates through
|
||||
// them, thereby avoiding this issue; the trade-off is that we will be unable to detect
|
||||
// materialised file moves to unexplored folders, therefore deleting the item when we could have
|
||||
// just moved it instead.
|
||||
//
|
||||
// Since it's not desirable to cancel a long recursive enumeration half-way through, we do the
|
||||
// fast enumeration by default. We prompt the user on the client side to run a proper, full
|
||||
// enumeration if they want for safety.
|
||||
lazy var config = FileProviderConfig(domainIdentifier: domain.identifier)
|
||||
|
||||
required init(domain: NSFileProviderDomain) {
|
||||
// The containing application must create a domain using
|
||||
// `NSFileProviderManager.add(_:, completionHandler:)`. The system will then launch the
|
||||
// application extension process, call `FileProviderExtension.init(domain:)` to instantiate
|
||||
// the extension for that domain, and call methods on the instance.
|
||||
self.domain = domain
|
||||
super.init()
|
||||
self.socketClient?.start()
|
||||
socketClient?.start()
|
||||
}
|
||||
|
||||
|
||||
func invalidate() {
|
||||
// TODO: cleanup any resources
|
||||
Logger.fileProviderExtension.debug("Extension for domain \(self.domain.displayName, privacy: .public) is being torn down")
|
||||
Logger.fileProviderExtension.debug(
|
||||
"Extension for domain \(self.domain.displayName, privacy: .public) is being torn down"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: NSFileProviderReplicatedExtension protocol methods
|
||||
|
||||
func item(for identifier: NSFileProviderItemIdentifier, request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void) -> Progress {
|
||||
// resolve the given identifier to a record in the model
|
||||
// MARK: - NSFileProviderReplicatedExtension protocol methods
|
||||
|
||||
Logger.fileProviderExtension.debug("Received item request for item with identifier: \(identifier.rawValue, privacy: .public)")
|
||||
if identifier == .rootContainer {
|
||||
guard let ncAccount = ncAccount else {
|
||||
Logger.fileProviderExtension.error("Not providing item: \(identifier.rawValue, privacy: .public) as account not set up yet")
|
||||
completionHandler(nil, NSFileProviderError(.notAuthenticated))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
let metadata = NextcloudItemMetadataTable()
|
||||
|
||||
metadata.account = ncAccount.ncKitAccount
|
||||
metadata.directory = true
|
||||
metadata.ocId = NSFileProviderItemIdentifier.rootContainer.rawValue
|
||||
metadata.fileName = "root"
|
||||
metadata.fileNameView = "root"
|
||||
metadata.serverUrl = ncAccount.serverUrl
|
||||
metadata.classFile = NKCommon.TypeClassFile.directory.rawValue
|
||||
|
||||
completionHandler(FileProviderItem(metadata: metadata, parentItemIdentifier: NSFileProviderItemIdentifier.rootContainer, ncKit: ncKit), nil)
|
||||
return Progress()
|
||||
}
|
||||
|
||||
let dbManager = NextcloudFilesDatabaseManager.shared
|
||||
|
||||
guard let metadata = dbManager.itemMetadataFromFileProviderItemIdentifier(identifier),
|
||||
let parentItemIdentifier = dbManager.parentItemIdentifierFromMetadata(metadata) else {
|
||||
func item(
|
||||
for identifier: NSFileProviderItemIdentifier,
|
||||
request _: NSFileProviderRequest,
|
||||
completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void
|
||||
) -> Progress {
|
||||
if let item = Item.storedItem(identifier: identifier, usingKit: ncKit) {
|
||||
completionHandler(item, nil)
|
||||
} else {
|
||||
completionHandler(nil, NSFileProviderError(.noSuchItem))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
completionHandler(FileProviderItem(metadata: metadata, parentItemIdentifier: parentItemIdentifier, ncKit: ncKit), nil)
|
||||
return Progress()
|
||||
}
|
||||
|
||||
func fetchContents(for itemIdentifier: NSFileProviderItemIdentifier, version requestedVersion: NSFileProviderItemVersion?, request: NSFileProviderRequest, completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void) -> Progress {
|
||||
|
||||
Logger.fileProviderExtension.debug("Received request to fetch contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public)")
|
||||
func fetchContents(
|
||||
for itemIdentifier: NSFileProviderItemIdentifier,
|
||||
version requestedVersion: NSFileProviderItemVersion?,
|
||||
request: NSFileProviderRequest,
|
||||
completionHandler: @escaping (URL?, NSFileProviderItem?, Error?) -> Void
|
||||
) -> Progress {
|
||||
Logger.fileProviderExtension.debug(
|
||||
"Received request to fetch contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public)"
|
||||
)
|
||||
|
||||
guard requestedVersion == nil else {
|
||||
// TODO: Add proper support for file versioning
|
||||
Logger.fileProviderExtension.error("Can't return contents for specific version as this is not supported.")
|
||||
completionHandler(nil, nil, NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:]))
|
||||
Logger.fileProviderExtension.error(
|
||||
"Can't return contents for a specific version as this is not supported."
|
||||
)
|
||||
completionHandler(
|
||||
nil,
|
||||
nil,
|
||||
NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError)
|
||||
)
|
||||
return Progress()
|
||||
}
|
||||
|
||||
guard ncAccount != nil else {
|
||||
Logger.fileProviderExtension.error("Not fetching contents item: \(itemIdentifier.rawValue, privacy: .public) as account not set up yet")
|
||||
Logger.fileProviderExtension.error(
|
||||
"""
|
||||
Not fetching contents for item: \(itemIdentifier.rawValue, privacy: .public)
|
||||
as account not set up yet.
|
||||
"""
|
||||
)
|
||||
completionHandler(nil, nil, NSFileProviderError(.notAuthenticated))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
let dbManager = NextcloudFilesDatabaseManager.shared
|
||||
let ocId = itemIdentifier.rawValue
|
||||
guard let metadata = dbManager.itemMetadataFromOcId(ocId) else {
|
||||
Logger.fileProviderExtension.error("Could not acquire metadata of item with identifier: \(itemIdentifier.rawValue, privacy: .public)")
|
||||
guard let item = Item.storedItem(identifier: itemIdentifier, usingKit: ncKit) else {
|
||||
Logger.fileProviderExtension.error(
|
||||
"""
|
||||
Not fetching contents for item: \(itemIdentifier.rawValue, privacy: .public)
|
||||
as item not found.
|
||||
"""
|
||||
)
|
||||
completionHandler(nil, nil, NSFileProviderError(.noSuchItem))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
guard !metadata.isDocumentViewableOnly else {
|
||||
Logger.fileProviderExtension.error("Could not get contents of item as is readonly: \(itemIdentifier.rawValue, privacy: .public) \(metadata.fileName, privacy: .public)")
|
||||
completionHandler(nil, nil, NSFileProviderError(.cannotSynchronize))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName
|
||||
|
||||
Logger.fileProviderExtension.debug("Fetching file with name \(metadata.fileName, privacy: .public) at URL: \(serverUrlFileName, privacy: .public)")
|
||||
|
||||
let progress = Progress()
|
||||
|
||||
// TODO: Handle folders nicely
|
||||
do {
|
||||
let fileNameLocalPath = try localPathForNCFile(ocId: metadata.ocId, fileNameView: metadata.fileNameView, domain: self.domain)
|
||||
|
||||
dbManager.setStatusForItemMetadata(metadata, status: NextcloudItemMetadataTable.Status.downloading) { updatedMetadata in
|
||||
|
||||
guard let updatedMetadata = updatedMetadata else {
|
||||
Logger.fileProviderExtension.error("Could not acquire updated metadata of item with identifier: \(itemIdentifier.rawValue, privacy: .public), unable to update item status to downloading")
|
||||
completionHandler(nil, nil, NSFileProviderError(.noSuchItem))
|
||||
return
|
||||
}
|
||||
|
||||
self.ncKit.download(serverUrlFileName: serverUrlFileName,
|
||||
fileNameLocalPath: fileNameLocalPath.path,
|
||||
requestHandler: { request in
|
||||
progress.setHandlersFromAfRequest(request)
|
||||
}, taskHandler: { task in
|
||||
NSFileProviderManager(for: self.domain)?.register(task, forItemWithIdentifier: itemIdentifier, completionHandler: { _ in })
|
||||
}, progressHandler: { downloadProgress in
|
||||
downloadProgress.copyCurrentStateToProgress(progress)
|
||||
}) { _, etag, date, _, _, _, error in
|
||||
if error == .success {
|
||||
Logger.fileTransfer.debug("Acquired contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public) and filename: \(updatedMetadata.fileName, privacy: .public)")
|
||||
|
||||
updatedMetadata.status = NextcloudItemMetadataTable.Status.normal.rawValue
|
||||
updatedMetadata.sessionError = ""
|
||||
updatedMetadata.date = (date ?? NSDate()) as Date
|
||||
updatedMetadata.etag = etag ?? ""
|
||||
|
||||
dbManager.addLocalFileMetadataFromItemMetadata(updatedMetadata)
|
||||
dbManager.addItemMetadata(updatedMetadata)
|
||||
|
||||
guard let parentItemIdentifier = dbManager.parentItemIdentifierFromMetadata(updatedMetadata) else {
|
||||
completionHandler(nil, nil, NSFileProviderError(.noSuchItem))
|
||||
return
|
||||
}
|
||||
|
||||
let fpItem = FileProviderItem(metadata: updatedMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit)
|
||||
|
||||
completionHandler(fileNameLocalPath, fpItem, nil)
|
||||
} else {
|
||||
Logger.fileTransfer.error("Could not acquire contents of item with identifier: \(itemIdentifier.rawValue, privacy: .public) and fileName: \(updatedMetadata.fileName, privacy: .public)")
|
||||
|
||||
updatedMetadata.status = NextcloudItemMetadataTable.Status.downloadError.rawValue
|
||||
updatedMetadata.sessionError = error.errorDescription
|
||||
|
||||
dbManager.addItemMetadata(updatedMetadata)
|
||||
|
||||
completionHandler(nil, nil, error.fileProviderError)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
Logger.fileProviderExtension.error("Could not find local path for file \(metadata.fileName, privacy: .public), received error: \(error.localizedDescription, privacy: .public)")
|
||||
completionHandler(nil, nil, NSFileProviderError(.cannotSynchronize))
|
||||
Task {
|
||||
let (localUrl, updatedItem, error) = await item.fetchContents(
|
||||
domain: self.domain, progress: progress
|
||||
)
|
||||
completionHandler(localUrl, updatedItem, error)
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
func createItem(basedOn itemTemplate: NSFileProviderItem, fields: NSFileProviderItemFields, contents url: URL?, options: NSFileProviderCreateItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) -> Progress {
|
||||
// TODO: a new item was created on disk, process the item's creation
|
||||
|
||||
Logger.fileProviderExtension.debug("Received create item request for item with identifier: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) and filename: \(itemTemplate.filename, privacy: .public)")
|
||||
func createItem(
|
||||
basedOn itemTemplate: NSFileProviderItem,
|
||||
fields: NSFileProviderItemFields,
|
||||
contents url: URL?,
|
||||
options: NSFileProviderCreateItemOptions = [],
|
||||
request: NSFileProviderRequest,
|
||||
completionHandler: @escaping (
|
||||
NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?
|
||||
) -> Void
|
||||
) -> Progress {
|
||||
|
||||
guard itemTemplate.contentType != .symbolicLink else {
|
||||
Logger.fileProviderExtension.error("Cannot create item, symbolic links not supported.")
|
||||
completionHandler(itemTemplate, NSFileProviderItemFields(), false, NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:]))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
guard let ncAccount = ncAccount else {
|
||||
Logger.fileProviderExtension.error("Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) as account not set up yet")
|
||||
completionHandler(itemTemplate, NSFileProviderItemFields(), false, NSFileProviderError(.notAuthenticated))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
let dbManager = NextcloudFilesDatabaseManager.shared
|
||||
let parentItemIdentifier = itemTemplate.parentItemIdentifier
|
||||
let itemTemplateIsFolder = itemTemplate.contentType == .folder ||
|
||||
itemTemplate.contentType == .directory
|
||||
|
||||
if options.contains(.mayAlreadyExist) {
|
||||
// TODO: This needs to be properly handled with a check in the db
|
||||
Logger.fileProviderExtension.info("Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) as it may already exist")
|
||||
completionHandler(itemTemplate, NSFileProviderItemFields(), false, NSFileProviderError(.noSuchItem))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
var parentItemServerUrl: String
|
||||
|
||||
if parentItemIdentifier == .rootContainer {
|
||||
parentItemServerUrl = ncAccount.davFilesUrl
|
||||
} else {
|
||||
guard let parentItemMetadata = dbManager.directoryMetadata(ocId: parentItemIdentifier.rawValue) else {
|
||||
Logger.fileProviderExtension.error("Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public), could not find metadata for parentItemIdentifier \(parentItemIdentifier.rawValue, privacy: .public)")
|
||||
completionHandler(itemTemplate, NSFileProviderItemFields(), false, NSFileProviderError(.noSuchItem))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
parentItemServerUrl = parentItemMetadata.serverUrl + "/" + parentItemMetadata.fileName
|
||||
}
|
||||
|
||||
let fileNameLocalPath = url?.path ?? ""
|
||||
let newServerUrlFileName = parentItemServerUrl + "/" + itemTemplate.filename
|
||||
|
||||
Logger.fileProviderExtension.debug("About to upload item with identifier: \(itemTemplate.itemIdentifier.rawValue, privacy: .public) of type: \(itemTemplate.contentType?.identifier ?? "UNKNOWN") (is folder: \(itemTemplateIsFolder ? "yes" : "no") and filename: \(itemTemplate.filename) to server url: \(newServerUrlFileName, privacy: .public) with contents located at: \(fileNameLocalPath, privacy: .public)")
|
||||
|
||||
if itemTemplateIsFolder {
|
||||
self.ncKit.createFolder(serverUrlFileName: newServerUrlFileName) { account, ocId, _, error in
|
||||
guard error == .success else {
|
||||
Logger.fileTransfer.error("Could not create new folder with name: \(itemTemplate.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)")
|
||||
completionHandler(itemTemplate, [], false, error.fileProviderError)
|
||||
return
|
||||
}
|
||||
|
||||
// Read contents after creation
|
||||
self.ncKit.readFileOrFolder(serverUrlFileName: newServerUrlFileName, depth: "0", showHiddenFiles: true) { account, files, _, error in
|
||||
guard error == .success else {
|
||||
Logger.fileTransfer.error("Could not read new folder with name: \(itemTemplate.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)")
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.global().async {
|
||||
NextcloudItemMetadataTable.metadatasFromDirectoryReadNKFiles(files, account: account) { directoryMetadata, childDirectoriesMetadata, metadatas in
|
||||
|
||||
dbManager.addItemMetadata(directoryMetadata)
|
||||
|
||||
let fpItem = FileProviderItem(metadata: directoryMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit)
|
||||
|
||||
completionHandler(fpItem, [], true, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let tempId = itemTemplate.itemIdentifier.rawValue
|
||||
Logger.fileProviderExtension.debug(
|
||||
"""
|
||||
Received create item request for item with identifier: \(tempId, privacy: .public)
|
||||
and filename: \(itemTemplate.filename, privacy: .public)
|
||||
"""
|
||||
)
|
||||
|
||||
guard let ncAccount else {
|
||||
Logger.fileProviderExtension.error(
|
||||
"""
|
||||
Not creating item: \(itemTemplate.itemIdentifier.rawValue, privacy: .public)
|
||||
as account not set up yet
|
||||
"""
|
||||
)
|
||||
completionHandler(
|
||||
itemTemplate,
|
||||
NSFileProviderItemFields(),
|
||||
false,
|
||||
NSFileProviderError(.notAuthenticated)
|
||||
)
|
||||
return Progress()
|
||||
}
|
||||
|
||||
let progress = Progress()
|
||||
|
||||
self.ncKit.upload(serverUrlFileName: newServerUrlFileName,
|
||||
fileNameLocalPath: fileNameLocalPath,
|
||||
requestHandler: { request in
|
||||
progress.setHandlersFromAfRequest(request)
|
||||
}, taskHandler: { task in
|
||||
NSFileProviderManager(for: self.domain)?.register(task, forItemWithIdentifier: itemTemplate.itemIdentifier, completionHandler: { _ in })
|
||||
}, progressHandler: { uploadProgress in
|
||||
uploadProgress.copyCurrentStateToProgress(progress)
|
||||
}) { account, ocId, etag, date, size, _, _, error in
|
||||
guard error == .success, let ocId = ocId else {
|
||||
Logger.fileTransfer.error("Could not upload item with filename: \(itemTemplate.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)")
|
||||
completionHandler(itemTemplate, [], false, error.fileProviderError)
|
||||
return
|
||||
Task {
|
||||
let (item, error) = await Item.create(
|
||||
basedOn: itemTemplate,
|
||||
fields: fields,
|
||||
contents: url,
|
||||
request: request,
|
||||
domain: self.domain,
|
||||
ncKit: ncKit,
|
||||
ncAccount: ncAccount,
|
||||
progress: progress
|
||||
)
|
||||
if error != nil {
|
||||
signalEnumerator(completionHandler: { _ in })
|
||||
}
|
||||
|
||||
Logger.fileTransfer.info("Successfully uploaded item with identifier: \(ocId, privacy: .public) and filename: \(itemTemplate.filename, privacy: .public)")
|
||||
|
||||
if size != itemTemplate.documentSize as? Int64 {
|
||||
Logger.fileTransfer.warning("Created item upload reported as successful, but there are differences between the received file size (\(size, privacy: .public)) and the original file size (\(itemTemplate.documentSize??.int64Value ?? 0))")
|
||||
}
|
||||
|
||||
let newMetadata = NextcloudItemMetadataTable()
|
||||
newMetadata.date = (date ?? NSDate()) as Date
|
||||
newMetadata.etag = etag ?? ""
|
||||
newMetadata.account = account
|
||||
newMetadata.fileName = itemTemplate.filename
|
||||
newMetadata.fileNameView = itemTemplate.filename
|
||||
newMetadata.ocId = ocId
|
||||
newMetadata.size = size
|
||||
newMetadata.contentType = itemTemplate.contentType?.preferredMIMEType ?? ""
|
||||
newMetadata.directory = itemTemplateIsFolder
|
||||
newMetadata.serverUrl = parentItemServerUrl
|
||||
newMetadata.session = ""
|
||||
newMetadata.sessionError = ""
|
||||
newMetadata.sessionTaskIdentifier = 0
|
||||
newMetadata.status = NextcloudItemMetadataTable.Status.normal.rawValue
|
||||
|
||||
dbManager.addLocalFileMetadataFromItemMetadata(newMetadata)
|
||||
dbManager.addItemMetadata(newMetadata)
|
||||
|
||||
let fpItem = FileProviderItem(metadata: newMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit)
|
||||
|
||||
completionHandler(fpItem, [], false, nil)
|
||||
completionHandler(
|
||||
item ?? itemTemplate,
|
||||
NSFileProviderItemFields(),
|
||||
false,
|
||||
error
|
||||
)
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
func modifyItem(_ item: NSFileProviderItem, baseVersion version: NSFileProviderItemVersion, changedFields: NSFileProviderItemFields, contents newContents: URL?, options: NSFileProviderModifyItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?) -> Void) -> Progress {
|
||||
|
||||
func modifyItem(
|
||||
_ item: NSFileProviderItem,
|
||||
baseVersion: NSFileProviderItemVersion,
|
||||
changedFields: NSFileProviderItemFields,
|
||||
contents newContents: URL?,
|
||||
options: NSFileProviderModifyItemOptions = [],
|
||||
request: NSFileProviderRequest,
|
||||
completionHandler: @escaping (
|
||||
NSFileProviderItem?, NSFileProviderItemFields, Bool, Error?
|
||||
) -> Void
|
||||
) -> Progress {
|
||||
// An item was modified on disk, process the item's modification
|
||||
// TODO: Handle finder things like tags, other possible item changed fields
|
||||
|
||||
Logger.fileProviderExtension.debug("Received modify item request for item with identifier: \(item.itemIdentifier.rawValue, privacy: .public) and filename: \(item.filename, privacy: .public)")
|
||||
let identifier = item.itemIdentifier
|
||||
let ocId = identifier.rawValue
|
||||
Logger.fileProviderExtension.debug(
|
||||
"""
|
||||
Received modify item request for item with identifier: \(ocId, privacy: .public)
|
||||
and filename: \(item.filename, privacy: .public)
|
||||
"""
|
||||
)
|
||||
|
||||
guard let ncAccount = ncAccount else {
|
||||
Logger.fileProviderExtension.error("Not modifying item: \(item.itemIdentifier.rawValue, privacy: .public) as account not set up yet")
|
||||
guard let ncAccount else {
|
||||
Logger.fileProviderExtension.error(
|
||||
"Not modifying item: \(ocId, privacy: .public) as account not set up yet."
|
||||
)
|
||||
completionHandler(item, [], false, NSFileProviderError(.notAuthenticated))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
let dbManager = NextcloudFilesDatabaseManager.shared
|
||||
let parentItemIdentifier = item.parentItemIdentifier
|
||||
let itemTemplateIsFolder = item.contentType == .folder ||
|
||||
item.contentType == .directory
|
||||
|
||||
if options.contains(.mayAlreadyExist) {
|
||||
// TODO: This needs to be properly handled with a check in the db
|
||||
Logger.fileProviderExtension.warning("Modification for item: \(item.itemIdentifier.rawValue, privacy: .public) may already exist")
|
||||
}
|
||||
|
||||
var parentItemServerUrl: String
|
||||
|
||||
if parentItemIdentifier == .rootContainer {
|
||||
parentItemServerUrl = ncAccount.davFilesUrl
|
||||
} else {
|
||||
guard let parentItemMetadata = dbManager.directoryMetadata(ocId: parentItemIdentifier.rawValue) else {
|
||||
Logger.fileProviderExtension.error("Not modifying item: \(item.itemIdentifier.rawValue, privacy: .public), could not find metadata for parentItemIdentifier \(parentItemIdentifier.rawValue, privacy: .public)")
|
||||
completionHandler(item, [], false, NSFileProviderError(.noSuchItem))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
parentItemServerUrl = parentItemMetadata.serverUrl + "/" + parentItemMetadata.fileName
|
||||
}
|
||||
|
||||
let fileNameLocalPath = newContents?.path ?? ""
|
||||
let newServerUrlFileName = parentItemServerUrl + "/" + item.filename
|
||||
|
||||
Logger.fileProviderExtension.debug("About to upload modified item with identifier: \(item.itemIdentifier.rawValue, privacy: .public) of type: \(item.contentType?.identifier ?? "UNKNOWN") (is folder: \(itemTemplateIsFolder ? "yes" : "no") and filename: \(item.filename, privacy: .public) to server url: \(newServerUrlFileName, privacy: .public) with contents located at: \(fileNameLocalPath, privacy: .public)")
|
||||
|
||||
var modifiedItem = item
|
||||
|
||||
// Create a serial dispatch queue
|
||||
// We want to wait for network operations to finish before we fire off subsequent network
|
||||
// operations, or we might cause explosions (e.g. trying to modify items that have just been
|
||||
// moved elsewhere)
|
||||
let dispatchQueue = DispatchQueue(label: "modifyItemQueue", qos: .userInitiated)
|
||||
|
||||
if changedFields.contains(.filename) || changedFields.contains(.parentItemIdentifier) {
|
||||
dispatchQueue.async {
|
||||
let ocId = item.itemIdentifier.rawValue
|
||||
Logger.fileProviderExtension.debug("Changed fields for item \(ocId, privacy: .public) with filename \(item.filename, privacy: .public) includes filename or parentitemidentifier...")
|
||||
|
||||
guard let metadata = dbManager.itemMetadataFromOcId(ocId) else {
|
||||
Logger.fileProviderExtension.error("Could not acquire metadata of item with identifier: \(item.itemIdentifier.rawValue, privacy: .public)")
|
||||
completionHandler(item, [], false, NSFileProviderError(.noSuchItem))
|
||||
return
|
||||
}
|
||||
|
||||
var renameError: NSFileProviderError?
|
||||
let oldServerUrlFileName = metadata.serverUrl + "/" + metadata.fileName
|
||||
|
||||
let moveFileOrFolderDispatchGroup = DispatchGroup() // Make this block wait until done
|
||||
moveFileOrFolderDispatchGroup.enter()
|
||||
|
||||
self.ncKit.moveFileOrFolder(serverUrlFileNameSource: oldServerUrlFileName,
|
||||
serverUrlFileNameDestination: newServerUrlFileName,
|
||||
overwrite: false) { account, error in
|
||||
guard error == .success else {
|
||||
Logger.fileTransfer.error("Could not move file or folder: \(oldServerUrlFileName, privacy: .public) to \(newServerUrlFileName, privacy: .public), received error: \(error.errorDescription, privacy: .public)")
|
||||
renameError = error.fileProviderError
|
||||
moveFileOrFolderDispatchGroup.leave()
|
||||
return
|
||||
}
|
||||
|
||||
// Remember that a folder metadata's serverUrl is its direct server URL, while for
|
||||
// an item metadata the server URL is the parent folder's URL
|
||||
if itemTemplateIsFolder {
|
||||
_ = dbManager.renameDirectoryAndPropagateToChildren(ocId: ocId, newServerUrl: newServerUrlFileName, newFileName: item.filename)
|
||||
self.signalEnumerator { error in
|
||||
if error != nil {
|
||||
Logger.fileTransfer.error("Error notifying change in moved directory: \(error)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dbManager.renameItemMetadata(ocId: ocId, newServerUrl: parentItemServerUrl, newFileName: item.filename)
|
||||
}
|
||||
|
||||
guard let newMetadata = dbManager.itemMetadataFromOcId(ocId) else {
|
||||
Logger.fileTransfer.error("Could not acquire metadata of item with identifier: \(ocId, privacy: .public), cannot correctly inform of modification")
|
||||
renameError = NSFileProviderError(.noSuchItem)
|
||||
moveFileOrFolderDispatchGroup.leave()
|
||||
return
|
||||
}
|
||||
|
||||
modifiedItem = FileProviderItem(metadata: newMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit)
|
||||
moveFileOrFolderDispatchGroup.leave()
|
||||
}
|
||||
|
||||
moveFileOrFolderDispatchGroup.wait()
|
||||
|
||||
guard renameError == nil else {
|
||||
Logger.fileTransfer.error("Stopping rename of item with ocId \(ocId, privacy: .public) due to error: \(renameError!.localizedDescription, privacy: .public)")
|
||||
completionHandler(modifiedItem, [], false, renameError)
|
||||
return
|
||||
}
|
||||
|
||||
guard !itemTemplateIsFolder else {
|
||||
Logger.fileTransfer.debug("Only handling renaming for folders. ocId: \(ocId, privacy: .public)")
|
||||
completionHandler(modifiedItem, [], false, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Return the progress if item is folder here while the async block runs
|
||||
guard !itemTemplateIsFolder else {
|
||||
return Progress()
|
||||
}
|
||||
}
|
||||
|
||||
guard !itemTemplateIsFolder else {
|
||||
Logger.fileTransfer.debug("System requested modification for folder with ocID \(item.itemIdentifier.rawValue, privacy: .public) (\(newServerUrlFileName, privacy: .public)) of something other than folder name.")
|
||||
completionHandler(modifiedItem, [], false, nil)
|
||||
guard let existingItem = Item.storedItem(identifier: identifier, usingKit: ncKit) else {
|
||||
Logger.fileProviderExtension.error(
|
||||
"Not modifying item: \(ocId, privacy: .public) as item not found."
|
||||
)
|
||||
completionHandler(item, [], false, NSFileProviderError(.noSuchItem))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
let progress = Progress()
|
||||
|
||||
if changedFields.contains(.contents) {
|
||||
dispatchQueue.async {
|
||||
Logger.fileProviderExtension.debug("Item modification for \(item.itemIdentifier.rawValue, privacy: .public) \(item.filename, privacy: .public) includes contents")
|
||||
|
||||
guard newContents != nil else {
|
||||
Logger.fileProviderExtension.warning("WARNING. Could not upload modified contents as was provided nil contents url. ocId: \(item.itemIdentifier.rawValue, privacy: .public)")
|
||||
completionHandler(modifiedItem, [], false, NSFileProviderError(.noSuchItem))
|
||||
return
|
||||
}
|
||||
|
||||
let ocId = item.itemIdentifier.rawValue
|
||||
guard let metadata = dbManager.itemMetadataFromOcId(ocId) else {
|
||||
Logger.fileProviderExtension.error("Could not acquire metadata of item with identifier: \(ocId, privacy: .public)")
|
||||
completionHandler(item, NSFileProviderItemFields(), false, NSFileProviderError(.noSuchItem))
|
||||
return
|
||||
}
|
||||
|
||||
dbManager.setStatusForItemMetadata(metadata, status: NextcloudItemMetadataTable.Status.uploading) { updatedMetadata in
|
||||
|
||||
if updatedMetadata == nil {
|
||||
Logger.fileProviderExtension.warning("Could not acquire updated metadata of item with identifier: \(ocId, privacy: .public), unable to update item status to uploading")
|
||||
}
|
||||
|
||||
self.ncKit.upload(serverUrlFileName: newServerUrlFileName,
|
||||
fileNameLocalPath: fileNameLocalPath,
|
||||
requestHandler: { request in
|
||||
progress.setHandlersFromAfRequest(request)
|
||||
}, taskHandler: { task in
|
||||
NSFileProviderManager(for: self.domain)?.register(task, forItemWithIdentifier: item.itemIdentifier, completionHandler: { _ in })
|
||||
}, progressHandler: { uploadProgress in
|
||||
uploadProgress.copyCurrentStateToProgress(progress)
|
||||
}) { account, ocId, etag, date, size, _, _, error in
|
||||
if error == .success, let ocId = ocId {
|
||||
Logger.fileProviderExtension.info("Successfully uploaded item with identifier: \(ocId, privacy: .public) and filename: \(item.filename, privacy: .public)")
|
||||
|
||||
if size != item.documentSize as? Int64 {
|
||||
Logger.fileTransfer.warning("Created item upload reported as successful, but there are differences between the received file size (\(size, privacy: .public)) and the original file size (\(item.documentSize??.int64Value ?? 0))")
|
||||
}
|
||||
|
||||
let newMetadata = NextcloudItemMetadataTable()
|
||||
newMetadata.date = (date ?? NSDate()) as Date
|
||||
newMetadata.etag = etag ?? ""
|
||||
newMetadata.account = account
|
||||
newMetadata.fileName = item.filename
|
||||
newMetadata.fileNameView = item.filename
|
||||
newMetadata.ocId = ocId
|
||||
newMetadata.size = size
|
||||
newMetadata.contentType = item.contentType?.preferredMIMEType ?? ""
|
||||
newMetadata.directory = itemTemplateIsFolder
|
||||
newMetadata.serverUrl = parentItemServerUrl
|
||||
newMetadata.session = ""
|
||||
newMetadata.sessionError = ""
|
||||
newMetadata.sessionTaskIdentifier = 0
|
||||
newMetadata.status = NextcloudItemMetadataTable.Status.normal.rawValue
|
||||
|
||||
dbManager.addLocalFileMetadataFromItemMetadata(newMetadata)
|
||||
dbManager.addItemMetadata(newMetadata)
|
||||
|
||||
modifiedItem = FileProviderItem(metadata: newMetadata, parentItemIdentifier: parentItemIdentifier, ncKit: self.ncKit)
|
||||
completionHandler(modifiedItem, [], false, nil)
|
||||
} else {
|
||||
Logger.fileTransfer.error("Could not upload item \(item.itemIdentifier.rawValue, privacy: .public) with filename: \(item.filename, privacy: .public), received error: \(error.errorDescription, privacy: .public)")
|
||||
|
||||
metadata.status = NextcloudItemMetadataTable.Status.uploadError.rawValue
|
||||
metadata.sessionError = error.errorDescription
|
||||
|
||||
dbManager.addItemMetadata(metadata)
|
||||
|
||||
completionHandler(modifiedItem, [], false, error.fileProviderError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
Task {
|
||||
let (modifiedItem, error) = await existingItem.modify(
|
||||
itemTarget: item,
|
||||
baseVersion: baseVersion,
|
||||
changedFields: changedFields,
|
||||
contents: newContents,
|
||||
options: options,
|
||||
request: request,
|
||||
ncAccount: ncAccount,
|
||||
domain: domain,
|
||||
progress: progress
|
||||
)
|
||||
if error != nil {
|
||||
signalEnumerator(completionHandler: { _ in })
|
||||
}
|
||||
} else {
|
||||
Logger.fileProviderExtension.debug("Nothing more to do with \(item.itemIdentifier.rawValue, privacy: .public) \(item.filename, privacy: .public), modifications complete")
|
||||
completionHandler(modifiedItem, [], false, nil)
|
||||
completionHandler(modifiedItem ?? item, [], false, error)
|
||||
}
|
||||
|
||||
return progress
|
||||
}
|
||||
|
||||
func deleteItem(identifier: NSFileProviderItemIdentifier, baseVersion version: NSFileProviderItemVersion, options: NSFileProviderDeleteItemOptions = [], request: NSFileProviderRequest, completionHandler: @escaping (Error?) -> Void) -> Progress {
|
||||
|
||||
Logger.fileProviderExtension.debug("Received delete item request for item with identifier: \(identifier.rawValue, privacy: .public)")
|
||||
func deleteItem(
|
||||
identifier: NSFileProviderItemIdentifier,
|
||||
baseVersion _: NSFileProviderItemVersion,
|
||||
options _: NSFileProviderDeleteItemOptions = [],
|
||||
request _: NSFileProviderRequest,
|
||||
completionHandler: @escaping (Error?) -> Void
|
||||
) -> Progress {
|
||||
Logger.fileProviderExtension.debug(
|
||||
"Received delete request for item: \(identifier.rawValue, privacy: .public)"
|
||||
)
|
||||
|
||||
guard ncAccount != nil else {
|
||||
Logger.fileProviderExtension.error("Not deleting item: \(identifier.rawValue, privacy: .public) as account not set up yet")
|
||||
Logger.fileProviderExtension.error(
|
||||
"Not deleting item \(identifier.rawValue, privacy: .public), account not set up yet"
|
||||
)
|
||||
completionHandler(NSFileProviderError(.notAuthenticated))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
let dbManager = NextcloudFilesDatabaseManager.shared
|
||||
let ocId = identifier.rawValue
|
||||
guard let itemMetadata = dbManager.itemMetadataFromOcId(ocId) else {
|
||||
|
||||
guard let item = Item.storedItem(identifier: identifier, usingKit: ncKit) else {
|
||||
Logger.fileProviderExtension.error(
|
||||
"Not deleting item \(identifier.rawValue, privacy: .public), item not found"
|
||||
)
|
||||
completionHandler(NSFileProviderError(.noSuchItem))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
let serverFileNameUrl = itemMetadata.serverUrl + "/" + itemMetadata.fileName
|
||||
guard serverFileNameUrl != "" else {
|
||||
completionHandler(NSFileProviderError(.noSuchItem))
|
||||
return Progress()
|
||||
}
|
||||
|
||||
self.ncKit.deleteFileOrFolder(serverUrlFileName: serverFileNameUrl) { account, error in
|
||||
guard error == .success else {
|
||||
Logger.fileTransfer.error("Could not delete item with ocId \(identifier.rawValue, privacy: .public) at \(serverFileNameUrl, privacy: .public), received error: \(error.errorDescription, privacy: .public)")
|
||||
completionHandler(error.fileProviderError)
|
||||
return
|
||||
let progress = Progress(totalUnitCount: 1)
|
||||
Task {
|
||||
let error = await item.delete()
|
||||
if error != nil {
|
||||
signalEnumerator(completionHandler: { _ in })
|
||||
}
|
||||
|
||||
Logger.fileTransfer.info("Successfully deleted item with identifier: \(identifier.rawValue, privacy: .public) at: \(serverFileNameUrl, privacy: .public)")
|
||||
|
||||
if itemMetadata.directory {
|
||||
_ = dbManager.deleteDirectoryAndSubdirectoriesMetadata(ocId: ocId)
|
||||
} else {
|
||||
dbManager.deleteItemMetadata(ocId: ocId)
|
||||
if dbManager.localFileMetadataFromOcId(ocId) != nil {
|
||||
dbManager.deleteLocalFileMetadata(ocId: ocId)
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(nil)
|
||||
progress.completedUnitCount = 1
|
||||
completionHandler(await item.delete())
|
||||
}
|
||||
|
||||
return Progress()
|
||||
return progress
|
||||
}
|
||||
|
||||
func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier, request: NSFileProviderRequest) throws -> NSFileProviderEnumerator {
|
||||
|
||||
guard let ncAccount = ncAccount else {
|
||||
Logger.fileProviderExtension.error("Not providing enumerator for container with identifier \(containerItemIdentifier.rawValue, privacy: .public) yet as account not set up")
|
||||
func enumerator(
|
||||
for containerItemIdentifier: NSFileProviderItemIdentifier, request _: NSFileProviderRequest
|
||||
) throws -> NSFileProviderEnumerator {
|
||||
guard let ncAccount else {
|
||||
Logger.fileProviderExtension.error(
|
||||
"Not providing enumerator for container with identifier \(containerItemIdentifier.rawValue, privacy: .public) yet as account not set up"
|
||||
)
|
||||
throw NSFileProviderError(.notAuthenticated)
|
||||
}
|
||||
|
||||
return FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier, ncAccount: ncAccount, ncKit: ncKit)
|
||||
return Enumerator(
|
||||
enumeratedItemIdentifier: containerItemIdentifier,
|
||||
ncAccount: ncAccount,
|
||||
ncKit: ncKit,
|
||||
domain: domain,
|
||||
fastEnumeration: config.fastEnumerationEnabled
|
||||
)
|
||||
}
|
||||
|
||||
func materializedItemsDidChange(completionHandler: @escaping () -> Void) {
|
||||
guard let ncAccount = self.ncAccount else {
|
||||
Logger.fileProviderExtension.error("Not purging stale local file metadatas, account not set up")
|
||||
guard let ncAccount else {
|
||||
Logger.fileProviderExtension.error(
|
||||
"Not purging stale local file metadatas, account not set up")
|
||||
completionHandler()
|
||||
return
|
||||
}
|
||||
|
||||
guard let fpManager = NSFileProviderManager(for: domain) else {
|
||||
Logger.fileProviderExtension.error("Could not get file provider manager for domain: \(self.domain.displayName, privacy: .public)")
|
||||
Logger.fileProviderExtension.error(
|
||||
"Could not get file provider manager for domain: \(self.domain.displayName, privacy: .public)"
|
||||
)
|
||||
completionHandler()
|
||||
return
|
||||
}
|
||||
|
||||
let materialisedEnumerator = fpManager.enumeratorForMaterializedItems()
|
||||
let materialisedObserver = FileProviderMaterialisedEnumerationObserver(ncKitAccount: ncAccount.ncKitAccount) { _ in
|
||||
let materialisedObserver = MaterialisedEnumerationObserver(
|
||||
ncKitAccount: ncAccount.ncKitAccount
|
||||
) { _ in
|
||||
completionHandler()
|
||||
}
|
||||
let startingPage = NSFileProviderPage(NSFileProviderPage.initialPageSortedByName as Data)
|
||||
|
@ -622,9 +362,11 @@ class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, NKComm
|
|||
materialisedEnumerator.enumerateItems(for: materialisedObserver, startingAt: startingPage)
|
||||
}
|
||||
|
||||
func signalEnumerator(completionHandler: @escaping(_ error: Error?) -> Void) {
|
||||
guard let fpManager = NSFileProviderManager(for: self.domain) else {
|
||||
Logger.fileProviderExtension.error("Could not get file provider manager for domain, could not signal enumerator. This might lead to future conflicts.")
|
||||
func signalEnumerator(completionHandler: @escaping (_ error: Error?) -> Void) {
|
||||
guard let fpManager = NSFileProviderManager(for: domain) else {
|
||||
Logger.fileProviderExtension.error(
|
||||
"Could not get file provider manager for domain, could not signal enumerator. This might lead to future conflicts."
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
import FileProvider
|
||||
import UniformTypeIdentifiers
|
||||
import NextcloudKit
|
||||
|
||||
class FileProviderItem: NSObject, NSFileProviderItem {
|
||||
|
||||
enum FileProviderItemTransferError: Error {
|
||||
case downloadError
|
||||
case uploadError
|
||||
}
|
||||
|
||||
let metadata: NextcloudItemMetadataTable
|
||||
let parentItemIdentifier: NSFileProviderItemIdentifier
|
||||
let ncKit: NextcloudKit
|
||||
|
||||
var itemIdentifier: NSFileProviderItemIdentifier {
|
||||
return NSFileProviderItemIdentifier(metadata.ocId)
|
||||
}
|
||||
|
||||
var capabilities: NSFileProviderItemCapabilities {
|
||||
guard !metadata.directory else {
|
||||
return [ .allowsAddingSubItems,
|
||||
.allowsContentEnumerating,
|
||||
.allowsReading,
|
||||
.allowsDeleting,
|
||||
.allowsRenaming ]
|
||||
}
|
||||
guard !metadata.lock else {
|
||||
return [ .allowsReading ]
|
||||
}
|
||||
return [ .allowsWriting,
|
||||
.allowsReading,
|
||||
.allowsDeleting,
|
||||
.allowsRenaming,
|
||||
.allowsReparenting ]
|
||||
}
|
||||
|
||||
var itemVersion: NSFileProviderItemVersion {
|
||||
NSFileProviderItemVersion(contentVersion: metadata.etag.data(using: .utf8)!,
|
||||
metadataVersion: metadata.etag.data(using: .utf8)!)
|
||||
}
|
||||
|
||||
var filename: String {
|
||||
return metadata.fileNameView
|
||||
}
|
||||
|
||||
var contentType: UTType {
|
||||
if self.itemIdentifier == .rootContainer || metadata.directory {
|
||||
return .folder
|
||||
}
|
||||
|
||||
let internalType = ncKit.nkCommonInstance.getInternalType(fileName: metadata.fileNameView,
|
||||
mimeType: "",
|
||||
directory: metadata.directory)
|
||||
return UTType(filenameExtension: internalType.ext) ?? .content
|
||||
}
|
||||
|
||||
var documentSize: NSNumber? {
|
||||
return NSNumber(value: metadata.size)
|
||||
}
|
||||
|
||||
var creationDate: Date? {
|
||||
return metadata.creationDate as Date
|
||||
}
|
||||
|
||||
var lastUsedDate: Date? {
|
||||
return metadata.date as Date
|
||||
}
|
||||
|
||||
var contentModificationDate: Date? {
|
||||
return metadata.date as Date
|
||||
}
|
||||
|
||||
var isDownloaded: Bool {
|
||||
return metadata.directory || NextcloudFilesDatabaseManager.shared.localFileMetadataFromOcId(metadata.ocId) != nil
|
||||
}
|
||||
|
||||
var isDownloading: Bool {
|
||||
return metadata.status == NextcloudItemMetadataTable.Status.downloading.rawValue
|
||||
}
|
||||
|
||||
var downloadingError: Error? {
|
||||
if metadata.status == NextcloudItemMetadataTable.Status.downloadError.rawValue {
|
||||
return FileProviderItemTransferError.downloadError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var isUploaded: Bool {
|
||||
return NextcloudFilesDatabaseManager.shared.localFileMetadataFromOcId(metadata.ocId) != nil
|
||||
}
|
||||
|
||||
var isUploading: Bool {
|
||||
return metadata.status == NextcloudItemMetadataTable.Status.uploading.rawValue
|
||||
}
|
||||
|
||||
var uploadingError: Error? {
|
||||
if metadata.status == NextcloudItemMetadataTable.Status.uploadError.rawValue {
|
||||
return FileProviderItemTransferError.uploadError
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var childItemCount: NSNumber? {
|
||||
if metadata.directory {
|
||||
return NSNumber(integerLiteral: NextcloudFilesDatabaseManager.shared.childItemsForDirectory(metadata).count)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
required init(metadata: NextcloudItemMetadataTable, parentItemIdentifier: NSFileProviderItemIdentifier, ncKit: NextcloudKit) {
|
||||
self.metadata = metadata
|
||||
self.parentItemIdentifier = parentItemIdentifier
|
||||
self.ncKit = ncKit
|
||||
super.init()
|
||||
}
|
||||
}
|
|
@ -12,45 +12,55 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import FileProvider
|
||||
import Foundation
|
||||
import NextcloudFileProviderKit
|
||||
import OSLog
|
||||
|
||||
class FileProviderMaterialisedEnumerationObserver : NSObject, NSFileProviderEnumerationObserver {
|
||||
class FileProviderMaterialisedEnumerationObserver: NSObject, NSFileProviderEnumerationObserver {
|
||||
let ncKitAccount: String
|
||||
let completionHandler: (_ deletedOcIds: Set<String>) -> Void
|
||||
var allEnumeratedItemIds: Set<String> = Set<String>()
|
||||
var allEnumeratedItemIds: Set<String> = .init()
|
||||
|
||||
required init(ncKitAccount: String, completionHandler: @escaping(_ deletedOcIds: Set<String>) -> Void) {
|
||||
required init(
|
||||
ncKitAccount: String, completionHandler: @escaping (_ deletedOcIds: Set<String>) -> Void
|
||||
) {
|
||||
self.ncKitAccount = ncKitAccount
|
||||
self.completionHandler = completionHandler
|
||||
super.init()
|
||||
}
|
||||
|
||||
func didEnumerate(_ updatedItems: [NSFileProviderItemProtocol]) {
|
||||
let updatedItemsIds = Array(updatedItems.map { $0.itemIdentifier.rawValue })
|
||||
let updatedItemsIds = Array(updatedItems.map(\.itemIdentifier.rawValue))
|
||||
|
||||
for updatedItemsId in updatedItemsIds {
|
||||
allEnumeratedItemIds.insert(updatedItemsId)
|
||||
}
|
||||
}
|
||||
|
||||
func finishEnumerating(upTo nextPage: NSFileProviderPage?) {
|
||||
func finishEnumerating(upTo _: NSFileProviderPage?) {
|
||||
Logger.materialisedFileHandling.debug("Handling enumerated materialised items.")
|
||||
FileProviderMaterialisedEnumerationObserver.handleEnumeratedItems(self.allEnumeratedItemIds,
|
||||
account: self.ncKitAccount,
|
||||
completionHandler: self.completionHandler)
|
||||
FileProviderMaterialisedEnumerationObserver.handleEnumeratedItems(
|
||||
allEnumeratedItemIds,
|
||||
account: ncKitAccount,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func finishEnumeratingWithError(_ error: Error) {
|
||||
Logger.materialisedFileHandling.error("Ran into error when enumerating materialised items: \(error.localizedDescription, privacy: .public). Handling items enumerated so far")
|
||||
FileProviderMaterialisedEnumerationObserver.handleEnumeratedItems(self.allEnumeratedItemIds,
|
||||
account: self.ncKitAccount,
|
||||
completionHandler: self.completionHandler)
|
||||
Logger.materialisedFileHandling.error(
|
||||
"Ran into error when enumerating materialised items: \(error.localizedDescription, privacy: .public). Handling items enumerated so far"
|
||||
)
|
||||
FileProviderMaterialisedEnumerationObserver.handleEnumeratedItems(
|
||||
allEnumeratedItemIds,
|
||||
account: ncKitAccount,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
static func handleEnumeratedItems(_ itemIds: Set<String>, account: String, completionHandler: @escaping(_ deletedOcIds: Set<String>) -> Void) {
|
||||
let dbManager = NextcloudFilesDatabaseManager.shared
|
||||
static func handleEnumeratedItems(
|
||||
_ itemIds: Set<String>, account: String,
|
||||
completionHandler: @escaping (_ deletedOcIds: Set<String>) -> Void
|
||||
) {
|
||||
let dbManager = FilesDatabaseManager.shared
|
||||
let databaseLocalFileMetadatas = dbManager.localFileMetadatas(account: account)
|
||||
var noLongerMaterialisedIds = Set<String>()
|
||||
|
||||
|
@ -60,12 +70,13 @@ class FileProviderMaterialisedEnumerationObserver : NSObject, NSFileProviderEnum
|
|||
|
||||
guard itemIds.contains(localFileOcId) else {
|
||||
noLongerMaterialisedIds.insert(localFileOcId)
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
Logger.materialisedFileHandling.info("Cleaning up local file metadatas for unmaterialised items")
|
||||
Logger.materialisedFileHandling.info(
|
||||
"Cleaning up local file metadatas for unmaterialised items")
|
||||
for itemId in noLongerMaterialisedIds {
|
||||
dbManager.deleteLocalFileMetadata(ocId: itemId)
|
||||
}
|
||||
|
|
|
@ -24,25 +24,27 @@ class FileProviderSocketLineProcessor: NSObject, LineProcessor {
|
|||
}
|
||||
|
||||
func process(_ line: String) {
|
||||
if (line.contains("~")) { // We use this as the separator specifically in ACCOUNT_DETAILS
|
||||
Logger.desktopClientConnection.debug("Processing file provider line with potentially sensitive user data")
|
||||
if line.contains("~") { // We use this as the separator specifically in ACCOUNT_DETAILS
|
||||
Logger.desktopClientConnection.debug(
|
||||
"Processing file provider line with potentially sensitive user data")
|
||||
} else {
|
||||
Logger.desktopClientConnection.debug("Processing file provider line: \(line, privacy: .public)")
|
||||
Logger.desktopClientConnection.debug(
|
||||
"Processing file provider line: \(line, privacy: .public)")
|
||||
}
|
||||
|
||||
let splitLine = line.split(separator: ":", maxSplits: 1)
|
||||
guard let commandSubsequence = splitLine.first else {
|
||||
Logger.desktopClientConnection.error("Input line did not have a first element")
|
||||
return;
|
||||
return
|
||||
}
|
||||
let command = String(commandSubsequence);
|
||||
let command = String(commandSubsequence)
|
||||
|
||||
Logger.desktopClientConnection.debug("Received command: \(command, privacy: .public)")
|
||||
if (command == "SEND_FILE_PROVIDER_DOMAIN_IDENTIFIER") {
|
||||
if command == "SEND_FILE_PROVIDER_DOMAIN_IDENTIFIER" {
|
||||
delegate.sendFileProviderDomainIdentifier()
|
||||
} else if (command == "ACCOUNT_NOT_AUTHENTICATED") {
|
||||
} else if command == "ACCOUNT_NOT_AUTHENTICATED" {
|
||||
delegate.removeAccountConfig()
|
||||
} else if (command == "ACCOUNT_DETAILS") {
|
||||
} else if command == "ACCOUNT_DETAILS" {
|
||||
guard let accountDetailsSubsequence = splitLine.last else { return }
|
||||
let splitAccountDetails = accountDetailsSubsequence.split(separator: "~", maxSplits: 2)
|
||||
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import FileProvider
|
||||
import OSLog
|
||||
|
||||
func pathForAppGroupContainer() -> URL? {
|
||||
guard let appGroupIdentifier = Bundle.main.object(forInfoDictionaryKey: "SocketApiPrefix") as? String else {
|
||||
Logger.localFileOps.critical("Could not get container url as missing SocketApiPrefix info in app Info.plist")
|
||||
return nil
|
||||
}
|
||||
|
||||
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
|
||||
}
|
||||
|
||||
func pathForFileProviderExtData() -> URL? {
|
||||
let containerUrl = pathForAppGroupContainer()
|
||||
return containerUrl?.appendingPathComponent("FileProviderExt/")
|
||||
}
|
||||
|
||||
func pathForFileProviderTempFilesForDomain(_ domain: NSFileProviderDomain) throws -> URL? {
|
||||
guard let fpManager = NSFileProviderManager(for: domain) else {
|
||||
Logger.localFileOps.error("Unable to get file provider manager for domain: \(domain.displayName, privacy: .public)")
|
||||
throw NSFileProviderError(.providerNotFound)
|
||||
}
|
||||
|
||||
let fileProviderDataUrl = try fpManager.temporaryDirectoryURL()
|
||||
return fileProviderDataUrl.appendingPathComponent("TemporaryNextcloudFiles/")
|
||||
}
|
||||
|
||||
func localPathForNCFile(ocId: String, fileNameView: String, domain: NSFileProviderDomain) throws -> URL {
|
||||
guard let fileProviderFilesPathUrl = try pathForFileProviderTempFilesForDomain(domain) else {
|
||||
Logger.localFileOps.error("Unable to get path for file provider temp files for domain: \(domain.displayName, privacy: .public)")
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
|
||||
let filePathUrl = fileProviderFilesPathUrl.appendingPathComponent(fileNameView)
|
||||
let filePath = filePathUrl.path
|
||||
|
||||
if !FileManager.default.fileExists(atPath: filePath) {
|
||||
FileManager.default.createFile(atPath: filePath, contents: nil)
|
||||
}
|
||||
|
||||
return filePathUrl
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2022 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import FileProvider
|
||||
|
||||
class NextcloudAccount: NSObject {
|
||||
static let webDavFilesUrlSuffix: String = "/remote.php/dav/files/"
|
||||
let username, password, ncKitAccount, serverUrl, davFilesUrl: String
|
||||
|
||||
init(user: String, serverUrl: String, password: String) {
|
||||
self.username = user
|
||||
self.password = password
|
||||
self.ncKitAccount = user + " " + serverUrl
|
||||
self.serverUrl = serverUrl
|
||||
self.davFilesUrl = serverUrl + NextcloudAccount.webDavFilesUrlSuffix + user
|
||||
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
#ifndef ClientCommunicationProtocol_h
|
||||
#define ClientCommunicationProtocol_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@protocol ClientCommunicationProtocol
|
||||
|
||||
- (void)getExtensionAccountIdWithCompletionHandler:(void(^)(NSString *extensionAccountId, NSError *error))completionHandler;
|
||||
- (void)configureAccountWithUser:(NSString *)user
|
||||
serverUrl:(NSString *)serverUrl
|
||||
password:(NSString *)password;
|
||||
- (void)removeAccountConfig;
|
||||
- (void)createDebugLogStringWithCompletionHandler:(void(^)(NSString *debugLogString, NSError *error))completionHandler;
|
||||
- (void)getFastEnumerationStateWithCompletionHandler:(void(^)(BOOL enabled, BOOL set))completionHandler;
|
||||
- (void)setFastEnumerationEnabled:(BOOL)enabled;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* ClientCommunicationProtocol_h */
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright (C) 2023 by Claudio Cambra <claudio.cambra@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.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import FileProvider
|
||||
import OSLog
|
||||
|
||||
class ClientCommunicationService: NSObject, NSFileProviderServiceSource, NSXPCListenerDelegate, ClientCommunicationProtocol {
|
||||
let listener = NSXPCListener.anonymous()
|
||||
let serviceName = NSFileProviderServiceName("com.nextcloud.desktopclient.ClientCommunicationService")
|
||||
let fpExtension: FileProviderExtension
|
||||
|
||||
init(fpExtension: FileProviderExtension) {
|
||||
Logger.desktopClientConnection.debug("Instantiating client communication service")
|
||||
self.fpExtension = fpExtension
|
||||
super.init()
|
||||
}
|
||||
|
||||
func makeListenerEndpoint() throws -> NSXPCListenerEndpoint {
|
||||
listener.delegate = self
|
||||
listener.resume()
|
||||
return listener.endpoint
|
||||
}
|
||||
|
||||
func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
|
||||
newConnection.exportedInterface = NSXPCInterface(with: ClientCommunicationProtocol.self)
|
||||
newConnection.exportedObject = self
|
||||
newConnection.resume()
|
||||
return true
|
||||
}
|
||||
|
||||
//MARK: - Client Communication Protocol methods
|
||||
|
||||
func getExtensionAccountId(completionHandler: @escaping (String?, Error?) -> Void) {
|
||||
let accountUserId = self.fpExtension.domain.identifier.rawValue
|
||||
Logger.desktopClientConnection.info("Sending extension account ID \(accountUserId, privacy: .public)")
|
||||
completionHandler(accountUserId, nil)
|
||||
}
|
||||
|
||||
func configureAccount(withUser user: String,
|
||||
serverUrl: String,
|
||||
password: String) {
|
||||
Logger.desktopClientConnection.info("Received configure account information over client communication service")
|
||||
self.fpExtension.setupDomainAccount(user: user,
|
||||
serverUrl: serverUrl,
|
||||
password: password)
|
||||
}
|
||||
|
||||
func removeAccountConfig() {
|
||||
self.fpExtension.removeAccountConfig()
|
||||
}
|
||||
|
||||
func createDebugLogString(completionHandler: ((String?, Error?) -> Void)!) {
|
||||
if #available(macOSApplicationExtension 12.0, *) {
|
||||
let (logs, error) = Logger.logEntries()
|
||||
guard error == nil else {
|
||||
Logger.logger.error("Cannot create debug archive, received error: \(error, privacy: .public)")
|
||||
completionHandler(nil, error)
|
||||
return
|
||||
}
|
||||
guard let logs = logs else {
|
||||
Logger.logger.error("Canot create debug archive with nil logs.")
|
||||
completionHandler(nil, nil)
|
||||
return
|
||||
}
|
||||
completionHandler(logs.joined(separator: "\n"), nil)
|
||||
}
|
||||
}
|
||||
|
||||
func getFastEnumerationState(completionHandler: @escaping (Bool, Bool) -> Void) {
|
||||
let enabled = fpExtension.config.fastEnumerationEnabled
|
||||
let set = fpExtension.config.fastEnumerationSet
|
||||
completionHandler(enabled, set)
|
||||
}
|
||||
|
||||
func setFastEnumerationEnabled(_ enabled: Bool) {
|
||||
fpExtension.config.fastEnumerationEnabled = enabled
|
||||
Logger.fileProviderExtension.info("Fast enumeration setting changed to: \(enabled, privacy: .public)")
|
||||
|
||||
guard enabled else { return }
|
||||
// If enabled, start full enumeration
|
||||
guard let fpManager = NSFileProviderManager(for: fpExtension.domain) else {
|
||||
let domainName = self.fpExtension.domain.displayName
|
||||
Logger.fileProviderExtension.error("Could not get file provider manager for domain \(domainName, privacy: .public), cannot run enumeration after fast enumeration setting change")
|
||||
return
|
||||
}
|
||||
|
||||
fpManager.signalEnumerator(for: .workingSet) { error in
|
||||
if error != nil {
|
||||
Logger.fileProviderExtension.error("Error signalling enumerator for working set, received error: \(error!.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// FPUIExtensionCommunicationProtocol.swift
|
||||
// FileProviderExt
|
||||
//
|
||||
// Created by Claudio Cambra on 21/2/24.
|
||||
//
|
||||
|
||||
import FileProvider
|
||||
import NextcloudKit
|
||||
|
||||
let fpUiExtensionServiceName = NSFileProviderServiceName(
|
||||
"com.nextcloud.desktopclient.FPUIExtensionService"
|
||||
)
|
||||
|
||||
@objc protocol FPUIExtensionService {
|
||||
func credentials() async -> NSDictionary
|
||||
func itemServerPath(identifier: NSFileProviderItemIdentifier) async -> NSString?
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// FPUIExtensionCommunicationService.swift
|
||||
// FileProviderExt
|
||||
//
|
||||
// Created by Claudio Cambra on 21/2/24.
|
||||
//
|
||||
|
||||
import FileProvider
|
||||
import Foundation
|
||||
import NextcloudKit
|
||||
import NextcloudFileProviderKit
|
||||
import OSLog
|
||||
|
||||
class FPUIExtensionServiceSource: NSObject, NSFileProviderServiceSource, NSXPCListenerDelegate, FPUIExtensionService {
|
||||
let listener = NSXPCListener.anonymous()
|
||||
let serviceName = fpUiExtensionServiceName
|
||||
let fpExtension: FileProviderExtension
|
||||
|
||||
init(fpExtension: FileProviderExtension) {
|
||||
Logger.fpUiExtensionService.debug("Instantiating FPUIExtensionService service")
|
||||
self.fpExtension = fpExtension
|
||||
super.init()
|
||||
}
|
||||
|
||||
func makeListenerEndpoint() throws -> NSXPCListenerEndpoint {
|
||||
listener.delegate = self
|
||||
listener.resume()
|
||||
return listener.endpoint
|
||||
}
|
||||
|
||||
func listener(
|
||||
_ listener: NSXPCListener,
|
||||
shouldAcceptNewConnection newConnection: NSXPCConnection
|
||||
) -> Bool {
|
||||
newConnection.exportedInterface = NSXPCInterface(with: FPUIExtensionService.self)
|
||||
newConnection.exportedObject = self
|
||||
newConnection.resume()
|
||||
return true
|
||||
}
|
||||
|
||||
//MARK: - FPUIExtensionService protocol methods
|
||||
|
||||
func credentials() async -> NSDictionary {
|
||||
return (fpExtension.ncAccount?.dictionary() ?? [:]) as NSDictionary
|
||||
}
|
||||
|
||||
func itemServerPath(identifier: NSFileProviderItemIdentifier) async -> NSString? {
|
||||
let rawIdentifier = identifier.rawValue
|
||||
Logger.shares.info("Fetching shares for item \(rawIdentifier, privacy: .public)")
|
||||
|
||||
guard let baseUrl = fpExtension.ncAccount?.davFilesUrl else {
|
||||
Logger.shares.error("Could not fetch shares as ncAccount on parent extension is nil")
|
||||
return nil
|
||||
}
|
||||
|
||||
let dbManager = FilesDatabaseManager.shared
|
||||
guard let item = dbManager.itemMetadataFromFileProviderItemIdentifier(identifier) else {
|
||||
Logger.shares.error("No item \(rawIdentifier, privacy: .public) in db, no shares.")
|
||||
return nil
|
||||
}
|
||||
|
||||
let completePath = item.serverUrl + "/" + item.fileName
|
||||
return completePath.replacingOccurrences(of: baseUrl, with: "") as NSString
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// DocumentActionViewController.swift
|
||||
// FileProviderUIExt
|
||||
//
|
||||
// Created by Claudio Cambra on 20/2/24.
|
||||
//
|
||||
|
||||
import FileProviderUI
|
||||
import OSLog
|
||||
|
||||
class DocumentActionViewController: FPUIActionExtensionViewController {
|
||||
var domain: NSFileProviderDomain {
|
||||
guard let identifier = extensionContext.domainIdentifier else {
|
||||
fatalError("not expected to be called with default domain")
|
||||
}
|
||||
return NSFileProviderDomain(
|
||||
identifier: NSFileProviderDomainIdentifier(rawValue: identifier.rawValue),
|
||||
displayName: ""
|
||||
)
|
||||
}
|
||||
|
||||
func prepare(childViewController: NSViewController) {
|
||||
addChild(childViewController)
|
||||
view.addSubview(childViewController.view)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
view.leadingAnchor.constraint(equalTo: childViewController.view.leadingAnchor),
|
||||
view.trailingAnchor.constraint(equalTo: childViewController.view.trailingAnchor),
|
||||
view.topAnchor.constraint(equalTo: childViewController.view.topAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: childViewController.view.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
override func prepare(
|
||||
forAction actionIdentifier: String, itemIdentifiers: [NSFileProviderItemIdentifier]
|
||||
) {
|
||||
Logger.actionViewController.info("Preparing action: \(actionIdentifier, privacy: .public)")
|
||||
|
||||
if actionIdentifier == "com.nextcloud.desktopclient.FileProviderUIExt.ShareAction" {
|
||||
prepare(childViewController: ShareViewController(itemIdentifiers))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override func prepare(forError error: Error) {
|
||||
Logger.actionViewController.info(
|
||||
"""
|
||||
Preparing for error: \(error.localizedDescription, privacy: .public)
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
override public func loadView() {
|
||||
self.view = NSView()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// Logger+Extensions.swift
|
||||
// FileProviderUIExt
|
||||
//
|
||||
// Created by Claudio Cambra on 21/2/24.
|
||||
//
|
||||
|
||||
import OSLog
|
||||
|
||||
extension Logger {
|
||||
private static var subsystem = Bundle.main.bundleIdentifier!
|
||||
|
||||
static let actionViewController = Logger(subsystem: subsystem, category: "actionViewController")
|
||||
static let shareCapabilities = Logger(subsystem: subsystem, category: "shareCapabilities")
|
||||
static let shareController = Logger(subsystem: subsystem, category: "shareController")
|
||||
static let shareeDataSource = Logger(subsystem: subsystem, category: "shareeDataSource")
|
||||
static let sharesDataSource = Logger(subsystem: subsystem, category: "sharesDataSource")
|
||||
static let shareOptionsView = Logger(subsystem: subsystem, category: "shareOptionsView")
|
||||
static let shareViewController = Logger(subsystem: subsystem, category: "shareViewController")
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
//
|
||||
// NKShare+Extensions.swift
|
||||
// FileProviderUIExt
|
||||
//
|
||||
// Created by Claudio Cambra on 28/2/24.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import NextcloudKit
|
||||
|
||||
extension NKShare {
|
||||
enum ShareType: Int {
|
||||
case user = 0
|
||||
case group = 1
|
||||
case publicLink = 3
|
||||
case email = 4
|
||||
case federatedCloud = 6
|
||||
case circle = 7
|
||||
case talkConversation = 10
|
||||
}
|
||||
|
||||
enum PermissionValues: Int {
|
||||
case readShare = 1
|
||||
case updateShare = 2
|
||||
case createShare = 4
|
||||
case deleteShare = 8
|
||||
case shareShare = 16
|
||||
case all = 31
|
||||
}
|
||||
|
||||
var typeImage: NSImage? {
|
||||
var image: NSImage?
|
||||
switch shareType {
|
||||
case ShareType.user.rawValue:
|
||||
image = NSImage(
|
||||
systemSymbolName: "person.circle.fill",
|
||||
accessibilityDescription: "User share icon"
|
||||
)
|
||||
case ShareType.group.rawValue:
|
||||
image = NSImage(
|
||||
systemSymbolName: "person.2.circle.fill",
|
||||
accessibilityDescription: "Group share icon"
|
||||
)
|
||||
case ShareType.publicLink.rawValue:
|
||||
image = NSImage(
|
||||
systemSymbolName: "link.circle.fill",
|
||||
accessibilityDescription: "Public link share icon"
|
||||
)
|
||||
case ShareType.email.rawValue:
|
||||
image = NSImage(
|
||||
systemSymbolName: "envelope.circle.fill",
|
||||
accessibilityDescription: "Email share icon"
|
||||
)
|
||||
case ShareType.federatedCloud.rawValue:
|
||||
image = NSImage(
|
||||
systemSymbolName: "cloud.circle.fill",
|
||||
accessibilityDescription: "Federated cloud share icon"
|
||||
)
|
||||
case ShareType.circle.rawValue:
|
||||
image = NSImage(
|
||||
systemSymbolName: "circle.circle.fill",
|
||||
accessibilityDescription: "Circle share icon"
|
||||
)
|
||||
case ShareType.talkConversation.rawValue:
|
||||
image = NSImage(
|
||||
systemSymbolName: "message.circle.fill",
|
||||
accessibilityDescription: "Talk conversation share icon"
|
||||
)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
var config = NSImage.SymbolConfiguration(textStyle: .body, scale: .large)
|
||||
if #available(macOS 12.0, *) {
|
||||
config = config.applying(
|
||||
.init(paletteColors: [.controlBackgroundColor, .controlAccentColor])
|
||||
)
|
||||
}
|
||||
return image?.withSymbolConfiguration(config)
|
||||
}
|
||||
|
||||
var displayString: String {
|
||||
if label != "" {
|
||||
return label
|
||||
}
|
||||
|
||||
switch shareType {
|
||||
case ShareType.user.rawValue:
|
||||
return "User share (\(shareWith))"
|
||||
case ShareType.group.rawValue:
|
||||
return "Group share (\(shareWith))"
|
||||
case ShareType.publicLink.rawValue:
|
||||
return "Public link share"
|
||||
case ShareType.email.rawValue:
|
||||
return "Email share (\(shareWith))"
|
||||
case ShareType.federatedCloud.rawValue:
|
||||
return "Federated cloud share (\(shareWith))"
|
||||
case ShareType.circle.rawValue:
|
||||
return "Circle share (\(shareWith))"
|
||||
case ShareType.talkConversation.rawValue:
|
||||
return "Talk conversation share (\(shareWith))"
|
||||
default:
|
||||
return "Unknown share"
|
||||
}
|
||||
}
|
||||
|
||||
var expirationDateString: String? {
|
||||
guard let date = expirationDate else { return nil }
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss"
|
||||
return dateFormatter.string(from: date as Date)
|
||||
}
|
||||
|
||||
var shareesCanEdit: Bool {
|
||||
get { (permissions & PermissionValues.updateShare.rawValue) != 0 }
|
||||
set {
|
||||
if newValue {
|
||||
permissions |= NKShare.PermissionValues.updateShare.rawValue
|
||||
} else {
|
||||
permissions &= ~NKShare.PermissionValues.updateShare.rawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func formattedDateString(date: Date) -> String {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss"
|
||||
return dateFormatter.string(from: date)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>$(OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX)$(OC_APPLICATION_REV_DOMAIN)</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>$(OC_APPLICATION_NAME) File Provider UI Extension</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(OC_APPLICATION_REV_DOMAIN).$(PRODUCT_NAME)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionFileProviderActions</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSExtensionFileProviderActionActivationRule</key>
|
||||
<string>TRUEPREDICATE</string>
|
||||
<key>NSExtensionFileProviderActionIdentifier</key>
|
||||
<string>com.nextcloud.desktopclient.FileProviderUIExt.ShareAction</string>
|
||||
<key>NSExtensionFileProviderActionName</key>
|
||||
<string>Share options</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.fileprovider-actionsui</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).DocumentActionViewController</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,155 @@
|
|||
//
|
||||
// ShareController.swift
|
||||
// FileProviderUIExt
|
||||
//
|
||||
// Created by Claudio Cambra on 4/3/24.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import NextcloudKit
|
||||
import OSLog
|
||||
|
||||
class ShareController: ObservableObject {
|
||||
@Published private(set) var share: NKShare
|
||||
private let kit: NextcloudKit
|
||||
|
||||
static func create(
|
||||
kit: NextcloudKit,
|
||||
shareType: NKShare.ShareType,
|
||||
itemServerRelativePath: String,
|
||||
shareWith: String?,
|
||||
password: String? = nil,
|
||||
expireDate: String? = nil,
|
||||
permissions: Int = 1,
|
||||
publicUpload: Bool = false,
|
||||
note: String? = nil,
|
||||
label: String? = nil,
|
||||
hideDownload: Bool,
|
||||
attributes: String? = nil,
|
||||
options: NKRequestOptions = NKRequestOptions()
|
||||
) async -> NKError? {
|
||||
Logger.shareController.info("Creating share: \(itemServerRelativePath)")
|
||||
return await withCheckedContinuation { continuation in
|
||||
if shareType == .publicLink {
|
||||
kit.createShareLink(
|
||||
path: itemServerRelativePath,
|
||||
hideDownload: hideDownload,
|
||||
publicUpload: publicUpload,
|
||||
password: password,
|
||||
permissions: permissions,
|
||||
options: options
|
||||
) { account, share, data, error in
|
||||
defer { continuation.resume(returning: error) }
|
||||
guard error == .success else {
|
||||
Logger.shareController.error(
|
||||
"""
|
||||
Error creating link share: \(error.errorDescription, privacy: .public)
|
||||
"""
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
guard let shareWith = shareWith else {
|
||||
let errorString = "No recipient for share!"
|
||||
Logger.shareController.error("\(errorString, privacy: .public)")
|
||||
let error = NKError(statusCode: 0, fallbackDescription: errorString)
|
||||
continuation.resume(returning: error)
|
||||
return
|
||||
}
|
||||
|
||||
kit.createShare(
|
||||
path: itemServerRelativePath,
|
||||
shareType: shareType.rawValue,
|
||||
shareWith: shareWith,
|
||||
password: password,
|
||||
permissions: permissions,
|
||||
options: options,
|
||||
attributes: attributes
|
||||
) { account, share, data, error in
|
||||
defer { continuation.resume(returning: error) }
|
||||
guard error == .success else {
|
||||
Logger.shareController.error(
|
||||
"""
|
||||
Error creating share: \(error.errorDescription, privacy: .public)
|
||||
"""
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(share: NKShare, kit: NextcloudKit) {
|
||||
self.share = share
|
||||
self.kit = kit
|
||||
}
|
||||
|
||||
func save(
|
||||
password: String? = nil,
|
||||
expireDate: String? = nil,
|
||||
permissions: Int = 1,
|
||||
publicUpload: Bool = false,
|
||||
note: String? = nil,
|
||||
label: String? = nil,
|
||||
hideDownload: Bool,
|
||||
attributes: String? = nil,
|
||||
options: NKRequestOptions = NKRequestOptions()
|
||||
) async -> NKError? {
|
||||
Logger.shareController.info("Saving share: \(self.share.url, privacy: .public)")
|
||||
return await withCheckedContinuation { continuation in
|
||||
kit.updateShare(
|
||||
idShare: share.idShare,
|
||||
password: password,
|
||||
expireDate: expireDate,
|
||||
permissions: permissions,
|
||||
publicUpload: publicUpload,
|
||||
note: note,
|
||||
label: label,
|
||||
hideDownload: hideDownload,
|
||||
attributes: attributes,
|
||||
options: options
|
||||
) { account, share, data, error in
|
||||
Logger.shareController.info(
|
||||
"""
|
||||
Received update response: \(share?.url ?? "", privacy: .public)
|
||||
"""
|
||||
)
|
||||
defer { continuation.resume(returning: error) }
|
||||
guard error == .success, let share = share else {
|
||||
Logger.shareController.error(
|
||||
"""
|
||||
Error updating save: \(error.errorDescription, privacy: .public)
|
||||
"""
|
||||
)
|
||||
return
|
||||
}
|
||||
self.share = share
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func delete() async -> NKError? {
|
||||
Logger.shareController.info("Deleting share: \(self.share.url, privacy: .public)")
|
||||
return await withCheckedContinuation { continuation in
|
||||
kit.deleteShare(idShare: share.idShare) { account, error in
|
||||
Logger.shareController.info(
|
||||
"""
|
||||
Received delete response: \(self.share.url, privacy: .public)
|
||||
"""
|
||||
)
|
||||
defer { continuation.resume(returning: error) }
|
||||
guard error == .success else {
|
||||
Logger.shareController.error(
|
||||
"""
|
||||
Error deleting save: \(error.errorDescription, privacy: .public)
|
||||
"""
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,354 @@
|
|||
//
|
||||
// ShareOptionsView.swift
|
||||
// FileProviderUIExt
|
||||
//
|
||||
// Created by Claudio Cambra on 28/2/24.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Combine
|
||||
import NextcloudKit
|
||||
import OSLog
|
||||
import SuggestionsTextFieldKit
|
||||
|
||||
class ShareOptionsView: NSView {
|
||||
@IBOutlet private weak var optionsTitleTextField: NSTextField!
|
||||
@IBOutlet private weak var shareRecipientTextField: NSTextField! // Hide if public link share
|
||||
@IBOutlet private weak var labelTextField: NSTextField!
|
||||
@IBOutlet private weak var uploadEditPermissionCheckbox: NSButton!
|
||||
@IBOutlet private weak var hideDownloadCheckbox: NSButton!
|
||||
@IBOutlet private weak var passwordProtectCheckbox: NSButton!
|
||||
@IBOutlet private weak var passwordSecureField: NSSecureTextField!
|
||||
@IBOutlet private weak var expirationDateCheckbox: NSButton!
|
||||
@IBOutlet private weak var expirationDatePicker: NSDatePicker!
|
||||
@IBOutlet private weak var noteForRecipientCheckbox: NSButton!
|
||||
@IBOutlet private weak var noteTextField: NSTextField!
|
||||
@IBOutlet private weak var saveButton: NSButton!
|
||||
@IBOutlet private weak var deleteButton: NSButton!
|
||||
@IBOutlet private weak var shareTypePicker: NSPopUpButton!
|
||||
@IBOutlet private weak var publicLinkShareMenuItem: NSMenuItem!
|
||||
@IBOutlet private weak var userShareMenuItem: NSMenuItem!
|
||||
@IBOutlet private weak var groupShareMenuItem: NSMenuItem!
|
||||
@IBOutlet private weak var emailShareMenuItem: NSMenuItem!
|
||||
@IBOutlet private weak var federatedCloudShareMenuItem: NSMenuItem!
|
||||
@IBOutlet private weak var circleShare: NSMenuItem!
|
||||
@IBOutlet private weak var talkConversationShare: NSMenuItem!
|
||||
|
||||
var kit: NextcloudKit? {
|
||||
didSet {
|
||||
Logger.shareOptionsView.info("Setting up the kit.")
|
||||
guard let kit = kit else {
|
||||
Logger.shareOptionsView.error("Could not configure suggestions data source.")
|
||||
return
|
||||
}
|
||||
|
||||
suggestionsTextFieldDelegate.suggestionsDataSource = ShareeSuggestionsDataSource(
|
||||
kit: kit
|
||||
)
|
||||
suggestionsTextFieldDelegate.confirmationHandler = { suggestion in
|
||||
guard let sharee = suggestion?.data as? NKSharee else { return }
|
||||
self.shareRecipientTextField.stringValue = sharee.shareWith
|
||||
Logger.shareOptionsView.debug("Chose sharee \(sharee.shareWith, privacy: .public)")
|
||||
}
|
||||
suggestionsTextFieldDelegate.targetTextField = shareRecipientTextField
|
||||
}
|
||||
}
|
||||
var dataSource: ShareTableViewDataSource?
|
||||
var controller: ShareController? {
|
||||
didSet {
|
||||
guard controller != nil else { return }
|
||||
optionsTitleTextField.stringValue = "Share options"
|
||||
deleteButton.title = "Delete"
|
||||
deleteButton.image = NSImage(
|
||||
systemSymbolName: "trash", accessibilityDescription: "Delete trash icon"
|
||||
)
|
||||
deleteButton.bezelColor = NSColor.systemRed
|
||||
cancellable?.cancel()
|
||||
createMode = false
|
||||
update()
|
||||
cancellable = controller.publisher.sink { _ in self.update() }
|
||||
}
|
||||
}
|
||||
var createMode = false {
|
||||
didSet {
|
||||
Logger.shareOptionsView.info("Create mode set: \(self.createMode, privacy: .public)")
|
||||
shareTypePicker.isHidden = !createMode
|
||||
shareRecipientTextField.isHidden = !createMode
|
||||
labelTextField.isHidden = createMode // Cannot set label on create API call
|
||||
guard createMode else { return }
|
||||
optionsTitleTextField.stringValue = "Create new share"
|
||||
deleteButton.title = "Cancel"
|
||||
deleteButton.image = NSImage(
|
||||
systemSymbolName: "xmark.bin", accessibilityDescription: "Cancel create icon"
|
||||
)
|
||||
deleteButton.bezelColor = NSColor.controlColor
|
||||
cancellable?.cancel()
|
||||
cancellable = nil
|
||||
controller = nil
|
||||
reset()
|
||||
setupCreateForm()
|
||||
}
|
||||
}
|
||||
private var cancellable: AnyCancellable?
|
||||
private var suggestionsWindowController = SuggestionsWindowController()
|
||||
private var suggestionsTextFieldDelegate = SuggestionsTextFieldDelegate()
|
||||
|
||||
private func update() {
|
||||
guard let share = controller?.share else {
|
||||
reset()
|
||||
setAllFields(enabled: false)
|
||||
saveButton.isEnabled = false
|
||||
deleteButton.isEnabled = false
|
||||
return
|
||||
}
|
||||
|
||||
deleteButton.isEnabled = share.canDelete
|
||||
saveButton.isEnabled = share.canEdit
|
||||
|
||||
setAllFields(enabled: share.canEdit)
|
||||
reset()
|
||||
|
||||
shareRecipientTextField.stringValue = share.shareWithDisplayname
|
||||
labelTextField.stringValue = share.label
|
||||
uploadEditPermissionCheckbox.state = share.shareesCanEdit ? .on : .off
|
||||
hideDownloadCheckbox.state = share.hideDownload ? .on : .off
|
||||
passwordProtectCheckbox.state = share.password.isEmpty ? .off : .on
|
||||
passwordSecureField.isHidden = passwordProtectCheckbox.state == .off
|
||||
passwordSecureField.stringValue = share.password
|
||||
expirationDateCheckbox.state = share.expirationDate == nil ? .off : .on
|
||||
expirationDatePicker.isHidden = expirationDateCheckbox.state == .off
|
||||
expirationDatePicker.dateValue = share.expirationDate as? Date ?? Date()
|
||||
noteForRecipientCheckbox.state = share.note.isEmpty ? .off : .on
|
||||
noteTextField.isHidden = noteForRecipientCheckbox.state == .off
|
||||
noteForRecipientCheckbox.stringValue = share.note
|
||||
}
|
||||
|
||||
private func reset() {
|
||||
shareRecipientTextField.stringValue = ""
|
||||
labelTextField.stringValue = ""
|
||||
uploadEditPermissionCheckbox.state = .off
|
||||
hideDownloadCheckbox.state = .off
|
||||
passwordProtectCheckbox.state = .off
|
||||
passwordSecureField.isHidden = true
|
||||
passwordSecureField.stringValue = ""
|
||||
expirationDateCheckbox.state = .off
|
||||
expirationDatePicker.isHidden = true
|
||||
expirationDatePicker.dateValue = NSDate.now
|
||||
expirationDatePicker.minDate = NSDate.now
|
||||
expirationDatePicker.maxDate = nil
|
||||
noteForRecipientCheckbox.state = .off
|
||||
noteTextField.isHidden = true
|
||||
noteTextField.stringValue = ""
|
||||
}
|
||||
|
||||
private func setupCreateForm() {
|
||||
guard createMode else { return }
|
||||
|
||||
setAllFields(enabled: true)
|
||||
|
||||
let type = pickedShareType()
|
||||
shareRecipientTextField.isHidden = type == .publicLink
|
||||
|
||||
if let caps = dataSource?.capabilities?.filesSharing {
|
||||
uploadEditPermissionCheckbox.state =
|
||||
caps.defaultPermissions & NKShare.PermissionValues.updateShare.rawValue != 0
|
||||
? .on : .off
|
||||
|
||||
switch type {
|
||||
case .publicLink:
|
||||
passwordProtectCheckbox.isHidden = false
|
||||
passwordProtectCheckbox.state = caps.publicLink?.passwordEnforced == true ? .on : .off
|
||||
passwordProtectCheckbox.isEnabled = caps.publicLink?.passwordEnforced == false
|
||||
expirationDateCheckbox.state = caps.publicLink?.expireDateEnforced == true ? .on : .off
|
||||
expirationDateCheckbox.isEnabled = caps.publicLink?.expireDateEnforced == false
|
||||
expirationDatePicker.dateValue = Date(
|
||||
timeIntervalSinceNow:
|
||||
TimeInterval((caps.publicLink?.expireDateDays ?? 1) * 24 * 60 * 60)
|
||||
)
|
||||
if caps.publicLink?.expireDateEnforced == true {
|
||||
expirationDatePicker.maxDate = expirationDatePicker.dateValue
|
||||
}
|
||||
case .email:
|
||||
passwordProtectCheckbox.isHidden = caps.email?.passwordEnabled == false
|
||||
passwordProtectCheckbox.state = caps.email?.passwordEnforced == true ? .on : .off
|
||||
default:
|
||||
passwordProtectCheckbox.isHidden = true
|
||||
passwordProtectCheckbox.state = .off
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
passwordSecureField.isHidden = passwordProtectCheckbox.state == .off
|
||||
expirationDatePicker.isHidden = expirationDateCheckbox.state == .off
|
||||
}
|
||||
|
||||
private func setAllFields(enabled: Bool) {
|
||||
shareTypePicker.isEnabled = enabled
|
||||
shareRecipientTextField.isEnabled = enabled
|
||||
labelTextField.isEnabled = enabled
|
||||
uploadEditPermissionCheckbox.isEnabled = enabled
|
||||
hideDownloadCheckbox.isEnabled = enabled
|
||||
passwordProtectCheckbox.isEnabled = enabled
|
||||
passwordSecureField.isEnabled = enabled
|
||||
expirationDateCheckbox.isEnabled = enabled
|
||||
expirationDatePicker.isEnabled = enabled
|
||||
noteForRecipientCheckbox.isEnabled = enabled
|
||||
noteTextField.isEnabled = enabled
|
||||
saveButton.isEnabled = enabled
|
||||
deleteButton.isEnabled = enabled
|
||||
}
|
||||
|
||||
private func pickedShareType() -> NKShare.ShareType {
|
||||
let selectedShareTypeItem = shareTypePicker.selectedItem
|
||||
var selectedShareType = NKShare.ShareType.publicLink
|
||||
if selectedShareTypeItem == publicLinkShareMenuItem {
|
||||
selectedShareType = .publicLink
|
||||
} else if selectedShareTypeItem == userShareMenuItem {
|
||||
selectedShareType = .user
|
||||
} else if selectedShareTypeItem == groupShareMenuItem {
|
||||
selectedShareType = .group
|
||||
} else if selectedShareTypeItem == emailShareMenuItem {
|
||||
selectedShareType = .email
|
||||
} else if selectedShareTypeItem == federatedCloudShareMenuItem {
|
||||
selectedShareType = .federatedCloud
|
||||
} else if selectedShareTypeItem == circleShare {
|
||||
selectedShareType = .circle
|
||||
} else if selectedShareTypeItem == talkConversationShare {
|
||||
selectedShareType = .talkConversation
|
||||
}
|
||||
return selectedShareType
|
||||
}
|
||||
|
||||
@IBAction func shareTypePickerAction(_ sender: Any) {
|
||||
if createMode {
|
||||
setupCreateForm()
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func passwordCheckboxAction(_ sender: Any) {
|
||||
passwordSecureField.isHidden = passwordProtectCheckbox.state == .off
|
||||
}
|
||||
|
||||
@IBAction func expirationDateCheckboxAction(_ sender: Any) {
|
||||
expirationDatePicker.isHidden = expirationDateCheckbox.state == .off
|
||||
}
|
||||
|
||||
@IBAction func noteForRecipientCheckboxAction(_ sender: Any) {
|
||||
noteTextField.isHidden = noteForRecipientCheckbox.state == .off
|
||||
}
|
||||
|
||||
@IBAction func save(_ sender: Any) {
|
||||
Task { @MainActor in
|
||||
let password = passwordProtectCheckbox.state == .on
|
||||
? passwordSecureField.stringValue
|
||||
: ""
|
||||
let expireDate = expirationDateCheckbox.state == .on
|
||||
? NKShare.formattedDateString(date: expirationDatePicker.dateValue)
|
||||
: ""
|
||||
let note = noteForRecipientCheckbox.state == .on
|
||||
? noteTextField.stringValue
|
||||
: ""
|
||||
let label = labelTextField.stringValue
|
||||
let hideDownload = hideDownloadCheckbox.state == .on
|
||||
let uploadAndEdit = uploadEditPermissionCheckbox.state == .on
|
||||
|
||||
guard !createMode else {
|
||||
Logger.shareOptionsView.info("Creating new share!")
|
||||
|
||||
guard let dataSource = dataSource,
|
||||
let kit = kit,
|
||||
let itemServerRelativePath = dataSource.itemServerRelativePath
|
||||
else {
|
||||
Logger.shareOptionsView.error("Cannot create new share due to missing data.")
|
||||
Logger.shareOptionsView.error("dataSource: \(self.dataSource, privacy: .public)")
|
||||
Logger.shareOptionsView.error("kit: \(self.kit, privacy: .public)")
|
||||
Logger.shareOptionsView.error(
|
||||
"path: \(self.dataSource?.itemServerRelativePath ?? "", privacy: .public)"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let selectedShareType = pickedShareType()
|
||||
let shareWith = shareRecipientTextField.stringValue
|
||||
|
||||
var permissions = NKShare.PermissionValues.all.rawValue
|
||||
permissions = uploadAndEdit
|
||||
? permissions | NKShare.PermissionValues.updateShare.rawValue
|
||||
: permissions & ~NKShare.PermissionValues.updateShare.rawValue
|
||||
|
||||
setAllFields(enabled: false)
|
||||
deleteButton.isEnabled = false
|
||||
saveButton.isEnabled = false
|
||||
let error = await ShareController.create(
|
||||
kit: kit,
|
||||
shareType: selectedShareType,
|
||||
itemServerRelativePath: itemServerRelativePath,
|
||||
shareWith: shareWith,
|
||||
password: password,
|
||||
expireDate: expireDate,
|
||||
permissions: permissions,
|
||||
note: note,
|
||||
label: label,
|
||||
hideDownload: hideDownload
|
||||
)
|
||||
if let error = error, error != .success {
|
||||
dataSource.uiDelegate?.showError("Error creating: \(error.errorDescription)")
|
||||
setAllFields(enabled: true)
|
||||
} else {
|
||||
dataSource.uiDelegate?.hideOptions(self)
|
||||
await dataSource.reload()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Logger.shareOptionsView.info("Editing existing share!")
|
||||
|
||||
guard let controller = controller else {
|
||||
Logger.shareOptionsView.error("No valid share controller, cannot edit share.")
|
||||
return
|
||||
}
|
||||
let share = controller.share
|
||||
let permissions = uploadAndEdit
|
||||
? share.permissions | NKShare.PermissionValues.updateShare.rawValue
|
||||
: share.permissions & ~NKShare.PermissionValues.updateShare.rawValue
|
||||
|
||||
setAllFields(enabled: false)
|
||||
deleteButton.isEnabled = false
|
||||
saveButton.isEnabled = false
|
||||
let error = await controller.save(
|
||||
password: password,
|
||||
expireDate: expireDate,
|
||||
permissions: permissions,
|
||||
note: note,
|
||||
label: label,
|
||||
hideDownload: hideDownload
|
||||
)
|
||||
if let error = error, error != .success {
|
||||
dataSource?.uiDelegate?.showError("Error updating share: \(error.errorDescription)")
|
||||
setAllFields(enabled: true)
|
||||
} else {
|
||||
dataSource?.uiDelegate?.hideOptions(self)
|
||||
await dataSource?.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func delete(_ sender: Any) {
|
||||
Task { @MainActor in
|
||||
guard !createMode else {
|
||||
dataSource?.uiDelegate?.hideOptions(self)
|
||||
reset()
|
||||
return
|
||||
}
|
||||
|
||||
setAllFields(enabled: false)
|
||||
deleteButton.isEnabled = false
|
||||
saveButton.isEnabled = false
|
||||
let error = await controller?.delete()
|
||||
if let error = error, error != .success {
|
||||
dataSource?.uiDelegate?.showError("Error deleting share: \(error.errorDescription)")
|
||||
}
|
||||
await dataSource?.reload()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// ShareTableItemView.swift
|
||||
// FileProviderUIExt
|
||||
//
|
||||
// Created by Claudio Cambra on 28/2/24.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import NextcloudKit
|
||||
|
||||
class ShareTableItemView: NSTableCellView {
|
||||
@IBOutlet private weak var typeImageView: NSImageView!
|
||||
@IBOutlet private weak var label: NSTextField!
|
||||
@IBOutlet private weak var copyLinkButton: NSButton!
|
||||
private var originalCopyImage: NSImage?
|
||||
private var copiedButtonImage: NSImage?
|
||||
private var tempButtonTimer: Timer?
|
||||
|
||||
var share: NKShare? {
|
||||
didSet {
|
||||
guard let share = share else {
|
||||
prepareForReuse()
|
||||
return
|
||||
}
|
||||
typeImageView.image = share.typeImage
|
||||
label.stringValue = share.displayString
|
||||
copyLinkButton.isHidden = share.shareType != NKShare.ShareType.publicLink.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
typeImageView.image = nil
|
||||
label.stringValue = ""
|
||||
copyLinkButton.isHidden = false
|
||||
super.prepareForReuse()
|
||||
}
|
||||
|
||||
@IBAction func copyShareLink(sender: Any) {
|
||||
guard let share = share else { return }
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.declareTypes([.string], owner: nil)
|
||||
pasteboard.setString(share.url, forType: .string)
|
||||
|
||||
guard tempButtonTimer == nil else { return }
|
||||
|
||||
originalCopyImage = copyLinkButton.image
|
||||
copiedButtonImage = NSImage(
|
||||
systemSymbolName: "checkmark.circle.fill",
|
||||
accessibilityDescription: "Public link has been copied icon"
|
||||
)
|
||||
var config = NSImage.SymbolConfiguration(scale: .medium)
|
||||
if #available(macOS 12.0, *) {
|
||||
config = config.applying(.init(hierarchicalColor: .systemGreen))
|
||||
}
|
||||
copiedButtonImage = copiedButtonImage?.withSymbolConfiguration(config)
|
||||
copyLinkButton.image = copiedButtonImage
|
||||
tempButtonTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { timer in
|
||||
self.copyLinkButton.image = self.originalCopyImage
|
||||
self.copiedButtonImage = nil
|
||||
self.tempButtonTimer?.invalidate()
|
||||
self.tempButtonTimer = nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
|
||||
<capability name="Image references" minToolsVersion="12.0"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner"/>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<tableCellView id="WWf-Il-fKw" customClass="ShareTableItemView" customModule="FileProviderUIExt" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="322" height="42"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wdu-Dj-FUn">
|
||||
<rect key="frame" x="5" y="5" width="312" height="32"/>
|
||||
<subviews>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Ex8-CB-iNc">
|
||||
<rect key="frame" x="0.0" y="-5" width="32" height="43"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="Ex8-CB-iNc" secondAttribute="height" multiplier="1:1" id="5ak-dc-HzR"/>
|
||||
<constraint firstAttribute="height" constant="32" id="Cus-qI-uen"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" controlSize="large" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" id="0Xf-Au-QjJ">
|
||||
<imageReference key="image" image="link.circle.fill" catalog="system" symbolScale="large"/>
|
||||
</imageCell>
|
||||
<color key="contentTintColor" name="AccentColor"/>
|
||||
</imageView>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="MwZ-er-vB4">
|
||||
<rect key="frame" x="38" y="8" width="236" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" alignment="left" title="Share link" id="Bcz-ws-5yW">
|
||||
<font key="font" metaFont="systemMedium" size="13"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button horizontalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="y6i-wm-BtQ">
|
||||
<rect key="frame" x="280" y="0.0" width="32" height="32"/>
|
||||
<buttonCell key="cell" type="inline" title="Copy share link" bezelStyle="inline" image="doc.on.doc.fill" catalog="system" imagePosition="only" alignment="center" controlSize="large" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ram-fe-8Pt">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="smallSystemBold"/>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="y6i-wm-BtQ" secondAttribute="height" multiplier="1:1" id="BlJ-WU-1y5"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="copyShareLinkWithSender:" target="WWf-Il-fKw" id="dgW-8v-wfd"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Ex8-CB-iNc" firstAttribute="top" secondItem="wdu-Dj-FUn" secondAttribute="top" id="2x8-EU-gHs"/>
|
||||
<constraint firstItem="y6i-wm-BtQ" firstAttribute="top" secondItem="wdu-Dj-FUn" secondAttribute="top" id="BjV-lg-nyL"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Ex8-CB-iNc" secondAttribute="bottom" id="dd0-Vh-Pbi"/>
|
||||
<constraint firstAttribute="bottom" secondItem="y6i-wm-BtQ" secondAttribute="bottom" id="ha0-oR-OxV"/>
|
||||
<constraint firstItem="MwZ-er-vB4" firstAttribute="centerY" secondItem="Ex8-CB-iNc" secondAttribute="centerY" id="m3A-Tu-qMJ"/>
|
||||
<constraint firstItem="Ex8-CB-iNc" firstAttribute="leading" secondItem="wdu-Dj-FUn" secondAttribute="leading" id="o1t-TB-uhe"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="wdu-Dj-FUn" firstAttribute="top" secondItem="WWf-Il-fKw" secondAttribute="top" constant="5" id="HIY-8X-dM2"/>
|
||||
<constraint firstItem="wdu-Dj-FUn" firstAttribute="leading" secondItem="WWf-Il-fKw" secondAttribute="leading" constant="5" id="g7a-hl-t5g"/>
|
||||
<constraint firstAttribute="bottom" secondItem="wdu-Dj-FUn" secondAttribute="bottom" constant="5" id="gAZ-x8-C4K"/>
|
||||
<constraint firstAttribute="trailing" secondItem="wdu-Dj-FUn" secondAttribute="trailing" constant="5" id="grc-5X-tMi"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="copyLinkButton" destination="y6i-wm-BtQ" id="8bc-yO-Txo"/>
|
||||
<outlet property="label" destination="MwZ-er-vB4" id="Gba-jf-C8H"/>
|
||||
<outlet property="typeImageView" destination="Ex8-CB-iNc" id="T7o-zd-qOo"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="128" y="-22"/>
|
||||
</tableCellView>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="doc.on.doc.fill" catalog="system" width="17" height="19"/>
|
||||
<image name="link.circle.fill" catalog="system" width="20" height="20"/>
|
||||
<namedColor name="AccentColor">
|
||||
<color red="0.0" green="0.46000000000000002" blue="0.89000000000000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
|
@ -0,0 +1,256 @@
|
|||
//
|
||||
// ShareTableViewDataSource.swift
|
||||
// FileProviderUIExt
|
||||
//
|
||||
// Created by Claudio Cambra on 27/2/24.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import FileProvider
|
||||
import NextcloudKit
|
||||
import NextcloudFileProviderKit
|
||||
import NextcloudCapabilitiesKit
|
||||
import OSLog
|
||||
|
||||
class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDelegate {
|
||||
private let shareItemViewIdentifier = NSUserInterfaceItemIdentifier("ShareTableItemView")
|
||||
private let shareItemViewNib = NSNib(nibNamed: "ShareTableItemView", bundle: nil)
|
||||
private let reattemptInterval: TimeInterval = 3.0
|
||||
|
||||
var uiDelegate: ShareViewDataSourceUIDelegate?
|
||||
var sharesTableView: NSTableView? {
|
||||
didSet {
|
||||
sharesTableView?.register(shareItemViewNib, forIdentifier: shareItemViewIdentifier)
|
||||
sharesTableView?.rowHeight = 42.0 // Height of view in ShareTableItemView XIB
|
||||
sharesTableView?.dataSource = self
|
||||
sharesTableView?.delegate = self
|
||||
sharesTableView?.reloadData()
|
||||
}
|
||||
}
|
||||
var capabilities: Capabilities?
|
||||
var itemMetadata: NKFile?
|
||||
|
||||
private(set) var kit: NextcloudKit?
|
||||
private(set) var itemURL: URL?
|
||||
private(set) var itemServerRelativePath: String?
|
||||
private(set) var shares: [NKShare] = [] {
|
||||
didSet { Task { @MainActor in sharesTableView?.reloadData() } }
|
||||
}
|
||||
private var account: Account? {
|
||||
didSet {
|
||||
guard let account = account else { return }
|
||||
kit = NextcloudKit()
|
||||
kit?.setup(
|
||||
user: account.username,
|
||||
userId: account.username,
|
||||
password: account.password,
|
||||
urlBase: account.serverUrl
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func loadItem(url: URL) {
|
||||
itemServerRelativePath = nil
|
||||
itemURL = url
|
||||
Task {
|
||||
await reload()
|
||||
}
|
||||
}
|
||||
|
||||
func reattempt() {
|
||||
DispatchQueue.main.async {
|
||||
Timer.scheduledTimer(withTimeInterval: self.reattemptInterval, repeats: false) { _ in
|
||||
Task { await self.reload() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reload() async {
|
||||
guard let itemURL = itemURL else { return }
|
||||
guard let itemIdentifier = await withCheckedContinuation({
|
||||
(continuation: CheckedContinuation<NSFileProviderItemIdentifier?, Never>) -> Void in
|
||||
NSFileProviderManager.getIdentifierForUserVisibleFile(
|
||||
at: itemURL
|
||||
) { identifier, domainIdentifier, error in
|
||||
defer { continuation.resume(returning: identifier) }
|
||||
guard error == nil else {
|
||||
self.presentError("No item with identifier: \(error.debugDescription)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}) else {
|
||||
presentError("Could not get identifier for item, no shares can be acquired.")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let connection = try await serviceConnection(url: itemURL)
|
||||
guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
|
||||
let credentials = await connection.credentials() as? Dictionary<String, String>,
|
||||
let convertedAccount = Account(dictionary: credentials),
|
||||
!convertedAccount.password.isEmpty
|
||||
else {
|
||||
presentError("Failed to get details from File Provider Extension. Retrying.")
|
||||
reattempt()
|
||||
return
|
||||
}
|
||||
let serverPathString = serverPath as String
|
||||
itemServerRelativePath = serverPathString
|
||||
account = convertedAccount
|
||||
await sharesTableView?.deselectAll(self)
|
||||
capabilities = await fetchCapabilities()
|
||||
guard capabilities != nil else { return }
|
||||
guard capabilities?.filesSharing?.apiEnabled == true else {
|
||||
presentError("Server does not support shares.")
|
||||
return
|
||||
}
|
||||
itemMetadata = await fetchItemMetadata(itemRelativePath: serverPathString)
|
||||
guard itemMetadata?.permissions.contains("R") == true else {
|
||||
presentError("This file cannot be shared.")
|
||||
return
|
||||
}
|
||||
shares = await fetch(
|
||||
itemIdentifier: itemIdentifier, itemRelativePath: serverPathString
|
||||
)
|
||||
} catch let error {
|
||||
presentError("Could not reload data: \(error), will try again.")
|
||||
reattempt()
|
||||
}
|
||||
}
|
||||
|
||||
private func serviceConnection(url: URL) async throws -> FPUIExtensionService {
|
||||
let services = try await FileManager().fileProviderServicesForItem(at: url)
|
||||
guard let service = services[fpUiExtensionServiceName] else {
|
||||
Logger.sharesDataSource.error("Couldn't get service, required service not present")
|
||||
throw NSFileProviderError(.providerNotFound)
|
||||
}
|
||||
let connection: NSXPCConnection
|
||||
connection = try await service.fileProviderConnection()
|
||||
connection.remoteObjectInterface = NSXPCInterface(with: FPUIExtensionService.self)
|
||||
connection.interruptionHandler = {
|
||||
Logger.sharesDataSource.error("Service connection interrupted")
|
||||
}
|
||||
connection.resume()
|
||||
guard let proxy = connection.remoteObjectProxy as? FPUIExtensionService else {
|
||||
throw NSFileProviderError(.serverUnreachable)
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
|
||||
private func fetch(
|
||||
itemIdentifier: NSFileProviderItemIdentifier, itemRelativePath: String
|
||||
) async -> [NKShare] {
|
||||
Task { @MainActor in uiDelegate?.fetchStarted() }
|
||||
defer { Task { @MainActor in uiDelegate?.fetchFinished() } }
|
||||
|
||||
let rawIdentifier = itemIdentifier.rawValue
|
||||
Logger.sharesDataSource.info("Fetching shares for item \(rawIdentifier, privacy: .public)")
|
||||
|
||||
guard let kit = kit else {
|
||||
self.presentError("NextcloudKit instance is unavailable, cannot fetch shares!")
|
||||
return []
|
||||
}
|
||||
|
||||
let parameter = NKShareParameter(path: itemRelativePath)
|
||||
|
||||
return await withCheckedContinuation { continuation in
|
||||
kit.readShares(parameters: parameter) { account, shares, data, error in
|
||||
let shareCount = shares?.count ?? 0
|
||||
Logger.sharesDataSource.info("Received \(shareCount, privacy: .public) shares")
|
||||
defer { continuation.resume(returning: shares ?? []) }
|
||||
guard error == .success else {
|
||||
self.presentError("Error fetching shares: \(error.errorDescription)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchCapabilities() async -> Capabilities? {
|
||||
return await withCheckedContinuation { continuation in
|
||||
kit?.getCapabilities { account, capabilitiesJson, error in
|
||||
guard error == .success, let capabilitiesJson = capabilitiesJson else {
|
||||
self.presentError("Error getting server caps: \(error.errorDescription)")
|
||||
continuation.resume(returning: nil)
|
||||
return
|
||||
}
|
||||
Logger.sharesDataSource.info("Successfully retrieved server share capabilities")
|
||||
continuation.resume(returning: Capabilities(data: capabilitiesJson))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchItemMetadata(itemRelativePath: String) async -> NKFile? {
|
||||
guard let kit = kit else {
|
||||
presentError("Could not fetch item metadata as NextcloudKit instance is unavailable")
|
||||
return nil
|
||||
}
|
||||
|
||||
func slashlessPath(_ string: String) -> String {
|
||||
var strCopy = string
|
||||
if strCopy.hasPrefix("/") {
|
||||
strCopy.removeFirst()
|
||||
}
|
||||
if strCopy.hasSuffix("/") {
|
||||
strCopy.removeLast()
|
||||
}
|
||||
return strCopy
|
||||
}
|
||||
|
||||
let nkCommon = kit.nkCommonInstance
|
||||
let urlBase = slashlessPath(nkCommon.urlBase)
|
||||
let davSuffix = slashlessPath(nkCommon.dav)
|
||||
let userId = nkCommon.userId
|
||||
let itemRelPath = slashlessPath(itemRelativePath)
|
||||
|
||||
let itemFullServerPath = "\(urlBase)/\(davSuffix)/files/\(userId)/\(itemRelPath)"
|
||||
return await withCheckedContinuation { continuation in
|
||||
kit.readFileOrFolder(serverUrlFileName: itemFullServerPath, depth: "0") {
|
||||
account, files, data, error in
|
||||
guard error == .success else {
|
||||
self.presentError("Error getting item metadata: \(error.errorDescription)")
|
||||
continuation.resume(returning: nil)
|
||||
return
|
||||
}
|
||||
Logger.sharesDataSource.info("Successfully retrieved item metadata")
|
||||
continuation.resume(returning: files.first)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentError(_ errorString: String) {
|
||||
Logger.sharesDataSource.error("\(errorString, privacy: .public)")
|
||||
Task { @MainActor in self.uiDelegate?.showError(errorString) }
|
||||
}
|
||||
|
||||
// MARK: - NSTableViewDataSource protocol methods
|
||||
|
||||
@objc func numberOfRows(in tableView: NSTableView) -> Int {
|
||||
shares.count
|
||||
}
|
||||
|
||||
// MARK: - NSTableViewDelegate protocol methods
|
||||
|
||||
@objc func tableView(
|
||||
_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int
|
||||
) -> NSView? {
|
||||
let share = shares[row]
|
||||
guard let view = tableView.makeView(
|
||||
withIdentifier: shareItemViewIdentifier, owner: self
|
||||
) as? ShareTableItemView else {
|
||||
Logger.sharesDataSource.error("Acquired item view from table is not a share item view!")
|
||||
return nil
|
||||
}
|
||||
view.share = share
|
||||
return view
|
||||
}
|
||||
|
||||
@objc func tableViewSelectionDidChange(_ notification: Notification) {
|
||||
guard let selectedRow = sharesTableView?.selectedRow, selectedRow >= 0 else {
|
||||
Task { @MainActor in uiDelegate?.hideOptions(self) }
|
||||
return
|
||||
}
|
||||
let share = shares[selectedRow]
|
||||
Task { @MainActor in uiDelegate?.showOptions(share: share) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
//
|
||||
// ShareViewController.swift
|
||||
// FileProviderUIExt
|
||||
//
|
||||
// Created by Claudio Cambra on 21/2/24.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import FileProvider
|
||||
import NextcloudKit
|
||||
import OSLog
|
||||
import QuickLookThumbnailing
|
||||
|
||||
class ShareViewController: NSViewController, ShareViewDataSourceUIDelegate {
|
||||
let shareDataSource = ShareTableViewDataSource()
|
||||
let itemIdentifiers: [NSFileProviderItemIdentifier]
|
||||
|
||||
@IBOutlet weak var fileNameIcon: NSImageView!
|
||||
@IBOutlet weak var fileNameLabel: NSTextField!
|
||||
@IBOutlet weak var descriptionLabel: NSTextField!
|
||||
@IBOutlet weak var createButton: NSButton!
|
||||
@IBOutlet weak var closeButton: NSButton!
|
||||
@IBOutlet weak var tableView: NSTableView!
|
||||
@IBOutlet weak var optionsView: ShareOptionsView!
|
||||
@IBOutlet weak var splitView: NSSplitView!
|
||||
@IBOutlet weak var loadingEffectView: NSVisualEffectView!
|
||||
@IBOutlet weak var loadingIndicator: NSProgressIndicator!
|
||||
@IBOutlet weak var errorMessageStackView: NSStackView!
|
||||
@IBOutlet weak var errorTextLabel: NSTextField!
|
||||
@IBOutlet weak var noSharesLabel: NSTextField!
|
||||
|
||||
public override var nibName: NSNib.Name? {
|
||||
return NSNib.Name(self.className)
|
||||
}
|
||||
|
||||
var actionViewController: DocumentActionViewController! {
|
||||
return parent as? DocumentActionViewController
|
||||
}
|
||||
|
||||
init(_ itemIdentifiers: [NSFileProviderItemIdentifier]) {
|
||||
self.itemIdentifiers = itemIdentifiers
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
guard let firstItem = itemIdentifiers.first else {
|
||||
Logger.shareViewController.error("called without items")
|
||||
closeAction(self)
|
||||
return
|
||||
}
|
||||
|
||||
Logger.shareViewController.info(
|
||||
"""
|
||||
Instantiated with itemIdentifiers:
|
||||
\(itemIdentifiers.map { $0.rawValue }, privacy: .public)
|
||||
"""
|
||||
)
|
||||
|
||||
Task {
|
||||
await processItemIdentifier(firstItem)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
dismissError(self)
|
||||
hideOptions(self)
|
||||
}
|
||||
|
||||
@IBAction func closeAction(_ sender: Any) {
|
||||
actionViewController.extensionContext.completeRequest()
|
||||
}
|
||||
|
||||
private func processItemIdentifier(_ itemIdentifier: NSFileProviderItemIdentifier) async {
|
||||
guard let manager = NSFileProviderManager(for: actionViewController.domain) else {
|
||||
fatalError("NSFileProviderManager isn't expected to fail")
|
||||
}
|
||||
|
||||
do {
|
||||
let itemUrl = try await manager.getUserVisibleURL(for: itemIdentifier)
|
||||
guard itemUrl.startAccessingSecurityScopedResource() else {
|
||||
Logger.shareViewController.error("Could not access scoped resource for item url!")
|
||||
return
|
||||
}
|
||||
await updateDisplay(itemUrl: itemUrl)
|
||||
shareDataSource.uiDelegate = self
|
||||
shareDataSource.sharesTableView = tableView
|
||||
shareDataSource.loadItem(url: itemUrl)
|
||||
optionsView.dataSource = shareDataSource
|
||||
itemUrl.stopAccessingSecurityScopedResource()
|
||||
} catch let error {
|
||||
let errorString = "Error processing item: \(error)"
|
||||
Logger.shareViewController.error("\(errorString, privacy: .public)")
|
||||
fileNameLabel.stringValue = "Unknown item"
|
||||
descriptionLabel.stringValue = errorString
|
||||
}
|
||||
}
|
||||
|
||||
private func updateDisplay(itemUrl: URL) async {
|
||||
fileNameLabel.stringValue = itemUrl.lastPathComponent
|
||||
|
||||
let request = QLThumbnailGenerator.Request(
|
||||
fileAt: itemUrl,
|
||||
size: CGSize(width: 128, height: 128),
|
||||
scale: 1.0,
|
||||
representationTypes: .icon
|
||||
)
|
||||
|
||||
let generator = QLThumbnailGenerator.shared
|
||||
let fileThumbnail = await withCheckedContinuation { continuation in
|
||||
generator.generateRepresentations(for: request) { thumbnail, type, error in
|
||||
if thumbnail == nil || error != nil {
|
||||
Logger.shareViewController.error(
|
||||
"""
|
||||
Could not get thumbnail: \(error, privacy: .public)
|
||||
"""
|
||||
)
|
||||
}
|
||||
continuation.resume(returning: thumbnail)
|
||||
}
|
||||
}
|
||||
fileNameIcon.image = fileThumbnail?.nsImage
|
||||
|
||||
let resourceValues = try? itemUrl.resourceValues(
|
||||
forKeys: [.fileSizeKey, .contentModificationDateKey]
|
||||
)
|
||||
var sizeDesc = "Unknown size"
|
||||
var modDesc = "Unknown modification date"
|
||||
if let fileSize = resourceValues?.fileSize {
|
||||
sizeDesc = ByteCountFormatter().string(fromByteCount: Int64(fileSize))
|
||||
}
|
||||
if let modificationDate = resourceValues?.contentModificationDate {
|
||||
let modDateString = DateFormatter.localizedString(
|
||||
from: modificationDate, dateStyle: .short, timeStyle: .short
|
||||
)
|
||||
modDesc = "Last modified: \(modDateString)"
|
||||
}
|
||||
descriptionLabel.stringValue = "\(sizeDesc) · \(modDesc)"
|
||||
}
|
||||
|
||||
@IBAction func dismissError(_ sender: Any) {
|
||||
errorMessageStackView.isHidden = true
|
||||
}
|
||||
|
||||
@IBAction func createShare(_ sender: Any) {
|
||||
guard let kit = shareDataSource.kit else { return }
|
||||
optionsView.kit = kit
|
||||
optionsView.createMode = true
|
||||
tableView.deselectAll(self)
|
||||
if !splitView.arrangedSubviews.contains(optionsView) {
|
||||
splitView.addArrangedSubview(optionsView)
|
||||
optionsView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func fetchStarted() {
|
||||
loadingEffectView.isHidden = false
|
||||
loadingIndicator.startAnimation(self)
|
||||
}
|
||||
|
||||
func fetchFinished() {
|
||||
noSharesLabel.isHidden = !shareDataSource.shares.isEmpty
|
||||
loadingEffectView.isHidden = true
|
||||
loadingIndicator.stopAnimation(self)
|
||||
}
|
||||
|
||||
func hideOptions(_ sender: Any) {
|
||||
if sender as? ShareTableViewDataSource == shareDataSource, optionsView.createMode {
|
||||
// Do not hide options if the table view has had everything deselected when we set the
|
||||
// options view to be in create mode
|
||||
return
|
||||
}
|
||||
splitView.removeArrangedSubview(optionsView)
|
||||
optionsView.isHidden = true
|
||||
}
|
||||
|
||||
func showOptions(share: NKShare) {
|
||||
guard let kit = shareDataSource.kit else { return }
|
||||
optionsView.kit = kit
|
||||
optionsView.controller = ShareController(share: share, kit: kit)
|
||||
if !splitView.arrangedSubviews.contains(optionsView) {
|
||||
splitView.addArrangedSubview(optionsView)
|
||||
optionsView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func showError(_ errorString: String) {
|
||||
errorMessageStackView.isHidden = false
|
||||
errorTextLabel.stringValue = errorString
|
||||
}
|
||||
}
|
|
@ -0,0 +1,532 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22689"/>
|
||||
<capability name="Image references" minToolsVersion="12.0"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="ShareViewController" customModule="FileProviderUIExt" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="closeButton" destination="aDA-n9-Zly" id="niR-Ad-FEa"/>
|
||||
<outlet property="createButton" destination="BMA-BP-wHc" id="HEq-bN-i9V"/>
|
||||
<outlet property="descriptionLabel" destination="gX0-nE-MrU" id="RoY-u1-1on"/>
|
||||
<outlet property="errorMessageStackView" destination="dFs-Gh-2WQ" id="kkQ-Uq-xk7"/>
|
||||
<outlet property="errorTextLabel" destination="770-HW-oC7" id="gfn-SV-TNM"/>
|
||||
<outlet property="fileNameIcon" destination="zSV-DV-Ray" id="336-e0-CEo"/>
|
||||
<outlet property="fileNameLabel" destination="slV-H6-zJ3" id="DPp-sN-Yff"/>
|
||||
<outlet property="loadingEffectView" destination="1ud-mC-gQV" id="HMT-Sb-Axl"/>
|
||||
<outlet property="loadingIndicator" destination="acb-Yu-Zpm" id="9Jf-dE-7LE"/>
|
||||
<outlet property="noSharesLabel" destination="K3D-6U-Cbr" id="zDP-E4-9bg"/>
|
||||
<outlet property="optionsView" destination="EXb-m8-yzj" id="uAb-lv-EZ4"/>
|
||||
<outlet property="splitView" destination="91w-SP-6sl" id="20T-gQ-SPY"/>
|
||||
<outlet property="tableView" destination="vb0-a6-eeH" id="KQo-eg-dba"/>
|
||||
<outlet property="view" destination="Jw6-da-U8j" id="5Ek-F1-w7C"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<view translatesAutoresizingMaskIntoConstraints="NO" id="Jw6-da-U8j">
|
||||
<rect key="frame" x="0.0" y="0.0" width="500" height="829"/>
|
||||
<subviews>
|
||||
<splitView arrangesAllSubviews="NO" dividerStyle="thin" translatesAutoresizingMaskIntoConstraints="NO" id="91w-SP-6sl">
|
||||
<rect key="frame" x="10" y="10" width="480" height="809"/>
|
||||
<subviews>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="leading" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wGI-UV-bB3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
|
||||
<subviews>
|
||||
<stackView distribution="fill" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9kj-aB-aJh">
|
||||
<rect key="frame" x="0.0" y="437" width="480" height="43"/>
|
||||
<subviews>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="zSV-DV-Ray">
|
||||
<rect key="frame" x="0.0" y="-3.5" width="43" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="zSV-DV-Ray" secondAttribute="height" multiplier="1:1" id="NSH-gA-7lL"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="doc" catalog="system" id="laO-OA-5sJ"/>
|
||||
</imageView>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="leading" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="oSY-fV-uws">
|
||||
<rect key="frame" x="51" y="0.0" width="375" height="43"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="slV-H6-zJ3">
|
||||
<rect key="frame" x="-2" y="24" width="78" height="19"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="File name" id="Uuo-1j-to8">
|
||||
<font key="font" metaFont="systemBold" size="16"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gX0-nE-MrU">
|
||||
<rect key="frame" x="-2" y="0.0" width="146" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="File size · Last modified" id="1GC-Gr-x29">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="BMA-BP-wHc">
|
||||
<rect key="frame" x="434" y="-3.5" width="18.5" height="51"/>
|
||||
<buttonCell key="cell" type="bevel" title="Create share" bezelStyle="regularSquare" imagePosition="only" alignment="center" imageScaling="proportionallyDown" inset="2" id="cdh-AC-lt4">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<imageReference key="image" image="plus" catalog="system" symbolScale="large"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="createShare:" target="-2" id="NLV-ZM-y1w"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="aDA-n9-Zly">
|
||||
<rect key="frame" x="460" y="-5" width="20" height="54"/>
|
||||
<buttonCell key="cell" type="bevel" title="Close" bezelStyle="rounded" imagePosition="only" alignment="center" imageScaling="proportionallyDown" inset="2" id="zNR-DC-3xZ">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<imageReference key="image" image="xmark.circle.fill" catalog="system" symbolScale="large"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="closeAction:" target="-2" id="D9k-gc-AcN"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="43" id="C79-af-CZw"/>
|
||||
<constraint firstItem="oSY-fV-uws" firstAttribute="height" secondItem="9kj-aB-aJh" secondAttribute="height" id="I6w-n6-pgZ"/>
|
||||
<constraint firstAttribute="bottom" secondItem="BMA-BP-wHc" secondAttribute="bottom" id="RE6-Rg-yv9"/>
|
||||
<constraint firstItem="zSV-DV-Ray" firstAttribute="height" secondItem="9kj-aB-aJh" secondAttribute="height" id="X4V-Vr-Q7m"/>
|
||||
<constraint firstItem="BMA-BP-wHc" firstAttribute="top" secondItem="9kj-aB-aJh" secondAttribute="top" id="hrc-la-YmY"/>
|
||||
<constraint firstItem="aDA-n9-Zly" firstAttribute="height" secondItem="9kj-aB-aJh" secondAttribute="height" id="qCh-8Z-jcN"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<stackView distribution="fillProportionally" orientation="horizontal" alignment="top" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" verticalCompressionResistancePriority="1000" detachesHiddenViews="YES" id="dFs-Gh-2WQ">
|
||||
<rect key="frame" x="0.0" y="308" width="480" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<subviews>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="770-HW-oC7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="411" height="40"/>
|
||||
<textFieldCell key="cell" selectable="YES" borderStyle="border" alignment="left" title="A long error message that provides detail about why some operations with the shares failed" drawsBackground="YES" id="Jvl-L9-x8K">
|
||||
<font key="font" metaFont="systemSemibold" size="13"/>
|
||||
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
<color key="backgroundColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button translatesAutoresizingMaskIntoConstraints="NO" id="qfq-F1-w0b">
|
||||
<rect key="frame" x="416" y="-4" width="67" height="47"/>
|
||||
<buttonCell key="cell" type="bevel" title="Dismiss" bezelStyle="regularSquare" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="dLP-yX-dyk">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<color key="bezelColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
|
||||
<connections>
|
||||
<action selector="dismissError:" target="-2" id="qLa-KM-cYK"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="770-HW-oC7" firstAttribute="top" secondItem="dFs-Gh-2WQ" secondAttribute="top" id="2Y8-H2-wTQ"/>
|
||||
<constraint firstItem="qfq-F1-w0b" firstAttribute="centerY" secondItem="dFs-Gh-2WQ" secondAttribute="centerY" id="e6n-1Y-kUh"/>
|
||||
<constraint firstAttribute="bottom" secondItem="770-HW-oC7" secondAttribute="bottom" id="fHi-My-Qyc"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<scrollView autohidesScrollers="YES" horizontalLineScroll="17" horizontalPageScroll="10" verticalLineScroll="17" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZzY-1Z-3xa">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="300"/>
|
||||
<clipView key="contentView" drawsBackground="NO" id="Ixg-th-Nw0">
|
||||
<rect key="frame" x="1" y="1" width="478" height="298"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="firstColumnOnly" columnReordering="NO" columnResizing="NO" multipleSelection="NO" autosaveColumns="NO" typeSelect="NO" rowSizeStyle="automatic" viewBased="YES" id="vb0-a6-eeH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="478" height="298"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<size key="intercellSpacing" width="17" height="0.0"/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
|
||||
<tableColumns>
|
||||
<tableColumn identifier="AutomaticTableColumnIdentifier.0" width="466" minWidth="40" maxWidth="1000" id="3Eb-aD-Ueu">
|
||||
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
|
||||
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
|
||||
</tableHeaderCell>
|
||||
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" title="Text Cell" id="mEe-4r-5cx">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
|
||||
</tableColumn>
|
||||
</tableColumns>
|
||||
</tableView>
|
||||
</subviews>
|
||||
<nil key="backgroundColor"/>
|
||||
</clipView>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="300" id="FAW-Ws-zy7"/>
|
||||
</constraints>
|
||||
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="hQs-hA-bDq">
|
||||
<rect key="frame" x="1" y="284" width="478" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="ryi-jk-XFM">
|
||||
<rect key="frame" x="224" y="17" width="15" height="102"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="ZzY-1Z-3xa" firstAttribute="leading" secondItem="wGI-UV-bB3" secondAttribute="leading" id="528-fv-ZfM"/>
|
||||
<constraint firstItem="9kj-aB-aJh" firstAttribute="top" secondItem="wGI-UV-bB3" secondAttribute="top" id="EOP-84-2ZH"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ZzY-1Z-3xa" secondAttribute="trailing" id="Kt7-wJ-gb5"/>
|
||||
<constraint firstItem="9kj-aB-aJh" firstAttribute="leading" secondItem="wGI-UV-bB3" secondAttribute="leading" id="UyT-D1-Awv"/>
|
||||
<constraint firstAttribute="trailing" secondItem="9kj-aB-aJh" secondAttribute="trailing" id="kua-OU-nbq"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
<view translatesAutoresizingMaskIntoConstraints="NO" id="EXb-m8-yzj" customClass="ShareOptionsView" customModule="FileProviderUIExt" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="481" width="480" height="328"/>
|
||||
<subviews>
|
||||
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="5" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T4D-9c-PyA">
|
||||
<rect key="frame" x="10" y="10" width="460" height="308"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" verticalCompressionResistancePriority="749" translatesAutoresizingMaskIntoConstraints="NO" id="AWy-Qo-wHH">
|
||||
<rect key="frame" x="-2" y="292" width="464" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="Share options" id="fzf-0v-uHo">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aRB-vw-b4r">
|
||||
<rect key="frame" x="-3" y="263" width="467" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Public link share" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="JhA-rv-1xy" id="S60-Qi-URJ">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
<menu key="menu" id="yag-Vc-J7Y">
|
||||
<items>
|
||||
<menuItem title="Public link share" state="on" id="JhA-rv-1xy"/>
|
||||
<menuItem title="User share" id="CpL-qc-lAA"/>
|
||||
<menuItem title="Group share" id="bnp-aV-ZvE"/>
|
||||
<menuItem title="Email share" id="5DB-JD-Ij0">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem title="Federated cloud share" id="RZP-ME-baz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem title="Circle share" id="yDE-lS-rJZ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
<menuItem title="Talk conversation share" id="aHo-Mr-vTn">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
<connections>
|
||||
<action selector="shareTypePickerAction:" target="EXb-m8-yzj" id="LN7-TC-RvV"/>
|
||||
</connections>
|
||||
</popUpButton>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="f8k-Ae-oQc">
|
||||
<rect key="frame" x="0.0" y="240" width="460" height="22"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Share recipient" bezelStyle="round" id="Ahi-gU-lmO">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="CXW-ZO-B2f">
|
||||
<rect key="frame" x="0.0" y="213" width="460" height="22"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Share label" bezelStyle="round" id="ZsJ-zc-mFT">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="luZ-Vf-V24">
|
||||
<rect key="frame" x="-2" y="191" width="175" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Allow upload and editing" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="vOP-1k-c75">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="2TN-96-apv">
|
||||
<rect key="frame" x="-2" y="170" width="117" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Hide download" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="6Eu-NS-uZ7">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ylD-hi-7Oq">
|
||||
<rect key="frame" x="-2" y="149" width="132" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Password protect" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="MWC-hf-0pc">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="passwordCheckboxAction:" target="EXb-m8-yzj" id="QMn-RM-jdf"/>
|
||||
</connections>
|
||||
</button>
|
||||
<secureTextField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pkv-9L-nhv">
|
||||
<rect key="frame" x="0.0" y="123" width="460" height="22"/>
|
||||
<secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a new password" usesSingleLineMode="YES" bezelStyle="round" id="hcA-we-oYG">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<allowedInputSourceLocales>
|
||||
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
|
||||
</allowedInputSourceLocales>
|
||||
</secureTextFieldCell>
|
||||
</secureTextField>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="HaL-4Z-csA">
|
||||
<rect key="frame" x="-2" y="101" width="117" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Expiration date" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="r1a-iX-8Xo">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="expirationDateCheckboxAction:" target="EXb-m8-yzj" id="KHG-U0-rsG"/>
|
||||
</connections>
|
||||
</button>
|
||||
<datePicker verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="46m-5F-ME5">
|
||||
<rect key="frame" x="0.0" y="73" width="463" height="28"/>
|
||||
<datePickerCell key="cell" borderStyle="bezel" alignment="left" useCurrentDate="YES" id="EHU-Gu-Dfh">
|
||||
<font key="font" metaFont="system"/>
|
||||
<date key="date" timeIntervalSinceReferenceDate="734115148.45631897">
|
||||
<!--2024-04-06 16:52:28 +0000-->
|
||||
</date>
|
||||
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
</datePickerCell>
|
||||
</datePicker>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="c10-ub-U31">
|
||||
<rect key="frame" x="-2" y="51" width="132" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Note for recipient" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="oLA-Nu-fzO">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="noteForRecipientCheckboxAction:" target="EXb-m8-yzj" id="j7a-Tc-uiu"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="io6-Kg-fLl">
|
||||
<rect key="frame" x="0.0" y="25" width="460" height="22"/>
|
||||
<textFieldCell key="cell" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Note for the recipient" bezelStyle="round" id="z5W-BH-NnM">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<stackView distribution="fillEqually" orientation="horizontal" alignment="bottom" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mWb-eX-nfh">
|
||||
<rect key="frame" x="0.0" y="0.0" width="460" height="20"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Bmf-yY-Y7V">
|
||||
<rect key="frame" x="-7" y="-7" width="240" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Delete" bezelStyle="rounded" image="trash.fill" catalog="system" imagePosition="leading" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Kb4-Qg-9Ag">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<color key="bezelColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
|
||||
<connections>
|
||||
<action selector="delete:" target="EXb-m8-yzj" id="PZq-SH-QVa"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button horizontalHuggingPriority="249" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="PaV-TL-cbq">
|
||||
<rect key="frame" x="227" y="-7" width="240" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Save" bezelStyle="rounded" image="arrow.up.square.fill" catalog="system" imagePosition="trailing" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="O1I-T0-iRC">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<color key="bezelColor" name="AccentColor"/>
|
||||
<connections>
|
||||
<action selector="save:" target="EXb-m8-yzj" id="O4N-dj-SlN"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="46m-5F-ME5" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="5LB-gI-S8e"/>
|
||||
<constraint firstItem="io6-Kg-fLl" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="94h-0I-pw6"/>
|
||||
<constraint firstAttribute="trailing" secondItem="pkv-9L-nhv" secondAttribute="trailing" id="IFt-uJ-etl"/>
|
||||
<constraint firstAttribute="trailing" secondItem="CXW-ZO-B2f" secondAttribute="trailing" id="Uvk-cg-D23"/>
|
||||
<constraint firstAttribute="trailing" secondItem="aRB-vw-b4r" secondAttribute="trailing" id="Zp9-vZ-oxU"/>
|
||||
<constraint firstItem="pkv-9L-nhv" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="bFe-9X-gFR"/>
|
||||
<constraint firstItem="AWy-Qo-wHH" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="dyr-SA-Kmy"/>
|
||||
<constraint firstAttribute="trailing" secondItem="46m-5F-ME5" secondAttribute="trailing" id="gV6-Jx-VH4"/>
|
||||
<constraint firstAttribute="bottom" secondItem="mWb-eX-nfh" secondAttribute="bottom" id="iLe-Zw-oO2"/>
|
||||
<constraint firstAttribute="trailing" secondItem="io6-Kg-fLl" secondAttribute="trailing" id="mkY-UE-SAy"/>
|
||||
<constraint firstItem="AWy-Qo-wHH" firstAttribute="top" secondItem="T4D-9c-PyA" secondAttribute="top" id="ndl-52-MnV"/>
|
||||
<constraint firstItem="CXW-ZO-B2f" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="oWL-JR-HTk"/>
|
||||
<constraint firstItem="mWb-eX-nfh" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="wiX-kw-Ohy"/>
|
||||
<constraint firstItem="aRB-vw-b4r" firstAttribute="leading" secondItem="T4D-9c-PyA" secondAttribute="leading" id="xPY-Ll-QCz"/>
|
||||
<constraint firstAttribute="trailing" secondItem="mWb-eX-nfh" secondAttribute="trailing" id="yUj-je-uXE"/>
|
||||
<constraint firstAttribute="trailing" secondItem="AWy-Qo-wHH" secondAttribute="trailing" id="zxC-eX-HrE"/>
|
||||
</constraints>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="T4D-9c-PyA" firstAttribute="leading" secondItem="EXb-m8-yzj" secondAttribute="leading" constant="10" id="Dhe-wL-ygv"/>
|
||||
<constraint firstItem="T4D-9c-PyA" firstAttribute="top" secondItem="EXb-m8-yzj" secondAttribute="top" constant="10" id="JrC-o6-So3"/>
|
||||
<constraint firstAttribute="trailing" secondItem="T4D-9c-PyA" secondAttribute="trailing" constant="10" id="gs6-zf-D6h"/>
|
||||
<constraint firstAttribute="bottom" secondItem="T4D-9c-PyA" secondAttribute="bottom" constant="10" id="r5C-pm-lv4"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="circleShare" destination="yDE-lS-rJZ" id="yHV-25-RJd"/>
|
||||
<outlet property="deleteButton" destination="Bmf-yY-Y7V" id="Wd0-LR-DV5"/>
|
||||
<outlet property="emailShareMenuItem" destination="5DB-JD-Ij0" id="LD5-P8-V2l"/>
|
||||
<outlet property="expirationDateCheckbox" destination="HaL-4Z-csA" id="vIf-I8-e3i"/>
|
||||
<outlet property="expirationDatePicker" destination="46m-5F-ME5" id="eGk-fe-IIf"/>
|
||||
<outlet property="federatedCloudShareMenuItem" destination="RZP-ME-baz" id="quL-N2-y1z"/>
|
||||
<outlet property="groupShareMenuItem" destination="bnp-aV-ZvE" id="8iU-IP-YXK"/>
|
||||
<outlet property="hideDownloadCheckbox" destination="2TN-96-apv" id="OPr-4x-aiK"/>
|
||||
<outlet property="labelTextField" destination="CXW-ZO-B2f" id="otQ-jh-Psr"/>
|
||||
<outlet property="noteForRecipientCheckbox" destination="c10-ub-U31" id="aG6-4P-cBv"/>
|
||||
<outlet property="noteTextField" destination="io6-Kg-fLl" id="JKm-A1-SqR"/>
|
||||
<outlet property="optionsTitleTextField" destination="AWy-Qo-wHH" id="BjX-oW-0Lp"/>
|
||||
<outlet property="passwordProtectCheckbox" destination="ylD-hi-7Oq" id="qdw-aF-uh2"/>
|
||||
<outlet property="passwordSecureField" destination="pkv-9L-nhv" id="992-i5-CPF"/>
|
||||
<outlet property="publicLinkShareMenuItem" destination="JhA-rv-1xy" id="usv-L6-M7k"/>
|
||||
<outlet property="saveButton" destination="PaV-TL-cbq" id="OvF-Le-oQj"/>
|
||||
<outlet property="shareRecipientTextField" destination="f8k-Ae-oQc" id="bfc-Vn-Zu9"/>
|
||||
<outlet property="shareTypePicker" destination="aRB-vw-b4r" id="Dfx-Dw-zEM"/>
|
||||
<outlet property="talkConversationShare" destination="aHo-Mr-vTn" id="hxF-qw-VQO"/>
|
||||
<outlet property="uploadEditPermissionCheckbox" destination="luZ-Vf-V24" id="ojW-WP-98U"/>
|
||||
<outlet property="userShareMenuItem" destination="CpL-qc-lAA" id="R2q-dc-og5"/>
|
||||
</connections>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="EXb-m8-yzj" secondAttribute="trailing" id="9yJ-jE-Iin"/>
|
||||
<constraint firstAttribute="bottom" secondItem="EXb-m8-yzj" secondAttribute="bottom" id="IDB-la-fxl"/>
|
||||
<constraint firstItem="EXb-m8-yzj" firstAttribute="leading" secondItem="91w-SP-6sl" secondAttribute="leading" id="imf-Yj-oUg"/>
|
||||
</constraints>
|
||||
<holdingPriorities>
|
||||
<real value="250"/>
|
||||
<real value="250"/>
|
||||
</holdingPriorities>
|
||||
</splitView>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="K3D-6U-Cbr">
|
||||
<rect key="frame" x="8" y="474" width="484" height="31"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="No shares" id="byC-34-Vtu">
|
||||
<font key="font" textStyle="largeTitle" name=".SFNS-Regular"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<visualEffectView blendingMode="withinWindow" material="HUDWindow" state="followsWindowActiveState" translatesAutoresizingMaskIntoConstraints="NO" id="1ud-mC-gQV">
|
||||
<rect key="frame" x="10" y="339" width="480" height="300"/>
|
||||
<subviews>
|
||||
<progressIndicator wantsLayer="YES" maxValue="100" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="acb-Yu-Zpm">
|
||||
<rect key="frame" x="224" y="134" width="32" height="32"/>
|
||||
</progressIndicator>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="acb-Yu-Zpm" firstAttribute="centerY" secondItem="1ud-mC-gQV" secondAttribute="centerY" id="Dhf-yv-2Wr"/>
|
||||
<constraint firstItem="acb-Yu-Zpm" firstAttribute="centerX" secondItem="1ud-mC-gQV" secondAttribute="centerX" id="Kzb-iw-xCx"/>
|
||||
</constraints>
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="1ud-mC-gQV" firstAttribute="bottom" secondItem="ZzY-1Z-3xa" secondAttribute="bottom" id="HTR-Xt-ACu"/>
|
||||
<constraint firstItem="1ud-mC-gQV" firstAttribute="top" secondItem="ZzY-1Z-3xa" secondAttribute="top" id="Hjv-Wk-Rk1"/>
|
||||
<constraint firstItem="91w-SP-6sl" firstAttribute="top" secondItem="Jw6-da-U8j" secondAttribute="top" constant="10" id="JFe-jp-e9y"/>
|
||||
<constraint firstAttribute="width" constant="500" id="KBX-aG-ZDU"/>
|
||||
<constraint firstItem="1ud-mC-gQV" firstAttribute="trailing" secondItem="ZzY-1Z-3xa" secondAttribute="trailing" id="Kpk-6o-ao7"/>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="380" id="LND-hR-7Co"/>
|
||||
<constraint firstAttribute="trailing" secondItem="91w-SP-6sl" secondAttribute="trailing" constant="10" id="XrV-hl-kxC"/>
|
||||
<constraint firstAttribute="bottom" secondItem="91w-SP-6sl" secondAttribute="bottom" constant="10" id="Z4B-85-NR5"/>
|
||||
<constraint firstItem="1ud-mC-gQV" firstAttribute="height" secondItem="ZzY-1Z-3xa" secondAttribute="height" id="dfF-9z-iG9"/>
|
||||
<constraint firstItem="91w-SP-6sl" firstAttribute="leading" secondItem="Jw6-da-U8j" secondAttribute="leading" constant="10" id="elm-Nc-tc0"/>
|
||||
<constraint firstItem="1ud-mC-gQV" firstAttribute="leading" secondItem="ZzY-1Z-3xa" secondAttribute="leading" id="kSz-tV-EFM"/>
|
||||
<constraint firstItem="K3D-6U-Cbr" firstAttribute="leading" secondItem="ZzY-1Z-3xa" secondAttribute="leading" id="lut-va-B3T"/>
|
||||
<constraint firstItem="K3D-6U-Cbr" firstAttribute="trailing" secondItem="ZzY-1Z-3xa" secondAttribute="trailing" id="vRC-IM-ovt"/>
|
||||
<constraint firstItem="K3D-6U-Cbr" firstAttribute="centerY" secondItem="ZzY-1Z-3xa" secondAttribute="centerY" id="wFD-ph-Bsp"/>
|
||||
</constraints>
|
||||
<point key="canvasLocation" x="-270" y="79"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="arrow.up.square.fill" catalog="system" width="15" height="14"/>
|
||||
<image name="doc" catalog="system" width="14" height="16"/>
|
||||
<image name="plus" catalog="system" width="18" height="17"/>
|
||||
<image name="trash.fill" catalog="system" width="15" height="17"/>
|
||||
<image name="xmark.circle.fill" catalog="system" width="20" height="20"/>
|
||||
<namedColor name="AccentColor">
|
||||
<color red="0.0" green="0.46000000000000002" blue="0.89000000000000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// ShareViewDataSourceUIDelegate.swift
|
||||
// FileProviderUIExt
|
||||
//
|
||||
// Created by Claudio Cambra on 28/2/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import NextcloudKit
|
||||
|
||||
protocol ShareViewDataSourceUIDelegate {
|
||||
func fetchStarted()
|
||||
func fetchFinished()
|
||||
func hideOptions(_ sender: Any)
|
||||
func showOptions(share: NKShare)
|
||||
func showError(_ errorString: String)
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// ShareeSuggestionsDataSource.swift
|
||||
// FileProviderUIExt
|
||||
//
|
||||
// Created by Claudio Cambra on 2/4/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import NextcloudKit
|
||||
import OSLog
|
||||
import SuggestionsTextFieldKit
|
||||
|
||||
class ShareeSuggestionsDataSource: SuggestionsDataSource {
|
||||
let kit: NextcloudKit
|
||||
var suggestions: [Suggestion] = []
|
||||
var inputString: String = "" {
|
||||
didSet { Task { await updateSuggestions() } }
|
||||
}
|
||||
|
||||
init(kit: NextcloudKit) {
|
||||
self.kit = kit
|
||||
}
|
||||
|
||||
private func updateSuggestions() async {
|
||||
let sharees = await fetchSharees(search: inputString)
|
||||
Logger.shareeDataSource.info("Fetched \(sharees.count, privacy: .public) sharees.")
|
||||
suggestions = suggestionsFromSharees(sharees)
|
||||
NotificationCenter.default.post(name: SuggestionsChangedNotificationName, object: self)
|
||||
}
|
||||
|
||||
private func fetchSharees(search: String) async -> [NKSharee] {
|
||||
Logger.shareeDataSource.debug("Searching sharees with: \(search, privacy: .public)")
|
||||
return await withCheckedContinuation { continuation in
|
||||
kit.searchSharees(
|
||||
search: inputString,
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
completion: { account, sharees, data, error in
|
||||
defer { continuation.resume(returning: sharees ?? []) }
|
||||
guard error == .success else {
|
||||
Logger.shareeDataSource.error(
|
||||
"Error fetching sharees: \(error.description, privacy: .public)"
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func suggestionsFromSharees(_ sharees: [NKSharee]) -> [Suggestion] {
|
||||
return sharees.map {
|
||||
Suggestion(
|
||||
imageName: "person.fill",
|
||||
displayText: $0.label.isEmpty ? $0.name : $0.label,
|
||||
data: $0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,29 +3,29 @@
|
|||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
5307A6E62965C6FA001E0C6A /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5307A6E52965C6FA001E0C6A /* NextcloudKit */; };
|
||||
5307A6E82965DAD8001E0C6A /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5307A6E72965DAD8001E0C6A /* NextcloudKit */; };
|
||||
5307A6EB2965DB8D001E0C6A /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5307A6EA2965DB8D001E0C6A /* RealmSwift */; };
|
||||
5307A6F229675346001E0C6A /* NextcloudFilesDatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5307A6F129675346001E0C6A /* NextcloudFilesDatabaseManager.swift */; };
|
||||
5318AD9129BF42FB00CBB71C /* NextcloudItemMetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9029BF42FB00CBB71C /* NextcloudItemMetadataTable.swift */; };
|
||||
5318AD9529BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9429BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift */; };
|
||||
5318AD9729BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9629BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift */; };
|
||||
5318AD9929BF58D000CBB71C /* NKError+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9829BF58D000CBB71C /* NKError+Extensions.swift */; };
|
||||
5352B36629DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36529DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift */; };
|
||||
5352B36829DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36729DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift */; };
|
||||
531522822B8E01C6002E31BE /* ShareTableItemView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 531522812B8E01C6002E31BE /* ShareTableItemView.xib */; };
|
||||
5350E4E92B0C534A00F276CB /* ClientCommunicationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */; };
|
||||
5352B36C29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */; };
|
||||
5352E85B29B7BFE6002CE85C /* Progress+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352E85A29B7BFE6002CE85C /* Progress+Extensions.swift */; };
|
||||
5358F2B92BAA0F5300E3C729 /* NextcloudCapabilitiesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */; };
|
||||
535AE30E29C0A2CC0042A9BA /* Logger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */; };
|
||||
53651E442BBC0CA300ECAC29 /* SuggestionsTextFieldKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53651E432BBC0CA300ECAC29 /* SuggestionsTextFieldKit */; };
|
||||
53651E462BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */; };
|
||||
536EFBF7295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536EFBF6295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift */; };
|
||||
536EFC36295E3C1100F4CB13 /* NextcloudAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */; };
|
||||
5374FD442B95EE1400C78D54 /* ShareController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5374FD432B95EE1400C78D54 /* ShareController.swift */; };
|
||||
5376307D2B85E2ED0026BFAB /* Logger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */; };
|
||||
537630912B85F4980026BFAB /* ShareViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 537630902B85F4980026BFAB /* ShareViewController.xib */; };
|
||||
537630932B85F4B00026BFAB /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630922B85F4B00026BFAB /* ShareViewController.swift */; };
|
||||
537630952B860D560026BFAB /* FPUIExtensionServiceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */; };
|
||||
537630972B860D920026BFAB /* FPUIExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630962B860D920026BFAB /* FPUIExtensionService.swift */; };
|
||||
537630982B8612F00026BFAB /* FPUIExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630962B860D920026BFAB /* FPUIExtensionService.swift */; };
|
||||
538E396A27F4765000FA63D5 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 538E396927F4765000FA63D5 /* UniformTypeIdentifiers.framework */; };
|
||||
538E396D27F4765000FA63D5 /* FileProviderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538E396C27F4765000FA63D5 /* FileProviderExtension.swift */; };
|
||||
538E396F27F4765000FA63D5 /* FileProviderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538E396E27F4765000FA63D5 /* FileProviderItem.swift */; };
|
||||
538E397127F4765000FA63D5 /* FileProviderEnumerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538E397027F4765000FA63D5 /* FileProviderEnumerator.swift */; };
|
||||
538E397627F4765000FA63D5 /* FileProviderExt.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 538E396727F4765000FA63D5 /* FileProviderExt.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
53903D1E2956164F00D0B308 /* NCDesktopClientSocketKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 53903D0E2956164F00D0B308 /* NCDesktopClientSocketKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
53903D212956164F00D0B308 /* NCDesktopClientSocketKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53903D0C2956164F00D0B308 /* NCDesktopClientSocketKit.framework */; };
|
||||
|
@ -38,10 +38,17 @@
|
|||
53903D352956184400D0B308 /* LocalSocketClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 539158B127BE891500816F56 /* LocalSocketClient.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
53903D37295618A400D0B308 /* LineProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 53903D36295618A400D0B308 /* LineProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
539158AC27BE71A900816F56 /* FinderSyncSocketLineProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 539158AB27BE71A900816F56 /* FinderSyncSocketLineProcessor.m */; };
|
||||
53D056312970594F00988392 /* LocalFilesUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D056302970594F00988392 /* LocalFilesUtils.swift */; };
|
||||
53ED472029C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ED471F29C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift */; };
|
||||
53ED472829C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ED472729C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift */; };
|
||||
53B979812B84C81F002DA742 /* DocumentActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53B979802B84C81F002DA742 /* DocumentActionViewController.swift */; };
|
||||
53C331B22BCD28C30093D38B /* NextcloudFileProviderKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53C331B12BCD28C30093D38B /* NextcloudFileProviderKit */; };
|
||||
53C331B62BCD3AFF0093D38B /* NextcloudFileProviderKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53C331B52BCD3AFF0093D38B /* NextcloudFileProviderKit */; };
|
||||
53D666612B70C9A70042C03D /* FileProviderConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D666602B70C9A70042C03D /* FileProviderConfig.swift */; };
|
||||
53ED473029C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ED472F29C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift */; };
|
||||
53FE14502B8E0658006C4193 /* ShareTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */; };
|
||||
53FE14542B8E1219006C4193 /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53FE14532B8E1219006C4193 /* NextcloudKit */; };
|
||||
53FE14592B8E3F6C006C4193 /* ShareTableItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE14582B8E3F6C006C4193 /* ShareTableItemView.swift */; };
|
||||
53FE145B2B8F1305006C4193 /* NKShare+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE145A2B8F1305006C4193 /* NKShare+Extensions.swift */; };
|
||||
53FE14652B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE14642B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift */; };
|
||||
53FE14672B8F78B6006C4193 /* ShareOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE14662B8F78B6006C4193 /* ShareOptionsView.swift */; };
|
||||
C2B573BA1B1CD91E00303B36 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C2B573B91B1CD91E00303B36 /* main.m */; };
|
||||
C2B573D21B1CD94B00303B36 /* main.m in Resources */ = {isa = PBXBuildFile; fileRef = C2B573B91B1CD91E00303B36 /* main.m */; };
|
||||
C2B573DE1B1CD9CE00303B36 /* FinderSync.m in Sources */ = {isa = PBXBuildFile; fileRef = C2B573DD1B1CD9CE00303B36 /* FinderSync.m */; };
|
||||
|
@ -140,23 +147,23 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
5307A6F129675346001E0C6A /* NextcloudFilesDatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudFilesDatabaseManager.swift; sourceTree = "<group>"; };
|
||||
5318AD9029BF42FB00CBB71C /* NextcloudItemMetadataTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudItemMetadataTable.swift; sourceTree = "<group>"; };
|
||||
5318AD9429BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudLocalFileMetadataTable.swift; sourceTree = "<group>"; };
|
||||
5318AD9629BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderMaterialisedEnumerationObserver.swift; sourceTree = "<group>"; };
|
||||
5318AD9829BF58D000CBB71C /* NKError+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NKError+Extensions.swift"; sourceTree = "<group>"; };
|
||||
5352B36529DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NextcloudFilesDatabaseManager+Directories.swift"; sourceTree = "<group>"; };
|
||||
5352B36729DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NextcloudFilesDatabaseManager+LocalFiles.swift"; sourceTree = "<group>"; };
|
||||
531522812B8E01C6002E31BE /* ShareTableItemView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShareTableItemView.xib; sourceTree = "<group>"; };
|
||||
5350E4E72B0C514400F276CB /* ClientCommunicationProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ClientCommunicationProtocol.h; sourceTree = "<group>"; };
|
||||
5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientCommunicationService.swift; sourceTree = "<group>"; };
|
||||
5350E4EA2B0C9CE100F276CB /* FileProviderExt-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FileProviderExt-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderExtension+Thumbnailing.swift"; sourceTree = "<group>"; };
|
||||
5352E85A29B7BFE6002CE85C /* Progress+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+Extensions.swift"; sourceTree = "<group>"; };
|
||||
535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Extensions.swift"; sourceTree = "<group>"; };
|
||||
53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareeSuggestionsDataSource.swift; sourceTree = "<group>"; };
|
||||
536EFBF6295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderSocketLineProcessor.swift; sourceTree = "<group>"; };
|
||||
536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudAccount.swift; sourceTree = "<group>"; };
|
||||
5374FD432B95EE1400C78D54 /* ShareController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareController.swift; sourceTree = "<group>"; };
|
||||
5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Extensions.swift"; sourceTree = "<group>"; };
|
||||
537630902B85F4980026BFAB /* ShareViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareViewController.xib; sourceTree = "<group>"; };
|
||||
537630922B85F4B00026BFAB /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUIExtensionServiceSource.swift; sourceTree = "<group>"; };
|
||||
537630962B860D920026BFAB /* FPUIExtensionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUIExtensionService.swift; sourceTree = "<group>"; };
|
||||
538E396727F4765000FA63D5 /* FileProviderExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderExt.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
538E396927F4765000FA63D5 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
|
||||
538E396C27F4765000FA63D5 /* FileProviderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderExtension.swift; sourceTree = "<group>"; };
|
||||
538E396E27F4765000FA63D5 /* FileProviderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderItem.swift; sourceTree = "<group>"; };
|
||||
538E397027F4765000FA63D5 /* FileProviderEnumerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderEnumerator.swift; sourceTree = "<group>"; };
|
||||
538E397227F4765000FA63D5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
538E397327F4765000FA63D5 /* FileProviderExt.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FileProviderExt.entitlements; sourceTree = "<group>"; };
|
||||
53903D0C2956164F00D0B308 /* NCDesktopClientSocketKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NCDesktopClientSocketKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -167,10 +174,17 @@
|
|||
539158AB27BE71A900816F56 /* FinderSyncSocketLineProcessor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FinderSyncSocketLineProcessor.m; sourceTree = "<group>"; };
|
||||
539158B127BE891500816F56 /* LocalSocketClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LocalSocketClient.h; sourceTree = "<group>"; };
|
||||
539158B227BEC98A00816F56 /* LocalSocketClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocalSocketClient.m; sourceTree = "<group>"; };
|
||||
53D056302970594F00988392 /* LocalFilesUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFilesUtils.swift; sourceTree = "<group>"; };
|
||||
53ED471F29C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderEnumerator+SyncEngine.swift"; sourceTree = "<group>"; };
|
||||
53ED472729C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NextcloudItemMetadataTable+NKFile.swift"; sourceTree = "<group>"; };
|
||||
53B9797E2B84C81F002DA742 /* FileProviderUIExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderUIExt.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
53B979802B84C81F002DA742 /* DocumentActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentActionViewController.swift; sourceTree = "<group>"; };
|
||||
53B979852B84C81F002DA742 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
53D666602B70C9A70042C03D /* FileProviderConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderConfig.swift; sourceTree = "<group>"; };
|
||||
53ED472F29C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderExtension+ClientInterface.swift"; sourceTree = "<group>"; };
|
||||
53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareTableViewDataSource.swift; sourceTree = "<group>"; };
|
||||
53FE14572B8E3A7C006C4193 /* FileProviderUIExt.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FileProviderUIExt.entitlements; sourceTree = "<group>"; };
|
||||
53FE14582B8E3F6C006C4193 /* ShareTableItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareTableItemView.swift; sourceTree = "<group>"; };
|
||||
53FE145A2B8F1305006C4193 /* NKShare+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NKShare+Extensions.swift"; sourceTree = "<group>"; };
|
||||
53FE14642B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewDataSourceUIDelegate.swift; sourceTree = "<group>"; };
|
||||
53FE14662B8F78B6006C4193 /* ShareOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareOptionsView.swift; sourceTree = "<group>"; };
|
||||
C2B573B11B1CD91E00303B36 /* desktopclient.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = desktopclient.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C2B573B51B1CD91E00303B36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
C2B573B91B1CD91E00303B36 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||
|
@ -191,10 +205,10 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5307A6EB2965DB8D001E0C6A /* RealmSwift in Frameworks */,
|
||||
5307A6E82965DAD8001E0C6A /* NextcloudKit in Frameworks */,
|
||||
538E396A27F4765000FA63D5 /* UniformTypeIdentifiers.framework in Frameworks */,
|
||||
53903D302956173F00D0B308 /* NCDesktopClientSocketKit.framework in Frameworks */,
|
||||
53C331B22BCD28C30093D38B /* NextcloudFileProviderKit in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -205,6 +219,17 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
53B9797B2B84C81F002DA742 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5358F2B92BAA0F5300E3C729 /* NextcloudCapabilitiesKit in Frameworks */,
|
||||
53C331B62BCD3AFF0093D38B /* NextcloudFileProviderKit in Frameworks */,
|
||||
53651E442BBC0CA300ECAC29 /* SuggestionsTextFieldKit in Frameworks */,
|
||||
53FE14542B8E1219006C4193 /* NextcloudKit in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
C2B573AE1B1CD91E00303B36 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -225,25 +250,30 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
5318AD8F29BF406500CBB71C /* Database */ = {
|
||||
5350E4C72B0C368B00F276CB /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5307A6F129675346001E0C6A /* NextcloudFilesDatabaseManager.swift */,
|
||||
5352B36529DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift */,
|
||||
5352B36729DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift */,
|
||||
5318AD9029BF42FB00CBB71C /* NextcloudItemMetadataTable.swift */,
|
||||
53ED472729C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift */,
|
||||
5318AD9429BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift */,
|
||||
5350E4E72B0C514400F276CB /* ClientCommunicationProtocol.h */,
|
||||
5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */,
|
||||
537630962B860D920026BFAB /* FPUIExtensionService.swift */,
|
||||
537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */,
|
||||
);
|
||||
path = Database;
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5352E85929B7BFB4002CE85C /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */,
|
||||
5318AD9829BF58D000CBB71C /* NKError+Extensions.swift */,
|
||||
5352E85A29B7BFE6002CE85C /* Progress+Extensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5376307B2B85E2E00026BFAB /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */,
|
||||
53FE145A2B8F1305006C4193 /* NKShare+Extensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
@ -259,20 +289,16 @@
|
|||
538E396B27F4765000FA63D5 /* FileProviderExt */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5318AD8F29BF406500CBB71C /* Database */,
|
||||
5352E85929B7BFB4002CE85C /* Extensions */,
|
||||
538E397027F4765000FA63D5 /* FileProviderEnumerator.swift */,
|
||||
53ED471F29C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift */,
|
||||
5350E4C72B0C368B00F276CB /* Services */,
|
||||
53D666602B70C9A70042C03D /* FileProviderConfig.swift */,
|
||||
538E396C27F4765000FA63D5 /* FileProviderExtension.swift */,
|
||||
53ED472F29C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift */,
|
||||
5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */,
|
||||
538E396E27F4765000FA63D5 /* FileProviderItem.swift */,
|
||||
5318AD9629BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift */,
|
||||
536EFBF6295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift */,
|
||||
53D056302970594F00988392 /* LocalFilesUtils.swift */,
|
||||
536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */,
|
||||
538E397327F4765000FA63D5 /* FileProviderExt.entitlements */,
|
||||
538E397227F4765000FA63D5 /* Info.plist */,
|
||||
5350E4EA2B0C9CE100F276CB /* FileProviderExt-Bridging-Header.h */,
|
||||
);
|
||||
path = FileProviderExt;
|
||||
sourceTree = "<group>";
|
||||
|
@ -288,12 +314,33 @@
|
|||
path = NCDesktopClientSocketKit;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
53B9797F2B84C81F002DA742 /* FileProviderUIExt */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5376307B2B85E2E00026BFAB /* Extensions */,
|
||||
53B979802B84C81F002DA742 /* DocumentActionViewController.swift */,
|
||||
5374FD432B95EE1400C78D54 /* ShareController.swift */,
|
||||
53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */,
|
||||
53FE14662B8F78B6006C4193 /* ShareOptionsView.swift */,
|
||||
53FE14582B8E3F6C006C4193 /* ShareTableItemView.swift */,
|
||||
531522812B8E01C6002E31BE /* ShareTableItemView.xib */,
|
||||
53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */,
|
||||
537630922B85F4B00026BFAB /* ShareViewController.swift */,
|
||||
537630902B85F4980026BFAB /* ShareViewController.xib */,
|
||||
53FE14642B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift */,
|
||||
53FE14572B8E3A7C006C4193 /* FileProviderUIExt.entitlements */,
|
||||
53B979852B84C81F002DA742 /* Info.plist */,
|
||||
);
|
||||
path = FileProviderUIExt;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C2B573941B1CD88000303B36 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C2B573B31B1CD91E00303B36 /* desktopclient */,
|
||||
C2B573D81B1CD9CE00303B36 /* FinderSyncExt */,
|
||||
538E396B27F4765000FA63D5 /* FileProviderExt */,
|
||||
53B9797F2B84C81F002DA742 /* FileProviderUIExt */,
|
||||
53903D0D2956164F00D0B308 /* NCDesktopClientSocketKit */,
|
||||
538E396827F4765000FA63D5 /* Frameworks */,
|
||||
C2B573B21B1CD91E00303B36 /* Products */,
|
||||
|
@ -307,6 +354,7 @@
|
|||
C2B573D71B1CD9CE00303B36 /* FinderSyncExt.appex */,
|
||||
538E396727F4765000FA63D5 /* FileProviderExt.appex */,
|
||||
53903D0C2956164F00D0B308 /* NCDesktopClientSocketKit.framework */,
|
||||
53B9797E2B84C81F002DA742 /* FileProviderUIExt.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
@ -388,7 +436,7 @@
|
|||
name = FileProviderExt;
|
||||
packageProductDependencies = (
|
||||
5307A6E72965DAD8001E0C6A /* NextcloudKit */,
|
||||
5307A6EA2965DB8D001E0C6A /* RealmSwift */,
|
||||
53C331B12BCD28C30093D38B /* NextcloudFileProviderKit */,
|
||||
);
|
||||
productName = FileProviderExt;
|
||||
productReference = 538E396727F4765000FA63D5 /* FileProviderExt.appex */;
|
||||
|
@ -412,6 +460,30 @@
|
|||
productReference = 53903D0C2956164F00D0B308 /* NCDesktopClientSocketKit.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
53B9797D2B84C81F002DA742 /* FileProviderUIExt */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 53B979882B84C820002DA742 /* Build configuration list for PBXNativeTarget "FileProviderUIExt" */;
|
||||
buildPhases = (
|
||||
53B9797A2B84C81F002DA742 /* Sources */,
|
||||
53B9797B2B84C81F002DA742 /* Frameworks */,
|
||||
53B9797C2B84C81F002DA742 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
53FE14522B8E1213006C4193 /* PBXTargetDependency */,
|
||||
);
|
||||
name = FileProviderUIExt;
|
||||
packageProductDependencies = (
|
||||
53FE14532B8E1219006C4193 /* NextcloudKit */,
|
||||
5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */,
|
||||
53651E432BBC0CA300ECAC29 /* SuggestionsTextFieldKit */,
|
||||
53C331B52BCD3AFF0093D38B /* NextcloudFileProviderKit */,
|
||||
);
|
||||
productName = FileProviderUIExt;
|
||||
productReference = 53B9797E2B84C81F002DA742 /* FileProviderUIExt.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
C2B573B01B1CD91E00303B36 /* desktopclient */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = C2B573CC1B1CD91E00303B36 /* Build configuration list for PBXNativeTarget "desktopclient" */;
|
||||
|
@ -464,7 +536,7 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 1420;
|
||||
LastSwiftUpdateCheck = 1530;
|
||||
LastUpgradeCheck = 1240;
|
||||
TargetAttributes = {
|
||||
538E396627F4765000FA63D5 = {
|
||||
|
@ -474,6 +546,9 @@
|
|||
CreatedOnToolsVersion = 14.2;
|
||||
ProvisioningStyle = Manual;
|
||||
};
|
||||
53B9797D2B84C81F002DA742 = {
|
||||
CreatedOnToolsVersion = 15.2;
|
||||
};
|
||||
C2B573B01B1CD91E00303B36 = {
|
||||
CreatedOnToolsVersion = 6.3.1;
|
||||
DevelopmentTeam = 9B5WD74GWJ;
|
||||
|
@ -503,7 +578,9 @@
|
|||
mainGroup = C2B573941B1CD88000303B36;
|
||||
packageReferences = (
|
||||
5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */,
|
||||
5307A6E92965DB57001E0C6A /* XCRemoteSwiftPackageReference "realm-swift" */,
|
||||
5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */,
|
||||
53651E422BBC0C7F00ECAC29 /* XCRemoteSwiftPackageReference "SuggestionsTextFieldKit" */,
|
||||
53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */,
|
||||
);
|
||||
productRefGroup = C2B573B21B1CD91E00303B36 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -512,6 +589,7 @@
|
|||
C2B573B01B1CD91E00303B36 /* desktopclient */,
|
||||
C2B573D61B1CD9CE00303B36 /* FinderSyncExt */,
|
||||
538E396627F4765000FA63D5 /* FileProviderExt */,
|
||||
53B9797D2B84C81F002DA742 /* FileProviderUIExt */,
|
||||
53903D0B2956164F00D0B308 /* NCDesktopClientSocketKit */,
|
||||
);
|
||||
};
|
||||
|
@ -532,6 +610,15 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
53B9797C2B84C81F002DA742 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
531522822B8E01C6002E31BE /* ShareTableItemView.xib in Resources */,
|
||||
537630912B85F4980026BFAB /* ShareViewController.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
C2B573AF1B1CD91E00303B36 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -576,25 +663,15 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5352E85B29B7BFE6002CE85C /* Progress+Extensions.swift in Sources */,
|
||||
536EFC36295E3C1100F4CB13 /* NextcloudAccount.swift in Sources */,
|
||||
53D666612B70C9A70042C03D /* FileProviderConfig.swift in Sources */,
|
||||
53ED473029C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift in Sources */,
|
||||
538E396D27F4765000FA63D5 /* FileProviderExtension.swift in Sources */,
|
||||
536EFBF7295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift in Sources */,
|
||||
53ED472029C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift in Sources */,
|
||||
5318AD9929BF58D000CBB71C /* NKError+Extensions.swift in Sources */,
|
||||
53ED472829C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift in Sources */,
|
||||
5318AD9529BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift in Sources */,
|
||||
537630972B860D920026BFAB /* FPUIExtensionService.swift in Sources */,
|
||||
535AE30E29C0A2CC0042A9BA /* Logger+Extensions.swift in Sources */,
|
||||
5307A6F229675346001E0C6A /* NextcloudFilesDatabaseManager.swift in Sources */,
|
||||
53D056312970594F00988392 /* LocalFilesUtils.swift in Sources */,
|
||||
538E396F27F4765000FA63D5 /* FileProviderItem.swift in Sources */,
|
||||
5352B36829DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift in Sources */,
|
||||
5318AD9129BF42FB00CBB71C /* NextcloudItemMetadataTable.swift in Sources */,
|
||||
5352B36629DC14970011CE03 /* NextcloudFilesDatabaseManager+Directories.swift in Sources */,
|
||||
5318AD9729BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift in Sources */,
|
||||
537630952B860D560026BFAB /* FPUIExtensionServiceSource.swift in Sources */,
|
||||
5350E4E92B0C534A00F276CB /* ClientCommunicationService.swift in Sources */,
|
||||
5352B36C29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift in Sources */,
|
||||
538E397127F4765000FA63D5 /* FileProviderEnumerator.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -606,6 +683,24 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
53B9797A2B84C81F002DA742 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
537630932B85F4B00026BFAB /* ShareViewController.swift in Sources */,
|
||||
53FE14672B8F78B6006C4193 /* ShareOptionsView.swift in Sources */,
|
||||
53651E462BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift in Sources */,
|
||||
53FE14652B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift in Sources */,
|
||||
53B979812B84C81F002DA742 /* DocumentActionViewController.swift in Sources */,
|
||||
5374FD442B95EE1400C78D54 /* ShareController.swift in Sources */,
|
||||
53FE145B2B8F1305006C4193 /* NKShare+Extensions.swift in Sources */,
|
||||
53FE14592B8E3F6C006C4193 /* ShareTableItemView.swift in Sources */,
|
||||
5376307D2B85E2ED0026BFAB /* Logger+Extensions.swift in Sources */,
|
||||
53FE14502B8E0658006C4193 /* ShareTableViewDataSource.swift in Sources */,
|
||||
537630982B8612F00026BFAB /* FPUIExtensionService.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
C2B573AD1B1CD91E00303B36 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -646,6 +741,10 @@
|
|||
target = 53903D0B2956164F00D0B308 /* NCDesktopClientSocketKit */;
|
||||
targetProxy = 53903D322956173F00D0B308 /* PBXContainerItemProxy */;
|
||||
};
|
||||
53FE14522B8E1213006C4193 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
productRef = 53FE14512B8E1213006C4193 /* NextcloudKit */;
|
||||
};
|
||||
C2B573E01B1CD9CE00303B36 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = C2B573D61B1CD9CE00303B36 /* FinderSyncExt */;
|
||||
|
@ -677,6 +776,7 @@
|
|||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_TESTING_SEARCH_PATHS = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
|
@ -704,6 +804,7 @@
|
|||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "FileProviderExt/FileProviderExt-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
|
@ -733,6 +834,7 @@
|
|||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_TESTING_SEARCH_PATHS = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
|
@ -753,6 +855,7 @@
|
|||
SKIP_INSTALL = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "FileProviderExt/FileProviderExt-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
|
@ -886,6 +989,128 @@
|
|||
};
|
||||
name = Release;
|
||||
};
|
||||
53B979862B84C81F002DA742 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = FileProviderUIExt/FileProviderUIExt.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = FileProviderUIExt/Info.plist;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_OUTPUT_FORMAT = "same-as-input";
|
||||
INFOPLIST_PREPROCESS = NO;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(OC_APPLICATION_REV_DOMAIN).$(PRODUCT_NAME)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = macosx;
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = macOS;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
53B979872B84C81F002DA742 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = FileProviderUIExt/FileProviderUIExt.entitlements;
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = FileProviderUIExt/Info.plist;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_OUTPUT_FORMAT = "same-as-input";
|
||||
INFOPLIST_PREPROCESS = NO;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(OC_APPLICATION_REV_DOMAIN).$(PRODUCT_NAME)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = macosx;
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = macOS;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
C2B573991B1CD88000303B36 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
|
@ -909,6 +1134,7 @@
|
|||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_TESTING_SEARCH_PATHS = YES;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
||||
|
@ -941,6 +1167,7 @@
|
|||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTING_SEARCH_PATHS = YES;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
||||
|
@ -1190,6 +1417,15 @@
|
|||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
53B979882B84C820002DA742 /* Build configuration list for PBXNativeTarget "FileProviderUIExt" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
53B979862B84C81F002DA742 /* Debug */,
|
||||
53B979872B84C81F002DA742 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
C2B573981B1CD88000303B36 /* Build configuration list for PBXProject "NextcloudIntegration" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
@ -1222,18 +1458,34 @@
|
|||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/nextcloud/NextcloudKit";
|
||||
repositoryURL = "https://github.com/nextcloud/NextcloudKit.git";
|
||||
requirement = {
|
||||
branch = develop;
|
||||
kind = branch;
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.5.9;
|
||||
};
|
||||
};
|
||||
5307A6E92965DB57001E0C6A /* XCRemoteSwiftPackageReference "realm-swift" */ = {
|
||||
5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/realm/realm-swift.git";
|
||||
repositoryURL = "https://github.com/claucambra/NextcloudCapabilitiesKit.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 10.33.0;
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.0.0;
|
||||
};
|
||||
};
|
||||
53651E422BBC0C7F00ECAC29 /* XCRemoteSwiftPackageReference "SuggestionsTextFieldKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/claucambra/SuggestionsTextFieldKit.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.0.0;
|
||||
};
|
||||
};
|
||||
53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/claucambra/NextcloudFileProviderKit.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.9.0;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
@ -1249,10 +1501,35 @@
|
|||
package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */;
|
||||
productName = NextcloudKit;
|
||||
};
|
||||
5307A6EA2965DB8D001E0C6A /* RealmSwift */ = {
|
||||
5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 5307A6E92965DB57001E0C6A /* XCRemoteSwiftPackageReference "realm-swift" */;
|
||||
productName = RealmSwift;
|
||||
package = 5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */;
|
||||
productName = NextcloudCapabilitiesKit;
|
||||
};
|
||||
53651E432BBC0CA300ECAC29 /* SuggestionsTextFieldKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 53651E422BBC0C7F00ECAC29 /* XCRemoteSwiftPackageReference "SuggestionsTextFieldKit" */;
|
||||
productName = SuggestionsTextFieldKit;
|
||||
};
|
||||
53C331B12BCD28C30093D38B /* NextcloudFileProviderKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */;
|
||||
productName = NextcloudFileProviderKit;
|
||||
};
|
||||
53C331B52BCD3AFF0093D38B /* NextcloudFileProviderKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 53C331B02BCD28C30093D38B /* XCRemoteSwiftPackageReference "NextcloudFileProviderKit" */;
|
||||
productName = NextcloudFileProviderKit;
|
||||
};
|
||||
53FE14512B8E1213006C4193 /* NextcloudKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */;
|
||||
productName = NextcloudKit;
|
||||
};
|
||||
53FE14532B8E1219006C4193 /* NextcloudKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */;
|
||||
productName = NextcloudKit;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
|
|
|
@ -2,20 +2,28 @@ project(dolphin-owncloud)
|
|||
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
set(QT_MIN_VERSION "5.15.0")
|
||||
set(KF5_MIN_VERSION "5.16.0")
|
||||
if(KF6KIO_FOUND)
|
||||
set(QT_MAJOR_VERSION "6")
|
||||
set(QT_MIN_VERSION "6.6.0")
|
||||
set(KF_MIN_VERSION "5.240.0")
|
||||
else()
|
||||
set(QT_MAJOR_VERSION "5")
|
||||
set(QT_MIN_VERSION "5.15.0")
|
||||
set(KF_MIN_VERSION "5.16.0")
|
||||
endif()
|
||||
|
||||
set(KDE_INSTALL_USE_QT_SYS_PATHS ON CACHE BOOL "Install the plugin in the right directory")
|
||||
|
||||
find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Network)
|
||||
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Network)
|
||||
|
||||
find_package(ECM ${KF5_MIN_VERSION} REQUIRED CONFIG)
|
||||
find_package(ECM ${KF_MIN_VERSION} REQUIRED CONFIG)
|
||||
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||
|
||||
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS CoreAddons KIO)
|
||||
find_package(KF${QT_MAJOR_VERSION} ${KF_MIN_VERSION} REQUIRED COMPONENTS CoreAddons KIO)
|
||||
|
||||
set(KDE_INSTALL_DIRS_NO_DEPRECATED TRUE)
|
||||
include(KDEInstallDirs)
|
||||
# Before KF5 5.54, kcoreaddons_add_plugin uses deprecated VAR PLUGIN_INSTALL_DIR
|
||||
# Before KF${QT_MAJOR_VERSION} 5.54, kcoreaddons_add_plugin uses deprecated VAR PLUGIN_INSTALL_DIR
|
||||
# when that is fixed and you want to remove this workaround,
|
||||
# you need to _require_ the new enough kcoreaddons
|
||||
set(PLUGIN_INSTALL_DIR "${KDE_INSTALL_PLUGINDIR}")
|
||||
|
@ -29,20 +37,24 @@ set(OWNCLOUDDOLPHINHELPER ${APPLICATION_EXECUTABLE}dolphinpluginhelper)
|
|||
add_library(${OWNCLOUDDOLPHINHELPER} SHARED
|
||||
ownclouddolphinpluginhelper.h
|
||||
ownclouddolphinpluginhelper.cpp)
|
||||
target_link_libraries(${OWNCLOUDDOLPHINHELPER} Qt5::Network)
|
||||
target_link_libraries(${OWNCLOUDDOLPHINHELPER} Qt${QT_MAJOR_VERSION}::Network)
|
||||
generate_export_header(${OWNCLOUDDOLPHINHELPER} BASE_NAME ownclouddolphinpluginhelper)
|
||||
install(TARGETS ${OWNCLOUDDOLPHINHELPER} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
|
||||
#---OVERLAY PLUGIN---
|
||||
set(OWNCLOUDDOLPHINOVERLAYPLUGIN ${APPLICATION_EXECUTABLE}dolphinoverlayplugin)
|
||||
kcoreaddons_add_plugin(${OWNCLOUDDOLPHINOVERLAYPLUGIN} INSTALL_NAMESPACE "kf5/overlayicon"
|
||||
JSON ownclouddolphinoverlayplugin.json SOURCES ownclouddolphinoverlayplugin.cpp)
|
||||
target_link_libraries(${OWNCLOUDDOLPHINOVERLAYPLUGIN} KF5::CoreAddons KF5::KIOCore KF5::KIOWidgets ${OWNCLOUDDOLPHINHELPER})
|
||||
if(KF6KIO_FOUND)
|
||||
kcoreaddons_add_plugin(${OWNCLOUDDOLPHINOVERLAYPLUGIN} INSTALL_NAMESPACE "kf${QT_MAJOR_VERSION}/overlayicon"
|
||||
SOURCES ownclouddolphinoverlayplugin.cpp)
|
||||
else()
|
||||
kcoreaddons_add_plugin(${OWNCLOUDDOLPHINOVERLAYPLUGIN} INSTALL_NAMESPACE "kf${QT_MAJOR_VERSION}/overlayicon"
|
||||
JSON ownclouddolphinoverlayplugin.json SOURCES ownclouddolphinoverlayplugin.cpp)
|
||||
endif()
|
||||
target_link_libraries(${OWNCLOUDDOLPHINOVERLAYPLUGIN} KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::KIOCore KF${QT_MAJOR_VERSION}::KIOWidgets ${OWNCLOUDDOLPHINHELPER})
|
||||
|
||||
#---ACTION PLUGIN---
|
||||
set(OWNCLOUDDOLPHINACTIONPLUGIN ${APPLICATION_EXECUTABLE}dolphinactionplugin)
|
||||
configure_file(ownclouddolphinactionplugin.desktop.in ${OWNCLOUDDOLPHINACTIONPLUGIN}.desktop ESCAPE_QUOTES @ONLY)
|
||||
kcoreaddons_add_plugin(${OWNCLOUDDOLPHINACTIONPLUGIN} INSTALL_NAMESPACE "kf5/kfileitemaction"
|
||||
configure_file(ownclouddolphinactionplugin.json.in ${OWNCLOUDDOLPHINACTIONPLUGIN}.json ESCAPE_QUOTES @ONLY)
|
||||
kcoreaddons_add_plugin(${OWNCLOUDDOLPHINACTIONPLUGIN} INSTALL_NAMESPACE "kf${QT_MAJOR_VERSION}/kfileitemaction"
|
||||
SOURCES ownclouddolphinactionplugin.cpp)
|
||||
target_link_libraries(${OWNCLOUDDOLPHINACTIONPLUGIN} KF5::CoreAddons KF5::KIOCore KF5::KIOWidgets ${OWNCLOUDDOLPHINHELPER})
|
||||
kcoreaddons_desktop_to_json(${OWNCLOUDDOLPHINACTIONPLUGIN} ${CMAKE_CURRENT_BINARY_DIR}/${OWNCLOUDDOLPHINACTIONPLUGIN}.desktop)
|
||||
target_link_libraries(${OWNCLOUDDOLPHINACTIONPLUGIN} KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::KIOCore KF${QT_MAJOR_VERSION}::KIOWidgets ${OWNCLOUDDOLPHINHELPER})
|
||||
|
|
|
@ -17,17 +17,16 @@
|
|||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
|
||||
******************************************************************************/
|
||||
|
||||
#include <KCoreAddons/KPluginFactory>
|
||||
#include <KCoreAddons/KPluginLoader>
|
||||
#include <KIOWidgets/kabstractfileitemactionplugin.h>
|
||||
#include <KPluginFactory>
|
||||
#include <KAbstractFileItemActionPlugin>
|
||||
#include <QtNetwork/QLocalSocket>
|
||||
#include <KIOCore/kfileitem.h>
|
||||
#include <KIOCore/KFileItemListProperties>
|
||||
#include <QtWidgets/QAction>
|
||||
#include <QtWidgets/QMenu>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtCore/QEventLoop>
|
||||
#include <KFileItem>
|
||||
#include <KFileItemListProperties>
|
||||
#include <QAction>
|
||||
#include <QMenu>
|
||||
#include <QDir>
|
||||
#include <QTimer>
|
||||
#include <QEventLoop>
|
||||
#include "ownclouddolphinpluginhelper.h"
|
||||
|
||||
class OwncloudDolphinPluginAction : public KAbstractFileItemActionPlugin
|
||||
|
@ -78,12 +77,12 @@ public:
|
|||
action->setDisabled(true);
|
||||
auto call = args.value(1).toLatin1();
|
||||
connect(action, &QAction::triggered, [helper, call, files] {
|
||||
helper->sendCommand(QByteArray(call + ":" + files + "\n"));
|
||||
helper->sendCommand(QByteArray(call + ":" + files + "\n").constData());
|
||||
});
|
||||
}
|
||||
});
|
||||
QTimer::singleShot(100, &loop, &QEventLoop::quit); // add a timeout to be sure we don't freeze dolphin
|
||||
helper->sendCommand(QByteArray("GET_MENU_ITEMS:" + files + "\n"));
|
||||
helper->sendCommand(QByteArray("GET_MENU_ITEMS:" + files + "\n").constData());
|
||||
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
disconnect(con);
|
||||
if (menu->actions().isEmpty()) {
|
||||
|
@ -112,20 +111,20 @@ public:
|
|||
|
||||
auto shareAction = menu->addAction(helper->shareActionTitle());
|
||||
connect(shareAction, &QAction::triggered, this, [localFile, helper] {
|
||||
helper->sendCommand(QByteArray("SHARE:" + localFile.toUtf8() + "\n"));
|
||||
helper->sendCommand(QByteArray("SHARE:" + localFile.toUtf8() + "\n").constData());
|
||||
});
|
||||
|
||||
if (!helper->copyPrivateLinkTitle().isEmpty()) {
|
||||
auto copyPrivateLinkAction = menu->addAction(helper->copyPrivateLinkTitle());
|
||||
connect(copyPrivateLinkAction, &QAction::triggered, this, [localFile, helper] {
|
||||
helper->sendCommand(QByteArray("COPY_PRIVATE_LINK:" + localFile.toUtf8() + "\n"));
|
||||
helper->sendCommand(QByteArray("COPY_PRIVATE_LINK:" + localFile.toUtf8() + "\n").constData());
|
||||
});
|
||||
}
|
||||
|
||||
if (!helper->emailPrivateLinkTitle().isEmpty()) {
|
||||
auto emailPrivateLinkAction = menu->addAction(helper->emailPrivateLinkTitle());
|
||||
connect(emailPrivateLinkAction, &QAction::triggered, this, [localFile, helper] {
|
||||
helper->sendCommand(QByteArray("EMAIL_PRIVATE_LINK:" + localFile.toUtf8() + "\n"));
|
||||
helper->sendCommand(QByteArray("EMAIL_PRIVATE_LINK:" + localFile.toUtf8() + "\n").constData());
|
||||
});
|
||||
}
|
||||
return { menuaction };
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
[Desktop Entry]
|
||||
Type=Service
|
||||
Name=@APPLICATION_NAME@Action
|
||||
ServiceTypes=KFileItemAction/Plugin
|
||||
MimeType=application/octet-stream;inode/directory;
|
||||
X-KDE-Library=@APPLICATION_EXECUTABLE@dolphinactionplugin
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"KPlugin": {
|
||||
"MimeTypes": [
|
||||
"application/octet-stream",
|
||||
"inode/directory"
|
||||
],
|
||||
"Name": "@APPLICATION_NAME@Action",
|
||||
"ServiceTypes": [
|
||||
"KFileItemAction/Plugin"
|
||||
]
|
||||
},
|
||||
"MimeType": "application/octet-stream;inode/directory;"
|
||||
}
|
|
@ -20,7 +20,7 @@
|
|||
#include <KOverlayIconPlugin>
|
||||
#include <KPluginFactory>
|
||||
#include <QtNetwork/QLocalSocket>
|
||||
#include <KIOCore/kfileitem.h>
|
||||
#include <KFileItem>
|
||||
#include <QDir>
|
||||
#include <QTimer>
|
||||
#include "ownclouddolphinpluginhelper.h"
|
||||
|
@ -50,7 +50,7 @@ public:
|
|||
QDir localPath(url.toLocalFile());
|
||||
const QByteArray localFile = localPath.canonicalPath().toUtf8();
|
||||
|
||||
helper->sendCommand(QByteArray("RETRIEVE_FILE_STATUS:" + localFile + "\n"));
|
||||
helper->sendCommand(QByteArray("RETRIEVE_FILE_STATUS:" + localFile + "\n").constData());
|
||||
|
||||
StatusMap::iterator it = m_status.find(localFile);
|
||||
if (it != m_status.constEnd()) {
|
||||
|
@ -66,16 +66,16 @@ private:
|
|||
return r;
|
||||
|
||||
if (status.startsWith("OK"))
|
||||
r << "vcs-normal";
|
||||
r << QStringLiteral("vcs-normal");
|
||||
if (status.startsWith("SYNC") || status.startsWith("NEW"))
|
||||
r << "vcs-update-required";
|
||||
r << QStringLiteral("vcs-update-required");
|
||||
if (status.startsWith("IGNORE") || status.startsWith("WARN"))
|
||||
r << "vcs-locally-modified-unstaged";
|
||||
r << QStringLiteral("vcs-locally-modified-unstaged");
|
||||
if (status.startsWith("ERROR"))
|
||||
r << "vcs-conflicting";
|
||||
r << QStringLiteral("vcs-conflicting");
|
||||
|
||||
if (status.contains("+SWM"))
|
||||
r << "document-share";
|
||||
r << QStringLiteral("document-share");
|
||||
|
||||
return r;
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ private:
|
|||
return;
|
||||
status = tokens[1];
|
||||
|
||||
emit overlaysChanged(QUrl::fromLocalFile(QString::fromUtf8(name)), overlaysForString(status));
|
||||
Q_EMIT overlaysChanged(QUrl::fromLocalFile(QString::fromUtf8(name)), overlaysForString(status));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ void OwncloudDolphinPluginHelper::tryConnect()
|
|||
}
|
||||
|
||||
QString socketPath = QStandardPaths::locate(QStandardPaths::RuntimeLocation,
|
||||
APPLICATION_SHORTNAME,
|
||||
QStringLiteral(APPLICATION_SHORTNAME),
|
||||
QStandardPaths::LocateDirectory);
|
||||
if(socketPath.isEmpty())
|
||||
return;
|
||||
|
@ -112,6 +112,6 @@ void OwncloudDolphinPluginHelper::slotReadyRead()
|
|||
return;
|
||||
}
|
||||
}
|
||||
emit commandRecieved(line);
|
||||
Q_EMIT commandRecieved(line);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,23 +36,23 @@ public:
|
|||
|
||||
[[nodiscard]] QString contextMenuTitle() const
|
||||
{
|
||||
return _strings.value("CONTEXT_MENU_TITLE", APPLICATION_NAME);
|
||||
return _strings.value(QStringLiteral("CONTEXT_MENU_TITLE"), QStringLiteral(APPLICATION_NAME));
|
||||
}
|
||||
[[nodiscard]] QString shareActionTitle() const
|
||||
{
|
||||
return _strings.value("SHARE_MENU_TITLE", "Share …");
|
||||
return _strings.value(QStringLiteral("SHARE_MENU_TITLE"), QStringLiteral("Share …"));
|
||||
}
|
||||
[[nodiscard]] QString contextMenuIconName() const
|
||||
{
|
||||
return _strings.value("CONTEXT_MENU_ICON", APPLICATION_ICON_NAME);
|
||||
return _strings.value(QStringLiteral("CONTEXT_MENU_ICON"), QStringLiteral(APPLICATION_ICON_NAME));
|
||||
}
|
||||
|
||||
[[nodiscard]] QString copyPrivateLinkTitle() const { return _strings["COPY_PRIVATE_LINK_MENU_TITLE"]; }
|
||||
[[nodiscard]] QString emailPrivateLinkTitle() const { return _strings["EMAIL_PRIVATE_LINK_MENU_TITLE"]; }
|
||||
[[nodiscard]] QString copyPrivateLinkTitle() const { return _strings[QStringLiteral("COPY_PRIVATE_LINK_MENU_TITLE")]; }
|
||||
[[nodiscard]] QString emailPrivateLinkTitle() const { return _strings[QStringLiteral("EMAIL_PRIVATE_LINK_MENU_TITLE")]; }
|
||||
|
||||
QByteArray version() { return _version; }
|
||||
|
||||
signals:
|
||||
Q_SIGNALS:
|
||||
void commandRecieved(const QByteArray &cmd);
|
||||
|
||||
protected:
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
macro(dbus_add_activation_service _sources)
|
||||
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.28.0")
|
||||
pkg_get_variable(_install_dir dbus-1 session_bus_services_dir DEFINE_VARIABLES datadir=${CMAKE_INSTALL_DATADIR})
|
||||
else()
|
||||
pkg_get_variable(_install_dir dbus-1 session_bus_services_dir)
|
||||
endif()
|
||||
foreach (_i ${_sources})
|
||||
get_filename_component(_service_file ${_i} ABSOLUTE)
|
||||
string(REGEX REPLACE "\\.service.*$" ".service" _output_file ${_i})
|
||||
|
@ -21,8 +25,13 @@ macro(libcloudproviders_add_config _sources)
|
|||
endmacro(libcloudproviders_add_config _sources)
|
||||
|
||||
|
||||
find_package(Qt5 5.15 COMPONENTS DBus)
|
||||
IF (Qt5DBus_FOUND)
|
||||
if (Qt6_FOUND)
|
||||
find_package(Qt6 COMPONENTS COMPONENTS DBus)
|
||||
else()
|
||||
set(REQUIRED_QT_VERSION "5.15.0")
|
||||
find_package(Qt5 ${REQUIRED_QT_VERSION} COMPONENTS DBus)
|
||||
endif()
|
||||
IF (Qt5DBus_FOUND OR Qt6DBus_FOUND)
|
||||
STRING(TOLOWER "${APPLICATION_VENDOR}" DBUS_VENDOR)
|
||||
STRING(REGEX REPLACE "[^A-z0-9]" "" DBUS_VENDOR "${DBUS_VENDOR}")
|
||||
STRING(REGEX REPLACE "[^A-z0-9]" "" DBUS_APPLICATION_NAME "${APPLICATION_SHORTNAME}")
|
||||
|
@ -34,5 +43,8 @@ IF (Qt5DBus_FOUND)
|
|||
set(LIBCLOUDPROVIDERS_DBUS_OBJECT_PATH "/${DBUS_PREFIX}/${DBUS_VENDOR}/${DBUS_APPLICATION_NAME}")
|
||||
|
||||
dbus_add_activation_service(org.freedesktop.CloudProviders.service.in)
|
||||
libcloudproviders_add_config(org.freedesktop.CloudProviders.ini.in)
|
||||
# The .ini file has been replaced by a declaration in the .desktop file in 0.3.3+
|
||||
if (${CLOUDPROVIDERS_VERSION} VERSION_LESS "0.3.3")
|
||||
libcloudproviders_add_config(org.freedesktop.CloudProviders.ini.in)
|
||||
endif ()
|
||||
ENDIF ()
|
||||
|
|
|
@ -48,7 +48,10 @@ NCClientInterface::ContextMenuInfo NCClientInterface::FetchInfo(const std::wstri
|
|||
ContextMenuInfo info;
|
||||
std::wstring response;
|
||||
int sleptCount = 0;
|
||||
while (sleptCount < 5) {
|
||||
constexpr auto noReplyTimeout = 20;
|
||||
constexpr auto replyTimeout = 200;
|
||||
bool receivedReplyFromDesktopClient = false;
|
||||
while ((!receivedReplyFromDesktopClient && sleptCount < noReplyTimeout) || (receivedReplyFromDesktopClient && sleptCount < replyTimeout)) {
|
||||
if (socket.ReadLine(&response)) {
|
||||
if (StringUtil::begins_with(response, wstring(L"REGISTER_PATH:"))) {
|
||||
wstring responsePath = response.substr(14); // length of REGISTER_PATH
|
||||
|
@ -65,6 +68,9 @@ NCClientInterface::ContextMenuInfo NCClientInterface::FetchInfo(const std::wstri
|
|||
if (!StringUtil::extractChunks(response, commandName, flags, title))
|
||||
continue;
|
||||
info.menuItems.push_back({ commandName, flags, title });
|
||||
} else if (StringUtil::begins_with(response, wstring(L"GET_MENU_ITEMS:BEGIN"))) {
|
||||
receivedReplyFromDesktopClient = true;
|
||||
continue;
|
||||
} else if (StringUtil::begins_with(response, wstring(L"GET_MENU_ITEMS:END"))) {
|
||||
break; // Stop once we completely received the last sent request
|
||||
}
|
||||
|
|
|
@ -1,261 +0,0 @@
|
|||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2014 Daniel Molkentin <daniel@molkentin.de>
|
||||
** Contact: http://www.qt-project.org/legal
|
||||
**
|
||||
** This file is part of the QtNetwork module of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:LGPL$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and Digia. For licensing terms and
|
||||
** conditions see http://qt.digia.com/licensing. For further information
|
||||
** use the contact form at http://qt.digia.com/contact-us.
|
||||
**
|
||||
** GNU Lesser General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU Lesser
|
||||
** General Public License version 2.1 as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.LGPL included in the
|
||||
** packaging of this file. Please review the following information to
|
||||
** ensure the GNU Lesser General Public License version 2.1 requirements
|
||||
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
|
||||
**
|
||||
** In addition, as a special exception, Digia gives you certain additional
|
||||
** rights. These rights are described in the Digia Qt LGPL Exception
|
||||
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 3.0 as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.GPL included in the
|
||||
** packaging of this file. Please review the following information to
|
||||
** ensure the GNU General Public License version 3.0 requirements will be
|
||||
** met: http://www.gnu.org/copyleft/gpl.html.
|
||||
**
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef TOKENIZER_H
|
||||
#define TOKENIZER_H
|
||||
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
#include <QSharedPointer>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
template <class T, class const_iterator>
|
||||
struct QTokenizerPrivate {
|
||||
using char_type = typename T::value_type;
|
||||
|
||||
struct State {
|
||||
bool inQuote = false;
|
||||
bool inEscape = false;
|
||||
char_type quoteChar = '\0';
|
||||
};
|
||||
|
||||
QTokenizerPrivate(const T& _string, const T& _delims) :
|
||||
string(_string)
|
||||
, begin(string.begin())
|
||||
, end(string.end())
|
||||
, tokenBegin(end)
|
||||
, tokenEnd(begin)
|
||||
, delimiters(_delims)
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isDelimiter(char_type c) const {
|
||||
return delimiters.contains(c);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isQuote(char_type c) const {
|
||||
return quotes.contains(c);
|
||||
}
|
||||
|
||||
// Returns true if a delimiter was not hit
|
||||
bool nextChar(State* state, char_type c) {
|
||||
if (state->inQuote) {
|
||||
if (state->inEscape) {
|
||||
state->inEscape = false;
|
||||
} else if (c == '\\') {
|
||||
state->inEscape = true;
|
||||
} else if (c == state->quoteChar) {
|
||||
state->inQuote = false;
|
||||
}
|
||||
} else {
|
||||
if (isDelimiter(c))
|
||||
return false;
|
||||
state->inQuote = isQuote(state->quoteChar = c);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
T string;
|
||||
// ### copies begin and end for performance, premature optimization?
|
||||
const_iterator begin;
|
||||
const_iterator end;
|
||||
const_iterator tokenBegin;
|
||||
const_iterator tokenEnd;
|
||||
T delimiters;
|
||||
T quotes;
|
||||
bool isDelim = false;
|
||||
bool returnDelimiters = false;
|
||||
bool returnQuotes = false;
|
||||
};
|
||||
|
||||
template <class T, class const_iterator = typename T::const_iterator>
|
||||
class QTokenizer {
|
||||
public:
|
||||
using char_type = typename T::value_type;
|
||||
|
||||
/*!
|
||||
\class QTokenizer
|
||||
\inmodule QtNetwork
|
||||
\brief QTokenizer tokenizes Strings on QString, QByteArray,
|
||||
std::string or std::wstring
|
||||
|
||||
Example Usage:
|
||||
|
||||
\code
|
||||
QString str = ...;
|
||||
QByteArrayTokenizer tokenizer(str, "; ");
|
||||
tokenizer.setQuoteCharacters("\"'");
|
||||
tokenizer.setReturnDelimiters(true);
|
||||
while (tokenizer.hasNext()) {
|
||||
QByteArray token = tokenizer.next();
|
||||
bool isDelimiter = tokenizer.isDelimiter();
|
||||
...
|
||||
}
|
||||
\endcode
|
||||
|
||||
\param string The string to tokenize
|
||||
\param delimiters A string containing delimiters
|
||||
|
||||
\sa QStringTokenizer, QByteArrayTokenizer, StringTokenizer, WStringTokenizer
|
||||
*/
|
||||
QTokenizer(const T& string, const T& delimiters)
|
||||
: d(new QTokenizerPrivate<T, const_iterator>(string, delimiters))
|
||||
{ }
|
||||
|
||||
/*!
|
||||
Whether or not to return delimiters as tokens
|
||||
\see setQuoteCharacters
|
||||
*/
|
||||
void setReturnDelimiters(bool enable) { d->returnDelimiters = enable; }
|
||||
|
||||
|
||||
/*!
|
||||
Sets characters that are considered to start and end quotes.
|
||||
|
||||
When between two characters considered a quote, delimiters will
|
||||
be ignored.
|
||||
|
||||
When between quotes, blackslash characters will cause the QTokenizer
|
||||
to skip the next character.
|
||||
|
||||
\param quotes Characters that delimit quotes.
|
||||
*/
|
||||
void setQuoteCharacters(const T& quotes) { d->quotes = quotes; }
|
||||
|
||||
|
||||
/*!
|
||||
Whether or not to return delimiters as tokens
|
||||
\see setQuoteCharacters
|
||||
*/
|
||||
void setReturnQuoteCharacters(bool enable) { d->returnQuotes = enable; }
|
||||
|
||||
|
||||
/*!
|
||||
Retrieve next token.
|
||||
|
||||
Returns true if there are more tokens, false otherwise.
|
||||
|
||||
\sa next()
|
||||
*/
|
||||
bool hasNext()
|
||||
{
|
||||
typename QTokenizerPrivate<T, const_iterator>::State state;
|
||||
d->isDelim = false;
|
||||
for (;;) {
|
||||
d->tokenBegin = d->tokenEnd;
|
||||
if (d->tokenEnd == d->end)
|
||||
return false;
|
||||
d->tokenEnd++;
|
||||
if (d->nextChar(&state, *d->tokenBegin))
|
||||
break;
|
||||
if (d->returnDelimiters) {
|
||||
d->isDelim = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
while (d->tokenEnd != d->end && d->nextChar(&state, *d->tokenEnd)) {
|
||||
d->tokenEnd++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*!
|
||||
Resets the tokenizer to the starting position.
|
||||
*/
|
||||
void reset() {
|
||||
d->tokenEnd = d->begin;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns true if the current token is a delimiter,
|
||||
if one more more delimiting characters have been set.
|
||||
*/
|
||||
[[nodiscard]] bool isDelimiter() const { return d->isDelim; }
|
||||
|
||||
/*!
|
||||
Returns the current token.
|
||||
|
||||
Use \c hasNext() to fetch the next token.
|
||||
*/
|
||||
[[nodiscard]] T next() const {
|
||||
int len = std::distance(d->tokenBegin, d->tokenEnd);
|
||||
const_iterator tmpStart = d->tokenBegin;
|
||||
if (!d->returnQuotes && len > 1 && d->isQuote(*d->tokenBegin)) {
|
||||
tmpStart++;
|
||||
len -= 2;
|
||||
}
|
||||
return T(tmpStart, len);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class QStringTokenizer;
|
||||
QSharedPointer<QTokenizerPrivate<T, const_iterator> > d;
|
||||
};
|
||||
|
||||
class QStringTokenizer : public QTokenizer<QString> {
|
||||
public:
|
||||
QStringTokenizer(const QString &string, const QString &delim) :
|
||||
QTokenizer<QString, QString::const_iterator>(string, delim) {}
|
||||
/**
|
||||
* @brief Like \see next(), but returns a lightweight string reference
|
||||
* @return A reference to the token within the string
|
||||
*/
|
||||
QStringRef stringRef() {
|
||||
// If those differences overflow an int we'd have a veeeeeery long string in memory
|
||||
int begin = std::distance(d->begin, d->tokenBegin);
|
||||
int end = std::distance(d->tokenBegin, d->tokenEnd);
|
||||
if (!d->returnQuotes && d->isQuote(*d->tokenBegin)) {
|
||||
begin++;
|
||||
end -= 2;
|
||||
}
|
||||
return QStringRef(&d->string, begin, end);
|
||||
}
|
||||
};
|
||||
|
||||
using QByteArrayTokenizer = QTokenizer<QByteArray>;
|
||||
using StringTokenizer = QTokenizer<std::string>;
|
||||
using WStringTokenizer = QTokenizer<std::wstring>;
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif // TOKENIZER_H
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
TEMPLATE = subdirs
|
||||
SUBDIRS = test
|
|
@ -1,8 +0,0 @@
|
|||
TEMPLATE = app
|
||||
QT += testlib
|
||||
CONFIG += testlib
|
||||
TARGET = test
|
||||
INCLUDEPATH += . ..
|
||||
|
||||
# Input
|
||||
SOURCES += tst_qtokenizer.cpp
|
|
@ -1,139 +0,0 @@
|
|||
#include <QtTest>
|
||||
|
||||
#include "qtokenizer.h"
|
||||
|
||||
namespace {
|
||||
const QString simple = QLatin1String("A simple tokenizer test");
|
||||
const QString quoted = QLatin1String("\"Wait for me!\" he shouted");
|
||||
}
|
||||
|
||||
class TestTokenizer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void tokenizeQStringSimple() {
|
||||
QStringTokenizer tokenizer(simple, " ");
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.next(), QLatin1String("A"));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.next(), QLatin1String("simple"));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.next(), QLatin1String("tokenizer"));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.next(), QLatin1String("test"));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), false);
|
||||
}
|
||||
|
||||
void tokenizeQStringSimpleRef() {
|
||||
QStringTokenizer tokenizer(simple, " ");
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QVERIFY(tokenizer.stringRef() == QLatin1String("A"));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QVERIFY(tokenizer.stringRef() == QLatin1String("simple"));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QVERIFY(tokenizer.stringRef() == QLatin1String("tokenizer"));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QVERIFY(tokenizer.stringRef() == QLatin1String("test"));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), false);
|
||||
}
|
||||
|
||||
void tokenizeQStringQuoted() {
|
||||
const QString multiquote(QLatin1String("\"'Billy - the Kid' is dead!\""));
|
||||
QStringTokenizer tokenizer(multiquote, " -");
|
||||
tokenizer.setQuoteCharacters("\"");
|
||||
tokenizer.setReturnQuoteCharacters(true);
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.next(), QLatin1String("\"'Billy - the Kid' is dead!\""));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), false);
|
||||
}
|
||||
|
||||
void tokenizeQStringSkipQuotes() {
|
||||
const QString multiquote(QLatin1String("\"'Billy - the Kid' is dead!\""));
|
||||
QStringTokenizer tokenizer(multiquote, " ");
|
||||
tokenizer.setQuoteCharacters("\"");
|
||||
tokenizer.setReturnQuoteCharacters(false);
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.next(), QLatin1String("'Billy - the Kid' is dead!"));
|
||||
QCOMPARE(tokenizer.stringRef().toString(), QLatin1String("'Billy - the Kid' is dead!"));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), false);
|
||||
}
|
||||
|
||||
|
||||
void tokenizeQStringWithDelims() {
|
||||
const QString delims(QLatin1String("I;Insist,On/a-Delimiter"));
|
||||
QStringTokenizer tokenizer(delims, ";,/-");
|
||||
tokenizer.setReturnDelimiters(true);
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.isDelimiter(), false);
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.isDelimiter(), true);
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.isDelimiter(), false);
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.isDelimiter(), true);
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.isDelimiter(), false);
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.isDelimiter(), true);
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.isDelimiter(), false);
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.isDelimiter(), true);
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.isDelimiter(), false);
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), false);
|
||||
}
|
||||
|
||||
void resetTokenizer() {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
QStringTokenizer tokenizer(simple, " ");
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.next(), QLatin1String("A"));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.next(), QLatin1String("simple"));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.next(), QLatin1String("tokenizer"));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), true);
|
||||
QCOMPARE(tokenizer.next(), QLatin1String("test"));
|
||||
|
||||
QCOMPARE(tokenizer.hasNext(), false);
|
||||
|
||||
tokenizer.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// ### QByteArray, other types
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(TestTokenizer)
|
||||
|
||||
#include "tst_qtokenizer.moc"
|
||||
|
|
@ -1,70 +1,78 @@
|
|||
# TODO: OSX and LIB_ONLY seem to require this to go to binary dir only
|
||||
if(NOT TOKEN_AUTH_ONLY)
|
||||
endif()
|
||||
|
||||
include(ECMEnableSanitizers)
|
||||
|
||||
set(REQUIRED_QT_VERSION "5.15.0")
|
||||
|
||||
find_package(Qt5Core ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
set_package_properties(Qt5Core PROPERTIES
|
||||
DESCRIPTION "Qt5 Core component."
|
||||
find_package(Qt${QT_MAJOR_VERSION}Core ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION}Core PROPERTIES
|
||||
DESCRIPTION "Qt${QT_MAJOR_VERSION} Core component."
|
||||
TYPE REQUIRED
|
||||
)
|
||||
|
||||
find_package(Qt5Network ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
set_package_properties(Qt5Network PROPERTIES
|
||||
DESCRIPTION "Qt5 Network component."
|
||||
find_package(Qt${QT_MAJOR_VERSION}Network ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION}Network PROPERTIES
|
||||
DESCRIPTION "Qt${QT_MAJOR_VERSION} Network component."
|
||||
TYPE REQUIRED
|
||||
)
|
||||
|
||||
find_package(Qt5Xml ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
set_package_properties(Qt5Xml PROPERTIES
|
||||
DESCRIPTION "Qt5 Xml component."
|
||||
find_package(Qt${QT_MAJOR_VERSION}Xml ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION}Xml PROPERTIES
|
||||
DESCRIPTION "Qt${QT_MAJOR_VERSION} Xml component."
|
||||
TYPE REQUIRED
|
||||
)
|
||||
|
||||
find_package(Qt5Concurrent ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
set_package_properties(Qt5Concurrent PROPERTIES
|
||||
DESCRIPTION "Qt5 Concurrent component."
|
||||
find_package(Qt${QT_MAJOR_VERSION}Concurrent ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION}Concurrent PROPERTIES
|
||||
DESCRIPTION "Qt${QT_MAJOR_VERSION} Concurrent component."
|
||||
TYPE REQUIRED
|
||||
)
|
||||
|
||||
find_package(Qt5WebEngineWidgets ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
find_package(Qt${QT_MAJOR_VERSION}QuickWidgets ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION}QuickWidgets PROPERTIES
|
||||
DESCRIPTION "Qt${QT_MAJOR_VERSION} QuickWidgets component."
|
||||
TYPE REQUIRED
|
||||
)
|
||||
|
||||
find_package(Qt${QT_VERSION_MAJOR}Core5Compat ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
set_package_properties(Qt${QT_VERSION_MAJOR}Core5Compat PROPERTIES
|
||||
DESCRIPTION "Qt${QT_VERSION_MAJOR} Core5Compat component."
|
||||
TYPE REQUIRED
|
||||
)
|
||||
|
||||
find_package(Qt${QT_MAJOR_VERSION}WebEngineWidgets ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
if(NOT BUILD_WITH_WEBENGINE)
|
||||
set_package_properties(Qt5WebEngineWidgets PROPERTIES
|
||||
DESCRIPTION "Qt5 WebEngineWidgets component."
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION}WebEngineWidgets PROPERTIES
|
||||
DESCRIPTION "Qt${QT_MAJOR_VERSION} WebEngineWidgets component."
|
||||
TYPE RECOMMENDED
|
||||
)
|
||||
else()
|
||||
set_package_properties(Qt5WebEngineWidgets PROPERTIES
|
||||
DESCRIPTION "Qt5 WebEngineWidgets component."
|
||||
set_package_properties(Qt${QT_MAJOR_VERSION}WebEngineWidgets PROPERTIES
|
||||
DESCRIPTION "Qt${QT_MAJOR_VERSION} WebEngineWidgets component."
|
||||
TYPE REQUIRED
|
||||
)
|
||||
endif()
|
||||
|
||||
find_package(Qt5WebEngine ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
if(NOT BUILD_WITH_WEBENGINE)
|
||||
set_package_properties(Qt5WebEngine PROPERTIES
|
||||
DESCRIPTION "Qt5 WebEngine component."
|
||||
find_package(Qt${QT_VERSION_MAJOR}WebEngineCore ${REQUIRED_QT_VERSION} CONFIG QUIET)
|
||||
if(APPLE)
|
||||
set_package_properties(Qt${QT_VERSION_MAJOR}WebEngineCore PROPERTIES
|
||||
DESCRIPTION "Qt${QT_VERSION_MAJOR} WebEngineCore component."
|
||||
TYPE RECOMMENDED
|
||||
)
|
||||
else()
|
||||
set_package_properties(Qt5WebEngine PROPERTIES
|
||||
DESCRIPTION "Qt5 WebEngine component."
|
||||
set_package_properties(Qt${QT_VERSION_MAJOR}WebEngine PROPERTIES
|
||||
DESCRIPTION "Qt${QT_VERSION_MAJOR} WebEngine component."
|
||||
TYPE REQUIRED
|
||||
)
|
||||
endif()
|
||||
|
||||
if(BUILD_WITH_WEBENGINE AND Qt5WebEngine_FOUND AND Qt5WebEngineWidgets_FOUND)
|
||||
if(Qt${QT_MAJOR_VERSION}WebEngine_FOUND AND Qt${QT_MAJOR_VERSION}WebEngineWidgets_FOUND)
|
||||
add_compile_definitions(WITH_WEBENGINE=1)
|
||||
endif()
|
||||
|
||||
get_target_property (QT_QMAKE_EXECUTABLE Qt5::qmake IMPORTED_LOCATION)
|
||||
message(STATUS "Using Qt ${Qt5Core_VERSION} (${QT_QMAKE_EXECUTABLE})")
|
||||
get_target_property (QT_QMAKE_EXECUTABLE Qt::qmake IMPORTED_LOCATION)
|
||||
message(STATUS "Using Qt ${Qt${QT_MAJOR_VERSION}Core_VERSION} (${QT_QMAKE_EXECUTABLE})")
|
||||
|
||||
if(NOT TOKEN_AUTH_ONLY)
|
||||
find_package(Qt5Keychain REQUIRED)
|
||||
find_package(Qt${QT_MAJOR_VERSION}Keychain REQUIRED)
|
||||
endif()
|
||||
|
||||
# TODO: Mingw64 7.3 might also need to be excluded here as it seems to not automatically link libssp
|
||||
|
@ -106,6 +114,9 @@ if(WIN32)
|
|||
elseif(UNIX AND NOT APPLE)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,relro -Wl,-z,now")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,relro -Wl,-z,now")
|
||||
elseif(APPLE)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-ld_classic")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-ld_classic")
|
||||
endif()
|
||||
|
||||
set(QML_IMPORT_PATH ${CMAKE_SOURCE_DIR}/theme CACHE STRING "" FORCE)
|
||||
|
|
|
@ -10,13 +10,10 @@ add_library(cmdCore STATIC
|
|||
target_link_libraries(cmdCore
|
||||
PUBLIC
|
||||
Nextcloud::sync
|
||||
Qt5::Core
|
||||
Qt5::Network
|
||||
Qt::Core
|
||||
Qt::Network
|
||||
)
|
||||
|
||||
# Need tokenizer for netrc parser
|
||||
target_include_directories(cmdCore PRIVATE ${CMAKE_SOURCE_DIR}/src/3rdparty/qtokenizer)
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIE")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIE")
|
||||
|
|
|
@ -15,8 +15,7 @@
|
|||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
|
||||
#include <qtokenizer.h>
|
||||
#include <QStringTokenizer>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
|
@ -59,33 +58,32 @@ bool NetrcParser::parse()
|
|||
}
|
||||
QString content = netrc.readAll();
|
||||
|
||||
QStringTokenizer tokenizer(content, " \n\t");
|
||||
tokenizer.setQuoteCharacters("\"'");
|
||||
auto tokenizer = QStringTokenizer{content, u" \n\t"};
|
||||
|
||||
LoginPair pair;
|
||||
QString machine;
|
||||
bool isDefault = false;
|
||||
while (tokenizer.hasNext()) {
|
||||
QString key = tokenizer.next();
|
||||
for(auto itToken = tokenizer.cbegin(); itToken != tokenizer.cend(); ++itToken) {
|
||||
const auto key = *itToken;
|
||||
if (key == defaultKeyword) {
|
||||
tryAddEntryAndClear(machine, pair, isDefault);
|
||||
isDefault = true;
|
||||
continue; // don't read a value
|
||||
}
|
||||
|
||||
if (!tokenizer.hasNext()) {
|
||||
if (itToken != tokenizer.cend()) {
|
||||
qDebug() << "error fetching value for" << key;
|
||||
return false;
|
||||
}
|
||||
QString value = tokenizer.next();
|
||||
auto value = *(++itToken);
|
||||
|
||||
if (key == machineKeyword) {
|
||||
tryAddEntryAndClear(machine, pair, isDefault);
|
||||
machine = value;
|
||||
machine = value.toString();
|
||||
} else if (key == loginKeyword) {
|
||||
pair.first = value;
|
||||
pair.first = value.toString();
|
||||
} else if (key == passwordKeyword) {
|
||||
pair.second = value;
|
||||
pair.second = value.toString();
|
||||
} // ignore unsupported tokens
|
||||
}
|
||||
tryAddEntryAndClear(machine, pair, isDefault);
|
||||
|
|
|
@ -91,6 +91,21 @@ Q_LOGGING_CATEGORY(lcChecksums, "nextcloud.sync.checksums", QtInfoMsg)
|
|||
|
||||
#define BUFSIZE qint64(500 * 1024) // 500 KiB
|
||||
|
||||
static QByteArray calcCryptoHash(const QByteArray &data, QCryptographicHash::Algorithm algo)
|
||||
{
|
||||
if (data.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
QCryptographicHash crypto(algo);
|
||||
crypto.addData(data);
|
||||
return crypto.result().toHex();
|
||||
}
|
||||
|
||||
QByteArray calcSha256(const QByteArray &data)
|
||||
{
|
||||
return calcCryptoHash(data, QCryptographicHash::Sha256);
|
||||
}
|
||||
|
||||
QByteArray makeChecksumHeader(const QByteArray &checksumType, const QByteArray &checksum)
|
||||
{
|
||||
if (checksumType.isEmpty() || checksum.isEmpty())
|
||||
|
|
|
@ -56,6 +56,8 @@ OCSYNC_EXPORT QByteArray parseChecksumHeaderType(const QByteArray &header);
|
|||
/// Checks OWNCLOUD_DISABLE_CHECKSUM_UPLOAD
|
||||
OCSYNC_EXPORT bool uploadChecksumEnabled();
|
||||
|
||||
OCSYNC_EXPORT QByteArray calcSha256(const QByteArray &data);
|
||||
|
||||
/**
|
||||
* Computes the checksum of a file.
|
||||
* \ingroup libsync
|
||||
|
|
|
@ -17,4 +17,18 @@ set(common_SOURCES
|
|||
${CMAKE_CURRENT_LIST_DIR}/syncfilestatus.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
list(APPEND common_SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/utility_win.cpp
|
||||
)
|
||||
elseif(APPLE)
|
||||
list(APPEND common_SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/utility_mac.mm
|
||||
)
|
||||
elseif(UNIX AND NOT APPLE)
|
||||
list(APPEND common_SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/utility_unix.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
configure_file(${CMAKE_CURRENT_LIST_DIR}/vfspluginmetadata.json.in ${CMAKE_CURRENT_BINARY_DIR}/vfspluginmetadata.json)
|
||||
|
|
|
@ -20,12 +20,13 @@
|
|||
|
||||
#include "config.h"
|
||||
|
||||
#include "csync/ocsynclib.h"
|
||||
|
||||
#include <QString>
|
||||
#include <ctime>
|
||||
#include <QFileInfo>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
#include <csync/ocsynclib.h>
|
||||
#include <ctime>
|
||||
|
||||
class QFile;
|
||||
|
||||
|
@ -42,6 +43,10 @@ OCSYNC_EXPORT Q_DECLARE_LOGGING_CATEGORY(lcFileSystem)
|
|||
* @brief This file contains file system helper
|
||||
*/
|
||||
namespace FileSystem {
|
||||
enum class FolderPermissions {
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Mark the file as hidden (only has effects on windows)
|
||||
|
|
|
@ -107,6 +107,7 @@ public:
|
|||
GetE2EeLockedFolderQuery,
|
||||
GetE2EeLockedFoldersQuery,
|
||||
DeleteE2EeLockedFolderQuery,
|
||||
ListAllTopLevelE2eeFoldersStatusLessThanQuery,
|
||||
|
||||
PreparedQueryCount
|
||||
};
|
||||
|
|
|
@ -17,10 +17,16 @@
|
|||
*/
|
||||
|
||||
#include "remotepermissions.h"
|
||||
|
||||
#include <QVariant>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcRemotePermissions, "nextcloud.sync.remotepermissions", QtInfoMsg)
|
||||
|
||||
static const char letters[] = " WDNVCKRSMm";
|
||||
|
||||
|
||||
|
@ -68,11 +74,43 @@ RemotePermissions RemotePermissions::fromDbValue(const QByteArray &value)
|
|||
return perm;
|
||||
}
|
||||
|
||||
RemotePermissions RemotePermissions::fromServerString(const QString &value)
|
||||
template <typename T>
|
||||
RemotePermissions RemotePermissions::internalFromServerString(const QString &value,
|
||||
const T&otherProperties,
|
||||
MountedPermissionAlgorithm algorithm)
|
||||
{
|
||||
RemotePermissions perm;
|
||||
perm.fromArray(value.utf16());
|
||||
|
||||
if (algorithm == MountedPermissionAlgorithm::WildGuessMountedSubProperty) {
|
||||
return perm;
|
||||
}
|
||||
|
||||
if ((otherProperties.contains(QStringLiteral("is-mount-root")) && otherProperties.value(QStringLiteral("is-mount-root")) == QStringLiteral("false") && perm.hasPermission(RemotePermissions::IsMounted)) ||
|
||||
(!otherProperties.contains(QStringLiteral("is-mount-root")) && perm.hasPermission(RemotePermissions::IsMounted))) {
|
||||
/* All the entries in a external storage have 'M' in their permission. However, for all
|
||||
purposes in the desktop client, we only need to know about the mount points.
|
||||
So replace the 'M' by a 'm' for every sub entries in an external storage */
|
||||
perm.unsetPermission(RemotePermissions::IsMounted);
|
||||
perm.setPermission(RemotePermissions::IsMountedSub);
|
||||
qCInfo(lcRemotePermissions()) << otherProperties.value(QStringLiteral("permissions")) << "replacing M permissions by m for subfolders inside a group folder";
|
||||
}
|
||||
|
||||
return perm;
|
||||
}
|
||||
|
||||
RemotePermissions RemotePermissions::fromServerString(const QString &value,
|
||||
MountedPermissionAlgorithm algorithm,
|
||||
const QMap<QString, QString> &otherProperties)
|
||||
{
|
||||
return internalFromServerString(value, otherProperties, algorithm);
|
||||
}
|
||||
|
||||
RemotePermissions RemotePermissions::fromServerString(const QString &value,
|
||||
MountedPermissionAlgorithm algorithm,
|
||||
const QVariantMap &otherProperties)
|
||||
{
|
||||
return internalFromServerString(value, otherProperties, algorithm);
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
|
|
@ -59,6 +59,11 @@ public:
|
|||
PermissionsCount = IsMountedSub
|
||||
};
|
||||
|
||||
enum class MountedPermissionAlgorithm {
|
||||
UseMountRootProperty,
|
||||
WildGuessMountedSubProperty,
|
||||
};
|
||||
|
||||
/// null permissions
|
||||
RemotePermissions() = default;
|
||||
|
||||
|
@ -72,7 +77,14 @@ public:
|
|||
static RemotePermissions fromDbValue(const QByteArray &);
|
||||
|
||||
/// read a permissions string received from the server, never null
|
||||
static RemotePermissions fromServerString(const QString &);
|
||||
static RemotePermissions fromServerString(const QString &value,
|
||||
MountedPermissionAlgorithm algorithm = MountedPermissionAlgorithm::WildGuessMountedSubProperty,
|
||||
const QMap<QString, QString> &otherProperties = {});
|
||||
|
||||
/// read a permissions string received from the server, never null
|
||||
static RemotePermissions fromServerString(const QString &value,
|
||||
MountedPermissionAlgorithm algorithm,
|
||||
const QVariantMap &otherProperties = {});
|
||||
|
||||
[[nodiscard]] bool hasPermission(Permissions p) const
|
||||
{
|
||||
|
@ -101,6 +113,13 @@ public:
|
|||
{
|
||||
return dbg << p.toString();
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
template <typename T>
|
||||
static RemotePermissions internalFromServerString(const QString &value,
|
||||
const T&otherProperties,
|
||||
MountedPermissionAlgorithm algorithm);
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -586,7 +586,7 @@ bool SyncJournalDb::checkConnect()
|
|||
createQuery.bindValue(1, MIRALL_VERSION_MAJOR);
|
||||
createQuery.bindValue(2, MIRALL_VERSION_MINOR);
|
||||
createQuery.bindValue(3, MIRALL_VERSION_PATCH);
|
||||
createQuery.bindValue(4, MIRALL_VERSION_BUILD);
|
||||
createQuery.bindValue(4, static_cast<qulonglong>(MIRALL_VERSION_BUILD));
|
||||
if (!createQuery.exec()) {
|
||||
return sqlFail(QStringLiteral("Update version"), createQuery);
|
||||
}
|
||||
|
@ -616,7 +616,7 @@ bool SyncJournalDb::checkConnect()
|
|||
createQuery.bindValue(1, MIRALL_VERSION_MAJOR);
|
||||
createQuery.bindValue(2, MIRALL_VERSION_MINOR);
|
||||
createQuery.bindValue(3, MIRALL_VERSION_PATCH);
|
||||
createQuery.bindValue(4, MIRALL_VERSION_BUILD);
|
||||
createQuery.bindValue(4, static_cast<qulonglong>(MIRALL_VERSION_BUILD));
|
||||
createQuery.bindValue(5, major);
|
||||
createQuery.bindValue(6, minor);
|
||||
createQuery.bindValue(7, patch);
|
||||
|
@ -1030,6 +1030,108 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
|
|||
return {};
|
||||
}
|
||||
|
||||
bool SyncJournalDb::getRootE2eFolderRecord(const QString &remoteFolderPath, SyncJournalFileRecord *rec)
|
||||
{
|
||||
Q_ASSERT(rec);
|
||||
rec->_path.clear();
|
||||
Q_ASSERT(!rec->isValid());
|
||||
|
||||
Q_ASSERT(!remoteFolderPath.isEmpty());
|
||||
|
||||
Q_ASSERT(!remoteFolderPath.isEmpty() && remoteFolderPath != QStringLiteral("/"));
|
||||
if (remoteFolderPath.isEmpty() || remoteFolderPath == QStringLiteral("/")) {
|
||||
qCWarning(lcDb) << "Invalid folder path!";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto remoteFolderPathSplit = remoteFolderPath.split(QLatin1Char('/'), Qt::SkipEmptyParts);
|
||||
|
||||
if (remoteFolderPathSplit.isEmpty()) {
|
||||
qCWarning(lcDb) << "Invalid folder path!";
|
||||
return false;
|
||||
}
|
||||
|
||||
while (!remoteFolderPathSplit.isEmpty()) {
|
||||
const auto result = getFileRecord(remoteFolderPathSplit.join(QLatin1Char('/')), rec);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
if (rec->isE2eEncrypted() && rec->_e2eMangledName.isEmpty()) {
|
||||
// it's a toplevel folder record
|
||||
return true;
|
||||
}
|
||||
remoteFolderPathSplit.removeLast();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SyncJournalDb::listAllE2eeFoldersWithEncryptionStatusLessThan(const int status, const std::function<void(const SyncJournalFileRecord &)> &rowCallback)
|
||||
{
|
||||
QMutexLocker locker(&_mutex);
|
||||
|
||||
if (_metadataTableIsEmpty)
|
||||
return true;
|
||||
|
||||
if (!checkConnect())
|
||||
return false;
|
||||
const auto query = _queryManager.get(PreparedSqlQueryManager::ListAllTopLevelE2eeFoldersStatusLessThanQuery,
|
||||
QByteArrayLiteral(GET_FILE_RECORD_QUERY " WHERE type == 2 AND isE2eEncrypted >= ?1 AND isE2eEncrypted < ?2 ORDER BY path||'/' ASC"),
|
||||
_db);
|
||||
if (!query) {
|
||||
return false;
|
||||
}
|
||||
query->bindValue(1, SyncJournalFileRecord::EncryptionStatus::Encrypted);
|
||||
query->bindValue(2, status);
|
||||
|
||||
if (!query->exec())
|
||||
return false;
|
||||
|
||||
forever {
|
||||
auto next = query->next();
|
||||
if (!next.ok)
|
||||
return false;
|
||||
if (!next.hasData)
|
||||
break;
|
||||
|
||||
SyncJournalFileRecord rec;
|
||||
fillFileRecordFromGetQuery(rec, *query);
|
||||
|
||||
if (rec._type == ItemTypeSkip) {
|
||||
continue;
|
||||
}
|
||||
|
||||
rowCallback(rec);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SyncJournalDb::findEncryptedAncestorForRecord(const QString &filename, SyncJournalFileRecord *rec)
|
||||
{
|
||||
Q_ASSERT(rec);
|
||||
rec->_path.clear();
|
||||
Q_ASSERT(!rec->isValid());
|
||||
|
||||
const auto slashPosition = filename.lastIndexOf(QLatin1Char('/'));
|
||||
const auto parentPath = slashPosition >= 0 ? filename.left(slashPosition) : QString();
|
||||
|
||||
auto pathComponents = parentPath.split(QLatin1Char('/'));
|
||||
while (!pathComponents.isEmpty()) {
|
||||
const auto pathCompontentsJointed = pathComponents.join(QLatin1Char('/'));
|
||||
if (!getFileRecord(pathCompontentsJointed, rec)) {
|
||||
qCDebug(lcDb) << "could not get file from local DB" << pathCompontentsJointed;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rec->isValid() && rec->isE2eEncrypted()) {
|
||||
break;
|
||||
}
|
||||
pathComponents.removeLast();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SyncJournalDb::keyValueStoreSet(const QString &key, QVariant value)
|
||||
{
|
||||
QMutexLocker locker(&_mutex);
|
||||
|
|
|
@ -70,6 +70,9 @@ public:
|
|||
[[nodiscard]] bool getFilesBelowPath(const QByteArray &path, const std::function<void(const SyncJournalFileRecord&)> &rowCallback);
|
||||
[[nodiscard]] bool listFilesInPath(const QByteArray &path, const std::function<void(const SyncJournalFileRecord&)> &rowCallback);
|
||||
[[nodiscard]] Result<void, QString> setFileRecord(const SyncJournalFileRecord &record);
|
||||
[[nodiscard]] bool getRootE2eFolderRecord(const QString &remoteFolderPath, SyncJournalFileRecord *rec);
|
||||
[[nodiscard]] bool listAllE2eeFoldersWithEncryptionStatusLessThan(const int status, const std::function<void(const SyncJournalFileRecord &)> &rowCallback);
|
||||
[[nodiscard]] bool findEncryptedAncestorForRecord(const QString &filename, SyncJournalFileRecord *rec);
|
||||
|
||||
void keyValueStoreSet(const QString &key, QVariant value);
|
||||
[[nodiscard]] qint64 keyValueStoreGetInt(const QString &key, qint64 defaultValue);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue