Compare commits
669
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0945304a79 | ||
|
|
9703dd9f57 | ||
|
|
259e9ef08e | ||
|
|
6a0c6128d8 | ||
|
|
b05ece0681 | ||
|
|
e9133ef82b | ||
|
|
67ba20d777 | ||
|
|
21da0d7890 | ||
|
|
ebbe57d0ab | ||
|
|
f4abc71dcc | ||
|
|
8aa02da93a | ||
|
|
0e560486db | ||
|
|
57d413099d | ||
|
|
1fdf07933c | ||
|
|
c50678618f | ||
|
|
8094b459e4 | ||
|
|
6765867a2e | ||
|
|
4fb8ee6a6f | ||
|
|
674834ccf4 | ||
|
|
3bd2bff23b | ||
|
|
40660c5fb7 | ||
|
|
d940d094a1 | ||
|
|
9d67727989 | ||
|
|
6f51700a7f | ||
|
|
598915193a | ||
|
|
905e5ec07f | ||
|
|
4075b886d0 | ||
|
|
cade790198 | ||
|
|
98555a9a80 | ||
|
|
48b757cac1 | ||
|
|
58c85fc9db | ||
|
|
ddd98a818a | ||
|
|
64b5a1b738 | ||
|
|
1a131a56f2 | ||
|
|
beda37f28b | ||
|
|
2532ac35cf | ||
|
|
bcd30ceaec | ||
|
|
9a3493c2f4 | ||
|
|
fa404d5a0d | ||
|
|
73ad18fbfb | ||
|
|
1dd264894a | ||
|
|
8c3d2f3bc5 | ||
|
|
702ed8ecc1 | ||
|
|
b038650810 | ||
|
|
a16bf555c0 | ||
|
|
cd6ea60fa1 | ||
|
|
0bf21d9db2 | ||
|
|
f61843ef2e | ||
|
|
23e8366f8d | ||
|
|
93e72cc83f | ||
|
|
190dff142c | ||
|
|
c667ada63a | ||
|
|
93ae30d889 | ||
|
|
486eebc4ac | ||
|
|
ff33d976d1 | ||
|
|
69890b4282 | ||
|
|
533c9a6ab0 | ||
|
|
9521bb3931 | ||
|
|
e46a0f99c3 | ||
|
|
ed97e365b2 | ||
|
|
b4776ea4e0 | ||
|
|
b5ffd0a796 | ||
|
|
c74299b59a | ||
|
|
8b6d837483 | ||
|
|
3e74b3dee2 | ||
|
|
2902da996c | ||
|
|
f6f144bf17 | ||
|
|
ab5c42f4a0 | ||
|
|
7db3f7eaac | ||
|
|
f0b666269b | ||
|
|
190a59842c | ||
|
|
40888c1a66 | ||
|
|
fa0d933e49 | ||
|
|
8372c0288f | ||
|
|
5f5d672a7d | ||
|
|
d23cd197e1 | ||
|
|
d7ca483df1 | ||
|
|
e48be98cd5 | ||
|
|
e9a2ff3aa6 | ||
|
|
2301f72c5b | ||
|
|
f7c8efd93c | ||
|
|
3e7ccf7c48 | ||
|
|
6bc2784e9a | ||
|
|
f15d50c2e8 | ||
|
|
f9007ed106 | ||
|
|
05cc6b0f43 | ||
|
|
1efcfeb3ad | ||
|
|
93195911bd | ||
|
|
6085e3a5eb | ||
|
|
e5b72da607 | ||
|
|
0d6117d585 | ||
|
|
629971687d | ||
|
|
3c955a9706 | ||
|
|
7f3c8dbff1 | ||
|
|
7762e39fb3 | ||
|
|
4235b2c406 | ||
|
|
3fd090bfa7 | ||
|
|
6dfa54efa6 | ||
|
|
67575e1736 | ||
|
|
65923fc255 | ||
|
|
aea763868f | ||
|
|
26b134ae7b | ||
|
|
893071d2ba | ||
|
|
435f2d2178 | ||
|
|
8461ca539b | ||
|
|
fb977dc61d | ||
|
|
ee7ab4ce25 | ||
|
|
c3ce9713d9 | ||
|
|
6a147091c5 | ||
|
|
6208c36417 | ||
|
|
453fd20eeb | ||
|
|
28f0cffdb6 | ||
|
|
87c16c6cf5 | ||
|
|
5495c98e63 | ||
|
|
da7d5ce608 | ||
|
|
b300c297c6 | ||
|
|
124673f7a8 | ||
|
|
0395cf2bc0 | ||
|
|
36cd70040a | ||
|
|
1fbd396ffa | ||
|
|
2834bad85e | ||
|
|
516f3e29e8 | ||
|
|
ab20c16982 | ||
|
|
0c1df81ee9 | ||
|
|
d324b2ac86 | ||
|
|
0231089b99 | ||
|
|
74ffb85467 | ||
|
|
dcd280e6e2 | ||
|
|
4e56dbd883 | ||
|
|
13d7881b80 | ||
|
|
1d2c53bf3c | ||
|
|
9c449c966b | ||
|
|
ec2d4638e3 | ||
|
|
0ce92befc8 | ||
|
|
2167ce9656 | ||
|
|
0dc85d74aa | ||
|
|
b6e3f8037b | ||
|
|
00e7161a8f | ||
|
|
b5a7879eca | ||
|
|
4355dc69ea | ||
|
|
371ba69447 | ||
|
|
79ef57d0ea | ||
|
|
8bd6bdd397 | ||
|
|
ce3248cea7 | ||
|
|
99a6f3a5b6 | ||
|
|
c2e10dc156 | ||
|
|
fc914f3237 | ||
|
|
811d3752d0 | ||
|
|
00827dd5c1 | ||
|
|
a981c21d27 | ||
|
|
163dc122f3 | ||
|
|
83727e0824 | ||
|
|
529d3ef764 | ||
|
|
fefbf4dcc8 | ||
|
|
b9c6d3ae09 | ||
|
|
7bea8c758a | ||
|
|
479c0d3f16 | ||
|
|
d9ce7c3166 | ||
|
|
69979996d9 | ||
|
|
da58e5c50c | ||
|
|
44e259142f | ||
|
|
77970d5113 | ||
|
|
2b8ee4c7a5 | ||
|
|
be952e5f2d | ||
|
|
43ebac4242 | ||
|
|
f08a0ed01c | ||
|
|
612fdff377 | ||
|
|
8ccb7f1924 | ||
|
|
65d0ca8aa9 | ||
|
|
e82ed6e3d3 | ||
|
|
4b815fc086 | ||
|
|
7eaf843de2 | ||
|
|
110e1ae6f9 | ||
|
|
896f9725ec | ||
|
|
1a529e9d5d | ||
|
|
36ef17df8f | ||
|
|
955ac7775e | ||
|
|
8f69e874c4 | ||
|
|
ac06fd97e9 | ||
|
|
3726b7d112 | ||
|
|
377200591e | ||
|
|
4afc898c2f | ||
|
|
ff7e4fef55 | ||
|
|
9ffddb1923 | ||
|
|
896b857fc4 | ||
|
|
acc5d2675b | ||
|
|
6ece4c1fd2 | ||
|
|
cc09f0170d | ||
|
|
bb234d6c0e | ||
|
|
e6acc64758 | ||
|
|
f18cf545b9 | ||
|
|
6d64daaba3 | ||
|
|
47f48faed7 | ||
|
|
cfa834177b | ||
|
|
c454fc8baa | ||
|
|
dbe7fa9155 | ||
|
|
4d842f7d3b | ||
|
|
0e68221c91 | ||
|
|
19f63c7ea3 | ||
|
|
fb939ec496 | ||
|
|
39df3173d4 | ||
|
|
429672e0b4 | ||
|
|
605fd6d726 | ||
|
|
3c476542d2 | ||
|
|
31874f3ebb | ||
|
|
77942747db | ||
|
|
fe01b396ba | ||
|
|
3583949706 | ||
|
|
23fc22ebc5 | ||
|
|
cba163a1fd | ||
|
|
a8e2c8edb6 | ||
|
|
3e501d9036 | ||
|
|
9ca101756d | ||
|
|
a873d12c65 | ||
|
|
8ff670c564 | ||
|
|
b1ed2802fb | ||
|
|
b70cb580c8 | ||
|
|
28be3ba788 | ||
|
|
d4770ddc77 | ||
|
|
cbe1220680 | ||
|
|
0b95c5fa76 | ||
|
|
0343bca257 | ||
|
|
878016db39 | ||
|
|
1f4fde9525 | ||
|
|
5b9d8a838f | ||
|
|
8b19cb1e11 | ||
|
|
ce1e259bb4 | ||
|
|
2238a288d9 | ||
|
|
c8ee2a5cf6 | ||
|
|
1704827d04 | ||
|
|
a156e88eef | ||
|
|
94d0195b63 | ||
|
|
1616edcee3 | ||
|
|
6505e123bb | ||
|
|
63e4659282 | ||
|
|
f3f5557c8e | ||
|
|
b794726e1f | ||
|
|
3d59740a0a | ||
|
|
66fb65b01f | ||
|
|
5c2fcbfd19 | ||
|
|
f9b72330a8 | ||
|
|
822b6ac36b | ||
|
|
77f7778292 | ||
|
|
aed2c66e52 | ||
|
|
68a1fd010f | ||
|
|
ac8b3342ac | ||
|
|
0ea90dd932 | ||
|
|
718b1ce2b7 | ||
|
|
29f7510f5a | ||
|
|
a7f9ed4a80 | ||
|
|
1baefea410 | ||
|
|
563cec8923 | ||
|
|
a3c340ece9 | ||
|
|
cb24638ec9 | ||
|
|
2fb24dc2cc | ||
|
|
9aa2d2c92f | ||
|
|
d1c5100c98 | ||
|
|
42e677c055 | ||
|
|
27bba2c0c2 | ||
|
|
feff334547 | ||
|
|
713cf357ce | ||
|
|
5342bec1b7 | ||
|
|
7df75e681d | ||
|
|
8dc826b234 | ||
|
|
9ef37e1485 | ||
|
|
7517d18fbb | ||
|
|
42d0fee536 | ||
|
|
2ca9d3b5c5 | ||
|
|
9cde068f2a | ||
|
|
1243083831 | ||
|
|
356c5055ad | ||
|
|
19693734a3 | ||
|
|
17e60b9e0c | ||
|
|
ac22b2d00a | ||
|
|
de0b4270df | ||
|
|
e738af7c56 | ||
|
|
a28441a9bf | ||
|
|
2e313716e5 | ||
|
|
0b5ff1f5f7 | ||
|
|
0fe6d97d3d | ||
|
|
0756e42a85 | ||
|
|
13ebe1c87f | ||
|
|
aea7fa5f22 | ||
|
|
403ce7e597 | ||
|
|
4704d3bc48 | ||
|
|
2794b04243 | ||
|
|
1ce64971fd | ||
|
|
eb6d80eac4 | ||
|
|
a8db3351ae | ||
|
|
23a900e096 | ||
|
|
5a304cf295 | ||
|
|
136b3742bf | ||
|
|
21e0f98fe2 | ||
|
|
2bb5b2244b | ||
|
|
2f281799c1 | ||
|
|
18a58a2ddc | ||
|
|
f283215fce | ||
|
|
495809ac9e | ||
|
|
9ca8addcf7 | ||
|
|
94181ade23 | ||
|
|
e50933433e | ||
|
|
a2b8f2361e | ||
|
|
4b60e86d02 | ||
|
|
d6b5676603 | ||
|
|
3821b6ceee | ||
|
|
973585e97d | ||
|
|
ba6ac2f604 | ||
|
|
57d399317e | ||
|
|
f2d6722348 | ||
|
|
7b1b77e50d | ||
|
|
06914b872b | ||
|
|
f6df8b40b4 | ||
|
|
e057f5ee9a | ||
|
|
a5bf110d90 | ||
|
|
debbe726e0 | ||
|
|
ec3e474a53 | ||
|
|
ebb1edc652 | ||
|
|
6204670c66 | ||
|
|
ff9b24f388 | ||
|
|
01b820dc78 | ||
|
|
79ae24df76 | ||
|
|
61b94b9ea5 | ||
|
|
6fb3c5ccf2 | ||
|
|
2e7c03420f | ||
|
|
faa56b4bb7 | ||
|
|
d7ba5316b8 | ||
|
|
bdfd0f0548 | ||
|
|
5d27185083 | ||
|
|
4dfb9d7c83 | ||
|
|
2f15670094 | ||
|
|
b49137ce36 | ||
|
|
7e4e65ebf5 | ||
|
|
8c8167a4ab | ||
|
|
73cc5553b6 | ||
|
|
2ab2488274 | ||
|
|
eb9cd363d0 | ||
|
|
7fe3906534 | ||
|
|
5fdab1bf11 | ||
|
|
13a6d43f0b | ||
|
|
ac942e2481 | ||
|
|
bbd2a7fbc5 | ||
|
|
07a9fa2dbd | ||
|
|
aa559bf496 | ||
|
|
2d968d46b7 | ||
|
|
86c4cafc96 | ||
|
|
c4dfb66d84 | ||
|
|
f4d160684b | ||
|
|
b76e6ce70d | ||
|
|
6b4028eede | ||
|
|
ad81ac8da7 | ||
|
|
7ebeaefe77 | ||
|
|
e1dd36561d | ||
|
|
96c30f8387 | ||
|
|
fc8b353011 | ||
|
|
416b9e8924 | ||
|
|
9f6d732587 | ||
|
|
f2f5786b33 | ||
|
|
eb617865d2 | ||
|
|
a49e318d25 | ||
|
|
e74674a019 | ||
|
|
d98fa474ae | ||
|
|
f16817632f | ||
|
|
bda4016109 | ||
|
|
8f5d07bd09 | ||
|
|
302b352d78 | ||
|
|
45beb28fa5 | ||
|
|
ee9b20e47a | ||
|
|
0f55d5fc3e | ||
|
|
35e153625c | ||
|
|
d5e1b99e6c | ||
|
|
3297624037 | ||
|
|
445e8cc532 | ||
|
|
e041877488 | ||
|
|
36e08f8eee | ||
|
|
8b321387c0 | ||
|
|
8edd67a569 | ||
|
|
e829a63295 | ||
|
|
6881a6897a | ||
|
|
9387e107b3 | ||
|
|
aa901790b9 | ||
|
|
17df4b8634 | ||
|
|
34ef30dd5c | ||
|
|
fc1c7a3c49 | ||
|
|
2abfefc18c | ||
|
|
86a08eb87d | ||
|
|
d330d65859 | ||
|
|
4e1831fa3a | ||
|
|
2f3eacdb6c | ||
|
|
1ce2af1238 | ||
|
|
683b48182c | ||
|
|
795aed306c | ||
|
|
cdefa535ed | ||
|
|
91084b83b4 | ||
|
|
5360e7153b | ||
|
|
5d0ca19350 | ||
|
|
e8d3529fed | ||
|
|
935a28c961 | ||
|
|
d21a2de055 | ||
|
|
6f1023665c | ||
|
|
c53a1f210c | ||
|
|
b71a930bfc | ||
|
|
a2cbc62521 | ||
|
|
768fd6bff8 | ||
|
|
48883e0e32 | ||
|
|
30fe2cf514 | ||
|
|
3850a08252 | ||
|
|
d0e407f3c3 | ||
|
|
16db6fcf3d | ||
|
|
4c5528bd0e | ||
|
|
d42fff1016 | ||
|
|
a28de73031 | ||
|
|
75310b58a0 | ||
|
|
8064957270 | ||
|
|
c1ec9a8826 | ||
|
|
1625b44892 | ||
|
|
d51760f410 | ||
|
|
7b1932d64e | ||
|
|
5bfc540c88 | ||
|
|
4cba99fcd4 | ||
|
|
2ae15aa454 | ||
|
|
47bcf4f8f4 | ||
|
|
a8b9096353 | ||
|
|
5328380691 | ||
|
|
6069cf39e5 | ||
|
|
1f7d236742 | ||
|
|
8e9ee3fbe8 | ||
|
|
35b0afc131 | ||
|
|
958ff67ccc | ||
|
|
13d9317a38 | ||
|
|
b184d46d8a | ||
|
|
3f32c5cb4b | ||
|
|
5c65a1bc83 | ||
|
|
d0a6dc5b13 | ||
|
|
439c6c5b7c | ||
|
|
aaee0c126b | ||
|
|
f3bd4d71de | ||
|
|
876d056705 | ||
|
|
e988978fa1 | ||
|
|
d5deede7a1 | ||
|
|
4f70f5c280 | ||
|
|
a1ad020b63 | ||
|
|
8f1b0df74b | ||
|
|
0f8dc6c1d3 | ||
|
|
8ae9db3b2d | ||
|
|
e477777f49 | ||
|
|
7a132bdf24 | ||
|
|
5e2b7825dc | ||
|
|
6d30c109e4 | ||
|
|
58bd931d90 | ||
|
|
854499382e | ||
|
|
cb4c1f9ad2 | ||
|
|
b452fb3ad2 | ||
|
|
c17a1fea77 | ||
|
|
d50511c5c6 | ||
|
|
bae6d5f375 | ||
|
|
b5082f6af8 | ||
|
|
9666e9701b | ||
|
|
86e1c5ff18 | ||
|
|
16ae1fbe5e | ||
|
|
11f508d9be | ||
|
|
9ce6a73f42 | ||
|
|
c5a991cf0a | ||
|
|
14569f12d3 | ||
|
|
a405c21ebb | ||
|
|
dc6a10dff4 | ||
|
|
d4c2acf6f6 | ||
|
|
483ecada80 | ||
|
|
9553365d31 | ||
|
|
5eb20580b1 | ||
|
|
ea1ea366d2 | ||
|
|
6e4574a9f7 | ||
|
|
3d0da5ac60 | ||
|
|
9f8e6966d8 | ||
|
|
a64ae36bcc | ||
|
|
690b55360f | ||
|
|
2f6187dc0e | ||
|
|
8294870ffc | ||
|
|
ac2e444a97 | ||
|
|
4f6b86a1c0 | ||
|
|
516c057d43 | ||
|
|
d644dce4e7 | ||
|
|
7c579880eb | ||
|
|
296db314f5 | ||
|
|
a8486b0468 | ||
|
|
f8a7a034a7 | ||
|
|
ceae56a860 | ||
|
|
dcafd6ec72 | ||
|
|
8619a03f01 | ||
|
|
b91d7711aa | ||
|
|
9940c91ebf | ||
|
|
80a577b025 | ||
|
|
d672175ce4 | ||
|
|
a44b31d173 | ||
|
|
70065e6b13 | ||
|
|
adbb3ed2e9 | ||
|
|
6ed9c0c34c | ||
|
|
19bbf4f6bf | ||
|
|
cf46bf0297 | ||
|
|
051cbdc713 | ||
|
|
58d1f3a471 | ||
|
|
c9dfd75d8e | ||
|
|
f47de83914 | ||
|
|
caedb19307 | ||
|
|
e860d3b974 | ||
|
|
ed66fba42b | ||
|
|
415f320005 | ||
|
|
4812600098 | ||
|
|
5ff11ce142 | ||
|
|
541572781b | ||
|
|
e38679d9bf | ||
|
|
f25a169c4c | ||
|
|
06ac10ee37 | ||
|
|
7c0223bd06 | ||
|
|
c6334e61aa | ||
|
|
38bbdebffa | ||
|
|
e80d04845e | ||
|
|
4138e22898 | ||
|
|
c42c0e7ceb | ||
|
|
5118538179 | ||
|
|
4d93648f75 | ||
|
|
29f100c162 | ||
|
|
cd98a43b80 | ||
|
|
4bf982376e | ||
|
|
29056d5873 | ||
|
|
2abba1dfb0 | ||
|
|
325b3b114f | ||
|
|
03590e5ac7 | ||
|
|
95b3c26da7 | ||
|
|
3e5f0b1d0e | ||
|
|
3130af3773 | ||
|
|
abd89f15f7 | ||
|
|
a80e6be353 | ||
|
|
acc532fc60 | ||
|
|
3cc3fb7504 | ||
|
|
a04cc95005 | ||
|
|
480fa4b915 | ||
|
|
92a4931850 | ||
|
|
bdfef9010f | ||
|
|
467522d04d | ||
|
|
3147285c60 | ||
|
|
acd767b30b | ||
|
|
40b3b9ad15 | ||
|
|
c2c6133aa5 | ||
|
|
ccec8a4cdb | ||
|
|
cbf0e31f69 | ||
|
|
c40dae315b | ||
|
|
ac0ce1c38f | ||
|
|
72c683aaca | ||
|
|
8042bd1a54 | ||
|
|
462389934b | ||
|
|
b347c14bd1 | ||
|
|
8dfec6983b | ||
|
|
9ebf2dae7b | ||
|
|
a8cacdca94 | ||
|
|
8b87cd5229 | ||
|
|
e09146ee03 | ||
|
|
b9c08d3814 | ||
|
|
58042b3129 | ||
|
|
eed12f3ec5 | ||
|
|
5323928159 | ||
|
|
97625ccc26 | ||
|
|
4fe746d9aa | ||
|
|
4f8cdd41ee | ||
|
|
406e3646e5 | ||
|
|
9d21b91124 | ||
|
|
b806026990 | ||
|
|
341b79814e | ||
|
|
319916124b | ||
|
|
b08b99e284 | ||
|
|
f565df628c | ||
|
|
855c6dc67b | ||
|
|
dc5e10fa2c | ||
|
|
b857e57a35 | ||
|
|
f42f041f53 | ||
|
|
6b6b2c6194 | ||
|
|
d70eb569f2 | ||
|
|
21c074cc2c | ||
|
|
f23c41221b | ||
|
|
24e230d455 | ||
|
|
31daa20367 | ||
|
|
e4d0f9dd6c | ||
|
|
df2ac7aaeb | ||
|
|
b96b23957b | ||
|
|
265ce139c5 | ||
|
|
48c95eb41d | ||
|
|
937895be69 | ||
|
|
a3886f778d | ||
|
|
6aecc2622c | ||
|
|
c55b205a0b | ||
|
|
2fcf7006e6 | ||
|
|
bf61e485a6 | ||
|
|
b2886f11b1 | ||
|
|
11ece5d89e | ||
|
|
cf68dfac43 | ||
|
|
c44de2cd58 | ||
|
|
6ff5ed6d23 | ||
|
|
25ec2b63ab | ||
|
|
c5ab71d7a5 | ||
|
|
1fc4c9d9c5 | ||
|
|
bebf2c259c | ||
|
|
b7d526903e | ||
|
|
92917c9f62 | ||
|
|
fdde05cf12 | ||
|
|
a7d7325e9b | ||
|
|
465823237f | ||
|
|
5a1f996e56 | ||
|
|
229b6a292c | ||
|
|
c84e47a7b2 | ||
|
|
daf7baaeff | ||
|
|
a4a1231e92 | ||
|
|
b99dee3ac3 | ||
|
|
89fc69249b | ||
|
|
d421d66a3f | ||
|
|
27aba3567b | ||
|
|
5532532db9 | ||
|
|
c369f8abb2 | ||
|
|
9e52f6cf2f | ||
|
|
d22a38d947 | ||
|
|
439fa6c848 | ||
|
|
6b475bdb78 | ||
|
|
7d56fba321 | ||
|
|
bf6ffbbd67 | ||
|
|
a972811f54 | ||
|
|
88da67d7c3 | ||
|
|
1f07e05470 | ||
|
|
5cab08a36a | ||
|
|
73c52eafb6 | ||
|
|
be5961f59b | ||
|
|
4e2bb58e2d | ||
|
|
ae176ea9cd | ||
|
|
2b17db8aa3 | ||
|
|
81a4b22d43 | ||
|
|
f7da96fb82 | ||
|
|
f5e5af391a | ||
|
|
5a3ac86c3f | ||
|
|
3d78ff9f68 | ||
|
|
f3127a66ee | ||
|
|
90b4711ad2 | ||
|
|
716b42103a | ||
|
|
405cdedcd3 | ||
|
|
0b3a101ccd | ||
|
|
089320aadc | ||
|
|
b2fb2ef276 | ||
|
|
e136d11dce | ||
|
|
3adfe2f91f | ||
|
|
1103a27337 | ||
|
|
ddce692f72 | ||
|
|
66faea7712 | ||
|
|
7e31ec5417 | ||
|
|
a4fa764b7d | ||
|
|
7226b8456b | ||
|
|
dae5eab787 | ||
|
|
f38e9628a1 | ||
|
|
43e3b12e29 | ||
|
|
aa01ff5d50 | ||
|
|
63503e0c98 | ||
|
|
947dd0db09 | ||
|
|
218b6e5193 | ||
|
|
9f131eee6b | ||
|
|
09efe03e1d | ||
|
|
0f87607cd5 | ||
|
|
9b660c1959 | ||
|
|
c867a5f5b3 | ||
|
|
1886b47031 | ||
|
|
f59ffc8ddd | ||
|
|
61444960bc | ||
|
|
30bb8f2116 | ||
|
|
4a8c691aef | ||
|
|
e8dafb979c |
@@ -1,2 +0,0 @@
|
|||||||
/AUTHORS @calmh
|
|
||||||
/*.md @calmh
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: If you're just not sure how to do something, see "ask a question".
|
|
||||||
labels: enhancement, needs-triage
|
|
||||||
---
|
|
||||||
|
|
||||||
### Include required information
|
|
||||||
|
|
||||||
Please be sure to include at least:
|
|
||||||
|
|
||||||
- what problem your new feature would solve
|
|
||||||
- how or why you think it is generally useful (i.e., not just for you)
|
|
||||||
- what alternatives or workarounds you considered
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: File a new feature request
|
||||||
|
labels: ["enhancement", "needs-triage"]
|
||||||
|
type: Feature
|
||||||
|
body:
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: feature
|
||||||
|
attributes:
|
||||||
|
label: Feature description
|
||||||
|
description: Please describe the behavior you'd like to see.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem-usecase
|
||||||
|
attributes:
|
||||||
|
label: Problem or use case
|
||||||
|
description: Please explain which problem this would solve, or what the use case is for the feature. Keep in mind that it's more likely to be implemented if it's generally useful for a larger number of users.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives or workarounds
|
||||||
|
description: Please describe any alternatives or workarounds you have considered and, possibly, rejected.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: If you're actually looking for support, see "ask a question".
|
|
||||||
labels: bug, needs-triage
|
|
||||||
---
|
|
||||||
|
|
||||||
### Does your log mention database corruption?
|
|
||||||
|
|
||||||
If your Syncthing log reports panics because of database corruption it is
|
|
||||||
most likely a fault with your system's storage or memory. Affected log
|
|
||||||
entries will contain lines starting with `panic: leveldb`. You will need to
|
|
||||||
delete the index database to clear this, by running `syncthing
|
|
||||||
-reset-database`.
|
|
||||||
|
|
||||||
### Include required information
|
|
||||||
|
|
||||||
Please be sure to include at least:
|
|
||||||
|
|
||||||
- which version of Syncthing and what operating system you are using
|
|
||||||
- browser and version, if applicable
|
|
||||||
- what happened,
|
|
||||||
- what you expected to happen instead, and
|
|
||||||
- any steps to reproduce the problem.
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: If you're actually looking for support instead, see "I need help / I have a question".
|
||||||
|
labels: ["bug", "needs-triage"]
|
||||||
|
type: Bug
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
:no_entry_sign: If you want to report a security issue, please see [our Security Policy](https://syncthing.net/security/) and do not report the issue here.
|
||||||
|
|
||||||
|
:interrobang: If you are not sure if there is a bug, but something isn't working right and you need help, please [use the forum](https://forum.syncthing.net/).
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: Also tell us, what did you expect to happen, and any steps we might use to reproduce the problem.
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Syncthing version
|
||||||
|
description: What version of Syncthing are you running?
|
||||||
|
placeholder: v1.27.4
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform & operating system
|
||||||
|
description: On what platform(s) are you seeing the problem?
|
||||||
|
placeholder: Linux arm64
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: browser
|
||||||
|
attributes:
|
||||||
|
label: Browser version
|
||||||
|
description: If the problem is related to the GUI, describe your browser and version.
|
||||||
|
placeholder: Safari 17.3.1
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Relevant log output
|
||||||
|
description: Please copy and paste any relevant log output or crash backtrace. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: shell
|
||||||
@@ -3,11 +3,11 @@ updates:
|
|||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
- package-ecosystem: "gomod"
|
- package-ecosystem: "gomod"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: monthly
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
version: 1
|
||||||
|
labels:
|
||||||
|
|
||||||
|
- label: enhancement
|
||||||
|
title: ^feat\b
|
||||||
|
|
||||||
|
- label: bug
|
||||||
|
title: ^fix\b
|
||||||
|
|
||||||
|
- label: documentation
|
||||||
|
title: ^docs\b
|
||||||
|
|
||||||
|
- label: chore
|
||||||
|
title: ^chore\b
|
||||||
|
|
||||||
|
- label: chore
|
||||||
|
title: ^refactor\b
|
||||||
|
|
||||||
|
- label: build
|
||||||
|
title: ^build\b
|
||||||
|
|
||||||
|
- label: dependencies
|
||||||
|
title: ^build\(deps\)\b
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
changelog:
|
||||||
|
exclude:
|
||||||
|
labels:
|
||||||
|
- dependencies
|
||||||
|
|
||||||
|
categories:
|
||||||
|
- title: Fixes
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
|
||||||
|
- title: Features
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
|
||||||
|
- title: Other
|
||||||
|
labels:
|
||||||
|
- '*'
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
name: Build Infrastructure Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- infrastructure
|
||||||
|
- infra-*
|
||||||
|
|
||||||
|
env:
|
||||||
|
GO_VERSION: "~1.24.0"
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
BUILD_USER: docker
|
||||||
|
BUILD_HOST: github.syncthing.net
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker-syncthing:
|
||||||
|
name: Build and push Docker images
|
||||||
|
if: github.repository == 'syncthing/syncthing'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: docker
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
pkg:
|
||||||
|
- stcrashreceiver
|
||||||
|
- strelaypoolsrv
|
||||||
|
- stupgrades
|
||||||
|
- ursrv
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build binaries
|
||||||
|
run: |
|
||||||
|
for arch in arm64 amd64; do
|
||||||
|
go run build.go -goos linux -goarch "$arch" build ${{ matrix.pkg }}
|
||||||
|
mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-"$arch"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Set Docker tags (all branches)
|
||||||
|
run: |
|
||||||
|
tags=docker.io/syncthing/${{ matrix.pkg }}:${{ github.sha }},ghcr.io/syncthing/infra/${{ matrix.pkg }}:${{ github.sha }}
|
||||||
|
echo "TAGS=$tags" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set Docker tags (latest)
|
||||||
|
if: github.ref == 'refs/heads/infrastructure'
|
||||||
|
run: |
|
||||||
|
tags=docker.io/syncthing/${{ matrix.pkg }}:latest,ghcr.io/syncthing/infra/${{ matrix.pkg }}:latest,${{ env.TAGS }}
|
||||||
|
echo "TAGS=$tags" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile.${{ matrix.pkg }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.TAGS }}
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.revision=${{ github.sha }}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: Build Syncthing (Nightly)
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run nightly build at 05:00 UTC
|
||||||
|
- cron: '00 05 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-syncthing:
|
||||||
|
uses: ./.github/workflows/build-syncthing.yaml
|
||||||
|
# if we only want nightlies to run for specific users:
|
||||||
|
# if: contains(fromJSON('["syncthing", "calmh"]'), github.repository_owner)
|
||||||
|
secrets: inherit
|
||||||
@@ -3,10 +3,17 @@ name: Build Syncthing
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- release
|
||||||
|
- release-rc*
|
||||||
|
workflow_call:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# The go version to use for builds.
|
# The go version to use for builds. We set check-latest to true when
|
||||||
GO_VERSION: "1.19.6"
|
# installing, so we get the latest patch version that matches the
|
||||||
|
# expression.
|
||||||
|
GO_VERSION: "~1.24.0"
|
||||||
|
|
||||||
# Optimize compatibility on the slow archictures.
|
# Optimize compatibility on the slow archictures.
|
||||||
GO386: softfloat
|
GO386: softfloat
|
||||||
@@ -42,7 +49,7 @@ jobs:
|
|||||||
runner: ["windows-latest", "ubuntu-latest", "macos-latest"]
|
runner: ["windows-latest", "ubuntu-latest", "macos-latest"]
|
||||||
# The oldest version in this list should match what we have in our go.mod.
|
# The oldest version in this list should match what we have in our go.mod.
|
||||||
# Variables don't seem to be supported here, or we could have done something nice.
|
# Variables don't seem to be supported here, or we could have done something nice.
|
||||||
go: ["1.19"] # Skip Go 1.20 for now, https://github.com/syncthing/syncthing/issues/8799
|
go: ["~1.23.0", "~1.24.0"]
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
steps:
|
steps:
|
||||||
- name: Set git to use LF
|
- name: Set git to use LF
|
||||||
@@ -55,42 +62,55 @@ jobs:
|
|||||||
git config --global core.autocrlf false
|
git config --global core.autocrlf false
|
||||||
git config --global core.eol lf
|
git config --global core.eol lf
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
cache: true
|
cache: true
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
go run build.go
|
go run build.go
|
||||||
|
|
||||||
|
- name: Install go-test-json-to-loki
|
||||||
|
run: |
|
||||||
|
go install calmh.dev/go-test-json-to-loki@latest
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
# Our Windows tests currently don't work on Go 1.20
|
|
||||||
# https://github.com/syncthing/syncthing/issues/8779
|
|
||||||
# https://github.com/syncthing/syncthing/issues/8778
|
|
||||||
if: "!(matrix.go == '1.20' && matrix.runner == 'windows-latest')"
|
|
||||||
run: |
|
run: |
|
||||||
go run build.go test
|
go version
|
||||||
|
go run build.go test | go-test-json-to-loki
|
||||||
|
env:
|
||||||
|
GOFLAGS: "-json"
|
||||||
|
LOKI_URL: ${{ secrets.LOKI_URL }}
|
||||||
|
LOKI_USER: ${{ secrets.LOKI_USER }}
|
||||||
|
LOKI_PASSWORD: ${{ secrets.LOKI_PASSWORD }}
|
||||||
|
LOKI_LABELS: "go=${{ matrix.go }},runner=${{ matrix.runner }},repo=${{ github.repository }},ref=${{ github.ref }}"
|
||||||
|
|
||||||
#
|
#
|
||||||
# Meta checks for formatting, copyright, etc
|
# The basic checks job is a virtual one that depends on the matrix tests,
|
||||||
|
# the correctness checks, and various builds that we always do. This makes
|
||||||
|
# it easy to have the PR process have a single test as a gatekeeper for
|
||||||
|
# merging, instead of having to add all the matrix tests and update them
|
||||||
|
# each time the version changes. (The top level test is not available for
|
||||||
|
# choosing there, only the matrix "children".)
|
||||||
#
|
#
|
||||||
|
|
||||||
correctness:
|
basics:
|
||||||
name: Check correctness
|
name: Basic checks passed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- build-test
|
||||||
|
- package-linux
|
||||||
|
- package-cross
|
||||||
|
- package-source
|
||||||
|
- package-debian
|
||||||
|
- package-windows
|
||||||
|
- govulncheck
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: ${{ env.GO_VERSION }}
|
|
||||||
|
|
||||||
- name: Check correctness
|
|
||||||
run: |
|
|
||||||
go test -v ./meta
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Windows
|
# Windows
|
||||||
@@ -98,10 +118,6 @@ jobs:
|
|||||||
|
|
||||||
package-windows:
|
package-windows:
|
||||||
name: Package for Windows
|
name: Package for Windows
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/release'
|
|
||||||
environment: signing
|
|
||||||
needs:
|
|
||||||
- build-test
|
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Set git to use LF
|
- name: Set git to use LF
|
||||||
@@ -113,15 +129,23 @@ jobs:
|
|||||||
git config --global core.autocrlf false
|
git config --global core.autocrlf false
|
||||||
git config --global core.eol lf
|
git config --global core.eol lf
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
|
||||||
|
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: false
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- name: Get actual Go version
|
||||||
|
run: |
|
||||||
|
go version
|
||||||
|
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~\AppData\Local\go-build
|
~\AppData\Local\go-build
|
||||||
@@ -134,22 +158,74 @@ jobs:
|
|||||||
|
|
||||||
- name: Create packages
|
- name: Create packages
|
||||||
run: |
|
run: |
|
||||||
go run build.go -goarch amd64 zip
|
$targets = 'syncthing', 'stdiscosrv', 'strelaysrv'
|
||||||
go run build.go -goarch arm zip
|
$archs = 'amd64', 'arm', 'arm64', '386'
|
||||||
go run build.go -goarch arm64 zip
|
foreach ($arch in $archs) {
|
||||||
go run build.go -goarch 386 zip
|
foreach ($tgt in $targets) {
|
||||||
|
go run build.go -goarch $arch zip $tgt
|
||||||
|
}
|
||||||
|
}
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
CODESIGN_SIGNTOOL: ${{ secrets.CODESIGN_SIGNTOOL }}
|
|
||||||
CODESIGN_CERTIFICATE_BASE64: ${{ secrets.CODESIGN_CERTIFICATE_BASE64 }}
|
|
||||||
CODESIGN_CERTIFICATE_PASSWORD: ${{ secrets.CODESIGN_CERTIFICATE_PASSWORD }}
|
|
||||||
CODESIGN_TIMESTAMP_SERVER: ${{ secrets.CODESIGN_TIMESTAMP_SERVER }}
|
|
||||||
|
|
||||||
- name: Archive artifacts
|
- name: Archive artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: packages
|
name: unsigned-packages-windows
|
||||||
path: syncthing-windows-*.zip
|
path: "*.zip"
|
||||||
|
|
||||||
|
codesign-windows:
|
||||||
|
name: Codesign for Windows
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
|
||||||
|
environment: release
|
||||||
|
runs-on: windows-latest
|
||||||
|
needs:
|
||||||
|
- package-windows
|
||||||
|
steps:
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: unsigned-packages-windows
|
||||||
|
path: packages
|
||||||
|
|
||||||
|
- name: Extract packages
|
||||||
|
working-directory: packages
|
||||||
|
run: |
|
||||||
|
$files = Get-ChildItem "." -Filter *.zip
|
||||||
|
foreach ($file in $files) {
|
||||||
|
7z x $file.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Sign files with Trusted Signing
|
||||||
|
uses: azure/trusted-signing-action@v0.5.1
|
||||||
|
with:
|
||||||
|
azure-tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }}
|
||||||
|
azure-client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }}
|
||||||
|
azure-client-secret: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_SECRET }}
|
||||||
|
endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
|
||||||
|
trusted-signing-account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT }}
|
||||||
|
certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_PROFILE }}
|
||||||
|
files-folder: ${{ github.workspace }}\packages
|
||||||
|
files-folder-filter: exe
|
||||||
|
files-folder-recurse: true
|
||||||
|
file-digest: SHA256
|
||||||
|
timestamp-rfc3161: http://timestamp.acs.microsoft.com
|
||||||
|
timestamp-digest: SHA256
|
||||||
|
|
||||||
|
- name: Repackage packages
|
||||||
|
working-directory: packages
|
||||||
|
run: |
|
||||||
|
$files = Get-ChildItem "." -Filter *.zip
|
||||||
|
foreach ($file in $files) {
|
||||||
|
Remove-Item $file.Name
|
||||||
|
7z a -tzip $file.Name $file.BaseName
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Archive artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: packages-windows
|
||||||
|
path: "packages/*.zip"
|
||||||
|
|
||||||
#
|
#
|
||||||
# Linux
|
# Linux
|
||||||
@@ -157,19 +233,25 @@ jobs:
|
|||||||
|
|
||||||
package-linux:
|
package-linux:
|
||||||
name: Package for Linux
|
name: Package for Linux
|
||||||
needs:
|
|
||||||
- build-test
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
|
||||||
|
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: false
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- name: Get actual Go version
|
||||||
|
run: |
|
||||||
|
go version
|
||||||
|
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
@@ -180,16 +262,20 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
archs=$(go tool dist list | grep linux | sed 's#linux/##')
|
archs=$(go tool dist list | grep linux | sed 's#linux/##')
|
||||||
for goarch in $archs ; do
|
for goarch in $archs ; do
|
||||||
go run build.go -goarch "$goarch" tar
|
for tgt in syncthing stdiscosrv strelaysrv ; do
|
||||||
|
go run build.go -goarch "$goarch" tar "$tgt"
|
||||||
|
done
|
||||||
done
|
done
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
|
|
||||||
- name: Archive artifacts
|
- name: Archive artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: packages
|
name: packages-linux
|
||||||
path: syncthing-linux-*.tar.gz
|
path: |
|
||||||
|
*.tar.gz
|
||||||
|
compat.json
|
||||||
|
|
||||||
#
|
#
|
||||||
# macOS
|
# macOS
|
||||||
@@ -197,21 +283,27 @@ jobs:
|
|||||||
|
|
||||||
package-macos:
|
package-macos:
|
||||||
name: Package for macOS
|
name: Package for macOS
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/release'
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
|
||||||
environment: signing
|
environment: release
|
||||||
needs:
|
|
||||||
- build-test
|
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
|
||||||
|
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: false
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- name: Get actual Go version
|
||||||
|
run: |
|
||||||
|
go version
|
||||||
|
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
@@ -245,7 +337,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Create package (amd64)
|
- name: Create package (amd64)
|
||||||
run: |
|
run: |
|
||||||
go run build.go -goarch amd64 zip
|
for tgt in syncthing stdiscosrv strelaysrv ; do
|
||||||
|
go run build.go -goarch amd64 zip "$tgt"
|
||||||
|
done
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "1"
|
CGO_ENABLED: "1"
|
||||||
|
|
||||||
@@ -259,7 +353,9 @@ jobs:
|
|||||||
go "\$@"
|
go "\$@"
|
||||||
EOT
|
EOT
|
||||||
chmod 755 xgo.sh
|
chmod 755 xgo.sh
|
||||||
go run build.go -gocmd ./xgo.sh -goarch arm64 zip
|
for tgt in syncthing stdiscosrv strelaysrv ; do
|
||||||
|
go run build.go -gocmd ./xgo.sh -goarch arm64 zip "$tgt"
|
||||||
|
done
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "1"
|
CGO_ENABLED: "1"
|
||||||
|
|
||||||
@@ -280,10 +376,39 @@ jobs:
|
|||||||
zip -r "../$universal.zip" "$universal"
|
zip -r "../$universal.zip" "$universal"
|
||||||
|
|
||||||
- name: Archive artifacts
|
- name: Archive artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: packages
|
name: packages-macos
|
||||||
path: syncthing-*.zip
|
path: "*.zip"
|
||||||
|
|
||||||
|
notarize-macos:
|
||||||
|
name: Notarize for macOS
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
|
||||||
|
environment: release
|
||||||
|
needs:
|
||||||
|
- package-macos
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: packages-macos
|
||||||
|
|
||||||
|
- name: Notarize binaries
|
||||||
|
run: |
|
||||||
|
APPSTORECONNECT_API_KEY_PATH="$RUNNER_TEMP/apikey.p8"
|
||||||
|
echo "$APPSTORECONNECT_API_KEY" | base64 -d -o "$APPSTORECONNECT_API_KEY_PATH"
|
||||||
|
for file in *-macos-*.zip ; do
|
||||||
|
xcrun notarytool submit \
|
||||||
|
-k "$APPSTORECONNECT_API_KEY_PATH" \
|
||||||
|
-d "$APPSTORECONNECT_API_KEY_ID" \
|
||||||
|
-i "$APPSTORECONNECT_API_KEY_ISSUER" \
|
||||||
|
$file
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
APPSTORECONNECT_API_KEY: ${{ secrets.APPSTORECONNECT_API_KEY }}
|
||||||
|
APPSTORECONNECT_API_KEY_ID: ${{ secrets.APPSTORECONNECT_API_KEY_ID }}
|
||||||
|
APPSTORECONNECT_API_KEY_ISSUER: ${{ secrets.APPSTORECONNECT_API_KEY_ISSUER }}
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cross compile other unixes
|
# Cross compile other unixes
|
||||||
@@ -291,19 +416,25 @@ jobs:
|
|||||||
|
|
||||||
package-cross:
|
package-cross:
|
||||||
name: Package cross compiled
|
name: Package cross compiled
|
||||||
needs:
|
|
||||||
- build-test
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
|
||||||
|
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: false
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- name: Get actual Go version
|
||||||
|
run: |
|
||||||
|
go version
|
||||||
|
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
@@ -320,26 +451,34 @@ jobs:
|
|||||||
| grep -v js/ \
|
| grep -v js/ \
|
||||||
| grep -v linux/ \
|
| grep -v linux/ \
|
||||||
| grep -v nacl/ \
|
| grep -v nacl/ \
|
||||||
| grep -v openbsd/arm\$ \
|
|
||||||
| grep -v openbsd/arm64 \
|
|
||||||
| grep -v openbsd/mips \
|
|
||||||
| grep -v plan9/ \
|
| grep -v plan9/ \
|
||||||
| grep -v windows/ \
|
| grep -v windows/ \
|
||||||
|
| grep -v /wasm \
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Build for each platform with errors silenced, because we expect
|
||||||
|
# some oddball platforms to fail. This avoids a bunch of errors in
|
||||||
|
# the GitHub Actions output, instead summarizing each build
|
||||||
|
# failure as a warning.
|
||||||
for plat in $platforms; do
|
for plat in $platforms; do
|
||||||
goos="${plat%/*}"
|
goos="${plat%/*}"
|
||||||
goarch="${plat#*/}"
|
goarch="${plat#*/}"
|
||||||
go run build.go -goos "$goos" -goarch "$goarch" tar
|
echo "::group ::$plat"
|
||||||
|
for tgt in syncthing stdiscosrv strelaysrv ; do
|
||||||
|
if ! go run build.go -goos "$goos" -goarch "$goarch" tar "$tgt" 2>/dev/null; then
|
||||||
|
echo "::warning ::Failed to build $tgt for $plat"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "::endgroup::"
|
||||||
done
|
done
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
|
|
||||||
- name: Archive artifacts
|
- name: Archive artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: packages
|
name: packages-other
|
||||||
path: syncthing-*.tar.gz
|
path: "*.tar.gz"
|
||||||
|
|
||||||
#
|
#
|
||||||
# Source
|
# Source
|
||||||
@@ -347,17 +486,18 @@ jobs:
|
|||||||
|
|
||||||
package-source:
|
package-source:
|
||||||
name: Package source code
|
name: Package source code
|
||||||
needs:
|
|
||||||
- build-test
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
|
||||||
|
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: false
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
- name: Package source
|
- name: Package source
|
||||||
run: |
|
run: |
|
||||||
@@ -376,7 +516,527 @@ jobs:
|
|||||||
mv "syncthing-source-$version.tar.gz" syncthing
|
mv "syncthing-source-$version.tar.gz" syncthing
|
||||||
|
|
||||||
- name: Archive artifacts
|
- name: Archive artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: packages
|
name: packages-source
|
||||||
path: syncthing-source-*.tar.gz
|
path: syncthing-source-*.tar.gz
|
||||||
|
|
||||||
|
#
|
||||||
|
# Sign binaries for auto upgrade, generate ASC signature files
|
||||||
|
#
|
||||||
|
|
||||||
|
sign-for-upgrade:
|
||||||
|
name: Sign for upgrade
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
|
||||||
|
environment: release
|
||||||
|
needs:
|
||||||
|
- codesign-windows
|
||||||
|
- package-linux
|
||||||
|
- package-macos
|
||||||
|
- package-cross
|
||||||
|
- package-source
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: syncthing/release-tools
|
||||||
|
path: tools
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: false
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Install signing tool
|
||||||
|
run: |
|
||||||
|
go install ./cmd/dev/stsigtool
|
||||||
|
|
||||||
|
- name: Sign archives
|
||||||
|
run: |
|
||||||
|
export PRIVATE_KEY="$RUNNER_TEMP/privkey.pem"
|
||||||
|
export PATH="$PATH:$(go env GOPATH)/bin"
|
||||||
|
echo "$STSIGTOOL_PRIVATE_KEY" | base64 -d > "$PRIVATE_KEY"
|
||||||
|
mkdir packages
|
||||||
|
mv packages-*/* packages
|
||||||
|
pushd packages
|
||||||
|
"$GITHUB_WORKSPACE/tools/sign-only"
|
||||||
|
rm -f "$PRIVATE_KEY"
|
||||||
|
env:
|
||||||
|
STSIGTOOL_PRIVATE_KEY: ${{ secrets.STSIGTOOL_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Create shasum files
|
||||||
|
run: |
|
||||||
|
pushd packages
|
||||||
|
files=(*.tar.gz *.zip)
|
||||||
|
sha1sum "${files[@]}" > sha1sum.txt
|
||||||
|
sha256sum "${files[@]}" > sha256sum.txt
|
||||||
|
popd
|
||||||
|
|
||||||
|
version=$(go run build.go version)
|
||||||
|
echo "VERSION=$version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Sign shasum files
|
||||||
|
uses: docker://ghcr.io/kastelo/ezapt:latest
|
||||||
|
with:
|
||||||
|
args:
|
||||||
|
sign
|
||||||
|
packages/sha1sum.txt packages/sha256sum.txt
|
||||||
|
env:
|
||||||
|
EZAPT_KEYRING_BASE64: ${{ secrets.APT_GPG_KEYRING_BASE64 }}
|
||||||
|
|
||||||
|
- name: Sign source
|
||||||
|
uses: docker://ghcr.io/kastelo/ezapt:latest
|
||||||
|
with:
|
||||||
|
args:
|
||||||
|
sign --detach --ascii
|
||||||
|
packages/syncthing-source-${{ env.VERSION }}.tar.gz
|
||||||
|
env:
|
||||||
|
EZAPT_KEYRING_BASE64: ${{ secrets.APT_GPG_KEYRING_BASE64 }}
|
||||||
|
|
||||||
|
- name: Archive artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: packages-signed
|
||||||
|
path: |
|
||||||
|
packages/*.tar.gz
|
||||||
|
packages/*.zip
|
||||||
|
packages/*.asc
|
||||||
|
packages/*.json
|
||||||
|
|
||||||
|
#
|
||||||
|
# Debian
|
||||||
|
#
|
||||||
|
|
||||||
|
package-debian:
|
||||||
|
name: Package for Debian
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: false
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Get actual Go version
|
||||||
|
run: |
|
||||||
|
go version
|
||||||
|
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: '3.0'
|
||||||
|
|
||||||
|
- name: Install fpm
|
||||||
|
run: |
|
||||||
|
gem install fpm
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-debian-${{ hashFiles('**/go.sum') }}
|
||||||
|
|
||||||
|
- name: Package for Debian
|
||||||
|
run: |
|
||||||
|
for arch in amd64 i386 armhf armel arm64 ; do
|
||||||
|
for tgt in syncthing stdiscosrv strelaysrv ; do
|
||||||
|
go run build.go -no-upgrade -installsuffix=no-upgrade -goarch "$arch" deb "$tgt"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
BUILD_USER: debian
|
||||||
|
|
||||||
|
- name: Archive artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: debian-packages
|
||||||
|
path: "*.deb"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Nightlies
|
||||||
|
#
|
||||||
|
|
||||||
|
publish-nightly:
|
||||||
|
name: Publish nightly build
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && startsWith(github.ref, 'refs/heads/release-nightly')
|
||||||
|
environment: release
|
||||||
|
needs:
|
||||||
|
- sign-for-upgrade
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: syncthing/release-tools
|
||||||
|
path: tools
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: packages-signed
|
||||||
|
path: packages
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: false
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Create release json
|
||||||
|
run: |
|
||||||
|
cd packages
|
||||||
|
"$GITHUB_WORKSPACE/tools/generate-release-json" "$BASE_URL" > nightly.json
|
||||||
|
env:
|
||||||
|
BASE_URL: ${{ secrets.NIGHTLY_BASE_URL }}
|
||||||
|
|
||||||
|
- name: Push artifacts
|
||||||
|
uses: docker://docker.io/rclone/rclone:latest
|
||||||
|
env:
|
||||||
|
RCLONE_CONFIG_OBJSTORE_TYPE: s3
|
||||||
|
RCLONE_CONFIG_OBJSTORE_PROVIDER: ${{ secrets.S3_PROVIDER }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ACL: public-read
|
||||||
|
with:
|
||||||
|
args: sync -v --no-update-modtime packages objstore:nightly
|
||||||
|
|
||||||
|
#
|
||||||
|
# Push release artifacts to Spaces
|
||||||
|
#
|
||||||
|
|
||||||
|
publish-release-files:
|
||||||
|
name: Publish release files
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/tags/v'))
|
||||||
|
environment: release
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
needs:
|
||||||
|
- sign-for-upgrade
|
||||||
|
- package-debian
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
|
||||||
|
|
||||||
|
- name: Download signed packages
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: packages-signed
|
||||||
|
path: packages
|
||||||
|
|
||||||
|
- name: Download debian packages
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: debian-packages
|
||||||
|
path: packages
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: false
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Set version
|
||||||
|
run: |
|
||||||
|
version=$(go run build.go version)
|
||||||
|
echo "VERSION=$version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Push to object store (${{ env.VERSION }})
|
||||||
|
uses: docker://docker.io/rclone/rclone:latest
|
||||||
|
env:
|
||||||
|
RCLONE_CONFIG_OBJSTORE_TYPE: s3
|
||||||
|
RCLONE_CONFIG_OBJSTORE_PROVIDER: ${{ secrets.S3_PROVIDER }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ACL: public-read
|
||||||
|
with:
|
||||||
|
args: sync -v --no-update-modtime packages objstore:release/${{ env.VERSION }}
|
||||||
|
|
||||||
|
- name: Push to object store (latest)
|
||||||
|
uses: docker://docker.io/rclone/rclone:latest
|
||||||
|
env:
|
||||||
|
RCLONE_CONFIG_OBJSTORE_TYPE: s3
|
||||||
|
RCLONE_CONFIG_OBJSTORE_PROVIDER: ${{ secrets.S3_PROVIDER }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ACL: public-read
|
||||||
|
with:
|
||||||
|
args: sync -v --no-update-modtime objstore:release/${{ env.VERSION }} objstore:release/latest
|
||||||
|
|
||||||
|
- name: Create GitHub releases and push binaries
|
||||||
|
run: |
|
||||||
|
maybePrerelease=""
|
||||||
|
if [[ $VERSION == *-* ]]; then
|
||||||
|
maybePrerelease="--prerelease"
|
||||||
|
fi
|
||||||
|
export GH_PROMPT_DISABLED=1
|
||||||
|
if ! gh release view --json name "$VERSION" >/dev/null 2>&1 ; then
|
||||||
|
gh release create "$VERSION" \
|
||||||
|
$maybePrerelease \
|
||||||
|
--title "$VERSION" \
|
||||||
|
--notes-from-tag
|
||||||
|
fi
|
||||||
|
gh release upload --clobber "$VERSION" \
|
||||||
|
packages/*.asc packages/*.json \
|
||||||
|
packages/syncthing-*.tar.gz \
|
||||||
|
packages/syncthing-*.zip \
|
||||||
|
packages/syncthing_*.deb
|
||||||
|
|
||||||
|
PKGS=$(pwd)/packages
|
||||||
|
cd /tmp # gh will not release for repo x while inside repo y
|
||||||
|
for repo in relaysrv discosrv ; do
|
||||||
|
export GH_REPO="syncthing/$repo"
|
||||||
|
if ! gh release view --json name "$VERSION" >/dev/null 2>&1 ; then
|
||||||
|
gh release create "$VERSION" \
|
||||||
|
$maybePrerelease \
|
||||||
|
--title "$VERSION" \
|
||||||
|
--notes "https://github.com/syncthing/syncthing/releases/tag/$VERSION"
|
||||||
|
fi
|
||||||
|
gh release upload --clobber "$VERSION" \
|
||||||
|
$PKGS/*.asc \
|
||||||
|
$PKGS/*${repo}*
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Push Debian/APT archive
|
||||||
|
#
|
||||||
|
|
||||||
|
publish-apt:
|
||||||
|
name: Publish APT
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
|
||||||
|
environment: release
|
||||||
|
needs:
|
||||||
|
- package-debian
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
|
||||||
|
|
||||||
|
- name: Download packages
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: debian-packages
|
||||||
|
path: packages
|
||||||
|
|
||||||
|
- name: Set version
|
||||||
|
run: |
|
||||||
|
version=$(go run build.go version)
|
||||||
|
echo "Version: $version"
|
||||||
|
echo "VERSION=$version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Decide whether packages should go to stable, candidate or nightly
|
||||||
|
- name: Prepare packages
|
||||||
|
run: |
|
||||||
|
kind=stable
|
||||||
|
if [[ $VERSION == *-rc.[0-9] ]] ; then
|
||||||
|
kind=candidate
|
||||||
|
elif [[ $VERSION == *-* ]] ; then
|
||||||
|
kind=nightly
|
||||||
|
fi
|
||||||
|
echo "Kind: $kind"
|
||||||
|
mkdir -p packages/syncthing/$kind
|
||||||
|
mv packages/*.deb packages/syncthing/$kind
|
||||||
|
|
||||||
|
- name: Pull archive
|
||||||
|
uses: docker://docker.io/rclone/rclone:latest
|
||||||
|
env:
|
||||||
|
RCLONE_CONFIG_OBJSTORE_TYPE: s3
|
||||||
|
RCLONE_CONFIG_OBJSTORE_PROVIDER: ${{ secrets.S3_PROVIDER }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ACL: public-read
|
||||||
|
with:
|
||||||
|
args: sync objstore:apt/dists dists
|
||||||
|
|
||||||
|
- name: Update archive
|
||||||
|
uses: docker://ghcr.io/kastelo/ezapt:latest
|
||||||
|
with:
|
||||||
|
args:
|
||||||
|
publish
|
||||||
|
--add packages
|
||||||
|
--dists dists
|
||||||
|
env:
|
||||||
|
EZAPT_KEYRING_BASE64: ${{ secrets.APT_GPG_KEYRING_BASE64 }}
|
||||||
|
|
||||||
|
- name: Push archive
|
||||||
|
uses: docker://docker.io/rclone/rclone:latest
|
||||||
|
env:
|
||||||
|
RCLONE_CONFIG_OBJSTORE_TYPE: s3
|
||||||
|
RCLONE_CONFIG_OBJSTORE_PROVIDER: ${{ secrets.S3_PROVIDER }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
|
||||||
|
RCLONE_CONFIG_OBJSTORE_ACL: public-read
|
||||||
|
with:
|
||||||
|
args: sync -v --no-update-modtime dists objstore:apt/dists
|
||||||
|
|
||||||
|
#
|
||||||
|
# Build and push to Docker Hub
|
||||||
|
#
|
||||||
|
|
||||||
|
docker-syncthing:
|
||||||
|
name: Build and push Docker images
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release-nightly' || github.ref == 'refs/heads/infrastructure' || startsWith(github.ref, 'refs/tags/v'))
|
||||||
|
environment: docker
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
pkg:
|
||||||
|
- syncthing
|
||||||
|
- strelaysrv
|
||||||
|
- stdiscosrv
|
||||||
|
include:
|
||||||
|
- pkg: syncthing
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: syncthing/syncthing
|
||||||
|
- pkg: strelaysrv
|
||||||
|
dockerfile: Dockerfile.strelaysrv
|
||||||
|
image: syncthing/relaysrv
|
||||||
|
- pkg: stdiscosrv
|
||||||
|
dockerfile: Dockerfile.stdiscosrv
|
||||||
|
image: syncthing/discosrv
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: false
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Get actual Go version
|
||||||
|
run: |
|
||||||
|
go version
|
||||||
|
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-docker-${{ matrix.pkg }}-${{ hashFiles('**/go.sum') }}
|
||||||
|
|
||||||
|
- name: Build binaries
|
||||||
|
run: |
|
||||||
|
for arch in amd64 arm64 arm; do
|
||||||
|
go run build.go -goos linux -goarch "$arch" -no-upgrade build ${{ matrix.pkg }}
|
||||||
|
mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-"$arch"
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
BUILD_USER: docker
|
||||||
|
|
||||||
|
- name: Check if we will be able to push images
|
||||||
|
run: |
|
||||||
|
if [[ "${{ secrets.DOCKERHUB_TOKEN }}" != "" ]]; then
|
||||||
|
echo "DOCKER_PUSH=true" >> $GITHUB_ENV;
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
if: env.DOCKER_PUSH == 'true'
|
||||||
|
with:
|
||||||
|
registry: docker.io
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
if: env.DOCKER_PUSH == 'true'
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Set version tags
|
||||||
|
run: |
|
||||||
|
version=$(go run build.go version)
|
||||||
|
version=${version#v}
|
||||||
|
if [[ $version == @([0-9]|[0-9][0-9]).@([0-9]|[0-9][0-9]).@([0-9]|[0-9][0-9]) ]] ; then
|
||||||
|
echo Release version, pushing to :latest and version tags
|
||||||
|
major=${version%.*.*}
|
||||||
|
minor=${version%.*}
|
||||||
|
tags=docker.io/${{ matrix.image }}:$version,ghcr.io/${{ matrix.image }}:$version,docker.io/${{ matrix.image }}:$major,ghcr.io/${{ matrix.image }}:$major,docker.io/${{ matrix.image }}:$minor,ghcr.io/${{ matrix.image }}:$minor,docker.io/${{ matrix.image }}:latest,ghcr.io/${{ matrix.image }}:latest
|
||||||
|
elif [[ $version == *-rc.@([0-9]|[0-9][0-9]) ]] ; then
|
||||||
|
echo Release candidate, pushing to :rc and version tags
|
||||||
|
tags=docker.io/${{ matrix.image }}:$version,ghcr.io/${{ matrix.image }}:$version,docker.io/${{ matrix.image }}:rc,ghcr.io/${{ matrix.image }}:rc
|
||||||
|
else
|
||||||
|
echo Development version, pushing to :edge
|
||||||
|
tags=docker.io/${{ matrix.image }}:edge,ghcr.io/${{ matrix.image }}:edge
|
||||||
|
fi
|
||||||
|
echo "DOCKER_TAGS=$tags" >> $GITHUB_ENV
|
||||||
|
echo "VERSION=$version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ${{ matrix.dockerfile }}
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/7
|
||||||
|
push: ${{ env.DOCKER_PUSH == 'true' }}
|
||||||
|
tags: ${{ env.DOCKER_TAGS }}
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.version=${{ env.VERSION }}
|
||||||
|
org.opencontainers.image.revision=${{ github.sha }}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Check for known vulnerabilities in Go dependencies
|
||||||
|
#
|
||||||
|
|
||||||
|
govulncheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Run govulncheck
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: false
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: run govulncheck
|
||||||
|
run: |
|
||||||
|
go run build.go assets
|
||||||
|
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
govulncheck ./...
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Run PR linters
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
#
|
||||||
|
# golangci-lint runs a suite of static analysis checks on the code
|
||||||
|
#
|
||||||
|
|
||||||
|
golangci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Golangci-lint
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
|
||||||
|
- name: ensure asset generation
|
||||||
|
run: go run build.go assets
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v8
|
||||||
|
with:
|
||||||
|
only-new-issues: true
|
||||||
|
|
||||||
|
#
|
||||||
|
# Meta checks for formatting, copyright, etc
|
||||||
|
#
|
||||||
|
|
||||||
|
meta:
|
||||||
|
name: Meta checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
go run build.go assets
|
||||||
|
go test -v ./meta
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: PR metadata
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
#
|
||||||
|
# Set labels on PRs, which are then used to categorise release notes
|
||||||
|
#
|
||||||
|
|
||||||
|
labels:
|
||||||
|
name: Set labels
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: srvaroa/labeler@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
name: Release Syncthing
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- release
|
||||||
|
- release-rc*
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-release-tag:
|
||||||
|
name: Create release tag
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: release
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
|
||||||
|
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
|
||||||
|
- name: Determine version to release
|
||||||
|
run: |
|
||||||
|
if [[ "$GITHUB_REF_NAME" == "release" ]] ; then
|
||||||
|
next=$(go run ./script/next-version.go)
|
||||||
|
else
|
||||||
|
next=$(go run ./script/next-version.go --pre)
|
||||||
|
fi
|
||||||
|
echo "NEXT=$next" >> $GITHUB_ENV
|
||||||
|
echo "Next version is $next"
|
||||||
|
|
||||||
|
prev=$(git describe --exclude "*-*" --abbrev=0)
|
||||||
|
echo "PREV=$prev" >> $GITHUB_ENV
|
||||||
|
echo "Previous version is $prev"
|
||||||
|
|
||||||
|
- name: Determine release notes
|
||||||
|
run: |
|
||||||
|
go run ./script/relnotes.go --new-ver "$NEXT" --branch "$GITHUB_REF_NAME" --prev-ver "$PREV" > notes.md
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Create and push tag
|
||||||
|
run: |
|
||||||
|
git config --global user.name 'Syncthing Release Automation'
|
||||||
|
git config --global user.email 'release@syncthing.net'
|
||||||
|
git tag -a -F notes.md --cleanup=whitespace "$NEXT"
|
||||||
|
git push origin "$NEXT"
|
||||||
|
|
||||||
|
- name: Trigger the build
|
||||||
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
|
with:
|
||||||
|
workflow: build-syncthing.yaml
|
||||||
|
ref: refs/tags/${{ env.NEXT }}
|
||||||
|
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: Trigger nightly build & release
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
# Run nightly build at 01:00 UTC
|
||||||
|
- cron: '00 01 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
trigger-nightly:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Push to release-nightly to trigger build
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
git push origin main:release-nightly
|
||||||
@@ -10,13 +10,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Update translations and documentation
|
name: Update translations and documentation
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
||||||
- uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f # v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.18.4
|
go-version: stable
|
||||||
- run: |
|
- run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
git config --global user.name 'Syncthing Release Automation'
|
git config --global user.name 'Syncthing Release Automation'
|
||||||
|
|||||||
+2
-3
@@ -1,11 +1,10 @@
|
|||||||
/syncthing
|
/syncthing
|
||||||
/stdiscosrv
|
/stdiscosrv
|
||||||
syncthing.exe
|
|
||||||
stdiscosrv.exe
|
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.zip
|
*.zip
|
||||||
*.asc
|
*.asc
|
||||||
*.deb
|
*.deb
|
||||||
|
*.exe
|
||||||
.jshintrc
|
.jshintrc
|
||||||
coverage.out
|
coverage.out
|
||||||
files/pidx
|
files/pidx
|
||||||
@@ -19,4 +18,4 @@ deb
|
|||||||
/repos
|
/repos
|
||||||
/proto/scripts/protoc-gen-gosyncthing
|
/proto/scripts/protoc-gen-gosyncthing
|
||||||
/gui/next-gen-gui
|
/gui/next-gen-gui
|
||||||
.idea
|
/compat.json
|
||||||
|
|||||||
+61
-20
@@ -1,26 +1,67 @@
|
|||||||
linters-settings:
|
version: "2"
|
||||||
maligned:
|
|
||||||
suggest-new: true
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable-all: true
|
default: all
|
||||||
disable:
|
disable:
|
||||||
- goimports
|
- cyclop
|
||||||
- depguard
|
- depguard
|
||||||
- lll
|
- exhaustive
|
||||||
- gochecknoinits
|
- exhaustruct
|
||||||
- gochecknoglobals
|
- forbidigo
|
||||||
- gofmt
|
|
||||||
- scopelint
|
|
||||||
- gocyclo
|
|
||||||
- funlen
|
- funlen
|
||||||
- wsl
|
- gochecknoglobals
|
||||||
|
- gochecknoinits
|
||||||
- gocognit
|
- gocognit
|
||||||
|
- goconst
|
||||||
|
- gocyclo
|
||||||
- godox
|
- godox
|
||||||
|
- gomoddirectives
|
||||||
service:
|
- inamedparam
|
||||||
golangci-lint-version: 1.21.x
|
- interfacebloat
|
||||||
prepare:
|
- ireturn
|
||||||
- rm -f go.sum # 1.12 -> 1.13 issues with QUIC-go
|
- lll
|
||||||
- GO111MODULE=on go mod vendor
|
- maintidx
|
||||||
- go run build.go assets
|
- mnd
|
||||||
|
- musttag
|
||||||
|
- nestif
|
||||||
|
- nlreturn
|
||||||
|
- nonamedreturns
|
||||||
|
- paralleltest
|
||||||
|
- prealloc
|
||||||
|
- predeclared
|
||||||
|
- protogetter
|
||||||
|
- recvcheck
|
||||||
|
- revive
|
||||||
|
- tagalign
|
||||||
|
- tagliatelle
|
||||||
|
- testpackage
|
||||||
|
- usetesting # go 1.24
|
||||||
|
- varnamelen
|
||||||
|
- whitespace
|
||||||
|
- wrapcheck
|
||||||
|
- wsl
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
paths:
|
||||||
|
- internal/gen
|
||||||
|
- cmd/dev
|
||||||
|
- repos
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofumpt
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- internal/gen
|
||||||
|
- cmd/dev
|
||||||
|
- repos
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
|||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
# This is the policy-bot configuration for this repository. It controls
|
||||||
|
# which approvals are required for any given pull request. The format is
|
||||||
|
# described at https://github.com/palantir/policy-bot. The syntax of the
|
||||||
|
# policy can be verified by the bot:
|
||||||
|
# curl https://pb.syncthing.net/api/validate -X PUT -T .policy.yml
|
||||||
|
|
||||||
|
# The policy below is what is required for any pull request.
|
||||||
|
policy:
|
||||||
|
approval:
|
||||||
|
- subject is conventional commit
|
||||||
|
- project metadata requires maintainer approval
|
||||||
|
- or:
|
||||||
|
- is approved by a syncthing contributor
|
||||||
|
- is a translation or dependency update by a contributor
|
||||||
|
- is a trivial change by a contributor
|
||||||
|
|
||||||
|
# Additionally, contributors can disapprove of a PR
|
||||||
|
disapproval:
|
||||||
|
requires:
|
||||||
|
teams:
|
||||||
|
- syncthing/contributors
|
||||||
|
|
||||||
|
# The rules for the policy are described below.
|
||||||
|
|
||||||
|
approval_rules:
|
||||||
|
|
||||||
|
# All commits (PRs before squashing) should have a valid conventional
|
||||||
|
# commit type subject.
|
||||||
|
- name: subject is conventional commit
|
||||||
|
requires:
|
||||||
|
conditions:
|
||||||
|
title:
|
||||||
|
matches:
|
||||||
|
- '^(feat|fix|docs|chore|refactor|build): [a-z].+'
|
||||||
|
- '^(feat|fix|docs|chore|refactor|build)\(\w+(, \w+)*\): [a-z].+'
|
||||||
|
|
||||||
|
# Changes to important project metadata and documentation, including this
|
||||||
|
# policy, require signoff by a maintainer
|
||||||
|
- name: project metadata requires maintainer approval
|
||||||
|
if:
|
||||||
|
changed_files:
|
||||||
|
paths:
|
||||||
|
- ^[^/]+\.md
|
||||||
|
- ^\.policy\.yml
|
||||||
|
- ^LICENSE
|
||||||
|
requires:
|
||||||
|
count: 1
|
||||||
|
teams:
|
||||||
|
- syncthing/maintainers
|
||||||
|
options:
|
||||||
|
ignore_update_merges: true
|
||||||
|
allow_contributor: true
|
||||||
|
|
||||||
|
# Regular pull requests require approval by an active contributor
|
||||||
|
- name: is approved by a syncthing contributor
|
||||||
|
requires:
|
||||||
|
count: 1
|
||||||
|
teams:
|
||||||
|
- syncthing/contributors
|
||||||
|
options:
|
||||||
|
ignore_update_merges: true
|
||||||
|
allow_contributor: true
|
||||||
|
|
||||||
|
# Changes to some files (translations, dependencies, compatibility) do not
|
||||||
|
# require approval if they were proposed by a contributor and have a
|
||||||
|
# matching commit subject
|
||||||
|
- name: is a translation or dependency update by a contributor
|
||||||
|
if:
|
||||||
|
only_changed_files:
|
||||||
|
paths:
|
||||||
|
- ^gui/default/assets/lang/
|
||||||
|
- ^go\.mod$
|
||||||
|
- ^go\.sum$
|
||||||
|
- ^compat\.yaml$
|
||||||
|
title:
|
||||||
|
matches:
|
||||||
|
- '^chore\(gui\):'
|
||||||
|
- '^build\(deps\):'
|
||||||
|
- '^build\(compat\):'
|
||||||
|
has_author_in:
|
||||||
|
teams:
|
||||||
|
- syncthing/contributors
|
||||||
|
|
||||||
|
# If the change is small and the label "trivial" is added, we accept that
|
||||||
|
# on trust. These PRs can be audited after the fact as appropriate.
|
||||||
|
# Features are not trivial.
|
||||||
|
- name: is a trivial change by a contributor
|
||||||
|
if:
|
||||||
|
modified_lines:
|
||||||
|
total: "< 25"
|
||||||
|
title:
|
||||||
|
not_matches:
|
||||||
|
- '^feat'
|
||||||
|
has_labels:
|
||||||
|
- trivial
|
||||||
|
has_author_in:
|
||||||
|
teams:
|
||||||
|
- syncthing/contributors
|
||||||
@@ -20,12 +20,15 @@ Alan Pope <alan@popey.com>
|
|||||||
Alberto Donato <albertodonato@users.noreply.github.com>
|
Alberto Donato <albertodonato@users.noreply.github.com>
|
||||||
Aleksey Vasenev <margtu-fivt@ya.ru>
|
Aleksey Vasenev <margtu-fivt@ya.ru>
|
||||||
Alessandro G. (alessandro.g89) <alessandro.g89@gmail.com>
|
Alessandro G. (alessandro.g89) <alessandro.g89@gmail.com>
|
||||||
|
Alex Ionescu <github@ionescu.sh>
|
||||||
Alex Lindeman <139387+aelindeman@users.noreply.github.com>
|
Alex Lindeman <139387+aelindeman@users.noreply.github.com>
|
||||||
Alex Xu <alex.hello71@gmail.com>
|
Alex Xu <alex.hello71@gmail.com>
|
||||||
Alexander Graf (alex2108) <register-github@alex-graf.de>
|
Alexander Graf (alex2108) <register-github@alex-graf.de>
|
||||||
|
Alexander Seiler <seileralex@gmail.com>
|
||||||
Alexandre Alves <alexandrealvesdb.contact@gmail.com>
|
Alexandre Alves <alexandrealvesdb.contact@gmail.com>
|
||||||
Alexandre Viau (aviau) <alexandre@alexandreviau.net> <aviau@debian.org>
|
Alexandre Viau (aviau) <alexandre@alexandreviau.net> <aviau@debian.org>
|
||||||
Aman Gupta <aman@tmm1.net>
|
Aman Gupta <aman@tmm1.net>
|
||||||
|
Anatoli Babenia <anatoli@rainforce.org>
|
||||||
Anderson Mesquita (andersonvom) <andersonvom@gmail.com>
|
Anderson Mesquita (andersonvom) <andersonvom@gmail.com>
|
||||||
Andreas Sommer <andreas.sommer87@googlemail.com>
|
Andreas Sommer <andreas.sommer87@googlemail.com>
|
||||||
andresvia <andres.via@gmail.com>
|
andresvia <andres.via@gmail.com>
|
||||||
@@ -36,6 +39,7 @@ Andrey D (scienmind) <scintertech@cryptolab.net> <scienmind@users.noreply.github
|
|||||||
André Colomb (acolomb) <src@andre.colomb.de> <github.com@andre.colomb.de>
|
André Colomb (acolomb) <src@andre.colomb.de> <github.com@andre.colomb.de>
|
||||||
andyleap <andyleap@gmail.com>
|
andyleap <andyleap@gmail.com>
|
||||||
Anjan Momi <anjan@momi.ca>
|
Anjan Momi <anjan@momi.ca>
|
||||||
|
Anthony Goeckner <agoeckner@users.noreply.github.com>
|
||||||
Antoine Lamielle (0x010C) <antoine.lamielle@0x010c.fr> <gh@0x010c.fr>
|
Antoine Lamielle (0x010C) <antoine.lamielle@0x010c.fr> <gh@0x010c.fr>
|
||||||
Antony Male (canton7) <antony.male@gmail.com>
|
Antony Male (canton7) <antony.male@gmail.com>
|
||||||
Anur <anurnomeru@163.com>
|
Anur <anurnomeru@163.com>
|
||||||
@@ -44,10 +48,12 @@ Arkadiusz Tymiński <gevleeog@gmail.com>
|
|||||||
Aroun <login@b-vo.fr>
|
Aroun <login@b-vo.fr>
|
||||||
Arthur Axel fREW Schmidt (frioux) <frew@afoolishmanifesto.com> <frioux@gmail.com>
|
Arthur Axel fREW Schmidt (frioux) <frew@afoolishmanifesto.com> <frioux@gmail.com>
|
||||||
Artur Zubilewicz <AkaZecik@users.noreply.github.com>
|
Artur Zubilewicz <AkaZecik@users.noreply.github.com>
|
||||||
|
Ashish Bhate <bhate.ashish@gmail.com>
|
||||||
Audrius Butkevicius (AudriusButkevicius) <audrius.butkevicius@gmail.com> <github@audrius.rocks>
|
Audrius Butkevicius (AudriusButkevicius) <audrius.butkevicius@gmail.com> <github@audrius.rocks>
|
||||||
Aurélien Rainone <476650+arl@users.noreply.github.com>
|
Aurélien Rainone <476650+arl@users.noreply.github.com>
|
||||||
BAHADIR YILMAZ <bahadiryilmaz32@gmail.com>
|
BAHADIR YILMAZ <bahadiryilmaz32@gmail.com>
|
||||||
Bart De Vries (mogwa1) <devriesb@gmail.com>
|
Bart De Vries (mogwa1) <devriesb@gmail.com>
|
||||||
|
Beat Reichenbach <44111292+beatreichenbach@users.noreply.github.com>
|
||||||
Ben Curthoys (bencurthoys) <ben@bencurthoys.com>
|
Ben Curthoys (bencurthoys) <ben@bencurthoys.com>
|
||||||
Ben Schulz (uok) <ueomkail@gmail.com> <uok@users.noreply.github.com>
|
Ben Schulz (uok) <ueomkail@gmail.com> <uok@users.noreply.github.com>
|
||||||
Ben Shepherd (benshep) <bjashepherd@gmail.com>
|
Ben Shepherd (benshep) <bjashepherd@gmail.com>
|
||||||
@@ -66,43 +72,55 @@ Brian R. Becker (brbecker) <brbecker@gmail.com>
|
|||||||
bt90 <btom1990@googlemail.com>
|
bt90 <btom1990@googlemail.com>
|
||||||
Caleb Callaway (cqcallaw) <enlightened.despot@gmail.com>
|
Caleb Callaway (cqcallaw) <enlightened.despot@gmail.com>
|
||||||
Carsten Hagemann (carstenhag) <moter8@gmail.com> <carsten@chagemann.de>
|
Carsten Hagemann (carstenhag) <moter8@gmail.com> <carsten@chagemann.de>
|
||||||
|
Catfriend1 <16361913+Catfriend1@users.noreply.github.com>
|
||||||
Cathryne Linenweaver (Cathryne) <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com> <katrinleinweber@MAC.local>
|
Cathryne Linenweaver (Cathryne) <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com> <katrinleinweber@MAC.local>
|
||||||
Cedric Staniewski (xduugu) <cedric@gmx.ca>
|
Cedric Staniewski (xduugu) <cedric@gmx.ca>
|
||||||
chenrui <rui@meetup.com>
|
chenrui <rui@meetup.com>
|
||||||
Chih-Hsuan Yen <yan12125@gmail.com>
|
Chih-Hsuan Yen <yan12125@gmail.com> <1937689+yan12125@users.noreply.github.com>
|
||||||
Choongkyu <choongkyu.kim+gh@gmail.com> <vapidlyrapid+gh@gmail.com>
|
Choongkyu <choongkyu.kim+gh@gmail.com> <vapidlyrapid+gh@gmail.com>
|
||||||
Chris Howie (cdhowie) <me@chrishowie.com>
|
Chris Howie (cdhowie) <me@chrishowie.com>
|
||||||
Chris Joel (cdata) <chris@scriptolo.gy>
|
Chris Joel (cdata) <chris@scriptolo.gy>
|
||||||
Chris Tonkinson <chris@masterbran.ch>
|
Chris Tonkinson <chris@masterbran.ch>
|
||||||
|
Christian Kujau <ckujau@users.noreply.github.com>
|
||||||
Christian Prescott <me@christianprescott.com>
|
Christian Prescott <me@christianprescott.com>
|
||||||
chucic <chucic@seznam.cz>
|
chucic <chucic@seznam.cz>
|
||||||
|
cjc7373 <niuchangcun@gmail.com>
|
||||||
Colin Kennedy (moshen) <moshen.colin@gmail.com>
|
Colin Kennedy (moshen) <moshen.colin@gmail.com>
|
||||||
Cromefire_ <tim.l@nghorst.net> <26320625+cromefire@users.noreply.github.com>
|
Cromefire_ <tim.l@nghorst.net> <26320625+cromefire@users.noreply.github.com>
|
||||||
cui fliter <imcusg@gmail.com>
|
cui fliter <imcusg@gmail.com>
|
||||||
Cyprien Devillez <cypx@users.noreply.github.com>
|
Cyprien Devillez <cypx@users.noreply.github.com>
|
||||||
|
d-volution <49024624+d-volution@users.noreply.github.com>
|
||||||
Dale Visser <dale.visser@live.com>
|
Dale Visser <dale.visser@live.com>
|
||||||
Dan <benda.daniel@gmail.com>
|
Dan <benda.daniel@gmail.com>
|
||||||
Daniel Barczyk <46358936+DanielBarczyk@users.noreply.github.com>
|
Daniel Barczyk <46358936+DanielBarczyk@users.noreply.github.com>
|
||||||
Daniel Bergmann (brgmnn) <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com>
|
Daniel Bergmann (brgmnn) <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com>
|
||||||
Daniel Harte (norgeous) <daniel@harte.me> <daniel@danielharte.co.uk> <norgeous@users.noreply.github.com>
|
Daniel Harte (norgeous) <daniel@harte.me> <daniel@danielharte.co.uk> <norgeous@users.noreply.github.com>
|
||||||
Daniel Martí (mvdan) <mvdan@mvdan.cc>
|
Daniel Martí (mvdan) <mvdan@mvdan.cc>
|
||||||
|
Daniel Padrta <64928366+danpadcz@users.noreply.github.com>
|
||||||
Darshil Chanpura (dtchanpura) <dtchanpura@gmail.com> <dcprime314@gmail.com>
|
Darshil Chanpura (dtchanpura) <dtchanpura@gmail.com> <dcprime314@gmail.com>
|
||||||
|
dashangcun <907225865@qq.com>
|
||||||
David Rimmer (dinosore) <dinosore@dbrsoftware.co.uk>
|
David Rimmer (dinosore) <dinosore@dbrsoftware.co.uk>
|
||||||
deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
|
deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
|
||||||
|
DeflateAwning <11021263+DeflateAwning@users.noreply.github.com>
|
||||||
Denis A. (dva) <denisva@gmail.com>
|
Denis A. (dva) <denisva@gmail.com>
|
||||||
Dennis Wilson (snnd) <dw@risu.io>
|
Dennis Wilson (snnd) <dw@risu.io>
|
||||||
dependabot-preview[bot] <dependabot-preview[bot]@users.noreply.github.com> <27856297+dependabot-preview[bot]@users.noreply.github.com>
|
dependabot-preview[bot] <dependabot-preview[bot]@users.noreply.github.com> <27856297+dependabot-preview[bot]@users.noreply.github.com>
|
||||||
dependabot[bot] <dependabot[bot]@users.noreply.github.com> <49699333+dependabot[bot]@users.noreply.github.com>
|
dependabot[bot] <dependabot[bot]@users.noreply.github.com> <49699333+dependabot[bot]@users.noreply.github.com>
|
||||||
derekriemer <derek.riemer@colorado.edu>
|
derekriemer <derek.riemer@colorado.edu>
|
||||||
|
DerRockWolf <50499906+DerRockWolf@users.noreply.github.com>
|
||||||
desbma <desbma@users.noreply.github.com>
|
desbma <desbma@users.noreply.github.com>
|
||||||
Devon G. Redekopp <devon@redekopp.com>
|
Devon G. Redekopp <devon@redekopp.com>
|
||||||
|
diemade <spamkill@posteo.ch>
|
||||||
|
digital <didev@dinid.net>
|
||||||
Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com>
|
Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com>
|
||||||
Dmitry Saveliev (dsaveliev) <d.e.saveliev@gmail.com>
|
Dmitry Saveliev (dsaveliev) <d.e.saveliev@gmail.com>
|
||||||
|
domain <32405309+szu17dmy@users.noreply.github.com>
|
||||||
Domenic Horner <domenic@tgxn.net>
|
Domenic Horner <domenic@tgxn.net>
|
||||||
Dominik Heidler (asdil12) <dominik@heidler.eu>
|
Dominik Heidler (asdil12) <dominik@heidler.eu>
|
||||||
Elias Jarlebring (jarlebring) <jarlebring@gmail.com>
|
Elias Jarlebring (jarlebring) <jarlebring@gmail.com>
|
||||||
Elliot Huffman <thelich2@gmail.com>
|
Elliot Huffman <thelich2@gmail.com>
|
||||||
Emil Hessman (ceh) <emil@hessman.se>
|
Emil Hessman (ceh) <emil@hessman.se>
|
||||||
|
Emil Lundberg <emil@emlun.se>
|
||||||
Eng Zer Jun <engzerjun@gmail.com>
|
Eng Zer Jun <engzerjun@gmail.com>
|
||||||
entity0xfe <109791748+entity0xfe@users.noreply.github.com> <entity0xfe@my.domain>
|
entity0xfe <109791748+entity0xfe@users.noreply.github.com> <entity0xfe@my.domain>
|
||||||
Eric Lesiuta <elesiuta@gmail.com>
|
Eric Lesiuta <elesiuta@gmail.com>
|
||||||
@@ -111,6 +129,7 @@ Erik Meitner (WSGCSysadmin) <e.meitner@willystreet.coop>
|
|||||||
Evan Spensley <94762716+0evan@users.noreply.github.com>
|
Evan Spensley <94762716+0evan@users.noreply.github.com>
|
||||||
Evgeny Kuznetsov <evgeny@kuznetsov.md>
|
Evgeny Kuznetsov <evgeny@kuznetsov.md>
|
||||||
Federico Castagnini (facastagnini) <federico.castagnini@gmail.com>
|
Federico Castagnini (facastagnini) <federico.castagnini@gmail.com>
|
||||||
|
Felix <53702818+f-eliks@users.noreply.github.com>
|
||||||
Felix Ableitner (Nutomic) <me@nutomic.com>
|
Felix Ableitner (Nutomic) <me@nutomic.com>
|
||||||
Felix Lampe <mail@flampe.de>
|
Felix Lampe <mail@flampe.de>
|
||||||
Felix Unterpaintner (bigbear2nd) <bigbear2nd@gmail.com>
|
Felix Unterpaintner (bigbear2nd) <bigbear2nd@gmail.com>
|
||||||
@@ -124,10 +143,15 @@ Gleb Sinyavskiy <zhulik.gleb@gmail.com>
|
|||||||
Graham Miln (grahammiln) <graham.miln@dssw.co.uk> <graham.miln@miln.eu>
|
Graham Miln (grahammiln) <graham.miln@dssw.co.uk> <graham.miln@miln.eu>
|
||||||
greatroar <61184462+greatroar@users.noreply.github.com>
|
greatroar <61184462+greatroar@users.noreply.github.com>
|
||||||
Greg <gco@jazzhaiku.com>
|
Greg <gco@jazzhaiku.com>
|
||||||
|
guangwu <guoguangwu@magic-shield.com>
|
||||||
|
gudvinr <gudvinr@gmail.com>
|
||||||
|
Gusted <postmaster@gusted.xyz> <williamzijl7@hotmail.com>
|
||||||
Han Boetes <han@boetes.org>
|
Han Boetes <han@boetes.org>
|
||||||
HansK-p <42314815+HansK-p@users.noreply.github.com>
|
HansK-p <42314815+HansK-p@users.noreply.github.com>
|
||||||
Harrison Jones (harrisonhjones) <harrisonhjones@users.noreply.github.com>
|
Harrison Jones (harrisonhjones) <harrisonhjones@users.noreply.github.com>
|
||||||
|
Hazem Krimi <me@hazemkrimi.tech>
|
||||||
Heiko Zuerker (Smiley73) <heiko@zuerker.org>
|
Heiko Zuerker (Smiley73) <heiko@zuerker.org>
|
||||||
|
Hireworks <129852174+hireworksltd@users.noreply.github.com>
|
||||||
Hugo Locurcio <hugo.locurcio@hugo.pro>
|
Hugo Locurcio <hugo.locurcio@hugo.pro>
|
||||||
Iain Barnett <iainspeed@gmail.com>
|
Iain Barnett <iainspeed@gmail.com>
|
||||||
Ian Johnson (anonymouse64) <ian.johnson@canonical.com> <person.uwsome@gmail.com>
|
Ian Johnson (anonymouse64) <ian.johnson@canonical.com> <person.uwsome@gmail.com>
|
||||||
@@ -140,13 +164,14 @@ Jacek Szafarkiewicz (hadogenes) <szafar@linux.pl>
|
|||||||
Jack Croft <jccroft1@users.noreply.github.com>
|
Jack Croft <jccroft1@users.noreply.github.com>
|
||||||
Jacob <jyundt@gmail.com>
|
Jacob <jyundt@gmail.com>
|
||||||
Jake Peterson (acogdev) <jake@acogdev.com>
|
Jake Peterson (acogdev) <jake@acogdev.com>
|
||||||
Jakob Borg (calmh) <jakob@nym.se> <jakob@kastelo.net>
|
Jakob Borg (calmh) <jakob@nym.se> <jakob@kastelo.net> <jborg@coreweave.com>
|
||||||
James O'Beirne <wild-github@au92.org>
|
James O'Beirne <wild-github@au92.org>
|
||||||
James Patterson (jpjp) <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
|
James Patterson (jpjp) <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
|
||||||
janost <janost@tuta.io>
|
janost <janost@tuta.io>
|
||||||
Jaroslav Lichtblau <svetlemodry@users.noreply.github.com>
|
Jaroslav Lichtblau <svetlemodry@users.noreply.github.com>
|
||||||
Jaroslav Malec (dzarda) <dzardacz@gmail.com>
|
Jaroslav Malec (dzarda) <dzardacz@gmail.com>
|
||||||
jaseg <githubaccount@jaseg.net>
|
jaseg <githubaccount@jaseg.net>
|
||||||
|
Jaspitta <ste.scarpitta@gmail.com>
|
||||||
Jauder Ho <jauderho@users.noreply.github.com>
|
Jauder Ho <jauderho@users.noreply.github.com>
|
||||||
Jaya Chithra (jayachithra) <s.k.jayachithra@gmail.com>
|
Jaya Chithra (jayachithra) <s.k.jayachithra@gmail.com>
|
||||||
Jaya Kumar <jaya.kumar@ict.nl>
|
Jaya Kumar <jaya.kumar@ict.nl>
|
||||||
@@ -165,11 +190,15 @@ Jonathan Cross <jcross@gmail.com>
|
|||||||
Jonta <359397+Jonta@users.noreply.github.com>
|
Jonta <359397+Jonta@users.noreply.github.com>
|
||||||
Jose Manuel Delicado (jmdaweb) <jmdaweb@hotmail.com> <jmdaweb@users.noreply.github.com>
|
Jose Manuel Delicado (jmdaweb) <jmdaweb@hotmail.com> <jmdaweb@users.noreply.github.com>
|
||||||
jtagcat <git-514635f7@jtag.cat> <git-12dbd862@jtag.cat>
|
jtagcat <git-514635f7@jtag.cat> <git-12dbd862@jtag.cat>
|
||||||
|
Julian Lehrhuber <jul13579@users.noreply.github.com>
|
||||||
Jörg Thalheim <Mic92@users.noreply.github.com>
|
Jörg Thalheim <Mic92@users.noreply.github.com>
|
||||||
Jędrzej Kula <kula.jedrek@gmail.com>
|
Jędrzej Kula <kula.jedrek@gmail.com>
|
||||||
|
K.B.Dharun Krishna <kbdharunkrishna@gmail.com>
|
||||||
Kalle Laine <pahakalle@protonmail.com>
|
Kalle Laine <pahakalle@protonmail.com>
|
||||||
|
Kapil Sareen <kapilsareen584@gmail.com>
|
||||||
Karol Różycki (krozycki) <rozycki.karol@gmail.com>
|
Karol Różycki (krozycki) <rozycki.karol@gmail.com>
|
||||||
Kebin Liu <lkebin@gmail.com>
|
Kebin Liu <lkebin@gmail.com>
|
||||||
|
Keith Harrison <keithh@protonmail.com>
|
||||||
Keith Turner <kturner@apache.org>
|
Keith Turner <kturner@apache.org>
|
||||||
Kelong Cong (kc1212) <kc04bc@gmx.com> <kc1212@users.noreply.github.com>
|
Kelong Cong (kc1212) <kc04bc@gmx.com> <kc1212@users.noreply.github.com>
|
||||||
Ken'ichi Kamada (kamadak) <kamada@nanohz.org>
|
Ken'ichi Kamada (kamadak) <kamada@nanohz.org>
|
||||||
@@ -178,6 +207,7 @@ Kevin Bushiri (keevBush) <keevbush@gmail.com> <36192217+keevBush@users.noreply.g
|
|||||||
Kevin White, Jr. (kwhite17) <kevinwhite1710@gmail.com>
|
Kevin White, Jr. (kwhite17) <kevinwhite1710@gmail.com>
|
||||||
klemens <ka7@github.com>
|
klemens <ka7@github.com>
|
||||||
Kurt Fitzner (Kudalufi) <kurt@va1der.ca> <kurt.fitzner@gmail.com>
|
Kurt Fitzner (Kudalufi) <kurt@va1der.ca> <kurt.fitzner@gmail.com>
|
||||||
|
kylosus <33132401+kylosus@users.noreply.github.com>
|
||||||
Lars K.W. Gohlke (lkwg82) <lkwg82@gmx.de>
|
Lars K.W. Gohlke (lkwg82) <lkwg82@gmx.de>
|
||||||
Lars Lehtonen <lars.lehtonen@gmail.com>
|
Lars Lehtonen <lars.lehtonen@gmail.com>
|
||||||
Laurent Arnoud <laurent@spkdev.net>
|
Laurent Arnoud <laurent@spkdev.net>
|
||||||
@@ -187,19 +217,25 @@ Liu Siyuan (liusy182) <liusy182@gmail.com> <liusy182@hotmail.com>
|
|||||||
Lode Hoste (Zillode) <zillode@zillode.be>
|
Lode Hoste (Zillode) <zillode@zillode.be>
|
||||||
Lord Landon Agahnim (LordLandon) <lordlandon@gmail.com>
|
Lord Landon Agahnim (LordLandon) <lordlandon@gmail.com>
|
||||||
LSmithx2 <42276854+lsmithx2@users.noreply.github.com>
|
LSmithx2 <42276854+lsmithx2@users.noreply.github.com>
|
||||||
|
luchenhan <168071714+luchenhan@users.noreply.github.com>
|
||||||
Lukas Lihotzki <lukas@lihotzki.de>
|
Lukas Lihotzki <lukas@lihotzki.de>
|
||||||
|
Luke Hamburg <1992842+luckman212@users.noreply.github.com>
|
||||||
luzpaz <luzpaz@users.noreply.github.com>
|
luzpaz <luzpaz@users.noreply.github.com>
|
||||||
Majed Abdulaziz (majedev) <majed.alhajry@gmail.com>
|
Majed Abdulaziz (majedev) <majed.alhajry@gmail.com>
|
||||||
Marc Laporte (marclaporte) <marc@marclaporte.com> <marc@laporte.name>
|
Marc Laporte (marclaporte) <marc@marclaporte.com> <marc@laporte.name>
|
||||||
Marc Pujol (kilburn) <kilburn@la3.org>
|
Marc Pujol (kilburn) <kilburn@la3.org>
|
||||||
|
Marcel Meyer <mm.marcelmeyer@gmail.com>
|
||||||
Marcin Dziadus (marcindziadus) <dziadus.marcin@gmail.com>
|
Marcin Dziadus (marcindziadus) <dziadus.marcin@gmail.com>
|
||||||
marco-m <marco.molteni@laposte.net>
|
marco-m <marco.molteni@laposte.net>
|
||||||
|
Marcus B Spencer <marcus@marcusspencer.xyz> <marcus@marcusspencer.us>
|
||||||
Marcus Legendre <marcus.legendre@gmail.com>
|
Marcus Legendre <marcus.legendre@gmail.com>
|
||||||
Mario Majila <mariustshipichik@gmail.com>
|
Mario Majila <mariustshipichik@gmail.com>
|
||||||
Mark Pulford (mpx) <mark@kyne.com.au>
|
Mark Pulford (mpx) <mark@kyne.com.au>
|
||||||
Martchus <martchus@gmx.net>
|
Martchus <martchus@gmx.net>
|
||||||
|
Martin Polehla <p0l0us@users.noreply.github.com>
|
||||||
Mateusz Naściszewski (mateon1) <matin1111@wp.pl>
|
Mateusz Naściszewski (mateon1) <matin1111@wp.pl>
|
||||||
Mateusz Ż <thedead4fun@live.com>
|
Mateusz Ż <thedead4fun@live.com>
|
||||||
|
mathias4833 <67101597+mathias4833@users.noreply.github.com>
|
||||||
Matic Potočnik <hairyfotr@gmail.com>
|
Matic Potočnik <hairyfotr@gmail.com>
|
||||||
Matt Burke (burkemw3) <mburke@amplify.com> <burkemw3@gmail.com>
|
Matt Burke (burkemw3) <mburke@amplify.com> <burkemw3@gmail.com>
|
||||||
Matt Robenolt <matt@ydekproductions.com>
|
Matt Robenolt <matt@ydekproductions.com>
|
||||||
@@ -207,14 +243,17 @@ Matteo Ruina <matteo.ruina@gmail.com>
|
|||||||
Maurizio Tomasi <ziotom78@gmail.com>
|
Maurizio Tomasi <ziotom78@gmail.com>
|
||||||
Max <github@germancoding.com>
|
Max <github@germancoding.com>
|
||||||
Max Schulze (kralo) <max.schulze@online.de> <kralo@users.noreply.github.com>
|
Max Schulze (kralo) <max.schulze@online.de> <kralo@users.noreply.github.com>
|
||||||
|
maxice8 <30738253+maxice8@users.noreply.github.com>
|
||||||
MaximAL <almaximal@ya.ru>
|
MaximAL <almaximal@ya.ru>
|
||||||
Maxime Thirouin <m@moox.io>
|
Maxime Thirouin <m@moox.io>
|
||||||
|
Maximilian <maxi.rostock@outlook.de> <public@complexvector.space>
|
||||||
mclang <1721600+mclang@users.noreply.github.com>
|
mclang <1721600+mclang@users.noreply.github.com>
|
||||||
Michael Jephcote (Rewt0r) <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
|
Michael Jephcote (Rewt0r) <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
|
||||||
Michael Ploujnikov (plouj) <ploujj@gmail.com>
|
Michael Ploujnikov (plouj) <ploujj@gmail.com>
|
||||||
Michael Rienstra <mrienstra@gmail.com>
|
Michael Rienstra <mrienstra@gmail.com>
|
||||||
Michael Tilli (pyfisch) <pyfisch@gmail.com>
|
Michael Tilli (pyfisch) <pyfisch@gmail.com>
|
||||||
MichaIng <micha@dietpi.com>
|
MichaIng <micha@dietpi.com>
|
||||||
|
Migelo <miha@filetki.si>
|
||||||
Mike Boone <mike@boonedocks.net>
|
Mike Boone <mike@boonedocks.net>
|
||||||
MikeLund <MikeLund@users.noreply.github.com>
|
MikeLund <MikeLund@users.noreply.github.com>
|
||||||
MikolajTwarog <43782609+MikolajTwarog@users.noreply.github.com>
|
MikolajTwarog <43782609+MikolajTwarog@users.noreply.github.com>
|
||||||
@@ -222,6 +261,7 @@ Mingxuan Lin <gdlmx@users.noreply.github.com>
|
|||||||
mv1005 <49659413+mv1005@users.noreply.github.com>
|
mv1005 <49659413+mv1005@users.noreply.github.com>
|
||||||
Nate Morrison (nrm21) <natemorrison@gmail.com>
|
Nate Morrison (nrm21) <natemorrison@gmail.com>
|
||||||
Naveen <172697+naveensrinivasan@users.noreply.github.com>
|
Naveen <172697+naveensrinivasan@users.noreply.github.com>
|
||||||
|
nf <nf@wh3rd.net>
|
||||||
Nicholas Rishel (PrototypeNM1) <rishel.nick@gmail.com> <PrototypeNM1@users.noreply.github.com>
|
Nicholas Rishel (PrototypeNM1) <rishel.nick@gmail.com> <PrototypeNM1@users.noreply.github.com>
|
||||||
Nick Busey <NickBusey@users.noreply.github.com>
|
Nick Busey <NickBusey@users.noreply.github.com>
|
||||||
Nico Stapelbroek <3368018+nstapelbroek@users.noreply.github.com>
|
Nico Stapelbroek <3368018+nstapelbroek@users.noreply.github.com>
|
||||||
@@ -233,6 +273,7 @@ NinoM4ster <ninom4ster@gmail.com>
|
|||||||
Nitroretro <43112364+Nitroretro@users.noreply.github.com>
|
Nitroretro <43112364+Nitroretro@users.noreply.github.com>
|
||||||
NoLooseEnds <jon.koslung@gmail.com>
|
NoLooseEnds <jon.koslung@gmail.com>
|
||||||
Oliver Freyermuth <o.freyermuth@googlemail.com>
|
Oliver Freyermuth <o.freyermuth@googlemail.com>
|
||||||
|
orangekame3 <miya.org.0309@gmail.com>
|
||||||
otbutz <tbutz@optitool.de>
|
otbutz <tbutz@optitool.de>
|
||||||
Otiel <Otiel@users.noreply.github.com>
|
Otiel <Otiel@users.noreply.github.com>
|
||||||
overkill <22098433+0verk1ll@users.noreply.github.com>
|
overkill <22098433+0verk1ll@users.noreply.github.com>
|
||||||
@@ -240,6 +281,7 @@ Oyebanji Jacob Mayowa <oyebanji05@gmail.com>
|
|||||||
Pablo <pbaeyens31+github@gmail.com>
|
Pablo <pbaeyens31+github@gmail.com>
|
||||||
Pascal Jungblut (pascalj) <github@pascalj.com> <mail@pascal-jungblut.com>
|
Pascal Jungblut (pascalj) <github@pascalj.com> <mail@pascal-jungblut.com>
|
||||||
Paul Brit <paulbrit44@gmail.com>
|
Paul Brit <paulbrit44@gmail.com>
|
||||||
|
Paul Donald <newtwen+github@gmail.com>
|
||||||
Pawel Palenica (qepasa) <pawelpalenica11@gmail.com>
|
Pawel Palenica (qepasa) <pawelpalenica11@gmail.com>
|
||||||
Paweł Rozlach <vespian@users.noreply.github.com>
|
Paweł Rozlach <vespian@users.noreply.github.com>
|
||||||
perewa <cavalcante.ten@gmail.com>
|
perewa <cavalcante.ten@gmail.com>
|
||||||
@@ -253,7 +295,9 @@ Philippe Schommers (filoozoom) <philippe@schommers.be>
|
|||||||
Phill Luby (pluby) <phill.luby@newredo.com>
|
Phill Luby (pluby) <phill.luby@newredo.com>
|
||||||
Pier Paolo Ramon <ramonpierre@gmail.com>
|
Pier Paolo Ramon <ramonpierre@gmail.com>
|
||||||
Piotr Bejda (piobpl) <piotrb10@gmail.com>
|
Piotr Bejda (piobpl) <piotrb10@gmail.com>
|
||||||
|
polyfloyd <polyfloyd@users.noreply.github.com>
|
||||||
Pramodh KP (pramodhkp) <pramodh.p@directi.com> <1507241+pramodhkp@users.noreply.github.com>
|
Pramodh KP (pramodhkp) <pramodh.p@directi.com> <1507241+pramodhkp@users.noreply.github.com>
|
||||||
|
pullmerge <166967364+pullmerge@users.noreply.github.com>
|
||||||
Quentin Hibon <qh.public@yahoo.com>
|
Quentin Hibon <qh.public@yahoo.com>
|
||||||
Rahmi Pruitt <rjpruitt16@gmail.com>
|
Rahmi Pruitt <rjpruitt16@gmail.com>
|
||||||
red_led <red-led@users.noreply.github.com>
|
red_led <red-led@users.noreply.github.com>
|
||||||
@@ -271,30 +315,43 @@ Sacheendra Talluri (sacheendra) <sacheendra.t@gmail.com>
|
|||||||
Scott Klupfel (kluppy) <kluppy@going2blue.com>
|
Scott Klupfel (kluppy) <kluppy@going2blue.com>
|
||||||
sec65 <106604020+sec65@users.noreply.github.com>
|
sec65 <106604020+sec65@users.noreply.github.com>
|
||||||
Sergey Mishin (ralder) <ralder@yandex.ru>
|
Sergey Mishin (ralder) <ralder@yandex.ru>
|
||||||
|
Sertonix <83883937+Sertonix@users.noreply.github.com>
|
||||||
|
Severin von Wnuck-Lipinski <ss7@live.de>
|
||||||
Shaarad Dalvi <60266155+shaaraddalvi@users.noreply.github.com> <shdalv@microsoft.com>
|
Shaarad Dalvi <60266155+shaaraddalvi@users.noreply.github.com> <shdalv@microsoft.com>
|
||||||
Simon Frei (imsodin) <freisim93@gmail.com>
|
Simon Frei (imsodin) <freisim93@gmail.com>
|
||||||
Simon Mwepu <simonmwepu@gmail.com>
|
Simon Mwepu <simonmwepu@gmail.com>
|
||||||
|
Simon Pickup <simon@pickupinfinity.com>
|
||||||
Sly_tom_cat <slytomcat@mail.ru>
|
Sly_tom_cat <slytomcat@mail.ru>
|
||||||
|
Sonu Kumar Saw <31889738+dev-saw99@users.noreply.github.com>
|
||||||
Stefan Kuntz (Stefan-Code) <stefan.github@gmail.com> <Stefan.github@gmail.com>
|
Stefan Kuntz (Stefan-Code) <stefan.github@gmail.com> <Stefan.github@gmail.com>
|
||||||
Stefan Tatschner (rumpelsepp) <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org> <stefan@rumpelsepp.org>
|
Stefan Tatschner (rumpelsepp) <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org> <stefan@rumpelsepp.org>
|
||||||
Steven Eckhoff <steven.eckhoff.opensource@gmail.com>
|
Steven Eckhoff <steven.eckhoff.opensource@gmail.com>
|
||||||
Suhas Gundimeda (snugghash) <suhas.gundimeda@gmail.com> <snugghash@gmail.com>
|
Suhas Gundimeda (snugghash) <suhas.gundimeda@gmail.com> <snugghash@gmail.com>
|
||||||
|
Sven Bachmann <dev@mcbachmann.de>
|
||||||
Syncthing Automation <automation@syncthing.net>
|
Syncthing Automation <automation@syncthing.net>
|
||||||
Syncthing Release Automation <release@syncthing.net>
|
Syncthing Release Automation <release@syncthing.net>
|
||||||
|
Sébastien WENSKE <sebastien@wenske.fr>
|
||||||
Taylor Khan (nelsonkhan) <nelsonkhan@gmail.com>
|
Taylor Khan (nelsonkhan) <nelsonkhan@gmail.com>
|
||||||
|
Terrance <git@terrance.allofti.me>
|
||||||
|
TheCreeper <TheCreeper@users.noreply.github.com>
|
||||||
|
Thomas <9749173+uhthomas@users.noreply.github.com>
|
||||||
Thomas Hipp <thomashipp@gmail.com>
|
Thomas Hipp <thomashipp@gmail.com>
|
||||||
Tim Abell (timabell) <tim@timwise.co.uk>
|
Tim Abell (timabell) <tim@timwise.co.uk>
|
||||||
Tim Howes (timhowes) <timhowes@berkeley.edu>
|
Tim Howes (timhowes) <timhowes@berkeley.edu>
|
||||||
|
Tim Nordenfur <tim@gurka.se>
|
||||||
|
Tobias Frölich <40638719+tobifroe@users.noreply.github.com>
|
||||||
Tobias Klauser <tobias.klauser@gmail.com>
|
Tobias Klauser <tobias.klauser@gmail.com>
|
||||||
Tobias Nygren (tnn2) <tnn@nygren.pp.se>
|
Tobias Nygren (tnn2) <tnn@nygren.pp.se>
|
||||||
Tobias Tom (tobiastom) <t.tom@succont.de>
|
Tobias Tom (tobiastom) <t.tom@succont.de>
|
||||||
Tom Jakubowski <tom@crystae.net>
|
Tom Jakubowski <tom@crystae.net>
|
||||||
Tomasz Wilczyński <5626656+tomasz1986@users.noreply.github.com> <twilczynski@naver.com>
|
Tomasz Wilczyński <5626656+tomasz1986@users.noreply.github.com> <twilczynski@naver.com>
|
||||||
Tommy Thorn <tommy-github-email@thorn.ws>
|
Tommy Thorn <tommy-github-email@thorn.ws>
|
||||||
|
Tommy van der Vorst <tommy-github@pixelspark.nl> <tommy@pixelspark.nl>
|
||||||
Tully Robinson (tojrobinson) <tully@tojr.org>
|
Tully Robinson (tojrobinson) <tully@tojr.org>
|
||||||
Tyler Brazier (tylerbrazier) <tyler@tylerbrazier.com>
|
Tyler Brazier (tylerbrazier) <tyler@tylerbrazier.com>
|
||||||
Tyler Kropp <kropptyler@gmail.com>
|
Tyler Kropp <kropptyler@gmail.com>
|
||||||
Unrud (Unrud) <unrud@openaliasbox.org> <Unrud@users.noreply.github.com>
|
Unrud (Unrud) <unrud@openaliasbox.org> <Unrud@users.noreply.github.com>
|
||||||
|
vapatel2 <149737089+vapatel2@users.noreply.github.com>
|
||||||
Veeti Paananen (veeti) <veeti.paananen@rojekti.fi>
|
Veeti Paananen (veeti) <veeti.paananen@rojekti.fi>
|
||||||
Victor Buinsky (buinsky) <vix_booja@tut.by>
|
Victor Buinsky (buinsky) <vix_booja@tut.by>
|
||||||
Vik <63919734+ViktorOn@users.noreply.github.com>
|
Vik <63919734+ViktorOn@users.noreply.github.com>
|
||||||
@@ -302,6 +359,8 @@ Vil Brekin (Vilbrekin) <vilbrekin@gmail.com>
|
|||||||
villekalliomaki <53118179+villekalliomaki@users.noreply.github.com>
|
villekalliomaki <53118179+villekalliomaki@users.noreply.github.com>
|
||||||
Vladimir Rusinov <vrusinov@google.com> <vladimir.rusinov@gmail.com>
|
Vladimir Rusinov <vrusinov@google.com> <vladimir.rusinov@gmail.com>
|
||||||
wangguoliang <liangcszzu@163.com>
|
wangguoliang <liangcszzu@163.com>
|
||||||
|
WangXi <xib1102@icloud.com>
|
||||||
|
Will Rouesnel <wrouesnel@wrouesnel.com>
|
||||||
William A. Kennington III (wkennington) <william@wkennington.com>
|
William A. Kennington III (wkennington) <william@wkennington.com>
|
||||||
wouter bolsterlee <wouter@bolsterl.ee>
|
wouter bolsterlee <wouter@bolsterl.ee>
|
||||||
Wulf Weich (wweich) <wweich@users.noreply.github.com> <wweich@gmx.de> <wulf@weich-kr.de>
|
Wulf Weich (wweich) <wweich@users.noreply.github.com> <wweich@gmx.de> <wulf@weich-kr.de>
|
||||||
|
|||||||
+32
-5
@@ -1,15 +1,41 @@
|
|||||||
ARG GOVERSION=latest
|
ARG GOVERSION=latest
|
||||||
|
|
||||||
|
#
|
||||||
|
# Maybe build Syncthing. This is a bit ugly as we can't make an entire
|
||||||
|
# section of the Dockerfile conditional, so we end up always pulling the
|
||||||
|
# golang image as builder. Then we check if the executable we need already
|
||||||
|
# exists (pre-built) otherwise we build it.
|
||||||
|
#
|
||||||
|
|
||||||
FROM golang:$GOVERSION AS builder
|
FROM golang:$GOVERSION AS builder
|
||||||
|
ARG BUILD_USER
|
||||||
|
ARG BUILD_HOST
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
ENV BUILD_HOST=syncthing.net
|
RUN if [ ! -f syncthing-linux-$TARGETARCH ] ; then \
|
||||||
ENV BUILD_USER=docker
|
go run build.go -no-upgrade build syncthing ; \
|
||||||
RUN rm -f syncthing && go run build.go -no-upgrade build syncthing
|
mv syncthing syncthing-linux-$TARGETARCH ; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
#
|
||||||
|
# The rest of the Dockerfile uses the binary from the builder, prebuilt or
|
||||||
|
# not.
|
||||||
|
#
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.authors="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.url="https://syncthing.net" \
|
||||||
|
org.opencontainers.image.documentation="https://docs.syncthing.net" \
|
||||||
|
org.opencontainers.image.source="https://github.com/syncthing/syncthing" \
|
||||||
|
org.opencontainers.image.vendor="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.licenses="MPL-2.0" \
|
||||||
|
org.opencontainers.image.title="Syncthing"
|
||||||
|
|
||||||
EXPOSE 8384 22000/tcp 22000/udp 21027/udp
|
EXPOSE 8384 22000/tcp 22000/udp 21027/udp
|
||||||
|
|
||||||
@@ -17,7 +43,7 @@ VOLUME ["/var/syncthing"]
|
|||||||
|
|
||||||
RUN apk add --no-cache ca-certificates curl libcap su-exec tzdata
|
RUN apk add --no-cache ca-certificates curl libcap su-exec tzdata
|
||||||
|
|
||||||
COPY --from=builder /src/syncthing /bin/syncthing
|
COPY --from=builder /src/syncthing-linux-$TARGETARCH /bin/syncthing
|
||||||
COPY --from=builder /src/script/docker-entrypoint.sh /bin/entrypoint.sh
|
COPY --from=builder /src/script/docker-entrypoint.sh /bin/entrypoint.sh
|
||||||
|
|
||||||
ENV PUID=1000 PGID=1000 HOME=/var/syncthing
|
ENV PUID=1000 PGID=1000 HOME=/var/syncthing
|
||||||
@@ -26,5 +52,6 @@ HEALTHCHECK --interval=1m --timeout=10s \
|
|||||||
CMD curl -fkLsS -m 2 127.0.0.1:8384/rest/noauth/health | grep -o --color=never OK || exit 1
|
CMD curl -fkLsS -m 2 127.0.0.1:8384/rest/noauth/health | grep -o --color=never OK || exit 1
|
||||||
|
|
||||||
ENV STGUIADDRESS=0.0.0.0:8384
|
ENV STGUIADDRESS=0.0.0.0:8384
|
||||||
|
ENV STHOMEDIR=/var/syncthing/config
|
||||||
RUN chmod 755 /bin/entrypoint.sh
|
RUN chmod 755 /bin/entrypoint.sh
|
||||||
ENTRYPOINT ["/bin/entrypoint.sh", "/bin/syncthing", "-home", "/var/syncthing/config"]
|
ENTRYPOINT ["/bin/entrypoint.sh", "/bin/syncthing"]
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
ARG GOVERSION=latest
|
ARG GOVERSION=latest
|
||||||
FROM golang:$GOVERSION
|
FROM golang:$GOVERSION
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.authors="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.url="https://syncthing.net" \
|
||||||
|
org.opencontainers.image.documentation="https://docs.syncthing.net" \
|
||||||
|
org.opencontainers.image.source="https://github.com/syncthing/syncthing" \
|
||||||
|
org.opencontainers.image.vendor="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.licenses="MPL-2.0" \
|
||||||
|
org.opencontainers.image.title="Syncthing Builder"
|
||||||
|
|
||||||
# FPM to build Debian packages
|
# FPM to build Debian packages
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
locales rubygems ruby-dev build-essential git \
|
locales rubygems ruby-dev build-essential git \
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
FROM alpine
|
|
||||||
ARG TARGETARCH
|
|
||||||
|
|
||||||
EXPOSE 8384 22000/tcp 22000/udp 21027/udp
|
|
||||||
|
|
||||||
VOLUME ["/var/syncthing"]
|
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates curl libcap su-exec tzdata
|
|
||||||
|
|
||||||
COPY ./syncthing-linux-$TARGETARCH /bin/syncthing
|
|
||||||
COPY ./script/docker-entrypoint.sh /bin/entrypoint.sh
|
|
||||||
|
|
||||||
ENV PUID=1000 PGID=1000 HOME=/var/syncthing
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=1m --timeout=10s \
|
|
||||||
CMD curl -fkLsS -m 2 127.0.0.1:8384/rest/noauth/health | grep -o --color=never OK || exit 1
|
|
||||||
|
|
||||||
ENV STGUIADDRESS=0.0.0.0:8384
|
|
||||||
ENTRYPOINT ["/bin/entrypoint.sh", "/bin/syncthing", "-home", "/var/syncthing/config"]
|
|
||||||
+10
-12
@@ -1,18 +1,16 @@
|
|||||||
ARG GOVERSION=latest
|
|
||||||
FROM golang:$GOVERSION AS builder
|
|
||||||
|
|
||||||
WORKDIR /src
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
ENV CGO_ENABLED=0
|
|
||||||
ENV BUILD_HOST=syncthing.net
|
|
||||||
ENV BUILD_USER=docker
|
|
||||||
RUN rm -f stcrashreceiver && go run build.go build stcrashreceiver
|
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.authors="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.url="https://syncthing.net" \
|
||||||
|
org.opencontainers.image.documentation="https://docs.syncthing.net" \
|
||||||
|
org.opencontainers.image.source="https://github.com/syncthing/syncthing" \
|
||||||
|
org.opencontainers.image.vendor="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.licenses="MPL-2.0" \
|
||||||
|
org.opencontainers.image.title="Syncthing Crash Receiver"
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
COPY --from=builder /src/stcrashreceiver /bin/stcrashreceiver
|
COPY stcrashreceiver-linux-${TARGETARCH} /bin/stcrashreceiver
|
||||||
|
|
||||||
ENTRYPOINT [ "/bin/stcrashreceiver" ]
|
ENTRYPOINT [ "/bin/stcrashreceiver" ]
|
||||||
|
|||||||
+17
-4
@@ -1,15 +1,28 @@
|
|||||||
ARG GOVERSION=latest
|
ARG GOVERSION=latest
|
||||||
FROM golang:$GOVERSION AS builder
|
FROM golang:$GOVERSION AS builder
|
||||||
|
ARG BUILD_USER
|
||||||
|
ARG BUILD_HOST
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
ENV BUILD_HOST=syncthing.net
|
RUN if [ ! -f stdiscosrv-linux-$TARGETARCH ] ; then \
|
||||||
ENV BUILD_USER=docker
|
go run build.go -no-upgrade build stdiscosrv ; \
|
||||||
RUN rm -f stdiscosrv && go run build.go -no-upgrade build stdiscosrv
|
mv stdiscosrv stdiscosrv-linux-$TARGETARCH ; \
|
||||||
|
fi
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.authors="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.url="https://syncthing.net" \
|
||||||
|
org.opencontainers.image.documentation="https://docs.syncthing.net" \
|
||||||
|
org.opencontainers.image.source="https://github.com/syncthing/syncthing" \
|
||||||
|
org.opencontainers.image.vendor="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.licenses="MPL-2.0" \
|
||||||
|
org.opencontainers.image.title="Syncthing Discovery Server"
|
||||||
|
|
||||||
EXPOSE 19200 8443
|
EXPOSE 19200 8443
|
||||||
|
|
||||||
@@ -17,7 +30,7 @@ VOLUME ["/var/stdiscosrv"]
|
|||||||
|
|
||||||
RUN apk add --no-cache ca-certificates su-exec
|
RUN apk add --no-cache ca-certificates su-exec
|
||||||
|
|
||||||
COPY --from=builder /src/stdiscosrv /bin/stdiscosrv
|
COPY --from=builder /src/stdiscosrv-linux-$TARGETARCH /bin/stdiscosrv
|
||||||
COPY --from=builder /src/script/docker-entrypoint.sh /bin/entrypoint.sh
|
COPY --from=builder /src/script/docker-entrypoint.sh /bin/entrypoint.sh
|
||||||
|
|
||||||
ENV PUID=1000 PGID=1000 HOME=/var/stdiscosrv
|
ENV PUID=1000 PGID=1000 HOME=/var/stdiscosrv
|
||||||
|
|||||||
+11
-21
@@ -1,26 +1,16 @@
|
|||||||
ARG GOVERSION=latest
|
|
||||||
FROM golang:$GOVERSION AS builder
|
|
||||||
|
|
||||||
WORKDIR /src
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
ENV CGO_ENABLED=0
|
|
||||||
ENV BUILD_HOST=syncthing.net
|
|
||||||
ENV BUILD_USER=docker
|
|
||||||
RUN rm -f strelaysrv && go run build.go -no-upgrade build strelaypoolsrv
|
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.authors="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.url="https://syncthing.net" \
|
||||||
|
org.opencontainers.image.documentation="https://docs.syncthing.net" \
|
||||||
|
org.opencontainers.image.source="https://github.com/syncthing/syncthing" \
|
||||||
|
org.opencontainers.image.vendor="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.licenses="MPL-2.0" \
|
||||||
|
org.opencontainers.image.title="Syncthing Relay Pool Server"
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates su-exec curl
|
COPY strelaypoolsrv-linux-${TARGETARCH} /bin/strelaypoolsrv
|
||||||
ENV PUID=1000 PGID=1000 MAXMIND_KEY=
|
|
||||||
|
|
||||||
RUN mkdir /var/strelaypoolsrv && chown 1000 /var/strelaypoolsrv
|
ENTRYPOINT ["/bin/strelaypoolsrv", "-listen", ":8080"]
|
||||||
USER 1000
|
|
||||||
|
|
||||||
COPY --from=builder /src/strelaypoolsrv /bin/strelaypoolsrv
|
|
||||||
COPY --from=builder /src/script/strelaypoolsrv-entrypoint.sh /bin/entrypoint.sh
|
|
||||||
|
|
||||||
WORKDIR /var/strelaypoolsrv
|
|
||||||
ENTRYPOINT ["/bin/entrypoint.sh", "/bin/strelaypoolsrv", "-listen", ":8080"]
|
|
||||||
|
|||||||
+17
-4
@@ -1,15 +1,28 @@
|
|||||||
ARG GOVERSION=latest
|
ARG GOVERSION=latest
|
||||||
FROM golang:$GOVERSION AS builder
|
FROM golang:$GOVERSION AS builder
|
||||||
|
ARG BUILD_USER
|
||||||
|
ARG BUILD_HOST
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
ENV BUILD_HOST=syncthing.net
|
RUN if [ ! -f strelaysrv-linux-$TARGETARCH ] ; then \
|
||||||
ENV BUILD_USER=docker
|
go run build.go -no-upgrade build strelaysrv ; \
|
||||||
RUN rm -f strelaysrv && go run build.go -no-upgrade build strelaysrv
|
mv strelaysrv strelaysrv-linux-$TARGETARCH ; \
|
||||||
|
fi
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.authors="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.url="https://syncthing.net" \
|
||||||
|
org.opencontainers.image.documentation="https://docs.syncthing.net" \
|
||||||
|
org.opencontainers.image.source="https://github.com/syncthing/syncthing" \
|
||||||
|
org.opencontainers.image.vendor="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.licenses="MPL-2.0" \
|
||||||
|
org.opencontainers.image.title="Syncthing Relay Server"
|
||||||
|
|
||||||
EXPOSE 22067 22070
|
EXPOSE 22067 22070
|
||||||
|
|
||||||
@@ -17,7 +30,7 @@ VOLUME ["/var/strelaysrv"]
|
|||||||
|
|
||||||
RUN apk add --no-cache ca-certificates su-exec
|
RUN apk add --no-cache ca-certificates su-exec
|
||||||
|
|
||||||
COPY --from=builder /src/strelaysrv /bin/strelaysrv
|
COPY --from=builder /src/strelaysrv-linux-$TARGETARCH /bin/strelaysrv
|
||||||
COPY --from=builder /src/script/docker-entrypoint.sh /bin/entrypoint.sh
|
COPY --from=builder /src/script/docker-entrypoint.sh /bin/entrypoint.sh
|
||||||
|
|
||||||
ENV PUID=1000 PGID=1000 HOME=/var/strelaysrv
|
ENV PUID=1000 PGID=1000 HOME=/var/strelaysrv
|
||||||
|
|||||||
+11
-18
@@ -1,23 +1,16 @@
|
|||||||
ARG GOVERSION=latest
|
|
||||||
FROM golang:$GOVERSION AS builder
|
|
||||||
|
|
||||||
WORKDIR /src
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
ENV CGO_ENABLED=0
|
|
||||||
ENV BUILD_HOST=syncthing.net
|
|
||||||
ENV BUILD_USER=docker
|
|
||||||
RUN rm -f stupgrades && go run build.go build stupgrades
|
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.authors="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.url="https://syncthing.net" \
|
||||||
|
org.opencontainers.image.documentation="https://docs.syncthing.net" \
|
||||||
|
org.opencontainers.image.source="https://github.com/syncthing/syncthing" \
|
||||||
|
org.opencontainers.image.vendor="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.licenses="MPL-2.0" \
|
||||||
|
org.opencontainers.image.title="Syncthing Upgrades"
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
COPY --from=builder /src/stupgrades /bin/stupgrades
|
COPY stupgrades-linux-${TARGETARCH} /bin/stupgrades
|
||||||
|
|
||||||
ENTRYPOINT [ \
|
|
||||||
"/bin/stupgrades", \
|
|
||||||
"-f", "/nightly.json->https://build.syncthing.net/guestAuth/repository/download/Release_Nightly/.lastSuccessful/nightly.json", \
|
|
||||||
"-f", "/syncthing-macos/appcast.xml->https://build.syncthing.net/guestAuth/repository/download/SyncthingMacOS_CreateAppcastXml/.lastSuccessful/appcast.xml" \
|
|
||||||
]
|
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/bin/stupgrades" ]
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
FROM alpine
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.authors="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.url="https://syncthing.net" \
|
||||||
|
org.opencontainers.image.documentation="https://docs.syncthing.net" \
|
||||||
|
org.opencontainers.image.source="https://github.com/syncthing/syncthing" \
|
||||||
|
org.opencontainers.image.vendor="The Syncthing Project" \
|
||||||
|
org.opencontainers.image.licenses="MPL-2.0" \
|
||||||
|
org.opencontainers.image.title="Syncthing Usage Reporting Server"
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
COPY ursrv-linux-${TARGETARCH} /bin/ursrv
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/bin/ursrv" ]
|
||||||
@@ -24,17 +24,17 @@ to avoid corrupting the user's files.
|
|||||||
### 2. Secure Against Attackers
|
### 2. Secure Against Attackers
|
||||||
|
|
||||||
Again, protecting the user's data is paramount. Regardless of our other
|
Again, protecting the user's data is paramount. Regardless of our other
|
||||||
goals we must never allow the user's data to be susceptible to eavesdropping
|
goals, we must never allow the user's data to be susceptible to eavesdropping
|
||||||
or modification by unauthorized parties.
|
or modification by unauthorized parties.
|
||||||
|
|
||||||
> This should be understood in context. It is not necessarily reasonable to
|
> This should be understood in context. It is not necessarily reasonable to
|
||||||
> expect Syncthing to be resistant against well equipped state level
|
> expect Syncthing to be resistant against well equipped state level
|
||||||
> attackers. We will however do our best. Note also that this is different
|
> attackers. We will, however, do our best. Note also that this is different
|
||||||
> from anonymity which is not, currently, a goal.
|
> from anonymity which is not, currently, a goal.
|
||||||
|
|
||||||
### 3. Easy to Use
|
### 3. Easy to Use
|
||||||
|
|
||||||
Syncthing should be approachable, understandable and inclusive.
|
Syncthing should be approachable, understandable, and inclusive.
|
||||||
|
|
||||||
> Complex concepts and maths form the base of Syncthing's functionality.
|
> Complex concepts and maths form the base of Syncthing's functionality.
|
||||||
> This should nonetheless be abstracted or hidden to a degree where
|
> This should nonetheless be abstracted or hidden to a degree where
|
||||||
@@ -52,18 +52,18 @@ User interaction should be required only when absolutely necessary.
|
|||||||
### 5. Universally Available
|
### 5. Universally Available
|
||||||
|
|
||||||
Syncthing should run on every common computer. We are mindful that the
|
Syncthing should run on every common computer. We are mindful that the
|
||||||
latest technology is not always available to any given individual.
|
latest technology is not always available to every individual.
|
||||||
|
|
||||||
> Computers include desktops, laptops, servers, virtual machines, small
|
> Computers include desktops, laptops, servers, virtual machines, small
|
||||||
> general purpose computers such as Raspberry Pis and, *where possible*,
|
> general purpose computers such as Raspberry Pis and, *where possible*,
|
||||||
> tablets and phones. NAS appliances, toasters, cars, firearms, thermostats
|
> tablets and phones. NAS appliances, toasters, cars, firearms, thermostats,
|
||||||
> and so on may include computing capabilities but it is not our goal for
|
> and so on may include computing capabilities but it is not our goal for
|
||||||
> Syncthing to run smoothly on these devices.
|
> Syncthing to run smoothly on these devices.
|
||||||
|
|
||||||
### 6. For Individuals
|
### 6. For Individuals
|
||||||
|
|
||||||
Syncthing is primarily about empowering the individual user with safe,
|
Syncthing is primarily about empowering the individual user with safe,
|
||||||
secure and easy to use file synchronization.
|
secure, and easy to use file synchronization.
|
||||||
|
|
||||||
> We acknowledge that it's also useful in an enterprise setting and include
|
> We acknowledge that it's also useful in an enterprise setting and include
|
||||||
> functionality to support that. If this is in conflict with the
|
> functionality to support that. If this is in conflict with the
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ To grant Syncthing additional capabilities without running as root, use the
|
|||||||
`PCAP` environment variable with the same syntax as that for `setcap(8)`.
|
`PCAP` environment variable with the same syntax as that for `setcap(8)`.
|
||||||
For example, `PCAP=cap_chown,cap_fowner+ep`.
|
For example, `PCAP=cap_chown,cap_fowner+ep`.
|
||||||
|
|
||||||
|
To set a different umask value, use the `UMASK` environment variable. For
|
||||||
|
example `UMASK=002`.
|
||||||
|
|
||||||
## Example Usage
|
## Example Usage
|
||||||
|
|
||||||
**Docker cli**
|
**Docker cli**
|
||||||
@@ -46,6 +49,11 @@ services:
|
|||||||
- 22000:22000/udp # QUIC file transfers
|
- 22000:22000/udp # QUIC file transfers
|
||||||
- 21027:21027/udp # Receive local discovery broadcasts
|
- 21027:21027/udp # Receive local discovery broadcasts
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: curl -fkLsS -m 2 127.0.0.1:8384/rest/noauth/health | grep -o --color=never OK || exit 1
|
||||||
|
interval: 1m
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
```
|
```
|
||||||
|
|
||||||
## Discovery
|
## Discovery
|
||||||
@@ -81,6 +89,11 @@ services:
|
|||||||
- /wherever/st-sync:/var/syncthing
|
- /wherever/st-sync:/var/syncthing
|
||||||
network_mode: host
|
network_mode: host
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: curl -fkLsS -m 2 127.0.0.1:8384/rest/noauth/health | grep -o --color=never OK || exit 1
|
||||||
|
interval: 1m
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
```
|
```
|
||||||
|
|
||||||
Be aware that syncthing alone is now in control of what interfaces and ports it
|
Be aware that syncthing alone is now in control of what interfaces and ports it
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[](https://build.syncthing.net/viewType.html?buildTypeId=Syncthing_BuildLinuxCross&guest=1)
|
|
||||||
[](https://build.syncthing.net/viewType.html?buildTypeId=Syncthing_BuildWindows&guest=1)
|
|
||||||
[](https://build.syncthing.net/viewType.html?buildTypeId=Syncthing_BuildMac&guest=1)
|
|
||||||
[](https://www.mozilla.org/MPL/2.0/)
|
[](https://www.mozilla.org/MPL/2.0/)
|
||||||
[](https://bestpractices.coreinfrastructure.org/projects/88)
|
[](https://bestpractices.coreinfrastructure.org/projects/88)
|
||||||
[](https://goreportcard.com/report/github.com/syncthing/syncthing)
|
[](https://goreportcard.com/report/github.com/syncthing/syncthing)
|
||||||
@@ -13,8 +10,8 @@
|
|||||||
|
|
||||||
Syncthing is a **continuous file synchronization program**. It synchronizes
|
Syncthing is a **continuous file synchronization program**. It synchronizes
|
||||||
files between two or more computers. We strive to fulfill the goals below.
|
files between two or more computers. We strive to fulfill the goals below.
|
||||||
The goals are listed in order of importance, the most important one being
|
The goals are listed in order of importance, the most important ones first.
|
||||||
the first. This is the summary version of the goal list - for more
|
This is the summary version of the goal list - for more
|
||||||
commentary, see the full [Goals document][13].
|
commentary, see the full [Goals document][13].
|
||||||
|
|
||||||
Syncthing should be:
|
Syncthing should be:
|
||||||
@@ -27,12 +24,12 @@ Syncthing should be:
|
|||||||
2. **Secure Against Attackers**
|
2. **Secure Against Attackers**
|
||||||
|
|
||||||
Again, protecting the user's data is paramount. Regardless of our other
|
Again, protecting the user's data is paramount. Regardless of our other
|
||||||
goals we must never allow the user's data to be susceptible to
|
goals, we must never allow the user's data to be susceptible to
|
||||||
eavesdropping or modification by unauthorized parties.
|
eavesdropping or modification by unauthorized parties.
|
||||||
|
|
||||||
3. **Easy to Use**
|
3. **Easy to Use**
|
||||||
|
|
||||||
Syncthing should be approachable, understandable and inclusive.
|
Syncthing should be approachable, understandable, and inclusive.
|
||||||
|
|
||||||
4. **Automatic**
|
4. **Automatic**
|
||||||
|
|
||||||
@@ -41,12 +38,12 @@ Syncthing should be:
|
|||||||
5. **Universally Available**
|
5. **Universally Available**
|
||||||
|
|
||||||
Syncthing should run on every common computer. We are mindful that the
|
Syncthing should run on every common computer. We are mindful that the
|
||||||
latest technology is not always available to any given individual.
|
latest technology is not always available to every individual.
|
||||||
|
|
||||||
6. **For Individuals**
|
6. **For Individuals**
|
||||||
|
|
||||||
Syncthing is primarily about empowering the individual user with safe,
|
Syncthing is primarily about empowering the individual user with safe,
|
||||||
secure and easy to use file synchronization.
|
secure, and easy to use file synchronization.
|
||||||
|
|
||||||
7. **Everything Else**
|
7. **Everything Else**
|
||||||
|
|
||||||
@@ -60,23 +57,22 @@ Take a look at the [getting started guide][2].
|
|||||||
|
|
||||||
There are a few examples for keeping Syncthing running in the background
|
There are a few examples for keeping Syncthing running in the background
|
||||||
on your system in [the etc directory][3]. There are also several [GUI
|
on your system in [the etc directory][3]. There are also several [GUI
|
||||||
implementations][11] for Windows, Mac and Linux.
|
implementations][11] for Windows, Mac, and Linux.
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
To run Syncthing in Docker, see [the Docker README][16].
|
To run Syncthing in Docker, see [the Docker README][16].
|
||||||
|
|
||||||
## Vote on features/bugs
|
|
||||||
|
|
||||||
We'd like to encourage you to [vote][12] on issues that matter to you.
|
|
||||||
This helps the team understand what are the biggest pain points for our users, and could potentially influence what is being worked on next.
|
|
||||||
|
|
||||||
## Getting in Touch
|
## Getting in Touch
|
||||||
|
|
||||||
The first and best point of contact is the [Forum][8].
|
The first and best point of contact is the [Forum][8].
|
||||||
If you've found something that is clearly a
|
If you've found something that is clearly a
|
||||||
bug, feel free to report it in the [GitHub issue tracker][10].
|
bug, feel free to report it in the [GitHub issue tracker][10].
|
||||||
|
|
||||||
|
If you believe that you’ve found a Syncthing-related security vulnerability,
|
||||||
|
please report it by emailing security@syncthing.net. Do not report it in the
|
||||||
|
Forum or issue tracker.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Building Syncthing from source is easy. After extracting the source bundle from
|
Building Syncthing from source is easy. After extracting the source bundle from
|
||||||
@@ -86,13 +82,11 @@ build process.
|
|||||||
|
|
||||||
## Signed Releases
|
## Signed Releases
|
||||||
|
|
||||||
As of v0.10.15 and onwards release binaries are GPG signed with the key
|
Release binaries are GPG signed with the key available from
|
||||||
D26E6ED000654A3E, available from https://syncthing.net/security.html and
|
https://syncthing.net/security/. There is also a built-in automatic
|
||||||
most key servers.
|
upgrade mechanism (disabled in some distribution channels) which uses a
|
||||||
|
compiled in ECDSA signature. macOS and Windows binaries are also
|
||||||
There is also a built in automatic upgrade mechanism (disabled in some
|
code-signed.
|
||||||
distribution channels) which uses a compiled in ECDSA signature. macOS
|
|
||||||
binaries are also properly code signed.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -109,7 +103,6 @@ All code is licensed under the [MPLv2 License][7].
|
|||||||
[8]: https://forum.syncthing.net/
|
[8]: https://forum.syncthing.net/
|
||||||
[10]: https://github.com/syncthing/syncthing/issues
|
[10]: https://github.com/syncthing/syncthing/issues
|
||||||
[11]: https://docs.syncthing.net/users/contrib.html#gui-wrappers
|
[11]: https://docs.syncthing.net/users/contrib.html#gui-wrappers
|
||||||
[12]: https://www.bountysource.com/teams/syncthing/issues
|
|
||||||
[13]: https://github.com/syncthing/syncthing/blob/main/GOALS.md
|
[13]: https://github.com/syncthing/syncthing/blob/main/GOALS.md
|
||||||
[14]: assets/logo-text-128.png
|
[14]: assets/logo-text-128.png
|
||||||
[15]: https://syncthing.net/
|
[15]: https://syncthing.net/
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
version: v2
|
||||||
|
managed:
|
||||||
|
enabled: true
|
||||||
|
override:
|
||||||
|
- file_option: go_package_prefix
|
||||||
|
value: github.com/syncthing/syncthing/internal/gen
|
||||||
|
plugins:
|
||||||
|
- remote: buf.build/protocolbuffers/go:v1.35.1
|
||||||
|
out: .
|
||||||
|
opt: module=github.com/syncthing/syncthing
|
||||||
|
inputs:
|
||||||
|
- directory: proto
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: v2
|
||||||
|
modules:
|
||||||
|
- path: proto
|
||||||
|
name: github.com/syncthing/syncthing
|
||||||
|
lint:
|
||||||
|
use:
|
||||||
|
- STANDARD
|
||||||
|
breaking:
|
||||||
|
use:
|
||||||
|
- WIRE_JSON
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
//go:build ignore
|
//go:build tools
|
||||||
// +build ignore
|
// +build tools
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"compress/flate"
|
"compress/flate"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
@@ -34,6 +33,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
buildpkg "github.com/syncthing/syncthing/lib/build"
|
buildpkg "github.com/syncthing/syncthing/lib/build"
|
||||||
|
"github.com/syncthing/syncthing/lib/upgrade"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -84,7 +85,6 @@ var targets = map[string]target{
|
|||||||
"all": {
|
"all": {
|
||||||
// Only valid for the "build" and "install" commands as it lacks all
|
// Only valid for the "build" and "install" commands as it lacks all
|
||||||
// the archive creation stuff. buildPkgs gets filled out in init()
|
// the archive creation stuff. buildPkgs gets filled out in init()
|
||||||
tags: []string{"purego"},
|
|
||||||
},
|
},
|
||||||
"syncthing": {
|
"syncthing": {
|
||||||
// The default target for "build", "install", "tar", "zip", "deb", etc.
|
// The default target for "build", "install", "tar", "zip", "deb", etc.
|
||||||
@@ -95,41 +95,40 @@ var targets = map[string]target{
|
|||||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/syncthing"},
|
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/syncthing"},
|
||||||
binaryName: "syncthing", // .exe will be added automatically for Windows builds
|
binaryName: "syncthing", // .exe will be added automatically for Windows builds
|
||||||
archiveFiles: []archiveFile{
|
archiveFiles: []archiveFile{
|
||||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
{src: "{{binary}}", dst: "{{binary}}", perm: 0o755},
|
||||||
{src: "README.md", dst: "README.txt", perm: 0644},
|
{src: "README.md", dst: "README.txt", perm: 0o644},
|
||||||
{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
|
{src: "LICENSE", dst: "LICENSE.txt", perm: 0o644},
|
||||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0o644},
|
||||||
// All files from etc/ and extra/ added automatically in init().
|
// All files from etc/ and extra/ added automatically in init().
|
||||||
},
|
},
|
||||||
systemdService: "syncthing@*.service",
|
systemdService: "syncthing@*.service",
|
||||||
installationFiles: []archiveFile{
|
installationFiles: []archiveFile{
|
||||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0o755},
|
||||||
{src: "README.md", dst: "deb/usr/share/doc/syncthing/README.txt", perm: 0644},
|
{src: "README.md", dst: "deb/usr/share/doc/syncthing/README.txt", perm: 0o644},
|
||||||
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing/LICENSE.txt", perm: 0644},
|
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing/LICENSE.txt", perm: 0o644},
|
||||||
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing/AUTHORS.txt", perm: 0644},
|
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing/AUTHORS.txt", perm: 0o644},
|
||||||
{src: "man/syncthing.1", dst: "deb/usr/share/man/man1/syncthing.1", perm: 0644},
|
{src: "man/syncthing.1", dst: "deb/usr/share/man/man1/syncthing.1", perm: 0o644},
|
||||||
{src: "man/syncthing-config.5", dst: "deb/usr/share/man/man5/syncthing-config.5", perm: 0644},
|
{src: "man/syncthing-config.5", dst: "deb/usr/share/man/man5/syncthing-config.5", perm: 0o644},
|
||||||
{src: "man/syncthing-stignore.5", dst: "deb/usr/share/man/man5/syncthing-stignore.5", perm: 0644},
|
{src: "man/syncthing-stignore.5", dst: "deb/usr/share/man/man5/syncthing-stignore.5", perm: 0o644},
|
||||||
{src: "man/syncthing-device-ids.7", dst: "deb/usr/share/man/man7/syncthing-device-ids.7", perm: 0644},
|
{src: "man/syncthing-device-ids.7", dst: "deb/usr/share/man/man7/syncthing-device-ids.7", perm: 0o644},
|
||||||
{src: "man/syncthing-event-api.7", dst: "deb/usr/share/man/man7/syncthing-event-api.7", perm: 0644},
|
{src: "man/syncthing-event-api.7", dst: "deb/usr/share/man/man7/syncthing-event-api.7", perm: 0o644},
|
||||||
{src: "man/syncthing-faq.7", dst: "deb/usr/share/man/man7/syncthing-faq.7", perm: 0644},
|
{src: "man/syncthing-faq.7", dst: "deb/usr/share/man/man7/syncthing-faq.7", perm: 0o644},
|
||||||
{src: "man/syncthing-networking.7", dst: "deb/usr/share/man/man7/syncthing-networking.7", perm: 0644},
|
{src: "man/syncthing-networking.7", dst: "deb/usr/share/man/man7/syncthing-networking.7", perm: 0o644},
|
||||||
{src: "man/syncthing-rest-api.7", dst: "deb/usr/share/man/man7/syncthing-rest-api.7", perm: 0644},
|
{src: "man/syncthing-rest-api.7", dst: "deb/usr/share/man/man7/syncthing-rest-api.7", perm: 0o644},
|
||||||
{src: "man/syncthing-security.7", dst: "deb/usr/share/man/man7/syncthing-security.7", perm: 0644},
|
{src: "man/syncthing-security.7", dst: "deb/usr/share/man/man7/syncthing-security.7", perm: 0o644},
|
||||||
{src: "man/syncthing-versioning.7", dst: "deb/usr/share/man/man7/syncthing-versioning.7", perm: 0644},
|
{src: "man/syncthing-versioning.7", dst: "deb/usr/share/man/man7/syncthing-versioning.7", perm: 0o644},
|
||||||
{src: "etc/linux-systemd/system/syncthing@.service", dst: "deb/lib/systemd/system/syncthing@.service", perm: 0644},
|
{src: "etc/linux-systemd/system/syncthing@.service", dst: "deb/lib/systemd/system/syncthing@.service", perm: 0o644},
|
||||||
{src: "etc/linux-systemd/system/syncthing-resume.service", dst: "deb/lib/systemd/system/syncthing-resume.service", perm: 0644},
|
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0o644},
|
||||||
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0644},
|
{src: "etc/linux-sysctl/30-syncthing.conf", dst: "deb/usr/lib/sysctl.d/30-syncthing.conf", perm: 0o644},
|
||||||
{src: "etc/linux-sysctl/30-syncthing.conf", dst: "deb/usr/lib/sysctl.d/30-syncthing.conf", perm: 0644},
|
{src: "etc/firewall-ufw/syncthing", dst: "deb/etc/ufw/applications.d/syncthing", perm: 0o644},
|
||||||
{src: "etc/firewall-ufw/syncthing", dst: "deb/etc/ufw/applications.d/syncthing", perm: 0644},
|
{src: "etc/linux-desktop/syncthing-start.desktop", dst: "deb/usr/share/applications/syncthing-start.desktop", perm: 0o644},
|
||||||
{src: "etc/linux-desktop/syncthing-start.desktop", dst: "deb/usr/share/applications/syncthing-start.desktop", perm: 0644},
|
{src: "etc/linux-desktop/syncthing-ui.desktop", dst: "deb/usr/share/applications/syncthing-ui.desktop", perm: 0o644},
|
||||||
{src: "etc/linux-desktop/syncthing-ui.desktop", dst: "deb/usr/share/applications/syncthing-ui.desktop", perm: 0644},
|
{src: "assets/logo-32.png", dst: "deb/usr/share/icons/hicolor/32x32/apps/syncthing.png", perm: 0o644},
|
||||||
{src: "assets/logo-32.png", dst: "deb/usr/share/icons/hicolor/32x32/apps/syncthing.png", perm: 0644},
|
{src: "assets/logo-64.png", dst: "deb/usr/share/icons/hicolor/64x64/apps/syncthing.png", perm: 0o644},
|
||||||
{src: "assets/logo-64.png", dst: "deb/usr/share/icons/hicolor/64x64/apps/syncthing.png", perm: 0644},
|
{src: "assets/logo-128.png", dst: "deb/usr/share/icons/hicolor/128x128/apps/syncthing.png", perm: 0o644},
|
||||||
{src: "assets/logo-128.png", dst: "deb/usr/share/icons/hicolor/128x128/apps/syncthing.png", perm: 0644},
|
{src: "assets/logo-256.png", dst: "deb/usr/share/icons/hicolor/256x256/apps/syncthing.png", perm: 0o644},
|
||||||
{src: "assets/logo-256.png", dst: "deb/usr/share/icons/hicolor/256x256/apps/syncthing.png", perm: 0644},
|
{src: "assets/logo-512.png", dst: "deb/usr/share/icons/hicolor/512x512/apps/syncthing.png", perm: 0o644},
|
||||||
{src: "assets/logo-512.png", dst: "deb/usr/share/icons/hicolor/512x512/apps/syncthing.png", perm: 0644},
|
{src: "assets/logo-only.svg", dst: "deb/usr/share/icons/hicolor/scalable/apps/syncthing.svg", perm: 0o644},
|
||||||
{src: "assets/logo-only.svg", dst: "deb/usr/share/icons/hicolor/scalable/apps/syncthing.svg", perm: 0644},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"stdiscosrv": {
|
"stdiscosrv": {
|
||||||
@@ -141,23 +140,22 @@ var targets = map[string]target{
|
|||||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/stdiscosrv"},
|
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/stdiscosrv"},
|
||||||
binaryName: "stdiscosrv", // .exe will be added automatically for Windows builds
|
binaryName: "stdiscosrv", // .exe will be added automatically for Windows builds
|
||||||
archiveFiles: []archiveFile{
|
archiveFiles: []archiveFile{
|
||||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
{src: "{{binary}}", dst: "{{binary}}", perm: 0o755},
|
||||||
{src: "cmd/stdiscosrv/README.md", dst: "README.txt", perm: 0644},
|
{src: "cmd/stdiscosrv/README.md", dst: "README.txt", perm: 0o644},
|
||||||
{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
|
{src: "LICENSE", dst: "LICENSE.txt", perm: 0o644},
|
||||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0o644},
|
||||||
},
|
},
|
||||||
systemdService: "stdiscosrv.service",
|
systemdService: "stdiscosrv.service",
|
||||||
installationFiles: []archiveFile{
|
installationFiles: []archiveFile{
|
||||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0o755},
|
||||||
{src: "cmd/stdiscosrv/README.md", dst: "deb/usr/share/doc/syncthing-discosrv/README.txt", perm: 0644},
|
{src: "cmd/stdiscosrv/README.md", dst: "deb/usr/share/doc/syncthing-discosrv/README.txt", perm: 0o644},
|
||||||
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing-discosrv/LICENSE.txt", perm: 0644},
|
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing-discosrv/LICENSE.txt", perm: 0o644},
|
||||||
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-discosrv/AUTHORS.txt", perm: 0644},
|
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-discosrv/AUTHORS.txt", perm: 0o644},
|
||||||
{src: "man/stdiscosrv.1", dst: "deb/usr/share/man/man1/stdiscosrv.1", perm: 0644},
|
{src: "man/stdiscosrv.1", dst: "deb/usr/share/man/man1/stdiscosrv.1", perm: 0o644},
|
||||||
{src: "cmd/stdiscosrv/etc/linux-systemd/stdiscosrv.service", dst: "deb/lib/systemd/system/stdiscosrv.service", perm: 0644},
|
{src: "cmd/stdiscosrv/etc/linux-systemd/stdiscosrv.service", dst: "deb/lib/systemd/system/stdiscosrv.service", perm: 0o644},
|
||||||
{src: "cmd/stdiscosrv/etc/linux-systemd/default", dst: "deb/etc/default/syncthing-discosrv", perm: 0644},
|
{src: "cmd/stdiscosrv/etc/linux-systemd/default", dst: "deb/etc/default/syncthing-discosrv", perm: 0o644},
|
||||||
{src: "cmd/stdiscosrv/etc/firewall-ufw/stdiscosrv", dst: "deb/etc/ufw/applications.d/stdiscosrv", perm: 0644},
|
{src: "cmd/stdiscosrv/etc/firewall-ufw/stdiscosrv", dst: "deb/etc/ufw/applications.d/stdiscosrv", perm: 0o644},
|
||||||
},
|
},
|
||||||
tags: []string{"purego"},
|
|
||||||
},
|
},
|
||||||
"strelaysrv": {
|
"strelaysrv": {
|
||||||
name: "strelaysrv",
|
name: "strelaysrv",
|
||||||
@@ -168,72 +166,60 @@ var targets = map[string]target{
|
|||||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/strelaysrv"},
|
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/strelaysrv"},
|
||||||
binaryName: "strelaysrv", // .exe will be added automatically for Windows builds
|
binaryName: "strelaysrv", // .exe will be added automatically for Windows builds
|
||||||
archiveFiles: []archiveFile{
|
archiveFiles: []archiveFile{
|
||||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
{src: "{{binary}}", dst: "{{binary}}", perm: 0o755},
|
||||||
{src: "cmd/strelaysrv/README.md", dst: "README.txt", perm: 0644},
|
{src: "cmd/strelaysrv/README.md", dst: "README.txt", perm: 0o644},
|
||||||
{src: "cmd/strelaysrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
{src: "cmd/strelaysrv/LICENSE", dst: "LICENSE.txt", perm: 0o644},
|
||||||
{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
|
{src: "LICENSE", dst: "LICENSE.txt", perm: 0o644},
|
||||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0o644},
|
||||||
},
|
},
|
||||||
systemdService: "strelaysrv.service",
|
systemdService: "strelaysrv.service",
|
||||||
installationFiles: []archiveFile{
|
installationFiles: []archiveFile{
|
||||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0o755},
|
||||||
{src: "cmd/strelaysrv/README.md", dst: "deb/usr/share/doc/syncthing-relaysrv/README.txt", perm: 0644},
|
{src: "cmd/strelaysrv/README.md", dst: "deb/usr/share/doc/syncthing-relaysrv/README.txt", perm: 0o644},
|
||||||
{src: "cmd/strelaysrv/LICENSE", dst: "deb/usr/share/doc/syncthing-relaysrv/LICENSE.txt", perm: 0644},
|
{src: "cmd/strelaysrv/LICENSE", dst: "deb/usr/share/doc/syncthing-relaysrv/LICENSE.txt", perm: 0o644},
|
||||||
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing-relaysrv/LICENSE.txt", perm: 0644},
|
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing-relaysrv/LICENSE.txt", perm: 0o644},
|
||||||
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-relaysrv/AUTHORS.txt", perm: 0644},
|
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-relaysrv/AUTHORS.txt", perm: 0o644},
|
||||||
{src: "man/strelaysrv.1", dst: "deb/usr/share/man/man1/strelaysrv.1", perm: 0644},
|
{src: "man/strelaysrv.1", dst: "deb/usr/share/man/man1/strelaysrv.1", perm: 0o644},
|
||||||
{src: "cmd/strelaysrv/etc/linux-systemd/strelaysrv.service", dst: "deb/lib/systemd/system/strelaysrv.service", perm: 0644},
|
{src: "cmd/strelaysrv/etc/linux-systemd/strelaysrv.service", dst: "deb/lib/systemd/system/strelaysrv.service", perm: 0o644},
|
||||||
{src: "cmd/strelaysrv/etc/linux-systemd/default", dst: "deb/etc/default/syncthing-relaysrv", perm: 0644},
|
{src: "cmd/strelaysrv/etc/linux-systemd/default", dst: "deb/etc/default/syncthing-relaysrv", perm: 0o644},
|
||||||
{src: "cmd/strelaysrv/etc/firewall-ufw/strelaysrv", dst: "deb/etc/ufw/applications.d/strelaysrv", perm: 0644},
|
{src: "cmd/strelaysrv/etc/firewall-ufw/strelaysrv", dst: "deb/etc/ufw/applications.d/strelaysrv", perm: 0o644},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"strelaypoolsrv": {
|
"strelaypoolsrv": {
|
||||||
name: "strelaypoolsrv",
|
name: "strelaypoolsrv",
|
||||||
debname: "syncthing-relaypoolsrv",
|
|
||||||
debdeps: []string{"libc6"},
|
|
||||||
description: "Syncthing Relay Pool Server",
|
description: "Syncthing Relay Pool Server",
|
||||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/strelaypoolsrv"},
|
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/infra/strelaypoolsrv"},
|
||||||
binaryName: "strelaypoolsrv", // .exe will be added automatically for Windows builds
|
binaryName: "strelaypoolsrv",
|
||||||
archiveFiles: []archiveFile{
|
|
||||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
|
||||||
{src: "cmd/strelaypoolsrv/README.md", dst: "README.txt", perm: 0644},
|
|
||||||
{src: "cmd/strelaypoolsrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
|
||||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
|
||||||
},
|
|
||||||
installationFiles: []archiveFile{
|
|
||||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
|
||||||
{src: "cmd/strelaypoolsrv/README.md", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/README.txt", perm: 0644},
|
|
||||||
{src: "cmd/strelaypoolsrv/LICENSE", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/LICENSE.txt", perm: 0644},
|
|
||||||
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/AUTHORS.txt", perm: 0644},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"stupgrades": {
|
"stupgrades": {
|
||||||
name: "stupgrades",
|
name: "stupgrades",
|
||||||
description: "Syncthing Upgrade Check Server",
|
description: "Syncthing Upgrade Check Server",
|
||||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/stupgrades"},
|
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/infra/stupgrades"},
|
||||||
binaryName: "stupgrades",
|
binaryName: "stupgrades",
|
||||||
},
|
},
|
||||||
"stcrashreceiver": {
|
"stcrashreceiver": {
|
||||||
name: "stupgrastcrashreceiverdes",
|
name: "stcrashreceiver",
|
||||||
description: "Syncthing Crash Server",
|
description: "Syncthing Crash Server",
|
||||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/stcrashreceiver"},
|
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/infra/stcrashreceiver"},
|
||||||
binaryName: "stcrashreceiver",
|
binaryName: "stcrashreceiver",
|
||||||
},
|
},
|
||||||
|
"ursrv": {
|
||||||
|
name: "ursrv",
|
||||||
|
description: "Syncthing Usage Reporting Server",
|
||||||
|
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/infra/ursrv"},
|
||||||
|
binaryName: "ursrv",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func initTargets() {
|
func initTargets() {
|
||||||
all := targets["all"]
|
all := targets["all"]
|
||||||
pkgs, _ := filepath.Glob("cmd/*")
|
pkgs, _ := filepath.Glob("cmd/*")
|
||||||
for _, pkg := range pkgs {
|
for _, pkg := range pkgs {
|
||||||
pkg = filepath.Base(pkg)
|
if files, err := filepath.Glob(pkg + "/*.go"); err != nil || len(files) == 0 {
|
||||||
if strings.HasPrefix(pkg, ".") {
|
// No go files in the directory
|
||||||
// ignore dotfiles
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if noupgrade && pkg == "stupgrades" {
|
all.buildPkgs = append(all.buildPkgs, fmt.Sprintf("github.com/syncthing/syncthing/%s", pkg))
|
||||||
continue
|
|
||||||
}
|
|
||||||
all.buildPkgs = append(all.buildPkgs, fmt.Sprintf("github.com/syncthing/syncthing/cmd/%s", pkg))
|
|
||||||
}
|
}
|
||||||
targets["all"] = all
|
targets["all"] = all
|
||||||
|
|
||||||
@@ -241,13 +227,13 @@ func initTargets() {
|
|||||||
// and "extra" dirs.
|
// and "extra" dirs.
|
||||||
syncthingPkg := targets["syncthing"]
|
syncthingPkg := targets["syncthing"]
|
||||||
for _, file := range listFiles("etc") {
|
for _, file := range listFiles("etc") {
|
||||||
syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0644})
|
syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0o644})
|
||||||
}
|
}
|
||||||
for _, file := range listFiles("extra") {
|
for _, file := range listFiles("extra") {
|
||||||
syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0644})
|
syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0o644})
|
||||||
}
|
}
|
||||||
for _, file := range listFiles("extra") {
|
for _, file := range listFiles("extra") {
|
||||||
syncthingPkg.installationFiles = append(syncthingPkg.installationFiles, archiveFile{src: file, dst: "deb/usr/share/doc/syncthing/" + filepath.Base(file), perm: 0644})
|
syncthingPkg.installationFiles = append(syncthingPkg.installationFiles, archiveFile{src: file, dst: "deb/usr/share/doc/syncthing/" + filepath.Base(file), perm: 0o644})
|
||||||
}
|
}
|
||||||
targets["syncthing"] = syncthingPkg
|
targets["syncthing"] = syncthingPkg
|
||||||
}
|
}
|
||||||
@@ -337,12 +323,14 @@ func runCommand(cmd string, target target) {
|
|||||||
|
|
||||||
case "tar":
|
case "tar":
|
||||||
buildTar(target, tags)
|
buildTar(target, tags)
|
||||||
|
writeCompatJSON()
|
||||||
|
|
||||||
case "zip":
|
case "zip":
|
||||||
buildZip(target, tags)
|
buildZip(target, tags)
|
||||||
|
writeCompatJSON()
|
||||||
|
|
||||||
case "deb":
|
case "deb":
|
||||||
buildDeb(target)
|
buildDeb(target, tags)
|
||||||
|
|
||||||
case "vet":
|
case "vet":
|
||||||
metalintShort()
|
metalintShort()
|
||||||
@@ -400,7 +388,6 @@ func parseFlags() {
|
|||||||
func test(tags []string, pkgs ...string) {
|
func test(tags []string, pkgs ...string) {
|
||||||
lazyRebuildAssets()
|
lazyRebuildAssets()
|
||||||
|
|
||||||
tags = append(tags, "purego")
|
|
||||||
args := []string{"test", "-tags", strings.Join(tags, " ")}
|
args := []string{"test", "-tags", strings.Join(tags, " ")}
|
||||||
if long {
|
if long {
|
||||||
timeout = longTimeout
|
timeout = longTimeout
|
||||||
@@ -434,7 +421,7 @@ func bench(tags []string, pkgs ...string) {
|
|||||||
func integration(bench bool) {
|
func integration(bench bool) {
|
||||||
lazyRebuildAssets()
|
lazyRebuildAssets()
|
||||||
args := []string{"test", "-v", "-timeout", "60m", "-tags"}
|
args := []string{"test", "-v", "-timeout", "60m", "-tags"}
|
||||||
tags := "purego,integration"
|
tags := "integration"
|
||||||
if bench {
|
if bench {
|
||||||
tags += ",benchmark"
|
tags += ",benchmark"
|
||||||
}
|
}
|
||||||
@@ -622,7 +609,7 @@ func buildZip(target target, tags []string) {
|
|||||||
fmt.Println(filename)
|
fmt.Println(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDeb(target target) {
|
func buildDeb(target target, tags []string) {
|
||||||
os.RemoveAll("deb")
|
os.RemoveAll("deb")
|
||||||
|
|
||||||
// "goarch" here is set to whatever the Debian packages expect. We correct
|
// "goarch" here is set to whatever the Debian packages expect. We correct
|
||||||
@@ -636,7 +623,7 @@ func buildDeb(target target) {
|
|||||||
goarch = "arm"
|
goarch = "arm"
|
||||||
}
|
}
|
||||||
|
|
||||||
build(target, []string{"noupgrade"})
|
build(target, append(tags, "noupgrade"))
|
||||||
|
|
||||||
for i := range target.installationFiles {
|
for i := range target.installationFiles {
|
||||||
target.installationFiles[i].src = strings.Replace(target.installationFiles[i].src, "{{binary}}", target.BinaryName(), 1)
|
target.installationFiles[i].src = strings.Replace(target.installationFiles[i].src, "{{binary}}", target.BinaryName(), 1)
|
||||||
@@ -658,6 +645,9 @@ func buildDeb(target target) {
|
|||||||
// than just 0.14.26. This rectifies that.
|
// than just 0.14.26. This rectifies that.
|
||||||
debver = strings.Replace(debver, "-", "~", -1)
|
debver = strings.Replace(debver, "-", "~", -1)
|
||||||
}
|
}
|
||||||
|
if strings.Contains(debver, "_") {
|
||||||
|
debver = strings.Replace(debver, "_", "~", -1)
|
||||||
|
}
|
||||||
args := []string{
|
args := []string{
|
||||||
"-t", "deb",
|
"-t", "deb",
|
||||||
"-s", "dir",
|
"-s", "dir",
|
||||||
@@ -745,7 +735,7 @@ func shouldBuildSyso(dir string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jsonPath := filepath.Join(dir, "versioninfo.json")
|
jsonPath := filepath.Join(dir, "versioninfo.json")
|
||||||
err = os.WriteFile(jsonPath, bs, 0644)
|
err = os.WriteFile(jsonPath, bs, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("failed to create " + jsonPath + ": " + err.Error())
|
return "", errors.New("failed to create " + jsonPath + ": " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -804,7 +794,7 @@ func copyFile(src, dst string, perm os.FileMode) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
copy:
|
copy:
|
||||||
os.MkdirAll(filepath.Dir(dst), 0777)
|
os.MkdirAll(filepath.Dir(dst), 0o777)
|
||||||
if err := os.WriteFile(dst, in, perm); err != nil {
|
if err := os.WriteFile(dst, in, perm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -829,12 +819,12 @@ func listFiles(dir string) []string {
|
|||||||
|
|
||||||
func rebuildAssets() {
|
func rebuildAssets() {
|
||||||
os.Setenv("SOURCE_DATE_EPOCH", fmt.Sprint(buildStamp()))
|
os.Setenv("SOURCE_DATE_EPOCH", fmt.Sprint(buildStamp()))
|
||||||
runPrint(goCmd, "generate", "github.com/syncthing/syncthing/lib/api/auto", "github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto")
|
runPrint(goCmd, "generate", "github.com/syncthing/syncthing/lib/api/auto", "github.com/syncthing/syncthing/cmd/infra/strelaypoolsrv/auto")
|
||||||
}
|
}
|
||||||
|
|
||||||
func lazyRebuildAssets() {
|
func lazyRebuildAssets() {
|
||||||
shouldRebuild := shouldRebuildAssets("lib/api/auto/gui.files.go", "gui") ||
|
shouldRebuild := shouldRebuildAssets("lib/api/auto/gui.files.go", "gui") ||
|
||||||
shouldRebuildAssets("cmd/strelaypoolsrv/auto/gui.files.go", "cmd/strelaypoolsrv/gui")
|
shouldRebuildAssets("cmd/infra/strelaypoolsrv/auto/gui.files.go", "cmd/infra/strelaypoolsrv/gui")
|
||||||
|
|
||||||
if withNextGenGUI {
|
if withNextGenGUI {
|
||||||
shouldRebuild = buildNextGenGUI() || shouldRebuild
|
shouldRebuild = buildNextGenGUI() || shouldRebuild
|
||||||
@@ -864,7 +854,7 @@ func buildNextGenGUI() bool {
|
|||||||
for _, src := range listFiles("next-gen-gui/dist") {
|
for _, src := range listFiles("next-gen-gui/dist") {
|
||||||
rel, _ := filepath.Rel("next-gen-gui/dist", src)
|
rel, _ := filepath.Rel("next-gen-gui/dist", src)
|
||||||
dst := filepath.Join("gui", rel)
|
dst := filepath.Join("gui", rel)
|
||||||
if err := copyFile(src, dst, 0644); err != nil {
|
if err := copyFile(src, dst, 0o644); err != nil {
|
||||||
fmt.Println("copy:", err)
|
fmt.Println("copy:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -920,22 +910,9 @@ func updateDependencies() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func proto() {
|
func proto() {
|
||||||
pv := protobufVersion()
|
// buf needs to be installed
|
||||||
repo := "https://github.com/gogo/protobuf.git"
|
// https://buf.build/docs/installation/
|
||||||
path := filepath.Join("repos", "protobuf")
|
runPrint("buf", "generate")
|
||||||
|
|
||||||
runPrint(goCmd, "install", fmt.Sprintf("github.com/gogo/protobuf/protoc-gen-gogofast@%v", pv))
|
|
||||||
os.MkdirAll("repos", 0755)
|
|
||||||
|
|
||||||
if _, err := os.Stat(path); err != nil {
|
|
||||||
runPrint("git", "clone", repo, path)
|
|
||||||
} else {
|
|
||||||
runPrintInDir(path, "git", "fetch")
|
|
||||||
}
|
|
||||||
runPrintInDir(path, "git", "checkout", pv)
|
|
||||||
|
|
||||||
runPrint(goCmd, "generate", "github.com/syncthing/syncthing/cmd/stdiscosrv")
|
|
||||||
runPrint(goCmd, "generate", "proto/generate.go")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testmocks() {
|
func testmocks() {
|
||||||
@@ -1108,10 +1085,14 @@ func getBranchSuffix() string {
|
|||||||
|
|
||||||
branch = parts[len(parts)-1]
|
branch = parts[len(parts)-1]
|
||||||
switch branch {
|
switch branch {
|
||||||
case "master", "release", "main":
|
case "release", "main":
|
||||||
// these are not special
|
// these are not special
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(branch, "release-") {
|
||||||
|
// release branches are not special
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
validBranchRe := regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`)
|
validBranchRe := regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`)
|
||||||
if !validBranchRe.MatchString(branch) {
|
if !validBranchRe.MatchString(branch) {
|
||||||
@@ -1366,10 +1347,7 @@ func zipFile(out string, files []archiveFile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func codesign(target target) {
|
func codesign(target target) {
|
||||||
switch goos {
|
if goos == "darwin" {
|
||||||
case "windows":
|
|
||||||
windowsCodesign(target.BinaryName())
|
|
||||||
case "darwin":
|
|
||||||
macosCodesign(target.BinaryName())
|
macosCodesign(target.BinaryName())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1393,70 +1371,6 @@ func macosCodesign(file string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func windowsCodesign(file string) {
|
|
||||||
st := "signtool.exe"
|
|
||||||
|
|
||||||
if path := os.Getenv("CODESIGN_SIGNTOOL"); path != "" {
|
|
||||||
st = path
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, algo := range []string{"sha1", "sha256"} {
|
|
||||||
args := []string{"sign", "/fd", algo}
|
|
||||||
if f := os.Getenv("CODESIGN_CERTIFICATE_FILE"); f != "" {
|
|
||||||
args = append(args, "/f", f)
|
|
||||||
} else if b := os.Getenv("CODESIGN_CERTIFICATE_BASE64"); b != "" {
|
|
||||||
// Decode the PFX certificate from base64.
|
|
||||||
bs, err := base64.RawStdEncoding.DecodeString(b)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Codesign: signing failed: decoding base64:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write it to a temporary file
|
|
||||||
f, err := os.CreateTemp("", "codesign-*.pfx")
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Codesign: signing failed: creating temp file:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = f.Chmod(0600) // best effort remove other users' access
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
if _, err := f.Write(bs); err != nil {
|
|
||||||
log.Println("Codesign: signing failed: writing temp file:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
log.Println("Codesign: signing failed: closing temp file:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use that when signing
|
|
||||||
args = append(args, "/f", f.Name())
|
|
||||||
}
|
|
||||||
if p := os.Getenv("CODESIGN_CERTIFICATE_PASSWORD"); p != "" {
|
|
||||||
args = append(args, "/p", p)
|
|
||||||
}
|
|
||||||
if tr := os.Getenv("CODESIGN_TIMESTAMP_SERVER"); tr != "" {
|
|
||||||
switch algo {
|
|
||||||
case "sha256":
|
|
||||||
args = append(args, "/tr", tr, "/td", algo)
|
|
||||||
default:
|
|
||||||
args = append(args, "/t", tr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if i > 0 {
|
|
||||||
args = append(args, "/as")
|
|
||||||
}
|
|
||||||
args = append(args, file)
|
|
||||||
|
|
||||||
bs, err := runError(st, args...)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Codesign: signing failed: %v: %s", err, string(bs))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println("Codesign: successfully signed", file, "using", algo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func metalint() {
|
func metalint() {
|
||||||
lazyRebuildAssets()
|
lazyRebuildAssets()
|
||||||
runPrint(goCmd, "test", "-run", "Metalint", "./meta")
|
runPrint(goCmd, "test", "-run", "Metalint", "./meta")
|
||||||
@@ -1474,14 +1388,6 @@ func (t target) BinaryName() string {
|
|||||||
return t.binaryName
|
return t.binaryName
|
||||||
}
|
}
|
||||||
|
|
||||||
func protobufVersion() string {
|
|
||||||
bs, err := runError(goCmd, "list", "-f", "{{.Version}}", "-m", "github.com/gogo/protobuf")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Getting protobuf version:", err)
|
|
||||||
}
|
|
||||||
return string(bs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func currentAndLatestVersions(n int) ([]string, error) {
|
func currentAndLatestVersions(n int) ([]string, error) {
|
||||||
bs, err := runError("git", "tag", "--sort", "taggerdate")
|
bs, err := runError("git", "tag", "--sort", "taggerdate")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1548,3 +1454,29 @@ func nextPatchVersion(ver string) string {
|
|||||||
digits[len(digits)-1] = strconv.Itoa(n + 1)
|
digits[len(digits)-1] = strconv.Itoa(n + 1)
|
||||||
return strings.Join(digits, ".")
|
return strings.Join(digits, ".")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeCompatJSON() {
|
||||||
|
bs, err := os.ReadFile("compat.yaml")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Reading compat.yaml:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []upgrade.ReleaseCompatibility
|
||||||
|
if err := yaml.Unmarshal(bs, &entries); err != nil {
|
||||||
|
log.Fatal("Parsing compat.yaml:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := runtime.Version()
|
||||||
|
for _, e := range entries {
|
||||||
|
if !strings.HasPrefix(rt, e.Runtime) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bs, _ := json.MarshalIndent(e, "", " ")
|
||||||
|
if err := os.WriteFile("compat.json", bs, 0o644); err != nil {
|
||||||
|
log.Fatal("Writing compat.json:", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Fatalf("runtime %v not found in compat.yaml", rt)
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,10 +23,11 @@ case "${1:-default}" in
|
|||||||
|
|
||||||
prerelease)
|
prerelease)
|
||||||
script authors
|
script authors
|
||||||
|
script copyrights
|
||||||
build weblate
|
build weblate
|
||||||
pushd man ; ./refresh.sh ; popd
|
pushd man ; ./refresh.sh ; popd
|
||||||
git add -A gui man AUTHORS
|
git add -A gui man AUTHORS
|
||||||
git commit -m 'gui, man, authors: Update docs, translations, and contributors'
|
git commit -m 'chore(gui, man, authors): update docs, translations, and contributors'
|
||||||
;;
|
;;
|
||||||
|
|
||||||
*)
|
*)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -15,7 +16,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/sha256"
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -15,6 +15,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/internal/gen/discoproto"
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
"github.com/syncthing/syncthing/lib/beacon"
|
"github.com/syncthing/syncthing/lib/beacon"
|
||||||
"github.com/syncthing/syncthing/lib/discover"
|
"github.com/syncthing/syncthing/lib/discover"
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
@@ -74,20 +78,21 @@ func recv(bc beacon.Interface) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var ann discover.Announce
|
var ann discoproto.Announce
|
||||||
ann.Unmarshal(data[4:])
|
proto.Unmarshal(data[4:], &ann)
|
||||||
|
|
||||||
if ann.ID == myID {
|
id, _ := protocol.DeviceIDFromBytes(ann.Id)
|
||||||
|
if id == myID {
|
||||||
// This is one of our own fake packets, don't print it.
|
// This is one of our own fake packets, don't print it.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print announcement details for the first packet from a given
|
// Print announcement details for the first packet from a given
|
||||||
// device ID and source address, or if -all was given.
|
// device ID and source address, or if -all was given.
|
||||||
key := ann.ID.String() + src.String()
|
key := id.String() + src.String()
|
||||||
if all || !seen[key] {
|
if all || !seen[key] {
|
||||||
log.Printf("Announcement from %v\n", src)
|
log.Printf("Announcement from %v\n", src)
|
||||||
log.Printf(" %v at %s\n", ann.ID, strings.Join(ann.Addresses, ", "))
|
log.Printf(" %v at %s\n", id, strings.Join(ann.Addresses, ", "))
|
||||||
seen[key] = true
|
seen[key] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,11 +100,11 @@ func recv(bc beacon.Interface) {
|
|||||||
|
|
||||||
// sends fake discovery announcements once every second
|
// sends fake discovery announcements once every second
|
||||||
func send(bc beacon.Interface) {
|
func send(bc beacon.Interface) {
|
||||||
ann := discover.Announce{
|
ann := &discoproto.Announce{
|
||||||
ID: myID,
|
Id: myID[:],
|
||||||
Addresses: []string{"tcp://fake.example.com:12345"},
|
Addresses: []string{"tcp://fake.example.com:12345"},
|
||||||
}
|
}
|
||||||
bs, _ := ann.Marshal()
|
bs, _ := proto.Marshal(ann)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
bc.Send(bs)
|
bc.Send(bs)
|
||||||
@@ -14,6 +14,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
)
|
)
|
||||||
|
|
||||||
type event struct {
|
type event struct {
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
"github.com/syncthing/syncthing/lib/scanner"
|
"github.com/syncthing/syncthing/lib/scanner"
|
||||||
)
|
)
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/discover"
|
"github.com/syncthing/syncthing/lib/discover"
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
"github.com/syncthing/syncthing/lib/fs"
|
"github.com/syncthing/syncthing/lib/fs"
|
||||||
"github.com/syncthing/syncthing/lib/ignore"
|
"github.com/syncthing/syncthing/lib/ignore"
|
||||||
)
|
)
|
||||||
@@ -15,6 +15,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -43,7 +45,7 @@ func generateFiles(dir string, files, maxexp int, srcname string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p0 := filepath.Join(dir, string(n[0]), n[0:2])
|
p0 := filepath.Join(dir, string(n[0]), n[0:2])
|
||||||
err = os.MkdirAll(p0, 0755)
|
err = os.MkdirAll(p0, 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -66,7 +68,7 @@ func generateFiles(dir string, files, maxexp int, srcname string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateOneFile(fd io.ReadSeeker, p1 string, s int64) error {
|
func generateOneFile(fd io.ReadSeeker, p1 string, s int64) error {
|
||||||
src := io.LimitReader(&inifiteReader{fd}, s)
|
src := io.LimitReader(&infiniteReader{fd}, s)
|
||||||
dst, err := os.Create(p1)
|
dst, err := os.Create(p1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -82,7 +84,7 @@ func generateOneFile(fd io.ReadSeeker, p1 string, s int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Chmod(p1, os.FileMode(rand.Intn(0777)|0400))
|
os.Chmod(p1, os.FileMode(rand.Intn(0o777)|0o400))
|
||||||
|
|
||||||
t := time.Now().Add(-time.Duration(rand.Intn(30*86400)) * time.Second)
|
t := time.Now().Add(-time.Duration(rand.Intn(30*86400)) * time.Second)
|
||||||
return os.Chtimes(p1, t, t)
|
return os.Chtimes(p1, t, t)
|
||||||
@@ -105,11 +107,11 @@ func readRand(bs []byte) (int, error) {
|
|||||||
return len(bs), nil
|
return len(bs), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type inifiteReader struct {
|
type infiniteReader struct {
|
||||||
rd io.ReadSeeker
|
rd io.ReadSeeker
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *inifiteReader) Read(bs []byte) (int, error) {
|
func (i *infiniteReader) Read(bs []byte) (int, error) {
|
||||||
n, err := i.rd.Read(bs)
|
n, err := i.rd.Read(bs)
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
err = nil
|
err = nil
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
"github.com/syncthing/syncthing/lib/signature"
|
"github.com/syncthing/syncthing/lib/signature"
|
||||||
"github.com/syncthing/syncthing/lib/upgrade"
|
"github.com/syncthing/syncthing/lib/upgrade"
|
||||||
)
|
)
|
||||||
@@ -26,6 +26,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -157,7 +158,7 @@ func saveCert(priv interface{}, derBytes []byte) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -7,13 +7,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/sha256"
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -8,13 +8,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"cmp"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,7 +42,7 @@ type currentFile struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *diskStore) Serve(ctx context.Context) {
|
func (d *diskStore) Serve(ctx context.Context) {
|
||||||
if err := os.MkdirAll(d.dir, 0750); err != nil {
|
if err := os.MkdirAll(d.dir, 0o700); err != nil {
|
||||||
log.Println("Creating directory:", err)
|
log.Println("Creating directory:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -60,7 +62,7 @@ func (d *diskStore) Serve(ctx context.Context) {
|
|||||||
case entry := <-d.inbox:
|
case entry := <-d.inbox:
|
||||||
path := d.fullPath(entry.path)
|
path := d.fullPath(entry.path)
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||||
log.Println("Creating directory:", err)
|
log.Println("Creating directory:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -75,7 +77,7 @@ func (d *diskStore) Serve(ctx context.Context) {
|
|||||||
log.Println("Failed to compress crash report:", err)
|
log.Println("Failed to compress crash report:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, buf.Bytes(), 0644); err != nil {
|
if err := os.WriteFile(path, buf.Bytes(), 0o600); err != nil {
|
||||||
log.Printf("Failed to write %s: %v", entry.path, err)
|
log.Printf("Failed to write %s: %v", entry.path, err)
|
||||||
_ = os.Remove(path)
|
_ = os.Remove(path)
|
||||||
continue
|
continue
|
||||||
@@ -147,6 +149,11 @@ func (d *diskStore) clean() {
|
|||||||
if len(d.currentFiles) > 0 {
|
if len(d.currentFiles) > 0 {
|
||||||
oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute)
|
oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metricDiskstoreFilesTotal.Set(float64(len(d.currentFiles)))
|
||||||
|
metricDiskstoreBytesTotal.Set(float64(d.currentSize))
|
||||||
|
metricDiskstoreOldestAgeSeconds.Set(math.Round(oldest.Seconds()))
|
||||||
|
|
||||||
log.Printf("Clean complete: %d files, %d MB, oldest is %v ago", len(d.currentFiles), d.currentSize>>20, oldest)
|
log.Printf("Clean complete: %d files, %d MB, oldest is %v ago", len(d.currentFiles), d.currentSize>>20, oldest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,13 +178,18 @@ func (d *diskStore) inventory() error {
|
|||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
sort.Slice(d.currentFiles, func(i, j int) bool {
|
slices.SortFunc(d.currentFiles, func(a, b currentFile) int {
|
||||||
return d.currentFiles[i].mtime < d.currentFiles[j].mtime
|
return cmp.Compare(a.mtime, b.mtime)
|
||||||
})
|
})
|
||||||
var oldest time.Duration
|
var oldest time.Duration
|
||||||
if len(d.currentFiles) > 0 {
|
if len(d.currentFiles) > 0 {
|
||||||
oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute)
|
oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metricDiskstoreFilesTotal.Set(float64(len(d.currentFiles)))
|
||||||
|
metricDiskstoreBytesTotal.Set(float64(d.currentSize))
|
||||||
|
metricDiskstoreOldestAgeSeconds.Set(math.Round(oldest.Seconds()))
|
||||||
|
|
||||||
log.Printf("Inventory complete: %d files, %d MB, oldest is %v ago", len(d.currentFiles), d.currentSize>>20, oldest)
|
log.Printf("Inventory complete: %d files, %d MB, oldest is %v ago", len(d.currentFiles), d.currentSize>>20, oldest)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -21,26 +22,29 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
"github.com/syncthing/syncthing/lib/sha256"
|
|
||||||
"github.com/syncthing/syncthing/lib/ur"
|
|
||||||
|
|
||||||
raven "github.com/getsentry/raven-go"
|
raven "github.com/getsentry/raven-go"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
|
"github.com/syncthing/syncthing/lib/ur"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxRequestSize = 1 << 20 // 1 MiB
|
const maxRequestSize = 1 << 20 // 1 MiB
|
||||||
|
|
||||||
type cli struct {
|
type cli struct {
|
||||||
Dir string `help:"Parent directory to store crash and failure reports in" env:"REPORTS_DIR" default:"."`
|
Dir string `help:"Parent directory to store crash and failure reports in" env:"REPORTS_DIR" default:"."`
|
||||||
DSN string `help:"Sentry DSN" env:"SENTRY_DSN"`
|
DSN string `help:"Sentry DSN" env:"SENTRY_DSN"`
|
||||||
Listen string `help:"HTTP listen address" default:":8080" env:"LISTEN_ADDRESS"`
|
Listen string `help:"HTTP listen address" default:":8080" env:"LISTEN_ADDRESS"`
|
||||||
MaxDiskFiles int `help:"Maximum number of reports on disk" default:"100000" env:"MAX_DISK_FILES"`
|
MaxDiskFiles int `help:"Maximum number of reports on disk" default:"100000" env:"MAX_DISK_FILES"`
|
||||||
MaxDiskSizeMB int64 `help:"Maximum disk space to use for reports" default:"1024" env:"MAX_DISK_SIZE_MB"`
|
MaxDiskSizeMB int64 `help:"Maximum disk space to use for reports" default:"1024" env:"MAX_DISK_SIZE_MB"`
|
||||||
CleanInterval time.Duration `help:"Interval between cleaning up old reports" default:"12h" env:"CLEAN_INTERVAL"`
|
SentryQueue int `help:"Maximum number of reports to queue for sending to Sentry" default:"64" env:"SENTRY_QUEUE"`
|
||||||
SentryQueue int `help:"Maximum number of reports to queue for sending to Sentry" default:"64" env:"SENTRY_QUEUE"`
|
DiskQueue int `help:"Maximum number of reports to queue for writing to disk" default:"64" env:"DISK_QUEUE"`
|
||||||
DiskQueue int `help:"Maximum number of reports to queue for writing to disk" default:"64" env:"DISK_QUEUE"`
|
MetricsListen string `help:"HTTP listen address for metrics" default:":8081" env:"METRICS_LISTEN_ADDRESS"`
|
||||||
|
IngorePatterns string `help:"File containing ignore patterns (regexp)" env:"IGNORE_PATTERNS" type:"existingfile"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -63,15 +67,38 @@ func main() {
|
|||||||
}
|
}
|
||||||
go ss.Serve(context.Background())
|
go ss.Serve(context.Background())
|
||||||
|
|
||||||
|
var ip *ignorePatterns
|
||||||
|
if params.IngorePatterns != "" {
|
||||||
|
var err error
|
||||||
|
ip, err = loadIgnorePatterns(params.IngorePatterns)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load ignore patterns: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cr := &crashReceiver{
|
cr := &crashReceiver{
|
||||||
store: ds,
|
store: ds,
|
||||||
sentry: ss,
|
sentry: ss,
|
||||||
|
ignore: ip,
|
||||||
}
|
}
|
||||||
|
|
||||||
mux.Handle("/", cr)
|
mux.Handle("/", cr)
|
||||||
|
mux.HandleFunc("/ping", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
w.Write([]byte("OK"))
|
||||||
|
})
|
||||||
|
|
||||||
|
if params.MetricsListen != "" {
|
||||||
|
mmux := http.NewServeMux()
|
||||||
|
mmux.Handle("/metrics", promhttp.Handler())
|
||||||
|
go func() {
|
||||||
|
if err := http.ListenAndServe(params.MetricsListen, mmux); err != nil {
|
||||||
|
log.Fatalln("HTTP serve metrics:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
if params.DSN != "" {
|
if params.DSN != "" {
|
||||||
mux.HandleFunc("/newcrash/failure", handleFailureFn(params.DSN, filepath.Join(params.Dir, "failure_reports")))
|
mux.HandleFunc("/newcrash/failure", handleFailureFn(params.DSN, filepath.Join(params.Dir, "failure_reports"), ip))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.SetOutput(os.Stdout)
|
log.SetOutput(os.Stdout)
|
||||||
@@ -80,8 +107,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFailureFn(dsn, failureDir string) func(w http.ResponseWriter, req *http.Request) {
|
func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http.ResponseWriter, req *http.Request) {
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
result := "failure"
|
||||||
|
defer func() {
|
||||||
|
metricFailureReportsTotal.WithLabelValues(result).Inc()
|
||||||
|
}()
|
||||||
|
|
||||||
lr := io.LimitReader(req.Body, maxRequestSize)
|
lr := io.LimitReader(req.Body, maxRequestSize)
|
||||||
bs, err := io.ReadAll(lr)
|
bs, err := io.ReadAll(lr)
|
||||||
req.Body.Close()
|
req.Body.Close()
|
||||||
@@ -90,6 +122,11 @@ func handleFailureFn(dsn, failureDir string) func(w http.ResponseWriter, req *ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ignore.match(bs) {
|
||||||
|
result = "ignored"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var reports []ur.FailureReport
|
var reports []ur.FailureReport
|
||||||
err = json.Unmarshal(bs, &reports)
|
err = json.Unmarshal(bs, &reports)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -102,7 +139,7 @@ func handleFailureFn(dsn, failureDir string) func(w http.ResponseWriter, req *ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
version, err := parseVersion(reports[0].Version)
|
version, err := build.ParseVersion(reports[0].Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), 400)
|
http.Error(w, err.Error(), 400)
|
||||||
return
|
return
|
||||||
@@ -132,6 +169,7 @@ func handleFailureFn(dsn, failureDir string) func(w http.ResponseWriter, req *ht
|
|||||||
log.Println("Failed to send failure report:", err)
|
log.Println("Failed to send failure report:", err)
|
||||||
} else {
|
} else {
|
||||||
log.Println("Sent failure report:", r.Description)
|
log.Println("Sent failure report:", r.Description)
|
||||||
|
result = "success"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,3 +187,42 @@ func saveFailureWithGoroutines(data ur.FailureData, failureDir string) (string,
|
|||||||
}
|
}
|
||||||
return reportServer + path, nil
|
return reportServer + path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ignorePatterns struct {
|
||||||
|
patterns []*regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadIgnorePatterns(path string) (*ignorePatterns, error) {
|
||||||
|
bs, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var patterns []*regexp.Regexp
|
||||||
|
for _, line := range strings.Split(string(bs), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
re, err := regexp.Compile(line)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
patterns = append(patterns, re)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d ignore patterns", len(patterns))
|
||||||
|
return &ignorePatterns{patterns: patterns}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ignorePatterns) match(report []byte) bool {
|
||||||
|
if i == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, re := range i.patterns {
|
||||||
|
if re.Match(report) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (C) 2023 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
metricCrashReportsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "crashreceiver",
|
||||||
|
Name: "crash_reports_total",
|
||||||
|
}, []string{"result"})
|
||||||
|
metricFailureReportsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "crashreceiver",
|
||||||
|
Name: "failure_reports_total",
|
||||||
|
}, []string{"result"})
|
||||||
|
metricDiskstoreFilesTotal = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "crashreceiver",
|
||||||
|
Name: "diskstore_files_total",
|
||||||
|
})
|
||||||
|
metricDiskstoreBytesTotal = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "crashreceiver",
|
||||||
|
Name: "diskstore_bytes_total",
|
||||||
|
})
|
||||||
|
metricDiskstoreOldestAgeSeconds = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "crashreceiver",
|
||||||
|
Name: "diskstore_oldest_age_seconds",
|
||||||
|
})
|
||||||
|
)
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
|
|
||||||
raven "github.com/getsentry/raven-go"
|
raven "github.com/getsentry/raven-go"
|
||||||
"github.com/maruel/panicparse/v2/stack"
|
"github.com/maruel/panicparse/v2/stack"
|
||||||
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
)
|
)
|
||||||
|
|
||||||
const reportServer = "https://crash.syncthing.net/report/"
|
const reportServer = "https://crash.syncthing.net/report/"
|
||||||
@@ -40,6 +41,7 @@ type sentryService struct {
|
|||||||
|
|
||||||
type sentryRequest struct {
|
type sentryRequest struct {
|
||||||
reportID string
|
reportID string
|
||||||
|
userID string
|
||||||
data []byte
|
data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ func (s *sentryService) Serve(ctx context.Context) {
|
|||||||
log.Println("Failed to parse crash report:", err)
|
log.Println("Failed to parse crash report:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := sendReport(s.dsn, pkt, req.reportID); err != nil {
|
if err := sendReport(s.dsn, pkt, req.userID); err != nil {
|
||||||
log.Println("Failed to send crash report:", err)
|
log.Println("Failed to send crash report:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,9 +64,9 @@ func (s *sentryService) Serve(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sentryService) Send(reportID string, data []byte) bool {
|
func (s *sentryService) Send(reportID, userID string, data []byte) bool {
|
||||||
select {
|
select {
|
||||||
case s.inbox <- sentryRequest{reportID, data}:
|
case s.inbox <- sentryRequest{reportID, userID, data}:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -104,7 +106,7 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
|
|||||||
return nil, errors.New("no first line")
|
return nil, errors.New("no first line")
|
||||||
}
|
}
|
||||||
|
|
||||||
version, err := parseVersion(string(parts[0]))
|
version, err := build.ParseVersion(string(parts[0]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -142,12 +144,12 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Lock the source code loader to the version we are processing here.
|
// Lock the source code loader to the version we are processing here.
|
||||||
if version.commit != "" {
|
if version.Commit != "" {
|
||||||
// We have a commit hash, so we know exactly which source to use
|
// We have a commit hash, so we know exactly which source to use
|
||||||
loader.LockWithVersion(version.commit)
|
loader.LockWithVersion(version.Commit)
|
||||||
} else if strings.HasPrefix(version.tag, "v") {
|
} else if strings.HasPrefix(version.Tag, "v") {
|
||||||
// Lets hope the tag is close enough
|
// Lets hope the tag is close enough
|
||||||
loader.LockWithVersion(version.tag)
|
loader.LockWithVersion(version.Tag)
|
||||||
} else {
|
} else {
|
||||||
// Last resort
|
// Last resort
|
||||||
loader.LockWithVersion("main")
|
loader.LockWithVersion("main")
|
||||||
@@ -214,89 +216,26 @@ func crashReportFingerprint(message string) []string {
|
|||||||
return []string{"{{ default }}", message}
|
return []string{"{{ default }}", message}
|
||||||
}
|
}
|
||||||
|
|
||||||
// syncthing v1.1.4-rc.1+30-g6aaae618-dirty-crashrep "Erbium Earthworm" (go1.12.5 darwin-amd64) jb@kvin.kastelo.net 2019-05-23 16:08:14 UTC [foo, bar]
|
func packet(version build.VersionParts, reportType string) *raven.Packet {
|
||||||
var longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?$`)
|
|
||||||
|
|
||||||
type version struct {
|
|
||||||
version string // "v1.1.4-rc.1+30-g6aaae618-dirty-crashrep"
|
|
||||||
tag string // "v1.1.4-rc.1"
|
|
||||||
commit string // "6aaae618", blank when absent
|
|
||||||
codename string // "Erbium Earthworm"
|
|
||||||
runtime string // "go1.12.5"
|
|
||||||
goos string // "darwin"
|
|
||||||
goarch string // "amd64"
|
|
||||||
builder string // "jb@kvin.kastelo.net"
|
|
||||||
extra []string // "foo", "bar"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v version) environment() string {
|
|
||||||
if v.commit != "" {
|
|
||||||
return "Development"
|
|
||||||
}
|
|
||||||
if strings.Contains(v.tag, "-rc.") {
|
|
||||||
return "Candidate"
|
|
||||||
}
|
|
||||||
if strings.Contains(v.tag, "-") {
|
|
||||||
return "Beta"
|
|
||||||
}
|
|
||||||
return "Stable"
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseVersion(line string) (version, error) {
|
|
||||||
m := longVersionRE.FindStringSubmatch(line)
|
|
||||||
if len(m) == 0 {
|
|
||||||
return version{}, errors.New("unintelligeble version string")
|
|
||||||
}
|
|
||||||
|
|
||||||
v := version{
|
|
||||||
version: m[1],
|
|
||||||
codename: m[2],
|
|
||||||
runtime: m[3],
|
|
||||||
goos: m[4],
|
|
||||||
goarch: m[5],
|
|
||||||
builder: m[6],
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(v.version, "+")
|
|
||||||
v.tag = parts[0]
|
|
||||||
if len(parts) > 1 {
|
|
||||||
fields := strings.Split(parts[1], "-")
|
|
||||||
if len(fields) >= 2 && strings.HasPrefix(fields[1], "g") {
|
|
||||||
v.commit = fields[1][1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m) >= 8 && m[7] != "" {
|
|
||||||
tags := strings.Split(m[7], ",")
|
|
||||||
for i := range tags {
|
|
||||||
tags[i] = strings.TrimSpace(tags[i])
|
|
||||||
}
|
|
||||||
v.extra = tags
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func packet(version version, reportType string) *raven.Packet {
|
|
||||||
pkt := &raven.Packet{
|
pkt := &raven.Packet{
|
||||||
Platform: "go",
|
Platform: "go",
|
||||||
Release: version.tag,
|
Release: version.Tag,
|
||||||
Environment: version.environment(),
|
Environment: version.Environment(),
|
||||||
Tags: raven.Tags{
|
Tags: raven.Tags{
|
||||||
raven.Tag{Key: "version", Value: version.version},
|
raven.Tag{Key: "version", Value: version.Version},
|
||||||
raven.Tag{Key: "tag", Value: version.tag},
|
raven.Tag{Key: "tag", Value: version.Tag},
|
||||||
raven.Tag{Key: "codename", Value: version.codename},
|
raven.Tag{Key: "codename", Value: version.Codename},
|
||||||
raven.Tag{Key: "runtime", Value: version.runtime},
|
raven.Tag{Key: "runtime", Value: version.Runtime},
|
||||||
raven.Tag{Key: "goos", Value: version.goos},
|
raven.Tag{Key: "goos", Value: version.GOOS},
|
||||||
raven.Tag{Key: "goarch", Value: version.goarch},
|
raven.Tag{Key: "goarch", Value: version.GOARCH},
|
||||||
raven.Tag{Key: "builder", Value: version.builder},
|
raven.Tag{Key: "builder", Value: version.Builder},
|
||||||
raven.Tag{Key: "report_type", Value: reportType},
|
raven.Tag{Key: "report_type", Value: reportType},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if version.commit != "" {
|
if version.Commit != "" {
|
||||||
pkt.Tags = append(pkt.Tags, raven.Tag{Key: "commit", Value: version.commit})
|
pkt.Tags = append(pkt.Tags, raven.Tag{Key: "commit", Value: version.Commit})
|
||||||
}
|
}
|
||||||
for _, tag := range version.extra {
|
for _, tag := range version.Extra {
|
||||||
pkt.Tags = append(pkt.Tags, raven.Tag{Key: tag, Value: "1"})
|
pkt.Tags = append(pkt.Tags, raven.Tag{Key: tag, Value: "1"})
|
||||||
}
|
}
|
||||||
return pkt
|
return pkt
|
||||||
@@ -12,52 +12,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseVersion(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
longVersion string
|
|
||||||
parsed version
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
longVersion: `syncthing v1.1.4-rc.1+30-g6aaae618-dirty-crashrep "Erbium Earthworm" (go1.12.5 darwin-amd64) jb@kvin.kastelo.net 2019-05-23 16:08:14 UTC`,
|
|
||||||
parsed: version{
|
|
||||||
version: "v1.1.4-rc.1+30-g6aaae618-dirty-crashrep",
|
|
||||||
tag: "v1.1.4-rc.1",
|
|
||||||
commit: "6aaae618",
|
|
||||||
codename: "Erbium Earthworm",
|
|
||||||
runtime: "go1.12.5",
|
|
||||||
goos: "darwin",
|
|
||||||
goarch: "amd64",
|
|
||||||
builder: "jb@kvin.kastelo.net",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
longVersion: `syncthing v1.1.4-rc.1+30-g6aaae618-dirty-crashrep "Erbium Earthworm" (go1.12.5 darwin-amd64) jb@kvin.kastelo.net 2019-05-23 16:08:14 UTC [foo, bar]`,
|
|
||||||
parsed: version{
|
|
||||||
version: "v1.1.4-rc.1+30-g6aaae618-dirty-crashrep",
|
|
||||||
tag: "v1.1.4-rc.1",
|
|
||||||
commit: "6aaae618",
|
|
||||||
codename: "Erbium Earthworm",
|
|
||||||
runtime: "go1.12.5",
|
|
||||||
goos: "darwin",
|
|
||||||
goarch: "amd64",
|
|
||||||
builder: "jb@kvin.kastelo.net",
|
|
||||||
extra: []string{"foo", "bar"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
v, err := parseVersion(tc.longVersion)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%s\nerror: %v\n", tc.longVersion, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fmt.Sprint(v) != fmt.Sprint(tc.parsed) {
|
|
||||||
t.Errorf("%s\nA: %v\nE: %v\n", tc.longVersion, v, tc.parsed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseReport(t *testing.T) {
|
func TestParseReport(t *testing.T) {
|
||||||
bs, err := os.ReadFile("_testdata/panic.log")
|
bs, err := os.ReadFile("_testdata/panic.log")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
-1
@@ -71,7 +71,6 @@ func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]b
|
|||||||
|
|
||||||
url := urlPrefix + l.version + filename[idx:]
|
url := urlPrefix + l.version + filename[idx:]
|
||||||
resp, err := l.client.Get(url)
|
resp, err := l.client.Get(url)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Loading source:", err)
|
fmt.Println("Loading source:", err)
|
||||||
return nil, 0
|
return nil, 0
|
||||||
+41
-1
@@ -12,11 +12,16 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type crashReceiver struct {
|
type crashReceiver struct {
|
||||||
store *diskStore
|
store *diskStore
|
||||||
sentry *sentryService
|
sentry *sentryService
|
||||||
|
ignore *ignorePatterns
|
||||||
|
|
||||||
|
ignoredMut sync.RWMutex
|
||||||
|
ignored map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
@@ -64,6 +69,12 @@ func (r *crashReceiver) serveGet(reportID string, w http.ResponseWriter, _ *http
|
|||||||
// serveHead responds to HEAD requests by checking if the named report
|
// serveHead responds to HEAD requests by checking if the named report
|
||||||
// already exists in the system.
|
// already exists in the system.
|
||||||
func (r *crashReceiver) serveHead(reportID string, w http.ResponseWriter, _ *http.Request) {
|
func (r *crashReceiver) serveHead(reportID string, w http.ResponseWriter, _ *http.Request) {
|
||||||
|
r.ignoredMut.RLock()
|
||||||
|
_, ignored := r.ignored[reportID]
|
||||||
|
r.ignoredMut.RUnlock()
|
||||||
|
if ignored {
|
||||||
|
return // found
|
||||||
|
}
|
||||||
if !r.store.Exists(reportID) {
|
if !r.store.Exists(reportID) {
|
||||||
http.Error(w, "Not found", http.StatusNotFound)
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
}
|
}
|
||||||
@@ -71,6 +82,20 @@ func (r *crashReceiver) serveHead(reportID string, w http.ResponseWriter, _ *htt
|
|||||||
|
|
||||||
// servePut accepts and stores the given report.
|
// servePut accepts and stores the given report.
|
||||||
func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *http.Request) {
|
func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *http.Request) {
|
||||||
|
result := "receive_failure"
|
||||||
|
defer func() {
|
||||||
|
metricCrashReportsTotal.WithLabelValues(result).Inc()
|
||||||
|
}()
|
||||||
|
|
||||||
|
r.ignoredMut.RLock()
|
||||||
|
_, ignored := r.ignored[reportID]
|
||||||
|
r.ignoredMut.RUnlock()
|
||||||
|
if ignored {
|
||||||
|
result = "ignored_cached"
|
||||||
|
io.Copy(io.Discard, req.Body)
|
||||||
|
return // found
|
||||||
|
}
|
||||||
|
|
||||||
// Read at most maxRequestSize of report data.
|
// Read at most maxRequestSize of report data.
|
||||||
log.Println("Receiving report", reportID)
|
log.Println("Receiving report", reportID)
|
||||||
lr := io.LimitReader(req.Body, maxRequestSize)
|
lr := io.LimitReader(req.Body, maxRequestSize)
|
||||||
@@ -81,13 +106,28 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.ignore.match(bs) {
|
||||||
|
r.ignoredMut.Lock()
|
||||||
|
if r.ignored == nil {
|
||||||
|
r.ignored = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
r.ignored[reportID] = struct{}{}
|
||||||
|
r.ignoredMut.Unlock()
|
||||||
|
result = "ignored"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result = "success"
|
||||||
|
|
||||||
// Store the report
|
// Store the report
|
||||||
if !r.store.Put(reportID, bs) {
|
if !r.store.Put(reportID, bs) {
|
||||||
log.Println("Failed to store report (queue full):", reportID)
|
log.Println("Failed to store report (queue full):", reportID)
|
||||||
|
result = "queue_failure"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the report to Sentry
|
// Send the report to Sentry
|
||||||
if !r.sentry.Send(reportID, bs) {
|
if !r.sentry.Send(reportID, userIDFor(req), bs) {
|
||||||
log.Println("Failed to send report to sentry (queue full):", reportID)
|
log.Println("Failed to send report to sentry (queue full):", reportID)
|
||||||
|
result = "sentry_failure"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,14 +9,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/sha256"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// userIDFor returns a string we can use as the user ID for the purpose of
|
// userIDFor returns a string we can use as the user ID for the purpose of
|
||||||
@@ -53,5 +52,5 @@ func compressAndWrite(bs []byte, fullPath string) error {
|
|||||||
gw.Close()
|
gw.Close()
|
||||||
|
|
||||||
// Create an output file with the compressed report
|
// Create an output file with the compressed report
|
||||||
return os.WriteFile(fullPath, buf.Bytes(), 0644)
|
return os.WriteFile(fullPath, buf.Bytes(), 0o644)
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ to NAT or firewall issues.
|
|||||||
|
|
||||||
There is very little reason why you'd want to run this yourself, as
|
There is very little reason why you'd want to run this yourself, as
|
||||||
`relaypoolsrv` is just used for announcement and lookup of public relay
|
`relaypoolsrv` is just used for announcement and lookup of public relay
|
||||||
servers. If you are looking to setup a private or a public relay, please
|
servers. If you are looking to set up a private or a public relay, please
|
||||||
check the documentation for
|
check the documentation for
|
||||||
[relaysrv](https://github.com/syncthing/relaysrv), which also explains how
|
[relaysrv](https://github.com/syncthing/relaysrv), which also explains how
|
||||||
to join the default public pool.
|
to join the default public pool.
|
||||||
@@ -21,4 +21,3 @@ See `relaypoolsrv -help` for configuration options.
|
|||||||
|
|
||||||
[oschwald/geoip2-golang](https://github.com/oschwald/geoip2-golang), [oschwald/maxminddb-golang](https://github.com/oschwald/maxminddb-golang), Copyright (C) 2015 [Gregory J. Oschwald](mailto:oschwald@gmail.com).
|
[oschwald/geoip2-golang](https://github.com/oschwald/geoip2-golang), [oschwald/maxminddb-golang](https://github.com/oschwald/maxminddb-golang), Copyright (C) 2015 [Gregory J. Oschwald](mailto:oschwald@gmail.com).
|
||||||
|
|
||||||
[lib/pq](https://github.com/lib/pq)</a>, Copyright (C) 2011-2013 'pq' Contributors Portions Copyright (C) 2011 Blake Mizerany.
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
//go:generate go run ../../../script/genassets.go -o gui.files.go ../gui
|
//go:generate go run ../../../../script/genassets.go -o gui.files.go ../gui
|
||||||
|
|
||||||
// Package auto contains auto generated files for web assets.
|
// Package auto contains auto generated files for web assets.
|
||||||
package auto
|
package auto
|
||||||
@@ -237,7 +237,7 @@
|
|||||||
uptimeSeconds: 0,
|
uptimeSeconds: 0,
|
||||||
};
|
};
|
||||||
$scope.map = L.map('map').setView([40.90296, 1.90925], 2);
|
$scope.map = L.map('map').setView([40.90296, 1.90925], 2);
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
{
|
{
|
||||||
attribution: 'Leaflet',
|
attribution: 'Leaflet',
|
||||||
maxZoom: 17
|
maxZoom: 17
|
||||||
@@ -259,7 +259,7 @@
|
|||||||
return a.value > b.value ? 1 : -1;
|
return a.value > b.value ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
$http.get("/endpoint").then(function(response) {
|
$http.get("/endpoint/full").then(function(response) {
|
||||||
$scope.relays = response.data.relays;
|
$scope.relays = response.data.relays;
|
||||||
|
|
||||||
angular.forEach($scope.relays, function(relay) {
|
angular.forEach($scope.relays, function(relay) {
|
||||||
@@ -338,7 +338,7 @@
|
|||||||
relay.showMarker = function() {
|
relay.showMarker = function() {
|
||||||
relay.marker.openPopup();
|
relay.marker.openPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
relay.hideMarker = function() {
|
relay.hideMarker = function() {
|
||||||
relay.marker.closePopup();
|
relay.marker.closePopup();
|
||||||
}
|
}
|
||||||
@@ -347,7 +347,7 @@
|
|||||||
|
|
||||||
function addCircleToMap(relay) {
|
function addCircleToMap(relay) {
|
||||||
console.log(relay.location.latitude)
|
console.log(relay.location.latitude)
|
||||||
L.circle([relay.location.latitude, relay.location.longitude],
|
L.circle([relay.location.latitude, relay.location.longitude],
|
||||||
{
|
{
|
||||||
radius: ((relay.stats.bytesProxied * 100) / $scope.totals.bytesProxied) * 10000,
|
radius: ((relay.stats.bytesProxied * 100) / $scope.totals.bytesProxied) * 10000,
|
||||||
color: "FF0000",
|
color: "FF0000",
|
||||||
@@ -21,14 +21,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
lru "github.com/hashicorp/golang-lru/v2"
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
"github.com/syncthing/syncthing/lib/httpcache"
|
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
|
||||||
|
|
||||||
"github.com/oschwald/geoip2-golang"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto"
|
|
||||||
|
"github.com/syncthing/syncthing/cmd/infra/strelaypoolsrv/auto"
|
||||||
"github.com/syncthing/syncthing/lib/assets"
|
"github.com/syncthing/syncthing/lib/assets"
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
|
"github.com/syncthing/syncthing/lib/geoip"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
"github.com/syncthing/syncthing/lib/rand"
|
"github.com/syncthing/syncthing/lib/rand"
|
||||||
"github.com/syncthing/syncthing/lib/relay/client"
|
"github.com/syncthing/syncthing/lib/relay/client"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
@@ -51,6 +51,10 @@ type relay struct {
|
|||||||
StatsRetrieved time.Time `json:"statsRetrieved"`
|
StatsRetrieved time.Time `json:"statsRetrieved"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type relayShort struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
type stats struct {
|
type stats struct {
|
||||||
StartTime time.Time `json:"startTime"`
|
StartTime time.Time `json:"startTime"`
|
||||||
UptimeSeconds int `json:"uptimeSeconds"`
|
UptimeSeconds int `json:"uptimeSeconds"`
|
||||||
@@ -95,16 +99,19 @@ var (
|
|||||||
testCert tls.Certificate
|
testCert tls.Certificate
|
||||||
knownRelaysFile = filepath.Join(os.TempDir(), "strelaypoolsrv_known_relays")
|
knownRelaysFile = filepath.Join(os.TempDir(), "strelaypoolsrv_known_relays")
|
||||||
listen = ":80"
|
listen = ":80"
|
||||||
|
metricsListen = ":8081"
|
||||||
dir string
|
dir string
|
||||||
evictionTime = time.Hour
|
evictionTime = time.Hour
|
||||||
debug bool
|
debug bool
|
||||||
permRelaysFile string
|
permRelaysFile string
|
||||||
ipHeader string
|
ipHeader string
|
||||||
geoipPath string
|
|
||||||
proto string
|
proto string
|
||||||
statsRefresh = time.Minute
|
statsRefresh = time.Minute
|
||||||
requestQueueLen = 64
|
requestQueueLen = 64
|
||||||
requestProcessors = 8
|
requestProcessors = 8
|
||||||
|
geoipLicenseKey = os.Getenv("GEOIP_LICENSE_KEY")
|
||||||
|
geoipAccountID, _ = strconv.Atoi(os.Getenv("GEOIP_ACCOUNT_ID"))
|
||||||
|
maxRelaysReturned = 100
|
||||||
|
|
||||||
requests chan request
|
requests chan request
|
||||||
|
|
||||||
@@ -124,40 +131,46 @@ func main() {
|
|||||||
log.SetFlags(log.Lshortfile)
|
log.SetFlags(log.Lshortfile)
|
||||||
|
|
||||||
flag.StringVar(&listen, "listen", listen, "Listen address")
|
flag.StringVar(&listen, "listen", listen, "Listen address")
|
||||||
|
flag.StringVar(&metricsListen, "metrics-listen", metricsListen, "Metrics listen address")
|
||||||
flag.StringVar(&dir, "keys", dir, "Directory where http-cert.pem and http-key.pem is stored for TLS listening")
|
flag.StringVar(&dir, "keys", dir, "Directory where http-cert.pem and http-key.pem is stored for TLS listening")
|
||||||
flag.BoolVar(&debug, "debug", debug, "Enable debug output")
|
flag.BoolVar(&debug, "debug", debug, "Enable debug output")
|
||||||
flag.DurationVar(&evictionTime, "eviction", evictionTime, "After how long the relay is evicted")
|
flag.DurationVar(&evictionTime, "eviction", evictionTime, "After how long the relay is evicted")
|
||||||
flag.StringVar(&permRelaysFile, "perm-relays", "", "Path to list of permanent relays")
|
flag.StringVar(&permRelaysFile, "perm-relays", "", "Path to list of permanent relays")
|
||||||
flag.StringVar(&knownRelaysFile, "known-relays", knownRelaysFile, "Path to list of current relays")
|
flag.StringVar(&knownRelaysFile, "known-relays", knownRelaysFile, "Path to list of current relays")
|
||||||
flag.StringVar(&ipHeader, "ip-header", "", "Name of header which holds clients ip:port. Only meaningful when running behind a reverse proxy.")
|
flag.StringVar(&ipHeader, "ip-header", "", "Name of header which holds clients ip:port. Only meaningful when running behind a reverse proxy.")
|
||||||
flag.StringVar(&geoipPath, "geoip", "GeoLite2-City.mmdb", "Path to GeoLite2-City database")
|
|
||||||
flag.StringVar(&proto, "protocol", "tcp", "Protocol used for listening. 'tcp' for IPv4 and IPv6, 'tcp4' for IPv4, 'tcp6' for IPv6")
|
flag.StringVar(&proto, "protocol", "tcp", "Protocol used for listening. 'tcp' for IPv4 and IPv6, 'tcp4' for IPv4, 'tcp6' for IPv6")
|
||||||
flag.DurationVar(&statsRefresh, "stats-refresh", statsRefresh, "Interval at which to refresh relay stats")
|
flag.DurationVar(&statsRefresh, "stats-refresh", statsRefresh, "Interval at which to refresh relay stats")
|
||||||
flag.IntVar(&requestQueueLen, "request-queue", requestQueueLen, "Queue length for incoming test requests")
|
flag.IntVar(&requestQueueLen, "request-queue", requestQueueLen, "Queue length for incoming test requests")
|
||||||
flag.IntVar(&requestProcessors, "request-processors", requestProcessors, "Number of request processor routines")
|
flag.IntVar(&requestProcessors, "request-processors", requestProcessors, "Number of request processor routines")
|
||||||
|
flag.StringVar(&geoipLicenseKey, "geoip-license-key", geoipLicenseKey, "License key for GeoIP database")
|
||||||
|
flag.IntVar(&maxRelaysReturned, "max-relays-returned", maxRelaysReturned, "Maximum number of relays returned for a normal endpoint query")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
requests = make(chan request, requestQueueLen)
|
requests = make(chan request, requestQueueLen)
|
||||||
|
geoip, err := geoip.NewGeoLite2CityProvider(context.Background(), geoipAccountID, geoipLicenseKey, os.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to create GeoIP provider:", err)
|
||||||
|
}
|
||||||
|
go geoip.Serve(context.TODO())
|
||||||
|
|
||||||
var listener net.Listener
|
var listener net.Listener
|
||||||
var err error
|
|
||||||
|
|
||||||
if permRelaysFile != "" {
|
if permRelaysFile != "" {
|
||||||
permanentRelays = loadRelays(permRelaysFile)
|
permanentRelays = loadRelays(permRelaysFile, geoip)
|
||||||
}
|
}
|
||||||
|
|
||||||
testCert = createTestCertificate()
|
testCert = createTestCertificate()
|
||||||
|
|
||||||
for i := 0; i < requestProcessors; i++ {
|
for i := 0; i < requestProcessors; i++ {
|
||||||
go requestProcessor()
|
go requestProcessor(geoip)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load relays from cache in the background.
|
// Load relays from cache in the background.
|
||||||
// Load them in a serial fashion to make sure any genuine requests
|
// Load them in a serial fashion to make sure any genuine requests
|
||||||
// are not dropped.
|
// are not dropped.
|
||||||
go func() {
|
go func() {
|
||||||
for _, relay := range loadRelays(knownRelaysFile) {
|
for _, relay := range loadRelays(knownRelaysFile, geoip) {
|
||||||
resultChan := make(chan result)
|
resultChan := make(chan result)
|
||||||
requests <- request{relay, resultChan, nil}
|
requests <- request{relay, resultChan, nil}
|
||||||
result := <-resultChan
|
result := <-resultChan
|
||||||
@@ -213,15 +226,40 @@ func main() {
|
|||||||
log.Fatalln("listen:", err)
|
log.Fatalln("listen:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := http.NewServeMux()
|
if metricsListen != "" {
|
||||||
handler.HandleFunc("/", handleAssets)
|
mmux := http.NewServeMux()
|
||||||
handler.Handle("/endpoint", httpcache.SinglePath(http.HandlerFunc(handleRequest), 15*time.Second))
|
mmux.HandleFunc("/metrics", handleMetrics)
|
||||||
handler.HandleFunc("/metrics", handleMetrics)
|
go func() {
|
||||||
|
if err := http.ListenAndServe(metricsListen, mmux); err != nil {
|
||||||
|
log.Fatalln("HTTP serve metrics:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
getMux := http.NewServeMux()
|
||||||
|
getMux.HandleFunc("/", handleAssets)
|
||||||
|
getMux.HandleFunc("/endpoint", withAPIMetrics(handleEndpointShort))
|
||||||
|
getMux.HandleFunc("/endpoint/full", withAPIMetrics(handleEndpointFull))
|
||||||
|
|
||||||
|
postMux := http.NewServeMux()
|
||||||
|
postMux.HandleFunc("/endpoint", withAPIMetrics(handleRegister))
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet, http.MethodHead, http.MethodOptions:
|
||||||
|
getMux.ServeHTTP(w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
postMux.ServeHTTP(w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
srv := http.Server{
|
srv := http.Server{
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
ReadTimeout: 10 * time.Second,
|
ReadTimeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
|
srv.SetKeepAlivesEnabled(false)
|
||||||
|
|
||||||
err = srv.Serve(listener)
|
err = srv.Serve(listener)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -255,39 +293,24 @@ func handleAssets(w http.ResponseWriter, r *http.Request) {
|
|||||||
assets.Serve(w, r, as)
|
assets.Serve(w, r, as)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
func withAPIMetrics(next http.HandlerFunc) http.HandlerFunc {
|
||||||
timer := prometheus.NewTimer(apiRequestsSeconds.WithLabelValues(r.Method))
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
timer := prometheus.NewTimer(apiRequestsSeconds.WithLabelValues(r.Method))
|
||||||
w = NewLoggingResponseWriter(w)
|
w = NewLoggingResponseWriter(w)
|
||||||
defer func() {
|
defer func() {
|
||||||
timer.ObserveDuration()
|
timer.ObserveDuration()
|
||||||
lw := w.(*loggingResponseWriter)
|
lw := w.(*loggingResponseWriter)
|
||||||
apiRequestsTotal.WithLabelValues(r.Method, strconv.Itoa(lw.statusCode)).Inc()
|
apiRequestsTotal.WithLabelValues(r.Method, strconv.Itoa(lw.statusCode)).Inc()
|
||||||
}()
|
}()
|
||||||
|
next(w, r)
|
||||||
if ipHeader != "" {
|
|
||||||
hdr := r.Header.Get(ipHeader)
|
|
||||||
fields := strings.Split(hdr, ",")
|
|
||||||
if len(fields) > 0 {
|
|
||||||
r.RemoteAddr = strings.TrimSpace(fields[len(fields)-1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
switch r.Method {
|
|
||||||
case "GET":
|
|
||||||
handleGetRequest(w, r)
|
|
||||||
case "POST":
|
|
||||||
handlePostRequest(w, r)
|
|
||||||
default:
|
|
||||||
if debug {
|
|
||||||
log.Println("Unhandled HTTP method", r.Method)
|
|
||||||
}
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGetRequest(rw http.ResponseWriter, r *http.Request) {
|
// handleEndpointFull returns the relay list with full metadata and
|
||||||
|
// statistics. Large, and expensive.
|
||||||
|
func handleEndpointFull(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.Header().Set("Content-Type", "application/json; charset=utf-8")
|
rw.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
rw.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
mut.RLock()
|
mut.RLock()
|
||||||
relays := make([]*relay, len(permanentRelays)+len(knownRelays))
|
relays := make([]*relay, len(permanentRelays)+len(knownRelays))
|
||||||
@@ -295,17 +318,42 @@ func handleGetRequest(rw http.ResponseWriter, r *http.Request) {
|
|||||||
copy(relays[n:], knownRelays)
|
copy(relays[n:], knownRelays)
|
||||||
mut.RUnlock()
|
mut.RUnlock()
|
||||||
|
|
||||||
// Shuffle
|
|
||||||
rand.Shuffle(relays)
|
|
||||||
|
|
||||||
_ = json.NewEncoder(rw).Encode(map[string][]*relay{
|
_ = json.NewEncoder(rw).Encode(map[string][]*relay{
|
||||||
"relays": relays,
|
"relays": relays,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePostRequest(w http.ResponseWriter, r *http.Request) {
|
// handleEndpointShort returns the relay list with only the URL.
|
||||||
|
func handleEndpointShort(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
rw.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
rw.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
mut.RLock()
|
||||||
|
relays := make([]relayShort, 0, len(permanentRelays)+len(knownRelays))
|
||||||
|
for _, r := range append(permanentRelays, knownRelays...) {
|
||||||
|
relays = append(relays, relayShort{URL: slimURL(r.URL)})
|
||||||
|
}
|
||||||
|
mut.RUnlock()
|
||||||
|
if len(relays) > maxRelaysReturned {
|
||||||
|
rand.Shuffle(relays)
|
||||||
|
relays = relays[:maxRelaysReturned]
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = json.NewEncoder(rw).Encode(map[string][]relayShort{
|
||||||
|
"relays": relays,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||||
// Get the IP address of the client
|
// Get the IP address of the client
|
||||||
rhost := r.RemoteAddr
|
rhost := r.RemoteAddr
|
||||||
|
if ipHeader != "" {
|
||||||
|
hdr := r.Header.Get(ipHeader)
|
||||||
|
fields := strings.Split(hdr, ",")
|
||||||
|
if len(fields) > 0 {
|
||||||
|
rhost = strings.TrimSpace(fields[len(fields)-1])
|
||||||
|
}
|
||||||
|
}
|
||||||
if host, _, err := net.SplitHostPort(rhost); err == nil {
|
if host, _, err := net.SplitHostPort(rhost); err == nil {
|
||||||
rhost = host
|
rhost = host
|
||||||
}
|
}
|
||||||
@@ -425,19 +473,19 @@ func handlePostRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestProcessor() {
|
func requestProcessor(geoip *geoip.Provider) {
|
||||||
for request := range requests {
|
for request := range requests {
|
||||||
if request.queueTimer != nil {
|
if request.queueTimer != nil {
|
||||||
request.queueTimer.ObserveDuration()
|
request.queueTimer.ObserveDuration()
|
||||||
}
|
}
|
||||||
|
|
||||||
timer := prometheus.NewTimer(relayTestActionsSeconds.WithLabelValues("test"))
|
timer := prometheus.NewTimer(relayTestActionsSeconds.WithLabelValues("test"))
|
||||||
handleRelayTest(request)
|
handleRelayTest(request, geoip)
|
||||||
timer.ObserveDuration()
|
timer.ObserveDuration()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRelayTest(request request) {
|
func handleRelayTest(request request, geoip *geoip.Provider) {
|
||||||
if debug {
|
if debug {
|
||||||
log.Println("Request for", request.relay)
|
log.Println("Request for", request.relay)
|
||||||
}
|
}
|
||||||
@@ -450,7 +498,7 @@ func handleRelayTest(request request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stats := fetchStats(request.relay)
|
stats := fetchStats(request.relay)
|
||||||
location := getLocation(request.relay.uri.Host)
|
location := getLocation(request.relay.uri.Host, geoip)
|
||||||
|
|
||||||
mut.Lock()
|
mut.Lock()
|
||||||
if stats != nil {
|
if stats != nil {
|
||||||
@@ -523,7 +571,7 @@ func evict(relay *relay) func() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadRelays(file string) []*relay {
|
func loadRelays(file string, geoip *geoip.Provider) []*relay {
|
||||||
content, err := os.ReadFile(file)
|
content, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Failed to load relays: " + err.Error())
|
log.Println("Failed to load relays: " + err.Error())
|
||||||
@@ -547,7 +595,7 @@ func loadRelays(file string) []*relay {
|
|||||||
|
|
||||||
relays = append(relays, &relay{
|
relays = append(relays, &relay{
|
||||||
URL: line,
|
URL: line,
|
||||||
Location: getLocation(uri.Host),
|
Location: getLocation(uri.Host, geoip),
|
||||||
uri: uri,
|
uri: uri,
|
||||||
})
|
})
|
||||||
if debug {
|
if debug {
|
||||||
@@ -580,21 +628,16 @@ func createTestCertificate() tls.Certificate {
|
|||||||
return cert
|
return cert
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLocation(host string) location {
|
func getLocation(host string, geoip *geoip.Provider) location {
|
||||||
timer := prometheus.NewTimer(locationLookupSeconds)
|
timer := prometheus.NewTimer(locationLookupSeconds)
|
||||||
defer timer.ObserveDuration()
|
defer timer.ObserveDuration()
|
||||||
db, err := geoip2.Open(geoipPath)
|
|
||||||
if err != nil {
|
|
||||||
return location{}
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
addr, err := net.ResolveTCPAddr("tcp", host)
|
addr, err := net.ResolveTCPAddr("tcp", host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return location{}
|
return location{}
|
||||||
}
|
}
|
||||||
|
|
||||||
city, err := db.City(addr.IP)
|
city, err := geoip.City(addr.IP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return location{}
|
return location{}
|
||||||
}
|
}
|
||||||
@@ -660,3 +703,16 @@ func (b *errorTracker) IsBlocked(host string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func slimURL(u string) string {
|
||||||
|
p, err := url.Parse(u)
|
||||||
|
if err != nil {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
newQuery := url.Values{}
|
||||||
|
if id := p.Query().Get("id"); id != "" {
|
||||||
|
newQuery.Set("id", id)
|
||||||
|
}
|
||||||
|
p.RawQuery = newQuery.Encode()
|
||||||
|
return p.String()
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ func TestHandleGetRequest(t *testing.T) {
|
|||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
w.Body = new(bytes.Buffer)
|
w.Body = new(bytes.Buffer)
|
||||||
handleGetRequest(w, httptest.NewRequest("GET", "/", nil))
|
handleEndpointFull(w, httptest.NewRequest("GET", "/", nil))
|
||||||
|
|
||||||
result := make(map[string][]*relay)
|
result := make(map[string][]*relay)
|
||||||
err := json.NewDecoder(w.Body).Decode(&result)
|
err := json.NewDecoder(w.Body).Decode(&result)
|
||||||
@@ -92,3 +92,18 @@ func TestCanonicalizeQueryValues(t *testing.T) {
|
|||||||
t.Errorf("expected %q, got %q", exp, str)
|
t.Errorf("expected %q, got %q", exp, str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSlimURL(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
in, out string
|
||||||
|
}{
|
||||||
|
{"http://example.com/", "http://example.com/"},
|
||||||
|
{"relay://192.0.2.42:22067/?globalLimitBps=0&id=EIC6B3M-EIC6B3M-EIC6B3M-EIC6B3M-EIC6B3M-EIC6B3M-EIC6B3M-EIC6B3M&networkTimeout=2m0s&pingInterval=1m0s&providedBy=Test&sessionLimitBps=0&statusAddr=%3A22070", "relay://192.0.2.42:22067/?id=EIC6B3M-EIC6B3M-EIC6B3M-EIC6B3M-EIC6B3M-EIC6B3M-EIC6B3M-EIC6B3M"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
if got := slimURL(c.in); got != c.out {
|
||||||
|
t.Errorf("expected %q, got %q", c.out, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,27 +6,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
processCollectorOpts := collectors.ProcessCollectorOpts{
|
|
||||||
Namespace: "syncthing_relaypoolsrv",
|
|
||||||
PidFn: func() (int, error) {
|
|
||||||
return os.Getpid(), nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
prometheus.MustRegister(
|
|
||||||
collectors.NewProcessCollector(processCollectorOpts),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
statusClient = http.Client{
|
statusClient = http.Client{
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
// Copyright (C) 2019 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
|
"github.com/syncthing/syncthing/lib/httpcache"
|
||||||
|
"github.com/syncthing/syncthing/lib/upgrade"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cli struct {
|
||||||
|
Listen string `default:":8080" help:"Listen address"`
|
||||||
|
MetricsListen string `default:":8082" help:"Listen address for metrics"`
|
||||||
|
URL string `short:"u" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=25" help:"GitHub releases url"`
|
||||||
|
Forward []string `short:"f" help:"Forwarded pages, format: /path->https://example/com/url"`
|
||||||
|
CacheTime time.Duration `default:"15m" help:"Cache time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var params cli
|
||||||
|
kong.Parse(¶ms)
|
||||||
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelInfo,
|
||||||
|
})))
|
||||||
|
|
||||||
|
if err := server(¶ms); err != nil {
|
||||||
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func server(params *cli) error {
|
||||||
|
if params.MetricsListen != "" {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/metrics", promhttp.Handler())
|
||||||
|
metricsListen, err := net.Listen("tcp", params.MetricsListen)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("metrics: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("Metrics listener started", "addr", params.MetricsListen)
|
||||||
|
go func() {
|
||||||
|
if err := http.Serve(metricsListen, mux); err != nil {
|
||||||
|
slog.Warn("Metrics server returned", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
cache := &cachedReleases{url: params.URL}
|
||||||
|
if err := cache.Update(context.Background()); err != nil {
|
||||||
|
return fmt.Errorf("initial cache update: %w", err)
|
||||||
|
} else {
|
||||||
|
slog.Info("Initial cache update done")
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for range time.NewTicker(params.CacheTime).C {
|
||||||
|
slog.Info("Refreshing cached releases", "url", params.URL)
|
||||||
|
if err := cache.Update(context.Background()); err != nil {
|
||||||
|
slog.Error("Failed to refresh cached releases", "url", params.URL, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ghRels := &githubReleases{cache: cache}
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/ping", ghRels.servePing)
|
||||||
|
mux.HandleFunc("/meta.json", ghRels.serveReleases)
|
||||||
|
|
||||||
|
for _, fwd := range params.Forward {
|
||||||
|
path, url, ok := strings.Cut(fwd, "->")
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid forward: %q", fwd)
|
||||||
|
}
|
||||||
|
slog.Info("Forwarding", "from", path, "to", url)
|
||||||
|
name := strings.ReplaceAll(path, "/", "_")
|
||||||
|
mux.Handle(path, httpcache.SinglePath(&proxy{name: name, url: url}, params.CacheTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: params.Listen,
|
||||||
|
Handler: mux,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
srv.SetKeepAlivesEnabled(false)
|
||||||
|
|
||||||
|
srvListener, err := net.Listen("tcp", params.Listen)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listen: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("Main listener started", "addr", params.Listen)
|
||||||
|
|
||||||
|
return srv.Serve(srvListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
type githubReleases struct {
|
||||||
|
cache *cachedReleases
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *githubReleases) servePing(w http.ResponseWriter, req *http.Request) {
|
||||||
|
rels := p.cache.Releases()
|
||||||
|
|
||||||
|
if len(rels) == 0 {
|
||||||
|
http.Error(w, "No releases available", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Syncthing-Num-Releases", strconv.Itoa(len(rels)))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *githubReleases) serveReleases(w http.ResponseWriter, req *http.Request) {
|
||||||
|
rels := p.cache.Releases()
|
||||||
|
|
||||||
|
ua := req.Header.Get("User-Agent")
|
||||||
|
osv := req.Header.Get("Syncthing-Os-Version")
|
||||||
|
if ua != "" && osv != "" {
|
||||||
|
// We should determine the compatibility of the releases.
|
||||||
|
rels = filterForCompabitility(rels, ua, osv)
|
||||||
|
} else {
|
||||||
|
metricFilterCalls.WithLabelValues("no-ua-or-osversion").Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
rels = filterForLatest(rels)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=900")
|
||||||
|
w.Header().Set("Vary", "User-Agent, Syncthing-Os-Version")
|
||||||
|
_ = json.NewEncoder(w).Encode(rels)
|
||||||
|
|
||||||
|
metricUpgradeChecks.Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
type proxy struct {
|
||||||
|
name string
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
req, err := http.NewRequestWithContext(req.Context(), http.MethodGet, p.url, nil)
|
||||||
|
if err != nil {
|
||||||
|
metricHTTPRequests.WithLabelValues(p.name, "error").Inc()
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
metricHTTPRequests.WithLabelValues(p.name, "error").Inc()
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
metricHTTPRequests.WithLabelValues(p.name, "success").Inc()
|
||||||
|
|
||||||
|
ct := resp.Header.Get("Content-Type")
|
||||||
|
w.Header().Set("Content-Type", ct)
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=900")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
||||||
|
}
|
||||||
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
if strings.HasPrefix(ct, "application/json") {
|
||||||
|
// Special JSON handling; clean it up a bit.
|
||||||
|
var v interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(v)
|
||||||
|
} else {
|
||||||
|
_, _ = io.Copy(w, resp.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterForLatest returns the latest stable and prerelease only. If the
|
||||||
|
// stable version is newer (comes first in the list) there is no need to go
|
||||||
|
// looking for a prerelease at all.
|
||||||
|
func filterForLatest(rels []upgrade.Release) []upgrade.Release {
|
||||||
|
var filtered []upgrade.Release
|
||||||
|
havePre := make(map[string]bool)
|
||||||
|
haveStable := make(map[string]bool)
|
||||||
|
for _, rel := range rels {
|
||||||
|
major, _, _ := strings.Cut(rel.Tag, ".")
|
||||||
|
if !rel.Prerelease && !haveStable[major] {
|
||||||
|
// Remember the first non-pre for each major
|
||||||
|
filtered = append(filtered, rel)
|
||||||
|
haveStable[major] = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rel.Prerelease && !havePre[major] && !haveStable[major] {
|
||||||
|
// We remember the first prerelease we find, unless we've
|
||||||
|
// already found a non-pre of the same major.
|
||||||
|
filtered = append(filtered, rel)
|
||||||
|
havePre[major] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
var userAgentOSArchExp = regexp.MustCompile(`^syncthing.*\(.+ (\w+)-(\w+)\)$`)
|
||||||
|
|
||||||
|
func filterForCompabitility(rels []upgrade.Release, ua, osv string) []upgrade.Release {
|
||||||
|
osArch := userAgentOSArchExp.FindStringSubmatch(ua)
|
||||||
|
if len(osArch) != 3 {
|
||||||
|
metricFilterCalls.WithLabelValues("bad-os-arch").Inc()
|
||||||
|
return rels
|
||||||
|
}
|
||||||
|
os := osArch[1]
|
||||||
|
|
||||||
|
var filtered []upgrade.Release
|
||||||
|
for _, rel := range rels {
|
||||||
|
if rel.Compatibility == nil {
|
||||||
|
// No requirements means it's compatible with everything.
|
||||||
|
filtered = append(filtered, rel)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
req, ok := rel.Compatibility.Requirements[os]
|
||||||
|
if !ok {
|
||||||
|
// No entry for the current OS means it's compatible.
|
||||||
|
filtered = append(filtered, rel)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if upgrade.CompareVersions(osv, req) >= 0 {
|
||||||
|
filtered = append(filtered, rel)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filtered) != len(rels) {
|
||||||
|
metricFilterCalls.WithLabelValues("filtered").Inc()
|
||||||
|
} else {
|
||||||
|
metricFilterCalls.WithLabelValues("unchanged").Inc()
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
type cachedReleases struct {
|
||||||
|
url string
|
||||||
|
mut sync.RWMutex
|
||||||
|
current []upgrade.Release
|
||||||
|
latestRel, latestPre string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cachedReleases) Releases() []upgrade.Release {
|
||||||
|
c.mut.RLock()
|
||||||
|
defer c.mut.RUnlock()
|
||||||
|
return c.current
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cachedReleases) Update(ctx context.Context) error {
|
||||||
|
rels, err := fetchGithubReleases(ctx, c.url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
latestRel, latestPre := "", ""
|
||||||
|
for _, rel := range rels {
|
||||||
|
if !rel.Prerelease && latestRel == "" {
|
||||||
|
latestRel = rel.Tag
|
||||||
|
}
|
||||||
|
if rel.Prerelease && latestPre == "" {
|
||||||
|
latestPre = rel.Tag
|
||||||
|
}
|
||||||
|
if latestRel != "" && latestPre != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mut.Lock()
|
||||||
|
c.current = rels
|
||||||
|
if latestRel != c.latestRel || latestPre != c.latestPre {
|
||||||
|
metricLatestReleaseInfo.DeleteLabelValues(c.latestRel, c.latestPre)
|
||||||
|
metricLatestReleaseInfo.WithLabelValues(latestRel, latestPre).Set(1)
|
||||||
|
c.latestRel = latestRel
|
||||||
|
c.latestPre = latestPre
|
||||||
|
}
|
||||||
|
c.mut.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchGithubReleases(ctx context.Context, url string) ([]upgrade.Release, error) {
|
||||||
|
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
metricHTTPRequests.WithLabelValues("github-releases", "error").Inc()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
metricHTTPRequests.WithLabelValues("github-releases", "error").Inc()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var rels []upgrade.Release
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&rels); err != nil {
|
||||||
|
metricHTTPRequests.WithLabelValues("github-releases", "error").Inc()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
metricHTTPRequests.WithLabelValues("github-releases", "success").Inc()
|
||||||
|
|
||||||
|
// Move the URL used for browser downloads to the URL field, and remove
|
||||||
|
// the browser URL field. This avoids going via the GitHub API for
|
||||||
|
// downloads, since Syncthing uses the URL field.
|
||||||
|
for _, rel := range rels {
|
||||||
|
for j, asset := range rel.Assets {
|
||||||
|
rel.Assets[j].URL = asset.BrowserURL
|
||||||
|
rel.Assets[j].BrowserURL = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addReleaseCompatibility(ctx, rels)
|
||||||
|
|
||||||
|
sort.Sort(upgrade.SortByRelease(rels))
|
||||||
|
return rels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addReleaseCompatibility(ctx context.Context, rels []upgrade.Release) {
|
||||||
|
for i := range rels {
|
||||||
|
rel := &rels[i]
|
||||||
|
for i, asset := range rel.Assets {
|
||||||
|
if asset.Name != "compat.json" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load compat.json into the Compatibility field
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, asset.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
metricHTTPRequests.WithLabelValues("compat-json", "error").Inc()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
metricHTTPRequests.WithLabelValues("compat-json", "error").Inc()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
metricHTTPRequests.WithLabelValues("compat-json", "error").Inc()
|
||||||
|
resp.Body.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_ = json.NewDecoder(io.LimitReader(resp.Body, 10<<10)).Decode(&rel.Compatibility)
|
||||||
|
metricHTTPRequests.WithLabelValues("compat-json", "success").Inc()
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
// Remove compat.json from the asset list since it's been processed
|
||||||
|
rel.Assets = append(rel.Assets[:i], rel.Assets[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright (C) 2024 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
metricUpgradeChecks = promauto.NewCounter(prometheus.CounterOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "upgrade",
|
||||||
|
Name: "metadata_requests",
|
||||||
|
})
|
||||||
|
metricFilterCalls = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "upgrade",
|
||||||
|
Name: "filter_calls",
|
||||||
|
}, []string{"result"})
|
||||||
|
metricHTTPRequests = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "upgrade",
|
||||||
|
Name: "http_requests",
|
||||||
|
}, []string{"target", "result"})
|
||||||
|
metricLatestReleaseInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "upgrade",
|
||||||
|
Name: "latest_release_info",
|
||||||
|
Help: "Release information",
|
||||||
|
}, []string{"latest_release", "latest_pre"})
|
||||||
|
)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (C) 2023 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
"github.com/syncthing/syncthing/cmd/infra/ursrv/serve"
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CLI struct {
|
||||||
|
Serve serve.CLI `cmd:"" default:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelInfo,
|
||||||
|
})))
|
||||||
|
|
||||||
|
var cli CLI
|
||||||
|
ctx := kong.Parse(&cli)
|
||||||
|
if err := ctx.Run(); err != nil {
|
||||||
|
log.Fatalf("%s: %v", ctx.Command(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
package main
|
package serve
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (C) 2023 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
metricReportsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "ursrv_v2",
|
||||||
|
Name: "incoming_reports_total",
|
||||||
|
}, []string{"result"})
|
||||||
|
metricsCollectsTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "ursrv_v2",
|
||||||
|
Name: "collects_total",
|
||||||
|
})
|
||||||
|
metricsCollectSecondsTotal = promauto.NewCounter(prometheus.CounterOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "ursrv_v2",
|
||||||
|
Name: "collect_seconds_total",
|
||||||
|
})
|
||||||
|
metricsCollectSecondsLast = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "ursrv_v2",
|
||||||
|
Name: "collect_seconds_last",
|
||||||
|
})
|
||||||
|
metricsWriteSecondsLast = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "ursrv_v2",
|
||||||
|
Name: "write_seconds_last",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
metricReportsTotal.WithLabelValues("fail")
|
||||||
|
metricReportsTotal.WithLabelValues("replace")
|
||||||
|
metricReportsTotal.WithLabelValues("accept")
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
// Copyright (C) 2024 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
const namePrefix = "syncthing_usage_"
|
||||||
|
|
||||||
|
type metricsSet struct {
|
||||||
|
srv *server
|
||||||
|
|
||||||
|
gauges map[string]prometheus.Gauge
|
||||||
|
gaugeVecs map[string]*prometheus.GaugeVec
|
||||||
|
gaugeVecLabels map[string][]string
|
||||||
|
summaries map[string]*metricSummary
|
||||||
|
|
||||||
|
collectMut sync.Mutex
|
||||||
|
collectCutoff time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMetricsSet(srv *server) *metricsSet {
|
||||||
|
s := &metricsSet{
|
||||||
|
srv: srv,
|
||||||
|
gauges: make(map[string]prometheus.Gauge),
|
||||||
|
gaugeVecs: make(map[string]*prometheus.GaugeVec),
|
||||||
|
gaugeVecLabels: make(map[string][]string),
|
||||||
|
summaries: make(map[string]*metricSummary),
|
||||||
|
collectCutoff: -24 * time.Hour,
|
||||||
|
}
|
||||||
|
|
||||||
|
var initForType func(reflect.Type)
|
||||||
|
initForType = func(t reflect.Type) {
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
if field.Type.Kind() == reflect.Struct {
|
||||||
|
initForType(field.Type)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name, typ, label := fieldNameTypeLabel(field)
|
||||||
|
sname, labels := nameConstLabels(name)
|
||||||
|
switch typ {
|
||||||
|
case "gauge":
|
||||||
|
s.gauges[name] = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Name: namePrefix + sname,
|
||||||
|
ConstLabels: labels,
|
||||||
|
})
|
||||||
|
case "summary":
|
||||||
|
s.summaries[name] = newMetricSummary(namePrefix+sname, nil, labels)
|
||||||
|
case "gaugeVec":
|
||||||
|
s.gaugeVecLabels[name] = append(s.gaugeVecLabels[name], label)
|
||||||
|
case "summaryVec":
|
||||||
|
s.summaries[name] = newMetricSummary(namePrefix+sname, []string{label}, labels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initForType(reflect.ValueOf(contract.Report{}).Type())
|
||||||
|
|
||||||
|
for name, labels := range s.gaugeVecLabels {
|
||||||
|
s.gaugeVecs[name] = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Name: namePrefix + name,
|
||||||
|
}, labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldNameTypeLabel(rf reflect.StructField) (string, string, string) {
|
||||||
|
metric := rf.Tag.Get("metric")
|
||||||
|
name, typ, ok := strings.Cut(metric, ",")
|
||||||
|
if !ok {
|
||||||
|
return "", "", ""
|
||||||
|
}
|
||||||
|
gv, label, ok := strings.Cut(typ, ":")
|
||||||
|
if ok {
|
||||||
|
typ = gv
|
||||||
|
}
|
||||||
|
return name, typ, label
|
||||||
|
}
|
||||||
|
|
||||||
|
func nameConstLabels(name string) (string, prometheus.Labels) {
|
||||||
|
if name == "-" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
name, labels, ok := strings.Cut(name, "{")
|
||||||
|
if !ok {
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
lls := strings.Split(labels[:len(labels)-1], ",")
|
||||||
|
m := make(map[string]string)
|
||||||
|
for _, l := range lls {
|
||||||
|
k, v, _ := strings.Cut(l, "=")
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
return name, m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *metricsSet) addReport(r *contract.Report) {
|
||||||
|
gaugeVecs := make(map[string][]string)
|
||||||
|
s.addReportStruct(reflect.ValueOf(r).Elem(), gaugeVecs)
|
||||||
|
for name, lv := range gaugeVecs {
|
||||||
|
s.gaugeVecs[name].WithLabelValues(lv...).Add(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *metricsSet) addReportStruct(v reflect.Value, gaugeVecs map[string][]string) {
|
||||||
|
t := v.Type()
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
field := v.Field(i)
|
||||||
|
if field.Kind() == reflect.Struct {
|
||||||
|
s.addReportStruct(field, gaugeVecs)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name, typ, label := fieldNameTypeLabel(t.Field(i))
|
||||||
|
switch typ {
|
||||||
|
case "gauge":
|
||||||
|
switch v := field.Interface().(type) {
|
||||||
|
case int:
|
||||||
|
s.gauges[name].Add(float64(v))
|
||||||
|
case string:
|
||||||
|
s.gaugeVecs[name].WithLabelValues(v).Add(1)
|
||||||
|
case bool:
|
||||||
|
if v {
|
||||||
|
s.gauges[name].Add(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "gaugeVec":
|
||||||
|
var labelValue string
|
||||||
|
switch v := field.Interface().(type) {
|
||||||
|
case string:
|
||||||
|
labelValue = v
|
||||||
|
case int:
|
||||||
|
labelValue = strconv.Itoa(v)
|
||||||
|
case map[string]int:
|
||||||
|
for k, v := range v {
|
||||||
|
labelValue = k
|
||||||
|
field.SetInt(int64(v))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := gaugeVecs[name]; !ok {
|
||||||
|
gaugeVecs[name] = make([]string, len(s.gaugeVecLabels[name]))
|
||||||
|
}
|
||||||
|
for i, l := range s.gaugeVecLabels[name] {
|
||||||
|
if l == label {
|
||||||
|
gaugeVecs[name][i] = labelValue
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "summary", "summaryVec":
|
||||||
|
switch v := field.Interface().(type) {
|
||||||
|
case int:
|
||||||
|
s.summaries[name].Observe("", float64(v))
|
||||||
|
case float64:
|
||||||
|
s.summaries[name].Observe("", v)
|
||||||
|
case []int:
|
||||||
|
for _, v := range v {
|
||||||
|
s.summaries[name].Observe("", float64(v))
|
||||||
|
}
|
||||||
|
case map[string]int:
|
||||||
|
for k, v := range v {
|
||||||
|
if k == "" {
|
||||||
|
// avoid empty string labels as those are the sign
|
||||||
|
// of a non-vec summary
|
||||||
|
k = "unknown"
|
||||||
|
}
|
||||||
|
s.summaries[name].Observe(k, float64(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *metricsSet) Describe(c chan<- *prometheus.Desc) {
|
||||||
|
for _, g := range s.gauges {
|
||||||
|
g.Describe(c)
|
||||||
|
}
|
||||||
|
for _, g := range s.gaugeVecs {
|
||||||
|
g.Describe(c)
|
||||||
|
}
|
||||||
|
for _, g := range s.summaries {
|
||||||
|
g.Describe(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *metricsSet) Collect(c chan<- prometheus.Metric) {
|
||||||
|
s.collectMut.Lock()
|
||||||
|
defer s.collectMut.Unlock()
|
||||||
|
|
||||||
|
t0 := time.Now()
|
||||||
|
defer func() {
|
||||||
|
dur := time.Since(t0).Seconds()
|
||||||
|
metricsCollectSecondsLast.Set(dur)
|
||||||
|
metricsCollectSecondsTotal.Add(dur)
|
||||||
|
metricsCollectsTotal.Inc()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, g := range s.gauges {
|
||||||
|
g.Set(0)
|
||||||
|
}
|
||||||
|
for _, g := range s.gaugeVecs {
|
||||||
|
g.Reset()
|
||||||
|
}
|
||||||
|
for _, g := range s.summaries {
|
||||||
|
g.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
cutoff := time.Now().Add(s.collectCutoff)
|
||||||
|
s.srv.reports.Range(func(key string, r *contract.Report) bool {
|
||||||
|
if s.collectCutoff < 0 && r.Received.Before(cutoff) {
|
||||||
|
s.srv.reports.Delete(key)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
s.addReport(r)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, g := range s.gauges {
|
||||||
|
c <- g
|
||||||
|
}
|
||||||
|
for _, g := range s.gaugeVecs {
|
||||||
|
g.Collect(c)
|
||||||
|
}
|
||||||
|
for _, g := range s.summaries {
|
||||||
|
g.Collect(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type metricSummary struct {
|
||||||
|
name string
|
||||||
|
values map[string][]float64
|
||||||
|
zeroes map[string]int
|
||||||
|
|
||||||
|
qDesc *prometheus.Desc
|
||||||
|
countDesc *prometheus.Desc
|
||||||
|
sumDesc *prometheus.Desc
|
||||||
|
zDesc *prometheus.Desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMetricSummary(name string, labels []string, constLabels prometheus.Labels) *metricSummary {
|
||||||
|
return &metricSummary{
|
||||||
|
name: name,
|
||||||
|
values: make(map[string][]float64),
|
||||||
|
zeroes: make(map[string]int),
|
||||||
|
qDesc: prometheus.NewDesc(name, "", append(labels, "quantile"), constLabels),
|
||||||
|
countDesc: prometheus.NewDesc(name+"_nonzero_count", "", labels, constLabels),
|
||||||
|
sumDesc: prometheus.NewDesc(name+"_sum", "", labels, constLabels),
|
||||||
|
zDesc: prometheus.NewDesc(name+"_zero_count", "", labels, constLabels),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *metricSummary) Observe(labelValue string, v float64) {
|
||||||
|
if v == 0 {
|
||||||
|
q.zeroes[labelValue]++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q.values[labelValue] = append(q.values[labelValue], v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *metricSummary) Describe(c chan<- *prometheus.Desc) {
|
||||||
|
c <- q.qDesc
|
||||||
|
c <- q.countDesc
|
||||||
|
c <- q.sumDesc
|
||||||
|
c <- q.zDesc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *metricSummary) Collect(c chan<- prometheus.Metric) {
|
||||||
|
for lv, vs := range q.values {
|
||||||
|
var labelVals []string
|
||||||
|
if lv != "" {
|
||||||
|
labelVals = []string{lv}
|
||||||
|
}
|
||||||
|
|
||||||
|
c <- prometheus.MustNewConstMetric(q.countDesc, prometheus.GaugeValue, float64(len(vs)), labelVals...)
|
||||||
|
c <- prometheus.MustNewConstMetric(q.zDesc, prometheus.GaugeValue, float64(q.zeroes[lv]), labelVals...)
|
||||||
|
|
||||||
|
var sum float64
|
||||||
|
for _, v := range vs {
|
||||||
|
sum += v
|
||||||
|
}
|
||||||
|
c <- prometheus.MustNewConstMetric(q.sumDesc, prometheus.GaugeValue, sum, labelVals...)
|
||||||
|
|
||||||
|
if len(vs) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.Sort(vs)
|
||||||
|
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[0], append(labelVals, "0")...)
|
||||||
|
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)*5/100], append(labelVals, "0.05")...)
|
||||||
|
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)/2], append(labelVals, "0.5")...)
|
||||||
|
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)*9/10], append(labelVals, "0.9")...)
|
||||||
|
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)*95/100], append(labelVals, "0.95")...)
|
||||||
|
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)-1], append(labelVals, "1")...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *metricSummary) Reset() {
|
||||||
|
clear(q.values)
|
||||||
|
clear(q.zeroes)
|
||||||
|
}
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
// Copyright (C) 2018 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package serve
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
_ "net/http/pprof"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
|
"github.com/syncthing/syncthing/internal/blob"
|
||||||
|
"github.com/syncthing/syncthing/internal/blob/azureblob"
|
||||||
|
"github.com/syncthing/syncthing/internal/blob/s3"
|
||||||
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
|
"github.com/syncthing/syncthing/lib/geoip"
|
||||||
|
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CLI struct {
|
||||||
|
Listen string `env:"UR_LISTEN" help:"Usage reporting & metrics endpoint listen address" default:"0.0.0.0:8080"`
|
||||||
|
ListenInternal string `env:"UR_LISTEN_INTERNAL" help:"Internal metrics endpoint listen address" default:"0.0.0.0:8082"`
|
||||||
|
GeoIPLicenseKey string `env:"UR_GEOIP_LICENSE_KEY"`
|
||||||
|
GeoIPAccountID int `env:"UR_GEOIP_ACCOUNT_ID"`
|
||||||
|
DumpFile string `env:"UR_DUMP_FILE" default:"reports.jsons.gz"`
|
||||||
|
DumpInterval time.Duration `env:"UR_DUMP_INTERVAL" default:"5m"`
|
||||||
|
|
||||||
|
S3Endpoint string `name:"s3-endpoint" env:"UR_S3_ENDPOINT"`
|
||||||
|
S3Region string `name:"s3-region" env:"UR_S3_REGION"`
|
||||||
|
S3Bucket string `name:"s3-bucket" env:"UR_S3_BUCKET"`
|
||||||
|
S3AccessKeyID string `name:"s3-access-key-id" env:"UR_S3_ACCESS_KEY_ID"`
|
||||||
|
S3SecretKey string `name:"s3-secret-key" env:"UR_S3_SECRET_KEY"`
|
||||||
|
|
||||||
|
AzureBlobAccount string `name:"azure-blob-account" env:"UR_AZUREBLOB_ACCOUNT"`
|
||||||
|
AzureBlobKey string `name:"azure-blob-key" env:"UR_AZUREBLOB_KEY"`
|
||||||
|
AzureBlobContainer string `name:"azure-blob-container" env:"UR_AZUREBLOB_CONTAINER"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
compilerRe = regexp.MustCompile(`\(([A-Za-z0-9()., -]+) \w+-\w+(?:| android| default)\) ([\w@.-]+)`)
|
||||||
|
knownDistributions = []distributionMatch{
|
||||||
|
// Maps well known builders to the official distribution method that
|
||||||
|
// they represent
|
||||||
|
|
||||||
|
{regexp.MustCompile(`\steamcity@build\.syncthing\.net`), "GitHub"},
|
||||||
|
{regexp.MustCompile(`\sjenkins@build\.syncthing\.net`), "GitHub"},
|
||||||
|
{regexp.MustCompile(`\sbuilder@github\.syncthing\.net`), "GitHub"},
|
||||||
|
|
||||||
|
{regexp.MustCompile(`\sdeb@build\.syncthing\.net`), "APT"},
|
||||||
|
{regexp.MustCompile(`\sdebian@github\.syncthing\.net`), "APT"},
|
||||||
|
|
||||||
|
{regexp.MustCompile(`\sdocker@syncthing\.net`), "Docker Hub"},
|
||||||
|
{regexp.MustCompile(`\sdocker@build.syncthing\.net`), "Docker Hub"},
|
||||||
|
{regexp.MustCompile(`\sdocker@github.syncthing\.net`), "Docker Hub"},
|
||||||
|
|
||||||
|
{regexp.MustCompile(`\sandroid-builder@github\.syncthing\.net`), "Google Play"},
|
||||||
|
{regexp.MustCompile(`\sandroid-.*teamcity@build\.syncthing\.net`), "Google Play"},
|
||||||
|
|
||||||
|
{regexp.MustCompile(`\sandroid-.*vagrant@basebox-stretch64`), "F-Droid"},
|
||||||
|
{regexp.MustCompile(`\svagrant@bullseye`), "F-Droid"},
|
||||||
|
{regexp.MustCompile(`\svagrant@bookworm`), "F-Droid"},
|
||||||
|
|
||||||
|
{regexp.MustCompile(`Anwender@NET2017`), "Syncthing-Fork (3rd party)"},
|
||||||
|
|
||||||
|
{regexp.MustCompile(`\sbuilduser@(archlinux|svetlemodry)`), "Arch (3rd party)"},
|
||||||
|
{regexp.MustCompile(`\ssyncthing@archlinux`), "Arch (3rd party)"},
|
||||||
|
{regexp.MustCompile(`@debian`), "Debian (3rd party)"},
|
||||||
|
{regexp.MustCompile(`@fedora`), "Fedora (3rd party)"},
|
||||||
|
{regexp.MustCompile(`@openSUSE`), "openSUSE (3rd party)"},
|
||||||
|
{regexp.MustCompile(`\sbrew@`), "Homebrew (3rd party)"},
|
||||||
|
{regexp.MustCompile(`\sroot@buildkitsandbox`), "LinuxServer.io (3rd party)"},
|
||||||
|
{regexp.MustCompile(`\sports@freebsd`), "FreeBSD (3rd party)"},
|
||||||
|
{regexp.MustCompile(`\snix@nix`), "Nix (3rd party)"},
|
||||||
|
{regexp.MustCompile(`.`), "Others"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type distributionMatch struct {
|
||||||
|
matcher *regexp.Regexp
|
||||||
|
distribution string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CLI) Run() error {
|
||||||
|
slog.Info("Starting", "version", build.Version)
|
||||||
|
|
||||||
|
// Listening
|
||||||
|
|
||||||
|
urListener, err := net.Listen("tcp", cli.Listen)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to listen (usage reports)", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Info("Listening (usage reports)", "address", urListener.Addr())
|
||||||
|
|
||||||
|
internalListener, err := net.Listen("tcp", cli.ListenInternal)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to listen (internal)", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Info("Listening (internal)", "address", internalListener.Addr())
|
||||||
|
|
||||||
|
var geo *geoip.Provider
|
||||||
|
if cli.GeoIPAccountID != 0 && cli.GeoIPLicenseKey != "" {
|
||||||
|
geo, err = geoip.NewGeoLite2CityProvider(context.Background(), cli.GeoIPAccountID, cli.GeoIPLicenseKey, os.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to load GeoIP", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go geo.Serve(context.TODO())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blob storage
|
||||||
|
|
||||||
|
var blobs blob.Store
|
||||||
|
if cli.S3Endpoint != "" {
|
||||||
|
blobs, err = s3.NewSession(cli.S3Endpoint, cli.S3Region, cli.S3Bucket, cli.S3AccessKeyID, cli.S3SecretKey)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to create S3 session", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if cli.AzureBlobAccount != "" {
|
||||||
|
blobs, err = azureblob.NewBlobStore(cli.AzureBlobAccount, cli.AzureBlobKey, cli.AzureBlobContainer)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to create Azure blob store", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(cli.DumpFile); err != nil && blobs != nil {
|
||||||
|
if err := cli.downloadDumpFile(blobs); err != nil {
|
||||||
|
slog.Error("Failed to download dump file", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// server
|
||||||
|
|
||||||
|
srv := &server{
|
||||||
|
geo: geo,
|
||||||
|
reports: xsync.NewMapOf[string, *contract.Report](),
|
||||||
|
}
|
||||||
|
|
||||||
|
if fd, err := os.Open(cli.DumpFile); err == nil {
|
||||||
|
gr, err := gzip.NewReader(fd)
|
||||||
|
if err == nil {
|
||||||
|
srv.load(gr)
|
||||||
|
}
|
||||||
|
fd.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for range time.Tick(cli.DumpInterval) {
|
||||||
|
if err := cli.saveDumpFile(srv, blobs); err != nil {
|
||||||
|
slog.Error("Failed to write dump file", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// The internal metrics endpoint just serves metrics about what the
|
||||||
|
// server is doing.
|
||||||
|
|
||||||
|
http.Handle("/metrics", promhttp.Handler())
|
||||||
|
|
||||||
|
internalSrv := http.Server{
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 15 * time.Second,
|
||||||
|
}
|
||||||
|
go internalSrv.Serve(internalListener)
|
||||||
|
|
||||||
|
// New external metrics endpoint accepts reports from clients and serves
|
||||||
|
// aggregated usage reporting metrics.
|
||||||
|
|
||||||
|
ms := newMetricsSet(srv)
|
||||||
|
reg := prometheus.NewRegistry()
|
||||||
|
reg.MustRegister(ms)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
|
||||||
|
mux.HandleFunc("/newdata", srv.handleNewData)
|
||||||
|
mux.HandleFunc("/ping", srv.handlePing)
|
||||||
|
|
||||||
|
metricsSrv := http.Server{
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 15 * time.Second,
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Ready to serve")
|
||||||
|
return metricsSrv.Serve(urListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CLI) downloadDumpFile(blobs blob.Store) error {
|
||||||
|
latestKey, err := blobs.LatestKey(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list latest S3 key: %w", err)
|
||||||
|
}
|
||||||
|
fd, err := os.Create(cli.DumpFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create dump file: %w", err)
|
||||||
|
}
|
||||||
|
if err := blobs.Download(context.Background(), latestKey, fd); err != nil {
|
||||||
|
_ = fd.Close()
|
||||||
|
return fmt.Errorf("download dump file: %w", err)
|
||||||
|
}
|
||||||
|
if err := fd.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close dump file: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("Dump file downloaded", "key", latestKey)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CLI) saveDumpFile(srv *server, blobs blob.Store) error {
|
||||||
|
fd, err := os.Create(cli.DumpFile + ".tmp")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating dump file: %w", err)
|
||||||
|
}
|
||||||
|
gw := gzip.NewWriter(fd)
|
||||||
|
if err := srv.save(gw); err != nil {
|
||||||
|
return fmt.Errorf("saving dump file: %w", err)
|
||||||
|
}
|
||||||
|
if err := gw.Close(); err != nil {
|
||||||
|
fd.Close()
|
||||||
|
return fmt.Errorf("closing gzip writer: %w", err)
|
||||||
|
}
|
||||||
|
if err := fd.Close(); err != nil {
|
||||||
|
return fmt.Errorf("closing dump file: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.Rename(cli.DumpFile+".tmp", cli.DumpFile); err != nil {
|
||||||
|
return fmt.Errorf("renaming dump file: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("Dump file saved")
|
||||||
|
|
||||||
|
if blobs != nil {
|
||||||
|
key := fmt.Sprintf("reports-%s.jsons.gz", time.Now().UTC().Format("2006-01-02"))
|
||||||
|
fd, err := os.Open(cli.DumpFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("opening dump file: %w", err)
|
||||||
|
}
|
||||||
|
if err := blobs.Upload(context.Background(), key, fd); err != nil {
|
||||||
|
return fmt.Errorf("uploading dump file: %w", err)
|
||||||
|
}
|
||||||
|
_ = fd.Close()
|
||||||
|
slog.Info("Dump file uploaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
geo *geoip.Provider
|
||||||
|
reports *xsync.MapOf[string, *contract.Report]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handlePing(w http.ResponseWriter, r *http.Request) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) handleNewData(w http.ResponseWriter, r *http.Request) {
|
||||||
|
result := "fail"
|
||||||
|
defer func() {
|
||||||
|
// result is "accept" (new report), "replace" (existing report) or
|
||||||
|
// "fail"
|
||||||
|
metricReportsTotal.WithLabelValues(result).Inc()
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := r.Header.Get("X-Forwarded-For")
|
||||||
|
if addr != "" {
|
||||||
|
addr = strings.Split(addr, ", ")[0]
|
||||||
|
} else {
|
||||||
|
addr = r.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
if host, _, err := net.SplitHostPort(addr); err == nil {
|
||||||
|
addr = host
|
||||||
|
}
|
||||||
|
|
||||||
|
log := slog.With("addr", addr)
|
||||||
|
|
||||||
|
if net.ParseIP(addr) == nil {
|
||||||
|
addr = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var rep contract.Report
|
||||||
|
|
||||||
|
lr := &io.LimitedReader{R: r.Body, N: 40 * 1024}
|
||||||
|
bs, _ := io.ReadAll(lr)
|
||||||
|
if err := json.Unmarshal(bs, &rep); err != nil {
|
||||||
|
log.Error("Failed to decode JSON", "error", err)
|
||||||
|
http.Error(w, "JSON Decode Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rep.Received = time.Now()
|
||||||
|
rep.Date = rep.Received.UTC().Format("20060102")
|
||||||
|
rep.Address = addr
|
||||||
|
|
||||||
|
if err := rep.Validate(); err != nil {
|
||||||
|
log.Error("Failed to validate report", "error", err)
|
||||||
|
http.Error(w, "Validation Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.addReport(&rep) {
|
||||||
|
result = "replace"
|
||||||
|
} else {
|
||||||
|
result = "accept"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) addReport(rep *contract.Report) bool {
|
||||||
|
if s.geo != nil {
|
||||||
|
if ip := net.ParseIP(rep.Address); ip != nil {
|
||||||
|
if city, err := s.geo.City(ip); err == nil {
|
||||||
|
rep.Country = city.Country.Names["en"]
|
||||||
|
rep.CountryCode = city.Country.IsoCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rep.Country == "" {
|
||||||
|
rep.Country = "Unknown"
|
||||||
|
}
|
||||||
|
if rep.CountryCode == "" {
|
||||||
|
rep.CountryCode = "ZZ"
|
||||||
|
}
|
||||||
|
|
||||||
|
rep.Version = transformVersion(rep.Version)
|
||||||
|
if strings.Contains(rep.Version, ".") {
|
||||||
|
split := strings.SplitN(rep.Version, ".", 3)
|
||||||
|
if len(split) == 3 {
|
||||||
|
rep.MajorVersion = strings.Join(split[:2], ".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rep.OS, rep.Arch, _ = strings.Cut(rep.Platform, "-")
|
||||||
|
|
||||||
|
if m := compilerRe.FindStringSubmatch(rep.LongVersion); len(m) == 3 {
|
||||||
|
rep.Compiler = m[1]
|
||||||
|
rep.Builder = m[2]
|
||||||
|
}
|
||||||
|
for _, d := range knownDistributions {
|
||||||
|
if d.matcher.MatchString(rep.LongVersion) {
|
||||||
|
rep.Distribution = d.distribution
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rep.DistDist = rep.Distribution
|
||||||
|
rep.DistOS = rep.OS
|
||||||
|
rep.DistArch = rep.Arch
|
||||||
|
|
||||||
|
_, loaded := s.reports.LoadAndStore(rep.UniqueID, rep)
|
||||||
|
return loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) save(w io.Writer) error {
|
||||||
|
bw := bufio.NewWriter(w)
|
||||||
|
enc := json.NewEncoder(bw)
|
||||||
|
var err error
|
||||||
|
s.reports.Range(func(k string, v *contract.Report) bool {
|
||||||
|
err = enc.Encode(v)
|
||||||
|
return err == nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bw.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) load(r io.Reader) {
|
||||||
|
dec := json.NewDecoder(r)
|
||||||
|
s.reports.Clear()
|
||||||
|
for {
|
||||||
|
var rep contract.Report
|
||||||
|
if err := dec.Decode(&rep); errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
slog.Error("Failed to load record", "error", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s.addReport(&rep)
|
||||||
|
}
|
||||||
|
slog.Info("Loaded reports", "count", s.reports.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
plusRe = regexp.MustCompile(`(\+.*|[.-]dev\..*)$`)
|
||||||
|
plusStr = "-dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
// transformVersion returns a version number formatted correctly, with all
|
||||||
|
// development versions aggregated into one.
|
||||||
|
func transformVersion(v string) string {
|
||||||
|
if v == "unknown-dev" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(v, "v") {
|
||||||
|
v = "v" + v
|
||||||
|
}
|
||||||
|
v = plusRe.ReplaceAllString(v, plusStr)
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
// Copyright (C) 2024 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
amqp "github.com/rabbitmq/amqp091-go"
|
||||||
|
"github.com/thejerf/suture/v4"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/internal/gen/discosrv"
|
||||||
|
"github.com/syncthing/syncthing/internal/protoutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
type amqpReplicator struct {
|
||||||
|
suture.Service
|
||||||
|
broker string
|
||||||
|
sender *amqpSender
|
||||||
|
receiver *amqpReceiver
|
||||||
|
outbox chan *discosrv.ReplicationRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAMQPReplicator(broker, clientID string, db database) *amqpReplicator {
|
||||||
|
svc := suture.New("amqpReplicator", suture.Spec{PassThroughPanics: true})
|
||||||
|
|
||||||
|
sender := &amqpSender{
|
||||||
|
broker: broker,
|
||||||
|
clientID: clientID,
|
||||||
|
outbox: make(chan *discosrv.ReplicationRecord, replicationOutboxSize),
|
||||||
|
}
|
||||||
|
svc.Add(sender)
|
||||||
|
|
||||||
|
receiver := &amqpReceiver{
|
||||||
|
broker: broker,
|
||||||
|
clientID: clientID,
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
svc.Add(receiver)
|
||||||
|
|
||||||
|
return &amqpReplicator{
|
||||||
|
Service: svc,
|
||||||
|
broker: broker,
|
||||||
|
sender: sender,
|
||||||
|
receiver: receiver,
|
||||||
|
outbox: make(chan *discosrv.ReplicationRecord, replicationOutboxSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *amqpReplicator) send(key *protocol.DeviceID, ps []*discosrv.DatabaseAddress, seen int64) {
|
||||||
|
s.sender.send(key, ps, seen)
|
||||||
|
}
|
||||||
|
|
||||||
|
type amqpSender struct {
|
||||||
|
broker string
|
||||||
|
clientID string
|
||||||
|
outbox chan *discosrv.ReplicationRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *amqpSender) Serve(ctx context.Context) error {
|
||||||
|
conn, ch, err := amqpChannel(s.broker)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer ch.Close()
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case rec := <-s.outbox:
|
||||||
|
size := proto.Size(rec)
|
||||||
|
if len(buf) < size {
|
||||||
|
buf = make([]byte, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := protoutil.MarshalTo(buf, rec)
|
||||||
|
if err != nil {
|
||||||
|
replicationSendsTotal.WithLabelValues("error").Inc()
|
||||||
|
return fmt.Errorf("replication marshal: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ch.PublishWithContext(ctx,
|
||||||
|
"discovery", // exchange
|
||||||
|
"", // routing key
|
||||||
|
false, // mandatory
|
||||||
|
false, // immediate
|
||||||
|
amqp.Publishing{
|
||||||
|
ContentType: "application/protobuf",
|
||||||
|
Body: buf[:n],
|
||||||
|
AppId: s.clientID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
replicationSendsTotal.WithLabelValues("error").Inc()
|
||||||
|
return fmt.Errorf("replication publish: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
replicationSendsTotal.WithLabelValues("success").Inc()
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *amqpSender) String() string {
|
||||||
|
return fmt.Sprintf("amqpSender(%q)", s.broker)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *amqpSender) send(key *protocol.DeviceID, ps []*discosrv.DatabaseAddress, seen int64) {
|
||||||
|
item := &discosrv.ReplicationRecord{
|
||||||
|
Key: key[:],
|
||||||
|
Addresses: ps,
|
||||||
|
Seen: seen,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The send should never block. The inbox is suitably buffered for at
|
||||||
|
// least a few seconds of stalls, which shouldn't happen in practice.
|
||||||
|
select {
|
||||||
|
case s.outbox <- item:
|
||||||
|
default:
|
||||||
|
replicationSendsTotal.WithLabelValues("drop").Inc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type amqpReceiver struct {
|
||||||
|
broker string
|
||||||
|
clientID string
|
||||||
|
db database
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *amqpReceiver) Serve(ctx context.Context) error {
|
||||||
|
conn, ch, err := amqpChannel(s.broker)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer ch.Close()
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
msgs, err := amqpConsume(ch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg, ok := <-msgs:
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("subscription closed: %w", io.EOF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore messages from ourself
|
||||||
|
if msg.AppId == s.clientID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var rec discosrv.ReplicationRecord
|
||||||
|
if err := proto.Unmarshal(msg.Body, &rec); err != nil {
|
||||||
|
replicationRecvsTotal.WithLabelValues("error").Inc()
|
||||||
|
return fmt.Errorf("replication unmarshal: %w", err)
|
||||||
|
}
|
||||||
|
id, err := protocol.DeviceIDFromBytes(rec.Key)
|
||||||
|
if err != nil {
|
||||||
|
id, err = protocol.DeviceIDFromString(string(rec.Key))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Replication device ID:", err)
|
||||||
|
replicationRecvsTotal.WithLabelValues("error").Inc()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.db.merge(&id, rec.Addresses, rec.Seen); err != nil {
|
||||||
|
return fmt.Errorf("replication database merge: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
replicationRecvsTotal.WithLabelValues("success").Inc()
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *amqpReceiver) String() string {
|
||||||
|
return fmt.Sprintf("amqpReceiver(%q)", s.broker)
|
||||||
|
}
|
||||||
|
|
||||||
|
func amqpChannel(dst string) (*amqp.Connection, *amqp.Channel, error) {
|
||||||
|
conn, err := amqp.Dial(dst)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("AMQP dial: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch, err := conn.Channel()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("AMQP channel: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ch.ExchangeDeclare(
|
||||||
|
"discovery", // name
|
||||||
|
"fanout", // type
|
||||||
|
false, // durable
|
||||||
|
false, // auto-deleted
|
||||||
|
false, // internal
|
||||||
|
false, // no-wait
|
||||||
|
nil, // arguments
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("AMQP declare exchange: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func amqpConsume(ch *amqp.Channel) (<-chan amqp.Delivery, error) {
|
||||||
|
q, err := ch.QueueDeclare(
|
||||||
|
"", // name
|
||||||
|
false, // durable
|
||||||
|
false, // delete when unused
|
||||||
|
true, // exclusive
|
||||||
|
false, // no-wait
|
||||||
|
nil, // arguments
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("AMQP declare queue: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ch.QueueBind(
|
||||||
|
q.Name, // queue name
|
||||||
|
"", // routing key
|
||||||
|
"discovery", // exchange
|
||||||
|
false,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("AMQP bind queue: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs, err := ch.Consume(
|
||||||
|
q.Name, // queue
|
||||||
|
"", // consumer
|
||||||
|
true, // auto-ack
|
||||||
|
false, // exclusive
|
||||||
|
false, // no-local
|
||||||
|
false, // no-wait
|
||||||
|
nil, // args
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("AMQP consume: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return msgs, nil
|
||||||
|
}
|
||||||
+206
-107
@@ -8,6 +8,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
@@ -15,18 +16,21 @@ import (
|
|||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
io "io"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/internal/gen/discosrv"
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/stringutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// announcement is the format received from and sent to clients
|
// announcement is the format received from and sent to clients
|
||||||
@@ -36,15 +40,20 @@ type announcement struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type apiSrv struct {
|
type apiSrv struct {
|
||||||
addr string
|
addr string
|
||||||
cert tls.Certificate
|
cert tls.Certificate
|
||||||
db database
|
db database
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
repl replicator // optional
|
repl replicator // optional
|
||||||
useHTTP bool
|
useHTTP bool
|
||||||
|
compression bool
|
||||||
|
gzipWriters sync.Pool
|
||||||
|
seenTracker *retryAfterTracker
|
||||||
|
notSeenTracker *retryAfterTracker
|
||||||
|
}
|
||||||
|
|
||||||
mapsMut sync.Mutex
|
type replicator interface {
|
||||||
misses map[string]int32
|
send(key *protocol.DeviceID, addrs []*discosrv.DatabaseAddress, seen int64)
|
||||||
}
|
}
|
||||||
|
|
||||||
type requestID int64
|
type requestID int64
|
||||||
@@ -57,18 +66,30 @@ type contextKey int
|
|||||||
|
|
||||||
const idKey contextKey = iota
|
const idKey contextKey = iota
|
||||||
|
|
||||||
func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator, useHTTP bool) *apiSrv {
|
func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator, useHTTP, compression bool, desiredNotFoundRate float64) *apiSrv {
|
||||||
return &apiSrv{
|
return &apiSrv{
|
||||||
addr: addr,
|
addr: addr,
|
||||||
cert: cert,
|
cert: cert,
|
||||||
db: db,
|
db: db,
|
||||||
repl: repl,
|
repl: repl,
|
||||||
useHTTP: useHTTP,
|
useHTTP: useHTTP,
|
||||||
misses: make(map[string]int32),
|
compression: compression,
|
||||||
|
seenTracker: &retryAfterTracker{
|
||||||
|
name: "seenTracker",
|
||||||
|
bucketStarts: time.Now(),
|
||||||
|
desiredRate: desiredNotFoundRate / 2,
|
||||||
|
currentDelay: notFoundRetryUnknownMinSeconds,
|
||||||
|
},
|
||||||
|
notSeenTracker: &retryAfterTracker{
|
||||||
|
name: "notSeenTracker",
|
||||||
|
bucketStarts: time.Now(),
|
||||||
|
desiredRate: desiredNotFoundRate / 2,
|
||||||
|
currentDelay: notFoundRetryUnknownMaxSeconds / 2,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSrv) Serve(_ context.Context) error {
|
func (s *apiSrv) Serve(ctx context.Context) error {
|
||||||
if s.useHTTP {
|
if s.useHTTP {
|
||||||
listener, err := net.Listen("tcp", s.addr)
|
listener, err := net.Listen("tcp", s.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -78,18 +99,10 @@ func (s *apiSrv) Serve(_ context.Context) error {
|
|||||||
s.listener = listener
|
s.listener = listener
|
||||||
} else {
|
} else {
|
||||||
tlsCfg := &tls.Config{
|
tlsCfg := &tls.Config{
|
||||||
Certificates: []tls.Certificate{s.cert},
|
Certificates: []tls.Certificate{s.cert},
|
||||||
ClientAuth: tls.RequestClientCert,
|
ClientAuth: tls.RequestClientCert,
|
||||||
SessionTicketsDisabled: true,
|
MinVersion: tls.VersionTLS12,
|
||||||
MinVersion: tls.VersionTLS12,
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
CipherSuites: []uint16{
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsListener, err := tls.Listen("tcp", s.addr, tlsCfg)
|
tlsListener, err := tls.Listen("tcp", s.addr, tlsCfg)
|
||||||
@@ -108,6 +121,14 @@ func (s *apiSrv) Serve(_ context.Context) error {
|
|||||||
WriteTimeout: httpWriteTimeout,
|
WriteTimeout: httpWriteTimeout,
|
||||||
MaxHeaderBytes: httpMaxHeaderBytes,
|
MaxHeaderBytes: httpMaxHeaderBytes,
|
||||||
}
|
}
|
||||||
|
if !debug {
|
||||||
|
srv.ErrorLog = log.New(io.Discard, "", 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
srv.Shutdown(context.Background())
|
||||||
|
}()
|
||||||
|
|
||||||
err := srv.Serve(s.listener)
|
err := srv.Serve(s.listener)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,8 +137,6 @@ func (s *apiSrv) Serve(_ context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var topCtx = context.Background()
|
|
||||||
|
|
||||||
func (s *apiSrv) handler(w http.ResponseWriter, req *http.Request) {
|
func (s *apiSrv) handler(w http.ResponseWriter, req *http.Request) {
|
||||||
t0 := time.Now()
|
t0 := time.Now()
|
||||||
|
|
||||||
@@ -130,10 +149,10 @@ func (s *apiSrv) handler(w http.ResponseWriter, req *http.Request) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
reqID := requestID(rand.Int63())
|
reqID := requestID(rand.Int63())
|
||||||
ctx := context.WithValue(topCtx, idKey, reqID)
|
req = req.WithContext(context.WithValue(req.Context(), idKey, reqID))
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
log.Println(reqID, req.Method, req.URL)
|
log.Println(reqID, req.Method, req.URL, req.Proto)
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteAddr := &net.TCPAddr{
|
remoteAddr := &net.TCPAddr{
|
||||||
@@ -142,7 +161,12 @@ func (s *apiSrv) handler(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.useHTTP {
|
if s.useHTTP {
|
||||||
remoteAddr.IP = net.ParseIP(req.Header.Get("X-Forwarded-For"))
|
// X-Forwarded-For can have multiple client IPs; split using the comma separator
|
||||||
|
forwardIP, _, _ := strings.Cut(req.Header.Get("X-Forwarded-For"), ",")
|
||||||
|
|
||||||
|
// net.ParseIP will return nil if leading/trailing whitespace exists; use strings.TrimSpace()
|
||||||
|
remoteAddr.IP = net.ParseIP(strings.TrimSpace(forwardIP))
|
||||||
|
|
||||||
if parsedPort, err := strconv.ParseInt(req.Header.Get("X-Client-Port"), 10, 0); err == nil {
|
if parsedPort, err := strconv.ParseInt(req.Header.Get("X-Client-Port"), 10, 0); err == nil {
|
||||||
remoteAddr.Port = int(parsedPort)
|
remoteAddr.Port = int(parsedPort)
|
||||||
}
|
}
|
||||||
@@ -159,22 +183,22 @@ func (s *apiSrv) handler(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
case "GET":
|
case http.MethodGet:
|
||||||
s.handleGET(ctx, lw, req)
|
s.handleGET(lw, req)
|
||||||
case "POST":
|
case http.MethodPost:
|
||||||
s.handlePOST(ctx, remoteAddr, lw, req)
|
s.handlePOST(remoteAddr, lw, req)
|
||||||
default:
|
default:
|
||||||
http.Error(lw, "Method Not Allowed", http.StatusMethodNotAllowed)
|
http.Error(lw, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSrv) handleGET(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
func (s *apiSrv) handleGET(w http.ResponseWriter, req *http.Request) {
|
||||||
reqID := ctx.Value(idKey).(requestID)
|
reqID := req.Context().Value(idKey).(requestID)
|
||||||
|
|
||||||
deviceID, err := protocol.DeviceIDFromString(req.URL.Query().Get("device"))
|
deviceID, err := protocol.DeviceIDFromString(req.URL.Query().Get("device"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if debug {
|
if debug {
|
||||||
log.Println(reqID, "bad device param")
|
log.Println(reqID, "bad device param:", err)
|
||||||
}
|
}
|
||||||
lookupRequestsTotal.WithLabelValues("bad_request").Inc()
|
lookupRequestsTotal.WithLabelValues("bad_request").Inc()
|
||||||
w.Header().Set("Retry-After", errorRetryAfterString())
|
w.Header().Set("Retry-After", errorRetryAfterString())
|
||||||
@@ -182,8 +206,7 @@ func (s *apiSrv) handleGET(ctx context.Context, w http.ResponseWriter, req *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
key := deviceID.String()
|
rec, err := s.db.get(&deviceID)
|
||||||
rec, err := s.db.get(key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// some sort of internal error
|
// some sort of internal error
|
||||||
lookupRequestsTotal.WithLabelValues("internal_error").Inc()
|
lookupRequestsTotal.WithLabelValues("internal_error").Inc()
|
||||||
@@ -193,43 +216,46 @@ func (s *apiSrv) handleGET(ctx context.Context, w http.ResponseWriter, req *http
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(rec.Addresses) == 0 {
|
if len(rec.Addresses) == 0 {
|
||||||
lookupRequestsTotal.WithLabelValues("not_found").Inc()
|
var afterS int
|
||||||
|
if rec.Seen == 0 {
|
||||||
s.mapsMut.Lock()
|
afterS = s.notSeenTracker.retryAfterS()
|
||||||
misses := s.misses[key]
|
lookupRequestsTotal.WithLabelValues("not_found_ever").Inc()
|
||||||
if misses < rec.Misses {
|
|
||||||
misses = rec.Misses + 1
|
|
||||||
} else {
|
} else {
|
||||||
misses++
|
afterS = s.seenTracker.retryAfterS()
|
||||||
|
lookupRequestsTotal.WithLabelValues("not_found_recent").Inc()
|
||||||
}
|
}
|
||||||
s.misses[key] = misses
|
w.Header().Set("Retry-After", strconv.Itoa(afterS))
|
||||||
s.mapsMut.Unlock()
|
|
||||||
|
|
||||||
if misses%notFoundMissesWriteInterval == 0 {
|
|
||||||
rec.Misses = misses
|
|
||||||
rec.Missed = time.Now().UnixNano()
|
|
||||||
rec.Addresses = nil
|
|
||||||
// rec.Seen retained from get
|
|
||||||
s.db.put(key, rec)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Retry-After", notFoundRetryAfterString(int(misses)))
|
|
||||||
http.Error(w, "Not Found", http.StatusNotFound)
|
http.Error(w, "Not Found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lookupRequestsTotal.WithLabelValues("success").Inc()
|
lookupRequestsTotal.WithLabelValues("success").Inc()
|
||||||
|
|
||||||
bs, _ := json.Marshal(announcement{
|
w.Header().Set("Content-Type", "application/json")
|
||||||
Seen: time.Unix(0, rec.Seen),
|
var bw io.Writer = w
|
||||||
|
|
||||||
|
// Use compression if the client asks for it
|
||||||
|
if s.compression && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
gw, ok := s.gzipWriters.Get().(*gzip.Writer)
|
||||||
|
if ok {
|
||||||
|
gw.Reset(w)
|
||||||
|
} else {
|
||||||
|
gw = gzip.NewWriter(w)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
defer gw.Close()
|
||||||
|
defer s.gzipWriters.Put(gw)
|
||||||
|
bw = gw
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(bw).Encode(announcement{
|
||||||
|
Seen: time.Unix(0, rec.Seen).Truncate(time.Second),
|
||||||
Addresses: addressStrs(rec.Addresses),
|
Addresses: addressStrs(rec.Addresses),
|
||||||
})
|
})
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(bs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSrv) handlePOST(ctx context.Context, remoteAddr *net.TCPAddr, w http.ResponseWriter, req *http.Request) {
|
func (s *apiSrv) handlePOST(remoteAddr *net.TCPAddr, w http.ResponseWriter, req *http.Request) {
|
||||||
reqID := ctx.Value(idKey).(requestID)
|
reqID := req.Context().Value(idKey).(requestID)
|
||||||
|
|
||||||
rawCert, err := certificateBytes(req)
|
rawCert, err := certificateBytes(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -257,6 +283,9 @@ func (s *apiSrv) handlePOST(ctx context.Context, remoteAddr *net.TCPAddr, w http
|
|||||||
|
|
||||||
addresses := fixupAddresses(remoteAddr, ann.Addresses)
|
addresses := fixupAddresses(remoteAddr, ann.Addresses)
|
||||||
if len(addresses) == 0 {
|
if len(addresses) == 0 {
|
||||||
|
if debug {
|
||||||
|
log.Println(reqID, "no addresses")
|
||||||
|
}
|
||||||
announceRequestsTotal.WithLabelValues("bad_request").Inc()
|
announceRequestsTotal.WithLabelValues("bad_request").Inc()
|
||||||
w.Header().Set("Retry-After", errorRetryAfterString())
|
w.Header().Set("Retry-After", errorRetryAfterString())
|
||||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
@@ -264,6 +293,9 @@ func (s *apiSrv) handlePOST(ctx context.Context, remoteAddr *net.TCPAddr, w http
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.handleAnnounce(deviceID, addresses); err != nil {
|
if err := s.handleAnnounce(deviceID, addresses); err != nil {
|
||||||
|
if debug {
|
||||||
|
log.Println(reqID, "handle:", err)
|
||||||
|
}
|
||||||
announceRequestsTotal.WithLabelValues("internal_error").Inc()
|
announceRequestsTotal.WithLabelValues("internal_error").Inc()
|
||||||
w.Header().Set("Retry-After", errorRetryAfterString())
|
w.Header().Set("Retry-After", errorRetryAfterString())
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
@@ -274,6 +306,9 @@ func (s *apiSrv) handlePOST(ctx context.Context, remoteAddr *net.TCPAddr, w http
|
|||||||
|
|
||||||
w.Header().Set("Reannounce-After", reannounceAfterString())
|
w.Header().Set("Reannounce-After", reannounceAfterString())
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
if debug {
|
||||||
|
log.Println(reqID, "announced", deviceID, addresses)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSrv) Stop() {
|
func (s *apiSrv) Stop() {
|
||||||
@@ -281,29 +316,31 @@ func (s *apiSrv) Stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSrv) handleAnnounce(deviceID protocol.DeviceID, addresses []string) error {
|
func (s *apiSrv) handleAnnounce(deviceID protocol.DeviceID, addresses []string) error {
|
||||||
key := deviceID.String()
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
expire := now.Add(addressExpiryTime).UnixNano()
|
expire := now.Add(addressExpiryTime).UnixNano()
|
||||||
|
|
||||||
dbAddrs := make([]DatabaseAddress, len(addresses))
|
|
||||||
for i := range addresses {
|
|
||||||
dbAddrs[i].Address = addresses[i]
|
|
||||||
dbAddrs[i].Expires = expire
|
|
||||||
}
|
|
||||||
|
|
||||||
// The address slice must always be sorted for database merges to work
|
// The address slice must always be sorted for database merges to work
|
||||||
// properly.
|
// properly.
|
||||||
sort.Sort(databaseAddressOrder(dbAddrs))
|
slices.Sort(addresses)
|
||||||
|
addresses = slices.Compact(addresses)
|
||||||
|
|
||||||
|
dbAddrs := make([]*discosrv.DatabaseAddress, len(addresses))
|
||||||
|
for i := range addresses {
|
||||||
|
dbAddrs[i] = &discosrv.DatabaseAddress{
|
||||||
|
Address: addresses[i],
|
||||||
|
Expires: expire,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
seen := now.UnixNano()
|
seen := now.UnixNano()
|
||||||
if s.repl != nil {
|
if s.repl != nil {
|
||||||
s.repl.send(key, dbAddrs, seen)
|
s.repl.send(&deviceID, dbAddrs, seen)
|
||||||
}
|
}
|
||||||
return s.db.merge(key, dbAddrs, seen)
|
return s.db.merge(&deviceID, dbAddrs, seen)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePing(w http.ResponseWriter, _ *http.Request) {
|
func handlePing(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(204)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func certificateBytes(req *http.Request) ([]byte, error) {
|
func certificateBytes(req *http.Request) ([]byte, error) {
|
||||||
@@ -349,24 +386,44 @@ func certificateBytes(req *http.Request) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bs = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: hdr})
|
bs = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: hdr})
|
||||||
} else if hdr := req.Header.Get("X-Forwarded-Tls-Client-Cert"); hdr != "" {
|
} else if cert := req.Header.Get("X-Forwarded-Tls-Client-Cert"); cert != "" {
|
||||||
// Traefik 2 passtlsclientcert
|
// Traefik 2 passtlsclientcert
|
||||||
// The certificate is in PEM format with url encoding but without newlines
|
//
|
||||||
// and start/end statements. We need to decode, reinstate the newlines every 64
|
// The certificate is in PEM format, maybe with URL encoding
|
||||||
|
// (depends on Traefik version) but without newlines and start/end
|
||||||
|
// statements. We need to decode, reinstate the newlines every 64
|
||||||
// character and add statements for the PEM decoder
|
// character and add statements for the PEM decoder
|
||||||
hdr, err := url.QueryUnescape(hdr)
|
|
||||||
if err != nil {
|
if strings.Contains(cert, "%") {
|
||||||
// Decoding failed
|
if unesc, err := url.QueryUnescape(cert); err == nil {
|
||||||
return nil, err
|
cert = unesc
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 64; i < len(hdr); i += 65 {
|
const (
|
||||||
hdr = hdr[:i] + "\n" + hdr[i:]
|
header = "-----BEGIN CERTIFICATE-----"
|
||||||
|
footer = "-----END CERTIFICATE-----"
|
||||||
|
)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
b.Grow(len(header) + 1 + len(cert) + len(cert)/64 + 1 + len(footer) + 1)
|
||||||
|
|
||||||
|
b.WriteString(header)
|
||||||
|
b.WriteByte('\n')
|
||||||
|
|
||||||
|
for i := 0; i < len(cert); i += 64 {
|
||||||
|
end := i + 64
|
||||||
|
if end > len(cert) {
|
||||||
|
end = len(cert)
|
||||||
|
}
|
||||||
|
b.WriteString(cert[i:end])
|
||||||
|
b.WriteByte('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
hdr = "-----BEGIN CERTIFICATE-----\n" + hdr
|
b.WriteString(footer)
|
||||||
hdr = hdr + "\n-----END CERTIFICATE-----\n"
|
b.WriteByte('\n')
|
||||||
bs = []byte(hdr)
|
|
||||||
|
bs = b.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
if bs == nil {
|
if bs == nil {
|
||||||
@@ -404,13 +461,13 @@ func fixupAddresses(remote *net.TCPAddr, addresses []string) []string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if remote != nil {
|
if host == "" || ip.IsUnspecified() {
|
||||||
if host == "" || ip.IsUnspecified() {
|
if remote != nil {
|
||||||
// Replace the unspecified IP with the request source.
|
// Replace the unspecified IP with the request source.
|
||||||
|
|
||||||
// ... unless the request source is the loopback address or
|
// ... unless the request source is the loopback address or
|
||||||
// multicast/unspecified (can't happen, really).
|
// multicast/unspecified (can't happen, really).
|
||||||
if remote.IP.IsLoopback() || remote.IP.IsMulticast() || remote.IP.IsUnspecified() {
|
if remote.IP == nil || remote.IP.IsLoopback() || remote.IP.IsMulticast() || remote.IP.IsUnspecified() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,11 +483,21 @@ func fixupAddresses(remote *net.TCPAddr, addresses []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
host = remote.IP.String()
|
host = remote.IP.String()
|
||||||
}
|
|
||||||
|
|
||||||
// If zero port was specified, use remote port.
|
} else {
|
||||||
if port == "0" && remote.Port > 0 {
|
// remote is nil, unable to determine host IP
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If zero port was specified, use remote port.
|
||||||
|
if port == "0" {
|
||||||
|
if remote != nil && remote.Port > 0 {
|
||||||
|
// use remote port
|
||||||
port = strconv.Itoa(remote.Port)
|
port = strconv.Itoa(remote.Port)
|
||||||
|
} else {
|
||||||
|
// unable to determine remote port
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,6 +505,9 @@ func fixupAddresses(remote *net.TCPAddr, addresses []string) []string {
|
|||||||
fixed = append(fixed, uri.String())
|
fixed = append(fixed, uri.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove duplicate addresses
|
||||||
|
fixed = stringutil.UniqueTrimmedStrings(fixed)
|
||||||
|
|
||||||
return fixed
|
return fixed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,7 +525,7 @@ func (lrw *loggingResponseWriter) WriteHeader(code int) {
|
|||||||
lrw.ResponseWriter.WriteHeader(code)
|
lrw.ResponseWriter.WriteHeader(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addressStrs(dbAddrs []DatabaseAddress) []string {
|
func addressStrs(dbAddrs []*discosrv.DatabaseAddress) []string {
|
||||||
res := make([]string, len(dbAddrs))
|
res := make([]string, len(dbAddrs))
|
||||||
for i, a := range dbAddrs {
|
for i, a := range dbAddrs {
|
||||||
res[i] = a.Address
|
res[i] = a.Address
|
||||||
@@ -467,15 +537,44 @@ func errorRetryAfterString() string {
|
|||||||
return strconv.Itoa(errorRetryAfterSeconds + rand.Intn(errorRetryFuzzSeconds))
|
return strconv.Itoa(errorRetryAfterSeconds + rand.Intn(errorRetryFuzzSeconds))
|
||||||
}
|
}
|
||||||
|
|
||||||
func notFoundRetryAfterString(misses int) string {
|
|
||||||
retryAfterS := notFoundRetryMinSeconds + notFoundRetryIncSeconds*misses
|
|
||||||
if retryAfterS > notFoundRetryMaxSeconds {
|
|
||||||
retryAfterS = notFoundRetryMaxSeconds
|
|
||||||
}
|
|
||||||
retryAfterS += rand.Intn(notFoundRetryFuzzSeconds)
|
|
||||||
return strconv.Itoa(retryAfterS)
|
|
||||||
}
|
|
||||||
|
|
||||||
func reannounceAfterString() string {
|
func reannounceAfterString() string {
|
||||||
return strconv.Itoa(reannounceAfterSeconds + rand.Intn(reannounzeFuzzSeconds))
|
return strconv.Itoa(reannounceAfterSeconds + rand.Intn(reannounzeFuzzSeconds))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type retryAfterTracker struct {
|
||||||
|
name string
|
||||||
|
desiredRate float64 // requests per second
|
||||||
|
|
||||||
|
mut sync.Mutex
|
||||||
|
lastCount int // requests in the last bucket
|
||||||
|
curCount int // requests in the current bucket
|
||||||
|
bucketStarts time.Time // start of the current bucket
|
||||||
|
currentDelay int // current delay in seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *retryAfterTracker) retryAfterS() int {
|
||||||
|
now := time.Now()
|
||||||
|
t.mut.Lock()
|
||||||
|
if durS := now.Sub(t.bucketStarts).Seconds(); durS > float64(t.currentDelay) {
|
||||||
|
t.bucketStarts = now
|
||||||
|
t.lastCount = t.curCount
|
||||||
|
lastRate := float64(t.lastCount) / durS
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case t.currentDelay > notFoundRetryUnknownMinSeconds &&
|
||||||
|
lastRate < 0.75*t.desiredRate:
|
||||||
|
t.currentDelay = max(8*t.currentDelay/10, notFoundRetryUnknownMinSeconds)
|
||||||
|
case t.currentDelay < notFoundRetryUnknownMaxSeconds &&
|
||||||
|
lastRate > 1.25*t.desiredRate:
|
||||||
|
t.currentDelay = min(3*t.currentDelay/2, notFoundRetryUnknownMaxSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.curCount = 0
|
||||||
|
}
|
||||||
|
if t.curCount == 0 {
|
||||||
|
retryAfterLevel.WithLabelValues(t.name).Set(float64(t.currentDelay))
|
||||||
|
}
|
||||||
|
t.curCount++
|
||||||
|
t.mut.Unlock()
|
||||||
|
return t.currentDelay + rand.Intn(t.currentDelay/4)
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,9 +7,20 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFixupAddresses(t *testing.T) {
|
func TestFixupAddresses(t *testing.T) {
|
||||||
@@ -69,6 +80,14 @@ func TestFixupAddresses(t *testing.T) {
|
|||||||
remote: addr("123.123.123.123", 9000),
|
remote: addr("123.123.123.123", 9000),
|
||||||
in: []string{"tcp://44.44.44.44:0"},
|
in: []string{"tcp://44.44.44.44:0"},
|
||||||
out: []string{"tcp://44.44.44.44:9000"},
|
out: []string{"tcp://44.44.44.44:9000"},
|
||||||
|
}, { // remote ip nil
|
||||||
|
remote: addr("", 9000),
|
||||||
|
in: []string{"tcp://:22000", "tcp://44.44.44.44:9000"},
|
||||||
|
out: []string{"tcp://44.44.44.44:9000"},
|
||||||
|
}, { // remote port 0
|
||||||
|
remote: addr("123.123.123.123", 0),
|
||||||
|
in: []string{"tcp://:22000", "tcp://44.44.44.44"},
|
||||||
|
out: []string{"tcp://123.123.123.123:22000"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,3 +105,79 @@ func addr(host string, port int) *net.TCPAddr {
|
|||||||
Port: port,
|
Port: port,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkAPIRequests(b *testing.B) {
|
||||||
|
db := newInMemoryStore(b.TempDir(), 0, nil)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
go db.Serve(ctx)
|
||||||
|
api := newAPISrv("127.0.0.1:0", tls.Certificate{}, db, nil, true, true, 1000)
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(api.handler))
|
||||||
|
|
||||||
|
kf := b.TempDir() + "/cert"
|
||||||
|
crt, err := tlsutil.NewCertificate(kf+".crt", kf+".key", "localhost", 7)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
certBs, err := os.ReadFile(kf + ".crt")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
certBs = regexp.MustCompile(`---[^\n]+---\n`).ReplaceAll(certBs, nil)
|
||||||
|
certString := string(strings.ReplaceAll(string(certBs), "\n", " "))
|
||||||
|
|
||||||
|
devID := protocol.NewDeviceID(crt.Certificate[0])
|
||||||
|
devIDString := devID.String()
|
||||||
|
|
||||||
|
b.Run("Announce", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
url := srv.URL + "/v2/?device=" + devIDString
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(`{"addresses":["tcp://10.10.10.10:42000"]}`))
|
||||||
|
req.Header.Set("X-Forwarded-Tls-Client-Cert", certString)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusNoContent {
|
||||||
|
b.Fatalf("unexpected status %s", resp.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("Lookup", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
url := srv.URL + "/v2/?device=" + devIDString
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
b.Fatalf("unexpected status %s", resp.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("LookupNoCompression", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
url := srv.URL + "/v2/?device=" + devIDString
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
req.Header.Set("Accept-Encoding", "identity") // disable compression
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
b.Fatalf("unexpected status %s", resp.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+331
-278
@@ -4,20 +4,31 @@
|
|||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
//go:generate go run ../../proto/scripts/protofmt.go database.proto
|
|
||||||
//go:generate protoc -I ../../ -I . --gogofast_out=. database.proto
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"sort"
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
"google.golang.org/protobuf/proto"
|
||||||
"github.com/syndtr/goleveldb/leveldb/util"
|
|
||||||
|
"github.com/syncthing/syncthing/internal/blob"
|
||||||
|
"github.com/syncthing/syncthing/internal/gen/discosrv"
|
||||||
|
"github.com/syncthing/syncthing/internal/protoutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
type clock interface {
|
type clock interface {
|
||||||
@@ -31,359 +42,401 @@ func (defaultClock) Now() time.Time {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type database interface {
|
type database interface {
|
||||||
put(key string, rec DatabaseRecord) error
|
put(key *protocol.DeviceID, rec *discosrv.DatabaseRecord) error
|
||||||
merge(key string, addrs []DatabaseAddress, seen int64) error
|
merge(key *protocol.DeviceID, addrs []*discosrv.DatabaseAddress, seen int64) error
|
||||||
get(key string) (DatabaseRecord, error)
|
get(key *protocol.DeviceID) (*discosrv.DatabaseRecord, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type levelDBStore struct {
|
type inMemoryStore struct {
|
||||||
db *leveldb.DB
|
m *xsync.MapOf[protocol.DeviceID, *discosrv.DatabaseRecord]
|
||||||
inbox chan func()
|
dir string
|
||||||
clock clock
|
flushInterval time.Duration
|
||||||
marshalBuf []byte
|
blobs blob.Store
|
||||||
|
objKey string
|
||||||
|
clock clock
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLevelDBStore(dir string) (*levelDBStore, error) {
|
func newInMemoryStore(dir string, flushInterval time.Duration, blobs blob.Store) *inMemoryStore {
|
||||||
db, err := leveldb.OpenFile(dir, levelDBOptions)
|
hn, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
hn = rand.String(8)
|
||||||
}
|
}
|
||||||
return &levelDBStore{
|
s := &inMemoryStore{
|
||||||
db: db,
|
m: xsync.NewMapOf[protocol.DeviceID, *discosrv.DatabaseRecord](),
|
||||||
inbox: make(chan func(), 16),
|
dir: dir,
|
||||||
clock: defaultClock{},
|
flushInterval: flushInterval,
|
||||||
}, nil
|
blobs: blobs,
|
||||||
}
|
objKey: hn + ".db",
|
||||||
|
clock: defaultClock{},
|
||||||
func newMemoryLevelDBStore() (*levelDBStore, error) {
|
|
||||||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
return &levelDBStore{
|
nr, err := s.read()
|
||||||
db: db,
|
if os.IsNotExist(err) && blobs != nil {
|
||||||
inbox: make(chan func(), 16),
|
// Try to read from blob storage
|
||||||
clock: defaultClock{},
|
latestKey, cerr := blobs.LatestKey(context.Background())
|
||||||
}, nil
|
if cerr != nil {
|
||||||
}
|
log.Println("Error finding database from blob storage:", cerr)
|
||||||
|
return s
|
||||||
func (s *levelDBStore) put(key string, rec DatabaseRecord) error {
|
|
||||||
t0 := time.Now()
|
|
||||||
defer func() {
|
|
||||||
databaseOperationSeconds.WithLabelValues(dbOpPut).Observe(time.Since(t0).Seconds())
|
|
||||||
}()
|
|
||||||
|
|
||||||
rc := make(chan error)
|
|
||||||
|
|
||||||
s.inbox <- func() {
|
|
||||||
size := rec.Size()
|
|
||||||
if len(s.marshalBuf) < size {
|
|
||||||
s.marshalBuf = make([]byte, size)
|
|
||||||
}
|
}
|
||||||
n, _ := rec.MarshalTo(s.marshalBuf)
|
fd, cerr := os.Create(path.Join(s.dir, "records.db"))
|
||||||
rc <- s.db.Put([]byte(key), s.marshalBuf[:n], nil)
|
if cerr != nil {
|
||||||
|
log.Println("Error creating database file:", cerr)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if cerr := blobs.Download(context.Background(), latestKey, fd); cerr != nil {
|
||||||
|
log.Printf("Error downloading database from blob storage: %v", cerr)
|
||||||
|
}
|
||||||
|
_ = fd.Close()
|
||||||
|
nr, err = s.read()
|
||||||
}
|
}
|
||||||
|
|
||||||
err := <-rc
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
databaseOperations.WithLabelValues(dbOpPut, dbResError).Inc()
|
log.Println("Error reading database:", err)
|
||||||
} else {
|
|
||||||
databaseOperations.WithLabelValues(dbOpPut, dbResSuccess).Inc()
|
|
||||||
}
|
}
|
||||||
|
log.Printf("Read %d records from database", nr)
|
||||||
return err
|
s.expireAndCalculateStatistics()
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *levelDBStore) merge(key string, addrs []DatabaseAddress, seen int64) error {
|
func (s *inMemoryStore) put(key *protocol.DeviceID, rec *discosrv.DatabaseRecord) error {
|
||||||
t0 := time.Now()
|
t0 := time.Now()
|
||||||
defer func() {
|
s.m.Store(*key, rec)
|
||||||
databaseOperationSeconds.WithLabelValues(dbOpMerge).Observe(time.Since(t0).Seconds())
|
databaseOperations.WithLabelValues(dbOpPut, dbResSuccess).Inc()
|
||||||
}()
|
databaseOperationSeconds.WithLabelValues(dbOpPut).Observe(time.Since(t0).Seconds())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
rc := make(chan error)
|
func (s *inMemoryStore) merge(key *protocol.DeviceID, addrs []*discosrv.DatabaseAddress, seen int64) error {
|
||||||
newRec := DatabaseRecord{
|
t0 := time.Now()
|
||||||
|
|
||||||
|
newRec := &discosrv.DatabaseRecord{
|
||||||
Addresses: addrs,
|
Addresses: addrs,
|
||||||
Seen: seen,
|
Seen: seen,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.inbox <- func() {
|
if oldRec, ok := s.m.Load(*key); ok {
|
||||||
// grab the existing record
|
newRec = merge(oldRec, newRec)
|
||||||
oldRec, err := s.get(key)
|
|
||||||
if err != nil {
|
|
||||||
// "not found" is not an error from get, so this is serious
|
|
||||||
// stuff only
|
|
||||||
rc <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
newRec = merge(newRec, oldRec)
|
|
||||||
|
|
||||||
// We replicate s.put() functionality here ourselves instead of
|
|
||||||
// calling it because we want to serialize our get above together
|
|
||||||
// with the put in the same function.
|
|
||||||
size := newRec.Size()
|
|
||||||
if len(s.marshalBuf) < size {
|
|
||||||
s.marshalBuf = make([]byte, size)
|
|
||||||
}
|
|
||||||
n, _ := newRec.MarshalTo(s.marshalBuf)
|
|
||||||
rc <- s.db.Put([]byte(key), s.marshalBuf[:n], nil)
|
|
||||||
}
|
}
|
||||||
|
s.m.Store(*key, newRec)
|
||||||
|
|
||||||
err := <-rc
|
databaseOperations.WithLabelValues(dbOpMerge, dbResSuccess).Inc()
|
||||||
if err != nil {
|
databaseOperationSeconds.WithLabelValues(dbOpMerge).Observe(time.Since(t0).Seconds())
|
||||||
databaseOperations.WithLabelValues(dbOpMerge, dbResError).Inc()
|
|
||||||
} else {
|
|
||||||
databaseOperations.WithLabelValues(dbOpMerge, dbResSuccess).Inc()
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *levelDBStore) get(key string) (DatabaseRecord, error) {
|
func (s *inMemoryStore) get(key *protocol.DeviceID) (*discosrv.DatabaseRecord, error) {
|
||||||
t0 := time.Now()
|
t0 := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
databaseOperationSeconds.WithLabelValues(dbOpGet).Observe(time.Since(t0).Seconds())
|
databaseOperationSeconds.WithLabelValues(dbOpGet).Observe(time.Since(t0).Seconds())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
keyBs := []byte(key)
|
rec, ok := s.m.Load(*key)
|
||||||
val, err := s.db.Get(keyBs, nil)
|
if !ok {
|
||||||
if err == leveldb.ErrNotFound {
|
|
||||||
databaseOperations.WithLabelValues(dbOpGet, dbResNotFound).Inc()
|
databaseOperations.WithLabelValues(dbOpGet, dbResNotFound).Inc()
|
||||||
return DatabaseRecord{}, nil
|
return &discosrv.DatabaseRecord{}, nil
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
databaseOperations.WithLabelValues(dbOpGet, dbResError).Inc()
|
|
||||||
return DatabaseRecord{}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var rec DatabaseRecord
|
rec.Addresses = expire(rec.Addresses, s.clock.Now())
|
||||||
|
|
||||||
if err := rec.Unmarshal(val); err != nil {
|
|
||||||
databaseOperations.WithLabelValues(dbOpGet, dbResUnmarshalError).Inc()
|
|
||||||
return DatabaseRecord{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rec.Addresses = expire(rec.Addresses, s.clock.Now().UnixNano())
|
|
||||||
databaseOperations.WithLabelValues(dbOpGet, dbResSuccess).Inc()
|
databaseOperations.WithLabelValues(dbOpGet, dbResSuccess).Inc()
|
||||||
return rec, nil
|
return rec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *levelDBStore) Serve(ctx context.Context) error {
|
func (s *inMemoryStore) Serve(ctx context.Context) error {
|
||||||
t := time.NewTimer(0)
|
if s.flushInterval <= 0 {
|
||||||
defer t.Stop()
|
<-ctx.Done()
|
||||||
defer s.db.Close()
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Start the statistics serve routine. It will exit with us when
|
t := time.NewTimer(s.flushInterval)
|
||||||
// statisticsTrigger is closed.
|
defer t.Stop()
|
||||||
statisticsTrigger := make(chan struct{})
|
|
||||||
statisticsDone := make(chan struct{})
|
|
||||||
go s.statisticsServe(statisticsTrigger, statisticsDone)
|
|
||||||
|
|
||||||
loop:
|
loop:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case fn := <-s.inbox:
|
|
||||||
// Run function in serialized order.
|
|
||||||
fn()
|
|
||||||
|
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
// Trigger the statistics routine to do its thing in the
|
log.Println("Calculating statistics")
|
||||||
// background.
|
s.expireAndCalculateStatistics()
|
||||||
statisticsTrigger <- struct{}{}
|
log.Println("Flushing database")
|
||||||
|
if err := s.write(); err != nil {
|
||||||
case <-statisticsDone:
|
log.Println("Error writing database:", err)
|
||||||
// The statistics routine is done with one iteratation, schedule
|
}
|
||||||
// the next.
|
log.Println("Finished flushing database")
|
||||||
t.Reset(databaseStatisticsInterval)
|
t.Reset(s.flushInterval)
|
||||||
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// We're done.
|
// We're done.
|
||||||
close(statisticsTrigger)
|
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also wait for statisticsServe to return
|
return s.write()
|
||||||
<-statisticsDone
|
}
|
||||||
|
|
||||||
|
func (s *inMemoryStore) expireAndCalculateStatistics() {
|
||||||
|
now := s.clock.Now()
|
||||||
|
cutoff24h := now.Add(-24 * time.Hour).UnixNano()
|
||||||
|
cutoff1w := now.Add(-7 * 24 * time.Hour).UnixNano()
|
||||||
|
current, currentIPv4, currentIPv6, currentIPv6GUA, last24h, last1w := 0, 0, 0, 0, 0, 0
|
||||||
|
|
||||||
|
n := 0
|
||||||
|
s.m.Range(func(key protocol.DeviceID, rec *discosrv.DatabaseRecord) bool {
|
||||||
|
if n%1000 == 0 {
|
||||||
|
runtime.Gosched()
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
|
||||||
|
addresses := expire(rec.Addresses, now)
|
||||||
|
if len(addresses) == 0 {
|
||||||
|
rec.Addresses = nil
|
||||||
|
s.m.Store(key, rec)
|
||||||
|
} else if len(addresses) != len(rec.Addresses) {
|
||||||
|
rec.Addresses = addresses
|
||||||
|
s.m.Store(key, rec)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(rec.Addresses) > 0:
|
||||||
|
current++
|
||||||
|
seenIPv4, seenIPv6, seenIPv6GUA := false, false, false
|
||||||
|
for _, addr := range rec.Addresses {
|
||||||
|
// We do fast and loose matching on strings here instead of
|
||||||
|
// parsing the address and the IP and doing "proper" checks,
|
||||||
|
// to keep things fast and generate less garbage.
|
||||||
|
if strings.Contains(addr.Address, "[") {
|
||||||
|
seenIPv6 = true
|
||||||
|
if strings.Contains(addr.Address, "[2") {
|
||||||
|
seenIPv6GUA = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
seenIPv4 = true
|
||||||
|
}
|
||||||
|
if seenIPv4 && seenIPv6 && seenIPv6GUA {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seenIPv4 {
|
||||||
|
currentIPv4++
|
||||||
|
}
|
||||||
|
if seenIPv6 {
|
||||||
|
currentIPv6++
|
||||||
|
}
|
||||||
|
if seenIPv6GUA {
|
||||||
|
currentIPv6GUA++
|
||||||
|
}
|
||||||
|
case rec.Seen > cutoff24h:
|
||||||
|
last24h++
|
||||||
|
case rec.Seen > cutoff1w:
|
||||||
|
last1w++
|
||||||
|
default:
|
||||||
|
// drop the record if it's older than a week
|
||||||
|
s.m.Delete(key)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
databaseKeys.WithLabelValues("current").Set(float64(current))
|
||||||
|
databaseKeys.WithLabelValues("currentIPv4").Set(float64(currentIPv4))
|
||||||
|
databaseKeys.WithLabelValues("currentIPv6").Set(float64(currentIPv6))
|
||||||
|
databaseKeys.WithLabelValues("currentIPv6GUA").Set(float64(currentIPv6GUA))
|
||||||
|
databaseKeys.WithLabelValues("last24h").Set(float64(last24h))
|
||||||
|
databaseKeys.WithLabelValues("last1w").Set(float64(last1w))
|
||||||
|
databaseStatisticsSeconds.Set(time.Since(now).Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inMemoryStore) write() (err error) {
|
||||||
|
t0 := time.Now()
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
databaseWriteSeconds.Set(time.Since(t0).Seconds())
|
||||||
|
databaseLastWritten.Set(float64(t0.Unix()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
dbf := path.Join(s.dir, "records.db")
|
||||||
|
fd, err := os.Create(dbf + ".tmp")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bw := bufio.NewWriter(fd)
|
||||||
|
|
||||||
|
var buf []byte
|
||||||
|
var rangeErr error
|
||||||
|
now := s.clock.Now()
|
||||||
|
cutoff1w := now.Add(-7 * 24 * time.Hour).UnixNano()
|
||||||
|
n := 0
|
||||||
|
s.m.Range(func(key protocol.DeviceID, value *discosrv.DatabaseRecord) bool {
|
||||||
|
if n%1000 == 0 {
|
||||||
|
runtime.Gosched()
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
|
||||||
|
if value.Seen < cutoff1w {
|
||||||
|
// drop the record if it's older than a week
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
rec := &discosrv.ReplicationRecord{
|
||||||
|
Key: key[:],
|
||||||
|
Addresses: value.Addresses,
|
||||||
|
Seen: value.Seen,
|
||||||
|
}
|
||||||
|
s := proto.Size(rec)
|
||||||
|
if s+4 > len(buf) {
|
||||||
|
buf = make([]byte, s+4)
|
||||||
|
}
|
||||||
|
n, err := protoutil.MarshalTo(buf[4:], rec)
|
||||||
|
if err != nil {
|
||||||
|
rangeErr = err
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint32(buf, uint32(n))
|
||||||
|
if _, err := bw.Write(buf[:n+4]); err != nil {
|
||||||
|
rangeErr = err
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if rangeErr != nil {
|
||||||
|
_ = fd.Close()
|
||||||
|
return rangeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bw.Flush(); err != nil {
|
||||||
|
_ = fd.Close
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := fd.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Rename(dbf+".tmp", dbf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload to blob storage
|
||||||
|
if s.blobs != nil {
|
||||||
|
fd, err = os.Open(dbf)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error uploading database to blob storage: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
if err := s.blobs.Upload(context.Background(), s.objKey, fd); err != nil {
|
||||||
|
log.Printf("Error uploading database to blob storage: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("Finished uploading database")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *levelDBStore) statisticsServe(trigger <-chan struct{}, done chan<- struct{}) {
|
func (s *inMemoryStore) read() (int, error) {
|
||||||
defer close(done)
|
fd, err := os.Open(path.Join(s.dir, "records.db"))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
for range trigger {
|
br := bufio.NewReader(fd)
|
||||||
t0 := time.Now()
|
var buf []byte
|
||||||
nowNanos := t0.UnixNano()
|
nr := 0
|
||||||
cutoff24h := t0.Add(-24 * time.Hour).UnixNano()
|
for {
|
||||||
cutoff1w := t0.Add(-7 * 24 * time.Hour).UnixNano()
|
var n uint32
|
||||||
cutoff2Mon := t0.Add(-60 * 24 * time.Hour).UnixNano()
|
if err := binary.Read(br, binary.BigEndian, &n); err != nil {
|
||||||
current, last24h, last1w, inactive, errors := 0, 0, 0, 0, 0
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
iter := s.db.NewIterator(&util.Range{}, nil)
|
|
||||||
for iter.Next() {
|
|
||||||
// Attempt to unmarshal the record and count the
|
|
||||||
// failure if there's something wrong with it.
|
|
||||||
var rec DatabaseRecord
|
|
||||||
if err := rec.Unmarshal(iter.Value()); err != nil {
|
|
||||||
errors++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are addresses that have not expired it's a current
|
|
||||||
// record, otherwise account it based on when it was last seen
|
|
||||||
// (last 24 hours or last week) or finally as inactice.
|
|
||||||
switch {
|
|
||||||
case len(expire(rec.Addresses, nowNanos)) > 0:
|
|
||||||
current++
|
|
||||||
case rec.Seen > cutoff24h:
|
|
||||||
last24h++
|
|
||||||
case rec.Seen > cutoff1w:
|
|
||||||
last1w++
|
|
||||||
case rec.Seen > cutoff2Mon:
|
|
||||||
inactive++
|
|
||||||
case rec.Missed < cutoff2Mon:
|
|
||||||
// It hasn't been seen lately and we haven't recorded
|
|
||||||
// someone asking for this device in a long time either;
|
|
||||||
// delete the record.
|
|
||||||
if err := s.db.Delete(iter.Key(), nil); err != nil {
|
|
||||||
databaseOperations.WithLabelValues(dbOpDelete, dbResError).Inc()
|
|
||||||
} else {
|
|
||||||
databaseOperations.WithLabelValues(dbOpDelete, dbResSuccess).Inc()
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
inactive++
|
|
||||||
}
|
}
|
||||||
|
return nr, err
|
||||||
|
}
|
||||||
|
if int(n) > len(buf) {
|
||||||
|
buf = make([]byte, n)
|
||||||
|
}
|
||||||
|
if _, err := io.ReadFull(br, buf[:n]); err != nil {
|
||||||
|
return nr, err
|
||||||
|
}
|
||||||
|
rec := &discosrv.ReplicationRecord{}
|
||||||
|
if err := proto.Unmarshal(buf[:n], rec); err != nil {
|
||||||
|
return nr, err
|
||||||
|
}
|
||||||
|
key, err := protocol.DeviceIDFromBytes(rec.Key)
|
||||||
|
if err != nil {
|
||||||
|
key, err = protocol.DeviceIDFromString(string(rec.Key))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Bad device ID:", err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
iter.Release()
|
slices.SortFunc(rec.Addresses, Cmp)
|
||||||
|
rec.Addresses = slices.CompactFunc(rec.Addresses, Equal)
|
||||||
databaseKeys.WithLabelValues("current").Set(float64(current))
|
s.m.Store(key, &discosrv.DatabaseRecord{
|
||||||
databaseKeys.WithLabelValues("last24h").Set(float64(last24h))
|
Addresses: expire(rec.Addresses, s.clock.Now()),
|
||||||
databaseKeys.WithLabelValues("last1w").Set(float64(last1w))
|
Seen: rec.Seen,
|
||||||
databaseKeys.WithLabelValues("inactive").Set(float64(inactive))
|
})
|
||||||
databaseKeys.WithLabelValues("error").Set(float64(errors))
|
nr++
|
||||||
databaseStatisticsSeconds.Set(time.Since(t0).Seconds())
|
|
||||||
|
|
||||||
// Signal that we are done and can be scheduled again.
|
|
||||||
done <- struct{}{}
|
|
||||||
}
|
}
|
||||||
|
return nr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// merge returns the merged result of the two database records a and b. The
|
// merge returns the merged result of the two database records a and b. The
|
||||||
// result is the union of the two address sets, with the newer expiry time
|
// result is the union of the two address sets, with the newer expiry time
|
||||||
// chosen for any duplicates.
|
// chosen for any duplicates. The address list in a is overwritten and
|
||||||
func merge(a, b DatabaseRecord) DatabaseRecord {
|
// reused for the result.
|
||||||
|
func merge(a, b *discosrv.DatabaseRecord) *discosrv.DatabaseRecord {
|
||||||
// Both lists must be sorted for this to work.
|
// Both lists must be sorted for this to work.
|
||||||
if !sort.IsSorted(databaseAddressOrder(a.Addresses)) {
|
|
||||||
log.Println("Warning: bug: addresses not correctly sorted in merge")
|
|
||||||
a.Addresses = sortedAddressCopy(a.Addresses)
|
|
||||||
}
|
|
||||||
if !sort.IsSorted(databaseAddressOrder(b.Addresses)) {
|
|
||||||
// no warning because this is the side we read from disk and it may
|
|
||||||
// legitimately predate correct sorting.
|
|
||||||
b.Addresses = sortedAddressCopy(b.Addresses)
|
|
||||||
}
|
|
||||||
|
|
||||||
res := DatabaseRecord{
|
a.Seen = max(a.Seen, b.Seen)
|
||||||
Addresses: make([]DatabaseAddress, 0, len(a.Addresses)+len(b.Addresses)),
|
|
||||||
Seen: a.Seen,
|
|
||||||
}
|
|
||||||
if b.Seen > a.Seen {
|
|
||||||
res.Seen = b.Seen
|
|
||||||
}
|
|
||||||
|
|
||||||
aIdx := 0
|
aIdx := 0
|
||||||
bIdx := 0
|
bIdx := 0
|
||||||
aAddrs := a.Addresses
|
for aIdx < len(a.Addresses) && bIdx < len(b.Addresses) {
|
||||||
bAddrs := b.Addresses
|
switch cmp.Compare(a.Addresses[aIdx].Address, b.Addresses[bIdx].Address) {
|
||||||
loop:
|
case 0:
|
||||||
for {
|
// a == b, choose the newer expiry time
|
||||||
switch {
|
a.Addresses[aIdx].Expires = max(a.Addresses[aIdx].Expires, b.Addresses[bIdx].Expires)
|
||||||
case aIdx == len(aAddrs) && bIdx == len(bAddrs):
|
|
||||||
// both lists are exhausted, we are done
|
|
||||||
break loop
|
|
||||||
|
|
||||||
case aIdx == len(aAddrs):
|
|
||||||
// a is exhausted, pick from b and continue
|
|
||||||
res.Addresses = append(res.Addresses, bAddrs[bIdx])
|
|
||||||
bIdx++
|
|
||||||
continue
|
|
||||||
|
|
||||||
case bIdx == len(bAddrs):
|
|
||||||
// b is exhausted, pick from a and continue
|
|
||||||
res.Addresses = append(res.Addresses, aAddrs[aIdx])
|
|
||||||
aIdx++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have values left on both sides.
|
|
||||||
aVal := aAddrs[aIdx]
|
|
||||||
bVal := bAddrs[bIdx]
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case aVal.Address == bVal.Address:
|
|
||||||
// update for same address, pick newer
|
|
||||||
if aVal.Expires > bVal.Expires {
|
|
||||||
res.Addresses = append(res.Addresses, aVal)
|
|
||||||
} else {
|
|
||||||
res.Addresses = append(res.Addresses, bVal)
|
|
||||||
}
|
|
||||||
aIdx++
|
aIdx++
|
||||||
bIdx++
|
bIdx++
|
||||||
|
case -1:
|
||||||
case aVal.Address < bVal.Address:
|
// a < b, keep a and move on
|
||||||
// a is smallest, pick it and continue
|
|
||||||
res.Addresses = append(res.Addresses, aVal)
|
|
||||||
aIdx++
|
aIdx++
|
||||||
|
case 1:
|
||||||
default:
|
// a > b, insert b before a
|
||||||
// b is smallest, pick it and continue
|
a.Addresses = append(a.Addresses[:aIdx], append([]*discosrv.DatabaseAddress{b.Addresses[bIdx]}, a.Addresses[aIdx:]...)...)
|
||||||
res.Addresses = append(res.Addresses, bVal)
|
|
||||||
bIdx++
|
bIdx++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res
|
if bIdx < len(b.Addresses) {
|
||||||
|
a.Addresses = append(a.Addresses, b.Addresses[bIdx:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
// expire returns the list of addresses after removing expired entries.
|
// expire returns the list of addresses after removing expired entries.
|
||||||
// Expiration happen in place, so the slice given as the parameter is
|
// Expiration happen in place, so the slice given as the parameter is
|
||||||
// destroyed. Internal order is not preserved.
|
// destroyed. Internal order is preserved.
|
||||||
func expire(addrs []DatabaseAddress, now int64) []DatabaseAddress {
|
func expire(addrs []*discosrv.DatabaseAddress, now time.Time) []*discosrv.DatabaseAddress {
|
||||||
i := 0
|
cutoff := now.UnixNano()
|
||||||
for i < len(addrs) {
|
naddrs := addrs[:0]
|
||||||
if addrs[i].Expires < now {
|
for i := range addrs {
|
||||||
// This item is expired. Replace it with the last in the list
|
if i > 0 && addrs[i].Address == addrs[i-1].Address {
|
||||||
// (noop if we are at the last item).
|
// Skip duplicates
|
||||||
addrs[i] = addrs[len(addrs)-1]
|
|
||||||
// Wipe the last item of the list to release references to
|
|
||||||
// strings and stuff.
|
|
||||||
addrs[len(addrs)-1] = DatabaseAddress{}
|
|
||||||
// Shorten the slice.
|
|
||||||
addrs = addrs[:len(addrs)-1]
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
i++
|
if addrs[i].Expires >= cutoff {
|
||||||
|
naddrs = append(naddrs, addrs[i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return addrs
|
if len(naddrs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return naddrs
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortedAddressCopy(addrs []DatabaseAddress) []DatabaseAddress {
|
func Cmp(d, other *discosrv.DatabaseAddress) (n int) {
|
||||||
sorted := make([]DatabaseAddress, len(addrs))
|
if c := cmp.Compare(d.Address, other.Address); c != 0 {
|
||||||
copy(sorted, addrs)
|
return c
|
||||||
sort.Sort(databaseAddressOrder(sorted))
|
}
|
||||||
return sorted
|
return cmp.Compare(d.Expires, other.Expires)
|
||||||
}
|
}
|
||||||
|
|
||||||
type databaseAddressOrder []DatabaseAddress
|
func Equal(d, other *discosrv.DatabaseAddress) bool {
|
||||||
|
return d.Address == other.Address
|
||||||
func (s databaseAddressOrder) Less(a, b int) bool {
|
|
||||||
return s[a].Address < s[b].Address
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s databaseAddressOrder) Swap(a, b int) {
|
|
||||||
s[a], s[b] = s[b], s[a]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s databaseAddressOrder) Len() int {
|
|
||||||
return len(s)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,847 +0,0 @@
|
|||||||
// Code generated by protoc-gen-gogo. DO NOT EDIT.
|
|
||||||
// source: database.proto
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
fmt "fmt"
|
|
||||||
_ "github.com/gogo/protobuf/gogoproto"
|
|
||||||
proto "github.com/gogo/protobuf/proto"
|
|
||||||
io "io"
|
|
||||||
math "math"
|
|
||||||
math_bits "math/bits"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reference imports to suppress errors if they are not otherwise used.
|
|
||||||
var _ = proto.Marshal
|
|
||||||
var _ = fmt.Errorf
|
|
||||||
var _ = math.Inf
|
|
||||||
|
|
||||||
// This is a compile-time assertion to ensure that this generated file
|
|
||||||
// is compatible with the proto package it is being compiled against.
|
|
||||||
// A compilation error at this line likely means your copy of the
|
|
||||||
// proto package needs to be updated.
|
|
||||||
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
|
|
||||||
|
|
||||||
type DatabaseRecord struct {
|
|
||||||
Addresses []DatabaseAddress `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses"`
|
|
||||||
Misses int32 `protobuf:"varint,2,opt,name=misses,proto3" json:"misses,omitempty"`
|
|
||||||
Seen int64 `protobuf:"varint,3,opt,name=seen,proto3" json:"seen,omitempty"`
|
|
||||||
Missed int64 `protobuf:"varint,4,opt,name=missed,proto3" json:"missed,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DatabaseRecord) Reset() { *m = DatabaseRecord{} }
|
|
||||||
func (m *DatabaseRecord) String() string { return proto.CompactTextString(m) }
|
|
||||||
func (*DatabaseRecord) ProtoMessage() {}
|
|
||||||
func (*DatabaseRecord) Descriptor() ([]byte, []int) {
|
|
||||||
return fileDescriptor_b90fe3356ea5df07, []int{0}
|
|
||||||
}
|
|
||||||
func (m *DatabaseRecord) XXX_Unmarshal(b []byte) error {
|
|
||||||
return m.Unmarshal(b)
|
|
||||||
}
|
|
||||||
func (m *DatabaseRecord) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
|
||||||
if deterministic {
|
|
||||||
return xxx_messageInfo_DatabaseRecord.Marshal(b, m, deterministic)
|
|
||||||
} else {
|
|
||||||
b = b[:cap(b)]
|
|
||||||
n, err := m.MarshalToSizedBuffer(b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return b[:n], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (m *DatabaseRecord) XXX_Merge(src proto.Message) {
|
|
||||||
xxx_messageInfo_DatabaseRecord.Merge(m, src)
|
|
||||||
}
|
|
||||||
func (m *DatabaseRecord) XXX_Size() int {
|
|
||||||
return m.Size()
|
|
||||||
}
|
|
||||||
func (m *DatabaseRecord) XXX_DiscardUnknown() {
|
|
||||||
xxx_messageInfo_DatabaseRecord.DiscardUnknown(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
var xxx_messageInfo_DatabaseRecord proto.InternalMessageInfo
|
|
||||||
|
|
||||||
type ReplicationRecord struct {
|
|
||||||
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
|
||||||
Addresses []DatabaseAddress `protobuf:"bytes,2,rep,name=addresses,proto3" json:"addresses"`
|
|
||||||
Seen int64 `protobuf:"varint,3,opt,name=seen,proto3" json:"seen,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ReplicationRecord) Reset() { *m = ReplicationRecord{} }
|
|
||||||
func (m *ReplicationRecord) String() string { return proto.CompactTextString(m) }
|
|
||||||
func (*ReplicationRecord) ProtoMessage() {}
|
|
||||||
func (*ReplicationRecord) Descriptor() ([]byte, []int) {
|
|
||||||
return fileDescriptor_b90fe3356ea5df07, []int{1}
|
|
||||||
}
|
|
||||||
func (m *ReplicationRecord) XXX_Unmarshal(b []byte) error {
|
|
||||||
return m.Unmarshal(b)
|
|
||||||
}
|
|
||||||
func (m *ReplicationRecord) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
|
||||||
if deterministic {
|
|
||||||
return xxx_messageInfo_ReplicationRecord.Marshal(b, m, deterministic)
|
|
||||||
} else {
|
|
||||||
b = b[:cap(b)]
|
|
||||||
n, err := m.MarshalToSizedBuffer(b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return b[:n], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (m *ReplicationRecord) XXX_Merge(src proto.Message) {
|
|
||||||
xxx_messageInfo_ReplicationRecord.Merge(m, src)
|
|
||||||
}
|
|
||||||
func (m *ReplicationRecord) XXX_Size() int {
|
|
||||||
return m.Size()
|
|
||||||
}
|
|
||||||
func (m *ReplicationRecord) XXX_DiscardUnknown() {
|
|
||||||
xxx_messageInfo_ReplicationRecord.DiscardUnknown(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
var xxx_messageInfo_ReplicationRecord proto.InternalMessageInfo
|
|
||||||
|
|
||||||
type DatabaseAddress struct {
|
|
||||||
Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
|
|
||||||
Expires int64 `protobuf:"varint,2,opt,name=expires,proto3" json:"expires,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DatabaseAddress) Reset() { *m = DatabaseAddress{} }
|
|
||||||
func (m *DatabaseAddress) String() string { return proto.CompactTextString(m) }
|
|
||||||
func (*DatabaseAddress) ProtoMessage() {}
|
|
||||||
func (*DatabaseAddress) Descriptor() ([]byte, []int) {
|
|
||||||
return fileDescriptor_b90fe3356ea5df07, []int{2}
|
|
||||||
}
|
|
||||||
func (m *DatabaseAddress) XXX_Unmarshal(b []byte) error {
|
|
||||||
return m.Unmarshal(b)
|
|
||||||
}
|
|
||||||
func (m *DatabaseAddress) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
|
||||||
if deterministic {
|
|
||||||
return xxx_messageInfo_DatabaseAddress.Marshal(b, m, deterministic)
|
|
||||||
} else {
|
|
||||||
b = b[:cap(b)]
|
|
||||||
n, err := m.MarshalToSizedBuffer(b)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return b[:n], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (m *DatabaseAddress) XXX_Merge(src proto.Message) {
|
|
||||||
xxx_messageInfo_DatabaseAddress.Merge(m, src)
|
|
||||||
}
|
|
||||||
func (m *DatabaseAddress) XXX_Size() int {
|
|
||||||
return m.Size()
|
|
||||||
}
|
|
||||||
func (m *DatabaseAddress) XXX_DiscardUnknown() {
|
|
||||||
xxx_messageInfo_DatabaseAddress.DiscardUnknown(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
var xxx_messageInfo_DatabaseAddress proto.InternalMessageInfo
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
proto.RegisterType((*DatabaseRecord)(nil), "main.DatabaseRecord")
|
|
||||||
proto.RegisterType((*ReplicationRecord)(nil), "main.ReplicationRecord")
|
|
||||||
proto.RegisterType((*DatabaseAddress)(nil), "main.DatabaseAddress")
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() { proto.RegisterFile("database.proto", fileDescriptor_b90fe3356ea5df07) }
|
|
||||||
|
|
||||||
var fileDescriptor_b90fe3356ea5df07 = []byte{
|
|
||||||
// 270 bytes of a gzipped FileDescriptorProto
|
|
||||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x90, 0x41, 0x4a, 0xc4, 0x30,
|
|
||||||
0x18, 0x85, 0x9b, 0x49, 0x1d, 0x99, 0x08, 0xa3, 0x06, 0x94, 0x20, 0x12, 0x4b, 0xdd, 0x74, 0xd5,
|
|
||||||
0x01, 0x5d, 0xb9, 0x74, 0xd0, 0x0b, 0xe4, 0x06, 0xe9, 0xe4, 0x77, 0x08, 0x3a, 0x4d, 0x49, 0x2a,
|
|
||||||
0xe8, 0x29, 0xf4, 0x58, 0x5d, 0xce, 0xd2, 0x95, 0x68, 0x7b, 0x11, 0x69, 0x26, 0x55, 0x14, 0x37,
|
|
||||||
0xb3, 0x7b, 0xdf, 0xff, 0xbf, 0x97, 0xbc, 0x84, 0x4c, 0x95, 0xac, 0x65, 0x21, 0x1d, 0xe4, 0x95,
|
|
||||||
0x35, 0xb5, 0xa1, 0xf1, 0x4a, 0xea, 0xf2, 0xe4, 0xdc, 0x42, 0x65, 0xdc, 0xcc, 0x8f, 0x8a, 0xc7,
|
|
||||||
0xbb, 0xd9, 0xd2, 0x2c, 0x8d, 0x07, 0xaf, 0x36, 0xd6, 0xf4, 0x05, 0x91, 0xe9, 0x4d, 0x48, 0x0b,
|
|
||||||
0x58, 0x18, 0xab, 0xe8, 0x15, 0x99, 0x48, 0xa5, 0x2c, 0x38, 0x07, 0x8e, 0xa1, 0x04, 0x67, 0x7b,
|
|
||||||
0x17, 0x47, 0x79, 0x7f, 0x62, 0x3e, 0x18, 0xaf, 0x37, 0xeb, 0x79, 0xdc, 0xbc, 0x9f, 0x45, 0xe2,
|
|
||||||
0xc7, 0x4d, 0x8f, 0xc9, 0x78, 0xa5, 0x7d, 0x6e, 0x94, 0xa0, 0x6c, 0x47, 0x04, 0xa2, 0x94, 0xc4,
|
|
||||||
0x0e, 0xa0, 0x64, 0x38, 0x41, 0x19, 0x16, 0x5e, 0x7f, 0x7b, 0x15, 0x8b, 0xfd, 0x34, 0x50, 0x5a,
|
|
||||||
0x93, 0x43, 0x01, 0xd5, 0x83, 0x5e, 0xc8, 0x5a, 0x9b, 0x32, 0x74, 0x3a, 0x20, 0xf8, 0x1e, 0x9e,
|
|
||||||
0x19, 0x4a, 0x50, 0x36, 0x11, 0xbd, 0xfc, 0xdd, 0x72, 0xb4, 0x55, 0xcb, 0x7f, 0xda, 0xa4, 0xb7,
|
|
||||||
0x64, 0xff, 0x4f, 0x8e, 0x32, 0xb2, 0x1b, 0x32, 0xe1, 0xde, 0x01, 0xfb, 0x0d, 0x3c, 0x55, 0xda,
|
|
||||||
0x86, 0x77, 0x62, 0x31, 0xe0, 0xfc, 0xb4, 0xf9, 0xe4, 0x51, 0xd3, 0x72, 0xb4, 0x6e, 0x39, 0xfa,
|
|
||||||
0x68, 0x39, 0x7a, 0xed, 0x78, 0xb4, 0xee, 0x78, 0xf4, 0xd6, 0xf1, 0xa8, 0x18, 0xfb, 0x3f, 0xbf,
|
|
||||||
0xfc, 0x0a, 0x00, 0x00, 0xff, 0xff, 0x7a, 0xa2, 0xf6, 0x1e, 0xb0, 0x01, 0x00, 0x00,
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DatabaseRecord) Marshal() (dAtA []byte, err error) {
|
|
||||||
size := m.Size()
|
|
||||||
dAtA = make([]byte, size)
|
|
||||||
n, err := m.MarshalToSizedBuffer(dAtA[:size])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return dAtA[:n], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DatabaseRecord) MarshalTo(dAtA []byte) (int, error) {
|
|
||||||
size := m.Size()
|
|
||||||
return m.MarshalToSizedBuffer(dAtA[:size])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DatabaseRecord) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
|
||||||
i := len(dAtA)
|
|
||||||
_ = i
|
|
||||||
var l int
|
|
||||||
_ = l
|
|
||||||
if m.Missed != 0 {
|
|
||||||
i = encodeVarintDatabase(dAtA, i, uint64(m.Missed))
|
|
||||||
i--
|
|
||||||
dAtA[i] = 0x20
|
|
||||||
}
|
|
||||||
if m.Seen != 0 {
|
|
||||||
i = encodeVarintDatabase(dAtA, i, uint64(m.Seen))
|
|
||||||
i--
|
|
||||||
dAtA[i] = 0x18
|
|
||||||
}
|
|
||||||
if m.Misses != 0 {
|
|
||||||
i = encodeVarintDatabase(dAtA, i, uint64(m.Misses))
|
|
||||||
i--
|
|
||||||
dAtA[i] = 0x10
|
|
||||||
}
|
|
||||||
if len(m.Addresses) > 0 {
|
|
||||||
for iNdEx := len(m.Addresses) - 1; iNdEx >= 0; iNdEx-- {
|
|
||||||
{
|
|
||||||
size, err := m.Addresses[iNdEx].MarshalToSizedBuffer(dAtA[:i])
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
i -= size
|
|
||||||
i = encodeVarintDatabase(dAtA, i, uint64(size))
|
|
||||||
}
|
|
||||||
i--
|
|
||||||
dAtA[i] = 0xa
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(dAtA) - i, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ReplicationRecord) Marshal() (dAtA []byte, err error) {
|
|
||||||
size := m.Size()
|
|
||||||
dAtA = make([]byte, size)
|
|
||||||
n, err := m.MarshalToSizedBuffer(dAtA[:size])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return dAtA[:n], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ReplicationRecord) MarshalTo(dAtA []byte) (int, error) {
|
|
||||||
size := m.Size()
|
|
||||||
return m.MarshalToSizedBuffer(dAtA[:size])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ReplicationRecord) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
|
||||||
i := len(dAtA)
|
|
||||||
_ = i
|
|
||||||
var l int
|
|
||||||
_ = l
|
|
||||||
if m.Seen != 0 {
|
|
||||||
i = encodeVarintDatabase(dAtA, i, uint64(m.Seen))
|
|
||||||
i--
|
|
||||||
dAtA[i] = 0x18
|
|
||||||
}
|
|
||||||
if len(m.Addresses) > 0 {
|
|
||||||
for iNdEx := len(m.Addresses) - 1; iNdEx >= 0; iNdEx-- {
|
|
||||||
{
|
|
||||||
size, err := m.Addresses[iNdEx].MarshalToSizedBuffer(dAtA[:i])
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
i -= size
|
|
||||||
i = encodeVarintDatabase(dAtA, i, uint64(size))
|
|
||||||
}
|
|
||||||
i--
|
|
||||||
dAtA[i] = 0x12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(m.Key) > 0 {
|
|
||||||
i -= len(m.Key)
|
|
||||||
copy(dAtA[i:], m.Key)
|
|
||||||
i = encodeVarintDatabase(dAtA, i, uint64(len(m.Key)))
|
|
||||||
i--
|
|
||||||
dAtA[i] = 0xa
|
|
||||||
}
|
|
||||||
return len(dAtA) - i, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DatabaseAddress) Marshal() (dAtA []byte, err error) {
|
|
||||||
size := m.Size()
|
|
||||||
dAtA = make([]byte, size)
|
|
||||||
n, err := m.MarshalToSizedBuffer(dAtA[:size])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return dAtA[:n], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DatabaseAddress) MarshalTo(dAtA []byte) (int, error) {
|
|
||||||
size := m.Size()
|
|
||||||
return m.MarshalToSizedBuffer(dAtA[:size])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DatabaseAddress) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
|
||||||
i := len(dAtA)
|
|
||||||
_ = i
|
|
||||||
var l int
|
|
||||||
_ = l
|
|
||||||
if m.Expires != 0 {
|
|
||||||
i = encodeVarintDatabase(dAtA, i, uint64(m.Expires))
|
|
||||||
i--
|
|
||||||
dAtA[i] = 0x10
|
|
||||||
}
|
|
||||||
if len(m.Address) > 0 {
|
|
||||||
i -= len(m.Address)
|
|
||||||
copy(dAtA[i:], m.Address)
|
|
||||||
i = encodeVarintDatabase(dAtA, i, uint64(len(m.Address)))
|
|
||||||
i--
|
|
||||||
dAtA[i] = 0xa
|
|
||||||
}
|
|
||||||
return len(dAtA) - i, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeVarintDatabase(dAtA []byte, offset int, v uint64) int {
|
|
||||||
offset -= sovDatabase(v)
|
|
||||||
base := offset
|
|
||||||
for v >= 1<<7 {
|
|
||||||
dAtA[offset] = uint8(v&0x7f | 0x80)
|
|
||||||
v >>= 7
|
|
||||||
offset++
|
|
||||||
}
|
|
||||||
dAtA[offset] = uint8(v)
|
|
||||||
return base
|
|
||||||
}
|
|
||||||
func (m *DatabaseRecord) Size() (n int) {
|
|
||||||
if m == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
var l int
|
|
||||||
_ = l
|
|
||||||
if len(m.Addresses) > 0 {
|
|
||||||
for _, e := range m.Addresses {
|
|
||||||
l = e.Size()
|
|
||||||
n += 1 + l + sovDatabase(uint64(l))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if m.Misses != 0 {
|
|
||||||
n += 1 + sovDatabase(uint64(m.Misses))
|
|
||||||
}
|
|
||||||
if m.Seen != 0 {
|
|
||||||
n += 1 + sovDatabase(uint64(m.Seen))
|
|
||||||
}
|
|
||||||
if m.Missed != 0 {
|
|
||||||
n += 1 + sovDatabase(uint64(m.Missed))
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ReplicationRecord) Size() (n int) {
|
|
||||||
if m == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
var l int
|
|
||||||
_ = l
|
|
||||||
l = len(m.Key)
|
|
||||||
if l > 0 {
|
|
||||||
n += 1 + l + sovDatabase(uint64(l))
|
|
||||||
}
|
|
||||||
if len(m.Addresses) > 0 {
|
|
||||||
for _, e := range m.Addresses {
|
|
||||||
l = e.Size()
|
|
||||||
n += 1 + l + sovDatabase(uint64(l))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if m.Seen != 0 {
|
|
||||||
n += 1 + sovDatabase(uint64(m.Seen))
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DatabaseAddress) Size() (n int) {
|
|
||||||
if m == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
var l int
|
|
||||||
_ = l
|
|
||||||
l = len(m.Address)
|
|
||||||
if l > 0 {
|
|
||||||
n += 1 + l + sovDatabase(uint64(l))
|
|
||||||
}
|
|
||||||
if m.Expires != 0 {
|
|
||||||
n += 1 + sovDatabase(uint64(m.Expires))
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func sovDatabase(x uint64) (n int) {
|
|
||||||
return (math_bits.Len64(x|1) + 6) / 7
|
|
||||||
}
|
|
||||||
func sozDatabase(x uint64) (n int) {
|
|
||||||
return sovDatabase(uint64((x << 1) ^ uint64((int64(x) >> 63))))
|
|
||||||
}
|
|
||||||
func (m *DatabaseRecord) Unmarshal(dAtA []byte) error {
|
|
||||||
l := len(dAtA)
|
|
||||||
iNdEx := 0
|
|
||||||
for iNdEx < l {
|
|
||||||
preIndex := iNdEx
|
|
||||||
var wire uint64
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
wire |= uint64(b&0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fieldNum := int32(wire >> 3)
|
|
||||||
wireType := int(wire & 0x7)
|
|
||||||
if wireType == 4 {
|
|
||||||
return fmt.Errorf("proto: DatabaseRecord: wiretype end group for non-group")
|
|
||||||
}
|
|
||||||
if fieldNum <= 0 {
|
|
||||||
return fmt.Errorf("proto: DatabaseRecord: illegal tag %d (wire type %d)", fieldNum, wire)
|
|
||||||
}
|
|
||||||
switch fieldNum {
|
|
||||||
case 1:
|
|
||||||
if wireType != 2 {
|
|
||||||
return fmt.Errorf("proto: wrong wireType = %d for field Addresses", wireType)
|
|
||||||
}
|
|
||||||
var msglen int
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
msglen |= int(b&0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if msglen < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
postIndex := iNdEx + msglen
|
|
||||||
if postIndex < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
if postIndex > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
m.Addresses = append(m.Addresses, DatabaseAddress{})
|
|
||||||
if err := m.Addresses[len(m.Addresses)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
iNdEx = postIndex
|
|
||||||
case 2:
|
|
||||||
if wireType != 0 {
|
|
||||||
return fmt.Errorf("proto: wrong wireType = %d for field Misses", wireType)
|
|
||||||
}
|
|
||||||
m.Misses = 0
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
m.Misses |= int32(b&0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 3:
|
|
||||||
if wireType != 0 {
|
|
||||||
return fmt.Errorf("proto: wrong wireType = %d for field Seen", wireType)
|
|
||||||
}
|
|
||||||
m.Seen = 0
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
m.Seen |= int64(b&0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 4:
|
|
||||||
if wireType != 0 {
|
|
||||||
return fmt.Errorf("proto: wrong wireType = %d for field Missed", wireType)
|
|
||||||
}
|
|
||||||
m.Missed = 0
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
m.Missed |= int64(b&0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
iNdEx = preIndex
|
|
||||||
skippy, err := skipDatabase(dAtA[iNdEx:])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
if (iNdEx + skippy) > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
iNdEx += skippy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if iNdEx > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (m *ReplicationRecord) Unmarshal(dAtA []byte) error {
|
|
||||||
l := len(dAtA)
|
|
||||||
iNdEx := 0
|
|
||||||
for iNdEx < l {
|
|
||||||
preIndex := iNdEx
|
|
||||||
var wire uint64
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
wire |= uint64(b&0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fieldNum := int32(wire >> 3)
|
|
||||||
wireType := int(wire & 0x7)
|
|
||||||
if wireType == 4 {
|
|
||||||
return fmt.Errorf("proto: ReplicationRecord: wiretype end group for non-group")
|
|
||||||
}
|
|
||||||
if fieldNum <= 0 {
|
|
||||||
return fmt.Errorf("proto: ReplicationRecord: illegal tag %d (wire type %d)", fieldNum, wire)
|
|
||||||
}
|
|
||||||
switch fieldNum {
|
|
||||||
case 1:
|
|
||||||
if wireType != 2 {
|
|
||||||
return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType)
|
|
||||||
}
|
|
||||||
var stringLen uint64
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
stringLen |= uint64(b&0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
intStringLen := int(stringLen)
|
|
||||||
if intStringLen < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
postIndex := iNdEx + intStringLen
|
|
||||||
if postIndex < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
if postIndex > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
m.Key = string(dAtA[iNdEx:postIndex])
|
|
||||||
iNdEx = postIndex
|
|
||||||
case 2:
|
|
||||||
if wireType != 2 {
|
|
||||||
return fmt.Errorf("proto: wrong wireType = %d for field Addresses", wireType)
|
|
||||||
}
|
|
||||||
var msglen int
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
msglen |= int(b&0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if msglen < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
postIndex := iNdEx + msglen
|
|
||||||
if postIndex < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
if postIndex > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
m.Addresses = append(m.Addresses, DatabaseAddress{})
|
|
||||||
if err := m.Addresses[len(m.Addresses)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
iNdEx = postIndex
|
|
||||||
case 3:
|
|
||||||
if wireType != 0 {
|
|
||||||
return fmt.Errorf("proto: wrong wireType = %d for field Seen", wireType)
|
|
||||||
}
|
|
||||||
m.Seen = 0
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
m.Seen |= int64(b&0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
iNdEx = preIndex
|
|
||||||
skippy, err := skipDatabase(dAtA[iNdEx:])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
if (iNdEx + skippy) > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
iNdEx += skippy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if iNdEx > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (m *DatabaseAddress) Unmarshal(dAtA []byte) error {
|
|
||||||
l := len(dAtA)
|
|
||||||
iNdEx := 0
|
|
||||||
for iNdEx < l {
|
|
||||||
preIndex := iNdEx
|
|
||||||
var wire uint64
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
wire |= uint64(b&0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fieldNum := int32(wire >> 3)
|
|
||||||
wireType := int(wire & 0x7)
|
|
||||||
if wireType == 4 {
|
|
||||||
return fmt.Errorf("proto: DatabaseAddress: wiretype end group for non-group")
|
|
||||||
}
|
|
||||||
if fieldNum <= 0 {
|
|
||||||
return fmt.Errorf("proto: DatabaseAddress: illegal tag %d (wire type %d)", fieldNum, wire)
|
|
||||||
}
|
|
||||||
switch fieldNum {
|
|
||||||
case 1:
|
|
||||||
if wireType != 2 {
|
|
||||||
return fmt.Errorf("proto: wrong wireType = %d for field Address", wireType)
|
|
||||||
}
|
|
||||||
var stringLen uint64
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
stringLen |= uint64(b&0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
intStringLen := int(stringLen)
|
|
||||||
if intStringLen < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
postIndex := iNdEx + intStringLen
|
|
||||||
if postIndex < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
if postIndex > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
m.Address = string(dAtA[iNdEx:postIndex])
|
|
||||||
iNdEx = postIndex
|
|
||||||
case 2:
|
|
||||||
if wireType != 0 {
|
|
||||||
return fmt.Errorf("proto: wrong wireType = %d for field Expires", wireType)
|
|
||||||
}
|
|
||||||
m.Expires = 0
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
m.Expires |= int64(b&0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
iNdEx = preIndex
|
|
||||||
skippy, err := skipDatabase(dAtA[iNdEx:])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
if (iNdEx + skippy) > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
iNdEx += skippy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if iNdEx > l {
|
|
||||||
return io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func skipDatabase(dAtA []byte) (n int, err error) {
|
|
||||||
l := len(dAtA)
|
|
||||||
iNdEx := 0
|
|
||||||
depth := 0
|
|
||||||
for iNdEx < l {
|
|
||||||
var wire uint64
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return 0, ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return 0, io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
wire |= (uint64(b) & 0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wireType := int(wire & 0x7)
|
|
||||||
switch wireType {
|
|
||||||
case 0:
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return 0, ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return 0, io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
iNdEx++
|
|
||||||
if dAtA[iNdEx-1] < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 1:
|
|
||||||
iNdEx += 8
|
|
||||||
case 2:
|
|
||||||
var length int
|
|
||||||
for shift := uint(0); ; shift += 7 {
|
|
||||||
if shift >= 64 {
|
|
||||||
return 0, ErrIntOverflowDatabase
|
|
||||||
}
|
|
||||||
if iNdEx >= l {
|
|
||||||
return 0, io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
b := dAtA[iNdEx]
|
|
||||||
iNdEx++
|
|
||||||
length |= (int(b) & 0x7F) << shift
|
|
||||||
if b < 0x80 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if length < 0 {
|
|
||||||
return 0, ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
iNdEx += length
|
|
||||||
case 3:
|
|
||||||
depth++
|
|
||||||
case 4:
|
|
||||||
if depth == 0 {
|
|
||||||
return 0, ErrUnexpectedEndOfGroupDatabase
|
|
||||||
}
|
|
||||||
depth--
|
|
||||||
case 5:
|
|
||||||
iNdEx += 4
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
|
|
||||||
}
|
|
||||||
if iNdEx < 0 {
|
|
||||||
return 0, ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
if depth == 0 {
|
|
||||||
return iNdEx, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidLengthDatabase = fmt.Errorf("proto: negative length found during unmarshaling")
|
|
||||||
ErrIntOverflowDatabase = fmt.Errorf("proto: integer overflow")
|
|
||||||
ErrUnexpectedEndOfGroupDatabase = fmt.Errorf("proto: unexpected end of group")
|
|
||||||
)
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// Copyright (C) 2018 The Syncthing Authors.
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package main;
|
|
||||||
|
|
||||||
import "repos/protobuf/gogoproto/gogo.proto";
|
|
||||||
|
|
||||||
option (gogoproto.goproto_getters_all) = false;
|
|
||||||
option (gogoproto.goproto_unkeyed_all) = false;
|
|
||||||
option (gogoproto.goproto_unrecognized_all) = false;
|
|
||||||
option (gogoproto.goproto_sizecache_all) = false;
|
|
||||||
|
|
||||||
message DatabaseRecord {
|
|
||||||
repeated DatabaseAddress addresses = 1 [(gogoproto.nullable) = false];
|
|
||||||
int32 misses = 2; // Number of lookups* without hits
|
|
||||||
int64 seen = 3; // Unix nanos, last device announce
|
|
||||||
int64 missed = 4; // Unix nanos, last* failed lookup
|
|
||||||
}
|
|
||||||
|
|
||||||
// *) Not every lookup results in a write, so may not be completely accurate
|
|
||||||
|
|
||||||
message ReplicationRecord {
|
|
||||||
string key = 1;
|
|
||||||
repeated DatabaseAddress addresses = 2 [(gogoproto.nullable) = false];
|
|
||||||
int64 seen = 3; // Unix nanos, last device announce
|
|
||||||
}
|
|
||||||
|
|
||||||
message DatabaseAddress {
|
|
||||||
string address = 1;
|
|
||||||
int64 expires = 2; // Unix nanos
|
|
||||||
}
|
|
||||||
+108
-55
@@ -11,29 +11,26 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/internal/gen/discosrv"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDatabaseGetSet(t *testing.T) {
|
func TestDatabaseGetSet(t *testing.T) {
|
||||||
db, err := newMemoryLevelDBStore()
|
db := newInMemoryStore(t.TempDir(), 0, nil)
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
go db.Serve(ctx)
|
go db.Serve(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Check missing record
|
// Check missing record
|
||||||
|
|
||||||
rec, err := db.get("abcd")
|
rec, err := db.get(&protocol.EmptyDeviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("not found should not be an error")
|
t.Error("not found should not be an error")
|
||||||
}
|
}
|
||||||
if len(rec.Addresses) != 0 {
|
if len(rec.Addresses) != 0 {
|
||||||
t.Error("addresses should be empty")
|
t.Error("addresses should be empty")
|
||||||
}
|
}
|
||||||
if rec.Misses != 0 {
|
|
||||||
t.Error("missing should be zero")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up a clock
|
// Set up a clock
|
||||||
|
|
||||||
@@ -43,16 +40,16 @@ func TestDatabaseGetSet(t *testing.T) {
|
|||||||
|
|
||||||
// Put a record
|
// Put a record
|
||||||
|
|
||||||
rec.Addresses = []DatabaseAddress{
|
rec.Addresses = []*discosrv.DatabaseAddress{
|
||||||
{Address: "tcp://1.2.3.4:5", Expires: tc.Now().Add(time.Minute).UnixNano()},
|
{Address: "tcp://1.2.3.4:5", Expires: tc.Now().Add(time.Minute).UnixNano()},
|
||||||
}
|
}
|
||||||
if err := db.put("abcd", rec); err != nil {
|
if err := db.put(&protocol.EmptyDeviceID, rec); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify it
|
// Verify it
|
||||||
|
|
||||||
rec, err = db.get("abcd")
|
rec, err = db.get(&protocol.EmptyDeviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -69,16 +66,16 @@ func TestDatabaseGetSet(t *testing.T) {
|
|||||||
|
|
||||||
tc.wind(30 * time.Second)
|
tc.wind(30 * time.Second)
|
||||||
|
|
||||||
addrs := []DatabaseAddress{
|
addrs := []*discosrv.DatabaseAddress{
|
||||||
{Address: "tcp://6.7.8.9:0", Expires: tc.Now().Add(time.Minute).UnixNano()},
|
{Address: "tcp://6.7.8.9:0", Expires: tc.Now().Add(time.Minute).UnixNano()},
|
||||||
}
|
}
|
||||||
if err := db.merge("abcd", addrs, tc.Now().UnixNano()); err != nil {
|
if err := db.merge(&protocol.EmptyDeviceID, addrs, tc.Now().UnixNano()); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify it
|
// Verify it
|
||||||
|
|
||||||
rec, err = db.get("abcd")
|
rec, err = db.get(&protocol.EmptyDeviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -101,7 +98,7 @@ func TestDatabaseGetSet(t *testing.T) {
|
|||||||
|
|
||||||
// Verify it
|
// Verify it
|
||||||
|
|
||||||
rec, err = db.get("abcd")
|
rec, err = db.get(&protocol.EmptyDeviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -114,40 +111,18 @@ func TestDatabaseGetSet(t *testing.T) {
|
|||||||
t.Error("incorrect address")
|
t.Error("incorrect address")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put a record with misses
|
|
||||||
|
|
||||||
rec = DatabaseRecord{Misses: 42, Missed: tc.Now().UnixNano()}
|
|
||||||
if err := db.put("efgh", rec); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify it
|
|
||||||
|
|
||||||
rec, err = db.get("efgh")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(rec.Addresses) != 0 {
|
|
||||||
t.Log(rec.Addresses)
|
|
||||||
t.Fatal("should have no addresses")
|
|
||||||
}
|
|
||||||
if rec.Misses != 42 {
|
|
||||||
t.Log(rec.Misses)
|
|
||||||
t.Error("incorrect misses")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set an address
|
// Set an address
|
||||||
|
|
||||||
addrs = []DatabaseAddress{
|
addrs = []*discosrv.DatabaseAddress{
|
||||||
{Address: "tcp://6.7.8.9:0", Expires: tc.Now().Add(time.Minute).UnixNano()},
|
{Address: "tcp://6.7.8.9:0", Expires: tc.Now().Add(time.Minute).UnixNano()},
|
||||||
}
|
}
|
||||||
if err := db.merge("efgh", addrs, tc.Now().UnixNano()); err != nil {
|
if err := db.merge(&protocol.GlobalDeviceID, addrs, tc.Now().UnixNano()); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify it
|
// Verify it
|
||||||
|
|
||||||
rec, err = db.get("efgh")
|
rec, err = db.get(&protocol.GlobalDeviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -155,48 +130,126 @@ func TestDatabaseGetSet(t *testing.T) {
|
|||||||
t.Log(rec.Addresses)
|
t.Log(rec.Addresses)
|
||||||
t.Fatal("should have one address")
|
t.Fatal("should have one address")
|
||||||
}
|
}
|
||||||
if rec.Misses != 0 {
|
|
||||||
t.Log(rec.Misses)
|
|
||||||
t.Error("should have no misses")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilter(t *testing.T) {
|
func TestFilter(t *testing.T) {
|
||||||
// all cases are expired with t=10
|
// all cases are expired with t=10
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
a []DatabaseAddress
|
a []*discosrv.DatabaseAddress
|
||||||
b []DatabaseAddress
|
b []*discosrv.DatabaseAddress
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
a: nil,
|
a: nil,
|
||||||
b: nil,
|
b: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
a: []DatabaseAddress{{Address: "a", Expires: 9}, {Address: "b", Expires: 9}, {Address: "c", Expires: 9}},
|
a: []*discosrv.DatabaseAddress{{Address: "a", Expires: 9}, {Address: "b", Expires: 9}, {Address: "c", Expires: 9}},
|
||||||
b: []DatabaseAddress{},
|
b: []*discosrv.DatabaseAddress{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
a: []DatabaseAddress{{Address: "a", Expires: 10}},
|
a: []*discosrv.DatabaseAddress{{Address: "a", Expires: 10}},
|
||||||
b: []DatabaseAddress{{Address: "a", Expires: 10}},
|
b: []*discosrv.DatabaseAddress{{Address: "a", Expires: 10}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
a: []DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
|
a: []*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
|
||||||
b: []DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
|
b: []*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
a: []DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}, {Address: "c", Expires: 5}, {Address: "d", Expires: 15}, {Address: "e", Expires: 5}},
|
a: []*discosrv.DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}, {Address: "c", Expires: 5}, {Address: "d", Expires: 15}, {Address: "e", Expires: 5}},
|
||||||
b: []DatabaseAddress{{Address: "d", Expires: 15}, {Address: "b", Expires: 15}}, // gets reordered
|
b: []*discosrv.DatabaseAddress{{Address: "b", Expires: 15}, {Address: "d", Expires: 15}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
res := expire(tc.a, 10)
|
res := expire(tc.a, time.Unix(0, 10))
|
||||||
if fmt.Sprint(res) != fmt.Sprint(tc.b) {
|
if fmt.Sprint(res) != fmt.Sprint(tc.b) {
|
||||||
t.Errorf("Incorrect result %v, expected %v", res, tc.b)
|
t.Errorf("Incorrect result %v, expected %v", res, tc.b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMerge(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
a, b, res []*discosrv.DatabaseAddress
|
||||||
|
}{
|
||||||
|
{nil, nil, nil},
|
||||||
|
{
|
||||||
|
nil,
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nil,
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 15}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 15}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "b", Expires: 15}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 15}, {Address: "b", Expires: 15}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 15}, {Address: "b", Expires: 15}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "b", Expires: 15}, {Address: "c", Expires: 20}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}, {Address: "c", Expires: 20}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "b", Expires: 5}, {Address: "c", Expires: 20}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}, {Address: "c", Expires: 20}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "y", Expires: 10}, {Address: "z", Expires: 10}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}, {Address: "y", Expires: 10}, {Address: "z", Expires: 10}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}, {Address: "d", Expires: 10}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "b", Expires: 5}, {Address: "c", Expires: 20}},
|
||||||
|
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}, {Address: "c", Expires: 20}, {Address: "d", Expires: 10}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
rec := merge(&discosrv.DatabaseRecord{Addresses: tc.a}, &discosrv.DatabaseRecord{Addresses: tc.b})
|
||||||
|
if fmt.Sprint(rec.Addresses) != fmt.Sprint(tc.res) {
|
||||||
|
t.Errorf("Incorrect result %v, expected %v", rec.Addresses, tc.res)
|
||||||
|
}
|
||||||
|
rec = merge(&discosrv.DatabaseRecord{Addresses: tc.b}, &discosrv.DatabaseRecord{Addresses: tc.a})
|
||||||
|
if fmt.Sprint(rec.Addresses) != fmt.Sprint(tc.res) {
|
||||||
|
t.Errorf("Incorrect result %v, expected %v", rec.Addresses, tc.res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMergeEqual(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
ar := []*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}}
|
||||||
|
br := []*discosrv.DatabaseAddress{{Address: "a", Expires: 15}, {Address: "b", Expires: 10}}
|
||||||
|
res := merge(&discosrv.DatabaseRecord{Addresses: ar}, &discosrv.DatabaseRecord{Addresses: br})
|
||||||
|
if len(res.Addresses) != 2 {
|
||||||
|
b.Fatal("wrong length")
|
||||||
|
}
|
||||||
|
if res.Addresses[0].Address != "a" || res.Addresses[1].Address != "b" {
|
||||||
|
b.Fatal("wrong address")
|
||||||
|
}
|
||||||
|
if res.Addresses[0].Expires != 15 || res.Addresses[1].Expires != 15 {
|
||||||
|
b.Fatal("wrong expiry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.ReportAllocs() // should be zero per operation
|
||||||
|
}
|
||||||
|
|
||||||
type testClock struct {
|
type testClock struct {
|
||||||
now time.Time
|
now time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
+99
-114
@@ -9,20 +9,26 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"flag"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
_ "net/http/pprof"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"github.com/thejerf/suture/v4"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/internal/blob"
|
||||||
|
"github.com/syncthing/syncthing/internal/blob/azureblob"
|
||||||
|
"github.com/syncthing/syncthing/internal/blob/s3"
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
"github.com/syncthing/syncthing/lib/build"
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/rand"
|
||||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
|
||||||
"github.com/thejerf/suture/v4"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -36,17 +42,12 @@ const (
|
|||||||
errorRetryAfterSeconds = 1500
|
errorRetryAfterSeconds = 1500
|
||||||
errorRetryFuzzSeconds = 300
|
errorRetryFuzzSeconds = 300
|
||||||
|
|
||||||
// Retry for not found is minSeconds + failures * incSeconds +
|
// Retry for not found is notFoundRetrySeenSeconds for records we have
|
||||||
// random(fuzz), where failures is the number of consecutive lookups
|
// seen an announcement for (but it's not active right now) and
|
||||||
// with no answer, up to maxSeconds. The fuzz is applied after capping
|
// notFoundRetryUnknownSeconds for records we have never seen (or not
|
||||||
// to maxSeconds.
|
// seen within the last week).
|
||||||
notFoundRetryMinSeconds = 60
|
notFoundRetryUnknownMinSeconds = 60
|
||||||
notFoundRetryMaxSeconds = 3540
|
notFoundRetryUnknownMaxSeconds = 3600
|
||||||
notFoundRetryIncSeconds = 10
|
|
||||||
notFoundRetryFuzzSeconds = 60
|
|
||||||
|
|
||||||
// How often (in requests) we serialize the missed counter to database.
|
|
||||||
notFoundMissesWriteInterval = 10
|
|
||||||
|
|
||||||
httpReadTimeout = 5 * time.Second
|
httpReadTimeout = 5 * time.Second
|
||||||
httpWriteTimeout = 5 * time.Second
|
httpWriteTimeout = 5 * time.Second
|
||||||
@@ -56,139 +57,123 @@ const (
|
|||||||
replicationOutboxSize = 10000
|
replicationOutboxSize = 10000
|
||||||
)
|
)
|
||||||
|
|
||||||
// These options make the database a little more optimized for writes, at
|
var debug = false
|
||||||
// the expense of some memory usage and risk of losing writes in a (system)
|
|
||||||
// crash.
|
type CLI struct {
|
||||||
var levelDBOptions = &opt.Options{
|
Cert string `group:"Listen" help:"Certificate file" default:"./cert.pem" env:"DISCOVERY_CERT_FILE"`
|
||||||
NoSync: true,
|
Key string `group:"Listen" help:"Key file" default:"./key.pem" env:"DISCOVERY_KEY_FILE"`
|
||||||
WriteBuffer: 32 << 20, // default 4<<20
|
HTTP bool `group:"Listen" help:"Listen on HTTP (behind an HTTPS proxy)" env:"DISCOVERY_HTTP"`
|
||||||
|
Compression bool `group:"Listen" help:"Enable GZIP compression of responses" env:"DISCOVERY_COMPRESSION"`
|
||||||
|
Listen string `group:"Listen" help:"Listen address" default:":8443" env:"DISCOVERY_LISTEN"`
|
||||||
|
MetricsListen string `group:"Listen" help:"Metrics listen address" env:"DISCOVERY_METRICS_LISTEN"`
|
||||||
|
DesiredNotFoundRate float64 `group:"Listen" help:"Desired maximum rate of not-found replies (/s)" default:"1000"`
|
||||||
|
|
||||||
|
DBDir string `group:"Database" help:"Database directory" default:"." env:"DISCOVERY_DB_DIR"`
|
||||||
|
DBFlushInterval time.Duration `group:"Database" help:"Interval between database flushes" default:"5m" env:"DISCOVERY_DB_FLUSH_INTERVAL"`
|
||||||
|
|
||||||
|
DBS3Endpoint string `name:"db-s3-endpoint" group:"Database (S3 backup)" hidden:"true" help:"S3 endpoint for database" env:"DISCOVERY_DB_S3_ENDPOINT"`
|
||||||
|
DBS3Region string `name:"db-s3-region" group:"Database (S3 backup)" hidden:"true" help:"S3 region for database" env:"DISCOVERY_DB_S3_REGION"`
|
||||||
|
DBS3Bucket string `name:"db-s3-bucket" group:"Database (S3 backup)" hidden:"true" help:"S3 bucket for database" env:"DISCOVERY_DB_S3_BUCKET"`
|
||||||
|
DBS3AccessKeyID string `name:"db-s3-access-key-id" group:"Database (S3 backup)" hidden:"true" help:"S3 access key ID for database" env:"DISCOVERY_DB_S3_ACCESS_KEY_ID"`
|
||||||
|
DBS3SecretKey string `name:"db-s3-secret-key" group:"Database (S3 backup)" hidden:"true" help:"S3 secret key for database" env:"DISCOVERY_DB_S3_SECRET_KEY"`
|
||||||
|
|
||||||
|
DBAzureBlobAccount string `name:"db-azure-blob-account" env:"DISCOVERY_DB_AZUREBLOB_ACCOUNT"`
|
||||||
|
DBAzureBlobKey string `name:"db-azure-blob-key" env:"DISCOVERY_DB_AZUREBLOB_KEY"`
|
||||||
|
DBAzureBlobContainer string `name:"db-azure-blob-container" env:"DISCOVERY_DB_AZUREBLOB_CONTAINER"`
|
||||||
|
|
||||||
|
AMQPAddress string `group:"AMQP replication" hidden:"true" help:"Address to AMQP broker" env:"DISCOVERY_AMQP_ADDRESS"`
|
||||||
|
|
||||||
|
Debug bool `short:"d" help:"Print debug output" env:"DISCOVERY_DEBUG"`
|
||||||
|
Version bool `short:"v" help:"Print version and exit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
debug = false
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var listen string
|
|
||||||
var dir string
|
|
||||||
var metricsListen string
|
|
||||||
var replicationListen string
|
|
||||||
var replicationPeers string
|
|
||||||
var certFile string
|
|
||||||
var keyFile string
|
|
||||||
var useHTTP bool
|
|
||||||
|
|
||||||
log.SetOutput(os.Stdout)
|
log.SetOutput(os.Stdout)
|
||||||
log.SetFlags(0)
|
|
||||||
|
|
||||||
flag.StringVar(&certFile, "cert", "./cert.pem", "Certificate file")
|
var cli CLI
|
||||||
flag.StringVar(&dir, "db-dir", "./discovery.db", "Database directory")
|
kong.Parse(&cli)
|
||||||
flag.BoolVar(&debug, "debug", false, "Print debug output")
|
debug = cli.Debug
|
||||||
flag.BoolVar(&useHTTP, "http", false, "Listen on HTTP (behind an HTTPS proxy)")
|
|
||||||
flag.StringVar(&listen, "listen", ":8443", "Listen address")
|
|
||||||
flag.StringVar(&keyFile, "key", "./key.pem", "Key file")
|
|
||||||
flag.StringVar(&metricsListen, "metrics-listen", "", "Metrics listen address")
|
|
||||||
flag.StringVar(&replicationPeers, "replicate", "", "Replication peers, id@address, comma separated")
|
|
||||||
flag.StringVar(&replicationListen, "replication-listen", ":19200", "Replication listen address")
|
|
||||||
showVersion := flag.Bool("version", false, "Show version")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
log.Println(build.LongVersionFor("stdiscosrv"))
|
log.Println(build.LongVersionFor("stdiscosrv"))
|
||||||
if *showVersion {
|
if cli.Version {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
buildInfo.WithLabelValues(build.Version, runtime.Version(), build.User, build.Date.UTC().Format("2006-01-02T15:04:05Z")).Set(1)
|
||||||
if os.IsNotExist(err) {
|
|
||||||
log.Println("Failed to load keypair. Generating one, this might take a while...")
|
|
||||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, "stdiscosrv", 20*365)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Failed to generate X509 key pair:", err)
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
log.Fatalln("Failed to load keypair:", err)
|
|
||||||
}
|
|
||||||
devID := protocol.NewDeviceID(cert.Certificate[0])
|
|
||||||
log.Println("Server device ID is", devID)
|
|
||||||
|
|
||||||
// Parse the replication specs, if any.
|
var cert tls.Certificate
|
||||||
var allowedReplicationPeers []protocol.DeviceID
|
if !cli.HTTP {
|
||||||
var replicationDestinations []string
|
var err error
|
||||||
parts := strings.Split(replicationPeers, ",")
|
cert, err = tls.LoadX509KeyPair(cli.Cert, cli.Key)
|
||||||
for _, part := range parts {
|
if os.IsNotExist(err) {
|
||||||
if part == "" {
|
log.Println("Failed to load keypair. Generating one, this might take a while...")
|
||||||
continue
|
cert, err = tlsutil.NewCertificate(cli.Cert, cli.Key, "stdiscosrv", 20*365)
|
||||||
}
|
|
||||||
|
|
||||||
fields := strings.Split(part, "@")
|
|
||||||
switch len(fields) {
|
|
||||||
case 2:
|
|
||||||
// This is an id@address specification. Grab the address for the
|
|
||||||
// destination list. Try to resolve it once to catch obvious
|
|
||||||
// syntax errors here rather than having the sender service fail
|
|
||||||
// repeatedly later.
|
|
||||||
_, err := net.ResolveTCPAddr("tcp", fields[1])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Resolving address:", err)
|
log.Fatalln("Failed to generate X509 key pair:", err)
|
||||||
}
|
}
|
||||||
replicationDestinations = append(replicationDestinations, fields[1])
|
} else if err != nil {
|
||||||
fallthrough // N.B.
|
log.Fatalln("Failed to load keypair:", err)
|
||||||
|
|
||||||
case 1:
|
|
||||||
// The first part is always a device ID.
|
|
||||||
id, err := protocol.DeviceIDFromString(fields[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Parsing device ID:", err)
|
|
||||||
}
|
|
||||||
if id == protocol.EmptyDeviceID {
|
|
||||||
log.Fatalf("Missing device ID for peer in %q", part)
|
|
||||||
}
|
|
||||||
allowedReplicationPeers = append(allowedReplicationPeers, id)
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Fatalln("Unrecognized replication spec:", part)
|
|
||||||
}
|
}
|
||||||
|
devID := protocol.NewDeviceID(cert.Certificate[0])
|
||||||
|
log.Println("Server device ID is", devID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Root of the service tree.
|
// Root of the service tree.
|
||||||
main := suture.New("main", suture.Spec{
|
main := suture.New("main", suture.Spec{
|
||||||
PassThroughPanics: true,
|
PassThroughPanics: true,
|
||||||
|
Timeout: 2 * time.Minute,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start the database.
|
// If configured, use blob storage for database backups.
|
||||||
db, err := newLevelDBStore(dir)
|
var blobs blob.Store
|
||||||
if err != nil {
|
var err error
|
||||||
log.Fatalln("Open database:", err)
|
if cli.DBS3Endpoint != "" {
|
||||||
|
blobs, err = s3.NewSession(cli.DBS3Endpoint, cli.DBS3Region, cli.DBS3Bucket, cli.DBS3AccessKeyID, cli.DBS3SecretKey)
|
||||||
|
} else if cli.DBAzureBlobAccount != "" {
|
||||||
|
blobs, err = azureblob.NewBlobStore(cli.DBAzureBlobAccount, cli.DBAzureBlobKey, cli.DBAzureBlobContainer)
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create blob store: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the database.
|
||||||
|
db := newInMemoryStore(cli.DBDir, cli.DBFlushInterval, blobs)
|
||||||
main.Add(db)
|
main.Add(db)
|
||||||
|
|
||||||
// Start any replication senders.
|
// If we have an AMQP broker for replication, start that
|
||||||
var repl replicationMultiplexer
|
var repl replicator
|
||||||
for _, dst := range replicationDestinations {
|
if cli.AMQPAddress != "" {
|
||||||
rs := newReplicationSender(dst, cert, allowedReplicationPeers)
|
clientID := rand.String(10)
|
||||||
main.Add(rs)
|
kr := newAMQPReplicator(cli.AMQPAddress, clientID, db)
|
||||||
repl = append(repl, rs)
|
main.Add(kr)
|
||||||
}
|
repl = kr
|
||||||
|
|
||||||
// If we have replication configured, start the replication listener.
|
|
||||||
if len(allowedReplicationPeers) > 0 {
|
|
||||||
rl := newReplicationListener(replicationListen, cert, allowedReplicationPeers, db)
|
|
||||||
main.Add(rl)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the main API server.
|
// Start the main API server.
|
||||||
qs := newAPISrv(listen, cert, db, repl, useHTTP)
|
qs := newAPISrv(cli.Listen, cert, db, repl, cli.HTTP, cli.Compression, cli.DesiredNotFoundRate)
|
||||||
main.Add(qs)
|
main.Add(qs)
|
||||||
|
|
||||||
// If we have a metrics port configured, start a metrics handler.
|
// If we have a metrics port configured, start a metrics handler.
|
||||||
if metricsListen != "" {
|
if cli.MetricsListen != "" {
|
||||||
go func() {
|
go func() {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("/metrics", promhttp.Handler())
|
mux.Handle("/metrics", promhttp.Handler())
|
||||||
log.Fatal(http.ListenAndServe(metricsListen, mux))
|
log.Fatal(http.ListenAndServe(cli.MetricsListen, mux))
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Cancel on signal
|
||||||
|
signalChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalChan, os.Interrupt)
|
||||||
|
go func() {
|
||||||
|
sig := <-signalChan
|
||||||
|
log.Printf("Received signal %s; shutting down", sig)
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
// Engage!
|
// Engage!
|
||||||
main.Serve(context.Background())
|
main.Serve(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,315 +0,0 @@
|
|||||||
// Copyright (C) 2018 The Syncthing Authors.
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
io "io"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
const replicationReadTimeout = time.Minute
|
|
||||||
const replicationHeartbeatInterval = time.Second * 30
|
|
||||||
|
|
||||||
type replicator interface {
|
|
||||||
send(key string, addrs []DatabaseAddress, seen int64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// a replicationSender tries to connect to the remote address and provide
|
|
||||||
// them with a feed of replication updates.
|
|
||||||
type replicationSender struct {
|
|
||||||
dst string
|
|
||||||
cert tls.Certificate // our certificate
|
|
||||||
allowedIDs []protocol.DeviceID
|
|
||||||
outbox chan ReplicationRecord
|
|
||||||
}
|
|
||||||
|
|
||||||
func newReplicationSender(dst string, cert tls.Certificate, allowedIDs []protocol.DeviceID) *replicationSender {
|
|
||||||
return &replicationSender{
|
|
||||||
dst: dst,
|
|
||||||
cert: cert,
|
|
||||||
allowedIDs: allowedIDs,
|
|
||||||
outbox: make(chan ReplicationRecord, replicationOutboxSize),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *replicationSender) Serve(ctx context.Context) error {
|
|
||||||
// Sleep a little at startup. Peers often restart at the same time, and
|
|
||||||
// this avoid the service failing and entering backoff state
|
|
||||||
// unnecessarily, while also reducing the reconnect rate to something
|
|
||||||
// reasonable by default.
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
|
||||||
tlsCfg := &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{s.cert},
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial the TLS connection.
|
|
||||||
conn, err := tls.Dial("tcp", s.dst, tlsCfg)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Replication connect:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
conn.SetWriteDeadline(time.Now().Add(time.Second))
|
|
||||||
conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Get the other side device ID.
|
|
||||||
remoteID, err := deviceID(conn)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Replication connect:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify it's in the set of allowed device IDs.
|
|
||||||
if !deviceIDIn(remoteID, s.allowedIDs) {
|
|
||||||
log.Println("Replication connect: unexpected device ID:", remoteID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
heartBeatTicker := time.NewTicker(replicationHeartbeatInterval)
|
|
||||||
defer heartBeatTicker.Stop()
|
|
||||||
|
|
||||||
// Send records.
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-heartBeatTicker.C:
|
|
||||||
if len(s.outbox) > 0 {
|
|
||||||
// No need to send heartbeats if there are events/prevrious
|
|
||||||
// heartbeats to send, they will keep the connection alive.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Empty replication message is the heartbeat:
|
|
||||||
s.outbox <- ReplicationRecord{}
|
|
||||||
|
|
||||||
case rec := <-s.outbox:
|
|
||||||
// Buffer must hold record plus four bytes for size
|
|
||||||
size := rec.Size()
|
|
||||||
if len(buf) < size+4 {
|
|
||||||
buf = make([]byte, size+4)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record comes after the four bytes size
|
|
||||||
n, err := rec.MarshalTo(buf[4:])
|
|
||||||
if err != nil {
|
|
||||||
// odd to get an error here, but we haven't sent anything
|
|
||||||
// yet so it's not fatal
|
|
||||||
replicationSendsTotal.WithLabelValues("error").Inc()
|
|
||||||
log.Println("Replication marshal:", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
binary.BigEndian.PutUint32(buf, uint32(n))
|
|
||||||
|
|
||||||
// Send
|
|
||||||
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
|
||||||
if _, err := conn.Write(buf[:4+n]); err != nil {
|
|
||||||
replicationSendsTotal.WithLabelValues("error").Inc()
|
|
||||||
log.Println("Replication write:", err)
|
|
||||||
// Yes, we are losing the replication event here.
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
replicationSendsTotal.WithLabelValues("success").Inc()
|
|
||||||
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *replicationSender) String() string {
|
|
||||||
return fmt.Sprintf("replicationSender(%q)", s.dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *replicationSender) send(key string, ps []DatabaseAddress, _ int64) {
|
|
||||||
item := ReplicationRecord{
|
|
||||||
Key: key,
|
|
||||||
Addresses: ps,
|
|
||||||
}
|
|
||||||
|
|
||||||
// The send should never block. The inbox is suitably buffered for at
|
|
||||||
// least a few seconds of stalls, which shouldn't happen in practice.
|
|
||||||
select {
|
|
||||||
case s.outbox <- item:
|
|
||||||
default:
|
|
||||||
replicationSendsTotal.WithLabelValues("drop").Inc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// a replicationMultiplexer sends to multiple replicators
|
|
||||||
type replicationMultiplexer []replicator
|
|
||||||
|
|
||||||
func (m replicationMultiplexer) send(key string, ps []DatabaseAddress, seen int64) {
|
|
||||||
for _, s := range m {
|
|
||||||
// each send is nonblocking
|
|
||||||
s.send(key, ps, seen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// replicationListener accepts incoming connections and reads replication
|
|
||||||
// items from them. Incoming items are applied to the KV store.
|
|
||||||
type replicationListener struct {
|
|
||||||
addr string
|
|
||||||
cert tls.Certificate
|
|
||||||
allowedIDs []protocol.DeviceID
|
|
||||||
db database
|
|
||||||
}
|
|
||||||
|
|
||||||
func newReplicationListener(addr string, cert tls.Certificate, allowedIDs []protocol.DeviceID, db database) *replicationListener {
|
|
||||||
return &replicationListener{
|
|
||||||
addr: addr,
|
|
||||||
cert: cert,
|
|
||||||
allowedIDs: allowedIDs,
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *replicationListener) Serve(ctx context.Context) error {
|
|
||||||
tlsCfg := &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{l.cert},
|
|
||||||
ClientAuth: tls.RequestClientCert,
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
lst, err := tls.Listen("tcp", l.addr, tlsCfg)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Replication listen:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer lst.Close()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept a connection
|
|
||||||
conn, err := lst.Accept()
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Replication accept:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out the other side device ID
|
|
||||||
remoteID, err := deviceID(conn.(*tls.Conn))
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Replication accept:", err)
|
|
||||||
conn.SetWriteDeadline(time.Now().Add(time.Second))
|
|
||||||
conn.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify it is in the set of allowed device IDs
|
|
||||||
if !deviceIDIn(remoteID, l.allowedIDs) {
|
|
||||||
log.Println("Replication accept: unexpected device ID:", remoteID)
|
|
||||||
conn.SetWriteDeadline(time.Now().Add(time.Second))
|
|
||||||
conn.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
go l.handle(ctx, conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *replicationListener) String() string {
|
|
||||||
return fmt.Sprintf("replicationListener(%q)", l.addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *replicationListener) handle(ctx context.Context, conn net.Conn) {
|
|
||||||
defer func() {
|
|
||||||
conn.SetWriteDeadline(time.Now().Add(time.Second))
|
|
||||||
conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.SetReadDeadline(time.Now().Add(replicationReadTimeout))
|
|
||||||
|
|
||||||
// First four bytes are the size
|
|
||||||
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
|
|
||||||
log.Println("Replication read size:", err)
|
|
||||||
replicationRecvsTotal.WithLabelValues("error").Inc()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the rest of the record
|
|
||||||
size := int(binary.BigEndian.Uint32(buf[:4]))
|
|
||||||
if len(buf) < size {
|
|
||||||
buf = make([]byte, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
if size == 0 {
|
|
||||||
// Heartbeat, ignore
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := io.ReadFull(conn, buf[:size]); err != nil {
|
|
||||||
log.Println("Replication read record:", err)
|
|
||||||
replicationRecvsTotal.WithLabelValues("error").Inc()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal
|
|
||||||
var rec ReplicationRecord
|
|
||||||
if err := rec.Unmarshal(buf[:size]); err != nil {
|
|
||||||
log.Println("Replication unmarshal:", err)
|
|
||||||
replicationRecvsTotal.WithLabelValues("error").Inc()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store
|
|
||||||
l.db.merge(rec.Key, rec.Addresses, rec.Seen)
|
|
||||||
replicationRecvsTotal.WithLabelValues("success").Inc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func deviceID(conn *tls.Conn) (protocol.DeviceID, error) {
|
|
||||||
// Handshake may not be complete on the server side yet, which we need
|
|
||||||
// to get the client certificate.
|
|
||||||
if !conn.ConnectionState().HandshakeComplete {
|
|
||||||
if err := conn.Handshake(); err != nil {
|
|
||||||
return protocol.DeviceID{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect exactly one certificate.
|
|
||||||
certs := conn.ConnectionState().PeerCertificates
|
|
||||||
if len(certs) != 1 {
|
|
||||||
return protocol.DeviceID{}, fmt.Errorf("unexpected number of certificates (%d != 1)", len(certs))
|
|
||||||
}
|
|
||||||
|
|
||||||
return protocol.NewDeviceID(certs[0].Raw), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deviceIDIn(id protocol.DeviceID, ids []protocol.DeviceID) bool {
|
|
||||||
for _, candidate := range ids {
|
|
||||||
if id == candidate {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
+36
-17
@@ -7,13 +7,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
buildInfo = prometheus.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "discovery",
|
||||||
|
Name: "build_info",
|
||||||
|
Help: "A metric with a constant '1' value labeled by version, goversion, builduser and builddate from which stdiscosrv was built.",
|
||||||
|
}, []string{"version", "goversion", "builduser", "builddate"})
|
||||||
|
|
||||||
apiRequestsTotal = prometheus.NewCounterVec(
|
apiRequestsTotal = prometheus.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Namespace: "syncthing",
|
Namespace: "syncthing",
|
||||||
@@ -90,6 +95,29 @@ var (
|
|||||||
Help: "Latency of database operations.",
|
Help: "Latency of database operations.",
|
||||||
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
|
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
|
||||||
}, []string{"operation"})
|
}, []string{"operation"})
|
||||||
|
|
||||||
|
databaseWriteSeconds = prometheus.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "discovery",
|
||||||
|
Name: "database_write_seconds",
|
||||||
|
Help: "Time spent writing the database.",
|
||||||
|
})
|
||||||
|
databaseLastWritten = prometheus.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "discovery",
|
||||||
|
Name: "database_last_written",
|
||||||
|
Help: "Timestamp of the last successful database write.",
|
||||||
|
})
|
||||||
|
|
||||||
|
retryAfterLevel = prometheus.NewGaugeVec(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "discovery",
|
||||||
|
Name: "retry_after_seconds",
|
||||||
|
Help: "Retry-After header value in seconds.",
|
||||||
|
}, []string{"name"})
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -104,21 +132,12 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
prometheus.MustRegister(apiRequestsTotal, apiRequestsSeconds,
|
prometheus.MustRegister(buildInfo,
|
||||||
|
apiRequestsTotal, apiRequestsSeconds,
|
||||||
lookupRequestsTotal, announceRequestsTotal,
|
lookupRequestsTotal, announceRequestsTotal,
|
||||||
replicationSendsTotal, replicationRecvsTotal,
|
replicationSendsTotal, replicationRecvsTotal,
|
||||||
databaseKeys, databaseStatisticsSeconds,
|
databaseKeys, databaseStatisticsSeconds,
|
||||||
databaseOperations, databaseOperationSeconds)
|
databaseOperations, databaseOperationSeconds,
|
||||||
|
databaseWriteSeconds, databaseLastWritten,
|
||||||
processCollectorOpts := collectors.ProcessCollectorOpts{
|
retryAfterLevel)
|
||||||
Namespace: "syncthing_discovery",
|
|
||||||
PidFn: func() (int, error) {
|
|
||||||
return os.Getpid(), nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
prometheus.MustRegister(
|
|
||||||
collectors.NewProcessCollector(processCollectorOpts),
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
|
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
|
||||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/relay/protocol"
|
"github.com/syncthing/syncthing/lib/relay/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -36,8 +35,14 @@ func listener(_, addr string, config *tls.Config, token string) {
|
|||||||
for {
|
for {
|
||||||
conn, isTLS, err := listener.AcceptNoWrapTLS()
|
conn, isTLS, err := listener.AcceptNoWrapTLS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Conn may be nil if accept failed, or non-nil if the initial
|
||||||
|
// read to figure out if it's TLS or not failed. In the latter
|
||||||
|
// case, close the connection before moving on.
|
||||||
|
if conn != nil {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
if debug {
|
if debug {
|
||||||
log.Println("Listener failed to accept connection from", conn.RemoteAddr(), ". Possibly a TCP Ping.")
|
log.Println("Listener failed to accept:", err)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -179,7 +184,7 @@ func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config, token strin
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// requestedPeer is the server, id is the client
|
// requestedPeer is the server, id is the client
|
||||||
ses := newSession(requestedPeer, id, sessionLimiter, globalLimiter)
|
ses := newSession(requestedPeer, id, sessionLimitBps, globalLimiter)
|
||||||
|
|
||||||
go ses.Serve()
|
go ses.Serve()
|
||||||
|
|
||||||
|
|||||||
+16
-12
@@ -19,19 +19,19 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/build"
|
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
|
||||||
"github.com/syncthing/syncthing/lib/relay/protocol"
|
|
||||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
"github.com/syncthing/syncthing/lib/nat"
|
"github.com/syncthing/syncthing/lib/nat"
|
||||||
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
_ "github.com/syncthing/syncthing/lib/pmp"
|
_ "github.com/syncthing/syncthing/lib/pmp"
|
||||||
_ "github.com/syncthing/syncthing/lib/upnp"
|
|
||||||
|
|
||||||
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
|
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/relay/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||||
|
_ "github.com/syncthing/syncthing/lib/upnp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -51,7 +51,6 @@ var (
|
|||||||
globalLimitBps int
|
globalLimitBps int
|
||||||
overLimit atomic.Bool
|
overLimit atomic.Bool
|
||||||
descriptorLimit int64
|
descriptorLimit int64
|
||||||
sessionLimiter *rate.Limiter
|
|
||||||
globalLimiter *rate.Limiter
|
globalLimiter *rate.Limiter
|
||||||
networkBufferSize int
|
networkBufferSize int
|
||||||
|
|
||||||
@@ -194,7 +193,15 @@ func main() {
|
|||||||
cfg.Options.NATTimeoutS = natTimeout
|
cfg.Options.NATTimeoutS = natTimeout
|
||||||
})
|
})
|
||||||
natSvc := nat.NewService(id, wrapper)
|
natSvc := nat.NewService(id, wrapper)
|
||||||
mapping := mapping{natSvc.NewMapping(nat.TCP, addr.IP, addr.Port)}
|
var ipVersion nat.IPVersion
|
||||||
|
if strings.HasSuffix(proto, "4") {
|
||||||
|
ipVersion = nat.IPv4Only
|
||||||
|
} else if strings.HasSuffix(proto, "6") {
|
||||||
|
ipVersion = nat.IPv6Only
|
||||||
|
} else {
|
||||||
|
ipVersion = nat.IPvAny
|
||||||
|
}
|
||||||
|
mapping := mapping{natSvc.NewMapping(nat.TCP, ipVersion, addr.IP, addr.Port)}
|
||||||
|
|
||||||
if natEnabled {
|
if natEnabled {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -220,9 +227,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sessionLimitBps > 0 {
|
|
||||||
sessionLimiter = rate.NewLimiter(rate.Limit(sessionLimitBps), 2*sessionLimitBps)
|
|
||||||
}
|
|
||||||
if globalLimitBps > 0 {
|
if globalLimitBps > 0 {
|
||||||
globalLimiter = rate.NewLimiter(rate.Limit(globalLimitBps), 2*globalLimitBps)
|
globalLimiter = rate.NewLimiter(rate.Limit(globalLimitBps), 2*globalLimitBps)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ var (
|
|||||||
bytesProxied atomic.Int64
|
bytesProxied atomic.Int64
|
||||||
)
|
)
|
||||||
|
|
||||||
func newSession(serverid, clientid syncthingprotocol.DeviceID, sessionRateLimit, globalRateLimit *rate.Limiter) *session {
|
func newSession(serverid, clientid syncthingprotocol.DeviceID, sessionLimitBps int, globalRateLimit *rate.Limiter) *session {
|
||||||
serverkey := make([]byte, 32)
|
serverkey := make([]byte, 32)
|
||||||
_, err := rand.Read(serverkey)
|
_, err := rand.Read(serverkey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -40,12 +40,17 @@ func newSession(serverid, clientid syncthingprotocol.DeviceID, sessionRateLimit,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sessionRateLimit *rate.Limiter
|
||||||
|
if sessionLimitBps > 0 {
|
||||||
|
sessionRateLimit = rate.NewLimiter(rate.Limit(sessionLimitBps), 2*sessionLimitBps)
|
||||||
|
}
|
||||||
ses := &session{
|
ses := &session{
|
||||||
serverkey: serverkey,
|
serverkey: serverkey,
|
||||||
serverid: serverid,
|
serverid: serverid,
|
||||||
clientkey: clientkey,
|
clientkey: clientkey,
|
||||||
clientid: clientid,
|
clientid: clientid,
|
||||||
rateLimit: makeRateLimitFunc(sessionRateLimit, globalRateLimit),
|
rateLimit: makeRateLimitFunc(sessionRateLimit, globalRateLimit),
|
||||||
|
limiter: sessionRateLimit,
|
||||||
connsChan: make(chan net.Conn),
|
connsChan: make(chan net.Conn),
|
||||||
conns: make([]net.Conn, 0, 2),
|
conns: make([]net.Conn, 0, 2),
|
||||||
}
|
}
|
||||||
@@ -68,7 +73,6 @@ func findSession(key string) *session {
|
|||||||
ses, ok := pendingSessions[key]
|
ses, ok := pendingSessions[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
delete(pendingSessions, key)
|
delete(pendingSessions, key)
|
||||||
return ses
|
return ses
|
||||||
@@ -110,6 +114,7 @@ type session struct {
|
|||||||
clientid syncthingprotocol.DeviceID
|
clientid syncthingprotocol.DeviceID
|
||||||
|
|
||||||
rateLimit func(bytes int)
|
rateLimit func(bytes int)
|
||||||
|
limiter *rate.Limiter
|
||||||
|
|
||||||
connsChan chan net.Conn
|
connsChan chan net.Conn
|
||||||
conns []net.Conn
|
conns []net.Conn
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||||
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
|
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
|
||||||
"github.com/syncthing/syncthing/lib/relay/client"
|
"github.com/syncthing/syncthing/lib/relay/client"
|
||||||
"github.com/syncthing/syncthing/lib/relay/protocol"
|
"github.com/syncthing/syncthing/lib/relay/protocol"
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
// Copyright (C) 2019 The Syncthing Authors.
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
|
||||||
"github.com/syncthing/syncthing/lib/httpcache"
|
|
||||||
"github.com/syncthing/syncthing/lib/upgrade"
|
|
||||||
)
|
|
||||||
|
|
||||||
type cli struct {
|
|
||||||
Listen string `default:":8080" help:"Listen address"`
|
|
||||||
URL string `short:"u" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=25" help:"GitHub releases url"`
|
|
||||||
Forward []string `short:"f" help:"Forwarded pages, format: /path->https://example/com/url"`
|
|
||||||
CacheTime time.Duration `default:"15m" help:"Cache time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var params cli
|
|
||||||
kong.Parse(¶ms)
|
|
||||||
if err := server(¶ms); err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func server(params *cli) error {
|
|
||||||
http.Handle("/meta.json", httpcache.SinglePath(&githubReleases{url: params.URL}, params.CacheTime))
|
|
||||||
|
|
||||||
for _, fwd := range params.Forward {
|
|
||||||
path, url, ok := strings.Cut(fwd, "->")
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid forward: %q", fwd)
|
|
||||||
}
|
|
||||||
http.Handle(path, httpcache.SinglePath(&proxy{url: url}, params.CacheTime))
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.ListenAndServe(params.Listen, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
type githubReleases struct {
|
|
||||||
url string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *githubReleases) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
||||||
log.Println("Fetching", p.url)
|
|
||||||
rels := upgrade.FetchLatestReleases(p.url, "")
|
|
||||||
if rels == nil {
|
|
||||||
http.Error(w, "no releases", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(upgrade.SortByRelease(rels))
|
|
||||||
rels = filterForLatest(rels)
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
_ = json.NewEncoder(buf).Encode(rels)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
|
||||||
w.Write(buf.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
type proxy struct {
|
|
||||||
url string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
||||||
log.Println("Fetching", p.url)
|
|
||||||
req, err := http.NewRequestWithContext(req.Context(), http.MethodGet, p.url, nil)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
ct := resp.Header.Get("Content-Type")
|
|
||||||
w.Header().Set("Content-Type", ct)
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
|
||||||
w.Header().Set("Cache-Control", "public, max-age=900")
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET")
|
|
||||||
}
|
|
||||||
w.WriteHeader(resp.StatusCode)
|
|
||||||
if strings.HasPrefix(ct, "application/json") {
|
|
||||||
// Special JSON handling; clean it up a bit.
|
|
||||||
var v interface{}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = json.NewEncoder(w).Encode(v)
|
|
||||||
} else {
|
|
||||||
_, _ = io.Copy(w, resp.Body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterForLatest returns the latest stable and prerelease only. If the
|
|
||||||
// stable version is newer (comes first in the list) there is no need to go
|
|
||||||
// looking for a prerelease at all.
|
|
||||||
func filterForLatest(rels []upgrade.Release) []upgrade.Release {
|
|
||||||
var filtered []upgrade.Release
|
|
||||||
var havePre bool
|
|
||||||
for _, rel := range rels {
|
|
||||||
if !rel.Prerelease {
|
|
||||||
// We found a stable version, we're good now.
|
|
||||||
filtered = append(filtered, rel)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if rel.Prerelease && !havePre {
|
|
||||||
// We remember the first prerelease we find.
|
|
||||||
filtered = append(filtered, rel)
|
|
||||||
havePre = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
+21
-11
@@ -13,6 +13,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/AudriusButkevicius/recli"
|
"github.com/AudriusButkevicius/recli"
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
@@ -23,9 +24,20 @@ type configHandler struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfigCommand(f *apiClientFactory) (cli.Command, error) {
|
type configCommand struct {
|
||||||
|
Args []string `arg:"" default:"-h"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configCommand) Run(ctx Context, _ *kong.Context) error {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "syncthing"
|
||||||
|
app.Author = "The Syncthing Authors"
|
||||||
|
app.Metadata = map[string]interface{}{
|
||||||
|
"clientFactory": ctx.clientFactory,
|
||||||
|
}
|
||||||
|
|
||||||
h := new(configHandler)
|
h := new(configHandler)
|
||||||
h.client, h.err = f.getClient()
|
h.client, h.err = ctx.clientFactory.getClient()
|
||||||
if h.err == nil {
|
if h.err == nil {
|
||||||
h.cfg, h.err = getConfig(h.client)
|
h.cfg, h.err = getConfig(h.client)
|
||||||
}
|
}
|
||||||
@@ -38,17 +50,15 @@ func getConfigCommand(f *apiClientFactory) (cli.Command, error) {
|
|||||||
|
|
||||||
commands, err := recli.New(recliCfg).Construct(&h.cfg)
|
commands, err := recli.New(recliCfg).Construct(&h.cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.Command{}, fmt.Errorf("config reflect: %w", err)
|
return fmt.Errorf("config reflect: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cli.Command{
|
app.Commands = commands
|
||||||
Name: "config",
|
app.HideHelp = true
|
||||||
HideHelp: true,
|
app.Before = h.configBefore
|
||||||
Usage: "Configuration modification command group",
|
app.After = h.configAfter
|
||||||
Subcommands: commands,
|
|
||||||
Before: h.configBefore,
|
return app.Run(append([]string{app.Name}, c.Args...))
|
||||||
After: h.configAfter,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *configHandler) configBefore(c *cli.Context) error {
|
func (h *configHandler) configBefore(c *cli.Context) error {
|
||||||
|
|||||||
+26
-36
@@ -9,47 +9,37 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var debugCommand = cli.Command{
|
type fileCommand struct {
|
||||||
Name: "debug",
|
FolderID string `arg:""`
|
||||||
HideHelp: true,
|
Path string `arg:""`
|
||||||
Usage: "Debug command group",
|
|
||||||
Subcommands: []cli.Command{
|
|
||||||
{
|
|
||||||
Name: "file",
|
|
||||||
Usage: "Show information about a file (or directory/symlink)",
|
|
||||||
ArgsUsage: "FOLDER-ID PATH",
|
|
||||||
Action: expects(2, debugFile()),
|
|
||||||
},
|
|
||||||
indexCommand,
|
|
||||||
{
|
|
||||||
Name: "profile",
|
|
||||||
Usage: "Save a profile to help figuring out what Syncthing does.",
|
|
||||||
ArgsUsage: "cpu | heap",
|
|
||||||
Action: expects(1, profile()),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func debugFile() cli.ActionFunc {
|
func (f *fileCommand) Run(ctx Context) error {
|
||||||
return func(c *cli.Context) error {
|
indexDumpOutput := indexDumpOutputWrapper(ctx.clientFactory)
|
||||||
query := make(url.Values)
|
|
||||||
query.Set("folder", c.Args()[0])
|
query := make(url.Values)
|
||||||
query.Set("file", normalizePath(c.Args()[1]))
|
query.Set("folder", f.FolderID)
|
||||||
return indexDumpOutput("debug/file?" + query.Encode())(c)
|
query.Set("file", normalizePath(f.Path))
|
||||||
|
return indexDumpOutput("debug/file?" + query.Encode())
|
||||||
|
}
|
||||||
|
|
||||||
|
type profileCommand struct {
|
||||||
|
Type string `arg:"" help:"cpu | heap"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *profileCommand) Run(ctx Context) error {
|
||||||
|
switch t := p.Type; t {
|
||||||
|
case "cpu", "heap":
|
||||||
|
return saveToFile(fmt.Sprintf("debug/%vprof", p.Type), ctx.clientFactory)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("expected cpu or heap as argument, got %v", t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func profile() cli.ActionFunc {
|
type debugCommand struct {
|
||||||
return func(c *cli.Context) error {
|
File fileCommand `cmd:"" help:"Show information about a file (or directory/symlink)"`
|
||||||
switch t := c.Args()[0]; t {
|
Profile profileCommand `cmd:"" help:"Save a profile to help figuring out what Syncthing does"`
|
||||||
case "cpu", "heap":
|
Index indexCommand `cmd:"" help:"Show information about the index (database)"`
|
||||||
return saveToFile(fmt.Sprintf("debug/%vprof", c.Args()[0]))(c)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("expected cpu or heap as argument, got %v", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-26
@@ -11,36 +11,25 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
"github.com/alecthomas/kong"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errorsCommand = cli.Command{
|
type errorsCommand struct {
|
||||||
Name: "errors",
|
Show struct{} `cmd:"" help:"Show pending errors"`
|
||||||
HideHelp: true,
|
Push errorsPushCommand `cmd:"" help:"Push an error to active clients"`
|
||||||
Usage: "Error command group",
|
Clear struct{} `cmd:"" help:"Clear pending errors"`
|
||||||
Subcommands: []cli.Command{
|
|
||||||
{
|
|
||||||
Name: "show",
|
|
||||||
Usage: "Show pending errors",
|
|
||||||
Action: expects(0, indexDumpOutput("system/error")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "push",
|
|
||||||
Usage: "Push an error to active clients",
|
|
||||||
ArgsUsage: "ERROR-MESSAGE",
|
|
||||||
Action: expects(1, errorsPush),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "clear",
|
|
||||||
Usage: "Clear pending errors",
|
|
||||||
Action: expects(0, emptyPost("system/error/clear")),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorsPush(c *cli.Context) error {
|
type errorsPushCommand struct {
|
||||||
client := c.App.Metadata["client"].(APIClient)
|
ErrorMessage string `arg:""`
|
||||||
errStr := strings.Join(c.Args(), " ")
|
}
|
||||||
|
|
||||||
|
func (e *errorsPushCommand) Run(ctx Context) error {
|
||||||
|
client, err := ctx.clientFactory.getClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
errStr := e.ErrorMessage
|
||||||
response, err := client.Post("system/error", strings.TrimSpace(errStr))
|
response, err := client.Post("system/error", strings.TrimSpace(errStr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -59,3 +48,13 @@ func errorsPush(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*errorsCommand) Run(ctx Context, kongCtx *kong.Context) error {
|
||||||
|
switch kongCtx.Selected().Name {
|
||||||
|
case "show":
|
||||||
|
return indexDumpOutput("system/error", ctx.clientFactory)
|
||||||
|
case "clear":
|
||||||
|
return emptyPost("system/error/clear", ctx.clientFactory)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+20
-26
@@ -7,32 +7,26 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/urfave/cli"
|
"github.com/alecthomas/kong"
|
||||||
)
|
)
|
||||||
|
|
||||||
var indexCommand = cli.Command{
|
type indexCommand struct {
|
||||||
Name: "index",
|
Dump struct{} `cmd:"" help:"Print the entire db"`
|
||||||
Usage: "Show information about the index (database)",
|
DumpSize struct{} `cmd:"" help:"Print the db size of different categories of information"`
|
||||||
Subcommands: []cli.Command{
|
Check struct{} `cmd:"" help:"Check the database for inconsistencies"`
|
||||||
{
|
Account struct{} `cmd:"" help:"Print key and value size statistics per key type"`
|
||||||
Name: "dump",
|
}
|
||||||
Usage: "Print the entire db",
|
|
||||||
Action: expects(0, indexDump),
|
func (*indexCommand) Run(kongCtx *kong.Context) error {
|
||||||
},
|
switch kongCtx.Selected().Name {
|
||||||
{
|
case "dump":
|
||||||
Name: "dump-size",
|
return indexDump()
|
||||||
Usage: "Print the db size of different categories of information",
|
case "dump-size":
|
||||||
Action: expects(0, indexDumpSize),
|
return indexDumpSize()
|
||||||
},
|
case "check":
|
||||||
{
|
return indexCheck()
|
||||||
Name: "check",
|
case "account":
|
||||||
Usage: "Check the database for inconsistencies",
|
return indexAccount()
|
||||||
Action: expects(0, indexCheck),
|
}
|
||||||
},
|
return nil
|
||||||
{
|
|
||||||
Name: "account",
|
|
||||||
Usage: "Print key and value size statistics per key type",
|
|
||||||
Action: expects(0, indexAccount),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// indexAccount prints key and data size statistics per class
|
// indexAccount prints key and data size statistics per class
|
||||||
func indexAccount(*cli.Context) error {
|
func indexAccount() error {
|
||||||
ldb, err := getDB()
|
ldb, err := getDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -11,13 +11,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/internal/gen/bep"
|
||||||
|
"github.com/syncthing/syncthing/internal/gen/dbproto"
|
||||||
"github.com/syncthing/syncthing/lib/db"
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
func indexDump(*cli.Context) error {
|
func indexDump() error {
|
||||||
ldb, err := getDB()
|
ldb, err := getDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -35,19 +37,19 @@ func indexDump(*cli.Context) error {
|
|||||||
name := nulString(key[1+4+4:])
|
name := nulString(key[1+4+4:])
|
||||||
fmt.Printf("[device] F:%d D:%d N:%q", folder, device, name)
|
fmt.Printf("[device] F:%d D:%d N:%q", folder, device, name)
|
||||||
|
|
||||||
var f protocol.FileInfo
|
var f bep.FileInfo
|
||||||
err := f.Unmarshal(it.Value())
|
err := proto.Unmarshal(it.Value(), &f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf(" V:%v\n", f)
|
fmt.Printf(" V:%v\n", &f)
|
||||||
|
|
||||||
case db.KeyTypeGlobal:
|
case db.KeyTypeGlobal:
|
||||||
folder := binary.BigEndian.Uint32(key[1:])
|
folder := binary.BigEndian.Uint32(key[1:])
|
||||||
name := nulString(key[1+4:])
|
name := nulString(key[1+4:])
|
||||||
var flv db.VersionList
|
var flv dbproto.VersionList
|
||||||
flv.Unmarshal(it.Value())
|
proto.Unmarshal(it.Value(), &flv)
|
||||||
fmt.Printf("[global] F:%d N:%q V:%s\n", folder, name, flv)
|
fmt.Printf("[global] F:%d N:%q V:%s\n", folder, name, &flv)
|
||||||
|
|
||||||
case db.KeyTypeBlock:
|
case db.KeyTypeBlock:
|
||||||
folder := binary.BigEndian.Uint32(key[1:])
|
folder := binary.BigEndian.Uint32(key[1:])
|
||||||
@@ -96,11 +98,11 @@ func indexDump(*cli.Context) error {
|
|||||||
case db.KeyTypeFolderMeta:
|
case db.KeyTypeFolderMeta:
|
||||||
folder := binary.BigEndian.Uint32(key[1:])
|
folder := binary.BigEndian.Uint32(key[1:])
|
||||||
fmt.Printf("[foldermeta] F:%d", folder)
|
fmt.Printf("[foldermeta] F:%d", folder)
|
||||||
var cs db.CountsSet
|
var cs dbproto.CountsSet
|
||||||
if err := cs.Unmarshal(it.Value()); err != nil {
|
if err := proto.Unmarshal(it.Value(), &cs); err != nil {
|
||||||
fmt.Printf(" (invalid)\n")
|
fmt.Printf(" (invalid)\n")
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" V:%v\n", cs)
|
fmt.Printf(" V:%v\n", &cs)
|
||||||
}
|
}
|
||||||
|
|
||||||
case db.KeyTypeMiscData:
|
case db.KeyTypeMiscData:
|
||||||
@@ -127,20 +129,20 @@ func indexDump(*cli.Context) error {
|
|||||||
|
|
||||||
case db.KeyTypeVersion:
|
case db.KeyTypeVersion:
|
||||||
fmt.Printf("[version] H:%x", key[1:])
|
fmt.Printf("[version] H:%x", key[1:])
|
||||||
var v protocol.Vector
|
var v bep.Vector
|
||||||
err := v.Unmarshal(it.Value())
|
err := proto.Unmarshal(it.Value(), &v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf(" (invalid)\n")
|
fmt.Printf(" (invalid)\n")
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" V:%v\n", v)
|
fmt.Printf(" V:%v\n", &v)
|
||||||
}
|
}
|
||||||
|
|
||||||
case db.KeyTypePendingFolder:
|
case db.KeyTypePendingFolder:
|
||||||
device := binary.BigEndian.Uint32(key[1:])
|
device := binary.BigEndian.Uint32(key[1:])
|
||||||
folder := string(key[5:])
|
folder := string(key[5:])
|
||||||
var of db.ObservedFolder
|
var of dbproto.ObservedFolder
|
||||||
of.Unmarshal(it.Value())
|
proto.Unmarshal(it.Value(), &of)
|
||||||
fmt.Printf("[pendingFolder] D:%d F:%s V:%v\n", device, folder, of)
|
fmt.Printf("[pendingFolder] D:%d F:%s V:%v\n", device, folder, &of)
|
||||||
|
|
||||||
case db.KeyTypePendingDevice:
|
case db.KeyTypePendingDevice:
|
||||||
device := "<invalid>"
|
device := "<invalid>"
|
||||||
@@ -148,9 +150,9 @@ func indexDump(*cli.Context) error {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
device = dev.String()
|
device = dev.String()
|
||||||
}
|
}
|
||||||
var od db.ObservedDevice
|
var od dbproto.ObservedDevice
|
||||||
od.Unmarshal(it.Value())
|
proto.Unmarshal(it.Value(), &od)
|
||||||
fmt.Printf("[pendingDevice] D:%v V:%v\n", device, od)
|
fmt.Printf("[pendingDevice] D:%v V:%v\n", device, &od)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
fmt.Printf("[??? %d]\n %x\n %x\n", key[0], key, it.Value())
|
fmt.Printf("[??? %d]\n %x\n %x\n", key[0], key, it.Value())
|
||||||
|
|||||||
@@ -7,16 +7,15 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"slices"
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/db"
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
func indexDumpSize(*cli.Context) error {
|
func indexDumpSize() error {
|
||||||
type sizedElement struct {
|
type sizedElement struct {
|
||||||
key string
|
key string
|
||||||
size int
|
size int
|
||||||
@@ -79,8 +78,8 @@ func indexDumpSize(*cli.Context) error {
|
|||||||
elems = append(elems, ele)
|
elems = append(elems, ele)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(elems, func(i, j int) bool {
|
slices.SortFunc(elems, func(a, b sizedElement) int {
|
||||||
return elems[i].size > elems[j].size
|
return cmp.Compare(b.size, a.size)
|
||||||
})
|
})
|
||||||
for _, ele := range elems {
|
for _, ele := range elems {
|
||||||
fmt.Println(ele.key, ele.size)
|
fmt.Println(ele.key, ele.size)
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"cmp"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"slices"
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/internal/gen/bep"
|
||||||
|
"github.com/syncthing/syncthing/internal/gen/dbproto"
|
||||||
"github.com/syncthing/syncthing/lib/db"
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
)
|
)
|
||||||
@@ -35,7 +38,7 @@ type sequenceKey struct {
|
|||||||
sequence uint64
|
sequence uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func indexCheck(*cli.Context) (err error) {
|
func indexCheck() (err error) {
|
||||||
ldb, err := getDB()
|
ldb, err := getDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -44,12 +47,12 @@ func indexCheck(*cli.Context) (err error) {
|
|||||||
folders := make(map[uint32]string)
|
folders := make(map[uint32]string)
|
||||||
devices := make(map[uint32]string)
|
devices := make(map[uint32]string)
|
||||||
deviceToIDs := make(map[string]uint32)
|
deviceToIDs := make(map[string]uint32)
|
||||||
fileInfos := make(map[fileInfoKey]protocol.FileInfo)
|
fileInfos := make(map[fileInfoKey]*bep.FileInfo)
|
||||||
globals := make(map[globalKey]db.VersionList)
|
globals := make(map[globalKey]*dbproto.VersionList)
|
||||||
sequences := make(map[sequenceKey]string)
|
sequences := make(map[sequenceKey]string)
|
||||||
needs := make(map[globalKey]struct{})
|
needs := make(map[globalKey]struct{})
|
||||||
blocklists := make(map[string]struct{})
|
blocklists := make(map[string]struct{})
|
||||||
versions := make(map[string]protocol.Vector)
|
versions := make(map[string]*bep.Vector)
|
||||||
usedBlocklists := make(map[string]struct{})
|
usedBlocklists := make(map[string]struct{})
|
||||||
usedVersions := make(map[string]struct{})
|
usedVersions := make(map[string]struct{})
|
||||||
var localDeviceKey uint32
|
var localDeviceKey uint32
|
||||||
@@ -76,26 +79,26 @@ func indexCheck(*cli.Context) (err error) {
|
|||||||
device := binary.BigEndian.Uint32(key[1+4:])
|
device := binary.BigEndian.Uint32(key[1+4:])
|
||||||
name := nulString(key[1+4+4:])
|
name := nulString(key[1+4+4:])
|
||||||
|
|
||||||
var f protocol.FileInfo
|
var f bep.FileInfo
|
||||||
err := f.Unmarshal(it.Value())
|
err := proto.Unmarshal(it.Value(), &f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Unable to unmarshal FileInfo:", err)
|
fmt.Println("Unable to unmarshal FileInfo:", err)
|
||||||
success = false
|
success = false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fileInfos[fileInfoKey{folder, device, name}] = f
|
fileInfos[fileInfoKey{folder, device, name}] = &f
|
||||||
|
|
||||||
case db.KeyTypeGlobal:
|
case db.KeyTypeGlobal:
|
||||||
folder := binary.BigEndian.Uint32(key[1:])
|
folder := binary.BigEndian.Uint32(key[1:])
|
||||||
name := nulString(key[1+4:])
|
name := nulString(key[1+4:])
|
||||||
var flv db.VersionList
|
var flv dbproto.VersionList
|
||||||
if err := flv.Unmarshal(it.Value()); err != nil {
|
if err := proto.Unmarshal(it.Value(), &flv); err != nil {
|
||||||
fmt.Println("Unable to unmarshal VersionList:", err)
|
fmt.Println("Unable to unmarshal VersionList:", err)
|
||||||
success = false
|
success = false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
globals[globalKey{folder, name}] = flv
|
globals[globalKey{folder, name}] = &flv
|
||||||
|
|
||||||
case db.KeyTypeFolderIdx:
|
case db.KeyTypeFolderIdx:
|
||||||
key := binary.BigEndian.Uint32(it.Key()[1:])
|
key := binary.BigEndian.Uint32(it.Key()[1:])
|
||||||
@@ -126,13 +129,13 @@ func indexCheck(*cli.Context) (err error) {
|
|||||||
|
|
||||||
case db.KeyTypeVersion:
|
case db.KeyTypeVersion:
|
||||||
hash := string(key[1:])
|
hash := string(key[1:])
|
||||||
var v protocol.Vector
|
var v bep.Vector
|
||||||
if err := v.Unmarshal(it.Value()); err != nil {
|
if err := proto.Unmarshal(it.Value(), &v); err != nil {
|
||||||
fmt.Println("Unable to unmarshal Vector:", err)
|
fmt.Println("Unable to unmarshal Vector:", err)
|
||||||
success = false
|
success = false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
versions[hash] = v
|
versions[hash] = &v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,11 +208,11 @@ func indexCheck(*cli.Context) (err error) {
|
|||||||
|
|
||||||
// Aggregate the ranges of missing sequence entries, print them
|
// Aggregate the ranges of missing sequence entries, print them
|
||||||
|
|
||||||
sort.Slice(missingSeq, func(a, b int) bool {
|
slices.SortFunc(missingSeq, func(a, b sequenceKey) int {
|
||||||
if missingSeq[a].folder != missingSeq[b].folder {
|
if a.folder != b.folder {
|
||||||
return missingSeq[a].folder < missingSeq[b].folder
|
return cmp.Compare(a.folder, b.folder)
|
||||||
}
|
}
|
||||||
return missingSeq[a].sequence < missingSeq[b].sequence
|
return cmp.Compare(a.sequence, b.sequence)
|
||||||
})
|
})
|
||||||
|
|
||||||
var folder uint32
|
var folder uint32
|
||||||
@@ -250,25 +253,27 @@ func indexCheck(*cli.Context) (err error) {
|
|||||||
if fi.VersionHash != nil {
|
if fi.VersionHash != nil {
|
||||||
fiv = versions[string(fi.VersionHash)]
|
fiv = versions[string(fi.VersionHash)]
|
||||||
}
|
}
|
||||||
if !fiv.Equal(version) {
|
if !protocol.VectorFromWire(fiv).Equal(version) {
|
||||||
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo version mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, version, fi.Version)
|
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo version mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, version, fi.Version)
|
||||||
success = false
|
success = false
|
||||||
}
|
}
|
||||||
if fi.IsInvalid() != invalid {
|
ffi := protocol.FileInfoFromDB(fi)
|
||||||
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo invalid mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, invalid, fi.IsInvalid())
|
if ffi.IsInvalid() != invalid {
|
||||||
|
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo invalid mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, invalid, ffi.IsInvalid())
|
||||||
success = false
|
success = false
|
||||||
}
|
}
|
||||||
if fi.IsDeleted() != deleted {
|
if ffi.IsDeleted() != deleted {
|
||||||
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo deleted mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, deleted, fi.IsDeleted())
|
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo deleted mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, deleted, ffi.IsDeleted())
|
||||||
success = false
|
success = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, fv := range vl.RawVersions {
|
for i, fv := range vl.Versions {
|
||||||
|
ver := protocol.VectorFromWire(fv.Version)
|
||||||
for _, device := range fv.Devices {
|
for _, device := range fv.Devices {
|
||||||
checkGlobal(i, device, fv.Version, false, fv.Deleted)
|
checkGlobal(i, device, ver, false, fv.Deleted)
|
||||||
}
|
}
|
||||||
for _, device := range fv.InvalidDevices {
|
for _, device := range fv.InvalidDevices {
|
||||||
checkGlobal(i, device, fv.Version, true, fv.Deleted)
|
checkGlobal(i, device, ver, true, fv.Deleted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,10 +283,10 @@ func indexCheck(*cli.Context) (err error) {
|
|||||||
if needsLocally(vl) {
|
if needsLocally(vl) {
|
||||||
_, ok := needs[gk]
|
_, ok := needs[gk]
|
||||||
if !ok {
|
if !ok {
|
||||||
fv, _ := vl.GetGlobal()
|
fv, _ := vlGetGlobal(vl)
|
||||||
devB, _ := fv.FirstDevice()
|
devB, _ := fvFirstDevice(fv)
|
||||||
dev := deviceToIDs[string(devB)]
|
dev := deviceToIDs[string(devB)]
|
||||||
fi := fileInfos[fileInfoKey{gk.folder, dev, gk.name}]
|
fi := protocol.FileInfoFromDB(fileInfos[fileInfoKey{gk.folder, dev, gk.name}])
|
||||||
if !fi.IsDeleted() && !fi.IsIgnored() {
|
if !fi.IsDeleted() && !fi.IsIgnored() {
|
||||||
fmt.Printf("Missing need entry for needed file %q, folder %q\n", gk.name, folder)
|
fmt.Printf("Missing need entry for needed file %q, folder %q\n", gk.name, folder)
|
||||||
}
|
}
|
||||||
@@ -347,11 +352,84 @@ func indexCheck(*cli.Context) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func needsLocally(vl db.VersionList) bool {
|
func needsLocally(vl *dbproto.VersionList) bool {
|
||||||
gfv, gok := vl.GetGlobal()
|
gfv, gok := vlGetGlobal(vl)
|
||||||
if !gok { // That's weird, but we hardly need something non-existent
|
if !gok { // That's weird, but we hardly need something non-existent
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
fv, ok := vl.Get(protocol.LocalDeviceID[:])
|
fv, ok := vlGet(vl, protocol.LocalDeviceID[:])
|
||||||
return db.Need(gfv, ok, fv.Version)
|
return db.Need(gfv, ok, protocol.VectorFromWire(fv.Version))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a FileVersion that contains the given device and whether it has
|
||||||
|
// been found at all.
|
||||||
|
func vlGet(vl *dbproto.VersionList, device []byte) (*dbproto.FileVersion, bool) {
|
||||||
|
_, i, _, ok := vlFindDevice(vl, device)
|
||||||
|
if !ok {
|
||||||
|
return &dbproto.FileVersion{}, false
|
||||||
|
}
|
||||||
|
return vl.Versions[i], true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGlobal returns the current global FileVersion. The returned FileVersion
|
||||||
|
// may be invalid, if all FileVersions are invalid. Returns false only if
|
||||||
|
// VersionList is empty.
|
||||||
|
func vlGetGlobal(vl *dbproto.VersionList) (*dbproto.FileVersion, bool) {
|
||||||
|
i := vlFindGlobal(vl)
|
||||||
|
if i == -1 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return vl.Versions[i], true
|
||||||
|
}
|
||||||
|
|
||||||
|
// findGlobal returns the first version that isn't invalid, or if all versions are
|
||||||
|
// invalid just the first version (i.e. 0) or -1, if there's no versions at all.
|
||||||
|
func vlFindGlobal(vl *dbproto.VersionList) int {
|
||||||
|
for i := range vl.Versions {
|
||||||
|
if !fvIsInvalid(vl.Versions[i]) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(vl.Versions) == 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// findDevice returns whether the device is in InvalidVersions or Versions and
|
||||||
|
// in InvalidDevices or Devices (true for invalid), the positions in the version
|
||||||
|
// and device slices and whether it has been found at all.
|
||||||
|
func vlFindDevice(vl *dbproto.VersionList, device []byte) (bool, int, int, bool) {
|
||||||
|
for i, v := range vl.Versions {
|
||||||
|
if j := deviceIndex(v.Devices, device); j != -1 {
|
||||||
|
return false, i, j, true
|
||||||
|
}
|
||||||
|
if j := deviceIndex(v.InvalidDevices, device); j != -1 {
|
||||||
|
return true, i, j, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, -1, -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func deviceIndex(devices [][]byte, device []byte) int {
|
||||||
|
for i, dev := range devices {
|
||||||
|
if bytes.Equal(device, dev) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func fvFirstDevice(fv *dbproto.FileVersion) ([]byte, bool) {
|
||||||
|
if len(fv.Devices) != 0 {
|
||||||
|
return fv.Devices[0], true
|
||||||
|
}
|
||||||
|
if len(fv.InvalidDevices) != 0 {
|
||||||
|
return fv.InvalidDevices[0], true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func fvIsInvalid(fv *dbproto.FileVersion) bool {
|
||||||
|
return fv == nil || len(fv.Devices) == 0
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-131
@@ -8,165 +8,88 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
"github.com/flynn-archive/go-shlex"
|
"github.com/kballard/go-shellquote"
|
||||||
"github.com/urfave/cli"
|
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/cmd/syncthing/cmdutil"
|
"github.com/syncthing/syncthing/cmd/syncthing/cmdutil"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type preCli struct {
|
type CLI struct {
|
||||||
|
cmdutil.CommonOptions
|
||||||
|
DataDir string `name:"data" placeholder:"PATH" env:"STDATADIR" help:"Set data directory (database and logs)"`
|
||||||
GUIAddress string `name:"gui-address"`
|
GUIAddress string `name:"gui-address"`
|
||||||
GUIAPIKey string `name:"gui-apikey"`
|
GUIAPIKey string `name:"gui-apikey"`
|
||||||
HomeDir string `name:"home"`
|
|
||||||
ConfDir string `name:"config"`
|
Show showCommand `cmd:"" help:"Show command group"`
|
||||||
DataDir string `name:"data"`
|
Debug debugCommand `cmd:"" help:"Debug command group"`
|
||||||
|
Operations operationCommand `cmd:"" help:"Operation command group"`
|
||||||
|
Errors errorsCommand `cmd:"" help:"Error command group"`
|
||||||
|
Config configCommand `cmd:"" help:"Configuration modification command group" passthrough:""`
|
||||||
|
Stdin stdinCommand `cmd:"" name:"-" help:"Read commands from stdin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run() error {
|
type Context struct {
|
||||||
// This is somewhat a hack around a chicken and egg problem. We need to set
|
clientFactory *apiClientFactory
|
||||||
// the home directory and potentially other flags to know where the
|
|
||||||
// syncthing instance is running in order to get it's config ... which we
|
|
||||||
// then use to construct the actual CLI ... at which point it's too late to
|
|
||||||
// add flags there...
|
|
||||||
c := preCli{}
|
|
||||||
parseFlags(&c)
|
|
||||||
return runInternal(c, os.Args)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunWithArgs(cliArgs []string) error {
|
func (cli CLI) AfterApply(kongCtx *kong.Context) error {
|
||||||
c := preCli{}
|
err := cmdutil.SetConfigDataLocationsFromFlags(cli.HomeDir, cli.ConfDir, cli.DataDir)
|
||||||
parseFlagsWithArgs(cliArgs, &c)
|
|
||||||
return runInternal(c, cliArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runInternal(c preCli, cliArgs []string) error {
|
|
||||||
// Not set as default above because the strings can be really long.
|
|
||||||
err := cmdutil.SetConfigDataLocationsFromFlags(c.HomeDir, c.ConfDir, c.DataDir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Command line options: %w", err)
|
return fmt.Errorf("command line options: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
clientFactory := &apiClientFactory{
|
clientFactory := &apiClientFactory{
|
||||||
cfg: config.GUIConfiguration{
|
cfg: config.GUIConfiguration{
|
||||||
RawAddress: c.GUIAddress,
|
RawAddress: cli.GUIAddress,
|
||||||
APIKey: c.GUIAPIKey,
|
APIKey: cli.GUIAPIKey,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
configCommand, err := getConfigCommand(clientFactory)
|
context := Context{
|
||||||
if err != nil {
|
clientFactory: clientFactory,
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement the same flags at the upper CLI, but do nothing with them.
|
kongCtx.Bind(context)
|
||||||
// This is so that the usage text is the same
|
return nil
|
||||||
fakeFlags := []cli.Flag{
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "gui-address",
|
|
||||||
Usage: "Override GUI address to `URL` (e.g. \"192.0.2.42:8443\")",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "gui-apikey",
|
|
||||||
Usage: "Override GUI API key to `API-KEY`",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "home",
|
|
||||||
Usage: "Set configuration and data directory to `PATH`",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "config",
|
|
||||||
Usage: "Set configuration directory (config and keys) to `PATH`",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "data",
|
|
||||||
Usage: "Set data directory (database and logs) to `PATH`",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the actual CLI
|
|
||||||
app := cli.NewApp()
|
|
||||||
app.Author = "The Syncthing Authors"
|
|
||||||
app.Metadata = map[string]interface{}{
|
|
||||||
"clientFactory": clientFactory,
|
|
||||||
}
|
|
||||||
app.Commands = []cli.Command{{
|
|
||||||
Name: "cli",
|
|
||||||
Usage: "Syncthing command line interface",
|
|
||||||
Flags: fakeFlags,
|
|
||||||
Subcommands: []cli.Command{
|
|
||||||
configCommand,
|
|
||||||
showCommand,
|
|
||||||
operationCommand,
|
|
||||||
errorsCommand,
|
|
||||||
debugCommand,
|
|
||||||
{
|
|
||||||
Name: "-",
|
|
||||||
HideHelp: true,
|
|
||||||
Usage: "Read commands from stdin",
|
|
||||||
Action: func(ctx *cli.Context) error {
|
|
||||||
if ctx.NArg() > 0 {
|
|
||||||
return errors.New("command does not expect any arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop the `-` not to recurse into self.
|
|
||||||
args := make([]string, len(cliArgs)-1)
|
|
||||||
copy(args, cliArgs)
|
|
||||||
|
|
||||||
fmt.Println("Reading commands from stdin...", args)
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
for scanner.Scan() {
|
|
||||||
input, err := shlex.Split(scanner.Text())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing input: %w", err)
|
|
||||||
}
|
|
||||||
if len(input) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err = app.Run(append(args, input...))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return scanner.Err()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
|
|
||||||
return app.Run(cliArgs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFlags(c *preCli) error {
|
type stdinCommand struct{}
|
||||||
// kong only needs to parse the global arguments after "cli" and before the
|
|
||||||
// subcommand (if any).
|
|
||||||
if len(os.Args) <= 2 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return parseFlagsWithArgs(os.Args[2:], c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseFlagsWithArgs(args []string, c *preCli) error {
|
func (*stdinCommand) Run() error {
|
||||||
for i := 0; i < len(args); i++ {
|
// Drop the `-` not to recurse into self.
|
||||||
if !strings.HasPrefix(args[i], "--") {
|
args := make([]string, len(os.Args)-1)
|
||||||
args = args[:i]
|
copy(args, os.Args)
|
||||||
break
|
|
||||||
|
fmt.Println("Reading commands from stdin...", args)
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
for scanner.Scan() {
|
||||||
|
input, err := shellquote.Split(scanner.Text())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing input: %w", err)
|
||||||
}
|
}
|
||||||
if !strings.Contains(args[i], "=") {
|
if len(input) == 0 {
|
||||||
i++
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var cli CLI
|
||||||
|
p, err := kong.New(&cli)
|
||||||
|
if err != nil {
|
||||||
|
// can't happen, really
|
||||||
|
return fmt.Errorf("creating parser: %w", err)
|
||||||
|
}
|
||||||
|
ctx, err := p.Parse(input)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := ctx.Run(); err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// We don't want kong to print anything nor os.Exit (e.g. on -h)
|
return scanner.Err()
|
||||||
parser, err := kong.New(c, kong.Writers(io.Discard, io.Discard), kong.Exit(func(int) {}))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = parser.Parse(args)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,48 +12,43 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/fs"
|
"github.com/syncthing/syncthing/lib/fs"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var operationCommand = cli.Command{
|
type folderOverrideCommand struct {
|
||||||
Name: "operations",
|
FolderID string `arg:""`
|
||||||
HideHelp: true,
|
|
||||||
Usage: "Operation command group",
|
|
||||||
Subcommands: []cli.Command{
|
|
||||||
{
|
|
||||||
Name: "restart",
|
|
||||||
Usage: "Restart syncthing",
|
|
||||||
Action: expects(0, emptyPost("system/restart")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "shutdown",
|
|
||||||
Usage: "Shutdown syncthing",
|
|
||||||
Action: expects(0, emptyPost("system/shutdown")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "upgrade",
|
|
||||||
Usage: "Upgrade syncthing (if a newer version is available)",
|
|
||||||
Action: expects(0, emptyPost("system/upgrade")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "folder-override",
|
|
||||||
Usage: "Override changes on folder (remote for sendonly, local for receiveonly). WARNING: Destructive - deletes/changes your data.",
|
|
||||||
ArgsUsage: "FOLDER-ID",
|
|
||||||
Action: expects(1, foldersOverride),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "default-ignores",
|
|
||||||
Usage: "Set the default ignores (config) from a file",
|
|
||||||
ArgsUsage: "PATH",
|
|
||||||
Action: expects(1, setDefaultIgnores),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func foldersOverride(c *cli.Context) error {
|
type defaultIgnoresCommand struct {
|
||||||
client, err := getClientFactory(c).getClient()
|
Path string `arg:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
type operationCommand struct {
|
||||||
|
Restart struct{} `cmd:"" help:"Restart syncthing"`
|
||||||
|
Shutdown struct{} `cmd:"" help:"Shutdown syncthing"`
|
||||||
|
Upgrade struct{} `cmd:"" help:"Upgrade syncthing (if a newer version is available)"`
|
||||||
|
FolderOverride folderOverrideCommand `cmd:"" help:"Override changes on folder (remote for sendonly, local for receiveonly). WARNING: Destructive - deletes/changes your data"`
|
||||||
|
DefaultIgnores defaultIgnoresCommand `cmd:"" help:"Set the default ignores (config) from a file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*operationCommand) Run(ctx Context, kongCtx *kong.Context) error {
|
||||||
|
f := ctx.clientFactory
|
||||||
|
|
||||||
|
switch kongCtx.Selected().Name {
|
||||||
|
case "restart":
|
||||||
|
return emptyPost("system/restart", f)
|
||||||
|
case "shutdown":
|
||||||
|
return emptyPost("system/shutdown", f)
|
||||||
|
case "upgrade":
|
||||||
|
return emptyPost("system/upgrade", f)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *folderOverrideCommand) Run(ctx Context) error {
|
||||||
|
client, err := ctx.clientFactory.getClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -61,7 +56,7 @@ func foldersOverride(c *cli.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rid := c.Args()[0]
|
rid := f.FolderID
|
||||||
for _, folder := range cfg.Folders {
|
for _, folder := range cfg.Folders {
|
||||||
if folder.ID == rid {
|
if folder.ID == rid {
|
||||||
response, err := client.Post("db/override", "")
|
response, err := client.Post("db/override", "")
|
||||||
@@ -86,12 +81,12 @@ func foldersOverride(c *cli.Context) error {
|
|||||||
return fmt.Errorf("Folder %q not found", rid)
|
return fmt.Errorf("Folder %q not found", rid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDefaultIgnores(c *cli.Context) error {
|
func (d *defaultIgnoresCommand) Run(ctx Context) error {
|
||||||
client, err := getClientFactory(c).getClient()
|
client, err := ctx.clientFactory.getClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dir, file := filepath.Split(c.Args()[0])
|
dir, file := filepath.Split(d.Path)
|
||||||
filesystem := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
|
filesystem := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
|
||||||
|
|
||||||
fd, err := filesystem.Open(file)
|
fd, err := filesystem.Open(file)
|
||||||
|
|||||||
@@ -9,37 +9,30 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
"github.com/alecthomas/kong"
|
||||||
)
|
)
|
||||||
|
|
||||||
var pendingCommand = cli.Command{
|
type pendingCommand struct {
|
||||||
Name: "pending",
|
Devices struct{} `cmd:"" help:"Show pending devices"`
|
||||||
HideHelp: true,
|
Folders struct {
|
||||||
Usage: "Pending subcommand group",
|
Device string `help:"Show pending folders offered by given device"`
|
||||||
Subcommands: []cli.Command{
|
} `cmd:"" help:"Show pending folders"`
|
||||||
{
|
|
||||||
Name: "devices",
|
|
||||||
Usage: "Show pending devices",
|
|
||||||
Action: expects(0, indexDumpOutput("cluster/pending/devices")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "folders",
|
|
||||||
Usage: "Show pending folders",
|
|
||||||
Flags: []cli.Flag{
|
|
||||||
cli.StringFlag{Name: "device", Usage: "Show pending folders offered by given device"},
|
|
||||||
},
|
|
||||||
Action: expects(0, folders()),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func folders() cli.ActionFunc {
|
func (p *pendingCommand) Run(ctx Context, kongCtx *kong.Context) error {
|
||||||
return func(c *cli.Context) error {
|
indexDumpOutput := indexDumpOutputWrapper(ctx.clientFactory)
|
||||||
if c.String("device") != "" {
|
|
||||||
|
switch kongCtx.Selected().Name {
|
||||||
|
case "devices":
|
||||||
|
return indexDumpOutput("cluster/pending/devices")
|
||||||
|
case "folders":
|
||||||
|
if p.Folders.Device != "" {
|
||||||
query := make(url.Values)
|
query := make(url.Values)
|
||||||
query.Set("device", c.String("device"))
|
query.Set("device", p.Folders.Device)
|
||||||
return indexDumpOutput("cluster/pending/folders?" + query.Encode())(c)
|
return indexDumpOutput("cluster/pending/folders?" + query.Encode())
|
||||||
}
|
}
|
||||||
return indexDumpOutput("cluster/pending/folders")(c)
|
return indexDumpOutput("cluster/pending/folders")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-38
@@ -7,44 +7,36 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/urfave/cli"
|
"github.com/alecthomas/kong"
|
||||||
)
|
)
|
||||||
|
|
||||||
var showCommand = cli.Command{
|
type showCommand struct {
|
||||||
Name: "show",
|
Version struct{} `cmd:"" help:"Show syncthing client version"`
|
||||||
HideHelp: true,
|
ConfigStatus struct{} `cmd:"" help:"Show configuration status, whether or not a restart is required for changes to take effect"`
|
||||||
Usage: "Show command group",
|
System struct{} `cmd:"" help:"Show system status"`
|
||||||
Subcommands: []cli.Command{
|
Connections struct{} `cmd:"" help:"Report about connections to other devices"`
|
||||||
{
|
Discovery struct{} `cmd:"" help:"Show the discovered addresses of remote devices (from cache of the running syncthing instance)"`
|
||||||
Name: "version",
|
Usage struct{} `cmd:"" help:"Show usage report"`
|
||||||
Usage: "Show syncthing client version",
|
Pending pendingCommand `cmd:"" help:"Pending subcommand group"`
|
||||||
Action: expects(0, indexDumpOutput("system/version")),
|
}
|
||||||
},
|
|
||||||
{
|
func (*showCommand) Run(ctx Context, kongCtx *kong.Context) error {
|
||||||
Name: "config-status",
|
indexDumpOutput := indexDumpOutputWrapper(ctx.clientFactory)
|
||||||
Usage: "Show configuration status, whether or not a restart is required for changes to take effect",
|
|
||||||
Action: expects(0, indexDumpOutput("config/restart-required")),
|
switch kongCtx.Selected().Name {
|
||||||
},
|
case "version":
|
||||||
{
|
return indexDumpOutput("system/version")
|
||||||
Name: "system",
|
case "config-status":
|
||||||
Usage: "Show system status",
|
return indexDumpOutput("config/restart-required")
|
||||||
Action: expects(0, indexDumpOutput("system/status")),
|
case "system":
|
||||||
},
|
return indexDumpOutput("system/status")
|
||||||
{
|
case "connections":
|
||||||
Name: "connections",
|
return indexDumpOutput("system/connections")
|
||||||
Usage: "Report about connections to other devices",
|
case "discovery":
|
||||||
Action: expects(0, indexDumpOutput("system/connections")),
|
return indexDumpOutput("system/discovery")
|
||||||
},
|
case "usage":
|
||||||
{
|
return indexDumpOutput("svc/report")
|
||||||
Name: "discovery",
|
}
|
||||||
Usage: "Show the discovered addresses of remote devices (from cache of the running syncthing instance)",
|
|
||||||
Action: expects(0, indexDumpOutput("system/discovery")),
|
return nil
|
||||||
},
|
|
||||||
pendingCommand,
|
|
||||||
{
|
|
||||||
Name: "usage",
|
|
||||||
Usage: "Show usage report",
|
|
||||||
Action: expects(0, indexDumpOutput("svc/report")),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-74
@@ -19,7 +19,6 @@ import (
|
|||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/db/backend"
|
"github.com/syncthing/syncthing/lib/db/backend"
|
||||||
"github.com/syncthing/syncthing/lib/locations"
|
"github.com/syncthing/syncthing/lib/locations"
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func responseToBArray(response *http.Response) ([]byte, error) {
|
func responseToBArray(response *http.Response) ([]byte, error) {
|
||||||
@@ -30,68 +29,72 @@ func responseToBArray(response *http.Response) ([]byte, error) {
|
|||||||
return bytes, response.Body.Close()
|
return bytes, response.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func emptyPost(url string) cli.ActionFunc {
|
func emptyPost(url string, apiClientFactory *apiClientFactory) error {
|
||||||
return func(c *cli.Context) error {
|
client, err := apiClientFactory.getClient()
|
||||||
client, err := getClientFactory(c).getClient()
|
if err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = client.Post(url, "")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
_, err = client.Post(url, "")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexDumpOutputWrapper(apiClientFactory *apiClientFactory) func(url string) error {
|
||||||
|
return func(url string) error {
|
||||||
|
return indexDumpOutput(url, apiClientFactory)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func indexDumpOutput(url string) cli.ActionFunc {
|
func indexDumpOutput(url string, apiClientFactory *apiClientFactory) error {
|
||||||
return func(c *cli.Context) error {
|
client, err := apiClientFactory.getClient()
|
||||||
client, err := getClientFactory(c).getClient()
|
if err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
response, err := client.Get(url)
|
|
||||||
if errors.Is(err, errNotFound) {
|
|
||||||
return errors.New("not found (folder/file not in database)")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return prettyPrintResponse(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveToFile(url string) cli.ActionFunc {
|
|
||||||
return func(c *cli.Context) error {
|
|
||||||
client, err := getClientFactory(c).getClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
response, err := client.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, params, err := mime.ParseMediaType(response.Header.Get("Content-Disposition"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
filename := params["filename"]
|
|
||||||
if filename == "" {
|
|
||||||
return errors.New("Missing filename in response")
|
|
||||||
}
|
|
||||||
bs, err := responseToBArray(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f, err := os.Create(filename)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
_, err = f.Write(bs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Println("Wrote results to", filename)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
response, err := client.Get(url)
|
||||||
|
if errors.Is(err, errNotFound) {
|
||||||
|
return errors.New("not found (folder/file not in database)")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return prettyPrintResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveToFile(url string, apiClientFactory *apiClientFactory) error {
|
||||||
|
client, err := apiClientFactory.getClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
response, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, params, err := mime.ParseMediaType(response.Header.Get("Content-Disposition"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
filename := params["filename"]
|
||||||
|
if filename == "" {
|
||||||
|
return errors.New("Missing filename in response")
|
||||||
|
}
|
||||||
|
bs, err := responseToBArray(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = f.Write(bs)
|
||||||
|
if err != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Wrote results to", filename)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfig(c APIClient) (config.Configuration, error) {
|
func getConfig(c APIClient) (config.Configuration, error) {
|
||||||
@@ -111,19 +114,6 @@ func getConfig(c APIClient) (config.Configuration, error) {
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func expects(n int, actionFunc cli.ActionFunc) cli.ActionFunc {
|
|
||||||
return func(ctx *cli.Context) error {
|
|
||||||
if ctx.NArg() != n {
|
|
||||||
plural := ""
|
|
||||||
if n != 1 {
|
|
||||||
plural = "s"
|
|
||||||
}
|
|
||||||
return fmt.Errorf("expected %d argument%s, got %d", n, plural, ctx.NArg())
|
|
||||||
}
|
|
||||||
return actionFunc(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func prettyPrintJSON(data interface{}) error {
|
func prettyPrintJSON(data interface{}) error {
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
@@ -159,7 +149,3 @@ func nulString(bs []byte) string {
|
|||||||
func normalizePath(path string) string {
|
func normalizePath(path string) string {
|
||||||
return filepath.ToSlash(filepath.Clean(path))
|
return filepath.ToSlash(filepath.Clean(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClientFactory(c *cli.Context) *apiClientFactory {
|
|
||||||
return c.App.Metadata["clientFactory"].(*apiClientFactory)
|
|
||||||
}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user