Compare commits
826
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a01f361ad | ||
|
|
86dcc83461 | ||
|
|
5de8af82bb | ||
|
|
25b0695d3e | ||
|
|
a2efcafb3e | ||
|
|
6b2667d4dc | ||
|
|
1fbf87995b | ||
|
|
981378c2e3 | ||
|
|
67ca417cb8 | ||
|
|
df9a5f11ae | ||
|
|
ec0d8a0c07 | ||
|
|
bb86e5b7e5 | ||
|
|
a97efd2a5b | ||
|
|
01e170ba69 | ||
|
|
7d39332a79 | ||
|
|
19930e6252 | ||
|
|
ea74562290 | ||
|
|
74f39239c6 | ||
|
|
067ad8cf16 | ||
|
|
0c79741216 | ||
|
|
94f28430a3 | ||
|
|
83476a9e84 | ||
|
|
43a35f4878 | ||
|
|
ca2ea8cef9 | ||
|
|
63ba0a9fab | ||
|
|
4fb34fcb7c | ||
|
|
9836caf2cb | ||
|
|
65496d247f | ||
|
|
07c6e8000c | ||
|
|
4687b1f805 | ||
|
|
7adae54f87 | ||
|
|
f5baccaf08 | ||
|
|
589f885b63 | ||
|
|
52278f02f4 | ||
|
|
0d40838196 | ||
|
|
3693218fc5 | ||
|
|
d4c19f790d | ||
|
|
bc7a1754ad | ||
|
|
f95374bb3e | ||
|
|
848556200b | ||
|
|
e043c84cae | ||
|
|
0314ea1103 | ||
|
|
6a1c3d27f8 | ||
|
|
d22d8f4f25 | ||
|
|
61cd658ab0 | ||
|
|
998f631ee8 | ||
|
|
e1f6dad499 | ||
|
|
58f01da459 | ||
|
|
3895dc8e14 | ||
|
|
3ea11abb56 | ||
|
|
3f48831e8b | ||
|
|
cda401356d | ||
|
|
897c50e8e6 | ||
|
|
5218eea8ad | ||
|
|
6e5d044251 | ||
|
|
fcb936bca9 | ||
|
|
9979a820d3 | ||
|
|
1ed8f162ae | ||
|
|
31b3fed274 | ||
|
|
3758e9cdf3 | ||
|
|
eae4f0a57d | ||
|
|
00c344112e | ||
|
|
6c17d380b7 | ||
|
|
3f95941045 | ||
|
|
fef3c7f937 | ||
|
|
4212138546 | ||
|
|
61859bbb09 | ||
|
|
5906091051 | ||
|
|
51a956fd84 | ||
|
|
cd5a4abc87 | ||
|
|
2dd2b309df | ||
|
|
cf80cdbe7d | ||
|
|
74b0269230 | ||
|
|
679b068dfa | ||
|
|
aea36229fb | ||
|
|
67d97c26ec | ||
|
|
0c200ba323 | ||
|
|
be253bb01b | ||
|
|
8d25922f27 | ||
|
|
4ff0d9d42a | ||
|
|
24cc345fbb | ||
|
|
0ababcf913 | ||
|
|
c5b47abf8a | ||
|
|
7960297ea2 | ||
|
|
3cd292db5b | ||
|
|
c708fbd984 | ||
|
|
f5f960bd81 | ||
|
|
2a76b81a32 | ||
|
|
43e4c121ee | ||
|
|
6765150ba9 | ||
|
|
ab14cc9b6d | ||
|
|
64d9689be5 | ||
|
|
904224447f | ||
|
|
47be32c804 | ||
|
|
98bbdc2e4e | ||
|
|
7da3553db3 | ||
|
|
c37c7ba874 | ||
|
|
def3d7cdd5 | ||
|
|
1565f7e509 | ||
|
|
75d503b94c | ||
|
|
63833eb650 | ||
|
|
7cce8baca6 | ||
|
|
59e18ca68c | ||
|
|
030a7d61ba | ||
|
|
40ee06560e | ||
|
|
4ce9ffdf5a | ||
|
|
b3e63984bb | ||
|
|
96f399293d | ||
|
|
49b738fb3c | ||
|
|
5b93739487 | ||
|
|
38a74fd195 | ||
|
|
ea9d101622 | ||
|
|
ea80f4591b | ||
|
|
f2d729022d | ||
|
|
94d33f2c4a | ||
|
|
20768266ca | ||
|
|
1db0e731e0 | ||
|
|
4d356318f0 | ||
|
|
cd82b41cf1 | ||
|
|
3b837b4b6b | ||
|
|
189922e534 | ||
|
|
4cde54bdb3 | ||
|
|
00dc0bcca6 | ||
|
|
047bd1c379 | ||
|
|
ce0a60df67 | ||
|
|
95b7927a4c | ||
|
|
e6d15c6a72 | ||
|
|
0175ffeb3f | ||
|
|
92f22d04b0 | ||
|
|
55ad2a536b | ||
|
|
18451bd4fe | ||
|
|
b34136ce06 | ||
|
|
0f89b5a4f5 | ||
|
|
004c8b7f38 | ||
|
|
4c00dba1cf | ||
|
|
2fe9f8c295 | ||
|
|
e8a7ad7476 | ||
|
|
0601685040 | ||
|
|
adec5fa746 | ||
|
|
f6d3a85a7f | ||
|
|
0eaf9094a1 | ||
|
|
20320f81c3 | ||
|
|
cbee800509 | ||
|
|
d9baeab63e | ||
|
|
7ef55b3c46 | ||
|
|
6b5b3d2b6b | ||
|
|
2d2ad9ee43 | ||
|
|
b2140f3c30 | ||
|
|
d3108b5e6c | ||
|
|
e35c5eb5c7 | ||
|
|
a4d26ab82d | ||
|
|
d3362ba85e | ||
|
|
9bb42a2906 | ||
|
|
9c696ad96a | ||
|
|
f2ff10e3f7 | ||
|
|
0f2a0eec8e | ||
|
|
9289bb424a | ||
|
|
859323c5ca | ||
|
|
61b0a24b28 | ||
|
|
8624b672e2 | ||
|
|
163c0e4058 | ||
|
|
27b1c948e9 | ||
|
|
ea48365d2f | ||
|
|
a193906d94 | ||
|
|
fad981daee | ||
|
|
2da9f84907 | ||
|
|
9ba0620cab | ||
|
|
cc2a98bbad | ||
|
|
75c660916d | ||
|
|
d2a19d21fe | ||
|
|
6d2178dfc9 | ||
|
|
3fcbe3aba9 | ||
|
|
3630744086 | ||
|
|
f4c6679b0e | ||
|
|
aff2ecbca0 | ||
|
|
38f831ab6b | ||
|
|
8ff7cc9e16 | ||
|
|
2ef1970479 | ||
|
|
4cdbecd705 | ||
|
|
0b977c2e1c | ||
|
|
25a51b2cc8 | ||
|
|
1e9960752f | ||
|
|
22ee12e28c | ||
|
|
86aadf9d6c | ||
|
|
c8f6e39199 | ||
|
|
48e55d4827 | ||
|
|
73762e460a | ||
|
|
d04d1f3a3b | ||
|
|
9dc3c807fb | ||
|
|
78067eaa4c | ||
|
|
b2578a19ba | ||
|
|
06bb0f7496 | ||
|
|
e1565f60d6 | ||
|
|
c6910bc2fe | ||
|
|
dccb5a9826 | ||
|
|
8f1cba3770 | ||
|
|
44ffc39729 | ||
|
|
d7a888c1fd | ||
|
|
b78cf2c710 | ||
|
|
92bb1ce576 | ||
|
|
bf56f338f2 | ||
|
|
82b398ff7f | ||
|
|
6cb696d508 | ||
|
|
dffb418e60 | ||
|
|
348b94a554 | ||
|
|
60b2c99660 | ||
|
|
ddd20ae5b5 | ||
|
|
ed615379c4 | ||
|
|
09aee68bd7 | ||
|
|
7174e0e081 | ||
|
|
ebe05db662 | ||
|
|
64e5479ca0 | ||
|
|
7fdd4f215e | ||
|
|
79d81662ce | ||
|
|
330c97ab30 | ||
|
|
c1bf0e12cc | ||
|
|
a5f01265d6 | ||
|
|
9b9c278718 | ||
|
|
e7ee5dd864 | ||
|
|
d466d40cf0 | ||
|
|
0acc14144a | ||
|
|
41f66edfb9 | ||
|
|
9c41451986 | ||
|
|
fb4e2b1fc9 | ||
|
|
ea5856c4b0 | ||
|
|
cf464ef25f | ||
|
|
bded394761 | ||
|
|
ef1155743e | ||
|
|
4ff4c5950d | ||
|
|
8e1053a116 | ||
|
|
576cef2d53 | ||
|
|
5e23680b36 | ||
|
|
4e7c3840ac | ||
|
|
265b64b6f0 | ||
|
|
3bd8c92f51 | ||
|
|
a51f26c785 | ||
|
|
af753ec392 | ||
|
|
8de24af4b9 | ||
|
|
790be1b02b | ||
|
|
a093e1c9f0 | ||
|
|
3381a84dec | ||
|
|
42913d3d18 | ||
|
|
b84dfc595f | ||
|
|
b6be4e8975 | ||
|
|
25a52f41b8 | ||
|
|
18bc3314fe | ||
|
|
4c4abc9266 | ||
|
|
8cf6ad560e | ||
|
|
059f0ca636 | ||
|
|
4cb79eeab9 | ||
|
|
9f0c7f4711 | ||
|
|
cfbae15999 | ||
|
|
a7a54bcc8b | ||
|
|
1eed7e5050 | ||
|
|
d21dd91a77 | ||
|
|
c792ff1fcb | ||
|
|
ec96ecafcc | ||
|
|
b875c504a0 | ||
|
|
1bb707f0a2 | ||
|
|
28dd15e07e | ||
|
|
0fb8ec2b7c | ||
|
|
ca1ebaaf14 | ||
|
|
7f165dd6f8 | ||
|
|
abe4603639 | ||
|
|
da61fec0a0 | ||
|
|
39bffdab9c | ||
|
|
c12bb0f081 | ||
|
|
96896d3060 | ||
|
|
d158b98b35 | ||
|
|
57eb691b53 | ||
|
|
fe8f8c43f5 | ||
|
|
39f79e03ce | ||
|
|
a9c564777a | ||
|
|
876f94ef4e | ||
|
|
3bc23edfd3 | ||
|
|
88cc804d01 | ||
|
|
993403bf76 | ||
|
|
35e58ae20f | ||
|
|
27b6f754b5 | ||
|
|
4186e30912 | ||
|
|
9205f414cc | ||
|
|
d986290db8 | ||
|
|
dcae5da0ec | ||
|
|
bcc0aa8fd6 | ||
|
|
8c36953790 | ||
|
|
18bc40a531 | ||
|
|
da74f45d46 | ||
|
|
527d104ba7 | ||
|
|
7e41284cc8 | ||
|
|
071d85cd22 | ||
|
|
d0ac577686 | ||
|
|
0dd992b7a6 | ||
|
|
2f95c66360 | ||
|
|
c7254ed8be | ||
|
|
bb25dd0ea1 | ||
|
|
c565071dad | ||
|
|
b96654dd14 | ||
|
|
048406edfd | ||
|
|
62c4e80c36 | ||
|
|
b3194da23c | ||
|
|
207e2dce52 | ||
|
|
dfd2b0e8b9 | ||
|
|
248a112236 | ||
|
|
dbfc62eef5 | ||
|
|
b8b4f937d4 | ||
|
|
f79d7f8eb4 | ||
|
|
c88b31ca2f | ||
|
|
4c5cb61802 | ||
|
|
90f0d0bb37 | ||
|
|
71cfefe6fa | ||
|
|
4aa8c9ac09 | ||
|
|
6bb542ca09 | ||
|
|
f9c65bf8e1 | ||
|
|
411a67c31f | ||
|
|
02e92a0db0 | ||
|
|
9b59e6029d | ||
|
|
ac84febce9 | ||
|
|
e57ef72be1 | ||
|
|
2d605ca694 | ||
|
|
c64f732878 | ||
|
|
40f8fb503a | ||
|
|
987bd0154f | ||
|
|
8680c24329 | ||
|
|
1a2ceb6e2b | ||
|
|
8d8aff2680 | ||
|
|
397252d97a | ||
|
|
58389432d7 | ||
|
|
a55306aa27 | ||
|
|
e44bcfedaf | ||
|
|
33a2b5cc84 | ||
|
|
a855f02f63 | ||
|
|
8eafaa266d | ||
|
|
b5dc56d72c | ||
|
|
a103c960a4 | ||
|
|
98b8584efc | ||
|
|
fa076ee83f | ||
|
|
d4b90e5202 | ||
|
|
84b21c62c5 | ||
|
|
caec31dd7f | ||
|
|
f86337ffc8 | ||
|
|
70163c3aaf | ||
|
|
5b98550349 | ||
|
|
0a24886ce6 | ||
|
|
9da4e0e7e9 | ||
|
|
d35d5c9437 | ||
|
|
ac018be8b1 | ||
|
|
6fb720e0ea | ||
|
|
e736ff313f | ||
|
|
f696d774bb | ||
|
|
50dcc735c7 | ||
|
|
3c30f72cd5 | ||
|
|
874d5bbca0 | ||
|
|
acf21c7d54 | ||
|
|
b7d4c27920 | ||
|
|
555ff804fe | ||
|
|
ec900d6e9d | ||
|
|
0b27fe7642 | ||
|
|
ad8055635d | ||
|
|
f4a27fb1d3 | ||
|
|
b0b75ac8ea | ||
|
|
108900e88a | ||
|
|
439182cdfa | ||
|
|
9b0aba1339 | ||
|
|
b3024740d0 | ||
|
|
cfd2ea8ece | ||
|
|
d0adbf5026 | ||
|
|
9d6e0886f4 | ||
|
|
f1d1a26b64 | ||
|
|
15ccf29f0b | ||
|
|
ad87d5e545 | ||
|
|
322a8d144a | ||
|
|
4aa9ce1a27 | ||
|
|
364ec32a39 | ||
|
|
6444042a02 | ||
|
|
5df4f13938 | ||
|
|
4eba413631 | ||
|
|
060c14792c | ||
|
|
a43d25841d | ||
|
|
da0398c103 | ||
|
|
3b320087f3 | ||
|
|
0c85336e82 | ||
|
|
367bd1978d | ||
|
|
8a6a5a6bc1 | ||
|
|
c064f89988 | ||
|
|
c36fe14272 | ||
|
|
8a19ea8dcb | ||
|
|
d0a4b05706 | ||
|
|
38e055ed12 | ||
|
|
0924413056 | ||
|
|
09acec0d11 | ||
|
|
fa8ac48c2a | ||
|
|
da70f7b78c | ||
|
|
e0c1e6f19d | ||
|
|
b71360ec01 | ||
|
|
3807d8e6ef | ||
|
|
d7cce3c9fb | ||
|
|
5eaa3bfe44 | ||
|
|
eb078d311a | ||
|
|
462eb1cc7a | ||
|
|
8b703efb55 | ||
|
|
a08c3873b4 | ||
|
|
2ffd735e25 | ||
|
|
761772f09a | ||
|
|
de12b35216 | ||
|
|
6f8a21e547 | ||
|
|
50e3d70f70 | ||
|
|
f9a064e9aa | ||
|
|
a8507d8120 | ||
|
|
acf5b95646 | ||
|
|
cd197dc7d7 | ||
|
|
016543db40 | ||
|
|
6ed6142129 | ||
|
|
cdfee1232e | ||
|
|
25191a2443 | ||
|
|
339dd2b006 | ||
|
|
eb7a6fb9dc | ||
|
|
8b86ad2918 | ||
|
|
3d624adc09 | ||
|
|
4062a76e21 | ||
|
|
9ae5710854 | ||
|
|
494b7552ce | ||
|
|
aaecd2ae6d | ||
|
|
1e4a25c15d | ||
|
|
ea9a51ec02 | ||
|
|
4593637e23 | ||
|
|
bdae3a76d0 | ||
|
|
a370efda87 | ||
|
|
bab107ebf6 | ||
|
|
ea6dd0dc99 | ||
|
|
20b9a36604 | ||
|
|
b3b50aa8ab | ||
|
|
f749e967f1 | ||
|
|
75b5193dc4 | ||
|
|
6bc8124413 | ||
|
|
06d68e25ac | ||
|
|
19bc9a0c93 | ||
|
|
4a3f20b236 | ||
|
|
82400407b9 | ||
|
|
d49384b98b | ||
|
|
3c1072db4a | ||
|
|
41fde6f1ad | ||
|
|
620731b464 | ||
|
|
558cecea76 | ||
|
|
ff81bf1fac | ||
|
|
71717a5dbb | ||
|
|
9155a3cbf2 | ||
|
|
f98d629a78 | ||
|
|
55e04cd2cd | ||
|
|
f6d8183001 | ||
|
|
b9f4ddd480 | ||
|
|
0d9360490d | ||
|
|
8854bbbe95 | ||
|
|
b617cdc690 | ||
|
|
62d26fbafd | ||
|
|
6c38474c40 | ||
|
|
fed41ba3aa | ||
|
|
181d74a139 | ||
|
|
72d83ca171 | ||
|
|
020b1f1d71 | ||
|
|
11e57a43f7 | ||
|
|
f9647c0691 | ||
|
|
92bdf7d57b | ||
|
|
d5ef4f9313 | ||
|
|
925005a9de | ||
|
|
354ee3b6c7 | ||
|
|
9f080b4533 | ||
|
|
16b7181ebc | ||
|
|
7c1d449743 | ||
|
|
9bbb1a02e5 | ||
|
|
37035bfacd | ||
|
|
434f149e04 | ||
|
|
25f30cacc2 | ||
|
|
8f862fd1a1 | ||
|
|
5904815908 | ||
|
|
36b80c25d4 | ||
|
|
994ffe3d5f | ||
|
|
08b8ecba32 | ||
|
|
675885d875 | ||
|
|
f0b8e5ab64 | ||
|
|
9a202207b4 | ||
|
|
f4027b433d | ||
|
|
ab4fee8229 | ||
|
|
1920a190bc | ||
|
|
b9db1881ca | ||
|
|
9c1c1e609d | ||
|
|
7a2d366bdd | ||
|
|
6497e461bd | ||
|
|
6a6b750b4e | ||
|
|
7b5c51a198 | ||
|
|
600ae38c0f | ||
|
|
f5b9275192 | ||
|
|
c33e944ec4 | ||
|
|
8263bbfde2 | ||
|
|
305cd2d795 | ||
|
|
93bf6d4dc4 | ||
|
|
3546fabdfa | ||
|
|
f507e4cabd | ||
|
|
9992b1fa7e | ||
|
|
e419e59f1d | ||
|
|
a3d5a56077 | ||
|
|
7bd593f0e5 | ||
|
|
63d353a078 | ||
|
|
bf7a8b0854 | ||
|
|
09f28cf829 | ||
|
|
58f0cf2916 | ||
|
|
d6a45cced0 | ||
|
|
8cc44c4a79 | ||
|
|
5b93c78ebd | ||
|
|
2085e3ae40 | ||
|
|
3886436841 | ||
|
|
40ce73db68 | ||
|
|
27b3213bac | ||
|
|
829cfb3054 | ||
|
|
3a3d21cd01 | ||
|
|
aa47d8ecf8 | ||
|
|
e8353897eb | ||
|
|
58f9e623a4 | ||
|
|
d871c9b061 | ||
|
|
4da28bcd86 | ||
|
|
f21f05585c | ||
|
|
a606e5f79e | ||
|
|
15bf4c7018 | ||
|
|
efa3acc368 | ||
|
|
64e0ba3b77 | ||
|
|
c9ded8956c | ||
|
|
6cb6b91bd4 | ||
|
|
6470454221 | ||
|
|
a3814f644c | ||
|
|
a89d6dc468 | ||
|
|
ad8f90d12e | ||
|
|
95cb91ffad | ||
|
|
04fd072ae9 | ||
|
|
6201951e3b | ||
|
|
9f1735110c | ||
|
|
440d63ade0 | ||
|
|
09e3eb3e54 | ||
|
|
22074a5fbd | ||
|
|
5a17320c44 | ||
|
|
7548082355 | ||
|
|
36181b0ef8 | ||
|
|
d478d973fd | ||
|
|
d80986e6d0 | ||
|
|
f929bf280f | ||
|
|
0013f64c61 | ||
|
|
a2d2c61f1e | ||
|
|
700e04cc14 | ||
|
|
baa275b05b | ||
|
|
e6d6cae35c | ||
|
|
3facea9a06 | ||
|
|
a1aacbffed | ||
|
|
401402021a | ||
|
|
e7602a164a | ||
|
|
980357509d | ||
|
|
ad021e5758 | ||
|
|
5e2a6778f0 | ||
|
|
296d9a86d0 | ||
|
|
85d83ed9ba | ||
|
|
d2b64fb272 | ||
|
|
8970730ec9 | ||
|
|
bc9ffc1a79 | ||
|
|
46f562e91d | ||
|
|
3695a29b32 | ||
|
|
03eb8834c1 | ||
|
|
79f5fc6908 | ||
|
|
526ddf12d5 | ||
|
|
d3acc71d95 | ||
|
|
3438770f30 | ||
|
|
46d5d464b6 | ||
|
|
01438d630d | ||
|
|
c3d8273caa | ||
|
|
2999f2dcdd | ||
|
|
1676d02d80 | ||
|
|
65e7d0a2cd | ||
|
|
afc9141a7c | ||
|
|
fd8ba951dc | ||
|
|
6374ef8d1b | ||
|
|
9f16e7c272 | ||
|
|
b59cc28dc5 | ||
|
|
9930db8dc7 | ||
|
|
9f902bfbe9 | ||
|
|
5729a32959 | ||
|
|
86616b6936 | ||
|
|
d7a2a12129 | ||
|
|
274573df28 | ||
|
|
20234269e9 | ||
|
|
edc9a14ef5 | ||
|
|
7e6c72a198 | ||
|
|
48ae45550d | ||
|
|
aaaab9deca | ||
|
|
53d10dc597 | ||
|
|
86ef6d2dd6 | ||
|
|
beccba6d95 | ||
|
|
c01441460b | ||
|
|
68ed132e61 | ||
|
|
b9c9424103 | ||
|
|
82eda9a092 | ||
|
|
8e6237e885 | ||
|
|
e99650615d | ||
|
|
58b4dc760e | ||
|
|
131f4a6968 | ||
|
|
533880e9f1 | ||
|
|
9be23b4ff7 | ||
|
|
53af9c6c1e | ||
|
|
51af8c223d | ||
|
|
dbc951a9d0 | ||
|
|
ac3a50a31c | ||
|
|
c111257f63 | ||
|
|
67f0b82ae4 | ||
|
|
0b97aaa302 | ||
|
|
227e7d47ab | ||
|
|
9d73e1d5c1 | ||
|
|
632d73698f | ||
|
|
674e784318 | ||
|
|
d77d8dbdd1 | ||
|
|
58812a568f | ||
|
|
e768e83507 | ||
|
|
dc69e93064 | ||
|
|
832d0d5bfa | ||
|
|
04184b1582 | ||
|
|
70dabf70dd | ||
|
|
f420ac144c | ||
|
|
1bc9242644 | ||
|
|
eb36eed4c0 | ||
|
|
91aacdd955 | ||
|
|
a48e8f357a | ||
|
|
34ef43786c | ||
|
|
fbdf820734 | ||
|
|
aecb95bc92 | ||
|
|
f900cbff0b | ||
|
|
206b695e62 | ||
|
|
c94a0c94f3 | ||
|
|
5599277bf2 | ||
|
|
7a62b348cd | ||
|
|
a907cde399 | ||
|
|
77bbb64feb | ||
|
|
b5aa0b135f | ||
|
|
9976a84d42 | ||
|
|
018406d25c | ||
|
|
639cc302d2 | ||
|
|
3cbdf798a2 | ||
|
|
74ebcc5616 | ||
|
|
0685f401f3 | ||
|
|
235c179d60 | ||
|
|
caa725dc20 | ||
|
|
feff4ad890 | ||
|
|
bee067ad76 | ||
|
|
2e47b23827 | ||
|
|
88bf93a2cc | ||
|
|
3f284889ac | ||
|
|
698acad3bd | ||
|
|
20f462e603 | ||
|
|
11cb1e1f31 | ||
|
|
5b1228e14d | ||
|
|
de1a5baaca | ||
|
|
3ca8814e46 | ||
|
|
7afde45ac6 | ||
|
|
357e889dad | ||
|
|
024f316b3a | ||
|
|
2e1c2cbc6e | ||
|
|
88832596da | ||
|
|
1375854237 | ||
|
|
c7fdd81691 | ||
|
|
276d935cee | ||
|
|
9905b2608b | ||
|
|
06f80f0646 | ||
|
|
cde907e15a | ||
|
|
94c95b44ec | ||
|
|
b8e3ec8b7b | ||
|
|
9a1dc89a99 | ||
|
|
90cee00319 | ||
|
|
1ad4eba17e | ||
|
|
b87c38e232 | ||
|
|
b602c08603 | ||
|
|
b18514d9fe | ||
|
|
e2dffad962 | ||
|
|
3957fa3be6 | ||
|
|
535358b057 | ||
|
|
9f4dbf0002 | ||
|
|
10a5677241 | ||
|
|
f2cedfd08e | ||
|
|
13236d8ebc | ||
|
|
d19f0534e4 | ||
|
|
f5d79a5469 | ||
|
|
88c58237c1 | ||
|
|
03d8c187ed | ||
|
|
d3ed644b9c | ||
|
|
6bfda6743e | ||
|
|
8cf5acd183 | ||
|
|
b0432487cc | ||
|
|
e5fe1b5a92 | ||
|
|
1f403291cc | ||
|
|
edaad85990 | ||
|
|
f4aeecae84 | ||
|
|
9142f28d55 | ||
|
|
23deea0c37 | ||
|
|
cc2d6194a9 | ||
|
|
340528f5af | ||
|
|
66067a3506 | ||
|
|
eed3c518cd | ||
|
|
88b0e8cd7e | ||
|
|
028cb99586 | ||
|
|
37eacb5d61 | ||
|
|
e7956901de | ||
|
|
0e3d7cc8ca | ||
|
|
103c8a4aef | ||
|
|
b7190b6a74 | ||
|
|
ddf976e240 | ||
|
|
d23cc0a239 | ||
|
|
ff5a0d6a45 | ||
|
|
6a1e7d9a20 | ||
|
|
7eb965977a | ||
|
|
48627c0901 | ||
|
|
fbaf96eb38 | ||
|
|
31517db9bf | ||
|
|
d0ecf74836 | ||
|
|
89fb94420a | ||
|
|
3e81762f27 | ||
|
|
06a362225f | ||
|
|
62c5d8e3b9 | ||
|
|
4724fab97e | ||
|
|
b7e94f2b52 | ||
|
|
ad99bbb7e8 | ||
|
|
7a1ddf9c83 | ||
|
|
cc134da1ed | ||
|
|
e6be9ea84e | ||
|
|
718df194fe | ||
|
|
26eb52adf1 | ||
|
|
f5ec49b375 | ||
|
|
9aaa80dcc1 | ||
|
|
3463b8d374 | ||
|
|
16b3787258 | ||
|
|
269f907d1d | ||
|
|
5f2144db0f | ||
|
|
693c159360 | ||
|
|
7a6c42df62 | ||
|
|
9f984054b6 | ||
|
|
1400e57cf7 | ||
|
|
7f4b18104f | ||
|
|
d9452509b5 | ||
|
|
d120da8000 | ||
|
|
bc5ffe1828 | ||
|
|
5211267cd2 | ||
|
|
67e0956042 | ||
|
|
0467933ae9 | ||
|
|
48af2866d2 | ||
|
|
d3922d812a | ||
|
|
2893c4b4ad | ||
|
|
b8dcac03cc | ||
|
|
5f8dc32ed6 | ||
|
|
293ada96c8 | ||
|
|
6c4c4f1e9d | ||
|
|
cbfaf7fd91 | ||
|
|
abc6f68062 | ||
|
|
1331123fb4 | ||
|
|
5f92bc3d8d | ||
|
|
c8c6b63cd2 | ||
|
|
8552b676cb | ||
|
|
fe90278b8e | ||
|
|
37e6722ffb | ||
|
|
7b27fca2ad | ||
|
|
f9919bd72c | ||
|
|
3a72587a9e | ||
|
|
71d88cdcba | ||
|
|
d439b804fe | ||
|
|
84c2ee1e1c | ||
|
|
fecae2228d | ||
|
|
2580b7693c | ||
|
|
a62e713219 | ||
|
|
410780d051 | ||
|
|
bb910e20ac | ||
|
|
31a735ca25 | ||
|
|
3d069dbb1c | ||
|
|
5b3f594b2b | ||
|
|
3643719161 | ||
|
|
d743317069 | ||
|
|
f274181270 | ||
|
|
c90af47f02 | ||
|
|
7496c79ae5 | ||
|
|
11d3d93693 | ||
|
|
277d40761b | ||
|
|
27ec72cdb9 | ||
|
|
e17e8e15c7 | ||
|
|
4b6ec6f310 | ||
|
|
04c55cd602 | ||
|
|
c242ef49a6 | ||
|
|
4502ca7bf4 | ||
|
|
9c72f05fbd | ||
|
|
1fb82a6d7f | ||
|
|
9242f78b88 | ||
|
|
5b9d403b7d | ||
|
|
cc802ebc69 | ||
|
|
0129c97c83 | ||
|
|
fc01b705a5 | ||
|
|
126d080c63 | ||
|
|
d5614eb5ee | ||
|
|
c07b26d568 | ||
|
|
11fcb088e5 | ||
|
|
f889aa1b71 | ||
|
|
ee123efd7d | ||
|
|
94f53aabe1 | ||
|
|
9fe502057e | ||
|
|
17c556607a | ||
|
|
a18938d0b1 | ||
|
|
d08fb78357 | ||
|
|
906d2b6614 | ||
|
|
a09e7ca1cb | ||
|
|
3ad690d624 | ||
|
|
db06096169 | ||
|
|
99d67d531b | ||
|
|
4e8585bf9a | ||
|
|
376e5ba119 | ||
|
|
3395f35b59 | ||
|
|
102b33c3dc | ||
|
|
aa4222588a | ||
|
|
15deb66dff | ||
|
|
4a5da29cfe | ||
|
|
18963546ef | ||
|
|
3404626dd6 | ||
|
|
e970fa1d4d | ||
|
|
e5b3b7a3c0 | ||
|
|
64eb6581e3 | ||
|
|
dc9f6f3917 | ||
|
|
3c2cd5d936 | ||
|
|
6e98744aab | ||
|
|
4beb0d2902 | ||
|
|
9c62ec1d2c |
No files matched your search
@@ -10,5 +10,5 @@ updates:
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
- dependency-name: "github.com/anyproto/go-chash"
|
||||
- dependency-name: "github.com/anytypeio/go-chash"
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOPRIVATE: github.com/anyproto
|
||||
GOPRIVATE: github.com/anytypeio
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v3
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Any Association
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
||||
OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,5 +1,5 @@
|
||||
.PHONY: proto test deps
|
||||
export GOPRIVATE=github.com/anyproto
|
||||
export GOPRIVATE=github.com/anytypeio
|
||||
export PATH:=deps:$(PATH)
|
||||
|
||||
proto:
|
||||
@@ -7,20 +7,17 @@ proto:
|
||||
|
||||
@$(eval P_ACL_RECORDS_PATH_PB := commonspace/object/acl/aclrecordproto)
|
||||
@$(eval P_TREE_CHANGES_PATH_PB := commonspace/object/tree/treechangeproto)
|
||||
@$(eval P_CRYPTO_PATH_PB := util/crypto/cryptoproto)
|
||||
@$(eval P_ACL_RECORDS := M$(P_ACL_RECORDS_PATH_PB)/protos/aclrecord.proto=github.com/anyproto/any-sync/$(P_ACL_RECORDS_PATH_PB))
|
||||
@$(eval P_TREE_CHANGES := M$(P_TREE_CHANGES_PATH_PB)/protos/treechange.proto=github.com/anyproto/any-sync/$(P_TREE_CHANGES_PATH_PB))
|
||||
@$(eval P_ACL_RECORDS := M$(P_ACL_RECORDS_PATH_PB)/protos/aclrecord.proto=github.com/anytypeio/any-sync/$(P_ACL_RECORDS_PATH_PB))
|
||||
@$(eval P_TREE_CHANGES := M$(P_TREE_CHANGES_PATH_PB)/protos/treechange.proto=github.com/anytypeio/any-sync/$(P_TREE_CHANGES_PATH_PB))
|
||||
|
||||
protoc --gogofaster_out=:. $(P_ACL_RECORDS_PATH_PB)/protos/*.proto
|
||||
protoc --gogofaster_out=:. $(P_TREE_CHANGES_PATH_PB)/protos/*.proto
|
||||
protoc --gogofaster_out=:. $(P_CRYPTO_PATH_PB)/protos/*.proto
|
||||
$(eval PKGMAP := $$(P_TREE_CHANGES),$$(P_ACL_RECORDS))
|
||||
protoc --gogofaster_out=$(PKGMAP):. --go-drpc_out=protolib=github.com/gogo/protobuf:. commonspace/spacesyncproto/protos/*.proto
|
||||
protoc --gogofaster_out=$(PKGMAP):. --go-drpc_out=protolib=github.com/gogo/protobuf:. commonfile/fileproto/protos/*.proto
|
||||
protoc --gogofaster_out=$(PKGMAP):. --go-drpc_out=protolib=github.com/gogo/protobuf:. net/streampool/testservice/protos/*.proto
|
||||
protoc --gogofaster_out=:. net/secureservice/handshake/handshakeproto/protos/*.proto
|
||||
protoc --gogofaster_out=$(PKGMAP):. --go-drpc_out=protolib=github.com/gogo/protobuf:. coordinator/coordinatorproto/protos/*.proto
|
||||
protoc --gogofaster_out=:. --go-drpc_out=protolib=github.com/gogo/protobuf:. consensus/consensusproto/protos/*.proto
|
||||
|
||||
deps:
|
||||
go mod download
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
//go:generate mockgen -destination mock_accountservice/mock_accountservice.go github.com/anyproto/any-sync/accountservice Service
|
||||
//go:generate mockgen -destination mock_accountservice/mock_accountservice.go github.com/anytypeio/any-sync/accountservice Service
|
||||
package accountservice
|
||||
|
||||
import (
|
||||
"github.com/anyproto/any-sync/app"
|
||||
"github.com/anyproto/any-sync/commonspace/object/accountdata"
|
||||
"github.com/anytypeio/any-sync/app"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/accountdata"
|
||||
)
|
||||
|
||||
const CName = "common.accountservice"
|
||||
|
||||
type Service interface {
|
||||
app.Component
|
||||
Account() *accountdata.AccountKeys
|
||||
Account() *accountdata.AccountData
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
PeerId string `yaml:"peerId"`
|
||||
PeerKey string `yaml:"peerKey"`
|
||||
SigningKey string `yaml:"signingKey"`
|
||||
PeerId string `yaml:"peerId"`
|
||||
PeerKey string `yaml:"peerKey"`
|
||||
SigningKey string `yaml:"signingKey"`
|
||||
EncryptionKey string `yaml:"encryptionKey"`
|
||||
}
|
||||
|
||||
type ConfigGetter interface {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package mock_accountservice
|
||||
|
||||
import (
|
||||
"github.com/anyproto/any-sync/accountservice"
|
||||
"github.com/anyproto/any-sync/commonspace/object/accountdata"
|
||||
"go.uber.org/mock/gomock"
|
||||
"github.com/anytypeio/any-sync/accountservice"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/accountdata"
|
||||
"github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
func NewAccountServiceWithAccount(ctrl *gomock.Controller, acc *accountdata.AccountKeys) *MockService {
|
||||
func NewAccountServiceWithAccount(ctrl *gomock.Controller, acc *accountdata.AccountData) *MockService {
|
||||
mock := NewMockService(ctrl)
|
||||
mock.EXPECT().Name().Return(accountservice.CName).AnyTimes()
|
||||
mock.EXPECT().Init(gomock.Any()).AnyTimes()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/anyproto/any-sync/accountservice (interfaces: Service)
|
||||
// Source: github.com/anytypeio/any-sync/accountservice (interfaces: Service)
|
||||
|
||||
// Package mock_accountservice is a generated GoMock package.
|
||||
package mock_accountservice
|
||||
@@ -7,9 +7,9 @@ package mock_accountservice
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
app "github.com/anyproto/any-sync/app"
|
||||
accountdata "github.com/anyproto/any-sync/commonspace/object/accountdata"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
app "github.com/anytypeio/any-sync/app"
|
||||
accountdata "github.com/anytypeio/any-sync/commonspace/object/accountdata"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockService is a mock of Service interface.
|
||||
@@ -36,10 +36,10 @@ func (m *MockService) EXPECT() *MockServiceMockRecorder {
|
||||
}
|
||||
|
||||
// Account mocks base method.
|
||||
func (m *MockService) Account() *accountdata.AccountKeys {
|
||||
func (m *MockService) Account() *accountdata.AccountData {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Account")
|
||||
ret0, _ := ret[0].(*accountdata.AccountKeys)
|
||||
ret0, _ := ret[0].(*accountdata.AccountData)
|
||||
return ret0
|
||||
}
|
||||
|
||||
|
||||
+26
-156
@@ -4,11 +4,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/anyproto/any-sync/app/logger"
|
||||
"github.com/anytypeio/any-sync/app/logger"
|
||||
"go.uber.org/zap"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -16,15 +15,12 @@ import (
|
||||
|
||||
var (
|
||||
// values of this vars will be defined while compilation
|
||||
AppName, GitCommit, GitBranch, GitState, GitSummary, BuildDate string
|
||||
name string
|
||||
GitCommit, GitBranch, GitState, GitSummary, BuildDate string
|
||||
name string
|
||||
)
|
||||
|
||||
var (
|
||||
log = logger.NewNamed("app")
|
||||
StopDeadline = time.Minute
|
||||
StopWarningAfter = time.Second * 10
|
||||
StartWarningAfter = time.Second * 10
|
||||
log = logger.NewNamed("app")
|
||||
)
|
||||
|
||||
// Component is a minimal interface for a common app.Component
|
||||
@@ -55,14 +51,10 @@ type ComponentStatable interface {
|
||||
// App is the central part of the application
|
||||
// It contains and manages all components
|
||||
type App struct {
|
||||
parent *App
|
||||
components []Component
|
||||
mu sync.RWMutex
|
||||
startStat Stat
|
||||
stopStat Stat
|
||||
deviceState int
|
||||
versionName string
|
||||
anySyncVersion string
|
||||
components []Component
|
||||
mu sync.RWMutex
|
||||
startStat StartStat
|
||||
deviceState int
|
||||
}
|
||||
|
||||
// Name returns app name
|
||||
@@ -70,47 +62,23 @@ func (app *App) Name() string {
|
||||
return name
|
||||
}
|
||||
|
||||
func (app *App) AppName() string {
|
||||
return AppName
|
||||
}
|
||||
|
||||
// Version return app version
|
||||
func (app *App) Version() string {
|
||||
return GitSummary
|
||||
}
|
||||
|
||||
// SetVersionName sets the custom application version
|
||||
func (app *App) SetVersionName(v string) {
|
||||
app.versionName = v
|
||||
}
|
||||
|
||||
// VersionName returns a string with the settled app version or auto-generated version if it didn't set
|
||||
func (app *App) VersionName() string {
|
||||
if app.versionName != "" {
|
||||
return app.versionName
|
||||
}
|
||||
return AppName + ":" + GitSummary + "/any-sync:" + app.AnySyncVersion()
|
||||
}
|
||||
|
||||
type Stat struct {
|
||||
type StartStat struct {
|
||||
SpentMsPerComp map[string]int64
|
||||
SpentMsTotal int64
|
||||
}
|
||||
|
||||
// StartStat returns total time spent per comp for the last Start
|
||||
func (app *App) StartStat() Stat {
|
||||
// StartStat returns total time spent per comp
|
||||
func (app *App) StartStat() StartStat {
|
||||
app.mu.Lock()
|
||||
defer app.mu.Unlock()
|
||||
return app.startStat
|
||||
}
|
||||
|
||||
// StopStat returns total time spent per comp for the last Close
|
||||
func (app *App) StopStat() Stat {
|
||||
app.mu.Lock()
|
||||
defer app.mu.Unlock()
|
||||
return app.stopStat
|
||||
}
|
||||
|
||||
// VersionDescription return the full info about the build
|
||||
func (app *App) VersionDescription() string {
|
||||
return VersionDescription()
|
||||
@@ -124,16 +92,6 @@ func VersionDescription() string {
|
||||
return fmt.Sprintf("build on %s from %s at #%s(%s)", BuildDate, GitBranch, GitCommit, GitState)
|
||||
}
|
||||
|
||||
// ChildApp creates a child container which has access to parent's components
|
||||
// It doesn't call Start on any of the parent's components
|
||||
func (app *App) ChildApp() *App {
|
||||
return &App{
|
||||
parent: app,
|
||||
deviceState: app.deviceState,
|
||||
anySyncVersion: app.AnySyncVersion(),
|
||||
}
|
||||
}
|
||||
|
||||
// Register adds service to registry
|
||||
// All components will be started in the order they were registered
|
||||
func (app *App) Register(s Component) *App {
|
||||
@@ -153,14 +111,10 @@ func (app *App) Register(s Component) *App {
|
||||
func (app *App) Component(name string) Component {
|
||||
app.mu.RLock()
|
||||
defer app.mu.RUnlock()
|
||||
current := app
|
||||
for current != nil {
|
||||
for _, s := range current.components {
|
||||
if s.Name() == name {
|
||||
return s
|
||||
}
|
||||
for _, s := range app.components {
|
||||
if s.Name() == name {
|
||||
return s
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -178,14 +132,10 @@ func (app *App) MustComponent(name string) Component {
|
||||
func MustComponent[i any](app *App) i {
|
||||
app.mu.RLock()
|
||||
defer app.mu.RUnlock()
|
||||
current := app
|
||||
for current != nil {
|
||||
for _, s := range current.components {
|
||||
if v, ok := s.(i); ok {
|
||||
return v
|
||||
}
|
||||
for _, s := range app.components {
|
||||
if v, ok := s.(i); ok {
|
||||
return v
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
empty := new(i)
|
||||
panic(fmt.Errorf("component with interface %T is not found", empty))
|
||||
@@ -195,13 +145,9 @@ func MustComponent[i any](app *App) i {
|
||||
func (app *App) ComponentNames() (names []string) {
|
||||
app.mu.RLock()
|
||||
defer app.mu.RUnlock()
|
||||
names = make([]string, 0, len(app.components))
|
||||
current := app
|
||||
for current != nil {
|
||||
for _, c := range current.components {
|
||||
names = append(names, c.Name())
|
||||
}
|
||||
current = current.parent
|
||||
names = make([]string, len(app.components))
|
||||
for i, c := range app.components {
|
||||
names[i] = c.Name()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -212,17 +158,7 @@ func (app *App) Start(ctx context.Context) (err error) {
|
||||
app.mu.RLock()
|
||||
defer app.mu.RUnlock()
|
||||
app.startStat.SpentMsPerComp = make(map[string]int64)
|
||||
var currentComponentStarting string
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-time.After(StartWarningAfter):
|
||||
l := statLogger(app.stopStat, log).With(zap.String("in_progress", currentComponentStarting))
|
||||
l.Warn("components start in progress")
|
||||
}
|
||||
}()
|
||||
|
||||
closeServices := func(idx int) {
|
||||
for i := idx; i >= 0; i-- {
|
||||
if serviceClose, ok := app.components[i].(ComponentRunnable); ok {
|
||||
@@ -236,7 +172,7 @@ func (app *App) Start(ctx context.Context) (err error) {
|
||||
for i, s := range app.components {
|
||||
if err = s.Init(app); err != nil {
|
||||
closeServices(i)
|
||||
return fmt.Errorf("can't init service '%s': %w", s.Name(), err)
|
||||
return fmt.Errorf("can't init service '%s': %v", s.Name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,32 +181,17 @@ func (app *App) Start(ctx context.Context) (err error) {
|
||||
start := time.Now()
|
||||
if err = serviceRun.Run(ctx); err != nil {
|
||||
closeServices(i)
|
||||
return fmt.Errorf("can't run service '%s': %w", serviceRun.Name(), err)
|
||||
return fmt.Errorf("can't run service '%s': %v", serviceRun.Name(), err)
|
||||
}
|
||||
spent := time.Since(start).Milliseconds()
|
||||
app.startStat.SpentMsTotal += spent
|
||||
app.startStat.SpentMsPerComp[s.Name()] = spent
|
||||
}
|
||||
}
|
||||
|
||||
close(done)
|
||||
l := statLogger(app.stopStat, log)
|
||||
if app.startStat.SpentMsTotal > StartWarningAfter.Milliseconds() {
|
||||
l.Warn("all components started")
|
||||
}
|
||||
l.Debug("all components started")
|
||||
log.Debug("all components started")
|
||||
return
|
||||
}
|
||||
|
||||
// IterateComponents iterates over all registered components. It's safe for concurrent use.
|
||||
func (app *App) IterateComponents(fn func(Component)) {
|
||||
app.mu.RLock()
|
||||
defer app.mu.RUnlock()
|
||||
for _, s := range app.components {
|
||||
fn(s)
|
||||
}
|
||||
}
|
||||
|
||||
func stackAllGoroutines() []byte {
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
@@ -282,41 +203,18 @@ func stackAllGoroutines() []byte {
|
||||
}
|
||||
}
|
||||
|
||||
func statLogger(stat Stat, ctxLogger logger.CtxLogger) logger.CtxLogger {
|
||||
l := ctxLogger
|
||||
for k, v := range stat.SpentMsPerComp {
|
||||
l = l.With(zap.Int64(k, v))
|
||||
}
|
||||
l = l.With(zap.Int64("total", stat.SpentMsTotal))
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// Close stops the application
|
||||
// All components with ComponentRunnable implementation will be closed in the reversed order
|
||||
func (app *App) Close(ctx context.Context) error {
|
||||
log.Debug("close components...")
|
||||
app.mu.RLock()
|
||||
defer app.mu.RUnlock()
|
||||
app.stopStat.SpentMsPerComp = make(map[string]int64)
|
||||
var currentComponentStopping string
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-time.After(StopWarningAfter):
|
||||
statLogger(app.stopStat, log).
|
||||
With(zap.String("in_progress", currentComponentStopping)).
|
||||
Warn("components close in progress")
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-time.After(StopDeadline):
|
||||
case <-time.After(time.Minute):
|
||||
_, _ = os.Stderr.Write([]byte("app.Close timeout\n"))
|
||||
_, _ = os.Stderr.Write(stackAllGoroutines())
|
||||
panic("app.Close timeout")
|
||||
@@ -326,27 +224,16 @@ func (app *App) Close(ctx context.Context) error {
|
||||
var errs []string
|
||||
for i := len(app.components) - 1; i >= 0; i-- {
|
||||
if serviceClose, ok := app.components[i].(ComponentRunnable); ok {
|
||||
start := time.Now()
|
||||
currentComponentStopping = app.components[i].Name()
|
||||
if e := serviceClose.Close(ctx); e != nil {
|
||||
errs = append(errs, fmt.Sprintf("Component '%s' close error: %v", serviceClose.Name(), e))
|
||||
}
|
||||
spent := time.Since(start).Milliseconds()
|
||||
app.stopStat.SpentMsTotal += spent
|
||||
app.stopStat.SpentMsPerComp[app.components[i].Name()] = spent
|
||||
}
|
||||
}
|
||||
close(done)
|
||||
if len(errs) > 0 {
|
||||
return errors.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
|
||||
l := statLogger(app.stopStat, log)
|
||||
if app.stopStat.SpentMsTotal > StopWarningAfter.Milliseconds() {
|
||||
l.Warn("all components have been closed")
|
||||
}
|
||||
|
||||
l.Debug("all components have been closed")
|
||||
log.Debug("all components have been closed")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -363,20 +250,3 @@ func (app *App) SetDeviceState(state int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var onceVersion sync.Once
|
||||
|
||||
func (app *App) AnySyncVersion() string {
|
||||
onceVersion.Do(func() {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if ok {
|
||||
for _, mod := range info.Deps {
|
||||
if mod.Path == "github.com/anyproto/any-sync" {
|
||||
app.anySyncVersion = mod.Version
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return app.anySyncVersion
|
||||
}
|
||||
@@ -34,40 +34,6 @@ func TestAppServiceRegistry(t *testing.T) {
|
||||
names := app.ComponentNames()
|
||||
assert.Equal(t, names, []string{"c1", "r1", "s1"})
|
||||
})
|
||||
t.Run("Child MustComponent", func(t *testing.T) {
|
||||
app := app.ChildApp()
|
||||
app.Register(newTestService(testTypeComponent, "x1", nil, nil))
|
||||
for _, name := range []string{"c1", "r1", "s1", "x1"} {
|
||||
assert.NotPanics(t, func() { app.MustComponent(name) }, name)
|
||||
}
|
||||
assert.Panics(t, func() { app.MustComponent("not-registered") })
|
||||
})
|
||||
t.Run("Child ComponentNames", func(t *testing.T) {
|
||||
app := app.ChildApp()
|
||||
app.Register(newTestService(testTypeComponent, "x1", nil, nil))
|
||||
names := app.ComponentNames()
|
||||
assert.Equal(t, names, []string{"x1", "c1", "r1", "s1"})
|
||||
})
|
||||
t.Run("Child override", func(t *testing.T) {
|
||||
app := app.ChildApp()
|
||||
app.Register(newTestService(testTypeRunnable, "s1", nil, nil))
|
||||
_ = app.MustComponent("s1").(*testRunnable)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApp_IterateComponents(t *testing.T) {
|
||||
app := new(App)
|
||||
|
||||
app.Register(newTestService(testTypeRunnable, "c1", nil, nil))
|
||||
app.Register(newTestService(testTypeRunnable, "r1", nil, nil))
|
||||
app.Register(newTestService(testTypeComponent, "s1", nil, nil))
|
||||
|
||||
var got []string
|
||||
app.IterateComponents(func(s Component) {
|
||||
got = append(got, s.Name())
|
||||
})
|
||||
|
||||
assert.ElementsMatch(t, []string{"c1", "r1", "s1"}, got)
|
||||
}
|
||||
|
||||
func TestAppStart(t *testing.T) {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
// Package ldiff provides a container of elements with fixed id and changeable content.
|
||||
// Diff can calculate the difference with another diff container (you can make it remote) with minimum hops and traffic.
|
||||
//
|
||||
//go:generate mockgen -destination mock_ldiff/mock_ldiff.go github.com/anyproto/any-sync/app/ldiff Diff,Remote
|
||||
//go:generate mockgen -destination mock_ldiff/mock_ldiff.go github.com/anytypeio/any-sync/app/ldiff Diff,Remote
|
||||
package ldiff
|
||||
|
||||
import (
|
||||
|
||||
@@ -3,9 +3,9 @@ package ldiff
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
"math"
|
||||
"sort"
|
||||
"testing"
|
||||
@@ -44,7 +44,7 @@ func TestDiff_Diff(t *testing.T) {
|
||||
d2 := New(16, 16)
|
||||
for i := 0; i < 1000; i++ {
|
||||
id := fmt.Sprint(i)
|
||||
head := uuid.NewString()
|
||||
head := bson.NewObjectId().Hex()
|
||||
d1.Set(Element{
|
||||
Id: id,
|
||||
Head: head,
|
||||
@@ -92,7 +92,7 @@ func TestDiff_Diff(t *testing.T) {
|
||||
for i := 0; i < 10; i++ {
|
||||
d2.Set(Element{
|
||||
Id: fmt.Sprint(i),
|
||||
Head: uuid.NewString(),
|
||||
Head: bson.NewObjectId().Hex(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ func TestDiff_Diff(t *testing.T) {
|
||||
for i := 0; i < 10; i++ {
|
||||
d2.Set(Element{
|
||||
Id: fmt.Sprint(i),
|
||||
Head: uuid.NewString(),
|
||||
Head: bson.NewObjectId().Hex(),
|
||||
})
|
||||
}
|
||||
var cancel func()
|
||||
@@ -123,7 +123,7 @@ func BenchmarkDiff_Ranges(b *testing.B) {
|
||||
d := New(16, 16)
|
||||
for i := 0; i < 10000; i++ {
|
||||
id := fmt.Sprint(i)
|
||||
head := uuid.NewString()
|
||||
head := bson.NewObjectId().Hex()
|
||||
d.Set(Element{
|
||||
Id: id,
|
||||
Head: head,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/anyproto/any-sync/app/ldiff (interfaces: Diff,Remote)
|
||||
// Source: github.com/anytypeio/any-sync/app/ldiff (interfaces: Diff,Remote)
|
||||
|
||||
// Package mock_ldiff is a generated GoMock package.
|
||||
package mock_ldiff
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
ldiff "github.com/anyproto/any-sync/app/ldiff"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
ldiff "github.com/anytypeio/any-sync/app/ldiff"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockDiff is a mock of Diff interface.
|
||||
|
||||
+13
-50
@@ -1,13 +1,10 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/anyproto/any-sync/util/slice"
|
||||
"github.com/anytypeio/any-sync/util/slice"
|
||||
)
|
||||
|
||||
type LogFormat int
|
||||
@@ -18,19 +15,14 @@ const (
|
||||
JSONOutput
|
||||
)
|
||||
|
||||
type NamedLevel struct {
|
||||
Name string `yaml:"name"`
|
||||
Level string `yaml:"level"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Production bool `yaml:"production"`
|
||||
DefaultLevel string `yaml:"defaultLevel"`
|
||||
Levels []NamedLevel `yaml:"levels"` // first match will be used
|
||||
AddOutputPaths []string `yaml:"outputPaths"`
|
||||
DisableStdErr bool `yaml:"disableStdErr"`
|
||||
Format LogFormat `yaml:"format"`
|
||||
ZapConfig *zap.Config `yaml:"-"` // optional, if set it will be used instead of other config options
|
||||
Production bool `yaml:"production"`
|
||||
DefaultLevel string `yaml:"defaultLevel"`
|
||||
NamedLevels map[string]string `yaml:"namedLevels"`
|
||||
AddOutputPaths []string `yaml:"outputPaths"`
|
||||
DisableStdErr bool `yaml:"disableStdErr"`
|
||||
Format LogFormat `yaml:"format"`
|
||||
ZapConfig *zap.Config `yaml:"-"` // optional, if set it will be used instead of other config options
|
||||
}
|
||||
|
||||
func (l Config) ApplyGlobal() {
|
||||
@@ -76,49 +68,20 @@ func (l Config) ApplyGlobal() {
|
||||
conf.Level = defaultLevel
|
||||
}
|
||||
}
|
||||
for _, v := range l.Levels {
|
||||
if lev, err := zap.ParseAtomicLevel(v.Level); err == nil {
|
||||
var lvl = make(map[string]zap.AtomicLevel)
|
||||
for k, v := range l.NamedLevels {
|
||||
if lev, err := zap.ParseAtomicLevel(v); err == nil {
|
||||
lvl[k] = lev
|
||||
// we need to have a minimum level of all named loggers for the main logger
|
||||
if lev.Level() < conf.Level.Level() {
|
||||
conf.Level.SetLevel(lev.Level())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lg, err := conf.Build()
|
||||
if err != nil {
|
||||
Default().Fatal("can't build logger", zap.Error(err))
|
||||
}
|
||||
SetDefault(lg)
|
||||
SetNamedLevels(l.Levels)
|
||||
}
|
||||
|
||||
// LevelsFromStr parses a string of the form "name1=DEBUG;prefix*=WARN;*=ERROR" into a slice of NamedLevel
|
||||
// it may be useful to parse the log level from the OS env var
|
||||
func LevelsFromStr(s string) (levels []NamedLevel) {
|
||||
for _, kv := range strings.Split(s, ";") {
|
||||
strings.TrimSpace(kv)
|
||||
parts := strings.Split(kv, "=")
|
||||
var key, value string
|
||||
if len(parts) == 1 {
|
||||
key = "*"
|
||||
value = parts[0]
|
||||
_, err := zap.ParseAtomicLevel(value)
|
||||
if err != nil {
|
||||
fmt.Printf("Can't parse log level %s: %s\n", parts[0], err.Error())
|
||||
continue
|
||||
}
|
||||
levels = append(levels, NamedLevel{Name: key, Level: value})
|
||||
} else if len(parts) == 2 {
|
||||
key = parts[0]
|
||||
value = parts[1]
|
||||
}
|
||||
_, err := zap.ParseAtomicLevel(value)
|
||||
if err != nil {
|
||||
fmt.Printf("Can't parse log level %s: %s\n", parts[0], err.Error())
|
||||
continue
|
||||
}
|
||||
levels = append(levels, NamedLevel{Name: key, Level: value})
|
||||
}
|
||||
return levels
|
||||
SetNamedLevels(lvl)
|
||||
}
|
||||
+20
-25
@@ -11,17 +11,12 @@ var (
|
||||
mu sync.Mutex
|
||||
logger *zap.Logger
|
||||
loggerConfig zap.Config
|
||||
namedLevels []namedLevel
|
||||
namedLevels = make(map[string]zap.AtomicLevel)
|
||||
namedGlobs = make(map[string]glob.Glob)
|
||||
namedLoggers = make(map[string]CtxLogger)
|
||||
namedSugarLoggers = make(map[string]*zap.SugaredLogger)
|
||||
)
|
||||
|
||||
type namedLevel struct {
|
||||
name string
|
||||
level zap.AtomicLevel
|
||||
}
|
||||
|
||||
func init() {
|
||||
loggerConfig = zap.NewDevelopmentConfig()
|
||||
logger, _ = loggerConfig.Build()
|
||||
@@ -40,23 +35,18 @@ func SetDefault(l *zap.Logger) {
|
||||
// it also supports glob patterns for names, like "app*"
|
||||
// can be racy in case there are existing named loggers
|
||||
// so consider to call only once at the beginning
|
||||
func SetNamedLevels(nls []NamedLevel) {
|
||||
func SetNamedLevels(l map[string]zap.AtomicLevel) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
namedLevels = namedLevels[:0]
|
||||
namedLevels = l
|
||||
|
||||
var minLevel = logger.Level()
|
||||
for _, nl := range nls {
|
||||
l, err := zap.ParseAtomicLevel(nl.Level)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
namedLevels = append(namedLevels, namedLevel{name: nl.Name, level: l})
|
||||
g, err := glob.Compile(nl.Name)
|
||||
for k, l := range namedLevels {
|
||||
g, err := glob.Compile(k)
|
||||
if err == nil {
|
||||
namedGlobs[nl.Name] = g
|
||||
namedGlobs[k] = g
|
||||
}
|
||||
|
||||
namedLevels[k] = l
|
||||
if l.Level() < minLevel {
|
||||
minLevel = l.Level()
|
||||
}
|
||||
@@ -91,18 +81,23 @@ func Default() *zap.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
// getLevel returns the level for the given name
|
||||
// it return the first matching name or glob pattern whatever comes first
|
||||
func getLevel(name string) zap.AtomicLevel {
|
||||
for _, nl := range namedLevels {
|
||||
if nl.name == name {
|
||||
return nl.level
|
||||
level, ok := namedLevels[name]
|
||||
if !ok {
|
||||
var found bool
|
||||
for globName, glob := range namedGlobs {
|
||||
if glob.Match(name) {
|
||||
found = true
|
||||
level, _ = namedLevels[globName]
|
||||
// no need to check ok, because we know that globName exists
|
||||
break
|
||||
}
|
||||
}
|
||||
if g, ok := namedGlobs[nl.name]; ok && g.Match(name) {
|
||||
return nl.level
|
||||
if !found {
|
||||
level = loggerConfig.Level
|
||||
}
|
||||
}
|
||||
return zap.NewAtomicLevelAt(logger.Level())
|
||||
return level
|
||||
}
|
||||
|
||||
func NewNamed(name string, fields ...zap.Field) CtxLogger {
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func Test_getLevel1(t *testing.T) {
|
||||
SetNamedLevels([]NamedLevel{
|
||||
{Name: "app", Level: "debug"},
|
||||
{Name: "app*", Level: "info"},
|
||||
{Name: "app.sub", Level: "warn"},
|
||||
{Name: "*", Level: "fatal"},
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
want zap.AtomicLevel
|
||||
}{
|
||||
{
|
||||
name: "app",
|
||||
want: zap.NewAtomicLevelAt(zap.DebugLevel),
|
||||
},
|
||||
{
|
||||
name: "app.aaa",
|
||||
want: zap.NewAtomicLevelAt(zap.InfoLevel),
|
||||
},
|
||||
{
|
||||
name: "app.sub",
|
||||
want: zap.NewAtomicLevelAt(zap.InfoLevel),
|
||||
},
|
||||
{
|
||||
name: "random",
|
||||
want: zap.NewAtomicLevelAt(zap.FatalLevel),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getLevel(tt.name); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("getLevel() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getLevel2(t *testing.T) {
|
||||
SetNamedLevels([]NamedLevel{
|
||||
{Name: "*", Level: "ERROR"},
|
||||
{Name: "app", Level: "info"},
|
||||
{Name: "app.sub", Level: "warn"},
|
||||
{Name: "*", Level: "fatal"},
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
want zap.AtomicLevel
|
||||
}{
|
||||
{
|
||||
name: "app",
|
||||
want: zap.NewAtomicLevelAt(zap.ErrorLevel),
|
||||
},
|
||||
{
|
||||
name: "app.aaa",
|
||||
want: zap.NewAtomicLevelAt(zap.ErrorLevel),
|
||||
},
|
||||
{
|
||||
name: "app.sub",
|
||||
want: zap.NewAtomicLevelAt(zap.ErrorLevel),
|
||||
},
|
||||
{
|
||||
name: "random",
|
||||
want: zap.NewAtomicLevelAt(zap.ErrorLevel),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getLevel(tt.name); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("getLevel() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getLevel3(t *testing.T) {
|
||||
SetNamedLevels([]NamedLevel{
|
||||
{Name: "app", Level: "info"},
|
||||
{Name: "*.sub", Level: "warn"},
|
||||
{Name: "*", Level: "fatal"},
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
want zap.AtomicLevel
|
||||
}{
|
||||
{
|
||||
name: "app",
|
||||
want: zap.NewAtomicLevelAt(zap.InfoLevel),
|
||||
},
|
||||
{
|
||||
name: "app.sub",
|
||||
want: zap.NewAtomicLevelAt(zap.WarnLevel),
|
||||
},
|
||||
{
|
||||
name: "random",
|
||||
want: zap.NewAtomicLevelAt(zap.FatalLevel),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getLevel(tt.name); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("getLevel() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getLevel4(t *testing.T) {
|
||||
SetNamedLevels([]NamedLevel{
|
||||
{Name: "*", Level: "invalid"},
|
||||
{Name: "app", Level: "info"},
|
||||
{Name: "b", Level: "invalid"},
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
want zap.AtomicLevel
|
||||
}{
|
||||
{
|
||||
name: "app",
|
||||
want: zap.NewAtomicLevelAt(zap.InfoLevel),
|
||||
},
|
||||
{
|
||||
name: "app.sub",
|
||||
want: zap.NewAtomicLevelAt(logger.Level()),
|
||||
},
|
||||
{
|
||||
name: "b",
|
||||
want: zap.NewAtomicLevelAt(logger.Level()),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getLevel(tt.name); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("getLevel() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+1
-17
@@ -2,10 +2,9 @@ package ocache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go.uber.org/zap"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type entryState int
|
||||
@@ -26,7 +25,6 @@ type entry struct {
|
||||
value Object
|
||||
close chan struct{}
|
||||
mx sync.Mutex
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func newEntry(id string, value Object, state entryState) *entry {
|
||||
@@ -51,20 +49,6 @@ func (e *entry) isClosing() bool {
|
||||
return e.state == entryStateClosed || e.state == entryStateClosing
|
||||
}
|
||||
|
||||
func (e *entry) setCancel(cancel context.CancelFunc) {
|
||||
e.mx.Lock()
|
||||
defer e.mx.Unlock()
|
||||
e.cancel = cancel
|
||||
}
|
||||
|
||||
func (e *entry) cancelLoad() {
|
||||
e.mx.Lock()
|
||||
defer e.mx.Unlock()
|
||||
if e.cancel != nil {
|
||||
e.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *entry) waitLoad(ctx context.Context, id string) (value Object, err error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -6,9 +6,6 @@ import (
|
||||
)
|
||||
|
||||
func WithPrometheus(reg *prometheus.Registry, namespace, subsystem string) Option {
|
||||
if reg == nil {
|
||||
return nil
|
||||
}
|
||||
if subsystem == "" {
|
||||
subsystem = "cache"
|
||||
}
|
||||
@@ -16,7 +13,9 @@ func WithPrometheus(reg *prometheus.Registry, namespace, subsystem string) Optio
|
||||
subSplit := strings.Split(subsystem, ".")
|
||||
namespace = strings.Join(nameSplit, "_")
|
||||
subsystem = strings.Join(subSplit, "_")
|
||||
|
||||
if reg == nil {
|
||||
return nil
|
||||
}
|
||||
return func(cache *oCache) {
|
||||
cache.metrics = &metrics{
|
||||
hit: prometheus.NewCounter(prometheus.CounterOpts{
|
||||
|
||||
@@ -3,11 +3,10 @@ package ocache
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/anytypeio/any-sync/app/logger"
|
||||
"go.uber.org/zap"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/app/logger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -158,10 +157,7 @@ func (c *oCache) Pick(ctx context.Context, id string) (value Object, err error)
|
||||
|
||||
func (c *oCache) load(ctx context.Context, id string, e *entry) {
|
||||
defer close(e.load)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
e.setCancel(cancel)
|
||||
value, err := c.loadFunc(ctx, id)
|
||||
cancel()
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -319,7 +315,6 @@ func (c *oCache) Close() (err error) {
|
||||
close(c.closeCh)
|
||||
var toClose []*entry
|
||||
for _, e := range c.data {
|
||||
e.cancelLoad()
|
||||
toClose = append(toClose, e)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -386,25 +386,6 @@ func Test_OCache_Remove(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestOCacheCancelWhenRemove(t *testing.T) {
|
||||
c := New(func(ctx context.Context, id string) (value Object, err error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}, WithTTL(time.Millisecond*10))
|
||||
stopLoad := make(chan struct{})
|
||||
var err error
|
||||
go func() {
|
||||
_, err = c.Get(context.TODO(), "id")
|
||||
stopLoad <- struct{}{}
|
||||
}()
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
c.Close()
|
||||
<-stopLoad
|
||||
require.Equal(t, context.Canceled, err)
|
||||
}
|
||||
|
||||
func TestOCacheFuzzy(t *testing.T) {
|
||||
t.Run("test many objects gc, get and remove simultaneously, close after", func(t *testing.T) {
|
||||
tryCloseIds := make(map[string]bool)
|
||||
|
||||
@@ -2,8 +2,8 @@ package fileblockstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/anyproto/any-sync/app/logger"
|
||||
"github.com/anyproto/any-sync/commonfile/fileproto/fileprotoerr"
|
||||
"github.com/anytypeio/any-sync/app/logger"
|
||||
"github.com/anytypeio/any-sync/commonfile/fileproto/fileprotoerr"
|
||||
blocks "github.com/ipfs/go-block-format"
|
||||
"github.com/ipfs/go-cid"
|
||||
)
|
||||
@@ -21,7 +21,6 @@ type ctxKey uint
|
||||
|
||||
const (
|
||||
ctxKeySpaceId ctxKey = iota
|
||||
ctxKeyFileId
|
||||
)
|
||||
|
||||
type BlockStore interface {
|
||||
@@ -49,12 +48,3 @@ func CtxGetSpaceId(ctx context.Context) (spaceId string) {
|
||||
spaceId, _ = ctx.Value(ctxKeySpaceId).(string)
|
||||
return
|
||||
}
|
||||
|
||||
func CtxWithFileId(ctx context.Context, spaceId string) context.Context {
|
||||
return context.WithValue(ctx, ctxKeyFileId, spaceId)
|
||||
}
|
||||
|
||||
func CtxGetFileId(ctx context.Context) (spaceId string) {
|
||||
spaceId, _ = ctx.Value(ctxKeyFileId).(string)
|
||||
return
|
||||
}
|
||||
+401
-1604
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
||||
// Code generated by protoc-gen-go-drpc. DO NOT EDIT.
|
||||
// protoc-gen-go-drpc version: v0.0.33
|
||||
// protoc-gen-go-drpc version: v0.0.32
|
||||
// source: commonfile/fileproto/protos/file.proto
|
||||
|
||||
package fileproto
|
||||
@@ -44,10 +44,8 @@ type DRPCFileClient interface {
|
||||
BlockPush(ctx context.Context, in *BlockPushRequest) (*BlockPushResponse, error)
|
||||
BlocksCheck(ctx context.Context, in *BlocksCheckRequest) (*BlocksCheckResponse, error)
|
||||
BlocksBind(ctx context.Context, in *BlocksBindRequest) (*BlocksBindResponse, error)
|
||||
FilesDelete(ctx context.Context, in *FilesDeleteRequest) (*FilesDeleteResponse, error)
|
||||
FilesInfo(ctx context.Context, in *FilesInfoRequest) (*FilesInfoResponse, error)
|
||||
BlocksDelete(ctx context.Context, in *BlocksDeleteRequest) (*BlocksDeleteResponse, error)
|
||||
Check(ctx context.Context, in *CheckRequest) (*CheckResponse, error)
|
||||
SpaceInfo(ctx context.Context, in *SpaceInfoRequest) (*SpaceInfoResponse, error)
|
||||
}
|
||||
|
||||
type drpcFileClient struct {
|
||||
@@ -96,18 +94,9 @@ func (c *drpcFileClient) BlocksBind(ctx context.Context, in *BlocksBindRequest)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *drpcFileClient) FilesDelete(ctx context.Context, in *FilesDeleteRequest) (*FilesDeleteResponse, error) {
|
||||
out := new(FilesDeleteResponse)
|
||||
err := c.cc.Invoke(ctx, "/filesync.File/FilesDelete", drpcEncoding_File_commonfile_fileproto_protos_file_proto{}, in, out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *drpcFileClient) FilesInfo(ctx context.Context, in *FilesInfoRequest) (*FilesInfoResponse, error) {
|
||||
out := new(FilesInfoResponse)
|
||||
err := c.cc.Invoke(ctx, "/filesync.File/FilesInfo", drpcEncoding_File_commonfile_fileproto_protos_file_proto{}, in, out)
|
||||
func (c *drpcFileClient) BlocksDelete(ctx context.Context, in *BlocksDeleteRequest) (*BlocksDeleteResponse, error) {
|
||||
out := new(BlocksDeleteResponse)
|
||||
err := c.cc.Invoke(ctx, "/filesync.File/BlocksDelete", drpcEncoding_File_commonfile_fileproto_protos_file_proto{}, in, out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -123,24 +112,13 @@ func (c *drpcFileClient) Check(ctx context.Context, in *CheckRequest) (*CheckRes
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *drpcFileClient) SpaceInfo(ctx context.Context, in *SpaceInfoRequest) (*SpaceInfoResponse, error) {
|
||||
out := new(SpaceInfoResponse)
|
||||
err := c.cc.Invoke(ctx, "/filesync.File/SpaceInfo", drpcEncoding_File_commonfile_fileproto_protos_file_proto{}, in, out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type DRPCFileServer interface {
|
||||
BlockGet(context.Context, *BlockGetRequest) (*BlockGetResponse, error)
|
||||
BlockPush(context.Context, *BlockPushRequest) (*BlockPushResponse, error)
|
||||
BlocksCheck(context.Context, *BlocksCheckRequest) (*BlocksCheckResponse, error)
|
||||
BlocksBind(context.Context, *BlocksBindRequest) (*BlocksBindResponse, error)
|
||||
FilesDelete(context.Context, *FilesDeleteRequest) (*FilesDeleteResponse, error)
|
||||
FilesInfo(context.Context, *FilesInfoRequest) (*FilesInfoResponse, error)
|
||||
BlocksDelete(context.Context, *BlocksDeleteRequest) (*BlocksDeleteResponse, error)
|
||||
Check(context.Context, *CheckRequest) (*CheckResponse, error)
|
||||
SpaceInfo(context.Context, *SpaceInfoRequest) (*SpaceInfoResponse, error)
|
||||
}
|
||||
|
||||
type DRPCFileUnimplementedServer struct{}
|
||||
@@ -161,11 +139,7 @@ func (s *DRPCFileUnimplementedServer) BlocksBind(context.Context, *BlocksBindReq
|
||||
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
|
||||
}
|
||||
|
||||
func (s *DRPCFileUnimplementedServer) FilesDelete(context.Context, *FilesDeleteRequest) (*FilesDeleteResponse, error) {
|
||||
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
|
||||
}
|
||||
|
||||
func (s *DRPCFileUnimplementedServer) FilesInfo(context.Context, *FilesInfoRequest) (*FilesInfoResponse, error) {
|
||||
func (s *DRPCFileUnimplementedServer) BlocksDelete(context.Context, *BlocksDeleteRequest) (*BlocksDeleteResponse, error) {
|
||||
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
|
||||
}
|
||||
|
||||
@@ -173,13 +147,9 @@ func (s *DRPCFileUnimplementedServer) Check(context.Context, *CheckRequest) (*Ch
|
||||
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
|
||||
}
|
||||
|
||||
func (s *DRPCFileUnimplementedServer) SpaceInfo(context.Context, *SpaceInfoRequest) (*SpaceInfoResponse, error) {
|
||||
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
|
||||
}
|
||||
|
||||
type DRPCFileDescription struct{}
|
||||
|
||||
func (DRPCFileDescription) NumMethods() int { return 8 }
|
||||
func (DRPCFileDescription) NumMethods() int { return 6 }
|
||||
|
||||
func (DRPCFileDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) {
|
||||
switch n {
|
||||
@@ -220,24 +190,15 @@ func (DRPCFileDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver,
|
||||
)
|
||||
}, DRPCFileServer.BlocksBind, true
|
||||
case 4:
|
||||
return "/filesync.File/FilesDelete", drpcEncoding_File_commonfile_fileproto_protos_file_proto{},
|
||||
return "/filesync.File/BlocksDelete", drpcEncoding_File_commonfile_fileproto_protos_file_proto{},
|
||||
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
|
||||
return srv.(DRPCFileServer).
|
||||
FilesDelete(
|
||||
BlocksDelete(
|
||||
ctx,
|
||||
in1.(*FilesDeleteRequest),
|
||||
in1.(*BlocksDeleteRequest),
|
||||
)
|
||||
}, DRPCFileServer.FilesDelete, true
|
||||
}, DRPCFileServer.BlocksDelete, true
|
||||
case 5:
|
||||
return "/filesync.File/FilesInfo", drpcEncoding_File_commonfile_fileproto_protos_file_proto{},
|
||||
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
|
||||
return srv.(DRPCFileServer).
|
||||
FilesInfo(
|
||||
ctx,
|
||||
in1.(*FilesInfoRequest),
|
||||
)
|
||||
}, DRPCFileServer.FilesInfo, true
|
||||
case 6:
|
||||
return "/filesync.File/Check", drpcEncoding_File_commonfile_fileproto_protos_file_proto{},
|
||||
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
|
||||
return srv.(DRPCFileServer).
|
||||
@@ -246,15 +207,6 @@ func (DRPCFileDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver,
|
||||
in1.(*CheckRequest),
|
||||
)
|
||||
}, DRPCFileServer.Check, true
|
||||
case 7:
|
||||
return "/filesync.File/SpaceInfo", drpcEncoding_File_commonfile_fileproto_protos_file_proto{},
|
||||
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
|
||||
return srv.(DRPCFileServer).
|
||||
SpaceInfo(
|
||||
ctx,
|
||||
in1.(*SpaceInfoRequest),
|
||||
)
|
||||
}, DRPCFileServer.SpaceInfo, true
|
||||
default:
|
||||
return "", nil, nil, nil, false
|
||||
}
|
||||
@@ -328,32 +280,16 @@ func (x *drpcFile_BlocksBindStream) SendAndClose(m *BlocksBindResponse) error {
|
||||
return x.CloseSend()
|
||||
}
|
||||
|
||||
type DRPCFile_FilesDeleteStream interface {
|
||||
type DRPCFile_BlocksDeleteStream interface {
|
||||
drpc.Stream
|
||||
SendAndClose(*FilesDeleteResponse) error
|
||||
SendAndClose(*BlocksDeleteResponse) error
|
||||
}
|
||||
|
||||
type drpcFile_FilesDeleteStream struct {
|
||||
type drpcFile_BlocksDeleteStream struct {
|
||||
drpc.Stream
|
||||
}
|
||||
|
||||
func (x *drpcFile_FilesDeleteStream) SendAndClose(m *FilesDeleteResponse) error {
|
||||
if err := x.MsgSend(m, drpcEncoding_File_commonfile_fileproto_protos_file_proto{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return x.CloseSend()
|
||||
}
|
||||
|
||||
type DRPCFile_FilesInfoStream interface {
|
||||
drpc.Stream
|
||||
SendAndClose(*FilesInfoResponse) error
|
||||
}
|
||||
|
||||
type drpcFile_FilesInfoStream struct {
|
||||
drpc.Stream
|
||||
}
|
||||
|
||||
func (x *drpcFile_FilesInfoStream) SendAndClose(m *FilesInfoResponse) error {
|
||||
func (x *drpcFile_BlocksDeleteStream) SendAndClose(m *BlocksDeleteResponse) error {
|
||||
if err := x.MsgSend(m, drpcEncoding_File_commonfile_fileproto_protos_file_proto{}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -375,19 +311,3 @@ func (x *drpcFile_CheckStream) SendAndClose(m *CheckResponse) error {
|
||||
}
|
||||
return x.CloseSend()
|
||||
}
|
||||
|
||||
type DRPCFile_SpaceInfoStream interface {
|
||||
drpc.Stream
|
||||
SendAndClose(*SpaceInfoResponse) error
|
||||
}
|
||||
|
||||
type drpcFile_SpaceInfoStream struct {
|
||||
drpc.Stream
|
||||
}
|
||||
|
||||
func (x *drpcFile_SpaceInfoStream) SendAndClose(m *SpaceInfoResponse) error {
|
||||
if err := x.MsgSend(m, drpcEncoding_File_commonfile_fileproto_protos_file_proto{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return x.CloseSend()
|
||||
}
|
||||
@@ -2,16 +2,12 @@ package fileprotoerr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anyproto/any-sync/commonfile/fileproto"
|
||||
"github.com/anyproto/any-sync/net/rpc/rpcerr"
|
||||
"github.com/anytypeio/any-sync/commonfile/fileproto"
|
||||
"github.com/anytypeio/any-sync/net/rpc/rpcerr"
|
||||
)
|
||||
|
||||
var (
|
||||
errGroup = rpcerr.ErrGroup(fileproto.ErrCodes_ErrorOffset)
|
||||
ErrUnexpected = errGroup.Register(fmt.Errorf("unexpected fileproto error"), uint64(fileproto.ErrCodes_Unexpected))
|
||||
ErrCIDNotFound = errGroup.Register(fmt.Errorf("CID not found"), uint64(fileproto.ErrCodes_CIDNotFound))
|
||||
ErrForbidden = errGroup.Register(fmt.Errorf("forbidden"), uint64(fileproto.ErrCodes_Forbidden))
|
||||
ErrSpaceLimitExceeded = errGroup.Register(fmt.Errorf("space limit exceeded"), uint64(fileproto.ErrCodes_SpaceLimitExceeded))
|
||||
ErrQuerySizeExceeded = errGroup.Register(fmt.Errorf("query size exceeded"), uint64(fileproto.ErrCodes_QuerySizeExceeded))
|
||||
ErrWrongHash = errGroup.Register(fmt.Errorf("wrong block hash"), uint64(fileproto.ErrCodes_WrongHash))
|
||||
errGroup = rpcerr.ErrGroup(fileproto.ErrCodes_ErrorOffset)
|
||||
ErrUnexpected = errGroup.Register(fmt.Errorf("unexpected fileproto error"), uint64(fileproto.ErrCodes_Unexpected))
|
||||
ErrCIDNotFound = errGroup.Register(fmt.Errorf("CID not found"), uint64(fileproto.ErrCodes_CIDNotFound))
|
||||
)
|
||||
@@ -6,10 +6,6 @@ option go_package = "commonfile/fileproto";
|
||||
enum ErrCodes {
|
||||
Unexpected = 0;
|
||||
CIDNotFound = 1;
|
||||
Forbidden = 2;
|
||||
SpaceLimitExceeded = 3;
|
||||
QuerySizeExceeded = 4;
|
||||
WrongHash = 5;
|
||||
ErrorOffset = 200;
|
||||
}
|
||||
|
||||
@@ -22,14 +18,10 @@ service File {
|
||||
rpc BlocksCheck(BlocksCheckRequest) returns (BlocksCheckResponse);
|
||||
// BlocksBind binds CIDs to space
|
||||
rpc BlocksBind(BlocksBindRequest) returns (BlocksBindResponse);
|
||||
// FilesDelete deletes files by id
|
||||
rpc FilesDelete(FilesDeleteRequest) returns (FilesDeleteResponse);
|
||||
// FilesInfo return info by given files id
|
||||
rpc FilesInfo(FilesInfoRequest) returns (FilesInfoResponse);
|
||||
// BlocksDelete deletes block from space
|
||||
rpc BlocksDelete(BlocksDeleteRequest) returns (BlocksDeleteResponse);
|
||||
// Check checks the connection and credentials
|
||||
rpc Check(CheckRequest) returns (CheckResponse);
|
||||
// SpaceInfo returns usage, limit, etc about space
|
||||
rpc SpaceInfo(SpaceInfoRequest) returns (SpaceInfoResponse);
|
||||
}
|
||||
|
||||
message BlockGetRequest {
|
||||
@@ -44,13 +36,19 @@ message BlockGetResponse {
|
||||
|
||||
message BlockPushRequest {
|
||||
string spaceId = 1;
|
||||
string fileId = 2;
|
||||
bytes cid = 3;
|
||||
bytes data = 4;
|
||||
bytes cid = 2;
|
||||
bytes data = 3;
|
||||
}
|
||||
|
||||
message BlockPushResponse {}
|
||||
|
||||
message BlocksDeleteRequest {
|
||||
string spaceId = 1;
|
||||
repeated bytes cids = 2;
|
||||
}
|
||||
|
||||
message BlocksDeleteResponse {}
|
||||
|
||||
|
||||
message BlocksCheckRequest {
|
||||
string spaceId = 1;
|
||||
@@ -74,50 +72,14 @@ enum AvailabilityStatus {
|
||||
|
||||
message BlocksBindRequest {
|
||||
string spaceId = 1;
|
||||
string fileId = 2;
|
||||
repeated bytes cids = 3;
|
||||
repeated bytes cids = 2;
|
||||
}
|
||||
|
||||
message BlocksBindResponse {}
|
||||
|
||||
|
||||
message FilesDeleteRequest {
|
||||
string spaceId = 1;
|
||||
repeated string fileIds = 2;
|
||||
}
|
||||
|
||||
message FilesDeleteResponse {}
|
||||
|
||||
message FilesInfoRequest {
|
||||
string spaceId = 1;
|
||||
repeated string fileIds = 2;
|
||||
}
|
||||
|
||||
message FilesInfoResponse {
|
||||
repeated FileInfo filesInfo = 1;
|
||||
}
|
||||
|
||||
message FileInfo {
|
||||
string fileId = 1;
|
||||
uint64 usageBytes = 2;
|
||||
uint32 cidsCount = 3;
|
||||
}
|
||||
|
||||
message CheckRequest {}
|
||||
|
||||
message CheckResponse {
|
||||
repeated string spaceIds = 1;
|
||||
bool allowWrite = 2;
|
||||
}
|
||||
|
||||
message SpaceInfoRequest {
|
||||
string spaceId = 1;
|
||||
}
|
||||
|
||||
message SpaceInfoResponse {
|
||||
uint64 limitBytes = 1;
|
||||
uint64 usageBytes = 2;
|
||||
uint64 cidsCount = 3;
|
||||
uint64 filesCount = 4;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package fileservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/anyproto/any-sync/commonfile/fileblockstore"
|
||||
"github.com/anytypeio/any-sync/commonfile/fileblockstore"
|
||||
blocks "github.com/ipfs/go-block-format"
|
||||
"github.com/ipfs/go-blockservice"
|
||||
"github.com/ipfs/go-cid"
|
||||
|
||||
@@ -3,9 +3,9 @@ package fileservice
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/anyproto/any-sync/app"
|
||||
"github.com/anyproto/any-sync/app/logger"
|
||||
"github.com/anyproto/any-sync/commonfile/fileblockstore"
|
||||
"github.com/anytypeio/any-sync/app"
|
||||
"github.com/anytypeio/any-sync/app/logger"
|
||||
"github.com/anytypeio/any-sync/commonfile/fileblockstore"
|
||||
"github.com/ipfs/go-cid"
|
||||
chunker "github.com/ipfs/go-ipfs-chunker"
|
||||
ipld "github.com/ipfs/go-ipld-format"
|
||||
@@ -22,10 +22,6 @@ const CName = "common.commonfile.fileservice"
|
||||
|
||||
var log = logger.NewNamed(CName)
|
||||
|
||||
const (
|
||||
ChunkSize = 1 << 20
|
||||
)
|
||||
|
||||
func New() FileService {
|
||||
return &fileService{}
|
||||
}
|
||||
@@ -78,7 +74,7 @@ func (fs *fileService) AddFile(ctx context.Context, r io.Reader) (ipld.Node, err
|
||||
Maxlinks: helpers.DefaultLinksPerBlock,
|
||||
CidBuilder: &fs.prefix,
|
||||
}
|
||||
dbh, err := dbp.New(chunker.NewSizeSplitter(r, ChunkSize))
|
||||
dbh, err := dbp.New(chunker.DefaultSplitter(r))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package commonspace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/syncobjectgetter"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/treegetter"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type commonGetter struct {
|
||||
treegetter.TreeGetter
|
||||
spaceId string
|
||||
reservedObjects []syncobjectgetter.SyncObject
|
||||
spaceIsClosed *atomic.Bool
|
||||
}
|
||||
|
||||
func newCommonGetter(spaceId string, getter treegetter.TreeGetter, spaceIsClosed *atomic.Bool) *commonGetter {
|
||||
return &commonGetter{
|
||||
TreeGetter: getter,
|
||||
spaceId: spaceId,
|
||||
spaceIsClosed: spaceIsClosed,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *commonGetter) AddObject(object syncobjectgetter.SyncObject) {
|
||||
c.reservedObjects = append(c.reservedObjects, object)
|
||||
}
|
||||
|
||||
func (c *commonGetter) GetTree(ctx context.Context, spaceId, treeId string) (objecttree.ObjectTree, error) {
|
||||
if c.spaceIsClosed.Load() {
|
||||
return nil, ErrSpaceClosed
|
||||
}
|
||||
if obj := c.getReservedObject(treeId); obj != nil {
|
||||
return obj.(objecttree.ObjectTree), nil
|
||||
}
|
||||
return c.TreeGetter.GetTree(ctx, spaceId, treeId)
|
||||
}
|
||||
|
||||
func (c *commonGetter) getReservedObject(id string) syncobjectgetter.SyncObject {
|
||||
for _, obj := range c.reservedObjects {
|
||||
if obj != nil && obj.Id() == id {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *commonGetter) GetObject(ctx context.Context, objectId string) (obj syncobjectgetter.SyncObject, err error) {
|
||||
if c.spaceIsClosed.Load() {
|
||||
return nil, ErrSpaceClosed
|
||||
}
|
||||
if obj := c.getReservedObject(objectId); obj != nil {
|
||||
return obj, nil
|
||||
}
|
||||
t, err := c.TreeGetter.GetTree(ctx, c.spaceId, objectId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
obj = t.(syncobjectgetter.SyncObject)
|
||||
return
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package commonspace
|
||||
|
||||
import (
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anyproto/any-sync/commonspace/spacestorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacestorage"
|
||||
)
|
||||
|
||||
type commonStorage struct {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package commonspace
|
||||
|
||||
type ConfigGetter interface {
|
||||
GetSpace() Config
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
GCTTL int `yaml:"gcTTL"`
|
||||
SyncPeriod int `yaml:"syncPeriod"`
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package config
|
||||
|
||||
type ConfigGetter interface {
|
||||
GetSpace() Config
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
GCTTL int `yaml:"gcTTL"`
|
||||
SyncPeriod int `yaml:"syncPeriod"`
|
||||
KeepTreeDataInMemory bool `yaml:"keepTreeDataInMemory"`
|
||||
}
|
||||
@@ -1,34 +1,52 @@
|
||||
//go:generate mockgen -destination mock_credentialprovider/mock_credentialprovider.go github.com/anyproto/any-sync/commonspace/credentialprovider CredentialProvider
|
||||
//go:generate mockgen -destination mock_credentialprovider/mock_credentialprovider.go github.com/anytypeio/any-sync/commonspace/credentialprovider CredentialProvider
|
||||
package credentialprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/anyproto/any-sync/app"
|
||||
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/app"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/coordinator/coordinatorclient"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
const CName = "common.commonspace.credentialprovider"
|
||||
|
||||
func New() app.Component {
|
||||
return &credentialProvider{}
|
||||
}
|
||||
|
||||
func NewNoOp() CredentialProvider {
|
||||
return &noOpProvider{}
|
||||
}
|
||||
|
||||
type CredentialProvider interface {
|
||||
app.Component
|
||||
GetCredential(ctx context.Context, spaceHeader *spacesyncproto.RawSpaceHeaderWithId) ([]byte, error)
|
||||
}
|
||||
|
||||
type noOpProvider struct {
|
||||
}
|
||||
|
||||
func (n noOpProvider) Init(a *app.App) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n noOpProvider) Name() (name string) {
|
||||
return CName
|
||||
}
|
||||
|
||||
func (n noOpProvider) GetCredential(ctx context.Context, spaceHeader *spacesyncproto.RawSpaceHeaderWithId) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type credentialProvider struct {
|
||||
client coordinatorclient.CoordinatorClient
|
||||
}
|
||||
|
||||
func (c *credentialProvider) Init(a *app.App) (err error) {
|
||||
c.client = a.MustComponent(coordinatorclient.CName).(coordinatorclient.CoordinatorClient)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *credentialProvider) Name() (name string) {
|
||||
return CName
|
||||
}
|
||||
|
||||
func (c *credentialProvider) GetCredential(ctx context.Context, spaceHeader *spacesyncproto.RawSpaceHeaderWithId) ([]byte, error) {
|
||||
receipt, err := c.client.SpaceSign(ctx, spaceHeader.Id, spaceHeader.RawHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return proto.Marshal(receipt)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/anyproto/any-sync/commonspace/credentialprovider (interfaces: CredentialProvider)
|
||||
// Source: github.com/anytypeio/any-sync/commonspace/credentialprovider (interfaces: CredentialProvider)
|
||||
|
||||
// Package mock_credentialprovider is a generated GoMock package.
|
||||
package mock_credentialprovider
|
||||
@@ -8,9 +8,8 @@ import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
app "github.com/anyproto/any-sync/app"
|
||||
spacesyncproto "github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
spacesyncproto "github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockCredentialProvider is a mock of CredentialProvider interface.
|
||||
@@ -50,31 +49,3 @@ func (mr *MockCredentialProviderMockRecorder) GetCredential(arg0, arg1 interface
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCredential", reflect.TypeOf((*MockCredentialProvider)(nil).GetCredential), arg0, arg1)
|
||||
}
|
||||
|
||||
// Init mocks base method.
|
||||
func (m *MockCredentialProvider) Init(arg0 *app.App) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Init", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Init indicates an expected call of Init.
|
||||
func (mr *MockCredentialProviderMockRecorder) Init(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockCredentialProvider)(nil).Init), arg0)
|
||||
}
|
||||
|
||||
// Name mocks base method.
|
||||
func (m *MockCredentialProvider) Name() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Name")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Name indicates an expected call of Name.
|
||||
func (mr *MockCredentialProviderMockRecorder) Name() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockCredentialProvider)(nil).Name))
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
package commonspace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/anyproto/any-sync/commonspace/object/accountdata"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anyproto/any-sync/commonspace/settings"
|
||||
"github.com/anyproto/any-sync/commonspace/settings/settingsstate"
|
||||
"github.com/anyproto/any-sync/commonspace/spacestorage"
|
||||
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
"github.com/stretchr/testify/require"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func addIncorrectSnapshot(settingsObject settings.SettingsObject, acc *accountdata.AccountKeys, partialIds map[string]struct{}, newId string) (err error) {
|
||||
factory := settingsstate.NewChangeFactory()
|
||||
bytes, err := factory.CreateObjectDeleteChange(newId, &settingsstate.State{DeletedIds: partialIds}, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ch, err := settingsObject.PrepareChange(objecttree.SignableChangeContent{
|
||||
Data: bytes,
|
||||
Key: acc.SignKey,
|
||||
IsSnapshot: true,
|
||||
IsEncrypted: false,
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res, err := settingsObject.AddRawChanges(context.Background(), objecttree.RawChangesPayload{
|
||||
NewHeads: []string{ch.Id},
|
||||
RawChanges: []*treechangeproto.RawTreeChangeWithId{ch},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if res.Mode != objecttree.Rebuild {
|
||||
return fmt.Errorf("incorrect mode: %d", res.Mode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestSpaceDeleteIds(t *testing.T) {
|
||||
fx := newFixture(t)
|
||||
acc := fx.account.Account()
|
||||
rk := crypto.NewAES()
|
||||
ctx := context.Background()
|
||||
totalObjs := 1500
|
||||
|
||||
// creating space
|
||||
sp, err := fx.spaceService.CreateSpace(ctx, SpaceCreatePayload{
|
||||
SigningKey: acc.SignKey,
|
||||
SpaceType: "type",
|
||||
ReadKey: rk.Bytes(),
|
||||
ReplicationKey: 10,
|
||||
MasterKey: acc.PeerKey,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sp)
|
||||
|
||||
// initializing space
|
||||
spc, err := fx.spaceService.NewSpace(ctx, sp)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, spc)
|
||||
// adding space to tree manager
|
||||
fx.treeManager.space = spc
|
||||
err = spc.Init(ctx)
|
||||
require.NoError(t, err)
|
||||
close(fx.treeManager.waitLoad)
|
||||
|
||||
var ids []string
|
||||
for i := 0; i < totalObjs; i++ {
|
||||
// creating a tree
|
||||
bytes := make([]byte, 32)
|
||||
rand.Read(bytes)
|
||||
doc, err := spc.TreeBuilder().CreateTree(ctx, objecttree.ObjectTreeCreatePayload{
|
||||
PrivKey: acc.SignKey,
|
||||
ChangeType: "some",
|
||||
SpaceId: spc.Id(),
|
||||
IsEncrypted: false,
|
||||
Seed: bytes,
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
tr, err := spc.TreeBuilder().PutTree(ctx, doc, nil)
|
||||
require.NoError(t, err)
|
||||
ids = append(ids, tr.Id())
|
||||
tr.Close()
|
||||
}
|
||||
// deleting trees
|
||||
for _, id := range ids {
|
||||
err = spc.DeleteTree(ctx, id)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
spc.Close()
|
||||
require.Equal(t, len(ids), len(fx.treeManager.deletedIds))
|
||||
}
|
||||
|
||||
func createTree(t *testing.T, ctx context.Context, spc Space, acc *accountdata.AccountKeys) string {
|
||||
bytes := make([]byte, 32)
|
||||
rand.Read(bytes)
|
||||
doc, err := spc.TreeBuilder().CreateTree(ctx, objecttree.ObjectTreeCreatePayload{
|
||||
PrivKey: acc.SignKey,
|
||||
ChangeType: "some",
|
||||
SpaceId: spc.Id(),
|
||||
IsEncrypted: false,
|
||||
Seed: bytes,
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
tr, err := spc.TreeBuilder().PutTree(ctx, doc, nil)
|
||||
require.NoError(t, err)
|
||||
tr.Close()
|
||||
return tr.Id()
|
||||
}
|
||||
|
||||
func TestSpaceDeleteIdsIncorrectSnapshot(t *testing.T) {
|
||||
fx := newFixture(t)
|
||||
acc := fx.account.Account()
|
||||
rk := crypto.NewAES()
|
||||
ctx := context.Background()
|
||||
totalObjs := 1500
|
||||
partialObjs := 300
|
||||
|
||||
// creating space
|
||||
sp, err := fx.spaceService.CreateSpace(ctx, SpaceCreatePayload{
|
||||
SigningKey: acc.SignKey,
|
||||
SpaceType: "type",
|
||||
ReadKey: rk.Bytes(),
|
||||
ReplicationKey: 10,
|
||||
MasterKey: acc.PeerKey,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sp)
|
||||
|
||||
// initializing space
|
||||
spc, err := fx.spaceService.NewSpace(ctx, sp)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, spc)
|
||||
// adding space to tree manager
|
||||
fx.treeManager.space = spc
|
||||
err = spc.Init(ctx)
|
||||
close(fx.treeManager.waitLoad)
|
||||
require.NoError(t, err)
|
||||
|
||||
settingsObject := spc.(*space).app.MustComponent(settings.CName).(settings.Settings).SettingsObject()
|
||||
var ids []string
|
||||
for i := 0; i < totalObjs; i++ {
|
||||
id := createTree(t, ctx, spc, acc)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
// copying storage, so we will have all the trees locally
|
||||
inmemory := spc.Storage().(*commonStorage).SpaceStorage.(*spacestorage.InMemorySpaceStorage)
|
||||
storageCopy := inmemory.CopyStorage()
|
||||
treesCopy := inmemory.AllTrees()
|
||||
|
||||
// deleting trees
|
||||
for _, id := range ids {
|
||||
err = spc.DeleteTree(ctx, id)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
mapIds := map[string]struct{}{}
|
||||
for _, id := range ids[:partialObjs] {
|
||||
mapIds[id] = struct{}{}
|
||||
}
|
||||
// adding snapshot that breaks the state
|
||||
err = addIncorrectSnapshot(settingsObject, acc, mapIds, ids[partialObjs])
|
||||
require.NoError(t, err)
|
||||
// copying the contents of the settings tree
|
||||
treesCopy[settingsObject.Id()] = settingsObject.Storage()
|
||||
storageCopy.SetTrees(treesCopy)
|
||||
spc.Close()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// now we replace the storage, so the trees are back, but the settings object says that they are deleted
|
||||
fx.storageProvider.(*spacestorage.InMemorySpaceStorageProvider).SetStorage(storageCopy)
|
||||
|
||||
spc, err = fx.spaceService.NewSpace(ctx, sp)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, spc)
|
||||
fx.treeManager.waitLoad = make(chan struct{})
|
||||
fx.treeManager.space = spc
|
||||
fx.treeManager.deletedIds = nil
|
||||
err = spc.Init(ctx)
|
||||
require.NoError(t, err)
|
||||
close(fx.treeManager.waitLoad)
|
||||
|
||||
// waiting until everything is deleted
|
||||
time.Sleep(3 * time.Second)
|
||||
require.Equal(t, len(ids), len(fx.treeManager.deletedIds))
|
||||
|
||||
// checking that new snapshot will contain all the changes
|
||||
settingsObject = spc.(*space).app.MustComponent(settings.CName).(settings.Settings).SettingsObject()
|
||||
settings.DoSnapshot = func(treeLen int) bool {
|
||||
return true
|
||||
}
|
||||
id := createTree(t, ctx, spc, acc)
|
||||
err = spc.DeleteTree(ctx, id)
|
||||
require.NoError(t, err)
|
||||
delIds := settingsObject.Root().Model.(*spacesyncproto.SettingsData).Snapshot.DeletedIds
|
||||
require.Equal(t, totalObjs+1, len(delIds))
|
||||
}
|
||||
|
||||
func TestSpaceDeleteIdsMarkDeleted(t *testing.T) {
|
||||
fx := newFixture(t)
|
||||
acc := fx.account.Account()
|
||||
rk := crypto.NewAES()
|
||||
ctx := context.Background()
|
||||
totalObjs := 1500
|
||||
|
||||
// creating space
|
||||
sp, err := fx.spaceService.CreateSpace(ctx, SpaceCreatePayload{
|
||||
SigningKey: acc.SignKey,
|
||||
SpaceType: "type",
|
||||
ReadKey: rk.Bytes(),
|
||||
ReplicationKey: 10,
|
||||
MasterKey: acc.PeerKey,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sp)
|
||||
|
||||
// initializing space
|
||||
spc, err := fx.spaceService.NewSpace(ctx, sp)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, spc)
|
||||
// adding space to tree manager
|
||||
fx.treeManager.space = spc
|
||||
err = spc.Init(ctx)
|
||||
require.NoError(t, err)
|
||||
close(fx.treeManager.waitLoad)
|
||||
|
||||
settingsObject := spc.(*space).app.MustComponent(settings.CName).(settings.Settings).SettingsObject()
|
||||
var ids []string
|
||||
for i := 0; i < totalObjs; i++ {
|
||||
id := createTree(t, ctx, spc, acc)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
// copying storage, so we will have the same contents, except for empty trees
|
||||
inmemory := spc.Storage().(*commonStorage).SpaceStorage.(*spacestorage.InMemorySpaceStorage)
|
||||
storageCopy := inmemory.CopyStorage()
|
||||
|
||||
// deleting trees, this will prepare the document to have all the deletion changes
|
||||
for _, id := range ids {
|
||||
err = spc.DeleteTree(ctx, id)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
treesMap := map[string]treestorage.TreeStorage{}
|
||||
// copying the contents of the settings tree
|
||||
treesMap[settingsObject.Id()] = settingsObject.Storage()
|
||||
storageCopy.SetTrees(treesMap)
|
||||
spc.Close()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// now we replace the storage, so the trees are back, but the settings object says that they are deleted
|
||||
fx.storageProvider.(*spacestorage.InMemorySpaceStorageProvider).SetStorage(storageCopy)
|
||||
|
||||
spc, err = fx.spaceService.NewSpace(ctx, sp)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, spc)
|
||||
fx.treeManager.space = spc
|
||||
fx.treeManager.waitLoad = make(chan struct{})
|
||||
fx.treeManager.deletedIds = nil
|
||||
fx.treeManager.markedIds = nil
|
||||
err = spc.Init(ctx)
|
||||
require.NoError(t, err)
|
||||
close(fx.treeManager.waitLoad)
|
||||
|
||||
// waiting until everything is deleted
|
||||
time.Sleep(3 * time.Second)
|
||||
require.Equal(t, len(ids), len(fx.treeManager.markedIds))
|
||||
require.Zero(t, len(fx.treeManager.deletedIds))
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/anyproto/any-sync/commonspace/deletionstate (interfaces: ObjectDeletionState)
|
||||
|
||||
// Package mock_deletionstate is a generated GoMock package.
|
||||
package mock_deletionstate
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
app "github.com/anyproto/any-sync/app"
|
||||
deletionstate "github.com/anyproto/any-sync/commonspace/deletionstate"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockObjectDeletionState is a mock of ObjectDeletionState interface.
|
||||
type MockObjectDeletionState struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockObjectDeletionStateMockRecorder
|
||||
}
|
||||
|
||||
// MockObjectDeletionStateMockRecorder is the mock recorder for MockObjectDeletionState.
|
||||
type MockObjectDeletionStateMockRecorder struct {
|
||||
mock *MockObjectDeletionState
|
||||
}
|
||||
|
||||
// NewMockObjectDeletionState creates a new mock instance.
|
||||
func NewMockObjectDeletionState(ctrl *gomock.Controller) *MockObjectDeletionState {
|
||||
mock := &MockObjectDeletionState{ctrl: ctrl}
|
||||
mock.recorder = &MockObjectDeletionStateMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockObjectDeletionState) EXPECT() *MockObjectDeletionStateMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Add mocks base method.
|
||||
func (m *MockObjectDeletionState) Add(arg0 map[string]struct{}) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Add", arg0)
|
||||
}
|
||||
|
||||
// Add indicates an expected call of Add.
|
||||
func (mr *MockObjectDeletionStateMockRecorder) Add(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockObjectDeletionState)(nil).Add), arg0)
|
||||
}
|
||||
|
||||
// AddObserver mocks base method.
|
||||
func (m *MockObjectDeletionState) AddObserver(arg0 deletionstate.StateUpdateObserver) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "AddObserver", arg0)
|
||||
}
|
||||
|
||||
// AddObserver indicates an expected call of AddObserver.
|
||||
func (mr *MockObjectDeletionStateMockRecorder) AddObserver(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddObserver", reflect.TypeOf((*MockObjectDeletionState)(nil).AddObserver), arg0)
|
||||
}
|
||||
|
||||
// Delete mocks base method.
|
||||
func (m *MockObjectDeletionState) Delete(arg0 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Delete", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete.
|
||||
func (mr *MockObjectDeletionStateMockRecorder) Delete(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockObjectDeletionState)(nil).Delete), arg0)
|
||||
}
|
||||
|
||||
// Exists mocks base method.
|
||||
func (m *MockObjectDeletionState) Exists(arg0 string) bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Exists", arg0)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Exists indicates an expected call of Exists.
|
||||
func (mr *MockObjectDeletionStateMockRecorder) Exists(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockObjectDeletionState)(nil).Exists), arg0)
|
||||
}
|
||||
|
||||
// Filter mocks base method.
|
||||
func (m *MockObjectDeletionState) Filter(arg0 []string) []string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Filter", arg0)
|
||||
ret0, _ := ret[0].([]string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Filter indicates an expected call of Filter.
|
||||
func (mr *MockObjectDeletionStateMockRecorder) Filter(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Filter", reflect.TypeOf((*MockObjectDeletionState)(nil).Filter), arg0)
|
||||
}
|
||||
|
||||
// GetQueued mocks base method.
|
||||
func (m *MockObjectDeletionState) GetQueued() []string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetQueued")
|
||||
ret0, _ := ret[0].([]string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetQueued indicates an expected call of GetQueued.
|
||||
func (mr *MockObjectDeletionStateMockRecorder) GetQueued() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQueued", reflect.TypeOf((*MockObjectDeletionState)(nil).GetQueued))
|
||||
}
|
||||
|
||||
// Init mocks base method.
|
||||
func (m *MockObjectDeletionState) Init(arg0 *app.App) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Init", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Init indicates an expected call of Init.
|
||||
func (mr *MockObjectDeletionStateMockRecorder) Init(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockObjectDeletionState)(nil).Init), arg0)
|
||||
}
|
||||
|
||||
// Name mocks base method.
|
||||
func (m *MockObjectDeletionState) Name() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Name")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Name indicates an expected call of Name.
|
||||
func (mr *MockObjectDeletionStateMockRecorder) Name() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockObjectDeletionState)(nil).Name))
|
||||
}
|
||||
@@ -3,17 +3,18 @@ package headsync
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/anyproto/any-sync/app/ldiff"
|
||||
"github.com/anyproto/any-sync/app/logger"
|
||||
"github.com/anyproto/any-sync/commonspace/credentialprovider"
|
||||
"github.com/anyproto/any-sync/commonspace/deletionstate"
|
||||
"github.com/anyproto/any-sync/commonspace/object/treemanager"
|
||||
"github.com/anyproto/any-sync/commonspace/peermanager"
|
||||
"github.com/anyproto/any-sync/commonspace/spacestorage"
|
||||
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anyproto/any-sync/commonspace/syncstatus"
|
||||
"github.com/anyproto/any-sync/net/peer"
|
||||
"github.com/anyproto/any-sync/net/rpc/rpcerr"
|
||||
"github.com/anytypeio/any-sync/app/ldiff"
|
||||
"github.com/anytypeio/any-sync/app/logger"
|
||||
"github.com/anytypeio/any-sync/commonspace/credentialprovider"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/synctree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/treegetter"
|
||||
"github.com/anytypeio/any-sync/commonspace/peermanager"
|
||||
"github.com/anytypeio/any-sync/commonspace/settings/settingsstate"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacestorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/syncstatus"
|
||||
"github.com/anytypeio/any-sync/net/peer"
|
||||
"github.com/anytypeio/any-sync/net/rpc/rpcerr"
|
||||
"go.uber.org/zap"
|
||||
"time"
|
||||
)
|
||||
@@ -22,22 +23,29 @@ type DiffSyncer interface {
|
||||
Sync(ctx context.Context) error
|
||||
RemoveObjects(ids []string)
|
||||
UpdateHeads(id string, heads []string)
|
||||
Init()
|
||||
Close() error
|
||||
Init(deletionState settingsstate.ObjectDeletionState)
|
||||
}
|
||||
|
||||
func newDiffSyncer(hs *headSync) DiffSyncer {
|
||||
func newDiffSyncer(
|
||||
spaceId string,
|
||||
diff ldiff.Diff,
|
||||
peerManager peermanager.PeerManager,
|
||||
cache treegetter.TreeGetter,
|
||||
storage spacestorage.SpaceStorage,
|
||||
clientFactory spacesyncproto.ClientFactory,
|
||||
syncStatus syncstatus.StatusUpdater,
|
||||
credentialProvider credentialprovider.CredentialProvider,
|
||||
log logger.CtxLogger) DiffSyncer {
|
||||
return &diffSyncer{
|
||||
diff: hs.diff,
|
||||
spaceId: hs.spaceId,
|
||||
treeManager: hs.treeManager,
|
||||
storage: hs.storage,
|
||||
peerManager: hs.peerManager,
|
||||
clientFactory: spacesyncproto.ClientFactoryFunc(spacesyncproto.NewDRPCSpaceSyncClient),
|
||||
credentialProvider: hs.credentialProvider,
|
||||
diff: diff,
|
||||
spaceId: spaceId,
|
||||
cache: cache,
|
||||
storage: storage,
|
||||
peerManager: peerManager,
|
||||
clientFactory: clientFactory,
|
||||
credentialProvider: credentialProvider,
|
||||
log: log,
|
||||
syncStatus: hs.syncStatus,
|
||||
deletionState: hs.deletionState,
|
||||
syncStatus: syncStatus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,19 +53,18 @@ type diffSyncer struct {
|
||||
spaceId string
|
||||
diff ldiff.Diff
|
||||
peerManager peermanager.PeerManager
|
||||
treeManager treemanager.TreeManager
|
||||
cache treegetter.TreeGetter
|
||||
storage spacestorage.SpaceStorage
|
||||
clientFactory spacesyncproto.ClientFactory
|
||||
log logger.CtxLogger
|
||||
deletionState deletionstate.ObjectDeletionState
|
||||
deletionState settingsstate.ObjectDeletionState
|
||||
credentialProvider credentialprovider.CredentialProvider
|
||||
syncStatus syncstatus.StatusUpdater
|
||||
treeSyncer treemanager.TreeSyncer
|
||||
}
|
||||
|
||||
func (d *diffSyncer) Init() {
|
||||
func (d *diffSyncer) Init(deletionState settingsstate.ObjectDeletionState) {
|
||||
d.deletionState = deletionState
|
||||
d.deletionState.AddObserver(d.RemoveObjects)
|
||||
d.treeSyncer = d.treeManager.NewTreeSyncer(d.spaceId, d.treeManager)
|
||||
}
|
||||
|
||||
func (d *diffSyncer) RemoveObjects(ids []string) {
|
||||
@@ -106,14 +113,8 @@ func (d *diffSyncer) Sync(ctx context.Context) error {
|
||||
|
||||
func (d *diffSyncer) syncWithPeer(ctx context.Context, p peer.Peer) (err error) {
|
||||
ctx = logger.CtxWithFields(ctx, zap.String("peerId", p.Id()))
|
||||
conn, err := p.AcquireDrpcConn(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer p.ReleaseDrpcConn(conn)
|
||||
|
||||
var (
|
||||
cl = d.clientFactory.Client(conn)
|
||||
cl = d.clientFactory.Client(p)
|
||||
rdiff = NewRemoteDiff(d.spaceId, cl)
|
||||
stateCounter = d.syncStatus.StateCounter()
|
||||
)
|
||||
@@ -123,7 +124,7 @@ func (d *diffSyncer) syncWithPeer(ctx context.Context, p peer.Peer) (err error)
|
||||
if err != nil && err != spacesyncproto.ErrSpaceMissing {
|
||||
if err == spacesyncproto.ErrSpaceIsDeleted {
|
||||
d.log.Debug("got space deleted while syncing")
|
||||
d.treeSyncer.SyncAll(ctx, p.Id(), []string{d.storage.SpaceSettingsId()}, nil)
|
||||
d.syncTrees(ctx, p.Id(), []string{d.storage.SpaceSettingsId()})
|
||||
}
|
||||
d.syncStatus.SetNodesOnline(p.Id(), false)
|
||||
return fmt.Errorf("diff error: %v", err)
|
||||
@@ -136,24 +137,45 @@ func (d *diffSyncer) syncWithPeer(ctx context.Context, p peer.Peer) (err error)
|
||||
|
||||
totalLen := len(newIds) + len(changedIds) + len(removedIds)
|
||||
// not syncing ids which were removed through settings document
|
||||
missingIds := d.deletionState.Filter(newIds)
|
||||
existingIds := append(d.deletionState.Filter(removedIds), d.deletionState.Filter(changedIds)...)
|
||||
filteredIds := d.deletionState.FilterJoin(newIds, changedIds, removedIds)
|
||||
|
||||
d.syncStatus.RemoveAllExcept(p.Id(), existingIds, stateCounter)
|
||||
d.syncStatus.RemoveAllExcept(p.Id(), filteredIds, stateCounter)
|
||||
|
||||
d.syncTrees(ctx, p.Id(), filteredIds)
|
||||
|
||||
err = d.treeSyncer.SyncAll(ctx, p.Id(), existingIds, missingIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.log.Info("sync done:", zap.Int("newIds", len(newIds)),
|
||||
zap.Int("changedIds", len(changedIds)),
|
||||
zap.Int("removedIds", len(removedIds)),
|
||||
zap.Int("already deleted ids", totalLen-len(existingIds)-len(missingIds)),
|
||||
zap.Int("already deleted ids", totalLen-len(filteredIds)),
|
||||
zap.String("peerId", p.Id()),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *diffSyncer) syncTrees(ctx context.Context, peerId string, trees []string) {
|
||||
for _, tId := range trees {
|
||||
tree, err := d.cache.GetTree(ctx, d.spaceId, tId)
|
||||
if err != nil {
|
||||
d.log.InfoCtx(ctx, "can't load tree", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
syncTree, ok := tree.(synctree.SyncTree)
|
||||
if !ok {
|
||||
d.log.InfoCtx(ctx, "not a sync tree", zap.String("objectId", tId))
|
||||
continue
|
||||
}
|
||||
// the idea why we call it directly is that if we try to get it from cache
|
||||
// it may be already there (i.e. loaded)
|
||||
// and build func will not be called, thus we won't sync the tree
|
||||
// therefore we just do it manually
|
||||
if err = syncTree.SyncWithPeer(ctx, peerId); err != nil {
|
||||
d.log.WarnCtx(ctx, "synctree.SyncWithPeer error", zap.Error(err), zap.String("treeId", tId))
|
||||
} else {
|
||||
d.log.DebugCtx(ctx, "success synctree.SyncWithPeer", zap.String("treeId", tId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *diffSyncer) sendPushSpaceRequest(ctx context.Context, peerId string, cl spacesyncproto.DRPCSpaceSyncClient) (err error) {
|
||||
aclStorage, err := d.storage.AclStorage()
|
||||
if err != nil {
|
||||
@@ -216,7 +238,3 @@ func (d *diffSyncer) subscribe(ctx context.Context, peerId string) (err error) {
|
||||
Payload: payload,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *diffSyncer) Close() error {
|
||||
return d.treeSyncer.Close()
|
||||
}
|
||||
@@ -4,19 +4,28 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/anytypeio/any-sync/app/ldiff"
|
||||
"github.com/anytypeio/any-sync/app/ldiff/mock_ldiff"
|
||||
"github.com/anytypeio/any-sync/app/logger"
|
||||
"github.com/anytypeio/any-sync/commonspace/credentialprovider/mock_credentialprovider"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/liststorage/mock_liststorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
mock_treestorage "github.com/anytypeio/any-sync/commonspace/object/tree/treestorage/mock_treestorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/treegetter/mock_treegetter"
|
||||
"github.com/anytypeio/any-sync/commonspace/peermanager/mock_peermanager"
|
||||
"github.com/anytypeio/any-sync/commonspace/settings/settingsstate/mock_settingsstate"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacestorage/mock_spacestorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacesyncproto/mock_spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/syncstatus"
|
||||
"github.com/anytypeio/any-sync/net/peer"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/libp2p/go-libp2p/core/sec"
|
||||
"github.com/stretchr/testify/require"
|
||||
"storj.io/drpc"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/app/ldiff"
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/liststorage/mock_liststorage"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage/mock_treestorage"
|
||||
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anyproto/any-sync/consensus/consensusproto"
|
||||
"github.com/anyproto/any-sync/net/peer"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"storj.io/drpc"
|
||||
)
|
||||
|
||||
type pushSpaceRequestMatcher struct {
|
||||
@@ -27,6 +36,56 @@ type pushSpaceRequestMatcher struct {
|
||||
spaceHeader *spacesyncproto.RawSpaceHeaderWithId
|
||||
}
|
||||
|
||||
func (p pushSpaceRequestMatcher) Matches(x interface{}) bool {
|
||||
res, ok := x.(*spacesyncproto.SpacePushRequest)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return res.Payload.AclPayloadId == p.aclRootId && res.Payload.SpaceHeader == p.spaceHeader && res.Payload.SpaceSettingsPayloadId == p.settingsId && bytes.Equal(p.credential, res.Credential)
|
||||
}
|
||||
|
||||
func (p pushSpaceRequestMatcher) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type mockPeer struct{}
|
||||
|
||||
func (m mockPeer) TryClose(objectTTL time.Duration) (res bool, err error) {
|
||||
return true, m.Close()
|
||||
}
|
||||
|
||||
func (m mockPeer) Id() string {
|
||||
return "mockId"
|
||||
}
|
||||
|
||||
func (m mockPeer) LastUsage() time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (m mockPeer) Secure() sec.SecureConn {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockPeer) UpdateLastUsage() {
|
||||
}
|
||||
|
||||
func (m mockPeer) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockPeer) Closed() <-chan struct{} {
|
||||
return make(chan struct{})
|
||||
}
|
||||
|
||||
func (m mockPeer) Invoke(ctx context.Context, rpc string, enc drpc.Encoding, in, out drpc.Message) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockPeer) NewStream(ctx context.Context, rpc string, enc drpc.Encoding) (drpc.Stream, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func newPushSpaceRequestMatcher(
|
||||
spaceId string,
|
||||
aclRootId string,
|
||||
@@ -42,135 +101,80 @@ func newPushSpaceRequestMatcher(
|
||||
}
|
||||
}
|
||||
|
||||
func (p pushSpaceRequestMatcher) Matches(x interface{}) bool {
|
||||
res, ok := x.(*spacesyncproto.SpacePushRequest)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return res.Payload.AclPayloadId == p.aclRootId && res.Payload.SpaceHeader == p.spaceHeader && res.Payload.SpaceSettingsPayloadId == p.settingsId && bytes.Equal(p.credential, res.Credential)
|
||||
}
|
||||
|
||||
func (p pushSpaceRequestMatcher) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type mockPeer struct {
|
||||
}
|
||||
|
||||
func (m mockPeer) Id() string {
|
||||
return "peerId"
|
||||
}
|
||||
|
||||
func (m mockPeer) Context() context.Context {
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
func (m mockPeer) AcquireDrpcConn(ctx context.Context) (drpc.Conn, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m mockPeer) ReleaseDrpcConn(conn drpc.Conn) {
|
||||
return
|
||||
}
|
||||
|
||||
func (m mockPeer) DoDrpc(ctx context.Context, do func(conn drpc.Conn) error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockPeer) IsClosed() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m mockPeer) TryClose(objectTTL time.Duration) (res bool, err error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
func (m mockPeer) Close() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fx *headSyncFixture) initDiffSyncer(t *testing.T) {
|
||||
fx.init(t)
|
||||
fx.diffSyncer = newDiffSyncer(fx.headSync).(*diffSyncer)
|
||||
fx.diffSyncer.clientFactory = spacesyncproto.ClientFactoryFunc(func(cc drpc.Conn) spacesyncproto.DRPCSpaceSyncClient {
|
||||
return fx.clientMock
|
||||
})
|
||||
fx.deletionStateMock.EXPECT().AddObserver(gomock.Any())
|
||||
fx.treeManagerMock.EXPECT().NewTreeSyncer(fx.spaceState.SpaceId, fx.treeManagerMock).Return(fx.treeSyncerMock)
|
||||
fx.diffSyncer.Init()
|
||||
}
|
||||
|
||||
func TestDiffSyncer(t *testing.T) {
|
||||
func TestDiffSyncer_Sync(t *testing.T) {
|
||||
// setup
|
||||
ctx := context.Background()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
diffMock := mock_ldiff.NewMockDiff(ctrl)
|
||||
peerManagerMock := mock_peermanager.NewMockPeerManager(ctrl)
|
||||
cacheMock := mock_treegetter.NewMockTreeGetter(ctrl)
|
||||
stMock := mock_spacestorage.NewMockSpaceStorage(ctrl)
|
||||
clientMock := mock_spacesyncproto.NewMockDRPCSpaceSyncClient(ctrl)
|
||||
factory := spacesyncproto.ClientFactoryFunc(func(cc drpc.Conn) spacesyncproto.DRPCSpaceSyncClient {
|
||||
return clientMock
|
||||
})
|
||||
credentialProvider := mock_credentialprovider.NewMockCredentialProvider(ctrl)
|
||||
delState := mock_settingsstate.NewMockObjectDeletionState(ctrl)
|
||||
spaceId := "spaceId"
|
||||
aclRootId := "aclRootId"
|
||||
l := logger.NewNamed(spaceId)
|
||||
diffSyncer := newDiffSyncer(spaceId, diffMock, peerManagerMock, cacheMock, stMock, factory, syncstatus.NewNoOpSyncStatus(), credentialProvider, l)
|
||||
delState.EXPECT().AddObserver(gomock.Any())
|
||||
diffSyncer.Init(delState)
|
||||
|
||||
t.Run("diff syncer sync", func(t *testing.T) {
|
||||
fx := newHeadSyncFixture(t)
|
||||
fx.initDiffSyncer(t)
|
||||
defer fx.stop()
|
||||
mPeer := mockPeer{}
|
||||
fx.peerManagerMock.EXPECT().
|
||||
peerManagerMock.EXPECT().
|
||||
GetResponsiblePeers(gomock.Any()).
|
||||
Return([]peer.Peer{mPeer}, nil)
|
||||
fx.diffMock.EXPECT().
|
||||
Diff(gomock.Any(), gomock.Eq(NewRemoteDiff(fx.spaceState.SpaceId, fx.clientMock))).
|
||||
Return([]peer.Peer{mockPeer{}}, nil)
|
||||
diffMock.EXPECT().
|
||||
Diff(gomock.Any(), gomock.Eq(NewRemoteDiff(spaceId, clientMock))).
|
||||
Return([]string{"new"}, []string{"changed"}, nil, nil)
|
||||
fx.deletionStateMock.EXPECT().Filter([]string{"new"}).Return([]string{"new"}).Times(1)
|
||||
fx.deletionStateMock.EXPECT().Filter([]string{"changed"}).Return([]string{"changed"}).Times(1)
|
||||
fx.deletionStateMock.EXPECT().Filter(nil).Return(nil).Times(1)
|
||||
fx.treeSyncerMock.EXPECT().SyncAll(gomock.Any(), mPeer.Id(), []string{"changed"}, []string{"new"}).Return(nil)
|
||||
require.NoError(t, fx.diffSyncer.Sync(ctx))
|
||||
delState.EXPECT().FilterJoin(gomock.Any()).Return([]string{"new", "changed"})
|
||||
for _, arg := range []string{"new", "changed"} {
|
||||
cacheMock.EXPECT().
|
||||
GetTree(gomock.Any(), spaceId, arg).
|
||||
Return(nil, nil)
|
||||
}
|
||||
require.NoError(t, diffSyncer.Sync(ctx))
|
||||
})
|
||||
|
||||
t.Run("diff syncer sync conf error", func(t *testing.T) {
|
||||
fx := newHeadSyncFixture(t)
|
||||
fx.initDiffSyncer(t)
|
||||
defer fx.stop()
|
||||
ctx := context.Background()
|
||||
fx.peerManagerMock.EXPECT().
|
||||
peerManagerMock.EXPECT().
|
||||
GetResponsiblePeers(gomock.Any()).
|
||||
Return(nil, fmt.Errorf("some error"))
|
||||
|
||||
require.Error(t, fx.diffSyncer.Sync(ctx))
|
||||
require.Error(t, diffSyncer.Sync(ctx))
|
||||
})
|
||||
|
||||
t.Run("deletion state remove objects", func(t *testing.T) {
|
||||
fx := newHeadSyncFixture(t)
|
||||
fx.initDiffSyncer(t)
|
||||
defer fx.stop()
|
||||
deletedId := "id"
|
||||
fx.deletionStateMock.EXPECT().Exists(deletedId).Return(true)
|
||||
delState.EXPECT().Exists(deletedId).Return(true)
|
||||
|
||||
// this should not result in any mock being called
|
||||
fx.diffSyncer.UpdateHeads(deletedId, []string{"someHead"})
|
||||
diffSyncer.UpdateHeads(deletedId, []string{"someHead"})
|
||||
})
|
||||
|
||||
t.Run("update heads updates diff", func(t *testing.T) {
|
||||
fx := newHeadSyncFixture(t)
|
||||
fx.initDiffSyncer(t)
|
||||
defer fx.stop()
|
||||
newId := "newId"
|
||||
newHeads := []string{"h1", "h2"}
|
||||
hash := "hash"
|
||||
fx.diffMock.EXPECT().Set(ldiff.Element{
|
||||
diffMock.EXPECT().Set(ldiff.Element{
|
||||
Id: newId,
|
||||
Head: concatStrings(newHeads),
|
||||
})
|
||||
fx.diffMock.EXPECT().Hash().Return(hash)
|
||||
fx.deletionStateMock.EXPECT().Exists(newId).Return(false)
|
||||
fx.storageMock.EXPECT().WriteSpaceHash(hash)
|
||||
fx.diffSyncer.UpdateHeads(newId, newHeads)
|
||||
diffMock.EXPECT().Hash().Return(hash)
|
||||
delState.EXPECT().Exists(newId).Return(false)
|
||||
stMock.EXPECT().WriteSpaceHash(hash)
|
||||
diffSyncer.UpdateHeads(newId, newHeads)
|
||||
})
|
||||
|
||||
t.Run("diff syncer sync space missing", func(t *testing.T) {
|
||||
fx := newHeadSyncFixture(t)
|
||||
fx.initDiffSyncer(t)
|
||||
defer fx.stop()
|
||||
aclStorageMock := mock_liststorage.NewMockListStorage(fx.ctrl)
|
||||
settingsStorage := mock_treestorage.NewMockTreeStorage(fx.ctrl)
|
||||
aclStorageMock := mock_liststorage.NewMockListStorage(ctrl)
|
||||
settingsStorage := mock_treestorage.NewMockTreeStorage(ctrl)
|
||||
settingsId := "settingsId"
|
||||
aclRootId := "aclRootId"
|
||||
aclRoot := &consensusproto.RawRecordWithId{
|
||||
aclRoot := &aclrecordproto.RawAclRecordWithId{
|
||||
Id: aclRootId,
|
||||
}
|
||||
settingsRoot := &treechangeproto.RawTreeChangeWithId{
|
||||
@@ -180,61 +184,56 @@ func TestDiffSyncer(t *testing.T) {
|
||||
spaceSettingsId := "spaceSettingsId"
|
||||
credential := []byte("credential")
|
||||
|
||||
fx.peerManagerMock.EXPECT().
|
||||
peerManagerMock.EXPECT().
|
||||
GetResponsiblePeers(gomock.Any()).
|
||||
Return([]peer.Peer{mockPeer{}}, nil)
|
||||
fx.diffMock.EXPECT().
|
||||
Diff(gomock.Any(), gomock.Eq(NewRemoteDiff(fx.spaceState.SpaceId, fx.clientMock))).
|
||||
diffMock.EXPECT().
|
||||
Diff(gomock.Any(), gomock.Eq(NewRemoteDiff(spaceId, clientMock))).
|
||||
Return(nil, nil, nil, spacesyncproto.ErrSpaceMissing)
|
||||
|
||||
fx.storageMock.EXPECT().AclStorage().Return(aclStorageMock, nil)
|
||||
fx.storageMock.EXPECT().SpaceHeader().Return(spaceHeader, nil)
|
||||
fx.storageMock.EXPECT().SpaceSettingsId().Return(spaceSettingsId)
|
||||
fx.storageMock.EXPECT().TreeStorage(spaceSettingsId).Return(settingsStorage, nil)
|
||||
stMock.EXPECT().AclStorage().Return(aclStorageMock, nil)
|
||||
stMock.EXPECT().SpaceHeader().Return(spaceHeader, nil)
|
||||
stMock.EXPECT().SpaceSettingsId().Return(spaceSettingsId)
|
||||
stMock.EXPECT().TreeStorage(spaceSettingsId).Return(settingsStorage, nil)
|
||||
|
||||
settingsStorage.EXPECT().Root().Return(settingsRoot, nil)
|
||||
aclStorageMock.EXPECT().
|
||||
Root().
|
||||
Return(aclRoot, nil)
|
||||
fx.credentialProviderMock.EXPECT().
|
||||
credentialProvider.EXPECT().
|
||||
GetCredential(gomock.Any(), spaceHeader).
|
||||
Return(credential, nil)
|
||||
fx.clientMock.EXPECT().
|
||||
SpacePush(gomock.Any(), newPushSpaceRequestMatcher(fx.spaceState.SpaceId, aclRootId, settingsId, credential, spaceHeader)).
|
||||
clientMock.EXPECT().
|
||||
SpacePush(gomock.Any(), newPushSpaceRequestMatcher(spaceId, aclRootId, settingsId, credential, spaceHeader)).
|
||||
Return(nil, nil)
|
||||
fx.peerManagerMock.EXPECT().SendPeer(gomock.Any(), "peerId", gomock.Any())
|
||||
peerManagerMock.EXPECT().SendPeer(gomock.Any(), "mockId", gomock.Any())
|
||||
|
||||
require.NoError(t, fx.diffSyncer.Sync(ctx))
|
||||
require.NoError(t, diffSyncer.Sync(ctx))
|
||||
})
|
||||
|
||||
t.Run("diff syncer sync unexpected", func(t *testing.T) {
|
||||
fx := newHeadSyncFixture(t)
|
||||
fx.initDiffSyncer(t)
|
||||
defer fx.stop()
|
||||
fx.peerManagerMock.EXPECT().
|
||||
peerManagerMock.EXPECT().
|
||||
GetResponsiblePeers(gomock.Any()).
|
||||
Return([]peer.Peer{mockPeer{}}, nil)
|
||||
fx.diffMock.EXPECT().
|
||||
Diff(gomock.Any(), gomock.Eq(NewRemoteDiff(fx.spaceState.SpaceId, fx.clientMock))).
|
||||
diffMock.EXPECT().
|
||||
Diff(gomock.Any(), gomock.Eq(NewRemoteDiff(spaceId, clientMock))).
|
||||
Return(nil, nil, nil, spacesyncproto.ErrUnexpected)
|
||||
|
||||
require.NoError(t, fx.diffSyncer.Sync(ctx))
|
||||
require.NoError(t, diffSyncer.Sync(ctx))
|
||||
})
|
||||
|
||||
t.Run("diff syncer sync space is deleted error", func(t *testing.T) {
|
||||
fx := newHeadSyncFixture(t)
|
||||
fx.initDiffSyncer(t)
|
||||
defer fx.stop()
|
||||
mPeer := mockPeer{}
|
||||
fx.peerManagerMock.EXPECT().
|
||||
peerManagerMock.EXPECT().
|
||||
GetResponsiblePeers(gomock.Any()).
|
||||
Return([]peer.Peer{mPeer}, nil)
|
||||
fx.diffMock.EXPECT().
|
||||
Diff(gomock.Any(), gomock.Eq(NewRemoteDiff(fx.spaceState.SpaceId, fx.clientMock))).
|
||||
Return([]peer.Peer{mockPeer{}}, nil)
|
||||
diffMock.EXPECT().
|
||||
Diff(gomock.Any(), gomock.Eq(NewRemoteDiff(spaceId, clientMock))).
|
||||
Return(nil, nil, nil, spacesyncproto.ErrSpaceIsDeleted)
|
||||
fx.storageMock.EXPECT().SpaceSettingsId().Return("settingsId")
|
||||
fx.treeSyncerMock.EXPECT().SyncAll(gomock.Any(), mPeer.Id(), []string{"settingsId"}, nil).Return(nil)
|
||||
stMock.EXPECT().SpaceSettingsId().Return("settingsId")
|
||||
cacheMock.EXPECT().
|
||||
GetTree(gomock.Any(), spaceId, "settingsId").
|
||||
Return(nil, nil)
|
||||
|
||||
require.NoError(t, fx.diffSyncer.Sync(ctx))
|
||||
require.NoError(t, diffSyncer.Sync(ctx))
|
||||
})
|
||||
}
|
||||
+105
-103
@@ -1,147 +1,125 @@
|
||||
//go:generate mockgen -destination mock_headsync/mock_headsync.go github.com/anyproto/any-sync/commonspace/headsync DiffSyncer
|
||||
//go:generate mockgen -destination mock_headsync/mock_headsync.go github.com/anytypeio/any-sync/commonspace/headsync DiffSyncer
|
||||
package headsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/anyproto/any-sync/app"
|
||||
"github.com/anyproto/any-sync/app/ldiff"
|
||||
"github.com/anyproto/any-sync/app/logger"
|
||||
config2 "github.com/anyproto/any-sync/commonspace/config"
|
||||
"github.com/anyproto/any-sync/commonspace/credentialprovider"
|
||||
"github.com/anyproto/any-sync/commonspace/deletionstate"
|
||||
"github.com/anyproto/any-sync/commonspace/object/treemanager"
|
||||
"github.com/anyproto/any-sync/commonspace/peermanager"
|
||||
"github.com/anyproto/any-sync/commonspace/spacestate"
|
||||
"github.com/anyproto/any-sync/commonspace/spacestorage"
|
||||
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anyproto/any-sync/commonspace/syncstatus"
|
||||
"github.com/anyproto/any-sync/net/peer"
|
||||
"github.com/anyproto/any-sync/nodeconf"
|
||||
"github.com/anyproto/any-sync/util/periodicsync"
|
||||
"github.com/anyproto/any-sync/util/slice"
|
||||
"github.com/anytypeio/any-sync/app/ldiff"
|
||||
"github.com/anytypeio/any-sync/app/logger"
|
||||
"github.com/anytypeio/any-sync/commonspace/credentialprovider"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/treegetter"
|
||||
"github.com/anytypeio/any-sync/commonspace/peermanager"
|
||||
"github.com/anytypeio/any-sync/commonspace/settings/settingsstate"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacestorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/syncstatus"
|
||||
"github.com/anytypeio/any-sync/net/peer"
|
||||
"github.com/anytypeio/any-sync/nodeconf"
|
||||
"github.com/anytypeio/any-sync/util/periodicsync"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/exp/slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var log = logger.NewNamed(CName)
|
||||
|
||||
const CName = "common.commonspace.headsync"
|
||||
|
||||
type TreeHeads struct {
|
||||
Id string
|
||||
Heads []string
|
||||
}
|
||||
|
||||
type HeadSync interface {
|
||||
app.ComponentRunnable
|
||||
ExternalIds() []string
|
||||
DebugAllHeads() (res []TreeHeads)
|
||||
AllIds() []string
|
||||
Init(objectIds []string, deletionState settingsstate.ObjectDeletionState)
|
||||
|
||||
UpdateHeads(id string, heads []string)
|
||||
HandleRangeRequest(ctx context.Context, req *spacesyncproto.HeadSyncRequest) (resp *spacesyncproto.HeadSyncResponse, err error)
|
||||
RemoveObjects(ids []string)
|
||||
AllIds() []string
|
||||
DebugAllHeads() (res []TreeHeads)
|
||||
|
||||
Close() (err error)
|
||||
}
|
||||
|
||||
type headSync struct {
|
||||
spaceId string
|
||||
periodicSync periodicsync.PeriodicSync
|
||||
storage spacestorage.SpaceStorage
|
||||
diff ldiff.Diff
|
||||
log logger.CtxLogger
|
||||
syncer DiffSyncer
|
||||
configuration nodeconf.Configuration
|
||||
spaceIsDeleted *atomic.Bool
|
||||
syncPeriod int
|
||||
|
||||
periodicSync periodicsync.PeriodicSync
|
||||
storage spacestorage.SpaceStorage
|
||||
diff ldiff.Diff
|
||||
log logger.CtxLogger
|
||||
syncer DiffSyncer
|
||||
configuration nodeconf.NodeConf
|
||||
peerManager peermanager.PeerManager
|
||||
treeManager treemanager.TreeManager
|
||||
credentialProvider credentialprovider.CredentialProvider
|
||||
syncStatus syncstatus.StatusService
|
||||
deletionState deletionstate.ObjectDeletionState
|
||||
syncPeriod int
|
||||
}
|
||||
|
||||
func New() HeadSync {
|
||||
return &headSync{}
|
||||
}
|
||||
func NewHeadSync(
|
||||
spaceId string,
|
||||
spaceIsDeleted *atomic.Bool,
|
||||
syncPeriod int,
|
||||
configuration nodeconf.Configuration,
|
||||
storage spacestorage.SpaceStorage,
|
||||
peerManager peermanager.PeerManager,
|
||||
cache treegetter.TreeGetter,
|
||||
syncStatus syncstatus.StatusUpdater,
|
||||
credentialProvider credentialprovider.CredentialProvider,
|
||||
log logger.CtxLogger) HeadSync {
|
||||
|
||||
var createDiffSyncer = newDiffSyncer
|
||||
|
||||
func (h *headSync) Init(a *app.App) (err error) {
|
||||
shared := a.MustComponent(spacestate.CName).(*spacestate.SpaceState)
|
||||
cfg := a.MustComponent("config").(config2.ConfigGetter)
|
||||
h.spaceId = shared.SpaceId
|
||||
h.spaceIsDeleted = shared.SpaceIsDeleted
|
||||
h.syncPeriod = cfg.GetSpace().SyncPeriod
|
||||
h.configuration = a.MustComponent(nodeconf.CName).(nodeconf.NodeConf)
|
||||
h.log = log.With(zap.String("spaceId", h.spaceId))
|
||||
h.storage = a.MustComponent(spacestorage.CName).(spacestorage.SpaceStorage)
|
||||
h.diff = ldiff.New(16, 16)
|
||||
h.peerManager = a.MustComponent(peermanager.CName).(peermanager.PeerManager)
|
||||
h.credentialProvider = a.MustComponent(credentialprovider.CName).(credentialprovider.CredentialProvider)
|
||||
h.syncStatus = a.MustComponent(syncstatus.CName).(syncstatus.StatusService)
|
||||
h.treeManager = a.MustComponent(treemanager.CName).(treemanager.TreeManager)
|
||||
h.deletionState = a.MustComponent(deletionstate.CName).(deletionstate.ObjectDeletionState)
|
||||
h.syncer = createDiffSyncer(h)
|
||||
diff := ldiff.New(16, 16)
|
||||
l := log.With(zap.String("spaceId", spaceId))
|
||||
factory := spacesyncproto.ClientFactoryFunc(spacesyncproto.NewDRPCSpaceSyncClient)
|
||||
syncer := newDiffSyncer(spaceId, diff, peerManager, cache, storage, factory, syncStatus, credentialProvider, l)
|
||||
sync := func(ctx context.Context) (err error) {
|
||||
// for clients cancelling the sync process
|
||||
if h.spaceIsDeleted.Load() && !h.configuration.IsResponsible(h.spaceId) {
|
||||
if spaceIsDeleted.Load() && !configuration.IsResponsible(spaceId) {
|
||||
return spacesyncproto.ErrSpaceIsDeleted
|
||||
}
|
||||
return h.syncer.Sync(ctx)
|
||||
return syncer.Sync(ctx)
|
||||
}
|
||||
h.periodicSync = periodicsync.NewPeriodicSync(h.syncPeriod, time.Minute, sync, h.log)
|
||||
// TODO: move to run?
|
||||
h.syncer.Init()
|
||||
return nil
|
||||
}
|
||||
periodicSync := periodicsync.NewPeriodicSync(syncPeriod, time.Minute, sync, l)
|
||||
|
||||
func (h *headSync) Name() (name string) {
|
||||
return CName
|
||||
}
|
||||
|
||||
func (h *headSync) Run(ctx context.Context) (err error) {
|
||||
initialIds, err := h.storage.StoredIds()
|
||||
if err != nil {
|
||||
return
|
||||
return &headSync{
|
||||
spaceId: spaceId,
|
||||
storage: storage,
|
||||
syncer: syncer,
|
||||
periodicSync: periodicSync,
|
||||
diff: diff,
|
||||
log: log,
|
||||
syncPeriod: syncPeriod,
|
||||
configuration: configuration,
|
||||
spaceIsDeleted: spaceIsDeleted,
|
||||
}
|
||||
h.fillDiff(initialIds)
|
||||
h.periodicSync.Run()
|
||||
return
|
||||
}
|
||||
|
||||
func (h *headSync) HandleRangeRequest(ctx context.Context, req *spacesyncproto.HeadSyncRequest) (resp *spacesyncproto.HeadSyncResponse, err error) {
|
||||
if h.spaceIsDeleted.Load() {
|
||||
func (d *headSync) Init(objectIds []string, deletionState settingsstate.ObjectDeletionState) {
|
||||
d.fillDiff(objectIds)
|
||||
d.syncer.Init(deletionState)
|
||||
d.periodicSync.Run()
|
||||
}
|
||||
|
||||
func (d *headSync) HandleRangeRequest(ctx context.Context, req *spacesyncproto.HeadSyncRequest) (resp *spacesyncproto.HeadSyncResponse, err error) {
|
||||
if d.spaceIsDeleted.Load() {
|
||||
peerId, err := peer.CtxPeerId(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// stop receiving all request for sync from clients
|
||||
if !slices.Contains(h.configuration.NodeIds(h.spaceId), peerId) {
|
||||
if !slices.Contains(d.configuration.NodeIds(d.spaceId), peerId) {
|
||||
return nil, spacesyncproto.ErrSpaceIsDeleted
|
||||
}
|
||||
}
|
||||
return HandleRangeRequest(ctx, h.diff, req)
|
||||
return HandleRangeRequest(ctx, d.diff, req)
|
||||
}
|
||||
|
||||
func (h *headSync) UpdateHeads(id string, heads []string) {
|
||||
h.syncer.UpdateHeads(id, heads)
|
||||
func (d *headSync) UpdateHeads(id string, heads []string) {
|
||||
d.syncer.UpdateHeads(id, heads)
|
||||
}
|
||||
|
||||
func (h *headSync) AllIds() []string {
|
||||
return h.diff.Ids()
|
||||
func (d *headSync) AllIds() []string {
|
||||
return d.diff.Ids()
|
||||
}
|
||||
|
||||
func (h *headSync) ExternalIds() []string {
|
||||
settingsId := h.storage.SpaceSettingsId()
|
||||
return slice.DiscardFromSlice(h.AllIds(), func(id string) bool {
|
||||
return id == settingsId
|
||||
})
|
||||
}
|
||||
|
||||
func (h *headSync) DebugAllHeads() (res []TreeHeads) {
|
||||
els := h.diff.Elements()
|
||||
func (d *headSync) DebugAllHeads() (res []TreeHeads) {
|
||||
els := d.diff.Elements()
|
||||
for _, el := range els {
|
||||
idHead := TreeHeads{
|
||||
Id: el.Id,
|
||||
@@ -152,19 +130,19 @@ func (h *headSync) DebugAllHeads() (res []TreeHeads) {
|
||||
return
|
||||
}
|
||||
|
||||
func (h *headSync) RemoveObjects(ids []string) {
|
||||
h.syncer.RemoveObjects(ids)
|
||||
func (d *headSync) RemoveObjects(ids []string) {
|
||||
d.syncer.RemoveObjects(ids)
|
||||
}
|
||||
|
||||
func (h *headSync) Close(ctx context.Context) (err error) {
|
||||
h.periodicSync.Close()
|
||||
return h.syncer.Close()
|
||||
func (d *headSync) Close() (err error) {
|
||||
d.periodicSync.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *headSync) fillDiff(objectIds []string) {
|
||||
func (d *headSync) fillDiff(objectIds []string) {
|
||||
var els = make([]ldiff.Element, 0, len(objectIds))
|
||||
for _, id := range objectIds {
|
||||
st, err := h.storage.TreeStorage(id)
|
||||
st, err := d.storage.TreeStorage(id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -177,8 +155,32 @@ func (h *headSync) fillDiff(objectIds []string) {
|
||||
Head: concatStrings(heads),
|
||||
})
|
||||
}
|
||||
h.diff.Set(els...)
|
||||
if err := h.storage.WriteSpaceHash(h.diff.Hash()); err != nil {
|
||||
h.log.Error("can't write space hash", zap.Error(err))
|
||||
d.diff.Set(els...)
|
||||
if err := d.storage.WriteSpaceHash(d.diff.Hash()); err != nil {
|
||||
d.log.Error("can't write space hash", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func concatStrings(strs []string) string {
|
||||
var (
|
||||
b strings.Builder
|
||||
totalLen int
|
||||
)
|
||||
for _, s := range strs {
|
||||
totalLen += len(s)
|
||||
}
|
||||
|
||||
b.Grow(totalLen)
|
||||
for _, s := range strs {
|
||||
b.WriteString(s)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func splitString(str string) (res []string) {
|
||||
const cidLen = 59
|
||||
for i := 0; i < len(str); i += cidLen {
|
||||
res = append(res, str[i:i+cidLen])
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,179 +1,70 @@
|
||||
package headsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/anyproto/any-sync/app"
|
||||
"github.com/anyproto/any-sync/app/ldiff"
|
||||
"github.com/anyproto/any-sync/app/ldiff/mock_ldiff"
|
||||
"github.com/anyproto/any-sync/commonspace/config"
|
||||
"github.com/anyproto/any-sync/commonspace/credentialprovider"
|
||||
"github.com/anyproto/any-sync/commonspace/credentialprovider/mock_credentialprovider"
|
||||
"github.com/anyproto/any-sync/commonspace/deletionstate"
|
||||
"github.com/anyproto/any-sync/commonspace/deletionstate/mock_deletionstate"
|
||||
"github.com/anyproto/any-sync/commonspace/headsync/mock_headsync"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage/mock_treestorage"
|
||||
"github.com/anyproto/any-sync/commonspace/object/treemanager"
|
||||
"github.com/anyproto/any-sync/commonspace/object/treemanager/mock_treemanager"
|
||||
"github.com/anyproto/any-sync/commonspace/peermanager"
|
||||
"github.com/anyproto/any-sync/commonspace/peermanager/mock_peermanager"
|
||||
"github.com/anyproto/any-sync/commonspace/spacestate"
|
||||
"github.com/anyproto/any-sync/commonspace/spacestorage"
|
||||
"github.com/anyproto/any-sync/commonspace/spacestorage/mock_spacestorage"
|
||||
"github.com/anyproto/any-sync/commonspace/spacesyncproto/mock_spacesyncproto"
|
||||
"github.com/anyproto/any-sync/commonspace/syncstatus"
|
||||
"github.com/anyproto/any-sync/nodeconf"
|
||||
"github.com/anyproto/any-sync/nodeconf/mock_nodeconf"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"sync/atomic"
|
||||
"github.com/anytypeio/any-sync/app/ldiff"
|
||||
"github.com/anytypeio/any-sync/app/ldiff/mock_ldiff"
|
||||
"github.com/anytypeio/any-sync/app/logger"
|
||||
"github.com/anytypeio/any-sync/commonspace/headsync/mock_headsync"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage/mock_treestorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/settings/settingsstate/mock_settingsstate"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacestorage/mock_spacestorage"
|
||||
"github.com/anytypeio/any-sync/util/periodicsync/mock_periodicsync"
|
||||
"github.com/golang/mock/gomock"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockConfig struct {
|
||||
}
|
||||
|
||||
func (m mockConfig) Init(a *app.App) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m mockConfig) Name() (name string) {
|
||||
return "config"
|
||||
}
|
||||
|
||||
func (m mockConfig) GetSpace() config.Config {
|
||||
return config.Config{}
|
||||
}
|
||||
|
||||
type headSyncFixture struct {
|
||||
spaceState *spacestate.SpaceState
|
||||
ctrl *gomock.Controller
|
||||
app *app.App
|
||||
|
||||
configurationMock *mock_nodeconf.MockService
|
||||
storageMock *mock_spacestorage.MockSpaceStorage
|
||||
peerManagerMock *mock_peermanager.MockPeerManager
|
||||
credentialProviderMock *mock_credentialprovider.MockCredentialProvider
|
||||
syncStatus syncstatus.StatusService
|
||||
treeManagerMock *mock_treemanager.MockTreeManager
|
||||
deletionStateMock *mock_deletionstate.MockObjectDeletionState
|
||||
diffSyncerMock *mock_headsync.MockDiffSyncer
|
||||
treeSyncerMock *mock_treemanager.MockTreeSyncer
|
||||
diffMock *mock_ldiff.MockDiff
|
||||
clientMock *mock_spacesyncproto.MockDRPCSpaceSyncClient
|
||||
headSync *headSync
|
||||
diffSyncer *diffSyncer
|
||||
}
|
||||
|
||||
func newHeadSyncFixture(t *testing.T) *headSyncFixture {
|
||||
spaceState := &spacestate.SpaceState{
|
||||
SpaceId: "spaceId",
|
||||
SpaceIsDeleted: &atomic.Bool{},
|
||||
}
|
||||
func TestDiffService(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
configurationMock := mock_nodeconf.NewMockService(ctrl)
|
||||
configurationMock.EXPECT().Name().AnyTimes().Return(nodeconf.CName)
|
||||
defer ctrl.Finish()
|
||||
|
||||
spaceId := "spaceId"
|
||||
l := logger.NewNamed("sync")
|
||||
pSyncMock := mock_periodicsync.NewMockPeriodicSync(ctrl)
|
||||
storageMock := mock_spacestorage.NewMockSpaceStorage(ctrl)
|
||||
storageMock.EXPECT().Name().AnyTimes().Return(spacestorage.CName)
|
||||
peerManagerMock := mock_peermanager.NewMockPeerManager(ctrl)
|
||||
peerManagerMock.EXPECT().Name().AnyTimes().Return(peermanager.CName)
|
||||
credentialProviderMock := mock_credentialprovider.NewMockCredentialProvider(ctrl)
|
||||
credentialProviderMock.EXPECT().Name().AnyTimes().Return(credentialprovider.CName)
|
||||
syncStatus := syncstatus.NewNoOpSyncStatus()
|
||||
treeManagerMock := mock_treemanager.NewMockTreeManager(ctrl)
|
||||
treeManagerMock.EXPECT().Name().AnyTimes().Return(treemanager.CName)
|
||||
deletionStateMock := mock_deletionstate.NewMockObjectDeletionState(ctrl)
|
||||
deletionStateMock.EXPECT().Name().AnyTimes().Return(deletionstate.CName)
|
||||
diffSyncerMock := mock_headsync.NewMockDiffSyncer(ctrl)
|
||||
treeSyncerMock := mock_treemanager.NewMockTreeSyncer(ctrl)
|
||||
treeStorageMock := mock_treestorage.NewMockTreeStorage(ctrl)
|
||||
diffMock := mock_ldiff.NewMockDiff(ctrl)
|
||||
clientMock := mock_spacesyncproto.NewMockDRPCSpaceSyncClient(ctrl)
|
||||
hs := &headSync{}
|
||||
a := &app.App{}
|
||||
a.Register(spaceState).
|
||||
Register(mockConfig{}).
|
||||
Register(configurationMock).
|
||||
Register(storageMock).
|
||||
Register(peerManagerMock).
|
||||
Register(credentialProviderMock).
|
||||
Register(syncStatus).
|
||||
Register(treeManagerMock).
|
||||
Register(deletionStateMock).
|
||||
Register(hs)
|
||||
return &headSyncFixture{
|
||||
spaceState: spaceState,
|
||||
ctrl: ctrl,
|
||||
app: a,
|
||||
configurationMock: configurationMock,
|
||||
storageMock: storageMock,
|
||||
peerManagerMock: peerManagerMock,
|
||||
credentialProviderMock: credentialProviderMock,
|
||||
syncStatus: syncStatus,
|
||||
treeManagerMock: treeManagerMock,
|
||||
deletionStateMock: deletionStateMock,
|
||||
headSync: hs,
|
||||
diffSyncerMock: diffSyncerMock,
|
||||
treeSyncerMock: treeSyncerMock,
|
||||
diffMock: diffMock,
|
||||
clientMock: clientMock,
|
||||
syncer := mock_headsync.NewMockDiffSyncer(ctrl)
|
||||
delState := mock_settingsstate.NewMockObjectDeletionState(ctrl)
|
||||
syncPeriod := 1
|
||||
initId := "initId"
|
||||
|
||||
service := &headSync{
|
||||
spaceId: spaceId,
|
||||
storage: storageMock,
|
||||
periodicSync: pSyncMock,
|
||||
syncer: syncer,
|
||||
diff: diffMock,
|
||||
log: l,
|
||||
syncPeriod: syncPeriod,
|
||||
}
|
||||
}
|
||||
|
||||
func (fx *headSyncFixture) init(t *testing.T) {
|
||||
createDiffSyncer = func(hs *headSync) DiffSyncer {
|
||||
return fx.diffSyncerMock
|
||||
}
|
||||
fx.diffSyncerMock.EXPECT().Init()
|
||||
err := fx.headSync.Init(fx.app)
|
||||
require.NoError(t, err)
|
||||
fx.headSync.diff = fx.diffMock
|
||||
}
|
||||
|
||||
func (fx *headSyncFixture) stop() {
|
||||
fx.ctrl.Finish()
|
||||
}
|
||||
|
||||
func TestHeadSync(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("run close", func(t *testing.T) {
|
||||
fx := newHeadSyncFixture(t)
|
||||
fx.init(t)
|
||||
defer fx.stop()
|
||||
|
||||
ids := []string{"id1"}
|
||||
treeMock := mock_treestorage.NewMockTreeStorage(fx.ctrl)
|
||||
fx.storageMock.EXPECT().StoredIds().Return(ids, nil)
|
||||
fx.storageMock.EXPECT().TreeStorage(ids[0]).Return(treeMock, nil)
|
||||
treeMock.EXPECT().Heads().Return([]string{"h1", "h2"}, nil)
|
||||
fx.diffMock.EXPECT().Set(ldiff.Element{
|
||||
Id: "id1",
|
||||
t.Run("init", func(t *testing.T) {
|
||||
storageMock.EXPECT().TreeStorage(initId).Return(treeStorageMock, nil)
|
||||
treeStorageMock.EXPECT().Heads().Return([]string{"h1", "h2"}, nil)
|
||||
syncer.EXPECT().Init(delState)
|
||||
diffMock.EXPECT().Set(ldiff.Element{
|
||||
Id: initId,
|
||||
Head: "h1h2",
|
||||
})
|
||||
fx.diffMock.EXPECT().Hash().Return("hash")
|
||||
fx.storageMock.EXPECT().WriteSpaceHash("hash").Return(nil)
|
||||
fx.diffSyncerMock.EXPECT().Sync(gomock.Any()).Return(nil)
|
||||
fx.diffSyncerMock.EXPECT().Close().Return(nil)
|
||||
err := fx.headSync.Run(ctx)
|
||||
require.NoError(t, err)
|
||||
err = fx.headSync.Close(ctx)
|
||||
require.NoError(t, err)
|
||||
hash := "123"
|
||||
diffMock.EXPECT().Hash().Return(hash)
|
||||
storageMock.EXPECT().WriteSpaceHash(hash)
|
||||
pSyncMock.EXPECT().Run()
|
||||
service.Init([]string{initId}, delState)
|
||||
})
|
||||
|
||||
t.Run("update heads", func(t *testing.T) {
|
||||
fx := newHeadSyncFixture(t)
|
||||
fx.init(t)
|
||||
defer fx.stop()
|
||||
|
||||
fx.diffSyncerMock.EXPECT().UpdateHeads("id1", []string{"h1"})
|
||||
fx.headSync.UpdateHeads("id1", []string{"h1"})
|
||||
syncer.EXPECT().UpdateHeads(initId, []string{"h1", "h2"})
|
||||
service.UpdateHeads(initId, []string{"h1", "h2"})
|
||||
})
|
||||
|
||||
t.Run("remove objects", func(t *testing.T) {
|
||||
fx := newHeadSyncFixture(t)
|
||||
fx.init(t)
|
||||
defer fx.stop()
|
||||
syncer.EXPECT().RemoveObjects([]string{"h1", "h2"})
|
||||
service.RemoveObjects([]string{"h1", "h2"})
|
||||
})
|
||||
|
||||
fx.diffSyncerMock.EXPECT().RemoveObjects([]string{"id1"})
|
||||
fx.headSync.RemoveObjects([]string{"id1"})
|
||||
t.Run("close", func(t *testing.T) {
|
||||
pSyncMock.EXPECT().Close()
|
||||
service.Close()
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/anyproto/any-sync/commonspace/headsync (interfaces: DiffSyncer)
|
||||
// Source: github.com/anytypeio/any-sync/commonspace/headsync (interfaces: DiffSyncer)
|
||||
|
||||
// Package mock_headsync is a generated GoMock package.
|
||||
package mock_headsync
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
settingsstate "github.com/anytypeio/any-sync/commonspace/settings/settingsstate"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockDiffSyncer is a mock of DiffSyncer interface.
|
||||
@@ -34,30 +35,16 @@ func (m *MockDiffSyncer) EXPECT() *MockDiffSyncerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Close mocks base method.
|
||||
func (m *MockDiffSyncer) Close() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Close")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Close indicates an expected call of Close.
|
||||
func (mr *MockDiffSyncerMockRecorder) Close() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDiffSyncer)(nil).Close))
|
||||
}
|
||||
|
||||
// Init mocks base method.
|
||||
func (m *MockDiffSyncer) Init() {
|
||||
func (m *MockDiffSyncer) Init(arg0 settingsstate.ObjectDeletionState) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Init")
|
||||
m.ctrl.Call(m, "Init", arg0)
|
||||
}
|
||||
|
||||
// Init indicates an expected call of Init.
|
||||
func (mr *MockDiffSyncerMockRecorder) Init() *gomock.Call {
|
||||
func (mr *MockDiffSyncerMockRecorder) Init(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockDiffSyncer)(nil).Init))
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockDiffSyncer)(nil).Init), arg0)
|
||||
}
|
||||
|
||||
// RemoveObjects mocks base method.
|
||||
|
||||
@@ -2,8 +2,8 @@ package headsync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/anyproto/any-sync/app/ldiff"
|
||||
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/app/ldiff"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
|
||||
@@ -3,8 +3,8 @@ package headsync
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/anyproto/any-sync/app/ldiff"
|
||||
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/app/ldiff"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package headsync
|
||||
|
||||
import "strings"
|
||||
|
||||
func concatStrings(strs []string) string {
|
||||
var (
|
||||
b strings.Builder
|
||||
totalLen int
|
||||
)
|
||||
for _, s := range strs {
|
||||
totalLen += len(s)
|
||||
}
|
||||
|
||||
b.Grow(totalLen)
|
||||
for _, s := range strs {
|
||||
b.WriteString(s)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func splitString(str string) (res []string) {
|
||||
const cidLen = 59
|
||||
for i := 0; i < len(str); i += cidLen {
|
||||
res = append(res, str[i:i+cidLen])
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,36 +1,14 @@
|
||||
package accountdata
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/encryptionkey"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/signingkey"
|
||||
)
|
||||
|
||||
type AccountKeys struct {
|
||||
PeerKey crypto.PrivKey
|
||||
SignKey crypto.PrivKey
|
||||
PeerId string
|
||||
}
|
||||
|
||||
func New(peerKey, signKey crypto.PrivKey) *AccountKeys {
|
||||
return &AccountKeys{
|
||||
PeerKey: peerKey,
|
||||
SignKey: signKey,
|
||||
PeerId: peerKey.GetPublic().PeerId(),
|
||||
}
|
||||
}
|
||||
|
||||
func NewRandom() (*AccountKeys, error) {
|
||||
peerKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &AccountKeys{
|
||||
PeerKey: peerKey,
|
||||
SignKey: signKey,
|
||||
PeerId: peerKey.GetPublic().PeerId(),
|
||||
}, nil
|
||||
type AccountData struct { // TODO: create a convenient constructor for this
|
||||
Identity []byte // public key
|
||||
PeerKey signingkey.PrivKey
|
||||
SignKey signingkey.PrivKey
|
||||
EncKey encryptionkey.PrivKey
|
||||
PeerId string
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package aclrecordproto
|
||||
|
||||
import (
|
||||
"github.com/anytypeio/any-sync/util/keys/symmetric"
|
||||
)
|
||||
|
||||
func AclReadKeyDerive(signKey []byte, encKey []byte) (*symmetric.Key, error) {
|
||||
concBuf := make([]byte, 0, len(signKey)+len(encKey))
|
||||
concBuf = append(concBuf, signKey...)
|
||||
concBuf = append(concBuf, encKey...)
|
||||
return symmetric.DeriveFromBytes(concBuf)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2,105 +2,117 @@ syntax = "proto3";
|
||||
package aclrecord;
|
||||
option go_package = "commonspace/object/acl/aclrecordproto";
|
||||
|
||||
// AclRoot is a root of access control list
|
||||
message RawAclRecord {
|
||||
bytes payload = 1;
|
||||
bytes signature = 2;
|
||||
bytes acceptorIdentity = 3;
|
||||
bytes acceptorSignature = 4;
|
||||
}
|
||||
|
||||
message RawAclRecordWithId {
|
||||
bytes payload = 1;
|
||||
string id = 2;
|
||||
}
|
||||
|
||||
message AclRecord {
|
||||
string prevId = 1;
|
||||
bytes identity = 2;
|
||||
bytes data = 3;
|
||||
uint64 currentReadKeyHash = 4;
|
||||
int64 timestamp = 5;
|
||||
}
|
||||
|
||||
message AclRoot {
|
||||
bytes identity = 1;
|
||||
bytes masterKey = 2;
|
||||
bytes encryptionKey = 2;
|
||||
string spaceId = 3;
|
||||
bytes encryptedReadKey = 4;
|
||||
int64 timestamp = 5;
|
||||
bytes identitySignature = 6;
|
||||
string derivationScheme = 5;
|
||||
uint64 currentReadKeyHash = 6;
|
||||
int64 timestamp = 7;
|
||||
}
|
||||
|
||||
// AclAccountInvite contains the public invite key, the private part of which is sent to the user directly
|
||||
message AclAccountInvite {
|
||||
bytes inviteKey = 1;
|
||||
}
|
||||
|
||||
// AclAccountRequestJoin contains the reference to the invite record and the data of the person who wants to join, confirmed by the private invite key
|
||||
message AclAccountRequestJoin {
|
||||
bytes inviteIdentity = 1;
|
||||
string inviteRecordId = 2;
|
||||
bytes inviteIdentitySignature = 3;
|
||||
bytes metadata = 4;
|
||||
}
|
||||
|
||||
// AclAccountRequestAccept contains the reference to join record and all read keys, encrypted with the identity of the requestor
|
||||
message AclAccountRequestAccept {
|
||||
bytes identity = 1;
|
||||
string requestRecordId = 2;
|
||||
repeated AclReadKeyWithRecord encryptedReadKeys = 3;
|
||||
AclUserPermissions permissions = 4;
|
||||
}
|
||||
|
||||
// AclAccountRequestDecline contains the reference to join record
|
||||
message AclAccountRequestDecline {
|
||||
string requestRecordId = 1;
|
||||
}
|
||||
|
||||
// AclAccountInviteRevoke revokes the invite record
|
||||
message AclAccountInviteRevoke {
|
||||
string inviteRecordId = 1;
|
||||
}
|
||||
|
||||
// AclReadKeys are a read key with record id
|
||||
message AclReadKeyWithRecord {
|
||||
string recordId = 1;
|
||||
bytes encryptedReadKey = 2;
|
||||
}
|
||||
|
||||
// AclEncryptedReadKeys are new key for specific identity
|
||||
message AclEncryptedReadKey {
|
||||
bytes identity = 1;
|
||||
bytes encryptedReadKey = 2;
|
||||
}
|
||||
|
||||
// AclAccountPermissionChange changes permissions of specific account
|
||||
message AclAccountPermissionChange {
|
||||
bytes identity = 1;
|
||||
AclUserPermissions permissions = 2;
|
||||
}
|
||||
|
||||
// AclReadKeyChange changes the key for a space
|
||||
message AclReadKeyChange {
|
||||
repeated AclEncryptedReadKey accountKeys = 1;
|
||||
}
|
||||
|
||||
// AclAccountRemove removes an account and changes read key for space
|
||||
message AclAccountRemove {
|
||||
repeated bytes identities = 1;
|
||||
repeated AclEncryptedReadKey accountKeys = 2;
|
||||
}
|
||||
|
||||
// AclAccountRequestRemove adds a request to remove an account
|
||||
message AclAccountRequestRemove {
|
||||
}
|
||||
|
||||
// AclContentValue contains possible values for Acl
|
||||
message AclContentValue {
|
||||
oneof value {
|
||||
AclAccountInvite invite = 1;
|
||||
AclAccountInviteRevoke inviteRevoke = 2;
|
||||
AclAccountRequestJoin requestJoin = 3;
|
||||
AclAccountRequestAccept requestAccept = 4;
|
||||
AclAccountPermissionChange permissionChange = 5;
|
||||
AclAccountRemove accountRemove = 6;
|
||||
AclReadKeyChange readKeyChange = 7;
|
||||
AclAccountRequestDecline requestDecline = 8;
|
||||
AclAccountRequestRemove accountRequestRemove = 9;
|
||||
AclUserAdd userAdd = 1;
|
||||
AclUserRemove userRemove = 2;
|
||||
AclUserPermissionChange userPermissionChange = 3;
|
||||
AclUserInvite userInvite = 4;
|
||||
AclUserJoin userJoin = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// AclData contains different acl content
|
||||
message AclData {
|
||||
repeated AclContentValue aclContent = 1;
|
||||
}
|
||||
|
||||
// AclUserPermissions contains different possible user roles
|
||||
enum AclUserPermissions {
|
||||
None = 0;
|
||||
Owner = 1;
|
||||
Admin = 2;
|
||||
Writer = 3;
|
||||
Reader = 4;
|
||||
message AclState {
|
||||
repeated uint64 readKeyHashes = 1;
|
||||
repeated AclUserState userStates = 2;
|
||||
map<string, AclUserInvite> invites = 3;
|
||||
}
|
||||
|
||||
message AclUserState {
|
||||
bytes identity = 1;
|
||||
bytes encryptionKey = 2;
|
||||
AclUserPermissions permissions = 3;
|
||||
}
|
||||
|
||||
message AclUserAdd {
|
||||
bytes identity = 1;
|
||||
bytes encryptionKey = 2;
|
||||
repeated bytes encryptedReadKeys = 3;
|
||||
AclUserPermissions permissions = 4;
|
||||
}
|
||||
|
||||
message AclUserInvite {
|
||||
bytes acceptPublicKey = 1;
|
||||
uint64 encryptSymKeyHash = 2;
|
||||
repeated bytes encryptedReadKeys = 3;
|
||||
AclUserPermissions permissions = 4;
|
||||
}
|
||||
|
||||
message AclUserJoin {
|
||||
bytes identity = 1;
|
||||
bytes encryptionKey = 2;
|
||||
bytes acceptSignature = 3;
|
||||
bytes acceptPubKey = 4;
|
||||
repeated bytes encryptedReadKeys = 5;
|
||||
}
|
||||
|
||||
message AclUserRemove {
|
||||
bytes identity = 1;
|
||||
repeated AclReadKeyReplace readKeyReplaces = 2;
|
||||
}
|
||||
|
||||
message AclReadKeyReplace {
|
||||
bytes identity = 1;
|
||||
bytes encryptionKey = 2;
|
||||
bytes encryptedReadKey = 3;
|
||||
}
|
||||
|
||||
message AclUserPermissionChange {
|
||||
bytes identity = 1;
|
||||
AclUserPermissions permissions = 2;
|
||||
}
|
||||
|
||||
enum AclUserPermissions {
|
||||
Admin = 0;
|
||||
Writer = 1;
|
||||
Reader = 2;
|
||||
}
|
||||
|
||||
message AclSyncMessage {
|
||||
AclSyncContentValue content = 2;
|
||||
}
|
||||
|
||||
// AclSyncContentValue provides different types for acl sync
|
||||
message AclSyncContentValue {
|
||||
oneof value {
|
||||
AclAddRecords addRecords = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message AclAddRecords {
|
||||
repeated RawAclRecordWithId records = 1;
|
||||
}
|
||||
@@ -1,499 +1,166 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/accountdata"
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anyproto/any-sync/consensus/consensusproto"
|
||||
"github.com/anyproto/any-sync/util/cidutil"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/keychain"
|
||||
"github.com/anytypeio/any-sync/util/cidutil"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/signingkey"
|
||||
"github.com/anytypeio/any-sync/util/keys/symmetric"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RootContent struct {
|
||||
PrivKey crypto.PrivKey
|
||||
MasterKey crypto.PrivKey
|
||||
SpaceId string
|
||||
EncryptedReadKey []byte
|
||||
}
|
||||
|
||||
type RequestJoinPayload struct {
|
||||
InviteRecordId string
|
||||
InviteKey crypto.PrivKey
|
||||
Metadata []byte
|
||||
}
|
||||
|
||||
type RequestAcceptPayload struct {
|
||||
RequestRecordId string
|
||||
Permissions AclPermissions
|
||||
}
|
||||
|
||||
type PermissionChangePayload struct {
|
||||
Identity crypto.PubKey
|
||||
Permissions AclPermissions
|
||||
}
|
||||
|
||||
type AccountRemovePayload struct {
|
||||
Identities []crypto.PubKey
|
||||
ReadKey crypto.SymKey
|
||||
}
|
||||
|
||||
type InviteResult struct {
|
||||
InviteRec *consensusproto.RawRecord
|
||||
InviteKey crypto.PrivKey
|
||||
}
|
||||
|
||||
// remove interface
|
||||
type AclRecordBuilder interface {
|
||||
UnmarshallWithId(rawIdRecord *consensusproto.RawRecordWithId) (rec *AclRecord, err error)
|
||||
Unmarshall(rawRecord *consensusproto.RawRecord) (rec *AclRecord, err error)
|
||||
|
||||
BuildRoot(content RootContent) (rec *consensusproto.RawRecordWithId, err error)
|
||||
BuildInvite() (res InviteResult, err error)
|
||||
BuildInviteRevoke(inviteRecordId string) (rawRecord *consensusproto.RawRecord, err error)
|
||||
BuildRequestJoin(payload RequestJoinPayload) (rawRecord *consensusproto.RawRecord, err error)
|
||||
BuildRequestAccept(payload RequestAcceptPayload) (rawRecord *consensusproto.RawRecord, err error)
|
||||
BuildRequestDecline(requestRecordId string) (rawRecord *consensusproto.RawRecord, err error)
|
||||
BuildRequestRemove() (rawRecord *consensusproto.RawRecord, err error)
|
||||
BuildPermissionChange(payload PermissionChangePayload) (rawRecord *consensusproto.RawRecord, err error)
|
||||
BuildReadKeyChange(newKey crypto.SymKey) (rawRecord *consensusproto.RawRecord, err error)
|
||||
BuildAccountRemove(payload AccountRemovePayload) (rawRecord *consensusproto.RawRecord, err error)
|
||||
ConvertFromRaw(rawIdRecord *aclrecordproto.RawAclRecordWithId) (rec *AclRecord, err error)
|
||||
BuildUserJoin(acceptPrivKeyBytes []byte, encSymKeyBytes []byte, state *AclState) (rec *aclrecordproto.RawAclRecord, err error)
|
||||
}
|
||||
|
||||
type aclRecordBuilder struct {
|
||||
id string
|
||||
keyStorage crypto.KeyStorage
|
||||
accountKeys *accountdata.AccountKeys
|
||||
verifier AcceptorVerifier
|
||||
state *AclState
|
||||
id string
|
||||
keychain *keychain.Keychain
|
||||
}
|
||||
|
||||
func NewAclRecordBuilder(id string, keyStorage crypto.KeyStorage, keys *accountdata.AccountKeys, verifier AcceptorVerifier) AclRecordBuilder {
|
||||
func newAclRecordBuilder(id string, keychain *keychain.Keychain) AclRecordBuilder {
|
||||
return &aclRecordBuilder{
|
||||
id: id,
|
||||
keyStorage: keyStorage,
|
||||
accountKeys: keys,
|
||||
verifier: verifier,
|
||||
id: id,
|
||||
keychain: keychain,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *aclRecordBuilder) buildRecord(aclContent *aclrecordproto.AclContentValue) (rawRec *consensusproto.RawRecord, err error) {
|
||||
func (a *aclRecordBuilder) BuildUserJoin(acceptPrivKeyBytes []byte, encSymKeyBytes []byte, state *AclState) (rec *aclrecordproto.RawAclRecord, err error) {
|
||||
acceptPrivKey, err := signingkey.NewSigningEd25519PrivKeyFromBytes(acceptPrivKeyBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
acceptPubKeyBytes, err := acceptPrivKey.GetPublic().Raw()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
encSymKey, err := symmetric.FromBytes(encSymKeyBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
invite, err := state.Invite(acceptPubKeyBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
encPrivKey, signPrivKey := state.UserKeys()
|
||||
var symKeys [][]byte
|
||||
for _, rk := range invite.EncryptedReadKeys {
|
||||
dec, err := encSymKey.Decrypt(rk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newEnc, err := encPrivKey.GetPublic().Encrypt(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
symKeys = append(symKeys, newEnc)
|
||||
}
|
||||
idSignature, err := acceptPrivKey.Sign(state.Identity())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
encPubKeyBytes, err := encPrivKey.GetPublic().Raw()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userJoin := &aclrecordproto.AclUserJoin{
|
||||
Identity: state.Identity(),
|
||||
EncryptionKey: encPubKeyBytes,
|
||||
AcceptSignature: idSignature,
|
||||
AcceptPubKey: acceptPubKeyBytes,
|
||||
EncryptedReadKeys: symKeys,
|
||||
}
|
||||
aclData := &aclrecordproto.AclData{AclContent: []*aclrecordproto.AclContentValue{
|
||||
aclContent,
|
||||
{Value: &aclrecordproto.AclContentValue_UserJoin{UserJoin: userJoin}},
|
||||
}}
|
||||
marshalledData, err := aclData.Marshal()
|
||||
marshalledJoin, err := aclData.Marshal()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
protoKey, err := a.accountKeys.SignKey.GetPublic().Marshall()
|
||||
aclRecord := &aclrecordproto.AclRecord{
|
||||
PrevId: state.LastRecordId(),
|
||||
Identity: state.Identity(),
|
||||
Data: marshalledJoin,
|
||||
CurrentReadKeyHash: state.CurrentReadKeyHash(),
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
marshalledRecord, err := aclRecord.Marshal()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rec := &consensusproto.Record{
|
||||
PrevId: a.state.lastRecordId,
|
||||
Identity: protoKey,
|
||||
Data: marshalledData,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
marshalledRec, err := rec.Marshal()
|
||||
recSignature, err := signPrivKey.Sign(marshalledRecord)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signature, err := a.accountKeys.SignKey.Sign(marshalledRec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rawRec = &consensusproto.RawRecord{
|
||||
Payload: marshalledRec,
|
||||
Signature: signature,
|
||||
rec = &aclrecordproto.RawAclRecord{
|
||||
Payload: marshalledRecord,
|
||||
Signature: recSignature,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (a *aclRecordBuilder) BuildInvite() (res InviteResult, err error) {
|
||||
if !a.state.Permissions(a.state.pubKey).CanManageAccounts() {
|
||||
err = ErrInsufficientPermissions
|
||||
return
|
||||
}
|
||||
privKey, pubKey, err := crypto.GenerateRandomEd25519KeyPair()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
invitePubKey, err := pubKey.Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
inviteRec := &aclrecordproto.AclAccountInvite{InviteKey: invitePubKey}
|
||||
content := &aclrecordproto.AclContentValue{Value: &aclrecordproto.AclContentValue_Invite{Invite: inviteRec}}
|
||||
rawRec, err := a.buildRecord(content)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res.InviteKey = privKey
|
||||
res.InviteRec = rawRec
|
||||
return
|
||||
}
|
||||
|
||||
func (a *aclRecordBuilder) BuildInviteRevoke(inviteRecordId string) (rawRecord *consensusproto.RawRecord, err error) {
|
||||
if !a.state.Permissions(a.state.pubKey).CanManageAccounts() {
|
||||
err = ErrInsufficientPermissions
|
||||
return
|
||||
}
|
||||
_, exists := a.state.inviteKeys[inviteRecordId]
|
||||
if !exists {
|
||||
err = ErrNoSuchInvite
|
||||
return
|
||||
}
|
||||
revokeRec := &aclrecordproto.AclAccountInviteRevoke{InviteRecordId: inviteRecordId}
|
||||
content := &aclrecordproto.AclContentValue{Value: &aclrecordproto.AclContentValue_InviteRevoke{InviteRevoke: revokeRec}}
|
||||
return a.buildRecord(content)
|
||||
}
|
||||
|
||||
func (a *aclRecordBuilder) BuildRequestJoin(payload RequestJoinPayload) (rawRecord *consensusproto.RawRecord, err error) {
|
||||
key, exists := a.state.inviteKeys[payload.InviteRecordId]
|
||||
if !exists {
|
||||
err = ErrNoSuchInvite
|
||||
return
|
||||
}
|
||||
if !payload.InviteKey.GetPublic().Equals(key) {
|
||||
err = ErrIncorrectInviteKey
|
||||
}
|
||||
rawIdentity, err := a.accountKeys.SignKey.GetPublic().Raw()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signature, err := payload.InviteKey.Sign(rawIdentity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
protoIdentity, err := a.accountKeys.SignKey.GetPublic().Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
joinRec := &aclrecordproto.AclAccountRequestJoin{
|
||||
InviteIdentity: protoIdentity,
|
||||
InviteRecordId: payload.InviteRecordId,
|
||||
InviteIdentitySignature: signature,
|
||||
Metadata: payload.Metadata,
|
||||
}
|
||||
content := &aclrecordproto.AclContentValue{Value: &aclrecordproto.AclContentValue_RequestJoin{RequestJoin: joinRec}}
|
||||
return a.buildRecord(content)
|
||||
}
|
||||
|
||||
func (a *aclRecordBuilder) BuildRequestAccept(payload RequestAcceptPayload) (rawRecord *consensusproto.RawRecord, err error) {
|
||||
if !a.state.Permissions(a.state.pubKey).CanManageAccounts() {
|
||||
err = ErrInsufficientPermissions
|
||||
return
|
||||
}
|
||||
request, exists := a.state.requestRecords[payload.RequestRecordId]
|
||||
if !exists {
|
||||
err = ErrNoSuchRequest
|
||||
return
|
||||
}
|
||||
var encryptedReadKeys []*aclrecordproto.AclReadKeyWithRecord
|
||||
for keyId, key := range a.state.userReadKeys {
|
||||
rawKey, err := key.Raw()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enc, err := request.RequestIdentity.Encrypt(rawKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encryptedReadKeys = append(encryptedReadKeys, &aclrecordproto.AclReadKeyWithRecord{
|
||||
RecordId: keyId,
|
||||
EncryptedReadKey: enc,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
requestIdentityProto, err := request.RequestIdentity.Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
acceptRec := &aclrecordproto.AclAccountRequestAccept{
|
||||
Identity: requestIdentityProto,
|
||||
RequestRecordId: payload.RequestRecordId,
|
||||
EncryptedReadKeys: encryptedReadKeys,
|
||||
Permissions: aclrecordproto.AclUserPermissions(payload.Permissions),
|
||||
}
|
||||
content := &aclrecordproto.AclContentValue{Value: &aclrecordproto.AclContentValue_RequestAccept{RequestAccept: acceptRec}}
|
||||
return a.buildRecord(content)
|
||||
}
|
||||
|
||||
func (a *aclRecordBuilder) BuildRequestDecline(requestRecordId string) (rawRecord *consensusproto.RawRecord, err error) {
|
||||
if !a.state.Permissions(a.state.pubKey).CanManageAccounts() {
|
||||
err = ErrInsufficientPermissions
|
||||
return
|
||||
}
|
||||
_, exists := a.state.requestRecords[requestRecordId]
|
||||
if !exists {
|
||||
err = ErrNoSuchRequest
|
||||
return
|
||||
}
|
||||
declineRec := &aclrecordproto.AclAccountRequestDecline{RequestRecordId: requestRecordId}
|
||||
content := &aclrecordproto.AclContentValue{Value: &aclrecordproto.AclContentValue_RequestDecline{RequestDecline: declineRec}}
|
||||
return a.buildRecord(content)
|
||||
}
|
||||
|
||||
func (a *aclRecordBuilder) BuildPermissionChange(payload PermissionChangePayload) (rawRecord *consensusproto.RawRecord, err error) {
|
||||
permissions := a.state.Permissions(a.state.pubKey)
|
||||
if !permissions.CanManageAccounts() || payload.Identity.Equals(a.state.pubKey) {
|
||||
err = ErrInsufficientPermissions
|
||||
return
|
||||
}
|
||||
if payload.Permissions.IsOwner() {
|
||||
err = ErrIsOwner
|
||||
return
|
||||
}
|
||||
protoIdentity, err := payload.Identity.Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
permissionRec := &aclrecordproto.AclAccountPermissionChange{
|
||||
Identity: protoIdentity,
|
||||
Permissions: aclrecordproto.AclUserPermissions(payload.Permissions),
|
||||
}
|
||||
content := &aclrecordproto.AclContentValue{Value: &aclrecordproto.AclContentValue_PermissionChange{PermissionChange: permissionRec}}
|
||||
return a.buildRecord(content)
|
||||
}
|
||||
|
||||
func (a *aclRecordBuilder) BuildReadKeyChange(newKey crypto.SymKey) (rawRecord *consensusproto.RawRecord, err error) {
|
||||
if !a.state.Permissions(a.state.pubKey).CanManageAccounts() {
|
||||
err = ErrInsufficientPermissions
|
||||
return
|
||||
}
|
||||
rawKey, err := newKey.Raw()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(rawKey) != crypto.KeyBytes {
|
||||
err = ErrIncorrectReadKey
|
||||
return
|
||||
}
|
||||
var aclReadKeys []*aclrecordproto.AclEncryptedReadKey
|
||||
for _, st := range a.state.userStates {
|
||||
protoIdentity, err := st.PubKey.Marshall()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enc, err := st.PubKey.Encrypt(rawKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aclReadKeys = append(aclReadKeys, &aclrecordproto.AclEncryptedReadKey{
|
||||
Identity: protoIdentity,
|
||||
EncryptedReadKey: enc,
|
||||
})
|
||||
}
|
||||
readRec := &aclrecordproto.AclReadKeyChange{AccountKeys: aclReadKeys}
|
||||
content := &aclrecordproto.AclContentValue{Value: &aclrecordproto.AclContentValue_ReadKeyChange{ReadKeyChange: readRec}}
|
||||
return a.buildRecord(content)
|
||||
}
|
||||
|
||||
func (a *aclRecordBuilder) BuildAccountRemove(payload AccountRemovePayload) (rawRecord *consensusproto.RawRecord, err error) {
|
||||
deletedMap := map[string]struct{}{}
|
||||
for _, key := range payload.Identities {
|
||||
permissions := a.state.Permissions(key)
|
||||
if permissions.IsOwner() {
|
||||
return nil, ErrInsufficientPermissions
|
||||
}
|
||||
if permissions.NoPermissions() {
|
||||
return nil, ErrNoSuchAccount
|
||||
}
|
||||
deletedMap[mapKeyFromPubKey(key)] = struct{}{}
|
||||
}
|
||||
if !a.state.Permissions(a.state.pubKey).CanManageAccounts() {
|
||||
err = ErrInsufficientPermissions
|
||||
return
|
||||
}
|
||||
rawKey, err := payload.ReadKey.Raw()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(rawKey) != crypto.KeyBytes {
|
||||
err = ErrIncorrectReadKey
|
||||
return
|
||||
}
|
||||
var aclReadKeys []*aclrecordproto.AclEncryptedReadKey
|
||||
for _, st := range a.state.userStates {
|
||||
if _, exists := deletedMap[mapKeyFromPubKey(st.PubKey)]; exists {
|
||||
continue
|
||||
}
|
||||
protoIdentity, err := st.PubKey.Marshall()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enc, err := st.PubKey.Encrypt(rawKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aclReadKeys = append(aclReadKeys, &aclrecordproto.AclEncryptedReadKey{
|
||||
Identity: protoIdentity,
|
||||
EncryptedReadKey: enc,
|
||||
})
|
||||
}
|
||||
var marshalledIdentities [][]byte
|
||||
for _, key := range payload.Identities {
|
||||
protoIdentity, err := key.Marshall()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
marshalledIdentities = append(marshalledIdentities, protoIdentity)
|
||||
}
|
||||
removeRec := &aclrecordproto.AclAccountRemove{AccountKeys: aclReadKeys, Identities: marshalledIdentities}
|
||||
content := &aclrecordproto.AclContentValue{Value: &aclrecordproto.AclContentValue_AccountRemove{AccountRemove: removeRec}}
|
||||
return a.buildRecord(content)
|
||||
}
|
||||
|
||||
func (a *aclRecordBuilder) BuildRequestRemove() (rawRecord *consensusproto.RawRecord, err error) {
|
||||
permissions := a.state.Permissions(a.state.pubKey)
|
||||
if permissions.NoPermissions() {
|
||||
err = ErrNoSuchAccount
|
||||
return
|
||||
}
|
||||
if permissions.IsOwner() {
|
||||
err = ErrIsOwner
|
||||
return
|
||||
}
|
||||
removeRec := &aclrecordproto.AclAccountRequestRemove{}
|
||||
content := &aclrecordproto.AclContentValue{Value: &aclrecordproto.AclContentValue_AccountRequestRemove{AccountRequestRemove: removeRec}}
|
||||
return a.buildRecord(content)
|
||||
}
|
||||
|
||||
func (a *aclRecordBuilder) Unmarshall(rawRecord *consensusproto.RawRecord) (rec *AclRecord, err error) {
|
||||
aclRecord := &consensusproto.Record{}
|
||||
err = proto.Unmarshal(rawRecord.Payload, aclRecord)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pubKey, err := a.keyStorage.PubKeyFromProto(aclRecord.Identity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
aclData := &aclrecordproto.AclData{}
|
||||
err = proto.Unmarshal(aclRecord.Data, aclData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rec = &AclRecord{
|
||||
PrevId: aclRecord.PrevId,
|
||||
Timestamp: aclRecord.Timestamp,
|
||||
Data: aclRecord.Data,
|
||||
Signature: rawRecord.Signature,
|
||||
Identity: pubKey,
|
||||
Model: aclData,
|
||||
}
|
||||
res, err := pubKey.Verify(rawRecord.Payload, rawRecord.Signature)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !res {
|
||||
err = ErrInvalidSignature
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (a *aclRecordBuilder) UnmarshallWithId(rawIdRecord *consensusproto.RawRecordWithId) (rec *AclRecord, err error) {
|
||||
var (
|
||||
rawRec = &consensusproto.RawRecord{}
|
||||
pubKey crypto.PubKey
|
||||
)
|
||||
func (a *aclRecordBuilder) ConvertFromRaw(rawIdRecord *aclrecordproto.RawAclRecordWithId) (rec *AclRecord, err error) {
|
||||
rawRec := &aclrecordproto.RawAclRecord{}
|
||||
err = proto.Unmarshal(rawIdRecord.Payload, rawRec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if rawIdRecord.Id == a.id {
|
||||
aclRoot := &aclrecordproto.AclRoot{}
|
||||
err = proto.Unmarshal(rawRec.Payload, aclRoot)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pubKey, err = a.keyStorage.PubKeyFromProto(aclRoot.Identity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rec = &AclRecord{
|
||||
Id: rawIdRecord.Id,
|
||||
Timestamp: aclRoot.Timestamp,
|
||||
Signature: rawRec.Signature,
|
||||
Identity: pubKey,
|
||||
Model: aclRoot,
|
||||
Id: rawIdRecord.Id,
|
||||
CurrentReadKeyHash: aclRoot.CurrentReadKeyHash,
|
||||
Timestamp: aclRoot.Timestamp,
|
||||
Signature: rawRec.Signature,
|
||||
Identity: aclRoot.Identity,
|
||||
Model: aclRoot,
|
||||
}
|
||||
} else {
|
||||
err = a.verifier.VerifyAcceptor(rawRec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
aclRecord := &consensusproto.Record{}
|
||||
aclRecord := &aclrecordproto.AclRecord{}
|
||||
err = proto.Unmarshal(rawRec.Payload, aclRecord)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pubKey, err = a.keyStorage.PubKeyFromProto(aclRecord.Identity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
aclData := &aclrecordproto.AclData{}
|
||||
err = proto.Unmarshal(aclRecord.Data, aclData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rec = &AclRecord{
|
||||
Id: rawIdRecord.Id,
|
||||
PrevId: aclRecord.PrevId,
|
||||
Timestamp: aclRecord.Timestamp,
|
||||
Data: aclRecord.Data,
|
||||
Signature: rawRec.Signature,
|
||||
Identity: pubKey,
|
||||
Model: aclData,
|
||||
Id: rawIdRecord.Id,
|
||||
PrevId: aclRecord.PrevId,
|
||||
CurrentReadKeyHash: aclRecord.CurrentReadKeyHash,
|
||||
Timestamp: aclRecord.Timestamp,
|
||||
Data: aclRecord.Data,
|
||||
Signature: rawRec.Signature,
|
||||
Identity: aclRecord.Identity,
|
||||
}
|
||||
}
|
||||
|
||||
err = verifyRaw(pubKey, rawRec, rawIdRecord)
|
||||
err = verifyRaw(a.keychain, rawRec, rawIdRecord, rec.Identity)
|
||||
return
|
||||
}
|
||||
|
||||
func (a *aclRecordBuilder) BuildRoot(content RootContent) (rec *consensusproto.RawRecordWithId, err error) {
|
||||
rawIdentity, err := content.PrivKey.GetPublic().Raw()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
identity, err := content.PrivKey.GetPublic().Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
masterKey, err := content.MasterKey.GetPublic().Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
identitySignature, err := content.MasterKey.Sign(rawIdentity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var timestamp int64
|
||||
if content.EncryptedReadKey != nil {
|
||||
timestamp = time.Now().Unix()
|
||||
}
|
||||
aclRoot := &aclrecordproto.AclRoot{
|
||||
Identity: identity,
|
||||
SpaceId: content.SpaceId,
|
||||
EncryptedReadKey: content.EncryptedReadKey,
|
||||
MasterKey: masterKey,
|
||||
IdentitySignature: identitySignature,
|
||||
Timestamp: timestamp,
|
||||
}
|
||||
return marshalAclRoot(aclRoot, content.PrivKey)
|
||||
}
|
||||
|
||||
func verifyRaw(
|
||||
pubKey crypto.PubKey,
|
||||
rawRec *consensusproto.RawRecord,
|
||||
recWithId *consensusproto.RawRecordWithId) (err error) {
|
||||
keychain *keychain.Keychain,
|
||||
rawRec *aclrecordproto.RawAclRecord,
|
||||
recWithId *aclrecordproto.RawAclRecordWithId,
|
||||
identity []byte) (err error) {
|
||||
identityKey, err := keychain.GetOrAdd(string(identity))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// verifying signature
|
||||
res, err := pubKey.Verify(rawRec.Payload, rawRec.Signature)
|
||||
res, err := identityKey.Verify(rawRec.Payload, rawRec.Signature)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -508,31 +175,3 @@ func verifyRaw(
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func marshalAclRoot(aclRoot *aclrecordproto.AclRoot, key crypto.PrivKey) (rawWithId *consensusproto.RawRecordWithId, err error) {
|
||||
marshalledRoot, err := aclRoot.Marshal()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signature, err := key.Sign(marshalledRoot)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
raw := &consensusproto.RawRecord{
|
||||
Payload: marshalledRoot,
|
||||
Signature: signature,
|
||||
}
|
||||
marshalledRaw, err := raw.Marshal()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
aclHeadId, err := cidutil.NewCidFromBytes(marshalledRaw)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rawWithId = &consensusproto.RawRecordWithId{
|
||||
Payload: marshalledRaw,
|
||||
Id: aclHeadId,
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"github.com/anytypeio/any-sync/commonspace/object/accountdata"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
acllistbuilder2 "github.com/anytypeio/any-sync/commonspace/object/acl/testutils/acllistbuilder"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/keychain"
|
||||
"github.com/anytypeio/any-sync/util/cidutil"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/signingkey"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAclRecordBuilder_BuildUserJoin(t *testing.T) {
|
||||
st, err := acllistbuilder2.NewListStorageWithTestName("userjoinexample.yml")
|
||||
require.NoError(t, err, "building storage should not result in error")
|
||||
|
||||
testKeychain := st.(*acllistbuilder2.AclListStorageBuilder).GetKeychain()
|
||||
identity := testKeychain.GeneratedIdentities["D"]
|
||||
signPrivKey := testKeychain.SigningKeysByYAMLName["D"]
|
||||
encPrivKey := testKeychain.EncryptionKeysByYAMLName["D"]
|
||||
acc := &accountdata.AccountData{
|
||||
Identity: []byte(identity),
|
||||
SignKey: signPrivKey,
|
||||
EncKey: encPrivKey,
|
||||
}
|
||||
|
||||
aclList, err := BuildAclListWithIdentity(acc, st)
|
||||
require.NoError(t, err, "building acl list should be without error")
|
||||
recordBuilder := newAclRecordBuilder(aclList.Id(), keychain.NewKeychain())
|
||||
rk, err := testKeychain.GetKey("key.Read.EncKey").(*acllistbuilder2.SymKey).Key.Raw()
|
||||
require.NoError(t, err)
|
||||
privKey, err := testKeychain.GetKey("key.Sign.Onetime1").(signingkey.PrivKey).Raw()
|
||||
require.NoError(t, err)
|
||||
|
||||
userJoin, err := recordBuilder.BuildUserJoin(privKey, rk, aclList.AclState())
|
||||
require.NoError(t, err)
|
||||
marshalledJoin, err := userJoin.Marshal()
|
||||
require.NoError(t, err)
|
||||
id, err := cidutil.NewCidFromBytes(marshalledJoin)
|
||||
require.NoError(t, err)
|
||||
rawRec := &aclrecordproto.RawAclRecordWithId{
|
||||
Payload: marshalledJoin,
|
||||
Id: id,
|
||||
}
|
||||
res, err := aclList.AddRawRecord(rawRec)
|
||||
require.True(t, res)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, aclrecordproto.AclUserPermissions_Writer, aclList.AclState().UserStates()[identity].Permissions)
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
|
||||
"github.com/anyproto/any-sync/app/logger"
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
"github.com/anytypeio/any-sync/app/logger"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/keychain"
|
||||
"github.com/anytypeio/any-sync/util/keys"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/encryptionkey"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/signingkey"
|
||||
"github.com/anytypeio/any-sync/util/keys/symmetric"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -13,130 +20,102 @@ import (
|
||||
var log = logger.NewNamedSugared("common.commonspace.acllist")
|
||||
|
||||
var (
|
||||
ErrNoSuchAccount = errors.New("no such account")
|
||||
ErrPendingRequest = errors.New("already exists pending request")
|
||||
ErrUnexpectedContentType = errors.New("unexpected content type")
|
||||
ErrIncorrectIdentity = errors.New("incorrect identity")
|
||||
ErrIncorrectInviteKey = errors.New("incorrect invite key")
|
||||
ErrFailedToDecrypt = errors.New("failed to decrypt key")
|
||||
ErrNoSuchRecord = errors.New("no such record")
|
||||
ErrNoSuchRequest = errors.New("no such request")
|
||||
ErrNoSuchInvite = errors.New("no such invite")
|
||||
ErrInsufficientPermissions = errors.New("insufficient permissions")
|
||||
ErrIsOwner = errors.New("can't be made by owner")
|
||||
ErrIncorrectNumberOfAccounts = errors.New("incorrect number of accounts")
|
||||
ErrDuplicateAccounts = errors.New("duplicate accounts")
|
||||
ErrNoReadKey = errors.New("acl state doesn't have a read key")
|
||||
ErrIncorrectReadKey = errors.New("incorrect read key")
|
||||
ErrInvalidSignature = errors.New("signature is invalid")
|
||||
ErrIncorrectRoot = errors.New("incorrect root")
|
||||
ErrIncorrectRecordSequence = errors.New("incorrect prev id of a record")
|
||||
ErrNoSuchUser = errors.New("no such user")
|
||||
ErrFailedToDecrypt = errors.New("failed to decrypt key")
|
||||
ErrUserRemoved = errors.New("user was removed from the document")
|
||||
ErrDocumentForbidden = errors.New("your user was forbidden access to the document")
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
ErrNoSuchRecord = errors.New("no such record")
|
||||
ErrNoSuchInvite = errors.New("no such invite")
|
||||
ErrOldInvite = errors.New("invite is too old")
|
||||
ErrInsufficientPermissions = errors.New("insufficient permissions")
|
||||
ErrNoReadKey = errors.New("acl state doesn't have a read key")
|
||||
ErrInvalidSignature = errors.New("signature is invalid")
|
||||
ErrIncorrectRoot = errors.New("incorrect root")
|
||||
ErrIncorrectRecordSequence = errors.New("incorrect prev id of a record")
|
||||
)
|
||||
|
||||
type UserPermissionPair struct {
|
||||
Identity crypto.PubKey
|
||||
Identity string
|
||||
Permission aclrecordproto.AclUserPermissions
|
||||
}
|
||||
|
||||
type AclState struct {
|
||||
id string
|
||||
currentReadKeyId string
|
||||
// userReadKeys is a map recordId -> read key which tells us about every read key
|
||||
userReadKeys map[string]crypto.SymKey
|
||||
// userStates is a map pubKey -> state which defines current user state
|
||||
userStates map[string]AclUserState
|
||||
// statesAtRecord is a map recordId -> state which define user state at particular record
|
||||
// probably this can grow rather large at some point, so we can maybe optimise later to have:
|
||||
// - map pubKey -> []recordIds (where recordIds is an array where such identity permissions were changed)
|
||||
statesAtRecord map[string][]AclUserState
|
||||
// inviteKeys is a map recordId -> invite
|
||||
inviteKeys map[string]crypto.PubKey
|
||||
// requestRecords is a map recordId -> RequestRecord
|
||||
requestRecords map[string]RequestRecord
|
||||
// pendingRequests is a map pubKey -> recordId
|
||||
pendingRequests map[string]string
|
||||
key crypto.PrivKey
|
||||
pubKey crypto.PubKey
|
||||
keyStore crypto.KeyStorage
|
||||
totalReadKeys int
|
||||
id string
|
||||
currentReadKeyHash uint64
|
||||
userReadKeys map[uint64]*symmetric.Key
|
||||
userStates map[string]*aclrecordproto.AclUserState
|
||||
userInvites map[string]*aclrecordproto.AclUserInvite
|
||||
encryptionKey encryptionkey.PrivKey
|
||||
signingKey signingkey.PrivKey
|
||||
totalReadKeys int
|
||||
|
||||
lastRecordId string
|
||||
contentValidator ContentValidator
|
||||
identity string
|
||||
permissionsAtRecord map[string][]UserPermissionPair
|
||||
lastRecordId string
|
||||
|
||||
keychain *keychain.Keychain
|
||||
}
|
||||
|
||||
func newAclStateWithKeys(
|
||||
id string,
|
||||
key crypto.PrivKey) (*AclState, error) {
|
||||
st := &AclState{
|
||||
id: id,
|
||||
key: key,
|
||||
pubKey: key.GetPublic(),
|
||||
userReadKeys: make(map[string]crypto.SymKey),
|
||||
userStates: make(map[string]AclUserState),
|
||||
statesAtRecord: make(map[string][]AclUserState),
|
||||
inviteKeys: make(map[string]crypto.PubKey),
|
||||
requestRecords: make(map[string]RequestRecord),
|
||||
pendingRequests: make(map[string]string),
|
||||
keyStore: crypto.NewKeyStorage(),
|
||||
signingKey signingkey.PrivKey,
|
||||
encryptionKey encryptionkey.PrivKey) (*AclState, error) {
|
||||
identity, err := signingKey.GetPublic().Raw()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
st.contentValidator = &contentValidator{
|
||||
keyStore: st.keyStore,
|
||||
aclState: st,
|
||||
}
|
||||
return st, nil
|
||||
return &AclState{
|
||||
id: id,
|
||||
identity: string(identity),
|
||||
signingKey: signingKey,
|
||||
encryptionKey: encryptionKey,
|
||||
userReadKeys: make(map[uint64]*symmetric.Key),
|
||||
userStates: make(map[string]*aclrecordproto.AclUserState),
|
||||
userInvites: make(map[string]*aclrecordproto.AclUserInvite),
|
||||
permissionsAtRecord: make(map[string][]UserPermissionPair),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newAclState(id string) *AclState {
|
||||
st := &AclState{
|
||||
id: id,
|
||||
userReadKeys: make(map[string]crypto.SymKey),
|
||||
userStates: make(map[string]AclUserState),
|
||||
statesAtRecord: make(map[string][]AclUserState),
|
||||
inviteKeys: make(map[string]crypto.PubKey),
|
||||
requestRecords: make(map[string]RequestRecord),
|
||||
pendingRequests: make(map[string]string),
|
||||
keyStore: crypto.NewKeyStorage(),
|
||||
return &AclState{
|
||||
id: id,
|
||||
userReadKeys: make(map[uint64]*symmetric.Key),
|
||||
userStates: make(map[string]*aclrecordproto.AclUserState),
|
||||
userInvites: make(map[string]*aclrecordproto.AclUserInvite),
|
||||
permissionsAtRecord: make(map[string][]UserPermissionPair),
|
||||
}
|
||||
st.contentValidator = &contentValidator{
|
||||
keyStore: st.keyStore,
|
||||
aclState: st,
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
func (st *AclState) Validator() ContentValidator {
|
||||
return st.contentValidator
|
||||
func (st *AclState) CurrentReadKeyHash() uint64 {
|
||||
return st.currentReadKeyHash
|
||||
}
|
||||
|
||||
func (st *AclState) CurrentReadKeyId() string {
|
||||
return st.currentReadKeyId
|
||||
}
|
||||
|
||||
func (st *AclState) CurrentReadKey() (crypto.SymKey, error) {
|
||||
key, exists := st.userReadKeys[st.CurrentReadKeyId()]
|
||||
func (st *AclState) CurrentReadKey() (*symmetric.Key, error) {
|
||||
key, exists := st.userReadKeys[st.currentReadKeyHash]
|
||||
if !exists {
|
||||
return nil, ErrNoReadKey
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (st *AclState) UserReadKeys() map[string]crypto.SymKey {
|
||||
func (st *AclState) UserReadKeys() map[uint64]*symmetric.Key {
|
||||
return st.userReadKeys
|
||||
}
|
||||
|
||||
func (st *AclState) StateAtRecord(id string, pubKey crypto.PubKey) (AclUserState, error) {
|
||||
userState, ok := st.statesAtRecord[id]
|
||||
func (st *AclState) PermissionsAtRecord(id string, identity string) (UserPermissionPair, error) {
|
||||
permissions, ok := st.permissionsAtRecord[id]
|
||||
if !ok {
|
||||
log.Errorf("missing record at id %s", id)
|
||||
return AclUserState{}, ErrNoSuchRecord
|
||||
return UserPermissionPair{}, ErrNoSuchRecord
|
||||
}
|
||||
|
||||
for _, perm := range userState {
|
||||
if perm.PubKey.Equals(pubKey) {
|
||||
for _, perm := range permissions {
|
||||
if perm.Identity == identity {
|
||||
return perm, nil
|
||||
}
|
||||
}
|
||||
return AclUserState{}, ErrNoSuchAccount
|
||||
return UserPermissionPair{}, ErrNoSuchUser
|
||||
}
|
||||
|
||||
func (st *AclState) applyRecord(record *AclRecord) (err error) {
|
||||
@@ -149,316 +128,338 @@ func (st *AclState) applyRecord(record *AclRecord) (err error) {
|
||||
err = ErrIncorrectRecordSequence
|
||||
return
|
||||
}
|
||||
// if the record is root record
|
||||
if record.Id == st.id {
|
||||
err = st.applyRoot(record)
|
||||
root, ok := record.Model.(*aclrecordproto.AclRoot)
|
||||
if !ok {
|
||||
return ErrIncorrectRoot
|
||||
}
|
||||
err = st.applyRoot(root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
st.statesAtRecord[record.Id] = []AclUserState{
|
||||
st.userStates[mapKeyFromPubKey(record.Identity)],
|
||||
st.permissionsAtRecord[record.Id] = []UserPermissionPair{
|
||||
{Identity: string(root.Identity), Permission: aclrecordproto.AclUserPermissions_Admin},
|
||||
}
|
||||
return
|
||||
}
|
||||
// if the model is not cached
|
||||
if record.Model == nil {
|
||||
aclData := &aclrecordproto.AclData{}
|
||||
aclData := &aclrecordproto.AclData{}
|
||||
|
||||
if record.Model != nil {
|
||||
aclData = record.Model.(*aclrecordproto.AclData)
|
||||
} else {
|
||||
err = proto.Unmarshal(record.Data, aclData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
record.Model = aclData
|
||||
}
|
||||
// applying records contents
|
||||
err = st.applyChangeData(record)
|
||||
|
||||
err = st.applyChangeData(aclData, record.CurrentReadKeyHash, record.Identity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// getting all states for users at record and saving them
|
||||
var states []AclUserState
|
||||
|
||||
// getting all permissions for users at record
|
||||
var permissions []UserPermissionPair
|
||||
for _, state := range st.userStates {
|
||||
states = append(states, state)
|
||||
permission := UserPermissionPair{
|
||||
Identity: string(state.Identity),
|
||||
Permission: state.Permissions,
|
||||
}
|
||||
permissions = append(permissions, permission)
|
||||
}
|
||||
st.statesAtRecord[record.Id] = states
|
||||
|
||||
st.permissionsAtRecord[record.Id] = permissions
|
||||
return
|
||||
}
|
||||
|
||||
func (st *AclState) applyRoot(record *AclRecord) (err error) {
|
||||
if st.key != nil && st.pubKey.Equals(record.Identity) {
|
||||
err = st.saveReadKeyFromRoot(record)
|
||||
func (st *AclState) applyRoot(root *aclrecordproto.AclRoot) (err error) {
|
||||
if st.signingKey != nil && st.encryptionKey != nil && st.identity == string(root.Identity) {
|
||||
err = st.saveReadKeyFromRoot(root)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// adding user to the list
|
||||
userState := AclUserState{
|
||||
PubKey: record.Identity,
|
||||
Permissions: AclPermissions(aclrecordproto.AclUserPermissions_Owner),
|
||||
userState := &aclrecordproto.AclUserState{
|
||||
Identity: root.Identity,
|
||||
EncryptionKey: root.EncryptionKey,
|
||||
Permissions: aclrecordproto.AclUserPermissions_Admin,
|
||||
}
|
||||
st.currentReadKeyId = record.Id
|
||||
st.userStates[mapKeyFromPubKey(record.Identity)] = userState
|
||||
st.currentReadKeyHash = root.CurrentReadKeyHash
|
||||
st.userStates[string(root.Identity)] = userState
|
||||
st.totalReadKeys++
|
||||
return
|
||||
}
|
||||
|
||||
func (st *AclState) saveReadKeyFromRoot(record *AclRecord) (err error) {
|
||||
var readKey crypto.SymKey
|
||||
root, ok := record.Model.(*aclrecordproto.AclRoot)
|
||||
if !ok {
|
||||
return ErrIncorrectRoot
|
||||
}
|
||||
if root.EncryptedReadKey == nil {
|
||||
readKey, err = st.deriveKey()
|
||||
func (st *AclState) saveReadKeyFromRoot(root *aclrecordproto.AclRoot) (err error) {
|
||||
var readKey *symmetric.Key
|
||||
if len(root.GetDerivationScheme()) != 0 {
|
||||
var encPrivKey []byte
|
||||
encPrivKey, err = st.encryptionKey.Raw()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var signPrivKey []byte
|
||||
signPrivKey, err = st.signingKey.Raw()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
readKey, err = aclrecordproto.AclReadKeyDerive(signPrivKey, encPrivKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
readKey, err = st.decryptReadKey(root.EncryptedReadKey)
|
||||
readKey, _, err = st.decryptReadKeyAndHash(root.EncryptedReadKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
st.userReadKeys[record.Id] = readKey
|
||||
|
||||
hasher := fnv.New64()
|
||||
_, err = hasher.Write(readKey.Bytes())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if hasher.Sum64() != root.CurrentReadKeyHash {
|
||||
return ErrIncorrectRoot
|
||||
}
|
||||
st.userReadKeys[root.CurrentReadKeyHash] = readKey
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (st *AclState) applyChangeData(record *AclRecord) (err error) {
|
||||
model := record.Model.(*aclrecordproto.AclData)
|
||||
for _, ch := range model.GetAclContent() {
|
||||
if err = st.applyChangeContent(ch, record.Id, record.Identity); err != nil {
|
||||
func (st *AclState) applyChangeData(changeData *aclrecordproto.AclData, hash uint64, identity []byte) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if hash != st.currentReadKeyHash {
|
||||
st.totalReadKeys++
|
||||
st.currentReadKeyHash = hash
|
||||
}
|
||||
}()
|
||||
|
||||
if !st.isUserJoin(changeData) {
|
||||
// we check signature when we add this to the List, so no need to do it here
|
||||
if _, exists := st.userStates[string(identity)]; !exists {
|
||||
err = ErrNoSuchUser
|
||||
return
|
||||
}
|
||||
|
||||
if !st.HasPermission(identity, aclrecordproto.AclUserPermissions_Admin) {
|
||||
err = fmt.Errorf("user %s must have admin permissions", identity)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, ch := range changeData.GetAclContent() {
|
||||
if err = st.applyChangeContent(ch); err != nil {
|
||||
log.Info("error while applying changes: %v; ignore", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) applyChangeContent(ch *aclrecordproto.AclContentValue, recordId string, authorIdentity crypto.PubKey) error {
|
||||
func (st *AclState) applyChangeContent(ch *aclrecordproto.AclContentValue) error {
|
||||
switch {
|
||||
case ch.GetPermissionChange() != nil:
|
||||
return st.applyPermissionChange(ch.GetPermissionChange(), recordId, authorIdentity)
|
||||
case ch.GetInvite() != nil:
|
||||
return st.applyInvite(ch.GetInvite(), recordId, authorIdentity)
|
||||
case ch.GetInviteRevoke() != nil:
|
||||
return st.applyInviteRevoke(ch.GetInviteRevoke(), recordId, authorIdentity)
|
||||
case ch.GetRequestJoin() != nil:
|
||||
return st.applyRequestJoin(ch.GetRequestJoin(), recordId, authorIdentity)
|
||||
case ch.GetRequestAccept() != nil:
|
||||
return st.applyRequestAccept(ch.GetRequestAccept(), recordId, authorIdentity)
|
||||
case ch.GetRequestDecline() != nil:
|
||||
return st.applyRequestDecline(ch.GetRequestDecline(), recordId, authorIdentity)
|
||||
case ch.GetAccountRemove() != nil:
|
||||
return st.applyAccountRemove(ch.GetAccountRemove(), recordId, authorIdentity)
|
||||
case ch.GetReadKeyChange() != nil:
|
||||
return st.applyReadKeyChange(ch.GetReadKeyChange(), recordId, authorIdentity)
|
||||
case ch.GetAccountRequestRemove() != nil:
|
||||
return st.applyRequestRemove(ch.GetAccountRequestRemove(), recordId, authorIdentity)
|
||||
case ch.GetUserPermissionChange() != nil:
|
||||
return st.applyUserPermissionChange(ch.GetUserPermissionChange())
|
||||
case ch.GetUserAdd() != nil:
|
||||
return st.applyUserAdd(ch.GetUserAdd())
|
||||
case ch.GetUserRemove() != nil:
|
||||
return st.applyUserRemove(ch.GetUserRemove())
|
||||
case ch.GetUserInvite() != nil:
|
||||
return st.applyUserInvite(ch.GetUserInvite())
|
||||
case ch.GetUserJoin() != nil:
|
||||
return st.applyUserJoin(ch.GetUserJoin())
|
||||
default:
|
||||
return ErrUnexpectedContentType
|
||||
return fmt.Errorf("unexpected change type: %v", ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (st *AclState) applyPermissionChange(ch *aclrecordproto.AclAccountPermissionChange, recordId string, authorIdentity crypto.PubKey) error {
|
||||
chIdentity, err := st.keyStore.PubKeyFromProto(ch.Identity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = st.contentValidator.ValidatePermissionChange(ch, authorIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stringKey := mapKeyFromPubKey(chIdentity)
|
||||
state, _ := st.userStates[stringKey]
|
||||
state.Permissions = AclPermissions(ch.Permissions)
|
||||
st.userStates[stringKey] = state
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) applyInvite(ch *aclrecordproto.AclAccountInvite, recordId string, authorIdentity crypto.PubKey) error {
|
||||
inviteKey, err := st.keyStore.PubKeyFromProto(ch.InviteKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = st.contentValidator.ValidateInvite(ch, authorIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.inviteKeys[recordId] = inviteKey
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) applyInviteRevoke(ch *aclrecordproto.AclAccountInviteRevoke, recordId string, authorIdentity crypto.PubKey) error {
|
||||
err := st.contentValidator.ValidateInviteRevoke(ch, authorIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delete(st.inviteKeys, ch.InviteRecordId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) applyRequestJoin(ch *aclrecordproto.AclAccountRequestJoin, recordId string, authorIdentity crypto.PubKey) error {
|
||||
err := st.contentValidator.ValidateRequestJoin(ch, authorIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.pendingRequests[mapKeyFromPubKey(authorIdentity)] = recordId
|
||||
st.requestRecords[recordId] = RequestRecord{
|
||||
RequestIdentity: authorIdentity,
|
||||
RequestMetadata: ch.Metadata,
|
||||
Type: RequestTypeJoin,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) applyRequestAccept(ch *aclrecordproto.AclAccountRequestAccept, recordId string, authorIdentity crypto.PubKey) error {
|
||||
err := st.contentValidator.ValidateRequestAccept(ch, authorIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
acceptIdentity, err := st.keyStore.PubKeyFromProto(ch.Identity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record, _ := st.requestRecords[ch.RequestRecordId]
|
||||
st.userStates[mapKeyFromPubKey(acceptIdentity)] = AclUserState{
|
||||
PubKey: acceptIdentity,
|
||||
Permissions: AclPermissions(ch.Permissions),
|
||||
RequestMetadata: record.RequestMetadata,
|
||||
}
|
||||
delete(st.pendingRequests, mapKeyFromPubKey(st.requestRecords[ch.RequestRecordId].RequestIdentity))
|
||||
if !st.pubKey.Equals(acceptIdentity) {
|
||||
return nil
|
||||
}
|
||||
for _, key := range ch.EncryptedReadKeys {
|
||||
decrypted, err := st.key.Decrypt(key.EncryptedReadKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sym, err := crypto.UnmarshallAESKey(decrypted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.userReadKeys[key.RecordId] = sym
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) applyRequestDecline(ch *aclrecordproto.AclAccountRequestDecline, recordId string, authorIdentity crypto.PubKey) error {
|
||||
err := st.contentValidator.ValidateRequestDecline(ch, authorIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delete(st.pendingRequests, mapKeyFromPubKey(st.requestRecords[ch.RequestRecordId].RequestIdentity))
|
||||
delete(st.requestRecords, ch.RequestRecordId)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) applyRequestRemove(ch *aclrecordproto.AclAccountRequestRemove, recordId string, authorIdentity crypto.PubKey) error {
|
||||
err := st.contentValidator.ValidateRequestRemove(ch, authorIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.requestRecords[recordId] = RequestRecord{
|
||||
RequestIdentity: authorIdentity,
|
||||
Type: RequestTypeRemove,
|
||||
}
|
||||
st.pendingRequests[mapKeyFromPubKey(authorIdentity)] = recordId
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) applyAccountRemove(ch *aclrecordproto.AclAccountRemove, recordId string, authorIdentity crypto.PubKey) error {
|
||||
err := st.contentValidator.ValidateAccountRemove(ch, authorIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rawIdentity := range ch.Identities {
|
||||
identity, err := st.keyStore.PubKeyFromProto(rawIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idKey := mapKeyFromPubKey(identity)
|
||||
delete(st.userStates, idKey)
|
||||
delete(st.pendingRequests, idKey)
|
||||
}
|
||||
return st.updateReadKey(ch.AccountKeys, recordId)
|
||||
}
|
||||
|
||||
func (st *AclState) applyReadKeyChange(ch *aclrecordproto.AclReadKeyChange, recordId string, authorIdentity crypto.PubKey) error {
|
||||
err := st.contentValidator.ValidateReadKeyChange(ch, authorIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.updateReadKey(ch.AccountKeys, recordId)
|
||||
}
|
||||
|
||||
func (st *AclState) updateReadKey(keys []*aclrecordproto.AclEncryptedReadKey, recordId string) error {
|
||||
for _, accKey := range keys {
|
||||
identity, _ := st.keyStore.PubKeyFromProto(accKey.Identity)
|
||||
if st.pubKey.Equals(identity) {
|
||||
res, err := st.decryptReadKey(accKey.EncryptedReadKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.userReadKeys[recordId] = res
|
||||
}
|
||||
}
|
||||
st.currentReadKeyId = recordId
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) decryptReadKey(msg []byte) (crypto.SymKey, error) {
|
||||
decrypted, err := st.key.Decrypt(msg)
|
||||
if err != nil {
|
||||
return nil, ErrFailedToDecrypt
|
||||
}
|
||||
key, err := crypto.UnmarshallAESKey(decrypted)
|
||||
if err != nil {
|
||||
return nil, ErrFailedToDecrypt
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (st *AclState) Permissions(identity crypto.PubKey) AclPermissions {
|
||||
state, exists := st.userStates[mapKeyFromPubKey(identity)]
|
||||
func (st *AclState) applyUserPermissionChange(ch *aclrecordproto.AclUserPermissionChange) error {
|
||||
chIdentity := string(ch.Identity)
|
||||
state, exists := st.userStates[chIdentity]
|
||||
if !exists {
|
||||
return AclPermissions(aclrecordproto.AclUserPermissions_None)
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
return state.Permissions
|
||||
|
||||
state.Permissions = ch.Permissions
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) JoinRecords() (records []RequestRecord) {
|
||||
for _, recId := range st.pendingRequests {
|
||||
rec := st.requestRecords[recId]
|
||||
if rec.Type == RequestTypeJoin {
|
||||
records = append(records, rec)
|
||||
func (st *AclState) applyUserInvite(ch *aclrecordproto.AclUserInvite) error {
|
||||
st.userInvites[string(ch.AcceptPublicKey)] = ch
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) applyUserJoin(ch *aclrecordproto.AclUserJoin) error {
|
||||
invite, exists := st.userInvites[string(ch.AcceptPubKey)]
|
||||
if !exists {
|
||||
return fmt.Errorf("no such invite with such public key %s", keys.EncodeBytesToString(ch.AcceptPubKey))
|
||||
}
|
||||
chIdentity := string(ch.Identity)
|
||||
|
||||
if _, exists = st.userStates[chIdentity]; exists {
|
||||
return ErrUserAlreadyExists
|
||||
}
|
||||
|
||||
// validating signature
|
||||
signature := ch.GetAcceptSignature()
|
||||
verificationKey, err := signingkey.NewSigningEd25519PubKeyFromBytes(invite.AcceptPublicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("public key verifying invite accepts is given in incorrect format: %v", err)
|
||||
}
|
||||
|
||||
res, err := verificationKey.Verify(ch.Identity, signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verification returned error: %w", err)
|
||||
}
|
||||
if !res {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
|
||||
// if ourselves -> we need to decrypt the read keys
|
||||
if st.identity == chIdentity {
|
||||
for _, key := range ch.EncryptedReadKeys {
|
||||
key, hash, err := st.decryptReadKeyAndHash(key)
|
||||
if err != nil {
|
||||
return ErrFailedToDecrypt
|
||||
}
|
||||
|
||||
st.userReadKeys[hash] = key
|
||||
}
|
||||
}
|
||||
|
||||
// adding user to the list
|
||||
userState := &aclrecordproto.AclUserState{
|
||||
Identity: ch.Identity,
|
||||
EncryptionKey: ch.EncryptionKey,
|
||||
Permissions: invite.Permissions,
|
||||
}
|
||||
st.userStates[chIdentity] = userState
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) applyUserAdd(ch *aclrecordproto.AclUserAdd) error {
|
||||
chIdentity := string(ch.Identity)
|
||||
if _, exists := st.userStates[chIdentity]; exists {
|
||||
return ErrUserAlreadyExists
|
||||
}
|
||||
|
||||
st.userStates[chIdentity] = &aclrecordproto.AclUserState{
|
||||
Identity: ch.Identity,
|
||||
EncryptionKey: ch.EncryptionKey,
|
||||
Permissions: ch.Permissions,
|
||||
}
|
||||
|
||||
if chIdentity == st.identity {
|
||||
for _, key := range ch.EncryptedReadKeys {
|
||||
key, hash, err := st.decryptReadKeyAndHash(key)
|
||||
if err != nil {
|
||||
return ErrFailedToDecrypt
|
||||
}
|
||||
|
||||
st.userReadKeys[hash] = key
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) applyUserRemove(ch *aclrecordproto.AclUserRemove) error {
|
||||
chIdentity := string(ch.Identity)
|
||||
if chIdentity == st.identity {
|
||||
return ErrDocumentForbidden
|
||||
}
|
||||
|
||||
if _, exists := st.userStates[chIdentity]; !exists {
|
||||
return ErrNoSuchUser
|
||||
}
|
||||
|
||||
delete(st.userStates, chIdentity)
|
||||
|
||||
for _, replace := range ch.ReadKeyReplaces {
|
||||
repIdentity := string(replace.Identity)
|
||||
// if this is our identity then we have to decrypt the key
|
||||
if repIdentity == st.identity {
|
||||
key, hash, err := st.decryptReadKeyAndHash(replace.EncryptedReadKey)
|
||||
if err != nil {
|
||||
return ErrFailedToDecrypt
|
||||
}
|
||||
|
||||
st.userReadKeys[hash] = key
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *AclState) decryptReadKeyAndHash(msg []byte) (*symmetric.Key, uint64, error) {
|
||||
decrypted, err := st.encryptionKey.Decrypt(msg)
|
||||
if err != nil {
|
||||
return nil, 0, ErrFailedToDecrypt
|
||||
}
|
||||
|
||||
key, err := symmetric.FromBytes(decrypted)
|
||||
if err != nil {
|
||||
return nil, 0, ErrFailedToDecrypt
|
||||
}
|
||||
|
||||
hasher := fnv.New64()
|
||||
hasher.Write(decrypted)
|
||||
return key, hasher.Sum64(), nil
|
||||
}
|
||||
|
||||
func (st *AclState) HasPermission(identity []byte, permission aclrecordproto.AclUserPermissions) bool {
|
||||
state, exists := st.userStates[string(identity)]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
return state.Permissions == permission
|
||||
}
|
||||
|
||||
func (st *AclState) isUserJoin(data *aclrecordproto.AclData) bool {
|
||||
// if we have a UserJoin, then it should always be the first one applied
|
||||
return data.GetAclContent() != nil && data.GetAclContent()[0].GetUserJoin() != nil
|
||||
}
|
||||
|
||||
func (st *AclState) isUserAdd(data *aclrecordproto.AclData, identity []byte) bool {
|
||||
// if we have a UserAdd, then it should always be the first one applied
|
||||
userAdd := data.GetAclContent()[0].GetUserAdd()
|
||||
return data.GetAclContent() != nil && userAdd != nil && bytes.Compare(userAdd.GetIdentity(), identity) == 0
|
||||
}
|
||||
|
||||
func (st *AclState) UserStates() map[string]*aclrecordproto.AclUserState {
|
||||
return st.userStates
|
||||
}
|
||||
|
||||
func (st *AclState) Invite(acceptPubKey []byte) (invite *aclrecordproto.AclUserInvite, err error) {
|
||||
invite, exists := st.userInvites[string(acceptPubKey)]
|
||||
if !exists {
|
||||
err = ErrNoSuchInvite
|
||||
return
|
||||
}
|
||||
if len(invite.EncryptedReadKeys) != st.totalReadKeys {
|
||||
err = ErrOldInvite
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (st *AclState) RemoveRecords() (records []RequestRecord) {
|
||||
for _, recId := range st.pendingRequests {
|
||||
rec := st.requestRecords[recId]
|
||||
if rec.Type == RequestTypeRemove {
|
||||
records = append(records, rec)
|
||||
}
|
||||
}
|
||||
return
|
||||
func (st *AclState) UserKeys() (encKey encryptionkey.PrivKey, signKey signingkey.PrivKey) {
|
||||
return st.encryptionKey, st.signingKey
|
||||
}
|
||||
|
||||
func (st *AclState) Identity() []byte {
|
||||
return []byte(st.identity)
|
||||
}
|
||||
|
||||
func (st *AclState) LastRecordId() string {
|
||||
return st.lastRecordId
|
||||
}
|
||||
|
||||
func (st *AclState) deriveKey() (crypto.SymKey, error) {
|
||||
keyBytes, err := st.key.Raw()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crypto.DeriveSymmetricKey(keyBytes, crypto.AnysyncSpacePath)
|
||||
}
|
||||
|
||||
func mapKeyFromPubKey(pubKey crypto.PubKey) string {
|
||||
return string(pubKey.Storage())
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"github.com/anyproto/any-sync/commonspace/object/accountdata"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/accountdata"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/encryptionkey"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/signingkey"
|
||||
)
|
||||
|
||||
type aclStateBuilder struct {
|
||||
privKey crypto.PrivKey
|
||||
id string
|
||||
signPrivKey signingkey.PrivKey
|
||||
encPrivKey encryptionkey.PrivKey
|
||||
id string
|
||||
}
|
||||
|
||||
func newAclStateBuilderWithIdentity(keys *accountdata.AccountKeys) *aclStateBuilder {
|
||||
func newAclStateBuilderWithIdentity(accountData *accountdata.AccountData) *aclStateBuilder {
|
||||
return &aclStateBuilder{
|
||||
privKey: keys.SignKey,
|
||||
signPrivKey: accountData.SignKey,
|
||||
encPrivKey: accountData.EncKey,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +28,8 @@ func (sb *aclStateBuilder) Init(id string) {
|
||||
}
|
||||
|
||||
func (sb *aclStateBuilder) Build(records []*AclRecord) (state *AclState, err error) {
|
||||
if sb.privKey != nil {
|
||||
state, err = newAclStateWithKeys(sb.id, sb.privKey)
|
||||
if sb.encPrivKey != nil && sb.signPrivKey != nil {
|
||||
state, err = newAclStateWithKeys(sb.id, sb.signPrivKey, sb.encPrivKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
//go:generate mockgen -destination mock_list/mock_list.go github.com/anyproto/any-sync/commonspace/object/acl/list AclList
|
||||
//go:generate mockgen -destination mock_list/mock_list.go github.com/anytypeio/any-sync/commonspace/object/acl/list AclList
|
||||
package list
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/accountdata"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/liststorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/keychain"
|
||||
"sync"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/accountdata"
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/liststorage"
|
||||
"github.com/anyproto/any-sync/consensus/consensusproto"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
)
|
||||
|
||||
type IterFunc = func(record *AclRecord) (IsContinue bool)
|
||||
|
||||
var (
|
||||
ErrIncorrectCID = errors.New("incorrect CID")
|
||||
ErrRecordAlreadyExists = errors.New("record already exists")
|
||||
)
|
||||
var ErrIncorrectCID = errors.New("incorrect CID")
|
||||
|
||||
type RWLocker interface {
|
||||
sync.Locker
|
||||
@@ -26,93 +22,48 @@ type RWLocker interface {
|
||||
RUnlock()
|
||||
}
|
||||
|
||||
type AcceptorVerifier interface {
|
||||
VerifyAcceptor(rec *consensusproto.RawRecord) (err error)
|
||||
}
|
||||
|
||||
type NoOpAcceptorVerifier struct {
|
||||
}
|
||||
|
||||
func (n NoOpAcceptorVerifier) VerifyAcceptor(rec *consensusproto.RawRecord) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
type AclList interface {
|
||||
RWLocker
|
||||
Id() string
|
||||
Root() *consensusproto.RawRecordWithId
|
||||
Root() *aclrecordproto.RawAclRecordWithId
|
||||
Records() []*AclRecord
|
||||
AclState() *AclState
|
||||
IsAfter(first string, second string) (bool, error)
|
||||
Head() *AclRecord
|
||||
Get(id string) (*AclRecord, error)
|
||||
GetIndex(idx int) (*AclRecord, error)
|
||||
Iterate(iterFunc IterFunc)
|
||||
IterateFrom(startId string, iterFunc IterFunc)
|
||||
|
||||
KeyStorage() crypto.KeyStorage
|
||||
RecordBuilder() AclRecordBuilder
|
||||
|
||||
ValidateRawRecord(record *consensusproto.RawRecord) (err error)
|
||||
AddRawRecord(rawRec *consensusproto.RawRecordWithId) (err error)
|
||||
AddRawRecord(rawRec *aclrecordproto.RawAclRecordWithId) (added bool, err error)
|
||||
|
||||
Close() (err error)
|
||||
}
|
||||
|
||||
type aclList struct {
|
||||
root *consensusproto.RawRecordWithId
|
||||
root *aclrecordproto.RawAclRecordWithId
|
||||
records []*AclRecord
|
||||
indexes map[string]int
|
||||
id string
|
||||
|
||||
stateBuilder *aclStateBuilder
|
||||
recordBuilder AclRecordBuilder
|
||||
keyStorage crypto.KeyStorage
|
||||
aclState *AclState
|
||||
keychain *keychain.Keychain
|
||||
storage liststorage.ListStorage
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type internalDeps struct {
|
||||
storage liststorage.ListStorage
|
||||
keyStorage crypto.KeyStorage
|
||||
stateBuilder *aclStateBuilder
|
||||
recordBuilder AclRecordBuilder
|
||||
acceptorVerifier AcceptorVerifier
|
||||
func BuildAclListWithIdentity(acc *accountdata.AccountData, storage liststorage.ListStorage) (AclList, error) {
|
||||
builder := newAclStateBuilderWithIdentity(acc)
|
||||
return build(storage.Id(), builder, newAclRecordBuilder(storage.Id(), keychain.NewKeychain()), storage)
|
||||
}
|
||||
|
||||
func BuildAclListWithIdentity(acc *accountdata.AccountKeys, storage liststorage.ListStorage, verifier AcceptorVerifier) (AclList, error) {
|
||||
keyStorage := crypto.NewKeyStorage()
|
||||
deps := internalDeps{
|
||||
storage: storage,
|
||||
keyStorage: keyStorage,
|
||||
stateBuilder: newAclStateBuilderWithIdentity(acc),
|
||||
recordBuilder: NewAclRecordBuilder(storage.Id(), keyStorage, acc, verifier),
|
||||
acceptorVerifier: verifier,
|
||||
}
|
||||
return build(deps)
|
||||
func BuildAclList(storage liststorage.ListStorage) (AclList, error) {
|
||||
return build(storage.Id(), newAclStateBuilder(), newAclRecordBuilder(storage.Id(), keychain.NewKeychain()), storage)
|
||||
}
|
||||
|
||||
func BuildAclList(storage liststorage.ListStorage, verifier AcceptorVerifier) (AclList, error) {
|
||||
keyStorage := crypto.NewKeyStorage()
|
||||
deps := internalDeps{
|
||||
storage: storage,
|
||||
keyStorage: keyStorage,
|
||||
stateBuilder: newAclStateBuilder(),
|
||||
recordBuilder: NewAclRecordBuilder(storage.Id(), keyStorage, nil, verifier),
|
||||
acceptorVerifier: verifier,
|
||||
}
|
||||
return build(deps)
|
||||
}
|
||||
|
||||
func build(deps internalDeps) (list AclList, err error) {
|
||||
var (
|
||||
storage = deps.storage
|
||||
id = deps.storage.Id()
|
||||
recBuilder = deps.recordBuilder
|
||||
stateBuilder = deps.stateBuilder
|
||||
)
|
||||
func build(id string, stateBuilder *aclStateBuilder, recBuilder AclRecordBuilder, storage liststorage.ListStorage) (list AclList, err error) {
|
||||
head, err := storage.Head()
|
||||
if err != nil {
|
||||
return
|
||||
@@ -123,7 +74,7 @@ func build(deps internalDeps) (list AclList, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
record, err := recBuilder.UnmarshallWithId(rawRecordWithId)
|
||||
record, err := recBuilder.ConvertFromRaw(rawRecordWithId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -135,7 +86,7 @@ func build(deps internalDeps) (list AclList, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
record, err = recBuilder.UnmarshallWithId(rawRecordWithId)
|
||||
record, err = recBuilder.ConvertFromRaw(rawRecordWithId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -165,7 +116,6 @@ func build(deps internalDeps) (list AclList, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
recBuilder.(*aclRecordBuilder).state = state
|
||||
list = &aclList{
|
||||
root: rootWithId,
|
||||
records: records,
|
||||
@@ -179,27 +129,15 @@ func build(deps internalDeps) (list AclList, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (a *aclList) RecordBuilder() AclRecordBuilder {
|
||||
return a.recordBuilder
|
||||
}
|
||||
|
||||
func (a *aclList) Records() []*AclRecord {
|
||||
return a.records
|
||||
}
|
||||
|
||||
func (a *aclList) ValidateRawRecord(rawRec *consensusproto.RawRecord) (err error) {
|
||||
record, err := a.recordBuilder.Unmarshall(rawRec)
|
||||
if err != nil {
|
||||
func (a *aclList) AddRawRecord(rawRec *aclrecordproto.RawAclRecordWithId) (added bool, err error) {
|
||||
if _, ok := a.indexes[rawRec.Id]; ok {
|
||||
return
|
||||
}
|
||||
return a.aclState.Validator().ValidateAclRecordContents(record)
|
||||
}
|
||||
|
||||
func (a *aclList) AddRawRecord(rawRec *consensusproto.RawRecordWithId) (err error) {
|
||||
if _, ok := a.indexes[rawRec.Id]; ok {
|
||||
return ErrRecordAlreadyExists
|
||||
}
|
||||
record, err := a.recordBuilder.UnmarshallWithId(rawRec)
|
||||
record, err := a.recordBuilder.ConvertFromRaw(rawRec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -214,6 +152,15 @@ func (a *aclList) AddRawRecord(rawRec *consensusproto.RawRecordWithId) (err erro
|
||||
if err = a.storage.SetHead(rawRec.Id); err != nil {
|
||||
return
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *aclList) IsValidNext(rawRec *aclrecordproto.RawAclRecordWithId) (err error) {
|
||||
_, err = a.recordBuilder.ConvertFromRaw(rawRec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// TODO: change state and add "check" method for records
|
||||
return
|
||||
}
|
||||
|
||||
@@ -221,7 +168,7 @@ func (a *aclList) Id() string {
|
||||
return a.id
|
||||
}
|
||||
|
||||
func (a *aclList) Root() *consensusproto.RawRecordWithId {
|
||||
func (a *aclList) Root() *aclrecordproto.RawAclRecordWithId {
|
||||
return a.root
|
||||
}
|
||||
|
||||
@@ -229,10 +176,6 @@ func (a *aclList) AclState() *AclState {
|
||||
return a.aclState
|
||||
}
|
||||
|
||||
func (a *aclList) KeyStorage() crypto.KeyStorage {
|
||||
return a.keyStorage
|
||||
}
|
||||
|
||||
func (a *aclList) IsAfter(first string, second string) (bool, error) {
|
||||
firstRec, okFirst := a.indexes[first]
|
||||
secondRec, okSecond := a.indexes[second]
|
||||
@@ -249,19 +192,11 @@ func (a *aclList) Head() *AclRecord {
|
||||
func (a *aclList) Get(id string) (*AclRecord, error) {
|
||||
recIdx, ok := a.indexes[id]
|
||||
if !ok {
|
||||
return nil, ErrNoSuchRecord
|
||||
return nil, fmt.Errorf("no such record")
|
||||
}
|
||||
return a.records[recIdx], nil
|
||||
}
|
||||
|
||||
func (a *aclList) GetIndex(idx int) (*AclRecord, error) {
|
||||
// TODO: when we add snapshots we will have to monitor record num in snapshots
|
||||
if idx < 0 || idx >= len(a.records) {
|
||||
return nil, ErrNoSuchRecord
|
||||
}
|
||||
return a.records[idx], nil
|
||||
}
|
||||
|
||||
func (a *aclList) Iterate(iterFunc IterFunc) {
|
||||
for _, rec := range a.records {
|
||||
if !iterFunc(rec) {
|
||||
|
||||
@@ -1,309 +1,91 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/accountdata"
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anyproto/any-sync/consensus/consensusproto"
|
||||
"github.com/anyproto/any-sync/util/cidutil"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/testutils/acllistbuilder"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func wrapRecord(rawRec *consensusproto.RawRecord) *consensusproto.RawRecordWithId {
|
||||
payload, err := rawRec.Marshal()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
id, err := cidutil.NewCidFromBytes(payload)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &consensusproto.RawRecordWithId{
|
||||
Payload: payload,
|
||||
Id: id,
|
||||
}
|
||||
}
|
||||
func TestAclList_AclState_UserInviteAndJoin(t *testing.T) {
|
||||
st, err := acllistbuilder.NewListStorageWithTestName("userjoinexample.yml")
|
||||
require.NoError(t, err, "building storage should not result in error")
|
||||
|
||||
type aclFixture struct {
|
||||
ownerKeys *accountdata.AccountKeys
|
||||
accountKeys *accountdata.AccountKeys
|
||||
ownerAcl *aclList
|
||||
accountAcl *aclList
|
||||
spaceId string
|
||||
}
|
||||
keychain := st.(*acllistbuilder.AclListStorageBuilder).GetKeychain()
|
||||
|
||||
func newFixture(t *testing.T) *aclFixture {
|
||||
ownerKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
accountKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
spaceId := "spaceId"
|
||||
ownerAcl, err := NewTestDerivedAcl(spaceId, ownerKeys)
|
||||
require.NoError(t, err)
|
||||
accountAcl, err := NewTestAclWithRoot(accountKeys, ownerAcl.Root())
|
||||
require.NoError(t, err)
|
||||
return &aclFixture{
|
||||
ownerKeys: ownerKeys,
|
||||
accountKeys: accountKeys,
|
||||
ownerAcl: ownerAcl.(*aclList),
|
||||
accountAcl: accountAcl.(*aclList),
|
||||
spaceId: spaceId,
|
||||
}
|
||||
}
|
||||
aclList, err := BuildAclList(st)
|
||||
require.NoError(t, err, "building acl list should be without error")
|
||||
|
||||
func (fx *aclFixture) addRec(t *testing.T, rec *consensusproto.RawRecordWithId) {
|
||||
err := fx.ownerAcl.AddRawRecord(rec)
|
||||
require.NoError(t, err)
|
||||
err = fx.accountAcl.AddRawRecord(rec)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
idA := keychain.GetIdentity("A")
|
||||
idB := keychain.GetIdentity("B")
|
||||
idC := keychain.GetIdentity("C")
|
||||
|
||||
func (fx *aclFixture) inviteAccount(t *testing.T, perms AclPermissions) {
|
||||
var (
|
||||
ownerAcl = fx.ownerAcl
|
||||
ownerState = fx.ownerAcl.aclState
|
||||
accountAcl = fx.accountAcl
|
||||
accountState = fx.accountAcl.aclState
|
||||
)
|
||||
// building invite
|
||||
inv, err := ownerAcl.RecordBuilder().BuildInvite()
|
||||
require.NoError(t, err)
|
||||
inviteRec := wrapRecord(inv.InviteRec)
|
||||
fx.addRec(t, inviteRec)
|
||||
// checking final state
|
||||
assert.Equal(t, aclrecordproto.AclUserPermissions_Admin, aclList.AclState().UserStates()[idA].Permissions)
|
||||
assert.Equal(t, aclrecordproto.AclUserPermissions_Writer, aclList.AclState().UserStates()[idB].Permissions)
|
||||
assert.Equal(t, aclrecordproto.AclUserPermissions_Reader, aclList.AclState().UserStates()[idC].Permissions)
|
||||
assert.Equal(t, aclList.Head().CurrentReadKeyHash, aclList.AclState().CurrentReadKeyHash())
|
||||
|
||||
// building request join
|
||||
requestJoin, err := accountAcl.RecordBuilder().BuildRequestJoin(RequestJoinPayload{
|
||||
InviteRecordId: inviteRec.Id,
|
||||
InviteKey: inv.InviteKey,
|
||||
var records []*AclRecord
|
||||
aclList.Iterate(func(record *AclRecord) (IsContinue bool) {
|
||||
records = append(records, record)
|
||||
return true
|
||||
})
|
||||
require.NoError(t, err)
|
||||
requestJoinRec := wrapRecord(requestJoin)
|
||||
fx.addRec(t, requestJoinRec)
|
||||
|
||||
// building request accept
|
||||
requestAccept, err := ownerAcl.RecordBuilder().BuildRequestAccept(RequestAcceptPayload{
|
||||
RequestRecordId: requestJoinRec.Id,
|
||||
Permissions: perms,
|
||||
// checking permissions at specific records
|
||||
assert.Equal(t, 3, len(records))
|
||||
|
||||
_, err = aclList.AclState().PermissionsAtRecord(records[1].Id, idB)
|
||||
assert.Error(t, err, "B should have no permissions at record 1")
|
||||
|
||||
perm, err := aclList.AclState().PermissionsAtRecord(records[2].Id, idB)
|
||||
assert.NoError(t, err, "should have no error with permissions of B in the record 2")
|
||||
assert.Equal(t, UserPermissionPair{
|
||||
Identity: idB,
|
||||
Permission: aclrecordproto.AclUserPermissions_Writer,
|
||||
}, perm)
|
||||
}
|
||||
|
||||
func TestAclList_AclState_UserJoinAndRemove(t *testing.T) {
|
||||
st, err := acllistbuilder.NewListStorageWithTestName("userremoveexample.yml")
|
||||
require.NoError(t, err, "building storage should not result in error")
|
||||
|
||||
keychain := st.(*acllistbuilder.AclListStorageBuilder).GetKeychain()
|
||||
|
||||
aclList, err := BuildAclList(st)
|
||||
require.NoError(t, err, "building acl list should be without error")
|
||||
|
||||
idA := keychain.GetIdentity("A")
|
||||
idB := keychain.GetIdentity("B")
|
||||
idC := keychain.GetIdentity("C")
|
||||
|
||||
// checking final state
|
||||
assert.Equal(t, aclrecordproto.AclUserPermissions_Admin, aclList.AclState().UserStates()[idA].Permissions)
|
||||
assert.Equal(t, aclrecordproto.AclUserPermissions_Reader, aclList.AclState().UserStates()[idC].Permissions)
|
||||
assert.Equal(t, aclList.Head().CurrentReadKeyHash, aclList.AclState().CurrentReadKeyHash())
|
||||
|
||||
_, exists := aclList.AclState().UserStates()[idB]
|
||||
assert.Equal(t, false, exists)
|
||||
|
||||
var records []*AclRecord
|
||||
aclList.Iterate(func(record *AclRecord) (IsContinue bool) {
|
||||
records = append(records, record)
|
||||
return true
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// validate
|
||||
err = ownerAcl.ValidateRawRecord(requestAccept)
|
||||
require.NoError(t, err)
|
||||
requestAcceptRec := wrapRecord(requestAccept)
|
||||
fx.addRec(t, requestAcceptRec)
|
||||
|
||||
// checking acl state
|
||||
require.True(t, ownerState.Permissions(ownerState.pubKey).IsOwner())
|
||||
require.True(t, ownerState.Permissions(accountState.pubKey).CanWrite())
|
||||
require.Equal(t, 0, len(ownerState.pendingRequests))
|
||||
require.Equal(t, 0, len(accountState.pendingRequests))
|
||||
require.True(t, accountState.Permissions(ownerState.pubKey).IsOwner())
|
||||
require.True(t, accountState.Permissions(accountState.pubKey).CanWrite())
|
||||
// checking permissions at specific records
|
||||
assert.Equal(t, 4, len(records))
|
||||
|
||||
_, err = ownerState.StateAtRecord(requestJoinRec.Id, accountState.pubKey)
|
||||
require.Equal(t, ErrNoSuchAccount, err)
|
||||
stateAtRec, err := ownerState.StateAtRecord(requestAcceptRec.Id, accountState.pubKey)
|
||||
require.NoError(t, err)
|
||||
require.True(t, stateAtRec.Permissions == perms)
|
||||
}
|
||||
|
||||
func TestAclList_BuildRoot(t *testing.T) {
|
||||
randomKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
randomAcl, err := NewTestDerivedAcl("spaceId", randomKeys)
|
||||
require.NoError(t, err)
|
||||
fmt.Println(randomAcl.Id())
|
||||
}
|
||||
|
||||
func TestAclList_InvitePipeline(t *testing.T) {
|
||||
fx := newFixture(t)
|
||||
fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Writer))
|
||||
}
|
||||
|
||||
func TestAclList_InviteRevoke(t *testing.T) {
|
||||
fx := newFixture(t)
|
||||
var (
|
||||
ownerState = fx.ownerAcl.aclState
|
||||
accountState = fx.accountAcl.aclState
|
||||
)
|
||||
// building invite
|
||||
inv, err := fx.ownerAcl.RecordBuilder().BuildInvite()
|
||||
require.NoError(t, err)
|
||||
inviteRec := wrapRecord(inv.InviteRec)
|
||||
fx.addRec(t, inviteRec)
|
||||
|
||||
// building invite revoke
|
||||
inviteRevoke, err := fx.ownerAcl.RecordBuilder().BuildInviteRevoke(ownerState.lastRecordId)
|
||||
require.NoError(t, err)
|
||||
inviteRevokeRec := wrapRecord(inviteRevoke)
|
||||
fx.addRec(t, inviteRevokeRec)
|
||||
|
||||
// checking acl state
|
||||
require.True(t, ownerState.Permissions(ownerState.pubKey).IsOwner())
|
||||
require.True(t, ownerState.Permissions(accountState.pubKey).NoPermissions())
|
||||
require.Empty(t, ownerState.inviteKeys)
|
||||
require.Empty(t, accountState.inviteKeys)
|
||||
}
|
||||
|
||||
func TestAclList_RequestDecline(t *testing.T) {
|
||||
fx := newFixture(t)
|
||||
var (
|
||||
ownerAcl = fx.ownerAcl
|
||||
ownerState = fx.ownerAcl.aclState
|
||||
accountAcl = fx.accountAcl
|
||||
accountState = fx.accountAcl.aclState
|
||||
)
|
||||
// building invite
|
||||
inv, err := ownerAcl.RecordBuilder().BuildInvite()
|
||||
require.NoError(t, err)
|
||||
inviteRec := wrapRecord(inv.InviteRec)
|
||||
fx.addRec(t, inviteRec)
|
||||
|
||||
// building request join
|
||||
requestJoin, err := accountAcl.RecordBuilder().BuildRequestJoin(RequestJoinPayload{
|
||||
InviteRecordId: inviteRec.Id,
|
||||
InviteKey: inv.InviteKey,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
requestJoinRec := wrapRecord(requestJoin)
|
||||
fx.addRec(t, requestJoinRec)
|
||||
|
||||
// building request decline
|
||||
requestDecline, err := ownerAcl.RecordBuilder().BuildRequestDecline(ownerState.lastRecordId)
|
||||
require.NoError(t, err)
|
||||
requestDeclineRec := wrapRecord(requestDecline)
|
||||
fx.addRec(t, requestDeclineRec)
|
||||
|
||||
// checking acl state
|
||||
require.True(t, ownerState.Permissions(ownerState.pubKey).IsOwner())
|
||||
require.True(t, ownerState.Permissions(accountState.pubKey).NoPermissions())
|
||||
require.Empty(t, ownerState.pendingRequests)
|
||||
require.Empty(t, accountState.pendingRequests)
|
||||
}
|
||||
|
||||
func TestAclList_Remove(t *testing.T) {
|
||||
fx := newFixture(t)
|
||||
var (
|
||||
ownerState = fx.ownerAcl.aclState
|
||||
accountState = fx.accountAcl.aclState
|
||||
)
|
||||
fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Writer))
|
||||
|
||||
newReadKey := crypto.NewAES()
|
||||
remove, err := fx.ownerAcl.RecordBuilder().BuildAccountRemove(AccountRemovePayload{
|
||||
Identities: []crypto.PubKey{fx.accountKeys.SignKey.GetPublic()},
|
||||
ReadKey: newReadKey,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
removeRec := wrapRecord(remove)
|
||||
fx.addRec(t, removeRec)
|
||||
|
||||
// checking acl state
|
||||
require.True(t, ownerState.Permissions(ownerState.pubKey).IsOwner())
|
||||
require.True(t, ownerState.Permissions(accountState.pubKey).NoPermissions())
|
||||
require.True(t, ownerState.userReadKeys[removeRec.Id].Equals(newReadKey))
|
||||
require.NotNil(t, ownerState.userReadKeys[fx.ownerAcl.Id()])
|
||||
require.Equal(t, 0, len(ownerState.pendingRequests))
|
||||
require.Equal(t, 0, len(accountState.pendingRequests))
|
||||
require.True(t, accountState.Permissions(ownerState.pubKey).IsOwner())
|
||||
require.True(t, accountState.Permissions(accountState.pubKey).NoPermissions())
|
||||
require.Nil(t, accountState.userReadKeys[removeRec.Id])
|
||||
require.NotNil(t, accountState.userReadKeys[fx.ownerAcl.Id()])
|
||||
}
|
||||
|
||||
func TestAclList_ReadKeyChange(t *testing.T) {
|
||||
fx := newFixture(t)
|
||||
var (
|
||||
ownerState = fx.ownerAcl.aclState
|
||||
accountState = fx.accountAcl.aclState
|
||||
)
|
||||
fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Admin))
|
||||
|
||||
newReadKey := crypto.NewAES()
|
||||
readKeyChange, err := fx.ownerAcl.RecordBuilder().BuildReadKeyChange(newReadKey)
|
||||
require.NoError(t, err)
|
||||
readKeyRec := wrapRecord(readKeyChange)
|
||||
fx.addRec(t, readKeyRec)
|
||||
|
||||
// checking acl state
|
||||
require.True(t, ownerState.Permissions(ownerState.pubKey).IsOwner())
|
||||
require.True(t, ownerState.Permissions(accountState.pubKey).CanManageAccounts())
|
||||
require.True(t, ownerState.userReadKeys[readKeyRec.Id].Equals(newReadKey))
|
||||
require.True(t, accountState.userReadKeys[readKeyRec.Id].Equals(newReadKey))
|
||||
require.NotNil(t, ownerState.userReadKeys[fx.ownerAcl.Id()])
|
||||
require.NotNil(t, accountState.userReadKeys[fx.ownerAcl.Id()])
|
||||
readKey, err := ownerState.CurrentReadKey()
|
||||
require.NoError(t, err)
|
||||
require.True(t, newReadKey.Equals(readKey))
|
||||
require.Equal(t, 0, len(ownerState.pendingRequests))
|
||||
require.Equal(t, 0, len(accountState.pendingRequests))
|
||||
}
|
||||
|
||||
func TestAclList_PermissionChange(t *testing.T) {
|
||||
fx := newFixture(t)
|
||||
var (
|
||||
ownerState = fx.ownerAcl.aclState
|
||||
accountState = fx.accountAcl.aclState
|
||||
)
|
||||
fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Admin))
|
||||
|
||||
permissionChange, err := fx.ownerAcl.RecordBuilder().BuildPermissionChange(PermissionChangePayload{
|
||||
Identity: fx.accountKeys.SignKey.GetPublic(),
|
||||
Permissions: AclPermissions(aclrecordproto.AclUserPermissions_Writer),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
permissionChangeRec := wrapRecord(permissionChange)
|
||||
fx.addRec(t, permissionChangeRec)
|
||||
|
||||
// checking acl state
|
||||
require.True(t, ownerState.Permissions(ownerState.pubKey).IsOwner())
|
||||
require.True(t, ownerState.Permissions(accountState.pubKey) == AclPermissions(aclrecordproto.AclUserPermissions_Writer))
|
||||
require.True(t, accountState.Permissions(ownerState.pubKey).IsOwner())
|
||||
require.True(t, accountState.Permissions(accountState.pubKey) == AclPermissions(aclrecordproto.AclUserPermissions_Writer))
|
||||
require.NotNil(t, ownerState.userReadKeys[fx.ownerAcl.Id()])
|
||||
require.NotNil(t, accountState.userReadKeys[fx.ownerAcl.Id()])
|
||||
require.Equal(t, 0, len(ownerState.pendingRequests))
|
||||
require.Equal(t, 0, len(accountState.pendingRequests))
|
||||
}
|
||||
|
||||
func TestAclList_RequestRemove(t *testing.T) {
|
||||
fx := newFixture(t)
|
||||
var (
|
||||
ownerState = fx.ownerAcl.aclState
|
||||
accountState = fx.accountAcl.aclState
|
||||
)
|
||||
fx.inviteAccount(t, AclPermissions(aclrecordproto.AclUserPermissions_Writer))
|
||||
|
||||
removeRequest, err := fx.accountAcl.RecordBuilder().BuildRequestRemove()
|
||||
require.NoError(t, err)
|
||||
removeRequestRec := wrapRecord(removeRequest)
|
||||
fx.addRec(t, removeRequestRec)
|
||||
|
||||
recs := fx.accountAcl.AclState().RemoveRecords()
|
||||
require.Len(t, recs, 1)
|
||||
require.True(t, accountState.pubKey.Equals(recs[0].RequestIdentity))
|
||||
|
||||
newReadKey := crypto.NewAES()
|
||||
remove, err := fx.ownerAcl.RecordBuilder().BuildAccountRemove(AccountRemovePayload{
|
||||
Identities: []crypto.PubKey{recs[0].RequestIdentity},
|
||||
ReadKey: newReadKey,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
removeRec := wrapRecord(remove)
|
||||
fx.addRec(t, removeRec)
|
||||
|
||||
// checking acl state
|
||||
require.True(t, ownerState.Permissions(ownerState.pubKey).IsOwner())
|
||||
require.True(t, ownerState.Permissions(accountState.pubKey).NoPermissions())
|
||||
require.True(t, ownerState.userReadKeys[removeRec.Id].Equals(newReadKey))
|
||||
require.NotNil(t, ownerState.userReadKeys[fx.ownerAcl.Id()])
|
||||
require.Equal(t, 0, len(ownerState.pendingRequests))
|
||||
require.Equal(t, 0, len(accountState.pendingRequests))
|
||||
require.True(t, accountState.Permissions(ownerState.pubKey).IsOwner())
|
||||
require.True(t, accountState.Permissions(accountState.pubKey).NoPermissions())
|
||||
require.Nil(t, accountState.userReadKeys[removeRec.Id])
|
||||
require.NotNil(t, accountState.userReadKeys[fx.ownerAcl.Id()])
|
||||
assert.NotEqual(t, records[2].CurrentReadKeyHash, aclList.AclState().CurrentReadKeyHash())
|
||||
|
||||
perm, err := aclList.AclState().PermissionsAtRecord(records[2].Id, idB)
|
||||
assert.NoError(t, err, "should have no error with permissions of B in the record 2")
|
||||
assert.Equal(t, UserPermissionPair{
|
||||
Identity: idB,
|
||||
Permission: aclrecordproto.AclUserPermissions_Writer,
|
||||
}, perm)
|
||||
|
||||
_, err = aclList.AclState().PermissionsAtRecord(records[3].Id, idB)
|
||||
assert.Error(t, err, "B should have no permissions at record 3, because user should be removed")
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"github.com/anyproto/any-sync/commonspace/object/accountdata"
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/liststorage"
|
||||
"github.com/anyproto/any-sync/consensus/consensusproto"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
)
|
||||
|
||||
func NewTestDerivedAcl(spaceId string, keys *accountdata.AccountKeys) (AclList, error) {
|
||||
builder := NewAclRecordBuilder("", crypto.NewKeyStorage(), keys, NoOpAcceptorVerifier{})
|
||||
masterKey, _, err := crypto.GenerateRandomEd25519KeyPair()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root, err := builder.BuildRoot(RootContent{
|
||||
PrivKey: keys.SignKey,
|
||||
SpaceId: spaceId,
|
||||
MasterKey: masterKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
st, err := liststorage.NewInMemoryAclListStorage(root.Id, []*consensusproto.RawRecordWithId{
|
||||
root,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return BuildAclListWithIdentity(keys, st, NoOpAcceptorVerifier{})
|
||||
}
|
||||
|
||||
func NewTestAclWithRoot(keys *accountdata.AccountKeys, root *consensusproto.RawRecordWithId) (AclList, error) {
|
||||
st, err := liststorage.NewInMemoryAclListStorage(root.Id, []*consensusproto.RawRecordWithId{
|
||||
root,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return BuildAclListWithIdentity(keys, st, NoOpAcceptorVerifier{})
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/anyproto/any-sync/commonspace/object/acl/list (interfaces: AclList)
|
||||
// Source: github.com/anytypeio/any-sync/commonspace/object/acl/list (interfaces: AclList)
|
||||
|
||||
// Package mock_list is a generated GoMock package.
|
||||
package mock_list
|
||||
@@ -7,10 +7,9 @@ package mock_list
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
list "github.com/anyproto/any-sync/commonspace/object/acl/list"
|
||||
consensusproto "github.com/anyproto/any-sync/consensus/consensusproto"
|
||||
crypto "github.com/anyproto/any-sync/util/crypto"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
aclrecordproto "github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
list "github.com/anytypeio/any-sync/commonspace/object/acl/list"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockAclList is a mock of AclList interface.
|
||||
@@ -51,11 +50,12 @@ func (mr *MockAclListMockRecorder) AclState() *gomock.Call {
|
||||
}
|
||||
|
||||
// AddRawRecord mocks base method.
|
||||
func (m *MockAclList) AddRawRecord(arg0 *consensusproto.RawRecordWithId) error {
|
||||
func (m *MockAclList) AddRawRecord(arg0 *aclrecordproto.RawAclRecordWithId) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddRawRecord", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// AddRawRecord indicates an expected call of AddRawRecord.
|
||||
@@ -93,21 +93,6 @@ func (mr *MockAclListMockRecorder) Get(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockAclList)(nil).Get), arg0)
|
||||
}
|
||||
|
||||
// GetIndex mocks base method.
|
||||
func (m *MockAclList) GetIndex(arg0 int) (*list.AclRecord, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetIndex", arg0)
|
||||
ret0, _ := ret[0].(*list.AclRecord)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetIndex indicates an expected call of GetIndex.
|
||||
func (mr *MockAclListMockRecorder) GetIndex(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIndex", reflect.TypeOf((*MockAclList)(nil).GetIndex), arg0)
|
||||
}
|
||||
|
||||
// Head mocks base method.
|
||||
func (m *MockAclList) Head() *list.AclRecord {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -175,20 +160,6 @@ func (mr *MockAclListMockRecorder) IterateFrom(arg0, arg1 interface{}) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateFrom", reflect.TypeOf((*MockAclList)(nil).IterateFrom), arg0, arg1)
|
||||
}
|
||||
|
||||
// KeyStorage mocks base method.
|
||||
func (m *MockAclList) KeyStorage() crypto.KeyStorage {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "KeyStorage")
|
||||
ret0, _ := ret[0].(crypto.KeyStorage)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// KeyStorage indicates an expected call of KeyStorage.
|
||||
func (mr *MockAclListMockRecorder) KeyStorage() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyStorage", reflect.TypeOf((*MockAclList)(nil).KeyStorage))
|
||||
}
|
||||
|
||||
// Lock mocks base method.
|
||||
func (m *MockAclList) Lock() {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -225,20 +196,6 @@ func (mr *MockAclListMockRecorder) RUnlock() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RUnlock", reflect.TypeOf((*MockAclList)(nil).RUnlock))
|
||||
}
|
||||
|
||||
// RecordBuilder mocks base method.
|
||||
func (m *MockAclList) RecordBuilder() list.AclRecordBuilder {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RecordBuilder")
|
||||
ret0, _ := ret[0].(list.AclRecordBuilder)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RecordBuilder indicates an expected call of RecordBuilder.
|
||||
func (mr *MockAclListMockRecorder) RecordBuilder() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordBuilder", reflect.TypeOf((*MockAclList)(nil).RecordBuilder))
|
||||
}
|
||||
|
||||
// Records mocks base method.
|
||||
func (m *MockAclList) Records() []*list.AclRecord {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -254,10 +211,10 @@ func (mr *MockAclListMockRecorder) Records() *gomock.Call {
|
||||
}
|
||||
|
||||
// Root mocks base method.
|
||||
func (m *MockAclList) Root() *consensusproto.RawRecordWithId {
|
||||
func (m *MockAclList) Root() *aclrecordproto.RawAclRecordWithId {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Root")
|
||||
ret0, _ := ret[0].(*consensusproto.RawRecordWithId)
|
||||
ret0, _ := ret[0].(*aclrecordproto.RawAclRecordWithId)
|
||||
return ret0
|
||||
}
|
||||
|
||||
@@ -278,17 +235,3 @@ func (mr *MockAclListMockRecorder) Unlock() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockAclList)(nil).Unlock))
|
||||
}
|
||||
|
||||
// ValidateRawRecord mocks base method.
|
||||
func (m *MockAclList) ValidateRawRecord(arg0 *consensusproto.RawRecord) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ValidateRawRecord", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ValidateRawRecord indicates an expected call of ValidateRawRecord.
|
||||
func (mr *MockAclListMockRecorder) ValidateRawRecord(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateRawRecord", reflect.TypeOf((*MockAclList)(nil).ValidateRawRecord), arg0)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
)
|
||||
|
||||
type AclRecord struct {
|
||||
Id string
|
||||
PrevId string
|
||||
Timestamp int64
|
||||
Data []byte
|
||||
Identity crypto.PubKey
|
||||
Model interface{}
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
type RequestRecord struct {
|
||||
RequestIdentity crypto.PubKey
|
||||
RequestMetadata []byte
|
||||
Type RequestType
|
||||
}
|
||||
|
||||
type AclUserState struct {
|
||||
PubKey crypto.PubKey
|
||||
Permissions AclPermissions
|
||||
RequestMetadata []byte
|
||||
}
|
||||
|
||||
type RequestType int
|
||||
|
||||
const (
|
||||
RequestTypeRemove RequestType = iota
|
||||
RequestTypeJoin
|
||||
)
|
||||
|
||||
type AclPermissions aclrecordproto.AclUserPermissions
|
||||
|
||||
func (p AclPermissions) NoPermissions() bool {
|
||||
return aclrecordproto.AclUserPermissions(p) == aclrecordproto.AclUserPermissions_None
|
||||
}
|
||||
|
||||
func (p AclPermissions) IsOwner() bool {
|
||||
return aclrecordproto.AclUserPermissions(p) == aclrecordproto.AclUserPermissions_Owner
|
||||
}
|
||||
|
||||
func (p AclPermissions) CanWrite() bool {
|
||||
switch aclrecordproto.AclUserPermissions(p) {
|
||||
case aclrecordproto.AclUserPermissions_Admin:
|
||||
return true
|
||||
case aclrecordproto.AclUserPermissions_Writer:
|
||||
return true
|
||||
case aclrecordproto.AclUserPermissions_Owner:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (p AclPermissions) CanManageAccounts() bool {
|
||||
switch aclrecordproto.AclUserPermissions(p) {
|
||||
case aclrecordproto.AclUserPermissions_Admin:
|
||||
return true
|
||||
case aclrecordproto.AclUserPermissions_Owner:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package list
|
||||
|
||||
type AclRecord struct {
|
||||
Id string
|
||||
PrevId string
|
||||
CurrentReadKeyHash uint64
|
||||
Timestamp int64
|
||||
Data []byte
|
||||
Identity []byte
|
||||
Model interface{}
|
||||
Signature []byte
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
)
|
||||
|
||||
type ContentValidator interface {
|
||||
ValidateAclRecordContents(ch *AclRecord) (err error)
|
||||
ValidatePermissionChange(ch *aclrecordproto.AclAccountPermissionChange, authorIdentity crypto.PubKey) (err error)
|
||||
ValidateInvite(ch *aclrecordproto.AclAccountInvite, authorIdentity crypto.PubKey) (err error)
|
||||
ValidateInviteRevoke(ch *aclrecordproto.AclAccountInviteRevoke, authorIdentity crypto.PubKey) (err error)
|
||||
ValidateRequestJoin(ch *aclrecordproto.AclAccountRequestJoin, authorIdentity crypto.PubKey) (err error)
|
||||
ValidateRequestAccept(ch *aclrecordproto.AclAccountRequestAccept, authorIdentity crypto.PubKey) (err error)
|
||||
ValidateRequestDecline(ch *aclrecordproto.AclAccountRequestDecline, authorIdentity crypto.PubKey) (err error)
|
||||
ValidateAccountRemove(ch *aclrecordproto.AclAccountRemove, authorIdentity crypto.PubKey) (err error)
|
||||
ValidateRequestRemove(ch *aclrecordproto.AclAccountRequestRemove, authorIdentity crypto.PubKey) (err error)
|
||||
ValidateReadKeyChange(ch *aclrecordproto.AclReadKeyChange, authorIdentity crypto.PubKey) (err error)
|
||||
}
|
||||
|
||||
type contentValidator struct {
|
||||
keyStore crypto.KeyStorage
|
||||
aclState *AclState
|
||||
}
|
||||
|
||||
func (c *contentValidator) ValidateAclRecordContents(ch *AclRecord) (err error) {
|
||||
if ch.PrevId != c.aclState.lastRecordId {
|
||||
return ErrIncorrectRecordSequence
|
||||
}
|
||||
aclData := ch.Model.(*aclrecordproto.AclData)
|
||||
for _, content := range aclData.AclContent {
|
||||
err = c.validateAclRecordContent(content, ch.Identity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *contentValidator) validateAclRecordContent(ch *aclrecordproto.AclContentValue, authorIdentity crypto.PubKey) (err error) {
|
||||
switch {
|
||||
case ch.GetPermissionChange() != nil:
|
||||
return c.ValidatePermissionChange(ch.GetPermissionChange(), authorIdentity)
|
||||
case ch.GetInvite() != nil:
|
||||
return c.ValidateInvite(ch.GetInvite(), authorIdentity)
|
||||
case ch.GetInviteRevoke() != nil:
|
||||
return c.ValidateInviteRevoke(ch.GetInviteRevoke(), authorIdentity)
|
||||
case ch.GetRequestJoin() != nil:
|
||||
return c.ValidateRequestJoin(ch.GetRequestJoin(), authorIdentity)
|
||||
case ch.GetRequestAccept() != nil:
|
||||
return c.ValidateRequestAccept(ch.GetRequestAccept(), authorIdentity)
|
||||
case ch.GetRequestDecline() != nil:
|
||||
return c.ValidateRequestDecline(ch.GetRequestDecline(), authorIdentity)
|
||||
case ch.GetAccountRemove() != nil:
|
||||
return c.ValidateAccountRemove(ch.GetAccountRemove(), authorIdentity)
|
||||
case ch.GetAccountRequestRemove() != nil:
|
||||
return c.ValidateRequestRemove(ch.GetAccountRequestRemove(), authorIdentity)
|
||||
case ch.GetReadKeyChange() != nil:
|
||||
return c.ValidateReadKeyChange(ch.GetReadKeyChange(), authorIdentity)
|
||||
default:
|
||||
return ErrUnexpectedContentType
|
||||
}
|
||||
}
|
||||
|
||||
func (c *contentValidator) ValidatePermissionChange(ch *aclrecordproto.AclAccountPermissionChange, authorIdentity crypto.PubKey) (err error) {
|
||||
if !c.aclState.Permissions(authorIdentity).CanManageAccounts() {
|
||||
return ErrInsufficientPermissions
|
||||
}
|
||||
chIdentity, err := c.keyStore.PubKeyFromProto(ch.Identity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, exists := c.aclState.userStates[mapKeyFromPubKey(chIdentity)]
|
||||
if !exists {
|
||||
return ErrNoSuchAccount
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *contentValidator) ValidateInvite(ch *aclrecordproto.AclAccountInvite, authorIdentity crypto.PubKey) (err error) {
|
||||
if !c.aclState.Permissions(authorIdentity).CanManageAccounts() {
|
||||
return ErrInsufficientPermissions
|
||||
}
|
||||
_, err = c.keyStore.PubKeyFromProto(ch.InviteKey)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *contentValidator) ValidateInviteRevoke(ch *aclrecordproto.AclAccountInviteRevoke, authorIdentity crypto.PubKey) (err error) {
|
||||
if !c.aclState.Permissions(authorIdentity).CanManageAccounts() {
|
||||
return ErrInsufficientPermissions
|
||||
}
|
||||
_, exists := c.aclState.inviteKeys[ch.InviteRecordId]
|
||||
if !exists {
|
||||
return ErrNoSuchInvite
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *contentValidator) ValidateRequestJoin(ch *aclrecordproto.AclAccountRequestJoin, authorIdentity crypto.PubKey) (err error) {
|
||||
inviteKey, exists := c.aclState.inviteKeys[ch.InviteRecordId]
|
||||
if !exists {
|
||||
return ErrNoSuchInvite
|
||||
}
|
||||
inviteIdentity, err := c.keyStore.PubKeyFromProto(ch.InviteIdentity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, exists := c.aclState.pendingRequests[mapKeyFromPubKey(inviteIdentity)]; exists {
|
||||
return ErrPendingRequest
|
||||
}
|
||||
if !authorIdentity.Equals(inviteIdentity) {
|
||||
return ErrIncorrectIdentity
|
||||
}
|
||||
rawInviteIdentity, err := inviteIdentity.Raw()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ok, err := inviteKey.Verify(rawInviteIdentity, ch.InviteIdentitySignature)
|
||||
if err != nil {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
if !ok {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *contentValidator) ValidateRequestAccept(ch *aclrecordproto.AclAccountRequestAccept, authorIdentity crypto.PubKey) (err error) {
|
||||
if !c.aclState.Permissions(authorIdentity).CanManageAccounts() {
|
||||
return ErrInsufficientPermissions
|
||||
}
|
||||
record, exists := c.aclState.requestRecords[ch.RequestRecordId]
|
||||
if !exists {
|
||||
return ErrNoSuchRequest
|
||||
}
|
||||
acceptIdentity, err := c.keyStore.PubKeyFromProto(ch.Identity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !acceptIdentity.Equals(record.RequestIdentity) {
|
||||
return ErrIncorrectIdentity
|
||||
}
|
||||
if ch.Permissions == aclrecordproto.AclUserPermissions_Owner {
|
||||
return ErrInsufficientPermissions
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *contentValidator) ValidateRequestDecline(ch *aclrecordproto.AclAccountRequestDecline, authorIdentity crypto.PubKey) (err error) {
|
||||
if !c.aclState.Permissions(authorIdentity).CanManageAccounts() {
|
||||
return ErrInsufficientPermissions
|
||||
}
|
||||
_, exists := c.aclState.requestRecords[ch.RequestRecordId]
|
||||
if !exists {
|
||||
return ErrNoSuchRequest
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *contentValidator) ValidateAccountRemove(ch *aclrecordproto.AclAccountRemove, authorIdentity crypto.PubKey) (err error) {
|
||||
if !c.aclState.Permissions(authorIdentity).CanManageAccounts() {
|
||||
return ErrInsufficientPermissions
|
||||
}
|
||||
seenIdentities := map[string]struct{}{}
|
||||
for _, rawIdentity := range ch.Identities {
|
||||
identity, err := c.keyStore.PubKeyFromProto(rawIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if identity.Equals(authorIdentity) {
|
||||
return ErrInsufficientPermissions
|
||||
}
|
||||
permissions := c.aclState.Permissions(identity)
|
||||
if permissions.NoPermissions() {
|
||||
return ErrNoSuchAccount
|
||||
}
|
||||
if permissions.IsOwner() {
|
||||
return ErrInsufficientPermissions
|
||||
}
|
||||
idKey := mapKeyFromPubKey(identity)
|
||||
if _, exists := seenIdentities[idKey]; exists {
|
||||
return ErrDuplicateAccounts
|
||||
}
|
||||
seenIdentities[mapKeyFromPubKey(identity)] = struct{}{}
|
||||
}
|
||||
return c.validateAccountReadKeys(ch.AccountKeys, len(c.aclState.userStates)-len(ch.Identities))
|
||||
}
|
||||
|
||||
func (c *contentValidator) ValidateRequestRemove(ch *aclrecordproto.AclAccountRequestRemove, authorIdentity crypto.PubKey) (err error) {
|
||||
if c.aclState.Permissions(authorIdentity).NoPermissions() {
|
||||
return ErrInsufficientPermissions
|
||||
}
|
||||
if _, exists := c.aclState.pendingRequests[mapKeyFromPubKey(authorIdentity)]; exists {
|
||||
return ErrPendingRequest
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *contentValidator) ValidateReadKeyChange(ch *aclrecordproto.AclReadKeyChange, authorIdentity crypto.PubKey) (err error) {
|
||||
return c.validateAccountReadKeys(ch.AccountKeys, len(c.aclState.userStates))
|
||||
}
|
||||
|
||||
func (c *contentValidator) validateAccountReadKeys(accountKeys []*aclrecordproto.AclEncryptedReadKey, usersNum int) (err error) {
|
||||
if len(accountKeys) != usersNum {
|
||||
return ErrIncorrectNumberOfAccounts
|
||||
}
|
||||
for _, encKeys := range accountKeys {
|
||||
identity, err := c.keyStore.PubKeyFromProto(encKeys.Identity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, exists := c.aclState.userStates[mapKeyFromPubKey(identity)]
|
||||
if !exists {
|
||||
return ErrNoSuchAccount
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -3,26 +3,24 @@ package liststorage
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/anyproto/any-sync/consensus/consensusproto"
|
||||
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type inMemoryAclListStorage struct {
|
||||
id string
|
||||
root *consensusproto.RawRecordWithId
|
||||
root *aclrecordproto.RawAclRecordWithId
|
||||
head string
|
||||
records map[string]*consensusproto.RawRecordWithId
|
||||
records map[string]*aclrecordproto.RawAclRecordWithId
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func NewInMemoryAclListStorage(
|
||||
id string,
|
||||
records []*consensusproto.RawRecordWithId) (ListStorage, error) {
|
||||
records []*aclrecordproto.RawAclRecordWithId) (ListStorage, error) {
|
||||
|
||||
allRecords := make(map[string]*consensusproto.RawRecordWithId)
|
||||
allRecords := make(map[string]*aclrecordproto.RawAclRecordWithId)
|
||||
for _, ch := range records {
|
||||
allRecords[ch.Id] = ch
|
||||
}
|
||||
@@ -43,7 +41,7 @@ func (t *inMemoryAclListStorage) Id() string {
|
||||
return t.id
|
||||
}
|
||||
|
||||
func (t *inMemoryAclListStorage) Root() (*consensusproto.RawRecordWithId, error) {
|
||||
func (t *inMemoryAclListStorage) Root() (*aclrecordproto.RawAclRecordWithId, error) {
|
||||
t.RLock()
|
||||
defer t.RUnlock()
|
||||
return t.root, nil
|
||||
@@ -62,7 +60,7 @@ func (t *inMemoryAclListStorage) SetHead(head string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *inMemoryAclListStorage) AddRawRecord(ctx context.Context, record *consensusproto.RawRecordWithId) error {
|
||||
func (t *inMemoryAclListStorage) AddRawRecord(ctx context.Context, record *aclrecordproto.RawAclRecordWithId) error {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
// TODO: better to do deep copy
|
||||
@@ -70,7 +68,7 @@ func (t *inMemoryAclListStorage) AddRawRecord(ctx context.Context, record *conse
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *inMemoryAclListStorage) GetRawRecord(ctx context.Context, recordId string) (*consensusproto.RawRecordWithId, error) {
|
||||
func (t *inMemoryAclListStorage) GetRawRecord(ctx context.Context, recordId string) (*aclrecordproto.RawAclRecordWithId, error) {
|
||||
t.RLock()
|
||||
defer t.RUnlock()
|
||||
if res, exists := t.records[recordId]; exists {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
//go:generate mockgen -destination mock_liststorage/mock_liststorage.go github.com/anyproto/any-sync/commonspace/object/acl/liststorage ListStorage
|
||||
//go:generate mockgen -destination mock_liststorage/mock_liststorage.go github.com/anytypeio/any-sync/commonspace/object/acl/liststorage ListStorage
|
||||
package liststorage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/anyproto/any-sync/consensus/consensusproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -15,15 +14,15 @@ var (
|
||||
)
|
||||
|
||||
type Exporter interface {
|
||||
ListStorage(root *consensusproto.RawRecordWithId) (ListStorage, error)
|
||||
ListStorage(root *aclrecordproto.RawAclRecordWithId) (ListStorage, error)
|
||||
}
|
||||
|
||||
type ListStorage interface {
|
||||
Id() string
|
||||
Root() (*consensusproto.RawRecordWithId, error)
|
||||
Root() (*aclrecordproto.RawAclRecordWithId, error)
|
||||
Head() (string, error)
|
||||
SetHead(headId string) error
|
||||
|
||||
GetRawRecord(ctx context.Context, id string) (*consensusproto.RawRecordWithId, error)
|
||||
AddRawRecord(ctx context.Context, rec *consensusproto.RawRecordWithId) error
|
||||
GetRawRecord(ctx context.Context, id string) (*aclrecordproto.RawAclRecordWithId, error)
|
||||
AddRawRecord(ctx context.Context, rec *aclrecordproto.RawAclRecordWithId) error
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/anyproto/any-sync/commonspace/object/acl/liststorage (interfaces: ListStorage)
|
||||
// Source: github.com/anytypeio/any-sync/commonspace/object/acl/liststorage (interfaces: ListStorage)
|
||||
|
||||
// Package mock_liststorage is a generated GoMock package.
|
||||
package mock_liststorage
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
consensusproto "github.com/anyproto/any-sync/consensus/consensusproto"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
aclrecordproto "github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockListStorage is a mock of ListStorage interface.
|
||||
@@ -36,7 +36,7 @@ func (m *MockListStorage) EXPECT() *MockListStorageMockRecorder {
|
||||
}
|
||||
|
||||
// AddRawRecord mocks base method.
|
||||
func (m *MockListStorage) AddRawRecord(arg0 context.Context, arg1 *consensusproto.RawRecordWithId) error {
|
||||
func (m *MockListStorage) AddRawRecord(arg0 context.Context, arg1 *aclrecordproto.RawAclRecordWithId) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddRawRecord", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
@@ -50,10 +50,10 @@ func (mr *MockListStorageMockRecorder) AddRawRecord(arg0, arg1 interface{}) *gom
|
||||
}
|
||||
|
||||
// GetRawRecord mocks base method.
|
||||
func (m *MockListStorage) GetRawRecord(arg0 context.Context, arg1 string) (*consensusproto.RawRecordWithId, error) {
|
||||
func (m *MockListStorage) GetRawRecord(arg0 context.Context, arg1 string) (*aclrecordproto.RawAclRecordWithId, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetRawRecord", arg0, arg1)
|
||||
ret0, _ := ret[0].(*consensusproto.RawRecordWithId)
|
||||
ret0, _ := ret[0].(*aclrecordproto.RawAclRecordWithId)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@@ -94,10 +94,10 @@ func (mr *MockListStorageMockRecorder) Id() *gomock.Call {
|
||||
}
|
||||
|
||||
// Root mocks base method.
|
||||
func (m *MockListStorage) Root() (*consensusproto.RawRecordWithId, error) {
|
||||
func (m *MockListStorage) Root() (*aclrecordproto.RawAclRecordWithId, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Root")
|
||||
ret0, _ := ret[0].(*consensusproto.RawRecordWithId)
|
||||
ret0, _ := ret[0].(*aclrecordproto.RawAclRecordWithId)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
@@ -1,44 +1,21 @@
|
||||
package syncacl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/anyproto/any-sync/accountservice"
|
||||
"github.com/anyproto/any-sync/app"
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anyproto/any-sync/commonspace/spacestorage"
|
||||
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anytypeio/any-sync/commonspace/objectsync"
|
||||
"github.com/anytypeio/any-sync/commonspace/objectsync/synchandler"
|
||||
)
|
||||
|
||||
const CName = "common.acl.syncacl"
|
||||
|
||||
func New() *SyncAcl {
|
||||
return &SyncAcl{}
|
||||
}
|
||||
|
||||
type SyncAcl struct {
|
||||
list.AclList
|
||||
synchandler.SyncHandler
|
||||
messagePool objectsync.MessagePool
|
||||
}
|
||||
|
||||
func (s *SyncAcl) HandleRequest(ctx context.Context, senderId string, request *spacesyncproto.ObjectSyncMessage) (response *spacesyncproto.ObjectSyncMessage, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *SyncAcl) HandleMessage(ctx context.Context, senderId string, request *spacesyncproto.ObjectSyncMessage) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SyncAcl) Init(a *app.App) (err error) {
|
||||
storage := a.MustComponent(spacestorage.CName).(spacestorage.SpaceStorage)
|
||||
aclStorage, err := storage.AclStorage()
|
||||
if err != nil {
|
||||
return err
|
||||
func NewSyncAcl(aclList list.AclList, messagePool objectsync.MessagePool) *SyncAcl {
|
||||
return &SyncAcl{
|
||||
AclList: aclList,
|
||||
SyncHandler: nil,
|
||||
messagePool: messagePool,
|
||||
}
|
||||
acc := a.MustComponent(accountservice.CName).(accountservice.Service)
|
||||
s.AclList, err = list.BuildAclListWithIdentity(acc.Account(), aclStorage, list.NoOpAcceptorVerifier{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SyncAcl) Name() (name string) {
|
||||
return CName
|
||||
}
|
||||
@@ -2,9 +2,10 @@ package syncacl
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
"fmt"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
)
|
||||
|
||||
type syncAclHandler struct {
|
||||
@@ -12,5 +13,19 @@ type syncAclHandler struct {
|
||||
}
|
||||
|
||||
func (s *syncAclHandler) HandleMessage(ctx context.Context, senderId string, req *spacesyncproto.ObjectSyncMessage) (err error) {
|
||||
return nil
|
||||
aclMsg := &aclrecordproto.AclSyncMessage{}
|
||||
if err = aclMsg.Unmarshal(req.Payload); err != nil {
|
||||
return
|
||||
}
|
||||
content := aclMsg.GetContent()
|
||||
switch {
|
||||
case content.GetAddRecords() != nil:
|
||||
return s.handleAddRecords(ctx, senderId, content.GetAddRecords())
|
||||
default:
|
||||
return fmt.Errorf("unexpected aclSync message: %T", content.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *syncAclHandler) handleAddRecords(ctx context.Context, senderId string, addRecord *aclrecordproto.AclAddRecords) (err error) {
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package acllistbuilder
|
||||
|
||||
import (
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anytypeio/any-sync/util/keys"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/encryptionkey"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/signingkey"
|
||||
"github.com/anytypeio/any-sync/util/keys/symmetric"
|
||||
"hash/fnv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SymKey struct {
|
||||
Hash uint64
|
||||
Key *symmetric.Key
|
||||
}
|
||||
|
||||
type YAMLKeychain struct {
|
||||
SigningKeysByYAMLName map[string]signingkey.PrivKey
|
||||
SigningKeysByRealIdentity map[string]signingkey.PrivKey
|
||||
EncryptionKeysByYAMLName map[string]encryptionkey.PrivKey
|
||||
ReadKeysByYAMLName map[string]*SymKey
|
||||
ReadKeysByHash map[uint64]*SymKey
|
||||
GeneratedIdentities map[string]string
|
||||
}
|
||||
|
||||
func NewKeychain() *YAMLKeychain {
|
||||
return &YAMLKeychain{
|
||||
SigningKeysByYAMLName: map[string]signingkey.PrivKey{},
|
||||
SigningKeysByRealIdentity: map[string]signingkey.PrivKey{},
|
||||
EncryptionKeysByYAMLName: map[string]encryptionkey.PrivKey{},
|
||||
GeneratedIdentities: map[string]string{},
|
||||
ReadKeysByYAMLName: map[string]*SymKey{},
|
||||
ReadKeysByHash: map[uint64]*SymKey{},
|
||||
}
|
||||
}
|
||||
|
||||
func (k *YAMLKeychain) ParseKeys(keys *Keys) {
|
||||
for _, encKey := range keys.Enc {
|
||||
k.AddEncryptionKey(encKey)
|
||||
}
|
||||
|
||||
for _, signKey := range keys.Sign {
|
||||
k.AddSigningKey(signKey)
|
||||
}
|
||||
|
||||
for _, readKey := range keys.Read {
|
||||
k.AddReadKey(readKey)
|
||||
}
|
||||
}
|
||||
|
||||
func (k *YAMLKeychain) AddEncryptionKey(key *Key) {
|
||||
if _, exists := k.EncryptionKeysByYAMLName[key.Name]; exists {
|
||||
return
|
||||
}
|
||||
var (
|
||||
newPrivKey encryptionkey.PrivKey
|
||||
err error
|
||||
)
|
||||
if key.Value == "generated" {
|
||||
newPrivKey, _, err = encryptionkey.GenerateRandomRSAKeyPair(2048)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
newPrivKey, err = keys.DecodeKeyFromString(key.Value, encryptionkey.NewEncryptionRsaPrivKeyFromBytes, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
k.EncryptionKeysByYAMLName[key.Name] = newPrivKey
|
||||
}
|
||||
|
||||
func (k *YAMLKeychain) AddSigningKey(key *Key) {
|
||||
if _, exists := k.SigningKeysByYAMLName[key.Name]; exists {
|
||||
return
|
||||
}
|
||||
var (
|
||||
newPrivKey signingkey.PrivKey
|
||||
pubKey signingkey.PubKey
|
||||
err error
|
||||
)
|
||||
if key.Value == "generated" {
|
||||
newPrivKey, pubKey, err = signingkey.GenerateRandomEd25519KeyPair()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
newPrivKey, err = keys.DecodeKeyFromString(key.Value, signingkey.NewSigningEd25519PrivKeyFromBytes, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pubKey = newPrivKey.GetPublic()
|
||||
}
|
||||
|
||||
k.SigningKeysByYAMLName[key.Name] = newPrivKey
|
||||
rawPubKey, err := pubKey.Raw()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
encoded := string(rawPubKey)
|
||||
|
||||
k.SigningKeysByRealIdentity[encoded] = newPrivKey
|
||||
k.GeneratedIdentities[key.Name] = encoded
|
||||
}
|
||||
|
||||
func (k *YAMLKeychain) AddReadKey(key *Key) {
|
||||
if _, exists := k.ReadKeysByYAMLName[key.Name]; exists {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
rkey *symmetric.Key
|
||||
err error
|
||||
)
|
||||
if key.Value == "generated" {
|
||||
rkey, err = symmetric.NewRandom()
|
||||
if err != nil {
|
||||
panic("should be able to generate symmetric key")
|
||||
}
|
||||
} else if key.Value == "derived" {
|
||||
signKey, _ := k.SigningKeysByYAMLName[key.Name].Raw()
|
||||
encKey, _ := k.EncryptionKeysByYAMLName[key.Name].Raw()
|
||||
rkey, err = aclrecordproto.AclReadKeyDerive(signKey, encKey)
|
||||
if err != nil {
|
||||
panic("should be able to derive symmetric key")
|
||||
}
|
||||
} else {
|
||||
rkey, err = symmetric.FromString(key.Value)
|
||||
if err != nil {
|
||||
panic("should be able to parse symmetric key")
|
||||
}
|
||||
}
|
||||
|
||||
hasher := fnv.New64()
|
||||
hasher.Write(rkey.Bytes())
|
||||
|
||||
k.ReadKeysByYAMLName[key.Name] = &SymKey{
|
||||
Hash: hasher.Sum64(),
|
||||
Key: rkey,
|
||||
}
|
||||
k.ReadKeysByHash[hasher.Sum64()] = &SymKey{
|
||||
Hash: hasher.Sum64(),
|
||||
Key: rkey,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *YAMLKeychain) AddKey(key *Key) {
|
||||
parts := strings.Split(key.Name, ".")
|
||||
if len(parts) != 3 {
|
||||
panic("cannot parse a key")
|
||||
}
|
||||
|
||||
switch parts[1] {
|
||||
case "Signature":
|
||||
k.AddSigningKey(key)
|
||||
case "Enc":
|
||||
k.AddEncryptionKey(key)
|
||||
case "Read":
|
||||
k.AddReadKey(key)
|
||||
default:
|
||||
panic("incorrect format")
|
||||
}
|
||||
}
|
||||
|
||||
func (k *YAMLKeychain) GetKey(key string) interface{} {
|
||||
parts := strings.Split(key, ".")
|
||||
if len(parts) != 3 {
|
||||
panic("cannot parse a key")
|
||||
}
|
||||
name := parts[2]
|
||||
|
||||
switch parts[1] {
|
||||
case "Sign":
|
||||
if key, exists := k.SigningKeysByYAMLName[name]; exists {
|
||||
return key
|
||||
}
|
||||
case "Enc":
|
||||
if key, exists := k.EncryptionKeysByYAMLName[name]; exists {
|
||||
return key
|
||||
}
|
||||
case "Read":
|
||||
if key, exists := k.ReadKeysByYAMLName[name]; exists {
|
||||
return key
|
||||
}
|
||||
default:
|
||||
panic("incorrect format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *YAMLKeychain) GetIdentity(name string) string {
|
||||
return k.GeneratedIdentities[name]
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
package acllistbuilder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/liststorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/testutils/yamltests"
|
||||
"github.com/anytypeio/any-sync/util/cidutil"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/encryptionkey"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/signingkey"
|
||||
"github.com/anytypeio/any-sync/util/keys/symmetric"
|
||||
"gopkg.in/yaml.v3"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
type AclListStorageBuilder struct {
|
||||
liststorage.ListStorage
|
||||
keychain *YAMLKeychain
|
||||
}
|
||||
|
||||
func NewAclListStorageBuilder(keychain *YAMLKeychain) *AclListStorageBuilder {
|
||||
return &AclListStorageBuilder{
|
||||
keychain: keychain,
|
||||
}
|
||||
}
|
||||
|
||||
func NewListStorageWithTestName(name string) (liststorage.ListStorage, error) {
|
||||
filePath := path.Join(yamltests.Path(), name)
|
||||
return NewAclListStorageBuilderFromFile(filePath)
|
||||
}
|
||||
|
||||
func NewAclListStorageBuilderFromFile(file string) (*AclListStorageBuilder, error) {
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ymlTree := YMLList{}
|
||||
err = yaml.Unmarshal(content, &ymlTree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tb := NewAclListStorageBuilder(NewKeychain())
|
||||
tb.Parse(&ymlTree)
|
||||
|
||||
return tb, nil
|
||||
}
|
||||
|
||||
func (t *AclListStorageBuilder) createRaw(rec proto.Marshaler, identity []byte) *aclrecordproto.RawAclRecordWithId {
|
||||
protoMarshalled, err := rec.Marshal()
|
||||
if err != nil {
|
||||
panic("should be able to marshal final acl message!")
|
||||
}
|
||||
|
||||
signature, err := t.keychain.SigningKeysByRealIdentity[string(identity)].Sign(protoMarshalled)
|
||||
if err != nil {
|
||||
panic("should be able to sign final acl message!")
|
||||
}
|
||||
|
||||
rawRec := &aclrecordproto.RawAclRecord{
|
||||
Payload: protoMarshalled,
|
||||
Signature: signature,
|
||||
}
|
||||
|
||||
rawMarshalled, err := proto.Marshal(rawRec)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
id, _ := cidutil.NewCidFromBytes(rawMarshalled)
|
||||
|
||||
return &aclrecordproto.RawAclRecordWithId{
|
||||
Payload: rawMarshalled,
|
||||
Id: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AclListStorageBuilder) GetKeychain() *YAMLKeychain {
|
||||
return t.keychain
|
||||
}
|
||||
|
||||
func (t *AclListStorageBuilder) Parse(l *YMLList) {
|
||||
// Just to clarify - we are generating new identities for the ones that
|
||||
// are specified in the yml file, because our identities should be Ed25519
|
||||
// the same thing is happening for the encryption keys
|
||||
t.keychain.ParseKeys(&l.Keys)
|
||||
rawRoot := t.parseRoot(l.Root)
|
||||
var err error
|
||||
t.ListStorage, err = liststorage.NewInMemoryAclListStorage(rawRoot.Id, []*aclrecordproto.RawAclRecordWithId{rawRoot})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
prevId := rawRoot.Id
|
||||
for _, rec := range l.Records {
|
||||
newRecord := t.parseRecord(rec, prevId)
|
||||
rawRecord := t.createRaw(newRecord, newRecord.Identity)
|
||||
err = t.AddRawRecord(context.Background(), rawRecord)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
prevId = rawRecord.Id
|
||||
}
|
||||
t.SetHead(prevId)
|
||||
}
|
||||
|
||||
func (t *AclListStorageBuilder) parseRecord(rec *Record, prevId string) *aclrecordproto.AclRecord {
|
||||
k := t.keychain.GetKey(rec.ReadKey).(*SymKey)
|
||||
var aclChangeContents []*aclrecordproto.AclContentValue
|
||||
for _, ch := range rec.AclChanges {
|
||||
aclChangeContent := t.parseAclChange(ch)
|
||||
aclChangeContents = append(aclChangeContents, aclChangeContent)
|
||||
}
|
||||
data := &aclrecordproto.AclData{
|
||||
AclContent: aclChangeContents,
|
||||
}
|
||||
bytes, _ := data.Marshal()
|
||||
|
||||
return &aclrecordproto.AclRecord{
|
||||
PrevId: prevId,
|
||||
Identity: []byte(t.keychain.GetIdentity(rec.Identity)),
|
||||
Data: bytes,
|
||||
CurrentReadKeyHash: k.Hash,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AclListStorageBuilder) parseAclChange(ch *AclChange) (convCh *aclrecordproto.AclContentValue) {
|
||||
switch {
|
||||
case ch.UserAdd != nil:
|
||||
add := ch.UserAdd
|
||||
|
||||
encKey := t.keychain.GetKey(add.EncryptionKey).(encryptionkey.PrivKey)
|
||||
rawKey, _ := encKey.GetPublic().Raw()
|
||||
|
||||
convCh = &aclrecordproto.AclContentValue{
|
||||
Value: &aclrecordproto.AclContentValue_UserAdd{
|
||||
UserAdd: &aclrecordproto.AclUserAdd{
|
||||
Identity: []byte(t.keychain.GetIdentity(add.Identity)),
|
||||
EncryptionKey: rawKey,
|
||||
EncryptedReadKeys: t.encryptReadKeysWithPubKey(add.EncryptedReadKeys, encKey),
|
||||
Permissions: t.convertPermission(add.Permission),
|
||||
},
|
||||
},
|
||||
}
|
||||
case ch.UserJoin != nil:
|
||||
join := ch.UserJoin
|
||||
|
||||
encKey := t.keychain.GetKey(join.EncryptionKey).(encryptionkey.PrivKey)
|
||||
rawKey, _ := encKey.GetPublic().Raw()
|
||||
|
||||
idKey, _ := t.keychain.SigningKeysByYAMLName[join.Identity].GetPublic().Raw()
|
||||
signKey := t.keychain.GetKey(join.AcceptKey).(signingkey.PrivKey)
|
||||
signature, err := signKey.Sign(idKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
acceptPubKey, _ := signKey.GetPublic().Raw()
|
||||
|
||||
convCh = &aclrecordproto.AclContentValue{
|
||||
Value: &aclrecordproto.AclContentValue_UserJoin{
|
||||
UserJoin: &aclrecordproto.AclUserJoin{
|
||||
Identity: []byte(t.keychain.GetIdentity(join.Identity)),
|
||||
EncryptionKey: rawKey,
|
||||
AcceptSignature: signature,
|
||||
AcceptPubKey: acceptPubKey,
|
||||
EncryptedReadKeys: t.encryptReadKeysWithPubKey(join.EncryptedReadKeys, encKey),
|
||||
},
|
||||
},
|
||||
}
|
||||
case ch.UserInvite != nil:
|
||||
invite := ch.UserInvite
|
||||
rawAcceptKey, _ := t.keychain.GetKey(invite.AcceptKey).(signingkey.PrivKey).GetPublic().Raw()
|
||||
hash := t.keychain.GetKey(invite.EncryptionKey).(*SymKey).Hash
|
||||
encKey := t.keychain.ReadKeysByHash[hash]
|
||||
|
||||
convCh = &aclrecordproto.AclContentValue{
|
||||
Value: &aclrecordproto.AclContentValue_UserInvite{
|
||||
UserInvite: &aclrecordproto.AclUserInvite{
|
||||
AcceptPublicKey: rawAcceptKey,
|
||||
EncryptSymKeyHash: hash,
|
||||
EncryptedReadKeys: t.encryptReadKeysWithSymKey(invite.EncryptedReadKeys, encKey.Key),
|
||||
Permissions: t.convertPermission(invite.Permissions),
|
||||
},
|
||||
},
|
||||
}
|
||||
case ch.UserPermissionChange != nil:
|
||||
permissionChange := ch.UserPermissionChange
|
||||
|
||||
convCh = &aclrecordproto.AclContentValue{
|
||||
Value: &aclrecordproto.AclContentValue_UserPermissionChange{
|
||||
UserPermissionChange: &aclrecordproto.AclUserPermissionChange{
|
||||
Identity: []byte(t.keychain.GetIdentity(permissionChange.Identity)),
|
||||
Permissions: t.convertPermission(permissionChange.Permission),
|
||||
},
|
||||
},
|
||||
}
|
||||
case ch.UserRemove != nil:
|
||||
remove := ch.UserRemove
|
||||
|
||||
newReadKey := t.keychain.GetKey(remove.NewReadKey).(*SymKey)
|
||||
|
||||
var replaces []*aclrecordproto.AclReadKeyReplace
|
||||
for _, id := range remove.IdentitiesLeft {
|
||||
encKey := t.keychain.EncryptionKeysByYAMLName[id]
|
||||
rawEncKey, _ := encKey.GetPublic().Raw()
|
||||
encReadKey, err := encKey.GetPublic().Encrypt(newReadKey.Key.Bytes())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
replaces = append(replaces, &aclrecordproto.AclReadKeyReplace{
|
||||
Identity: []byte(t.keychain.GetIdentity(id)),
|
||||
EncryptionKey: rawEncKey,
|
||||
EncryptedReadKey: encReadKey,
|
||||
})
|
||||
}
|
||||
|
||||
convCh = &aclrecordproto.AclContentValue{
|
||||
Value: &aclrecordproto.AclContentValue_UserRemove{
|
||||
UserRemove: &aclrecordproto.AclUserRemove{
|
||||
Identity: []byte(t.keychain.GetIdentity(remove.RemovedIdentity)),
|
||||
ReadKeyReplaces: replaces,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if convCh == nil {
|
||||
panic("cannot have empty acl change")
|
||||
}
|
||||
|
||||
return convCh
|
||||
}
|
||||
|
||||
func (t *AclListStorageBuilder) encryptReadKeysWithPubKey(keys []string, encKey encryptionkey.PrivKey) (enc [][]byte) {
|
||||
for _, k := range keys {
|
||||
realKey := t.keychain.GetKey(k).(*SymKey).Key.Bytes()
|
||||
res, err := encKey.GetPublic().Encrypt(realKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
enc = append(enc, res)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t *AclListStorageBuilder) encryptReadKeysWithSymKey(keys []string, key *symmetric.Key) (enc [][]byte) {
|
||||
for _, k := range keys {
|
||||
realKey := t.keychain.GetKey(k).(*SymKey).Key.Bytes()
|
||||
res, err := key.Encrypt(realKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
enc = append(enc, res)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t *AclListStorageBuilder) convertPermission(perm string) aclrecordproto.AclUserPermissions {
|
||||
switch perm {
|
||||
case "admin":
|
||||
return aclrecordproto.AclUserPermissions_Admin
|
||||
case "writer":
|
||||
return aclrecordproto.AclUserPermissions_Writer
|
||||
case "reader":
|
||||
return aclrecordproto.AclUserPermissions_Reader
|
||||
default:
|
||||
panic(fmt.Sprintf("incorrect permission: %s", perm))
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AclListStorageBuilder) traverseFromHead(f func(rec *aclrecordproto.AclRecord, id string) error) (err error) {
|
||||
panic("this was removed, add if needed")
|
||||
}
|
||||
|
||||
func (t *AclListStorageBuilder) parseRoot(root *Root) (rawRoot *aclrecordproto.RawAclRecordWithId) {
|
||||
rawSignKey, _ := t.keychain.SigningKeysByYAMLName[root.Identity].GetPublic().Raw()
|
||||
rawEncKey, _ := t.keychain.EncryptionKeysByYAMLName[root.Identity].GetPublic().Raw()
|
||||
readKey := t.keychain.ReadKeysByYAMLName[root.Identity]
|
||||
aclRoot := &aclrecordproto.AclRoot{
|
||||
Identity: rawSignKey,
|
||||
EncryptionKey: rawEncKey,
|
||||
SpaceId: root.SpaceId,
|
||||
EncryptedReadKey: nil,
|
||||
DerivationScheme: "scheme",
|
||||
CurrentReadKeyHash: readKey.Hash,
|
||||
}
|
||||
return t.createRaw(aclRoot, rawSignKey)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build ((!linux && !darwin) || android || ios || nographviz || !cgo) && !amd64
|
||||
// +build !linux,!darwin android ios nographviz !cgo
|
||||
// +build !amd64
|
||||
|
||||
package acllistbuilder
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (t *AclListStorageBuilder) Graph() (string, error) {
|
||||
return "", fmt.Errorf("building graphs is not supported")
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
//go:build (linux || darwin) && !android && !ios && !nographviz && cgo && (amd64 || arm64)
|
||||
// +build linux darwin
|
||||
// +build !android
|
||||
// +build !ios
|
||||
// +build !nographviz
|
||||
// +build cgo
|
||||
// +build amd64 arm64
|
||||
|
||||
package acllistbuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
|
||||
"github.com/awalterschulze/gographviz"
|
||||
)
|
||||
|
||||
// To quickly look at visualized string you can use https://dreampuf.github.io/GraphvizOnline
|
||||
|
||||
type EdgeParameters struct {
|
||||
style string
|
||||
color string
|
||||
label string
|
||||
}
|
||||
|
||||
func (t *AclListStorageBuilder) Graph() (string, error) {
|
||||
// TODO: check updates on https://github.com/goccy/go-graphviz/issues/52 or make a fix yourself to use better library here
|
||||
graph := gographviz.NewGraph()
|
||||
graph.SetName("G")
|
||||
graph.SetDir(true)
|
||||
var nodes = make(map[string]struct{})
|
||||
|
||||
var addNodes = func(r *aclrecordproto.AclRecord, id string) error {
|
||||
style := "solid"
|
||||
|
||||
var chSymbs []string
|
||||
aclData := &aclrecordproto.AclData{}
|
||||
err := proto.Unmarshal(r.GetData(), aclData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, chc := range aclData.AclContent {
|
||||
tp := fmt.Sprintf("%T", chc.Value)
|
||||
tp = strings.Replace(tp, "AclChangeAclContentValueValueOf", "", 1)
|
||||
res := ""
|
||||
for _, ts := range tp {
|
||||
if unicode.IsUpper(ts) {
|
||||
res += string(ts)
|
||||
}
|
||||
}
|
||||
chSymbs = append(chSymbs, res)
|
||||
}
|
||||
|
||||
shortId := id
|
||||
label := fmt.Sprintf("Id: %s\nChanges: %s\n",
|
||||
shortId,
|
||||
strings.Join(chSymbs, ","),
|
||||
)
|
||||
e := graph.AddNode("G", "\""+id+"\"", map[string]string{
|
||||
"label": "\"" + label + "\"",
|
||||
"style": "\"" + style + "\"",
|
||||
})
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
nodes[id] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
var createEdge = func(firstId, secondId string, params EdgeParameters) error {
|
||||
_, exists := nodes[firstId]
|
||||
if !exists {
|
||||
return fmt.Errorf("no such node")
|
||||
}
|
||||
_, exists = nodes[secondId]
|
||||
if !exists {
|
||||
return fmt.Errorf("no previous node")
|
||||
}
|
||||
|
||||
err := graph.AddEdge("\""+firstId+"\"", "\""+secondId+"\"", true, map[string]string{
|
||||
"color": params.color,
|
||||
"style": params.style,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var addLinks = func(r *aclrecordproto.AclRecord, id string) error {
|
||||
if r.PrevId == "" {
|
||||
return nil
|
||||
}
|
||||
err := createEdge(id, r.PrevId, EdgeParameters{
|
||||
style: "dashed",
|
||||
color: "red",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err := t.traverseFromHead(addNodes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = t.traverseFromHead(addLinks)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return graph.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package acllistbuilder
|
||||
|
||||
type Key struct {
|
||||
Name string `yaml:"name"`
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
|
||||
type Keys struct {
|
||||
Derived string `yaml:"Derived"`
|
||||
Enc []*Key `yaml:"Enc"`
|
||||
Sign []*Key `yaml:"Sign"`
|
||||
Read []*Key `yaml:"Read"`
|
||||
}
|
||||
|
||||
type AclChange struct {
|
||||
UserAdd *struct {
|
||||
Identity string `yaml:"identity"`
|
||||
EncryptionKey string `yaml:"encryptionKey"`
|
||||
EncryptedReadKeys []string `yaml:"encryptedReadKeys"`
|
||||
Permission string `yaml:"permission"`
|
||||
} `yaml:"userAdd"`
|
||||
|
||||
UserJoin *struct {
|
||||
Identity string `yaml:"identity"`
|
||||
EncryptionKey string `yaml:"encryptionKey"`
|
||||
AcceptKey string `yaml:"acceptKey"`
|
||||
EncryptedReadKeys []string `yaml:"encryptedReadKeys"`
|
||||
} `yaml:"userJoin"`
|
||||
|
||||
UserInvite *struct {
|
||||
AcceptKey string `yaml:"acceptKey"`
|
||||
EncryptionKey string `yaml:"encryptionKey"`
|
||||
EncryptedReadKeys []string `yaml:"encryptedReadKeys"`
|
||||
Permissions string `yaml:"permissions"`
|
||||
} `yaml:"userInvite"`
|
||||
|
||||
UserRemove *struct {
|
||||
RemovedIdentity string `yaml:"removedIdentity"`
|
||||
NewReadKey string `yaml:"newReadKey"`
|
||||
IdentitiesLeft []string `yaml:"identitiesLeft"`
|
||||
} `yaml:"userRemove"`
|
||||
|
||||
UserPermissionChange *struct {
|
||||
Identity string `yaml:"identity"`
|
||||
Permission string `yaml:"permission"`
|
||||
}
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
Identity string `yaml:"identity"`
|
||||
AclChanges []*AclChange `yaml:"aclChanges"`
|
||||
ReadKey string `yaml:"readKey"`
|
||||
}
|
||||
|
||||
type Header struct {
|
||||
FirstChangeId string `yaml:"firstChangeId"`
|
||||
IsWorkspace bool `yaml:"isWorkspace"`
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
Identity string `yaml:"identity"`
|
||||
SpaceId string `yaml:"spaceId"`
|
||||
}
|
||||
|
||||
type YMLList struct {
|
||||
Root *Root
|
||||
Records []*Record `yaml:"records"`
|
||||
|
||||
Keys Keys `yaml:"keys"`
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package yamltests
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
_, b, _, _ = runtime.Caller(0)
|
||||
basepath = filepath.Dir(b)
|
||||
)
|
||||
|
||||
func Path() string {
|
||||
return basepath
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
root:
|
||||
identity: A
|
||||
spaceId: space
|
||||
records:
|
||||
- identity: A
|
||||
aclChanges:
|
||||
- userInvite:
|
||||
acceptKey: key.Sign.Onetime1
|
||||
encryptionKey: key.Read.EncKey
|
||||
encryptedReadKeys: [key.Read.A]
|
||||
permissions: writer
|
||||
- userAdd:
|
||||
identity: C
|
||||
permission: reader
|
||||
encryptionKey: key.Enc.C
|
||||
encryptedReadKeys: [key.Read.A]
|
||||
readKey: key.Read.A
|
||||
- identity: B
|
||||
aclChanges:
|
||||
- userJoin:
|
||||
identity: B
|
||||
encryptionKey: key.Enc.B
|
||||
acceptKey: key.Sign.Onetime1
|
||||
encryptedReadKeys: [key.Read.A]
|
||||
readKey: key.Read.A
|
||||
keys:
|
||||
Enc:
|
||||
- name: A
|
||||
value: generated
|
||||
- name: B
|
||||
value: generated
|
||||
- name: C
|
||||
value: generated
|
||||
- name: D
|
||||
value: generated
|
||||
- name: Onetime1
|
||||
value: generated
|
||||
Sign:
|
||||
- name: A
|
||||
value: generated
|
||||
- name: B
|
||||
value: generated
|
||||
- name: C
|
||||
value: generated
|
||||
- name: D
|
||||
value: generated
|
||||
- name: Onetime1
|
||||
value: generated
|
||||
Read:
|
||||
- name: A
|
||||
value: derived
|
||||
- name: EncKey
|
||||
value: generated
|
||||
@@ -0,0 +1,58 @@
|
||||
root:
|
||||
identity: A
|
||||
spaceId: space
|
||||
records:
|
||||
- identity: A
|
||||
aclChanges:
|
||||
- userInvite:
|
||||
acceptKey: key.Sign.Onetime1
|
||||
encryptionKey: key.Read.EncKey
|
||||
encryptedReadKeys: [key.Read.A]
|
||||
permissions: writer
|
||||
- userAdd:
|
||||
identity: C
|
||||
permission: reader
|
||||
encryptionKey: key.Enc.C
|
||||
encryptedReadKeys: [key.Read.A]
|
||||
readKey: key.Read.A
|
||||
- identity: B
|
||||
aclChanges:
|
||||
- userJoin:
|
||||
identity: B
|
||||
encryptionKey: key.Enc.B
|
||||
acceptKey: key.Sign.Onetime1
|
||||
encryptedReadKeys: [key.Read.A]
|
||||
readKey: key.Read.A
|
||||
- identity: A
|
||||
aclChanges:
|
||||
- userRemove:
|
||||
removedIdentity: B
|
||||
newReadKey: key.Read.2
|
||||
identitiesLeft: [A, C]
|
||||
readKey: key.Read.2
|
||||
keys:
|
||||
Enc:
|
||||
- name: A
|
||||
value: generated
|
||||
- name: B
|
||||
value: generated
|
||||
- name: C
|
||||
value: generated
|
||||
- name: Onetime1
|
||||
value: generated
|
||||
Sign:
|
||||
- name: A
|
||||
value: generated
|
||||
- name: B
|
||||
value: generated
|
||||
- name: C
|
||||
value: generated
|
||||
- name: Onetime1
|
||||
value: generated
|
||||
Read:
|
||||
- name: A
|
||||
value: derived
|
||||
- name: 2
|
||||
value: generated
|
||||
- name: EncKey
|
||||
value: generated
|
||||
@@ -0,0 +1,28 @@
|
||||
package keychain
|
||||
|
||||
import (
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/signingkey"
|
||||
)
|
||||
|
||||
type Keychain struct {
|
||||
keys map[string]signingkey.PubKey
|
||||
}
|
||||
|
||||
func NewKeychain() *Keychain {
|
||||
return &Keychain{
|
||||
keys: make(map[string]signingkey.PubKey),
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Keychain) GetOrAdd(identity string) (signingkey.PubKey, error) {
|
||||
if key, exists := k.keys[identity]; exists {
|
||||
return key, nil
|
||||
}
|
||||
res, err := signingkey.NewSigningEd25519PubKeyFromBytes([]byte(identity))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k.keys[identity] = res.(signingkey.PubKey)
|
||||
return res.(signingkey.PubKey), nil
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package syncobjectgetter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/anyproto/any-sync/commonspace/objectsync/synchandler"
|
||||
"github.com/anytypeio/any-sync/commonspace/objectsync/synchandler"
|
||||
)
|
||||
|
||||
type SyncObject interface {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package exporter
|
||||
|
||||
import (
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/liststorage"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/liststorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/keychain"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||
)
|
||||
|
||||
type DataConverter interface {
|
||||
@@ -48,7 +48,7 @@ func (t *treeExporter) ExportUnencrypted(tree objecttree.ReadableObjectTree) (er
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
changeBuilder := objecttree.NewChangeBuilder(crypto.NewKeyStorage(), tree.Header())
|
||||
changeBuilder := objecttree.NewChangeBuilder(keychain.NewKeychain(), tree.Header())
|
||||
putStorage := func(change *objecttree.Change) (err error) {
|
||||
var raw *treechangeproto.RawTreeChangeWithId
|
||||
raw, err = changeBuilder.Marshall(change)
|
||||
@@ -68,7 +68,7 @@ func (t *treeExporter) ExportUnencrypted(tree objecttree.ReadableObjectTree) (er
|
||||
return false
|
||||
}
|
||||
// that means that change is unencrypted
|
||||
change.ReadKeyId = ""
|
||||
change.ReadKeyHash = 0
|
||||
change.Data = data
|
||||
err = putStorage(change)
|
||||
return err == nil
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package exporter
|
||||
|
||||
import (
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/liststorage"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/liststorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||
)
|
||||
|
||||
type TreeImportParams struct {
|
||||
@@ -15,7 +15,7 @@ type TreeImportParams struct {
|
||||
}
|
||||
|
||||
func ImportHistoryTree(params TreeImportParams) (tree objecttree.ReadableObjectTree, err error) {
|
||||
aclList, err := list.BuildAclList(params.ListStorage, list.NoOpAcceptorVerifier{})
|
||||
aclList, err := list.BuildAclList(params.ListStorage)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@ package objecttree
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -19,36 +18,37 @@ type Change struct {
|
||||
AclHeadId string
|
||||
Id string
|
||||
SnapshotId string
|
||||
IsSnapshot bool
|
||||
Timestamp int64
|
||||
ReadKeyId string
|
||||
Identity crypto.PubKey
|
||||
ReadKeyHash uint64
|
||||
Identity string
|
||||
Data []byte
|
||||
Model interface{}
|
||||
Signature []byte
|
||||
|
||||
// iterator helpers
|
||||
visited bool
|
||||
branchesFinished bool
|
||||
IsSnapshot bool
|
||||
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
func NewChange(id string, identity crypto.PubKey, ch *treechangeproto.TreeChange, signature []byte) *Change {
|
||||
func NewChange(id string, ch *treechangeproto.TreeChange, signature []byte) *Change {
|
||||
return &Change{
|
||||
Next: nil,
|
||||
PreviousIds: ch.TreeHeadIds,
|
||||
AclHeadId: ch.AclHeadId,
|
||||
Timestamp: ch.Timestamp,
|
||||
ReadKeyId: ch.ReadKeyId,
|
||||
ReadKeyHash: ch.CurrentReadKeyHash,
|
||||
Id: id,
|
||||
Data: ch.ChangesData,
|
||||
SnapshotId: ch.SnapshotBaseId,
|
||||
IsSnapshot: ch.IsSnapshot,
|
||||
Identity: identity,
|
||||
Identity: string(ch.Identity),
|
||||
Signature: signature,
|
||||
}
|
||||
}
|
||||
|
||||
func NewChangeFromRoot(id string, identity crypto.PubKey, ch *treechangeproto.RootChange, signature []byte) *Change {
|
||||
func NewChangeFromRoot(id string, ch *treechangeproto.RootChange, signature []byte) *Change {
|
||||
changeInfo := &treechangeproto.TreeChangeInfo{
|
||||
ChangeType: ch.ChangeType,
|
||||
ChangePayload: ch.ChangePayload,
|
||||
@@ -60,7 +60,7 @@ func NewChangeFromRoot(id string, identity crypto.PubKey, ch *treechangeproto.Ro
|
||||
Id: id,
|
||||
IsSnapshot: true,
|
||||
Timestamp: ch.Timestamp,
|
||||
Identity: identity,
|
||||
Identity: string(ch.Identity),
|
||||
Signature: signature,
|
||||
Data: data,
|
||||
Model: changeInfo,
|
||||
|
||||
@@ -2,29 +2,33 @@ package objecttree
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/util/cidutil"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/keychain"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/util/cidutil"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/signingkey"
|
||||
"github.com/anytypeio/any-sync/util/keys/symmetric"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrEmptyChange = errors.New("change payload should not be empty")
|
||||
|
||||
type BuilderContent struct {
|
||||
TreeHeadIds []string
|
||||
AclHeadId string
|
||||
SnapshotBaseId string
|
||||
ReadKeyId string
|
||||
IsSnapshot bool
|
||||
PrivKey crypto.PrivKey
|
||||
ReadKey crypto.SymKey
|
||||
Content []byte
|
||||
Timestamp int64
|
||||
TreeHeadIds []string
|
||||
AclHeadId string
|
||||
SnapshotBaseId string
|
||||
CurrentReadKeyHash uint64
|
||||
Identity []byte
|
||||
IsSnapshot bool
|
||||
SigningKey signingkey.PrivKey
|
||||
ReadKey *symmetric.Key
|
||||
Content []byte
|
||||
}
|
||||
|
||||
type InitialContent struct {
|
||||
AclHeadId string
|
||||
PrivKey crypto.PrivKey
|
||||
Identity []byte
|
||||
SigningKey signingkey.PrivKey
|
||||
SpaceId string
|
||||
Seed []byte
|
||||
ChangeType string
|
||||
@@ -59,16 +63,13 @@ type ChangeBuilder interface {
|
||||
Marshall(ch *Change) (*treechangeproto.RawTreeChangeWithId, error)
|
||||
}
|
||||
|
||||
type newChangeFunc = func(id string, identity crypto.PubKey, ch *treechangeproto.TreeChange, signature []byte) *Change
|
||||
|
||||
type changeBuilder struct {
|
||||
rootChange *treechangeproto.RawTreeChangeWithId
|
||||
keys crypto.KeyStorage
|
||||
newChange newChangeFunc
|
||||
keys *keychain.Keychain
|
||||
}
|
||||
|
||||
func NewChangeBuilder(keys crypto.KeyStorage, rootChange *treechangeproto.RawTreeChangeWithId) ChangeBuilder {
|
||||
return &changeBuilder{keys: keys, rootChange: rootChange, newChange: NewChange}
|
||||
func NewChangeBuilder(keys *keychain.Keychain, rootChange *treechangeproto.RawTreeChangeWithId) ChangeBuilder {
|
||||
return &changeBuilder{keys: keys, rootChange: rootChange}
|
||||
}
|
||||
|
||||
func (c *changeBuilder) Unmarshall(rawIdChange *treechangeproto.RawTreeChangeWithId, verify bool) (ch *Change, err error) {
|
||||
@@ -96,9 +97,15 @@ func (c *changeBuilder) Unmarshall(rawIdChange *treechangeproto.RawTreeChangeWit
|
||||
}
|
||||
|
||||
if verify {
|
||||
var identityKey signingkey.PubKey
|
||||
identityKey, err = c.keys.GetOrAdd(ch.Identity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// verifying signature
|
||||
var res bool
|
||||
res, err = ch.Identity.Verify(raw.Payload, raw.Signature)
|
||||
res, err = identityKey.Verify(raw.Payload, raw.Signature)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -115,14 +122,10 @@ func (c *changeBuilder) SetRootRawChange(rawIdChange *treechangeproto.RawTreeCha
|
||||
}
|
||||
|
||||
func (c *changeBuilder) BuildRoot(payload InitialContent) (ch *Change, rawIdChange *treechangeproto.RawTreeChangeWithId, err error) {
|
||||
identity, err := payload.PrivKey.GetPublic().Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
change := &treechangeproto.RootChange{
|
||||
AclHeadId: payload.AclHeadId,
|
||||
Timestamp: payload.Timestamp,
|
||||
Identity: identity,
|
||||
Identity: payload.Identity,
|
||||
ChangeType: payload.ChangeType,
|
||||
ChangePayload: payload.ChangePayload,
|
||||
SpaceId: payload.SpaceId,
|
||||
@@ -132,7 +135,7 @@ func (c *changeBuilder) BuildRoot(payload InitialContent) (ch *Change, rawIdChan
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signature, err := payload.PrivKey.Sign(marshalledChange)
|
||||
signature, err := payload.SigningKey.Sign(marshalledChange)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -148,7 +151,7 @@ func (c *changeBuilder) BuildRoot(payload InitialContent) (ch *Change, rawIdChan
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ch = NewChangeFromRoot(id, payload.PrivKey.GetPublic(), change, signature)
|
||||
ch = NewChangeFromRoot(id, change, signature)
|
||||
rawIdChange = &treechangeproto.RawTreeChangeWithId{
|
||||
RawChange: marshalledRawChange,
|
||||
Id: id,
|
||||
@@ -157,18 +160,14 @@ func (c *changeBuilder) BuildRoot(payload InitialContent) (ch *Change, rawIdChan
|
||||
}
|
||||
|
||||
func (c *changeBuilder) Build(payload BuilderContent) (ch *Change, rawIdChange *treechangeproto.RawTreeChangeWithId, err error) {
|
||||
identity, err := payload.PrivKey.GetPublic().Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
change := &treechangeproto.TreeChange{
|
||||
TreeHeadIds: payload.TreeHeadIds,
|
||||
AclHeadId: payload.AclHeadId,
|
||||
SnapshotBaseId: payload.SnapshotBaseId,
|
||||
ReadKeyId: payload.ReadKeyId,
|
||||
Timestamp: payload.Timestamp,
|
||||
Identity: identity,
|
||||
IsSnapshot: payload.IsSnapshot,
|
||||
TreeHeadIds: payload.TreeHeadIds,
|
||||
AclHeadId: payload.AclHeadId,
|
||||
SnapshotBaseId: payload.SnapshotBaseId,
|
||||
CurrentReadKeyHash: payload.CurrentReadKeyHash,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Identity: payload.Identity,
|
||||
IsSnapshot: payload.IsSnapshot,
|
||||
}
|
||||
if payload.ReadKey != nil {
|
||||
var encrypted []byte
|
||||
@@ -184,7 +183,7 @@ func (c *changeBuilder) Build(payload BuilderContent) (ch *Change, rawIdChange *
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signature, err := payload.PrivKey.Sign(marshalledChange)
|
||||
signature, err := payload.SigningKey.Sign(marshalledChange)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -200,7 +199,7 @@ func (c *changeBuilder) Build(payload BuilderContent) (ch *Change, rawIdChange *
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ch = c.newChange(id, payload.PrivKey.GetPublic(), change, signature)
|
||||
ch = NewChange(id, change, signature)
|
||||
rawIdChange = &treechangeproto.RawTreeChangeWithId{
|
||||
RawChange: marshalledRawChange,
|
||||
Id: id,
|
||||
@@ -212,19 +211,15 @@ func (c *changeBuilder) Marshall(ch *Change) (raw *treechangeproto.RawTreeChange
|
||||
if c.isRoot(ch.Id) {
|
||||
return c.rootChange, nil
|
||||
}
|
||||
identity, err := ch.Identity.Marshall()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
treeChange := &treechangeproto.TreeChange{
|
||||
TreeHeadIds: ch.PreviousIds,
|
||||
AclHeadId: ch.AclHeadId,
|
||||
SnapshotBaseId: ch.SnapshotId,
|
||||
ChangesData: ch.Data,
|
||||
ReadKeyId: ch.ReadKeyId,
|
||||
Timestamp: ch.Timestamp,
|
||||
Identity: identity,
|
||||
IsSnapshot: ch.IsSnapshot,
|
||||
TreeHeadIds: ch.PreviousIds,
|
||||
AclHeadId: ch.AclHeadId,
|
||||
SnapshotBaseId: ch.SnapshotId,
|
||||
ChangesData: ch.Data,
|
||||
CurrentReadKeyHash: ch.ReadKeyHash,
|
||||
Timestamp: ch.Timestamp,
|
||||
Identity: []byte(ch.Identity),
|
||||
IsSnapshot: ch.IsSnapshot,
|
||||
}
|
||||
var marshalled []byte
|
||||
marshalled, err = treeChange.Marshal()
|
||||
@@ -248,18 +243,13 @@ func (c *changeBuilder) Marshall(ch *Change) (raw *treechangeproto.RawTreeChange
|
||||
}
|
||||
|
||||
func (c *changeBuilder) unmarshallRawChange(raw *treechangeproto.RawTreeChange, id string) (ch *Change, err error) {
|
||||
var key crypto.PubKey
|
||||
if c.isRoot(id) {
|
||||
unmarshalled := &treechangeproto.RootChange{}
|
||||
err = proto.Unmarshal(raw.Payload, unmarshalled)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
key, err = c.keys.PubKeyFromProto(unmarshalled.Identity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ch = NewChangeFromRoot(id, key, unmarshalled, raw.Signature)
|
||||
ch = NewChangeFromRoot(id, unmarshalled, raw.Signature)
|
||||
return
|
||||
}
|
||||
unmarshalled := &treechangeproto.TreeChange{}
|
||||
@@ -267,11 +257,8 @@ func (c *changeBuilder) unmarshallRawChange(raw *treechangeproto.RawTreeChange,
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
key, err = c.keys.PubKeyFromProto(unmarshalled.Identity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ch = c.newChange(id, key, unmarshalled, raw.Signature)
|
||||
|
||||
ch = NewChange(id, unmarshalled, raw.Signature)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -14,47 +14,39 @@ type historyTree struct {
|
||||
*objectTree
|
||||
}
|
||||
|
||||
func (h *historyTree) rebuildFromStorage(params HistoryTreeParams) (err error) {
|
||||
err = h.rebuild(params)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h.aclList.RLock()
|
||||
defer h.aclList.RUnlock()
|
||||
state := h.aclList.AclState()
|
||||
|
||||
return h.readKeysFromAclState(state)
|
||||
}
|
||||
|
||||
func (h *historyTree) rebuild(params HistoryTreeParams) (err error) {
|
||||
var (
|
||||
beforeId = params.BeforeId
|
||||
include = params.IncludeBeforeId
|
||||
full = params.BuildFullTree
|
||||
)
|
||||
h.treeBuilder.Reset()
|
||||
if full {
|
||||
h.tree, err = h.treeBuilder.BuildFull()
|
||||
return
|
||||
}
|
||||
if beforeId == h.Id() && !include {
|
||||
func (h *historyTree) rebuildFromStorage(beforeId string, include bool) (err error) {
|
||||
ot := h.objectTree
|
||||
ot.treeBuilder.Reset()
|
||||
if beforeId == ot.Id() && !include {
|
||||
return ErrLoadBeforeRoot
|
||||
}
|
||||
|
||||
heads := []string{beforeId}
|
||||
if beforeId == "" {
|
||||
heads, err = h.treeStorage.Heads()
|
||||
heads, err = ot.treeStorage.Heads()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if !include {
|
||||
beforeChange, err := h.treeBuilder.loadChange(beforeId)
|
||||
beforeChange, err := ot.treeBuilder.loadChange(beforeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
heads = beforeChange.PreviousIds
|
||||
}
|
||||
|
||||
h.tree, err = h.treeBuilder.build(heads, nil, nil)
|
||||
ot.tree, err = ot.treeBuilder.build(heads, nil, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ot.aclList.RLock()
|
||||
defer ot.aclList.RUnlock()
|
||||
state := ot.aclList.AclState()
|
||||
|
||||
if len(ot.keys) != len(state.UserReadKeys()) {
|
||||
for key, value := range state.UserReadKeys() {
|
||||
ot.keys[key] = value
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/anyproto/any-sync/commonspace/object/tree/objecttree (interfaces: ObjectTree)
|
||||
// Source: github.com/anytypeio/any-sync/commonspace/object/tree/objecttree (interfaces: ObjectTree)
|
||||
|
||||
// Package mock_objecttree is a generated GoMock package.
|
||||
package mock_objecttree
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
list "github.com/anyproto/any-sync/commonspace/object/acl/list"
|
||||
objecttree "github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
|
||||
treechangeproto "github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
treestorage "github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
list "github.com/anytypeio/any-sync/commonspace/object/acl/list"
|
||||
objecttree "github.com/anytypeio/any-sync/commonspace/object/tree/objecttree"
|
||||
treechangeproto "github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
treestorage "github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockObjectTree is a mock of ObjectTree interface.
|
||||
@@ -258,20 +258,6 @@ func (mr *MockObjectTreeMockRecorder) IterateRoot(arg0, arg1 interface{}) *gomoc
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateRoot", reflect.TypeOf((*MockObjectTree)(nil).IterateRoot), arg0, arg1)
|
||||
}
|
||||
|
||||
// Len mocks base method.
|
||||
func (m *MockObjectTree) Len() int {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Len")
|
||||
ret0, _ := ret[0].(int)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Len indicates an expected call of Len.
|
||||
func (mr *MockObjectTreeMockRecorder) Len() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Len", reflect.TypeOf((*MockObjectTree)(nil).Len))
|
||||
}
|
||||
|
||||
// Lock mocks base method.
|
||||
func (m *MockObjectTree) Lock() {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:generate mockgen -destination mock_objecttree/mock_objecttree.go github.com/anyproto/any-sync/commonspace/object/tree/objecttree ObjectTree
|
||||
//go:generate mockgen -destination mock_objecttree/mock_objecttree.go github.com/anytypeio/any-sync/commonspace/object/tree/objecttree ObjectTree
|
||||
package objecttree
|
||||
|
||||
import (
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anyproto/any-sync/util/slice"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anytypeio/any-sync/util/keys/symmetric"
|
||||
"github.com/anytypeio/any-sync/util/slice"
|
||||
)
|
||||
|
||||
type RWLocker interface {
|
||||
@@ -27,7 +27,6 @@ var (
|
||||
ErrHasInvalidChanges = errors.New("the change is invalid")
|
||||
ErrNoCommonSnapshot = errors.New("trees doesn't have a common snapshot")
|
||||
ErrNoChangeInTree = errors.New("no such change in tree")
|
||||
ErrMissingKey = errors.New("missing current read key")
|
||||
)
|
||||
|
||||
type AddResultSummary int
|
||||
@@ -57,7 +56,6 @@ type ReadableObjectTree interface {
|
||||
ChangeInfo() *treechangeproto.TreeChangeInfo
|
||||
Heads() []string
|
||||
Root() *Change
|
||||
Len() int
|
||||
|
||||
AclList() list.AclList
|
||||
|
||||
@@ -101,8 +99,7 @@ type objectTree struct {
|
||||
root *Change
|
||||
tree *Tree
|
||||
|
||||
keys map[string]crypto.SymKey
|
||||
currentReadKey crypto.SymKey
|
||||
keys map[uint64]*symmetric.Key
|
||||
|
||||
// buffers
|
||||
difSnapshotBuf []*treechangeproto.RawTreeChangeWithId
|
||||
@@ -116,25 +113,13 @@ type objectTree struct {
|
||||
}
|
||||
|
||||
func (ot *objectTree) rebuildFromStorage(theirHeads []string, newChanges []*Change) (err error) {
|
||||
oldTree := ot.tree
|
||||
ot.treeBuilder.Reset()
|
||||
|
||||
ot.tree, err = ot.treeBuilder.Build(theirHeads, newChanges)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// in case there are new heads
|
||||
if theirHeads != nil && oldTree != nil {
|
||||
// checking that old root is still in tree
|
||||
rootCh, rootExists := ot.tree.attached[oldTree.RootId()]
|
||||
|
||||
// checking the case where theirHeads were actually below prevHeads
|
||||
// so if we did load some extra data in the tree, let's reduce it to old root
|
||||
if slice.UnsortedEquals(oldTree.headIds, ot.tree.headIds) && rootExists && ot.tree.RootId() != oldTree.RootId() {
|
||||
ot.tree.makeRootAndRemove(rootCh)
|
||||
}
|
||||
}
|
||||
|
||||
// during building the tree we may have marked some changes as possible roots,
|
||||
// but obviously they are not roots, because of the way how we construct the tree
|
||||
ot.tree.clearPossibleRoots()
|
||||
@@ -148,10 +133,6 @@ func (ot *objectTree) Id() string {
|
||||
return ot.id
|
||||
}
|
||||
|
||||
func (ot *objectTree) Len() int {
|
||||
return ot.tree.Len()
|
||||
}
|
||||
|
||||
func (ot *objectTree) AclList() list.AclList {
|
||||
return ot.aclList
|
||||
}
|
||||
@@ -199,7 +180,7 @@ func (ot *objectTree) AddContent(ctx context.Context, content SignableChangeCont
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = ot.treeStorage.AddRawChangesSetHeads([]*treechangeproto.RawTreeChangeWithId{rawChange}, []string{objChange.Id})
|
||||
err = ot.treeStorage.TransactionAdd([]*treechangeproto.RawTreeChangeWithId{rawChange}, []string{objChange.Id})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -243,38 +224,34 @@ func (ot *objectTree) prepareBuilderContent(content SignableChangeContent) (cnt
|
||||
defer ot.aclList.RUnlock()
|
||||
|
||||
var (
|
||||
state = ot.aclList.AclState() // special method for own keys
|
||||
readKey crypto.SymKey
|
||||
pubKey = content.Key.GetPublic()
|
||||
readKeyId string
|
||||
state = ot.aclList.AclState() // special method for own keys
|
||||
readKey *symmetric.Key
|
||||
readKeyHash uint64
|
||||
)
|
||||
if !state.Permissions(pubKey).CanWrite() {
|
||||
canWrite := state.HasPermission(content.Identity, aclrecordproto.AclUserPermissions_Writer) ||
|
||||
state.HasPermission(content.Identity, aclrecordproto.AclUserPermissions_Admin)
|
||||
if !canWrite {
|
||||
err = list.ErrInsufficientPermissions
|
||||
return
|
||||
}
|
||||
|
||||
if content.IsEncrypted {
|
||||
readKeyId = state.CurrentReadKeyId()
|
||||
if ot.currentReadKey == nil {
|
||||
err = ErrMissingKey
|
||||
readKeyHash = state.CurrentReadKeyHash()
|
||||
readKey, err = state.CurrentReadKey()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
readKey = ot.currentReadKey
|
||||
}
|
||||
timestamp := content.Timestamp
|
||||
if timestamp <= 0 {
|
||||
timestamp = time.Now().Unix()
|
||||
}
|
||||
cnt = BuilderContent{
|
||||
TreeHeadIds: ot.tree.Heads(),
|
||||
AclHeadId: ot.aclList.Head().Id,
|
||||
SnapshotBaseId: ot.tree.RootId(),
|
||||
ReadKeyId: readKeyId,
|
||||
IsSnapshot: content.IsSnapshot,
|
||||
PrivKey: content.Key,
|
||||
ReadKey: readKey,
|
||||
Content: content.Data,
|
||||
Timestamp: timestamp,
|
||||
TreeHeadIds: ot.tree.Heads(),
|
||||
AclHeadId: ot.aclList.Head().Id,
|
||||
SnapshotBaseId: ot.tree.RootId(),
|
||||
CurrentReadKeyHash: readKeyHash,
|
||||
Identity: content.Identity,
|
||||
IsSnapshot: content.IsSnapshot,
|
||||
SigningKey: content.Key,
|
||||
ReadKey: readKey,
|
||||
Content: content.Data,
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -294,11 +271,7 @@ func (ot *objectTree) AddRawChanges(ctx context.Context, changesPayload RawChang
|
||||
addResult.Mode = Rebuild
|
||||
}
|
||||
|
||||
err = ot.treeStorage.AddRawChangesSetHeads(addResult.Added, addResult.Heads)
|
||||
if err != nil {
|
||||
// rolling back all changes made to inmemory state
|
||||
ot.rebuildFromStorage(nil, nil)
|
||||
}
|
||||
err = ot.treeStorage.TransactionAdd(addResult.Added, addResult.Heads)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -361,7 +334,7 @@ func (ot *objectTree) addRawChanges(ctx context.Context, changesPayload RawChang
|
||||
}
|
||||
|
||||
// checks if we need to go to database
|
||||
snapshotNotInTree := func(ch *Change) bool {
|
||||
isOldSnapshot := func(ch *Change) bool {
|
||||
if ch.SnapshotId == ot.tree.RootId() {
|
||||
return false
|
||||
}
|
||||
@@ -376,12 +349,26 @@ func (ot *objectTree) addRawChanges(ctx context.Context, changesPayload RawChang
|
||||
|
||||
shouldRebuildFromStorage := false
|
||||
// checking if we have some changes with different snapshot and then rebuilding
|
||||
for _, ch := range ot.newChangesBuf {
|
||||
if snapshotNotInTree(ch) {
|
||||
for idx, ch := range ot.newChangesBuf {
|
||||
if isOldSnapshot(ch) {
|
||||
var exists bool
|
||||
// checking if it exists in the storage, if yes, then at some point it was added to the tree
|
||||
// thus we don't need to look at this change
|
||||
exists, err = ot.treeStorage.HasChange(ctx, ch.Id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
// marking as nil to delete after
|
||||
ot.newChangesBuf[idx] = nil
|
||||
continue
|
||||
}
|
||||
// we haven't seen the change, and it refers to old snapshot, so we should rebuild
|
||||
shouldRebuildFromStorage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// discarding all previously seen changes
|
||||
ot.newChangesBuf = slice.DiscardFromSlice(ot.newChangesBuf, func(ch *Change) bool { return ch == nil })
|
||||
|
||||
if shouldRebuildFromStorage {
|
||||
err = ot.rebuildFromStorage(changesPayload.NewHeads, ot.newChangesBuf)
|
||||
@@ -478,11 +465,6 @@ func (ot *objectTree) createAddResult(oldHeads []string, mode Mode, treeChangesA
|
||||
|
||||
var added []*treechangeproto.RawTreeChangeWithId
|
||||
added, err = getAddedChanges(treeChangesAdded)
|
||||
if !ot.treeBuilder.keepInMemoryData {
|
||||
for _, ch := range treeChangesAdded {
|
||||
ch.Data = nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -506,11 +488,11 @@ func (ot *objectTree) IterateFrom(id string, convert ChangeConvertFunc, iterate
|
||||
}
|
||||
decrypt := func(c *Change) (decrypted []byte, err error) {
|
||||
// the change is not encrypted
|
||||
if c.ReadKeyId == "" {
|
||||
if c.ReadKeyHash == 0 {
|
||||
decrypted = c.Data
|
||||
return
|
||||
}
|
||||
readKey, exists := ot.keys[c.ReadKeyId]
|
||||
readKey, exists := ot.keys[c.ReadKeyHash]
|
||||
if !exists {
|
||||
err = list.ErrNoReadKey
|
||||
return
|
||||
@@ -549,8 +531,22 @@ func (ot *objectTree) IterateFrom(id string, convert ChangeConvertFunc, iterate
|
||||
}
|
||||
|
||||
func (ot *objectTree) HasChanges(chs ...string) bool {
|
||||
hasChange := func(s string) bool {
|
||||
_, attachedExists := ot.tree.attached[s]
|
||||
if attachedExists {
|
||||
return attachedExists
|
||||
}
|
||||
|
||||
has, err := ot.treeStorage.HasChange(context.Background(), s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return has
|
||||
}
|
||||
|
||||
for _, ch := range chs {
|
||||
if _, attachedExists := ot.tree.attached[ch]; !attachedExists {
|
||||
if !hasChange(ch) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -617,7 +613,19 @@ func (ot *objectTree) ChangesAfterCommonSnapshot(theirPath, theirHeads []string)
|
||||
}
|
||||
}
|
||||
|
||||
return ot.rawChangeLoader.Load(commonSnapshot, ot.tree, theirHeads)
|
||||
if commonSnapshot == ot.tree.RootId() {
|
||||
return ot.getChangesFromTree(theirHeads)
|
||||
} else {
|
||||
return ot.getChangesFromDB(commonSnapshot, theirHeads)
|
||||
}
|
||||
}
|
||||
|
||||
func (ot *objectTree) getChangesFromTree(theirHeads []string) (rawChanges []*treechangeproto.RawTreeChangeWithId, err error) {
|
||||
return ot.rawChangeLoader.LoadFromTree(ot.tree, theirHeads)
|
||||
}
|
||||
|
||||
func (ot *objectTree) getChangesFromDB(commonSnapshot string, theirHeads []string) (rawChanges []*treechangeproto.RawTreeChangeWithId, err error) {
|
||||
return ot.rawChangeLoader.LoadFromStorage(commonSnapshot, ot.tree.headIds, theirHeads)
|
||||
}
|
||||
|
||||
func (ot *objectTree) snapshotPathIsActual() bool {
|
||||
@@ -629,9 +637,11 @@ func (ot *objectTree) validateTree(newChanges []*Change) error {
|
||||
defer ot.aclList.RUnlock()
|
||||
state := ot.aclList.AclState()
|
||||
|
||||
err := ot.readKeysFromAclState(state)
|
||||
if err != nil {
|
||||
return err
|
||||
// just not to take lock many times, updating the key map from aclList
|
||||
if len(ot.keys) != len(state.UserReadKeys()) {
|
||||
for key, value := range state.UserReadKeys() {
|
||||
ot.keys[key] = value
|
||||
}
|
||||
}
|
||||
if len(newChanges) == 0 {
|
||||
return ot.validator.ValidateFullTree(ot.tree, ot.aclList)
|
||||
@@ -640,26 +650,6 @@ func (ot *objectTree) validateTree(newChanges []*Change) error {
|
||||
return ot.validator.ValidateNewChanges(ot.tree, ot.aclList, newChanges)
|
||||
}
|
||||
|
||||
func (ot *objectTree) readKeysFromAclState(state *list.AclState) (err error) {
|
||||
// just not to take lock many times, updating the key map from aclList
|
||||
if len(ot.keys) == len(state.UserReadKeys()) {
|
||||
return nil
|
||||
}
|
||||
for key, value := range state.UserReadKeys() {
|
||||
treeKey, err := deriveTreeKey(value, ot.id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ot.keys[key] = treeKey
|
||||
}
|
||||
curKey, err := state.CurrentReadKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ot.currentReadKey, err = deriveTreeKey(curKey, ot.id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ot *objectTree) Debug(parser DescriptionParser) (DebugInfo, error) {
|
||||
return objectTreeDebug{}.debugInfo(ot, parser)
|
||||
}
|
||||
@@ -2,45 +2,91 @@ package objecttree
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/accountdata"
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/testutils/acllistbuilder"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockChangeCreator struct{}
|
||||
|
||||
func (c *mockChangeCreator) createRoot(id, aclId string) *treechangeproto.RawTreeChangeWithId {
|
||||
aclChange := &treechangeproto.RootChange{
|
||||
AclHeadId: aclId,
|
||||
}
|
||||
res, _ := aclChange.Marshal()
|
||||
|
||||
raw := &treechangeproto.RawTreeChange{
|
||||
Payload: res,
|
||||
Signature: nil,
|
||||
}
|
||||
rawMarshalled, _ := raw.Marshal()
|
||||
|
||||
return &treechangeproto.RawTreeChangeWithId{
|
||||
RawChange: rawMarshalled,
|
||||
Id: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *mockChangeCreator) createRaw(id, aclId, snapshotId string, isSnapshot bool, prevIds ...string) *treechangeproto.RawTreeChangeWithId {
|
||||
aclChange := &treechangeproto.TreeChange{
|
||||
TreeHeadIds: prevIds,
|
||||
AclHeadId: aclId,
|
||||
SnapshotBaseId: snapshotId,
|
||||
ChangesData: nil,
|
||||
IsSnapshot: isSnapshot,
|
||||
}
|
||||
res, _ := aclChange.Marshal()
|
||||
|
||||
raw := &treechangeproto.RawTreeChange{
|
||||
Payload: res,
|
||||
Signature: nil,
|
||||
}
|
||||
rawMarshalled, _ := raw.Marshal()
|
||||
|
||||
return &treechangeproto.RawTreeChangeWithId{
|
||||
RawChange: rawMarshalled,
|
||||
Id: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *mockChangeCreator) createNewTreeStorage(treeId, aclHeadId string) treestorage.TreeStorage {
|
||||
root := c.createRoot(treeId, aclHeadId)
|
||||
treeStorage, _ := treestorage.NewInMemoryTreeStorage(root, []string{root.Id}, []*treechangeproto.RawTreeChangeWithId{root})
|
||||
return treeStorage
|
||||
}
|
||||
|
||||
type testTreeContext struct {
|
||||
aclList list.AclList
|
||||
treeStorage treestorage.TreeStorage
|
||||
changeCreator *MockChangeCreator
|
||||
changeBuilder ChangeBuilder
|
||||
changeCreator *mockChangeCreator
|
||||
objTree ObjectTree
|
||||
}
|
||||
|
||||
func prepareAclList(t *testing.T) (list.AclList, *accountdata.AccountKeys) {
|
||||
randKeys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
aclList, err := list.NewTestDerivedAcl("spaceId", randKeys)
|
||||
func prepareAclList(t *testing.T) list.AclList {
|
||||
st, err := acllistbuilder.NewListStorageWithTestName("userjoinexample.yml")
|
||||
require.NoError(t, err, "building storage should not result in error")
|
||||
|
||||
aclList, err := list.BuildAclList(st)
|
||||
require.NoError(t, err, "building acl list should be without error")
|
||||
|
||||
return aclList, randKeys
|
||||
return aclList
|
||||
}
|
||||
|
||||
func prepareHistoryTreeDeps(aclList list.AclList) (*MockChangeCreator, objectTreeDeps) {
|
||||
changeCreator := NewMockChangeCreator()
|
||||
treeStorage := changeCreator.CreateNewTreeStorage("0", aclList.Head().Id)
|
||||
func prepareTreeDeps(aclList list.AclList) (*mockChangeCreator, objectTreeDeps) {
|
||||
changeCreator := &mockChangeCreator{}
|
||||
treeStorage := changeCreator.createNewTreeStorage("0", aclList.Head().Id)
|
||||
root, _ := treeStorage.Root()
|
||||
changeBuilder := &nonVerifiableChangeBuilder{
|
||||
ChangeBuilder: NewChangeBuilder(newMockKeyStorage(), root),
|
||||
ChangeBuilder: NewChangeBuilder(nil, root),
|
||||
}
|
||||
deps := objectTreeDeps{
|
||||
changeBuilder: changeBuilder,
|
||||
treeBuilder: newTreeBuilder(true, treeStorage, changeBuilder),
|
||||
treeBuilder: newTreeBuilder(treeStorage, changeBuilder),
|
||||
treeStorage: treeStorage,
|
||||
rawChangeLoader: newRawChangeLoader(treeStorage, changeBuilder),
|
||||
validator: &noOpTreeValidator{},
|
||||
@@ -50,26 +96,23 @@ func prepareHistoryTreeDeps(aclList list.AclList) (*MockChangeCreator, objectTre
|
||||
}
|
||||
|
||||
func prepareTreeContext(t *testing.T, aclList list.AclList) testTreeContext {
|
||||
return prepareContext(t, aclList, BuildTestableTree, nil)
|
||||
}
|
||||
|
||||
func prepareEmptyDataTreeContext(t *testing.T, aclList list.AclList, additionalChanges func(changeCreator *MockChangeCreator) RawChangesPayload) testTreeContext {
|
||||
return prepareContext(t, aclList, BuildEmptyDataTestableTree, additionalChanges)
|
||||
}
|
||||
|
||||
func prepareContext(
|
||||
t *testing.T,
|
||||
aclList list.AclList,
|
||||
objTreeBuilder BuildObjectTreeFunc,
|
||||
additionalChanges func(changeCreator *MockChangeCreator) RawChangesPayload) testTreeContext {
|
||||
changeCreator := NewMockChangeCreator()
|
||||
treeStorage := changeCreator.CreateNewTreeStorage("0", aclList.Head().Id)
|
||||
if additionalChanges != nil {
|
||||
payload := additionalChanges(changeCreator)
|
||||
err := treeStorage.AddRawChangesSetHeads(payload.RawChanges, payload.NewHeads)
|
||||
require.NoError(t, err)
|
||||
changeCreator := &mockChangeCreator{}
|
||||
treeStorage := changeCreator.createNewTreeStorage("0", aclList.Head().Id)
|
||||
root, _ := treeStorage.Root()
|
||||
changeBuilder := &nonVerifiableChangeBuilder{
|
||||
ChangeBuilder: NewChangeBuilder(nil, root),
|
||||
}
|
||||
objTree, err := objTreeBuilder(treeStorage, aclList)
|
||||
deps := objectTreeDeps{
|
||||
changeBuilder: changeBuilder,
|
||||
treeBuilder: newTreeBuilder(treeStorage, changeBuilder),
|
||||
treeStorage: treeStorage,
|
||||
rawChangeLoader: newRawChangeLoader(treeStorage, changeBuilder),
|
||||
validator: &noOpTreeValidator{},
|
||||
aclList: aclList,
|
||||
}
|
||||
|
||||
// check build
|
||||
objTree, err := buildObjectTree(deps)
|
||||
require.NoError(t, err, "building tree should be without error")
|
||||
|
||||
// check tree iterate
|
||||
@@ -79,71 +122,18 @@ func prepareContext(
|
||||
return true
|
||||
})
|
||||
require.NoError(t, err, "iterate should be without error")
|
||||
if additionalChanges == nil {
|
||||
assert.Equal(t, []string{"0"}, iterChangesId)
|
||||
}
|
||||
assert.Equal(t, []string{"0"}, iterChangesId)
|
||||
return testTreeContext{
|
||||
aclList: aclList,
|
||||
treeStorage: treeStorage,
|
||||
changeBuilder: changeBuilder,
|
||||
changeCreator: changeCreator,
|
||||
objTree: objTree,
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectTree(t *testing.T) {
|
||||
aclList, keys := prepareAclList(t)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("add content", func(t *testing.T) {
|
||||
root, err := CreateObjectTreeRoot(ObjectTreeCreatePayload{
|
||||
PrivKey: keys.SignKey,
|
||||
ChangeType: "changeType",
|
||||
ChangePayload: nil,
|
||||
SpaceId: "spaceId",
|
||||
IsEncrypted: true,
|
||||
}, aclList)
|
||||
require.NoError(t, err)
|
||||
store, _ := treestorage.NewInMemoryTreeStorage(root, []string{root.Id}, []*treechangeproto.RawTreeChangeWithId{root})
|
||||
oTree, err := BuildObjectTree(store, aclList)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("0 timestamp is changed to current", func(t *testing.T) {
|
||||
start := time.Now()
|
||||
res, err := oTree.AddContent(ctx, SignableChangeContent{
|
||||
Data: []byte("some"),
|
||||
Key: keys.SignKey,
|
||||
IsSnapshot: false,
|
||||
IsEncrypted: true,
|
||||
Timestamp: 0,
|
||||
})
|
||||
end := time.Now()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, oTree.Heads(), 1)
|
||||
require.Equal(t, res.Added[0].Id, oTree.Heads()[0])
|
||||
ch, err := oTree.(*objectTree).changeBuilder.Unmarshall(res.Added[0], true)
|
||||
require.NoError(t, err)
|
||||
require.GreaterOrEqual(t, start.Unix(), ch.Timestamp)
|
||||
require.LessOrEqual(t, end.Unix(), ch.Timestamp)
|
||||
require.Equal(t, res.Added[0].Id, oTree.(*objectTree).tree.lastIteratedHeadId)
|
||||
})
|
||||
t.Run("timestamp is set correctly", func(t *testing.T) {
|
||||
someTs := time.Now().Add(time.Hour).Unix()
|
||||
res, err := oTree.AddContent(ctx, SignableChangeContent{
|
||||
Data: []byte("some"),
|
||||
Key: keys.SignKey,
|
||||
IsSnapshot: false,
|
||||
IsEncrypted: true,
|
||||
Timestamp: someTs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, oTree.Heads(), 1)
|
||||
require.Equal(t, res.Added[0].Id, oTree.Heads()[0])
|
||||
ch, err := oTree.(*objectTree).changeBuilder.Unmarshall(res.Added[0], true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ch.Timestamp, someTs)
|
||||
require.Equal(t, res.Added[0].Id, oTree.(*objectTree).tree.lastIteratedHeadId)
|
||||
})
|
||||
})
|
||||
aclList := prepareAclList(t)
|
||||
|
||||
t.Run("add simple", func(t *testing.T) {
|
||||
ctx := prepareTreeContext(t, aclList)
|
||||
@@ -152,8 +142,8 @@ func TestObjectTree(t *testing.T) {
|
||||
objTree := ctx.objTree
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.createRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.createRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
}
|
||||
payload := RawChangesPayload{
|
||||
NewHeads: []string{rawChanges[len(rawChanges)-1].Id},
|
||||
@@ -197,7 +187,7 @@ func TestObjectTree(t *testing.T) {
|
||||
objTree := ctx.objTree
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("0", aclList.Head().Id, "", true, ""),
|
||||
changeCreator.createRaw("0", aclList.Head().Id, "", true, ""),
|
||||
}
|
||||
payload := RawChangesPayload{
|
||||
NewHeads: []string{rawChanges[len(rawChanges)-1].Id},
|
||||
@@ -221,33 +211,7 @@ func TestObjectTree(t *testing.T) {
|
||||
objTree := ctx.objTree
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
}
|
||||
payload := RawChangesPayload{
|
||||
NewHeads: []string{rawChanges[len(rawChanges)-1].Id},
|
||||
RawChanges: rawChanges,
|
||||
}
|
||||
res, err := objTree.AddRawChanges(context.Background(), payload)
|
||||
require.NoError(t, err, "adding changes should be without error")
|
||||
|
||||
// check result
|
||||
assert.Equal(t, []string{"0"}, res.OldHeads)
|
||||
assert.Equal(t, []string{"0"}, res.Heads)
|
||||
assert.Equal(t, 0, len(res.Added))
|
||||
assert.Equal(t, Nothing, res.Mode)
|
||||
|
||||
// check tree heads
|
||||
assert.Equal(t, []string{"0"}, objTree.Heads())
|
||||
})
|
||||
|
||||
t.Run("add not connected changes", func(t *testing.T) {
|
||||
ctx := prepareTreeContext(t, aclList)
|
||||
changeCreator := ctx.changeCreator
|
||||
objTree := ctx.objTree
|
||||
|
||||
// this change could in theory replace current snapshot, we should prevent that
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "0", true, "1"),
|
||||
changeCreator.createRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
}
|
||||
payload := RawChangesPayload{
|
||||
NewHeads: []string{rawChanges[len(rawChanges)-1].Id},
|
||||
@@ -273,10 +237,10 @@ func TestObjectTree(t *testing.T) {
|
||||
objTree := ctx.objTree
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.CreateRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
changeCreator.CreateRaw("4", aclList.Head().Id, "3", false, "3"),
|
||||
changeCreator.createRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.createRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.createRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
changeCreator.createRaw("4", aclList.Head().Id, "3", false, "3"),
|
||||
}
|
||||
payload := RawChangesPayload{
|
||||
NewHeads: []string{rawChanges[len(rawChanges)-1].Id},
|
||||
@@ -323,9 +287,9 @@ func TestObjectTree(t *testing.T) {
|
||||
objTree := ctx.objTree
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.CreateRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
changeCreator.createRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.createRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.createRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
}
|
||||
payload := RawChangesPayload{
|
||||
NewHeads: []string{rawChanges[len(rawChanges)-1].Id},
|
||||
@@ -341,195 +305,18 @@ func TestObjectTree(t *testing.T) {
|
||||
assert.Equal(t, true, objTree.(*objectTree).snapshotPathIsActual())
|
||||
})
|
||||
|
||||
t.Run("test empty data tree", func(t *testing.T) {
|
||||
t.Run("empty tree add", func(t *testing.T) {
|
||||
ctx := prepareEmptyDataTreeContext(t, aclList, nil)
|
||||
changeCreator := ctx.changeCreator
|
||||
objTree := ctx.objTree
|
||||
|
||||
rawChangesFirst := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRawWithData("1", aclList.Head().Id, "0", false, []byte("1"), "0"),
|
||||
changeCreator.CreateRawWithData("2", aclList.Head().Id, "0", false, []byte("2"), "1"),
|
||||
changeCreator.CreateRawWithData("3", aclList.Head().Id, "0", false, []byte("3"), "2"),
|
||||
}
|
||||
rawChangesSecond := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRawWithData("4", aclList.Head().Id, "0", false, []byte("4"), "2"),
|
||||
changeCreator.CreateRawWithData("5", aclList.Head().Id, "0", false, []byte("5"), "1"),
|
||||
changeCreator.CreateRawWithData("6", aclList.Head().Id, "0", false, []byte("6"), "3", "4", "5"),
|
||||
}
|
||||
|
||||
// making them to be saved in unattached
|
||||
_, err := objTree.AddRawChanges(context.Background(), RawChangesPayload{
|
||||
NewHeads: []string{"6"},
|
||||
RawChanges: rawChangesSecond,
|
||||
})
|
||||
require.NoError(t, err, "adding changes should be without error")
|
||||
// attaching them
|
||||
res, err := objTree.AddRawChanges(context.Background(), RawChangesPayload{
|
||||
NewHeads: []string{"3"},
|
||||
RawChanges: rawChangesFirst,
|
||||
})
|
||||
|
||||
require.NoError(t, err, "adding changes should be without error")
|
||||
require.Equal(t, "0", objTree.Root().Id)
|
||||
require.Equal(t, []string{"6"}, objTree.Heads())
|
||||
require.Equal(t, 6, len(res.Added))
|
||||
|
||||
// checking that added changes still have data
|
||||
for _, ch := range res.Added {
|
||||
unmarshallRaw := &treechangeproto.RawTreeChange{}
|
||||
proto.Unmarshal(ch.RawChange, unmarshallRaw)
|
||||
treeCh := &treechangeproto.TreeChange{}
|
||||
proto.Unmarshal(unmarshallRaw.Payload, treeCh)
|
||||
require.Equal(t, ch.Id, string(treeCh.ChangesData))
|
||||
}
|
||||
|
||||
// checking that the tree doesn't have data in memory
|
||||
err = objTree.IterateRoot(nil, func(change *Change) bool {
|
||||
if change.Id == "0" {
|
||||
return true
|
||||
}
|
||||
require.Nil(t, change.Data)
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("empty tree load", func(t *testing.T) {
|
||||
ctx := prepareEmptyDataTreeContext(t, aclList, func(changeCreator *MockChangeCreator) RawChangesPayload {
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRawWithData("1", aclList.Head().Id, "0", false, []byte("1"), "0"),
|
||||
changeCreator.CreateRawWithData("2", aclList.Head().Id, "0", false, []byte("2"), "1"),
|
||||
changeCreator.CreateRawWithData("3", aclList.Head().Id, "0", false, []byte("3"), "2"),
|
||||
changeCreator.CreateRawWithData("4", aclList.Head().Id, "0", false, []byte("4"), "2"),
|
||||
changeCreator.CreateRawWithData("5", aclList.Head().Id, "0", false, []byte("5"), "1"),
|
||||
changeCreator.CreateRawWithData("6", aclList.Head().Id, "0", false, []byte("6"), "3", "4", "5"),
|
||||
}
|
||||
return RawChangesPayload{NewHeads: []string{"6"}, RawChanges: rawChanges}
|
||||
})
|
||||
ctx.objTree.IterateRoot(nil, func(change *Change) bool {
|
||||
if change.Id == "0" {
|
||||
return true
|
||||
}
|
||||
require.Nil(t, change.Data)
|
||||
return true
|
||||
})
|
||||
rawChanges, err := ctx.objTree.ChangesAfterCommonSnapshot([]string{"0"}, []string{"6"})
|
||||
require.NoError(t, err)
|
||||
for _, ch := range rawChanges {
|
||||
unmarshallRaw := &treechangeproto.RawTreeChange{}
|
||||
proto.Unmarshal(ch.RawChange, unmarshallRaw)
|
||||
treeCh := &treechangeproto.TreeChange{}
|
||||
proto.Unmarshal(unmarshallRaw.Payload, treeCh)
|
||||
require.Equal(t, ch.Id, string(treeCh.ChangesData))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("rollback when add to storage returns error", func(t *testing.T) {
|
||||
ctx := prepareTreeContext(t, aclList)
|
||||
changeCreator := ctx.changeCreator
|
||||
objTree := ctx.objTree
|
||||
store := ctx.treeStorage.(*treestorage.InMemoryTreeStorage)
|
||||
addErr := fmt.Errorf("error saving")
|
||||
store.SetReturnErrorOnAdd(addErr)
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", true, "0"),
|
||||
}
|
||||
payload := RawChangesPayload{
|
||||
NewHeads: []string{"1"},
|
||||
RawChanges: rawChanges,
|
||||
}
|
||||
_, err := objTree.AddRawChanges(context.Background(), payload)
|
||||
require.Error(t, err, addErr)
|
||||
require.Equal(t, "0", objTree.Root().Id)
|
||||
})
|
||||
|
||||
t.Run("their heads before common snapshot", func(t *testing.T) {
|
||||
// checking that adding old changes did not affect the tree
|
||||
ctx := prepareTreeContext(t, aclList)
|
||||
changeCreator := ctx.changeCreator
|
||||
objTree := ctx.objTree
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", true, "0"),
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "1", false, "1"),
|
||||
changeCreator.CreateRaw("3", aclList.Head().Id, "1", true, "2"),
|
||||
changeCreator.CreateRaw("4", aclList.Head().Id, "1", false, "2"),
|
||||
changeCreator.CreateRaw("5", aclList.Head().Id, "1", false, "1"),
|
||||
changeCreator.CreateRaw("6", aclList.Head().Id, "1", true, "3", "4", "5"),
|
||||
}
|
||||
payload := RawChangesPayload{
|
||||
NewHeads: []string{rawChanges[len(rawChanges)-1].Id},
|
||||
RawChanges: rawChanges,
|
||||
}
|
||||
_, err := objTree.AddRawChanges(context.Background(), payload)
|
||||
require.NoError(t, err, "adding changes should be without error")
|
||||
require.Equal(t, "6", objTree.Root().Id)
|
||||
|
||||
rawChangesPrevious := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", true, "0"),
|
||||
}
|
||||
payload = RawChangesPayload{
|
||||
NewHeads: []string{"1"},
|
||||
RawChanges: rawChangesPrevious,
|
||||
}
|
||||
_, err = objTree.AddRawChanges(context.Background(), payload)
|
||||
require.NoError(t, err, "adding changes should be without error")
|
||||
require.Equal(t, "6", objTree.Root().Id)
|
||||
})
|
||||
|
||||
t.Run("stored changes will not break the pipeline if heads were not updated", func(t *testing.T) {
|
||||
ctx := prepareTreeContext(t, aclList)
|
||||
changeCreator := ctx.changeCreator
|
||||
objTree := ctx.objTree
|
||||
store := ctx.treeStorage.(*treestorage.InMemoryTreeStorage)
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", true, "0"),
|
||||
}
|
||||
payload := RawChangesPayload{
|
||||
NewHeads: []string{rawChanges[len(rawChanges)-1].Id},
|
||||
RawChanges: rawChanges,
|
||||
}
|
||||
_, err := objTree.AddRawChanges(context.Background(), payload)
|
||||
require.NoError(t, err, "adding changes should be without error")
|
||||
require.Equal(t, "1", objTree.Root().Id)
|
||||
|
||||
// creating changes to save in the storage
|
||||
// to imitate the condition where all changes are in the storage
|
||||
// but the head was not updated
|
||||
storageChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "1", false, "1"),
|
||||
changeCreator.CreateRaw("3", aclList.Head().Id, "1", true, "2"),
|
||||
changeCreator.CreateRaw("4", aclList.Head().Id, "1", false, "2"),
|
||||
changeCreator.CreateRaw("5", aclList.Head().Id, "1", false, "1"),
|
||||
changeCreator.CreateRaw("6", aclList.Head().Id, "1", true, "3", "4", "5"),
|
||||
}
|
||||
store.AddRawChangesSetHeads(storageChanges, []string{"1"})
|
||||
|
||||
// updating with subset of those changes to see that everything will still work
|
||||
payload = RawChangesPayload{
|
||||
NewHeads: []string{"6"},
|
||||
RawChanges: storageChanges,
|
||||
}
|
||||
_, err = objTree.AddRawChanges(context.Background(), payload)
|
||||
require.NoError(t, err, "adding changes should be without error")
|
||||
require.Equal(t, "6", objTree.Root().Id)
|
||||
})
|
||||
|
||||
t.Run("changes from tree after common snapshot complex", func(t *testing.T) {
|
||||
ctx := prepareTreeContext(t, aclList)
|
||||
changeCreator := ctx.changeCreator
|
||||
objTree := ctx.objTree
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.CreateRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
changeCreator.CreateRaw("4", aclList.Head().Id, "0", false, "2"),
|
||||
changeCreator.CreateRaw("5", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.CreateRaw("6", aclList.Head().Id, "0", false, "3", "4", "5"),
|
||||
changeCreator.createRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.createRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.createRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
changeCreator.createRaw("4", aclList.Head().Id, "0", false, "2"),
|
||||
changeCreator.createRaw("5", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.createRaw("6", aclList.Head().Id, "0", false, "3", "4", "5"),
|
||||
}
|
||||
|
||||
payload := RawChangesPayload{
|
||||
@@ -603,13 +390,13 @@ func TestObjectTree(t *testing.T) {
|
||||
objTree := ctx.objTree
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.CreateRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
changeCreator.CreateRaw("4", aclList.Head().Id, "0", false, "2"),
|
||||
changeCreator.CreateRaw("5", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.createRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.createRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.createRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
changeCreator.createRaw("4", aclList.Head().Id, "0", false, "2"),
|
||||
changeCreator.createRaw("5", aclList.Head().Id, "0", false, "1"),
|
||||
// main difference from tree example
|
||||
changeCreator.CreateRaw("6", aclList.Head().Id, "0", true, "3", "4", "5"),
|
||||
changeCreator.createRaw("6", aclList.Head().Id, "0", true, "3", "4", "5"),
|
||||
}
|
||||
|
||||
payload := RawChangesPayload{
|
||||
@@ -684,9 +471,9 @@ func TestObjectTree(t *testing.T) {
|
||||
objTree := ctx.objTree
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.CreateRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
changeCreator.createRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.createRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.createRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
}
|
||||
payload := RawChangesPayload{
|
||||
NewHeads: []string{rawChanges[len(rawChanges)-1].Id},
|
||||
@@ -698,9 +485,9 @@ func TestObjectTree(t *testing.T) {
|
||||
require.Equal(t, "3", objTree.Root().Id)
|
||||
|
||||
rawChanges = []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("4", aclList.Head().Id, "0", false, "2"),
|
||||
changeCreator.CreateRaw("5", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.CreateRaw("6", aclList.Head().Id, "0", false, "3", "4", "5"),
|
||||
changeCreator.createRaw("4", aclList.Head().Id, "0", false, "2"),
|
||||
changeCreator.createRaw("5", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.createRaw("6", aclList.Head().Id, "0", false, "3", "4", "5"),
|
||||
}
|
||||
payload = RawChangesPayload{
|
||||
NewHeads: []string{rawChanges[len(rawChanges)-1].Id},
|
||||
@@ -741,17 +528,17 @@ func TestObjectTree(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("test history tree not include", func(t *testing.T) {
|
||||
changeCreator, deps := prepareHistoryTreeDeps(aclList)
|
||||
changeCreator, deps := prepareTreeDeps(aclList)
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.CreateRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
changeCreator.CreateRaw("4", aclList.Head().Id, "0", false, "2"),
|
||||
changeCreator.CreateRaw("5", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.CreateRaw("6", aclList.Head().Id, "0", false, "3", "4", "5"),
|
||||
changeCreator.createRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.createRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.createRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
changeCreator.createRaw("4", aclList.Head().Id, "0", false, "2"),
|
||||
changeCreator.createRaw("5", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.createRaw("6", aclList.Head().Id, "0", false, "3", "4", "5"),
|
||||
}
|
||||
deps.treeStorage.AddRawChangesSetHeads(rawChanges, []string{"6"})
|
||||
deps.treeStorage.TransactionAdd(rawChanges, []string{"6"})
|
||||
hTree, err := buildHistoryTree(deps, HistoryTreeParams{
|
||||
BeforeId: "6",
|
||||
IncludeBeforeId: false,
|
||||
@@ -771,49 +558,18 @@ func TestObjectTree(t *testing.T) {
|
||||
assert.Equal(t, "0", hTree.Root().Id)
|
||||
})
|
||||
|
||||
t.Run("test history tree build full", func(t *testing.T) {
|
||||
changeCreator, deps := prepareHistoryTreeDeps(aclList)
|
||||
|
||||
// sequence of snapshots: 5->1->0
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", true, "0"),
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "1", false, "1"),
|
||||
changeCreator.CreateRaw("3", aclList.Head().Id, "1", true, "2"),
|
||||
changeCreator.CreateRaw("4", aclList.Head().Id, "1", false, "2"),
|
||||
changeCreator.CreateRaw("5", aclList.Head().Id, "1", true, "3", "4"),
|
||||
changeCreator.CreateRaw("6", aclList.Head().Id, "5", false, "5"),
|
||||
}
|
||||
deps.treeStorage.AddRawChangesSetHeads(rawChanges, []string{"6"})
|
||||
hTree, err := buildHistoryTree(deps, HistoryTreeParams{
|
||||
BuildFullTree: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// check tree heads
|
||||
assert.Equal(t, []string{"6"}, hTree.Heads())
|
||||
|
||||
// check tree iterate
|
||||
var iterChangesId []string
|
||||
err = hTree.IterateFrom(hTree.Root().Id, nil, func(change *Change) bool {
|
||||
iterChangesId = append(iterChangesId, change.Id)
|
||||
return true
|
||||
})
|
||||
require.NoError(t, err, "iterate should be without error")
|
||||
assert.Equal(t, []string{"0", "1", "2", "3", "4", "5", "6"}, iterChangesId)
|
||||
assert.Equal(t, "0", hTree.Root().Id)
|
||||
})
|
||||
|
||||
t.Run("test history tree include", func(t *testing.T) {
|
||||
changeCreator, deps := prepareHistoryTreeDeps(aclList)
|
||||
changeCreator, deps := prepareTreeDeps(aclList)
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.CreateRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
changeCreator.CreateRaw("4", aclList.Head().Id, "0", false, "2"),
|
||||
changeCreator.CreateRaw("5", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.CreateRaw("6", aclList.Head().Id, "0", false, "3", "4", "5"),
|
||||
changeCreator.createRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.createRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.createRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
changeCreator.createRaw("4", aclList.Head().Id, "0", false, "2"),
|
||||
changeCreator.createRaw("5", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.createRaw("6", aclList.Head().Id, "0", false, "3", "4", "5"),
|
||||
}
|
||||
deps.treeStorage.AddRawChangesSetHeads(rawChanges, []string{"6"})
|
||||
deps.treeStorage.TransactionAdd(rawChanges, []string{"6"})
|
||||
hTree, err := buildHistoryTree(deps, HistoryTreeParams{
|
||||
BeforeId: "6",
|
||||
IncludeBeforeId: true,
|
||||
@@ -834,7 +590,7 @@ func TestObjectTree(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("test history tree root", func(t *testing.T) {
|
||||
_, deps := prepareHistoryTreeDeps(aclList)
|
||||
_, deps := prepareTreeDeps(aclList)
|
||||
hTree, err := buildHistoryTree(deps, HistoryTreeParams{
|
||||
BeforeId: "0",
|
||||
IncludeBeforeId: true,
|
||||
@@ -853,40 +609,4 @@ func TestObjectTree(t *testing.T) {
|
||||
assert.Equal(t, []string{"0"}, iterChangesId)
|
||||
assert.Equal(t, "0", hTree.Root().Id)
|
||||
})
|
||||
|
||||
t.Run("validate correct tree", func(t *testing.T) {
|
||||
ctx := prepareTreeContext(t, aclList)
|
||||
changeCreator := ctx.changeCreator
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
ctx.objTree.Header(),
|
||||
changeCreator.CreateRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||
changeCreator.CreateRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||
changeCreator.CreateRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
}
|
||||
defaultObjectTreeDeps = nonVerifiableTreeDeps
|
||||
err := ValidateRawTree(treestorage.TreeStorageCreatePayload{
|
||||
RootRawChange: ctx.objTree.Header(),
|
||||
Heads: []string{"3"},
|
||||
Changes: rawChanges,
|
||||
}, ctx.aclList)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("fail to validate not connected tree", func(t *testing.T) {
|
||||
ctx := prepareTreeContext(t, aclList)
|
||||
changeCreator := ctx.changeCreator
|
||||
|
||||
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||
ctx.objTree.Header(),
|
||||
changeCreator.CreateRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||
}
|
||||
defaultObjectTreeDeps = nonVerifiableTreeDeps
|
||||
err := ValidateRawTree(treestorage.TreeStorageCreatePayload{
|
||||
RootRawChange: ctx.objTree.Header(),
|
||||
Heads: []string{"3"},
|
||||
Changes: rawChanges,
|
||||
}, ctx.aclList)
|
||||
require.Equal(t, ErrHasInvalidChanges, err)
|
||||
})
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
package objecttree
|
||||
|
||||
import (
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/keychain"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/signingkey"
|
||||
"github.com/anytypeio/any-sync/util/keys/symmetric"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ObjectTreeCreatePayload struct {
|
||||
PrivKey crypto.PrivKey
|
||||
SignKey signingkey.PrivKey
|
||||
ChangeType string
|
||||
ChangePayload []byte
|
||||
SpaceId string
|
||||
Identity []byte
|
||||
IsEncrypted bool
|
||||
Seed []byte
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
type HistoryTreeParams struct {
|
||||
@@ -22,7 +25,6 @@ type HistoryTreeParams struct {
|
||||
AclList list.AclList
|
||||
BeforeId string
|
||||
IncludeBeforeId bool
|
||||
BuildFullTree bool
|
||||
}
|
||||
|
||||
type objectTreeDeps struct {
|
||||
@@ -34,16 +36,14 @@ type objectTreeDeps struct {
|
||||
aclList list.AclList
|
||||
}
|
||||
|
||||
type BuildObjectTreeFunc = func(treeStorage treestorage.TreeStorage, aclList list.AclList) (ObjectTree, error)
|
||||
|
||||
var defaultObjectTreeDeps = verifiableTreeDeps
|
||||
|
||||
func verifiableTreeDeps(
|
||||
func defaultObjectTreeDeps(
|
||||
rootChange *treechangeproto.RawTreeChangeWithId,
|
||||
treeStorage treestorage.TreeStorage,
|
||||
aclList list.AclList) objectTreeDeps {
|
||||
changeBuilder := NewChangeBuilder(crypto.NewKeyStorage(), rootChange)
|
||||
treeBuilder := newTreeBuilder(true, treeStorage, changeBuilder)
|
||||
|
||||
keychain := keychain.NewKeychain()
|
||||
changeBuilder := NewChangeBuilder(keychain, rootChange)
|
||||
treeBuilder := newTreeBuilder(treeStorage, changeBuilder)
|
||||
return objectTreeDeps{
|
||||
changeBuilder: changeBuilder,
|
||||
treeBuilder: treeBuilder,
|
||||
@@ -54,28 +54,12 @@ func verifiableTreeDeps(
|
||||
}
|
||||
}
|
||||
|
||||
func emptyDataTreeDeps(
|
||||
rootChange *treechangeproto.RawTreeChangeWithId,
|
||||
treeStorage treestorage.TreeStorage,
|
||||
aclList list.AclList) objectTreeDeps {
|
||||
changeBuilder := NewChangeBuilder(crypto.NewKeyStorage(), rootChange)
|
||||
treeBuilder := newTreeBuilder(false, treeStorage, changeBuilder)
|
||||
return objectTreeDeps{
|
||||
changeBuilder: changeBuilder,
|
||||
treeBuilder: treeBuilder,
|
||||
treeStorage: treeStorage,
|
||||
validator: newTreeValidator(),
|
||||
rawChangeLoader: newStorageLoader(treeStorage, changeBuilder),
|
||||
aclList: aclList,
|
||||
}
|
||||
}
|
||||
|
||||
func nonVerifiableTreeDeps(
|
||||
rootChange *treechangeproto.RawTreeChangeWithId,
|
||||
treeStorage treestorage.TreeStorage,
|
||||
aclList list.AclList) objectTreeDeps {
|
||||
changeBuilder := &nonVerifiableChangeBuilder{NewChangeBuilder(newMockKeyStorage(), rootChange)}
|
||||
treeBuilder := newTreeBuilder(true, treeStorage, changeBuilder)
|
||||
changeBuilder := &nonVerifiableChangeBuilder{NewChangeBuilder(nil, rootChange)}
|
||||
treeBuilder := newTreeBuilder(treeStorage, changeBuilder)
|
||||
return objectTreeDeps{
|
||||
changeBuilder: changeBuilder,
|
||||
treeBuilder: treeBuilder,
|
||||
@@ -86,47 +70,17 @@ func nonVerifiableTreeDeps(
|
||||
}
|
||||
}
|
||||
|
||||
func BuildEmptyDataObjectTree(treeStorage treestorage.TreeStorage, aclList list.AclList) (ObjectTree, error) {
|
||||
rootChange, err := treeStorage.Root()
|
||||
func CreateObjectTreeRoot(payload ObjectTreeCreatePayload, aclList list.AclList) (root *treechangeproto.RawTreeChangeWithId, err error) {
|
||||
bytes := make([]byte, 32)
|
||||
_, err = rand.Read(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return
|
||||
}
|
||||
deps := emptyDataTreeDeps(rootChange, treeStorage, aclList)
|
||||
return buildObjectTree(deps)
|
||||
return createObjectTreeRoot(payload, time.Now().Unix(), bytes, aclList)
|
||||
}
|
||||
|
||||
func BuildTestableTree(treeStorage treestorage.TreeStorage, aclList list.AclList) (ObjectTree, error) {
|
||||
root, _ := treeStorage.Root()
|
||||
changeBuilder := &nonVerifiableChangeBuilder{
|
||||
ChangeBuilder: NewChangeBuilder(newMockKeyStorage(), root),
|
||||
}
|
||||
deps := objectTreeDeps{
|
||||
changeBuilder: changeBuilder,
|
||||
treeBuilder: newTreeBuilder(true, treeStorage, changeBuilder),
|
||||
treeStorage: treeStorage,
|
||||
rawChangeLoader: newRawChangeLoader(treeStorage, changeBuilder),
|
||||
validator: &noOpTreeValidator{},
|
||||
aclList: aclList,
|
||||
}
|
||||
|
||||
return buildObjectTree(deps)
|
||||
}
|
||||
|
||||
func BuildEmptyDataTestableTree(treeStorage treestorage.TreeStorage, aclList list.AclList) (ObjectTree, error) {
|
||||
root, _ := treeStorage.Root()
|
||||
changeBuilder := &nonVerifiableChangeBuilder{
|
||||
ChangeBuilder: NewChangeBuilder(newMockKeyStorage(), root),
|
||||
}
|
||||
deps := objectTreeDeps{
|
||||
changeBuilder: changeBuilder,
|
||||
treeBuilder: newTreeBuilder(false, treeStorage, changeBuilder),
|
||||
treeStorage: treeStorage,
|
||||
rawChangeLoader: newStorageLoader(treeStorage, changeBuilder),
|
||||
validator: &noOpTreeValidator{},
|
||||
aclList: aclList,
|
||||
}
|
||||
|
||||
return buildObjectTree(deps)
|
||||
func DeriveObjectTreeRoot(payload ObjectTreeCreatePayload, aclList list.AclList) (root *treechangeproto.RawTreeChangeWithId, err error) {
|
||||
return createObjectTreeRoot(payload, 0, nil, aclList)
|
||||
}
|
||||
|
||||
func BuildObjectTree(treeStorage treestorage.TreeStorage, aclList list.AclList) (ObjectTree, error) {
|
||||
@@ -156,7 +110,54 @@ func BuildHistoryTree(params HistoryTreeParams) (HistoryTree, error) {
|
||||
return buildHistoryTree(deps, params)
|
||||
}
|
||||
|
||||
func CreateObjectTreeRoot(payload ObjectTreeCreatePayload, aclList list.AclList) (root *treechangeproto.RawTreeChangeWithId, err error) {
|
||||
func CreateDerivedObjectTree(
|
||||
payload ObjectTreeCreatePayload,
|
||||
aclList list.AclList,
|
||||
createStorage treestorage.TreeStorageCreatorFunc) (objTree ObjectTree, err error) {
|
||||
return createObjectTree(payload, 0, nil, aclList, createStorage)
|
||||
}
|
||||
|
||||
func CreateObjectTree(
|
||||
payload ObjectTreeCreatePayload,
|
||||
aclList list.AclList,
|
||||
createStorage treestorage.TreeStorageCreatorFunc) (objTree ObjectTree, err error) {
|
||||
bytes := make([]byte, 32)
|
||||
_, err = rand.Read(bytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return createObjectTree(payload, time.Now().Unix(), bytes, aclList, createStorage)
|
||||
}
|
||||
|
||||
func createObjectTree(
|
||||
payload ObjectTreeCreatePayload,
|
||||
timestamp int64,
|
||||
seed []byte,
|
||||
aclList list.AclList,
|
||||
createStorage treestorage.TreeStorageCreatorFunc) (objTree ObjectTree, err error) {
|
||||
raw, err := createObjectTreeRoot(payload, timestamp, seed, aclList)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// create storage
|
||||
st, err := createStorage(treestorage.TreeStorageCreatePayload{
|
||||
RootRawChange: raw,
|
||||
Changes: []*treechangeproto.RawTreeChangeWithId{raw},
|
||||
Heads: []string{raw.Id},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return BuildObjectTree(st, aclList)
|
||||
}
|
||||
|
||||
func createObjectTreeRoot(
|
||||
payload ObjectTreeCreatePayload,
|
||||
timestamp int64,
|
||||
seed []byte,
|
||||
aclList list.AclList) (root *treechangeproto.RawTreeChangeWithId, err error) {
|
||||
aclList.RLock()
|
||||
aclHeadId := aclList.Head().Id
|
||||
aclList.RUnlock()
|
||||
@@ -166,15 +167,16 @@ func CreateObjectTreeRoot(payload ObjectTreeCreatePayload, aclList list.AclList)
|
||||
}
|
||||
cnt := InitialContent{
|
||||
AclHeadId: aclHeadId,
|
||||
PrivKey: payload.PrivKey,
|
||||
Identity: payload.Identity,
|
||||
SigningKey: payload.SignKey,
|
||||
SpaceId: payload.SpaceId,
|
||||
ChangeType: payload.ChangeType,
|
||||
ChangePayload: payload.ChangePayload,
|
||||
Timestamp: payload.Timestamp,
|
||||
Seed: payload.Seed,
|
||||
Timestamp: timestamp,
|
||||
Seed: seed,
|
||||
}
|
||||
|
||||
_, root, err = NewChangeBuilder(crypto.NewKeyStorage(), nil).BuildRoot(cnt)
|
||||
_, root, err = NewChangeBuilder(keychain.NewKeychain(), nil).BuildRoot(cnt)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -187,7 +189,7 @@ func buildObjectTree(deps objectTreeDeps) (ObjectTree, error) {
|
||||
aclList: deps.aclList,
|
||||
changeBuilder: deps.changeBuilder,
|
||||
rawChangeLoader: deps.rawChangeLoader,
|
||||
keys: make(map[string]crypto.SymKey),
|
||||
keys: make(map[uint64]*symmetric.Key),
|
||||
newChangesBuf: make([]*Change, 0, 10),
|
||||
difSnapshotBuf: make([]*treechangeproto.RawTreeChangeWithId, 0, 10),
|
||||
notSeenIdxBuf: make([]int, 0, 10),
|
||||
@@ -223,7 +225,7 @@ func buildHistoryTree(deps objectTreeDeps, params HistoryTreeParams) (ht History
|
||||
aclList: deps.aclList,
|
||||
changeBuilder: deps.changeBuilder,
|
||||
rawChangeLoader: deps.rawChangeLoader,
|
||||
keys: make(map[string]crypto.SymKey),
|
||||
keys: make(map[uint64]*symmetric.Key),
|
||||
newChangesBuf: make([]*Change, 0, 10),
|
||||
difSnapshotBuf: make([]*treechangeproto.RawTreeChangeWithId, 0, 10),
|
||||
notSeenIdxBuf: make([]int, 0, 10),
|
||||
@@ -231,7 +233,7 @@ func buildHistoryTree(deps objectTreeDeps, params HistoryTreeParams) (ht History
|
||||
}
|
||||
|
||||
hTree := &historyTree{objectTree: objTree}
|
||||
err = hTree.rebuildFromStorage(params)
|
||||
err = hTree.rebuildFromStorage(params.BeforeId, params.IncludeBeforeId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package objecttree
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anyproto/any-sync/util/slice"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||
)
|
||||
|
||||
type ObjectTreeValidator interface {
|
||||
@@ -52,18 +50,20 @@ func (v *objectTreeValidator) ValidateNewChanges(tree *Tree, aclList list.AclLis
|
||||
|
||||
func (v *objectTreeValidator) validateChange(tree *Tree, aclList list.AclList, c *Change) (err error) {
|
||||
var (
|
||||
userState list.AclUserState
|
||||
state = aclList.AclState()
|
||||
perm list.UserPermissionPair
|
||||
state = aclList.AclState()
|
||||
)
|
||||
// checking if the user could write
|
||||
userState, err = state.StateAtRecord(c.AclHeadId, c.Identity)
|
||||
perm, err = state.PermissionsAtRecord(c.AclHeadId, c.Identity)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !userState.Permissions.CanWrite() {
|
||||
|
||||
if perm.Permission != aclrecordproto.AclUserPermissions_Writer && perm.Permission != aclrecordproto.AclUserPermissions_Admin {
|
||||
err = list.ErrInsufficientPermissions
|
||||
return
|
||||
}
|
||||
|
||||
if c.Id == tree.RootId() {
|
||||
return
|
||||
}
|
||||
@@ -88,23 +88,11 @@ func (v *objectTreeValidator) validateChange(tree *Tree, aclList list.AclList, c
|
||||
}
|
||||
|
||||
func ValidateRawTree(payload treestorage.TreeStorageCreatePayload, aclList list.AclList) (err error) {
|
||||
treeStorage, err := treestorage.NewInMemoryTreeStorage(payload.RootRawChange, []string{payload.RootRawChange.Id}, nil)
|
||||
treeStorage, err := treestorage.NewInMemoryTreeStorage(payload.RootRawChange, payload.Heads, payload.Changes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tree, err := BuildObjectTree(treeStorage, aclList)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res, err := tree.AddRawChanges(context.Background(), RawChangesPayload{
|
||||
NewHeads: payload.Heads,
|
||||
RawChanges: payload.Changes,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !slice.UnsortedEquals(res.Heads, payload.Heads) {
|
||||
return ErrHasInvalidChanges
|
||||
}
|
||||
|
||||
_, err = BuildObjectTree(treeStorage, aclList)
|
||||
return
|
||||
}
|
||||
@@ -2,17 +2,15 @@ package objecttree
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anytypeio/any-sync/util/slice"
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anyproto/any-sync/util/slice"
|
||||
)
|
||||
|
||||
type rawChangeLoader struct {
|
||||
treeStorage treestorage.TreeStorage
|
||||
changeBuilder ChangeBuilder
|
||||
alwaysFromStorage bool
|
||||
treeStorage treestorage.TreeStorage
|
||||
changeBuilder ChangeBuilder
|
||||
|
||||
// buffers
|
||||
idStack []string
|
||||
@@ -23,13 +21,6 @@ type rawCacheEntry struct {
|
||||
change *Change
|
||||
rawChange *treechangeproto.RawTreeChangeWithId
|
||||
position int
|
||||
removed bool
|
||||
}
|
||||
|
||||
func newStorageLoader(treeStorage treestorage.TreeStorage, changeBuilder ChangeBuilder) *rawChangeLoader {
|
||||
loader := newRawChangeLoader(treeStorage, changeBuilder)
|
||||
loader.alwaysFromStorage = true
|
||||
return loader
|
||||
}
|
||||
|
||||
func newRawChangeLoader(treeStorage treestorage.TreeStorage, changeBuilder ChangeBuilder) *rawChangeLoader {
|
||||
@@ -39,15 +30,7 @@ func newRawChangeLoader(treeStorage treestorage.TreeStorage, changeBuilder Chang
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rawChangeLoader) Load(commonSnapshot string, t *Tree, breakpoints []string) ([]*treechangeproto.RawTreeChangeWithId, error) {
|
||||
if commonSnapshot == t.root.Id && !r.alwaysFromStorage {
|
||||
return r.loadFromTree(t, breakpoints)
|
||||
} else {
|
||||
return r.loadFromStorage(commonSnapshot, t.Heads(), breakpoints)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rawChangeLoader) loadFromTree(t *Tree, breakpoints []string) ([]*treechangeproto.RawTreeChangeWithId, error) {
|
||||
func (r *rawChangeLoader) LoadFromTree(t *Tree, breakpoints []string) ([]*treechangeproto.RawTreeChangeWithId, error) {
|
||||
var stack []*Change
|
||||
for _, h := range t.headIds {
|
||||
stack = append(stack, t.attached[h])
|
||||
@@ -115,7 +98,7 @@ func (r *rawChangeLoader) loadFromTree(t *Tree, breakpoints []string) ([]*treech
|
||||
return convert(results)
|
||||
}
|
||||
|
||||
func (r *rawChangeLoader) loadFromStorage(commonSnapshot string, heads, breakpoints []string) ([]*treechangeproto.RawTreeChangeWithId, error) {
|
||||
func (r *rawChangeLoader) LoadFromStorage(commonSnapshot string, heads, breakpoints []string) ([]*treechangeproto.RawTreeChangeWithId, error) {
|
||||
// resetting cache
|
||||
r.cache = make(map[string]rawCacheEntry)
|
||||
defer func() {
|
||||
@@ -128,6 +111,7 @@ func (r *rawChangeLoader) loadFromStorage(commonSnapshot string, heads, breakpoi
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
entry.position = -1
|
||||
r.cache[b] = entry
|
||||
existingBreakpoints = append(existingBreakpoints, b)
|
||||
}
|
||||
@@ -136,7 +120,8 @@ func (r *rawChangeLoader) loadFromStorage(commonSnapshot string, heads, breakpoi
|
||||
dfs := func(
|
||||
commonSnapshot string,
|
||||
heads []string,
|
||||
shouldVisit func(entry rawCacheEntry, mapExists bool) bool,
|
||||
startCounter int,
|
||||
shouldVisit func(counter int, mapExists bool) bool,
|
||||
visit func(entry rawCacheEntry) rawCacheEntry) bool {
|
||||
|
||||
// resetting stack
|
||||
@@ -150,7 +135,7 @@ func (r *rawChangeLoader) loadFromStorage(commonSnapshot string, heads, breakpoi
|
||||
r.idStack = r.idStack[:len(r.idStack)-1]
|
||||
|
||||
entry, exists := r.cache[id]
|
||||
if !shouldVisit(entry, exists) {
|
||||
if !shouldVisit(entry.position, exists) {
|
||||
continue
|
||||
}
|
||||
if id == commonSnapshot {
|
||||
@@ -159,6 +144,7 @@ func (r *rawChangeLoader) loadFromStorage(commonSnapshot string, heads, breakpoi
|
||||
}
|
||||
if !exists {
|
||||
entry, err = r.loadEntry(id)
|
||||
entry.position = -1
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -173,7 +159,7 @@ func (r *rawChangeLoader) loadFromStorage(commonSnapshot string, heads, breakpoi
|
||||
break
|
||||
}
|
||||
prevEntry, exists := r.cache[prev]
|
||||
if !shouldVisit(prevEntry, exists) {
|
||||
if !shouldVisit(prevEntry.position, exists) {
|
||||
continue
|
||||
}
|
||||
r.idStack = append(r.idStack, prev)
|
||||
@@ -186,8 +172,8 @@ func (r *rawChangeLoader) loadFromStorage(commonSnapshot string, heads, breakpoi
|
||||
r.idStack = append(r.idStack, heads...)
|
||||
var buffer []*treechangeproto.RawTreeChangeWithId
|
||||
|
||||
rootVisited := dfs(commonSnapshot, heads,
|
||||
func(_ rawCacheEntry, mapExists bool) bool {
|
||||
rootVisited := dfs(commonSnapshot, heads, 0,
|
||||
func(counter int, mapExists bool) bool {
|
||||
return !mapExists
|
||||
},
|
||||
func(entry rawCacheEntry) rawCacheEntry {
|
||||
@@ -212,13 +198,11 @@ func (r *rawChangeLoader) loadFromStorage(commonSnapshot string, heads, breakpoi
|
||||
}
|
||||
|
||||
// marking all visited as nil
|
||||
dfs(commonSnapshot, existingBreakpoints,
|
||||
func(entry rawCacheEntry, mapExists bool) bool {
|
||||
// only going through already loaded changes
|
||||
return mapExists && !entry.removed
|
||||
dfs(commonSnapshot, existingBreakpoints, len(buffer),
|
||||
func(counter int, mapExists bool) bool {
|
||||
return !mapExists || counter < len(buffer)
|
||||
},
|
||||
func(entry rawCacheEntry) rawCacheEntry {
|
||||
entry.removed = true
|
||||
if entry.position != -1 {
|
||||
buffer[entry.position] = nil
|
||||
}
|
||||
@@ -249,7 +233,6 @@ func (r *rawChangeLoader) loadEntry(id string) (entry rawCacheEntry, err error)
|
||||
entry = rawCacheEntry{
|
||||
change: change,
|
||||
rawChange: rawChange,
|
||||
position: -1,
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,19 +1,13 @@
|
||||
package objecttree
|
||||
|
||||
import (
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
"github.com/anytypeio/any-sync/util/keys/asymmetric/signingkey"
|
||||
)
|
||||
|
||||
// SignableChangeContent is a payload to be passed when we are creating change
|
||||
type SignableChangeContent struct {
|
||||
// Data is a data provided by the client
|
||||
Data []byte
|
||||
// Key is the key which will be used to sign the change
|
||||
Key crypto.PrivKey
|
||||
// IsSnapshot tells if the change has snapshot of all previous data
|
||||
IsSnapshot bool
|
||||
// IsEncrypted tells if we encrypt the data with the relevant symmetric key
|
||||
Data []byte
|
||||
Key signingkey.PrivKey
|
||||
Identity []byte
|
||||
IsSnapshot bool
|
||||
IsEncrypted bool
|
||||
// Timestamp is a timestamp of change, if it is <= 0, then we use current timestamp
|
||||
Timestamp int64
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package objecttree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
libcrypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||
)
|
||||
|
||||
type mockPubKey struct {
|
||||
}
|
||||
|
||||
const mockKeyValue = "mockKey"
|
||||
|
||||
func (m mockPubKey) Equals(key crypto.Key) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (m mockPubKey) Raw() ([]byte, error) {
|
||||
return []byte(mockKeyValue), nil
|
||||
}
|
||||
|
||||
func (m mockPubKey) Encrypt(message []byte) ([]byte, error) {
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func (m mockPubKey) Verify(data []byte, sig []byte) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m mockPubKey) Marshall() ([]byte, error) {
|
||||
return []byte(mockKeyValue), nil
|
||||
}
|
||||
|
||||
func (m mockPubKey) Storage() []byte {
|
||||
return []byte(mockKeyValue)
|
||||
}
|
||||
|
||||
func (m mockPubKey) Account() string {
|
||||
return mockKeyValue
|
||||
}
|
||||
|
||||
func (m mockPubKey) Network() string {
|
||||
return mockKeyValue
|
||||
}
|
||||
|
||||
func (m mockPubKey) PeerId() string {
|
||||
return mockKeyValue
|
||||
}
|
||||
|
||||
func (m mockPubKey) LibP2P() (libcrypto.PubKey, error) {
|
||||
return nil, fmt.Errorf("can't be converted in libp2p")
|
||||
}
|
||||
|
||||
type mockKeyStorage struct {
|
||||
}
|
||||
|
||||
func newMockKeyStorage() mockKeyStorage {
|
||||
return mockKeyStorage{}
|
||||
}
|
||||
|
||||
func (m mockKeyStorage) PubKeyFromProto(protoBytes []byte) (crypto.PubKey, error) {
|
||||
return mockPubKey{}, nil
|
||||
}
|
||||
|
||||
type MockChangeCreator struct{}
|
||||
|
||||
func NewMockChangeCreator() *MockChangeCreator {
|
||||
return &MockChangeCreator{}
|
||||
}
|
||||
|
||||
func (c *MockChangeCreator) CreateRoot(id, aclId string) *treechangeproto.RawTreeChangeWithId {
|
||||
aclChange := &treechangeproto.RootChange{
|
||||
AclHeadId: aclId,
|
||||
}
|
||||
res, _ := aclChange.Marshal()
|
||||
|
||||
raw := &treechangeproto.RawTreeChange{
|
||||
Payload: res,
|
||||
Signature: nil,
|
||||
}
|
||||
rawMarshalled, _ := raw.Marshal()
|
||||
|
||||
return &treechangeproto.RawTreeChangeWithId{
|
||||
RawChange: rawMarshalled,
|
||||
Id: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MockChangeCreator) CreateRaw(id, aclId, snapshotId string, isSnapshot bool, prevIds ...string) *treechangeproto.RawTreeChangeWithId {
|
||||
return c.CreateRawWithData(id, aclId, snapshotId, isSnapshot, nil, prevIds...)
|
||||
}
|
||||
|
||||
func (c *MockChangeCreator) CreateRawWithData(id, aclId, snapshotId string, isSnapshot bool, data []byte, prevIds ...string) *treechangeproto.RawTreeChangeWithId {
|
||||
aclChange := &treechangeproto.TreeChange{
|
||||
TreeHeadIds: prevIds,
|
||||
AclHeadId: aclId,
|
||||
SnapshotBaseId: snapshotId,
|
||||
ChangesData: data,
|
||||
IsSnapshot: isSnapshot,
|
||||
}
|
||||
res, _ := aclChange.Marshal()
|
||||
|
||||
raw := &treechangeproto.RawTreeChange{
|
||||
Payload: res,
|
||||
Signature: nil,
|
||||
}
|
||||
rawMarshalled, _ := raw.Marshal()
|
||||
|
||||
return &treechangeproto.RawTreeChangeWithId{
|
||||
RawChange: rawMarshalled,
|
||||
Id: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MockChangeCreator) CreateNewTreeStorage(treeId, aclHeadId string) treestorage.TreeStorage {
|
||||
root := c.CreateRoot(treeId, aclHeadId)
|
||||
treeStorage, _ := treestorage.NewInMemoryTreeStorage(root, []string{root.Id}, []*treechangeproto.RawTreeChangeWithId{root})
|
||||
return treeStorage
|
||||
}
|
||||
@@ -82,7 +82,6 @@ func (t *Tree) AddMergedHead(c *Change) error {
|
||||
}
|
||||
}
|
||||
t.headIds = []string{c.Id}
|
||||
t.lastIteratedHeadId = c.Id
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@ package objecttree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newChange(id string, snapshotId string, prevIds ...string) *Change {
|
||||
@@ -28,17 +26,6 @@ func newSnapshot(id, snapshotId string, prevIds ...string) *Change {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTree_AddMergedHead(t *testing.T) {
|
||||
tr := new(Tree)
|
||||
_, _ = tr.Add(
|
||||
newSnapshot("root", ""),
|
||||
newChange("one", "root", "root"),
|
||||
)
|
||||
require.Equal(t, tr.lastIteratedHeadId, "one")
|
||||
tr.AddMergedHead(newChange("two", "root", "one"))
|
||||
require.Equal(t, tr.lastIteratedHeadId, "two")
|
||||
}
|
||||
|
||||
func TestTree_Add(t *testing.T) {
|
||||
t.Run("add first el", func(t *testing.T) {
|
||||
tr := new(Tree)
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/app/logger"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anyproto/any-sync/util/slice"
|
||||
"github.com/anytypeio/any-sync/app/logger"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anytypeio/any-sync/util/slice"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -21,20 +21,18 @@ type treeBuilder struct {
|
||||
treeStorage treestorage.TreeStorage
|
||||
builder ChangeBuilder
|
||||
|
||||
cache map[string]*Change
|
||||
tree *Tree
|
||||
keepInMemoryData bool
|
||||
cache map[string]*Change
|
||||
tree *Tree
|
||||
|
||||
// buffers
|
||||
idStack []string
|
||||
loadBuffer []*Change
|
||||
}
|
||||
|
||||
func newTreeBuilder(keepData bool, storage treestorage.TreeStorage, builder ChangeBuilder) *treeBuilder {
|
||||
func newTreeBuilder(storage treestorage.TreeStorage, builder ChangeBuilder) *treeBuilder {
|
||||
return &treeBuilder{
|
||||
treeStorage: storage,
|
||||
builder: builder,
|
||||
keepInMemoryData: keepData,
|
||||
treeStorage: storage,
|
||||
builder: builder,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,27 +49,7 @@ func (tb *treeBuilder) Build(theirHeads []string, newChanges []*Change) (*Tree,
|
||||
return tb.build(heads, theirHeads, newChanges)
|
||||
}
|
||||
|
||||
func (tb *treeBuilder) BuildFull() (*Tree, error) {
|
||||
defer func() {
|
||||
tb.cache = make(map[string]*Change)
|
||||
}()
|
||||
tb.cache = make(map[string]*Change)
|
||||
heads, err := tb.treeStorage.Heads()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = tb.buildTree(heads, tb.treeStorage.Id())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tb.tree, nil
|
||||
}
|
||||
|
||||
func (tb *treeBuilder) build(heads []string, theirHeads []string, newChanges []*Change) (*Tree, error) {
|
||||
defer func() {
|
||||
tb.cache = make(map[string]*Change)
|
||||
}()
|
||||
|
||||
var proposedHeads []string
|
||||
tb.cache = make(map[string]*Change)
|
||||
|
||||
@@ -121,8 +99,8 @@ func (tb *treeBuilder) buildTree(heads []string, breakpoint string) (err error)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
changes := tb.dfs(heads, breakpoint)
|
||||
tb.tree.AddFast(ch)
|
||||
changes := tb.dfs(heads, breakpoint)
|
||||
tb.tree.AddFast(changes...)
|
||||
return
|
||||
}
|
||||
@@ -181,9 +159,6 @@ func (tb *treeBuilder) loadChange(id string) (ch *Change, err error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !tb.keepInMemoryData {
|
||||
ch.Data = nil
|
||||
}
|
||||
|
||||
tb.cache[id] = ch
|
||||
return ch, nil
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
package objecttree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anyproto/any-sync/util/crypto"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
func commonSnapshotForTwoPaths(ourPath []string, theirPath []string) (string, error) {
|
||||
var i int
|
||||
var j int
|
||||
@@ -33,24 +27,3 @@ OuterLoop:
|
||||
}
|
||||
return ourPath[i+1], nil
|
||||
}
|
||||
|
||||
func deriveTreeKey(key crypto.SymKey, cid string) (crypto.SymKey, error) {
|
||||
raw, err := key.Raw()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crypto.DeriveSymmetricKey(raw, fmt.Sprintf(crypto.AnysyncTreePath, cid))
|
||||
}
|
||||
|
||||
func DoSnapshot(treeLen int) bool {
|
||||
if treeLen <= 100 {
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
delta = treeLen/50 + 1
|
||||
midPoint = 1000
|
||||
val = rand.Intn(midPoint * 2)
|
||||
)
|
||||
return midPoint-delta <= val && val <= midPoint+delta
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/anyproto/any-sync/commonspace/object/tree/synctree (interfaces: SyncTree,ReceiveQueue,HeadNotifiable,SyncClient,RequestFactory,TreeSyncProtocol)
|
||||
// Source: github.com/anytypeio/any-sync/commonspace/object/tree/synctree (interfaces: SyncClient,SyncTree,ReceiveQueue,HeadNotifiable)
|
||||
|
||||
// Package mock_synctree is a generated GoMock package.
|
||||
package mock_synctree
|
||||
@@ -9,15 +9,124 @@ import (
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
list "github.com/anyproto/any-sync/commonspace/object/acl/list"
|
||||
objecttree "github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
|
||||
updatelistener "github.com/anyproto/any-sync/commonspace/object/tree/synctree/updatelistener"
|
||||
treechangeproto "github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
treestorage "github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
spacesyncproto "github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
list "github.com/anytypeio/any-sync/commonspace/object/acl/list"
|
||||
objecttree "github.com/anytypeio/any-sync/commonspace/object/tree/objecttree"
|
||||
updatelistener "github.com/anytypeio/any-sync/commonspace/object/tree/synctree/updatelistener"
|
||||
treechangeproto "github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
treestorage "github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||
spacesyncproto "github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
)
|
||||
|
||||
// MockSyncClient is a mock of SyncClient interface.
|
||||
type MockSyncClient struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockSyncClientMockRecorder
|
||||
}
|
||||
|
||||
// MockSyncClientMockRecorder is the mock recorder for MockSyncClient.
|
||||
type MockSyncClientMockRecorder struct {
|
||||
mock *MockSyncClient
|
||||
}
|
||||
|
||||
// NewMockSyncClient creates a new mock instance.
|
||||
func NewMockSyncClient(ctrl *gomock.Controller) *MockSyncClient {
|
||||
mock := &MockSyncClient{ctrl: ctrl}
|
||||
mock.recorder = &MockSyncClientMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockSyncClient) EXPECT() *MockSyncClientMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Broadcast mocks base method.
|
||||
func (m *MockSyncClient) Broadcast(arg0 context.Context, arg1 *treechangeproto.TreeSyncMessage) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Broadcast", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Broadcast indicates an expected call of Broadcast.
|
||||
func (mr *MockSyncClientMockRecorder) Broadcast(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Broadcast", reflect.TypeOf((*MockSyncClient)(nil).Broadcast), arg0, arg1)
|
||||
}
|
||||
|
||||
// CreateFullSyncRequest mocks base method.
|
||||
func (m *MockSyncClient) CreateFullSyncRequest(arg0 objecttree.ObjectTree, arg1, arg2 []string) (*treechangeproto.TreeSyncMessage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateFullSyncRequest", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CreateFullSyncRequest indicates an expected call of CreateFullSyncRequest.
|
||||
func (mr *MockSyncClientMockRecorder) CreateFullSyncRequest(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFullSyncRequest", reflect.TypeOf((*MockSyncClient)(nil).CreateFullSyncRequest), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// CreateFullSyncResponse mocks base method.
|
||||
func (m *MockSyncClient) CreateFullSyncResponse(arg0 objecttree.ObjectTree, arg1, arg2 []string) (*treechangeproto.TreeSyncMessage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateFullSyncResponse", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CreateFullSyncResponse indicates an expected call of CreateFullSyncResponse.
|
||||
func (mr *MockSyncClientMockRecorder) CreateFullSyncResponse(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFullSyncResponse", reflect.TypeOf((*MockSyncClient)(nil).CreateFullSyncResponse), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// CreateHeadUpdate mocks base method.
|
||||
func (m *MockSyncClient) CreateHeadUpdate(arg0 objecttree.ObjectTree, arg1 []*treechangeproto.RawTreeChangeWithId) *treechangeproto.TreeSyncMessage {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateHeadUpdate", arg0, arg1)
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CreateHeadUpdate indicates an expected call of CreateHeadUpdate.
|
||||
func (mr *MockSyncClientMockRecorder) CreateHeadUpdate(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateHeadUpdate", reflect.TypeOf((*MockSyncClient)(nil).CreateHeadUpdate), arg0, arg1)
|
||||
}
|
||||
|
||||
// CreateNewTreeRequest mocks base method.
|
||||
func (m *MockSyncClient) CreateNewTreeRequest() *treechangeproto.TreeSyncMessage {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateNewTreeRequest")
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CreateNewTreeRequest indicates an expected call of CreateNewTreeRequest.
|
||||
func (mr *MockSyncClientMockRecorder) CreateNewTreeRequest() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNewTreeRequest", reflect.TypeOf((*MockSyncClient)(nil).CreateNewTreeRequest))
|
||||
}
|
||||
|
||||
// SendWithReply mocks base method.
|
||||
func (m *MockSyncClient) SendWithReply(arg0 context.Context, arg1 string, arg2 *treechangeproto.TreeSyncMessage, arg3 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SendWithReply", arg0, arg1, arg2, arg3)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SendWithReply indicates an expected call of SendWithReply.
|
||||
func (mr *MockSyncClientMockRecorder) SendWithReply(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendWithReply", reflect.TypeOf((*MockSyncClient)(nil).SendWithReply), arg0, arg1, arg2, arg3)
|
||||
}
|
||||
|
||||
// MockSyncTree is a mock of SyncTree interface.
|
||||
type MockSyncTree struct {
|
||||
ctrl *gomock.Controller
|
||||
@@ -186,21 +295,6 @@ func (mr *MockSyncTreeMockRecorder) HandleMessage(arg0, arg1, arg2 interface{})
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleMessage", reflect.TypeOf((*MockSyncTree)(nil).HandleMessage), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// HandleRequest mocks base method.
|
||||
func (m *MockSyncTree) HandleRequest(arg0 context.Context, arg1 string, arg2 *spacesyncproto.ObjectSyncMessage) (*spacesyncproto.ObjectSyncMessage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HandleRequest", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*spacesyncproto.ObjectSyncMessage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// HandleRequest indicates an expected call of HandleRequest.
|
||||
func (mr *MockSyncTreeMockRecorder) HandleRequest(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleRequest", reflect.TypeOf((*MockSyncTree)(nil).HandleRequest), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// HasChanges mocks base method.
|
||||
func (m *MockSyncTree) HasChanges(arg0 ...string) bool {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -289,20 +383,6 @@ func (mr *MockSyncTreeMockRecorder) IterateRoot(arg0, arg1 interface{}) *gomock.
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateRoot", reflect.TypeOf((*MockSyncTree)(nil).IterateRoot), arg0, arg1)
|
||||
}
|
||||
|
||||
// Len mocks base method.
|
||||
func (m *MockSyncTree) Len() int {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Len")
|
||||
ret0, _ := ret[0].(int)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Len indicates an expected call of Len.
|
||||
func (mr *MockSyncTreeMockRecorder) Len() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Len", reflect.TypeOf((*MockSyncTree)(nil).Len))
|
||||
}
|
||||
|
||||
// Lock mocks base method.
|
||||
func (m *MockSyncTree) Lock() {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -605,287 +685,3 @@ func (mr *MockHeadNotifiableMockRecorder) UpdateHeads(arg0, arg1 interface{}) *g
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateHeads", reflect.TypeOf((*MockHeadNotifiable)(nil).UpdateHeads), arg0, arg1)
|
||||
}
|
||||
|
||||
// MockSyncClient is a mock of SyncClient interface.
|
||||
type MockSyncClient struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockSyncClientMockRecorder
|
||||
}
|
||||
|
||||
// MockSyncClientMockRecorder is the mock recorder for MockSyncClient.
|
||||
type MockSyncClientMockRecorder struct {
|
||||
mock *MockSyncClient
|
||||
}
|
||||
|
||||
// NewMockSyncClient creates a new mock instance.
|
||||
func NewMockSyncClient(ctrl *gomock.Controller) *MockSyncClient {
|
||||
mock := &MockSyncClient{ctrl: ctrl}
|
||||
mock.recorder = &MockSyncClientMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockSyncClient) EXPECT() *MockSyncClientMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Broadcast mocks base method.
|
||||
func (m *MockSyncClient) Broadcast(arg0 *treechangeproto.TreeSyncMessage) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Broadcast", arg0)
|
||||
}
|
||||
|
||||
// Broadcast indicates an expected call of Broadcast.
|
||||
func (mr *MockSyncClientMockRecorder) Broadcast(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Broadcast", reflect.TypeOf((*MockSyncClient)(nil).Broadcast), arg0)
|
||||
}
|
||||
|
||||
// CreateFullSyncRequest mocks base method.
|
||||
func (m *MockSyncClient) CreateFullSyncRequest(arg0 objecttree.ObjectTree, arg1, arg2 []string) (*treechangeproto.TreeSyncMessage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateFullSyncRequest", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CreateFullSyncRequest indicates an expected call of CreateFullSyncRequest.
|
||||
func (mr *MockSyncClientMockRecorder) CreateFullSyncRequest(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFullSyncRequest", reflect.TypeOf((*MockSyncClient)(nil).CreateFullSyncRequest), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// CreateFullSyncResponse mocks base method.
|
||||
func (m *MockSyncClient) CreateFullSyncResponse(arg0 objecttree.ObjectTree, arg1, arg2 []string) (*treechangeproto.TreeSyncMessage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateFullSyncResponse", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CreateFullSyncResponse indicates an expected call of CreateFullSyncResponse.
|
||||
func (mr *MockSyncClientMockRecorder) CreateFullSyncResponse(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFullSyncResponse", reflect.TypeOf((*MockSyncClient)(nil).CreateFullSyncResponse), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// CreateHeadUpdate mocks base method.
|
||||
func (m *MockSyncClient) CreateHeadUpdate(arg0 objecttree.ObjectTree, arg1 []*treechangeproto.RawTreeChangeWithId) *treechangeproto.TreeSyncMessage {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateHeadUpdate", arg0, arg1)
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CreateHeadUpdate indicates an expected call of CreateHeadUpdate.
|
||||
func (mr *MockSyncClientMockRecorder) CreateHeadUpdate(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateHeadUpdate", reflect.TypeOf((*MockSyncClient)(nil).CreateHeadUpdate), arg0, arg1)
|
||||
}
|
||||
|
||||
// CreateNewTreeRequest mocks base method.
|
||||
func (m *MockSyncClient) CreateNewTreeRequest() *treechangeproto.TreeSyncMessage {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateNewTreeRequest")
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CreateNewTreeRequest indicates an expected call of CreateNewTreeRequest.
|
||||
func (mr *MockSyncClientMockRecorder) CreateNewTreeRequest() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNewTreeRequest", reflect.TypeOf((*MockSyncClient)(nil).CreateNewTreeRequest))
|
||||
}
|
||||
|
||||
// QueueRequest mocks base method.
|
||||
func (m *MockSyncClient) QueueRequest(arg0, arg1 string, arg2 *treechangeproto.TreeSyncMessage) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "QueueRequest", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// QueueRequest indicates an expected call of QueueRequest.
|
||||
func (mr *MockSyncClientMockRecorder) QueueRequest(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueueRequest", reflect.TypeOf((*MockSyncClient)(nil).QueueRequest), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// SendRequest mocks base method.
|
||||
func (m *MockSyncClient) SendRequest(arg0 context.Context, arg1, arg2 string, arg3 *treechangeproto.TreeSyncMessage) (*spacesyncproto.ObjectSyncMessage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SendRequest", arg0, arg1, arg2, arg3)
|
||||
ret0, _ := ret[0].(*spacesyncproto.ObjectSyncMessage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SendRequest indicates an expected call of SendRequest.
|
||||
func (mr *MockSyncClientMockRecorder) SendRequest(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendRequest", reflect.TypeOf((*MockSyncClient)(nil).SendRequest), arg0, arg1, arg2, arg3)
|
||||
}
|
||||
|
||||
// SendUpdate mocks base method.
|
||||
func (m *MockSyncClient) SendUpdate(arg0, arg1 string, arg2 *treechangeproto.TreeSyncMessage) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SendUpdate", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SendUpdate indicates an expected call of SendUpdate.
|
||||
func (mr *MockSyncClientMockRecorder) SendUpdate(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendUpdate", reflect.TypeOf((*MockSyncClient)(nil).SendUpdate), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// MockRequestFactory is a mock of RequestFactory interface.
|
||||
type MockRequestFactory struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockRequestFactoryMockRecorder
|
||||
}
|
||||
|
||||
// MockRequestFactoryMockRecorder is the mock recorder for MockRequestFactory.
|
||||
type MockRequestFactoryMockRecorder struct {
|
||||
mock *MockRequestFactory
|
||||
}
|
||||
|
||||
// NewMockRequestFactory creates a new mock instance.
|
||||
func NewMockRequestFactory(ctrl *gomock.Controller) *MockRequestFactory {
|
||||
mock := &MockRequestFactory{ctrl: ctrl}
|
||||
mock.recorder = &MockRequestFactoryMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockRequestFactory) EXPECT() *MockRequestFactoryMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// CreateFullSyncRequest mocks base method.
|
||||
func (m *MockRequestFactory) CreateFullSyncRequest(arg0 objecttree.ObjectTree, arg1, arg2 []string) (*treechangeproto.TreeSyncMessage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateFullSyncRequest", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CreateFullSyncRequest indicates an expected call of CreateFullSyncRequest.
|
||||
func (mr *MockRequestFactoryMockRecorder) CreateFullSyncRequest(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFullSyncRequest", reflect.TypeOf((*MockRequestFactory)(nil).CreateFullSyncRequest), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// CreateFullSyncResponse mocks base method.
|
||||
func (m *MockRequestFactory) CreateFullSyncResponse(arg0 objecttree.ObjectTree, arg1, arg2 []string) (*treechangeproto.TreeSyncMessage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateFullSyncResponse", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CreateFullSyncResponse indicates an expected call of CreateFullSyncResponse.
|
||||
func (mr *MockRequestFactoryMockRecorder) CreateFullSyncResponse(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFullSyncResponse", reflect.TypeOf((*MockRequestFactory)(nil).CreateFullSyncResponse), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// CreateHeadUpdate mocks base method.
|
||||
func (m *MockRequestFactory) CreateHeadUpdate(arg0 objecttree.ObjectTree, arg1 []*treechangeproto.RawTreeChangeWithId) *treechangeproto.TreeSyncMessage {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateHeadUpdate", arg0, arg1)
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CreateHeadUpdate indicates an expected call of CreateHeadUpdate.
|
||||
func (mr *MockRequestFactoryMockRecorder) CreateHeadUpdate(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateHeadUpdate", reflect.TypeOf((*MockRequestFactory)(nil).CreateHeadUpdate), arg0, arg1)
|
||||
}
|
||||
|
||||
// CreateNewTreeRequest mocks base method.
|
||||
func (m *MockRequestFactory) CreateNewTreeRequest() *treechangeproto.TreeSyncMessage {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateNewTreeRequest")
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CreateNewTreeRequest indicates an expected call of CreateNewTreeRequest.
|
||||
func (mr *MockRequestFactoryMockRecorder) CreateNewTreeRequest() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNewTreeRequest", reflect.TypeOf((*MockRequestFactory)(nil).CreateNewTreeRequest))
|
||||
}
|
||||
|
||||
// MockTreeSyncProtocol is a mock of TreeSyncProtocol interface.
|
||||
type MockTreeSyncProtocol struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockTreeSyncProtocolMockRecorder
|
||||
}
|
||||
|
||||
// MockTreeSyncProtocolMockRecorder is the mock recorder for MockTreeSyncProtocol.
|
||||
type MockTreeSyncProtocolMockRecorder struct {
|
||||
mock *MockTreeSyncProtocol
|
||||
}
|
||||
|
||||
// NewMockTreeSyncProtocol creates a new mock instance.
|
||||
func NewMockTreeSyncProtocol(ctrl *gomock.Controller) *MockTreeSyncProtocol {
|
||||
mock := &MockTreeSyncProtocol{ctrl: ctrl}
|
||||
mock.recorder = &MockTreeSyncProtocolMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockTreeSyncProtocol) EXPECT() *MockTreeSyncProtocolMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// FullSyncRequest mocks base method.
|
||||
func (m *MockTreeSyncProtocol) FullSyncRequest(arg0 context.Context, arg1 string, arg2 *treechangeproto.TreeFullSyncRequest) (*treechangeproto.TreeSyncMessage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FullSyncRequest", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FullSyncRequest indicates an expected call of FullSyncRequest.
|
||||
func (mr *MockTreeSyncProtocolMockRecorder) FullSyncRequest(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FullSyncRequest", reflect.TypeOf((*MockTreeSyncProtocol)(nil).FullSyncRequest), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// FullSyncResponse mocks base method.
|
||||
func (m *MockTreeSyncProtocol) FullSyncResponse(arg0 context.Context, arg1 string, arg2 *treechangeproto.TreeFullSyncResponse) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FullSyncResponse", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// FullSyncResponse indicates an expected call of FullSyncResponse.
|
||||
func (mr *MockTreeSyncProtocolMockRecorder) FullSyncResponse(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FullSyncResponse", reflect.TypeOf((*MockTreeSyncProtocol)(nil).FullSyncResponse), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// HeadUpdate mocks base method.
|
||||
func (m *MockTreeSyncProtocol) HeadUpdate(arg0 context.Context, arg1 string, arg2 *treechangeproto.TreeHeadUpdate) (*treechangeproto.TreeSyncMessage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HeadUpdate", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*treechangeproto.TreeSyncMessage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// HeadUpdate indicates an expected call of HeadUpdate.
|
||||
func (mr *MockTreeSyncProtocolMockRecorder) HeadUpdate(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadUpdate", reflect.TypeOf((*MockTreeSyncProtocol)(nil).HeadUpdate), arg0, arg1, arg2)
|
||||
}
|
||||
@@ -1,362 +0,0 @@
|
||||
package synctree
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/accountdata"
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anyproto/any-sync/util/slice"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEmptyClientGetsFullHistory(t *testing.T) {
|
||||
treeId := "treeId"
|
||||
spaceId := "spaceId"
|
||||
keys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
aclList, err := list.NewTestDerivedAcl(spaceId, keys)
|
||||
require.NoError(t, err)
|
||||
storage := createStorage(treeId, aclList)
|
||||
changeCreator := objecttree.NewMockChangeCreator()
|
||||
deps := fixtureDeps{
|
||||
aclList: aclList,
|
||||
initStorage: storage.(*treestorage.InMemoryTreeStorage),
|
||||
connectionMap: map[string][]string{
|
||||
"peer1": []string{"peer2"},
|
||||
"peer2": []string{"peer1"},
|
||||
},
|
||||
emptyTrees: []string{"peer2"},
|
||||
treeBuilder: objecttree.BuildTestableTree,
|
||||
}
|
||||
fx := newProtocolFixture(t, spaceId, deps)
|
||||
fx.run(t)
|
||||
fx.handlers["peer1"].sendRawChanges(context.Background(), objecttree.RawChangesPayload{
|
||||
NewHeads: nil,
|
||||
RawChanges: []*treechangeproto.RawTreeChangeWithId{
|
||||
changeCreator.CreateRaw("1", aclList.Id(), treeId, true, treeId),
|
||||
},
|
||||
})
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
fx.stop()
|
||||
firstHeads := fx.handlers["peer1"].tree().Heads()
|
||||
secondHeads := fx.handlers["peer2"].tree().Heads()
|
||||
require.True(t, slice.UnsortedEquals(firstHeads, secondHeads))
|
||||
require.Equal(t, []string{"1"}, firstHeads)
|
||||
logMsgs := fx.log.batcher.GetAll()
|
||||
|
||||
var fullResponseMsg msgDescription
|
||||
for _, msg := range logMsgs {
|
||||
descr := msg.description()
|
||||
if descr.name == "FullSyncResponse" {
|
||||
fullResponseMsg = descr
|
||||
}
|
||||
}
|
||||
// that means that we got not only the last snapshot, but also the first one
|
||||
require.Len(t, fullResponseMsg.changes, 2)
|
||||
}
|
||||
|
||||
func TestTreeFuzzyMerge(t *testing.T) {
|
||||
testTreeFuzzyMerge(t, true)
|
||||
testTreeFuzzyMerge(t, false)
|
||||
}
|
||||
|
||||
func testTreeFuzzyMerge(t *testing.T, withData bool) {
|
||||
var (
|
||||
rnd = rand.New(rand.NewSource(time.Now().Unix()))
|
||||
levels = 20
|
||||
perLevel = 20
|
||||
rounds = 10
|
||||
)
|
||||
for i := 0; i < rounds; i++ {
|
||||
testTreeMerge(t, levels, perLevel, withData, func() bool {
|
||||
return true
|
||||
})
|
||||
testTreeMerge(t, levels, perLevel, withData, func() bool {
|
||||
return false
|
||||
})
|
||||
testTreeMerge(t, levels, perLevel, withData, func() bool {
|
||||
return rnd.Intn(10) > 8
|
||||
})
|
||||
levels += 2
|
||||
}
|
||||
}
|
||||
|
||||
func testTreeMerge(t *testing.T, levels, perLevel int, hasData bool, isSnapshot func() bool) {
|
||||
treeId := "treeId"
|
||||
spaceId := "spaceId"
|
||||
keys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
aclList, err := list.NewTestDerivedAcl(spaceId, keys)
|
||||
storage := createStorage(treeId, aclList)
|
||||
changeCreator := objecttree.NewMockChangeCreator()
|
||||
builder := objecttree.BuildTestableTree
|
||||
if hasData {
|
||||
builder = objecttree.BuildEmptyDataTestableTree
|
||||
}
|
||||
params := genParams{
|
||||
prefix: "peer1",
|
||||
aclId: aclList.Id(),
|
||||
startIdx: 0,
|
||||
levels: levels,
|
||||
perLevel: perLevel,
|
||||
snapshotId: treeId,
|
||||
prevHeads: []string{treeId},
|
||||
isSnapshot: isSnapshot,
|
||||
hasData: hasData,
|
||||
}
|
||||
// generating initial tree
|
||||
initialRes := genChanges(changeCreator, params)
|
||||
err = storage.AddRawChangesSetHeads(initialRes.changes, initialRes.heads)
|
||||
require.NoError(t, err)
|
||||
deps := fixtureDeps{
|
||||
aclList: aclList,
|
||||
initStorage: storage.(*treestorage.InMemoryTreeStorage),
|
||||
connectionMap: map[string][]string{
|
||||
"peer1": []string{"node1"},
|
||||
"peer2": []string{"node1"},
|
||||
"node1": []string{"peer1", "peer2"},
|
||||
},
|
||||
emptyTrees: []string{"peer2", "node1"},
|
||||
treeBuilder: builder,
|
||||
}
|
||||
fx := newProtocolFixture(t, spaceId, deps)
|
||||
fx.run(t)
|
||||
fx.handlers["peer1"].sendRawChanges(context.Background(), objecttree.RawChangesPayload{
|
||||
NewHeads: initialRes.heads,
|
||||
RawChanges: initialRes.changes,
|
||||
})
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
firstHeads := fx.handlers["peer1"].tree().Heads()
|
||||
secondHeads := fx.handlers["peer2"].tree().Heads()
|
||||
require.True(t, slice.UnsortedEquals(firstHeads, secondHeads))
|
||||
params = genParams{
|
||||
prefix: "peer1",
|
||||
aclId: aclList.Id(),
|
||||
startIdx: levels,
|
||||
levels: levels,
|
||||
perLevel: perLevel,
|
||||
|
||||
snapshotId: initialRes.snapshotId,
|
||||
prevHeads: initialRes.heads,
|
||||
isSnapshot: isSnapshot,
|
||||
hasData: hasData,
|
||||
}
|
||||
// generating different additions to the tree for different peers
|
||||
peer1Res := genChanges(changeCreator, params)
|
||||
params.prefix = "peer2"
|
||||
peer2Res := genChanges(changeCreator, params)
|
||||
fx.handlers["peer1"].sendRawChanges(context.Background(), objecttree.RawChangesPayload{
|
||||
NewHeads: peer1Res.heads,
|
||||
RawChanges: peer1Res.changes,
|
||||
})
|
||||
fx.handlers["peer2"].sendRawChanges(context.Background(), objecttree.RawChangesPayload{
|
||||
NewHeads: peer2Res.heads,
|
||||
RawChanges: peer2Res.changes,
|
||||
})
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
fx.stop()
|
||||
firstTree := fx.handlers["peer1"].tree()
|
||||
secondTree := fx.handlers["peer2"].tree()
|
||||
firstHeads = firstTree.Heads()
|
||||
secondHeads = secondTree.Heads()
|
||||
require.True(t, slice.UnsortedEquals(firstHeads, secondHeads))
|
||||
require.True(t, slice.UnsortedEquals(firstHeads, append(peer1Res.heads, peer2Res.heads...)))
|
||||
firstStorage := firstTree.Storage().(*treestorage.InMemoryTreeStorage)
|
||||
secondStorage := secondTree.Storage().(*treestorage.InMemoryTreeStorage)
|
||||
require.True(t, firstStorage.Equal(secondStorage))
|
||||
if hasData {
|
||||
for _, ch := range secondStorage.Changes {
|
||||
if ch.Id == treeId {
|
||||
continue
|
||||
}
|
||||
unmarshallRaw := &treechangeproto.RawTreeChange{}
|
||||
proto.Unmarshal(ch.RawChange, unmarshallRaw)
|
||||
treeCh := &treechangeproto.TreeChange{}
|
||||
proto.Unmarshal(unmarshallRaw.Payload, treeCh)
|
||||
require.Equal(t, ch.Id, string(treeCh.ChangesData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeStorageHasExtraChanges(t *testing.T) {
|
||||
var (
|
||||
rnd = rand.New(rand.NewSource(time.Now().Unix()))
|
||||
levels = 20
|
||||
perLevel = 40
|
||||
)
|
||||
|
||||
// simulating cases where one peer has some extra changes saved in storage
|
||||
// and checking that this will not break the sync
|
||||
t.Run("tree storage has extra simple", func(t *testing.T) {
|
||||
testTreeStorageHasExtra(t, levels, perLevel, false, func() bool {
|
||||
return false
|
||||
})
|
||||
testTreeStorageHasExtra(t, levels, perLevel, false, func() bool {
|
||||
return rnd.Intn(10) > 5
|
||||
})
|
||||
testTreeStorageHasExtra(t, levels, perLevel, true, func() bool {
|
||||
return false
|
||||
})
|
||||
testTreeStorageHasExtra(t, levels, perLevel, true, func() bool {
|
||||
return rnd.Intn(10) > 5
|
||||
})
|
||||
})
|
||||
t.Run("tree storage has extra three parts", func(t *testing.T) {
|
||||
testTreeStorageHasExtraThreeParts(t, levels, perLevel, false, func() bool {
|
||||
return false
|
||||
})
|
||||
testTreeStorageHasExtraThreeParts(t, levels, perLevel, false, func() bool {
|
||||
return rnd.Intn(10) > 5
|
||||
})
|
||||
testTreeStorageHasExtraThreeParts(t, levels, perLevel, true, func() bool {
|
||||
return false
|
||||
})
|
||||
testTreeStorageHasExtraThreeParts(t, levels, perLevel, true, func() bool {
|
||||
return rnd.Intn(10) > 5
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testTreeStorageHasExtra(t *testing.T, levels, perLevel int, hasData bool, isSnapshot func() bool) {
|
||||
treeId := "treeId"
|
||||
spaceId := "spaceId"
|
||||
keys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
aclList, err := list.NewTestDerivedAcl(spaceId, keys)
|
||||
storage := createStorage(treeId, aclList)
|
||||
changeCreator := objecttree.NewMockChangeCreator()
|
||||
builder := objecttree.BuildTestableTree
|
||||
if hasData {
|
||||
builder = objecttree.BuildEmptyDataTestableTree
|
||||
}
|
||||
params := genParams{
|
||||
prefix: "peer1",
|
||||
aclId: aclList.Id(),
|
||||
startIdx: 0,
|
||||
levels: levels,
|
||||
perLevel: perLevel,
|
||||
snapshotId: treeId,
|
||||
prevHeads: []string{treeId},
|
||||
isSnapshot: isSnapshot,
|
||||
hasData: hasData,
|
||||
}
|
||||
deps := fixtureDeps{
|
||||
aclList: aclList,
|
||||
initStorage: storage.(*treestorage.InMemoryTreeStorage),
|
||||
connectionMap: map[string][]string{
|
||||
"peer1": []string{"peer2"},
|
||||
"peer2": []string{"peer1"},
|
||||
},
|
||||
treeBuilder: builder,
|
||||
}
|
||||
fx := newProtocolFixture(t, spaceId, deps)
|
||||
|
||||
// generating initial tree
|
||||
initialRes := genChanges(changeCreator, params)
|
||||
fx.run(t)
|
||||
|
||||
// adding some changes to store, but without updating heads
|
||||
store := fx.handlers["peer1"].tree().Storage().(*treestorage.InMemoryTreeStorage)
|
||||
oldHeads, _ := store.Heads()
|
||||
store.AddRawChangesSetHeads(initialRes.changes, oldHeads)
|
||||
|
||||
// sending those changes to other peer
|
||||
fx.handlers["peer2"].sendRawChanges(context.Background(), objecttree.RawChangesPayload{
|
||||
NewHeads: initialRes.heads,
|
||||
RawChanges: initialRes.changes,
|
||||
})
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// here we want that the saved changes in storage should not affect the sync protocol
|
||||
firstHeads := fx.handlers["peer1"].tree().Heads()
|
||||
secondHeads := fx.handlers["peer2"].tree().Heads()
|
||||
require.True(t, slice.UnsortedEquals(firstHeads, secondHeads))
|
||||
require.True(t, slice.UnsortedEquals(initialRes.heads, firstHeads))
|
||||
}
|
||||
|
||||
func testTreeStorageHasExtraThreeParts(t *testing.T, levels, perLevel int, hasData bool, isSnapshot func() bool) {
|
||||
treeId := "treeId"
|
||||
spaceId := "spaceId"
|
||||
keys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
aclList, err := list.NewTestDerivedAcl(spaceId, keys)
|
||||
storage := createStorage(treeId, aclList)
|
||||
changeCreator := objecttree.NewMockChangeCreator()
|
||||
builder := objecttree.BuildTestableTree
|
||||
if hasData {
|
||||
builder = objecttree.BuildEmptyDataTestableTree
|
||||
}
|
||||
params := genParams{
|
||||
prefix: "peer1",
|
||||
aclId: aclList.Id(),
|
||||
startIdx: 0,
|
||||
levels: levels,
|
||||
perLevel: perLevel,
|
||||
snapshotId: treeId,
|
||||
prevHeads: []string{treeId},
|
||||
isSnapshot: isSnapshot,
|
||||
hasData: hasData,
|
||||
}
|
||||
deps := fixtureDeps{
|
||||
aclList: aclList,
|
||||
initStorage: storage.(*treestorage.InMemoryTreeStorage),
|
||||
connectionMap: map[string][]string{
|
||||
"peer1": []string{"peer2"},
|
||||
"peer2": []string{"peer1"},
|
||||
},
|
||||
treeBuilder: builder,
|
||||
}
|
||||
fx := newProtocolFixture(t, spaceId, deps)
|
||||
|
||||
// generating parts
|
||||
firstPart := genChanges(changeCreator, params)
|
||||
params.startIdx = levels
|
||||
params.snapshotId = firstPart.snapshotId
|
||||
params.prevHeads = firstPart.heads
|
||||
secondPart := genChanges(changeCreator, params)
|
||||
params.startIdx = levels * 2
|
||||
params.snapshotId = secondPart.snapshotId
|
||||
params.prevHeads = secondPart.heads
|
||||
thirdPart := genChanges(changeCreator, params)
|
||||
|
||||
// adding part1 to first peer and saving part2 and part3 in its storage
|
||||
res, _ := fx.handlers["peer1"].tree().AddRawChanges(context.Background(), objecttree.RawChangesPayload{
|
||||
NewHeads: firstPart.heads,
|
||||
RawChanges: firstPart.changes,
|
||||
})
|
||||
require.True(t, slice.UnsortedEquals(res.Heads, firstPart.heads))
|
||||
store := fx.handlers["peer1"].tree().Storage().(*treestorage.InMemoryTreeStorage)
|
||||
oldHeads, _ := store.Heads()
|
||||
store.AddRawChangesSetHeads(secondPart.changes, oldHeads)
|
||||
store.AddRawChangesSetHeads(thirdPart.changes, oldHeads)
|
||||
|
||||
var peer2Initial []*treechangeproto.RawTreeChangeWithId
|
||||
peer2Initial = append(peer2Initial, firstPart.changes...)
|
||||
peer2Initial = append(peer2Initial, secondPart.changes...)
|
||||
|
||||
// adding part1 and part2 to second peer
|
||||
res, _ = fx.handlers["peer2"].tree().AddRawChanges(context.Background(), objecttree.RawChangesPayload{
|
||||
NewHeads: secondPart.heads,
|
||||
RawChanges: peer2Initial,
|
||||
})
|
||||
require.True(t, slice.UnsortedEquals(res.Heads, secondPart.heads))
|
||||
fx.run(t)
|
||||
|
||||
// sending part3 changes to other peer
|
||||
fx.handlers["peer2"].sendRawChanges(context.Background(), objecttree.RawChangesPayload{
|
||||
NewHeads: thirdPart.heads,
|
||||
RawChanges: thirdPart.changes,
|
||||
})
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
firstHeads := fx.handlers["peer1"].tree().Heads()
|
||||
secondHeads := fx.handlers["peer2"].tree().Heads()
|
||||
require.True(t, slice.UnsortedEquals(firstHeads, secondHeads))
|
||||
require.True(t, slice.UnsortedEquals(thirdPart.heads, firstHeads))
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package synctree
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"sync"
|
||||
)
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ package synctree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/util/slice"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/util/slice"
|
||||
)
|
||||
|
||||
type RequestFactory interface {
|
||||
@@ -14,8 +14,10 @@ type RequestFactory interface {
|
||||
CreateFullSyncResponse(t objecttree.ObjectTree, theirHeads, theirSnapshotPath []string) (*treechangeproto.TreeSyncMessage, error)
|
||||
}
|
||||
|
||||
func NewRequestFactory() RequestFactory {
|
||||
return &requestFactory{}
|
||||
var sharedFactory = &requestFactory{}
|
||||
|
||||
func GetRequestFactory() RequestFactory {
|
||||
return sharedFactory
|
||||
}
|
||||
|
||||
type requestFactory struct{}
|
||||
|
||||
@@ -1,73 +1,57 @@
|
||||
//go:generate mockgen -destination mock_synctree/mock_synctree.go github.com/anytypeio/any-sync/commonspace/object/tree/synctree SyncClient,SyncTree,ReceiveQueue,HeadNotifiable
|
||||
package synctree
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/peermanager"
|
||||
"github.com/anyproto/any-sync/commonspace/requestmanager"
|
||||
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
"go.uber.org/zap"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/objectsync"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/nodeconf"
|
||||
)
|
||||
|
||||
type SyncClient interface {
|
||||
RequestFactory
|
||||
Broadcast(msg *treechangeproto.TreeSyncMessage)
|
||||
SendUpdate(peerId, objectId string, msg *treechangeproto.TreeSyncMessage) (err error)
|
||||
QueueRequest(peerId, objectId string, msg *treechangeproto.TreeSyncMessage) (err error)
|
||||
SendRequest(ctx context.Context, peerId, objectId string, msg *treechangeproto.TreeSyncMessage) (reply *spacesyncproto.ObjectSyncMessage, err error)
|
||||
Broadcast(ctx context.Context, msg *treechangeproto.TreeSyncMessage) (err error)
|
||||
SendWithReply(ctx context.Context, peerId string, msg *treechangeproto.TreeSyncMessage, replyId string) (err error)
|
||||
}
|
||||
|
||||
type syncClient struct {
|
||||
objectsync.MessagePool
|
||||
RequestFactory
|
||||
spaceId string
|
||||
requestManager requestmanager.RequestManager
|
||||
peerManager peermanager.PeerManager
|
||||
spaceId string
|
||||
configuration nodeconf.Configuration
|
||||
}
|
||||
|
||||
func NewSyncClient(spaceId string, requestManager requestmanager.RequestManager, peerManager peermanager.PeerManager) SyncClient {
|
||||
func newSyncClient(
|
||||
spaceId string,
|
||||
pool objectsync.MessagePool,
|
||||
factory RequestFactory,
|
||||
configuration nodeconf.Configuration) SyncClient {
|
||||
return &syncClient{
|
||||
RequestFactory: &requestFactory{},
|
||||
MessagePool: pool,
|
||||
RequestFactory: factory,
|
||||
configuration: configuration,
|
||||
spaceId: spaceId,
|
||||
requestManager: requestManager,
|
||||
peerManager: peerManager,
|
||||
}
|
||||
}
|
||||
func (s *syncClient) Broadcast(msg *treechangeproto.TreeSyncMessage) {
|
||||
objMsg, err := MarshallTreeMessage(msg, s.spaceId, msg.RootChange.Id, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = s.peerManager.Broadcast(context.Background(), objMsg)
|
||||
if err != nil {
|
||||
log.Debug("broadcast error", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *syncClient) SendUpdate(peerId, objectId string, msg *treechangeproto.TreeSyncMessage) (err error) {
|
||||
objMsg, err := MarshallTreeMessage(msg, s.spaceId, objectId, "")
|
||||
func (s *syncClient) Broadcast(ctx context.Context, msg *treechangeproto.TreeSyncMessage) (err error) {
|
||||
objMsg, err := marshallTreeMessage(msg, s.spaceId, msg.RootChange.Id, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return s.peerManager.SendPeer(context.Background(), peerId, objMsg)
|
||||
return s.MessagePool.Broadcast(ctx, objMsg)
|
||||
}
|
||||
|
||||
func (s *syncClient) SendRequest(ctx context.Context, peerId, objectId string, msg *treechangeproto.TreeSyncMessage) (reply *spacesyncproto.ObjectSyncMessage, err error) {
|
||||
objMsg, err := MarshallTreeMessage(msg, s.spaceId, objectId, "")
|
||||
func (s *syncClient) SendWithReply(ctx context.Context, peerId string, msg *treechangeproto.TreeSyncMessage, replyId string) (err error) {
|
||||
objMsg, err := marshallTreeMessage(msg, s.spaceId, msg.RootChange.Id, replyId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return s.requestManager.SendRequest(ctx, peerId, objMsg)
|
||||
return s.MessagePool.SendPeer(ctx, peerId, objMsg)
|
||||
}
|
||||
|
||||
func (s *syncClient) QueueRequest(peerId, objectId string, msg *treechangeproto.TreeSyncMessage) (err error) {
|
||||
objMsg, err := MarshallTreeMessage(msg, s.spaceId, objectId, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return s.requestManager.QueueRequest(peerId, objMsg)
|
||||
}
|
||||
|
||||
func MarshallTreeMessage(message *treechangeproto.TreeSyncMessage, spaceId, objectId, replyId string) (objMsg *spacesyncproto.ObjectSyncMessage, err error) {
|
||||
func marshallTreeMessage(message *treechangeproto.TreeSyncMessage, spaceId, objectId, replyId string) (objMsg *spacesyncproto.ObjectSyncMessage, err error) {
|
||||
payload, err := message.Marshal()
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
//go:generate mockgen -destination mock_synctree/mock_synctree.go github.com/anyproto/any-sync/commonspace/object/tree/synctree SyncTree,ReceiveQueue,HeadNotifiable,SyncClient,RequestFactory,TreeSyncProtocol
|
||||
package synctree
|
||||
|
||||
import (
|
||||
@@ -6,16 +5,17 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/app/logger"
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/synctree/updatelistener"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anyproto/any-sync/commonspace/objectsync/synchandler"
|
||||
"github.com/anyproto/any-sync/commonspace/spacestorage"
|
||||
"github.com/anyproto/any-sync/commonspace/syncstatus"
|
||||
"github.com/anyproto/any-sync/net/peer"
|
||||
"github.com/anyproto/any-sync/nodeconf"
|
||||
"github.com/anytypeio/any-sync/app/logger"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/synctree/updatelistener"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/objectsync"
|
||||
"github.com/anytypeio/any-sync/commonspace/objectsync/synchandler"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacestorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/syncstatus"
|
||||
"github.com/anytypeio/any-sync/net/peer"
|
||||
"github.com/anytypeio/any-sync/nodeconf"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -54,35 +54,35 @@ type syncTree struct {
|
||||
|
||||
var log = logger.NewNamed("common.commonspace.synctree")
|
||||
|
||||
var buildObjectTree = objecttree.BuildObjectTree
|
||||
var createSyncClient = newSyncClient
|
||||
|
||||
type ResponsiblePeersGetter interface {
|
||||
GetResponsiblePeers(ctx context.Context) (peers []peer.Peer, err error)
|
||||
}
|
||||
|
||||
type BuildDeps struct {
|
||||
SpaceId string
|
||||
SyncClient SyncClient
|
||||
Configuration nodeconf.NodeConf
|
||||
HeadNotifiable HeadNotifiable
|
||||
Listener updatelistener.UpdateListener
|
||||
AclList list.AclList
|
||||
SpaceStorage spacestorage.SpaceStorage
|
||||
TreeStorage treestorage.TreeStorage
|
||||
OnClose func(id string)
|
||||
SyncStatus syncstatus.StatusUpdater
|
||||
PeerGetter ResponsiblePeersGetter
|
||||
BuildObjectTree objecttree.BuildObjectTreeFunc
|
||||
SpaceId string
|
||||
ObjectSync objectsync.ObjectSync
|
||||
Configuration nodeconf.Configuration
|
||||
HeadNotifiable HeadNotifiable
|
||||
Listener updatelistener.UpdateListener
|
||||
AclList list.AclList
|
||||
SpaceStorage spacestorage.SpaceStorage
|
||||
TreeStorage treestorage.TreeStorage
|
||||
OnClose func(id string)
|
||||
SyncStatus syncstatus.StatusUpdater
|
||||
PeerGetter ResponsiblePeersGetter
|
||||
WaitTreeRemoteSync bool
|
||||
}
|
||||
|
||||
func BuildSyncTreeOrGetRemote(ctx context.Context, id string, deps BuildDeps) (t SyncTree, err error) {
|
||||
var (
|
||||
remoteGetter = treeRemoteGetter{treeId: id, deps: deps}
|
||||
isRemote bool
|
||||
)
|
||||
deps.TreeStorage, isRemote, err = remoteGetter.getTree(ctx)
|
||||
remoteGetter := treeRemoteGetter{treeId: id, deps: deps}
|
||||
deps.TreeStorage, err = remoteGetter.getTree(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return buildSyncTree(ctx, isRemote, deps)
|
||||
return buildSyncTree(ctx, true, deps)
|
||||
}
|
||||
|
||||
func PutSyncTree(ctx context.Context, payload treestorage.TreeStorageCreatePayload, deps BuildDeps) (t SyncTree, err error) {
|
||||
@@ -93,12 +93,16 @@ func PutSyncTree(ctx context.Context, payload treestorage.TreeStorageCreatePaylo
|
||||
return buildSyncTree(ctx, true, deps)
|
||||
}
|
||||
|
||||
func buildSyncTree(ctx context.Context, sendUpdate bool, deps BuildDeps) (t SyncTree, err error) {
|
||||
objTree, err := deps.BuildObjectTree(deps.TreeStorage, deps.AclList)
|
||||
func buildSyncTree(ctx context.Context, isFirstBuild bool, deps BuildDeps) (t SyncTree, err error) {
|
||||
objTree, err := buildObjectTree(deps.TreeStorage, deps.AclList)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
syncClient := deps.SyncClient
|
||||
syncClient := createSyncClient(
|
||||
deps.SpaceId,
|
||||
deps.ObjectSync.MessagePool(),
|
||||
sharedFactory,
|
||||
deps.Configuration)
|
||||
syncTree := &syncTree{
|
||||
ObjectTree: objTree,
|
||||
syncClient: syncClient,
|
||||
@@ -114,10 +118,12 @@ func buildSyncTree(ctx context.Context, sendUpdate bool, deps BuildDeps) (t Sync
|
||||
syncTree.afterBuild()
|
||||
syncTree.Unlock()
|
||||
|
||||
if sendUpdate {
|
||||
if isFirstBuild {
|
||||
headUpdate := syncTree.syncClient.CreateHeadUpdate(t, nil)
|
||||
// send to everybody, because everybody should know that the node or client got new tree
|
||||
syncTree.syncClient.Broadcast(headUpdate)
|
||||
if e := syncTree.syncClient.Broadcast(ctx, headUpdate); e != nil {
|
||||
log.ErrorCtx(ctx, "broadcast error", zap.Error(e))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -154,7 +160,7 @@ func (s *syncTree) AddContent(ctx context.Context, content objecttree.SignableCh
|
||||
}
|
||||
s.syncStatus.HeadsChange(s.Id(), res.Heads)
|
||||
headUpdate := s.syncClient.CreateHeadUpdate(s, res.Added)
|
||||
s.syncClient.Broadcast(headUpdate)
|
||||
err = s.syncClient.Broadcast(ctx, headUpdate)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -181,7 +187,7 @@ func (s *syncTree) AddRawChanges(ctx context.Context, changesPayload objecttree.
|
||||
s.notifiable.UpdateHeads(s.Id(), res.Heads)
|
||||
}
|
||||
headUpdate := s.syncClient.CreateHeadUpdate(s, res.Added)
|
||||
s.syncClient.Broadcast(headUpdate)
|
||||
err = s.syncClient.Broadcast(ctx, headUpdate)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -205,27 +211,18 @@ func (s *syncTree) Delete() (err error) {
|
||||
}
|
||||
|
||||
func (s *syncTree) TryClose(objectTTL time.Duration) (bool, error) {
|
||||
if !s.TryLock() {
|
||||
return false, nil
|
||||
}
|
||||
log.Debug("closing sync tree", zap.String("id", s.Id()))
|
||||
return true, s.close()
|
||||
return true, s.Close()
|
||||
}
|
||||
|
||||
func (s *syncTree) Close() (err error) {
|
||||
log.Debug("closing sync tree", zap.String("id", s.Id()))
|
||||
s.Lock()
|
||||
return s.close()
|
||||
}
|
||||
|
||||
func (s *syncTree) close() (err error) {
|
||||
defer s.Unlock()
|
||||
defer func() {
|
||||
log.Debug("closed sync tree", zap.Error(err), zap.String("id", s.Id()))
|
||||
}()
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.isClosed {
|
||||
err = ErrSyncTreeClosed
|
||||
return
|
||||
return ErrSyncTreeClosed
|
||||
}
|
||||
s.onClose(s.Id())
|
||||
s.isClosed = true
|
||||
@@ -246,7 +243,7 @@ func (s *syncTree) SyncWithPeer(ctx context.Context, peerId string) (err error)
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
headUpdate := s.syncClient.CreateHeadUpdate(s, nil)
|
||||
return s.syncClient.SendUpdate(peerId, headUpdate.RootChange.Id, headUpdate)
|
||||
return s.syncClient.SendWithReply(ctx, peerId, headUpdate, "")
|
||||
}
|
||||
|
||||
func (s *syncTree) afterBuild() {
|
||||
|
||||
@@ -2,17 +2,17 @@ package synctree
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree/mock_objecttree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/synctree/mock_synctree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/synctree/updatelistener"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/synctree/updatelistener/mock_updatelistener"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/objectsync"
|
||||
"github.com/anyproto/any-sync/commonspace/syncstatus"
|
||||
"github.com/anyproto/any-sync/nodeconf"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/objecttree/mock_objecttree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/synctree/mock_synctree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/synctree/updatelistener"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/synctree/updatelistener/mock_updatelistener"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/objectsync"
|
||||
"github.com/anytypeio/any-sync/commonspace/syncstatus"
|
||||
"github.com/anytypeio/any-sync/nodeconf"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -34,8 +34,8 @@ func (s syncTreeMatcher) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func syncClientFuncCreator(client SyncClient) func(spaceId string, factory RequestFactory, objectSync objectsync.ObjectSync, configuration nodeconf.NodeConf) SyncClient {
|
||||
return func(spaceId string, factory RequestFactory, objectSync objectsync.ObjectSync, configuration nodeconf.NodeConf) SyncClient {
|
||||
func syncClientFuncCreator(client SyncClient) func(spaceId string, factory RequestFactory, objectSync objectsync.ObjectSync, configuration nodeconf.Configuration) SyncClient {
|
||||
return func(spaceId string, factory RequestFactory, objectSync objectsync.ObjectSync, configuration nodeconf.Configuration) SyncClient {
|
||||
return client
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func Test_BuildSyncTree(t *testing.T) {
|
||||
updateListenerMock.EXPECT().Update(tr)
|
||||
|
||||
syncClientMock.EXPECT().CreateHeadUpdate(gomock.Eq(tr), gomock.Eq(changes)).Return(headUpdate)
|
||||
syncClientMock.EXPECT().Broadcast(gomock.Eq(headUpdate))
|
||||
syncClientMock.EXPECT().Broadcast(gomock.Any(), gomock.Eq(headUpdate)).Return(nil)
|
||||
res, err := tr.AddRawChanges(ctx, payload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedRes, res)
|
||||
@@ -95,7 +95,7 @@ func Test_BuildSyncTree(t *testing.T) {
|
||||
updateListenerMock.EXPECT().Rebuild(tr)
|
||||
|
||||
syncClientMock.EXPECT().CreateHeadUpdate(gomock.Eq(tr), gomock.Eq(changes)).Return(headUpdate)
|
||||
syncClientMock.EXPECT().Broadcast(gomock.Eq(headUpdate))
|
||||
syncClientMock.EXPECT().Broadcast(gomock.Any(), gomock.Eq(headUpdate)).Return(nil)
|
||||
res, err := tr.AddRawChanges(ctx, payload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedRes, res)
|
||||
@@ -133,7 +133,7 @@ func Test_BuildSyncTree(t *testing.T) {
|
||||
Return(expectedRes, nil)
|
||||
|
||||
syncClientMock.EXPECT().CreateHeadUpdate(gomock.Eq(tr), gomock.Eq(changes)).Return(headUpdate)
|
||||
syncClientMock.EXPECT().Broadcast(gomock.Eq(headUpdate))
|
||||
syncClientMock.EXPECT().Broadcast(gomock.Any(), gomock.Eq(headUpdate)).Return(nil)
|
||||
res, err := tr.AddContent(ctx, content)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedRes, res)
|
||||
|
||||
@@ -2,142 +2,218 @@ package synctree
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/objectsync/synchandler"
|
||||
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anyproto/any-sync/commonspace/syncstatus"
|
||||
"github.com/anyproto/any-sync/util/slice"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/objectsync/synchandler"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/syncstatus"
|
||||
"github.com/anytypeio/any-sync/util/slice"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMessageIsRequest = errors.New("message is request")
|
||||
ErrMessageIsNotRequest = errors.New("message is not request")
|
||||
ErrMoreThanOneRequest = errors.New("more than one request for same peer")
|
||||
"go.uber.org/zap"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type syncTreeHandler struct {
|
||||
objTree objecttree.ObjectTree
|
||||
syncClient SyncClient
|
||||
syncProtocol TreeSyncProtocol
|
||||
syncStatus syncstatus.StatusUpdater
|
||||
spaceId string
|
||||
|
||||
handlerLock sync.Mutex
|
||||
pendingRequests map[string]struct{}
|
||||
heads []string
|
||||
objTree objecttree.ObjectTree
|
||||
syncClient SyncClient
|
||||
syncStatus syncstatus.StatusUpdater
|
||||
handlerLock sync.Mutex
|
||||
spaceId string
|
||||
queue ReceiveQueue
|
||||
}
|
||||
|
||||
const maxQueueSize = 5
|
||||
|
||||
func newSyncTreeHandler(spaceId string, objTree objecttree.ObjectTree, syncClient SyncClient, syncStatus syncstatus.StatusUpdater) synchandler.SyncHandler {
|
||||
return &syncTreeHandler{
|
||||
objTree: objTree,
|
||||
syncProtocol: newTreeSyncProtocol(spaceId, objTree, syncClient),
|
||||
syncClient: syncClient,
|
||||
syncStatus: syncStatus,
|
||||
spaceId: spaceId,
|
||||
pendingRequests: make(map[string]struct{}),
|
||||
objTree: objTree,
|
||||
syncClient: syncClient,
|
||||
syncStatus: syncStatus,
|
||||
spaceId: spaceId,
|
||||
queue: newReceiveQueue(maxQueueSize),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *syncTreeHandler) HandleRequest(ctx context.Context, senderId string, request *spacesyncproto.ObjectSyncMessage) (response *spacesyncproto.ObjectSyncMessage, err error) {
|
||||
unmarshalled := &treechangeproto.TreeSyncMessage{}
|
||||
err = proto.Unmarshal(request.Payload, unmarshalled)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fullSyncRequest := unmarshalled.GetContent().GetFullSyncRequest()
|
||||
if fullSyncRequest == nil {
|
||||
return nil, ErrMessageIsNotRequest
|
||||
}
|
||||
// setting pending requests
|
||||
s.handlerLock.Lock()
|
||||
_, exists := s.pendingRequests[senderId]
|
||||
if exists {
|
||||
s.handlerLock.Unlock()
|
||||
return nil, ErrMoreThanOneRequest
|
||||
}
|
||||
s.pendingRequests[senderId] = struct{}{}
|
||||
s.handlerLock.Unlock()
|
||||
|
||||
response, err = s.handleRequest(ctx, senderId, fullSyncRequest)
|
||||
|
||||
// removing pending requests
|
||||
s.handlerLock.Lock()
|
||||
delete(s.pendingRequests, senderId)
|
||||
s.handlerLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (s *syncTreeHandler) handleRequest(ctx context.Context, senderId string, fullSyncRequest *treechangeproto.TreeFullSyncRequest) (response *spacesyncproto.ObjectSyncMessage, err error) {
|
||||
s.objTree.Lock()
|
||||
defer s.objTree.Unlock()
|
||||
treeResp, err := s.syncProtocol.FullSyncRequest(ctx, senderId, fullSyncRequest)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
response, err = MarshallTreeMessage(treeResp, s.spaceId, s.objTree.Id(), "")
|
||||
return
|
||||
}
|
||||
|
||||
func (s *syncTreeHandler) HandleMessage(ctx context.Context, senderId string, msg *spacesyncproto.ObjectSyncMessage) (err error) {
|
||||
unmarshalled := &treechangeproto.TreeSyncMessage{}
|
||||
err = proto.Unmarshal(msg.Payload, unmarshalled)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
heads := treechangeproto.GetHeads(unmarshalled)
|
||||
s.syncStatus.HeadsReceive(senderId, msg.ObjectId, heads)
|
||||
s.handlerLock.Lock()
|
||||
// if the update has same heads then returning not to hang on a lock
|
||||
if unmarshalled.GetContent().GetHeadUpdate() != nil && slice.UnsortedEquals(heads, s.heads) {
|
||||
s.handlerLock.Unlock()
|
||||
s.syncStatus.HeadsReceive(senderId, msg.ObjectId, treechangeproto.GetHeads(unmarshalled))
|
||||
|
||||
queueFull := s.queue.AddMessage(senderId, unmarshalled, msg.RequestId)
|
||||
if queueFull {
|
||||
return
|
||||
}
|
||||
s.handlerLock.Unlock()
|
||||
return s.handleMessage(ctx, unmarshalled, senderId)
|
||||
|
||||
return s.handleMessage(ctx, senderId)
|
||||
}
|
||||
|
||||
func (s *syncTreeHandler) handleMessage(ctx context.Context, msg *treechangeproto.TreeSyncMessage, senderId string) (err error) {
|
||||
func (s *syncTreeHandler) handleMessage(ctx context.Context, senderId string) (err error) {
|
||||
s.objTree.Lock()
|
||||
defer s.objTree.Unlock()
|
||||
msg, replyId, err := s.queue.GetMessage(senderId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer s.queue.ClearQueue(senderId)
|
||||
|
||||
content := msg.GetContent()
|
||||
switch {
|
||||
case content.GetHeadUpdate() != nil:
|
||||
return s.handleHeadUpdate(ctx, senderId, content.GetHeadUpdate(), replyId)
|
||||
case content.GetFullSyncRequest() != nil:
|
||||
return s.handleFullSyncRequest(ctx, senderId, content.GetFullSyncRequest(), replyId)
|
||||
case content.GetFullSyncResponse() != nil:
|
||||
return s.handleFullSyncResponse(ctx, senderId, content.GetFullSyncResponse())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *syncTreeHandler) handleHeadUpdate(
|
||||
ctx context.Context,
|
||||
senderId string,
|
||||
update *treechangeproto.TreeHeadUpdate,
|
||||
replyId string) (err error) {
|
||||
var (
|
||||
copyHeads = make([]string, 0, len(s.objTree.Heads()))
|
||||
treeId = s.objTree.Id()
|
||||
content = msg.GetContent()
|
||||
fullRequest *treechangeproto.TreeSyncMessage
|
||||
isEmptyUpdate = len(update.Changes) == 0
|
||||
objTree = s.objTree
|
||||
)
|
||||
|
||||
// getting old heads
|
||||
copyHeads = append(copyHeads, s.objTree.Heads()...)
|
||||
log := log.With(zap.Strings("heads", objTree.Heads()), zap.String("treeId", objTree.Id()), zap.String("spaceId", s.spaceId))
|
||||
log.DebugCtx(ctx, "received head update message")
|
||||
|
||||
defer func() {
|
||||
// checking if something changed
|
||||
if !slice.UnsortedEquals(copyHeads, s.objTree.Heads()) {
|
||||
s.handlerLock.Lock()
|
||||
defer s.handlerLock.Unlock()
|
||||
s.heads = s.heads[:0]
|
||||
for _, h := range s.objTree.Heads() {
|
||||
s.heads = append(s.heads, h)
|
||||
if err != nil {
|
||||
log.With(zap.Error(err)).Debug("head update finished with error")
|
||||
} else if fullRequest != nil {
|
||||
log.DebugCtx(ctx, "sending full sync request")
|
||||
} else {
|
||||
if !isEmptyUpdate {
|
||||
log.DebugCtx(ctx, "head update finished correctly")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
switch {
|
||||
case content.GetHeadUpdate() != nil:
|
||||
var syncReq *treechangeproto.TreeSyncMessage
|
||||
syncReq, err = s.syncProtocol.HeadUpdate(ctx, senderId, content.GetHeadUpdate())
|
||||
if err != nil || syncReq == nil {
|
||||
// isEmptyUpdate is sent when the tree is brought up from cache
|
||||
if isEmptyUpdate {
|
||||
headEquals := slice.UnsortedEquals(objTree.Heads(), update.Heads)
|
||||
log.DebugCtx(ctx, "is empty update", zap.String("treeId", objTree.Id()), zap.Bool("headEquals", headEquals))
|
||||
if headEquals {
|
||||
return
|
||||
}
|
||||
return s.syncClient.QueueRequest(senderId, treeId, syncReq)
|
||||
case content.GetFullSyncRequest() != nil:
|
||||
return ErrMessageIsRequest
|
||||
case content.GetFullSyncResponse() != nil:
|
||||
return s.syncProtocol.FullSyncResponse(ctx, senderId, content.GetFullSyncResponse())
|
||||
|
||||
// we need to sync in any case
|
||||
fullRequest, err = s.syncClient.CreateFullSyncRequest(objTree, update.Heads, update.SnapshotPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return s.syncClient.SendWithReply(ctx, senderId, fullRequest, replyId)
|
||||
}
|
||||
|
||||
if s.alreadyHasHeads(objTree, update.Heads) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = objTree.AddRawChanges(ctx, objecttree.RawChangesPayload{
|
||||
NewHeads: update.Heads,
|
||||
RawChanges: update.Changes,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if s.alreadyHasHeads(objTree, update.Heads) {
|
||||
return
|
||||
}
|
||||
|
||||
fullRequest, err = s.syncClient.CreateFullSyncRequest(objTree, update.Heads, update.SnapshotPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return s.syncClient.SendWithReply(ctx, senderId, fullRequest, replyId)
|
||||
}
|
||||
|
||||
func (s *syncTreeHandler) handleFullSyncRequest(
|
||||
ctx context.Context,
|
||||
senderId string,
|
||||
request *treechangeproto.TreeFullSyncRequest,
|
||||
replyId string) (err error) {
|
||||
var (
|
||||
fullResponse *treechangeproto.TreeSyncMessage
|
||||
header = s.objTree.Header()
|
||||
objTree = s.objTree
|
||||
)
|
||||
|
||||
log := log.With(zap.String("senderId", senderId),
|
||||
zap.Strings("heads", request.Heads),
|
||||
zap.String("treeId", s.objTree.Id()),
|
||||
zap.String("replyId", replyId),
|
||||
zap.String("spaceId", s.spaceId))
|
||||
log.DebugCtx(ctx, "received full sync request message")
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.With(zap.Error(err)).DebugCtx(ctx, "full sync request finished with error")
|
||||
|
||||
s.syncClient.SendWithReply(ctx, senderId, treechangeproto.WrapError(err, header), replyId)
|
||||
return
|
||||
} else if fullResponse != nil {
|
||||
log.DebugCtx(ctx, "full sync response sent")
|
||||
}
|
||||
}()
|
||||
|
||||
if len(request.Changes) != 0 && !s.alreadyHasHeads(objTree, request.Heads) {
|
||||
_, err = objTree.AddRawChanges(ctx, objecttree.RawChangesPayload{
|
||||
NewHeads: request.Heads,
|
||||
RawChanges: request.Changes,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
fullResponse, err = s.syncClient.CreateFullSyncResponse(objTree, request.Heads, request.SnapshotPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return s.syncClient.SendWithReply(ctx, senderId, fullResponse, replyId)
|
||||
}
|
||||
|
||||
func (s *syncTreeHandler) handleFullSyncResponse(
|
||||
ctx context.Context,
|
||||
senderId string,
|
||||
response *treechangeproto.TreeFullSyncResponse) (err error) {
|
||||
var (
|
||||
objTree = s.objTree
|
||||
)
|
||||
log := log.With(zap.Strings("heads", response.Heads), zap.String("treeId", s.objTree.Id()), zap.String("spaceId", s.spaceId))
|
||||
log.DebugCtx(ctx, "received full sync response message")
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.With(zap.Error(err)).DebugCtx(ctx, "full sync response failed")
|
||||
} else {
|
||||
log.DebugCtx(ctx, "full sync response succeeded")
|
||||
}
|
||||
}()
|
||||
|
||||
if s.alreadyHasHeads(objTree, response.Heads) {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = objTree.AddRawChanges(ctx, objecttree.RawChangesPayload{
|
||||
NewHeads: response.Heads,
|
||||
RawChanges: response.Changes,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (s *syncTreeHandler) alreadyHasHeads(t objecttree.ObjectTree, heads []string) bool {
|
||||
return slice.UnsortedEquals(t.Heads(), heads) || t.HasChanges(heads...)
|
||||
}
|
||||
@@ -2,15 +2,19 @@ package synctree
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree/mock_objecttree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/synctree/mock_synctree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/syncstatus"
|
||||
"github.com/anytypeio/any-sync/app/logger"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/objecttree/mock_objecttree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/synctree/mock_synctree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/syncstatus"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type testObjTreeMock struct {
|
||||
@@ -52,38 +56,29 @@ type syncHandlerFixture struct {
|
||||
ctrl *gomock.Controller
|
||||
syncClientMock *mock_synctree.MockSyncClient
|
||||
objectTreeMock *testObjTreeMock
|
||||
syncProtocolMock *mock_synctree.MockTreeSyncProtocol
|
||||
spaceId string
|
||||
senderId string
|
||||
treeId string
|
||||
receiveQueueMock ReceiveQueue
|
||||
|
||||
syncHandler *syncTreeHandler
|
||||
}
|
||||
|
||||
func newSyncHandlerFixture(t *testing.T) *syncHandlerFixture {
|
||||
ctrl := gomock.NewController(t)
|
||||
objectTreeMock := newTestObjMock(mock_objecttree.NewMockObjectTree(ctrl))
|
||||
syncClientMock := mock_synctree.NewMockSyncClient(ctrl)
|
||||
syncProtocolMock := mock_synctree.NewMockTreeSyncProtocol(ctrl)
|
||||
spaceId := "spaceId"
|
||||
objectTreeMock := newTestObjMock(mock_objecttree.NewMockObjectTree(ctrl))
|
||||
receiveQueue := newReceiveQueue(5)
|
||||
|
||||
syncHandler := &syncTreeHandler{
|
||||
objTree: objectTreeMock,
|
||||
syncClient: syncClientMock,
|
||||
syncProtocol: syncProtocolMock,
|
||||
spaceId: spaceId,
|
||||
syncStatus: syncstatus.NewNoOpSyncStatus(),
|
||||
pendingRequests: map[string]struct{}{},
|
||||
objTree: objectTreeMock,
|
||||
syncClient: syncClientMock,
|
||||
queue: receiveQueue,
|
||||
syncStatus: syncstatus.NewNoOpSyncStatus(),
|
||||
}
|
||||
return &syncHandlerFixture{
|
||||
ctrl: ctrl,
|
||||
objectTreeMock: objectTreeMock,
|
||||
syncProtocolMock: syncProtocolMock,
|
||||
syncClientMock: syncClientMock,
|
||||
objectTreeMock: objectTreeMock,
|
||||
receiveQueueMock: receiveQueue,
|
||||
syncHandler: syncHandler,
|
||||
spaceId: spaceId,
|
||||
senderId: "senderId",
|
||||
treeId: "treeId",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,149 +86,333 @@ func (fx *syncHandlerFixture) stop() {
|
||||
fx.ctrl.Finish()
|
||||
}
|
||||
|
||||
func TestSyncTreeHandler_HandleMessage(t *testing.T) {
|
||||
func TestSyncHandler_HandleHeadUpdate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
log = logger.CtxLogger{Logger: zap.NewNop()}
|
||||
|
||||
t.Run("handle head update message, heads not equal, request returned", func(t *testing.T) {
|
||||
t.Run("head update non empty all heads added", func(t *testing.T) {
|
||||
fx := newSyncHandlerFixture(t)
|
||||
defer fx.stop()
|
||||
treeId := "treeId"
|
||||
senderId := "senderId"
|
||||
chWithId := &treechangeproto.RawTreeChangeWithId{}
|
||||
headUpdate := &treechangeproto.TreeHeadUpdate{
|
||||
Heads: []string{"h3"},
|
||||
Heads: []string{"h1"},
|
||||
Changes: []*treechangeproto.RawTreeChangeWithId{chWithId},
|
||||
SnapshotPath: []string{"h1"},
|
||||
}
|
||||
treeMsg := treechangeproto.WrapHeadUpdate(headUpdate, chWithId)
|
||||
objectMsg, _ := MarshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
objectMsg, _ := marshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
|
||||
syncReq := &treechangeproto.TreeSyncMessage{}
|
||||
fx.syncHandler.heads = []string{"h2"}
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(fx.treeId)
|
||||
fx.objectTreeMock.EXPECT().Heads().Times(2).Return([]string{"h2"})
|
||||
fx.objectTreeMock.EXPECT().Heads().Times(2).Return([]string{"h3"})
|
||||
fx.syncProtocolMock.EXPECT().HeadUpdate(ctx, fx.senderId, gomock.Any()).Return(syncReq, nil)
|
||||
fx.syncClientMock.EXPECT().QueueRequest(fx.senderId, fx.treeId, syncReq).Return(nil)
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(treeId)
|
||||
fx.objectTreeMock.EXPECT().Heads().Return([]string{"h2"}).Times(2)
|
||||
fx.objectTreeMock.EXPECT().HasChanges(gomock.Eq([]string{"h1"})).Return(false)
|
||||
fx.objectTreeMock.EXPECT().
|
||||
AddRawChanges(gomock.Any(), gomock.Eq(objecttree.RawChangesPayload{
|
||||
NewHeads: []string{"h1"},
|
||||
RawChanges: []*treechangeproto.RawTreeChangeWithId{chWithId},
|
||||
})).
|
||||
Return(objecttree.AddResult{}, nil)
|
||||
fx.objectTreeMock.EXPECT().Heads().Return([]string{"h2", "h1"})
|
||||
fx.objectTreeMock.EXPECT().HasChanges(gomock.Eq([]string{"h1"})).Return(true)
|
||||
|
||||
err := fx.syncHandler.HandleMessage(ctx, fx.senderId, objectMsg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"h3"}, fx.syncHandler.heads)
|
||||
})
|
||||
|
||||
t.Run("handle head update message, heads equal", func(t *testing.T) {
|
||||
fx := newSyncHandlerFixture(t)
|
||||
defer fx.stop()
|
||||
treeId := "treeId"
|
||||
chWithId := &treechangeproto.RawTreeChangeWithId{}
|
||||
headUpdate := &treechangeproto.TreeHeadUpdate{
|
||||
Heads: []string{"h1"},
|
||||
}
|
||||
treeMsg := treechangeproto.WrapHeadUpdate(headUpdate, chWithId)
|
||||
objectMsg, _ := MarshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
|
||||
fx.syncHandler.heads = []string{"h1"}
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(fx.treeId)
|
||||
|
||||
err := fx.syncHandler.HandleMessage(ctx, fx.senderId, objectMsg)
|
||||
err := fx.syncHandler.HandleMessage(ctx, senderId, objectMsg)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("handle head update message, empty sync request returned", func(t *testing.T) {
|
||||
t.Run("head update non empty heads not added", func(t *testing.T) {
|
||||
fx := newSyncHandlerFixture(t)
|
||||
defer fx.stop()
|
||||
treeId := "treeId"
|
||||
senderId := "senderId"
|
||||
chWithId := &treechangeproto.RawTreeChangeWithId{}
|
||||
headUpdate := &treechangeproto.TreeHeadUpdate{
|
||||
Heads: []string{"h3"},
|
||||
Heads: []string{"h1"},
|
||||
Changes: []*treechangeproto.RawTreeChangeWithId{chWithId},
|
||||
SnapshotPath: []string{"h1"},
|
||||
}
|
||||
treeMsg := treechangeproto.WrapHeadUpdate(headUpdate, chWithId)
|
||||
objectMsg, _ := MarshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
objectMsg, _ := marshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
fullRequest := &treechangeproto.TreeSyncMessage{}
|
||||
|
||||
fx.syncHandler.heads = []string{"h2"}
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(fx.treeId)
|
||||
fx.objectTreeMock.EXPECT().Heads().Times(2).Return([]string{"h2"})
|
||||
fx.objectTreeMock.EXPECT().Heads().Times(2).Return([]string{"h3"})
|
||||
fx.syncProtocolMock.EXPECT().HeadUpdate(ctx, fx.senderId, gomock.Any()).Return(nil, nil)
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(treeId)
|
||||
fx.objectTreeMock.EXPECT().Heads().Return([]string{"h2"}).AnyTimes()
|
||||
fx.objectTreeMock.EXPECT().HasChanges(gomock.Eq([]string{"h1"})).Return(false)
|
||||
fx.objectTreeMock.EXPECT().
|
||||
AddRawChanges(gomock.Any(), gomock.Eq(objecttree.RawChangesPayload{
|
||||
NewHeads: []string{"h1"},
|
||||
RawChanges: []*treechangeproto.RawTreeChangeWithId{chWithId},
|
||||
})).
|
||||
Return(objecttree.AddResult{}, nil)
|
||||
fx.objectTreeMock.EXPECT().HasChanges(gomock.Eq([]string{"h1"})).Return(false)
|
||||
fx.syncClientMock.EXPECT().
|
||||
CreateFullSyncRequest(gomock.Eq(fx.objectTreeMock), gomock.Eq([]string{"h1"}), gomock.Eq([]string{"h1"})).
|
||||
Return(fullRequest, nil)
|
||||
fx.syncClientMock.EXPECT().SendWithReply(gomock.Any(), gomock.Eq(senderId), gomock.Eq(fullRequest), gomock.Eq(""))
|
||||
|
||||
err := fx.syncHandler.HandleMessage(ctx, fx.senderId, objectMsg)
|
||||
err := fx.syncHandler.HandleMessage(ctx, senderId, objectMsg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"h3"}, fx.syncHandler.heads)
|
||||
})
|
||||
|
||||
t.Run("handle full sync request returns error", func(t *testing.T) {
|
||||
t.Run("head update non empty equal heads", func(t *testing.T) {
|
||||
fx := newSyncHandlerFixture(t)
|
||||
defer fx.stop()
|
||||
treeId := "treeId"
|
||||
senderId := "senderId"
|
||||
chWithId := &treechangeproto.RawTreeChangeWithId{}
|
||||
fullRequest := &treechangeproto.TreeFullSyncRequest{
|
||||
Heads: []string{"h3"},
|
||||
headUpdate := &treechangeproto.TreeHeadUpdate{
|
||||
Heads: []string{"h1"},
|
||||
Changes: []*treechangeproto.RawTreeChangeWithId{chWithId},
|
||||
SnapshotPath: []string{"h1"},
|
||||
}
|
||||
treeMsg := treechangeproto.WrapFullRequest(fullRequest, chWithId)
|
||||
objectMsg, _ := MarshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
treeMsg := treechangeproto.WrapHeadUpdate(headUpdate, chWithId)
|
||||
objectMsg, _ := marshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
|
||||
fx.syncHandler.heads = []string{"h2"}
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(fx.treeId)
|
||||
fx.objectTreeMock.EXPECT().Heads().Times(3).Return([]string{"h2"})
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(treeId)
|
||||
fx.objectTreeMock.EXPECT().Heads().Return([]string{"h1"}).AnyTimes()
|
||||
|
||||
err := fx.syncHandler.HandleMessage(ctx, fx.senderId, objectMsg)
|
||||
require.Equal(t, err, ErrMessageIsRequest)
|
||||
err := fx.syncHandler.HandleMessage(ctx, senderId, objectMsg)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("handle full sync response", func(t *testing.T) {
|
||||
t.Run("head update empty", func(t *testing.T) {
|
||||
fx := newSyncHandlerFixture(t)
|
||||
defer fx.stop()
|
||||
treeId := "treeId"
|
||||
senderId := "senderId"
|
||||
chWithId := &treechangeproto.RawTreeChangeWithId{}
|
||||
headUpdate := &treechangeproto.TreeHeadUpdate{
|
||||
Heads: []string{"h1"},
|
||||
Changes: nil,
|
||||
SnapshotPath: []string{"h1"},
|
||||
}
|
||||
treeMsg := treechangeproto.WrapHeadUpdate(headUpdate, chWithId)
|
||||
objectMsg, _ := marshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
fullRequest := &treechangeproto.TreeSyncMessage{}
|
||||
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(treeId)
|
||||
fx.objectTreeMock.EXPECT().Heads().Return([]string{"h2"}).AnyTimes()
|
||||
fx.syncClientMock.EXPECT().
|
||||
CreateFullSyncRequest(gomock.Eq(fx.objectTreeMock), gomock.Eq([]string{"h1"}), gomock.Eq([]string{"h1"})).
|
||||
Return(fullRequest, nil)
|
||||
fx.syncClientMock.EXPECT().SendWithReply(gomock.Any(), gomock.Eq(senderId), gomock.Eq(fullRequest), gomock.Eq(""))
|
||||
|
||||
err := fx.syncHandler.HandleMessage(ctx, senderId, objectMsg)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("head update empty equal heads", func(t *testing.T) {
|
||||
fx := newSyncHandlerFixture(t)
|
||||
defer fx.stop()
|
||||
treeId := "treeId"
|
||||
senderId := "senderId"
|
||||
chWithId := &treechangeproto.RawTreeChangeWithId{}
|
||||
headUpdate := &treechangeproto.TreeHeadUpdate{
|
||||
Heads: []string{"h1"},
|
||||
Changes: nil,
|
||||
SnapshotPath: []string{"h1"},
|
||||
}
|
||||
treeMsg := treechangeproto.WrapHeadUpdate(headUpdate, chWithId)
|
||||
objectMsg, _ := marshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(treeId)
|
||||
fx.objectTreeMock.EXPECT().Heads().Return([]string{"h1"}).AnyTimes()
|
||||
|
||||
err := fx.syncHandler.HandleMessage(ctx, senderId, objectMsg)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSyncHandler_HandleFullSyncRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
log = logger.CtxLogger{Logger: zap.NewNop()}
|
||||
|
||||
t.Run("full sync request with change", func(t *testing.T) {
|
||||
fx := newSyncHandlerFixture(t)
|
||||
defer fx.stop()
|
||||
treeId := "treeId"
|
||||
senderId := "senderId"
|
||||
chWithId := &treechangeproto.RawTreeChangeWithId{}
|
||||
fullSyncRequest := &treechangeproto.TreeFullSyncRequest{
|
||||
Heads: []string{"h1"},
|
||||
Changes: []*treechangeproto.RawTreeChangeWithId{chWithId},
|
||||
SnapshotPath: []string{"h1"},
|
||||
}
|
||||
treeMsg := treechangeproto.WrapFullRequest(fullSyncRequest, chWithId)
|
||||
objectMsg, _ := marshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
fullResponse := &treechangeproto.TreeSyncMessage{}
|
||||
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(treeId)
|
||||
fx.objectTreeMock.EXPECT().Header().Return(nil)
|
||||
fx.objectTreeMock.EXPECT().Heads().Return([]string{"h2"}).AnyTimes()
|
||||
fx.objectTreeMock.EXPECT().HasChanges(gomock.Eq([]string{"h1"})).Return(false)
|
||||
fx.objectTreeMock.EXPECT().
|
||||
AddRawChanges(gomock.Any(), gomock.Eq(objecttree.RawChangesPayload{
|
||||
NewHeads: []string{"h1"},
|
||||
RawChanges: []*treechangeproto.RawTreeChangeWithId{chWithId},
|
||||
})).
|
||||
Return(objecttree.AddResult{}, nil)
|
||||
fx.syncClientMock.EXPECT().
|
||||
CreateFullSyncResponse(gomock.Eq(fx.objectTreeMock), gomock.Eq([]string{"h1"}), gomock.Eq([]string{"h1"})).
|
||||
Return(fullResponse, nil)
|
||||
fx.syncClientMock.EXPECT().SendWithReply(gomock.Any(), gomock.Eq(senderId), gomock.Eq(fullResponse), gomock.Eq(""))
|
||||
|
||||
err := fx.syncHandler.HandleMessage(ctx, senderId, objectMsg)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("full sync request with change same heads", func(t *testing.T) {
|
||||
fx := newSyncHandlerFixture(t)
|
||||
defer fx.stop()
|
||||
treeId := "treeId"
|
||||
senderId := "senderId"
|
||||
chWithId := &treechangeproto.RawTreeChangeWithId{}
|
||||
fullSyncRequest := &treechangeproto.TreeFullSyncRequest{
|
||||
Heads: []string{"h1"},
|
||||
Changes: []*treechangeproto.RawTreeChangeWithId{chWithId},
|
||||
SnapshotPath: []string{"h1"},
|
||||
}
|
||||
treeMsg := treechangeproto.WrapFullRequest(fullSyncRequest, chWithId)
|
||||
objectMsg, _ := marshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
fullResponse := &treechangeproto.TreeSyncMessage{}
|
||||
|
||||
fx.objectTreeMock.EXPECT().
|
||||
Id().AnyTimes().Return(treeId)
|
||||
fx.objectTreeMock.EXPECT().Header().Return(nil)
|
||||
fx.objectTreeMock.EXPECT().
|
||||
Heads().
|
||||
Return([]string{"h1"}).AnyTimes()
|
||||
fx.syncClientMock.EXPECT().
|
||||
CreateFullSyncResponse(gomock.Eq(fx.objectTreeMock), gomock.Eq([]string{"h1"}), gomock.Eq([]string{"h1"})).
|
||||
Return(fullResponse, nil)
|
||||
fx.syncClientMock.EXPECT().SendWithReply(gomock.Any(), gomock.Eq(senderId), gomock.Eq(fullResponse), gomock.Eq(""))
|
||||
|
||||
err := fx.syncHandler.HandleMessage(ctx, senderId, objectMsg)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("full sync request without change but with reply id", func(t *testing.T) {
|
||||
fx := newSyncHandlerFixture(t)
|
||||
defer fx.stop()
|
||||
treeId := "treeId"
|
||||
senderId := "senderId"
|
||||
replyId := "replyId"
|
||||
chWithId := &treechangeproto.RawTreeChangeWithId{}
|
||||
fullSyncRequest := &treechangeproto.TreeFullSyncRequest{
|
||||
Heads: []string{"h1"},
|
||||
SnapshotPath: []string{"h1"},
|
||||
}
|
||||
treeMsg := treechangeproto.WrapFullRequest(fullSyncRequest, chWithId)
|
||||
objectMsg, _ := marshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
objectMsg.RequestId = replyId
|
||||
fullResponse := &treechangeproto.TreeSyncMessage{}
|
||||
|
||||
fx.objectTreeMock.EXPECT().
|
||||
Id().AnyTimes().Return(treeId)
|
||||
fx.objectTreeMock.EXPECT().Header().Return(nil)
|
||||
fx.syncClientMock.EXPECT().
|
||||
CreateFullSyncResponse(gomock.Eq(fx.objectTreeMock), gomock.Eq([]string{"h1"}), gomock.Eq([]string{"h1"})).
|
||||
Return(fullResponse, nil)
|
||||
fx.syncClientMock.EXPECT().SendWithReply(gomock.Any(), gomock.Eq(senderId), gomock.Eq(fullResponse), gomock.Eq(replyId))
|
||||
|
||||
err := fx.syncHandler.HandleMessage(ctx, senderId, objectMsg)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("full sync request with add raw changes error", func(t *testing.T) {
|
||||
fx := newSyncHandlerFixture(t)
|
||||
defer fx.stop()
|
||||
treeId := "treeId"
|
||||
senderId := "senderId"
|
||||
chWithId := &treechangeproto.RawTreeChangeWithId{}
|
||||
fullSyncRequest := &treechangeproto.TreeFullSyncRequest{
|
||||
Heads: []string{"h1"},
|
||||
Changes: []*treechangeproto.RawTreeChangeWithId{chWithId},
|
||||
SnapshotPath: []string{"h1"},
|
||||
}
|
||||
treeMsg := treechangeproto.WrapFullRequest(fullSyncRequest, chWithId)
|
||||
objectMsg, _ := marshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
|
||||
fx.objectTreeMock.EXPECT().
|
||||
Id().AnyTimes().Return(treeId)
|
||||
fx.objectTreeMock.EXPECT().Header().Return(nil)
|
||||
fx.objectTreeMock.EXPECT().
|
||||
Heads().
|
||||
Return([]string{"h2"})
|
||||
fx.objectTreeMock.EXPECT().
|
||||
HasChanges(gomock.Eq([]string{"h1"})).
|
||||
Return(false)
|
||||
fx.objectTreeMock.EXPECT().
|
||||
AddRawChanges(gomock.Any(), gomock.Eq(objecttree.RawChangesPayload{
|
||||
NewHeads: []string{"h1"},
|
||||
RawChanges: []*treechangeproto.RawTreeChangeWithId{chWithId},
|
||||
})).
|
||||
Return(objecttree.AddResult{}, fmt.Errorf(""))
|
||||
fx.syncClientMock.EXPECT().SendWithReply(gomock.Any(), gomock.Eq(senderId), gomock.Any(), gomock.Eq(""))
|
||||
|
||||
err := fx.syncHandler.HandleMessage(ctx, senderId, objectMsg)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSyncHandler_HandleFullSyncResponse(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
log = logger.CtxLogger{Logger: zap.NewNop()}
|
||||
|
||||
t.Run("full sync response with change", func(t *testing.T) {
|
||||
fx := newSyncHandlerFixture(t)
|
||||
defer fx.stop()
|
||||
treeId := "treeId"
|
||||
senderId := "senderId"
|
||||
replyId := "replyId"
|
||||
chWithId := &treechangeproto.RawTreeChangeWithId{}
|
||||
fullSyncResponse := &treechangeproto.TreeFullSyncResponse{
|
||||
Heads: []string{"h3"},
|
||||
Heads: []string{"h1"},
|
||||
Changes: []*treechangeproto.RawTreeChangeWithId{chWithId},
|
||||
SnapshotPath: []string{"h1"},
|
||||
}
|
||||
treeMsg := treechangeproto.WrapFullResponse(fullSyncResponse, chWithId)
|
||||
objectMsg, _ := MarshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
objectMsg, _ := marshallTreeMessage(treeMsg, "spaceId", treeId, replyId)
|
||||
|
||||
fx.syncHandler.heads = []string{"h2"}
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(fx.treeId)
|
||||
fx.objectTreeMock.EXPECT().Heads().Times(2).Return([]string{"h2"})
|
||||
fx.objectTreeMock.EXPECT().Heads().Times(2).Return([]string{"h3"})
|
||||
fx.syncProtocolMock.EXPECT().FullSyncResponse(ctx, fx.senderId, gomock.Any()).Return(nil)
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(treeId)
|
||||
fx.objectTreeMock.EXPECT().
|
||||
Heads().
|
||||
Return([]string{"h2"}).AnyTimes()
|
||||
fx.objectTreeMock.EXPECT().
|
||||
HasChanges(gomock.Eq([]string{"h1"})).
|
||||
Return(false)
|
||||
fx.objectTreeMock.EXPECT().
|
||||
AddRawChanges(gomock.Any(), gomock.Eq(objecttree.RawChangesPayload{
|
||||
NewHeads: []string{"h1"},
|
||||
RawChanges: []*treechangeproto.RawTreeChangeWithId{chWithId},
|
||||
})).
|
||||
Return(objecttree.AddResult{}, nil)
|
||||
|
||||
err := fx.syncHandler.HandleMessage(ctx, fx.senderId, objectMsg)
|
||||
err := fx.syncHandler.HandleMessage(ctx, senderId, objectMsg)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSyncTreeHandler_HandleRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("handle request", func(t *testing.T) {
|
||||
t.Run("full sync response with same heads", func(t *testing.T) {
|
||||
fx := newSyncHandlerFixture(t)
|
||||
defer fx.stop()
|
||||
treeId := "treeId"
|
||||
senderId := "senderId"
|
||||
replyId := "replyId"
|
||||
chWithId := &treechangeproto.RawTreeChangeWithId{}
|
||||
fullRequest := &treechangeproto.TreeFullSyncRequest{}
|
||||
treeMsg := treechangeproto.WrapFullRequest(fullRequest, chWithId)
|
||||
objectMsg, _ := MarshallTreeMessage(treeMsg, "spaceId", treeId, "")
|
||||
|
||||
syncResp := &treechangeproto.TreeSyncMessage{}
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(fx.treeId)
|
||||
fx.syncProtocolMock.EXPECT().FullSyncRequest(ctx, fx.senderId, gomock.Any()).Return(syncResp, nil)
|
||||
|
||||
res, err := fx.syncHandler.HandleRequest(ctx, fx.senderId, objectMsg)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, res)
|
||||
})
|
||||
|
||||
t.Run("handle other message", func(t *testing.T) {
|
||||
fx := newSyncHandlerFixture(t)
|
||||
defer fx.stop()
|
||||
treeId := "treeId"
|
||||
chWithId := &treechangeproto.RawTreeChangeWithId{}
|
||||
fullResponse := &treechangeproto.TreeFullSyncResponse{}
|
||||
responseMsg := treechangeproto.WrapFullResponse(fullResponse, chWithId)
|
||||
headUpdate := &treechangeproto.TreeHeadUpdate{}
|
||||
headUpdateMsg := treechangeproto.WrapHeadUpdate(headUpdate, chWithId)
|
||||
for _, msg := range []*treechangeproto.TreeSyncMessage{responseMsg, headUpdateMsg} {
|
||||
objectMsg, _ := MarshallTreeMessage(msg, "spaceId", treeId, "")
|
||||
|
||||
_, err := fx.syncHandler.HandleRequest(ctx, fx.senderId, objectMsg)
|
||||
require.Equal(t, err, ErrMessageIsNotRequest)
|
||||
fullSyncResponse := &treechangeproto.TreeFullSyncResponse{
|
||||
Heads: []string{"h1"},
|
||||
Changes: []*treechangeproto.RawTreeChangeWithId{chWithId},
|
||||
SnapshotPath: []string{"h1"},
|
||||
}
|
||||
treeMsg := treechangeproto.WrapFullResponse(fullSyncResponse, chWithId)
|
||||
objectMsg, _ := marshallTreeMessage(treeMsg, "spaceId", treeId, replyId)
|
||||
|
||||
fx.objectTreeMock.EXPECT().Id().AnyTimes().Return(treeId)
|
||||
fx.objectTreeMock.EXPECT().
|
||||
Heads().
|
||||
Return([]string{"h1"}).AnyTimes()
|
||||
|
||||
err := fx.syncHandler.HandleMessage(ctx, senderId, objectMsg)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -2,19 +2,15 @@ package synctree
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anyproto/any-sync/commonspace/spacestorage"
|
||||
"github.com/anyproto/any-sync/net/peer"
|
||||
"fmt"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/objecttree"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||
"github.com/anytypeio/any-sync/commonspace/spacestorage"
|
||||
"github.com/anytypeio/any-sync/net/peer"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoResponsiblePeers = errors.New("no responsible peers")
|
||||
"time"
|
||||
)
|
||||
|
||||
type treeRemoteGetter struct {
|
||||
@@ -39,7 +35,7 @@ func (t treeRemoteGetter) getPeers(ctx context.Context) (peerIds []string, err e
|
||||
return
|
||||
}
|
||||
if len(respPeers) == 0 {
|
||||
err = ErrNoResponsiblePeers
|
||||
err = fmt.Errorf("no responsible peers")
|
||||
return
|
||||
}
|
||||
for _, p := range respPeers {
|
||||
@@ -49,8 +45,13 @@ func (t treeRemoteGetter) getPeers(ctx context.Context) (peerIds []string, err e
|
||||
}
|
||||
|
||||
func (t treeRemoteGetter) treeRequest(ctx context.Context, peerId string) (msg *treechangeproto.TreeSyncMessage, err error) {
|
||||
newTreeRequest := t.deps.SyncClient.CreateNewTreeRequest()
|
||||
resp, err := t.deps.SyncClient.SendRequest(ctx, peerId, t.treeId, newTreeRequest)
|
||||
newTreeRequest := GetRequestFactory().CreateNewTreeRequest()
|
||||
objMsg, err := marshallTreeMessage(newTreeRequest, t.deps.SpaceId, t.treeId, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := t.deps.ObjectSync.MessagePool().SendSync(ctx, peerId, objMsg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -60,20 +61,45 @@ func (t treeRemoteGetter) treeRequest(ctx context.Context, peerId string) (msg *
|
||||
return
|
||||
}
|
||||
|
||||
func (t treeRemoteGetter) treeRequestLoop(ctx context.Context) (msg *treechangeproto.TreeSyncMessage, err error) {
|
||||
availablePeers, err := t.getPeers(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
func (t treeRemoteGetter) treeRequestLoop(ctx context.Context, wait bool) (msg *treechangeproto.TreeSyncMessage, err error) {
|
||||
peerIdx := 0
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("waiting for object %s interrupted, context closed", t.treeId)
|
||||
default:
|
||||
break
|
||||
}
|
||||
availablePeers, err := t.getPeers(ctx)
|
||||
if err != nil {
|
||||
if !wait {
|
||||
return nil, err
|
||||
}
|
||||
select {
|
||||
// wait for peers to connect
|
||||
case <-time.After(1 * time.Second):
|
||||
continue Loop
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("waiting for object %s interrupted, context closed", t.treeId)
|
||||
}
|
||||
}
|
||||
|
||||
peerIdx = peerIdx % len(availablePeers)
|
||||
msg, err = t.treeRequest(ctx, availablePeers[peerIdx])
|
||||
if err == nil || !wait {
|
||||
return msg, err
|
||||
}
|
||||
peerIdx++
|
||||
}
|
||||
// in future we will try to load from different peers
|
||||
return t.treeRequest(ctx, availablePeers[0])
|
||||
}
|
||||
|
||||
func (t treeRemoteGetter) getTree(ctx context.Context) (treeStorage treestorage.TreeStorage, isRemote bool, err error) {
|
||||
func (t treeRemoteGetter) getTree(ctx context.Context) (treeStorage treestorage.TreeStorage, err error) {
|
||||
treeStorage, err = t.deps.SpaceStorage.TreeStorage(t.treeId)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil && err != treestorage.ErrUnknownTreeId {
|
||||
return
|
||||
}
|
||||
@@ -87,16 +113,15 @@ func (t treeRemoteGetter) getTree(ctx context.Context) (treeStorage treestorage.
|
||||
return
|
||||
}
|
||||
|
||||
isRemote = true
|
||||
resp, err := t.treeRequestLoop(ctx)
|
||||
resp, err := t.treeRequestLoop(ctx, t.deps.WaitTreeRemoteSync)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fullSyncResp := resp.GetContent().GetFullSyncResponse()
|
||||
if fullSyncResp == nil {
|
||||
err = treechangeproto.ErrUnexpected
|
||||
if resp.GetContent().GetFullSyncResponse() == nil {
|
||||
err = fmt.Errorf("expected to get full sync response, but got something else")
|
||||
return
|
||||
}
|
||||
fullSyncResp := resp.GetContent().GetFullSyncResponse()
|
||||
|
||||
payload := treestorage.TreeStorageCreatePayload{
|
||||
RootRawChange: resp.RootChange,
|
||||
@@ -111,6 +136,5 @@ func (t treeRemoteGetter) getTree(ctx context.Context) (treeStorage treestorage.
|
||||
return
|
||||
}
|
||||
// now we are sure that we can save it to the storage
|
||||
treeStorage, err = t.deps.SpaceStorage.CreateTreeStorage(payload)
|
||||
return
|
||||
return t.deps.SpaceStorage.CreateTreeStorage(payload)
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package synctree
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/synctree/mock_synctree"
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
|
||||
"github.com/anyproto/any-sync/commonspace/peermanager/mock_peermanager"
|
||||
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
|
||||
"github.com/anyproto/any-sync/net/peer"
|
||||
"github.com/anyproto/any-sync/net/peer/mock_peer"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
type treeRemoteGetterFixture struct {
|
||||
ctrl *gomock.Controller
|
||||
|
||||
treeGetter treeRemoteGetter
|
||||
syncClientMock *mock_synctree.MockSyncClient
|
||||
peerGetterMock *mock_peermanager.MockPeerManager
|
||||
}
|
||||
|
||||
func newTreeRemoteGetterFixture(t *testing.T) *treeRemoteGetterFixture {
|
||||
ctrl := gomock.NewController(t)
|
||||
syncClientMock := mock_synctree.NewMockSyncClient(ctrl)
|
||||
peerGetterMock := mock_peermanager.NewMockPeerManager(ctrl)
|
||||
treeGetter := treeRemoteGetter{
|
||||
deps: BuildDeps{
|
||||
SyncClient: syncClientMock,
|
||||
PeerGetter: peerGetterMock,
|
||||
},
|
||||
treeId: "treeId",
|
||||
}
|
||||
return &treeRemoteGetterFixture{
|
||||
ctrl: ctrl,
|
||||
treeGetter: treeGetter,
|
||||
syncClientMock: syncClientMock,
|
||||
peerGetterMock: peerGetterMock,
|
||||
}
|
||||
}
|
||||
|
||||
func (fx *treeRemoteGetterFixture) stop() {
|
||||
fx.ctrl.Finish()
|
||||
}
|
||||
|
||||
func TestTreeRemoteGetter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
peerId := "peerId"
|
||||
treeRequest := &treechangeproto.TreeSyncMessage{}
|
||||
treeResponse := &treechangeproto.TreeSyncMessage{
|
||||
RootChange: &treechangeproto.RawTreeChangeWithId{Id: "id"},
|
||||
}
|
||||
marshalled, _ := proto.Marshal(treeResponse)
|
||||
objectResponse := &spacesyncproto.ObjectSyncMessage{
|
||||
Payload: marshalled,
|
||||
}
|
||||
|
||||
t.Run("request works", func(t *testing.T) {
|
||||
fx := newTreeRemoteGetterFixture(t)
|
||||
defer fx.stop()
|
||||
mockPeer := mock_peer.NewMockPeer(fx.ctrl)
|
||||
mockPeer.EXPECT().Id().AnyTimes().Return(peerId)
|
||||
fx.peerGetterMock.EXPECT().GetResponsiblePeers(ctx).Return([]peer.Peer{mockPeer}, nil)
|
||||
fx.syncClientMock.EXPECT().CreateNewTreeRequest().Return(treeRequest)
|
||||
fx.syncClientMock.EXPECT().SendRequest(ctx, peerId, fx.treeGetter.treeId, treeRequest).Return(objectResponse, nil)
|
||||
resp, err := fx.treeGetter.treeRequestLoop(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "id", resp.RootChange.Id)
|
||||
})
|
||||
|
||||
t.Run("request fails", func(t *testing.T) {
|
||||
fx := newTreeRemoteGetterFixture(t)
|
||||
defer fx.stop()
|
||||
treeRequest := &treechangeproto.TreeSyncMessage{}
|
||||
mockPeer := mock_peer.NewMockPeer(fx.ctrl)
|
||||
mockPeer.EXPECT().Id().AnyTimes().Return(peerId)
|
||||
fx.peerGetterMock.EXPECT().GetResponsiblePeers(ctx).Return([]peer.Peer{mockPeer}, nil)
|
||||
fx.syncClientMock.EXPECT().CreateNewTreeRequest().Return(treeRequest)
|
||||
fx.syncClientMock.EXPECT().SendRequest(ctx, peerId, fx.treeGetter.treeId, treeRequest).AnyTimes().Return(nil, fmt.Errorf("some"))
|
||||
_, err := fx.treeGetter.treeRequestLoop(ctx)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
Loaded 100 of 277 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user