Compare commits
1671 Commits
dev_object
...
1.0.0-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
051ea7786f | ||
|
|
42b645e355 | ||
|
|
f27ee96014 | ||
|
|
20cd117aa6 | ||
|
|
fc8931d69f | ||
|
|
0167b2decd | ||
|
|
e67980ff3c | ||
|
|
96760bba5a | ||
|
|
2501d7d241 | ||
|
|
55b84262b5 | ||
|
|
ce4252eb1a | ||
|
|
db708917b4 | ||
|
|
8ddb45627d | ||
|
|
550c225b79 | ||
|
|
0d46b550a8 | ||
|
|
0693cca1a4 | ||
|
|
0d9f9e381a | ||
|
|
6c7aa5a7ae | ||
|
|
a27d935925 | ||
|
|
b4f87a4fee | ||
|
|
ee5f94a2e2 | ||
|
|
9c3cf554d3 | ||
|
|
addbfa5487 | ||
|
|
5eb461d7b7 | ||
|
|
1ea45afcd7 | ||
|
|
dbd86f6aee | ||
|
|
af693f7b3f | ||
|
|
3be5ee6445 | ||
|
|
0acc8fe26a | ||
|
|
ecf40eb86c | ||
|
|
48ce7055f8 | ||
|
|
749f55d688 | ||
|
|
e5d17f5382 | ||
|
|
982cc66c74 | ||
|
|
74bf4909c8 | ||
|
|
9c956b4445 | ||
|
|
4c1fc9317e | ||
|
|
a9d77a618f | ||
|
|
38cdc87e93 | ||
|
|
f5ff93b65e | ||
|
|
6ef6f188e5 | ||
|
|
ccad91a4a9 | ||
|
|
63b79ae151 | ||
|
|
9284f64e2a | ||
|
|
b9bbae27de | ||
|
|
36e3efb5a5 | ||
|
|
04d1c8724d | ||
|
|
4fb4b353f8 | ||
|
|
564a02f344 | ||
|
|
5b582a4234 | ||
|
|
2e9792577f | ||
|
|
2066e0a03b | ||
|
|
a4d49a500f | ||
|
|
a8fbced928 | ||
|
|
99ca405279 | ||
|
|
2e1d1018aa | ||
|
|
c57b4be1c7 | ||
|
|
238a016242 | ||
|
|
2c0c7fafa3 | ||
|
|
ee4962fe31 | ||
|
|
55895d0a10 | ||
|
|
676897d389 | ||
|
|
5205ff6695 | ||
|
|
15cf3ce92b | ||
|
|
c0441b2412 | ||
|
|
6267872ddb | ||
|
|
618779a89d | ||
|
|
b3ec2325ed | ||
|
|
49a5643e76 | ||
|
|
657395af8a | ||
|
|
4de62ed77e | ||
|
|
505f493729 | ||
|
|
be05b704b0 | ||
|
|
b33c2fa3cf | ||
|
|
98674c60d4 | ||
|
|
e39eb86967 | ||
|
|
646070ae7a | ||
|
|
2525b66658 | ||
|
|
58c5a633e2 | ||
|
|
aefd894fc2 | ||
|
|
1e1d4646a2 | ||
|
|
b97845fffd | ||
|
|
84f5a4cb48 | ||
|
|
2832f0e089 | ||
|
|
a3b5445824 | ||
|
|
363e37c791 | ||
|
|
1b0b041530 | ||
|
|
7d5fc87002 | ||
|
|
13130e9dd4 | ||
|
|
1061ce11a3 | ||
|
|
9f9a74000d | ||
|
|
d1863018df | ||
|
|
166080aac8 | ||
|
|
78b2487639 | ||
|
|
79f4e81fea | ||
|
|
28da78d544 | ||
|
|
df2eb9bc6a | ||
|
|
7c20d92fe5 | ||
|
|
b4c316c662 | ||
|
|
411b511937 | ||
|
|
c902475443 | ||
|
|
00d8008a89 | ||
|
|
36acb5bce9 | ||
|
|
e033b019f6 | ||
|
|
259b80777e | ||
|
|
abdfad8521 | ||
|
|
c498fbcb27 | ||
|
|
874d486b1e | ||
|
|
21516251b0 | ||
|
|
a2f83b0d2d | ||
|
|
aa65766312 | ||
|
|
660f004cfd | ||
|
|
6d2c420f54 | ||
|
|
5f0b9a5fa8 | ||
|
|
8378e308e0 | ||
|
|
b9f54519fd | ||
|
|
4108a9649f | ||
|
|
6244e23451 | ||
|
|
713b322f99 | ||
|
|
e1a5a195c3 | ||
|
|
bc37417d6c | ||
|
|
3dbcaaa221 | ||
|
|
49f480d346 | ||
|
|
055a99ba25 | ||
|
|
2bd11d476e | ||
|
|
297004c259 | ||
|
|
4e2c4d8dba | ||
|
|
0626099c3b | ||
|
|
107ddcf394 | ||
|
|
8893ffc10f | ||
|
|
f23e855d23 | ||
|
|
8366413970 | ||
|
|
9862677fcf | ||
|
|
e50bc4c60c | ||
|
|
5f6104731d | ||
|
|
6a6866c337 | ||
|
|
ce2ce4b16e | ||
|
|
1ecd5a87d9 | ||
|
|
72aead5466 | ||
|
|
abd5dff9b5 | ||
|
|
040b05c318 | ||
|
|
ce470c95c4 | ||
|
|
32e531bc61 | ||
|
|
dcf25e46af | ||
|
|
2b079ae065 | ||
|
|
d41ccc1551 | ||
|
|
fa17f7b1e3 | ||
|
|
c41299a29f | ||
|
|
79156d2d82 | ||
|
|
26542b741e | ||
|
|
8b2b4a0146 | ||
|
|
5cf9087113 | ||
|
|
dd12250987 | ||
|
|
e172b277f2 | ||
|
|
086331b8e7 | ||
|
|
96d22c3276 | ||
|
|
caa3564439 | ||
|
|
18933fdb58 | ||
|
|
65a731a243 | ||
|
|
89035d3b3b | ||
|
|
c6527643a3 | ||
|
|
b9157d5e9d | ||
|
|
20be2d9859 | ||
|
|
855541678e | ||
|
|
73d3d8ab5c | ||
|
|
6983a3ffce | ||
|
|
d6653f1258 | ||
|
|
7ab53a6d7d | ||
|
|
85ee9811d8 | ||
|
|
61bd76f77e | ||
|
|
8cf611426b | ||
|
|
b0ac977a3d | ||
|
|
6a5fbe7ef1 | ||
|
|
e20e84fedb | ||
|
|
5826396cd0 | ||
|
|
0be4264eb1 | ||
|
|
70ae0cd1a8 | ||
|
|
bf690ad353 | ||
|
|
2e14b32ccd | ||
|
|
557e85c0c1 | ||
|
|
8d30e68284 | ||
|
|
830d2b8df5 | ||
|
|
cb19bbef22 | ||
|
|
291de5bb96 | ||
|
|
86f9e6c49f | ||
|
|
97e29a27a0 | ||
|
|
d3575dccce | ||
|
|
aeffacb53e | ||
|
|
b9ff63d064 | ||
|
|
68bc59c6eb | ||
|
|
cc63e56b8a | ||
|
|
7c79d4c1ca | ||
|
|
a61e9dd27d | ||
|
|
3c9ed62bbb | ||
|
|
cf7a930d39 | ||
|
|
e641036cda | ||
|
|
d736c6224d | ||
|
|
c8868a47d7 | ||
|
|
9d74f56f57 | ||
|
|
cc440f23a3 | ||
|
|
ddf19b3444 | ||
|
|
18a3902d2c | ||
|
|
024f9512be | ||
|
|
f62db65a39 | ||
|
|
4fa389c047 | ||
|
|
f069790a72 | ||
|
|
2b1241de96 | ||
|
|
fff2bc6523 | ||
|
|
a2828a8015 | ||
|
|
3fd70de4a6 | ||
|
|
37861d9af6 | ||
|
|
38220697e0 | ||
|
|
0ea5351a1b | ||
|
|
68090d1c35 | ||
|
|
57c8c76e58 | ||
|
|
c5358ba62c | ||
|
|
c63c6187c5 | ||
|
|
880d22a4bb | ||
|
|
2fe0c8cd32 | ||
|
|
e9610f1fb7 | ||
|
|
8218070ebc | ||
|
|
e4971b6d83 | ||
|
|
24738dc635 | ||
|
|
0606d3fc29 | ||
|
|
bfeb746c5f | ||
|
|
7b76fc0efe | ||
|
|
dab493009e | ||
|
|
8e75ea07e7 | ||
|
|
b21f701a81 | ||
|
|
62e68d4e51 | ||
|
|
a96f352fa7 | ||
|
|
a231f05d4f | ||
|
|
6ea075bd46 | ||
|
|
6a24ad1ecd | ||
|
|
9a5a5f0cde | ||
|
|
111f994a9d | ||
|
|
cc1b0eff1a | ||
|
|
20054d0a56 | ||
|
|
54c45739f1 | ||
|
|
3fbbde4897 | ||
|
|
b69a4ed691 | ||
|
|
f46ffd8885 | ||
|
|
c7095d913f | ||
|
|
f30345aaa8 | ||
|
|
4d86866d61 | ||
|
|
a770b17e0c | ||
|
|
f38108d20d | ||
|
|
4ed84a6bc4 | ||
|
|
e4690c48b4 | ||
|
|
c358e6203f | ||
|
|
7b891750e8 | ||
|
|
1efcce0001 | ||
|
|
fafba030c2 | ||
|
|
7bd8431006 | ||
|
|
8a4cb0319a | ||
|
|
749537664f | ||
|
|
35489ea352 | ||
|
|
bd5d27ffbd | ||
|
|
b2610df635 | ||
|
|
f4014a56b6 | ||
|
|
33e5d5a14c | ||
|
|
dbe27dc9cd | ||
|
|
ca30b78a1b | ||
|
|
ce7cfedfcb | ||
|
|
2698d3aa10 | ||
|
|
26f84a9696 | ||
|
|
831cb0b6d9 | ||
|
|
61e600b9ba | ||
|
|
280d14997a | ||
|
|
7a7ca6e154 | ||
|
|
37b4ab935d | ||
|
|
39a290e061 | ||
|
|
a6cfb04453 | ||
|
|
a99e8543db | ||
|
|
d72da751c8 | ||
|
|
3579bd1bb5 | ||
|
|
746b29a641 | ||
|
|
ca18177e59 | ||
|
|
65fb1b5f8d | ||
|
|
7e17d7d729 | ||
|
|
364c3098b2 | ||
|
|
e8a5b5d12c | ||
|
|
3d5bf5a6d6 | ||
|
|
46c50b1bb1 | ||
|
|
833f9c908f | ||
|
|
caf8d80318 | ||
|
|
a0f8b2f2be | ||
|
|
317cadaed8 | ||
|
|
8690e4a3ef | ||
|
|
005881f838 | ||
|
|
81f0c9763f | ||
|
|
811d1a598b | ||
|
|
17076a56b3 | ||
|
|
34ec13e532 | ||
|
|
dc93398768 | ||
|
|
4654ba0937 | ||
|
|
de3773fbbc | ||
|
|
3817d2d682 | ||
|
|
7b2e5aa0ae | ||
|
|
270047f47a | ||
|
|
744bcfc4a9 | ||
|
|
7bb7f9e309 | ||
|
|
2c176dd864 | ||
|
|
0178ec5026 | ||
|
|
5a5712fde0 | ||
|
|
080af2b39f | ||
|
|
4559baaeeb | ||
|
|
1722780560 | ||
|
|
5155a3d544 | ||
|
|
928453db62 | ||
|
|
cc71f40a6d | ||
|
|
c7af6587f5 | ||
|
|
e0f65e5e24 | ||
|
|
48d530cb13 | ||
|
|
d3cc36f6e0 | ||
|
|
3721c93a55 | ||
|
|
e145586b65 | ||
|
|
c658d88d25 | ||
|
|
e6b019c29d | ||
|
|
09e8bc8f02 | ||
|
|
450db14305 | ||
|
|
147bfe3de5 | ||
|
|
a48d800426 | ||
|
|
e7135b8f4d | ||
|
|
3a68060e58 | ||
|
|
58faf141bd | ||
|
|
039108ee5e | ||
|
|
5b1b527851 | ||
|
|
85421657ad | ||
|
|
d019f3d5bd | ||
|
|
bbd13a5aac | ||
|
|
a28d9c814f | ||
|
|
81a790f13f | ||
|
|
0f7a98a91f | ||
|
|
e81a57ce24 | ||
|
|
efae4f5203 | ||
|
|
65ff5faada | ||
|
|
04cfe837a3 | ||
|
|
4a786618d4 | ||
|
|
4cadc4c12d | ||
|
|
086eab8c70 | ||
|
|
da4a4e7cbe | ||
|
|
39e988537c | ||
|
|
e520299c4b | ||
|
|
fa8ac29e76 | ||
|
|
c48ebd5149 | ||
|
|
e254ddc947 | ||
|
|
ca298b460c | ||
|
|
52342f2f8e | ||
|
|
2f3f86a9f2 | ||
|
|
ac4f1400fc | ||
|
|
d29bf4809d | ||
|
|
1606276223 | ||
|
|
57cda74fbd | ||
|
|
82cc1402c4 | ||
|
|
2f3dbac59b | ||
|
|
3c5e20b633 | ||
|
|
8309d2f8be | ||
|
|
3a567768c1 | ||
|
|
87423bfb8c | ||
|
|
af948f9e88 | ||
|
|
048727f183 | ||
|
|
b14cd7508b | ||
|
|
47d72baf88 | ||
|
|
09095f2abd | ||
|
|
bb282bcd5d | ||
|
|
2c5b01eb6f | ||
|
|
c413465645 | ||
|
|
e82a69c9ca | ||
|
|
8452c11e9a | ||
|
|
5cc56784a7 | ||
|
|
0ca03465e3 | ||
|
|
d529b9abbd | ||
|
|
8d636dba7f | ||
|
|
3135c8017d | ||
|
|
2cdd6ad192 | ||
|
|
bdb7e8d321 | ||
|
|
e8a59d7c07 | ||
|
|
2eeb9dbcbc | ||
|
|
7c9046c2cd | ||
|
|
cc0caa751c | ||
|
|
4fa8cb7ef0 | ||
|
|
b4210b1c3f | ||
|
|
e40562b03d | ||
|
|
98ce64aaae | ||
|
|
7ca9f44fca | ||
|
|
682ad8742d | ||
|
|
4252377249 | ||
|
|
6afb26b377 | ||
|
|
e6b931f71e | ||
|
|
7b890108ee | ||
|
|
8a81c3d95d | ||
|
|
40d99a5377 | ||
|
|
7527162bec | ||
|
|
3338f3236f | ||
|
|
0e575ef2be | ||
|
|
5c610e26c9 | ||
|
|
754ffd0ff2 | ||
|
|
3cf265611a | ||
|
|
9339093638 | ||
|
|
3321439951 | ||
|
|
6ea0185519 | ||
|
|
e62947f7b2 | ||
|
|
4bbf1c33b8 | ||
|
|
711cab777f | ||
|
|
f85ef06783 | ||
|
|
cecde068e1 | ||
|
|
28c71cc351 | ||
|
|
a64bd436c8 | ||
|
|
91c099e35f | ||
|
|
152ad57c1d | ||
|
|
3edfdbea76 | ||
|
|
2e4218706b | ||
|
|
11712414bb | ||
|
|
f798bb0fce | ||
|
|
1d2aeb288a | ||
|
|
27ab2350c9 | ||
|
|
96de65ebab | ||
|
|
ec5be0a2c3 | ||
|
|
9495df6d5e | ||
|
|
690ce7fae4 | ||
|
|
bf48d88a47 | ||
|
|
158462ca0b | ||
|
|
d66525a22f | ||
|
|
62a7824d0d | ||
|
|
1432ddb119 | ||
|
|
b570b6aa36 | ||
|
|
17aaf2cbc2 | ||
|
|
3076ba3253 | ||
|
|
df9347b34a | ||
|
|
b51ee48699 | ||
|
|
32d1db184a | ||
|
|
15a3012d05 | ||
|
|
7032318858 | ||
|
|
3862e255f0 | ||
|
|
f199718e3b | ||
|
|
b003d08d55 | ||
|
|
c589972fa7 | ||
|
|
2755d4125d | ||
|
|
77ddd1cfe0 | ||
|
|
1f62c35846 | ||
|
|
3eaa3d68c7 | ||
|
|
2fec6d161c | ||
|
|
3f8d56501c | ||
|
|
29fba447c3 | ||
|
|
6302764512 | ||
|
|
c03458202a | ||
|
|
a394a3163f | ||
|
|
6ea770b6c8 | ||
|
|
8c15835414 | ||
|
|
3dacde092c | ||
|
|
998f4578b1 | ||
|
|
a5bbce4920 | ||
|
|
db355bb26b | ||
|
|
4199bf2ba4 | ||
|
|
726ec9a8b5 | ||
|
|
9384b831ec | ||
|
|
3c7b66e039 | ||
|
|
8555d48ca2 | ||
|
|
6b0de4254f | ||
|
|
906cc5ffd7 | ||
|
|
7fe0cc74d2 | ||
|
|
703c465bd0 | ||
|
|
7f983e72e7 | ||
|
|
004b6945f2 | ||
|
|
541b812bb4 | ||
|
|
15bd320231 | ||
|
|
d3e2c9cb86 | ||
|
|
7cff7f0bd1 | ||
|
|
18096353c0 | ||
|
|
632e064ea5 | ||
|
|
4a9b842aa3 | ||
|
|
2edc2e243c | ||
|
|
545e5b2efb | ||
|
|
eefb2f8127 | ||
|
|
2f487be832 | ||
|
|
eec086c1ec | ||
|
|
644698e7ca | ||
|
|
2bad907cc6 | ||
|
|
3ceec7b5f2 | ||
|
|
ce672917cd | ||
|
|
1a921df86b | ||
|
|
ace04bcd97 | ||
|
|
a6a5aabf01 | ||
|
|
f9310f0b97 | ||
|
|
155cb546ac | ||
|
|
059a546604 | ||
|
|
581eb6641c | ||
|
|
1dddb4bb84 | ||
|
|
7dde4083db | ||
|
|
fe15655f1e | ||
|
|
d27ddcb8f7 | ||
|
|
8660c2af12 | ||
|
|
7154b8f871 | ||
|
|
f13e921c14 | ||
|
|
2a802acfe0 | ||
|
|
8747a91df8 | ||
|
|
4aaeff8e4c | ||
|
|
c74410858e | ||
|
|
f05a0b8e83 | ||
|
|
fb42ba1a14 | ||
|
|
2967deaed8 | ||
|
|
7069f9e7a2 | ||
|
|
cad2aa436b | ||
|
|
50e17d81fd | ||
|
|
de35928496 | ||
|
|
cf8acd38d0 | ||
|
|
b5c846132e | ||
|
|
b2e2c0fecd | ||
|
|
6350398c31 | ||
|
|
462e75b227 | ||
|
|
ecbd1e0bc3 | ||
|
|
181e08cb8e | ||
|
|
87a4ed2107 | ||
|
|
15befb705f | ||
|
|
af9bcde89f | ||
|
|
a1f4abf6c3 | ||
|
|
96d0155bcc | ||
|
|
84791e4877 | ||
|
|
8e48e4f490 | ||
|
|
a9b7b95606 | ||
|
|
c65d0caa1b | ||
|
|
f669838a2f | ||
|
|
0a7b0485b1 | ||
|
|
7b5f1d5835 | ||
|
|
0d9f13740d | ||
|
|
dbe573513e | ||
|
|
08a6cacdc2 | ||
|
|
adb3ea171a | ||
|
|
b5dcafefbb | ||
|
|
c29841a5e7 | ||
|
|
1620b0f0d8 | ||
|
|
ce16ad868b | ||
|
|
f03b945e4f | ||
|
|
1240b5320f | ||
|
|
63b0cb2c7a | ||
|
|
9d594cbda6 | ||
|
|
2b641b7ef3 | ||
|
|
a95138868e | ||
|
|
ade4d33eb1 | ||
|
|
ca8f399832 | ||
|
|
e4ac7d7a3e | ||
|
|
faf195cc4b | ||
|
|
a9c6bb8f0d | ||
|
|
50e52206cc | ||
|
|
732f59d10a | ||
|
|
25418e1372 | ||
|
|
864fcce07b | ||
|
|
319ff77b07 | ||
|
|
cea6ddbdf1 | ||
|
|
394fb36362 | ||
|
|
8c0d3fa227 | ||
|
|
7abcfe31e8 | ||
|
|
ea7a178ca7 | ||
|
|
61af17a4ec | ||
|
|
08a64bffac | ||
|
|
d858cd8d19 | ||
|
|
ebbf3a7bc3 | ||
|
|
54972a57b1 | ||
|
|
13317322de | ||
|
|
7adc1ba09d | ||
|
|
e4cc8ed5b9 | ||
|
|
2cadbba6ad | ||
|
|
c14bf4b479 | ||
|
|
dee19fdc35 | ||
|
|
864071d641 | ||
|
|
1e48f8d74e | ||
|
|
fb9f8319b2 | ||
|
|
7dfe5f92cc | ||
|
|
1f577b14b0 | ||
|
|
4d726273aa | ||
|
|
fc194f3a4a | ||
|
|
7eb722b444 | ||
|
|
9d90913697 | ||
|
|
d1a2f2939c | ||
|
|
1a9bfd1b86 | ||
|
|
7ebcfebecb | ||
|
|
0f1e9d0c63 | ||
|
|
17928cf9c8 | ||
|
|
cae578d6b5 | ||
|
|
42b38336ad | ||
|
|
ad71db6367 | ||
|
|
bc398ccf1b | ||
|
|
e838f4292c | ||
|
|
df71cea9af | ||
|
|
077fb4ed69 | ||
|
|
3eb0fd7ddd | ||
|
|
d7b3e20233 | ||
|
|
c970ed1587 | ||
|
|
d9d889cb4f | ||
|
|
9604efd5cc | ||
|
|
5fff195295 | ||
|
|
1a91371306 | ||
|
|
530c73916e | ||
|
|
c6b3051c67 | ||
|
|
a6c3b122bd | ||
|
|
0f22b21c8d | ||
|
|
1c088546b3 | ||
|
|
2d9394a8e0 | ||
|
|
72f5a24144 | ||
|
|
c6de1ae994 | ||
|
|
791780dd68 | ||
|
|
be8a615cd7 | ||
|
|
dff7476143 | ||
|
|
fd1bf30de8 | ||
|
|
704043ef02 | ||
|
|
6659b77f9a | ||
|
|
60635aeb65 | ||
|
|
5304b73588 | ||
|
|
44958797e5 | ||
|
|
136118ed21 | ||
|
|
9e9f721c08 | ||
|
|
41194b3f6b | ||
|
|
0477e8d3ed | ||
|
|
cdd285dec8 | ||
|
|
c69a2f83a6 | ||
|
|
76e38526c6 | ||
|
|
571cedf4ce | ||
|
|
dd7da015e3 | ||
|
|
0c351965a2 | ||
|
|
07c3cb3f0a | ||
|
|
33cd4c546a | ||
|
|
f6f1f3b329 | ||
|
|
780ff5f5df | ||
|
|
f8f58a8355 | ||
|
|
43f2963d14 | ||
|
|
a7115ba699 | ||
|
|
f64458018b | ||
|
|
b1358d4711 | ||
|
|
2a9a60197b | ||
|
|
db100a6db9 | ||
|
|
fd85b55d37 | ||
|
|
fd03ba54f3 | ||
|
|
e6e0821512 | ||
|
|
dc03f53655 | ||
|
|
57cbd35de1 | ||
|
|
d7cac5c727 | ||
|
|
e90b531567 | ||
|
|
5cb040f863 | ||
|
|
7a94363b38 | ||
|
|
76fdefeca4 | ||
|
|
29ddf4dbc8 | ||
|
|
de275b0510 | ||
|
|
0ac1095c70 | ||
|
|
fc47ca9dd2 | ||
|
|
01d5383ce3 | ||
|
|
08f7de2e33 | ||
|
|
05fad0aca0 | ||
|
|
b411a38813 | ||
|
|
a45411edbe | ||
|
|
8f917e4a19 | ||
|
|
7e1135df8f | ||
|
|
e5cd061e18 | ||
|
|
bd9509e030 | ||
|
|
1dde7015de | ||
|
|
e346d20228 | ||
|
|
79d58a98f4 | ||
|
|
6aecd72acc | ||
|
|
cdcf5d0917 | ||
|
|
5a363c5523 | ||
|
|
19cdf9660b | ||
|
|
66f5bf1bbc | ||
|
|
95ababe7a8 | ||
|
|
c1590a054c | ||
|
|
738a81e50e | ||
|
|
26d69cdc7f | ||
|
|
82c8b7f308 | ||
|
|
a6e3561f83 | ||
|
|
43df8b6927 | ||
|
|
42bec96db0 | ||
|
|
569099af9e | ||
|
|
750194e4dc | ||
|
|
68a89d59e5 | ||
|
|
c25c94ec30 | ||
|
|
e9d6e2ca95 | ||
|
|
e8f8a0872d | ||
|
|
87bec2e6a6 | ||
|
|
d71b0958db | ||
|
|
9590d99e7c | ||
|
|
82ddc2c76a | ||
|
|
05f1412323 | ||
|
|
ed4a9db9a0 | ||
|
|
8e3c22b595 | ||
|
|
9fc4bb919e | ||
|
|
7dae5f8ab7 | ||
|
|
7926ac015a | ||
|
|
86353d98d5 | ||
|
|
57b136d854 | ||
|
|
e650c117af | ||
|
|
a96907dda8 | ||
|
|
ec69f9376e | ||
|
|
ef597fff31 | ||
|
|
a095a607c0 | ||
|
|
4613b36697 | ||
|
|
e67c7d977f | ||
|
|
e38dd25bf6 | ||
|
|
35dac63c36 | ||
|
|
b2da8148bd | ||
|
|
56575d58f4 | ||
|
|
a745b91ded | ||
|
|
08d0021cd6 | ||
|
|
707c773dd9 | ||
|
|
92319f190c | ||
|
|
ef3f86ccf5 | ||
|
|
e58eaacb20 | ||
|
|
45a9b6313e | ||
|
|
380a451bc1 | ||
|
|
827818b493 | ||
|
|
cffd4fc28d | ||
|
|
cf2ed47fe8 | ||
|
|
ad7ff448d6 | ||
|
|
8981dfae9f | ||
|
|
fff7e5f827 | ||
|
|
873a04aed0 | ||
|
|
b2e1a7ac33 | ||
|
|
ea13098beb | ||
|
|
52a1f9b8a7 | ||
|
|
6d1a4ab824 | ||
|
|
c8ab89292e | ||
|
|
4f347a92c1 | ||
|
|
e4453adf82 | ||
|
|
15b6a426fb | ||
|
|
0ed92b3b6f | ||
|
|
18b50c1752 | ||
|
|
59c008b0ee | ||
|
|
27400394e4 | ||
|
|
536b2e2ed9 | ||
|
|
6c70c03985 | ||
|
|
5af648230a | ||
|
|
770f28d205 | ||
|
|
177dec627c | ||
|
|
70e94fd018 | ||
|
|
3b6397012b | ||
|
|
ad4dec1aa2 | ||
|
|
29fbfc3d6e | ||
|
|
cbb41c8fa3 | ||
|
|
b1d8e60802 | ||
|
|
bfc165abe0 | ||
|
|
21a829e7cf | ||
|
|
8ea6f7e627 | ||
|
|
cf70e12e14 | ||
|
|
9dfb8f8dcf | ||
|
|
f624ec5ba6 | ||
|
|
779a2d0a1a | ||
|
|
800ced24b7 | ||
|
|
bbd2bc2dde | ||
|
|
69598b48e3 | ||
|
|
fffa1ec671 | ||
|
|
7fc280c22b | ||
|
|
f1d84c7abe | ||
|
|
04874f5986 | ||
|
|
e5b8abcfcd | ||
|
|
85c8ea5ba6 | ||
|
|
38b7bbbc49 | ||
|
|
b9fb5e7e84 | ||
|
|
08fe5192fb | ||
|
|
cefdf4d6f9 | ||
|
|
c40099a636 | ||
|
|
3940ae2d69 | ||
|
|
c651bea903 | ||
|
|
d76d60a295 | ||
|
|
eb58ca0d8d | ||
|
|
26f128df02 | ||
|
|
b3ee5c8d4f | ||
|
|
e90bae35b9 | ||
|
|
37109fc618 | ||
|
|
33a0b9669c | ||
|
|
445e7df835 | ||
|
|
c3a17caa80 | ||
|
|
24d6c555f7 | ||
|
|
42c8890d4a | ||
|
|
ab8b19eb5d | ||
|
|
1b24fbdb00 | ||
|
|
9baede4bc4 | ||
|
|
9c6580c24b | ||
|
|
b88b95d885 | ||
|
|
f97c262a1b | ||
|
|
0c435c6a05 | ||
|
|
6a4fffaae7 | ||
|
|
f5a97b63b9 | ||
|
|
6d31834799 | ||
|
|
10b787b852 | ||
|
|
dc61d07206 | ||
|
|
fdd7b14825 | ||
|
|
f4d8033138 | ||
|
|
f48d8fc65e | ||
|
|
f73bc6a82b | ||
|
|
952f04149f | ||
|
|
c872901269 | ||
|
|
3201ad9315 | ||
|
|
befea90c25 | ||
|
|
d3ce2e04fa | ||
|
|
a07ca8ae81 | ||
|
|
dd5d84cc42 | ||
|
|
c900faba81 | ||
|
|
d44d8bedc1 | ||
|
|
99a5866680 | ||
|
|
ebf1a9d9c4 | ||
|
|
5fd9b17b53 | ||
|
|
80ce54cc1d | ||
|
|
71d6d9ec48 | ||
|
|
fce968f893 | ||
|
|
b2f5e28b95 | ||
|
|
2f5c386628 | ||
|
|
b128cc9db4 | ||
|
|
f6b734ca69 | ||
|
|
9964457d09 | ||
|
|
2ae7661810 | ||
|
|
5188d00a09 | ||
|
|
d29bfcca9a | ||
|
|
885944802a | ||
|
|
3dbdd38889 | ||
|
|
69e27bfece | ||
|
|
00a83c56d5 | ||
|
|
0ed8a8dd19 | ||
|
|
99b473e229 | ||
|
|
96625b42ff | ||
|
|
212ec9ca69 | ||
|
|
8de029bd7a | ||
|
|
aa6e4e416b | ||
|
|
5204cb67d8 | ||
|
|
b740e53110 | ||
|
|
4136dd5393 | ||
|
|
5de28e6e7e | ||
|
|
56dd13981c | ||
|
|
3a0ea8992f | ||
|
|
0b552b1697 | ||
|
|
b2194758dc | ||
|
|
c57ee8e471 | ||
|
|
0c7748658c | ||
|
|
cb82b0aac8 | ||
|
|
8d4c3dfa0e | ||
|
|
2d750bc030 | ||
|
|
2a90b3bb70 | ||
|
|
f47a417319 | ||
|
|
b365aab902 | ||
|
|
15efeb572f | ||
|
|
d017409f5b | ||
|
|
de0e9bee20 | ||
|
|
28edca1b63 | ||
|
|
1994302574 | ||
|
|
9b3561fb15 | ||
|
|
9bdc96de8c | ||
|
|
f2692b78dd | ||
|
|
96cc708b5b | ||
|
|
dad94b5c74 | ||
|
|
e79a12fc2b | ||
|
|
0598183f4f | ||
|
|
b950f61b70 | ||
|
|
9d9bc150f6 | ||
|
|
83e2c8f69f | ||
|
|
0b270bf0cc | ||
|
|
63ef986bac | ||
|
|
c87e50b002 | ||
|
|
31697a55b6 | ||
|
|
4b8fcc4b31 | ||
|
|
f2ebffd0ca | ||
|
|
d516eec200 | ||
|
|
4d88af731c | ||
|
|
2c8b9a8323 | ||
|
|
d12817a772 | ||
|
|
c4c6d439bc | ||
|
|
6f706c102e | ||
|
|
66cca1f7bc | ||
|
|
3e1be45739 | ||
|
|
64d87faaf3 | ||
|
|
35f4cdd297 | ||
|
|
147df8ab0b | ||
|
|
9c666d31f4 | ||
|
|
23ead0ea99 | ||
|
|
530b2d9d74 | ||
|
|
a208f876f8 | ||
|
|
7ea163fbe1 | ||
|
|
ab31be8d46 | ||
|
|
d91429c5cf | ||
|
|
c8e13b8ab5 | ||
|
|
2b57af75ea | ||
|
|
545ae79e44 | ||
|
|
c3b47adf39 | ||
|
|
cd89be47b9 | ||
|
|
aeb696687b | ||
|
|
a12d48595e | ||
|
|
28a4a917d4 | ||
|
|
197ae72e93 | ||
|
|
f1c6f276e6 | ||
|
|
b3339c258f | ||
|
|
4795638763 | ||
|
|
c3ecfeae6c | ||
|
|
bcdd204fa0 | ||
|
|
ef162396cf | ||
|
|
ff4769ca1e | ||
|
|
01cf4c663d | ||
|
|
17d7c869ac | ||
|
|
c3ca7960e9 | ||
|
|
eaa506add5 | ||
|
|
70031effa7 | ||
|
|
7a7aee2049 | ||
|
|
4b4dbab1ae | ||
|
|
bcab86fc38 | ||
|
|
937a0c7dee | ||
|
|
b821533b19 | ||
|
|
4ecb53c526 | ||
|
|
a0fef99f8a | ||
|
|
bbd9fcbc84 | ||
|
|
1689cef204 | ||
|
|
9b2e36afc4 | ||
|
|
a635fc8f88 | ||
|
|
1e3421e973 | ||
|
|
efdff73bbf | ||
|
|
6fcdde0617 | ||
|
|
951c08ec4f | ||
|
|
6690396b65 | ||
|
|
f1f5967117 | ||
|
|
a3f0313288 | ||
|
|
bd38457adc | ||
|
|
9b77084ffe | ||
|
|
220ac24bce | ||
|
|
ffe13d1e21 | ||
|
|
fb8953206f | ||
|
|
f9dc9ef5f8 | ||
|
|
ad30f0db89 | ||
|
|
4ff452ffd2 | ||
|
|
c8fbb54fac | ||
|
|
5e226f6581 | ||
|
|
7be453420d | ||
|
|
aa4bff263b | ||
|
|
c10d723c6d | ||
|
|
cdd7a6d753 | ||
|
|
f2929fd2ee | ||
|
|
6660b048a1 | ||
|
|
69b6c0f660 | ||
|
|
145bb6ac7c | ||
|
|
7e59bfa877 | ||
|
|
79eabe4e4c | ||
|
|
03c3d4abfe | ||
|
|
09b855a3a9 | ||
|
|
be910f65be | ||
|
|
6dbf7d8d65 | ||
|
|
34dc49c2f5 | ||
|
|
f113f8afea | ||
|
|
cff10f8c55 | ||
|
|
7ffe6a9063 | ||
|
|
637db138fd | ||
|
|
4fb9913c93 | ||
|
|
e9ba185362 | ||
|
|
44b02afdfd | ||
|
|
d136b09fc2 | ||
|
|
94798f70d3 | ||
|
|
35aa6ba454 | ||
|
|
68a9936566 | ||
|
|
442c618013 | ||
|
|
bc513c2604 | ||
|
|
5988606410 | ||
|
|
de3dd17417 | ||
|
|
dab937d241 | ||
|
|
5ca101b81c | ||
|
|
dc3ab0ee55 | ||
|
|
bf3cc5c13c | ||
|
|
3b56bb69e6 | ||
|
|
b5ebc90667 | ||
|
|
0a01113c83 | ||
|
|
7f89b2a0ea | ||
|
|
f9c7d6e26f | ||
|
|
9f57ea9bb6 | ||
|
|
dae6616720 | ||
|
|
15a246bf75 | ||
|
|
a9404aa8e2 | ||
|
|
65971bb657 | ||
|
|
8a2e41cf47 | ||
|
|
48a6c770fa | ||
|
|
f3e04374e9 | ||
|
|
c07d165ceb | ||
|
|
b117281c58 | ||
|
|
19d897a40b | ||
|
|
b11665b1eb | ||
|
|
e87cc87cbf | ||
|
|
ff41d4f964 | ||
|
|
2030ad946e | ||
|
|
0a756e48b8 | ||
|
|
c8a1013685 | ||
|
|
82a0d80ee7 | ||
|
|
3f5e6e9983 | ||
|
|
bd1ceae375 | ||
|
|
704c41b116 | ||
|
|
c04536b132 | ||
|
|
03589201fb | ||
|
|
8b28e0869c | ||
|
|
391eb5b6f9 | ||
|
|
279f8397f7 | ||
|
|
08bca5a129 | ||
|
|
836956ad87 | ||
|
|
624889bae5 | ||
|
|
f8cc6f45b8 | ||
|
|
0be26498d9 | ||
|
|
5f5ddc32a7 | ||
|
|
d00bfd6243 | ||
|
|
4a60b31ae3 | ||
|
|
1c15eac66a | ||
|
|
9861361aee | ||
|
|
ea78c416d0 | ||
|
|
7ba0df6dfc | ||
|
|
6a7a447df3 | ||
|
|
375834ea46 | ||
|
|
5c339f0757 | ||
|
|
e13ffc8137 | ||
|
|
e268845322 | ||
|
|
67dc3bc658 | ||
|
|
05af5d7ddf | ||
|
|
9535a9a7ad | ||
|
|
4f1cbf72c6 | ||
|
|
296791b56b | ||
|
|
b29b15f3b5 | ||
|
|
821ff036be | ||
|
|
c62ceef1d3 | ||
|
|
f382c799ad | ||
|
|
56f8b1fb81 | ||
|
|
bbf2da962e | ||
|
|
babe201e64 | ||
|
|
cfb9cfa7c7 | ||
|
|
133a294024 | ||
|
|
403e278af2 | ||
|
|
4d9eca1667 | ||
|
|
4a449d85e6 | ||
|
|
bfe8c5e9bf | ||
|
|
121be5eeda | ||
|
|
0aaef2d9a8 | ||
|
|
1c44e2d099 | ||
|
|
9831d14d5e | ||
|
|
4545a08297 | ||
|
|
1dd4544680 | ||
|
|
9e7b5991eb | ||
|
|
6d6f936ba0 | ||
|
|
30ef259c89 | ||
|
|
a19dccf52f | ||
|
|
d25d233cdc | ||
|
|
3a799a6cca | ||
|
|
b8e4515714 | ||
|
|
59343a7263 | ||
|
|
105fd5e9d5 | ||
|
|
47f365d385 | ||
|
|
4cc787b0ec | ||
|
|
db5b247d58 | ||
|
|
dd0a744a3e | ||
|
|
2d0f286a66 | ||
|
|
82799c3ac5 | ||
|
|
535ab137f4 | ||
|
|
c749cda90d | ||
|
|
3fb689ebfa | ||
|
|
390664f2ed | ||
|
|
450ff132e0 | ||
|
|
4d7848fa3b | ||
|
|
f52882ede6 | ||
|
|
a55b45c0ee | ||
|
|
2f5f352f9a | ||
|
|
dc3154b883 | ||
|
|
a0e38f5d51 | ||
|
|
5e44901b45 | ||
|
|
874679e65d | ||
|
|
57f0370ab8 | ||
|
|
dfc633dcee | ||
|
|
2126c36a73 | ||
|
|
83ac391603 | ||
|
|
7e4112a912 | ||
|
|
5e131c9b49 | ||
|
|
b4da1006e9 | ||
|
|
70c3ac27eb | ||
|
|
25fb79776b | ||
|
|
e484002d4a | ||
|
|
d84c0e03d9 | ||
|
|
d2c71a5a47 | ||
|
|
1af62dafa4 | ||
|
|
34b16293ba | ||
|
|
ff23706b10 | ||
|
|
037ea961f2 | ||
|
|
9c7bcd4c2f | ||
|
|
27cde3ec20 | ||
|
|
f08ec77a73 | ||
|
|
2e0ee441cf | ||
|
|
fe6dd0c6eb | ||
|
|
938df62167 | ||
|
|
bfd58e5749 | ||
|
|
e725c97536 | ||
|
|
9dec7b1f66 | ||
|
|
d4c58cb41f | ||
|
|
c078a4e04c | ||
|
|
ab26b7c1ca | ||
|
|
b46a78fb63 | ||
|
|
a40c2e2978 | ||
|
|
259e0f95da | ||
|
|
67ea60a0fa | ||
|
|
5158ca14c9 | ||
|
|
34448c9e89 | ||
|
|
13302c3586 | ||
|
|
efed0a4884 | ||
|
|
e95318092c | ||
|
|
f472c16e8a | ||
|
|
fac0feeeaa | ||
|
|
f902d4b1f6 | ||
|
|
8467f299a0 | ||
|
|
84953bf15b | ||
|
|
2951b7ef28 | ||
|
|
03b84cb0f1 | ||
|
|
582980fa5d | ||
|
|
2406e14e04 | ||
|
|
4195371a1e | ||
|
|
dc0d415b15 | ||
|
|
803ef21305 | ||
|
|
64e631fcdb | ||
|
|
9dd75b4a3b | ||
|
|
8ab8201403 | ||
|
|
99c229524d | ||
|
|
adf4cfd76a | ||
|
|
a46aaf3f11 | ||
|
|
5ef03665bd | ||
|
|
b92d68e4aa | ||
|
|
92cafd766e | ||
|
|
78d00d60f3 | ||
|
|
707311f3d8 | ||
|
|
b7fdd5f70a | ||
|
|
5501649d79 | ||
|
|
43bc4db87c | ||
|
|
25e3807428 | ||
|
|
0cf7c19b96 | ||
|
|
2786174ffb | ||
|
|
4ccd69c782 | ||
|
|
f5e63f42a4 | ||
|
|
245e57de63 | ||
|
|
e7b6a17e42 | ||
|
|
caae9a92a1 | ||
|
|
1c4e41598c | ||
|
|
dd089200ab | ||
|
|
5341da3021 | ||
|
|
da4daee645 | ||
|
|
f6e3575a50 | ||
|
|
87b985ad36 | ||
|
|
8172c4f06a | ||
|
|
637d614618 | ||
|
|
f32e5edcef | ||
|
|
76c0de5f38 | ||
|
|
9608ef917b | ||
|
|
b286689025 | ||
|
|
a4588521a1 | ||
|
|
f1f310ad86 | ||
|
|
edd9bb5da6 | ||
|
|
1b26715cc8 | ||
|
|
e960e6255d | ||
|
|
a362c0788d | ||
|
|
d40581f9d4 | ||
|
|
e6dafd2402 | ||
|
|
1bfd370606 | ||
|
|
087cb64b8c | ||
|
|
2042f6aa67 | ||
|
|
b6708c511b | ||
|
|
6232b62e3e | ||
|
|
03adb9f57e | ||
|
|
a413b34c3e | ||
|
|
0552254c23 | ||
|
|
7d2c9b06de | ||
|
|
e80ca05a2e | ||
|
|
47231c559c | ||
|
|
0e9d552712 | ||
|
|
fbcd8407ce | ||
|
|
df0fb73ea4 | ||
|
|
52711a38a5 | ||
|
|
e7f62c2f20 | ||
|
|
1362225754 | ||
|
|
008919cdfa | ||
|
|
8f8a645568 | ||
|
|
d66c22c4a6 | ||
|
|
0fefa5f2a9 | ||
|
|
810d555abd | ||
|
|
26a5dd6401 | ||
|
|
950c04894e | ||
|
|
8444d18eac | ||
|
|
16d813e7bb | ||
|
|
97d7f0343a | ||
|
|
9424361fc9 | ||
|
|
13e77a781f | ||
|
|
ba5e3e4590 | ||
|
|
badd1ef940 | ||
|
|
b154ed4202 | ||
|
|
af6deab60d | ||
|
|
ed0966cca3 | ||
|
|
b38c181992 | ||
|
|
a2fb252330 | ||
|
|
c83d5e1e59 | ||
|
|
c189503def | ||
|
|
1dae53d9b2 | ||
|
|
d99086c5aa | ||
|
|
84d4a08128 | ||
|
|
3e56641b0a | ||
|
|
25e0151d0f | ||
|
|
e39775bb34 | ||
|
|
9533e312b1 | ||
|
|
a7a2ae2781 | ||
|
|
3de66a37d1 | ||
|
|
eb12c9b005 | ||
|
|
c6c48cb803 | ||
|
|
14627a5c2b | ||
|
|
7148c5d5cd | ||
|
|
c98e849621 | ||
|
|
4a173bbd98 | ||
|
|
123c517f11 | ||
|
|
7ef16996e9 | ||
|
|
cef786bfe2 | ||
|
|
05057a56d9 | ||
|
|
78b7551081 | ||
|
|
fc2c11d095 | ||
|
|
357a334b0b | ||
|
|
44d2ed12e2 | ||
|
|
d01e913354 | ||
|
|
ee8875fd18 | ||
|
|
fdf5555f11 | ||
|
|
bedc81e973 | ||
|
|
f707639392 | ||
|
|
f87b2bee95 | ||
|
|
8c632986a0 | ||
|
|
190a674581 | ||
|
|
cb3b739969 | ||
|
|
43a0a35f22 | ||
|
|
9f73339e58 | ||
|
|
c673adef70 | ||
|
|
a4d5a17f4e | ||
|
|
bb2f765e5d | ||
|
|
8519a596ec | ||
|
|
6dc9e6d3ee | ||
|
|
bfa0e77f78 | ||
|
|
33dc6c53a4 | ||
|
|
048da8c972 | ||
|
|
48d1e6ccb2 | ||
|
|
1a418c74b4 | ||
|
|
df60aa50fe | ||
|
|
26c5ebea53 | ||
|
|
e95bb4c4d7 | ||
|
|
6ddfc80943 | ||
|
|
a421b5be2b | ||
|
|
98ed93133b | ||
|
|
bbc76302a2 | ||
|
|
82a6d529c1 | ||
|
|
9d95d14129 | ||
|
|
73efd4f493 | ||
|
|
9e6ec15f32 | ||
|
|
9f9be9d1a2 | ||
|
|
f3b627e91b | ||
|
|
bbe3d9231e | ||
|
|
13b53efb2a | ||
|
|
1b9ae3ccb3 | ||
|
|
5cc138e23d | ||
|
|
7029524504 | ||
|
|
b79b967036 | ||
|
|
b1510d1107 | ||
|
|
bb5d41147a | ||
|
|
f56a0d1b6d | ||
|
|
a5d8d1990c | ||
|
|
11abe53ef6 | ||
|
|
0606de818d | ||
|
|
237690b876 | ||
|
|
1bfba1e913 | ||
|
|
f88126008b | ||
|
|
597a3dc5b7 | ||
|
|
821bb6fe3f | ||
|
|
e9583c6a47 | ||
|
|
8439b375ed | ||
|
|
1d81a9e40f | ||
|
|
e6d5fe328d | ||
|
|
ec8b73bd9c | ||
|
|
e302ac8059 | ||
|
|
e94d462652 | ||
|
|
2ea2fd4308 | ||
|
|
706fce49b9 | ||
|
|
a79f684e25 | ||
|
|
6291087ecb | ||
|
|
01e0e4b673 | ||
|
|
7eed1a6db3 | ||
|
|
2defaa801b | ||
|
|
caad7a38a4 | ||
|
|
be7d1ab0cc | ||
|
|
fe50ccd39f | ||
|
|
4ca8a7ffcf | ||
|
|
33ad10de29 | ||
|
|
26b6d3a1ec | ||
|
|
b400860538 | ||
|
|
47ade0f135 | ||
|
|
e50530bc44 | ||
|
|
ff3f8f5cc7 | ||
|
|
81dc9f0e8f | ||
|
|
c07c500026 | ||
|
|
1100490c97 | ||
|
|
164a7c71cb | ||
|
|
2ed6d48a1b | ||
|
|
bf71bc27b9 | ||
|
|
3edaefe168 | ||
|
|
70374bda43 | ||
|
|
bce600ba10 | ||
|
|
1300f3922e | ||
|
|
49160f03c0 | ||
|
|
91c2592213 | ||
|
|
8b401a4f6c | ||
|
|
476d93d076 | ||
|
|
2e9ea680c1 | ||
|
|
e202c3bb42 | ||
|
|
2f1265e8cb | ||
|
|
b416c45bbb | ||
|
|
c410784cc9 | ||
|
|
0ae379fd7a | ||
|
|
e922c70c99 | ||
|
|
692048d537 | ||
|
|
69e19fe8ab | ||
|
|
38781d439e | ||
|
|
2aba454318 | ||
|
|
d4a220bddc | ||
|
|
6f75546d15 | ||
|
|
c3ef8bf5cd | ||
|
|
4a9d44ec28 | ||
|
|
16a81047e3 | ||
|
|
71d4f1568f | ||
|
|
2c26ea534f | ||
|
|
6ab538050d | ||
|
|
4a5443996f | ||
|
|
52a81f8fd5 | ||
|
|
09ea11c13d | ||
|
|
28dc7379a6 | ||
|
|
3d9757e5bf | ||
|
|
60ab942882 | ||
|
|
1e28b9efa9 | ||
|
|
a8d36fba8f | ||
|
|
640cf1caf3 | ||
|
|
c0e6751893 | ||
|
|
e6cd184cd6 | ||
|
|
57c6a08b29 | ||
|
|
edb1e692f1 | ||
|
|
49593dd77d | ||
|
|
9ff243a7a9 | ||
|
|
76fb6775d1 | ||
|
|
3f2772c74c | ||
|
|
e4f5ca7ea8 | ||
|
|
7723419142 | ||
|
|
16d66a4206 | ||
|
|
dd9abc1db0 | ||
|
|
420648536a | ||
|
|
02300b65ce | ||
|
|
15abf7bb0f | ||
|
|
505381e760 | ||
|
|
c365bf79a6 | ||
|
|
707bcd9183 | ||
|
|
f9b6b88a96 | ||
|
|
762b436e46 | ||
|
|
8de9b72c49 | ||
|
|
f520fda6fb | ||
|
|
a96ac8c024 | ||
|
|
db3bd12b1e | ||
|
|
12114dce16 | ||
|
|
0f39887cdb | ||
|
|
472b67e930 | ||
|
|
90d67846e6 | ||
|
|
a0e37098bb | ||
|
|
adef6476ec | ||
|
|
8f14e0d024 | ||
|
|
e18d2d2f28 | ||
|
|
40ddf0df0d | ||
|
|
563ec523a3 | ||
|
|
05d4339427 | ||
|
|
81dc3d4404 | ||
|
|
386e2e0736 | ||
|
|
eae5b68f52 | ||
|
|
07675f3039 | ||
|
|
fd3d742d71 | ||
|
|
c9f07fb8ab | ||
|
|
3adbadd808 | ||
|
|
7b3721aa44 | ||
|
|
2ec64ce5a4 | ||
|
|
f88a8cf8fb | ||
|
|
3c72a90f02 | ||
|
|
22463c9eb1 | ||
|
|
1bbd5168c9 | ||
|
|
000866556e | ||
|
|
c9423038da | ||
|
|
1b61aa135e | ||
|
|
7cc8a5a405 | ||
|
|
4c22dd2c82 | ||
|
|
f62d81314c | ||
|
|
b95d0238e2 | ||
|
|
9aaab22f1a | ||
|
|
540878d2a9 | ||
|
|
ae2a901725 | ||
|
|
47afe69cf0 | ||
|
|
38db5a1d49 | ||
|
|
4871cc3243 | ||
|
|
9cf7dc128e | ||
|
|
46f7c010ca | ||
|
|
10c63effd4 | ||
|
|
cc7b27325e | ||
|
|
3a608e5554 | ||
|
|
1092b2696a | ||
|
|
c29199515c | ||
|
|
2f046b00f7 | ||
|
|
973cfff6e2 | ||
|
|
9931245b94 | ||
|
|
25bfd1bbc4 | ||
|
|
63191be183 | ||
|
|
66c6e532f6 | ||
|
|
32baa7f146 | ||
|
|
87a332ee81 | ||
|
|
80248981dd | ||
|
|
9421da7b22 | ||
|
|
3537fad8db | ||
|
|
8edd667ff4 | ||
|
|
cbaea166c4 | ||
|
|
91e65867bb | ||
|
|
b4a95d20e2 | ||
|
|
dfb4c9f726 | ||
|
|
bd02676dbc | ||
|
|
b4f8088f0f | ||
|
|
04ab9d75a9 | ||
|
|
a65b074e9f | ||
|
|
cef6a583b7 | ||
|
|
8e3bc8b8a7 | ||
|
|
3d6172c544 | ||
|
|
96c69ef224 | ||
|
|
03e3c8fb70 | ||
|
|
99055e0cd8 | ||
|
|
071e913a08 | ||
|
|
053cbc1b35 | ||
|
|
98f48ba067 | ||
|
|
67c05c9c37 | ||
|
|
5d46bba216 | ||
|
|
4a11fa61ed | ||
|
|
b0d3641b29 | ||
|
|
ca3dac9a25 | ||
|
|
e29ef738f2 | ||
|
|
e2b89e5ae2 | ||
|
|
d759eb8b7c | ||
|
|
1509f37f62 | ||
|
|
0ce707538a | ||
|
|
be2504ce82 | ||
|
|
3faa233056 | ||
|
|
72201049f0 | ||
|
|
d91856d824 | ||
|
|
f46c53b77e | ||
|
|
4f1e31999d | ||
|
|
ad4a32762d | ||
|
|
2b50f44ab2 | ||
|
|
43c94c7be2 | ||
|
|
6329eee92a | ||
|
|
6bd0da059f | ||
|
|
84b1ebd2c8 | ||
|
|
0fa13379e4 | ||
|
|
b54458c433 | ||
|
|
2252ac9592 | ||
|
|
180905ecba | ||
|
|
ed85e4e24c | ||
|
|
d0f52a2b78 | ||
|
|
1f40e97dd8 | ||
|
|
33c3ceb361 | ||
|
|
8d1fb577e2 | ||
|
|
4f8526dacb | ||
|
|
76d8be8818 | ||
|
|
dbb6980e96 | ||
|
|
29100d5250 | ||
|
|
8cce210d64 | ||
|
|
54101378e3 | ||
|
|
ed6fde5e4d | ||
|
|
6a3e01b3cc | ||
|
|
5417965e38 | ||
|
|
8381eb96d5 | ||
|
|
c95cca5efc | ||
|
|
e1071574e6 | ||
|
|
f6be1cecfe | ||
|
|
ef8995006d | ||
|
|
20ba112bd8 | ||
|
|
3bc98eec16 | ||
|
|
184d593d27 | ||
|
|
629f2f000f | ||
|
|
48d2285d36 | ||
|
|
91dd4703e2 | ||
|
|
a920204f3e | ||
|
|
f1004a1324 | ||
|
|
42abe26bd7 | ||
|
|
83590cb742 | ||
|
|
3e34718bfe | ||
|
|
1a4498a58c | ||
|
|
b88c9a5ae1 | ||
|
|
bfa73f630d | ||
|
|
5b0342526d | ||
|
|
ef870a6ac2 | ||
|
|
bc9868f926 | ||
|
|
c71e86b8d1 | ||
|
|
f9ab3e24b1 | ||
|
|
1f1e73ce08 | ||
|
|
60d02f3afd | ||
|
|
2b3f03eae9 | ||
|
|
175482371c | ||
|
|
b715eb530a | ||
|
|
bac129069b | ||
|
|
5e70d8b841 | ||
|
|
61cf686527 | ||
|
|
c25cf508ff | ||
|
|
f4fea92cea | ||
|
|
210d7a4d59 | ||
|
|
266d5618ae | ||
|
|
2dd46203ee | ||
|
|
7f35685b35 | ||
|
|
ff52b376fe | ||
|
|
dc6f485aca | ||
|
|
ff053c36e5 | ||
|
|
2339bdcbdf | ||
|
|
a2b7c4b245 | ||
|
|
f593cce6c9 | ||
|
|
4eec956905 | ||
|
|
aa8135a1ad | ||
|
|
35f0d7a82a | ||
|
|
1aa9af8081 | ||
|
|
7e108f167d | ||
|
|
c494bd56ed | ||
|
|
8d1447e08c | ||
|
|
fc533c7b2b | ||
|
|
c930fb45f1 | ||
|
|
f7e7f6e257 | ||
|
|
32b64e74d0 | ||
|
|
0e7d3811c1 | ||
|
|
72b754881e | ||
|
|
2935945585 | ||
|
|
ced17f6deb | ||
|
|
da627f24a3 | ||
|
|
df8cdfeaef | ||
|
|
21c414a5e9 | ||
|
|
8c24e38ee1 | ||
|
|
39c3a72cc0 | ||
|
|
29f4eaf94f | ||
|
|
a8e546c9af | ||
|
|
38fafed013 | ||
|
|
78f3944b5d | ||
|
|
84b7b86ed5 | ||
|
|
d93bc39d8d | ||
|
|
9370cdc88b | ||
|
|
cfd6e2b32f | ||
|
|
0a373bc260 | ||
|
|
939d37822d | ||
|
|
2058f765b0 | ||
|
|
7f8854d7f5 | ||
|
|
0080383c40 | ||
|
|
d183dd6e91 | ||
|
|
b9280ab62d | ||
|
|
a8d9970500 | ||
|
|
f289e75651 | ||
|
|
05056f6b8d | ||
|
|
df4504eb06 | ||
|
|
edf56c122f | ||
|
|
25e98cd1e2 | ||
|
|
077076a547 | ||
|
|
de09015154 | ||
|
|
11c3c093c8 | ||
|
|
adae1143cd | ||
|
|
022fed41a4 | ||
|
|
9137ff4a15 | ||
|
|
1b64b5fa80 | ||
|
|
6a7b719725 | ||
|
|
5677839d48 | ||
|
|
10642afabf | ||
|
|
d2bc9e0784 | ||
|
|
278aae8fff | ||
|
|
d1b1e896f6 | ||
|
|
49d3d77fd6 | ||
|
|
299e8f7a2c | ||
|
|
f5263cf8d7 | ||
|
|
9f4c49c07d | ||
|
|
81757e7182 | ||
|
|
71ff5aa253 | ||
|
|
5411691fb2 | ||
|
|
82bca87418 | ||
|
|
b007903616 | ||
|
|
24b3afbe3d | ||
|
|
7dacc47f16 | ||
|
|
669808069d | ||
|
|
d73499ac79 | ||
|
|
c36c145b34 | ||
|
|
4735cedba2 | ||
|
|
7b8c11ecf3 | ||
|
|
a5f9b392ed | ||
|
|
4263772246 | ||
|
|
c6f804bebf | ||
|
|
07c11b2097 | ||
|
|
6165ac334b | ||
|
|
bb81b7b8d9 | ||
|
|
5d3cc2146b | ||
|
|
d378753f7c | ||
|
|
1c1e51e082 | ||
|
|
c130037574 | ||
|
|
4b289122c1 | ||
|
|
449f8633cf | ||
|
|
b53056fe09 | ||
|
|
7bae23ce85 | ||
|
|
07c6905146 | ||
|
|
ac4fe6cd1d | ||
|
|
1b60d05434 | ||
|
|
22c85612c7 | ||
|
|
815bc126a7 | ||
|
|
111cd16827 | ||
|
|
89cc7641cb | ||
|
|
1a6fa86fc2 | ||
|
|
fc04e08873 | ||
|
|
73ee65e3a7 | ||
|
|
e8685c977f | ||
|
|
a487dcdfa5 | ||
|
|
d84f85e636 | ||
|
|
f991e7b5f3 | ||
|
|
3e77ca6497 | ||
|
|
98b4422956 | ||
|
|
59f63b3bf9 | ||
|
|
c4775920f4 | ||
|
|
e3fe26ae72 | ||
|
|
87930d0f7d | ||
|
|
13b9c0ef34 | ||
|
|
365aa7e368 | ||
|
|
5880f9fd30 | ||
|
|
278f2c3239 | ||
|
|
d69439a11a | ||
|
|
6812c94cc6 | ||
|
|
563f4d79a3 | ||
|
|
1db958ec53 | ||
|
|
38216648d4 | ||
|
|
a0649be28d | ||
|
|
62b1f34472 | ||
|
|
c71e62e810 | ||
|
|
72565fa493 | ||
|
|
98df9b91ba | ||
|
|
edb2016b28 | ||
|
|
91c3fc2fe1 | ||
|
|
9db3a04275 | ||
|
|
e9f9af0e7a | ||
|
|
c6aa724060 | ||
|
|
0cc9970184 | ||
|
|
7d0d95b298 | ||
|
|
18b07128fc | ||
|
|
735c08caf4 | ||
|
|
2b7005ad49 | ||
|
|
f9462162a5 | ||
|
|
b33a788c20 | ||
|
|
bbc8986917 | ||
|
|
38b62d9d6c | ||
|
|
1234db75b1 | ||
|
|
3c70c3c6e8 | ||
|
|
65b883ace9 | ||
|
|
fb949bec56 | ||
|
|
a225663049 | ||
|
|
d1a62f6697 | ||
|
|
5eb6d52daf | ||
|
|
ace3fbdfa0 | ||
|
|
e013d33691 | ||
|
|
9db300035a | ||
|
|
de438cc66f | ||
|
|
e2f3721459 | ||
|
|
064a180b3a | ||
|
|
4c7c3d4c96 | ||
|
|
a9e515e5c0 | ||
|
|
3463fbec81 | ||
|
|
8d820c3096 | ||
|
|
fd626ec94d | ||
|
|
b0804ac7f0 | ||
|
|
732a9d0712 | ||
|
|
03572ebcab | ||
|
|
2681bbd08f | ||
|
|
918a263fd0 | ||
|
|
57a10e87b6 | ||
|
|
faeb477880 | ||
|
|
d3714de65b | ||
|
|
0e4e574bd4 | ||
|
|
92d6b07578 | ||
|
|
0a9f06096b | ||
|
|
069c5377be | ||
|
|
9161b8fffe |
927
.cursorrules
Normal file
@@ -0,0 +1,927 @@
|
|||||||
|
# RustFS Project Cursor Rules
|
||||||
|
|
||||||
|
## 🚨🚨🚨 CRITICAL DEVELOPMENT RULES - ZERO TOLERANCE 🚨🚨🚨
|
||||||
|
|
||||||
|
### ⛔️ ABSOLUTE PROHIBITION: NEVER COMMIT DIRECTLY TO MASTER/MAIN BRANCH ⛔️
|
||||||
|
|
||||||
|
**🔥 THIS IS THE MOST CRITICAL RULE - VIOLATION WILL RESULT IN IMMEDIATE REVERSAL 🔥**
|
||||||
|
|
||||||
|
- **🚫 ZERO DIRECT COMMITS TO MAIN/MASTER BRANCH - ABSOLUTELY FORBIDDEN**
|
||||||
|
- **🚫 ANY DIRECT COMMIT TO MAIN BRANCH MUST BE IMMEDIATELY REVERTED**
|
||||||
|
- **🚫 NO EXCEPTIONS FOR HOTFIXES, EMERGENCIES, OR URGENT CHANGES**
|
||||||
|
- **🚫 NO EXCEPTIONS FOR SMALL CHANGES, TYPOS, OR DOCUMENTATION UPDATES**
|
||||||
|
- **🚫 NO EXCEPTIONS FOR ANYONE - MAINTAINERS, CONTRIBUTORS, OR ADMINS**
|
||||||
|
|
||||||
|
### 📋 MANDATORY WORKFLOW - STRICTLY ENFORCED
|
||||||
|
|
||||||
|
**EVERY SINGLE CHANGE MUST FOLLOW THIS WORKFLOW:**
|
||||||
|
|
||||||
|
1. **Check current branch**: `git branch` (MUST NOT be on main/master)
|
||||||
|
2. **Switch to main**: `git checkout main`
|
||||||
|
3. **Pull latest**: `git pull origin main`
|
||||||
|
4. **Create feature branch**: `git checkout -b feat/your-feature-name`
|
||||||
|
5. **Make changes ONLY on feature branch**
|
||||||
|
6. **Test thoroughly before committing**
|
||||||
|
7. **Commit and push to feature branch**: `git push origin feat/your-feature-name`
|
||||||
|
8. **Create Pull Request**: Use `gh pr create` (MANDATORY)
|
||||||
|
9. **Wait for PR approval**: NO self-merging allowed
|
||||||
|
10. **Merge through GitHub interface**: ONLY after approval
|
||||||
|
|
||||||
|
### 🔒 ENFORCEMENT MECHANISMS
|
||||||
|
|
||||||
|
- **Branch protection rules**: Main branch is protected
|
||||||
|
- **Pre-commit hooks**: Will block direct commits to main
|
||||||
|
- **CI/CD checks**: All PRs must pass before merging
|
||||||
|
- **Code review requirement**: At least one approval needed
|
||||||
|
- **Automated reversal**: Direct commits to main will be automatically reverted
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
RustFS is a high-performance distributed object storage system written in Rust, compatible with S3 API. The project adopts a modular architecture, supporting erasure coding storage, multi-tenant management, observability, and other enterprise-level features.
|
||||||
|
|
||||||
|
## Core Architecture Principles
|
||||||
|
|
||||||
|
### 1. Modular Design
|
||||||
|
|
||||||
|
- Project uses Cargo workspace structure, containing multiple independent crates
|
||||||
|
- Core modules: `rustfs` (main service), `ecstore` (erasure coding storage), `common` (shared components)
|
||||||
|
- Functional modules: `iam` (identity management), `madmin` (management interface), `crypto` (encryption), etc.
|
||||||
|
- Tool modules: `cli` (command line tool), `crates/*` (utility libraries)
|
||||||
|
|
||||||
|
### 2. Asynchronous Programming Pattern
|
||||||
|
|
||||||
|
- Comprehensive use of `tokio` async runtime
|
||||||
|
- Prioritize `async/await` syntax
|
||||||
|
- Use `async-trait` for async methods in traits
|
||||||
|
- Avoid blocking operations, use `spawn_blocking` when necessary
|
||||||
|
|
||||||
|
### 3. Error Handling Strategy
|
||||||
|
|
||||||
|
- **Use modular, type-safe error handling with `thiserror`**
|
||||||
|
- Each module should define its own error type using `thiserror::Error` derive macro
|
||||||
|
- Support error chains and context information through `#[from]` and `#[source]` attributes
|
||||||
|
- Use `Result<T>` type aliases for consistency within each module
|
||||||
|
- Error conversion between modules should use explicit `From` implementations
|
||||||
|
- Follow the pattern: `pub type Result<T> = core::result::Result<T, Error>`
|
||||||
|
- Use `#[error("description")]` attributes for clear error messages
|
||||||
|
- Support error downcasting when needed through `other()` helper methods
|
||||||
|
- Implement `Clone` for errors when required by the domain logic
|
||||||
|
- **Current module error types:**
|
||||||
|
- `ecstore::error::StorageError` - Storage layer errors
|
||||||
|
- `ecstore::disk::error::DiskError` - Disk operation errors
|
||||||
|
- `iam::error::Error` - Identity and access management errors
|
||||||
|
- `policy::error::Error` - Policy-related errors
|
||||||
|
- `crypto::error::Error` - Cryptographic operation errors
|
||||||
|
- `filemeta::error::Error` - File metadata errors
|
||||||
|
- `rustfs::error::ApiError` - API layer errors
|
||||||
|
- Module-specific error types for specialized functionality
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
### 1. Formatting Configuration
|
||||||
|
|
||||||
|
```toml
|
||||||
|
max_width = 130
|
||||||
|
fn_call_width = 90
|
||||||
|
single_line_let_else_max_width = 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **🔧 MANDATORY Code Formatting Rules**
|
||||||
|
|
||||||
|
**CRITICAL**: All code must be properly formatted before committing. This project enforces strict formatting standards to maintain code consistency and readability.
|
||||||
|
|
||||||
|
#### Pre-commit Requirements (MANDATORY)
|
||||||
|
|
||||||
|
Before every commit, you **MUST**:
|
||||||
|
|
||||||
|
1. **Format your code**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify formatting**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all --check
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Pass clippy checks**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Ensure compilation**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo check --all-targets
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Quick Commands
|
||||||
|
|
||||||
|
Use these convenient Makefile targets for common tasks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format all code
|
||||||
|
make fmt
|
||||||
|
|
||||||
|
# Check if code is properly formatted
|
||||||
|
make fmt-check
|
||||||
|
|
||||||
|
# Run clippy checks
|
||||||
|
make clippy
|
||||||
|
|
||||||
|
# Run compilation check
|
||||||
|
make check
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Run all pre-commit checks (format + clippy + check + test)
|
||||||
|
make pre-commit
|
||||||
|
|
||||||
|
# Setup git hooks (one-time setup)
|
||||||
|
make setup-hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🔒 Automated Pre-commit Hooks
|
||||||
|
|
||||||
|
This project includes a pre-commit hook that automatically runs before each commit to ensure:
|
||||||
|
|
||||||
|
- ✅ Code is properly formatted (`cargo fmt --all --check`)
|
||||||
|
- ✅ No clippy warnings (`cargo clippy --all-targets --all-features -- -D warnings`)
|
||||||
|
- ✅ Code compiles successfully (`cargo check --all-targets`)
|
||||||
|
|
||||||
|
**Setting Up Pre-commit Hooks** (MANDATORY for all developers):
|
||||||
|
|
||||||
|
Run this command once after cloning the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make setup-hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🚫 Commit Prevention
|
||||||
|
|
||||||
|
If your code doesn't meet the formatting requirements, the pre-commit hook will:
|
||||||
|
|
||||||
|
1. **Block the commit** and show clear error messages
|
||||||
|
2. **Provide exact commands** to fix the issues
|
||||||
|
3. **Guide you through** the resolution process
|
||||||
|
|
||||||
|
Example output when formatting fails:
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Code formatting check failed!
|
||||||
|
💡 Please run 'cargo fmt --all' to format your code before committing.
|
||||||
|
|
||||||
|
🔧 Quick fix:
|
||||||
|
cargo fmt --all
|
||||||
|
git add .
|
||||||
|
git commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Naming Conventions
|
||||||
|
|
||||||
|
- Use `snake_case` for functions, variables, modules
|
||||||
|
- Use `PascalCase` for types, traits, enums
|
||||||
|
- Constants use `SCREAMING_SNAKE_CASE`
|
||||||
|
- Global variables prefix `GLOBAL_`, e.g., `GLOBAL_Endpoints`
|
||||||
|
- Use meaningful and descriptive names for variables, functions, and methods
|
||||||
|
- Avoid meaningless names like `temp`, `data`, `foo`, `bar`, `test123`
|
||||||
|
- Choose names that clearly express the purpose and intent
|
||||||
|
|
||||||
|
### 4. Type Declaration Guidelines
|
||||||
|
|
||||||
|
- **Prefer type inference over explicit type declarations** when the type is obvious from context
|
||||||
|
- Let the Rust compiler infer types whenever possible to reduce verbosity and improve maintainability
|
||||||
|
- Only specify types explicitly when:
|
||||||
|
- The type cannot be inferred by the compiler
|
||||||
|
- Explicit typing improves code clarity and readability
|
||||||
|
- Required for API boundaries (function signatures, public struct fields)
|
||||||
|
- Needed to resolve ambiguity between multiple possible types
|
||||||
|
|
||||||
|
**Good examples (prefer these):**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Compiler can infer the type
|
||||||
|
let items = vec![1, 2, 3, 4];
|
||||||
|
let config = Config::default();
|
||||||
|
let result = process_data(&input);
|
||||||
|
|
||||||
|
// Iterator chains with clear context
|
||||||
|
let filtered: Vec<_> = items.iter().filter(|&&x| x > 2).collect();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Avoid unnecessary explicit types:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Unnecessary - type is obvious
|
||||||
|
let items: Vec<i32> = vec![1, 2, 3, 4];
|
||||||
|
let config: Config = Config::default();
|
||||||
|
let result: ProcessResult = process_data(&input);
|
||||||
|
```
|
||||||
|
|
||||||
|
**When explicit types are beneficial:**
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// API boundaries - always specify types
|
||||||
|
pub fn process_data(input: &[u8]) -> Result<ProcessResult, Error> { ... }
|
||||||
|
|
||||||
|
// Ambiguous cases - explicit type needed
|
||||||
|
let value: f64 = "3.14".parse().unwrap();
|
||||||
|
|
||||||
|
// Complex generic types - explicit for clarity
|
||||||
|
let cache: HashMap<String, Arc<Mutex<CacheEntry>>> = HashMap::new();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Documentation Comments
|
||||||
|
|
||||||
|
- Public APIs must have documentation comments
|
||||||
|
- Use `///` for documentation comments
|
||||||
|
- Complex functions add `# Examples` and `# Parameters` descriptions
|
||||||
|
- Error cases use `# Errors` descriptions
|
||||||
|
- Always use English for all comments and documentation
|
||||||
|
- Avoid meaningless comments like "debug 111" or placeholder text
|
||||||
|
|
||||||
|
### 6. Import Guidelines
|
||||||
|
|
||||||
|
- Standard library imports first
|
||||||
|
- Third-party crate imports in the middle
|
||||||
|
- Project internal imports last
|
||||||
|
- Group `use` statements with blank lines between groups
|
||||||
|
|
||||||
|
## Asynchronous Programming Guidelines
|
||||||
|
|
||||||
|
### 1. Trait Definition
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait StorageAPI: Send + Sync {
|
||||||
|
async fn get_object(&self, bucket: &str, object: &str) -> Result<ObjectInfo>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Error Handling
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Use ? operator to propagate errors
|
||||||
|
async fn example_function() -> Result<()> {
|
||||||
|
let data = read_file("path").await?;
|
||||||
|
process_data(data).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Concurrency Control
|
||||||
|
|
||||||
|
- Use `Arc` and `Mutex`/`RwLock` for shared state management
|
||||||
|
- Prioritize async locks from `tokio::sync`
|
||||||
|
- Avoid holding locks for long periods
|
||||||
|
|
||||||
|
## Logging and Tracing Guidelines
|
||||||
|
|
||||||
|
### 1. Tracing Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[tracing::instrument(skip(self, data))]
|
||||||
|
async fn process_data(&self, data: &[u8]) -> Result<()> {
|
||||||
|
info!("Processing {} bytes", data.len());
|
||||||
|
// Implementation logic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Log Levels
|
||||||
|
|
||||||
|
- `error!`: System errors requiring immediate attention
|
||||||
|
- `warn!`: Warning information that may affect functionality
|
||||||
|
- `info!`: Important business information
|
||||||
|
- `debug!`: Debug information for development use
|
||||||
|
- `trace!`: Detailed execution paths
|
||||||
|
|
||||||
|
### 3. Structured Logging
|
||||||
|
|
||||||
|
```rust
|
||||||
|
info!(
|
||||||
|
counter.rustfs_api_requests_total = 1_u64,
|
||||||
|
key_request_method = %request.method(),
|
||||||
|
key_request_uri_path = %request.uri().path(),
|
||||||
|
"API request processed"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling Guidelines
|
||||||
|
|
||||||
|
### 1. Error Type Definition
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Use thiserror for module-specific error types
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum MyError {
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("Storage error: {0}")]
|
||||||
|
Storage(#[from] ecstore::error::StorageError),
|
||||||
|
|
||||||
|
#[error("Custom error: {message}")]
|
||||||
|
Custom { message: String },
|
||||||
|
|
||||||
|
#[error("File not found: {path}")]
|
||||||
|
FileNotFound { path: String },
|
||||||
|
|
||||||
|
#[error("Invalid configuration: {0}")]
|
||||||
|
InvalidConfig(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide Result type alias for the module
|
||||||
|
pub type Result<T> = core::result::Result<T, MyError>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Error Helper Methods
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl MyError {
|
||||||
|
/// Create error from any compatible error type
|
||||||
|
pub fn other<E>(error: E) -> Self
|
||||||
|
where
|
||||||
|
E: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||||
|
{
|
||||||
|
MyError::Io(std::io::Error::other(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Error Conversion Between Modules
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Convert between different module error types
|
||||||
|
impl From<ecstore::error::StorageError> for MyError {
|
||||||
|
fn from(e: ecstore::error::StorageError) -> Self {
|
||||||
|
match e {
|
||||||
|
ecstore::error::StorageError::FileNotFound => {
|
||||||
|
MyError::FileNotFound { path: "unknown".to_string() }
|
||||||
|
}
|
||||||
|
_ => MyError::Storage(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide reverse conversion when needed
|
||||||
|
impl From<MyError> for ecstore::error::StorageError {
|
||||||
|
fn from(e: MyError) -> Self {
|
||||||
|
match e {
|
||||||
|
MyError::FileNotFound { .. } => ecstore::error::StorageError::FileNotFound,
|
||||||
|
MyError::Storage(e) => e,
|
||||||
|
_ => ecstore::error::StorageError::other(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Error Context and Propagation
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Use ? operator for clean error propagation
|
||||||
|
async fn example_function() -> Result<()> {
|
||||||
|
let data = read_file("path").await?;
|
||||||
|
process_data(data).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add context to errors
|
||||||
|
fn process_with_context(path: &str) -> Result<()> {
|
||||||
|
std::fs::read(path)
|
||||||
|
.map_err(|e| MyError::Custom {
|
||||||
|
message: format!("Failed to read {}: {}", path, e)
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. API Error Conversion (S3 Example)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Convert storage errors to API-specific errors
|
||||||
|
use s3s::{S3Error, S3ErrorCode};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ApiError {
|
||||||
|
pub code: S3ErrorCode,
|
||||||
|
pub message: String,
|
||||||
|
pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ecstore::error::StorageError> for ApiError {
|
||||||
|
fn from(err: ecstore::error::StorageError) -> Self {
|
||||||
|
let code = match &err {
|
||||||
|
ecstore::error::StorageError::BucketNotFound(_) => S3ErrorCode::NoSuchBucket,
|
||||||
|
ecstore::error::StorageError::ObjectNotFound(_, _) => S3ErrorCode::NoSuchKey,
|
||||||
|
ecstore::error::StorageError::BucketExists(_) => S3ErrorCode::BucketAlreadyExists,
|
||||||
|
ecstore::error::StorageError::InvalidArgument(_, _, _) => S3ErrorCode::InvalidArgument,
|
||||||
|
ecstore::error::StorageError::MethodNotAllowed => S3ErrorCode::MethodNotAllowed,
|
||||||
|
ecstore::error::StorageError::StorageFull => S3ErrorCode::ServiceUnavailable,
|
||||||
|
_ => S3ErrorCode::InternalError,
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiError {
|
||||||
|
code,
|
||||||
|
message: err.to_string(),
|
||||||
|
source: Some(Box::new(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ApiError> for S3Error {
|
||||||
|
fn from(err: ApiError) -> Self {
|
||||||
|
let mut s3e = S3Error::with_message(err.code, err.message);
|
||||||
|
if let Some(source) = err.source {
|
||||||
|
s3e.set_source(source);
|
||||||
|
}
|
||||||
|
s3e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Error Handling Best Practices
|
||||||
|
|
||||||
|
#### Pattern Matching and Error Classification
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Use pattern matching for specific error handling
|
||||||
|
async fn handle_storage_operation() -> Result<()> {
|
||||||
|
match storage.get_object("bucket", "key").await {
|
||||||
|
Ok(object) => process_object(object),
|
||||||
|
Err(ecstore::error::StorageError::ObjectNotFound(bucket, key)) => {
|
||||||
|
warn!("Object not found: {}/{}", bucket, key);
|
||||||
|
create_default_object(bucket, key).await
|
||||||
|
}
|
||||||
|
Err(ecstore::error::StorageError::BucketNotFound(bucket)) => {
|
||||||
|
error!("Bucket not found: {}", bucket);
|
||||||
|
Err(MyError::Custom {
|
||||||
|
message: format!("Bucket {} does not exist", bucket)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Storage operation failed: {}", e);
|
||||||
|
Err(MyError::Storage(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Error Aggregation and Reporting
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Collect and report multiple errors
|
||||||
|
pub fn validate_configuration(config: &Config) -> Result<()> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
if config.bucket_name.is_empty() {
|
||||||
|
errors.push("Bucket name cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.region.is_empty() {
|
||||||
|
errors.push("Region must be specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errors.is_empty() {
|
||||||
|
return Err(MyError::Custom {
|
||||||
|
message: format!("Configuration validation failed: {}", errors.join(", "))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Contextual Error Information
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Add operation context to errors
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
async fn upload_file(&self, bucket: &str, key: &str, data: Vec<u8>) -> Result<()> {
|
||||||
|
self.storage
|
||||||
|
.put_object(bucket, key, data)
|
||||||
|
.await
|
||||||
|
.map_err(|e| MyError::Custom {
|
||||||
|
message: format!("Failed to upload {}/{}: {}", bucket, key, e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization Guidelines
|
||||||
|
|
||||||
|
### 1. Memory Management
|
||||||
|
|
||||||
|
- Use `Bytes` instead of `Vec<u8>` for zero-copy operations
|
||||||
|
- Avoid unnecessary cloning, use reference passing
|
||||||
|
- Use `Arc` for sharing large objects
|
||||||
|
|
||||||
|
### 2. Concurrency Optimization
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Use join_all for concurrent operations
|
||||||
|
let futures = disks.iter().map(|disk| disk.operation());
|
||||||
|
let results = join_all(futures).await;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Caching Strategy
|
||||||
|
|
||||||
|
- Use `LazyLock` for global caching
|
||||||
|
- Implement LRU cache to avoid memory leaks
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
### 1. Unit Tests
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use test_case::test_case;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_async_function() {
|
||||||
|
let result = async_function().await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_case("input1", "expected1")]
|
||||||
|
#[test_case("input2", "expected2")]
|
||||||
|
fn test_with_cases(input: &str, expected: &str) {
|
||||||
|
assert_eq!(function(input), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_conversion() {
|
||||||
|
use ecstore::error::StorageError;
|
||||||
|
|
||||||
|
let storage_err = StorageError::BucketNotFound("test-bucket".to_string());
|
||||||
|
let api_err: ApiError = storage_err.into();
|
||||||
|
|
||||||
|
assert_eq!(api_err.code, S3ErrorCode::NoSuchBucket);
|
||||||
|
assert!(api_err.message.contains("test-bucket"));
|
||||||
|
assert!(api_err.source.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_types() {
|
||||||
|
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
|
||||||
|
let my_err = MyError::Io(io_err);
|
||||||
|
|
||||||
|
// Test error matching
|
||||||
|
match my_err {
|
||||||
|
MyError::Io(_) => {}, // Expected
|
||||||
|
_ => panic!("Unexpected error type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_context() {
|
||||||
|
let result = process_with_context("nonexistent_file.txt");
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
let err = result.unwrap_err();
|
||||||
|
match err {
|
||||||
|
MyError::Custom { message } => {
|
||||||
|
assert!(message.contains("Failed to read"));
|
||||||
|
assert!(message.contains("nonexistent_file.txt"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Custom error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Integration Tests
|
||||||
|
|
||||||
|
- Use `e2e_test` module for end-to-end testing
|
||||||
|
- Simulate real storage environments
|
||||||
|
|
||||||
|
### 3. Test Quality Standards
|
||||||
|
|
||||||
|
- Write meaningful test cases that verify actual functionality
|
||||||
|
- Avoid placeholder or debug content like "debug 111", "test test", etc.
|
||||||
|
- Use descriptive test names that clearly indicate what is being tested
|
||||||
|
- Each test should have a clear purpose and verify specific behavior
|
||||||
|
- Test data should be realistic and representative of actual use cases
|
||||||
|
|
||||||
|
## Cross-Platform Compatibility Guidelines
|
||||||
|
|
||||||
|
### 1. CPU Architecture Compatibility
|
||||||
|
|
||||||
|
- **Always consider multi-platform and different CPU architecture compatibility** when writing code
|
||||||
|
- Support major architectures: x86_64, aarch64 (ARM64), and other target platforms
|
||||||
|
- Use conditional compilation for architecture-specific code:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(target_arch = "x86_64")]
|
||||||
|
fn optimized_x86_64_function() { /* x86_64 specific implementation */ }
|
||||||
|
|
||||||
|
#[cfg(target_arch = "aarch64")]
|
||||||
|
fn optimized_aarch64_function() { /* ARM64 specific implementation */ }
|
||||||
|
|
||||||
|
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
||||||
|
fn generic_function() { /* Generic fallback implementation */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Platform-Specific Dependencies
|
||||||
|
|
||||||
|
- Use feature flags for platform-specific dependencies
|
||||||
|
- Provide fallback implementations for unsupported platforms
|
||||||
|
- Test on multiple architectures in CI/CD pipeline
|
||||||
|
|
||||||
|
### 3. Endianness Considerations
|
||||||
|
|
||||||
|
- Use explicit byte order conversion when dealing with binary data
|
||||||
|
- Prefer `to_le_bytes()`, `from_le_bytes()` for consistent little-endian format
|
||||||
|
- Use `byteorder` crate for complex binary format handling
|
||||||
|
|
||||||
|
### 4. SIMD and Performance Optimizations
|
||||||
|
|
||||||
|
- Use portable SIMD libraries like `wide` or `packed_simd`
|
||||||
|
- Provide fallback implementations for non-SIMD architectures
|
||||||
|
- Use runtime feature detection when appropriate
|
||||||
|
|
||||||
|
## Security Guidelines
|
||||||
|
|
||||||
|
### 1. Memory Safety
|
||||||
|
|
||||||
|
- Disable `unsafe` code (workspace.lints.rust.unsafe_code = "deny")
|
||||||
|
- Use `rustls` instead of `openssl`
|
||||||
|
|
||||||
|
### 2. Authentication and Authorization
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Use IAM system for permission checks
|
||||||
|
let identity = iam.authenticate(&access_key, &secret_key).await?;
|
||||||
|
iam.authorize(&identity, &action, &resource).await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Management Guidelines
|
||||||
|
|
||||||
|
### 1. Environment Variables
|
||||||
|
|
||||||
|
- Use `RUSTFS_` prefix
|
||||||
|
- Support both configuration files and environment variables
|
||||||
|
- Provide reasonable default values
|
||||||
|
|
||||||
|
### 2. Configuration Structure
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub address: String,
|
||||||
|
pub volumes: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub console_enable: bool,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Management Guidelines
|
||||||
|
|
||||||
|
### 1. Workspace Dependencies
|
||||||
|
|
||||||
|
- Manage versions uniformly at workspace level
|
||||||
|
- Use `workspace = true` to inherit configuration
|
||||||
|
|
||||||
|
### 2. Feature Flags
|
||||||
|
|
||||||
|
```rust
|
||||||
|
[features]
|
||||||
|
default = ["file"]
|
||||||
|
gpu = ["dep:nvml-wrapper"]
|
||||||
|
kafka = ["dep:rdkafka"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment and Operations Guidelines
|
||||||
|
|
||||||
|
### 1. Containerization
|
||||||
|
|
||||||
|
- Provide Dockerfile and docker-compose configuration
|
||||||
|
- Support multi-stage builds to optimize image size
|
||||||
|
|
||||||
|
### 2. Observability
|
||||||
|
|
||||||
|
- Integrate OpenTelemetry for distributed tracing
|
||||||
|
- Support Prometheus metrics collection
|
||||||
|
- Provide Grafana dashboards
|
||||||
|
|
||||||
|
### 3. Health Checks
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Implement health check endpoint
|
||||||
|
async fn health_check() -> Result<HealthStatus> {
|
||||||
|
// Check component status
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Review Checklist
|
||||||
|
|
||||||
|
### 1. **Code Formatting and Quality (MANDATORY)**
|
||||||
|
|
||||||
|
- [ ] **Code is properly formatted** (`cargo fmt --all --check` passes)
|
||||||
|
- [ ] **All clippy warnings are resolved** (`cargo clippy --all-targets --all-features -- -D warnings` passes)
|
||||||
|
- [ ] **Code compiles successfully** (`cargo check --all-targets` passes)
|
||||||
|
- [ ] **Pre-commit hooks are working** and all checks pass
|
||||||
|
- [ ] **No formatting-related changes** mixed with functional changes (separate commits)
|
||||||
|
|
||||||
|
### 2. Functionality
|
||||||
|
|
||||||
|
- [ ] Are all error cases properly handled?
|
||||||
|
- [ ] Is there appropriate logging?
|
||||||
|
- [ ] Is there necessary test coverage?
|
||||||
|
|
||||||
|
### 3. Performance
|
||||||
|
|
||||||
|
- [ ] Are unnecessary memory allocations avoided?
|
||||||
|
- [ ] Are async operations used correctly?
|
||||||
|
- [ ] Are there potential deadlock risks?
|
||||||
|
|
||||||
|
### 4. Security
|
||||||
|
|
||||||
|
- [ ] Are input parameters properly validated?
|
||||||
|
- [ ] Are there appropriate permission checks?
|
||||||
|
- [ ] Is information leakage avoided?
|
||||||
|
|
||||||
|
### 5. Cross-Platform Compatibility
|
||||||
|
|
||||||
|
- [ ] Does the code work on different CPU architectures (x86_64, aarch64)?
|
||||||
|
- [ ] Are platform-specific features properly gated with conditional compilation?
|
||||||
|
- [ ] Is byte order handling correct for binary data?
|
||||||
|
- [ ] Are there appropriate fallback implementations for unsupported platforms?
|
||||||
|
|
||||||
|
### 6. Code Commits and Documentation
|
||||||
|
|
||||||
|
- [ ] Does it comply with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)?
|
||||||
|
- [ ] Are commit messages concise and under 72 characters for the title line?
|
||||||
|
- [ ] Commit titles should be concise and in English, avoid Chinese
|
||||||
|
- [ ] Is PR description provided in copyable markdown format for easy copying?
|
||||||
|
|
||||||
|
## Common Patterns and Best Practices
|
||||||
|
|
||||||
|
### 1. Resource Management
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Use RAII pattern for resource management
|
||||||
|
pub struct ResourceGuard {
|
||||||
|
resource: Resource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ResourceGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Clean up resources
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Dependency Injection
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Use dependency injection pattern
|
||||||
|
pub struct Service {
|
||||||
|
config: Arc<Config>,
|
||||||
|
storage: Arc<dyn StorageAPI>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Graceful Shutdown
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Implement graceful shutdown
|
||||||
|
async fn shutdown_gracefully(shutdown_rx: &mut Receiver<()>) {
|
||||||
|
tokio::select! {
|
||||||
|
_ = shutdown_rx.recv() => {
|
||||||
|
info!("Received shutdown signal");
|
||||||
|
// Perform cleanup operations
|
||||||
|
}
|
||||||
|
_ = tokio::time::sleep(SHUTDOWN_TIMEOUT) => {
|
||||||
|
warn!("Shutdown timeout reached");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Domain-Specific Guidelines
|
||||||
|
|
||||||
|
### 1. Storage Operations
|
||||||
|
|
||||||
|
- All storage operations must support erasure coding
|
||||||
|
- Implement read/write quorum mechanisms
|
||||||
|
- Support data integrity verification
|
||||||
|
|
||||||
|
### 2. Network Communication
|
||||||
|
|
||||||
|
- Use gRPC for internal service communication
|
||||||
|
- HTTP/HTTPS support for S3-compatible API
|
||||||
|
- Implement connection pooling and retry mechanisms
|
||||||
|
|
||||||
|
### 3. Metadata Management
|
||||||
|
|
||||||
|
- Use FlatBuffers for serialization
|
||||||
|
- Support version control and migration
|
||||||
|
- Implement metadata caching
|
||||||
|
|
||||||
|
These rules should serve as guiding principles when developing the RustFS project, ensuring code quality, performance, and maintainability.
|
||||||
|
|
||||||
|
### 4. Code Operations
|
||||||
|
|
||||||
|
#### Branch Management
|
||||||
|
|
||||||
|
- **🚨 CRITICAL: NEVER modify code directly on main or master branch - THIS IS ABSOLUTELY FORBIDDEN 🚨**
|
||||||
|
- **⚠️ ANY DIRECT COMMITS TO MASTER/MAIN WILL BE REJECTED AND MUST BE REVERTED IMMEDIATELY ⚠️**
|
||||||
|
- **🔒 ALL CHANGES MUST GO THROUGH PULL REQUESTS - NO DIRECT COMMITS TO MAIN UNDER ANY CIRCUMSTANCES 🔒**
|
||||||
|
- **Always work on feature branches - NO EXCEPTIONS**
|
||||||
|
- Always check the .cursorrules file before starting to ensure you understand the project guidelines
|
||||||
|
- **MANDATORY workflow for ALL changes:**
|
||||||
|
1. `git checkout main` (switch to main branch)
|
||||||
|
2. `git pull` (get latest changes)
|
||||||
|
3. `git checkout -b feat/your-feature-name` (create and switch to feature branch)
|
||||||
|
4. Make your changes ONLY on the feature branch
|
||||||
|
5. Test thoroughly before committing
|
||||||
|
6. Commit and push to the feature branch
|
||||||
|
7. **Create a pull request for code review - THIS IS THE ONLY WAY TO MERGE TO MAIN**
|
||||||
|
8. **Wait for PR approval before merging - NEVER merge your own PRs without review**
|
||||||
|
- Use descriptive branch names following the pattern: `feat/feature-name`, `fix/issue-name`, `refactor/component-name`, etc.
|
||||||
|
- **Double-check current branch before ANY commit: `git branch` to ensure you're NOT on main/master**
|
||||||
|
- **Pull Request Requirements:**
|
||||||
|
- All changes must be submitted via PR regardless of size or urgency
|
||||||
|
- PRs must include comprehensive description and testing information
|
||||||
|
- PRs must pass all CI/CD checks before merging
|
||||||
|
- PRs require at least one approval from code reviewers
|
||||||
|
- Even hotfixes and emergency changes must go through PR process
|
||||||
|
- **Enforcement:**
|
||||||
|
- Main branch should be protected with branch protection rules
|
||||||
|
- Direct pushes to main should be blocked by repository settings
|
||||||
|
- Any accidental direct commits to main must be immediately reverted via PR
|
||||||
|
|
||||||
|
#### Development Workflow
|
||||||
|
|
||||||
|
## 🎯 **Core Development Principles**
|
||||||
|
|
||||||
|
- **🔴 Every change must be precise - don't modify unless you're confident**
|
||||||
|
- Carefully analyze code logic and ensure complete understanding before making changes
|
||||||
|
- When uncertain, prefer asking users or consulting documentation over blind modifications
|
||||||
|
- Use small iterative steps, modify only necessary parts at a time
|
||||||
|
- Evaluate impact scope before changes to ensure no new issues are introduced
|
||||||
|
|
||||||
|
- **🚀 GitHub PR creation prioritizes gh command usage**
|
||||||
|
- Prefer using `gh pr create` command to create Pull Requests
|
||||||
|
- Avoid having users manually create PRs through web interface
|
||||||
|
- Provide clear and professional PR titles and descriptions
|
||||||
|
- Using `gh` commands ensures better integration and automation
|
||||||
|
|
||||||
|
## 📝 **Code Quality Requirements**
|
||||||
|
|
||||||
|
- Use English for all code comments, documentation, and variable names
|
||||||
|
- Write meaningful and descriptive names for variables, functions, and methods
|
||||||
|
- Avoid meaningless test content like "debug 111" or placeholder values
|
||||||
|
- Before each change, carefully read the existing code to ensure you understand the code structure and implementation, do not break existing logic implementation, do not introduce new issues
|
||||||
|
- Ensure each change provides sufficient test cases to guarantee code correctness
|
||||||
|
- Do not arbitrarily modify numbers and constants in test cases, carefully analyze their meaning to ensure test case correctness
|
||||||
|
- When writing or modifying tests, check existing test cases to ensure they have scientific naming and rigorous logic testing, if not compliant, modify test cases to ensure scientific and rigorous testing
|
||||||
|
- **Before committing any changes, run `cargo clippy --all-targets --all-features -- -D warnings` to ensure all code passes Clippy checks**
|
||||||
|
- After each development completion, first git add . then git commit -m "feat: feature description" or "fix: issue description", ensure compliance with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||||
|
- **Keep commit messages concise and under 72 characters** for the title line, use body for detailed explanations if needed
|
||||||
|
- After each development completion, first git push to remote repository
|
||||||
|
- After each change completion, summarize the changes, do not create summary files, provide a brief change description, ensure compliance with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||||
|
- Provide change descriptions needed for PR in the conversation, ensure compliance with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||||
|
- **Always provide PR descriptions in English** after completing any changes, including:
|
||||||
|
- Clear and concise title following Conventional Commits format
|
||||||
|
- Detailed description of what was changed and why
|
||||||
|
- List of key changes and improvements
|
||||||
|
- Any breaking changes or migration notes if applicable
|
||||||
|
- Testing information and verification steps
|
||||||
|
- **Provide PR descriptions in copyable markdown format** enclosed in code blocks for easy one-click copying
|
||||||
|
|
||||||
|
## 🚫 AI 文档生成限制
|
||||||
|
|
||||||
|
### 禁止生成总结文档
|
||||||
|
|
||||||
|
- **严格禁止创建任何形式的AI生成总结文档**
|
||||||
|
- **不得创建包含大量表情符号、详细格式化表格和典型AI风格的文档**
|
||||||
|
- **不得在项目中生成以下类型的文档:**
|
||||||
|
- 基准测试总结文档(BENCHMARK*.md)
|
||||||
|
- 实现对比分析文档(IMPLEMENTATION_COMPARISON*.md)
|
||||||
|
- 性能分析报告文档
|
||||||
|
- 架构总结文档
|
||||||
|
- 功能对比文档
|
||||||
|
- 任何带有大量表情符号和格式化内容的文档
|
||||||
|
- **如果需要文档,请只在用户明确要求时创建,并保持简洁实用的风格**
|
||||||
|
- **文档应当专注于实际需要的信息,避免过度格式化和装饰性内容**
|
||||||
|
- **任何发现的AI生成总结文档都应该立即删除**
|
||||||
|
|
||||||
|
### 允许的文档类型
|
||||||
|
|
||||||
|
- README.md(项目介绍,保持简洁)
|
||||||
|
- 技术文档(仅在明确需要时创建)
|
||||||
|
- 用户手册(仅在明确需要时创建)
|
||||||
|
- API文档(从代码生成)
|
||||||
|
- 变更日志(CHANGELOG.md)
|
||||||
261
.docker/README.md
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
# RustFS Docker Images
|
||||||
|
|
||||||
|
This directory contains Docker configuration files and supporting infrastructure for building and running RustFS container images.
|
||||||
|
|
||||||
|
## 📁 Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
rustfs/
|
||||||
|
├── Dockerfile # Production image (Alpine + pre-built binaries)
|
||||||
|
├── Dockerfile.source # Development image (Debian + source build)
|
||||||
|
├── docker-buildx.sh # Multi-architecture build script
|
||||||
|
├── Makefile # Build automation with simplified commands
|
||||||
|
└── .docker/ # Supporting infrastructure
|
||||||
|
├── observability/ # Monitoring and observability configs
|
||||||
|
├── compose/ # Docker Compose configurations
|
||||||
|
├── mqtt/ # MQTT broker configs
|
||||||
|
└── openobserve-otel/ # OpenObserve + OpenTelemetry configs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Image Variants
|
||||||
|
|
||||||
|
### Core Images
|
||||||
|
|
||||||
|
| Image | Base OS | Build Method | Size | Use Case |
|
||||||
|
|-------|---------|--------------|------|----------|
|
||||||
|
| `production` (default) | Alpine 3.18 | GitHub Releases | Smallest | Production deployment |
|
||||||
|
| `source` | Debian Bookworm | Source build | Medium | Custom builds with cross-compilation |
|
||||||
|
| `dev` | Debian Bookworm | Development tools | Large | Interactive development |
|
||||||
|
|
||||||
|
## 🚀 Usage Examples
|
||||||
|
|
||||||
|
### Quick Start (Production)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default production image (Alpine + GitHub Releases)
|
||||||
|
docker run -p 9000:9000 rustfs/rustfs:latest
|
||||||
|
|
||||||
|
# Specific version
|
||||||
|
docker run -p 9000:9000 rustfs/rustfs:1.2.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Tag Strategy Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stable Releases
|
||||||
|
docker run rustfs/rustfs:1.2.3 # Main version (production)
|
||||||
|
docker run rustfs/rustfs:1.2.3-production # Explicit production variant
|
||||||
|
docker run rustfs/rustfs:1.2.3-source # Source build variant
|
||||||
|
docker run rustfs/rustfs:latest # Latest stable
|
||||||
|
|
||||||
|
# Prerelease Versions
|
||||||
|
docker run rustfs/rustfs:1.3.0-alpha.2 # Specific alpha version
|
||||||
|
docker run rustfs/rustfs:alpha # Latest alpha
|
||||||
|
docker run rustfs/rustfs:beta # Latest beta
|
||||||
|
docker run rustfs/rustfs:rc # Latest release candidate
|
||||||
|
|
||||||
|
# Development Versions
|
||||||
|
docker run rustfs/rustfs:dev # Latest main branch development
|
||||||
|
docker run rustfs/rustfs:dev-13e4a0b # Specific commit
|
||||||
|
docker run rustfs/rustfs:dev-latest # Latest development
|
||||||
|
docker run rustfs/rustfs:main-latest # Main branch latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Quick setup using Makefile (recommended)
|
||||||
|
make docker-dev-local # Build development image locally
|
||||||
|
make dev-env-start # Start development container
|
||||||
|
|
||||||
|
# Manual Docker commands
|
||||||
|
docker run -it -v $(pwd):/workspace -p 9000:9000 rustfs/rustfs:latest-dev
|
||||||
|
|
||||||
|
# Build from source locally
|
||||||
|
docker build -f Dockerfile.source -t rustfs:custom .
|
||||||
|
|
||||||
|
# Development with hot reload
|
||||||
|
docker-compose up rustfs-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Build Arguments and Scripts
|
||||||
|
|
||||||
|
### Using Makefile Commands (Recommended)
|
||||||
|
|
||||||
|
The easiest way to build images using simplified commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development images (build from source)
|
||||||
|
make docker-dev-local # Build for local use (single arch)
|
||||||
|
make docker-dev # Build multi-arch (for CI/CD)
|
||||||
|
make docker-dev-push REGISTRY=xxx # Build and push to registry
|
||||||
|
|
||||||
|
# Production images (using pre-built binaries)
|
||||||
|
make docker-buildx # Build multi-arch production images
|
||||||
|
make docker-buildx-push # Build and push production images
|
||||||
|
make docker-buildx-version VERSION=v1.0.0 # Build specific version
|
||||||
|
|
||||||
|
# Development environment
|
||||||
|
make dev-env-start # Start development container
|
||||||
|
make dev-env-stop # Stop development container
|
||||||
|
make dev-env-restart # Restart development container
|
||||||
|
|
||||||
|
# Help
|
||||||
|
make help-docker # Show all Docker-related commands
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using docker-buildx.sh (Advanced)
|
||||||
|
|
||||||
|
For direct script usage and advanced scenarios:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build latest version for all architectures
|
||||||
|
./docker-buildx.sh
|
||||||
|
|
||||||
|
# Build and push to registry
|
||||||
|
./docker-buildx.sh --push
|
||||||
|
|
||||||
|
# Build specific version
|
||||||
|
./docker-buildx.sh --release v1.2.3
|
||||||
|
|
||||||
|
# Build and push specific version
|
||||||
|
./docker-buildx.sh --release v1.2.3 --push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Docker Builds
|
||||||
|
|
||||||
|
All images support dynamic version selection:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build production image with latest release
|
||||||
|
docker build --build-arg RELEASE="latest" -t rustfs:latest .
|
||||||
|
|
||||||
|
# Build from source with specific target
|
||||||
|
docker build -f Dockerfile.source \
|
||||||
|
--build-arg TARGETPLATFORM="linux/amd64" \
|
||||||
|
-t rustfs:source .
|
||||||
|
|
||||||
|
# Development build
|
||||||
|
docker build -f Dockerfile.source -t rustfs:dev .
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Binary Download Sources
|
||||||
|
|
||||||
|
### Unified GitHub Releases
|
||||||
|
|
||||||
|
The production image downloads from GitHub Releases for reliability and transparency:
|
||||||
|
|
||||||
|
- ✅ **production** → GitHub Releases API with automatic latest detection
|
||||||
|
- ✅ **Checksum verification** → SHA256SUMS validation when available
|
||||||
|
- ✅ **Multi-architecture** → Supports amd64 and arm64
|
||||||
|
|
||||||
|
### Source Build
|
||||||
|
|
||||||
|
The source variant compiles from source code with advanced features:
|
||||||
|
|
||||||
|
- 🔧 **Cross-compilation** → Supports multiple target platforms via `TARGETPLATFORM`
|
||||||
|
- ⚡ **Build caching** → sccache for faster compilation
|
||||||
|
- 🎯 **Optimized builds** → Release optimizations with LTO and symbol stripping
|
||||||
|
|
||||||
|
## 📋 Architecture Support
|
||||||
|
|
||||||
|
All variants support multi-architecture builds:
|
||||||
|
|
||||||
|
- **linux/amd64** (x86_64)
|
||||||
|
- **linux/arm64** (aarch64)
|
||||||
|
|
||||||
|
Architecture is automatically detected during build using Docker's `TARGETARCH` build argument.
|
||||||
|
|
||||||
|
## 🔐 Security Features
|
||||||
|
|
||||||
|
- **Checksum Verification**: Production image verifies SHA256SUMS when available
|
||||||
|
- **Non-root User**: All images run as user `rustfs` (UID 1000)
|
||||||
|
- **Minimal Runtime**: Production image only includes necessary dependencies
|
||||||
|
- **Secure Defaults**: No hardcoded credentials or keys
|
||||||
|
|
||||||
|
## 🛠️ Development Workflow
|
||||||
|
|
||||||
|
### Quick Start with Makefile (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Start development environment
|
||||||
|
make dev-env-start
|
||||||
|
|
||||||
|
# 2. Your development container is now running with:
|
||||||
|
# - Port 9000 exposed for RustFS
|
||||||
|
# - Port 9010 exposed for admin console
|
||||||
|
# - Current directory mounted as /workspace
|
||||||
|
|
||||||
|
# 3. Stop when done
|
||||||
|
make dev-env-stop
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Development Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build development image from source
|
||||||
|
make docker-dev-local
|
||||||
|
|
||||||
|
# Or use traditional Docker commands
|
||||||
|
docker build -f Dockerfile.source -t rustfs:dev .
|
||||||
|
|
||||||
|
# Run with development tools
|
||||||
|
docker run -it -v $(pwd):/workspace -p 9000:9000 rustfs:dev bash
|
||||||
|
|
||||||
|
# Or use docker-compose for complex setups
|
||||||
|
docker-compose up rustfs-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Development Tasks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and test locally
|
||||||
|
make build # Build binary natively
|
||||||
|
make docker-dev-local # Build development Docker image
|
||||||
|
make test # Run tests
|
||||||
|
make fmt # Format code
|
||||||
|
make clippy # Run linter
|
||||||
|
|
||||||
|
# Get help
|
||||||
|
make help # General help
|
||||||
|
make help-docker # Docker-specific help
|
||||||
|
make help-build # Build-specific help
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 CI/CD Integration
|
||||||
|
|
||||||
|
The project uses GitHub Actions for automated multi-architecture Docker builds:
|
||||||
|
|
||||||
|
### Automated Builds
|
||||||
|
|
||||||
|
- **Tags**: Automatic builds triggered on version tags (e.g., `v1.2.3`)
|
||||||
|
- **Main Branch**: Development builds with `dev-latest` and `main-latest` tags
|
||||||
|
- **Pull Requests**: Test builds without registry push
|
||||||
|
|
||||||
|
### Build Variants
|
||||||
|
|
||||||
|
Each build creates three image variants:
|
||||||
|
|
||||||
|
- `rustfs/rustfs:v1.2.3` (production - Alpine-based)
|
||||||
|
- `rustfs/rustfs:v1.2.3-source` (source build - Debian-based)
|
||||||
|
- `rustfs/rustfs:v1.2.3-dev` (development - Debian-based with tools)
|
||||||
|
|
||||||
|
### Manual Builds
|
||||||
|
|
||||||
|
Trigger custom builds via GitHub Actions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use workflow_dispatch to build specific versions
|
||||||
|
# Available options: latest, main-latest, dev-latest, v1.2.3, dev-abc123
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Supporting Infrastructure
|
||||||
|
|
||||||
|
The `.docker/` directory contains supporting configuration files:
|
||||||
|
|
||||||
|
- **observability/** - Prometheus, Grafana, OpenTelemetry configs
|
||||||
|
- **compose/** - Multi-service Docker Compose setups
|
||||||
|
- **mqtt/** - MQTT broker configurations
|
||||||
|
- **openobserve-otel/** - Log aggregation and tracing setup
|
||||||
|
|
||||||
|
See individual README files in each subdirectory for specific usage instructions.
|
||||||
80
.docker/compose/README.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Docker Compose Configurations
|
||||||
|
|
||||||
|
This directory contains specialized Docker Compose configurations for different use cases.
|
||||||
|
|
||||||
|
## 📁 Configuration Files
|
||||||
|
|
||||||
|
This directory contains specialized Docker Compose configurations and their associated Dockerfiles, keeping related files organized together.
|
||||||
|
|
||||||
|
### Main Configuration (Root Directory)
|
||||||
|
|
||||||
|
- **`../../docker-compose.yml`** - **Default Production Setup**
|
||||||
|
- Complete production-ready configuration
|
||||||
|
- Includes RustFS server + full observability stack
|
||||||
|
- Supports multiple profiles: `dev`, `observability`, `cache`, `proxy`
|
||||||
|
- Recommended for most users
|
||||||
|
|
||||||
|
### Specialized Configurations
|
||||||
|
|
||||||
|
- **`docker-compose.cluster.yaml`** - **Distributed Testing**
|
||||||
|
- 4-node cluster setup for testing distributed storage
|
||||||
|
- Uses local compiled binaries
|
||||||
|
- Simulates multi-node environment
|
||||||
|
- Ideal for development and cluster testing
|
||||||
|
|
||||||
|
- **`docker-compose.observability.yaml`** - **Observability Focus**
|
||||||
|
- Specialized setup for testing observability features
|
||||||
|
- Includes OpenTelemetry, Jaeger, Prometheus, Loki, Grafana
|
||||||
|
- Uses `../../Dockerfile.source` for builds
|
||||||
|
- Perfect for observability development
|
||||||
|
|
||||||
|
## 🚀 Usage Examples
|
||||||
|
|
||||||
|
### Production Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start main service
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Start with development profile
|
||||||
|
docker-compose --profile dev up -d
|
||||||
|
|
||||||
|
# Start with full observability
|
||||||
|
docker-compose --profile observability up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cluster Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and start 4-node cluster (run from project root)
|
||||||
|
cd .docker/compose
|
||||||
|
docker-compose -f docker-compose.cluster.yaml up -d
|
||||||
|
|
||||||
|
# Or run directly from project root
|
||||||
|
docker-compose -f .docker/compose/docker-compose.cluster.yaml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Observability Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start observability-focused environment (run from project root)
|
||||||
|
cd .docker/compose
|
||||||
|
docker-compose -f docker-compose.observability.yaml up -d
|
||||||
|
|
||||||
|
# Or run directly from project root
|
||||||
|
docker-compose -f .docker/compose/docker-compose.observability.yaml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Configuration Overview
|
||||||
|
|
||||||
|
| Configuration | Nodes | Storage | Observability | Use Case |
|
||||||
|
|---------------|-------|---------|---------------|----------|
|
||||||
|
| **Main** | 1 | Volume mounts | Full stack | Production |
|
||||||
|
| **Cluster** | 4 | HTTP endpoints | Basic | Testing |
|
||||||
|
| **Observability** | 4 | Local data | Advanced | Development |
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
- Always ensure you have built the required binaries before starting cluster tests
|
||||||
|
- The main configuration is sufficient for most use cases
|
||||||
|
- Specialized configurations are for specific testing scenarios
|
||||||
82
.docker/compose/docker-compose.cluster.yaml
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
services:
|
||||||
|
node0:
|
||||||
|
image: rustfs/rustfs:latest # Replace with your image name and label
|
||||||
|
container_name: node0
|
||||||
|
hostname: node0
|
||||||
|
environment:
|
||||||
|
- RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3}
|
||||||
|
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||||
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
- RUSTFS_ACCESS_KEY=rustfsadmin
|
||||||
|
- RUSTFS_SECRET_KEY=rustfsadmin
|
||||||
|
platform: linux/amd64
|
||||||
|
ports:
|
||||||
|
- "9000:9000" # Map port 9001 of the host to port 9000 of the container
|
||||||
|
volumes:
|
||||||
|
- ../../target/x86_64-unknown-linux-musl/release/rustfs:/app/rustfs
|
||||||
|
command: "/app/rustfs"
|
||||||
|
|
||||||
|
node1:
|
||||||
|
image: rustfs/rustfs:latest
|
||||||
|
container_name: node1
|
||||||
|
hostname: node1
|
||||||
|
environment:
|
||||||
|
- RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3}
|
||||||
|
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||||
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
- RUSTFS_ACCESS_KEY=rustfsadmin
|
||||||
|
- RUSTFS_SECRET_KEY=rustfsadmin
|
||||||
|
platform: linux/amd64
|
||||||
|
ports:
|
||||||
|
- "9001:9000" # Map port 9002 of the host to port 9000 of the container
|
||||||
|
volumes:
|
||||||
|
- ../../target/x86_64-unknown-linux-musl/release/rustfs:/app/rustfs
|
||||||
|
command: "/app/rustfs"
|
||||||
|
|
||||||
|
node2:
|
||||||
|
image: rustfs/rustfs:latest
|
||||||
|
container_name: node2
|
||||||
|
hostname: node2
|
||||||
|
environment:
|
||||||
|
- RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3}
|
||||||
|
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||||
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
- RUSTFS_ACCESS_KEY=rustfsadmin
|
||||||
|
- RUSTFS_SECRET_KEY=rustfsadmin
|
||||||
|
platform: linux/amd64
|
||||||
|
ports:
|
||||||
|
- "9002:9000" # Map port 9003 of the host to port 9000 of the container
|
||||||
|
volumes:
|
||||||
|
- ../../target/x86_64-unknown-linux-musl/release/rustfs:/app/rustfs
|
||||||
|
command: "/app/rustfs"
|
||||||
|
|
||||||
|
node3:
|
||||||
|
image: rustfs/rustfs:latest
|
||||||
|
container_name: node3
|
||||||
|
hostname: node3
|
||||||
|
environment:
|
||||||
|
- RUSTFS_VOLUMES=http://node{0...3}:9000/data/rustfs{0...3}
|
||||||
|
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||||
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
- RUSTFS_ACCESS_KEY=rustfsadmin
|
||||||
|
- RUSTFS_SECRET_KEY=rustfsadmin
|
||||||
|
platform: linux/amd64
|
||||||
|
ports:
|
||||||
|
- "9003:9000" # Map port 9004 of the host to port 9000 of the container
|
||||||
|
volumes:
|
||||||
|
- ../../target/x86_64-unknown-linux-musl/release/rustfs:/app/rustfs
|
||||||
|
command: "/app/rustfs"
|
||||||
146
.docker/compose/docker-compose.observability.yaml
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
services:
|
||||||
|
otel-collector:
|
||||||
|
image: otel/opentelemetry-collector-contrib:0.129.1
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- ../../.docker/observability/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml
|
||||||
|
ports:
|
||||||
|
- 1888:1888
|
||||||
|
- 8888:8888
|
||||||
|
- 8889:8889
|
||||||
|
- 13133:13133
|
||||||
|
- 4317:4317
|
||||||
|
- 4318:4318
|
||||||
|
- 55679:55679
|
||||||
|
networks:
|
||||||
|
- rustfs-network
|
||||||
|
jaeger:
|
||||||
|
image: jaegertracing/jaeger:2.8.0
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
ports:
|
||||||
|
- "16686:16686"
|
||||||
|
- "14317:4317"
|
||||||
|
- "14318:4318"
|
||||||
|
networks:
|
||||||
|
- rustfs-network
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:v3.4.2
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- ../../.docker/observability/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
|
ports:
|
||||||
|
- "9090:9090"
|
||||||
|
networks:
|
||||||
|
- rustfs-network
|
||||||
|
loki:
|
||||||
|
image: grafana/loki:3.5.1
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- ../../.docker/observability/loki-config.yaml:/etc/loki/local-config.yaml
|
||||||
|
ports:
|
||||||
|
- "3100:3100"
|
||||||
|
command: -config.file=/etc/loki/local-config.yaml
|
||||||
|
networks:
|
||||||
|
- rustfs-network
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:12.0.2
|
||||||
|
ports:
|
||||||
|
- "3000:3000" # Web UI
|
||||||
|
environment:
|
||||||
|
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
networks:
|
||||||
|
- rustfs-network
|
||||||
|
|
||||||
|
node1:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: Dockerfile.source
|
||||||
|
container_name: node1
|
||||||
|
environment:
|
||||||
|
- RUSTFS_VOLUMES=http://node{1...4}:9000/root/data/target/volume/test{1...4}
|
||||||
|
- RUSTFS_ADDRESS=:9000
|
||||||
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4317
|
||||||
|
- RUSTFS_OBS_LOGGER_LEVEL=debug
|
||||||
|
platform: linux/amd64
|
||||||
|
ports:
|
||||||
|
- "9001:9000" # Map port 9001 of the host to port 9000 of the container
|
||||||
|
networks:
|
||||||
|
- rustfs-network
|
||||||
|
|
||||||
|
node2:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: Dockerfile.source
|
||||||
|
container_name: node2
|
||||||
|
environment:
|
||||||
|
- RUSTFS_VOLUMES=http://node{1...4}:9000/root/data/target/volume/test{1...4}
|
||||||
|
- RUSTFS_ADDRESS=:9000
|
||||||
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4317
|
||||||
|
- RUSTFS_OBS_LOGGER_LEVEL=debug
|
||||||
|
platform: linux/amd64
|
||||||
|
ports:
|
||||||
|
- "9002:9000" # Map port 9002 of the host to port 9000 of the container
|
||||||
|
networks:
|
||||||
|
- rustfs-network
|
||||||
|
|
||||||
|
node3:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: Dockerfile.source
|
||||||
|
container_name: node3
|
||||||
|
environment:
|
||||||
|
- RUSTFS_VOLUMES=http://node{1...4}:9000/root/data/target/volume/test{1...4}
|
||||||
|
- RUSTFS_ADDRESS=:9000
|
||||||
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4317
|
||||||
|
- RUSTFS_OBS_LOGGER_LEVEL=debug
|
||||||
|
platform: linux/amd64
|
||||||
|
ports:
|
||||||
|
- "9003:9000" # Map port 9003 of the host to port 9000 of the container
|
||||||
|
networks:
|
||||||
|
- rustfs-network
|
||||||
|
|
||||||
|
node4:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: Dockerfile.source
|
||||||
|
container_name: node4
|
||||||
|
environment:
|
||||||
|
- RUSTFS_VOLUMES=http://node{1...4}:9000/root/data/target/volume/test{1...4}
|
||||||
|
- RUSTFS_ADDRESS=:9000
|
||||||
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4317
|
||||||
|
- RUSTFS_OBS_LOGGER_LEVEL=debug
|
||||||
|
platform: linux/amd64
|
||||||
|
ports:
|
||||||
|
- "9004:9000" # Map port 9004 of the host to port 9000 of the container
|
||||||
|
networks:
|
||||||
|
- rustfs-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
rustfs-network:
|
||||||
|
driver: bridge
|
||||||
|
name: "network_rustfs_config"
|
||||||
|
driver_opts:
|
||||||
|
com.docker.network.enable_ipv6: "true"
|
||||||
37
.docker/mqtt/config/emqx.conf
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# 节点配置
|
||||||
|
node.name = "emqx@127.0.0.1"
|
||||||
|
node.cookie = "aBcDeFgHiJkLmNoPqRsTuVwXyZ012345"
|
||||||
|
node.data_dir = "/opt/emqx/data"
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
log.console = {level = info, enable = true}
|
||||||
|
log.file = {path = "/opt/emqx/log/emqx.log", enable = true, level = info}
|
||||||
|
|
||||||
|
# MQTT TCP 监听器
|
||||||
|
listeners.tcp.default = {bind = "0.0.0.0:1883", max_connections = 1000000, enable = true}
|
||||||
|
|
||||||
|
# MQTT SSL 监听器
|
||||||
|
listeners.ssl.default = {bind = "0.0.0.0:8883", enable = false}
|
||||||
|
|
||||||
|
# MQTT WebSocket 监听器
|
||||||
|
listeners.ws.default = {bind = "0.0.0.0:8083", enable = true}
|
||||||
|
|
||||||
|
# MQTT WebSocket SSL 监听器
|
||||||
|
listeners.wss.default = {bind = "0.0.0.0:8084", enable = false}
|
||||||
|
|
||||||
|
# 管理控制台
|
||||||
|
dashboard.listeners.http = {bind = "0.0.0.0:18083", enable = true}
|
||||||
|
|
||||||
|
# HTTP API
|
||||||
|
management.listeners.http = {bind = "0.0.0.0:8081", enable = true}
|
||||||
|
|
||||||
|
# 认证配置
|
||||||
|
authentication = [
|
||||||
|
{enable = true, mechanism = password_based, backend = built_in_database, user_id_type = username}
|
||||||
|
]
|
||||||
|
|
||||||
|
# 授权配置
|
||||||
|
authorization.sources = [{type = built_in_database, enable = true}]
|
||||||
|
|
||||||
|
# 持久化消息存储
|
||||||
|
message.storage.backend = built_in_database
|
||||||
9
.docker/mqtt/config/vm.args
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-name emqx@127.0.0.1
|
||||||
|
-setcookie aBcDeFgHiJkLmNoPqRsTuVwXyZ012345
|
||||||
|
+P 2097152
|
||||||
|
+t 1048576
|
||||||
|
+zdbbl 32768
|
||||||
|
-kernel inet_dist_listen_min 6000
|
||||||
|
-kernel inet_dist_listen_max 6100
|
||||||
|
-smp enable
|
||||||
|
-mnesia dir "/opt/emqx/data/mnesia"
|
||||||
74
.docker/mqtt/docker-compose-more.yml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
services:
|
||||||
|
emqx:
|
||||||
|
image: emqx/emqx:latest
|
||||||
|
container_name: emqx
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- EMQX_NODE__NAME=emqx@127.0.0.1
|
||||||
|
- EMQX_NODE__COOKIE=aBcDeFgHiJkLmNoPqRsTuVwXyZ012345
|
||||||
|
- EMQX_NODE__DATA_DIR=/opt/emqx/data
|
||||||
|
- EMQX_LOG__CONSOLE__LEVEL=info
|
||||||
|
- EMQX_LOG__CONSOLE__ENABLE=true
|
||||||
|
- EMQX_LOG__FILE__PATH=/opt/emqx/log/emqx.log
|
||||||
|
- EMQX_LOG__FILE__LEVEL=info
|
||||||
|
- EMQX_LOG__FILE__ENABLE=true
|
||||||
|
- EMQX_LISTENERS__TCP__DEFAULT__BIND=0.0.0.0:1883
|
||||||
|
- EMQX_LISTENERS__TCP__DEFAULT__MAX_CONNECTIONS=1000000
|
||||||
|
- EMQX_LISTENERS__TCP__DEFAULT__ENABLE=true
|
||||||
|
- EMQX_LISTENERS__SSL__DEFAULT__BIND=0.0.0.0:8883
|
||||||
|
- EMQX_LISTENERS__SSL__DEFAULT__ENABLE=false
|
||||||
|
- EMQX_LISTENERS__WS__DEFAULT__BIND=0.0.0.0:8083
|
||||||
|
- EMQX_LISTENERS__WS__DEFAULT__ENABLE=true
|
||||||
|
- EMQX_LISTENERS__WSS__DEFAULT__BIND=0.0.0.0:8084
|
||||||
|
- EMQX_LISTENERS__WSS__DEFAULT__ENABLE=false
|
||||||
|
- EMQX_DASHBOARD__LISTENERS__HTTP__BIND=0.0.0.0:18083
|
||||||
|
- EMQX_DASHBOARD__LISTENERS__HTTP__ENABLE=true
|
||||||
|
- EMQX_MANAGEMENT__LISTENERS__HTTP__BIND=0.0.0.0:8081
|
||||||
|
- EMQX_MANAGEMENT__LISTENERS__HTTP__ENABLE=true
|
||||||
|
- EMQX_AUTHENTICATION__1__ENABLE=true
|
||||||
|
- EMQX_AUTHENTICATION__1__MECHANISM=password_based
|
||||||
|
- EMQX_AUTHENTICATION__1__BACKEND=built_in_database
|
||||||
|
- EMQX_AUTHENTICATION__1__USER_ID_TYPE=username
|
||||||
|
- EMQX_AUTHORIZATION__SOURCES__1__TYPE=built_in_database
|
||||||
|
- EMQX_AUTHORIZATION__SOURCES__1__ENABLE=true
|
||||||
|
ports:
|
||||||
|
- "1883:1883" # MQTT TCP
|
||||||
|
- "8883:8883" # MQTT SSL
|
||||||
|
- "8083:8083" # MQTT WebSocket
|
||||||
|
- "8084:8084" # MQTT WebSocket SSL
|
||||||
|
- "18083:18083" # Web 管理控制台
|
||||||
|
- "8081:8081" # HTTP API
|
||||||
|
volumes:
|
||||||
|
- ./data:/opt/emqx/data
|
||||||
|
- ./log:/opt/emqx/log
|
||||||
|
- ./config:/opt/emqx/etc
|
||||||
|
networks:
|
||||||
|
- mqtt-net
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "/opt/emqx/bin/emqx_ctl", "status" ]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "100m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
mqtt-net:
|
||||||
|
driver: bridge
|
||||||
29
.docker/mqtt/docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
services:
|
||||||
|
emqx:
|
||||||
|
image: emqx/emqx:latest
|
||||||
|
container_name: emqx
|
||||||
|
ports:
|
||||||
|
- "1883:1883"
|
||||||
|
- "8083:8083"
|
||||||
|
- "8084:8084"
|
||||||
|
- "8883:8883"
|
||||||
|
- "18083:18083"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
driver: bridge
|
||||||
109
.docker/observability/README.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# Observability
|
||||||
|
|
||||||
|
This directory contains the observability stack for the application. The stack is composed of the following components:
|
||||||
|
|
||||||
|
- Prometheus v3.2.1
|
||||||
|
- Grafana 11.6.0
|
||||||
|
- Loki 3.4.2
|
||||||
|
- Jaeger 2.4.0
|
||||||
|
- Otel Collector 0.120.0 # 0.121.0 remove loki
|
||||||
|
|
||||||
|
## Prometheus
|
||||||
|
|
||||||
|
Prometheus is a monitoring and alerting toolkit. It scrapes metrics from instrumented jobs, either directly or via an
|
||||||
|
intermediary push gateway for short-lived jobs. It stores all scraped samples locally and runs rules over this data to
|
||||||
|
either aggregate and record new time series from existing data or generate alerts. Grafana or other API consumers can be
|
||||||
|
used to visualize the collected data.
|
||||||
|
|
||||||
|
## Grafana
|
||||||
|
|
||||||
|
Grafana is a multi-platform open-source analytics and interactive visualization web application. It provides charts,
|
||||||
|
graphs, and alerts for the web when connected to supported data sources.
|
||||||
|
|
||||||
|
## Loki
|
||||||
|
|
||||||
|
Loki is a horizontally-scalable, highly-available, multi-tenant log aggregation system inspired by Prometheus. It is
|
||||||
|
designed to be very cost-effective and easy to operate. It does not index the contents of the logs, but rather a set of
|
||||||
|
labels for each log stream.
|
||||||
|
|
||||||
|
## Jaeger
|
||||||
|
|
||||||
|
Jaeger is a distributed tracing system released as open source by Uber Technologies. It is used for monitoring and
|
||||||
|
troubleshooting microservices-based distributed systems, including:
|
||||||
|
|
||||||
|
- Distributed context propagation
|
||||||
|
- Distributed transaction monitoring
|
||||||
|
- Root cause analysis
|
||||||
|
- Service dependency analysis
|
||||||
|
- Performance / latency optimization
|
||||||
|
|
||||||
|
## Otel Collector
|
||||||
|
|
||||||
|
The OpenTelemetry Collector offers a vendor-agnostic implementation on how to receive, process, and export telemetry
|
||||||
|
data. It removes the need to run, operate, and maintain multiple agents/collectors in order to support open-source
|
||||||
|
observability data formats (e.g. Jaeger, Prometheus, etc.) sending to one or more open-source or commercial back-ends.
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
To deploy the observability stack, run the following command:
|
||||||
|
|
||||||
|
- docker latest version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.override.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
- docker compose v2.0.0 or before
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docke-compose -f docker-compose.yml -f docker-compose.override.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
To access the Grafana dashboard, navigate to `http://localhost:3000` in your browser. The default username and password
|
||||||
|
are `admin` and `admin`, respectively.
|
||||||
|
|
||||||
|
To access the Jaeger dashboard, navigate to `http://localhost:16686` in your browser.
|
||||||
|
|
||||||
|
To access the Prometheus dashboard, navigate to `http://localhost:9090` in your browser.
|
||||||
|
|
||||||
|
## How to stop
|
||||||
|
|
||||||
|
To stop the observability stack, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.override.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to remove data
|
||||||
|
|
||||||
|
To remove the data generated by the observability stack, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.override.yml down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to configure
|
||||||
|
|
||||||
|
To configure the observability stack, modify the `docker-compose.override.yml` file. The file contains the following
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
prometheus:
|
||||||
|
environment:
|
||||||
|
- PROMETHEUS_CONFIG_FILE=/etc/prometheus/prometheus.yml
|
||||||
|
volumes:
|
||||||
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
environment:
|
||||||
|
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||||
|
volumes:
|
||||||
|
- ./grafana/provisioning:/etc/grafana/provisioning
|
||||||
|
```
|
||||||
|
|
||||||
|
The `prometheus` service mounts the `prometheus.yml` file to `/etc/prometheus/prometheus.yml`. The `grafana` service
|
||||||
|
mounts the `grafana/provisioning` directory to `/etc/grafana/provisioning`. You can modify these files to configure the
|
||||||
|
observability stack.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
27
.docker/observability/README_ZH.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
## 部署可观测性系统
|
||||||
|
|
||||||
|
OpenTelemetry Collector 提供了一个厂商中立的遥测数据处理方案,用于接收、处理和导出遥测数据。它消除了为支持多种开源可观测性数据格式(如
|
||||||
|
Jaeger、Prometheus 等)而需要运行和维护多个代理/收集器的必要性。
|
||||||
|
|
||||||
|
### 快速部署
|
||||||
|
|
||||||
|
1. 进入 `.docker/observability` 目录
|
||||||
|
2. 执行以下命令启动服务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 访问监控面板
|
||||||
|
|
||||||
|
服务启动后,可通过以下地址访问各个监控面板:
|
||||||
|
|
||||||
|
- Grafana: `http://localhost:3000` (默认账号/密码:`admin`/`admin`)
|
||||||
|
- Jaeger: `http://localhost:16686`
|
||||||
|
- Prometheus: `http://localhost:9090`
|
||||||
|
|
||||||
|
## 配置可观测性
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export RUSTFS_OBS_ENDPOINT="http://localhost:4317" # OpenTelemetry Collector 地址
|
||||||
|
```
|
||||||
97
.docker/observability/docker-compose.yml
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
tempo:
|
||||||
|
image: grafana/tempo:latest
|
||||||
|
#user: root # The container must be started with root to execute chown in the script
|
||||||
|
#entrypoint: [ "/etc/tempo/entrypoint.sh" ] # Specify a custom entry point
|
||||||
|
command: [ "-config.file=/etc/tempo.yaml" ] # This is passed as a parameter to the entry point script
|
||||||
|
volumes:
|
||||||
|
- ./tempo-entrypoint.sh:/etc/tempo/entrypoint.sh # Mount entry point script
|
||||||
|
- ./tempo.yaml:/etc/tempo.yaml
|
||||||
|
- ./tempo-data:/var/tempo
|
||||||
|
ports:
|
||||||
|
- "3200:3200" # tempo
|
||||||
|
- "24317:4317" # otlp grpc
|
||||||
|
networks:
|
||||||
|
- otel-network
|
||||||
|
|
||||||
|
otel-collector:
|
||||||
|
image: otel/opentelemetry-collector-contrib:0.129.1
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml
|
||||||
|
ports:
|
||||||
|
- "1888:1888"
|
||||||
|
- "8888:8888"
|
||||||
|
- "8889:8889"
|
||||||
|
- "13133:13133"
|
||||||
|
- "4317:4317"
|
||||||
|
- "4318:4318"
|
||||||
|
- "55679:55679"
|
||||||
|
networks:
|
||||||
|
- otel-network
|
||||||
|
jaeger:
|
||||||
|
image: jaegertracing/jaeger:2.8.0
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
ports:
|
||||||
|
- "16686:16686"
|
||||||
|
- "14317:4317"
|
||||||
|
- "14318:4318"
|
||||||
|
networks:
|
||||||
|
- otel-network
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:v3.4.2
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
|
ports:
|
||||||
|
- "9090:9090"
|
||||||
|
networks:
|
||||||
|
- otel-network
|
||||||
|
loki:
|
||||||
|
image: grafana/loki:3.5.1
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- ./loki-config.yaml:/etc/loki/local-config.yaml
|
||||||
|
ports:
|
||||||
|
- "3100:3100"
|
||||||
|
command: -config.file=/etc/loki/local-config.yaml
|
||||||
|
networks:
|
||||||
|
- otel-network
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:12.0.2
|
||||||
|
ports:
|
||||||
|
- "3000:3000" # Web UI
|
||||||
|
volumes:
|
||||||
|
- ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
|
||||||
|
environment:
|
||||||
|
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
networks:
|
||||||
|
- otel-network
|
||||||
|
|
||||||
|
|
||||||
|
networks:
|
||||||
|
otel-network:
|
||||||
|
driver: bridge
|
||||||
|
name: "network_otel_config"
|
||||||
|
driver_opts:
|
||||||
|
com.docker.network.enable_ipv6: "true"
|
||||||
32
.docker/observability/grafana-datasources.yaml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Prometheus
|
||||||
|
type: prometheus
|
||||||
|
uid: prometheus
|
||||||
|
access: proxy
|
||||||
|
orgId: 1
|
||||||
|
url: http://prometheus:9090
|
||||||
|
basicAuth: false
|
||||||
|
isDefault: false
|
||||||
|
version: 1
|
||||||
|
editable: false
|
||||||
|
jsonData:
|
||||||
|
httpMethod: GET
|
||||||
|
- name: Tempo
|
||||||
|
type: tempo
|
||||||
|
access: proxy
|
||||||
|
orgId: 1
|
||||||
|
url: http://tempo:3200
|
||||||
|
basicAuth: false
|
||||||
|
isDefault: true
|
||||||
|
version: 1
|
||||||
|
editable: false
|
||||||
|
apiVersion: 1
|
||||||
|
uid: tempo
|
||||||
|
jsonData:
|
||||||
|
httpMethod: GET
|
||||||
|
serviceMap:
|
||||||
|
datasourceUid: prometheus
|
||||||
|
streamingEnabled:
|
||||||
|
search: true
|
||||||
112
.docker/observability/jaeger-config.yaml
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
service:
|
||||||
|
extensions: [ jaeger_storage, jaeger_query, remote_sampling, healthcheckv2 ]
|
||||||
|
pipelines:
|
||||||
|
traces:
|
||||||
|
receivers: [ otlp, jaeger, zipkin ]
|
||||||
|
processors: [ batch, adaptive_sampling ]
|
||||||
|
exporters: [ jaeger_storage_exporter ]
|
||||||
|
telemetry:
|
||||||
|
resource:
|
||||||
|
service.name: jaeger
|
||||||
|
metrics:
|
||||||
|
level: detailed
|
||||||
|
readers:
|
||||||
|
- pull:
|
||||||
|
exporter:
|
||||||
|
prometheus:
|
||||||
|
host: 0.0.0.0
|
||||||
|
port: 8888
|
||||||
|
logs:
|
||||||
|
level: debug
|
||||||
|
# TODO Initialize telemetry tracer once OTEL released new feature.
|
||||||
|
# https://github.com/open-telemetry/opentelemetry-collector/issues/10663
|
||||||
|
|
||||||
|
extensions:
|
||||||
|
healthcheckv2:
|
||||||
|
use_v2: true
|
||||||
|
http:
|
||||||
|
|
||||||
|
# pprof:
|
||||||
|
# endpoint: 0.0.0.0:1777
|
||||||
|
# zpages:
|
||||||
|
# endpoint: 0.0.0.0:55679
|
||||||
|
|
||||||
|
jaeger_query:
|
||||||
|
storage:
|
||||||
|
traces: some_store
|
||||||
|
traces_archive: another_store
|
||||||
|
ui:
|
||||||
|
config_file: ./cmd/jaeger/config-ui.json
|
||||||
|
log_access: true
|
||||||
|
# The maximum duration that is considered for clock skew adjustments.
|
||||||
|
# Defaults to 0 seconds, which means it's disabled.
|
||||||
|
max_clock_skew_adjust: 0s
|
||||||
|
grpc:
|
||||||
|
endpoint: 0.0.0.0:16685
|
||||||
|
http:
|
||||||
|
endpoint: 0.0.0.0:16686
|
||||||
|
|
||||||
|
jaeger_storage:
|
||||||
|
backends:
|
||||||
|
some_store:
|
||||||
|
memory:
|
||||||
|
max_traces: 1000000
|
||||||
|
another_store:
|
||||||
|
memory:
|
||||||
|
max_traces: 1000000
|
||||||
|
metric_backends:
|
||||||
|
some_metrics_storage:
|
||||||
|
prometheus:
|
||||||
|
endpoint: http://prometheus:9090
|
||||||
|
normalize_calls: true
|
||||||
|
normalize_duration: true
|
||||||
|
|
||||||
|
remote_sampling:
|
||||||
|
# You can either use file or adaptive sampling strategy in remote_sampling
|
||||||
|
# file:
|
||||||
|
# path: ./cmd/jaeger/sampling-strategies.json
|
||||||
|
adaptive:
|
||||||
|
sampling_store: some_store
|
||||||
|
initial_sampling_probability: 0.1
|
||||||
|
http:
|
||||||
|
grpc:
|
||||||
|
|
||||||
|
receivers:
|
||||||
|
otlp:
|
||||||
|
protocols:
|
||||||
|
grpc:
|
||||||
|
http:
|
||||||
|
|
||||||
|
jaeger:
|
||||||
|
protocols:
|
||||||
|
grpc:
|
||||||
|
thrift_binary:
|
||||||
|
thrift_compact:
|
||||||
|
thrift_http:
|
||||||
|
|
||||||
|
zipkin:
|
||||||
|
|
||||||
|
processors:
|
||||||
|
batch:
|
||||||
|
# Adaptive Sampling Processor is required to support adaptive sampling.
|
||||||
|
# It expects remote_sampling extension with `adaptive:` config to be enabled.
|
||||||
|
adaptive_sampling:
|
||||||
|
|
||||||
|
exporters:
|
||||||
|
jaeger_storage_exporter:
|
||||||
|
trace_storage: some_store
|
||||||
|
|
||||||
77
.docker/observability/loki-config.yaml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
auth_enabled: false
|
||||||
|
|
||||||
|
server:
|
||||||
|
http_listen_port: 3100
|
||||||
|
grpc_listen_port: 9096
|
||||||
|
log_level: debug
|
||||||
|
grpc_server_max_concurrent_streams: 1000
|
||||||
|
|
||||||
|
common:
|
||||||
|
instance_addr: 127.0.0.1
|
||||||
|
path_prefix: /tmp/loki
|
||||||
|
storage:
|
||||||
|
filesystem:
|
||||||
|
chunks_directory: /tmp/loki/chunks
|
||||||
|
rules_directory: /tmp/loki/rules
|
||||||
|
replication_factor: 1
|
||||||
|
ring:
|
||||||
|
kvstore:
|
||||||
|
store: inmemory
|
||||||
|
|
||||||
|
query_range:
|
||||||
|
results_cache:
|
||||||
|
cache:
|
||||||
|
embedded_cache:
|
||||||
|
enabled: true
|
||||||
|
max_size_mb: 100
|
||||||
|
|
||||||
|
limits_config:
|
||||||
|
metric_aggregation_enabled: true
|
||||||
|
|
||||||
|
schema_config:
|
||||||
|
configs:
|
||||||
|
- from: 2020-10-24
|
||||||
|
store: tsdb
|
||||||
|
object_store: filesystem
|
||||||
|
schema: v13
|
||||||
|
index:
|
||||||
|
prefix: index_
|
||||||
|
period: 24h
|
||||||
|
|
||||||
|
pattern_ingester:
|
||||||
|
enabled: true
|
||||||
|
metric_aggregation:
|
||||||
|
loki_address: localhost:3100
|
||||||
|
|
||||||
|
ruler:
|
||||||
|
alertmanager_url: http://localhost:9093
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
encoding: protobuf
|
||||||
|
|
||||||
|
# By default, Loki will send anonymous, but uniquely-identifiable usage and configuration
|
||||||
|
# analytics to Grafana Labs. These statistics are sent to https://stats.grafana.org/
|
||||||
|
#
|
||||||
|
# Statistics help us better understand how Loki is used, and they show us performance
|
||||||
|
# levels for most users. This helps us prioritize features and documentation.
|
||||||
|
# For more information on what's sent, look at
|
||||||
|
# https://github.com/grafana/loki/blob/main/pkg/analytics/stats.go
|
||||||
|
# Refer to the buildReport method to see what goes into a report.
|
||||||
|
#
|
||||||
|
# If you would like to disable reporting, uncomment the following lines:
|
||||||
|
#analytics:
|
||||||
|
# reporting_enabled: false
|
||||||
81
.docker/observability/otel-collector-config.yaml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
receivers:
|
||||||
|
otlp:
|
||||||
|
protocols:
|
||||||
|
grpc: # OTLP gRPC 接收器
|
||||||
|
endpoint: 0.0.0.0:4317
|
||||||
|
http: # OTLP HTTP 接收器
|
||||||
|
endpoint: 0.0.0.0:4318
|
||||||
|
|
||||||
|
processors:
|
||||||
|
batch: # 批处理处理器,提升吞吐量
|
||||||
|
timeout: 5s
|
||||||
|
send_batch_size: 1000
|
||||||
|
memory_limiter:
|
||||||
|
check_interval: 1s
|
||||||
|
limit_mib: 512
|
||||||
|
|
||||||
|
exporters:
|
||||||
|
otlp/traces: # OTLP 导出器,用于跟踪数据
|
||||||
|
endpoint: "jaeger:4317" # Jaeger 的 OTLP gRPC 端点
|
||||||
|
tls:
|
||||||
|
insecure: true # 开发环境禁用 TLS,生产环境需配置证书
|
||||||
|
otlp/tempo: # OTLP 导出器,用于跟踪数据
|
||||||
|
endpoint: "tempo:4317" # tempo 的 OTLP gRPC 端点
|
||||||
|
tls:
|
||||||
|
insecure: true # 开发环境禁用 TLS,生产环境需配置证书
|
||||||
|
prometheus: # Prometheus 导出器,用于指标数据
|
||||||
|
endpoint: "0.0.0.0:8889" # Prometheus 刮取端点
|
||||||
|
namespace: "rustfs" # 指标前缀
|
||||||
|
send_timestamps: true # 发送时间戳
|
||||||
|
# enable_open_metrics: true
|
||||||
|
loki: # Loki 导出器,用于日志数据
|
||||||
|
# endpoint: "http://loki:3100/otlp/v1/logs"
|
||||||
|
endpoint: "http://loki:3100/loki/api/v1/push"
|
||||||
|
tls:
|
||||||
|
insecure: true
|
||||||
|
extensions:
|
||||||
|
health_check:
|
||||||
|
pprof:
|
||||||
|
zpages:
|
||||||
|
service:
|
||||||
|
extensions: [ health_check, pprof, zpages ] # 启用扩展
|
||||||
|
pipelines:
|
||||||
|
traces:
|
||||||
|
receivers: [ otlp ]
|
||||||
|
processors: [ memory_limiter,batch ]
|
||||||
|
exporters: [ otlp/traces,otlp/tempo ]
|
||||||
|
metrics:
|
||||||
|
receivers: [ otlp ]
|
||||||
|
processors: [ batch ]
|
||||||
|
exporters: [ prometheus ]
|
||||||
|
logs:
|
||||||
|
receivers: [ otlp ]
|
||||||
|
processors: [ batch ]
|
||||||
|
exporters: [ loki ]
|
||||||
|
telemetry:
|
||||||
|
logs:
|
||||||
|
level: "info" # Collector 日志级别
|
||||||
|
metrics:
|
||||||
|
level: "detailed" # 可以是 basic, normal, detailed
|
||||||
|
readers:
|
||||||
|
- periodic:
|
||||||
|
exporter:
|
||||||
|
otlp:
|
||||||
|
protocol: http/protobuf
|
||||||
|
endpoint: http://otel-collector:4318
|
||||||
|
|
||||||
|
|
||||||
28
.docker/observability/prometheus.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
global:
|
||||||
|
scrape_interval: 5s # 刮取间隔
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'otel-collector'
|
||||||
|
static_configs:
|
||||||
|
- targets: [ 'otel-collector:8888' ] # 从 Collector 刮取指标
|
||||||
|
- job_name: 'otel-metrics'
|
||||||
|
static_configs:
|
||||||
|
- targets: [ 'otel-collector:8889' ] # 应用指标
|
||||||
|
- job_name: 'tempo'
|
||||||
|
static_configs:
|
||||||
|
- targets: [ 'tempo:3200' ]
|
||||||
|
|
||||||
1
.docker/observability/tempo-data/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*
|
||||||
8
.docker/observability/tempo-entrypoint.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Run as root to fix directory permissions
|
||||||
|
chown -R 10001:10001 /var/tempo
|
||||||
|
|
||||||
|
# Use su-exec (a lightweight sudo/gosu alternative, commonly used in Alpine mirroring)
|
||||||
|
# Switch to user 10001 and execute the original command (CMD) passed to the script
|
||||||
|
# "$@" represents all parameters passed to this script, i.e. command in docker-compose
|
||||||
|
exec su-exec 10001:10001 /tempo "$@"
|
||||||
55
.docker/observability/tempo.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
stream_over_http_enabled: true
|
||||||
|
server:
|
||||||
|
http_listen_port: 3200
|
||||||
|
log_level: info
|
||||||
|
|
||||||
|
query_frontend:
|
||||||
|
search:
|
||||||
|
duration_slo: 5s
|
||||||
|
throughput_bytes_slo: 1.073741824e+09
|
||||||
|
metadata_slo:
|
||||||
|
duration_slo: 5s
|
||||||
|
throughput_bytes_slo: 1.073741824e+09
|
||||||
|
trace_by_id:
|
||||||
|
duration_slo: 5s
|
||||||
|
|
||||||
|
distributor:
|
||||||
|
receivers:
|
||||||
|
otlp:
|
||||||
|
protocols:
|
||||||
|
grpc:
|
||||||
|
endpoint: "tempo:4317"
|
||||||
|
|
||||||
|
ingester:
|
||||||
|
max_block_duration: 5m # cut the headblock when this much time passes. this is being set for demo purposes and should probably be left alone normally
|
||||||
|
|
||||||
|
compactor:
|
||||||
|
compaction:
|
||||||
|
block_retention: 1h # overall Tempo trace retention. set for demo purposes
|
||||||
|
|
||||||
|
metrics_generator:
|
||||||
|
registry:
|
||||||
|
external_labels:
|
||||||
|
source: tempo
|
||||||
|
cluster: docker-compose
|
||||||
|
storage:
|
||||||
|
path: /var/tempo/generator/wal
|
||||||
|
remote_write:
|
||||||
|
- url: http://prometheus:9090/api/v1/write
|
||||||
|
send_exemplars: true
|
||||||
|
traces_storage:
|
||||||
|
path: /var/tempo/generator/traces
|
||||||
|
|
||||||
|
storage:
|
||||||
|
trace:
|
||||||
|
backend: local # backend configuration to use
|
||||||
|
wal:
|
||||||
|
path: /var/tempo/wal # where to store the wal locally
|
||||||
|
local:
|
||||||
|
path: /var/tempo/blocks
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
defaults:
|
||||||
|
metrics_generator:
|
||||||
|
processors: [ service-graphs, span-metrics, local-blocks ] # enables metrics generator
|
||||||
|
generate_native_histograms: both
|
||||||
75
.docker/openobserve-otel/README.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# OpenObserve + OpenTelemetry Collector
|
||||||
|
|
||||||
|
[](https://openobserve.org)
|
||||||
|
[](https://opentelemetry.io/)
|
||||||
|
|
||||||
|
English | [中文](README_ZH.md)
|
||||||
|
|
||||||
|
This directory contains the configuration files for setting up an observability stack with OpenObserve and OpenTelemetry
|
||||||
|
Collector.
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
This setup provides a complete observability solution for your applications:
|
||||||
|
|
||||||
|
- **OpenObserve**: A modern, open-source observability platform for logs, metrics, and traces.
|
||||||
|
- **OpenTelemetry Collector**: Collects and processes telemetry data before sending it to OpenObserve.
|
||||||
|
|
||||||
|
### Setup Instructions
|
||||||
|
|
||||||
|
1. **Prerequisites**:
|
||||||
|
- Docker and Docker Compose installed
|
||||||
|
- Sufficient memory resources (minimum 2GB recommended)
|
||||||
|
|
||||||
|
2. **Starting the Services**:
|
||||||
|
```bash
|
||||||
|
cd .docker/openobserve-otel
|
||||||
|
docker compose -f docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Accessing the Dashboard**:
|
||||||
|
- OpenObserve UI: http://localhost:5080
|
||||||
|
- Default credentials:
|
||||||
|
- Username: root@rustfs.com
|
||||||
|
- Password: rustfs123
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
#### OpenObserve Configuration
|
||||||
|
|
||||||
|
The OpenObserve service is configured with:
|
||||||
|
|
||||||
|
- Root user credentials
|
||||||
|
- Data persistence through a volume mount
|
||||||
|
- Memory cache enabled
|
||||||
|
- Health checks
|
||||||
|
- Exposed ports:
|
||||||
|
- 5080: HTTP API and UI
|
||||||
|
- 5081: OTLP gRPC
|
||||||
|
|
||||||
|
#### OpenTelemetry Collector Configuration
|
||||||
|
|
||||||
|
The collector is configured to:
|
||||||
|
|
||||||
|
- Receive telemetry data via OTLP (HTTP and gRPC)
|
||||||
|
- Collect logs from files
|
||||||
|
- Process data in batches
|
||||||
|
- Export data to OpenObserve
|
||||||
|
- Manage memory usage
|
||||||
|
|
||||||
|
### Integration with Your Application
|
||||||
|
|
||||||
|
To send telemetry data from your application, configure your OpenTelemetry SDK to send data to:
|
||||||
|
|
||||||
|
- OTLP gRPC: `localhost:4317`
|
||||||
|
- OTLP HTTP: `localhost:4318`
|
||||||
|
|
||||||
|
For example, in a Rust application using the `rustfs-obs` library:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export RUSTFS_OBS_ENDPOINT=http://localhost:4317
|
||||||
|
export RUSTFS_OBS_SERVICE_NAME=yourservice
|
||||||
|
export RUSTFS_OBS_SERVICE_VERSION=1.0.0
|
||||||
|
export RUSTFS_OBS_ENVIRONMENT=development
|
||||||
|
```
|
||||||
|
|
||||||
75
.docker/openobserve-otel/README_ZH.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# OpenObserve + OpenTelemetry Collector
|
||||||
|
|
||||||
|
[](https://openobserve.org)
|
||||||
|
[](https://opentelemetry.io/)
|
||||||
|
|
||||||
|
[English](README.md) | 中文
|
||||||
|
|
||||||
|
## 中文
|
||||||
|
|
||||||
|
本目录包含搭建 OpenObserve 和 OpenTelemetry Collector 可观测性栈的配置文件。
|
||||||
|
|
||||||
|
### 概述
|
||||||
|
|
||||||
|
此设置为应用程序提供了完整的可观测性解决方案:
|
||||||
|
|
||||||
|
- **OpenObserve**:现代化、开源的可观测性平台,用于日志、指标和追踪。
|
||||||
|
- **OpenTelemetry Collector**:收集和处理遥测数据,然后将其发送到 OpenObserve。
|
||||||
|
|
||||||
|
### 设置说明
|
||||||
|
|
||||||
|
1. **前提条件**:
|
||||||
|
- 已安装 Docker 和 Docker Compose
|
||||||
|
- 足够的内存资源(建议至少 2GB)
|
||||||
|
|
||||||
|
2. **启动服务**:
|
||||||
|
```bash
|
||||||
|
cd .docker/openobserve-otel
|
||||||
|
docker compose -f docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **访问仪表板**:
|
||||||
|
- OpenObserve UI:http://localhost:5080
|
||||||
|
- 默认凭据:
|
||||||
|
- 用户名:root@rustfs.com
|
||||||
|
- 密码:rustfs123
|
||||||
|
|
||||||
|
### 配置
|
||||||
|
|
||||||
|
#### OpenObserve 配置
|
||||||
|
|
||||||
|
OpenObserve 服务配置:
|
||||||
|
|
||||||
|
- 根用户凭据
|
||||||
|
- 通过卷挂载实现数据持久化
|
||||||
|
- 启用内存缓存
|
||||||
|
- 健康检查
|
||||||
|
- 暴露端口:
|
||||||
|
- 5080:HTTP API 和 UI
|
||||||
|
- 5081:OTLP gRPC
|
||||||
|
|
||||||
|
#### OpenTelemetry Collector 配置
|
||||||
|
|
||||||
|
收集器配置为:
|
||||||
|
|
||||||
|
- 通过 OTLP(HTTP 和 gRPC)接收遥测数据
|
||||||
|
- 从文件中收集日志
|
||||||
|
- 批处理数据
|
||||||
|
- 将数据导出到 OpenObserve
|
||||||
|
- 管理内存使用
|
||||||
|
|
||||||
|
### 与应用程序集成
|
||||||
|
|
||||||
|
要从应用程序发送遥测数据,将 OpenTelemetry SDK 配置为发送数据到:
|
||||||
|
|
||||||
|
- OTLP gRPC:`localhost:4317`
|
||||||
|
- OTLP HTTP:`localhost:4318`
|
||||||
|
|
||||||
|
例如,在使用 `rustfs-obs` 库的 Rust 应用程序中:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export RUSTFS_OBS_ENDPOINT=http://localhost:4317
|
||||||
|
export RUSTFS_OBS_SERVICE_NAME=yourservice
|
||||||
|
export RUSTFS_OBS_SERVICE_VERSION=1.0.0
|
||||||
|
export RUSTFS_OBS_ENVIRONMENT=development
|
||||||
|
```
|
||||||
87
.docker/openobserve-otel/docker-compose.yml
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
services:
|
||||||
|
openobserve:
|
||||||
|
image: public.ecr.aws/zinclabs/openobserve:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
ZO_ROOT_USER_EMAIL: "root@rustfs.com"
|
||||||
|
ZO_ROOT_USER_PASSWORD: "rustfs123"
|
||||||
|
ZO_TRACING_HEADER_KEY: "Authorization"
|
||||||
|
ZO_TRACING_HEADER_VALUE: "Basic cm9vdEBydXN0ZnMuY29tOmQ4SXlCSEJTUkk3RGVlcEQ="
|
||||||
|
ZO_DATA_DIR: "/data"
|
||||||
|
ZO_MEMORY_CACHE_ENABLED: "true"
|
||||||
|
ZO_MEMORY_CACHE_MAX_SIZE: "256"
|
||||||
|
RUST_LOG: "info"
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
ports:
|
||||||
|
- "5080:5080"
|
||||||
|
- "5081:5081"
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "curl", "-f", "http://localhost:5080/health" ]
|
||||||
|
start_period: 60s
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 6
|
||||||
|
networks:
|
||||||
|
- otel-network
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1024M
|
||||||
|
reservations:
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
otel-collector:
|
||||||
|
image: otel/opentelemetry-collector-contrib:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml
|
||||||
|
ports:
|
||||||
|
- "4317:4317" # OTLP gRPC
|
||||||
|
- "4318:4318" # OTLP HTTP
|
||||||
|
- "13133:13133" # Health check
|
||||||
|
- "1777:1777" # pprof
|
||||||
|
- "55679:55679" # zpages
|
||||||
|
- "1888:1888" # Metrics
|
||||||
|
- "8888:8888" # Prometheus metrics
|
||||||
|
- "8889:8889" # Additional metrics endpoint
|
||||||
|
depends_on:
|
||||||
|
- openobserve
|
||||||
|
networks:
|
||||||
|
- otel-network
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 10240M
|
||||||
|
reservations:
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
networks:
|
||||||
|
otel-network:
|
||||||
|
driver: bridge
|
||||||
|
name: otel-network
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.28.0.0/16
|
||||||
|
gateway: 172.28.0.1
|
||||||
|
labels:
|
||||||
|
com.example.description: "Network for OpenObserve and OpenTelemetry Collector"
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
92
.docker/openobserve-otel/otel-collector-config.yaml
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
receivers:
|
||||||
|
otlp:
|
||||||
|
protocols:
|
||||||
|
grpc:
|
||||||
|
endpoint: 0.0.0.0:4317
|
||||||
|
http:
|
||||||
|
endpoint: 0.0.0.0:4318
|
||||||
|
filelog:
|
||||||
|
include: [ "/var/log/app/*.log" ]
|
||||||
|
start_at: end
|
||||||
|
|
||||||
|
processors:
|
||||||
|
batch:
|
||||||
|
timeout: 1s
|
||||||
|
send_batch_size: 1024
|
||||||
|
memory_limiter:
|
||||||
|
check_interval: 1s
|
||||||
|
limit_mib: 400
|
||||||
|
spike_limit_mib: 100
|
||||||
|
|
||||||
|
exporters:
|
||||||
|
otlphttp/openobserve:
|
||||||
|
endpoint: http://openobserve:5080/api/default # http://127.0.0.1:5080/api/default
|
||||||
|
headers:
|
||||||
|
Authorization: "Basic cm9vdEBydXN0ZnMuY29tOmQ4SXlCSEJTUkk3RGVlcEQ="
|
||||||
|
stream-name: default
|
||||||
|
organization: default
|
||||||
|
compression: gzip
|
||||||
|
retry_on_failure:
|
||||||
|
enabled: true
|
||||||
|
initial_interval: 5s
|
||||||
|
max_interval: 30s
|
||||||
|
max_elapsed_time: 300s
|
||||||
|
timeout: 10s
|
||||||
|
otlp/openobserve:
|
||||||
|
endpoint: openobserve:5081 # http://127.0.0.1:5080/api/default
|
||||||
|
headers:
|
||||||
|
Authorization: "Basic cm9vdEBydXN0ZnMuY29tOmQ4SXlCSEJTUkk3RGVlcEQ="
|
||||||
|
stream-name: default
|
||||||
|
organization: default
|
||||||
|
compression: gzip
|
||||||
|
retry_on_failure:
|
||||||
|
enabled: true
|
||||||
|
initial_interval: 5s
|
||||||
|
max_interval: 30s
|
||||||
|
max_elapsed_time: 300s
|
||||||
|
timeout: 10s
|
||||||
|
tls:
|
||||||
|
insecure: true
|
||||||
|
|
||||||
|
extensions:
|
||||||
|
health_check:
|
||||||
|
endpoint: 0.0.0.0:13133
|
||||||
|
pprof:
|
||||||
|
endpoint: 0.0.0.0:1777
|
||||||
|
zpages:
|
||||||
|
endpoint: 0.0.0.0:55679
|
||||||
|
|
||||||
|
service:
|
||||||
|
extensions: [ health_check, pprof, zpages ]
|
||||||
|
pipelines:
|
||||||
|
traces:
|
||||||
|
receivers: [ otlp ]
|
||||||
|
processors: [ memory_limiter, batch ]
|
||||||
|
exporters: [ otlp/openobserve ]
|
||||||
|
metrics:
|
||||||
|
receivers: [ otlp ]
|
||||||
|
processors: [ memory_limiter, batch ]
|
||||||
|
exporters: [ otlp/openobserve ]
|
||||||
|
logs:
|
||||||
|
receivers: [ otlp, filelog ]
|
||||||
|
processors: [ memory_limiter, batch ]
|
||||||
|
exporters: [ otlp/openobserve ]
|
||||||
|
telemetry:
|
||||||
|
logs:
|
||||||
|
level: "info" # Collector 日志级别
|
||||||
|
metrics:
|
||||||
|
address: "0.0.0.0:8888" # Collector 自身指标暴露
|
||||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
98
.github/actions/setup/action.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
name: "Setup Rust Environment"
|
||||||
|
description: "Setup Rust development environment with caching for RustFS"
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
rust-version:
|
||||||
|
description: "Rust version to install"
|
||||||
|
required: false
|
||||||
|
default: "stable"
|
||||||
|
cache-shared-key:
|
||||||
|
description: "Shared cache key for Rust dependencies"
|
||||||
|
required: false
|
||||||
|
default: "rustfs-deps"
|
||||||
|
cache-save-if:
|
||||||
|
description: "Condition for saving cache"
|
||||||
|
required: false
|
||||||
|
default: "true"
|
||||||
|
install-cross-tools:
|
||||||
|
description: "Install cross-compilation tools"
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
target:
|
||||||
|
description: "Target architecture to add"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
github-token:
|
||||||
|
description: "GitHub token for API access"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Install system dependencies (Ubuntu)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
musl-tools \
|
||||||
|
build-essential \
|
||||||
|
lld \
|
||||||
|
libdbus-1-dev \
|
||||||
|
libwayland-dev \
|
||||||
|
libwebkit2gtk-4.1-dev \
|
||||||
|
libxdo-dev \
|
||||||
|
pkg-config \
|
||||||
|
libssl-dev
|
||||||
|
|
||||||
|
- name: Install protoc
|
||||||
|
uses: arduino/setup-protoc@v3
|
||||||
|
with:
|
||||||
|
version: "31.1"
|
||||||
|
repo-token: ${{ inputs.github-token }}
|
||||||
|
|
||||||
|
- name: Install flatc
|
||||||
|
uses: Nugine/setup-flatc@v1
|
||||||
|
with:
|
||||||
|
version: "25.2.10"
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
toolchain: ${{ inputs.rust-version }}
|
||||||
|
targets: ${{ inputs.target }}
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Install Zig
|
||||||
|
if: inputs.install-cross-tools == 'true'
|
||||||
|
uses: mlugg/setup-zig@v2
|
||||||
|
|
||||||
|
- name: Install cargo-zigbuild
|
||||||
|
if: inputs.install-cross-tools == 'true'
|
||||||
|
uses: taiki-e/install-action@cargo-zigbuild
|
||||||
|
|
||||||
|
- name: Install cargo-nextest
|
||||||
|
uses: taiki-e/install-action@cargo-nextest
|
||||||
|
|
||||||
|
- name: Setup Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
cache-all-crates: true
|
||||||
|
cache-on-failure: true
|
||||||
|
shared-key: ${{ inputs.cache-shared-key }}
|
||||||
|
save-if: ${{ inputs.cache-save-if }}
|
||||||
29
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "cargo" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
|
groups:
|
||||||
|
dependencies:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
39
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!--
|
||||||
|
Pull Request Template for RustFS
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Type of Change
|
||||||
|
- [ ] New Feature
|
||||||
|
- [ ] Bug Fix
|
||||||
|
- [ ] Documentation
|
||||||
|
- [ ] Performance Improvement
|
||||||
|
- [ ] Test/CI
|
||||||
|
- [ ] Refactor
|
||||||
|
- [ ] Other:
|
||||||
|
|
||||||
|
## Related Issues
|
||||||
|
<!-- List related Issue numbers, e.g. #123 -->
|
||||||
|
|
||||||
|
## Summary of Changes
|
||||||
|
<!-- Briefly describe the main changes and motivation for this PR -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have read and followed the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines
|
||||||
|
- [ ] Code is formatted with `cargo fmt --all`
|
||||||
|
- [ ] Passed `cargo clippy --all-targets --all-features -- -D warnings`
|
||||||
|
- [ ] Passed `cargo check --all-targets`
|
||||||
|
- [ ] Added/updated necessary tests
|
||||||
|
- [ ] Documentation updated (if needed)
|
||||||
|
- [ ] CI/CD passed (if applicable)
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
- [ ] Breaking change (compatibility)
|
||||||
|
- [ ] Requires doc/config/deployment update
|
||||||
|
- [ ] Other impact:
|
||||||
|
|
||||||
|
## Additional Notes
|
||||||
|
<!-- Any extra information for reviewers -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Thank you for your contribution! Please ensure your PR follows the community standards ([CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)) and sign the CLA if this is your first contribution.
|
||||||
78
.github/workflows/audit.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
name: Security Audit
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '.github/workflows/audit.yml'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- '**/Cargo.toml'
|
||||||
|
- '**/Cargo.lock'
|
||||||
|
- '.github/workflows/audit.yml'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 0' # Weekly on Sunday at midnight UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security-audit:
|
||||||
|
name: Security Audit
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install cargo-audit
|
||||||
|
uses: taiki-e/install-action@v2
|
||||||
|
with:
|
||||||
|
tool: cargo-audit
|
||||||
|
|
||||||
|
- name: Run security audit
|
||||||
|
run: |
|
||||||
|
cargo audit -D warnings --json | tee audit-results.json
|
||||||
|
|
||||||
|
- name: Upload audit results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: security-audit-results-${{ github.run_number }}
|
||||||
|
path: audit-results.json
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
dependency-review:
|
||||||
|
name: Dependency Review
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Dependency Review
|
||||||
|
uses: actions/dependency-review-action@v4
|
||||||
|
with:
|
||||||
|
fail-on-severity: moderate
|
||||||
|
comment-summary-in-pr: true
|
||||||
756
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,756 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# Build and Release Workflow
|
||||||
|
#
|
||||||
|
# This workflow builds RustFS binaries and automatically triggers Docker image builds.
|
||||||
|
#
|
||||||
|
# Flow:
|
||||||
|
# 1. Build binaries for multiple platforms
|
||||||
|
# 2. Upload binaries to OSS storage
|
||||||
|
# 3. Trigger docker.yml to build and push images using the uploaded binaries
|
||||||
|
#
|
||||||
|
# Manual Parameters:
|
||||||
|
# - build_docker: Build and push Docker images (default: true)
|
||||||
|
|
||||||
|
name: Build and Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["*.*.*"]
|
||||||
|
branches: [main]
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
- "**.txt"
|
||||||
|
- ".github/**"
|
||||||
|
- "docs/**"
|
||||||
|
- "deploy/**"
|
||||||
|
- "scripts/dev_*.sh"
|
||||||
|
- "LICENSE*"
|
||||||
|
- "README*"
|
||||||
|
- "**/*.png"
|
||||||
|
- "**/*.jpg"
|
||||||
|
- "**/*.svg"
|
||||||
|
- ".gitignore"
|
||||||
|
- ".dockerignore"
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
- "**.txt"
|
||||||
|
- ".github/**"
|
||||||
|
- "docs/**"
|
||||||
|
- "deploy/**"
|
||||||
|
- "scripts/dev_*.sh"
|
||||||
|
- "LICENSE*"
|
||||||
|
- "README*"
|
||||||
|
- "**/*.png"
|
||||||
|
- "**/*.jpg"
|
||||||
|
- "**/*.svg"
|
||||||
|
- ".gitignore"
|
||||||
|
- ".dockerignore"
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * 0" # Weekly on Sunday at midnight UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
build_docker:
|
||||||
|
description: "Build and push Docker images after binary build"
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
# Optimize build performance
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Build strategy check - determine build type based on trigger
|
||||||
|
build-check:
|
||||||
|
name: Build Strategy Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_build: ${{ steps.check.outputs.should_build }}
|
||||||
|
build_type: ${{ steps.check.outputs.build_type }}
|
||||||
|
version: ${{ steps.check.outputs.version }}
|
||||||
|
short_sha: ${{ steps.check.outputs.short_sha }}
|
||||||
|
is_prerelease: ${{ steps.check.outputs.is_prerelease }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Determine build strategy
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
should_build=false
|
||||||
|
build_type="none"
|
||||||
|
version=""
|
||||||
|
short_sha=""
|
||||||
|
is_prerelease=false
|
||||||
|
|
||||||
|
# Get short SHA for all builds
|
||||||
|
short_sha=$(git rev-parse --short HEAD)
|
||||||
|
|
||||||
|
# Determine build type based on trigger
|
||||||
|
if [[ "${{ startsWith(github.ref, 'refs/tags/') }}" == "true" ]]; then
|
||||||
|
# Tag push - release or prerelease
|
||||||
|
should_build=true
|
||||||
|
tag_name="${GITHUB_REF#refs/tags/}"
|
||||||
|
version="${tag_name}"
|
||||||
|
|
||||||
|
# Check if this is a prerelease
|
||||||
|
if [[ "$tag_name" == *"alpha"* ]] || [[ "$tag_name" == *"beta"* ]] || [[ "$tag_name" == *"rc"* ]]; then
|
||||||
|
build_type="prerelease"
|
||||||
|
is_prerelease=true
|
||||||
|
echo "🚀 Prerelease build detected: $tag_name"
|
||||||
|
else
|
||||||
|
build_type="release"
|
||||||
|
echo "📦 Release build detected: $tag_name"
|
||||||
|
fi
|
||||||
|
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||||
|
# Main branch push - development build
|
||||||
|
should_build=true
|
||||||
|
build_type="development"
|
||||||
|
version="dev-${short_sha}"
|
||||||
|
echo "🛠️ Development build detected"
|
||||||
|
elif [[ "${{ github.event_name }}" == "schedule" ]] || \
|
||||||
|
[[ "${{ github.event_name }}" == "workflow_dispatch" ]] || \
|
||||||
|
[[ "${{ contains(github.event.head_commit.message, '--build') }}" == "true" ]]; then
|
||||||
|
# Scheduled or manual build
|
||||||
|
should_build=true
|
||||||
|
build_type="development"
|
||||||
|
version="dev-${short_sha}"
|
||||||
|
echo "⚡ Manual/scheduled build detected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "should_build=$should_build" >> $GITHUB_OUTPUT
|
||||||
|
echo "build_type=$build_type" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=$version" >> $GITHUB_OUTPUT
|
||||||
|
echo "short_sha=$short_sha" >> $GITHUB_OUTPUT
|
||||||
|
echo "is_prerelease=$is_prerelease" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
echo "📊 Build Summary:"
|
||||||
|
echo " - Should build: $should_build"
|
||||||
|
echo " - Build type: $build_type"
|
||||||
|
echo " - Version: $version"
|
||||||
|
echo " - Short SHA: $short_sha"
|
||||||
|
echo " - Is prerelease: $is_prerelease"
|
||||||
|
|
||||||
|
# Build RustFS binaries
|
||||||
|
build-rustfs:
|
||||||
|
name: Build RustFS
|
||||||
|
needs: [build-check]
|
||||||
|
if: needs.build-check.outputs.should_build == 'true'
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 60
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: ${{ matrix.cross == 'false' && '-C target-cpu=native' || '' }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# Linux builds
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: x86_64-unknown-linux-musl
|
||||||
|
cross: false
|
||||||
|
platform: linux
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: aarch64-unknown-linux-musl
|
||||||
|
cross: true
|
||||||
|
platform: linux
|
||||||
|
# macOS builds
|
||||||
|
- os: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
cross: false
|
||||||
|
platform: macos
|
||||||
|
- os: macos-latest
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
cross: false
|
||||||
|
platform: macos
|
||||||
|
# # Windows builds (temporarily disabled)
|
||||||
|
# - os: windows-latest
|
||||||
|
# target: x86_64-pc-windows-msvc
|
||||||
|
# cross: false
|
||||||
|
# platform: windows
|
||||||
|
# - os: windows-latest
|
||||||
|
# target: aarch64-pc-windows-msvc
|
||||||
|
# cross: true
|
||||||
|
# platform: windows
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Rust environment
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
rust-version: stable
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
cache-shared-key: build-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
cache-save-if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }}
|
||||||
|
install-cross-tools: ${{ matrix.cross }}
|
||||||
|
|
||||||
|
- name: Download static console assets
|
||||||
|
run: |
|
||||||
|
mkdir -p ./rustfs/static
|
||||||
|
if [[ "${{ matrix.platform }}" == "windows" ]]; then
|
||||||
|
curl.exe -L "https://dl.rustfs.com/artifacts/console/rustfs-console-latest.zip" -o console.zip --retry 3 --retry-delay 5 --max-time 300
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
unzip -o console.zip -d ./rustfs/static
|
||||||
|
rm console.zip
|
||||||
|
else
|
||||||
|
echo "Warning: Failed to download console assets, continuing without them"
|
||||||
|
echo "// Static assets not available" > ./rustfs/static/empty.txt
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
chmod +w ./rustfs/static/LICENSE || true
|
||||||
|
curl -L "https://dl.rustfs.com/artifacts/console/rustfs-console-latest.zip" \
|
||||||
|
-o console.zip --retry 3 --retry-delay 5 --max-time 300
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
unzip -o console.zip -d ./rustfs/static
|
||||||
|
rm console.zip
|
||||||
|
else
|
||||||
|
echo "Warning: Failed to download console assets, continuing without them"
|
||||||
|
echo "// Static assets not available" > ./rustfs/static/empty.txt
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build RustFS
|
||||||
|
run: |
|
||||||
|
# Force rebuild by touching build.rs
|
||||||
|
touch rustfs/build.rs
|
||||||
|
|
||||||
|
if [[ "${{ matrix.cross }}" == "true" ]]; then
|
||||||
|
if [[ "${{ matrix.platform }}" == "windows" ]]; then
|
||||||
|
# Use cross for Windows ARM64
|
||||||
|
cargo install cross --git https://github.com/cross-rs/cross
|
||||||
|
cross build --release --target ${{ matrix.target }} -p rustfs --bins
|
||||||
|
else
|
||||||
|
# Use zigbuild for other cross-compilation
|
||||||
|
cargo zigbuild --release --target ${{ matrix.target }} -p rustfs --bins
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
cargo build --release --target ${{ matrix.target }} -p rustfs --bins
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create release package
|
||||||
|
id: package
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
BUILD_TYPE="${{ needs.build-check.outputs.build_type }}"
|
||||||
|
VERSION="${{ needs.build-check.outputs.version }}"
|
||||||
|
SHORT_SHA="${{ needs.build-check.outputs.short_sha }}"
|
||||||
|
|
||||||
|
# Extract platform and arch from target
|
||||||
|
TARGET="${{ matrix.target }}"
|
||||||
|
PLATFORM="${{ matrix.platform }}"
|
||||||
|
|
||||||
|
# Map target to architecture
|
||||||
|
case "$TARGET" in
|
||||||
|
*x86_64*)
|
||||||
|
ARCH="x86_64"
|
||||||
|
;;
|
||||||
|
*aarch64*|*arm64*)
|
||||||
|
ARCH="aarch64"
|
||||||
|
;;
|
||||||
|
*armv7*)
|
||||||
|
ARCH="armv7"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
ARCH="unknown"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Generate package name based on build type
|
||||||
|
if [[ "$BUILD_TYPE" == "development" ]]; then
|
||||||
|
# Development build: rustfs-${platform}-${arch}-dev-${short_sha}.zip
|
||||||
|
PACKAGE_NAME="rustfs-${PLATFORM}-${ARCH}-dev-${SHORT_SHA}"
|
||||||
|
else
|
||||||
|
# Release/Prerelease build: rustfs-${platform}-${arch}-v${version}.zip
|
||||||
|
PACKAGE_NAME="rustfs-${PLATFORM}-${ARCH}-v${VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create zip packages for all platforms
|
||||||
|
# Ensure zip is available
|
||||||
|
if ! command -v zip &> /dev/null; then
|
||||||
|
if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then
|
||||||
|
sudo apt-get update && sudo apt-get install -y zip
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd target/${{ matrix.target }}/release
|
||||||
|
zip "../../../${PACKAGE_NAME}.zip" rustfs
|
||||||
|
cd ../../..
|
||||||
|
|
||||||
|
echo "package_name=${PACKAGE_NAME}" >> $GITHUB_OUTPUT
|
||||||
|
echo "package_file=${PACKAGE_NAME}.zip" >> $GITHUB_OUTPUT
|
||||||
|
echo "build_type=${BUILD_TYPE}" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
echo "📦 Package created: ${PACKAGE_NAME}.zip"
|
||||||
|
echo "🔧 Build type: ${BUILD_TYPE}"
|
||||||
|
echo "📊 Version: ${VERSION}"
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ steps.package.outputs.package_name }}
|
||||||
|
path: ${{ steps.package.outputs.package_file }}
|
||||||
|
retention-days: ${{ startsWith(github.ref, 'refs/tags/') && 30 || 7 }}
|
||||||
|
|
||||||
|
- name: Upload to Aliyun OSS
|
||||||
|
if: env.OSS_ACCESS_KEY_ID != '' && (needs.build-check.outputs.build_type == 'release' || needs.build-check.outputs.build_type == 'prerelease' || needs.build-check.outputs.build_type == 'development')
|
||||||
|
env:
|
||||||
|
OSS_ACCESS_KEY_ID: ${{ secrets.ALICLOUDOSS_KEY_ID }}
|
||||||
|
OSS_ACCESS_KEY_SECRET: ${{ secrets.ALICLOUDOSS_KEY_SECRET }}
|
||||||
|
OSS_REGION: cn-beijing
|
||||||
|
OSS_ENDPOINT: https://oss-cn-beijing.aliyuncs.com
|
||||||
|
run: |
|
||||||
|
BUILD_TYPE="${{ needs.build-check.outputs.build_type }}"
|
||||||
|
|
||||||
|
# Install ossutil (platform-specific)
|
||||||
|
OSSUTIL_VERSION="2.1.1"
|
||||||
|
case "${{ matrix.platform }}" in
|
||||||
|
linux)
|
||||||
|
if [[ "$(uname -m)" == "arm64" ]]; then
|
||||||
|
ARCH="arm64"
|
||||||
|
else
|
||||||
|
ARCH="amd64"
|
||||||
|
fi
|
||||||
|
OSSUTIL_ZIP="ossutil-${OSSUTIL_VERSION}-linux-${ARCH}.zip"
|
||||||
|
OSSUTIL_DIR="ossutil-${OSSUTIL_VERSION}-linux-${ARCH}"
|
||||||
|
|
||||||
|
curl -o "$OSSUTIL_ZIP" "https://gosspublic.alicdn.com/ossutil/v2/${OSSUTIL_VERSION}/${OSSUTIL_ZIP}"
|
||||||
|
unzip "$OSSUTIL_ZIP"
|
||||||
|
mv "${OSSUTIL_DIR}/ossutil" /usr/local/bin/
|
||||||
|
rm -rf "$OSSUTIL_DIR" "$OSSUTIL_ZIP"
|
||||||
|
chmod +x /usr/local/bin/ossutil
|
||||||
|
OSSUTIL_BIN=ossutil
|
||||||
|
;;
|
||||||
|
macos)
|
||||||
|
if [[ "$(uname -m)" == "arm64" ]]; then
|
||||||
|
ARCH="arm64"
|
||||||
|
else
|
||||||
|
ARCH="amd64"
|
||||||
|
fi
|
||||||
|
OSSUTIL_ZIP="ossutil-${OSSUTIL_VERSION}-mac-${ARCH}.zip"
|
||||||
|
OSSUTIL_DIR="ossutil-${OSSUTIL_VERSION}-mac-${ARCH}"
|
||||||
|
|
||||||
|
curl -o "$OSSUTIL_ZIP" "https://gosspublic.alicdn.com/ossutil/v2/${OSSUTIL_VERSION}/${OSSUTIL_ZIP}"
|
||||||
|
unzip "$OSSUTIL_ZIP"
|
||||||
|
mv "${OSSUTIL_DIR}/ossutil" /usr/local/bin/
|
||||||
|
rm -rf "$OSSUTIL_DIR" "$OSSUTIL_ZIP"
|
||||||
|
chmod +x /usr/local/bin/ossutil
|
||||||
|
OSSUTIL_BIN=ossutil
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Determine upload path based on build type
|
||||||
|
if [[ "$BUILD_TYPE" == "development" ]]; then
|
||||||
|
OSS_PATH="oss://rustfs-artifacts/artifacts/rustfs/dev/"
|
||||||
|
echo "📤 Uploading development build to OSS dev directory"
|
||||||
|
else
|
||||||
|
OSS_PATH="oss://rustfs-artifacts/artifacts/rustfs/release/"
|
||||||
|
echo "📤 Uploading release build to OSS release directory"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload the package file to OSS
|
||||||
|
echo "Uploading ${{ steps.package.outputs.package_file }} to $OSS_PATH..."
|
||||||
|
$OSSUTIL_BIN cp "${{ steps.package.outputs.package_file }}" "$OSS_PATH" --force
|
||||||
|
|
||||||
|
# For release and prerelease builds, also create a latest version
|
||||||
|
if [[ "$BUILD_TYPE" == "release" ]] || [[ "$BUILD_TYPE" == "prerelease" ]]; then
|
||||||
|
# Extract platform and arch from package name
|
||||||
|
PACKAGE_NAME="${{ steps.package.outputs.package_name }}"
|
||||||
|
|
||||||
|
# Create latest version filename
|
||||||
|
# Convert from rustfs-linux-x86_64-v1.0.0 to rustfs-linux-x86_64-latest
|
||||||
|
LATEST_FILE="${PACKAGE_NAME%-v*}-latest.zip"
|
||||||
|
|
||||||
|
# Copy the original file to latest version
|
||||||
|
cp "${{ steps.package.outputs.package_file }}" "$LATEST_FILE"
|
||||||
|
|
||||||
|
# Upload the latest version
|
||||||
|
echo "Uploading latest version: $LATEST_FILE to $OSS_PATH..."
|
||||||
|
$OSSUTIL_BIN cp "$LATEST_FILE" "$OSS_PATH" --force
|
||||||
|
|
||||||
|
echo "✅ Latest version uploaded: $LATEST_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For development builds, create dev-latest version
|
||||||
|
if [[ "$BUILD_TYPE" == "development" ]]; then
|
||||||
|
# Extract platform and arch from package name
|
||||||
|
PACKAGE_NAME="${{ steps.package.outputs.package_name }}"
|
||||||
|
|
||||||
|
# Create dev-latest version filename
|
||||||
|
# Convert from rustfs-linux-x86_64-dev-abc123 to rustfs-linux-x86_64-dev-latest
|
||||||
|
DEV_LATEST_FILE="${PACKAGE_NAME%-*}-latest.zip"
|
||||||
|
|
||||||
|
# Copy the original file to dev-latest version
|
||||||
|
cp "${{ steps.package.outputs.package_file }}" "$DEV_LATEST_FILE"
|
||||||
|
|
||||||
|
# Upload the dev-latest version
|
||||||
|
echo "Uploading dev-latest version: $DEV_LATEST_FILE to $OSS_PATH..."
|
||||||
|
$OSSUTIL_BIN cp "$DEV_LATEST_FILE" "$OSS_PATH" --force
|
||||||
|
|
||||||
|
echo "✅ Dev-latest version uploaded: $DEV_LATEST_FILE"
|
||||||
|
|
||||||
|
# For main branch builds, also create a main-latest version
|
||||||
|
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||||
|
# Create main-latest version filename
|
||||||
|
# Convert from rustfs-linux-x86_64-dev-abc123 to rustfs-linux-x86_64-main-latest
|
||||||
|
MAIN_LATEST_FILE="${PACKAGE_NAME%-dev-*}-main-latest.zip"
|
||||||
|
|
||||||
|
# Copy the original file to main-latest version
|
||||||
|
cp "${{ steps.package.outputs.package_file }}" "$MAIN_LATEST_FILE"
|
||||||
|
|
||||||
|
# Upload the main-latest version
|
||||||
|
echo "Uploading main-latest version: $MAIN_LATEST_FILE to $OSS_PATH..."
|
||||||
|
$OSSUTIL_BIN cp "$MAIN_LATEST_FILE" "$OSS_PATH" --force
|
||||||
|
|
||||||
|
echo "✅ Main-latest version uploaded: $MAIN_LATEST_FILE"
|
||||||
|
|
||||||
|
# Also create a generic main-latest for Docker builds
|
||||||
|
if [[ "${{ matrix.platform }}" == "linux" ]]; then
|
||||||
|
DOCKER_MAIN_LATEST_FILE="rustfs-linux-${{ matrix.target == 'x86_64-unknown-linux-musl' && 'x86_64' || 'aarch64' }}-main-latest.zip"
|
||||||
|
|
||||||
|
cp "${{ steps.package.outputs.package_file }}" "$DOCKER_MAIN_LATEST_FILE"
|
||||||
|
$OSSUTIL_BIN cp "$DOCKER_MAIN_LATEST_FILE" "$OSS_PATH" --force
|
||||||
|
echo "✅ Docker main-latest version uploaded: $DOCKER_MAIN_LATEST_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Upload completed successfully"
|
||||||
|
|
||||||
|
# Build summary
|
||||||
|
build-summary:
|
||||||
|
name: Build Summary
|
||||||
|
needs: [build-check, build-rustfs]
|
||||||
|
if: always() && needs.build-check.outputs.should_build == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Build completion summary
|
||||||
|
run: |
|
||||||
|
BUILD_TYPE="${{ needs.build-check.outputs.build_type }}"
|
||||||
|
VERSION="${{ needs.build-check.outputs.version }}"
|
||||||
|
|
||||||
|
echo "🎉 Build completed successfully!"
|
||||||
|
echo "📦 Build type: $BUILD_TYPE"
|
||||||
|
echo "🔢 Version: $VERSION"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check build status
|
||||||
|
BUILD_STATUS="${{ needs.build-rustfs.result }}"
|
||||||
|
|
||||||
|
echo "📊 Build Results:"
|
||||||
|
echo " 📦 All platforms: $BUILD_STATUS"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
case "$BUILD_TYPE" in
|
||||||
|
"development")
|
||||||
|
echo "🛠️ Development build artifacts have been uploaded to OSS dev directory"
|
||||||
|
echo "⚠️ This is a development build - not suitable for production use"
|
||||||
|
;;
|
||||||
|
"release")
|
||||||
|
echo "🚀 Release build artifacts have been uploaded to OSS release directory"
|
||||||
|
echo "✅ This build is ready for production use"
|
||||||
|
echo "🏷️ GitHub Release will be created in this workflow"
|
||||||
|
;;
|
||||||
|
"prerelease")
|
||||||
|
echo "🧪 Prerelease build artifacts have been uploaded to OSS release directory"
|
||||||
|
echo "⚠️ This is a prerelease build - use with caution"
|
||||||
|
echo "🏷️ GitHub Release will be created in this workflow"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🐳 Docker Images:"
|
||||||
|
if [[ "${{ github.event.inputs.build_docker }}" == "false" ]]; then
|
||||||
|
echo "⏭️ Docker image build was skipped (binary only build)"
|
||||||
|
elif [[ "$BUILD_STATUS" == "success" ]]; then
|
||||||
|
echo "🔄 Docker images will be built and pushed automatically via workflow_run event"
|
||||||
|
else
|
||||||
|
echo "❌ Docker image build will be skipped due to build failure"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create GitHub Release (only for tag pushes)
|
||||||
|
create-release:
|
||||||
|
name: Create GitHub Release
|
||||||
|
needs: [build-check, build-rustfs]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && needs.build-check.outputs.build_type != 'development'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
outputs:
|
||||||
|
release_id: ${{ steps.create.outputs.release_id }}
|
||||||
|
release_url: ${{ steps.create.outputs.release_url }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
id: create
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
TAG="${{ needs.build-check.outputs.version }}"
|
||||||
|
VERSION="${{ needs.build-check.outputs.version }}"
|
||||||
|
IS_PRERELEASE="${{ needs.build-check.outputs.is_prerelease }}"
|
||||||
|
BUILD_TYPE="${{ needs.build-check.outputs.build_type }}"
|
||||||
|
|
||||||
|
# Determine release type for title
|
||||||
|
if [[ "$BUILD_TYPE" == "prerelease" ]]; then
|
||||||
|
if [[ "$TAG" == *"alpha"* ]]; then
|
||||||
|
RELEASE_TYPE="alpha"
|
||||||
|
elif [[ "$TAG" == *"beta"* ]]; then
|
||||||
|
RELEASE_TYPE="beta"
|
||||||
|
elif [[ "$TAG" == *"rc"* ]]; then
|
||||||
|
RELEASE_TYPE="rc"
|
||||||
|
else
|
||||||
|
RELEASE_TYPE="prerelease"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
RELEASE_TYPE="release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if release already exists
|
||||||
|
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||||
|
echo "Release $TAG already exists"
|
||||||
|
RELEASE_ID=$(gh release view "$TAG" --json databaseId --jq '.databaseId')
|
||||||
|
RELEASE_URL=$(gh release view "$TAG" --json url --jq '.url')
|
||||||
|
else
|
||||||
|
# Get release notes from tag message
|
||||||
|
RELEASE_NOTES=$(git tag -l --format='%(contents)' "${TAG}")
|
||||||
|
if [[ -z "$RELEASE_NOTES" || "$RELEASE_NOTES" =~ ^[[:space:]]*$ ]]; then
|
||||||
|
if [[ "$IS_PRERELEASE" == "true" ]]; then
|
||||||
|
RELEASE_NOTES="Pre-release ${VERSION} (${RELEASE_TYPE})"
|
||||||
|
else
|
||||||
|
RELEASE_NOTES="Release ${VERSION}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create release title
|
||||||
|
if [[ "$IS_PRERELEASE" == "true" ]]; then
|
||||||
|
TITLE="RustFS $VERSION (${RELEASE_TYPE})"
|
||||||
|
else
|
||||||
|
TITLE="RustFS $VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create the release
|
||||||
|
PRERELEASE_FLAG=""
|
||||||
|
if [[ "$IS_PRERELEASE" == "true" ]]; then
|
||||||
|
PRERELEASE_FLAG="--prerelease"
|
||||||
|
fi
|
||||||
|
|
||||||
|
gh release create "$TAG" \
|
||||||
|
--title "$TITLE" \
|
||||||
|
--notes "$RELEASE_NOTES" \
|
||||||
|
$PRERELEASE_FLAG \
|
||||||
|
--draft
|
||||||
|
|
||||||
|
RELEASE_ID=$(gh release view "$TAG" --json databaseId --jq '.databaseId')
|
||||||
|
RELEASE_URL=$(gh release view "$TAG" --json url --jq '.url')
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "release_id=$RELEASE_ID" >> $GITHUB_OUTPUT
|
||||||
|
echo "release_url=$RELEASE_URL" >> $GITHUB_OUTPUT
|
||||||
|
echo "Created release: $RELEASE_URL"
|
||||||
|
|
||||||
|
# Prepare and upload release assets
|
||||||
|
upload-release-assets:
|
||||||
|
name: Upload Release Assets
|
||||||
|
needs: [build-check, build-rustfs, create-release]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && needs.build-check.outputs.build_type != 'development'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download all build artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ./artifacts
|
||||||
|
pattern: rustfs-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Prepare release assets
|
||||||
|
id: prepare
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.build-check.outputs.version }}"
|
||||||
|
TAG="${{ needs.build-check.outputs.version }}"
|
||||||
|
|
||||||
|
mkdir -p ./release-assets
|
||||||
|
|
||||||
|
# Copy and verify artifacts
|
||||||
|
ASSETS_COUNT=0
|
||||||
|
for file in ./artifacts/*.zip; do
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
cp "$file" ./release-assets/
|
||||||
|
ASSETS_COUNT=$((ASSETS_COUNT + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $ASSETS_COUNT -eq 0 ]]; then
|
||||||
|
echo "❌ No artifacts found!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ./release-assets
|
||||||
|
|
||||||
|
# Generate checksums
|
||||||
|
if ls *.zip >/dev/null 2>&1; then
|
||||||
|
sha256sum *.zip > SHA256SUMS
|
||||||
|
sha512sum *.zip > SHA512SUMS
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create signature placeholder files
|
||||||
|
for file in *.zip; do
|
||||||
|
echo "# Signature for $file" > "${file}.asc"
|
||||||
|
echo "# GPG signature will be added in future versions" >> "${file}.asc"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "📦 Prepared assets:"
|
||||||
|
ls -la
|
||||||
|
|
||||||
|
echo "🔢 Asset count: $ASSETS_COUNT"
|
||||||
|
|
||||||
|
- name: Upload to GitHub Release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
TAG="${{ needs.build-check.outputs.version }}"
|
||||||
|
|
||||||
|
cd ./release-assets
|
||||||
|
|
||||||
|
# Upload all files
|
||||||
|
for file in *; do
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
echo "📤 Uploading $file..."
|
||||||
|
gh release upload "$TAG" "$file" --clobber
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "✅ All assets uploaded successfully"
|
||||||
|
|
||||||
|
# Update latest.json for stable releases only
|
||||||
|
update-latest-version:
|
||||||
|
name: Update Latest Version
|
||||||
|
needs: [build-check, upload-release-assets]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Update latest.json
|
||||||
|
env:
|
||||||
|
OSS_ACCESS_KEY_ID: ${{ secrets.ALICLOUDOSS_KEY_ID }}
|
||||||
|
OSS_ACCESS_KEY_SECRET: ${{ secrets.ALICLOUDOSS_KEY_SECRET }}
|
||||||
|
OSS_REGION: cn-beijing
|
||||||
|
OSS_ENDPOINT: https://oss-cn-beijing.aliyuncs.com
|
||||||
|
run: |
|
||||||
|
if [[ -z "$OSS_ACCESS_KEY_ID" ]]; then
|
||||||
|
echo "⚠️ OSS credentials not available, skipping latest.json update"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="${{ needs.build-check.outputs.version }}"
|
||||||
|
TAG="${{ needs.build-check.outputs.version }}"
|
||||||
|
|
||||||
|
# Install ossutil
|
||||||
|
OSSUTIL_VERSION="2.1.1"
|
||||||
|
OSSUTIL_ZIP="ossutil-${OSSUTIL_VERSION}-linux-amd64.zip"
|
||||||
|
OSSUTIL_DIR="ossutil-${OSSUTIL_VERSION}-linux-amd64"
|
||||||
|
|
||||||
|
curl -o "$OSSUTIL_ZIP" "https://gosspublic.alicdn.com/ossutil/v2/${OSSUTIL_VERSION}/${OSSUTIL_ZIP}"
|
||||||
|
unzip "$OSSUTIL_ZIP"
|
||||||
|
mv "${OSSUTIL_DIR}/ossutil" /usr/local/bin/
|
||||||
|
rm -rf "$OSSUTIL_DIR" "$OSSUTIL_ZIP"
|
||||||
|
chmod +x /usr/local/bin/ossutil
|
||||||
|
|
||||||
|
# Create latest.json
|
||||||
|
cat > latest.json << EOF
|
||||||
|
{
|
||||||
|
"version": "${VERSION}",
|
||||||
|
"tag": "${TAG}",
|
||||||
|
"release_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||||
|
"release_type": "stable",
|
||||||
|
"download_url": "https://github.com/${{ github.repository }}/releases/tag/${TAG}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Upload to OSS
|
||||||
|
ossutil cp latest.json oss://rustfs-version/latest.json --force
|
||||||
|
|
||||||
|
echo "✅ Updated latest.json for stable release $VERSION"
|
||||||
|
|
||||||
|
# Publish release (remove draft status)
|
||||||
|
publish-release:
|
||||||
|
name: Publish Release
|
||||||
|
needs: [build-check, create-release, upload-release-assets]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && needs.build-check.outputs.build_type != 'development'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Update release notes and publish
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
TAG="${{ needs.build-check.outputs.version }}"
|
||||||
|
VERSION="${{ needs.build-check.outputs.version }}"
|
||||||
|
IS_PRERELEASE="${{ needs.build-check.outputs.is_prerelease }}"
|
||||||
|
BUILD_TYPE="${{ needs.build-check.outputs.build_type }}"
|
||||||
|
|
||||||
|
# Determine release type
|
||||||
|
if [[ "$BUILD_TYPE" == "prerelease" ]]; then
|
||||||
|
if [[ "$TAG" == *"alpha"* ]]; then
|
||||||
|
RELEASE_TYPE="alpha"
|
||||||
|
elif [[ "$TAG" == *"beta"* ]]; then
|
||||||
|
RELEASE_TYPE="beta"
|
||||||
|
elif [[ "$TAG" == *"rc"* ]]; then
|
||||||
|
RELEASE_TYPE="rc"
|
||||||
|
else
|
||||||
|
RELEASE_TYPE="prerelease"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
RELEASE_TYPE="release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get original release notes from tag
|
||||||
|
ORIGINAL_NOTES=$(git tag -l --format='%(contents)' "${TAG}")
|
||||||
|
if [[ -z "$ORIGINAL_NOTES" || "$ORIGINAL_NOTES" =~ ^[[:space:]]*$ ]]; then
|
||||||
|
if [[ "$IS_PRERELEASE" == "true" ]]; then
|
||||||
|
ORIGINAL_NOTES="Pre-release ${VERSION} (${RELEASE_TYPE})"
|
||||||
|
else
|
||||||
|
ORIGINAL_NOTES="Release ${VERSION}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Publish the release (remove draft status)
|
||||||
|
gh release edit "$TAG" --draft=false
|
||||||
|
|
||||||
|
echo "🎉 Released $TAG successfully!"
|
||||||
|
echo "📄 Release URL: ${{ needs.create-release.outputs.release_url }}"
|
||||||
156
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
name: Continuous Integration
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
- "**.txt"
|
||||||
|
- "docs/**"
|
||||||
|
- "deploy/**"
|
||||||
|
- "scripts/dev_*.sh"
|
||||||
|
- "scripts/probe.sh"
|
||||||
|
- "LICENSE*"
|
||||||
|
- ".gitignore"
|
||||||
|
- ".dockerignore"
|
||||||
|
- "README*"
|
||||||
|
- "**/*.png"
|
||||||
|
- "**/*.jpg"
|
||||||
|
- "**/*.svg"
|
||||||
|
- ".github/workflows/build.yml"
|
||||||
|
- ".github/workflows/docker.yml"
|
||||||
|
- ".github/workflows/audit.yml"
|
||||||
|
- ".github/workflows/performance.yml"
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
- "**.txt"
|
||||||
|
- "docs/**"
|
||||||
|
- "deploy/**"
|
||||||
|
- "scripts/dev_*.sh"
|
||||||
|
- "scripts/probe.sh"
|
||||||
|
- "LICENSE*"
|
||||||
|
- ".gitignore"
|
||||||
|
- ".dockerignore"
|
||||||
|
- "README*"
|
||||||
|
- "**/*.png"
|
||||||
|
- "**/*.jpg"
|
||||||
|
- "**/*.svg"
|
||||||
|
- ".github/workflows/build.yml"
|
||||||
|
- ".github/workflows/docker.yml"
|
||||||
|
- ".github/workflows/audit.yml"
|
||||||
|
- ".github/workflows/performance.yml"
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * 0" # Weekly on Sunday at midnight UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
skip-check:
|
||||||
|
name: Skip Duplicate Actions
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
contents: read
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||||
|
steps:
|
||||||
|
- name: Skip duplicate actions
|
||||||
|
id: skip_check
|
||||||
|
uses: fkirc/skip-duplicate-actions@v5
|
||||||
|
with:
|
||||||
|
concurrent_skipping: "same_content_newer"
|
||||||
|
cancel_others: true
|
||||||
|
paths_ignore: '["*.md", "docs/**", "deploy/**"]'
|
||||||
|
# Never skip release events and tag pushes
|
||||||
|
do_not_skip: '["workflow_dispatch", "schedule", "merge_group", "release", "push"]'
|
||||||
|
|
||||||
|
test-and-lint:
|
||||||
|
name: Test and Lint
|
||||||
|
needs: skip-check
|
||||||
|
if: needs.skip-check.outputs.should_skip != 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Rust environment
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
rust-version: stable
|
||||||
|
cache-shared-key: ci-test-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
cargo nextest run --all --exclude e2e_test
|
||||||
|
cargo test --all --doc
|
||||||
|
|
||||||
|
- name: Check code formatting
|
||||||
|
run: cargo fmt --all --check
|
||||||
|
|
||||||
|
- name: Run clippy lints
|
||||||
|
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
|
e2e-tests:
|
||||||
|
name: End-to-End Tests
|
||||||
|
needs: skip-check
|
||||||
|
if: needs.skip-check.outputs.should_skip != 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Rust environment
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
rust-version: stable
|
||||||
|
cache-shared-key: ci-e2e-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install s3s-e2e test tool
|
||||||
|
uses: taiki-e/cache-cargo-install-action@v2
|
||||||
|
with:
|
||||||
|
tool: s3s-e2e
|
||||||
|
git: https://github.com/Nugine/s3s.git
|
||||||
|
rev: b7714bfaa17ddfa9b23ea01774a1e7bbdbfc2ca3
|
||||||
|
|
||||||
|
- name: Build debug binary
|
||||||
|
run: |
|
||||||
|
touch rustfs/build.rs
|
||||||
|
cargo build -p rustfs --bins
|
||||||
|
|
||||||
|
- name: Run end-to-end tests
|
||||||
|
run: |
|
||||||
|
s3s-e2e --version
|
||||||
|
./scripts/e2e-run.sh ./target/debug/rustfs /tmp/rustfs
|
||||||
|
|
||||||
|
- name: Upload test logs
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: e2e-test-logs-${{ github.run_number }}
|
||||||
|
path: /tmp/rustfs.log
|
||||||
|
retention-days: 3
|
||||||
417
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# Docker Images Workflow
|
||||||
|
#
|
||||||
|
# This workflow builds Docker images using pre-built binaries from the build workflow.
|
||||||
|
#
|
||||||
|
# Trigger Types:
|
||||||
|
# 1. workflow_run: Automatically triggered when "Build and Release" workflow completes
|
||||||
|
# 2. workflow_dispatch: Manual trigger for standalone Docker builds
|
||||||
|
#
|
||||||
|
# Key Features:
|
||||||
|
# - Only triggers when Linux builds (x86_64 + aarch64) are successful
|
||||||
|
# - Independent of macOS/Windows build status
|
||||||
|
# - Uses workflow_run event for precise control
|
||||||
|
# - Only builds Docker images for releases and prereleases (development builds are skipped)
|
||||||
|
|
||||||
|
name: Docker Images
|
||||||
|
|
||||||
|
# Permissions needed for workflow_run event and Docker registry access
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Automatically triggered when build workflow completes
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Build and Release"]
|
||||||
|
types: [completed]
|
||||||
|
# Manual trigger with same parameters for consistency
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
push_images:
|
||||||
|
description: "Push images to registries"
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
version:
|
||||||
|
description: "Version to build (latest for stable release, or specific version like v1.0.0, v1.0.0-alpha1)"
|
||||||
|
required: false
|
||||||
|
default: "latest"
|
||||||
|
type: string
|
||||||
|
force_rebuild:
|
||||||
|
description: "Force rebuild even if binary exists (useful for testing)"
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOCKERHUB_USERNAME: rustfs
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
REGISTRY_DOCKERHUB: rustfs/rustfs
|
||||||
|
REGISTRY_GHCR: ghcr.io/${{ github.repository }}
|
||||||
|
DOCKER_PLATFORMS: linux/amd64,linux/arm64
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Check if we should build Docker images
|
||||||
|
build-check:
|
||||||
|
name: Docker Build Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_build: ${{ steps.check.outputs.should_build }}
|
||||||
|
should_push: ${{ steps.check.outputs.should_push }}
|
||||||
|
build_type: ${{ steps.check.outputs.build_type }}
|
||||||
|
version: ${{ steps.check.outputs.version }}
|
||||||
|
short_sha: ${{ steps.check.outputs.short_sha }}
|
||||||
|
is_prerelease: ${{ steps.check.outputs.is_prerelease }}
|
||||||
|
create_latest: ${{ steps.check.outputs.create_latest }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
# For workflow_run events, checkout the specific commit that triggered the workflow
|
||||||
|
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||||
|
|
||||||
|
- name: Check build conditions
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
should_build=false
|
||||||
|
should_push=false
|
||||||
|
build_type="none"
|
||||||
|
version=""
|
||||||
|
short_sha=""
|
||||||
|
is_prerelease=false
|
||||||
|
create_latest=false
|
||||||
|
|
||||||
|
if [[ "${{ github.event_name }}" == "workflow_run" ]]; then
|
||||||
|
# Triggered by build workflow completion
|
||||||
|
echo "🔗 Triggered by build workflow completion"
|
||||||
|
|
||||||
|
# Check if the triggering workflow was successful
|
||||||
|
# If the workflow succeeded, it means ALL builds (including Linux x86_64 and aarch64) succeeded
|
||||||
|
if [[ "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then
|
||||||
|
echo "✅ Build workflow succeeded, all builds including Linux are successful"
|
||||||
|
should_build=true
|
||||||
|
should_push=true
|
||||||
|
else
|
||||||
|
echo "❌ Build workflow failed (conclusion: ${{ github.event.workflow_run.conclusion }}), skipping Docker build"
|
||||||
|
should_build=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract version info from commit message or use commit SHA
|
||||||
|
# Use Git to generate consistent short SHA (ensures uniqueness like build.yml)
|
||||||
|
short_sha=$(git rev-parse --short "${{ github.event.workflow_run.head_sha }}")
|
||||||
|
|
||||||
|
# Determine build type based on triggering workflow event and ref
|
||||||
|
triggering_event="${{ github.event.workflow_run.event }}"
|
||||||
|
head_branch="${{ github.event.workflow_run.head_branch }}"
|
||||||
|
|
||||||
|
echo "🔍 Analyzing triggering workflow:"
|
||||||
|
echo " 📋 Event: $triggering_event"
|
||||||
|
echo " 🌿 Head branch: $head_branch"
|
||||||
|
echo " 📎 Head SHA: ${{ github.event.workflow_run.head_sha }}"
|
||||||
|
|
||||||
|
# Check if this was triggered by a tag push
|
||||||
|
if [[ "$triggering_event" == "push" ]]; then
|
||||||
|
# For tag pushes, head_branch will be like "refs/tags/v1.0.0" or just "v1.0.0"
|
||||||
|
if [[ "$head_branch" == refs/tags/* ]]; then
|
||||||
|
# Extract tag name from refs/tags/TAG_NAME
|
||||||
|
tag_name="${head_branch#refs/tags/}"
|
||||||
|
version="$tag_name"
|
||||||
|
elif [[ "$head_branch" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||||
|
# Direct tag name like "v1.0.0" or "1.0.0-alpha.1"
|
||||||
|
version="$head_branch"
|
||||||
|
elif [[ "$head_branch" == "main" ]]; then
|
||||||
|
# Regular branch push to main
|
||||||
|
build_type="development"
|
||||||
|
version="dev-${short_sha}"
|
||||||
|
should_build=false
|
||||||
|
echo "⏭️ Skipping Docker build for development version (main branch push)"
|
||||||
|
else
|
||||||
|
# Other branch push
|
||||||
|
build_type="development"
|
||||||
|
version="dev-${short_sha}"
|
||||||
|
should_build=false
|
||||||
|
echo "⏭️ Skipping Docker build for development version (branch: $head_branch)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If we extracted a version (tag), determine release type
|
||||||
|
if [[ -n "$version" ]] && [[ "$version" != "dev-${short_sha}" ]]; then
|
||||||
|
# Remove 'v' prefix if present for consistent version format
|
||||||
|
if [[ "$version" == v* ]]; then
|
||||||
|
version="${version#v}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]] || [[ "$version" == *"rc"* ]]; then
|
||||||
|
build_type="prerelease"
|
||||||
|
is_prerelease=true
|
||||||
|
echo "🧪 Building Docker image for prerelease: $version"
|
||||||
|
else
|
||||||
|
build_type="release"
|
||||||
|
create_latest=true
|
||||||
|
echo "🚀 Building Docker image for release: $version"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Non-push events
|
||||||
|
build_type="development"
|
||||||
|
version="dev-${short_sha}"
|
||||||
|
should_build=false
|
||||||
|
echo "⏭️ Skipping Docker build for development version (event: $triggering_event)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔄 Build triggered by workflow_run:"
|
||||||
|
echo " 📋 Conclusion: ${{ github.event.workflow_run.conclusion }}"
|
||||||
|
echo " 🌿 Branch: ${{ github.event.workflow_run.head_branch }}"
|
||||||
|
echo " 📎 SHA: ${{ github.event.workflow_run.head_sha }}"
|
||||||
|
echo " 🎯 Event: ${{ github.event.workflow_run.event }}"
|
||||||
|
|
||||||
|
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||||
|
# Manual trigger
|
||||||
|
input_version="${{ github.event.inputs.version }}"
|
||||||
|
version="${input_version}"
|
||||||
|
should_push="${{ github.event.inputs.push_images }}"
|
||||||
|
should_build=true
|
||||||
|
|
||||||
|
# Get short SHA
|
||||||
|
short_sha=$(git rev-parse --short HEAD)
|
||||||
|
|
||||||
|
echo "🎯 Manual Docker build triggered:"
|
||||||
|
echo " 📋 Requested version: $input_version"
|
||||||
|
echo " 🔧 Force rebuild: ${{ github.event.inputs.force_rebuild }}"
|
||||||
|
echo " 🚀 Push images: $should_push"
|
||||||
|
|
||||||
|
case "$input_version" in
|
||||||
|
"latest")
|
||||||
|
build_type="release"
|
||||||
|
create_latest=true
|
||||||
|
echo "🚀 Building with latest stable release version"
|
||||||
|
;;
|
||||||
|
# Prerelease versions (must match first, more specific)
|
||||||
|
v*alpha*|v*beta*|v*rc*|*alpha*|*beta*|*rc*)
|
||||||
|
build_type="prerelease"
|
||||||
|
is_prerelease=true
|
||||||
|
echo "🧪 Building with prerelease version: $input_version"
|
||||||
|
;;
|
||||||
|
# Release versions (match after prereleases, more general)
|
||||||
|
v[0-9]*|[0-9]*.*.*)
|
||||||
|
build_type="release"
|
||||||
|
create_latest=true
|
||||||
|
echo "📦 Building with specific release version: $input_version"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Invalid version for Docker build
|
||||||
|
should_build=false
|
||||||
|
echo "❌ Invalid version for Docker build: $input_version"
|
||||||
|
echo "⚠️ Only release versions (latest, v1.0.0, 1.0.0) and prereleases (v1.0.0-alpha1, 1.0.0-beta2) are supported"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "should_build=$should_build" >> $GITHUB_OUTPUT
|
||||||
|
echo "should_push=$should_push" >> $GITHUB_OUTPUT
|
||||||
|
echo "build_type=$build_type" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=$version" >> $GITHUB_OUTPUT
|
||||||
|
echo "short_sha=$short_sha" >> $GITHUB_OUTPUT
|
||||||
|
echo "is_prerelease=$is_prerelease" >> $GITHUB_OUTPUT
|
||||||
|
echo "create_latest=$create_latest" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
echo "🐳 Docker Build Summary:"
|
||||||
|
echo " - Should build: $should_build"
|
||||||
|
echo " - Should push: $should_push"
|
||||||
|
echo " - Build type: $build_type"
|
||||||
|
echo " - Version: $version"
|
||||||
|
echo " - Short SHA: $short_sha"
|
||||||
|
echo " - Is prerelease: $is_prerelease"
|
||||||
|
echo " - Create latest: $create_latest"
|
||||||
|
|
||||||
|
# Build multi-arch Docker images
|
||||||
|
# Strategy: Build images using pre-built binaries from dl.rustfs.com
|
||||||
|
# Supports both release and dev channel binaries based on build context
|
||||||
|
# Only runs when should_build is true (which includes workflow success check)
|
||||||
|
build-docker:
|
||||||
|
name: Build Docker Images
|
||||||
|
needs: build-check
|
||||||
|
if: needs.build-check.outputs.should_build == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ env.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
# - name: Login to GitHub Container Registry
|
||||||
|
# uses: docker/login-action@v3
|
||||||
|
# with:
|
||||||
|
# registry: ghcr.io
|
||||||
|
# username: ${{ github.actor }}
|
||||||
|
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Extract metadata and generate tags
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
BUILD_TYPE="${{ needs.build-check.outputs.build_type }}"
|
||||||
|
VERSION="${{ needs.build-check.outputs.version }}"
|
||||||
|
SHORT_SHA="${{ needs.build-check.outputs.short_sha }}"
|
||||||
|
CREATE_LATEST="${{ needs.build-check.outputs.create_latest }}"
|
||||||
|
|
||||||
|
# Convert version format for Dockerfile compatibility
|
||||||
|
case "$VERSION" in
|
||||||
|
"latest")
|
||||||
|
# For stable latest, use RELEASE=latest + release CHANNEL
|
||||||
|
DOCKER_RELEASE="latest"
|
||||||
|
DOCKER_CHANNEL="release"
|
||||||
|
;;
|
||||||
|
v*)
|
||||||
|
# For versioned releases (v1.0.0), remove 'v' prefix for Dockerfile
|
||||||
|
DOCKER_RELEASE="${VERSION#v}"
|
||||||
|
DOCKER_CHANNEL="release"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# For other versions, pass as-is
|
||||||
|
DOCKER_RELEASE="${VERSION}"
|
||||||
|
DOCKER_CHANNEL="release"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "docker_release=$DOCKER_RELEASE" >> $GITHUB_OUTPUT
|
||||||
|
echo "docker_channel=$DOCKER_CHANNEL" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
echo "🐳 Docker build parameters:"
|
||||||
|
echo " - Original version: $VERSION"
|
||||||
|
echo " - Docker RELEASE: $DOCKER_RELEASE"
|
||||||
|
echo " - Docker CHANNEL: $DOCKER_CHANNEL"
|
||||||
|
|
||||||
|
# Generate tags based on build type
|
||||||
|
# Only support release and prerelease builds (no development builds)
|
||||||
|
TAGS="${{ env.REGISTRY_DOCKERHUB }}:${VERSION}"
|
||||||
|
|
||||||
|
# Add channel tags for prereleases and latest for stable
|
||||||
|
if [[ "$CREATE_LATEST" == "true" ]]; then
|
||||||
|
# Stable release
|
||||||
|
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:latest"
|
||||||
|
elif [[ "$BUILD_TYPE" == "prerelease" ]]; then
|
||||||
|
# Prerelease channel tags (alpha, beta, rc)
|
||||||
|
if [[ "$VERSION" == *"alpha"* ]]; then
|
||||||
|
CHANNEL="alpha"
|
||||||
|
elif [[ "$VERSION" == *"beta"* ]]; then
|
||||||
|
CHANNEL="beta"
|
||||||
|
elif [[ "$VERSION" == *"rc"* ]]; then
|
||||||
|
CHANNEL="rc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$CHANNEL" ]]; then
|
||||||
|
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:${CHANNEL}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Output tags
|
||||||
|
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Generate labels
|
||||||
|
LABELS="org.opencontainers.image.title=RustFS"
|
||||||
|
LABELS="$LABELS,org.opencontainers.image.description=RustFS distributed object storage system"
|
||||||
|
LABELS="$LABELS,org.opencontainers.image.version=$VERSION"
|
||||||
|
LABELS="$LABELS,org.opencontainers.image.revision=${{ github.sha }}"
|
||||||
|
LABELS="$LABELS,org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}"
|
||||||
|
LABELS="$LABELS,org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||||
|
LABELS="$LABELS,org.opencontainers.image.build-type=$BUILD_TYPE"
|
||||||
|
|
||||||
|
echo "labels=$LABELS" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
echo "🐳 Generated Docker tags:"
|
||||||
|
echo "$TAGS" | tr ',' '\n' | sed 's/^/ - /'
|
||||||
|
echo "📋 Build type: $BUILD_TYPE"
|
||||||
|
echo "🔖 Version: $VERSION"
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
platforms: ${{ env.DOCKER_PLATFORMS }}
|
||||||
|
push: ${{ needs.build-check.outputs.should_push == 'true' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: |
|
||||||
|
type=gha,scope=docker-binary
|
||||||
|
cache-to: |
|
||||||
|
type=gha,mode=max,scope=docker-binary
|
||||||
|
build-args: |
|
||||||
|
BUILDTIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
VERSION=${{ needs.build-check.outputs.version }}
|
||||||
|
BUILD_TYPE=${{ needs.build-check.outputs.build_type }}
|
||||||
|
REVISION=${{ github.sha }}
|
||||||
|
RELEASE=${{ steps.meta.outputs.docker_release }}
|
||||||
|
CHANNEL=${{ steps.meta.outputs.docker_channel }}
|
||||||
|
BUILDKIT_INLINE_CACHE=1
|
||||||
|
# Enable advanced BuildKit features for better performance
|
||||||
|
provenance: false
|
||||||
|
sbom: false
|
||||||
|
# Add retry mechanism by splitting the build process
|
||||||
|
no-cache: false
|
||||||
|
pull: true
|
||||||
|
|
||||||
|
# Note: Manifest creation is no longer needed as we only build one variant
|
||||||
|
# Multi-arch manifests are automatically created by docker/build-push-action
|
||||||
|
|
||||||
|
# Docker build summary
|
||||||
|
docker-summary:
|
||||||
|
name: Docker Build Summary
|
||||||
|
needs: [build-check, build-docker]
|
||||||
|
if: always() && needs.build-check.outputs.should_build == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Docker build completion summary
|
||||||
|
run: |
|
||||||
|
BUILD_TYPE="${{ needs.build-check.outputs.build_type }}"
|
||||||
|
VERSION="${{ needs.build-check.outputs.version }}"
|
||||||
|
CREATE_LATEST="${{ needs.build-check.outputs.create_latest }}"
|
||||||
|
|
||||||
|
echo "🐳 Docker build completed successfully!"
|
||||||
|
echo "📦 Build type: $BUILD_TYPE"
|
||||||
|
echo "🔢 Version: $VERSION"
|
||||||
|
echo "🚀 Strategy: Images using pre-built binaries (release channel only)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
case "$BUILD_TYPE" in
|
||||||
|
"release")
|
||||||
|
echo "🚀 Release Docker image has been built with ${VERSION} tags"
|
||||||
|
echo "✅ This image is ready for production use"
|
||||||
|
if [[ "$CREATE_LATEST" == "true" ]]; then
|
||||||
|
echo "🏷️ Latest tag has been created for stable release"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
"prerelease")
|
||||||
|
echo "🧪 Prerelease Docker image has been built with ${VERSION} tags"
|
||||||
|
echo "⚠️ This is a prerelease image - use with caution"
|
||||||
|
echo "🚫 Latest tag NOT created for prerelease"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ Unexpected build type: $BUILD_TYPE"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
32
.github/workflows/issue-translator.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
name: "issue-translator"
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: usthe/issues-translate-action@v2.7
|
||||||
|
with:
|
||||||
|
IS_MODIFY_TITLE: false
|
||||||
|
# not require, default false, . Decide whether to modify the issue title
|
||||||
|
# if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot.
|
||||||
|
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically.
|
||||||
|
# not require. Customize the translation robot prefix message.
|
||||||
139
.github/workflows/performance.yml
vendored
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
name: Performance Testing
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- "**/*.rs"
|
||||||
|
- "**/Cargo.toml"
|
||||||
|
- "**/Cargo.lock"
|
||||||
|
- ".github/workflows/performance.yml"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
profile_duration:
|
||||||
|
description: "Profiling duration in seconds"
|
||||||
|
required: false
|
||||||
|
default: "120"
|
||||||
|
type: string
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
performance-profile:
|
||||||
|
name: Performance Profiling
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Rust environment
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
rust-version: nightly
|
||||||
|
cache-shared-key: perf-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install additional nightly components
|
||||||
|
run: rustup component add llvm-tools-preview
|
||||||
|
|
||||||
|
- name: Install samply profiler
|
||||||
|
uses: taiki-e/cache-cargo-install-action@v2
|
||||||
|
with:
|
||||||
|
tool: samply
|
||||||
|
|
||||||
|
- name: Configure kernel for profiling
|
||||||
|
run: echo '1' | sudo tee /proc/sys/kernel/perf_event_paranoid
|
||||||
|
|
||||||
|
- name: Prepare test environment
|
||||||
|
run: |
|
||||||
|
# Create test volumes
|
||||||
|
for i in {0..4}; do
|
||||||
|
mkdir -p ./target/volume/test$i
|
||||||
|
done
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
echo "RUSTFS_VOLUMES=./target/volume/test{0...4}" >> $GITHUB_ENV
|
||||||
|
echo "RUST_LOG=rustfs=info,ecstore=info,s3s=info,iam=info,rustfs-obs=info" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Verify console static assets
|
||||||
|
run: |
|
||||||
|
# Console static assets are already embedded in the repository
|
||||||
|
echo "Console static assets size: $(du -sh rustfs/static/)"
|
||||||
|
echo "Console static assets are embedded via rust-embed, no external download needed"
|
||||||
|
|
||||||
|
- name: Build with profiling optimizations
|
||||||
|
run: |
|
||||||
|
RUSTFLAGS="-C force-frame-pointers=yes -C debug-assertions=off" \
|
||||||
|
cargo +nightly build --profile profiling -p rustfs --bins
|
||||||
|
|
||||||
|
- name: Run performance profiling
|
||||||
|
id: profiling
|
||||||
|
run: |
|
||||||
|
DURATION="${{ github.event.inputs.profile_duration || '120' }}"
|
||||||
|
echo "Running profiling for ${DURATION} seconds..."
|
||||||
|
|
||||||
|
timeout "${DURATION}s" samply record \
|
||||||
|
--output samply-profile.json \
|
||||||
|
./target/profiling/rustfs ${RUSTFS_VOLUMES} || true
|
||||||
|
|
||||||
|
if [ -f "samply-profile.json" ]; then
|
||||||
|
echo "profile_generated=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Profile generated successfully"
|
||||||
|
else
|
||||||
|
echo "profile_generated=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "::warning::Profile data not generated"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload profile data
|
||||||
|
if: steps.profiling.outputs.profile_generated == 'true'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: performance-profile-${{ github.run_number }}
|
||||||
|
path: samply-profile.json
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
benchmark:
|
||||||
|
name: Benchmark Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 45
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Rust environment
|
||||||
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
rust-version: stable
|
||||||
|
cache-shared-key: bench-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
cache-save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
|
- name: Run benchmarks
|
||||||
|
run: |
|
||||||
|
cargo bench --package ecstore --bench comparison_benchmark -- --output-format json | \
|
||||||
|
tee benchmark-results.json
|
||||||
|
|
||||||
|
- name: Upload benchmark results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: benchmark-results-${{ github.run_number }}
|
||||||
|
path: benchmark-results.json
|
||||||
|
retention-days: 7
|
||||||
21
.gitignore
vendored
@@ -1 +1,22 @@
|
|||||||
/target
|
/target
|
||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
/test
|
||||||
|
/logs
|
||||||
|
/data
|
||||||
|
.devcontainer
|
||||||
|
rustfs/static/*
|
||||||
|
!rustfs/static/.gitkeep
|
||||||
|
vendor
|
||||||
|
cli/rustfs-gui/embedded-rustfs/rustfs
|
||||||
|
*.log
|
||||||
|
deploy/certs/*
|
||||||
|
*jsonl
|
||||||
|
.env
|
||||||
|
.rustfs.sys
|
||||||
|
.cargo
|
||||||
|
profile.json
|
||||||
|
.docker/openobserve-otel/data
|
||||||
|
*.zst
|
||||||
|
.secrets
|
||||||
|
|||||||
90
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
// 使用 IntelliSense 了解相关属性。
|
||||||
|
// 悬停以查看现有属性的描述。
|
||||||
|
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug executable 'rustfs'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"--bin=rustfs",
|
||||||
|
"--package=rustfs"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "rustfs",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"RUST_LOG": "rustfs=debug,ecstore=info,s3s=debug"
|
||||||
|
},
|
||||||
|
"args": [
|
||||||
|
"--access-key",
|
||||||
|
"AKEXAMPLERUSTFS",
|
||||||
|
"--secret-key",
|
||||||
|
"SKEXAMPLERUSTFS",
|
||||||
|
"--address",
|
||||||
|
"0.0.0.0:9010",
|
||||||
|
"--domain-name",
|
||||||
|
"127.0.0.1:9010",
|
||||||
|
"./target/volume/test{0...4}"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug unit tests in executable 'rustfs'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"test",
|
||||||
|
"--no-run",
|
||||||
|
"--bin=rustfs",
|
||||||
|
"--package=rustfs"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "rustfs",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug unit tests in library 'ecstore'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"test",
|
||||||
|
"--no-run",
|
||||||
|
"--lib",
|
||||||
|
"--package=ecstore"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "ecstore",
|
||||||
|
"kind": "lib"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug executable target/debug/rustfs",
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}/target/debug/rustfs",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
//"stopAtEntry": false,
|
||||||
|
//"preLaunchTask": "cargo build",
|
||||||
|
"sourceLanguages": [
|
||||||
|
"rust"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
39
CLA.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
RustFS Individual Contributor License Agreement
|
||||||
|
|
||||||
|
Thank you for your interest in contributing documentation and related software code to a project hosted or managed by RustFS. In order to clarify the intellectual property license granted with Contributions from any person or entity, RustFS must have a Contributor License Agreement (“CLA”) on file that has been signed by each Contributor, indicating agreement to the license terms below. This version of the Contributor License Agreement allows an individual to submit Contributions to the applicable project. If you are making a submission on behalf of a legal entity, then you should sign the separate Corporate Contributor License Agreement.
|
||||||
|
|
||||||
|
You accept and agree to the following terms and conditions for Your present and future Contributions submitted to RustFS. You hereby irrevocably assign and transfer to RustFS all right, title, and interest in and to Your Contributions, including all copyrights and other intellectual property rights therein.
|
||||||
|
|
||||||
|
Definitions
|
||||||
|
|
||||||
|
“You” (or “Your”) shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with RustFS. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, “control” means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
“Contribution” shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to RustFS for inclusion in, or documentation of, any of the products or projects owned or managed by RustFS (the “Work”), including without limitation any Work described in Schedule A. For the purposes of this definition, “submitted” means any form of electronic or written communication sent to RustFS or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, RustFS for the purpose of discussing and improving the Work.
|
||||||
|
|
||||||
|
Assignment of Copyright
|
||||||
|
|
||||||
|
Subject to the terms and conditions of this Agreement, You hereby irrevocably assign and transfer to RustFS all right, title, and interest in and to Your Contributions, including all copyrights and other intellectual property rights therein, for the entire term of such rights, including all renewals and extensions. You agree to execute all documents and take all actions as may be reasonably necessary to vest in RustFS the ownership of Your Contributions and to assist RustFS in perfecting, maintaining, and enforcing its rights in Your Contributions.
|
||||||
|
|
||||||
|
Grant of Patent License
|
||||||
|
|
||||||
|
Subject to the terms and conditions of this Agreement, You hereby grant to RustFS and to recipients of documentation and software distributed by RustFS a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
|
||||||
|
|
||||||
|
You represent that you are legally entitled to grant the above assignment and license.
|
||||||
|
|
||||||
|
You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.
|
||||||
|
|
||||||
|
You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
|
||||||
|
Should You wish to submit work that is not Your original creation, You may submit it to RustFS separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as “Submitted on behalf of a third-party: [named here]”.
|
||||||
|
|
||||||
|
You agree to notify RustFS of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
|
||||||
|
|
||||||
|
Modification of CLA
|
||||||
|
|
||||||
|
RustFS reserves the right to update or modify this CLA in the future. Any updates or modifications to this CLA shall apply only to Contributions made after the effective date of the revised CLA. Contributions made prior to the update shall remain governed by the version of the CLA that was in effect at the time of submission. It is not necessary for all Contributors to re-sign the CLA when the CLA is updated or modified.
|
||||||
|
|
||||||
|
Governing Law and Dispute Resolution
|
||||||
|
|
||||||
|
This Agreement will be governed by and construed in accordance with the laws of the People’s Republic of China excluding that body of laws known as conflict of laws. The parties expressly agree that the United Nations Convention on Contracts for the International Sale of Goods will not apply. Any legal action or proceeding arising under this Agreement will be brought exclusively in the courts located in Beijing, China, and the parties hereby irrevocably consent to the personal jurisdiction and venue therein.
|
||||||
|
|
||||||
|
For your reading convenience, this Agreement is written in parallel English and Chinese sections. To the extent there is a conflict between the English and Chinese sections, the English sections shall govern.
|
||||||
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
hello@rustfs.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
189
CONTRIBUTING.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# RustFS Development Guide
|
||||||
|
|
||||||
|
## 📋 Code Quality Requirements
|
||||||
|
|
||||||
|
### 🔧 Code Formatting Rules
|
||||||
|
|
||||||
|
**MANDATORY**: All code must be properly formatted before committing. This project enforces strict formatting standards to maintain code consistency and readability.
|
||||||
|
|
||||||
|
#### Pre-commit Requirements
|
||||||
|
|
||||||
|
Before every commit, you **MUST**:
|
||||||
|
|
||||||
|
1. **Format your code**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify formatting**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all --check
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Pass clippy checks**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Ensure compilation**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo check --all-targets
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Quick Commands
|
||||||
|
|
||||||
|
We provide convenient Makefile targets for common tasks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format all code
|
||||||
|
make fmt
|
||||||
|
|
||||||
|
# Check if code is properly formatted
|
||||||
|
make fmt-check
|
||||||
|
|
||||||
|
# Run clippy checks
|
||||||
|
make clippy
|
||||||
|
|
||||||
|
# Run compilation check
|
||||||
|
make check
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Run all pre-commit checks (format + clippy + check + test)
|
||||||
|
make pre-commit
|
||||||
|
|
||||||
|
# Setup git hooks (one-time setup)
|
||||||
|
make setup-hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔒 Automated Pre-commit Hooks
|
||||||
|
|
||||||
|
This project includes a pre-commit hook that automatically runs before each commit to ensure:
|
||||||
|
|
||||||
|
- ✅ Code is properly formatted (`cargo fmt --all --check`)
|
||||||
|
- ✅ No clippy warnings (`cargo clippy --all-targets --all-features -- -D warnings`)
|
||||||
|
- ✅ Code compiles successfully (`cargo check --all-targets`)
|
||||||
|
|
||||||
|
#### Setting Up Pre-commit Hooks
|
||||||
|
|
||||||
|
Run this command once after cloning the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make setup-hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📝 Formatting Configuration
|
||||||
|
|
||||||
|
The project uses the following rustfmt configuration (defined in `rustfmt.toml`):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
max_width = 130
|
||||||
|
fn_call_width = 90
|
||||||
|
single_line_let_else_max_width = 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🚫 Commit Prevention
|
||||||
|
|
||||||
|
If your code doesn't meet the formatting requirements, the pre-commit hook will:
|
||||||
|
|
||||||
|
1. **Block the commit** and show clear error messages
|
||||||
|
2. **Provide exact commands** to fix the issues
|
||||||
|
3. **Guide you through** the resolution process
|
||||||
|
|
||||||
|
Example output when formatting fails:
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Code formatting check failed!
|
||||||
|
💡 Please run 'cargo fmt --all' to format your code before committing.
|
||||||
|
|
||||||
|
🔧 Quick fix:
|
||||||
|
cargo fmt --all
|
||||||
|
git add .
|
||||||
|
git commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔄 Development Workflow
|
||||||
|
|
||||||
|
1. **Make your changes**
|
||||||
|
2. **Format your code**: `make fmt` or `cargo fmt --all`
|
||||||
|
3. **Run pre-commit checks**: `make pre-commit`
|
||||||
|
4. **Commit your changes**: `git commit -m "your message"`
|
||||||
|
5. **Push to your branch**: `git push`
|
||||||
|
|
||||||
|
### 🛠️ IDE Integration
|
||||||
|
|
||||||
|
#### VS Code
|
||||||
|
|
||||||
|
Install the `rust-analyzer` extension and add to your `settings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rust-analyzer.rustfmt.extraArgs": ["--config-path", "./rustfmt.toml"],
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"[rust]": {
|
||||||
|
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Other IDEs
|
||||||
|
|
||||||
|
Configure your IDE to:
|
||||||
|
|
||||||
|
- Use the project's `rustfmt.toml` configuration
|
||||||
|
- Format on save
|
||||||
|
- Run clippy checks
|
||||||
|
|
||||||
|
### ❗ Important Notes
|
||||||
|
|
||||||
|
- **Never bypass formatting checks** - they are there for a reason
|
||||||
|
- **All CI/CD pipelines** will also enforce these same checks
|
||||||
|
- **Pull requests** will be automatically rejected if formatting checks fail
|
||||||
|
- **Consistent formatting** improves code readability and reduces merge conflicts
|
||||||
|
|
||||||
|
### 🆘 Troubleshooting
|
||||||
|
|
||||||
|
#### Pre-commit hook not running?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if hook is executable
|
||||||
|
ls -la .git/hooks/pre-commit
|
||||||
|
|
||||||
|
# Make it executable if needed
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Formatting issues?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format all code
|
||||||
|
cargo fmt --all
|
||||||
|
|
||||||
|
# Check specific issues
|
||||||
|
cargo fmt --all --check --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Clippy issues?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# See detailed clippy output
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
|
# Fix automatically fixable issues
|
||||||
|
cargo clippy --fix --all-targets --all-features
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Following these guidelines ensures high code quality and smooth collaboration across the RustFS project! 🚀
|
||||||
11685
Cargo.lock
generated
298
Cargo.toml
@@ -1,21 +1,291 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"rustfs", # Core file system implementation
|
||||||
|
"cli/rustfs-gui", # Graphical user interface client
|
||||||
|
"crates/appauth", # Application authentication and authorization
|
||||||
|
"crates/common", # Shared utilities and data structures
|
||||||
|
"crates/config", # Configuration management
|
||||||
|
"crates/crypto", # Cryptography and security features
|
||||||
|
"crates/ecstore", # Erasure coding storage implementation
|
||||||
|
"crates/e2e_test", # End-to-end test suite
|
||||||
|
"crates/filemeta", # File metadata management
|
||||||
|
"crates/iam", # Identity and Access Management
|
||||||
|
"crates/lock", # Distributed locking implementation
|
||||||
|
"crates/madmin", # Management dashboard and admin API interface
|
||||||
|
"crates/notify", # Notification system for events
|
||||||
|
"crates/obs", # Observability utilities
|
||||||
|
"crates/protos", # Protocol buffer definitions
|
||||||
|
"crates/rio", # Rust I/O utilities and abstractions
|
||||||
|
"crates/s3select-api", # S3 Select API interface
|
||||||
|
"crates/s3select-query", # S3 Select query engine
|
||||||
|
"crates/signer", # client signer
|
||||||
|
"crates/utils", # Utility functions and helpers
|
||||||
|
"crates/workers", # Worker thread pools and task scheduling
|
||||||
|
"crates/zip", # ZIP file handling and compression
|
||||||
|
"crates/ahm",
|
||||||
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["rustfs", "store"]
|
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "Apache-2.0"
|
||||||
repository = "https://github.com/rustfs/rustfs"
|
repository = "https://github.com/rustfs/rustfs"
|
||||||
rust-version = "1.75"
|
rust-version = "1.85"
|
||||||
|
version = "0.0.5"
|
||||||
|
homepage = "https://rustfs.com"
|
||||||
|
description = "RustFS is a high-performance distributed object storage software built using Rust, one of the most popular languages worldwide. "
|
||||||
|
keywords = ["RustFS", "Minio", "object-storage", "filesystem", "s3"]
|
||||||
|
categories = ["web-programming", "development-tools", "filesystem", "network-programming"]
|
||||||
|
|
||||||
|
[workspace.lints.rust]
|
||||||
|
unsafe_code = "deny"
|
||||||
|
|
||||||
|
[workspace.lints.clippy]
|
||||||
|
all = "warn"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
rustfs-utils = { path = "crates/utils" }
|
||||||
|
rustfs-filemeta = { path = "crates/filemeta" }
|
||||||
|
rustfs-rio = { path = "crates/rio" }
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
serde = { version = "1.0.203", features = ["derive"] }
|
rustfs-ahm = { path = "crates/ahm", version = "0.0.5" }
|
||||||
serde_json = "1.0.117"
|
rustfs-s3select-api = { path = "crates/s3select-api", version = "0.0.5" }
|
||||||
tracing = "0.1.40"
|
rustfs-appauth = { path = "crates/appauth", version = "0.0.5" }
|
||||||
futures = "0.3.30"
|
rustfs-common = { path = "crates/common", version = "0.0.5" }
|
||||||
bytes = "1.6.0"
|
rustfs-crypto = { path = "crates/crypto", version = "0.0.5" }
|
||||||
http = "1.1.0"
|
rustfs-ecstore = { path = "crates/ecstore", version = "0.0.5" }
|
||||||
thiserror = "1.0.61"
|
rustfs-iam = { path = "crates/iam", version = "0.0.5" }
|
||||||
time = "0.3.36"
|
rustfs-lock = { path = "crates/lock", version = "0.0.5" }
|
||||||
async-trait = "0.1.80"
|
rustfs-madmin = { path = "crates/madmin", version = "0.0.5" }
|
||||||
tokio = { version = "1.38.0", features = ["macros", "rt", "rt-multi-thread"] }
|
rustfs-policy = { path = "crates/policy", version = "0.0.5" }
|
||||||
|
rustfs-protos = { path = "crates/protos", version = "0.0.5" }
|
||||||
|
rustfs-s3select-query = { path = "crates/s3select-query", version = "0.0.5" }
|
||||||
|
rustfs = { path = "./rustfs", version = "0.0.5" }
|
||||||
|
rustfs-zip = { path = "./crates/zip", version = "0.0.5" }
|
||||||
|
rustfs-config = { path = "./crates/config", version = "0.0.5" }
|
||||||
|
rustfs-obs = { path = "crates/obs", version = "0.0.5" }
|
||||||
|
rustfs-notify = { path = "crates/notify", version = "0.0.5" }
|
||||||
|
rustfs-utils = { path = "crates/utils", version = "0.0.5" }
|
||||||
|
rustfs-rio = { path = "crates/rio", version = "0.0.5" }
|
||||||
|
rustfs-filemeta = { path = "crates/filemeta", version = "0.0.5" }
|
||||||
|
rustfs-signer = { path = "crates/signer", version = "0.0.5" }
|
||||||
|
rustfs-workers = { path = "crates/workers", version = "0.0.5" }
|
||||||
|
aes-gcm = { version = "0.10.3", features = ["std"] }
|
||||||
|
arc-swap = "1.7.1"
|
||||||
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
|
atoi = "2.0.0"
|
||||||
|
async-channel = "2.5.0"
|
||||||
|
async-recursion = "1.1.1"
|
||||||
|
async-trait = "0.1.88"
|
||||||
|
async-compression = { version = "0.4.0" }
|
||||||
|
atomic_enum = "0.3.0"
|
||||||
|
aws-sdk-s3 = "1.96.0"
|
||||||
|
axum = "0.8.4"
|
||||||
|
axum-extra = "0.10.1"
|
||||||
|
axum-server = { version = "0.7.2", features = ["tls-rustls"] }
|
||||||
|
base64-simd = "0.8.0"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
brotli = "8.0.1"
|
||||||
|
bytes = { version = "1.10.1", features = ["serde"] }
|
||||||
|
bytesize = "2.0.1"
|
||||||
|
byteorder = "1.5.0"
|
||||||
|
cfg-if = "1.0.1"
|
||||||
|
chacha20poly1305 = { version = "0.10.1" }
|
||||||
|
chrono = { version = "0.4.41", features = ["serde"] }
|
||||||
|
clap = { version = "4.5.41", features = ["derive", "env"] }
|
||||||
|
const-str = { version = "0.6.3", features = ["std", "proc"] }
|
||||||
|
crc32fast = "1.5.0"
|
||||||
|
criterion = { version = "0.5", features = ["html_reports"] }
|
||||||
|
dashmap = "6.1.0"
|
||||||
|
datafusion = "46.0.1"
|
||||||
|
derive_builder = "0.20.2"
|
||||||
|
dioxus = { version = "0.6.3", features = ["router"] }
|
||||||
|
dirs = "6.0.0"
|
||||||
|
enumset = "1.1.7"
|
||||||
|
flatbuffers = "25.2.10"
|
||||||
|
flate2 = "1.1.2"
|
||||||
|
flexi_logger = { version = "0.31.2", features = ["trc", "dont_minimize_extra_stacks"] }
|
||||||
|
form_urlencoded = "1.2.1"
|
||||||
|
futures = "0.3.31"
|
||||||
|
futures-core = "0.3.31"
|
||||||
|
futures-util = "0.3.31"
|
||||||
|
glob = "0.3.2"
|
||||||
|
hex = "0.4.3"
|
||||||
|
hex-simd = "0.8.0"
|
||||||
|
highway = { version = "1.3.0" }
|
||||||
|
hmac = "0.12.1"
|
||||||
|
hyper = "1.6.0"
|
||||||
|
hyper-util = { version = "0.1.15", features = [
|
||||||
|
"tokio",
|
||||||
|
"server-auto",
|
||||||
|
"server-graceful",
|
||||||
|
] }
|
||||||
|
hyper-rustls = "0.27.7"
|
||||||
|
http = "1.3.1"
|
||||||
|
http-body = "1.0.1"
|
||||||
|
humantime = "2.2.0"
|
||||||
|
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||||
|
jsonwebtoken = "9.3.1"
|
||||||
|
keyring = { version = "3.6.2", features = [
|
||||||
|
"apple-native",
|
||||||
|
"windows-native",
|
||||||
|
"sync-secret-service",
|
||||||
|
] }
|
||||||
|
lazy_static = "1.5.0"
|
||||||
|
libsystemd = { version = "0.7.2" }
|
||||||
|
local-ip-address = "0.6.5"
|
||||||
|
lz4 = "1.28.1"
|
||||||
|
matchit = "0.8.4"
|
||||||
|
md-5 = "0.10.6"
|
||||||
|
mime_guess = "2.0.5"
|
||||||
|
netif = "0.1.6"
|
||||||
|
nix = { version = "0.30.1", features = ["fs"] }
|
||||||
|
nu-ansi-term = "0.50.1"
|
||||||
|
num_cpus = { version = "1.17.0" }
|
||||||
|
nvml-wrapper = "0.11.0"
|
||||||
|
object_store = "0.11.2"
|
||||||
|
once_cell = "1.21.3"
|
||||||
|
opentelemetry = { version = "0.30.0" }
|
||||||
|
opentelemetry-appender-tracing = { version = "0.30.1", features = [
|
||||||
|
"experimental_use_tracing_span_context",
|
||||||
|
"experimental_metadata_attributes",
|
||||||
|
"spec_unstable_logs_enabled"
|
||||||
|
] }
|
||||||
|
opentelemetry_sdk = { version = "0.30.0" }
|
||||||
|
opentelemetry-stdout = { version = "0.30.0" }
|
||||||
|
opentelemetry-otlp = { version = "0.30.0", default-features = false, features = [
|
||||||
|
"grpc-tonic", "gzip-tonic", "trace", "metrics", "logs", "internal-logs"
|
||||||
|
] }
|
||||||
|
opentelemetry-semantic-conventions = { version = "0.30.0", features = [
|
||||||
|
"semconv_experimental",
|
||||||
|
] }
|
||||||
|
parking_lot = "0.12.4"
|
||||||
|
path-absolutize = "3.1.1"
|
||||||
|
path-clean = "1.0.1"
|
||||||
|
blake3 = { version = "1.8.2" }
|
||||||
|
pbkdf2 = "0.12.2"
|
||||||
|
percent-encoding = "2.3.1"
|
||||||
|
pin-project-lite = "0.2.16"
|
||||||
|
prost = "0.13.5"
|
||||||
|
quick-xml = "0.38.0"
|
||||||
|
rand = "0.9.1"
|
||||||
|
rdkafka = { version = "0.38.0", features = ["tokio"] }
|
||||||
|
reed-solomon-simd = { version = "3.0.1" }
|
||||||
|
regex = { version = "1.11.1" }
|
||||||
|
reqwest = { version = "0.12.22", default-features = false, features = [
|
||||||
|
"rustls-tls",
|
||||||
|
"charset",
|
||||||
|
"http2",
|
||||||
|
"system-proxy",
|
||||||
|
"stream",
|
||||||
|
"json",
|
||||||
|
"blocking",
|
||||||
|
] }
|
||||||
|
rfd = { version = "0.15.4", default-features = false, features = [
|
||||||
|
"xdg-portal",
|
||||||
|
"tokio",
|
||||||
|
] }
|
||||||
|
rmp = "0.8.14"
|
||||||
|
rmp-serde = "1.3.0"
|
||||||
|
rsa = "0.9.8"
|
||||||
|
rumqttc = { version = "0.24" }
|
||||||
|
rust-embed = { version = "8.7.2" }
|
||||||
|
rust-i18n = { version = "3.1.5" }
|
||||||
|
rustfs-rsc = "2025.506.1"
|
||||||
|
rustls = { version = "0.23.29" }
|
||||||
|
rustls-pki-types = "1.12.0"
|
||||||
|
rustls-pemfile = "2.2.0"
|
||||||
|
s3s = { version = "0.12.0-minio-preview.2" }
|
||||||
|
shadow-rs = { version = "1.2.0", default-features = false }
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = { version = "1.0.140", features = ["raw_value"] }
|
||||||
|
serde-xml-rs = "0.8.1"
|
||||||
|
serde_urlencoded = "0.7.1"
|
||||||
|
sha1 = "0.10.6"
|
||||||
|
sha2 = "0.10.9"
|
||||||
|
siphasher = "1.0.1"
|
||||||
|
smallvec = { version = "1.15.1", features = ["serde"] }
|
||||||
|
snafu = "0.8.6"
|
||||||
|
snap = "1.1.1"
|
||||||
|
socket2 = "0.6.0"
|
||||||
|
strum = { version = "0.27.1", features = ["derive"] }
|
||||||
|
sysinfo = "0.36.0"
|
||||||
|
sysctl = "0.6.0"
|
||||||
|
tempfile = "3.20.0"
|
||||||
|
temp-env = "0.3.6"
|
||||||
|
test-case = "3.3.1"
|
||||||
|
thiserror = "2.0.12"
|
||||||
|
time = { version = "0.3.41", features = [
|
||||||
|
"std",
|
||||||
|
"parsing",
|
||||||
|
"formatting",
|
||||||
|
"macros",
|
||||||
|
"serde",
|
||||||
|
] }
|
||||||
|
tokio = { version = "1.46.1", features = ["fs", "rt-multi-thread"] }
|
||||||
|
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||||
|
tokio-stream = { version = "0.1.17" }
|
||||||
|
tokio-tar = "0.3.1"
|
||||||
|
tokio-test = "0.4.4"
|
||||||
|
tokio-util = { version = "0.7.15", features = ["io", "compat"] }
|
||||||
|
tonic = { version = "0.13.1", features = ["gzip"] }
|
||||||
|
tonic-build = { version = "0.13.1" }
|
||||||
|
tower = { version = "0.5.2", features = ["timeout"] }
|
||||||
|
tower-http = { version = "0.6.6", features = ["cors"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-core = "0.1.34"
|
||||||
|
tracing-error = "0.2.1"
|
||||||
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "time"] }
|
||||||
|
tracing-appender = "0.2.3"
|
||||||
|
tracing-opentelemetry = "0.31.0"
|
||||||
|
transform-stream = "0.3.1"
|
||||||
|
url = "2.5.4"
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
uuid = { version = "1.17.0", features = [
|
||||||
|
"v4",
|
||||||
|
"fast-rng",
|
||||||
|
"macro-diagnostics",
|
||||||
|
] }
|
||||||
|
wildmatch = { version = "2.4.0", features = ["serde"] }
|
||||||
|
winapi = { version = "0.3.9" }
|
||||||
|
xxhash-rust = { version = "0.8.15", features = ["xxh64", "xxh3"] }
|
||||||
|
zip = "2.4.2"
|
||||||
|
zstd = "0.13.3"
|
||||||
|
anyhow = "1.0.98"
|
||||||
|
|
||||||
|
[profile.wasm-dev]
|
||||||
|
inherits = "dev"
|
||||||
|
opt-level = 1
|
||||||
|
|
||||||
|
[profile.server-dev]
|
||||||
|
inherits = "dev"
|
||||||
|
|
||||||
|
[profile.android-dev]
|
||||||
|
inherits = "dev"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
|
[profile.production]
|
||||||
|
inherits = "release"
|
||||||
|
lto = "fat"
|
||||||
|
codegen-units = 1
|
||||||
|
|
||||||
|
[profile.profiling]
|
||||||
|
inherits = "release"
|
||||||
|
debug = true
|
||||||
|
|||||||
95
Dockerfile
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# Multi-stage build for RustFS production image
|
||||||
|
|
||||||
|
# Build stage: Download and extract RustFS binary
|
||||||
|
FROM alpine:3.22 AS build
|
||||||
|
|
||||||
|
# Build arguments for platform and release
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG RELEASE=latest
|
||||||
|
|
||||||
|
# Install minimal dependencies for downloading and extracting
|
||||||
|
RUN apk add --no-cache ca-certificates curl unzip
|
||||||
|
|
||||||
|
# Create build directory
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Detect architecture and download corresponding binary
|
||||||
|
RUN case "${TARGETARCH}" in \
|
||||||
|
amd64) ARCH="x86_64" ;; \
|
||||||
|
arm64) ARCH="aarch64" ;; \
|
||||||
|
*) echo "Unsupported architecture: ${TARGETARCH}" >&2 && exit 1 ;; \
|
||||||
|
esac && \
|
||||||
|
if [ "${RELEASE}" = "latest" ]; then \
|
||||||
|
VERSION="latest"; \
|
||||||
|
else \
|
||||||
|
VERSION="v${RELEASE#v}"; \
|
||||||
|
fi && \
|
||||||
|
BASE_URL="https://dl.rustfs.com/artifacts/rustfs/release" && \
|
||||||
|
PACKAGE_NAME="rustfs-linux-${ARCH}-${VERSION}.zip" && \
|
||||||
|
DOWNLOAD_URL="${BASE_URL}/${PACKAGE_NAME}" && \
|
||||||
|
echo "Downloading ${PACKAGE_NAME} from ${DOWNLOAD_URL}" >&2 && \
|
||||||
|
curl -f -L "${DOWNLOAD_URL}" -o rustfs.zip && \
|
||||||
|
unzip rustfs.zip -d /build && \
|
||||||
|
chmod +x /build/rustfs && \
|
||||||
|
rm rustfs.zip || { echo "Failed to download or extract ${PACKAGE_NAME}" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Runtime stage: Configure runtime environment
|
||||||
|
FROM alpine:3.22.1
|
||||||
|
|
||||||
|
# Build arguments and labels
|
||||||
|
ARG RELEASE=latest
|
||||||
|
ARG BUILD_DATE
|
||||||
|
ARG VCS_REF
|
||||||
|
|
||||||
|
LABEL name="RustFS" \
|
||||||
|
vendor="RustFS Team" \
|
||||||
|
maintainer="RustFS Team <dev@rustfs.com>" \
|
||||||
|
version="${RELEASE}" \
|
||||||
|
release="${RELEASE}" \
|
||||||
|
build-date="${BUILD_DATE}" \
|
||||||
|
vcs-ref="${VCS_REF}" \
|
||||||
|
summary="High-performance distributed object storage system compatible with S3 API" \
|
||||||
|
description="RustFS is a distributed object storage system written in Rust, supporting erasure coding, multi-tenant management, and observability." \
|
||||||
|
url="https://rustfs.com" \
|
||||||
|
license="Apache-2.0"
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN echo "https://dl-cdn.alpinelinux.org/alpine/v3.20/community" >> /etc/apk/repositories && \
|
||||||
|
apk update && \
|
||||||
|
apk add --no-cache ca-certificates bash gosu coreutils shadow && \
|
||||||
|
addgroup -g 1000 rustfs && \
|
||||||
|
adduser -u 1000 -G rustfs -s /bin/bash -D rustfs
|
||||||
|
|
||||||
|
# Copy CA certificates and RustFS binary from build stage
|
||||||
|
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
COPY --from=build /build/rustfs /usr/bin/rustfs
|
||||||
|
|
||||||
|
# Copy entry point script
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
# Set permissions
|
||||||
|
RUN chmod +x /usr/bin/rustfs /entrypoint.sh && \
|
||||||
|
mkdir -p /data /logs && \
|
||||||
|
chown rustfs:rustfs /data /logs && \
|
||||||
|
chmod 700 /data /logs
|
||||||
|
|
||||||
|
# Environment variables (credentials should be set via environment or secrets)
|
||||||
|
ENV RUSTFS_ADDRESS=:9000 \
|
||||||
|
RUSTFS_ACCESS_KEY=rustfsadmin \
|
||||||
|
RUSTFS_SECRET_KEY=rustfsadmin \
|
||||||
|
RUSTFS_CONSOLE_ENABLE=true \
|
||||||
|
RUSTFS_VOLUMES=/data \
|
||||||
|
RUST_LOG=warn \
|
||||||
|
RUSTFS_OBS_LOG_DIRECTORY=/logs \
|
||||||
|
RUSTFS_SINKS_FILE_PATH=/logs
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
# Volumes for data and logs
|
||||||
|
VOLUME ["/data", "/logs"]
|
||||||
|
|
||||||
|
# Set entry point
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
CMD ["/usr/bin/rustfs"]
|
||||||
|
|
||||||
156
Dockerfile.source
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Multi-stage Dockerfile for RustFS - LOCAL DEVELOPMENT ONLY
|
||||||
|
#
|
||||||
|
# ⚠️ IMPORTANT: This Dockerfile is for local development and testing only.
|
||||||
|
# ⚠️ It builds RustFS from source code and is NOT used in CI/CD pipelines.
|
||||||
|
# ⚠️ CI/CD pipeline uses pre-built binaries from Dockerfile instead.
|
||||||
|
#
|
||||||
|
# Usage for local development:
|
||||||
|
# docker build -f Dockerfile.source -t rustfs:dev-local .
|
||||||
|
# docker run --rm -p 9000:9000 rustfs:dev-local
|
||||||
|
#
|
||||||
|
# Supports cross-compilation for amd64 and arm64 architectures
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
ARG BUILDPLATFORM
|
||||||
|
|
||||||
|
# Build stage
|
||||||
|
FROM --platform=$BUILDPLATFORM rust:1.88-bookworm AS builder
|
||||||
|
|
||||||
|
# Re-declare build arguments after FROM (required for multi-stage builds)
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
ARG BUILDPLATFORM
|
||||||
|
|
||||||
|
# Debug: Print platform information
|
||||||
|
RUN echo "🐳 Build Info: BUILDPLATFORM=$BUILDPLATFORM, TARGETPLATFORM=$TARGETPLATFORM"
|
||||||
|
|
||||||
|
# Install required build dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
wget \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
unzip \
|
||||||
|
gcc \
|
||||||
|
pkg-config \
|
||||||
|
libssl-dev \
|
||||||
|
lld \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Note: sccache removed for simpler builds
|
||||||
|
|
||||||
|
# Install cross-compilation tools for ARM64
|
||||||
|
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y gcc-aarch64-linux-gnu && \
|
||||||
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install protoc
|
||||||
|
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v31.1/protoc-31.1-linux-x86_64.zip \
|
||||||
|
&& unzip protoc-31.1-linux-x86_64.zip -d protoc3 \
|
||||||
|
&& mv protoc3/bin/* /usr/local/bin/ && chmod +x /usr/local/bin/protoc \
|
||||||
|
&& mv protoc3/include/* /usr/local/include/ && rm -rf protoc-31.1-linux-x86_64.zip protoc3
|
||||||
|
|
||||||
|
# Install flatc
|
||||||
|
RUN wget https://github.com/google/flatbuffers/releases/download/v25.2.10/Linux.flatc.binary.g++-13.zip \
|
||||||
|
&& unzip Linux.flatc.binary.g++-13.zip \
|
||||||
|
&& mv flatc /usr/local/bin/ && chmod +x /usr/local/bin/flatc && rm -rf Linux.flatc.binary.g++-13.zip
|
||||||
|
|
||||||
|
# Set up Rust targets based on platform
|
||||||
|
RUN set -e && \
|
||||||
|
PLATFORM="${TARGETPLATFORM:-linux/amd64}" && \
|
||||||
|
echo "🎯 Setting up Rust target for platform: $PLATFORM" && \
|
||||||
|
case "$PLATFORM" in \
|
||||||
|
"linux/amd64") rustup target add x86_64-unknown-linux-gnu ;; \
|
||||||
|
"linux/arm64") rustup target add aarch64-unknown-linux-gnu ;; \
|
||||||
|
*) echo "❌ Unsupported platform: $PLATFORM" && exit 1 ;; \
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Set up environment for cross-compilation
|
||||||
|
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
|
||||||
|
ENV CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
|
||||||
|
ENV CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
|
||||||
|
|
||||||
|
WORKDIR /usr/src/rustfs
|
||||||
|
|
||||||
|
# Copy all source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Configure cargo for optimized builds
|
||||||
|
ENV CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
||||||
|
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse \
|
||||||
|
CARGO_INCREMENTAL=0 \
|
||||||
|
CARGO_PROFILE_RELEASE_DEBUG=false \
|
||||||
|
CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO=off \
|
||||||
|
CARGO_PROFILE_RELEASE_STRIP=symbols
|
||||||
|
|
||||||
|
# Generate protobuf code
|
||||||
|
RUN cargo run --bin gproto
|
||||||
|
|
||||||
|
# Build the actual application with optimizations
|
||||||
|
RUN case "$TARGETPLATFORM" in \
|
||||||
|
"linux/amd64") \
|
||||||
|
echo "🔨 Building for amd64..." && \
|
||||||
|
rustup target add x86_64-unknown-linux-gnu && \
|
||||||
|
cargo build --release --target x86_64-unknown-linux-gnu --bin rustfs -j $(nproc) && \
|
||||||
|
cp target/x86_64-unknown-linux-gnu/release/rustfs /usr/local/bin/rustfs \
|
||||||
|
;; \
|
||||||
|
"linux/arm64") \
|
||||||
|
echo "🔨 Building for arm64..." && \
|
||||||
|
rustup target add aarch64-unknown-linux-gnu && \
|
||||||
|
cargo build --release --target aarch64-unknown-linux-gnu --bin rustfs -j $(nproc) && \
|
||||||
|
cp target/aarch64-unknown-linux-gnu/release/rustfs /usr/local/bin/rustfs \
|
||||||
|
;; \
|
||||||
|
*) \
|
||||||
|
echo "❌ Unsupported platform: $TARGETPLATFORM" && exit 1 \
|
||||||
|
;; \
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Runtime stage - Ubuntu minimal for better compatibility
|
||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
wget \
|
||||||
|
coreutils \
|
||||||
|
passwd \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create rustfs user and group
|
||||||
|
RUN groupadd -g 1000 rustfs && \
|
||||||
|
useradd -d /app -g rustfs -u 1000 -s /bin/bash rustfs
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Create data directories
|
||||||
|
RUN mkdir -p /data/rustfs{0,1,2,3} && \
|
||||||
|
chown -R rustfs:rustfs /data /app
|
||||||
|
|
||||||
|
# Copy binary from builder stage
|
||||||
|
COPY --from=builder /usr/local/bin/rustfs /app/rustfs
|
||||||
|
RUN chmod +x /app/rustfs && chown rustfs:rustfs /app/rustfs
|
||||||
|
|
||||||
|
# Copy entrypoint script
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER rustfs
|
||||||
|
|
||||||
|
# Expose ports
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
ENV RUSTFS_ACCESS_KEY=rustfsadmin \
|
||||||
|
RUSTFS_SECRET_KEY=rustfsadmin \
|
||||||
|
RUSTFS_ADDRESS=":9000" \
|
||||||
|
RUSTFS_CONSOLE_ENABLE=true \
|
||||||
|
RUSTFS_VOLUMES=/data \
|
||||||
|
RUST_LOG=warn
|
||||||
|
|
||||||
|
# Volume for data
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
# Set entrypoint and default command
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
CMD ["/app/rustfs"]
|
||||||
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2024 Beijing Henghesha Technology Co., Ltd.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
350
Makefile
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
###########
|
||||||
|
# 远程开发,需要 VSCode 安装 Dev Containers, Remote SSH, Remote Explorer
|
||||||
|
# https://code.visualstudio.com/docs/remote/containers
|
||||||
|
###########
|
||||||
|
DOCKER_CLI ?= docker
|
||||||
|
IMAGE_NAME ?= rustfs:v1.0.0
|
||||||
|
CONTAINER_NAME ?= rustfs-dev
|
||||||
|
# Docker build configurations
|
||||||
|
DOCKERFILE_PRODUCTION = Dockerfile
|
||||||
|
DOCKERFILE_SOURCE = Dockerfile.source
|
||||||
|
|
||||||
|
# Code quality and formatting targets
|
||||||
|
.PHONY: fmt
|
||||||
|
fmt:
|
||||||
|
@echo "🔧 Formatting code..."
|
||||||
|
cargo fmt --all
|
||||||
|
|
||||||
|
.PHONY: fmt-check
|
||||||
|
fmt-check:
|
||||||
|
@echo "📝 Checking code formatting..."
|
||||||
|
cargo fmt --all --check
|
||||||
|
|
||||||
|
.PHONY: clippy
|
||||||
|
clippy:
|
||||||
|
@echo "🔍 Running clippy checks..."
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
|
.PHONY: check
|
||||||
|
check:
|
||||||
|
@echo "🔨 Running compilation check..."
|
||||||
|
cargo check --all-targets
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
@echo "🧪 Running tests..."
|
||||||
|
cargo nextest run --all --exclude e2e_test
|
||||||
|
cargo test --all --doc
|
||||||
|
|
||||||
|
.PHONY: pre-commit
|
||||||
|
pre-commit: fmt clippy check test
|
||||||
|
@echo "✅ All pre-commit checks passed!"
|
||||||
|
|
||||||
|
.PHONY: setup-hooks
|
||||||
|
setup-hooks:
|
||||||
|
@echo "🔧 Setting up git hooks..."
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
@echo "✅ Git hooks setup complete!"
|
||||||
|
|
||||||
|
.PHONY: e2e-server
|
||||||
|
e2e-server:
|
||||||
|
sh $(shell pwd)/scripts/run.sh
|
||||||
|
|
||||||
|
.PHONY: probe-e2e
|
||||||
|
probe-e2e:
|
||||||
|
sh $(shell pwd)/scripts/probe.sh
|
||||||
|
|
||||||
|
# Native build using build-rustfs.sh script
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
@echo "🔨 Building RustFS using build-rustfs.sh script..."
|
||||||
|
./build-rustfs.sh
|
||||||
|
|
||||||
|
.PHONY: build-dev
|
||||||
|
build-dev:
|
||||||
|
@echo "🔨 Building RustFS in development mode..."
|
||||||
|
./build-rustfs.sh --dev
|
||||||
|
|
||||||
|
# Docker-based build (alternative approach)
|
||||||
|
# Usage: make BUILD_OS=ubuntu22.04 build-docker
|
||||||
|
# Output: target/ubuntu22.04/release/rustfs
|
||||||
|
BUILD_OS ?= rockylinux9.3
|
||||||
|
.PHONY: build-docker
|
||||||
|
build-docker: SOURCE_BUILD_IMAGE_NAME = rustfs-$(BUILD_OS):v1
|
||||||
|
build-docker: SOURCE_BUILD_CONTAINER_NAME = rustfs-$(BUILD_OS)-build
|
||||||
|
build-docker: BUILD_CMD = /root/.cargo/bin/cargo build --release --bin rustfs --target-dir /root/s3-rustfs/target/$(BUILD_OS)
|
||||||
|
build-docker:
|
||||||
|
@echo "🐳 Building RustFS using Docker ($(BUILD_OS))..."
|
||||||
|
$(DOCKER_CLI) build -t $(SOURCE_BUILD_IMAGE_NAME) -f $(DOCKERFILE_SOURCE) .
|
||||||
|
$(DOCKER_CLI) run --rm --name $(SOURCE_BUILD_CONTAINER_NAME) -v $(shell pwd):/root/s3-rustfs -it $(SOURCE_BUILD_IMAGE_NAME) $(BUILD_CMD)
|
||||||
|
|
||||||
|
.PHONY: build-musl
|
||||||
|
build-musl:
|
||||||
|
@echo "🔨 Building rustfs for x86_64-unknown-linux-musl..."
|
||||||
|
@echo "💡 On macOS/Windows, use 'make build-docker' or 'make docker-dev' instead"
|
||||||
|
./build-rustfs.sh --platform x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
.PHONY: build-gnu
|
||||||
|
build-gnu:
|
||||||
|
@echo "🔨 Building rustfs for x86_64-unknown-linux-gnu..."
|
||||||
|
@echo "💡 On macOS/Windows, use 'make build-docker' or 'make docker-dev' instead"
|
||||||
|
./build-rustfs.sh --platform x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
|
.PHONY: deploy-dev
|
||||||
|
deploy-dev: build-musl
|
||||||
|
@echo "🚀 Deploying to dev server: $${IP}"
|
||||||
|
./scripts/dev_deploy.sh $${IP}
|
||||||
|
|
||||||
|
# ========================================================================================
|
||||||
|
# Docker Multi-Architecture Builds (Primary Methods)
|
||||||
|
# ========================================================================================
|
||||||
|
|
||||||
|
# Production builds using docker-buildx.sh (for CI/CD and production)
|
||||||
|
.PHONY: docker-buildx
|
||||||
|
docker-buildx:
|
||||||
|
@echo "🏗️ Building multi-architecture production Docker images with buildx..."
|
||||||
|
./docker-buildx.sh
|
||||||
|
|
||||||
|
.PHONY: docker-buildx-push
|
||||||
|
docker-buildx-push:
|
||||||
|
@echo "🚀 Building and pushing multi-architecture production Docker images with buildx..."
|
||||||
|
./docker-buildx.sh --push
|
||||||
|
|
||||||
|
.PHONY: docker-buildx-version
|
||||||
|
docker-buildx-version:
|
||||||
|
@if [ -z "$(VERSION)" ]; then \
|
||||||
|
echo "❌ 错误: 请指定版本, 例如: make docker-buildx-version VERSION=v1.0.0"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "🏗️ Building multi-architecture production Docker images (version: $(VERSION))..."
|
||||||
|
./docker-buildx.sh --release $(VERSION)
|
||||||
|
|
||||||
|
.PHONY: docker-buildx-push-version
|
||||||
|
docker-buildx-push-version:
|
||||||
|
@if [ -z "$(VERSION)" ]; then \
|
||||||
|
echo "❌ 错误: 请指定版本, 例如: make docker-buildx-push-version VERSION=v1.0.0"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "🚀 Building and pushing multi-architecture production Docker images (version: $(VERSION))..."
|
||||||
|
./docker-buildx.sh --release $(VERSION) --push
|
||||||
|
|
||||||
|
# Development/Source builds using direct buildx commands
|
||||||
|
.PHONY: docker-dev
|
||||||
|
docker-dev:
|
||||||
|
@echo "🏗️ Building multi-architecture development Docker images with buildx..."
|
||||||
|
@echo "💡 This builds from source code and is intended for local development and testing"
|
||||||
|
@echo "⚠️ Multi-arch images cannot be loaded locally, use docker-dev-push to push to registry"
|
||||||
|
$(DOCKER_CLI) buildx build \
|
||||||
|
--platform linux/amd64,linux/arm64 \
|
||||||
|
--file $(DOCKERFILE_SOURCE) \
|
||||||
|
--tag rustfs:source-latest \
|
||||||
|
--tag rustfs:dev-latest \
|
||||||
|
.
|
||||||
|
|
||||||
|
.PHONY: docker-dev-local
|
||||||
|
docker-dev-local:
|
||||||
|
@echo "🏗️ Building single-architecture development Docker image for local use..."
|
||||||
|
@echo "💡 This builds from source code for the current platform and loads locally"
|
||||||
|
$(DOCKER_CLI) buildx build \
|
||||||
|
--file $(DOCKERFILE_SOURCE) \
|
||||||
|
--tag rustfs:source-latest \
|
||||||
|
--tag rustfs:dev-latest \
|
||||||
|
--load \
|
||||||
|
.
|
||||||
|
|
||||||
|
.PHONY: docker-dev-push
|
||||||
|
docker-dev-push:
|
||||||
|
@if [ -z "$(REGISTRY)" ]; then \
|
||||||
|
echo "❌ 错误: 请指定镜像仓库, 例如: make docker-dev-push REGISTRY=ghcr.io/username"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "🚀 Building and pushing multi-architecture development Docker images..."
|
||||||
|
@echo "💡 推送到仓库: $(REGISTRY)"
|
||||||
|
$(DOCKER_CLI) buildx build \
|
||||||
|
--platform linux/amd64,linux/arm64 \
|
||||||
|
--file $(DOCKERFILE_SOURCE) \
|
||||||
|
--tag $(REGISTRY)/rustfs:source-latest \
|
||||||
|
--tag $(REGISTRY)/rustfs:dev-latest \
|
||||||
|
--push \
|
||||||
|
.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Local production builds using direct buildx (alternative to docker-buildx.sh)
|
||||||
|
.PHONY: docker-buildx-production-local
|
||||||
|
docker-buildx-production-local:
|
||||||
|
@echo "🏗️ Building single-architecture production Docker image locally..."
|
||||||
|
@echo "💡 Alternative to docker-buildx.sh for local testing"
|
||||||
|
$(DOCKER_CLI) buildx build \
|
||||||
|
--file $(DOCKERFILE_PRODUCTION) \
|
||||||
|
--tag rustfs:production-latest \
|
||||||
|
--tag rustfs:latest \
|
||||||
|
--load \
|
||||||
|
--build-arg RELEASE=latest \
|
||||||
|
.
|
||||||
|
|
||||||
|
# ========================================================================================
|
||||||
|
# Single Architecture Docker Builds (Traditional)
|
||||||
|
# ========================================================================================
|
||||||
|
|
||||||
|
.PHONY: docker-build-production
|
||||||
|
docker-build-production:
|
||||||
|
@echo "🏗️ Building single-architecture production Docker image..."
|
||||||
|
@echo "💡 Consider using 'make docker-buildx-production-local' for multi-arch support"
|
||||||
|
$(DOCKER_CLI) build -f $(DOCKERFILE_PRODUCTION) -t rustfs:latest .
|
||||||
|
|
||||||
|
.PHONY: docker-build-source
|
||||||
|
docker-build-source:
|
||||||
|
@echo "🏗️ Building single-architecture source Docker image..."
|
||||||
|
@echo "💡 Consider using 'make docker-dev-local' for multi-arch support"
|
||||||
|
$(DOCKER_CLI) build -f $(DOCKERFILE_SOURCE) -t rustfs:source .
|
||||||
|
|
||||||
|
# ========================================================================================
|
||||||
|
# Development Environment
|
||||||
|
# ========================================================================================
|
||||||
|
|
||||||
|
.PHONY: dev-env-start
|
||||||
|
dev-env-start:
|
||||||
|
@echo "🚀 Starting development environment..."
|
||||||
|
$(DOCKER_CLI) buildx build \
|
||||||
|
--file $(DOCKERFILE_SOURCE) \
|
||||||
|
--tag rustfs:dev \
|
||||||
|
--load \
|
||||||
|
.
|
||||||
|
$(DOCKER_CLI) stop $(CONTAINER_NAME) 2>/dev/null || true
|
||||||
|
$(DOCKER_CLI) rm $(CONTAINER_NAME) 2>/dev/null || true
|
||||||
|
$(DOCKER_CLI) run -d --name $(CONTAINER_NAME) \
|
||||||
|
-p 9010:9010 -p 9000:9000 \
|
||||||
|
-v $(shell pwd):/workspace \
|
||||||
|
-it rustfs:dev
|
||||||
|
|
||||||
|
.PHONY: dev-env-stop
|
||||||
|
dev-env-stop:
|
||||||
|
@echo "🛑 Stopping development environment..."
|
||||||
|
$(DOCKER_CLI) stop $(CONTAINER_NAME) 2>/dev/null || true
|
||||||
|
$(DOCKER_CLI) rm $(CONTAINER_NAME) 2>/dev/null || true
|
||||||
|
|
||||||
|
.PHONY: dev-env-restart
|
||||||
|
dev-env-restart: dev-env-stop dev-env-start
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================================================================
|
||||||
|
# Build Utilities
|
||||||
|
# ========================================================================================
|
||||||
|
|
||||||
|
.PHONY: docker-inspect-multiarch
|
||||||
|
docker-inspect-multiarch:
|
||||||
|
@if [ -z "$(IMAGE)" ]; then \
|
||||||
|
echo "❌ 错误: 请指定镜像, 例如: make docker-inspect-multiarch IMAGE=rustfs/rustfs:latest"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "🔍 Inspecting multi-architecture image: $(IMAGE)"
|
||||||
|
docker buildx imagetools inspect $(IMAGE)
|
||||||
|
|
||||||
|
.PHONY: build-cross-all
|
||||||
|
build-cross-all:
|
||||||
|
@echo "🔧 Building all target architectures..."
|
||||||
|
@echo "💡 On macOS/Windows, use 'make docker-dev' for reliable multi-arch builds"
|
||||||
|
@echo "🔨 Generating protobuf code..."
|
||||||
|
cargo run --bin gproto || true
|
||||||
|
@echo "🔨 Building x86_64-unknown-linux-musl..."
|
||||||
|
./build-rustfs.sh --platform x86_64-unknown-linux-musl
|
||||||
|
@echo "🔨 Building aarch64-unknown-linux-gnu..."
|
||||||
|
./build-rustfs.sh --platform aarch64-unknown-linux-gnu
|
||||||
|
@echo "✅ All architectures built successfully!"
|
||||||
|
|
||||||
|
# ========================================================================================
|
||||||
|
# Help and Documentation
|
||||||
|
# ========================================================================================
|
||||||
|
|
||||||
|
.PHONY: help-build
|
||||||
|
help-build:
|
||||||
|
@echo "🔨 RustFS 构建帮助:"
|
||||||
|
@echo ""
|
||||||
|
@echo "🚀 本地构建 (推荐使用):"
|
||||||
|
@echo " make build # 构建 RustFS 二进制文件 (默认包含 console)"
|
||||||
|
@echo " make build-dev # 开发模式构建"
|
||||||
|
@echo " make build-musl # 构建 musl 版本"
|
||||||
|
@echo " make build-gnu # 构建 GNU 版本"
|
||||||
|
@echo ""
|
||||||
|
@echo "🐳 Docker 构建:"
|
||||||
|
@echo " make build-docker # 使用 Docker 容器构建"
|
||||||
|
@echo " make build-docker BUILD_OS=ubuntu22.04 # 指定构建系统"
|
||||||
|
@echo ""
|
||||||
|
@echo "🏗️ 跨架构构建:"
|
||||||
|
@echo " make build-cross-all # 构建所有架构的二进制文件"
|
||||||
|
@echo ""
|
||||||
|
@echo "🔧 直接使用 build-rustfs.sh 脚本:"
|
||||||
|
@echo " ./build-rustfs.sh --help # 查看脚本帮助"
|
||||||
|
@echo " ./build-rustfs.sh --no-console # 构建时跳过 console 资源"
|
||||||
|
@echo " ./build-rustfs.sh --force-console-update # 强制更新 console 资源"
|
||||||
|
@echo " ./build-rustfs.sh --dev # 开发模式构建"
|
||||||
|
@echo " ./build-rustfs.sh --sign # 签名二进制文件"
|
||||||
|
@echo " ./build-rustfs.sh --platform x86_64-unknown-linux-musl # 指定目标平台"
|
||||||
|
@echo " ./build-rustfs.sh --skip-verification # 跳过二进制验证"
|
||||||
|
@echo ""
|
||||||
|
@echo "💡 build-rustfs.sh 脚本提供了更多选项、智能检测和二进制验证功能"
|
||||||
|
|
||||||
|
.PHONY: help-docker
|
||||||
|
help-docker:
|
||||||
|
@echo "🐳 Docker 多架构构建帮助:"
|
||||||
|
@echo ""
|
||||||
|
@echo "🚀 生产镜像构建 (推荐使用 docker-buildx.sh):"
|
||||||
|
@echo " make docker-buildx # 构建生产多架构镜像(不推送)"
|
||||||
|
@echo " make docker-buildx-push # 构建并推送生产多架构镜像"
|
||||||
|
@echo " make docker-buildx-version VERSION=v1.0.0 # 构建指定版本"
|
||||||
|
@echo " make docker-buildx-push-version VERSION=v1.0.0 # 构建并推送指定版本"
|
||||||
|
@echo ""
|
||||||
|
@echo "🔧 开发/源码镜像构建 (本地开发测试):"
|
||||||
|
@echo " make docker-dev # 构建开发多架构镜像(无法本地加载)"
|
||||||
|
@echo " make docker-dev-local # 构建开发单架构镜像(本地加载)"
|
||||||
|
@echo " make docker-dev-push REGISTRY=xxx # 构建并推送开发镜像"
|
||||||
|
@echo ""
|
||||||
|
@echo "🏗️ 本地生产镜像构建 (替代方案):"
|
||||||
|
@echo " make docker-buildx-production-local # 本地构建生产单架构镜像"
|
||||||
|
@echo ""
|
||||||
|
@echo "📦 单架构构建 (传统方式):"
|
||||||
|
@echo " make docker-build-production # 构建单架构生产镜像"
|
||||||
|
@echo " make docker-build-source # 构建单架构源码镜像"
|
||||||
|
@echo ""
|
||||||
|
@echo "🚀 开发环境管理:"
|
||||||
|
@echo " make dev-env-start # 启动开发容器环境"
|
||||||
|
@echo " make dev-env-stop # 停止开发容器环境"
|
||||||
|
@echo " make dev-env-restart # 重启开发容器环境"
|
||||||
|
@echo ""
|
||||||
|
@echo "🔧 辅助工具:"
|
||||||
|
@echo " make build-cross-all # 构建所有架构的二进制文件"
|
||||||
|
@echo " make docker-inspect-multiarch IMAGE=xxx # 检查镜像的架构支持"
|
||||||
|
@echo ""
|
||||||
|
@echo "📋 环境变量:"
|
||||||
|
@echo " REGISTRY 镜像仓库地址 (推送时需要)"
|
||||||
|
@echo " DOCKERHUB_USERNAME Docker Hub 用户名"
|
||||||
|
@echo " DOCKERHUB_TOKEN Docker Hub 访问令牌"
|
||||||
|
@echo " GITHUB_TOKEN GitHub 访问令牌"
|
||||||
|
@echo ""
|
||||||
|
@echo "💡 建议:"
|
||||||
|
@echo " - 生产用途: 使用 docker-buildx* 命令 (基于预编译二进制)"
|
||||||
|
@echo " - 本地开发: 使用 docker-dev* 命令 (从源码构建)"
|
||||||
|
@echo " - 开发环境: 使用 dev-env-* 命令管理开发容器"
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@echo "🦀 RustFS Makefile 帮助:"
|
||||||
|
@echo ""
|
||||||
|
@echo "📋 主要命令分类:"
|
||||||
|
@echo " make help-build # 显示构建相关帮助"
|
||||||
|
@echo " make help-docker # 显示 Docker 相关帮助"
|
||||||
|
@echo ""
|
||||||
|
@echo "🔧 代码质量:"
|
||||||
|
@echo " make fmt # 格式化代码"
|
||||||
|
@echo " make clippy # 运行 clippy 检查"
|
||||||
|
@echo " make test # 运行测试"
|
||||||
|
@echo " make pre-commit # 运行所有预提交检查"
|
||||||
|
@echo ""
|
||||||
|
@echo "🚀 快速开始:"
|
||||||
|
@echo " make build # 构建 RustFS 二进制"
|
||||||
|
@echo " make docker-dev-local # 构建开发 Docker 镜像(本地)"
|
||||||
|
@echo " make dev-env-start # 启动开发环境"
|
||||||
|
@echo ""
|
||||||
|
@echo "💡 更多帮助请使用 'make help-build' 或 'make help-docker'"
|
||||||
170
README.md
@@ -1 +1,169 @@
|
|||||||
# s3-rustfs
|
[](https://rustfs.com)
|
||||||
|
|
||||||
|
<p align="center">RustFS is a high-performance distributed object storage software built using Rust</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||||
|
<a href="https://github.com/rustfs/rustfs/actions/workflows/docker.yml"><img alt="Build and Push Docker Images" src="https://github.com/rustfs/rustfs/actions/workflows/docker.yml/badge.svg" /></a>
|
||||||
|
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/rustfs/rustfs"/>
|
||||||
|
<img alt="Github Last Commit" src="https://img.shields.io/github/last-commit/rustfs/rustfs"/>
|
||||||
|
<a href="https://hellogithub.com/repository/rustfs/rustfs" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=b95bcb72bdc340b68f16fdf6790b7d5b&claim_uid=MsbvjYeLDKAH457&theme=small" alt="Featured|HelloGitHub" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://docs.rustfs.com/en/introduction.html">Getting Started</a>
|
||||||
|
· <a href="https://docs.rustfs.com/en/">Docs</a>
|
||||||
|
· <a href="https://github.com/rustfs/rustfs/issues">Bug reports</a>
|
||||||
|
· <a href="https://github.com/rustfs/rustfs/discussions">Discussions</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
English | <a href="https://github.com/rustfs/rustfs/blob/main/README_ZH.md">简体中文</a> |
|
||||||
|
<!-- Keep these links. Translations will automatically update with the README. -->
|
||||||
|
<a href="https://readme-i18n.com/rustfs/rustfs?lang=de">Deutsch</a> |
|
||||||
|
<a href="https://readme-i18n.com/rustfs/rustfs?lang=es">Español</a> |
|
||||||
|
<a href="https://readme-i18n.com/rustfs/rustfs?lang=fr">français</a> |
|
||||||
|
<a href="https://readme-i18n.com/rustfs/rustfs?lang=ja">日本語</a> |
|
||||||
|
<a href="https://readme-i18n.com/rustfs/rustfs?lang=ko">한국어</a> |
|
||||||
|
<a href="https://readme-i18n.com/rustfs/rustfs?lang=pt">Português</a> |
|
||||||
|
<a href="https://readme-i18n.com/rustfs/rustfs?lang=ru">Русский</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
RustFS is a high-performance distributed object storage software built using Rust, one of the most popular languages worldwide. Along with MinIO, it shares a range of advantages such as simplicity, S3 compatibility, open-source nature, support for data lakes, AI, and big data. Furthermore, it has a better and more user-friendly open-source license in comparison to other storage systems, being constructed under the Apache license. As Rust serves as its foundation, RustFS provides faster speed and safer distributed features for high-performance object storage.
|
||||||
|
|
||||||
|
> ⚠️ **RustFS is under rapid development. Do NOT use in production environments!**
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **High Performance**: Built with Rust, ensuring speed and efficiency.
|
||||||
|
- **Distributed Architecture**: Scalable and fault-tolerant design for large-scale deployments.
|
||||||
|
- **S3 Compatibility**: Seamless integration with existing S3-compatible applications.
|
||||||
|
- **Data Lake Support**: Optimized for big data and AI workloads.
|
||||||
|
- **Open Source**: Licensed under Apache 2.0, encouraging community contributions and transparency.
|
||||||
|
- **User-Friendly**: Designed with simplicity in mind, making it easy to deploy and manage.
|
||||||
|
|
||||||
|
## RustFS vs MinIO
|
||||||
|
|
||||||
|
Stress test server parameters
|
||||||
|
|
||||||
|
| Type | parameter | Remark |
|
||||||
|
| - | - | - |
|
||||||
|
|CPU | 2 Core | Intel Xeon(Sapphire Rapids) Platinum 8475B , 2.7/3.2 GHz| |
|
||||||
|
|Memory| 4GB | |
|
||||||
|
|Network | 15Gbp | |
|
||||||
|
|Driver | 40GB x 4 | IOPS 3800 / Driver |
|
||||||
|
|
||||||
|
<https://github.com/user-attachments/assets/2e4979b5-260c-4f2c-ac12-c87fd558072a>
|
||||||
|
|
||||||
|
### RustFS vs Other object storage
|
||||||
|
|
||||||
|
| RustFS | Other object storage|
|
||||||
|
| - | - |
|
||||||
|
| Powerful Console | Simple and useless Console |
|
||||||
|
| Developed based on Rust language, memory is safer | Developed in Go or C, with potential issues like memory GC/leaks |
|
||||||
|
| Does not report logs to third-party countries | Reporting logs to other third countries may violate national security laws |
|
||||||
|
| Licensed under Apache, more business-friendly | AGPL V3 License and other License, polluted open source and License traps, infringement of intellectual property rights |
|
||||||
|
| Comprehensive S3 support, works with domestic and international cloud providers | Full support for S3, but no local cloud vendor support |
|
||||||
|
| Rust-based development, strong support for secure and innovative devices | Poor support for edge gateways and secure innovative devices|
|
||||||
|
| Stable commercial prices, free community support | High pricing, with costs up to $250,000 for 1PiB |
|
||||||
|
| No risk | Intellectual property risks and risks of prohibited uses |
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
To get started with RustFS, follow these steps:
|
||||||
|
|
||||||
|
1. **One-click installation script (Option 1)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -O https://rustfs.com/install_rustfs.sh && bash install_rustfs.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Docker Quick Start (Option 2)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Latest stable release
|
||||||
|
docker run -d -p 9000:9000 -v /data:/data rustfs/rustfs:latest
|
||||||
|
|
||||||
|
# Development version (main branch)
|
||||||
|
docker run -d -p 9000:9000 -v /data:/data rustfs/rustfs:main-latest
|
||||||
|
|
||||||
|
# Specific version
|
||||||
|
docker run -d -p 9000:9000 -v /data:/data rustfs/rustfs:v1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Build from Source (Option 3) - Advanced Users**
|
||||||
|
|
||||||
|
For developers who want to build RustFS Docker images from source with multi-architecture support:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build multi-architecture images locally
|
||||||
|
./docker-buildx.sh --build-arg RELEASE=latest
|
||||||
|
|
||||||
|
# Build and push to registry
|
||||||
|
./docker-buildx.sh --push
|
||||||
|
|
||||||
|
# Build specific version
|
||||||
|
./docker-buildx.sh --release v1.0.0 --push
|
||||||
|
|
||||||
|
# Build for custom registry
|
||||||
|
./docker-buildx.sh --registry your-registry.com --namespace yourname --push
|
||||||
|
```
|
||||||
|
|
||||||
|
The `docker-buildx.sh` script supports:
|
||||||
|
- **Multi-architecture builds**: `linux/amd64`, `linux/arm64`
|
||||||
|
- **Automatic version detection**: Uses git tags or commit hashes
|
||||||
|
- **Registry flexibility**: Supports Docker Hub, GitHub Container Registry, etc.
|
||||||
|
- **Build optimization**: Includes caching and parallel builds
|
||||||
|
|
||||||
|
You can also use Make targets for convenience:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docker-buildx # Build locally
|
||||||
|
make docker-buildx-push # Build and push
|
||||||
|
make docker-buildx-version VERSION=v1.0.0 # Build specific version
|
||||||
|
make help-docker # Show all Docker-related commands
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Access the Console**: Open your web browser and navigate to `http://localhost:9000` to access the RustFS console, default username and password is `rustfsadmin` .
|
||||||
|
5. **Create a Bucket**: Use the console to create a new bucket for your objects.
|
||||||
|
6. **Upload Objects**: You can upload files directly through the console or use S3-compatible APIs to interact with your RustFS instance.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
For detailed documentation, including configuration options, API references, and advanced usage, please visit our [Documentation](https://docs.rustfs.com).
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
If you have any questions or need assistance, you can:
|
||||||
|
|
||||||
|
- Check the [FAQ](https://github.com/rustfs/rustfs/discussions/categories/q-a) for common issues and solutions.
|
||||||
|
- Join our [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) to ask questions and share your experiences.
|
||||||
|
- Open an issue on our [GitHub Issues](https://github.com/rustfs/rustfs/issues) page for bug reports or feature requests.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [Documentation](https://docs.rustfs.com) - The manual you should read
|
||||||
|
- [Changelog](https://github.com/rustfs/rustfs/releases) - What we broke and fixed
|
||||||
|
- [GitHub Discussions](https://github.com/rustfs/rustfs/discussions) - Where the community lives
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
- **Bugs**: [GitHub Issues](https://github.com/rustfs/rustfs/issues)
|
||||||
|
- **Business**: <hello@rustfs.com>
|
||||||
|
- **Jobs**: <jobs@rustfs.com>
|
||||||
|
- **General Discussion**: [GitHub Discussions](https://github.com/rustfs/rustfs/discussions)
|
||||||
|
- **Contributing**: [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
RustFS is a community-driven project, and we appreciate all contributions. Check out the [Contributors](https://github.com/rustfs/rustfs/graphs/contributors) page to see the amazing people who have helped make RustFS better.
|
||||||
|
|
||||||
|
<a href="https://github.com/rustfs/rustfs/graphs/contributors">
|
||||||
|
<img src="https://opencollective.com/rustfs/contributors.svg?width=890&limit=500&button=false" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[Apache 2.0](https://opensource.org/licenses/Apache-2.0)
|
||||||
|
|
||||||
|
**RustFS** is a trademark of RustFS, Inc. All other trademarks are the property of their respective owners.
|
||||||
|
|||||||
119
README_ZH.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
[](https://rustfs.com)
|
||||||
|
|
||||||
|
<p align="center">RustFS 是一个使用 Rust 构建的高性能分布式对象存储软件</p >
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/rustfs/rustfs/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/rustfs/rustfs/actions/workflows/ci.yml/badge.svg" /></a>
|
||||||
|
<a href="https://github.com/rustfs/rustfs/actions/workflows/docker.yml"><img alt="Build and Push Docker Images" src="https://github.com/rustfs/rustfs/actions/workflows/docker.yml/badge.svg" /></a>
|
||||||
|
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/rustfs/rustfs"/>
|
||||||
|
<img alt="Github Last Commit" src="https://img.shields.io/github/last-commit/rustfs/rustfs"/>
|
||||||
|
<a href="https://hellogithub.com/repository/rustfs/rustfs" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=b95bcb72bdc340b68f16fdf6790b7d5b&claim_uid=MsbvjYeLDKAH457&theme=small" alt="Featured|HelloGitHub" /></a>
|
||||||
|
</p >
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://docs.rustfs.com/zh/introduction.html">快速开始</a >
|
||||||
|
· <a href="https://docs.rustfs.com/zh/">文档</a >
|
||||||
|
· <a href="https://github.com/rustfs/rustfs/issues">问题报告</a >
|
||||||
|
· <a href="https://github.com/rustfs/rustfs/discussions">讨论</a >
|
||||||
|
</p >
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/rustfs/rustfs/blob/main/README.md">English</a > | 简体中文
|
||||||
|
</p >
|
||||||
|
|
||||||
|
RustFS 是一个使用 Rust(全球最受欢迎的编程语言之一)构建的高性能分布式对象存储软件。与 MinIO 一样,它具有简单性、S3 兼容性、开源特性以及对数据湖、AI 和大数据的支持等一系列优势。此外,与其他存储系统相比,它采用 Apache 许可证构建,拥有更好、更用户友好的开源许可证。由于以 Rust 为基础,RustFS 为高性能对象存储提供了更快的速度和更安全的分布式功能。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- **高性能**:使用 Rust 构建,确保速度和效率。
|
||||||
|
- **分布式架构**:可扩展且容错的设计,适用于大规模部署。
|
||||||
|
- **S3 兼容性**:与现有 S3 兼容应用程序无缝集成。
|
||||||
|
- **数据湖支持**:针对大数据和 AI 工作负载进行了优化。
|
||||||
|
- **开源**:采用 Apache 2.0 许可证,鼓励社区贡献和透明度。
|
||||||
|
- **用户友好**:设计简单,易于部署和管理。
|
||||||
|
|
||||||
|
## RustFS vs MinIO
|
||||||
|
|
||||||
|
压力测试服务器参数
|
||||||
|
|
||||||
|
| 类型 | 参数 | 备注 |
|
||||||
|
| - | - | - |
|
||||||
|
|CPU | 2 核心 | Intel Xeon(Sapphire Rapids) Platinum 8475B , 2.7/3.2 GHz| |
|
||||||
|
|内存| 4GB | |
|
||||||
|
|网络 | 15Gbp | |
|
||||||
|
|驱动器 | 40GB x 4 | IOPS 3800 / 驱动器 |
|
||||||
|
|
||||||
|
<https://github.com/user-attachments/assets/2e4979b5-260c-4f2c-ac12-c87fd558072a>
|
||||||
|
|
||||||
|
### RustFS vs 其他对象存储
|
||||||
|
|
||||||
|
| RustFS | 其他对象存储|
|
||||||
|
| - | - |
|
||||||
|
| 强大的控制台 | 简单且无用的控制台 |
|
||||||
|
| 基于 Rust 语言开发,内存更安全 | 使用 Go 或 C 开发,存在内存 GC/泄漏等潜在问题 |
|
||||||
|
| 不向第三方国家报告日志 | 向其他第三方国家报告日志可能违反国家安全法律 |
|
||||||
|
| 采用 Apache 许可证,对商业更友好 | AGPL V3 许可证等其他许可证,污染开源和许可证陷阱,侵犯知识产权 |
|
||||||
|
| 全面的 S3 支持,适用于国内外云提供商 | 完全支持 S3,但不支持本地云厂商 |
|
||||||
|
| 基于 Rust 开发,对安全和创新设备有强大支持 | 对边缘网关和安全创新设备支持较差|
|
||||||
|
| 稳定的商业价格,免费社区支持 | 高昂的定价,1PiB 成本高达 $250,000 |
|
||||||
|
| 无风险 | 知识产权风险和禁止使用的风险 |
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
要开始使用 RustFS,请按照以下步骤操作:
|
||||||
|
|
||||||
|
1. **一键脚本快速启动 (方案一)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -O https://rustfs.com/install_rustfs.sh && bash install_rustfs.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Docker快速启动(方案二)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 9000:9000 -v /data:/data rustfs/rustfs
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **访问控制台**:打开 Web 浏览器并导航到 `http://localhost:9000` 以访问 RustFS 控制台,默认的用户名和密码是 `rustfsadmin` 。
|
||||||
|
4. **创建存储桶**:使用控制台为您的对象创建新的存储桶。
|
||||||
|
5. **上传对象**:您可以直接通过控制台上传文件,或使用 S3 兼容的 API 与您的 RustFS 实例交互。
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
有关详细文档,包括配置选项、API 参考和高级用法,请访问我们的[文档](https://docs.rustfs.com)。
|
||||||
|
|
||||||
|
## 获取帮助
|
||||||
|
|
||||||
|
如果您有任何问题或需要帮助,您可以:
|
||||||
|
|
||||||
|
- 查看[常见问题解答](https://github.com/rustfs/rustfs/discussions/categories/q-a)以获取常见问题和解决方案。
|
||||||
|
- 加入我们的 [GitHub 讨论](https://github.com/rustfs/rustfs/discussions)来提问和分享您的经验。
|
||||||
|
- 在我们的 [GitHub Issues](https://github.com/rustfs/rustfs/issues) 页面上开启问题,报告错误或功能请求。
|
||||||
|
|
||||||
|
## 链接
|
||||||
|
|
||||||
|
- [文档](https://docs.rustfs.com) - 您应该阅读的手册
|
||||||
|
- [更新日志](https://docs.rustfs.com/changelog) - 我们破坏和修复的内容
|
||||||
|
- [GitHub 讨论](https://github.com/rustfs/rustfs/discussions) - 社区所在地
|
||||||
|
|
||||||
|
## 联系
|
||||||
|
|
||||||
|
- **错误报告**:[GitHub Issues](https://github.com/rustfs/rustfs/issues)
|
||||||
|
- **商务合作**:<hello@rustfs.com>
|
||||||
|
- **招聘**:<jobs@rustfs.com>
|
||||||
|
- **一般讨论**:[GitHub 讨论](https://github.com/rustfs/rustfs/discussions)
|
||||||
|
- **贡献**:[CONTRIBUTING.md](CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## 贡献者
|
||||||
|
|
||||||
|
RustFS 是一个社区驱动的项目,我们感谢所有的贡献。查看[贡献者](https://github.com/rustfs/rustfs/graphs/contributors)页面,了解帮助 RustFS 变得更好的杰出人员。
|
||||||
|
|
||||||
|
<a href="https://github.com/rustfs/rustfs/graphs/contributors">
|
||||||
|
<img src="https://opencollective.com/rustfs/contributors.svg?width=890&limit=500&button=false" />
|
||||||
|
</a >
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
[Apache 2.0](https://opensource.org/licenses/Apache-2.0)
|
||||||
|
|
||||||
|
**RustFS** 是 RustFS, Inc. 的商标。所有其他商标均为其各自所有者的财产。
|
||||||
18
SECURITY.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Use this section to tell people about which versions of your project are
|
||||||
|
currently being supported with security updates.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| 1.x.x | :white_check_mark: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Use this section to tell people how to report a vulnerability.
|
||||||
|
|
||||||
|
Tell them where to go, how often they can expect to get an update on a
|
||||||
|
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||||
|
declined, etc.
|
||||||
564
build-rustfs.sh
Executable file
@@ -0,0 +1,564 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# RustFS Binary Build Script
|
||||||
|
# This script compiles RustFS binaries for different platforms and architectures
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Auto-detect current platform
|
||||||
|
detect_platform() {
|
||||||
|
local arch=$(uname -m)
|
||||||
|
local os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
|
case "$os" in
|
||||||
|
"linux")
|
||||||
|
case "$arch" in
|
||||||
|
"x86_64")
|
||||||
|
echo "x86_64-unknown-linux-musl"
|
||||||
|
;;
|
||||||
|
"aarch64"|"arm64")
|
||||||
|
echo "aarch64-unknown-linux-musl"
|
||||||
|
;;
|
||||||
|
"armv7l")
|
||||||
|
echo "armv7-unknown-linux-musleabihf"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknown-platform"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
"darwin")
|
||||||
|
case "$arch" in
|
||||||
|
"x86_64")
|
||||||
|
echo "x86_64-apple-darwin"
|
||||||
|
;;
|
||||||
|
"arm64"|"aarch64")
|
||||||
|
echo "aarch64-apple-darwin"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknown-platform"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknown-platform"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cross-platform SHA256 checksum generation
|
||||||
|
generate_sha256() {
|
||||||
|
local file="$1"
|
||||||
|
local output_file="$2"
|
||||||
|
local os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
|
||||||
|
case "$os" in
|
||||||
|
"linux")
|
||||||
|
if command -v sha256sum &> /dev/null; then
|
||||||
|
sha256sum "$file" > "$output_file"
|
||||||
|
elif command -v shasum &> /dev/null; then
|
||||||
|
shasum -a 256 "$file" > "$output_file"
|
||||||
|
else
|
||||||
|
print_message $RED "❌ No SHA256 command found (sha256sum or shasum)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
"darwin")
|
||||||
|
if command -v shasum &> /dev/null; then
|
||||||
|
shasum -a 256 "$file" > "$output_file"
|
||||||
|
elif command -v sha256sum &> /dev/null; then
|
||||||
|
sha256sum "$file" > "$output_file"
|
||||||
|
else
|
||||||
|
print_message $RED "❌ No SHA256 command found (shasum or sha256sum)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Try common commands in order
|
||||||
|
if command -v sha256sum &> /dev/null; then
|
||||||
|
sha256sum "$file" > "$output_file"
|
||||||
|
elif command -v shasum &> /dev/null; then
|
||||||
|
shasum -a 256 "$file" > "$output_file"
|
||||||
|
else
|
||||||
|
print_message $RED "❌ No SHA256 command found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
OUTPUT_DIR="target/release"
|
||||||
|
PLATFORM=$(detect_platform) # Auto-detect current platform
|
||||||
|
BINARY_NAME="rustfs"
|
||||||
|
BUILD_TYPE="release"
|
||||||
|
SIGN=false
|
||||||
|
WITH_CONSOLE=true
|
||||||
|
FORCE_CONSOLE_UPDATE=false
|
||||||
|
CONSOLE_VERSION="latest"
|
||||||
|
SKIP_VERIFICATION=false
|
||||||
|
CUSTOM_PLATFORM=""
|
||||||
|
|
||||||
|
# Print usage
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 [OPTIONS]"
|
||||||
|
echo ""
|
||||||
|
echo "Description:"
|
||||||
|
echo " Build RustFS binary for the current platform. Designed for CI/CD pipelines"
|
||||||
|
echo " where different runners build platform-specific binaries natively."
|
||||||
|
echo " Includes automatic verification to ensure the built binary is functional."
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " -o, --output-dir DIR Output directory (default: target/release)"
|
||||||
|
echo " -b, --binary-name NAME Binary name (default: rustfs)"
|
||||||
|
echo " -p, --platform TARGET Target platform (default: auto-detect)"
|
||||||
|
echo " --dev Build in dev mode"
|
||||||
|
echo " --sign Sign binaries after build"
|
||||||
|
echo " --with-console Download console static assets (default)"
|
||||||
|
echo " --no-console Skip console static assets"
|
||||||
|
echo " --force-console-update Force update console assets even if they exist"
|
||||||
|
echo " --console-version VERSION Console version to download (default: latest)"
|
||||||
|
echo " --skip-verification Skip binary verification after build"
|
||||||
|
echo " -h, --help Show this help message"
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 # Build for current platform (includes console assets)"
|
||||||
|
echo " $0 --dev # Development build"
|
||||||
|
echo " $0 --sign # Build and sign binary (release CI)"
|
||||||
|
echo " $0 --no-console # Build without console static assets"
|
||||||
|
echo " $0 --force-console-update # Force update console assets"
|
||||||
|
echo " $0 --platform x86_64-unknown-linux-musl # Build for specific platform"
|
||||||
|
echo " $0 --skip-verification # Skip binary verification (for cross-compilation)"
|
||||||
|
echo ""
|
||||||
|
echo "Detected platform: $(detect_platform)"
|
||||||
|
echo "CI Usage: Run this script on each platform's runner to build native binaries"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print colored message
|
||||||
|
print_message() {
|
||||||
|
local color=$1
|
||||||
|
local message=$2
|
||||||
|
echo -e "${color}${message}${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get version from git
|
||||||
|
get_version() {
|
||||||
|
if git describe --abbrev=0 --tags >/dev/null 2>&1; then
|
||||||
|
git describe --abbrev=0 --tags
|
||||||
|
else
|
||||||
|
git rev-parse --short HEAD
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Setup rust environment
|
||||||
|
setup_rust_environment() {
|
||||||
|
print_message $BLUE "🔧 Setting up Rust environment..."
|
||||||
|
|
||||||
|
# Install required target for current platform
|
||||||
|
print_message $YELLOW "Installing target: $PLATFORM"
|
||||||
|
rustup target add "$PLATFORM"
|
||||||
|
|
||||||
|
# Set up environment variables for musl targets
|
||||||
|
if [[ "$PLATFORM" == *"musl"* ]]; then
|
||||||
|
print_message $YELLOW "Setting up environment for musl target..."
|
||||||
|
export RUSTFLAGS="-C target-feature=-crt-static"
|
||||||
|
|
||||||
|
# For cargo-zigbuild, set up additional environment variables
|
||||||
|
if command -v cargo-zigbuild &> /dev/null; then
|
||||||
|
print_message $YELLOW "Configuring cargo-zigbuild for musl target..."
|
||||||
|
|
||||||
|
# Set environment variables for better musl support
|
||||||
|
export CC_x86_64_unknown_linux_musl="zig cc -target x86_64-linux-musl"
|
||||||
|
export CXX_x86_64_unknown_linux_musl="zig c++ -target x86_64-linux-musl"
|
||||||
|
export AR_x86_64_unknown_linux_musl="zig ar"
|
||||||
|
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="zig cc -target x86_64-linux-musl"
|
||||||
|
|
||||||
|
export CC_aarch64_unknown_linux_musl="zig cc -target aarch64-linux-musl"
|
||||||
|
export CXX_aarch64_unknown_linux_musl="zig c++ -target aarch64-linux-musl"
|
||||||
|
export AR_aarch64_unknown_linux_musl="zig ar"
|
||||||
|
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="zig cc -target aarch64-linux-musl"
|
||||||
|
|
||||||
|
# Set environment variables for zstd-sys to avoid target parsing issues
|
||||||
|
export ZSTD_SYS_USE_PKG_CONFIG=1
|
||||||
|
export PKG_CONFIG_ALLOW_CROSS=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install required tools
|
||||||
|
if [ "$SIGN" = true ]; then
|
||||||
|
if ! command -v minisign &> /dev/null; then
|
||||||
|
print_message $YELLOW "Installing minisign for binary signing..."
|
||||||
|
cargo install minisign
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download console static assets
|
||||||
|
download_console_assets() {
|
||||||
|
local static_dir="rustfs/static"
|
||||||
|
local console_exists=false
|
||||||
|
|
||||||
|
# Check if console assets already exist
|
||||||
|
if [ -d "$static_dir" ] && [ -f "$static_dir/index.html" ]; then
|
||||||
|
console_exists=true
|
||||||
|
local static_size=$(du -sh "$static_dir" 2>/dev/null | cut -f1 || echo "unknown")
|
||||||
|
print_message $YELLOW "Console static assets already exist ($static_size)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine if we need to download
|
||||||
|
local should_download=false
|
||||||
|
if [ "$WITH_CONSOLE" = true ]; then
|
||||||
|
if [ "$console_exists" = false ]; then
|
||||||
|
print_message $BLUE "🎨 Console assets not found, downloading..."
|
||||||
|
should_download=true
|
||||||
|
elif [ "$FORCE_CONSOLE_UPDATE" = true ]; then
|
||||||
|
print_message $BLUE "🎨 Force updating console assets..."
|
||||||
|
should_download=true
|
||||||
|
else
|
||||||
|
print_message $GREEN "✅ Console assets already available, skipping download"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ "$console_exists" = true ]; then
|
||||||
|
print_message $GREEN "✅ Using existing console assets"
|
||||||
|
else
|
||||||
|
print_message $YELLOW "⚠️ Console assets not found. Use --download-console to download them."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$should_download" = true ]; then
|
||||||
|
print_message $BLUE "📥 Downloading console static assets..."
|
||||||
|
|
||||||
|
# Create static directory
|
||||||
|
mkdir -p "$static_dir"
|
||||||
|
|
||||||
|
# Download from GitHub Releases (consistent with Docker build)
|
||||||
|
local download_url
|
||||||
|
if [ "$CONSOLE_VERSION" = "latest" ]; then
|
||||||
|
print_message $YELLOW "Getting latest console release info..."
|
||||||
|
# For now, use dl.rustfs.com as fallback until GitHub Releases includes console assets
|
||||||
|
download_url="https://dl.rustfs.com/artifacts/console/rustfs-console-latest.zip"
|
||||||
|
else
|
||||||
|
download_url="https://dl.rustfs.com/artifacts/console/rustfs-console-${CONSOLE_VERSION}.zip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_message $YELLOW "Downloading from: $download_url"
|
||||||
|
|
||||||
|
# Download with retries
|
||||||
|
local temp_file="console-assets-temp.zip"
|
||||||
|
local download_success=false
|
||||||
|
|
||||||
|
for i in {1..3}; do
|
||||||
|
if curl -L "$download_url" -o "$temp_file" --retry 3 --retry-delay 5 --max-time 300; then
|
||||||
|
download_success=true
|
||||||
|
break
|
||||||
|
else
|
||||||
|
print_message $YELLOW "Download attempt $i failed, retrying..."
|
||||||
|
sleep 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$download_success" = true ]; then
|
||||||
|
# Verify the downloaded file
|
||||||
|
if [ -f "$temp_file" ] && [ -s "$temp_file" ]; then
|
||||||
|
print_message $BLUE "📦 Extracting console assets..."
|
||||||
|
|
||||||
|
# Extract to static directory
|
||||||
|
if unzip -o "$temp_file" -d "$static_dir"; then
|
||||||
|
rm "$temp_file"
|
||||||
|
local final_size=$(du -sh "$static_dir" 2>/dev/null | cut -f1 || echo "unknown")
|
||||||
|
print_message $GREEN "✅ Console assets downloaded successfully ($final_size)"
|
||||||
|
else
|
||||||
|
print_message $RED "❌ Failed to extract console assets"
|
||||||
|
rm -f "$temp_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_message $RED "❌ Downloaded file is empty or invalid"
|
||||||
|
rm -f "$temp_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_message $RED "❌ Failed to download console assets after 3 attempts"
|
||||||
|
print_message $YELLOW "💡 Console assets are optional. Build will continue without them."
|
||||||
|
rm -f "$temp_file"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify binary functionality
|
||||||
|
verify_binary() {
|
||||||
|
local binary_path="$1"
|
||||||
|
|
||||||
|
# Check if binary exists
|
||||||
|
if [ ! -f "$binary_path" ]; then
|
||||||
|
print_message $RED "❌ Binary file not found: $binary_path"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if binary is executable
|
||||||
|
if [ ! -x "$binary_path" ]; then
|
||||||
|
print_message $RED "❌ Binary is not executable: $binary_path"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check basic functionality - try to run help command
|
||||||
|
print_message $YELLOW " Testing --help command..."
|
||||||
|
if ! "$binary_path" --help >/dev/null 2>&1; then
|
||||||
|
print_message $RED "❌ Binary failed to run --help command"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check version command
|
||||||
|
print_message $YELLOW " Testing --version command..."
|
||||||
|
if ! "$binary_path" --version >/dev/null 2>&1; then
|
||||||
|
print_message $YELLOW "⚠️ Binary does not support --version command (this is optional)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to get some basic info about the binary
|
||||||
|
local file_info=$(file "$binary_path" 2>/dev/null || echo "unknown")
|
||||||
|
print_message $YELLOW " Binary info: $file_info"
|
||||||
|
|
||||||
|
# Check if it's a valid ELF/Mach-O binary
|
||||||
|
if command -v readelf >/dev/null 2>&1; then
|
||||||
|
if readelf -h "$binary_path" >/dev/null 2>&1; then
|
||||||
|
print_message $YELLOW " ELF binary structure: valid"
|
||||||
|
fi
|
||||||
|
elif command -v otool >/dev/null 2>&1; then
|
||||||
|
if otool -h "$binary_path" >/dev/null 2>&1; then
|
||||||
|
print_message $YELLOW " Mach-O binary structure: valid"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build binary for current platform
|
||||||
|
build_binary() {
|
||||||
|
local version=$(get_version)
|
||||||
|
local output_file="${OUTPUT_DIR}/${PLATFORM}/${BINARY_NAME}"
|
||||||
|
|
||||||
|
print_message $BLUE "🏗️ Building for platform: $PLATFORM"
|
||||||
|
print_message $YELLOW " Version: $version"
|
||||||
|
print_message $YELLOW " Output: $output_file"
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
mkdir -p "${OUTPUT_DIR}/${PLATFORM}"
|
||||||
|
|
||||||
|
# Simple build logic matching the working version (4fb4b353)
|
||||||
|
# Force rebuild by touching build.rs
|
||||||
|
touch rustfs/build.rs
|
||||||
|
|
||||||
|
# Determine build command based on platform and cross-compilation needs
|
||||||
|
local build_cmd=""
|
||||||
|
local current_platform=$(detect_platform)
|
||||||
|
|
||||||
|
print_message $BLUE "📦 Using working version build logic..."
|
||||||
|
|
||||||
|
# Check if we need cross-compilation
|
||||||
|
if [ "$PLATFORM" != "$current_platform" ]; then
|
||||||
|
# Cross-compilation needed
|
||||||
|
if [[ "$PLATFORM" == *"apple-darwin"* ]]; then
|
||||||
|
print_message $RED "❌ macOS cross-compilation not supported"
|
||||||
|
print_message $YELLOW "💡 macOS targets must be built natively on macOS runners"
|
||||||
|
return 1
|
||||||
|
elif [[ "$PLATFORM" == *"windows"* ]]; then
|
||||||
|
# Use cross for Windows ARM64
|
||||||
|
if ! command -v cross &> /dev/null; then
|
||||||
|
print_message $YELLOW "📦 Installing cross tool..."
|
||||||
|
cargo install cross --git https://github.com/cross-rs/cross
|
||||||
|
fi
|
||||||
|
build_cmd="cross build"
|
||||||
|
else
|
||||||
|
# Use zigbuild for Linux ARM64 (matches working version)
|
||||||
|
if ! command -v cargo-zigbuild &> /dev/null; then
|
||||||
|
print_message $RED "❌ cargo-zigbuild not found. Please install it first."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
build_cmd="cargo zigbuild"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Native compilation
|
||||||
|
build_cmd="cargo build"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$BUILD_TYPE" = "release" ]; then
|
||||||
|
build_cmd+=" --release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
build_cmd+=" --target $PLATFORM"
|
||||||
|
build_cmd+=" -p rustfs --bins"
|
||||||
|
|
||||||
|
print_message $BLUE "📦 Executing: $build_cmd"
|
||||||
|
|
||||||
|
# Execute build (this matches exactly what the working version does)
|
||||||
|
if eval $build_cmd; then
|
||||||
|
print_message $GREEN "✅ Successfully built for $PLATFORM"
|
||||||
|
|
||||||
|
# Copy binary to output directory
|
||||||
|
cp "target/${PLATFORM}/${BUILD_TYPE}/${BINARY_NAME}" "$output_file"
|
||||||
|
|
||||||
|
# Generate checksums
|
||||||
|
print_message $BLUE "🔐 Generating checksums..."
|
||||||
|
(cd "${OUTPUT_DIR}/${PLATFORM}" && generate_sha256 "${BINARY_NAME}" "${BINARY_NAME}.sha256sum")
|
||||||
|
|
||||||
|
# Verify binary functionality (if not skipped)
|
||||||
|
if [ "$SKIP_VERIFICATION" = false ]; then
|
||||||
|
print_message $BLUE "🔍 Verifying binary functionality..."
|
||||||
|
if verify_binary "$output_file"; then
|
||||||
|
print_message $GREEN "✅ Binary verification passed"
|
||||||
|
else
|
||||||
|
print_message $RED "❌ Binary verification failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_message $YELLOW "⚠️ Binary verification skipped by user request"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sign binary if requested
|
||||||
|
if [ "$SIGN" = true ]; then
|
||||||
|
print_message $BLUE "✍️ Signing binary..."
|
||||||
|
(cd "${OUTPUT_DIR}/${PLATFORM}" && minisign -S -m "${BINARY_NAME}" -s ~/.minisign/minisign.key)
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_message $GREEN "✅ Build completed successfully"
|
||||||
|
else
|
||||||
|
print_message $RED "❌ Failed to build for $PLATFORM"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Main build function
|
||||||
|
build_rustfs() {
|
||||||
|
local version=$(get_version)
|
||||||
|
|
||||||
|
print_message $BLUE "🚀 Starting RustFS binary build process..."
|
||||||
|
print_message $YELLOW " Version: $version"
|
||||||
|
print_message $YELLOW " Platform: $PLATFORM"
|
||||||
|
print_message $YELLOW " Output Directory: $OUTPUT_DIR"
|
||||||
|
print_message $YELLOW " Build Type: $BUILD_TYPE"
|
||||||
|
print_message $YELLOW " Sign: $SIGN"
|
||||||
|
print_message $YELLOW " With Console: $WITH_CONSOLE"
|
||||||
|
if [ "$WITH_CONSOLE" = true ]; then
|
||||||
|
print_message $YELLOW " Console Version: $CONSOLE_VERSION"
|
||||||
|
print_message $YELLOW " Force Console Update: $FORCE_CONSOLE_UPDATE"
|
||||||
|
fi
|
||||||
|
print_message $YELLOW " Skip Verification: $SKIP_VERIFICATION"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Setup environment
|
||||||
|
setup_rust_environment
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Download console assets if requested
|
||||||
|
download_console_assets
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Build binary
|
||||||
|
build_binary
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
print_message $GREEN "🎉 Build process completed successfully!"
|
||||||
|
|
||||||
|
# Show built binary
|
||||||
|
local binary_file="${OUTPUT_DIR}/${PLATFORM}/${BINARY_NAME}"
|
||||||
|
if [ -f "$binary_file" ]; then
|
||||||
|
local size=$(ls -lh "$binary_file" | awk '{print $5}')
|
||||||
|
print_message $BLUE "📋 Built binary: $binary_file ($size)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-o|--output-dir)
|
||||||
|
OUTPUT_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-b|--binary-name)
|
||||||
|
BINARY_NAME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-p|--platform)
|
||||||
|
CUSTOM_PLATFORM="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dev)
|
||||||
|
BUILD_TYPE="debug"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--sign)
|
||||||
|
SIGN=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--with-console)
|
||||||
|
WITH_CONSOLE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-console)
|
||||||
|
WITH_CONSOLE=false
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--force-console-update)
|
||||||
|
FORCE_CONSOLE_UPDATE=true
|
||||||
|
WITH_CONSOLE=true # Auto-enable download when forcing update
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--console-version)
|
||||||
|
CONSOLE_VERSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--skip-verification)
|
||||||
|
SKIP_VERIFICATION=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_message $RED "❌ Unknown option: $1"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Main execution
|
||||||
|
main() {
|
||||||
|
print_message $BLUE "🦀 RustFS Binary Build Script"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if we're in a Rust project
|
||||||
|
if [ ! -f "Cargo.toml" ]; then
|
||||||
|
print_message $RED "❌ No Cargo.toml found. Are you in a Rust project directory?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Override platform if specified
|
||||||
|
if [ -n "$CUSTOM_PLATFORM" ]; then
|
||||||
|
PLATFORM="$CUSTOM_PLATFORM"
|
||||||
|
print_message $YELLOW "🎯 Using specified platform: $PLATFORM"
|
||||||
|
|
||||||
|
# Auto-enable skip verification for cross-compilation
|
||||||
|
if [ "$PLATFORM" != "$(detect_platform)" ]; then
|
||||||
|
SKIP_VERIFICATION=true
|
||||||
|
print_message $YELLOW "⚠️ Cross-compilation detected, enabling --skip-verification"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start build process
|
||||||
|
build_rustfs
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function
|
||||||
|
main
|
||||||
|
|
||||||
46
cli/rustfs-gui/Cargo.toml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "rustfs-gui"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = { workspace = true }
|
||||||
|
dioxus = { workspace = true, features = ["router"] }
|
||||||
|
dirs = { workspace = true }
|
||||||
|
hex = { workspace = true }
|
||||||
|
keyring = { workspace = true }
|
||||||
|
rfd = { workspace = true }
|
||||||
|
rust-embed = { workspace = true, features = ["interpolate-folder-path"] }
|
||||||
|
rust-i18n = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
sha2 = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["io-util", "net", "process", "sync"] }
|
||||||
|
tracing-subscriber = { workspace = true, features = ["fmt", "env-filter", "tracing-log", "time", "local-time", "json"] }
|
||||||
|
tracing-appender = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["desktop"]
|
||||||
|
web = ["dioxus/web"]
|
||||||
|
desktop = ["dioxus/desktop"]
|
||||||
|
mobile = ["dioxus/mobile"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
52
cli/rustfs-gui/Dioxus.toml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
[application]
|
||||||
|
|
||||||
|
# App (Project) Name
|
||||||
|
name = "rustfs-gui"
|
||||||
|
|
||||||
|
# The static resource path
|
||||||
|
asset_dir = "public"
|
||||||
|
|
||||||
|
[web.app]
|
||||||
|
|
||||||
|
# HTML title tag content
|
||||||
|
title = "rustfs-gui"
|
||||||
|
|
||||||
|
# include `assets` in web platform
|
||||||
|
[web.resource]
|
||||||
|
|
||||||
|
# Additional CSS style files
|
||||||
|
style = []
|
||||||
|
|
||||||
|
# Additional JavaScript files
|
||||||
|
script = []
|
||||||
|
|
||||||
|
[web.resource.dev]
|
||||||
|
|
||||||
|
# Javascript code file
|
||||||
|
# serve: [dev-server] only
|
||||||
|
script = []
|
||||||
|
|
||||||
|
[bundle]
|
||||||
|
identifier = "com.rustfs.cli.gui"
|
||||||
|
|
||||||
|
publisher = "RustFsGUI"
|
||||||
|
|
||||||
|
category = "Utility"
|
||||||
|
|
||||||
|
copyright = "Copyright 2025 rustfs.com"
|
||||||
|
|
||||||
|
icon = [
|
||||||
|
"assets/icons/icon.icns",
|
||||||
|
"assets/icons/icon.ico",
|
||||||
|
"assets/icons/icon.png",
|
||||||
|
"assets/icons/rustfs-icon.png",
|
||||||
|
]
|
||||||
|
#[bundle.macos]
|
||||||
|
#provider_short_name = "RustFs"
|
||||||
|
[bundle.windows]
|
||||||
|
tsp = true
|
||||||
|
icon_path = "assets/icons/icon.ico"
|
||||||
|
allow_downgrades = true
|
||||||
|
[bundle.windows.webview_install_mode]
|
||||||
|
[bundle.windows.webview_install_mode.EmbedBootstrapper]
|
||||||
|
silent = true
|
||||||
34
cli/rustfs-gui/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
## Rustfs GUI
|
||||||
|
|
||||||
|
### Tailwind
|
||||||
|
|
||||||
|
1. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
|
||||||
|
2. Install the Tailwind CSS CLI: https://tailwindcss.com/docs/installation
|
||||||
|
3. Run the following command in the root of the project to start the Tailwind CSS compiler:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tailwindcss -i ./input.css -o ./assets/tailwind.css --watch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dioxus CLI
|
||||||
|
|
||||||
|
#### Install the stable version (recommended)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo install dioxus-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
### Serving Your App
|
||||||
|
|
||||||
|
Run the following command in the root of your project to start developing with the default platform:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dx serve
|
||||||
|
```
|
||||||
|
|
||||||
|
To run for a different platform, use the `--platform platform` flag. E.g.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dx serve --platform desktop
|
||||||
|
```
|
||||||
|
|
||||||
BIN
cli/rustfs-gui/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
cli/rustfs-gui/assets/icon.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
cli/rustfs-gui/assets/icons/icon-all.icns
Normal file
BIN
cli/rustfs-gui/assets/icons/icon-all.ico
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
cli/rustfs-gui/assets/icons/icon.icns
Normal file
BIN
cli/rustfs-gui/assets/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
cli/rustfs-gui/assets/icons/icon.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
cli/rustfs-gui/assets/icons/icon_128x128.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
cli/rustfs-gui/assets/icons/icon_128x128@2x.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
cli/rustfs-gui/assets/icons/icon_16x16.png
Normal file
|
After Width: | Height: | Size: 498 B |
BIN
cli/rustfs-gui/assets/icons/icon_16x16@2x.png
Normal file
|
After Width: | Height: | Size: 969 B |
BIN
cli/rustfs-gui/assets/icons/icon_256x256.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
cli/rustfs-gui/assets/icons/icon_256x256@2x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
cli/rustfs-gui/assets/icons/icon_32x32.png
Normal file
|
After Width: | Height: | Size: 969 B |
BIN
cli/rustfs-gui/assets/icons/icon_32x32@2x.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
cli/rustfs-gui/assets/icons/icon_512x512.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
cli/rustfs-gui/assets/icons/icon_512x512@2x.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
cli/rustfs-gui/assets/icons/rustfs-icon.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
48
cli/rustfs-gui/assets/js/sts.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 RustFS Team
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
window.switchTab = function (tabId) {
|
||||||
|
// Hide everything
|
||||||
|
document.querySelectorAll('.tab-content').forEach(content => {
|
||||||
|
content.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset all label styles
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||||
|
btn.classList.remove('border-b-2', 'border-black');
|
||||||
|
btn.classList.add('text-gray-500');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Displays the selected content
|
||||||
|
const activeContent = document.getElementById(tabId);
|
||||||
|
if (activeContent) {
|
||||||
|
activeContent.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the selected label style
|
||||||
|
const activeBtn = document.querySelector(`[data-tab="${tabId}"]`);
|
||||||
|
if (activeBtn) {
|
||||||
|
activeBtn.classList.add('border-b-2', 'border-black');
|
||||||
|
activeBtn.classList.remove('text-gray-500');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.togglePassword = function (button) {
|
||||||
|
const input = button.parentElement.querySelector('input[type="password"], input[type="text"]');
|
||||||
|
if (input) {
|
||||||
|
input.type = input.type === 'password' ? 'text' : 'password';
|
||||||
|
}
|
||||||
|
};
|
||||||
BIN
cli/rustfs-gui/assets/rustfs-icon.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
cli/rustfs-gui/assets/rustfs-logo-square.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
15
cli/rustfs-gui/assets/rustfs-logo.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<svg width="1558" height="260" viewBox="0 0 1558 260" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_0_3)">
|
||||||
|
<path d="M1288.5 112.905H1159.75V58.4404H1262L1270 0L1074 0V260H1159.75V162.997H1296.95L1288.5 112.905Z" fill="#0196D0"/>
|
||||||
|
<path d="M1058.62 58.4404V0H789V58.4404H881.133V260H966.885V58.4404H1058.62Z" fill="#0196D0"/>
|
||||||
|
<path d="M521 179.102V0L454.973 15V161C454.973 181.124 452.084 193.146 443.5 202C434.916 211.257 419.318 214.5 400.5 214.5C381.022 214.5 366.744 210.854 357.5 202C348.916 193.548 346.357 175.721 346.357 156V0L280 15V175.48C280 208.08 290.234 229.412 309.712 241.486C329.19 253.56 358.903 260 400.5 260C440.447 260 470.159 253.56 490.297 241.486C510.766 229.412 521 208.483 521 179.102Z" fill="#0196D0"/>
|
||||||
|
<path d="M172.84 84.2813C172.84 97.7982 168.249 107.737 158.41 113.303C149.883 118.471 137.092 121.254 120.693 122.049V162.997C129.876 163.792 138.076 166.177 144.307 176.514L184.647 260H265L225.316 180.489C213.181 155.046 201.374 149.48 178.744 143.517C212.197 138.349 241.386 118.471 241.386 73.1499C241.386 53.2722 233.843 30.2141 218.756 17.8899C203.998 5.56575 183.991 0 159.394 0H120.693V48.5015H127.58C142.23 48.5015 153.6 51.4169 161.689 57.2477C169.233 62.8135 172.84 71.5596 172.84 84.2813ZM120.693 122.049C119.163 122.049 117.741 122.049 116.43 122.049H68.5457V48.5015H120.693V0H0V260H70.5137V162.997H110.526C113.806 162.997 117.741 162.997 120.693 162.997V122.049Z" fill="#0196D0"/>
|
||||||
|
<path d="M774 179.297C774 160.829 766.671 144.669 752.013 131.972C738.127 119.66 712.025 110.169 673.708 103.5C662.136 101.191 651.722 99.6523 643.235 97.3437C586.532 84.6467 594.632 52.7118 650.564 52.7118C680.651 52.7118 709.582 61.946 738.127 66.9478C742.37 67.7174 743.913 68.1021 744.298 68.1021L750.47 12.697C720.383 3.46282 684.895 0 654.036 0C616.619 0 587.689 6.54088 567.245 19.2379C546.801 31.9349 536 57.7137 536 82.3382C536 103.5 543.715 119.66 559.916 131.972C575.731 143.515 604.276 152.749 645.55 160.059C658.279 162.368 668.694 163.907 676.794 166.215C685.023 168.524 691.066 170.704 694.924 172.756C702.253 176.604 706.11 182.375 706.11 188.531C706.11 196.611 701.481 202.767 692.224 207C664.836 220.081 587.689 212.001 556.83 198.15L543.715 247.784C547.186 248.169 552.972 249.323 559.916 250.477C616.619 259.327 690.681 270.869 741.212 238.935C762.814 225.468 774 206.23 774 179.297Z" fill="#0196D0"/>
|
||||||
|
<path d="M1558 179.568C1558 160.383 1550.42 144.268 1535.67 131.99C1521.32 119.968 1494.34 110.631 1454.74 103.981C1442.38 101.679 1432.01 99.3764 1422.84 97.8416C1422.44 97.8416 1422.04 97.8416 1422.04 97.4579V112.422L1361.04 75.2038L1422.04 38.3692V52.9496C1424.7 52.9496 1427.49 52.9496 1430.41 52.9496C1461.51 52.9496 1491.42 62.5419 1521.32 67.5299C1525.31 67.9136 1526.9 67.9136 1527.3 67.9136L1533.68 12.6619C1502.98 3.83692 1465.9 0 1434 0C1395.33 0 1365.43 6.52277 1345.09 19.5683C1323.16 32.6139 1312 57.9376 1312 82.8776C1312 103.981 1320.37 120.096 1336.72 131.607C1353.46 143.885 1382.97 153.093 1425.23 160.383C1434 161.535 1441.18 162.686 1447.56 164.22L1448.36 150.791L1507.36 190.312L1445.57 224.844L1445.96 212.949C1409.68 215.635 1357.45 209.112 1333.53 197.985L1320.37 247.482C1323.56 248.249 1329.54 248.633 1336.72 250.551C1395.33 259.376 1471.88 270.887 1524.11 238.657C1546.84 225.611 1558 205.659 1558 179.568Z" fill="#0196D0"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_0_3">
|
||||||
|
<rect width="1558" height="260" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
33
cli/rustfs-gui/assets/styling/navbar.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 RustFS Team
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#navbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navbar a {
|
||||||
|
color: #ffffff;
|
||||||
|
margin-right: 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navbar a:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #ffffff;
|
||||||
|
/ / #91a4d2;
|
||||||
|
}
|
||||||
972
cli/rustfs-gui/assets/tailwind.css
Normal file
@@ -0,0 +1,972 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 RustFS Team
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
*, ::before, ::after {
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style: ;
|
||||||
|
}
|
||||||
|
|
||||||
|
::backdrop {
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style: ;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||||
|
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||||
|
*/
|
||||||
|
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 1 */
|
||||||
|
border-width: 0;
|
||||||
|
/* 2 */
|
||||||
|
border-style: solid;
|
||||||
|
/* 2 */
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
--tw-content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use a consistent sensible line-height in all browsers.
|
||||||
|
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
3. Use a more readable tab size.
|
||||||
|
4. Use the user's configured `sans` font-family by default.
|
||||||
|
5. Use the user's configured `sans` font-feature-settings by default.
|
||||||
|
6. Use the user's configured `sans` font-variation-settings by default.
|
||||||
|
7. Disable tap highlights on iOS
|
||||||
|
*/
|
||||||
|
|
||||||
|
html,
|
||||||
|
:host {
|
||||||
|
line-height: 1.5;
|
||||||
|
/* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
/* 2 */
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
/* 3 */
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
/* 3 */
|
||||||
|
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
/* 4 */
|
||||||
|
font-feature-settings: normal;
|
||||||
|
/* 5 */
|
||||||
|
font-variation-settings: normal;
|
||||||
|
/* 6 */
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
/* 7 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Remove the margin in all browsers.
|
||||||
|
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
/* 1 */
|
||||||
|
line-height: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Add the correct height in Firefox.
|
||||||
|
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||||
|
3. Ensure horizontal rules are visible by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 0;
|
||||||
|
/* 1 */
|
||||||
|
color: inherit;
|
||||||
|
/* 2 */
|
||||||
|
border-top-width: 1px;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr:where([title]) {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the default font size and weight for headings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Reset links to optimize for opt-in styling instead of opt-out.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct font weight in Edge and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use the user's configured `mono` font-family by default.
|
||||||
|
2. Use the user's configured `mono` font-feature-settings by default.
|
||||||
|
3. Use the user's configured `mono` font-variation-settings by default.
|
||||||
|
4. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp,
|
||||||
|
pre {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
/* 1 */
|
||||||
|
font-feature-settings: normal;
|
||||||
|
/* 2 */
|
||||||
|
font-variation-settings: normal;
|
||||||
|
/* 3 */
|
||||||
|
font-size: 1em;
|
||||||
|
/* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||||
|
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||||
|
3. Remove gaps between table borders by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
text-indent: 0;
|
||||||
|
/* 1 */
|
||||||
|
border-color: inherit;
|
||||||
|
/* 2 */
|
||||||
|
border-collapse: collapse;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Change the font styles in all browsers.
|
||||||
|
2. Remove the margin in Firefox and Safari.
|
||||||
|
3. Remove default padding in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-feature-settings: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-variation-settings: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 100%;
|
||||||
|
/* 1 */
|
||||||
|
font-weight: inherit;
|
||||||
|
/* 1 */
|
||||||
|
line-height: inherit;
|
||||||
|
/* 1 */
|
||||||
|
letter-spacing: inherit;
|
||||||
|
/* 1 */
|
||||||
|
color: inherit;
|
||||||
|
/* 1 */
|
||||||
|
margin: 0;
|
||||||
|
/* 2 */
|
||||||
|
padding: 0;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the inheritance of text transform in Edge and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
2. Remove default button styles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input:where([type='button']),
|
||||||
|
input:where([type='reset']),
|
||||||
|
input:where([type='submit']) {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
background-color: transparent;
|
||||||
|
/* 2 */
|
||||||
|
background-image: none;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Use the modern Firefox focus style for all focusable elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-focusring {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-ui-invalid {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct vertical alignment in Chrome and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Correct the cursor style of increment and decrement buttons in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button,
|
||||||
|
::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type='search'] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
/* 1 */
|
||||||
|
outline-offset: -2px;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
font: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct display in Chrome and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Removes the default spacing and border for appropriate elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
dl,
|
||||||
|
dd,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
hr,
|
||||||
|
figure,
|
||||||
|
p,
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
menu {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Reset default styling for dialogs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent resizing textareas horizontally by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||||
|
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
/* 1 */
|
||||||
|
color: #9ca3af;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
/* 1 */
|
||||||
|
color: #9ca3af;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set the default cursor for buttons.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Make sure disabled buttons don't get the pointer cursor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||||
|
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
audio,
|
||||||
|
iframe,
|
||||||
|
embed,
|
||||||
|
object {
|
||||||
|
display: block;
|
||||||
|
/* 1 */
|
||||||
|
vertical-align: middle;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
*/
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||||
|
|
||||||
|
[hidden]:where(:not([hidden="until-found"])) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.static {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-2 {
|
||||||
|
right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-6 {
|
||||||
|
right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-1\/2 {
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-4 {
|
||||||
|
top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z-10 {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-6 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-8 {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ml-2 {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-16 {
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-24 {
|
||||||
|
height: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-4 {
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-5 {
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-6 {
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-h-screen {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-16 {
|
||||||
|
width: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-20 {
|
||||||
|
width: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-24 {
|
||||||
|
width: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-4 {
|
||||||
|
width: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-48 {
|
||||||
|
width: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-5 {
|
||||||
|
width: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-6 {
|
||||||
|
width: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1 1 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.-translate-y-1\/2 {
|
||||||
|
--tw-translate-y: -50%;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
.transform {
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-x-reverse: 0;
|
||||||
|
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||||
|
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-x-4 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-x-reverse: 0;
|
||||||
|
margin-right: calc(1rem * var(--tw-space-x-reverse));
|
||||||
|
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-x-8 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-x-reverse: 0;
|
||||||
|
margin-right: calc(2rem * var(--tw-space-x-reverse));
|
||||||
|
margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-y-4 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-y-6 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-full {
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-lg {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-md {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border {
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-b {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-b-2 {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-black {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(0 0 0 / var(--tw-border-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-gray-200 {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(229 231 235 / var(--tw-border-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-\[\#111827\] {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gray-100 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gray-900 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-red-500 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-white {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-2 {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-4 {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-8 {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-1 {
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-3 {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-4 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.py-0\.5 {
|
||||||
|
padding-top: 0.125rem;
|
||||||
|
padding-bottom: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.py-2 {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.py-4 {
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.py-6 {
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr-10 {
|
||||||
|
padding-right: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-2xl {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-base {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-medium {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-blue-500 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(59 130 246 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-blue-600 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-400 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-500 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(107 114 128 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-600 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-white {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-25 {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.opacity-75 {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-\[\#1f2937\]:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-gray-100:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-red-600:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:text-gray-700:hover {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(55 65 81 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:text-gray-900:hover {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus\:outline-none:focus {
|
||||||
|
outline: 2px solid transparent;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus\:ring-2:focus {
|
||||||
|
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||||
|
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus\:ring-blue-500:focus {
|
||||||
|
--tw-ring-opacity: 1;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
|
||||||
|
}
|
||||||
1
cli/rustfs-gui/embedded-rustfs/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rustfs bin path, do not delete
|
||||||
19
cli/rustfs-gui/input.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 RustFS Team
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
330
cli/rustfs-gui/src/components/home.rs
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::components::navbar::LoadingSpinner;
|
||||||
|
use crate::route::Route;
|
||||||
|
use crate::utils::{RustFSConfig, ServiceManager};
|
||||||
|
use chrono::Datelike;
|
||||||
|
use dioxus::logger::tracing::debug;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const HEADER_LOGO: Asset = asset!("/assets/rustfs-logo.svg");
|
||||||
|
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
||||||
|
|
||||||
|
/// Define the state of the service
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
enum ServiceState {
|
||||||
|
Start,
|
||||||
|
Stop,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Define the Home component
|
||||||
|
/// The Home component is the main component of the application
|
||||||
|
/// It is responsible for starting and stopping the service
|
||||||
|
/// It also displays the service status and provides a button to toggle the service
|
||||||
|
/// The Home component also displays the footer of the application
|
||||||
|
/// The footer contains links to the official site, documentation, GitHub, and license
|
||||||
|
/// The footer also displays the version of the application
|
||||||
|
/// The Home component also contains a button to change the theme of the application
|
||||||
|
/// The Home component also contains a button to go to the settings page
|
||||||
|
#[component]
|
||||||
|
pub fn Home() -> Element {
|
||||||
|
#[allow(clippy::redundant_closure)]
|
||||||
|
let service = use_signal(|| ServiceManager::new());
|
||||||
|
let conf = RustFSConfig::load().unwrap_or_else(|e| {
|
||||||
|
ServiceManager::show_error(&format!("load config failed: {e}"));
|
||||||
|
RustFSConfig::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
debug!("loaded configurations: {:?}", conf);
|
||||||
|
let config = use_signal(|| conf.clone());
|
||||||
|
|
||||||
|
use dioxus_router::prelude::Link;
|
||||||
|
use document::{Meta, Stylesheet, Title};
|
||||||
|
let mut service_state = use_signal(|| ServiceState::Start);
|
||||||
|
// Create a periodic check on the effect of the service status
|
||||||
|
use_effect(move || {
|
||||||
|
spawn(async move {
|
||||||
|
loop {
|
||||||
|
if let Some(pid) = ServiceManager::check_service_status().await {
|
||||||
|
debug!("service_running true pid: {:?}", pid);
|
||||||
|
service_state.set(ServiceState::Stop);
|
||||||
|
} else {
|
||||||
|
debug!("service_running true pid: 0");
|
||||||
|
service_state.set(ServiceState::Start);
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
debug!("project start service_state: {:?}", service_state.read());
|
||||||
|
// Use 'use_signal' to manage service status
|
||||||
|
let mut loading = use_signal(|| false);
|
||||||
|
let mut start_service = move |_| {
|
||||||
|
let service = service;
|
||||||
|
let config = config.read().clone();
|
||||||
|
let mut service_state = service_state;
|
||||||
|
// set the loading status
|
||||||
|
loading.set(true);
|
||||||
|
debug!("stop loading_state: {:?}", loading.read());
|
||||||
|
spawn(async move {
|
||||||
|
match service.read().start(config).await {
|
||||||
|
Ok(result) => {
|
||||||
|
if result.success {
|
||||||
|
let duration = result.end_time - result.start_time;
|
||||||
|
debug!("The service starts successfully and takes a long time:{}ms", duration.num_milliseconds());
|
||||||
|
service_state.set(ServiceState::Stop);
|
||||||
|
} else {
|
||||||
|
ServiceManager::show_error(&result.message);
|
||||||
|
service_state.set(ServiceState::Start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
ServiceManager::show_error(&format!("start service failed: {e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only set loading to false when it's actually done
|
||||||
|
loading.set(false);
|
||||||
|
debug!("start loading_state: {:?}", loading.read());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut stop_service = move |_| {
|
||||||
|
let service = service;
|
||||||
|
let mut service_state = service_state;
|
||||||
|
// set the loading status
|
||||||
|
loading.set(true);
|
||||||
|
spawn(async move {
|
||||||
|
match service.read().stop().await {
|
||||||
|
Ok(result) => {
|
||||||
|
if result.success {
|
||||||
|
let duration = result.end_time - result.start_time;
|
||||||
|
debug!("The service stops successfully and takes a long time:{}ms", duration.num_milliseconds());
|
||||||
|
service_state.set(ServiceState::Start);
|
||||||
|
} else {
|
||||||
|
ServiceManager::show_error(&result.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
ServiceManager::show_error(&format!("stop service failed: {e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("service_state: {:?}", service_state.read());
|
||||||
|
// Only set loading to false when it's actually done
|
||||||
|
loading.set(false);
|
||||||
|
debug!("stop loading_state: {:?}", loading.read());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle the state when the button is clicked
|
||||||
|
let toggle_service = {
|
||||||
|
let mut service_state = service_state;
|
||||||
|
debug!("toggle_service service_state: {:?}", service_state.read());
|
||||||
|
move |_| {
|
||||||
|
if service_state.read().eq(&ServiceState::Stop) {
|
||||||
|
// If the service status is started, you need to run a command to stop the service
|
||||||
|
stop_service(());
|
||||||
|
service_state.set(ServiceState::Start);
|
||||||
|
} else {
|
||||||
|
start_service(());
|
||||||
|
service_state.set(ServiceState::Stop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define dynamic styles based on state
|
||||||
|
let button_class = if service_state.read().eq(&ServiceState::Start) {
|
||||||
|
"bg-[#111827] hover:bg-[#1f2937] text-white px-4 py-2 rounded-md flex items-center space-x-2"
|
||||||
|
} else {
|
||||||
|
"bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md flex items-center space-x-2"
|
||||||
|
};
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
// The Stylesheet component inserts a style link into the head of the document
|
||||||
|
Stylesheet {href: TAILWIND_CSS,}
|
||||||
|
Title { "RustFS APP" }
|
||||||
|
Meta {
|
||||||
|
name: "description",
|
||||||
|
// TODO: translate to english
|
||||||
|
content: "RustFS RustFS 用热门安全的 Rust 语言开发,兼容 S3 协议。适用于 AI/ML 及海量数据存储、大数据、互联网、工业和保密存储等全部场景。近乎免费使用。遵循 Apache 2 协议,支持国产保密设备和系统。",
|
||||||
|
}
|
||||||
|
div { class: "min-h-screen flex flex-col items-center bg-white",
|
||||||
|
div { class: "absolute top-4 right-6 flex space-x-2",
|
||||||
|
// change theme
|
||||||
|
button { class: "p-2 hover:bg-gray-100 rounded-lg", ChangeThemeButton {} }
|
||||||
|
// setting button
|
||||||
|
Link {
|
||||||
|
class: "p-2 hover:bg-gray-100 rounded-lg",
|
||||||
|
to: Route::SettingViews {},
|
||||||
|
SettingButton {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main { class: "flex-1 flex flex-col items-center justify-center space-y-6 p-4",
|
||||||
|
div { class: "w-24 h-24 bg-gray-900 rounded-full flex items-center justify-center",
|
||||||
|
img { alt: "Logo", class: "w-16 h-16", src: HEADER_LOGO }
|
||||||
|
}
|
||||||
|
div { class: "text-gray-600",
|
||||||
|
"Service is running on "
|
||||||
|
span { class: "text-blue-600", " 127.0.0.1:9000 " }
|
||||||
|
}
|
||||||
|
LoadingSpinner {
|
||||||
|
loading: loading.read().to_owned(),
|
||||||
|
text: "processing...",
|
||||||
|
}
|
||||||
|
button { class: button_class, onclick: toggle_service,
|
||||||
|
svg {
|
||||||
|
class: "h-4 w-4",
|
||||||
|
fill: "none",
|
||||||
|
stroke: "currentColor",
|
||||||
|
view_box: "0 0 24 24",
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
if service_state.read().eq(&ServiceState::Start) {
|
||||||
|
path {
|
||||||
|
d: "M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z",
|
||||||
|
stroke_linecap: "round",
|
||||||
|
stroke_linejoin: "round",
|
||||||
|
stroke_width: "2",
|
||||||
|
}
|
||||||
|
path {
|
||||||
|
d: "M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
|
||||||
|
stroke_linecap: "round",
|
||||||
|
stroke_linejoin: "round",
|
||||||
|
stroke_width: "2",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path {
|
||||||
|
stroke_linecap: "round",
|
||||||
|
stroke_linejoin: "round",
|
||||||
|
stroke_width: "2",
|
||||||
|
d: "M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
|
||||||
|
}
|
||||||
|
path {
|
||||||
|
stroke_linecap: "round",
|
||||||
|
stroke_linejoin: "round",
|
||||||
|
stroke_width: "2",
|
||||||
|
d: "M9 10h6v4H9z",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span { id: "serviceStatus",
|
||||||
|
if service_state.read().eq(&ServiceState::Start) {
|
||||||
|
"Start service"
|
||||||
|
} else {
|
||||||
|
"Stop service"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Footer { version: "v1.0.0".to_string() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Footer(version: String) -> Element {
|
||||||
|
let now = chrono::Local::now();
|
||||||
|
let year = now.naive_local().year();
|
||||||
|
rsx! {
|
||||||
|
footer { class: "w-full py-6 flex flex-col items-center space-y-4 mb-6",
|
||||||
|
nav { class: "flex space-x-4 text-gray-600",
|
||||||
|
a { class: "hover:text-gray-900", href: "https://rustfs.com", "Official Site" }
|
||||||
|
a {
|
||||||
|
class: "hover:text-gray-900",
|
||||||
|
href: "https://rustfs.com/docs",
|
||||||
|
"Documentation"
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
class: "hover:text-gray-900",
|
||||||
|
href: "https://github.com/rustfs/rustfs",
|
||||||
|
"GitHub"
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
class: "hover:text-gray-900",
|
||||||
|
href: "https://rustfs.com/docs/license/",
|
||||||
|
"License"
|
||||||
|
}
|
||||||
|
a { class: "hover:text-gray-900", href: "#", "Sponsors" }
|
||||||
|
}
|
||||||
|
div { class: "text-gray-500 text-sm", " © rustfs.com {year}, All rights reserved." }
|
||||||
|
div { class: "text-gray-400 text-sm mb-8", " version {version} " }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn GoBackButtons() -> Element {
|
||||||
|
rsx! {
|
||||||
|
button {
|
||||||
|
class: "p-2 hover:bg-gray-100 rounded-lg",
|
||||||
|
"onclick": "window.history.back()",
|
||||||
|
"Back to the Past"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn GoForwardButtons() -> Element {
|
||||||
|
rsx! {
|
||||||
|
button {
|
||||||
|
class: "p-2 hover:bg-gray-100 rounded-lg",
|
||||||
|
"onclick": "window.history.forward()",
|
||||||
|
"Back to the Future"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ChangeThemeButton() -> Element {
|
||||||
|
rsx! {
|
||||||
|
svg {
|
||||||
|
class: "h-6 w-6 text-gray-600",
|
||||||
|
fill: "none",
|
||||||
|
stroke: "currentColor",
|
||||||
|
view_box: "0 0 24 24",
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
path {
|
||||||
|
d: "M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z",
|
||||||
|
stroke_linecap: "round",
|
||||||
|
stroke_linejoin: "round",
|
||||||
|
stroke_width: "2",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SettingButton() -> Element {
|
||||||
|
rsx! {
|
||||||
|
svg {
|
||||||
|
class: "h-6 w-6 text-gray-600",
|
||||||
|
fill: "none",
|
||||||
|
stroke: "currentColor",
|
||||||
|
view_box: "0 0 24 24",
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
path {
|
||||||
|
d: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z",
|
||||||
|
stroke_linecap: "round",
|
||||||
|
stroke_linejoin: "round",
|
||||||
|
stroke_width: "2",
|
||||||
|
}
|
||||||
|
path {
|
||||||
|
d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z",
|
||||||
|
stroke_linecap: "round",
|
||||||
|
stroke_linejoin: "round",
|
||||||
|
stroke_width: "2",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
cli/rustfs-gui/src/components/mod.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod home;
|
||||||
|
pub use home::Home;
|
||||||
|
mod navbar;
|
||||||
|
pub use navbar::Navbar;
|
||||||
|
mod setting;
|
||||||
|
pub use setting::Setting;
|
||||||
74
cli/rustfs-gui/src/components/navbar.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::route::Route;
|
||||||
|
use dioxus::logger::tracing::debug;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css");
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Navbar() -> Element {
|
||||||
|
rsx! {
|
||||||
|
document::Link { rel: "stylesheet", href: NAVBAR_CSS }
|
||||||
|
|
||||||
|
div { id: "navbar", class: "hidden", style: "display: none;",
|
||||||
|
Link { to: Route::HomeViews {}, "Home" }
|
||||||
|
Link { to: Route::SettingViews {}, "Setting" }
|
||||||
|
}
|
||||||
|
|
||||||
|
Outlet::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props, PartialEq, Debug, Clone)]
|
||||||
|
pub struct LoadingSpinnerProps {
|
||||||
|
#[props(default = true)]
|
||||||
|
loading: bool,
|
||||||
|
#[props(default = "正在处理中...")]
|
||||||
|
text: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn LoadingSpinner(props: LoadingSpinnerProps) -> Element {
|
||||||
|
debug!("loading: {}", props.loading);
|
||||||
|
if !props.loading {
|
||||||
|
debug!("LoadingSpinner false loading: {}", props.loading);
|
||||||
|
return rsx! {};
|
||||||
|
}
|
||||||
|
rsx! {
|
||||||
|
div { class: "flex items-center justify-center z-10",
|
||||||
|
svg {
|
||||||
|
class: "animate-spin h-5 w-5 text-blue-500",
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
fill: "none",
|
||||||
|
view_box: "0 0 24 24",
|
||||||
|
circle {
|
||||||
|
class: "opacity-25",
|
||||||
|
cx: "12",
|
||||||
|
cy: "12",
|
||||||
|
r: "10",
|
||||||
|
stroke: "currentColor",
|
||||||
|
stroke_width: "4",
|
||||||
|
}
|
||||||
|
path {
|
||||||
|
class: "opacity-75",
|
||||||
|
fill: "currentColor",
|
||||||
|
d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span { class: "ml-2 text-gray-600", "{props.text}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
216
cli/rustfs-gui/src/components/setting.rs
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::components::navbar::LoadingSpinner;
|
||||||
|
use dioxus::logger::tracing::{debug, error};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
const SETTINGS_JS: Asset = asset!("/assets/js/sts.js");
|
||||||
|
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
||||||
|
#[component]
|
||||||
|
pub fn Setting() -> Element {
|
||||||
|
use crate::utils::{RustFSConfig, ServiceManager};
|
||||||
|
use document::{Meta, Script, Stylesheet, Title};
|
||||||
|
|
||||||
|
#[allow(clippy::redundant_closure)]
|
||||||
|
let service = use_signal(|| ServiceManager::new());
|
||||||
|
let conf = RustFSConfig::load().unwrap_or_else(|e| {
|
||||||
|
error!("load config error: {}", e);
|
||||||
|
RustFSConfig::default_config()
|
||||||
|
});
|
||||||
|
debug!("conf address: {:?}", conf.clone().address);
|
||||||
|
|
||||||
|
let config = use_signal(|| conf.clone());
|
||||||
|
let address_state = use_signal(|| conf.address.to_string());
|
||||||
|
let mut host_state = use_signal(|| conf.host.to_string());
|
||||||
|
let mut port_state = use_signal(|| conf.port.to_string());
|
||||||
|
let mut access_key_state = use_signal(|| conf.access_key.to_string());
|
||||||
|
let mut secret_key_state = use_signal(|| conf.secret_key.to_string());
|
||||||
|
let mut volume_name_state = use_signal(|| conf.volume_name.to_string());
|
||||||
|
let loading = use_signal(|| false);
|
||||||
|
|
||||||
|
let save_and_restart = {
|
||||||
|
let host_state = host_state;
|
||||||
|
let port_state = port_state;
|
||||||
|
let access_key_state = access_key_state;
|
||||||
|
let secret_key_state = secret_key_state;
|
||||||
|
let volume_name_state = volume_name_state;
|
||||||
|
let mut loading = loading;
|
||||||
|
debug!("save_and_restart access_key:{}", access_key_state.read());
|
||||||
|
move |_| {
|
||||||
|
// set the loading status
|
||||||
|
loading.set(true);
|
||||||
|
let mut config = config;
|
||||||
|
config.write().address = format!("{}:{}", host_state.read(), port_state.read());
|
||||||
|
config.write().host = host_state.read().to_string();
|
||||||
|
config.write().port = port_state.read().to_string();
|
||||||
|
config.write().access_key = access_key_state.read().to_string();
|
||||||
|
config.write().secret_key = secret_key_state.read().to_string();
|
||||||
|
config.write().volume_name = volume_name_state.read().to_string();
|
||||||
|
// restart service
|
||||||
|
let service = service;
|
||||||
|
let config = config.read().clone();
|
||||||
|
spawn(async move {
|
||||||
|
if let Err(e) = service.read().restart(config).await {
|
||||||
|
ServiceManager::show_error(&format!("发送重启命令失败:{e}"));
|
||||||
|
}
|
||||||
|
// reset the status when you're done
|
||||||
|
loading.set(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
Title { "Settings - RustFS App" }
|
||||||
|
Meta { name: "description", content: "Settings - RustFS App." }
|
||||||
|
// The Stylesheet component inserts a style link into the head of the document
|
||||||
|
Stylesheet { href: TAILWIND_CSS }
|
||||||
|
Script { src: SETTINGS_JS }
|
||||||
|
div { class: "bg-white p-8",
|
||||||
|
h1 { class: "text-2xl font-semibold mb-6", "Settings" }
|
||||||
|
div { class: "border-b border-gray-200 mb-6",
|
||||||
|
nav { class: "flex space-x-8",
|
||||||
|
button {
|
||||||
|
class: "tab-btn px-1 py-4 text-sm font-medium border-b-2 border-black",
|
||||||
|
"data-tab": "service",
|
||||||
|
"onclick": "switchTab('service')",
|
||||||
|
"Service "
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
class: "tab-btn px-1 py-4 text-sm font-medium text-gray-500 hover:text-gray-700",
|
||||||
|
"data-tab": "user",
|
||||||
|
"onclick": "switchTab('user')",
|
||||||
|
"User "
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
class: "tab-btn px-1 py-4 text-sm font-medium text-gray-500 hover:text-gray-700 hidden",
|
||||||
|
"data-tab": "logs",
|
||||||
|
"onclick": "switchTab('logs')",
|
||||||
|
"Logs "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div { id: "tabContent",
|
||||||
|
div { class: "tab-content", id: "service",
|
||||||
|
div { class: "mb-8",
|
||||||
|
h2 { class: "text-base font-medium mb-2", "Service address" }
|
||||||
|
p { class: "text-gray-600 mb-4",
|
||||||
|
" The service address is the IP address and port number of the service. the default address is "
|
||||||
|
code { class: "bg-gray-100 px-1 py-0.5 rounded", {address_state} }
|
||||||
|
". "
|
||||||
|
}
|
||||||
|
div { class: "flex space-x-2",
|
||||||
|
input {
|
||||||
|
class: "border rounded px-3 py-2 w-48 focus:outline-none focus:ring-2 focus:ring-blue-500",
|
||||||
|
r#type: "text",
|
||||||
|
value: host_state,
|
||||||
|
oninput: move |evt| host_state.set(evt.value().clone()),
|
||||||
|
}
|
||||||
|
span { class: "flex items-center", ":" }
|
||||||
|
input {
|
||||||
|
class: "border rounded px-3 py-2 w-20 focus:outline-none focus:ring-2 focus:ring-blue-500",
|
||||||
|
r#type: "text",
|
||||||
|
value: port_state,
|
||||||
|
oninput: move |evt| port_state.set(evt.value().clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div { class: "mb-8",
|
||||||
|
h2 { class: "text-base font-medium mb-2", "Storage path" }
|
||||||
|
p { class: "text-gray-600 mb-4",
|
||||||
|
"Update the storage path of the service. the default path is {volume_name_state}."
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
class: "border rounded px-3 py-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500",
|
||||||
|
r#type: "text",
|
||||||
|
value: volume_name_state,
|
||||||
|
oninput: move |evt| volume_name_state.set(evt.value().clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div { class: "tab-content hidden", id: "user",
|
||||||
|
div { class: "mb-8",
|
||||||
|
h2 { class: "text-base font-medium mb-2", "User" }
|
||||||
|
p { class: "text-gray-600 mb-4",
|
||||||
|
"The user is the owner of the service. the default user is "
|
||||||
|
code { class: "bg-gray-100 px-1 py-0.5 rounded", {access_key_state} }
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
class: "border rounded px-3 py-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500",
|
||||||
|
r#type: "text",
|
||||||
|
value: access_key_state,
|
||||||
|
oninput: move |evt| access_key_state.set(evt.value().clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div { class: "mb-8",
|
||||||
|
h2 { class: "text-base font-medium mb-2", "Password" }
|
||||||
|
p { class: "text-gray-600 mb-4",
|
||||||
|
"The password is the password of the user. the default password is "
|
||||||
|
code { class: "bg-gray-100 px-1 py-0.5 rounded", {secret_key_state} }
|
||||||
|
}
|
||||||
|
div { class: "relative",
|
||||||
|
input {
|
||||||
|
class: "border rounded px-3 py-2 w-full pr-10 focus:outline-none focus:ring-2 focus:ring-blue-500",
|
||||||
|
r#type: "password",
|
||||||
|
value: secret_key_state,
|
||||||
|
oninput: move |evt| secret_key_state.set(evt.value().clone()),
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
class: "absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700",
|
||||||
|
"onclick": "togglePassword(this)",
|
||||||
|
svg {
|
||||||
|
class: "h-5 w-5",
|
||||||
|
fill: "currentColor",
|
||||||
|
view_box: "0 0 20 20",
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
path { d: "M10 12a2 2 0 100-4 2 2 0 000 4z" }
|
||||||
|
path {
|
||||||
|
clip_rule: "evenodd",
|
||||||
|
d: "M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z",
|
||||||
|
fill_rule: "evenodd",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div { class: "tab-content hidden", id: "logs",
|
||||||
|
div { class: "mb-8",
|
||||||
|
h2 { class: "text-base font-medium mb-2", "Logs storage path" }
|
||||||
|
p { class: "text-gray-600 mb-4",
|
||||||
|
"The logs storage path is the path where the logs are stored. the default path is /var/log/rustfs. "
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
class: "border rounded px-3 py-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500",
|
||||||
|
r#type: "text",
|
||||||
|
value: "/var/logs/rustfs",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div { class: "flex space-x-4",
|
||||||
|
button {
|
||||||
|
class: "bg-[#111827] text-white px-4 py-2 rounded hover:bg-[#1f2937]",
|
||||||
|
onclick: save_and_restart,
|
||||||
|
" Save and restart "
|
||||||
|
}
|
||||||
|
GoBackButton { "Back" }
|
||||||
|
}
|
||||||
|
LoadingSpinner {
|
||||||
|
loading: loading.read().to_owned(),
|
||||||
|
text: "服务处理中...",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
cli/rustfs-gui/src/main.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod components;
|
||||||
|
mod route;
|
||||||
|
mod utils;
|
||||||
|
mod views;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let _worker_guard = utils::init_logger();
|
||||||
|
dioxus::launch(views::App);
|
||||||
|
}
|
||||||
17
cli/rustfs-gui/src/route/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod router;
|
||||||
|
|
||||||
|
pub use router::Route;
|
||||||
28
cli/rustfs-gui/src/route/router.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::components::Navbar;
|
||||||
|
use crate::views::{HomeViews, SettingViews};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
/// The router for the application
|
||||||
|
#[derive(Debug, Clone, Routable, PartialEq)]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub enum Route {
|
||||||
|
#[layout(Navbar)]
|
||||||
|
#[route("/")]
|
||||||
|
HomeViews {},
|
||||||
|
#[route("/settings")]
|
||||||
|
SettingViews {},
|
||||||
|
}
|
||||||
564
cli/rustfs-gui/src/utils/config.rs
Normal file
@@ -0,0 +1,564 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use keyring::Entry;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
/// Configuration for the RustFS service
|
||||||
|
///
|
||||||
|
/// # Fields
|
||||||
|
/// * `address` - The address of the RustFS service
|
||||||
|
/// * `host` - The host of the RustFS service
|
||||||
|
/// * `port` - The port of the RustFS service
|
||||||
|
/// * `access_key` - The access key of the RustFS service
|
||||||
|
/// * `secret_key` - The secret key of the RustFS service
|
||||||
|
/// * `domain_name` - The domain name of the RustFS service
|
||||||
|
/// * `volume_name` - The volume name of the RustFS service
|
||||||
|
/// * `console_address` - The console address of the RustFS service
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let config = RustFSConfig {
|
||||||
|
/// address: "127.0.0.1:9000".to_string(),
|
||||||
|
/// host: "127.0.0.1".to_string(),
|
||||||
|
/// port: "9000".to_string(),
|
||||||
|
/// access_key: "rustfsadmin".to_string(),
|
||||||
|
/// secret_key: "rustfsadmin".to_string(),
|
||||||
|
/// domain_name: "demo.rustfs.com".to_string(),
|
||||||
|
/// volume_name: "data".to_string(),
|
||||||
|
/// console_address: "127.0.0.1:9001".to_string(),
|
||||||
|
/// };
|
||||||
|
/// println!("{:?}", config);
|
||||||
|
/// assert_eq!(config.address, "127.0.0.1:9000");
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize, Serialize, Ord, PartialOrd, Eq, PartialEq)]
|
||||||
|
pub struct RustFSConfig {
|
||||||
|
pub address: String,
|
||||||
|
pub host: String,
|
||||||
|
pub port: String,
|
||||||
|
pub access_key: String,
|
||||||
|
pub secret_key: String,
|
||||||
|
pub domain_name: String,
|
||||||
|
pub volume_name: String,
|
||||||
|
pub console_address: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RustFSConfig {
|
||||||
|
/// keyring the name of the service
|
||||||
|
const SERVICE_NAME: &'static str = "rustfs-service";
|
||||||
|
/// keyring the key of the service
|
||||||
|
const SERVICE_KEY: &'static str = "rustfs_key";
|
||||||
|
/// default domain name
|
||||||
|
const DEFAULT_DOMAIN_NAME_VALUE: &'static str = "demo.rustfs.com";
|
||||||
|
/// default address value
|
||||||
|
const DEFAULT_ADDRESS_VALUE: &'static str = "127.0.0.1:9000";
|
||||||
|
/// default port value
|
||||||
|
const DEFAULT_PORT_VALUE: &'static str = "9000";
|
||||||
|
/// default host value
|
||||||
|
const DEFAULT_HOST_VALUE: &'static str = "127.0.0.1";
|
||||||
|
/// default access key value
|
||||||
|
const DEFAULT_ACCESS_KEY_VALUE: &'static str = "rustfsadmin";
|
||||||
|
/// default secret key value
|
||||||
|
const DEFAULT_SECRET_KEY_VALUE: &'static str = "rustfsadmin";
|
||||||
|
/// default console address value
|
||||||
|
const DEFAULT_CONSOLE_ADDRESS_VALUE: &'static str = "127.0.0.1:9001";
|
||||||
|
|
||||||
|
/// get the default volume_name
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * The default volume name
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let volume_name = RustFSConfig::default_volume_name();
|
||||||
|
/// ```
|
||||||
|
pub fn default_volume_name() -> String {
|
||||||
|
dirs::home_dir()
|
||||||
|
.map(|home| home.join("rustfs").join("data"))
|
||||||
|
.and_then(|path| path.to_str().map(String::from))
|
||||||
|
.unwrap_or_else(|| "data".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// create a default configuration
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * The default configuration
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let config = RustFSConfig::default_config();
|
||||||
|
/// println!("{:?}", config);
|
||||||
|
/// assert_eq!(config.address, "127.0.0.1:9000");
|
||||||
|
/// ```
|
||||||
|
pub fn default_config() -> Self {
|
||||||
|
Self {
|
||||||
|
address: Self::DEFAULT_ADDRESS_VALUE.to_string(),
|
||||||
|
host: Self::DEFAULT_HOST_VALUE.to_string(),
|
||||||
|
port: Self::DEFAULT_PORT_VALUE.to_string(),
|
||||||
|
access_key: Self::DEFAULT_ACCESS_KEY_VALUE.to_string(),
|
||||||
|
secret_key: Self::DEFAULT_SECRET_KEY_VALUE.to_string(),
|
||||||
|
domain_name: Self::DEFAULT_DOMAIN_NAME_VALUE.to_string(),
|
||||||
|
volume_name: Self::default_volume_name(),
|
||||||
|
console_address: Self::DEFAULT_CONSOLE_ADDRESS_VALUE.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the configuration from the keyring
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// * If the configuration cannot be loaded from the keyring
|
||||||
|
/// * If the configuration cannot be deserialized
|
||||||
|
/// * If the address cannot be extracted from the configuration
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let config = RustFSConfig::load().unwrap();
|
||||||
|
/// println!("{:?}", config);
|
||||||
|
/// assert_eq!(config.address, "127.0.0.1:9000");
|
||||||
|
/// ```
|
||||||
|
pub fn load() -> Result<Self, Box<dyn Error>> {
|
||||||
|
let mut config = Self::default_config();
|
||||||
|
|
||||||
|
// Try to get the configuration of the storage from the keyring
|
||||||
|
let entry = Entry::new(Self::SERVICE_NAME, Self::SERVICE_KEY)?;
|
||||||
|
if let Ok(stored_json) = entry.get_password() {
|
||||||
|
if let Ok(stored_config) = serde_json::from_str::<RustFSConfig>(&stored_json) {
|
||||||
|
// update fields that are not empty and non default
|
||||||
|
if !stored_config.address.is_empty() && stored_config.address != Self::DEFAULT_ADDRESS_VALUE {
|
||||||
|
config.address = stored_config.address;
|
||||||
|
let (host, port) = Self::extract_host_port(config.address.as_str())
|
||||||
|
.ok_or_else(|| format!("无法从地址 '{}' 中提取主机和端口", config.address))?;
|
||||||
|
config.host = host.to_string();
|
||||||
|
config.port = port.to_string();
|
||||||
|
}
|
||||||
|
if !stored_config.access_key.is_empty() && stored_config.access_key != Self::DEFAULT_ACCESS_KEY_VALUE {
|
||||||
|
config.access_key = stored_config.access_key;
|
||||||
|
}
|
||||||
|
if !stored_config.secret_key.is_empty() && stored_config.secret_key != Self::DEFAULT_SECRET_KEY_VALUE {
|
||||||
|
config.secret_key = stored_config.secret_key;
|
||||||
|
}
|
||||||
|
if !stored_config.domain_name.is_empty() && stored_config.domain_name != Self::DEFAULT_DOMAIN_NAME_VALUE {
|
||||||
|
config.domain_name = stored_config.domain_name;
|
||||||
|
}
|
||||||
|
// The stored volume_name is updated only if it is not empty and different from the default
|
||||||
|
if !stored_config.volume_name.is_empty() && stored_config.volume_name != Self::default_volume_name() {
|
||||||
|
config.volume_name = stored_config.volume_name;
|
||||||
|
}
|
||||||
|
if !stored_config.console_address.is_empty()
|
||||||
|
&& stored_config.console_address != Self::DEFAULT_CONSOLE_ADDRESS_VALUE
|
||||||
|
{
|
||||||
|
config.console_address = stored_config.console_address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Auxiliary method: Extract the host and port from the address string
|
||||||
|
/// # Arguments
|
||||||
|
/// * `address` - The address string
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Some((host, port))` - The host and port
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// * If the address is not in the form 'host:port'
|
||||||
|
/// * If the port is not a valid u16
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let (host, port) = RustFSConfig::extract_host_port("127.0.0.1:9000").unwrap();
|
||||||
|
/// assert_eq!(host, "127.0.0.1");
|
||||||
|
/// assert_eq!(port, 9000);
|
||||||
|
/// ```
|
||||||
|
pub fn extract_host_port(address: &str) -> Option<(&str, u16)> {
|
||||||
|
let parts: Vec<&str> = address.split(':').collect();
|
||||||
|
if parts.len() == 2 {
|
||||||
|
if let Ok(port) = parts[1].parse::<u16>() {
|
||||||
|
return Some((parts[0], port));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// save the configuration to keyring
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// * If the configuration cannot be serialized
|
||||||
|
/// * If the configuration cannot be saved to the keyring
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let config = RustFSConfig::default_config();
|
||||||
|
/// config.save().unwrap();
|
||||||
|
/// ```
|
||||||
|
pub fn save(&self) -> Result<(), Box<dyn Error>> {
|
||||||
|
let entry = Entry::new(Self::SERVICE_NAME, Self::SERVICE_KEY)?;
|
||||||
|
let json = serde_json::to_string(self)?;
|
||||||
|
entry.set_password(&json)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the stored configuration from the system keyring
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// `Ok(())` if the configuration was successfully cleared, or an error if the operation failed.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// RustFSConfig::clear().unwrap();
|
||||||
|
/// ```
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn clear() -> Result<(), Box<dyn Error>> {
|
||||||
|
let entry = Entry::new(Self::SERVICE_NAME, Self::SERVICE_KEY)?;
|
||||||
|
entry.delete_credential()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rustfs_config_default() {
|
||||||
|
let config = RustFSConfig::default();
|
||||||
|
assert!(config.address.is_empty());
|
||||||
|
assert!(config.host.is_empty());
|
||||||
|
assert!(config.port.is_empty());
|
||||||
|
assert!(config.access_key.is_empty());
|
||||||
|
assert!(config.secret_key.is_empty());
|
||||||
|
assert!(config.domain_name.is_empty());
|
||||||
|
assert!(config.volume_name.is_empty());
|
||||||
|
assert!(config.console_address.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rustfs_config_creation() {
|
||||||
|
let config = RustFSConfig {
|
||||||
|
address: "192.168.1.100:9000".to_string(),
|
||||||
|
host: "192.168.1.100".to_string(),
|
||||||
|
port: "9000".to_string(),
|
||||||
|
access_key: "testuser".to_string(),
|
||||||
|
secret_key: "testpass".to_string(),
|
||||||
|
domain_name: "test.rustfs.com".to_string(),
|
||||||
|
volume_name: "/data/rustfs".to_string(),
|
||||||
|
console_address: "192.168.1.100:9001".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(config.address, "192.168.1.100:9000");
|
||||||
|
assert_eq!(config.host, "192.168.1.100");
|
||||||
|
assert_eq!(config.port, "9000");
|
||||||
|
assert_eq!(config.access_key, "testuser");
|
||||||
|
assert_eq!(config.secret_key, "testpass");
|
||||||
|
assert_eq!(config.domain_name, "test.rustfs.com");
|
||||||
|
assert_eq!(config.volume_name, "/data/rustfs");
|
||||||
|
assert_eq!(config.console_address, "192.168.1.100:9001");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_volume_name() {
|
||||||
|
let volume_name = RustFSConfig::default_volume_name();
|
||||||
|
assert!(!volume_name.is_empty());
|
||||||
|
// Should either be the home directory path or fallback to "data"
|
||||||
|
assert!(volume_name.contains("rustfs") || volume_name == "data");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_config() {
|
||||||
|
let config = RustFSConfig::default_config();
|
||||||
|
assert_eq!(config.address, RustFSConfig::DEFAULT_ADDRESS_VALUE);
|
||||||
|
assert_eq!(config.host, RustFSConfig::DEFAULT_HOST_VALUE);
|
||||||
|
assert_eq!(config.port, RustFSConfig::DEFAULT_PORT_VALUE);
|
||||||
|
assert_eq!(config.access_key, RustFSConfig::DEFAULT_ACCESS_KEY_VALUE);
|
||||||
|
assert_eq!(config.secret_key, RustFSConfig::DEFAULT_SECRET_KEY_VALUE);
|
||||||
|
assert_eq!(config.domain_name, RustFSConfig::DEFAULT_DOMAIN_NAME_VALUE);
|
||||||
|
assert_eq!(config.console_address, RustFSConfig::DEFAULT_CONSOLE_ADDRESS_VALUE);
|
||||||
|
assert!(!config.volume_name.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_host_port_valid() {
|
||||||
|
let test_cases = vec![
|
||||||
|
("127.0.0.1:9000", Some(("127.0.0.1", 9000))),
|
||||||
|
("localhost:8080", Some(("localhost", 8080))),
|
||||||
|
("192.168.1.100:3000", Some(("192.168.1.100", 3000))),
|
||||||
|
("0.0.0.0:80", Some(("0.0.0.0", 80))),
|
||||||
|
("example.com:443", Some(("example.com", 443))),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (input, expected) in test_cases {
|
||||||
|
let result = RustFSConfig::extract_host_port(input);
|
||||||
|
assert_eq!(result, expected, "Failed for input: {input}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_host_port_invalid() {
|
||||||
|
let invalid_cases = vec![
|
||||||
|
"127.0.0.1", // Missing port
|
||||||
|
"127.0.0.1:", // Empty port
|
||||||
|
"127.0.0.1:abc", // Invalid port
|
||||||
|
"127.0.0.1:99999", // Port out of range
|
||||||
|
"", // Empty string
|
||||||
|
"127.0.0.1:9000:extra", // Too many parts
|
||||||
|
"invalid", // No colon
|
||||||
|
];
|
||||||
|
|
||||||
|
for input in invalid_cases {
|
||||||
|
let result = RustFSConfig::extract_host_port(input);
|
||||||
|
assert_eq!(result, None, "Should be None for input: {input}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case: empty host but valid port should still work
|
||||||
|
let result = RustFSConfig::extract_host_port(":9000");
|
||||||
|
assert_eq!(result, Some(("", 9000)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_host_port_edge_cases() {
|
||||||
|
// Test edge cases for port numbers
|
||||||
|
assert_eq!(RustFSConfig::extract_host_port("host:0"), Some(("host", 0)));
|
||||||
|
assert_eq!(RustFSConfig::extract_host_port("host:65535"), Some(("host", 65535)));
|
||||||
|
assert_eq!(RustFSConfig::extract_host_port("host:65536"), None); // Out of range
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialization() {
|
||||||
|
let config = RustFSConfig {
|
||||||
|
address: "127.0.0.1:9000".to_string(),
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: "9000".to_string(),
|
||||||
|
access_key: "admin".to_string(),
|
||||||
|
secret_key: "password".to_string(),
|
||||||
|
domain_name: "test.com".to_string(),
|
||||||
|
volume_name: "/data".to_string(),
|
||||||
|
console_address: "127.0.0.1:9001".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&config).unwrap();
|
||||||
|
assert!(json.contains("127.0.0.1:9000"));
|
||||||
|
assert!(json.contains("admin"));
|
||||||
|
assert!(json.contains("test.com"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialization() {
|
||||||
|
let json = r#"{
|
||||||
|
"address": "192.168.1.100:9000",
|
||||||
|
"host": "192.168.1.100",
|
||||||
|
"port": "9000",
|
||||||
|
"access_key": "testuser",
|
||||||
|
"secret_key": "testpass",
|
||||||
|
"domain_name": "example.com",
|
||||||
|
"volume_name": "/opt/data",
|
||||||
|
"console_address": "192.168.1.100:9001"
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let config: RustFSConfig = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(config.address, "192.168.1.100:9000");
|
||||||
|
assert_eq!(config.host, "192.168.1.100");
|
||||||
|
assert_eq!(config.port, "9000");
|
||||||
|
assert_eq!(config.access_key, "testuser");
|
||||||
|
assert_eq!(config.secret_key, "testpass");
|
||||||
|
assert_eq!(config.domain_name, "example.com");
|
||||||
|
assert_eq!(config.volume_name, "/opt/data");
|
||||||
|
assert_eq!(config.console_address, "192.168.1.100:9001");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialization_deserialization_roundtrip() {
|
||||||
|
let original_config = RustFSConfig {
|
||||||
|
address: "10.0.0.1:8080".to_string(),
|
||||||
|
host: "10.0.0.1".to_string(),
|
||||||
|
port: "8080".to_string(),
|
||||||
|
access_key: "roundtrip_user".to_string(),
|
||||||
|
secret_key: "roundtrip_pass".to_string(),
|
||||||
|
domain_name: "roundtrip.test".to_string(),
|
||||||
|
volume_name: "/tmp/roundtrip".to_string(),
|
||||||
|
console_address: "10.0.0.1:8081".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&original_config).unwrap();
|
||||||
|
let deserialized_config: RustFSConfig = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(original_config, deserialized_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config_ordering() {
|
||||||
|
let config1 = RustFSConfig {
|
||||||
|
address: "127.0.0.1:9000".to_string(),
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: "9000".to_string(),
|
||||||
|
access_key: "admin".to_string(),
|
||||||
|
secret_key: "password".to_string(),
|
||||||
|
domain_name: "test.com".to_string(),
|
||||||
|
volume_name: "/data".to_string(),
|
||||||
|
console_address: "127.0.0.1:9001".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let config2 = RustFSConfig {
|
||||||
|
address: "127.0.0.1:9000".to_string(),
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: "9000".to_string(),
|
||||||
|
access_key: "admin".to_string(),
|
||||||
|
secret_key: "password".to_string(),
|
||||||
|
domain_name: "test.com".to_string(),
|
||||||
|
volume_name: "/data".to_string(),
|
||||||
|
console_address: "127.0.0.1:9001".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let config3 = RustFSConfig {
|
||||||
|
address: "127.0.0.1:9001".to_string(), // Different port
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: "9001".to_string(),
|
||||||
|
access_key: "admin".to_string(),
|
||||||
|
secret_key: "password".to_string(),
|
||||||
|
domain_name: "test.com".to_string(),
|
||||||
|
volume_name: "/data".to_string(),
|
||||||
|
console_address: "127.0.0.1:9002".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(config1, config2);
|
||||||
|
assert_ne!(config1, config3);
|
||||||
|
assert!(config1 < config3); // Lexicographic ordering
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_clone() {
|
||||||
|
let original = RustFSConfig::default_config();
|
||||||
|
let cloned = original.clone();
|
||||||
|
|
||||||
|
assert_eq!(original, cloned);
|
||||||
|
assert_eq!(original.address, cloned.address);
|
||||||
|
assert_eq!(original.access_key, cloned.access_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_debug_format() {
|
||||||
|
let config = RustFSConfig::default_config();
|
||||||
|
let debug_str = format!("{config:?}");
|
||||||
|
|
||||||
|
assert!(debug_str.contains("RustFSConfig"));
|
||||||
|
assert!(debug_str.contains("address"));
|
||||||
|
assert!(debug_str.contains("127.0.0.1:9000"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_constants() {
|
||||||
|
assert_eq!(RustFSConfig::SERVICE_NAME, "rustfs-service");
|
||||||
|
assert_eq!(RustFSConfig::SERVICE_KEY, "rustfs_key");
|
||||||
|
assert_eq!(RustFSConfig::DEFAULT_DOMAIN_NAME_VALUE, "demo.rustfs.com");
|
||||||
|
assert_eq!(RustFSConfig::DEFAULT_ADDRESS_VALUE, "127.0.0.1:9000");
|
||||||
|
assert_eq!(RustFSConfig::DEFAULT_PORT_VALUE, "9000");
|
||||||
|
assert_eq!(RustFSConfig::DEFAULT_HOST_VALUE, "127.0.0.1");
|
||||||
|
assert_eq!(RustFSConfig::DEFAULT_ACCESS_KEY_VALUE, "rustfsadmin");
|
||||||
|
assert_eq!(RustFSConfig::DEFAULT_SECRET_KEY_VALUE, "rustfsadmin");
|
||||||
|
assert_eq!(RustFSConfig::DEFAULT_CONSOLE_ADDRESS_VALUE, "127.0.0.1:9001");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_strings() {
|
||||||
|
let config = RustFSConfig {
|
||||||
|
address: "".to_string(),
|
||||||
|
host: "".to_string(),
|
||||||
|
port: "".to_string(),
|
||||||
|
access_key: "".to_string(),
|
||||||
|
secret_key: "".to_string(),
|
||||||
|
domain_name: "".to_string(),
|
||||||
|
volume_name: "".to_string(),
|
||||||
|
console_address: "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(config.address.is_empty());
|
||||||
|
assert!(config.host.is_empty());
|
||||||
|
assert!(config.port.is_empty());
|
||||||
|
assert!(config.access_key.is_empty());
|
||||||
|
assert!(config.secret_key.is_empty());
|
||||||
|
assert!(config.domain_name.is_empty());
|
||||||
|
assert!(config.volume_name.is_empty());
|
||||||
|
assert!(config.console_address.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_very_long_strings() {
|
||||||
|
let long_string = "a".repeat(1000);
|
||||||
|
let config = RustFSConfig {
|
||||||
|
address: format!("{long_string}:9000"),
|
||||||
|
host: long_string.clone(),
|
||||||
|
port: "9000".to_string(),
|
||||||
|
access_key: long_string.clone(),
|
||||||
|
secret_key: long_string.clone(),
|
||||||
|
domain_name: format!("{long_string}.com"),
|
||||||
|
volume_name: format!("/data/{long_string}"),
|
||||||
|
console_address: format!("{long_string}:9001"),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(config.host.len(), 1000);
|
||||||
|
assert_eq!(config.access_key.len(), 1000);
|
||||||
|
assert_eq!(config.secret_key.len(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_special_characters() {
|
||||||
|
let config = RustFSConfig {
|
||||||
|
address: "127.0.0.1:9000".to_string(),
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: "9000".to_string(),
|
||||||
|
access_key: "user@domain.com".to_string(),
|
||||||
|
secret_key: "p@ssw0rd!#$%".to_string(),
|
||||||
|
domain_name: "test-domain.example.com".to_string(),
|
||||||
|
volume_name: "/data/rust-fs/storage".to_string(),
|
||||||
|
console_address: "127.0.0.1:9001".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(config.access_key.contains("@"));
|
||||||
|
assert!(config.secret_key.contains("!#$%"));
|
||||||
|
assert!(config.domain_name.contains("-"));
|
||||||
|
assert!(config.volume_name.contains("/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unicode_strings() {
|
||||||
|
let config = RustFSConfig {
|
||||||
|
address: "127.0.0.1:9000".to_string(),
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: "9000".to_string(),
|
||||||
|
access_key: "用户名".to_string(),
|
||||||
|
secret_key: "密码 123".to_string(),
|
||||||
|
domain_name: "测试.com".to_string(),
|
||||||
|
volume_name: "/数据/存储".to_string(),
|
||||||
|
console_address: "127.0.0.1:9001".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(config.access_key, "用户名");
|
||||||
|
assert_eq!(config.secret_key, "密码 123");
|
||||||
|
assert_eq!(config.domain_name, "测试.com");
|
||||||
|
assert_eq!(config.volume_name, "/数据/存储");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_memory_efficiency() {
|
||||||
|
// Test that the structure doesn't use excessive memory
|
||||||
|
assert!(std::mem::size_of::<RustFSConfig>() < 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Keyring-related tests (load, save, clear) are not included here
|
||||||
|
// because they require actual keyring access and would be integration tests
|
||||||
|
// rather than unit tests. They should be tested separately in an integration
|
||||||
|
// test environment where keyring access can be properly mocked or controlled.
|
||||||
|
}
|
||||||
899
cli/rustfs-gui/src/utils/helper.rs
Normal file
@@ -0,0 +1,899 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::utils::RustFSConfig;
|
||||||
|
use dioxus::logger::tracing::{debug, error, info};
|
||||||
|
use rust_embed::RustEmbed;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command as StdCommand;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::fs;
|
||||||
|
use tokio::fs::File;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::sync::{Mutex, mpsc};
|
||||||
|
|
||||||
|
#[derive(RustEmbed)]
|
||||||
|
#[folder = "$CARGO_MANIFEST_DIR/embedded-rustfs/"]
|
||||||
|
struct Asset;
|
||||||
|
|
||||||
|
// Use `LazyLock` to cache the checksum of embedded resources
|
||||||
|
static RUSTFS_HASH: LazyLock<Mutex<String>> = LazyLock::new(|| {
|
||||||
|
let rustfs_file = if cfg!(windows) { "rustfs.exe" } else { "rustfs" };
|
||||||
|
let rustfs_data = Asset::get(rustfs_file).expect("RustFs binary not embedded");
|
||||||
|
let hash = hex::encode(Sha256::digest(&rustfs_data.data));
|
||||||
|
Mutex::new(hash)
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Service command
|
||||||
|
/// This enum represents the commands that can be sent to the service manager
|
||||||
|
/// to start, stop, or restart the service
|
||||||
|
/// The `Start` variant contains the configuration for the service
|
||||||
|
/// The `Restart` variant contains the configuration for the service
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let config = RustFSConfig {
|
||||||
|
/// address: "127.0.0.1:9000".to_string(),
|
||||||
|
/// host: "127.0.0.1".to_string(),
|
||||||
|
/// port: "9000".to_string(),
|
||||||
|
/// access_key: "rustfsadmin".to_string(),
|
||||||
|
/// secret_key: "rustfsadmin".to_string(),
|
||||||
|
/// domain_name: "demo.rustfs.com".to_string(),
|
||||||
|
/// volume_name: "data".to_string(),
|
||||||
|
/// console_address: "127.0.0.1:9001".to_string(),
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// let command = ServiceCommand::Start(config);
|
||||||
|
/// println!("{:?}", command);
|
||||||
|
///
|
||||||
|
/// assert_eq!(command, ServiceCommand::Start(config));
|
||||||
|
/// ```
|
||||||
|
pub enum ServiceCommand {
|
||||||
|
Start(RustFSConfig),
|
||||||
|
Stop,
|
||||||
|
Restart(RustFSConfig),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service operation result
|
||||||
|
/// This struct represents the result of a service operation
|
||||||
|
/// It contains information about the success of the operation,
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use chrono::Local;
|
||||||
|
///
|
||||||
|
/// let result = ServiceOperationResult {
|
||||||
|
/// success: true,
|
||||||
|
/// start_time: chrono::Local::now(),
|
||||||
|
/// end_time: chrono::Local::now(),
|
||||||
|
/// message: "服务启动成功".to_string(),
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// println!("{:?}", result);
|
||||||
|
/// assert_eq!(result.success, true);
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ServiceOperationResult {
|
||||||
|
pub success: bool,
|
||||||
|
pub start_time: chrono::DateTime<chrono::Local>,
|
||||||
|
pub end_time: chrono::DateTime<chrono::Local>,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service manager
|
||||||
|
/// This struct represents a service manager that can be used to start, stop, or restart a service
|
||||||
|
/// It contains a command sender that can be used to send commands to the service manager
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let service_manager = ServiceManager::new();
|
||||||
|
/// println!("{:?}", service_manager);
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ServiceManager {
|
||||||
|
command_tx: mpsc::Sender<ServiceCommand>,
|
||||||
|
// process: Arc<Mutex<Option<Child>>>,
|
||||||
|
// pid: Arc<Mutex<Option<u32>>>, // Add PID storage
|
||||||
|
// current_config: Arc<Mutex<Option<RustFSConfig>>>, // Add configuration storage
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServiceManager {
|
||||||
|
/// check if the service is running and return a pid
|
||||||
|
/// This function is platform dependent
|
||||||
|
/// On Unix systems, it uses the `ps` command to check for the service
|
||||||
|
/// On Windows systems, it uses the `wmic` command to check for the service
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let pid = check_service_status().await;
|
||||||
|
/// println!("{:?}", pid);
|
||||||
|
/// ```
|
||||||
|
pub async fn check_service_status() -> Option<u32> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
// use the ps command on a unix system
|
||||||
|
if let Ok(output) = StdCommand::new("ps").arg("-ef").output() {
|
||||||
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||||
|
for line in output_str.lines() {
|
||||||
|
// match contains `rustfs/bin/rustfs` of the line
|
||||||
|
if line.contains("rustfs/bin/rustfs") && !line.contains("grep") {
|
||||||
|
if let Some(pid_str) = line.split_whitespace().nth(1) {
|
||||||
|
if let Ok(pid) = pid_str.parse::<u32>() {
|
||||||
|
return Some(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
if let Ok(output) = StdCommand::new("wmic")
|
||||||
|
.arg("process")
|
||||||
|
.arg("where")
|
||||||
|
.arg("caption='rustfs.exe'")
|
||||||
|
.arg("get")
|
||||||
|
.arg("processid")
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||||
|
for line in output_str.lines() {
|
||||||
|
if let Ok(pid) = line.trim().parse::<u32>() {
|
||||||
|
return Some(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare the service
|
||||||
|
/// This function downloads the service executable if it doesn't exist
|
||||||
|
/// It also creates the necessary directories for the service
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let executable_path = prepare_service().await;
|
||||||
|
/// println!("{:?}", executable_path);
|
||||||
|
/// ```
|
||||||
|
async fn prepare_service() -> Result<PathBuf, Box<dyn Error>> {
|
||||||
|
// get the user directory
|
||||||
|
let home_dir = dirs::home_dir().ok_or("无法获取用户目录")?;
|
||||||
|
let rustfs_dir = home_dir.join("rustfs");
|
||||||
|
let bin_dir = rustfs_dir.join("bin");
|
||||||
|
let data_dir = rustfs_dir.join("data");
|
||||||
|
let logs_dir = rustfs_dir.join("logs");
|
||||||
|
|
||||||
|
// create the necessary directories
|
||||||
|
for dir in [&bin_dir, &data_dir, &logs_dir] {
|
||||||
|
if !dir.exists() {
|
||||||
|
tokio::fs::create_dir_all(dir).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let rustfs_file = if cfg!(windows) { "rustfs.exe" } else { "rustfs" };
|
||||||
|
let executable_path = bin_dir.join(rustfs_file);
|
||||||
|
let hash_path = bin_dir.join("embedded_rustfs.sha256");
|
||||||
|
|
||||||
|
if executable_path.exists() && hash_path.exists() {
|
||||||
|
let cached_hash = fs::read_to_string(&hash_path).await?;
|
||||||
|
let expected_hash = RUSTFS_HASH.lock().await;
|
||||||
|
if cached_hash == *expected_hash {
|
||||||
|
println!("Use cached rustfs: {executable_path:?}");
|
||||||
|
return Ok(executable_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract and write files
|
||||||
|
let rustfs_data = Asset::get(rustfs_file).expect("RustFS binary not embedded");
|
||||||
|
let mut file = File::create(&executable_path).await?;
|
||||||
|
file.write_all(&rustfs_data.data).await?;
|
||||||
|
let expected_hash = hex::encode(Sha256::digest(&rustfs_data.data));
|
||||||
|
fs::write(&hash_path, expected_hash).await?;
|
||||||
|
|
||||||
|
// set execution permissions on unix systems
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = std::fs::metadata(&executable_path)?.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
std::fs::set_permissions(&executable_path, perms)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(executable_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function: Extracts the port from the address string
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let address = "127.0.0.1:9000";
|
||||||
|
/// let port = extract_port(address);
|
||||||
|
/// println!("{:?}", port);
|
||||||
|
/// ```
|
||||||
|
fn extract_port(address: &str) -> Option<u16> {
|
||||||
|
address.split(':').nth(1)?.parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new instance of the service manager
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let service_manager = ServiceManager::new();
|
||||||
|
/// println!("{:?}", service_manager);
|
||||||
|
/// ```
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
let (command_tx, mut command_rx) = mpsc::channel(10);
|
||||||
|
// Start the control loop
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(cmd) = command_rx.recv().await {
|
||||||
|
match cmd {
|
||||||
|
ServiceCommand::Start(config) => {
|
||||||
|
if let Err(e) = Self::start_service(&config).await {
|
||||||
|
Self::show_error(&format!("启动服务失败:{e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServiceCommand::Stop => {
|
||||||
|
if let Err(e) = Self::stop_service().await {
|
||||||
|
Self::show_error(&format!("停止服务失败:{e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServiceCommand::Restart(config) => {
|
||||||
|
if Self::check_service_status().await.is_some() {
|
||||||
|
if let Err(e) = Self::stop_service().await {
|
||||||
|
Self::show_error(&format!("重启服务失败:{e}"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(e) = Self::start_service(&config).await {
|
||||||
|
Self::show_error(&format!("重启服务失败:{e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ServiceManager { command_tx }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the service
|
||||||
|
/// This function starts the service with the given configuration
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let config = RustFSConfig {
|
||||||
|
/// address: "127.0.0.1:9000".to_string(),
|
||||||
|
/// host: "127.0.0.1".to_string(),
|
||||||
|
/// port: "9000".to_string(),
|
||||||
|
/// access_key: "rustfsadmin".to_string(),
|
||||||
|
/// secret_key: "rustfsadmin".to_string(),
|
||||||
|
/// domain_name: "demo.rustfs.com".to_string(),
|
||||||
|
/// volume_name: "data".to_string(),
|
||||||
|
/// console_address: "127.0.0.1:9001".to_string(),
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// let result = start_service(&config).await;
|
||||||
|
/// println!("{:?}", result);
|
||||||
|
/// ```
|
||||||
|
async fn start_service(config: &RustFSConfig) -> Result<(), Box<dyn Error>> {
|
||||||
|
// Check if the service is already running
|
||||||
|
if let Some(existing_pid) = Self::check_service_status().await {
|
||||||
|
return Err(format!("服务已经在运行,PID: {existing_pid}").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the service program
|
||||||
|
let executable_path = Self::prepare_service().await?;
|
||||||
|
// Check the data catalog
|
||||||
|
let volume_name_path = Path::new(&config.volume_name);
|
||||||
|
if !volume_name_path.exists() {
|
||||||
|
tokio::fs::create_dir_all(&config.volume_name).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the port from the configuration
|
||||||
|
let main_port = Self::extract_port(&config.address).ok_or("无法解析主服务端口")?;
|
||||||
|
let console_port = Self::extract_port(&config.console_address).ok_or("无法解析控制台端口")?;
|
||||||
|
|
||||||
|
let host = config.address.split(':').next().ok_or("无法解析主机地址")?;
|
||||||
|
|
||||||
|
// Check the port
|
||||||
|
let ports = vec![main_port, console_port];
|
||||||
|
for port in ports {
|
||||||
|
if Self::is_port_in_use(host, port).await {
|
||||||
|
return Err(format!("端口 {port} 已被占用").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the service
|
||||||
|
let mut child = tokio::process::Command::new(executable_path)
|
||||||
|
.arg("--address")
|
||||||
|
.arg(&config.address)
|
||||||
|
.arg("--access-key")
|
||||||
|
.arg(&config.access_key)
|
||||||
|
.arg("--secret-key")
|
||||||
|
.arg(&config.secret_key)
|
||||||
|
.arg("--console-address")
|
||||||
|
.arg(&config.console_address)
|
||||||
|
.arg(config.volume_name.clone())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
let process_pid = child.id().unwrap();
|
||||||
|
// Wait for the service to start
|
||||||
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
// Check if the service started successfully
|
||||||
|
if Self::is_port_in_use(host, main_port).await {
|
||||||
|
Self::show_info(&format!("服务启动成功!进程 ID: {process_pid}"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
child.kill().await?;
|
||||||
|
Err("服务启动失败".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the service
|
||||||
|
/// This function stops the service
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let result = stop_service().await;
|
||||||
|
/// println!("{:?}", result);
|
||||||
|
/// ```
|
||||||
|
async fn stop_service() -> Result<(), Box<dyn Error>> {
|
||||||
|
let existing_pid = Self::check_service_status().await;
|
||||||
|
debug!("existing_pid: {:?}", existing_pid);
|
||||||
|
if let Some(service_pid) = existing_pid {
|
||||||
|
// An attempt was made to terminate the process
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
StdCommand::new("kill").arg("-9").arg(service_pid.to_string()).output()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
StdCommand::new("taskkill")
|
||||||
|
.arg("/F")
|
||||||
|
.arg("/PID")
|
||||||
|
.arg(&service_pid.to_string())
|
||||||
|
.output()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the service is indeed stopped
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
if Self::check_service_status().await.is_some() {
|
||||||
|
return Err("服务停止失败".into());
|
||||||
|
}
|
||||||
|
Self::show_info("服务已成功停止");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err("服务未运行".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the port is in use
|
||||||
|
/// This function checks if the given port is in use on the given host
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let host = "127.0.0.1";
|
||||||
|
/// let port = 9000;
|
||||||
|
/// let result = is_port_in_use(host, port).await;
|
||||||
|
/// println!("{:?}", result);
|
||||||
|
/// ```
|
||||||
|
async fn is_port_in_use(host: &str, port: u16) -> bool {
|
||||||
|
TcpStream::connect(format!("{host}:{port}")).await.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show an error message
|
||||||
|
/// This function shows an error message dialog
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// show_error("This is an error message");
|
||||||
|
/// ```
|
||||||
|
pub(crate) fn show_error(message: &str) {
|
||||||
|
rfd::MessageDialog::new()
|
||||||
|
.set_title("错误")
|
||||||
|
.set_description(message)
|
||||||
|
.set_level(rfd::MessageLevel::Error)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show an information message
|
||||||
|
/// This function shows an information message dialog
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// show_info("This is an information message");
|
||||||
|
/// ```
|
||||||
|
pub(crate) fn show_info(message: &str) {
|
||||||
|
rfd::MessageDialog::new()
|
||||||
|
.set_title("成功")
|
||||||
|
.set_description(message)
|
||||||
|
.set_level(rfd::MessageLevel::Info)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the service
|
||||||
|
/// This function sends a `Start` command to the service manager
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let config = RustFSConfig {
|
||||||
|
/// address: "127.0.0.1:9000".to_string(),
|
||||||
|
/// host: "127.0.0.1".to_string(),
|
||||||
|
/// port: "9000".to_string(),
|
||||||
|
/// access_key: "rustfsadmin".to_string(),
|
||||||
|
/// secret_key: "rustfsadmin".to_string(),
|
||||||
|
/// domain_name: "demo.rustfs.com".to_string(),
|
||||||
|
/// volume_name: "data".to_string(),
|
||||||
|
/// console_address: "127.0.0.1:9001".to_string(),
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// let service_manager = ServiceManager::new();
|
||||||
|
/// let result = service_manager.start(config).await;
|
||||||
|
/// println!("{:?}", result);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function returns an error if the service fails to start
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// This function panics if the port number is invalid
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// This function is not marked as unsafe
|
||||||
|
///
|
||||||
|
/// # Performance
|
||||||
|
/// This function is not optimized for performance
|
||||||
|
///
|
||||||
|
/// # Design
|
||||||
|
/// This function is designed to be simple and easy to use
|
||||||
|
///
|
||||||
|
/// # Security
|
||||||
|
/// This function does not have any security implications
|
||||||
|
pub async fn start(&self, config: RustFSConfig) -> Result<ServiceOperationResult, Box<dyn Error>> {
|
||||||
|
let start_time = chrono::Local::now();
|
||||||
|
self.command_tx.send(ServiceCommand::Start(config.clone())).await?;
|
||||||
|
|
||||||
|
let host = &config.host;
|
||||||
|
let port = config.port.parse::<u16>().expect("无效的端口号");
|
||||||
|
// wait for the service to actually start
|
||||||
|
let mut retries = 0;
|
||||||
|
while retries < 30 {
|
||||||
|
// wait up to 30 seconds
|
||||||
|
if Self::check_service_status().await.is_some() && Self::is_port_in_use(host, port).await {
|
||||||
|
let end_time = chrono::Local::now();
|
||||||
|
return Ok(ServiceOperationResult {
|
||||||
|
success: true,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
message: "服务启动成功".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
retries += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err("服务启动超时".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the service
|
||||||
|
/// This function sends a `Stop` command to the service manager
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let service_manager = ServiceManager::new();
|
||||||
|
/// let result = service_manager.stop().await;
|
||||||
|
/// println!("{:?}", result);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function returns an error if the service fails to stop
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// This function panics if the port number is invalid
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// This function is not marked as unsafe
|
||||||
|
///
|
||||||
|
/// # Performance
|
||||||
|
/// This function is not optimized for performance
|
||||||
|
///
|
||||||
|
/// # Design
|
||||||
|
/// This function is designed to be simple and easy to use
|
||||||
|
///
|
||||||
|
/// # Security
|
||||||
|
/// This function does not have any security implications
|
||||||
|
pub async fn stop(&self) -> Result<ServiceOperationResult, Box<dyn Error>> {
|
||||||
|
let start_time = chrono::Local::now();
|
||||||
|
self.command_tx.send(ServiceCommand::Stop).await?;
|
||||||
|
|
||||||
|
// Wait for the service to actually stop
|
||||||
|
let mut retries = 0;
|
||||||
|
while retries < 15 {
|
||||||
|
// Wait up to 15 seconds
|
||||||
|
if Self::check_service_status().await.is_none() {
|
||||||
|
let end_time = chrono::Local::now();
|
||||||
|
return Ok(ServiceOperationResult {
|
||||||
|
success: true,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
message: "服务停止成功".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
retries += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Err("服务停止超时".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restart the service
|
||||||
|
/// This function sends a `Restart` command to the service manager
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// let config = RustFSConfig {
|
||||||
|
/// address: "127.0.0.1:9000".to_string(),
|
||||||
|
/// host: "127.0.0.1".to_string(),
|
||||||
|
/// port: "9000".to_string(),
|
||||||
|
/// access_key: "rustfsadmin".to_string(),
|
||||||
|
/// secret_key: "rustfsadmin".to_string(),
|
||||||
|
/// domain_name: "demo.rustfs.com".to_string(),
|
||||||
|
/// volume_name: "data".to_string(),
|
||||||
|
/// console_address: "127.0.0.1:9001".to_string(),
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// let service_manager = ServiceManager::new();
|
||||||
|
/// let result = service_manager.restart(config).await;
|
||||||
|
/// println!("{:?}", result);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function returns an error if the service fails to restart
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// This function panics if the port number is invalid
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// This function is not marked as unsafe
|
||||||
|
///
|
||||||
|
/// # Performance
|
||||||
|
/// This function is not optimized for performance
|
||||||
|
///
|
||||||
|
/// # Design
|
||||||
|
/// This function is designed to be simple and easy to use
|
||||||
|
///
|
||||||
|
/// # Security
|
||||||
|
/// This function does not have any security implications
|
||||||
|
pub async fn restart(&self, config: RustFSConfig) -> Result<ServiceOperationResult, Box<dyn Error>> {
|
||||||
|
let start_time = chrono::Local::now();
|
||||||
|
self.command_tx.send(ServiceCommand::Restart(config.clone())).await?;
|
||||||
|
|
||||||
|
let host = &config.host;
|
||||||
|
let port = config.port.parse::<u16>().expect("无效的端口号");
|
||||||
|
|
||||||
|
// wait for the service to restart
|
||||||
|
let mut retries = 0;
|
||||||
|
while retries < 45 {
|
||||||
|
// Longer waiting time is given as both the stop and start processes are involved
|
||||||
|
if Self::check_service_status().await.is_some() && Self::is_port_in_use(host, port).await {
|
||||||
|
match config.save() {
|
||||||
|
Ok(_) => info!("save config success"),
|
||||||
|
Err(e) => {
|
||||||
|
error!("save config error: {}", e);
|
||||||
|
self.command_tx.send(ServiceCommand::Stop).await?;
|
||||||
|
Self::show_error("保存配置失败");
|
||||||
|
return Err("保存配置失败".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let end_time = chrono::Local::now();
|
||||||
|
return Ok(ServiceOperationResult {
|
||||||
|
success: true,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
message: "服务重启成功".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
retries += 1;
|
||||||
|
}
|
||||||
|
Err("服务重启超时".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_service_command_creation() {
|
||||||
|
let config = RustFSConfig::default_config();
|
||||||
|
|
||||||
|
let start_cmd = ServiceCommand::Start(config.clone());
|
||||||
|
let stop_cmd = ServiceCommand::Stop;
|
||||||
|
let restart_cmd = ServiceCommand::Restart(config);
|
||||||
|
|
||||||
|
// Test that commands can be created
|
||||||
|
match start_cmd {
|
||||||
|
ServiceCommand::Start(_) => {}
|
||||||
|
_ => panic!("Expected Start command"),
|
||||||
|
}
|
||||||
|
|
||||||
|
match stop_cmd {
|
||||||
|
ServiceCommand::Stop => {}
|
||||||
|
_ => panic!("Expected Stop command"),
|
||||||
|
}
|
||||||
|
|
||||||
|
match restart_cmd {
|
||||||
|
ServiceCommand::Restart(_) => {}
|
||||||
|
_ => panic!("Expected Restart command"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_service_operation_result_creation() {
|
||||||
|
let start_time = chrono::Local::now();
|
||||||
|
let end_time = chrono::Local::now();
|
||||||
|
|
||||||
|
let success_result = ServiceOperationResult {
|
||||||
|
success: true,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
message: "Operation successful".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let failure_result = ServiceOperationResult {
|
||||||
|
success: false,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
message: "Operation failed".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(success_result.success);
|
||||||
|
assert_eq!(success_result.message, "Operation successful");
|
||||||
|
|
||||||
|
assert!(!failure_result.success);
|
||||||
|
assert_eq!(failure_result.message, "Operation failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_service_operation_result_debug() {
|
||||||
|
let result = ServiceOperationResult {
|
||||||
|
success: true,
|
||||||
|
start_time: chrono::Local::now(),
|
||||||
|
end_time: chrono::Local::now(),
|
||||||
|
message: "Test message".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let debug_str = format!("{result:?}");
|
||||||
|
assert!(debug_str.contains("ServiceOperationResult"));
|
||||||
|
assert!(debug_str.contains("success: true"));
|
||||||
|
assert!(debug_str.contains("Test message"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_service_manager_creation() {
|
||||||
|
// Test ServiceManager creation in a tokio runtime
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
rt.block_on(async {
|
||||||
|
let service_manager = ServiceManager::new();
|
||||||
|
|
||||||
|
// Test that ServiceManager can be created and cloned
|
||||||
|
let cloned_manager = service_manager.clone();
|
||||||
|
|
||||||
|
// Both should be valid (we can't test much more without async runtime)
|
||||||
|
assert!(format!("{service_manager:?}").contains("ServiceManager"));
|
||||||
|
assert!(format!("{cloned_manager:?}").contains("ServiceManager"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_port_valid() {
|
||||||
|
let test_cases = vec![
|
||||||
|
("127.0.0.1:9000", Some(9000)),
|
||||||
|
("localhost:8080", Some(8080)),
|
||||||
|
("192.168.1.100:3000", Some(3000)),
|
||||||
|
("0.0.0.0:80", Some(80)),
|
||||||
|
("example.com:443", Some(443)),
|
||||||
|
("host:65535", Some(65535)),
|
||||||
|
("host:1", Some(1)),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (input, expected) in test_cases {
|
||||||
|
let result = ServiceManager::extract_port(input);
|
||||||
|
assert_eq!(result, expected, "Failed for input: {input}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_port_invalid() {
|
||||||
|
let invalid_cases = vec![
|
||||||
|
"127.0.0.1", // Missing port
|
||||||
|
"127.0.0.1:", // Empty port
|
||||||
|
"127.0.0.1:abc", // Invalid port
|
||||||
|
"127.0.0.1:99999", // Port out of range
|
||||||
|
"", // Empty string
|
||||||
|
"invalid", // No colon
|
||||||
|
"host:-1", // Negative port
|
||||||
|
"host:0.5", // Decimal port
|
||||||
|
];
|
||||||
|
|
||||||
|
for input in invalid_cases {
|
||||||
|
let result = ServiceManager::extract_port(input);
|
||||||
|
assert_eq!(result, None, "Should be None for input: {input}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case: empty host but valid port should still work
|
||||||
|
assert_eq!(ServiceManager::extract_port(":9000"), Some(9000));
|
||||||
|
|
||||||
|
// Special case: multiple colons - extract_port takes the second part
|
||||||
|
// For "127.0.0.1:9000:extra", it takes "9000" which is valid
|
||||||
|
assert_eq!(ServiceManager::extract_port("127.0.0.1:9000:extra"), Some(9000));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_port_edge_cases() {
|
||||||
|
// Test edge cases for port numbers
|
||||||
|
assert_eq!(ServiceManager::extract_port("host:0"), Some(0));
|
||||||
|
assert_eq!(ServiceManager::extract_port("host:65535"), Some(65535));
|
||||||
|
assert_eq!(ServiceManager::extract_port("host:65536"), None); // Out of range
|
||||||
|
// IPv6-like address - extract_port takes the second part after split(':')
|
||||||
|
// For "::1:8080", split(':') gives ["", "", "1", "8080"], nth(1) gives ""
|
||||||
|
assert_eq!(ServiceManager::extract_port("::1:8080"), None); // Second part is empty
|
||||||
|
// For "[::1]:8080", split(':') gives ["[", "", "1]", "8080"], nth(1) gives ""
|
||||||
|
assert_eq!(ServiceManager::extract_port("[::1]:8080"), None); // Second part is empty
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_show_error() {
|
||||||
|
// Test that show_error function exists and can be called
|
||||||
|
// We can't actually test the dialog in a test environment
|
||||||
|
// so we just verify the function signature
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_show_info() {
|
||||||
|
// Test that show_info function exists and can be called
|
||||||
|
// We can't actually test the dialog in a test environment
|
||||||
|
// so we just verify the function signature
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_service_operation_result_timing() {
|
||||||
|
let start_time = chrono::Local::now();
|
||||||
|
std::thread::sleep(Duration::from_millis(10)); // Small delay
|
||||||
|
let end_time = chrono::Local::now();
|
||||||
|
|
||||||
|
let result = ServiceOperationResult {
|
||||||
|
success: true,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
message: "Timing test".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// End time should be after start time
|
||||||
|
assert!(result.end_time >= result.start_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_service_operation_result_with_unicode() {
|
||||||
|
let result = ServiceOperationResult {
|
||||||
|
success: true,
|
||||||
|
start_time: chrono::Local::now(),
|
||||||
|
end_time: chrono::Local::now(),
|
||||||
|
message: "操作成功 🎉".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(result.message, "操作成功 🎉");
|
||||||
|
assert!(result.success);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_service_operation_result_with_long_message() {
|
||||||
|
let long_message = "A".repeat(10000);
|
||||||
|
let result = ServiceOperationResult {
|
||||||
|
success: false,
|
||||||
|
start_time: chrono::Local::now(),
|
||||||
|
end_time: chrono::Local::now(),
|
||||||
|
message: long_message.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(result.message.len(), 10000);
|
||||||
|
assert_eq!(result.message, long_message);
|
||||||
|
assert!(!result.success);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_service_command_with_different_configs() {
|
||||||
|
let config1 = RustFSConfig {
|
||||||
|
address: "127.0.0.1:9000".to_string(),
|
||||||
|
host: "127.0.0.1".to_string(),
|
||||||
|
port: "9000".to_string(),
|
||||||
|
access_key: "admin1".to_string(),
|
||||||
|
secret_key: "pass1".to_string(),
|
||||||
|
domain_name: "test1.com".to_string(),
|
||||||
|
volume_name: "/data1".to_string(),
|
||||||
|
console_address: "127.0.0.1:9001".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let config2 = RustFSConfig {
|
||||||
|
address: "192.168.1.100:8080".to_string(),
|
||||||
|
host: "192.168.1.100".to_string(),
|
||||||
|
port: "8080".to_string(),
|
||||||
|
access_key: "admin2".to_string(),
|
||||||
|
secret_key: "pass2".to_string(),
|
||||||
|
domain_name: "test2.com".to_string(),
|
||||||
|
volume_name: "/data2".to_string(),
|
||||||
|
console_address: "192.168.1.100:8081".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let start_cmd1 = ServiceCommand::Start(config1);
|
||||||
|
let restart_cmd2 = ServiceCommand::Restart(config2);
|
||||||
|
|
||||||
|
// Test that different configs can be used
|
||||||
|
match start_cmd1 {
|
||||||
|
ServiceCommand::Start(config) => {
|
||||||
|
assert_eq!(config.address, "127.0.0.1:9000");
|
||||||
|
assert_eq!(config.access_key, "admin1");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Start command"),
|
||||||
|
}
|
||||||
|
|
||||||
|
match restart_cmd2 {
|
||||||
|
ServiceCommand::Restart(config) => {
|
||||||
|
assert_eq!(config.address, "192.168.1.100:8080");
|
||||||
|
assert_eq!(config.access_key, "admin2");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Restart command"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_memory_efficiency() {
|
||||||
|
// Test that structures don't use excessive memory
|
||||||
|
assert!(std::mem::size_of::<ServiceCommand>() < 2000);
|
||||||
|
assert!(std::mem::size_of::<ServiceOperationResult>() < 1000);
|
||||||
|
assert!(std::mem::size_of::<ServiceManager>() < 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The following methods are not tested here because they require:
|
||||||
|
// - Async runtime (tokio)
|
||||||
|
// - File system access
|
||||||
|
// - Network access
|
||||||
|
// - Process management
|
||||||
|
// - External dependencies (embedded assets)
|
||||||
|
//
|
||||||
|
// These should be tested in integration tests:
|
||||||
|
// - check_service_status()
|
||||||
|
// - prepare_service()
|
||||||
|
// - start_service()
|
||||||
|
// - stop_service()
|
||||||
|
// - is_port_in_use()
|
||||||
|
// - ServiceManager::start()
|
||||||
|
// - ServiceManager::stop()
|
||||||
|
// - ServiceManager::restart()
|
||||||
|
//
|
||||||
|
// The RUSTFS_HASH lazy_static is also not tested here as it depends
|
||||||
|
// on embedded assets that may not be available in unit test environment.
|
||||||
|
}
|
||||||
300
cli/rustfs-gui/src/utils/logger.rs
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use dioxus::logger::tracing::debug;
|
||||||
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
|
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||||
|
use tracing_subscriber::fmt;
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
|
||||||
|
/// Initialize the logger with a rolling file appender
|
||||||
|
/// that rotates log files daily
|
||||||
|
pub fn init_logger() -> WorkerGuard {
|
||||||
|
// configuring rolling logs rolling by day
|
||||||
|
let home_dir = dirs::home_dir().expect("无法获取用户目录");
|
||||||
|
let rustfs_dir = home_dir.join("rustfs");
|
||||||
|
let logs_dir = rustfs_dir.join("logs");
|
||||||
|
let file_appender = RollingFileAppender::builder()
|
||||||
|
.rotation(Rotation::DAILY) // rotate log files once every hour
|
||||||
|
.filename_prefix("rustfs-cli") // log file names will be prefixed with `myapp.`
|
||||||
|
.filename_suffix("log") // log file names will be suffixed with `.log`
|
||||||
|
.build(logs_dir) // try to build an appender that stores log files in `/ var/ log`
|
||||||
|
.expect("initializing rolling file appender failed");
|
||||||
|
// non-blocking writer for improved performance
|
||||||
|
let (non_blocking_file, worker_guard) = tracing_appender::non_blocking(file_appender);
|
||||||
|
|
||||||
|
// console output layer
|
||||||
|
let console_layer = fmt::layer()
|
||||||
|
.with_writer(std::io::stdout)
|
||||||
|
.with_ansi(true)
|
||||||
|
.with_line_number(true); // enable colors in the console
|
||||||
|
|
||||||
|
// file output layer
|
||||||
|
let file_layer = fmt::layer()
|
||||||
|
.with_writer(non_blocking_file)
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_thread_names(true)
|
||||||
|
.with_target(true)
|
||||||
|
.with_thread_ids(true)
|
||||||
|
.with_level(true)
|
||||||
|
.with_line_number(true); // disable colors in the file
|
||||||
|
|
||||||
|
// Combine all tiers and initialize global subscribers
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(console_layer)
|
||||||
|
.with(file_layer)
|
||||||
|
.with(tracing_subscriber::EnvFilter::new("info")) // filter the log level by environment variables
|
||||||
|
.init();
|
||||||
|
debug!("Logger initialized");
|
||||||
|
worker_guard
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::sync::Once;
|
||||||
|
|
||||||
|
static INIT: Once = Once::new();
|
||||||
|
|
||||||
|
// Helper function to ensure logger is only initialized once in tests
|
||||||
|
fn ensure_logger_init() {
|
||||||
|
INIT.call_once(|| {
|
||||||
|
// Initialize a simple test logger to avoid conflicts
|
||||||
|
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_logger_initialization_components() {
|
||||||
|
ensure_logger_init();
|
||||||
|
|
||||||
|
// Test that we can create the components used in init_logger
|
||||||
|
// without actually initializing the global logger again
|
||||||
|
|
||||||
|
// Test home directory access
|
||||||
|
let home_dir_result = dirs::home_dir();
|
||||||
|
assert!(home_dir_result.is_some(), "Should be able to get home directory");
|
||||||
|
|
||||||
|
let home_dir = home_dir_result.unwrap();
|
||||||
|
let rustfs_dir = home_dir.join("rustfs");
|
||||||
|
let logs_dir = rustfs_dir.join("logs");
|
||||||
|
|
||||||
|
// Test path construction
|
||||||
|
assert!(rustfs_dir.to_string_lossy().contains("rustfs"));
|
||||||
|
assert!(logs_dir.to_string_lossy().contains("logs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rolling_file_appender_builder() {
|
||||||
|
ensure_logger_init();
|
||||||
|
|
||||||
|
// Test that we can create a RollingFileAppender builder
|
||||||
|
let builder = RollingFileAppender::builder()
|
||||||
|
.rotation(Rotation::DAILY)
|
||||||
|
.filename_prefix("test-rustfs-cli")
|
||||||
|
.filename_suffix("log");
|
||||||
|
|
||||||
|
// We can't actually build it without creating directories,
|
||||||
|
// but we can verify the builder pattern works
|
||||||
|
let debug_str = format!("{builder:?}");
|
||||||
|
// The actual debug format might be different, so just check it's not empty
|
||||||
|
assert!(!debug_str.is_empty());
|
||||||
|
// Check that it contains some expected parts
|
||||||
|
assert!(debug_str.contains("Builder") || debug_str.contains("builder") || debug_str.contains("RollingFileAppender"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rotation_types() {
|
||||||
|
ensure_logger_init();
|
||||||
|
|
||||||
|
// Test different rotation types
|
||||||
|
let daily = Rotation::DAILY;
|
||||||
|
let hourly = Rotation::HOURLY;
|
||||||
|
let minutely = Rotation::MINUTELY;
|
||||||
|
let never = Rotation::NEVER;
|
||||||
|
|
||||||
|
// Test that rotation types can be created and formatted
|
||||||
|
assert!(!format!("{daily:?}").is_empty());
|
||||||
|
assert!(!format!("{hourly:?}").is_empty());
|
||||||
|
assert!(!format!("{minutely:?}").is_empty());
|
||||||
|
assert!(!format!("{never:?}").is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fmt_layer_configuration() {
|
||||||
|
ensure_logger_init();
|
||||||
|
|
||||||
|
// Test that we can create fmt layers with different configurations
|
||||||
|
// We can't actually test the layers directly due to type complexity,
|
||||||
|
// but we can test that the configuration values are correct
|
||||||
|
|
||||||
|
// Test console layer settings
|
||||||
|
let console_ansi = true;
|
||||||
|
let console_line_number = true;
|
||||||
|
assert!(console_ansi);
|
||||||
|
assert!(console_line_number);
|
||||||
|
|
||||||
|
// Test file layer settings
|
||||||
|
let file_ansi = false;
|
||||||
|
let file_thread_names = true;
|
||||||
|
let file_target = true;
|
||||||
|
let file_thread_ids = true;
|
||||||
|
let file_level = true;
|
||||||
|
let file_line_number = true;
|
||||||
|
|
||||||
|
assert!(!file_ansi);
|
||||||
|
assert!(file_thread_names);
|
||||||
|
assert!(file_target);
|
||||||
|
assert!(file_thread_ids);
|
||||||
|
assert!(file_level);
|
||||||
|
assert!(file_line_number);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_env_filter_creation() {
|
||||||
|
ensure_logger_init();
|
||||||
|
|
||||||
|
// Test that EnvFilter can be created with different levels
|
||||||
|
let info_filter = tracing_subscriber::EnvFilter::new("info");
|
||||||
|
let debug_filter = tracing_subscriber::EnvFilter::new("debug");
|
||||||
|
let warn_filter = tracing_subscriber::EnvFilter::new("warn");
|
||||||
|
let error_filter = tracing_subscriber::EnvFilter::new("error");
|
||||||
|
|
||||||
|
// Test that filters can be created
|
||||||
|
assert!(!format!("{info_filter:?}").is_empty());
|
||||||
|
assert!(!format!("{debug_filter:?}").is_empty());
|
||||||
|
assert!(!format!("{warn_filter:?}").is_empty());
|
||||||
|
assert!(!format!("{error_filter:?}").is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_construction() {
|
||||||
|
ensure_logger_init();
|
||||||
|
|
||||||
|
// Test path construction logic used in init_logger
|
||||||
|
if let Some(home_dir) = dirs::home_dir() {
|
||||||
|
let rustfs_dir = home_dir.join("rustfs");
|
||||||
|
let logs_dir = rustfs_dir.join("logs");
|
||||||
|
|
||||||
|
// Test that paths are constructed correctly
|
||||||
|
assert!(rustfs_dir.ends_with("rustfs"));
|
||||||
|
assert!(logs_dir.ends_with("logs"));
|
||||||
|
assert!(logs_dir.parent().unwrap().ends_with("rustfs"));
|
||||||
|
|
||||||
|
// Test path string representation
|
||||||
|
let rustfs_str = rustfs_dir.to_string_lossy();
|
||||||
|
let logs_str = logs_dir.to_string_lossy();
|
||||||
|
|
||||||
|
assert!(rustfs_str.contains("rustfs"));
|
||||||
|
assert!(logs_str.contains("rustfs"));
|
||||||
|
assert!(logs_str.contains("logs"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filename_patterns() {
|
||||||
|
ensure_logger_init();
|
||||||
|
|
||||||
|
// Test the filename patterns used in the logger
|
||||||
|
let prefix = "rustfs-cli";
|
||||||
|
let suffix = "log";
|
||||||
|
|
||||||
|
assert_eq!(prefix, "rustfs-cli");
|
||||||
|
assert_eq!(suffix, "log");
|
||||||
|
|
||||||
|
// Test that these would create valid filenames
|
||||||
|
let sample_filename = format!("{prefix}.2024-01-01.{suffix}");
|
||||||
|
assert_eq!(sample_filename, "rustfs-cli.2024-01-01.log");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_worker_guard_type() {
|
||||||
|
ensure_logger_init();
|
||||||
|
|
||||||
|
// Test that WorkerGuard type exists and can be referenced
|
||||||
|
// We can't actually create one without the full setup, but we can test the type
|
||||||
|
let guard_size = std::mem::size_of::<WorkerGuard>();
|
||||||
|
assert!(guard_size > 0, "WorkerGuard should have non-zero size");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_logger_configuration_constants() {
|
||||||
|
ensure_logger_init();
|
||||||
|
|
||||||
|
// Test the configuration values used in the logger
|
||||||
|
let default_log_level = "info";
|
||||||
|
let filename_prefix = "rustfs-cli";
|
||||||
|
let filename_suffix = "log";
|
||||||
|
let rotation = Rotation::DAILY;
|
||||||
|
|
||||||
|
assert_eq!(default_log_level, "info");
|
||||||
|
assert_eq!(filename_prefix, "rustfs-cli");
|
||||||
|
assert_eq!(filename_suffix, "log");
|
||||||
|
assert!(matches!(rotation, Rotation::DAILY));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_directory_names() {
|
||||||
|
ensure_logger_init();
|
||||||
|
|
||||||
|
// Test the directory names used in the logger setup
|
||||||
|
let rustfs_dir_name = "rustfs";
|
||||||
|
let logs_dir_name = "logs";
|
||||||
|
|
||||||
|
assert_eq!(rustfs_dir_name, "rustfs");
|
||||||
|
assert_eq!(logs_dir_name, "logs");
|
||||||
|
|
||||||
|
// Test path joining
|
||||||
|
let combined = format!("{rustfs_dir_name}/{logs_dir_name}");
|
||||||
|
assert_eq!(combined, "rustfs/logs");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_layer_settings() {
|
||||||
|
ensure_logger_init();
|
||||||
|
|
||||||
|
// Test the boolean settings used in layer configuration
|
||||||
|
let console_ansi = true;
|
||||||
|
let console_line_number = true;
|
||||||
|
let file_ansi = false;
|
||||||
|
let file_thread_names = true;
|
||||||
|
let file_target = true;
|
||||||
|
let file_thread_ids = true;
|
||||||
|
let file_level = true;
|
||||||
|
let file_line_number = true;
|
||||||
|
|
||||||
|
// Verify the settings
|
||||||
|
assert!(console_ansi);
|
||||||
|
assert!(console_line_number);
|
||||||
|
assert!(!file_ansi);
|
||||||
|
assert!(file_thread_names);
|
||||||
|
assert!(file_target);
|
||||||
|
assert!(file_thread_ids);
|
||||||
|
assert!(file_level);
|
||||||
|
assert!(file_line_number);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The actual init_logger() function is not tested here because:
|
||||||
|
// 1. It initializes a global tracing subscriber which can only be done once
|
||||||
|
// 2. It requires file system access to create directories
|
||||||
|
// 3. It has side effects that would interfere with other tests
|
||||||
|
// 4. It returns a WorkerGuard that needs to be kept alive
|
||||||
|
//
|
||||||
|
// This function should be tested in integration tests where:
|
||||||
|
// - File system access can be properly controlled
|
||||||
|
// - The global state can be managed
|
||||||
|
// - The actual logging behavior can be verified
|
||||||
|
// - The WorkerGuard lifecycle can be properly managed
|
||||||
|
}
|
||||||
21
cli/rustfs-gui/src/utils/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod helper;
|
||||||
|
mod logger;
|
||||||
|
|
||||||
|
pub use config::RustFSConfig;
|
||||||
|
pub use helper::ServiceManager;
|
||||||
|
pub use logger::init_logger;
|
||||||
38
cli/rustfs-gui/src/views/app.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::route::Route;
|
||||||
|
use dioxus::logger::tracing::info;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||||
|
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
||||||
|
|
||||||
|
/// The main application component
|
||||||
|
/// This is the root component of the application
|
||||||
|
/// It contains the global resources and the router
|
||||||
|
/// for the application
|
||||||
|
#[component]
|
||||||
|
pub fn App() -> Element {
|
||||||
|
// Build cool things ✌️
|
||||||
|
use document::{Link, Title};
|
||||||
|
info!("App rendered");
|
||||||
|
rsx! {
|
||||||
|
// Global app resources
|
||||||
|
Link { rel: "icon", href: FAVICON }
|
||||||
|
Link { rel: "stylesheet", href: TAILWIND_CSS }
|
||||||
|
Title { "RustFS" }
|
||||||
|
Router::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
cli/rustfs-gui/src/views/home.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::components::Home;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn HomeViews() -> Element {
|
||||||
|
rsx! {
|
||||||
|
Home {}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
cli/rustfs-gui/src/views/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod home;
|
||||||
|
mod setting;
|
||||||
|
|
||||||
|
pub use app::App;
|
||||||
|
pub use home::HomeViews;
|
||||||
|
pub use setting::SettingViews;
|
||||||
23
cli/rustfs-gui/src/views/setting.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::components::Setting;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn SettingViews() -> Element {
|
||||||
|
rsx! {
|
||||||
|
Setting {}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
cli/rustfs-gui/tailwind.config.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2024 RustFS Team
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: "all",
|
||||||
|
content: ["./src/**/*.{rs,html,css}", "./dist/**/*.html"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
41
crates/ahm/Cargo.toml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
[package]
|
||||||
|
name = "rustfs-ahm"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
authors = ["RustFS Team"]
|
||||||
|
license.workspace = true
|
||||||
|
description = "RustFS AHM (Automatic Health Management) Scanner"
|
||||||
|
repository.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
documentation = "https://docs.rs/rustfs-ahm/latest/rustfs_ahm/"
|
||||||
|
keywords = ["RustFS", "AHM", "health-management", "scanner", "Minio"]
|
||||||
|
categories = ["web-programming", "development-tools", "filesystem"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rustfs-ecstore = { workspace = true }
|
||||||
|
rustfs-common = { workspace = true }
|
||||||
|
rustfs-filemeta = { workspace = true }
|
||||||
|
rustfs-madmin = { workspace = true }
|
||||||
|
rustfs-utils = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
tokio-util = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
bytes = { workspace = true }
|
||||||
|
time = { workspace = true, features = ["serde"] }
|
||||||
|
uuid = { workspace = true, features = ["v4", "serde"] }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
async-trait = { workspace = true }
|
||||||
|
futures = { workspace = true }
|
||||||
|
url = { workspace = true }
|
||||||
|
rustfs-lock = { workspace = true }
|
||||||
|
|
||||||
|
lazy_static = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rmp-serde = { workspace = true }
|
||||||
|
tokio-test = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
45
crates/ahm/src/error.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("I/O error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("Storage error: {0}")]
|
||||||
|
Storage(#[from] rustfs_ecstore::error::Error),
|
||||||
|
|
||||||
|
#[error("Configuration error: {0}")]
|
||||||
|
Config(String),
|
||||||
|
|
||||||
|
#[error("Scanner error: {0}")]
|
||||||
|
Scanner(String),
|
||||||
|
|
||||||
|
#[error("Metrics error: {0}")]
|
||||||
|
Metrics(String),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Other(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
// Implement conversion from ahm::Error to std::io::Error for use in main.rs
|
||||||
|
impl From<Error> for std::io::Error {
|
||||||
|
fn from(err: Error) -> Self {
|
||||||
|
std::io::Error::other(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
54
crates/ahm/src/lib.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
pub mod scanner;
|
||||||
|
|
||||||
|
pub use error::{Error, Result};
|
||||||
|
pub use scanner::{
|
||||||
|
BucketTargetUsageInfo, BucketUsageInfo, DataUsageInfo, Scanner, ScannerMetrics, load_data_usage_from_backend,
|
||||||
|
store_data_usage_in_backend,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global cancellation token for AHM services (scanner and other background tasks)
|
||||||
|
static GLOBAL_AHM_SERVICES_CANCEL_TOKEN: OnceLock<CancellationToken> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Initialize the global AHM services cancellation token
|
||||||
|
pub fn init_ahm_services_cancel_token(cancel_token: CancellationToken) -> Result<()> {
|
||||||
|
GLOBAL_AHM_SERVICES_CANCEL_TOKEN
|
||||||
|
.set(cancel_token)
|
||||||
|
.map_err(|_| Error::Config("AHM services cancel token already initialized".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the global AHM services cancellation token
|
||||||
|
pub fn get_ahm_services_cancel_token() -> Option<&'static CancellationToken> {
|
||||||
|
GLOBAL_AHM_SERVICES_CANCEL_TOKEN.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create and initialize the global AHM services cancellation token
|
||||||
|
pub fn create_ahm_services_cancel_token() -> CancellationToken {
|
||||||
|
let cancel_token = CancellationToken::new();
|
||||||
|
init_ahm_services_cancel_token(cancel_token.clone()).expect("AHM services cancel token already initialized");
|
||||||
|
cancel_token
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shutdown all AHM services gracefully
|
||||||
|
pub fn shutdown_ahm_services() {
|
||||||
|
if let Some(cancel_token) = GLOBAL_AHM_SERVICES_CANCEL_TOKEN.get() {
|
||||||
|
cancel_token.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
1247
crates/ahm/src/scanner/data_scanner.rs
Normal file
671
crates/ahm/src/scanner/data_usage.rs
Normal file
@@ -0,0 +1,671 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::{collections::HashMap, sync::Arc, time::SystemTime};
|
||||||
|
|
||||||
|
use rustfs_ecstore::{bucket::metadata_sys::get_replication_config, config::com::read_config, store::ECStore};
|
||||||
|
use rustfs_utils::path::SLASH_SEPARATOR;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
|
// Data usage storage constants
|
||||||
|
pub const DATA_USAGE_ROOT: &str = SLASH_SEPARATOR;
|
||||||
|
const DATA_USAGE_OBJ_NAME: &str = ".usage.json";
|
||||||
|
const DATA_USAGE_BLOOM_NAME: &str = ".bloomcycle.bin";
|
||||||
|
pub const DATA_USAGE_CACHE_NAME: &str = ".usage-cache.bin";
|
||||||
|
|
||||||
|
// Data usage storage paths
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
pub static ref DATA_USAGE_BUCKET: String = format!("{}{}{}",
|
||||||
|
rustfs_ecstore::disk::RUSTFS_META_BUCKET,
|
||||||
|
SLASH_SEPARATOR,
|
||||||
|
rustfs_ecstore::disk::BUCKET_META_PREFIX
|
||||||
|
);
|
||||||
|
pub static ref DATA_USAGE_OBJ_NAME_PATH: String = format!("{}{}{}",
|
||||||
|
rustfs_ecstore::disk::BUCKET_META_PREFIX,
|
||||||
|
SLASH_SEPARATOR,
|
||||||
|
DATA_USAGE_OBJ_NAME
|
||||||
|
);
|
||||||
|
pub static ref DATA_USAGE_BLOOM_NAME_PATH: String = format!("{}{}{}",
|
||||||
|
rustfs_ecstore::disk::BUCKET_META_PREFIX,
|
||||||
|
SLASH_SEPARATOR,
|
||||||
|
DATA_USAGE_BLOOM_NAME
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bucket target usage info provides replication statistics
|
||||||
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BucketTargetUsageInfo {
|
||||||
|
pub replication_pending_size: u64,
|
||||||
|
pub replication_failed_size: u64,
|
||||||
|
pub replicated_size: u64,
|
||||||
|
pub replica_size: u64,
|
||||||
|
pub replication_pending_count: u64,
|
||||||
|
pub replication_failed_count: u64,
|
||||||
|
pub replicated_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bucket usage info provides bucket-level statistics
|
||||||
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BucketUsageInfo {
|
||||||
|
pub size: u64,
|
||||||
|
// Following five fields suffixed with V1 are here for backward compatibility
|
||||||
|
// Total Size for objects that have not yet been replicated
|
||||||
|
pub replication_pending_size_v1: u64,
|
||||||
|
// Total size for objects that have witness one or more failures and will be retried
|
||||||
|
pub replication_failed_size_v1: u64,
|
||||||
|
// Total size for objects that have been replicated to destination
|
||||||
|
pub replicated_size_v1: u64,
|
||||||
|
// Total number of objects pending replication
|
||||||
|
pub replication_pending_count_v1: u64,
|
||||||
|
// Total number of objects that failed replication
|
||||||
|
pub replication_failed_count_v1: u64,
|
||||||
|
|
||||||
|
pub objects_count: u64,
|
||||||
|
pub object_size_histogram: HashMap<String, u64>,
|
||||||
|
pub object_versions_histogram: HashMap<String, u64>,
|
||||||
|
pub versions_count: u64,
|
||||||
|
pub delete_markers_count: u64,
|
||||||
|
pub replica_size: u64,
|
||||||
|
pub replica_count: u64,
|
||||||
|
pub replication_info: HashMap<String, BucketTargetUsageInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DataUsageInfo represents data usage stats of the underlying storage
|
||||||
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DataUsageInfo {
|
||||||
|
/// Total capacity
|
||||||
|
pub total_capacity: u64,
|
||||||
|
/// Total used capacity
|
||||||
|
pub total_used_capacity: u64,
|
||||||
|
/// Total free capacity
|
||||||
|
pub total_free_capacity: u64,
|
||||||
|
|
||||||
|
/// LastUpdate is the timestamp of when the data usage info was last updated
|
||||||
|
pub last_update: Option<SystemTime>,
|
||||||
|
|
||||||
|
/// Objects total count across all buckets
|
||||||
|
pub objects_total_count: u64,
|
||||||
|
/// Versions total count across all buckets
|
||||||
|
pub versions_total_count: u64,
|
||||||
|
/// Delete markers total count across all buckets
|
||||||
|
pub delete_markers_total_count: u64,
|
||||||
|
/// Objects total size across all buckets
|
||||||
|
pub objects_total_size: u64,
|
||||||
|
/// Replication info across all buckets
|
||||||
|
pub replication_info: HashMap<String, BucketTargetUsageInfo>,
|
||||||
|
|
||||||
|
/// Total number of buckets in this cluster
|
||||||
|
pub buckets_count: u64,
|
||||||
|
/// Buckets usage info provides following information across all buckets
|
||||||
|
pub buckets_usage: HashMap<String, BucketUsageInfo>,
|
||||||
|
/// Deprecated kept here for backward compatibility reasons
|
||||||
|
pub bucket_sizes: HashMap<String, u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Size summary for a single object or group of objects
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct SizeSummary {
|
||||||
|
/// Total size
|
||||||
|
pub total_size: usize,
|
||||||
|
/// Number of versions
|
||||||
|
pub versions: usize,
|
||||||
|
/// Number of delete markers
|
||||||
|
pub delete_markers: usize,
|
||||||
|
/// Replicated size
|
||||||
|
pub replicated_size: usize,
|
||||||
|
/// Replicated count
|
||||||
|
pub replicated_count: usize,
|
||||||
|
/// Pending size
|
||||||
|
pub pending_size: usize,
|
||||||
|
/// Failed size
|
||||||
|
pub failed_size: usize,
|
||||||
|
/// Replica size
|
||||||
|
pub replica_size: usize,
|
||||||
|
/// Replica count
|
||||||
|
pub replica_count: usize,
|
||||||
|
/// Pending count
|
||||||
|
pub pending_count: usize,
|
||||||
|
/// Failed count
|
||||||
|
pub failed_count: usize,
|
||||||
|
/// Replication target stats
|
||||||
|
pub repl_target_stats: HashMap<String, ReplTargetSizeSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replication target size summary
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct ReplTargetSizeSummary {
|
||||||
|
/// Replicated size
|
||||||
|
pub replicated_size: usize,
|
||||||
|
/// Replicated count
|
||||||
|
pub replicated_count: usize,
|
||||||
|
/// Pending size
|
||||||
|
pub pending_size: usize,
|
||||||
|
/// Failed size
|
||||||
|
pub failed_size: usize,
|
||||||
|
/// Pending count
|
||||||
|
pub pending_count: usize,
|
||||||
|
/// Failed count
|
||||||
|
pub failed_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataUsageInfo {
|
||||||
|
/// Create a new DataUsageInfo
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add object metadata to data usage statistics
|
||||||
|
pub fn add_object(&mut self, object_path: &str, meta_object: &rustfs_filemeta::MetaObject) {
|
||||||
|
// This method is kept for backward compatibility
|
||||||
|
// For accurate version counting, use add_object_from_file_meta instead
|
||||||
|
let bucket_name = match self.extract_bucket_from_path(object_path) {
|
||||||
|
Ok(name) => name,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update bucket statistics
|
||||||
|
if let Some(bucket_usage) = self.buckets_usage.get_mut(&bucket_name) {
|
||||||
|
bucket_usage.size += meta_object.size as u64;
|
||||||
|
bucket_usage.objects_count += 1;
|
||||||
|
bucket_usage.versions_count += 1; // Simplified: assume 1 version per object
|
||||||
|
|
||||||
|
// Update size histogram
|
||||||
|
let total_size = meta_object.size as u64;
|
||||||
|
let size_ranges = [
|
||||||
|
("0-1KB", 0, 1024),
|
||||||
|
("1KB-1MB", 1024, 1024 * 1024),
|
||||||
|
("1MB-10MB", 1024 * 1024, 10 * 1024 * 1024),
|
||||||
|
("10MB-100MB", 10 * 1024 * 1024, 100 * 1024 * 1024),
|
||||||
|
("100MB-1GB", 100 * 1024 * 1024, 1024 * 1024 * 1024),
|
||||||
|
("1GB+", 1024 * 1024 * 1024, u64::MAX),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (range_name, min_size, max_size) in size_ranges {
|
||||||
|
if total_size >= min_size && total_size < max_size {
|
||||||
|
*bucket_usage.object_size_histogram.entry(range_name.to_string()).or_insert(0) += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update version histogram (simplified - count as single version)
|
||||||
|
*bucket_usage
|
||||||
|
.object_versions_histogram
|
||||||
|
.entry("SINGLE_VERSION".to_string())
|
||||||
|
.or_insert(0) += 1;
|
||||||
|
} else {
|
||||||
|
// Create new bucket usage
|
||||||
|
let mut bucket_usage = BucketUsageInfo {
|
||||||
|
size: meta_object.size as u64,
|
||||||
|
objects_count: 1,
|
||||||
|
versions_count: 1,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
bucket_usage.object_size_histogram.insert("0-1KB".to_string(), 1);
|
||||||
|
bucket_usage.object_versions_histogram.insert("SINGLE_VERSION".to_string(), 1);
|
||||||
|
self.buckets_usage.insert(bucket_name, bucket_usage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update global statistics
|
||||||
|
self.objects_total_size += meta_object.size as u64;
|
||||||
|
self.objects_total_count += 1;
|
||||||
|
self.versions_total_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add object from FileMeta for accurate version counting
|
||||||
|
pub fn add_object_from_file_meta(&mut self, object_path: &str, file_meta: &rustfs_filemeta::FileMeta) {
|
||||||
|
let bucket_name = match self.extract_bucket_from_path(object_path) {
|
||||||
|
Ok(name) => name,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate accurate statistics from all versions
|
||||||
|
let mut total_size = 0u64;
|
||||||
|
let mut versions_count = 0u64;
|
||||||
|
let mut delete_markers_count = 0u64;
|
||||||
|
let mut latest_object_size = 0u64;
|
||||||
|
|
||||||
|
// Process all versions to get accurate counts
|
||||||
|
for version in &file_meta.versions {
|
||||||
|
match rustfs_filemeta::FileMetaVersion::try_from(version.clone()) {
|
||||||
|
Ok(ver) => {
|
||||||
|
if let Some(obj) = ver.object {
|
||||||
|
total_size += obj.size as u64;
|
||||||
|
versions_count += 1;
|
||||||
|
latest_object_size = obj.size as u64; // Keep track of latest object size
|
||||||
|
} else if ver.delete_marker.is_some() {
|
||||||
|
delete_markers_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Skip invalid versions
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update bucket statistics
|
||||||
|
if let Some(bucket_usage) = self.buckets_usage.get_mut(&bucket_name) {
|
||||||
|
bucket_usage.size += total_size;
|
||||||
|
bucket_usage.objects_count += 1;
|
||||||
|
bucket_usage.versions_count += versions_count;
|
||||||
|
bucket_usage.delete_markers_count += delete_markers_count;
|
||||||
|
|
||||||
|
// Update size histogram based on latest object size
|
||||||
|
let size_ranges = [
|
||||||
|
("0-1KB", 0, 1024),
|
||||||
|
("1KB-1MB", 1024, 1024 * 1024),
|
||||||
|
("1MB-10MB", 1024 * 1024, 10 * 1024 * 1024),
|
||||||
|
("10MB-100MB", 10 * 1024 * 1024, 100 * 1024 * 1024),
|
||||||
|
("100MB-1GB", 100 * 1024 * 1024, 1024 * 1024 * 1024),
|
||||||
|
("1GB+", 1024 * 1024 * 1024, u64::MAX),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (range_name, min_size, max_size) in size_ranges {
|
||||||
|
if latest_object_size >= min_size && latest_object_size < max_size {
|
||||||
|
*bucket_usage.object_size_histogram.entry(range_name.to_string()).or_insert(0) += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update version histogram based on actual version count
|
||||||
|
let version_ranges = [
|
||||||
|
("1", 1, 1),
|
||||||
|
("2-5", 2, 5),
|
||||||
|
("6-10", 6, 10),
|
||||||
|
("11-50", 11, 50),
|
||||||
|
("51-100", 51, 100),
|
||||||
|
("100+", 101, usize::MAX),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (range_name, min_versions, max_versions) in version_ranges {
|
||||||
|
if versions_count as usize >= min_versions && versions_count as usize <= max_versions {
|
||||||
|
*bucket_usage
|
||||||
|
.object_versions_histogram
|
||||||
|
.entry(range_name.to_string())
|
||||||
|
.or_insert(0) += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new bucket usage
|
||||||
|
let mut bucket_usage = BucketUsageInfo {
|
||||||
|
size: total_size,
|
||||||
|
objects_count: 1,
|
||||||
|
versions_count,
|
||||||
|
delete_markers_count,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set size histogram
|
||||||
|
let size_ranges = [
|
||||||
|
("0-1KB", 0, 1024),
|
||||||
|
("1KB-1MB", 1024, 1024 * 1024),
|
||||||
|
("1MB-10MB", 1024 * 1024, 10 * 1024 * 1024),
|
||||||
|
("10MB-100MB", 10 * 1024 * 1024, 100 * 1024 * 1024),
|
||||||
|
("100MB-1GB", 100 * 1024 * 1024, 1024 * 1024 * 1024),
|
||||||
|
("1GB+", 1024 * 1024 * 1024, u64::MAX),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (range_name, min_size, max_size) in size_ranges {
|
||||||
|
if latest_object_size >= min_size && latest_object_size < max_size {
|
||||||
|
bucket_usage.object_size_histogram.insert(range_name.to_string(), 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set version histogram
|
||||||
|
let version_ranges = [
|
||||||
|
("1", 1, 1),
|
||||||
|
("2-5", 2, 5),
|
||||||
|
("6-10", 6, 10),
|
||||||
|
("11-50", 11, 50),
|
||||||
|
("51-100", 51, 100),
|
||||||
|
("100+", 101, usize::MAX),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (range_name, min_versions, max_versions) in version_ranges {
|
||||||
|
if versions_count as usize >= min_versions && versions_count as usize <= max_versions {
|
||||||
|
bucket_usage.object_versions_histogram.insert(range_name.to_string(), 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buckets_usage.insert(bucket_name, bucket_usage);
|
||||||
|
// Update buckets count when adding new bucket
|
||||||
|
self.buckets_count = self.buckets_usage.len() as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update global statistics
|
||||||
|
self.objects_total_size += total_size;
|
||||||
|
self.objects_total_count += 1;
|
||||||
|
self.versions_total_count += versions_count;
|
||||||
|
self.delete_markers_total_count += delete_markers_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract bucket name from object path
|
||||||
|
fn extract_bucket_from_path(&self, object_path: &str) -> Result<String> {
|
||||||
|
let parts: Vec<&str> = object_path.split('/').collect();
|
||||||
|
if parts.is_empty() {
|
||||||
|
return Err(Error::Scanner("Invalid object path: empty".to_string()));
|
||||||
|
}
|
||||||
|
Ok(parts[0].to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update capacity information
|
||||||
|
pub fn update_capacity(&mut self, total: u64, used: u64, free: u64) {
|
||||||
|
self.total_capacity = total;
|
||||||
|
self.total_used_capacity = used;
|
||||||
|
self.total_free_capacity = free;
|
||||||
|
self.last_update = Some(SystemTime::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add bucket usage info
|
||||||
|
pub fn add_bucket_usage(&mut self, bucket: String, usage: BucketUsageInfo) {
|
||||||
|
self.buckets_usage.insert(bucket.clone(), usage);
|
||||||
|
self.buckets_count = self.buckets_usage.len() as u64;
|
||||||
|
self.last_update = Some(SystemTime::now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get bucket usage info
|
||||||
|
pub fn get_bucket_usage(&self, bucket: &str) -> Option<&BucketUsageInfo> {
|
||||||
|
self.buckets_usage.get(bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate total statistics from all buckets
|
||||||
|
pub fn calculate_totals(&mut self) {
|
||||||
|
self.objects_total_count = 0;
|
||||||
|
self.versions_total_count = 0;
|
||||||
|
self.delete_markers_total_count = 0;
|
||||||
|
self.objects_total_size = 0;
|
||||||
|
|
||||||
|
for usage in self.buckets_usage.values() {
|
||||||
|
self.objects_total_count += usage.objects_count;
|
||||||
|
self.versions_total_count += usage.versions_count;
|
||||||
|
self.delete_markers_total_count += usage.delete_markers_count;
|
||||||
|
self.objects_total_size += usage.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge another DataUsageInfo into this one
|
||||||
|
pub fn merge(&mut self, other: &DataUsageInfo) {
|
||||||
|
// Merge bucket usage
|
||||||
|
for (bucket, usage) in &other.buckets_usage {
|
||||||
|
if let Some(existing) = self.buckets_usage.get_mut(bucket) {
|
||||||
|
existing.merge(usage);
|
||||||
|
} else {
|
||||||
|
self.buckets_usage.insert(bucket.clone(), usage.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate totals
|
||||||
|
self.calculate_totals();
|
||||||
|
|
||||||
|
// Ensure buckets_count stays consistent with buckets_usage
|
||||||
|
self.buckets_count = self.buckets_usage.len() as u64;
|
||||||
|
|
||||||
|
// Update last update time
|
||||||
|
if let Some(other_update) = other.last_update {
|
||||||
|
if self.last_update.is_none() || other_update > self.last_update.unwrap() {
|
||||||
|
self.last_update = Some(other_update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BucketUsageInfo {
|
||||||
|
/// Create a new BucketUsageInfo
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add size summary to this bucket usage
|
||||||
|
pub fn add_size_summary(&mut self, summary: &SizeSummary) {
|
||||||
|
self.size += summary.total_size as u64;
|
||||||
|
self.versions_count += summary.versions as u64;
|
||||||
|
self.delete_markers_count += summary.delete_markers as u64;
|
||||||
|
self.replica_size += summary.replica_size as u64;
|
||||||
|
self.replica_count += summary.replica_count as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge another BucketUsageInfo into this one
|
||||||
|
pub fn merge(&mut self, other: &BucketUsageInfo) {
|
||||||
|
self.size += other.size;
|
||||||
|
self.objects_count += other.objects_count;
|
||||||
|
self.versions_count += other.versions_count;
|
||||||
|
self.delete_markers_count += other.delete_markers_count;
|
||||||
|
self.replica_size += other.replica_size;
|
||||||
|
self.replica_count += other.replica_count;
|
||||||
|
|
||||||
|
// Merge histograms
|
||||||
|
for (key, value) in &other.object_size_histogram {
|
||||||
|
*self.object_size_histogram.entry(key.clone()).or_insert(0) += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, value) in &other.object_versions_histogram {
|
||||||
|
*self.object_versions_histogram.entry(key.clone()).or_insert(0) += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge replication info
|
||||||
|
for (target, info) in &other.replication_info {
|
||||||
|
let entry = self.replication_info.entry(target.clone()).or_default();
|
||||||
|
entry.replicated_size += info.replicated_size;
|
||||||
|
entry.replica_size += info.replica_size;
|
||||||
|
entry.replication_pending_size += info.replication_pending_size;
|
||||||
|
entry.replication_failed_size += info.replication_failed_size;
|
||||||
|
entry.replication_pending_count += info.replication_pending_count;
|
||||||
|
entry.replication_failed_count += info.replication_failed_count;
|
||||||
|
entry.replicated_count += info.replicated_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge backward compatibility fields
|
||||||
|
self.replication_pending_size_v1 += other.replication_pending_size_v1;
|
||||||
|
self.replication_failed_size_v1 += other.replication_failed_size_v1;
|
||||||
|
self.replicated_size_v1 += other.replicated_size_v1;
|
||||||
|
self.replication_pending_count_v1 += other.replication_pending_count_v1;
|
||||||
|
self.replication_failed_count_v1 += other.replication_failed_count_v1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SizeSummary {
|
||||||
|
/// Create a new SizeSummary
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add another SizeSummary to this one
|
||||||
|
pub fn add(&mut self, other: &SizeSummary) {
|
||||||
|
self.total_size += other.total_size;
|
||||||
|
self.versions += other.versions;
|
||||||
|
self.delete_markers += other.delete_markers;
|
||||||
|
self.replicated_size += other.replicated_size;
|
||||||
|
self.replicated_count += other.replicated_count;
|
||||||
|
self.pending_size += other.pending_size;
|
||||||
|
self.failed_size += other.failed_size;
|
||||||
|
self.replica_size += other.replica_size;
|
||||||
|
self.replica_count += other.replica_count;
|
||||||
|
self.pending_count += other.pending_count;
|
||||||
|
self.failed_count += other.failed_count;
|
||||||
|
|
||||||
|
// Merge replication target stats
|
||||||
|
for (target, stats) in &other.repl_target_stats {
|
||||||
|
let entry = self.repl_target_stats.entry(target.clone()).or_default();
|
||||||
|
entry.replicated_size += stats.replicated_size;
|
||||||
|
entry.replicated_count += stats.replicated_count;
|
||||||
|
entry.pending_size += stats.pending_size;
|
||||||
|
entry.failed_size += stats.failed_size;
|
||||||
|
entry.pending_count += stats.pending_count;
|
||||||
|
entry.failed_count += stats.failed_count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store data usage info to backend storage
|
||||||
|
pub async fn store_data_usage_in_backend(data_usage_info: DataUsageInfo, store: Arc<ECStore>) -> Result<()> {
|
||||||
|
let data =
|
||||||
|
serde_json::to_vec(&data_usage_info).map_err(|e| Error::Config(format!("Failed to serialize data usage info: {e}")))?;
|
||||||
|
|
||||||
|
// Save to backend using the same mechanism as original code
|
||||||
|
rustfs_ecstore::config::com::save_config(store, &DATA_USAGE_OBJ_NAME_PATH, data)
|
||||||
|
.await
|
||||||
|
.map_err(Error::Storage)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load data usage info from backend storage
|
||||||
|
pub async fn load_data_usage_from_backend(store: Arc<ECStore>) -> Result<DataUsageInfo> {
|
||||||
|
let buf = match read_config(store, &DATA_USAGE_OBJ_NAME_PATH).await {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to read data usage info from backend: {}", e);
|
||||||
|
if e == rustfs_ecstore::error::Error::ConfigNotFound {
|
||||||
|
return Ok(DataUsageInfo::default());
|
||||||
|
}
|
||||||
|
return Err(Error::Storage(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut data_usage_info: DataUsageInfo =
|
||||||
|
serde_json::from_slice(&buf).map_err(|e| Error::Config(format!("Failed to deserialize data usage info: {e}")))?;
|
||||||
|
|
||||||
|
warn!("Loaded data usage info from backend {:?}", &data_usage_info);
|
||||||
|
|
||||||
|
// Handle backward compatibility like original code
|
||||||
|
if data_usage_info.buckets_usage.is_empty() {
|
||||||
|
data_usage_info.buckets_usage = data_usage_info
|
||||||
|
.bucket_sizes
|
||||||
|
.iter()
|
||||||
|
.map(|(bucket, &size)| {
|
||||||
|
(
|
||||||
|
bucket.clone(),
|
||||||
|
BucketUsageInfo {
|
||||||
|
size,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if data_usage_info.bucket_sizes.is_empty() {
|
||||||
|
data_usage_info.bucket_sizes = data_usage_info
|
||||||
|
.buckets_usage
|
||||||
|
.iter()
|
||||||
|
.map(|(bucket, bui)| (bucket.clone(), bui.size))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (bucket, bui) in &data_usage_info.buckets_usage {
|
||||||
|
if bui.replicated_size_v1 > 0
|
||||||
|
|| bui.replication_failed_count_v1 > 0
|
||||||
|
|| bui.replication_failed_size_v1 > 0
|
||||||
|
|| bui.replication_pending_count_v1 > 0
|
||||||
|
{
|
||||||
|
if let Ok((cfg, _)) = get_replication_config(bucket).await {
|
||||||
|
if !cfg.role.is_empty() {
|
||||||
|
data_usage_info.replication_info.insert(
|
||||||
|
cfg.role.clone(),
|
||||||
|
BucketTargetUsageInfo {
|
||||||
|
replication_failed_size: bui.replication_failed_size_v1,
|
||||||
|
replication_failed_count: bui.replication_failed_count_v1,
|
||||||
|
replicated_size: bui.replicated_size_v1,
|
||||||
|
replication_pending_count: bui.replication_pending_count_v1,
|
||||||
|
replication_pending_size: bui.replication_pending_size_v1,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data_usage_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Example function showing how to use AHM data usage functionality
|
||||||
|
/// This demonstrates the integration pattern for DataUsageInfoHandler
|
||||||
|
pub async fn example_data_usage_integration() -> Result<()> {
|
||||||
|
// Get the global storage instance
|
||||||
|
let Some(store) = rustfs_ecstore::new_object_layer_fn() else {
|
||||||
|
return Err(Error::Config("Storage not initialized".to_string()));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load data usage from backend (this replaces the original load_data_usage_from_backend)
|
||||||
|
let data_usage = load_data_usage_from_backend(store).await?;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Loaded data usage info: {} buckets, {} total objects",
|
||||||
|
data_usage.buckets_count, data_usage.objects_total_count
|
||||||
|
);
|
||||||
|
|
||||||
|
// Example: Store updated data usage back to backend
|
||||||
|
// This would typically be called by the scanner after collecting new statistics
|
||||||
|
// store_data_usage_in_backend(data_usage, store).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_data_usage_info_creation() {
|
||||||
|
let mut info = DataUsageInfo::new();
|
||||||
|
info.update_capacity(1000, 500, 500);
|
||||||
|
|
||||||
|
assert_eq!(info.total_capacity, 1000);
|
||||||
|
assert_eq!(info.total_used_capacity, 500);
|
||||||
|
assert_eq!(info.total_free_capacity, 500);
|
||||||
|
assert!(info.last_update.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bucket_usage_info_merge() {
|
||||||
|
let mut usage1 = BucketUsageInfo::new();
|
||||||
|
usage1.size = 100;
|
||||||
|
usage1.objects_count = 10;
|
||||||
|
usage1.versions_count = 5;
|
||||||
|
|
||||||
|
let mut usage2 = BucketUsageInfo::new();
|
||||||
|
usage2.size = 200;
|
||||||
|
usage2.objects_count = 20;
|
||||||
|
usage2.versions_count = 10;
|
||||||
|
|
||||||
|
usage1.merge(&usage2);
|
||||||
|
|
||||||
|
assert_eq!(usage1.size, 300);
|
||||||
|
assert_eq!(usage1.objects_count, 30);
|
||||||
|
assert_eq!(usage1.versions_count, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_size_summary_add() {
|
||||||
|
let mut summary1 = SizeSummary::new();
|
||||||
|
summary1.total_size = 100;
|
||||||
|
summary1.versions = 5;
|
||||||
|
|
||||||
|
let mut summary2 = SizeSummary::new();
|
||||||
|
summary2.total_size = 200;
|
||||||
|
summary2.versions = 10;
|
||||||
|
|
||||||
|
summary1.add(&summary2);
|
||||||
|
|
||||||
|
assert_eq!(summary1.total_size, 300);
|
||||||
|
assert_eq!(summary1.versions, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||