Compare commits
4292 Commits
xo-web/v5.
...
xo-server-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
089cffcca1 | ||
|
|
88df5a8337 | ||
|
|
794c1cf89b | ||
|
|
9a5eea6e78 | ||
|
|
40568cd61f | ||
|
|
358e1441cc | ||
|
|
be930e127e | ||
|
|
3656e83df5 | ||
|
|
abbb0450f8 | ||
|
|
8e4beeb00f | ||
|
|
05d10ef985 | ||
|
|
989d27154d | ||
|
|
ec9957bd86 | ||
|
|
dc8a7c46e0 | ||
|
|
9ee2d8e0c2 | ||
|
|
6c62d6840a | ||
|
|
2a2135ac71 | ||
|
|
efaad2efb2 | ||
|
|
3b244c24d7 | ||
|
|
915052d5f6 | ||
|
|
05c6c7830d | ||
|
|
0217c51559 | ||
|
|
0c514198bb | ||
|
|
0e68834b4c | ||
|
|
ee99ef6264 | ||
|
|
bebb9bf0df | ||
|
|
4830ac9623 | ||
|
|
58b1d0fba8 | ||
|
|
cc4e69e631 | ||
|
|
e14fda6e8a | ||
|
|
ec48b77af3 | ||
|
|
c7d6a19864 | ||
|
|
7d714c8ce4 | ||
|
|
f70989c3a2 | ||
|
|
70490988b0 | ||
|
|
d0795fdded | ||
|
|
1c736e9910 | ||
|
|
62979d5c22 | ||
|
|
ec8a4da73c | ||
|
|
dea1bfee01 | ||
|
|
c18b82504a | ||
|
|
ed5460273f | ||
|
|
b91f8b21b9 | ||
|
|
5cea18e577 | ||
|
|
148eaa6a72 | ||
|
|
80794211af | ||
|
|
75dcbae417 | ||
|
|
b19682b3c5 | ||
|
|
dd3b97cae2 | ||
|
|
79891235f3 | ||
|
|
1e2f72ab6b | ||
|
|
66d02e3808 | ||
|
|
275e1f8f4c | ||
|
|
84dbbb0fbb | ||
|
|
a36ef5209c | ||
|
|
3497889302 | ||
|
|
0a2f6b4ce7 | ||
|
|
f8be44d746 | ||
|
|
379253c5ae | ||
|
|
aed1ba474c | ||
|
|
bc72e67442 | ||
|
|
26c965faa9 | ||
|
|
b3a3965ed2 | ||
|
|
7f88b46f4c | ||
|
|
dd60d82d3d | ||
|
|
4eeb995340 | ||
|
|
1d29348e30 | ||
|
|
a24db3f896 | ||
|
|
cffac27d0a | ||
|
|
b207cbdd77 | ||
|
|
10baecefb9 | ||
|
|
42620323a9 | ||
|
|
4d91006994 | ||
|
|
a81f0b9a93 | ||
|
|
2cee413ae1 | ||
|
|
53099eacc8 | ||
|
|
b628c5c07e | ||
|
|
12889b6a09 | ||
|
|
0c23ca5b66 | ||
|
|
d732ee3ade | ||
|
|
65cb0bc4cf | ||
|
|
1ba68a94e3 | ||
|
|
084430451a | ||
|
|
458a4d4efe | ||
|
|
62eeab2a74 | ||
|
|
790b43910d | ||
|
|
ba65461c4d | ||
|
|
5bd468791f | ||
|
|
37f71bb36c | ||
|
|
2ed4b7ad3f | ||
|
|
7eb970f22a | ||
|
|
13db4a8411 | ||
|
|
49a7a89bbf | ||
|
|
0af8a60c1c | ||
|
|
e1650b376c | ||
|
|
873b40cc70 | ||
|
|
d45265b180 | ||
|
|
ff50b2848e | ||
|
|
d67fae22ab | ||
|
|
d809002558 | ||
|
|
5c30559d15 | ||
|
|
cbb5b011e1 | ||
|
|
f5bff408a8 | ||
|
|
d7cfe4d3dc | ||
|
|
7be8f38c6b | ||
|
|
08a7e605ce | ||
|
|
4b57db5893 | ||
|
|
8b1ae3f3c9 | ||
|
|
77d35a5928 | ||
|
|
323d409e6c | ||
|
|
9f2f2b7b69 | ||
|
|
b44fa7beca | ||
|
|
6d4e310b8e | ||
|
|
6726530229 | ||
|
|
8351352541 | ||
|
|
3f9e8d79ea | ||
|
|
685f2328bd | ||
|
|
746567a8a7 | ||
|
|
c116c41c42 | ||
|
|
3768a7de37 | ||
|
|
11ef0ee54f | ||
|
|
33ae531e3a | ||
|
|
8cc9924751 | ||
|
|
c329ab863b | ||
|
|
41820ea316 | ||
|
|
bf00f80716 | ||
|
|
9baf0c74e4 | ||
|
|
b59ccdf26f | ||
|
|
9cae978923 | ||
|
|
311d914b96 | ||
|
|
592cb4ef9e | ||
|
|
ec2db7f2d0 | ||
|
|
71eab7ba9b | ||
|
|
5e07171d60 | ||
|
|
3f73e3d964 | ||
|
|
0ebe78b4a2 | ||
|
|
61c3379298 | ||
|
|
44866f3316 | ||
|
|
4bb8ce8779 | ||
|
|
58eb6a8b5f | ||
|
|
52f6a79e01 | ||
|
|
129f79d44b | ||
|
|
385c3eb563 | ||
|
|
e56be51b45 | ||
|
|
24ae65b254 | ||
|
|
d5dffbacbd | ||
|
|
c6ae969a82 | ||
|
|
005a9fdc01 | ||
|
|
f505d4d911 | ||
|
|
8ada6b121e | ||
|
|
b9a87efb0d | ||
|
|
89485a82d2 | ||
|
|
451f87c6b4 | ||
|
|
c3cb5a3221 | ||
|
|
458609ed2e | ||
|
|
fcec8113f3 | ||
|
|
ebbd882ee4 | ||
|
|
0506e19a66 | ||
|
|
ecc62e4f54 | ||
|
|
2b95eb4e4d | ||
|
|
bcde9e0f74 | ||
|
|
114501ebc7 | ||
|
|
ebab7c0867 | ||
|
|
0e2270fb6e | ||
|
|
593493ec0c | ||
|
|
d92898a806 | ||
|
|
7890e46551 | ||
|
|
ef942a6209 | ||
|
|
fdde916388 | ||
|
|
31314d201b | ||
|
|
a29a949c51 | ||
|
|
cc1ce8c5f8 | ||
|
|
a21bf4ebe5 | ||
|
|
3d0420dbd9 | ||
|
|
04c74dd30f | ||
|
|
2f256291ae | ||
|
|
bcb66a4145 | ||
|
|
2d9368062e | ||
|
|
b110bacf61 | ||
|
|
78afdc0af5 | ||
|
|
ad6cd7985a | ||
|
|
a61661776d | ||
|
|
1a9ebddcab | ||
|
|
7ab907a854 | ||
|
|
68a34f7cdb | ||
|
|
da4ff3082d | ||
|
|
9c05a59b5f | ||
|
|
6780146505 | ||
|
|
2758833fc6 | ||
|
|
2786d7ec46 | ||
|
|
945a2006c9 | ||
|
|
b9e574e32f | ||
|
|
34f1ef1680 | ||
|
|
4ac4310bc1 | ||
|
|
a10997ca66 | ||
|
|
0e52a4c7dc | ||
|
|
a4b3e22c2b | ||
|
|
441bd7c754 | ||
|
|
ddbd32d1cb | ||
|
|
a5b0cbeaea | ||
|
|
c6f3b2b1ce | ||
|
|
3d869d9fa1 | ||
|
|
7a5229741f | ||
|
|
78e0c2d8fa | ||
|
|
5928984069 | ||
|
|
61a472f108 | ||
|
|
e45f78ea20 | ||
|
|
b3ae9d88eb | ||
|
|
f7f26537be | ||
|
|
96848fc6d4 | ||
|
|
51e6f0c79f | ||
|
|
4f94ad40b7 | ||
|
|
43e1eb9939 | ||
|
|
1f6d7de861 | ||
|
|
bd623c2daf | ||
|
|
40c71c2102 | ||
|
|
72a1580eff | ||
|
|
9e2404a0d7 | ||
|
|
7dd84d1518 | ||
|
|
d800db5d09 | ||
|
|
2714ccff38 | ||
|
|
1d493e411b | ||
|
|
2a0c222f2d | ||
|
|
641d68de0e | ||
|
|
2dd0fd660b | ||
|
|
bb5441c7bc | ||
|
|
eeea9e662b | ||
|
|
8d4874e240 | ||
|
|
a8ba4a1a8e | ||
|
|
0c027247ec | ||
|
|
164cb39c1b | ||
|
|
52503de645 | ||
|
|
83b8b5de61 | ||
|
|
3e326c4e62 | ||
|
|
a6b0690416 | ||
|
|
dcd007c5c7 | ||
|
|
eb090e4874 | ||
|
|
4b716584f7 | ||
|
|
4bc348f39f | ||
|
|
9c75992fe4 | ||
|
|
4bb2702ac5 | ||
|
|
ea8133cb41 | ||
|
|
fc40c7b03d | ||
|
|
7fe5b66fdb | ||
|
|
0f1d052493 | ||
|
|
56a182f795 | ||
|
|
e8da1b943b | ||
|
|
3913b0eba1 | ||
|
|
7990e45095 | ||
|
|
a7068ec166 | ||
|
|
55b35ac0cf | ||
|
|
a251f8ca75 | ||
|
|
172ce2c7a1 | ||
|
|
3cef668a75 | ||
|
|
e6deb29070 | ||
|
|
51609d45a2 | ||
|
|
5cb6dc6d92 | ||
|
|
c5174a61b7 | ||
|
|
93e987982c | ||
|
|
fc421428fd | ||
|
|
7400bd657a | ||
|
|
da62cba3f8 | ||
|
|
461cc7e547 | ||
|
|
b898ed4785 | ||
|
|
149530e73f | ||
|
|
7e627c953e | ||
|
|
bc86984f19 | ||
|
|
e40f3acdd4 | ||
|
|
63d93224e0 | ||
|
|
c87356c319 | ||
|
|
74f4a83aea | ||
|
|
e67038a04d | ||
|
|
1fa73b57a2 | ||
|
|
73c746fdd3 | ||
|
|
ab1413b741 | ||
|
|
c087eaf229 | ||
|
|
8b9f9ffa3e | ||
|
|
a83fa90d87 | ||
|
|
505f06c1d8 | ||
|
|
2ac1093543 | ||
|
|
b3d8ce2041 | ||
|
|
b47789bf82 | ||
|
|
0a5e1a9bce | ||
|
|
f333679319 | ||
|
|
20d3faa306 | ||
|
|
cf11ed0830 | ||
|
|
acd390ac42 | ||
|
|
8a2fbe3ab5 | ||
|
|
7a6e7ec153 | ||
|
|
7d90346c91 | ||
|
|
abb5193ced | ||
|
|
52e845834e | ||
|
|
c1c17fad44 | ||
|
|
d7b4025893 | ||
|
|
934356571c | ||
|
|
738d98eb42 | ||
|
|
7e689076d8 | ||
|
|
0b9d031965 | ||
|
|
53f470518b | ||
|
|
664d648435 | ||
|
|
0d718bd632 | ||
|
|
ed5e0c3509 | ||
|
|
20d5047b55 | ||
|
|
4cfe3ec06e | ||
|
|
87664ff16a | ||
|
|
adf278fc83 | ||
|
|
a4d0fa62d2 | ||
|
|
ff59d091f1 | ||
|
|
4cac99d79a | ||
|
|
d1a046279d | ||
|
|
cb9fa5c42b | ||
|
|
05f9e6895b | ||
|
|
63b5ee6f96 | ||
|
|
36d2de049f | ||
|
|
86b0d5e2b7 | ||
|
|
d34f641130 | ||
|
|
39d7b4c7bd | ||
|
|
ad0d4156fb | ||
|
|
80187e2789 | ||
|
|
89e25c9b81 | ||
|
|
ca51d59815 | ||
|
|
433f445e99 | ||
|
|
474a765e1b | ||
|
|
7d4b17380d | ||
|
|
b58b1d94cd | ||
|
|
16e7257e3b | ||
|
|
ca1a46f980 | ||
|
|
596bd12f59 | ||
|
|
301ab65c01 | ||
|
|
35f210e074 | ||
|
|
c239b518e0 | ||
|
|
f45935aa44 | ||
|
|
782505b292 | ||
|
|
1368e3b86d | ||
|
|
ab9c24401e | ||
|
|
831f4e48d1 | ||
|
|
f5511449af | ||
|
|
80c1e39b53 | ||
|
|
3ce4e86784 | ||
|
|
fb617418bb | ||
|
|
9fb0f793b2 | ||
|
|
3b21a097ab | ||
|
|
ef09a42a89 | ||
|
|
74d8f2a859 | ||
|
|
48910f9c0f | ||
|
|
788a1accbd | ||
|
|
b254e7e852 | ||
|
|
e288fa1b8a | ||
|
|
eb9ec68494 | ||
|
|
10ab4f2d79 | ||
|
|
b1986dc275 | ||
|
|
831e36ae5f | ||
|
|
77a2d37d98 | ||
|
|
37b90e25dc | ||
|
|
41f16846b6 | ||
|
|
3e89c62e72 | ||
|
|
b7d3762c06 | ||
|
|
481bc9430a | ||
|
|
13f2470887 | ||
|
|
0308fe4e6e | ||
|
|
197273193e | ||
|
|
e4b11a793b | ||
|
|
927d3135c4 | ||
|
|
aa533c20d6 | ||
|
|
7fd615525a | ||
|
|
6abf3fc0af | ||
|
|
6bb0929822 | ||
|
|
feebc04e55 | ||
|
|
2d406cd7c1 | ||
|
|
788bfe632f | ||
|
|
1149102f37 | ||
|
|
8bd949f618 | ||
|
|
489b142a66 | ||
|
|
cbbbb6da4f | ||
|
|
6701c7e3af | ||
|
|
ecd460a991 | ||
|
|
b4d7648ffe | ||
|
|
eb3dfb0f30 | ||
|
|
2b9ba69480 | ||
|
|
8f784162ea | ||
|
|
a2ab64b142 | ||
|
|
052817ccbf | ||
|
|
48b2297bc1 | ||
|
|
e76a0ad4bd | ||
|
|
baf6d30348 | ||
|
|
7d250dd90b | ||
|
|
efaabb02e8 | ||
|
|
0c3b98d451 | ||
|
|
28d1539ea6 | ||
|
|
8ad02d2d51 | ||
|
|
1947a066e0 | ||
|
|
d99e643634 | ||
|
|
65e1ac2ef9 | ||
|
|
64a768090f | ||
|
|
488eed046e | ||
|
|
dccddd78a6 | ||
|
|
3c247abcf9 | ||
|
|
db795e91fd | ||
|
|
f060f56c93 | ||
|
|
51be573f5e | ||
|
|
4257cbb618 | ||
|
|
e25d6b712d | ||
|
|
b499d60130 | ||
|
|
68e06303a4 | ||
|
|
60085798f2 | ||
|
|
c62cab39f1 | ||
|
|
30483ab2d9 | ||
|
|
c38c716616 | ||
|
|
ded1127d64 | ||
|
|
38d6130e89 | ||
|
|
ee47e40d1a | ||
|
|
80e66415d7 | ||
|
|
81e6372070 | ||
|
|
dbfbd42d29 | ||
|
|
e0d34b1747 | ||
|
|
9a8f9dd1d7 | ||
|
|
75521f8757 | ||
|
|
11d4cb2f04 | ||
|
|
d90cb09b56 | ||
|
|
a02d393457 | ||
|
|
01a5963947 | ||
|
|
7ef314d9f4 | ||
|
|
2ff25d1f61 | ||
|
|
ede12b6732 | ||
|
|
8a010f62fd | ||
|
|
51da4a7e70 | ||
|
|
fd2580f5da | ||
|
|
c5fdab7d47 | ||
|
|
ae094438b1 | ||
|
|
3e5af9e894 | ||
|
|
10093afb91 | ||
|
|
58032738b9 | ||
|
|
89cbbaeeea | ||
|
|
5ca08eb400 | ||
|
|
fad049d2ac | ||
|
|
87466cb5bd | ||
|
|
bb69ad8019 | ||
|
|
d5373b85c7 | ||
|
|
d71e699582 | ||
|
|
f464751752 | ||
|
|
d69f355206 | ||
|
|
769c32a1b1 | ||
|
|
44075d2ad5 | ||
|
|
2b4ae3f1c2 | ||
|
|
8689b48c55 | ||
|
|
443882f4be | ||
|
|
9c3adaedf0 | ||
|
|
4ba4d383f0 | ||
|
|
947febcdaa | ||
|
|
b0a152612e | ||
|
|
c9991655cf | ||
|
|
8971d218bb | ||
|
|
8b05486945 | ||
|
|
42cd5d8e58 | ||
|
|
31f420937e | ||
|
|
952d086c5e | ||
|
|
422a04795f | ||
|
|
28a8807cfb | ||
|
|
c6327a953b | ||
|
|
cc71f2e4a3 | ||
|
|
701086ec26 | ||
|
|
20acaebb68 | ||
|
|
22997cd903 | ||
|
|
7cb720b11f | ||
|
|
8bf35b2a63 | ||
|
|
8b72690c17 | ||
|
|
a0cc768af9 | ||
|
|
9fc73e03a2 | ||
|
|
edd57f7205 | ||
|
|
8b155b62dc | ||
|
|
bf5daa1a9b | ||
|
|
4f154e33bc | ||
|
|
395bcade67 | ||
|
|
985a038482 | ||
|
|
2da04c5bb1 | ||
|
|
1baf8cfaa6 | ||
|
|
956a9a7dde | ||
|
|
d7bb457fd0 | ||
|
|
563ff38c25 | ||
|
|
05caaa0814 | ||
|
|
1f283d285a | ||
|
|
adaa7ca413 | ||
|
|
247e797d2b | ||
|
|
9c7c030b00 | ||
|
|
867c9ea66c | ||
|
|
59985adc5d | ||
|
|
fd4504f0ee | ||
|
|
6c6aacc64e | ||
|
|
678adc0726 | ||
|
|
be3b684866 | ||
|
|
179aadafa4 | ||
|
|
f9dc129601 | ||
|
|
6beea2d208 | ||
|
|
57ef84ad3b | ||
|
|
9d9fdcff1e | ||
|
|
83e82e4789 | ||
|
|
4a12e0b6a0 | ||
|
|
642118959e | ||
|
|
600011be17 | ||
|
|
6765251332 | ||
|
|
afa5b046cb | ||
|
|
138d5627b5 | ||
|
|
682d9e88ec | ||
|
|
191c531bad | ||
|
|
f026e51c54 | ||
|
|
693afc52ff | ||
|
|
b22914e88a | ||
|
|
50b081608f | ||
|
|
214528a880 | ||
|
|
4f13b32f9a | ||
|
|
8ce8f5763c | ||
|
|
1e5b2f54c3 | ||
|
|
348bc16d6d | ||
|
|
ff2c69102d | ||
|
|
9479b7dc74 | ||
|
|
39f4e13915 | ||
|
|
5ea9f26ec4 | ||
|
|
c1d9cc7e2a | ||
|
|
6b80124a35 | ||
|
|
83a3754fe5 | ||
|
|
bf9b53c4d9 | ||
|
|
2b5f7dc8de | ||
|
|
4349911076 | ||
|
|
152f68624c | ||
|
|
3770194598 | ||
|
|
2b4a7c40e1 | ||
|
|
62a9158074 | ||
|
|
354fd88028 | ||
|
|
1b191c2bc9 | ||
|
|
f94212729a | ||
|
|
43e5398d99 | ||
|
|
920107e13f | ||
|
|
d52bed9618 | ||
|
|
5ecc31b977 | ||
|
|
ed3ecc6f4e | ||
|
|
add10ea556 | ||
|
|
d627fad299 | ||
|
|
6f8b31eaa2 | ||
|
|
8d47c83117 | ||
|
|
75ddf2681e | ||
|
|
4ad72b2ef0 | ||
|
|
a7334061ef | ||
|
|
c89c7dab60 | ||
|
|
0e0e4aae69 | ||
|
|
971c039086 | ||
|
|
5e8e1968e1 | ||
|
|
76a1c827d1 | ||
|
|
a4ab028ad6 | ||
|
|
d2fdf0586c | ||
|
|
0825806093 | ||
|
|
44b1bee080 | ||
|
|
d87f54d4a4 | ||
|
|
dc12896b19 | ||
|
|
23e992de85 | ||
|
|
6f6b047537 | ||
|
|
9fc6013934 | ||
|
|
8a02d557f4 | ||
|
|
1406f1f8e5 | ||
|
|
d5f6b65861 | ||
|
|
8d2bbe86d5 | ||
|
|
1a2f553094 | ||
|
|
4d69866532 | ||
|
|
bdddea2e29 | ||
|
|
be4ca0eede | ||
|
|
495c97b44b | ||
|
|
e817b3254e | ||
|
|
dd6987efe9 | ||
|
|
d7f8d12d88 | ||
|
|
504895a730 | ||
|
|
cde92836f3 | ||
|
|
9158e33546 | ||
|
|
c787988b06 | ||
|
|
316d71b34d | ||
|
|
1b344b7d9c | ||
|
|
3df01cd0cf | ||
|
|
d2331bb62f | ||
|
|
51504cc6a3 | ||
|
|
cd03ebe49d | ||
|
|
86c7ae3693 | ||
|
|
6e3b85e764 | ||
|
|
b3bc9a2edd | ||
|
|
90369acb63 | ||
|
|
94e84ce55f | ||
|
|
68cef1aa5d | ||
|
|
2f9ecb8fa8 | ||
|
|
6281374cf3 | ||
|
|
13580923ea | ||
|
|
898434b267 | ||
|
|
6f6e760a25 | ||
|
|
38fb549709 | ||
|
|
6e44c65a07 | ||
|
|
736b00b4e2 | ||
|
|
b5a139619a | ||
|
|
03028bca50 | ||
|
|
b835c5bd40 | ||
|
|
a65b1325fc | ||
|
|
cfcd302294 | ||
|
|
a601154fc7 | ||
|
|
c8669dc88f | ||
|
|
82240979c2 | ||
|
|
db5d495105 | ||
|
|
6e8dfe8833 | ||
|
|
242d9e20c4 | ||
|
|
d4e46a0696 | ||
|
|
d5deaf72f7 | ||
|
|
f9c942d526 | ||
|
|
e446eb0cd0 | ||
|
|
b63efe579a | ||
|
|
f3410f1491 | ||
|
|
b27ac11d56 | ||
|
|
a55d73614e | ||
|
|
25cd1957c7 | ||
|
|
abd97abc24 | ||
|
|
6ddfd909f0 | ||
|
|
e054eec555 | ||
|
|
e253657770 | ||
|
|
041c32a4a7 | ||
|
|
102e629e16 | ||
|
|
2d0e3b93cc | ||
|
|
668d572549 | ||
|
|
e023809c8c | ||
|
|
0bcabb9f0d | ||
|
|
594a9d5399 | ||
|
|
242a02836c | ||
|
|
b144382831 | ||
|
|
6936f223f3 | ||
|
|
7e64e1a11e | ||
|
|
54772fad33 | ||
|
|
892139908b | ||
|
|
ba6393165d | ||
|
|
2174a9cec7 | ||
|
|
5fe03f2934 | ||
|
|
09e7f04794 | ||
|
|
feecd51770 | ||
|
|
b5a855b2ad | ||
|
|
0b6bb65c17 | ||
|
|
79ea50c829 | ||
|
|
8532d563de | ||
|
|
2e945e8349 | ||
|
|
22e3037e85 | ||
|
|
a505ded8a1 | ||
|
|
bea7b90eb2 | ||
|
|
e1eb948c54 | ||
|
|
1b42523e2d | ||
|
|
ed7a40bc73 | ||
|
|
c8dc786008 | ||
|
|
a6869638f5 | ||
|
|
3ef08d3d5e | ||
|
|
5b7a9fe969 | ||
|
|
97d54cab11 | ||
|
|
980a1cc205 | ||
|
|
9517686e22 | ||
|
|
4905a0310a | ||
|
|
d2f88d0a02 | ||
|
|
76000d2563 | ||
|
|
bc02d51882 | ||
|
|
aada9e4a33 | ||
|
|
cf47a80676 | ||
|
|
c190067181 | ||
|
|
4490a2508b | ||
|
|
938e7704e6 | ||
|
|
d93eda34c9 | ||
|
|
b84ad98fd0 | ||
|
|
abf526f91a | ||
|
|
c06020745e | ||
|
|
431b85c98f | ||
|
|
299fdc19d6 | ||
|
|
7531a25588 | ||
|
|
d8f89191e8 | ||
|
|
d9ad53d954 | ||
|
|
043fc5cdfc | ||
|
|
bf3f9b4ac2 | ||
|
|
4c91667d2c | ||
|
|
a3fe9aff94 | ||
|
|
ed45888d7f | ||
|
|
dbadc487ae | ||
|
|
a5b80655da | ||
|
|
99d4789049 | ||
|
|
2de4163553 | ||
|
|
0fc0be19b2 | ||
|
|
3c271ffffd | ||
|
|
155422bcb5 | ||
|
|
68b6dc5985 | ||
|
|
1aa793886b | ||
|
|
46cd2f10a5 | ||
|
|
337abe2a2b | ||
|
|
0441103f0c | ||
|
|
b5829e2484 | ||
|
|
c23be2185a | ||
|
|
c43ea11017 | ||
|
|
bd18a9270e | ||
|
|
e24cba9684 | ||
|
|
084e96be0e | ||
|
|
5dfefe8d35 | ||
|
|
4a6ba998d4 | ||
|
|
7f953c62af | ||
|
|
c9412b02af | ||
|
|
536a9d6e50 | ||
|
|
4ee0944037 | ||
|
|
98f5db9fbd | ||
|
|
bbefa3a009 | ||
|
|
575fe300fa | ||
|
|
4749e452c4 | ||
|
|
0881b66ab6 | ||
|
|
50966dce5d | ||
|
|
0e6bcefee6 | ||
|
|
41ec831df0 | ||
|
|
f33ea6c47c | ||
|
|
f419ec5702 | ||
|
|
24b7dc1c30 | ||
|
|
4619e387ad | ||
|
|
d7d2425435 | ||
|
|
2c99a4db0f | ||
|
|
8dd5428ef6 | ||
|
|
88d9111196 | ||
|
|
8ec30ec76d | ||
|
|
17e9885586 | ||
|
|
826628b18f | ||
|
|
a49c9d646c | ||
|
|
d7e2690430 | ||
|
|
71ab2dedb9 | ||
|
|
9e9027d4c6 | ||
|
|
a43dfc62c2 | ||
|
|
c84f61d388 | ||
|
|
11b1ff478e | ||
|
|
ea7c44b544 | ||
|
|
56cf6fba2f | ||
|
|
ef9f75514f | ||
|
|
bd4644dd4f | ||
|
|
f419cd6ac8 | ||
|
|
e714303f96 | ||
|
|
50581c539c | ||
|
|
6eed3f2fc7 | ||
|
|
7c5000d588 | ||
|
|
ec0dd0b174 | ||
|
|
a5f55c5f92 | ||
|
|
7412a6c01b | ||
|
|
5983ea32fd | ||
|
|
5716130e6f | ||
|
|
37f0bffcaa | ||
|
|
0e6a460332 | ||
|
|
28172607c6 | ||
|
|
cdd9eed3d8 | ||
|
|
34ef2700f2 | ||
|
|
38742d0668 | ||
|
|
0c9f6e0923 | ||
|
|
56d6347209 | ||
|
|
2db582339a | ||
|
|
7f6386dd8c | ||
|
|
92e33efc04 | ||
|
|
805409cb48 | ||
|
|
93400352ca | ||
|
|
585b943a52 | ||
|
|
a0bf1bca8b | ||
|
|
738e3a0fbe | ||
|
|
05f3171910 | ||
|
|
f0771e4fed | ||
|
|
a3cd3b3fb1 | ||
|
|
323577f1ca | ||
|
|
d22619adc7 | ||
|
|
d9ab8e83dc | ||
|
|
d5e783e7e6 | ||
|
|
f20604d3ad | ||
|
|
6b17e80e28 | ||
|
|
c996b61eae | ||
|
|
025cb6c6ca | ||
|
|
1715dadf8f | ||
|
|
70d75d1c69 | ||
|
|
1b76ad4252 | ||
|
|
22772a5fac | ||
|
|
3bd7798eda | ||
|
|
a6c2622f6b | ||
|
|
454412d9d4 | ||
|
|
1037083ed2 | ||
|
|
7f87a03830 | ||
|
|
4815af35cf | ||
|
|
f5fc17c10e | ||
|
|
06ad1dcb02 | ||
|
|
cb4276a2ee | ||
|
|
cf5f9d4080 | ||
|
|
f5febef10f | ||
|
|
f89d8d22a0 | ||
|
|
757a1e2353 | ||
|
|
08d7c189eb | ||
|
|
5e11e87a0c | ||
|
|
84d7ba5758 | ||
|
|
083f3df29d | ||
|
|
429fa1553b | ||
|
|
15809333c1 | ||
|
|
815855964e | ||
|
|
5649a10c44 | ||
|
|
075f94d438 | ||
|
|
a2b06972d0 | ||
|
|
5bb27131a3 | ||
|
|
d06aacbe25 | ||
|
|
5488a2d60a | ||
|
|
851182a92e | ||
|
|
f263097008 | ||
|
|
21fee6122f | ||
|
|
68a94da44c | ||
|
|
1ded2f8101 | ||
|
|
e3d436a019 | ||
|
|
d9a5764dda | ||
|
|
9859afe8b7 | ||
|
|
76357fb918 | ||
|
|
2a6c476189 | ||
|
|
7132a68a73 | ||
|
|
b9e2a28915 | ||
|
|
465c25e71c | ||
|
|
c6851cd31e | ||
|
|
026e4a8982 | ||
|
|
e5bd024de2 | ||
|
|
f05117b6a4 | ||
|
|
4932f417b6 | ||
|
|
6d0dcce5e3 | ||
|
|
1194f7e1b3 | ||
|
|
a4087530c5 | ||
|
|
d5fae68b98 | ||
|
|
457483f373 | ||
|
|
7f706bdbe9 | ||
|
|
9f7c01599e | ||
|
|
b462f57060 | ||
|
|
28faaf32d2 | ||
|
|
9a0090570c | ||
|
|
5ef22f9921 | ||
|
|
1239f80018 | ||
|
|
49d1d060d6 | ||
|
|
c21c6f3a55 | ||
|
|
1b161843ed | ||
|
|
084650e8ba | ||
|
|
dd5c32f889 | ||
|
|
2cfa31609e | ||
|
|
64101c8337 | ||
|
|
241fed25a2 | ||
|
|
209b34e451 | ||
|
|
b73add61c6 | ||
|
|
46067a4e56 | ||
|
|
6089ebae2a | ||
|
|
58ec9f4676 | ||
|
|
d4f8d98d2b | ||
|
|
9df1716480 | ||
|
|
9c34e64d0e | ||
|
|
5e50ed15bd | ||
|
|
b0bff55dac | ||
|
|
81cad34170 | ||
|
|
c38379e229 | ||
|
|
49ae88ff98 | ||
|
|
33548c95aa | ||
|
|
1c2748c04b | ||
|
|
40041cd727 | ||
|
|
cfc6374995 | ||
|
|
e3f1545cdb | ||
|
|
bc853da09a | ||
|
|
7bcece7f76 | ||
|
|
168f48eea9 | ||
|
|
fd81f24030 | ||
|
|
d851a8147d | ||
|
|
e0e4099750 | ||
|
|
adc54c8c51 | ||
|
|
0350838438 | ||
|
|
5f10d9af5e | ||
|
|
07945ef8a6 | ||
|
|
35d7fa4a85 | ||
|
|
ad3a0d4c39 | ||
|
|
c6ca365e26 | ||
|
|
589d21bac0 | ||
|
|
0eb7bcd013 | ||
|
|
87f694f698 | ||
|
|
fbb6870310 | ||
|
|
81efe25fbc | ||
|
|
63d440677b | ||
|
|
2e0df47581 | ||
|
|
a84ced70a6 | ||
|
|
4a9498e25e | ||
|
|
1978e218f4 | ||
|
|
2ab912c4f4 | ||
|
|
ca329fa2ff | ||
|
|
db122ed8c5 | ||
|
|
28d7f106b2 | ||
|
|
d6e46adfde | ||
|
|
93109fe9b8 | ||
|
|
439992d96f | ||
|
|
ff1653383b | ||
|
|
f94fe0b325 | ||
|
|
cc543ae33a | ||
|
|
5df1e62d53 | ||
|
|
1dff6f81fc | ||
|
|
de7672bdfb | ||
|
|
975da2c057 | ||
|
|
ecf2d95318 | ||
|
|
a1af272180 | ||
|
|
72f0793aeb | ||
|
|
e150be6a3e | ||
|
|
d71b13f2a2 | ||
|
|
66c88b37a4 | ||
|
|
a7198d2e83 | ||
|
|
d3584b5820 | ||
|
|
e2e6267dd7 | ||
|
|
23ce94fdae | ||
|
|
aa5baecdc3 | ||
|
|
601b2aa6bb | ||
|
|
d09ba38dbd | ||
|
|
b847c2b7b5 | ||
|
|
30e44cb533 | ||
|
|
d43b83081b | ||
|
|
699363c548 | ||
|
|
6ea3eb4ba6 | ||
|
|
4aa20b6f6a | ||
|
|
9c8ea27238 | ||
|
|
22c515b0e7 | ||
|
|
a4ab8e712d | ||
|
|
fd75326bb8 | ||
|
|
51e09ecfcc | ||
|
|
72a53c0c09 | ||
|
|
05e75c9a26 | ||
|
|
8c7d254244 | ||
|
|
9a5a0d7a2b | ||
|
|
a18d88a3f1 | ||
|
|
c49d68e457 | ||
|
|
13fd265e49 | ||
|
|
ea7b03a00c | ||
|
|
1336920a1a | ||
|
|
024d14d529 | ||
|
|
61089c1128 | ||
|
|
8a74210049 | ||
|
|
a45a1b19b6 | ||
|
|
f3d3c62809 | ||
|
|
7e2bd52f25 | ||
|
|
e65dd15edc | ||
|
|
fec976a494 | ||
|
|
c18b49a4a1 | ||
|
|
872eabefba | ||
|
|
feda929f0b | ||
|
|
6718193dee | ||
|
|
d63ac7df1c | ||
|
|
e462ff3f4b | ||
|
|
8f84b767d2 | ||
|
|
b7d746ee09 | ||
|
|
83f3fcd913 | ||
|
|
781ffa5574 | ||
|
|
8fe3b1a368 | ||
|
|
039075736c | ||
|
|
1bead03151 | ||
|
|
e64dc51a17 | ||
|
|
7f4df49933 | ||
|
|
2bc77ee0cd | ||
|
|
b25adf7f57 | ||
|
|
bd0c2385e2 | ||
|
|
689720b4b0 | ||
|
|
69c48e2770 | ||
|
|
aa934ad725 | ||
|
|
8596ca607d | ||
|
|
191a14e6c0 | ||
|
|
9c5df507ae | ||
|
|
643ea9e523 | ||
|
|
90be44738d | ||
|
|
dbbf6d8c1f | ||
|
|
ce2a08373e | ||
|
|
d9e2229335 | ||
|
|
61454e2030 | ||
|
|
6f79e8145e | ||
|
|
a39e6846fe | ||
|
|
9d251b5876 | ||
|
|
a3ecb520cb | ||
|
|
60c4673992 | ||
|
|
7c597539ff | ||
|
|
8b80c12aa4 | ||
|
|
9c802c7b0c | ||
|
|
00c0ba9863 | ||
|
|
ca11656844 | ||
|
|
9d27730a4a | ||
|
|
4ce0ca702c | ||
|
|
242e28ddb9 | ||
|
|
40d20b0f9c | ||
|
|
d157a94f89 | ||
|
|
12e4186972 | ||
|
|
46dbd320d3 | ||
|
|
07c70361da | ||
|
|
b92611f4e0 | ||
|
|
9f922b1c4e | ||
|
|
c0cf6b2551 | ||
|
|
eff3221f7d | ||
|
|
214f04d7a2 | ||
|
|
be57004257 | ||
|
|
8875640eb6 | ||
|
|
f29c3a4c49 | ||
|
|
2b3dff46aa | ||
|
|
f52218748f | ||
|
|
dc37009737 | ||
|
|
d7ce5eb651 | ||
|
|
730ed8bc69 | ||
|
|
32b8076c82 | ||
|
|
8b1f9a1b0e | ||
|
|
0d49361f50 | ||
|
|
26f630d9d6 | ||
|
|
7481874ba2 | ||
|
|
52a5ce95b6 | ||
|
|
f361c22fce | ||
|
|
a348585e76 | ||
|
|
6e1155adc6 | ||
|
|
7a4f62a793 | ||
|
|
4b9e15bbbc | ||
|
|
cf72467510 | ||
|
|
3a0f40633a | ||
|
|
12a6b75bfb | ||
|
|
db8e7817a3 | ||
|
|
8d135bc1fc | ||
|
|
2c00f0ffab | ||
|
|
9adce54987 | ||
|
|
264611bb71 | ||
|
|
d51d147b92 | ||
|
|
5e1d627834 | ||
|
|
37b23bdc47 | ||
|
|
9e1f97811e | ||
|
|
a0a944310f | ||
|
|
61c6c98fca | ||
|
|
188b8d2d1a | ||
|
|
c6b0e38fec | ||
|
|
92e69570e1 | ||
|
|
62f7a0c54b | ||
|
|
665fc2b7f9 | ||
|
|
dfd17bc96c | ||
|
|
41924f7e66 | ||
|
|
2340045dd1 | ||
|
|
ddfbf1d6c4 | ||
|
|
8639faeffc | ||
|
|
abf73193fd | ||
|
|
ff1b643cf4 | ||
|
|
a73acedc4d | ||
|
|
7ffeb44e77 | ||
|
|
5ae1488864 | ||
|
|
fb3ff165db | ||
|
|
cdf59bb877 | ||
|
|
b6371dde23 | ||
|
|
3989bfa832 | ||
|
|
740e1ccd5b | ||
|
|
bc03f2ed17 | ||
|
|
4b5cb86d87 | ||
|
|
b877d6088b | ||
|
|
26cfd439e1 | ||
|
|
8f4a43de75 | ||
|
|
e2d4a41d9a | ||
|
|
eafcf66f4d | ||
|
|
a8fad8193b | ||
|
|
ca695c38cd | ||
|
|
6e0f98a809 | ||
|
|
d2af0152d0 | ||
|
|
37f825ab6d | ||
|
|
8901d3ce64 | ||
|
|
92104f4ab7 | ||
|
|
408a2498d0 | ||
|
|
2641966285 | ||
|
|
8d473001aa | ||
|
|
ceaf0bd745 | ||
|
|
a96849bc89 | ||
|
|
09dc2265fe | ||
|
|
a1fa139ef1 | ||
|
|
df26a500c4 | ||
|
|
182b7ae84d | ||
|
|
e6e254bd86 | ||
|
|
3b4e7a0bfe | ||
|
|
a0887bb5b3 | ||
|
|
5c6a592aba | ||
|
|
a465218ba3 | ||
|
|
1ead6eb916 | ||
|
|
c8c0b24420 | ||
|
|
0da5d4376d | ||
|
|
44fe1b31ba | ||
|
|
0856d3d5c9 | ||
|
|
f6b74ea836 | ||
|
|
e0cef71700 | ||
|
|
47f3788f55 | ||
|
|
19d1f70458 | ||
|
|
840a236e61 | ||
|
|
b07b998304 | ||
|
|
43c103e436 | ||
|
|
6ff4f096a3 | ||
|
|
e820b13f9f | ||
|
|
d8d82441c3 | ||
|
|
63be8afd35 | ||
|
|
1166db7834 | ||
|
|
4f489e1854 | ||
|
|
3662f4f256 | ||
|
|
9ab275df5d | ||
|
|
66c1754eb8 | ||
|
|
b03335eb94 | ||
|
|
31e3677111 | ||
|
|
e67bab1f5c | ||
|
|
ceb6667112 | ||
|
|
cafba0b361 | ||
|
|
9bc1a51def | ||
|
|
d215885dab | ||
|
|
68a1c93fbb | ||
|
|
9c6302fa50 | ||
|
|
d60440e512 | ||
|
|
957b9319cb | ||
|
|
9a28b9d1f1 | ||
|
|
543d061251 | ||
|
|
acba90b293 | ||
|
|
8992e9988f | ||
|
|
3ce87b9cb3 | ||
|
|
cbe7b3046c | ||
|
|
62344cb814 | ||
|
|
72d45a5fd2 | ||
|
|
c4e4dd501d | ||
|
|
3e4efcf297 | ||
|
|
4ccadac148 | ||
|
|
6e148c18b0 | ||
|
|
75f849982e | ||
|
|
8722ef45ac | ||
|
|
3d96765b1f | ||
|
|
02d052c071 | ||
|
|
4c52fe11d3 | ||
|
|
b347c78a8c | ||
|
|
c70d92cf09 | ||
|
|
4590bed56e | ||
|
|
7c00d118f3 | ||
|
|
88ae24855a | ||
|
|
356884ea53 | ||
|
|
68e37fff79 | ||
|
|
6958e71efd | ||
|
|
d0618182d1 | ||
|
|
e8891e27dd | ||
|
|
adb80933ee | ||
|
|
23b942c7da | ||
|
|
7eef7d4636 | ||
|
|
51fba21dd6 | ||
|
|
6aa5d608bf | ||
|
|
21ad2c5744 | ||
|
|
dea1163159 | ||
|
|
c4c2e8cf74 | ||
|
|
5883c35cf3 | ||
|
|
4d2719a424 | ||
|
|
7cf2d0d01c | ||
|
|
5a08b512a7 | ||
|
|
aa6ff6cd64 | ||
|
|
72baa1a786 | ||
|
|
4d72569030 | ||
|
|
89421d292c | ||
|
|
55c6515bac | ||
|
|
5db6f6a58c | ||
|
|
eeedf6ab28 | ||
|
|
3758cd207b | ||
|
|
c15ede6239 | ||
|
|
e54e31e059 | ||
|
|
8c573aa8e4 | ||
|
|
654559086c | ||
|
|
b0d9679568 | ||
|
|
87fdaf7fa7 | ||
|
|
bb3a365166 | ||
|
|
2be183d980 | ||
|
|
c6dc846838 | ||
|
|
1142f1d59a | ||
|
|
126c470979 | ||
|
|
d679dc3e8b | ||
|
|
d5422dfe89 | ||
|
|
d64237b4f2 | ||
|
|
7b7e4942f2 | ||
|
|
e4c343a587 | ||
|
|
1a8ae21478 | ||
|
|
dd37a5b584 | ||
|
|
eec340e6c0 | ||
|
|
c2fb5ba1f0 | ||
|
|
36d7e17b86 | ||
|
|
5a1dc49428 | ||
|
|
47caf54772 | ||
|
|
6af50a8c44 | ||
|
|
1b27407970 | ||
|
|
4da6306c67 | ||
|
|
f950b7a725 | ||
|
|
930cf9ed04 | ||
|
|
744016e752 | ||
|
|
2fb4e907df | ||
|
|
ef2a815e52 | ||
|
|
33a81d4f3c | ||
|
|
4911df0bf9 | ||
|
|
2b612b5db7 | ||
|
|
bfe81b52ef | ||
|
|
26f6a4beb9 | ||
|
|
532a368606 | ||
|
|
52e9c3053a | ||
|
|
908d1f1ec8 | ||
|
|
6a1120f95b | ||
|
|
2a2780c25c | ||
|
|
7d4152197b | ||
|
|
9c742600ff | ||
|
|
6c4178e107 | ||
|
|
a035bf132a | ||
|
|
05a68030b6 | ||
|
|
b989d157a0 | ||
|
|
c5f3228ba7 | ||
|
|
db3728f9e4 | ||
|
|
ca2d3f5b48 | ||
|
|
f3a992a55f | ||
|
|
b5d1e7c459 | ||
|
|
eecd89772f | ||
|
|
261587511b | ||
|
|
ff798801fb | ||
|
|
9b4aab0d19 | ||
|
|
02d1fb436d | ||
|
|
1c97bf9019 | ||
|
|
bfc4bb1f4c | ||
|
|
ccdce32562 | ||
|
|
87fdcf14e5 | ||
|
|
a0570f41fb | ||
|
|
6e42cf9952 | ||
|
|
4c3a8ca312 | ||
|
|
a63eb48f03 | ||
|
|
d0214f805e | ||
|
|
d736bd6501 | ||
|
|
5fd42bf216 | ||
|
|
2ce4a11e0a | ||
|
|
e5ab8fe3e4 | ||
|
|
3e55d8d9df | ||
|
|
d9f4a196d0 | ||
|
|
768ef8a316 | ||
|
|
a478d02e5d | ||
|
|
b98a7de3ae | ||
|
|
657b74a084 | ||
|
|
dfee98b66b | ||
|
|
f65b9f695e | ||
|
|
4056385cd3 | ||
|
|
96d56d43bc | ||
|
|
2e31ff42ed | ||
|
|
9a3b8f2b10 | ||
|
|
079790bce6 | ||
|
|
d99c4c6d14 | ||
|
|
6839b4d8a4 | ||
|
|
4c47cc6ea8 | ||
|
|
de81649a98 | ||
|
|
d31eab673e | ||
|
|
31189be2c8 | ||
|
|
45dd2c6519 | ||
|
|
5176d2000e | ||
|
|
6f606761e4 | ||
|
|
cfabadffe4 | ||
|
|
eba8f95e58 | ||
|
|
7e2da1ff93 | ||
|
|
b7b7e81468 | ||
|
|
0c7768f5d2 | ||
|
|
8fe6a56dfc | ||
|
|
7523cb3489 | ||
|
|
505a9d7c70 | ||
|
|
7b9dae980d | ||
|
|
b59ba6b7bb | ||
|
|
8cdee4d173 | ||
|
|
c9ed5fbe00 | ||
|
|
e698e89968 | ||
|
|
9581764cc8 | ||
|
|
02f198d42c | ||
|
|
f03493a252 | ||
|
|
61d2d0263b | ||
|
|
ed477e99a8 | ||
|
|
22f2a05c8a | ||
|
|
a703ecc7e1 | ||
|
|
c90a687179 | ||
|
|
e08689ff0e | ||
|
|
80ffd811c9 | ||
|
|
56b583fc99 | ||
|
|
37a4e108be | ||
|
|
9ca531541d | ||
|
|
e304c554d0 | ||
|
|
a7dfa7a381 | ||
|
|
1449be8d66 | ||
|
|
090d48b636 | ||
|
|
55567bf666 | ||
|
|
5867d84eaa | ||
|
|
4d8f1ab169 | ||
|
|
6a2963be41 | ||
|
|
b10f7b35ee | ||
|
|
dc1bb8992f | ||
|
|
5eb7119821 | ||
|
|
3fa8fc19dc | ||
|
|
341d98e00d | ||
|
|
7eee0f4341 | ||
|
|
0b71b7862a | ||
|
|
e2893a0eba | ||
|
|
0c39a4e193 | ||
|
|
066072b22d | ||
|
|
d548503590 | ||
|
|
3a02fc99a2 | ||
|
|
d8ae943d8a | ||
|
|
63dae9ed70 | ||
|
|
97f57f1f2b | ||
|
|
060ba6423e | ||
|
|
f31bbcdaab | ||
|
|
2a4bae54ab | ||
|
|
95df2f66a3 | ||
|
|
c0c63f49b1 | ||
|
|
4e1bef9537 | ||
|
|
8b78727d20 | ||
|
|
4cd7025be4 | ||
|
|
015ce2690d | ||
|
|
3e7f552a63 | ||
|
|
28902d8747 | ||
|
|
d534592479 | ||
|
|
b2f6ea9116 | ||
|
|
8bf38bb29b | ||
|
|
9c6a78b678 | ||
|
|
128b169ae2 | ||
|
|
fdd33abe91 | ||
|
|
22882b1ff2 | ||
|
|
213898987d | ||
|
|
850199d7fc | ||
|
|
4282928960 | ||
|
|
356dd89d9f | ||
|
|
7dd2391e5a | ||
|
|
e0093f236a | ||
|
|
8c5c32268a | ||
|
|
b61ccc1af1 | ||
|
|
7caf0e40f4 | ||
|
|
a16508db10 | ||
|
|
044c9bed4c | ||
|
|
81bff342b9 | ||
|
|
49d41a76a0 | ||
|
|
b1732b3298 | ||
|
|
9372cdb6c7 | ||
|
|
81062638eb | ||
|
|
1d8e54b83e | ||
|
|
30c5600271 | ||
|
|
9f7e5c3a9a | ||
|
|
37c9342717 | ||
|
|
8827f8e940 | ||
|
|
58334bf4a1 | ||
|
|
b898a6702c | ||
|
|
6d78a810b9 | ||
|
|
8c0028055a | ||
|
|
8fc4eb8cdf | ||
|
|
b3fac0c56f | ||
|
|
0b063b1f5e | ||
|
|
480f05e676 | ||
|
|
1ac8af34ec | ||
|
|
74f060b309 | ||
|
|
12de0ca463 | ||
|
|
72ed59b65b | ||
|
|
fe369bfa18 | ||
|
|
116661d958 | ||
|
|
0dd70c57cd | ||
|
|
df0aa1c46d | ||
|
|
9b93a47e45 | ||
|
|
06f6912fb9 | ||
|
|
c708fd65d7 | ||
|
|
0c497900a2 | ||
|
|
34ff8b0f02 | ||
|
|
77c3684e28 | ||
|
|
93038ea838 | ||
|
|
46348f7cba | ||
|
|
ccc0e45daf | ||
|
|
46ca03b017 | ||
|
|
1bfe3197a5 | ||
|
|
4d2617fe68 | ||
|
|
92e289f9da | ||
|
|
a8c7558a77 | ||
|
|
c756e7ecbe | ||
|
|
1998c56e84 | ||
|
|
0b92ceec90 | ||
|
|
731bcc68ef | ||
|
|
7e5131c4d2 | ||
|
|
f0554fc77c | ||
|
|
c7814b1f04 | ||
|
|
d585b47360 | ||
|
|
4530d95f48 | ||
|
|
2ed55b1616 | ||
|
|
0c8d456fd3 | ||
|
|
c7e6e72ce8 | ||
|
|
013d4b9411 | ||
|
|
9e4924caf6 | ||
|
|
7f391a5860 | ||
|
|
5c7249c8fc | ||
|
|
0a8fed1950 | ||
|
|
6dc4b4dc1b | ||
|
|
959b8863eb | ||
|
|
932d00133d | ||
|
|
32a371bf13 | ||
|
|
b25f418411 | ||
|
|
985a4780f5 | ||
|
|
92a1f2c6d5 | ||
|
|
81b9348e50 | ||
|
|
04e7d54620 | ||
|
|
729dbe16c0 | ||
|
|
974650bc56 | ||
|
|
0899a16333 | ||
|
|
8c9ed833c3 | ||
|
|
5e86e64e18 | ||
|
|
235c789c5e | ||
|
|
3f69a22229 | ||
|
|
ab48d06967 | ||
|
|
18ca950cd2 | ||
|
|
82489b36c8 | ||
|
|
a67b6130f8 | ||
|
|
eab007db6e | ||
|
|
5d0622d2cf | ||
|
|
9ab9155bf0 | ||
|
|
889eae276e | ||
|
|
2b2a72252b | ||
|
|
13e4568d3b | ||
|
|
86a1ed6d46 | ||
|
|
b3c9936d74 | ||
|
|
92c4dda801 | ||
|
|
3e59ba4563 | ||
|
|
99c95626df | ||
|
|
20a9fc2497 | ||
|
|
27e0621ad8 | ||
|
|
abecff222c | ||
|
|
3f0d91101a | ||
|
|
252be59ac3 | ||
|
|
1987a8c68a | ||
|
|
5088fd0e82 | ||
|
|
2de4d36a0f | ||
|
|
21b4d7cf11 | ||
|
|
4ec07f9ff8 | ||
|
|
fb32aeeeb6 | ||
|
|
76cb4037d4 | ||
|
|
b7c89d6f64 | ||
|
|
af1530db36 | ||
|
|
9d6560aece | ||
|
|
caba246e0b | ||
|
|
0eb168ec70 | ||
|
|
731e2dc4c4 | ||
|
|
815bdf3454 | ||
|
|
b36ef9fdb1 | ||
|
|
fee3f7a716 | ||
|
|
8ac1a66e93 | ||
|
|
301da3662a | ||
|
|
e474946cb7 | ||
|
|
dd00d6581f | ||
|
|
9a0ca1ebb2 | ||
|
|
f4fc7acf4d | ||
|
|
5b8cdf06b3 | ||
|
|
682315e672 | ||
|
|
520f7b2a77 | ||
|
|
c0b3b3aab8 | ||
|
|
d499332ce3 | ||
|
|
19ce06e0bb | ||
|
|
e33c7fd1c1 | ||
|
|
9b3668423e | ||
|
|
7227af9aac | ||
|
|
7c8194307e | ||
|
|
ea6ff4224e | ||
|
|
36c38063f2 | ||
|
|
9b5fac9e2b | ||
|
|
871d1f8632 | ||
|
|
47991b7d1a | ||
|
|
a5ea24311a | ||
|
|
77ce2ff6d1 | ||
|
|
3c11f0acda | ||
|
|
6383104796 | ||
|
|
4a5222fa3b | ||
|
|
b99b4159c8 | ||
|
|
4294dfd8fe | ||
|
|
8bedb1f3b9 | ||
|
|
dc85804a27 | ||
|
|
42a31e512a | ||
|
|
2be7388696 | ||
|
|
e7b739bb3b | ||
|
|
baf5f7491d | ||
|
|
a2afe2fa1a | ||
|
|
8496a9bebd | ||
|
|
bc5b00781b | ||
|
|
313e2b3de6 | ||
|
|
0bbd002060 | ||
|
|
5e785266a5 | ||
|
|
5870769e7d | ||
|
|
0d6b7d6f04 | ||
|
|
59d6a51635 | ||
|
|
b4693019f7 | ||
|
|
320d7a89ca | ||
|
|
79b80dcd07 | ||
|
|
1016f5f26f | ||
|
|
6f6e547e6c | ||
|
|
9284aee3fa | ||
|
|
32726efe88 | ||
|
|
87daf0271c | ||
|
|
70e73a5a65 | ||
|
|
1642798aa6 | ||
|
|
588c369615 | ||
|
|
9aa9d4452c | ||
|
|
d7fe25c4fc | ||
|
|
990c5e9f08 | ||
|
|
352c9357df | ||
|
|
1ba4641641 | ||
|
|
60e0047285 | ||
|
|
7e2f2f6102 | ||
|
|
51def6535f | ||
|
|
14156b0911 | ||
|
|
e2ba1fa7f8 | ||
|
|
36b589c2db | ||
|
|
cdf1a5fe47 | ||
|
|
abf146707f | ||
|
|
235e7c143c | ||
|
|
522d6eed92 | ||
|
|
9d1d6ea4c5 | ||
|
|
0afd506a41 | ||
|
|
9dfb837e3f | ||
|
|
4ab63b569f | ||
|
|
8d390d256d | ||
|
|
4eec5e06fc | ||
|
|
e4063b1ba8 | ||
|
|
c24a4009c8 | ||
|
|
7b928c4d41 | ||
|
|
4d50eae3c9 | ||
|
|
c3a5e0592d | ||
|
|
207aef7cb3 | ||
|
|
fbbd9ae249 | ||
|
|
7aa591ffbd | ||
|
|
1fd91de50d | ||
|
|
0c3227cf8e | ||
|
|
8492ceee09 | ||
|
|
1f710b9b78 | ||
|
|
7bed200bf5 | ||
|
|
b87ad2df54 | ||
|
|
4f763e2109 | ||
|
|
c9e2f94daf | ||
|
|
75167fb65b | ||
|
|
675588f780 | ||
|
|
2d6f94edd8 | ||
|
|
52774c7d6d | ||
|
|
247c66ef4b | ||
|
|
01686b8e60 | ||
|
|
0317d6a862 | ||
|
|
2a316b1ffa | ||
|
|
9b9f0e5607 | ||
|
|
590f6cb7b3 | ||
|
|
1076fac40f | ||
|
|
14a4a415a2 | ||
|
|
524355b59c | ||
|
|
b120146cdc | ||
|
|
36fe49f3f5 | ||
|
|
7587458f99 | ||
|
|
d1e69f5957 | ||
|
|
2c35ee11b3 | ||
|
|
bd0c747d98 | ||
|
|
f5fb066975 | ||
|
|
cd6a0fa678 | ||
|
|
e735420dd8 | ||
|
|
bc2f17c840 | ||
|
|
8c459cac0f | ||
|
|
878d2d9260 | ||
|
|
c0c0af9b14 | ||
|
|
d1e472d482 | ||
|
|
c80e43ad0d | ||
|
|
fdd395e2b6 | ||
|
|
e094437168 | ||
|
|
14bca4bbf7 | ||
|
|
ea55c10c4d | ||
|
|
75cde40b0e | ||
|
|
5434b4987f | ||
|
|
2ee0be7466 | ||
|
|
2784a7cc92 | ||
|
|
b09f998d6c | ||
|
|
bdeb5895f6 | ||
|
|
3944b8aaee | ||
|
|
6e66cffb92 | ||
|
|
54befb2814 | ||
|
|
74fba76b85 | ||
|
|
2b8996e965 | ||
|
|
a9aad547b2 | ||
|
|
4673af6fd8 | ||
|
|
33ae81ce05 | ||
|
|
57092ee788 | ||
|
|
70e9e1c706 | ||
|
|
075c43ce0e | ||
|
|
10fddc51bb | ||
|
|
b32b319198 | ||
|
|
2d14fde671 | ||
|
|
9662b8fbee | ||
|
|
98cd2746ef | ||
|
|
ea18e4129c | ||
|
|
6980e2b959 | ||
|
|
7e7ec83c12 | ||
|
|
b12fd45df1 | ||
|
|
a8340c24c3 | ||
|
|
9f66421ae7 | ||
|
|
50584c2e50 | ||
|
|
7be4e1901a | ||
|
|
f49f3fb2a6 | ||
|
|
3c9ef8d199 | ||
|
|
b47146de45 | ||
|
|
97b229b2c7 | ||
|
|
669c24ebd5 | ||
|
|
6bb5bb9403 | ||
|
|
8c4b8271d8 | ||
|
|
69291c0574 | ||
|
|
2dc073dcd6 | ||
|
|
1894cb35d2 | ||
|
|
cd37420b07 | ||
|
|
16dde5c772 | ||
|
|
55cb6b39db | ||
|
|
89d13b2285 | ||
|
|
1b64b0468a | ||
|
|
085fb83294 | ||
|
|
edd606563f | ||
|
|
fb804e99f0 | ||
|
|
1707cbcb54 | ||
|
|
6d6a630c31 | ||
|
|
ff2990e8e5 | ||
|
|
d679aff0fb | ||
|
|
603a444905 | ||
|
|
a002958448 | ||
|
|
cb4bc37424 | ||
|
|
0fc6f917e6 | ||
|
|
ec0d012b24 | ||
|
|
2cd4b171a1 | ||
|
|
0cb6906c4d | ||
|
|
4c19b93c30 | ||
|
|
6165f1b405 | ||
|
|
37a4221e43 | ||
|
|
9831b222b5 | ||
|
|
7b6f44fb74 | ||
|
|
399f4d0ea3 | ||
|
|
26a668a875 | ||
|
|
bf96262b6e | ||
|
|
96635a98f5 | ||
|
|
1155fa1fe9 | ||
|
|
1875d31731 | ||
|
|
6f855fd14e | ||
|
|
08e392bb46 | ||
|
|
66d63e0546 | ||
|
|
7ee56fe8bc | ||
|
|
669d04ee48 | ||
|
|
cb1b37326e | ||
|
|
7bb73bee67 | ||
|
|
7286ddc338 | ||
|
|
54ef65ced9 | ||
|
|
4fb6bef04c | ||
|
|
7d1f9e33fe | ||
|
|
63c676ebfe | ||
|
|
7d5e7f5c73 | ||
|
|
15a8b97410 | ||
|
|
874162df87 | ||
|
|
0c8b9d8539 | ||
|
|
7c6579b264 | ||
|
|
fcaf6b7923 | ||
|
|
e3473721f6 | ||
|
|
28799c62a9 | ||
|
|
224a79840d | ||
|
|
c21fef7c72 | ||
|
|
99ae3e0f7f | ||
|
|
02a4161ecb | ||
|
|
1a68c3947d | ||
|
|
4ac58b3f44 | ||
|
|
a9758e6997 | ||
|
|
bb546c6bb7 | ||
|
|
d877a6f1ad | ||
|
|
ef7a563d97 | ||
|
|
df33772eba | ||
|
|
97c1cc8ed9 | ||
|
|
9f347a170a | ||
|
|
2f7cd4426d | ||
|
|
854f256470 | ||
|
|
5d0b40f752 | ||
|
|
27a2853ee8 | ||
|
|
67f6b80312 | ||
|
|
016037adc1 | ||
|
|
70d5c1034d | ||
|
|
ed6fb8754f | ||
|
|
6d08a9b11c | ||
|
|
cf6aa7cf79 | ||
|
|
b7f34b9da6 | ||
|
|
f82a6efda1 | ||
|
|
f869c02395 | ||
|
|
43e0217008 | ||
|
|
6c4e57aae0 | ||
|
|
f89538c6f7 | ||
|
|
1ee0085c22 | ||
|
|
d56590c6e6 | ||
|
|
d08a04959c | ||
|
|
2762f74ce5 | ||
|
|
556ca71c3a | ||
|
|
42be2d5031 | ||
|
|
6ebcf6eec5 | ||
|
|
25b78fb7e1 | ||
|
|
876e22b092 | ||
|
|
f9f9c16cb0 | ||
|
|
f63f5d0ac6 | ||
|
|
44dbbeb2a3 | ||
|
|
a881090e65 | ||
|
|
670dd2dd96 | ||
|
|
3d9fce02a4 | ||
|
|
dea401fc7c | ||
|
|
cca21fcd32 | ||
|
|
ab6b1e2c32 | ||
|
|
1baf04f786 | ||
|
|
59d9b3c6b4 | ||
|
|
f7b6fcf684 | ||
|
|
df3ec9a629 | ||
|
|
ce05b7a041 | ||
|
|
6bc4bf308b | ||
|
|
2ddb84f457 | ||
|
|
6b82cc7510 | ||
|
|
2764f4d455 | ||
|
|
28fecc428f | ||
|
|
290cc146c8 | ||
|
|
db4d46a584 | ||
|
|
8ed2e51dde | ||
|
|
33702c09a6 | ||
|
|
45aeca3753 | ||
|
|
deae7dfb4d | ||
|
|
8851a661c0 | ||
|
|
2d327961da | ||
|
|
2af043ebdd | ||
|
|
ac6d14113a | ||
|
|
398db72b44 | ||
|
|
32777e17c4 | ||
|
|
a3da3299fa | ||
|
|
3a41efbea8 | ||
|
|
e763db7102 | ||
|
|
e121295735 | ||
|
|
7c1c405a64 | ||
|
|
2b504ce5ab | ||
|
|
5d7c95a34d | ||
|
|
504c934fc9 | ||
|
|
81b0223f73 | ||
|
|
6d1e410bfd | ||
|
|
26c5c6152d | ||
|
|
d83bf0ebaf | ||
|
|
5adfe9a552 | ||
|
|
883f461dc7 | ||
|
|
8595ebc258 | ||
|
|
2bd31f4560 | ||
|
|
6df85ecadd | ||
|
|
07829918e4 | ||
|
|
b0d400b6eb | ||
|
|
706cb895ad | ||
|
|
45bf539b3c | ||
|
|
0923981f8d | ||
|
|
b0ac14363d | ||
|
|
5d346aba37 | ||
|
|
124cb15ebe | ||
|
|
a244ab898d | ||
|
|
3c551590eb | ||
|
|
10e30cccbc | ||
|
|
806a6b86a2 | ||
|
|
9719fdf5cc | ||
|
|
6d8764f8cb | ||
|
|
d9fd9cb408 | ||
|
|
39a16f9a7f | ||
|
|
f3ea8d012f | ||
|
|
7710ec0aba | ||
|
|
c97bd78cd0 | ||
|
|
728c5aa86e | ||
|
|
83d68ca293 | ||
|
|
47d7561db4 | ||
|
|
7d993e8319 | ||
|
|
1d1a597b22 | ||
|
|
23082f9300 | ||
|
|
ea1a7f9376 | ||
|
|
1b9aa63096 | ||
|
|
45b8fd0100 | ||
|
|
11bde53069 | ||
|
|
f115ee18c4 | ||
|
|
162a56232c | ||
|
|
1796c7bab8 | ||
|
|
65ad76479a | ||
|
|
422db04ec8 | ||
|
|
d12f60fe37 | ||
|
|
194c1c991c | ||
|
|
de6f0ef8eb | ||
|
|
3e8e2222c1 | ||
|
|
1620327a33 | ||
|
|
b1131e3667 | ||
|
|
cd8a92c30b | ||
|
|
10030c4959 | ||
|
|
db0250ac08 | ||
|
|
0a6b605760 | ||
|
|
81ac2375e5 | ||
|
|
6bcaca6cd7 | ||
|
|
ec8375252e | ||
|
|
766aa1762f | ||
|
|
5165e0a54c | ||
|
|
a2f7ad627e | ||
|
|
1176c162d4 | ||
|
|
a4880cd017 | ||
|
|
ffc155c341 | ||
|
|
42ea76eb2a | ||
|
|
be503f1341 | ||
|
|
6f4509c260 | ||
|
|
383bdce416 | ||
|
|
b2331084d1 | ||
|
|
ab3a594884 | ||
|
|
592adcf42e | ||
|
|
978c881ab7 | ||
|
|
7cc300dd83 | ||
|
|
687809db9d | ||
|
|
1127ec3a90 | ||
|
|
a797edfae9 | ||
|
|
938e106252 | ||
|
|
a0eb9caaa2 | ||
|
|
442f53d45e | ||
|
|
68de1ca248 | ||
|
|
e16061141e | ||
|
|
64cbe3d209 | ||
|
|
ebdc6376d8 | ||
|
|
68335123a1 | ||
|
|
25b18f4ef8 | ||
|
|
99727447ef | ||
|
|
e02cb56ee0 | ||
|
|
9ad615b0ff | ||
|
|
12eaceb032 | ||
|
|
3263511b72 | ||
|
|
75cae8c647 | ||
|
|
9991ef624c | ||
|
|
489e9fce27 | ||
|
|
0655628073 | ||
|
|
9460822529 | ||
|
|
d02358ac0d | ||
|
|
366237a625 | ||
|
|
2f2da18994 | ||
|
|
ecd30db215 | ||
|
|
1980854f6f | ||
|
|
7d4f006c25 | ||
|
|
b697be2383 | ||
|
|
143e53c43f | ||
|
|
02f75fb2e1 | ||
|
|
6cced719dc | ||
|
|
45ec0b9b01 | ||
|
|
2451ac3ade | ||
|
|
45a94fe73d | ||
|
|
6dde1ade01 | ||
|
|
d4de391ac5 | ||
|
|
af15f4bc6a | ||
|
|
e1173e6565 | ||
|
|
46f6911ef8 | ||
|
|
7629bf5be2 | ||
|
|
fb17de7988 | ||
|
|
32f4d42e59 | ||
|
|
d4ace24caa | ||
|
|
0031c2a9b7 | ||
|
|
509e99913a | ||
|
|
c639cfcfd9 | ||
|
|
b140a2ca3e | ||
|
|
c5ab47fa66 | ||
|
|
d60051b629 | ||
|
|
aa448e7a41 | ||
|
|
220750f887 | ||
|
|
808cc5d8d0 | ||
|
|
22ff330ee7 | ||
|
|
dd62bef66d | ||
|
|
e7feb99f8d | ||
|
|
6358accece | ||
|
|
9ce8a24eea | ||
|
|
4d0673f489 | ||
|
|
02e1a32fae | ||
|
|
d1690bda81 | ||
|
|
fbe1e6a7d5 | ||
|
|
4ed02ca501 | ||
|
|
af245ed9fe | ||
|
|
fc86a3e882 | ||
|
|
f9109edcf1 | ||
|
|
ec100e1a91 | ||
|
|
746c5f4a79 | ||
|
|
b2611728a1 | ||
|
|
fc6cc4234d | ||
|
|
7706c1cb63 | ||
|
|
4d7a07220c | ||
|
|
436875f7dc | ||
|
|
21c6f53ecc | ||
|
|
5472be8b72 | ||
|
|
d22542fcf3 | ||
|
|
1d8341eb27 | ||
|
|
1897a7ada3 | ||
|
|
a048698c66 | ||
|
|
f891e57f4a | ||
|
|
fcc590e48a | ||
|
|
9a02a2a65b | ||
|
|
536a6c5c60 | ||
|
|
86a6871ee8 | ||
|
|
6046045151 | ||
|
|
9c3ddd4ba4 | ||
|
|
155f2fc36c | ||
|
|
6c9f55c1d7 | ||
|
|
5bec3d7dcd | ||
|
|
a4c309efe8 | ||
|
|
4e22a208dd | ||
|
|
ff9e77118e | ||
|
|
6c6dfa9ac4 | ||
|
|
d60d5207d8 | ||
|
|
c3dc136de4 | ||
|
|
4cbc5c4e2f | ||
|
|
68820aaf59 | ||
|
|
cd65bc7683 | ||
|
|
8c0ae892f5 | ||
|
|
f570492a11 | ||
|
|
4d4a2897a5 | ||
|
|
cd73c8f82f | ||
|
|
934e67d146 | ||
|
|
4c96e44b9b | ||
|
|
189900549a | ||
|
|
6d18420a5d | ||
|
|
ab3d307393 | ||
|
|
89b2156f61 | ||
|
|
2f95da1892 | ||
|
|
6e0956f09f | ||
|
|
1191f0ba93 | ||
|
|
6534ffea26 | ||
|
|
cc447304f5 | ||
|
|
f9173c41d1 | ||
|
|
a2faedcacb | ||
|
|
cb56b3b9d0 | ||
|
|
a61b50548c | ||
|
|
75ad588e0b | ||
|
|
306d5d8fc7 | ||
|
|
8f8c6366e3 | ||
|
|
3b13bcb098 | ||
|
|
5553d5fefa | ||
|
|
9328518bbc | ||
|
|
df60784b51 | ||
|
|
6564edcc32 | ||
|
|
75bb7d5a2d | ||
|
|
ceab4e37cd | ||
|
|
afb6974cc0 | ||
|
|
bae3122bb5 | ||
|
|
0770aef4bf | ||
|
|
c198350bfa | ||
|
|
a4dc965c23 | ||
|
|
8b65c280a8 | ||
|
|
a2ed388777 | ||
|
|
f6670c699a | ||
|
|
5fa4c95480 | ||
|
|
5b8608c186 | ||
|
|
bb75d42ede | ||
|
|
b4b6def07a | ||
|
|
b305700987 | ||
|
|
40232b7eb1 | ||
|
|
67ff666db4 | ||
|
|
5960fd4fe0 | ||
|
|
f8b28c519c | ||
|
|
c46c0018ea | ||
|
|
4ab24d2fe5 | ||
|
|
c70ca2ff64 | ||
|
|
649ab26da8 | ||
|
|
c921ea6eb7 | ||
|
|
58aed76aa3 | ||
|
|
e286c57ce4 | ||
|
|
840f0b6379 | ||
|
|
538025edd5 | ||
|
|
ee1105b6dd | ||
|
|
4778274c97 | ||
|
|
d7ecb32238 | ||
|
|
744306fc50 | ||
|
|
11bbb8ed4d | ||
|
|
b5092a4444 | ||
|
|
e2442c07a9 | ||
|
|
35e8dcc3be | ||
|
|
d1600fd058 | ||
|
|
6f924d4e83 | ||
|
|
faf1508914 | ||
|
|
7eb8152835 | ||
|
|
8f45905831 | ||
|
|
4ba2ffce5b | ||
|
|
1416fb0c71 | ||
|
|
2975db247d | ||
|
|
ffb3659ef5 | ||
|
|
6dec07d562 | ||
|
|
afb22f3279 | ||
|
|
f2f369db64 | ||
|
|
03eaa652ce | ||
|
|
635c76db93 | ||
|
|
5f50f1928d | ||
|
|
32c9ed1dc2 | ||
|
|
0536926a1f | ||
|
|
3959c98479 | ||
|
|
1e7d1b1628 | ||
|
|
2ce5735676 | ||
|
|
defd42f74e | ||
|
|
71741e144e | ||
|
|
f2e64cdd5e | ||
|
|
afaa5d5e9e | ||
|
|
d82861727d | ||
|
|
90f0795416 | ||
|
|
aa54ab6e51 | ||
|
|
f0c28c74d8 | ||
|
|
3e285d6131 | ||
|
|
9efbe7771c | ||
|
|
a75caac13d | ||
|
|
279d0d20ea | ||
|
|
332ba96d34 | ||
|
|
3f6e5b7606 | ||
|
|
94703492fd | ||
|
|
df78117617 | ||
|
|
909b9480e4 | ||
|
|
21762ac1aa | ||
|
|
412bc175b4 | ||
|
|
dc0eb76e88 | ||
|
|
c96d94329e | ||
|
|
2695941a3c | ||
|
|
3506be1a70 | ||
|
|
cbf4786b39 | ||
|
|
8dbf334208 | ||
|
|
60ba5fbc72 | ||
|
|
c3ace0c44f | ||
|
|
8eceb90e63 | ||
|
|
4754e19e83 | ||
|
|
627227f2f9 | ||
|
|
42cef0da88 | ||
|
|
06f60b7d92 | ||
|
|
3ddb4d2b23 | ||
|
|
5a825bd459 | ||
|
|
ab1f08f687 | ||
|
|
2f89e3658a | ||
|
|
a0559d0dc9 | ||
|
|
8d03ce19b0 | ||
|
|
2470d851e9 | ||
|
|
df99f5c0a5 | ||
|
|
36f5084c52 | ||
|
|
b77d3f123d | ||
|
|
3c14405155 | ||
|
|
c10b0afaa8 | ||
|
|
3f7a2d6bfb | ||
|
|
f2a0d56e01 | ||
|
|
eac29993d3 | ||
|
|
0736cc8414 | ||
|
|
53240d40a0 | ||
|
|
4137dd7cc8 | ||
|
|
8907290d27 | ||
|
|
401dc1cb10 | ||
|
|
a6b5d26f56 | ||
|
|
eb55cba34a | ||
|
|
b0b41d984e | ||
|
|
947f64e32d | ||
|
|
24ccbfa9b6 | ||
|
|
8110acb795 | ||
|
|
7473aede60 | ||
|
|
6f204f721b | ||
|
|
7b0e08094a | ||
|
|
322e1a75b9 | ||
|
|
f08ab729bd | ||
|
|
052c974369 | ||
|
|
e760e868c1 | ||
|
|
5e3831a1a4 | ||
|
|
99e046ddea | ||
|
|
12e0759711 | ||
|
|
da0c1cec22 | ||
|
|
a0806d98a1 | ||
|
|
182897d971 | ||
|
|
f90a639fcc | ||
|
|
d95d7208a2 | ||
|
|
bbac8ffe64 | ||
|
|
801a649fb1 | ||
|
|
d23df2ab15 | ||
|
|
dbe828097c | ||
|
|
48a0623ded | ||
|
|
7c09ceecfd | ||
|
|
8c4954fb9b | ||
|
|
fbe892105b | ||
|
|
584e1bb847 | ||
|
|
c437ab282e | ||
|
|
42a100d138 | ||
|
|
65807bf35d | ||
|
|
2995f48ede | ||
|
|
3527b86ec5 | ||
|
|
fe7a9104a8 | ||
|
|
cbfb94afcb | ||
|
|
74d8eff6d8 | ||
|
|
d452702aef | ||
|
|
d7ed9ab64e | ||
|
|
3b6c5898fe | ||
|
|
ae22adc920 | ||
|
|
f8ed9c7357 | ||
|
|
5dacf9c3f5 | ||
|
|
9143120177 | ||
|
|
fd3b1bee92 | ||
|
|
bff42954d1 | ||
|
|
9129bfa284 | ||
|
|
96190c21d6 | ||
|
|
af2a9225b8 | ||
|
|
aa117a0ee3 | ||
|
|
39b0ea381b | ||
|
|
021cea0b34 | ||
|
|
eaad41fe55 | ||
|
|
e25d58d70a | ||
|
|
6b74fd6a02 | ||
|
|
9c0967170a | ||
|
|
0547cebfe2 | ||
|
|
caefdf4300 | ||
|
|
a59df15994 | ||
|
|
33304eb8d9 | ||
|
|
eb21a1bfb3 | ||
|
|
ce0333b0a7 | ||
|
|
25a1b53a91 | ||
|
|
6aba73f970 | ||
|
|
6406bb7fb6 | ||
|
|
abd89df365 | ||
|
|
2458107903 | ||
|
|
628f9bd9b5 | ||
|
|
2d791571d5 | ||
|
|
ed57127a79 | ||
|
|
6d9bcff8e1 | ||
|
|
8126cd1879 | ||
|
|
ab34c2261c | ||
|
|
651e4bb775 | ||
|
|
6953f65970 | ||
|
|
52073e79fa | ||
|
|
8e3484bb17 | ||
|
|
7110da8a36 | ||
|
|
7f06d6e68c | ||
|
|
7ffd6ded51 | ||
|
|
5e04547ecf | ||
|
|
7cbe5f64ce | ||
|
|
47ed78031a | ||
|
|
fd3d24b834 | ||
|
|
c2f607b452 | ||
|
|
b1328bb6e2 | ||
|
|
2a02583e27 | ||
|
|
cfb49f9136 | ||
|
|
5f20091f24 | ||
|
|
e5146f7def | ||
|
|
a37b8e35a1 | ||
|
|
d9bf7c7d12 | ||
|
|
84c980c3ea | ||
|
|
5823057b41 | ||
|
|
024a9b1763 | ||
|
|
0425780cd3 | ||
|
|
20734dc7f3 | ||
|
|
0574c58f16 | ||
|
|
31e3117190 | ||
|
|
b0bf18e235 | ||
|
|
f780ba2c5a | ||
|
|
f001b2c68f | ||
|
|
f125b593bf | ||
|
|
baee4e185d | ||
|
|
ca8476d466 | ||
|
|
a24de7fe3f | ||
|
|
757bf82a78 | ||
|
|
644887f727 | ||
|
|
563b643461 | ||
|
|
296141ad3d | ||
|
|
7abba0a69b | ||
|
|
0e4a6fd2e1 | ||
|
|
d452bf1f1c | ||
|
|
dee7767427 | ||
|
|
126828a813 | ||
|
|
03dc6fb73a | ||
|
|
087a71367d | ||
|
|
3653e89714 | ||
|
|
318dd14e42 | ||
|
|
2d13844b5d | ||
|
|
bec2e3b4a0 | ||
|
|
b777b7432a | ||
|
|
6f91c225c2 | ||
|
|
274884ef4d | ||
|
|
d3f52cdd1a | ||
|
|
c355e9ca4a | ||
|
|
4514ea8123 | ||
|
|
a9a1472cb7 | ||
|
|
250b0eee28 | ||
|
|
5cd7527937 | ||
|
|
57ebd5bb7a | ||
|
|
cb97e37c15 | ||
|
|
c18a697d6b | ||
|
|
ad40b72508 | ||
|
|
3a72e5910d | ||
|
|
8f3eb65a05 | ||
|
|
3013fa86b6 | ||
|
|
2593743746 | ||
|
|
700cd83ff5 | ||
|
|
0c27881eaf | ||
|
|
ab6bd56006 | ||
|
|
f7fdc6acd2 | ||
|
|
2c5f844edc | ||
|
|
a253de43c5 | ||
|
|
dbaf67a986 | ||
|
|
5175d06e37 | ||
|
|
651a27b558 | ||
|
|
fd41f8def6 | ||
|
|
208ea04fd5 | ||
|
|
5ee83a1af9 | ||
|
|
901c7704f4 | ||
|
|
c6f7290f92 | ||
|
|
5368eda98b | ||
|
|
7b9be209c8 | ||
|
|
cee05fea7c | ||
|
|
b87acb47e2 | ||
|
|
cb192bf9ea | ||
|
|
e02b8522d6 | ||
|
|
8cf74c88ce | ||
|
|
4095d6dcf6 | ||
|
|
cd4221e4f2 | ||
|
|
e4e65e4576 | ||
|
|
72e93786df | ||
|
|
16351ba7f3 | ||
|
|
96ba128942 | ||
|
|
76c8d4af25 | ||
|
|
3ea2b3cc00 | ||
|
|
0df0936022 | ||
|
|
4fc11a7fd3 | ||
|
|
8c509271a6 | ||
|
|
67d5b63ef9 | ||
|
|
4f999511a6 | ||
|
|
cfbf239175 | ||
|
|
1aedf9bb07 | ||
|
|
c2d4423720 | ||
|
|
c2f7a2620c | ||
|
|
6f0cda34b4 | ||
|
|
1a472fdf1f | ||
|
|
0551f61228 | ||
|
|
b900adfddd | ||
|
|
0e339daef5 | ||
|
|
5f5733e8b9 | ||
|
|
1372050a7b | ||
|
|
1960951c5e | ||
|
|
bc070407c7 | ||
|
|
0172ee0b6b | ||
|
|
2953bc6bb8 | ||
|
|
c0ed3a9e3c | ||
|
|
5456e4fe75 | ||
|
|
548754821e | ||
|
|
77f85579e3 | ||
|
|
867a1e960e | ||
|
|
48dc68c3fe | ||
|
|
2c719f326b | ||
|
|
201f92eb93 | ||
|
|
46f055b216 | ||
|
|
08305e679b | ||
|
|
e9e0b70199 | ||
|
|
441d784027 | ||
|
|
558956bf55 | ||
|
|
0d8250a3ac | ||
|
|
dc1f5826f8 | ||
|
|
06fb06829b | ||
|
|
bbf52d2611 | ||
|
|
f55a6617e9 | ||
|
|
e3ec03794a | ||
|
|
05c325d686 | ||
|
|
3bd273fbdd | ||
|
|
cf9472eb56 | ||
|
|
1b64a543f1 | ||
|
|
bb2085436e | ||
|
|
d882a4249a | ||
|
|
a2420e4288 | ||
|
|
3687a230e1 | ||
|
|
c274486057 | ||
|
|
94e9bbfd63 | ||
|
|
adb71ad174 | ||
|
|
4a95f5cd9d | ||
|
|
f3b368fae4 | ||
|
|
20919a8a39 | ||
|
|
f2e86efc4d | ||
|
|
98b27d647e | ||
|
|
97b07f7d42 | ||
|
|
7a884d1f87 | ||
|
|
d21f5f427c | ||
|
|
ebb472b8f6 | ||
|
|
1a2ef6479e | ||
|
|
876c63fe80 | ||
|
|
32236962f5 | ||
|
|
9ba2a6628d | ||
|
|
ba66af922f | ||
|
|
739641b857 | ||
|
|
d02a110f99 | ||
|
|
28b9bbe54f | ||
|
|
bf6bd7cbdc | ||
|
|
ddcb2468a6 | ||
|
|
f048b58935 | ||
|
|
09f6200c2e | ||
|
|
354692fb06 | ||
|
|
2c5858c2e0 | ||
|
|
1f41fd0436 | ||
|
|
e0bbefdfae | ||
|
|
bc6fbb2797 | ||
|
|
b579cf8128 | ||
|
|
a94ed014b7 | ||
|
|
9120db2dc6 | ||
|
|
0db991b668 | ||
|
|
347ced6942 | ||
|
|
5d7a775b2b | ||
|
|
df732ab4bf | ||
|
|
31cd3953d6 | ||
|
|
4666b13892 | ||
|
|
bf586d0837 | ||
|
|
37d7ddb4b0 | ||
|
|
ed45a9b156 | ||
|
|
3abbaeb44b | ||
|
|
f4ea39b602 | ||
|
|
847ea49042 | ||
|
|
779068c2ee | ||
|
|
140cd6882d | ||
|
|
a6dca12bad | ||
|
|
e40411ef91 | ||
|
|
fd20c21243 | ||
|
|
2e295c2391 | ||
|
|
5c0474ef96 | ||
|
|
73d1de75f9 | ||
|
|
596b0995f4 | ||
|
|
8ea6989ea4 | ||
|
|
b61fe97893 | ||
|
|
209aa2ebe6 | ||
|
|
c03a0e857e | ||
|
|
2854d698e6 | ||
|
|
944163be0e | ||
|
|
269a9eaff0 | ||
|
|
7f9c49cbc4 | ||
|
|
2b6bfeeb15 | ||
|
|
fa9742bc92 | ||
|
|
472e419abc | ||
|
|
169d11387b | ||
|
|
e59ac6d947 | ||
|
|
e193b45562 | ||
|
|
1ac34f810e | ||
|
|
e65e5c6e5f | ||
|
|
16a9f44d4d | ||
|
|
af6365c76a | ||
|
|
8c672b23b5 | ||
|
|
3b53f5ac11 | ||
|
|
ccdc744748 | ||
|
|
261f0b4bf0 | ||
|
|
495b59c2e5 | ||
|
|
f26fc9a0f0 | ||
|
|
3fd637b3c7 | ||
|
|
d6e1c13c39 | ||
|
|
ea605383d5 | ||
|
|
a4fa670dc5 | ||
|
|
56f99b2129 | ||
|
|
ccb508705b | ||
|
|
6f8fa96150 | ||
|
|
72e1632499 | ||
|
|
f7f13b9e07 | ||
|
|
62564d747f | ||
|
|
1d5d59c4c0 | ||
|
|
e8380b8a12 | ||
|
|
c304d9cc62 | ||
|
|
aad4ebf287 | ||
|
|
7d7cc56527 | ||
|
|
85d0271b86 | ||
|
|
749d5e22bb | ||
|
|
6c2f48181c | ||
|
|
480b6ff7d6 | ||
|
|
4bdd6f972c | ||
|
|
61c61adea1 | ||
|
|
6674d8456a | ||
|
|
d1478ff694 | ||
|
|
cb20d46b74 | ||
|
|
9dd2538043 | ||
|
|
f25136a512 | ||
|
|
03eb56ad2a | ||
|
|
2508840701 | ||
|
|
6e098f5a4f | ||
|
|
31b33406fd | ||
|
|
7ab7c763ed | ||
|
|
06258e757a | ||
|
|
5919b43a21 | ||
|
|
7d4b9521e7 | ||
|
|
f9d2fd7997 | ||
|
|
bdbc20c3c6 | ||
|
|
69d6d03714 | ||
|
|
f40e1e55b0 | ||
|
|
b9082ed838 | ||
|
|
4edfefa9a2 | ||
|
|
0f98ee5407 | ||
|
|
7fdf119873 | ||
|
|
3c054e6ea1 | ||
|
|
98899ece72 | ||
|
|
2061a006d0 | ||
|
|
5496c2d7fd | ||
|
|
d6b862a4a9 | ||
|
|
d581f8a852 | ||
|
|
3a593ee35a | ||
|
|
415d34fdaa | ||
|
|
7d28191bb5 | ||
|
|
e2c7693370 | ||
|
|
f17ff02f4d | ||
|
|
225043e01d | ||
|
|
56f78349f8 | ||
|
|
8839d4f55a | ||
|
|
2562aec1d2 | ||
|
|
db2361be84 | ||
|
|
d08fcbfef3 | ||
|
|
7601b93e65 | ||
|
|
1103ec40e0 | ||
|
|
af32c7e3db | ||
|
|
170918eb3b | ||
|
|
a91e615a8d | ||
|
|
cc92c26fe3 | ||
|
|
937135db32 | ||
|
|
01366558b4 | ||
|
|
b0dbd54ea4 | ||
|
|
f113915307 | ||
|
|
0a3c3d9bb1 | ||
|
|
ba2e005c3e | ||
|
|
b9ea52d65f | ||
|
|
f1e328d333 | ||
|
|
23f1965398 | ||
|
|
fc82f185cb | ||
|
|
56b25f373f | ||
|
|
1ac6add122 | ||
|
|
91b1a903f9 | ||
|
|
a8d6654ef5 | ||
|
|
63093b1be6 | ||
|
|
60abe8f37e | ||
|
|
7ba3909aa1 | ||
|
|
eecdba2d05 | ||
|
|
7bdc005aa7 | ||
|
|
d46703fdc4 | ||
|
|
e4aa85f603 | ||
|
|
233124ef50 | ||
|
|
36a3012de2 | ||
|
|
2b4ee96ed7 | ||
|
|
85a2afd55c | ||
|
|
6cd0d8456a | ||
|
|
7750a0a773 | ||
|
|
a5364b9257 | ||
|
|
e0e7b1406d | ||
|
|
38b67a0002 | ||
|
|
18dd4f8a52 | ||
|
|
879f9b4ea9 | ||
|
|
3db0dda67a | ||
|
|
ed9ee15b90 | ||
|
|
44ff85e8e9 | ||
|
|
cb07e9ba11 | ||
|
|
bfe05ce5fc | ||
|
|
64ee23cec0 | ||
|
|
c022d3c4a4 | ||
|
|
69c764301f | ||
|
|
2f777daef6 | ||
|
|
a10bf7330e | ||
|
|
782bb5967d | ||
|
|
aeb2f55f0d | ||
|
|
ae68749b1b | ||
|
|
a3c25d56a0 | ||
|
|
c8a6fd19a7 | ||
|
|
0b143b580a | ||
|
|
d2b9cc8df9 | ||
|
|
2027daa75c | ||
|
|
f3493a08bd | ||
|
|
ed69fedc0a | ||
|
|
8098464f58 | ||
|
|
ea0db57388 | ||
|
|
6fcc148105 | ||
|
|
3485cb4ec4 | ||
|
|
f3963269ae | ||
|
|
ae2212c245 | ||
|
|
3a19ac4c93 | ||
|
|
666f546cf0 | ||
|
|
e6ef6ccccf | ||
|
|
4826e14cad | ||
|
|
5515f90147 | ||
|
|
2193c26acb | ||
|
|
1974a2c0e4 | ||
|
|
84fbe9ee06 | ||
|
|
b8e2cfc47f | ||
|
|
464f57d7da | ||
|
|
2a192f33a1 | ||
|
|
553fc6f5d9 | ||
|
|
f5d790b264 | ||
|
|
9ca2674261 | ||
|
|
641e13496e | ||
|
|
a6e18819d4 | ||
|
|
faf5ff6aa4 | ||
|
|
ae20ca5558 | ||
|
|
22bd87c965 | ||
|
|
2129645f39 | ||
|
|
93a07b6207 | ||
|
|
24bc91dc0c | ||
|
|
cf2d5b502f | ||
|
|
61450ef602 | ||
|
|
78f1d1738e | ||
|
|
9f595cf5f7 | ||
|
|
25b8e49975 | ||
|
|
d40086cd13 | ||
|
|
8f9d8d93b9 | ||
|
|
1080c10004 | ||
|
|
866aeca220 | ||
|
|
121b3afc61 | ||
|
|
e8406b04b4 | ||
|
|
8e7fe81806 | ||
|
|
852807b5d7 | ||
|
|
9928d47fa2 | ||
|
|
412a1bd62a | ||
|
|
b290520951 | ||
|
|
dde677b6d3 | ||
|
|
75030847bd | ||
|
|
e7b9cb76bc | ||
|
|
e96c4c0dd3 | ||
|
|
b553b3fa50 | ||
|
|
c6fb924b8f | ||
|
|
b13844c4a6 | ||
|
|
ab6c83a3fc | ||
|
|
b2a51bd658 | ||
|
|
e5ab1dc154 | ||
|
|
7e0a97973f | ||
|
|
6274969635 | ||
|
|
069c430346 | ||
|
|
6a8a79bba5 | ||
|
|
4a0c58c50a | ||
|
|
cbcc4dd21d | ||
|
|
eb0c963332 | ||
|
|
023fe82932 | ||
|
|
2e1a06c7bf | ||
|
|
8b6961d40c | ||
|
|
53351877da | ||
|
|
522445894e | ||
|
|
550351bb16 | ||
|
|
328adbb56f | ||
|
|
b4cdf4d277 | ||
|
|
716d7bfcf6 | ||
|
|
b45a169a2f | ||
|
|
44a36bbba3 | ||
|
|
4cc4adeda6 | ||
|
|
c14e6f2a63 | ||
|
|
cfcb2d54d8 | ||
|
|
010d60e504 | ||
|
|
eabde07ff6 | ||
|
|
be19ad5f2a | ||
|
|
d1d0816961 | ||
|
|
7be7170504 | ||
|
|
478272f515 | ||
|
|
09af6958c8 | ||
|
|
adb3a2b64e | ||
|
|
1ee7e842dc | ||
|
|
b080a57406 | ||
|
|
7c017e345a | ||
|
|
4b91343155 | ||
|
|
02a3df8ad0 | ||
|
|
6a7080f4ee | ||
|
|
4547042577 | ||
|
|
0e39eea7f8 | ||
|
|
1e5aefea63 | ||
|
|
02c4f333b0 | ||
|
|
1e8fc4020b | ||
|
|
f969701ac1 | ||
|
|
b236243857 | ||
|
|
39edc64922 | ||
|
|
f22ece403f | ||
|
|
f5423bb314 | ||
|
|
b1e5945ebe | ||
|
|
d98a457271 | ||
|
|
ef3a0d881f | ||
|
|
669a9e5cc3 | ||
|
|
5eb58ae6cc | ||
|
|
7fa1923400 | ||
|
|
4c165bd620 | ||
|
|
7c04a455b4 | ||
|
|
06b6061518 | ||
|
|
3821ee3dcd | ||
|
|
76b5be8171 | ||
|
|
03a33646d6 | ||
|
|
791183553e | ||
|
|
de6ef49043 | ||
|
|
804bca2041 | ||
|
|
720b9ef999 | ||
|
|
9b9e4dddfc | ||
|
|
10602b47b4 | ||
|
|
8d7c522596 | ||
|
|
3ac455c5a7 | ||
|
|
2b19a459df | ||
|
|
41ba2d9bf6 | ||
|
|
a7b5eb69d3 | ||
|
|
67c209bb5e | ||
|
|
a6d436d9ea | ||
|
|
652c784e13 | ||
|
|
7434e0352f | ||
|
|
26d61af902 | ||
|
|
5bd12c5f9e | ||
|
|
e07fae4290 | ||
|
|
a0a3b7a158 | ||
|
|
28bf7ee90b | ||
|
|
4d1ca7ede4 | ||
|
|
f3b46515c5 | ||
|
|
0aa5e7ba63 | ||
|
|
8d8bf43b46 | ||
|
|
789f51bd2a | ||
|
|
9983407c8b | ||
|
|
2471ad4215 | ||
|
|
f266982560 | ||
|
|
c059a416f7 | ||
|
|
82dc0c0593 | ||
|
|
5faad3db92 | ||
|
|
099db6792a | ||
|
|
c2f1a74f96 | ||
|
|
a9ed7a3f3b | ||
|
|
74a31f3301 | ||
|
|
f88c0b9b67 | ||
|
|
61ef313b1c | ||
|
|
b348e88a5f | ||
|
|
1615395866 | ||
|
|
e483abcad0 | ||
|
|
12b6760f6e | ||
|
|
048cea19b7 | ||
|
|
3e77c76c34 | ||
|
|
9b6c5d2ea3 | ||
|
|
6fde6d7eac | ||
|
|
a7ef891217 | ||
|
|
2fa081a4ba | ||
|
|
8f22dfe87b | ||
|
|
2dc7fab39a | ||
|
|
5b9a3ca1cb | ||
|
|
bf7c56a269 | ||
|
|
d33af742dd | ||
|
|
823879e9f9 | ||
|
|
98eb285e14 | ||
|
|
74cb2e3c63 | ||
|
|
37fd2e1103 | ||
|
|
56db5dc341 | ||
|
|
d48fa235b1 | ||
|
|
06a111495b | ||
|
|
f3fb0797bf | ||
|
|
561b8f4eed | ||
|
|
6e763a58f1 | ||
|
|
8cfc6f0b1d | ||
|
|
7e04f26f78 | ||
|
|
348c30b61e | ||
|
|
ad1e60e137 | ||
|
|
97e0a983f1 | ||
|
|
a8e72ed410 | ||
|
|
fcdfd5f936 | ||
|
|
f1faa463c1 | ||
|
|
a0f4952b54 | ||
|
|
bd82ded07d | ||
|
|
016e17dedb | ||
|
|
5cd3e1b368 | ||
|
|
b2b39458da | ||
|
|
556bbe394d | ||
|
|
07288b3f26 | ||
|
|
45681e645b | ||
|
|
a4e9f1a683 | ||
|
|
f8c74daef5 | ||
|
|
b3a593afd7 | ||
|
|
90f79b7708 | ||
|
|
e220786a20 | ||
|
|
f16b993294 | ||
|
|
d45461bc47 | ||
|
|
c241bea3bf | ||
|
|
084654cd3c | ||
|
|
d21742afb6 | ||
|
|
b5259384e8 | ||
|
|
bf78ad9fbe | ||
|
|
ab3577c369 | ||
|
|
6efb90c94e | ||
|
|
cbcc400eb4 | ||
|
|
15aec7da7e | ||
|
|
46535e4f56 | ||
|
|
e3f945c079 | ||
|
|
04239c57fe | ||
|
|
58aa2b6a49 | ||
|
|
ad4439ed55 | ||
|
|
fb06905c86 | ||
|
|
9fe3ef430f | ||
|
|
ff30773097 | ||
|
|
f7531d1e18 | ||
|
|
4a2911557d | ||
|
|
99caa5dddc | ||
|
|
86b42a3716 | ||
|
|
12c4680501 | ||
|
|
b7e05c236f | ||
|
|
658008ab64 | ||
|
|
b089d63112 | ||
|
|
ee9b1b7f57 | ||
|
|
cd0fc8176f | ||
|
|
8e291e3e46 | ||
|
|
e3024076cd | ||
|
|
6105874abc | ||
|
|
1855f7829d | ||
|
|
456e8bd9c0 | ||
|
|
d5f2efac26 | ||
|
|
21e692623c | ||
|
|
80e9589af5 | ||
|
|
b2b9ae0677 | ||
|
|
63122905e6 | ||
|
|
f99b6f4646 | ||
|
|
39090c2a22 | ||
|
|
76baa8c791 | ||
|
|
74e4b9d6d2 | ||
|
|
bbfc5039f7 | ||
|
|
b2fd694483 | ||
|
|
b03f38ff22 | ||
|
|
fe48811047 | ||
|
|
bd9396b031 | ||
|
|
f0497ec16d | ||
|
|
7e9e179fa7 | ||
|
|
de62464ad8 | ||
|
|
f6911ca195 | ||
|
|
aec09ed8d2 | ||
|
|
51a983e460 | ||
|
|
0eb46e29c7 | ||
|
|
5ee11c7b6b | ||
|
|
b55accd76f | ||
|
|
fef2be1bc7 | ||
|
|
0b3858f91d | ||
|
|
d07ea1b337 | ||
|
|
7e2dbc7358 | ||
|
|
c676f08a7c | ||
|
|
92f24b5728 | ||
|
|
0254e71435 | ||
|
|
2972fc5814 | ||
|
|
975c96217c | ||
|
|
c30c1848bc | ||
|
|
94615d3b36 | ||
|
|
37a00f0d16 | ||
|
|
0dbe70f5af | ||
|
|
7584374b0b | ||
|
|
71ca51dc1a | ||
|
|
aa81e72e45 | ||
|
|
9954bb9c15 | ||
|
|
e304395179 | ||
|
|
6b83130853 | ||
|
|
9565718699 | ||
|
|
e5c0250423 | ||
|
|
135799ed5e | ||
|
|
22c3b57960 | ||
|
|
7054dd74a4 | ||
|
|
d4f1e52ef6 | ||
|
|
76a44459cf | ||
|
|
a5590b090c | ||
|
|
74c9a57070 | ||
|
|
06e283e070 | ||
|
|
8ab2ca3f24 | ||
|
|
0eb949ba39 | ||
|
|
be35693814 | ||
|
|
1e5f13795c | ||
|
|
cca2265633 | ||
|
|
0f0d1ac370 | ||
|
|
d52d4ac183 | ||
|
|
841220fd01 | ||
|
|
ca5e10784b | ||
|
|
712319974b | ||
|
|
067a6d01bc | ||
|
|
27825f9e2e | ||
|
|
425eb115dc | ||
|
|
0a5ce55e2b | ||
|
|
dbc8ed9d4c | ||
|
|
e31e990684 | ||
|
|
8618f56481 | ||
|
|
a39fc4667e | ||
|
|
4c369e240b | ||
|
|
4e291d01d4 | ||
|
|
0f03208aa1 | ||
|
|
b32fce46cb | ||
|
|
d1fcd45aac | ||
|
|
ebdb92c708 | ||
|
|
112909d35b | ||
|
|
c6e2c559f1 | ||
|
|
cf8613886a | ||
|
|
39c25c2001 | ||
|
|
9c9721ade5 | ||
|
|
4b8abe4ce8 | ||
|
|
33dfaba276 | ||
|
|
dd8bbcf358 | ||
|
|
cc3e4369ed | ||
|
|
548355fce6 | ||
|
|
a4e0e6544b | ||
|
|
62067e0801 | ||
|
|
5eb40d2299 | ||
|
|
d58add18fc | ||
|
|
53990a531b | ||
|
|
d799aea3c4 | ||
|
|
9af86cbba2 | ||
|
|
6fbfece4ff | ||
|
|
3a0413d8bb | ||
|
|
9122f9b291 | ||
|
|
d279db2a0e | ||
|
|
d56cca7873 | ||
|
|
fa1096a6ba | ||
|
|
16ff721331 | ||
|
|
798ee9dc46 | ||
|
|
bc17c60305 | ||
|
|
432688d577 | ||
|
|
da8d4b47f1 | ||
|
|
c6657b9619 | ||
|
|
ed1e2e2449 | ||
|
|
b9744b4688 | ||
|
|
80d8388eb6 | ||
|
|
b1ee4bdc09 | ||
|
|
2f328f8f37 | ||
|
|
ffefd7e50b | ||
|
|
c2445f8a7c | ||
|
|
54d44079cc | ||
|
|
87f089a12c | ||
|
|
aa2172e4db | ||
|
|
0a33e94e79 | ||
|
|
fada54abae | ||
|
|
802641b719 | ||
|
|
1da93829d4 | ||
|
|
9e7acbc49a | ||
|
|
318765d40b | ||
|
|
ac11885379 | ||
|
|
277669a13c | ||
|
|
fcbc476462 | ||
|
|
94ba20dfa1 | ||
|
|
4944b415c7 | ||
|
|
5da7312d2d | ||
|
|
954d19fe50 | ||
|
|
addd86f5d2 | ||
|
|
2ad3dc4a32 | ||
|
|
eef940dd7c | ||
|
|
1b90223210 | ||
|
|
95989ff63b | ||
|
|
1b5fc12ac1 | ||
|
|
c1c7b8dfcd | ||
|
|
d4510c2afe | ||
|
|
799f758dce | ||
|
|
f241f073a3 | ||
|
|
26a6c72611 | ||
|
|
51cee7804b | ||
|
|
52228430f1 | ||
|
|
4fcd45d8a4 | ||
|
|
ea5736947d | ||
|
|
6b5f36fb7e | ||
|
|
b328d6d95f | ||
|
|
2e06921bf8 | ||
|
|
fd95e2d711 | ||
|
|
490e253b79 | ||
|
|
8a10f5cd52 | ||
|
|
5cebcc2424 | ||
|
|
7663b89289 | ||
|
|
d0d1f9e3c0 | ||
|
|
5b782993fd | ||
|
|
138e60e77c | ||
|
|
9771402c54 | ||
|
|
30dcb4d8d2 | ||
|
|
4433760e37 | ||
|
|
c418c766d8 | ||
|
|
334d843955 | ||
|
|
2e5169eb46 | ||
|
|
3cdc1c03c3 | ||
|
|
733c619b1f | ||
|
|
2021b644c0 | ||
|
|
b63fcd2254 | ||
|
|
eb2aa352ef | ||
|
|
f55ed13bd2 | ||
|
|
14b7072b5b | ||
|
|
98395abc17 | ||
|
|
5db5c4e52c | ||
|
|
ec95f198ec | ||
|
|
f166e480f1 | ||
|
|
b31f55063d | ||
|
|
71e77ad45a | ||
|
|
25873e0e02 | ||
|
|
22638a8147 | ||
|
|
2051f0486c | ||
|
|
f33fc5d730 | ||
|
|
1603df3503 | ||
|
|
6055ac182b | ||
|
|
4678f60adf | ||
|
|
e075f1c08b | ||
|
|
b48521950e | ||
|
|
45da6fb39f | ||
|
|
1604d327da | ||
|
|
98b2b325a1 | ||
|
|
55b3def630 | ||
|
|
7e0aa719b4 | ||
|
|
ce7bc9f438 | ||
|
|
43a362d0eb | ||
|
|
7d7e6e10b9 | ||
|
|
73821b0f12 | ||
|
|
2071a7d308 | ||
|
|
c439daadad | ||
|
|
083f325076 | ||
|
|
ee53433dcc | ||
|
|
ad10d13a75 | ||
|
|
1b5f6bd3eb | ||
|
|
3472b85345 | ||
|
|
1c6ae53656 | ||
|
|
a84a56f611 | ||
|
|
69751aa415 | ||
|
|
9ef0337807 | ||
|
|
a3fdd274c3 | ||
|
|
f069c02153 | ||
|
|
ad0d14c517 | ||
|
|
a72da79743 | ||
|
|
4fd9639457 | ||
|
|
2f2ee1f431 | ||
|
|
6bc35217d9 | ||
|
|
c526e9ac8f | ||
|
|
3dfebcadc1 | ||
|
|
ad06c8147f | ||
|
|
693af532a2 | ||
|
|
4bac57fcd1 | ||
|
|
a2f5f1cb0e | ||
|
|
1fbe7d92eb | ||
|
|
760974c7c7 | ||
|
|
e1587d11b1 | ||
|
|
0595360808 | ||
|
|
1a8149e456 | ||
|
|
59291fcac2 | ||
|
|
1f21b202a5 | ||
|
|
b3ca02a166 | ||
|
|
2462f8f87e | ||
|
|
18268acc1f | ||
|
|
fd6f92f6b5 | ||
|
|
ddf7226ba8 | ||
|
|
a1cd95752a | ||
|
|
d131a26a41 | ||
|
|
10a7c75001 | ||
|
|
1f454ababf | ||
|
|
44f4423a9d | ||
|
|
37250a6f48 | ||
|
|
4ee352fdb2 | ||
|
|
fd97541417 | ||
|
|
dba3e30d02 | ||
|
|
96ea3ded4a | ||
|
|
8bbc6e9ff5 | ||
|
|
5962283ad3 | ||
|
|
cdd705831b | ||
|
|
a1a7c5e4bb | ||
|
|
c1e9061568 | ||
|
|
90ee04de57 | ||
|
|
cd24cfbe5c | ||
|
|
af7029812c | ||
|
|
5b05668a30 | ||
|
|
a2eca9589f | ||
|
|
a2adbb19bd | ||
|
|
012e5c09ed | ||
|
|
84c9532456 | ||
|
|
aa7c9bca46 | ||
|
|
3d00b4ffbe | ||
|
|
5f33aa7fd4 | ||
|
|
7ea2204dfd | ||
|
|
be8cadf5ac | ||
|
|
fc7192c748 | ||
|
|
c517b59138 | ||
|
|
5c6b6b3919 | ||
|
|
87553ac685 | ||
|
|
384f396bbb | ||
|
|
d40560bd49 | ||
|
|
0304d6079d | ||
|
|
ae41e64999 | ||
|
|
8c9f32c927 | ||
|
|
5485e8a322 | ||
|
|
67952ca246 | ||
|
|
b4c612ff6d | ||
|
|
e708268067 | ||
|
|
4ddbefb147 | ||
|
|
edabc17ac9 | ||
|
|
c7e8211fae | ||
|
|
dc3f07dd52 | ||
|
|
fae6a5cfb4 | ||
|
|
cafd225797 | ||
|
|
bf77d2973e | ||
|
|
dfd8133431 | ||
|
|
e9d78b8990 | ||
|
|
13532cdffb | ||
|
|
c17620efb7 | ||
|
|
d3b5b1080f | ||
|
|
e4b0c23bb9 | ||
|
|
e61873f335 | ||
|
|
6c24fb6d79 | ||
|
|
bd93551c13 | ||
|
|
6c3719b9b8 | ||
|
|
dc0799a56b | ||
|
|
8c9ea7885a | ||
|
|
e1fca1520b | ||
|
|
c72f3b8efb | ||
|
|
afcd172a94 | ||
|
|
5957700699 | ||
|
|
451d023e81 | ||
|
|
64d166cebf | ||
|
|
2540ac34b3 | ||
|
|
76e5d41a34 | ||
|
|
51b067def7 | ||
|
|
de25e8dd6f | ||
|
|
39ac291ba2 | ||
|
|
722d26c5b9 | ||
|
|
0346760a75 | ||
|
|
a2015143f8 | ||
|
|
071d99a7b4 | ||
|
|
2c32a4e912 | ||
|
|
c66f7235b6 | ||
|
|
e6d8e063c9 | ||
|
|
29526144c9 | ||
|
|
b88bcafddd | ||
|
|
78d844d693 | ||
|
|
49310b53e9 | ||
|
|
4684487ff8 | ||
|
|
89250205dc | ||
|
|
9039919b03 | ||
|
|
df16803f11 | ||
|
|
48327cf5f8 | ||
|
|
2ead125b9d | ||
|
|
23807f11f9 | ||
|
|
cad666d07f | ||
|
|
a0b11051e1 | ||
|
|
b6e6fb9de2 | ||
|
|
4172fd8aff | ||
|
|
d60df6ca69 | ||
|
|
f9ba7377eb | ||
|
|
5d3c47b8ae | ||
|
|
ef9cc860a9 | ||
|
|
5a1ec8aa74 | ||
|
|
ac435f3699 | ||
|
|
20cbf0c710 | ||
|
|
0374dec79c | ||
|
|
41b6bf38c4 | ||
|
|
ddba251449 | ||
|
|
bf76e47648 | ||
|
|
5a3ad35bdd | ||
|
|
7da18274ad | ||
|
|
9b66ae05eb | ||
|
|
d407964625 | ||
|
|
bc254d5a8d | ||
|
|
1d9128a650 | ||
|
|
a1f2e9e4e2 | ||
|
|
8c8188c24f | ||
|
|
e42eaf73b8 | ||
|
|
5444381f7d | ||
|
|
fbd8552c0b | ||
|
|
daa26b2e1e | ||
|
|
dc44679031 | ||
|
|
18473e6819 | ||
|
|
2cbd17b745 | ||
|
|
a51d6b9a74 | ||
|
|
706871c12a | ||
|
|
16c49e965c | ||
|
|
f29e867441 | ||
|
|
4a5f6d8393 | ||
|
|
39e3025077 | ||
|
|
8de2066634 | ||
|
|
dfc312c092 | ||
|
|
ce15dbf31b | ||
|
|
9ee293816d | ||
|
|
b3c1397753 | ||
|
|
93d63538bb | ||
|
|
09c28d2311 | ||
|
|
5b8ed496db | ||
|
|
27b4cf743e | ||
|
|
95938a65b6 | ||
|
|
70780fc4ef | ||
|
|
21d265d12f | ||
|
|
788526d6eb | ||
|
|
a7203e9a17 | ||
|
|
eff50daae4 | ||
|
|
7612db15e0 | ||
|
|
0731ee473d | ||
|
|
3a6a76fe19 | ||
|
|
edb0509194 | ||
|
|
1806be1fd7 | ||
|
|
7769f25869 | ||
|
|
f6e5743f20 | ||
|
|
9ef13696d8 | ||
|
|
c3f635fd12 | ||
|
|
e3d1380435 | ||
|
|
1ef6196de6 | ||
|
|
f83737b538 | ||
|
|
bb1ea4e4d0 | ||
|
|
9cb4de2ea8 | ||
|
|
048cbf60ec | ||
|
|
36f40b4188 | ||
|
|
ea1afb260a | ||
|
|
75ceee23ec | ||
|
|
38f2372ee8 | ||
|
|
b58826da6e | ||
|
|
a3bba92063 | ||
|
|
ebcc6c9341 | ||
|
|
95f765055e | ||
|
|
49aa5ffccc | ||
|
|
2c05e606c7 | ||
|
|
92f1c080c4 | ||
|
|
8f79699e84 | ||
|
|
e7b3e28f76 | ||
|
|
123677b6c6 | ||
|
|
f393c7173c | ||
|
|
0abfb9f1e4 | ||
|
|
379771a5c1 | ||
|
|
2f4a76b3fa | ||
|
|
7d82e199b7 | ||
|
|
de693a08ad | ||
|
|
923091ca62 | ||
|
|
cb65ddedb6 | ||
|
|
7aa75539c9 | ||
|
|
a1a7cf59b3 | ||
|
|
f572cb5f3e | ||
|
|
8f1a31ad13 | ||
|
|
dcf63e3da2 | ||
|
|
153dca8137 | ||
|
|
db97787b15 | ||
|
|
7148fec6e1 | ||
|
|
db6d48f8ca | ||
|
|
6d87a1a604 | ||
|
|
d09d3fa80b | ||
|
|
4c8cd50643 | ||
|
|
eee72f4f27 | ||
|
|
5c414fc7b4 | ||
|
|
127f4446ae | ||
|
|
45f6a7cb4d | ||
|
|
8866bd8663 | ||
|
|
3f9c515f1d | ||
|
|
e9e0fdae37 | ||
|
|
124f7f43ab | ||
|
|
27df44bf44 | ||
|
|
b934a7de6a | ||
|
|
d521c75085 | ||
|
|
5e18b6b878 | ||
|
|
3183ca02b3 | ||
|
|
60a278490f | ||
|
|
b78e74cdf6 | ||
|
|
f61a16074b | ||
|
|
82766d1645 | ||
|
|
49a49e2a2c | ||
|
|
8b9389b468 | ||
|
|
ee17336d64 | ||
|
|
2b768d2fb5 | ||
|
|
4fc63010d4 | ||
|
|
3311ded843 | ||
|
|
135c2e7a71 | ||
|
|
a3a8f600b6 | ||
|
|
23643b99e2 | ||
|
|
764a334952 | ||
|
|
6f163f2d01 | ||
|
|
aae65f8235 | ||
|
|
764ea571eb | ||
|
|
aef719b574 | ||
|
|
73e52e625b | ||
|
|
a967588fe1 | ||
|
|
72e530f062 | ||
|
|
a0b422f99e | ||
|
|
6b92f30324 | ||
|
|
464cfe701c | ||
|
|
070fa03108 | ||
|
|
51abeabb33 | ||
|
|
74b0697da1 | ||
|
|
0ca409bb22 | ||
|
|
527cf25d1b | ||
|
|
a49594e6a5 | ||
|
|
5cd22b41d6 | ||
|
|
934949c514 | ||
|
|
cc61e8d334 | ||
|
|
81be499c49 | ||
|
|
082aa55566 | ||
|
|
c783039557 | ||
|
|
206dfeb879 | ||
|
|
f839e76f4b | ||
|
|
c67151f922 | ||
|
|
aedec5de18 | ||
|
|
3df973a1ea | ||
|
|
0253031d7f | ||
|
|
f4445d4681 | ||
|
|
e399dfa7e6 | ||
|
|
787f95ac3a | ||
|
|
d2f35d46d2 | ||
|
|
b21d078c5d | ||
|
|
032c3fb856 | ||
|
|
bbd79379ce | ||
|
|
d01d544a0a | ||
|
|
01ecd76976 | ||
|
|
26e8ae4bf3 | ||
|
|
3befdbc93d | ||
|
|
c91a890d42 | ||
|
|
3369ab601a | ||
|
|
bfe5b71f19 | ||
|
|
eb25cf65dd | ||
|
|
aa1ca3be64 | ||
|
|
a4e03daeee | ||
|
|
cbd0b9db1d | ||
|
|
621e8e89a5 | ||
|
|
c9eca5ec7e | ||
|
|
05063b76eb | ||
|
|
7d1d34d1eb | ||
|
|
725f471a6a | ||
|
|
0b01a79d9d | ||
|
|
2653ff6536 | ||
|
|
0f30cc8e59 | ||
|
|
e3879cd4d1 | ||
|
|
7a4cdf8688 | ||
|
|
40423a0547 | ||
|
|
682804b1ad | ||
|
|
790e67866c | ||
|
|
8399edb4dc | ||
|
|
55a1c27d6d | ||
|
|
35bf7dc484 | ||
|
|
4d3dfa1dca | ||
|
|
ac4686125f | ||
|
|
fb2ca3bb19 | ||
|
|
c0122aace7 | ||
|
|
c141e92cc4 | ||
|
|
c92567d4fa | ||
|
|
1839bf938a | ||
|
|
5236441be0 | ||
|
|
44d4096a79 | ||
|
|
41280c9d38 | ||
|
|
94620748ab | ||
|
|
da70c03845 | ||
|
|
337eb0f27b | ||
|
|
26a63c4baf | ||
|
|
8b03890f2a | ||
|
|
df3c76fa72 | ||
|
|
cea4157402 | ||
|
|
61731e2c2e | ||
|
|
5c1611c484 | ||
|
|
a502965d19 | ||
|
|
b55764db56 | ||
|
|
510897f672 | ||
|
|
2689fd17d0 | ||
|
|
75e3949cec | ||
|
|
a7bb4b7104 | ||
|
|
9bcb2ac094 | ||
|
|
9ab110277a | ||
|
|
d1506bcdae | ||
|
|
00c38c96cd | ||
|
|
9798d4ff6a | ||
|
|
5801b29ede | ||
|
|
22ed022787 | ||
|
|
f7e7ecf5ae | ||
|
|
c116d3f453 | ||
|
|
d9b3d263ae | ||
|
|
7c54adec9d | ||
|
|
68abd91fc2 | ||
|
|
4d2e42d244 | ||
|
|
5a87a6c502 | ||
|
|
9a265a0437 | ||
|
|
d8ca15ceb3 | ||
|
|
a17f718517 | ||
|
|
3589dda8ee | ||
|
|
21f8e4d55b | ||
|
|
811e0123c9 | ||
|
|
47c4516060 | ||
|
|
16450e2133 | ||
|
|
5678742810 | ||
|
|
29ce3bd05e | ||
|
|
b3d58f4f0c | ||
|
|
76d551a238 | ||
|
|
5467c4b1b8 | ||
|
|
e48d277440 | ||
|
|
13913334b6 | ||
|
|
7f60725c88 | ||
|
|
d55fb36182 | ||
|
|
41205aef20 | ||
|
|
aeadbc1d58 | ||
|
|
bd12ade426 | ||
|
|
f9c26089cd | ||
|
|
7ddb57078c | ||
|
|
3e7f1275d8 | ||
|
|
e963938016 | ||
|
|
9d1da81557 | ||
|
|
91f557ac9e | ||
|
|
452826bd61 | ||
|
|
84564bb7fb | ||
|
|
bf7647c737 | ||
|
|
1f98d7e5ec | ||
|
|
e4486f4c17 | ||
|
|
65daa23a74 | ||
|
|
523a30afb4 | ||
|
|
1d0de4584e | ||
|
|
7aac124407 | ||
|
|
03e8b664ac | ||
|
|
312fcea5f1 | ||
|
|
66883ae37c | ||
|
|
9d05653f5b | ||
|
|
644ebd0a4f | ||
|
|
1033bfcfe5 | ||
|
|
2a075d929a | ||
|
|
d93d234c71 | ||
|
|
7fe9ae8a04 | ||
|
|
87cf1ed7cb | ||
|
|
a0ba5c8a57 | ||
|
|
d7208a15d9 | ||
|
|
debde0c67a | ||
|
|
97db55156a | ||
|
|
9d3477d465 | ||
|
|
41147483d8 | ||
|
|
ca517784ed | ||
|
|
4dd3be1568 | ||
|
|
7412d97bf3 | ||
|
|
ab7b2da83b | ||
|
|
19e26729a8 | ||
|
|
0101365ebc | ||
|
|
1f56e63f9c | ||
|
|
883a30c7ad | ||
|
|
6e151a9f8b | ||
|
|
321bb299b1 | ||
|
|
2ca18340c7 | ||
|
|
74d4237913 | ||
|
|
a4f9b9208d | ||
|
|
8fd65b7365 | ||
|
|
4f4d0bf6aa | ||
|
|
873e2aed94 | ||
|
|
89d485e188 | ||
|
|
2e9870014f | ||
|
|
528529c0d1 | ||
|
|
9bddec2dfd | ||
|
|
f986487df9 | ||
|
|
031af000e6 | ||
|
|
0512fac3aa | ||
|
|
99461a70e6 | ||
|
|
adbbb15a92 | ||
|
|
d85a4c9ad4 | ||
|
|
41baea780a | ||
|
|
a165884bcb | ||
|
|
456adc5d0b | ||
|
|
cfc42906b9 | ||
|
|
738d657c8e | ||
|
|
a51452ee7c | ||
|
|
5d2a41082a | ||
|
|
f9dd00b79b | ||
|
|
898244d04d | ||
|
|
33334830cc | ||
|
|
8503350bfd | ||
|
|
a4bb2aaf12 | ||
|
|
da443045bf | ||
|
|
b9927cd48d | ||
|
|
4272e8196a | ||
|
|
140f9d05df | ||
|
|
9222733243 | ||
|
|
5838c56c4e | ||
|
|
7af3f7e881 | ||
|
|
ee81febc89 | ||
|
|
8146bee846 | ||
|
|
53e94378ae | ||
|
|
8592ead0e3 | ||
|
|
67699372f2 | ||
|
|
95a8ced558 | ||
|
|
ae437be6e7 | ||
|
|
1441d9f4ee | ||
|
|
1814e0a260 | ||
|
|
711c5781e6 | ||
|
|
adeb5c2344 | ||
|
|
f0b0277b9d | ||
|
|
a688310b95 | ||
|
|
6fb5fb63e7 | ||
|
|
5330cc5ae9 | ||
|
|
bcc2244fdb | ||
|
|
ad2de95f32 | ||
|
|
e3ffc8784e | ||
|
|
bb35fc3801 | ||
|
|
c804630576 | ||
|
|
e5f3ca1623 | ||
|
|
453dee33ba | ||
|
|
0880787d68 | ||
|
|
cd582e2e3a | ||
|
|
aebd9319f5 | ||
|
|
de6cbb0f45 | ||
|
|
e14dcd0184 | ||
|
|
17ef653903 | ||
|
|
f5d5b5efc0 | ||
|
|
59dbee8f28 | ||
|
|
13f36b3f79 | ||
|
|
719b63ee02 | ||
|
|
38a5698f90 | ||
|
|
4db6971cc4 | ||
|
|
71482bd06c | ||
|
|
c3acf8341b | ||
|
|
a05b60f48e | ||
|
|
8694ecd417 | ||
|
|
1bc48fbf96 | ||
|
|
d45348c167 | ||
|
|
22caa0ee66 | ||
|
|
e6e8ccc855 | ||
|
|
d78522f5e1 | ||
|
|
3da2a618b9 | ||
|
|
100d007271 | ||
|
|
a1a764d807 | ||
|
|
047fa5b2db | ||
|
|
6cb30adf5d | ||
|
|
9eb939e38f | ||
|
|
3e26060979 | ||
|
|
cc60aa7b84 | ||
|
|
c763794ef3 | ||
|
|
bdfdafaec0 | ||
|
|
7737dc6b6c | ||
|
|
1a89465201 | ||
|
|
7a4dcd52c4 | ||
|
|
e8e7a92131 | ||
|
|
7e8c2211d8 | ||
|
|
99694161e1 | ||
|
|
fe3ce45b8e | ||
|
|
7af0883f08 | ||
|
|
f48c21b124 | ||
|
|
abc2e74f2c | ||
|
|
6130c49b83 | ||
|
|
433b58511c | ||
|
|
f0858b7d93 | ||
|
|
3af6c28ab0 | ||
|
|
5c31c7f14c | ||
|
|
b04111c79b | ||
|
|
06ca0079b3 | ||
|
|
ff53c6b49d | ||
|
|
da58458fb7 | ||
|
|
1a21989ad1 | ||
|
|
2610a9c777 | ||
|
|
58cf611497 | ||
|
|
76d54b8914 | ||
|
|
00f944f3f4 | ||
|
|
1269411771 | ||
|
|
d4d8ea6cf2 | ||
|
|
160522c520 | ||
|
|
7024b5ec1b | ||
|
|
5b020035d6 | ||
|
|
fcea7fd4bf | ||
|
|
d22d64f68c | ||
|
|
580ae005f4 | ||
|
|
75ab9d2e8c | ||
|
|
37e5bcad61 | ||
|
|
20679a62fd | ||
|
|
bb5a5bf2ed | ||
|
|
c1db993b92 | ||
|
|
61631e405b | ||
|
|
c19916ff1c | ||
|
|
6fa2e79c1c | ||
|
|
185e0849b1 | ||
|
|
f48b9d364b | ||
|
|
e4f1a7d4c1 | ||
|
|
e02f19ff67 | ||
|
|
72a2110845 | ||
|
|
9baa415249 | ||
|
|
22b840af14 | ||
|
|
61f32d89ca | ||
|
|
3c7da93dfc | ||
|
|
5831616fac | ||
|
|
d7b6d9f124 | ||
|
|
6e7588e9fc | ||
|
|
03cc8248bc | ||
|
|
068df6f2b1 | ||
|
|
0966ba909b | ||
|
|
6c246768e9 | ||
|
|
245978e2b3 | ||
|
|
3aae60bde9 | ||
|
|
7941a24d51 | ||
|
|
bc75bc199b | ||
|
|
91d36122eb | ||
|
|
e004ba63f8 | ||
|
|
f234b6a540 | ||
|
|
36c1e2cc73 | ||
|
|
4a0a09ba3e | ||
|
|
bff8bfea7b | ||
|
|
48bf0d1942 | ||
|
|
04bbb84845 | ||
|
|
1f30a19566 | ||
|
|
51f4578a41 | ||
|
|
311f8cd00f | ||
|
|
ed0ab78048 | ||
|
|
bd3954a5f1 | ||
|
|
94967add7c | ||
|
|
783ab0b611 | ||
|
|
0eec1c1f61 | ||
|
|
b4a3b832dc | ||
|
|
04b44cff2b | ||
|
|
8309755ee3 | ||
|
|
8e7830dd7d | ||
|
|
41a75d404c | ||
|
|
8eb63de201 | ||
|
|
1dec134a6b | ||
|
|
6f6a8e4bb1 | ||
|
|
d32a18d965 | ||
|
|
7d00d47cb6 | ||
|
|
b0853eb119 | ||
|
|
25d29fb389 | ||
|
|
87d216c578 | ||
|
|
bb0ee61870 | ||
|
|
653a9526f5 | ||
|
|
34ac4b25af | ||
|
|
c7b7f9236f | ||
|
|
881779744f | ||
|
|
ddf3356b5d | ||
|
|
ed241ede9d | ||
|
|
94e8f3e2c1 | ||
|
|
2c4a7f48d1 | ||
|
|
af47c23ca1 | ||
|
|
326a001c57 | ||
|
|
fe9e548e89 | ||
|
|
e58ef1f436 | ||
|
|
f249fa6f65 | ||
|
|
68c6b63c9c | ||
|
|
a249597225 | ||
|
|
4906c677af | ||
|
|
2534a9f14a | ||
|
|
5488c2bdeb | ||
|
|
e072ff2d77 | ||
|
|
41dfbc2709 | ||
|
|
04a27d5778 | ||
|
|
964e461597 | ||
|
|
ef2eec4c4a | ||
|
|
bf1d76d853 | ||
|
|
0682cbd554 | ||
|
|
f5191cdd42 | ||
|
|
b1c73208c5 | ||
|
|
ab221a465b | ||
|
|
4ecfa0477d | ||
|
|
bab2de36ad | ||
|
|
70a2067a06 | ||
|
|
4a13c01817 | ||
|
|
151c2b573c | ||
|
|
4cdb3f4c6a | ||
|
|
3cf0384bc5 | ||
|
|
ad2f6ebe93 | ||
|
|
178a429f26 | ||
|
|
f479e914bb | ||
|
|
45441653f6 | ||
|
|
0303558ae1 | ||
|
|
b70e0e3e2b | ||
|
|
5e0c4d7b7a | ||
|
|
6c83308451 | ||
|
|
eeb898179e | ||
|
|
0ea662d8fe | ||
|
|
ea3219fa10 | ||
|
|
fb9203d396 | ||
|
|
23e7542871 | ||
|
|
a40832dffd | ||
|
|
5ba7493613 | ||
|
|
e7b406c127 | ||
|
|
dd1d16f91c | ||
|
|
82e2a19749 | ||
|
|
b73126e6c1 | ||
|
|
cbd93f450e | ||
|
|
35c64be3d7 | ||
|
|
71194d5b4e | ||
|
|
771c530b85 | ||
|
|
96ea70c027 | ||
|
|
ea277d0579 | ||
|
|
10d7cd1520 | ||
|
|
9051322338 | ||
|
|
d26b6103b5 | ||
|
|
f2e7963e1f | ||
|
|
78a6b622fb | ||
|
|
0177bbebe0 | ||
|
|
a3d7e541d3 | ||
|
|
8deed4a9cd | ||
|
|
4617025bd4 | ||
|
|
0693e19605 | ||
|
|
62618acfed | ||
|
|
1feaa43d2e | ||
|
|
92d7d61926 | ||
|
|
265d77d776 | ||
|
|
60b2576ce8 | ||
|
|
90cc58a8fe | ||
|
|
d79f750e30 | ||
|
|
9f9ab01508 | ||
|
|
4dc89c9082 | ||
|
|
48d9fde3b6 | ||
|
|
500349a8bd | ||
|
|
e299f3e510 | ||
|
|
a9ea1a02ed | ||
|
|
19cd5c8881 | ||
|
|
cdb9c661bd | ||
|
|
c3a01c240b | ||
|
|
e2f748e63d | ||
|
|
6114f4644f | ||
|
|
eb664404e1 | ||
|
|
370f645cf0 | ||
|
|
bd5b18a163 | ||
|
|
c51b0c6a41 | ||
|
|
d56f6e75f9 | ||
|
|
c743348872 | ||
|
|
578049bfb6 | ||
|
|
564cd37628 | ||
|
|
60ecf91935 | ||
|
|
7b452e93b2 | ||
|
|
0bc1f7bf8c | ||
|
|
62c2421d85 | ||
|
|
f2edf56d02 | ||
|
|
376e5aeb45 | ||
|
|
3d1c7e0bc1 | ||
|
|
a2603f882d | ||
|
|
0c1dcafc35 | ||
|
|
d5108f8007 | ||
|
|
ebeca9aa04 | ||
|
|
7307d9f7f1 | ||
|
|
eac7cdae1c | ||
|
|
7e548cb133 | ||
|
|
0a61512fc7 | ||
|
|
52fa4f11ac | ||
|
|
479f2010a9 | ||
|
|
55796932c4 | ||
|
|
bfcabd30c5 | ||
|
|
095ea470a1 | ||
|
|
c9b502c72b | ||
|
|
9390eacb7c | ||
|
|
f193ce87bf | ||
|
|
9267adf79a | ||
|
|
cc2f86cb06 | ||
|
|
14c6895135 | ||
|
|
83f6647352 | ||
|
|
f6b3f898de | ||
|
|
792ecee399 | ||
|
|
93a1ef6bdb | ||
|
|
62607b16f8 | ||
|
|
ca60376447 | ||
|
|
3cc4b5db79 | ||
|
|
f8179c83e7 | ||
|
|
b2233f61e4 | ||
|
|
a7e2f776e4 | ||
|
|
de5ba5d0d3 | ||
|
|
50d672892c | ||
|
|
e6154db6e5 | ||
|
|
34e8f57f7d | ||
|
|
4a8c089fa9 | ||
|
|
b3aa5ee247 | ||
|
|
64bf98a7d3 | ||
|
|
e8e38beeb8 | ||
|
|
de96a96ac6 | ||
|
|
ee3ad17163 | ||
|
|
fafd8a5d51 | ||
|
|
c5879f17f8 | ||
|
|
999cbd314c | ||
|
|
a9d34a223a | ||
|
|
3381030ed5 | ||
|
|
2bacc6cfe8 | ||
|
|
973c936514 | ||
|
|
c5121a7fc5 | ||
|
|
37bf0f6b53 | ||
|
|
203d51cdbf | ||
|
|
9669d8eb8b | ||
|
|
1b7571be5b | ||
|
|
614ff2a30e | ||
|
|
5b11671997 | ||
|
|
5158e08901 | ||
|
|
2cb9c7211e | ||
|
|
24e26c95ff | ||
|
|
b8286af8fa | ||
|
|
c539dd5570 | ||
|
|
de76afea99 | ||
|
|
1d3616ae71 | ||
|
|
d76cb440f9 | ||
|
|
7c89d658f7 | ||
|
|
735279c27c | ||
|
|
292c929117 | ||
|
|
d75be22d1f | ||
|
|
40f1b1c665 | ||
|
|
daf42b63c8 | ||
|
|
07da03618f | ||
|
|
d076c875ed | ||
|
|
771c7fe863 | ||
|
|
dda51f2801 | ||
|
|
25472bcfa6 | ||
|
|
369454c12a | ||
|
|
1784eacf58 | ||
|
|
e86f5b3b7c | ||
|
|
80ff6cda04 | ||
|
|
dff96cfd95 | ||
|
|
31d244ef78 | ||
|
|
8325a84ab2 | ||
|
|
793839c7d5 | ||
|
|
23cf87dbc0 | ||
|
|
7171de336d | ||
|
|
e206cfe6d6 | ||
|
|
1a71cc9223 | ||
|
|
ed6fcf5ae7 | ||
|
|
bb31693a4d | ||
|
|
d15c8b16f3 | ||
|
|
8b9c932b80 | ||
|
|
c10e8f5f9a | ||
|
|
cd27e43994 | ||
|
|
20614cf64b | ||
|
|
c69c02bcb3 | ||
|
|
21b177cbb4 | ||
|
|
31a7e48768 | ||
|
|
fd3d60ed7d | ||
|
|
e2e369a463 | ||
|
|
0c3304f041 | ||
|
|
0d4b9b4bce | ||
|
|
1192bf6a87 | ||
|
|
2366a91e8d | ||
|
|
6ff17d16f0 | ||
|
|
06b7116692 | ||
|
|
3c3ea0f3e1 | ||
|
|
db4d6511d6 | ||
|
|
6e42a67268 | ||
|
|
fd066e5eef | ||
|
|
3dd0c44410 | ||
|
|
12b42854e4 | ||
|
|
2fcb6d0c7c | ||
|
|
68e863723a | ||
|
|
d0b37d0f9a | ||
|
|
a0a1353445 | ||
|
|
2c4e46c630 | ||
|
|
a989e296b0 | ||
|
|
26648dbcd2 | ||
|
|
147d759d35 | ||
|
|
f6b07c5609 | ||
|
|
29fa7a053f | ||
|
|
f380f245c6 | ||
|
|
1824a30cde | ||
|
|
8ab86fd6bb | ||
|
|
2c7bdc54c1 | ||
|
|
39b57da42b | ||
|
|
a98a9fd97a | ||
|
|
093a5c1019 | ||
|
|
ba25acaab9 | ||
|
|
9783802370 | ||
|
|
47bb02ac08 | ||
|
|
1f952d81aa | ||
|
|
0632019e44 | ||
|
|
bc14d0f580 | ||
|
|
4bc3998010 | ||
|
|
cd9c2d1988 | ||
|
|
5d597c22bf | ||
|
|
574144f9b1 | ||
|
|
b26bee3524 | ||
|
|
c835cf7829 | ||
|
|
a1d819dcb6 | ||
|
|
2234cc9334 | ||
|
|
cabd1506b7 | ||
|
|
a7c2e321bf | ||
|
|
31b75179bd | ||
|
|
06152f3131 | ||
|
|
c82f8c997f | ||
|
|
6725cc6f61 | ||
|
|
7e9639052b | ||
|
|
21bd5ba376 | ||
|
|
f06840f4b8 | ||
|
|
11c3d6d056 | ||
|
|
814a566845 | ||
|
|
28d5fb1822 | ||
|
|
cd3c031df1 | ||
|
|
a6db0f6fd9 | ||
|
|
c80f6e8285 | ||
|
|
c6d779853a | ||
|
|
1b720a504c | ||
|
|
72f8854a7a | ||
|
|
097d195f00 | ||
|
|
807da8f696 | ||
|
|
a8ca6b6fcb | ||
|
|
85e2e14c81 | ||
|
|
f3bcaf2710 | ||
|
|
f9dba9266f | ||
|
|
00f7df3982 | ||
|
|
5ae45ddd55 | ||
|
|
4e0a3da01e | ||
|
|
541a99bbc5 | ||
|
|
f62008aba4 | ||
|
|
74f7415f84 | ||
|
|
1e1e079b65 | ||
|
|
0b4f808b2d | ||
|
|
ae4af99c59 | ||
|
|
0b17556fa4 | ||
|
|
e6ebc347e5 | ||
|
|
f2323a9d19 | ||
|
|
ce53fe5e31 | ||
|
|
f1d359b3e7 | ||
|
|
da99f3bc2a | ||
|
|
fec8dd74af | ||
|
|
f598e0d0d5 | ||
|
|
656d2494b0 | ||
|
|
6d07d58f37 | ||
|
|
b873a77409 | ||
|
|
dd5270f620 | ||
|
|
216f895953 | ||
|
|
6acb87b7ea | ||
|
|
acdccd697c | ||
|
|
cecc4b1f6d | ||
|
|
f93f115e13 | ||
|
|
debcf086b5 | ||
|
|
c6db974962 | ||
|
|
e9b0b0c42e | ||
|
|
ebee1a02fd | ||
|
|
cd5a26398a | ||
|
|
5e8a614d82 | ||
|
|
bad601edb1 | ||
|
|
58297219a8 | ||
|
|
087b191e5b | ||
|
|
de4468a15a | ||
|
|
b73de087d2 | ||
|
|
39fd092055 | ||
|
|
dec88bd601 | ||
|
|
bc9975baa1 | ||
|
|
d2d401883e | ||
|
|
3b3ac1688a | ||
|
|
6bb0ca22d0 | ||
|
|
e157bc7b97 | ||
|
|
578da0f1a7 | ||
|
|
8f0dd0b0c6 | ||
|
|
9e05bc4fad | ||
|
|
4cee341ce5 | ||
|
|
c40192aa46 | ||
|
|
651748cd4e | ||
|
|
9e8f8357b1 | ||
|
|
3cc4c07fa1 | ||
|
|
8ed52af203 | ||
|
|
8e9a941b5d | ||
|
|
4538a4d33a | ||
|
|
a8469456ce | ||
|
|
6ca25f913a | ||
|
|
da74c7df8a | ||
|
|
278aa87753 | ||
|
|
0f77294718 | ||
|
|
38df160a5e | ||
|
|
dc61a3307c | ||
|
|
2724f8d3c5 | ||
|
|
36e1c1eff0 | ||
|
|
81f830fe23 | ||
|
|
6a42482b92 | ||
|
|
8be83c278b | ||
|
|
eb82980cbc | ||
|
|
08a6d28868 | ||
|
|
37afbc8e9d | ||
|
|
12546b3f17 | ||
|
|
70821c5d26 | ||
|
|
2f7725c5a9 | ||
|
|
a96d26c3bd | ||
|
|
a9f37c9238 | ||
|
|
de05139dfc | ||
|
|
239ebcdcb8 | ||
|
|
9e6e62c5e8 | ||
|
|
3a0e9f422e | ||
|
|
ef8beb9310 | ||
|
|
260094a666 | ||
|
|
659f9a8f18 | ||
|
|
cf2e3c8018 | ||
|
|
e88d0579b0 | ||
|
|
a05901e792 | ||
|
|
0928ec5c4c | ||
|
|
ef843d02c0 | ||
|
|
07cdc6bf2f | ||
|
|
5564e4daa2 | ||
|
|
4742cd4a03 | ||
|
|
832c1ef83c | ||
|
|
f5d0fc8672 | ||
|
|
99985f4fab | ||
|
|
4c0c917fb5 | ||
|
|
9e5ac261e2 | ||
|
|
42bb3b5aca | ||
|
|
06abfc4337 | ||
|
|
70bedaf8dd | ||
|
|
c9c54200aa | ||
|
|
c5f948099d | ||
|
|
a78cb59bc3 | ||
|
|
fb5ff24747 | ||
|
|
38683a7fea | ||
|
|
ae1a68500c | ||
|
|
578f842eed | ||
|
|
acb2ede658 | ||
|
|
a7dea95f90 | ||
|
|
1080562d96 | ||
|
|
b8cf5a2347 | ||
|
|
e9706d605a | ||
|
|
4b338c56b5 | ||
|
|
05b1bffeef | ||
|
|
b330b55054 | ||
|
|
9a67e63e9b | ||
|
|
0d629a5385 | ||
|
|
4e6a214581 | ||
|
|
333c591771 | ||
|
|
e6645101ad | ||
|
|
48788986cb | ||
|
|
d400beffb6 | ||
|
|
7762135cb1 | ||
|
|
d0d594df69 | ||
|
|
ed97f2d786 | ||
|
|
f3a724237f | ||
|
|
aeb3a308a8 | ||
|
|
62cc97bc44 | ||
|
|
20928c7a7f | ||
|
|
6f4a865604 | ||
|
|
e7919416d5 | ||
|
|
cf8a512af4 | ||
|
|
3efef7bbbb | ||
|
|
65d8e01529 | ||
|
|
e0dd7a9b4b | ||
|
|
3a32d220dd | ||
|
|
5efaf7b23e | ||
|
|
91375a5447 | ||
|
|
f6574d346c | ||
|
|
b9b29b11d4 | ||
|
|
54d062156d | ||
|
|
4dc24e48d8 | ||
|
|
3a24464c90 | ||
|
|
f2ad9395fc | ||
|
|
61539416e5 | ||
|
|
ecf7c1f3f3 | ||
|
|
d18886ed7c | ||
|
|
07cc529f6e | ||
|
|
01b87bfbfd | ||
|
|
b126f0cb79 | ||
|
|
10c6901a04 | ||
|
|
c26efb111e | ||
|
|
f07cbe0087 | ||
|
|
7d72e196e0 | ||
|
|
9bc935da96 | ||
|
|
d2e3b317ef | ||
|
|
0edd39805b | ||
|
|
15189014d0 | ||
|
|
46ab3ed1ea | ||
|
|
edfbbe8c5a | ||
|
|
9d35295b47 | ||
|
|
b59d07fb5f | ||
|
|
9c0fda2b52 | ||
|
|
2974a96e96 | ||
|
|
ab7fd3d019 | ||
|
|
54c5659496 | ||
|
|
8c0811885f | ||
|
|
291570dfd7 | ||
|
|
f6b9b4cc19 | ||
|
|
a06403ab7c | ||
|
|
e64a95d1d7 | ||
|
|
6939e49643 | ||
|
|
bb18586484 | ||
|
|
09b39a4bf9 | ||
|
|
1d7d639654 | ||
|
|
42ec509574 | ||
|
|
dcef864c1c | ||
|
|
e8cb4f90f4 | ||
|
|
4e0b2e4f77 | ||
|
|
9ab1a980a7 | ||
|
|
a2bcadeb7c | ||
|
|
84a4242e27 | ||
|
|
6373d667d9 | ||
|
|
07d91b8f50 | ||
|
|
5645921a8e | ||
|
|
7840d601e1 | ||
|
|
1893061c0d | ||
|
|
0289cc3ee5 | ||
|
|
9c17ac1c33 | ||
|
|
a35c9d2d9e | ||
|
|
7f739a1371 | ||
|
|
62df78f329 | ||
|
|
a80968aaa7 | ||
|
|
2589a15e0a | ||
|
|
950efde3b8 | ||
|
|
de8d5e4489 | ||
|
|
d6eefa5185 | ||
|
|
5ad0cffbb3 | ||
|
|
89bd00c65e | ||
|
|
be4511d95b | ||
|
|
b17e6058d1 | ||
|
|
99c28a184f | ||
|
|
7ed9adaf49 | ||
|
|
c3ef051657 | ||
|
|
ba149efa4a | ||
|
|
e30a7b3849 | ||
|
|
97ad0483ec | ||
|
|
3981c772a2 | ||
|
|
e762002560 | ||
|
|
061d8dc94f | ||
|
|
b7e7a0df94 | ||
|
|
84e6228f90 | ||
|
|
e3f02fc4d6 | ||
|
|
81bece9dc4 | ||
|
|
db4b05e9d2 | ||
|
|
0b9029c7b5 | ||
|
|
7fd9a65ce5 | ||
|
|
8cd2d52b3d | ||
|
|
d8e8c5504a | ||
|
|
7535f6af51 | ||
|
|
347effcf70 | ||
|
|
2b2176ba24 | ||
|
|
a0b2d9384d | ||
|
|
58cc047ffa | ||
|
|
cfacd90815 | ||
|
|
f46dca910b | ||
|
|
d362dfc359 | ||
|
|
4dff1ead06 | ||
|
|
ec8aa28aea | ||
|
|
e49abd909d | ||
|
|
ce2c700f1d | ||
|
|
dcb9f90a8c | ||
|
|
c936d4f5a3 | ||
|
|
44f9292bfd | ||
|
|
259fae0134 | ||
|
|
04c28824b2 | ||
|
|
c5bf4d1723 | ||
|
|
b72a7f2c22 | ||
|
|
71cef03a4b | ||
|
|
13abde0b0f | ||
|
|
e4850e0a48 | ||
|
|
1c225ff1ec | ||
|
|
1b4e80858d | ||
|
|
4f683edcb9 | ||
|
|
301cc22985 | ||
|
|
834fe6da45 | ||
|
|
35678661d1 | ||
|
|
29398b9869 | ||
|
|
1ef6b0b5f9 | ||
|
|
d1cf246600 | ||
|
|
8b65a75235 | ||
|
|
093e09c732 | ||
|
|
313f7e8173 | ||
|
|
d9e615e696 | ||
|
|
b526067eeb | ||
|
|
5c25a82a2b | ||
|
|
2f224fc26e | ||
|
|
2969f8da36 | ||
|
|
8959d12746 | ||
|
|
6e6574a235 | ||
|
|
bf3eea4a41 | ||
|
|
044594c614 | ||
|
|
aeba346a4e | ||
|
|
779a3d2d70 | ||
|
|
6a8667cc4b | ||
|
|
29972fe54e | ||
|
|
cf9e9e4fcf | ||
|
|
ddd14999d7 | ||
|
|
cfc9c60794 | ||
|
|
71f206446c | ||
|
|
e368465591 | ||
|
|
2f4314257c | ||
|
|
204d32b9fd | ||
|
|
a66f522e77 | ||
|
|
9016b7be90 | ||
|
|
545da61855 | ||
|
|
4c22f2525c | ||
|
|
2e5c4bb5ee | ||
|
|
6a85db1d5d | ||
|
|
2b16b961f4 | ||
|
|
a2531b3073 | ||
|
|
88a2baf19e | ||
|
|
f0766f753c | ||
|
|
8d67aed95a | ||
|
|
678dbd76cb | ||
|
|
ae9269d1b1 | ||
|
|
971c238313 | ||
|
|
e713befd59 | ||
|
|
eb95c75c29 | ||
|
|
a522e2bcbd | ||
|
|
b1a8902e33 | ||
|
|
8c706d2393 | ||
|
|
1b55746213 | ||
|
|
3f5a7809f0 | ||
|
|
ecfd05f788 | ||
|
|
41585f1467 | ||
|
|
3c6a3674a5 | ||
|
|
e4bc31357f | ||
|
|
a9436f9668 | ||
|
|
f90c236fa4 | ||
|
|
c5a2d6a1eb | ||
|
|
9bf41edb88 | ||
|
|
4c5f27301e | ||
|
|
99c9ad69ac | ||
|
|
6866267cf5 | ||
|
|
92307013a7 | ||
|
|
ed81261dc2 | ||
|
|
1409ae026d | ||
|
|
566ca24833 | ||
|
|
844a10911a | ||
|
|
015f6d2536 | ||
|
|
c1fd149118 | ||
|
|
887533018c | ||
|
|
7dfbb6753a | ||
|
|
5c2f69c820 | ||
|
|
0abed8f132 | ||
|
|
158d0ea65e | ||
|
|
cdb31a18f5 | ||
|
|
17f75bbd77 | ||
|
|
d1b54c4195 | ||
|
|
a3e18c95f5 | ||
|
|
693fd6d6f3 | ||
|
|
b3029514c7 | ||
|
|
fd6dc6090e | ||
|
|
a94bc50e25 | ||
|
|
13d5ecb79f | ||
|
|
de0b365bc7 | ||
|
|
b287406c94 | ||
|
|
320865c262 | ||
|
|
e550ea2141 | ||
|
|
19e47a2742 | ||
|
|
ca64db3e54 | ||
|
|
ea57b40d8c | ||
|
|
7c4e7eb225 | ||
|
|
028e950c81 | ||
|
|
d5e118d292 | ||
|
|
5b50f3d294 | ||
|
|
ef5d97194f | ||
|
|
d4f34cbdb6 | ||
|
|
0152b7e534 | ||
|
|
ace5c873a3 | ||
|
|
eb7b76bed3 | ||
|
|
d72a9a3907 | ||
|
|
cfeb9f7cd4 | ||
|
|
a690982a09 | ||
|
|
2949a830d9 | ||
|
|
a757db7a44 | ||
|
|
e73899d083 | ||
|
|
05c45e1b6b | ||
|
|
00027434ad | ||
|
|
60c46d529c | ||
|
|
5b91e6aa59 | ||
|
|
615fb3f387 | ||
|
|
24751675c5 | ||
|
|
0e2e54778d | ||
|
|
f336832395 | ||
|
|
44826da3ee | ||
|
|
e492f4fdc9 | ||
|
|
b786d4ac06 | ||
|
|
15abc40bd0 | ||
|
|
e5ac1bc629 | ||
|
|
d67a99b8f8 | ||
|
|
6816f93ecb | ||
|
|
30ecab05cf | ||
|
|
ff13998205 | ||
|
|
1e1e2166b9 | ||
|
|
fcfa5bcb8d | ||
|
|
284599d249 | ||
|
|
caf2ffc167 | ||
|
|
e73ac0af22 | ||
|
|
53dff59c6a | ||
|
|
1d8c8ea1b0 | ||
|
|
b18acfac83 | ||
|
|
fb1f9c675b | ||
|
|
02cef37128 | ||
|
|
08a84df7d8 | ||
|
|
084ba95b6a | ||
|
|
37c11bfcb2 | ||
|
|
5bba2b7bf0 | ||
|
|
9e5c8a650e | ||
|
|
44dc8210bc | ||
|
|
86d40b774e | ||
|
|
e8ff2f8229 | ||
|
|
1ca7faf7c6 | ||
|
|
eeba0940b6 | ||
|
|
c81f7cebf7 | ||
|
|
4affdd91a7 | ||
|
|
d8153d1263 | ||
|
|
3f7ec9e0c2 | ||
|
|
d0ef14ccd8 | ||
|
|
f6f35f3e49 | ||
|
|
ec0302644f | ||
|
|
0e5f7f8d1b | ||
|
|
3498f3fd2f | ||
|
|
b68cfc8f72 | ||
|
|
af11b7cd26 | ||
|
|
6aad4a4a98 | ||
|
|
d7a4395557 | ||
|
|
4df8b90bfe | ||
|
|
0ded953527 | ||
|
|
5a345ce4c9 | ||
|
|
8f8dc390ca | ||
|
|
52fb5641ce | ||
|
|
f47eea03cc | ||
|
|
432864c323 | ||
|
|
4608216e28 | ||
|
|
befdd2e7ad | ||
|
|
13b73acee6 | ||
|
|
d1800fe86a | ||
|
|
13b7b72865 | ||
|
|
0894114aac | ||
|
|
50ae55d74f | ||
|
|
9e8ed150a3 | ||
|
|
7e682eac22 | ||
|
|
172fbb51a4 | ||
|
|
f4bee77131 | ||
|
|
1dcce05f98 | ||
|
|
58239efa84 | ||
|
|
d42121a426 | ||
|
|
4a031d70a5 | ||
|
|
3a5c512a62 | ||
|
|
1de8f9a92a | ||
|
|
6a6dddfef1 | ||
|
|
8180075d8f | ||
|
|
466fd49ad0 | ||
|
|
8ae8e99b25 | ||
|
|
3dc0066e06 | ||
|
|
826add7b43 | ||
|
|
3bc345d391 | ||
|
|
91b0acd4e4 | ||
|
|
774b4051bc | ||
|
|
ad5e68091f | ||
|
|
280b3275b4 | ||
|
|
a3a87f54df | ||
|
|
8ed912913d | ||
|
|
99068ca605 | ||
|
|
514109fa4e | ||
|
|
f979092f59 | ||
|
|
1d9d1ca7dc | ||
|
|
4df7235b09 | ||
|
|
ee72e5ea91 | ||
|
|
6575891546 | ||
|
|
30c7fbddaa | ||
|
|
e1c97ac393 | ||
|
|
c443422b10 | ||
|
|
2ef2f9bbc3 | ||
|
|
1c98a05b4f | ||
|
|
f637cab5ec | ||
|
|
0e232b4c49 | ||
|
|
3d37260d5c | ||
|
|
ce56f072d4 | ||
|
|
3fc5aba3bc | ||
|
|
92f450cda0 | ||
|
|
76b26a7ee6 | ||
|
|
cdc9eff2fb | ||
|
|
7fb3b19cb8 | ||
|
|
76b9c3c0e4 | ||
|
|
109864c3bf | ||
|
|
522fc51c3a | ||
|
|
3dd66e40f8 | ||
|
|
54dc3e5a71 | ||
|
|
23311c0235 | ||
|
|
6e75a3829a | ||
|
|
8b3373a7cc | ||
|
|
7fc9ca96b2 | ||
|
|
7c3dcf586c | ||
|
|
a1ef1353da | ||
|
|
9ce2c55cc4 | ||
|
|
49fcefbf2d | ||
|
|
7eb0c36a8e | ||
|
|
95dc2d430c | ||
|
|
a3d04fa810 | ||
|
|
26aaf5f53c | ||
|
|
5208d3d091 | ||
|
|
177f7af7d7 | ||
|
|
047f8a7888 | ||
|
|
e302b33d3b | ||
|
|
0c5c76b5cc | ||
|
|
54a7de62e0 | ||
|
|
5b056f563e | ||
|
|
a38972bd2e | ||
|
|
0f3c3920a1 | ||
|
|
f23905f5ee | ||
|
|
e69db958b2 | ||
|
|
ac39e3b98e | ||
|
|
c22f642418 | ||
|
|
c8a47d7b99 | ||
|
|
302d9cb321 | ||
|
|
95a4dc94a4 | ||
|
|
3ca027fa69 | ||
|
|
02ae845bab | ||
|
|
0472c22b08 | ||
|
|
424c6403e1 | ||
|
|
e8b1be6568 | ||
|
|
c759df855d | ||
|
|
e105ce8c65 | ||
|
|
9a813adf0e | ||
|
|
e703824198 | ||
|
|
9eb3973131 | ||
|
|
1f09d9e8b9 | ||
|
|
10edf58bf9 | ||
|
|
426dbf4fff | ||
|
|
e68f1d64a3 | ||
|
|
3f25d10e6f | ||
|
|
537e3b6f1f | ||
|
|
6988a3250b | ||
|
|
4c902c5da5 | ||
|
|
f7c793ce8c | ||
|
|
ac69349822 | ||
|
|
937345f0e0 | ||
|
|
34a77d4a11 | ||
|
|
e3519f571e | ||
|
|
dd4a40ccab | ||
|
|
2d0e8d371a | ||
|
|
a6f391dd68 | ||
|
|
6dff423206 | ||
|
|
87b880d869 | ||
|
|
e53742e554 | ||
|
|
94b88d8f8a | ||
|
|
0b9f8b8340 | ||
|
|
e384c8f5a7 | ||
|
|
8ae74ee4fe | ||
|
|
35a06a4364 | ||
|
|
f9055952a4 | ||
|
|
20e675a080 | ||
|
|
199d107914 | ||
|
|
7812266dea | ||
|
|
53af1f5fdd | ||
|
|
f619d3f9a7 | ||
|
|
64e2dc3529 | ||
|
|
cbd0b91d6b | ||
|
|
f4d7ce72c2 | ||
|
|
3c52c56315 | ||
|
|
c3119d1b29 | ||
|
|
4b5d13fd19 | ||
|
|
0fdceb2281 | ||
|
|
9b2c56815b | ||
|
|
c650a92a1d | ||
|
|
c611cccc06 | ||
|
|
4ea335b974 | ||
|
|
c140216fc3 | ||
|
|
d01d89767a | ||
|
|
9adfec28cf | ||
|
|
bcc400c7d7 | ||
|
|
d80ff58bb8 | ||
|
|
93bb0e9097 | ||
|
|
1c61ec3bd3 | ||
|
|
d3ec9a0669 | ||
|
|
00f0fee211 | ||
|
|
c9cd1ebee2 | ||
|
|
16667e0d40 | ||
|
|
2134e3b4d1 | ||
|
|
a51d7663c6 | ||
|
|
09d71aff49 | ||
|
|
d3361a3f1d | ||
|
|
085400bd17 | ||
|
|
479474d10a | ||
|
|
92fb359108 | ||
|
|
a4b58b1fd7 | ||
|
|
54019d7843 | ||
|
|
a39a0a9d84 | ||
|
|
ddf259f8b7 | ||
|
|
22ce37c0cd | ||
|
|
fd05a1a9cf | ||
|
|
ae091cdb8c | ||
|
|
acce572374 | ||
|
|
6cc3241eed | ||
|
|
1a7e9730e7 | ||
|
|
8f46aa2242 | ||
|
|
ee1d1f3852 | ||
|
|
5a32a55900 | ||
|
|
4327137cb0 | ||
|
|
332aa1d2ef | ||
|
|
89596591e9 | ||
|
|
bbe4f8c6ef | ||
|
|
3c990f6d07 | ||
|
|
4d608c5e7a | ||
|
|
50647fd9b7 | ||
|
|
5f26cd7342 | ||
|
|
2c0913637b | ||
|
|
1065fbc422 | ||
|
|
ce1f58956a | ||
|
|
25d00f2fcb | ||
|
|
27178cfd09 | ||
|
|
029a9293f0 | ||
|
|
1203149cb7 | ||
|
|
0ab5c33ddf | ||
|
|
1209143de7 | ||
|
|
bc2806bd05 | ||
|
|
65614921b5 | ||
|
|
9ee3208cff | ||
|
|
abb2f50e10 |
@@ -11,7 +11,7 @@ root = true
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespaces = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# CoffeeScript
|
||||
#
|
||||
@@ -28,12 +28,12 @@ indent_style = space
|
||||
# Package.json
|
||||
#
|
||||
# This indentation style is the one used by npm.
|
||||
[/package.json]
|
||||
[package.json]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Jade
|
||||
[*.jade]
|
||||
# Pug (Jade)
|
||||
[*.{jade,pug}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
@@ -41,7 +41,7 @@ indent_style = space
|
||||
#
|
||||
# Two spaces seems to be the standard most common style, at least in
|
||||
# Node.js (http://nodeguide.com/style.html#tabs-vs-spaces).
|
||||
[*.js]
|
||||
[*.{js,jsx,ts,tsx}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
|
||||
10
.eslintrc.js
10
.eslintrc.js
@@ -2,11 +2,21 @@ module.exports = {
|
||||
extends: ['standard', 'standard-jsx'],
|
||||
globals: {
|
||||
__DEV__: true,
|
||||
$Dict: true,
|
||||
$Diff: true,
|
||||
$Exact: true,
|
||||
$Keys: true,
|
||||
$PropertyType: true,
|
||||
$Shape: true,
|
||||
},
|
||||
parser: 'babel-eslint',
|
||||
rules: {
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
indent: 'off',
|
||||
'no-var': 'error',
|
||||
'node/no-extraneous-import': 'error',
|
||||
'node/no-extraneous-require': 'error',
|
||||
'prefer-const': 'error',
|
||||
'react/jsx-indent': 'off',
|
||||
},
|
||||
}
|
||||
|
||||
16
.flowconfig
Normal file
16
.flowconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
[ignore]
|
||||
<PROJECT_ROOT>/node_modules/.*
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
|
||||
[lints]
|
||||
|
||||
[options]
|
||||
esproposal.decorators=ignore
|
||||
esproposal.optional_chaining=enable
|
||||
include_warnings=true
|
||||
module.use_strict=true
|
||||
|
||||
[strict]
|
||||
28
.gitignore
vendored
28
.gitignore
vendored
@@ -1,9 +1,31 @@
|
||||
/dist/
|
||||
/coverage/
|
||||
/node_modules/
|
||||
/src/common/intl/locales/index.js
|
||||
/src/common/themes/index.js
|
||||
/lerna-debug.log
|
||||
/lerna-debug.log.*
|
||||
|
||||
/@xen-orchestra/*/dist/
|
||||
/@xen-orchestra/*/node_modules/
|
||||
/packages/*/dist/
|
||||
/packages/*/node_modules/
|
||||
|
||||
/packages/vhd-cli/src/commands/index.js
|
||||
|
||||
/packages/xen-api/examples/node_modules/
|
||||
/packages/xen-api/plot.dat
|
||||
|
||||
/packages/xo-server/.xo-server.*
|
||||
/packages/xo-server/src/api/index.js
|
||||
/packages/xo-server/src/xapi/mixins/index.js
|
||||
/packages/xo-server/src/xo-mixins/index.js
|
||||
|
||||
/packages/xo-server-auth-ldap/ldap.cache.conf
|
||||
|
||||
/packages/xo-web/src/common/intl/locales/index.js
|
||||
/packages/xo-web/src/common/themes/index.js
|
||||
|
||||
npm-debug.log
|
||||
npm-debug.log.*
|
||||
pnpm-debug.log
|
||||
pnpm-debug.log.*
|
||||
yarn-error.log
|
||||
yarn-error.log.*
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
}
|
||||
|
||||
25
.travis.yml
25
.travis.yml
@@ -1,11 +1,26 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '6'
|
||||
#- '4' # npm 3's flat tree is needed because some packages do not
|
||||
# declare their deps correctly (e.g. chartist-plugin-tooltip)
|
||||
|
||||
cache: yarn
|
||||
#- stable # disable for now due to an issue of indirect dep upath with Node 9
|
||||
- 8
|
||||
- 6
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
sudo: false
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- qemu-utils
|
||||
- blktap-utils
|
||||
- vmdk-stream-converter
|
||||
|
||||
before_install:
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
|
||||
script:
|
||||
- yarn run test
|
||||
- yarn run test-integration
|
||||
|
||||
63
@xen-orchestra/babel-config/index.js
Normal file
63
@xen-orchestra/babel-config/index.js
Normal file
@@ -0,0 +1,63 @@
|
||||
'use strict'
|
||||
|
||||
const PLUGINS_RE = /^(?:@babel\/|babel-)plugin-.+$/
|
||||
const PRESETS_RE = /^@babel\/preset-.+$/
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV || 'development'
|
||||
const __PROD__ = NODE_ENV === 'production'
|
||||
const __TEST__ = NODE_ENV === 'test'
|
||||
|
||||
const configs = {
|
||||
'@babel/plugin-proposal-decorators': {
|
||||
legacy: true,
|
||||
},
|
||||
'@babel/preset-env' (pkg) {
|
||||
return {
|
||||
debug: !__TEST__,
|
||||
loose: true,
|
||||
shippedProposals: true,
|
||||
targets: __PROD__
|
||||
? (() => {
|
||||
let node = (pkg.engines || {}).node
|
||||
if (node !== undefined) {
|
||||
const trimChars = '^=>~'
|
||||
while (trimChars.includes(node[0])) {
|
||||
node = node.slice(1)
|
||||
}
|
||||
return { node: node }
|
||||
}
|
||||
})()
|
||||
: { browsers: '', node: 'current' },
|
||||
useBuiltIns: '@babel/polyfill' in (pkg.dependencies || {}) && 'usage',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const getConfig = (key, ...args) => {
|
||||
const config = configs[key]
|
||||
return config === undefined
|
||||
? {}
|
||||
: typeof config === 'function'
|
||||
? config(...args)
|
||||
: config
|
||||
}
|
||||
|
||||
module.exports = function (pkg, plugins, presets) {
|
||||
plugins === undefined && (plugins = {})
|
||||
presets === undefined && (presets = {})
|
||||
|
||||
Object.keys(pkg.devDependencies || {}).forEach(name => {
|
||||
if (!(name in presets) && PLUGINS_RE.test(name)) {
|
||||
plugins[name] = getConfig(name, pkg)
|
||||
} else if (!(name in presets) && PRESETS_RE.test(name)) {
|
||||
presets[name] = getConfig(name, pkg)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
comments: !__PROD__,
|
||||
ignore: __TEST__ ? undefined : [/\.spec\.js$/],
|
||||
plugins: Object.keys(plugins).map(plugin => [plugin, plugins[plugin]]),
|
||||
presets: Object.keys(presets).map(preset => [preset, presets[preset]]),
|
||||
}
|
||||
}
|
||||
11
@xen-orchestra/babel-config/package.json
Normal file
11
@xen-orchestra/babel-config/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/babel-config",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/babel-config",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
}
|
||||
}
|
||||
3
@xen-orchestra/cron/.babelrc.js
Normal file
3
@xen-orchestra/cron/.babelrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
24
@xen-orchestra/cron/.npmignore
Normal file
24
@xen-orchestra/cron/.npmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
/benchmark/
|
||||
/benchmarks/
|
||||
*.bench.js
|
||||
*.bench.js.map
|
||||
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/fixture/
|
||||
/fixtures/
|
||||
*.fixture.js
|
||||
*.fixture.js.map
|
||||
*.fixtures.js
|
||||
*.fixtures.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
__snapshots__/
|
||||
145
@xen-orchestra/cron/README.md
Normal file
145
@xen-orchestra/cron/README.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# @xen-orchestra/cron [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> Focused, well maintained, cron parser/scheduler
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/cron):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/cron
|
||||
```
|
||||
|
||||
### Pattern syntax
|
||||
|
||||
```
|
||||
<minute> <hour> <day of month> <month> <day of week>
|
||||
```
|
||||
|
||||
|
||||
Each entry can be:
|
||||
|
||||
- a single value
|
||||
- a range (`0-23` or `*/2`)
|
||||
- a list of values/ranges (`1,8-12`)
|
||||
|
||||
A wildcard (`*`) can be used as a shortcut for the whole range
|
||||
(`first-last`).
|
||||
|
||||
Step values can be used in conjunctions with ranges. For instance,
|
||||
`1-7/2` is the same as `1,3,5,7`.
|
||||
|
||||
| Field | Allowed values |
|
||||
|------------------|----------------|
|
||||
| minute | 0-59 |
|
||||
| hour | 0-23 |
|
||||
| day of the month | 1-31 or 3-letter names (`jan`, `feb`, …) |
|
||||
| month | 0-11 |
|
||||
| day of week | 0-7 (0 and 7 both mean Sunday) or 3-letter names (`mon`, `tue`, …) |
|
||||
|
||||
> Note: the month range is 0-11 to be compatible with
|
||||
> [cron](https://github.com/kelektiv/node-cron), it does not appear to
|
||||
> be very standard though.
|
||||
|
||||
### API
|
||||
|
||||
`createSchedule(pattern: string, zone: string = 'utc'): Schedule`
|
||||
|
||||
> Create a new schedule.
|
||||
|
||||
- `pattern`: the pattern to use, see [the syntax](#pattern-syntax)
|
||||
- `zone`: the timezone to use, use `'local'` for the local timezone
|
||||
|
||||
```js
|
||||
import { createSchedule } from '@xen-orchestra/cron'
|
||||
|
||||
const schedule = createSchedule('0 0 * * sun', 'America/New_York')
|
||||
```
|
||||
|
||||
`Schedule#createJob(fn: Function): Job`
|
||||
|
||||
> Create a new job from this schedule.
|
||||
|
||||
- `fn`: function to execute, if it returns a promise, it will be
|
||||
awaited before scheduling the next run.
|
||||
|
||||
```js
|
||||
const job = schedule.createJob(() => {
|
||||
console.log(new Date())
|
||||
})
|
||||
```
|
||||
|
||||
`Schedule#next(n: number): Array<Date>`
|
||||
|
||||
> Returns the next dates matching this schedule.
|
||||
|
||||
- `n`: number of dates to return
|
||||
|
||||
```js
|
||||
schedule.next(2)
|
||||
// [ 2018-02-11T05:00:00.000Z, 2018-02-18T05:00:00.000Z ]
|
||||
```
|
||||
|
||||
`Schedule#startJob(fn: Function): () => void`
|
||||
|
||||
> Start a new job from this schedule and return a function to stop it.
|
||||
|
||||
- `fn`: function to execute, if it returns a promise, it will be
|
||||
awaited before scheduling the next run.
|
||||
|
||||
```js
|
||||
const stopJob = schedule.startJob(() => {
|
||||
console.log(new Date())
|
||||
})
|
||||
stopJob()
|
||||
```
|
||||
|
||||
`Job#start(): void`
|
||||
|
||||
> Start this job.
|
||||
|
||||
```js
|
||||
job.start()
|
||||
```
|
||||
|
||||
`Job#stop(): void`
|
||||
|
||||
> Stop this job.
|
||||
|
||||
```js
|
||||
job.stop()
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> yarn
|
||||
|
||||
# Run the tests
|
||||
> yarn test
|
||||
|
||||
# Continuously compile
|
||||
> yarn dev
|
||||
|
||||
# Continuously run the tests
|
||||
> yarn dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> yarn build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
59
@xen-orchestra/cron/package.json
Normal file
59
@xen-orchestra/cron/package.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@xen-orchestra/cron",
|
||||
"version": "1.0.3",
|
||||
"license": "ISC",
|
||||
"description": "Focused, well maintained, cron parser/scheduler",
|
||||
"keywords": [
|
||||
"cron",
|
||||
"cronjob",
|
||||
"crontab",
|
||||
"job",
|
||||
"parser",
|
||||
"pattern",
|
||||
"schedule",
|
||||
"scheduling",
|
||||
"task"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/cron",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"browserslist": [
|
||||
">2%"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.4",
|
||||
"moment-timezone": "^0.5.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@babel/preset-flow": "7.0.0-beta.49",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run clean",
|
||||
"prepublishOnly": "yarn run build"
|
||||
}
|
||||
}
|
||||
76
@xen-orchestra/cron/src/index.js
Normal file
76
@xen-orchestra/cron/src/index.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import moment from 'moment-timezone'
|
||||
|
||||
import next from './next'
|
||||
import parse from './parse'
|
||||
|
||||
const MAX_DELAY = 2 ** 31 - 1
|
||||
|
||||
class Job {
|
||||
constructor (schedule, fn) {
|
||||
const wrapper = () => {
|
||||
const result = fn()
|
||||
let then
|
||||
if (result != null && typeof (then = result.then) === 'function') {
|
||||
then.call(result, scheduleNext, scheduleNext)
|
||||
} else {
|
||||
scheduleNext()
|
||||
}
|
||||
}
|
||||
const scheduleNext = () => {
|
||||
const delay = schedule._nextDelay()
|
||||
this._timeout =
|
||||
delay < MAX_DELAY
|
||||
? setTimeout(wrapper, delay)
|
||||
: setTimeout(scheduleNext, MAX_DELAY)
|
||||
}
|
||||
|
||||
this._scheduleNext = scheduleNext
|
||||
this._timeout = undefined
|
||||
}
|
||||
|
||||
start () {
|
||||
this.stop()
|
||||
this._scheduleNext()
|
||||
}
|
||||
|
||||
stop () {
|
||||
clearTimeout(this._timeout)
|
||||
}
|
||||
}
|
||||
|
||||
class Schedule {
|
||||
constructor (pattern, zone = 'utc') {
|
||||
this._schedule = parse(pattern)
|
||||
this._createDate =
|
||||
zone.toLowerCase() === 'utc'
|
||||
? moment.utc
|
||||
: zone === 'local' ? moment : () => moment.tz(zone)
|
||||
}
|
||||
|
||||
createJob (fn) {
|
||||
return new Job(this, fn)
|
||||
}
|
||||
|
||||
next (n) {
|
||||
const dates = new Array(n)
|
||||
const schedule = this._schedule
|
||||
let date = this._createDate()
|
||||
for (let i = 0; i < n; ++i) {
|
||||
dates[i] = (date = next(schedule, date)).toDate()
|
||||
}
|
||||
return dates
|
||||
}
|
||||
|
||||
_nextDelay () {
|
||||
const now = this._createDate()
|
||||
return next(this._schedule, now) - now
|
||||
}
|
||||
|
||||
startJob (fn) {
|
||||
const job = this.createJob(fn)
|
||||
job.start()
|
||||
return job.stop.bind(job)
|
||||
}
|
||||
}
|
||||
|
||||
export const createSchedule = (...args) => new Schedule(...args)
|
||||
89
@xen-orchestra/cron/src/next.js
Normal file
89
@xen-orchestra/cron/src/next.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import moment from 'moment-timezone'
|
||||
import sortedIndex from 'lodash/sortedIndex'
|
||||
|
||||
const NEXT_MAPPING = {
|
||||
month: { year: 1 },
|
||||
date: { month: 1 },
|
||||
day: { week: 1 },
|
||||
hour: { day: 1 },
|
||||
minute: { hour: 1 },
|
||||
}
|
||||
|
||||
const getFirst = values => (values !== undefined ? values[0] : 0)
|
||||
|
||||
const setFirstAvailable = (date, unit, values) => {
|
||||
if (values === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const curr = date.get(unit)
|
||||
const next = values[sortedIndex(values, curr) % values.length]
|
||||
if (curr === next) {
|
||||
return
|
||||
}
|
||||
|
||||
const timestamp = +date
|
||||
date.set(unit, next)
|
||||
if (timestamp > +date) {
|
||||
date.add(NEXT_MAPPING[unit])
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// returns the next run, after the passed date
|
||||
export default (schedule, fromDate) => {
|
||||
let date = moment(fromDate)
|
||||
.set({
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
})
|
||||
.add({ minute: 1 })
|
||||
|
||||
const { minute, hour, dayOfMonth, month, dayOfWeek } = schedule
|
||||
setFirstAvailable(date, 'minute', minute)
|
||||
|
||||
if (setFirstAvailable(date, 'hour', hour)) {
|
||||
date.set('minute', getFirst(minute))
|
||||
}
|
||||
|
||||
let loop
|
||||
let i = 0
|
||||
do {
|
||||
loop = false
|
||||
|
||||
if (setFirstAvailable(date, 'month', month)) {
|
||||
date.set({
|
||||
date: 1,
|
||||
hour: getFirst(hour),
|
||||
minute: getFirst(minute),
|
||||
})
|
||||
}
|
||||
|
||||
let newDate = date.clone()
|
||||
if (dayOfMonth === undefined) {
|
||||
if (dayOfWeek !== undefined) {
|
||||
setFirstAvailable(newDate, 'day', dayOfWeek)
|
||||
}
|
||||
} else if (dayOfWeek === undefined) {
|
||||
setFirstAvailable(newDate, 'date', dayOfMonth)
|
||||
} else {
|
||||
const dateDay = newDate.clone()
|
||||
setFirstAvailable(dateDay, 'date', dayOfMonth)
|
||||
setFirstAvailable(newDate, 'day', dayOfWeek)
|
||||
newDate = moment.min(dateDay, newDate)
|
||||
}
|
||||
if (+date !== +newDate) {
|
||||
loop = date.month() !== newDate.month()
|
||||
date = newDate.set({
|
||||
hour: getFirst(hour),
|
||||
minute: getFirst(minute),
|
||||
})
|
||||
}
|
||||
} while (loop && ++i < 5)
|
||||
|
||||
if (loop) {
|
||||
throw new Error('no solutions found for this schedule')
|
||||
}
|
||||
|
||||
return date
|
||||
}
|
||||
48
@xen-orchestra/cron/src/next.spec.js
Normal file
48
@xen-orchestra/cron/src/next.spec.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import moment from 'moment-timezone'
|
||||
|
||||
import next from './next'
|
||||
import parse from './parse'
|
||||
|
||||
const N = (pattern, fromDate = '2018-04-09T06:25') => {
|
||||
const iso = next(parse(pattern), moment.utc(fromDate)).toISOString()
|
||||
return iso.slice(0, iso.lastIndexOf(':'))
|
||||
}
|
||||
|
||||
describe('next()', () => {
|
||||
mapValues(
|
||||
{
|
||||
minutely: ['* * * * *', '2018-04-09T06:26'],
|
||||
hourly: ['@hourly', '2018-04-09T07:00'],
|
||||
daily: ['@daily', '2018-04-10T00:00'],
|
||||
monthly: ['@monthly', '2018-05-01T00:00'],
|
||||
yearly: ['@yearly', '2019-01-01T00:00'],
|
||||
weekly: ['@weekly', '2018-04-15T00:00'],
|
||||
},
|
||||
([pattern, result], title) =>
|
||||
it(title, () => {
|
||||
expect(N(pattern)).toBe(result)
|
||||
})
|
||||
)
|
||||
|
||||
it('select first between month-day and week-day', () => {
|
||||
expect(N('* * 10 * wen')).toBe('2018-04-10T00:00')
|
||||
expect(N('* * 12 * wen')).toBe('2018-04-11T00:00')
|
||||
})
|
||||
|
||||
it('select the last available day of a month', () => {
|
||||
expect(N('* * 29 feb *')).toBe('2020-02-29T00:00')
|
||||
})
|
||||
|
||||
it('fails when no solutions has been found', () => {
|
||||
expect(() => N('0 0 30 feb *')).toThrow(
|
||||
'no solutions found for this schedule'
|
||||
)
|
||||
})
|
||||
|
||||
it('select the first sunday of the month', () => {
|
||||
expect(N('* * * * 0', '2018-03-31T00:00')).toBe('2018-04-01T00:00')
|
||||
})
|
||||
})
|
||||
193
@xen-orchestra/cron/src/parse.js
Normal file
193
@xen-orchestra/cron/src/parse.js
Normal file
@@ -0,0 +1,193 @@
|
||||
const compareNumbers = (a, b) => a - b
|
||||
|
||||
const createParser = ({ fields: [...fields], presets: { ...presets } }) => {
|
||||
const m = fields.length
|
||||
|
||||
for (let j = 0; j < m; ++j) {
|
||||
const field = fields[j]
|
||||
let { aliases } = field
|
||||
if (aliases !== undefined) {
|
||||
let symbols = aliases
|
||||
|
||||
if (Array.isArray(aliases)) {
|
||||
aliases = {}
|
||||
const [start] = field.range
|
||||
symbols.forEach((alias, i) => {
|
||||
aliases[alias] = start + i
|
||||
})
|
||||
} else {
|
||||
symbols = Object.keys(aliases)
|
||||
}
|
||||
|
||||
fields[j] = {
|
||||
...field,
|
||||
aliases,
|
||||
aliasesRegExp: new RegExp(symbols.join('|'), 'y'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let field, i, n, pattern, schedule, values
|
||||
|
||||
const isDigit = c => c >= '0' && c <= '9'
|
||||
const match = c => pattern[i] === c && (++i, true)
|
||||
|
||||
const consumeWhitespaces = () => {
|
||||
let c
|
||||
while ((c = pattern[i]) === ' ' || c === '\t') {
|
||||
++i
|
||||
}
|
||||
}
|
||||
|
||||
const parseInteger = () => {
|
||||
let c
|
||||
const digits = []
|
||||
while (isDigit((c = pattern[i]))) {
|
||||
++i
|
||||
digits.push(c)
|
||||
}
|
||||
if (digits.length === 0) {
|
||||
throw new SyntaxError(`${field.name}: missing integer at character ${i}`)
|
||||
}
|
||||
return Number.parseInt(digits.join(''), 10)
|
||||
}
|
||||
|
||||
const parseValue = () => {
|
||||
let value
|
||||
|
||||
const { aliasesRegExp } = field
|
||||
if (aliasesRegExp === undefined || isDigit(pattern[i])) {
|
||||
value = parseInteger()
|
||||
const { post } = field
|
||||
if (post !== undefined) {
|
||||
value = post(value)
|
||||
}
|
||||
} else {
|
||||
aliasesRegExp.lastIndex = i
|
||||
const matches = aliasesRegExp.exec(pattern)
|
||||
if (matches === null) {
|
||||
throw new SyntaxError(
|
||||
`${field.name}: missing alias or integer at character ${i}`
|
||||
)
|
||||
}
|
||||
const [alias] = matches
|
||||
i += alias.length
|
||||
value = field.aliases[alias]
|
||||
}
|
||||
|
||||
const { range } = field
|
||||
if (value < range[0] || value > range[1]) {
|
||||
throw new SyntaxError(
|
||||
`${field.name}: ${value} is not between ${range[0]} and ${range[1]}`
|
||||
)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const parseRange = () => {
|
||||
let end, start, step
|
||||
if (match('*')) {
|
||||
if (!match('/')) {
|
||||
return
|
||||
}
|
||||
;[start, end] = field.range
|
||||
step = parseInteger()
|
||||
} else {
|
||||
start = parseValue()
|
||||
if (!match('-')) {
|
||||
values.add(start)
|
||||
return
|
||||
}
|
||||
end = parseValue()
|
||||
step = match('/') ? parseInteger() : 1
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i += step) {
|
||||
values.add(i)
|
||||
}
|
||||
}
|
||||
|
||||
const parseSequence = () => {
|
||||
do {
|
||||
parseRange()
|
||||
} while (match(','))
|
||||
}
|
||||
|
||||
const parse = p => {
|
||||
{
|
||||
const schedule = presets[p]
|
||||
if (schedule !== undefined) {
|
||||
return typeof schedule === 'string'
|
||||
? (presets[p] = parse(schedule))
|
||||
: schedule
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
i = 0
|
||||
n = p.length
|
||||
pattern = p
|
||||
schedule = {}
|
||||
|
||||
for (let j = 0; j < m; ++j) {
|
||||
consumeWhitespaces()
|
||||
|
||||
field = fields[j]
|
||||
values = new Set()
|
||||
parseSequence()
|
||||
if (values.size !== 0) {
|
||||
schedule[field.name] = Array.from(values).sort(compareNumbers)
|
||||
}
|
||||
}
|
||||
|
||||
consumeWhitespaces()
|
||||
if (i !== n) {
|
||||
throw new SyntaxError(
|
||||
`unexpected character at offset ${i}, expected end`
|
||||
)
|
||||
}
|
||||
|
||||
return schedule
|
||||
} finally {
|
||||
field = pattern = schedule = values = undefined
|
||||
}
|
||||
}
|
||||
|
||||
return parse
|
||||
}
|
||||
|
||||
export default createParser({
|
||||
fields: [
|
||||
{
|
||||
name: 'minute',
|
||||
range: [0, 59],
|
||||
},
|
||||
{
|
||||
name: 'hour',
|
||||
range: [0, 23],
|
||||
},
|
||||
{
|
||||
name: 'dayOfMonth',
|
||||
range: [1, 31],
|
||||
},
|
||||
{
|
||||
aliases: 'jan feb mar apr may jun jul aug sep oct nov dec'.split(' '),
|
||||
name: 'month',
|
||||
range: [0, 11],
|
||||
},
|
||||
{
|
||||
aliases: 'sun mon tue wen thu fri sat'.split(' '),
|
||||
name: 'dayOfWeek',
|
||||
post: value => (value === 7 ? 0 : value),
|
||||
range: [0, 6],
|
||||
},
|
||||
],
|
||||
presets: {
|
||||
'@annually': '0 0 1 jan *',
|
||||
'@daily': '0 0 * * *',
|
||||
'@hourly': '0 * * * *',
|
||||
'@monthly': '0 0 1 * *',
|
||||
'@weekly': '0 0 * * sun',
|
||||
'@yearly': '0 0 1 jan *',
|
||||
},
|
||||
})
|
||||
49
@xen-orchestra/cron/src/parse.spec.js
Normal file
49
@xen-orchestra/cron/src/parse.spec.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import parse from './parse'
|
||||
|
||||
describe('parse()', () => {
|
||||
it('works', () => {
|
||||
expect(parse('0 0-10 */10 jan,2,4-11/3 *')).toEqual({
|
||||
minute: [0],
|
||||
hour: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
dayOfMonth: [1, 11, 21, 31],
|
||||
month: [0, 2, 4, 7, 10],
|
||||
})
|
||||
})
|
||||
|
||||
it('correctly parse months', () => {
|
||||
expect(parse('* * * 0,11 *')).toEqual({
|
||||
month: [0, 11],
|
||||
})
|
||||
expect(parse('* * * jan,dec *')).toEqual({
|
||||
month: [0, 11],
|
||||
})
|
||||
})
|
||||
|
||||
it('correctly parse days', () => {
|
||||
expect(parse('* * * * mon,sun')).toEqual({
|
||||
dayOfWeek: [0, 1],
|
||||
})
|
||||
})
|
||||
|
||||
it('reports missing integer', () => {
|
||||
expect(() => parse('*/a')).toThrow('minute: missing integer at character 2')
|
||||
expect(() => parse('*')).toThrow('hour: missing integer at character 1')
|
||||
})
|
||||
|
||||
it('reports invalid aliases', () => {
|
||||
expect(() => parse('* * * jan-foo *')).toThrow(
|
||||
'month: missing alias or integer at character 10'
|
||||
)
|
||||
})
|
||||
|
||||
it('dayOfWeek: 0 and 7 bind to sunday', () => {
|
||||
expect(parse('* * * * 0')).toEqual({
|
||||
dayOfWeek: [0],
|
||||
})
|
||||
expect(parse('* * * * 7')).toEqual({
|
||||
dayOfWeek: [0],
|
||||
})
|
||||
})
|
||||
})
|
||||
3
@xen-orchestra/fs/.babelrc.js
Normal file
3
@xen-orchestra/fs/.babelrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
54
@xen-orchestra/fs/package.json
Normal file
54
@xen-orchestra/fs/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "@xen-orchestra/fs",
|
||||
"version": "0.1.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "The File System for Xen Orchestra backups.",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"preferGlobal": true,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0-beta.49",
|
||||
"@marsaud/smb2-promise": "^0.2.1",
|
||||
"execa": "^0.10.0",
|
||||
"fs-extra": "^6.0.1",
|
||||
"get-stream": "^3.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
"through2": "^2.0.3",
|
||||
"tmp": "^0.0.33",
|
||||
"xo-remote-parser": "^0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/plugin-proposal-function-bind": "7.0.0-beta.49",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@babel/preset-flow": "7.0.0-beta.49",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^2.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run clean",
|
||||
"prepare": "yarn run build"
|
||||
}
|
||||
}
|
||||
307
@xen-orchestra/fs/src/abstract.js
Normal file
307
@xen-orchestra/fs/src/abstract.js
Normal file
@@ -0,0 +1,307 @@
|
||||
// @flow
|
||||
|
||||
import getStream from 'get-stream'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { fromCallback, fromEvent, ignoreErrors } from 'promise-toolbox'
|
||||
import { type Readable, type Writable } from 'stream'
|
||||
import { parse } from 'xo-remote-parser'
|
||||
|
||||
import { createChecksumStream, validChecksumOfReadStream } from './checksum'
|
||||
|
||||
type Data = Buffer | Readable | string
|
||||
type FileDescriptor = {| fd: mixed, path: string |}
|
||||
type LaxReadable = Readable & Object
|
||||
type LaxWritable = Writable & Object
|
||||
|
||||
type File = FileDescriptor | string
|
||||
|
||||
const checksumFile = file => file + '.checksum'
|
||||
|
||||
export default class RemoteHandlerAbstract {
|
||||
_remote: Object
|
||||
constructor (remote: any) {
|
||||
this._remote = { ...remote, ...parse(remote.url) }
|
||||
if (this._remote.type !== this.type) {
|
||||
throw new Error('Incorrect remote type')
|
||||
}
|
||||
}
|
||||
|
||||
get type (): string {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the handler to sync the state of the effective remote with its' metadata
|
||||
*/
|
||||
async sync (): Promise<mixed> {
|
||||
return this._sync()
|
||||
}
|
||||
|
||||
async _sync (): Promise<mixed> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the resources possibly dedicated to put the remote at work, when it is no more needed
|
||||
*/
|
||||
async forget (): Promise<void> {
|
||||
await this._forget()
|
||||
}
|
||||
|
||||
async _forget (): Promise<void> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async test (): Promise<Object> {
|
||||
const testFileName = `${Date.now()}.test`
|
||||
const data = await fromCallback(cb => randomBytes(1024 * 1024, cb))
|
||||
let step = 'write'
|
||||
try {
|
||||
await this.outputFile(testFileName, data)
|
||||
step = 'read'
|
||||
const read = await this.readFile(testFileName)
|
||||
if (data.compare(read) !== 0) {
|
||||
throw new Error('output and input did not match')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
step,
|
||||
file: testFileName,
|
||||
error: error.message || String(error),
|
||||
}
|
||||
} finally {
|
||||
ignoreErrors.call(this.unlink(testFileName))
|
||||
}
|
||||
}
|
||||
|
||||
async outputFile (file: string, data: Data, options?: Object): Promise<void> {
|
||||
return this._outputFile(file, data, {
|
||||
flags: 'wx',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
async _outputFile (file: string, data: Data, options?: Object): Promise<void> {
|
||||
const stream = await this.createOutputStream(file, options)
|
||||
const promise = fromEvent(stream, 'finish')
|
||||
stream.end(data)
|
||||
await promise
|
||||
}
|
||||
|
||||
async read (
|
||||
file: File,
|
||||
buffer: Buffer,
|
||||
position?: number
|
||||
): Promise<{| bytesRead: number, buffer: Buffer |}> {
|
||||
return this._read(file, buffer, position)
|
||||
}
|
||||
|
||||
_read (
|
||||
file: File,
|
||||
buffer: Buffer,
|
||||
position?: number
|
||||
): Promise<{| bytesRead: number, buffer: Buffer |}> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async readFile (file: string, options?: Object): Promise<Buffer> {
|
||||
return this._readFile(file, options)
|
||||
}
|
||||
|
||||
_readFile (file: string, options?: Object): Promise<Buffer> {
|
||||
return this.createReadStream(file, options).then(getStream.buffer)
|
||||
}
|
||||
|
||||
async rename (
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
{ checksum = false }: Object = {}
|
||||
) {
|
||||
let p = this._rename(oldPath, newPath)
|
||||
if (checksum) {
|
||||
p = Promise.all([
|
||||
p,
|
||||
this._rename(checksumFile(oldPath), checksumFile(newPath)),
|
||||
])
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
async _rename (oldPath: string, newPath: string) {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async list (
|
||||
dir: string = '.',
|
||||
{
|
||||
filter,
|
||||
prependDir = false,
|
||||
}: { filter?: (name: string) => boolean, prependDir?: boolean } = {}
|
||||
): Promise<string[]> {
|
||||
let entries = await this._list(dir)
|
||||
if (filter !== undefined) {
|
||||
entries = entries.filter(filter)
|
||||
}
|
||||
|
||||
if (prependDir) {
|
||||
entries.forEach((entry, i) => {
|
||||
entries[i] = dir + '/' + entry
|
||||
})
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
async _list (dir: string): Promise<string[]> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
createReadStream (
|
||||
file: string,
|
||||
{ checksum = false, ignoreMissingChecksum = false, ...options }: Object = {}
|
||||
): Promise<LaxReadable> {
|
||||
const path = typeof file === 'string' ? file : file.path
|
||||
const streamP = this._createReadStream(file, options).then(stream => {
|
||||
// detect early errors
|
||||
let promise = fromEvent(stream, 'readable')
|
||||
|
||||
// try to add the length prop if missing and not a range stream
|
||||
if (
|
||||
stream.length === undefined &&
|
||||
options.end === undefined &&
|
||||
options.start === undefined
|
||||
) {
|
||||
promise = Promise.all([
|
||||
promise,
|
||||
ignoreErrors.call(
|
||||
this.getSize(file).then(size => {
|
||||
stream.length = size
|
||||
})
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
return promise.then(() => stream)
|
||||
})
|
||||
|
||||
if (!checksum) {
|
||||
return streamP
|
||||
}
|
||||
|
||||
// avoid a unhandled rejection warning
|
||||
ignoreErrors.call(streamP)
|
||||
|
||||
return this.readFile(checksumFile(path)).then(
|
||||
checksum =>
|
||||
streamP.then(stream => {
|
||||
const { length } = stream
|
||||
stream = (validChecksumOfReadStream(
|
||||
stream,
|
||||
String(checksum).trim()
|
||||
): LaxReadable)
|
||||
stream.length = length
|
||||
|
||||
return stream
|
||||
}),
|
||||
error => {
|
||||
if (ignoreMissingChecksum && error && error.code === 'ENOENT') {
|
||||
return streamP
|
||||
}
|
||||
throw error
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async _createReadStream (
|
||||
file: string,
|
||||
options?: Object
|
||||
): Promise<LaxReadable> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async openFile (path: string, flags?: string): Promise<FileDescriptor> {
|
||||
return { fd: await this._openFile(path, flags), path }
|
||||
}
|
||||
|
||||
async _openFile (path: string, flags?: string): Promise<mixed> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async closeFile (fd: FileDescriptor): Promise<void> {
|
||||
await this._closeFile(fd.fd)
|
||||
}
|
||||
|
||||
async _closeFile (fd: mixed): Promise<void> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async refreshChecksum (path: string): Promise<void> {
|
||||
const stream = (await this.createReadStream(path)).pipe(
|
||||
createChecksumStream()
|
||||
)
|
||||
stream.resume() // start reading the whole file
|
||||
await this.outputFile(checksumFile(path), await stream.checksum)
|
||||
}
|
||||
|
||||
async createOutputStream (
|
||||
file: File,
|
||||
{ checksum = false, ...options }: Object = {}
|
||||
): Promise<LaxWritable> {
|
||||
const path = typeof file === 'string' ? file : file.path
|
||||
const streamP = this._createOutputStream(file, {
|
||||
flags: 'wx',
|
||||
...options,
|
||||
})
|
||||
|
||||
if (!checksum) {
|
||||
return streamP
|
||||
}
|
||||
|
||||
const checksumStream = createChecksumStream()
|
||||
const forwardError = error => {
|
||||
checksumStream.emit('error', error)
|
||||
}
|
||||
|
||||
const stream = await streamP
|
||||
stream.on('error', forwardError)
|
||||
checksumStream.pipe(stream)
|
||||
|
||||
// $FlowFixMe
|
||||
checksumStream.checksumWritten = checksumStream.checksum
|
||||
.then(value => this.outputFile(checksumFile(path), value))
|
||||
.catch(forwardError)
|
||||
|
||||
return checksumStream
|
||||
}
|
||||
|
||||
async _createOutputStream (
|
||||
file: mixed,
|
||||
options?: Object
|
||||
): Promise<LaxWritable> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async unlink (file: string, { checksum = true }: Object = {}): Promise<void> {
|
||||
if (checksum) {
|
||||
ignoreErrors.call(this._unlink(checksumFile(file)))
|
||||
}
|
||||
|
||||
await this._unlink(file)
|
||||
}
|
||||
|
||||
async _unlink (file: mixed): Promise<void> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async getSize (file: mixed): Promise<number> {
|
||||
return this._getSize(file)
|
||||
}
|
||||
|
||||
async _getSize (file: mixed): Promise<number> {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
}
|
||||
100
@xen-orchestra/fs/src/checksum.js
Normal file
100
@xen-orchestra/fs/src/checksum.js
Normal file
@@ -0,0 +1,100 @@
|
||||
// @flow
|
||||
|
||||
// $FlowFixMe
|
||||
import through2 from 'through2'
|
||||
import { createHash } from 'crypto'
|
||||
import { defer, fromEvent } from 'promise-toolbox'
|
||||
import { invert } from 'lodash'
|
||||
import { type Readable, type Transform } from 'stream'
|
||||
|
||||
// Format: $<algorithm>$<salt>$<encrypted>
|
||||
//
|
||||
// http://man7.org/linux/man-pages/man3/crypt.3.html#NOTES
|
||||
const ALGORITHM_TO_ID = {
|
||||
md5: '1',
|
||||
sha256: '5',
|
||||
sha512: '6',
|
||||
}
|
||||
|
||||
const ID_TO_ALGORITHM = invert(ALGORITHM_TO_ID)
|
||||
|
||||
// Create a through stream which computes the checksum of all data going
|
||||
// through.
|
||||
//
|
||||
// The `checksum` attribute is a promise which resolves at the end of the stream
|
||||
// with a string representation of the checksum.
|
||||
//
|
||||
// const source = ...
|
||||
// const checksumStream = source.pipe(createChecksumStream())
|
||||
// checksumStream.resume() // make the data flow without an output
|
||||
// console.log(await checksumStream.checksum)
|
||||
export const createChecksumStream = (
|
||||
algorithm: string = 'md5'
|
||||
): Transform & { checksum: Promise<string> } => {
|
||||
const algorithmId = ALGORITHM_TO_ID[algorithm]
|
||||
|
||||
if (!algorithmId) {
|
||||
throw new Error(`unknown algorithm: ${algorithm}`)
|
||||
}
|
||||
|
||||
const hash = createHash(algorithm)
|
||||
const { promise, resolve, reject } = defer()
|
||||
|
||||
const stream = through2(
|
||||
(chunk, enc, callback) => {
|
||||
hash.update(chunk)
|
||||
callback(null, chunk)
|
||||
},
|
||||
callback => {
|
||||
resolve(`$${algorithmId}$$${hash.digest('hex')}`)
|
||||
callback()
|
||||
}
|
||||
).once('error', reject)
|
||||
stream.checksum = promise
|
||||
return stream
|
||||
}
|
||||
|
||||
// Check if the checksum of a readable stream is equals to an expected checksum.
|
||||
// The given stream is wrapped in a stream which emits an error event
|
||||
// if the computed checksum is not equals to the expected checksum.
|
||||
export const validChecksumOfReadStream = (
|
||||
stream: Readable,
|
||||
expectedChecksum: string
|
||||
): Readable & { checksumVerified: Promise<void> } => {
|
||||
const algorithmId = expectedChecksum.slice(
|
||||
1,
|
||||
expectedChecksum.indexOf('$', 1)
|
||||
)
|
||||
|
||||
if (!algorithmId) {
|
||||
throw new Error(`unknown algorithm: ${algorithmId}`)
|
||||
}
|
||||
|
||||
const hash = createHash(ID_TO_ALGORITHM[algorithmId])
|
||||
|
||||
const wrapper: any = stream.pipe(
|
||||
through2(
|
||||
{ highWaterMark: 0 },
|
||||
(chunk, enc, callback) => {
|
||||
hash.update(chunk)
|
||||
callback(null, chunk)
|
||||
},
|
||||
callback => {
|
||||
const checksum = `$${algorithmId}$$${hash.digest('hex')}`
|
||||
|
||||
callback(
|
||||
checksum !== expectedChecksum
|
||||
? new Error(
|
||||
`Bad checksum (${checksum}), expected: ${expectedChecksum}`
|
||||
)
|
||||
: null
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
stream.on('error', error => wrapper.emit('error', error))
|
||||
wrapper.checksumVerified = fromEvent(wrapper, 'end')
|
||||
|
||||
return wrapper
|
||||
}
|
||||
26
@xen-orchestra/fs/src/fs.integ.spec.js
Normal file
26
@xen-orchestra/fs/src/fs.integ.spec.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import rimraf from 'rimraf'
|
||||
import tmp from 'tmp'
|
||||
|
||||
import { fromCallback as pFromCallback } from 'promise-toolbox'
|
||||
import { getHandler } from '.'
|
||||
|
||||
const initialDir = process.cwd()
|
||||
|
||||
beforeEach(async () => {
|
||||
const dir = await pFromCallback(cb => tmp.dir(cb))
|
||||
process.chdir(dir)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const tmpDir = process.cwd()
|
||||
process.chdir(initialDir)
|
||||
await pFromCallback(cb => rimraf(tmpDir, cb))
|
||||
})
|
||||
|
||||
test("fs test doesn't crash", async () => {
|
||||
const handler = getHandler({ url: 'file://' + process.cwd() })
|
||||
const result = await handler.test()
|
||||
expect(result.success).toBeTruthy()
|
||||
})
|
||||
26
@xen-orchestra/fs/src/index.js
Normal file
26
@xen-orchestra/fs/src/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// @flow
|
||||
|
||||
import type RemoteHandler from './abstract'
|
||||
import RemoteHandlerLocal from './local'
|
||||
import RemoteHandlerNfs from './nfs'
|
||||
import RemoteHandlerSmb from './smb'
|
||||
|
||||
export type { default as RemoteHandler } from './abstract'
|
||||
export type Remote = { url: string }
|
||||
|
||||
const HANDLERS = {
|
||||
file: RemoteHandlerLocal,
|
||||
smb: RemoteHandlerSmb,
|
||||
nfs: RemoteHandlerNfs,
|
||||
}
|
||||
|
||||
export const getHandler = (remote: Remote): RemoteHandler => {
|
||||
// FIXME: should be done in xo-remote-parser.
|
||||
const type = remote.url.split('://')[0]
|
||||
|
||||
const Handler = HANDLERS[type]
|
||||
if (!Handler) {
|
||||
throw new Error('Unhandled remote type')
|
||||
}
|
||||
return new Handler(remote)
|
||||
}
|
||||
132
@xen-orchestra/fs/src/local.js
Normal file
132
@xen-orchestra/fs/src/local.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import fs from 'fs-extra'
|
||||
import { dirname, resolve } from 'path'
|
||||
import { noop, startsWith } from 'lodash'
|
||||
|
||||
import RemoteHandlerAbstract from './abstract'
|
||||
|
||||
export default class LocalHandler extends RemoteHandlerAbstract {
|
||||
get type () {
|
||||
return 'file'
|
||||
}
|
||||
|
||||
_getRealPath () {
|
||||
return this._remote.path
|
||||
}
|
||||
|
||||
_getFilePath (file) {
|
||||
const realPath = this._getRealPath()
|
||||
const parts = [realPath]
|
||||
if (file) {
|
||||
parts.push(file)
|
||||
}
|
||||
const path = resolve.apply(null, parts)
|
||||
if (!startsWith(path, realPath)) {
|
||||
throw new Error('Remote path is unavailable')
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
async _sync () {
|
||||
if (this._remote.enabled) {
|
||||
try {
|
||||
const path = this._getRealPath()
|
||||
await fs.ensureDir(path)
|
||||
await fs.access(path, fs.R_OK | fs.W_OK)
|
||||
} catch (exc) {
|
||||
this._remote.enabled = false
|
||||
this._remote.error = exc.message
|
||||
}
|
||||
}
|
||||
return this._remote
|
||||
}
|
||||
|
||||
async _forget () {
|
||||
return noop()
|
||||
}
|
||||
|
||||
async _outputFile (file, data, options) {
|
||||
const path = this._getFilePath(file)
|
||||
await fs.ensureDir(dirname(path))
|
||||
await fs.writeFile(path, data, options)
|
||||
}
|
||||
|
||||
async _read (file, buffer, position) {
|
||||
const needsClose = typeof file === 'string'
|
||||
file = needsClose ? await fs.open(this._getFilePath(file), 'r') : file.fd
|
||||
try {
|
||||
return await fs.read(
|
||||
file,
|
||||
buffer,
|
||||
0,
|
||||
buffer.length,
|
||||
position === undefined ? null : position
|
||||
)
|
||||
} finally {
|
||||
if (needsClose) {
|
||||
await fs.close(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _readFile (file, options) {
|
||||
return fs.readFile(this._getFilePath(file), options)
|
||||
}
|
||||
|
||||
async _rename (oldPath, newPath) {
|
||||
return fs.rename(this._getFilePath(oldPath), this._getFilePath(newPath))
|
||||
}
|
||||
|
||||
async _list (dir = '.') {
|
||||
return fs.readdir(this._getFilePath(dir))
|
||||
}
|
||||
|
||||
async _createReadStream (file, options) {
|
||||
if (typeof file === 'string') {
|
||||
return fs.createReadStream(this._getFilePath(file), options)
|
||||
} else {
|
||||
return fs.createReadStream('', {
|
||||
autoClose: false,
|
||||
...options,
|
||||
fd: file.fd,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async _createOutputStream (file, options) {
|
||||
if (typeof file === 'string') {
|
||||
const path = this._getFilePath(file)
|
||||
await fs.ensureDir(dirname(path))
|
||||
return fs.createWriteStream(path, options)
|
||||
} else {
|
||||
return fs.createWriteStream('', {
|
||||
autoClose: false,
|
||||
...options,
|
||||
fd: file.fd,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async _unlink (file) {
|
||||
return fs.unlink(this._getFilePath(file)).catch(error => {
|
||||
// do not throw if the file did not exist
|
||||
if (error == null || error.code !== 'ENOENT') {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async _getSize (file) {
|
||||
const stats = await fs.stat(
|
||||
this._getFilePath(typeof file === 'string' ? file : file.path)
|
||||
)
|
||||
return stats.size
|
||||
}
|
||||
|
||||
async _openFile (path, flags) {
|
||||
return fs.open(this._getFilePath(path), flags)
|
||||
}
|
||||
|
||||
async _closeFile (fd) {
|
||||
return fs.close(fd)
|
||||
}
|
||||
}
|
||||
96
@xen-orchestra/fs/src/nfs.js
Normal file
96
@xen-orchestra/fs/src/nfs.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import execa from 'execa'
|
||||
import fs from 'fs-extra'
|
||||
import { forEach } from 'lodash'
|
||||
|
||||
import LocalHandler from './local'
|
||||
|
||||
export default class NfsHandler extends LocalHandler {
|
||||
get type () {
|
||||
return 'nfs'
|
||||
}
|
||||
|
||||
_getRealPath () {
|
||||
return `/run/xo-server/mounts/${this._remote.id}`
|
||||
}
|
||||
|
||||
async _loadRealMounts () {
|
||||
let stdout
|
||||
const mounted = {}
|
||||
try {
|
||||
stdout = await execa.stdout('findmnt', [
|
||||
'-P',
|
||||
'-t',
|
||||
'nfs,nfs4',
|
||||
'--output',
|
||||
'SOURCE,TARGET',
|
||||
'--noheadings',
|
||||
])
|
||||
const regex = /^SOURCE="([^:]*):(.*)" TARGET="(.*)"$/
|
||||
forEach(stdout.split('\n'), m => {
|
||||
if (m) {
|
||||
const match = regex.exec(m)
|
||||
mounted[match[3]] = {
|
||||
host: match[1],
|
||||
share: match[2],
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (exc) {
|
||||
// When no mounts are found, the call pretends to fail...
|
||||
if (exc.stderr !== '') {
|
||||
throw exc
|
||||
}
|
||||
}
|
||||
|
||||
this._realMounts = mounted
|
||||
return mounted
|
||||
}
|
||||
|
||||
_matchesRealMount () {
|
||||
return this._getRealPath() in this._realMounts
|
||||
}
|
||||
|
||||
async _mount () {
|
||||
await fs.ensureDir(this._getRealPath())
|
||||
return execa('mount', [
|
||||
'-t',
|
||||
'nfs',
|
||||
'-o',
|
||||
'vers=3',
|
||||
`${this._remote.host}:${this._remote.path}`,
|
||||
this._getRealPath(),
|
||||
])
|
||||
}
|
||||
|
||||
async _sync () {
|
||||
await this._loadRealMounts()
|
||||
if (this._matchesRealMount() && !this._remote.enabled) {
|
||||
try {
|
||||
await this._umount(this._remote)
|
||||
} catch (exc) {
|
||||
this._remote.enabled = true
|
||||
this._remote.error = exc.message
|
||||
}
|
||||
} else if (!this._matchesRealMount() && this._remote.enabled) {
|
||||
try {
|
||||
await this._mount()
|
||||
} catch (exc) {
|
||||
this._remote.enabled = false
|
||||
this._remote.error = exc.message
|
||||
}
|
||||
}
|
||||
return this._remote
|
||||
}
|
||||
|
||||
async _forget () {
|
||||
try {
|
||||
await this._umount(this._remote)
|
||||
} catch (_) {
|
||||
// We have to go on...
|
||||
}
|
||||
}
|
||||
|
||||
async _umount (remote) {
|
||||
await execa('umount', ['--force', this._getRealPath()])
|
||||
}
|
||||
}
|
||||
217
@xen-orchestra/fs/src/smb.js
Normal file
217
@xen-orchestra/fs/src/smb.js
Normal file
@@ -0,0 +1,217 @@
|
||||
import Smb2 from '@marsaud/smb2-promise'
|
||||
import { lastly as pFinally } from 'promise-toolbox'
|
||||
|
||||
import RemoteHandlerAbstract from './abstract'
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
// Normalize the error code for file not found.
|
||||
const normalizeError = error => {
|
||||
const { code } = error
|
||||
|
||||
return code === 'STATUS_OBJECT_NAME_NOT_FOUND' ||
|
||||
code === 'STATUS_OBJECT_PATH_NOT_FOUND'
|
||||
? Object.create(error, {
|
||||
code: {
|
||||
configurable: true,
|
||||
readable: true,
|
||||
value: 'ENOENT',
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
: error
|
||||
}
|
||||
|
||||
export default class SmbHandler extends RemoteHandlerAbstract {
|
||||
constructor (remote) {
|
||||
super(remote)
|
||||
this._forget = noop
|
||||
}
|
||||
|
||||
get type () {
|
||||
return 'smb'
|
||||
}
|
||||
|
||||
_getClient (remote) {
|
||||
return new Smb2({
|
||||
share: `\\\\${remote.host}`,
|
||||
domain: remote.domain,
|
||||
username: remote.username,
|
||||
password: remote.password,
|
||||
autoCloseTimeout: 0,
|
||||
})
|
||||
}
|
||||
|
||||
_getFilePath (file) {
|
||||
if (file === '.') {
|
||||
file = undefined
|
||||
}
|
||||
|
||||
let path = this._remote.path !== '' ? this._remote.path : ''
|
||||
|
||||
// Ensure remote path is a directory.
|
||||
if (path !== '' && path[path.length - 1] !== '\\') {
|
||||
path += '\\'
|
||||
}
|
||||
|
||||
if (file) {
|
||||
path += file.replace(/\//g, '\\')
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
_dirname (file) {
|
||||
const parts = file.split('\\')
|
||||
parts.pop()
|
||||
return parts.join('\\')
|
||||
}
|
||||
|
||||
async _sync () {
|
||||
if (this._remote.enabled) {
|
||||
try {
|
||||
// Check access (smb2 does not expose connect in public so far...)
|
||||
await this.list()
|
||||
} catch (error) {
|
||||
this._remote.enabled = false
|
||||
this._remote.error = error.message
|
||||
}
|
||||
}
|
||||
return this._remote
|
||||
}
|
||||
|
||||
async _outputFile (file, data, options = {}) {
|
||||
const client = this._getClient(this._remote)
|
||||
const path = this._getFilePath(file)
|
||||
const dir = this._dirname(path)
|
||||
|
||||
if (dir) {
|
||||
await client.ensureDir(dir)
|
||||
}
|
||||
|
||||
return client.writeFile(path, data, options)::pFinally(() => {
|
||||
client.close()
|
||||
})
|
||||
}
|
||||
|
||||
async _readFile (file, options = {}) {
|
||||
const client = this._getClient(this._remote)
|
||||
let content
|
||||
|
||||
try {
|
||||
content = await client
|
||||
.readFile(this._getFilePath(file), options)
|
||||
::pFinally(() => {
|
||||
client.close()
|
||||
})
|
||||
} catch (error) {
|
||||
throw normalizeError(error)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
async _rename (oldPath, newPath) {
|
||||
const client = this._getClient(this._remote)
|
||||
|
||||
try {
|
||||
await client
|
||||
.rename(this._getFilePath(oldPath), this._getFilePath(newPath))
|
||||
::pFinally(() => {
|
||||
client.close()
|
||||
})
|
||||
} catch (error) {
|
||||
throw normalizeError(error)
|
||||
}
|
||||
}
|
||||
|
||||
async _list (dir = '.') {
|
||||
const client = this._getClient(this._remote)
|
||||
let list
|
||||
|
||||
try {
|
||||
list = await client.readdir(this._getFilePath(dir))::pFinally(() => {
|
||||
client.close()
|
||||
})
|
||||
} catch (error) {
|
||||
throw normalizeError(error)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
async _createReadStream (file, options = {}) {
|
||||
if (typeof file !== 'string') {
|
||||
file = file.path
|
||||
}
|
||||
const client = this._getClient(this._remote)
|
||||
let stream
|
||||
|
||||
try {
|
||||
// FIXME ensure that options are properly handled by @marsaud/smb2
|
||||
stream = await client.createReadStream(this._getFilePath(file), options)
|
||||
stream.on('end', () => client.close())
|
||||
} catch (error) {
|
||||
throw normalizeError(error)
|
||||
}
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
async _createOutputStream (file, options = {}) {
|
||||
if (typeof file !== 'string') {
|
||||
file = file.path
|
||||
}
|
||||
const client = this._getClient(this._remote)
|
||||
const path = this._getFilePath(file)
|
||||
const dir = this._dirname(path)
|
||||
let stream
|
||||
try {
|
||||
if (dir) {
|
||||
await client.ensureDir(dir)
|
||||
}
|
||||
stream = await client.createWriteStream(path, options) // FIXME ensure that options are properly handled by @marsaud/smb2
|
||||
} catch (err) {
|
||||
client.close()
|
||||
throw err
|
||||
}
|
||||
stream.on('finish', () => client.close())
|
||||
return stream
|
||||
}
|
||||
|
||||
async _unlink (file) {
|
||||
const client = this._getClient(this._remote)
|
||||
|
||||
try {
|
||||
await client.unlink(this._getFilePath(file))::pFinally(() => {
|
||||
client.close()
|
||||
})
|
||||
} catch (error) {
|
||||
throw normalizeError(error)
|
||||
}
|
||||
}
|
||||
|
||||
async _getSize (file) {
|
||||
const client = await this._getClient(this._remote)
|
||||
let size
|
||||
|
||||
try {
|
||||
size = await client
|
||||
.getSize(this._getFilePath(typeof file === 'string' ? file : file.path))
|
||||
::pFinally(() => {
|
||||
client.close()
|
||||
})
|
||||
} catch (error) {
|
||||
throw normalizeError(error)
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
// this is a fake
|
||||
async _openFile (path) {
|
||||
return this._getFilePath(path)
|
||||
}
|
||||
|
||||
async _closeFile (fd) {}
|
||||
}
|
||||
2122
CHANGELOG.md
2122
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at julien.fontanet@vates.fr. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
@@ -6,18 +6,17 @@ Here you can:
|
||||
- propose an enhancement
|
||||
- ask a question
|
||||
|
||||
The template below is only a proposition for your ticket, feel free to
|
||||
change it as appropriate :)
|
||||
Please, respect this template as much as possible, it helps us sort
|
||||
the issues :)
|
||||
-->
|
||||
|
||||
### Context
|
||||
|
||||
- **XO version**: XO appliance / `stable` branch / `next-release` branch
|
||||
|
||||
If from the sources:
|
||||
|
||||
- **Component**: xo-web / xo-server / *unknown*
|
||||
- **Node/npm version**: *just execute `npm version`*
|
||||
- **XO origin**: the sources / XO Appliance
|
||||
- **Versions**:
|
||||
- Node: **FILL HERE**
|
||||
- xo-web: **FILL HERE**
|
||||
- xo-server: **FILL HERE**
|
||||
|
||||
### Expected behavior
|
||||
|
||||
|
||||
19
PULL_REQUEST_TEMPLATE.md
Normal file
19
PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,19 @@
|
||||
### Check list
|
||||
|
||||
> Check items when done or if not relevant
|
||||
|
||||
- [ ] PR reference the relevant issue (e.g. `Fixes #007`)
|
||||
- [ ] if UI changes, a screenshot has been added to the PR
|
||||
- [ ] CHANGELOG updated
|
||||
- [ ] documentation updated
|
||||
|
||||
### Process
|
||||
|
||||
1. create a PR as soon as possible
|
||||
1. mark it as `WiP:` (Work in Progress) if not ready to be merged
|
||||
1. when you want a review, add a reviewer
|
||||
1. if necessary, update your PR, and readd a reviewer
|
||||
|
||||
### List of packages to release
|
||||
|
||||
> No need to mention xo-server and xo-web.
|
||||
82
README.md
82
README.md
@@ -1,91 +1,11 @@
|
||||
# Xen Orchestra Web [](https://go.crisp.im/chat/embed/?website_id=-JzqzzwddSV7bKGtEyAQ) [](https://travis-ci.org/vatesfr/xo-web)
|
||||
# Xen Orchestra [](https://go.crisp.im/chat/embed/?website_id=-JzqzzwddSV7bKGtEyAQ) [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||

|
||||
|
||||
XO-Web is part of [Xen Orchestra](https://github.com/vatesfr/xo), a web interface for XenServer or XAPI enabled hosts.
|
||||
|
||||
It is a web client for [XO-Server](https://github.com/vatesfr/xo-server).
|
||||
|
||||
___
|
||||
|
||||
## Installation
|
||||
|
||||
XOA or manual install procedure is [available here](https://xen-orchestra.com/docs/installation.html)
|
||||
|
||||
## Compilation
|
||||
|
||||
Production build:
|
||||
|
||||
```
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
Development build:
|
||||
|
||||
```
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
### Environment
|
||||
|
||||
#### `NODE_ENV`
|
||||
|
||||
Set to *production* it disables many checks which result in increased
|
||||
performance.
|
||||
|
||||
#### `XOA_PLAN`
|
||||
|
||||
- 1: Free
|
||||
- 2: Starter
|
||||
- 3: Enterprise
|
||||
- 4: Premium
|
||||
- 5: Sources
|
||||
|
||||
```js
|
||||
if (process.env.XOA_PLAN < 5) {
|
||||
console.log('included only in XOA')
|
||||
}
|
||||
|
||||
if (process.env.XOA_PLAN > 3) {
|
||||
console.log('included only in Premium and Sources')
|
||||
}
|
||||
```
|
||||
|
||||
## How to report a bug?
|
||||
|
||||
If you are certain the bug is exclusively related to XO-Web, you may use the [bugtracker of this repository](https://github.com/vatesfr/xo-web/issues).
|
||||
|
||||
Otherwise, please consider using the [bugtracker of the general repository](https://github.com/vatesfr/xo/issues).
|
||||
|
||||
## Process for new release
|
||||
|
||||
```bash
|
||||
# Switch to the stable branch.
|
||||
git checkout stable
|
||||
|
||||
# Fetches latest changes.
|
||||
git pull --ff-only
|
||||
|
||||
# Merge changes of the next-release branch.
|
||||
git merge next-release
|
||||
|
||||
# Increment the version (patch, minor or major).
|
||||
npm version minor
|
||||
|
||||
# Go back to the next-release branch.
|
||||
git checkout next-release
|
||||
|
||||
# Fetches the last changes (the merge and version bump) from stable to
|
||||
# next-release.
|
||||
git merge --ff-only stable
|
||||
|
||||
# Push the changes on git.
|
||||
git push --follow-tags origin stable next-release
|
||||
|
||||
# Publish this release to npm.
|
||||
npm publish
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
AGPL3 © [Vates SAS](http://vates.fr)
|
||||
|
||||
5
babel.config.js
Normal file
5
babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
// Necessary for jest to be able to find the `.babelrc.js` closest to the file
|
||||
// instead of only the one in this directory.
|
||||
babelrcRoots: true,
|
||||
}
|
||||
6
flow-typed/limit-concurrency-decorator.js
vendored
Normal file
6
flow-typed/limit-concurrency-decorator.js
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare module 'limit-concurrency-decorator' {
|
||||
declare function limitConcurrencyDecorator(
|
||||
concurrency: number
|
||||
): <T: Function>(T) => T
|
||||
declare export default typeof limitConcurrencyDecorator
|
||||
}
|
||||
33
flow-typed/lodash.js
vendored
Normal file
33
flow-typed/lodash.js
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
declare module 'lodash' {
|
||||
declare export function countBy<K, V>(
|
||||
object: { [K]: V },
|
||||
iteratee: K | ((V, K) => string)
|
||||
): { [string]: number }
|
||||
declare export function forEach<K, V>(
|
||||
object: { [K]: V },
|
||||
iteratee: (V, K) => void
|
||||
): void
|
||||
declare export function groupBy<K, V>(
|
||||
object: { [K]: V },
|
||||
iteratee: K | ((V, K) => string)
|
||||
): { [string]: V[] }
|
||||
declare export function invert<K, V>(object: { [K]: V }): { [V]: K }
|
||||
declare export function isEmpty(mixed): boolean
|
||||
declare export function keyBy<T>(array: T[], iteratee: string): boolean
|
||||
declare export function last<T>(array?: T[]): T | void
|
||||
declare export function map<T1, T2>(
|
||||
collection: T1[],
|
||||
iteratee: (T1) => T2
|
||||
): T2[]
|
||||
declare export function mapValues<K, V1, V2>(
|
||||
object: { [K]: V1 },
|
||||
iteratee: (V1, K) => V2
|
||||
): { [K]: V2 }
|
||||
declare export function noop(...args: mixed[]): void
|
||||
declare export function some<T>(
|
||||
collection: T[],
|
||||
iteratee: (T, number) => boolean
|
||||
): boolean
|
||||
declare export function sum(values: number[]): number
|
||||
declare export function values<K, V>(object: { [K]: V }): V[]
|
||||
}
|
||||
17
flow-typed/promise-toolbox.js
vendored
Normal file
17
flow-typed/promise-toolbox.js
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
declare module 'promise-toolbox' {
|
||||
declare export class CancelToken {
|
||||
static source(): { cancel: (message: any) => void, token: CancelToken };
|
||||
}
|
||||
declare export function cancelable(Function): Function
|
||||
declare export function defer<T>(): {|
|
||||
promise: Promise<T>,
|
||||
reject: T => void,
|
||||
resolve: T => void,
|
||||
|}
|
||||
declare export function fromCallback<T>(
|
||||
(cb: (error: any, value: T) => void) => void
|
||||
): Promise<T>
|
||||
declare export function fromEvent(emitter: mixed, string): Promise<mixed>
|
||||
declare export function ignoreErrors(): Promise<void>
|
||||
declare export function timeout<T>(delay: number): Promise<T>
|
||||
}
|
||||
2
flow-typed/xo.js
vendored
Normal file
2
flow-typed/xo.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
declare type $Dict<T, K = string> = { [K]: T }
|
||||
257
package.json
257
package.json
@@ -1,219 +1,62 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.15.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
"xen",
|
||||
"orchestra",
|
||||
"xen-orchestra",
|
||||
"web"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xo-web",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xo-web"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4",
|
||||
"npm": ">=3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nraynaud/novnc": "0.6.1",
|
||||
"ansi_up": "^2.0.2",
|
||||
"asap": "^2.0.6",
|
||||
"babel-eslint": "^8.0.3",
|
||||
"babel-plugin-dev": "^1.0.0",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-react-constant-elements": "^6.5.0",
|
||||
"babel-plugin-transform-react-inline-elements": "^6.6.5",
|
||||
"babel-plugin-transform-react-jsx-self": "^6.11.0",
|
||||
"babel-plugin-transform-react-jsx-source": "^6.9.0",
|
||||
"babel-plugin-transform-runtime": "^6.6.0",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babel-register": "^6.26.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"babelify": "^8.0.0",
|
||||
"benchmark": "^2.1.0",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"browserify": "^14.5.0",
|
||||
"bundle-collapser": "^1.3.0",
|
||||
"chartist": "^0.10.1",
|
||||
"chartist-plugin-legend": "^0.6.1",
|
||||
"chartist-plugin-tooltip": "0.0.11",
|
||||
"classnames": "^2.2.3",
|
||||
"complex-matcher": "^0.1.1",
|
||||
"cookies-js": "^1.2.2",
|
||||
"d3": "^4.12.0",
|
||||
"dependency-check": "^2.9.2",
|
||||
"enzyme": "^3.1.1",
|
||||
"enzyme-adapter-react-15": "^1.0.5",
|
||||
"enzyme-to-json": "^3.3.0",
|
||||
"eslint": "^4.13.1",
|
||||
"eslint-config-standard": "^10.2.1",
|
||||
"eslint-config-standard-jsx": "^4.0.2",
|
||||
"@babel/core": "^7.0.0-beta.49",
|
||||
"@babel/register": "^7.0.0-beta.49",
|
||||
"babel-core": "^7.0.0-0",
|
||||
"babel-eslint": "^8.1.2",
|
||||
"babel-jest": "^23.0.1",
|
||||
"benchmark": "^2.1.4",
|
||||
"eslint": "^4.14.0",
|
||||
"eslint-config-standard": "^11.0.0-beta.0",
|
||||
"eslint-config-standard-jsx": "^5.0.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-node": "^5.2.1",
|
||||
"eslint-plugin-node": "^6.0.0",
|
||||
"eslint-plugin-promise": "^3.6.0",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"eslint-plugin-react": "^7.6.1",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"font-mfizz": "^2.4.1",
|
||||
"get-stream": "^3.0.0",
|
||||
"globby": "^7.1.1",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^4.0.0",
|
||||
"gulp-csso": "^3.0.0",
|
||||
"gulp-embedlr": "^0.5.2",
|
||||
"gulp-plumber": "^1.1.0",
|
||||
"gulp-pug": "^3.1.0",
|
||||
"gulp-refresh": "^1.1.0",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"gulp-sourcemaps": "^2.2.3",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"human-format": "^0.9.2",
|
||||
"exec-promise": "^0.7.0",
|
||||
"flow-bin": "^0.73.0",
|
||||
"globby": "^8.0.0",
|
||||
"husky": "^0.14.3",
|
||||
"immutable": "^3.8.2",
|
||||
"index-modules": "^0.3.0",
|
||||
"is-ip": "^2.0.0",
|
||||
"jest": "^22.0.0",
|
||||
"jsonrpc-websocket-client": "^0.2.0",
|
||||
"kindof": "^2.0.0",
|
||||
"later": "^1.2.0",
|
||||
"lint-staged": "^6.0.0",
|
||||
"lodash": "^4.6.1",
|
||||
"loose-envify": "^1.1.0",
|
||||
"make-error": "^1.2.1",
|
||||
"marked": "^0.3.7",
|
||||
"modular-css": "^7.2.0",
|
||||
"moment": "^2.20.0",
|
||||
"moment-timezone": "^0.5.14",
|
||||
"notifyjs": "^3.0.0",
|
||||
"prettier": "^1.9.2",
|
||||
"jest": "^23.0.1",
|
||||
"lodash": "^4.17.4",
|
||||
"prettier": "^1.10.2",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
"prop-types": "^15.6.0",
|
||||
"random-password": "^0.1.2",
|
||||
"react": "^15.4.1",
|
||||
"react-addons-shallow-compare": "^15.6.2",
|
||||
"react-addons-test-utils": "^15.6.2",
|
||||
"react-bootstrap-4": "^0.29.1",
|
||||
"react-chartist": "^0.13.0",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-dnd": "^2.5.4",
|
||||
"react-dnd-html5-backend": "^2.5.4",
|
||||
"react-document-title": "^2.0.2",
|
||||
"react-dom": "^15.4.1",
|
||||
"react-dropzone": "^4.2.3",
|
||||
"react-intl": "^2.4.0",
|
||||
"react-key-handler": "^1.0.1",
|
||||
"react-notify": "^3.0.0",
|
||||
"react-overlays": "^0.8.3",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-router": "^3.0.0",
|
||||
"react-select": "^1.1.0",
|
||||
"react-shortcuts": "^2.0.0",
|
||||
"react-sparklines": "1.6.0",
|
||||
"react-test-renderer": "^15.6.2",
|
||||
"react-virtualized": "^8.0.8",
|
||||
"readable-stream": "^2.3.3",
|
||||
"redux": "^3.7.2",
|
||||
"redux-devtools": "^3.4.1",
|
||||
"redux-devtools-dock-monitor": "^1.1.0",
|
||||
"redux-devtools-log-monitor": "^1.4.0",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.5.4",
|
||||
"semver": "^5.4.1",
|
||||
"styled-components": "^2.3.0",
|
||||
"tar-stream": "^1.5.5",
|
||||
"uglify-es": "^3.2.2",
|
||||
"uncontrollable-input": "^0.0.1",
|
||||
"url-parse": "^1.2.0",
|
||||
"vinyl": "^2.1.0",
|
||||
"watchify": "^3.7.0",
|
||||
"whatwg-fetch": "^2.0.3",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.2.3",
|
||||
"xo-common": "^0.1.1",
|
||||
"xo-lib": "^0.8.0",
|
||||
"xo-remote-parser": "^0.3"
|
||||
"sorted-object": "^2.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"benchmarks": "./tools/run-benchmarks.js 'src/**/*.bench.js'",
|
||||
"build": "npm run build-indexes && NODE_ENV=production gulp build",
|
||||
"build-indexes": "index-modules --auto src",
|
||||
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
|
||||
"dev-test": "jest --watch",
|
||||
"lint-staged-stash": "touch .lint-staged && git stash save --include-untracked --keep-index && true",
|
||||
"lint-staged-unstash": "git stash pop && rm -f .lint-staged && true",
|
||||
"posttest": "eslint --ignore-path .gitignore src/",
|
||||
"precommit": "lint-staged",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "jest"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify",
|
||||
"loose-envify"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"env": {
|
||||
"development": {
|
||||
"plugins": [
|
||||
"transform-react-jsx-self",
|
||||
"transform-react-jsx-source"
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"plugins": [
|
||||
"transform-react-constant-elements",
|
||||
"transform-react-inline-elements"
|
||||
]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"dev",
|
||||
"lodash",
|
||||
"transform-decorators-legacy",
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react",
|
||||
"stage-0"
|
||||
]
|
||||
"engines": {
|
||||
"yarn": "^1.7.0"
|
||||
},
|
||||
"jest": {
|
||||
"setupTestFrameworkScriptFile": "./setup-tests.js",
|
||||
"snapshotSerializers": [
|
||||
"enzyme-to-json/serializer"
|
||||
]
|
||||
"collectCoverage": true,
|
||||
"projects": [
|
||||
"<rootDir>"
|
||||
],
|
||||
"testEnvironment": "node",
|
||||
"testPathIgnorePatterns": [
|
||||
"/dist/",
|
||||
"/xo-web/"
|
||||
],
|
||||
"testRegex": "\\.spec\\.js$",
|
||||
"transform": {
|
||||
"\\.jsx?$": "babel-jest"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"lint-staged-stash",
|
||||
"prettier --write",
|
||||
"eslint --fix",
|
||||
"jest --findRelatedTests --passWithNoTests",
|
||||
"git add",
|
||||
"lint-staged-unstash"
|
||||
]
|
||||
}
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "scripts/run-script --parallel build",
|
||||
"clean": "scripts/run-script --parallel clean",
|
||||
"dev": "scripts/run-script --parallel dev",
|
||||
"dev-test": "jest --bail --watch \"^(?!.*\\.integ\\.spec\\.js$)\"",
|
||||
"posttest": "scripts/run-script test",
|
||||
"precommit": "scripts/lint-staged",
|
||||
"prepare": "scripts/run-script prepare",
|
||||
"pretest": "eslint --ignore-path .gitignore .",
|
||||
"test": "jest \"^(?!.*\\.integ\\.spec\\.js$)\"",
|
||||
"test-integration": "jest \".integ\\.spec\\.js$\""
|
||||
},
|
||||
"workspaces": [
|
||||
"@xen-orchestra/*",
|
||||
"packages/*"
|
||||
]
|
||||
}
|
||||
|
||||
3
packages/complex-matcher/.babelrc.js
Normal file
3
packages/complex-matcher/.babelrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
24
packages/complex-matcher/.npmignore
Normal file
24
packages/complex-matcher/.npmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
/benchmark/
|
||||
/benchmarks/
|
||||
*.bench.js
|
||||
*.bench.js.map
|
||||
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/fixture/
|
||||
/fixtures/
|
||||
*.fixture.js
|
||||
*.fixture.js.map
|
||||
*.fixtures.js
|
||||
*.fixtures.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
__snapshots__/
|
||||
66
packages/complex-matcher/README.md
Normal file
66
packages/complex-matcher/README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# complex-matcher [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> ${pkg.description}
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/complex-matcher):
|
||||
|
||||
```
|
||||
> npm install --save complex-matcher
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import * as CM from 'complex-matcher'
|
||||
|
||||
const characters = [
|
||||
{ name: 'Catwoman', costumeColor: 'black' },
|
||||
{ name: 'Superman', costumeColor: 'blue', hasCape: true },
|
||||
{ name: 'Wonder Woman', costumeColor: 'blue' },
|
||||
]
|
||||
|
||||
const predicate = CM.parse('costumeColor:blue hasCape?').createPredicate()
|
||||
|
||||
characters.filter(predicate)
|
||||
// [
|
||||
// { name: 'Superman', costumeColor: 'blue', hasCape: true },
|
||||
// ]
|
||||
|
||||
new CM.String('foo').createPredicate()
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> yarn
|
||||
|
||||
# Run the tests
|
||||
> yarn test
|
||||
|
||||
# Continuously compile
|
||||
> yarn dev
|
||||
|
||||
# Continuously run the tests
|
||||
> yarn dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> yarn build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
48
packages/complex-matcher/package.json
Normal file
48
packages/complex-matcher/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "complex-matcher",
|
||||
"version": "0.3.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/complex-matcher",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"browserslist": [
|
||||
">2%"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.1",
|
||||
"rimraf": "^2.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
}
|
||||
}
|
||||
12
packages/complex-matcher/src/index.bench.js
Normal file
12
packages/complex-matcher/src/index.bench.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { parse } from './'
|
||||
import { ast, pattern } from './index.fixtures'
|
||||
|
||||
export default ({ benchmark }) => {
|
||||
benchmark('parse', () => {
|
||||
parse(pattern)
|
||||
})
|
||||
|
||||
benchmark('toString', () => {
|
||||
ast.toString()
|
||||
})
|
||||
}
|
||||
15
packages/complex-matcher/src/index.fixtures.js
Normal file
15
packages/complex-matcher/src/index.fixtures.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as CM from './'
|
||||
|
||||
export const pattern =
|
||||
'foo !"\\\\ \\"" name:|(wonderwoman batman) hasCape? age:32'
|
||||
|
||||
export const ast = new CM.And([
|
||||
new CM.String('foo'),
|
||||
new CM.Not(new CM.String('\\ "')),
|
||||
new CM.Property(
|
||||
'name',
|
||||
new CM.Or([new CM.String('wonderwoman'), new CM.String('batman')])
|
||||
),
|
||||
new CM.TruthyProperty('hasCape'),
|
||||
new CM.Property('age', new CM.Number(32)),
|
||||
])
|
||||
531
packages/complex-matcher/src/index.js
Normal file
531
packages/complex-matcher/src/index.js
Normal file
@@ -0,0 +1,531 @@
|
||||
import { isPlainObject, some } from 'lodash'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const RAW_STRING_CHARS = (() => {
|
||||
const chars = { __proto__: null }
|
||||
const add = (a, b = a) => {
|
||||
let i = a.charCodeAt(0)
|
||||
const j = b.charCodeAt(0)
|
||||
while (i <= j) {
|
||||
chars[String.fromCharCode(i++)] = true
|
||||
}
|
||||
}
|
||||
add('$')
|
||||
add('-')
|
||||
add('.')
|
||||
add('0', '9')
|
||||
add('_')
|
||||
add('A', 'Z')
|
||||
add('a', 'z')
|
||||
return chars
|
||||
})()
|
||||
const isRawString = string => {
|
||||
const { length } = string
|
||||
for (let i = 0; i < length; ++i) {
|
||||
if (!(string[i] in RAW_STRING_CHARS)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
class Node {
|
||||
createPredicate () {
|
||||
return value => this.match(value)
|
||||
}
|
||||
}
|
||||
|
||||
export class Null extends Node {
|
||||
match () {
|
||||
return true
|
||||
}
|
||||
|
||||
toString () {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const formatTerms = terms => terms.map(term => term.toString(true)).join(' ')
|
||||
|
||||
export class And extends Node {
|
||||
constructor (children) {
|
||||
super()
|
||||
|
||||
if (children.length === 1) {
|
||||
return children[0]
|
||||
}
|
||||
this.children = children
|
||||
}
|
||||
|
||||
match (value) {
|
||||
return this.children.every(child => child.match(value))
|
||||
}
|
||||
|
||||
toString (isNested) {
|
||||
const terms = formatTerms(this.children)
|
||||
return isNested ? `(${terms})` : terms
|
||||
}
|
||||
}
|
||||
|
||||
export class Comparison extends Node {
|
||||
constructor (operator, value) {
|
||||
super()
|
||||
this._comparator = Comparison.comparators[operator]
|
||||
this._operator = operator
|
||||
this._value = value
|
||||
}
|
||||
|
||||
match (value) {
|
||||
return typeof value === 'number' && this._comparator(value, this._value)
|
||||
}
|
||||
|
||||
toString () {
|
||||
return this._operator + String(this._value)
|
||||
}
|
||||
}
|
||||
Comparison.comparators = {
|
||||
'>': (a, b) => a > b,
|
||||
'>=': (a, b) => a >= b,
|
||||
'<': (a, b) => a < b,
|
||||
'<=': (a, b) => a <= b,
|
||||
}
|
||||
|
||||
export class Or extends Node {
|
||||
constructor (children) {
|
||||
super()
|
||||
|
||||
if (children.length === 1) {
|
||||
return children[0]
|
||||
}
|
||||
this.children = children
|
||||
}
|
||||
|
||||
match (value) {
|
||||
return this.children.some(child => child.match(value))
|
||||
}
|
||||
|
||||
toString () {
|
||||
return `|(${formatTerms(this.children)})`
|
||||
}
|
||||
}
|
||||
|
||||
export class Not extends Node {
|
||||
constructor (child) {
|
||||
super()
|
||||
|
||||
this.child = child
|
||||
}
|
||||
|
||||
match (value) {
|
||||
return !this.child.match(value)
|
||||
}
|
||||
|
||||
toString () {
|
||||
return '!' + this.child.toString(true)
|
||||
}
|
||||
}
|
||||
|
||||
export class NumberNode extends Node {
|
||||
constructor (value) {
|
||||
super()
|
||||
|
||||
this.value = value
|
||||
|
||||
// should not be enumerable for the tests
|
||||
Object.defineProperty(this, 'match', {
|
||||
value: this.match.bind(this),
|
||||
})
|
||||
}
|
||||
|
||||
match (value) {
|
||||
return (
|
||||
value === this.value ||
|
||||
(value !== null && typeof value === 'object' && some(value, this.match))
|
||||
)
|
||||
}
|
||||
|
||||
toString () {
|
||||
return String(this.value)
|
||||
}
|
||||
}
|
||||
export { NumberNode as Number }
|
||||
|
||||
export class Property extends Node {
|
||||
constructor (name, child) {
|
||||
super()
|
||||
|
||||
this.name = name
|
||||
this.child = child
|
||||
}
|
||||
|
||||
match (value) {
|
||||
return value != null && this.child.match(value[this.name])
|
||||
}
|
||||
|
||||
toString () {
|
||||
return `${formatString(this.name)}:${this.child.toString(true)}`
|
||||
}
|
||||
}
|
||||
|
||||
const escapeChar = char => '\\' + char
|
||||
const formatString = value =>
|
||||
Number.isNaN(+value)
|
||||
? isRawString(value) ? value : `"${value.replace(/\\|"/g, escapeChar)}"`
|
||||
: `"${value}"`
|
||||
|
||||
export class StringNode extends Node {
|
||||
constructor (value) {
|
||||
super()
|
||||
|
||||
this.lcValue = value.toLowerCase()
|
||||
this.value = value
|
||||
|
||||
// should not be enumerable for the tests
|
||||
Object.defineProperty(this, 'match', {
|
||||
value: this.match.bind(this),
|
||||
})
|
||||
}
|
||||
|
||||
match (value) {
|
||||
if (typeof value === 'string') {
|
||||
return value.toLowerCase().indexOf(this.lcValue) !== -1
|
||||
}
|
||||
|
||||
if (Array.isArray(value) || isPlainObject(value)) {
|
||||
return some(value, this.match)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
toString () {
|
||||
return formatString(this.value)
|
||||
}
|
||||
}
|
||||
export { StringNode as String }
|
||||
|
||||
export class TruthyProperty extends Node {
|
||||
constructor (name) {
|
||||
super()
|
||||
|
||||
this.name = name
|
||||
}
|
||||
|
||||
match (value) {
|
||||
return value != null && !!value[this.name]
|
||||
}
|
||||
|
||||
toString () {
|
||||
return formatString(this.name) + '?'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// https://gist.github.com/yelouafi/556e5159e869952335e01f6b473c4ec1
|
||||
|
||||
class Failure {
|
||||
constructor (pos, expected) {
|
||||
this.expected = expected
|
||||
this.pos = pos
|
||||
}
|
||||
|
||||
get value () {
|
||||
throw new Error(
|
||||
`parse error: expected ${this.expected} at position ${this.pos}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Success {
|
||||
constructor (pos, value) {
|
||||
this.pos = pos
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
class P {
|
||||
static alt (...parsers) {
|
||||
const { length } = parsers
|
||||
return new P((input, pos, end) => {
|
||||
for (let i = 0; i < length; ++i) {
|
||||
const result = parsers[i]._parse(input, pos, end)
|
||||
if (result instanceof Success) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return new Failure(pos, 'alt')
|
||||
})
|
||||
}
|
||||
|
||||
static grammar (rules) {
|
||||
const grammar = {}
|
||||
Object.keys(rules).forEach(k => {
|
||||
const rule = rules[k]
|
||||
grammar[k] = rule instanceof P ? rule : P.lazy(rule, grammar)
|
||||
})
|
||||
return grammar
|
||||
}
|
||||
|
||||
static lazy (parserCreator, arg) {
|
||||
const parser = new P((input, pos, end) =>
|
||||
(parser._parse = parserCreator(arg)._parse)(input, pos, end)
|
||||
)
|
||||
return parser
|
||||
}
|
||||
|
||||
static regex (regex) {
|
||||
regex = new RegExp(regex.source, 'y')
|
||||
return new P((input, pos) => {
|
||||
regex.lastIndex = pos
|
||||
const matches = regex.exec(input)
|
||||
return matches !== null
|
||||
? new Success(regex.lastIndex, matches[0])
|
||||
: new Failure(pos, regex)
|
||||
})
|
||||
}
|
||||
|
||||
static seq (...parsers) {
|
||||
const { length } = parsers
|
||||
return new P((input, pos, end) => {
|
||||
const values = new Array(length)
|
||||
for (let i = 0; i < length; ++i) {
|
||||
const result = parsers[i]._parse(input, pos, end)
|
||||
if (result instanceof Failure) {
|
||||
return result
|
||||
}
|
||||
pos = result.pos
|
||||
values[i] = result.value
|
||||
}
|
||||
return new Success(pos, values)
|
||||
})
|
||||
}
|
||||
|
||||
static text (text) {
|
||||
const { length } = text
|
||||
return new P(
|
||||
(input, pos) =>
|
||||
input.startsWith(text, pos)
|
||||
? new Success(pos + length, text)
|
||||
: new Failure(pos, `'${text}'`)
|
||||
)
|
||||
}
|
||||
|
||||
constructor (parse) {
|
||||
this._parse = parse
|
||||
}
|
||||
|
||||
map (fn) {
|
||||
return new P((input, pos, end) => {
|
||||
const result = this._parse(input, pos, end)
|
||||
if (result instanceof Success) {
|
||||
result.value = fn(result.value)
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
parse (input, pos = 0, end = input.length) {
|
||||
return this._parse(input, pos, end).value
|
||||
}
|
||||
|
||||
repeat (min = 0, max = Infinity) {
|
||||
return new P((input, pos, end) => {
|
||||
const value = []
|
||||
let result
|
||||
let i = 0
|
||||
while (i < min) {
|
||||
++i
|
||||
result = this._parse(input, pos, end)
|
||||
if (result instanceof Failure) {
|
||||
return result
|
||||
}
|
||||
value.push(result.value)
|
||||
pos = result.pos
|
||||
}
|
||||
while (
|
||||
i < max &&
|
||||
(result = this._parse(input, pos, end)) instanceof Success
|
||||
) {
|
||||
++i
|
||||
value.push(result.value)
|
||||
pos = result.pos
|
||||
}
|
||||
return new Success(pos, value)
|
||||
})
|
||||
}
|
||||
|
||||
skip (otherParser) {
|
||||
return new P((input, pos, end) => {
|
||||
const result = this._parse(input, pos, end)
|
||||
if (result instanceof Failure) {
|
||||
return result
|
||||
}
|
||||
const otherResult = otherParser._parse(input, result.pos, end)
|
||||
if (otherResult instanceof Failure) {
|
||||
return otherResult
|
||||
}
|
||||
result.pos = otherResult.pos
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
P.eof = new P(
|
||||
(input, pos, end) =>
|
||||
pos < end ? new Failure(pos, 'end of input') : new Success(pos)
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const parser = P.grammar({
|
||||
default: r =>
|
||||
P.seq(r.ws, r.term.repeat(), P.eof).map(
|
||||
([, terms]) => (terms.length === 0 ? new Null() : new And(terms))
|
||||
),
|
||||
quotedString: new P((input, pos, end) => {
|
||||
if (input[pos] !== '"') {
|
||||
return new Failure(pos, '"')
|
||||
}
|
||||
++pos
|
||||
|
||||
const value = []
|
||||
let char
|
||||
while (pos < end && (char = input[pos++]) !== '"') {
|
||||
if (char === '\\') {
|
||||
char = input[pos++]
|
||||
}
|
||||
value.push(char)
|
||||
}
|
||||
|
||||
return new Success(pos, value.join(''))
|
||||
}),
|
||||
rawString: new P((input, pos, end) => {
|
||||
let value = ''
|
||||
let c
|
||||
while (pos < end && RAW_STRING_CHARS[(c = input[pos])]) {
|
||||
++pos
|
||||
value += c
|
||||
}
|
||||
return value.length === 0
|
||||
? new Failure(pos, 'a raw string')
|
||||
: new Success(pos, value)
|
||||
}),
|
||||
string: r => P.alt(r.quotedString, r.rawString),
|
||||
term: r =>
|
||||
P.alt(
|
||||
P.seq(P.text('('), r.ws, r.term.repeat(1), P.text(')')).map(
|
||||
_ => new And(_[2])
|
||||
),
|
||||
P.seq(
|
||||
P.text('|'),
|
||||
r.ws,
|
||||
P.text('('),
|
||||
r.ws,
|
||||
r.term.repeat(1),
|
||||
P.text(')')
|
||||
).map(_ => new Or(_[4])),
|
||||
P.seq(P.text('!'), r.ws, r.term).map(_ => new Not(_[2])),
|
||||
P.seq(P.regex(/[<>]=?/), r.rawString).map(([op, val]) => {
|
||||
val = +val
|
||||
if (Number.isNaN(val)) {
|
||||
throw new TypeError('value must be a number')
|
||||
}
|
||||
return new Comparison(op, val)
|
||||
}),
|
||||
P.seq(r.string, r.ws, P.text(':'), r.ws, r.term).map(
|
||||
_ => new Property(_[0], _[4])
|
||||
),
|
||||
P.seq(r.string, P.text('?')).map(_ => new TruthyProperty(_[0])),
|
||||
P.alt(
|
||||
r.quotedString.map(_ => new StringNode(_)),
|
||||
r.rawString.map(str => {
|
||||
const asNum = +str
|
||||
return Number.isNaN(asNum)
|
||||
? new StringNode(str)
|
||||
: new NumberNode(asNum)
|
||||
})
|
||||
)
|
||||
).skip(r.ws),
|
||||
ws: P.regex(/\s*/),
|
||||
}).default
|
||||
export const parse = parser.parse.bind(parser)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _getPropertyClauseStrings = ({ child }) => {
|
||||
if (child instanceof Or) {
|
||||
const strings = []
|
||||
child.children.forEach(child => {
|
||||
if (child instanceof StringNode) {
|
||||
strings.push(child.value)
|
||||
}
|
||||
})
|
||||
return strings
|
||||
}
|
||||
|
||||
if (child instanceof StringNode) {
|
||||
return [child.value]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// Find possible values for property clauses in a and clause.
|
||||
export const getPropertyClausesStrings = node => {
|
||||
if (!node) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (node instanceof Property) {
|
||||
return {
|
||||
[node.name]: _getPropertyClauseStrings(node),
|
||||
}
|
||||
}
|
||||
|
||||
if (node instanceof And) {
|
||||
const strings = {}
|
||||
node.children.forEach(node => {
|
||||
if (node instanceof Property) {
|
||||
const { name } = node
|
||||
const values = strings[name]
|
||||
if (values) {
|
||||
values.push.apply(values, _getPropertyClauseStrings(node))
|
||||
} else {
|
||||
strings[name] = _getPropertyClauseStrings(node)
|
||||
}
|
||||
}
|
||||
})
|
||||
return strings
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const setPropertyClause = (node, name, child) => {
|
||||
const property =
|
||||
child &&
|
||||
new Property(
|
||||
name,
|
||||
typeof child === 'string' ? new StringNode(child) : child
|
||||
)
|
||||
|
||||
if (node === undefined) {
|
||||
return property
|
||||
}
|
||||
|
||||
const children = (node instanceof And ? node.children : [node]).filter(
|
||||
child => !(child instanceof Property && child.name === name)
|
||||
)
|
||||
if (property !== undefined) {
|
||||
children.push(property)
|
||||
}
|
||||
return new And(children)
|
||||
}
|
||||
86
packages/complex-matcher/src/index.spec.js
Normal file
86
packages/complex-matcher/src/index.spec.js
Normal file
@@ -0,0 +1,86 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { ast, pattern } from './index.fixtures'
|
||||
import {
|
||||
getPropertyClausesStrings,
|
||||
Null,
|
||||
NumberNode,
|
||||
parse,
|
||||
setPropertyClause,
|
||||
} from './'
|
||||
|
||||
it('getPropertyClausesStrings', () => {
|
||||
const tmp = getPropertyClausesStrings(parse('foo bar:baz baz:|(foo bar)'))
|
||||
expect(tmp).toEqual({
|
||||
bar: ['baz'],
|
||||
baz: ['foo', 'bar'],
|
||||
})
|
||||
})
|
||||
|
||||
describe('parse', () => {
|
||||
it('analyses a string and returns a node/tree', () => {
|
||||
expect(parse(pattern)).toEqual(ast)
|
||||
})
|
||||
|
||||
it('supports an empty string', () => {
|
||||
expect(parse('')).toEqual(new Null())
|
||||
})
|
||||
|
||||
it('differentiate between numbers and numbers in strings', () => {
|
||||
let node
|
||||
|
||||
node = parse('32')
|
||||
expect(node.match(32)).toBe(true)
|
||||
expect(node.match('32')).toBe(false)
|
||||
expect(node.toString()).toBe('32')
|
||||
|
||||
node = parse('"32"')
|
||||
expect(node.match(32)).toBe(false)
|
||||
expect(node.match('32')).toBe(true)
|
||||
expect(node.toString()).toBe('"32"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Number', () => {
|
||||
it('match a number recursively', () => {
|
||||
expect(new NumberNode(3).match([{ foo: 3 }])).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setPropertyClause', () => {
|
||||
it('creates a node if none passed', () => {
|
||||
expect(setPropertyClause(undefined, 'foo', 'bar').toString()).toBe(
|
||||
'foo:bar'
|
||||
)
|
||||
})
|
||||
|
||||
it('adds a property clause if there was none', () => {
|
||||
expect(setPropertyClause(parse('baz'), 'foo', 'bar').toString()).toBe(
|
||||
'baz foo:bar'
|
||||
)
|
||||
})
|
||||
|
||||
it('replaces the property clause if there was one', () => {
|
||||
expect(
|
||||
setPropertyClause(parse('plip foo:baz plop'), 'foo', 'bar').toString()
|
||||
).toBe('plip plop foo:bar')
|
||||
|
||||
expect(
|
||||
setPropertyClause(parse('foo:|(baz plop)'), 'foo', 'bar').toString()
|
||||
).toBe('foo:bar')
|
||||
})
|
||||
|
||||
it('removes the property clause if no chid is passed', () => {
|
||||
expect(
|
||||
setPropertyClause(parse('foo bar:baz qux'), 'bar', undefined).toString()
|
||||
).toBe('foo qux')
|
||||
|
||||
expect(
|
||||
setPropertyClause(parse('foo bar:baz qux'), 'baz', undefined).toString()
|
||||
).toBe('foo bar:baz qux')
|
||||
})
|
||||
})
|
||||
|
||||
it('toString', () => {
|
||||
expect(pattern).toBe(ast.toString())
|
||||
})
|
||||
3
packages/value-matcher/.babelrc.js
Normal file
3
packages/value-matcher/.babelrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
24
packages/value-matcher/.npmignore
Normal file
24
packages/value-matcher/.npmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
/benchmark/
|
||||
/benchmarks/
|
||||
*.bench.js
|
||||
*.bench.js.map
|
||||
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/fixture/
|
||||
/fixtures/
|
||||
*.fixture.js
|
||||
*.fixture.js.map
|
||||
*.fixtures.js
|
||||
*.fixtures.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
__snapshots__/
|
||||
66
packages/value-matcher/README.md
Normal file
66
packages/value-matcher/README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# value-matcher [](https://travis-ci.org/vatefr/xen-orchestra)
|
||||
|
||||
> ${pkg.description}
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/value-matcher):
|
||||
|
||||
```
|
||||
> npm install --save value-matcher
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import { createPredicate } from 'value-matcher'
|
||||
|
||||
[
|
||||
{ user: 'sam', age: 65, active: false },
|
||||
{ user: 'barney', age: 36, active: true },
|
||||
{ user: 'fred', age: 40, active: false },
|
||||
].filter(createPredicate({
|
||||
__or: [
|
||||
{ user: 'sam' },
|
||||
{ active: true },
|
||||
],
|
||||
}))
|
||||
// [
|
||||
// { user: 'sam', age: 65, active: false },
|
||||
// { user: 'barney', age: 36, active: true },
|
||||
// ]
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> yarn
|
||||
|
||||
# Run the tests
|
||||
> yarn test
|
||||
|
||||
# Continuously compile
|
||||
> yarn dev
|
||||
|
||||
# Continuously run the tests
|
||||
> yarn dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> yarn build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
47
packages/value-matcher/package.json
Normal file
47
packages/value-matcher/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "value-matcher",
|
||||
"version": "0.2.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/value-matcher",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"browserslist": [
|
||||
">2%"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@babel/preset-flow": "7.0.0-beta.49",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepare": "yarn run build",
|
||||
"prepublishOnly": "yarn run build"
|
||||
}
|
||||
}
|
||||
81
packages/value-matcher/src/index.js
Normal file
81
packages/value-matcher/src/index.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// @flow
|
||||
|
||||
/* eslint-disable no-use-before-define */
|
||||
export type Pattern =
|
||||
| AndPattern
|
||||
| OrPattern
|
||||
| NotPattern
|
||||
| ObjectPattern
|
||||
| ArrayPattern
|
||||
| ValuePattern
|
||||
/* eslint-enable no-use-before-define */
|
||||
|
||||
// all patterns must match
|
||||
type AndPattern = {| __and: Array<Pattern> |}
|
||||
|
||||
// one of the pattern must match
|
||||
type OrPattern = {| __or: Array<Pattern> |}
|
||||
|
||||
// the pattern must not match
|
||||
type NotPattern = {| __not: Pattern |}
|
||||
|
||||
// value is an object with properties matching the patterns
|
||||
type ObjectPattern = { [string]: Pattern }
|
||||
|
||||
// value is an array and each patterns must match a different item
|
||||
type ArrayPattern = Array<Pattern>
|
||||
|
||||
// value equals the pattern
|
||||
type ValuePattern = boolean | number | string
|
||||
|
||||
const match = (pattern: Pattern, value: any) => {
|
||||
if (Array.isArray(pattern)) {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
pattern.every((subpattern, i) =>
|
||||
// FIXME: subpatterns should match different subvalues
|
||||
value.some(subvalue => match(subpattern, subvalue))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (pattern !== null && typeof pattern === 'object') {
|
||||
const keys = Object.keys(pattern)
|
||||
const { length } = keys
|
||||
|
||||
if (length === 1) {
|
||||
const [key] = keys
|
||||
if (key === '__and') {
|
||||
const andPattern: AndPattern = (pattern: any)
|
||||
return andPattern.__and.every(subpattern => match(subpattern, value))
|
||||
}
|
||||
if (key === '__or') {
|
||||
const orPattern: OrPattern = (pattern: any)
|
||||
return orPattern.__or.some(subpattern => match(subpattern, value))
|
||||
}
|
||||
if (key === '__not') {
|
||||
const notPattern: NotPattern = (pattern: any)
|
||||
return !match(notPattern.__not, value)
|
||||
}
|
||||
}
|
||||
|
||||
if (value === null || typeof value !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
const objectPattern: ObjectPattern = (pattern: any)
|
||||
for (let i = 0; i < length; ++i) {
|
||||
const key = keys[i]
|
||||
const subvalue = value[key]
|
||||
if (subvalue === undefined || !match(objectPattern[key], subvalue)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return pattern === value
|
||||
}
|
||||
|
||||
export const createPredicate = (pattern: Pattern) => (value: any) =>
|
||||
match(pattern, value)
|
||||
3
packages/vhd-cli/.babelrc.js
Normal file
3
packages/vhd-cli/.babelrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
24
packages/vhd-cli/.npmignore
Normal file
24
packages/vhd-cli/.npmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
/benchmark/
|
||||
/benchmarks/
|
||||
*.bench.js
|
||||
*.bench.js.map
|
||||
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/fixture/
|
||||
/fixtures/
|
||||
*.fixture.js
|
||||
*.fixture.js.map
|
||||
*.fixtures.js
|
||||
*.fixtures.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
__snapshots__/
|
||||
51
packages/vhd-cli/README.md
Normal file
51
packages/vhd-cli/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# vhd-cli [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> ${pkg.description}
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/vhd-cli):
|
||||
|
||||
```
|
||||
> npm install --global vhd-cli
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
> vhd-cli <VHD file>
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install
|
||||
|
||||
# Run the tests
|
||||
> npm test
|
||||
|
||||
# Continuously compile
|
||||
> npm run dev
|
||||
|
||||
# Continuously run the tests
|
||||
> npm run dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> npm run build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
54
packages/vhd-cli/package.json
Normal file
54
packages/vhd-cli/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "vhd-cli",
|
||||
"version": "0.0.1",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/vhd-cli",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {
|
||||
"vhd-cli": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^0.1.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"struct-fu": "^1.2.0",
|
||||
"vhd-lib": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0-beta.49",
|
||||
"@babel/core": "^7.0.0-beta.49",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
|
||||
"@babel/preset-env": "^7.0.0-beta.49",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"execa": "^0.10.0",
|
||||
"index-modules": "^0.3.0",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
"rimraf": "^2.6.1",
|
||||
"tmp": "^0.0.33"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/ && index-modules --cjs-lazy src/commands",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepare": "yarn run build"
|
||||
}
|
||||
}
|
||||
23
packages/vhd-cli/src/_utils.js
Normal file
23
packages/vhd-cli/src/_utils.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const { createWriteStream } = require('fs')
|
||||
const { PassThrough } = require('stream')
|
||||
|
||||
const createOutputStream = path => {
|
||||
if (path !== undefined && path !== '-') {
|
||||
return createWriteStream(path)
|
||||
}
|
||||
|
||||
// introduce a through stream because stdout is not a normal stream!
|
||||
const stream = new PassThrough()
|
||||
stream.pipe(process.stdout)
|
||||
return stream
|
||||
}
|
||||
|
||||
export const writeStream = (input, path) => {
|
||||
const output = createOutputStream(path)
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
input
|
||||
.on('error', reject)
|
||||
.pipe(output.on('error', reject).on('finish', resolve))
|
||||
)
|
||||
}
|
||||
15
packages/vhd-cli/src/commands/check.js
Normal file
15
packages/vhd-cli/src/commands/check.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import Vhd from 'vhd-lib'
|
||||
import { getHandler } from '@xen-orchestra/fs'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default async args => {
|
||||
const handler = getHandler({ url: 'file:///' })
|
||||
for (const vhd of args) {
|
||||
try {
|
||||
await new Vhd(handler, resolve(vhd)).readHeaderAndFooter()
|
||||
console.log('ok:', vhd)
|
||||
} catch (error) {
|
||||
console.error('nok:', vhd, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
12
packages/vhd-cli/src/commands/info.js
Normal file
12
packages/vhd-cli/src/commands/info.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import Vhd from 'vhd-lib'
|
||||
import { getHandler } from '@xen-orchestra/fs'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default async args => {
|
||||
const vhd = new Vhd(getHandler({ url: 'file:///' }), resolve(args[0]))
|
||||
|
||||
await vhd.readHeaderAndFooter()
|
||||
|
||||
console.log(vhd.header)
|
||||
console.log(vhd.footer)
|
||||
}
|
||||
16
packages/vhd-cli/src/commands/raw.js
Normal file
16
packages/vhd-cli/src/commands/raw.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createContentStream } from 'vhd-lib'
|
||||
import { getHandler } from '@xen-orchestra/fs'
|
||||
import { resolve } from 'path'
|
||||
|
||||
import { writeStream } from '../_utils'
|
||||
|
||||
export default async args => {
|
||||
if (args.length < 2 || args.some(_ => _ === '-h' || _ === '--help')) {
|
||||
return `Usage: ${this.command} <input VHD> [<output raw>]`
|
||||
}
|
||||
|
||||
await writeStream(
|
||||
createContentStream(getHandler({ url: 'file:///' }), resolve(args[0])),
|
||||
args[1]
|
||||
)
|
||||
}
|
||||
21
packages/vhd-cli/src/commands/synthetize.js
Normal file
21
packages/vhd-cli/src/commands/synthetize.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import path from 'path'
|
||||
import { createSyntheticStream } from 'vhd-lib'
|
||||
import { createWriteStream } from 'fs'
|
||||
import { getHandler } from '@xen-orchestra/fs'
|
||||
|
||||
export default async function main (args) {
|
||||
if (args.length < 2 || args.some(_ => _ === '-h' || _ === '--help')) {
|
||||
return `Usage: ${this.command} <input VHD> <output VHD>`
|
||||
}
|
||||
|
||||
const handler = getHandler({ url: 'file:///' })
|
||||
return new Promise((resolve, reject) => {
|
||||
createSyntheticStream(handler, path.resolve(args[0]))
|
||||
.on('error', reject)
|
||||
.pipe(
|
||||
createWriteStream(args[1])
|
||||
.on('error', reject)
|
||||
.on('finish', resolve)
|
||||
)
|
||||
})
|
||||
}
|
||||
44
packages/vhd-cli/src/index.js
Executable file
44
packages/vhd-cli/src/index.js
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import execPromise from 'exec-promise'
|
||||
|
||||
import commands from './commands'
|
||||
|
||||
function runCommand (commands, [command, ...args]) {
|
||||
if (command === undefined || command === '-h' || command === '--help') {
|
||||
command = 'help'
|
||||
}
|
||||
|
||||
const fn = commands[command]
|
||||
|
||||
if (fn === undefined) {
|
||||
if (command === 'help') {
|
||||
return `Usage:
|
||||
|
||||
${Object.keys(commands)
|
||||
.filter(command => command !== 'help')
|
||||
.map(command => ` ${this.command} ${command}`)
|
||||
.join('\n\n')}`
|
||||
}
|
||||
|
||||
throw `invalid command ${command}` // eslint-disable-line no-throw-literal
|
||||
}
|
||||
|
||||
return fn.call(
|
||||
{
|
||||
__proto__: this,
|
||||
command: `${this.command} ${command}`,
|
||||
},
|
||||
args
|
||||
)
|
||||
}
|
||||
|
||||
execPromise(
|
||||
runCommand.bind(
|
||||
{
|
||||
command: 'vhd-cli',
|
||||
runCommand,
|
||||
},
|
||||
commands
|
||||
)
|
||||
)
|
||||
28
packages/vhd-cli/src/info.integ.spec.js
Normal file
28
packages/vhd-cli/src/info.integ.spec.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import execa from 'execa'
|
||||
import rimraf from 'rimraf'
|
||||
import tmp from 'tmp'
|
||||
import { fromCallback as pFromCallback } from 'promise-toolbox'
|
||||
|
||||
import command from './commands/info'
|
||||
|
||||
const initialDir = process.cwd()
|
||||
|
||||
jest.setTimeout(10000)
|
||||
|
||||
beforeEach(async () => {
|
||||
const dir = await pFromCallback(cb => tmp.dir(cb))
|
||||
process.chdir(dir)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const tmpDir = process.cwd()
|
||||
process.chdir(initialDir)
|
||||
await pFromCallback(cb => rimraf(tmpDir, cb))
|
||||
})
|
||||
|
||||
test('can run the command', async () => {
|
||||
await execa('qemu-img', ['create', '-fvpc', 'empty.vhd', '1G'])
|
||||
await command(['empty.vhd'])
|
||||
})
|
||||
3
packages/vhd-lib/.babelrc.js
Normal file
3
packages/vhd-lib/.babelrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
56
packages/vhd-lib/package.json
Normal file
56
packages/vhd-lib/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "vhd-lib",
|
||||
"version": "0.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Primitives for VHD file handling",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/vhd-lib",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"preferGlobal": true,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0-beta.49",
|
||||
"async-iterator-to-stream": "^1.0.2",
|
||||
"from2": "^2.3.0",
|
||||
"fs-extra": "^6.0.1",
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
"struct-fu": "^1.2.0",
|
||||
"uuid": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.0.0-beta.49",
|
||||
"@babel/core": "7.0.0-beta.49",
|
||||
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
|
||||
"@babel/preset-env": "7.0.0-beta.49",
|
||||
"@babel/preset-flow": "7.0.0-beta.49",
|
||||
"@xen-orchestra/fs": "^0.1.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"execa": "^0.10.0",
|
||||
"fs-promise": "^2.0.0",
|
||||
"get-stream": "^3.0.0",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"tmp": "^0.0.33"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run clean",
|
||||
"prepare": "yarn run build"
|
||||
}
|
||||
}
|
||||
7
packages/vhd-lib/src/_bitmap.js
Normal file
7
packages/vhd-lib/src/_bitmap.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const MASK = 0x80
|
||||
|
||||
export const set = (map, bit) => {
|
||||
map[bit >> 3] |= MASK >> (bit & 7)
|
||||
}
|
||||
|
||||
export const test = (map, bit) => ((map[bit >> 3] << (bit & 7)) & MASK) !== 0
|
||||
37
packages/vhd-lib/src/_computeGeometryForSize.js
Normal file
37
packages/vhd-lib/src/_computeGeometryForSize.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { SECTOR_SIZE } from './_constants'
|
||||
|
||||
export default function computeGeometryForSize (size) {
|
||||
const totalSectors = Math.ceil(size / 512)
|
||||
let sectorsPerTrackCylinder
|
||||
let heads
|
||||
let cylinderTimesHeads
|
||||
if (totalSectors > 65535 * 16 * 255) {
|
||||
throw Error('disk is too big')
|
||||
}
|
||||
// straight copypasta from the file spec appendix on CHS Calculation
|
||||
if (totalSectors >= 65535 * 16 * 63) {
|
||||
sectorsPerTrackCylinder = 255
|
||||
heads = 16
|
||||
cylinderTimesHeads = totalSectors / sectorsPerTrackCylinder
|
||||
} else {
|
||||
sectorsPerTrackCylinder = 17
|
||||
cylinderTimesHeads = totalSectors / sectorsPerTrackCylinder
|
||||
heads = Math.floor((cylinderTimesHeads + 1023) / 1024)
|
||||
if (heads < 4) {
|
||||
heads = 4
|
||||
}
|
||||
if (cylinderTimesHeads >= heads * 1024 || heads > 16) {
|
||||
sectorsPerTrackCylinder = 31
|
||||
heads = 16
|
||||
cylinderTimesHeads = totalSectors / sectorsPerTrackCylinder
|
||||
}
|
||||
if (cylinderTimesHeads >= heads * 1024) {
|
||||
sectorsPerTrackCylinder = 63
|
||||
heads = 16
|
||||
cylinderTimesHeads = totalSectors / sectorsPerTrackCylinder
|
||||
}
|
||||
}
|
||||
const cylinders = Math.ceil(cylinderTimesHeads / heads)
|
||||
const actualSize = cylinders * heads * sectorsPerTrackCylinder * SECTOR_SIZE
|
||||
return { cylinders, heads, sectorsPerTrackCylinder, actualSize }
|
||||
}
|
||||
42
packages/vhd-lib/src/_constant-stream.js
Normal file
42
packages/vhd-lib/src/_constant-stream.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import from2 from 'from2'
|
||||
|
||||
const constantStream = (data, n = 1) => {
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
data = Buffer.from(data)
|
||||
}
|
||||
|
||||
const { length } = data
|
||||
|
||||
if (!length) {
|
||||
throw new Error('data should not be empty')
|
||||
}
|
||||
|
||||
n *= length
|
||||
let currentLength = length
|
||||
|
||||
return from2((size, next) => {
|
||||
if (n <= 0) {
|
||||
return next(null, null)
|
||||
}
|
||||
|
||||
if (n < size) {
|
||||
size = n
|
||||
}
|
||||
|
||||
if (size < currentLength) {
|
||||
const m = Math.floor(size / length) * length || length
|
||||
n -= m
|
||||
return next(null, data.slice(0, m))
|
||||
}
|
||||
|
||||
// if more than twice the data length is requested, repeat the data
|
||||
if (size > currentLength * 2) {
|
||||
currentLength = Math.floor(size / length) * length
|
||||
data = Buffer.alloc(currentLength, data)
|
||||
}
|
||||
|
||||
n -= currentLength
|
||||
return next(null, data)
|
||||
})
|
||||
}
|
||||
export { constantStream as default }
|
||||
30
packages/vhd-lib/src/_constants.js
Normal file
30
packages/vhd-lib/src/_constants.js
Normal file
@@ -0,0 +1,30 @@
|
||||
export const BLOCK_UNUSED = 0xffffffff
|
||||
|
||||
// This lib has been extracted from the Xen Orchestra project.
|
||||
export const CREATOR_APPLICATION = 'xo '
|
||||
|
||||
// Sizes in bytes.
|
||||
export const FOOTER_SIZE = 512
|
||||
export const HEADER_SIZE = 1024
|
||||
export const SECTOR_SIZE = 512
|
||||
export const DEFAULT_BLOCK_SIZE = 0x00200000 // from the spec
|
||||
|
||||
export const FOOTER_COOKIE = 'conectix'
|
||||
export const HEADER_COOKIE = 'cxsparse'
|
||||
|
||||
export const DISK_TYPE_FIXED = 2
|
||||
export const DISK_TYPE_DYNAMIC = 3
|
||||
export const DISK_TYPE_DIFFERENCING = 4
|
||||
|
||||
export const PARENT_LOCATOR_ENTRIES = 8
|
||||
|
||||
export const PLATFORM_NONE = 0
|
||||
export const PLATFORM_WI2R = 0x57693272
|
||||
export const PLATFORM_WI2K = 0x5769326b
|
||||
export const PLATFORM_W2RU = 0x57327275
|
||||
export const PLATFORM_W2KU = 0x57326b75
|
||||
export const PLATFORM_MAC = 0x4d616320
|
||||
export const PLATFORM_MACX = 0x4d616358
|
||||
|
||||
export const FILE_FORMAT_VERSION = 1 << 16
|
||||
export const HEADER_VERSION = 1 << 16
|
||||
56
packages/vhd-lib/src/_createFooterHeader.js
Normal file
56
packages/vhd-lib/src/_createFooterHeader.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { v4 as generateUuid } from 'uuid'
|
||||
|
||||
import { checksumStruct, fuFooter, fuHeader } from './_structs'
|
||||
import {
|
||||
CREATOR_APPLICATION,
|
||||
DEFAULT_BLOCK_SIZE as VHD_BLOCK_SIZE_BYTES,
|
||||
DISK_TYPE_FIXED,
|
||||
FILE_FORMAT_VERSION,
|
||||
FOOTER_COOKIE,
|
||||
FOOTER_SIZE,
|
||||
HEADER_COOKIE,
|
||||
HEADER_SIZE,
|
||||
HEADER_VERSION,
|
||||
PLATFORM_WI2K,
|
||||
} from './_constants'
|
||||
|
||||
export function createFooter (
|
||||
size,
|
||||
timestamp,
|
||||
geometry,
|
||||
dataOffset,
|
||||
diskType = DISK_TYPE_FIXED
|
||||
) {
|
||||
const footer = fuFooter.pack({
|
||||
cookie: FOOTER_COOKIE,
|
||||
features: 2,
|
||||
fileFormatVersion: FILE_FORMAT_VERSION,
|
||||
dataOffset,
|
||||
timestamp,
|
||||
creatorApplication: CREATOR_APPLICATION,
|
||||
creatorHostOs: PLATFORM_WI2K, // it looks like everybody is using Wi2k
|
||||
originalSize: size,
|
||||
currentSize: size,
|
||||
diskGeometry: geometry,
|
||||
diskType,
|
||||
uuid: generateUuid(null, Buffer.allocUnsafe(16)),
|
||||
})
|
||||
checksumStruct(footer, fuFooter)
|
||||
return footer
|
||||
}
|
||||
|
||||
export function createHeader (
|
||||
maxTableEntries,
|
||||
tableOffset = HEADER_SIZE + FOOTER_SIZE,
|
||||
blockSize = VHD_BLOCK_SIZE_BYTES
|
||||
) {
|
||||
const header = fuHeader.pack({
|
||||
cookie: HEADER_COOKIE,
|
||||
tableOffset,
|
||||
headerVersion: HEADER_VERSION,
|
||||
maxTableEntries,
|
||||
blockSize,
|
||||
})
|
||||
checksumStruct(header, fuHeader)
|
||||
return header
|
||||
}
|
||||
121
packages/vhd-lib/src/_structs.js
Normal file
121
packages/vhd-lib/src/_structs.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import assert from 'assert'
|
||||
import fu from 'struct-fu'
|
||||
|
||||
import { FOOTER_SIZE, HEADER_SIZE, PARENT_LOCATOR_ENTRIES } from './_constants'
|
||||
|
||||
const SIZE_OF_32_BITS = Math.pow(2, 32)
|
||||
|
||||
const uint64 = fu.derive(
|
||||
fu.uint32(2),
|
||||
number => [Math.floor(number / SIZE_OF_32_BITS), number % SIZE_OF_32_BITS],
|
||||
_ => _[0] * SIZE_OF_32_BITS + _[1]
|
||||
)
|
||||
const uint64Undefinable = fu.derive(
|
||||
fu.uint32(2),
|
||||
number =>
|
||||
number === undefined
|
||||
? [0xffffffff, 0xffffffff]
|
||||
: [Math.floor(number / SIZE_OF_32_BITS), number % SIZE_OF_32_BITS],
|
||||
_ =>
|
||||
_[0] === 0xffffffff && _[1] === 0xffffffff
|
||||
? undefined
|
||||
: _[0] * SIZE_OF_32_BITS + _[1]
|
||||
)
|
||||
|
||||
export const fuFooter = fu.struct([
|
||||
fu.char('cookie', 8), // 0
|
||||
fu.uint32('features'), // 8
|
||||
fu.uint32('fileFormatVersion'), // 12
|
||||
uint64Undefinable('dataOffset'), // offset of the header
|
||||
fu.uint32('timestamp'), // 24
|
||||
fu.char('creatorApplication', 4), // 28
|
||||
fu.uint32('creatorVersion'), // 32
|
||||
fu.uint32('creatorHostOs'), // 36
|
||||
uint64('originalSize'),
|
||||
uint64('currentSize'),
|
||||
fu.struct('diskGeometry', [
|
||||
fu.uint16('cylinders'), // 56
|
||||
fu.uint8('heads'), // 58
|
||||
fu.uint8('sectorsPerTrackCylinder'), // 59
|
||||
]),
|
||||
fu.uint32('diskType'), // 60 Disk type, must be equal to HARD_DISK_TYPE_DYNAMIC/HARD_DISK_TYPE_DIFFERENCING.
|
||||
fu.uint32('checksum'), // 64
|
||||
fu.byte('uuid', 16), // 68
|
||||
fu.char('saved'), // 84
|
||||
fu.char('hidden'), // 85 TODO: should probably be merged in reserved
|
||||
fu.char('reserved', 426), // 86
|
||||
])
|
||||
assert.strictEqual(fuFooter.size, FOOTER_SIZE)
|
||||
|
||||
export const fuHeader = fu.struct([
|
||||
fu.char('cookie', 8),
|
||||
uint64Undefinable('dataOffset'),
|
||||
uint64('tableOffset'),
|
||||
fu.uint32('headerVersion'),
|
||||
fu.uint32('maxTableEntries'), // Max entries in the Block Allocation Table.
|
||||
fu.uint32('blockSize'), // Block size in bytes. Default (2097152 => 2MB)
|
||||
fu.uint32('checksum'),
|
||||
fu.byte('parentUuid', 16),
|
||||
fu.uint32('parentTimestamp'),
|
||||
fu.uint32('reserved1'),
|
||||
fu.char16be('parentUnicodeName', 512),
|
||||
fu.struct(
|
||||
'parentLocatorEntry',
|
||||
[
|
||||
fu.uint32('platformCode'),
|
||||
fu.uint32('platformDataSpace'),
|
||||
fu.uint32('platformDataLength'),
|
||||
fu.uint32('reserved'),
|
||||
uint64('platformDataOffset'), // Absolute byte offset of the locator data.
|
||||
],
|
||||
PARENT_LOCATOR_ENTRIES
|
||||
),
|
||||
fu.char('reserved2', 256),
|
||||
])
|
||||
assert.strictEqual(fuHeader.size, HEADER_SIZE)
|
||||
|
||||
export const packField = (field, value, buf) => {
|
||||
const { offset } = field
|
||||
|
||||
field.pack(
|
||||
value,
|
||||
buf,
|
||||
typeof offset !== 'object' ? { bytes: offset, bits: 0 } : offset
|
||||
)
|
||||
}
|
||||
|
||||
export const unpackField = (field, buf) => {
|
||||
const { offset } = field
|
||||
|
||||
return field.unpack(
|
||||
buf,
|
||||
typeof offset !== 'object' ? { bytes: offset, bits: 0 } : offset
|
||||
)
|
||||
}
|
||||
|
||||
// Returns the checksum of a raw struct.
|
||||
// The raw struct (footer or header) is altered with the new sum.
|
||||
export function checksumStruct (buf, struct) {
|
||||
const checksumField = struct.fields.checksum
|
||||
let sum = 0
|
||||
|
||||
// Do not use the stored checksum to compute the new checksum.
|
||||
const checksumOffset = checksumField.offset
|
||||
for (let i = 0, n = checksumOffset; i < n; ++i) {
|
||||
sum += buf[i]
|
||||
}
|
||||
for (
|
||||
let i = checksumOffset + checksumField.size, n = struct.size;
|
||||
i < n;
|
||||
++i
|
||||
) {
|
||||
sum += buf[i]
|
||||
}
|
||||
|
||||
sum = ~sum >>> 0
|
||||
|
||||
// Write new sum.
|
||||
packField(checksumField, sum, buf)
|
||||
|
||||
return sum
|
||||
}
|
||||
37
packages/vhd-lib/src/chain.js
Normal file
37
packages/vhd-lib/src/chain.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { dirname, relative } from 'path'
|
||||
|
||||
import Vhd from './vhd'
|
||||
import { DISK_TYPE_DIFFERENCING } from './_constants'
|
||||
|
||||
export default async function chain (
|
||||
parentHandler,
|
||||
parentPath,
|
||||
childHandler,
|
||||
childPath,
|
||||
force = false
|
||||
) {
|
||||
const parentVhd = new Vhd(parentHandler, parentPath)
|
||||
const childVhd = new Vhd(childHandler, childPath)
|
||||
|
||||
await childVhd.readHeaderAndFooter()
|
||||
const { header, footer } = childVhd
|
||||
|
||||
if (footer.diskType !== DISK_TYPE_DIFFERENCING) {
|
||||
if (!force) {
|
||||
throw new Error('cannot chain disk of type ' + footer.diskType)
|
||||
}
|
||||
footer.diskType = DISK_TYPE_DIFFERENCING
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
childVhd.readBlockAllocationTable(),
|
||||
parentVhd.readHeaderAndFooter(),
|
||||
])
|
||||
|
||||
const parentName = relative(dirname(childPath), parentPath)
|
||||
header.parentUuid = parentVhd.footer.uuid
|
||||
header.parentUnicodeName = parentName
|
||||
await childVhd.setUniqueParentLocator(parentName)
|
||||
await childVhd.writeHeader()
|
||||
await childVhd.writeFooter()
|
||||
}
|
||||
31
packages/vhd-lib/src/createContentStream.js
Normal file
31
packages/vhd-lib/src/createContentStream.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import asyncIteratorToStream from 'async-iterator-to-stream'
|
||||
|
||||
import Vhd from './vhd'
|
||||
|
||||
export default asyncIteratorToStream(async function * (handler, path) {
|
||||
const fd = await handler.openFile(path, 'r')
|
||||
try {
|
||||
const vhd = new Vhd(handler, fd)
|
||||
await vhd.readHeaderAndFooter()
|
||||
await vhd.readBlockAllocationTable()
|
||||
const {
|
||||
footer: { currentSize },
|
||||
header: { blockSize },
|
||||
} = vhd
|
||||
const nFullBlocks = Math.floor(currentSize / blockSize)
|
||||
const nLeftoverBytes = currentSize % blockSize
|
||||
|
||||
const emptyBlock = Buffer.alloc(blockSize)
|
||||
for (let i = 0; i < nFullBlocks; ++i) {
|
||||
yield vhd.containsBlock(i) ? (await vhd._readBlock(i)).data : emptyBlock
|
||||
}
|
||||
if (nLeftoverBytes !== 0) {
|
||||
yield (vhd.containsBlock(nFullBlocks)
|
||||
? (await vhd._readBlock(nFullBlocks)).data
|
||||
: emptyBlock
|
||||
).slice(0, nLeftoverBytes)
|
||||
}
|
||||
} finally {
|
||||
await handler.closeFile(fd)
|
||||
}
|
||||
})
|
||||
42
packages/vhd-lib/src/createReadableRawStream.js
Normal file
42
packages/vhd-lib/src/createReadableRawStream.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import asyncIteratorToStream from 'async-iterator-to-stream'
|
||||
|
||||
import computeGeometryForSize from './_computeGeometryForSize'
|
||||
import { createFooter } from './_createFooterHeader'
|
||||
|
||||
export default asyncIteratorToStream(async function * (size, blockParser) {
|
||||
const geometry = computeGeometryForSize(size)
|
||||
const actualSize = geometry.actualSize
|
||||
const footer = createFooter(
|
||||
actualSize,
|
||||
Math.floor(Date.now() / 1000),
|
||||
geometry
|
||||
)
|
||||
let position = 0
|
||||
|
||||
function * filePadding (paddingLength) {
|
||||
if (paddingLength > 0) {
|
||||
const chunkSize = 1024 * 1024 // 1Mo
|
||||
for (
|
||||
let paddingPosition = 0;
|
||||
paddingPosition + chunkSize < paddingLength;
|
||||
paddingPosition += chunkSize
|
||||
) {
|
||||
yield Buffer.alloc(chunkSize)
|
||||
}
|
||||
yield Buffer.alloc(paddingLength % chunkSize)
|
||||
}
|
||||
}
|
||||
|
||||
let next
|
||||
while ((next = await blockParser.next()) !== null) {
|
||||
const paddingLength = next.offsetBytes - position
|
||||
if (paddingLength < 0) {
|
||||
throw new Error('Received out of order blocks')
|
||||
}
|
||||
yield * filePadding(paddingLength)
|
||||
yield next.data
|
||||
position = next.offsetBytes + next.data.length
|
||||
}
|
||||
yield * filePadding(actualSize - position)
|
||||
yield footer
|
||||
})
|
||||
128
packages/vhd-lib/src/createReadableSparseStream.js
Normal file
128
packages/vhd-lib/src/createReadableSparseStream.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import assert from 'assert'
|
||||
import asyncIteratorToStream from 'async-iterator-to-stream'
|
||||
|
||||
import computeGeometryForSize from './_computeGeometryForSize'
|
||||
import { createFooter, createHeader } from './_createFooterHeader'
|
||||
import {
|
||||
BLOCK_UNUSED,
|
||||
DEFAULT_BLOCK_SIZE as VHD_BLOCK_SIZE_BYTES,
|
||||
DISK_TYPE_DYNAMIC,
|
||||
FOOTER_SIZE,
|
||||
HEADER_SIZE,
|
||||
SECTOR_SIZE,
|
||||
} from './_constants'
|
||||
|
||||
import { set as setBitmap } from './_bitmap'
|
||||
|
||||
const VHD_BLOCK_SIZE_SECTORS = VHD_BLOCK_SIZE_BYTES / SECTOR_SIZE
|
||||
|
||||
/**
|
||||
* @returns {Array} an array of occupation bitmap, each bit mapping an input block size of bytes
|
||||
*/
|
||||
function createBAT (
|
||||
firstBlockPosition,
|
||||
blockAddressList,
|
||||
ratio,
|
||||
bat,
|
||||
bitmapSize
|
||||
) {
|
||||
let currentVhdPositionSector = firstBlockPosition / SECTOR_SIZE
|
||||
blockAddressList.forEach(blockPosition => {
|
||||
assert.strictEqual(blockPosition % SECTOR_SIZE, 0)
|
||||
const vhdTableIndex = Math.floor(blockPosition / VHD_BLOCK_SIZE_BYTES)
|
||||
if (bat.readUInt32BE(vhdTableIndex * 4) === BLOCK_UNUSED) {
|
||||
bat.writeUInt32BE(currentVhdPositionSector, vhdTableIndex * 4)
|
||||
currentVhdPositionSector +=
|
||||
(bitmapSize + VHD_BLOCK_SIZE_BYTES) / SECTOR_SIZE
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default asyncIteratorToStream(async function * (
|
||||
diskSize,
|
||||
incomingBlockSize,
|
||||
blockAddressList,
|
||||
blockIterator
|
||||
) {
|
||||
const ratio = VHD_BLOCK_SIZE_BYTES / incomingBlockSize
|
||||
if (ratio % 1 !== 0) {
|
||||
throw new Error(
|
||||
`Can't import file, grain size (${incomingBlockSize}) is not a divider of VHD block size ${VHD_BLOCK_SIZE_BYTES}`
|
||||
)
|
||||
}
|
||||
if (ratio > 53) {
|
||||
throw new Error(
|
||||
`Can't import file, grain size / block size ratio is > 53 (${ratio})`
|
||||
)
|
||||
}
|
||||
|
||||
const maxTableEntries = Math.ceil(diskSize / VHD_BLOCK_SIZE_BYTES) + 1
|
||||
const tablePhysicalSizeBytes =
|
||||
Math.ceil(maxTableEntries * 4 / SECTOR_SIZE) * SECTOR_SIZE
|
||||
|
||||
const batPosition = FOOTER_SIZE + HEADER_SIZE
|
||||
const firstBlockPosition = batPosition + tablePhysicalSizeBytes
|
||||
const geometry = computeGeometryForSize(diskSize)
|
||||
const actualSize = geometry.actualSize
|
||||
const footer = createFooter(
|
||||
actualSize,
|
||||
Math.floor(Date.now() / 1000),
|
||||
geometry,
|
||||
FOOTER_SIZE,
|
||||
DISK_TYPE_DYNAMIC
|
||||
)
|
||||
const header = createHeader(
|
||||
maxTableEntries,
|
||||
batPosition,
|
||||
VHD_BLOCK_SIZE_BYTES
|
||||
)
|
||||
const bitmapSize =
|
||||
Math.ceil(VHD_BLOCK_SIZE_SECTORS / 8 / SECTOR_SIZE) * SECTOR_SIZE
|
||||
const bat = Buffer.alloc(tablePhysicalSizeBytes, 0xff)
|
||||
createBAT(firstBlockPosition, blockAddressList, ratio, bat, bitmapSize)
|
||||
let position = 0
|
||||
function * yieldAndTrack (buffer, expectedPosition) {
|
||||
if (expectedPosition !== undefined) {
|
||||
assert.strictEqual(position, expectedPosition)
|
||||
}
|
||||
if (buffer.length > 0) {
|
||||
yield buffer
|
||||
position += buffer.length
|
||||
}
|
||||
}
|
||||
async function * generateFileContent (blockIterator, bitmapSize, ratio) {
|
||||
let currentBlock = -1
|
||||
let currentVhdBlockIndex = -1
|
||||
let currentBlockWithBitmap = Buffer.alloc(0)
|
||||
for await (const next of blockIterator) {
|
||||
currentBlock++
|
||||
assert.strictEqual(blockAddressList[currentBlock], next.offsetBytes)
|
||||
const batIndex = Math.floor(next.offsetBytes / VHD_BLOCK_SIZE_BYTES)
|
||||
if (batIndex !== currentVhdBlockIndex) {
|
||||
if (currentVhdBlockIndex >= 0) {
|
||||
yield * yieldAndTrack(
|
||||
currentBlockWithBitmap,
|
||||
bat.readUInt32BE(currentVhdBlockIndex * 4) * SECTOR_SIZE
|
||||
)
|
||||
}
|
||||
currentBlockWithBitmap = Buffer.alloc(bitmapSize + VHD_BLOCK_SIZE_BYTES)
|
||||
currentVhdBlockIndex = batIndex
|
||||
}
|
||||
const blockOffset =
|
||||
(next.offsetBytes / SECTOR_SIZE) % VHD_BLOCK_SIZE_SECTORS
|
||||
for (let bitPos = 0; bitPos < VHD_BLOCK_SIZE_SECTORS / ratio; bitPos++) {
|
||||
setBitmap(currentBlockWithBitmap, blockOffset + bitPos)
|
||||
}
|
||||
next.data.copy(
|
||||
currentBlockWithBitmap,
|
||||
bitmapSize + next.offsetBytes % VHD_BLOCK_SIZE_BYTES
|
||||
)
|
||||
}
|
||||
yield * yieldAndTrack(currentBlockWithBitmap)
|
||||
}
|
||||
yield * yieldAndTrack(footer, 0)
|
||||
yield * yieldAndTrack(header, FOOTER_SIZE)
|
||||
yield * yieldAndTrack(bat, FOOTER_SIZE + HEADER_SIZE)
|
||||
yield * generateFileContent(blockIterator, bitmapSize, ratio)
|
||||
yield * yieldAndTrack(footer)
|
||||
})
|
||||
153
packages/vhd-lib/src/createSyntheticStream.js
Normal file
153
packages/vhd-lib/src/createSyntheticStream.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import asyncIteratorToStream from 'async-iterator-to-stream'
|
||||
import { dirname, resolve } from 'path'
|
||||
|
||||
import Vhd from './vhd'
|
||||
import {
|
||||
BLOCK_UNUSED,
|
||||
DISK_TYPE_DYNAMIC,
|
||||
FOOTER_SIZE,
|
||||
HEADER_SIZE,
|
||||
SECTOR_SIZE,
|
||||
} from './_constants'
|
||||
import { fuFooter, fuHeader, checksumStruct } from './_structs'
|
||||
import { test as mapTestBit } from './_bitmap'
|
||||
|
||||
const resolveRelativeFromFile = (file, path) =>
|
||||
resolve('/', dirname(file), path).slice(1)
|
||||
|
||||
export default asyncIteratorToStream(function * (handler, path) {
|
||||
const fds = []
|
||||
|
||||
try {
|
||||
const vhds = []
|
||||
while (true) {
|
||||
const fd = yield handler.openFile(path, 'r')
|
||||
fds.push(fd)
|
||||
const vhd = new Vhd(handler, fd)
|
||||
vhds.push(vhd)
|
||||
yield vhd.readHeaderAndFooter()
|
||||
yield vhd.readBlockAllocationTable()
|
||||
|
||||
if (vhd.footer.diskType === DISK_TYPE_DYNAMIC) {
|
||||
break
|
||||
}
|
||||
|
||||
path = resolveRelativeFromFile(path, vhd.header.parentUnicodeName)
|
||||
}
|
||||
const nVhds = vhds.length
|
||||
|
||||
// this the VHD we want to synthetize
|
||||
const vhd = vhds[0]
|
||||
|
||||
// this is the root VHD
|
||||
const rootVhd = vhds[nVhds - 1]
|
||||
|
||||
// data of our synthetic VHD
|
||||
// TODO: set parentLocatorEntry-s in header
|
||||
let header = {
|
||||
...vhd.header,
|
||||
tableOffset: FOOTER_SIZE + HEADER_SIZE,
|
||||
parentTimestamp: rootVhd.header.parentTimestamp,
|
||||
parentUnicodeName: rootVhd.header.parentUnicodeName,
|
||||
parentUuid: rootVhd.header.parentUuid,
|
||||
}
|
||||
|
||||
const bat = Buffer.allocUnsafe(vhd.batSize)
|
||||
let footer = {
|
||||
...vhd.footer,
|
||||
dataOffset: FOOTER_SIZE,
|
||||
diskType: rootVhd.footer.diskType,
|
||||
}
|
||||
const sectorsPerBlockData = vhd.sectorsPerBlock
|
||||
const sectorsPerBlock = sectorsPerBlockData + vhd.bitmapSize / SECTOR_SIZE
|
||||
|
||||
const nBlocks = Math.ceil(footer.currentSize / header.blockSize)
|
||||
|
||||
const blocksOwner = new Array(nBlocks)
|
||||
for (
|
||||
let iBlock = 0,
|
||||
blockOffset = Math.ceil(
|
||||
(header.tableOffset + bat.length) / SECTOR_SIZE
|
||||
);
|
||||
iBlock < nBlocks;
|
||||
++iBlock
|
||||
) {
|
||||
let blockSector = BLOCK_UNUSED
|
||||
for (let i = 0; i < nVhds; ++i) {
|
||||
if (vhds[i].containsBlock(iBlock)) {
|
||||
blocksOwner[iBlock] = i
|
||||
blockSector = blockOffset
|
||||
blockOffset += sectorsPerBlock
|
||||
break
|
||||
}
|
||||
}
|
||||
bat.writeUInt32BE(blockSector, iBlock * 4)
|
||||
}
|
||||
|
||||
footer = fuFooter.pack(footer)
|
||||
checksumStruct(footer, fuFooter)
|
||||
yield footer
|
||||
|
||||
header = fuHeader.pack(header)
|
||||
checksumStruct(header, fuHeader)
|
||||
yield header
|
||||
|
||||
yield bat
|
||||
|
||||
// TODO: for generic usage the bitmap needs to be properly computed for each block
|
||||
const bitmap = Buffer.alloc(vhd.bitmapSize, 0xff)
|
||||
for (let iBlock = 0; iBlock < nBlocks; ++iBlock) {
|
||||
const owner = blocksOwner[iBlock]
|
||||
if (owner === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
yield bitmap
|
||||
|
||||
const blocksByVhd = new Map()
|
||||
const emitBlockSectors = function * (iVhd, i, n) {
|
||||
const vhd = vhds[iVhd]
|
||||
const isRootVhd = vhd === rootVhd
|
||||
if (!vhd.containsBlock(iBlock)) {
|
||||
if (isRootVhd) {
|
||||
yield Buffer.alloc((n - i) * SECTOR_SIZE)
|
||||
} else {
|
||||
yield * emitBlockSectors(iVhd + 1, i, n)
|
||||
}
|
||||
return
|
||||
}
|
||||
let block = blocksByVhd.get(vhd)
|
||||
if (block === undefined) {
|
||||
block = yield vhd._readBlock(iBlock)
|
||||
blocksByVhd.set(vhd, block)
|
||||
}
|
||||
const { bitmap, data } = block
|
||||
if (isRootVhd) {
|
||||
yield data.slice(i * SECTOR_SIZE, n * SECTOR_SIZE)
|
||||
return
|
||||
}
|
||||
while (i < n) {
|
||||
const hasData = mapTestBit(bitmap, i)
|
||||
const start = i
|
||||
do {
|
||||
++i
|
||||
} while (i < n && mapTestBit(bitmap, i) === hasData)
|
||||
if (hasData) {
|
||||
yield data.slice(start * SECTOR_SIZE, i * SECTOR_SIZE)
|
||||
} else {
|
||||
yield * emitBlockSectors(iVhd + 1, start, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
yield * emitBlockSectors(owner, 0, sectorsPerBlockData)
|
||||
}
|
||||
|
||||
yield footer
|
||||
} finally {
|
||||
for (let i = 0, n = fds.length; i < n; ++i) {
|
||||
handler.closeFile(fds[i]).catch(error => {
|
||||
console.warn('createReadStream, closeFd', i, error)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
9
packages/vhd-lib/src/index.js
Normal file
9
packages/vhd-lib/src/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export { default } from './vhd'
|
||||
export { default as chainVhd } from './chain'
|
||||
export { default as createContentStream } from './createContentStream'
|
||||
export { default as createReadableRawStream } from './createReadableRawStream'
|
||||
export {
|
||||
default as createReadableSparseStream,
|
||||
} from './createReadableSparseStream'
|
||||
export { default as createSyntheticStream } from './createSyntheticStream'
|
||||
export { default as mergeVhd } from './merge'
|
||||
283
packages/vhd-lib/src/merge.integ.spec.js
Normal file
283
packages/vhd-lib/src/merge.integ.spec.js
Normal file
@@ -0,0 +1,283 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import execa from 'execa'
|
||||
import fs from 'fs-extra'
|
||||
import getStream from 'get-stream'
|
||||
import rimraf from 'rimraf'
|
||||
import tmp from 'tmp'
|
||||
import { getHandler } from '@xen-orchestra/fs'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { fromEvent, fromCallback as pFromCallback } from 'promise-toolbox'
|
||||
|
||||
import chainVhd from './chain'
|
||||
import createReadStream from './createSyntheticStream'
|
||||
import Vhd from './vhd'
|
||||
import vhdMerge from './merge'
|
||||
import { SECTOR_SIZE } from './_constants'
|
||||
|
||||
const initialDir = process.cwd()
|
||||
|
||||
jest.setTimeout(60000)
|
||||
|
||||
beforeEach(async () => {
|
||||
const dir = await pFromCallback(cb => tmp.dir(cb))
|
||||
process.chdir(dir)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const tmpDir = process.cwd()
|
||||
process.chdir(initialDir)
|
||||
await pFromCallback(cb => rimraf(tmpDir, cb))
|
||||
})
|
||||
|
||||
async function createRandomFile (name, sizeMb) {
|
||||
await execa('bash', [
|
||||
'-c',
|
||||
`< /dev/urandom tr -dc "\\t\\n [:alnum:]" | head -c ${sizeMb}M >${name}`,
|
||||
])
|
||||
}
|
||||
|
||||
async function checkFile (vhdName) {
|
||||
await execa('vhd-util', ['check', '-p', '-b', '-t', '-n', vhdName])
|
||||
}
|
||||
|
||||
async function recoverRawContent (vhdName, rawName, originalSize) {
|
||||
await checkFile(vhdName)
|
||||
await execa('qemu-img', ['convert', '-fvpc', '-Oraw', vhdName, rawName])
|
||||
if (originalSize !== undefined) {
|
||||
await execa('truncate', ['-s', originalSize, rawName])
|
||||
}
|
||||
}
|
||||
|
||||
async function convertFromRawToVhd (rawName, vhdName) {
|
||||
await execa('qemu-img', ['convert', '-f', 'raw', '-Ovpc', rawName, vhdName])
|
||||
}
|
||||
|
||||
test('blocks can be moved', async () => {
|
||||
const initalSize = 4
|
||||
await createRandomFile('randomfile', initalSize)
|
||||
await convertFromRawToVhd('randomfile', 'randomfile.vhd')
|
||||
const handler = getHandler({ url: 'file://' + process.cwd() })
|
||||
const originalSize = await handler.getSize('randomfile')
|
||||
const newVhd = new Vhd(handler, 'randomfile.vhd')
|
||||
await newVhd.readHeaderAndFooter()
|
||||
await newVhd.readBlockAllocationTable()
|
||||
await newVhd._freeFirstBlockSpace(8000000)
|
||||
await recoverRawContent('randomfile.vhd', 'recovered', originalSize)
|
||||
expect(await fs.readFile('recovered')).toEqual(
|
||||
await fs.readFile('randomfile')
|
||||
)
|
||||
})
|
||||
|
||||
test('the BAT MSB is not used for sign', async () => {
|
||||
const randomBuffer = await pFromCallback(cb => randomBytes(SECTOR_SIZE, cb))
|
||||
await execa('qemu-img', ['create', '-fvpc', 'empty.vhd', '1.8T'])
|
||||
const handler = getHandler({ url: 'file://' + process.cwd() })
|
||||
const vhd = new Vhd(handler, 'empty.vhd')
|
||||
await vhd.readHeaderAndFooter()
|
||||
await vhd.readBlockAllocationTable()
|
||||
// we want the bit 31 to be on, to prove it's not been used for sign
|
||||
const hugeWritePositionSectors = Math.pow(2, 31) + 200
|
||||
await vhd.writeData(hugeWritePositionSectors, randomBuffer)
|
||||
await checkFile('empty.vhd')
|
||||
// here we are moving the first sector very far in the VHD to prove the BAT doesn't use signed int32
|
||||
const hugePositionBytes = hugeWritePositionSectors * SECTOR_SIZE
|
||||
await vhd._freeFirstBlockSpace(hugePositionBytes)
|
||||
|
||||
// we recover the data manually for speed reasons.
|
||||
// fs.write() with offset is way faster than qemu-img when there is a 1.5To
|
||||
// hole before the block of data
|
||||
const recoveredFile = await fs.open('recovered', 'w')
|
||||
try {
|
||||
const vhd2 = new Vhd(handler, 'empty.vhd')
|
||||
await vhd2.readHeaderAndFooter()
|
||||
await vhd2.readBlockAllocationTable()
|
||||
for (let i = 0; i < vhd.header.maxTableEntries; i++) {
|
||||
const entry = vhd._getBatEntry(i)
|
||||
if (entry !== 0xffffffff) {
|
||||
const block = (await vhd2._readBlock(i)).data
|
||||
await fs.write(
|
||||
recoveredFile,
|
||||
block,
|
||||
0,
|
||||
block.length,
|
||||
vhd2.header.blockSize * i
|
||||
)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fs.close(recoveredFile)
|
||||
}
|
||||
const recovered = await getStream.buffer(
|
||||
await fs.createReadStream('recovered', {
|
||||
start: hugePositionBytes,
|
||||
end: hugePositionBytes + randomBuffer.length - 1,
|
||||
})
|
||||
)
|
||||
expect(recovered).toEqual(randomBuffer)
|
||||
})
|
||||
|
||||
test('writeData on empty file', async () => {
|
||||
const mbOfRandom = 3
|
||||
await createRandomFile('randomfile', mbOfRandom)
|
||||
await execa('qemu-img', ['create', '-fvpc', 'empty.vhd', mbOfRandom + 'M'])
|
||||
const randomData = await fs.readFile('randomfile')
|
||||
const handler = getHandler({ url: 'file://' + process.cwd() })
|
||||
const originalSize = await handler.getSize('randomfile')
|
||||
const newVhd = new Vhd(handler, 'empty.vhd')
|
||||
await newVhd.readHeaderAndFooter()
|
||||
await newVhd.readBlockAllocationTable()
|
||||
await newVhd.writeData(0, randomData)
|
||||
await recoverRawContent('empty.vhd', 'recovered', originalSize)
|
||||
expect(await fs.readFile('recovered')).toEqual(randomData)
|
||||
})
|
||||
|
||||
test('writeData in 2 non-overlaping operations', async () => {
|
||||
const mbOfRandom = 3
|
||||
await createRandomFile('randomfile', mbOfRandom)
|
||||
await execa('qemu-img', ['create', '-fvpc', 'empty.vhd', mbOfRandom + 'M'])
|
||||
const randomData = await fs.readFile('randomfile')
|
||||
const handler = getHandler({ url: 'file://' + process.cwd() })
|
||||
const originalSize = await handler.getSize('randomfile')
|
||||
const newVhd = new Vhd(handler, 'empty.vhd')
|
||||
await newVhd.readHeaderAndFooter()
|
||||
await newVhd.readBlockAllocationTable()
|
||||
const splitPointSectors = 2
|
||||
await newVhd.writeData(0, randomData.slice(0, splitPointSectors * 512))
|
||||
await newVhd.writeData(
|
||||
splitPointSectors,
|
||||
randomData.slice(splitPointSectors * 512)
|
||||
)
|
||||
await recoverRawContent('empty.vhd', 'recovered', originalSize)
|
||||
expect(await fs.readFile('recovered')).toEqual(randomData)
|
||||
})
|
||||
|
||||
test('writeData in 2 overlaping operations', async () => {
|
||||
const mbOfRandom = 3
|
||||
await createRandomFile('randomfile', mbOfRandom)
|
||||
await execa('qemu-img', ['create', '-fvpc', 'empty.vhd', mbOfRandom + 'M'])
|
||||
const randomData = await fs.readFile('randomfile')
|
||||
const handler = getHandler({ url: 'file://' + process.cwd() })
|
||||
const originalSize = await handler.getSize('randomfile')
|
||||
const newVhd = new Vhd(handler, 'empty.vhd')
|
||||
await newVhd.readHeaderAndFooter()
|
||||
await newVhd.readBlockAllocationTable()
|
||||
const endFirstWrite = 3
|
||||
const startSecondWrite = 2
|
||||
await newVhd.writeData(0, randomData.slice(0, endFirstWrite * 512))
|
||||
await newVhd.writeData(
|
||||
startSecondWrite,
|
||||
randomData.slice(startSecondWrite * 512)
|
||||
)
|
||||
await recoverRawContent('empty.vhd', 'recovered', originalSize)
|
||||
expect(await fs.readFile('recovered')).toEqual(randomData)
|
||||
})
|
||||
|
||||
test('BAT can be extended and blocks moved', async () => {
|
||||
const initalSize = 4
|
||||
await createRandomFile('randomfile', initalSize)
|
||||
await convertFromRawToVhd('randomfile', 'randomfile.vhd')
|
||||
const handler = getHandler({ url: 'file://' + process.cwd() })
|
||||
const originalSize = await handler.getSize('randomfile')
|
||||
const newVhd = new Vhd(handler, 'randomfile.vhd')
|
||||
await newVhd.readHeaderAndFooter()
|
||||
await newVhd.readBlockAllocationTable()
|
||||
await newVhd.ensureBatSize(2000)
|
||||
await recoverRawContent('randomfile.vhd', 'recovered', originalSize)
|
||||
expect(await fs.readFile('recovered')).toEqual(
|
||||
await fs.readFile('randomfile')
|
||||
)
|
||||
})
|
||||
|
||||
test('coalesce works with empty parent files', async () => {
|
||||
const mbOfRandom = 2
|
||||
await createRandomFile('randomfile', mbOfRandom)
|
||||
await convertFromRawToVhd('randomfile', 'randomfile.vhd')
|
||||
await execa('qemu-img', [
|
||||
'create',
|
||||
'-fvpc',
|
||||
'empty.vhd',
|
||||
mbOfRandom + 1 + 'M',
|
||||
])
|
||||
await checkFile('randomfile.vhd')
|
||||
await checkFile('empty.vhd')
|
||||
const handler = getHandler({ url: 'file://' + process.cwd() })
|
||||
const originalSize = await handler._getSize('randomfile')
|
||||
await chainVhd(handler, 'empty.vhd', handler, 'randomfile.vhd', true)
|
||||
await checkFile('randomfile.vhd')
|
||||
await checkFile('empty.vhd')
|
||||
await vhdMerge(handler, 'empty.vhd', handler, 'randomfile.vhd')
|
||||
await recoverRawContent('empty.vhd', 'recovered', originalSize)
|
||||
expect(await fs.readFile('recovered')).toEqual(
|
||||
await fs.readFile('randomfile')
|
||||
)
|
||||
})
|
||||
|
||||
test('coalesce works in normal cases', async () => {
|
||||
const mbOfRandom = 5
|
||||
await createRandomFile('randomfile', mbOfRandom)
|
||||
await createRandomFile('small_randomfile', Math.ceil(mbOfRandom / 2))
|
||||
await execa('qemu-img', [
|
||||
'create',
|
||||
'-fvpc',
|
||||
'parent.vhd',
|
||||
mbOfRandom + 1 + 'M',
|
||||
])
|
||||
await convertFromRawToVhd('randomfile', 'child1.vhd')
|
||||
const handler = getHandler({ url: 'file://' + process.cwd() })
|
||||
await execa('vhd-util', ['snapshot', '-n', 'child2.vhd', '-p', 'child1.vhd'])
|
||||
const vhd = new Vhd(handler, 'child2.vhd')
|
||||
await vhd.readHeaderAndFooter()
|
||||
await vhd.readBlockAllocationTable()
|
||||
vhd.footer.creatorApplication = 'xoa'
|
||||
await vhd.writeFooter()
|
||||
|
||||
const originalSize = await handler._getSize('randomfile')
|
||||
await chainVhd(handler, 'parent.vhd', handler, 'child1.vhd', true)
|
||||
await execa('vhd-util', ['check', '-t', '-n', 'child1.vhd'])
|
||||
await chainVhd(handler, 'child1.vhd', handler, 'child2.vhd', true)
|
||||
await execa('vhd-util', ['check', '-t', '-n', 'child2.vhd'])
|
||||
const smallRandom = await fs.readFile('small_randomfile')
|
||||
const newVhd = new Vhd(handler, 'child2.vhd')
|
||||
await newVhd.readHeaderAndFooter()
|
||||
await newVhd.readBlockAllocationTable()
|
||||
await newVhd.writeData(5, smallRandom)
|
||||
await checkFile('child2.vhd')
|
||||
await checkFile('child1.vhd')
|
||||
await checkFile('parent.vhd')
|
||||
await vhdMerge(handler, 'parent.vhd', handler, 'child1.vhd')
|
||||
await checkFile('parent.vhd')
|
||||
await chainVhd(handler, 'parent.vhd', handler, 'child2.vhd', true)
|
||||
await checkFile('child2.vhd')
|
||||
await vhdMerge(handler, 'parent.vhd', handler, 'child2.vhd')
|
||||
await checkFile('parent.vhd')
|
||||
await recoverRawContent(
|
||||
'parent.vhd',
|
||||
'recovered_from_coalescing',
|
||||
originalSize
|
||||
)
|
||||
await execa('cp', ['randomfile', 'randomfile2'])
|
||||
const fd = await fs.open('randomfile2', 'r+')
|
||||
try {
|
||||
await fs.write(fd, smallRandom, 0, smallRandom.length, 5 * SECTOR_SIZE)
|
||||
} finally {
|
||||
await fs.close(fd)
|
||||
}
|
||||
expect(await fs.readFile('recovered_from_coalescing')).toEqual(
|
||||
await fs.readFile('randomfile2')
|
||||
)
|
||||
})
|
||||
|
||||
test('createSyntheticStream passes vhd-util check', async () => {
|
||||
const initalSize = 4
|
||||
await createRandomFile('randomfile', initalSize)
|
||||
await convertFromRawToVhd('randomfile', 'randomfile.vhd')
|
||||
const handler = getHandler({ url: 'file://' + process.cwd() })
|
||||
const stream = createReadStream(handler, 'randomfile.vhd')
|
||||
await fromEvent(
|
||||
stream.pipe(await fs.createWriteStream('recovered.vhd')),
|
||||
'finish'
|
||||
)
|
||||
await checkFile('recovered.vhd')
|
||||
await execa('qemu-img', ['compare', 'recovered.vhd', 'randomfile'])
|
||||
})
|
||||
77
packages/vhd-lib/src/merge.js
Normal file
77
packages/vhd-lib/src/merge.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// TODO: remove once completely merged in vhd.js
|
||||
|
||||
import assert from 'assert'
|
||||
import concurrency from 'limit-concurrency-decorator'
|
||||
|
||||
import Vhd from './vhd'
|
||||
import { DISK_TYPE_DIFFERENCING, DISK_TYPE_DYNAMIC } from './_constants'
|
||||
|
||||
// Merge vhd child into vhd parent.
|
||||
export default concurrency(2)(async function merge (
|
||||
parentHandler,
|
||||
parentPath,
|
||||
childHandler,
|
||||
childPath
|
||||
) {
|
||||
const parentFd = await parentHandler.openFile(parentPath, 'r+')
|
||||
try {
|
||||
const parentVhd = new Vhd(parentHandler, parentFd)
|
||||
const childFd = await childHandler.openFile(childPath, 'r')
|
||||
try {
|
||||
const childVhd = new Vhd(childHandler, childFd)
|
||||
|
||||
// Reading footer and header.
|
||||
await Promise.all([
|
||||
parentVhd.readHeaderAndFooter(),
|
||||
childVhd.readHeaderAndFooter(),
|
||||
])
|
||||
|
||||
assert(childVhd.header.blockSize === parentVhd.header.blockSize)
|
||||
|
||||
const parentDiskType = parentVhd.footer.diskType
|
||||
assert(
|
||||
parentDiskType === DISK_TYPE_DIFFERENCING ||
|
||||
parentDiskType === DISK_TYPE_DYNAMIC
|
||||
)
|
||||
assert.strictEqual(childVhd.footer.diskType, DISK_TYPE_DIFFERENCING)
|
||||
|
||||
// Read allocation table of child/parent.
|
||||
await Promise.all([
|
||||
parentVhd.readBlockAllocationTable(),
|
||||
childVhd.readBlockAllocationTable(),
|
||||
])
|
||||
|
||||
await parentVhd.ensureBatSize(childVhd.header.maxTableEntries)
|
||||
|
||||
let mergedDataSize = 0
|
||||
for (
|
||||
let blockId = 0;
|
||||
blockId < childVhd.header.maxTableEntries;
|
||||
blockId++
|
||||
) {
|
||||
if (childVhd.containsBlock(blockId)) {
|
||||
mergedDataSize += await parentVhd.coalesceBlock(childVhd, blockId)
|
||||
}
|
||||
}
|
||||
|
||||
const cFooter = childVhd.footer
|
||||
const pFooter = parentVhd.footer
|
||||
|
||||
pFooter.currentSize = cFooter.currentSize
|
||||
pFooter.diskGeometry = { ...cFooter.diskGeometry }
|
||||
pFooter.originalSize = cFooter.originalSize
|
||||
pFooter.timestamp = cFooter.timestamp
|
||||
pFooter.uuid = cFooter.uuid
|
||||
|
||||
// necessary to update values and to recreate the footer after block
|
||||
// creation
|
||||
await parentVhd.writeFooter()
|
||||
|
||||
return mergedDataSize
|
||||
} finally {
|
||||
await childHandler.closeFile(childFd)
|
||||
}
|
||||
} finally {
|
||||
await parentHandler.closeFile(parentFd)
|
||||
}
|
||||
})
|
||||
134
packages/vhd-lib/src/vhd.integ.spec.js
Normal file
134
packages/vhd-lib/src/vhd.integ.spec.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/* eslint-env jest */
|
||||
import execa from 'execa'
|
||||
import rimraf from 'rimraf'
|
||||
import tmp from 'tmp'
|
||||
import { createWriteStream, readFile } from 'fs-promise'
|
||||
import { fromCallback as pFromCallback, fromEvent } from 'promise-toolbox'
|
||||
|
||||
import { createFooter } from './_createFooterHeader'
|
||||
import createReadableRawVHDStream from './createReadableRawStream'
|
||||
import createReadableSparseVHDStream from './createReadableSparseStream'
|
||||
|
||||
const initialDir = process.cwd()
|
||||
|
||||
beforeEach(async () => {
|
||||
const dir = await pFromCallback(cb => tmp.dir(cb))
|
||||
process.chdir(dir)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
const tmpDir = process.cwd()
|
||||
process.chdir(initialDir)
|
||||
await pFromCallback(cb => rimraf(tmpDir, cb))
|
||||
})
|
||||
|
||||
test('createFooter() does not crash', () => {
|
||||
createFooter(104448, Math.floor(Date.now() / 1000), {
|
||||
cylinders: 3,
|
||||
heads: 4,
|
||||
sectorsPerTrack: 17,
|
||||
})
|
||||
})
|
||||
|
||||
test('ReadableRawVHDStream does not crash', async () => {
|
||||
const data = [
|
||||
{
|
||||
offsetBytes: 100,
|
||||
data: Buffer.from('azerzaerazeraze', 'ascii'),
|
||||
},
|
||||
{
|
||||
offsetBytes: 700,
|
||||
data: Buffer.from('gdfslkdfguer', 'ascii'),
|
||||
},
|
||||
]
|
||||
let index = 0
|
||||
const mockParser = {
|
||||
next: () => {
|
||||
if (index < data.length) {
|
||||
const result = data[index]
|
||||
index++
|
||||
return result
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
}
|
||||
const fileSize = 1000
|
||||
const stream = createReadableRawVHDStream(fileSize, mockParser)
|
||||
const pipe = stream.pipe(createWriteStream('output.vhd'))
|
||||
await fromEvent(pipe, 'finish')
|
||||
await execa('vhd-util', ['check', '-t', '-i', '-n', 'output.vhd'])
|
||||
})
|
||||
|
||||
test('ReadableRawVHDStream detects when blocks are out of order', async () => {
|
||||
const data = [
|
||||
{
|
||||
offsetBytes: 700,
|
||||
data: Buffer.from('azerzaerazeraze', 'ascii'),
|
||||
},
|
||||
{
|
||||
offsetBytes: 100,
|
||||
data: Buffer.from('gdfslkdfguer', 'ascii'),
|
||||
},
|
||||
]
|
||||
let index = 0
|
||||
const mockParser = {
|
||||
next: () => {
|
||||
if (index < data.length) {
|
||||
const result = data[index]
|
||||
index++
|
||||
return result
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
}
|
||||
return expect(
|
||||
new Promise((resolve, reject) => {
|
||||
const stream = createReadableRawVHDStream(100000, mockParser)
|
||||
stream.on('error', reject)
|
||||
const pipe = stream.pipe(createWriteStream('outputStream'))
|
||||
pipe.on('finish', resolve)
|
||||
pipe.on('error', reject)
|
||||
})
|
||||
).rejects.toThrow('Received out of order blocks')
|
||||
})
|
||||
|
||||
test('ReadableSparseVHDStream can handle a sparse file', async () => {
|
||||
const blockSize = Math.pow(2, 16)
|
||||
const blocks = [
|
||||
{
|
||||
offsetBytes: blockSize * 3,
|
||||
data: Buffer.alloc(blockSize, 'azerzaerazeraze', 'ascii'),
|
||||
},
|
||||
{
|
||||
offsetBytes: blockSize * 100,
|
||||
data: Buffer.alloc(blockSize, 'gdfslkdfguer', 'ascii'),
|
||||
},
|
||||
]
|
||||
const fileSize = blockSize * 110
|
||||
const stream = createReadableSparseVHDStream(
|
||||
fileSize,
|
||||
blockSize,
|
||||
blocks.map(b => b.offsetBytes),
|
||||
blocks
|
||||
)
|
||||
const pipe = stream.pipe(createWriteStream('output.vhd'))
|
||||
await fromEvent(pipe, 'finish')
|
||||
await execa('vhd-util', ['check', '-t', '-i', '-n', 'output.vhd'])
|
||||
await execa('qemu-img', [
|
||||
'convert',
|
||||
'-f',
|
||||
'vpc',
|
||||
'-O',
|
||||
'raw',
|
||||
'output.vhd',
|
||||
'out1.raw',
|
||||
])
|
||||
const out1 = await readFile('out1.raw')
|
||||
const expected = Buffer.alloc(fileSize)
|
||||
blocks.forEach(b => {
|
||||
b.data.copy(expected, b.offsetBytes)
|
||||
})
|
||||
await expect(out1.slice(0, expected.length)).toEqual(expected)
|
||||
})
|
||||
624
packages/vhd-lib/src/vhd.js
Normal file
624
packages/vhd-lib/src/vhd.js
Normal file
@@ -0,0 +1,624 @@
|
||||
import assert from 'assert'
|
||||
import { fromEvent } from 'promise-toolbox'
|
||||
|
||||
import constantStream from './_constant-stream'
|
||||
import { fuFooter, fuHeader, checksumStruct, unpackField } from './_structs'
|
||||
import { set as mapSetBit, test as mapTestBit } from './_bitmap'
|
||||
import {
|
||||
BLOCK_UNUSED,
|
||||
DISK_TYPE_DIFFERENCING,
|
||||
DISK_TYPE_DYNAMIC,
|
||||
FILE_FORMAT_VERSION,
|
||||
FOOTER_COOKIE,
|
||||
FOOTER_SIZE,
|
||||
HEADER_COOKIE,
|
||||
HEADER_SIZE,
|
||||
HEADER_VERSION,
|
||||
PARENT_LOCATOR_ENTRIES,
|
||||
PLATFORM_NONE,
|
||||
PLATFORM_W2KU,
|
||||
SECTOR_SIZE,
|
||||
} from './_constants'
|
||||
|
||||
const VHD_UTIL_DEBUG = 0
|
||||
const debug = VHD_UTIL_DEBUG
|
||||
? str => console.log(`[vhd-merge]${str}`)
|
||||
: () => null
|
||||
|
||||
// ===================================================================
|
||||
//
|
||||
// Spec:
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=23850
|
||||
//
|
||||
// C implementation:
|
||||
// https://github.com/rubiojr/vhd-util-convert
|
||||
//
|
||||
// ===================================================================
|
||||
|
||||
const computeBatSize = entries =>
|
||||
sectorsToBytes(sectorsRoundUpNoZero(entries * 4))
|
||||
|
||||
// Sectors conversions.
|
||||
const sectorsRoundUpNoZero = bytes => Math.ceil(bytes / SECTOR_SIZE) || 1
|
||||
const sectorsToBytes = sectors => sectors * SECTOR_SIZE
|
||||
|
||||
const assertChecksum = (name, buf, struct) => {
|
||||
const actual = unpackField(struct.fields.checksum, buf)
|
||||
const expected = checksumStruct(buf, struct)
|
||||
if (actual !== expected) {
|
||||
throw new Error(`invalid ${name} checksum ${actual}, expected ${expected}`)
|
||||
}
|
||||
}
|
||||
|
||||
// unused block as buffer containing a uint32BE
|
||||
const BUF_BLOCK_UNUSED = Buffer.allocUnsafe(4)
|
||||
BUF_BLOCK_UNUSED.writeUInt32BE(BLOCK_UNUSED, 0)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Format:
|
||||
//
|
||||
// 1. Footer (512)
|
||||
// 2. Header (1024)
|
||||
// 3. Unordered entries
|
||||
// - BAT (batSize @ header.tableOffset)
|
||||
// - Blocks (@ blockOffset(i))
|
||||
// - bitmap (blockBitmapSize)
|
||||
// - data (header.blockSize)
|
||||
// - Parent locators (parentLocatorSize(i) @ parentLocatorOffset(i))
|
||||
// 4. Footer (512 @ vhdSize - 512)
|
||||
//
|
||||
// Variables:
|
||||
//
|
||||
// - batSize = min(1, ceil(header.maxTableEntries * 4 / sectorSize)) * sectorSize
|
||||
// - blockBitmapSize = ceil(header.blockSize / sectorSize / 8 / sectorSize) * sectorSize
|
||||
// - blockOffset(i) = bat[i] * sectorSize
|
||||
// - nBlocks = ceil(footer.currentSize / header.blockSize)
|
||||
// - parentLocatorOffset(i) = header.parentLocatorEntry[i].platformDataOffset
|
||||
// - parentLocatorSize(i) = header.parentLocatorEntry[i].platformDataSpace * sectorSize
|
||||
// - sectorSize = 512
|
||||
|
||||
export default class Vhd {
|
||||
get batSize () {
|
||||
return computeBatSize(this.header.maxTableEntries)
|
||||
}
|
||||
|
||||
constructor (handler, path) {
|
||||
this._handler = handler
|
||||
this._path = path
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Read functions.
|
||||
// =================================================================
|
||||
|
||||
async _read (start, n) {
|
||||
const { bytesRead, buffer } = await this._handler.read(
|
||||
this._path,
|
||||
Buffer.alloc(n),
|
||||
start
|
||||
)
|
||||
assert.equal(bytesRead, n)
|
||||
return buffer
|
||||
}
|
||||
|
||||
containsBlock (id) {
|
||||
return this._getBatEntry(id) !== BLOCK_UNUSED
|
||||
}
|
||||
|
||||
// Returns the first address after metadata. (In bytes)
|
||||
getEndOfHeaders () {
|
||||
const { header } = this
|
||||
|
||||
let end = FOOTER_SIZE + HEADER_SIZE
|
||||
|
||||
// Max(end, block allocation table end)
|
||||
end = Math.max(end, header.tableOffset + this.batSize)
|
||||
|
||||
for (let i = 0; i < PARENT_LOCATOR_ENTRIES; i++) {
|
||||
const entry = header.parentLocatorEntry[i]
|
||||
|
||||
if (entry.platformCode !== PLATFORM_NONE) {
|
||||
end = Math.max(
|
||||
end,
|
||||
entry.platformDataOffset + sectorsToBytes(entry.platformDataSpace)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
debug(`End of headers: ${end}.`)
|
||||
|
||||
return end
|
||||
}
|
||||
|
||||
// Returns the first sector after data.
|
||||
getEndOfData () {
|
||||
let end = Math.ceil(this.getEndOfHeaders() / SECTOR_SIZE)
|
||||
|
||||
const fullBlockSize = this.sectorsOfBitmap + this.sectorsPerBlock
|
||||
const { maxTableEntries } = this.header
|
||||
for (let i = 0; i < maxTableEntries; i++) {
|
||||
const blockAddr = this._getBatEntry(i)
|
||||
|
||||
if (blockAddr !== BLOCK_UNUSED) {
|
||||
end = Math.max(end, blockAddr + fullBlockSize)
|
||||
}
|
||||
}
|
||||
|
||||
debug(`End of data: ${end}.`)
|
||||
|
||||
return sectorsToBytes(end)
|
||||
}
|
||||
|
||||
// TODO: extract the checks into reusable functions:
|
||||
// - better human reporting
|
||||
// - auto repair if possible
|
||||
async readHeaderAndFooter (checkSecondFooter = true) {
|
||||
const buf = await this._read(0, FOOTER_SIZE + HEADER_SIZE)
|
||||
const bufFooter = buf.slice(0, FOOTER_SIZE)
|
||||
const bufHeader = buf.slice(FOOTER_SIZE)
|
||||
|
||||
assertChecksum('footer', bufFooter, fuFooter)
|
||||
assertChecksum('header', bufHeader, fuHeader)
|
||||
|
||||
if (checkSecondFooter) {
|
||||
const size = await this._handler.getSize(this._path)
|
||||
assert(
|
||||
bufFooter.equals(await this._read(size - FOOTER_SIZE, FOOTER_SIZE)),
|
||||
'footer1 !== footer2'
|
||||
)
|
||||
}
|
||||
|
||||
const footer = (this.footer = fuFooter.unpack(bufFooter))
|
||||
assert.strictEqual(footer.cookie, FOOTER_COOKIE, 'footer cookie')
|
||||
assert.strictEqual(footer.dataOffset, FOOTER_SIZE)
|
||||
assert.strictEqual(footer.fileFormatVersion, FILE_FORMAT_VERSION)
|
||||
assert(footer.originalSize <= footer.currentSize)
|
||||
assert(
|
||||
footer.diskType === DISK_TYPE_DIFFERENCING ||
|
||||
footer.diskType === DISK_TYPE_DYNAMIC
|
||||
)
|
||||
|
||||
const header = (this.header = fuHeader.unpack(bufHeader))
|
||||
assert.strictEqual(header.cookie, HEADER_COOKIE)
|
||||
assert.strictEqual(header.dataOffset, undefined)
|
||||
assert.strictEqual(header.headerVersion, HEADER_VERSION)
|
||||
assert(header.maxTableEntries >= footer.currentSize / header.blockSize)
|
||||
assert(Number.isInteger(Math.log2(header.blockSize / SECTOR_SIZE)))
|
||||
|
||||
// Compute the number of sectors in one block.
|
||||
// Default: One block contains 4096 sectors of 512 bytes.
|
||||
const sectorsPerBlock = (this.sectorsPerBlock =
|
||||
header.blockSize / SECTOR_SIZE)
|
||||
|
||||
// Compute bitmap size in sectors.
|
||||
// Default: 1.
|
||||
const sectorsOfBitmap = (this.sectorsOfBitmap = sectorsRoundUpNoZero(
|
||||
sectorsPerBlock >> 3
|
||||
))
|
||||
|
||||
// Full block size => data block size + bitmap size.
|
||||
this.fullBlockSize = sectorsToBytes(sectorsPerBlock + sectorsOfBitmap)
|
||||
|
||||
// In bytes.
|
||||
// Default: 512.
|
||||
this.bitmapSize = sectorsToBytes(sectorsOfBitmap)
|
||||
}
|
||||
|
||||
// Returns a buffer that contains the block allocation table of a vhd file.
|
||||
async readBlockAllocationTable () {
|
||||
const { header } = this
|
||||
this.blockTable = await this._read(
|
||||
header.tableOffset,
|
||||
header.maxTableEntries * 4
|
||||
)
|
||||
}
|
||||
|
||||
// return the first sector (bitmap) of a block
|
||||
_getBatEntry (block) {
|
||||
return this.blockTable.readUInt32BE(block * 4)
|
||||
}
|
||||
|
||||
_readBlock (blockId, onlyBitmap = false) {
|
||||
const blockAddr = this._getBatEntry(blockId)
|
||||
if (blockAddr === BLOCK_UNUSED) {
|
||||
throw new Error(`no such block ${blockId}`)
|
||||
}
|
||||
|
||||
return this._read(
|
||||
sectorsToBytes(blockAddr),
|
||||
onlyBitmap ? this.bitmapSize : this.fullBlockSize
|
||||
).then(
|
||||
buf =>
|
||||
onlyBitmap
|
||||
? { id: blockId, bitmap: buf }
|
||||
: {
|
||||
id: blockId,
|
||||
bitmap: buf.slice(0, this.bitmapSize),
|
||||
data: buf.slice(this.bitmapSize),
|
||||
buffer: buf,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// get the identifiers and first sectors of the first and last block
|
||||
// in the file
|
||||
//
|
||||
_getFirstAndLastBlocks () {
|
||||
const n = this.header.maxTableEntries
|
||||
const bat = this.blockTable
|
||||
let i = 0
|
||||
let j = 0
|
||||
let first, firstSector, last, lastSector
|
||||
|
||||
// get first allocated block for initialization
|
||||
while ((firstSector = bat.readUInt32BE(j)) === BLOCK_UNUSED) {
|
||||
i += 1
|
||||
j += 4
|
||||
|
||||
if (i === n) {
|
||||
const error = new Error('no allocated block found')
|
||||
error.noBlock = true
|
||||
throw error
|
||||
}
|
||||
}
|
||||
lastSector = firstSector
|
||||
first = last = i
|
||||
|
||||
while (i < n) {
|
||||
const sector = bat.readUInt32BE(j)
|
||||
if (sector !== BLOCK_UNUSED) {
|
||||
if (sector < firstSector) {
|
||||
first = i
|
||||
firstSector = sector
|
||||
} else if (sector > lastSector) {
|
||||
last = i
|
||||
lastSector = sector
|
||||
}
|
||||
}
|
||||
|
||||
i += 1
|
||||
j += 4
|
||||
}
|
||||
|
||||
return { first, firstSector, last, lastSector }
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Write functions.
|
||||
// =================================================================
|
||||
|
||||
// Write a buffer/stream at a given position in a vhd file.
|
||||
async _write (data, offset) {
|
||||
debug(
|
||||
`_write offset=${offset} size=${
|
||||
Buffer.isBuffer(data) ? data.length : '???'
|
||||
}`
|
||||
)
|
||||
// TODO: could probably be merged in remote handlers.
|
||||
const stream = await this._handler.createOutputStream(this._path, {
|
||||
flags: 'r+',
|
||||
start: offset,
|
||||
})
|
||||
return Buffer.isBuffer(data)
|
||||
? new Promise((resolve, reject) => {
|
||||
stream.on('error', reject)
|
||||
stream.end(data, resolve)
|
||||
})
|
||||
: fromEvent(data.pipe(stream), 'finish')
|
||||
}
|
||||
|
||||
async _freeFirstBlockSpace (spaceNeededBytes) {
|
||||
try {
|
||||
const { first, firstSector, lastSector } = this._getFirstAndLastBlocks()
|
||||
const tableOffset = this.header.tableOffset
|
||||
const { batSize } = this
|
||||
const newMinSector = Math.ceil(
|
||||
(tableOffset + batSize + spaceNeededBytes) / SECTOR_SIZE
|
||||
)
|
||||
if (
|
||||
tableOffset + batSize + spaceNeededBytes >=
|
||||
sectorsToBytes(firstSector)
|
||||
) {
|
||||
const { fullBlockSize } = this
|
||||
const newFirstSector = Math.max(
|
||||
lastSector + fullBlockSize / SECTOR_SIZE,
|
||||
newMinSector
|
||||
)
|
||||
debug(
|
||||
`freeFirstBlockSpace: move first block ${firstSector} -> ${newFirstSector}`
|
||||
)
|
||||
// copy the first block at the end
|
||||
const block = await this._read(
|
||||
sectorsToBytes(firstSector),
|
||||
fullBlockSize
|
||||
)
|
||||
await this._write(block, sectorsToBytes(newFirstSector))
|
||||
await this._setBatEntry(first, newFirstSector)
|
||||
await this.writeFooter(true)
|
||||
spaceNeededBytes -= this.fullBlockSize
|
||||
if (spaceNeededBytes > 0) {
|
||||
return this._freeFirstBlockSpace(spaceNeededBytes)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (!e.noBlock) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async ensureBatSize (entries) {
|
||||
const { header } = this
|
||||
const prevMaxTableEntries = header.maxTableEntries
|
||||
if (prevMaxTableEntries >= entries) {
|
||||
return
|
||||
}
|
||||
|
||||
const newBatSize = computeBatSize(entries)
|
||||
await this._freeFirstBlockSpace(newBatSize - this.batSize)
|
||||
const maxTableEntries = (header.maxTableEntries = entries)
|
||||
const prevBat = this.blockTable
|
||||
const bat = (this.blockTable = Buffer.allocUnsafe(newBatSize))
|
||||
prevBat.copy(bat)
|
||||
bat.fill(BUF_BLOCK_UNUSED, prevMaxTableEntries * 4)
|
||||
debug(
|
||||
`ensureBatSize: extend BAT ${prevMaxTableEntries} -> ${maxTableEntries}`
|
||||
)
|
||||
await this._write(
|
||||
constantStream(BUF_BLOCK_UNUSED, maxTableEntries - prevMaxTableEntries),
|
||||
header.tableOffset + prevBat.length
|
||||
)
|
||||
await this.writeHeader()
|
||||
}
|
||||
|
||||
// set the first sector (bitmap) of a block
|
||||
_setBatEntry (block, blockSector) {
|
||||
const i = block * 4
|
||||
const { blockTable } = this
|
||||
|
||||
blockTable.writeUInt32BE(blockSector, i)
|
||||
|
||||
return this._write(blockTable.slice(i, i + 4), this.header.tableOffset + i)
|
||||
}
|
||||
|
||||
// Make a new empty block at vhd end.
|
||||
// Update block allocation table in context and in file.
|
||||
async createBlock (blockId) {
|
||||
const blockAddr = Math.ceil(this.getEndOfData() / SECTOR_SIZE)
|
||||
|
||||
debug(`create block ${blockId} at ${blockAddr}`)
|
||||
|
||||
await Promise.all([
|
||||
// Write an empty block and addr in vhd file.
|
||||
this._write(
|
||||
constantStream([0], this.fullBlockSize),
|
||||
sectorsToBytes(blockAddr)
|
||||
),
|
||||
|
||||
this._setBatEntry(blockId, blockAddr),
|
||||
])
|
||||
|
||||
return blockAddr
|
||||
}
|
||||
|
||||
// Write a bitmap at a block address.
|
||||
async writeBlockBitmap (blockAddr, bitmap) {
|
||||
const { bitmapSize } = this
|
||||
|
||||
if (bitmap.length !== bitmapSize) {
|
||||
throw new Error(`Bitmap length is not correct ! ${bitmap.length}`)
|
||||
}
|
||||
|
||||
const offset = sectorsToBytes(blockAddr)
|
||||
|
||||
debug(
|
||||
`Write bitmap at: ${offset}. (size=${bitmapSize}, data=${bitmap.toString(
|
||||
'hex'
|
||||
)})`
|
||||
)
|
||||
await this._write(bitmap, sectorsToBytes(blockAddr))
|
||||
}
|
||||
|
||||
async writeEntireBlock (block) {
|
||||
let blockAddr = this._getBatEntry(block.id)
|
||||
|
||||
if (blockAddr === BLOCK_UNUSED) {
|
||||
blockAddr = await this.createBlock(block.id)
|
||||
}
|
||||
await this._write(block.buffer, sectorsToBytes(blockAddr))
|
||||
}
|
||||
|
||||
async writeBlockSectors (block, beginSectorId, endSectorId, parentBitmap) {
|
||||
let blockAddr = this._getBatEntry(block.id)
|
||||
|
||||
if (blockAddr === BLOCK_UNUSED) {
|
||||
blockAddr = await this.createBlock(block.id)
|
||||
parentBitmap = Buffer.alloc(this.bitmapSize, 0)
|
||||
} else if (parentBitmap === undefined) {
|
||||
parentBitmap = (await this._readBlock(block.id, true)).bitmap
|
||||
}
|
||||
|
||||
const offset = blockAddr + this.sectorsOfBitmap + beginSectorId
|
||||
|
||||
debug(
|
||||
`writeBlockSectors at ${offset} block=${
|
||||
block.id
|
||||
}, sectors=${beginSectorId}...${endSectorId}`
|
||||
)
|
||||
|
||||
for (let i = beginSectorId; i < endSectorId; ++i) {
|
||||
mapSetBit(parentBitmap, i)
|
||||
}
|
||||
|
||||
await this.writeBlockBitmap(blockAddr, parentBitmap)
|
||||
await this._write(
|
||||
block.data.slice(
|
||||
sectorsToBytes(beginSectorId),
|
||||
sectorsToBytes(endSectorId)
|
||||
),
|
||||
sectorsToBytes(offset)
|
||||
)
|
||||
}
|
||||
|
||||
async coalesceBlock (child, blockId) {
|
||||
const block = await child._readBlock(blockId)
|
||||
const { bitmap, data } = block
|
||||
|
||||
debug(`coalesceBlock block=${blockId}`)
|
||||
|
||||
// For each sector of block data...
|
||||
const { sectorsPerBlock } = child
|
||||
let parentBitmap = null
|
||||
for (let i = 0; i < sectorsPerBlock; i++) {
|
||||
// If no changes on one sector, skip.
|
||||
if (!mapTestBit(bitmap, i)) {
|
||||
continue
|
||||
}
|
||||
let endSector = i + 1
|
||||
|
||||
// Count changed sectors.
|
||||
while (endSector < sectorsPerBlock && mapTestBit(bitmap, endSector)) {
|
||||
++endSector
|
||||
}
|
||||
|
||||
// Write n sectors into parent.
|
||||
debug(`coalesceBlock: write sectors=${i}...${endSector}`)
|
||||
|
||||
const isFullBlock = i === 0 && endSector === sectorsPerBlock
|
||||
if (isFullBlock) {
|
||||
await this.writeEntireBlock(block)
|
||||
} else {
|
||||
if (parentBitmap === null) {
|
||||
parentBitmap = (await this._readBlock(blockId, true)).bitmap
|
||||
}
|
||||
await this.writeBlockSectors(block, i, endSector, parentBitmap)
|
||||
}
|
||||
|
||||
i = endSector
|
||||
}
|
||||
|
||||
// Return the merged data size
|
||||
return data.length
|
||||
}
|
||||
|
||||
// Write a context footer. (At the end and beginning of a vhd file.)
|
||||
async writeFooter (onlyEndFooter = false) {
|
||||
const { footer } = this
|
||||
|
||||
const rawFooter = fuFooter.pack(footer)
|
||||
const eof = await this._handler.getSize(this._path)
|
||||
// sometimes the file is longer than anticipated, we still need to put the footer at the end
|
||||
const offset = Math.max(this.getEndOfData(), eof - rawFooter.length)
|
||||
|
||||
footer.checksum = checksumStruct(rawFooter, fuFooter)
|
||||
debug(
|
||||
`Write footer at: ${offset} (checksum=${
|
||||
footer.checksum
|
||||
}). (data=${rawFooter.toString('hex')})`
|
||||
)
|
||||
if (!onlyEndFooter) {
|
||||
await this._write(rawFooter, 0)
|
||||
}
|
||||
await this._write(rawFooter, offset)
|
||||
}
|
||||
|
||||
writeHeader () {
|
||||
const { header } = this
|
||||
const rawHeader = fuHeader.pack(header)
|
||||
header.checksum = checksumStruct(rawHeader, fuHeader)
|
||||
const offset = FOOTER_SIZE
|
||||
debug(
|
||||
`Write header at: ${offset} (checksum=${
|
||||
header.checksum
|
||||
}). (data=${rawHeader.toString('hex')})`
|
||||
)
|
||||
return this._write(rawHeader, offset)
|
||||
}
|
||||
|
||||
async writeData (offsetSectors, buffer) {
|
||||
const bufferSizeSectors = Math.ceil(buffer.length / SECTOR_SIZE)
|
||||
const startBlock = Math.floor(offsetSectors / this.sectorsPerBlock)
|
||||
const endBufferSectors = offsetSectors + bufferSizeSectors
|
||||
const lastBlock = Math.ceil(endBufferSectors / this.sectorsPerBlock) - 1
|
||||
await this.ensureBatSize(lastBlock)
|
||||
const blockSizeBytes = this.sectorsPerBlock * SECTOR_SIZE
|
||||
const coversWholeBlock = (offsetInBlockSectors, endInBlockSectors) =>
|
||||
offsetInBlockSectors === 0 && endInBlockSectors === this.sectorsPerBlock
|
||||
|
||||
for (
|
||||
let currentBlock = startBlock;
|
||||
currentBlock <= lastBlock;
|
||||
currentBlock++
|
||||
) {
|
||||
const offsetInBlockSectors = Math.max(
|
||||
0,
|
||||
offsetSectors - currentBlock * this.sectorsPerBlock
|
||||
)
|
||||
const endInBlockSectors = Math.min(
|
||||
endBufferSectors - currentBlock * this.sectorsPerBlock,
|
||||
this.sectorsPerBlock
|
||||
)
|
||||
const startInBuffer = Math.max(
|
||||
0,
|
||||
(currentBlock * this.sectorsPerBlock - offsetSectors) * SECTOR_SIZE
|
||||
)
|
||||
const endInBuffer = Math.min(
|
||||
((currentBlock + 1) * this.sectorsPerBlock - offsetSectors) *
|
||||
SECTOR_SIZE,
|
||||
buffer.length
|
||||
)
|
||||
let inputBuffer
|
||||
if (coversWholeBlock(offsetInBlockSectors, endInBlockSectors)) {
|
||||
inputBuffer = buffer.slice(startInBuffer, endInBuffer)
|
||||
} else {
|
||||
inputBuffer = Buffer.alloc(blockSizeBytes, 0)
|
||||
buffer.copy(
|
||||
inputBuffer,
|
||||
offsetInBlockSectors * SECTOR_SIZE,
|
||||
startInBuffer,
|
||||
endInBuffer
|
||||
)
|
||||
}
|
||||
await this.writeBlockSectors(
|
||||
{ id: currentBlock, data: inputBuffer },
|
||||
offsetInBlockSectors,
|
||||
endInBlockSectors
|
||||
)
|
||||
}
|
||||
await this.writeFooter()
|
||||
}
|
||||
|
||||
async ensureSpaceForParentLocators (neededSectors) {
|
||||
const firstLocatorOffset = FOOTER_SIZE + HEADER_SIZE
|
||||
const currentSpace =
|
||||
Math.floor(this.header.tableOffset / SECTOR_SIZE) -
|
||||
firstLocatorOffset / SECTOR_SIZE
|
||||
if (currentSpace < neededSectors) {
|
||||
const deltaSectors = neededSectors - currentSpace
|
||||
await this._freeFirstBlockSpace(sectorsToBytes(deltaSectors))
|
||||
this.header.tableOffset += sectorsToBytes(deltaSectors)
|
||||
await this._write(this.blockTable, this.header.tableOffset)
|
||||
}
|
||||
return firstLocatorOffset
|
||||
}
|
||||
|
||||
async setUniqueParentLocator (fileNameString) {
|
||||
const { header } = this
|
||||
header.parentLocatorEntry[0].platformCode = PLATFORM_W2KU
|
||||
const encodedFilename = Buffer.from(fileNameString, 'utf16le')
|
||||
const dataSpaceSectors = Math.ceil(encodedFilename.length / SECTOR_SIZE)
|
||||
const position = await this.ensureSpaceForParentLocators(dataSpaceSectors)
|
||||
await this._write(encodedFilename, position)
|
||||
header.parentLocatorEntry[0].platformDataSpace =
|
||||
dataSpaceSectors * SECTOR_SIZE
|
||||
header.parentLocatorEntry[0].platformDataLength = encodedFilename.length
|
||||
header.parentLocatorEntry[0].platformDataOffset = position
|
||||
for (let i = 1; i < 8; i++) {
|
||||
header.parentLocatorEntry[i].platformCode = PLATFORM_NONE
|
||||
header.parentLocatorEntry[i].platformDataSpace = 0
|
||||
header.parentLocatorEntry[i].platformDataLength = 0
|
||||
header.parentLocatorEntry[i].platformDataOffset = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
24
packages/xen-api/.npmignore
Normal file
24
packages/xen-api/.npmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
/benchmark/
|
||||
/benchmarks/
|
||||
*.bench.js
|
||||
*.bench.js.map
|
||||
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/fixture/
|
||||
/fixtures/
|
||||
*.fixture.js
|
||||
*.fixture.js.map
|
||||
*.fixtures.js
|
||||
*.fixtures.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
__snapshots__/
|
||||
130
packages/xen-api/README.md
Normal file
130
packages/xen-api/README.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# xen-api [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> Connector to the Xen API
|
||||
|
||||
Tested with:
|
||||
|
||||
- XenServer 7.3
|
||||
- XenServer 7.2
|
||||
- XenServer 7.1
|
||||
- XenServer 7
|
||||
- XenServer 6.5
|
||||
- XenServer 6.2
|
||||
- XenServer 5.6
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xen-api):
|
||||
|
||||
```
|
||||
> npm install --save xen-api
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Library
|
||||
|
||||
```javascript
|
||||
const { createClient } = require('xen-api')
|
||||
|
||||
const xapi = createClient({
|
||||
url: 'https://xen1.company.net',
|
||||
allowUnauthorized: false,
|
||||
auth: {
|
||||
user: 'root',
|
||||
password: 'important secret password'
|
||||
},
|
||||
readOnly: false
|
||||
})
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `url`: address of a host in the pool we are trying to connect to
|
||||
- `allowUnauthorized`: whether to accept self-signed certificates
|
||||
- `auth`: credentials used to sign in (can also be specified in the URL)
|
||||
- `readOnly = false`: if true, no methods with side-effects can be called
|
||||
|
||||
```js
|
||||
// Force connection.
|
||||
xapi.connect().catch(error => {
|
||||
console.error(error)
|
||||
})
|
||||
|
||||
// Watch objects.
|
||||
xapi.objects.on('add', objects => {
|
||||
console.log('new objects:', objects)
|
||||
})
|
||||
```
|
||||
|
||||
> Note: all objects are frozen and cannot be altered!
|
||||
|
||||
Custom fields on objects (hidden − ie. non enumerable):
|
||||
- `$type`: the type of the object (`VM`, `task`, …);
|
||||
- `$ref`: the (opaque) reference of the object;
|
||||
- `$id`: the identifier of this object (its UUID if any, otherwise its reference);
|
||||
- `$pool`: the pool object this object belongs to.
|
||||
|
||||
Furthermore, any field containing a reference (or references if an
|
||||
array) can be resolved by prepending the field name with a `$`:
|
||||
|
||||
```javascript
|
||||
console.log(xapi.pool.$master.$resident_VMs[0].name_label)
|
||||
// vm1
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
A CLI is provided to help exploration and discovery of the XAPI.
|
||||
|
||||
```
|
||||
> xen-api https://xen1.company.net root
|
||||
Password: ******
|
||||
root@xen1.company.net> xapi.status
|
||||
'connected'
|
||||
root@xen1.company.net> xapi.pool.master
|
||||
'OpaqueRef:ec7c5147-8aee-990f-c70b-0de916a8e993'
|
||||
root@xen1.company.net> xapi.pool.$master.name_label
|
||||
'xen1'
|
||||
```
|
||||
|
||||
To ease searches, `find()` and `findAll()` functions are available:
|
||||
|
||||
```
|
||||
root@xen1.company.net> findAll({ $type: 'vm' }).length
|
||||
183
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install
|
||||
|
||||
# Run the tests
|
||||
> npm test
|
||||
|
||||
# Continuously compile
|
||||
> npm run dev
|
||||
|
||||
# Continuously run the tests
|
||||
> npm run dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> npm run build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/xen-api/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Julien Fontanet](https://github.com/julien-f)
|
||||
53
packages/xen-api/examples/export-vdi
Executable file
53
packages/xen-api/examples/export-vdi
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
process.env.DEBUG = '*'
|
||||
|
||||
const defer = require('golike-defer').default
|
||||
const pump = require('pump')
|
||||
const { CancelToken, fromCallback } = require('promise-toolbox')
|
||||
|
||||
const { createClient } = require('../')
|
||||
|
||||
const { createOutputStream, resolveRef } = require('./utils')
|
||||
|
||||
defer(async ($defer, args) => {
|
||||
let raw = false
|
||||
if (args[0] === '--raw') {
|
||||
raw = true
|
||||
args.shift()
|
||||
}
|
||||
|
||||
if (args.length < 2) {
|
||||
return console.log('Usage: export-vdi [--raw] <XS URL> <VDI identifier> [<VHD file>]')
|
||||
}
|
||||
|
||||
const xapi = createClient({
|
||||
allowUnauthorized: true,
|
||||
url: args[0],
|
||||
watchEvents: false
|
||||
})
|
||||
|
||||
await xapi.connect()
|
||||
$defer(() => xapi.disconnect())
|
||||
|
||||
const { cancel, token } = CancelToken.source()
|
||||
process.on('SIGINT', cancel)
|
||||
|
||||
// https://xapi-project.github.io/xen-api/snapshots.html#downloading-a-disk-or-snapshot
|
||||
const exportStream = await xapi.getResource(token, '/export_raw_vdi/', {
|
||||
query: {
|
||||
format: raw ? 'raw' : 'vhd',
|
||||
vdi: await resolveRef(xapi, 'VDI', args[1])
|
||||
}
|
||||
})
|
||||
|
||||
console.warn('Export task:', exportStream.headers['task-id'])
|
||||
|
||||
await fromCallback(cb => pump(
|
||||
exportStream,
|
||||
createOutputStream(args[2]),
|
||||
cb
|
||||
))
|
||||
})(process.argv.slice(2)).catch(
|
||||
console.error.bind(console, 'error')
|
||||
)
|
||||
47
packages/xen-api/examples/export-vm
Executable file
47
packages/xen-api/examples/export-vm
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
process.env.DEBUG = '*'
|
||||
|
||||
const defer = require('golike-defer').default
|
||||
const pump = require('pump')
|
||||
const { CancelToken, fromCallback } = require('promise-toolbox')
|
||||
|
||||
const { createClient } = require('../')
|
||||
|
||||
const { createOutputStream, resolveRef } = require('./utils')
|
||||
|
||||
defer(async ($defer, args) => {
|
||||
if (args.length < 2) {
|
||||
return console.log('Usage: export-vm <XS URL> <VM identifier> [<XVA file>]')
|
||||
}
|
||||
|
||||
const xapi = createClient({
|
||||
allowUnauthorized: true,
|
||||
url: args[0],
|
||||
watchEvents: false
|
||||
})
|
||||
|
||||
await xapi.connect()
|
||||
$defer(() => xapi.disconnect())
|
||||
|
||||
const { cancel, token } = CancelToken.source()
|
||||
process.on('SIGINT', cancel)
|
||||
|
||||
// https://xapi-project.github.io/xen-api/importexport.html
|
||||
const exportStream = await xapi.getResource(token, '/export/', {
|
||||
query: {
|
||||
ref: await resolveRef(xapi, 'VM', args[1]),
|
||||
use_compression: 'true'
|
||||
}
|
||||
})
|
||||
|
||||
console.warn('Export task:', exportStream.headers['task-id'])
|
||||
|
||||
await fromCallback(cb => pump(
|
||||
exportStream,
|
||||
createOutputStream(args[2]),
|
||||
cb
|
||||
))
|
||||
})(process.argv.slice(2)).catch(
|
||||
console.error.bind(console, 'error')
|
||||
)
|
||||
44
packages/xen-api/examples/import-vdi
Executable file
44
packages/xen-api/examples/import-vdi
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
process.env.DEBUG = '*'
|
||||
|
||||
const defer = require('golike-defer').default
|
||||
const { CancelToken } = require('promise-toolbox')
|
||||
|
||||
const { createClient } = require('../')
|
||||
|
||||
const { createInputStream, resolveRef } = require('./utils')
|
||||
|
||||
defer(async ($defer, args) => {
|
||||
let raw = false
|
||||
if (args[0] === '--raw') {
|
||||
raw = true
|
||||
args.shift()
|
||||
}
|
||||
|
||||
if (args.length < 2) {
|
||||
return console.log('Usage: import-vdi [--raw] <XS URL> <VDI identifier> [<VHD file>]')
|
||||
}
|
||||
|
||||
const xapi = createClient({
|
||||
allowUnauthorized: true,
|
||||
url: args[0],
|
||||
watchEvents: false
|
||||
})
|
||||
|
||||
await xapi.connect()
|
||||
$defer(() => xapi.disconnect())
|
||||
|
||||
const { cancel, token } = CancelToken.source()
|
||||
process.on('SIGINT', cancel)
|
||||
|
||||
// https://xapi-project.github.io/xen-api/snapshots.html#uploading-a-disk-or-snapshot
|
||||
await xapi.putResource(token, createInputStream(args[2]), '/import_raw_vdi/', {
|
||||
query: {
|
||||
format: raw ? 'raw' : 'vhd',
|
||||
vdi: await resolveRef(xapi, 'VDI', args[1])
|
||||
}
|
||||
})
|
||||
})(process.argv.slice(2)).catch(
|
||||
console.error.bind(console, 'error')
|
||||
)
|
||||
35
packages/xen-api/examples/import-vm
Executable file
35
packages/xen-api/examples/import-vm
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
process.env.DEBUG = '*'
|
||||
|
||||
const defer = require('golike-defer').default
|
||||
const { CancelToken } = require('promise-toolbox')
|
||||
|
||||
const { createClient } = require('../')
|
||||
|
||||
const { createInputStream, resolveRef } = require('./utils')
|
||||
|
||||
defer(async ($defer, args) => {
|
||||
if (args.length < 1) {
|
||||
return console.log('Usage: import-vm <XS URL> [<XVA file>] [<SR identifier>]')
|
||||
}
|
||||
|
||||
const xapi = createClient({
|
||||
allowUnauthorized: true,
|
||||
url: args[0],
|
||||
watchEvents: false
|
||||
})
|
||||
|
||||
await xapi.connect()
|
||||
$defer(() => xapi.disconnect())
|
||||
|
||||
const { cancel, token } = CancelToken.source()
|
||||
process.on('SIGINT', cancel)
|
||||
|
||||
// https://xapi-project.github.io/xen-api/importexport.html
|
||||
await xapi.putResource(token, createInputStream(args[1]), '/import/', {
|
||||
query: args[2] && { sr_id: await resolveRef(xapi, 'SR', args[2]) }
|
||||
})
|
||||
})(process.argv.slice(2)).catch(
|
||||
console.error.bind(console, 'error')
|
||||
)
|
||||
57
packages/xen-api/examples/log-events
Executable file
57
packages/xen-api/examples/log-events
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('source-map-support').install()
|
||||
|
||||
const { forEach, size } = require('lodash')
|
||||
|
||||
const { createClient } = require('../')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
return console.log('Usage: log-events <XS URL>')
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Creation
|
||||
|
||||
const xapi = createClient({
|
||||
allowUnauthorized: true,
|
||||
url: process.argv[2]
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Method call
|
||||
|
||||
xapi.connect().then(() => {
|
||||
xapi.call('VM.get_all_records')
|
||||
.then(function (vms) {
|
||||
console.log('%s VMs fetched', size(vms))
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Objects
|
||||
|
||||
const objects = xapi.objects
|
||||
|
||||
objects.on('add', objects => {
|
||||
forEach(objects, object => {
|
||||
console.log('+ %s: %s', object.$type, object.$id)
|
||||
})
|
||||
})
|
||||
|
||||
objects.on('update', objects => {
|
||||
forEach(objects, object => {
|
||||
console.log('± %s: %s', object.$type, object.$id)
|
||||
})
|
||||
})
|
||||
|
||||
objects.on('remove', objects => {
|
||||
forEach(objects, (value, id) => {
|
||||
console.log('- %s', id)
|
||||
})
|
||||
})
|
||||
6
packages/xen-api/examples/package.json
Normal file
6
packages/xen-api/examples/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.4.1",
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
}
|
||||
43
packages/xen-api/examples/utils.js
Normal file
43
packages/xen-api/examples/utils.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const { createReadStream, createWriteStream, statSync } = require('fs')
|
||||
const { PassThrough } = require('stream')
|
||||
|
||||
const { isOpaqueRef } = require('../')
|
||||
|
||||
exports.createInputStream = path => {
|
||||
if (path === undefined || path === '-') {
|
||||
return process.stdin
|
||||
}
|
||||
|
||||
const { size } = statSync(path)
|
||||
|
||||
const stream = createReadStream(path)
|
||||
stream.length = size
|
||||
return stream
|
||||
}
|
||||
|
||||
exports.createOutputStream = path => {
|
||||
if (path !== undefined && path !== '-') {
|
||||
return createWriteStream(path)
|
||||
}
|
||||
|
||||
// introduce a through stream because stdout is not a normal stream!
|
||||
const stream = new PassThrough()
|
||||
stream.pipe(process.stdout)
|
||||
return stream
|
||||
}
|
||||
|
||||
exports.resolveRef = (xapi, type, refOrUuidOrNameLabel) =>
|
||||
isOpaqueRef(refOrUuidOrNameLabel)
|
||||
? refOrUuidOrNameLabel
|
||||
: xapi.call(`${type}.get_by_uuid`, refOrUuidOrNameLabel).catch(() =>
|
||||
xapi
|
||||
.call(`${type}.get_by_name_label`, refOrUuidOrNameLabel)
|
||||
.then(refs => {
|
||||
if (refs.length === 1) {
|
||||
return refs[0]
|
||||
}
|
||||
throw new Error(
|
||||
`no single match for ${type} with name label ${refOrUuidOrNameLabel}`
|
||||
)
|
||||
})
|
||||
)
|
||||
30
packages/xen-api/examples/yarn.lock
Normal file
30
packages/xen-api/examples/yarn.lock
Normal file
@@ -0,0 +1,30 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
end-of-stream@^1.1.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
|
||||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
golike-defer@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/golike-defer/-/golike-defer-0.4.1.tgz#7a1cd435d61e461305805d980b133a0f3db4e1cc"
|
||||
|
||||
once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||
dependencies:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
4
packages/xen-api/memory-test.gnu
Normal file
4
packages/xen-api/memory-test.gnu
Normal file
@@ -0,0 +1,4 @@
|
||||
set yrange [ 0 : ]
|
||||
set grid
|
||||
|
||||
plot for [i=2:4] "plot.dat" using 1:i with lines
|
||||
89
packages/xen-api/package.json
Normal file
89
packages/xen-api/package.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"name": "xen-api",
|
||||
"version": "0.16.11",
|
||||
"license": "ISC",
|
||||
"description": "Connector to the Xen API",
|
||||
"keywords": [
|
||||
"xen",
|
||||
"api",
|
||||
"xen-api",
|
||||
"xenapi",
|
||||
"xapi"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xen-api",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {
|
||||
"xen-api": "dist/cli.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-polyfill": "^6.23.0",
|
||||
"blocked": "^1.2.1",
|
||||
"debug": "^3.1.0",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"http-request-plus": "^0.5.0",
|
||||
"iterable-backoff": "^0.0.0",
|
||||
"json-rpc-protocol": "^0.11.2",
|
||||
"kindof": "^2.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"make-error": "^1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
"pw": "0.0.4",
|
||||
"xmlrpc": "^1.3.2",
|
||||
"xo-collection": "^0.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-function-bind": "^6.22.0",
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"babel-preset-stage-3": "^6.24.1",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"plot": "gnuplot -p memory-test.gnu",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"lodash",
|
||||
"transform-decorators-legacy",
|
||||
"transform-function-bind"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"node": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"stage-3"
|
||||
]
|
||||
}
|
||||
}
|
||||
106
packages/xen-api/src/cli.js
Executable file
106
packages/xen-api/src/cli.js
Executable file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import 'babel-polyfill'
|
||||
|
||||
import blocked from 'blocked'
|
||||
import createDebug from 'debug'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import execPromise from 'exec-promise'
|
||||
import minimist from 'minimist'
|
||||
import pw from 'pw'
|
||||
import { asCallback, fromCallback } from 'promise-toolbox'
|
||||
import { filter, find, isArray } from 'lodash'
|
||||
import { start as createRepl } from 'repl'
|
||||
|
||||
import { createClient } from './'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function askPassword (prompt = 'Password: ') {
|
||||
if (prompt) {
|
||||
process.stdout.write(prompt)
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
pw(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const usage = 'Usage: xen-api <url> [<user> [<password>]]'
|
||||
|
||||
const main = async args => {
|
||||
const opts = minimist(args, {
|
||||
boolean: ['allow-unauthorized', 'help', 'read-only', 'verbose'],
|
||||
|
||||
alias: {
|
||||
'allow-unauthorized': 'au',
|
||||
debounce: 'd',
|
||||
help: 'h',
|
||||
'read-only': 'ro',
|
||||
verbose: 'v',
|
||||
},
|
||||
})
|
||||
|
||||
if (opts.help) {
|
||||
return usage
|
||||
}
|
||||
|
||||
if (opts.verbose) {
|
||||
// Does not work perfectly.
|
||||
//
|
||||
// https://github.com/visionmedia/debug/pull/156
|
||||
createDebug.enable('xen-api,xen-api:*')
|
||||
}
|
||||
|
||||
let auth
|
||||
if (opts._.length > 1) {
|
||||
const [, user, password = await askPassword()] = opts._
|
||||
auth = { user, password }
|
||||
}
|
||||
|
||||
{
|
||||
const debug = createDebug('xen-api:perf')
|
||||
blocked(ms => {
|
||||
debug('blocked for %sms', ms | 0)
|
||||
})
|
||||
}
|
||||
|
||||
const xapi = createClient({
|
||||
url: opts._[0],
|
||||
allowUnauthorized: opts.au,
|
||||
auth,
|
||||
debounce: opts.debounce != null ? +opts.debounce : null,
|
||||
readOnly: opts.ro,
|
||||
})
|
||||
await xapi.connect()
|
||||
|
||||
const repl = createRepl({
|
||||
prompt: `${xapi._humanId}> `,
|
||||
})
|
||||
repl.context.xapi = xapi
|
||||
|
||||
repl.context.find = predicate => find(xapi.objects.all, predicate)
|
||||
repl.context.findAll = predicate => filter(xapi.objects.all, predicate)
|
||||
|
||||
// Make the REPL waits for promise completion.
|
||||
repl.eval = (evaluate => (cmd, context, filename, cb) => {
|
||||
;fromCallback(cb => {
|
||||
evaluate.call(repl, cmd, context, filename, cb)
|
||||
})
|
||||
.then(value => (isArray(value) ? Promise.all(value) : value))
|
||||
::asCallback(cb)
|
||||
})(repl.eval)
|
||||
|
||||
await eventToPromise(repl, 'exit')
|
||||
|
||||
try {
|
||||
await xapi.disconnect()
|
||||
} catch (error) {}
|
||||
}
|
||||
export default main
|
||||
|
||||
if (!module.parent) {
|
||||
execPromise(main)
|
||||
}
|
||||
1073
packages/xen-api/src/index.js
Normal file
1073
packages/xen-api/src/index.js
Normal file
File diff suppressed because it is too large
Load Diff
32
packages/xen-api/src/inject-events.js
Executable file
32
packages/xen-api/src/inject-events.js
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { delay as pDelay } from 'promise-toolbox'
|
||||
|
||||
import { createClient } from './'
|
||||
|
||||
const xapi = (() => {
|
||||
const [, , url, user, password] = process.argv
|
||||
|
||||
return createClient({
|
||||
auth: { user, password },
|
||||
url,
|
||||
watchEvents: false,
|
||||
})
|
||||
})()
|
||||
|
||||
xapi
|
||||
.connect()
|
||||
|
||||
// Get the pool record's ref.
|
||||
.then(() => xapi.call('pool.get_all'))
|
||||
|
||||
// Injects lots of events.
|
||||
.then(([poolRef]) => {
|
||||
const loop = () =>
|
||||
xapi
|
||||
.call('event.inject', 'pool', poolRef)
|
||||
::pDelay(10) // A small delay is required to avoid overloading the Xen API.
|
||||
.then(loop)
|
||||
|
||||
return loop()
|
||||
})
|
||||
22
packages/xen-api/src/memory-test.js
Executable file
22
packages/xen-api/src/memory-test.js
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createClient } from './'
|
||||
|
||||
let i = 0
|
||||
setInterval(() => {
|
||||
const usage = process.memoryUsage()
|
||||
console.log(
|
||||
'%s %s %s %s',
|
||||
i++,
|
||||
Math.round(usage.rss / 1e6),
|
||||
Math.round(usage.heapTotal / 1e6),
|
||||
Math.round(usage.heapUsed / 1e6)
|
||||
)
|
||||
}, 1e2)
|
||||
|
||||
const [, , url, user, password] = process.argv
|
||||
createClient({
|
||||
auth: { user, password },
|
||||
readOnly: true,
|
||||
url,
|
||||
}).connect()
|
||||
3
packages/xen-api/src/transports/_utils.js
Normal file
3
packages/xen-api/src/transports/_utils.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import makeError from 'make-error'
|
||||
|
||||
export const UnsupportedTransport = makeError('UnsupportedTransport')
|
||||
36
packages/xen-api/src/transports/auto.js
Normal file
36
packages/xen-api/src/transports/auto.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import jsonRpc from './json-rpc'
|
||||
import xmlRpc from './xml-rpc'
|
||||
import xmlRpcJson from './xml-rpc-json'
|
||||
import { UnsupportedTransport } from './_utils'
|
||||
|
||||
const factories = [jsonRpc, xmlRpcJson, xmlRpc]
|
||||
const { length } = factories
|
||||
|
||||
export default opts => {
|
||||
let i = 0
|
||||
|
||||
let call
|
||||
function create () {
|
||||
const current = factories[i++](opts)
|
||||
if (i < length) {
|
||||
const currentI = i
|
||||
call = (method, args) =>
|
||||
current(method, args).catch(error => {
|
||||
if (error instanceof UnsupportedTransport) {
|
||||
if (currentI === i) {
|
||||
// not changed yet
|
||||
create()
|
||||
}
|
||||
return call(method, args)
|
||||
}
|
||||
|
||||
throw error
|
||||
})
|
||||
} else {
|
||||
call = current
|
||||
}
|
||||
}
|
||||
create()
|
||||
|
||||
return (method, args) => call(method, args)
|
||||
}
|
||||
43
packages/xen-api/src/transports/json-rpc.js
Normal file
43
packages/xen-api/src/transports/json-rpc.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import httpRequestPlus from 'http-request-plus'
|
||||
import { format, parse } from 'json-rpc-protocol'
|
||||
|
||||
import { UnsupportedTransport } from './_utils'
|
||||
|
||||
export default ({ allowUnauthorized, url }) => {
|
||||
return (method, args) =>
|
||||
httpRequestPlus
|
||||
.post(url, {
|
||||
rejectUnauthorized: !allowUnauthorized,
|
||||
body: format.request(0, method, args),
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
path: '/jsonrpc',
|
||||
})
|
||||
.readAll('utf8')
|
||||
.then(
|
||||
text => {
|
||||
let response
|
||||
try {
|
||||
response = parse(text)
|
||||
} catch (error) {
|
||||
throw new UnsupportedTransport()
|
||||
}
|
||||
|
||||
if (response.type === 'response') {
|
||||
return response.result
|
||||
}
|
||||
|
||||
throw response.error
|
||||
},
|
||||
error => {
|
||||
if (error.response !== undefined) {
|
||||
// HTTP error
|
||||
throw new UnsupportedTransport()
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
)
|
||||
}
|
||||
87
packages/xen-api/src/transports/xml-rpc-json.js
Normal file
87
packages/xen-api/src/transports/xml-rpc-json.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import { createClient, createSecureClient } from 'xmlrpc'
|
||||
import { promisify } from 'promise-toolbox'
|
||||
|
||||
import { UnsupportedTransport } from './_utils'
|
||||
|
||||
const logError = error => {
|
||||
if (error.res) {
|
||||
console.error(
|
||||
'XML-RPC Error: %s (response status %s)',
|
||||
error.message,
|
||||
error.res.statusCode
|
||||
)
|
||||
console.error('%s', error.body)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
const SPECIAL_CHARS = {
|
||||
'\r': '\\r',
|
||||
'\t': '\\t',
|
||||
}
|
||||
const SPECIAL_CHARS_RE = new RegExp(Object.keys(SPECIAL_CHARS).join('|'), 'g')
|
||||
|
||||
const parseResult = result => {
|
||||
const status = result.Status
|
||||
|
||||
// Return the plain result if it does not have a valid XAPI
|
||||
// format.
|
||||
if (status === undefined) {
|
||||
return result
|
||||
}
|
||||
|
||||
if (status !== 'Success') {
|
||||
throw result.ErrorDescription
|
||||
}
|
||||
|
||||
const value = result.Value
|
||||
|
||||
// XAPI returns an empty string (invalid JSON) for an empty
|
||||
// result.
|
||||
if (value === '') {
|
||||
return ''
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (error) {
|
||||
// XAPI JSON sometimes contains invalid characters.
|
||||
if (!(error instanceof SyntaxError)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
let replaced = false
|
||||
const fixedValue = value.replace(SPECIAL_CHARS_RE, match => {
|
||||
replaced = true
|
||||
return SPECIAL_CHARS[match]
|
||||
})
|
||||
|
||||
if (replaced) {
|
||||
try {
|
||||
return JSON.parse(fixedValue)
|
||||
} catch (error) {
|
||||
if (!(error instanceof SyntaxError)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnsupportedTransport()
|
||||
}
|
||||
|
||||
export default ({
|
||||
allowUnauthorized,
|
||||
url: { hostname, path, port, protocol },
|
||||
}) => {
|
||||
const client = (protocol === 'https:' ? createSecureClient : createClient)({
|
||||
host: hostname,
|
||||
path: '/json',
|
||||
port,
|
||||
rejectUnauthorized: !allowUnauthorized,
|
||||
})
|
||||
const call = promisify(client.methodCall, client)
|
||||
|
||||
return (method, args) => call(method, args).then(parseResult, logError)
|
||||
}
|
||||
45
packages/xen-api/src/transports/xml-rpc.js
Normal file
45
packages/xen-api/src/transports/xml-rpc.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { createClient, createSecureClient } from 'xmlrpc'
|
||||
import { promisify } from 'promise-toolbox'
|
||||
|
||||
const logError = error => {
|
||||
if (error.res) {
|
||||
console.error(
|
||||
'XML-RPC Error: %s (response status %s)',
|
||||
error.message,
|
||||
error.res.statusCode
|
||||
)
|
||||
console.error('%s', error.body)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
const parseResult = result => {
|
||||
const status = result.Status
|
||||
|
||||
// Return the plain result if it does not have a valid XAPI
|
||||
// format.
|
||||
if (status === undefined) {
|
||||
return result
|
||||
}
|
||||
|
||||
if (status !== 'Success') {
|
||||
throw result.ErrorDescription
|
||||
}
|
||||
|
||||
return result.Value
|
||||
}
|
||||
|
||||
export default ({
|
||||
allowUnauthorized,
|
||||
url: { hostname, path, port, protocol },
|
||||
}) => {
|
||||
const client = (protocol === 'https:' ? createSecureClient : createClient)({
|
||||
host: hostname,
|
||||
port,
|
||||
rejectUnauthorized: !allowUnauthorized,
|
||||
})
|
||||
const call = promisify(client.methodCall, client)
|
||||
|
||||
return (method, args) => call(method, args).then(parseResult, logError)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user