Compare commits
1912 Commits
v1.3.1
...
kyle/patch
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2b63fbed03 | ||
![]() |
e969735955 | ||
![]() |
45bb29a393 | ||
![]() |
f38bca290f | ||
![]() |
fb8ed5b529 | ||
![]() |
09e13e9b43 | ||
![]() |
13e1e8e504 | ||
![]() |
acab03ad77 | ||
![]() |
0a6c15f702 | ||
![]() |
589ce9c28e | ||
![]() |
f716c74ef7 | ||
![]() |
2d7a6220cd | ||
![]() |
e0b26d455c | ||
![]() |
06d246e3fd | ||
![]() |
67b26a5b69 | ||
![]() |
b4b9db7ffa | ||
![]() |
cc037d23c4 | ||
![]() |
9c9c036956 | ||
![]() |
7fdbc439f7 | ||
![]() |
9410669294 | ||
![]() |
497bb0e2cb | ||
![]() |
a42be8ee74 | ||
![]() |
16b50d2a71 | ||
![]() |
882b385c88 | ||
![]() |
059a36659e | ||
![]() |
cd9cf09422 | ||
![]() |
02a4067118 | ||
![]() |
6fae328f1f | ||
![]() |
81b0eed4d4 | ||
![]() |
b786c8bc10 | ||
![]() |
856b426dc9 | ||
![]() |
197a8f9c57 | ||
![]() |
bc4ee48c1b | ||
![]() |
0d9ac71088 | ||
![]() |
a0fc9bbd68 | ||
![]() |
7e0519df9a | ||
![]() |
bf0360e7f4 | ||
![]() |
62bae7c52e | ||
![]() |
802f5b2980 | ||
![]() |
496f131c4b | ||
![]() |
f582b0215c | ||
![]() |
4c3c4babea | ||
![]() |
6ec0550b4c | ||
![]() |
4e9039c244 | ||
![]() |
e479b95d72 | ||
![]() |
926ff2b754 | ||
![]() |
394b64319d | ||
![]() |
96fa53b6ee | ||
![]() |
9b54e2af0b | ||
![]() |
b01cf3c2e1 | ||
![]() |
46307d85d8 | ||
![]() |
772df8f5e7 | ||
![]() |
04fa3dcd8c | ||
![]() |
6538864de4 | ||
![]() |
480adc3426 | ||
![]() |
c11db0a279 | ||
![]() |
6f7570d265 | ||
![]() |
ae976894a3 | ||
![]() |
cd00f78c05 | ||
![]() |
3c1dd6ce29 | ||
![]() |
5099c6ff21 | ||
![]() |
c63c98b80a | ||
![]() |
6834dae281 | ||
![]() |
df7854111a | ||
![]() |
c0404597c0 | ||
![]() |
64eafdc6f0 | ||
![]() |
b51418814f | ||
![]() |
748f9d9147 | ||
![]() |
5c8c4ea412 | ||
![]() |
e6d33eda2b | ||
![]() |
324102bc73 | ||
![]() |
e6182ff807 | ||
![]() |
7ee174edce | ||
![]() |
cbb4810260 | ||
![]() |
c3257216c2 | ||
![]() |
a140faaebe | ||
![]() |
79200d1f79 | ||
![]() |
10e2da2c00 | ||
![]() |
85a49a221f | ||
![]() |
1bc64bbaf8 | ||
![]() |
180bd29afa | ||
![]() |
48ddafd120 | ||
![]() |
851219f835 | ||
![]() |
286c74b41b | ||
![]() |
8a0711e2a6 | ||
![]() |
887ec42847 | ||
![]() |
62c3c35526 | ||
![]() |
1a368f55fa | ||
![]() |
19d1e093fc | ||
![]() |
407138c999 | ||
![]() |
b5536bfc7f | ||
![]() |
72af77860b | ||
![]() |
8e63be2efe | ||
![]() |
5f014e163e | ||
![]() |
bd88e5a1ca | ||
![]() |
5bd4e06cb9 | ||
![]() |
46c406e8c1 | ||
![]() |
615e035a5d | ||
![]() |
7616c41564 | ||
![]() |
d5ba1ea5e1 | ||
![]() |
54d3636a22 | ||
![]() |
45f6ee667d | ||
![]() |
d25eda9a7d | ||
![]() |
86d43a536f | ||
![]() |
6c417e35a1 | ||
![]() |
2b728d3c52 | ||
![]() |
f3f7ecb852 | ||
![]() |
41fca03c98 | ||
![]() |
10caf8f1b6 | ||
![]() |
7420283249 | ||
![]() |
453952440f | ||
![]() |
2475d576c7 | ||
![]() |
8cd6da1260 | ||
![]() |
82dd4fc1d1 | ||
![]() |
33fe4b2c1a | ||
![]() |
b1c1188107 | ||
![]() |
63b45aefae | ||
![]() |
f79cb0fac5 | ||
![]() |
ec42892c7c | ||
![]() |
371716fe6a | ||
![]() |
d5fb6bec15 | ||
![]() |
c5e7bf26d7 | ||
![]() |
e3072ac416 | ||
![]() |
dfaf06e4cf | ||
![]() |
6e24d25576 | ||
![]() |
b59b171e43 | ||
![]() |
28726584c2 | ||
![]() |
00b151311a | ||
![]() |
36c813714b | ||
![]() |
2ae6764dd9 | ||
![]() |
debefc9652 | ||
![]() |
b068b847c7 | ||
![]() |
6c410c07ce | ||
![]() |
c01206c1f3 | ||
![]() |
2e85fb45de | ||
![]() |
67513e384d | ||
![]() |
828dafa493 | ||
![]() |
5c5a761222 | ||
![]() |
fab10e5fc5 | ||
![]() |
797345fc1c | ||
![]() |
a0388a43c3 | ||
![]() |
f5b0a3023b | ||
![]() |
dc1d7bd1fd | ||
![]() |
9d674321b6 | ||
![]() |
f9c8378d6a | ||
![]() |
65da751a52 | ||
![]() |
72142be0de | ||
![]() |
11cea7c926 | ||
![]() |
8d46ee4c22 | ||
![]() |
d63c09e2c2 | ||
![]() |
c9e9d7d109 | ||
![]() |
2412d20eb4 | ||
![]() |
7f90d23a12 | ||
![]() |
b9a82be29b | ||
![]() |
638673ba5e | ||
![]() |
898fe4f216 | ||
![]() |
7e805662d1 | ||
![]() |
baf59c73ac | ||
![]() |
38ad9c97c6 | ||
![]() |
8fc574f059 | ||
![]() |
78b0f00e88 | ||
![]() |
0f10f2d483 | ||
![]() |
eb5f5bbb9e | ||
![]() |
67d26ff790 | ||
![]() |
17f2008d88 | ||
![]() |
db1bf7e488 | ||
![]() |
4b786b8a9f | ||
![]() |
fdfa0d3258 | ||
![]() |
757aa77d89 | ||
![]() |
d70ea06565 | ||
![]() |
f2ebd10053 | ||
![]() |
cd67b442c9 | ||
![]() |
852c83c4fb | ||
![]() |
e3b2ee3b83 | ||
![]() |
927a026b86 | ||
![]() |
c851e1d54f | ||
![]() |
e6fdca171f | ||
![]() |
c9cfb87733 | ||
![]() |
b0b7c53294 | ||
![]() |
e8dc6579fe | ||
![]() |
f0747abe3f | ||
![]() |
32fab87340 | ||
![]() |
adcd8e0325 | ||
![]() |
7b5808eb2b | ||
![]() |
a8f7422cf5 | ||
![]() |
5ae9a26361 | ||
![]() |
cf1fdb8c5f | ||
![]() |
bf7ebde100 | ||
![]() |
88c5fa5035 | ||
![]() |
887b0dd538 | ||
![]() |
364d1db56a | ||
![]() |
c431222909 | ||
![]() |
55a0f68b97 | ||
![]() |
af2563dfc2 | ||
![]() |
33f8851083 | ||
![]() |
fe1f19b9fa | ||
![]() |
871cf3ec0a | ||
![]() |
686a5837b6 | ||
![]() |
23f2dd5ce5 | ||
![]() |
d5d39b395b | ||
![]() |
2acad790d3 | ||
![]() |
30133306d6 | ||
![]() |
04a62f2ad8 | ||
![]() |
17858a7d72 | ||
![]() |
620307568f | ||
![]() |
a349c5d9ac | ||
![]() |
0d740ad12d | ||
![]() |
85a3f28869 | ||
![]() |
dbd5397405 | ||
![]() |
85c183b9ef | ||
![]() |
0d0af1d1dd | ||
![]() |
ad423fc187 | ||
![]() |
d8b2a7a236 | ||
![]() |
13ec8cbe98 | ||
![]() |
a7cae23612 | ||
![]() |
86bb093f3d | ||
![]() |
997e1eb2f2 | ||
![]() |
34cc8b8933 | ||
![]() |
f26b074811 | ||
![]() |
adaa07b4b0 | ||
![]() |
96f4569342 | ||
![]() |
be190c6c80 | ||
![]() |
809617a82d | ||
![]() |
df02732002 | ||
![]() |
d35f3c3049 | ||
![]() |
8b047e3b14 | ||
![]() |
fa41d21e27 | ||
![]() |
54e6c5e2c1 | ||
![]() |
43fc3dd7eb | ||
![]() |
12a1340c8e | ||
![]() |
cf8b5790a1 | ||
![]() |
659d85a833 | ||
![]() |
96c44d31c9 | ||
![]() |
ba812b4f64 | ||
![]() |
4087258fbd | ||
![]() |
955be13129 | ||
![]() |
32011c0dea | ||
![]() |
b68325c71c | ||
![]() |
84bce86fce | ||
![]() |
d68eab1dda | ||
![]() |
09cf014d14 | ||
![]() |
d5bab5805f | ||
![]() |
b5ab500a14 | ||
![]() |
49253d37c9 | ||
![]() |
97cf3b25ad | ||
![]() |
99862b95a5 | ||
![]() |
8b765d58e5 | ||
![]() |
8f566e45b8 | ||
![]() |
b8af86e30c | ||
![]() |
784f193b6d | ||
![]() |
3967adb1b5 | ||
![]() |
0667d1110f | ||
![]() |
61dd22bdf3 | ||
![]() |
24eb8b05b0 | ||
![]() |
6991a4950b | ||
![]() |
bb169cf674 | ||
![]() |
e5d0d2e262 | ||
![]() |
72b4d4f4fa | ||
![]() |
9b2f2eb4c3 | ||
![]() |
ce52ef95a9 | ||
![]() |
aa3756ad17 | ||
![]() |
73081e726d | ||
![]() |
d53dc4149b | ||
![]() |
0d5bb4935f | ||
![]() |
14aeb0060b | ||
![]() |
239726f3ce | ||
![]() |
4ed3002716 | ||
![]() |
7286fba240 | ||
![]() |
895c306fb7 | ||
![]() |
f3844d56e2 | ||
![]() |
540dc3150a | ||
![]() |
035c8dfec3 | ||
![]() |
03d6a011db | ||
![]() |
27f64650f9 | ||
![]() |
ccca009972 | ||
![]() |
57a6ceff0e | ||
![]() |
30c4baa58b | ||
![]() |
a930d77064 | ||
![]() |
0d1cfffa5c | ||
![]() |
3c7422764c | ||
![]() |
55176b9f8f | ||
![]() |
156b9314b5 | ||
![]() |
76d22280dc | ||
![]() |
e4251a3862 | ||
![]() |
831339bd2c | ||
![]() |
952ea80e15 | ||
![]() |
813c497e4b | ||
![]() |
1b5b647135 | ||
![]() |
7de99003ca | ||
![]() |
e09bdd734b | ||
![]() |
306e087ec6 | ||
![]() |
c6b0178a87 | ||
![]() |
4e581ea1ce | ||
![]() |
26dc2d19e5 | ||
![]() |
b99282acfb | ||
![]() |
4e48724d0c | ||
![]() |
448ce141d5 | ||
![]() |
695f287190 | ||
![]() |
4de3271e15 | ||
![]() |
77b33b127d | ||
![]() |
9cd13ba381 | ||
![]() |
9df23c8a3f | ||
![]() |
e3618b939e | ||
![]() |
6a39f5869a | ||
![]() |
fd472efadc | ||
![]() |
7e2c2eae63 | ||
![]() |
5266571ca4 | ||
![]() |
797868c474 | ||
![]() |
2c2a5c7c2b | ||
![]() |
9e536d5337 | ||
![]() |
860e680dd9 | ||
![]() |
7bb52aa170 | ||
![]() |
1c370f9100 | ||
![]() |
ec7c772d0b | ||
![]() |
cc0285a77d | ||
![]() |
256d3550d1 | ||
![]() |
db3a5f3b0a | ||
![]() |
0e58edf113 | ||
![]() |
db136926a9 | ||
![]() |
d84e7211be | ||
![]() |
8357cc19d2 | ||
![]() |
2752b9fa95 | ||
![]() |
0214be4953 | ||
![]() |
a4f944e795 | ||
![]() |
cd2ebf15fc | ||
![]() |
7a7ea374e9 | ||
![]() |
330df325f9 | ||
![]() |
2fc0882b2e | ||
![]() |
4dd779e010 | ||
![]() |
3dc54405fe | ||
![]() |
3f1aa5bac3 | ||
![]() |
8f52fdb900 | ||
![]() |
1b93891ed8 | ||
![]() |
33adc8ecf8 | ||
![]() |
0455f7ea58 | ||
![]() |
ea5a167f4f | ||
![]() |
8a1c4a4cc8 | ||
![]() |
bd8bc81713 | ||
![]() |
98a5ddf58a | ||
![]() |
6223dbc541 | ||
![]() |
7c56621c57 | ||
![]() |
a61aa8e2be | ||
![]() |
7df4f9615b | ||
![]() |
5742452fdf | ||
![]() |
fe09f9f862 | ||
![]() |
3a4687ea0f | ||
![]() |
db6490fb1b | ||
![]() |
1642297101 | ||
![]() |
5ecd223cfc | ||
![]() |
306e40fd7b | ||
![]() |
b58249b9c8 | ||
![]() |
b23b4b34d0 | ||
![]() |
73bc921713 | ||
![]() |
f356e4c303 | ||
![]() |
9888167f2e | ||
![]() |
4561690478 | ||
![]() |
576113febf | ||
![]() |
cc139bf750 | ||
![]() |
ae91958c06 | ||
![]() |
33dea6267f | ||
![]() |
c9a8bca96f | ||
![]() |
8af376e608 | ||
![]() |
9ab307df4f | ||
![]() |
e8a716f8bb | ||
![]() |
a40e64f6cd | ||
![]() |
2e53feb38c | ||
![]() |
5945ab1f50 | ||
![]() |
59d67220d4 | ||
![]() |
61610ded84 | ||
![]() |
c87a132f40 | ||
![]() |
350d4de32b | ||
![]() |
f5f9025d6d | ||
![]() |
549d744d04 | ||
![]() |
6194460dc2 | ||
![]() |
8370f638b4 | ||
![]() |
ac34c51125 | ||
![]() |
b241470fe1 | ||
![]() |
179697040c | ||
![]() |
335766ed12 | ||
![]() |
4c5d052a71 | ||
![]() |
86423342a8 | ||
![]() |
d8b41552e3 | ||
![]() |
11c65fb392 | ||
![]() |
bed126506f | ||
![]() |
f6aeb52b16 | ||
![]() |
a5201942b8 | ||
![]() |
c1f7164273 | ||
![]() |
6774bf784c | ||
![]() |
56ec8b4eac | ||
![]() |
35868509af | ||
![]() |
3ab6749f49 | ||
![]() |
7a012a92bc | ||
![]() |
aba01825a0 | ||
![]() |
907a3308de | ||
![]() |
4366bb372f | ||
![]() |
a6f6cd4a19 | ||
![]() |
03ee428039 | ||
![]() |
8d652d064d | ||
![]() |
28adc34239 | ||
![]() |
120e9bf42f | ||
![]() |
59f54e194b | ||
![]() |
c4834e61a7 | ||
![]() |
e4d02bc561 | ||
![]() |
b9e54e39f7 | ||
![]() |
f3c32eac65 | ||
![]() |
9a303ab344 | ||
![]() |
9c1b55bebc | ||
![]() |
30ae4bbd86 | ||
![]() |
c6126a980a | ||
![]() |
ef90d048ca | ||
![]() |
b938132038 | ||
![]() |
3cb2e78fe7 | ||
![]() |
ea9875ddf0 | ||
![]() |
65dacd2ff2 | ||
![]() |
a190818827 | ||
![]() |
98e33b619b | ||
![]() |
685ed715ac | ||
![]() |
3cf3c4b398 | ||
![]() |
1c2ef4b1d4 | ||
![]() |
d22fc91585 | ||
![]() |
0a28af5c35 | ||
![]() |
0c1e5b88ef | ||
![]() |
790201be90 | ||
![]() |
d8d379f05e | ||
![]() |
b5e9701048 | ||
![]() |
292f86d6f5 | ||
![]() |
76ca9934c8 | ||
![]() |
37b826ee4e | ||
![]() |
1e1bd3c508 | ||
![]() |
00e8f11913 | ||
![]() |
a3c24a26a0 | ||
![]() |
4232928ad8 | ||
![]() |
b165fb78da | ||
![]() |
e9f6c5ead9 | ||
![]() |
b2d0c1c9dd | ||
![]() |
14d91400a4 | ||
![]() |
d0114aece7 | ||
![]() |
dff2df4aab | ||
![]() |
13159f93ee | ||
![]() |
3ece1fd841 | ||
![]() |
f46963b6b3 | ||
![]() |
b97f4e0031 | ||
![]() |
e2d233d74b | ||
![]() |
a7ca2e527b | ||
![]() |
396a053c0a | ||
![]() |
d1a3f1cb88 | ||
![]() |
9f96558cdd | ||
![]() |
b3bc589d70 | ||
![]() |
18d2c28110 | ||
![]() |
b272ef296d | ||
![]() |
32ca28a3a9 | ||
![]() |
4d5e5a3b0b | ||
![]() |
8b3f37102d | ||
![]() |
4b74253631 | ||
![]() |
a81b552b95 | ||
![]() |
53f53c0f75 | ||
![]() |
fdaf5c69d6 | ||
![]() |
061afca5d3 | ||
![]() |
ccb08a48f1 | ||
![]() |
a8f3d45b12 | ||
![]() |
7e333caaf9 | ||
![]() |
70229e8684 | ||
![]() |
261700389b | ||
![]() |
250aed2eb1 | ||
![]() |
ed1f008fe2 | ||
![]() |
e9ce270dab | ||
![]() |
1ee110bc95 | ||
![]() |
33dd07c675 | ||
![]() |
39ccbbeeda | ||
![]() |
55d2400ac7 | ||
![]() |
0bdea5c54c | ||
![]() |
3be372d49f | ||
![]() |
d0c66b2c48 | ||
![]() |
65082c4790 | ||
![]() |
e87ed9beed | ||
![]() |
bc5563d9c2 | ||
![]() |
ad83ab5dcc | ||
![]() |
0dc1cf9701 | ||
![]() |
11489c6538 | ||
![]() |
2619d4bc86 | ||
![]() |
3730efd350 | ||
![]() |
6ece32c546 | ||
![]() |
fd9996a3cc | ||
![]() |
f06cc89152 | ||
![]() |
c1d7ab3fa9 | ||
![]() |
b206483c7c | ||
![]() |
c3eb8c7b56 | ||
![]() |
0849d4f435 | ||
![]() |
1dba3ae19b | ||
![]() |
f33f2e3771 | ||
![]() |
e56aaed973 | ||
![]() |
a4659f038e | ||
![]() |
cd462818da | ||
![]() |
37769efbed | ||
![]() |
0f70c4bbce | ||
![]() |
48b5e8b9d9 | ||
![]() |
1f138f0ecc | ||
![]() |
73f67e99ca | ||
![]() |
9114da2445 | ||
![]() |
554bbcc780 | ||
![]() |
4db2289cfd | ||
![]() |
c15b56bc23 | ||
![]() |
9f52dda6ae | ||
![]() |
fadcefb11a | ||
![]() |
361c32913c | ||
![]() |
5c2042198e | ||
![]() |
99df53098c | ||
![]() |
aa563c87bd | ||
![]() |
1188888956 | ||
![]() |
f9d7991dc8 | ||
![]() |
53954e81fd | ||
![]() |
f82996bfd1 | ||
![]() |
b74069eb41 | ||
![]() |
e8c7591751 | ||
![]() |
3521b61a81 | ||
![]() |
93db90c725 | ||
![]() |
1dc56aed14 | ||
![]() |
d814202424 | ||
![]() |
c54856a616 | ||
![]() |
fc45df270a | ||
![]() |
3cde2faed0 | ||
![]() |
b4b8c89aad | ||
![]() |
36d05724c0 | ||
![]() |
b1e4e681d1 | ||
![]() |
3987078c11 | ||
![]() |
de0010eb72 | ||
![]() |
1f94f44b18 | ||
![]() |
fe0b45cae6 | ||
![]() |
c32e485f27 | ||
![]() |
409b78fc21 | ||
![]() |
2f08142f5a | ||
![]() |
8c4edaabba | ||
![]() |
05497ce85c | ||
![]() |
d3df2fe57e | ||
![]() |
a0f07082f2 | ||
![]() |
b7efa8e1f0 | ||
![]() |
3647457bb5 | ||
![]() |
2e5a39dcd8 | ||
![]() |
edabacfb3a | ||
![]() |
f46176fd10 | ||
![]() |
2158e20380 | ||
![]() |
fa593e33d1 | ||
![]() |
50730bd3df | ||
![]() |
4e68955981 | ||
![]() |
3c0084d012 | ||
![]() |
8bd11a01ae | ||
![]() |
da3a22d0f6 | ||
![]() |
e708212d41 | ||
![]() |
a5ceba8435 | ||
![]() |
446e8e1253 | ||
![]() |
c69b2fa053 | ||
![]() |
0597c0e908 | ||
![]() |
af2b6bc8ca | ||
![]() |
a2c7a542df | ||
![]() |
e37ae2743f | ||
![]() |
644d955f08 | ||
![]() |
e7b4f09021 | ||
![]() |
1e0a6a3129 | ||
![]() |
ef3b8915d8 | ||
![]() |
e58cfd89c5 | ||
![]() |
1c52379ee3 | ||
![]() |
e2c2b40690 | ||
![]() |
bddb89e4a1 | ||
![]() |
560ed91e2e | ||
![]() |
1f8f7ad7f8 | ||
![]() |
a2a0f2ef41 | ||
![]() |
40e5fb2287 | ||
![]() |
6c49c71b3f | ||
![]() |
deb3db0fff | ||
![]() |
4872fa3d6e | ||
![]() |
640a7409ee | ||
![]() |
a7637ad8d4 | ||
![]() |
31409c61ca | ||
![]() |
e74dc9eb60 | ||
![]() |
06997fdf29 | ||
![]() |
611e659626 | ||
![]() |
e484ae9837 | ||
![]() |
7e7ca9524e | ||
![]() |
db09b7440d | ||
![]() |
e9603505d2 | ||
![]() |
0f45f6aca1 | ||
![]() |
0a28a7794d | ||
![]() |
7c2644ec51 | ||
![]() |
ae62812c61 | ||
![]() |
68e24df52b | ||
![]() |
b9076d01af | ||
![]() |
78a5339e3e | ||
![]() |
b099770cb1 | ||
![]() |
b76366a514 | ||
![]() |
eeab351636 | ||
![]() |
3e45691d0b | ||
![]() |
f9d79521a1 | ||
![]() |
14a89b3b8a | ||
![]() |
8fa6e618c4 | ||
![]() |
093008dee7 | ||
![]() |
42838eba09 | ||
![]() |
aa72c5d3bb | ||
![]() |
bb04098062 | ||
![]() |
dda022df37 | ||
![]() |
377dfb8e22 | ||
![]() |
07befd0bd1 | ||
![]() |
2635a410df | ||
![]() |
5e5f82c4b5 | ||
![]() |
991cbf6b7f | ||
![]() |
688d697a99 | ||
![]() |
7894a67719 | ||
![]() |
7a7ea74984 | ||
![]() |
12cd8a39c1 | ||
![]() |
2c07538f8f | ||
![]() |
c9bfd350ed | ||
![]() |
a485d2b4df | ||
![]() |
8ed5ff25a5 | ||
![]() |
a17a919c37 | ||
![]() |
55cafb9268 | ||
![]() |
92dfdc6edd | ||
![]() |
fff9452509 | ||
![]() |
27e560c961 | ||
![]() |
34489f0d66 | ||
![]() |
b7f8c8368c | ||
![]() |
f383f0be6c | ||
![]() |
ff08cb44f9 | ||
![]() |
6cb914e969 | ||
![]() |
a24be20e95 | ||
![]() |
08716efbd5 | ||
![]() |
24c8ede746 | ||
![]() |
548475996c | ||
![]() |
7f9add3f1e | ||
![]() |
6eab47259e | ||
![]() |
46663e3a6f | ||
![]() |
9797a2152d | ||
![]() |
a7c3431556 | ||
![]() |
fef9cd7bec | ||
![]() |
b2c4f7a250 | ||
![]() |
88ae9fcbd1 | ||
![]() |
bc092114c1 | ||
![]() |
9f29dc8b76 | ||
![]() |
5fbaa3a3db | ||
![]() |
0c59168ceb | ||
![]() |
540fe90609 | ||
![]() |
1f44f3944f | ||
![]() |
fbacb8187d | ||
![]() |
ac2d4ae8f3 | ||
![]() |
a3322e9fd7 | ||
![]() |
281f119456 | ||
![]() |
140f3452ed | ||
![]() |
481be42eb5 | ||
![]() |
f2a37079eb | ||
![]() |
76fa698995 | ||
![]() |
f8e21e2338 | ||
![]() |
482c29bc2a | ||
![]() |
0bf1ec4958 | ||
![]() |
3b105d5a6a | ||
![]() |
6d9c81da43 | ||
![]() |
c2e23855b3 | ||
![]() |
3f59d35fb6 | ||
![]() |
44c74f33d9 | ||
![]() |
512785e0a9 | ||
![]() |
963fc574c3 | ||
![]() |
3218fc2c83 | ||
![]() |
dc9351713c | ||
![]() |
e72049d6e8 | ||
![]() |
170126a490 | ||
![]() |
7d53d0aadc | ||
![]() |
5eac622b8c | ||
![]() |
175e41de8d | ||
![]() |
61f4762341 | ||
![]() |
7c24d1486f | ||
![]() |
630f6c691c | ||
![]() |
5c5273bd6c | ||
![]() |
9bde38df5a | ||
![]() |
391e4444d4 | ||
![]() |
e5ee0f1961 | ||
![]() |
c8737806c0 | ||
![]() |
953f572b53 | ||
![]() |
05d0f7142d | ||
![]() |
ba29d76a00 | ||
![]() |
692274691e | ||
![]() |
394d3e0bf2 | ||
![]() |
784dd03ba7 | ||
![]() |
8560189a1e | ||
![]() |
098ca9a9a1 | ||
![]() |
3ca50a1e2d | ||
![]() |
00f193541d | ||
![]() |
8ce9eac704 | ||
![]() |
76086a8f91 | ||
![]() |
9b71772e35 | ||
![]() |
72e5631167 | ||
![]() |
339c7d56bd | ||
![]() |
ba16995070 | ||
![]() |
b32c4ee728 | ||
![]() |
14e4cbf749 | ||
![]() |
406955ca3e | ||
![]() |
5a45f8b122 | ||
![]() |
129e7e20e8 | ||
![]() |
7165a8190b | ||
![]() |
07fde0d73f | ||
![]() |
a360370c4e | ||
![]() |
92cd3d688d | ||
![]() |
6554ccf0f8 | ||
![]() |
9444f0e1b1 | ||
![]() |
d63f5eca0d | ||
![]() |
e39fed1f25 | ||
![]() |
2dc359b19c | ||
![]() |
7aec8a4ae2 | ||
![]() |
af9d3ba9f1 | ||
![]() |
b0c71b21b3 | ||
![]() |
71c7fbd3a2 | ||
![]() |
f8cc7c36b4 | ||
![]() |
5d95fcb81f | ||
![]() |
d481536a3f | ||
![]() |
62b42e9254 | ||
![]() |
03e3354d50 | ||
![]() |
f01f1ddd7a | ||
![]() |
2cb58bbbf0 | ||
![]() |
2aedea3139 | ||
![]() |
59e37182be | ||
![]() |
52bdd02a4b | ||
![]() |
b1376dfa73 | ||
![]() |
37ed18c38b | ||
![]() |
b7ad7bd729 | ||
![]() |
b43ec4414e | ||
![]() |
f05f9d33f9 | ||
![]() |
fcc9c5e577 | ||
![]() |
3259a8206f | ||
![]() |
3fa9611971 | ||
![]() |
b749c2d45a | ||
![]() |
29e2e9c657 | ||
![]() |
f983d88e52 | ||
![]() |
1449478c5b | ||
![]() |
7e7a669116 | ||
![]() |
28f9954661 | ||
![]() |
b7e82f7694 | ||
![]() |
f0bbd1a1cd | ||
![]() |
5f5c66e3f2 | ||
![]() |
2fc8b07e29 | ||
![]() |
bdb1690a49 | ||
![]() |
10b028355f | ||
![]() |
a4366556c0 | ||
![]() |
9c25cc663a | ||
![]() |
ba21da4f0b | ||
![]() |
34349f64d5 | ||
![]() |
f5c7dc932a | ||
![]() |
4880275e7b | ||
![]() |
6db0172a50 | ||
![]() |
95ff5c98a8 | ||
![]() |
e9f9f90137 | ||
![]() |
0ebfecc60c | ||
![]() |
afa29a0ed1 | ||
![]() |
8d707dc815 | ||
![]() |
5b509d147f | ||
![]() |
bb6d909949 | ||
![]() |
8513d63a3e | ||
![]() |
d2f3345c7a | ||
![]() |
aee3a0a281 | ||
![]() |
4752fa6dd2 | ||
![]() |
4e08cf3879 | ||
![]() |
11bda8e76a | ||
![]() |
e33172060f | ||
![]() |
0dee6a9888 | ||
![]() |
3d855dcbfc | ||
![]() |
ed3b7f7971 | ||
![]() |
c0a4fb16e2 | ||
![]() |
688e7fff9c | ||
![]() |
880e56e563 | ||
![]() |
bf26d4ec95 | ||
![]() |
d5df3de1d7 | ||
![]() |
5d005211d4 | ||
![]() |
cc08ac9236 | ||
![]() |
09a6a340c9 | ||
![]() |
2692104ccd | ||
![]() |
b1fd539d25 | ||
![]() |
33d48fe4f7 | ||
![]() |
1ebc8e9362 | ||
![]() |
8b5a5241f2 | ||
![]() |
959b9ffbac | ||
![]() |
c9cbe41f9e | ||
![]() |
d62cbdc391 | ||
![]() |
31bd8ce7ae | ||
![]() |
c25db503e0 | ||
![]() |
ac51e6aae3 | ||
![]() |
72c9d616fd | ||
![]() |
52f80293a2 | ||
![]() |
a3a9edd41a | ||
![]() |
f9cbff1eec | ||
![]() |
b71482284f | ||
![]() |
d90e3a816e | ||
![]() |
869d875b5f | ||
![]() |
fb1a360360 | ||
![]() |
943765bd4d | ||
![]() |
9280113350 | ||
![]() |
627adb1755 | ||
![]() |
ad421eae11 | ||
![]() |
b0af9d535a | ||
![]() |
5ab69dfb7f | ||
![]() |
f1214e6ffd | ||
![]() |
a09e029216 | ||
![]() |
8782c70640 | ||
![]() |
7099a36bdb | ||
![]() |
7bd8b0c152 | ||
![]() |
b1cbf54711 | ||
![]() |
84f003d907 | ||
![]() |
4257e696da | ||
![]() |
c5c0d46ab8 | ||
![]() |
b397240664 | ||
![]() |
a31e27ee06 | ||
![]() |
483d7b6e58 | ||
![]() |
bfb6133871 | ||
![]() |
917ff89d9d | ||
![]() |
ef5762864f | ||
![]() |
50586cdb42 | ||
![]() |
82a0b8de0c | ||
![]() |
6db800d6d2 | ||
![]() |
b23bfc2f6e | ||
![]() |
929279b35a | ||
![]() |
795e4bad5f | ||
![]() |
6e20b6034e | ||
![]() |
6f34a27bd3 | ||
![]() |
240a605977 | ||
![]() |
4a6a471345 | ||
![]() |
f1be4f50a3 | ||
![]() |
8ef32e8081 | ||
![]() |
71e02ef833 | ||
![]() |
c70d7e475d | ||
![]() |
0f31f05e61 | ||
![]() |
7971a003cc | ||
![]() |
49491b9b8c | ||
![]() |
ea11f17954 | ||
![]() |
ebd37b9e2f | ||
![]() |
5de4fe3d23 | ||
![]() |
eb47f1227a | ||
![]() |
f84cde7d04 | ||
![]() |
4d3eb2887c | ||
![]() |
bc631612df | ||
![]() |
5d8a211961 | ||
![]() |
e62add6893 | ||
![]() |
44fc429f64 | ||
![]() |
ffe281f25d | ||
![]() |
ba39ff433d | ||
![]() |
795b8614ad | ||
![]() |
745a2f1886 | ||
![]() |
9bf58c89d4 | ||
![]() |
ee62b9a4c7 | ||
![]() |
e6125b893d | ||
![]() |
83ed333fa5 | ||
![]() |
39ed67d667 | ||
![]() |
ac2e973cb0 | ||
![]() |
94a0be3b05 | ||
![]() |
124e8af649 | ||
![]() |
f07ed68d82 | ||
![]() |
8f39dbf6b1 | ||
![]() |
6dde9ee6c4 | ||
![]() |
dbe6fe442d | ||
![]() |
1e2ac86ac6 | ||
![]() |
83c5ba04cd | ||
![]() |
b3f25c176b | ||
![]() |
52cf6375eb | ||
![]() |
82a3c37c16 | ||
![]() |
d63df5a156 | ||
![]() |
63ad3739fd | ||
![]() |
7eddb16f2f | ||
![]() |
7c4f4cacc9 | ||
![]() |
dc6ad72b2d | ||
![]() |
be729c87af | ||
![]() |
4ee83d9da4 | ||
![]() |
8b2f06442a | ||
![]() |
21181f011f | ||
![]() |
b4b099ecb1 | ||
![]() |
166b30bb0a | ||
![]() |
8eeb81f58e | ||
![]() |
0b20a1eeaa | ||
![]() |
d8cb8f7815 | ||
![]() |
36f79593cf | ||
![]() |
1014b25bf5 | ||
![]() |
55dcfc1a85 | ||
![]() |
9b6a628d51 | ||
![]() |
8b5a42073d | ||
![]() |
7991d40760 | ||
![]() |
4203296414 | ||
![]() |
93d319275f | ||
![]() |
94d262263c | ||
![]() |
ed90f21188 | ||
![]() |
80e0231727 | ||
![]() |
981197583a | ||
![]() |
6f58344e7b | ||
![]() |
07be844985 | ||
![]() |
45262583e6 | ||
![]() |
c113e38531 | ||
![]() |
8771f311d7 | ||
![]() |
fdec65e9bd | ||
![]() |
f8b46dc647 | ||
![]() |
847e47b5db | ||
![]() |
227bad9e99 | ||
![]() |
cb8168de41 | ||
![]() |
c200a0c7ac | ||
![]() |
81e80572d8 | ||
![]() |
2aa6c83714 | ||
![]() |
a22ea0b82b | ||
![]() |
af64579eb2 | ||
![]() |
f2705a611d | ||
![]() |
990dcc9d5a | ||
![]() |
c09237f0c3 | ||
![]() |
571a3533fb | ||
![]() |
6fcd9e1595 | ||
![]() |
9caa42d257 | ||
![]() |
18fdbbaabb | ||
![]() |
7381c1c0cb | ||
![]() |
2bdcae7209 | ||
![]() |
fc694b90b6 | ||
![]() |
945cd7ff8e | ||
![]() |
3b32ca1e60 | ||
![]() |
98611267d5 | ||
![]() |
4d53002e5c | ||
![]() |
f6b7b0d3d2 | ||
![]() |
fbbd7ccf49 | ||
![]() |
d41ce65a78 | ||
![]() |
c477fd2071 | ||
![]() |
7fab8395c8 | ||
![]() |
7d72e0c046 | ||
![]() |
9ce97be6a4 | ||
![]() |
121b69b0c3 | ||
![]() |
cb7cc2f276 | ||
![]() |
d01849306e | ||
![]() |
a4e87982a6 | ||
![]() |
e1c3c80c0f | ||
![]() |
fd6346ed59 | ||
![]() |
2e4f7b5a8c | ||
![]() |
d812d4e12e | ||
![]() |
10b3f09e7e | ||
![]() |
2d3776844c | ||
![]() |
914a4574de | ||
![]() |
2b3c84f21a | ||
![]() |
f4eb1af8d0 | ||
![]() |
c01fc332d2 | ||
![]() |
b8fdbc3e94 | ||
![]() |
3c7c55364b | ||
![]() |
bff4355a1a | ||
![]() |
9ea57a7df1 | ||
![]() |
4c4171e7fb | ||
![]() |
77ece044ad | ||
![]() |
d633b36b23 | ||
![]() |
2eda6601c0 | ||
![]() |
6202393637 | ||
![]() |
1b76044242 | ||
![]() |
28648e27cf | ||
![]() |
90921a74ea | ||
![]() |
950b764ff1 | ||
![]() |
15ba30bf8f | ||
![]() |
c96654d50f | ||
![]() |
b5f175d220 | ||
![]() |
c535543922 | ||
![]() |
9913030e6f | ||
![]() |
e7f58fc7fa | ||
![]() |
746ee50027 | ||
![]() |
683c2da224 | ||
![]() |
2671c83337 | ||
![]() |
bd35c89c04 | ||
![]() |
616baecafb | ||
![]() |
bfe895c690 | ||
![]() |
97aff2eb4c | ||
![]() |
1c46ee2988 | ||
![]() |
d0d4ee843d | ||
![]() |
fd127da342 | ||
![]() |
a8728336ca | ||
![]() |
c0eb9bd1e9 | ||
![]() |
c85896845f | ||
![]() |
efe953d8cd | ||
![]() |
b5593ef5b2 | ||
![]() |
d08d2e00ee | ||
![]() |
bc8908cca1 | ||
![]() |
9109f0ccd5 | ||
![]() |
30c2ef58cd | ||
![]() |
23b295c7c1 | ||
![]() |
db24ee4d37 | ||
![]() |
e737a1edbd | ||
![]() |
109d84302c | ||
![]() |
e50974a86a | ||
![]() |
ef491e1e96 | ||
![]() |
f366a68159 | ||
![]() |
0377faadd6 | ||
![]() |
a5825373e1 | ||
![]() |
fadfadd9e9 | ||
![]() |
596b316d65 | ||
![]() |
c1e24406d9 | ||
![]() |
13dfb090b5 | ||
![]() |
ddd1ff0101 | ||
![]() |
b266a72726 | ||
![]() |
255fae3a90 | ||
![]() |
b4a60cfee2 | ||
![]() |
233a2e6400 | ||
![]() |
f31cb49e2a | ||
![]() |
47fd12e7a4 | ||
![]() |
d5eb679cf0 | ||
![]() |
26d0e46367 | ||
![]() |
146bfaa9de | ||
![]() |
315051c14c | ||
![]() |
3a7d770f6d | ||
![]() |
2cd60af841 | ||
![]() |
e2f5775b07 | ||
![]() |
c27be733a9 | ||
![]() |
54fda697ce | ||
![]() |
04e0b56dd5 | ||
![]() |
b71824c5e8 | ||
![]() |
65293ea5e4 | ||
![]() |
05c2f5bebd | ||
![]() |
e8b2255be0 | ||
![]() |
2c227d3475 | ||
![]() |
958f7b535a | ||
![]() |
9e34096139 | ||
![]() |
12b5536e22 | ||
![]() |
171a5b1793 | ||
![]() |
b4fb82066b | ||
![]() |
57145436ab | ||
![]() |
cba69ca467 | ||
![]() |
375fcab788 | ||
![]() |
de65c02222 | ||
![]() |
444b0beaca | ||
![]() |
4c931278b8 | ||
![]() |
3bdac794b3 | ||
![]() |
67eb593164 | ||
![]() |
fe230e7d30 | ||
![]() |
2f0ce3ee37 | ||
![]() |
992b8a6fb6 | ||
![]() |
84e45caa6c | ||
![]() |
68d9542816 | ||
![]() |
c9c9c50d6c | ||
![]() |
9f4e0ce920 | ||
![]() |
388852d6b7 | ||
![]() |
4e1f071951 | ||
![]() |
8e47829905 | ||
![]() |
84fe5004a9 | ||
![]() |
28b51a9b46 | ||
![]() |
07fc7af911 | ||
![]() |
330405ae42 | ||
![]() |
ffb26ba67f | ||
![]() |
fc597abbc9 | ||
![]() |
177f10f76d | ||
![]() |
a7a7f83e3e | ||
![]() |
b6fb44d6a5 | ||
![]() |
996c2b55a4 | ||
![]() |
21d9d31a27 | ||
![]() |
00536cba3a | ||
![]() |
641dde81e5 | ||
![]() |
8177e98014 | ||
![]() |
abfc6be84d | ||
![]() |
1d15d582d9 | ||
![]() |
5cd3c5fcc0 | ||
![]() |
5e568d7dd8 | ||
![]() |
c251bce44d | ||
![]() |
1408dd48a1 | ||
![]() |
a77734797a | ||
![]() |
a119ae7efa | ||
![]() |
7d284a7e18 | ||
![]() |
4d65bd9f1b | ||
![]() |
517511e5be | ||
![]() |
2ef38fe06d | ||
![]() |
b128d36121 | ||
![]() |
082025f0b6 | ||
![]() |
220b7f6d53 | ||
![]() |
062723bf15 | ||
![]() |
bcbbb64042 | ||
![]() |
59230a0f9e | ||
![]() |
18fb9c9de3 | ||
![]() |
26e827e4dc | ||
![]() |
2f828b1d39 | ||
![]() |
5b22fcc2f5 | ||
![]() |
4f36b00ec3 | ||
![]() |
707c20513e | ||
![]() |
cddd068887 | ||
![]() |
cf6863b2c6 | ||
![]() |
994d311ed3 | ||
![]() |
1098f8cb1e | ||
![]() |
1be1a2b8f7 | ||
![]() |
07a6e40917 | ||
![]() |
2c2057b5cb | ||
![]() |
caf09e7498 | ||
![]() |
9488468b67 | ||
![]() |
d071bf8ade | ||
![]() |
1626c01ff4 | ||
![]() |
3dd6895662 | ||
![]() |
0ab967b7a4 | ||
![]() |
3b07946065 | ||
![]() |
4c0a079d1e | ||
![]() |
1878b39e21 | ||
![]() |
7050111bf4 | ||
![]() |
572f7d826a | ||
![]() |
a155811678 | ||
![]() |
54ccee3c0f | ||
![]() |
88b7665b7f | ||
![]() |
a66007f8cc | ||
![]() |
d5f348c039 | ||
![]() |
c0d1899ad3 | ||
![]() |
ea14ef6314 | ||
![]() |
75e6f1e39a | ||
![]() |
f372fba1fd | ||
![]() |
d494cee0da | ||
![]() |
1b8380c5dc | ||
![]() |
1ee2eb05eb | ||
![]() |
9b82891abb | ||
![]() |
64a28f891f | ||
![]() |
c4944f31d6 | ||
![]() |
6fd696546c | ||
![]() |
e957dab993 | ||
![]() |
831e7af9ed | ||
![]() |
8ab779ffb9 | ||
![]() |
506f9bf0e0 | ||
![]() |
5151d751a3 | ||
![]() |
bde9a97b17 | ||
![]() |
cede823a33 | ||
![]() |
dda2f6eb70 | ||
![]() |
c54f2e08c2 | ||
![]() |
2a2e025ef7 | ||
![]() |
93ea4efb33 | ||
![]() |
284301a659 | ||
![]() |
8425dd9aa7 | ||
![]() |
02bd8ed459 | ||
![]() |
25a7cf18cf | ||
![]() |
003929754d | ||
![]() |
f6c0172257 | ||
![]() |
75be3a3778 | ||
![]() |
5cfb95e8ea | ||
![]() |
8c2c4e233a | ||
![]() |
f4ac4dee60 | ||
![]() |
1b4053d959 | ||
![]() |
8df5d972fc | ||
![]() |
3a68d84376 | ||
![]() |
865ea0ddd2 | ||
![]() |
54dca31d66 | ||
![]() |
17103570f1 | ||
![]() |
b5d04a2031 | ||
![]() |
86238af380 | ||
![]() |
d10073a052 | ||
![]() |
b99b0d4bf8 | ||
![]() |
27b5b1bf10 | ||
![]() |
bab9069dee | ||
![]() |
52a3258814 | ||
![]() |
da548f59d1 | ||
![]() |
ecc500907c | ||
![]() |
724dade1f6 | ||
![]() |
c5dc869c03 | ||
![]() |
273f7e4535 | ||
![]() |
a58e060138 | ||
![]() |
ef4d2fcc72 | ||
![]() |
330c06d926 | ||
![]() |
be9c36828a | ||
![]() |
17f83135c5 | ||
![]() |
543ba51d3c | ||
![]() |
33df23fc8c | ||
![]() |
3236d6b934 | ||
![]() |
e0e7775367 | ||
![]() |
198679583c | ||
![]() |
6dae2a604f | ||
![]() |
68905c6ae4 | ||
![]() |
26630c4d64 | ||
![]() |
d382f030f0 | ||
![]() |
08fca87b2f | ||
![]() |
33441a1c5c | ||
![]() |
6d8346b13a | ||
![]() |
d9b340ca45 | ||
![]() |
ebbc52ee1f | ||
![]() |
de5bee29ef | ||
![]() |
25f843ec0b | ||
![]() |
df600a9e14 | ||
![]() |
156c25cea1 | ||
![]() |
f7dd04e3de | ||
![]() |
3036d86cfa | ||
![]() |
3fccd52884 | ||
![]() |
00640274fc | ||
![]() |
a7e8fb98b3 | ||
![]() |
bed6643437 | ||
![]() |
f815e8511f | ||
![]() |
6360fd42e7 | ||
![]() |
62a9656888 | ||
![]() |
ffb89c7e5b | ||
![]() |
aa52735006 | ||
![]() |
b65526d8ee | ||
![]() |
ea8e2999ae | ||
![]() |
0b5017f992 | ||
![]() |
8bf1bdaa04 | ||
![]() |
01eb3b1c94 | ||
![]() |
3402c9f601 | ||
![]() |
13c3518c5e | ||
![]() |
821fad27dc | ||
![]() |
50a34e2f4c | ||
![]() |
2a19b2afbe | ||
![]() |
dc92d010fb | ||
![]() |
9cb27a616a | ||
![]() |
518a0ca45b | ||
![]() |
3526a0e3c5 | ||
![]() |
6386f85258 | ||
![]() |
e80106d8f8 | ||
![]() |
1145cbc75c | ||
![]() |
e669b81072 | ||
![]() |
9d78da941b | ||
![]() |
63d0f5e2c6 | ||
![]() |
dae047eff1 | ||
![]() |
8a2db8bced | ||
![]() |
792fab20e6 | ||
![]() |
f40c0f6bd3 | ||
![]() |
ccf11b9861 | ||
![]() |
1fcde5a17c | ||
![]() |
88f543dd25 | ||
![]() |
8b6f3f6022 | ||
![]() |
294ef8045a | ||
![]() |
1f7e4c886b | ||
![]() |
63c047009f | ||
![]() |
2c5f5004cc | ||
![]() |
2fa5426cf5 | ||
![]() |
428c777402 | ||
![]() |
7e2c62c520 | ||
![]() |
3d3b4f4a46 | ||
![]() |
a543dcf166 | ||
![]() |
5de54bb6bf | ||
![]() |
d95401e614 | ||
![]() |
2c835437e9 | ||
![]() |
498e70ed2b | ||
![]() |
fce5b500bf | ||
![]() |
11def54adb | ||
![]() |
9da9e73f7a | ||
![]() |
f90cd49a6d | ||
![]() |
6e72c07190 | ||
![]() |
1997e1faeb | ||
![]() |
b33b34bd71 | ||
![]() |
6a9b739541 | ||
![]() |
6cb0bdd1a4 | ||
![]() |
b73ebb6f92 | ||
![]() |
24a83260ca | ||
![]() |
fc1c1b402b | ||
![]() |
af462b3486 | ||
![]() |
3e236996c8 | ||
![]() |
c4e84483df | ||
![]() |
e9532f2158 | ||
![]() |
15fc8ab2e7 | ||
![]() |
b39dbeb25e | ||
![]() |
e181c21f85 | ||
![]() |
db771bc2cc | ||
![]() |
0695cfb3c0 | ||
![]() |
ff3982efa4 | ||
![]() |
40de7f5d54 | ||
![]() |
18a848696f | ||
![]() |
58de7375a2 | ||
![]() |
b61109a269 | ||
![]() |
164fd8f022 | ||
![]() |
cafaa9ff22 | ||
![]() |
34c98d1dcd | ||
![]() |
ec015da795 | ||
![]() |
a21a82c64e | ||
![]() |
1aca669c62 | ||
![]() |
39573ada54 | ||
![]() |
bceb7c77d1 | ||
![]() |
68fa771905 | ||
![]() |
8a85968ec4 | ||
![]() |
03b1a2dcff | ||
![]() |
5cb231e427 | ||
![]() |
e90f6764f5 | ||
![]() |
d078055e40 | ||
![]() |
98134eb612 | ||
![]() |
b0f2dfc4fb | ||
![]() |
195f07c09f | ||
![]() |
15f87edc96 | ||
![]() |
52caae8f05 | ||
![]() |
99f7fc99da | ||
![]() |
89fc98c6f6 | ||
![]() |
0c2eb1caab | ||
![]() |
303969bdbf | ||
![]() |
8b7266a435 | ||
![]() |
fc9282fff7 | ||
![]() |
33fb79e0de | ||
![]() |
b4418660df | ||
![]() |
3560a22a77 | ||
![]() |
818b466687 | ||
![]() |
91d29c1e2c | ||
![]() |
e06fe72131 | ||
![]() |
7d715fdca0 | ||
![]() |
3cfa6988ab | ||
![]() |
1c707b2d37 | ||
![]() |
2ed4c76abf | ||
![]() |
94e91723f4 | ||
![]() |
091bddbad8 | ||
![]() |
b3f68c2638 | ||
![]() |
00c94c8efd | ||
![]() |
66b19677bf | ||
![]() |
2d4170e0d7 | ||
![]() |
7f8f38ddf1 | ||
![]() |
2e1763f19a | ||
![]() |
a5f8b3c164 | ||
![]() |
a802087009 | ||
![]() |
3b16c06f70 | ||
![]() |
a979ae3ced | ||
![]() |
02e0f40702 | ||
![]() |
a4d7853bb2 | ||
![]() |
ac463e0f65 | ||
![]() |
c187aa46b7 | ||
![]() |
e65fa9dcae | ||
![]() |
fea230cfab | ||
![]() |
90838c99fc | ||
![]() |
8e96adeda9 | ||
![]() |
3cdb0f840e | ||
![]() |
b6ad6e0a85 | ||
![]() |
e73a577452 | ||
![]() |
16e8aa2447 | ||
![]() |
1d6958a67e | ||
![]() |
136ca282eb | ||
![]() |
388fc2f7d9 | ||
![]() |
72fdb7127a | ||
![]() |
f67832644f | ||
![]() |
2614f3261c | ||
![]() |
e84204b49a | ||
![]() |
86aaa725d5 | ||
![]() |
dd583a176f | ||
![]() |
5299d958f2 | ||
![]() |
f0374cf9d9 | ||
![]() |
6b6a0d7b4f | ||
![]() |
4317892421 | ||
![]() |
8052b2adfa | ||
![]() |
d44927447a | ||
![]() |
09e6c6422d | ||
![]() |
df85ffb254 | ||
![]() |
752ba4405c | ||
![]() |
1f3a02b83d | ||
![]() |
8e372f1e93 | ||
![]() |
caeb84f58b | ||
![]() |
759722bf7d | ||
![]() |
cefdc95c9b | ||
![]() |
3be7029078 | ||
![]() |
3f16858a93 | ||
![]() |
c61c6deaa8 | ||
![]() |
90c8483df8 | ||
![]() |
d70b189cec | ||
![]() |
41a7fc4de5 | ||
![]() |
db119d5230 | ||
![]() |
c88245954d | ||
![]() |
b1ab3834b6 | ||
![]() |
34b7c1be81 | ||
![]() |
082c77586f | ||
![]() |
3b6fe7b548 | ||
![]() |
da072e7621 | ||
![]() |
4568404a70 | ||
![]() |
43319853ef | ||
![]() |
ce9f142621 | ||
![]() |
8ef3add183 | ||
![]() |
ef45696015 | ||
![]() |
a8f8c2cd85 | ||
![]() |
6d79a8e23a | ||
![]() |
d65dc6ccac | ||
![]() |
ccc9076a80 | ||
![]() |
3c007cea34 | ||
![]() |
bf29312ecf | ||
![]() |
a53095b29e | ||
![]() |
d5c9e6b054 | ||
![]() |
d4f29bd2af | ||
![]() |
a6661ac759 | ||
![]() |
6aa83819d1 | ||
![]() |
8ff4c8a4a5 | ||
![]() |
1d77b8dae7 | ||
![]() |
3e0062621b | ||
![]() |
b20e220910 | ||
![]() |
0259572ded | ||
![]() |
a78866069b | ||
![]() |
b8fc83577c | ||
![]() |
c21969ab4e | ||
![]() |
e02cfc4529 | ||
![]() |
ac07c63631 | ||
![]() |
1054bc995b | ||
![]() |
3560ab6387 | ||
![]() |
e50274b962 | ||
![]() |
bb1773a1a1 | ||
![]() |
941dc3b1b4 | ||
![]() |
92d08d24ab | ||
![]() |
7f6ffe0f73 | ||
![]() |
66457ca0c7 | ||
![]() |
3344f1fd88 | ||
![]() |
c4b636f80a | ||
![]() |
0ea0975bd4 | ||
![]() |
534f3a7469 | ||
![]() |
f3110ba018 | ||
![]() |
e76cc81fe1 | ||
![]() |
adcc3343ec | ||
![]() |
645e114a1f | ||
![]() |
65d86460cb | ||
![]() |
aaccd10c2a | ||
![]() |
a237bfd930 | ||
![]() |
73f64d93b1 | ||
![]() |
871db09447 | ||
![]() |
8d79103392 | ||
![]() |
fd765443e4 | ||
![]() |
63967d1558 | ||
![]() |
6b270885bf | ||
![]() |
47937d6aaa | ||
![]() |
7d2ba45620 | ||
![]() |
b270d819a8 | ||
![]() |
c50553fbf6 | ||
![]() |
a541c863be | ||
![]() |
254b482651 | ||
![]() |
d3c2cd4215 | ||
![]() |
4f7cc7dd6b | ||
![]() |
21f1f4e503 | ||
![]() |
47f2336673 | ||
![]() |
5ae93bf6d0 | ||
![]() |
caf5f10326 | ||
![]() |
e68dbcf4ee | ||
![]() |
a42e81cf8c | ||
![]() |
98a8588c1b | ||
![]() |
8630af7646 | ||
![]() |
268c5302e8 | ||
![]() |
d07d535993 | ||
![]() |
a8a75f22b2 | ||
![]() |
6143023502 | ||
![]() |
ca6aa5d4aa | ||
![]() |
bc028ed41f | ||
![]() |
911d3a9188 | ||
![]() |
b88e715fc5 | ||
![]() |
a4dfa5f281 | ||
![]() |
05fc1711b9 | ||
![]() |
c16fbb5b47 | ||
![]() |
7ca3e2b519 | ||
![]() |
8c8a0bf8eb | ||
![]() |
e85251d2e3 | ||
![]() |
73e4827249 | ||
![]() |
c37270ea08 | ||
![]() |
8cc33b46bb | ||
![]() |
700341f9cc | ||
![]() |
ca85ad5995 | ||
![]() |
1c8c36a224 | ||
![]() |
25b814e796 | ||
![]() |
e89566e04a | ||
![]() |
e946f388c0 | ||
![]() |
7616e8dab7 | ||
![]() |
2dc4fef4d3 | ||
![]() |
f2ca997195 | ||
![]() |
7445004abf | ||
![]() |
9b76abe2ed | ||
![]() |
a97205c9fc | ||
![]() |
bf3d069aad | ||
![]() |
27ea74722c | ||
![]() |
2525456d8b | ||
![]() |
9fa32df3a6 | ||
![]() |
5bdd5da13b | ||
![]() |
b8756edd29 | ||
![]() |
c93910e858 | ||
![]() |
ad4226ace7 | ||
![]() |
d71b3fe1bc | ||
![]() |
07858eecac | ||
![]() |
64ec6d0e58 | ||
![]() |
cf722427ab | ||
![]() |
1b7ff07efc | ||
![]() |
87533f4417 | ||
![]() |
9077c95cdd | ||
![]() |
22acc5ae96 | ||
![]() |
cf596a88ab | ||
![]() |
7354fa3050 | ||
![]() |
978497b287 | ||
![]() |
a52d745250 | ||
![]() |
aae71f8105 | ||
![]() |
5419b4b732 | ||
![]() |
caf5a8917c | ||
![]() |
beec00dcb3 | ||
![]() |
7565e809b0 | ||
![]() |
5c9a646bc1 | ||
![]() |
dd8ef288f7 | ||
![]() |
db8d2953cb | ||
![]() |
b75ca26db2 | ||
![]() |
948a04122a | ||
![]() |
b7c4562b85 | ||
![]() |
6d0fea1983 | ||
![]() |
1a158a919a | ||
![]() |
9a83bd4267 | ||
![]() |
7e3f516b04 | ||
![]() |
afd888e14d | ||
![]() |
76af6e975e | ||
![]() |
2017df9ec6 | ||
![]() |
aa1e83dc24 | ||
![]() |
20996b153d | ||
![]() |
b298e53fc4 | ||
![]() |
7fb382bee0 | ||
![]() |
2158772e3b | ||
![]() |
333298e6c3 | ||
![]() |
6e9deeba5b | ||
![]() |
15951509a7 | ||
![]() |
dd8b7e42d6 | ||
![]() |
2655c86be3 | ||
![]() |
c4c4d347cf | ||
![]() |
a3f7239c1b | ||
![]() |
f90c2ad74e | ||
![]() |
2907cd173b | ||
![]() |
779ee8294f | ||
![]() |
a229c9e10e | ||
![]() |
3f8d2e4242 | ||
![]() |
c1a8b0c303 | ||
![]() |
8e92c5b844 | ||
![]() |
c366fbde22 | ||
![]() |
890e866ae2 | ||
![]() |
8eb11a8957 | ||
![]() |
9cc65a386b | ||
![]() |
e4d4af1587 | ||
![]() |
b3aab5116a | ||
![]() |
7227c76538 | ||
![]() |
c2c59f4a9e | ||
![]() |
eeede318fd | ||
![]() |
3855bb4d56 | ||
![]() |
89fa682721 | ||
![]() |
cb701a7bbc | ||
![]() |
407325b8ce | ||
![]() |
161debb35a | ||
![]() |
fd5385b127 | ||
![]() |
e85e1410aa | ||
![]() |
ac068f353a | ||
![]() |
fe7c6d0d57 | ||
![]() |
abf1e4a8ac | ||
![]() |
f9dfb2d0c7 | ||
![]() |
da23740f17 | ||
![]() |
47f81251d7 | ||
![]() |
7e01eca7f5 | ||
![]() |
941b5c9e45 | ||
![]() |
207c0d612d | ||
![]() |
4035176d88 | ||
![]() |
b9f9968f84 | ||
![]() |
7a2dac8d5b | ||
![]() |
2fb8ad146f | ||
![]() |
2f4a7352d9 | ||
![]() |
c3ff030542 | ||
![]() |
aa05cd1449 | ||
![]() |
31cd33f86c | ||
![]() |
9abca204e3 | ||
![]() |
e9760c2100 | ||
![]() |
1c5bab63a5 | ||
![]() |
0d80957639 | ||
![]() |
6c73ddcaca | ||
![]() |
1bb86fe4a8 | ||
![]() |
6b7be7a82d | ||
![]() |
37b25d8422 | ||
![]() |
49edd1a6dc | ||
![]() |
a338c6e60a | ||
![]() |
f9805f3bc7 | ||
![]() |
b24c4ea030 | ||
![]() |
0cabac1eed | ||
![]() |
7c08dbfbd2 | ||
![]() |
c0ec74bbb7 | ||
![]() |
ea834f6778 | ||
![]() |
2271f32140 | ||
![]() |
93906b9b17 | ||
![]() |
40d84b7a82 | ||
![]() |
d0ee569989 | ||
![]() |
92d969b075 | ||
![]() |
49b70d0efd | ||
![]() |
26779ef1fb | ||
![]() |
fd19af23a6 | ||
![]() |
f798fef212 | ||
![]() |
c74eab4fb5 | ||
![]() |
6a0198639f | ||
![]() |
8c870f2db8 | ||
![]() |
73d287e7ee | ||
![]() |
9eb3eea3f1 | ||
![]() |
df769396b1 | ||
![]() |
c2e47ca9dc | ||
![]() |
b348159e0e | ||
![]() |
45b62f0e77 | ||
![]() |
1fb420c2fc | ||
![]() |
b4f2bc1cb3 | ||
![]() |
d6cc182da2 | ||
![]() |
9d7baa86aa | ||
![]() |
872cd90dc6 | ||
![]() |
00ab816791 | ||
![]() |
2a9e9962e8 | ||
![]() |
ed25dd931e | ||
![]() |
66031f1bc2 | ||
![]() |
a902872880 | ||
![]() |
4c2d440871 | ||
![]() |
0da17de422 | ||
![]() |
07025ae76b | ||
![]() |
d99fe944f3 | ||
![]() |
408b2a473e | ||
![]() |
fc22e9e28a | ||
![]() |
d1c44ab7b1 | ||
![]() |
4ddac50d9b | ||
![]() |
7208ad67f1 | ||
![]() |
fffdeb1320 | ||
![]() |
e8fa7d8812 | ||
![]() |
dfdb92957e | ||
![]() |
441069f04b | ||
![]() |
201995eb90 | ||
![]() |
5863319c0b | ||
![]() |
2986d85b26 | ||
![]() |
f312457f35 | ||
![]() |
de501f5ba3 | ||
![]() |
c5e5141b21 | ||
![]() |
c08cf61d0c | ||
![]() |
6728382141 | ||
![]() |
7c3f104d1b | ||
![]() |
ad6be11bbc | ||
![]() |
f09faf6645 | ||
![]() |
117a7762e1 | ||
![]() |
3d47f494a8 | ||
![]() |
e0ebdc9045 | ||
![]() |
53f8e9328d | ||
![]() |
687e0b563b | ||
![]() |
6232cc7d49 | ||
![]() |
2a6670a404 | ||
![]() |
447efc7096 | ||
![]() |
c47878202d | ||
![]() |
349076bf34 | ||
![]() |
1748bf2e2a | ||
![]() |
d9a7730511 | ||
![]() |
4a239cc217 | ||
![]() |
ce2534c5b7 | ||
![]() |
3083f5fd55 | ||
![]() |
2045066b16 | ||
![]() |
995177498f | ||
![]() |
c00b7b62d6 | ||
![]() |
93b772f197 | ||
![]() |
7782f94daa | ||
![]() |
7c97dc8004 | ||
![]() |
34ce00e2d5 | ||
![]() |
702658cca5 | ||
![]() |
e472fe0276 | ||
![]() |
fb1c381ab7 | ||
![]() |
03c7998c11 | ||
![]() |
35729fc36b | ||
![]() |
f6ce603e45 | ||
![]() |
81a75ca955 | ||
![]() |
b57c9a51f8 | ||
![]() |
df396966b0 | ||
![]() |
b8897e0193 | ||
![]() |
150e8112ea | ||
![]() |
196f16b941 | ||
![]() |
21cb7a4847 | ||
![]() |
bb2dac7504 | ||
![]() |
f5fd2f2be3 | ||
![]() |
e6ea3879c3 | ||
![]() |
c07895d418 | ||
![]() |
58fb1cf4c3 | ||
![]() |
52cc8cb8fc | ||
![]() |
1b30dab8eb | ||
![]() |
40df4a94a7 | ||
![]() |
262d06f035 | ||
![]() |
408ab99774 | ||
![]() |
28cb21db13 | ||
![]() |
7f37f4ca41 | ||
![]() |
5f85258e84 | ||
![]() |
cde1776a2d | ||
![]() |
7f6303391a | ||
![]() |
66c7806cfa | ||
![]() |
2cdb6945ba | ||
![]() |
ca45855ed7 | ||
![]() |
07ed90ed11 | ||
![]() |
c1b97b1b44 | ||
![]() |
674019ea75 | ||
![]() |
8f762484f2 | ||
![]() |
4174991345 | ||
![]() |
71064cc760 | ||
![]() |
4c40c8ff30 | ||
![]() |
d7211b130b | ||
![]() |
e40d5a0a5d | ||
![]() |
553fbf1a77 | ||
![]() |
d3a4753b79 | ||
![]() |
12cc0de571 | ||
![]() |
cbd531e161 | ||
![]() |
a8bbe02e21 | ||
![]() |
6605d5ee63 | ||
![]() |
df8bacd82e | ||
![]() |
ee831da52d | ||
![]() |
8f969374c7 | ||
![]() |
5a788b04b5 | ||
![]() |
b88a45aa79 | ||
![]() |
3c20a056e6 | ||
![]() |
82a57d34b8 | ||
![]() |
c4d7076fe8 | ||
![]() |
00b11157b2 | ||
![]() |
b63bb1ac0c | ||
![]() |
882d0ecba8 | ||
![]() |
963f1a11eb | ||
![]() |
ba8acb40ec | ||
![]() |
1d4ea2164f | ||
![]() |
0199243ce9 | ||
![]() |
4c8b97afb3 | ||
![]() |
3bac0225e5 | ||
![]() |
7d13c8d9be | ||
![]() |
4190a87171 | ||
![]() |
69e719c4c7 | ||
![]() |
c5df8742c5 | ||
![]() |
5867edcc70 | ||
![]() |
b3a7255eed | ||
![]() |
4179def119 | ||
![]() |
ef9b897394 | ||
![]() |
c225dd89c6 | ||
![]() |
d59ebad167 | ||
![]() |
347932b213 | ||
![]() |
32bc615e78 | ||
![]() |
355373f24d | ||
![]() |
a044d2fe86 | ||
![]() |
1d44eff896 | ||
![]() |
c1b746ed51 | ||
![]() |
3381ad042b | ||
![]() |
a1b5766a71 | ||
![]() |
aeeb1d4294 | ||
![]() |
7ceec47246 | ||
![]() |
ede510139f | ||
![]() |
cc848ef9f2 | ||
![]() |
1c43ee1b18 | ||
![]() |
741f540f77 | ||
![]() |
fdd26ba8f1 | ||
![]() |
bec61f4ce7 | ||
![]() |
2af1496f11 | ||
![]() |
f9e23a17d9 | ||
![]() |
8050fa58a5 | ||
![]() |
e04023bdfc | ||
![]() |
950f853fa3 | ||
![]() |
c3c15e222d | ||
![]() |
3906816e67 | ||
![]() |
fe43e21484 | ||
![]() |
5f7fd506a3 | ||
![]() |
65a3e51ff9 | ||
![]() |
722c1c6257 | ||
![]() |
2f872375ef | ||
![]() |
f119ca683e | ||
![]() |
176c6b76cd | ||
![]() |
64604dbcc6 | ||
![]() |
0344a3f104 | ||
![]() |
c17247da58 | ||
![]() |
9601daedb7 | ||
![]() |
f958f3751d | ||
![]() |
0d1b9641c7 | ||
![]() |
9a9dd43646 | ||
![]() |
222257d25d | ||
![]() |
dd961ad30b | ||
![]() |
abf2dc3efc | ||
![]() |
99f819dfbf | ||
![]() |
dc484d79ed | ||
![]() |
42f522532e | ||
![]() |
f97b439bc0 | ||
![]() |
65a4a959bb | ||
![]() |
07d6fde34e | ||
![]() |
b1dfbcbceb | ||
![]() |
5e6f7e41e6 | ||
![]() |
df95ab1217 | ||
![]() |
edf924d012 | ||
![]() |
8886732b21 | ||
![]() |
543098cba3 | ||
![]() |
8bb20de912 | ||
![]() |
7f118c9841 | ||
![]() |
73155cca66 | ||
![]() |
267ccf6f97 | ||
![]() |
1f8e09868d | ||
![]() |
c9a2a47ee1 | ||
![]() |
58f7811c7d | ||
![]() |
a32ecb032e | ||
![]() |
bc2ad581ba | ||
![]() |
3498d59258 | ||
![]() |
f8accd62ed | ||
![]() |
25b10490a1 | ||
![]() |
800f0f45e5 | ||
![]() |
57c4a285d8 | ||
![]() |
1104987b9b | ||
![]() |
3dd646485f | ||
![]() |
13eb1718aa | ||
![]() |
e41d4dad3c | ||
![]() |
9c1e32d4ba | ||
![]() |
ce71b81dbe | ||
![]() |
62cc2201b0 | ||
![]() |
831c9aee2a | ||
![]() |
cd697d7205 | ||
![]() |
4be22e9ece | ||
![]() |
482ea6d8f9 | ||
![]() |
bcf0d80c47 | ||
![]() |
9bb292f38e | ||
![]() |
1d89cf2b75 | ||
![]() |
8afc872252 | ||
![]() |
559036518c | ||
![]() |
b0562f79d2 | ||
![]() |
f57df3f2c1 | ||
![]() |
0da123265c | ||
![]() |
cf340f48c3 | ||
![]() |
f0e0eaace4 | ||
![]() |
4140d49db3 | ||
![]() |
8bd9ff58c4 | ||
![]() |
fecccb0b28 | ||
![]() |
43d79ebd05 | ||
![]() |
ad7c8760eb | ||
![]() |
22f6cc1c6f | ||
![]() |
98dcb13201 | ||
![]() |
497e8b5f28 | ||
![]() |
4108979b65 | ||
![]() |
e01aec7b52 | ||
![]() |
c5b311359a | ||
![]() |
624b95921a | ||
![]() |
69c35f7f79 | ||
![]() |
3424b996c8 | ||
![]() |
ef634227aa | ||
![]() |
c724e4cb20 | ||
![]() |
936142cf7e | ||
![]() |
2aa37571f0 | ||
![]() |
9b42960b2f | ||
![]() |
6647167d02 | ||
![]() |
835f2cf769 | ||
![]() |
3fe5d7711f | ||
![]() |
faf9b113a9 | ||
![]() |
f72cc6bbbc | ||
![]() |
69e85a7ac6 | ||
![]() |
82b65399af | ||
![]() |
a83e397643 | ||
![]() |
f02c090b8d | ||
![]() |
49dd6553fb | ||
![]() |
d116cd7e90 | ||
![]() |
ed18842281 | ||
![]() |
4a3bd5fe7a | ||
![]() |
8a70cb59d1 | ||
![]() |
35772b0370 | ||
![]() |
68b33fcfb9 | ||
![]() |
0b306219c1 | ||
![]() |
bdd05a3f71 | ||
![]() |
c11205f3cd | ||
![]() |
11a0aa322f | ||
![]() |
a12bb4ee7c | ||
![]() |
1cc5422557 | ||
![]() |
55ed4dbc51 | ||
![]() |
201b8dccaa | ||
![]() |
544cd96e3d | ||
![]() |
2604da104d | ||
![]() |
a392d3b1b4 | ||
![]() |
b7ef95f68c | ||
![]() |
d12166a872 | ||
![]() |
74d9fcdbbc | ||
![]() |
3e9536cef8 | ||
![]() |
95c2c25f1b | ||
![]() |
81932e5d9f | ||
![]() |
d91d957777 | ||
![]() |
804ac8b4dc | ||
![]() |
bde1e32e29 | ||
![]() |
cb25db2556 | ||
![]() |
1536e62734 | ||
![]() |
401c2c7cc1 | ||
![]() |
d0faf9bc2f | ||
![]() |
81b5016168 | ||
![]() |
dcbe41bc40 | ||
![]() |
4e112dc98c | ||
![]() |
d233558b44 | ||
![]() |
207c2ef5b6 | ||
![]() |
d9ccc43d15 | ||
![]() |
ee93013220 | ||
![]() |
677fc5aa68 | ||
![]() |
8a86d0e655 | ||
![]() |
27c4c0c749 | ||
![]() |
2c0cdcf947 | ||
![]() |
a56755e2c6 | ||
![]() |
765de94ca3 | ||
![]() |
f060dc896f | ||
![]() |
1dcbcef705 | ||
![]() |
5be64e124b | ||
![]() |
f70df03b1a | ||
![]() |
8b577ca12f | ||
![]() |
91719435d9 | ||
![]() |
b1b82301d9 | ||
![]() |
fdf15f53d7 | ||
![]() |
4802854cff | ||
![]() |
b0538099cf | ||
![]() |
1fa9d7d7d7 | ||
![]() |
2b66762dec | ||
![]() |
7063f254c6 | ||
![]() |
7c9f15d8a9 | ||
![]() |
2982dc478c | ||
![]() |
47fc1b7357 | ||
![]() |
6990d7632a | ||
![]() |
4fa0f990e5 | ||
![]() |
e0f789bc14 | ||
![]() |
b93dabd670 | ||
![]() |
83528df18b | ||
![]() |
1e65a09e8e | ||
![]() |
34b4f65c20 | ||
![]() |
de4f9e4257 | ||
![]() |
dc2c16d6b5 | ||
![]() |
fbb175608d | ||
![]() |
ab026b1635 | ||
![]() |
0bc09defa7 | ||
![]() |
04352494a0 | ||
![]() |
96c76177af | ||
![]() |
d5a14031c6 | ||
![]() |
ad0b5e7583 | ||
![]() |
f2424095e0 | ||
![]() |
db2bed896a | ||
![]() |
c3600ee8fc | ||
![]() |
bb5efb3450 | ||
![]() |
2e1764af82 | ||
![]() |
a0c704cd94 | ||
![]() |
76f537a636 | ||
![]() |
fdd0d781ca | ||
![]() |
47338cf07b | ||
![]() |
4c1884fc95 | ||
![]() |
c2ba053737 | ||
![]() |
4cd16a40d0 | ||
![]() |
0038c6c65a | ||
![]() |
56b0f13525 | ||
![]() |
974315868d | ||
![]() |
5634954b7e | ||
![]() |
dab1eece4c | ||
![]() |
d01b73a661 | ||
![]() |
5754b4c6af | ||
![]() |
e769ef7d0b | ||
![]() |
41a694e4a4 | ||
![]() |
76a05d2dc9 | ||
![]() |
1895601198 | ||
![]() |
f6bfdb2ced | ||
![]() |
1eb7ce8741 | ||
![]() |
a5aaf760d0 | ||
![]() |
df2ebf93b6 | ||
![]() |
4dc64ee15b | ||
![]() |
a83ae05316 | ||
![]() |
b0e6170d93 | ||
![]() |
890122894f | ||
![]() |
9c019cafd5 | ||
![]() |
b357047e3a | ||
![]() |
f932e947e0 | ||
![]() |
3249af4eaa | ||
![]() |
737b3be5be |
@@ -7,7 +7,15 @@ indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.ts]
|
||||
indent_style = tab
|
||||
|
||||
[*.tsx]
|
||||
indent_style = tab
|
||||
|
@@ -7,9 +7,13 @@ env:
|
||||
plugins:
|
||||
- lodash
|
||||
- jsdoc
|
||||
- node
|
||||
- react
|
||||
extends: 'standard'
|
||||
parserOptions:
|
||||
sourceType: 'script'
|
||||
ecmaFeatures:
|
||||
jsx: true
|
||||
settings:
|
||||
jsdoc:
|
||||
additionalTagNames:
|
||||
@@ -188,6 +192,9 @@ rules:
|
||||
- min: 2
|
||||
exceptions:
|
||||
- "_"
|
||||
id-match:
|
||||
- error
|
||||
- "^[_0-9A-Za-z\\$]+$"
|
||||
line-comment-position:
|
||||
- error
|
||||
- position: above
|
||||
@@ -254,7 +261,8 @@ rules:
|
||||
- allowAfterThis: false
|
||||
object-curly-newline:
|
||||
- error
|
||||
- minProperties: 1
|
||||
- minProperties: 3
|
||||
consistent: true
|
||||
object-curly-spacing:
|
||||
- error
|
||||
- always
|
||||
@@ -281,7 +289,7 @@ rules:
|
||||
- error
|
||||
- anonymous: always
|
||||
named: always
|
||||
asyncArrow: never
|
||||
asyncArrow: always
|
||||
template-tag-spacing:
|
||||
- error
|
||||
- always
|
||||
@@ -290,9 +298,6 @@ rules:
|
||||
|
||||
# ECMAScript 6
|
||||
|
||||
arrow-body-style:
|
||||
- error
|
||||
- always
|
||||
arrow-parens:
|
||||
- error
|
||||
- always
|
||||
@@ -313,8 +318,6 @@ rules:
|
||||
- always
|
||||
prefer-const:
|
||||
- error
|
||||
prefer-reflect:
|
||||
- error
|
||||
prefer-spread:
|
||||
- error
|
||||
prefer-numeric-literals:
|
||||
@@ -423,3 +426,29 @@ rules:
|
||||
- error
|
||||
jsdoc/require-returns-type:
|
||||
- error
|
||||
|
||||
# Node
|
||||
|
||||
node/no-deprecated-api:
|
||||
- error
|
||||
node/no-missing-import:
|
||||
- error
|
||||
node/no-missing-require:
|
||||
- error
|
||||
node/process-exit-as-throw:
|
||||
- error
|
||||
node/no-extraneous-require:
|
||||
- error
|
||||
node/no-extraneous-import:
|
||||
- error
|
||||
|
||||
# React
|
||||
|
||||
react/jsx-uses-vars:
|
||||
- error
|
||||
|
||||
overrides:
|
||||
files: ['*.jsx']
|
||||
rules:
|
||||
require-jsdoc:
|
||||
- off
|
||||
|
22
.gitattributes
vendored
@@ -1,5 +1,11 @@
|
||||
# default
|
||||
* text
|
||||
|
||||
# Javascript files must retain LF line-endings (to keep eslint happy)
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
# CSS and SCSS files must retain LF line-endings (to keep ensure-staged-sass.sh happy)
|
||||
*.css text eol=lf
|
||||
*.scss text eol=lf
|
||||
@@ -11,7 +17,7 @@ Dockerfile* text
|
||||
etcher text
|
||||
.git* text
|
||||
*.html text
|
||||
*.json text
|
||||
*.json text eol=lf
|
||||
*.cpp text
|
||||
*.h text
|
||||
*.gyp text
|
||||
@@ -24,6 +30,9 @@ Makefile text
|
||||
*.yml text
|
||||
*.patch text
|
||||
*.txt text
|
||||
*.tpl text
|
||||
CODEOWNERS text
|
||||
*.plist text
|
||||
|
||||
# Binary files (no line-ending conversions)
|
||||
*.bz2 binary diff=hex
|
||||
@@ -44,5 +53,16 @@ Makefile text
|
||||
*.bin binary diff=hex
|
||||
*.dmg binary diff=hex
|
||||
*.rpi-sdcard binary diff=hex
|
||||
*.wic binary diff=hex
|
||||
*.foo binary diff=hex
|
||||
*.eot binary diff=hex
|
||||
*.otf binary diff=hex
|
||||
*.woff binary diff=hex
|
||||
*.woff2 binary diff=hex
|
||||
*.ttf binary diff=hex
|
||||
xz-without-extension binary diff=hex
|
||||
wmic-output.txt binary diff=hex
|
||||
|
||||
# gitsecret
|
||||
*.secret binary
|
||||
.gitsecret/** binary
|
||||
|
7
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,6 +1,11 @@
|
||||
- **Etcher version:**
|
||||
- **Operating system and architecture:**
|
||||
- **Image flashed:**
|
||||
- **What do you think should have happened:** <!-- or a step by step reproduction process -->
|
||||
- **What happened:**
|
||||
- **Do you see any meaningful error information in the DevTools?**
|
||||
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Opt+I` if you're on macOS. -->
|
||||
|
||||
<!-- You can open DevTools by pressing `Ctrl+Alt+I`, or `Cmd+Alt+I` if you're running OS X. -->
|
||||
<!-- issues with missing information will be labeled as not-enough-info and closed shortly -->
|
||||
<!-- please try to include as many influencing elements as possible are you root, does any other process block the device, etc. -->
|
||||
<!-- if you find a solution in the meantime thank you for sharing the fix and not just closing / abandoning your issue -->
|
||||
|
169
.github/actions/publish/action.yml
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
---
|
||||
name: package and publish GitHub (draft) release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||
required: true
|
||||
secrets:
|
||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
XCODE_APP_LOADER_EMAIL:
|
||||
type: string
|
||||
default: "accounts+apple@balena.io"
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: "18.x"
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Download custom source artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
- name: Extract custom source artifact
|
||||
if: runner.os != 'Windows'
|
||||
shell: pwsh
|
||||
working-directory: .
|
||||
run: tar -xf ${{ runner.temp }}/custom.tgz
|
||||
|
||||
- name: Extract custom source artifact
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
working-directory: .
|
||||
run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -xf ${{ runner.temp }}\custom.tgz
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Install yq
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: choco install yq
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
# https://www.electron.build/code-signing.html
|
||||
# https://github.com/Apple-Actions/import-codesign-certs
|
||||
- name: Import Apple code signing certificate
|
||||
if: runner.os == 'macOS'
|
||||
uses: apple-actions/import-codesign-certs@v1
|
||||
with:
|
||||
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
|
||||
- name: Import Windows code signing certificate
|
||||
if: runner.os == 'Windows'
|
||||
shell: powershell
|
||||
run: |
|
||||
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
|
||||
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/certificate.pfx
|
||||
Remove-Item -path ${{ runner.temp }} -include certificate.base64
|
||||
|
||||
Import-PfxCertificate `
|
||||
-FilePath ${{ runner.temp }}/certificate.pfx `
|
||||
-CertStoreLocation Cert:\CurrentUser\My `
|
||||
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
|
||||
|
||||
Remove-Item -path ${{ runner.temp }} -include certificate.pfx
|
||||
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
|
||||
|
||||
# ... or refactor (e.g.) https://github.com/samuelmeuli/action-electron-builder
|
||||
# https://github.com/product-os/scripts/tree/master/electron
|
||||
# https://github.com/product-os/scripts/tree/master/shared
|
||||
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
|
||||
- name: Package release
|
||||
id: package_release
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
|
||||
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
ELECTRON_BUILDER_ARCHITECTURE="${runner_arch}"
|
||||
APPLICATION_VERSION="$(jq -r '.version' package.json)"
|
||||
ARCHITECTURE_FLAGS="--${ELECTRON_BUILDER_ARCHITECTURE}"
|
||||
|
||||
if [[ $runner_os =~ linux ]]; then
|
||||
ELECTRON_BUILDER_OS='--linux'
|
||||
TARGETS="$(yq e .linux.target[] electron-builder.yml)"
|
||||
|
||||
elif [[ $runner_os =~ darwin|macos|osx ]]; then
|
||||
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
CSC_KEYCHAIN=signing_temp
|
||||
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
ELECTRON_BUILDER_OS='--mac'
|
||||
TARGETS="$(yq e .mac.target[] electron-builder.yml)"
|
||||
|
||||
elif [[ $runner_os =~ windows|win ]]; then
|
||||
ARCHITECTURE_FLAGS="--ia32 ${ARCHITECTURE_FLAGS}"
|
||||
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
|
||||
CSC_LINK=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
|
||||
ELECTRON_BUILDER_OS='--win'
|
||||
TARGETS="$(yq e .win.target[] electron-builder.yml)"
|
||||
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npm link electron-builder
|
||||
|
||||
for target in ${TARGETS}; do
|
||||
electron-builder ${ELECTRON_BUILDER_OS} ${target} ${ARCHITECTURE_FLAGS} \
|
||||
--c.extraMetadata.analytics.sentry.token='https://739bbcfc0ba4481481138d3fc831136d@o95242.ingest.sentry.io/4504451487301632' \
|
||||
--c.extraMetadata.analytics.amplitude.token='balena-etcher' \
|
||||
--c.extraMetadata.packageType="${target}"
|
||||
|
||||
find dist -type f -maxdepth 1
|
||||
done
|
||||
|
||||
echo "version=${APPLICATION_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
env:
|
||||
# Apple notarization (afterSignHook.js)
|
||||
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
|
||||
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
|
||||
# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
|
||||
# https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
|
||||
# https://www.electron.build/auto-update.html#staged-rollouts
|
||||
- name: Configure staged rollout(s)
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
percentage="$(cat < repo.yml | yq e .triggerNotification.stagingPercentage)"
|
||||
|
||||
find dist -type f -maxdepth 1 \
|
||||
-name "latest*.yml" \
|
||||
-exec yq -i e .version=\"${{ steps.package_release.outputs.version }}\" {} \;
|
||||
|
||||
find dist -type f -maxdepth 1 \
|
||||
-name "latest*.yml" \
|
||||
-exec yq -i e .stagingPercentage=\"$percentage\" {} \;
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
|
||||
path: dist
|
||||
retention-days: 1
|
64
.github/actions/test/action.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: test release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||
required: true
|
||||
secrets:
|
||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: "16.x"
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Test release
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
npm run flowzone-preinstall-${runner_os}
|
||||
npm ci
|
||||
npm run build
|
||||
npm run test-${runner_os}
|
||||
|
||||
env:
|
||||
# https://www.electronjs.org/docs/latest/api/environment-variables
|
||||
ELECTRON_NO_ATTACH_CONSOLE: true
|
||||
|
||||
- name: Compress custom source
|
||||
if: runner.os != 'Windows'
|
||||
shell: pwsh
|
||||
run: tar -acf ${{ runner.temp }}/custom.tgz .
|
||||
|
||||
- name: Compress custom source
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -acf ${{ runner.temp }}\custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
|
||||
path: ${{ runner.temp }}/custom.tgz
|
||||
retention-days: 1
|
24
.github/workflows/flowzone.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Flowzone
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
# allow external contributions to use secrets within trusted code
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
jobs:
|
||||
flowzone:
|
||||
name: Flowzone
|
||||
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
|
||||
# prevent duplicate workflows and only allow one `pull_request` or `pull_request_target` for
|
||||
# internal or external contributions respectively
|
||||
if: |
|
||||
(github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request') ||
|
||||
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
|
||||
secrets: inherit
|
||||
with:
|
||||
tests_run_on: '["ubuntu-20.04","macos-latest","windows-2019"]'
|
||||
restrict_custom_actions: false
|
||||
github_prerelease: true
|
||||
cloudflare_website: "etcher"
|
13
.github/workflows/winget.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Publish to WinGet
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: windows-latest # action can only be run on windows
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v1
|
||||
with:
|
||||
identifier: Balena.Etcher
|
||||
installers-regex: 'balenaEtcher-Setup.*.exe$'
|
||||
token: ${{ secrets.WINGET_PAT }}
|
21
.gitignore
vendored
@@ -11,6 +11,9 @@ pids
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
/lib-cov
|
||||
|
||||
# Image stream output directory
|
||||
/tests/image-stream/output
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
/coverage
|
||||
|
||||
@@ -23,6 +26,10 @@ pids
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
/generated
|
||||
/binaries
|
||||
|
||||
# Dependency directory
|
||||
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
||||
node_modules
|
||||
@@ -37,3 +44,17 @@ node_modules
|
||||
*.cer
|
||||
*.crt
|
||||
*.pem
|
||||
|
||||
# OSX files
|
||||
|
||||
.DS_Store
|
||||
|
||||
# VSCode files
|
||||
|
||||
.vscode
|
||||
.gitsecret/keys/random_seed
|
||||
!*.secret
|
||||
secrets/APPLE_SIGNING_PASSWORD.txt
|
||||
secrets/WINDOWS_SIGNING_PASSWORD.txt
|
||||
secrets/XCODE_APP_LOADER_PASSWORD.txt
|
||||
secrets/WINDOWS_SIGNING.pfx
|
||||
|
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "scripts/resin"]
|
||||
path = scripts/resin
|
||||
url = https://github.com/balena-io/scripts.git
|
||||
branch = master
|
BIN
.gitsecret/keys/pubring.kbx
Normal file
BIN
.gitsecret/keys/pubring.kbx~
Normal file
BIN
.gitsecret/keys/trustdb.gpg
Normal file
5
.gitsecret/paths/mapping.cfg
Normal file
@@ -0,0 +1,5 @@
|
||||
secrets/APPLE_SIGNING_PASSWORD.txt:5c9cfeb1ea5142b547bc842cc6e0b4a932641ae9811ee47abe2c3953f2a4de5d
|
||||
secrets/WINDOWS_SIGNING_PASSWORD.txt:852e431628494f2559793c39cf09c34e9406dd79bb15b90c9f88194020470568
|
||||
secrets/XCODE_APP_LOADER_PASSWORD.txt:005eb9a3c7035c77232973c9355468fc396b94e62783fb8e6dce16bce95b94a1
|
||||
secrets/WINDOWS_SIGNING.pfx:929f401db38733ffc41572539de7c0d938023af51ed06c205a72a71c1f815714
|
||||
secrets/APPLE_SIGNING.p12:61abf7b4ff2eec76ce889d71bcdd568b99a6a719b4947ac20f03966265b0946a
|
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"node-cli": {
|
||||
"node": "6.1.0",
|
||||
"dependencies": {
|
||||
"linux": [
|
||||
"libudev-dev",
|
||||
"libusb-1.0-0-dev"
|
||||
]
|
||||
}
|
||||
},
|
||||
"electron": {
|
||||
"dependencies": {
|
||||
"linux": [
|
||||
"libudev-dev",
|
||||
"libusb-1.0-0-dev",
|
||||
"libyaml-dev"
|
||||
]
|
||||
},
|
||||
"builder": {
|
||||
"appId": "io.resin.etcher",
|
||||
"copyright": "Copyright 2016 Resinio Ltd",
|
||||
"productName": "Etcher",
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools"
|
||||
},
|
||||
"dmg": {
|
||||
"iconSize": 110,
|
||||
"contents": [
|
||||
{
|
||||
"x": 140,
|
||||
"y": 225
|
||||
},
|
||||
{
|
||||
"x": 415,
|
||||
"y": 225,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
],
|
||||
"window": {
|
||||
"width": 540,
|
||||
"height": 405
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"category": "Utility",
|
||||
"packageCategory": "utils",
|
||||
"synopsis": "Etcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more."
|
||||
},
|
||||
"deb": {
|
||||
"priority": "optional",
|
||||
"depends": [
|
||||
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
# sass-lint config generated by make-sass-lint-config v0.1.2
|
||||
|
||||
files:
|
||||
include: lib/gui/scss/**/*.scss
|
||||
options:
|
||||
formatter: stylish
|
||||
merge-default-rules: false
|
||||
rules:
|
||||
no-css-comments: 0
|
||||
no-important: 0
|
||||
no-qualifying-elements: 0
|
||||
placeholder-in-extend: 0
|
||||
property-sort-order: 0
|
||||
quotes:
|
||||
- 1
|
||||
- style: double
|
||||
|
75
.travis.yml
@@ -1,75 +0,0 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
node_js:
|
||||
- "6.10.3"
|
||||
|
||||
# Remove wine from cache
|
||||
before_cache:
|
||||
- rm -rf $HOME/.cache/electron-builder/wine
|
||||
|
||||
cache:
|
||||
ccache: true
|
||||
directories:
|
||||
- $HOME/.cache/electron
|
||||
- $HOME/.cache/electron-builder
|
||||
- $HOME/.npm/_prebuilds
|
||||
- $HOME/Library/Caches/electron
|
||||
- $HOME/Library/Caches/electron-builder
|
||||
- $HOME/.pkg-cache
|
||||
- node_modules
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- libstdc++-6-dev
|
||||
|
||||
env:
|
||||
global:
|
||||
- CCACHE_TEMPDIR=/tmp/.ccache-temp
|
||||
- CCACHE_COMPRESS=1
|
||||
- CC="clang"
|
||||
- CXX="clang++"
|
||||
- HOMEBREW_NO_AUTO_UPDATE=1
|
||||
matrix:
|
||||
- TARGET_ARCH=x64
|
||||
- TARGET_ARCH=x86
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
exclude:
|
||||
- os: osx
|
||||
env: TARGET_ARCH=x86
|
||||
|
||||
os:
|
||||
- linux
|
||||
|
||||
before_install:
|
||||
- export HOST_OS="$TRAVIS_OS_NAME";
|
||||
|
||||
install:
|
||||
- ./scripts/ci/install.sh -o $HOST_OS -r $TARGET_ARCH
|
||||
|
||||
script:
|
||||
- ./scripts/ci/test.sh -o $HOST_OS -r $TARGET_ARCH
|
||||
- ./scripts/ci/build-installers.sh -o $HOST_OS -r $TARGET_ARCH
|
||||
|
||||
deploy:
|
||||
provider: script
|
||||
skip_cleanup: true
|
||||
script: scripts/ci/deploy.sh -o $HOST_OS -r $TARGET_ARCH
|
||||
on:
|
||||
branch: master
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
webhooks:
|
||||
urls:
|
||||
- https://webhooks.gitter.im/e/0a019c8b9828eb9f6a72
|
||||
on_success: change
|
||||
on_failure: always
|
||||
on_start: never
|
13351
.versionbot/CHANGELOG.yml
Normal file
2284
CHANGELOG.md
531
Makefile
@@ -2,23 +2,22 @@
|
||||
# Build configuration
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
RESIN_SCRIPTS ?= ./scripts/resin
|
||||
export NPM_VERSION ?= 6.14.8
|
||||
S3_BUCKET = artifacts.ci.balena-cloud.com
|
||||
|
||||
# This directory will be completely deleted by the `clean` rule
|
||||
BUILD_DIRECTORY ?= dist
|
||||
|
||||
# See http://stackoverflow.com/a/20763842/1641422
|
||||
BUILD_DIRECTORY_PARENT = $(dir $(BUILD_DIRECTORY))
|
||||
ifeq ($(wildcard $(BUILD_DIRECTORY_PARENT).),)
|
||||
$(error $(BUILD_DIRECTORY_PARENT) does not exist)
|
||||
endif
|
||||
|
||||
BUILD_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
|
||||
|
||||
# See https://github.com/electron/spectron/issues/127
|
||||
ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))')
|
||||
$(BUILD_DIRECTORY):
|
||||
mkdir $@
|
||||
|
||||
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
|
||||
mkdir $@
|
||||
|
||||
# See https://stackoverflow.com/a/13468229/1641422
|
||||
SHELL := /bin/bash
|
||||
PATH := $(shell pwd)/node_modules/.bin:$(PATH)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Operating system and architecture detection
|
||||
@@ -51,6 +50,15 @@ else
|
||||
ifeq ($(shell uname -m),armv7l)
|
||||
HOST_ARCH = armv7hf
|
||||
endif
|
||||
ifeq ($(shell uname -m),aarch64)
|
||||
HOST_ARCH = aarch64
|
||||
endif
|
||||
ifeq ($(shell uname -m),armv8)
|
||||
HOST_ARCH = aarch64
|
||||
endif
|
||||
ifeq ($(shell uname -m),arm64)
|
||||
HOST_ARCH = aarch64
|
||||
endif
|
||||
endif
|
||||
ifeq ($(shell uname -s),Darwin)
|
||||
PLATFORM = darwin
|
||||
@@ -58,14 +66,17 @@ else
|
||||
ifeq ($(shell uname -m),x86_64)
|
||||
HOST_ARCH = x64
|
||||
endif
|
||||
ifeq ($(shell uname -m),arm64)
|
||||
HOST_ARCH = aarch64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifndef PLATFORM
|
||||
$(error We couldn't detect your host platform)
|
||||
$(error We could not detect your host platform)
|
||||
endif
|
||||
ifndef HOST_ARCH
|
||||
$(error We couldn't detect your host architecture)
|
||||
$(error We could not detect your host architecture)
|
||||
endif
|
||||
|
||||
# Default to host architecture. You can override by doing:
|
||||
@@ -74,286 +85,29 @@ endif
|
||||
#
|
||||
TARGET_ARCH ?= $(HOST_ARCH)
|
||||
|
||||
# Support x86 builds from x64 in GNU/Linux
|
||||
# See https://github.com/addaleax/lzma-native/issues/27
|
||||
ifeq ($(PLATFORM),linux)
|
||||
ifneq ($(HOST_ARCH),$(TARGET_ARCH))
|
||||
ifeq ($(TARGET_ARCH),x86)
|
||||
export CFLAGS += -m32
|
||||
else
|
||||
$(error Can't build $(TARGET_ARCH) binaries on a $(HOST_ARCH) host)
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Application configuration
|
||||
# Electron
|
||||
# ---------------------------------------------------------------------
|
||||
electron-develop:
|
||||
git submodule update --init && \
|
||||
npm ci && \
|
||||
npm run webpack
|
||||
|
||||
ELECTRON_VERSION = $(shell jq -r '.devDependencies["electron"]' package.json)
|
||||
NODE_VERSION = 6.1.0
|
||||
COMPANY_NAME = Resinio Ltd
|
||||
APPLICATION_NAME = $(shell jq -r '.displayName' package.json)
|
||||
APPLICATION_DESCRIPTION = $(shell jq -r '.description' package.json)
|
||||
APPLICATION_COPYRIGHT = $(shell cat electron-builder.yml | shyaml get-value copyright)
|
||||
|
||||
BINTRAY_ORGANIZATION = resin-io
|
||||
BINTRAY_REPOSITORY_DEBIAN = debian
|
||||
BINTRAY_REPOSITORY_REDHAT = redhat
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Extra variables
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
TARGET_ARCH_DEBIAN = $(shell ./scripts/build/architecture-convert.sh -r $(TARGET_ARCH) -t debian)
|
||||
TARGET_ARCH_REDHAT = $(shell ./scripts/build/architecture-convert.sh -r $(TARGET_ARCH) -t redhat)
|
||||
TARGET_ARCH_APPIMAGE = $(shell ./scripts/build/architecture-convert.sh -r $(TARGET_ARCH) -t appimage)
|
||||
TARGET_ARCH_ELECTRON_BUILDER = $(shell ./scripts/build/architecture-convert.sh -r $(TARGET_ARCH) -t electron-builder)
|
||||
PLATFORM_PKG = $(shell ./scripts/build/platform-convert.sh -r $(PLATFORM) -t pkg)
|
||||
ENTRY_POINT_CLI = lib/cli/etcher.js
|
||||
ETCHER_CLI_BINARY = $(APPLICATION_NAME_LOWERCASE)
|
||||
ifeq ($(PLATFORM),win32)
|
||||
ETCHER_CLI_BINARY = $(APPLICATION_NAME_LOWERCASE).exe
|
||||
endif
|
||||
|
||||
APPLICATION_NAME_LOWERCASE = $(shell echo $(APPLICATION_NAME) | tr A-Z a-z)
|
||||
APPLICATION_VERSION_DEBIAN = $(shell echo $(APPLICATION_VERSION) | tr "-" "~")
|
||||
APPLICATION_VERSION_REDHAT = $(shell echo $(APPLICATION_VERSION) | tr "-" "~")
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Release type
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
# Add the current commit to the version if release type is "snapshot"
|
||||
RELEASE_TYPE ?= snapshot
|
||||
PACKAGE_JSON_VERSION = $(shell jq -r '.version' package.json)
|
||||
ifeq ($(RELEASE_TYPE),production)
|
||||
APPLICATION_VERSION = $(PACKAGE_JSON_VERSION)
|
||||
S3_BUCKET = resin-production-downloads
|
||||
BINTRAY_COMPONENT = $(APPLICATION_NAME_LOWERCASE)
|
||||
endif
|
||||
ifeq ($(RELEASE_TYPE),snapshot)
|
||||
CURRENT_COMMIT_HASH = $(shell git log -1 --format="%h")
|
||||
APPLICATION_VERSION = $(PACKAGE_JSON_VERSION)+$(CURRENT_COMMIT_HASH)
|
||||
S3_BUCKET = resin-nightly-downloads
|
||||
BINTRAY_COMPONENT = $(APPLICATION_NAME_LOWERCASE)-devel
|
||||
endif
|
||||
ifndef APPLICATION_VERSION
|
||||
$(error Invalid release type: $(RELEASE_TYPE))
|
||||
endif
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Code signing
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
ifeq ($(PLATFORM),darwin)
|
||||
ifndef CSC_NAME
|
||||
$(warning No code-sign identity found (CSC_NAME is not set))
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM),win32)
|
||||
ifndef CSC_LINK
|
||||
$(warning No code-sign certificate found (CSC_LINK is not set))
|
||||
ifndef CSC_KEY_PASSWORD
|
||||
$(warning No code-sign certificate password found (CSC_KEY_PASSWORD is not set))
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Electron Builder
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
ELECTRON_BUILDER_OPTIONS = --$(TARGET_ARCH_ELECTRON_BUILDER)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Analytics
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
ifndef ANALYTICS_SENTRY_TOKEN
|
||||
$(warning No Sentry token found (ANALYTICS_SENTRY_TOKEN is not set))
|
||||
else
|
||||
ELECTRON_BUILDER_OPTIONS += --extraMetadata.analytics.sentry.token=$(ANALYTICS_SENTRY_TOKEN)
|
||||
endif
|
||||
|
||||
ifndef ANALYTICS_MIXPANEL_TOKEN
|
||||
$(warning No Mixpanel token found (ANALYTICS_MIXPANEL_TOKEN is not set))
|
||||
else
|
||||
ELECTRON_BUILDER_OPTIONS += --extraMetadata.analytics.mixpanel.token=$(ANALYTICS_MIXPANEL_TOKEN)
|
||||
endif
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Rules
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
# See http://stackoverflow.com/a/12528721
|
||||
# Note that the blank line before 'endef' is actually important - don't delete it
|
||||
define execute-command
|
||||
$(1)
|
||||
|
||||
endef
|
||||
|
||||
$(BUILD_DIRECTORY):
|
||||
mkdir $@
|
||||
|
||||
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
|
||||
mkdir $@
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)-app: \
|
||||
package.json npm-shrinkwrap.json \
|
||||
| $(BUILD_DIRECTORY)
|
||||
mkdir -p $@
|
||||
./scripts/build/dependencies-npm.sh -p \
|
||||
-r "$(TARGET_ARCH)" \
|
||||
-v "$(NODE_VERSION)" \
|
||||
-x $@ \
|
||||
-t node \
|
||||
-s "$(PLATFORM)"
|
||||
git apply --directory $@/node_modules/lzma-native patches/cli/lzma-native-index-static-addon-require.patch
|
||||
cp -r lib $@
|
||||
cp package.json $@
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH): \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)-app \
|
||||
| $(BUILD_DIRECTORY)
|
||||
mkdir $@
|
||||
cd $< && pkg --output ../../$@/$(ETCHER_CLI_BINARY) -t node6-$(PLATFORM_PKG)-$(TARGET_ARCH) $(ENTRY_POINT_CLI)
|
||||
./scripts/build/dependencies-npm-extract-addons.sh \
|
||||
-d $</node_modules \
|
||||
-o $@/node_modules
|
||||
# pkg currently has a bug where darwin executables
|
||||
# can't be code-signed
|
||||
# See https://github.com/zeit/pkg/issues/128
|
||||
# ifeq ($(PLATFORM),darwin)
|
||||
# ifdef CSC_NAME
|
||||
# ./scripts/build/electron-sign-file-darwin.sh -f $@/$(ETCHER_CLI_BINARY) -i "$(CSC_NAME)"
|
||||
# endif
|
||||
# endif
|
||||
|
||||
# pkg currently has a bug where Windows executables
|
||||
# can't be branded
|
||||
# See https://github.com/zeit/pkg/issues/149
|
||||
# ifeq ($(PLATFORM),win32)
|
||||
# ./scripts/build/electron-brand-exe.sh \
|
||||
# -f $@/$(ETCHER_CLI_BINARY) \
|
||||
# -n $(APPLICATION_NAME) \
|
||||
# -d "$(APPLICATION_DESCRIPTION)" \
|
||||
# -v "$(APPLICATION_VERSION)" \
|
||||
# -c "$(APPLICATION_COPYRIGHT)" \
|
||||
# -m "$(COMPANY_NAME)" \
|
||||
# -i assets/icon.ico \
|
||||
# -w $(BUILD_TEMPORARY_DIRECTORY)
|
||||
# endif
|
||||
|
||||
ifeq ($(PLATFORM),win32)
|
||||
ifdef CSC_LINK
|
||||
ifdef CSC_KEY_PASSWORD
|
||||
./scripts/build/electron-sign-exe-win32.sh -f $@/$(ETCHER_CLI_BINARY) \
|
||||
-d "$(APPLICATION_NAME) - $(APPLICATION_VERSION)" \
|
||||
-c $(CSC_LINK) \
|
||||
-p $(CSC_KEY_PASSWORD)
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).zip: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)
|
||||
./scripts/build/zip-file.sh -f $< -s $(PLATFORM) -o $@
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)
|
||||
./scripts/build/tar-gz-file.sh -f $< -o $@
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# GUI
|
||||
# ---------------------------------------------------------------------
|
||||
electron-test:
|
||||
$(RESIN_SCRIPTS)/electron/test.sh \
|
||||
-b $(shell pwd) \
|
||||
-s $(PLATFORM)
|
||||
|
||||
assets/dmg/background.tiff: assets/dmg/background.png assets/dmg/background@2x.png
|
||||
tiffutil -cathidpicheck $^ -out $@
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION).dmg: assets/dmg/background.tiff \
|
||||
| $(BUILD_DIRECTORY)
|
||||
TARGET_ARCH=$(TARGET_ARCH) build --mac dmg $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
||||
--extraMetadata.packageType=dmg
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-mac.zip: assets/dmg/background.tiff \
|
||||
| $(BUILD_DIRECTORY)
|
||||
TARGET_ARCH=$(TARGET_ARCH) build --mac zip $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
||||
--extraMetadata.packageType=zip
|
||||
|
||||
APPLICATION_NAME_ELECTRON = $(APPLICATION_NAME_LOWERCASE)-electron
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)-$(APPLICATION_VERSION_REDHAT).$(TARGET_ARCH_REDHAT).rpm: \
|
||||
| $(BUILD_DIRECTORY)
|
||||
build --linux rpm $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.name=$(APPLICATION_NAME_ELECTRON) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION_REDHAT) \
|
||||
--extraMetadata.packageType=rpm
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb: \
|
||||
| $(BUILD_DIRECTORY)
|
||||
build --linux deb $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.name=$(APPLICATION_NAME_ELECTRON) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION_DEBIAN) \
|
||||
--extraMetadata.packageType=deb
|
||||
|
||||
ifeq ($(TARGET_ARCH),x64)
|
||||
ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY = linux-unpacked
|
||||
else
|
||||
ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY = linux-$(TARGET_ARCH_ELECTRON_BUILDER)-unpacked
|
||||
endif
|
||||
|
||||
$(BUILD_DIRECTORY)/$(ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY)/$(APPLICATION_NAME_ELECTRON): | $(BUILD_DIRECTORY)
|
||||
build --dir --linux $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.name=$(APPLICATION_NAME_ELECTRON) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
||||
--extraMetadata.packageType=AppImage
|
||||
touch $@
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM).AppDir: \
|
||||
$(BUILD_DIRECTORY)/$(ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY)/$(APPLICATION_NAME_ELECTRON) \
|
||||
| $(BUILD_DIRECTORY)
|
||||
./scripts/build/electron-create-appdir.sh \
|
||||
-n $(APPLICATION_NAME) \
|
||||
-d "$(APPLICATION_DESCRIPTION)" \
|
||||
-p $(dir $<) \
|
||||
electron-build: assets/dmg/background.tiff | $(BUILD_TEMPORARY_DIRECTORY)
|
||||
$(RESIN_SCRIPTS)/electron/build.sh \
|
||||
-b $(shell pwd) \
|
||||
-r $(TARGET_ARCH) \
|
||||
-b $(APPLICATION_NAME_ELECTRON) \
|
||||
-i assets/icon.png \
|
||||
-o $@
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(TARGET_ARCH_APPIMAGE).AppImage: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM).AppDir \
|
||||
| $(BUILD_DIRECTORY) $(BUILD_TEMPORARY_DIRECTORY)
|
||||
./scripts/build/electron-create-appimage-linux.sh \
|
||||
-d $< \
|
||||
-r $(TARGET_ARCH) \
|
||||
-w $(BUILD_TEMPORARY_DIRECTORY) \
|
||||
-o $@
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH_APPIMAGE).zip: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(TARGET_ARCH_APPIMAGE).AppImage \
|
||||
| $(BUILD_DIRECTORY)
|
||||
./scripts/build/zip-file.sh -f $< -s $(PLATFORM) -o $@
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Portable-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe: \
|
||||
| $(BUILD_DIRECTORY)
|
||||
TARGET_ARCH=$(TARGET_ARCH) build --win portable $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
||||
--extraMetadata.packageType=portable
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Setup-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe: \
|
||||
| $(BUILD_DIRECTORY)
|
||||
TARGET_ARCH=$(TARGET_ARCH) build --win nsis $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
||||
--extraMetadata.packageType=nsis
|
||||
-s $(PLATFORM) \
|
||||
-v production \
|
||||
-n $(BUILD_TEMPORARY_DIRECTORY)/npm
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Phony targets
|
||||
@@ -363,225 +117,36 @@ TARGETS = \
|
||||
help \
|
||||
info \
|
||||
lint \
|
||||
lint-js \
|
||||
lint-sass \
|
||||
lint-cpp \
|
||||
lint-html \
|
||||
lint-spell \
|
||||
test-spectron \
|
||||
test-gui \
|
||||
test-sdk \
|
||||
test-cli \
|
||||
test \
|
||||
sanity-checks \
|
||||
clean \
|
||||
distclean \
|
||||
changelog \
|
||||
package-electron \
|
||||
package-cli \
|
||||
cli-develop \
|
||||
installers-all \
|
||||
publish-all \
|
||||
electron-develop
|
||||
|
||||
changelog:
|
||||
versionist
|
||||
|
||||
package-electron:
|
||||
TARGET_ARCH=$(TARGET_ARCH) build --dir $(ELECTRON_BUILDER_OPTIONS)
|
||||
|
||||
package-cli: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)
|
||||
|
||||
ifeq ($(PLATFORM),darwin)
|
||||
electron-installer-app-zip: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-mac.zip
|
||||
electron-installer-dmg: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION).dmg
|
||||
cli-installer-tar-gz: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
|
||||
TARGETS += \
|
||||
electron-installer-dmg \
|
||||
electron-installer-app-zip \
|
||||
cli-installer-tar-gz
|
||||
PUBLISH_AWS_S3 += \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-mac.zip \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION).dmg \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM),linux)
|
||||
electron-installer-appimage: $(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH_APPIMAGE).zip
|
||||
electron-installer-debian: $(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb
|
||||
electron-installer-redhat: $(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)-$(APPLICATION_VERSION_REDHAT).$(TARGET_ARCH_REDHAT).rpm
|
||||
cli-installer-tar-gz: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
|
||||
TARGETS += \
|
||||
electron-installer-appimage \
|
||||
electron-installer-debian \
|
||||
electron-installer-redhat \
|
||||
cli-installer-tar-gz
|
||||
PUBLISH_AWS_S3 += \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH_APPIMAGE).zip \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
|
||||
PUBLISH_BINTRAY_DEBIAN += \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb
|
||||
PUBLISH_BINTRAY_REDHAT += \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)-$(APPLICATION_VERSION_REDHAT).$(TARGET_ARCH_REDHAT).rpm
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM),win32)
|
||||
electron-installer-portable: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Portable-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe
|
||||
electron-installer-nsis: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Setup-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe
|
||||
cli-installer-zip: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).zip
|
||||
TARGETS += \
|
||||
electron-installer-portable \
|
||||
electron-installer-nsis \
|
||||
cli-installer-zip
|
||||
PUBLISH_AWS_S3 += \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Portable-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Setup-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).zip
|
||||
endif
|
||||
|
||||
installers-all: $(PUBLISH_AWS_S3) $(PUBLISH_BINTRAY_DEBIAN) $(PUBLISH_BINTRAY_REDHAT)
|
||||
|
||||
ifdef PUBLISH_AWS_S3
|
||||
publish-aws-s3: $(PUBLISH_AWS_S3)
|
||||
ifeq ($(RELEASE_TYPE),production)
|
||||
$(foreach publishable,$^,$(call execute-command,./scripts/publish/aws-s3.sh \
|
||||
-f $(publishable) \
|
||||
-b $(S3_BUCKET) \
|
||||
-v $(APPLICATION_VERSION) \
|
||||
-p $(APPLICATION_NAME_LOWERCASE)))
|
||||
endif
|
||||
ifeq ($(RELEASE_TYPE),snapshot)
|
||||
$(foreach publishable,$^,$(call execute-command,./scripts/publish/aws-s3.sh \
|
||||
-f $(publishable) \
|
||||
-b $(S3_BUCKET) \
|
||||
-v $(APPLICATION_VERSION) \
|
||||
-p $(APPLICATION_NAME_LOWERCASE) \
|
||||
-k $(shell date +"%Y-%m-%d")))
|
||||
endif
|
||||
|
||||
PUBLISHABLES += publish-aws-s3
|
||||
TARGETS += publish-aws-s3
|
||||
endif
|
||||
|
||||
ifdef PUBLISH_BINTRAY_DEBIAN
|
||||
publish-bintray-debian: $(PUBLISH_BINTRAY_DEBIAN)
|
||||
$(foreach publishable,$^,$(call execute-command,./scripts/publish/bintray.sh \
|
||||
-f $(publishable) \
|
||||
-v $(APPLICATION_VERSION_DEBIAN) \
|
||||
-r $(TARGET_ARCH) \
|
||||
-t $(RELEASE_TYPE) \
|
||||
-o $(BINTRAY_ORGANIZATION) \
|
||||
-p $(BINTRAY_REPOSITORY_DEBIAN) \
|
||||
-c $(BINTRAY_COMPONENT) \
|
||||
-y debian))
|
||||
|
||||
PUBLISHABLES += publish-bintray-debian
|
||||
TARGETS += publish-bintray-debian
|
||||
endif
|
||||
|
||||
ifdef PUBLISH_BINTRAY_REDHAT
|
||||
publish-bintray-redhat: $(PUBLISH_BINTRAY_REDHAT)
|
||||
$(foreach publishable,$^,$(call execute-command,./scripts/publish/bintray.sh \
|
||||
-f $(publishable) \
|
||||
-v $(APPLICATION_VERSION_REDHAT) \
|
||||
-r $(TARGET_ARCH) \
|
||||
-t $(RELEASE_TYPE) \
|
||||
-o $(BINTRAY_ORGANIZATION) \
|
||||
-p $(BINTRAY_REPOSITORY_REDHAT) \
|
||||
-c $(BINTRAY_COMPONENT) \
|
||||
-y redhat))
|
||||
|
||||
PUBLISHABLES += publish-bintray-redhat
|
||||
TARGETS += publish-bintray-redhat
|
||||
endif
|
||||
|
||||
publish-all: $(PUBLISHABLES)
|
||||
electron-develop \
|
||||
electron-test \
|
||||
electron-build
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
|
||||
cli-develop:
|
||||
./scripts/build/dependencies-npm.sh \
|
||||
-r "$(TARGET_ARCH)" \
|
||||
-v "$(NODE_VERSION)" \
|
||||
-t node \
|
||||
-s "$(PLATFORM)"
|
||||
lint:
|
||||
npm run lint
|
||||
|
||||
electron-develop:
|
||||
./scripts/build/dependencies-npm.sh \
|
||||
-r "$(TARGET_ARCH)" \
|
||||
-v "$(ELECTRON_VERSION)" \
|
||||
-t electron \
|
||||
-s "$(PLATFORM)"
|
||||
|
||||
sass:
|
||||
node-sass lib/gui/scss/main.scss > lib/gui/css/main.css
|
||||
|
||||
lint-js:
|
||||
eslint lib tests scripts bin versionist.conf.js
|
||||
|
||||
lint-sass:
|
||||
sass-lint lib/gui/scss
|
||||
|
||||
lint-cpp:
|
||||
cpplint --recursive src
|
||||
|
||||
lint-html:
|
||||
node scripts/html-lint.js
|
||||
|
||||
lint-spell:
|
||||
codespell \
|
||||
--dictionary - \
|
||||
--dictionary dictionary.txt \
|
||||
--skip *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \
|
||||
lib tests docs scripts Makefile *.md LICENSE
|
||||
|
||||
lint: lint-js lint-sass lint-cpp lint-html lint-spell
|
||||
|
||||
MOCHA_OPTIONS=--recursive --reporter spec
|
||||
|
||||
test-spectron:
|
||||
ETCHER_SPECTRON_ENTRYPOINT="$(ETCHER_SPECTRON_ENTRYPOINT)" mocha $(MOCHA_OPTIONS) tests/spectron
|
||||
|
||||
test-gui:
|
||||
electron-mocha $(MOCHA_OPTIONS) --renderer tests/gui
|
||||
|
||||
test-sdk:
|
||||
electron-mocha $(MOCHA_OPTIONS) \
|
||||
tests/shared \
|
||||
tests/image-stream
|
||||
|
||||
test-cli:
|
||||
mocha $(MOCHA_OPTIONS) \
|
||||
tests/shared \
|
||||
tests/image-stream
|
||||
|
||||
test: test-gui test-sdk test-spectron
|
||||
test:
|
||||
npm run test
|
||||
|
||||
help:
|
||||
@echo "Available targets: $(TARGETS)"
|
||||
|
||||
info:
|
||||
@echo "Application version : $(APPLICATION_VERSION)"
|
||||
@echo "Release type : $(RELEASE_TYPE)"
|
||||
@echo "Platform : $(PLATFORM)"
|
||||
@echo "Host arch : $(HOST_ARCH)"
|
||||
@echo "Target arch : $(TARGET_ARCH)"
|
||||
|
||||
sanity-checks:
|
||||
./scripts/ci/ensure-all-node-requirements-available.sh
|
||||
./scripts/ci/ensure-staged-sass.sh
|
||||
./scripts/ci/ensure-staged-shrinkwrap.sh
|
||||
./scripts/ci/ensure-npm-dependencies-compatibility.sh
|
||||
./scripts/ci/ensure-npm-valid-dependencies.sh
|
||||
./scripts/ci/ensure-npm-shrinkwrap-versions.sh
|
||||
./scripts/ci/ensure-all-file-extensions-in-gitattributes.sh
|
||||
./scripts/ci/ensure-all-text-files-only-ascii.sh
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIRECTORY)
|
||||
|
||||
distclean: clean
|
||||
rm -rf node_modules
|
||||
rm -rf build
|
||||
rm -rf dist
|
||||
rm -rf generated
|
||||
rm -rf $(BUILD_TEMPORARY_DIRECTORY)
|
||||
|
||||
.DEFAULT_GOAL = help
|
||||
|
166
README.md
@@ -1,123 +1,94 @@
|
||||
Etcher
|
||||
======
|
||||
# Etcher
|
||||
|
||||
> Flash OS images to SD cards & USB drives, safely and easily.
|
||||
|
||||
Etcher is a powerful OS image flasher built with web technologies to ensure
|
||||
flashing an SDCard or USB drive is a pleasant and safe experience. It protects
|
||||
you from accidentally writing to your hard-drives, ensures every byte of data
|
||||
was written correctly and much more.
|
||||
was written correctly, and much more. It can also directly flash Raspberry Pi devices that support [USB device boot mode](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#usb-device-boot-mode).
|
||||
|
||||
[](https://etcher.io)
|
||||

|
||||
[](https://travis-ci.org/resin-io/etcher/branches)
|
||||
[](https://ci.appveyor.com/project/resin-io/etcher/branch/master)
|
||||
[](https://david-dm.org/resin-io/etcher)
|
||||
[](https://gitter.im/resin-io/etcher)
|
||||
[](https://waffle.io/resin-io/etcher)
|
||||
[](https://balena.io/etcher)
|
||||
[](https://github.com/balena-io/etcher/blob/master/LICENSE)
|
||||
[](https://forums.balena.io/c/etcher)
|
||||
|
||||
***
|
||||
---
|
||||
|
||||
[**Download**][etcher] | [**Support**][SUPPORT] | [**Documentation**][USER-DOCUMENTATION] | [**Contributing**][CONTRIBUTING] | [**Roadmap**][milestones] | [**CLI**][CLI]
|
||||
[**Download**][etcher] | [**Support**][support] | [**Documentation**][user-documentation] | [**Contributing**][contributing] | [**Roadmap**][milestones]
|
||||
|
||||

|
||||
|
||||
Supported Operating Systems
|
||||
---------------------------
|
||||
## Supported Operating Systems
|
||||
|
||||
- Linux (most distros)
|
||||
- macOS 10.9 and later
|
||||
- macOS 10.10 (Yosemite) and later
|
||||
- Microsoft Windows 7 and later
|
||||
|
||||
Note that Etcher will run on any platform officially supported by
|
||||
**Note**: Etcher will run on any platform officially supported by
|
||||
[Electron][electron]. Read more in their
|
||||
[documentation][electron-supported-platforms].
|
||||
|
||||
Installers
|
||||
----------
|
||||
## Installers
|
||||
|
||||
Refer to the [downloads page][etcher] for the latest pre-made
|
||||
installers for all supported operating systems.
|
||||
|
||||
## Packages
|
||||
|
||||
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
1. Add Etcher debian repository:
|
||||
Package for Debian and Ubuntu can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/)
|
||||
|
||||
```
|
||||
echo "deb https://dl.bintray.com/resin-io/debian stable etcher" | sudo tee /etc/apt/sources.list.d/etcher.list
|
||||
```
|
||||
##### Install .deb file using apt
|
||||
|
||||
2. Trust Bintray.com's GPG key:
|
||||
|
||||
```sh
|
||||
sudo apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 379CE192D401AB61
|
||||
```
|
||||
|
||||
3. Update and install:
|
||||
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install etcher-electron
|
||||
```
|
||||
```sh
|
||||
sudo apt install ./balena-etcher_******_amd64.deb
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo apt-get remove etcher-electron
|
||||
sudo rm /etc/apt/sources.list.d/etcher.list
|
||||
sudo apt-get update
|
||||
```
|
||||
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
|
||||
```sh
|
||||
sudo apt remove balena-etcher
|
||||
```
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
#### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
```sh
|
||||
sudo wget https://bintray.com/resin-io/redhat/rpm -O /etc/yum.repos.d/bintray-resin-io-redhat.repo
|
||||
```
|
||||
##### Yum
|
||||
|
||||
2. Update and install:
|
||||
Package for Fedora-based and Redhat can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/)
|
||||
|
||||
```sh
|
||||
sudo yum install -y etcher-electron
|
||||
```
|
||||
or
|
||||
```sh
|
||||
sudo dnf install -y etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```
|
||||
sudo yum remove -y etcher-electron
|
||||
sudo rm /etc/yum.repos.d/bintray-resin-io-redhat.repo
|
||||
sudo yum clean all
|
||||
sudo yum makecache fast
|
||||
```
|
||||
or
|
||||
```
|
||||
sudo dnf remove -y etcher-electron
|
||||
sudo rm /etc/yum.repos.d/bintray-resin-io-redhat.repo
|
||||
sudo dnf clean all
|
||||
sudo dnf makecache
|
||||
```
|
||||
|
||||
#### Brew Cask (macOS)
|
||||
|
||||
Note that the Etcher Cask has to be updated manually to point to new versions,
|
||||
so it might not refer to the latest version immediately after an Etcher
|
||||
release.
|
||||
1. Install using yum
|
||||
|
||||
```sh
|
||||
brew cask install etcher
|
||||
sudo yum localinstall balena-etcher-***.x86_64.rpm
|
||||
```
|
||||
|
||||
#### Arch/Manjaro Linux (GNU/Linux x64)
|
||||
|
||||
Etcher is offered through the Arch User Repository and can be installed on both Manjaro and Arch systems. You can compile it from the source code in this repository using [`balena-etcher`](https://aur.archlinux.org/packages/balena-etcher/). The following example uses a common AUR helper to install the latest release:
|
||||
|
||||
```sh
|
||||
yay -S balena-etcher
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
brew cask uninstall etcher
|
||||
yay -R balena-etcher
|
||||
```
|
||||
|
||||
### Chocolatey (Windows)
|
||||
#### WinGet (Windows)
|
||||
|
||||
This package is updated by [gh-action](https://github.com/vedantmgoyal2009/winget-releaser), and is kept up to date automatically.
|
||||
|
||||
```sh
|
||||
winget install balenaEtcher #or Balena.Etcher
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
winget uninstall balenaEtcher
|
||||
```
|
||||
|
||||
#### Chocolatey (Windows)
|
||||
|
||||
This package is maintained by [@majkinetor](https://github.com/majkinetor), and
|
||||
is kept up to date automatically.
|
||||
@@ -126,25 +97,28 @@ is kept up to date automatically.
|
||||
choco install etcher
|
||||
```
|
||||
|
||||
Support
|
||||
-------
|
||||
##### Uninstall
|
||||
|
||||
If you're having any problem, please [raise an issue][newissue] on GitHub and
|
||||
the resin.io team will be happy to help.
|
||||
```sh
|
||||
choco uninstall etcher
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
## Support
|
||||
|
||||
Etcher is free software, and may be redistributed under the terms specified in
|
||||
If you're having any problem, please [raise an issue][newissue] on GitHub, and
|
||||
the balena.io team will be happy to help.
|
||||
|
||||
## License
|
||||
|
||||
Etcher is free software and may be redistributed under the terms specified in
|
||||
the [license].
|
||||
|
||||
[etcher]: https://etcher.io
|
||||
[electron]: http://electron.atom.io
|
||||
[electron-supported-platforms]: http://electron.atom.io/docs/tutorial/supported-platforms/
|
||||
[SUPPORT]: https://github.com/resin-io/etcher/blob/master/SUPPORT.md
|
||||
[CONTRIBUTING]: https://github.com/resin-io/etcher/blob/master/docs/CONTRIBUTING.md
|
||||
[CLI]: https://github.com/resin-io/etcher/blob/master/docs/CLI.md
|
||||
[USER-DOCUMENTATION]: https://github.com/resin-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[milestones]: https://github.com/resin-io/etcher/milestones
|
||||
[newissue]: https://github.com/resin-io/etcher/issues/new
|
||||
[license]: https://github.com/resin-io/etcher/blob/master/LICENSE
|
||||
[etcher]: https://balena.io/etcher
|
||||
[electron]: https://electronjs.org/
|
||||
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
|
||||
[support]: https://github.com/balena-io/etcher/blob/master/docs/SUPPORT.md
|
||||
[contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
|
||||
[user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[milestones]: https://github.com/balena-io/etcher/milestones
|
||||
[newissue]: https://github.com/balena-io/etcher/issues/new
|
||||
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE
|
||||
|
33
SUPPORT.md
@@ -1,33 +0,0 @@
|
||||
Getting help with Etcher
|
||||
========================
|
||||
|
||||
There are various ways to get support for Etcher if you experience an issue or
|
||||
have an idea you'd like to share with us.
|
||||
|
||||
Gitter
|
||||
------
|
||||
|
||||
We have a [Gitter chat room][gitter] for Etcher which is open to everyone,
|
||||
please come join us :). Drop us a line there and the resin.io staff and
|
||||
community users will be happy to assist.
|
||||
|
||||
Make sure to mention the following information to help us provide better
|
||||
support:
|
||||
|
||||
- The Etcher version you're running.
|
||||
|
||||
- The operating system you're running Etcher in.
|
||||
|
||||
- Relevant logging output, if any, from DevTools, which you can open by
|
||||
pressing `Ctrl+Alt+I` or `Cmd+Alt+I` depending on your platform.
|
||||
|
||||
GitHub
|
||||
------
|
||||
|
||||
If you encounter an issue or have a suggestion, head on over to Etcher's [issue
|
||||
tracker][issues] and if there isn't a ticket covering it, [create
|
||||
one][new-issue].
|
||||
|
||||
[gitter]: https://gitter.im/resin-io/etcher
|
||||
[issues]: https://github.com/resin-io/etcher/issues
|
||||
[new-issue]: https://github.com/resin-io/etcher/issues/new
|
11
after-install.tpl
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Link to the binary
|
||||
# Must hardcode balenaEtcher directory; no variable available
|
||||
ln -sf '/opt/balenaEtcher/${executable}' '/usr/bin/${executable}'
|
||||
|
||||
# SUID chrome-sandbox for Electron 5+
|
||||
chmod 4755 '/opt/balenaEtcher/chrome-sandbox' || true
|
||||
|
||||
update-mime-database /usr/share/mime || true
|
||||
update-desktop-database /usr/share/applications || true
|
31
afterPack.js
Normal file
@@ -0,0 +1,31 @@
|
||||
'use strict'
|
||||
|
||||
const cp = require('child_process')
|
||||
const fs = require('fs')
|
||||
const outdent = require('outdent')
|
||||
const path = require('path')
|
||||
|
||||
exports.default = function(context) {
|
||||
if (context.packager.platform.name !== 'linux') {
|
||||
return
|
||||
}
|
||||
const scriptPath = path.join(context.appOutDir, context.packager.executableName)
|
||||
const binPath = scriptPath + '.bin'
|
||||
cp.execFileSync('mv', [scriptPath, binPath])
|
||||
fs.writeFileSync(
|
||||
scriptPath,
|
||||
outdent({trimTrailingNewline: false})`
|
||||
#!/bin/bash
|
||||
|
||||
# Resolve symlinks. Warning, readlink -f doesn't work on MacOS/BSD
|
||||
script_dir="$(dirname "$(readlink -f "\${BASH_SOURCE[0]}")")"
|
||||
|
||||
if [[ $EUID -ne 0 ]] || [[ $ELECTRON_RUN_AS_NODE ]]; then
|
||||
"\${script_dir}"/${context.packager.executableName}.bin "$@"
|
||||
else
|
||||
"\${script_dir}"/${context.packager.executableName}.bin "$@" --no-sandbox
|
||||
fi
|
||||
`
|
||||
)
|
||||
cp.execFileSync('chmod', ['+x', scriptPath])
|
||||
}
|
25
afterSignHook.js
Normal file
@@ -0,0 +1,25 @@
|
||||
'use strict'
|
||||
|
||||
const { notarize } = require('electron-notarize')
|
||||
const { ELECTRON_SKIP_NOTARIZATION } = process.env
|
||||
|
||||
async function main(context) {
|
||||
const { electronPlatformName, appOutDir } = context
|
||||
if (electronPlatformName !== 'darwin' || ELECTRON_SKIP_NOTARIZATION === 'true') {
|
||||
return
|
||||
}
|
||||
|
||||
const appName = context.packager.appInfo.productFilename
|
||||
const appleId = process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io'
|
||||
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD
|
||||
|
||||
// https://github.com/electron/notarize/blob/main/README.md
|
||||
await notarize({
|
||||
appBundleId: 'io.balena.etcher',
|
||||
appPath: `${appOutDir}/${appName}.app`,
|
||||
appleId,
|
||||
appleIdPassword
|
||||
})
|
||||
}
|
||||
|
||||
exports.default = main
|
58
appveyor.yml
@@ -1,58 +0,0 @@
|
||||
# appveyor file
|
||||
# http://www.appveyor.com/docs/appveyor-yml
|
||||
|
||||
image: Visual Studio 2015
|
||||
|
||||
# See https://github.com/electron/spectron#on-appveyor
|
||||
os: unstable
|
||||
|
||||
cache:
|
||||
- C:\Users\appveyor\.node-gyp
|
||||
- '%LOCALAPPDATA%\electron\Cache'
|
||||
- '%LOCALAPPDATA%\electron-builder\cache'
|
||||
- '%AppData%\npm-cache'
|
||||
- '%USERPROFILE%\.pkg-cache'
|
||||
- node_modules -> npm-shrinkwrap.json
|
||||
- C:\ProgramData\chocolatey\bin -> appveyor.yml
|
||||
- C:\ProgramData\chocolatey\lib -> appveyor.yml
|
||||
- C:\Users\appveyor\AppData\Local\Temp\chocolatey -> appveyor.yml
|
||||
|
||||
# what combinations to test
|
||||
environment:
|
||||
global:
|
||||
ELECTRON_NO_ATTACH_CONSOLE: true
|
||||
nodejs_version: "6.10.3"
|
||||
|
||||
platform:
|
||||
- x86
|
||||
- x64
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
install:
|
||||
- ps: Update-NodeJsInstallation $env:nodejs_version $env:Platform
|
||||
- set PATH=C:\Program Files (x86)\Windows Kits\8.1\bin\x86;%PATH%
|
||||
- set PATH=C:\Program Files (x86)\NSIS;%PATH%
|
||||
- set PATH=C:\MinGW\bin;%PATH%
|
||||
- set PATH=C:\MinGW\msys\1.0\bin;%PATH%
|
||||
- bash .\scripts\ci\install.sh -o win32 -r %Platform%
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- bash .\scripts\ci\test.sh -o win32 -r %Platform%
|
||||
- bash .\scripts\ci\build-installers.sh -o win32 -r %Platform%
|
||||
|
||||
deploy_script:
|
||||
- if %APPVEYOR_REPO_BRANCH%==master (bash .\scripts\ci\deploy.sh -o win32 -r %Platform%)
|
||||
|
||||
notifications:
|
||||
|
||||
- provider: Webhook
|
||||
url: https://webhooks.gitter.im/e/0becb34b32e20d389bb8
|
||||
on_build_success: false
|
||||
on_build_failure: true
|
||||
on_build_status_changed: true
|
BIN
assets/dmg/background.png
Normal file → Executable file
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 38 KiB |
BIN
assets/dmg/background.tiff
Normal file → Executable file
BIN
assets/dmg/background@2x.png
Normal file → Executable file
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 49 KiB |
BIN
assets/icon.icns
Normal file → Executable file
BIN
assets/icon.ico
Normal file → Executable file
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 361 KiB |
BIN
assets/icon.png
Normal file → Executable file
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 881 B After Width: | Height: | Size: 479 B |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 802 B |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 11 KiB |
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
require('../lib/cli/etcher');
|
25
binding.gyp
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "elevator",
|
||||
"include_dirs" : [
|
||||
"src",
|
||||
"<!(node -e \"require('nan')\")"
|
||||
],
|
||||
'conditions': [
|
||||
|
||||
[ 'OS=="win"', {
|
||||
"sources": [
|
||||
"src/utils/v8utils.cpp",
|
||||
"src/os/win32/elevate.cpp",
|
||||
"src/elevator_init.cpp",
|
||||
],
|
||||
"libraries": [
|
||||
"-lShell32.lib",
|
||||
],
|
||||
} ]
|
||||
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
4
dev-app-update.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
owner: balena-io
|
||||
repo: etcher
|
||||
provider: github
|
||||
updaterCacheDirName: balena-etcher-updater
|
@@ -12,12 +12,9 @@ technologies used in Etcher that you should become familiar with:
|
||||
|
||||
- [Electron][electron]
|
||||
- [NodeJS][nodejs]
|
||||
- [AngularJS][angularjs]
|
||||
- [Redux][redux]
|
||||
- [ImmutableJS][immutablejs]
|
||||
- [Bootstrap][bootstrap]
|
||||
- [Sass][sass]
|
||||
- [Flexbox Grid][flexbox-grid]
|
||||
- [Mocha][mocha]
|
||||
- [JSDoc][jsdoc]
|
||||
|
||||
@@ -40,66 +37,18 @@ to submit their work or bug reports.
|
||||
|
||||
These are the main Etcher components, in a nutshell:
|
||||
|
||||
- [Etcher Image Write][etcher-image-write]
|
||||
|
||||
This is the repository that implements the actual procedures to write an image
|
||||
to a raw device and the place where image validation resides. Its main purpose
|
||||
is to abstract the messy details of interacting with raw devices in all major
|
||||
operating systems.
|
||||
|
||||
- [Etcher Image Stream](../lib/image-stream)
|
||||
|
||||
> (Moved from a separate repository into the main Etcher codebase)
|
||||
|
||||
This module converts any kind of input into a readable stream
|
||||
representing the image so it can be plugged to [etcher-image-write]. Inputs
|
||||
that this module might handle could be, for example: a simple image file, a URL
|
||||
to an image, a compressed image, an image inside a ZIP archive, etc. Together
|
||||
with [etcher-image-write], these modules are the building blocks needed to take
|
||||
an image representation to the user's device, the "Etcher's backend".
|
||||
|
||||
- [Drivelist](https://github.com/resin-io-modules/drivelist)
|
||||
- [Drivelist](https://github.com/balena-io-modules/drivelist)
|
||||
|
||||
As the name implies, this module's duty is to detect the connected drives
|
||||
uniformly in all major operating systems, along with valuable metadata, like if
|
||||
a drive is removable or not, to prevent users from trying to write an image to
|
||||
a system drive.
|
||||
|
||||
- [Etcher](https://github.com/resin-io/etcher)
|
||||
- [Etcher](https://github.com/balena-io/etcher)
|
||||
|
||||
This is the *"main repository"*, from which you're reading this from, which is
|
||||
basically the front-end and glue for all previously listed projects.
|
||||
|
||||
Front-ends
|
||||
----------
|
||||
|
||||
The main repository consists of the implementation of the Etcher CLI and the
|
||||
Etcher GUI (the desktop application), located at [`lib/cli/`][cli-dir] and
|
||||
[`lib/gui/`][gui-dir], respectively.
|
||||
|
||||
In fact, the only front-end that interacts directly with Etcher's backend is
|
||||
the CLI. The GUI merely forks the CLI and communicates with its child process
|
||||
to get state information.
|
||||
|
||||
In this sense, you can consider the GUI as being the front-end to the CLI,
|
||||
which is in turn the front-end to the actual image writing functionality.
|
||||
|
||||
As a way to simplify how the GUI forks the CLI in a packaged and distributed
|
||||
context, both the CLI and GUI share the same application entry point. This
|
||||
means that the same Etcher binary can behave as CLI or GUI as needed.
|
||||
|
||||
## Process communication
|
||||
|
||||
As mentioned before, the Etcher GUI forks the CLI and retrieves information
|
||||
from it to update its state. In order to accomplish this, the Etcher CLI
|
||||
contains certain features to ease communication:
|
||||
|
||||
- [Well-documented exit codes.][exit-codes]
|
||||
|
||||
- An environment variable called `ETCHER_CLI_ROBOT` option, which when set
|
||||
causes the Etcher CLI to output state in a way that can be easily
|
||||
parsed by a machine.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
@@ -110,17 +59,12 @@ since fresh eyes could help unveil things that we take for granted, but should
|
||||
be documented instead!
|
||||
|
||||
[lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328
|
||||
[etcher-image-write]: https://github.com/resin-io-modules/etcher-image-write
|
||||
[exit-codes]: https://github.com/resin-io/etcher/blob/master/lib/shared/exit-codes.js
|
||||
[cli-dir]: https://github.com/resin-io/etcher/tree/master/lib/cli
|
||||
[gui-dir]: https://github.com/resin-io/etcher/tree/master/lib/gui
|
||||
[exit-codes]: https://github.com/balena-io/etcher/blob/master/lib/shared/exit-codes.js
|
||||
[gui-dir]: https://github.com/balena-io/etcher/tree/master/lib/gui
|
||||
[electron]: http://electron.atom.io
|
||||
[nodejs]: https://nodejs.org
|
||||
[angularjs]: https://angularjs.org
|
||||
[redux]: http://redux.js.org
|
||||
[immutablejs]: http://facebook.github.io/immutable-js/
|
||||
[bootstrap]: http://getbootstrap.com
|
||||
[sass]: http://sass-lang.com
|
||||
[flexbox-grid]: http://flexboxgrid.com
|
||||
[mocha]: http://mochajs.org
|
||||
[jsdoc]: http://usejsdoc.org
|
||||
|
@@ -1,61 +0,0 @@
|
||||
### macOS and GNU/Linux
|
||||
|
||||
- Extract the `.tar.gz` package by running:
|
||||
|
||||
```sh
|
||||
tar fvx path/to/cli.tar.gz
|
||||
```
|
||||
|
||||
- Move the resulting directory to `/opt/etcher-cli`
|
||||
|
||||
- Add `/opt/etcher-cli` to the `PATH`. For example, add the following to
|
||||
`.bashrc` or `.zshrc`:
|
||||
|
||||
```sh
|
||||
export PATH="$PATH:/opt/etcher-cli"
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
- Unzip the `.zip` package by right-clicking on it and selecting "Extract All"
|
||||
|
||||
- Move the resulting directory to `C:\etcher-cli`
|
||||
|
||||
- Add `C:\etcher-cli` to the `%PATH%`
|
||||
|
||||
- On Windows 10 and Windows 8
|
||||
- Open *Control Panel*
|
||||
- Open *System*
|
||||
- Click the *Advanced system settings* link
|
||||
- Click *Environment Variables*
|
||||
- Find the `PATH` environment variable, and click *Edit*
|
||||
- Append `;C:\etcher-cli` to the environment variable value
|
||||
- Click *OK*
|
||||
|
||||
- On Windows 7
|
||||
- Right-click the *My Computer* icon
|
||||
- Open the *Properties* menu
|
||||
- Open the *Advanced* tab
|
||||
- Click *Environment Variables*
|
||||
- Find the `PATH` environment variable, and click *Edit*
|
||||
- Append `;C:\etcher-cli` to the environment variable value
|
||||
- Click *OK*
|
||||
|
||||
- Re-open `cmd.exe`, or PowerShell
|
||||
|
||||
### Running
|
||||
|
||||
```sh
|
||||
etcher -v
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--help, -h show help
|
||||
--version, -v show version number
|
||||
--drive, -d drive
|
||||
--check, -c validate write
|
||||
--yes, -y confirm non-interactively
|
||||
--unmount, -u unmount on success
|
||||
```
|
42
docs/CLI.md
@@ -1,42 +0,0 @@
|
||||
Etcher CLI
|
||||
==========
|
||||
|
||||
The Etcher CLI is a command-line tool that aims to provide all the benefits of
|
||||
the Etcher desktop application in a way that can be run from a terminal, or
|
||||
even used from a script.
|
||||
|
||||
In fact, the Etcher desktop application is simply a wrapper around the CLI,
|
||||
which is the place where the actual writing logic takes place.
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
Head over to [etcher.io/cli][etcher-cli], download the package that corresponds to
|
||||
your operating system, and then follow the installation instructions there.
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
```sh
|
||||
etcher -v
|
||||
```
|
||||
|
||||
Options
|
||||
-------
|
||||
|
||||
```
|
||||
--help, -h show help
|
||||
--version, -v show version number
|
||||
--drive, -d drive
|
||||
--check, -c validate write
|
||||
--yes, -y confirm non-interactively
|
||||
--unmount, -u unmount on success
|
||||
```
|
||||
|
||||
Debug mode
|
||||
----------
|
||||
|
||||
You can set the `ETCHER_CLI_DEBUG` environment variable to make the Etcher CLI
|
||||
print error stack traces.
|
||||
|
||||
[etcher-cli]: https://etcher.io/cli
|
@@ -12,72 +12,29 @@ over the commit history.
|
||||
- Be able to automatically reference relevant changes from a dependency
|
||||
upgrade.
|
||||
|
||||
The guidelines are inspired by the [AngularJS git commit
|
||||
guidelines][angular-commit-guidelines].
|
||||
|
||||
Commit structure
|
||||
----------------
|
||||
|
||||
Each commit message consists of a header, a body and a footer. The header has a
|
||||
special format that includes a type, a scope and a subject.
|
||||
Each commit message needs to specify the semver-type. Which can be `patch|minor|major`.
|
||||
See the [Semantic Versioning][semver] specification for a more detailed explanation of the meaning of these types.
|
||||
See balena commit guidelines for more info about the whole commit structure.
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
<semver-type>: <subject>
|
||||
```
|
||||
or
|
||||
```
|
||||
<subject>
|
||||
<BLANK LINE>
|
||||
<body>
|
||||
<details>
|
||||
<BLANK LINE>
|
||||
<footer>
|
||||
Change-Type: <semver-type>
|
||||
```
|
||||
|
||||
The subject should not contain more than 70 characters, including the type and
|
||||
scope, and the body should be wrapped at 72 characters.
|
||||
|
||||
Type
|
||||
----
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
- `feat`: A new feature.
|
||||
- `fix`: A bug fix.
|
||||
- `minifix`: A minimal fix that doesn't warrant an entry in the CHANGELOG.
|
||||
- `docs`: Documentation only changes.
|
||||
- `style`: Changes that do not affect the meaning of the code (white-space,
|
||||
formatting, missing semi-colons, JSDoc annotations, comments, etc).
|
||||
- `refactor`: A code change that neither fixes a bug nor adds a feature.
|
||||
- `perf`: A code change that improves performance.
|
||||
- `test`: Adding missing tests.
|
||||
- `chore`: Changes to the build process or auxiliary tools and libraries.
|
||||
- `upgrade`: A version upgrade of a project dependency.
|
||||
|
||||
Scope
|
||||
-----
|
||||
|
||||
The scope is required for types that make sense, such as `feat`, `fix`,
|
||||
`test`, etc. Certain commit types, such as `chore` might not have a clearly
|
||||
defined scope, in which case its better to omit it.
|
||||
|
||||
When it applies, the scope must be either `GUI` or `CLI`.
|
||||
|
||||
A commit that takes part in both the GUI and CLI scopes, and makes more logical
|
||||
sense that way, might entirely omit the scope.
|
||||
|
||||
Subject
|
||||
-------
|
||||
|
||||
The subject should contain a short description of the change:
|
||||
|
||||
- Use the imperative, present tense.
|
||||
- Don't capitalize the first letter.
|
||||
- No dot (.) at the end.
|
||||
|
||||
Footer
|
||||
------
|
||||
|
||||
The footer contains extra information about the commit, such as tags.
|
||||
|
||||
**Breaking Changes** should start with the word BREAKING CHANGE: with a space
|
||||
or two newlines. The rest of the commit message is then used for this.
|
||||
|
||||
Tags
|
||||
----
|
||||
|
||||
@@ -122,129 +79,8 @@ A commit can include multiple instances of this tag.
|
||||
Examples:
|
||||
|
||||
```
|
||||
Closes: https://github.com/resin-io/etcher/issues/XXX
|
||||
Fixes: https://github.com/resin-io/etcher/issues/XXX
|
||||
Closes: https://github.com/balena-io/etcher/issues/XXX
|
||||
Fixes: https://github.com/balena-io/etcher/issues/XXX
|
||||
```
|
||||
|
||||
### `Change-Type: <type>`
|
||||
|
||||
This tag is used to determine the change type that a commit introduces. The
|
||||
following types are supported:
|
||||
|
||||
- `major`
|
||||
- `minor`
|
||||
- `patch`
|
||||
|
||||
This tag can be omitted for commits that don't change the application from the
|
||||
user's point of view, such as for refactoring commits.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
Change-Type: major
|
||||
Change-Type: minor
|
||||
Change-Type: patch
|
||||
```
|
||||
|
||||
See the [Semantic Versioning][semver] specification for a more detailed
|
||||
explanation of the meaning of these types.
|
||||
|
||||
### `Changelog-Entry: <message>`
|
||||
|
||||
This tag is used to describe the changes introduced by the commit in a more
|
||||
human style that would fit the `CHANGELOG.md` better.
|
||||
|
||||
If the commit type is either `fix` or `feat`, the commit will take part in the
|
||||
CHANGELOG. If this tag is not defined, then the commit subject will be used
|
||||
instead.
|
||||
|
||||
You explicitly can use this tag to make a commit whose type is not `fix` nor
|
||||
`feat` appear in the `CHANGELOG.md`.
|
||||
|
||||
Since whatever your write here will be shown *as it is* in the `CHANGELOG.md`,
|
||||
take some time to write a decent entry. Consider the following guidelines:
|
||||
|
||||
- Use the imperative, present tense.
|
||||
- Capitalize the first letter.
|
||||
|
||||
There is no fixed length limit for the contents of this tag, but always strive
|
||||
to make as short as possible without compromising its quality.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
Changelog-Entry: Fix EPERM errors when flashing to a GPT drive.
|
||||
```
|
||||
|
||||
Complete examples
|
||||
-----------------
|
||||
|
||||
```
|
||||
fix(GUI): ignore extensions before the first non-compressed extension
|
||||
|
||||
Currently, we extract all the extensions from an image path and report back
|
||||
that the image is invalid if *any* of the extensions is not valid , however
|
||||
this can cause trouble with images including information between dots that are
|
||||
not strictly extensions, for example:
|
||||
|
||||
elementaryos-0.3.2-stable-i386.20151209.iso
|
||||
|
||||
Etcher will consider `20151209` to be an invalid extension and therefore
|
||||
will prevent such image from being selected at all.
|
||||
|
||||
As a way to allow these corner cases but still make use of our enforced check
|
||||
controls, the validation routine now only consider extensions starting from the
|
||||
first non compressed extension.
|
||||
|
||||
Change-Type: patch
|
||||
Changelog-Entry: Don't interpret image file name information between dots as image extensions.
|
||||
Fixes: https://github.com/resin-io/etcher/issues/492
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
```
|
||||
upgrade: etcher-image-write to v5.0.2
|
||||
|
||||
This version contains a fix to an `EPERM` issue happening to some Windows user,
|
||||
triggered by the `write` system call during the first ~5% of a flash given that
|
||||
the operating system still thinks the drive has a file system.
|
||||
|
||||
Change-Type: patch
|
||||
Changelog-Entry: Upgrade `etcher-image-write` to v5.0.2.
|
||||
Link: https://github.com/resin-io-modules/etcher-image-write/blob/master/CHANGELOG.md#502---2016-06-27
|
||||
Fixes: https://github.com/resin-io/etcher/issues/531
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
```
|
||||
feat(GUI): implement update notifier functionality
|
||||
|
||||
Auto-update functionality is not ready for usage. As a workaround to
|
||||
prevent users staying with older versions, we now check for updates at
|
||||
startup, and if the user is not running the latest version, we present a
|
||||
modal informing the user of the availiblity of a new version, and
|
||||
provide a call to action to open the Etcher website in his web browser.
|
||||
|
||||
Extra features:
|
||||
|
||||
- The user can skip the update, and tell the program to delay the
|
||||
notification for 7 days.
|
||||
|
||||
Misc changes:
|
||||
|
||||
- Center modal with flexbox, to allow more flexibility on the modal height.
|
||||
interacting with the S3 server.
|
||||
- Implement `ManifestBindService`, which now serves as a backend for the
|
||||
`manifest-bind` directive to allow the directive's functionality to be
|
||||
re-used by other services.
|
||||
- Namespace checkbox styles that are specific to the settings page.
|
||||
|
||||
Change-Type: minor
|
||||
Changelog-Entry: Check for updates and show a modal prompting the user to download the latest version.
|
||||
Closes: https://github.com/resin-io/etcher/issues/396
|
||||
```
|
||||
|
||||
[angular-commit-guidelines]: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit
|
||||
[semver]: http://semver.org
|
||||
|
@@ -17,10 +17,11 @@ Developing
|
||||
|
||||
#### Common
|
||||
|
||||
- [NodeJS](https://nodejs.org) (at least v6)
|
||||
- [Python 2.7](https://www.python.org)
|
||||
- [NodeJS](https://nodejs.org) (at least v16.11)
|
||||
- [Python 3](https://www.python.org)
|
||||
- [jq](https://stedolan.github.io/jq/)
|
||||
- [curl](https://curl.haxx.se/)
|
||||
- [npm](https://www.npmjs.com/)
|
||||
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
@@ -32,16 +33,16 @@ You might need to run this with `sudo` or administrator permissions.
|
||||
|
||||
- [NSIS v2.51](http://nsis.sourceforge.net/Main_Page) (v3.x won't work)
|
||||
- Either one of the following:
|
||||
- [Visual C++ 2015 Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools) containing standalone compilers, libraries and scripts
|
||||
- Install the [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) via npm with `npm install --global windows-build-tools`
|
||||
- [Visual Studio Community 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48146) (free) (other editions, like Professional and Enterprise, should work too)
|
||||
**NOTE:** Visual Studio 2015 doesn't install C++ by default. You have to rerun the
|
||||
- [Visual C++ 2019 Build Tools](https://visualstudio.microsoft.com/vs/features/cplusplus/) containing standalone compilers, libraries and scripts
|
||||
- The [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools#windows-build-tools) should be installed along with NodeJS
|
||||
- [Visual Studio Community 2019](https://visualstudio.microsoft.com/vs/) (free) (other editions, like Professional and Enterprise, should work too)
|
||||
**NOTE:** Visual Studio doesn't install C++ by default. You have to rerun the
|
||||
setup, select "Modify" and then check `Visual C++ -> Common Tools for Visual
|
||||
C++ 2015` (see http://stackoverflow.com/a/31955339)
|
||||
C++` (see http://stackoverflow.com/a/31955339)
|
||||
- [MinGW](http://www.mingw.org)
|
||||
|
||||
You might need to `npm config set msvs_version 2015` for node-gyp to correctly detect
|
||||
the version of Visual Studio you're using (in this example VS2015).
|
||||
You might need to `npm config set msvs_version 2019` for node-gyp to correctly detect
|
||||
the version of Visual Studio you're using (in this example VS2019).
|
||||
|
||||
The following MinGW packages are required:
|
||||
|
||||
@@ -51,51 +52,35 @@ The following MinGW packages are required:
|
||||
- `msys-bash`
|
||||
- `msys-coreutils`
|
||||
|
||||
#### OS X
|
||||
#### macOS
|
||||
|
||||
- [XCode](https://developer.apple.com/xcode/) or [XCode Command Line Tools],
|
||||
which can be installed by running `xcode-select --install`.
|
||||
- [Xcode](https://developer.apple.com/xcode/)
|
||||
|
||||
It's not enough to have [Xcode Command Line Tools] installed. Xcode must be installed
|
||||
as well.
|
||||
|
||||
#### Linux
|
||||
|
||||
- `libudev-dev` for libusb (install with `sudo apt install libudev-dev` for example)
|
||||
- `libudev-dev` for libusb (for example install with `sudo apt install libudev-dev`, or on fedora `systemd-devel` contains the required package)
|
||||
|
||||
### Cloning the project
|
||||
|
||||
```sh
|
||||
git clone https://github.com/resin-io/etcher
|
||||
git clone --recursive https://github.com/balena-io/etcher
|
||||
cd etcher
|
||||
```
|
||||
|
||||
### Installing npm dependencies
|
||||
|
||||
**NOTE:** Please make use of the following command to install npm dependencies rather
|
||||
than simply running `npm install` given that we need to do extra configuration
|
||||
to make sure native dependencies are correctly compiled for Electron, otherwise
|
||||
the application might not run successfully.
|
||||
|
||||
If you're on Windows, **run the command from the _Developer Command Prompt for
|
||||
VS2015_**, to ensure all Visual Studio command utilities are available in the
|
||||
`%PATH%`.
|
||||
|
||||
```sh
|
||||
make electron-develop
|
||||
```
|
||||
|
||||
### Running the application
|
||||
|
||||
#### GUI
|
||||
|
||||
```sh
|
||||
# Build the GUI
|
||||
npm run webpack #or npm run build
|
||||
# Start Electron
|
||||
npm start
|
||||
```
|
||||
|
||||
#### CLI
|
||||
|
||||
```sh
|
||||
node bin/etcher
|
||||
```
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
@@ -119,10 +104,6 @@ systems as they can before sending a pull request.
|
||||
*The test suite is run automatically by CI servers when you send a pull
|
||||
request.*
|
||||
|
||||
We also rely on various `make` targets to perform some common tasks:
|
||||
|
||||
- `make lint`: Run the linter.
|
||||
- `make sass`: Compile SCSS files.
|
||||
|
||||
We make use of [EditorConfig] to communicate indentation, line endings and
|
||||
other text editing default. We encourage you to install the relevant plugin in
|
||||
@@ -132,20 +113,7 @@ process.
|
||||
Updating a dependency
|
||||
---------------------
|
||||
|
||||
Given we use [npm shrinkwrap][shrinkwrap], we have to take extra steps to make
|
||||
sure the `npm-shrinkwrap.json` file gets updated correctly when we update a
|
||||
dependency.
|
||||
|
||||
Use the following steps to ensure everything goes flawlessly:
|
||||
|
||||
- Run `make electron-develop` to ensure you don't have extraneous dependencies
|
||||
you might have brought during development, or you are running older
|
||||
dependencies because you come from another branch or reference.
|
||||
|
||||
- Install the new version of the dependency. For example: `npm install --save
|
||||
<package>@<version>`. This will update the `npm-shrinkwrap.json` file.
|
||||
|
||||
- Commit *both* `package.json` and `npm-shrinkwrap.json`.
|
||||
- Commit *both* `package.json` and `package-lock.json`.
|
||||
|
||||
Diffing Binaries
|
||||
----------------
|
||||
@@ -201,7 +169,7 @@ Before your pull request can be merged, the following conditions must hold:
|
||||
|
||||
- The linter doesn't throw any warning.
|
||||
|
||||
- All the tests passes.
|
||||
- All the tests pass.
|
||||
|
||||
- The coding style aligns with the project's convention.
|
||||
|
||||
@@ -210,9 +178,9 @@ systems we support.
|
||||
|
||||
Don't hesitate to get in touch if you have any questions or need any help!
|
||||
|
||||
[ARCHITECTURE]: https://github.com/resin-io/etcher/blob/master/docs/ARCHITECTURE.md
|
||||
[COMMIT-GUIDELINES]: https://github.com/resin-io/etcher/blob/master/docs/COMMIT-GUIDELINES.md
|
||||
[ARCHITECTURE]: https://github.com/balena-io/etcher/blob/master/docs/ARCHITECTURE.md
|
||||
[COMMIT-GUIDELINES]: https://github.com/balena-io/etcher/blob/master/docs/COMMIT-GUIDELINES.md
|
||||
[EditorConfig]: http://editorconfig.org
|
||||
[shrinkwrap]: https://docs.npmjs.com/cli/shrinkwrap
|
||||
[hxd]: https://github.com/jhermsmeier/hxd
|
||||
[XCode Command Line Tools]: https://developer.apple.com/library/content/technotes/tn2339/_index.html
|
||||
[Xcode Command Line Tools]: https://developer.apple.com/library/content/technotes/tn2339/_index.html
|
||||
|
52
docs/FAQ.md
Normal file
@@ -0,0 +1,52 @@
|
||||
## Why is my drive not bootable?
|
||||
|
||||
Etcher copies images to drives byte by byte, without doing any transformation to the final device, which means images that require special treatment to be made bootable, like Windows images, will not work out of the box. In these cases, the general advice is to use software specific to those kind of images, usually available from the image publishers themselves. You can find more information [here](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#why-is-my-drive-not-bootable).
|
||||
|
||||
## How can I configure persistent storage?
|
||||
|
||||
Some programs, usually oriented at making GNU/Linux live USB drives, include an option to set persistent storage. This is currently not supported by Etcher, so if you require this functionality, we advise to fallback to [UNetbootin](https://unetbootin.github.io/).
|
||||
|
||||
## How do I flash Ubuntu ISOs
|
||||
|
||||
Ubuntu images (and potentially some other related GNU/Linux distributions) have a peculiar format that allows the image to boot without any further modification from both CDs and USB drives.
|
||||
A consequence of this enhancement is that some programs, like parted get confused about the drive's format and partition table, printing warnings such as:
|
||||
|
||||
> /dev/xxx contains GPT signatures, indicating that it has a GPT table. However, it does not have a valid fake msdos partition table, as it should. Perhaps it was corrupted -- possibly by a program that doesn't understand GPT partition tables. Or perhaps you deleted the GPT table, and are now using an msdos partition table. Is this a GPT partition table? Both the primary and backup GPT tables are corrupt. Try making a fresh table, and using Parted's rescue feature to recover partitions.
|
||||
|
||||
> Warning: The driver descriptor says the physical block size is 2048 bytes, but Linux says it is 512 bytes.
|
||||
|
||||
All these warnings are safe to ignore, and your drive should be able to boot without any problems.
|
||||
Refer to [the following message from Ubuntu's mailing list](https://lists.ubuntu.com/archives/ubuntu-devel/2011-June/033495.html) if you want to learn more.
|
||||
|
||||
## How do I run Etcher on Wayland?
|
||||
|
||||
The XWayland Server provides backwards compatibility to run any X client on Wayland, including Etcher.
|
||||
This usually works out of the box on mainstream GNU/Linux distributions that properly support Wayland. If it doesn't, make sure the xwayland.so module is being loaded by declaring it in your [weston.ini](http://manpages.ubuntu.com/manpages/wily/man5/weston.ini.5.html):
|
||||
|
||||
```
|
||||
[core]
|
||||
modules=xwayland.so
|
||||
```
|
||||
|
||||
## What are the runtime GNU/LINUX dependencies?
|
||||
|
||||
[This entry](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#runtime-gnulinux-dependencies) aims to provide an up to date list of runtime dependencies needed to run Etcher on a GNU/Linux system.
|
||||
|
||||
## How can I recover the broken drive?
|
||||
|
||||
Sometimes, things might go wrong, and you end up with a half-flashed drive that is unusable by your operating systems, and common graphical tools might even refuse to get it back to a normal state.
|
||||
To solve these kinds of problems, we've collected [a list of fail-proof methods](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#recovering-broken-drives) to completely erase your drive in major operating systems.
|
||||
|
||||
## I receive "No polkit authentication agent found" error in GNU/Linux
|
||||
|
||||
Etcher requires an available [polkit authentication agent](https://wiki.archlinux.org/index.php/Polkit#Authentication_agents) in your system in order to show a secure password prompt dialog to perform elevation. Make sure you have one installed for the desktop environment of your choice.
|
||||
|
||||
## May I run Etcher in older macOS versions?
|
||||
|
||||
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.10 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
|
||||
|
||||
## Can I use the Flash With Etcher button on my site?
|
||||
|
||||
You can use the Flash with Etcher button on your site or blog, if you have an OS that you want your users to be able to easily flash using Etcher, add the following code where you want to button to be:
|
||||
|
||||
`<a href="https://efp.balena.io/open-image-url?imageUrl=<your image URL>"><img src="http://balena.io/flash-with-etcher.png" /></a>`
|
@@ -8,70 +8,41 @@ Releasing
|
||||
|
||||
### Release Types
|
||||
|
||||
- **snapshot** (default): A continues snapshot of current master, made by the CI services
|
||||
- **production**: Full releases
|
||||
- **draft**: A continues snapshot of current master, made by the CI services
|
||||
- **pre-release** (default): A continues snapshot of current master, made by the CI services
|
||||
- **release**: Full releases
|
||||
|
||||
Draft release is created from each PR, tagged with the branch name.
|
||||
All merged PR will generate a new tag/version as a *pre-release*.
|
||||
Mark the pre-release as final when it is necessary, then distribute the packages in alternative channels as necessary.
|
||||
|
||||
### Flight Plan
|
||||
|
||||
#### Preparation
|
||||
|
||||
- [Prepare the new version](#preparing-a-new-version)
|
||||
- [Generate build artefacts](#generating-binaries) (binaries, archives, etc.)
|
||||
- [Draft a release on GitHub](https://github.com/resin-io/etcher/releases)
|
||||
- Upload build artefacts to GitHub release draft
|
||||
- [Generate build artifacts](#generating-binaries) (binaries, archives, etc.)
|
||||
- [Draft a release on GitHub](https://github.com/balena-io/etcher/releases)
|
||||
- Upload build artifacts to GitHub release draft
|
||||
|
||||
#### Testing
|
||||
|
||||
- Test the prepared release and build artefacts properly on **all supported operating systems** to prevent regressions that went uncaught by the CI tests (see [MANUAL-TESTING.md](MANUAL-TESTING.md))
|
||||
- Test the prepared release and build artifacts properly on **all supported operating systems** to prevent regressions that went uncaught by the CI tests (see [MANUAL-TESTING.md](MANUAL-TESTING.md))
|
||||
- If regressions or other issues arise, create issues on the repository for each one, and decide whether to fix them in this release (meaning repeating the process up until this point), or to follow up with a patch release
|
||||
|
||||
#### Publishing
|
||||
|
||||
- [Publish release draft on GitHub](https://github.com/resin-io/etcher/releases)
|
||||
- [Post release note to forums](https://forums.resin.io/c/etcher)
|
||||
- [Update the website](https://github.com/resin-io/etcher-homepage)
|
||||
- Wait 2-3 hours for analytics (Sentry, Mixpanel) to trickle in and check for elevated error rates, or regressions
|
||||
- [Publish release draft on GitHub](https://github.com/balena-io/etcher/releases)
|
||||
- [Post release note to forums](https://forums.balena.io/c/etcher)
|
||||
- [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec)
|
||||
- [Update the website](https://github.com/balena-io/etcher-homepage)
|
||||
- Wait 2-3 hours for analytics (Sentry, Amplitude) to trickle in and check for elevated error rates, or regressions
|
||||
- If regressions arise; pull the release, and release a patched version, else:
|
||||
- [Upload deb & rpm packages to Bintray](#uploading-packages-to-bintray)
|
||||
- [Upload build artefacts to Amazon S3](#uploading-binaries-to-amazon-s3)
|
||||
- Post changelog with `#release-notes` tag on Flowdock
|
||||
- [Upload deb & rpm packages to Cloudfront](#uploading-packages-to-cloudfront)
|
||||
- Post changelog with `#release-notes` tag on internal chat
|
||||
- If this release packs noteworthy major changes:
|
||||
- Write a blog post about it, and / or
|
||||
- Write about it to the Etcher mailing list
|
||||
|
||||
### Preparing a New Version
|
||||
|
||||
- Create & hop onto a new release branch, i.e. `release-1.0.0`
|
||||
- Bump the version number in the `package.json`'s `version` property.
|
||||
- Bump the version number in the `npm-shrinkwrap.json`'s `version` property
|
||||
- Add a new entry to `CHANGELOG.md` by running `make changelog`
|
||||
- Manually revise the `CHANGELOG.md` versionist output
|
||||
- Update `screenshot.png` so it displays the latest version in the bottom
|
||||
right corner
|
||||
- Revise the `updates.semverRange` version in `package.json`
|
||||
- Commit the changes with the version number as the commit title, including the `v` prefix, to `master`. For example:
|
||||
|
||||
**NOTE:** The version **MUST** be prefixed with a "v"
|
||||
|
||||
```bash
|
||||
git commit -m "v1.0.0" # not 1.0.0
|
||||
```
|
||||
|
||||
- Create an annotated tag for the new version. The commit title should equal the annotated tag name. For example:
|
||||
|
||||
```bash
|
||||
git tag -a v1.0.0 -m "v1.0.0"
|
||||
```
|
||||
|
||||
- Push the commit and the annotated tag.
|
||||
|
||||
```bash
|
||||
git push
|
||||
git push --tags
|
||||
```
|
||||
|
||||
- Open a pull request against `master` titled "Release v1.0.0"
|
||||
|
||||
### Generating binaries
|
||||
|
||||
**Environment**
|
||||
@@ -80,7 +51,7 @@ Make sure to set the analytics tokens when generating production release binarie
|
||||
|
||||
```bash
|
||||
export ANALYTICS_SENTRY_TOKEN="xxxxxx"
|
||||
export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
|
||||
export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx"
|
||||
```
|
||||
|
||||
#### Linux
|
||||
@@ -89,97 +60,28 @@ export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
|
||||
|
||||
**NOTE:** Make sure to adjust the path as necessary (here the Etcher repository has been cloned to `/home/$USER/code/etcher`)
|
||||
|
||||
```bash
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make distclean"
|
||||
```
|
||||
##### Generating artifacts
|
||||
|
||||
##### Generating artefacts
|
||||
|
||||
```bash
|
||||
# x64
|
||||
|
||||
# Build Debian packages
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
|
||||
# Build RPM packages
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
|
||||
# Build AppImages
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
|
||||
# Build CLI
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production cli-installer-tar-gz"
|
||||
|
||||
# x86
|
||||
|
||||
# Build Debian packages
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
|
||||
# Build RPM packages
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
|
||||
# Build AppImages
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
|
||||
# Build CLI
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production cli-installer-tar-gz"
|
||||
```
|
||||
The artifacts are generated by the CI and published as draft-release or pre-release.
|
||||
`electron-builder` is used to create the packaged application.
|
||||
|
||||
#### Mac OS
|
||||
|
||||
**ATTENTION:** For production releases you'll need the code-signing key,
|
||||
and set `CSC_NAME` to generate signed binaries on Mac OS.
|
||||
|
||||
**NOTE:** The CLI is not code-signed for either at this time.
|
||||
|
||||
```bash
|
||||
make electron-develop
|
||||
|
||||
# Build the CLI
|
||||
make RELEASE_TYPE=production cli-installer-tar-gz
|
||||
# Build the zip
|
||||
make RELEASE_TYPE=production electron-installer-app-zip
|
||||
# Build the dmg
|
||||
make RELEASE_TYPE=production electron-installer-dmg
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
**ATTENTION:** For production releases you'll need the code-signing key,
|
||||
and set `CSC_LINK`, and `CSC_KEY_PASSWORD` to generate signed binaries on Windows.
|
||||
|
||||
**NOTE:**
|
||||
- The CLI is not code-signed for either at this time.
|
||||
- Keep in mind to also generate artefacts for x86, with `TARGET_ARCH=x86`.
|
||||
- Keep in mind to also generate artifacts for x86, with `TARGET_ARCH=x86`.
|
||||
|
||||
```bash
|
||||
make electron-develop
|
||||
|
||||
# Build the CLI
|
||||
make RELEASE_TYPE=production cli-installer-zip
|
||||
# Build the Portable version
|
||||
make RELEASE_TYPE=production electron-installer-portable
|
||||
# Build the Installer
|
||||
make RELEASE_TYPE=production electron-installer-nsis
|
||||
```
|
||||
### Uploading packages to Cloudfront
|
||||
|
||||
### Uploading packages to Bintray
|
||||
|
||||
```bash
|
||||
export BINTRAY_USER="username@account"
|
||||
export BINTRAY_API_KEY="youruserapikey"
|
||||
```
|
||||
|
||||
```bash
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "debian" -y "debian" -r "x64" -f "dist/etcher-electron_1.2.1_amd64.deb"
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "debian" -y "debian" -r "x86" -f "dist/etcher-electron_1.2.1_i386.deb"
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "redhat" -y "redhat" -r "x64" -f "dist/etcher-electron-1.2.1.x86_64.rpm"
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "redhat" -y "redhat" -r "x86" -f "dist/etcher-electron-1.2.1.i686.rpm"
|
||||
```
|
||||
|
||||
### Uploading binaries to Amazon S3
|
||||
|
||||
```bash
|
||||
export S3_KEY="..."
|
||||
```
|
||||
|
||||
```bash
|
||||
./scripts/publish/aws-s3.sh -b "resin-production-downloads" -v "1.2.1" -p "etcher" -f "dist/<filename>"
|
||||
```
|
||||
Log in to cloudfront and upload the `rpm` and `deb` files.
|
||||
|
||||
### Dealing with a Problematic Release
|
||||
|
||||
@@ -189,7 +91,7 @@ revert the problematic release as soon as possible, until the bugs are fixed.
|
||||
|
||||
You can revert a version by deleting its builds from the S3 bucket and Bintray.
|
||||
Refer to the `Makefile` for the up to date information about the S3 bucket
|
||||
where we push builds to, and get in touch with the resin.io operations team to
|
||||
where we push builds to, and get in touch with the balena.io operations team to
|
||||
get write access to it.
|
||||
|
||||
The Etcher update notifier dialog and the website only show the a certain
|
||||
@@ -203,3 +105,15 @@ aws s3api delete-object --bucket <bucket name> --key <file name>
|
||||
```
|
||||
|
||||
The Bintray dashboard provides an easy way to delete a version's files.
|
||||
|
||||
|
||||
### Submitting binaries to Symantec
|
||||
|
||||
- [Report a Suspected Erroneous Detection](https://submit.symantec.com/false_positive/standard/)
|
||||
- Fill out form:
|
||||
- **Select Submission Type:** "Provide a direct download URL"
|
||||
- **Name of the software being detected:** Etcher
|
||||
- **Name of detection given by Symantec product:** WS.Reputation.1
|
||||
- **Contact name:** Balena.io Ltd
|
||||
- **E-mail address:** hello@etcher.io
|
||||
- **Are you the creator or distributor of the software in question?** Yes
|
||||
|
@@ -112,4 +112,4 @@ Analytics
|
||||
- [ ] Disable analytics, open DevTools Network pane or a packet sniffer, and
|
||||
check that no request is sent
|
||||
- [ ] **Disable analytics, refresh application from DevTools (using Cmd-R or
|
||||
F5), and check that initial events are not sent to Mixpanel**
|
||||
F5), and check that initial events are not sent to Amplitude**
|
||||
|
@@ -7,44 +7,9 @@ systems.
|
||||
Release Types
|
||||
-------------
|
||||
|
||||
Etcher supports **production** and **snapshot** release types. Each is
|
||||
published to a different S3 bucket, and production release types are code
|
||||
signed, while snapshot release types aren't and include a short git commit-hash
|
||||
as a build number. For example, `1.0.0-beta.19` is a production release type,
|
||||
while `1.0.0-beta.19+531ab82` is a snapshot release type.
|
||||
|
||||
In terms of comparison: `1.0.0-beta.19` (production) < `1.0.0-beta.19+531ab82`
|
||||
(snapshot) < `1.0.0-rc.1` (production) < `1.0.0-rc.1+7fde24a` (snapshot) <
|
||||
`1.0.0` (production) < `1.0.0+2201e5f` (snapshot). Keep in mind that if you're
|
||||
running a production release type, you'll only be prompted to update to
|
||||
production release types, and if you're running a snapshot release type, you'll
|
||||
only be prompted to update to other snapshot release types.
|
||||
|
||||
The build system creates (and publishes) snapshot release types by default, but
|
||||
you can build a specific release type by setting the `RELEASE_TYPE` make
|
||||
variable. For example:
|
||||
|
||||
```sh
|
||||
make <target> RELEASE_TYPE=snapshot
|
||||
make <target> RELEASE_TYPE=production
|
||||
```
|
||||
|
||||
We can control the version range a specific Etcher version will consider when
|
||||
showing the update notification dialog by tweaking the `updates.semverRange`
|
||||
property of `package.json`.
|
||||
|
||||
Update Channels
|
||||
---------------
|
||||
|
||||
Etcher has a setting to include the unstable update channel. If this option is
|
||||
set, Etcher will consider both stable and unstable versions when showing the
|
||||
update notifier dialog. Unstable versions are the ones that contain a `beta`
|
||||
pre-release tag. For example:
|
||||
|
||||
- Production unstable version: `1.4.0-beta.1`
|
||||
- Snapshot unstable version: `1.4.0-beta.1+7fde24a`
|
||||
- Production stable version: `1.4.0`
|
||||
- Snapshot stable version: `1.4.0+7fde24a`
|
||||
Etcher supports **pre-release** and **final** release types as does Github. Each is
|
||||
published to Github releases.
|
||||
The release version is generated automatically from the commit messasges.
|
||||
|
||||
Signing
|
||||
-------
|
||||
@@ -52,7 +17,7 @@ Signing
|
||||
### OS X
|
||||
|
||||
1. Get our Apple Developer ID certificate for signing applications distributed
|
||||
outside the Mac App Store from the resin.io Apple account.
|
||||
outside the Mac App Store from the balena.io Apple account.
|
||||
|
||||
2. Install the Developer ID certificate to your Mac's Keychain by double
|
||||
clicking on the certificate file.
|
||||
@@ -62,7 +27,7 @@ packaging for OS X.
|
||||
|
||||
### Windows
|
||||
|
||||
1. Get access to our code signing certificate and decryption key as a resin.io
|
||||
1. Get access to our code signing certificate and decryption key as a balena.io
|
||||
employee by asking for it from the relevant people.
|
||||
|
||||
2. Place the certificate in the root of the Etcher repository naming it
|
||||
@@ -73,63 +38,19 @@ Packaging
|
||||
|
||||
The resulting installers will be saved to `dist/out`.
|
||||
|
||||
Run the following commands:
|
||||
|
||||
### OS X
|
||||
Run the following commands on all platforms with the right arguments:
|
||||
|
||||
```sh
|
||||
make electron-installer-dmg
|
||||
make electron-installer-app-zip
|
||||
./node_modules/electron-builder build <...>
|
||||
```
|
||||
|
||||
### GNU/Linux
|
||||
|
||||
```sh
|
||||
make electron-installer-appimage
|
||||
make electron-installer-debian
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```sh
|
||||
make electron-installer-zip
|
||||
make electron-installer-nsis
|
||||
```
|
||||
|
||||
Publishing to Bintray
|
||||
Publishing to Cloudfront
|
||||
---------------------
|
||||
|
||||
We publish GNU/Linux Debian packages to [Bintray][bintray].
|
||||
We publish GNU/Linux Debian packages to [Cloudfront][cloudfront].
|
||||
|
||||
Make sure you set the following environment variables:
|
||||
|
||||
- `BINTRAY_USER`
|
||||
- `BINTRAY_API_KEY`
|
||||
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
make publish-bintray-debian
|
||||
```
|
||||
|
||||
Publishing to S3
|
||||
----------------
|
||||
|
||||
- [AWS CLI][aws-cli]
|
||||
|
||||
Make sure you have the [AWS CLI tool][aws-cli] installed and configured to
|
||||
access resin.io's production or snapshot S3 bucket.
|
||||
|
||||
Run the following command to publish all files for the current combination of
|
||||
_platform_ and _arch_ (building them if necessary):
|
||||
|
||||
```sh
|
||||
make publish-aws-s3
|
||||
```
|
||||
|
||||
Also add links to each AWS S3 file in [GitHub Releases][github-releases]. See
|
||||
[`v1.0.0-beta.17`](https://github.com/resin-io/etcher/releases/tag/v1.0.0-beta.17)
|
||||
as an example.
|
||||
Log in to cloudfront and upload the `rpm` and `deb` files.
|
||||
|
||||
Publishing to Homebrew Cask
|
||||
---------------------------
|
||||
@@ -143,14 +64,16 @@ Publishing to Homebrew Cask
|
||||
Announcing
|
||||
----------
|
||||
|
||||
Post messages to the [Etcher forum][resin-forum-etcher] and
|
||||
[Etcher gitter channel][gitter-etcher] announcing the new version
|
||||
Post messages to the [Etcher forum][balena-forum-etcher] announcing the new version
|
||||
of Etcher, and including the relevant section of the Changelog.
|
||||
|
||||
[aws-cli]: https://aws.amazon.com/cli
|
||||
[bintray]: https://bintray.com
|
||||
[etcher-cask-file]: https://github.com/caskroom/homebrew-cask/blob/master/Casks/etcher.rb
|
||||
[cloudfront]: https://cloudfront.com
|
||||
[etcher-cask-file]: https://github.com/caskroom/homebrew-cask/blob/master/Casks/balenaetcher.rb
|
||||
[homebrew-cask]: https://github.com/caskroom/homebrew-cask
|
||||
[resin-forum-etcher]: https://forums.resin.io/c/etcher
|
||||
[gitter-etcher]: https://gitter.im/resin-io/etcher
|
||||
[github-releases]: https://github.com/resin-io/etcher/releases
|
||||
[balena-forum-etcher]: https://forums.balena.io/c/etcher
|
||||
[github-releases]: https://github.com/balena-io/etcher/releases
|
||||
|
||||
Updating EFP / Success-Banner
|
||||
-----------------------------
|
||||
Etcher Featured Project is automatically run based on an algorithm which promoted projects from the balena marketplace which have been contributed by the community, the algorithm prioritises projects which give uses the best experience. Editing both EFP and the Etcher Success-Banner can only be done by someone from balena, instruction are on the [Etcher-EFP repo (private)](https://github.com/balena-io/etcher-efp)
|
||||
|
43
docs/SUPPORT.md
Normal file
@@ -0,0 +1,43 @@
|
||||
Getting help with BalenaEtcher
|
||||
===============================
|
||||
|
||||
There are various ways to get support for Etcher if you experience an issue or
|
||||
have an idea you'd like to share with us.
|
||||
|
||||
Documentation
|
||||
------
|
||||
|
||||
We have answers to a variety of frequently asked questions in the [user
|
||||
documentation][documentation] and also in the [FAQs][faq] on the Etcher website.
|
||||
|
||||
|
||||
Forums
|
||||
------
|
||||
|
||||
We have a [Discourse forum][discourse] which is open to everyone, so please
|
||||
come join us :). Drop us a line there and the balena.io staff and community
|
||||
users will be happy to assist. Your question might already be answered, so take
|
||||
a look at the existing threads before opening a new one!
|
||||
|
||||
Make sure to mention the following information to help us provide better
|
||||
support:
|
||||
|
||||
- The BalenaEtcher version you're running.
|
||||
|
||||
- The operating system you're running Etcher in.
|
||||
|
||||
- Relevant logging output, if any, from DevTools, which you can open by
|
||||
pressing `Ctrl+Shift+I` or `Cmd+Alt+I` depending on your platform.
|
||||
|
||||
GitHub
|
||||
------
|
||||
|
||||
If you encounter an issue or have a suggestion, head on over to BalenaEtcher's [issue
|
||||
tracker][issues] and if there isn't a ticket covering it, [create
|
||||
one][new-issue].
|
||||
|
||||
[discourse]: https://forums.balena.io/c/etcher
|
||||
[issues]: https://github.com/balena-io/etcher/issues
|
||||
[new-issue]: https://github.com/balena-io/etcher/issues/new
|
||||
[documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[faq]: https://etcher.io
|
@@ -3,6 +3,11 @@ Etcher User Documentation
|
||||
|
||||
This document contains how-tos and FAQs oriented to Etcher users.
|
||||
|
||||
Config
|
||||
------
|
||||
Etcher's configuration is saved to the `config.json` file in the apps folder.
|
||||
Not all the options are surfaced to the UI. You may edit this file to tweak settings even before launching the app.
|
||||
|
||||
Why is my drive not bootable?
|
||||
-----------------------------
|
||||
|
||||
@@ -14,8 +19,8 @@ images, usually available from the image publishers themselves.
|
||||
|
||||
Images known to require special treatment:
|
||||
|
||||
- Microsoft Windows (use [Windows USB/DVD Download Tool][windows-usb-tool], or
|
||||
[Rufus][rufus]).
|
||||
- Microsoft Windows (use [Windows USB/DVD Download Tool][windows-usb-tool],
|
||||
[Rufus][rufus], or [WoeUSB][woeusb]).
|
||||
|
||||
- Windows 10 IoT (use the [Windows 10 IoT Core Dashboard][windows-iot-dashboard])
|
||||
|
||||
@@ -30,7 +35,7 @@ if you require this functionality, we advise to fallback to
|
||||
Deactivate desktop shortcut prompt on GNU/Linux
|
||||
-----------------------------------------------
|
||||
|
||||
This is a feature provided by [AppImages](appimage), where the applications
|
||||
This is a feature provided by [AppImages][appimage], where the applications
|
||||
prompts the user to automatically register a desktop shortcut to easily access
|
||||
the application.
|
||||
|
||||
@@ -130,21 +135,6 @@ run Etcher on a GNU/Linux system.
|
||||
|
||||
- liblzma (for xz decompression)
|
||||
|
||||
Simulate an update alert
|
||||
------------------------
|
||||
|
||||
You can set the `ETCHER_FAKE_S3_LATEST_VERSION` environment variable to a valid
|
||||
semver version (greater than the current version) to trick the application into
|
||||
thinking that what you put there is the latest available version, therefore
|
||||
causing the update notification dialog to be presented at startup.
|
||||
|
||||
Note that the value of the variable will be ignored if it doesn't match the
|
||||
release type of the current application version. For example, setting the
|
||||
variable to a production version (e.g. `ETCHER_FAKE_S3_LATEST_VERSION=2.0.0`)
|
||||
will be ignored if you're running a snapshot build, and vice-versa.
|
||||
|
||||
See [`PUBLISHING.md`][publishing] for more details about release types.
|
||||
|
||||
Recovering broken drives
|
||||
------------------------
|
||||
|
||||
@@ -175,13 +165,25 @@ pre-installed in all modern Windows versions.
|
||||
- Run `clean`. This command will completely clean your drive by erasing any
|
||||
existent filesystem.
|
||||
|
||||
- Run `create partition primary`. This command will create a new partition.
|
||||
|
||||
- Run `active`. This command will active the partition.
|
||||
|
||||
- Run `list partition`. This command will show available partition.
|
||||
|
||||
- Run `select partition N`, where `N` corresponds to the id of the newly available partition.
|
||||
|
||||
- Run `format override quick`. This command will format the partition. You can choose a specific formatting by adding `FS=xx` where `xx` could be `NTFS or FAT or FAT32` after `format`. Example : `format FS=NTFS override quick`
|
||||
|
||||
- Run `exit` to quit diskpart.
|
||||
|
||||
### OS X
|
||||
|
||||
Run the following command in `Terminal.app`, replacing `N` by the corresponding
|
||||
disk number, which you can find by running `diskutil list`:
|
||||
|
||||
```sh
|
||||
diskutil eraseDisk free UNTITLED /dev/diskN
|
||||
diskutil eraseDisk FAT32 UNTITLED MBRFormat /dev/diskN
|
||||
```
|
||||
|
||||
### GNU/Linux
|
||||
@@ -206,21 +208,20 @@ Running in older macOS versions
|
||||
-------------------------------
|
||||
|
||||
Etcher GUI is based on the [Electron][electron] framework, [which only supports
|
||||
macOS 10.9 and newer versions][electron-supported-platforms].
|
||||
macOS 10.10 (Yosemite) and newer versions][electron-supported-platforms].
|
||||
|
||||
You can however, run the [Etcher CLI][etcher-cli], which should work in older
|
||||
platforms.
|
||||
|
||||
[resin.io]: https://resin.io
|
||||
[balena.io]: https://balena.io
|
||||
[appimage]: http://appimage.org
|
||||
[xwayland]: https://wayland.freedesktop.org/xserver.html
|
||||
[weston.ini]: http://manpages.ubuntu.com/manpages/wily/man5/weston.ini.5.html
|
||||
[diskpart]: https://technet.microsoft.com/en-us/library/cc770877(v=ws.11).aspx
|
||||
[electron]: http://electron.atom.io
|
||||
[electron-supported-platforms]: https://github.com/electron/electron/blob/master/docs/tutorial/supported-platforms.md
|
||||
[etcher-cli]: https://github.com/resin-io/etcher/blob/master/docs/CLI.md
|
||||
[publishing]: https://github.com/resin-io/etcher/blob/master/docs/PUBLISHING.md
|
||||
[electron]: https://electronjs.org/
|
||||
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
|
||||
[publishing]: https://github.com/balena-io/etcher/blob/master/docs/PUBLISHING.md
|
||||
[windows-usb-tool]: https://www.microsoft.com/en-us/download/windows-usb-dvd-download-tool
|
||||
[rufus]: https://rufus.akeo.ie
|
||||
[unetbootin]: https://unetbootin.github.io
|
||||
[windows-iot-dashboard]: https://developer.microsoft.com/en-us/windows/iot/downloads
|
||||
[woeusb]: https://github.com/slacka/WoeUSB
|
||||
|
||||
See [PUBLISHING](/docs/PUBLISHING.md) for more details about release types.
|
@@ -1 +0,0 @@
|
||||
theme: jekyll-theme-minimal
|
@@ -1,17 +1,23 @@
|
||||
appId: io.resin.etcher
|
||||
copyright: Copyright 2016 Resinio Ltd
|
||||
productName: Etcher
|
||||
npmRebuild: true
|
||||
nodeGypRebuild: true
|
||||
publish: null
|
||||
# https://www.electron.build/configuration/configuration
|
||||
appId: io.balena.etcher
|
||||
copyright: Copyright 2016-2023 Balena Ltd
|
||||
productName: balenaEtcher
|
||||
afterPack: ./afterPack.js
|
||||
afterSign: ./afterSignHook.js
|
||||
asar: false
|
||||
files:
|
||||
- lib
|
||||
- build/**/*.node
|
||||
- assets/icon.png
|
||||
- node_modules/**/*
|
||||
- generated
|
||||
- lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js
|
||||
- lib/shared/catalina-sudo/sudo-askpass.osascript-en.js
|
||||
mac:
|
||||
icon: assets/icon.icns
|
||||
category: public.app-category.developer-tools
|
||||
hardenedRuntime: true
|
||||
entitlements: "entitlements.mac.plist"
|
||||
entitlementsInherit: "entitlements.mac.plist"
|
||||
artifactName: "${productName}-${version}.${ext}"
|
||||
target:
|
||||
- dmg
|
||||
dmg:
|
||||
background: assets/dmg/background.tiff
|
||||
icon: assets/icon.icns
|
||||
@@ -28,6 +34,10 @@ dmg:
|
||||
height: 405
|
||||
win:
|
||||
icon: assets/icon.ico
|
||||
target:
|
||||
- zip
|
||||
- nsis
|
||||
- portable
|
||||
nsis:
|
||||
oneClick: true
|
||||
runAfterFinish: true
|
||||
@@ -35,22 +45,28 @@ nsis:
|
||||
uninstallerIcon: assets/icon.ico
|
||||
deleteAppDataOnUninstall: true
|
||||
license: LICENSE
|
||||
artifactName: "${productName}-Setup-${version}-${env.TARGET_ARCH}.${ext}"
|
||||
artifactName: "${productName}-Setup-${version}.${ext}"
|
||||
portable:
|
||||
artifactName: "${productName}-Portable-${version}-${env.TARGET_ARCH}.${ext}"
|
||||
artifactName: "${productName}-Portable-${version}.${ext}"
|
||||
requestExecutionLevel: user
|
||||
linux:
|
||||
icon: assets/iconset
|
||||
target:
|
||||
- AppImage
|
||||
- rpm
|
||||
- deb
|
||||
category: Utility
|
||||
packageCategory: utils
|
||||
executableName: etcher-electron
|
||||
synopsis: Etcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.
|
||||
icon: assets/iconset
|
||||
executableName: balena-etcher
|
||||
synopsis: balenaEtcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.
|
||||
appImage:
|
||||
artifactName: ${productName}-${version}-${env.ELECTRON_BUILDER_ARCHITECTURE}.${ext}
|
||||
deb:
|
||||
priority: optional
|
||||
compression: bzip2
|
||||
depends:
|
||||
- gconf2
|
||||
- gconf-service
|
||||
- libappindicator1
|
||||
- gconf2
|
||||
- libasound2
|
||||
- libatk1.0-0
|
||||
- libc6
|
||||
@@ -60,16 +76,17 @@ deb:
|
||||
- libexpat1
|
||||
- libfontconfig1
|
||||
- libfreetype6
|
||||
- libgbm1
|
||||
- libgcc1
|
||||
- libgconf-2-4
|
||||
- libgdk-pixbuf2.0-0
|
||||
- libglib2.0-0
|
||||
- libgtk2.0-0
|
||||
- libgtk-3-0
|
||||
- liblzma5
|
||||
- libnotify4
|
||||
- libnspr4
|
||||
- libnss3
|
||||
- libpango1.0-0
|
||||
- libpango1.0-0 | libpango-1.0-0
|
||||
- libstdc++6
|
||||
- libx11-6
|
||||
- libxcomposite1
|
||||
@@ -83,7 +100,11 @@ deb:
|
||||
- libxss1
|
||||
- libxtst6
|
||||
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
|
||||
afterInstall: "./after-install.tpl"
|
||||
rpm:
|
||||
depends:
|
||||
- lsb
|
||||
- libXScrnSaver
|
||||
- util-linux
|
||||
protocols:
|
||||
name: etcher
|
||||
schemes:
|
||||
- etcher
|
||||
|
24
entitlements.mac.plist
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.usb</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.get-task-allow</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
@@ -1,339 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
@@ -1,4 +0,0 @@
|
||||
gpu_mem=16
|
||||
dtoverlay=dwc2,dr_mode=peripheral
|
||||
dtparam=act_led_trigger=none
|
||||
dtparam=act_led_activelow=off
|
@@ -1,30 +0,0 @@
|
||||
Copyright (c) 2006, Broadcom Corporation.
|
||||
Copyright (c) 2015, Raspberry Pi (Trading) Ltd
|
||||
All rights reserved.
|
||||
|
||||
Redistribution. Redistribution and use in binary form, without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* This software may only be used for the purposes of developing for,
|
||||
running or using a Raspberry Pi device.
|
||||
* Redistributions must reproduce the above copyright notice and the
|
||||
following disclaimer in the documentation and/or other materials
|
||||
provided with the distribution.
|
||||
* Neither the name of Broadcom Corporation nor the names of its suppliers
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
DISCLAIMER. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
|
||||
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
||||
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
||||
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
|
@@ -1,21 +0,0 @@
|
||||
Etcher CLI
|
||||
==========
|
||||
|
||||
The Etcher CLI is a command line interface to the Etcher writer backend, and
|
||||
currently the only module in the "Etcher" umbrella that makes use of this
|
||||
backend directly.
|
||||
|
||||
This module also has the task of unmounting the drives before and after
|
||||
flashing.
|
||||
|
||||
Notice the Etcher CLI is not worried about elevation, and assumes it has enough
|
||||
permissions to continue, throwing an error otherwise.
|
||||
|
||||
Exit codes
|
||||
----------
|
||||
|
||||
The Etcher CLI uses certain exit codes to signal the result of the operation.
|
||||
These are documented in [`lib/shared/exit-codes.js`][exit-codes] and are also
|
||||
printed on the Etcher CLI help page.
|
||||
|
||||
[exit-codes]: https://github.com/resin-io/etcher/blob/master/lib/shared/exit-codes.js
|
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 resin.io
|
||||
*
|
||||
* 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 strict'
|
||||
|
||||
const os = require('os')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const crypto = require('crypto')
|
||||
const childProcess = require('child_process')
|
||||
const debug = require('debug')('etcher:cli:diskpart')
|
||||
const Promise = require('bluebird')
|
||||
const retry = require('bluebird-retry')
|
||||
|
||||
const TMP_RANDOM_BYTES = 6
|
||||
const DISKPART_DELAY = 2000
|
||||
const DISKPART_RETRIES = 5
|
||||
|
||||
/**
|
||||
* @summary Generate a tmp filename with full path of OS' tmp dir
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {String} extension - temporary file extension
|
||||
* @returns {String} filename
|
||||
*
|
||||
* @example
|
||||
* const filename = tmpFilename('.sh');
|
||||
*/
|
||||
const tmpFilename = (extension) => {
|
||||
const random = crypto.randomBytes(TMP_RANDOM_BYTES).toString('hex')
|
||||
const filename = `etcher-diskpart-${random}${extension}`
|
||||
return path.join(os.tmpdir(), filename)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Run a diskpart script
|
||||
* @param {Array<String>} commands - list of commands to run
|
||||
* @param {Function} callback - callback(error)
|
||||
* @example
|
||||
* runDiskpart(['rescan'], (error) => {
|
||||
* ...
|
||||
* })
|
||||
*/
|
||||
const runDiskpart = (commands, callback) => {
|
||||
if (os.platform() !== 'win32') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
const filename = tmpFilename('')
|
||||
const script = commands.join('\r\n')
|
||||
|
||||
fs.writeFile(filename, script, {
|
||||
mode: 0o755
|
||||
}, (writeError) => {
|
||||
debug('write %s:', filename, writeError || 'OK')
|
||||
|
||||
childProcess.exec(`diskpart /s ${filename}`, (execError, stdout, stderr) => {
|
||||
debug('stdout:', stdout)
|
||||
debug('stderr:', stderr)
|
||||
|
||||
fs.unlink(filename, (unlinkError) => {
|
||||
debug('unlink %s:', filename, unlinkError || 'OK')
|
||||
callback(execError)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
/**
|
||||
* @summary Clean a device's partition tables
|
||||
* @param {String} device - device path
|
||||
* @example
|
||||
* diskpart.clean('\\\\.\\PhysicalDrive2')
|
||||
* .then(...)
|
||||
* .catch(...)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
clean (device) {
|
||||
if (os.platform() !== 'win32') {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
debug('clean', device)
|
||||
|
||||
const pattern = /PHYSICALDRIVE(\d+)/i
|
||||
|
||||
if (pattern.test(device)) {
|
||||
const deviceId = device.match(pattern).pop()
|
||||
return retry(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
runDiskpart([ `select disk ${deviceId}`, 'clean', 'rescan' ], (error) => {
|
||||
return error ? reject(error) : resolve()
|
||||
})
|
||||
}).delay(DISKPART_DELAY)
|
||||
}, {
|
||||
/* eslint-disable camelcase */
|
||||
max_tries: DISKPART_RETRIES
|
||||
/* eslint-enable camelcase */
|
||||
}).catch((error) => {
|
||||
throw new Error(`Couldn't clean the drive, ${error.failure.message} (code ${error.failure.code})`)
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Invalid device: "${device}"`))
|
||||
}
|
||||
|
||||
}
|
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* 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 strict'
|
||||
|
||||
const path = require('path')
|
||||
const Bluebird = require('bluebird')
|
||||
const visuals = require('resin-cli-visuals')
|
||||
const form = require('resin-cli-form')
|
||||
const writer = require('./writer')
|
||||
const utils = require('./utils')
|
||||
const options = require('./options')
|
||||
const messages = require('../shared/messages')
|
||||
const EXIT_CODES = require('../shared/exit-codes')
|
||||
const errors = require('../shared/errors')
|
||||
const permissions = require('../shared/permissions')
|
||||
|
||||
const ARGV_IMAGE_PATH_INDEX = 0
|
||||
const imagePath = options._[ARGV_IMAGE_PATH_INDEX]
|
||||
|
||||
permissions.isElevated().then((elevated) => {
|
||||
if (!elevated) {
|
||||
throw errors.createUserError({
|
||||
title: messages.error.elevationRequired(),
|
||||
description: 'This tool requires special permissions to write to external drives'
|
||||
})
|
||||
}
|
||||
|
||||
return form.run([
|
||||
{
|
||||
message: 'Select drive',
|
||||
type: 'drive',
|
||||
name: 'drive'
|
||||
},
|
||||
{
|
||||
message: 'This will erase the selected drive. Are you sure?',
|
||||
type: 'confirm',
|
||||
name: 'yes',
|
||||
default: false
|
||||
}
|
||||
], {
|
||||
override: {
|
||||
drive: options.drive,
|
||||
|
||||
// If `options.yes` is `false`, pass `null`,
|
||||
// otherwise the question will not be asked because
|
||||
// `false` is a defined value.
|
||||
yes: options.yes || null
|
||||
|
||||
}
|
||||
})
|
||||
}).then((answers) => {
|
||||
if (!answers.yes) {
|
||||
throw errors.createUserError({
|
||||
title: 'Aborted',
|
||||
description: 'We can\'t proceed without confirmation'
|
||||
})
|
||||
}
|
||||
|
||||
const progressBars = {
|
||||
write: new visuals.Progress('Flashing'),
|
||||
check: new visuals.Progress('Validating')
|
||||
}
|
||||
|
||||
return writer.writeImage(imagePath, answers.drive, {
|
||||
unmountOnSuccess: options.unmount,
|
||||
validateWriteOnSuccess: options.check
|
||||
}, (state) => {
|
||||
progressBars[state.type].update(state)
|
||||
}).then((results) => {
|
||||
return {
|
||||
imagePath,
|
||||
flash: results
|
||||
}
|
||||
})
|
||||
}).then((results) => {
|
||||
return Bluebird.try(() => {
|
||||
console.log(messages.info.flashComplete({
|
||||
drive: results.flash.drive,
|
||||
imageBasename: path.basename(results.imagePath)
|
||||
}))
|
||||
|
||||
if (results.flash.checksum.crc32) {
|
||||
console.log(`Checksum: ${results.flash.checksum.crc32}`)
|
||||
}
|
||||
|
||||
return Bluebird.resolve()
|
||||
}).then(() => {
|
||||
process.exit(EXIT_CODES.SUCCESS)
|
||||
})
|
||||
}).catch((error) => {
|
||||
return Bluebird.try(() => {
|
||||
utils.printError(error)
|
||||
return Bluebird.resolve()
|
||||
}).then(() => {
|
||||
if (error.code === 'EVALIDATION') {
|
||||
process.exit(EXIT_CODES.VALIDATION_ERROR)
|
||||
}
|
||||
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
})
|
||||
})
|
@@ -1,166 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* 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 strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
const fs = require('fs')
|
||||
const yargs = require('yargs')
|
||||
const utils = require('./utils')
|
||||
const EXIT_CODES = require('../shared/exit-codes')
|
||||
const errors = require('../shared/errors')
|
||||
const packageJSON = require('../../package.json')
|
||||
|
||||
/**
|
||||
* @summary The minimum required number of CLI arguments
|
||||
* @constant
|
||||
* @private
|
||||
* @type {Number}
|
||||
*/
|
||||
const MINIMUM_NUMBER_OF_ARGUMENTS = 1
|
||||
|
||||
/**
|
||||
* @summary The index of the image argument
|
||||
* @constant
|
||||
* @private
|
||||
* @type {Number}
|
||||
*/
|
||||
const IMAGE_PATH_ARGV_INDEX = 0
|
||||
|
||||
/**
|
||||
* @summary The first index that represents an actual option argument
|
||||
* @constant
|
||||
* @private
|
||||
* @type {Number}
|
||||
*
|
||||
* @description
|
||||
* The first arguments are usually the program executable itself, etc.
|
||||
*/
|
||||
const OPTIONS_INDEX_START = 2
|
||||
|
||||
/**
|
||||
* @summary Parsed CLI options and arguments
|
||||
* @type {Object}
|
||||
* @public
|
||||
*/
|
||||
module.exports = yargs
|
||||
|
||||
// Don't wrap at all
|
||||
.wrap(null)
|
||||
|
||||
.demand(MINIMUM_NUMBER_OF_ARGUMENTS, 'Missing image')
|
||||
|
||||
// Usage help
|
||||
.usage('Usage: $0 [options] <image>')
|
||||
.epilogue([
|
||||
'Exit codes:',
|
||||
_.map(EXIT_CODES, (value, key) => {
|
||||
const reason = _.map(_.split(key, '_'), _.capitalize).join(' ')
|
||||
return ` ${value} - ${reason}`
|
||||
}).join('\n'),
|
||||
'',
|
||||
'If you need help, don\'t hesitate in contacting us at:',
|
||||
'',
|
||||
' GitHub: https://github.com/resin-io/etcher/issues/new',
|
||||
' Gitter: https://gitter.im/resin-io/etcher'
|
||||
].join('\n'))
|
||||
|
||||
// Examples
|
||||
.example('$0 raspberry-pi.img')
|
||||
.example('$0 --no-check raspberry-pi.img')
|
||||
.example('$0 -d /dev/disk2 ubuntu.iso')
|
||||
.example('$0 -d /dev/disk2 -y rpi.img')
|
||||
|
||||
// Help option
|
||||
.help()
|
||||
|
||||
// Version option
|
||||
.version(_.constant(packageJSON.version))
|
||||
|
||||
// Error reporting
|
||||
.fail((message, error) => {
|
||||
const errorObject = error || errors.createUserError({
|
||||
title: message
|
||||
})
|
||||
|
||||
yargs.showHelp()
|
||||
utils.printError(errorObject)
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
})
|
||||
|
||||
// Assert that image exists
|
||||
.check((argv) => {
|
||||
const imagePath = argv._[IMAGE_PATH_ARGV_INDEX]
|
||||
|
||||
try {
|
||||
fs.accessSync(imagePath)
|
||||
} catch (error) {
|
||||
throw errors.createUserError({
|
||||
title: 'Unable to access file',
|
||||
description: `The image ${imagePath} is not accessible`
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Assert that if the `yes` flag is provided, the `drive` flag is also provided.
|
||||
.check((argv) => {
|
||||
if (argv.yes && !argv.drive) {
|
||||
throw errors.createUserError({
|
||||
title: 'Missing drive',
|
||||
description: 'You need to explicitly pass a drive when disabling interactively'
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
.options({
|
||||
help: {
|
||||
describe: 'show help',
|
||||
boolean: true,
|
||||
alias: 'h'
|
||||
},
|
||||
version: {
|
||||
describe: 'show version number',
|
||||
boolean: true,
|
||||
alias: 'v'
|
||||
},
|
||||
drive: {
|
||||
describe: 'drive',
|
||||
string: true,
|
||||
alias: 'd'
|
||||
},
|
||||
check: {
|
||||
describe: 'validate write',
|
||||
boolean: true,
|
||||
alias: 'c',
|
||||
default: true
|
||||
},
|
||||
yes: {
|
||||
describe: 'confirm non-interactively',
|
||||
boolean: true,
|
||||
alias: 'y'
|
||||
},
|
||||
unmount: {
|
||||
describe: 'unmount on success',
|
||||
boolean: true,
|
||||
alias: 'u',
|
||||
default: true
|
||||
}
|
||||
})
|
||||
.parse(process.argv.slice(OPTIONS_INDEX_START))
|
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* 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 strict'
|
||||
|
||||
const chalk = require('chalk')
|
||||
const errors = require('../shared/errors')
|
||||
|
||||
/**
|
||||
* @summary Print an error to stderr
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Error} error - error
|
||||
*
|
||||
* @example
|
||||
* utils.printError(new Error('Oops!'));
|
||||
*/
|
||||
exports.printError = (error) => {
|
||||
const title = errors.getTitle(error)
|
||||
const description = errors.getDescription(error, {
|
||||
userFriendlyDescriptionsOnly: true
|
||||
})
|
||||
|
||||
console.error(chalk.red(title))
|
||||
|
||||
if (description) {
|
||||
console.error(`\n${chalk.red(description)}`)
|
||||
}
|
||||
|
||||
if (process.env.ETCHER_CLI_DEBUG && error.stack) {
|
||||
console.error(`\n${chalk.red(error.stack)}`)
|
||||
}
|
||||
}
|
@@ -1,147 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* 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 strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
const Bluebird = require('bluebird')
|
||||
const fs = Bluebird.promisifyAll(require('fs'))
|
||||
const mountutils = Bluebird.promisifyAll(require('mountutils'))
|
||||
const drivelist = Bluebird.promisifyAll(require('drivelist'))
|
||||
const os = require('os')
|
||||
const imageStream = require('../image-stream')
|
||||
const errors = require('../shared/errors')
|
||||
const constraints = require('../shared/drive-constraints')
|
||||
const ImageWriter = require('../writer')
|
||||
const diskpart = require('./diskpart')
|
||||
|
||||
/**
|
||||
* @summary Timeout, in milliseconds, to wait before unmounting on success
|
||||
* @constant
|
||||
* @type {Number}
|
||||
*/
|
||||
const UNMOUNT_ON_SUCCESS_TIMEOUT_MS = 2000
|
||||
|
||||
/**
|
||||
* @summary Write an image to a disk drive
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} imagePath - path to image
|
||||
* @param {String} drive - drive
|
||||
* @param {Object} options - options
|
||||
* @param {Boolean} [options.unmountOnSuccess=false] - unmount on success
|
||||
* @param {Boolean} [options.validateWriteOnSuccess=false] - validate write on success
|
||||
* @param {Function} onProgress - on progress callback (state)
|
||||
*
|
||||
* @fulfil {Boolean} - whether the operation was successful
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* writer.writeImage('path/to/image.img', '/dev/disk2', {
|
||||
* unmountOnSuccess: true,
|
||||
* validateWriteOnSuccess: true
|
||||
* }, (state) => {
|
||||
* console.log(state.percentage);
|
||||
* }).then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.writeImage = (imagePath, drive, options, onProgress) => {
|
||||
return drivelist.listAsync().then((drives) => {
|
||||
const selectedDrive = _.find(drives, {
|
||||
device: drive
|
||||
})
|
||||
|
||||
if (!selectedDrive) {
|
||||
throw errors.createUserError({
|
||||
title: 'The selected drive was not found',
|
||||
description: `We can't find ${drive} in your system. Did you unplug the drive?`,
|
||||
code: 'EUNPLUGGED'
|
||||
})
|
||||
}
|
||||
|
||||
return selectedDrive
|
||||
}).then((driveObject) => {
|
||||
return Bluebird.try(() => {
|
||||
// Unmounting a drive in Windows means we can't write to it anymore
|
||||
if (os.platform() === 'win32') {
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
return mountutils.unmountDiskAsync(driveObject.device)
|
||||
}).then(() => {
|
||||
return diskpart.clean(driveObject.device)
|
||||
}).then(() => {
|
||||
/* eslint-disable no-bitwise */
|
||||
const flags = fs.constants.O_RDWR |
|
||||
fs.constants.O_EXCL |
|
||||
fs.constants.O_NONBLOCK |
|
||||
fs.constants.O_SYNC |
|
||||
fs.constants.O_DIRECT
|
||||
/* eslint-enable no-bitwise */
|
||||
|
||||
return fs.openAsync(driveObject.raw, flags)
|
||||
}).then((driveFileDescriptor) => {
|
||||
return imageStream.getFromFilePath(imagePath).then((image) => {
|
||||
if (!constraints.isDriveLargeEnough(driveObject, image)) {
|
||||
throw errors.createUserError({
|
||||
title: 'The image you selected is too big for this drive',
|
||||
description: 'Please connect a bigger drive and try again'
|
||||
})
|
||||
}
|
||||
|
||||
const writer = new ImageWriter({
|
||||
image,
|
||||
fd: driveFileDescriptor,
|
||||
path: driveObject.raw,
|
||||
verify: options.validateWriteOnSuccess,
|
||||
checksumAlgorithms: [ 'crc32' ]
|
||||
})
|
||||
|
||||
return writer.write()
|
||||
}).then((writer) => {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
writer.on('progress', onProgress)
|
||||
writer.on('error', reject)
|
||||
writer.on('finish', (results) => {
|
||||
results.drive = driveObject
|
||||
resolve(results)
|
||||
})
|
||||
})
|
||||
}).tap(() => {
|
||||
// Make sure the device stream file descriptor is closed
|
||||
// before returning control the the caller. Not closing
|
||||
// the file descriptor (and waiting for it) results in
|
||||
// `EBUSY` errors when attempting to unmount the drive
|
||||
// right afterwards in some Windows 7 systems.
|
||||
return fs.closeAsync(driveFileDescriptor).then(() => {
|
||||
if (!options.unmountOnSuccess) {
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
// Closing a file descriptor on a drive containing mountable
|
||||
// partitions causes macOS to mount the drive. If we try to
|
||||
// unmount to quickly, then the drive might get re-mounted
|
||||
// right afterwards.
|
||||
return Bluebird.delay(UNMOUNT_ON_SUCCESS_TIMEOUT_MS)
|
||||
.return(driveObject.device)
|
||||
.then(mountutils.unmountDiskAsync)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
362
lib/gui/app.js
@@ -1,362 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* 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 Etcher
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/* eslint-disable no-var */
|
||||
|
||||
var angular = require('angular')
|
||||
|
||||
/* eslint-enable no-var */
|
||||
|
||||
const electron = require('electron')
|
||||
const Bluebird = require('bluebird')
|
||||
const semver = require('semver')
|
||||
const EXIT_CODES = require('../shared/exit-codes')
|
||||
const messages = require('../shared/messages')
|
||||
const s3Packages = require('../shared/s3-packages')
|
||||
const release = require('../shared/release')
|
||||
const store = require('../shared/store')
|
||||
const errors = require('../shared/errors')
|
||||
const packageJSON = require('../../package.json')
|
||||
const flashState = require('../shared/models/flash-state')
|
||||
const settings = require('./models/settings')
|
||||
const windowProgress = require('./os/window-progress')
|
||||
const analytics = require('./modules/analytics')
|
||||
const updateNotifier = require('./components/update-notifier')
|
||||
const availableDrives = require('../shared/models/available-drives')
|
||||
const selectionState = require('../shared/models/selection-state')
|
||||
const driveScanner = require('./modules/drive-scanner')
|
||||
const osDialog = require('./os/dialog')
|
||||
const exceptionReporter = require('./modules/exception-reporter')
|
||||
|
||||
// Enable debug information from all modules that use `debug`
|
||||
// See https://github.com/visionmedia/debug#browser-support
|
||||
//
|
||||
// Enable drivelist debugging information
|
||||
// See https://github.com/resin-io-modules/drivelist
|
||||
electron.remote.process.env.DRIVELIST_DEBUG = /drivelist|^\*$/i.test(electron.remote.process.env.DEBUG) ? '1' : ''
|
||||
window.localStorage.debug = electron.remote.process.env.DEBUG
|
||||
|
||||
const app = angular.module('Etcher', [
|
||||
require('angular-ui-router'),
|
||||
require('angular-ui-bootstrap'),
|
||||
require('angular-if-state'),
|
||||
|
||||
// Components
|
||||
require('./components/svg-icon'),
|
||||
require('./components/warning-modal/warning-modal'),
|
||||
require('./components/safe-webview'),
|
||||
|
||||
// Pages
|
||||
require('./pages/main/main'),
|
||||
require('./pages/finish/finish'),
|
||||
require('./pages/settings/settings'),
|
||||
|
||||
// OS
|
||||
require('./os/open-external/open-external'),
|
||||
require('./os/dropzone/dropzone'),
|
||||
|
||||
// Utils
|
||||
require('./utils/manifest-bind/manifest-bind')
|
||||
])
|
||||
|
||||
app.run(() => {
|
||||
console.log([
|
||||
' _____ _ _',
|
||||
'| ___| | | |',
|
||||
'| |__ | |_ ___| |__ ___ _ __',
|
||||
'| __|| __/ __| \'_ \\ / _ \\ \'__|',
|
||||
'| |___| || (__| | | | __/ |',
|
||||
'\\____/ \\__\\___|_| |_|\\___|_|',
|
||||
'',
|
||||
'Interested in joining the Etcher team?',
|
||||
'Drop us a line at join+etcher@resin.io',
|
||||
'',
|
||||
`Version = ${packageJSON.version}, Type = ${packageJSON.packageType}`
|
||||
].join('\n'))
|
||||
})
|
||||
|
||||
app.run(() => {
|
||||
const currentVersion = packageJSON.version
|
||||
|
||||
analytics.logEvent('Application start', {
|
||||
packageType: packageJSON.packageType,
|
||||
version: currentVersion
|
||||
})
|
||||
|
||||
const shouldCheckForUpdates = updateNotifier.shouldCheckForUpdates({
|
||||
currentVersion,
|
||||
lastSleptUpdateNotifier: settings.get('lastSleptUpdateNotifier'),
|
||||
lastSleptUpdateNotifierVersion: settings.get('lastSleptUpdateNotifierVersion')
|
||||
})
|
||||
|
||||
const isStableRelease = release.isStableRelease(currentVersion)
|
||||
const updatesEnabled = settings.get('updatesEnabled')
|
||||
|
||||
if (!shouldCheckForUpdates || !updatesEnabled) {
|
||||
analytics.logEvent('Not checking for updates', {
|
||||
shouldCheckForUpdates,
|
||||
updatesEnabled,
|
||||
stable: isStableRelease
|
||||
})
|
||||
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
const updateSemverRange = packageJSON.updates.semverRange
|
||||
const includeUnstableChannel = settings.get('includeUnstableUpdateChannel')
|
||||
|
||||
analytics.logEvent('Checking for updates', {
|
||||
currentVersion,
|
||||
stable: isStableRelease,
|
||||
updateSemverRange,
|
||||
includeUnstableChannel
|
||||
})
|
||||
|
||||
return s3Packages.getLatestVersion(release.getReleaseType(currentVersion), {
|
||||
range: updateSemverRange,
|
||||
includeUnstableChannel
|
||||
}).then((latestVersion) => {
|
||||
if (semver.gte(currentVersion, latestVersion || '0.0.0')) {
|
||||
analytics.logEvent('Update notification skipped', {
|
||||
reason: 'Latest version'
|
||||
})
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
// In case the internet connection is not good and checking the
|
||||
// latest published version takes too long, only show notify
|
||||
// the user about the new version if he didn't start the flash
|
||||
// process (e.g: selected an image), otherwise such interruption
|
||||
// might be annoying.
|
||||
if (selectionState.hasImage()) {
|
||||
analytics.logEvent('Update notification skipped', {
|
||||
reason: 'Image selected'
|
||||
})
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
analytics.logEvent('Notifying update', {
|
||||
latestVersion
|
||||
})
|
||||
|
||||
return updateNotifier.notify(latestVersion, {
|
||||
allowSleepUpdateCheck: isStableRelease
|
||||
})
|
||||
|
||||
// If the error is an update user error, then we don't want
|
||||
// to bother users each time they open the app.
|
||||
// See: https://github.com/resin-io/etcher/issues/1525
|
||||
}).catch((error) => {
|
||||
return errors.isUserError(error) && error.code === 'UPDATE_USER_ERROR'
|
||||
}, (error) => {
|
||||
analytics.logEvent('Update check user error', {
|
||||
title: errors.getTitle(error),
|
||||
description: errors.getDescription(error)
|
||||
})
|
||||
}).catch(exceptionReporter.report)
|
||||
})
|
||||
|
||||
app.run(() => {
|
||||
store.subscribe(() => {
|
||||
if (!flashState.isFlashing()) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentFlashState = flashState.getFlashState()
|
||||
|
||||
// NOTE: There is usually a short time period between the `isFlashing()`
|
||||
// property being set, and the flashing actually starting, which
|
||||
// might cause some non-sense flashing state logs including
|
||||
// `undefined` values.
|
||||
analytics.logDebug(
|
||||
`Progress (${currentFlashState.type}): ` +
|
||||
`${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` +
|
||||
`(eta ${currentFlashState.eta}s)`
|
||||
)
|
||||
|
||||
windowProgress.set(currentFlashState)
|
||||
})
|
||||
})
|
||||
|
||||
app.run(($timeout) => {
|
||||
driveScanner.on('devices', (drives) => {
|
||||
// Safely trigger a digest cycle.
|
||||
// In some cases, AngularJS doesn't acknowledge that the
|
||||
// available drives list has changed, and incorrectly
|
||||
// keeps asking the user to "Connect a drive".
|
||||
$timeout(() => {
|
||||
availableDrives.setDrives(drives)
|
||||
})
|
||||
})
|
||||
|
||||
driveScanner.on('error', (error) => {
|
||||
// Stop the drive scanning loop in case of errors,
|
||||
// otherwise we risk presenting the same error over
|
||||
// and over again to the user, while also heavily
|
||||
// spamming our error reporting service.
|
||||
driveScanner.stop()
|
||||
|
||||
return exceptionReporter.report(error)
|
||||
})
|
||||
|
||||
driveScanner.start()
|
||||
})
|
||||
|
||||
app.run(($window) => {
|
||||
let popupExists = false
|
||||
|
||||
$window.addEventListener('beforeunload', (event) => {
|
||||
if (!flashState.isFlashing() || popupExists) {
|
||||
analytics.logEvent('Close application', {
|
||||
isFlashing: flashState.isFlashing()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Don't close window while flashing
|
||||
event.returnValue = false
|
||||
|
||||
// Don't open any more popups
|
||||
popupExists = true
|
||||
|
||||
analytics.logEvent('Close attempt while flashing')
|
||||
|
||||
osDialog.showWarning({
|
||||
confirmationLabel: 'Yes, quit',
|
||||
rejectionLabel: 'Cancel',
|
||||
title: 'Are you sure you want to close Etcher?',
|
||||
description: messages.warning.exitWhileFlashing()
|
||||
}).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
analytics.logEvent('Close confirmed while flashing', {
|
||||
uuid: flashState.getFlashUuid()
|
||||
})
|
||||
|
||||
// This circumvents the 'beforeunload' event unlike
|
||||
// electron.remote.app.quit() which does not.
|
||||
electron.remote.process.exit(EXIT_CODES.SUCCESS)
|
||||
}
|
||||
|
||||
analytics.logEvent('Close rejected while flashing')
|
||||
popupExists = false
|
||||
}).catch(exceptionReporter.report)
|
||||
})
|
||||
})
|
||||
|
||||
app.run(($rootScope) => {
|
||||
$rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
|
||||
// Ignore first navigation
|
||||
if (!fromState.name) {
|
||||
return
|
||||
}
|
||||
|
||||
analytics.logEvent('Navigate', {
|
||||
to: toState.name,
|
||||
from: fromState.name
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
app.config(($urlRouterProvider) => {
|
||||
$urlRouterProvider.otherwise('/main')
|
||||
})
|
||||
|
||||
app.config(($provide) => {
|
||||
$provide.decorator('$exceptionHandler', ($delegate) => {
|
||||
return (exception, cause) => {
|
||||
exceptionReporter.report(exception)
|
||||
$delegate(exception, cause)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
app.controller('HeaderController', function (OSOpenExternalService) {
|
||||
/**
|
||||
* @summary Open help page
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* This application will open either the image's support url, declared
|
||||
* in the archive `manifest.json`, or the default Etcher help page.
|
||||
*
|
||||
* @example
|
||||
* HeaderController.openHelpPage();
|
||||
*/
|
||||
this.openHelpPage = () => {
|
||||
const DEFAULT_SUPPORT_URL = 'https://github.com/resin-io/etcher/blob/master/SUPPORT.md'
|
||||
const supportUrl = selectionState.getImageSupportUrl() || DEFAULT_SUPPORT_URL
|
||||
OSOpenExternalService.open(supportUrl)
|
||||
}
|
||||
})
|
||||
|
||||
app.controller('StateController', function ($rootScope, $scope) {
|
||||
const unregisterStateChange = $rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
|
||||
this.previousName = fromState.name
|
||||
this.currentName = toState.name
|
||||
})
|
||||
|
||||
$scope.$on('$destroy', unregisterStateChange)
|
||||
|
||||
/**
|
||||
* @summary Get the previous state name
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} previous state name
|
||||
*
|
||||
* @example
|
||||
* if (StateController.previousName === 'main') {
|
||||
* console.log('We left the main screen!');
|
||||
* }
|
||||
*/
|
||||
this.previousName = null
|
||||
|
||||
/**
|
||||
* @summary Get the current state name
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} current state name
|
||||
*
|
||||
* @example
|
||||
* if (StateController.currentName === 'main') {
|
||||
* console.log('We are on the main screen!');
|
||||
* }
|
||||
*/
|
||||
this.currentName = null
|
||||
})
|
||||
|
||||
// Handle keyboard shortcut to open the settings
|
||||
app.run(($state) => {
|
||||
electron.ipcRenderer.on('menu:preferences', () => {
|
||||
$state.go('settings')
|
||||
})
|
||||
})
|
||||
|
||||
// Ensure user settings are loaded before
|
||||
// we bootstrap the Angular.js application
|
||||
angular.element(document).ready(() => {
|
||||
settings.load().then(() => {
|
||||
angular.bootstrap(document, [ 'Etcher' ])
|
||||
}).catch(exceptionReporter.report)
|
||||
})
|
234
lib/gui/app/app.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as remote from '@electron/remote';
|
||||
import { debounce, capitalize, Dictionary, values } from 'lodash';
|
||||
import outdent from 'outdent';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import * as packageJSON from '../../../package.json';
|
||||
import { DrivelistDrive } from '../../shared/drive-constraints';
|
||||
import * as EXIT_CODES from '../../shared/exit-codes';
|
||||
import * as messages from '../../shared/messages';
|
||||
import * as availableDrives from './models/available-drives';
|
||||
import * as flashState from './models/flash-state';
|
||||
import * as settings from './models/settings';
|
||||
import { Actions, observe, store } from './models/store';
|
||||
import * as analytics from './modules/analytics';
|
||||
import { startApiAndSpawnChild } from './modules/api';
|
||||
import * as exceptionReporter from './modules/exception-reporter';
|
||||
import * as osDialog from './os/dialog';
|
||||
import * as windowProgress from './os/window-progress';
|
||||
import MainPage from './pages/main/MainPage';
|
||||
import './css/main.css';
|
||||
import * as i18next from 'i18next';
|
||||
import { promises } from 'dns';
|
||||
import { SourceMetadata } from '../../shared/typings/source-selector';
|
||||
|
||||
window.addEventListener(
|
||||
'unhandledrejection',
|
||||
(event: PromiseRejectionEvent | any) => {
|
||||
// Promise: event.reason
|
||||
// Anything else: event
|
||||
const error = event.reason || event;
|
||||
analytics.logException(error);
|
||||
event.preventDefault();
|
||||
},
|
||||
);
|
||||
|
||||
// Set application session UUID
|
||||
store.dispatch({
|
||||
type: Actions.SET_APPLICATION_SESSION_UUID,
|
||||
data: uuidV4(),
|
||||
});
|
||||
|
||||
// Set first flashing workflow UUID
|
||||
store.dispatch({
|
||||
type: Actions.SET_FLASHING_WORKFLOW_UUID,
|
||||
data: uuidV4(),
|
||||
});
|
||||
|
||||
const applicationSessionUuid = store.getState().toJS().applicationSessionUuid;
|
||||
const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid;
|
||||
|
||||
console.log(outdent`
|
||||
${outdent}
|
||||
_____ _ _
|
||||
| ___| | | |
|
||||
| |__ | |_ ___| |__ ___ _ __
|
||||
| __|| __/ __| '_ \\ / _ \\ '__|
|
||||
| |___| || (__| | | | __/ |
|
||||
\\____/ \\__\\___|_| |_|\\___|_|
|
||||
|
||||
Interested in joining the Etcher team?
|
||||
Drop us a line at join+etcher@balena.io
|
||||
|
||||
Version = ${packageJSON.version}, Type = ${packageJSON.packageType}
|
||||
`);
|
||||
|
||||
const currentVersion = packageJSON.version;
|
||||
|
||||
analytics.logEvent('Application start', {
|
||||
packageType: packageJSON.packageType,
|
||||
version: currentVersion,
|
||||
});
|
||||
|
||||
const debouncedLog = debounce(console.log, 1000, { maxWait: 1000 });
|
||||
|
||||
function pluralize(word: string, quantity: number) {
|
||||
return `${quantity} ${word}${quantity === 1 ? '' : 's'}`;
|
||||
}
|
||||
|
||||
observe(() => {
|
||||
if (!flashState.isFlashing()) {
|
||||
return;
|
||||
}
|
||||
const currentFlashState = flashState.getFlashState();
|
||||
windowProgress.set(currentFlashState);
|
||||
|
||||
let eta = '';
|
||||
if (currentFlashState.eta !== undefined) {
|
||||
eta = `eta in ${currentFlashState.eta.toFixed(0)}s`;
|
||||
}
|
||||
let active = '';
|
||||
if (currentFlashState.type !== 'decompressing') {
|
||||
active = pluralize('device', currentFlashState.active);
|
||||
}
|
||||
// NOTE: There is usually a short time period between the `isFlashing()`
|
||||
// property being set, and the flashing actually starting, which
|
||||
// might cause some non-sense flashing state logs including
|
||||
// `undefined` values.
|
||||
debouncedLog(outdent({ newline: ' ' })`
|
||||
${capitalize(currentFlashState.type)}
|
||||
${active},
|
||||
${currentFlashState.percentage}%
|
||||
at
|
||||
${(currentFlashState.speed || 0).toFixed(2)}
|
||||
MB/s
|
||||
(total ${(currentFlashState.speed * currentFlashState.active).toFixed(2)} MB/s)
|
||||
${eta}
|
||||
with
|
||||
${pluralize('failed device', currentFlashState.failed)}
|
||||
`);
|
||||
});
|
||||
|
||||
function setDrives(drives: Dictionary<DrivelistDrive>) {
|
||||
// prevent setting drives while flashing otherwise we might lose some while we unmount them
|
||||
if (!flashState.isFlashing()) {
|
||||
availableDrives.setDrives(values(drives));
|
||||
}
|
||||
}
|
||||
|
||||
// Spwaning the child process without privileges to get the drives list
|
||||
// TODO: clean up this mess of exports
|
||||
export let requestMetadata: any;
|
||||
|
||||
// start the api and spawn the child process
|
||||
startApiAndSpawnChild({
|
||||
withPrivileges: false,
|
||||
}).then(({ emit, registerHandler }) => {
|
||||
// start scanning
|
||||
emit('scan');
|
||||
|
||||
// make the sourceMetada awaitable to be used on source selection
|
||||
requestMetadata = async (params: any): Promise<SourceMetadata> => {
|
||||
emit('sourceMetadata', JSON.stringify(params));
|
||||
|
||||
return new Promise((resolve) =>
|
||||
registerHandler('sourceMetadata', (data: any) => {
|
||||
resolve(JSON.parse(data));
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
registerHandler('drives', (data: any) => {
|
||||
setDrives(JSON.parse(data));
|
||||
});
|
||||
});
|
||||
|
||||
let popupExists = false;
|
||||
|
||||
analytics.initAnalytics();
|
||||
|
||||
window.addEventListener('beforeunload', async (event) => {
|
||||
if (!flashState.isFlashing() || popupExists) {
|
||||
analytics.logEvent('Close application', {
|
||||
isFlashing: flashState.isFlashing(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't close window while flashing
|
||||
event.returnValue = false;
|
||||
|
||||
// Don't open any more popups
|
||||
popupExists = true;
|
||||
|
||||
analytics.logEvent('Close attempt while flashing');
|
||||
|
||||
try {
|
||||
const confirmed = await osDialog.showWarning({
|
||||
confirmationLabel: i18next.t('yesExit'),
|
||||
rejectionLabel: i18next.t('cancel'),
|
||||
title: i18next.t('reallyExit'),
|
||||
description: messages.warning.exitWhileFlashing(),
|
||||
});
|
||||
if (confirmed) {
|
||||
analytics.logEvent('Close confirmed while flashing', {
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
});
|
||||
|
||||
// This circumvents the 'beforeunload' event unlike
|
||||
// remote.app.quit() which does not.
|
||||
remote.process.exit(EXIT_CODES.SUCCESS);
|
||||
}
|
||||
|
||||
analytics.logEvent('Close rejected while flashing', {
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
});
|
||||
popupExists = false;
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
});
|
||||
|
||||
export async function main() {
|
||||
try {
|
||||
const { init: ledsInit } = require('./models/leds');
|
||||
await ledsInit();
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(MainPage),
|
||||
document.getElementById('main'),
|
||||
// callback to set the correct zoomFactor for webviews as well
|
||||
async () => {
|
||||
const fullscreen = await settings.get('fullscreen');
|
||||
const width = fullscreen ? window.screen.width : window.outerWidth;
|
||||
try {
|
||||
electron.webFrame.setZoomFactor(width / settings.DEFAULT_WIDTH);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
563
lib/gui/app/components/drive-selector/drive-selector.tsx
Normal file
@@ -0,0 +1,563 @@
|
||||
/*
|
||||
* Copyright 2019 balena.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||
import * as sourceDestination from 'etcher-sdk/build/source-destination/';
|
||||
import * as React from 'react';
|
||||
import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
isDriveValid,
|
||||
DriveStatus,
|
||||
DrivelistDrive,
|
||||
isDriveSizeLarge,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility, warning } from '../../../../shared/messages';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||
import { getImage, isDriveSelected } from '../../models/selection-state';
|
||||
import { store } from '../../models/store';
|
||||
import { logEvent, logException } from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import {
|
||||
Alert,
|
||||
GenericTableProps,
|
||||
Modal,
|
||||
Table,
|
||||
} from '../../styled-components';
|
||||
|
||||
import { SourceMetadata } from '../source-selector/source-selector';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
||||
progress: number;
|
||||
}
|
||||
|
||||
interface DriverlessDrive {
|
||||
displayName: string; // added in app.ts
|
||||
description: string;
|
||||
link: string;
|
||||
linkTitle: string;
|
||||
linkMessage: string;
|
||||
linkCTA: string;
|
||||
}
|
||||
|
||||
type Drive = DrivelistDrive | DriverlessDrive | UsbbootDrive;
|
||||
|
||||
function isUsbbootDrive(drive: Drive): drive is UsbbootDrive {
|
||||
return (drive as UsbbootDrive).progress !== undefined;
|
||||
}
|
||||
|
||||
function isDriverlessDrive(drive: Drive): drive is DriverlessDrive {
|
||||
return (drive as DriverlessDrive).link !== undefined;
|
||||
}
|
||||
|
||||
function isDrivelistDrive(drive: Drive): drive is DrivelistDrive {
|
||||
return typeof (drive as DrivelistDrive).size === 'number';
|
||||
}
|
||||
|
||||
const DrivesTable = styled((props: GenericTableProps<Drive>) => (
|
||||
<Table<Drive> {...props} />
|
||||
))`
|
||||
[data-display='table-head'],
|
||||
[data-display='table-body'] {
|
||||
> [data-display='table-row'] > [data-display='table-cell'] {
|
||||
&:nth-child(2) {
|
||||
width: 32%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
width: 32%;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function badgeShadeFromStatus(status: string) {
|
||||
switch (status) {
|
||||
case compatibility.containsImage():
|
||||
return 16;
|
||||
case compatibility.system():
|
||||
case compatibility.tooSmall():
|
||||
return 5;
|
||||
default:
|
||||
return 14;
|
||||
}
|
||||
}
|
||||
|
||||
const InitProgress = styled(
|
||||
({
|
||||
value,
|
||||
...props
|
||||
}: {
|
||||
value: number;
|
||||
props?: React.ProgressHTMLAttributes<Element>;
|
||||
}) => {
|
||||
return <progress max="100" value={value} {...props} />;
|
||||
},
|
||||
)`
|
||||
/* Reset the default appearance */
|
||||
appearance: none;
|
||||
|
||||
::-webkit-progress-bar {
|
||||
width: 130px;
|
||||
height: 4px;
|
||||
background-color: #dde1f0;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
::-webkit-progress-value {
|
||||
background-color: #1496e1;
|
||||
border-radius: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface DriveSelectorProps
|
||||
extends Omit<ModalProps, 'done' | 'cancel' | 'onSelect'> {
|
||||
write: boolean;
|
||||
multipleSelection: boolean;
|
||||
showWarnings?: boolean;
|
||||
cancel: (drives: DrivelistDrive[]) => void;
|
||||
done: (drives: DrivelistDrive[]) => void;
|
||||
titleLabel: string;
|
||||
emptyListLabel: string;
|
||||
emptyListIcon: JSX.Element;
|
||||
selectedList?: DrivelistDrive[];
|
||||
updateSelectedList?: () => DrivelistDrive[];
|
||||
onSelect?: (drive: DrivelistDrive) => void;
|
||||
}
|
||||
|
||||
interface DriveSelectorState {
|
||||
drives: Drive[];
|
||||
image?: SourceMetadata;
|
||||
missingDriversModal: { drive?: DriverlessDrive };
|
||||
selectedList: DrivelistDrive[];
|
||||
showSystemDrives: boolean;
|
||||
}
|
||||
|
||||
function isSystemDrive(drive: Drive) {
|
||||
return isDrivelistDrive(drive) && drive.isSystem;
|
||||
}
|
||||
|
||||
export class DriveSelector extends React.Component<
|
||||
DriveSelectorProps,
|
||||
DriveSelectorState
|
||||
> {
|
||||
private unsubscribe: (() => void) | undefined;
|
||||
tableColumns: Array<TableColumn<Drive>>;
|
||||
originalList: DrivelistDrive[];
|
||||
|
||||
constructor(props: DriveSelectorProps) {
|
||||
super(props);
|
||||
|
||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||
const selectedList = this.props.selectedList || [];
|
||||
this.originalList = [...(this.props.selectedList || [])];
|
||||
|
||||
this.state = {
|
||||
drives: getDrives(),
|
||||
image: getImage(),
|
||||
missingDriversModal: defaultMissingDriversModalState,
|
||||
selectedList,
|
||||
showSystemDrives: false,
|
||||
};
|
||||
|
||||
this.tableColumns = [
|
||||
{
|
||||
field: 'description',
|
||||
label: i18next.t('drives.name'),
|
||||
render: (description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive)) {
|
||||
const isLargeDrive = isDriveSizeLarge(drive);
|
||||
const hasWarnings =
|
||||
this.props.showWarnings && (isLargeDrive || drive.isSystem);
|
||||
return (
|
||||
<Flex alignItems="center">
|
||||
{hasWarnings && (
|
||||
<ExclamationTriangleSvg
|
||||
height="1em"
|
||||
fill={drive.isSystem ? '#fca321' : '#8f9297'}
|
||||
/>
|
||||
)}
|
||||
<Txt ml={(hasWarnings && 8) || 0}>
|
||||
{middleEllipsis(description, 32)}
|
||||
</Txt>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
return <Txt>{description}</Txt>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'size',
|
||||
label: i18next.t('drives.size'),
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive) && drive.size !== null) {
|
||||
return prettyBytes(drive.size);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'link',
|
||||
label: i18next.t('drives.location'),
|
||||
render: (_description: string, drive: Drive) => {
|
||||
return (
|
||||
<Txt>
|
||||
{drive.displayName}
|
||||
{isDriverlessDrive(drive) && (
|
||||
<>
|
||||
{' '}
|
||||
-{' '}
|
||||
<b>
|
||||
<a onClick={() => this.installMissingDrivers(drive)}>
|
||||
{drive.linkCTA}
|
||||
</a>
|
||||
</b>
|
||||
</>
|
||||
)}
|
||||
</Txt>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'extra',
|
||||
// We use an empty React fragment otherwise it uses the field name as label
|
||||
label: <></>,
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isUsbbootDrive(drive)) {
|
||||
return this.renderProgress(drive.progress);
|
||||
} else if (isDrivelistDrive(drive)) {
|
||||
return this.renderStatuses(drive);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private driveShouldBeDisabled(drive: Drive, image?: SourceMetadata) {
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
!isDriveValid(drive, image, this.props.write) ||
|
||||
(this.props.write && drive.isReadOnly)
|
||||
);
|
||||
}
|
||||
|
||||
private getDisplayedDrives(drives: Drive[]): Drive[] {
|
||||
return drives.filter((drive) => {
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
isDriveSelected(drive.device) ||
|
||||
this.state.showSystemDrives ||
|
||||
!drive.isSystem
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private getDisabledDrives(drives: Drive[], image?: SourceMetadata): string[] {
|
||||
return drives
|
||||
.filter((drive) => this.driveShouldBeDisabled(drive, image))
|
||||
.map((drive) => drive.displayName);
|
||||
}
|
||||
|
||||
private renderProgress(progress: number) {
|
||||
return (
|
||||
<Flex flexDirection="column">
|
||||
<Txt fontSize={12}>Initializing device</Txt>
|
||||
<InitProgress value={progress} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
private warningFromStatus(
|
||||
status: string,
|
||||
drive: { device: string; size: number },
|
||||
) {
|
||||
switch (status) {
|
||||
case compatibility.containsImage():
|
||||
return warning.sourceDrive();
|
||||
case compatibility.largeDrive():
|
||||
return warning.largeDriveSize();
|
||||
case compatibility.system():
|
||||
return warning.systemDrive();
|
||||
case compatibility.tooSmall():
|
||||
const size =
|
||||
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
|
||||
return warning.tooSmall({ size }, drive);
|
||||
}
|
||||
}
|
||||
|
||||
private renderStatuses(drive: DrivelistDrive) {
|
||||
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
|
||||
drive,
|
||||
this.state.image,
|
||||
this.props.write,
|
||||
).slice(0, 2);
|
||||
return (
|
||||
// the column render fn expects a single Element
|
||||
<>
|
||||
{statuses.map((status) => {
|
||||
const badgeShade = badgeShadeFromStatus(status.message);
|
||||
const warningMessage = this.warningFromStatus(status.message, {
|
||||
device: drive.device,
|
||||
size: drive.size || 0,
|
||||
});
|
||||
return (
|
||||
<Badge
|
||||
key={status.message}
|
||||
shade={badgeShade}
|
||||
mr="8px"
|
||||
tooltip={this.props.showWarnings ? warningMessage : ''}
|
||||
>
|
||||
{status.message}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private installMissingDrivers(drive: DriverlessDrive) {
|
||||
if (drive.link) {
|
||||
logEvent('Open driver link modal', {
|
||||
url: drive.link,
|
||||
});
|
||||
this.setState({ missingDriversModal: { drive } });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribe = store.subscribe(() => {
|
||||
const drives = getDrives();
|
||||
const image = getImage();
|
||||
this.setState({
|
||||
drives,
|
||||
image,
|
||||
selectedList:
|
||||
(this.props.updateSelectedList && this.props.updateSelectedList()) ||
|
||||
[],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe?.();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { cancel, done, ...props } = this.props;
|
||||
const { selectedList, drives, image, missingDriversModal } = this.state;
|
||||
|
||||
const displayedDrives = this.getDisplayedDrives(drives);
|
||||
const disabledDrives = this.getDisabledDrives(drives, image);
|
||||
const numberOfSystemDrives = drives.filter(isSystemDrive).length;
|
||||
const numberOfDisplayedSystemDrives =
|
||||
displayedDrives.filter(isSystemDrive).length;
|
||||
const numberOfHiddenSystemDrives =
|
||||
numberOfSystemDrives - numberOfDisplayedSystemDrives;
|
||||
const hasSystemDrives = selectedList.filter(isSystemDrive).length;
|
||||
const showWarnings = this.props.showWarnings && hasSystemDrives;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Flex alignItems="baseline" mb={18}>
|
||||
<Txt fontSize={24} align="left">
|
||||
{this.props.titleLabel}
|
||||
</Txt>
|
||||
<Txt
|
||||
fontSize={11}
|
||||
ml={12}
|
||||
color="#5b82a7"
|
||||
style={{ fontWeight: 600 }}
|
||||
>
|
||||
{i18next.t('drives.find', { length: drives.length })}
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
|
||||
cancel={() => cancel(this.originalList)}
|
||||
done={() => done(selectedList)}
|
||||
action={i18next.t('drives.select', { select: selectedList.length })}
|
||||
primaryButtonProps={{
|
||||
primary: !showWarnings,
|
||||
warning: showWarnings,
|
||||
disabled: !hasAvailableDrives(),
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{!hasAvailableDrives() ? (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
>
|
||||
{this.props.emptyListIcon}
|
||||
<b>{this.props.emptyListLabel}</b>
|
||||
</Flex>
|
||||
) : (
|
||||
<>
|
||||
<DrivesTable
|
||||
refFn={(t) => {
|
||||
if (t !== null) {
|
||||
t.setRowSelection(selectedList);
|
||||
}
|
||||
}}
|
||||
checkedRowsNumber={selectedList.length}
|
||||
multipleSelection={this.props.multipleSelection}
|
||||
columns={this.tableColumns}
|
||||
data={displayedDrives}
|
||||
disabledRows={disabledDrives}
|
||||
getRowClass={(row: Drive) =>
|
||||
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
|
||||
}
|
||||
rowKey="displayName"
|
||||
onCheck={(rows: Drive[]) => {
|
||||
let newSelection = rows.filter(isDrivelistDrive);
|
||||
if (this.props.multipleSelection) {
|
||||
if (rows.length === 0) {
|
||||
newSelection = [];
|
||||
}
|
||||
const deselecting = selectedList.filter(
|
||||
(selected) =>
|
||||
newSelection.filter(
|
||||
(row) => row.device === selected.device,
|
||||
).length === 0,
|
||||
);
|
||||
const selecting = newSelection.filter(
|
||||
(row) =>
|
||||
selectedList.filter(
|
||||
(selected) => row.device === selected.device,
|
||||
).length === 0,
|
||||
);
|
||||
deselecting.concat(selecting).forEach((row) => {
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(row);
|
||||
}
|
||||
});
|
||||
this.setState({
|
||||
selectedList: newSelection,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(newSelection[newSelection.length - 1]);
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newSelection.slice(newSelection.length - 1),
|
||||
});
|
||||
}}
|
||||
onRowClick={(row: Drive) => {
|
||||
if (
|
||||
!isDrivelistDrive(row) ||
|
||||
this.driveShouldBeDisabled(row, image)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(row);
|
||||
}
|
||||
const index = selectedList.findIndex(
|
||||
(d) => d.device === row.device,
|
||||
);
|
||||
const newList = this.props.multipleSelection
|
||||
? [...selectedList]
|
||||
: [];
|
||||
if (index === -1) {
|
||||
newList.push(row);
|
||||
} else {
|
||||
// Deselect if selected
|
||||
newList.splice(index, 1);
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newList,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{numberOfHiddenSystemDrives > 0 && (
|
||||
<Link
|
||||
mt={15}
|
||||
mb={15}
|
||||
fontSize="14px"
|
||||
onClick={() => this.setState({ showSystemDrives: true })}
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||
<Txt ml={8}>
|
||||
{i18next.t('drives.showHidden', {
|
||||
num: numberOfHiddenSystemDrives,
|
||||
})}
|
||||
</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{this.props.showWarnings && hasSystemDrives ? (
|
||||
<Alert className="system-drive-alert" style={{ width: '67%' }}>
|
||||
{i18next.t('drives.systemDriveDanger')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{missingDriversModal.drive !== undefined && (
|
||||
<Modal
|
||||
width={400}
|
||||
title={missingDriversModal.drive.linkTitle}
|
||||
cancel={() => this.setState({ missingDriversModal: {} })}
|
||||
done={() => {
|
||||
try {
|
||||
if (missingDriversModal.drive !== undefined) {
|
||||
openExternal(missingDriversModal.drive.link);
|
||||
}
|
||||
} catch (error: any) {
|
||||
logException(error);
|
||||
} finally {
|
||||
this.setState({ missingDriversModal: {} });
|
||||
}
|
||||
}}
|
||||
action={i18next.t('yesContinue')}
|
||||
cancelButtonProps={{
|
||||
children: i18next.t('cancel'),
|
||||
}}
|
||||
children={
|
||||
missingDriversModal.drive.linkMessage ||
|
||||
i18next.t('drives.openInBrowser', {
|
||||
link: missingDriversModal.drive.link,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Badge, Flex, Txt, ModalProps } from 'rendition';
|
||||
import { Modal, ScrollableFlex } from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { DriveWithWarnings } from '../../pages/main/Flash';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const DriveStatusWarningModal = ({
|
||||
done,
|
||||
cancel,
|
||||
isSystem,
|
||||
drivesWithWarnings,
|
||||
}: ModalProps & {
|
||||
isSystem: boolean;
|
||||
drivesWithWarnings: DriveWithWarnings[];
|
||||
}) => {
|
||||
let warningSubtitle = i18next.t('drives.largeDriveWarning');
|
||||
let warningCta = i18next.t('drives.largeDriveWarningMsg');
|
||||
|
||||
if (isSystem) {
|
||||
warningSubtitle = i18next.t('drives.systemDriveWarning');
|
||||
warningCta = i18next.t('drives.systemDriveWarningMsg');
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
footerShadow={false}
|
||||
reverseFooterButtons={true}
|
||||
done={done}
|
||||
cancel={cancel}
|
||||
cancelButtonProps={{
|
||||
primary: false,
|
||||
warning: true,
|
||||
children: i18next.t('drives.changeTarget'),
|
||||
}}
|
||||
action={i18next.t('sure')}
|
||||
primaryButtonProps={{
|
||||
primary: false,
|
||||
outline: true,
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
width="100%"
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
<ExclamationTriangleSvg height="2em" fill="#fca321" />
|
||||
<Txt fontSize="24px" color="#fca321">
|
||||
{i18next.t('warning')}
|
||||
</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize="24px">{warningSubtitle}</Txt>
|
||||
<ScrollableFlex
|
||||
flexDirection="column"
|
||||
backgroundColor="#fff5e6"
|
||||
m="2em 0"
|
||||
p="1em 2em"
|
||||
width="420px"
|
||||
maxHeight="100px"
|
||||
>
|
||||
{drivesWithWarnings.map((drive, i, array) => (
|
||||
<>
|
||||
<Flex justifyContent="space-between" alignItems="baseline">
|
||||
<strong>{middleEllipsis(drive.description, 28)}</strong>{' '}
|
||||
{drive.size && prettyBytes(drive.size) + ' '}
|
||||
<Badge shade={5}>{drive.statuses[0].message}</Badge>
|
||||
</Flex>
|
||||
{i !== array.length - 1 ? <hr style={{ width: '100%' }} /> : null}
|
||||
</>
|
||||
))}
|
||||
</ScrollableFlex>
|
||||
<Txt style={{ fontWeight: 600 }}>{warningCta}</Txt>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DriveStatusWarningModal;
|
124
lib/gui/app/components/finish/finish.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2019 balena.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Flex } from 'rendition';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import * as settings from '../../models/settings';
|
||||
import { Actions, store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { FlashAnother } from '../flash-another/flash-another';
|
||||
import { FlashResults, FlashError } from '../flash-results/flash-results';
|
||||
import { SafeWebview } from '../safe-webview/safe-webview';
|
||||
|
||||
function restart(goToMain: () => void) {
|
||||
selectionState.deselectAllDrives();
|
||||
analytics.logEvent('Restart');
|
||||
|
||||
// Reset the flashing workflow uuid
|
||||
store.dispatch({
|
||||
type: Actions.SET_FLASHING_WORKFLOW_UUID,
|
||||
data: uuidV4(),
|
||||
});
|
||||
|
||||
goToMain();
|
||||
}
|
||||
|
||||
async function getSuccessBannerURL() {
|
||||
return (
|
||||
(await settings.get('successBannerURL')) ??
|
||||
'https://efp.balena.io/success-banner?borderTop=false&darkBackground=true'
|
||||
);
|
||||
}
|
||||
|
||||
function FinishPage({ goToMain }: { goToMain: () => void }) {
|
||||
const [webviewShowing, setWebviewShowing] = React.useState(false);
|
||||
const [successBannerURL, setSuccessBannerURL] = React.useState('');
|
||||
(async () => {
|
||||
setSuccessBannerURL(await getSuccessBannerURL());
|
||||
})();
|
||||
const flashResults = flashState.getFlashResults();
|
||||
const errors: FlashError[] = (
|
||||
store.getState().toJS().failedDeviceErrors || []
|
||||
).map(([, error]: [string, FlashError]) => ({
|
||||
...error,
|
||||
}));
|
||||
const { averageSpeed, blockmappedSize, bytesWritten, failed, size } =
|
||||
flashState.getFlashState();
|
||||
const {
|
||||
skip,
|
||||
results = {
|
||||
bytesWritten,
|
||||
sourceMetadata: {
|
||||
size,
|
||||
blockmappedSize,
|
||||
},
|
||||
averageFlashingSpeed: averageSpeed,
|
||||
devices: { failed, successful: 0 },
|
||||
},
|
||||
} = flashResults;
|
||||
return (
|
||||
<Flex height="100%" justifyContent="space-between">
|
||||
<Flex
|
||||
width={webviewShowing ? '36.2vw' : '100vw'}
|
||||
height="100vh"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
flexDirection="column"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||
}}
|
||||
>
|
||||
<FlashResults
|
||||
image={selectionState.getImage()?.name}
|
||||
results={results}
|
||||
skip={skip}
|
||||
errors={errors}
|
||||
mb="32px"
|
||||
goToMain={goToMain}
|
||||
/>
|
||||
|
||||
<FlashAnother
|
||||
onClick={() => {
|
||||
restart(goToMain);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
{successBannerURL.length && (
|
||||
<SafeWebview
|
||||
src={successBannerURL}
|
||||
onWebviewShow={setWebviewShowing}
|
||||
style={{
|
||||
display: webviewShowing ? 'flex' : 'none',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '63.8vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default FinishPage;
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
* Copyright 2019 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,14 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.page-settings .checkbox input[type="checkbox"] + * {
|
||||
color: $palette-theme-dark-foreground;
|
||||
import * as React from 'react';
|
||||
|
||||
import { BaseButton } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
export interface FlashAnotherProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
.page-settings .checkbox input[type="checkbox"]:not(:checked) + * {
|
||||
color: $palette-theme-dark-soft-foreground;
|
||||
}
|
||||
|
||||
.page-settings .title {
|
||||
color: $palette-theme-dark-foreground;
|
||||
}
|
||||
export const FlashAnother = (props: FlashAnotherProps) => {
|
||||
return (
|
||||
<BaseButton primary onClick={props.onClick}>
|
||||
{i18next.t('flash.another')}
|
||||
</BaseButton>
|
||||
);
|
||||
};
|
244
lib/gui/app/components/flash-results/flash-results.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright 2019 balena.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
||||
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
|
||||
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
|
||||
import outdent from 'outdent';
|
||||
import * as React from 'react';
|
||||
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { progress } from '../../../../shared/messages';
|
||||
import { bytesToMegabytes } from '../../../../shared/units';
|
||||
|
||||
import FlashSvg from '../../../assets/flash.svg';
|
||||
import { getDrives } from '../../models/available-drives';
|
||||
import { resetState } from '../../models/flash-state';
|
||||
import * as selection from '../../models/selection-state';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import { Modal, Table } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
|
||||
&&& [data-display='table-head'],
|
||||
&&& [data-display='table-body'] {
|
||||
> [data-display='table-row'] {
|
||||
> [data-display='table-cell'] {
|
||||
&:first-child {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const DoneIcon = (props: {
|
||||
skipped: boolean;
|
||||
color: string;
|
||||
allFailed: boolean;
|
||||
}) => {
|
||||
const svgProps = {
|
||||
width: '28px',
|
||||
fill: props.color,
|
||||
style: {
|
||||
marginTop: '-25px',
|
||||
marginLeft: '13px',
|
||||
zIndex: 1,
|
||||
},
|
||||
};
|
||||
return props.allFailed && !props.skipped ? (
|
||||
<TimesCircleSvg {...svgProps} />
|
||||
) : (
|
||||
<CheckCircleSvg {...svgProps} />
|
||||
);
|
||||
};
|
||||
|
||||
export interface FlashError extends Error {
|
||||
description: string;
|
||||
device: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
function formattedErrors(errors: FlashError[]) {
|
||||
return errors
|
||||
.map((error) => `${error.device}: ${error.message || error.code}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
const columns: Array<TableColumn<FlashError>> = [
|
||||
{
|
||||
field: 'description',
|
||||
label: i18next.t('flash.target'),
|
||||
},
|
||||
{
|
||||
field: 'device',
|
||||
label: i18next.t('flash.location'),
|
||||
},
|
||||
{
|
||||
field: 'message',
|
||||
label: i18next.t('flash.error'),
|
||||
render: (message: string, { code }: FlashError) => {
|
||||
return message ?? code;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function getEffectiveSpeed(results: {
|
||||
sourceMetadata: {
|
||||
size: number;
|
||||
blockmappedSize?: number;
|
||||
};
|
||||
averageFlashingSpeed: number;
|
||||
}) {
|
||||
const flashedSize =
|
||||
results.sourceMetadata.blockmappedSize ?? results.sourceMetadata.size;
|
||||
const timeSpent = flashedSize / results.averageFlashingSpeed;
|
||||
return results.sourceMetadata.size / timeSpent;
|
||||
}
|
||||
|
||||
export function FlashResults({
|
||||
goToMain,
|
||||
image = '',
|
||||
errors,
|
||||
results,
|
||||
skip,
|
||||
...props
|
||||
}: {
|
||||
goToMain: () => void;
|
||||
image?: string;
|
||||
errors: FlashError[];
|
||||
skip: boolean;
|
||||
results: {
|
||||
sourceMetadata: {
|
||||
size: number;
|
||||
blockmappedSize?: number;
|
||||
};
|
||||
averageFlashingSpeed: number;
|
||||
devices: { failed: number; successful: number };
|
||||
};
|
||||
} & FlexProps) {
|
||||
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
|
||||
|
||||
const allFailed = !skip && results?.devices?.successful === 0;
|
||||
const someFailed = results?.devices?.failed !== 0 || errors?.length !== 0;
|
||||
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
|
||||
1,
|
||||
);
|
||||
return (
|
||||
<Flex flexDirection="column" {...props}>
|
||||
<Flex alignItems="center" flexDirection="column">
|
||||
<Flex
|
||||
alignItems="center"
|
||||
mt="50px"
|
||||
mb="32px"
|
||||
color="#7e8085"
|
||||
flexDirection="column"
|
||||
>
|
||||
<FlashSvg width="40px" height="40px" className="disabled" />
|
||||
<DoneIcon
|
||||
skipped={skip}
|
||||
allFailed={allFailed}
|
||||
color={allFailed || someFailed ? '#c6c8c9' : '#1ac135'}
|
||||
/>
|
||||
<Txt>{middleEllipsis(image, 24)}</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize={24} color="#fff" mb="17px">
|
||||
{allFailed
|
||||
? i18next.t('flash.flashFailed')
|
||||
: i18next.t('flash.flashCompleted')}
|
||||
</Txt>
|
||||
{skip ? <Txt color="#7e8085">{i18next.t('flash.skip')}</Txt> : null}
|
||||
</Flex>
|
||||
<Flex flexDirection="column" color="#7e8085">
|
||||
{results.devices.successful !== 0 ? (
|
||||
<Flex alignItems="center">
|
||||
<CircleSvg width="14px" fill="#1ac135" />
|
||||
<Txt ml="10px" color="#fff">
|
||||
{results.devices.successful}
|
||||
</Txt>
|
||||
<Txt ml="10px">
|
||||
{progress.successful(results.devices.successful)}
|
||||
</Txt>
|
||||
</Flex>
|
||||
) : null}
|
||||
{errors.length !== 0 ? (
|
||||
<Flex alignItems="center">
|
||||
<CircleSvg width="14px" fill="#ff4444" />
|
||||
<Txt ml="10px" color="#fff">
|
||||
{errors.length}
|
||||
</Txt>
|
||||
<Txt ml="10px" tooltip={formattedErrors(errors)}>
|
||||
{progress.failed(errors.length)}
|
||||
</Txt>
|
||||
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
|
||||
{i18next.t('flash.moreInfo')}
|
||||
</Link>
|
||||
</Flex>
|
||||
) : null}
|
||||
{!allFailed && (
|
||||
<Txt
|
||||
fontSize="10px"
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
tooltip={i18next.t('flash.speedTip')}
|
||||
>
|
||||
{i18next.t('flash.speed', { speed: effectiveSpeed })}
|
||||
</Txt>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{showErrorsInfo && (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Flex alignItems="baseline" mb={18}>
|
||||
<Txt fontSize={24} align="left">
|
||||
{i18next.t('failedTarget')}
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
action={i18next.t('failedRetry')}
|
||||
cancel={() => setShowErrorsInfo(false)}
|
||||
done={() => {
|
||||
setShowErrorsInfo(false);
|
||||
resetState();
|
||||
getDrives()
|
||||
.map((drive) => {
|
||||
selection.deselectDrive(drive.device);
|
||||
return drive.device;
|
||||
})
|
||||
.filter((driveDevice) =>
|
||||
errors.some((error) => error.device === driveDevice),
|
||||
)
|
||||
.forEach((driveDevice) => selection.selectDrive(driveDevice));
|
||||
goToMain();
|
||||
}}
|
||||
>
|
||||
<ErrorsTable columns={columns} data={errors} />
|
||||
</Modal>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
136
lib/gui/app/components/progress-button/progress-button.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Flex, Button, ProgressBar, Txt } from 'rendition';
|
||||
import { default as styled } from 'styled-components';
|
||||
|
||||
import { fromFlashState } from '../../modules/progress-status';
|
||||
import { StepButton } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const FlashProgressBar = styled(ProgressBar)`
|
||||
> div {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
color: white !important;
|
||||
text-shadow: none !important;
|
||||
transition-duration: 0s;
|
||||
|
||||
> div {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 14px;
|
||||
font-size: 16px;
|
||||
line-height: 48px;
|
||||
|
||||
background: #2f3033;
|
||||
`;
|
||||
|
||||
interface ProgressButtonProps {
|
||||
type: 'decompressing' | 'flashing' | 'verifying';
|
||||
active: boolean;
|
||||
percentage: number;
|
||||
position: number;
|
||||
disabled: boolean;
|
||||
cancel: (type: string) => void;
|
||||
callback: () => void;
|
||||
warning?: boolean;
|
||||
}
|
||||
|
||||
const colors = {
|
||||
decompressing: '#00aeef',
|
||||
flashing: '#da60ff',
|
||||
verifying: '#1ac135',
|
||||
} as const;
|
||||
|
||||
const CancelButton = styled(({ type, onClick, ...props }) => {
|
||||
const status = type === 'verifying' ? i18next.t('skip') : i18next.t('cancel');
|
||||
return (
|
||||
<Button plain onClick={() => onClick(status)} {...props}>
|
||||
{status}
|
||||
</Button>
|
||||
);
|
||||
})`
|
||||
font-weight: 600;
|
||||
|
||||
&&& {
|
||||
width: auto;
|
||||
height: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||
public render() {
|
||||
const percentage = this.props.percentage;
|
||||
const warning = this.props.warning;
|
||||
const { status, position } = fromFlashState({
|
||||
type: this.props.type,
|
||||
percentage,
|
||||
position: this.props.position,
|
||||
});
|
||||
const type = this.props.type || 'default';
|
||||
if (this.props.active) {
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
alignItems="baseline"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
style={{
|
||||
marginTop: 42,
|
||||
marginBottom: '6px',
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<Flex>
|
||||
<Txt color="#fff">{status} </Txt>
|
||||
<Txt color={colors[type]}>{position}</Txt>
|
||||
</Flex>
|
||||
{type && (
|
||||
<CancelButton
|
||||
type={type}
|
||||
onClick={this.props.cancel}
|
||||
color="#00aeef"
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<FlashProgressBar background={colors[type]} value={percentage} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<StepButton
|
||||
primary={!warning}
|
||||
warning={warning}
|
||||
onClick={this.props.callback}
|
||||
disabled={this.props.disabled}
|
||||
style={{
|
||||
marginTop: 30,
|
||||
}}
|
||||
>
|
||||
{i18next.t('flash.flashNow')}
|
||||
</StepButton>
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
|
||||
import DriveSvg from '../../../assets/drive.svg';
|
||||
import ImageSvg from '../../../assets/image.svg';
|
||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
interface ReducedFlashingInfosProps {
|
||||
imageLogo?: string;
|
||||
imageName?: string;
|
||||
imageSize: string;
|
||||
driveTitle: string;
|
||||
driveLabel: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export class ReducedFlashingInfos extends React.Component<ReducedFlashingInfosProps> {
|
||||
constructor(props: ReducedFlashingInfosProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { imageName = '' } = this.props;
|
||||
return (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
style={this.props.style ? this.props.style : undefined}
|
||||
>
|
||||
<Flex mb={16}>
|
||||
<SVGIcon
|
||||
disabled
|
||||
width="21px"
|
||||
height="21px"
|
||||
contents={this.props.imageLogo}
|
||||
fallback={ImageSvg}
|
||||
style={{ marginRight: '9px' }}
|
||||
/>
|
||||
<Txt
|
||||
style={{ marginRight: '9px' }}
|
||||
tooltip={{ text: imageName, placement: 'right' }}
|
||||
>
|
||||
{middleEllipsis(imageName, 16)}
|
||||
</Txt>
|
||||
<Txt color="#7e8085">{this.props.imageSize}</Txt>
|
||||
</Flex>
|
||||
|
||||
<Flex>
|
||||
<DriveSvg width="21px" height="21px" style={{ marginRight: '9px' }} />
|
||||
<Txt tooltip={{ text: this.props.driveLabel, placement: 'right' }}>
|
||||
{middleEllipsis(this.props.driveTitle, 16)}
|
||||
</Txt>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
211
lib/gui/app/components/safe-webview/safe-webview.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2017 balena.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as remote from '@electron/remote';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
|
||||
import * as packageJSON from '../../../../../package.json';
|
||||
import * as settings from '../../models/settings';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
|
||||
/**
|
||||
* @summary Electron session identifier
|
||||
*/
|
||||
const ELECTRON_SESSION = 'persist:success-banner';
|
||||
|
||||
/**
|
||||
* @summary Etcher version search-parameter key
|
||||
*/
|
||||
const ETCHER_VERSION_PARAM = 'etcher-version';
|
||||
|
||||
/**
|
||||
* @summary API version search-parameter key
|
||||
*/
|
||||
const API_VERSION_PARAM = 'api-version';
|
||||
|
||||
/**
|
||||
* @summary Opt-out analytics search-parameter key
|
||||
*/
|
||||
const OPT_OUT_ANALYTICS_PARAM = 'optOutAnalytics';
|
||||
|
||||
/**
|
||||
* @summary Webview API version
|
||||
*
|
||||
* @description
|
||||
* Changing this number represents a departure from an older API and as such
|
||||
* should only be changed when truly necessary as it introduces breaking changes.
|
||||
* This version number is exposed to the banner such that it can determine what
|
||||
* features are safe to utilize.
|
||||
*
|
||||
* See `git blame -L n` where n is the line below for the history of version changes.
|
||||
*/
|
||||
const API_VERSION = '2';
|
||||
|
||||
interface SafeWebviewProps {
|
||||
// The website source URL
|
||||
src: string;
|
||||
// Webview lifecycle event
|
||||
onWebviewShow?: (isWebviewShowing: boolean) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
interface SafeWebviewState {
|
||||
shouldShow: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Webviews that hide/show depending on the HTTP status returned
|
||||
*/
|
||||
export class SafeWebview extends React.PureComponent<
|
||||
SafeWebviewProps,
|
||||
SafeWebviewState
|
||||
> {
|
||||
private entryHref: string;
|
||||
private session: electron.Session;
|
||||
private webviewRef: React.RefObject<electron.WebviewTag>;
|
||||
|
||||
constructor(props: SafeWebviewProps) {
|
||||
super(props);
|
||||
this.webviewRef = React.createRef();
|
||||
this.state = {
|
||||
shouldShow: true,
|
||||
};
|
||||
const url = new window.URL(this.props.src);
|
||||
// We set the version GET parameters here.
|
||||
url.searchParams.set(ETCHER_VERSION_PARAM, packageJSON.version);
|
||||
url.searchParams.set(API_VERSION_PARAM, API_VERSION);
|
||||
url.searchParams.set(
|
||||
OPT_OUT_ANALYTICS_PARAM,
|
||||
(!settings.getSync('errorReporting')).toString(),
|
||||
);
|
||||
this.entryHref = url.href;
|
||||
// Events steal 'this'
|
||||
this.handleDomReady = _.bind(this.handleDomReady, this);
|
||||
this.didFailLoad = _.bind(this.didFailLoad, this);
|
||||
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this);
|
||||
// Make a persistent electron session for the webview
|
||||
this.session = remote.session.fromPartition(ELECTRON_SESSION, {
|
||||
// Disable the cache for the session such that new content shows up when refreshing
|
||||
cache: false,
|
||||
});
|
||||
}
|
||||
|
||||
private static logWebViewMessage(event: electron.ConsoleMessageEvent) {
|
||||
console.log('Message from SafeWebview:', event.message);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
style = {
|
||||
flex: this.state.shouldShow ? undefined : '0 1',
|
||||
width: this.state.shouldShow ? undefined : '0',
|
||||
height: this.state.shouldShow ? undefined : '0',
|
||||
},
|
||||
} = this.props;
|
||||
return (
|
||||
<webview
|
||||
ref={this.webviewRef}
|
||||
partition={ELECTRON_SESSION}
|
||||
style={style}
|
||||
// @ts-ignore
|
||||
allowpopups="true"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Add the Webview events
|
||||
public componentDidMount() {
|
||||
// Events React is unaware of have to be handled manually
|
||||
if (this.webviewRef.current !== null) {
|
||||
this.webviewRef.current.addEventListener(
|
||||
'did-fail-load',
|
||||
this.didFailLoad,
|
||||
);
|
||||
this.webviewRef.current.addEventListener(
|
||||
'dom-ready',
|
||||
this.handleDomReady,
|
||||
);
|
||||
this.webviewRef.current.addEventListener(
|
||||
'console-message',
|
||||
SafeWebview.logWebViewMessage,
|
||||
);
|
||||
this.session.webRequest.onCompleted(this.didGetResponseDetails);
|
||||
// It's important that this comes after the partition setting, otherwise it will
|
||||
// use another session and we can't change it without destroying the element again
|
||||
this.webviewRef.current.src = this.entryHref;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the Webview events
|
||||
public componentWillUnmount() {
|
||||
// Events that React is unaware of have to be handled manually
|
||||
if (this.webviewRef.current !== null) {
|
||||
this.webviewRef.current.removeEventListener(
|
||||
'did-fail-load',
|
||||
this.didFailLoad,
|
||||
);
|
||||
this.webviewRef.current.removeEventListener(
|
||||
'dom-ready',
|
||||
this.handleDomReady,
|
||||
);
|
||||
this.webviewRef.current.removeEventListener(
|
||||
'console-message',
|
||||
SafeWebview.logWebViewMessage,
|
||||
);
|
||||
}
|
||||
this.session.webRequest.onCompleted(null);
|
||||
}
|
||||
|
||||
handleDomReady() {
|
||||
const webview = this.webviewRef.current;
|
||||
if (webview == null) {
|
||||
return;
|
||||
}
|
||||
const id = webview.getWebContentsId();
|
||||
electron.ipcRenderer.send('webview-dom-ready', id);
|
||||
}
|
||||
|
||||
// Set the element state to hidden
|
||||
public didFailLoad() {
|
||||
this.setState({
|
||||
shouldShow: false,
|
||||
});
|
||||
if (this.props.onWebviewShow) {
|
||||
this.props.onWebviewShow(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the element state depending on the HTTP response code
|
||||
public didGetResponseDetails(event: electron.OnCompletedListenerDetails) {
|
||||
// This seems to pick up all requests related to the webview,
|
||||
// only care about this event if it's a request for the main frame
|
||||
if (event.resourceType === 'mainFrame') {
|
||||
const HTTP_OK = 200;
|
||||
const { webContents, ...webviewEvent } = event;
|
||||
analytics.logEvent('SafeWebview loaded', {
|
||||
...webviewEvent,
|
||||
});
|
||||
this.setState({
|
||||
shouldShow: event.statusCode === HTTP_OK,
|
||||
});
|
||||
if (this.props.onWebviewShow) {
|
||||
this.props.onWebviewShow(event.statusCode === HTTP_OK);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
156
lib/gui/app/components/settings/settings.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2019 balena.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Box, Checkbox, Flex, TextWithCopy, Txt } from 'rendition';
|
||||
|
||||
import { version, packageType } from '../../../../../package.json';
|
||||
import * as settings from '../../models/settings';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import { Modal } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
import { etcherProInfo } from '../../utils/etcher-pro-specific';
|
||||
|
||||
interface Setting {
|
||||
name: string;
|
||||
label: string | JSX.Element;
|
||||
}
|
||||
|
||||
async function getSettingsList(): Promise<Setting[]> {
|
||||
const list: Setting[] = [
|
||||
{
|
||||
name: 'errorReporting',
|
||||
label: i18next.t('settings.errorReporting'),
|
||||
},
|
||||
{
|
||||
name: 'autoBlockmapping',
|
||||
label: i18next.t('settings.trimExtPartitions'),
|
||||
},
|
||||
];
|
||||
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
|
||||
list.push({
|
||||
name: 'updatesEnabled',
|
||||
label: i18next.t('settings.autoUpdate'),
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
interface SettingsModalProps {
|
||||
toggleModal: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const EPInfo = etcherProInfo();
|
||||
|
||||
const InfoBox = (props: any) => (
|
||||
<Box fontSize={14}>
|
||||
<Txt>{props.label}</Txt>
|
||||
<TextWithCopy code text={props.value} copy={props.value} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (settingsList.length === 0) {
|
||||
setCurrentSettingsList(await getSettingsList());
|
||||
}
|
||||
})();
|
||||
});
|
||||
const [currentSettings, setCurrentSettings] = React.useState<
|
||||
_.Dictionary<boolean>
|
||||
>({});
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (_.isEmpty(currentSettings)) {
|
||||
setCurrentSettings(await settings.getAll());
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
const toggleSetting = async (setting: string) => {
|
||||
const value = currentSettings[setting];
|
||||
analytics.logEvent('Toggle setting', { setting, value });
|
||||
await settings.set(setting, !value);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[setting]: !value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Txt fontSize={24} mb={24}>
|
||||
{i18next.t('settings.settings')}
|
||||
</Txt>
|
||||
}
|
||||
done={() => toggleModal(false)}
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
{settingsList.map((setting: Setting, i: number) => {
|
||||
return (
|
||||
<Flex key={setting.name} mb={14}>
|
||||
<Checkbox
|
||||
toggle
|
||||
tabIndex={6 + i}
|
||||
label={setting.label}
|
||||
checked={currentSettings[setting.name]}
|
||||
onChange={() => toggleSetting(setting.name)}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
{EPInfo !== undefined && (
|
||||
<Flex flexDirection="column">
|
||||
<Txt fontSize={24}>{i18next.t('settings.systemInformation')}</Txt>
|
||||
{EPInfo.get_serial() === undefined ? (
|
||||
<InfoBox label="UUID" value={EPInfo.uuid} />
|
||||
) : (
|
||||
<InfoBox label="Serial" value={EPInfo.get_serial()} />
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
<Flex
|
||||
mt={18}
|
||||
alignItems="center"
|
||||
color="#00aeef"
|
||||
style={{
|
||||
width: 'fit-content',
|
||||
cursor: 'pointer',
|
||||
fontSize: 14,
|
||||
}}
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
|
||||
)
|
||||
}
|
||||
>
|
||||
<GithubSvg
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Txt style={{ borderBottom: '1px solid #00aeef' }}>{version}</Txt>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
}
|
790
lib/gui/app/components/source-selector/source-selector.tsx
Normal file
@@ -0,0 +1,790 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
|
||||
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
|
||||
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import { uniqBy, isNil } from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as React from 'react';
|
||||
import { requestMetadata } from '../../app';
|
||||
|
||||
import {
|
||||
Flex,
|
||||
ButtonProps,
|
||||
Modal as SmallModal,
|
||||
Txt,
|
||||
Card as BaseCard,
|
||||
Input,
|
||||
Spinner,
|
||||
Link,
|
||||
} from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as errors from '../../../../shared/errors';
|
||||
import * as messages from '../../../../shared/messages';
|
||||
import * as supportedFormats from '../../../../shared/supported-formats';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import { observe } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import * as exceptionReporter from '../../modules/exception-reporter';
|
||||
import * as osDialog from '../../os/dialog';
|
||||
|
||||
import {
|
||||
ChangeButton,
|
||||
DetailsText,
|
||||
Modal,
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
ScrollableFlex,
|
||||
} from '../../styled-components';
|
||||
import { colors } from '../../theme';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
|
||||
import ImageSvg from '../../../assets/image.svg';
|
||||
import SrcSvg from '../../../assets/src.svg';
|
||||
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import { isJson } from '../../../../shared/utils';
|
||||
import {
|
||||
SourceMetadata,
|
||||
Authentication,
|
||||
Source,
|
||||
} from '../../../../shared/typings/source-selector';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const recentUrlImagesKey = 'recentUrlImages';
|
||||
|
||||
function normalizeRecentUrlImages(urls: any[]): URL[] {
|
||||
if (!Array.isArray(urls)) {
|
||||
urls = [];
|
||||
}
|
||||
urls = urls
|
||||
.map((url) => {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (error: any) {
|
||||
// Invalid URL, skip
|
||||
}
|
||||
})
|
||||
.filter((url) => url !== undefined);
|
||||
urls = uniqBy(urls, (url) => url.href);
|
||||
return urls.slice(urls.length - 5);
|
||||
}
|
||||
|
||||
function getRecentUrlImages(): URL[] {
|
||||
let urls = [];
|
||||
try {
|
||||
urls = JSON.parse(localStorage.getItem(recentUrlImagesKey) || '[]');
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
return normalizeRecentUrlImages(urls);
|
||||
}
|
||||
|
||||
function setRecentUrlImages(urls: URL[]) {
|
||||
const normalized = normalizeRecentUrlImages(urls.map((url: URL) => url.href));
|
||||
localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized));
|
||||
}
|
||||
|
||||
const isURL = (imagePath: string) =>
|
||||
imagePath.startsWith('https://') || imagePath.startsWith('http://');
|
||||
|
||||
const Card = styled(BaseCard)`
|
||||
hr {
|
||||
margin: 5px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
// TODO move these styles to rendition
|
||||
const ModalText = styled.p`
|
||||
a {
|
||||
color: rgb(0, 174, 239);
|
||||
|
||||
&:hover {
|
||||
color: rgb(0, 139, 191);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function getState() {
|
||||
const image = selectionState.getImage();
|
||||
return {
|
||||
hasImage: selectionState.hasImage(),
|
||||
imageName: image?.name,
|
||||
imageSize: image?.size,
|
||||
};
|
||||
}
|
||||
|
||||
function isString(value: any): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
const URLSelector = ({
|
||||
done,
|
||||
cancel,
|
||||
}: {
|
||||
done: (imageURL: string, auth?: Authentication) => void;
|
||||
cancel: () => void;
|
||||
}) => {
|
||||
const [imageURL, setImageURL] = React.useState('');
|
||||
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [showBasicAuth, setShowBasicAuth] = React.useState(false);
|
||||
const [username, setUsername] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
React.useEffect(() => {
|
||||
const fetchRecentUrlImages = async () => {
|
||||
const recentUrlImages: URL[] = await getRecentUrlImages();
|
||||
setRecentImages(recentUrlImages);
|
||||
};
|
||||
fetchRecentUrlImages();
|
||||
}, []);
|
||||
return (
|
||||
<Modal
|
||||
cancel={cancel}
|
||||
primaryButtonProps={{
|
||||
disabled: loading || !imageURL,
|
||||
}}
|
||||
action={loading ? <Spinner /> : i18next.t('ok')}
|
||||
done={async () => {
|
||||
setLoading(true);
|
||||
const urlStrings = recentImages.map((url: URL) => url.href);
|
||||
const normalizedRecentUrls = normalizeRecentUrlImages([
|
||||
...urlStrings,
|
||||
imageURL,
|
||||
]);
|
||||
setRecentUrlImages(normalizedRecentUrls);
|
||||
const auth = username ? { username, password } : undefined;
|
||||
await done(imageURL, auth);
|
||||
}}
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
<Flex mb={15} style={{ width: '100%' }} flexDirection="column">
|
||||
<Txt mb="10px" fontSize="24px">
|
||||
{i18next.t('source.useSourceURL')}
|
||||
</Txt>
|
||||
<Input
|
||||
value={imageURL}
|
||||
placeholder={i18next.t('source.enterValidURL')}
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setImageURL(evt.target.value)
|
||||
}
|
||||
/>
|
||||
<Link
|
||||
mt={15}
|
||||
mb={15}
|
||||
fontSize="14px"
|
||||
onClick={() => {
|
||||
if (showBasicAuth) {
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
}
|
||||
setShowBasicAuth(!showBasicAuth);
|
||||
}}
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
{showBasicAuth && (
|
||||
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||
)}
|
||||
{!showBasicAuth && (
|
||||
<ChevronRightSvg height="1em" fill="currentColor" />
|
||||
)}
|
||||
<Txt ml={8}>{i18next.t('source.auth')}</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
{showBasicAuth && (
|
||||
<React.Fragment>
|
||||
<Input
|
||||
mb={15}
|
||||
value={username}
|
||||
placeholder={i18next.t('source.username')}
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setUsername(evt.target.value)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
value={password}
|
||||
placeholder={i18next.t('source.password')}
|
||||
type="password"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setPassword(evt.target.value)
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Flex>
|
||||
{recentImages.length > 0 && (
|
||||
<Flex flexDirection="column" height="78.6%">
|
||||
<Txt fontSize={18}>Recent</Txt>
|
||||
<ScrollableFlex flexDirection="column">
|
||||
<Card
|
||||
p="10px 15px"
|
||||
rows={recentImages
|
||||
.map((recent) => (
|
||||
<Txt
|
||||
key={recent.href}
|
||||
onClick={() => {
|
||||
setImageURL(recent.href);
|
||||
}}
|
||||
style={{
|
||||
overflowWrap: 'break-word',
|
||||
}}
|
||||
>
|
||||
{recent.pathname.split('/').pop()} - {recent.href}
|
||||
</Txt>
|
||||
))
|
||||
.reverse()}
|
||||
/>
|
||||
</ScrollableFlex>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface Flow {
|
||||
icon?: JSX.Element;
|
||||
onClick: (evt: React.MouseEvent) => void;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const FlowSelector = styled(
|
||||
({ flow, ...props }: { flow: Flow } & ButtonProps) => (
|
||||
<StepButton
|
||||
plain={!props.primary}
|
||||
primary={props.primary}
|
||||
onClick={(evt: React.MouseEvent<Element, MouseEvent>) =>
|
||||
flow.onClick(evt)
|
||||
}
|
||||
icon={flow.icon}
|
||||
{...props}
|
||||
>
|
||||
{flow.label}
|
||||
</StepButton>
|
||||
),
|
||||
)`
|
||||
border-radius: 24px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
|
||||
:enabled:focus,
|
||||
:enabled:focus svg {
|
||||
color: ${colors.primary.foreground} !important;
|
||||
}
|
||||
|
||||
:enabled:hover {
|
||||
background-color: ${colors.primary.background};
|
||||
color: ${colors.primary.foreground};
|
||||
font-weight: 600;
|
||||
|
||||
svg {
|
||||
color: ${colors.primary.foreground} !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface SourceSelectorProps {
|
||||
flashing: boolean;
|
||||
}
|
||||
|
||||
interface SourceSelectorState {
|
||||
hasImage: boolean;
|
||||
imageName?: string;
|
||||
imageSize?: number;
|
||||
warning: { message: string; title: string | null } | null;
|
||||
showImageDetails: boolean;
|
||||
showURLSelector: boolean;
|
||||
showDriveSelector: boolean;
|
||||
defaultFlowActive: boolean;
|
||||
imageSelectorOpen: boolean;
|
||||
imageLoading: boolean;
|
||||
}
|
||||
|
||||
export class SourceSelector extends React.Component<
|
||||
SourceSelectorProps,
|
||||
SourceSelectorState
|
||||
> {
|
||||
private unsubscribe: (() => void) | undefined;
|
||||
|
||||
constructor(props: SourceSelectorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...getState(),
|
||||
warning: null,
|
||||
showImageDetails: false,
|
||||
showURLSelector: false,
|
||||
showDriveSelector: false,
|
||||
defaultFlowActive: true,
|
||||
imageSelectorOpen: false,
|
||||
imageLoading: false,
|
||||
};
|
||||
|
||||
// Bind `this` since it's used in an event's callback
|
||||
this.onSelectImage = this.onSelectImage.bind(this);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.unsubscribe = observe(() => {
|
||||
this.setState(getState());
|
||||
});
|
||||
ipcRenderer.on('select-image', this.onSelectImage);
|
||||
ipcRenderer.send('source-selector-ready');
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.unsubscribe?.();
|
||||
ipcRenderer.removeListener('select-image', this.onSelectImage);
|
||||
}
|
||||
|
||||
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
||||
this.setState({ imageLoading: true });
|
||||
await this.selectSource(
|
||||
imagePath,
|
||||
isURL(this.normalizeImagePath(imagePath)) ? 'Http' : 'File',
|
||||
).promise;
|
||||
this.setState({ imageLoading: false });
|
||||
}
|
||||
|
||||
public normalizeImagePath(imgPath: string) {
|
||||
const decodedPath = decodeURIComponent(imgPath);
|
||||
if (isJson(decodedPath)) {
|
||||
return JSON.parse(decodedPath).url ?? decodedPath;
|
||||
}
|
||||
return decodedPath;
|
||||
}
|
||||
|
||||
private reselectSource() {
|
||||
analytics.logEvent('Reselect image', {
|
||||
previousImage: selectionState.getImage(),
|
||||
});
|
||||
|
||||
selectionState.deselectImage();
|
||||
}
|
||||
|
||||
private selectSource(
|
||||
selected: string | DrivelistDrive,
|
||||
SourceType: Source,
|
||||
auth?: Authentication,
|
||||
): { promise: Promise<void>; cancel: () => void } {
|
||||
let cancelled = false;
|
||||
return {
|
||||
cancel: () => {
|
||||
cancelled = true;
|
||||
},
|
||||
promise: (async () => {
|
||||
const sourcePath = isString(selected) ? selected : selected.device;
|
||||
let metadata: SourceMetadata | undefined;
|
||||
if (isString(selected)) {
|
||||
if (
|
||||
SourceType === 'Http' &&
|
||||
!isURL(this.normalizeImagePath(selected))
|
||||
) {
|
||||
this.handleError(
|
||||
i18next.t('source.unsupportedProtocol'),
|
||||
selected,
|
||||
messages.error.unsupportedProtocol(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (supportedFormats.looksLikeWindowsImage(selected)) {
|
||||
analytics.logEvent('Possibly Windows image', { image: selected });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.looksLikeWindowsImage(),
|
||||
title: i18next.t('source.windowsImage'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// this will send an event down the ipcMain asking for metadata
|
||||
// we'll get the response through an event
|
||||
|
||||
metadata = await requestMetadata({ selected, SourceType, auth });
|
||||
|
||||
if (!metadata?.hasMBR && this.state.warning === null) {
|
||||
analytics.logEvent('Missing partition table', { metadata });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.missingPartitionTable(),
|
||||
title: i18next.t('source.partitionTable'),
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.handleError(
|
||||
i18next.t('source.errorOpen'),
|
||||
sourcePath,
|
||||
messages.error.openSource(sourcePath, error.message),
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (selected.partitionTableType === null) {
|
||||
analytics.logEvent('Missing partition table', { selected });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.driveMissingPartitionTable(),
|
||||
title: i18next.t('source.partitionTable'),
|
||||
},
|
||||
});
|
||||
}
|
||||
metadata = {
|
||||
path: selected.device,
|
||||
displayName: selected.displayName,
|
||||
description: selected.displayName,
|
||||
size: selected.size as SourceMetadata['size'],
|
||||
SourceType: 'BlockDevice',
|
||||
drive: selected,
|
||||
};
|
||||
}
|
||||
|
||||
if (metadata !== undefined) {
|
||||
metadata.auth = auth;
|
||||
metadata.SourceType = SourceType;
|
||||
selectionState.selectSource(metadata);
|
||||
analytics.logEvent('Select image', {
|
||||
// An easy way so we can quickly identify if we're making use of
|
||||
// certain features without printing pages of text to DevTools.
|
||||
image: {
|
||||
...metadata,
|
||||
logo: Boolean(metadata.logo),
|
||||
blockMap: Boolean(metadata.blockMap),
|
||||
},
|
||||
});
|
||||
}
|
||||
})(),
|
||||
};
|
||||
}
|
||||
|
||||
private handleError(
|
||||
title: string,
|
||||
sourcePath: string,
|
||||
description: string,
|
||||
error?: Error,
|
||||
) {
|
||||
const imageError = errors.createUserError({
|
||||
title,
|
||||
description,
|
||||
});
|
||||
osDialog.showError(imageError);
|
||||
if (error) {
|
||||
analytics.logException(error);
|
||||
return;
|
||||
}
|
||||
analytics.logEvent(title, { path: sourcePath });
|
||||
}
|
||||
|
||||
private async openImageSelector() {
|
||||
analytics.logEvent('Open image selector');
|
||||
this.setState({ imageSelectorOpen: true });
|
||||
|
||||
try {
|
||||
const imagePath = await osDialog.selectImage();
|
||||
// Avoid analytics and selection state changes
|
||||
// if no file was resolved from the dialog.
|
||||
if (!imagePath) {
|
||||
analytics.logEvent('Image selector closed');
|
||||
return;
|
||||
}
|
||||
await this.selectSource(imagePath, 'File').promise;
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
} finally {
|
||||
this.setState({ imageSelectorOpen: false });
|
||||
}
|
||||
}
|
||||
|
||||
private async onDrop(event: React.DragEvent<HTMLDivElement>) {
|
||||
const [file] = event.dataTransfer.files;
|
||||
if (file) {
|
||||
await this.selectSource(file.path, 'File').promise;
|
||||
}
|
||||
}
|
||||
|
||||
private openURLSelector() {
|
||||
analytics.logEvent('Open image URL selector');
|
||||
|
||||
this.setState({
|
||||
showURLSelector: true,
|
||||
});
|
||||
}
|
||||
|
||||
private openDriveSelector() {
|
||||
analytics.logEvent('Open drive selector');
|
||||
|
||||
this.setState({
|
||||
showDriveSelector: true,
|
||||
});
|
||||
}
|
||||
|
||||
private onDragOver(event: React.DragEvent<HTMLDivElement>) {
|
||||
// Needed to get onDrop events on div elements
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
private onDragEnter(event: React.DragEvent<HTMLDivElement>) {
|
||||
// Needed to get onDrop events on div elements
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
private showSelectedImageDetails() {
|
||||
analytics.logEvent('Show selected image tooltip', {
|
||||
imagePath: selectionState.getImage()?.path,
|
||||
});
|
||||
|
||||
this.setState({
|
||||
showImageDetails: true,
|
||||
});
|
||||
}
|
||||
|
||||
private setDefaultFlowActive(defaultFlowActive: boolean) {
|
||||
this.setState({ defaultFlowActive });
|
||||
}
|
||||
|
||||
private closeModal() {
|
||||
this.setState({
|
||||
showDriveSelector: false,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO add a visual change when dragging a file over the selector
|
||||
public render() {
|
||||
const { flashing } = this.props;
|
||||
const {
|
||||
showImageDetails,
|
||||
showURLSelector,
|
||||
showDriveSelector,
|
||||
imageLoading,
|
||||
} = this.state;
|
||||
const selectionImage = selectionState.getImage();
|
||||
let image: SourceMetadata | DrivelistDrive =
|
||||
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
|
||||
|
||||
image = image.drive ?? image;
|
||||
|
||||
let cancelURLSelection = () => {
|
||||
// noop
|
||||
};
|
||||
image.name = image.description || image.name;
|
||||
const imagePath = image.path || image.displayName || '';
|
||||
const imageBasename = path.basename(imagePath);
|
||||
const imageName = image.name || '';
|
||||
const imageSize = image.size;
|
||||
const imageLogo = image.logo || '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
onDrop={(evt: React.DragEvent<HTMLDivElement>) => this.onDrop(evt)}
|
||||
onDragEnter={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||
this.onDragEnter(evt)
|
||||
}
|
||||
onDragOver={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||
this.onDragOver(evt)
|
||||
}
|
||||
>
|
||||
<SVGIcon
|
||||
contents={imageLogo}
|
||||
fallback={ImageSvg}
|
||||
style={{
|
||||
marginBottom: 30,
|
||||
}}
|
||||
/>
|
||||
|
||||
{selectionImage !== undefined || imageLoading ? (
|
||||
<>
|
||||
<StepNameButton
|
||||
plain
|
||||
onClick={() => this.showSelectedImageDetails()}
|
||||
tooltip={imageName || imageBasename}
|
||||
>
|
||||
<Spinner show={imageLoading}>
|
||||
{middleEllipsis(imageName || imageBasename, 20)}
|
||||
</Spinner>
|
||||
</StepNameButton>
|
||||
{!flashing && !imageLoading && (
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={() => this.reselectSource()}
|
||||
>
|
||||
{i18next.t('cancel')}
|
||||
</ChangeButton>
|
||||
)}
|
||||
{!isNil(imageSize) && !imageLoading && (
|
||||
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FlowSelector
|
||||
disabled={this.state.imageSelectorOpen}
|
||||
primary={this.state.defaultFlowActive}
|
||||
key="Flash from file"
|
||||
flow={{
|
||||
onClick: () => this.openImageSelector(),
|
||||
label: i18next.t('source.fromFile'),
|
||||
icon: <FileSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||
/>
|
||||
<FlowSelector
|
||||
key="Flash from URL"
|
||||
flow={{
|
||||
onClick: () => this.openURLSelector(),
|
||||
label: i18next.t('source.fromURL'),
|
||||
icon: <LinkSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||
/>
|
||||
<FlowSelector
|
||||
key="Clone drive"
|
||||
flow={{
|
||||
onClick: () => this.openDriveSelector(),
|
||||
label: i18next.t('source.clone'),
|
||||
icon: <CopySvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{this.state.warning != null && (
|
||||
<SmallModal
|
||||
style={{
|
||||
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
titleElement={
|
||||
<span>
|
||||
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
|
||||
<span>{this.state.warning.title}</span>
|
||||
</span>
|
||||
}
|
||||
action={i18next.t('continue')}
|
||||
cancel={() => {
|
||||
this.setState({ warning: null });
|
||||
this.reselectSource();
|
||||
}}
|
||||
done={() => {
|
||||
this.setState({ warning: null });
|
||||
}}
|
||||
primaryButtonProps={{ warning: true, primary: false }}
|
||||
>
|
||||
<ModalText
|
||||
dangerouslySetInnerHTML={{ __html: this.state.warning.message }}
|
||||
/>
|
||||
</SmallModal>
|
||||
)}
|
||||
|
||||
{showImageDetails && (
|
||||
<SmallModal
|
||||
title={i18next.t('source.image')}
|
||||
done={() => {
|
||||
this.setState({ showImageDetails: false });
|
||||
}}
|
||||
>
|
||||
<Txt.p>
|
||||
<Txt.span bold>{i18next.t('source.name')}</Txt.span>
|
||||
<Txt.span>{imageName || imageBasename}</Txt.span>
|
||||
</Txt.p>
|
||||
<Txt.p>
|
||||
<Txt.span bold>{i18next.t('source.path')}</Txt.span>
|
||||
<Txt.span>{imagePath}</Txt.span>
|
||||
</Txt.p>
|
||||
</SmallModal>
|
||||
)}
|
||||
|
||||
{showURLSelector && (
|
||||
<URLSelector
|
||||
cancel={() => {
|
||||
cancelURLSelection();
|
||||
this.setState({
|
||||
showURLSelector: false,
|
||||
});
|
||||
}}
|
||||
done={async (imageURL: string, auth?: Authentication) => {
|
||||
// Avoid analytics and selection state changes
|
||||
// if no file was resolved from the dialog.
|
||||
if (!imageURL) {
|
||||
analytics.logEvent('URL selector closed');
|
||||
} else {
|
||||
let promise;
|
||||
({ promise, cancel: cancelURLSelection } = this.selectSource(
|
||||
imageURL,
|
||||
'Http',
|
||||
auth,
|
||||
));
|
||||
await promise;
|
||||
}
|
||||
this.setState({
|
||||
showURLSelector: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDriveSelector && (
|
||||
<DriveSelector
|
||||
write={false}
|
||||
multipleSelection={false}
|
||||
titleLabel={i18next.t('source.selectSource')}
|
||||
emptyListLabel={i18next.t('source.plugSource')}
|
||||
emptyListIcon={<SrcSvg width="40px" />}
|
||||
cancel={(originalList) => {
|
||||
if (originalList.length) {
|
||||
const originalSource = originalList[0];
|
||||
if (selectionImage?.drive?.device !== originalSource.device) {
|
||||
this.selectSource(originalSource, 'BlockDevice');
|
||||
}
|
||||
} else {
|
||||
selectionState.deselectImage();
|
||||
}
|
||||
this.closeModal();
|
||||
}}
|
||||
done={() => this.closeModal()}
|
||||
onSelect={(drive) => {
|
||||
if (drive) {
|
||||
if (
|
||||
selectionState.getImage()?.drive?.device === drive?.device
|
||||
) {
|
||||
return selectionState.deselectImage();
|
||||
}
|
||||
this.selectSource(drive, 'BlockDevice');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
74
lib/gui/app/components/svg-icon/svg-icon.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2018 balena.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
const domParser = new window.DOMParser();
|
||||
|
||||
const DEFAULT_SIZE = '40px';
|
||||
|
||||
/**
|
||||
* @summary Try to parse SVG contents and return it data encoded
|
||||
*
|
||||
*/
|
||||
function tryParseSVGContents(contents?: string): string | undefined {
|
||||
if (contents === undefined) {
|
||||
return;
|
||||
}
|
||||
const doc = domParser.parseFromString(contents, 'image/svg+xml');
|
||||
const parserError = doc.querySelector('parsererror');
|
||||
const svg = doc.querySelector('svg');
|
||||
if (!parserError && svg) {
|
||||
return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`;
|
||||
}
|
||||
}
|
||||
|
||||
interface SVGIconProps {
|
||||
// Optional string representing the SVG contents to be tried
|
||||
contents?: string;
|
||||
// Fallback SVG element to show if `contents` is invalid/undefined
|
||||
fallback: React.FunctionComponent<React.SVGProps<HTMLOrSVGElement>>;
|
||||
// SVG image width unit
|
||||
width?: string;
|
||||
// SVG image height unit
|
||||
height?: string;
|
||||
// Should the element visually appear grayed out and disabled?
|
||||
disabled?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary SVG element that takes file contents
|
||||
*/
|
||||
export class SVGIcon extends React.PureComponent<SVGIconProps> {
|
||||
public render() {
|
||||
const svgData = tryParseSVGContents(this.props.contents);
|
||||
const { width, height, style = {} } = this.props;
|
||||
style.width = width || DEFAULT_SIZE;
|
||||
style.height = height || DEFAULT_SIZE;
|
||||
if (svgData !== undefined) {
|
||||
return (
|
||||
<img
|
||||
className={this.props.disabled ? 'disabled' : ''}
|
||||
style={style}
|
||||
src={svgData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const { fallback: FallbackSVG } = this.props;
|
||||
return <FallbackSVG style={style} />;
|
||||
}
|
||||
}
|
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2019 balena.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import * as React from 'react';
|
||||
import { Flex, FlexProps, Txt } from 'rendition';
|
||||
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
DriveStatus,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility, warning } from '../../../../shared/messages';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { getImage, getSelectedDrives } from '../../models/selection-state';
|
||||
import {
|
||||
ChangeButton,
|
||||
DetailsText,
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
} from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
interface TargetSelectorProps {
|
||||
targets: any[];
|
||||
disabled: boolean;
|
||||
openDriveSelector: () => void;
|
||||
reselectDrive: () => void;
|
||||
flashing: boolean;
|
||||
show: boolean;
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
function getDriveWarning(status: DriveStatus) {
|
||||
switch (status.message) {
|
||||
case compatibility.containsImage():
|
||||
return warning.sourceDrive();
|
||||
case compatibility.largeDrive():
|
||||
return warning.largeDriveSize();
|
||||
case compatibility.system():
|
||||
return warning.systemDrive();
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const DriveCompatibilityWarning = ({
|
||||
warnings,
|
||||
...props
|
||||
}: {
|
||||
warnings: string[];
|
||||
} & FlexProps) => {
|
||||
const systemDrive = warnings.find(
|
||||
(message) => message === warning.systemDrive(),
|
||||
);
|
||||
return (
|
||||
<Flex tooltip={warnings.join(', ')} {...props}>
|
||||
<ExclamationTriangleSvg
|
||||
fill={systemDrive ? '#fca321' : '#8f9297'}
|
||||
height="1em"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
const targets = getSelectedDrives();
|
||||
|
||||
if (targets.length === 1) {
|
||||
const target = targets[0];
|
||||
const warnings = getDriveImageCompatibilityStatuses(
|
||||
target,
|
||||
getImage(),
|
||||
true,
|
||||
).map(getDriveWarning);
|
||||
return (
|
||||
<>
|
||||
<StepNameButton plain tooltip={props.tooltip}>
|
||||
{warnings.length > 0 && (
|
||||
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||
)}
|
||||
{middleEllipsis(target.description, 20)}
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
<ChangeButton plain mb={14} onClick={props.reselectDrive}>
|
||||
{i18next.t('target.change')}
|
||||
</ChangeButton>
|
||||
)}
|
||||
{target.size != null && (
|
||||
<DetailsText>{prettyBytes(target.size)}</DetailsText>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (targets.length > 1) {
|
||||
const targetsTemplate = [];
|
||||
for (const target of targets) {
|
||||
const warnings = getDriveImageCompatibilityStatuses(
|
||||
target,
|
||||
getImage(),
|
||||
true,
|
||||
).map(getDriveWarning);
|
||||
targetsTemplate.push(
|
||||
<DetailsText
|
||||
key={target.device}
|
||||
tooltip={`${target.description} ${target.displayName} ${
|
||||
target.size != null ? prettyBytes(target.size) : ''
|
||||
}`}
|
||||
px={21}
|
||||
>
|
||||
{warnings.length > 0 ? (
|
||||
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||
) : null}
|
||||
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
|
||||
{target.size != null && <Txt>{prettyBytes(target.size)}</Txt>}
|
||||
</DetailsText>,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<StepNameButton plain tooltip={props.tooltip}>
|
||||
{targets.length} {i18next.t('target.targets')}
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
<ChangeButton plain onClick={props.reselectDrive} mb={14}>
|
||||
{i18next.t('target.change')}
|
||||
</ChangeButton>
|
||||
)}
|
||||
{targetsTemplate}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StepButton
|
||||
primary
|
||||
tabIndex={targets.length > 0 ? -1 : 2}
|
||||
disabled={props.disabled}
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
||||
{i18next.t('target.selectTarget')}
|
||||
</StepButton>
|
||||
);
|
||||
}
|
194
lib/gui/app/components/target-selector/target-selector.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
|
||||
import {
|
||||
DriveSelector,
|
||||
DriveSelectorProps,
|
||||
} from '../drive-selector/drive-selector';
|
||||
import {
|
||||
isDriveSelected,
|
||||
getImage,
|
||||
getSelectedDrives,
|
||||
deselectDrive,
|
||||
selectDrive,
|
||||
deselectAllDrives,
|
||||
} from '../../models/selection-state';
|
||||
import { observe } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { TargetSelectorButton } from './target-selector-button';
|
||||
|
||||
import TgtSvg from '../../../assets/tgt.svg';
|
||||
import DriveSvg from '../../../assets/drive.svg';
|
||||
import { warning } from '../../../../shared/messages';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
export const getDriveListLabel = () => {
|
||||
return getSelectedDrives()
|
||||
.map((drive: any) => {
|
||||
return `${drive.description} (${drive.displayName})`;
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
const getDriveSelectionStateSlice = () => ({
|
||||
driveListLabel: getDriveListLabel(),
|
||||
targets: getSelectedDrives(),
|
||||
image: getImage(),
|
||||
});
|
||||
|
||||
export const TargetSelectorModal = (
|
||||
props: Omit<
|
||||
DriveSelectorProps,
|
||||
'titleLabel' | 'emptyListLabel' | 'multipleSelection' | 'emptyListIcon'
|
||||
>,
|
||||
) => (
|
||||
<DriveSelector
|
||||
multipleSelection={true}
|
||||
titleLabel={i18next.t('target.selectTarget')}
|
||||
emptyListLabel={i18next.t('target.plugTarget')}
|
||||
emptyListIcon={<TgtSvg width="40px" />}
|
||||
showWarnings={true}
|
||||
selectedList={getSelectedDrives()}
|
||||
updateSelectedList={getSelectedDrives}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const selectAllTargets = (modalTargets: DrivelistDrive[]) => {
|
||||
const selectedDrivesFromState = getSelectedDrives();
|
||||
const deselected = selectedDrivesFromState.filter(
|
||||
(drive) =>
|
||||
!modalTargets.find((modalTarget) => modalTarget.device === drive.device),
|
||||
);
|
||||
// deselect drives
|
||||
deselected.forEach((drive) => {
|
||||
analytics.logEvent('Toggle drive', {
|
||||
drive,
|
||||
previouslySelected: true,
|
||||
});
|
||||
deselectDrive(drive.device);
|
||||
});
|
||||
// select drives
|
||||
modalTargets.forEach((drive) => {
|
||||
// Don't send events for drives that were already selected
|
||||
if (!isDriveSelected(drive.device)) {
|
||||
analytics.logEvent('Toggle drive', {
|
||||
drive,
|
||||
previouslySelected: false,
|
||||
});
|
||||
}
|
||||
selectDrive(drive.device);
|
||||
});
|
||||
};
|
||||
|
||||
interface TargetSelectorProps {
|
||||
disabled: boolean;
|
||||
hasDrive: boolean;
|
||||
flashing: boolean;
|
||||
}
|
||||
|
||||
export const TargetSelector = ({
|
||||
disabled,
|
||||
hasDrive,
|
||||
flashing,
|
||||
}: TargetSelectorProps) => {
|
||||
// TODO: inject these from redux-connector
|
||||
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
|
||||
getDriveSelectionStateSlice(),
|
||||
);
|
||||
const [showTargetSelectorModal, setShowTargetSelectorModal] =
|
||||
React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
return observe(() => {
|
||||
setStateSlice(getDriveSelectionStateSlice());
|
||||
});
|
||||
}, []);
|
||||
|
||||
const hasSystemDrives = targets.some((target) => target.isSystem);
|
||||
return (
|
||||
<Flex flexDirection="column" alignItems="center">
|
||||
<DriveSvg
|
||||
className={disabled ? 'disabled' : ''}
|
||||
width="40px"
|
||||
style={{
|
||||
marginBottom: 30,
|
||||
}}
|
||||
/>
|
||||
|
||||
<TargetSelectorButton
|
||||
disabled={disabled}
|
||||
show={!hasDrive}
|
||||
tooltip={driveListLabel}
|
||||
openDriveSelector={() => {
|
||||
setShowTargetSelectorModal(true);
|
||||
}}
|
||||
reselectDrive={() => {
|
||||
analytics.logEvent('Reselect drive');
|
||||
setShowTargetSelectorModal(true);
|
||||
}}
|
||||
flashing={flashing}
|
||||
targets={targets}
|
||||
/>
|
||||
|
||||
{hasSystemDrives ? (
|
||||
<Txt
|
||||
color="#fca321"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '25px',
|
||||
}}
|
||||
>
|
||||
Warning: {warning.systemDrive()}
|
||||
</Txt>
|
||||
) : null}
|
||||
|
||||
{showTargetSelectorModal && (
|
||||
<TargetSelectorModal
|
||||
write={true}
|
||||
cancel={(originalList) => {
|
||||
if (originalList.length) {
|
||||
selectAllTargets(originalList);
|
||||
} else {
|
||||
deselectAllDrives();
|
||||
}
|
||||
setShowTargetSelectorModal(false);
|
||||
}}
|
||||
done={(modalTargets) => {
|
||||
if (modalTargets.length === 0) {
|
||||
deselectAllDrives();
|
||||
}
|
||||
setShowTargetSelectorModal(false);
|
||||
}}
|
||||
onSelect={(drive) => {
|
||||
if (
|
||||
getSelectedDrives().find(
|
||||
(selectedDrive) => selectedDrive.device === drive.device,
|
||||
)
|
||||
) {
|
||||
return deselectDrive(drive.device);
|
||||
}
|
||||
selectDrive(drive.device);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
BIN
lib/gui/app/css/fonts/SourceSansPro-Regular.ttf
Normal file
BIN
lib/gui/app/css/fonts/SourceSansPro-SemiBold.ttf
Normal file
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,36 +14,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* Prevent text selection */
|
||||
body {
|
||||
-webkit-user-select: none;
|
||||
@font-face {
|
||||
font-family: "SourceSansPro";
|
||||
src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
/* Allow window to be dragged from anywhere */
|
||||
* {
|
||||
-webkit-app-region: drag;
|
||||
@font-face {
|
||||
font-family: "SourceSansPro";
|
||||
src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
button,
|
||||
a,
|
||||
input {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* Prevent WebView bounce effect in OS X */
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
|
||||
/* Prevent white flash when running application */
|
||||
background-color: #4d5057;
|
||||
|
||||
/* Prevent WebView bounce effect in OS X */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Prevent text selection */
|
||||
body {
|
||||
overflow: hidden;
|
||||
-webkit-user-select: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
@@ -51,11 +51,16 @@ body {
|
||||
a:focus,
|
||||
input:focus,
|
||||
button:focus,
|
||||
[tabindex]:focus {
|
||||
[tabindex]:focus,
|
||||
input[type="checkbox"] + div {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Titles don't have margins on desktop apps */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0;
|
||||
.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
#rendition-tooltip-root > div {
|
||||
font-family: "SourceSansPro", sans-serif;
|
||||
}
|
44
lib/gui/app/i18n.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as i18next from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import zh_CN_translation from './i18n/zh-CN';
|
||||
import zh_TW_translation from './i18n/zh-TW';
|
||||
import en_translation from './i18n/en';
|
||||
|
||||
export function langParser() {
|
||||
if (process.env.LANG !== undefined) {
|
||||
// Bypass mocha, where lang-detect don't works
|
||||
return 'en';
|
||||
}
|
||||
|
||||
const lang = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
|
||||
switch (lang.substr(0, 2)) {
|
||||
case 'zh':
|
||||
if (lang === 'zh-CN' || lang === 'zh-SG') {
|
||||
return 'zh-CN';
|
||||
} // Simplified Chinese
|
||||
else {
|
||||
return 'zh-TW';
|
||||
} // Traditional Chinese
|
||||
default:
|
||||
return lang.substr(0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
i18next.use(initReactI18next).init({
|
||||
lng: langParser(),
|
||||
fallbackLng: 'en',
|
||||
nonExplicitSupportedLngs: true,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
resources: {
|
||||
'zh-CN': zh_CN_translation,
|
||||
'zh-TW': zh_TW_translation,
|
||||
en: en_translation,
|
||||
},
|
||||
});
|
||||
|
||||
export const supportedLocales = ['en', 'zh'];
|
||||
|
||||
export default i18next;
|
23
lib/gui/app/i18n/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# i18n
|
||||
|
||||
## How it was done
|
||||
|
||||
Using the open-source lib [i18next](https://www.i18next.com/).
|
||||
|
||||
## How to add your own language
|
||||
|
||||
1. Go to `lib/gui/app/i18n` and add a file named `xx.ts` (use the codes mentioned
|
||||
in [the link](https://www.science.co.il/language/Locale-codes.php), and we support styles as `fr`, `de`, `es-ES`
|
||||
and `pt-BR`)
|
||||
.
|
||||
2. Copy the content from an existing translation and start to translate.
|
||||
3. Once done, go to `lib/gui/app/i18n.ts` and add a line of `import xx_translation from './i18n/xx'` after the
|
||||
already-added imports and add `xx: xx_translation` in the `resources` section of `i18next.init()` function.
|
||||
4. Now go to `lib/shared/catalina-sudo/` and copy the `sudo-askpass.osascript-en.js`, change it to
|
||||
be `sudo-askpass.osascript-xx.js` and edit
|
||||
the `'balenaEtcher needs privileged access in order to flash disks.\n\nType your password to allow this.'` line and
|
||||
those `Ok`s and `Cancel`s to your own language.
|
||||
5. If, your language has several variations when they are used in several countries/regions, such as `zh-CN` and `zh-TW`
|
||||
, or `pt-BR` and `pt-PT`, edit
|
||||
the `langParser()` in the `lib/gui/app/i18n.ts` file to meet your need.
|
||||
6. Make a commit, and then a pull request on GitHub.
|
162
lib/gui/app/i18n/en.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
const translation = {
|
||||
translation: {
|
||||
continue: 'Continue',
|
||||
ok: 'OK',
|
||||
cancel: 'Cancel',
|
||||
skip: 'Skip',
|
||||
sure: "Yes, I'm sure",
|
||||
warning: 'WARNING! ',
|
||||
attention: 'Attention',
|
||||
failed: 'Failed',
|
||||
completed: 'Completed',
|
||||
yesContinue: 'Yes, continue',
|
||||
reallyExit: 'Are you sure you want to close Etcher?',
|
||||
yesExit: 'Yes, quit',
|
||||
progress: {
|
||||
starting: 'Starting...',
|
||||
decompressing: 'Decompressing...',
|
||||
flashing: 'Flashing...',
|
||||
finishing: 'Finishing...',
|
||||
verifying: 'Validating...',
|
||||
failing: 'Failed',
|
||||
},
|
||||
message: {
|
||||
sizeNotRecommended: 'Not recommended',
|
||||
tooSmall: 'Too small',
|
||||
locked: 'Locked',
|
||||
system: 'System drive',
|
||||
containsImage: 'Source drive',
|
||||
largeDrive: 'Large drive',
|
||||
sourceLarger: 'The selected source is {{byte}} larger than this drive.',
|
||||
flashSucceed_one: 'Successful target',
|
||||
flashSucceed_other: 'Successful targets',
|
||||
flashFail_one: 'Failed target',
|
||||
flashFail_other: 'Failed targets',
|
||||
toDrive: 'to {{description}} ({{name}})',
|
||||
toTarget_one: 'to {{num}} target',
|
||||
toTarget_other: 'to {{num}} targets',
|
||||
andFailTarget_one: 'and failed to be flashed to {{num}} target',
|
||||
andFailTarget_other: 'and failed to be flashed to {{num}} targets',
|
||||
succeedTo: '{{name}} was successfully flashed {{target}}',
|
||||
exitWhileFlashing:
|
||||
'You are currently flashing a drive. Closing Etcher may leave your drive in an unusable state.',
|
||||
looksLikeWindowsImage:
|
||||
'It looks like you are trying to burn a Windows image.\n\nUnlike other images, Windows images require special processing to be made bootable. We suggest you use a tool specially designed for this purpose, such as <a href="https://rufus.akeo.ie">Rufus</a> (Windows), <a href="https://github.com/slacka/WoeUSB">WoeUSB</a> (Linux), or Boot Camp Assistant (macOS).',
|
||||
image: 'image',
|
||||
drive: 'drive',
|
||||
missingPartitionTable:
|
||||
'It looks like this is not a bootable {{type}}.\n\nThe {{type}} does not appear to contain a partition table, and might not be recognized or bootable by your device.',
|
||||
largeDriveSize:
|
||||
"This is a large drive! Make sure it doesn't contain files that you want to keep.",
|
||||
systemDrive:
|
||||
'Selecting your system drive is dangerous and will erase your drive!',
|
||||
sourceDrive: 'Contains the image you chose to flash',
|
||||
noSpace:
|
||||
'Not enough space on the drive. Please insert larger one and try again.',
|
||||
genericFlashError:
|
||||
'Something went wrong. If it is a compressed image, please check that the archive is not corrupted.\n{{error}}',
|
||||
validation:
|
||||
'The write has been completed successfully but Etcher detected potential corruption issues when reading the image back from the drive. \n\nPlease consider writing the image to a different drive.',
|
||||
openError:
|
||||
'Something went wrong while opening {{source}}.\n\nError: {{error}}',
|
||||
flashError: 'Something went wrong while writing {{image}} {{targets}}.',
|
||||
unplug:
|
||||
"Looks like Etcher lost access to the drive. Did it get unplugged accidentally?\n\nSometimes this error is caused by faulty readers that don't provide stable access to the drive.",
|
||||
cannotWrite:
|
||||
'Looks like Etcher is not able to write to this location of the drive. This error is usually caused by a faulty drive, reader, or port. \n\nPlease try again with another drive, reader, or port.',
|
||||
childWriterDied:
|
||||
'The writer process ended unexpectedly. Please try again, and contact the Etcher team if the problem persists.',
|
||||
badProtocol: 'Only http:// and https:// URLs are supported.',
|
||||
},
|
||||
target: {
|
||||
selectTarget: 'Select target',
|
||||
plugTarget: 'Plug a target drive',
|
||||
targets: 'Targets',
|
||||
change: 'Change',
|
||||
},
|
||||
source: {
|
||||
useSourceURL: 'Use Image URL',
|
||||
auth: 'Authentication',
|
||||
username: 'Enter username',
|
||||
password: 'Enter password',
|
||||
unsupportedProtocol: 'Unsupported protocol',
|
||||
windowsImage: 'Possible Windows image detected',
|
||||
partitionTable: 'Missing partition table',
|
||||
errorOpen: 'Error opening source',
|
||||
fromFile: 'Flash from file',
|
||||
fromURL: 'Flash from URL',
|
||||
clone: 'Clone drive',
|
||||
image: 'Image',
|
||||
name: 'Name: ',
|
||||
path: 'Path: ',
|
||||
selectSource: 'Select source',
|
||||
plugSource: 'Plug a source drive',
|
||||
osImages: 'OS Images',
|
||||
allFiles: 'All',
|
||||
enterValidURL: 'Enter a valid URL',
|
||||
},
|
||||
drives: {
|
||||
name: 'Name',
|
||||
size: 'Size',
|
||||
location: 'Location',
|
||||
find: '{{length}} found',
|
||||
select: 'Select {{select}}',
|
||||
showHidden: 'Show {{num}} hidden',
|
||||
systemDriveDanger:
|
||||
'Selecting your system drive is dangerous and will erase your drive!',
|
||||
openInBrowser: '`Etcher will open {{link}} in your browser`',
|
||||
changeTarget: 'Change target',
|
||||
largeDriveWarning: 'You are about to erase an unusually large drive',
|
||||
largeDriveWarningMsg:
|
||||
'Are you sure the selected drive is not a storage drive?',
|
||||
systemDriveWarning: "You are about to erase your computer's drives",
|
||||
systemDriveWarningMsg:
|
||||
'Are you sure you want to flash your system drive?',
|
||||
},
|
||||
flash: {
|
||||
another: 'Flash another',
|
||||
target: 'Target',
|
||||
location: 'Location',
|
||||
error: 'Error',
|
||||
flash: 'Flash',
|
||||
flashNow: 'Flash!',
|
||||
skip: 'Validation has been skipped',
|
||||
moreInfo: 'more info',
|
||||
speedTip:
|
||||
'The speed is calculated by dividing the image size by the flashing time.\nDisk images with ext partitions flash faster as we are able to skip unused parts.',
|
||||
speed: 'Effective speed: {{speed}} MB/s',
|
||||
speedShort: '{{speed}} MB/s',
|
||||
eta: 'ETA: {{eta}}',
|
||||
failedTarget: 'Failed targets',
|
||||
failedRetry: 'Retry failed targets',
|
||||
flashFailed: 'Flash Failed.',
|
||||
flashCompleted: 'Flash Completed!',
|
||||
},
|
||||
settings: {
|
||||
errorReporting:
|
||||
'Anonymously report errors and usage statistics to balena.io',
|
||||
autoUpdate: 'Auto-updates enabled',
|
||||
settings: 'Settings',
|
||||
systemInformation: 'System Information',
|
||||
trimExtPartitions:
|
||||
'Trim unallocated space on raw images (in ext-type partitions)',
|
||||
},
|
||||
menu: {
|
||||
edit: 'Edit',
|
||||
view: 'View',
|
||||
devTool: 'Toggle Developer Tools',
|
||||
window: 'Window',
|
||||
help: 'Help',
|
||||
pro: 'Etcher Pro',
|
||||
website: 'Etcher Website',
|
||||
issue: 'Report an issue',
|
||||
about: 'About Etcher',
|
||||
hide: 'Hide Etcher',
|
||||
hideOthers: 'Hide Others',
|
||||
unhide: 'Unhide All',
|
||||
quit: 'Quit Etcher',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default translation;
|
152
lib/gui/app/i18n/zh-CN.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
const translation = {
|
||||
translation: {
|
||||
ok: '好',
|
||||
cancel: '取消',
|
||||
continue: '继续',
|
||||
skip: '跳过',
|
||||
sure: '我确定',
|
||||
warning: '请注意!',
|
||||
attention: '请注意',
|
||||
failed: '失败',
|
||||
completed: '完毕',
|
||||
yesExit: '是的,可以退出',
|
||||
reallyExit: '真的要现在退出 Etcher 吗?',
|
||||
yesContinue: '是的,继续',
|
||||
progress: {
|
||||
starting: '正在启动……',
|
||||
decompressing: '正在解压……',
|
||||
flashing: '正在烧录……',
|
||||
finishing: '正在结束……',
|
||||
verifying: '正在验证……',
|
||||
failing: '失败……',
|
||||
},
|
||||
message: {
|
||||
sizeNotRecommended: '大小不推荐',
|
||||
tooSmall: '空间太小',
|
||||
locked: '被锁定',
|
||||
system: '系统盘',
|
||||
containsImage: '存放源镜像',
|
||||
largeDrive: '很大的磁盘',
|
||||
sourceLarger: '所选的镜像比目标盘大了 {{byte}} 比特。',
|
||||
flashSucceed_one: '烧录成功',
|
||||
flashSucceed_other: '烧录成功',
|
||||
flashFail_one: '烧录失败',
|
||||
flashFail_other: '烧录失败',
|
||||
toDrive: '到 {{description}} ({{name}})',
|
||||
toTarget_one: '到 {{num}} 个目标',
|
||||
toTarget_other: '到 {{num}} 个目标',
|
||||
andFailTarget_one: '并烧录失败了 {{num}} 个目标',
|
||||
andFailTarget_other: '并烧录失败了 {{num}} 个目标',
|
||||
succeedTo: '{{name}} 被成功烧录 {{target}}',
|
||||
exitWhileFlashing:
|
||||
'您当前正在刷机。 关闭 Etcher 可能会导致您的磁盘无法使用。',
|
||||
looksLikeWindowsImage:
|
||||
'看起来您正在尝试刻录 Windows 镜像。\n\n与其他镜像不同,Windows 镜像需要特殊处理才能使其可启动。 我们建议您使用专门为此目的设计的工具,例如 <a href="https://rufus.akeo.ie">Rufus</a> (Windows)、<a href="https://github. com/slacka/WoeUSB">WoeUSB</a> (Linux) 或 Boot Camp 助理 (macOS)。',
|
||||
image: '镜像',
|
||||
drive: '磁盘',
|
||||
missingPartitionTable:
|
||||
'看起来这不是一个可启动的{{type}}。\n\n这个{{type}}似乎不包含分区表,因此您的设备可能无法识别或无法正确启动。',
|
||||
largeDriveSize: '这是个很大的磁盘!请检查并确认它不包含对您很重要的信息',
|
||||
systemDrive: '选择系统盘很危险,因为这将会删除你的系统',
|
||||
sourceDrive: '源镜像位于这个分区中',
|
||||
noSpace: '磁盘空间不足。 请插入另一个较大的磁盘并重试。',
|
||||
genericFlashError:
|
||||
'出了点问题。如果源镜像曾被压缩过,请检查它是否已损坏。\n{{error}}',
|
||||
validation:
|
||||
'写入已成功完成,但 Etcher 在从磁盘读取镜像时检测到潜在的损坏问题。 \n\n请考虑将镜像写入其他磁盘。',
|
||||
openError: '打开 {{source}} 时出错。\n\n错误信息: {{error}}',
|
||||
flashError: '烧录 {{image}} {{targets}} 失败。',
|
||||
unplug:
|
||||
'看起来 Etcher 失去了对磁盘的连接。 它是不是被意外拔掉了?\n\n有时这个错误是因为读卡器出了故障。',
|
||||
cannotWrite:
|
||||
'看起来 Etcher 无法写入磁盘的这个位置。 此错误通常是由故障的磁盘、读取器或端口引起的。 \n\n请使用其他磁盘、读卡器或端口重试。',
|
||||
childWriterDied:
|
||||
'写入进程意外崩溃。请再试一次,如果问题仍然存在,请联系 Etcher 团队。',
|
||||
badProtocol: '仅支持 http:// 和 https:// 开头的网址。',
|
||||
},
|
||||
target: {
|
||||
selectTarget: '选择目标磁盘',
|
||||
plugTarget: '请插入目标磁盘',
|
||||
targets: '个目标',
|
||||
change: '更改',
|
||||
},
|
||||
menu: {
|
||||
edit: '编辑',
|
||||
view: '视图',
|
||||
devTool: '打开开发者工具',
|
||||
window: '窗口',
|
||||
help: '帮助',
|
||||
pro: 'Etcher 专业版',
|
||||
website: 'Etcher 的官网',
|
||||
issue: '提交一个 issue',
|
||||
about: '关于 Etcher',
|
||||
hide: '隐藏 Etcher',
|
||||
hideOthers: '隐藏其它窗口',
|
||||
unhide: '取消隐藏',
|
||||
quit: '退出 Etcher',
|
||||
},
|
||||
source: {
|
||||
useSourceURL: '使用镜像网络地址',
|
||||
auth: '验证',
|
||||
username: '输入用户名',
|
||||
password: '输入密码',
|
||||
unsupportedProtocol: '不支持的协议',
|
||||
windowsImage: '这可能是 Windows 系统镜像',
|
||||
partitionTable: '找不到分区表',
|
||||
errorOpen: '打开源镜像时出错',
|
||||
fromFile: '从文件烧录',
|
||||
fromURL: '从在线地址烧录',
|
||||
clone: '克隆磁盘',
|
||||
image: '镜像信息',
|
||||
name: '名称:',
|
||||
path: '路径:',
|
||||
selectSource: '选择源',
|
||||
plugSource: '请插入源磁盘',
|
||||
osImages: '系统镜像格式',
|
||||
allFiles: '任何文件格式',
|
||||
enterValidURL: '请输入一个正确的地址',
|
||||
},
|
||||
drives: {
|
||||
name: '名称',
|
||||
size: '大小',
|
||||
location: '位置',
|
||||
find: '找到 {{length}} 个',
|
||||
select: '选定 {{select}}',
|
||||
showHidden: '显示 {{num}} 个隐藏的磁盘',
|
||||
systemDriveDanger: '选择系统盘很危险,因为这将会删除你的系统!',
|
||||
openInBrowser: 'Etcher 会在浏览器中打开 {{link}}',
|
||||
changeTarget: '改变目标',
|
||||
largeDriveWarning: '您即将擦除一个非常大的磁盘',
|
||||
largeDriveWarningMsg: '您确定所选磁盘不是存储磁盘吗?',
|
||||
systemDriveWarning: '您将要擦除系统盘',
|
||||
systemDriveWarningMsg: '您确定要烧录到系统盘吗?',
|
||||
},
|
||||
flash: {
|
||||
another: '烧录另一目标',
|
||||
target: '目标',
|
||||
location: '位置',
|
||||
error: '错误',
|
||||
flash: '烧录',
|
||||
flashNow: '现在烧录!',
|
||||
skip: '跳过了验证',
|
||||
moreInfo: '更多信息',
|
||||
speedTip:
|
||||
'通过将镜像大小除以烧录时间来计算速度。\n由于我们能够跳过未使用的部分,因此具有EXT分区的磁盘镜像烧录速度更快。',
|
||||
speed: '速度:{{speed}} MB/秒',
|
||||
speedShort: '{{speed}} MB/秒',
|
||||
eta: '预计还需要:{{eta}}',
|
||||
failedTarget: '失败的烧录目标',
|
||||
failedRetry: '重试烧录失败目标',
|
||||
flashFailed: '烧录失败。',
|
||||
flashCompleted: '烧录成功!',
|
||||
},
|
||||
settings: {
|
||||
errorReporting: '匿名地向 balena.io 报告运行错误和使用统计',
|
||||
autoUpdate: '自动更新',
|
||||
settings: '软件设置',
|
||||
systemInformation: '系统信息',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default translation;
|