mirror of
https://github.com/syncthing/syncthing.git
synced 2026-08-07 05:43:24 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3850a08252 | ||
|
|
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 | ||
|
|
3a41d4afa5 | ||
|
|
6a66aee489 | ||
|
|
51e85d5162 | ||
|
|
b49f535834 | ||
|
|
34b312b85b | ||
|
|
be3751ab5f | ||
|
|
526e21ae26 | ||
|
|
bc371eacf6 | ||
|
|
5caf9bfc18 | ||
|
|
f378e63147 | ||
|
|
fb89898462 | ||
|
|
358cf25cff | ||
|
|
59de7048bd | ||
|
|
466b56ded1 | ||
|
|
3ffe859fe8 | ||
|
|
da72df6ffc | ||
|
|
b53c1b9a04 | ||
|
|
b00976781c | ||
|
|
58f9dcca31 | ||
|
|
ab8e6a82ab | ||
|
|
3cdcb7dd72 | ||
|
|
4558eef446 | ||
|
|
8bbf2ba9ac | ||
|
|
49c56b8caa | ||
|
|
0b8aa7974b | ||
|
|
cbec697e5f | ||
|
|
8991ecf444 | ||
|
|
17887ce0b1 | ||
|
|
2f88dafa56 | ||
|
|
2321d0db08 | ||
|
|
7047e00d1e | ||
|
|
a8533b269d | ||
|
|
ee30647bf7 | ||
|
|
750accb25f | ||
|
|
0530f0edbf | ||
|
|
784129e1cf | ||
|
|
38f2b34d29 | ||
|
|
882b711958 | ||
|
|
99595ce3d9 | ||
|
|
d5e4ef249f | ||
|
|
65cfefaa3c | ||
|
|
a6c2a5a0ce | ||
|
|
a5b6199507 | ||
|
|
966e9f9a68 | ||
|
|
b10e11abb7 | ||
|
|
5a50de1154 | ||
|
|
dc6d695c00 | ||
|
|
b120278b3a | ||
|
|
c25f5b40d9 | ||
|
|
abdac2caa2 | ||
|
|
5f1e27bb7f | ||
|
|
a85b4ab263 | ||
|
|
cf962a4fe8 | ||
|
|
39c0bfa05a | ||
|
|
458d6cff2a | ||
|
|
ded881c372 | ||
|
|
fb4209e382 | ||
|
|
473ca68dc4 | ||
|
|
c4e69cd66c | ||
|
|
634a3d0e3b | ||
|
|
09f4d865ae | ||
|
|
ad0044fec8 | ||
|
|
d157d12037 | ||
|
|
f9d68474ac | ||
|
|
f0126fe1ab | ||
|
|
940536a964 | ||
|
|
f378c4f191 | ||
|
|
358d2143e2 | ||
|
|
622ade40ad | ||
|
|
323405ea07 | ||
|
|
88c226ef87 | ||
|
|
d4b668caac | ||
|
|
05738001ac | ||
|
|
d40d9d5698 | ||
|
|
648c71cbd1 | ||
|
|
ab0eb909a2 | ||
|
|
d16c0652f7 | ||
|
|
0dae06deb3 | ||
|
|
663106ef6e | ||
|
|
0b2b8baf19 | ||
|
|
24275b4584 | ||
|
|
2a8362d7af | ||
|
|
8cca9dfef5 | ||
|
|
6aa04118a6 | ||
|
|
1b32e9f858 | ||
|
|
a523fef78e | ||
|
|
ce2a68622c | ||
|
|
a29605750d | ||
|
|
5e384c9185 | ||
|
|
a1cc293c21 | ||
|
|
452daad14b | ||
|
|
fbdaa265d3 | ||
|
|
46b375e8cc | ||
|
|
1e9bf17512 | ||
|
|
413c8cf4ea | ||
|
|
d8296ce111 | ||
|
|
06a1635d1d | ||
|
|
36221b70ac | ||
|
|
bf1e418e4a | ||
|
|
922946683d | ||
|
|
abb921acbc | ||
|
|
816354e66b | ||
|
|
8228020ff0 | ||
|
|
c96f76e1fd | ||
|
|
d3f50637d2 | ||
|
|
ed588ce335 | ||
|
|
34d91b228d | ||
|
|
fb6a35c98c | ||
|
|
87bf09ea40 | ||
|
|
a13bb926b7 | ||
|
|
7a402409f1 | ||
|
|
c791dba392 | ||
|
|
a0c80e030a | ||
|
|
b6bb67b142 | ||
|
|
1306d86a62 | ||
|
|
57443804ba | ||
|
|
2839baf0de | ||
|
|
7e848a150b | ||
|
|
81bdde79ea | ||
|
|
a206366d10 | ||
|
|
1e652de5af | ||
|
|
ad986f372d | ||
|
|
0935886045 | ||
|
|
fd0a6225aa | ||
|
|
b39985483d | ||
|
|
f38df0dadb | ||
|
|
361f7ae564 | ||
|
|
1cd2f5a91f | ||
|
|
dab68cf841 | ||
|
|
d51fe3fb3f | ||
|
|
5baf5fedb5 | ||
|
|
3f2742a275 | ||
|
|
0e23002414 | ||
|
|
641941562f | ||
|
|
698346edc3 | ||
|
|
39d3424e34 | ||
|
|
6cac308bcd | ||
|
|
8065cf7e97 | ||
|
|
6e768a8387 | ||
|
|
7d3c390c91 | ||
|
|
3e99ddfbf0 | ||
|
|
43f0e5c91d | ||
|
|
c3902f9887 | ||
|
|
33e3643aed | ||
|
|
78be7225fb | ||
|
|
6afaa9f20c | ||
|
|
152388b3a3 | ||
|
|
053425695a | ||
|
|
5d917e1306 | ||
|
|
837ffcfab5 | ||
|
|
62d4261f62 | ||
|
|
6dedffe3f7 | ||
|
|
b10d106a55 | ||
|
|
75eeae0ee7 | ||
|
|
5fd6278609 | ||
|
|
eb81f7400c | ||
|
|
06273875ae | ||
|
|
4d4bfe8032 | ||
|
|
28c660e41d | ||
|
|
d6a90f0022 | ||
|
|
63de838f27 | ||
|
|
20dea04aa2 | ||
|
|
f5aa604efc | ||
|
|
4eef43c62d | ||
|
|
d2f132f37f | ||
|
|
ecdb970468 | ||
|
|
b32e43fbb3 | ||
|
|
373859be83 | ||
|
|
209e68c1ba | ||
|
|
cc54488e55 | ||
|
|
c7b4fd5784 | ||
|
|
5c69761bc8 | ||
|
|
de7d62cc1b | ||
|
|
5977868165 | ||
|
|
585fb3f49b | ||
|
|
8e3f1190d1 | ||
|
|
a3c724f2c3 | ||
|
|
f13d65262a | ||
|
|
f2be9d1166 | ||
|
|
dde275c6cc | ||
|
|
212258d213 | ||
|
|
966db0d076 | ||
|
|
6686810943 | ||
|
|
6baa93e13f | ||
|
|
0c8b22c696 | ||
|
|
81d8fa1cb5 | ||
|
|
79f8bd0f33 | ||
|
|
5958f42294 | ||
|
|
8b4bd43306 | ||
|
|
755d21953f | ||
|
|
388e4db9cd | ||
|
|
f3835122bb | ||
|
|
5130c414da | ||
|
|
7bdb5faa9c | ||
|
|
7e26f74f38 | ||
|
|
0bdd0d595b | ||
|
|
80ec4acb53 | ||
|
|
a2c5d901f2 | ||
|
|
1b575b4461 | ||
|
|
c6a319d98b | ||
|
|
9f4d23cacf | ||
|
|
adce6fa473 | ||
|
|
34a5f087c8 | ||
|
|
a6dba7c6d6 | ||
|
|
2eabc79d4c | ||
|
|
9c2428c2ee | ||
|
|
9b390f1264 | ||
|
|
c06a169f5f | ||
|
|
4335285a64 | ||
|
|
34c05bee6d | ||
|
|
7cb8af9029 | ||
|
|
8facaf5a6a | ||
|
|
abea3d7552 | ||
|
|
13d545c9f2 | ||
|
|
3a6ebb8482 | ||
|
|
e071f16531 | ||
|
|
920f2ce1e6 | ||
|
|
2e135d0c65 | ||
|
|
0e79b532cf | ||
|
|
f7e30a9f10 | ||
|
|
b140b4a994 | ||
|
|
6fd7cd808c | ||
|
|
02224be83f | ||
|
|
ebe45b3965 | ||
|
|
486bb2da0e | ||
|
|
68aa00539e | ||
|
|
52a883d34e | ||
|
|
5ac122b85f | ||
|
|
de2d7c33a3 | ||
|
|
2ca8a5ac61 | ||
|
|
e3078cc531 | ||
|
|
5495d0f8ab | ||
|
|
388b21d9db | ||
|
|
a162e8d9f9 | ||
|
|
235422c26d | ||
|
|
2bcaa17fc3 | ||
|
|
97291c9184 | ||
|
|
520ca4bcb0 | ||
|
|
f35fb974d0 | ||
|
|
f8c51d801a | ||
|
|
16c0c2f7a7 | ||
|
|
2145b3701d | ||
|
|
ce0ded7c78 | ||
|
|
31a78592e8 | ||
|
|
334a78f185 | ||
|
|
233d3e7f7b | ||
|
|
41a429b52c | ||
|
|
d00a30069a | ||
|
|
49488c0e71 | ||
|
|
4031568cdf | ||
|
|
fff9bf98eb | ||
|
|
6a7fc49c6b | ||
|
|
89f5d0d400 | ||
|
|
1eda82b95f | ||
|
|
623ec03dad | ||
|
|
c0de42e3df | ||
|
|
4893513800 | ||
|
|
100142067d | ||
|
|
3907cb0693 | ||
|
|
61dffabf97 | ||
|
|
bc27aa12cd | ||
|
|
0537b9546f | ||
|
|
0525c755f4 | ||
|
|
bcd91f536e | ||
|
|
f9c6c69fa8 | ||
|
|
0c46e0a9cc | ||
|
|
bca6d31b95 | ||
|
|
db72579f0e | ||
|
|
9b09bcc5f1 | ||
|
|
22e12904c9 | ||
|
|
b947056e62 | ||
|
|
e30898ddb3 | ||
|
|
072fa46bfd | ||
|
|
edc3a77b98 | ||
|
|
2b80848341 | ||
|
|
30fa462e33 | ||
|
|
11ac945b87 | ||
|
|
55c513b827 | ||
|
|
0eca0ac45a | ||
|
|
4be867c560 | ||
|
|
44b11ec257 | ||
|
|
53926a1ae6 | ||
|
|
5f383923df | ||
|
|
26eaedc491 | ||
|
|
7b63254a35 | ||
|
|
d0fd6c6c82 | ||
|
|
6862dd04ab | ||
|
|
e1b1631c65 | ||
|
|
1d74b547dd | ||
|
|
a3a4da6e3e | ||
|
|
e974c13c7a | ||
|
|
1999383443 | ||
|
|
bd0acd04b1 | ||
|
|
f25947e5eb | ||
|
|
f890fe6fd3 | ||
|
|
10f9d95cd2 | ||
|
|
013c757a84 | ||
|
|
ffa46c2461 | ||
|
|
48fd9d05b5 | ||
|
|
0b1f792290 | ||
|
|
c25fcf0001 | ||
|
|
ba2c79f310 | ||
|
|
c396124bc9 | ||
|
|
d35d7d2360 | ||
|
|
2738735321 | ||
|
|
1c74944cca | ||
|
|
412616bb96 | ||
|
|
518d5174e6 | ||
|
|
2656c1b8e1 | ||
|
|
635085d139 | ||
|
|
d1e81a0acf | ||
|
|
6094b95784 | ||
|
|
d37a5b03f1 | ||
|
|
1794d45ff3 | ||
|
|
7b0fbb6fef | ||
|
|
80d4bc1cea | ||
|
|
8763fb05ec | ||
|
|
73e2e2a794 | ||
|
|
479712cdc5 | ||
|
|
9efac0f067 | ||
|
|
a0fd619df3 | ||
|
|
1af87577e1 | ||
|
|
99f96c5cb7 | ||
|
|
4329ba316f | ||
|
|
a78aedec7e | ||
|
|
f0d5dc5696 | ||
|
|
e40289ce7a | ||
|
|
b6d1e16b4e | ||
|
|
e700ef3208 | ||
|
|
45d145a281 | ||
|
|
b2996eee87 | ||
|
|
21d04b895a | ||
|
|
40bb52fdd8 | ||
|
|
1242ac74ab | ||
|
|
3bfd41c48b | ||
|
|
4fb7e04686 | ||
|
|
dc0dbed96e | ||
|
|
0cba3154f0 | ||
|
|
fec476cc80 | ||
|
|
bc3d306dd7 | ||
|
|
5237337626 | ||
|
|
368094e15d | ||
|
|
0f93e76e80 | ||
|
|
083fa1803a | ||
|
|
c00c6b3957 | ||
|
|
cc39341eb9 | ||
|
|
bf7f82f7b2 | ||
|
|
eb857dbc45 | ||
|
|
e7620e951d | ||
|
|
286a25ae49 | ||
|
|
ae70046b49 | ||
|
|
c366933416 | ||
|
|
6a9716e8a1 | ||
|
|
b84ee4d240 | ||
|
|
8a1e54d58a | ||
|
|
3e032c4da6 | ||
|
|
7c3b267645 | ||
|
|
7161a99b04 | ||
|
|
1754c93370 | ||
|
|
4b750b6dc3 | ||
|
|
bf89bffb0b | ||
|
|
e2288fe441 | ||
|
|
8265dac127 | ||
|
|
100870e142 | ||
|
|
12fb7f2a0a | ||
|
|
f1bf4d899a | ||
|
|
591e4d8af1 | ||
|
|
dec6f80d2b | ||
|
|
ec8a748514 | ||
|
|
db15e52743 | ||
|
|
41bfb7a330 | ||
|
|
1c2e96a5ca | ||
|
|
28ff033da6 | ||
|
|
807a6b1022 | ||
|
|
296cc1bca2 | ||
|
|
951b058952 | ||
|
|
8f8e8a9285 | ||
|
|
46082f194c | ||
|
|
cb607e8551 | ||
|
|
3e3954eb38 | ||
|
|
517667c590 | ||
|
|
36c044562c | ||
|
|
f760ef15b0 | ||
|
|
e557ba82e7 | ||
|
|
7c292cc812 | ||
|
|
c94b797f00 | ||
|
|
4e513b8393 | ||
|
|
708a5c2070 | ||
|
|
92eaf52c21 | ||
|
|
b75d083035 | ||
|
|
b0460079c1 | ||
|
|
e20d4e192d | ||
|
|
8d8f331a4a | ||
|
|
793035de61 | ||
|
|
198028d627 | ||
|
|
c5ec6cd7ef | ||
|
|
73c5184518 | ||
|
|
f96c211198 | ||
|
|
6d2489a562 | ||
|
|
fa05a1ba8c | ||
|
|
a4489dec30 | ||
|
|
30e5243f5e | ||
|
|
06998b3484 | ||
|
|
3c66d93aba | ||
|
|
a5792f3c42 | ||
|
|
721cd740d8 | ||
|
|
de719ac409 | ||
|
|
0ffa7f3f57 | ||
|
|
4868d347db | ||
|
|
7fa141ea39 | ||
|
|
13196ddd92 | ||
|
|
eafb40460d | ||
|
|
4e2a9bb139 | ||
|
|
3b2239357f | ||
|
|
7be1f0a71c | ||
|
|
96f5a11fd5 | ||
|
|
7501bee430 | ||
|
|
445c5f13c3 | ||
|
|
ed98039aa5 | ||
|
|
2816780b52 | ||
|
|
c76bd7dcc4 | ||
|
|
48796a1b60 | ||
|
|
5711bacd83 | ||
|
|
70a840d3d5 | ||
|
|
37df662325 | ||
|
|
ca908270ec | ||
|
|
d47745a86b | ||
|
|
8c94ce8d14 | ||
|
|
0fe72e6fc5 | ||
|
|
c025e76f30 | ||
|
|
e1bf1e672e | ||
|
|
a5bbb500e6 | ||
|
|
a615f868a5 | ||
|
|
db302b15ea | ||
|
|
952f3ffb0c | ||
|
|
fe77fac23f | ||
|
|
e61091d240 | ||
|
|
50aacdf1f0 | ||
|
|
e56e8b7aa1 | ||
|
|
6d0816e85a | ||
|
|
abdf024517 | ||
|
|
5a1f6cb813 | ||
|
|
37d0ba1660 | ||
|
|
734c2fc870 | ||
|
|
490ec4350c | ||
|
|
0836439256 | ||
|
|
b197b698a4 | ||
|
|
67b18569cf | ||
|
|
dc0dd09e93 | ||
|
|
7ec76095e6 | ||
|
|
cb26552440 | ||
|
|
1ae5ac7d0b | ||
|
|
eeb7091180 | ||
|
|
dc38e6ae88 | ||
|
|
eb6cad7f93 | ||
|
|
ae30b46bfe | ||
|
|
eab268f5f8 | ||
|
|
1e21042138 | ||
|
|
a56f70ab94 | ||
|
|
11c57b9097 | ||
|
|
1921533c4c | ||
|
|
89e762fd6e | ||
|
|
a63d3ee625 | ||
|
|
82741ad207 | ||
|
|
bd363fe0b7 | ||
|
|
7a4c6d262f | ||
|
|
445a82f120 | ||
|
|
69ce121267 | ||
|
|
08e3cd1cce | ||
|
|
da0e5edbec | ||
|
|
c78fa42f31 | ||
|
|
993a3ebe73 | ||
|
|
8040502599 | ||
|
|
c84e8d1e09 | ||
|
|
5fb72eed85 | ||
|
|
400d62c1e6 | ||
|
|
857caf3637 | ||
|
|
aeca1fb575 | ||
|
|
ac2988a485 | ||
|
|
cb5ef250f4 | ||
|
|
23a0e18292 | ||
|
|
aa6c55dec1 | ||
|
|
7e0c24ec89 | ||
|
|
2c7d9b59c6 | ||
|
|
adb7763f87 | ||
|
|
89740490ac | ||
|
|
0b290f7206 | ||
|
|
1e7a3997e3 | ||
|
|
e7f8538e4d | ||
|
|
18a608a6ff | ||
|
|
1a22689328 | ||
|
|
ce65aea0ab | ||
|
|
45edad867c | ||
|
|
ea0a408849 | ||
|
|
18592af993 | ||
|
|
b1e0e7b923 | ||
|
|
1e212a8dc6 | ||
|
|
0e9d2a13af | ||
|
|
6494a9332d | ||
|
|
41baccb85d | ||
|
|
52eb7392c4 | ||
|
|
855c53ad02 | ||
|
|
004eded398 | ||
|
|
df48276300 | ||
|
|
accaf23aa3 | ||
|
|
de6fe4dc6f | ||
|
|
08f6a91441 | ||
|
|
95c9561e97 | ||
|
|
fcb19518c7 | ||
|
|
fbaf696821 | ||
|
|
abe3483a8f | ||
|
|
22e09334ec | ||
|
|
58592e3ef1 | ||
|
|
0126188ba7 | ||
|
|
5bdb6798a9 | ||
|
|
ab2729ab79 | ||
|
|
58e81fdffb | ||
|
|
0619a27872 | ||
|
|
0e52ce830a | ||
|
|
97437cad64 | ||
|
|
5b90a98650 | ||
|
|
96dae7bfec | ||
|
|
93a02c677e | ||
|
|
0d054f9b64 | ||
|
|
1107f6eb5f | ||
|
|
3650364017 | ||
|
|
086508f51a | ||
|
|
4ace451013 | ||
|
|
c9ea773a22 | ||
|
|
0f4ae7636d | ||
|
|
87d3a8363b | ||
|
|
c494ced21f | ||
|
|
a8e2fc6f61 | ||
|
|
aca1b45e93 | ||
|
|
5cb2a10138 | ||
|
|
411796606c | ||
|
|
1a9b54c9fa | ||
|
|
c7f4f15272 | ||
|
|
713527facf | ||
|
|
6e662dc9fc | ||
|
|
eb178caf3a | ||
|
|
adf3f641ce | ||
|
|
6157c766de | ||
|
|
745cd4744a | ||
|
|
faf15b4567 | ||
|
|
3746c899b7 | ||
|
|
7bbca12ff8 | ||
|
|
3967b39a17 | ||
|
|
2b2d24fe20 | ||
|
|
f4e112f404 | ||
|
|
87a0eecc31 | ||
|
|
f09dcb98eb | ||
|
|
f90870b99f | ||
|
|
75b58eb480 | ||
|
|
ed9cb923fb | ||
|
|
dd39556759 | ||
|
|
d5141c6d51 | ||
|
|
5675644341 | ||
|
|
1f30383866 | ||
|
|
40531ef247 | ||
|
|
366700dc36 | ||
|
|
f23fd683a9 | ||
|
|
755ed6f69f | ||
|
|
66662cd678 | ||
|
|
ec86db176e | ||
|
|
60e8630413 | ||
|
|
9d29dbbe5d | ||
|
|
8734fa65fc | ||
|
|
c53e5c5f17 | ||
|
|
74823e81e9 | ||
|
|
ef4b8a2cf8 | ||
|
|
54e27f551d | ||
|
|
59bdcdabba | ||
|
|
f6375ecbfc | ||
|
|
031b91c0ed | ||
|
|
e4c995a321 | ||
|
|
130d14cec9 | ||
|
|
e893ca1c9a | ||
|
|
156d96e582 | ||
|
|
9ba7611537 | ||
|
|
15d2dc3a4f | ||
|
|
f6df1a760d | ||
|
|
f71fcd440a | ||
|
|
c2bb11a794 | ||
|
|
1a00ea7c6e | ||
|
|
ec0a66c75b | ||
|
|
1658afc883 | ||
|
|
67aaeef537 | ||
|
|
13679284ac | ||
|
|
a514b65d81 | ||
|
|
7931af1078 | ||
|
|
4fc3446f24 | ||
|
|
fb4fdaf4c0 | ||
|
|
8e38ecdeb2 | ||
|
|
04623718ce | ||
|
|
9e857ed2d4 | ||
|
|
f30f9c50f8 | ||
|
|
96ba5c2b23 | ||
|
|
0dcd9794d4 | ||
|
|
34f0feb13a | ||
|
|
7778f50b50 | ||
|
|
fb2d85b9d5 | ||
|
|
7f0d4f6ba8 | ||
|
|
33212716cf | ||
|
|
707001c403 | ||
|
|
f93c6fbe4a | ||
|
|
70b9654671 | ||
|
|
0d7a77ba85 | ||
|
|
924b96856f | ||
|
|
f7929229c8 | ||
|
|
6b25eb2e79 | ||
|
|
bc08a951f1 | ||
|
|
a87c5515bd | ||
|
|
ebcd22b02b | ||
|
|
4b02b7e6f1 | ||
|
|
fdd823d2cb | ||
|
|
8ef504f745 | ||
|
|
960e850a78 | ||
|
|
ea701a4e9e | ||
|
|
6c573a5762 | ||
|
|
3ac858b150 | ||
|
|
f4372710bf | ||
|
|
f39477bbd5 | ||
|
|
6e5514419d | ||
|
|
f014b7b919 | ||
|
|
81484699db | ||
|
|
6d93d9c488 | ||
|
|
0930bccf88 | ||
|
|
e321bd3941 | ||
|
|
4b02937862 | ||
|
|
21e6849f2d | ||
|
|
39c2d1bc1a | ||
|
|
cd21b8dfa5 | ||
|
|
40fbdc87ce | ||
|
|
1814f4693d | ||
|
|
3f2b584c4e | ||
|
|
e0dd737822 | ||
|
|
d2d4fcc1df | ||
|
|
273ee09925 | ||
|
|
bb886868d2 | ||
|
|
f80ee472c2 | ||
|
|
a12ede3bbe | ||
|
|
97a8777d03 | ||
|
|
8a4c00d82e | ||
|
|
31f859e909 | ||
|
|
4d979a1ce9 | ||
|
|
4465cdf8bc | ||
|
|
3938b61c3f | ||
|
|
cdef503db6 | ||
|
|
df08984a58 | ||
|
|
cf838c71f7 | ||
|
|
9a001051d6 | ||
|
|
5548a8eb7a | ||
|
|
727df34aa1 | ||
|
|
9587a523b3 | ||
|
|
22e44642a0 | ||
|
|
c00520281b | ||
|
|
587c89d979 | ||
|
|
310fba4c12 | ||
|
|
c1d06d9501 | ||
|
|
4735575e8d | ||
|
|
767e1c6f58 | ||
|
|
83fcb49894 | ||
|
|
5fd88481b6 | ||
|
|
69c63e94bf | ||
|
|
3d91f7c975 | ||
|
|
7945430e64 | ||
|
|
e2120c4728 | ||
|
|
56b5352f64 | ||
|
|
55d5e03639 | ||
|
|
cca17f5306 | ||
|
|
ffcaffa32f | ||
|
|
0ffd80f380 | ||
|
|
25151b14e7 | ||
|
|
428c5c02ce | ||
|
|
e2e5643c3c | ||
|
|
956d212e99 | ||
|
|
fff8805ff6 | ||
|
|
a69afc9eeb | ||
|
|
0bf9645f2f | ||
|
|
6bfad8fce8 | ||
|
|
6ebab5db07 | ||
|
|
34aa89a7d6 | ||
|
|
44e4f754dd | ||
|
|
60c218efdb | ||
|
|
8d290eb055 | ||
|
|
cd17aa2cab | ||
|
|
edaff81c0a | ||
|
|
dd7d6188f2 | ||
|
|
9be6f1a70e | ||
|
|
b0cce98648 | ||
|
|
e159f76ced | ||
|
|
07d3af21c2 | ||
|
|
1ed0116147 | ||
|
|
c5663689a3 | ||
|
|
9caaaa49b6 | ||
|
|
c18dc6a629 | ||
|
|
b1fbd87680 | ||
|
|
be630691b1 | ||
|
|
aa1c274231 | ||
|
|
78c2844e3f | ||
|
|
57a7f4391f | ||
|
|
0970aed596 | ||
|
|
342ade501a | ||
|
|
35316e9142 | ||
|
|
2a0775da03 | ||
|
|
8bd67f72dd | ||
|
|
82f190285a | ||
|
|
f5590c3345 | ||
|
|
9be07de7c6 | ||
|
|
327604719a | ||
|
|
e8ef586cef | ||
|
|
c014fc5ec5 | ||
|
|
fb078068b4 | ||
|
|
b2c9e7b07b | ||
|
|
d117b4b570 | ||
|
|
80fc238bec | ||
|
|
7e4e2f3720 | ||
|
|
5e9c7bc022 | ||
|
|
55afa625fc | ||
|
|
f2e9b40ad1 | ||
|
|
6697b8fde3 | ||
|
|
4f20c900d0 | ||
|
|
0471daf771 | ||
|
|
5788ad2529 | ||
|
|
472affb6a3 | ||
|
|
802b933778 | ||
|
|
f1ec7fe55b | ||
|
|
d842197931 | ||
|
|
4e7510dea9 | ||
|
|
c0f353c0e8 | ||
|
|
e95d005c21 | ||
|
|
11e9d575c8 | ||
|
|
46bbc78e82 | ||
|
|
50a621bc7b | ||
|
|
d0c84916c7 | ||
|
|
6db8dc33f2 | ||
|
|
194501c958 | ||
|
|
27a34609a1 | ||
|
|
ffc14a77c6 | ||
|
|
31119ed61a | ||
|
|
070bf3b776 | ||
|
|
ade8d79d42 | ||
|
|
42917d707d | ||
|
|
1522cf74bc | ||
|
|
3b7a57d108 | ||
|
|
a7d9268e4d | ||
|
|
052dc13487 | ||
|
|
249bcb3a01 | ||
|
|
fbe52faf49 | ||
|
|
8b86171642 | ||
|
|
e19d6e993d | ||
|
|
ef0473c091 | ||
|
|
3406a3ba95 | ||
|
|
7c1ed420a9 | ||
|
|
d7e86af6c6 | ||
|
|
76cf326290 | ||
|
|
6c3e187d1d | ||
|
|
e32a516b5f | ||
|
|
6da83ac9f5 | ||
|
|
adc07eddf6 | ||
|
|
9c88efd55f | ||
|
|
ffcb57580f | ||
|
|
abfbd13f17 | ||
|
|
f63cdbfcfa | ||
|
|
b2d82da20d | ||
|
|
70fddb6523 | ||
|
|
83cfd308b4 | ||
|
|
36acaf5e21 | ||
|
|
572ccfe3e2 | ||
|
|
253049a4cc | ||
|
|
8f199e12b3 | ||
|
|
f6fac3e949 | ||
|
|
0b193b76c2 | ||
|
|
e6f0ed65be | ||
|
|
b13b15758d | ||
|
|
c48eb4241a | ||
|
|
0f8290485e | ||
|
|
3d1edd2492 | ||
|
|
a73db6fd84 | ||
|
|
a05dc6cc47 | ||
|
|
5440d1dc3b | ||
|
|
f13e6ca631 | ||
|
|
81553b4da7 | ||
|
|
07618f8674 | ||
|
|
1555a4da7f | ||
|
|
a20a5f61f0 | ||
|
|
4bcc38cf63 | ||
|
|
05f25e600e | ||
|
|
a744dee94c | ||
|
|
78bd0341a8 | ||
|
|
b5de49917c | ||
|
|
c845e245a1 | ||
|
|
4a787986cd | ||
|
|
78a41828fc | ||
|
|
bd0c9913cf | ||
|
|
d904dfa191 | ||
|
|
fa40ccece1 | ||
|
|
7919310dc6 | ||
|
|
7669af578a | ||
|
|
739e99c4d9 | ||
|
|
7502997e7e | ||
|
|
4470cd5aaa | ||
|
|
466e8a5cd0 | ||
|
|
4142a431b5 | ||
|
|
5565afdd9f | ||
|
|
0db3b7a530 | ||
|
|
b37ecc3cf4 | ||
|
|
ec5a5d5218 | ||
|
|
7980c8cea2 | ||
|
|
e9b68a224c | ||
|
|
8fd6b1d428 | ||
|
|
4198b5061f | ||
|
|
b0a525a504 | ||
|
|
25d904dc37 | ||
|
|
777a30e870 | ||
|
|
aea66ff25a | ||
|
|
9dd319f4da | ||
|
|
8b4a1f52d0 | ||
|
|
7a9f317ee1 | ||
|
|
48efea99e5 | ||
|
|
fbef856a97 | ||
|
|
3b7c760ffd | ||
|
|
ae776ff8df | ||
|
|
24a637e9e6 | ||
|
|
f53475a204 | ||
|
|
3fbc51cd38 | ||
|
|
5f1aba9a37 | ||
|
|
95f7a26bce | ||
|
|
6e1828ff63 | ||
|
|
89c53c508b | ||
|
|
9918cb4ffc | ||
|
|
7b61f800c3 | ||
|
|
33d47063e8 | ||
|
|
e67b91977d | ||
|
|
ac603e9228 | ||
|
|
632b6f8fbd | ||
|
|
b247ef2632 | ||
|
|
5a6d79a66e | ||
|
|
1e33cc9720 | ||
|
|
ee465c0890 | ||
|
|
236816cb93 | ||
|
|
3ee9bc097f | ||
|
|
f205ce14c1 | ||
|
|
8199e0a7a9 | ||
|
|
f18fc40436 | ||
|
|
f509c65509 | ||
|
|
bf062db83f | ||
|
|
9907523321 | ||
|
|
915faabe25 | ||
|
|
a9b6801b22 | ||
|
|
d76f1fe356 | ||
|
|
3bdceb1d6b | ||
|
|
cce8b60515 | ||
|
|
06eacb87d8 | ||
|
|
865f1f2ea6 | ||
|
|
f59a26cb80 | ||
|
|
d384ac52a1 | ||
|
|
42fa03aa4a | ||
|
|
dae1f990a5 | ||
|
|
33398b0b6b | ||
|
|
192e843989 | ||
|
|
8851f4571c | ||
|
|
7fed8334b9 | ||
|
|
6af2657d7e | ||
|
|
933a57af7a | ||
|
|
e5b85ff1a0 | ||
|
|
c382aa04e4 | ||
|
|
86141c1eef | ||
|
|
d67c9b66d3 | ||
|
|
6158b20a8c | ||
|
|
485a234263 | ||
|
|
7226a87c6d | ||
|
|
7aca2bcbc0 | ||
|
|
3840d57f8d | ||
|
|
b7f3425f36 | ||
|
|
9fe0c30299 | ||
|
|
9d126760fa | ||
|
|
09673ba0c6 | ||
|
|
bc7d71ee3d | ||
|
|
78a31449aa | ||
|
|
0361d303f2 | ||
|
|
2fe94736e6 | ||
|
|
9589f25e57 | ||
|
|
15c2556e06 | ||
|
|
f342a2a54b | ||
|
|
598dc991e8 | ||
|
|
7d59dc6c18 | ||
|
|
05f01a5d94 | ||
|
|
03c6fb2f82 | ||
|
|
7a657bf3c7 | ||
|
|
02d14c7e9b | ||
|
|
32c849e18c | ||
|
|
c27fe5694d | ||
|
|
b0ebf56283 | ||
|
|
fb33a8ad6b | ||
|
|
9207562545 | ||
|
|
230eb20a34 | ||
|
|
e0d9542bfe | ||
|
|
063951cffa | ||
|
|
dae0872379 | ||
|
|
123f4a6d9d | ||
|
|
65679892b0 | ||
|
|
c4979cf6d6 | ||
|
|
09d498bb80 | ||
|
|
90ed7aa121 | ||
|
|
206c0d0933 | ||
|
|
81c539c516 | ||
|
|
75b3b31d11 | ||
|
|
90c39de0cc | ||
|
|
241864b43c | ||
|
|
b2d839d4e7 | ||
|
|
8bdf409d07 | ||
|
|
08da982de5 | ||
|
|
f250425ef1 | ||
|
|
aebcc495f9 | ||
|
|
eaffbc0f92 | ||
|
|
37f2f2cdf6 | ||
|
|
82f5706350 | ||
|
|
ad2902b3ee | ||
|
|
ce2c6c01da | ||
|
|
fb4c7d288c | ||
|
|
838b2a6a34 | ||
|
|
fd0de9eb6c | ||
|
|
6b89991ba8 | ||
|
|
4727570870 | ||
|
|
ebf0541385 | ||
|
|
af3d380b6a | ||
|
|
cf3de8cf35 | ||
|
|
61b4de581d | ||
|
|
031fd72dfd | ||
|
|
1d249a877e | ||
|
|
3eb6045dee |
No files matched your search
@@ -1,5 +1,20 @@
|
|||||||
|
comment: false
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
range: "40...100"
|
range: "40...100"
|
||||||
|
precision: 1
|
||||||
|
status:
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
informational: true
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
informational: true
|
||||||
|
|
||||||
|
github_checks:
|
||||||
|
annotations: false
|
||||||
|
|
||||||
ignore:
|
ignore:
|
||||||
- "**.pb.go"
|
- "**.pb.go"
|
||||||
|
- "**_mocked.go"
|
||||||
|
- "**/mocks/*"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
version = 1
|
||||||
|
|
||||||
|
exclude_patterns = ["**/*.pb.go"]
|
||||||
|
test_patterns = ["**/*_test.go"]
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "go"
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[analyzers.meta]
|
||||||
|
import_paths = ["github.com/syncthing/syncthing"]
|
||||||
|
build_tags = ["noassets"]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
name: Build Infrastructure Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- infrastructure
|
||||||
|
|
||||||
|
env:
|
||||||
|
GO_VERSION: "~1.21.5"
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
BUILD_USER: docker
|
||||||
|
BUILD_HOST: github.syncthing.net
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker-syncthing:
|
||||||
|
name: Build and push Docker images
|
||||||
|
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@v4
|
||||||
|
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: 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: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile.${{ matrix.pkg }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: syncthing/${{ matrix.pkg }}:latest,syncthing/${{ matrix.pkg }}:${{ github.sha }}
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.revision=${{ github.sha }}
|
||||||
@@ -0,0 +1,830 @@
|
|||||||
|
name: Build Syncthing
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
schedule:
|
||||||
|
# Run nightly build at 05:00 UTC
|
||||||
|
- cron: '00 05 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
# The go version to use for builds. We set check-latest to true when
|
||||||
|
# installing, so we get the latest patch version that matches the
|
||||||
|
# expression.
|
||||||
|
GO_VERSION: "~1.21.5"
|
||||||
|
|
||||||
|
# Optimize compatibility on the slow archictures.
|
||||||
|
GO386: softfloat
|
||||||
|
GOARM: "5"
|
||||||
|
GOMIPS: softfloat
|
||||||
|
|
||||||
|
# Avoid hilarious amounts of obscuring log output when running tests.
|
||||||
|
LOGGER_DISCARD: "1"
|
||||||
|
|
||||||
|
# Our build metadata
|
||||||
|
BUILD_USER: builder
|
||||||
|
BUILD_HOST: github.syncthing.net
|
||||||
|
|
||||||
|
# A note on actions and third party code... The actions under actions/ (like
|
||||||
|
# `uses: actions/checkout`) are maintained by GitHub, and we need to trust
|
||||||
|
# GitHub to maintain their code and infrastructure or we're in deep shit in
|
||||||
|
# general. The same doesn't necessarily apply to other actions authors, so
|
||||||
|
# some care needs to be taken when adding steps, especially in the paths
|
||||||
|
# that lead up to code being packaged and signed.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
#
|
||||||
|
# Tests for all platforms. Runs a matrix build on Windows, Linux and Mac,
|
||||||
|
# with the list of expected supported Go versions (current, previous).
|
||||||
|
#
|
||||||
|
|
||||||
|
build-test:
|
||||||
|
name: Build and test
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
runner: ["windows-latest", "ubuntu-latest", "macos-latest"]
|
||||||
|
# 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.
|
||||||
|
go: ["~1.20.12", "~1.21.5"]
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
steps:
|
||||||
|
- name: Set git to use LF
|
||||||
|
if: matrix.runner == 'windows-latest'
|
||||||
|
# Without this, the Windows checkout will happen with CRLF line
|
||||||
|
# endings, which is fine for the source code but messes up tests
|
||||||
|
# that depend on data on disk being as expected. Ideally, those
|
||||||
|
# tests should be fixed, but not today.
|
||||||
|
run: |
|
||||||
|
git config --global core.autocrlf false
|
||||||
|
git config --global core.eol lf
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
cache: true
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
go run build.go
|
||||||
|
|
||||||
|
- name: Install go-test-json-to-loki
|
||||||
|
run: |
|
||||||
|
go install calmh.dev/go-test-json-to-loki@latest
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
go version
|
||||||
|
go run build.go test | go-test-json-to-loki
|
||||||
|
env:
|
||||||
|
GOFLAGS: "-json"
|
||||||
|
LOKI_URL: ${{ vars.LOKI_URL }}
|
||||||
|
LOKI_USER: ${{ vars.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
|
||||||
|
#
|
||||||
|
|
||||||
|
correctness:
|
||||||
|
name: Check correctness
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: false
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Check correctness
|
||||||
|
run: |
|
||||||
|
go test -v ./meta
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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".)
|
||||||
|
#
|
||||||
|
|
||||||
|
basics:
|
||||||
|
name: Basic checks passed
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- build-test
|
||||||
|
- correctness
|
||||||
|
- package-linux
|
||||||
|
- package-cross
|
||||||
|
- package-source
|
||||||
|
- package-debian
|
||||||
|
- govulncheck
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
#
|
||||||
|
# Windows
|
||||||
|
#
|
||||||
|
|
||||||
|
package-windows:
|
||||||
|
name: Package for Windows
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-'))
|
||||||
|
environment: signing
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Set git to use LF
|
||||||
|
# Without this, the checkout will happen with CRLF line endings,
|
||||||
|
# which is fine for the source code but messes up tests that depend
|
||||||
|
# on data on disk being as expected. Ideally, those tests should be
|
||||||
|
# fixed, but not today.
|
||||||
|
run: |
|
||||||
|
git config --global core.autocrlf false
|
||||||
|
git config --global core.eol lf
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
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@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~\AppData\Local\go-build
|
||||||
|
~\go\pkg\mod
|
||||||
|
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-${{ hashFiles('**/go.sum') }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@v1.4.0
|
||||||
|
|
||||||
|
- name: Create packages
|
||||||
|
run: |
|
||||||
|
go run build.go -goarch amd64 zip
|
||||||
|
go run build.go -goarch arm zip
|
||||||
|
go run build.go -goarch arm64 zip
|
||||||
|
go run build.go -goarch 386 zip
|
||||||
|
env:
|
||||||
|
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
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: packages-windows
|
||||||
|
path: syncthing-windows-*.zip
|
||||||
|
|
||||||
|
#
|
||||||
|
# Linux
|
||||||
|
#
|
||||||
|
|
||||||
|
package-linux:
|
||||||
|
name: Package for Linux
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
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@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-${{ hashFiles('**/go.sum') }}
|
||||||
|
|
||||||
|
- name: Create packages
|
||||||
|
run: |
|
||||||
|
archs=$(go tool dist list | grep linux | sed 's#linux/##')
|
||||||
|
for goarch in $archs ; do
|
||||||
|
go run build.go -goarch "$goarch" tar
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
|
||||||
|
- name: Archive artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: packages-linux
|
||||||
|
path: syncthing-linux-*.tar.gz
|
||||||
|
|
||||||
|
#
|
||||||
|
# macOS
|
||||||
|
#
|
||||||
|
|
||||||
|
package-macos:
|
||||||
|
name: Package for macOS
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-'))
|
||||||
|
environment: signing
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
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@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-${{ hashFiles('**/go.sum') }}
|
||||||
|
|
||||||
|
- name: Import signing certificate
|
||||||
|
run: |
|
||||||
|
# Set up a run-specific keychain, making it available for the
|
||||||
|
# `codesign` tool.
|
||||||
|
umask 066
|
||||||
|
KEYCHAIN_PATH=$RUNNER_TEMP/codesign.keychain
|
||||||
|
KEYCHAIN_PASSWORD=$(uuidgen)
|
||||||
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
security default-keychain -s "$KEYCHAIN_PATH"
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
|
||||||
|
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
|
# Import the certificate
|
||||||
|
CERTIFICATE_PATH=$RUNNER_TEMP/codesign.p12
|
||||||
|
echo "$DEVELOPER_ID_CERTIFICATE_BASE64" | base64 -d -o "$CERTIFICATE_PATH"
|
||||||
|
security import "$CERTIFICATE_PATH" -k "$KEYCHAIN_PATH" -P "$DEVELOPER_ID_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productsign
|
||||||
|
security set-key-partition-list -S apple-tool:,apple: -s -k actions "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
|
# Set the codesign identity for following steps
|
||||||
|
echo "CODESIGN_IDENTITY=$CODESIGN_IDENTITY" >> $GITHUB_ENV
|
||||||
|
env:
|
||||||
|
DEVELOPER_ID_CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }}
|
||||||
|
DEVELOPER_ID_CERTIFICATE_PASSWORD: ${{ secrets.DEVELOPER_ID_CERTIFICATE_PASSWORD }}
|
||||||
|
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
|
||||||
|
|
||||||
|
- name: Create package (amd64)
|
||||||
|
run: |
|
||||||
|
go run build.go -goarch amd64 zip
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "1"
|
||||||
|
|
||||||
|
- name: Create package (arm64 cross)
|
||||||
|
run: |
|
||||||
|
cat <<EOT > xgo.sh
|
||||||
|
#!/bin/bash
|
||||||
|
CGO_ENABLED=1 \
|
||||||
|
CGO_CFLAGS="-target arm64-apple-macos10.15" \
|
||||||
|
CGO_LDFLAGS="-target arm64-apple-macos10.15" \
|
||||||
|
go "\$@"
|
||||||
|
EOT
|
||||||
|
chmod 755 xgo.sh
|
||||||
|
go run build.go -gocmd ./xgo.sh -goarch arm64 zip
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "1"
|
||||||
|
|
||||||
|
- name: Create package (universal)
|
||||||
|
run: |
|
||||||
|
rm -rf _tmp
|
||||||
|
mkdir _tmp
|
||||||
|
pushd _tmp
|
||||||
|
|
||||||
|
unzip ../syncthing-macos-amd64-*.zip
|
||||||
|
unzip ../syncthing-macos-arm64-*.zip
|
||||||
|
lipo -create syncthing-macos-amd64-*/syncthing syncthing-macos-arm64-*/syncthing -o syncthing
|
||||||
|
|
||||||
|
amd64=(syncthing-macos-amd64-*)
|
||||||
|
universal="${amd64/amd64/universal}"
|
||||||
|
mv "$amd64" "$universal"
|
||||||
|
mv syncthing "$universal"
|
||||||
|
zip -r "../$universal.zip" "$universal"
|
||||||
|
|
||||||
|
- name: Archive artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: packages-macos
|
||||||
|
path: syncthing-*.zip
|
||||||
|
|
||||||
|
notarize-macos:
|
||||||
|
name: Notarize for macOS
|
||||||
|
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-'))
|
||||||
|
environment: signing
|
||||||
|
needs:
|
||||||
|
- package-macos
|
||||||
|
- basics
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
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 syncthing-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
|
||||||
|
#
|
||||||
|
|
||||||
|
package-cross:
|
||||||
|
name: Package cross compiled
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
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@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-cross-${{ hashFiles('**/go.sum') }}
|
||||||
|
|
||||||
|
- name: Create packages
|
||||||
|
run: |
|
||||||
|
platforms=$(go tool dist list \
|
||||||
|
| grep -v aix/ppc64 \
|
||||||
|
| grep -v android/ \
|
||||||
|
| grep -v darwin/ \
|
||||||
|
| grep -v ios/ \
|
||||||
|
| grep -v js/ \
|
||||||
|
| grep -v linux/ \
|
||||||
|
| grep -v nacl/ \
|
||||||
|
| grep -v plan9/ \
|
||||||
|
| 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
|
||||||
|
goos="${plat%/*}"
|
||||||
|
goarch="${plat#*/}"
|
||||||
|
echo "::group ::$plat"
|
||||||
|
if ! go run build.go -goos "$goos" -goarch "$goarch" tar 2>/dev/null; then
|
||||||
|
echo "::warning ::Failed to build for $plat"
|
||||||
|
fi
|
||||||
|
echo "::endgroup::"
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
|
||||||
|
- name: Archive artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: packages-other
|
||||||
|
path: syncthing-*.tar.gz
|
||||||
|
|
||||||
|
#
|
||||||
|
# Source
|
||||||
|
#
|
||||||
|
|
||||||
|
package-source:
|
||||||
|
name: Package source code
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
cache: false
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Package source
|
||||||
|
run: |
|
||||||
|
version=$(go run build.go version)
|
||||||
|
echo "$version" > RELEASE
|
||||||
|
|
||||||
|
go mod vendor
|
||||||
|
go run build.go assets
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
tar c -z -f "syncthing-source-$version.tar.gz" \
|
||||||
|
--exclude .git \
|
||||||
|
syncthing
|
||||||
|
|
||||||
|
mv "syncthing-source-$version.tar.gz" syncthing
|
||||||
|
|
||||||
|
- name: Archive artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: packages-source
|
||||||
|
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' || startsWith(github.ref, 'refs/heads/release-'))
|
||||||
|
environment: signing
|
||||||
|
needs:
|
||||||
|
- basics
|
||||||
|
- package-windows
|
||||||
|
- package-linux
|
||||||
|
- package-macos
|
||||||
|
- package-cross
|
||||||
|
- package-source
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: syncthing/release-tools
|
||||||
|
path: tools
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
|
||||||
|
- name: Install signing tool
|
||||||
|
run: |
|
||||||
|
go install ./cmd/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 and sign .asc files
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt -y install gnupg
|
||||||
|
|
||||||
|
export SIGNING_KEY="$RUNNER_TEMP/gpg-secret.asc"
|
||||||
|
echo "$GNUPG_SIGNING_KEY_BASE64" | base64 -d > "$SIGNING_KEY"
|
||||||
|
gpg --import < "$SIGNING_KEY"
|
||||||
|
|
||||||
|
pushd packages
|
||||||
|
files=(*.tar.gz *.zip)
|
||||||
|
sha1sum "${files[@]}" | gpg --clearsign > sha1sum.txt.asc
|
||||||
|
sha256sum "${files[@]}" | gpg --clearsign > sha256sum.txt.asc
|
||||||
|
gpg --sign --armour --detach syncthing-source-*.tar.gz
|
||||||
|
popd
|
||||||
|
rm -f "$SIGNING_KEY" .gnupg
|
||||||
|
env:
|
||||||
|
GNUPG_SIGNING_KEY_BASE64: ${{ secrets.GNUPG_SIGNING_KEY_BASE64 }}
|
||||||
|
|
||||||
|
- name: Archive artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: packages-signed
|
||||||
|
path: packages/*
|
||||||
|
|
||||||
|
#
|
||||||
|
# Debian
|
||||||
|
#
|
||||||
|
|
||||||
|
package-debian:
|
||||||
|
name: Package for Debian
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
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@v3
|
||||||
|
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
|
||||||
|
go run build.go -no-upgrade -installsuffix=no-upgrade -goarch "$arch" deb
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
BUILD_USER: debian
|
||||||
|
|
||||||
|
- name: Archive artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
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: signing
|
||||||
|
needs:
|
||||||
|
- sign-for-upgrade
|
||||||
|
- notarize-macos
|
||||||
|
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@v3
|
||||||
|
with:
|
||||||
|
name: packages-signed
|
||||||
|
path: packages
|
||||||
|
|
||||||
|
- name: Create release json
|
||||||
|
run: |
|
||||||
|
cd packages
|
||||||
|
"$GITHUB_WORKSPACE/tools/generate-release-json" "$BASE_URL" > nightly.json
|
||||||
|
env:
|
||||||
|
BASE_URL: https://syncthing.ams3.digitaloceanspaces.com/nightly/
|
||||||
|
|
||||||
|
- name: Push artifacts
|
||||||
|
uses: docker://docker.io/rclone/rclone:latest
|
||||||
|
env:
|
||||||
|
RCLONE_CONFIG_SPACES_TYPE: s3
|
||||||
|
RCLONE_CONFIG_SPACES_PROVIDER: DigitalOcean
|
||||||
|
RCLONE_CONFIG_SPACES_ACCESS_KEY_ID: ${{ secrets.SPACES_KEY }}
|
||||||
|
RCLONE_CONFIG_SPACES_SECRET_ACCESS_KEY: ${{ secrets.SPACES_SECRET }}
|
||||||
|
RCLONE_CONFIG_SPACES_ENDPOINT: ams3.digitaloceanspaces.com
|
||||||
|
RCLONE_CONFIG_SPACES_ACL: public-read
|
||||||
|
with:
|
||||||
|
args: sync packages spaces:syncthing/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'
|
||||||
|
environment: signing
|
||||||
|
needs:
|
||||||
|
- sign-for-upgrade
|
||||||
|
- package-debian
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Download signed packages
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: packages-signed
|
||||||
|
path: packages
|
||||||
|
|
||||||
|
- name: Download debian packages
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: debian-packages
|
||||||
|
path: packages
|
||||||
|
|
||||||
|
- name: Set version
|
||||||
|
run: |
|
||||||
|
version=$(go run build.go version)
|
||||||
|
echo "VERSION=$version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Push to Spaces (${{ env.VERSION }})
|
||||||
|
uses: docker://docker.io/rclone/rclone:latest
|
||||||
|
env:
|
||||||
|
RCLONE_CONFIG_SPACES_TYPE: s3
|
||||||
|
RCLONE_CONFIG_SPACES_PROVIDER: DigitalOcean
|
||||||
|
RCLONE_CONFIG_SPACES_ACCESS_KEY_ID: ${{ secrets.SPACES_KEY }}
|
||||||
|
RCLONE_CONFIG_SPACES_SECRET_ACCESS_KEY: ${{ secrets.SPACES_SECRET }}
|
||||||
|
RCLONE_CONFIG_SPACES_ENDPOINT: ams3.digitaloceanspaces.com
|
||||||
|
RCLONE_CONFIG_SPACES_ACL: public-read
|
||||||
|
with:
|
||||||
|
args: sync packages spaces:syncthing/release/${{ env.VERSION }}
|
||||||
|
|
||||||
|
- name: Push to Spaces (latest)
|
||||||
|
uses: docker://docker.io/rclone/rclone:latest
|
||||||
|
env:
|
||||||
|
RCLONE_CONFIG_SPACES_TYPE: s3
|
||||||
|
RCLONE_CONFIG_SPACES_PROVIDER: DigitalOcean
|
||||||
|
RCLONE_CONFIG_SPACES_ACCESS_KEY_ID: ${{ secrets.SPACES_KEY }}
|
||||||
|
RCLONE_CONFIG_SPACES_SECRET_ACCESS_KEY: ${{ secrets.SPACES_SECRET }}
|
||||||
|
RCLONE_CONFIG_SPACES_ENDPOINT: ams3.digitaloceanspaces.com
|
||||||
|
RCLONE_CONFIG_SPACES_ACL: public-read
|
||||||
|
with:
|
||||||
|
args: sync spaces:syncthing/release/${{ env.VERSION }} spaces:syncthing/release/latest
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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/release' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/infrastructure' || startsWith(github.ref, 'refs/heads/release-'))
|
||||||
|
environment: docker
|
||||||
|
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
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
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@v3
|
||||||
|
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:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_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=${{ matrix.image }}:$version,${{ matrix.image }}:$major,${{ matrix.image }}:$minor,${{ matrix.image }}:latest
|
||||||
|
elif [[ $version == *-rc.@([0-9]|[0-9][0-9]) ]] ; then
|
||||||
|
echo Release candidate, pushing to :rc
|
||||||
|
tags=${{ matrix.image }}:rc
|
||||||
|
else
|
||||||
|
echo Development version, pushing to :edge
|
||||||
|
tags=${{ 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@v4
|
||||||
|
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,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
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
name: Update translations and documentation
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '42 3 * * 1'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
update_transifex_docs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Update translations and documentation
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ^1.19.6
|
||||||
|
- run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config --global user.name 'Syncthing Release Automation'
|
||||||
|
git config --global user.email 'release@syncthing.net'
|
||||||
|
bash build.sh translate
|
||||||
|
bash build.sh prerelease
|
||||||
|
git push
|
||||||
|
env:
|
||||||
|
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
|
||||||
+2
-2
@@ -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
|
||||||
@@ -18,3 +17,4 @@ deb
|
|||||||
*.bz2
|
*.bz2
|
||||||
/repos
|
/repos
|
||||||
/proto/scripts/protoc-gen-gosyncthing
|
/proto/scripts/protoc-gen-gosyncthing
|
||||||
|
/gui/next-gen-gui
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
line_ending: lf
|
||||||
|
formatter:
|
||||||
|
type: basic
|
||||||
|
retain_line_breaks: true
|
||||||
@@ -18,24 +18,33 @@ Adam Piggott (ProactiveServices) <aD@simplypeachy.co.uk> <simplypeachy@users.nor
|
|||||||
Adel Qalieh (adelq) <aqalieh95@gmail.com> <adelq@users.noreply.github.com>
|
Adel Qalieh (adelq) <aqalieh95@gmail.com> <adelq@users.noreply.github.com>
|
||||||
Alan Pope <alan@popey.com>
|
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>
|
||||||
Alessandro G. (alessandro.g89) <alessandro.g89@gmail.com>
|
Alessandro G. (alessandro.g89) <alessandro.g89@gmail.com>
|
||||||
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 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>
|
||||||
andresvia <andres.via@gmail.com>
|
andresvia <andres.via@gmail.com>
|
||||||
Andrew Dunham (andrew-d) <andrew@du.nham.ca>
|
Andrew Dunham (andrew-d) <andrew@du.nham.ca>
|
||||||
|
Andrew Meyer <andrewm.bpi@gmail.com>
|
||||||
Andrew Rabert (nvllsvm) <ar@nullsum.net> <6550543+nvllsvm@users.noreply.github.com>
|
Andrew Rabert (nvllsvm) <ar@nullsum.net> <6550543+nvllsvm@users.noreply.github.com>
|
||||||
Andrey D (scienmind) <scintertech@cryptolab.net> <scienmind@users.noreply.github.com>
|
Andrey D (scienmind) <scintertech@cryptolab.net> <scienmind@users.noreply.github.com>
|
||||||
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>
|
||||||
Aranjedeath <Aranjedeath@users.noreply.github.com>
|
Aranjedeath <Aranjedeath@users.noreply.github.com>
|
||||||
Arkadiusz Tymiński <gevleeog@gmail.com>
|
Arkadiusz Tymiński <gevleeog@gmail.com>
|
||||||
|
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>
|
||||||
Audrius Butkevicius (AudriusButkevicius) <audrius.butkevicius@gmail.com> <github@audrius.rocks>
|
Audrius Butkevicius (AudriusButkevicius) <audrius.butkevicius@gmail.com> <github@audrius.rocks>
|
||||||
@@ -48,6 +57,7 @@ Ben Shepherd (benshep) <bjashepherd@gmail.com>
|
|||||||
Ben Sidhom (bsidhom) <bsidhom@gmail.com>
|
Ben Sidhom (bsidhom) <bsidhom@gmail.com>
|
||||||
Benedikt Heine (bebehei) <bebe@bebehei.de>
|
Benedikt Heine (bebehei) <bebe@bebehei.de>
|
||||||
Benedikt Morbach <benedikt.morbach@googlemail.com>
|
Benedikt Morbach <benedikt.morbach@googlemail.com>
|
||||||
|
Benjamin Nater <17193640+bn4t@users.noreply.github.com>
|
||||||
Benno Fünfstück <benno.fuenfstueck@gmail.com>
|
Benno Fünfstück <benno.fuenfstueck@gmail.com>
|
||||||
Benny Ng (tpng) <benny.tpng@gmail.com>
|
Benny Ng (tpng) <benny.tpng@gmail.com>
|
||||||
boomsquared <54829195+boomsquared@users.noreply.github.com>
|
boomsquared <54829195+boomsquared@users.noreply.github.com>
|
||||||
@@ -56,51 +66,76 @@ Boris Rybalkin <ribalkin@gmail.com>
|
|||||||
Brandon Philips (philips) <brandon@ifup.org>
|
Brandon Philips (philips) <brandon@ifup.org>
|
||||||
Brendan Long (brendanlong) <self@brendanlong.com>
|
Brendan Long (brendanlong) <self@brendanlong.com>
|
||||||
Brian R. Becker (brbecker) <brbecker@gmail.com>
|
Brian R. Becker (brbecker) <brbecker@gmail.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> <1937689+yan12125@users.noreply.github.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>
|
||||||
chucic <chucic@seznam.cz>
|
chucic <chucic@seznam.cz>
|
||||||
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>
|
||||||
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 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>
|
||||||
Darshil Chanpura (dtchanpura) <dtchanpura@gmail.com> <dcprime314@gmail.com>
|
Darshil Chanpura (dtchanpura) <dtchanpura@gmail.com> <dcprime314@gmail.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>
|
||||||
|
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>
|
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>
|
||||||
|
digital <didev@dinid.net>
|
||||||
|
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>
|
||||||
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>
|
||||||
|
entity0xfe <109791748+entity0xfe@users.noreply.github.com> <entity0xfe@my.domain>
|
||||||
|
Eric Lesiuta <elesiuta@gmail.com>
|
||||||
|
Eric P <eric@kastelo.net>
|
||||||
Erik Meitner (WSGCSysadmin) <e.meitner@willystreet.coop>
|
Erik Meitner (WSGCSysadmin) <e.meitner@willystreet.coop>
|
||||||
|
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>
|
||||||
Francois-Xavier Gsell (zukoo) <fxgsell@gmail.com>
|
Francois-Xavier Gsell (zukoo) <fxgsell@gmail.com>
|
||||||
Frank Isemann (fti7) <frank@isemann.name>
|
Frank Isemann (fti7) <frank@isemann.name>
|
||||||
|
Gahl Saraf <saraf.gahl@gmail.com> <gahl@raftt.io>
|
||||||
georgespatton <georgespatton@users.noreply.github.com>
|
georgespatton <georgespatton@users.noreply.github.com>
|
||||||
ghjklw <malo@jaffre.info>
|
ghjklw <malo@jaffre.info>
|
||||||
Gilli Sigurdsson (gillisig) <gilli@vx.is>
|
Gilli Sigurdsson (gillisig) <gilli@vx.is>
|
||||||
Gleb Sinyavskiy <zhulik.gleb@gmail.com>
|
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>
|
||||||
|
guangwu <guoguangwu@magic-shield.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>
|
||||||
@@ -108,6 +143,8 @@ Heiko Zuerker (Smiley73) <heiko@zuerker.org>
|
|||||||
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>
|
||||||
|
ignacy123 <ignacy.buczek@onet.pl>
|
||||||
|
Ikko Ashimine <eltociear@gmail.com>
|
||||||
Ilya Brin <464157+ilyabrin@users.noreply.github.com>
|
Ilya Brin <464157+ilyabrin@users.noreply.github.com>
|
||||||
Iskander Sharipov (Alex) <quasilyte@gmail.com>
|
Iskander Sharipov (Alex) <quasilyte@gmail.com>
|
||||||
Jaakko Hannikainen (jgke) <jgke@jgke.fi>
|
Jaakko Hannikainen (jgke) <jgke@jgke.fi>
|
||||||
@@ -116,27 +153,37 @@ 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>
|
||||||
|
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>
|
||||||
|
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>
|
||||||
|
Jeffery To <jeffery.to@gmail.com>
|
||||||
jelle van der Waa <jelle@vdwaa.nl>
|
jelle van der Waa <jelle@vdwaa.nl>
|
||||||
Jens Diemer (jedie) <github.com@jensdiemer.de> <git@jensdiemer.de>
|
Jens Diemer (jedie) <github.com@jensdiemer.de> <git@jensdiemer.de>
|
||||||
Jerry Jacobs (xor-gate) <jerry.jacobs@xor-gate.org> <xor-gate@users.noreply.github.com>
|
Jerry Jacobs (xor-gate) <jerry.jacobs@xor-gate.org> <xor-gate@users.noreply.github.com>
|
||||||
|
Jesse Lucas <jesse@jesselucas.com>
|
||||||
Jochen Voss (seehuhn) <voss@seehuhn.de>
|
Jochen Voss (seehuhn) <voss@seehuhn.de>
|
||||||
Johan Andersson <j@i19.se>
|
Johan Andersson <j@i19.se>
|
||||||
Johan Vromans (sciurius) <jvromans@squirrel.nl>
|
Johan Vromans (sciurius) <jvromans@squirrel.nl>
|
||||||
John Rinehart (fuzzybear3965) <johnrichardrinehart@gmail.com>
|
John Rinehart (fuzzybear3965) <johnrichardrinehart@gmail.com>
|
||||||
Jonas Thelemann <e-mail@jonas-thelemann.de>
|
Jonas Thelemann <e-mail@jonas-thelemann.de>
|
||||||
Jonathan <artback@protonmail.com>
|
Jonathan <artback@protonmail.com> <jonagn@gmail.com>
|
||||||
Jonathan Cross <jcross@gmail.com>
|
Jonathan Cross <jcross@gmail.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>
|
||||||
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>
|
||||||
Karol Różycki (krozycki) <rozycki.karol@gmail.com>
|
Karol Różycki (krozycki) <rozycki.karol@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>
|
||||||
@@ -146,13 +193,16 @@ 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>
|
||||||
Lars K.W. Gohlke (lkwg82) <lkwg82@gmx.de>
|
Lars K.W. Gohlke (lkwg82) <lkwg82@gmx.de>
|
||||||
|
Lars Lehtonen <lars.lehtonen@gmail.com>
|
||||||
Laurent Arnoud <laurent@spkdev.net>
|
Laurent Arnoud <laurent@spkdev.net>
|
||||||
Laurent Etiemble (letiemble) <laurent.etiemble@gmail.com> <laurent.etiemble@monobjc.net>
|
Laurent Etiemble (letiemble) <laurent.etiemble@gmail.com> <laurent.etiemble@monobjc.net>
|
||||||
Leo Arias (elopio) <yo@elopio.net>
|
Leo Arias (elopio) <yo@elopio.net>
|
||||||
Liu Siyuan (liusy182) <liusy182@gmail.com> <liusy182@hotmail.com>
|
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>
|
||||||
Lukas Lihotzki <lukas@lihotzki.de>
|
Lukas Lihotzki <lukas@lihotzki.de>
|
||||||
|
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>
|
||||||
@@ -161,6 +211,8 @@ marco-m <marco.molteni@laposte.net>
|
|||||||
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>
|
||||||
|
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>
|
||||||
Matic Potočnik <hairyfotr@gmail.com>
|
Matic Potočnik <hairyfotr@gmail.com>
|
||||||
@@ -168,20 +220,27 @@ Matt Burke (burkemw3) <mburke@amplify.com> <burkemw3@gmail.com>
|
|||||||
Matt Robenolt <matt@ydekproductions.com>
|
Matt Robenolt <matt@ydekproductions.com>
|
||||||
Matteo Ruina <matteo.ruina@gmail.com>
|
Matteo Ruina <matteo.ruina@gmail.com>
|
||||||
Maurizio Tomasi <ziotom78@gmail.com>
|
Maurizio Tomasi <ziotom78@gmail.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>
|
||||||
MaximAL <almaximal@ya.ru>
|
MaximAL <almaximal@ya.ru>
|
||||||
Maxime Thirouin <m@moox.io>
|
Maxime Thirouin <m@moox.io>
|
||||||
|
Maximilian <maxi.rostock@outlook.de>
|
||||||
|
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>
|
||||||
|
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>
|
||||||
Mingxuan Lin <gdlmx@users.noreply.github.com>
|
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>
|
||||||
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>
|
||||||
Nico Stapelbroek <3368018+nstapelbroek@users.noreply.github.com>
|
Nico Stapelbroek <3368018+nstapelbroek@users.noreply.github.com>
|
||||||
Nicolas Braud-Santoni <nicolas@braud-santoni.eu>
|
Nicolas Braud-Santoni <nicolas@braud-santoni.eu>
|
||||||
Nicolas Perraut <n.perraut@gmail.com>
|
Nicolas Perraut <n.perraut@gmail.com>
|
||||||
@@ -191,8 +250,10 @@ 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>
|
||||||
Oyebanji Jacob Mayowa <oyebanji05@gmail.com>
|
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>
|
||||||
@@ -211,25 +272,33 @@ 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>
|
||||||
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>
|
||||||
|
Quentin Hibon <qh.public@yahoo.com>
|
||||||
Rahmi Pruitt <rjpruitt16@gmail.com>
|
Rahmi Pruitt <rjpruitt16@gmail.com>
|
||||||
|
red_led <red-led@users.noreply.github.com>
|
||||||
Richard Hartmann <RichiH@users.noreply.github.com>
|
Richard Hartmann <RichiH@users.noreply.github.com>
|
||||||
Robert Carosi (nov1n) <robert@carosi.nl>
|
Robert Carosi (nov1n) <robert@carosi.nl>
|
||||||
|
Roberto Santalla <roobre@users.noreply.github.com>
|
||||||
Robin Schoonover <robin@cornhooves.org>
|
Robin Schoonover <robin@cornhooves.org>
|
||||||
Roman Zaynetdinov (zaynetro) <romanznet@gmail.com>
|
Roman Zaynetdinov (zaynetro) <romanznet@gmail.com>
|
||||||
Ross Smith II (rasa) <ross@smithii.com>
|
Ross Smith II (rasa) <ross@smithii.com>
|
||||||
rubenbe <github-com-00ff86@vandamme.email>
|
rubenbe <github-com-00ff86@vandamme.email>
|
||||||
Ruslan Yevdokymov <38809160+ruslanye@users.noreply.github.com>
|
Ruslan Yevdokymov <38809160+ruslanye@users.noreply.github.com>
|
||||||
|
Ryan Qian <i@bitbili.net>
|
||||||
Ryan Sullivan (KayoticSully) <kayoticsully@gmail.com>
|
Ryan Sullivan (KayoticSully) <kayoticsully@gmail.com>
|
||||||
Sacheendra Talluri (sacheendra) <sacheendra.t@gmail.com>
|
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>
|
||||||
Sergey Mishin (ralder) <ralder@yandex.ru>
|
Sergey Mishin (ralder) <ralder@yandex.ru>
|
||||||
Shaarad Dalvi <60266155+shaaraddalvi@users.noreply.github.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>
|
||||||
Sly_tom_cat <slytomcat@mail.ru>
|
Sly_tom_cat <slytomcat@mail.ru>
|
||||||
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>
|
||||||
Suhas Gundimeda (snugghash) <suhas.gundimeda@gmail.com> <snugghash@gmail.com>
|
Suhas Gundimeda (snugghash) <suhas.gundimeda@gmail.com> <snugghash@gmail.com>
|
||||||
|
Syncthing Automation <automation@syncthing.net>
|
||||||
|
Syncthing Release Automation <release@syncthing.net>
|
||||||
Taylor Khan (nelsonkhan) <nelsonkhan@gmail.com>
|
Taylor Khan (nelsonkhan) <nelsonkhan@gmail.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>
|
||||||
@@ -244,15 +313,21 @@ 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>
|
||||||
Vil Brekin (Vilbrekin) <vilbrekin@gmail.com>
|
Vil Brekin (Vilbrekin) <vilbrekin@gmail.com>
|
||||||
Vladimir Rusinov <vrusinov@google.com>
|
villekalliomaki <53118179+villekalliomaki@users.noreply.github.com>
|
||||||
|
Vladimir Rusinov <vrusinov@google.com> <vladimir.rusinov@gmail.com>
|
||||||
wangguoliang <liangcszzu@163.com>
|
wangguoliang <liangcszzu@163.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>
|
||||||
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>
|
||||||
xarx00 <xarx00@users.noreply.github.com>
|
xarx00 <xarx00@users.noreply.github.com>
|
||||||
Xavier O. (damajor) <damajor@gmail.com>
|
Xavier O. (damajor) <damajor@gmail.com>
|
||||||
xjtdy888 (xjtdy888) <xjtdy888@163.com>
|
xjtdy888 (xjtdy888) <xjtdy888@163.com>
|
||||||
Yannic A. (eipiminus1) <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com>
|
Yannic A. (eipiminus1) <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com>
|
||||||
佛跳墙 <daoquan@qq.com>
|
佛跳墙 <daoquan@qq.com>
|
||||||
|
落心 <luoxin.ttt@gmail.com>
|
||||||
+9
-4
@@ -1,6 +1,6 @@
|
|||||||
## Reporting Bugs
|
## Reporting Bugs
|
||||||
|
|
||||||
Please file bugs in the [Github Issue
|
Please file bugs in the [GitHub Issue
|
||||||
Tracker](https://github.com/syncthing/syncthing/issues). Include at
|
Tracker](https://github.com/syncthing/syncthing/issues). Include at
|
||||||
least the following:
|
least the following:
|
||||||
|
|
||||||
@@ -24,10 +24,15 @@ too much information will never get you yelled at. :)
|
|||||||
## Contributing Translations
|
## Contributing Translations
|
||||||
|
|
||||||
All translations are done via
|
All translations are done via
|
||||||
[Transifex](https://www.transifex.com/projects/p/syncthing/). If you
|
[Weblate](https://hosted.weblate.org/projects/syncthing/). If you wish
|
||||||
wish to contribute to a translation, just head over there and sign up.
|
to contribute to a translation, just head over there and sign up.
|
||||||
Before every release, the language resources are updated from the
|
Before every release, the language resources are updated from the
|
||||||
latest info on Transifex.
|
latest info on Weblate.
|
||||||
|
|
||||||
|
Note that the previously used service at
|
||||||
|
[Transifex](https://www.transifex.com/projects/p/syncthing/) is being
|
||||||
|
retired and we kindly ask you to sign up on Weblate for continued
|
||||||
|
involvement.
|
||||||
|
|
||||||
## Contributing Code
|
## Contributing Code
|
||||||
|
|
||||||
|
|||||||
+36
-8
@@ -1,29 +1,57 @@
|
|||||||
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
|
||||||
|
|
||||||
EXPOSE 8384 22000 21027/udp
|
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
|
||||||
|
|
||||||
VOLUME ["/var/syncthing"]
|
VOLUME ["/var/syncthing"]
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates 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
|
||||||
|
|
||||||
HEALTHCHECK --interval=1m --timeout=10s \
|
HEALTHCHECK --interval=1m --timeout=10s \
|
||||||
CMD nc -z 127.0.0.1 8384 || 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
|
||||||
ENTRYPOINT ["/bin/entrypoint.sh", "/bin/syncthing", "-home", "/var/syncthing/config"]
|
ENV STHOMEDIR=/var/syncthing/config
|
||||||
|
RUN chmod 755 /bin/entrypoint.sh
|
||||||
|
ENTRYPOINT ["/bin/entrypoint.sh", "/bin/syncthing"]
|
||||||
+9
-1
@@ -1,9 +1,17 @@
|
|||||||
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 \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& gem install --no-ri --no-rdoc fpm
|
&& gem install fpm
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
FROM alpine
|
|
||||||
ARG TARGETARCH
|
|
||||||
|
|
||||||
EXPOSE 8384 22000 21027/udp
|
|
||||||
|
|
||||||
VOLUME ["/var/syncthing"]
|
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates 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 nc -z 127.0.0.1 8384 || exit 1
|
|
||||||
|
|
||||||
ENV STGUIADDRESS=0.0.0.0:8384
|
|
||||||
ENTRYPOINT ["/bin/entrypoint.sh", "/bin/syncthing", "-home", "/var/syncthing/config"]
|
|
||||||
@@ -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 Crash Receiver"
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
COPY stcrashreceiver-linux-${TARGETARCH} /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
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates su-exec curl
|
||||||
|
ENV PUID=1000 PGID=1000 MAXMIND_KEY=
|
||||||
|
|
||||||
|
RUN mkdir /var/strelaypoolsrv && chown 1000 /var/strelaypoolsrv
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
COPY strelaypoolsrv-linux-${TARGETARCH} /bin/strelaypoolsrv
|
||||||
|
COPY 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
|
||||||
|
|||||||
@@ -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 Upgrades"
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
COPY stupgrades-linux-${TARGETARCH} /bin/stupgrades
|
||||||
|
|
||||||
|
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 capabitilies 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
|
||||||
|
|||||||
+56
-6
@@ -7,25 +7,57 @@ Use the `/var/syncthing` volume to have the synchronized files available on the
|
|||||||
host. You can add more folders and map them as you prefer.
|
host. You can add more folders and map them as you prefer.
|
||||||
|
|
||||||
Note that Syncthing runs as UID 1000 and GID 1000 by default. These may be
|
Note that Syncthing runs as UID 1000 and GID 1000 by default. These may be
|
||||||
altered with the ``PUID`` and ``PGID`` environment variables.
|
altered with the `PUID` and `PGID` environment variables. In addition
|
||||||
|
the name of the Syncthing instance can be optionally defined by using
|
||||||
|
`--hostname=syncthing` parameter.
|
||||||
|
|
||||||
|
To grant Syncthing additional capabilities without running as root, use the
|
||||||
|
`PCAP` environment variable with the same syntax as that for `setcap(8)`.
|
||||||
|
For example, `PCAP=cap_chown,cap_fowner+ep`.
|
||||||
|
|
||||||
## Example Usage
|
## Example Usage
|
||||||
|
|
||||||
|
**Docker cli**
|
||||||
```
|
```
|
||||||
$ docker pull syncthing/syncthing
|
$ docker pull syncthing/syncthing
|
||||||
$ docker run -p 8384:8384 -p 22000:22000 \
|
$ docker run -p 8384:8384 -p 22000:22000/tcp -p 22000:22000/udp -p 21027:21027/udp \
|
||||||
-v /wherever/st-sync:/var/syncthing \
|
-v /wherever/st-sync:/var/syncthing \
|
||||||
|
--hostname=my-syncthing \
|
||||||
syncthing/syncthing:latest
|
syncthing/syncthing:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Docker compose**
|
||||||
|
```yml
|
||||||
|
---
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
syncthing:
|
||||||
|
image: syncthing/syncthing
|
||||||
|
container_name: syncthing
|
||||||
|
hostname: my-syncthing
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
volumes:
|
||||||
|
- /wherever/st-sync:/var/syncthing
|
||||||
|
ports:
|
||||||
|
- 8384:8384 # Web UI
|
||||||
|
- 22000:22000/tcp # TCP file transfers
|
||||||
|
- 22000:22000/udp # QUIC file transfers
|
||||||
|
- 21027:21027/udp # Receive local discovery broadcasts
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
## Discovery
|
## Discovery
|
||||||
|
|
||||||
Note that local device discovery will not work with the above command,
|
Note that Docker's default network mode prevents local IP addresses from
|
||||||
resulting in poor local transfer rates if local device addresses are not
|
being discovered, as Syncthing is only able to see the internal IP of the
|
||||||
manually configured.
|
container on the `172.17.0.0/16` subnet. This will result in poor transfer rates
|
||||||
|
if local device addresses are not manually configured.
|
||||||
|
|
||||||
To allow local discovery, the docker host network can be used instead:
|
It is therefore advisable to use the [host network mode](https://docs.docker.com/network/host/) instead:
|
||||||
|
|
||||||
|
**Docker cli**
|
||||||
```
|
```
|
||||||
$ docker pull syncthing/syncthing
|
$ docker pull syncthing/syncthing
|
||||||
$ docker run --network=host \
|
$ docker run --network=host \
|
||||||
@@ -33,6 +65,24 @@ $ docker run --network=host \
|
|||||||
syncthing/syncthing:latest
|
syncthing/syncthing:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Docker compose**
|
||||||
|
```yml
|
||||||
|
---
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
syncthing:
|
||||||
|
image: syncthing/syncthing
|
||||||
|
container_name: syncthing
|
||||||
|
hostname: my-syncthing
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
volumes:
|
||||||
|
- /wherever/st-sync:/var/syncthing
|
||||||
|
network_mode: host
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
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
|
||||||
listens on. You can edit the syncthing configuration to change the defaults if
|
listens on. You can edit the syncthing configuration to change the defaults if
|
||||||
there are conflicts.
|
there are conflicts.
|
||||||
|
|||||||
@@ -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,7 +57,7 @@ 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
|
||||||
|
|
||||||
@@ -69,45 +66,49 @@ To run Syncthing in Docker, see [the Docker README][16].
|
|||||||
## Vote on features/bugs
|
## Vote on features/bugs
|
||||||
|
|
||||||
We'd like to encourage you to [vote][12] on issues that matter to you.
|
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.
|
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]. There is also an IRC
|
The first and best point of contact is the [Forum][8].
|
||||||
channel, `#syncthing` on [freenode][4] (with a [web client][9]), for talking
|
If you've found something that is clearly a
|
||||||
directly to developers and users. 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, and there's a [guide][5]
|
Building Syncthing from source is easy. After extracting the source bundle from
|
||||||
that describes it for both Unix and Windows systems.
|
a release or checking out git, you just need to run `go run build.go` and the
|
||||||
|
binaries are created in `./bin`. There's [a guide][5] with more details on the
|
||||||
|
build process.
|
||||||
|
|
||||||
## Signed Releases
|
## Signed Releases
|
||||||
|
|
||||||
As of v0.10.15 and onwards release binaries are GPG signed with the key
|
As of v0.10.15 and onwards, release binaries are GPG signed with the key
|
||||||
D26E6ED000654A3E, available from https://syncthing.net/security.html and
|
D26E6ED000654A3E, available from https://syncthing.net/security.html and
|
||||||
most key servers.
|
most key servers.
|
||||||
|
|
||||||
There is also a built in automatic upgrade mechanism (disabled in some
|
There is also a built-in automatic upgrade mechanism (disabled in some
|
||||||
distribution channels) which uses a compiled in ECDSA signature. macOS
|
distribution channels) which uses a compiled in ECDSA signature. macOS
|
||||||
binaries are also properly code signed.
|
binaries are also properly code signed.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
Please see the [Syncthing documentation site][6].
|
Please see the Syncthing [documentation site][6] [[source]][17].
|
||||||
|
|
||||||
All code is licensed under the [MPLv2 License][7].
|
All code is licensed under the [MPLv2 License][7].
|
||||||
|
|
||||||
[1]: https://docs.syncthing.net/specs/bep-v1.html
|
[1]: https://docs.syncthing.net/specs/bep-v1.html
|
||||||
[2]: https://docs.syncthing.net/intro/getting-started.html
|
[2]: https://docs.syncthing.net/intro/getting-started.html
|
||||||
[3]: https://github.com/syncthing/syncthing/blob/main/etc
|
[3]: https://github.com/syncthing/syncthing/blob/main/etc
|
||||||
[4]: https://www.freenode.net/
|
|
||||||
[5]: https://docs.syncthing.net/dev/building.html
|
[5]: https://docs.syncthing.net/dev/building.html
|
||||||
[6]: https://docs.syncthing.net/
|
[6]: https://docs.syncthing.net/
|
||||||
[7]: https://github.com/syncthing/syncthing/blob/main/LICENSE
|
[7]: https://github.com/syncthing/syncthing/blob/main/LICENSE
|
||||||
[8]: https://forum.syncthing.net/
|
[8]: https://forum.syncthing.net/
|
||||||
[9]: https://kiwiirc.com/client/irc.freenode.net/#syncthing
|
|
||||||
[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
|
[12]: https://www.bountysource.com/teams/syncthing/issues
|
||||||
@@ -115,4 +116,4 @@ All code is licensed under the [MPLv2 License][7].
|
|||||||
[14]: assets/logo-text-128.png
|
[14]: assets/logo-text-128.png
|
||||||
[15]: https://syncthing.net/
|
[15]: https://syncthing.net/
|
||||||
[16]: https://github.com/syncthing/syncthing/blob/main/README-Docker.md
|
[16]: https://github.com/syncthing/syncthing/blob/main/README-Docker.md
|
||||||
|
[17]: https://github.com/syncthing/docs
|
||||||
@@ -4,6 +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:build ignore
|
||||||
// +build ignore
|
// +build ignore
|
||||||
|
|
||||||
package main
|
package main
|
||||||
@@ -14,13 +15,12 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"compress/flate"
|
"compress/flate"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"crypto/sha256"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -30,27 +30,34 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
buildpkg "github.com/syncthing/syncthing/lib/build"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
goarch string
|
goarch string
|
||||||
goos string
|
goos string
|
||||||
noupgrade bool
|
noupgrade bool
|
||||||
version string
|
version string
|
||||||
goCmd string
|
goCmd string
|
||||||
race bool
|
race bool
|
||||||
debug = os.Getenv("BUILDDEBUG") != ""
|
debug = os.Getenv("BUILDDEBUG") != ""
|
||||||
extraTags string
|
extraTags string
|
||||||
installSuffix string
|
installSuffix string
|
||||||
pkgdir string
|
pkgdir string
|
||||||
cc string
|
cc string
|
||||||
run string
|
run string
|
||||||
benchRun string
|
benchRun string
|
||||||
debugBinary bool
|
buildOut string
|
||||||
coverage bool
|
debugBinary bool
|
||||||
timeout = "120s"
|
coverage bool
|
||||||
numVersions = 5
|
long bool
|
||||||
|
timeout = "120s"
|
||||||
|
longTimeout = "600s"
|
||||||
|
numVersions = 5
|
||||||
|
withNextGenGUI = os.Getenv("BUILD_NEXT_GEN_GUI") != ""
|
||||||
)
|
)
|
||||||
|
|
||||||
type target struct {
|
type target struct {
|
||||||
@@ -58,12 +65,11 @@ type target struct {
|
|||||||
debname string
|
debname string
|
||||||
debdeps []string
|
debdeps []string
|
||||||
debpre string
|
debpre string
|
||||||
debpost string
|
|
||||||
description string
|
description string
|
||||||
buildPkgs []string
|
buildPkgs []string
|
||||||
binaryName string
|
binaryName string
|
||||||
archiveFiles []archiveFile
|
archiveFiles []archiveFile
|
||||||
systemdServices []string
|
systemdService string
|
||||||
installationFiles []archiveFile
|
installationFiles []archiveFile
|
||||||
tags []string
|
tags []string
|
||||||
}
|
}
|
||||||
@@ -85,7 +91,6 @@ var targets = map[string]target{
|
|||||||
name: "syncthing",
|
name: "syncthing",
|
||||||
debname: "syncthing",
|
debname: "syncthing",
|
||||||
debdeps: []string{"libc6", "procps"},
|
debdeps: []string{"libc6", "procps"},
|
||||||
debpost: "script/post-upgrade",
|
|
||||||
description: "Open Source Continuous File Synchronization",
|
description: "Open Source Continuous File Synchronization",
|
||||||
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
|
||||||
@@ -96,6 +101,7 @@ var targets = map[string]target{
|
|||||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||||
// All files from etc/ and extra/ added automatically in init().
|
// All files from etc/ and extra/ added automatically in init().
|
||||||
},
|
},
|
||||||
|
systemdService: "syncthing@*.service",
|
||||||
installationFiles: []archiveFile{
|
installationFiles: []archiveFile{
|
||||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
||||||
{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: 0644},
|
||||||
@@ -114,6 +120,7 @@ var targets = map[string]target{
|
|||||||
{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: 0644},
|
||||||
{src: "etc/linux-systemd/system/syncthing-resume.service", dst: "deb/lib/systemd/system/syncthing-resume.service", perm: 0644},
|
{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: 0644},
|
{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: 0644},
|
||||||
{src: "etc/firewall-ufw/syncthing", dst: "deb/etc/ufw/applications.d/syncthing", perm: 0644},
|
{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: 0644},
|
{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: 0644},
|
{src: "etc/linux-desktop/syncthing-ui.desktop", dst: "deb/usr/share/applications/syncthing-ui.desktop", perm: 0644},
|
||||||
@@ -139,15 +146,14 @@ var targets = map[string]target{
|
|||||||
{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
|
{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||||
},
|
},
|
||||||
systemdServices: []string{
|
systemdService: "stdiscosrv.service",
|
||||||
"cmd/stdiscosrv/etc/linux-systemd/stdiscosrv.service",
|
|
||||||
},
|
|
||||||
installationFiles: []archiveFile{
|
installationFiles: []archiveFile{
|
||||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
||||||
{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: 0644},
|
||||||
{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: 0644},
|
||||||
{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: 0644},
|
||||||
{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: 0644},
|
||||||
|
{src: "cmd/stdiscosrv/etc/linux-systemd/stdiscosrv.service", dst: "deb/lib/systemd/system/stdiscosrv.service", perm: 0644},
|
||||||
{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: 0644},
|
||||||
{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: 0644},
|
||||||
},
|
},
|
||||||
@@ -168,9 +174,7 @@ var targets = map[string]target{
|
|||||||
{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
|
{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||||
},
|
},
|
||||||
systemdServices: []string{
|
systemdService: "strelaysrv.service",
|
||||||
"cmd/strelaysrv/etc/linux-systemd/strelaysrv.service",
|
|
||||||
},
|
|
||||||
installationFiles: []archiveFile{
|
installationFiles: []archiveFile{
|
||||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
||||||
{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: 0644},
|
||||||
@@ -178,6 +182,7 @@ var targets = map[string]target{
|
|||||||
{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: 0644},
|
||||||
{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: 0644},
|
||||||
{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: 0644},
|
||||||
|
{src: "cmd/strelaysrv/etc/linux-systemd/strelaysrv.service", dst: "deb/lib/systemd/system/strelaysrv.service", perm: 0644},
|
||||||
{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: 0644},
|
||||||
{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: 0644},
|
||||||
},
|
},
|
||||||
@@ -202,21 +207,27 @@ var targets = map[string]target{
|
|||||||
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/AUTHORS.txt", perm: 0644},
|
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/AUTHORS.txt", perm: 0644},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"stupgrades": {
|
||||||
|
name: "stupgrades",
|
||||||
|
description: "Syncthing Upgrade Check Server",
|
||||||
|
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/stupgrades"},
|
||||||
|
binaryName: "stupgrades",
|
||||||
|
},
|
||||||
|
"stcrashreceiver": {
|
||||||
|
name: "stcrashreceiver",
|
||||||
|
description: "Syncthing Crash Server",
|
||||||
|
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/stcrashreceiver"},
|
||||||
|
binaryName: "stcrashreceiver",
|
||||||
|
},
|
||||||
|
"ursrv": {
|
||||||
|
name: "ursrv",
|
||||||
|
description: "Syncthing Usage Reporting Server",
|
||||||
|
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/ursrv"},
|
||||||
|
binaryName: "ursrv",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// These are repos we need to clone to run "go generate"
|
func initTargets() {
|
||||||
|
|
||||||
type dependencyRepo struct {
|
|
||||||
path string
|
|
||||||
repo string
|
|
||||||
commit string
|
|
||||||
}
|
|
||||||
|
|
||||||
var dependencyRepos = []dependencyRepo{
|
|
||||||
{path: "xdr", repo: "https://github.com/calmh/xdr.git", commit: "08e072f9cb16"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
all := targets["all"]
|
all := targets["all"]
|
||||||
pkgs, _ := filepath.Glob("cmd/*")
|
pkgs, _ := filepath.Glob("cmd/*")
|
||||||
for _, pkg := range pkgs {
|
for _, pkg := range pkgs {
|
||||||
@@ -225,6 +236,9 @@ func init() {
|
|||||||
// ignore dotfiles
|
// ignore dotfiles
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if noupgrade && pkg == "stupgrades" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
all.buildPkgs = append(all.buildPkgs, fmt.Sprintf("github.com/syncthing/syncthing/cmd/%s", pkg))
|
all.buildPkgs = append(all.buildPkgs, fmt.Sprintf("github.com/syncthing/syncthing/cmd/%s", pkg))
|
||||||
}
|
}
|
||||||
targets["all"] = all
|
targets["all"] = all
|
||||||
@@ -256,6 +270,8 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initTargets()
|
||||||
|
|
||||||
// Invoking build.go with no parameters at all builds everything (incrementally),
|
// Invoking build.go with no parameters at all builds everything (incrementally),
|
||||||
// which is what you want for maximum error checking during development.
|
// which is what you want for maximum error checking during development.
|
||||||
if flag.NArg() == 0 {
|
if flag.NArg() == 0 {
|
||||||
@@ -307,15 +323,24 @@ func runCommand(cmd string, target target) {
|
|||||||
case "assets":
|
case "assets":
|
||||||
rebuildAssets()
|
rebuildAssets()
|
||||||
|
|
||||||
|
case "update-deps":
|
||||||
|
updateDependencies()
|
||||||
|
|
||||||
case "proto":
|
case "proto":
|
||||||
proto()
|
proto()
|
||||||
|
|
||||||
|
case "testmocks":
|
||||||
|
testmocks()
|
||||||
|
|
||||||
case "translate":
|
case "translate":
|
||||||
translate()
|
translate()
|
||||||
|
|
||||||
case "transifex":
|
case "transifex":
|
||||||
transifex()
|
transifex()
|
||||||
|
|
||||||
|
case "weblate":
|
||||||
|
weblate()
|
||||||
|
|
||||||
case "tar":
|
case "tar":
|
||||||
buildTar(target, tags)
|
buildTar(target, tags)
|
||||||
|
|
||||||
@@ -369,9 +394,12 @@ func parseFlags() {
|
|||||||
flag.StringVar(&cc, "cc", os.Getenv("CC"), "Set CC environment variable for `go build`")
|
flag.StringVar(&cc, "cc", os.Getenv("CC"), "Set CC environment variable for `go build`")
|
||||||
flag.BoolVar(&debugBinary, "debug-binary", debugBinary, "Create unoptimized binary to use with delve, set -gcflags='-N -l' and omit -ldflags")
|
flag.BoolVar(&debugBinary, "debug-binary", debugBinary, "Create unoptimized binary to use with delve, set -gcflags='-N -l' and omit -ldflags")
|
||||||
flag.BoolVar(&coverage, "coverage", coverage, "Write coverage profile of tests to coverage.txt")
|
flag.BoolVar(&coverage, "coverage", coverage, "Write coverage profile of tests to coverage.txt")
|
||||||
|
flag.BoolVar(&long, "long", long, "Run tests without the -short flag")
|
||||||
flag.IntVar(&numVersions, "num-versions", numVersions, "Number of versions for changelog command")
|
flag.IntVar(&numVersions, "num-versions", numVersions, "Number of versions for changelog command")
|
||||||
flag.StringVar(&run, "run", "", "Specify which tests to run")
|
flag.StringVar(&run, "run", "", "Specify which tests to run")
|
||||||
flag.StringVar(&benchRun, "bench", "", "Specify which benchmarks to run")
|
flag.StringVar(&benchRun, "bench", "", "Specify which benchmarks to run")
|
||||||
|
flag.BoolVar(&withNextGenGUI, "with-next-gen-gui", withNextGenGUI, "Also build 'newgui'")
|
||||||
|
flag.StringVar(&buildOut, "build-out", "", "Set the '-o' value for 'go build'")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,11 +407,17 @@ func test(tags []string, pkgs ...string) {
|
|||||||
lazyRebuildAssets()
|
lazyRebuildAssets()
|
||||||
|
|
||||||
tags = append(tags, "purego")
|
tags = append(tags, "purego")
|
||||||
args := []string{"test", "-short", "-timeout", timeout, "-tags", strings.Join(tags, " ")}
|
args := []string{"test", "-tags", strings.Join(tags, " ")}
|
||||||
|
if long {
|
||||||
|
timeout = longTimeout
|
||||||
|
} else {
|
||||||
|
args = append(args, "-short")
|
||||||
|
}
|
||||||
|
args = append(args, "-timeout", timeout)
|
||||||
|
|
||||||
if runtime.GOARCH == "amd64" {
|
if runtime.GOARCH == "amd64" {
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "darwin", "linux", "freebsd": // , "windows": # See https://github.com/golang/go/issues/27089
|
case buildpkg.Darwin, buildpkg.Linux, buildpkg.FreeBSD: // , "windows": # See https://github.com/golang/go/issues/27089
|
||||||
args = append(args, "-race")
|
args = append(args, "-race")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -438,6 +472,10 @@ func benchArgs() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func install(target target, tags []string) {
|
func install(target target, tags []string) {
|
||||||
|
if (target.name == "syncthing" || target.name == "") && !withNextGenGUI {
|
||||||
|
log.Println("Notice: Next generation GUI will not be built; see --with-next-gen-gui.")
|
||||||
|
}
|
||||||
|
|
||||||
lazyRebuildAssets()
|
lazyRebuildAssets()
|
||||||
|
|
||||||
tags = append(target.tags, tags...)
|
tags = append(target.tags, tags...)
|
||||||
@@ -461,12 +499,16 @@ func install(target target, tags []string) {
|
|||||||
defer shouldCleanupSyso(sysoPath)
|
defer shouldCleanupSyso(sysoPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{"install", "-v", "-trimpath"}
|
args := []string{"install", "-v"}
|
||||||
args = appendParameters(args, tags, target.buildPkgs...)
|
args = appendParameters(args, tags, target.buildPkgs...)
|
||||||
runPrint(goCmd, args...)
|
runPrint(goCmd, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func build(target target, tags []string) {
|
func build(target target, tags []string) {
|
||||||
|
if (target.name == "syncthing" || target.name == "") && !withNextGenGUI {
|
||||||
|
log.Println("Notice: Next generation GUI will not be built; see --with-next-gen-gui.")
|
||||||
|
}
|
||||||
|
|
||||||
lazyRebuildAssets()
|
lazyRebuildAssets()
|
||||||
tags = append(target.tags, tags...)
|
tags = append(target.tags, tags...)
|
||||||
|
|
||||||
@@ -489,7 +531,10 @@ func build(target target, tags []string) {
|
|||||||
defer shouldCleanupSyso(sysoPath)
|
defer shouldCleanupSyso(sysoPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{"build", "-v", "-trimpath"}
|
args := []string{"build", "-v"}
|
||||||
|
if buildOut != "" {
|
||||||
|
args = append(args, "-o", buildOut)
|
||||||
|
}
|
||||||
args = appendParameters(args, tags, target.buildPkgs...)
|
args = appendParameters(args, tags, target.buildPkgs...)
|
||||||
runPrint(goCmd, args...)
|
runPrint(goCmd, args...)
|
||||||
}
|
}
|
||||||
@@ -523,13 +568,13 @@ func appendParameters(args []string, tags []string, pkgs ...string) []string {
|
|||||||
|
|
||||||
if !debugBinary {
|
if !debugBinary {
|
||||||
// Regular binaries get version tagged and skip some debug symbols
|
// Regular binaries get version tagged and skip some debug symbols
|
||||||
args = append(args, "-ldflags", ldflags(tags))
|
args = append(args, "-trimpath", "-ldflags", ldflags(tags))
|
||||||
} else {
|
} else {
|
||||||
// -gcflags to disable optimizations and inlining. Skip -ldflags
|
// -gcflags to disable optimizations and inlining. Skip -ldflags
|
||||||
// because `Could not launch program: decoding dwarf section info at
|
// because `Could not launch program: decoding dwarf section info at
|
||||||
// offset 0x0: too short` on 'dlv exec ...' see
|
// offset 0x0: too short` on 'dlv exec ...' see
|
||||||
// https://github.com/go-delve/delve/issues/79
|
// https://github.com/go-delve/delve/issues/79
|
||||||
args = append(args, "-gcflags", "-N -l")
|
args = append(args, "-gcflags", "all=-N -l")
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(args, pkgs...)
|
return append(args, pkgs...)
|
||||||
@@ -635,11 +680,13 @@ func buildDeb(target target) {
|
|||||||
for _, dep := range target.debdeps {
|
for _, dep := range target.debdeps {
|
||||||
args = append(args, "-d", dep)
|
args = append(args, "-d", dep)
|
||||||
}
|
}
|
||||||
for _, service := range target.systemdServices {
|
if target.systemdService != "" {
|
||||||
args = append(args, "--deb-systemd", service)
|
debpost, err := createPostInstScript(target)
|
||||||
}
|
defer os.Remove(debpost)
|
||||||
if target.debpost != "" {
|
if err != nil {
|
||||||
args = append(args, "--after-upgrade", target.debpost)
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
args = append(args, "--after-upgrade", debpost)
|
||||||
}
|
}
|
||||||
if target.debpre != "" {
|
if target.debpre != "" {
|
||||||
args = append(args, "--before-install", target.debpre)
|
args = append(args, "--before-install", target.debpre)
|
||||||
@@ -647,6 +694,28 @@ func buildDeb(target target) {
|
|||||||
runPrint("fpm", args...)
|
runPrint("fpm", args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createPostInstScript(target target) (string, error) {
|
||||||
|
scriptname := filepath.Join("script", "deb-post-inst.template")
|
||||||
|
t, err := template.ParseFiles(scriptname)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
scriptname = strings.TrimSuffix(scriptname, ".template")
|
||||||
|
w, err := os.Create(scriptname)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer w.Close()
|
||||||
|
if err = t.Execute(w, struct {
|
||||||
|
Service, Command string
|
||||||
|
}{
|
||||||
|
target.systemdService, target.binaryName,
|
||||||
|
}); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return scriptname, nil
|
||||||
|
}
|
||||||
|
|
||||||
func shouldBuildSyso(dir string) (string, error) {
|
func shouldBuildSyso(dir string) (string, error) {
|
||||||
type M map[string]interface{}
|
type M map[string]interface{}
|
||||||
version := getVersion()
|
version := getVersion()
|
||||||
@@ -666,11 +735,14 @@ func shouldBuildSyso(dir string) (string, error) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"StringFileInfo": M{
|
"StringFileInfo": M{
|
||||||
"FileDescription": "Open Source Continuous File Synchronization",
|
"CompanyName": "The Syncthing Authors",
|
||||||
"LegalCopyright": "The Syncthing Authors",
|
"FileDescription": "Syncthing - Open Source Continuous File Synchronization",
|
||||||
"FileVersion": version,
|
"FileVersion": version,
|
||||||
"ProductVersion": version,
|
"InternalName": "syncthing",
|
||||||
"ProductName": "Syncthing",
|
"LegalCopyright": "The Syncthing Authors",
|
||||||
|
"OriginalFilename": "syncthing",
|
||||||
|
"ProductName": "Syncthing",
|
||||||
|
"ProductVersion": version,
|
||||||
},
|
},
|
||||||
"IconPath": "assets/logo.ico",
|
"IconPath": "assets/logo.ico",
|
||||||
})
|
})
|
||||||
@@ -679,7 +751,7 @@ func shouldBuildSyso(dir string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jsonPath := filepath.Join(dir, "versioninfo.json")
|
jsonPath := filepath.Join(dir, "versioninfo.json")
|
||||||
err = ioutil.WriteFile(jsonPath, bs, 0644)
|
err = os.WriteFile(jsonPath, bs, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("failed to create " + jsonPath + ": " + err.Error())
|
return "", errors.New("failed to create " + jsonPath + ": " + err.Error())
|
||||||
}
|
}
|
||||||
@@ -692,7 +764,13 @@ func shouldBuildSyso(dir string) (string, error) {
|
|||||||
|
|
||||||
sysoPath := filepath.Join(dir, "cmd", "syncthing", "resource.syso")
|
sysoPath := filepath.Join(dir, "cmd", "syncthing", "resource.syso")
|
||||||
|
|
||||||
if _, err := runError("goversioninfo", "-o", sysoPath); err != nil {
|
// See https://github.com/josephspurrier/goversioninfo#command-line-flags
|
||||||
|
armOption := ""
|
||||||
|
if strings.Contains(goarch, "arm") {
|
||||||
|
armOption = "-arm=true"
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := runError("goversioninfo", "-o", sysoPath, armOption); err != nil {
|
||||||
return "", errors.New("failed to create " + sysoPath + ": " + err.Error())
|
return "", errors.New("failed to create " + sysoPath + ": " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,12 +790,12 @@ func shouldCleanupSyso(sysoFilePath string) {
|
|||||||
// exists. The permission bits are copied as well. If dst already exists and
|
// exists. The permission bits are copied as well. If dst already exists and
|
||||||
// the contents are identical to src the modification time is not updated.
|
// the contents are identical to src the modification time is not updated.
|
||||||
func copyFile(src, dst string, perm os.FileMode) error {
|
func copyFile(src, dst string, perm os.FileMode) error {
|
||||||
in, err := ioutil.ReadFile(src)
|
in, err := os.ReadFile(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := ioutil.ReadFile(dst)
|
out, err := os.ReadFile(dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// The destination probably doesn't exist, we should create
|
// The destination probably doesn't exist, we should create
|
||||||
// it.
|
// it.
|
||||||
@@ -733,7 +811,7 @@ func copyFile(src, dst string, perm os.FileMode) error {
|
|||||||
|
|
||||||
copy:
|
copy:
|
||||||
os.MkdirAll(filepath.Dir(dst), 0777)
|
os.MkdirAll(filepath.Dir(dst), 0777)
|
||||||
if err := ioutil.WriteFile(dst, in, perm); err != nil {
|
if err := os.WriteFile(dst, in, perm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,11 +839,46 @@ func rebuildAssets() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func lazyRebuildAssets() {
|
func lazyRebuildAssets() {
|
||||||
if shouldRebuildAssets("lib/api/auto/gui.files.go", "gui") || shouldRebuildAssets("cmd/strelaypoolsrv/auto/gui.files.go", "cmd/strelaypoolsrv/gui") {
|
shouldRebuild := shouldRebuildAssets("lib/api/auto/gui.files.go", "gui") ||
|
||||||
|
shouldRebuildAssets("cmd/strelaypoolsrv/auto/gui.files.go", "cmd/strelaypoolsrv/gui")
|
||||||
|
|
||||||
|
if withNextGenGUI {
|
||||||
|
shouldRebuild = buildNextGenGUI() || shouldRebuild
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldRebuild {
|
||||||
rebuildAssets()
|
rebuildAssets()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildNextGenGUI() bool {
|
||||||
|
// Check if we need to run the npm process, and if so also set the flag
|
||||||
|
// to rebuild Go assets afterwards. The index.html is regenerated every
|
||||||
|
// time by the build process. This assumes the new GUI ends up in
|
||||||
|
// next-gen-gui/dist/next-gen-gui.
|
||||||
|
|
||||||
|
if !shouldRebuildAssets("gui/next-gen-gui/index.html", "next-gen-gui") {
|
||||||
|
// The GUI is up to date.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
runPrintInDir("next-gen-gui", "npm", "install")
|
||||||
|
runPrintInDir("next-gen-gui", "npm", "run", "build", "--", "--prod", "--subresource-integrity")
|
||||||
|
|
||||||
|
rmr("gui/tech-ui")
|
||||||
|
|
||||||
|
for _, src := range listFiles("next-gen-gui/dist") {
|
||||||
|
rel, _ := filepath.Rel("next-gen-gui/dist", src)
|
||||||
|
dst := filepath.Join("gui", rel)
|
||||||
|
if err := copyFile(src, dst, 0644); err != nil {
|
||||||
|
fmt.Println("copy:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func shouldRebuildAssets(target, srcdir string) bool {
|
func shouldRebuildAssets(target, srcdir string) bool {
|
||||||
info, err := os.Stat(target)
|
info, err := os.Stat(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -792,27 +905,59 @@ func shouldRebuildAssets(target, srcdir string) bool {
|
|||||||
return assetsAreNewer
|
return assetsAreNewer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateDependencies() {
|
||||||
|
// Figure out desired Go version
|
||||||
|
bs, err := os.ReadFile("go.mod")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
re := regexp.MustCompile(`(?m)^go\s+([0-9.]+)`)
|
||||||
|
matches := re.FindSubmatch(bs)
|
||||||
|
if len(matches) != 2 {
|
||||||
|
log.Fatal("failed to parse go.mod")
|
||||||
|
}
|
||||||
|
goVersion := string(matches[1])
|
||||||
|
|
||||||
|
runPrint(goCmd, "get", "-u", "./...")
|
||||||
|
runPrint(goCmd, "mod", "tidy", "-go="+goVersion, "-compat="+goVersion)
|
||||||
|
|
||||||
|
// We might have updated the protobuf package and should regenerate to match.
|
||||||
|
proto()
|
||||||
|
}
|
||||||
|
|
||||||
func proto() {
|
func proto() {
|
||||||
pv := protobufVersion()
|
pv := protobufVersion()
|
||||||
dependencyRepos = append(dependencyRepos,
|
repo := "https://github.com/gogo/protobuf.git"
|
||||||
dependencyRepo{path: "protobuf", repo: "https://github.com/gogo/protobuf.git", commit: pv},
|
path := filepath.Join("repos", "protobuf")
|
||||||
)
|
|
||||||
|
|
||||||
runPrint(goCmd, "get", fmt.Sprintf("github.com/gogo/protobuf/protoc-gen-gogofast@%v", pv))
|
runPrint(goCmd, "install", fmt.Sprintf("github.com/gogo/protobuf/protoc-gen-gogofast@%v", pv))
|
||||||
os.MkdirAll("repos", 0755)
|
os.MkdirAll("repos", 0755)
|
||||||
for _, dep := range dependencyRepos {
|
|
||||||
path := filepath.Join("repos", dep.path)
|
if _, err := os.Stat(path); err != nil {
|
||||||
if _, err := os.Stat(path); err != nil {
|
runPrint("git", "clone", repo, path)
|
||||||
runPrintInDir("repos", "git", "clone", dep.repo, dep.path)
|
} else {
|
||||||
} else {
|
runPrintInDir(path, "git", "fetch")
|
||||||
runPrintInDir(path, "git", "fetch")
|
|
||||||
}
|
|
||||||
runPrintInDir(path, "git", "checkout", dep.commit)
|
|
||||||
}
|
}
|
||||||
runPrint(goCmd, "generate", "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/stdiscosrv")
|
runPrintInDir(path, "git", "checkout", pv)
|
||||||
|
|
||||||
|
runPrint(goCmd, "generate", "github.com/syncthing/syncthing/cmd/stdiscosrv")
|
||||||
runPrint(goCmd, "generate", "proto/generate.go")
|
runPrint(goCmd, "generate", "proto/generate.go")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testmocks() {
|
||||||
|
args := []string{
|
||||||
|
"generate",
|
||||||
|
"github.com/syncthing/syncthing/lib/config",
|
||||||
|
"github.com/syncthing/syncthing/lib/connections",
|
||||||
|
"github.com/syncthing/syncthing/lib/discover",
|
||||||
|
"github.com/syncthing/syncthing/lib/events",
|
||||||
|
"github.com/syncthing/syncthing/lib/logger",
|
||||||
|
"github.com/syncthing/syncthing/lib/model",
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol",
|
||||||
|
}
|
||||||
|
runPrint(goCmd, args...)
|
||||||
|
}
|
||||||
|
|
||||||
func translate() {
|
func translate() {
|
||||||
os.Chdir("gui/default/assets/lang")
|
os.Chdir("gui/default/assets/lang")
|
||||||
runPipe("lang-en-new.json", goCmd, "run", "../../../../script/translate.go", "lang-en.json", "../../../")
|
runPipe("lang-en-new.json", goCmd, "run", "../../../../script/translate.go", "lang-en.json", "../../../")
|
||||||
@@ -829,6 +974,11 @@ func transifex() {
|
|||||||
runPrint(goCmd, "run", "../../../../script/transifexdl.go")
|
runPrint(goCmd, "run", "../../../../script/transifexdl.go")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func weblate() {
|
||||||
|
os.Chdir("gui/default/assets/lang")
|
||||||
|
runPrint(goCmd, "run", "../../../../script/weblatedl.go")
|
||||||
|
}
|
||||||
|
|
||||||
func ldflags(tags []string) string {
|
func ldflags(tags []string) string {
|
||||||
b := new(strings.Builder)
|
b := new(strings.Builder)
|
||||||
b.WriteString("-w")
|
b.WriteString("-w")
|
||||||
@@ -853,7 +1003,7 @@ func rmr(paths ...string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getReleaseVersion() (string, error) {
|
func getReleaseVersion() (string, error) {
|
||||||
bs, err := ioutil.ReadFile("RELEASE")
|
bs, err := os.ReadFile("RELEASE")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -876,7 +1026,7 @@ func getGitVersion() (string, error) {
|
|||||||
v0 := string(bs)
|
v0 := string(bs)
|
||||||
|
|
||||||
// To be more semantic-versionish and ensure proper ordering in our
|
// To be more semantic-versionish and ensure proper ordering in our
|
||||||
// upgrade process, we make sure there's only one hypen in the version.
|
// upgrade process, we make sure there's only one hyphen in the version.
|
||||||
|
|
||||||
versionRe := regexp.MustCompile(`-([0-9]{1,3}-g[0-9a-f]{5,10}(-dirty)?)`)
|
versionRe := regexp.MustCompile(`-([0-9]{1,3}-g[0-9a-f]{5,10}(-dirty)?)`)
|
||||||
if m := versionRe.FindStringSubmatch(vcur); len(m) > 0 {
|
if m := versionRe.FindStringSubmatch(vcur); len(m) > 0 {
|
||||||
@@ -964,10 +1114,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) {
|
||||||
@@ -1185,11 +1339,11 @@ func zipFile(out string, files []archiveFile) {
|
|||||||
|
|
||||||
if strings.HasSuffix(f.dst, ".txt") {
|
if strings.HasSuffix(f.dst, ".txt") {
|
||||||
// Text file. Read it and convert line endings.
|
// Text file. Read it and convert line endings.
|
||||||
bs, err := ioutil.ReadAll(sf)
|
bs, err := io.ReadAll(sf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
bs = bytes.Replace(bs, []byte{'\n'}, []byte{'\n', '\r'}, -1)
|
bs = bytes.Replace(bs, []byte{'\n'}, []byte{'\r', '\n'}, -1)
|
||||||
fh.UncompressedSize = uint32(len(bs))
|
fh.UncompressedSize = uint32(len(bs))
|
||||||
fh.UncompressedSize64 = uint64(len(bs))
|
fh.UncompressedSize64 = uint64(len(bs))
|
||||||
|
|
||||||
@@ -1260,6 +1414,33 @@ func windowsCodesign(file string) {
|
|||||||
args := []string{"sign", "/fd", algo}
|
args := []string{"sign", "/fd", algo}
|
||||||
if f := os.Getenv("CODESIGN_CERTIFICATE_FILE"); f != "" {
|
if f := os.Getenv("CODESIGN_CERTIFICATE_FILE"); f != "" {
|
||||||
args = append(args, "/f", 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 != "" {
|
if p := os.Getenv("CODESIGN_CERTIFICATE_PASSWORD"); p != "" {
|
||||||
args = append(args, "/p", p)
|
args = append(args, "/p", p)
|
||||||
@@ -1279,7 +1460,7 @@ func windowsCodesign(file string) {
|
|||||||
|
|
||||||
bs, err := runError(st, args...)
|
bs, err := runError(st, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Codesign: signing failed:", string(bs))
|
log.Printf("Codesign: signing failed: %v: %s", err, string(bs))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Println("Codesign: successfully signed", file, "using", algo)
|
log.Println("Codesign: successfully signed", file, "using", algo)
|
||||||
@@ -1296,32 +1477,6 @@ func metalintShort() {
|
|||||||
runPrint(goCmd, "test", "-short", "-run", "Metalint", "./meta")
|
runPrint(goCmd, "test", "-short", "-run", "Metalint", "./meta")
|
||||||
}
|
}
|
||||||
|
|
||||||
func temporaryBuildDir() (string, error) {
|
|
||||||
// The base of our temp dir is "syncthing-xxxxxxxx" where the x:es
|
|
||||||
// are eight bytes from the sha256 of our working directory. We do
|
|
||||||
// this because we want a name in the global temp dir that doesn't
|
|
||||||
// conflict with someone else building syncthing on the same
|
|
||||||
// machine, yet is persistent between runs from the same source
|
|
||||||
// directory.
|
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
hash := sha256.Sum256([]byte(wd))
|
|
||||||
base := fmt.Sprintf("syncthing-%x", hash[:4])
|
|
||||||
|
|
||||||
// The temp dir is taken from $STTMPDIR if set, otherwise the system
|
|
||||||
// default (potentially infrluenced by $TMPDIR on unixes).
|
|
||||||
var tmpDir string
|
|
||||||
if t := os.Getenv("STTMPDIR"); t != "" {
|
|
||||||
tmpDir = t
|
|
||||||
} else {
|
|
||||||
tmpDir = os.TempDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Join(tmpDir, base), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t target) BinaryName() string {
|
func (t target) BinaryName() string {
|
||||||
if goos == "windows" {
|
if goos == "windows" {
|
||||||
return t.binaryName + ".exe"
|
return t.binaryName + ".exe"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ case "${1:-default}" in
|
|||||||
|
|
||||||
prerelease)
|
prerelease)
|
||||||
script authors
|
script authors
|
||||||
build transifex
|
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 'gui, man, authors: Update docs, translations, and contributors'
|
||||||
|
|||||||
@@ -1,96 +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"
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type APIClient struct {
|
|
||||||
http.Client
|
|
||||||
cfg config.GUIConfiguration
|
|
||||||
apikey string
|
|
||||||
}
|
|
||||||
|
|
||||||
func getClient(cfg config.GUIConfiguration) *APIClient {
|
|
||||||
httpClient := http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
},
|
|
||||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
|
||||||
return net.Dial(cfg.Network(), cfg.Address())
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return &APIClient{
|
|
||||||
Client: httpClient,
|
|
||||||
cfg: cfg,
|
|
||||||
apikey: cfg.APIKey,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *APIClient) Endpoint() string {
|
|
||||||
if c.cfg.Network() == "unix" {
|
|
||||||
return "http://unix/"
|
|
||||||
}
|
|
||||||
url := c.cfg.URL()
|
|
||||||
if !strings.HasSuffix(url, "/") {
|
|
||||||
url += "/"
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *APIClient) Do(req *http.Request) (*http.Response, error) {
|
|
||||||
req.Header.Set("X-API-Key", c.apikey)
|
|
||||||
resp, err := c.Client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp, checkResponse(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *APIClient) Get(url string) (*http.Response, error) {
|
|
||||||
request, err := http.NewRequest("GET", c.Endpoint()+"rest/"+url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return c.Do(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *APIClient) Post(url, body string) (*http.Response, error) {
|
|
||||||
request, err := http.NewRequest("POST", c.Endpoint()+"rest/"+url, bytes.NewBufferString(body))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return c.Do(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkResponse(response *http.Response) error {
|
|
||||||
if response.StatusCode == 404 {
|
|
||||||
return errors.New("invalid endpoint or API call")
|
|
||||||
} else if response.StatusCode == 403 {
|
|
||||||
return errors.New("invalid API key")
|
|
||||||
} else if response.StatusCode != 200 {
|
|
||||||
data, err := responseToBArray(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
body := strings.TrimSpace(string(data))
|
|
||||||
return fmt.Errorf("unexpected HTTP status returned: %s\n%s", response.Status, body)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,192 +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 (
|
|
||||||
"bufio"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/AudriusButkevicius/recli"
|
|
||||||
"github.com/flynn-archive/go-shlex"
|
|
||||||
"github.com/mattn/go-isatty"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/syncthing/syncthing/lib/build"
|
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
|
||||||
"github.com/syncthing/syncthing/lib/locations"
|
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// This is somewhat a hack around a chicken and egg problem.
|
|
||||||
// We need to set 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...
|
|
||||||
homeBaseDir := locations.GetBaseDir(locations.ConfigBaseDir)
|
|
||||||
guiCfg := config.GUIConfiguration{}
|
|
||||||
|
|
||||||
flags := flag.NewFlagSet("", flag.ContinueOnError)
|
|
||||||
flags.StringVar(&guiCfg.RawAddress, "gui-address", guiCfg.RawAddress, "Override GUI address (e.g. \"http://192.0.2.42:8443\")")
|
|
||||||
flags.StringVar(&guiCfg.APIKey, "gui-apikey", guiCfg.APIKey, "Override GUI API key")
|
|
||||||
flags.StringVar(&homeBaseDir, "home", homeBaseDir, "Set configuration directory")
|
|
||||||
|
|
||||||
// Implement the same flags at the lower CLI, with the same default values (pre-parse), but do nothing with them.
|
|
||||||
// This is so that we could reuse os.Args
|
|
||||||
fakeFlags := []cli.Flag{
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "gui-address",
|
|
||||||
Value: guiCfg.RawAddress,
|
|
||||||
Usage: "Override GUI address (e.g. \"http://192.0.2.42:8443\")",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "gui-apikey",
|
|
||||||
Value: guiCfg.APIKey,
|
|
||||||
Usage: "Override GUI API key",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "home",
|
|
||||||
Value: homeBaseDir,
|
|
||||||
Usage: "Set configuration directory",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not print usage of these flags, and ignore errors as this can't understand plenty of things
|
|
||||||
flags.Usage = func() {}
|
|
||||||
_ = flags.Parse(os.Args[1:])
|
|
||||||
|
|
||||||
// Now if the API key and address is not provided (we are not connecting to a remote instance),
|
|
||||||
// try to rip it out of the config.
|
|
||||||
if guiCfg.RawAddress == "" && guiCfg.APIKey == "" {
|
|
||||||
// Update the base directory
|
|
||||||
err := locations.SetBaseDir(locations.ConfigBaseDir, homeBaseDir)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(errors.Wrap(err, "setting home"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the certs and get the ID
|
|
||||||
cert, err := tls.LoadX509KeyPair(
|
|
||||||
locations.Get(locations.CertFile),
|
|
||||||
locations.Get(locations.KeyFile),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(errors.Wrap(err, "reading device ID"))
|
|
||||||
}
|
|
||||||
|
|
||||||
myID := protocol.NewDeviceID(cert.Certificate[0])
|
|
||||||
|
|
||||||
// Load the config
|
|
||||||
cfg, _, err := config.Load(locations.Get(locations.ConfigFile), myID, events.NoopLogger)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(errors.Wrap(err, "loading config"))
|
|
||||||
}
|
|
||||||
|
|
||||||
guiCfg = cfg.GUI()
|
|
||||||
} else if guiCfg.Address() == "" || guiCfg.APIKey == "" {
|
|
||||||
log.Fatalln("Both -gui-address and -gui-apikey should be specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
if guiCfg.Address() == "" {
|
|
||||||
log.Fatalln("Could not find GUI Address")
|
|
||||||
}
|
|
||||||
|
|
||||||
if guiCfg.APIKey == "" {
|
|
||||||
log.Fatalln("Could not find GUI API key")
|
|
||||||
}
|
|
||||||
|
|
||||||
client := getClient(guiCfg)
|
|
||||||
|
|
||||||
cfg, err := getConfig(client)
|
|
||||||
original := cfg.Copy()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(errors.Wrap(err, "getting config"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the config and set the default flags
|
|
||||||
recliCfg := recli.DefaultConfig
|
|
||||||
recliCfg.IDTag.Name = "xml"
|
|
||||||
recliCfg.SkipTag.Name = "json"
|
|
||||||
|
|
||||||
commands, err := recli.New(recliCfg).Construct(&cfg)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(errors.Wrap(err, "config reflect"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the actual CLI
|
|
||||||
app := cli.NewApp()
|
|
||||||
app.Name = "stcli"
|
|
||||||
app.HelpName = app.Name
|
|
||||||
app.Author = "The Syncthing Authors"
|
|
||||||
app.Usage = "Syncthing command line interface"
|
|
||||||
app.Version = build.Version
|
|
||||||
app.Flags = fakeFlags
|
|
||||||
app.Metadata = map[string]interface{}{
|
|
||||||
"client": client,
|
|
||||||
}
|
|
||||||
app.Commands = []cli.Command{
|
|
||||||
{
|
|
||||||
Name: "config",
|
|
||||||
HideHelp: true,
|
|
||||||
Usage: "Configuration modification command group",
|
|
||||||
Subcommands: commands,
|
|
||||||
},
|
|
||||||
showCommand,
|
|
||||||
operationCommand,
|
|
||||||
errorsCommand,
|
|
||||||
}
|
|
||||||
|
|
||||||
tty := isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())
|
|
||||||
if !tty {
|
|
||||||
// Not a TTY, consume from stdin
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
for scanner.Scan() {
|
|
||||||
input, err := shlex.Split(scanner.Text())
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(errors.Wrap(err, "parsing input"))
|
|
||||||
}
|
|
||||||
if len(input) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err = app.Run(append(os.Args, input...))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = scanner.Err()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = app.Run(os.Args)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(cfg, original) {
|
|
||||||
body, err := json.MarshalIndent(cfg, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
resp, err := client.Post("system/config", string(body))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
body, err := responseToBArray(resp)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
log.Fatalln(string(body))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +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 (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
|
||||||
|
|
||||||
func responseToBArray(response *http.Response) ([]byte, error) {
|
|
||||||
bytes, err := ioutil.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return bytes, response.Body.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func emptyPost(url string) cli.ActionFunc {
|
|
||||||
return func(c *cli.Context) error {
|
|
||||||
client := c.App.Metadata["client"].(*APIClient)
|
|
||||||
_, err := client.Post(url, "")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dumpOutput(url string) cli.ActionFunc {
|
|
||||||
return func(c *cli.Context) error {
|
|
||||||
client := c.App.Metadata["client"].(*APIClient)
|
|
||||||
response, err := client.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return prettyPrintResponse(c, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getConfig(c *APIClient) (config.Configuration, error) {
|
|
||||||
cfg := config.Configuration{}
|
|
||||||
response, err := c.Get("system/config")
|
|
||||||
if err != nil {
|
|
||||||
return cfg, err
|
|
||||||
}
|
|
||||||
bytes, err := responseToBArray(response)
|
|
||||||
if err != nil {
|
|
||||||
return cfg, err
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(bytes, &cfg)
|
|
||||||
if err == nil {
|
|
||||||
return cfg, err
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
return enc.Encode(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func prettyPrintResponse(c *cli.Context, response *http.Response) error {
|
|
||||||
bytes, err := responseToBArray(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var data interface{}
|
|
||||||
if err := json.Unmarshal(bytes, &data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// TODO: Check flag for pretty print format
|
|
||||||
return prettyPrintJSON(data)
|
|
||||||
}
|
|
||||||
+7
-10
@@ -7,7 +7,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -15,6 +14,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/sha256"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -74,7 +75,7 @@ type fileInfo struct {
|
|||||||
name string
|
name string
|
||||||
mode os.FileMode
|
mode os.FileMode
|
||||||
mod int64
|
mod int64
|
||||||
hash [16]byte
|
hash [sha256.Size]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f fileInfo) String() string {
|
func (f fileInfo) String() string {
|
||||||
@@ -106,11 +107,7 @@ func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) chan er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
h := md5.New()
|
f.hash = sha256.Sum256([]byte(tgt))
|
||||||
h.Write([]byte(tgt))
|
|
||||||
hash := h.Sum(nil)
|
|
||||||
|
|
||||||
copy(f.hash[:], hash)
|
|
||||||
} else if info.IsDir() {
|
} else if info.IsDir() {
|
||||||
f = fileInfo{
|
f = fileInfo{
|
||||||
name: rn,
|
name: rn,
|
||||||
@@ -123,7 +120,7 @@ func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) chan er
|
|||||||
mode: info.Mode(),
|
mode: info.Mode(),
|
||||||
mod: info.ModTime().Unix(),
|
mod: info.ModTime().Unix(),
|
||||||
}
|
}
|
||||||
sum, err := md5file(path)
|
sum, err := sha256file(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -150,14 +147,14 @@ func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) chan er
|
|||||||
return errc
|
return errc
|
||||||
}
|
}
|
||||||
|
|
||||||
func md5file(fname string) (hash [16]byte, err error) {
|
func sha256file(fname string) (hash [sha256.Size]byte, err error) {
|
||||||
f, err := os.Open(fname)
|
f, err := os.Open(fname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
h := md5.New()
|
h := sha256.New()
|
||||||
io.Copy(h, f)
|
io.Copy(h, f)
|
||||||
hb := h.Sum(nil)
|
hb := h.Sum(nil)
|
||||||
copy(hash[:], hb)
|
copy(hash[:], hb)
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
// 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 (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type diskStore struct {
|
||||||
|
dir string
|
||||||
|
inbox chan diskEntry
|
||||||
|
maxBytes int64
|
||||||
|
maxFiles int
|
||||||
|
|
||||||
|
currentFiles []currentFile
|
||||||
|
currentSize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type diskEntry struct {
|
||||||
|
path string
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type currentFile struct {
|
||||||
|
path string
|
||||||
|
size int64
|
||||||
|
mtime int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *diskStore) Serve(ctx context.Context) {
|
||||||
|
if err := os.MkdirAll(d.dir, 0o700); err != nil {
|
||||||
|
log.Println("Creating directory:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.inventory(); err != nil {
|
||||||
|
log.Println("Failed to inventory disk store:", err)
|
||||||
|
}
|
||||||
|
d.clean()
|
||||||
|
|
||||||
|
cleanTimer := time.NewTicker(time.Minute)
|
||||||
|
inventoryTimer := time.NewTicker(24 * time.Hour)
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
gw := gzip.NewWriter(buf)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case entry := <-d.inbox:
|
||||||
|
path := d.fullPath(entry.path)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||||
|
log.Println("Creating directory:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
gw.Reset(buf)
|
||||||
|
if _, err := gw.Write(entry.data); err != nil {
|
||||||
|
log.Println("Failed to compress crash report:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := gw.Close(); err != nil {
|
||||||
|
log.Println("Failed to compress crash report:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, buf.Bytes(), 0o600); err != nil {
|
||||||
|
log.Printf("Failed to write %s: %v", entry.path, err)
|
||||||
|
_ = os.Remove(path)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
d.currentSize += int64(buf.Len())
|
||||||
|
d.currentFiles = append(d.currentFiles, currentFile{
|
||||||
|
size: int64(len(entry.data)),
|
||||||
|
path: path,
|
||||||
|
})
|
||||||
|
|
||||||
|
case <-cleanTimer.C:
|
||||||
|
d.clean()
|
||||||
|
|
||||||
|
case <-inventoryTimer.C:
|
||||||
|
if err := d.inventory(); err != nil {
|
||||||
|
log.Println("Failed to inventory disk store:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *diskStore) Put(path string, data []byte) bool {
|
||||||
|
select {
|
||||||
|
case d.inbox <- diskEntry{
|
||||||
|
path: path,
|
||||||
|
data: data,
|
||||||
|
}:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *diskStore) Get(path string) ([]byte, error) {
|
||||||
|
path = d.fullPath(path)
|
||||||
|
bs, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gr, err := gzip.NewReader(bytes.NewReader(bs))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer gr.Close()
|
||||||
|
return io.ReadAll(gr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *diskStore) Exists(path string) bool {
|
||||||
|
path = d.fullPath(path)
|
||||||
|
_, err := os.Lstat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *diskStore) clean() {
|
||||||
|
for len(d.currentFiles) > 0 && (len(d.currentFiles) > d.maxFiles || d.currentSize > d.maxBytes) {
|
||||||
|
f := d.currentFiles[0]
|
||||||
|
log.Println("Removing", f.path)
|
||||||
|
if err := os.Remove(f.path); err != nil {
|
||||||
|
log.Println("Failed to remove file:", err)
|
||||||
|
}
|
||||||
|
d.currentFiles = d.currentFiles[1:]
|
||||||
|
d.currentSize -= f.size
|
||||||
|
}
|
||||||
|
var oldest time.Duration
|
||||||
|
if len(d.currentFiles) > 0 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *diskStore) inventory() error {
|
||||||
|
d.currentFiles = nil
|
||||||
|
d.currentSize = 0
|
||||||
|
err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if filepath.Ext(path) != ".gz" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
d.currentSize += info.Size()
|
||||||
|
d.currentFiles = append(d.currentFiles, currentFile{
|
||||||
|
path: path,
|
||||||
|
size: info.Size(),
|
||||||
|
mtime: info.ModTime().Unix(),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
sort.Slice(d.currentFiles, func(i, j int) bool {
|
||||||
|
return d.currentFiles[i].mtime < d.currentFiles[j].mtime
|
||||||
|
})
|
||||||
|
var oldest time.Duration
|
||||||
|
if len(d.currentFiles) > 0 {
|
||||||
|
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)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *diskStore) fullPath(path string) string {
|
||||||
|
return filepath.Join(d.dir, path[0:2], path[2:]) + ".gz"
|
||||||
|
}
|
||||||
+75
-33
@@ -13,17 +13,17 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"github.com/syncthing/syncthing/lib/sha256"
|
"github.com/syncthing/syncthing/lib/sha256"
|
||||||
"github.com/syncthing/syncthing/lib/ur"
|
"github.com/syncthing/syncthing/lib/ur"
|
||||||
|
|
||||||
@@ -32,34 +32,66 @@ import (
|
|||||||
|
|
||||||
const maxRequestSize = 1 << 20 // 1 MiB
|
const maxRequestSize = 1 << 20 // 1 MiB
|
||||||
|
|
||||||
|
type cli struct {
|
||||||
|
Dir string `help:"Parent directory to store crash and failure reports in" env:"REPORTS_DIR" default:"."`
|
||||||
|
DSN string `help:"Sentry DSN" env:"SENTRY_DSN"`
|
||||||
|
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"`
|
||||||
|
MaxDiskSizeMB int64 `help:"Maximum disk space to use for reports" default:"1024" env:"MAX_DISK_SIZE_MB"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
dir := flag.String("dir", ".", "Directory to store reports in")
|
var params cli
|
||||||
dsn := flag.String("dsn", "", "Sentry DSN")
|
kong.Parse(¶ms)
|
||||||
listen := flag.String("listen", ":22039", "HTTP listen address")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
cr := &crashReceiver{
|
ds := &diskStore{
|
||||||
dir: *dir,
|
dir: filepath.Join(params.Dir, "crash_reports"),
|
||||||
dsn: *dsn,
|
inbox: make(chan diskEntry, params.DiskQueue),
|
||||||
|
maxFiles: params.MaxDiskFiles,
|
||||||
|
maxBytes: params.MaxDiskSizeMB << 20,
|
||||||
}
|
}
|
||||||
mux.Handle("/", cr)
|
go ds.Serve(context.Background())
|
||||||
|
|
||||||
if *dsn != "" {
|
ss := &sentryService{
|
||||||
mux.HandleFunc("/failure", handleFailureFn(*dsn))
|
dsn: params.DSN,
|
||||||
|
inbox: make(chan sentryRequest, params.SentryQueue),
|
||||||
|
}
|
||||||
|
go ss.Serve(context.Background())
|
||||||
|
|
||||||
|
cr := &crashReceiver{
|
||||||
|
store: ds,
|
||||||
|
sentry: ss,
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.Handle("/", cr)
|
||||||
|
mux.HandleFunc("/ping", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
w.Write([]byte("OK"))
|
||||||
|
})
|
||||||
|
mux.Handle("/metrics", promhttp.Handler())
|
||||||
|
|
||||||
|
if params.DSN != "" {
|
||||||
|
mux.HandleFunc("/newcrash/failure", handleFailureFn(params.DSN, filepath.Join(params.Dir, "failure_reports")))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.SetOutput(os.Stdout)
|
log.SetOutput(os.Stdout)
|
||||||
if err := http.ListenAndServe(*listen, mux); err != nil {
|
if err := http.ListenAndServe(params.Listen, mux); err != nil {
|
||||||
log.Fatalln("HTTP serve:", err)
|
log.Fatalln("HTTP serve:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFailureFn(dsn string) func(w http.ResponseWriter, req *http.Request) {
|
func handleFailureFn(dsn, failureDir string) 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 := ioutil.ReadAll(lr)
|
bs, err := io.ReadAll(lr)
|
||||||
req.Body.Close()
|
req.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
@@ -84,35 +116,45 @@ func handleFailureFn(dsn string) func(w http.ResponseWriter, req *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, r := range reports {
|
for _, r := range reports {
|
||||||
pkt := packet(version)
|
pkt := packet(version, "failure")
|
||||||
pkt.Message = r.Description
|
pkt.Message = r.Description
|
||||||
pkt.Extra = raven.Extra{
|
pkt.Extra = raven.Extra{
|
||||||
"count": r.Count,
|
"count": r.Count,
|
||||||
}
|
}
|
||||||
pkt.Fingerprint = []string{r.Description}
|
for k, v := range r.Extra {
|
||||||
|
pkt.Extra[k] = v
|
||||||
|
}
|
||||||
|
if r.Goroutines != "" {
|
||||||
|
url, err := saveFailureWithGoroutines(r.FailureData, failureDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Saving failure report:", err)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pkt.Extra["goroutinesURL"] = url
|
||||||
|
}
|
||||||
|
message := sanitizeMessageLDB(r.Description)
|
||||||
|
pkt.Fingerprint = []string{message}
|
||||||
|
|
||||||
if err := sendReport(dsn, pkt, userIDFor(req)); err != nil {
|
if err := sendReport(dsn, pkt, userIDFor(req)); err != nil {
|
||||||
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// userIDFor returns a string we can use as the user ID for the purpose of
|
func saveFailureWithGoroutines(data ur.FailureData, failureDir string) (string, error) {
|
||||||
// counting affected users. It's the truncated hash of a salt, the user
|
bs := make([]byte, len(data.Description)+len(data.Goroutines))
|
||||||
// remote IP, and the current month.
|
copy(bs, data.Description)
|
||||||
func userIDFor(req *http.Request) string {
|
copy(bs[len(data.Description):], data.Goroutines)
|
||||||
addr := req.RemoteAddr
|
id := fmt.Sprintf("%x", sha256.Sum256(bs))
|
||||||
if fwd := req.Header.Get("x-forwarded-for"); fwd != "" {
|
path := fullPathCompressed(failureDir, id)
|
||||||
addr = fwd
|
err := compressAndWrite(bs, path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
if host, _, err := net.SplitHostPort(addr); err == nil {
|
return reportServer + path, nil
|
||||||
addr = host
|
|
||||||
}
|
|
||||||
now := time.Now().Format("200601")
|
|
||||||
salt := "stcrashreporter"
|
|
||||||
hash := sha256.Sum256([]byte(salt + addr + now))
|
|
||||||
return fmt.Sprintf("%x", hash[:8])
|
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
})
|
||||||
|
)
|
||||||
@@ -8,14 +8,16 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io"
|
||||||
|
"log"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
raven "github.com/getsentry/raven-go"
|
raven "github.com/getsentry/raven-go"
|
||||||
"github.com/maruel/panicparse/stack"
|
"github.com/maruel/panicparse/v2/stack"
|
||||||
)
|
)
|
||||||
|
|
||||||
const reportServer = "https://crash.syncthing.net/report/"
|
const reportServer = "https://crash.syncthing.net/report/"
|
||||||
@@ -31,6 +33,45 @@ var (
|
|||||||
clientsMut sync.Mutex
|
clientsMut sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type sentryService struct {
|
||||||
|
dsn string
|
||||||
|
inbox chan sentryRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
type sentryRequest struct {
|
||||||
|
reportID string
|
||||||
|
userID string
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sentryService) Serve(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case req := <-s.inbox:
|
||||||
|
pkt, err := parseCrashReport(req.reportID, req.data)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to parse crash report:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := sendReport(s.dsn, pkt, req.userID); err != nil {
|
||||||
|
log.Println("Failed to send crash report:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sentryService) Send(reportID, userID string, data []byte) bool {
|
||||||
|
select {
|
||||||
|
case s.inbox <- sentryRequest{reportID, userID, data}:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sendReport(dsn string, pkt *raven.Packet, userID string) error {
|
func sendReport(dsn string, pkt *raven.Packet, userID string) error {
|
||||||
pkt.Interfaces = append(pkt.Interfaces, &raven.User{ID: userID})
|
pkt.Interfaces = append(pkt.Interfaces, &raven.User{ID: userID})
|
||||||
|
|
||||||
@@ -93,10 +134,13 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r := bytes.NewReader(report)
|
r := bytes.NewReader(report)
|
||||||
ctx, err := stack.ParseDump(r, ioutil.Discard, false)
|
ctx, _, err := stack.ScanSnapshot(r, io.Discard, stack.DefaultOpts())
|
||||||
if err != nil {
|
if err != nil && err != io.EOF {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if ctx == nil || len(ctx.Goroutines) == 0 {
|
||||||
|
return nil, errors.New("no goroutines found")
|
||||||
|
}
|
||||||
|
|
||||||
// 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 != "" {
|
||||||
@@ -116,13 +160,13 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
|
|||||||
if gr.First {
|
if gr.First {
|
||||||
trace.Frames = make([]*raven.StacktraceFrame, len(gr.Stack.Calls))
|
trace.Frames = make([]*raven.StacktraceFrame, len(gr.Stack.Calls))
|
||||||
for i, sc := range gr.Stack.Calls {
|
for i, sc := range gr.Stack.Calls {
|
||||||
trace.Frames[len(trace.Frames)-1-i] = raven.NewStacktraceFrame(0, sc.Func.Name(), sc.SrcPath, sc.Line, 3, nil)
|
trace.Frames[len(trace.Frames)-1-i] = raven.NewStacktraceFrame(0, sc.Func.Name, sc.RemoteSrcPath, sc.Line, 3, nil)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pkt := packet(version)
|
pkt := packet(version, "crash")
|
||||||
pkt.Message = string(subjectLine)
|
pkt.Message = string(subjectLine)
|
||||||
pkt.Extra = raven.Extra{
|
pkt.Extra = raven.Extra{
|
||||||
"url": reportServer + path,
|
"url": reportServer + path,
|
||||||
@@ -143,15 +187,20 @@ var (
|
|||||||
ldbPathRe = regexp.MustCompile(`(open|write|read) .+[\\/].+[\\/]index[^\\/]+[\\/][^\\/]+: `)
|
ldbPathRe = regexp.MustCompile(`(open|write|read) .+[\\/].+[\\/]index[^\\/]+[\\/][^\\/]+: `)
|
||||||
)
|
)
|
||||||
|
|
||||||
func crashReportFingerprint(message string) []string {
|
func sanitizeMessageLDB(message string) string {
|
||||||
// Do not fingerprint on the stack in case of db corruption or fatal
|
|
||||||
// db io error - where it occurs doesn't matter.
|
|
||||||
orig := message
|
|
||||||
message = ldbPosRe.ReplaceAllString(message, "${1}x)")
|
message = ldbPosRe.ReplaceAllString(message, "${1}x)")
|
||||||
message = ldbFileRe.ReplaceAllString(message, "${1}x${3}")
|
message = ldbFileRe.ReplaceAllString(message, "${1}x${3}")
|
||||||
message = ldbChecksumRe.ReplaceAllString(message, "${1}X${3}X")
|
message = ldbChecksumRe.ReplaceAllString(message, "${1}X${3}X")
|
||||||
message = ldbInternalKeyRe.ReplaceAllString(message, "${1}x${2}x")
|
message = ldbInternalKeyRe.ReplaceAllString(message, "${1}x${2}x")
|
||||||
message = ldbPathRe.ReplaceAllString(message, "$1 x: ")
|
message = ldbPathRe.ReplaceAllString(message, "$1 x: ")
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
func crashReportFingerprint(message string) []string {
|
||||||
|
// Do not fingerprint on the stack in case of db corruption or fatal
|
||||||
|
// db io error - where it occurs doesn't matter.
|
||||||
|
orig := message
|
||||||
|
message = sanitizeMessageLDB(message)
|
||||||
if message != orig {
|
if message != orig {
|
||||||
return []string{message}
|
return []string{message}
|
||||||
}
|
}
|
||||||
@@ -167,7 +216,13 @@ func crashReportFingerprint(message string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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]
|
// 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]
|
||||||
var longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?$`)
|
// or, somewhere along the way the "+" in the version tag disappeared:
|
||||||
|
// syncthing v1.23.7-dev.26.gdf7b56ae.dirty-stversionextra "Fermium Flea" (go1.20.5 darwin-arm64) jb@ok.kastelo.net 2023-07-12 06:55:26 UTC [Some Wrapper, purego, stnoupgrade]
|
||||||
|
var (
|
||||||
|
longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?$`)
|
||||||
|
gitExtraRE = regexp.MustCompile(`\.\d+\.g[0-9a-f]+`) // ".1.g6aaae618"
|
||||||
|
gitExtraSepRE = regexp.MustCompile(`[.-]`) // dot or dash
|
||||||
|
)
|
||||||
|
|
||||||
type version struct {
|
type version struct {
|
||||||
version string // "v1.1.4-rc.1+30-g6aaae618-dirty-crashrep"
|
version string // "v1.1.4-rc.1+30-g6aaae618-dirty-crashrep"
|
||||||
@@ -209,10 +264,21 @@ func parseVersion(line string) (version, error) {
|
|||||||
builder: m[6],
|
builder: m[6],
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(v.version, "+")
|
// Split the version tag into tag and commit. This is old style
|
||||||
|
// v1.2.3-something.4+11-g12345678 or newer with just dots
|
||||||
|
// v1.2.3-something.4.11.g12345678 or v1.2.3-dev.11.g12345678.
|
||||||
|
parts := []string{v.version}
|
||||||
|
if strings.Contains(v.version, "+") {
|
||||||
|
parts = strings.Split(v.version, "+")
|
||||||
|
} else {
|
||||||
|
idxs := gitExtraRE.FindStringIndex(v.version)
|
||||||
|
if len(idxs) > 0 {
|
||||||
|
parts = []string{v.version[:idxs[0]], v.version[idxs[0]+1:]}
|
||||||
|
}
|
||||||
|
}
|
||||||
v.tag = parts[0]
|
v.tag = parts[0]
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
fields := strings.Split(parts[1], "-")
|
fields := gitExtraSepRE.Split(parts[1], -1)
|
||||||
if len(fields) >= 2 && strings.HasPrefix(fields[1], "g") {
|
if len(fields) >= 2 && strings.HasPrefix(fields[1], "g") {
|
||||||
v.commit = fields[1][1:]
|
v.commit = fields[1][1:]
|
||||||
}
|
}
|
||||||
@@ -229,7 +295,7 @@ func parseVersion(line string) (version, error) {
|
|||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func packet(version version) *raven.Packet {
|
func packet(version version, reportType string) *raven.Packet {
|
||||||
pkt := &raven.Packet{
|
pkt := &raven.Packet{
|
||||||
Platform: "go",
|
Platform: "go",
|
||||||
Release: version.tag,
|
Release: version.tag,
|
||||||
@@ -242,6 +308,7 @@ func packet(version version) *raven.Packet {
|
|||||||
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},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if version.commit != "" {
|
if version.commit != "" {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,6 +44,20 @@ func TestParseVersion(t *testing.T) {
|
|||||||
extra: []string{"foo", "bar"},
|
extra: []string{"foo", "bar"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
longVersion: `syncthing v1.23.7-dev.26.gdf7b56ae-stversionextra "Fermium Flea" (go1.20.5 darwin-arm64) jb@ok.kastelo.net 2023-07-12 06:55:26 UTC [Some Wrapper, purego, stnoupgrade]`,
|
||||||
|
parsed: version{
|
||||||
|
version: "v1.23.7-dev.26.gdf7b56ae-stversionextra",
|
||||||
|
tag: "v1.23.7-dev",
|
||||||
|
commit: "df7b56ae",
|
||||||
|
codename: "Fermium Flea",
|
||||||
|
runtime: "go1.20.5",
|
||||||
|
goos: "darwin",
|
||||||
|
goarch: "arm64",
|
||||||
|
builder: "jb@ok.kastelo.net",
|
||||||
|
extra: []string{"Some Wrapper", "purego", "stnoupgrade"},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@@ -59,7 +73,7 @@ func TestParseVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseReport(t *testing.T) {
|
func TestParseReport(t *testing.T) {
|
||||||
bs, err := ioutil.ReadFile("_testdata/panic.log")
|
bs, err := os.ReadFile("_testdata/panic.log")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -80,7 +80,7 @@ func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]b
|
|||||||
fmt.Println("Loading source:", resp.Status)
|
fmt.Println("Loading source:", resp.Status)
|
||||||
return nil, 0
|
return nil, 0
|
||||||
}
|
}
|
||||||
data, err := ioutil.ReadAll(resp.Body)
|
data, err := io.ReadAll(resp.Body)
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Loading source:", err.Error())
|
fmt.Println("Loading source:", err.Error())
|
||||||
|
|||||||
@@ -7,21 +7,16 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type crashReceiver struct {
|
type crashReceiver struct {
|
||||||
dir string
|
store *diskStore
|
||||||
dsn string
|
sentry *sentryService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
@@ -44,99 +39,64 @@ func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// The location of the report on disk, compressed
|
|
||||||
fullPath := filepath.Join(r.dir, r.dirFor(reportID), reportID) + ".gz"
|
|
||||||
|
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
r.serveGet(fullPath, w, req)
|
r.serveGet(reportID, w, req)
|
||||||
case http.MethodHead:
|
case http.MethodHead:
|
||||||
r.serveHead(fullPath, w, req)
|
r.serveHead(reportID, w, req)
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
r.servePut(reportID, fullPath, w, req)
|
r.servePut(reportID, w, req)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveGet responds to GET requests by serving the uncompressed report.
|
// serveGet responds to GET requests by serving the uncompressed report.
|
||||||
func (r *crashReceiver) serveGet(fullPath string, w http.ResponseWriter, _ *http.Request) {
|
func (r *crashReceiver) serveGet(reportID string, w http.ResponseWriter, _ *http.Request) {
|
||||||
fd, err := os.Open(fullPath)
|
bs, err := r.store.Get(reportID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Not found", http.StatusNotFound)
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
w.Write(bs)
|
||||||
defer fd.Close()
|
|
||||||
gr, err := gzip.NewReader(fd)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, _ = io.Copy(w, gr) // best effort
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(fullPath string, w http.ResponseWriter, _ *http.Request) {
|
func (r *crashReceiver) serveHead(reportID string, w http.ResponseWriter, _ *http.Request) {
|
||||||
if _, err := os.Lstat(fullPath); err != nil {
|
if !r.store.Exists(reportID) {
|
||||||
http.Error(w, "Not found", http.StatusNotFound)
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// servePut accepts and stores the given report.
|
// servePut accepts and stores the given report.
|
||||||
func (r *crashReceiver) servePut(reportID, fullPath string, w http.ResponseWriter, req *http.Request) {
|
func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *http.Request) {
|
||||||
// Ensure the destination directory exists
|
result := "receive_failure"
|
||||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
defer func() {
|
||||||
log.Println("Creating directory:", err)
|
metricCrashReportsTotal.WithLabelValues(result).Inc()
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
}()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
||||||
bs, err := ioutil.ReadAll(lr)
|
bs, err := io.ReadAll(lr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Reading report:", err)
|
log.Println("Reading report:", err)
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compress the report for storage
|
result = "success"
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
gw := gzip.NewWriter(buf)
|
|
||||||
_, _ = gw.Write(bs) // can't fail
|
|
||||||
gw.Close()
|
|
||||||
|
|
||||||
// Create an output file with the compressed report
|
// Store the report
|
||||||
err = ioutil.WriteFile(fullPath, buf.Bytes(), 0644)
|
if !r.store.Put(reportID, bs) {
|
||||||
if err != nil {
|
log.Println("Failed to store report (queue full):", reportID)
|
||||||
log.Println("Saving report:", err)
|
result = "queue_failure"
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the report to Sentry
|
// Send the report to Sentry
|
||||||
if r.dsn != "" {
|
if !r.sentry.Send(reportID, userIDFor(req), bs) {
|
||||||
// Remote ID
|
log.Println("Failed to send report to sentry (queue full):", reportID)
|
||||||
user := userIDFor(req)
|
result = "sentry_failure"
|
||||||
|
|
||||||
go func() {
|
|
||||||
// There's no need for the client to have to wait for this part.
|
|
||||||
pkt, err := parseCrashReport(reportID, bs)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to parse crash report:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := sendReport(r.dsn, pkt, user); err != nil {
|
|
||||||
log.Println("Failed to send crash report:", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 01234567890abcdef... => 01/23
|
|
||||||
func (r *crashReceiver) dirFor(base string) string {
|
|
||||||
return filepath.Join(base[0:2], base[2:4])
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (C) 2021 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"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/sha256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// userIDFor returns a string we can use as the user ID for the purpose of
|
||||||
|
// counting affected users. It's the truncated hash of a salt, the user
|
||||||
|
// remote IP, and the current month.
|
||||||
|
func userIDFor(req *http.Request) string {
|
||||||
|
addr := req.RemoteAddr
|
||||||
|
if fwd := req.Header.Get("x-forwarded-for"); fwd != "" {
|
||||||
|
addr = fwd
|
||||||
|
}
|
||||||
|
if host, _, err := net.SplitHostPort(addr); err == nil {
|
||||||
|
addr = host
|
||||||
|
}
|
||||||
|
now := time.Now().Format("200601")
|
||||||
|
salt := "stcrashreporter"
|
||||||
|
hash := sha256.Sum256([]byte(salt + addr + now))
|
||||||
|
return fmt.Sprintf("%x", hash[:8])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 01234567890abcdef... => 01/23
|
||||||
|
func dirFor(base string) string {
|
||||||
|
return filepath.Join(base[0:2], base[2:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
func fullPathCompressed(root, reportID string) string {
|
||||||
|
return filepath.Join(root, dirFor(reportID), reportID) + ".gz"
|
||||||
|
}
|
||||||
|
|
||||||
|
func compressAndWrite(bs []byte, fullPath string) error {
|
||||||
|
// Compress the report for storage
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
gw := gzip.NewWriter(buf)
|
||||||
|
_, _ = gw.Write(bs) // can't fail
|
||||||
|
gw.Close()
|
||||||
|
|
||||||
|
// Create an output file with the compressed report
|
||||||
|
return os.WriteFile(fullPath, buf.Bytes(), 0644)
|
||||||
|
}
|
||||||
+95
-57
@@ -8,11 +8,15 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
io "io"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
@@ -25,6 +29,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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
|
||||||
@@ -66,7 +71,7 @@ func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSrv) Serve(ctx context.Context) error {
|
func (s *apiSrv) Serve(_ 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 {
|
||||||
@@ -76,18 +81,10 @@ func (s *apiSrv) Serve(ctx 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)
|
||||||
@@ -105,6 +102,7 @@ func (s *apiSrv) Serve(ctx context.Context) error {
|
|||||||
ReadTimeout: httpReadTimeout,
|
ReadTimeout: httpReadTimeout,
|
||||||
WriteTimeout: httpWriteTimeout,
|
WriteTimeout: httpWriteTimeout,
|
||||||
MaxHeaderBytes: httpMaxHeaderBytes,
|
MaxHeaderBytes: httpMaxHeaderBytes,
|
||||||
|
ErrorLog: log.New(io.Discard, "", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := srv.Serve(s.listener)
|
err := srv.Serve(s.listener)
|
||||||
@@ -114,8 +112,6 @@ func (s *apiSrv) Serve(ctx 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()
|
||||||
|
|
||||||
@@ -128,10 +124,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{
|
||||||
@@ -140,7 +136,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)
|
||||||
}
|
}
|
||||||
@@ -157,17 +158,17 @@ 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 {
|
||||||
@@ -211,28 +212,39 @@ func (s *apiSrv) handleGET(ctx context.Context, w http.ResponseWriter, req *http
|
|||||||
s.db.put(key, rec)
|
s.db.put(key, rec)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Retry-After", notFoundRetryAfterString(int(misses)))
|
afterS := notFoundRetryAfterSeconds(int(misses))
|
||||||
|
retryAfterHistogram.Observe(float64(afterS))
|
||||||
|
w.Header().Set("Retry-After", strconv.Itoa(afterS))
|
||||||
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 strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
gw := gzip.NewWriter(bw)
|
||||||
|
defer gw.Close()
|
||||||
|
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 := certificateBytes(req)
|
rawCert, err := certificateBytes(req)
|
||||||
if rawCert == nil {
|
if err != nil {
|
||||||
if debug {
|
if debug {
|
||||||
log.Println(reqID, "no certificates")
|
log.Println(reqID, "no certificates:", err)
|
||||||
}
|
}
|
||||||
announceRequestsTotal.WithLabelValues("no_certificate").Inc()
|
announceRequestsTotal.WithLabelValues("no_certificate").Inc()
|
||||||
w.Header().Set("Retry-After", errorRetryAfterString())
|
w.Header().Set("Retry-After", errorRetryAfterString())
|
||||||
@@ -300,13 +312,13 @@ func (s *apiSrv) handleAnnounce(deviceID protocol.DeviceID, addresses []string)
|
|||||||
return s.db.merge(key, dbAddrs, seen)
|
return s.db.merge(key, dbAddrs, seen)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePing(w http.ResponseWriter, r *http.Request) {
|
func handlePing(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(204)
|
w.WriteHeader(204)
|
||||||
}
|
}
|
||||||
|
|
||||||
func certificateBytes(req *http.Request) []byte {
|
func certificateBytes(req *http.Request) ([]byte, error) {
|
||||||
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
|
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
|
||||||
return req.TLS.PeerCertificates[0].Raw
|
return req.TLS.PeerCertificates[0].Raw, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var bs []byte
|
var bs []byte
|
||||||
@@ -319,7 +331,7 @@ func certificateBytes(req *http.Request) []byte {
|
|||||||
hdr, err := url.QueryUnescape(hdr)
|
hdr, err := url.QueryUnescape(hdr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Decoding failed
|
// Decoding failed
|
||||||
return nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
bs = []byte(hdr)
|
bs = []byte(hdr)
|
||||||
@@ -338,15 +350,27 @@ func certificateBytes(req *http.Request) []byte {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if hdr := req.Header.Get("X-Forwarded-Tls-Client-Cert"); hdr != "" {
|
} else if hdr := req.Header.Get("X-Tls-Client-Cert-Der-Base64"); hdr != "" {
|
||||||
// Traefik 2 passtlsclientcert
|
// Caddy {tls_client_certificate_der_base64}
|
||||||
// The certificate is in PEM format with url encoding but without newlines
|
hdr, err := base64.StdEncoding.DecodeString(hdr)
|
||||||
// and start/end statements. We need to decode, reinstate the newlines every 64
|
|
||||||
// character and add statements for the PEM decoder
|
|
||||||
hdr, err := url.QueryUnescape(hdr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Decoding failed
|
// Decoding failed
|
||||||
return nil
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bs = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: hdr})
|
||||||
|
} else if hdr := req.Header.Get("X-Forwarded-Tls-Client-Cert"); hdr != "" {
|
||||||
|
// Traefik 2 passtlsclientcert
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
if strings.Contains(hdr, "%") {
|
||||||
|
if unesc, err := url.QueryUnescape(hdr); err == nil {
|
||||||
|
hdr = unesc
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 64; i < len(hdr); i += 65 {
|
for i := 64; i < len(hdr); i += 65 {
|
||||||
@@ -354,21 +378,21 @@ func certificateBytes(req *http.Request) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hdr = "-----BEGIN CERTIFICATE-----\n" + hdr
|
hdr = "-----BEGIN CERTIFICATE-----\n" + hdr
|
||||||
hdr = hdr + "\n-----END CERTIFICATE-----\n"
|
hdr += "\n-----END CERTIFICATE-----\n"
|
||||||
bs = []byte(hdr)
|
bs = []byte(hdr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if bs == nil {
|
if bs == nil {
|
||||||
return nil
|
return nil, errors.New("empty certificate header")
|
||||||
}
|
}
|
||||||
|
|
||||||
block, _ := pem.Decode(bs)
|
block, _ := pem.Decode(bs)
|
||||||
if block == nil {
|
if block == nil {
|
||||||
// Decoding failed
|
// Decoding failed
|
||||||
return nil
|
return nil, errors.New("certificate decode result is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
return block.Bytes
|
return block.Bytes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixupAddresses checks the list of addresses, removing invalid ones and
|
// fixupAddresses checks the list of addresses, removing invalid ones and
|
||||||
@@ -393,13 +417,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,11 +439,22 @@ func fixupAddresses(remote *net.TCPAddr, addresses []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
host = remote.IP.String()
|
host = remote.IP.String()
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// remote is nil, unable to determine host IP
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// If zero port was specified, use remote port.
|
}
|
||||||
if port == "0" && remote.Port > 0 {
|
|
||||||
port = fmt.Sprintf("%d", remote.Port)
|
// 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)
|
||||||
|
} else {
|
||||||
|
// unable to determine remote port
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,6 +462,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,13 +494,13 @@ func errorRetryAfterString() string {
|
|||||||
return strconv.Itoa(errorRetryAfterSeconds + rand.Intn(errorRetryFuzzSeconds))
|
return strconv.Itoa(errorRetryAfterSeconds + rand.Intn(errorRetryFuzzSeconds))
|
||||||
}
|
}
|
||||||
|
|
||||||
func notFoundRetryAfterString(misses int) string {
|
func notFoundRetryAfterSeconds(misses int) int {
|
||||||
retryAfterS := notFoundRetryMinSeconds + notFoundRetryIncSeconds*misses
|
retryAfterS := notFoundRetryMinSeconds + notFoundRetryIncSeconds*misses
|
||||||
if retryAfterS > notFoundRetryMaxSeconds {
|
if retryAfterS > notFoundRetryMaxSeconds {
|
||||||
retryAfterS = notFoundRetryMaxSeconds
|
retryAfterS = notFoundRetryMaxSeconds
|
||||||
}
|
}
|
||||||
retryAfterS += rand.Intn(notFoundRetryFuzzSeconds)
|
retryAfterS += rand.Intn(notFoundRetryFuzzSeconds)
|
||||||
return strconv.Itoa(retryAfterS)
|
return retryAfterS
|
||||||
}
|
}
|
||||||
|
|
||||||
func reannounceAfterString() string {
|
func reannounceAfterString() string {
|
||||||
|
|||||||
@@ -69,6 +69,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"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+47
-10
@@ -12,10 +12,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/sliceutil"
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||||
"github.com/syndtr/goleveldb/leveldb/util"
|
"github.com/syndtr/goleveldb/leveldb/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,6 +58,18 @@ func newLevelDBStore(dir string) (*levelDBStore, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newMemoryLevelDBStore() (*levelDBStore, error) {
|
||||||
|
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &levelDBStore{
|
||||||
|
db: db,
|
||||||
|
inbox: make(chan func(), 16),
|
||||||
|
clock: defaultClock{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *levelDBStore) put(key string, rec DatabaseRecord) error {
|
func (s *levelDBStore) put(key string, rec DatabaseRecord) error {
|
||||||
t0 := time.Now()
|
t0 := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -204,7 +220,7 @@ func (s *levelDBStore) statisticsServe(trigger <-chan struct{}, done chan<- stru
|
|||||||
cutoff24h := t0.Add(-24 * time.Hour).UnixNano()
|
cutoff24h := t0.Add(-24 * time.Hour).UnixNano()
|
||||||
cutoff1w := t0.Add(-7 * 24 * time.Hour).UnixNano()
|
cutoff1w := t0.Add(-7 * 24 * time.Hour).UnixNano()
|
||||||
cutoff2Mon := t0.Add(-60 * 24 * time.Hour).UnixNano()
|
cutoff2Mon := t0.Add(-60 * 24 * time.Hour).UnixNano()
|
||||||
current, last24h, last1w, inactive, errors := 0, 0, 0, 0, 0
|
current, currentIPv4, currentIPv6, last24h, last1w, inactive, errors := 0, 0, 0, 0, 0, 0, 0
|
||||||
|
|
||||||
iter := s.db.NewIterator(&util.Range{}, nil)
|
iter := s.db.NewIterator(&util.Range{}, nil)
|
||||||
for iter.Next() {
|
for iter.Next() {
|
||||||
@@ -219,9 +235,35 @@ func (s *levelDBStore) statisticsServe(trigger <-chan struct{}, done chan<- stru
|
|||||||
// If there are addresses that have not expired it's a current
|
// If there are addresses that have not expired it's a current
|
||||||
// record, otherwise account it based on when it was last seen
|
// record, otherwise account it based on when it was last seen
|
||||||
// (last 24 hours or last week) or finally as inactice.
|
// (last 24 hours or last week) or finally as inactice.
|
||||||
|
addrs := expire(rec.Addresses, nowNanos)
|
||||||
switch {
|
switch {
|
||||||
case len(expire(rec.Addresses, nowNanos)) > 0:
|
case len(addrs) > 0:
|
||||||
current++
|
current++
|
||||||
|
seenIPv4, seenIPv6 := false, false
|
||||||
|
for _, addr := range addrs {
|
||||||
|
uri, err := url.Parse(addr.Address)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
host, _, err := net.SplitHostPort(uri.Host)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ip := net.ParseIP(host); ip != nil && ip.To4() != nil {
|
||||||
|
seenIPv4 = true
|
||||||
|
} else if ip != nil {
|
||||||
|
seenIPv6 = true
|
||||||
|
}
|
||||||
|
if seenIPv4 && seenIPv6 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seenIPv4 {
|
||||||
|
currentIPv4++
|
||||||
|
}
|
||||||
|
if seenIPv6 {
|
||||||
|
currentIPv6++
|
||||||
|
}
|
||||||
case rec.Seen > cutoff24h:
|
case rec.Seen > cutoff24h:
|
||||||
last24h++
|
last24h++
|
||||||
case rec.Seen > cutoff1w:
|
case rec.Seen > cutoff1w:
|
||||||
@@ -245,6 +287,8 @@ func (s *levelDBStore) statisticsServe(trigger <-chan struct{}, done chan<- stru
|
|||||||
iter.Release()
|
iter.Release()
|
||||||
|
|
||||||
databaseKeys.WithLabelValues("current").Set(float64(current))
|
databaseKeys.WithLabelValues("current").Set(float64(current))
|
||||||
|
databaseKeys.WithLabelValues("currentIPv4").Set(float64(currentIPv4))
|
||||||
|
databaseKeys.WithLabelValues("currentIPv6").Set(float64(currentIPv6))
|
||||||
databaseKeys.WithLabelValues("last24h").Set(float64(last24h))
|
databaseKeys.WithLabelValues("last24h").Set(float64(last24h))
|
||||||
databaseKeys.WithLabelValues("last1w").Set(float64(last1w))
|
databaseKeys.WithLabelValues("last1w").Set(float64(last1w))
|
||||||
databaseKeys.WithLabelValues("inactive").Set(float64(inactive))
|
databaseKeys.WithLabelValues("inactive").Set(float64(inactive))
|
||||||
@@ -339,14 +383,7 @@ func expire(addrs []DatabaseAddress, now int64) []DatabaseAddress {
|
|||||||
i := 0
|
i := 0
|
||||||
for i < len(addrs) {
|
for i < len(addrs) {
|
||||||
if addrs[i].Expires < now {
|
if addrs[i].Expires < now {
|
||||||
// This item is expired. Replace it with the last in the list
|
addrs = sliceutil.RemoveAndZero(addrs, i)
|
||||||
// (noop if we are at the last item).
|
|
||||||
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++
|
i++
|
||||||
|
|||||||
@@ -510,10 +510,7 @@ func (m *DatabaseRecord) Unmarshal(dAtA []byte) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if skippy < 0 {
|
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
if (iNdEx + skippy) < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
return ErrInvalidLengthDatabase
|
||||||
}
|
}
|
||||||
if (iNdEx + skippy) > l {
|
if (iNdEx + skippy) > l {
|
||||||
@@ -648,10 +645,7 @@ func (m *ReplicationRecord) Unmarshal(dAtA []byte) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if skippy < 0 {
|
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
if (iNdEx + skippy) < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
return ErrInvalidLengthDatabase
|
||||||
}
|
}
|
||||||
if (iNdEx + skippy) > l {
|
if (iNdEx + skippy) > l {
|
||||||
@@ -752,10 +746,7 @@ func (m *DatabaseAddress) Unmarshal(dAtA []byte) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if skippy < 0 {
|
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
||||||
return ErrInvalidLengthDatabase
|
|
||||||
}
|
|
||||||
if (iNdEx + skippy) < 0 {
|
|
||||||
return ErrInvalidLengthDatabase
|
return ErrInvalidLengthDatabase
|
||||||
}
|
}
|
||||||
if (iNdEx + skippy) > l {
|
if (iNdEx + skippy) > l {
|
||||||
|
|||||||
@@ -9,15 +9,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDatabaseGetSet(t *testing.T) {
|
func TestDatabaseGetSet(t *testing.T) {
|
||||||
os.RemoveAll("_database")
|
db, err := newMemoryLevelDBStore()
|
||||||
defer os.RemoveAll("_database")
|
|
||||||
db, err := newLevelDBStore("_database")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -119,7 +116,7 @@ func TestDatabaseGetSet(t *testing.T) {
|
|||||||
|
|
||||||
// Put a record with misses
|
// Put a record with misses
|
||||||
|
|
||||||
rec = DatabaseRecord{Misses: 42}
|
rec = DatabaseRecord{Misses: 42, Missed: tc.Now().UnixNano()}
|
||||||
if err := db.put("efgh", rec); err != nil {
|
if err := db.put("efgh", rec); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -188,7 +185,7 @@ func TestFilter(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
a: []DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}, {Address: "c", Expires: 5}, {Address: "d", Expires: 15}, {Address: "e", Expires: 5}},
|
a: []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: []DatabaseAddress{{Address: "b", Expires: 15}, {Address: "d", Expires: 15}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,5 +206,6 @@ func (t *testClock) wind(d time.Duration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *testClock) Now() time.Time {
|
func (t *testClock) Now() time.Time {
|
||||||
|
t.now = t.now.Add(time.Nanosecond)
|
||||||
return t.now
|
return t.now
|
||||||
}
|
}
|
||||||
+39
-7
@@ -14,6 +14,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -64,9 +65,7 @@ var levelDBOptions = &opt.Options{
|
|||||||
WriteBuffer: 32 << 20, // default 4<<20
|
WriteBuffer: 32 << 20, // default 4<<20
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var debug = false
|
||||||
debug = false
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var listen string
|
var listen string
|
||||||
@@ -76,20 +75,26 @@ func main() {
|
|||||||
var replicationPeers string
|
var replicationPeers string
|
||||||
var certFile string
|
var certFile string
|
||||||
var keyFile string
|
var keyFile string
|
||||||
|
var replCertFile string
|
||||||
|
var replKeyFile string
|
||||||
var useHTTP bool
|
var useHTTP bool
|
||||||
|
var largeDB bool
|
||||||
|
|
||||||
log.SetOutput(os.Stdout)
|
log.SetOutput(os.Stdout)
|
||||||
log.SetFlags(0)
|
log.SetFlags(0)
|
||||||
|
|
||||||
flag.StringVar(&certFile, "cert", "./cert.pem", "Certificate file")
|
flag.StringVar(&certFile, "cert", "./cert.pem", "Certificate file")
|
||||||
|
flag.StringVar(&keyFile, "key", "./key.pem", "Key file")
|
||||||
flag.StringVar(&dir, "db-dir", "./discovery.db", "Database directory")
|
flag.StringVar(&dir, "db-dir", "./discovery.db", "Database directory")
|
||||||
flag.BoolVar(&debug, "debug", false, "Print debug output")
|
flag.BoolVar(&debug, "debug", false, "Print debug output")
|
||||||
flag.BoolVar(&useHTTP, "http", false, "Listen on HTTP (behind an HTTPS proxy)")
|
flag.BoolVar(&useHTTP, "http", false, "Listen on HTTP (behind an HTTPS proxy)")
|
||||||
flag.StringVar(&listen, "listen", ":8443", "Listen address")
|
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(&metricsListen, "metrics-listen", "", "Metrics listen address")
|
||||||
flag.StringVar(&replicationPeers, "replicate", "", "Replication peers, id@address, comma separated")
|
flag.StringVar(&replicationPeers, "replicate", "", "Replication peers, id@address, comma separated")
|
||||||
flag.StringVar(&replicationListen, "replication-listen", ":19200", "Replication listen address")
|
flag.StringVar(&replicationListen, "replication-listen", ":19200", "Replication listen address")
|
||||||
|
flag.StringVar(&replCertFile, "replication-cert", "", "Certificate file for replication")
|
||||||
|
flag.StringVar(&replKeyFile, "replication-key", "", "Key file for replication")
|
||||||
|
flag.BoolVar(&largeDB, "large-db", false, "Use larger database settings")
|
||||||
showVersion := flag.Bool("version", false, "Show version")
|
showVersion := flag.Bool("version", false, "Show version")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -98,6 +103,17 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildInfo.WithLabelValues(build.Version, runtime.Version(), build.User, build.Date.UTC().Format("2006-01-02T15:04:05Z")).Set(1)
|
||||||
|
|
||||||
|
if largeDB {
|
||||||
|
levelDBOptions.BlockCacheCapacity = 64 << 20
|
||||||
|
levelDBOptions.BlockSize = 64 << 10
|
||||||
|
levelDBOptions.CompactionTableSize = 16 << 20
|
||||||
|
levelDBOptions.CompactionTableSizeMultiplier = 2.0
|
||||||
|
levelDBOptions.WriteBuffer = 64 << 20
|
||||||
|
levelDBOptions.CompactionL0Trigger = 8
|
||||||
|
}
|
||||||
|
|
||||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
log.Println("Failed to load keypair. Generating one, this might take a while...")
|
log.Println("Failed to load keypair. Generating one, this might take a while...")
|
||||||
@@ -111,13 +127,26 @@ func main() {
|
|||||||
devID := protocol.NewDeviceID(cert.Certificate[0])
|
devID := protocol.NewDeviceID(cert.Certificate[0])
|
||||||
log.Println("Server device ID is", devID)
|
log.Println("Server device ID is", devID)
|
||||||
|
|
||||||
|
replCert := cert
|
||||||
|
if replCertFile != "" && replKeyFile != "" {
|
||||||
|
replCert, err = tls.LoadX509KeyPair(replCertFile, replKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Failed to load replication keypair:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replDevID := protocol.NewDeviceID(replCert.Certificate[0])
|
||||||
|
log.Println("Replication device ID is", replDevID)
|
||||||
|
|
||||||
// Parse the replication specs, if any.
|
// Parse the replication specs, if any.
|
||||||
var allowedReplicationPeers []protocol.DeviceID
|
var allowedReplicationPeers []protocol.DeviceID
|
||||||
var replicationDestinations []string
|
var replicationDestinations []string
|
||||||
parts := strings.Split(replicationPeers, ",")
|
parts := strings.Split(replicationPeers, ",")
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
fields := strings.Split(part, "@")
|
if part == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := strings.Split(part, "@")
|
||||||
switch len(fields) {
|
switch len(fields) {
|
||||||
case 2:
|
case 2:
|
||||||
// This is an id@address specification. Grab the address for the
|
// This is an id@address specification. Grab the address for the
|
||||||
@@ -137,6 +166,9 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Parsing device ID:", err)
|
log.Fatalln("Parsing device ID:", err)
|
||||||
}
|
}
|
||||||
|
if id == protocol.EmptyDeviceID {
|
||||||
|
log.Fatalf("Missing device ID for peer in %q", part)
|
||||||
|
}
|
||||||
allowedReplicationPeers = append(allowedReplicationPeers, id)
|
allowedReplicationPeers = append(allowedReplicationPeers, id)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -159,14 +191,14 @@ func main() {
|
|||||||
// Start any replication senders.
|
// Start any replication senders.
|
||||||
var repl replicationMultiplexer
|
var repl replicationMultiplexer
|
||||||
for _, dst := range replicationDestinations {
|
for _, dst := range replicationDestinations {
|
||||||
rs := newReplicationSender(dst, cert, allowedReplicationPeers)
|
rs := newReplicationSender(dst, replCert, allowedReplicationPeers)
|
||||||
main.Add(rs)
|
main.Add(rs)
|
||||||
repl = append(repl, rs)
|
repl = append(repl, rs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have replication configured, start the replication listener.
|
// If we have replication configured, start the replication listener.
|
||||||
if len(allowedReplicationPeers) > 0 {
|
if len(allowedReplicationPeers) > 0 {
|
||||||
rl := newReplicationListener(replicationListen, cert, allowedReplicationPeers, db)
|
rl := newReplicationListener(replicationListen, replCert, allowedReplicationPeers, db)
|
||||||
main.Add(rl)
|
main.Add(rl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ import (
|
|||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
const replicationReadTimeout = time.Minute
|
const (
|
||||||
const replicationHeartbeatInterval = time.Second * 30
|
replicationReadTimeout = time.Minute
|
||||||
|
replicationWriteTimeout = 30 * time.Second
|
||||||
|
replicationHeartbeatInterval = time.Second * 30
|
||||||
|
)
|
||||||
|
|
||||||
type replicator interface {
|
type replicator interface {
|
||||||
send(key string, addrs []DatabaseAddress, seen int64)
|
send(key string, addrs []DatabaseAddress, seen int64)
|
||||||
@@ -68,6 +71,12 @@ func (s *replicationSender) Serve(ctx context.Context) error {
|
|||||||
conn.Close()
|
conn.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// The replication stream is not especially latency sensitive, but it is
|
||||||
|
// quite a lot of data in small writes. Make it more efficient.
|
||||||
|
if tcpc, ok := conn.NetConn().(*net.TCPConn); ok {
|
||||||
|
_ = tcpc.SetNoDelay(false)
|
||||||
|
}
|
||||||
|
|
||||||
// Get the other side device ID.
|
// Get the other side device ID.
|
||||||
remoteID, err := deviceID(conn)
|
remoteID, err := deviceID(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,11 +125,11 @@ func (s *replicationSender) Serve(ctx context.Context) error {
|
|||||||
binary.BigEndian.PutUint32(buf, uint32(n))
|
binary.BigEndian.PutUint32(buf, uint32(n))
|
||||||
|
|
||||||
// Send
|
// Send
|
||||||
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
conn.SetWriteDeadline(time.Now().Add(replicationWriteTimeout))
|
||||||
if _, err := conn.Write(buf[:4+n]); err != nil {
|
if _, err := conn.Write(buf[:4+n]); err != nil {
|
||||||
replicationSendsTotal.WithLabelValues("error").Inc()
|
replicationSendsTotal.WithLabelValues("error").Inc()
|
||||||
log.Println("Replication write:", err)
|
log.Println("Replication write:", err)
|
||||||
// Yes, we are loosing the replication event here.
|
// Yes, we are losing the replication event here.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
replicationSendsTotal.WithLabelValues("success").Inc()
|
replicationSendsTotal.WithLabelValues("success").Inc()
|
||||||
@@ -135,7 +144,7 @@ func (s *replicationSender) String() string {
|
|||||||
return fmt.Sprintf("replicationSender(%q)", s.dst)
|
return fmt.Sprintf("replicationSender(%q)", s.dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *replicationSender) send(key string, ps []DatabaseAddress, seen int64) {
|
func (s *replicationSender) send(key string, ps []DatabaseAddress, _ int64) {
|
||||||
item := ReplicationRecord{
|
item := ReplicationRecord{
|
||||||
Key: key,
|
Key: key,
|
||||||
Addresses: ps,
|
Addresses: ps,
|
||||||
|
|||||||
+23
-5
@@ -10,9 +10,18 @@ import (
|
|||||||
"os"
|
"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",
|
||||||
@@ -89,6 +98,14 @@ 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"})
|
||||||
|
|
||||||
|
retryAfterHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||||
|
Namespace: "syncthing",
|
||||||
|
Subsystem: "discovery",
|
||||||
|
Name: "retry_after_seconds",
|
||||||
|
Help: "Retry-After header value in seconds.",
|
||||||
|
Buckets: prometheus.ExponentialBuckets(60, 2, 7), // 60, 120, 240, 480, 960, 1920, 3840
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -103,13 +120,15 @@ 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,
|
||||||
|
retryAfterHistogram)
|
||||||
|
|
||||||
processCollectorOpts := prometheus.ProcessCollectorOpts{
|
processCollectorOpts := collectors.ProcessCollectorOpts{
|
||||||
Namespace: "syncthing_discovery",
|
Namespace: "syncthing_discovery",
|
||||||
PidFn: func() (int, error) {
|
PidFn: func() (int, error) {
|
||||||
return os.Getpid(), nil
|
return os.Getpid(), nil
|
||||||
@@ -117,7 +136,6 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prometheus.MustRegister(
|
prometheus.MustRegister(
|
||||||
prometheus.NewProcessCollector(processCollectorOpts),
|
collectors.NewProcessCollector(processCollectorOpts),
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -28,16 +28,21 @@ func main() {
|
|||||||
log.SetFlags(0)
|
log.SetFlags(0)
|
||||||
|
|
||||||
target := flag.String("target", "localhost:8384", "Target Syncthing instance")
|
target := flag.String("target", "localhost:8384", "Target Syncthing instance")
|
||||||
|
types := flag.String("types", "", "Filter for specific event types (comma-separated)")
|
||||||
apikey := flag.String("apikey", "", "Syncthing API key")
|
apikey := flag.String("apikey", "", "Syncthing API key")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *apikey == "" {
|
if *apikey == "" {
|
||||||
log.Fatal("Must give -apikey argument")
|
log.Fatal("Must give -apikey argument")
|
||||||
}
|
}
|
||||||
|
var eventsArg string
|
||||||
|
if len(*types) > 0 {
|
||||||
|
eventsArg = "&events=" + *types
|
||||||
|
}
|
||||||
|
|
||||||
since := 0
|
since := 0
|
||||||
for {
|
for {
|
||||||
req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/rest/events?since=%d", *target, since), nil)
|
req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/rest/events?since=%d%s", *target, since, eventsArg), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ func checkServers(deviceID protocol.DeviceID, servers ...string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkServer(deviceID protocol.DeviceID, server string) checkResult {
|
func checkServer(deviceID protocol.DeviceID, server string) checkResult {
|
||||||
disco, err := discover.NewGlobal(server, tls.Certificate{}, nil, events.NoopLogger)
|
disco, err := discover.NewGlobal(server, tls.Certificate{}, nil, events.NoopLogger, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return checkResult{error: err}
|
return checkResult{error: 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/.
|
||||||
|
|
||||||
// Commmand stfindignored lists ignored files under a given folder root.
|
// Command stfindignored lists ignored files under a given folder root.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -66,7 +66,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
|
||||||
@@ -105,11 +105,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
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
// Copyright (C) 2014 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 (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/db/backend"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var mode string
|
|
||||||
log.SetFlags(0)
|
|
||||||
log.SetOutput(os.Stdout)
|
|
||||||
|
|
||||||
flag.StringVar(&mode, "mode", "dump", "Mode of operation: dump, dumpsize, idxck")
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
path := flag.Arg(0)
|
|
||||||
if path == "" {
|
|
||||||
path = filepath.Join(defaultConfigDir(), "index-v0.14.0.db")
|
|
||||||
}
|
|
||||||
|
|
||||||
var ldb backend.Backend
|
|
||||||
var err error
|
|
||||||
if looksLikeBadger(path) {
|
|
||||||
ldb, err = backend.OpenBadger(path)
|
|
||||||
} else {
|
|
||||||
ldb, err = backend.OpenLevelDBRO(path)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch mode {
|
|
||||||
case "dump":
|
|
||||||
dump(ldb)
|
|
||||||
case "dumpsize":
|
|
||||||
dumpsize(ldb)
|
|
||||||
case "idxck":
|
|
||||||
if !idxck(ldb) {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
case "account":
|
|
||||||
account(ldb)
|
|
||||||
default:
|
|
||||||
fmt.Println("Unknown mode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func looksLikeBadger(path string) bool {
|
|
||||||
_, err := os.Stat(filepath.Join(path, "KEYREGISTRY"))
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
// Copyright (C) 2015 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"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func nulString(bs []byte) string {
|
|
||||||
for i := range bs {
|
|
||||||
if bs[i] == 0 {
|
|
||||||
return string(bs[:i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return string(bs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultConfigDir() string {
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "windows":
|
|
||||||
if p := os.Getenv("LocalAppData"); p != "" {
|
|
||||||
return filepath.Join(p, "Syncthing")
|
|
||||||
}
|
|
||||||
return filepath.Join(os.Getenv("AppData"), "Syncthing")
|
|
||||||
|
|
||||||
case "darwin":
|
|
||||||
dir, err := fs.ExpandTilde("~/Library/Application Support/Syncthing")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
return dir
|
|
||||||
|
|
||||||
default:
|
|
||||||
if xdgCfg := os.Getenv("XDG_CONFIG_HOME"); xdgCfg != "" {
|
|
||||||
return filepath.Join(xdgCfg, "syncthing")
|
|
||||||
}
|
|
||||||
dir, err := fs.ExpandTilde("~/.config/syncthing")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (C) 2021 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/.
|
||||||
|
|
||||||
|
//go:build noassets
|
||||||
|
// +build noassets
|
||||||
|
|
||||||
|
package auto
|
||||||
|
|
||||||
|
import "github.com/syncthing/syncthing/lib/assets"
|
||||||
|
|
||||||
|
func Assets() map[string]assets.Asset {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -46,18 +46,18 @@
|
|||||||
<h1>Relay Pool Data</h1>
|
<h1>Relay Pool Data</h1>
|
||||||
<div ng-if="relays === undefined" class="text-center">
|
<div ng-if="relays === undefined" class="text-center">
|
||||||
<img src="https://cdnjs.cloudflare.com/ajax/libs/galleriffic/2.0.1/css/loader.gif" alt=""/>
|
<img src="https://cdnjs.cloudflare.com/ajax/libs/galleriffic/2.0.1/css/loader.gif" alt=""/>
|
||||||
<p>Please wait while we gather data</p>
|
<p>Please wait while we gather data…</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div ng-show="relays !== undefined" class="ng-hide">
|
<div ng-show="relays !== undefined" class="ng-hide">
|
||||||
<p>
|
<p>
|
||||||
The relays listed on this page are not managed or vetted by the Syncthing project.
|
The relays listed on this page are not managed or vetted by the Syncthing project.
|
||||||
Each relay is the responsibility of the relay operator.
|
Each relay is the responsibility of the relay operator.
|
||||||
Currently {{ relays.length }} relays online.
|
Currently {{ relays.length }} relays are online.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="map"></div> <!-- Can't hide the map, otherwise it freaks out -->
|
<div id="map"></div> <!-- Can't hide the map, otherwise it freaks out -->
|
||||||
<p>The circle size represents how much bytes the relay transferred relative to other relays</p>
|
<p>The circle size represents how much bytes the relay has transferred relatively to other relays.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<table class="table table-striped table-condensed table">
|
<table class="table table-striped table-condensed table">
|
||||||
@@ -188,7 +188,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<p>
|
<p>
|
||||||
This product includes GeoLite2 data created by MaxMind, available from
|
This product includes GeoLite2 data created by MaxMind, available from
|
||||||
<a href="http://www.maxmind.com">http://www.maxmind.com</a>.
|
<a href="https://www.maxmind.com">https://www.maxmind.com</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -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('http://{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
|
||||||
|
|||||||
+105
-82
@@ -3,14 +3,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"compress/gzip"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -19,9 +17,13 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/groupcache/lru"
|
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/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"
|
||||||
@@ -31,7 +33,6 @@ import (
|
|||||||
"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"
|
||||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||||
"golang.org/x/time/rate"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type location struct {
|
type location struct {
|
||||||
@@ -97,27 +98,13 @@ var (
|
|||||||
dir string
|
dir string
|
||||||
evictionTime = time.Hour
|
evictionTime = time.Hour
|
||||||
debug bool
|
debug bool
|
||||||
getLRUSize = 10 << 10
|
|
||||||
getLimitBurst = 10
|
|
||||||
getLimitAvg = 2
|
|
||||||
postLRUSize = 1 << 10
|
|
||||||
postLimitBurst = 2
|
|
||||||
postLimitAvg = 2
|
|
||||||
getLimit time.Duration
|
|
||||||
postLimit time.Duration
|
|
||||||
permRelaysFile string
|
permRelaysFile string
|
||||||
ipHeader string
|
ipHeader string
|
||||||
geoipPath string
|
geoipPath string
|
||||||
proto string
|
proto string
|
||||||
statsRefresh = time.Minute / 2
|
statsRefresh = time.Minute
|
||||||
requestQueueLen = 10
|
requestQueueLen = 64
|
||||||
requestProcessors = 1
|
requestProcessors = 8
|
||||||
|
|
||||||
getMut = sync.NewMutex()
|
|
||||||
getLRUCache *lru.Cache
|
|
||||||
|
|
||||||
postMut = sync.NewMutex()
|
|
||||||
postLRUCache *lru.Cache
|
|
||||||
|
|
||||||
requests chan request
|
requests chan request
|
||||||
|
|
||||||
@@ -125,6 +112,7 @@ var (
|
|||||||
knownRelays = make([]*relay, 0)
|
knownRelays = make([]*relay, 0)
|
||||||
permanentRelays = make([]*relay, 0)
|
permanentRelays = make([]*relay, 0)
|
||||||
evictionTimers = make(map[string]*time.Timer)
|
evictionTimers = make(map[string]*time.Timer)
|
||||||
|
globalBlocklist = newErrorTracker(1000)
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -139,13 +127,8 @@ func main() {
|
|||||||
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.IntVar(&getLRUSize, "get-limit-cache", getLRUSize, "Get request limiter cache size")
|
|
||||||
flag.IntVar(&getLimitAvg, "get-limit-avg", getLimitAvg, "Allowed average get request rate, per 10 s")
|
|
||||||
flag.IntVar(&getLimitBurst, "get-limit-burst", getLimitBurst, "Allowed burst get requests")
|
|
||||||
flag.IntVar(&postLRUSize, "post-limit-cache", postLRUSize, "Post request limiter cache size")
|
|
||||||
flag.IntVar(&postLimitAvg, "post-limit-avg", postLimitAvg, "Allowed average post request rate, per minute")
|
|
||||||
flag.IntVar(&postLimitBurst, "post-limit-burst", postLimitBurst, "Allowed burst post requests")
|
|
||||||
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(&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(&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")
|
||||||
@@ -157,12 +140,6 @@ func main() {
|
|||||||
|
|
||||||
requests = make(chan request, requestQueueLen)
|
requests = make(chan request, requestQueueLen)
|
||||||
|
|
||||||
getLimit = 10 * time.Second / time.Duration(getLimitAvg)
|
|
||||||
postLimit = time.Minute / time.Duration(postLimitAvg)
|
|
||||||
|
|
||||||
getLRUCache = lru.New(getLRUSize)
|
|
||||||
postLRUCache = lru.New(postLRUSize)
|
|
||||||
|
|
||||||
var listener net.Listener
|
var listener net.Listener
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -208,6 +185,7 @@ func main() {
|
|||||||
tlsCfg := &tls.Config{
|
tlsCfg := &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
MinVersion: tls.VersionTLS10, // No SSLv3
|
MinVersion: tls.VersionTLS10, // No SSLv3
|
||||||
|
ClientAuth: tls.RequestClientCert,
|
||||||
CipherSuites: []uint16{
|
CipherSuites: []uint16{
|
||||||
// No RC4
|
// No RC4
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
@@ -237,7 +215,7 @@ func main() {
|
|||||||
|
|
||||||
handler := http.NewServeMux()
|
handler := http.NewServeMux()
|
||||||
handler.HandleFunc("/", handleAssets)
|
handler.HandleFunc("/", handleAssets)
|
||||||
handler.HandleFunc("/endpoint", handleRequest)
|
handler.Handle("/endpoint", httpcache.SinglePath(http.HandlerFunc(handleRequest), 15*time.Second))
|
||||||
handler.HandleFunc("/metrics", handleMetrics)
|
handler.HandleFunc("/metrics", handleMetrics)
|
||||||
|
|
||||||
srv := http.Server{
|
srv := http.Server{
|
||||||
@@ -288,21 +266,17 @@ func handleRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if ipHeader != "" {
|
if ipHeader != "" {
|
||||||
r.RemoteAddr = r.Header.Get(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", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case "GET":
|
case "GET":
|
||||||
if limit(r.RemoteAddr, getLRUCache, getMut, getLimit, getLimitBurst) {
|
|
||||||
w.WriteHeader(httpStatusEnhanceYourCalm)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
handleGetRequest(w, r)
|
handleGetRequest(w, r)
|
||||||
case "POST":
|
case "POST":
|
||||||
if limit(r.RemoteAddr, postLRUCache, postMut, postLimit, postLimitBurst) {
|
|
||||||
w.WriteHeader(httpStatusEnhanceYourCalm)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
handlePostRequest(w, r)
|
handlePostRequest(w, r)
|
||||||
default:
|
default:
|
||||||
if debug {
|
if debug {
|
||||||
@@ -324,20 +298,34 @@ func handleGetRequest(rw http.ResponseWriter, r *http.Request) {
|
|||||||
// Shuffle
|
// Shuffle
|
||||||
rand.Shuffle(relays)
|
rand.Shuffle(relays)
|
||||||
|
|
||||||
w := io.Writer(rw)
|
_ = json.NewEncoder(rw).Encode(map[string][]*relay{
|
||||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
||||||
rw.Header().Set("Content-Encoding", "gzip")
|
|
||||||
gw := gzip.NewWriter(rw)
|
|
||||||
defer gw.Close()
|
|
||||||
w = gw
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = json.NewEncoder(w).Encode(map[string][]*relay{
|
|
||||||
"relays": relays,
|
"relays": relays,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePostRequest(w http.ResponseWriter, r *http.Request) {
|
func handlePostRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get the IP address of the client
|
||||||
|
rhost := r.RemoteAddr
|
||||||
|
if host, _, err := net.SplitHostPort(rhost); err == nil {
|
||||||
|
rhost = host
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the black list. A client is blacklisted if their last 10
|
||||||
|
// attempts to join have all failed. The "Unauthorized" status return
|
||||||
|
// causes strelaysrv to cease attempting to join.
|
||||||
|
if globalBlocklist.IsBlocked(rhost) {
|
||||||
|
log.Println("Rejected blocked client", rhost)
|
||||||
|
http.Error(w, "Too many errors", http.StatusUnauthorized)
|
||||||
|
globalBlocklist.ClearErrors(rhost)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var relayCert *x509.Certificate
|
||||||
|
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
|
||||||
|
relayCert = r.TLS.PeerCertificates[0]
|
||||||
|
log.Printf("Got TLS cert from relay server")
|
||||||
|
}
|
||||||
|
|
||||||
var newRelay relay
|
var newRelay relay
|
||||||
err := json.NewDecoder(r.Body).Decode(&newRelay)
|
err := json.NewDecoder(r.Body).Decode(&newRelay)
|
||||||
r.Body.Close()
|
r.Body.Close()
|
||||||
@@ -359,6 +347,21 @@ func handlePostRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Canonicalize the URL. In particular, parse and re-encode the query
|
||||||
|
// string so that it's guaranteed to be valid.
|
||||||
|
uri.RawQuery = uri.Query().Encode()
|
||||||
|
newRelay.URL = uri.String()
|
||||||
|
|
||||||
|
if relayCert != nil {
|
||||||
|
advertisedId := uri.Query().Get("id")
|
||||||
|
idFromCert := protocol.NewDeviceID(relayCert.Raw).String()
|
||||||
|
if advertisedId != idFromCert {
|
||||||
|
log.Println("Warning: Relay server requested to join with an ID different from the join request, rejecting")
|
||||||
|
http.Error(w, "mismatched advertised id and join request cert", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
host, port, err := net.SplitHostPort(uri.Host)
|
host, port, err := net.SplitHostPort(uri.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if debug {
|
if debug {
|
||||||
@@ -368,18 +371,12 @@ func handlePostRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the IP address of the client
|
|
||||||
rhost := r.RemoteAddr
|
|
||||||
if host, _, err := net.SplitHostPort(rhost); err == nil {
|
|
||||||
rhost = host
|
|
||||||
}
|
|
||||||
|
|
||||||
ip := net.ParseIP(host)
|
ip := net.ParseIP(host)
|
||||||
// The client did not provide an IP address, use the IP address of the client.
|
// The client did not provide an IP address, use the IP address of the client.
|
||||||
if ip == nil || ip.IsUnspecified() {
|
if ip == nil || ip.IsUnspecified() {
|
||||||
uri.Host = net.JoinHostPort(rhost, port)
|
uri.Host = net.JoinHostPort(rhost, port)
|
||||||
newRelay.URL = uri.String()
|
newRelay.URL = uri.String()
|
||||||
} else if host != rhost {
|
} else if host != rhost && relayCert == nil {
|
||||||
if debug {
|
if debug {
|
||||||
log.Println("IP address advertised does not match client IP address", r.RemoteAddr, uri)
|
log.Println("IP address advertised does not match client IP address", r.RemoteAddr, uri)
|
||||||
}
|
}
|
||||||
@@ -405,10 +402,14 @@ func handlePostRequest(w http.ResponseWriter, r *http.Request) {
|
|||||||
case requests <- request{&newRelay, reschan, prometheus.NewTimer(relayTestActionsSeconds.WithLabelValues("queue"))}:
|
case requests <- request{&newRelay, reschan, prometheus.NewTimer(relayTestActionsSeconds.WithLabelValues("queue"))}:
|
||||||
result := <-reschan
|
result := <-reschan
|
||||||
if result.err != nil {
|
if result.err != nil {
|
||||||
|
log.Println("Join from", r.RemoteAddr, "failed:", result.err)
|
||||||
|
globalBlocklist.AddError(rhost)
|
||||||
relayTestsTotal.WithLabelValues("failed").Inc()
|
relayTestsTotal.WithLabelValues("failed").Inc()
|
||||||
http.Error(w, result.err.Error(), http.StatusBadRequest)
|
http.Error(w, result.err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Println("Join from", r.RemoteAddr, "succeeded")
|
||||||
|
globalBlocklist.ClearErrors(rhost)
|
||||||
relayTestsTotal.WithLabelValues("success").Inc()
|
relayTestsTotal.WithLabelValues("success").Inc()
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
json.NewEncoder(w).Encode(map[string]time.Duration{
|
json.NewEncoder(w).Encode(map[string]time.Duration{
|
||||||
@@ -456,7 +457,7 @@ func handleRelayTest(request request) {
|
|||||||
updateMetrics(request.relay.uri.Host, *stats, location)
|
updateMetrics(request.relay.uri.Host, *stats, location)
|
||||||
}
|
}
|
||||||
request.relay.Stats = stats
|
request.relay.Stats = stats
|
||||||
request.relay.StatsRetrieved = time.Now()
|
request.relay.StatsRetrieved = time.Now().Truncate(time.Second)
|
||||||
request.relay.Location = location
|
request.relay.Location = location
|
||||||
|
|
||||||
timer, ok := evictionTimers[request.relay.uri.Host]
|
timer, ok := evictionTimers[request.relay.uri.Host]
|
||||||
@@ -522,25 +523,8 @@ func evict(relay *relay) func() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func limit(addr string, cache *lru.Cache, lock sync.Mutex, intv time.Duration, burst int) bool {
|
|
||||||
if host, _, err := net.SplitHostPort(addr); err == nil {
|
|
||||||
addr = host
|
|
||||||
}
|
|
||||||
|
|
||||||
lock.Lock()
|
|
||||||
v, _ := cache.Get(addr)
|
|
||||||
bkt, ok := v.(*rate.Limiter)
|
|
||||||
if !ok {
|
|
||||||
bkt = rate.NewLimiter(rate.Every(intv), burst)
|
|
||||||
cache.Add(addr, bkt)
|
|
||||||
}
|
|
||||||
lock.Unlock()
|
|
||||||
|
|
||||||
return !bkt.Allow()
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadRelays(file string) []*relay {
|
func loadRelays(file string) []*relay {
|
||||||
content, err := ioutil.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())
|
||||||
return nil
|
return nil
|
||||||
@@ -548,7 +532,7 @@ func loadRelays(file string) []*relay {
|
|||||||
|
|
||||||
var relays []*relay
|
var relays []*relay
|
||||||
for _, line := range strings.Split(string(content), "\n") {
|
for _, line := range strings.Split(string(content), "\n") {
|
||||||
if len(line) == 0 {
|
if line == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,11 +562,11 @@ func saveRelays(file string, relays []*relay) error {
|
|||||||
for _, relay := range relays {
|
for _, relay := range relays {
|
||||||
content += relay.uri.String() + "\n"
|
content += relay.uri.String() + "\n"
|
||||||
}
|
}
|
||||||
return ioutil.WriteFile(file, []byte(content), 0777)
|
return os.WriteFile(file, []byte(content), 0o777)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTestCertificate() tls.Certificate {
|
func createTestCertificate() tls.Certificate {
|
||||||
tmpDir, err := ioutil.TempDir("", "relaypoolsrv")
|
tmpDir, err := os.MkdirTemp("", "relaypoolsrv")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -637,3 +621,42 @@ func (lrw *loggingResponseWriter) WriteHeader(code int) {
|
|||||||
lrw.statusCode = code
|
lrw.statusCode = code
|
||||||
lrw.ResponseWriter.WriteHeader(code)
|
lrw.ResponseWriter.WriteHeader(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type errorTracker struct {
|
||||||
|
errors *lru.TwoQueueCache[string, *errorCounter]
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorCounter struct {
|
||||||
|
count atomic.Int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func newErrorTracker(size int) *errorTracker {
|
||||||
|
cache, err := lru.New2Q[string, *errorCounter](size)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return &errorTracker{
|
||||||
|
errors: cache,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *errorTracker) AddError(host string) {
|
||||||
|
entry, ok := b.errors.Get(host)
|
||||||
|
if !ok {
|
||||||
|
entry = &errorCounter{}
|
||||||
|
b.errors.Add(host, entry)
|
||||||
|
}
|
||||||
|
c := entry.count.Add(1)
|
||||||
|
log.Printf("Error count for %s is now %d", host, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *errorTracker) ClearErrors(host string) {
|
||||||
|
b.errors.Remove(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *errorTracker) IsBlocked(host string) bool {
|
||||||
|
if be, ok := b.errors.Get(host); ok {
|
||||||
|
return be.count.Load() > 10
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -65,3 +66,29 @@ func TestHandleGetRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCanonicalizeQueryValues(t *testing.T) {
|
||||||
|
// This just demonstrates and validates the uri.Parse/String stuff in
|
||||||
|
// regards to query strings.
|
||||||
|
|
||||||
|
in := "http://example.com/?some weird= query^value"
|
||||||
|
exp := "http://example.com/?some+weird=+query%5Evalue"
|
||||||
|
|
||||||
|
uri, err := url.Parse(in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
str := uri.String()
|
||||||
|
if str != in {
|
||||||
|
// Just re-encoding the URL doesn't sanitize the query string.
|
||||||
|
t.Errorf("expected %q, got %q", in, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
uri.RawQuery = uri.Query().Encode()
|
||||||
|
str = uri.String()
|
||||||
|
if str != exp {
|
||||||
|
// The query string is now in correct format.
|
||||||
|
t.Errorf("expected %q, got %q", exp, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,11 +10,12 @@ import (
|
|||||||
"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() {
|
func init() {
|
||||||
processCollectorOpts := prometheus.ProcessCollectorOpts{
|
processCollectorOpts := collectors.ProcessCollectorOpts{
|
||||||
Namespace: "syncthing_relaypoolsrv",
|
Namespace: "syncthing_relaypoolsrv",
|
||||||
PidFn: func() (int, error) {
|
PidFn: func() (int, error) {
|
||||||
return os.Getpid(), nil
|
return os.Getpid(), nil
|
||||||
@@ -22,7 +23,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prometheus.MustRegister(
|
prometheus.MustRegister(
|
||||||
prometheus.NewProcessCollector(processCollectorOpts),
|
collectors.NewProcessCollector(processCollectorOpts),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-10
@@ -20,10 +20,10 @@ import (
|
|||||||
var (
|
var (
|
||||||
outboxesMut = sync.RWMutex{}
|
outboxesMut = sync.RWMutex{}
|
||||||
outboxes = make(map[syncthingprotocol.DeviceID]chan interface{})
|
outboxes = make(map[syncthingprotocol.DeviceID]chan interface{})
|
||||||
numConnections int64
|
numConnections atomic.Int64
|
||||||
)
|
)
|
||||||
|
|
||||||
func listener(proto, addr string, config *tls.Config) {
|
func listener(_, addr string, config *tls.Config, token string) {
|
||||||
tcpListener, err := net.Listen("tcp", addr)
|
tcpListener, err := net.Listen("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
@@ -36,8 +36,14 @@ func listener(proto, addr string, config *tls.Config) {
|
|||||||
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
|
||||||
}
|
}
|
||||||
@@ -49,7 +55,7 @@ func listener(proto, addr string, config *tls.Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isTLS {
|
if isTLS {
|
||||||
go protocolConnectionHandler(conn, config)
|
go protocolConnectionHandler(conn, config, token)
|
||||||
} else {
|
} else {
|
||||||
go sessionConnectionHandler(conn)
|
go sessionConnectionHandler(conn)
|
||||||
}
|
}
|
||||||
@@ -57,7 +63,7 @@ func listener(proto, addr string, config *tls.Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config) {
|
func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config, token string) {
|
||||||
conn := tls.Server(tcpConn, config)
|
conn := tls.Server(tcpConn, config)
|
||||||
if err := conn.SetDeadline(time.Now().Add(messageTimeout)); err != nil {
|
if err := conn.SetDeadline(time.Now().Add(messageTimeout)); err != nil {
|
||||||
if debug {
|
if debug {
|
||||||
@@ -76,7 +82,7 @@ func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state := conn.ConnectionState()
|
state := conn.ConnectionState()
|
||||||
if (!state.NegotiatedProtocolIsMutual || state.NegotiatedProtocol != protocol.ProtocolName) && debug {
|
if debug && state.NegotiatedProtocol != protocol.ProtocolName {
|
||||||
log.Println("Protocol negotiation error")
|
log.Println("Protocol negotiation error")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +125,16 @@ func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config) {
|
|||||||
|
|
||||||
switch msg := message.(type) {
|
switch msg := message.(type) {
|
||||||
case protocol.JoinRelayRequest:
|
case protocol.JoinRelayRequest:
|
||||||
if atomic.LoadInt32(&overLimit) > 0 {
|
if token != "" && msg.Token != token {
|
||||||
|
if debug {
|
||||||
|
log.Printf("invalid token %s\n", msg.Token)
|
||||||
|
}
|
||||||
|
protocol.WriteMessage(conn, protocol.ResponseWrongToken)
|
||||||
|
conn.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if overLimit.Load() {
|
||||||
protocol.WriteMessage(conn, protocol.RelayFull{})
|
protocol.WriteMessage(conn, protocol.RelayFull{})
|
||||||
if debug {
|
if debug {
|
||||||
log.Println("Refusing join request from", id, "due to being over limits")
|
log.Println("Refusing join request from", id, "due to being over limits")
|
||||||
@@ -258,7 +273,7 @@ func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config) {
|
|||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if atomic.LoadInt32(&overLimit) > 0 && !hasSessions(id) {
|
if overLimit.Load() && !hasSessions(id) {
|
||||||
if debug {
|
if debug {
|
||||||
log.Println("Dropping", id, "as it has no sessions and we are over our limits")
|
log.Println("Dropping", id, "as it has no sessions and we are over our limits")
|
||||||
}
|
}
|
||||||
@@ -351,8 +366,8 @@ func sessionConnectionHandler(conn net.Conn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func messageReader(conn net.Conn, messages chan<- interface{}, errors chan<- error) {
|
func messageReader(conn net.Conn, messages chan<- interface{}, errors chan<- error) {
|
||||||
atomic.AddInt64(&numConnections, 1)
|
numConnections.Add(1)
|
||||||
defer atomic.AddInt64(&numConnections, -1)
|
defer numConnections.Add(-1)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
msg, err := protocol.ReadMessage(conn)
|
msg, err := protocol.ReadMessage(conn)
|
||||||
|
|||||||
+41
-15
@@ -14,7 +14,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -50,13 +49,14 @@ var (
|
|||||||
|
|
||||||
sessionLimitBps int
|
sessionLimitBps int
|
||||||
globalLimitBps int
|
globalLimitBps int
|
||||||
overLimit int32
|
overLimit atomic.Bool
|
||||||
descriptorLimit int64
|
descriptorLimit int64
|
||||||
sessionLimiter *rate.Limiter
|
sessionLimiter *rate.Limiter
|
||||||
globalLimiter *rate.Limiter
|
globalLimiter *rate.Limiter
|
||||||
networkBufferSize int
|
networkBufferSize int
|
||||||
|
|
||||||
statusAddr string
|
statusAddr string
|
||||||
|
token string
|
||||||
poolAddrs string
|
poolAddrs string
|
||||||
pools []string
|
pools []string
|
||||||
providedBy string
|
providedBy string
|
||||||
@@ -90,6 +90,7 @@ func main() {
|
|||||||
flag.IntVar(&globalLimitBps, "global-rate", globalLimitBps, "Global rate limit, in bytes/s")
|
flag.IntVar(&globalLimitBps, "global-rate", globalLimitBps, "Global rate limit, in bytes/s")
|
||||||
flag.BoolVar(&debug, "debug", debug, "Enable debug output")
|
flag.BoolVar(&debug, "debug", debug, "Enable debug output")
|
||||||
flag.StringVar(&statusAddr, "status-srv", ":22070", "Listen address for status service (blank to disable)")
|
flag.StringVar(&statusAddr, "status-srv", ":22070", "Listen address for status service (blank to disable)")
|
||||||
|
flag.StringVar(&token, "token", "", "Token to restrict access to the relay (optional). Disables joining any pools.")
|
||||||
flag.StringVar(&poolAddrs, "pools", defaultPoolAddrs, "Comma separated list of relay pool addresses to join")
|
flag.StringVar(&poolAddrs, "pools", defaultPoolAddrs, "Comma separated list of relay pool addresses to join")
|
||||||
flag.StringVar(&providedBy, "provided-by", "", "An optional description about who provides the relay")
|
flag.StringVar(&providedBy, "provided-by", "", "An optional description about who provides the relay")
|
||||||
flag.StringVar(&extAddress, "ext-address", "", "An optional address to advertise as being available on.\n\tAllows listening on an unprivileged port with port forwarding from e.g. 443, and be connected to on port 443.")
|
flag.StringVar(&extAddress, "ext-address", "", "An optional address to advertise as being available on.\n\tAllows listening on an unprivileged port with port forwarding from e.g. 443, and be connected to on port 443.")
|
||||||
@@ -99,7 +100,7 @@ func main() {
|
|||||||
flag.IntVar(&natRenewal, "nat-renewal", 30, "NAT renewal frequency in minutes")
|
flag.IntVar(&natRenewal, "nat-renewal", 30, "NAT renewal frequency in minutes")
|
||||||
flag.IntVar(&natTimeout, "nat-timeout", 10, "NAT discovery timeout in seconds")
|
flag.IntVar(&natTimeout, "nat-timeout", 10, "NAT discovery timeout in seconds")
|
||||||
flag.BoolVar(&pprofEnabled, "pprof", false, "Enable the built in profiling on the status server")
|
flag.BoolVar(&pprofEnabled, "pprof", false, "Enable the built in profiling on the status server")
|
||||||
flag.IntVar(&networkBufferSize, "network-buffer", 2048, "Network buffer size (two of these per proxied connection)")
|
flag.IntVar(&networkBufferSize, "network-buffer", 65536, "Network buffer size (two of these per proxied connection)")
|
||||||
showVersion := flag.Bool("version", false, "Show version")
|
showVersion := flag.Bool("version", false, "Show version")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -146,7 +147,7 @@ func main() {
|
|||||||
log.Println("Connection limit", descriptorLimit)
|
log.Println("Connection limit", descriptorLimit)
|
||||||
|
|
||||||
go monitorLimits()
|
go monitorLimits()
|
||||||
} else if err != nil && runtime.GOOS != "windows" {
|
} else if err != nil && !build.IsWindows {
|
||||||
log.Println("Assuming no connection limit, due to error retrieving rlimits:", err)
|
log.Println("Assuming no connection limit, due to error retrieving rlimits:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,10 +187,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wrapper := config.Wrap("config", config.New(id), id, events.NoopLogger)
|
wrapper := config.Wrap("config", config.New(id), id, events.NoopLogger)
|
||||||
wrapper.SetOptions(config.OptionsConfiguration{
|
go wrapper.Serve(context.TODO())
|
||||||
NATLeaseM: natLease,
|
wrapper.Modify(func(cfg *config.Configuration) {
|
||||||
NATRenewalM: natRenewal,
|
cfg.Options.NATLeaseM = natLease
|
||||||
NATTimeoutS: natTimeout,
|
cfg.Options.NATRenewalM = natRenewal
|
||||||
|
cfg.Options.NATTimeoutS = natTimeout
|
||||||
})
|
})
|
||||||
natSvc := nat.NewService(id, wrapper)
|
natSvc := nat.NewService(id, wrapper)
|
||||||
mapping := mapping{natSvc.NewMapping(nat.TCP, addr.IP, addr.Port)}
|
mapping := mapping{natSvc.NewMapping(nat.TCP, addr.IP, addr.Port)}
|
||||||
@@ -199,7 +201,7 @@ func main() {
|
|||||||
go natSvc.Serve(ctx)
|
go natSvc.Serve(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
found := make(chan struct{})
|
found := make(chan struct{})
|
||||||
mapping.OnChanged(func(_ *nat.Mapping, _, _ []nat.Address) {
|
mapping.OnChanged(func() {
|
||||||
select {
|
select {
|
||||||
case found <- struct{}{}:
|
case found <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -229,13 +231,37 @@ func main() {
|
|||||||
go statusService(statusAddr)
|
go statusService(statusAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
uri, err := url.Parse(fmt.Sprintf("relay://%s/?id=%s&pingInterval=%s&networkTimeout=%s&sessionLimitBps=%d&globalLimitBps=%d&statusAddr=%s&providedBy=%s", mapping.Address(), id, pingInterval, networkTimeout, sessionLimitBps, globalLimitBps, statusAddr, providedBy))
|
uri, err := url.Parse(fmt.Sprintf("relay://%s/", mapping.Address()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Failed to construct URI", err)
|
log.Fatalln("Failed to construct URI", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add properly encoded query string parameters to URL.
|
||||||
|
query := make(url.Values)
|
||||||
|
query.Set("id", id.String())
|
||||||
|
query.Set("pingInterval", pingInterval.String())
|
||||||
|
query.Set("networkTimeout", networkTimeout.String())
|
||||||
|
if sessionLimitBps > 0 {
|
||||||
|
query.Set("sessionLimitBps", fmt.Sprint(sessionLimitBps))
|
||||||
|
}
|
||||||
|
if globalLimitBps > 0 {
|
||||||
|
query.Set("globalLimitBps", fmt.Sprint(globalLimitBps))
|
||||||
|
}
|
||||||
|
if statusAddr != "" {
|
||||||
|
query.Set("statusAddr", statusAddr)
|
||||||
|
}
|
||||||
|
if providedBy != "" {
|
||||||
|
query.Set("providedBy", providedBy)
|
||||||
|
}
|
||||||
|
uri.RawQuery = query.Encode()
|
||||||
|
|
||||||
log.Println("URI:", uri.String())
|
log.Println("URI:", uri.String())
|
||||||
|
|
||||||
|
if token != "" {
|
||||||
|
poolAddrs = ""
|
||||||
|
}
|
||||||
|
|
||||||
if poolAddrs == defaultPoolAddrs {
|
if poolAddrs == defaultPoolAddrs {
|
||||||
log.Println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
|
log.Println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
|
||||||
log.Println("!! Joining default relay pools, this relay will be available for public use. !!")
|
log.Println("!! Joining default relay pools, this relay will be available for public use. !!")
|
||||||
@@ -247,11 +273,11 @@ func main() {
|
|||||||
for _, pool := range pools {
|
for _, pool := range pools {
|
||||||
pool = strings.TrimSpace(pool)
|
pool = strings.TrimSpace(pool)
|
||||||
if len(pool) > 0 {
|
if len(pool) > 0 {
|
||||||
go poolHandler(pool, uri, mapping)
|
go poolHandler(pool, uri, mapping, cert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go listener(proto, listen, tlsCfg)
|
go listener(proto, listen, tlsCfg, token)
|
||||||
|
|
||||||
sigs := make(chan os.Signal, 1)
|
sigs := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
@@ -282,10 +308,10 @@ func main() {
|
|||||||
func monitorLimits() {
|
func monitorLimits() {
|
||||||
limitCheckTimer = time.NewTimer(time.Minute)
|
limitCheckTimer = time.NewTimer(time.Minute)
|
||||||
for range limitCheckTimer.C {
|
for range limitCheckTimer.C {
|
||||||
if atomic.LoadInt64(&numConnections)+atomic.LoadInt64(&numProxies) > descriptorLimit {
|
if numConnections.Load()+numProxies.Load() > descriptorLimit {
|
||||||
atomic.StoreInt32(&overLimit, 1)
|
overLimit.Store(true)
|
||||||
log.Println("Gone past our connection limits. Starting to refuse new/drop idle connections.")
|
log.Println("Gone past our connection limits. Starting to refuse new/drop idle connections.")
|
||||||
} else if atomic.CompareAndSwapInt32(&overLimit, 1, 0) {
|
} else if overLimit.CompareAndSwap(true, false) {
|
||||||
log.Println("Dropped below our connection limits. Accepting new connections.")
|
log.Println("Dropped below our connection limits. Accepting new connections.")
|
||||||
}
|
}
|
||||||
limitCheckTimer.Reset(time.Minute)
|
limitCheckTimer.Reset(time.Minute)
|
||||||
|
|||||||
+23
-6
@@ -4,8 +4,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -16,7 +17,7 @@ const (
|
|||||||
httpStatusEnhanceYourCalm = 429
|
httpStatusEnhanceYourCalm = 429
|
||||||
)
|
)
|
||||||
|
|
||||||
func poolHandler(pool string, uri *url.URL, mapping mapping) {
|
func poolHandler(pool string, uri *url.URL, mapping mapping, ownCert tls.Certificate) {
|
||||||
if debug {
|
if debug {
|
||||||
log.Println("Joining", pool)
|
log.Println("Joining", pool)
|
||||||
}
|
}
|
||||||
@@ -31,14 +32,31 @@ func poolHandler(pool string, uri *url.URL, mapping mapping) {
|
|||||||
uriCopy.String(),
|
uriCopy.String(),
|
||||||
})
|
})
|
||||||
|
|
||||||
resp, err := httpClient.Post(pool, "application/json", &b)
|
poolUrl, err := url.Parse(pool)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not parse pool url '%s': %v", pool, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := http.DefaultClient
|
||||||
|
if poolUrl.Scheme == "https" {
|
||||||
|
// Sent our certificate in join request
|
||||||
|
client = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{ownCert},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Post(pool, "application/json", &b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error joining pool %v: HTTP request: %v", pool, err)
|
log.Printf("Error joining pool %v: HTTP request: %v", pool, err)
|
||||||
time.Sleep(time.Minute)
|
time.Sleep(time.Minute)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
bs, err := ioutil.ReadAll(resp.Body)
|
bs, err := io.ReadAll(resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error joining pool %v: reading response: %v", pool, err)
|
log.Printf("Error joining pool %v: reading response: %v", pool, err)
|
||||||
@@ -56,9 +74,8 @@ func poolHandler(pool string, uri *url.URL, mapping mapping) {
|
|||||||
log.Printf("Joined pool %s, rejoining in %v", pool, rejoin)
|
log.Printf("Joined pool %s, rejoining in %v", pool, rejoin)
|
||||||
time.Sleep(rejoin)
|
time.Sleep(rejoin)
|
||||||
continue
|
continue
|
||||||
} else {
|
|
||||||
log.Printf("Joined pool %s, failed to deserialize response: %v", pool, err)
|
|
||||||
}
|
}
|
||||||
|
log.Printf("Joined pool %s, failed to deserialize response: %v", pool, err)
|
||||||
|
|
||||||
case http.StatusInternalServerError:
|
case http.StatusInternalServerError:
|
||||||
log.Printf("Failed to join %v: server error", pool)
|
log.Printf("Failed to join %v: server error", pool)
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ var (
|
|||||||
sessionMut = sync.RWMutex{}
|
sessionMut = sync.RWMutex{}
|
||||||
activeSessions = make([]*session, 0)
|
activeSessions = make([]*session, 0)
|
||||||
pendingSessions = make(map[string]*session)
|
pendingSessions = make(map[string]*session)
|
||||||
numProxies int64
|
numProxies atomic.Int64
|
||||||
bytesProxied int64
|
bytesProxied atomic.Int64
|
||||||
)
|
)
|
||||||
|
|
||||||
func newSession(serverid, clientid syncthingprotocol.DeviceID, sessionRateLimit, globalRateLimit *rate.Limiter) *session {
|
func newSession(serverid, clientid syncthingprotocol.DeviceID, sessionRateLimit, globalRateLimit *rate.Limiter) *session {
|
||||||
@@ -251,8 +251,8 @@ func (s *session) proxy(c1, c2 net.Conn) error {
|
|||||||
log.Println("Proxy", c1.RemoteAddr(), "->", c2.RemoteAddr())
|
log.Println("Proxy", c1.RemoteAddr(), "->", c2.RemoteAddr())
|
||||||
}
|
}
|
||||||
|
|
||||||
atomic.AddInt64(&numProxies, 1)
|
numProxies.Add(1)
|
||||||
defer atomic.AddInt64(&numProxies, -1)
|
defer numProxies.Add(-1)
|
||||||
|
|
||||||
buf := make([]byte, networkBufferSize)
|
buf := make([]byte, networkBufferSize)
|
||||||
for {
|
for {
|
||||||
@@ -262,7 +262,7 @@ func (s *session) proxy(c1, c2 net.Conn) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
atomic.AddInt64(&bytesProxied, int64(n))
|
bytesProxied.Add(int64(n))
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
log.Printf("%d bytes from %s to %s", n, c1.RemoteAddr(), c2.RemoteAddr())
|
log.Printf("%d bytes from %s to %s", n, c1.RemoteAddr(), c2.RemoteAddr())
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func statusService(addr string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStatus(w http.ResponseWriter, r *http.Request) {
|
func getStatus(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
status := make(map[string]interface{})
|
status := make(map[string]interface{})
|
||||||
|
|
||||||
@@ -51,9 +51,9 @@ func getStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
status["numPendingSessionKeys"] = len(pendingSessions)
|
status["numPendingSessionKeys"] = len(pendingSessions)
|
||||||
status["numActiveSessions"] = len(activeSessions)
|
status["numActiveSessions"] = len(activeSessions)
|
||||||
sessionMut.Unlock()
|
sessionMut.Unlock()
|
||||||
status["numConnections"] = atomic.LoadInt64(&numConnections)
|
status["numConnections"] = numConnections.Load()
|
||||||
status["numProxies"] = atomic.LoadInt64(&numProxies)
|
status["numProxies"] = numProxies.Load()
|
||||||
status["bytesProxied"] = atomic.LoadInt64(&bytesProxied)
|
status["bytesProxied"] = bytesProxied.Load()
|
||||||
status["goVersion"] = runtime.Version()
|
status["goVersion"] = runtime.Version()
|
||||||
status["goOS"] = runtime.GOOS
|
status["goOS"] = runtime.GOOS
|
||||||
status["goArch"] = runtime.GOARCH
|
status["goArch"] = runtime.GOARCH
|
||||||
@@ -88,13 +88,13 @@ func getStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type rateCalculator struct {
|
type rateCalculator struct {
|
||||||
counter *int64 // atomic, must remain 64-bit aligned
|
counter *atomic.Int64
|
||||||
rates []int64
|
rates []int64
|
||||||
prev int64
|
prev int64
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRateCalculator(keepIntervals int, interval time.Duration, counter *int64) *rateCalculator {
|
func newRateCalculator(keepIntervals int, interval time.Duration, counter *atomic.Int64) *rateCalculator {
|
||||||
r := &rateCalculator{
|
r := &rateCalculator{
|
||||||
rates: make([]int64, keepIntervals),
|
rates: make([]int64, keepIntervals),
|
||||||
counter: counter,
|
counter: counter,
|
||||||
@@ -112,7 +112,7 @@ func (r *rateCalculator) updateRates(interval time.Duration) {
|
|||||||
next := now.Truncate(interval).Add(interval)
|
next := now.Truncate(interval).Add(interval)
|
||||||
time.Sleep(next.Sub(now))
|
time.Sleep(next.Sub(now))
|
||||||
|
|
||||||
cur := atomic.LoadInt64(r.counter)
|
cur := r.counter.Load()
|
||||||
rate := int64(float64(cur-r.prev) / interval.Seconds())
|
rate := int64(float64(cur-r.prev) / interval.Seconds())
|
||||||
copy(r.rates[1:], r.rates)
|
copy(r.rates[1:], r.rates)
|
||||||
r.rates[0] = rate
|
r.rates[0] = rate
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func main() {
|
|||||||
|
|
||||||
if join {
|
if join {
|
||||||
log.Println("Creating client")
|
log.Println("Creating client")
|
||||||
relay, err := client.NewClient(uri, []tls.Certificate{cert}, nil, 10*time.Second)
|
relay, err := client.NewClient(uri, []tls.Certificate{cert}, 10*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -127,10 +127,6 @@ func stdinReader(c chan<- string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func connectToStdio(stdin <-chan string, conn net.Conn) {
|
func connectToStdio(stdin <-chan string, conn net.Conn) {
|
||||||
go func() {
|
|
||||||
|
|
||||||
}()
|
|
||||||
|
|
||||||
buf := make([]byte, 1024)
|
buf := make([]byte, 1024)
|
||||||
for {
|
for {
|
||||||
conn.SetReadDeadline(time.Now().Add(time.Millisecond))
|
conn.SetReadDeadline(time.Now().Add(time.Millisecond))
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -69,7 +68,7 @@ func gen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sign(keyname, dataname string) {
|
func sign(keyname, dataname string) {
|
||||||
privkey, err := ioutil.ReadFile(keyname)
|
privkey, err := os.ReadFile(keyname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -95,7 +94,7 @@ func sign(keyname, dataname string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func verifyWithFile(signame, dataname, keyname string) {
|
func verifyWithFile(signame, dataname, keyname string) {
|
||||||
pubkey, err := ioutil.ReadFile(keyname)
|
pubkey, err := os.ReadFile(keyname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -103,7 +102,7 @@ func verifyWithFile(signame, dataname, keyname string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func verifyWithKey(signame, dataname string, pubkey []byte) {
|
func verifyWithKey(signame, dataname string, pubkey []byte) {
|
||||||
sig, err := ioutil.ReadFile(signame)
|
sig, err := os.ReadFile(signame)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+101
-10
@@ -7,31 +7,122 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
"github.com/syncthing/syncthing/lib/httpcache"
|
||||||
"github.com/syncthing/syncthing/lib/upgrade"
|
"github.com/syncthing/syncthing/lib/upgrade"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultURL = "https://api.github.com/repos/syncthing/syncthing/releases?per_page=25"
|
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() {
|
func main() {
|
||||||
url := flag.String("u", defaultURL, "GitHub releases url")
|
var params cli
|
||||||
flag.Parse()
|
kong.Parse(¶ms)
|
||||||
|
if err := server(¶ms); err != nil {
|
||||||
rels := upgrade.FetchLatestReleases(*url, "")
|
fmt.Printf("Error: %v\n", err)
|
||||||
if rels == nil {
|
|
||||||
// An error was already logged
|
|
||||||
os.Exit(1)
|
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, _ *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))
|
sort.Sort(upgrade.SortByRelease(rels))
|
||||||
rels = filterForLatest(rels)
|
rels = filterForLatest(rels)
|
||||||
|
|
||||||
if err := json.NewEncoder(os.Stdout).Encode(rels); err != nil {
|
// Move the URL used for browser downloads to the URL field, and remove
|
||||||
os.Exit(1)
|
// 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 = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,14 +37,14 @@ type result struct {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
prefix := strings.ToUpper(strings.Replace(flag.Arg(0), "-", "", -1))
|
prefix := strings.ToUpper(strings.ReplaceAll(flag.Arg(0), "-", ""))
|
||||||
if len(prefix) > 7 {
|
if len(prefix) > 7 {
|
||||||
prefix = prefix[:7] + "-" + prefix[7:]
|
prefix = prefix[:7] + "-" + prefix[7:]
|
||||||
}
|
}
|
||||||
|
|
||||||
found := make(chan result)
|
found := make(chan result)
|
||||||
stop := make(chan struct{})
|
stop := make(chan struct{})
|
||||||
var count int64
|
var count atomic.Int64
|
||||||
|
|
||||||
// Print periodic progress reports.
|
// Print periodic progress reports.
|
||||||
go printProgress(prefix, &count)
|
go printProgress(prefix, &count)
|
||||||
@@ -72,7 +72,7 @@ func main() {
|
|||||||
// Try certificates until one is found that has the prefix at the start of
|
// Try certificates until one is found that has the prefix at the start of
|
||||||
// the resulting device ID. Increments count atomically, sends the result to
|
// the resulting device ID. Increments count atomically, sends the result to
|
||||||
// found, returns when stop is closed.
|
// found, returns when stop is closed.
|
||||||
func generatePrefixed(prefix string, count *int64, found chan<- result, stop <-chan struct{}) {
|
func generatePrefixed(prefix string, count *atomic.Int64, found chan<- result, stop <-chan struct{}) {
|
||||||
notBefore := time.Now()
|
notBefore := time.Now()
|
||||||
notAfter := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
|
notAfter := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ func generatePrefixed(prefix string, count *int64, found chan<- result, stop <-c
|
|||||||
}
|
}
|
||||||
|
|
||||||
id := protocol.NewDeviceID(derBytes)
|
id := protocol.NewDeviceID(derBytes)
|
||||||
atomic.AddInt64(count, 1)
|
count.Add(1)
|
||||||
|
|
||||||
if strings.HasPrefix(id.String(), prefix) {
|
if strings.HasPrefix(id.String(), prefix) {
|
||||||
select {
|
select {
|
||||||
@@ -121,7 +121,7 @@ func generatePrefixed(prefix string, count *int64, found chan<- result, stop <-c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printProgress(prefix string, count *int64) {
|
func printProgress(prefix string, count *atomic.Int64) {
|
||||||
started := time.Now()
|
started := time.Now()
|
||||||
wantBits := 5 * len(prefix)
|
wantBits := 5 * len(prefix)
|
||||||
if wantBits > 63 {
|
if wantBits > 63 {
|
||||||
@@ -132,7 +132,7 @@ func printProgress(prefix string, count *int64) {
|
|||||||
fmt.Printf("Want %d bits for prefix %q, about %.2g certs to test (statistically speaking)\n", wantBits, prefix, expectedIterations)
|
fmt.Printf("Want %d bits for prefix %q, about %.2g certs to test (statistically speaking)\n", wantBits, prefix, expectedIterations)
|
||||||
|
|
||||||
for range time.NewTicker(15 * time.Second).C {
|
for range time.NewTicker(15 * time.Second).C {
|
||||||
tried := atomic.LoadInt64(count)
|
tried := count.Load()
|
||||||
elapsed := time.Since(started)
|
elapsed := time.Since(started)
|
||||||
rate := float64(tried) / elapsed.Seconds()
|
rate := float64(tried) / elapsed.Seconds()
|
||||||
expected := timeStr(expectedIterations / rate)
|
expected := timeStr(expectedIterations / rate)
|
||||||
|
|||||||
+22
-23
@@ -7,31 +7,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/md5"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/sha256"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getmd5(filePath string) ([]byte, error) {
|
|
||||||
var result []byte
|
|
||||||
file, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
hash := md5.New()
|
|
||||||
if _, err := io.Copy(hash, file); err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return hash.Sum(result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
period := flag.Duration("period", 200*time.Millisecond, "Sleep period between checks")
|
period := flag.Duration("period", 200*time.Millisecond, "Sleep period between checks")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@@ -46,7 +30,7 @@ func main() {
|
|||||||
exists := true
|
exists := true
|
||||||
size := int64(0)
|
size := int64(0)
|
||||||
mtime := time.Time{}
|
mtime := time.Time{}
|
||||||
hash := []byte{}
|
var hash [sha256.Size]byte
|
||||||
|
|
||||||
for {
|
for {
|
||||||
time.Sleep(*period)
|
time.Sleep(*period)
|
||||||
@@ -72,7 +56,7 @@ func main() {
|
|||||||
if !exists {
|
if !exists {
|
||||||
size = 0
|
size = 0
|
||||||
mtime = time.Time{}
|
mtime = time.Time{}
|
||||||
hash = []byte{}
|
hash = [sha256.Size]byte{}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,12 +67,12 @@ func main() {
|
|||||||
newSize := fi.Size()
|
newSize := fi.Size()
|
||||||
newMtime := fi.ModTime()
|
newMtime := fi.ModTime()
|
||||||
|
|
||||||
newHash, err := getmd5(file)
|
newHash, err := sha256file(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("getmd5:", err)
|
fmt.Println("sha256file:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if newSize != size || newMtime != mtime || !bytes.Equal(newHash, hash) {
|
if newSize != size || newMtime != mtime || newHash != hash {
|
||||||
fmt.Println(file, "Size:", newSize, "Mtime:", newMtime, "Hash:", fmt.Sprintf("%x", newHash))
|
fmt.Println(file, "Size:", newSize, "Mtime:", newMtime, "Hash:", fmt.Sprintf("%x", newHash))
|
||||||
hash = newHash
|
hash = newHash
|
||||||
size = newSize
|
size = newSize
|
||||||
@@ -96,3 +80,18 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sha256file(fname string) (hash [sha256.Size]byte, err error) {
|
||||||
|
f, err := os.Open(fname)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
io.Copy(h, f)
|
||||||
|
hb := h.Sum(nil)
|
||||||
|
copy(hash[:], hb)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
+10
-12
@@ -15,19 +15,17 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func startBlockProfiler() {
|
||||||
if innerProcess && os.Getenv("STBLOCKPROFILE") != "" {
|
profiler := pprof.Lookup("block")
|
||||||
profiler := pprof.Lookup("block")
|
if profiler == nil {
|
||||||
if profiler == nil {
|
panic("Couldn't find block profiler")
|
||||||
panic("Couldn't find block profiler")
|
|
||||||
}
|
|
||||||
l.Debugln("Starting block profiling")
|
|
||||||
go func() {
|
|
||||||
err := saveBlockingProfiles(profiler) // Only returns on error
|
|
||||||
l.Warnln("Block profiler failed:", err)
|
|
||||||
panic("Block profiler failed")
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
l.Debugln("Starting block profiling")
|
||||||
|
go func() {
|
||||||
|
err := saveBlockingProfiles(profiler) // Only returns on error
|
||||||
|
l.Warnln("Block profiler failed:", err)
|
||||||
|
panic("Block profiler failed")
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveBlockingProfiles(profiler *pprof.Profile) error {
|
func saveBlockingProfiles(profiler *pprof.Profile) error {
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
// 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 cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
|
"github.com/syncthing/syncthing/lib/locations"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIClient interface {
|
||||||
|
Get(url string) (*http.Response, error)
|
||||||
|
Post(url, body string) (*http.Response, error)
|
||||||
|
PutJSON(url string, o interface{}) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiClient struct {
|
||||||
|
http.Client
|
||||||
|
cfg config.GUIConfiguration
|
||||||
|
apikey string
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiClientFactory struct {
|
||||||
|
cfg config.GUIConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *apiClientFactory) getClient() (APIClient, error) {
|
||||||
|
// Now if the API key and address is not provided (we are not connecting to a remote instance),
|
||||||
|
// try to rip it out of the config.
|
||||||
|
if f.cfg.RawAddress == "" && f.cfg.APIKey == "" {
|
||||||
|
var err error
|
||||||
|
f.cfg, err = loadGUIConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if f.cfg.Address() == "" || f.cfg.APIKey == "" {
|
||||||
|
return nil, errors.New("Both --gui-address and --gui-apikey should be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient := http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||||
|
return net.Dial(f.cfg.Network(), f.cfg.Address())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return &apiClient{
|
||||||
|
Client: httpClient,
|
||||||
|
cfg: f.cfg,
|
||||||
|
apikey: f.cfg.APIKey,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadGUIConfig() (config.GUIConfiguration, error) {
|
||||||
|
// Load the certs and get the ID
|
||||||
|
cert, err := tls.LoadX509KeyPair(
|
||||||
|
locations.Get(locations.CertFile),
|
||||||
|
locations.Get(locations.KeyFile),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return config.GUIConfiguration{}, fmt.Errorf("reading device ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
myID := protocol.NewDeviceID(cert.Certificate[0])
|
||||||
|
|
||||||
|
// Load the config
|
||||||
|
cfg, _, err := config.Load(locations.Get(locations.ConfigFile), myID, events.NoopLogger)
|
||||||
|
if err != nil {
|
||||||
|
return config.GUIConfiguration{}, fmt.Errorf("loading config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
guiCfg := cfg.GUI()
|
||||||
|
|
||||||
|
if guiCfg.Address() == "" {
|
||||||
|
return config.GUIConfiguration{}, errors.New("Could not find GUI Address")
|
||||||
|
}
|
||||||
|
|
||||||
|
if guiCfg.APIKey == "" {
|
||||||
|
return config.GUIConfiguration{}, errors.New("Could not find GUI API key")
|
||||||
|
}
|
||||||
|
|
||||||
|
return guiCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *apiClient) Endpoint() string {
|
||||||
|
if c.cfg.Network() == "unix" {
|
||||||
|
return "http://unix/"
|
||||||
|
}
|
||||||
|
url := c.cfg.URL()
|
||||||
|
if !strings.HasSuffix(url, "/") {
|
||||||
|
url += "/"
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *apiClient) Do(req *http.Request) (*http.Response, error) {
|
||||||
|
req.Header.Set("X-API-Key", c.apikey)
|
||||||
|
resp, err := c.Client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, checkResponse(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *apiClient) Request(url, method string, r io.Reader) (*http.Response, error) {
|
||||||
|
request, err := http.NewRequest(method, c.Endpoint()+"rest/"+url, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.Do(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *apiClient) RequestString(url, method, data string) (*http.Response, error) {
|
||||||
|
return c.Request(url, method, bytes.NewBufferString(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *apiClient) RequestJSON(url, method string, o interface{}) (*http.Response, error) {
|
||||||
|
data, err := json.Marshal(o)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.Request(url, method, bytes.NewBuffer(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *apiClient) Get(url string) (*http.Response, error) {
|
||||||
|
return c.RequestString(url, "GET", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *apiClient) Post(url, body string) (*http.Response, error) {
|
||||||
|
return c.RequestString(url, "POST", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *apiClient) PutJSON(url string, o interface{}) (*http.Response, error) {
|
||||||
|
return c.RequestJSON(url, "PUT", o)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNotFound = errors.New("invalid endpoint or API call")
|
||||||
|
|
||||||
|
func checkResponse(response *http.Response) error {
|
||||||
|
if response.StatusCode == http.StatusNotFound {
|
||||||
|
return errNotFound
|
||||||
|
} else if response.StatusCode == http.StatusUnauthorized {
|
||||||
|
return errors.New("invalid API key")
|
||||||
|
} else if response.StatusCode != http.StatusOK {
|
||||||
|
data, err := responseToBArray(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body := strings.TrimSpace(string(data))
|
||||||
|
return fmt.Errorf("unexpected HTTP status returned: %s\n%s", response.Status, body)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (C) 2021 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 cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/AudriusButkevicius/recli"
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
type configHandler struct {
|
||||||
|
original, cfg config.Configuration
|
||||||
|
client APIClient
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfigCommand(f *apiClientFactory) (cli.Command, error) {
|
||||||
|
h := new(configHandler)
|
||||||
|
h.client, h.err = f.getClient()
|
||||||
|
if h.err == nil {
|
||||||
|
h.cfg, h.err = getConfig(h.client)
|
||||||
|
}
|
||||||
|
h.original = h.cfg.Copy()
|
||||||
|
|
||||||
|
// Copy the config and set the default flags
|
||||||
|
recliCfg := recli.DefaultConfig
|
||||||
|
recliCfg.IDTag.Name = "xml"
|
||||||
|
recliCfg.SkipTag.Name = "json"
|
||||||
|
|
||||||
|
commands, err := recli.New(recliCfg).Construct(&h.cfg)
|
||||||
|
if err != nil {
|
||||||
|
return cli.Command{}, fmt.Errorf("config reflect: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cli.Command{
|
||||||
|
Name: "config",
|
||||||
|
HideHelp: true,
|
||||||
|
Usage: "Configuration modification command group",
|
||||||
|
Subcommands: commands,
|
||||||
|
Before: h.configBefore,
|
||||||
|
After: h.configAfter,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *configHandler) configBefore(c *cli.Context) error {
|
||||||
|
for _, arg := range c.Args() {
|
||||||
|
if arg == "--help" || arg == "-h" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *configHandler) configAfter(_ *cli.Context) error {
|
||||||
|
if h.err != nil {
|
||||||
|
// Error was already returned in configBefore
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if reflect.DeepEqual(h.cfg, h.original) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
body, err := json.MarshalIndent(h.cfg, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := h.client.Post("system/config", string(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
body, err := responseToBArray(resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return errors.New(string(body))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (C) 2021 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 cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var debugCommand = cli.Command{
|
||||||
|
Name: "debug",
|
||||||
|
HideHelp: true,
|
||||||
|
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 {
|
||||||
|
return func(c *cli.Context) error {
|
||||||
|
query := make(url.Values)
|
||||||
|
query.Set("folder", c.Args()[0])
|
||||||
|
query.Set("file", normalizePath(c.Args()[1]))
|
||||||
|
return indexDumpOutput("debug/file?" + query.Encode())(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func profile() cli.ActionFunc {
|
||||||
|
return func(c *cli.Context) error {
|
||||||
|
switch t := c.Args()[0]; t {
|
||||||
|
case "cpu", "heap":
|
||||||
|
return saveToFile(fmt.Sprintf("debug/%vprof", c.Args()[0]))(c)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("expected cpu or heap as argument, got %v", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -22,12 +22,12 @@ var errorsCommand = cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "show",
|
Name: "show",
|
||||||
Usage: "Show pending errors",
|
Usage: "Show pending errors",
|
||||||
Action: expects(0, dumpOutput("system/error")),
|
Action: expects(0, indexDumpOutput("system/error")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "push",
|
Name: "push",
|
||||||
Usage: "Push an error to active clients",
|
Usage: "Push an error to active clients",
|
||||||
ArgsUsage: "[error message]",
|
ArgsUsage: "ERROR-MESSAGE",
|
||||||
Action: expects(1, errorsPush),
|
Action: expects(1, errorsPush),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -39,7 +39,7 @@ var errorsCommand = cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func errorsPush(c *cli.Context) error {
|
func errorsPush(c *cli.Context) error {
|
||||||
client := c.App.Metadata["client"].(*APIClient)
|
client := c.App.Metadata["client"].(APIClient)
|
||||||
errStr := strings.Join(c.Args(), " ")
|
errStr := strings.Join(c.Args(), " ")
|
||||||
response, err := client.Post("system/error", strings.TrimSpace(errStr))
|
response, err := client.Post("system/error", strings.TrimSpace(errStr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright (C) 2014 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 cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var indexCommand = cli.Command{
|
||||||
|
Name: "index",
|
||||||
|
Usage: "Show information about the index (database)",
|
||||||
|
Subcommands: []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "dump",
|
||||||
|
Usage: "Print the entire db",
|
||||||
|
Action: expects(0, indexDump),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "dump-size",
|
||||||
|
Usage: "Print the db size of different categories of information",
|
||||||
|
Action: expects(0, indexDumpSize),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "check",
|
||||||
|
Usage: "Check the database for inconsistencies",
|
||||||
|
Action: expects(0, indexCheck),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "account",
|
||||||
|
Usage: "Print key and value size statistics per key type",
|
||||||
|
Action: expects(0, indexAccount),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -4,22 +4,26 @@
|
|||||||
// 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 cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/db/backend"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
// account prints key and data size statistics per class
|
// indexAccount prints key and data size statistics per class
|
||||||
func account(ldb backend.Backend) {
|
func indexAccount(*cli.Context) error {
|
||||||
|
ldb, err := getDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
it, err := ldb.NewPrefixIterator(nil)
|
it, err := ldb.NewPrefixIterator(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var ksizes [256]int
|
var ksizes [256]int
|
||||||
@@ -55,4 +59,6 @@ func account(ldb backend.Backend) {
|
|||||||
}
|
}
|
||||||
fmt.Fprintf(tw, "Total\t%d items,\t%d KB keys +\t%d KB data.\t\n", toti, totks/1000, totds/1000)
|
fmt.Fprintf(tw, "Total\t%d items,\t%d KB keys +\t%d KB data.\t\n", toti, totks/1000, totds/1000)
|
||||||
tw.Flush()
|
tw.Flush()
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
@@ -4,23 +4,27 @@
|
|||||||
// 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 cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/db"
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
"github.com/syncthing/syncthing/lib/db/backend"
|
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
func dump(ldb backend.Backend) {
|
func indexDump(*cli.Context) error {
|
||||||
|
ldb, err := getDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
it, err := ldb.NewPrefixIterator(nil)
|
it, err := ldb.NewPrefixIterator(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
for it.Next() {
|
for it.Next() {
|
||||||
key := it.Key()
|
key := it.Key()
|
||||||
@@ -34,7 +38,7 @@ func dump(ldb backend.Backend) {
|
|||||||
var f protocol.FileInfo
|
var f protocol.FileInfo
|
||||||
err := f.Unmarshal(it.Value())
|
err := f.Unmarshal(it.Value())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf(" V:%v\n", f)
|
fmt.Printf(" V:%v\n", f)
|
||||||
|
|
||||||
@@ -61,10 +65,10 @@ func dump(ldb backend.Backend) {
|
|||||||
folder := binary.BigEndian.Uint32(key[1:])
|
folder := binary.BigEndian.Uint32(key[1:])
|
||||||
name := nulString(key[1+4:])
|
name := nulString(key[1+4:])
|
||||||
val := it.Value()
|
val := it.Value()
|
||||||
var real, virt time.Time
|
var realTime, virtualTime time.Time
|
||||||
real.UnmarshalBinary(val[:len(val)/2])
|
realTime.UnmarshalBinary(val[:len(val)/2])
|
||||||
virt.UnmarshalBinary(val[len(val)/2:])
|
virtualTime.UnmarshalBinary(val[len(val)/2:])
|
||||||
fmt.Printf("[mtime] F:%d N:%q R:%v V:%v\n", folder, name, real, virt)
|
fmt.Printf("[mtime] F:%d N:%q R:%v V:%v\n", folder, name, realTime, virtualTime)
|
||||||
|
|
||||||
case db.KeyTypeFolderIdx:
|
case db.KeyTypeFolderIdx:
|
||||||
key := binary.BigEndian.Uint32(key[1:])
|
key := binary.BigEndian.Uint32(key[1:])
|
||||||
@@ -131,8 +135,26 @@ func dump(ldb backend.Backend) {
|
|||||||
fmt.Printf(" V:%v\n", v)
|
fmt.Printf(" V:%v\n", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case db.KeyTypePendingFolder:
|
||||||
|
device := binary.BigEndian.Uint32(key[1:])
|
||||||
|
folder := string(key[5:])
|
||||||
|
var of db.ObservedFolder
|
||||||
|
of.Unmarshal(it.Value())
|
||||||
|
fmt.Printf("[pendingFolder] D:%d F:%s V:%v\n", device, folder, of)
|
||||||
|
|
||||||
|
case db.KeyTypePendingDevice:
|
||||||
|
device := "<invalid>"
|
||||||
|
dev, err := protocol.DeviceIDFromBytes(key[1:])
|
||||||
|
if err == nil {
|
||||||
|
device = dev.String()
|
||||||
|
}
|
||||||
|
var od db.ObservedDevice
|
||||||
|
od.Unmarshal(it.Value())
|
||||||
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
@@ -4,51 +4,38 @@
|
|||||||
// 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 cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/heap"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/db"
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
"github.com/syncthing/syncthing/lib/db/backend"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SizedElement struct {
|
func indexDumpSize(*cli.Context) error {
|
||||||
key string
|
type sizedElement struct {
|
||||||
size int
|
key string
|
||||||
}
|
size int
|
||||||
|
}
|
||||||
|
|
||||||
type ElementHeap []SizedElement
|
ldb, err := getDB()
|
||||||
|
if err != nil {
|
||||||
func (h ElementHeap) Len() int { return len(h) }
|
return err
|
||||||
func (h ElementHeap) Less(i, j int) bool { return h[i].size > h[j].size }
|
}
|
||||||
func (h ElementHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
|
||||||
|
|
||||||
func (h *ElementHeap) Push(x interface{}) {
|
|
||||||
*h = append(*h, x.(SizedElement))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ElementHeap) Pop() interface{} {
|
|
||||||
old := *h
|
|
||||||
n := len(old)
|
|
||||||
x := old[n-1]
|
|
||||||
*h = old[0 : n-1]
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|
||||||
func dumpsize(ldb backend.Backend) {
|
|
||||||
h := &ElementHeap{}
|
|
||||||
heap.Init(h)
|
|
||||||
|
|
||||||
it, err := ldb.NewPrefixIterator(nil)
|
it, err := ldb.NewPrefixIterator(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
var ele SizedElement
|
|
||||||
|
var elems []sizedElement
|
||||||
for it.Next() {
|
for it.Next() {
|
||||||
|
var ele sizedElement
|
||||||
|
|
||||||
key := it.Key()
|
key := it.Key()
|
||||||
switch key[0] {
|
switch key[0] {
|
||||||
case db.KeyTypeDevice:
|
case db.KeyTypeDevice:
|
||||||
@@ -89,11 +76,15 @@ func dumpsize(ldb backend.Backend) {
|
|||||||
ele.key = fmt.Sprintf("UNKNOWN:%x", key)
|
ele.key = fmt.Sprintf("UNKNOWN:%x", key)
|
||||||
}
|
}
|
||||||
ele.size = len(it.Value())
|
ele.size = len(it.Value())
|
||||||
heap.Push(h, ele)
|
elems = append(elems, ele)
|
||||||
}
|
}
|
||||||
|
|
||||||
for h.Len() > 0 {
|
sort.Slice(elems, func(i, j int) bool {
|
||||||
ele = heap.Pop(h).(SizedElement)
|
return elems[i].size > elems[j].size
|
||||||
|
})
|
||||||
|
for _, ele := range elems {
|
||||||
fmt.Println(ele.key, ele.size)
|
fmt.Println(ele.key, ele.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
@@ -4,17 +4,18 @@
|
|||||||
// 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 cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/db"
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
"github.com/syncthing/syncthing/lib/db/backend"
|
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,7 +35,12 @@ type sequenceKey struct {
|
|||||||
sequence uint64
|
sequence uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
func idxck(ldb backend.Backend) (success bool) {
|
func indexCheck(*cli.Context) (err error) {
|
||||||
|
ldb, err := getDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -47,11 +53,20 @@ func idxck(ldb backend.Backend) (success bool) {
|
|||||||
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
|
||||||
success = true
|
success := true
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
if success {
|
||||||
|
fmt.Println("Index check completed successfully.")
|
||||||
|
} else {
|
||||||
|
err = errors.New("Inconsistencies found in the index")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
it, err := ldb.NewPrefixIterator(nil)
|
it, err := ldb.NewPrefixIterator(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
for it.Next() {
|
for it.Next() {
|
||||||
key := it.Key()
|
key := it.Key()
|
||||||
@@ -329,12 +344,12 @@ func idxck(ldb backend.Backend) (success bool) {
|
|||||||
fmt.Printf("%d version entries out of %d needs GC\n", d, len(versions))
|
fmt.Printf("%d version entries out of %d needs GC\n", d, len(versions))
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func needsLocally(vl db.VersionList) bool {
|
func needsLocally(vl db.VersionList) bool {
|
||||||
gfv, gok := vl.GetGlobal()
|
gfv, gok := vl.GetGlobal()
|
||||||
if !gok { // That's weird, but we hardly need something non-existant
|
if !gok { // That's weird, but we hardly need something non-existent
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
fv, ok := vl.Get(protocol.LocalDeviceID[:])
|
fv, ok := vl.Get(protocol.LocalDeviceID[:])
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
// 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 cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
"github.com/flynn-archive/go-shlex"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/cmd/syncthing/cmdutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type preCli struct {
|
||||||
|
GUIAddress string `name:"gui-address"`
|
||||||
|
GUIAPIKey string `name:"gui-apikey"`
|
||||||
|
HomeDir string `name:"home"`
|
||||||
|
ConfDir string `name:"config"`
|
||||||
|
DataDir string `name:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run() error {
|
||||||
|
// This is somewhat a hack around a chicken and egg problem. We need to set
|
||||||
|
// 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 {
|
||||||
|
c := preCli{}
|
||||||
|
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 {
|
||||||
|
return fmt.Errorf("Command line options: %w", err)
|
||||||
|
}
|
||||||
|
clientFactory := &apiClientFactory{
|
||||||
|
cfg: config.GUIConfiguration{
|
||||||
|
RawAddress: c.GUIAddress,
|
||||||
|
APIKey: c.GUIAPIKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
configCommand, err := getConfigCommand(clientFactory)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement the same flags at the upper CLI, but do nothing with them.
|
||||||
|
// This is so that the usage text is the same
|
||||||
|
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 {
|
||||||
|
// 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 {
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
if !strings.HasPrefix(args[i], "--") {
|
||||||
|
args = args[:i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !strings.Contains(args[i], "=") {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We don't want kong to print anything nor os.Exit (e.g. on -h)
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -4,11 +4,16 @@
|
|||||||
// 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 cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/fs"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,11 +32,6 @@ var operationCommand = cli.Command{
|
|||||||
Usage: "Shutdown syncthing",
|
Usage: "Shutdown syncthing",
|
||||||
Action: expects(0, emptyPost("system/shutdown")),
|
Action: expects(0, emptyPost("system/shutdown")),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "reset",
|
|
||||||
Usage: "Reset syncthing deleting all folders and devices",
|
|
||||||
Action: expects(0, emptyPost("system/reset")),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "upgrade",
|
Name: "upgrade",
|
||||||
Usage: "Upgrade syncthing (if a newer version is available)",
|
Usage: "Upgrade syncthing (if a newer version is available)",
|
||||||
@@ -39,15 +39,24 @@ var operationCommand = cli.Command{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "folder-override",
|
Name: "folder-override",
|
||||||
Usage: "Override changes on folder (remote for sendonly, local for receiveonly)",
|
Usage: "Override changes on folder (remote for sendonly, local for receiveonly). WARNING: Destructive - deletes/changes your data.",
|
||||||
ArgsUsage: "[folder id]",
|
ArgsUsage: "FOLDER-ID",
|
||||||
Action: expects(1, foldersOverride),
|
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 {
|
func foldersOverride(c *cli.Context) error {
|
||||||
client := c.App.Metadata["client"].(*APIClient)
|
client, err := getClientFactory(c).getClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
cfg, err := getConfig(client)
|
cfg, err := getConfig(client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -69,10 +78,36 @@ func foldersOverride(c *cli.Context) error {
|
|||||||
if body != "" {
|
if body != "" {
|
||||||
errStr += "\nBody: " + body
|
errStr += "\nBody: " + body
|
||||||
}
|
}
|
||||||
return fmt.Errorf(errStr)
|
return errors.New(errStr)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Errorf("Folder " + rid + " not found")
|
return fmt.Errorf("Folder %q not found", rid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDefaultIgnores(c *cli.Context) error {
|
||||||
|
client, err := getClientFactory(c).getClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dir, file := filepath.Split(c.Args()[0])
|
||||||
|
filesystem := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
|
||||||
|
|
||||||
|
fd, err := filesystem.Open(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(fd)
|
||||||
|
var lines []string
|
||||||
|
for scanner.Scan() {
|
||||||
|
lines = append(lines, scanner.Text())
|
||||||
|
}
|
||||||
|
fd.Close()
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.PutJSON("config/defaults/ignores", config.Ignores{Lines: lines})
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (C) 2021 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 cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pendingCommand = cli.Command{
|
||||||
|
Name: "pending",
|
||||||
|
HideHelp: true,
|
||||||
|
Usage: "Pending subcommand group",
|
||||||
|
Subcommands: []cli.Command{
|
||||||
|
{
|
||||||
|
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 {
|
||||||
|
return func(c *cli.Context) error {
|
||||||
|
if c.String("device") != "" {
|
||||||
|
query := make(url.Values)
|
||||||
|
query.Set("device", c.String("device"))
|
||||||
|
return indexDumpOutput("cluster/pending/folders?" + query.Encode())(c)
|
||||||
|
}
|
||||||
|
return indexDumpOutput("cluster/pending/folders")(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
@@ -18,27 +18,33 @@ var showCommand = cli.Command{
|
|||||||
{
|
{
|
||||||
Name: "version",
|
Name: "version",
|
||||||
Usage: "Show syncthing client version",
|
Usage: "Show syncthing client version",
|
||||||
Action: expects(0, dumpOutput("system/version")),
|
Action: expects(0, indexDumpOutput("system/version")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "config-status",
|
Name: "config-status",
|
||||||
Usage: "Show configuration status, whether or not a restart is required for changes to take effect",
|
Usage: "Show configuration status, whether or not a restart is required for changes to take effect",
|
||||||
Action: expects(0, dumpOutput("system/config/insync")),
|
Action: expects(0, indexDumpOutput("config/restart-required")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "system",
|
Name: "system",
|
||||||
Usage: "Show system status",
|
Usage: "Show system status",
|
||||||
Action: expects(0, dumpOutput("system/status")),
|
Action: expects(0, indexDumpOutput("system/status")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "connections",
|
Name: "connections",
|
||||||
Usage: "Report about connections to other devices",
|
Usage: "Report about connections to other devices",
|
||||||
Action: expects(0, dumpOutput("system/connections")),
|
Action: expects(0, indexDumpOutput("system/connections")),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "discovery",
|
||||||
|
Usage: "Show the discovered addresses of remote devices (from cache of the running syncthing instance)",
|
||||||
|
Action: expects(0, indexDumpOutput("system/discovery")),
|
||||||
|
},
|
||||||
|
pendingCommand,
|
||||||
{
|
{
|
||||||
Name: "usage",
|
Name: "usage",
|
||||||
Usage: "Show usage report",
|
Usage: "Show usage report",
|
||||||
Action: expects(0, dumpOutput("svc/report")),
|
Action: expects(0, indexDumpOutput("svc/report")),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
// 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 cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/db/backend"
|
||||||
|
"github.com/syncthing/syncthing/lib/locations"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func responseToBArray(response *http.Response) ([]byte, error) {
|
||||||
|
bytes, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bytes, response.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func emptyPost(url string) cli.ActionFunc {
|
||||||
|
return func(c *cli.Context) error {
|
||||||
|
client, err := getClientFactory(c).getClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = client.Post(url, "")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexDumpOutput(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 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfig(c APIClient) (config.Configuration, error) {
|
||||||
|
cfg := config.Configuration{}
|
||||||
|
response, err := c.Get("system/config")
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
bytes, err := responseToBArray(response)
|
||||||
|
if err != nil {
|
||||||
|
return cfg, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(bytes, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
return config.Configuration{}, err
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prettyPrintResponse(response *http.Response) error {
|
||||||
|
bytes, err := responseToBArray(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var data interface{}
|
||||||
|
if err := json.Unmarshal(bytes, &data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// TODO: Check flag for pretty print format
|
||||||
|
return prettyPrintJSON(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDB() (backend.Backend, error) {
|
||||||
|
return backend.OpenLevelDBRO(locations.Get(locations.Database))
|
||||||
|
}
|
||||||
|
|
||||||
|
func nulString(bs []byte) string {
|
||||||
|
for i := range bs {
|
||||||
|
if bs[i] == 0 {
|
||||||
|
return string(bs[:i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizePath(path string) string {
|
||||||
|
return filepath.ToSlash(filepath.Clean(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClientFactory(c *cli.Context) *apiClientFactory {
|
||||||
|
return c.App.Metadata["clientFactory"].(*apiClientFactory)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (C) 2021 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 cmdutil
|
||||||
|
|
||||||
|
// CommonOptions are reused among several subcommands
|
||||||
|
type CommonOptions struct {
|
||||||
|
buildCommonOptions
|
||||||
|
ConfDir string `name:"config" placeholder:"PATH" env:"STCONFDIR" help:"Set configuration directory (config and keys)"`
|
||||||
|
HomeDir string `name:"home" placeholder:"PATH" env:"STHOMEDIR" help:"Set configuration and data directory"`
|
||||||
|
NoDefaultFolder bool `env:"STNODEFAULTFOLDER" help:"Don't create the \"default\" folder on first startup"`
|
||||||
|
SkipPortProbing bool `help:"Don't try to find free ports for GUI and listen addresses on first startup"`
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
// Copyright (C) 2016 The Syncthing Authors.
|
// Copyright (C) 2021 The Syncthing Authors.
|
||||||
//
|
//
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
// 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,
|
// 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/.
|
||||||
|
|
||||||
// The existence of this file means we get 0% test coverage rather than no
|
//go:build !windows
|
||||||
// test coverage at all. Remove when implementing an actual test.
|
// +build !windows
|
||||||
|
|
||||||
package stats
|
package cmdutil
|
||||||
|
|
||||||
|
type buildCommonOptions struct {
|
||||||
|
HideConsole bool `hidden:""`
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (C) 2021 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 cmdutil
|
||||||
|
|
||||||
|
type buildCommonOptions struct {
|
||||||
|
HideConsole bool `name:"no-console" help:"Hide console window"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (C) 2014 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 cmdutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/locations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetConfigDataLocationsFromFlags(homeDir, confDir, dataDir string) error {
|
||||||
|
homeSet := homeDir != ""
|
||||||
|
confSet := confDir != ""
|
||||||
|
dataSet := dataDir != ""
|
||||||
|
switch {
|
||||||
|
case dataSet != confSet:
|
||||||
|
return errors.New("either both or none of --config and --data must be given, use --home to set both at once")
|
||||||
|
case homeSet && dataSet:
|
||||||
|
return errors.New("--home must not be used together with --config and --data")
|
||||||
|
case homeSet:
|
||||||
|
confDir = homeDir
|
||||||
|
dataDir = homeDir
|
||||||
|
fallthrough
|
||||||
|
case dataSet:
|
||||||
|
if err := locations.SetBaseDir(locations.ConfigBaseDir, confDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return locations.SetBaseDir(locations.DataBaseDir, dataDir)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -62,7 +61,7 @@ func uploadPanicLogs(ctx context.Context, urlBase, dir string) {
|
|||||||
// the log contents. A HEAD request is made to see if the log has already
|
// the log contents. A HEAD request is made to see if the log has already
|
||||||
// been reported. If not, a PUT is made with the log contents.
|
// been reported. If not, a PUT is made with the log contents.
|
||||||
func uploadPanicLog(ctx context.Context, urlBase, file string) error {
|
func uploadPanicLog(ctx context.Context, urlBase, file string) error {
|
||||||
data, err := ioutil.ReadFile(file)
|
data, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
// Copyright (C) 2021 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 decrypt implements the `syncthing decrypt` subcommand.
|
||||||
|
package decrypt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/fs"
|
||||||
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/scanner"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CLI struct {
|
||||||
|
Path string `arg:"" required:"1" help:"Path to encrypted folder"`
|
||||||
|
To string `xor:"mode" placeholder:"PATH" help:"Destination directory, when decrypting"`
|
||||||
|
VerifyOnly bool `xor:"mode" help:"Don't write decrypted files to disk (but verify plaintext hashes)"`
|
||||||
|
Password string `help:"Folder password for decryption / verification" env:"FOLDER_PASSWORD"`
|
||||||
|
FolderID string `help:"Folder ID of the encrypted folder, if it cannot be determined automatically"`
|
||||||
|
Continue bool `help:"Continue processing next file in case of error, instead of aborting"`
|
||||||
|
Verbose bool `help:"Show verbose progress information"`
|
||||||
|
TokenPath string `placeholder:"PATH" help:"Path to the token file within the folder (used to determine folder ID)"`
|
||||||
|
|
||||||
|
folderKey *[32]byte
|
||||||
|
keyGen *protocol.KeyGenerator
|
||||||
|
}
|
||||||
|
|
||||||
|
type storedEncryptionToken struct {
|
||||||
|
FolderID string
|
||||||
|
Token []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CLI) Run() error {
|
||||||
|
log.SetFlags(0)
|
||||||
|
|
||||||
|
if c.To == "" && !c.VerifyOnly {
|
||||||
|
return errors.New("must set --to or --verify-only")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.TokenPath == "" {
|
||||||
|
// This is a bit long to show as default in --help
|
||||||
|
c.TokenPath = filepath.Join(config.DefaultMarkerName, config.EncryptionTokenName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.FolderID == "" {
|
||||||
|
// We should try to figure out the folder ID
|
||||||
|
folderID, err := c.getFolderID()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("No --folder-id given and couldn't read folder token")
|
||||||
|
return fmt.Errorf("getting folder ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.FolderID = folderID
|
||||||
|
if c.Verbose {
|
||||||
|
log.Println("Found folder ID:", c.FolderID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.keyGen = protocol.NewKeyGenerator()
|
||||||
|
c.folderKey = c.keyGen.KeyFromPassword(c.FolderID, c.Password)
|
||||||
|
|
||||||
|
return c.walk()
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk finds and processes every file in the encrypted folder
|
||||||
|
func (c *CLI) walk() error {
|
||||||
|
srcFs := fs.NewFilesystem(fs.FilesystemTypeBasic, c.Path)
|
||||||
|
var dstFs fs.Filesystem
|
||||||
|
if c.To != "" {
|
||||||
|
dstFs = fs.NewFilesystem(fs.FilesystemTypeBasic, c.To)
|
||||||
|
}
|
||||||
|
|
||||||
|
return srcFs.Walk(".", func(path string, info fs.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.IsRegular() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if fs.IsInternal(path) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.withContinue(c.process(srcFs, dstFs, path))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// If --continue was set we just mention the error and return nil to
|
||||||
|
// continue processing.
|
||||||
|
func (c *CLI) withContinue(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if c.Continue {
|
||||||
|
log.Println("Warning:", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFolderID returns the folder ID found in the encrypted token, or an
|
||||||
|
// error.
|
||||||
|
func (c *CLI) getFolderID() (string, error) {
|
||||||
|
tokenPath := filepath.Join(c.Path, c.TokenPath)
|
||||||
|
bs, err := os.ReadFile(tokenPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading folder token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tok storedEncryptionToken
|
||||||
|
if err := json.Unmarshal(bs, &tok); err != nil {
|
||||||
|
return "", fmt.Errorf("parsing folder token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tok.FolderID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// process handles the file named path in srcFs, decrypting it into dstFs
|
||||||
|
// unless dstFs is nil.
|
||||||
|
func (c *CLI) process(srcFs fs.Filesystem, dstFs fs.Filesystem, path string) error {
|
||||||
|
// Which filemode bits to preserve
|
||||||
|
const retainBits = fs.ModePerm | fs.ModeSetgid | fs.ModeSetuid | fs.ModeSticky
|
||||||
|
|
||||||
|
if c.Verbose {
|
||||||
|
log.Printf("Processing %q", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
encFd, err := srcFs.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer encFd.Close()
|
||||||
|
|
||||||
|
encFi, err := loadEncryptedFileInfo(encFd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: loading metadata trailer: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workaround for a bug in <= v1.15.0-rc.5 where we stored names
|
||||||
|
// in native format, while protocol expects wire format (slashes).
|
||||||
|
encFi.Name = osutil.NormalizedFilename(encFi.Name)
|
||||||
|
|
||||||
|
plainFi, err := protocol.DecryptFileInfo(c.keyGen, *encFi, c.folderKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: decrypting metadata: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Verbose {
|
||||||
|
log.Printf("Plaintext filename is %q", plainFi.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var plainFd fs.File
|
||||||
|
if dstFs != nil {
|
||||||
|
if err := dstFs.MkdirAll(filepath.Dir(plainFi.Name), 0o700); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", plainFi.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plainFd, err = dstFs.Create(plainFi.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", plainFi.Name, err)
|
||||||
|
}
|
||||||
|
defer plainFd.Close() // also closed explicitly in the return
|
||||||
|
if err := dstFs.Chmod(plainFi.Name, fs.FileMode(plainFi.Permissions&uint32(retainBits))); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", plainFi.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.decryptFile(encFi, &plainFi, encFd, plainFd); err != nil {
|
||||||
|
// Decrypting the file failed, leaving it in an inconsistent state.
|
||||||
|
// Delete it. Even --continue currently doesn't mean "leave broken
|
||||||
|
// stuff in place", it just means "try the next file instead of
|
||||||
|
// aborting".
|
||||||
|
if plainFd != nil {
|
||||||
|
_ = dstFs.Remove(plainFd.Name())
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s: %s: %w", path, plainFi.Name, err)
|
||||||
|
} else if c.Verbose {
|
||||||
|
log.Printf("Data verified for %q", plainFi.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if plainFd != nil {
|
||||||
|
if err := plainFd.Close(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", plainFi.Name, err)
|
||||||
|
}
|
||||||
|
if err := dstFs.Chtimes(plainFi.Name, plainFi.ModTime(), plainFi.ModTime()); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", plainFi.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decryptFile reads, decrypts and verifies all the blocks in src, writing
|
||||||
|
// it to dst if dst is non-nil. (If dst is nil it just becomes a
|
||||||
|
// read-and-verify operation.)
|
||||||
|
func (c *CLI) decryptFile(encFi *protocol.FileInfo, plainFi *protocol.FileInfo, src io.ReaderAt, dst io.WriterAt) error {
|
||||||
|
// The encrypted and plaintext files must consist of an equal number of blocks
|
||||||
|
if len(encFi.Blocks) != len(plainFi.Blocks) {
|
||||||
|
return fmt.Errorf("block count mismatch: encrypted %d != plaintext %d", len(encFi.Blocks), len(plainFi.Blocks))
|
||||||
|
}
|
||||||
|
|
||||||
|
fileKey := c.keyGen.FileKey(plainFi.Name, c.folderKey)
|
||||||
|
for i, encBlock := range encFi.Blocks {
|
||||||
|
// Read the encrypted block
|
||||||
|
buf := make([]byte, encBlock.Size)
|
||||||
|
if _, err := src.ReadAt(buf, encBlock.Offset); err != nil {
|
||||||
|
return fmt.Errorf("encrypted block %d (%d bytes): %w", i, encBlock.Size, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt it
|
||||||
|
dec, err := protocol.DecryptBytes(buf, fileKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encrypted block %d (%d bytes): %w", i, encBlock.Size, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the block size against the expected plaintext
|
||||||
|
plainBlock := plainFi.Blocks[i]
|
||||||
|
if i == len(plainFi.Blocks)-1 && len(dec) > plainBlock.Size {
|
||||||
|
// The last block might be padded, which is fine (we skip the padding)
|
||||||
|
dec = dec[:plainBlock.Size]
|
||||||
|
} else if len(dec) != plainBlock.Size {
|
||||||
|
return fmt.Errorf("plaintext block %d size mismatch, actual %d != expected %d", i, len(dec), plainBlock.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the hash against the plaintext block info
|
||||||
|
if !scanner.Validate(dec, plainBlock.Hash, 0) {
|
||||||
|
// The block decrypted correctly but fails the hash check. This
|
||||||
|
// is odd and unexpected, but it it's still a valid block from
|
||||||
|
// the source. The file might have changed while we pulled it?
|
||||||
|
err := fmt.Errorf("plaintext block %d (%d bytes) failed validation after decryption", i, plainBlock.Size)
|
||||||
|
if c.Continue {
|
||||||
|
log.Printf("Warning: %s: %s: %v", encFi.Name, plainFi.Name, err)
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write it to the destination, unless we're just verifying.
|
||||||
|
if dst != nil {
|
||||||
|
if _, err := dst.WriteAt(dec, plainBlock.Offset); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadEncryptedFileInfo loads the encrypted FileInfo trailer from a file on
|
||||||
|
// disk.
|
||||||
|
func loadEncryptedFileInfo(fd fs.File) (*protocol.FileInfo, error) {
|
||||||
|
// Seek to the size of the trailer block
|
||||||
|
if _, err := fd.Seek(-4, io.SeekEnd); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var bs [4]byte
|
||||||
|
if _, err := io.ReadFull(fd, bs[:]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
size := int64(binary.BigEndian.Uint32(bs[:]))
|
||||||
|
|
||||||
|
// Seek to the start of the trailer
|
||||||
|
if _, err := fd.Seek(-(4 + size), io.SeekEnd); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
trailer := make([]byte, size)
|
||||||
|
if _, err := io.ReadFull(fd, trailer); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var encFi protocol.FileInfo
|
||||||
|
if err := encFi.Unmarshal(trailer); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &encFi, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
// Copyright (C) 2021 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 generate implements the `syncthing generate` subcommand.
|
||||||
|
package generate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/cmd/syncthing/cmdutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
|
"github.com/syncthing/syncthing/lib/fs"
|
||||||
|
"github.com/syncthing/syncthing/lib/locations"
|
||||||
|
"github.com/syncthing/syncthing/lib/logger"
|
||||||
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/syncthing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CLI struct {
|
||||||
|
cmdutil.CommonOptions
|
||||||
|
GUIUser string `placeholder:"STRING" help:"Specify new GUI authentication user name"`
|
||||||
|
GUIPassword string `placeholder:"STRING" help:"Specify new GUI authentication password (use - to read from standard input)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CLI) Run(l logger.Logger) error {
|
||||||
|
if c.HideConsole {
|
||||||
|
osutil.HideConsole()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.HomeDir != "" {
|
||||||
|
if c.ConfDir != "" {
|
||||||
|
return errors.New("--home must not be used together with --config")
|
||||||
|
}
|
||||||
|
c.ConfDir = c.HomeDir
|
||||||
|
}
|
||||||
|
if c.ConfDir == "" {
|
||||||
|
c.ConfDir = locations.GetBaseDir(locations.ConfigBaseDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support reading the password from a pipe or similar
|
||||||
|
if c.GUIPassword == "-" {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
password, _, err := reader.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed reading GUI password: %w", err)
|
||||||
|
}
|
||||||
|
c.GUIPassword = string(password)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Generate(l, c.ConfDir, c.GUIUser, c.GUIPassword, c.NoDefaultFolder, c.SkipPortProbing); err != nil {
|
||||||
|
return fmt.Errorf("failed to generate config and keys: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Generate(l logger.Logger, confDir, guiUser, guiPassword string, noDefaultFolder, skipPortProbing bool) error {
|
||||||
|
dir, err := fs.ExpandTilde(confDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := syncthing.EnsureDir(dir, 0o700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
locations.SetBaseDir(locations.ConfigBaseDir, dir)
|
||||||
|
|
||||||
|
var myID protocol.DeviceID
|
||||||
|
certFile, keyFile := locations.Get(locations.CertFile), locations.Get(locations.KeyFile)
|
||||||
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err == nil {
|
||||||
|
l.Warnln("Key exists; will not overwrite.")
|
||||||
|
} else {
|
||||||
|
cert, err = syncthing.GenerateCertificate(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create certificate: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
myID = protocol.NewDeviceID(cert.Certificate[0])
|
||||||
|
l.Infoln("Device ID:", myID)
|
||||||
|
|
||||||
|
cfgFile := locations.Get(locations.ConfigFile)
|
||||||
|
cfg, _, err := config.Load(cfgFile, myID, events.NoopLogger)
|
||||||
|
if fs.IsNotExist(err) {
|
||||||
|
if cfg, err = syncthing.DefaultConfig(cfgFile, myID, events.NoopLogger, noDefaultFolder, skipPortProbing); err != nil {
|
||||||
|
return fmt.Errorf("create config: %w", err)
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("load config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
go cfg.Serve(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var updateErr error
|
||||||
|
waiter, err := cfg.Modify(func(cfg *config.Configuration) {
|
||||||
|
updateErr = updateGUIAuthentication(l, &cfg.GUI, guiUser, guiPassword)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("modify config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
waiter.Wait()
|
||||||
|
if updateErr != nil {
|
||||||
|
return updateErr
|
||||||
|
}
|
||||||
|
if err := cfg.Save(); err != nil {
|
||||||
|
return fmt.Errorf("save config: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateGUIAuthentication(l logger.Logger, guiCfg *config.GUIConfiguration, guiUser, guiPassword string) error {
|
||||||
|
if guiUser != "" && guiCfg.User != guiUser {
|
||||||
|
guiCfg.User = guiUser
|
||||||
|
l.Infoln("Updated GUI authentication user name:", guiUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
if guiPassword != "" && guiCfg.Password != guiPassword {
|
||||||
|
if err := guiCfg.SetPassword(guiPassword); err != nil {
|
||||||
|
return fmt.Errorf("failed to set GUI authentication password: %w", err)
|
||||||
|
}
|
||||||
|
l.Infoln("Updated GUI authentication password.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -11,24 +11,17 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
"strconv"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func startHeapProfiler() {
|
||||||
if innerProcess && os.Getenv("STHEAPPROFILE") != "" {
|
l.Debugln("Starting heap profiling")
|
||||||
rate := 1
|
go func() {
|
||||||
if i, err := strconv.Atoi(os.Getenv("STHEAPPROFILE")); err == nil {
|
err := saveHeapProfiles(1) // Only returns on error
|
||||||
rate = i
|
l.Warnln("Heap profiler failed:", err)
|
||||||
}
|
panic("Heap profiler failed")
|
||||||
l.Debugln("Starting heap profiling")
|
}()
|
||||||
go func() {
|
|
||||||
err := saveHeapProfiles(rate) // Only returns on error
|
|
||||||
l.Warnln("Heap profiler failed:", err)
|
|
||||||
panic("Heap profiler failed")
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveHeapProfiles(rate int) error {
|
func saveHeapProfiles(rate int) error {
|
||||||
|
|||||||
+339
-402
File diff suppressed because it is too large.
Load diff
+51
-44
@@ -15,18 +15,16 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
"github.com/syncthing/syncthing/lib/fs"
|
"github.com/syncthing/syncthing/lib/fs"
|
||||||
"github.com/syncthing/syncthing/lib/locations"
|
"github.com/syncthing/syncthing/lib/locations"
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/svcutil"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
"github.com/syncthing/syncthing/lib/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -36,20 +34,21 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
countRestarts = 4
|
restartCounts = 4
|
||||||
loopThreshold = 60 * time.Second
|
restartPause = 1 * time.Second
|
||||||
|
restartLoopThreshold = 60 * time.Second
|
||||||
logFileAutoCloseDelay = 5 * time.Second
|
logFileAutoCloseDelay = 5 * time.Second
|
||||||
logFileMaxOpenTime = time.Minute
|
logFileMaxOpenTime = time.Minute
|
||||||
panicUploadMaxWait = 30 * time.Second
|
panicUploadMaxWait = 30 * time.Second
|
||||||
panicUploadNoticeWait = 10 * time.Second
|
panicUploadNoticeWait = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func monitorMain(runtimeOptions RuntimeOptions) {
|
func monitorMain(options serveOptions) {
|
||||||
l.SetPrefix("[monitor] ")
|
l.SetPrefix("[monitor] ")
|
||||||
|
|
||||||
var dst io.Writer = os.Stdout
|
var dst io.Writer = os.Stdout
|
||||||
|
|
||||||
logFile := runtimeOptions.logFile
|
logFile := locations.Get(locations.LogFile)
|
||||||
if logFile != "-" {
|
if logFile != "-" {
|
||||||
if expanded, err := fs.ExpandTilde(logFile); err == nil {
|
if expanded, err := fs.ExpandTilde(logFile); err == nil {
|
||||||
logFile = expanded
|
logFile = expanded
|
||||||
@@ -59,15 +58,15 @@ func monitorMain(runtimeOptions RuntimeOptions) {
|
|||||||
open := func(name string) (io.WriteCloser, error) {
|
open := func(name string) (io.WriteCloser, error) {
|
||||||
return newAutoclosedFile(name, logFileAutoCloseDelay, logFileMaxOpenTime)
|
return newAutoclosedFile(name, logFileAutoCloseDelay, logFileMaxOpenTime)
|
||||||
}
|
}
|
||||||
if runtimeOptions.logMaxSize > 0 {
|
if options.LogMaxSize > 0 {
|
||||||
fileDst, err = newRotatedFile(logFile, open, int64(runtimeOptions.logMaxSize), runtimeOptions.logMaxFiles)
|
fileDst, err = newRotatedFile(logFile, open, int64(options.LogMaxSize), options.LogMaxFiles)
|
||||||
} else {
|
} else {
|
||||||
fileDst, err = open(logFile)
|
fileDst, err = open(logFile)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warnln("Failed to setup logging to file, proceeding with logging to stdout only:", err)
|
l.Warnln("Failed to setup logging to file, proceeding with logging to stdout only:", err)
|
||||||
} else {
|
} else {
|
||||||
if runtime.GOOS == "windows" {
|
if build.IsWindows {
|
||||||
// Translate line breaks to Windows standard
|
// Translate line breaks to Windows standard
|
||||||
fileDst = osutil.ReplacingWriter{
|
fileDst = osutil.ReplacingWriter{
|
||||||
Writer: fileDst,
|
Writer: fileDst,
|
||||||
@@ -84,7 +83,12 @@ func monitorMain(runtimeOptions RuntimeOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
args := os.Args
|
args := os.Args
|
||||||
var restarts [countRestarts]time.Time
|
binary, err := getBinary(args[0])
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("Error starting the main Syncthing process:", err)
|
||||||
|
panic("Error starting the main Syncthing process")
|
||||||
|
}
|
||||||
|
var restarts [restartCounts]time.Time
|
||||||
|
|
||||||
stopSign := make(chan os.Signal, 1)
|
stopSign := make(chan os.Signal, 1)
|
||||||
signal.Notify(stopSign, os.Interrupt, sigTerm)
|
signal.Notify(stopSign, os.Interrupt, sigTerm)
|
||||||
@@ -97,15 +101,15 @@ func monitorMain(runtimeOptions RuntimeOptions) {
|
|||||||
for {
|
for {
|
||||||
maybeReportPanics()
|
maybeReportPanics()
|
||||||
|
|
||||||
if t := time.Since(restarts[0]); t < loopThreshold {
|
if t := time.Since(restarts[0]); t < restartLoopThreshold {
|
||||||
l.Warnf("%d restarts in %v; not retrying further", countRestarts, t)
|
l.Warnf("%d restarts in %v; not retrying further", restartCounts, t)
|
||||||
os.Exit(util.ExitError.AsInt())
|
os.Exit(svcutil.ExitError.AsInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(restarts[0:], restarts[1:])
|
copy(restarts[0:], restarts[1:])
|
||||||
restarts[len(restarts)-1] = time.Now()
|
restarts[len(restarts)-1] = time.Now()
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(binary, args[1:]...)
|
||||||
cmd.Env = childEnv
|
cmd.Env = childEnv
|
||||||
|
|
||||||
stderr, err := cmd.StderrPipe()
|
stderr, err := cmd.StderrPipe()
|
||||||
@@ -169,31 +173,31 @@ func monitorMain(runtimeOptions RuntimeOptions) {
|
|||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Successful exit indicates an intentional shutdown
|
// Successful exit indicates an intentional shutdown
|
||||||
os.Exit(util.ExitSuccess.AsInt())
|
os.Exit(svcutil.ExitSuccess.AsInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||||
exitCode := exiterr.ExitCode()
|
exitCode := exiterr.ExitCode()
|
||||||
if stopped || runtimeOptions.noRestart {
|
if stopped || options.NoRestart {
|
||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
}
|
}
|
||||||
if exitCode == util.ExitUpgrade.AsInt() {
|
if exitCode == svcutil.ExitUpgrade.AsInt() {
|
||||||
// Restart the monitor process to release the .old
|
// Restart the monitor process to release the .old
|
||||||
// binary as part of the upgrade process.
|
// binary as part of the upgrade process.
|
||||||
l.Infoln("Restarting monitor...")
|
l.Infoln("Restarting monitor...")
|
||||||
if err = restartMonitor(args); err != nil {
|
if err = restartMonitor(binary, args); err != nil {
|
||||||
l.Warnln("Restart:", err)
|
l.Warnln("Restart:", err)
|
||||||
}
|
}
|
||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if runtimeOptions.noRestart {
|
if options.NoRestart {
|
||||||
os.Exit(util.ExitError.AsInt())
|
os.Exit(svcutil.ExitError.AsInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Infoln("Syncthing exited:", err)
|
l.Infoln("Syncthing exited:", err)
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(restartPause)
|
||||||
|
|
||||||
if first {
|
if first {
|
||||||
// Let the next child process know that this is not the first time
|
// Let the next child process know that this is not the first time
|
||||||
@@ -204,6 +208,19 @@ func monitorMain(runtimeOptions RuntimeOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getBinary(args0 string) (string, error) {
|
||||||
|
e, err := os.Executable()
|
||||||
|
if err == nil {
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
// Check if args0 cuts it
|
||||||
|
e, lerr := exec.LookPath(args0)
|
||||||
|
if lerr == nil {
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
func copyStderr(stderr io.Reader, dst io.Writer) {
|
func copyStderr(stderr io.Reader, dst io.Writer) {
|
||||||
br := bufio.NewReader(stderr)
|
br := bufio.NewReader(stderr)
|
||||||
|
|
||||||
@@ -256,7 +273,7 @@ func copyStderr(stderr io.Reader, dst io.Writer) {
|
|||||||
* This crash usually occurs due to one of the following reasons: *
|
* This crash usually occurs due to one of the following reasons: *
|
||||||
* - Syncthing being stopped abruptly (killed/loss of power) *
|
* - Syncthing being stopped abruptly (killed/loss of power) *
|
||||||
* - Bad hardware (memory/disk issues) *
|
* - Bad hardware (memory/disk issues) *
|
||||||
* - Software that affects disk writes (SSD caching software and simillar) *
|
* - Software that affects disk writes (SSD caching software and similar) *
|
||||||
* *
|
* *
|
||||||
* Please see the following URL for instructions on how to recover: *
|
* Please see the following URL for instructions on how to recover: *
|
||||||
* https://docs.syncthing.net/users/faq.html#my-syncthing-database-is-corrupt *
|
* https://docs.syncthing.net/users/faq.html#my-syncthing-database-is-corrupt *
|
||||||
@@ -310,40 +327,30 @@ func copyStdout(stdout io.Reader, dst io.Writer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restartMonitor(args []string) error {
|
func restartMonitor(binary string, args []string) error {
|
||||||
// Set the STRESTART environment variable to indicate to the next
|
// Set the STRESTART environment variable to indicate to the next
|
||||||
// process that this is a restart and not initial start. This prevents
|
// process that this is a restart and not initial start. This prevents
|
||||||
// opening the browser on startup.
|
// opening the browser on startup.
|
||||||
os.Setenv("STRESTART", "yes")
|
os.Setenv("STRESTART", "yes")
|
||||||
|
|
||||||
if runtime.GOOS != "windows" {
|
if !build.IsWindows {
|
||||||
// syscall.Exec is the cleanest way to restart on Unixes as it
|
// syscall.Exec is the cleanest way to restart on Unixes as it
|
||||||
// replaces the current process with the new one, keeping the pid and
|
// replaces the current process with the new one, keeping the pid and
|
||||||
// controlling terminal and so on
|
// controlling terminal and so on
|
||||||
return restartMonitorUnix(args)
|
return restartMonitorUnix(binary, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// but it isn't supported on Windows, so there we start a normal
|
// but it isn't supported on Windows, so there we start a normal
|
||||||
// exec.Command and return.
|
// exec.Command and return.
|
||||||
return restartMonitorWindows(args)
|
return restartMonitorWindows(binary, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restartMonitorUnix(args []string) error {
|
func restartMonitorUnix(binary string, args []string) error {
|
||||||
if !strings.ContainsRune(args[0], os.PathSeparator) {
|
return syscall.Exec(binary, args, os.Environ())
|
||||||
// The path to the binary doesn't contain a slash, so it should be
|
|
||||||
// found in $PATH.
|
|
||||||
binary, err := exec.LookPath(args[0])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
args[0] = binary
|
|
||||||
}
|
|
||||||
|
|
||||||
return syscall.Exec(args[0], args, os.Environ())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func restartMonitorWindows(args []string) error {
|
func restartMonitorWindows(binary string, args []string) error {
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(binary, args[1:]...)
|
||||||
// Retain the standard streams
|
// Retain the standard streams
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
@@ -514,7 +521,7 @@ func (f *autoclosedFile) ensureOpenLocked() error {
|
|||||||
// We open the file for write only, and create it if it doesn't exist.
|
// We open the file for write only, and create it if it doesn't exist.
|
||||||
flags := os.O_WRONLY | os.O_CREATE | os.O_APPEND
|
flags := os.O_WRONLY | os.O_CREATE | os.O_APPEND
|
||||||
|
|
||||||
fd, err := os.OpenFile(f.name, flags, 0644)
|
fd, err := os.OpenFile(f.name, flags, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -563,7 +570,7 @@ func childEnv() []string {
|
|||||||
// panicUploadMaxWait uploading panics...
|
// panicUploadMaxWait uploading panics...
|
||||||
func maybeReportPanics() {
|
func maybeReportPanics() {
|
||||||
// Try to get a config to see if/where panics should be reported.
|
// Try to get a config to see if/where panics should be reported.
|
||||||
cfg, err := loadOrDefaultConfig(protocol.EmptyDeviceID, events.NoopLogger)
|
cfg, err := loadOrDefaultConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warnln("Couldn't load config; not reporting crash")
|
l.Warnln("Couldn't load config; not reporting crash")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -18,14 +17,17 @@ import (
|
|||||||
func TestRotatedFile(t *testing.T) {
|
func TestRotatedFile(t *testing.T) {
|
||||||
// Verify that log rotation happens.
|
// Verify that log rotation happens.
|
||||||
|
|
||||||
dir, err := ioutil.TempDir("", "syncthing")
|
dir := t.TempDir()
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(dir)
|
|
||||||
|
|
||||||
open := func(name string) (io.WriteCloser, error) {
|
open := func(name string) (io.WriteCloser, error) {
|
||||||
return os.Create(name)
|
f, err := os.Create(name)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if f != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return f, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logName := filepath.Join(dir, "log.txt")
|
logName := filepath.Join(dir, "log.txt")
|
||||||
@@ -179,7 +181,7 @@ func TestAutoClosedFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The file should have both writes in it.
|
// The file should have both writes in it.
|
||||||
bs, err := ioutil.ReadFile(file)
|
bs, err := os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -199,7 +201,7 @@ func TestAutoClosedFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// It should now contain three writes, as the file is always opened for appending
|
// It should now contain three writes, as the file is always opened for appending
|
||||||
bs, err = ioutil.ReadFile(file)
|
bs, err = os.ReadFile(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,25 @@
|
|||||||
// 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 !windows
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
)
|
)
|
||||||
|
|
||||||
func openURL(url string) error {
|
func openURL(url string) error {
|
||||||
switch runtime.GOOS {
|
if build.IsDarwin {
|
||||||
case "darwin":
|
|
||||||
return exec.Command("open", url).Run()
|
return exec.Command("open", url).Run()
|
||||||
|
|
||||||
default:
|
|
||||||
cmd := exec.Command("xdg-open", url)
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
Setpgid: true,
|
|
||||||
}
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
}
|
||||||
|
cmd := exec.Command("xdg-open", url)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Setpgid: true,
|
||||||
|
}
|
||||||
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
@@ -4,6 +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:build windows
|
||||||
// +build windows
|
// +build windows
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|||||||
@@ -4,6 +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:build !solaris && !windows
|
||||||
// +build !solaris,!windows
|
// +build !solaris,!windows
|
||||||
|
|
||||||
package main
|
package main
|
||||||
@@ -18,10 +19,8 @@ import (
|
|||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func startPerfStats() {
|
||||||
if innerProcess && os.Getenv("STPERFSTATS") != "" {
|
go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()))
|
||||||
go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func savePerfStats(file string) {
|
func savePerfStats(file string) {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (C) 2021 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/.
|
||||||
|
|
||||||
|
//go:build solaris || windows
|
||||||
|
// +build solaris windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
func startPerfStats() {
|
||||||
|
}
|
||||||
@@ -4,7 +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/.
|
||||||
|
|
||||||
//+build go1.7
|
//go:build go1.7
|
||||||
|
// +build go1.7
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
// Copyright (C) 2014 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"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"text/tabwriter"
|
|
||||||
)
|
|
||||||
|
|
||||||
func optionTable(w io.Writer, rows [][]string) {
|
|
||||||
tw := tabwriter.NewWriter(w, 2, 4, 2, ' ', 0)
|
|
||||||
for _, row := range rows {
|
|
||||||
for i, cell := range row {
|
|
||||||
if i > 0 {
|
|
||||||
tw.Write([]byte("\t"))
|
|
||||||
}
|
|
||||||
tw.Write([]byte(cell))
|
|
||||||
}
|
|
||||||
tw.Write([]byte("\n"))
|
|
||||||
}
|
|
||||||
tw.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
func usageFor(fs *flag.FlagSet, usage string, extra string) func() {
|
|
||||||
return func() {
|
|
||||||
var b bytes.Buffer
|
|
||||||
b.WriteString("Usage:\n " + usage + "\n")
|
|
||||||
|
|
||||||
var options [][]string
|
|
||||||
fs.VisitAll(func(f *flag.Flag) {
|
|
||||||
var opt = " -" + f.Name
|
|
||||||
|
|
||||||
if f.DefValue != "false" {
|
|
||||||
opt += "=" + fmt.Sprintf(`"%s"`, f.DefValue)
|
|
||||||
}
|
|
||||||
options = append(options, []string{opt, f.Usage})
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(options) > 0 {
|
|
||||||
b.WriteString("\nOptions:\n")
|
|
||||||
optionTable(&b, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(b.String())
|
|
||||||
|
|
||||||
if len(extra) > 0 {
|
|
||||||
fmt.Println(extra)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,330 +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 (
|
|
||||||
"database/sql"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
)
|
|
||||||
|
|
||||||
var dbConn = getEnvDefault("UR_DB_URL", "postgres://user:password@localhost/ur?sslmode=disable")
|
|
||||||
|
|
||||||
func getEnvDefault(key, def string) string {
|
|
||||||
if val := os.Getenv(key); val != "" {
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.SetFlags(log.Ltime | log.Ldate)
|
|
||||||
log.SetOutput(os.Stdout)
|
|
||||||
|
|
||||||
db, err := sql.Open("postgres", dbConn)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("database:", err)
|
|
||||||
}
|
|
||||||
err = setupDB(db)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("database:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
runAggregation(db)
|
|
||||||
// Sleep until one minute past next midnight
|
|
||||||
sleepUntilNext(24*time.Hour, 1*time.Minute)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runAggregation(db *sql.DB) {
|
|
||||||
since := maxIndexedDay(db, "VersionSummary")
|
|
||||||
log.Println("Aggregating VersionSummary data since", since)
|
|
||||||
rows, err := aggregateVersionSummary(db, since)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("aggregate:", err)
|
|
||||||
}
|
|
||||||
log.Println("Inserted", rows, "rows")
|
|
||||||
|
|
||||||
log.Println("Aggregating UserMovement data")
|
|
||||||
rows, err = aggregateUserMovement(db)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("aggregate:", err)
|
|
||||||
}
|
|
||||||
log.Println("Inserted", rows, "rows")
|
|
||||||
|
|
||||||
log.Println("Aggregating Performance data")
|
|
||||||
since = maxIndexedDay(db, "Performance")
|
|
||||||
rows, err = aggregatePerformance(db, since)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("aggregate:", err)
|
|
||||||
}
|
|
||||||
log.Println("Inserted", rows, "rows")
|
|
||||||
|
|
||||||
log.Println("Aggregating BlockStats data")
|
|
||||||
since = maxIndexedDay(db, "BlockStats")
|
|
||||||
rows, err = aggregateBlockStats(db, since)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("aggregate:", err)
|
|
||||||
}
|
|
||||||
log.Println("Inserted", rows, "rows")
|
|
||||||
}
|
|
||||||
|
|
||||||
func sleepUntilNext(intv, margin time.Duration) {
|
|
||||||
now := time.Now().UTC()
|
|
||||||
next := now.Truncate(intv).Add(intv).Add(margin)
|
|
||||||
log.Println("Sleeping until", next)
|
|
||||||
time.Sleep(next.Sub(now))
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupDB(db *sql.DB) error {
|
|
||||||
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS VersionSummary (
|
|
||||||
Day TIMESTAMP NOT NULL,
|
|
||||||
Version VARCHAR(8) NOT NULL,
|
|
||||||
Count INTEGER NOT NULL
|
|
||||||
)`)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS UserMovement (
|
|
||||||
Day TIMESTAMP NOT NULL,
|
|
||||||
Added INTEGER NOT NULL,
|
|
||||||
Bounced INTEGER NOT NULL,
|
|
||||||
Removed INTEGER NOT NULL
|
|
||||||
)`)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS Performance (
|
|
||||||
Day TIMESTAMP NOT NULL,
|
|
||||||
TotFiles INTEGER NOT NULL,
|
|
||||||
TotMiB INTEGER NOT NULL,
|
|
||||||
SHA256Perf DOUBLE PRECISION NOT NULL,
|
|
||||||
MemorySize INTEGER NOT NULL,
|
|
||||||
MemoryUsageMiB INTEGER NOT NULL
|
|
||||||
)`)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS BlockStats (
|
|
||||||
Day TIMESTAMP NOT NULL,
|
|
||||||
Reports INTEGER NOT NULL,
|
|
||||||
Total INTEGER NOT NULL,
|
|
||||||
Renamed INTEGER NOT NULL,
|
|
||||||
Reused INTEGER NOT NULL,
|
|
||||||
Pulled INTEGER NOT NULL,
|
|
||||||
CopyOrigin INTEGER NOT NULL,
|
|
||||||
CopyOriginShifted INTEGER NOT NULL,
|
|
||||||
CopyElsewhere INTEGER NOT NULL
|
|
||||||
)`)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var t string
|
|
||||||
|
|
||||||
row := db.QueryRow(`SELECT 'UniqueDayVersionIndex'::regclass`)
|
|
||||||
if err := row.Scan(&t); err != nil {
|
|
||||||
_, err = db.Exec(`CREATE UNIQUE INDEX UniqueDayVersionIndex ON VersionSummary (Day, Version)`)
|
|
||||||
}
|
|
||||||
|
|
||||||
row = db.QueryRow(`SELECT 'VersionDayIndex'::regclass`)
|
|
||||||
if err := row.Scan(&t); err != nil {
|
|
||||||
_, err = db.Exec(`CREATE INDEX VersionDayIndex ON VersionSummary (Day)`)
|
|
||||||
}
|
|
||||||
|
|
||||||
row = db.QueryRow(`SELECT 'MovementDayIndex'::regclass`)
|
|
||||||
if err := row.Scan(&t); err != nil {
|
|
||||||
_, err = db.Exec(`CREATE INDEX MovementDayIndex ON UserMovement (Day)`)
|
|
||||||
}
|
|
||||||
|
|
||||||
row = db.QueryRow(`SELECT 'PerformanceDayIndex'::regclass`)
|
|
||||||
if err := row.Scan(&t); err != nil {
|
|
||||||
_, err = db.Exec(`CREATE INDEX PerformanceDayIndex ON Performance (Day)`)
|
|
||||||
}
|
|
||||||
|
|
||||||
row = db.QueryRow(`SELECT 'BlockStatsDayIndex'::regclass`)
|
|
||||||
if err := row.Scan(&t); err != nil {
|
|
||||||
_, err = db.Exec(`CREATE INDEX BlockStatsDayIndex ON BlockStats (Day)`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func maxIndexedDay(db *sql.DB, table string) time.Time {
|
|
||||||
var t time.Time
|
|
||||||
row := db.QueryRow("SELECT MAX(Day) FROM " + table)
|
|
||||||
err := row.Scan(&t)
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func aggregateVersionSummary(db *sql.DB, since time.Time) (int64, error) {
|
|
||||||
res, err := db.Exec(`INSERT INTO VersionSummary (
|
|
||||||
SELECT
|
|
||||||
DATE_TRUNC('day', Received) AS Day,
|
|
||||||
SUBSTRING(Report->>'version' FROM '^v\d.\d+') AS Ver,
|
|
||||||
COUNT(*) AS Count
|
|
||||||
FROM ReportsJson
|
|
||||||
WHERE
|
|
||||||
DATE_TRUNC('day', Received) > $1
|
|
||||||
AND DATE_TRUNC('day', Received) < DATE_TRUNC('day', NOW())
|
|
||||||
AND Report->>'version' like 'v_.%'
|
|
||||||
GROUP BY Day, Ver
|
|
||||||
);
|
|
||||||
`, since)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.RowsAffected()
|
|
||||||
}
|
|
||||||
|
|
||||||
func aggregateUserMovement(db *sql.DB) (int64, error) {
|
|
||||||
rows, err := db.Query(`SELECT
|
|
||||||
DATE_TRUNC('day', Received) AS Day,
|
|
||||||
Report->>'uniqueID'
|
|
||||||
FROM ReportsJson
|
|
||||||
WHERE
|
|
||||||
DATE_TRUNC('day', Received) < DATE_TRUNC('day', NOW())
|
|
||||||
AND Report->>'version' like 'v_.%'
|
|
||||||
ORDER BY Day
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
firstSeen := make(map[string]time.Time)
|
|
||||||
lastSeen := make(map[string]time.Time)
|
|
||||||
var minTs time.Time
|
|
||||||
minTs = minTs.In(time.UTC)
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var ts time.Time
|
|
||||||
var id string
|
|
||||||
if err := rows.Scan(&ts, &id); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if minTs.IsZero() {
|
|
||||||
minTs = ts
|
|
||||||
}
|
|
||||||
if _, ok := firstSeen[id]; !ok {
|
|
||||||
firstSeen[id] = ts
|
|
||||||
}
|
|
||||||
lastSeen[id] = ts
|
|
||||||
}
|
|
||||||
|
|
||||||
type sumRow struct {
|
|
||||||
day time.Time
|
|
||||||
added int
|
|
||||||
removed int
|
|
||||||
bounced int
|
|
||||||
}
|
|
||||||
var sumRows []sumRow
|
|
||||||
for t := minTs; t.Before(time.Now().Truncate(24 * time.Hour)); t = t.AddDate(0, 0, 1) {
|
|
||||||
var added, removed, bounced int
|
|
||||||
old := t.Before(time.Now().AddDate(0, 0, -30))
|
|
||||||
for id, first := range firstSeen {
|
|
||||||
last := lastSeen[id]
|
|
||||||
if first.Equal(t) && last.Equal(t) && old {
|
|
||||||
bounced++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if first.Equal(t) {
|
|
||||||
added++
|
|
||||||
}
|
|
||||||
if last == t && old {
|
|
||||||
removed++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sumRows = append(sumRows, sumRow{t, added, removed, bounced})
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if _, err := tx.Exec("DELETE FROM UserMovement"); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
for _, r := range sumRows {
|
|
||||||
if _, err := tx.Exec("INSERT INTO UserMovement (Day, Added, Removed, Bounced) VALUES ($1, $2, $3, $4)", r.day, r.added, r.removed, r.bounced); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return int64(len(sumRows)), tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func aggregatePerformance(db *sql.DB, since time.Time) (int64, error) {
|
|
||||||
res, err := db.Exec(`INSERT INTO Performance (
|
|
||||||
SELECT
|
|
||||||
DATE_TRUNC('day', Received) AS Day,
|
|
||||||
AVG((Report->>'totFiles')::numeric) As TotFiles,
|
|
||||||
AVG((Report->>'totMiB')::numeric) As TotMiB,
|
|
||||||
AVG((Report->>'sha256Perf')::numeric) As SHA256Perf,
|
|
||||||
AVG((Report->>'memorySize')::numeric) As MemorySize,
|
|
||||||
AVG((Report->>'memoryUsageMiB')::numeric) As MemoryUsageMiB
|
|
||||||
FROM ReportsJson
|
|
||||||
WHERE
|
|
||||||
DATE_TRUNC('day', Received) > $1
|
|
||||||
AND DATE_TRUNC('day', Received) < DATE_TRUNC('day', NOW())
|
|
||||||
AND Report->>'version' like 'v_.%'
|
|
||||||
GROUP BY Day
|
|
||||||
);
|
|
||||||
`, since)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.RowsAffected()
|
|
||||||
}
|
|
||||||
|
|
||||||
func aggregateBlockStats(db *sql.DB, since time.Time) (int64, error) {
|
|
||||||
// Filter out anything prior 0.14.41 as that has sum aggregations which
|
|
||||||
// made no sense.
|
|
||||||
res, err := db.Exec(`INSERT INTO BlockStats (
|
|
||||||
SELECT
|
|
||||||
DATE_TRUNC('day', Received) AS Day,
|
|
||||||
COUNT(1) As Reports,
|
|
||||||
SUM((Report->'blockStats'->>'total')::numeric) AS Total,
|
|
||||||
SUM((Report->'blockStats'->>'renamed')::numeric) AS Renamed,
|
|
||||||
SUM((Report->'blockStats'->>'reused')::numeric) AS Reused,
|
|
||||||
SUM((Report->'blockStats'->>'pulled')::numeric) AS Pulled,
|
|
||||||
SUM((Report->'blockStats'->>'copyOrigin')::numeric) AS CopyOrigin,
|
|
||||||
SUM((Report->'blockStats'->>'copyOriginShifted')::numeric) AS CopyOriginShifted,
|
|
||||||
SUM((Report->'blockStats'->>'copyElsewhere')::numeric) AS CopyElsewhere
|
|
||||||
FROM ReportsJson
|
|
||||||
WHERE
|
|
||||||
DATE_TRUNC('day', Received) > $1
|
|
||||||
AND DATE_TRUNC('day', Received) < DATE_TRUNC('day', NOW())
|
|
||||||
AND (Report->>'urVersion')::numeric >= 3
|
|
||||||
AND Report->>'version' like 'v_.%'
|
|
||||||
AND Report->>'version' NOT LIKE 'v0.14.40%'
|
|
||||||
AND Report->>'version' NOT LIKE 'v0.14.39%'
|
|
||||||
AND Report->>'version' NOT LIKE 'v0.14.38%'
|
|
||||||
GROUP BY Day
|
|
||||||
);
|
|
||||||
`, since)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.RowsAffected()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
// 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 aggregate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CLI struct {
|
||||||
|
DBConn string `env:"UR_DB_URL" default:"postgres://user:password@localhost/ur?sslmode=disable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *CLI) Run() error {
|
||||||
|
log.SetFlags(log.Ltime | log.Ldate)
|
||||||
|
log.SetOutput(os.Stdout)
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", cli.DBConn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("database: %w", err)
|
||||||
|
}
|
||||||
|
err = setupDB(db)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
runAggregation(db)
|
||||||
|
// Sleep until one minute past next midnight
|
||||||
|
sleepUntilNext(24*time.Hour, 1*time.Minute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAggregation(db *sql.DB) {
|
||||||
|
since := maxIndexedDay(db, "VersionSummary")
|
||||||
|
log.Println("Aggregating VersionSummary data since", since)
|
||||||
|
rows, err := aggregateVersionSummary(db, since.Add(24*time.Hour))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("aggregate:", err)
|
||||||
|
}
|
||||||
|
log.Println("Inserted", rows, "rows")
|
||||||
|
|
||||||
|
since = maxIndexedDay(db, "Performance")
|
||||||
|
log.Println("Aggregating Performance data since", since)
|
||||||
|
rows, err = aggregatePerformance(db, since.Add(24*time.Hour))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("aggregate:", err)
|
||||||
|
}
|
||||||
|
log.Println("Inserted", rows, "rows")
|
||||||
|
|
||||||
|
since = maxIndexedDay(db, "BlockStats")
|
||||||
|
log.Println("Aggregating BlockStats data since", since)
|
||||||
|
rows, err = aggregateBlockStats(db, since.Add(24*time.Hour))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("aggregate:", err)
|
||||||
|
}
|
||||||
|
log.Println("Inserted", rows, "rows")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sleepUntilNext(intv, margin time.Duration) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
next := now.Truncate(intv).Add(intv).Add(margin)
|
||||||
|
log.Println("Sleeping until", next)
|
||||||
|
time.Sleep(next.Sub(now))
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupDB(db *sql.DB) error {
|
||||||
|
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS VersionSummary (
|
||||||
|
Day TIMESTAMP NOT NULL,
|
||||||
|
Version VARCHAR(8) NOT NULL,
|
||||||
|
Count INTEGER NOT NULL
|
||||||
|
)`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS Performance (
|
||||||
|
Day TIMESTAMP NOT NULL,
|
||||||
|
TotFiles INTEGER NOT NULL,
|
||||||
|
TotMiB INTEGER NOT NULL,
|
||||||
|
SHA256Perf DOUBLE PRECISION NOT NULL,
|
||||||
|
MemorySize INTEGER NOT NULL,
|
||||||
|
MemoryUsageMiB INTEGER NOT NULL
|
||||||
|
)`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS BlockStats (
|
||||||
|
Day TIMESTAMP NOT NULL,
|
||||||
|
Reports INTEGER NOT NULL,
|
||||||
|
Total BIGINT NOT NULL,
|
||||||
|
Renamed BIGINT NOT NULL,
|
||||||
|
Reused BIGINT NOT NULL,
|
||||||
|
Pulled BIGINT NOT NULL,
|
||||||
|
CopyOrigin BIGINT NOT NULL,
|
||||||
|
CopyOriginShifted BIGINT NOT NULL,
|
||||||
|
CopyElsewhere BIGINT NOT NULL
|
||||||
|
)`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var t string
|
||||||
|
|
||||||
|
row := db.QueryRow(`SELECT 'UniqueDayVersionIndex'::regclass`)
|
||||||
|
if err := row.Scan(&t); err != nil {
|
||||||
|
_, _ = db.Exec(`CREATE UNIQUE INDEX UniqueDayVersionIndex ON VersionSummary (Day, Version)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
row = db.QueryRow(`SELECT 'VersionDayIndex'::regclass`)
|
||||||
|
if err := row.Scan(&t); err != nil {
|
||||||
|
_, _ = db.Exec(`CREATE INDEX VersionDayIndex ON VersionSummary (Day)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
row = db.QueryRow(`SELECT 'PerformanceDayIndex'::regclass`)
|
||||||
|
if err := row.Scan(&t); err != nil {
|
||||||
|
_, _ = db.Exec(`CREATE INDEX PerformanceDayIndex ON Performance (Day)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
row = db.QueryRow(`SELECT 'BlockStatsDayIndex'::regclass`)
|
||||||
|
if err := row.Scan(&t); err != nil {
|
||||||
|
_, _ = db.Exec(`CREATE INDEX BlockStatsDayIndex ON BlockStats (Day)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxIndexedDay(db *sql.DB, table string) time.Time {
|
||||||
|
var t time.Time
|
||||||
|
row := db.QueryRow("SELECT MAX(DATE_TRUNC('day', Day)) FROM " + table)
|
||||||
|
err := row.Scan(&t)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func aggregateVersionSummary(db *sql.DB, since time.Time) (int64, error) {
|
||||||
|
res, err := db.Exec(`INSERT INTO VersionSummary (
|
||||||
|
SELECT
|
||||||
|
DATE_TRUNC('day', Received) AS Day,
|
||||||
|
SUBSTRING(Report->>'version' FROM '^v\d.\d+') AS Ver,
|
||||||
|
COUNT(*) AS Count
|
||||||
|
FROM ReportsJson
|
||||||
|
WHERE
|
||||||
|
Received > $1
|
||||||
|
AND Received < DATE_TRUNC('day', NOW())
|
||||||
|
AND Report->>'version' like 'v_.%'
|
||||||
|
GROUP BY Day, Ver
|
||||||
|
);
|
||||||
|
`, since)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func aggregatePerformance(db *sql.DB, since time.Time) (int64, error) {
|
||||||
|
res, err := db.Exec(`INSERT INTO Performance (
|
||||||
|
SELECT
|
||||||
|
DATE_TRUNC('day', Received) AS Day,
|
||||||
|
AVG((Report->>'totFiles')::numeric) As TotFiles,
|
||||||
|
AVG((Report->>'totMiB')::numeric) As TotMiB,
|
||||||
|
AVG((Report->>'sha256Perf')::numeric) As SHA256Perf,
|
||||||
|
AVG((Report->>'memorySize')::numeric) As MemorySize,
|
||||||
|
AVG((Report->>'memoryUsageMiB')::numeric) As MemoryUsageMiB
|
||||||
|
FROM ReportsJson
|
||||||
|
WHERE
|
||||||
|
Received > $1
|
||||||
|
AND Received < DATE_TRUNC('day', NOW())
|
||||||
|
AND Report->>'version' like 'v_.%'
|
||||||
|
/* Some custom implementation reported bytes when we expect megabytes, cap at petabyte */
|
||||||
|
AND (Report->>'memorySize')::numeric < 1073741824
|
||||||
|
GROUP BY Day
|
||||||
|
);
|
||||||
|
`, since)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RowsAffected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func aggregateBlockStats(db *sql.DB, since time.Time) (int64, error) {
|
||||||
|
// Filter out anything prior 0.14.41 as that has sum aggregations which
|
||||||
|
// made no sense.
|
||||||
|
res, err := db.Exec(`INSERT INTO BlockStats (
|
||||||
|
SELECT
|
||||||
|
DATE_TRUNC('day', Received) AS Day,
|
||||||
|
COUNT(1) As Reports,
|
||||||
|
SUM((Report->'blockStats'->>'total')::numeric)::bigint AS Total,
|
||||||
|
SUM((Report->'blockStats'->>'renamed')::numeric)::bigint AS Renamed,
|
||||||
|
SUM((Report->'blockStats'->>'reused')::numeric)::bigint AS Reused,
|
||||||
|
SUM((Report->'blockStats'->>'pulled')::numeric)::bigint AS Pulled,
|
||||||
|
SUM((Report->'blockStats'->>'copyOrigin')::numeric)::bigint AS CopyOrigin,
|
||||||
|
SUM((Report->'blockStats'->>'copyOriginShifted')::numeric)::bigint AS CopyOriginShifted,
|
||||||
|
SUM((Report->'blockStats'->>'copyElsewhere')::numeric)::bigint AS CopyElsewhere
|
||||||
|
FROM ReportsJson
|
||||||
|
WHERE
|
||||||
|
Received > $1
|
||||||
|
AND Received < DATE_TRUNC('day', NOW())
|
||||||
|
AND (Report->>'urVersion')::numeric >= 3
|
||||||
|
AND Report->>'version' like 'v_.%'
|
||||||
|
AND Report->>'version' NOT LIKE 'v0.14.40%'
|
||||||
|
AND Report->>'version' NOT LIKE 'v0.14.39%'
|
||||||
|
AND Report->>'version' NOT LIKE 'v0.14.38%'
|
||||||
|
GROUP BY Day
|
||||||
|
);
|
||||||
|
`, since)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.RowsAffected()
|
||||||
|
}
|
||||||
Loaded 100 of 808 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user