mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-06 14:45:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66d04cb83f | ||
|
|
42cb9b76c4 | ||
|
|
21315f709d | ||
|
|
42b8f1145a | ||
|
|
39ea8484c1 | ||
|
|
168f461761 | ||
|
|
8f78a069c6 | ||
|
|
4a490fe35a | ||
|
|
5741134ca0 | ||
|
|
d9189c857c | ||
|
|
df4b9df142 | ||
|
|
ec5a305624 | ||
|
|
9d3be33e5c | ||
|
|
2371770fe8 | ||
|
|
36d1e739e6 | ||
|
|
adf12c8825 | ||
|
|
1bfb96f34d | ||
|
|
29f1673d97 | ||
|
|
6946c51430 | ||
|
|
6b39effe7f | ||
|
|
c28e8a2397 | ||
|
|
ffa2827ab1 | ||
|
|
63619f8b8e | ||
|
|
52b24a9902 | ||
|
|
fb43a9d7b9 | ||
|
|
e2276b0626 | ||
|
|
09383860e9 | ||
|
|
093f8bb917 | ||
|
|
31038acccf | ||
|
|
060ce1d9a2 | ||
|
|
5f8c8caec6 | ||
|
|
d5197d6a50 | ||
|
|
650d1af978 | ||
|
|
091f6297c4 | ||
|
|
aea86cbf89 | ||
|
|
b0838db5a8 | ||
|
|
ce566f579e | ||
|
|
c85ee7c0fd | ||
|
|
4119c61df8 | ||
|
|
5b50182658 | ||
|
|
7ace0efd65 | ||
|
|
738247d47e | ||
|
|
c6b646e92c | ||
|
|
bea162c34c | ||
|
|
d3be8e022f | ||
|
|
f0677c5af1 | ||
|
|
db22666a30 | ||
|
|
4263805c78 | ||
|
|
a3def2bbd9 | ||
|
|
44d7a49a48 | ||
|
|
57e159b369 | ||
|
|
c6a567acab | ||
|
|
7a6f49cd42 | ||
|
|
ceca078acb | ||
|
|
021f5d689b | ||
|
|
b089f1a492 | ||
|
|
11eb014311 | ||
|
|
f970abf14a | ||
|
|
6ecefad692 | ||
|
|
1cbd53c01c | ||
|
|
d9871e92d2 | ||
|
|
76cba561cf | ||
|
|
871dee4674 | ||
|
|
86dd933596 | ||
|
|
444dfded84 | ||
|
|
2e32b44061 | ||
|
|
6344469a9b | ||
|
|
d64546c791 | ||
|
|
e12b6c0306 | ||
|
|
0ed816d4e9 | ||
|
|
fb1ed0e30d | ||
|
|
19d546938a | ||
|
|
60d3681371 | ||
|
|
393e1f70ad | ||
|
|
99256100a0 | ||
|
|
e5ad2223a1 | ||
|
|
c9b3ef58de | ||
|
|
2f2e417b68 | ||
|
|
b282e34dc2 | ||
|
|
5065d4ab2d | ||
|
|
b318167cd3 | ||
|
|
06474e5990 | ||
|
|
ee3fe07acf | ||
|
|
469863f7f0 | ||
|
|
d2924c7748 | ||
|
|
cb5a10c6a3 | ||
|
|
f1db22d48b | ||
|
|
4fcfc9410f | ||
|
|
9655dc9ca9 | ||
|
|
f8eb988347 | ||
|
|
60421b23f0 | ||
|
|
2929563294 | ||
|
|
d3b7965c76 | ||
|
|
d89774cb7f | ||
|
|
2e6a3c7b16 | ||
|
|
f224b9fe28 | ||
|
|
11b0a9cb76 | ||
|
|
a62adf62ca | ||
|
|
affe708961 | ||
|
|
a09eaf8756 | ||
|
|
8178fc9973 | ||
|
|
2697d6a4fb | ||
|
|
70008b093e | ||
|
|
0d095f72e4 | ||
|
|
a4ceb051a7 | ||
|
|
6e53c38c2f | ||
|
|
cbd94b18a5 | ||
|
|
6e11923551 | ||
|
|
ed8efb5aef | ||
|
|
60db731e36 | ||
|
|
f9dbaf6292 | ||
|
|
c77c4e3c60 | ||
|
|
8d3f9c0b75 | ||
|
|
1a64a817ac | ||
|
|
85d1d99459 | ||
|
|
6c45787a37 | ||
|
|
ff4189ab25 | ||
|
|
8dfac6a4cf | ||
|
|
f050b22d6c | ||
|
|
1fcbed4e90 | ||
|
|
776308c4eb | ||
|
|
93e0dddbf3 | ||
|
|
3c54b3cff4 | ||
|
|
a64613172d | ||
|
|
3674ad9f8e | ||
|
|
db584ff7f8 | ||
|
|
6219a9ad1f | ||
|
|
4c97ff3717 | ||
|
|
200cbcde62 | ||
|
|
a11c28cb4c | ||
|
|
79371c03a8 | ||
|
|
fe31f389b0 | ||
|
|
29eff0ecd7 | ||
|
|
082bb49c7c | ||
|
|
854fb38612 | ||
|
|
9e98bba206 | ||
|
|
6a756136ef | ||
|
|
c58b73ad9b | ||
|
|
7f1de86779 | ||
|
|
b861899919 | ||
|
|
a846a11e10 | ||
|
|
07f635c3bc | ||
|
|
82a5d39778 | ||
|
|
496a7398d1 | ||
|
|
037b2b0135 | ||
|
|
201c24c110 | ||
|
|
f4ad5b0579 | ||
|
|
f3f0f9750a | ||
|
|
46eb77f9bb | ||
|
|
eec951cad2 | ||
|
|
f8b2d3cacc | ||
|
|
66a353dcb5 | ||
|
|
fda771c83f | ||
|
|
b423bb9e2d | ||
|
|
333709657c | ||
|
|
f9410bcaa7 | ||
|
|
8301519fb0 | ||
|
|
db152ca540 | ||
|
|
67d1e572ca | ||
|
|
b1daede91b | ||
|
|
91ac2e0dd9 | ||
|
|
249eeccf32 | ||
|
|
9db53cf256 | ||
|
|
ff66269b62 | ||
|
|
3ddcd87098 | ||
|
|
ab85118233 | ||
|
|
41aba0c02c | ||
|
|
4af6b4eb0e | ||
|
|
556382ff5f | ||
|
|
64ed47502b | ||
|
|
08266b7256 | ||
|
|
1d7e01b754 | ||
|
|
da6154b29c | ||
|
|
60d2f1557d | ||
|
|
b7763c1cb6 | ||
|
|
3f6f50d0ba | ||
|
|
9f4c2f87d3 | ||
|
|
453d0be4d4 | ||
|
|
88ad8d3a89 | ||
|
|
4db68669b6 | ||
|
|
1d3f045e85 | ||
|
|
96a23591e7 | ||
|
|
dbe33236fb | ||
|
|
fffbc2d490 | ||
|
|
a5b5887efc | ||
|
|
84d4a2c8a0 | ||
|
|
f8c2704d2b | ||
|
|
14858f6224 | ||
|
|
04da4c8094 | ||
|
|
4a8ef198d7 | ||
|
|
56c5c7b1b6 | ||
|
|
e8ef501f02 | ||
|
|
56a5d5af8d | ||
|
|
98384b62fd | ||
|
|
f11d531d45 | ||
|
|
06c43286cd | ||
|
|
746017ea4f | ||
|
|
f7ff84f17f | ||
|
|
97ecd20fc9 | ||
|
|
8904bc4cde | ||
|
|
f53632a84b | ||
|
|
b5f3578fe4 | ||
|
|
83ab3966d1 | ||
|
|
a362261972 | ||
|
|
31ed352ab6 | ||
|
|
60c1ada4c1 | ||
|
|
578f8a6dec | ||
|
|
200d9b9ac2 | ||
|
|
a60767c43a | ||
|
|
12ba8b9799 | ||
|
|
b1b8c4ed32 | ||
|
|
b16d47030e | ||
|
|
9fe24991d5 | ||
|
|
7d6b2a4740 | ||
|
|
420f068376 | ||
|
|
d3e1f20dcc | ||
|
|
13d6a74eb2 | ||
|
|
fdc8a267de | ||
|
|
9a3d46f8e3 | ||
|
|
01ceace18e | ||
|
|
402ab598b6 | ||
|
|
7bb62b9f21 | ||
|
|
f905a1e779 | ||
|
|
db69ce89f0 | ||
|
|
a6cfaba018 | ||
|
|
d6d62e8d4b | ||
|
|
a8cd7df44a | ||
|
|
d5149e592e | ||
|
|
d9b32db8d7 | ||
|
|
f5471bcebf | ||
|
|
25fb6f9a46 | ||
|
|
779641e9d4 | ||
|
|
b7a375f5aa | ||
|
|
4e1f1a0b92 | ||
|
|
788acdf527 | ||
|
|
865a017936 | ||
|
|
97575e7185 | ||
|
|
70e67b7c6d | ||
|
|
5c6e3f457f | ||
|
|
bc03233364 | ||
|
|
d923ba2206 | ||
|
|
bea4f6ca14 | ||
|
|
f628953c33 | ||
|
|
f6d8525d1d | ||
|
|
98d9aadd37 | ||
|
|
bcd06304cc | ||
|
|
439e88b5a6 | ||
|
|
47b2e49068 | ||
|
|
2bd2959ac3 | ||
|
|
bb7e165ce8 | ||
|
|
1690a080b2 | ||
|
|
bc025d5305 | ||
|
|
267201ff44 | ||
|
|
109385e955 | ||
|
|
296acc4a0a | ||
|
|
164091c269 | ||
|
|
9b3bf0e46c | ||
|
|
bc8d34143d | ||
|
|
1937be2bb2 | ||
|
|
d7a4b12d2a | ||
|
|
83ca595472 | ||
|
|
747377142b | ||
|
|
dd37f0540f | ||
|
|
75e749039b | ||
|
|
43ff60cb23 | ||
|
|
40d19d2064 | ||
|
|
f177cec509 | ||
|
|
d614716ff2 | ||
|
|
ce6a2140a2 | ||
|
|
7f760f16b0 | ||
|
|
35bafd9861 | ||
|
|
7ad5a90127 | ||
|
|
2ed6d8cca6 | ||
|
|
03f852c799 | ||
|
|
0b0ece9649 | ||
|
|
0adb2ba6b8 | ||
|
|
06b81c063a | ||
|
|
4fbb57f789 | ||
|
|
aafe8d84cb | ||
|
|
88f1d32cc4 | ||
|
|
acd8836d27 | ||
|
|
73f331df84 | ||
|
|
76cbe8bf33 | ||
|
|
5ccf8e8078 | ||
|
|
ac793a3c5a | ||
|
|
d361a1ddf5 | ||
|
|
694f93de80 | ||
|
|
37e4e022fd | ||
|
|
6e3f4d1079 | ||
|
|
98b8f8649d | ||
|
|
2b4277ec18 | ||
|
|
ae4e654d2a | ||
|
|
b23883b3b6 | ||
|
|
1f030af06f | ||
|
|
bb92fd1db0 | ||
|
|
53d171cf5b | ||
|
|
906472b21d | ||
|
|
f22d6b87e1 | ||
|
|
748870b03f | ||
|
|
95b997fb35 | ||
|
|
f827ada811 | ||
|
|
c91372daa6 | ||
|
|
ee920d4f83 | ||
|
|
708acee502 | ||
|
|
9b51ed7af2 | ||
|
|
ae9b81c18f | ||
|
|
5c467083e5 | ||
|
|
f036ef8a3c | ||
|
|
75d63db60d | ||
|
|
eca4b928d2 | ||
|
|
b931ced0a3 | ||
|
|
2e427c66ac | ||
|
|
c81f1cda47 | ||
|
|
fa3ded4134 | ||
|
|
495a776671 | ||
|
|
44d8e35988 | ||
|
|
d09606c65b | ||
|
|
032f1226db | ||
|
|
452bd0b013 | ||
|
|
d3205a0070 | ||
|
|
97603d6e17 | ||
|
|
b5100a7aaf | ||
|
|
7214a8e265 | ||
|
|
cfbfc7cb67 | ||
|
|
4e0522a80c | ||
|
|
888eb2abb5 | ||
|
|
1d779389cb | ||
|
|
01bcc89803 | ||
|
|
1ab8232b55 | ||
|
|
e8d4be579d | ||
|
|
a12c7b86b0 | ||
|
|
f82ac793b4 | ||
|
|
10bb76fe6b | ||
|
|
4d322df95e | ||
|
|
0d98949199 | ||
|
|
e4b7e31902 | ||
|
|
57ded4fd80 | ||
|
|
ea8ecf35f4 | ||
|
|
655b449270 | ||
|
|
0fb4f59fa6 | ||
|
|
69d1b88b9e | ||
|
|
e8a5ee4f48 | ||
|
|
77fadab560 | ||
|
|
4f317c7e3d | ||
|
|
0b99557c29 | ||
|
|
9bf93e698e | ||
|
|
aae77e8ea3 | ||
|
|
c7af289f4b | ||
|
|
5a237af973 | ||
|
|
6491192aa3 | ||
|
|
a9069520ae | ||
|
|
04356ad141 | ||
|
|
6039b25bc9 | ||
|
|
3c7ffa2a2c | ||
|
|
ebad3a44ab | ||
|
|
7f32eb1e25 | ||
|
|
c133e72f62 | ||
|
|
ff331178aa | ||
|
|
7e76a7d21b | ||
|
|
1c77d323b0 | ||
|
|
5e55f0da4d | ||
|
|
19c537b687 | ||
|
|
20853972b2 | ||
|
|
056871242b | ||
|
|
0655151b8c | ||
|
|
6319d84f42 | ||
|
|
922c614bde | ||
|
|
16c66e097b | ||
|
|
3423c1da18 | ||
|
|
49375d6031 | ||
|
|
5068b6ae7d | ||
|
|
12825938d7 | ||
|
|
8f240276db | ||
|
|
bfa87b4f50 | ||
|
|
648626115a | ||
|
|
12e6ccc581 | ||
|
|
702d7ac424 | ||
|
|
852af28f91 | ||
|
|
6f30a78a6c | ||
|
|
7e2031b18f | ||
|
|
dc8dd8b486 | ||
|
|
c98df05ed0 | ||
|
|
feb8eeb830 | ||
|
|
8480008a9a | ||
|
|
80d80daf64 | ||
|
|
dad733086a | ||
|
|
e73b859b1d | ||
|
|
568132c0c9 | ||
|
|
13e5541e17 | ||
|
|
a09ef6002a | ||
|
|
feea33d5e0 | ||
|
|
d429b6296b | ||
|
|
c7228fefa7 | ||
|
|
702fbdf731 | ||
|
|
834a25f084 | ||
|
|
7abfab8e77 | ||
|
|
3edfe1d28f | ||
|
|
85eae57e4c | ||
|
|
67763fb375 | ||
|
|
6d58916cfd | ||
|
|
842dab07b4 | ||
|
|
a51aa4c586 | ||
|
|
9e974f6a1b | ||
|
|
cf5739946b | ||
|
|
1ed41f8b21 | ||
|
|
c523bc4f07 | ||
|
|
2fd48df9d9 | ||
|
|
f7afa80250 | ||
|
|
1e1c368ae8 | ||
|
|
007c4a1965 | ||
|
|
8a12fd9ec4 | ||
|
|
6c4fee85e3 | ||
|
|
e2849a36e5 | ||
|
|
3db6182181 | ||
|
|
940b5edcaa | ||
|
|
50d351944f | ||
|
|
d0982cafa7 | ||
|
|
5b1fd374be | ||
|
|
d4d7ced922 | ||
|
|
922032b9bb | ||
|
|
702ff48cda | ||
|
|
2ad45ca04f | ||
|
|
5bfb72d058 | ||
|
|
aba1fe01b3 | ||
|
|
defcd4edc5 | ||
|
|
c8234a5af6 | ||
|
|
79d18c69b4 | ||
|
|
77fc8c5914 | ||
|
|
28e5f20c8e | ||
|
|
9e6ad558c5 | ||
|
|
9bcaa65574 | ||
|
|
0d83c1b91e | ||
|
|
018a9a20be | ||
|
|
cb19a7da99 | ||
|
|
ef98d26c9e | ||
|
|
afc1532b24 | ||
|
|
233a9a9b6c | ||
|
|
49f4216280 | ||
|
|
7f6500ae07 | ||
|
|
f339e9c284 | ||
|
|
03a0def4c3 | ||
|
|
9ba335a7c4 | ||
|
|
b46cf2bb0e | ||
|
|
c0122eeef3 | ||
|
|
f1e3ac5dec | ||
|
|
16e72211c9 | ||
|
|
cf76711789 | ||
|
|
241039ebbe | ||
|
|
697427b584 | ||
|
|
3a6f9c9bcf | ||
|
|
7f7e4e9885 | ||
|
|
3699996985 | ||
|
|
00fbca99dc | ||
|
|
131dfd0c19 | ||
|
|
a8d343318b | ||
|
|
f0845c7fff | ||
|
|
14c6f12b0c | ||
|
|
a08f8a4b80 | ||
|
|
db0c25b3d8 | ||
|
|
eb2acd11c2 | ||
|
|
5519dbd445 | ||
|
|
7b621bb5e1 | ||
|
|
0467195f07 | ||
|
|
4433b4c006 | ||
|
|
03f50180f3 | ||
|
|
69b4f93830 | ||
|
|
17918f0b82 | ||
|
|
eaf000eff1 | ||
|
|
d2a811eef2 | ||
|
|
5bd72696ac | ||
|
|
98110c1697 | ||
|
|
f1273073fc | ||
|
|
cec1d97035 | ||
|
|
ae23e46313 | ||
|
|
e755540be9 | ||
|
|
daabdfe357 | ||
|
|
5c25d29272 | ||
|
|
b3447f4375 | ||
|
|
6816661b0f | ||
|
|
299312c805 | ||
|
|
95fe745a0c | ||
|
|
c546c309f1 | ||
|
|
a146a48ccc | ||
|
|
ebf58709d3 | ||
|
|
43e3f5724c | ||
|
|
a6f7f9b0b8 | ||
|
|
7efeb146c5 | ||
|
|
4d1eedfa28 | ||
|
|
dc13e10637 | ||
|
|
a218eaf1f0 | ||
|
|
430eb67489 | ||
|
|
44acf4b756 | ||
|
|
0301504184 | ||
|
|
26e7cd8c75 | ||
|
|
2fe0d479f1 | ||
|
|
2d7dea1a09 | ||
|
|
8a48baa056 | ||
|
|
78aabf66ce | ||
|
|
4e3ea49cff | ||
|
|
c4b92e17d0 | ||
|
|
74332e1a61 | ||
|
|
be0e88a606 | ||
|
|
5c4480ec6c | ||
|
|
8ab7dd9d08 | ||
|
|
d0b238d2db | ||
|
|
07e0d13d2d | ||
|
|
dd5661b297 | ||
|
|
9b603f5ffa | ||
|
|
8db9319a06 | ||
|
|
480a073f1f | ||
|
|
5b844d7011 | ||
|
|
8b0718ac92 | ||
|
|
458ada173e | ||
|
|
8647191bee | ||
|
|
03d76479a5 | ||
|
|
a6a6b87f1b | ||
|
|
ab86d263c9 | ||
|
|
603de2d5db | ||
|
|
f124ebab1d | ||
|
|
1c0a2a7db8 | ||
|
|
869161a261 | ||
|
|
87b3b84471 | ||
|
|
955eae3500 | ||
|
|
2da84ed331 | ||
|
|
68bcaff14d | ||
|
|
c1e353491a | ||
|
|
828e4a5ace | ||
|
|
9e77563c99 | ||
|
|
c34feca59c | ||
|
|
6de84c64c6 | ||
|
|
72fc001d0d | ||
|
|
ac66c133a5 | ||
|
|
6d1bcd4b8c | ||
|
|
0a223838bd | ||
|
|
f8776ad5cd | ||
|
|
a01557f4cb | ||
|
|
c11d84f314 | ||
|
|
7b81cf3762 | ||
|
|
9f4f8de9ad | ||
|
|
e18c7e160c | ||
|
|
d773e11c7a | ||
|
|
00d0274fd7 | ||
|
|
849b6ec28d | ||
|
|
d84b80b795 | ||
|
|
536559f62d | ||
|
|
1675243f29 | ||
|
|
e97c60cc10 | ||
|
|
468514f525 | ||
|
|
3a3553dc46 | ||
|
|
88d608b661 | ||
|
|
69b9d8c3c2 | ||
|
|
873868cc10 | ||
|
|
7362de9a18 | ||
|
|
03fd66e209 | ||
|
|
04c2c5e58d | ||
|
|
c5a63f4775 | ||
|
|
6ce0097846 | ||
|
|
9dd228747c | ||
|
|
f07482382b | ||
|
|
cd47528a75 | ||
|
|
cc66e25cd2 | ||
|
|
7ca5052942 | ||
|
|
793dd81ca2 | ||
|
|
ab4ddb1e0e | ||
|
|
e31c514b00 | ||
|
|
6bf31467c7 | ||
|
|
b303a02461 | ||
|
|
c7a71d35b0 | ||
|
|
58954bf46f | ||
|
|
67401f1908 | ||
|
|
c5cf9bd290 | ||
|
|
f01d5616b3 | ||
|
|
a4740ca836 | ||
|
|
2dd5b777f7 | ||
|
|
75a2b05aa3 | ||
|
|
fc7f81f6e7 | ||
|
|
9ecbf92569 | ||
|
|
81af1bafba | ||
|
|
a808df5019 | ||
|
|
8a1d640dc4 | ||
|
|
9f345da20f | ||
|
|
942c2cbd7b | ||
|
|
57c5405657 | ||
|
|
6db1c8bafb | ||
|
|
e2c7be3a2e | ||
|
|
43a69d20bf | ||
|
|
4936d6c342 | ||
|
|
cb6b1d8d41 | ||
|
|
be58993f47 | ||
|
|
b857cf9d9b | ||
|
|
456738ba64 | ||
|
|
2cada598c6 | ||
|
|
f3e07c231b | ||
|
|
4d5e1e5947 | ||
|
|
7f49514f6f | ||
|
|
fffb14bc87 | ||
|
|
050ab19264 | ||
|
|
94a35f25f3 | ||
|
|
63c4884b16 | ||
|
|
b9068c1304 | ||
|
|
02a1597642 | ||
|
|
230258702f | ||
|
|
e4b1e5c3c8 | ||
|
|
d49e91cd78 | ||
|
|
69facc0e2f | ||
|
|
15b8499f8d | ||
|
|
01dc9e8abb | ||
|
|
d712c87ec0 | ||
|
|
6448e0c25d | ||
|
|
8c64d06b3a | ||
|
|
da0d090af9 | ||
|
|
b3d1db249d | ||
|
|
9cd6b33e97 | ||
|
|
4b9b9547d4 | ||
|
|
fc3924ac70 | ||
|
|
b2b8a003c1 | ||
|
|
371ad2e397 | ||
|
|
61ff330e99 | ||
|
|
1d09291f95 | ||
|
|
70f6740309 | ||
|
|
9026b3e86e | ||
|
|
97f757c669 | ||
|
|
d8c275306e | ||
|
|
70c5cbd436 | ||
|
|
a44700ed1f | ||
|
|
8b4597aefa | ||
|
|
6d4925af17 | ||
|
|
0e67fa9688 | ||
|
|
ef6224e45d | ||
|
|
e7e378c211 | ||
|
|
5a1fc03545 | ||
|
|
e1530dc190 | ||
|
|
7c1008e5e5 | ||
|
|
2815e3bae9 | ||
|
|
f080ca43df | ||
|
|
5409dfbb0f | ||
|
|
a2112e1be0 | ||
|
|
3288d729ae | ||
|
|
58344980e4 | ||
|
|
f7c22f0159 | ||
|
|
b3f714612d | ||
|
|
52fc476a6f | ||
|
|
dbee816205 | ||
|
|
3a8746ca54 | ||
|
|
1129956f7f | ||
|
|
3bf883327e | ||
|
|
e0dfa7a069 | ||
|
|
0909f669a0 | ||
|
|
4a0939a4e4 | ||
|
|
f9a1769df1 | ||
|
|
1fbb8723dc | ||
|
|
2c7dad589d | ||
|
|
72e89b615b | ||
|
|
761c0eb1ed | ||
|
|
5047bdb4a2 | ||
|
|
f5bed84340 | ||
|
|
83efe1547d | ||
|
|
48919009fc | ||
|
|
f66fef0ec5 | ||
|
|
478fe0ecf2 | ||
|
|
7ecc0f4b11 | ||
|
|
021ef0d14b | ||
|
|
e3fc676260 | ||
|
|
82c3ccc8dd | ||
|
|
80cefade65 | ||
|
|
bd97cbc523 | ||
|
|
c5f23b26fc | ||
|
|
a7faefd4ce | ||
|
|
b3513d3167 | ||
|
|
4ce6586710 | ||
|
|
68e2dee2cd | ||
|
|
366fe0d394 | ||
|
|
9fcff1bd9a | ||
|
|
7ab9160ee1 | ||
|
|
a337b844ec | ||
|
|
e936fe9001 | ||
|
|
c46e91d229 | ||
|
|
37b79174d1 | ||
|
|
b095496d30 | ||
|
|
14ec3bfc1d | ||
|
|
427c7fbf93 | ||
|
|
a72c92e55f | ||
|
|
3a67b6ddc1 | ||
|
|
7f85e15990 | ||
|
|
b20cdff82d | ||
|
|
813a4f7456 | ||
|
|
470d950918 | ||
|
|
4049c72e6c | ||
|
|
30f138a538 | ||
|
|
41f686ab9d | ||
|
|
98d66a569b | ||
|
|
654173e7c7 | ||
|
|
543476cba1 | ||
|
|
9a8e020559 | ||
|
|
f0d140f164 | ||
|
|
ee6067e98c | ||
|
|
c2ce97785f | ||
|
|
f302c9fda2 | ||
|
|
3cb0071025 | ||
|
|
043b7a7c65 | ||
|
|
bfd2739fce | ||
|
|
0656838fe5 | ||
|
|
4fb5bb09b6 | ||
|
|
e074a54a20 | ||
|
|
492f93416d | ||
|
|
18b3afc97a | ||
|
|
0c898df430 | ||
|
|
c8f54aad8b | ||
|
|
8de0027df5 | ||
|
|
5b0f5f456e | ||
|
|
59022b6fe0 | ||
|
|
8b15523c9d | ||
|
|
e05cc109a6 | ||
|
|
a55ae8ad52 | ||
|
|
76da296799 | ||
|
|
3065506b38 | ||
|
|
d953ed442c | ||
|
|
1be354bdce | ||
|
|
adc8d344a5 | ||
|
|
e767c3ea4f | ||
|
|
e46c3415f7 | ||
|
|
4beaaa0650 | ||
|
|
9fa648e570 | ||
|
|
66e7013dfe | ||
|
|
07a51815e9 | ||
|
|
90a6f43c56 | ||
|
|
53cf1b4900 | ||
|
|
ed44f12f6d | ||
|
|
7b6837cbc2 | ||
|
|
b14332df96 | ||
|
|
da9b672d1b | ||
|
|
86a7c56275 | ||
|
|
13e7069eb9 | ||
|
|
54dd925283 | ||
|
|
80398a2a3a | ||
|
|
4f28876c8d | ||
|
|
e61c9af523 | ||
|
|
4d1ac7332e | ||
|
|
e2100d0626 | ||
|
|
47b6ce93f8 | ||
|
|
0d8cf87a8c | ||
|
|
f9c20aff9d | ||
|
|
83ce8c30fc | ||
|
|
19159ec479 | ||
|
|
9cb034d49e | ||
|
|
d1562e48e1 | ||
|
|
dc42d15352 | ||
|
|
4d3353750e | ||
|
|
297f64e286 | ||
|
|
8fb29e5230 | ||
|
|
2d13382c68 | ||
|
|
d96d0a87cf | ||
|
|
f7ff98c747 | ||
|
|
5b18cf09e7 | ||
|
|
6b5867ce9d | ||
|
|
43e655afc8 | ||
|
|
cc34ac0bd5 | ||
|
|
e88795be13 | ||
|
|
b18ec59c56 | ||
|
|
10853e4d2f | ||
|
|
a1783a14cc | ||
|
|
855058858a | ||
|
|
64509dd6ca | ||
|
|
924f797c58 | ||
|
|
e025fc00a4 | ||
|
|
5ee7fdc0cc | ||
|
|
3450eff288 | ||
|
|
3e29d0f75b | ||
|
|
809aa028ec | ||
|
|
89ccb6be05 | ||
|
|
5385bf9555 | ||
|
|
9ba52db585 | ||
|
|
dcaf1796fe | ||
|
|
f54f530ada | ||
|
|
d0cc9a5634 | ||
|
|
b9f385bd66 | ||
|
|
096ffa9744 | ||
|
|
3320e495f4 | ||
|
|
57005ed7f8 | ||
|
|
36f9633223 | ||
|
|
df49692dff | ||
|
|
05eda424fc | ||
|
|
4194ae33fd | ||
|
|
88d0df523e | ||
|
|
54bd1c406a | ||
|
|
48a24559a3 | ||
|
|
79fdea0de4 | ||
|
|
7d73bbb073 | ||
|
|
7d382ba5fe | ||
|
|
ce1f7ab662 | ||
|
|
ed848425c7 | ||
|
|
dffad65f2f | ||
|
|
7e29218327 | ||
|
|
8fa61c28e4 | ||
|
|
61550f1b0a | ||
|
|
0fbec28858 | ||
|
|
1bc041b46d | ||
|
|
603f8e4f86 | ||
|
|
01cbd5cb58 | ||
|
|
1c36706306 | ||
|
|
99e47fc5fc | ||
|
|
78474409a5 | ||
|
|
1c2affea8e | ||
|
|
0559aa9673 | ||
|
|
c4b7966dbe | ||
|
|
3aa021a812 | ||
|
|
e7b237c8da | ||
|
|
b0035747e3 | ||
|
|
9992fe648d | ||
|
|
a3c8235f86 | ||
|
|
fa8198f44b | ||
|
|
f04d8fcbcc | ||
|
|
d5b0538f7b | ||
|
|
01d3f69c52 | ||
|
|
a89a3c86d0 | ||
|
|
0d331c1e3a | ||
|
|
0f4c7dd8fd | ||
|
|
ce99bb927d | ||
|
|
34837afc7a | ||
|
|
14d4252904 | ||
|
|
6d12230021 | ||
|
|
09059bfdcc | ||
|
|
b6ce40e87f | ||
|
|
87fee21ef5 | ||
|
|
761ef1c73e | ||
|
|
52e55508da | ||
|
|
b9b3651a98 | ||
|
|
09bd3d015d | ||
|
|
0ba3f47e31 | ||
|
|
78678f4bcb | ||
|
|
185a916f5e | ||
|
|
a5be4a6d40 | ||
|
|
a57162e8bf | ||
|
|
cdae9fc680 | ||
|
|
40ba6d2a6f | ||
|
|
574cc41f0b | ||
|
|
dd84637c34 | ||
|
|
af905a3ff7 | ||
|
|
c63cebaee1 | ||
|
|
d3ae3a8e14 | ||
|
|
b38ff39e8b | ||
|
|
d981eb282f | ||
|
|
576573711a | ||
|
|
3d8df0f709 | ||
|
|
582d5d526c | ||
|
|
3713119ab9 | ||
|
|
2fa2235fe3 | ||
|
|
c527d85d97 | ||
|
|
16f6984d02 | ||
|
|
dda13def2a | ||
|
|
3018443cd7 | ||
|
|
2e74fb60c8 | ||
|
|
8dfe22ef1c | ||
|
|
339856eb78 | ||
|
|
5771225f54 | ||
|
|
079fdbad13 | ||
|
|
b617b13c43 | ||
|
|
d0dbf6d2ea | ||
|
|
b6de5f00b3 | ||
|
|
1f63094542 | ||
|
|
c601ef03b1 | ||
|
|
0647c75a06 | ||
|
|
101e784577 | ||
|
|
79b0a58132 | ||
|
|
356b8048c5 | ||
|
|
18cf59c375 | ||
|
|
fe8dc26849 | ||
|
|
cc01c352c3 | ||
|
|
a61453d3ec | ||
|
|
f6d75476b9 | ||
|
|
fa2a7b5f2f | ||
|
|
e416679cea | ||
|
|
3a25af223f | ||
|
|
26bb9094d4 | ||
|
|
0e3140c54c | ||
|
|
04380d6a36 | ||
|
|
d514b6f3f9 | ||
|
|
1ef81ac518 | ||
|
|
e20ef8a077 | ||
|
|
bb839ba268 | ||
|
|
96d2f11f81 | ||
|
|
b456173eb8 | ||
|
|
535b1610c8 | ||
|
|
4f47b8cff9 | ||
|
|
a6e948840d | ||
|
|
5ce6bbf076 | ||
|
|
caeffa3998 | ||
|
|
6df18a9181 | ||
|
|
46a89a7d61 | ||
|
|
59017da4f2 | ||
|
|
45a12505f2 | ||
|
|
8e67c4f597 | ||
|
|
c24c1ffd1a | ||
|
|
9d7868ee4e | ||
|
|
dfde029430 | ||
|
|
db1ac7b3eb | ||
|
|
4734efa9a1 | ||
|
|
57c85adc53 | ||
|
|
f8b03c45ef | ||
|
|
e639f1f889 | ||
|
|
f101ad6855 | ||
|
|
766512c475 | ||
|
|
3626ec5cf7 | ||
|
|
e8193ec201 | ||
|
|
596f58507e | ||
|
|
442e798985 | ||
|
|
1d86ac8761 | ||
|
|
c9e33d3709 | ||
|
|
adcfaa5735 | ||
|
|
6dd257a81f | ||
|
|
b03e7b26b5 | ||
|
|
3fb98a904b | ||
|
|
74b045e9b6 | ||
|
|
6e21aeea59 | ||
|
|
d1fd372eb2 | ||
|
|
f91baa4b4b | ||
|
|
0649d778a7 | ||
|
|
95e0d2f1b2 | ||
|
|
1b41b8682b | ||
|
|
274230dbcb | ||
|
|
6c5eddaf1d | ||
|
|
7a7f4cecbb | ||
|
|
b873b856f2 | ||
|
|
c67aee7012 | ||
|
|
112e2c6922 | ||
|
|
62f9ce180d | ||
|
|
eec72b5136 | ||
|
|
5364b3d8f5 | ||
|
|
b39c384d6d | ||
|
|
113392bce8 | ||
|
|
a192373c25 | ||
|
|
3523ad5239 | ||
|
|
18254d9aae | ||
|
|
f0cc264757 | ||
|
|
5d100994b1 | ||
|
|
a87fe8ffce | ||
|
|
7bc10d46e3 | ||
|
|
6876bfa685 | ||
|
|
7dcbbaa653 | ||
|
|
a2bb5a34fd | ||
|
|
ca2a6cc36b | ||
|
|
67cd8959bc | ||
|
|
c359a5117f | ||
|
|
cc4d475519 | ||
|
|
62074c7653 | ||
|
|
4c8367a8b3 | ||
|
|
f7f9129b05 | ||
|
|
a98f359122 | ||
|
|
a436b0345a | ||
|
|
61b1d73f5a | ||
|
|
7cf64ae07a | ||
|
|
40f7c5d286 | ||
|
|
166cc0d48a | ||
|
|
6c88abf62f | ||
|
|
963c36338a | ||
|
|
4e48f64512 | ||
|
|
4d56039ad4 | ||
|
|
9210bf4be1 | ||
|
|
3c9ef6245e | ||
|
|
688b99eb73 | ||
|
|
7ae7029154 | ||
|
|
e921cb1a9d | ||
|
|
df5e54e02a | ||
|
|
031d26527f | ||
|
|
521be17a9e | ||
|
|
f925374db6 | ||
|
|
90d410d627 | ||
|
|
b177afc326 | ||
|
|
aa44028b46 | ||
|
|
f240c57f16 | ||
|
|
82e6d67ccc | ||
|
|
4fda5f8bfd | ||
|
|
daac5de1ba | ||
|
|
a9a336fdff | ||
|
|
34963de458 | ||
|
|
662915e691 | ||
|
|
520875d455 | ||
|
|
d0e2475ece | ||
|
|
2c0a7fe75e | ||
|
|
de5e07ce3e | ||
|
|
6b0894d806 | ||
|
|
f825d23789 | ||
|
|
1817864a45 | ||
|
|
a4f64c0116 | ||
|
|
020dd480ed | ||
|
|
70d050416b | ||
|
|
dcb8215482 | ||
|
|
94757a209b | ||
|
|
805740069a | ||
|
|
faaf44f171 | ||
|
|
6983277042 | ||
|
|
2395ee9b2e | ||
|
|
3c48d871d1 | ||
|
|
5feae4f74c | ||
|
|
2dcdd121ec | ||
|
|
bbcde8a19d | ||
|
|
1e78b8bc28 | ||
|
|
2a777a970c | ||
|
|
91db227b27 | ||
|
|
1c6729d4c1 | ||
|
|
4b0904e9c4 | ||
|
|
7d1e9a6b8a | ||
|
|
320e946d50 | ||
|
|
c4897df09c | ||
|
|
8b1f83a0ed | ||
|
|
430f371a97 | ||
|
|
9b8f8f4bb9 | ||
|
|
d8bb3845ae | ||
|
|
9bf10975bd | ||
|
|
a6aa79aab2 | ||
|
|
2c595d2d16 | ||
|
|
a4d3f2da3d | ||
|
|
3476d31e50 | ||
|
|
8d10b91cf3 | ||
|
|
95855da282 | ||
|
|
3b2645979d | ||
|
|
418878edce | ||
|
|
30242409f4 | ||
|
|
fd2e6bcf5d | ||
|
|
26cdd11d8f | ||
|
|
51aac49e82 | ||
|
|
e09f45f5ed | ||
|
|
32ba7fc6c0 | ||
|
|
86cd40fba8 | ||
|
|
7272af8ec4 | ||
|
|
c86ae76e02 | ||
|
|
c89b4fd18b | ||
|
|
36a31771f1 | ||
|
|
8f88d382a5 | ||
|
|
d3d23e779a | ||
|
|
3afa3bea73 | ||
|
|
85bfab612c | ||
|
|
dc2c1aedcc | ||
|
|
c7cf4fa46c | ||
|
|
ebf8e9f663 | ||
|
|
4e1c30b60a | ||
|
|
99aee22e08 | ||
|
|
5646ca9458 | ||
|
|
96b73e3e94 | ||
|
|
3188382ea7 | ||
|
|
b8314fb054 | ||
|
|
66317b8438 | ||
|
|
dac3b592ed | ||
|
|
98d4adbb8a | ||
|
|
e9790c030e | ||
|
|
49b1ba5bbf | ||
|
|
8660ecc7ec | ||
|
|
50b909e537 | ||
|
|
5c760523be | ||
|
|
dc47db5512 | ||
|
|
b7a18580b8 | ||
|
|
c42d33e800 | ||
|
|
eaad4fa3c4 | ||
|
|
c65b9588e2 | ||
|
|
9cd221cc33 | ||
|
|
5c81fa1075 | ||
|
|
d47a6701c6 | ||
|
|
6f96862662 | ||
|
|
6e876e0582 | ||
|
|
3f253a587c | ||
|
|
382441f9ff | ||
|
|
71dfecd96d | ||
|
|
283138eb86 | ||
|
|
ea37a8ca64 | ||
|
|
86e80aa9a0 | ||
|
|
d09475d23b | ||
|
|
df17271f05 | ||
|
|
b9e8b5a816 | ||
|
|
1a194a578c | ||
|
|
d960e94290 | ||
|
|
08c41fc7e7 | ||
|
|
51432ac724 | ||
|
|
ba84350e58 | ||
|
|
bf32977dcb | ||
|
|
e736963f3c | ||
|
|
4d1ccf3e10 | ||
|
|
03d99503bb | ||
|
|
f0c2a2dcb3 | ||
|
|
41f29e9d8c | ||
|
|
c1bc529654 | ||
|
|
087b839354 | ||
|
|
67146ffa78 | ||
|
|
53b663af35 | ||
|
|
2a7972e8ea | ||
|
|
7cc1f473b6 | ||
|
|
49d639ecab | ||
|
|
c28d55d343 | ||
|
|
3a024d62df |
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
Sponsors SeaweedFS via Patreon https://www.patreon.com/seaweedfs
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**System Setup**
|
||||
List the command line to start "weed master", "weed volume", "weed filer", "weed s3", "weed mount".
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
+80
-1
@@ -1,3 +1,82 @@
|
||||
go/weed/.goxc*
|
||||
.goxc*
|
||||
vendor
|
||||
tags
|
||||
*.swp
|
||||
### OSX template
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
|
||||
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
# User-specific stuff:
|
||||
# .idea/workspace.xml
|
||||
# .idea/tasks.xml
|
||||
# .idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
# .idea/dataSources.ids
|
||||
# .idea/dataSources.xml
|
||||
# .idea/sqlDataSources.xml
|
||||
# .idea/dynamic.xml
|
||||
# .idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
# .idea/gradle.xml
|
||||
# .idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
# .idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
|
||||
test_data
|
||||
build
|
||||
target
|
||||
*.class
|
||||
other/java/hdfs/dependency-reduced-pom.xml
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>weed-fs</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>goclipse.goNature</nature>
|
||||
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
+38
-8
@@ -1,14 +1,44 @@
|
||||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.3
|
||||
- 1.4.1
|
||||
- tip
|
||||
go:
|
||||
- 1.10.x
|
||||
- 1.11.x
|
||||
- tip
|
||||
|
||||
before_install:
|
||||
- export PATH=/home/travis/gopath/bin:$PATH
|
||||
- export PATH=/home/travis/gopath/bin:$PATH
|
||||
|
||||
install:
|
||||
- go get ./go/...
|
||||
|
||||
- go get ./weed/...
|
||||
|
||||
script:
|
||||
- go test ./go/...
|
||||
- go test ./weed/...
|
||||
|
||||
before_deploy:
|
||||
- make release
|
||||
deploy:
|
||||
provider: releases
|
||||
skip_cleanup: true
|
||||
api_key:
|
||||
secure: ERL986+ncQ8lwAJUYDrQ8s2/FxF/cyNIwJIFCqspnWxQgGNNyokET9HapmlPSxjpFRF0q6L2WCg9OY3mSVRq4oI6hg1igOQ12KlLyN71XSJ3c8w0Ay5ho48TQ9l3f3Iu97mntBCe9l0R9pnT8wj1VI8YJxloXwUMG2yeTjA9aBI=
|
||||
file:
|
||||
- build/linux_arm.tar.gz
|
||||
- build/linux_arm64.tar.gz
|
||||
- build/linux_386.tar.gz
|
||||
- build/linux_amd64.tar.gz
|
||||
- build/darwin_amd64.tar.gz
|
||||
- build/windows_386.zip
|
||||
- build/windows_amd64.zip
|
||||
- build/freebsd_arm.tar.gz
|
||||
- build/freebsd_amd64.tar.gz
|
||||
- build/freebsd_386.tar.gz
|
||||
- build/netbsd_arm.tar.gz
|
||||
- build/netbsd_amd64.tar.gz
|
||||
- build/netbsd_386.tar.gz
|
||||
- build/openbsd_arm.tar.gz
|
||||
- build/openbsd_amd64.tar.gz
|
||||
- build/openbsd_386.tar.gz
|
||||
on:
|
||||
tags: true
|
||||
repo: chrislusf/seaweedfs
|
||||
go: tip
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
FROM progrium/busybox
|
||||
|
||||
WORKDIR /opt/weed
|
||||
|
||||
RUN opkg-install curl
|
||||
RUN echo insecure >> ~/.curlrc
|
||||
|
||||
RUN \
|
||||
curl -Lks https://bintray.com$(curl -Lk http://bintray.com/chrislusf/Weed-FS/seaweed/_latestVersion | grep linux_amd64.tar.gz | sed -n "/href/ s/.*href=['\"]\([^'\"]*\)['\"].*/\1/gp") | gunzip | tar -xf - -C /opt/weed/ && \
|
||||
mv weed_*/* /bin && \
|
||||
chmod +x ./bin/weed
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 9333
|
||||
|
||||
VOLUME /data
|
||||
|
||||
ENV WEED_HOME /opt/weed
|
||||
ENV PATH ${PATH}:${WEED_HOME}/bin
|
||||
|
||||
ENTRYPOINT ["weed"]
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
FROM cydev/go
|
||||
RUN go get github.com/chrislusf/weed-fs/go/weed
|
||||
RUN go get github.com/chrislusf/seaweedfs/weed
|
||||
EXPOSE 8080
|
||||
EXPOSE 9333
|
||||
VOLUME /data
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2016 Chris Lu
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,113 @@
|
||||
BINARY = weed/weed
|
||||
package = github.com/chrislusf/seaweedfs/weed
|
||||
|
||||
GO_FLAGS = #-v
|
||||
SOURCE_DIR = ./weed/
|
||||
|
||||
appname := weed
|
||||
|
||||
sources := $(wildcard *.go)
|
||||
|
||||
build = CGO_ENABLED=0 GOOS=$(1) GOARCH=$(2) go build -ldflags "-extldflags -static" -o build/$(appname)$(3) $(SOURCE_DIR)
|
||||
tar = cd build && tar -cvzf $(1)_$(2).tar.gz $(appname)$(3) && rm $(appname)$(3)
|
||||
zip = cd build && zip $(1)_$(2).zip $(appname)$(3) && rm $(appname)$(3)
|
||||
|
||||
|
||||
all: build
|
||||
|
||||
.PHONY : clean deps build linux release windows_build darwin_build linux_build bsd_build clean
|
||||
|
||||
clean:
|
||||
go clean -i $(GO_FLAGS) $(SOURCE_DIR)
|
||||
rm -f $(BINARY)
|
||||
rm -rf build/
|
||||
|
||||
deps:
|
||||
go get $(GO_FLAGS) -d $(SOURCE_DIR)
|
||||
|
||||
build: deps
|
||||
go build $(GO_FLAGS) -o $(BINARY) $(SOURCE_DIR)
|
||||
|
||||
linux: deps
|
||||
mkdir -p linux
|
||||
GOOS=linux GOARCH=amd64 go build $(GO_FLAGS) -o linux/$(BINARY) $(SOURCE_DIR)
|
||||
|
||||
release: deps windows_build darwin_build linux_build bsd_build
|
||||
|
||||
##### LINUX BUILDS #####
|
||||
linux_build: build/linux_arm.tar.gz build/linux_arm64.tar.gz build/linux_386.tar.gz build/linux_amd64.tar.gz
|
||||
|
||||
build/linux_386.tar.gz: $(sources)
|
||||
$(call build,linux,386,)
|
||||
$(call tar,linux,386)
|
||||
|
||||
build/linux_amd64.tar.gz: $(sources)
|
||||
$(call build,linux,amd64,)
|
||||
$(call tar,linux,amd64)
|
||||
|
||||
build/linux_arm.tar.gz: $(sources)
|
||||
$(call build,linux,arm,)
|
||||
$(call tar,linux,arm)
|
||||
|
||||
build/linux_arm64.tar.gz: $(sources)
|
||||
$(call build,linux,arm64,)
|
||||
$(call tar,linux,arm64)
|
||||
|
||||
##### DARWIN (MAC) BUILDS #####
|
||||
darwin_build: build/darwin_amd64.tar.gz
|
||||
|
||||
build/darwin_amd64.tar.gz: $(sources)
|
||||
$(call build,darwin,amd64,)
|
||||
$(call tar,darwin,amd64)
|
||||
|
||||
##### WINDOWS BUILDS #####
|
||||
windows_build: build/windows_386.zip build/windows_amd64.zip
|
||||
|
||||
build/windows_386.zip: $(sources)
|
||||
$(call build,windows,386,.exe)
|
||||
$(call zip,windows,386,.exe)
|
||||
|
||||
build/windows_amd64.zip: $(sources)
|
||||
$(call build,windows,amd64,.exe)
|
||||
$(call zip,windows,amd64,.exe)
|
||||
|
||||
##### BSD BUILDS #####
|
||||
bsd_build: build/freebsd_arm.tar.gz build/freebsd_386.tar.gz build/freebsd_amd64.tar.gz \
|
||||
build/netbsd_arm.tar.gz build/netbsd_386.tar.gz build/netbsd_amd64.tar.gz \
|
||||
build/openbsd_arm.tar.gz build/openbsd_386.tar.gz build/openbsd_amd64.tar.gz
|
||||
|
||||
build/freebsd_386.tar.gz: $(sources)
|
||||
$(call build,freebsd,386,)
|
||||
$(call tar,freebsd,386)
|
||||
|
||||
build/freebsd_amd64.tar.gz: $(sources)
|
||||
$(call build,freebsd,amd64,)
|
||||
$(call tar,freebsd,amd64)
|
||||
|
||||
build/freebsd_arm.tar.gz: $(sources)
|
||||
$(call build,freebsd,arm,)
|
||||
$(call tar,freebsd,arm)
|
||||
|
||||
build/netbsd_386.tar.gz: $(sources)
|
||||
$(call build,netbsd,386,)
|
||||
$(call tar,netbsd,386)
|
||||
|
||||
build/netbsd_amd64.tar.gz: $(sources)
|
||||
$(call build,netbsd,amd64,)
|
||||
$(call tar,netbsd,amd64)
|
||||
|
||||
build/netbsd_arm.tar.gz: $(sources)
|
||||
$(call build,netbsd,arm,)
|
||||
$(call tar,netbsd,arm)
|
||||
|
||||
build/openbsd_386.tar.gz: $(sources)
|
||||
$(call build,openbsd,386,)
|
||||
$(call tar,openbsd,386)
|
||||
|
||||
build/openbsd_amd64.tar.gz: $(sources)
|
||||
$(call build,openbsd,amd64,)
|
||||
$(call tar,openbsd,amd64)
|
||||
|
||||
build/openbsd_arm.tar.gz: $(sources)
|
||||
$(call build,openbsd,arm,)
|
||||
$(call tar,openbsd,arm)
|
||||
@@ -1,11 +1,56 @@
|
||||
Seaweed File System
|
||||
=======
|
||||
# SeaweedFS
|
||||
|
||||
[](https://travis-ci.org/chrislusf/weed-fs)
|
||||
[](https://godoc.org/github.com/chrislusf/weed-fs/go)
|
||||
[](https://github.com/chrislusf/weed-fs/wiki)
|
||||
[](https://travis-ci.org/chrislusf/seaweedfs)
|
||||
[](https://godoc.org/github.com/chrislusf/seaweedfs/weed)
|
||||
[](https://github.com/chrislusf/seaweedfs/wiki)
|
||||
|
||||

|
||||
|
||||
<h2 align="center">Supporting SeaweedFS</h2>
|
||||
|
||||
SeaweedFS is Apache-licensed open source project, independent project with its ongoing development made
|
||||
possible entirely thanks to the support by these awesome [backers](https://github.com/chrislusf/seaweedfs/blob/master/backers.md).
|
||||
If you'd like to grow SeaweedFS even stronger, please consider to
|
||||
<a href="https://www.patreon.com/seaweedfs">Sponsor SeaweedFS via Patreon</a>.
|
||||
|
||||
Platinum ($2500/month), Gold ($500/month): put your company logo on the SeaweedFS github page
|
||||
Generous Backer($50/month), Backer($10/month): put your name on the SeaweedFS backer page.
|
||||
|
||||
Your support will be really appreciated by me and other supporters!
|
||||
|
||||
<h3 align="center"><a href="https://www.patreon.com/seaweedfs">Sponsors SeaweedFS via Patreon</a></h3>
|
||||
|
||||
<h4 align="center">Platinum</h4>
|
||||
|
||||
<p align="center">
|
||||
<a href="" target="_blank">
|
||||
Add your name or icon here
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h4 align="center">Gold</h4>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="" target="_blank">
|
||||
Add your name or icon here
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
|
||||
- [Download Binaries for different platforms](https://github.com/chrislusf/seaweedfs/releases/latest)
|
||||
- [SeaweedFS on Slack](https://join.slack.com/t/seaweedfs/shared_invite/enQtMzI4MTMwMjU2MzA3LTc4MmVlYmFlNjBmZTgzZmJlYmI1MDE1YzkyNWYyZjkwZDFiM2RlMDdjNjVlNjdjYzc4NGFhZGIyYzEyMzJkYTA)
|
||||
- [SeaweedFS Mailing List](https://groups.google.com/d/forum/seaweedfs)
|
||||
- [Wiki Documentation](https://github.com/chrislusf/seaweedfs/wiki)
|
||||
|
||||

|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -14,45 +59,47 @@ SeaweedFS is a simple and highly scalable distributed file system. There are two
|
||||
1. to store billions of files!
|
||||
2. to serve the files fast!
|
||||
|
||||
Instead of supporting full POSIX file system semantics, SeaweedFS choose to implement only a key~file mapping. Similar to the word "NoSQL", you can call it as "NoFS".
|
||||
SeaweedFS started as an Object Store to handle small files efficiently. Instead of managing all file metadata in a central master, the central master only manages file volumes, and it lets these volume servers manage files and their metadata. This relieves concurrency pressure from the central master and spreads file metadata into volume servers, allowing faster file access (just one disk read operation).
|
||||
|
||||
Instead of managing all file metadata in a central master, SeaweedFS choose to manages file volumes in the central master, and let volume servers manage files and the metadata. This relieves concurrency pressure from the central master and spreads file metadata into volume servers' memories, allowing faster file access with just one disk read operation!
|
||||
There is only a 40 bytes disk storage overhead for each file's metadata. It is so simple with O(1) disk read that you are welcome to challenge the performance with your actual use cases.
|
||||
|
||||
SeaweedFS models after [Facebook's Haystack design paper](http://www.usenix.org/event/osdi10/tech/full_papers/Beaver.pdf).
|
||||
SeaweedFS started by implementing [Facebook's Haystack design paper](http://www.usenix.org/event/osdi10/tech/full_papers/Beaver.pdf).
|
||||
|
||||
SeaweedFS costs only 40 bytes disk storage for each file's metadata. It is so simple with O(1) disk read that you are welcome to challenge the performance with your actual use cases.
|
||||
|
||||
|
||||

|
||||
https://bintray.com/chrislusf/Weed-FS/seaweed Download latest compiled binaries for different platforms here.
|
||||
|
||||
http://groups.google.com/group/weed-file-system Seaweed File System Discussion Group
|
||||
SeaweedFS can work very well with just the object store. [[Filer]] is added later to support directories and POSIX attributes. Filer is a separate linearly-scalable stateless server with customizable metadata stores, e.g., MySql/Postgres/Redis/Cassandra/LevelDB.
|
||||
|
||||
## Additional Features
|
||||
* Can choose no replication or different replication level, rack and data center aware
|
||||
* Automatic master servers failover. No single point of failure, SPOF.
|
||||
* Automatic master servers failover - no single point of failure (SPOF)
|
||||
* Automatic Gzip compression depending on file mime type
|
||||
* Automatic compaction to reclaimed disk spaces after deletion or update
|
||||
* Servers in the same cluster can have different disk spaces, different file systems, different OS
|
||||
* Adding/Removing servers do not cause any data re-balancing
|
||||
* Optional [filer server][Filer] provides "normal" directories and files via http
|
||||
* For jpeg pictures, optionally fix the orientation.
|
||||
* Servers in the same cluster can have different disk spaces, file systems, OS etc.
|
||||
* Adding/Removing servers does **not** cause any data re-balancing
|
||||
* Optionally fix the orientation for jpeg pictures
|
||||
* Support Etag, Accept-Range, Last-Modified, etc.
|
||||
* Support in-memory/leveldb/boltdb mode tuning for memory/performance balance.
|
||||
* Support in-memory/leveldb/boltdb/btree mode tuning for memory/performance balance.
|
||||
## Filer Features
|
||||
* [filer server][Filer] provide "normal" directories and files via http.
|
||||
* [mount filer][Mount] to read and write files directly as a local directory via FUSE.
|
||||
* [Amazon S3 compatible API][AmazonS3API] to access files with S3 tooling.
|
||||
* [Hadoop Compatible File System][Hadoop] to access files from Hadoop/Spark/Flink/etc jobs.
|
||||
* [Async Backup To Cloud][BackupToCloud] can enjoy extreme fast local access and backup to Amazon S3, Google Cloud Storage, Azure, BackBlaze.
|
||||
|
||||
[Filer]: https://github.com/chrislusf/weed-fs/wiki/Filer
|
||||
[Filer]: https://github.com/chrislusf/seaweedfs/wiki/Directories-and-Files
|
||||
[Mount]: https://github.com/chrislusf/seaweedfs/wiki/Mount
|
||||
[AmazonS3API]: https://github.com/chrislusf/seaweedfs/wiki/Amazon-S3-API
|
||||
[BackupToCloud]: https://github.com/chrislusf/seaweedfs/wiki/Backup-to-Cloud
|
||||
[Hadoop]: https://github.com/chrislusf/seaweedfs/wiki/Hadoop-Compatible-File-System
|
||||
|
||||
## Example Usage
|
||||
By default, the master node runs on port 9333, and the volume nodes runs on port 8080.
|
||||
Here I will start one master node, and two volume nodes on port 8080 and 8081. Ideally, they should be started from different machines. Here I just use localhost as example.
|
||||
By default, the master node runs on port 9333, and the volume nodes run on port 8080.
|
||||
Here I will start one master node, and two volume nodes on port 8080 and 8081. Ideally, they should be started from different machines. I just use localhost as an example.
|
||||
|
||||
SeaweedFS uses HTTP REST operations to write, read, delete. The return results are JSON or JSONP format.
|
||||
SeaweedFS uses HTTP REST operations to write, read, delete. The responses are in JSON or JSONP format.
|
||||
|
||||
### Start Master Server
|
||||
|
||||
```
|
||||
> ./weed master
|
||||
|
||||
```
|
||||
|
||||
### Start Volume Servers ###
|
||||
@@ -60,26 +107,26 @@ SeaweedFS uses HTTP REST operations to write, read, delete. The return results a
|
||||
```
|
||||
> weed volume -dir="/tmp/data1" -max=5 -mserver="localhost:9333" -port=8080 &
|
||||
> weed volume -dir="/tmp/data2" -max=10 -mserver="localhost:9333" -port=8081 &
|
||||
|
||||
```
|
||||
|
||||
|
||||
### Write File ###
|
||||
|
||||
To upload a file, first, send a HTTP POST, PUT, or GET request to `/dir/assign` to get an fid and a volume server url:
|
||||
To upload a file: first, send a HTTP POST, PUT, or GET request to `/dir/assign` to get an `fid` and a volume server url:
|
||||
|
||||
```
|
||||
> curl -X POST http://localhost:9333/dir/assign
|
||||
> curl http://localhost:9333/dir/assign
|
||||
{"count":1,"fid":"3,01637037d6","url":"127.0.0.1:8080","publicUrl":"localhost:8080"}
|
||||
```
|
||||
|
||||
Second, to store the file content, send a HTTP multipart PUT or POST request to `url + '/' + fid` from the response:
|
||||
Second, to store the file content, send a HTTP multi-part POST request to `url + '/' + fid` from the response:
|
||||
|
||||
```
|
||||
> curl -X PUT -F file=@/home/chris/myphoto.jpg http://127.0.0.1:8080/3,01637037d6
|
||||
> curl -F file=@/home/chris/myphoto.jpg http://127.0.0.1:8080/3,01637037d6
|
||||
{"size": 43234}
|
||||
```
|
||||
|
||||
For update, send another PUT or POST request with updated file content.
|
||||
To update, send another POST request with updated file content.
|
||||
|
||||
For deletion, send an HTTP DELETE request to the same `url + '/' + fid` URL:
|
||||
|
||||
@@ -87,30 +134,31 @@ For deletion, send an HTTP DELETE request to the same `url + '/' + fid` URL:
|
||||
> curl -X DELETE http://127.0.0.1:8080/3,01637037d6
|
||||
```
|
||||
### Save File Id ###
|
||||
Now you can save the fid, 3,01637037d6 in this case, to some database field.
|
||||
|
||||
The number 3 here, is a volume id. After the comma, it's one file key, 01, and a file cookie, 637037d6.
|
||||
Now you can save the `fid`, 3,01637037d6 in this case, to a database field.
|
||||
|
||||
The volume id is an unsigned 32 bit integer. The file key is an unsigned 64bit integer. The file cookie is an unsigned 32bit integer, used to prevent URL guessing.
|
||||
The number 3 at the start represents a volume id. After the comma, it's one file key, 01, and a file cookie, 637037d6.
|
||||
|
||||
The file key and file cookie are both coded in hex. You can store the <volume id, file key, file cookie> tuple in your own format, or simply store the fid as string.
|
||||
The volume id is an unsigned 32-bit integer. The file key is an unsigned 64-bit integer. The file cookie is an unsigned 32-bit integer, used to prevent URL guessing.
|
||||
|
||||
The file key and file cookie are both coded in hex. You can store the <volume id, file key, file cookie> tuple in your own format, or simply store the `fid` as string.
|
||||
|
||||
If stored as a string, in theory, you would need 8+1+16+8=33 bytes. A char(33) would be enough, if not more than enough, since most usage would not need 2^32 volumes.
|
||||
|
||||
If space is really a concern, you can store the file id in your own format. You would need one 4-byte integer for volume id, 8-byte long number for file key, 4-byte integer for file cookie. So 16 bytes are enough (more than enough).
|
||||
If space is really a concern, you can store the file id in your own format. You would need one 4-byte integer for volume id, 8-byte long number for file key, 4-byte integer for file cookie. So 16 bytes are more than enough.
|
||||
|
||||
### Read File ###
|
||||
|
||||
Here is the example on how to render the URL.
|
||||
Here is an example of how to render the URL.
|
||||
|
||||
First lookup the volume server's URLs by the file's volumeId:
|
||||
First look up the volume server's URLs by the file's volumeId:
|
||||
|
||||
```
|
||||
> curl http://localhost:9333/dir/lookup?volumeId=3
|
||||
{"locations":[{"publicUrl":"localhost:8080","url":"localhost:8080"}]}
|
||||
```
|
||||
|
||||
(However, since usually there are not too many volume servers, and volumes does not move often, you can cache the results most of the time. Depends on the replication type, one volume can have multiple replica locations. Just randomly pick one location to read.)
|
||||
Since (usually) there are not too many volume servers, and volumes don't move often, you can cache the results most of the time. Depending on the replication type, one volume can have multiple replica locations. Just randomly pick one location to read.
|
||||
|
||||
Now you can take the public url, render the url or directly read from the volume server via url:
|
||||
|
||||
@@ -118,7 +166,7 @@ Now you can take the public url, render the url or directly read from the volume
|
||||
http://localhost:8080/3,01637037d6.jpg
|
||||
```
|
||||
|
||||
Notice we add an file extension ".jpg" here. It's optional and just one way for the client to specify the file content type.
|
||||
Notice we add a file extension ".jpg" here. It's optional and just one way for the client to specify the file content type.
|
||||
|
||||
If you want a nicer URL, you can use one of these alternative URL formats:
|
||||
|
||||
@@ -130,14 +178,23 @@ If you want a nicer URL, you can use one of these alternative URL formats:
|
||||
http://localhost:8080/3,01637037d6
|
||||
```
|
||||
|
||||
If you want to get a scaled version of an image, you can add some params:
|
||||
|
||||
```
|
||||
http://localhost:8080/3/01637037d6.jpg?height=200&width=200
|
||||
http://localhost:8080/3/01637037d6.jpg?height=200&width=200&mode=fit
|
||||
http://localhost:8080/3/01637037d6.jpg?height=200&width=200&mode=fill
|
||||
```
|
||||
|
||||
### Rack-Aware and Data Center-Aware Replication ###
|
||||
SeaweedFS apply the replication strategy on a volume level. So when you are getting a file id, you can specify the replication strategy. For example:
|
||||
|
||||
SeaweedFS applies the replication strategy at a volume level. So, when you are getting a file id, you can specify the replication strategy. For example:
|
||||
|
||||
```
|
||||
curl -X POST http://localhost:9333/dir/assign?replication=001
|
||||
curl http://localhost:9333/dir/assign?replication=001
|
||||
```
|
||||
|
||||
Here is the meaning of the replication parameter:
|
||||
The replication parameter options are:
|
||||
|
||||
```
|
||||
000: no replication
|
||||
@@ -150,7 +207,7 @@ Here is the meaning of the replication parameter:
|
||||
|
||||
More details about replication can be found [on the wiki][Replication].
|
||||
|
||||
[Replication]: https://github.com/chrislusf/weed-fs/wiki/Replication
|
||||
[Replication]: https://github.com/chrislusf/seaweedfs/wiki/Replication
|
||||
|
||||
You can also set the default replication strategy when starting the master server.
|
||||
|
||||
@@ -163,9 +220,7 @@ Volume servers can start with a specific data center name:
|
||||
weed volume -dir=/tmp/2 -port=8081 -dataCenter=dc2
|
||||
```
|
||||
|
||||
Or the master server can determine the data center via volume server's IP address and settings in weed.conf file.
|
||||
|
||||
Now when requesting a file key, an optional "dataCenter" parameter can limit the assigned volume to the specific data center. For example, this specifies that the assigned volume should be limited to 'dc1':
|
||||
When requesting a file key, an optional "dataCenter" parameter can limit the assigned volume to the specific data center. For example, this specifies that the assigned volume should be limited to 'dc1':
|
||||
|
||||
```
|
||||
http://localhost:9333/dir/assign?dataCenter=dc1
|
||||
@@ -177,92 +232,116 @@ Now when requesting a file key, an optional "dataCenter" parameter can limit the
|
||||
* [Chunking large files][feat-3]
|
||||
* [Collection as a Simple Name Space][feat-4]
|
||||
|
||||
[feat-1]: https://github.com/chrislusf/weed-fs/wiki/Failover-Master-Server
|
||||
[feat-2]: https://github.com/chrislusf/weed-fs/wiki/Optimization#insert-with-your-own-keys
|
||||
[feat-3]: https://github.com/chrislusf/weed-fs/wiki/Optimization#upload-large-files
|
||||
[feat-4]: https://github.com/chrislusf/weed-fs/wiki/Optimization#collection-as-a-simple-name-space
|
||||
[feat-1]: https://github.com/chrislusf/seaweedfs/wiki/Failover-Master-Server
|
||||
[feat-2]: https://github.com/chrislusf/seaweedfs/wiki/Optimization#insert-with-your-own-keys
|
||||
[feat-3]: https://github.com/chrislusf/seaweedfs/wiki/Optimization#upload-large-files
|
||||
[feat-4]: https://github.com/chrislusf/seaweedfs/wiki/Optimization#collection-as-a-simple-name-space
|
||||
|
||||
## Architecture ##
|
||||
Usually distributed file system split each file into chunks, and a central master keeps a mapping of a filename and a chunk index to chunk handles, and also which chunks each chunk server has.
|
||||
|
||||
This has the draw back that the central master can not handle many small files efficiently, and since all read requests need to go through the chunk master, responses would be slow for many concurrent web users.
|
||||
Usually distributed file systems split each file into chunks, a central master keeps a mapping of filenames, chunk indices to chunk handles, and also which chunks each chunk server has.
|
||||
|
||||
Instead of managing chunks, SeaweedFS choose to manage data volumes in the master server. Each data volume is size 32GB, and can hold a lot of files. And each storage node can has many data volumes. So the master node only needs to store the metadata about the volumes, which is fairly small amount of data and pretty static most of the time.
|
||||
The main drawback is that the central master can't handle many small files efficiently, and since all read requests need to go through the chunk master, might not scale well for many concurrent users.
|
||||
|
||||
The actual file metadata is stored in each volume on volume servers. Since each volume server only manage metadata of files on its own disk, and only 16 bytes for each file, all file access can read file metadata just from memory and only needs one disk operation to actually read file data.
|
||||
Instead of managing chunks, SeaweedFS manages data volumes in the master server. Each data volume is size 32GB, and can hold a lot of files. And each storage node can have many data volumes. So the master node only needs to store the metadata about the volumes, which is fairly small amount of data and is generally stable.
|
||||
|
||||
The actual file metadata is stored in each volume on volume servers. Since each volume server only manages metadata of files on its own disk, with only 16 bytes for each file, all file access can read file metadata just from memory and only needs one disk operation to actually read file data.
|
||||
|
||||
For comparison, consider that an xfs inode structure in Linux is 536 bytes.
|
||||
|
||||
### Master Server and Volume Server ###
|
||||
|
||||
The architecture is fairly simple. The actual data is stored in volumes on storage nodes. One volume server can have multiple volumes, and can both support read and write access with basic authentication.
|
||||
|
||||
All volumes are managed by a master server. The master server contains volume id to volume server mapping. This is fairly static information, and could be cached easily.
|
||||
|
||||
On each write request, the master server also generates a file key, which is a growing 64bit unsigned integer. Since the write requests are not as busy as read requests, one master server should be able to handle the concurrency well.
|
||||
|
||||
On each write request, the master server also generates a file key, which is a growing 64-bit unsigned integer. Since write requests are not generally as frequent as read requests, one master server should be able to handle the concurrency well.
|
||||
|
||||
### Write and Read files ###
|
||||
|
||||
When a client sends a write request, the master server returns <volume id, file key, file cookie, volume node url> for the file. The client then contact the volume node and POST the file content via REST.
|
||||
When a client sends a write request, the master server returns (volume id, file key, file cookie, volume node url) for the file. The client then contacts the volume node and POSTs the file content.
|
||||
|
||||
When a client needs to read a file based on <volume id, file key, file cookie>, it can ask the master server by the <volum id> for the <volume node url, volume node public url>, or from cache. Then the client can HTTP GET the content via REST, or just render the URL on web pages and let browsers to fetch the content.
|
||||
When a client needs to read a file based on (volume id, file key, file cookie), it can ask the master server by the volume id for the (volume node url, volume node public url), or retrieve this from a cache. Then the client can GET the content, or just render the URL on web pages and let browsers fetch the content.
|
||||
|
||||
Please see the example for details on write-read process.
|
||||
Please see the example for details on the write-read process.
|
||||
|
||||
### Storage Size ###
|
||||
In current implementation, each volume can be size of 8x2^32 bytes (32GiB). This is because of aligning contents to 8 bytes. We can be easily increased to 64G, or 128G, or more, by changing 2 lines of code, at the cost of some wasted padding space due to alignment.
|
||||
|
||||
In the current implementation, each volume can be 8x2^32 bytes (32GiB). This is because of we align content to 8 bytes. We can easily increase this to 64G, or 128G, or more, by changing 2 lines of code, at the cost of some wasted padding space due to alignment.
|
||||
|
||||
There can be 2^32 volumes. So total system size is 8 x 2^32 bytes x 2^32 = 8 x 4GiB x 4Gi = 128EiB (2^67 bytes, or 128 exbibytes).
|
||||
|
||||
Each individual file size is limited to the volume size.
|
||||
|
||||
### Saving memory ###
|
||||
|
||||
All file meta information on volume server is readable from memory without disk access. Each file just takes an 16-byte map entry of <64bit key, 32bit offset, 32bit size>. Of course, each map entry has its own the space cost for the map. But usually the disk runs out before the memory does.
|
||||
|
||||
|
||||
## Compared to Other File Systems##
|
||||
Frankly, I don't use other distributed file systems too often. All seems more complicated than necessary. Please correct me if anything here is wrong.
|
||||
## Compared to Other File Systems ##
|
||||
|
||||
### Compared to Ceph ###
|
||||
Ceph can be setup similar to SeaweedFS as a key~blob store. It is much more complicated, with the need to support layers on top of it. Here is a more detailed comparison. https://code.google.com/p/weed-fs/issues/detail?id=44
|
||||
Most other distributed file systems seem more complicated than necessary.
|
||||
|
||||
SeaweedFS is meant to be fast and simple, both during usage and during setup. If you do not understand how it works when you reach here, we failed! Jokes aside, you should not need any consulting service for it.
|
||||
|
||||
SeaweedFS has a centralized master to lookup free volumes, while Ceph uses hashing to locate its objects. Having a centralized master makes it easy to code and manage. HDFS/GFS has the single name node for years. SeaweedFS now support multiple master nodes.
|
||||
|
||||
Ceph hashing avoids SPOF, but makes it complicated when moving or adding servers.
|
||||
SeaweedFS is meant to be fast and simple, both during usage and during setup. If you do not understand how it works when you reach here, we failed! Please raise an issue with any questions or update this file with clarifications.
|
||||
|
||||
### Compared to HDFS ###
|
||||
HDFS uses the chunk approach for each file, and is ideal for streaming large files.
|
||||
|
||||
HDFS uses the chunk approach for each file, and is ideal for storing large files.
|
||||
|
||||
SeaweedFS is ideal for serving relatively smaller files quickly and concurrently.
|
||||
|
||||
SeaweedFS can also store extra large files by splitting them into manageable data chunks, and store the file ids of the data chunks into a meta chunk. This is managed by "weed upload/download" tool, and the weed master or volume servers are agnostic about it.
|
||||
|
||||
### Compared to MogileFS###
|
||||
SeaweedFS has 2 components: directory server, storage nodes.
|
||||
|
||||
MogileFS has 3 components: tracers, database, storage nodes.
|
||||
### Compared to GlusterFS, Ceph ###
|
||||
|
||||
One more layer means slower access, more operation complexity, more failure possibility.
|
||||
The architectures are mostly the same. SeaweedFS aims to store and read files fast, with a simple and flat architecture. The main differences are
|
||||
|
||||
* SeaweedFS optimizes for small files, ensuring O(1) disk seek operation, and can also handle large files.
|
||||
* SeaweedFS statically assigns a volume id for a file. Locating file content becomes just a lookup of the volume id, which can be easily cached.
|
||||
* SeaweedFS Filer metadata store can be any well-known and proven data stores, e.g., Cassandra, Redis, MySql, Postgres, etc, and is easy to customized.
|
||||
* SeaweedFS Volume server also communicates directly with clients via HTTP, supporting range queries, direct uploads, etc.
|
||||
|
||||
| System | File Meta | File Content Read| POSIX | REST API | Optimized for small files |
|
||||
| ------------- | ------------------------------- | ---------------- | ------ | -------- | ------------------------- |
|
||||
| SeaweedFS | lookup volume id, cacheable | O(1) disk seek | | Yes | Yes |
|
||||
| SeaweedFS Filer| Linearly Scalable, Customizable | O(1) disk seek | FUSE | Yes | Yes |
|
||||
| GlusterFS | hashing | | FUSE, NFS | | |
|
||||
| Ceph | hashing + rules | | FUSE | Yes | |
|
||||
|
||||
### Compared to GlusterFS ###
|
||||
SeaweedFS is not POSIX compliant, and has simple implementation.
|
||||
|
||||
GlusterFS is POSIX compliant, much more complex.
|
||||
GlusterFS stores files, both directories and content, in configurable volumes called "bricks".
|
||||
|
||||
### Compared to Mongo's GridFS ###
|
||||
Mongo's GridFS splits files into chunks and manage chunks in the central mongodb. For every read or write request, the database needs to query the metadata. It's OK if this is not a bottleneck yet, but for a lot of concurrent reads this unnecessary query could slow things down.
|
||||
GlusterFS hashes the path and filename into ids, and assigned to virtual volumes, and then mapped to "bricks".
|
||||
|
||||
Since files are chunked(default to 256KB), there will be multiple metadata readings and multiple chunk readings, linear to the file size. One 2.56MB file would require at least 20 disk read requests.
|
||||
### Compared to Ceph ###
|
||||
|
||||
Ceph can be setup similar to SeaweedFS as a key->blob store. It is much more complicated, with the need to support layers on top of it. [Here is a more detailed comparison](https://github.com/chrislusf/seaweedfs/issues/120)
|
||||
|
||||
SeaweedFS has a centralized master group to look up free volumes, while Ceph uses hashing and metadata servers to locate its objects. Having a centralized master makes it easy to code and manage.
|
||||
|
||||
Same as SeaweedFS, Ceph is also based on a object store RADOS. Ceph is rather complicated with mixed reviews.
|
||||
|
||||
Ceph uses CRUSH hashing to automatically manage the data placement. SeaweedFS places data by assigned volumes.
|
||||
|
||||
SeaweedFS is optimized for small files. Small files are stored as one continuous block of content, with at most 8 unused bytes between files. Small file access is O(1) disk read.
|
||||
|
||||
SeaweedFS Filer uses off-the-shelf stores, such as MySql, Postgres, Redis, Cassandra, to manage file directories. There are proven, scalable, and easier to manage.
|
||||
|
||||
| SeaweedFS | comparable to Ceph | advantage |
|
||||
| ------------- | ------------- | ---------------- |
|
||||
| Master | MDS | simpler |
|
||||
| Volume | OSD | optimized for small files |
|
||||
| Filer | Ceph FS | linearly scalable, Customizable, O(1) or O(logN) |
|
||||
|
||||
On the contrary, SeaweedFS uses large file volume of 32G size to store lots of files, and only manages file volumes in the master server. Each volume manages file metadata themselves. So all the file metadata is spread onto the volume nodes memories, and just one disk read is needed.
|
||||
|
||||
## Dev plan ##
|
||||
|
||||
More tools and documentation, on how to maintain and scale the system. For example, how to move volumes, automatically balancing data, how to grow volumes, how to check system status, etc.
|
||||
Other key features include: Erasure Encoding, JWT security.
|
||||
|
||||
This is a super exciting project! And I need helpers!
|
||||
This is a super exciting project! And I need helpers and [support](https://www.patreon.com/seaweedfs)!
|
||||
|
||||
|
||||
## Installation guide for users who are not familiar with golang
|
||||
@@ -278,88 +357,81 @@ step 2: also you may need to install Mercurial by following the instructions bel
|
||||
|
||||
http://mercurial.selenic.com/downloads
|
||||
|
||||
|
||||
step 3: download, compile, and install the project by executing the following command
|
||||
|
||||
go get github.com/chrislusf/weed-fs/go/weed
|
||||
```bash
|
||||
go get github.com/chrislusf/seaweedfs/weed
|
||||
```
|
||||
|
||||
once this is done, you should see the executable "weed" under $GOPATH/bin
|
||||
once this is done, you should see the executable "weed" under `$GOPATH/bin`
|
||||
|
||||
step 4: after you modify your code locally, you could start a local build by calling "go install" under $GOPATH/src/github.com/chrislusf/weed-fs/go/weed
|
||||
step 4: after you modify your code locally, you could start a local build by calling `go install` under
|
||||
|
||||
## Reference
|
||||
|
||||
For pre-compiled releases,
|
||||
https://bintray.com/chrislusf/Weed-FS/seaweed
|
||||
```
|
||||
$GOPATH/src/github.com/chrislusf/seaweedfs/weed
|
||||
```
|
||||
|
||||
## Disk Related topics ##
|
||||
|
||||
### Hard Drive Performance ###
|
||||
When testing read performance on SeaweedFS, it basically becomes performance test your hard drive's random read speed. Hard Drive usually get 100MB/s~200MB/s.
|
||||
|
||||
When testing read performance on SeaweedFS, it basically becomes performance test for your hard drive's random read speed. Hard Drive usually get 100MB/s~200MB/s.
|
||||
|
||||
### Solid State Disk
|
||||
|
||||
To modify or delete small files, SSD must delete a whole block at a time, and move content in existing blocks to a new block. SSD is fast when brand new, but will get fragmented over time and you have to garbage collect, compacting blocks. SeaweedFS is friendly to SSD since it is append-only. Deletion and compaction are done on volume level in the background, not slowing reading and not causing fragmentation.
|
||||
|
||||
## Not Planned
|
||||
|
||||
POSIX support
|
||||
|
||||
|
||||
## Benchmark
|
||||
|
||||
My Own Unscientific Single Machine Results on Mac Book with Solid State Disk, CPU: 1 Intel Core i7 2.2GHz.
|
||||
My Own Unscientific Single Machine Results on Mac Book with Solid State Disk, CPU: 1 Intel Core i7 2.6GHz.
|
||||
|
||||
Write 1 million 1KB file:
|
||||
```
|
||||
Concurrency Level: 64
|
||||
Time taken for tests: 182.456 seconds
|
||||
Concurrency Level: 16
|
||||
Time taken for tests: 88.796 seconds
|
||||
Complete requests: 1048576
|
||||
Failed requests: 0
|
||||
Total transferred: 1073741824 bytes
|
||||
Requests per second: 5747.01 [#/sec]
|
||||
Transfer rate: 5747.01 [Kbytes/sec]
|
||||
Total transferred: 1106764659 bytes
|
||||
Requests per second: 11808.87 [#/sec]
|
||||
Transfer rate: 12172.05 [Kbytes/sec]
|
||||
|
||||
Connection Times (ms)
|
||||
min avg max std
|
||||
Total: 0.3 10.9 430.9 5.7
|
||||
Total: 0.2 1.3 44.8 0.9
|
||||
|
||||
Percentage of the requests served within a certain time (ms)
|
||||
50% 10.2 ms
|
||||
66% 12.0 ms
|
||||
75% 12.6 ms
|
||||
80% 12.9 ms
|
||||
90% 14.0 ms
|
||||
95% 14.9 ms
|
||||
98% 16.2 ms
|
||||
99% 17.3 ms
|
||||
100% 430.9 ms
|
||||
50% 1.1 ms
|
||||
66% 1.3 ms
|
||||
75% 1.5 ms
|
||||
80% 1.7 ms
|
||||
90% 2.1 ms
|
||||
95% 2.6 ms
|
||||
98% 3.7 ms
|
||||
99% 4.6 ms
|
||||
100% 44.8 ms
|
||||
```
|
||||
|
||||
Randomly read 1 million files:
|
||||
```
|
||||
Concurrency Level: 64
|
||||
Time taken for tests: 80.732 seconds
|
||||
Concurrency Level: 16
|
||||
Time taken for tests: 34.263 seconds
|
||||
Complete requests: 1048576
|
||||
Failed requests: 0
|
||||
Total transferred: 1073741824 bytes
|
||||
Requests per second: 12988.37 [#/sec]
|
||||
Transfer rate: 12988.37 [Kbytes/sec]
|
||||
Total transferred: 1106762945 bytes
|
||||
Requests per second: 30603.34 [#/sec]
|
||||
Transfer rate: 31544.49 [Kbytes/sec]
|
||||
|
||||
Connection Times (ms)
|
||||
min avg max std
|
||||
Total: 0.0 4.7 254.3 6.3
|
||||
Total: 0.0 0.5 20.7 0.7
|
||||
|
||||
Percentage of the requests served within a certain time (ms)
|
||||
50% 2.6 ms
|
||||
66% 2.9 ms
|
||||
75% 3.7 ms
|
||||
80% 4.7 ms
|
||||
90% 10.3 ms
|
||||
95% 16.6 ms
|
||||
98% 26.3 ms
|
||||
99% 34.8 ms
|
||||
100% 254.3 ms
|
||||
50% 0.4 ms
|
||||
75% 0.5 ms
|
||||
95% 0.6 ms
|
||||
98% 0.8 ms
|
||||
99% 1.2 ms
|
||||
100% 20.7 ms
|
||||
```
|
||||
|
||||
|
||||
@@ -376,3 +448,8 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://starcharts.herokuapp.com/chrislusf/seaweedfs)
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<h1 align="center">Sponsors & Backers</h1>
|
||||
|
||||
- [Become a backer or sponsor on Patreon](https://www.patreon.com/seaweedfs).
|
||||
|
||||
<h2 align="center">Generous Backers ($50+)</h2>
|
||||
|
||||
- [Evercam Camera Management Software](https://evercam.io/)
|
||||
|
||||
<h2 align="center">Backers</h2>
|
||||
|
||||
- [ColorfulClouds Tech Co. Ltd.](https://caiyunai.com/)
|
||||
- [Haravan - Ecommerce Platform](https://www.haravan.com)
|
||||
@@ -0,0 +1,27 @@
|
||||
FROM frolvlad/alpine-glibc:alpine-3.5
|
||||
|
||||
# Tried to use curl only (curl -o /tmp/linux_amd64.tar.gz ...), however it turned out that the following tar command failed with "gzip: stdin: not in gzip format"
|
||||
RUN apk add --no-cache --virtual build-dependencies --update wget curl ca-certificates && \
|
||||
wget -P /tmp https://github.com/$(curl -s -L https://github.com/chrislusf/seaweedfs/releases/latest | egrep -o 'chrislusf/seaweedfs/releases/download/.*/linux_amd64.tar.gz') && \
|
||||
tar -C /usr/bin/ -xzvf /tmp/linux_amd64.tar.gz && \
|
||||
apk del build-dependencies && \
|
||||
rm -rf /tmp/*
|
||||
|
||||
# volume server gprc port
|
||||
EXPOSE 18080
|
||||
# volume server http port
|
||||
EXPOSE 8080
|
||||
# filer server gprc port
|
||||
EXPOSE 18888
|
||||
# filer server http port
|
||||
EXPOSE 8888
|
||||
# master server shared gprc+http port
|
||||
EXPOSE 9333
|
||||
|
||||
VOLUME /data
|
||||
|
||||
COPY filer.toml /etc/seaweedfs/filer.toml
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,40 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
master:
|
||||
#image: chrislusf/seaweedfs # use a remote image
|
||||
build: . # build our container from the local Dockerfile
|
||||
ports:
|
||||
- 9333:9333
|
||||
command: "master"
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- seaweed_master
|
||||
volume:
|
||||
#image: chrislusf/seaweedfs # use a remote image
|
||||
build: . # build our container from the local Dockerfile
|
||||
ports:
|
||||
- 8080:8080
|
||||
- 18080:18080
|
||||
command: 'volume -max=5 -mserver="master:9333" -port=8080'
|
||||
depends_on:
|
||||
- master
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- seaweed_volume
|
||||
filer:
|
||||
#image: chrislusf/seaweedfs # use a remote image
|
||||
build: . # build our container from the local Dockerfile
|
||||
ports:
|
||||
- 8888:8888
|
||||
- 18888:18888
|
||||
command: 'filer -master="master:9333"'
|
||||
depends_on:
|
||||
- master
|
||||
- volume
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- seaweed_filer
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
|
||||
case "$1" in
|
||||
|
||||
'master')
|
||||
ARGS="-ip `hostname -i` -mdir /data"
|
||||
# Is this instance linked with an other master? (Docker commandline "--link master1:master")
|
||||
if [ -n "$MASTER_PORT_9333_TCP_ADDR" ] ; then
|
||||
ARGS="$ARGS -peers=$MASTER_PORT_9333_TCP_ADDR:$MASTER_PORT_9333_TCP_PORT"
|
||||
fi
|
||||
exec /usr/bin/weed $@ $ARGS
|
||||
;;
|
||||
|
||||
'volume')
|
||||
ARGS="-ip `hostname -i` -dir /data"
|
||||
# Is this instance linked with a master? (Docker commandline "--link master1:master")
|
||||
if [ -n "$MASTER_PORT_9333_TCP_ADDR" ] ; then
|
||||
ARGS="$ARGS -mserver=$MASTER_PORT_9333_TCP_ADDR:$MASTER_PORT_9333_TCP_PORT"
|
||||
fi
|
||||
exec /usr/bin/weed $@ $ARGS
|
||||
;;
|
||||
|
||||
'server')
|
||||
ARGS="-ip `hostname -i` -dir /data"
|
||||
if [ -n "$MASTER_PORT_9333_TCP_ADDR" ] ; then
|
||||
ARGS="$ARGS -master.peers=$MASTER_PORT_9333_TCP_ADDR:$MASTER_PORT_9333_TCP_PORT"
|
||||
fi
|
||||
exec /usr/bin/weed $@ $ARGS
|
||||
;;
|
||||
|
||||
'filer')
|
||||
ARGS="-ip `hostname -i`"
|
||||
if [ -n "$MASTER_PORT_9333_TCP_ADDR" ] ; then
|
||||
ARGS="$ARGS -master=$MASTER_PORT_9333_TCP_ADDR:$MASTER_PORT_9333_TCP_PORT"
|
||||
fi
|
||||
exec /usr/bin/weed $@ $ARGS
|
||||
;;
|
||||
|
||||
*)
|
||||
exec /usr/bin/weed $@
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,3 @@
|
||||
[leveldb]
|
||||
enabled = true
|
||||
dir = "."
|
||||
@@ -1,45 +0,0 @@
|
||||
package compress
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSortedData(t *testing.T) {
|
||||
data := make([]int32, 102400)
|
||||
for i := 1; i < len(data); i++ {
|
||||
data[i] = data[i-1] + rand.Int31n(15)
|
||||
}
|
||||
testCompressAndUncompress(t, data, "Sorted data")
|
||||
}
|
||||
|
||||
func TestUnsortedData(t *testing.T) {
|
||||
data := make([]int32, 102400)
|
||||
for i := 0; i < len(data); i++ {
|
||||
data[i] = rand.Int31n(255)
|
||||
}
|
||||
testCompressAndUncompress(t, data, "Unsorted data")
|
||||
}
|
||||
|
||||
func testCompressAndUncompress(t *testing.T, data []int32, desc string) {
|
||||
|
||||
compressed_data, err := Compress32(data)
|
||||
if err != nil {
|
||||
t.Fatal("Compress error", err.Error())
|
||||
}
|
||||
uncompressed_data, err := Uncompress32(compressed_data, make([]int32, len(data)*2))
|
||||
if err != nil {
|
||||
t.Fatal("Compress error", err.Error())
|
||||
}
|
||||
if len(uncompressed_data) != len(data) {
|
||||
t.Fatal("Len differs", len(data), len(uncompressed_data))
|
||||
}
|
||||
for i := 0; i < len(data); i++ {
|
||||
if data[i] != uncompressed_data[i] {
|
||||
t.Fatal("Data differs:", i, data[i], uncompressed_data[i])
|
||||
}
|
||||
}
|
||||
|
||||
println(desc, " Data length:", len(data), " => Compressed length:", len(compressed_data))
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package compress
|
||||
|
||||
import (
|
||||
"github.com/reducedb/encoding/cursor"
|
||||
"github.com/reducedb/encoding/delta/bp32"
|
||||
)
|
||||
|
||||
// Compress compresses in[]int32 to out[]int32
|
||||
func Compress32(in []int32) (out []int32, err error) {
|
||||
out = make([]int32, len(in)*2)
|
||||
inpos := cursor.New()
|
||||
outpos := cursor.New()
|
||||
|
||||
if err = bp32.New().Compress(in, inpos, len(in), out, outpos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out[:outpos.Get()], nil
|
||||
}
|
||||
|
||||
// Uncompress uncompresses in[]int32 to out[]int32
|
||||
func Uncompress32(in []int32, buffer []int32) (out []int32, err error) {
|
||||
out = buffer
|
||||
inpos := cursor.New()
|
||||
outpos := cursor.New()
|
||||
|
||||
if err = bp32.New().Uncompress(in, inpos, len(in), out, outpos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out[:outpos.Get()], nil
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package cassandra_store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
|
||||
"github.com/gocql/gocql"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Basically you need a table just like this:
|
||||
|
||||
CREATE TABLE seaweed_files (
|
||||
path varchar,
|
||||
fids list<varchar>,
|
||||
PRIMARY KEY (path)
|
||||
);
|
||||
|
||||
Need to match flat_namespace.FlatNamespaceStore interface
|
||||
Put(fullFileName string, fid string) (err error)
|
||||
Get(fullFileName string) (fid string, err error)
|
||||
Delete(fullFileName string) (fid string, err error)
|
||||
|
||||
*/
|
||||
type CassandraStore struct {
|
||||
cluster *gocql.ClusterConfig
|
||||
session *gocql.Session
|
||||
}
|
||||
|
||||
func NewCassandraStore(keyspace string, hosts ...string) (c *CassandraStore, err error) {
|
||||
c = &CassandraStore{}
|
||||
c.cluster = gocql.NewCluster(hosts...)
|
||||
c.cluster.Keyspace = keyspace
|
||||
c.cluster.Consistency = gocql.Quorum
|
||||
c.session, err = c.cluster.CreateSession()
|
||||
if err != nil {
|
||||
glog.V(0).Infof("Failed to open cassandra store, hosts %v, keyspace %s", hosts, keyspace)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *CassandraStore) Put(fullFileName string, fid string) (err error) {
|
||||
var input []string
|
||||
input = append(input, fid)
|
||||
if err := c.session.Query(
|
||||
`INSERT INTO seaweed_files (path, fids) VALUES (?, ?)`,
|
||||
fullFileName, input).Exec(); err != nil {
|
||||
glog.V(0).Infof("Failed to save file %s with id %s: %v", fullFileName, fid, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (c *CassandraStore) Get(fullFileName string) (fid string, err error) {
|
||||
var output []string
|
||||
if err := c.session.Query(
|
||||
`select fids FROM seaweed_files WHERE path = ? LIMIT 1`,
|
||||
fullFileName).Consistency(gocql.One).Scan(&output); err != nil {
|
||||
if err != gocql.ErrNotFound {
|
||||
glog.V(0).Infof("Failed to find file %s: %v", fullFileName, fid, err)
|
||||
}
|
||||
}
|
||||
if len(output) == 0 {
|
||||
return "", fmt.Errorf("No file id found for %s", fullFileName)
|
||||
}
|
||||
return output[0], nil
|
||||
}
|
||||
|
||||
// Currently the fid is not returned
|
||||
func (c *CassandraStore) Delete(fullFileName string) (fid string, err error) {
|
||||
if err := c.session.Query(
|
||||
`DELETE FROM seaweed_files WHERE path = ?`,
|
||||
fullFileName).Exec(); err != nil {
|
||||
if err != gocql.ErrNotFound {
|
||||
glog.V(0).Infof("Failed to delete file %s: %v", fullFileName, err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (c *CassandraStore) Close() {
|
||||
if c.session != nil {
|
||||
c.session.Close()
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
|
||||
Here is the CQL to create the table.CassandraStore
|
||||
|
||||
Optionally you can adjust the keyspace name and replication settings.
|
||||
|
||||
For production server, very likely you want to set replication_factor to 3
|
||||
|
||||
*/
|
||||
|
||||
create keyspace seaweed WITH replication = {
|
||||
'class':'SimpleStrategy',
|
||||
'replication_factor':1
|
||||
};
|
||||
|
||||
use seaweed;
|
||||
|
||||
CREATE TABLE seaweed_files (
|
||||
path varchar,
|
||||
fids list<varchar>,
|
||||
PRIMARY KEY (path)
|
||||
);
|
||||
@@ -1,70 +0,0 @@
|
||||
package filer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type ApiRequest struct {
|
||||
Command string //"listFiles", "listDirectories"
|
||||
Directory string
|
||||
FileName string
|
||||
}
|
||||
|
||||
type ListFilesResult struct {
|
||||
Files []FileEntry
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func ListFiles(server string, directory string, fileName string) (*ListFilesResult, error) {
|
||||
var ret ListFilesResult
|
||||
if err := call(server, ApiRequest{Command: "listFiles", Directory: directory, FileName: fileName}, &ret); err == nil {
|
||||
if ret.Error != "" {
|
||||
return nil, errors.New(ret.Error)
|
||||
}
|
||||
return &ret, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
type ListDirectoriesResult struct {
|
||||
Directories []DirectoryEntry
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func ListDirectories(server string, directory string) (*ListDirectoriesResult, error) {
|
||||
var ret ListDirectoriesResult
|
||||
if err := call(server, ApiRequest{Command: "listDirectories", Directory: directory}, &ret); err == nil {
|
||||
if ret.Error != "" {
|
||||
return nil, errors.New(ret.Error)
|
||||
}
|
||||
return &ret, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func call(server string, request ApiRequest, ret interface{}) error {
|
||||
b, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
fmt.Println("error:", err)
|
||||
return nil
|
||||
}
|
||||
values := make(url.Values)
|
||||
values.Add("request", string(b))
|
||||
jsonBlob, err := util.Post("http://"+server+"/__api__", values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(jsonBlob, ret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
Design Assumptions:
|
||||
1. the number of directories are magnitudely smaller than the number of files
|
||||
2. unlimited number of files under any directories
|
||||
Phylosophy:
|
||||
metadata for directories and files should be separated
|
||||
Design:
|
||||
Store directories in normal map
|
||||
all of directories hopefully all be in memory
|
||||
efficient to move/rename/list_directories
|
||||
Log directory changes to append only log file
|
||||
Store files in sorted string table in <dir_id/filename> format
|
||||
efficient to list_files, just simple iterator
|
||||
efficient to locate files, binary search
|
||||
|
||||
Testing:
|
||||
1. starting server, "weed server -filer=true"
|
||||
2. posting files to different folders
|
||||
curl -F "filename=@design.txt" "http://localhost:8888/sources/"
|
||||
curl -F "filename=@design.txt" "http://localhost:8888/design/"
|
||||
curl -F "filename=@directory.go" "http://localhost:8888/sources/weed/go/"
|
||||
curl -F "filename=@directory.go" "http://localhost:8888/sources/testing/go/"
|
||||
curl -F "filename=@filer.go" "http://localhost:8888/sources/weed/go/"
|
||||
curl -F "filename=@filer_in_leveldb.go" "http://localhost:8888/sources/weed/go/"
|
||||
curl "http://localhost:8888/?pretty=y"
|
||||
curl "http://localhost:8888/sources/weed/go/?pretty=y"
|
||||
curl "http://localhost:8888/sources/weed/go/?pretty=y"
|
||||
@@ -1,15 +0,0 @@
|
||||
package embedded_filer
|
||||
|
||||
import (
|
||||
"github.com/chrislusf/weed-fs/go/filer"
|
||||
)
|
||||
|
||||
type DirectoryManager interface {
|
||||
FindDirectory(dirPath string) (filer.DirectoryId, error)
|
||||
ListDirectories(dirPath string) (dirs []filer.DirectoryEntry, err error)
|
||||
MakeDirectory(currentDirPath string, dirName string) (filer.DirectoryId, error)
|
||||
MoveUnderDirectory(oldDirPath string, newParentDirPath string) error
|
||||
DeleteDirectory(dirPath string) error
|
||||
//functions used by FUSE
|
||||
FindDirectoryById(filer.DirectoryId, error)
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
package embedded_filer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/filer"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
)
|
||||
|
||||
var writeLock sync.Mutex //serialize changes to dir.log
|
||||
|
||||
type DirectoryEntryInMap struct {
|
||||
Name string
|
||||
Parent *DirectoryEntryInMap
|
||||
SubDirectories map[string]*DirectoryEntryInMap
|
||||
Id filer.DirectoryId
|
||||
}
|
||||
|
||||
type DirectoryManagerInMap struct {
|
||||
Root *DirectoryEntryInMap
|
||||
max filer.DirectoryId
|
||||
logFile *os.File
|
||||
isLoading bool
|
||||
}
|
||||
|
||||
func (dm *DirectoryManagerInMap) NewDirectoryEntryInMap(parent *DirectoryEntryInMap, name string) (d *DirectoryEntryInMap, err error) {
|
||||
writeLock.Lock()
|
||||
defer writeLock.Unlock()
|
||||
d = &DirectoryEntryInMap{Name: name, Parent: parent, SubDirectories: make(map[string]*DirectoryEntryInMap)}
|
||||
var parts []string
|
||||
for p := d; p != nil && p.Name != ""; p = p.Parent {
|
||||
parts = append(parts, p.Name)
|
||||
}
|
||||
n := len(parts)
|
||||
if n <= 0 {
|
||||
return nil, fmt.Errorf("Failed to create folder %s/%s", parent.Name, name)
|
||||
}
|
||||
for i := 0; i < n/2; i++ {
|
||||
parts[i], parts[n-1-i] = parts[n-1-i], parts[i]
|
||||
}
|
||||
dm.max++
|
||||
d.Id = dm.max
|
||||
dm.log("add", "/"+strings.Join(parts, "/"), strconv.Itoa(int(d.Id)))
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (dm *DirectoryManagerInMap) log(words ...string) {
|
||||
if !dm.isLoading {
|
||||
dm.logFile.WriteString(strings.Join(words, "\t") + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
func NewDirectoryManagerInMap(dirLogFile string) (dm *DirectoryManagerInMap, err error) {
|
||||
dm = &DirectoryManagerInMap{}
|
||||
//dm.Root do not use NewDirectoryEntryInMap, since dm.max will be changed
|
||||
dm.Root = &DirectoryEntryInMap{SubDirectories: make(map[string]*DirectoryEntryInMap)}
|
||||
if dm.logFile, err = os.OpenFile(dirLogFile, os.O_RDWR|os.O_CREATE, 0644); err != nil {
|
||||
return nil, fmt.Errorf("cannot write directory log file %s.idx: %v", dirLogFile, err)
|
||||
}
|
||||
return dm, dm.load()
|
||||
}
|
||||
|
||||
func (dm *DirectoryManagerInMap) processEachLine(line string) error {
|
||||
if strings.HasPrefix(line, "#") {
|
||||
return nil
|
||||
}
|
||||
if line == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(line, "\t")
|
||||
if len(parts) == 0 {
|
||||
return nil
|
||||
}
|
||||
switch parts[0] {
|
||||
case "add":
|
||||
v, pe := strconv.Atoi(parts[2])
|
||||
if pe != nil {
|
||||
return pe
|
||||
}
|
||||
if e := dm.loadDirectory(parts[1], filer.DirectoryId(v)); e != nil {
|
||||
return e
|
||||
}
|
||||
case "mov":
|
||||
newName := ""
|
||||
if len(parts) >= 4 {
|
||||
newName = parts[3]
|
||||
}
|
||||
if e := dm.MoveUnderDirectory(parts[1], parts[2], newName); e != nil {
|
||||
return e
|
||||
}
|
||||
case "del":
|
||||
if e := dm.DeleteDirectory(parts[1]); e != nil {
|
||||
return e
|
||||
}
|
||||
default:
|
||||
fmt.Printf("line %s has %s!\n", line, parts[0])
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (dm *DirectoryManagerInMap) load() error {
|
||||
dm.max = 0
|
||||
lines := bufio.NewReader(dm.logFile)
|
||||
dm.isLoading = true
|
||||
defer func() { dm.isLoading = false }()
|
||||
for {
|
||||
line, err := util.Readln(lines)
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
if pe := dm.processEachLine(string(line)); pe != nil {
|
||||
return pe
|
||||
}
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dm *DirectoryManagerInMap) findDirectory(dirPath string) (*DirectoryEntryInMap, error) {
|
||||
if dirPath == "" {
|
||||
return dm.Root, nil
|
||||
}
|
||||
dirPath = filepath.Clean(dirPath)
|
||||
if dirPath == "/" {
|
||||
return dm.Root, nil
|
||||
}
|
||||
parts := strings.Split(dirPath, "/")
|
||||
dir := dm.Root
|
||||
for i := 1; i < len(parts); i++ {
|
||||
if sub, ok := dir.SubDirectories[parts[i]]; ok {
|
||||
dir = sub
|
||||
} else {
|
||||
return dm.Root, fmt.Errorf("Directory %s Not Found", dirPath)
|
||||
}
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
func (dm *DirectoryManagerInMap) FindDirectory(dirPath string) (filer.DirectoryId, error) {
|
||||
d, e := dm.findDirectory(dirPath)
|
||||
if e == nil {
|
||||
return d.Id, nil
|
||||
}
|
||||
return dm.Root.Id, e
|
||||
}
|
||||
|
||||
func (dm *DirectoryManagerInMap) loadDirectory(dirPath string, dirId filer.DirectoryId) error {
|
||||
dirPath = filepath.Clean(dirPath)
|
||||
if dirPath == "/" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(dirPath, "/")
|
||||
dir := dm.Root
|
||||
for i := 1; i < len(parts); i++ {
|
||||
sub, ok := dir.SubDirectories[parts[i]]
|
||||
if !ok {
|
||||
if i != len(parts)-1 {
|
||||
return fmt.Errorf("%s should be created after parent %s", dirPath, parts[i])
|
||||
}
|
||||
var err error
|
||||
sub, err = dm.NewDirectoryEntryInMap(dir, parts[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sub.Id != dirId {
|
||||
return fmt.Errorf("%s should be have id %v instead of %v", dirPath, sub.Id, dirId)
|
||||
}
|
||||
dir.SubDirectories[parts[i]] = sub
|
||||
}
|
||||
dir = sub
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dm *DirectoryManagerInMap) makeDirectory(dirPath string) (dir *DirectoryEntryInMap, created bool) {
|
||||
dirPath = filepath.Clean(dirPath)
|
||||
if dirPath == "/" {
|
||||
return dm.Root, false
|
||||
}
|
||||
parts := strings.Split(dirPath, "/")
|
||||
dir = dm.Root
|
||||
for i := 1; i < len(parts); i++ {
|
||||
sub, ok := dir.SubDirectories[parts[i]]
|
||||
if !ok {
|
||||
var err error
|
||||
sub, err = dm.NewDirectoryEntryInMap(dir, parts[i])
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
dir.SubDirectories[parts[i]] = sub
|
||||
created = true
|
||||
}
|
||||
dir = sub
|
||||
}
|
||||
return dir, created
|
||||
}
|
||||
|
||||
func (dm *DirectoryManagerInMap) MakeDirectory(dirPath string) (filer.DirectoryId, error) {
|
||||
dir, _ := dm.makeDirectory(dirPath)
|
||||
return dir.Id, nil
|
||||
}
|
||||
|
||||
func (dm *DirectoryManagerInMap) MoveUnderDirectory(oldDirPath string, newParentDirPath string, newName string) error {
|
||||
writeLock.Lock()
|
||||
defer writeLock.Unlock()
|
||||
oldDir, oe := dm.findDirectory(oldDirPath)
|
||||
if oe != nil {
|
||||
return oe
|
||||
}
|
||||
parentDir, pe := dm.findDirectory(newParentDirPath)
|
||||
if pe != nil {
|
||||
return pe
|
||||
}
|
||||
dm.log("mov", oldDirPath, newParentDirPath, newName)
|
||||
delete(oldDir.Parent.SubDirectories, oldDir.Name)
|
||||
if newName == "" {
|
||||
newName = oldDir.Name
|
||||
}
|
||||
parentDir.SubDirectories[newName] = oldDir
|
||||
oldDir.Name = newName
|
||||
oldDir.Parent = parentDir
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dm *DirectoryManagerInMap) ListDirectories(dirPath string) (dirNames []filer.DirectoryEntry, err error) {
|
||||
d, e := dm.findDirectory(dirPath)
|
||||
if e != nil {
|
||||
return dirNames, e
|
||||
}
|
||||
for k, v := range d.SubDirectories {
|
||||
dirNames = append(dirNames, filer.DirectoryEntry{Name: k, Id: v.Id})
|
||||
}
|
||||
return dirNames, nil
|
||||
}
|
||||
func (dm *DirectoryManagerInMap) DeleteDirectory(dirPath string) error {
|
||||
writeLock.Lock()
|
||||
defer writeLock.Unlock()
|
||||
if dirPath == "/" {
|
||||
return fmt.Errorf("Can not delete %s", dirPath)
|
||||
}
|
||||
d, e := dm.findDirectory(dirPath)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if len(d.SubDirectories) != 0 {
|
||||
return fmt.Errorf("dir %s still has sub directories", dirPath)
|
||||
}
|
||||
delete(d.Parent.SubDirectories, d.Name)
|
||||
d.Parent = nil
|
||||
dm.log("del", dirPath)
|
||||
return nil
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package embedded_filer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDirectory(t *testing.T) {
|
||||
dm, _ := NewDirectoryManagerInMap("/tmp/dir.log")
|
||||
defer func() {
|
||||
if true {
|
||||
os.Remove("/tmp/dir.log")
|
||||
}
|
||||
}()
|
||||
dm.MakeDirectory("/a/b/c")
|
||||
dm.MakeDirectory("/a/b/d")
|
||||
dm.MakeDirectory("/a/b/e")
|
||||
dm.MakeDirectory("/a/b/e/f")
|
||||
dm.MakeDirectory("/a/b/e/f/g")
|
||||
dm.MoveUnderDirectory("/a/b/e/f/g", "/a/b", "t")
|
||||
if _, err := dm.FindDirectory("/a/b/e/f/g"); err == nil {
|
||||
t.Fatal("/a/b/e/f/g should not exist any more after moving")
|
||||
}
|
||||
if _, err := dm.FindDirectory("/a/b/t"); err != nil {
|
||||
t.Fatal("/a/b/t should exist after moving")
|
||||
}
|
||||
if _, err := dm.FindDirectory("/a/b/g"); err == nil {
|
||||
t.Fatal("/a/b/g should not exist after moving")
|
||||
}
|
||||
dm.MoveUnderDirectory("/a/b/e/f", "/a/b", "")
|
||||
if _, err := dm.FindDirectory("/a/b/f"); err != nil {
|
||||
t.Fatal("/a/b/g should not exist after moving")
|
||||
}
|
||||
dm.MakeDirectory("/a/b/g/h/i")
|
||||
dm.DeleteDirectory("/a/b/e/f")
|
||||
dm.DeleteDirectory("/a/b/e")
|
||||
dirNames, _ := dm.ListDirectories("/a/b/e")
|
||||
for _, v := range dirNames {
|
||||
println("sub1 dir:", v.Name, "id", v.Id)
|
||||
}
|
||||
dm.logFile.Close()
|
||||
|
||||
var path []string
|
||||
printTree(dm.Root, path)
|
||||
|
||||
dm2, e := NewDirectoryManagerInMap("/tmp/dir.log")
|
||||
if e != nil {
|
||||
println("load error", e.Error())
|
||||
}
|
||||
if !compare(dm.Root, dm2.Root) {
|
||||
t.Fatal("restored dir not the same!")
|
||||
}
|
||||
printTree(dm2.Root, path)
|
||||
}
|
||||
|
||||
func printTree(node *DirectoryEntryInMap, path []string) {
|
||||
println(strings.Join(path, "/") + "/" + node.Name)
|
||||
path = append(path, node.Name)
|
||||
for _, v := range node.SubDirectories {
|
||||
printTree(v, path)
|
||||
}
|
||||
}
|
||||
|
||||
func compare(root1 *DirectoryEntryInMap, root2 *DirectoryEntryInMap) bool {
|
||||
if len(root1.SubDirectories) != len(root2.SubDirectories) {
|
||||
return false
|
||||
}
|
||||
if root1.Name != root2.Name {
|
||||
return false
|
||||
}
|
||||
if root1.Id != root2.Id {
|
||||
return false
|
||||
}
|
||||
if !(root1.Parent == nil && root2.Parent == nil) {
|
||||
if root1.Parent.Id != root2.Parent.Id {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for k, v := range root1.SubDirectories {
|
||||
if !compare(v, root2.SubDirectories[k]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package embedded_filer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/filer"
|
||||
"github.com/chrislusf/weed-fs/go/operation"
|
||||
)
|
||||
|
||||
type FilerEmbedded struct {
|
||||
master string
|
||||
directories *DirectoryManagerInMap
|
||||
files *FileListInLevelDb
|
||||
}
|
||||
|
||||
func NewFilerEmbedded(master string, dir string) (filer *FilerEmbedded, err error) {
|
||||
dm, de := NewDirectoryManagerInMap(filepath.Join(dir, "dir.log"))
|
||||
if de != nil {
|
||||
return nil, de
|
||||
}
|
||||
fl, fe := NewFileListInLevelDb(dir)
|
||||
if fe != nil {
|
||||
return nil, fe
|
||||
}
|
||||
filer = &FilerEmbedded{
|
||||
master: master,
|
||||
directories: dm,
|
||||
files: fl,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (filer *FilerEmbedded) CreateFile(filePath string, fid string) (err error) {
|
||||
dir, file := filepath.Split(filePath)
|
||||
dirId, e := filer.directories.MakeDirectory(dir)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
return filer.files.CreateFile(dirId, file, fid)
|
||||
}
|
||||
func (filer *FilerEmbedded) FindFile(filePath string) (fid string, err error) {
|
||||
dir, file := filepath.Split(filePath)
|
||||
dirId, e := filer.directories.FindDirectory(dir)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
return filer.files.FindFile(dirId, file)
|
||||
}
|
||||
func (filer *FilerEmbedded) FindDirectory(dirPath string) (dirId filer.DirectoryId, err error) {
|
||||
return filer.directories.FindDirectory(dirPath)
|
||||
}
|
||||
func (filer *FilerEmbedded) ListDirectories(dirPath string) (dirs []filer.DirectoryEntry, err error) {
|
||||
return filer.directories.ListDirectories(dirPath)
|
||||
}
|
||||
func (filer *FilerEmbedded) ListFiles(dirPath string, lastFileName string, limit int) (files []filer.FileEntry, err error) {
|
||||
dirId, e := filer.directories.FindDirectory(dirPath)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return filer.files.ListFiles(dirId, lastFileName, limit), nil
|
||||
}
|
||||
func (filer *FilerEmbedded) DeleteDirectory(dirPath string, recursive bool) (err error) {
|
||||
dirId, e := filer.directories.FindDirectory(dirPath)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if sub_dirs, sub_err := filer.directories.ListDirectories(dirPath); sub_err == nil {
|
||||
if len(sub_dirs) > 0 && !recursive {
|
||||
return fmt.Errorf("Fail to delete directory %s: %d sub directories found!", dirPath, len(sub_dirs))
|
||||
}
|
||||
for _, sub := range sub_dirs {
|
||||
if delete_sub_err := filer.DeleteDirectory(filepath.Join(dirPath, sub.Name), recursive); delete_sub_err != nil {
|
||||
return delete_sub_err
|
||||
}
|
||||
}
|
||||
}
|
||||
list := filer.files.ListFiles(dirId, "", 100)
|
||||
if len(list) != 0 && !recursive {
|
||||
if !recursive {
|
||||
return fmt.Errorf("Fail to delete non-empty directory %s!", dirPath)
|
||||
}
|
||||
}
|
||||
for {
|
||||
if len(list) == 0 {
|
||||
return filer.directories.DeleteDirectory(dirPath)
|
||||
}
|
||||
var fids []string
|
||||
for _, fileEntry := range list {
|
||||
fids = append(fids, string(fileEntry.Id))
|
||||
}
|
||||
if result_list, delete_file_err := operation.DeleteFiles(filer.master, fids); delete_file_err != nil {
|
||||
return delete_file_err
|
||||
} else {
|
||||
if len(result_list.Errors) > 0 {
|
||||
return errors.New(strings.Join(result_list.Errors, "\n"))
|
||||
}
|
||||
}
|
||||
lastFile := list[len(list)-1]
|
||||
list = filer.files.ListFiles(dirId, lastFile.Name, 100)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (filer *FilerEmbedded) DeleteFile(filePath string) (fid string, err error) {
|
||||
dir, file := filepath.Split(filePath)
|
||||
dirId, e := filer.directories.FindDirectory(dir)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
return filer.files.DeleteFile(dirId, file)
|
||||
}
|
||||
|
||||
/*
|
||||
Move a folder or a file, with 4 Use cases:
|
||||
mv fromDir toNewDir
|
||||
mv fromDir toOldDir
|
||||
mv fromFile toDir
|
||||
mv fromFile toFile
|
||||
*/
|
||||
func (filer *FilerEmbedded) Move(fromPath string, toPath string) error {
|
||||
if _, dir_err := filer.FindDirectory(fromPath); dir_err == nil {
|
||||
if _, err := filer.FindDirectory(toPath); err == nil {
|
||||
// move folder under an existing folder
|
||||
return filer.directories.MoveUnderDirectory(fromPath, toPath, "")
|
||||
}
|
||||
// move folder to a new folder
|
||||
return filer.directories.MoveUnderDirectory(fromPath, filepath.Dir(toPath), filepath.Base(toPath))
|
||||
}
|
||||
if fid, file_err := filer.DeleteFile(fromPath); file_err == nil {
|
||||
if _, err := filer.FindDirectory(toPath); err == nil {
|
||||
// move file under an existing folder
|
||||
return filer.CreateFile(filepath.Join(toPath, filepath.Base(fromPath)), fid)
|
||||
}
|
||||
// move to a folder with new name
|
||||
return filer.CreateFile(toPath, fid)
|
||||
}
|
||||
return fmt.Errorf("File %s is not found!", fromPath)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package embedded_filer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/filer"
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
/*
|
||||
The entry in level db has this format:
|
||||
key: genKey(dirId, fileName)
|
||||
value: []byte(fid)
|
||||
And genKey(dirId, fileName) use first 4 bytes to store dirId, and rest for fileName
|
||||
*/
|
||||
|
||||
type FileListInLevelDb struct {
|
||||
db *leveldb.DB
|
||||
}
|
||||
|
||||
func NewFileListInLevelDb(dir string) (fl *FileListInLevelDb, err error) {
|
||||
fl = &FileListInLevelDb{}
|
||||
if fl.db, err = leveldb.OpenFile(dir, nil); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func genKey(dirId filer.DirectoryId, fileName string) []byte {
|
||||
ret := make([]byte, 0, 4+len(fileName))
|
||||
for i := 3; i >= 0; i-- {
|
||||
ret = append(ret, byte(dirId>>(uint(i)*8)))
|
||||
}
|
||||
ret = append(ret, []byte(fileName)...)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (fl *FileListInLevelDb) CreateFile(dirId filer.DirectoryId, fileName string, fid string) (err error) {
|
||||
glog.V(4).Infoln("directory", dirId, "fileName", fileName, "fid", fid)
|
||||
return fl.db.Put(genKey(dirId, fileName), []byte(fid), nil)
|
||||
}
|
||||
func (fl *FileListInLevelDb) DeleteFile(dirId filer.DirectoryId, fileName string) (fid string, err error) {
|
||||
if fid, err = fl.FindFile(dirId, fileName); err != nil {
|
||||
return
|
||||
}
|
||||
err = fl.db.Delete(genKey(dirId, fileName), nil)
|
||||
return fid, err
|
||||
}
|
||||
func (fl *FileListInLevelDb) FindFile(dirId filer.DirectoryId, fileName string) (fid string, err error) {
|
||||
data, e := fl.db.Get(genKey(dirId, fileName), nil)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
func (fl *FileListInLevelDb) ListFiles(dirId filer.DirectoryId, lastFileName string, limit int) (files []filer.FileEntry) {
|
||||
glog.V(4).Infoln("directory", dirId, "lastFileName", lastFileName, "limit", limit)
|
||||
dirKey := genKey(dirId, "")
|
||||
iter := fl.db.NewIterator(&util.Range{Start: genKey(dirId, lastFileName)}, nil)
|
||||
limitCounter := 0
|
||||
for iter.Next() {
|
||||
key := iter.Key()
|
||||
if !bytes.HasPrefix(key, dirKey) {
|
||||
break
|
||||
}
|
||||
fileName := string(key[len(dirKey):])
|
||||
if fileName == lastFileName {
|
||||
continue
|
||||
}
|
||||
limitCounter++
|
||||
if limit > 0 {
|
||||
if limitCounter > limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
files = append(files, filer.FileEntry{Name: fileName, Id: filer.FileId(string(iter.Value()))})
|
||||
}
|
||||
iter.Release()
|
||||
return
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package filer
|
||||
|
||||
type FileId string //file id on weedfs
|
||||
|
||||
type FileEntry struct {
|
||||
Name string `json:"name,omitempty"` //file name without path
|
||||
Id FileId `json:"fid,omitempty"`
|
||||
}
|
||||
|
||||
type DirectoryId int32
|
||||
|
||||
type DirectoryEntry struct {
|
||||
Name string //dir name without path
|
||||
Id DirectoryId
|
||||
}
|
||||
|
||||
type Filer interface {
|
||||
CreateFile(fullFileName string, fid string) (err error)
|
||||
FindFile(fullFileName string) (fid string, err error)
|
||||
DeleteFile(fullFileName string) (fid string, err error)
|
||||
|
||||
//Optional functions. embedded filer support these
|
||||
FindDirectory(dirPath string) (dirId DirectoryId, err error)
|
||||
ListDirectories(dirPath string) (dirs []DirectoryEntry, err error)
|
||||
ListFiles(dirPath string, lastFileName string, limit int) (files []FileEntry, err error)
|
||||
DeleteDirectory(dirPath string, recursive bool) (err error)
|
||||
Move(fromPath string, toPath string) (err error)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package flat_namespace
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/filer"
|
||||
)
|
||||
|
||||
type FlatNamesapceFiler struct {
|
||||
master string
|
||||
store FlatNamespaceStore
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNotImplemented = errors.New("Not Implemented for flat namespace meta data store")
|
||||
)
|
||||
|
||||
func NewFlatNamesapceFiler(master string, store FlatNamespaceStore) *FlatNamesapceFiler {
|
||||
return &FlatNamesapceFiler{
|
||||
master: master,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (filer *FlatNamesapceFiler) CreateFile(fullFileName string, fid string) (err error) {
|
||||
return filer.store.Put(fullFileName, fid)
|
||||
}
|
||||
func (filer *FlatNamesapceFiler) FindFile(fullFileName string) (fid string, err error) {
|
||||
return filer.store.Get(fullFileName)
|
||||
}
|
||||
func (filer *FlatNamesapceFiler) FindDirectory(dirPath string) (dirId filer.DirectoryId, err error) {
|
||||
return 0, ErrNotImplemented
|
||||
}
|
||||
func (filer *FlatNamesapceFiler) ListDirectories(dirPath string) (dirs []filer.DirectoryEntry, err error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
func (filer *FlatNamesapceFiler) ListFiles(dirPath string, lastFileName string, limit int) (files []filer.FileEntry, err error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
func (filer *FlatNamesapceFiler) DeleteDirectory(dirPath string, recursive bool) (err error) {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
|
||||
func (filer *FlatNamesapceFiler) DeleteFile(fullFileName string) (fid string, err error) {
|
||||
return filer.store.Delete(fullFileName)
|
||||
}
|
||||
|
||||
func (filer *FlatNamesapceFiler) Move(fromPath string, toPath string) error {
|
||||
return ErrNotImplemented
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package flat_namespace
|
||||
|
||||
import ()
|
||||
|
||||
type FlatNamespaceStore interface {
|
||||
Put(fullFileName string, fid string) (err error)
|
||||
Get(fullFileName string) (fid string, err error)
|
||||
Delete(fullFileName string) (fid string, err error)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package redis_store
|
||||
|
||||
import (
|
||||
redis "gopkg.in/redis.v2"
|
||||
)
|
||||
|
||||
type RedisStore struct {
|
||||
Client *redis.Client
|
||||
}
|
||||
|
||||
func NewRedisStore(hostPort string, database int) *RedisStore {
|
||||
client := redis.NewTCPClient(&redis.Options{
|
||||
Addr: hostPort,
|
||||
Password: "", // no password set
|
||||
DB: int64(database),
|
||||
})
|
||||
return &RedisStore{Client: client}
|
||||
}
|
||||
|
||||
func (s *RedisStore) Get(fullFileName string) (fid string, err error) {
|
||||
fid, err = s.Client.Get(fullFileName).Result()
|
||||
if err == redis.Nil {
|
||||
err = nil
|
||||
}
|
||||
return fid, err
|
||||
}
|
||||
func (s *RedisStore) Put(fullFileName string, fid string) (err error) {
|
||||
_, err = s.Client.Set(fullFileName, fid).Result()
|
||||
if err == redis.Nil {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Currently the fid is not returned
|
||||
func (s *RedisStore) Delete(fullFileName string) (fid string, err error) {
|
||||
_, err = s.Client.Del(fullFileName).Result()
|
||||
if err == redis.Nil {
|
||||
err = nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (s *RedisStore) Close() {
|
||||
if s.Client != nil {
|
||||
s.Client.Close()
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package images
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/disintegration/imaging"
|
||||
)
|
||||
|
||||
func Resized(ext string, data []byte, width, height int) (resized []byte, w int, h int) {
|
||||
if width == 0 && height == 0 {
|
||||
return data, 0, 0
|
||||
}
|
||||
srcImage, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err == nil {
|
||||
bounds := srcImage.Bounds()
|
||||
var dstImage *image.NRGBA
|
||||
if bounds.Dx() > width && width != 0 || bounds.Dy() > height && height != 0 {
|
||||
if width == height && bounds.Dx() != bounds.Dy() {
|
||||
dstImage = imaging.Thumbnail(srcImage, width, height, imaging.Lanczos)
|
||||
w, h = width, height
|
||||
} else {
|
||||
dstImage = imaging.Resize(srcImage, width, height, imaging.Lanczos)
|
||||
}
|
||||
} else {
|
||||
return data, bounds.Dx(), bounds.Dy()
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
switch ext {
|
||||
case ".png":
|
||||
png.Encode(&buf, dstImage)
|
||||
case ".jpg", ".jpeg":
|
||||
jpeg.Encode(&buf, dstImage, nil)
|
||||
case ".gif":
|
||||
gif.Encode(&buf, dstImage, nil)
|
||||
}
|
||||
return buf.Bytes(), dstImage.Bounds().Dx(), dstImage.Bounds().Dy()
|
||||
} else {
|
||||
glog.Error(err)
|
||||
}
|
||||
return data, 0, 0
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package operation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
)
|
||||
|
||||
type AssignResult struct {
|
||||
Fid string `json:"fid,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
PublicUrl string `json:"publicUrl,omitempty"`
|
||||
Count uint64 `json:"count,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func Assign(server string, count uint64, replication string, collection string, ttl string) (*AssignResult, error) {
|
||||
values := make(url.Values)
|
||||
values.Add("count", strconv.FormatUint(count, 10))
|
||||
if replication != "" {
|
||||
values.Add("replication", replication)
|
||||
}
|
||||
if collection != "" {
|
||||
values.Add("collection", collection)
|
||||
}
|
||||
if ttl != "" {
|
||||
values.Add("ttl", ttl)
|
||||
}
|
||||
jsonBlob, err := util.Post("http://"+server+"/dir/assign", values)
|
||||
glog.V(2).Info("assign result :", string(jsonBlob))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ret AssignResult
|
||||
err = json.Unmarshal(jsonBlob, &ret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("/dir/assign result JSON unmarshal error:%v, json:%s", err, string(jsonBlob))
|
||||
}
|
||||
if ret.Count <= 0 {
|
||||
return nil, errors.New(ret.Error)
|
||||
}
|
||||
return &ret, nil
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package operation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/security"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
)
|
||||
|
||||
type DeleteResult struct {
|
||||
Fid string `json:"fid"`
|
||||
Size int `json:"size"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func DeleteFile(master string, fileId string, jwt security.EncodedJwt) error {
|
||||
fileUrl, err := LookupFileId(master, fileId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return util.Delete(fileUrl, jwt)
|
||||
}
|
||||
|
||||
func ParseFileId(fid string) (vid string, key_cookie string, err error) {
|
||||
commaIndex := strings.Index(fid, ",")
|
||||
if commaIndex <= 0 {
|
||||
return "", "", errors.New("Wrong fid format.")
|
||||
}
|
||||
return fid[:commaIndex], fid[commaIndex+1:], nil
|
||||
}
|
||||
|
||||
type DeleteFilesResult struct {
|
||||
Errors []string
|
||||
Results []DeleteResult
|
||||
}
|
||||
|
||||
func DeleteFiles(master string, fileIds []string) (*DeleteFilesResult, error) {
|
||||
vid_to_fileIds := make(map[string][]string)
|
||||
ret := &DeleteFilesResult{}
|
||||
var vids []string
|
||||
for _, fileId := range fileIds {
|
||||
vid, _, err := ParseFileId(fileId)
|
||||
if err != nil {
|
||||
ret.Results = append(ret.Results, DeleteResult{Fid: vid, Error: err.Error()})
|
||||
continue
|
||||
}
|
||||
if _, ok := vid_to_fileIds[vid]; !ok {
|
||||
vid_to_fileIds[vid] = make([]string, 0)
|
||||
vids = append(vids, vid)
|
||||
}
|
||||
vid_to_fileIds[vid] = append(vid_to_fileIds[vid], fileId)
|
||||
}
|
||||
|
||||
lookupResults, err := LookupVolumeIds(master, vids)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
server_to_fileIds := make(map[string][]string)
|
||||
for vid, result := range lookupResults {
|
||||
if result.Error != "" {
|
||||
ret.Errors = append(ret.Errors, result.Error)
|
||||
continue
|
||||
}
|
||||
for _, location := range result.Locations {
|
||||
if _, ok := server_to_fileIds[location.Url]; !ok {
|
||||
server_to_fileIds[location.Url] = make([]string, 0)
|
||||
}
|
||||
server_to_fileIds[location.Url] = append(
|
||||
server_to_fileIds[location.Url], vid_to_fileIds[vid]...)
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for server, fidList := range server_to_fileIds {
|
||||
wg.Add(1)
|
||||
go func(server string, fidList []string) {
|
||||
defer wg.Done()
|
||||
values := make(url.Values)
|
||||
for _, fid := range fidList {
|
||||
values.Add("fid", fid)
|
||||
}
|
||||
jsonBlob, err := util.Post("http://"+server+"/delete", values)
|
||||
if err != nil {
|
||||
ret.Errors = append(ret.Errors, err.Error()+" "+string(jsonBlob))
|
||||
return
|
||||
}
|
||||
var result []DeleteResult
|
||||
err = json.Unmarshal(jsonBlob, &result)
|
||||
if err != nil {
|
||||
ret.Errors = append(ret.Errors, err.Error()+" "+string(jsonBlob))
|
||||
return
|
||||
}
|
||||
ret.Results = append(ret.Results, result...)
|
||||
}(server, fidList)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
// Code generated by protoc-gen-go.
|
||||
// source: system_message.proto
|
||||
// DO NOT EDIT!
|
||||
|
||||
/*
|
||||
Package operation is a generated protocol buffer package.
|
||||
|
||||
It is generated from these files:
|
||||
system_message.proto
|
||||
|
||||
It has these top-level messages:
|
||||
VolumeInformationMessage
|
||||
JoinMessage
|
||||
*/
|
||||
package operation
|
||||
|
||||
import proto "github.com/golang/protobuf/proto"
|
||||
import math "math"
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = math.Inf
|
||||
|
||||
type VolumeInformationMessage struct {
|
||||
Id *uint32 `protobuf:"varint,1,req,name=id" json:"id,omitempty"`
|
||||
Size *uint64 `protobuf:"varint,2,req,name=size" json:"size,omitempty"`
|
||||
Collection *string `protobuf:"bytes,3,opt,name=collection" json:"collection,omitempty"`
|
||||
FileCount *uint64 `protobuf:"varint,4,req,name=file_count" json:"file_count,omitempty"`
|
||||
DeleteCount *uint64 `protobuf:"varint,5,req,name=delete_count" json:"delete_count,omitempty"`
|
||||
DeletedByteCount *uint64 `protobuf:"varint,6,req,name=deleted_byte_count" json:"deleted_byte_count,omitempty"`
|
||||
ReadOnly *bool `protobuf:"varint,7,opt,name=read_only" json:"read_only,omitempty"`
|
||||
ReplicaPlacement *uint32 `protobuf:"varint,8,req,name=replica_placement" json:"replica_placement,omitempty"`
|
||||
Version *uint32 `protobuf:"varint,9,opt,name=version,def=2" json:"version,omitempty"`
|
||||
Ttl *uint32 `protobuf:"varint,10,opt,name=ttl" json:"ttl,omitempty"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
}
|
||||
|
||||
func (m *VolumeInformationMessage) Reset() { *m = VolumeInformationMessage{} }
|
||||
func (m *VolumeInformationMessage) String() string { return proto.CompactTextString(m) }
|
||||
func (*VolumeInformationMessage) ProtoMessage() {}
|
||||
|
||||
const Default_VolumeInformationMessage_Version uint32 = 2
|
||||
|
||||
func (m *VolumeInformationMessage) GetId() uint32 {
|
||||
if m != nil && m.Id != nil {
|
||||
return *m.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *VolumeInformationMessage) GetSize() uint64 {
|
||||
if m != nil && m.Size != nil {
|
||||
return *m.Size
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *VolumeInformationMessage) GetCollection() string {
|
||||
if m != nil && m.Collection != nil {
|
||||
return *m.Collection
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *VolumeInformationMessage) GetFileCount() uint64 {
|
||||
if m != nil && m.FileCount != nil {
|
||||
return *m.FileCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *VolumeInformationMessage) GetDeleteCount() uint64 {
|
||||
if m != nil && m.DeleteCount != nil {
|
||||
return *m.DeleteCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *VolumeInformationMessage) GetDeletedByteCount() uint64 {
|
||||
if m != nil && m.DeletedByteCount != nil {
|
||||
return *m.DeletedByteCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *VolumeInformationMessage) GetReadOnly() bool {
|
||||
if m != nil && m.ReadOnly != nil {
|
||||
return *m.ReadOnly
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *VolumeInformationMessage) GetReplicaPlacement() uint32 {
|
||||
if m != nil && m.ReplicaPlacement != nil {
|
||||
return *m.ReplicaPlacement
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *VolumeInformationMessage) GetVersion() uint32 {
|
||||
if m != nil && m.Version != nil {
|
||||
return *m.Version
|
||||
}
|
||||
return Default_VolumeInformationMessage_Version
|
||||
}
|
||||
|
||||
func (m *VolumeInformationMessage) GetTtl() uint32 {
|
||||
if m != nil && m.Ttl != nil {
|
||||
return *m.Ttl
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type JoinMessage struct {
|
||||
IsInit *bool `protobuf:"varint,1,opt,name=is_init" json:"is_init,omitempty"`
|
||||
Ip *string `protobuf:"bytes,2,req,name=ip" json:"ip,omitempty"`
|
||||
Port *uint32 `protobuf:"varint,3,req,name=port" json:"port,omitempty"`
|
||||
PublicUrl *string `protobuf:"bytes,4,opt,name=public_url" json:"public_url,omitempty"`
|
||||
MaxVolumeCount *uint32 `protobuf:"varint,5,req,name=max_volume_count" json:"max_volume_count,omitempty"`
|
||||
MaxFileKey *uint64 `protobuf:"varint,6,req,name=max_file_key" json:"max_file_key,omitempty"`
|
||||
DataCenter *string `protobuf:"bytes,7,opt,name=data_center" json:"data_center,omitempty"`
|
||||
Rack *string `protobuf:"bytes,8,opt,name=rack" json:"rack,omitempty"`
|
||||
Volumes []*VolumeInformationMessage `protobuf:"bytes,9,rep,name=volumes" json:"volumes,omitempty"`
|
||||
AdminPort *uint32 `protobuf:"varint,10,opt,name=admin_port" json:"admin_port,omitempty"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
}
|
||||
|
||||
func (m *JoinMessage) Reset() { *m = JoinMessage{} }
|
||||
func (m *JoinMessage) String() string { return proto.CompactTextString(m) }
|
||||
func (*JoinMessage) ProtoMessage() {}
|
||||
|
||||
func (m *JoinMessage) GetIsInit() bool {
|
||||
if m != nil && m.IsInit != nil {
|
||||
return *m.IsInit
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *JoinMessage) GetIp() string {
|
||||
if m != nil && m.Ip != nil {
|
||||
return *m.Ip
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *JoinMessage) GetPort() uint32 {
|
||||
if m != nil && m.Port != nil {
|
||||
return *m.Port
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *JoinMessage) GetPublicUrl() string {
|
||||
if m != nil && m.PublicUrl != nil {
|
||||
return *m.PublicUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *JoinMessage) GetMaxVolumeCount() uint32 {
|
||||
if m != nil && m.MaxVolumeCount != nil {
|
||||
return *m.MaxVolumeCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *JoinMessage) GetMaxFileKey() uint64 {
|
||||
if m != nil && m.MaxFileKey != nil {
|
||||
return *m.MaxFileKey
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *JoinMessage) GetDataCenter() string {
|
||||
if m != nil && m.DataCenter != nil {
|
||||
return *m.DataCenter
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *JoinMessage) GetRack() string {
|
||||
if m != nil && m.Rack != nil {
|
||||
return *m.Rack
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *JoinMessage) GetVolumes() []*VolumeInformationMessage {
|
||||
if m != nil {
|
||||
return m.Volumes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *JoinMessage) GetAdminPort() uint32 {
|
||||
if m != nil && m.AdminPort != nil {
|
||||
return *m.AdminPort
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func init() {
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package operation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestSerialDeserial(t *testing.T) {
|
||||
volumeMessage := &VolumeInformationMessage{
|
||||
Id: proto.Uint32(12),
|
||||
Size: proto.Uint64(2341234),
|
||||
Collection: proto.String("benchmark"),
|
||||
FileCount: proto.Uint64(2341234),
|
||||
DeleteCount: proto.Uint64(234),
|
||||
DeletedByteCount: proto.Uint64(21234),
|
||||
ReadOnly: proto.Bool(false),
|
||||
ReplicaPlacement: proto.Uint32(210),
|
||||
Version: proto.Uint32(2),
|
||||
}
|
||||
var volumeMessages []*VolumeInformationMessage
|
||||
volumeMessages = append(volumeMessages, volumeMessage)
|
||||
|
||||
joinMessage := &JoinMessage{
|
||||
IsInit: proto.Bool(true),
|
||||
Ip: proto.String("127.0.3.12"),
|
||||
Port: proto.Uint32(34546),
|
||||
PublicUrl: proto.String("localhost:2342"),
|
||||
MaxVolumeCount: proto.Uint32(210),
|
||||
MaxFileKey: proto.Uint64(324234423),
|
||||
DataCenter: proto.String("dc1"),
|
||||
Rack: proto.String("rack2"),
|
||||
Volumes: volumeMessages,
|
||||
}
|
||||
|
||||
data, err := proto.Marshal(joinMessage)
|
||||
if err != nil {
|
||||
log.Fatal("marshaling error: ", err)
|
||||
}
|
||||
newMessage := &JoinMessage{}
|
||||
err = proto.Unmarshal(data, newMessage)
|
||||
if err != nil {
|
||||
log.Fatal("unmarshaling error: ", err)
|
||||
}
|
||||
log.Println("The pb data size is", len(data))
|
||||
|
||||
jsonData, jsonError := json.Marshal(joinMessage)
|
||||
if jsonError != nil {
|
||||
log.Fatal("json marshaling error: ", jsonError)
|
||||
}
|
||||
log.Println("The json data size is", len(jsonData), string(jsonData))
|
||||
|
||||
// Now test and newTest contain the same data.
|
||||
if *joinMessage.PublicUrl != *newMessage.PublicUrl {
|
||||
log.Fatalf("data mismatch %q != %q", *joinMessage.PublicUrl, *newMessage.PublicUrl)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
TARG=../operation
|
||||
|
||||
all:
|
||||
protoc --go_out=$(TARG) system_message.proto
|
||||
@@ -1,27 +0,0 @@
|
||||
package operation;
|
||||
|
||||
message VolumeInformationMessage {
|
||||
required uint32 id = 1;
|
||||
required uint64 size = 2;
|
||||
optional string collection = 3;
|
||||
required uint64 file_count = 4;
|
||||
required uint64 delete_count = 5;
|
||||
required uint64 deleted_byte_count = 6;
|
||||
optional bool read_only = 7;
|
||||
required uint32 replica_placement = 8;
|
||||
optional uint32 version = 9 [default=2];
|
||||
optional uint32 ttl = 10;
|
||||
}
|
||||
|
||||
message JoinMessage {
|
||||
optional bool is_init = 1;
|
||||
required string ip = 2;
|
||||
required uint32 port = 3;
|
||||
optional string public_url = 4;
|
||||
required uint32 max_volume_count = 5;
|
||||
required uint64 max_file_key = 6;
|
||||
optional string data_center = 7;
|
||||
optional string rack = 8;
|
||||
repeated VolumeInformationMessage volumes = 9;
|
||||
optional uint32 admin_port = 10;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package stats
|
||||
|
||||
type DiskStatus struct {
|
||||
Dir string
|
||||
All uint64
|
||||
Used uint64
|
||||
Free uint64
|
||||
}
|
||||
|
||||
func NewDiskStatus(path string) (disk *DiskStatus) {
|
||||
disk = &DiskStatus{Dir: path}
|
||||
disk.fillInStatus()
|
||||
return
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// +build windows openbsd netbsd plan9 solaris
|
||||
|
||||
package stats
|
||||
|
||||
func (disk *DiskStatus) fillInStatus() {
|
||||
return
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type MemStatus struct {
|
||||
Goroutines int
|
||||
All uint64
|
||||
Used uint64
|
||||
Free uint64
|
||||
Self uint64
|
||||
Heap uint64
|
||||
Stack uint64
|
||||
}
|
||||
|
||||
func MemStat() MemStatus {
|
||||
mem := MemStatus{}
|
||||
mem.Goroutines = runtime.NumGoroutine()
|
||||
memStat := new(runtime.MemStats)
|
||||
runtime.ReadMemStats(memStat)
|
||||
mem.Self = memStat.Alloc
|
||||
mem.Heap = memStat.HeapAlloc
|
||||
mem.Stack = memStat.StackInuse
|
||||
|
||||
mem.fillInStatus()
|
||||
return mem
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// +build !linux
|
||||
|
||||
package stats
|
||||
|
||||
func (mem *MemStatus) fillInStatus() {
|
||||
return
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package storage
|
||||
|
||||
type NeedleValue struct {
|
||||
Key Key
|
||||
Offset uint32 `comment:"Volume offset"` //since aligned to 8 bytes, range is 4G*8=32G
|
||||
Size uint32 `comment:"Size of the data portion"`
|
||||
}
|
||||
|
||||
const (
|
||||
batch = 100000
|
||||
)
|
||||
|
||||
type Key uint64
|
||||
|
||||
type CompactSection struct {
|
||||
values []NeedleValue
|
||||
overflow map[Key]NeedleValue
|
||||
start Key
|
||||
end Key
|
||||
counter int
|
||||
}
|
||||
|
||||
func NewCompactSection(start Key) *CompactSection {
|
||||
return &CompactSection{
|
||||
values: make([]NeedleValue, batch),
|
||||
overflow: make(map[Key]NeedleValue),
|
||||
start: start,
|
||||
}
|
||||
}
|
||||
|
||||
//return old entry size
|
||||
func (cs *CompactSection) Set(key Key, offset uint32, size uint32) uint32 {
|
||||
ret := uint32(0)
|
||||
if key > cs.end {
|
||||
cs.end = key
|
||||
}
|
||||
if i := cs.binarySearchValues(key); i >= 0 {
|
||||
ret = cs.values[i].Size
|
||||
//println("key", key, "old size", ret)
|
||||
cs.values[i].Offset, cs.values[i].Size = offset, size
|
||||
} else {
|
||||
needOverflow := cs.counter >= batch
|
||||
needOverflow = needOverflow || cs.counter > 0 && cs.values[cs.counter-1].Key > key
|
||||
if needOverflow {
|
||||
//println("start", cs.start, "counter", cs.counter, "key", key)
|
||||
if oldValue, found := cs.overflow[key]; found {
|
||||
ret = oldValue.Size
|
||||
}
|
||||
cs.overflow[key] = NeedleValue{Key: key, Offset: offset, Size: size}
|
||||
} else {
|
||||
p := &cs.values[cs.counter]
|
||||
p.Key, p.Offset, p.Size = key, offset, size
|
||||
//println("added index", cs.counter, "key", key, cs.values[cs.counter].Key)
|
||||
cs.counter++
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
//return old entry size
|
||||
func (cs *CompactSection) Delete(key Key) uint32 {
|
||||
ret := uint32(0)
|
||||
if i := cs.binarySearchValues(key); i >= 0 {
|
||||
if cs.values[i].Size > 0 {
|
||||
ret = cs.values[i].Size
|
||||
cs.values[i].Size = 0
|
||||
}
|
||||
}
|
||||
if v, found := cs.overflow[key]; found {
|
||||
delete(cs.overflow, key)
|
||||
ret = v.Size
|
||||
}
|
||||
return ret
|
||||
}
|
||||
func (cs *CompactSection) Get(key Key) (*NeedleValue, bool) {
|
||||
if v, ok := cs.overflow[key]; ok {
|
||||
return &v, true
|
||||
}
|
||||
if i := cs.binarySearchValues(key); i >= 0 {
|
||||
return &cs.values[i], true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
func (cs *CompactSection) binarySearchValues(key Key) int {
|
||||
l, h := 0, cs.counter-1
|
||||
if h >= 0 && cs.values[h].Key < key {
|
||||
return -2
|
||||
}
|
||||
//println("looking for key", key)
|
||||
for l <= h {
|
||||
m := (l + h) / 2
|
||||
//println("mid", m, "key", cs.values[m].Key, cs.values[m].Offset, cs.values[m].Size)
|
||||
if cs.values[m].Key < key {
|
||||
l = m + 1
|
||||
} else if key < cs.values[m].Key {
|
||||
h = m - 1
|
||||
} else {
|
||||
//println("found", m)
|
||||
return m
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
//This map assumes mostly inserting increasing keys
|
||||
type CompactMap struct {
|
||||
list []*CompactSection
|
||||
}
|
||||
|
||||
func NewCompactMap() CompactMap {
|
||||
return CompactMap{}
|
||||
}
|
||||
|
||||
func (cm *CompactMap) Set(key Key, offset uint32, size uint32) uint32 {
|
||||
x := cm.binarySearchCompactSection(key)
|
||||
if x < 0 {
|
||||
//println(x, "creating", len(cm.list), "section, starting", key)
|
||||
cm.list = append(cm.list, NewCompactSection(key))
|
||||
x = len(cm.list) - 1
|
||||
//keep compact section sorted by start
|
||||
for x > 0 {
|
||||
if cm.list[x-1].start > cm.list[x].start {
|
||||
cm.list[x-1], cm.list[x] = cm.list[x], cm.list[x-1]
|
||||
x = x - 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return cm.list[x].Set(key, offset, size)
|
||||
}
|
||||
func (cm *CompactMap) Delete(key Key) uint32 {
|
||||
x := cm.binarySearchCompactSection(key)
|
||||
if x < 0 {
|
||||
return uint32(0)
|
||||
}
|
||||
return cm.list[x].Delete(key)
|
||||
}
|
||||
func (cm *CompactMap) Get(key Key) (*NeedleValue, bool) {
|
||||
x := cm.binarySearchCompactSection(key)
|
||||
if x < 0 {
|
||||
return nil, false
|
||||
}
|
||||
return cm.list[x].Get(key)
|
||||
}
|
||||
func (cm *CompactMap) binarySearchCompactSection(key Key) int {
|
||||
l, h := 0, len(cm.list)-1
|
||||
if h < 0 {
|
||||
return -5
|
||||
}
|
||||
if cm.list[h].start <= key {
|
||||
if cm.list[h].counter < batch || key <= cm.list[h].end {
|
||||
return h
|
||||
}
|
||||
return -4
|
||||
}
|
||||
for l <= h {
|
||||
m := (l + h) / 2
|
||||
if key < cm.list[m].start {
|
||||
h = m - 1
|
||||
} else { // cm.list[m].start <= key
|
||||
if cm.list[m+1].start <= key {
|
||||
l = m + 1
|
||||
} else {
|
||||
return m
|
||||
}
|
||||
}
|
||||
}
|
||||
return -3
|
||||
}
|
||||
|
||||
func (cm *CompactMap) Visit(visit func(NeedleValue) error) error {
|
||||
for _, cs := range cm.list {
|
||||
for _, v := range cs.overflow {
|
||||
if err := visit(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, v := range cs.values {
|
||||
if _, found := cs.overflow[v.Key]; !found {
|
||||
if err := visit(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
)
|
||||
|
||||
func TestMemoryUsage(t *testing.T) {
|
||||
|
||||
indexFile, ie := os.OpenFile("../../test/sample.idx", os.O_RDWR|os.O_RDONLY, 0644)
|
||||
if ie != nil {
|
||||
log.Fatalln(ie)
|
||||
}
|
||||
LoadNewNeedleMap(indexFile)
|
||||
|
||||
}
|
||||
|
||||
func LoadNewNeedleMap(file *os.File) CompactMap {
|
||||
m := NewCompactMap()
|
||||
bytes := make([]byte, 16*1024)
|
||||
count, e := file.Read(bytes)
|
||||
if count > 0 {
|
||||
fstat, _ := file.Stat()
|
||||
glog.V(0).Infoln("Loading index file", fstat.Name(), "size", fstat.Size())
|
||||
}
|
||||
for count > 0 && e == nil {
|
||||
for i := 0; i < count; i += 16 {
|
||||
key := util.BytesToUint64(bytes[i : i+8])
|
||||
offset := util.BytesToUint32(bytes[i+8 : i+12])
|
||||
size := util.BytesToUint32(bytes[i+12 : i+16])
|
||||
if offset > 0 {
|
||||
m.Set(Key(key), offset, size)
|
||||
} else {
|
||||
//delete(m, key)
|
||||
}
|
||||
}
|
||||
|
||||
count, e = file.Read(bytes)
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIssue52(t *testing.T) {
|
||||
m := NewCompactMap()
|
||||
m.Set(Key(10002), 10002, 10002)
|
||||
if element, ok := m.Get(Key(10002)); ok {
|
||||
println("key", 10002, "ok", ok, element.Key, element.Offset, element.Size)
|
||||
}
|
||||
m.Set(Key(10001), 10001, 10001)
|
||||
if element, ok := m.Get(Key(10002)); ok {
|
||||
println("key", 10002, "ok", ok, element.Key, element.Offset, element.Size)
|
||||
} else {
|
||||
t.Fatal("key 10002 missing after setting 10001")
|
||||
}
|
||||
}
|
||||
|
||||
func TestXYZ(t *testing.T) {
|
||||
m := NewCompactMap()
|
||||
for i := uint32(0); i < 100*batch; i += 2 {
|
||||
m.Set(Key(i), i, i)
|
||||
}
|
||||
|
||||
for i := uint32(0); i < 100*batch; i += 37 {
|
||||
m.Delete(Key(i))
|
||||
}
|
||||
|
||||
for i := uint32(0); i < 10*batch; i += 3 {
|
||||
m.Set(Key(i), i+11, i+5)
|
||||
}
|
||||
|
||||
// for i := uint32(0); i < 100; i++ {
|
||||
// if v := m.Get(Key(i)); v != nil {
|
||||
// glog.V(4).Infoln(i, "=", v.Key, v.Offset, v.Size)
|
||||
// }
|
||||
// }
|
||||
|
||||
for i := uint32(0); i < 10*batch; i++ {
|
||||
v, ok := m.Get(Key(i))
|
||||
if i%3 == 0 {
|
||||
if !ok {
|
||||
t.Fatal("key", i, "missing!")
|
||||
}
|
||||
if v.Size != i+5 {
|
||||
t.Fatal("key", i, "size", v.Size)
|
||||
}
|
||||
} else if i%37 == 0 {
|
||||
if ok && v.Size > 0 {
|
||||
t.Fatal("key", i, "should have been deleted needle value", v)
|
||||
}
|
||||
} else if i%2 == 0 {
|
||||
if v.Size != i {
|
||||
t.Fatal("key", i, "size", v.Size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := uint32(10 * batch); i < 100*batch; i++ {
|
||||
v, ok := m.Get(Key(i))
|
||||
if i%37 == 0 {
|
||||
if ok && v.Size > 0 {
|
||||
t.Fatal("key", i, "should have been deleted needle value", v)
|
||||
}
|
||||
} else if i%2 == 0 {
|
||||
if v == nil {
|
||||
t.Fatal("key", i, "missing")
|
||||
}
|
||||
if v.Size != i {
|
||||
t.Fatal("key", i, "size", v.Size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
)
|
||||
|
||||
type FileId struct {
|
||||
VolumeId VolumeId
|
||||
Key uint64
|
||||
Hashcode uint32
|
||||
}
|
||||
|
||||
func NewFileIdFromNeedle(VolumeId VolumeId, n *Needle) *FileId {
|
||||
return &FileId{VolumeId: VolumeId, Key: n.Id, Hashcode: n.Cookie}
|
||||
}
|
||||
func NewFileId(VolumeId VolumeId, Key uint64, Hashcode uint32) *FileId {
|
||||
return &FileId{VolumeId: VolumeId, Key: Key, Hashcode: Hashcode}
|
||||
}
|
||||
func ParseFileId(fid string) (*FileId, error) {
|
||||
a := strings.Split(fid, ",")
|
||||
if len(a) != 2 {
|
||||
glog.V(1).Infoln("Invalid fid ", fid, ", split length ", len(a))
|
||||
return nil, errors.New("Invalid fid " + fid)
|
||||
}
|
||||
vid_string, key_hash_string := a[0], a[1]
|
||||
volumeId, _ := NewVolumeId(vid_string)
|
||||
key, hash, e := ParseKeyHash(key_hash_string)
|
||||
return &FileId{VolumeId: volumeId, Key: key, Hashcode: hash}, e
|
||||
}
|
||||
func (n *FileId) String() string {
|
||||
bytes := make([]byte, 12)
|
||||
util.Uint64toBytes(bytes[0:8], n.Key)
|
||||
util.Uint32toBytes(bytes[8:12], n.Hashcode)
|
||||
nonzero_index := 0
|
||||
for ; bytes[nonzero_index] == 0; nonzero_index++ {
|
||||
}
|
||||
return n.VolumeId.String() + "," + hex.EncodeToString(bytes[nonzero_index:])
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/images"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
)
|
||||
|
||||
const (
|
||||
NeedleHeaderSize = 16 //should never change this
|
||||
NeedlePaddingSize = 8
|
||||
NeedleChecksumSize = 4
|
||||
MaxPossibleVolumeSize = 4 * 1024 * 1024 * 1024 * 8
|
||||
)
|
||||
|
||||
/*
|
||||
* A Needle means a uploaded and stored file.
|
||||
* Needle file size is limited to 4GB for now.
|
||||
*/
|
||||
type Needle struct {
|
||||
Cookie uint32 `comment:"random number to mitigate brute force lookups"`
|
||||
Id uint64 `comment:"needle id"`
|
||||
Size uint32 `comment:"sum of DataSize,Data,NameSize,Name,MimeSize,Mime"`
|
||||
|
||||
DataSize uint32 `comment:"Data size"` //version2
|
||||
Data []byte `comment:"The actual file data"`
|
||||
Flags byte `comment:"boolean flags"` //version2
|
||||
NameSize uint8 //version2
|
||||
Name []byte `comment:"maximum 256 characters"` //version2
|
||||
MimeSize uint8 //version2
|
||||
Mime []byte `comment:"maximum 256 characters"` //version2
|
||||
LastModified uint64 //only store LastModifiedBytesLength bytes, which is 5 bytes to disk
|
||||
Ttl *TTL
|
||||
|
||||
Checksum CRC `comment:"CRC32 to check integrity"`
|
||||
Padding []byte `comment:"Aligned to 8 bytes"`
|
||||
}
|
||||
|
||||
func (n *Needle) String() (str string) {
|
||||
str = fmt.Sprintf("Cookie:%d, Id:%d, Size:%d, DataSize:%d, Name: %s, Mime: %s", n.Cookie, n.Id, n.Size, n.DataSize, n.Name, n.Mime)
|
||||
return
|
||||
}
|
||||
|
||||
func ParseUpload(r *http.Request) (fileName string, data []byte, mimeType string, isGzipped bool, modifiedTime uint64, ttl *TTL, e error) {
|
||||
form, fe := r.MultipartReader()
|
||||
if fe != nil {
|
||||
glog.V(0).Infoln("MultipartReader [ERROR]", fe)
|
||||
e = fe
|
||||
return
|
||||
}
|
||||
|
||||
//first multi-part item
|
||||
part, fe := form.NextPart()
|
||||
if fe != nil {
|
||||
glog.V(0).Infoln("Reading Multi part [ERROR]", fe)
|
||||
e = fe
|
||||
return
|
||||
}
|
||||
|
||||
fileName = part.FileName()
|
||||
if fileName != "" {
|
||||
fileName = path.Base(fileName)
|
||||
}
|
||||
|
||||
data, e = ioutil.ReadAll(part)
|
||||
if e != nil {
|
||||
glog.V(0).Infoln("Reading Content [ERROR]", e)
|
||||
return
|
||||
}
|
||||
|
||||
//if the filename is empty string, do a search on the other multi-part items
|
||||
for fileName == "" {
|
||||
part2, fe := form.NextPart()
|
||||
if fe != nil {
|
||||
break // no more or on error, just safely break
|
||||
}
|
||||
|
||||
fName := part2.FileName()
|
||||
|
||||
//found the first <file type> multi-part has filename
|
||||
if fName != "" {
|
||||
data2, fe2 := ioutil.ReadAll(part2)
|
||||
if fe2 != nil {
|
||||
glog.V(0).Infoln("Reading Content [ERROR]", fe2)
|
||||
e = fe2
|
||||
return
|
||||
}
|
||||
|
||||
//update
|
||||
data = data2
|
||||
fileName = path.Base(fName)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
dotIndex := strings.LastIndex(fileName, ".")
|
||||
ext, mtype := "", ""
|
||||
if dotIndex > 0 {
|
||||
ext = strings.ToLower(fileName[dotIndex:])
|
||||
mtype = mime.TypeByExtension(ext)
|
||||
}
|
||||
contentType := part.Header.Get("Content-Type")
|
||||
if contentType != "" && mtype != contentType {
|
||||
mimeType = contentType //only return mime type if not deductable
|
||||
mtype = contentType
|
||||
}
|
||||
if part.Header.Get("Content-Encoding") == "gzip" {
|
||||
isGzipped = true
|
||||
} else if IsGzippable(ext, mtype) {
|
||||
if data, e = GzipData(data); e != nil {
|
||||
return
|
||||
}
|
||||
isGzipped = true
|
||||
}
|
||||
if ext == ".gz" {
|
||||
isGzipped = true
|
||||
}
|
||||
if strings.HasSuffix(fileName, ".gz") &&
|
||||
!strings.HasSuffix(fileName, ".tar.gz") {
|
||||
fileName = fileName[:len(fileName)-3]
|
||||
}
|
||||
modifiedTime, _ = strconv.ParseUint(r.FormValue("ts"), 10, 64)
|
||||
ttl, _ = ReadTTL(r.FormValue("ttl"))
|
||||
return
|
||||
}
|
||||
func NewNeedle(r *http.Request, fixJpgOrientation bool) (n *Needle, e error) {
|
||||
fname, mimeType, isGzipped := "", "", false
|
||||
n = new(Needle)
|
||||
fname, n.Data, mimeType, isGzipped, n.LastModified, n.Ttl, e = ParseUpload(r)
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
if len(fname) < 256 {
|
||||
n.Name = []byte(fname)
|
||||
n.SetHasName()
|
||||
}
|
||||
if len(mimeType) < 256 {
|
||||
n.Mime = []byte(mimeType)
|
||||
n.SetHasMime()
|
||||
}
|
||||
if isGzipped {
|
||||
n.SetGzipped()
|
||||
}
|
||||
if n.LastModified == 0 {
|
||||
n.LastModified = uint64(time.Now().Unix())
|
||||
}
|
||||
n.SetHasLastModifiedDate()
|
||||
if n.Ttl != EMPTY_TTL {
|
||||
n.SetHasTtl()
|
||||
}
|
||||
|
||||
if fixJpgOrientation {
|
||||
loweredName := strings.ToLower(fname)
|
||||
if mimeType == "image/jpeg" || strings.HasSuffix(loweredName, ".jpg") || strings.HasSuffix(loweredName, ".jpeg") {
|
||||
n.Data = images.FixJpgOrientation(n.Data)
|
||||
}
|
||||
}
|
||||
|
||||
n.Checksum = NewCRC(n.Data)
|
||||
|
||||
commaSep := strings.LastIndex(r.URL.Path, ",")
|
||||
dotSep := strings.LastIndex(r.URL.Path, ".")
|
||||
fid := r.URL.Path[commaSep+1:]
|
||||
if dotSep > 0 {
|
||||
fid = r.URL.Path[commaSep+1 : dotSep]
|
||||
}
|
||||
|
||||
e = n.ParsePath(fid)
|
||||
|
||||
return
|
||||
}
|
||||
func (n *Needle) ParsePath(fid string) (err error) {
|
||||
length := len(fid)
|
||||
if length <= 8 {
|
||||
return errors.New("Invalid fid:" + fid)
|
||||
}
|
||||
delta := ""
|
||||
deltaIndex := strings.LastIndex(fid, "_")
|
||||
if deltaIndex > 0 {
|
||||
fid, delta = fid[0:deltaIndex], fid[deltaIndex+1:]
|
||||
}
|
||||
n.Id, n.Cookie, err = ParseKeyHash(fid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if delta != "" {
|
||||
if d, e := strconv.ParseUint(delta, 10, 64); e == nil {
|
||||
n.Id += d
|
||||
} else {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ParseKeyHash(key_hash_string string) (uint64, uint32, error) {
|
||||
key_hash_bytes, khe := hex.DecodeString(key_hash_string)
|
||||
key_hash_len := len(key_hash_bytes)
|
||||
if khe != nil || key_hash_len <= 4 {
|
||||
glog.V(0).Infoln("Invalid key_hash", key_hash_string, "length:", key_hash_len, "error", khe)
|
||||
return 0, 0, errors.New("Invalid key and hash:" + key_hash_string)
|
||||
}
|
||||
key := util.BytesToUint64(key_hash_bytes[0 : key_hash_len-4])
|
||||
hash := util.BytesToUint32(key_hash_bytes[key_hash_len-4 : key_hash_len])
|
||||
return key, hash, nil
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
)
|
||||
|
||||
type NeedleMapType int
|
||||
|
||||
const (
|
||||
NeedleMapInMemory NeedleMapType = iota
|
||||
NeedleMapLevelDb
|
||||
NeedleMapBoltDb
|
||||
)
|
||||
|
||||
type NeedleMapper interface {
|
||||
Put(key uint64, offset uint32, size uint32) error
|
||||
Get(key uint64) (element *NeedleValue, ok bool)
|
||||
Delete(key uint64) error
|
||||
Close()
|
||||
Destroy() error
|
||||
ContentSize() uint64
|
||||
DeletedSize() uint64
|
||||
FileCount() int
|
||||
DeletedCount() int
|
||||
MaxFileKey() uint64
|
||||
}
|
||||
|
||||
type mapMetric struct {
|
||||
DeletionCounter int `json:"DeletionCounter"`
|
||||
FileCounter int `json:"FileCounter"`
|
||||
DeletionByteCounter uint64 `json:"DeletionByteCounter"`
|
||||
FileByteCounter uint64 `json:"FileByteCounter"`
|
||||
MaximumFileKey uint64 `json:"MaxFileKey"`
|
||||
}
|
||||
|
||||
func appendToIndexFile(indexFile *os.File,
|
||||
key uint64, offset uint32, size uint32) error {
|
||||
bytes := make([]byte, 16)
|
||||
util.Uint64toBytes(bytes[0:8], key)
|
||||
util.Uint32toBytes(bytes[8:12], offset)
|
||||
util.Uint32toBytes(bytes[12:16], size)
|
||||
if _, err := indexFile.Seek(0, 2); err != nil {
|
||||
return fmt.Errorf("cannot seek end of indexfile %s: %v",
|
||||
indexFile.Name(), err)
|
||||
}
|
||||
_, err := indexFile.Write(bytes)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mm *mapMetric) logDelete(deletedByteCount uint32) {
|
||||
mm.DeletionByteCounter = mm.DeletionByteCounter + uint64(deletedByteCount)
|
||||
mm.DeletionCounter++
|
||||
}
|
||||
|
||||
func (mm *mapMetric) logPut(key uint64, oldSize uint32, newSize uint32) {
|
||||
if key > mm.MaximumFileKey {
|
||||
mm.MaximumFileKey = key
|
||||
}
|
||||
mm.FileCounter++
|
||||
mm.FileByteCounter = mm.FileByteCounter + uint64(newSize)
|
||||
if oldSize > 0 {
|
||||
mm.DeletionCounter++
|
||||
mm.DeletionByteCounter = mm.DeletionByteCounter + uint64(oldSize)
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
)
|
||||
|
||||
type NeedleMap struct {
|
||||
indexFile *os.File
|
||||
m CompactMap
|
||||
|
||||
mapMetric
|
||||
}
|
||||
|
||||
func NewNeedleMap(file *os.File) *NeedleMap {
|
||||
nm := &NeedleMap{
|
||||
m: NewCompactMap(),
|
||||
indexFile: file,
|
||||
}
|
||||
return nm
|
||||
}
|
||||
|
||||
const (
|
||||
RowsToRead = 1024
|
||||
)
|
||||
|
||||
func LoadNeedleMap(file *os.File) (*NeedleMap, error) {
|
||||
nm := NewNeedleMap(file)
|
||||
e := WalkIndexFile(file, func(key uint64, offset, size uint32) error {
|
||||
if key > nm.MaximumFileKey {
|
||||
nm.MaximumFileKey = key
|
||||
}
|
||||
nm.FileCounter++
|
||||
nm.FileByteCounter = nm.FileByteCounter + uint64(size)
|
||||
if offset > 0 {
|
||||
oldSize := nm.m.Set(Key(key), offset, size)
|
||||
glog.V(3).Infoln("reading key", key, "offset", offset*NeedlePaddingSize, "size", size, "oldSize", oldSize)
|
||||
if oldSize > 0 {
|
||||
nm.DeletionCounter++
|
||||
nm.DeletionByteCounter = nm.DeletionByteCounter + uint64(oldSize)
|
||||
}
|
||||
} else {
|
||||
oldSize := nm.m.Delete(Key(key))
|
||||
glog.V(3).Infoln("removing key", key, "offset", offset*NeedlePaddingSize, "size", size, "oldSize", oldSize)
|
||||
nm.DeletionCounter++
|
||||
nm.DeletionByteCounter = nm.DeletionByteCounter + uint64(oldSize)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
glog.V(1).Infoln("max file key:", nm.MaximumFileKey)
|
||||
return nm, e
|
||||
}
|
||||
|
||||
// walks through the index file, calls fn function with each key, offset, size
|
||||
// stops with the error returned by the fn function
|
||||
func WalkIndexFile(r *os.File, fn func(key uint64, offset, size uint32) error) error {
|
||||
var readerOffset int64
|
||||
bytes := make([]byte, 16*RowsToRead)
|
||||
count, e := r.ReadAt(bytes, readerOffset)
|
||||
glog.V(3).Infoln("file", r.Name(), "readerOffset", readerOffset, "count", count, "e", e)
|
||||
readerOffset += int64(count)
|
||||
var (
|
||||
key uint64
|
||||
offset, size uint32
|
||||
i int
|
||||
)
|
||||
|
||||
for count > 0 && e == nil || e == io.EOF {
|
||||
for i = 0; i+16 <= count; i += 16 {
|
||||
key = util.BytesToUint64(bytes[i : i+8])
|
||||
offset = util.BytesToUint32(bytes[i+8 : i+12])
|
||||
size = util.BytesToUint32(bytes[i+12 : i+16])
|
||||
if e = fn(key, offset, size); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
if e == io.EOF {
|
||||
return nil
|
||||
}
|
||||
count, e = r.ReadAt(bytes, readerOffset)
|
||||
glog.V(3).Infoln("file", r.Name(), "readerOffset", readerOffset, "count", count, "e", e)
|
||||
readerOffset += int64(count)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (nm *NeedleMap) Put(key uint64, offset uint32, size uint32) error {
|
||||
oldSize := nm.m.Set(Key(key), offset, size)
|
||||
nm.logPut(key, oldSize, size)
|
||||
return appendToIndexFile(nm.indexFile, key, offset, size)
|
||||
}
|
||||
func (nm *NeedleMap) Get(key uint64) (element *NeedleValue, ok bool) {
|
||||
element, ok = nm.m.Get(Key(key))
|
||||
return
|
||||
}
|
||||
func (nm *NeedleMap) Delete(key uint64) error {
|
||||
deletedBytes := nm.m.Delete(Key(key))
|
||||
nm.logDelete(deletedBytes)
|
||||
return appendToIndexFile(nm.indexFile, key, 0, 0)
|
||||
}
|
||||
func (nm *NeedleMap) Close() {
|
||||
_ = nm.indexFile.Close()
|
||||
}
|
||||
func (nm *NeedleMap) Destroy() error {
|
||||
nm.Close()
|
||||
return os.Remove(nm.indexFile.Name())
|
||||
}
|
||||
func (nm NeedleMap) ContentSize() uint64 {
|
||||
return nm.FileByteCounter
|
||||
}
|
||||
func (nm NeedleMap) DeletedSize() uint64 {
|
||||
return nm.DeletionByteCounter
|
||||
}
|
||||
func (nm NeedleMap) FileCount() int {
|
||||
return nm.FileCounter
|
||||
}
|
||||
func (nm NeedleMap) DeletedCount() int {
|
||||
return nm.DeletionCounter
|
||||
}
|
||||
|
||||
func (nm NeedleMap) MaxFileKey() uint64 {
|
||||
return nm.MaximumFileKey
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/operation"
|
||||
"github.com/chrislusf/weed-fs/go/security"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
"github.com/golang/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
MAX_TTL_VOLUME_REMOVAL_DELAY = 10 // 10 minutes
|
||||
)
|
||||
|
||||
type DiskLocation struct {
|
||||
Directory string
|
||||
MaxVolumeCount int
|
||||
volumes map[VolumeId]*Volume
|
||||
}
|
||||
|
||||
func (mn *DiskLocation) reset() {
|
||||
}
|
||||
|
||||
type MasterNodes struct {
|
||||
nodes []string
|
||||
lastNode int
|
||||
}
|
||||
|
||||
func (mn *MasterNodes) String() string {
|
||||
return fmt.Sprintf("nodes:%v, lastNode:%d", mn.nodes, mn.lastNode)
|
||||
}
|
||||
|
||||
func NewMasterNodes(bootstrapNode string) (mn *MasterNodes) {
|
||||
mn = &MasterNodes{nodes: []string{bootstrapNode}, lastNode: -1}
|
||||
return
|
||||
}
|
||||
func (mn *MasterNodes) reset() {
|
||||
if len(mn.nodes) > 1 && mn.lastNode > 0 {
|
||||
mn.lastNode = -mn.lastNode
|
||||
}
|
||||
}
|
||||
func (mn *MasterNodes) findMaster() (string, error) {
|
||||
if len(mn.nodes) == 0 {
|
||||
return "", errors.New("No master node found!")
|
||||
}
|
||||
if mn.lastNode < 0 {
|
||||
for _, m := range mn.nodes {
|
||||
if masters, e := operation.ListMasters(m); e == nil {
|
||||
if len(masters) == 0 {
|
||||
continue
|
||||
}
|
||||
mn.nodes = masters
|
||||
mn.lastNode = rand.Intn(len(mn.nodes))
|
||||
glog.V(2).Info("current master node is :", mn.nodes[mn.lastNode])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if mn.lastNode < 0 {
|
||||
return "", errors.New("No master node available!")
|
||||
}
|
||||
return mn.nodes[mn.lastNode], nil
|
||||
}
|
||||
|
||||
/*
|
||||
* A VolumeServer contains one Store
|
||||
*/
|
||||
type Store struct {
|
||||
Ip string
|
||||
Port int
|
||||
PublicUrl string
|
||||
Locations []*DiskLocation
|
||||
dataCenter string //optional informaton, overwriting master setting if exists
|
||||
rack string //optional information, overwriting master setting if exists
|
||||
connected bool
|
||||
volumeSizeLimit uint64 //read from the master
|
||||
masterNodes *MasterNodes
|
||||
}
|
||||
|
||||
func (s *Store) String() (str string) {
|
||||
str = fmt.Sprintf("Ip:%s, Port:%d, PublicUrl:%s, dataCenter:%s, rack:%s, connected:%v, volumeSizeLimit:%d, masterNodes:%s", s.Ip, s.Port, s.PublicUrl, s.dataCenter, s.rack, s.connected, s.volumeSizeLimit, s.masterNodes)
|
||||
return
|
||||
}
|
||||
|
||||
func NewStore(port int, ip, publicUrl string, dirnames []string, maxVolumeCounts []int, needleMapKind NeedleMapType) (s *Store) {
|
||||
s = &Store{Port: port, Ip: ip, PublicUrl: publicUrl}
|
||||
s.Locations = make([]*DiskLocation, 0)
|
||||
for i := 0; i < len(dirnames); i++ {
|
||||
location := &DiskLocation{Directory: dirnames[i], MaxVolumeCount: maxVolumeCounts[i]}
|
||||
location.volumes = make(map[VolumeId]*Volume)
|
||||
location.loadExistingVolumes(needleMapKind)
|
||||
s.Locations = append(s.Locations, location)
|
||||
}
|
||||
return
|
||||
}
|
||||
func (s *Store) AddVolume(volumeListString string, collection string, needleMapKind NeedleMapType, replicaPlacement string, ttlString string) error {
|
||||
rt, e := NewReplicaPlacementFromString(replicaPlacement)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
ttl, e := ReadTTL(ttlString)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
for _, range_string := range strings.Split(volumeListString, ",") {
|
||||
if strings.Index(range_string, "-") < 0 {
|
||||
id_string := range_string
|
||||
id, err := NewVolumeId(id_string)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Volume Id %s is not a valid unsigned integer!", id_string)
|
||||
}
|
||||
e = s.addVolume(VolumeId(id), collection, needleMapKind, rt, ttl)
|
||||
} else {
|
||||
pair := strings.Split(range_string, "-")
|
||||
start, start_err := strconv.ParseUint(pair[0], 10, 64)
|
||||
if start_err != nil {
|
||||
return fmt.Errorf("Volume Start Id %s is not a valid unsigned integer!", pair[0])
|
||||
}
|
||||
end, end_err := strconv.ParseUint(pair[1], 10, 64)
|
||||
if end_err != nil {
|
||||
return fmt.Errorf("Volume End Id %s is not a valid unsigned integer!", pair[1])
|
||||
}
|
||||
for id := start; id <= end; id++ {
|
||||
if err := s.addVolume(VolumeId(id), collection, needleMapKind, rt, ttl); err != nil {
|
||||
e = err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return e
|
||||
}
|
||||
func (s *Store) DeleteCollection(collection string) (e error) {
|
||||
for _, location := range s.Locations {
|
||||
for k, v := range location.volumes {
|
||||
if v.Collection == collection {
|
||||
e = v.Destroy()
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
delete(location.volumes, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
func (s *Store) DeleteVolume(volumes map[VolumeId]*Volume, v *Volume) (e error) {
|
||||
e = v.Destroy()
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
delete(volumes, v.Id)
|
||||
return
|
||||
}
|
||||
func (s *Store) findVolume(vid VolumeId) *Volume {
|
||||
for _, location := range s.Locations {
|
||||
if v, found := location.volumes[vid]; found {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *Store) findFreeLocation() (ret *DiskLocation) {
|
||||
max := 0
|
||||
for _, location := range s.Locations {
|
||||
currentFreeCount := location.MaxVolumeCount - len(location.volumes)
|
||||
if currentFreeCount > max {
|
||||
max = currentFreeCount
|
||||
ret = location
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
func (s *Store) addVolume(vid VolumeId, collection string, needleMapKind NeedleMapType, replicaPlacement *ReplicaPlacement, ttl *TTL) error {
|
||||
if s.findVolume(vid) != nil {
|
||||
return fmt.Errorf("Volume Id %d already exists!", vid)
|
||||
}
|
||||
if location := s.findFreeLocation(); location != nil {
|
||||
glog.V(0).Infof("In dir %s adds volume:%v collection:%s replicaPlacement:%v ttl:%v",
|
||||
location.Directory, vid, collection, replicaPlacement, ttl)
|
||||
if volume, err := NewVolume(location.Directory, collection, vid, needleMapKind, replicaPlacement, ttl); err == nil {
|
||||
location.volumes[vid] = volume
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("No more free space left")
|
||||
}
|
||||
|
||||
func (l *DiskLocation) loadExistingVolumes(needleMapKind NeedleMapType) {
|
||||
if dirs, err := ioutil.ReadDir(l.Directory); err == nil {
|
||||
for _, dir := range dirs {
|
||||
name := dir.Name()
|
||||
if !dir.IsDir() && strings.HasSuffix(name, ".dat") {
|
||||
collection := ""
|
||||
base := name[:len(name)-len(".dat")]
|
||||
i := strings.Index(base, "_")
|
||||
if i > 0 {
|
||||
collection, base = base[0:i], base[i+1:]
|
||||
}
|
||||
if vid, err := NewVolumeId(base); err == nil {
|
||||
if l.volumes[vid] == nil {
|
||||
if v, e := NewVolume(l.Directory, collection, vid, needleMapKind, nil, nil); e == nil {
|
||||
l.volumes[vid] = v
|
||||
glog.V(0).Infof("data file %s, replicaPlacement=%s v=%d size=%d ttl=%s", l.Directory+"/"+name, v.ReplicaPlacement, v.Version(), v.Size(), v.Ttl.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
glog.V(0).Infoln("Store started on dir:", l.Directory, "with", len(l.volumes), "volumes", "max", l.MaxVolumeCount)
|
||||
}
|
||||
func (s *Store) Status() []*VolumeInfo {
|
||||
var stats []*VolumeInfo
|
||||
for _, location := range s.Locations {
|
||||
for k, v := range location.volumes {
|
||||
s := &VolumeInfo{Id: VolumeId(k), Size: v.ContentSize(),
|
||||
Collection: v.Collection,
|
||||
ReplicaPlacement: v.ReplicaPlacement,
|
||||
Version: v.Version(),
|
||||
FileCount: v.nm.FileCount(),
|
||||
DeleteCount: v.nm.DeletedCount(),
|
||||
DeletedByteCount: v.nm.DeletedSize(),
|
||||
ReadOnly: v.readOnly,
|
||||
Ttl: v.Ttl}
|
||||
stats = append(stats, s)
|
||||
}
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
func (s *Store) SetDataCenter(dataCenter string) {
|
||||
s.dataCenter = dataCenter
|
||||
}
|
||||
func (s *Store) SetRack(rack string) {
|
||||
s.rack = rack
|
||||
}
|
||||
|
||||
func (s *Store) SetBootstrapMaster(bootstrapMaster string) {
|
||||
s.masterNodes = NewMasterNodes(bootstrapMaster)
|
||||
}
|
||||
func (s *Store) SendHeartbeatToMaster() (masterNode string, secretKey security.Secret, e error) {
|
||||
masterNode, e = s.masterNodes.findMaster()
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
var volumeMessages []*operation.VolumeInformationMessage
|
||||
maxVolumeCount := 0
|
||||
var maxFileKey uint64
|
||||
for _, location := range s.Locations {
|
||||
maxVolumeCount = maxVolumeCount + location.MaxVolumeCount
|
||||
for k, v := range location.volumes {
|
||||
if maxFileKey < v.nm.MaxFileKey() {
|
||||
maxFileKey = v.nm.MaxFileKey()
|
||||
}
|
||||
if !v.expired(s.volumeSizeLimit) {
|
||||
volumeMessage := &operation.VolumeInformationMessage{
|
||||
Id: proto.Uint32(uint32(k)),
|
||||
Size: proto.Uint64(uint64(v.Size())),
|
||||
Collection: proto.String(v.Collection),
|
||||
FileCount: proto.Uint64(uint64(v.nm.FileCount())),
|
||||
DeleteCount: proto.Uint64(uint64(v.nm.DeletedCount())),
|
||||
DeletedByteCount: proto.Uint64(v.nm.DeletedSize()),
|
||||
ReadOnly: proto.Bool(v.readOnly),
|
||||
ReplicaPlacement: proto.Uint32(uint32(v.ReplicaPlacement.Byte())),
|
||||
Version: proto.Uint32(uint32(v.Version())),
|
||||
Ttl: proto.Uint32(v.Ttl.ToUint32()),
|
||||
}
|
||||
volumeMessages = append(volumeMessages, volumeMessage)
|
||||
} else {
|
||||
if v.exiredLongEnough(MAX_TTL_VOLUME_REMOVAL_DELAY) {
|
||||
s.DeleteVolume(location.volumes, v)
|
||||
glog.V(0).Infoln("volume", v.Id, "is deleted.")
|
||||
} else {
|
||||
glog.V(0).Infoln("volume", v.Id, "is expired.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
joinMessage := &operation.JoinMessage{
|
||||
IsInit: proto.Bool(!s.connected),
|
||||
Ip: proto.String(s.Ip),
|
||||
Port: proto.Uint32(uint32(s.Port)),
|
||||
PublicUrl: proto.String(s.PublicUrl),
|
||||
MaxVolumeCount: proto.Uint32(uint32(maxVolumeCount)),
|
||||
MaxFileKey: proto.Uint64(maxFileKey),
|
||||
DataCenter: proto.String(s.dataCenter),
|
||||
Rack: proto.String(s.rack),
|
||||
Volumes: volumeMessages,
|
||||
}
|
||||
|
||||
data, err := proto.Marshal(joinMessage)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
joinUrl := "http://" + masterNode + "/dir/join"
|
||||
|
||||
jsonBlob, err := util.PostBytes(joinUrl, data)
|
||||
if err != nil {
|
||||
s.masterNodes.reset()
|
||||
return "", "", err
|
||||
}
|
||||
var ret operation.JoinResult
|
||||
if err := json.Unmarshal(jsonBlob, &ret); err != nil {
|
||||
glog.V(0).Infof("Failed to join %s with response: %s", joinUrl, string(jsonBlob))
|
||||
return masterNode, "", err
|
||||
}
|
||||
if ret.Error != "" {
|
||||
return masterNode, "", errors.New(ret.Error)
|
||||
}
|
||||
s.volumeSizeLimit = ret.VolumeSizeLimit
|
||||
secretKey = security.Secret(ret.SecretKey)
|
||||
s.connected = true
|
||||
return
|
||||
}
|
||||
func (s *Store) Close() {
|
||||
for _, location := range s.Locations {
|
||||
for _, v := range location.volumes {
|
||||
v.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s *Store) Write(i VolumeId, n *Needle) (size uint32, err error) {
|
||||
if v := s.findVolume(i); v != nil {
|
||||
if v.readOnly {
|
||||
err = fmt.Errorf("Volume %d is read only", i)
|
||||
return
|
||||
}
|
||||
if MaxPossibleVolumeSize >= v.ContentSize()+uint64(size) {
|
||||
size, err = v.write(n)
|
||||
} else {
|
||||
err = fmt.Errorf("Volume Size Limit %d Exceeded! Current size is %d", s.volumeSizeLimit, v.ContentSize())
|
||||
}
|
||||
if s.volumeSizeLimit < v.ContentSize()+3*uint64(size) {
|
||||
glog.V(0).Infoln("volume", i, "size", v.ContentSize(), "will exceed limit", s.volumeSizeLimit)
|
||||
if _, _, e := s.SendHeartbeatToMaster(); e != nil {
|
||||
glog.V(0).Infoln("error when reporting size:", e)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
glog.V(0).Infoln("volume", i, "not found!")
|
||||
err = fmt.Errorf("Volume %d not found!", i)
|
||||
return
|
||||
}
|
||||
func (s *Store) Delete(i VolumeId, n *Needle) (uint32, error) {
|
||||
if v := s.findVolume(i); v != nil && !v.readOnly {
|
||||
return v.delete(n)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
func (s *Store) Read(i VolumeId, n *Needle) (int, error) {
|
||||
if v := s.findVolume(i); v != nil {
|
||||
return v.read(n)
|
||||
}
|
||||
return 0, fmt.Errorf("Volume %v not found!", i)
|
||||
}
|
||||
func (s *Store) GetVolume(i VolumeId) *Volume {
|
||||
return s.findVolume(i)
|
||||
}
|
||||
|
||||
func (s *Store) HasVolume(i VolumeId) bool {
|
||||
v := s.findVolume(i)
|
||||
return v != nil
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
)
|
||||
|
||||
func (s *Store) CheckCompactVolume(volumeIdString string, garbageThresholdString string) (error, bool) {
|
||||
vid, err := NewVolumeId(volumeIdString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Volume Id %s is not a valid unsigned integer", volumeIdString), false
|
||||
}
|
||||
garbageThreshold, e := strconv.ParseFloat(garbageThresholdString, 32)
|
||||
if e != nil {
|
||||
return fmt.Errorf("garbageThreshold %s is not a valid float number", garbageThresholdString), false
|
||||
}
|
||||
if v := s.findVolume(vid); v != nil {
|
||||
glog.V(3).Infoln(vid, "garbage level is", v.garbageLevel())
|
||||
return nil, garbageThreshold < v.garbageLevel()
|
||||
}
|
||||
return fmt.Errorf("volume id %d is not found during check compact", vid), false
|
||||
}
|
||||
func (s *Store) CompactVolume(volumeIdString string) error {
|
||||
vid, err := NewVolumeId(volumeIdString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Volume Id %s is not a valid unsigned integer", volumeIdString)
|
||||
}
|
||||
if v := s.findVolume(vid); v != nil {
|
||||
return v.Compact()
|
||||
}
|
||||
return fmt.Errorf("volume id %d is not found during compact", vid)
|
||||
}
|
||||
func (s *Store) CommitCompactVolume(volumeIdString string) error {
|
||||
vid, err := NewVolumeId(volumeIdString)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Volume Id %s is not a valid unsigned integer", volumeIdString)
|
||||
}
|
||||
if v := s.findVolume(vid); v != nil {
|
||||
return v.commitCompact()
|
||||
}
|
||||
return fmt.Errorf("volume id %d is not found during commit compact", vid)
|
||||
}
|
||||
@@ -1,392 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
)
|
||||
|
||||
type Volume struct {
|
||||
Id VolumeId
|
||||
dir string
|
||||
Collection string
|
||||
dataFile *os.File
|
||||
nm NeedleMapper
|
||||
needleMapKind NeedleMapType
|
||||
readOnly bool
|
||||
|
||||
SuperBlock
|
||||
|
||||
accessLock sync.Mutex
|
||||
lastModifiedTime uint64 //unix time in seconds
|
||||
}
|
||||
|
||||
func NewVolume(dirname string, collection string, id VolumeId, needleMapKind NeedleMapType, replicaPlacement *ReplicaPlacement, ttl *TTL) (v *Volume, e error) {
|
||||
v = &Volume{dir: dirname, Collection: collection, Id: id}
|
||||
v.SuperBlock = SuperBlock{ReplicaPlacement: replicaPlacement, Ttl: ttl}
|
||||
v.needleMapKind = needleMapKind
|
||||
e = v.load(true, true, needleMapKind)
|
||||
return
|
||||
}
|
||||
func (v *Volume) String() string {
|
||||
return fmt.Sprintf("Id:%v, dir:%s, Collection:%s, dataFile:%v, nm:%v, readOnly:%v", v.Id, v.dir, v.Collection, v.dataFile, v.nm, v.readOnly)
|
||||
}
|
||||
|
||||
func loadVolumeWithoutIndex(dirname string, collection string, id VolumeId, needleMapKind NeedleMapType) (v *Volume, e error) {
|
||||
v = &Volume{dir: dirname, Collection: collection, Id: id}
|
||||
v.SuperBlock = SuperBlock{}
|
||||
v.needleMapKind = needleMapKind
|
||||
e = v.load(false, false, needleMapKind)
|
||||
return
|
||||
}
|
||||
func (v *Volume) FileName() (fileName string) {
|
||||
if v.Collection == "" {
|
||||
fileName = path.Join(v.dir, v.Id.String())
|
||||
} else {
|
||||
fileName = path.Join(v.dir, v.Collection+"_"+v.Id.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
func (v *Volume) load(alsoLoadIndex bool, createDatIfMissing bool, needleMapKind NeedleMapType) error {
|
||||
var e error
|
||||
fileName := v.FileName()
|
||||
|
||||
if exists, canRead, canWrite, modifiedTime := checkFile(fileName + ".dat"); exists {
|
||||
if !canRead {
|
||||
return fmt.Errorf("cannot read Volume Data file %s.dat", fileName)
|
||||
}
|
||||
if canWrite {
|
||||
v.dataFile, e = os.OpenFile(fileName+".dat", os.O_RDWR|os.O_CREATE, 0644)
|
||||
v.lastModifiedTime = uint64(modifiedTime.Unix())
|
||||
} else {
|
||||
glog.V(0).Infoln("opening " + fileName + ".dat in READONLY mode")
|
||||
v.dataFile, e = os.Open(fileName + ".dat")
|
||||
v.readOnly = true
|
||||
}
|
||||
} else {
|
||||
if createDatIfMissing {
|
||||
v.dataFile, e = os.OpenFile(fileName+".dat", os.O_RDWR|os.O_CREATE, 0644)
|
||||
} else {
|
||||
return fmt.Errorf("Volume Data file %s.dat does not exist.", fileName)
|
||||
}
|
||||
}
|
||||
|
||||
if e != nil {
|
||||
if !os.IsPermission(e) {
|
||||
return fmt.Errorf("cannot load Volume Data %s.dat: %v", fileName, e)
|
||||
}
|
||||
}
|
||||
|
||||
if v.ReplicaPlacement == nil {
|
||||
e = v.readSuperBlock()
|
||||
} else {
|
||||
e = v.maybeWriteSuperBlock()
|
||||
}
|
||||
if e == nil && alsoLoadIndex {
|
||||
var indexFile *os.File
|
||||
if v.readOnly {
|
||||
glog.V(1).Infoln("open to read file", fileName+".idx")
|
||||
if indexFile, e = os.OpenFile(fileName+".idx", os.O_RDONLY, 0644); e != nil {
|
||||
return fmt.Errorf("cannot read Volume Index %s.idx: %v", fileName, e)
|
||||
}
|
||||
} else {
|
||||
glog.V(1).Infoln("open to write file", fileName+".idx")
|
||||
if indexFile, e = os.OpenFile(fileName+".idx", os.O_RDWR|os.O_CREATE, 0644); e != nil {
|
||||
return fmt.Errorf("cannot write Volume Index %s.idx: %v", fileName, e)
|
||||
}
|
||||
}
|
||||
switch needleMapKind {
|
||||
case NeedleMapInMemory:
|
||||
glog.V(0).Infoln("loading index file", fileName+".idx", "readonly", v.readOnly)
|
||||
if v.nm, e = LoadNeedleMap(indexFile); e != nil {
|
||||
glog.V(0).Infof("loading index %s error: %v", fileName+".idx", e)
|
||||
}
|
||||
case NeedleMapLevelDb:
|
||||
glog.V(0).Infoln("loading leveldb file", fileName+".ldb")
|
||||
if v.nm, e = NewLevelDbNeedleMap(fileName+".ldb", indexFile); e != nil {
|
||||
glog.V(0).Infof("loading leveldb %s error: %v", fileName+".ldb", e)
|
||||
}
|
||||
case NeedleMapBoltDb:
|
||||
glog.V(0).Infoln("loading boltdb file", fileName+".bdb")
|
||||
if v.nm, e = NewBoltDbNeedleMap(fileName+".bdb", indexFile); e != nil {
|
||||
glog.V(0).Infof("loading boltdb %s error: %v", fileName+".bdb", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return e
|
||||
}
|
||||
func (v *Volume) Version() Version {
|
||||
return v.SuperBlock.Version()
|
||||
}
|
||||
func (v *Volume) Size() int64 {
|
||||
stat, e := v.dataFile.Stat()
|
||||
if e == nil {
|
||||
return stat.Size()
|
||||
}
|
||||
glog.V(0).Infof("Failed to read file size %s %v", v.dataFile.Name(), e)
|
||||
return -1
|
||||
}
|
||||
|
||||
// Close cleanly shuts down this volume
|
||||
func (v *Volume) Close() {
|
||||
v.accessLock.Lock()
|
||||
defer v.accessLock.Unlock()
|
||||
v.nm.Close()
|
||||
_ = v.dataFile.Close()
|
||||
}
|
||||
|
||||
func (v *Volume) NeedToReplicate() bool {
|
||||
return v.ReplicaPlacement.GetCopyCount() > 1
|
||||
}
|
||||
|
||||
// isFileUnchanged checks whether this needle to write is same as last one.
|
||||
// It requires serialized access in the same volume.
|
||||
func (v *Volume) isFileUnchanged(n *Needle) bool {
|
||||
nv, ok := v.nm.Get(n.Id)
|
||||
if ok && nv.Offset > 0 {
|
||||
oldNeedle := new(Needle)
|
||||
oldNeedle.Read(v.dataFile, int64(nv.Offset)*NeedlePaddingSize, nv.Size, v.Version())
|
||||
if oldNeedle.Checksum == n.Checksum && bytes.Equal(oldNeedle.Data, n.Data) {
|
||||
n.DataSize = oldNeedle.DataSize
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Destroy removes everything related to this volume
|
||||
func (v *Volume) Destroy() (err error) {
|
||||
if v.readOnly {
|
||||
err = fmt.Errorf("%s is read-only", v.dataFile.Name())
|
||||
return
|
||||
}
|
||||
v.Close()
|
||||
err = os.Remove(v.dataFile.Name())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = v.nm.Destroy()
|
||||
return
|
||||
}
|
||||
|
||||
func (v *Volume) write(n *Needle) (size uint32, err error) {
|
||||
glog.V(4).Infof("writing needle %s", NewFileIdFromNeedle(v.Id, n).String())
|
||||
if v.readOnly {
|
||||
err = fmt.Errorf("%s is read-only", v.dataFile.Name())
|
||||
return
|
||||
}
|
||||
v.accessLock.Lock()
|
||||
defer v.accessLock.Unlock()
|
||||
if v.isFileUnchanged(n) {
|
||||
size = n.DataSize
|
||||
glog.V(4).Infof("needle is unchanged!")
|
||||
return
|
||||
}
|
||||
var offset int64
|
||||
if offset, err = v.dataFile.Seek(0, 2); err != nil {
|
||||
glog.V(0).Infof("failed to seek the end of file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
//ensure file writing starting from aligned positions
|
||||
if offset%NeedlePaddingSize != 0 {
|
||||
offset = offset + (NeedlePaddingSize - offset%NeedlePaddingSize)
|
||||
if offset, err = v.dataFile.Seek(offset, 0); err != nil {
|
||||
glog.V(0).Infof("failed to align in datafile %s: %v", v.dataFile.Name(), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if size, err = n.Append(v.dataFile, v.Version()); err != nil {
|
||||
if e := v.dataFile.Truncate(offset); e != nil {
|
||||
err = fmt.Errorf("%s\ncannot truncate %s: %v", err, v.dataFile.Name(), e)
|
||||
}
|
||||
return
|
||||
}
|
||||
nv, ok := v.nm.Get(n.Id)
|
||||
if !ok || int64(nv.Offset)*NeedlePaddingSize < offset {
|
||||
if err = v.nm.Put(n.Id, uint32(offset/NeedlePaddingSize), n.Size); err != nil {
|
||||
glog.V(4).Infof("failed to save in needle map %d: %v", n.Id, err)
|
||||
}
|
||||
}
|
||||
if v.lastModifiedTime < n.LastModified {
|
||||
v.lastModifiedTime = n.LastModified
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (v *Volume) delete(n *Needle) (uint32, error) {
|
||||
glog.V(4).Infof("delete needle %s", NewFileIdFromNeedle(v.Id, n).String())
|
||||
if v.readOnly {
|
||||
return 0, fmt.Errorf("%s is read-only", v.dataFile.Name())
|
||||
}
|
||||
v.accessLock.Lock()
|
||||
defer v.accessLock.Unlock()
|
||||
nv, ok := v.nm.Get(n.Id)
|
||||
//fmt.Println("key", n.Id, "volume offset", nv.Offset, "data_size", n.Size, "cached size", nv.Size)
|
||||
if ok {
|
||||
size := nv.Size
|
||||
if err := v.nm.Delete(n.Id); err != nil {
|
||||
return size, err
|
||||
}
|
||||
if _, err := v.dataFile.Seek(0, 2); err != nil {
|
||||
return size, err
|
||||
}
|
||||
n.Data = nil
|
||||
_, err := n.Append(v.dataFile, v.Version())
|
||||
return size, err
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (v *Volume) read(n *Needle) (int, error) {
|
||||
nv, ok := v.nm.Get(n.Id)
|
||||
if !ok || nv.Offset == 0 {
|
||||
return -1, errors.New("Not Found")
|
||||
}
|
||||
bytesRead, err := n.Read(v.dataFile, int64(nv.Offset)*NeedlePaddingSize, nv.Size, v.Version())
|
||||
if err != nil {
|
||||
return bytesRead, err
|
||||
}
|
||||
if !n.HasTtl() {
|
||||
return bytesRead, err
|
||||
}
|
||||
ttlMinutes := n.Ttl.Minutes()
|
||||
if ttlMinutes == 0 {
|
||||
return bytesRead, nil
|
||||
}
|
||||
if !n.HasLastModifiedDate() {
|
||||
return bytesRead, nil
|
||||
}
|
||||
if uint64(time.Now().Unix()) < n.LastModified+uint64(ttlMinutes*60) {
|
||||
return bytesRead, nil
|
||||
}
|
||||
return -1, errors.New("Not Found")
|
||||
}
|
||||
|
||||
func ScanVolumeFile(dirname string, collection string, id VolumeId,
|
||||
needleMapKind NeedleMapType,
|
||||
visitSuperBlock func(SuperBlock) error,
|
||||
readNeedleBody bool,
|
||||
visitNeedle func(n *Needle, offset int64) error) (err error) {
|
||||
var v *Volume
|
||||
if v, err = loadVolumeWithoutIndex(dirname, collection, id, needleMapKind); err != nil {
|
||||
return fmt.Errorf("Failed to load volume %d: %v", id, err)
|
||||
}
|
||||
if err = visitSuperBlock(v.SuperBlock); err != nil {
|
||||
return fmt.Errorf("Failed to read volume %d super block: %v", id, err)
|
||||
}
|
||||
|
||||
version := v.Version()
|
||||
|
||||
offset := int64(SuperBlockSize)
|
||||
n, rest, e := ReadNeedleHeader(v.dataFile, version, offset)
|
||||
if e != nil {
|
||||
err = fmt.Errorf("cannot read needle header: %v", e)
|
||||
return
|
||||
}
|
||||
for n != nil {
|
||||
if readNeedleBody {
|
||||
if err = n.ReadNeedleBody(v.dataFile, version, offset+int64(NeedleHeaderSize), rest); err != nil {
|
||||
glog.V(0).Infof("cannot read needle body: %v", err)
|
||||
//err = fmt.Errorf("cannot read needle body: %v", err)
|
||||
//return
|
||||
}
|
||||
if n.DataSize >= n.Size {
|
||||
// this should come from a bug reported on #87 and #93
|
||||
// fixed in v0.69
|
||||
// remove this whole "if" clause later, long after 0.69
|
||||
oldRest, oldSize := rest, n.Size
|
||||
padding := NeedlePaddingSize - ((n.Size + NeedleHeaderSize + NeedleChecksumSize) % NeedlePaddingSize)
|
||||
n.Size = 0
|
||||
rest = n.Size + NeedleChecksumSize + padding
|
||||
if rest%NeedlePaddingSize != 0 {
|
||||
rest += (NeedlePaddingSize - rest%NeedlePaddingSize)
|
||||
}
|
||||
glog.V(4).Infof("Adjusting n.Size %d=>0 rest:%d=>%d %+v", oldSize, oldRest, rest, n)
|
||||
}
|
||||
}
|
||||
if err = visitNeedle(n, offset); err != nil {
|
||||
glog.V(0).Infof("visit needle error: %v", err)
|
||||
}
|
||||
offset += int64(NeedleHeaderSize) + int64(rest)
|
||||
glog.V(4).Infof("==> new entry offset %d", offset)
|
||||
if n, rest, err = ReadNeedleHeader(v.dataFile, version, offset); err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cannot read needle header: %v", err)
|
||||
}
|
||||
glog.V(4).Infof("new entry needle size:%d rest:%d", n.Size, rest)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (v *Volume) ContentSize() uint64 {
|
||||
return v.nm.ContentSize()
|
||||
}
|
||||
|
||||
func checkFile(filename string) (exists, canRead, canWrite bool, modTime time.Time) {
|
||||
exists = true
|
||||
fi, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
exists = false
|
||||
return
|
||||
}
|
||||
if fi.Mode()&0400 != 0 {
|
||||
canRead = true
|
||||
}
|
||||
if fi.Mode()&0200 != 0 {
|
||||
canWrite = true
|
||||
}
|
||||
modTime = fi.ModTime()
|
||||
return
|
||||
}
|
||||
|
||||
// volume is expired if modified time + volume ttl < now
|
||||
// except when volume is empty
|
||||
// or when the volume does not have a ttl
|
||||
// or when volumeSizeLimit is 0 when server just starts
|
||||
func (v *Volume) expired(volumeSizeLimit uint64) bool {
|
||||
if volumeSizeLimit == 0 {
|
||||
//skip if we don't know size limit
|
||||
return false
|
||||
}
|
||||
if v.ContentSize() == 0 {
|
||||
return false
|
||||
}
|
||||
if v.Ttl == nil || v.Ttl.Minutes() == 0 {
|
||||
return false
|
||||
}
|
||||
glog.V(0).Infof("now:%v lastModified:%v", time.Now().Unix(), v.lastModifiedTime)
|
||||
livedMinutes := (time.Now().Unix() - int64(v.lastModifiedTime)) / 60
|
||||
glog.V(0).Infof("ttl:%v lived:%v", v.Ttl, livedMinutes)
|
||||
if int64(v.Ttl.Minutes()) < livedMinutes {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// wait either maxDelayMinutes or 10% of ttl minutes
|
||||
func (v *Volume) exiredLongEnough(maxDelayMinutes uint32) bool {
|
||||
if v.Ttl == nil || v.Ttl.Minutes() == 0 {
|
||||
return false
|
||||
}
|
||||
removalDelay := v.Ttl.Minutes() / 10
|
||||
if removalDelay > maxDelayMinutes {
|
||||
removalDelay = maxDelayMinutes
|
||||
}
|
||||
|
||||
if uint64(v.Ttl.Minutes()+removalDelay)*60+v.lastModifiedTime < uint64(time.Now().Unix()) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/chrislusf/weed-fs/go/operation"
|
||||
)
|
||||
|
||||
type VolumeInfo struct {
|
||||
Id VolumeId
|
||||
Size uint64
|
||||
ReplicaPlacement *ReplicaPlacement
|
||||
Ttl *TTL
|
||||
Collection string
|
||||
Version Version
|
||||
FileCount int
|
||||
DeleteCount int
|
||||
DeletedByteCount uint64
|
||||
ReadOnly bool
|
||||
}
|
||||
|
||||
func NewVolumeInfo(m *operation.VolumeInformationMessage) (vi VolumeInfo, err error) {
|
||||
vi = VolumeInfo{
|
||||
Id: VolumeId(*m.Id),
|
||||
Size: *m.Size,
|
||||
Collection: *m.Collection,
|
||||
FileCount: int(*m.FileCount),
|
||||
DeleteCount: int(*m.DeleteCount),
|
||||
DeletedByteCount: *m.DeletedByteCount,
|
||||
ReadOnly: *m.ReadOnly,
|
||||
Version: Version(*m.Version),
|
||||
}
|
||||
rp, e := NewReplicaPlacementFromByte(byte(*m.ReplicaPlacement))
|
||||
if e != nil {
|
||||
return vi, e
|
||||
}
|
||||
vi.ReplicaPlacement = rp
|
||||
vi.Ttl = LoadTTLFromUint32(*m.Ttl)
|
||||
return vi, nil
|
||||
}
|
||||
|
||||
func (vi VolumeInfo) String() string {
|
||||
return fmt.Sprintf("Id:%d, Size:%d, ReplicaPlacement:%s, Collection:%s, Version:%v, FileCount:%d, DeleteCount:%d, DeletedByteCount:%d, ReadOnly:%v",
|
||||
vi.Id, vi.Size, vi.ReplicaPlacement, vi.Collection, vi.Version, vi.FileCount, vi.DeleteCount, vi.DeletedByteCount, vi.ReadOnly)
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
)
|
||||
|
||||
const (
|
||||
SuperBlockSize = 8
|
||||
)
|
||||
|
||||
/*
|
||||
* Super block currently has 8 bytes allocated for each volume.
|
||||
* Byte 0: version, 1 or 2
|
||||
* Byte 1: Replica Placement strategy, 000, 001, 002, 010, etc
|
||||
* Byte 2 and byte 3: Time to live. See TTL for definition
|
||||
* Rest bytes: Reserved
|
||||
*/
|
||||
type SuperBlock struct {
|
||||
version Version
|
||||
ReplicaPlacement *ReplicaPlacement
|
||||
Ttl *TTL
|
||||
}
|
||||
|
||||
func (s *SuperBlock) Version() Version {
|
||||
return s.version
|
||||
}
|
||||
func (s *SuperBlock) Bytes() []byte {
|
||||
header := make([]byte, SuperBlockSize)
|
||||
header[0] = byte(s.version)
|
||||
header[1] = s.ReplicaPlacement.Byte()
|
||||
s.Ttl.ToBytes(header[2:4])
|
||||
return header
|
||||
}
|
||||
|
||||
func (v *Volume) maybeWriteSuperBlock() error {
|
||||
stat, e := v.dataFile.Stat()
|
||||
if e != nil {
|
||||
glog.V(0).Infof("failed to stat datafile %s: %v", v.dataFile, e)
|
||||
return e
|
||||
}
|
||||
if stat.Size() == 0 {
|
||||
v.SuperBlock.version = CurrentVersion
|
||||
_, e = v.dataFile.Write(v.SuperBlock.Bytes())
|
||||
if e != nil && os.IsPermission(e) {
|
||||
//read-only, but zero length - recreate it!
|
||||
if v.dataFile, e = os.Create(v.dataFile.Name()); e == nil {
|
||||
if _, e = v.dataFile.Write(v.SuperBlock.Bytes()); e == nil {
|
||||
v.readOnly = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return e
|
||||
}
|
||||
func (v *Volume) readSuperBlock() (err error) {
|
||||
if _, err = v.dataFile.Seek(0, 0); err != nil {
|
||||
return fmt.Errorf("cannot seek to the beginning of %s: %v", v.dataFile.Name(), err)
|
||||
}
|
||||
header := make([]byte, SuperBlockSize)
|
||||
if _, e := v.dataFile.Read(header); e != nil {
|
||||
return fmt.Errorf("cannot read volume %d super block: %v", v.Id, e)
|
||||
}
|
||||
v.SuperBlock, err = ParseSuperBlock(header)
|
||||
return err
|
||||
}
|
||||
func ParseSuperBlock(header []byte) (superBlock SuperBlock, err error) {
|
||||
superBlock.version = Version(header[0])
|
||||
if superBlock.ReplicaPlacement, err = NewReplicaPlacementFromByte(header[1]); err != nil {
|
||||
err = fmt.Errorf("cannot read replica type: %s", err.Error())
|
||||
}
|
||||
superBlock.Ttl = LoadTTLFromBytes(header[2:4])
|
||||
return
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
)
|
||||
|
||||
func (v *Volume) garbageLevel() float64 {
|
||||
return float64(v.nm.DeletedSize()) / float64(v.ContentSize())
|
||||
}
|
||||
|
||||
func (v *Volume) Compact() error {
|
||||
glog.V(3).Infof("Compacting ...")
|
||||
//no need to lock for copy on write
|
||||
//v.accessLock.Lock()
|
||||
//defer v.accessLock.Unlock()
|
||||
//glog.V(3).Infof("Got Compaction lock...")
|
||||
|
||||
filePath := v.FileName()
|
||||
glog.V(3).Infof("creating copies for volume %d ...", v.Id)
|
||||
return v.copyDataAndGenerateIndexFile(filePath+".cpd", filePath+".cpx")
|
||||
}
|
||||
func (v *Volume) commitCompact() error {
|
||||
glog.V(3).Infof("Committing vacuuming...")
|
||||
v.accessLock.Lock()
|
||||
defer v.accessLock.Unlock()
|
||||
glog.V(3).Infof("Got Committing lock...")
|
||||
_ = v.dataFile.Close()
|
||||
var e error
|
||||
if e = os.Rename(v.FileName()+".cpd", v.FileName()+".dat"); e != nil {
|
||||
return e
|
||||
}
|
||||
if e = os.Rename(v.FileName()+".cpx", v.FileName()+".idx"); e != nil {
|
||||
return e
|
||||
}
|
||||
//glog.V(3).Infof("Pretending to be vacuuming...")
|
||||
//time.Sleep(20 * time.Second)
|
||||
if e = v.load(true, false, v.needleMapKind); e != nil {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *Volume) copyDataAndGenerateIndexFile(dstName, idxName string) (err error) {
|
||||
var (
|
||||
dst, idx *os.File
|
||||
)
|
||||
if dst, err = os.OpenFile(dstName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if idx, err = os.OpenFile(idxName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
|
||||
return
|
||||
}
|
||||
defer idx.Close()
|
||||
|
||||
nm := NewNeedleMap(idx)
|
||||
new_offset := int64(SuperBlockSize)
|
||||
|
||||
now := uint64(time.Now().Unix())
|
||||
|
||||
err = ScanVolumeFile(v.dir, v.Collection, v.Id, v.needleMapKind,
|
||||
func(superBlock SuperBlock) error {
|
||||
_, err = dst.Write(superBlock.Bytes())
|
||||
return err
|
||||
}, true, func(n *Needle, offset int64) error {
|
||||
if n.HasTtl() && now >= n.LastModified+uint64(v.Ttl.Minutes()*60) {
|
||||
return nil
|
||||
}
|
||||
nv, ok := v.nm.Get(n.Id)
|
||||
glog.V(4).Infoln("needle expected offset ", offset, "ok", ok, "nv", nv)
|
||||
if ok && int64(nv.Offset)*NeedlePaddingSize == offset && nv.Size > 0 {
|
||||
if err = nm.Put(n.Id, uint32(new_offset/NeedlePaddingSize), n.Size); err != nil {
|
||||
return fmt.Errorf("cannot put needle: %s", err)
|
||||
}
|
||||
if _, err = n.Append(dst, v.Version()); err != nil {
|
||||
return fmt.Errorf("cannot append needle: %s", err)
|
||||
}
|
||||
new_offset += n.DiskSize()
|
||||
glog.V(3).Infoln("saving key", n.Id, "volume offset", offset, "=>", new_offset, "data_size", n.Size)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/storage"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
)
|
||||
|
||||
type AllocateVolumeResult struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
func AllocateVolume(dn *DataNode, vid storage.VolumeId, option *VolumeGrowOption) error {
|
||||
values := make(url.Values)
|
||||
values.Add("volume", vid.String())
|
||||
values.Add("collection", option.Collection)
|
||||
values.Add("replication", option.ReplicaPlacement.String())
|
||||
values.Add("ttl", option.Ttl.String())
|
||||
jsonBlob, err := util.Post("http://"+dn.Url()+"/admin/assign_volume", values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var ret AllocateVolumeResult
|
||||
if err := json.Unmarshal(jsonBlob, &ret); err != nil {
|
||||
return fmt.Errorf("Invalid JSON result for %s: %s", "/admin/assign_volum", string(jsonBlob))
|
||||
}
|
||||
if ret.Error != "" {
|
||||
return errors.New(ret.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadConfiguration(t *testing.T) {
|
||||
|
||||
confContent := `
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Configuration>
|
||||
<Topology>
|
||||
<DataCenter name="dc1">
|
||||
<Rack name="rack1">
|
||||
<Ip>192.168.1.1</Ip>
|
||||
</Rack>
|
||||
</DataCenter>
|
||||
<DataCenter name="dc2">
|
||||
<Rack name="rack1">
|
||||
<Ip>192.168.1.2</Ip>
|
||||
</Rack>
|
||||
<Rack name="rack2">
|
||||
<Ip>192.168.1.3</Ip>
|
||||
<Ip>192.168.1.4</Ip>
|
||||
</Rack>
|
||||
</DataCenter>
|
||||
</Topology>
|
||||
</Configuration>
|
||||
`
|
||||
c, err := NewConfiguration([]byte(confContent))
|
||||
|
||||
fmt.Printf("%s\n", c)
|
||||
if err != nil {
|
||||
t.Fatalf("unmarshal error:%v", err)
|
||||
}
|
||||
|
||||
if len(c.Topo.DataCenters) <= 0 || c.Topo.DataCenters[0].Name != "dc1" {
|
||||
t.Fatalf("unmarshal error:%s", c)
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/operation"
|
||||
"github.com/chrislusf/weed-fs/go/security"
|
||||
"github.com/chrislusf/weed-fs/go/storage"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
)
|
||||
|
||||
func ReplicatedWrite(masterNode string, s *storage.Store,
|
||||
volumeId storage.VolumeId, needle *storage.Needle,
|
||||
r *http.Request) (size uint32, errorStatus string) {
|
||||
|
||||
//check JWT
|
||||
jwt := security.GetJwt(r)
|
||||
|
||||
ret, err := s.Write(volumeId, needle)
|
||||
needToReplicate := !s.HasVolume(volumeId)
|
||||
if err != nil {
|
||||
errorStatus = "Failed to write to local disk (" + err.Error() + ")"
|
||||
} else if ret > 0 {
|
||||
needToReplicate = needToReplicate || s.GetVolume(volumeId).NeedToReplicate()
|
||||
} else {
|
||||
errorStatus = "Failed to write to local disk"
|
||||
}
|
||||
if !needToReplicate && ret > 0 {
|
||||
needToReplicate = s.GetVolume(volumeId).NeedToReplicate()
|
||||
}
|
||||
if needToReplicate { //send to other replica locations
|
||||
if r.FormValue("type") != "replicate" {
|
||||
if !distributedOperation(masterNode, s, volumeId, func(location operation.Location) bool {
|
||||
_, err := operation.Upload(
|
||||
"http://"+location.Url+r.URL.Path+"?type=replicate&ts="+strconv.FormatUint(needle.LastModified, 10),
|
||||
string(needle.Name), bytes.NewReader(needle.Data), needle.IsGzipped(), string(needle.Mime),
|
||||
jwt)
|
||||
return err == nil
|
||||
}) {
|
||||
ret = 0
|
||||
errorStatus = "Failed to write to replicas for volume " + volumeId.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
size = ret
|
||||
return
|
||||
}
|
||||
|
||||
func ReplicatedDelete(masterNode string, store *storage.Store,
|
||||
volumeId storage.VolumeId, n *storage.Needle,
|
||||
r *http.Request) (ret uint32) {
|
||||
|
||||
//check JWT
|
||||
jwt := security.GetJwt(r)
|
||||
|
||||
ret, err := store.Delete(volumeId, n)
|
||||
if err != nil {
|
||||
glog.V(0).Infoln("delete error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
needToReplicate := !store.HasVolume(volumeId)
|
||||
if !needToReplicate && ret > 0 {
|
||||
needToReplicate = store.GetVolume(volumeId).NeedToReplicate()
|
||||
}
|
||||
if needToReplicate { //send to other replica locations
|
||||
if r.FormValue("type") != "replicate" {
|
||||
if !distributedOperation(masterNode, store, volumeId, func(location operation.Location) bool {
|
||||
return nil == util.Delete("http://"+location.Url+r.URL.Path+"?type=replicate", jwt)
|
||||
}) {
|
||||
ret = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func distributedOperation(masterNode string, store *storage.Store, volumeId storage.VolumeId, op func(location operation.Location) bool) bool {
|
||||
if lookupResult, lookupErr := operation.Lookup(masterNode, volumeId.String()); lookupErr == nil {
|
||||
length := 0
|
||||
selfUrl := (store.Ip + ":" + strconv.Itoa(store.Port))
|
||||
results := make(chan bool)
|
||||
for _, location := range lookupResult.Locations {
|
||||
if location.Url != selfUrl {
|
||||
length++
|
||||
go func(location operation.Location, results chan bool) {
|
||||
results <- op(location)
|
||||
}(location, results)
|
||||
}
|
||||
}
|
||||
ret := true
|
||||
for i := 0; i < length; i++ {
|
||||
ret = ret && <-results
|
||||
}
|
||||
return ret
|
||||
} else {
|
||||
glog.V(0).Infoln("Failed to lookup for", volumeId, lookupErr.Error())
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRemoveDataCenter(t *testing.T) {
|
||||
topo := setup(topologyLayout)
|
||||
topo.UnlinkChildNode(NodeId("dc2"))
|
||||
if topo.GetActiveVolumeCount() != 15 {
|
||||
t.Fail()
|
||||
}
|
||||
topo.UnlinkChildNode(NodeId("dc3"))
|
||||
if topo.GetActiveVolumeCount() != 12 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package topology
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/storage"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
)
|
||||
|
||||
func batchVacuumVolumeCheck(vl *VolumeLayout, vid storage.VolumeId, locationlist *VolumeLocationList, garbageThreshold string) bool {
|
||||
ch := make(chan bool, locationlist.Length())
|
||||
for index, dn := range locationlist.list {
|
||||
go func(index int, url string, vid storage.VolumeId) {
|
||||
//glog.V(0).Infoln(index, "Check vacuuming", vid, "on", dn.Url())
|
||||
if e, ret := vacuumVolume_Check(url, vid, garbageThreshold); e != nil {
|
||||
//glog.V(0).Infoln(index, "Error when checking vacuuming", vid, "on", url, e)
|
||||
ch <- false
|
||||
} else {
|
||||
//glog.V(0).Infoln(index, "Checked vacuuming", vid, "on", url, "needVacuum", ret)
|
||||
ch <- ret
|
||||
}
|
||||
}(index, dn.Url(), vid)
|
||||
}
|
||||
isCheckSuccess := true
|
||||
for _ = range locationlist.list {
|
||||
select {
|
||||
case canVacuum := <-ch:
|
||||
isCheckSuccess = isCheckSuccess && canVacuum
|
||||
case <-time.After(30 * time.Minute):
|
||||
isCheckSuccess = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return isCheckSuccess
|
||||
}
|
||||
func batchVacuumVolumeCompact(vl *VolumeLayout, vid storage.VolumeId, locationlist *VolumeLocationList) bool {
|
||||
vl.removeFromWritable(vid)
|
||||
ch := make(chan bool, locationlist.Length())
|
||||
for index, dn := range locationlist.list {
|
||||
go func(index int, url string, vid storage.VolumeId) {
|
||||
glog.V(0).Infoln(index, "Start vacuuming", vid, "on", url)
|
||||
if e := vacuumVolume_Compact(url, vid); e != nil {
|
||||
glog.V(0).Infoln(index, "Error when vacuuming", vid, "on", url, e)
|
||||
ch <- false
|
||||
} else {
|
||||
glog.V(0).Infoln(index, "Complete vacuuming", vid, "on", url)
|
||||
ch <- true
|
||||
}
|
||||
}(index, dn.Url(), vid)
|
||||
}
|
||||
isVacuumSuccess := true
|
||||
for _ = range locationlist.list {
|
||||
select {
|
||||
case _ = <-ch:
|
||||
case <-time.After(30 * time.Minute):
|
||||
isVacuumSuccess = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return isVacuumSuccess
|
||||
}
|
||||
func batchVacuumVolumeCommit(vl *VolumeLayout, vid storage.VolumeId, locationlist *VolumeLocationList) bool {
|
||||
isCommitSuccess := true
|
||||
for _, dn := range locationlist.list {
|
||||
glog.V(0).Infoln("Start Commiting vacuum", vid, "on", dn.Url())
|
||||
if e := vacuumVolume_Commit(dn.Url(), vid); e != nil {
|
||||
glog.V(0).Infoln("Error when committing vacuum", vid, "on", dn.Url(), e)
|
||||
isCommitSuccess = false
|
||||
} else {
|
||||
glog.V(0).Infoln("Complete Commiting vacuum", vid, "on", dn.Url())
|
||||
}
|
||||
if isCommitSuccess {
|
||||
vl.SetVolumeAvailable(dn, vid)
|
||||
}
|
||||
}
|
||||
return isCommitSuccess
|
||||
}
|
||||
func (t *Topology) Vacuum(garbageThreshold string) int {
|
||||
for _, col := range t.collectionMap.Items {
|
||||
c := col.(*Collection)
|
||||
for _, vl := range c.storageType2VolumeLayout.Items {
|
||||
if vl != nil {
|
||||
volumeLayout := vl.(*VolumeLayout)
|
||||
for vid, locationlist := range volumeLayout.vid2location {
|
||||
if batchVacuumVolumeCheck(volumeLayout, vid, locationlist, garbageThreshold) {
|
||||
if batchVacuumVolumeCompact(volumeLayout, vid, locationlist) {
|
||||
batchVacuumVolumeCommit(volumeLayout, vid, locationlist)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type VacuumVolumeResult struct {
|
||||
Result bool
|
||||
Error string
|
||||
}
|
||||
|
||||
func vacuumVolume_Check(urlLocation string, vid storage.VolumeId, garbageThreshold string) (error, bool) {
|
||||
values := make(url.Values)
|
||||
values.Add("volume", vid.String())
|
||||
values.Add("garbageThreshold", garbageThreshold)
|
||||
jsonBlob, err := util.Post("http://"+urlLocation+"/admin/vacuum_volume_check", values)
|
||||
if err != nil {
|
||||
glog.V(0).Infoln("parameters:", values)
|
||||
return err, false
|
||||
}
|
||||
var ret VacuumVolumeResult
|
||||
if err := json.Unmarshal(jsonBlob, &ret); err != nil {
|
||||
return err, false
|
||||
}
|
||||
if ret.Error != "" {
|
||||
return errors.New(ret.Error), false
|
||||
}
|
||||
return nil, ret.Result
|
||||
}
|
||||
func vacuumVolume_Compact(urlLocation string, vid storage.VolumeId) error {
|
||||
values := make(url.Values)
|
||||
values.Add("volume", vid.String())
|
||||
jsonBlob, err := util.Post("http://"+urlLocation+"/admin/vacuum_volume_compact", values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var ret VacuumVolumeResult
|
||||
if err := json.Unmarshal(jsonBlob, &ret); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Error != "" {
|
||||
return errors.New(ret.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func vacuumVolume_Commit(urlLocation string, vid storage.VolumeId) error {
|
||||
values := make(url.Values)
|
||||
values.Add("volume", vid.String())
|
||||
jsonBlob, err := util.Post("http://"+urlLocation+"/admin/vacuum_volume_commit", values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var ret VacuumVolumeResult
|
||||
if err := json.Unmarshal(jsonBlob, &ret); err != nil {
|
||||
return err
|
||||
}
|
||||
if ret.Error != "" {
|
||||
return errors.New(ret.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A mostly for read map, which can thread-safely
|
||||
// initialize the map entries.
|
||||
type ConcurrentReadMap struct {
|
||||
rmutex sync.RWMutex
|
||||
mutex sync.Mutex
|
||||
Items map[string]interface{}
|
||||
}
|
||||
|
||||
func NewConcurrentReadMap() *ConcurrentReadMap {
|
||||
return &ConcurrentReadMap{Items: make(map[string]interface{})}
|
||||
}
|
||||
|
||||
func (m *ConcurrentReadMap) initMapEntry(key string, newEntry func() interface{}) (value interface{}) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
if value, ok := m.Items[key]; ok {
|
||||
return value
|
||||
}
|
||||
value = newEntry()
|
||||
m.Items[key] = value
|
||||
return value
|
||||
}
|
||||
|
||||
func (m *ConcurrentReadMap) Get(key string, newEntry func() interface{}) interface{} {
|
||||
m.rmutex.RLock()
|
||||
if value, ok := m.Items[key]; ok {
|
||||
m.rmutex.RUnlock()
|
||||
return value
|
||||
}
|
||||
m.rmutex.RUnlock()
|
||||
return m.initMapEntry(key, newEntry)
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package util
|
||||
|
||||
// Copyright 2011 Numerotron Inc.
|
||||
// Use of this source code is governed by an MIT-style license
|
||||
// that can be found in the LICENSE file.
|
||||
//
|
||||
// Developed at www.stathat.com by Patrick Crosby
|
||||
// Contact us on twitter with any questions: twitter.com/stat_hat
|
||||
|
||||
// The jconfig package provides a simple, basic configuration file parser using JSON.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
data map[string]interface{}
|
||||
filename string
|
||||
}
|
||||
|
||||
func newConfig() *Config {
|
||||
result := new(Config)
|
||||
result.data = make(map[string]interface{})
|
||||
return result
|
||||
}
|
||||
|
||||
// Loads config information from a JSON file
|
||||
func LoadConfig(filename string) *Config {
|
||||
result := newConfig()
|
||||
result.filename = filename
|
||||
err := result.parse()
|
||||
if err != nil {
|
||||
glog.Fatalf("error loading config file %s: %s", filename, err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Loads config information from a JSON string
|
||||
func LoadConfigString(s string) *Config {
|
||||
result := newConfig()
|
||||
err := json.Unmarshal([]byte(s), &result.data)
|
||||
if err != nil {
|
||||
glog.Fatalf("error parsing config string %s: %s", s, err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *Config) StringMerge(s string) {
|
||||
next := LoadConfigString(s)
|
||||
c.merge(next.data)
|
||||
}
|
||||
|
||||
func (c *Config) LoadMerge(filename string) {
|
||||
next := LoadConfig(filename)
|
||||
c.merge(next.data)
|
||||
}
|
||||
|
||||
func (c *Config) merge(ndata map[string]interface{}) {
|
||||
for k, v := range ndata {
|
||||
c.data[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) parse() error {
|
||||
f, err := os.Open(c.filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
b := new(bytes.Buffer)
|
||||
_, err = b.ReadFrom(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(b.Bytes(), &c.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns a string for the config variable key
|
||||
func (c *Config) GetString(key string) string {
|
||||
result, present := c.data[key]
|
||||
if !present {
|
||||
return ""
|
||||
}
|
||||
return result.(string)
|
||||
}
|
||||
|
||||
// Returns an int for the config variable key
|
||||
func (c *Config) GetInt(key string) int {
|
||||
x, ok := c.data[key]
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return int(x.(float64))
|
||||
}
|
||||
|
||||
// Returns a float for the config variable key
|
||||
func (c *Config) GetFloat(key string) float64 {
|
||||
x, ok := c.data[key]
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return x.(float64)
|
||||
}
|
||||
|
||||
// Returns a bool for the config variable key
|
||||
func (c *Config) GetBool(key string) bool {
|
||||
x, ok := c.data[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return x.(bool)
|
||||
}
|
||||
|
||||
// Returns an array for the config variable key
|
||||
func (c *Config) GetArray(key string) []interface{} {
|
||||
result, present := c.data[key]
|
||||
if !present {
|
||||
return []interface{}(nil)
|
||||
}
|
||||
return result.([]interface{})
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package util
|
||||
|
||||
const (
|
||||
VERSION = "0.69 20150413"
|
||||
)
|
||||
@@ -1,106 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/security"
|
||||
)
|
||||
|
||||
var (
|
||||
client *http.Client
|
||||
Transport *http.Transport
|
||||
)
|
||||
|
||||
func init() {
|
||||
Transport = &http.Transport{
|
||||
MaxIdleConnsPerHost: 1024,
|
||||
}
|
||||
client = &http.Client{Transport: Transport}
|
||||
}
|
||||
|
||||
func PostBytes(url string, body []byte) ([]byte, error) {
|
||||
r, err := client.Post(url, "application/octet-stream", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Post to %s: %v", url, err)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Read response body: %v", err)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func Post(url string, values url.Values) ([]byte, error) {
|
||||
r, err := client.PostForm(url, values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func Get(url string) ([]byte, error) {
|
||||
r, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if r.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("%s: %s", url, r.Status)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func Delete(url string, jwt security.EncodedJwt) error {
|
||||
req, err := http.NewRequest("DELETE", url, nil)
|
||||
if jwt != "" {
|
||||
req.Header.Set("Authorization", "BEARER "+string(jwt))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, e := client.Do(req)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if _, err := ioutil.ReadAll(resp.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DownloadUrl(fileUrl string) (filename string, content []byte, e error) {
|
||||
response, err := client.Get(fileUrl)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
contentDisposition := response.Header["Content-Disposition"]
|
||||
if len(contentDisposition) > 0 {
|
||||
if strings.HasPrefix(contentDisposition[0], "filename=") {
|
||||
filename = contentDisposition[0][len("filename="):]
|
||||
filename = strings.Trim(filename, "\"")
|
||||
}
|
||||
}
|
||||
content, e = ioutil.ReadAll(response.Body)
|
||||
return
|
||||
}
|
||||
|
||||
func Do(req *http.Request) (resp *http.Response, err error) {
|
||||
return client.Do(req)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/storage"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdCompact.Run = runCompact // break init cycle
|
||||
cmdCompact.IsDebug = cmdCompact.Flag.Bool("debug", false, "enable debug mode")
|
||||
}
|
||||
|
||||
var cmdCompact = &Command{
|
||||
UsageLine: "compact -dir=/tmp -volumeId=234",
|
||||
Short: "run weed tool compact on volume file",
|
||||
Long: `Force an compaction to remove deleted files from volume files.
|
||||
The compacted .dat file is stored as .cpd file.
|
||||
The compacted .idx file is stored as .cpx file.
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
var (
|
||||
compactVolumePath = cmdCompact.Flag.String("dir", "/tmp", "data directory to store files")
|
||||
compactVolumeCollection = cmdCompact.Flag.String("collection", "", "volume collection name")
|
||||
compactVolumeId = cmdCompact.Flag.Int("volumeId", -1, "a volume id. The volume should already exist in the dir.")
|
||||
)
|
||||
|
||||
func runCompact(cmd *Command, args []string) bool {
|
||||
|
||||
if *compactVolumeId == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
vid := storage.VolumeId(*compactVolumeId)
|
||||
v, err := storage.NewVolume(*compactVolumePath, *compactVolumeCollection, vid,
|
||||
storage.NeedleMapInMemory, nil, nil)
|
||||
if err != nil {
|
||||
glog.Fatalf("Load Volume [ERROR] %s\n", err)
|
||||
}
|
||||
if err = v.Compact(); err != nil {
|
||||
glog.Fatalf("Compact Volume [ERROR] %s\n", err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1,477 +0,0 @@
|
||||
mode: set
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:41.27,43.20 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:46.2,46.17 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:43.20,45.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:49.13,56.19 6 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:60.2,60.23 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:71.2,71.31 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:88.2,90.8 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:56.19,58.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:60.23,62.32 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:68.3,68.9 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:62.32,63.65 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:63.65,66.5 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:71.31,72.46 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:72.46,73.28 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:74.4,77.27 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:83.4,84.10 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:73.28,73.43 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:77.27,82.5 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:114.55,118.43 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:118.43,120.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:123.34,124.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:127.2,128.43 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:124.13,126.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:131.30,133.2 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:135.14,140.2 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:143.26,144.20 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:149.2,149.20 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:154.2,156.31 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:164.2,165.12 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:144.20,148.3 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:149.20,152.3 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:156.31,157.24 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:157.24,161.4 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:170.23,172.2 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:174.13,175.32 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:178.2,178.21 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:175.32,177.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/weed.go:181.35,183.2 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:17.13,19.2 1 1
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:50.50,51.18 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:54.2,55.61 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:58.2,58.34 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:62.2,73.14 6 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:77.2,77.12 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:91.2,91.44 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:94.2,94.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:51.18,53.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:55.61,57.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:58.34,60.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:73.14,75.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:77.12,80.24 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:83.3,84.25 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:87.3,88.31 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:80.24,82.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:84.25,86.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/master.go:91.44,93.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:11.13,13.2 1 1
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:25.53,29.19 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:38.2,38.28 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:46.2,46.34 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:55.2,56.6 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:29.19,31.47 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:34.3,34.34 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:31.47,33.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:34.34,36.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:38.28,40.17 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:44.3,44.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:40.17,43.4 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:46.34,47.16 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:52.3,52.11 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:47.16,48.48 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:48.48,50.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/shell.go:56.6,60.3 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:23.13,34.2 10 1
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:59.50,61.37 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:102.2,102.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:61.37,62.23 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:65.3,65.82 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:62.23,64.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:65.82,66.18 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:89.4,89.14 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:66.18,67.22 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:67.22,68.24 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:73.6,74.18 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:77.6,82.18 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:68.24,69.70 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:69.70,71.8 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:74.18,76.7 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:82.18,84.7 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:86.5,88.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:91.3,93.15 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:96.3,100.29 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/upload.go:93.15,95.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/signal_handling.go:11.29,22.12 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/signal_handling.go:22.12,23.28 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/signal_handling.go:23.28,26.4 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:38.13,52.2 13 1
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:68.50,69.19 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:72.2,77.44 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:84.2,84.46 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:87.2,87.35 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:94.2,94.34 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:98.2,98.17 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:101.2,101.24 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:105.2,105.24 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:108.2,112.27 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:116.2,127.14 5 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:130.2,130.27 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:144.2,144.21 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:148.2,148.52 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:151.2,151.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:69.19,71.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:77.44,78.50 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:78.50,80.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:80.4,82.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:84.46,86.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:87.35,88.57 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:88.57,90.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:94.34,96.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:98.17,100.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:101.24,103.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:105.24,107.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:112.27,114.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:127.14,129.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:130.27,134.15 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:137.3,137.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:134.15,136.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:137.13,138.66 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:138.66,140.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:144.21,146.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/volume.go:148.52,150.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:20.13,25.2 4 1
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:41.52,42.27 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:78.2,78.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:42.27,44.15 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:48.3,48.21 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:51.3,51.43 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:44.15,46.12 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:48.21,50.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:51.43,55.18 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:59.4,60.32 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:55.18,57.13 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:60.32,63.19 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:66.5,66.36 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:69.5,69.19 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:63.19,65.6 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:66.36,68.6 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:69.19,71.11 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:74.4,76.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:81.91,83.24 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:86.2,87.8 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:83.24,85.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:90.70,92.16 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:95.2,97.33 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:100.2,100.12 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:92.16,94.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/download.go:97.33,99.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:12.13,14.2 1 1
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:30.47,32.24 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:36.2,37.32 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:40.2,41.16 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:44.2,50.116 5 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:63.2,63.16 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:67.2,67.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:32.24,34.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:37.32,39.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:41.16,43.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:50.116,52.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:52.56,54.17 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:61.3,61.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:54.17,57.4 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:57.4,60.4 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/fix.go:63.16,65.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:28.13,30.2 1 1
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:77.13,89.2 11 1
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:91.50,93.37 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:102.2,102.34 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:106.2,108.49 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:112.2,112.28 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:116.2,116.23 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:119.2,124.44 5 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:131.2,131.36 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:134.2,134.33 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:140.2,140.29 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:143.2,143.22 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:152.2,152.67 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:156.2,156.34 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:160.2,160.22 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:186.2,192.12 5 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:224.2,226.28 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:229.2,232.27 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:235.2,247.20 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:250.2,250.27 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:264.2,264.21 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:269.2,269.58 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:273.2,273.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:93.37,95.17 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:98.3,99.31 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:95.17,97.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:102.34,104.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:108.49,110.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:112.28,114.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:116.23,118.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:124.44,125.50 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:125.50,127.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:127.4,129.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:131.36,133.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:134.33,135.57 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:135.57,137.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:140.29,142.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:143.22,144.30 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:148.3,148.68 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:144.30,147.4 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:148.68,150.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:152.67,154.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:156.34,158.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:160.22,161.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:161.13,169.22 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:172.4,177.16 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:180.4,180.51 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:169.22,171.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:177.16,179.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:180.51,182.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:192.12,201.15 5 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:205.3,205.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:218.3,219.51 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:201.15,203.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:205.13,210.26 5 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:213.4,215.21 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:210.26,212.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:219.51,221.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:226.28,228.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:232.27,234.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:247.20,249.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:250.27,254.15 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:257.3,257.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:254.15,256.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:257.13,258.66 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:258.66,260.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:264.21,267.3 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/server.go:269.58,271.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/filer.go:32.13,46.2 12 1
|
||||
github.com/chrislusf/weed-fs/go/weed/filer.go:70.49,72.56 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/filer.go:76.2,83.20 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/filer.go:86.2,91.14 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/filer.go:94.2,94.49 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/filer.go:98.2,98.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/filer.go:72.56,74.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/filer.go:83.20,85.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/filer.go:91.14,93.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/filer.go:94.49,96.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:46.13,64.2 17 1
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:104.53,106.19 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:109.2,110.25 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:119.2,119.14 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:123.2,123.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:127.2,127.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:106.19,108.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:110.25,112.17 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:115.3,116.31 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:112.17,114.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:119.14,121.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:123.13,125.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:130.20,136.38 6 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:140.2,143.40 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:146.2,154.25 9 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:136.38,139.3 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:143.40,145.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:157.19,165.38 8 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:169.2,175.24 7 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:165.38,168.3 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:183.71,189.25 5 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:207.2,207.25 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:235.2,236.25 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:189.25,191.13 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:191.13,193.38 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:193.38,194.39 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:197.5,198.52 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:194.39,196.6 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:198.52,200.6 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:200.6,202.6 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:207.25,211.93 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:211.93,213.61 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:226.4,227.29 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:213.61,214.45 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:220.5,221.30 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:214.45,217.6 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:217.6,219.6 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:222.5,225.5 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:227.29,229.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:230.4,233.4 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:239.53,242.34 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:242.34,243.20 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:246.3,246.20 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:249.3,249.28 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:252.3,255.43 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:267.3,267.42 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:243.20,244.12 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:246.20,247.12 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:249.28,251.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:255.43,257.47 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:265.4,265.21 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:257.47,258.65 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:258.65,259.32 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:259.32,262.7 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:267.42,269.51 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:269.51,273.5 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:273.5,276.5 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:277.4,280.4 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:284.86,286.16 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:289.2,291.6 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:286.16,288.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:291.6,292.10 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:293.3,295.10 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:296.3,298.28 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:303.63,305.16 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:308.2,311.23 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:335.2,335.23 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:305.16,307.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:311.23,312.7 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:312.7,313.42 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:313.42,315.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:315.5,316.10 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:319.3,321.7 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:328.3,328.21 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:321.7,322.42 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:322.42,324.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:324.5,325.10 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:328.21,329.41 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:329.41,331.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:361.29,367.2 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:369.44,371.15 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:371.15,373.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:373.3,373.32 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:373.32,375.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:375.3,377.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:380.70,384.6 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:384.6,385.10 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:386.3,388.10 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:389.3,391.43 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:396.4,401.72 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:391.43,395.5 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:406.30,408.41 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:414.2,424.35 11 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:436.2,437.39 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:446.2,448.35 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:454.2,454.39 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:458.2,465.40 7 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:468.2,471.35 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:481.2,482.39 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:408.41,413.3 4 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:424.35,427.20 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:427.20,428.15 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:431.4,431.15 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:428.15,430.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:431.15,433.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:437.39,439.26 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:442.3,442.26 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:439.26,441.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:442.26,444.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:448.35,449.20 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:449.20,452.4 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:454.39,457.3 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:465.40,467.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:471.35,473.104 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:473.104,476.89 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:476.89,478.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:482.39,484.87 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:484.87,487.89 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:487.89,489.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:500.56,501.17 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:504.2,504.28 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:509.2,509.12 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:514.2,515.8 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:501.17,503.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:504.28,506.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:506.3,508.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:509.12,510.26 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:510.26,512.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:518.64,521.15 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:532.2,532.20 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:521.15,523.24 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:526.3,527.15 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:530.3,530.16 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:523.24,525.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:527.15,529.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:535.46,541.29 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:545.2,545.16 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/benchmark.go:541.29,544.3 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/command.go:32.33,35.12 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/command.go:38.2,38.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/command.go:35.12,37.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/command.go:41.27,48.2 6 0
|
||||
github.com/chrislusf/weed-fs/go/weed/command.go:52.35,54.2 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:18.13,20.2 1 1
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:48.50,50.27 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:54.2,55.17 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:84.2,85.29 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:88.2,90.16 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:93.2,96.16 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:100.2,102.116 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:120.2,120.16 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:123.2,123.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:50.27,52.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:55.17,56.56 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:61.3,61.68 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:66.3,67.19 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:74.3,81.33 5 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:56.56,59.4 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:61.68,64.4 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:67.19,69.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:69.4,70.46 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:70.46,72.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:85.29,87.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:90.16,92.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:96.16,98.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:102.116,105.3 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:105.55,109.56 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:118.3,118.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:109.56,111.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:111.4,112.11 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:112.11,114.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:114.5,116.5 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:120.16,122.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:133.91,135.18 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:170.2,170.8 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:135.18,143.17 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:146.3,148.45 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:152.3,153.54 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:156.3,156.31 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:143.17,145.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:148.45,150.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:153.54,155.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:157.3,159.34 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:162.3,168.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/export.go:159.34,161.4 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/version.go:17.51,18.20 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/version.go:22.2,23.13 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/version.go:18.20,20.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/compact.go:8.13,11.2 2 1
|
||||
github.com/chrislusf/weed-fs/go/weed/compact.go:29.51,31.28 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/compact.go:35.2,37.16 3 0
|
||||
github.com/chrislusf/weed-fs/go/weed/compact.go:40.2,40.35 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/compact.go:44.2,44.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/compact.go:31.28,33.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/compact.go:37.16,39.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/compact.go:40.35,42.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount.go:12.13,17.2 4 1
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:19.49,21.29 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:26.2,27.16 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:32.2,32.21 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:37.2,38.16 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:43.2,44.37 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:48.2,48.13 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:21.29,24.3 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:27.16,30.3 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:32.21,35.3 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:38.16,40.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:44.37,46.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:56.30,58.2 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:59.58,61.2 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:68.33,70.2 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:72.74,74.14 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:77.2,77.33 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:80.2,80.55 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:74.14,76.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:77.33,79.3 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:85.36,87.2 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:89.69,91.79 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:97.2,97.78 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:106.2,106.17 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:91.79,92.38 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:92.38,95.4 2 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:97.78,98.33 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:98.33,99.64 1 0
|
||||
github.com/chrislusf/weed-fs/go/weed/mount_std.go:99.64,102.5 2 0
|
||||
@@ -1,101 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/operation"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
)
|
||||
|
||||
var (
|
||||
downloadReplication *string
|
||||
downloadDir *string
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdDownload.Run = runDownload // break init cycle
|
||||
cmdDownload.IsDebug = cmdDownload.Flag.Bool("debug", false, "verbose debug information")
|
||||
server = cmdDownload.Flag.String("server", "localhost:9333", "weedfs master location")
|
||||
downloadDir = cmdDownload.Flag.String("dir", ".", "Download the whole folder recursively if specified.")
|
||||
}
|
||||
|
||||
var cmdDownload = &Command{
|
||||
UsageLine: "download -server=localhost:9333 -dir=one_directory fid1 [fid2 fid3 ...]",
|
||||
Short: "download files by file id",
|
||||
Long: `download files by file id.
|
||||
|
||||
Usually you just need to use curl to lookup the file's volume server, and then download them directly.
|
||||
This download tool combine the two steps into one.
|
||||
|
||||
What's more, if you use "weed upload -maxMB=..." option to upload a big file divided into chunks, you can
|
||||
use this tool to download the chunks and merge them automatically.
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
func runDownload(cmd *Command, args []string) bool {
|
||||
for _, fid := range args {
|
||||
filename, content, e := fetchFileId(*server, fid)
|
||||
if e != nil {
|
||||
fmt.Println("Fetch Error:", e)
|
||||
continue
|
||||
}
|
||||
if filename == "" {
|
||||
filename = fid
|
||||
}
|
||||
if strings.HasSuffix(filename, "-list") {
|
||||
filename = filename[0 : len(filename)-len("-list")]
|
||||
fids := strings.Split(string(content), "\n")
|
||||
f, err := os.OpenFile(path.Join(*downloadDir, filename), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
|
||||
if err != nil {
|
||||
fmt.Println("File Creation Error:", e)
|
||||
continue
|
||||
}
|
||||
defer f.Close()
|
||||
for _, partId := range fids {
|
||||
var n int
|
||||
_, part, err := fetchFileId(*server, partId)
|
||||
if err == nil {
|
||||
n, err = f.Write(part)
|
||||
}
|
||||
if err == nil && n < len(part) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println("File Write Error:", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ioutil.WriteFile(path.Join(*downloadDir, filename), content, os.ModePerm)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func fetchFileId(server string, fileId string) (filename string, content []byte, e error) {
|
||||
fileUrl, lookupError := operation.LookupFileId(server, fileId)
|
||||
if lookupError != nil {
|
||||
return "", nil, lookupError
|
||||
}
|
||||
filename, content, e = util.DownloadUrl(fileUrl)
|
||||
return
|
||||
}
|
||||
|
||||
func WriteFile(filename string, data []byte, perm os.FileMode) error {
|
||||
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := f.Write(data)
|
||||
f.Close()
|
||||
if err == nil && n < len(data) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/storage"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdExport.Run = runExport // break init cycle
|
||||
}
|
||||
|
||||
const (
|
||||
defaultFnFormat = `{{.Mime}}/{{.Id}}:{{.Name}}`
|
||||
)
|
||||
|
||||
var cmdExport = &Command{
|
||||
UsageLine: "export -dir=/tmp -volumeId=234 -o=/dir/name.tar -fileNameFormat={{.Name}}",
|
||||
Short: "list or export files from one volume data file",
|
||||
Long: `List all files in a volume, or Export all files in a volume to a tar file if the output is specified.
|
||||
|
||||
The format of file name in the tar file can be customized. Default is {{.Mime}}/{{.Id}}:{{.Name}}. Also available is {{.Key}}.
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
var (
|
||||
exportVolumePath = cmdExport.Flag.String("dir", "/tmp", "input data directory to store volume data files")
|
||||
exportCollection = cmdExport.Flag.String("collection", "", "the volume collection name")
|
||||
exportVolumeId = cmdExport.Flag.Int("volumeId", -1, "a volume id. The volume .dat and .idx files should already exist in the dir.")
|
||||
dest = cmdExport.Flag.String("o", "", "output tar file name, must ends with .tar, or just a \"-\" for stdout")
|
||||
format = cmdExport.Flag.String("fileNameFormat", defaultFnFormat, "filename format, default to {{.Mime}}/{{.Id}}:{{.Name}}")
|
||||
tarFh *tar.Writer
|
||||
tarHeader tar.Header
|
||||
fnTmpl *template.Template
|
||||
fnTmplBuf = bytes.NewBuffer(nil)
|
||||
)
|
||||
|
||||
func runExport(cmd *Command, args []string) bool {
|
||||
|
||||
if *exportVolumeId == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
var err error
|
||||
if *dest != "" {
|
||||
if *dest != "-" && !strings.HasSuffix(*dest, ".tar") {
|
||||
fmt.Println("the output file", *dest, "should be '-' or end with .tar")
|
||||
return false
|
||||
}
|
||||
|
||||
if fnTmpl, err = template.New("name").Parse(*format); err != nil {
|
||||
fmt.Println("cannot parse format " + *format + ": " + err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
var fh *os.File
|
||||
if *dest == "-" {
|
||||
fh = os.Stdout
|
||||
} else {
|
||||
if fh, err = os.Create(*dest); err != nil {
|
||||
glog.Fatalf("cannot open output tar %s: %s", *dest, err)
|
||||
}
|
||||
}
|
||||
defer fh.Close()
|
||||
tarFh = tar.NewWriter(fh)
|
||||
defer tarFh.Close()
|
||||
t := time.Now()
|
||||
tarHeader = tar.Header{Mode: 0644,
|
||||
ModTime: t, Uid: os.Getuid(), Gid: os.Getgid(),
|
||||
Typeflag: tar.TypeReg,
|
||||
AccessTime: t, ChangeTime: t}
|
||||
}
|
||||
|
||||
fileName := strconv.Itoa(*exportVolumeId)
|
||||
if *exportCollection != "" {
|
||||
fileName = *exportCollection + "_" + fileName
|
||||
}
|
||||
vid := storage.VolumeId(*exportVolumeId)
|
||||
indexFile, err := os.OpenFile(path.Join(*exportVolumePath, fileName+".idx"), os.O_RDONLY, 0644)
|
||||
if err != nil {
|
||||
glog.Fatalf("Create Volume Index [ERROR] %s\n", err)
|
||||
}
|
||||
defer indexFile.Close()
|
||||
|
||||
nm, err := storage.LoadNeedleMap(indexFile)
|
||||
if err != nil {
|
||||
glog.Fatalf("cannot load needle map from %s: %s", indexFile.Name(), err)
|
||||
}
|
||||
|
||||
var version storage.Version
|
||||
|
||||
err = storage.ScanVolumeFile(*exportVolumePath, *exportCollection, vid,
|
||||
storage.NeedleMapInMemory,
|
||||
func(superBlock storage.SuperBlock) error {
|
||||
version = superBlock.Version()
|
||||
return nil
|
||||
}, true, func(n *storage.Needle, offset int64) error {
|
||||
nv, ok := nm.Get(n.Id)
|
||||
glog.V(3).Infof("key %d offset %d size %d disk_size %d gzip %v ok %v nv %+v",
|
||||
n.Id, offset, n.Size, n.DiskSize(), n.IsGzipped(), ok, nv)
|
||||
if ok && nv.Size > 0 && int64(nv.Offset)*8 == offset {
|
||||
return walker(vid, n, version)
|
||||
}
|
||||
if !ok {
|
||||
glog.V(2).Infof("This seems deleted %d size %d", n.Id, n.Size)
|
||||
} else {
|
||||
glog.V(2).Infof("Skipping later-updated Id %d size %d", n.Id, n.Size)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
glog.Fatalf("Export Volume File [ERROR] %s\n", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type nameParams struct {
|
||||
Name string
|
||||
Id uint64
|
||||
Mime string
|
||||
Key string
|
||||
}
|
||||
|
||||
func walker(vid storage.VolumeId, n *storage.Needle, version storage.Version) (err error) {
|
||||
key := storage.NewFileIdFromNeedle(vid, n).String()
|
||||
if tarFh != nil {
|
||||
fnTmplBuf.Reset()
|
||||
if err = fnTmpl.Execute(fnTmplBuf,
|
||||
nameParams{Name: string(n.Name),
|
||||
Id: n.Id,
|
||||
Mime: string(n.Mime),
|
||||
Key: key,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
nm := fnTmplBuf.String()
|
||||
|
||||
if n.IsGzipped() && path.Ext(nm) != ".gz" {
|
||||
nm = nm + ".gz"
|
||||
}
|
||||
|
||||
tarHeader.Name, tarHeader.Size = nm, int64(len(n.Data))
|
||||
if err = tarFh.WriteHeader(&tarHeader); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tarFh.Write(n.Data)
|
||||
} else {
|
||||
size := n.DataSize
|
||||
if version == storage.Version1 {
|
||||
size = n.Size
|
||||
}
|
||||
fmt.Printf("key=%s Name=%s Size=%d gzip=%t mime=%s\n",
|
||||
key,
|
||||
n.Name,
|
||||
size,
|
||||
n.IsGzipped(),
|
||||
n.Mime,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
"github.com/chrislusf/weed-fs/go/weed/weed_server"
|
||||
)
|
||||
|
||||
var (
|
||||
f FilerOptions
|
||||
)
|
||||
|
||||
type FilerOptions struct {
|
||||
master *string
|
||||
port *int
|
||||
collection *string
|
||||
defaultReplicaPlacement *string
|
||||
dir *string
|
||||
redirectOnRead *bool
|
||||
disableDirListing *bool
|
||||
secretKey *string
|
||||
cassandra_server *string
|
||||
cassandra_keyspace *string
|
||||
redis_server *string
|
||||
redis_database *int
|
||||
}
|
||||
|
||||
func init() {
|
||||
cmdFiler.Run = runFiler // break init cycle
|
||||
f.master = cmdFiler.Flag.String("master", "localhost:9333", "master server location")
|
||||
f.collection = cmdFiler.Flag.String("collection", "", "all data will be stored in this collection")
|
||||
f.port = cmdFiler.Flag.Int("port", 8888, "filer server http listen port")
|
||||
f.dir = cmdFiler.Flag.String("dir", os.TempDir(), "directory to store meta data")
|
||||
f.defaultReplicaPlacement = cmdFiler.Flag.String("defaultReplicaPlacement", "000", "default replication type if not specified")
|
||||
f.redirectOnRead = cmdFiler.Flag.Bool("redirectOnRead", false, "whether proxy or redirect to volume server during file GET request")
|
||||
f.disableDirListing = cmdFiler.Flag.Bool("disableDirListing", false, "turn off directory listing")
|
||||
f.cassandra_server = cmdFiler.Flag.String("cassandra.server", "", "host[:port] of the cassandra server")
|
||||
f.cassandra_keyspace = cmdFiler.Flag.String("cassandra.keyspace", "seaweed", "keyspace of the cassandra server")
|
||||
f.redis_server = cmdFiler.Flag.String("redis.server", "", "host:port of the redis server, e.g., 127.0.0.1:6379")
|
||||
f.redis_database = cmdFiler.Flag.Int("redis.database", 0, "the database on the redis server")
|
||||
f.secretKey = cmdFiler.Flag.String("secure.secret", "", "secret to encrypt Json Web Token(JWT)")
|
||||
|
||||
}
|
||||
|
||||
var cmdFiler = &Command{
|
||||
UsageLine: "filer -port=8888 -dir=/tmp -master=<ip:port>",
|
||||
Short: "start a file server that points to a master server",
|
||||
Long: `start a file server which accepts REST operation for any files.
|
||||
|
||||
//create or overwrite the file, the directories /path/to will be automatically created
|
||||
POST /path/to/file
|
||||
//get the file content
|
||||
GET /path/to/file
|
||||
//create or overwrite the file, the filename in the multipart request will be used
|
||||
POST /path/to/
|
||||
//return a json format subdirectory and files listing
|
||||
GET /path/to/
|
||||
|
||||
Current <fullpath~fileid> mapping metadata store is local embedded leveldb.
|
||||
It should be highly scalable to hundreds of millions of files on a modest machine.
|
||||
|
||||
Future we will ensure it can avoid of being SPOF.
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
func runFiler(cmd *Command, args []string) bool {
|
||||
|
||||
if err := util.TestFolderWritable(*f.dir); err != nil {
|
||||
glog.Fatalf("Check Meta Folder (-dir) Writable %s : %s", *f.dir, err)
|
||||
}
|
||||
|
||||
r := http.NewServeMux()
|
||||
_, nfs_err := weed_server.NewFilerServer(r, *f.port, *f.master, *f.dir, *f.collection,
|
||||
*f.defaultReplicaPlacement, *f.redirectOnRead, *f.disableDirListing,
|
||||
*f.secretKey,
|
||||
*f.cassandra_server, *f.cassandra_keyspace,
|
||||
*f.redis_server, *f.redis_database,
|
||||
)
|
||||
if nfs_err != nil {
|
||||
glog.Fatalf("Filer startup error: %v", nfs_err)
|
||||
}
|
||||
glog.V(0).Infoln("Start Seaweed Filer", util.VERSION, "at port", strconv.Itoa(*f.port))
|
||||
filerListener, e := util.NewListener(
|
||||
":"+strconv.Itoa(*f.port),
|
||||
time.Duration(10)*time.Second,
|
||||
)
|
||||
if e != nil {
|
||||
glog.Fatalf("Filer listener error: %v", e)
|
||||
}
|
||||
if e := http.Serve(filerListener, r); e != nil {
|
||||
glog.Fatalf("Filer Fail to serve: %v", e)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
"github.com/chrislusf/weed-fs/go/weed/weed_server"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdMaster.Run = runMaster // break init cycle
|
||||
}
|
||||
|
||||
var cmdMaster = &Command{
|
||||
UsageLine: "master -port=9333",
|
||||
Short: "start a master server",
|
||||
Long: `start a master server to provide volume=>location mapping service
|
||||
and sequence number of file ids
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
var (
|
||||
mport = cmdMaster.Flag.Int("port", 9333, "http listen port")
|
||||
masterIp = cmdMaster.Flag.String("ip", "localhost", "master <ip>|<server> address")
|
||||
masterBindIp = cmdMaster.Flag.String("ip.bind", "0.0.0.0", "ip address to bind to")
|
||||
mPublicUrl = cmdMaster.Flag.String("publicUrl", "", "peer accessible <ip>|<server_name>:port")
|
||||
metaFolder = cmdMaster.Flag.String("mdir", os.TempDir(), "data directory to store meta data")
|
||||
masterPeers = cmdMaster.Flag.String("peers", "", "other master nodes in comma separated ip:port list")
|
||||
volumeSizeLimitMB = cmdMaster.Flag.Uint("volumeSizeLimitMB", 30*1000, "Master stops directing writes to oversized volumes.")
|
||||
mpulse = cmdMaster.Flag.Int("pulseSeconds", 5, "number of seconds between heartbeats")
|
||||
confFile = cmdMaster.Flag.String("conf", "/etc/weedfs/weedfs.conf", "Deprecating! xml configuration file")
|
||||
defaultReplicaPlacement = cmdMaster.Flag.String("defaultReplication", "000", "Default replication type if not specified.")
|
||||
mTimeout = cmdMaster.Flag.Int("idleTimeout", 10, "connection idle seconds")
|
||||
mMaxCpu = cmdMaster.Flag.Int("maxCpu", 0, "maximum number of CPUs. 0 means all available CPUs")
|
||||
garbageThreshold = cmdMaster.Flag.String("garbageThreshold", "0.3", "threshold to vacuum and reclaim spaces")
|
||||
masterWhiteListOption = cmdMaster.Flag.String("whiteList", "", "comma separated Ip addresses having write permission. No limit if empty.")
|
||||
masterSecureKey = cmdMaster.Flag.String("secure.secret", "", "secret to encrypt Json Web Token(JWT)")
|
||||
|
||||
masterWhiteList []string
|
||||
)
|
||||
|
||||
func runMaster(cmd *Command, args []string) bool {
|
||||
if *mMaxCpu < 1 {
|
||||
*mMaxCpu = runtime.NumCPU()
|
||||
}
|
||||
runtime.GOMAXPROCS(*mMaxCpu)
|
||||
if err := util.TestFolderWritable(*metaFolder); err != nil {
|
||||
glog.Fatalf("Check Meta Folder (-mdir) Writable %s : %s", *metaFolder, err)
|
||||
}
|
||||
if *masterWhiteListOption != "" {
|
||||
masterWhiteList = strings.Split(*masterWhiteListOption, ",")
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
ms := weed_server.NewMasterServer(r, *mport, *metaFolder,
|
||||
*volumeSizeLimitMB, *mpulse, *confFile, *defaultReplicaPlacement, *garbageThreshold,
|
||||
masterWhiteList, *masterSecureKey,
|
||||
)
|
||||
|
||||
listeningAddress := *masterBindIp + ":" + strconv.Itoa(*mport)
|
||||
|
||||
glog.V(0).Infoln("Start Seaweed Master", util.VERSION, "at", listeningAddress)
|
||||
|
||||
listener, e := util.NewListener(listeningAddress, time.Duration(*mTimeout)*time.Second)
|
||||
if e != nil {
|
||||
glog.Fatalf("Master startup error: %v", e)
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
myMasterAddress := *masterIp + ":" + strconv.Itoa(*mport)
|
||||
if *mPublicUrl != "" {
|
||||
myMasterAddress = *mPublicUrl
|
||||
}
|
||||
var peers []string
|
||||
if *masterPeers != "" {
|
||||
peers = strings.Split(*masterPeers, ",")
|
||||
}
|
||||
raftServer := weed_server.NewRaftServer(r, peers, myMasterAddress, *metaFolder, ms.Topo, *mpulse)
|
||||
ms.SetRaftServer(raftServer)
|
||||
}()
|
||||
|
||||
if e := http.Serve(listener, r); e != nil {
|
||||
glog.Fatalf("Fail to serve: %v", e)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package main
|
||||
|
||||
type MountOptions struct {
|
||||
filer *string
|
||||
dir *string
|
||||
}
|
||||
|
||||
var (
|
||||
mountOptions MountOptions
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdMount.Run = runMount // break init cycle
|
||||
cmdMount.IsDebug = cmdMount.Flag.Bool("debug", false, "verbose debug information")
|
||||
mountOptions.filer = cmdMount.Flag.String("filer", "localhost:8888", "weedfs filer location")
|
||||
mountOptions.dir = cmdMount.Flag.String("dir", "", "mount weed filer to this directory")
|
||||
}
|
||||
|
||||
var cmdMount = &Command{
|
||||
UsageLine: "mount -filer=localhost:8888 -dir=/some/dir",
|
||||
Short: "mount weed filer to a directory as file system in userspace(FUSE)",
|
||||
Long: `mount weed file system to userspace.
|
||||
|
||||
Pre-requisites:
|
||||
1) have a weed file system running
|
||||
2) have a "weed filer" running
|
||||
These 2 requirements can be achieved with one command "weed server -filer=true"
|
||||
|
||||
This uses bazil.org/fuse, whichenables writing FUSE file systems on
|
||||
Linux, and OS X.
|
||||
|
||||
On OS X, it requires OSXFUSE (http://osxfuse.github.com/).
|
||||
|
||||
`,
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
// +build linux darwin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"bazil.org/fuse"
|
||||
"bazil.org/fuse/fs"
|
||||
"github.com/chrislusf/weed-fs/go/filer"
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/storage"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func runMount(cmd *Command, args []string) bool {
|
||||
fmt.Printf("This is Seaweed File System version %s %s %s\n", util.VERSION, runtime.GOOS, runtime.GOARCH)
|
||||
if *mountOptions.dir == "" {
|
||||
fmt.Printf("Please specify the mount directory via \"-dir\"")
|
||||
return false
|
||||
}
|
||||
|
||||
c, err := fuse.Mount(*mountOptions.dir)
|
||||
if err != nil {
|
||||
glog.Fatal(err)
|
||||
return false
|
||||
}
|
||||
|
||||
OnInterrupt(func() {
|
||||
fuse.Unmount(*mountOptions.dir)
|
||||
c.Close()
|
||||
})
|
||||
|
||||
err = fs.Serve(c, WFS{})
|
||||
if err != nil {
|
||||
fuse.Unmount(*mountOptions.dir)
|
||||
}
|
||||
|
||||
// check if the mount process has an error to report
|
||||
<-c.Ready
|
||||
if err := c.MountError; err != nil {
|
||||
glog.Fatal(err)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type File struct {
|
||||
FileId filer.FileId
|
||||
Name string
|
||||
}
|
||||
|
||||
func (File) Attr(attr *fuse.Attr) {
|
||||
}
|
||||
func (File) ReadAll(ctx context.Context) ([]byte, error) {
|
||||
return []byte("hello, world\n"), nil
|
||||
}
|
||||
|
||||
type Dir struct {
|
||||
Path string
|
||||
Id uint64
|
||||
}
|
||||
|
||||
func (dir Dir) Attr(attr *fuse.Attr) {
|
||||
}
|
||||
|
||||
func (dir Dir) Lookup(ctx context.Context, name string) (fs.Node, error) {
|
||||
files_result, e := filer.ListFiles(*mountOptions.filer, dir.Path, name)
|
||||
if e != nil {
|
||||
return nil, fuse.ENOENT
|
||||
}
|
||||
if len(files_result.Files) > 0 {
|
||||
return File{files_result.Files[0].Id, files_result.Files[0].Name}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("File Not Found for %s", name)
|
||||
}
|
||||
|
||||
type WFS struct{}
|
||||
|
||||
func (WFS) Root() (fs.Node, error) {
|
||||
return Dir{}, nil
|
||||
}
|
||||
|
||||
func (dir *Dir) ReadDir(ctx context.Context) ([]fuse.Dirent, error) {
|
||||
var ret []fuse.Dirent
|
||||
if dirs, e := filer.ListDirectories(*mountOptions.filer, dir.Path); e == nil {
|
||||
for _, d := range dirs.Directories {
|
||||
dirId := uint64(d.Id)
|
||||
ret = append(ret, fuse.Dirent{Inode: dirId, Name: d.Name, Type: fuse.DT_Dir})
|
||||
}
|
||||
}
|
||||
if files, e := filer.ListFiles(*mountOptions.filer, dir.Path, ""); e == nil {
|
||||
for _, f := range files.Files {
|
||||
if fileId, e := storage.ParseFileId(string(f.Id)); e == nil {
|
||||
fileInode := uint64(fileId.VolumeId)<<48 + fileId.Key
|
||||
ret = append(ret, fuse.Dirent{Inode: fileInode, Name: f.Name, Type: fuse.DT_File})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/storage"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
"github.com/chrislusf/weed-fs/go/weed/weed_server"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type ServerOptions struct {
|
||||
cpuprofile *string
|
||||
}
|
||||
|
||||
var (
|
||||
serverOptions ServerOptions
|
||||
filerOptions FilerOptions
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdServer.Run = runServer // break init cycle
|
||||
}
|
||||
|
||||
var cmdServer = &Command{
|
||||
UsageLine: "server -port=8080 -dir=/tmp -volume.max=5 -ip=server_name",
|
||||
Short: "start a server, including volume server, and automatically elect a master server",
|
||||
Long: `start both a volume server to provide storage spaces
|
||||
and a master server to provide volume=>location mapping service and sequence number of file ids
|
||||
|
||||
This is provided as a convenient way to start both volume server and master server.
|
||||
The servers are exactly the same as starting them separately.
|
||||
|
||||
So other volume servers can use this embedded master server also.
|
||||
|
||||
Optionally, one filer server can be started. Logically, filer servers should not be in a cluster.
|
||||
They run with meta data on disk, not shared. So each filer server is different.
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
var (
|
||||
serverIp = cmdServer.Flag.String("ip", "localhost", "ip or server name")
|
||||
serverPublicUrl = cmdServer.Flag.String("publicUrl", "", "publicly accessible address")
|
||||
serverBindIp = cmdServer.Flag.String("ip.bind", "0.0.0.0", "ip address to bind to")
|
||||
serverMaxCpu = cmdServer.Flag.Int("maxCpu", 0, "maximum number of CPUs. 0 means all available CPUs")
|
||||
serverTimeout = cmdServer.Flag.Int("idleTimeout", 10, "connection idle seconds")
|
||||
serverDataCenter = cmdServer.Flag.String("dataCenter", "", "current volume server's data center name")
|
||||
serverRack = cmdServer.Flag.String("rack", "", "current volume server's rack name")
|
||||
serverWhiteListOption = cmdServer.Flag.String("whiteList", "", "comma separated Ip addresses having write permission. No limit if empty.")
|
||||
serverPeers = cmdServer.Flag.String("master.peers", "", "other master nodes in comma separated ip:masterPort list")
|
||||
serverSecureKey = cmdServer.Flag.String("secure.secret", "", "secret to encrypt Json Web Token(JWT)")
|
||||
serverGarbageThreshold = cmdServer.Flag.String("garbageThreshold", "0.3", "threshold to vacuum and reclaim spaces")
|
||||
masterPort = cmdServer.Flag.Int("master.port", 9333, "master server http listen port")
|
||||
masterMetaFolder = cmdServer.Flag.String("master.dir", "", "data directory to store meta data, default to same as -dir specified")
|
||||
masterVolumeSizeLimitMB = cmdServer.Flag.Uint("master.volumeSizeLimitMB", 30*1000, "Master stops directing writes to oversized volumes.")
|
||||
masterConfFile = cmdServer.Flag.String("master.conf", "/etc/weedfs/weedfs.conf", "xml configuration file")
|
||||
masterDefaultReplicaPlacement = cmdServer.Flag.String("master.defaultReplicaPlacement", "000", "Default replication type if not specified.")
|
||||
volumePort = cmdServer.Flag.Int("volume.port", 8080, "volume server http listen port")
|
||||
volumePublicPort = cmdServer.Flag.Int("volume.port.public", 0, "volume server public port")
|
||||
volumeDataFolders = cmdServer.Flag.String("dir", os.TempDir(), "directories to store data files. dir[,dir]...")
|
||||
volumeMaxDataVolumeCounts = cmdServer.Flag.String("volume.max", "7", "maximum numbers of volumes, count[,count]...")
|
||||
volumePulse = cmdServer.Flag.Int("pulseSeconds", 5, "number of seconds between heartbeats")
|
||||
volumeIndexType = cmdServer.Flag.String("volume.index", "memory", "Choose [memory|leveldb|boltdb] mode for memory~performance balance.")
|
||||
volumeFixJpgOrientation = cmdServer.Flag.Bool("volume.images.fix.orientation", true, "Adjust jpg orientation when uploading.")
|
||||
isStartingFiler = cmdServer.Flag.Bool("filer", false, "whether to start filer")
|
||||
|
||||
serverWhiteList []string
|
||||
)
|
||||
|
||||
func init() {
|
||||
serverOptions.cpuprofile = cmdServer.Flag.String("cpuprofile", "", "cpu profile output file")
|
||||
filerOptions.master = cmdServer.Flag.String("filer.master", "", "default to current master server")
|
||||
filerOptions.collection = cmdServer.Flag.String("filer.collection", "", "all data will be stored in this collection")
|
||||
filerOptions.port = cmdServer.Flag.Int("filer.port", 8888, "filer server http listen port")
|
||||
filerOptions.dir = cmdServer.Flag.String("filer.dir", "", "directory to store meta data, default to a 'filer' sub directory of what -mdir is specified")
|
||||
filerOptions.defaultReplicaPlacement = cmdServer.Flag.String("filer.defaultReplicaPlacement", "", "Default replication type if not specified during runtime.")
|
||||
filerOptions.redirectOnRead = cmdServer.Flag.Bool("filer.redirectOnRead", false, "whether proxy or redirect to volume server during file GET request")
|
||||
filerOptions.disableDirListing = cmdServer.Flag.Bool("filer.disableDirListing", false, "turn off directory listing")
|
||||
filerOptions.cassandra_server = cmdServer.Flag.String("filer.cassandra.server", "", "host[:port] of the cassandra server")
|
||||
filerOptions.cassandra_keyspace = cmdServer.Flag.String("filer.cassandra.keyspace", "seaweed", "keyspace of the cassandra server")
|
||||
filerOptions.redis_server = cmdServer.Flag.String("filer.redis.server", "", "host:port of the redis server, e.g., 127.0.0.1:6379")
|
||||
filerOptions.redis_database = cmdServer.Flag.Int("filer.redis.database", 0, "the database on the redis server")
|
||||
}
|
||||
|
||||
func runServer(cmd *Command, args []string) bool {
|
||||
filerOptions.secretKey = serverSecureKey
|
||||
if *serverOptions.cpuprofile != "" {
|
||||
f, err := os.Create(*serverOptions.cpuprofile)
|
||||
if err != nil {
|
||||
glog.Fatal(err)
|
||||
}
|
||||
pprof.StartCPUProfile(f)
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
if *filerOptions.redirectOnRead {
|
||||
*isStartingFiler = true
|
||||
}
|
||||
|
||||
*filerOptions.master = *serverIp + ":" + strconv.Itoa(*masterPort)
|
||||
|
||||
if *filerOptions.defaultReplicaPlacement == "" {
|
||||
*filerOptions.defaultReplicaPlacement = *masterDefaultReplicaPlacement
|
||||
}
|
||||
|
||||
if *volumePublicPort == 0 {
|
||||
*volumePublicPort = *volumePort
|
||||
}
|
||||
|
||||
if *serverMaxCpu < 1 {
|
||||
*serverMaxCpu = runtime.NumCPU()
|
||||
}
|
||||
runtime.GOMAXPROCS(*serverMaxCpu)
|
||||
|
||||
folders := strings.Split(*volumeDataFolders, ",")
|
||||
maxCountStrings := strings.Split(*volumeMaxDataVolumeCounts, ",")
|
||||
var maxCounts []int
|
||||
for _, maxString := range maxCountStrings {
|
||||
if max, e := strconv.Atoi(maxString); e == nil {
|
||||
maxCounts = append(maxCounts, max)
|
||||
} else {
|
||||
glog.Fatalf("The max specified in -max not a valid number %s", maxString)
|
||||
}
|
||||
}
|
||||
if len(folders) != len(maxCounts) {
|
||||
glog.Fatalf("%d directories by -dir, but only %d max is set by -max", len(folders), len(maxCounts))
|
||||
}
|
||||
for _, folder := range folders {
|
||||
if err := util.TestFolderWritable(folder); err != nil {
|
||||
glog.Fatalf("Check Data Folder(-dir) Writable %s : %s", folder, err)
|
||||
}
|
||||
}
|
||||
|
||||
if *masterMetaFolder == "" {
|
||||
*masterMetaFolder = folders[0]
|
||||
}
|
||||
if *isStartingFiler {
|
||||
if *filerOptions.dir == "" {
|
||||
*filerOptions.dir = *masterMetaFolder + "/filer"
|
||||
os.MkdirAll(*filerOptions.dir, 0700)
|
||||
}
|
||||
if err := util.TestFolderWritable(*filerOptions.dir); err != nil {
|
||||
glog.Fatalf("Check Mapping Meta Folder (-filer.dir=\"%s\") Writable: %s", *filerOptions.dir, err)
|
||||
}
|
||||
}
|
||||
if err := util.TestFolderWritable(*masterMetaFolder); err != nil {
|
||||
glog.Fatalf("Check Meta Folder (-mdir=\"%s\") Writable: %s", *masterMetaFolder, err)
|
||||
}
|
||||
|
||||
if *serverWhiteListOption != "" {
|
||||
serverWhiteList = strings.Split(*serverWhiteListOption, ",")
|
||||
}
|
||||
|
||||
if *isStartingFiler {
|
||||
go func() {
|
||||
r := http.NewServeMux()
|
||||
_, nfs_err := weed_server.NewFilerServer(r, *filerOptions.port, *filerOptions.master, *filerOptions.dir, *filerOptions.collection,
|
||||
*filerOptions.defaultReplicaPlacement,
|
||||
*filerOptions.redirectOnRead, *filerOptions.disableDirListing,
|
||||
*filerOptions.secretKey,
|
||||
"", "",
|
||||
"", 0,
|
||||
)
|
||||
if nfs_err != nil {
|
||||
glog.Fatalf("Filer startup error: %v", nfs_err)
|
||||
}
|
||||
glog.V(0).Infoln("Start Seaweed Filer", util.VERSION, "at port", strconv.Itoa(*filerOptions.port))
|
||||
filerListener, e := util.NewListener(
|
||||
":"+strconv.Itoa(*filerOptions.port),
|
||||
time.Duration(10)*time.Second,
|
||||
)
|
||||
if e != nil {
|
||||
glog.Fatalf("Filer listener error: %v", e)
|
||||
}
|
||||
if e := http.Serve(filerListener, r); e != nil {
|
||||
glog.Fatalf("Filer Fail to serve: %v", e)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var raftWaitForMaster sync.WaitGroup
|
||||
var volumeWait sync.WaitGroup
|
||||
|
||||
raftWaitForMaster.Add(1)
|
||||
volumeWait.Add(1)
|
||||
|
||||
go func() {
|
||||
r := mux.NewRouter()
|
||||
ms := weed_server.NewMasterServer(r, *masterPort, *masterMetaFolder,
|
||||
*masterVolumeSizeLimitMB, *volumePulse, *masterConfFile, *masterDefaultReplicaPlacement, *serverGarbageThreshold,
|
||||
serverWhiteList, *serverSecureKey,
|
||||
)
|
||||
|
||||
glog.V(0).Infoln("Start Seaweed Master", util.VERSION, "at", *serverIp+":"+strconv.Itoa(*masterPort))
|
||||
masterListener, e := util.NewListener(*serverBindIp+":"+strconv.Itoa(*masterPort), time.Duration(*serverTimeout)*time.Second)
|
||||
if e != nil {
|
||||
glog.Fatalf("Master startup error: %v", e)
|
||||
}
|
||||
|
||||
go func() {
|
||||
raftWaitForMaster.Wait()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
myAddress := *serverIp + ":" + strconv.Itoa(*masterPort)
|
||||
var peers []string
|
||||
if *serverPeers != "" {
|
||||
peers = strings.Split(*serverPeers, ",")
|
||||
}
|
||||
raftServer := weed_server.NewRaftServer(r, peers, myAddress, *masterMetaFolder, ms.Topo, *volumePulse)
|
||||
ms.SetRaftServer(raftServer)
|
||||
volumeWait.Done()
|
||||
}()
|
||||
|
||||
raftWaitForMaster.Done()
|
||||
if e := http.Serve(masterListener, r); e != nil {
|
||||
glog.Fatalf("Master Fail to serve:%s", e.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
volumeWait.Wait()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if *volumePublicPort == 0 {
|
||||
*volumePublicPort = *volumePort
|
||||
}
|
||||
if *serverPublicUrl == "" {
|
||||
*serverPublicUrl = *serverIp + ":" + strconv.Itoa(*volumePublicPort)
|
||||
}
|
||||
isSeperatedPublicPort := *volumePublicPort != *volumePort
|
||||
volumeMux := http.NewServeMux()
|
||||
publicVolumeMux := volumeMux
|
||||
if isSeperatedPublicPort {
|
||||
publicVolumeMux = http.NewServeMux()
|
||||
}
|
||||
volumeNeedleMapKind := storage.NeedleMapInMemory
|
||||
switch *volumeIndexType {
|
||||
case "leveldb":
|
||||
volumeNeedleMapKind = storage.NeedleMapLevelDb
|
||||
case "boltdb":
|
||||
volumeNeedleMapKind = storage.NeedleMapBoltDb
|
||||
}
|
||||
volumeServer := weed_server.NewVolumeServer(volumeMux, publicVolumeMux,
|
||||
*serverIp, *volumePort, *serverPublicUrl,
|
||||
folders, maxCounts,
|
||||
volumeNeedleMapKind,
|
||||
*serverIp+":"+strconv.Itoa(*masterPort), *volumePulse, *serverDataCenter, *serverRack,
|
||||
serverWhiteList, *volumeFixJpgOrientation,
|
||||
)
|
||||
|
||||
glog.V(0).Infoln("Start Seaweed volume server", util.VERSION, "at", *serverIp+":"+strconv.Itoa(*volumePort))
|
||||
volumeListener, eListen := util.NewListener(
|
||||
*serverBindIp+":"+strconv.Itoa(*volumePort),
|
||||
time.Duration(*serverTimeout)*time.Second,
|
||||
)
|
||||
if eListen != nil {
|
||||
glog.Fatalf("Volume server listener error: %v", eListen)
|
||||
}
|
||||
if isSeperatedPublicPort {
|
||||
publicListeningAddress := *serverIp + ":" + strconv.Itoa(*volumePublicPort)
|
||||
glog.V(0).Infoln("Start Seaweed volume server", util.VERSION, "public at", publicListeningAddress)
|
||||
publicListener, e := util.NewListener(publicListeningAddress, time.Duration(*serverTimeout)*time.Second)
|
||||
if e != nil {
|
||||
glog.Fatalf("Volume server listener error:%v", e)
|
||||
}
|
||||
go func() {
|
||||
if e := http.Serve(publicListener, publicVolumeMux); e != nil {
|
||||
glog.Fatalf("Volume server fail to serve public: %v", e)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
OnInterrupt(func() {
|
||||
volumeServer.Shutdown()
|
||||
pprof.StopCPUProfile()
|
||||
})
|
||||
|
||||
if e := http.Serve(volumeListener, volumeMux); e != nil {
|
||||
glog.Fatalf("Volume server fail to serve:%v", e)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// +build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func OnInterrupt(fn func()) {
|
||||
// deal with control+c,etc
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan,
|
||||
os.Interrupt,
|
||||
os.Kill,
|
||||
syscall.SIGALRM,
|
||||
syscall.SIGHUP,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
syscall.SIGQUIT)
|
||||
go func() {
|
||||
for _ = range signalChan {
|
||||
fn()
|
||||
os.Exit(0)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/operation"
|
||||
"github.com/chrislusf/weed-fs/go/security"
|
||||
)
|
||||
|
||||
var (
|
||||
uploadReplication *string
|
||||
uploadCollection *string
|
||||
uploadDir *string
|
||||
uploadTtl *string
|
||||
include *string
|
||||
uploadSecretKey *string
|
||||
maxMB *int
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdUpload.Run = runUpload // break init cycle
|
||||
cmdUpload.IsDebug = cmdUpload.Flag.Bool("debug", false, "verbose debug information")
|
||||
server = cmdUpload.Flag.String("server", "localhost:9333", "weedfs master location")
|
||||
uploadDir = cmdUpload.Flag.String("dir", "", "Upload the whole folder recursively if specified.")
|
||||
include = cmdUpload.Flag.String("include", "", "pattens of files to upload, e.g., *.pdf, *.html, ab?d.txt, works together with -dir")
|
||||
uploadReplication = cmdUpload.Flag.String("replication", "", "replication type")
|
||||
uploadCollection = cmdUpload.Flag.String("collection", "", "optional collection name")
|
||||
uploadTtl = cmdUpload.Flag.String("ttl", "", "time to live, e.g.: 1m, 1h, 1d, 1M, 1y")
|
||||
maxMB = cmdUpload.Flag.Int("maxMB", 0, "split files larger than the limit")
|
||||
uploadSecretKey = cmdUpload.Flag.String("secure.secret", "", "secret to encrypt Json Web Token(JWT)")
|
||||
}
|
||||
|
||||
var cmdUpload = &Command{
|
||||
UsageLine: "upload -server=localhost:9333 file1 [file2 file3]\n upload -server=localhost:9333 -dir=one_directory -include=*.pdf",
|
||||
Short: "upload one or a list of files",
|
||||
Long: `upload one or a list of files, or batch upload one whole folder recursively.
|
||||
|
||||
If uploading a list of files:
|
||||
It uses consecutive file keys for the list of files.
|
||||
e.g. If the file1 uses key k, file2 can be read via k_1
|
||||
|
||||
If uploading a whole folder recursively:
|
||||
All files under the folder and subfolders will be uploaded, each with its own file key.
|
||||
Optional parameter "-include" allows you to specify the file name patterns.
|
||||
|
||||
If any file has a ".gz" extension, the content are considered gzipped already, and will be stored as is.
|
||||
This can save volume server's gzipped processing and allow customizable gzip compression level.
|
||||
The file name will strip out ".gz" and stored. For example, "jquery.js.gz" will be stored as "jquery.js".
|
||||
|
||||
If "maxMB" is set to a positive number, files larger than it would be split into chunks and uploaded separatedly.
|
||||
The list of file ids of those chunks would be stored in an additional chunk, and this additional chunk's file id would be returned.
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
func runUpload(cmd *Command, args []string) bool {
|
||||
secret := security.Secret(*uploadSecretKey)
|
||||
if len(cmdUpload.Flag.Args()) == 0 {
|
||||
if *uploadDir == "" {
|
||||
return false
|
||||
}
|
||||
filepath.Walk(*uploadDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err == nil {
|
||||
if !info.IsDir() {
|
||||
if *include != "" {
|
||||
if ok, _ := filepath.Match(*include, filepath.Base(path)); !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
parts, e := operation.NewFileParts([]string{path})
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
results, e := operation.SubmitFiles(*server, parts,
|
||||
*uploadReplication, *uploadCollection,
|
||||
*uploadTtl, *maxMB, secret)
|
||||
bytes, _ := json.Marshal(results)
|
||||
fmt.Println(string(bytes))
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
} else {
|
||||
parts, e := operation.NewFileParts(args)
|
||||
if e != nil {
|
||||
fmt.Println(e.Error())
|
||||
}
|
||||
results, _ := operation.SubmitFiles(*server, parts,
|
||||
*uploadReplication, *uploadCollection,
|
||||
*uploadTtl, *maxMB, secret)
|
||||
bytes, _ := json.Marshal(results)
|
||||
fmt.Println(string(bytes))
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/filer"
|
||||
"github.com/chrislusf/weed-fs/go/filer/cassandra_store"
|
||||
"github.com/chrislusf/weed-fs/go/filer/embedded_filer"
|
||||
"github.com/chrislusf/weed-fs/go/filer/flat_namespace"
|
||||
"github.com/chrislusf/weed-fs/go/filer/redis_store"
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/security"
|
||||
)
|
||||
|
||||
type FilerServer struct {
|
||||
port string
|
||||
master string
|
||||
collection string
|
||||
defaultReplication string
|
||||
redirectOnRead bool
|
||||
disableDirListing bool
|
||||
secret security.Secret
|
||||
filer filer.Filer
|
||||
}
|
||||
|
||||
func NewFilerServer(r *http.ServeMux, port int, master string, dir string, collection string,
|
||||
replication string, redirectOnRead bool, disableDirListing bool,
|
||||
secret string,
|
||||
cassandra_server string, cassandra_keyspace string,
|
||||
redis_server string, redis_database int,
|
||||
) (fs *FilerServer, err error) {
|
||||
fs = &FilerServer{
|
||||
master: master,
|
||||
collection: collection,
|
||||
defaultReplication: replication,
|
||||
redirectOnRead: redirectOnRead,
|
||||
disableDirListing: disableDirListing,
|
||||
port: ":" + strconv.Itoa(port),
|
||||
}
|
||||
|
||||
if cassandra_server != "" {
|
||||
cassandra_store, err := cassandra_store.NewCassandraStore(cassandra_keyspace, cassandra_server)
|
||||
if err != nil {
|
||||
glog.Fatalf("Can not connect to cassandra server %s with keyspace %s: %v", cassandra_server, cassandra_keyspace, err)
|
||||
}
|
||||
fs.filer = flat_namespace.NewFlatNamesapceFiler(master, cassandra_store)
|
||||
} else if redis_server != "" {
|
||||
redis_store := redis_store.NewRedisStore(redis_server, redis_database)
|
||||
fs.filer = flat_namespace.NewFlatNamesapceFiler(master, redis_store)
|
||||
} else {
|
||||
if fs.filer, err = embedded_filer.NewFilerEmbedded(master, dir); err != nil {
|
||||
glog.Fatalf("Can not start filer in dir %s : %v", dir, err)
|
||||
return
|
||||
}
|
||||
|
||||
r.HandleFunc("/admin/mv", fs.moveHandler)
|
||||
}
|
||||
|
||||
r.HandleFunc("/", fs.filerHandler)
|
||||
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
func (fs *FilerServer) jwt(fileId string) security.EncodedJwt {
|
||||
return security.GenJwt(fs.secret, fileId)
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/operation"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
func (fs *FilerServer) filerHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
fs.GetOrHeadHandler(w, r, true)
|
||||
case "HEAD":
|
||||
fs.GetOrHeadHandler(w, r, false)
|
||||
case "DELETE":
|
||||
fs.DeleteHandler(w, r)
|
||||
case "PUT":
|
||||
fs.PostHandler(w, r)
|
||||
case "POST":
|
||||
fs.PostHandler(w, r)
|
||||
}
|
||||
}
|
||||
func (fs *FilerServer) listDirectoryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasSuffix(r.URL.Path, "/") {
|
||||
return
|
||||
}
|
||||
dirlist, err := fs.filer.ListDirectories(r.URL.Path)
|
||||
if err == leveldb.ErrNotFound {
|
||||
glog.V(3).Infoln("Directory Not Found in db", r.URL.Path)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
m["Directory"] = r.URL.Path
|
||||
m["Subdirectories"] = dirlist
|
||||
lastFileName := r.FormValue("lastFileName")
|
||||
limit, limit_err := strconv.Atoi(r.FormValue("limit"))
|
||||
if limit_err != nil {
|
||||
limit = 100
|
||||
}
|
||||
m["Files"], _ = fs.filer.ListFiles(r.URL.Path, lastFileName, limit)
|
||||
writeJsonQuiet(w, r, http.StatusOK, m)
|
||||
}
|
||||
func (fs *FilerServer) GetOrHeadHandler(w http.ResponseWriter, r *http.Request, isGetMethod bool) {
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
if fs.disableDirListing {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
fs.listDirectoryHandler(w, r)
|
||||
return
|
||||
}
|
||||
fileId, err := fs.filer.FindFile(r.URL.Path)
|
||||
if err == leveldb.ErrNotFound {
|
||||
glog.V(3).Infoln("Not found in db", r.URL.Path)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
parts := strings.Split(fileId, ",")
|
||||
if len(parts) != 2 {
|
||||
glog.V(1).Infoln("Invalid fileId", fileId)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
lookup, lookupError := operation.Lookup(fs.master, parts[0])
|
||||
if lookupError != nil {
|
||||
glog.V(1).Infoln("Invalid lookup", lookupError.Error())
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if len(lookup.Locations) == 0 {
|
||||
glog.V(1).Infoln("Can not find location for volume", parts[0])
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
urlLocation := lookup.Locations[rand.Intn(len(lookup.Locations))].Url
|
||||
urlString := "http://" + urlLocation + "/" + fileId
|
||||
if fs.redirectOnRead {
|
||||
http.Redirect(w, r, urlString, http.StatusFound)
|
||||
return
|
||||
}
|
||||
u, _ := url.Parse(urlString)
|
||||
request := &http.Request{
|
||||
Method: r.Method,
|
||||
URL: u,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
Header: r.Header,
|
||||
Body: r.Body,
|
||||
Host: r.Host,
|
||||
ContentLength: r.ContentLength,
|
||||
}
|
||||
glog.V(3).Infoln("retrieving from", u)
|
||||
resp, do_err := util.Do(request)
|
||||
if do_err != nil {
|
||||
glog.V(0).Infoln("failing to connect to volume server", do_err.Error())
|
||||
writeJsonError(w, r, http.StatusInternalServerError, do_err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
for k, v := range resp.Header {
|
||||
w.Header()[k] = v
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
||||
|
||||
func (fs *FilerServer) PostHandler(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
replication := query.Get("replication")
|
||||
if replication == "" {
|
||||
replication = fs.defaultReplication
|
||||
}
|
||||
assignResult, ae := operation.Assign(fs.master, 1, replication, fs.collection, query.Get("ttl"))
|
||||
if ae != nil {
|
||||
glog.V(0).Infoln("failing to assign a file id", ae.Error())
|
||||
writeJsonError(w, r, http.StatusInternalServerError, ae)
|
||||
return
|
||||
}
|
||||
|
||||
u, _ := url.Parse("http://" + assignResult.Url + "/" + assignResult.Fid)
|
||||
glog.V(4).Infoln("post to", u)
|
||||
request := &http.Request{
|
||||
Method: r.Method,
|
||||
URL: u,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
Header: r.Header,
|
||||
Body: r.Body,
|
||||
Host: r.Host,
|
||||
ContentLength: r.ContentLength,
|
||||
}
|
||||
resp, do_err := util.Do(request)
|
||||
if do_err != nil {
|
||||
glog.V(0).Infoln("failing to connect to volume server", r.RequestURI, do_err.Error())
|
||||
writeJsonError(w, r, http.StatusInternalServerError, do_err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
resp_body, ra_err := ioutil.ReadAll(resp.Body)
|
||||
if ra_err != nil {
|
||||
glog.V(0).Infoln("failing to upload to volume server", r.RequestURI, ra_err.Error())
|
||||
writeJsonError(w, r, http.StatusInternalServerError, ra_err)
|
||||
return
|
||||
}
|
||||
glog.V(4).Infoln("post result", string(resp_body))
|
||||
var ret operation.UploadResult
|
||||
unmarshal_err := json.Unmarshal(resp_body, &ret)
|
||||
if unmarshal_err != nil {
|
||||
glog.V(0).Infoln("failing to read upload resonse", r.RequestURI, string(resp_body))
|
||||
writeJsonError(w, r, http.StatusInternalServerError, unmarshal_err)
|
||||
return
|
||||
}
|
||||
if ret.Error != "" {
|
||||
glog.V(0).Infoln("failing to post to volume server", r.RequestURI, ret.Error)
|
||||
writeJsonError(w, r, http.StatusInternalServerError, errors.New(ret.Error))
|
||||
return
|
||||
}
|
||||
path := r.URL.Path
|
||||
if strings.HasSuffix(path, "/") {
|
||||
if ret.Name != "" {
|
||||
path += ret.Name
|
||||
} else {
|
||||
operation.DeleteFile(fs.master, assignResult.Fid, fs.jwt(assignResult.Fid)) //clean up
|
||||
glog.V(0).Infoln("Can not to write to folder", path, "without a file name!")
|
||||
writeJsonError(w, r, http.StatusInternalServerError,
|
||||
errors.New("Can not to write to folder "+path+" without a file name"))
|
||||
return
|
||||
}
|
||||
}
|
||||
glog.V(4).Infoln("saving", path, "=>", assignResult.Fid)
|
||||
if db_err := fs.filer.CreateFile(path, assignResult.Fid); db_err != nil {
|
||||
operation.DeleteFile(fs.master, assignResult.Fid, fs.jwt(assignResult.Fid)) //clean up
|
||||
glog.V(0).Infof("failing to write %s to filer server : %v", path, db_err)
|
||||
writeJsonError(w, r, http.StatusInternalServerError, db_err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write(resp_body)
|
||||
}
|
||||
|
||||
// curl -X DELETE http://localhost:8888/path/to
|
||||
// curl -X DELETE http://localhost:8888/path/to?recursive=true
|
||||
func (fs *FilerServer) DeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
var fid string
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
isRecursive := r.FormValue("recursive") == "true"
|
||||
err = fs.filer.DeleteDirectory(r.URL.Path, isRecursive)
|
||||
} else {
|
||||
fid, err = fs.filer.DeleteFile(r.URL.Path)
|
||||
if err == nil {
|
||||
err = operation.DeleteFile(fs.master, fid, fs.jwt(fid))
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
writeJsonQuiet(w, r, http.StatusAccepted, map[string]string{"error": ""})
|
||||
} else {
|
||||
glog.V(4).Infoln("deleting", r.URL.Path, ":", err.Error())
|
||||
writeJsonError(w, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
)
|
||||
|
||||
/*
|
||||
Move a folder or a file, with 4 Use cases:
|
||||
mv fromDir toNewDir
|
||||
mv fromDir toOldDir
|
||||
mv fromFile toDir
|
||||
mv fromFile toFile
|
||||
|
||||
Wildcard is not supported.
|
||||
|
||||
*/
|
||||
func (fs *FilerServer) moveHandler(w http.ResponseWriter, r *http.Request) {
|
||||
from := r.FormValue("from")
|
||||
to := r.FormValue("to")
|
||||
err := fs.filer.Move(from, to)
|
||||
if err != nil {
|
||||
glog.V(4).Infoln("moving", from, "->", to, err.Error())
|
||||
writeJsonError(w, r, http.StatusInternalServerError, err)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/stats"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
ui "github.com/chrislusf/weed-fs/go/weed/weed_server/master_ui"
|
||||
)
|
||||
|
||||
func (ms *MasterServer) uiStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
infos := make(map[string]interface{})
|
||||
infos["Version"] = util.VERSION
|
||||
args := struct {
|
||||
Version string
|
||||
Topology interface{}
|
||||
Leader string
|
||||
Peers interface{}
|
||||
Stats map[string]interface{}
|
||||
Counters *stats.ServerStats
|
||||
}{
|
||||
util.VERSION,
|
||||
ms.Topo.ToMap(),
|
||||
ms.Topo.RaftServer.Leader(),
|
||||
ms.Topo.RaftServer.Peers(),
|
||||
infos,
|
||||
serverStats,
|
||||
}
|
||||
ui.StatusTpl.Execute(w, args)
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/topology"
|
||||
"github.com/goraft/raft"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type RaftServer struct {
|
||||
peers []string // initial peers to join with
|
||||
raftServer raft.Server
|
||||
dataDir string
|
||||
httpAddr string
|
||||
router *mux.Router
|
||||
topo *topology.Topology
|
||||
}
|
||||
|
||||
func NewRaftServer(r *mux.Router, peers []string, httpAddr string, dataDir string, topo *topology.Topology, pulseSeconds int) *RaftServer {
|
||||
s := &RaftServer{
|
||||
peers: peers,
|
||||
httpAddr: httpAddr,
|
||||
dataDir: dataDir,
|
||||
router: r,
|
||||
topo: topo,
|
||||
}
|
||||
|
||||
if glog.V(4) {
|
||||
raft.SetLogLevel(2)
|
||||
}
|
||||
|
||||
raft.RegisterCommand(&topology.MaxVolumeIdCommand{})
|
||||
|
||||
var err error
|
||||
transporter := raft.NewHTTPTransporter("/cluster", 0)
|
||||
transporter.Transport.MaxIdleConnsPerHost = 1024
|
||||
glog.V(1).Infof("Starting RaftServer with IP:%v:", httpAddr)
|
||||
|
||||
// Clear old cluster configurations if peers are set
|
||||
if len(s.peers) > 0 {
|
||||
os.RemoveAll(path.Join(s.dataDir, "conf"))
|
||||
os.RemoveAll(path.Join(s.dataDir, "log"))
|
||||
os.RemoveAll(path.Join(s.dataDir, "snapshot"))
|
||||
}
|
||||
|
||||
s.raftServer, err = raft.NewServer(s.httpAddr, s.dataDir, transporter, nil, topo, "")
|
||||
if err != nil {
|
||||
glog.V(0).Infoln(err)
|
||||
return nil
|
||||
}
|
||||
transporter.Install(s.raftServer, s)
|
||||
s.raftServer.SetHeartbeatInterval(1 * time.Second)
|
||||
s.raftServer.SetElectionTimeout(time.Duration(pulseSeconds) * 3450 * time.Millisecond)
|
||||
s.raftServer.Start()
|
||||
|
||||
s.router.HandleFunc("/cluster/join", s.joinHandler).Methods("POST")
|
||||
s.router.HandleFunc("/cluster/status", s.statusHandler).Methods("GET")
|
||||
|
||||
if len(s.peers) > 0 {
|
||||
// Join to leader if specified.
|
||||
glog.V(0).Infoln("Joining cluster:", strings.Join(s.peers, ","))
|
||||
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
|
||||
firstJoinError := s.Join(s.peers)
|
||||
if firstJoinError != nil {
|
||||
glog.V(0).Infoln("No existing server found. Starting as leader in the new cluster.")
|
||||
_, err := s.raftServer.Do(&raft.DefaultJoinCommand{
|
||||
Name: s.raftServer.Name(),
|
||||
ConnectionString: "http://" + s.httpAddr,
|
||||
})
|
||||
if err != nil {
|
||||
glog.V(0).Infoln(err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else if s.raftServer.IsLogEmpty() {
|
||||
// Initialize the server by joining itself.
|
||||
glog.V(0).Infoln("Initializing new cluster")
|
||||
|
||||
_, err := s.raftServer.Do(&raft.DefaultJoinCommand{
|
||||
Name: s.raftServer.Name(),
|
||||
ConnectionString: "http://" + s.httpAddr,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
glog.V(0).Infoln(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
} else {
|
||||
glog.V(0).Infoln("Old conf,log,snapshot should have been removed.")
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *RaftServer) Peers() (members []string) {
|
||||
peers := s.raftServer.Peers()
|
||||
|
||||
for _, p := range peers {
|
||||
members = append(members, strings.TrimPrefix(p.ConnectionString, "http://"))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Join joins an existing cluster.
|
||||
func (s *RaftServer) Join(peers []string) error {
|
||||
command := &raft.DefaultJoinCommand{
|
||||
Name: s.raftServer.Name(),
|
||||
ConnectionString: "http://" + s.httpAddr,
|
||||
}
|
||||
|
||||
var err error
|
||||
var b bytes.Buffer
|
||||
json.NewEncoder(&b).Encode(command)
|
||||
for _, m := range peers {
|
||||
if m == s.httpAddr {
|
||||
continue
|
||||
}
|
||||
target := fmt.Sprintf("http://%s/cluster/join", strings.TrimSpace(m))
|
||||
glog.V(0).Infoln("Attempting to connect to:", target)
|
||||
|
||||
err = postFollowingOneRedirect(target, "application/json", &b)
|
||||
|
||||
if err != nil {
|
||||
glog.V(0).Infoln("Post returned error: ", err.Error())
|
||||
if _, ok := err.(*url.Error); ok {
|
||||
// If we receive a network error try the next member
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("Could not connect to any cluster peers")
|
||||
}
|
||||
|
||||
// a workaround because http POST following redirection misses request body
|
||||
func postFollowingOneRedirect(target string, contentType string, b *bytes.Buffer) error {
|
||||
backupReader := bytes.NewReader(b.Bytes())
|
||||
resp, err := http.Post(target, contentType, b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
reply, _ := ioutil.ReadAll(resp.Body)
|
||||
statusCode := resp.StatusCode
|
||||
|
||||
if statusCode == http.StatusMovedPermanently {
|
||||
var urlStr string
|
||||
if urlStr = resp.Header.Get("Location"); urlStr == "" {
|
||||
return fmt.Errorf("%d response missing Location header", resp.StatusCode)
|
||||
}
|
||||
|
||||
glog.V(0).Infoln("Post redirected to ", urlStr)
|
||||
resp2, err2 := http.Post(urlStr, contentType, backupReader)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
defer resp2.Body.Close()
|
||||
reply, _ = ioutil.ReadAll(resp2.Body)
|
||||
statusCode = resp2.StatusCode
|
||||
}
|
||||
|
||||
glog.V(0).Infoln("Post returned status: ", statusCode, string(reply))
|
||||
if statusCode != http.StatusOK {
|
||||
return errors.New(string(reply))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/operation"
|
||||
"github.com/goraft/raft"
|
||||
)
|
||||
|
||||
// Handles incoming RAFT joins.
|
||||
func (s *RaftServer) joinHandler(w http.ResponseWriter, req *http.Request) {
|
||||
glog.V(0).Infoln("Processing incoming join. Current Leader", s.raftServer.Leader(), "Self", s.raftServer.Name(), "Peers", s.raftServer.Peers())
|
||||
command := &raft.DefaultJoinCommand{}
|
||||
|
||||
commandText, _ := ioutil.ReadAll(req.Body)
|
||||
glog.V(0).Info("Command:", string(commandText))
|
||||
if err := json.NewDecoder(strings.NewReader(string(commandText))).Decode(&command); err != nil {
|
||||
glog.V(0).Infoln("Error decoding json message:", err, string(commandText))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
glog.V(0).Infoln("join command from Name", command.Name, "Connection", command.ConnectionString)
|
||||
|
||||
if _, err := s.raftServer.Do(command); err != nil {
|
||||
switch err {
|
||||
case raft.NotLeaderError:
|
||||
s.redirectToLeader(w, req)
|
||||
default:
|
||||
glog.V(0).Infoln("Error processing join:", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RaftServer) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
|
||||
s.router.HandleFunc(pattern, handler)
|
||||
}
|
||||
|
||||
func (s *RaftServer) redirectToLeader(w http.ResponseWriter, req *http.Request) {
|
||||
if leader, e := s.topo.Leader(); e == nil {
|
||||
//http.StatusMovedPermanently does not cause http POST following redirection
|
||||
glog.V(0).Infoln("Redirecting to", http.StatusMovedPermanently, "http://"+leader+req.URL.Path)
|
||||
http.Redirect(w, req, "http://"+leader+req.URL.Path, http.StatusMovedPermanently)
|
||||
} else {
|
||||
glog.V(0).Infoln("Error: Leader Unknown")
|
||||
http.Error(w, "Leader unknown", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RaftServer) statusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ret := operation.ClusterStatusResult{
|
||||
IsLeader: s.topo.IsLeader(),
|
||||
Peers: s.Peers(),
|
||||
}
|
||||
if leader, e := s.topo.Leader(); e == nil {
|
||||
ret.Leader = leader
|
||||
}
|
||||
writeJsonQuiet(w, r, http.StatusOK, ret)
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/security"
|
||||
"github.com/chrislusf/weed-fs/go/storage"
|
||||
)
|
||||
|
||||
type VolumeServer struct {
|
||||
masterNode string
|
||||
mnLock sync.RWMutex
|
||||
pulseSeconds int
|
||||
dataCenter string
|
||||
rack string
|
||||
store *storage.Store
|
||||
guard *security.Guard
|
||||
|
||||
needleMapKind storage.NeedleMapType
|
||||
FixJpgOrientation bool
|
||||
}
|
||||
|
||||
func NewVolumeServer(adminMux, publicMux *http.ServeMux, ip string,
|
||||
port int, publicUrl string,
|
||||
folders []string, maxCounts []int,
|
||||
needleMapKind storage.NeedleMapType,
|
||||
masterNode string, pulseSeconds int,
|
||||
dataCenter string, rack string,
|
||||
whiteList []string,
|
||||
fixJpgOrientation bool) *VolumeServer {
|
||||
vs := &VolumeServer{
|
||||
pulseSeconds: pulseSeconds,
|
||||
dataCenter: dataCenter,
|
||||
rack: rack,
|
||||
needleMapKind: needleMapKind,
|
||||
FixJpgOrientation: fixJpgOrientation,
|
||||
}
|
||||
vs.SetMasterNode(masterNode)
|
||||
vs.store = storage.NewStore(port, ip, publicUrl, folders, maxCounts, vs.needleMapKind)
|
||||
|
||||
vs.guard = security.NewGuard(whiteList, "")
|
||||
|
||||
adminMux.HandleFunc("/ui/index.html", vs.uiStatusHandler)
|
||||
adminMux.HandleFunc("/status", vs.guard.WhiteList(vs.statusHandler))
|
||||
adminMux.HandleFunc("/admin/assign_volume", vs.guard.WhiteList(vs.assignVolumeHandler))
|
||||
adminMux.HandleFunc("/admin/vacuum_volume_check", vs.guard.WhiteList(vs.vacuumVolumeCheckHandler))
|
||||
adminMux.HandleFunc("/admin/vacuum_volume_compact", vs.guard.WhiteList(vs.vacuumVolumeCompactHandler))
|
||||
adminMux.HandleFunc("/admin/vacuum_volume_commit", vs.guard.WhiteList(vs.vacuumVolumeCommitHandler))
|
||||
adminMux.HandleFunc("/admin/delete_collection", vs.guard.WhiteList(vs.deleteCollectionHandler))
|
||||
adminMux.HandleFunc("/stats/counter", vs.guard.WhiteList(statsCounterHandler))
|
||||
adminMux.HandleFunc("/stats/memory", vs.guard.WhiteList(statsMemoryHandler))
|
||||
adminMux.HandleFunc("/stats/disk", vs.guard.WhiteList(vs.statsDiskHandler))
|
||||
adminMux.HandleFunc("/delete", vs.guard.WhiteList(vs.batchDeleteHandler))
|
||||
adminMux.HandleFunc("/", vs.privateStoreHandler)
|
||||
if publicMux != adminMux {
|
||||
// separated admin and public port
|
||||
publicMux.HandleFunc("/", vs.publicReadOnlyHandler)
|
||||
}
|
||||
|
||||
go func() {
|
||||
connected := true
|
||||
|
||||
vs.store.SetBootstrapMaster(vs.GetMasterNode())
|
||||
vs.store.SetDataCenter(vs.dataCenter)
|
||||
vs.store.SetRack(vs.rack)
|
||||
for {
|
||||
master, secretKey, err := vs.store.SendHeartbeatToMaster()
|
||||
if err == nil {
|
||||
if !connected {
|
||||
connected = true
|
||||
vs.SetMasterNode(master)
|
||||
vs.guard.SecretKey = secretKey
|
||||
glog.V(0).Infoln("Volume Server Connected with master at", master)
|
||||
}
|
||||
} else {
|
||||
glog.V(1).Infof("Volume Server Failed to talk with master %+v: %v", vs, err)
|
||||
if connected {
|
||||
connected = false
|
||||
}
|
||||
}
|
||||
if connected {
|
||||
time.Sleep(time.Duration(float32(vs.pulseSeconds*1e3)*(1+rand.Float32())) * time.Millisecond)
|
||||
} else {
|
||||
time.Sleep(time.Duration(float32(vs.pulseSeconds*1e3)*0.25) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return vs
|
||||
}
|
||||
|
||||
func (vs *VolumeServer) GetMasterNode() string {
|
||||
vs.mnLock.RLock()
|
||||
defer vs.mnLock.RUnlock()
|
||||
return vs.masterNode
|
||||
}
|
||||
|
||||
func (vs *VolumeServer) SetMasterNode(masterNode string) {
|
||||
vs.mnLock.Lock()
|
||||
defer vs.mnLock.Unlock()
|
||||
vs.masterNode = masterNode
|
||||
}
|
||||
|
||||
func (vs *VolumeServer) Shutdown() {
|
||||
glog.V(0).Infoln("Shutting down volume server...")
|
||||
vs.store.Close()
|
||||
glog.V(0).Infoln("Shut down successfully!")
|
||||
}
|
||||
|
||||
func (vs *VolumeServer) jwt(fileId string) security.EncodedJwt {
|
||||
return security.GenJwt(vs.guard.SecretKey, fileId)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/stats"
|
||||
"github.com/chrislusf/weed-fs/go/util"
|
||||
)
|
||||
|
||||
func (vs *VolumeServer) statusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
m := make(map[string]interface{})
|
||||
m["Version"] = util.VERSION
|
||||
m["Volumes"] = vs.store.Status()
|
||||
writeJsonQuiet(w, r, http.StatusOK, m)
|
||||
}
|
||||
|
||||
func (vs *VolumeServer) assignVolumeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := vs.store.AddVolume(r.FormValue("volume"), r.FormValue("collection"), vs.needleMapKind, r.FormValue("replication"), r.FormValue("ttl"))
|
||||
if err == nil {
|
||||
writeJsonQuiet(w, r, http.StatusAccepted, map[string]string{"error": ""})
|
||||
} else {
|
||||
writeJsonError(w, r, http.StatusNotAcceptable, err)
|
||||
}
|
||||
glog.V(2).Infoln("assign volume =", r.FormValue("volume"), ", collection =", r.FormValue("collection"), ", replication =", r.FormValue("replication"), ", error =", err)
|
||||
}
|
||||
|
||||
func (vs *VolumeServer) deleteCollectionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if "benchmark" != r.FormValue("collection") {
|
||||
glog.V(0).Infoln("deleting collection =", r.FormValue("collection"), "!!!")
|
||||
return
|
||||
}
|
||||
err := vs.store.DeleteCollection(r.FormValue("collection"))
|
||||
if err == nil {
|
||||
writeJsonQuiet(w, r, http.StatusOK, map[string]string{"error": ""})
|
||||
} else {
|
||||
writeJsonError(w, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
glog.V(2).Infoln("deleting collection =", r.FormValue("collection"), ", error =", err)
|
||||
}
|
||||
|
||||
func (vs *VolumeServer) statsDiskHandler(w http.ResponseWriter, r *http.Request) {
|
||||
m := make(map[string]interface{})
|
||||
m["Version"] = util.VERSION
|
||||
var ds []*stats.DiskStatus
|
||||
for _, loc := range vs.store.Locations {
|
||||
if dir, e := filepath.Abs(loc.Directory); e == nil {
|
||||
ds = append(ds, stats.NewDiskStatus(dir))
|
||||
}
|
||||
}
|
||||
m["DiskStatuses"] = ds
|
||||
writeJsonQuiet(w, r, http.StatusOK, m)
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/images"
|
||||
"github.com/chrislusf/weed-fs/go/operation"
|
||||
"github.com/chrislusf/weed-fs/go/storage"
|
||||
)
|
||||
|
||||
var fileNameEscaper = strings.NewReplacer("\\", "\\\\", "\"", "\\\"")
|
||||
|
||||
func (vs *VolumeServer) GetOrHeadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
n := new(storage.Needle)
|
||||
vid, fid, filename, ext, _ := parseURLPath(r.URL.Path)
|
||||
volumeId, err := storage.NewVolumeId(vid)
|
||||
if err != nil {
|
||||
glog.V(2).Infoln("parsing error:", err, r.URL.Path)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = n.ParsePath(fid)
|
||||
if err != nil {
|
||||
glog.V(2).Infoln("parsing fid error:", err, r.URL.Path)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
glog.V(4).Infoln("volume", volumeId, "reading", n)
|
||||
if !vs.store.HasVolume(volumeId) {
|
||||
lookupResult, err := operation.Lookup(vs.GetMasterNode(), volumeId.String())
|
||||
glog.V(2).Infoln("volume", volumeId, "found on", lookupResult, "error", err)
|
||||
if err == nil && len(lookupResult.Locations) > 0 {
|
||||
http.Redirect(w, r, "http://"+lookupResult.Locations[0].Url+r.URL.Path, http.StatusMovedPermanently)
|
||||
} else {
|
||||
glog.V(2).Infoln("lookup error:", err, r.URL.Path)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
return
|
||||
}
|
||||
cookie := n.Cookie
|
||||
count, e := vs.store.Read(volumeId, n)
|
||||
glog.V(4).Infoln("read bytes", count, "error", e)
|
||||
if e != nil || count <= 0 {
|
||||
glog.V(0).Infoln("read error:", e, r.URL.Path)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if n.Cookie != cookie {
|
||||
glog.V(0).Infoln("request", r.URL.Path, "with unmaching cookie seen:", cookie, "expected:", n.Cookie, "from", r.RemoteAddr, "agent", r.UserAgent())
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if n.LastModified != 0 {
|
||||
w.Header().Set("Last-Modified", time.Unix(int64(n.LastModified), 0).UTC().Format(http.TimeFormat))
|
||||
if r.Header.Get("If-Modified-Since") != "" {
|
||||
if t, parseError := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); parseError == nil {
|
||||
if t.Unix() >= int64(n.LastModified) {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
etag := n.Etag()
|
||||
if inm := r.Header.Get("If-None-Match"); inm == etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Etag", etag)
|
||||
if n.NameSize > 0 && filename == "" {
|
||||
filename = string(n.Name)
|
||||
dotIndex := strings.LastIndex(filename, ".")
|
||||
if dotIndex > 0 {
|
||||
ext = filename[dotIndex:]
|
||||
}
|
||||
}
|
||||
mtype := ""
|
||||
if ext != "" {
|
||||
mtype = mime.TypeByExtension(ext)
|
||||
}
|
||||
if n.MimeSize > 0 {
|
||||
mt := string(n.Mime)
|
||||
if mt != "application/octet-stream" {
|
||||
mtype = mt
|
||||
}
|
||||
}
|
||||
if mtype != "" {
|
||||
w.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
if filename != "" {
|
||||
w.Header().Set("Content-Disposition", "filename=\""+fileNameEscaper.Replace(filename)+"\"")
|
||||
}
|
||||
if ext != ".gz" {
|
||||
if n.IsGzipped() {
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
} else {
|
||||
if n.Data, err = storage.UnGzipData(n.Data); err != nil {
|
||||
glog.V(0).Infoln("lookup error:", err, r.URL.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ext == ".png" || ext == ".jpg" || ext == ".gif" {
|
||||
width, height := 0, 0
|
||||
if r.FormValue("width") != "" {
|
||||
width, _ = strconv.Atoi(r.FormValue("width"))
|
||||
}
|
||||
if r.FormValue("height") != "" {
|
||||
height, _ = strconv.Atoi(r.FormValue("height"))
|
||||
}
|
||||
n.Data, _, _ = images.Resized(ext, n.Data, width, height)
|
||||
}
|
||||
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
if r.Method == "HEAD" {
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(n.Data)))
|
||||
return
|
||||
}
|
||||
rangeReq := r.Header.Get("Range")
|
||||
if rangeReq == "" {
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(n.Data)))
|
||||
if _, e = w.Write(n.Data); e != nil {
|
||||
glog.V(0).Infoln("response write error:", e)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//the rest is dealing with partial content request
|
||||
//mostly copy from src/pkg/net/http/fs.go
|
||||
size := int64(len(n.Data))
|
||||
ranges, err := parseRange(rangeReq, size)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable)
|
||||
return
|
||||
}
|
||||
if sumRangesSize(ranges) > size {
|
||||
// The total number of bytes in all the ranges
|
||||
// is larger than the size of the file by
|
||||
// itself, so this is probably an attack, or a
|
||||
// dumb client. Ignore the range request.
|
||||
ranges = nil
|
||||
return
|
||||
}
|
||||
if len(ranges) == 0 {
|
||||
return
|
||||
}
|
||||
if len(ranges) == 1 {
|
||||
// RFC 2616, Section 14.16:
|
||||
// "When an HTTP message includes the content of a single
|
||||
// range (for example, a response to a request for a
|
||||
// single range, or to a request for a set of ranges
|
||||
// that overlap without any holes), this content is
|
||||
// transmitted with a Content-Range header, and a
|
||||
// Content-Length header showing the number of bytes
|
||||
// actually transferred.
|
||||
// ...
|
||||
// A response to a request for a single range MUST NOT
|
||||
// be sent using the multipart/byteranges media type."
|
||||
ra := ranges[0]
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(ra.length, 10))
|
||||
w.Header().Set("Content-Range", ra.contentRange(size))
|
||||
w.WriteHeader(http.StatusPartialContent)
|
||||
if _, e = w.Write(n.Data[ra.start : ra.start+ra.length]); e != nil {
|
||||
glog.V(0).Infoln("response write error:", e)
|
||||
}
|
||||
return
|
||||
}
|
||||
// process mulitple ranges
|
||||
for _, ra := range ranges {
|
||||
if ra.start > size {
|
||||
http.Error(w, "Out of Range", http.StatusRequestedRangeNotSatisfiable)
|
||||
return
|
||||
}
|
||||
}
|
||||
sendSize := rangesMIMESize(ranges, mtype, size)
|
||||
pr, pw := io.Pipe()
|
||||
mw := multipart.NewWriter(pw)
|
||||
w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary())
|
||||
sendContent := pr
|
||||
defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.
|
||||
go func() {
|
||||
for _, ra := range ranges {
|
||||
part, err := mw.CreatePart(ra.mimeHeader(mtype, size))
|
||||
if err != nil {
|
||||
pw.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
if _, err = part.Write(n.Data[ra.start : ra.start+ra.length]); err != nil {
|
||||
pw.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
mw.Close()
|
||||
pw.Close()
|
||||
}()
|
||||
if w.Header().Get("Content-Encoding") == "" {
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
|
||||
}
|
||||
w.WriteHeader(http.StatusPartialContent)
|
||||
io.CopyN(w, sendContent, sendSize)
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
)
|
||||
|
||||
func (vs *VolumeServer) vacuumVolumeCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err, ret := vs.store.CheckCompactVolume(r.FormValue("volume"), r.FormValue("garbageThreshold"))
|
||||
if err == nil {
|
||||
writeJsonQuiet(w, r, http.StatusOK, map[string]interface{}{"error": "", "result": ret})
|
||||
} else {
|
||||
writeJsonQuiet(w, r, http.StatusInternalServerError, map[string]interface{}{"error": err.Error(), "result": false})
|
||||
}
|
||||
glog.V(2).Infoln("checked compacting volume =", r.FormValue("volume"), "garbageThreshold =", r.FormValue("garbageThreshold"), "vacuum =", ret)
|
||||
}
|
||||
func (vs *VolumeServer) vacuumVolumeCompactHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := vs.store.CompactVolume(r.FormValue("volume"))
|
||||
if err == nil {
|
||||
writeJsonQuiet(w, r, http.StatusOK, map[string]string{"error": ""})
|
||||
} else {
|
||||
writeJsonError(w, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
glog.V(2).Infoln("compacted volume =", r.FormValue("volume"), ", error =", err)
|
||||
}
|
||||
func (vs *VolumeServer) vacuumVolumeCommitHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := vs.store.CommitCompactVolume(r.FormValue("volume"))
|
||||
if err == nil {
|
||||
writeJsonQuiet(w, r, http.StatusOK, map[string]string{"error": ""})
|
||||
} else {
|
||||
writeJsonError(w, r, http.StatusInternalServerError, err)
|
||||
}
|
||||
glog.V(2).Infoln("commit compact volume =", r.FormValue("volume"), ", error =", err)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/chrislusf/weed-fs/go/glog"
|
||||
"github.com/chrislusf/weed-fs/go/operation"
|
||||
"github.com/chrislusf/weed-fs/go/storage"
|
||||
"github.com/chrislusf/weed-fs/go/topology"
|
||||
)
|
||||
|
||||
func (vs *VolumeServer) PostHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if e := r.ParseForm(); e != nil {
|
||||
glog.V(0).Infoln("form parse error:", e)
|
||||
writeJsonError(w, r, http.StatusBadRequest, e)
|
||||
return
|
||||
}
|
||||
vid, _, _, _, _ := parseURLPath(r.URL.Path)
|
||||
volumeId, ve := storage.NewVolumeId(vid)
|
||||
if ve != nil {
|
||||
glog.V(0).Infoln("NewVolumeId error:", ve)
|
||||
writeJsonError(w, r, http.StatusBadRequest, ve)
|
||||
return
|
||||
}
|
||||
needle, ne := storage.NewNeedle(r, vs.FixJpgOrientation)
|
||||
if ne != nil {
|
||||
writeJsonError(w, r, http.StatusBadRequest, ne)
|
||||
return
|
||||
}
|
||||
|
||||
ret := operation.UploadResult{}
|
||||
size, errorStatus := topology.ReplicatedWrite(vs.GetMasterNode(),
|
||||
vs.store, volumeId, needle, r)
|
||||
httpStatus := http.StatusCreated
|
||||
if errorStatus != "" {
|
||||
httpStatus = http.StatusInternalServerError
|
||||
ret.Error = errorStatus
|
||||
}
|
||||
if needle.HasName() {
|
||||
ret.Name = string(needle.Name)
|
||||
}
|
||||
ret.Size = size
|
||||
writeJsonQuiet(w, r, httpStatus, ret)
|
||||
}
|
||||
|
||||
func (vs *VolumeServer) DeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
n := new(storage.Needle)
|
||||
vid, fid, _, _, _ := parseURLPath(r.URL.Path)
|
||||
volumeId, _ := storage.NewVolumeId(vid)
|
||||
n.ParsePath(fid)
|
||||
|
||||
glog.V(2).Infoln("deleting", n)
|
||||
|
||||
cookie := n.Cookie
|
||||
count, ok := vs.store.Read(volumeId, n)
|
||||
|
||||
if ok != nil {
|
||||
m := make(map[string]uint32)
|
||||
m["size"] = 0
|
||||
writeJsonQuiet(w, r, http.StatusNotFound, m)
|
||||
return
|
||||
}
|
||||
|
||||
if n.Cookie != cookie {
|
||||
glog.V(0).Infoln("delete", r.URL.Path, "with unmaching cookie from ", r.RemoteAddr, "agent", r.UserAgent())
|
||||
return
|
||||
}
|
||||
|
||||
ret := topology.ReplicatedDelete(vs.GetMasterNode(), vs.store, volumeId, n, r)
|
||||
|
||||
if ret != 0 {
|
||||
m := make(map[string]uint32)
|
||||
m["size"] = uint32(count)
|
||||
writeJsonQuiet(w, r, http.StatusAccepted, m)
|
||||
} else {
|
||||
writeJsonError(w, r, http.StatusInternalServerError, errors.New("Deletion Failed."))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Experts only: takes multiple fid parameters. This function does not propagate deletes to replicas.
|
||||
func (vs *VolumeServer) batchDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
var ret []operation.DeleteResult
|
||||
for _, fid := range r.Form["fid"] {
|
||||
vid, id_cookie, err := operation.ParseFileId(fid)
|
||||
if err != nil {
|
||||
ret = append(ret, operation.DeleteResult{Fid: fid, Error: err.Error()})
|
||||
continue
|
||||
}
|
||||
n := new(storage.Needle)
|
||||
volumeId, _ := storage.NewVolumeId(vid)
|
||||
n.ParsePath(id_cookie)
|
||||
glog.V(4).Infoln("batch deleting", n)
|
||||
cookie := n.Cookie
|
||||
if _, err := vs.store.Read(volumeId, n); err != nil {
|
||||
ret = append(ret, operation.DeleteResult{Fid: fid, Error: err.Error()})
|
||||
continue
|
||||
}
|
||||
if n.Cookie != cookie {
|
||||
ret = append(ret, operation.DeleteResult{Fid: fid, Error: "File Random Cookie does not match."})
|
||||
glog.V(0).Infoln("deleting", fid, "with unmaching cookie from ", r.RemoteAddr, "agent", r.UserAgent())
|
||||
return
|
||||
}
|
||||
if size, err := vs.store.Delete(volumeId, n); err != nil {
|
||||
ret = append(ret, operation.DeleteResult{Fid: fid, Error: err.Error()})
|
||||
} else {
|
||||
ret = append(ret, operation.DeleteResult{Fid: fid, Size: int(size)})
|
||||
}
|
||||
}
|
||||
|
||||
writeJsonQuiet(w, r, http.StatusAccepted, ret)
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
64 32G volumes consumes 10G memory
|
||||
Each volume has 25M index, so each cost 160MB memory
|
||||
|
||||
|
||||
|
||||
Things happened when I use lots of threads ( almost 120 ) keeping read file from Weed-FS.
|
||||
But I'm not so familiar with linux so I can't tell you exactly what happened.
|
||||
Next I'll show you things I know , if you need more info , contact me
|
||||
|
||||
My weed-fs version is about 0.12
|
||||
|
||||
1. top
|
||||
|
||||
top - 12:07:37 up 1 day, 3:17, 2 users, load average: 0.00, 0.00, 0.00
|
||||
Tasks: 152 total, 1 running, 151 sleeping, 0 stopped, 0 zombie
|
||||
Cpu(s): 0.0%us, 0.0%sy, 0.0%ni, 99.8%id, 0.1%wa, 0.0%hi, 0.0%si, 0.0%st
|
||||
Mem: 16269880k total, 16192364k used, 77516k free, 58172k buffers
|
||||
Swap: 2064376k total, 12324k used, 2052052k free, 2827520k cached
|
||||
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
1499 root 20 0 11.6g 10g 1424 S 0.0 65.6 7:32.53 weedvolume
|
||||
1498 root 20 0 3204m 2.1g 1428 S 0.0 13.5 4:36.59 weedvolume
|
||||
1737 root 20 0 98868 4932 2920 S 0.0 0.0 0:00.56 sshd
|
||||
1497 root 20 0 151m 4404 1152 S 0.0 0.0 1:21.40 weedmaster
|
||||
1335 root 20 0 97816 3044 2896 S 0.0 0.0 0:00.76 sshd
|
||||
|
||||
After system became steady , weedvolume used 65.6% memory .
|
||||
|
||||
2. free -m
|
||||
|
||||
total used free shared buffers cached
|
||||
Mem: 15888 15809 79 0 56 2758
|
||||
-/+ buffers/cache: 12994 2894
|
||||
Swap: 2015 12 2003
|
||||
|
||||
3. startup cmd
|
||||
|
||||
screen -d -m /opt/weed/weedmaster -mdir /data/weeddata/ > /data/logs/weed/master.log &
|
||||
screen -d -m /opt/weed/weedvolume -volumes=0-64 -dir /data/weeddata/ -pulseSeconds=20 -publicUrl="x.y.z:9334" -port 9334 > /data/logs/weed/s01.log &
|
||||
screen -d -m /opt/weed/weedvolume -volumes=65-107 -dir /data/weeddata/ -pulseSeconds=20 -publicUrl="x.y.z:9335" -port 9335 > /data/logs/weed/s02.log &
|
||||
|
||||
4. du -sh .
|
||||
|
||||
32G 0.dat
|
||||
26M 0.idx
|
||||
8.2G 100.dat
|
||||
6.8M 100.idx
|
||||
8.2G 101.dat
|
||||
6.9M 101.idx
|
||||
8.2G 102.dat
|
||||
6.8M 102.idx
|
||||
8.2G 103.dat
|
||||
6.8M 103.idx
|
||||
8.2G 104.dat
|
||||
6.8M 104.idx
|
||||
8.2G 105.dat
|
||||
6.9M 105.idx
|
||||
8.2G 106.dat
|
||||
6.9M 106.idx
|
||||
8.2G 107.dat
|
||||
6.9M 107.idx
|
||||
32G 10.dat
|
||||
25M 10.idx
|
||||
32G 11.dat
|
||||
25M 11.idx
|
||||
32G 12.dat
|
||||
25M 12.idx
|
||||
32G 13.dat
|
||||
25M 13.idx
|
||||
32G 14.dat
|
||||
25M 14.idx
|
||||
32G 15.dat
|
||||
25M 15.idx
|
||||
32G 16.dat
|
||||
25M 16.idx
|
||||
32G 17.dat
|
||||
25M 17.idx
|
||||
32G 18.dat
|
||||
25M 18.idx
|
||||
32G 19.dat
|
||||
25M 19.idx
|
||||
32G 1.dat
|
||||
26M 1.idx
|
||||
32G 20.dat
|
||||
25M 20.idx
|
||||
32G 21.dat
|
||||
25M 21.idx
|
||||
32G 22.dat
|
||||
25M 22.idx
|
||||
32G 23.dat
|
||||
25M 23.idx
|
||||
32G 24.dat
|
||||
25M 24.idx
|
||||
32G 25.dat
|
||||
25M 25.idx
|
||||
32G 26.dat
|
||||
25M 26.idx
|
||||
32G 27.dat
|
||||
25M 27.idx
|
||||
32G 28.dat
|
||||
25M 28.idx
|
||||
32G 29.dat
|
||||
25M 29.idx
|
||||
32G 2.dat
|
||||
26M 2.idx
|
||||
32G 30.dat
|
||||
25M 30.idx
|
||||
32G 31.dat
|
||||
25M 31.idx
|
||||
32G 32.dat
|
||||
25M 32.idx
|
||||
32G 33.dat
|
||||
25M 33.idx
|
||||
32G 34.dat
|
||||
25M 34.idx
|
||||
32G 35.dat
|
||||
25M 35.idx
|
||||
32G 36.dat
|
||||
25M 36.idx
|
||||
32G 37.dat
|
||||
25M 37.idx
|
||||
32G 38.dat
|
||||
25M 38.idx
|
||||
32G 39.dat
|
||||
25M 39.idx
|
||||
32G 3.dat
|
||||
26M 3.idx
|
||||
32G 40.dat
|
||||
25M 40.idx
|
||||
32G 41.dat
|
||||
25M 41.idx
|
||||
32G 42.dat
|
||||
25M 42.idx
|
||||
32G 43.dat
|
||||
25M 43.idx
|
||||
32G 44.dat
|
||||
25M 44.idx
|
||||
32G 45.dat
|
||||
25M 45.idx
|
||||
32G 46.dat
|
||||
25M 46.idx
|
||||
32G 47.dat
|
||||
25M 47.idx
|
||||
32G 48.dat
|
||||
25M 48.idx
|
||||
32G 49.dat
|
||||
25M 49.idx
|
||||
32G 4.dat
|
||||
26M 4.idx
|
||||
32G 50.dat
|
||||
25M 50.idx
|
||||
32G 51.dat
|
||||
25M 51.idx
|
||||
32G 52.dat
|
||||
25M 52.idx
|
||||
32G 53.dat
|
||||
25M 53.idx
|
||||
32G 54.dat
|
||||
25M 54.idx
|
||||
32G 55.dat
|
||||
25M 55.idx
|
||||
32G 56.dat
|
||||
25M 56.idx
|
||||
32G 57.dat
|
||||
25M 57.idx
|
||||
32G 58.dat
|
||||
25M 58.idx
|
||||
32G 59.dat
|
||||
25M 59.idx
|
||||
32G 5.dat
|
||||
26M 5.idx
|
||||
32G 60.dat
|
||||
25M 60.idx
|
||||
32G 61.dat
|
||||
25M 61.idx
|
||||
32G 62.dat
|
||||
25M 62.idx
|
||||
32G 63.dat
|
||||
25M 63.idx
|
||||
32G 64.dat
|
||||
25M 64.idx
|
||||
8.2G 65.dat
|
||||
6.9M 65.idx
|
||||
8.2G 66.dat
|
||||
6.9M 66.idx
|
||||
8.2G 67.dat
|
||||
6.9M 67.idx
|
||||
8.2G 68.dat
|
||||
6.8M 68.idx
|
||||
8.2G 69.dat
|
||||
6.9M 69.idx
|
||||
32G 6.dat
|
||||
25M 6.idx
|
||||
8.2G 70.dat
|
||||
6.8M 70.idx
|
||||
8.2G 71.dat
|
||||
6.9M 71.idx
|
||||
8.2G 72.dat
|
||||
6.9M 72.idx
|
||||
8.2G 73.dat
|
||||
6.9M 73.idx
|
||||
8.2G 74.dat
|
||||
6.9M 74.idx
|
||||
8.2G 75.dat
|
||||
6.9M 75.idx
|
||||
8.1G 76.dat
|
||||
6.8M 76.idx
|
||||
8.2G 77.dat
|
||||
6.8M 77.idx
|
||||
8.2G 78.dat
|
||||
6.8M 78.idx
|
||||
8.1G 79.dat
|
||||
6.8M 79.idx
|
||||
32G 7.dat
|
||||
25M 7.idx
|
||||
8.2G 80.dat
|
||||
6.8M 80.idx
|
||||
8.2G 81.dat
|
||||
6.9M 81.idx
|
||||
8.2G 82.dat
|
||||
6.9M 82.idx
|
||||
8.2G 83.dat
|
||||
6.9M 83.idx
|
||||
8.2G 84.dat
|
||||
6.9M 84.idx
|
||||
8.2G 85.dat
|
||||
6.8M 85.idx
|
||||
8.2G 86.dat
|
||||
6.9M 86.idx
|
||||
8.2G 87.dat
|
||||
6.9M 87.idx
|
||||
8.2G 88.dat
|
||||
6.9M 88.idx
|
||||
8.2G 89.dat
|
||||
6.8M 89.idx
|
||||
32G 8.dat
|
||||
25M 8.idx
|
||||
8.2G 90.dat
|
||||
6.9M 90.idx
|
||||
8.1G 91.dat
|
||||
6.8M 91.idx
|
||||
8.1G 92.dat
|
||||
6.8M 92.idx
|
||||
8.1G 93.dat
|
||||
6.8M 93.idx
|
||||
8.2G 94.dat
|
||||
6.9M 94.idx
|
||||
8.2G 95.dat
|
||||
6.9M 95.idx
|
||||
8.2G 96.dat
|
||||
6.9M 96.idx
|
||||
8.2G 97.dat
|
||||
6.9M 97.idx
|
||||
8.2G 98.dat
|
||||
6.9M 98.idx
|
||||
8.2G 99.dat
|
||||
6.9M 99.idx
|
||||
32G 9.dat
|
||||
25M 9.idx
|
||||
4.0K directory.seq
|
||||
|
||||
You can see the volume 1-64 is now full.
|
||||
|
||||
5. more log
|
||||
|
||||
see logs.zip
|
||||
|
||||
In messages you can see these lines: (Line 51095)
|
||||
|
||||
Sep 26 06:14:31 wedb-01 kernel: auditd: page allocation failure. order:0, mode:0x20
|
||||
Sep 26 06:14:31 wedb-01 kernel: Pid: 1009, comm: auditd Not tainted 2.6.32-220.el6.x86_64 #1
|
||||
Sep 26 06:14:31 wedb-01 kernel: Call Trace:
|
||||
|
||||
After those lines , the system deny any new network connect request
|
||||
|
||||
6. /dir/status
|
||||
|
||||
{"Machines":[{"Server":{"Url":"127.0.0.1:9335","PublicUrl":"x.y.z:9335"},"Volumes":[{"Id":106,"Size":8728909632},{"Id":66,"Size":8729852744},{"Id":90,"Size":8747834896},{"Id":103,"Size":8718106024},{"Id":87,"Size":8732133512},{"Id":96,"Size":8737251904},{"Id":80,"Size":8704130712},{"Id":77,"Size":8717989496},{"Id":70,"Size":8731474744},{"Id":94,"Size":8758656144},{"Id":107,"Size":8729599232},{"Id":67,"Size":8736848088},{"Id":91,"Size":8665847760},{"Id":100,"Size":8703272552},{"Id":84,"Size":8745121528},{"Id":97,"Size":8713031744},{"Id":81,"Size":8726088872},{"Id":74,"Size":8738588152},{"Id":71,"Size":8729349920},{"Id":95,"Size":8741526896},{"Id":104,"Size":8699374736},{"Id":88,"Size":8740362880},{"Id":101,"Size":8711832992},{"Id":85,"Size":8723479552},{"Id":78,"Size":8700345400},{"Id":75,"Size":8727796912},{"Id":68,"Size":8698607440},{"Id":92,"Size":8682683056},{"Id":105,"Size":8741226152},{"Id":65,"Size":8725365752},{"Id":89,"Size":8703062600},{"Id":98,"Size":8742331560},{"Id":82,"Size":8762554952},{"Id":79,"Size":8696300376},{"Id":72,"Size":8708217304},{"Id":69,"Size":8740268144},{"Id":93,"Size":8685060320},{"Id":102,"Size":8708695352},{"Id":86,"Size":8783247776},{"Id":99,"Size":8753463608},{"Id":83,"Size":8725963952},{"Id":76,"Size":8694693536},{"Id":73,"Size":8733560832}]},{"Server":{"Url":"127.0.0.1:9334","PublicUrl":"x.y.z:9334"},"Volumes":[{"Id":34,"Size":33415706800},{"Id":58,"Size":33569224784},{"Id":18,"Size":33474649968},{"Id":55,"Size":33542422680},{"Id":15,"Size":33517247576},{"Id":48,"Size":33574860328},{"Id":8,"Size":33511257144},{"Id":45,"Size":33463948408},{"Id":5,"Size":34317702920},{"Id":29,"Size":33465695776},{"Id":38,"Size":33553119624},{"Id":62,"Size":33448316736},{"Id":22,"Size":33566586296},{"Id":35,"Size":33493733728},{"Id":59,"Size":33498554904},{"Id":19,"Size":33493313784},{"Id":52,"Size":33552978448},{"Id":12,"Size":33505183752},{"Id":49,"Size":33603029896},{"Id":9,"Size":33515778064},{"Id":42,"Size":33500402248},{"Id":2,"Size":34223232992},{"Id":26,"Size":33526519600},{"Id":39,"Size":33580414336},{"Id":63,"Size":33476332456},{"Id":23,"Size":33543872592},{"Id":32,"Size":33515290168},{"Id":56,"Size":33499171184},{"Id":16,"Size":33556591168},{"Id":64,"Size":33495148616},{"Id":53,"Size":33467738560},{"Id":13,"Size":33596873960},{"Id":46,"Size":33508120448},{"Id":6,"Size":33417470256},{"Id":30,"Size":33532933992},{"Id":43,"Size":33591802008},{"Id":3,"Size":34270682080},{"Id":27,"Size":33525736944},{"Id":36,"Size":33443597824},{"Id":60,"Size":33427931336},{"Id":20,"Size":33499083096},{"Id":33,"Size":33531396280},{"Id":57,"Size":33578015104},{"Id":17,"Size":33510525480},{"Id":50,"Size":33503123704},{"Id":10,"Size":33502391608},{"Id":47,"Size":33521868568},{"Id":7,"Size":33497101664},{"Id":31,"Size":33426905232},{"Id":40,"Size":33472978696},{"Id":0,"Size":34337344304},{"Id":24,"Size":33550157192},{"Id":37,"Size":33477162720},{"Id":61,"Size":33537175080},{"Id":21,"Size":33517192456},{"Id":54,"Size":33480720288},{"Id":14,"Size":33513192896},{"Id":51,"Size":33531336080},{"Id":11,"Size":33562385088},{"Id":44,"Size":33554479104},{"Id":4,"Size":34333127520},{"Id":28,"Size":33510503000},{"Id":41,"Size":33574922928},{"Id":1,"Size":34307181368},{"Id":25,"Size":33542834568}]}],"Writers":[106,66,90,103,87,96,80,77,70,94,107,67,91,100,84,97,81,74,71,95,104,88,101,85,78,75,68,92,105,65,89,98,82,79,72,69,93,102,86,99,83,76,73,34,58,18,55,15,48,8,45,5,29,38,62,22,35,59,19,52,12,49,9,42,2,26,39,63,23,32,56,16,64,53,13,46,6,30,43,3,27,36,60,20,33,57,17,50,10,47,7,31,40,0,24,37,61,21,54,14,51,11,44,4,28,41,1,25],"FileIdSequence":110250000}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
1. each file can choose the replication factor
|
||||
2. replication granularity is in volume level
|
||||
3. if not enough spaces, we can automatically decrease some volume's the replication factor, especially for cold data
|
||||
4. plan to support migrating data to cheaper storage
|
||||
5. plan to manual volume placement, access-based volume placement, auction based volume placement
|
||||
|
||||
When a new volume server is started, it reports
|
||||
1. how many volumes it can hold
|
||||
2. current list of existing volumes and each volume's replication type
|
||||
Each volume server remembers:
|
||||
1. current volume ids
|
||||
2. replica locations are read from the master
|
||||
|
||||
The master assign volume ids based on
|
||||
1. replication factor
|
||||
data center, rack
|
||||
2. concurrent write support
|
||||
On master, stores the replication configuration
|
||||
{
|
||||
replication:{
|
||||
{type:"00", min_volume_count:3, weight:10},
|
||||
{type:"01", min_volume_count:2, weight:20},
|
||||
{type:"10", min_volume_count:2, weight:20},
|
||||
{type:"11", min_volume_count:3, weight:30},
|
||||
{type:"20", min_volume_count:2, weight:20}
|
||||
},
|
||||
port:9333,
|
||||
}
|
||||
Or manually via command line
|
||||
1. add volume with specified replication factor
|
||||
2. add volume with specified volume id
|
||||
|
||||
|
||||
If duplicated volume ids are reported from different volume servers,
|
||||
the master determines the replication factor of the volume,
|
||||
if less than the replication factor, the volume is in readonly mode
|
||||
if more than the replication factor, the volume will purge the smallest/oldest volume
|
||||
if equal, the volume will function as usual
|
||||
|
||||
|
||||
Use cases:
|
||||
on volume server
|
||||
1. weed volume -mserver="xx.xx.xx.xx:9333" -publicUrl="good.com:8080" -dir="/tmp" -volumes=50
|
||||
on weed master
|
||||
1. weed master -port=9333
|
||||
generate a default json configuration file if doesn't exist
|
||||
|
||||
Bootstrap
|
||||
1. at the very beginning, the system has no volumes at all.
|
||||
When data node starts:
|
||||
1. each data node send to master its existing volumes and max volume blocks
|
||||
2. master remembers the topology/data_center/rack/data_node/volumes
|
||||
for each replication level, stores
|
||||
volume id ~ data node
|
||||
writable volume ids
|
||||
If any "assign" request comes in
|
||||
1. find a writable volume with the right replicationLevel
|
||||
2. if not found, grow the volumes with the right replication level
|
||||
3. return a writable volume to the user
|
||||
|
||||
|
||||
for data node:
|
||||
0. detect existing volumes DONE
|
||||
1. onStartUp, and periodically, send existing volumes and maxVolumeCount store.Join(), DONE
|
||||
2. accept command to grow a volume( id + replication level) DONE
|
||||
/admin/assign_volume?volume=some_id&replicationType=01
|
||||
3. accept setting volumeLocationList DONE
|
||||
/admin/set_volume_locations_list?volumeLocationsList=[{Vid:xxx,Locations:[loc1,loc2,loc3]}]
|
||||
4. for each write, pass the write to the next location, (Step 2)
|
||||
POST method should accept an index, like ttl, get decremented every hop
|
||||
for master:
|
||||
1. accept data node's report of existing volumes and maxVolumeCount ALREADY EXISTS /dir/join
|
||||
2. periodically refresh for active data nodes, and adjust writable volumes
|
||||
3. send command to grow a volume(id + replication level) DONE
|
||||
5. accept lookup for volume locations ALREADY EXISTS /dir/lookup
|
||||
6. read topology/datacenter/rack layout
|
||||
|
||||
An algorithm to allocate volumes evenly, but may be inefficient if free volumes are plenty:
|
||||
input: replication=xyz
|
||||
algorithm:
|
||||
ret_dcs = []
|
||||
foreach dc that has y+z+1 volumes{
|
||||
ret_racks = []
|
||||
foreach rack with z+1 volumes{
|
||||
ret = select z+1 servers with 1 volume
|
||||
if ret.size()==z+1 {
|
||||
ret_racks.append(ret)
|
||||
}
|
||||
}
|
||||
randomly pick one rack from ret_racks
|
||||
ret += select y racks with 1 volume each
|
||||
if ret.size()==y+z+1{
|
||||
ret_dcs.append(ret)
|
||||
}
|
||||
}
|
||||
randomly pick one dc from ret_dcs
|
||||
ret += select x data centers with 1 volume each
|
||||
|
||||
A simple replica placement algorithm, but may fail when free volume slots are not plenty:
|
||||
ret := []volumes
|
||||
dc = randomly pick 1 data center with y+z+1 volumes
|
||||
rack = randomly pick 1 rack with z+1 volumes
|
||||
ret = ret.append(randomly pick z+1 volumes)
|
||||
ret = ret.append(randomly pick y racks with 1 volume)
|
||||
ret = ret.append(randomly pick x data centers with 1 volume)
|
||||
|
||||
|
||||
TODO:
|
||||
1. replicate content to the other server if the replication type needs replicas
|
||||
@@ -1,51 +0,0 @@
|
||||
Design for Seaweed-FS security
|
||||
|
||||
Design Objectives
|
||||
Security can mean many different things. The original vision is that: if you have one machine lying around
|
||||
somewhere with some disk space, it should be able to join your file system to contribute some disk space and
|
||||
network bandwidth, securely!
|
||||
|
||||
To achieve this purpose, the security should be able to:
|
||||
1. Secure the inter-server communication. Only real cluster servers can join and communicate.
|
||||
2. allow clients to securely write to volume servers
|
||||
|
||||
Non Objective
|
||||
Multi-tenant support. Avoid filers or clients cross-updating files.
|
||||
User specific access control.
|
||||
|
||||
Design Architect
|
||||
master, and volume servers all talk securely via 2-way SSL for admin operations.
|
||||
upon joining, master gives its secret key to volume servers.
|
||||
filer or clients talk to master to get secret key, and use the key to generate JWT to write on volume server.
|
||||
A side benefit:
|
||||
a time limited read feature?
|
||||
4. volume server needs to expose https ports
|
||||
|
||||
HTTP Connections
|
||||
clear http
|
||||
filer~>master, need to get a JWT from master
|
||||
filer~>volume
|
||||
2-way https
|
||||
master~ssl~>volume
|
||||
volume~ssl~>master
|
||||
|
||||
file uploading:
|
||||
when volume server starts, it asks master for the secret key to decode JWT
|
||||
when filer/clients wants to upload, master generate a JWT
|
||||
filer~>volume(public port)
|
||||
master~>volume(public port)
|
||||
|
||||
Currently, volume server has 2 ip addresses: ip and publicUrl.
|
||||
The ip is for admin purpose, and master talk to volume server this way.
|
||||
The publicUrl is for clients to access the server, via http GET/POST/DELETE etc.
|
||||
The write operations are secured by JWT.
|
||||
clients talk to master also via https? possible. Decide on this later.
|
||||
|
||||
Dev plan:
|
||||
1. volume server separate admin from public GET/POST/DELETE handlers
|
||||
The step 1 may be good enough for most use cases.
|
||||
|
||||
If 2-way ssl are still needed
|
||||
2. volume server add ssl support
|
||||
3. https connections to operate on volume servers
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
How to submit a content
|
||||
1. Find physical volumes
|
||||
1.c Create a hash value
|
||||
1.d find a write logic volume id, and return [logic volume id, {physical volume ids}]
|
||||
2. submit to physical volumes
|
||||
2.c
|
||||
generate the cookie
|
||||
generate a unique id as key
|
||||
choose the right altKey
|
||||
send bytes to physical volumes
|
||||
2.s each
|
||||
save bytes
|
||||
store map[key uint64, altKey uint32]<offset, size>
|
||||
for updated entry, set old entry's offset to zero
|
||||
3.c
|
||||
wait for all physical volumes to finish
|
||||
store the /<logic volume id>/<key>_<cookie>_<altKey>.<ext>
|
||||
|
||||
How to retrieve a content
|
||||
1.c
|
||||
send logic volume id
|
||||
1.d
|
||||
find least busy volume's id
|
||||
2.c
|
||||
send URI /<physical volume id>/<key>_<cookie>_<altKey>.<ext>
|
||||
|
||||
|
||||
How to submit a content
|
||||
1. send bytes to weedfs, got <volume id, key uint64, cookie code>
|
||||
store <key uint64, volume id uint32, cookie code uint32, ext>, and other information
|
||||
|
||||
To read a content
|
||||
2. use logic volume id to lookup a <machine id>
|
||||
render url as /<machine id>/<volume id>/<key>/<cookie>.ext
|
||||
|
||||
The directory server
|
||||
0.init
|
||||
load and collect <logic volume id, machine ids> mapping
|
||||
1.on submit content
|
||||
find a free logic volume id, start sending content to 3 machines
|
||||
if all of them finishes, return <logic volume id, key, cookie code>
|
||||
2.on read content
|
||||
based on logic volume id, pick a machine with less load,
|
||||
return <machine id>
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.github.chrislusf</groupId>
|
||||
<artifactId>seaweedfs-client</artifactId>
|
||||
<version>1.0.2</version>
|
||||
|
||||
<parent>
|
||||
<groupId>org.sonatype.oss</groupId>
|
||||
<artifactId>oss-parent</artifactId>
|
||||
<version>9</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<protobuf.version>3.5.1</protobuf.version>
|
||||
<grpc.version>1.16.1</grpc.version>
|
||||
<guava.version>26.0-jre</guava.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java -->
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>${protobuf.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>${guava.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-netty-shaded</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>1.7.25</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<distributionManagement>
|
||||
<snapshotRepository>
|
||||
<id>ossrh</id>
|
||||
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
<build>
|
||||
<extensions>
|
||||
<extension>
|
||||
<groupId>kr.motd.maven</groupId>
|
||||
<artifactId>os-maven-plugin</artifactId>
|
||||
<version>1.5.0.Final</version>
|
||||
</extension>
|
||||
</extensions>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.xolstice.maven.plugins</groupId>
|
||||
<artifactId>protobuf-maven-plugin</artifactId>
|
||||
<version>0.5.1</version>
|
||||
<configuration>
|
||||
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}-1:exe:${os.detected.classifier}</protocArtifact>
|
||||
<pluginId>grpc-java</pluginId>
|
||||
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
<goal>compile-custom</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>1.5</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<version>1.6.7</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<serverId>ossrh</serverId>
|
||||
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
|
||||
<autoReleaseAfterClose>true</autoReleaseAfterClose>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>2.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>2.9.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,207 @@
|
||||
package seaweedfs.client;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class FilerClient {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FilerClient.class);
|
||||
|
||||
private FilerGrpcClient filerGrpcClient;
|
||||
|
||||
public FilerClient(String host, int grpcPort) {
|
||||
filerGrpcClient = new FilerGrpcClient(host, grpcPort);
|
||||
}
|
||||
|
||||
public FilerClient(FilerGrpcClient filerGrpcClient) {
|
||||
this.filerGrpcClient = filerGrpcClient;
|
||||
}
|
||||
|
||||
public boolean mkdirs(String path, int mode) {
|
||||
String currentUser = System.getProperty("user.name");
|
||||
return mkdirs(path, mode, 0, 0, currentUser, new String[]{});
|
||||
}
|
||||
|
||||
public boolean mkdirs(String path, int mode, String userName, String[] groupNames) {
|
||||
return mkdirs(path, mode, 0, 0, userName, groupNames);
|
||||
}
|
||||
|
||||
public boolean mkdirs(String path, int mode, int uid, int gid, String userName, String[] groupNames) {
|
||||
|
||||
Path pathObject = Paths.get(path);
|
||||
String parent = pathObject.getParent().toString();
|
||||
String name = pathObject.getFileName().toString();
|
||||
|
||||
if ("/".equals(parent)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
mkdirs(parent, mode, uid, gid, userName, groupNames);
|
||||
|
||||
FilerProto.Entry existingEntry = lookupEntry(parent, name);
|
||||
|
||||
if (existingEntry != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return createEntry(
|
||||
parent,
|
||||
newDirectoryEntry(name, mode, uid, gid, userName, groupNames).build()
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public boolean rm(String path, boolean isRecursive) {
|
||||
|
||||
Path pathObject = Paths.get(path);
|
||||
String parent = pathObject.getParent().toString();
|
||||
String name = pathObject.getFileName().toString();
|
||||
|
||||
return deleteEntry(
|
||||
parent,
|
||||
name,
|
||||
true,
|
||||
isRecursive);
|
||||
}
|
||||
|
||||
public boolean touch(String path, int mode) {
|
||||
String currentUser = System.getProperty("user.name");
|
||||
return touch(path, mode, 0, 0, currentUser, new String[]{});
|
||||
}
|
||||
|
||||
public boolean touch(String path, int mode, int uid, int gid, String userName, String[] groupNames) {
|
||||
|
||||
Path pathObject = Paths.get(path);
|
||||
String parent = pathObject.getParent().toString();
|
||||
String name = pathObject.getFileName().toString();
|
||||
|
||||
FilerProto.Entry entry = lookupEntry(parent, name);
|
||||
if (entry == null) {
|
||||
return createEntry(
|
||||
parent,
|
||||
newFileEntry(name, mode, uid, gid, userName, groupNames).build()
|
||||
);
|
||||
}
|
||||
long now = System.currentTimeMillis() / 1000L;
|
||||
FilerProto.FuseAttributes.Builder attr = entry.getAttributes().toBuilder()
|
||||
.setMtime(now)
|
||||
.setUid(uid)
|
||||
.setGid(gid)
|
||||
.setUserName(userName)
|
||||
.clearGroupName()
|
||||
.addAllGroupName(Arrays.asList(groupNames));
|
||||
return updateEntry(parent, entry.toBuilder().setAttributes(attr).build());
|
||||
}
|
||||
|
||||
public FilerProto.Entry.Builder newDirectoryEntry(String name, int mode,
|
||||
int uid, int gid, String userName, String[] groupNames) {
|
||||
|
||||
long now = System.currentTimeMillis() / 1000L;
|
||||
|
||||
return FilerProto.Entry.newBuilder()
|
||||
.setName(name)
|
||||
.setIsDirectory(true)
|
||||
.setAttributes(FilerProto.FuseAttributes.newBuilder()
|
||||
.setMtime(now)
|
||||
.setCrtime(now)
|
||||
.setUid(uid)
|
||||
.setGid(gid)
|
||||
.setFileMode(mode | 1 << 31)
|
||||
.setUserName(userName)
|
||||
.clearGroupName()
|
||||
.addAllGroupName(Arrays.asList(groupNames)));
|
||||
}
|
||||
|
||||
public FilerProto.Entry.Builder newFileEntry(String name, int mode,
|
||||
int uid, int gid, String userName, String[] groupNames) {
|
||||
|
||||
long now = System.currentTimeMillis() / 1000L;
|
||||
|
||||
return FilerProto.Entry.newBuilder()
|
||||
.setName(name)
|
||||
.setIsDirectory(false)
|
||||
.setAttributes(FilerProto.FuseAttributes.newBuilder()
|
||||
.setMtime(now)
|
||||
.setCrtime(now)
|
||||
.setUid(uid)
|
||||
.setGid(gid)
|
||||
.setFileMode(mode)
|
||||
.setUserName(userName)
|
||||
.clearGroupName()
|
||||
.addAllGroupName(Arrays.asList(groupNames)));
|
||||
}
|
||||
|
||||
public List<FilerProto.Entry> listEntries(String path) {
|
||||
return listEntries(path, "", "", 100000);
|
||||
}
|
||||
|
||||
public List<FilerProto.Entry> listEntries(String path, String entryPrefix, String lastEntryName, int limit) {
|
||||
return filerGrpcClient.getBlockingStub().listEntries(FilerProto.ListEntriesRequest.newBuilder()
|
||||
.setDirectory(path)
|
||||
.setPrefix(entryPrefix)
|
||||
.setStartFromFileName(lastEntryName)
|
||||
.setLimit(limit)
|
||||
.build()).getEntriesList();
|
||||
}
|
||||
|
||||
public FilerProto.Entry lookupEntry(String directory, String entryName) {
|
||||
try {
|
||||
return filerGrpcClient.getBlockingStub().lookupDirectoryEntry(
|
||||
FilerProto.LookupDirectoryEntryRequest.newBuilder()
|
||||
.setDirectory(directory)
|
||||
.setName(entryName)
|
||||
.build()).getEntry();
|
||||
} catch (Exception e) {
|
||||
LOG.warn("lookupEntry {}/{}: {}", directory, entryName, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean createEntry(String parent, FilerProto.Entry entry) {
|
||||
try {
|
||||
filerGrpcClient.getBlockingStub().createEntry(FilerProto.CreateEntryRequest.newBuilder()
|
||||
.setDirectory(parent)
|
||||
.setEntry(entry)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
LOG.warn("createEntry {}/{}: {}", parent, entry.getName(), e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean updateEntry(String parent, FilerProto.Entry entry) {
|
||||
try {
|
||||
filerGrpcClient.getBlockingStub().updateEntry(FilerProto.UpdateEntryRequest.newBuilder()
|
||||
.setDirectory(parent)
|
||||
.setEntry(entry)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
LOG.warn("createEntry {}/{}: {}", parent, entry.getName(), e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean deleteEntry(String parent, String entryName, boolean isDeleteFileChunk, boolean isRecursive) {
|
||||
try {
|
||||
filerGrpcClient.getBlockingStub().deleteEntry(FilerProto.DeleteEntryRequest.newBuilder()
|
||||
.setDirectory(parent)
|
||||
.setName(entryName)
|
||||
.setIsDeleteData(isDeleteFileChunk)
|
||||
.setIsRecursive(isRecursive)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
LOG.warn("deleteEntry {}/{}: {}", parent, entryName, e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package seaweedfs.client;
|
||||
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class FilerGrpcClient {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(FilerGrpcClient.class.getName());
|
||||
|
||||
private final ManagedChannel channel;
|
||||
private final SeaweedFilerGrpc.SeaweedFilerBlockingStub blockingStub;
|
||||
private final SeaweedFilerGrpc.SeaweedFilerStub asyncStub;
|
||||
private final SeaweedFilerGrpc.SeaweedFilerFutureStub futureStub;
|
||||
|
||||
|
||||
public FilerGrpcClient(String host, int grpcPort) {
|
||||
this(ManagedChannelBuilder.forAddress(host, grpcPort).usePlaintext());
|
||||
}
|
||||
|
||||
public FilerGrpcClient(ManagedChannelBuilder<?> channelBuilder) {
|
||||
channel = channelBuilder.build();
|
||||
blockingStub = SeaweedFilerGrpc.newBlockingStub(channel);
|
||||
asyncStub = SeaweedFilerGrpc.newStub(channel);
|
||||
futureStub = SeaweedFilerGrpc.newFutureStub(channel);
|
||||
}
|
||||
|
||||
public void shutdown() throws InterruptedException {
|
||||
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public SeaweedFilerGrpc.SeaweedFilerBlockingStub getBlockingStub() {
|
||||
return blockingStub;
|
||||
}
|
||||
|
||||
public SeaweedFilerGrpc.SeaweedFilerStub getAsyncStub() {
|
||||
return asyncStub;
|
||||
}
|
||||
|
||||
public SeaweedFilerGrpc.SeaweedFilerFutureStub getFutureStub() {
|
||||
return futureStub;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package filer_pb;
|
||||
|
||||
option java_package = "seaweedfs.client";
|
||||
option java_outer_classname = "FilerProto";
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
service SeaweedFiler {
|
||||
|
||||
rpc LookupDirectoryEntry (LookupDirectoryEntryRequest) returns (LookupDirectoryEntryResponse) {
|
||||
}
|
||||
|
||||
rpc ListEntries (ListEntriesRequest) returns (ListEntriesResponse) {
|
||||
}
|
||||
|
||||
rpc CreateEntry (CreateEntryRequest) returns (CreateEntryResponse) {
|
||||
}
|
||||
|
||||
rpc UpdateEntry (UpdateEntryRequest) returns (UpdateEntryResponse) {
|
||||
}
|
||||
|
||||
rpc DeleteEntry (DeleteEntryRequest) returns (DeleteEntryResponse) {
|
||||
}
|
||||
|
||||
rpc AssignVolume (AssignVolumeRequest) returns (AssignVolumeResponse) {
|
||||
}
|
||||
|
||||
rpc LookupVolume (LookupVolumeRequest) returns (LookupVolumeResponse) {
|
||||
}
|
||||
|
||||
rpc DeleteCollection (DeleteCollectionRequest) returns (DeleteCollectionResponse) {
|
||||
}
|
||||
|
||||
rpc Statistics (StatisticsRequest) returns (StatisticsResponse) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
message LookupDirectoryEntryRequest {
|
||||
string directory = 1;
|
||||
string name = 2;
|
||||
}
|
||||
|
||||
message LookupDirectoryEntryResponse {
|
||||
Entry entry = 1;
|
||||
}
|
||||
|
||||
message ListEntriesRequest {
|
||||
string directory = 1;
|
||||
string prefix = 2;
|
||||
string startFromFileName = 3;
|
||||
bool inclusiveStartFrom = 4;
|
||||
uint32 limit = 5;
|
||||
}
|
||||
|
||||
message ListEntriesResponse {
|
||||
repeated Entry entries = 1;
|
||||
}
|
||||
|
||||
message Entry {
|
||||
string name = 1;
|
||||
bool is_directory = 2;
|
||||
repeated FileChunk chunks = 3;
|
||||
FuseAttributes attributes = 4;
|
||||
map<string, bytes> extended = 5;
|
||||
}
|
||||
|
||||
message EventNotification {
|
||||
Entry old_entry = 1;
|
||||
Entry new_entry = 2;
|
||||
bool delete_chunks = 3;
|
||||
}
|
||||
|
||||
message FileChunk {
|
||||
string file_id = 1;
|
||||
int64 offset = 2;
|
||||
uint64 size = 3;
|
||||
int64 mtime = 4;
|
||||
string e_tag = 5;
|
||||
string source_file_id = 6;
|
||||
}
|
||||
|
||||
message FuseAttributes {
|
||||
uint64 file_size = 1;
|
||||
int64 mtime = 2; // unix time in seconds
|
||||
uint32 file_mode = 3;
|
||||
uint32 uid = 4;
|
||||
uint32 gid = 5;
|
||||
int64 crtime = 6; // unix time in seconds
|
||||
string mime = 7;
|
||||
string replication = 8;
|
||||
string collection = 9;
|
||||
int32 ttl_sec = 10;
|
||||
string user_name = 11; // for hdfs
|
||||
repeated string group_name = 12; // for hdfs
|
||||
}
|
||||
|
||||
message CreateEntryRequest {
|
||||
string directory = 1;
|
||||
Entry entry = 2;
|
||||
}
|
||||
|
||||
message CreateEntryResponse {
|
||||
}
|
||||
|
||||
message UpdateEntryRequest {
|
||||
string directory = 1;
|
||||
Entry entry = 2;
|
||||
}
|
||||
message UpdateEntryResponse {
|
||||
}
|
||||
|
||||
message DeleteEntryRequest {
|
||||
string directory = 1;
|
||||
string name = 2;
|
||||
// bool is_directory = 3;
|
||||
bool is_delete_data = 4;
|
||||
bool is_recursive = 5;
|
||||
}
|
||||
|
||||
message DeleteEntryResponse {
|
||||
}
|
||||
|
||||
message AssignVolumeRequest {
|
||||
int32 count = 1;
|
||||
string collection = 2;
|
||||
string replication = 3;
|
||||
int32 ttl_sec = 4;
|
||||
string data_center = 5;
|
||||
}
|
||||
|
||||
message AssignVolumeResponse {
|
||||
string file_id = 1;
|
||||
string url = 2;
|
||||
string public_url = 3;
|
||||
int32 count = 4;
|
||||
}
|
||||
|
||||
message LookupVolumeRequest {
|
||||
repeated string volume_ids = 1;
|
||||
}
|
||||
|
||||
message Locations {
|
||||
repeated Location locations = 1;
|
||||
}
|
||||
|
||||
message Location {
|
||||
string url = 1;
|
||||
string public_url = 2;
|
||||
}
|
||||
message LookupVolumeResponse {
|
||||
map<string, Locations> locations_map = 1;
|
||||
}
|
||||
|
||||
message DeleteCollectionRequest {
|
||||
string collection = 1;
|
||||
}
|
||||
|
||||
message DeleteCollectionResponse {
|
||||
}
|
||||
|
||||
message StatisticsRequest {
|
||||
string replication = 1;
|
||||
string collection = 2;
|
||||
string ttl = 3;
|
||||
}
|
||||
message StatisticsResponse {
|
||||
string replication = 1;
|
||||
string collection = 2;
|
||||
string ttl = 3;
|
||||
uint64 total_size = 4;
|
||||
uint64 used_size = 5;
|
||||
uint64 file_count = 6;
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.github.chrislusf</groupId>
|
||||
<artifactId>seaweedfs-hadoop-client</artifactId>
|
||||
<version>1.0.2</version>
|
||||
|
||||
<parent>
|
||||
<groupId>org.sonatype.oss</groupId>
|
||||
<artifactId>oss-parent</artifactId>
|
||||
<version>9</version>
|
||||
</parent>
|
||||
|
||||
<distributionManagement>
|
||||
<snapshotRepository>
|
||||
<id>ossrh</id>
|
||||
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
|
||||
</snapshotRepository>
|
||||
</distributionManagement>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>7</source>
|
||||
<target>7</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
<exclude>org/slf4j/**</exclude>
|
||||
<exclude>META-INF/maven/org.slf4j/**</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||
</transformers>
|
||||
<relocations>
|
||||
<relocation>
|
||||
<pattern>com.google</pattern>
|
||||
<shadedPattern>shaded.com.google</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>io.grpc.internal</pattern>
|
||||
<shadedPattern>shaded.io.grpc.internal</shadedPattern>
|
||||
</relocation>
|
||||
<relocation>
|
||||
<pattern>org.apache.commons</pattern>
|
||||
<shadedPattern>shaded.org.apache.commons</shadedPattern>
|
||||
<excludes>
|
||||
<exclude>org.apache.hadoop</exclude>
|
||||
<exclude>org.apache.log4j</exclude>
|
||||
</excludes>
|
||||
</relocation>
|
||||
</relocations>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>1.5</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>nexus-staging-maven-plugin</artifactId>
|
||||
<version>1.6.7</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<serverId>ossrh</serverId>
|
||||
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
|
||||
<autoReleaseAfterClose>true</autoReleaseAfterClose>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>2.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>2.9.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<properties>
|
||||
<hadoop.version>3.1.1</hadoop.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.hadoop</groupId>
|
||||
<artifactId>hadoop-client</artifactId>
|
||||
<version>${hadoop.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.chrislusf</groupId>
|
||||
<artifactId>seaweedfs-client</artifactId>
|
||||
<version>1.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.hadoop</groupId>
|
||||
<artifactId>hadoop-common</artifactId>
|
||||
<version>${hadoop.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpmime</artifactId>
|
||||
<version>4.5.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package seaweed.hdfs;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
class ReadBuffer {
|
||||
|
||||
private SeaweedInputStream stream;
|
||||
private long offset; // offset within the file for the buffer
|
||||
private int length; // actual length, set after the buffer is filles
|
||||
private int requestedLength; // requested length of the read
|
||||
private byte[] buffer; // the buffer itself
|
||||
private int bufferindex = -1; // index in the buffers array in Buffer manager
|
||||
private ReadBufferStatus status; // status of the buffer
|
||||
private CountDownLatch latch = null; // signaled when the buffer is done reading, so any client
|
||||
// waiting on this buffer gets unblocked
|
||||
|
||||
// fields to help with eviction logic
|
||||
private long timeStamp = 0; // tick at which buffer became available to read
|
||||
private boolean isFirstByteConsumed = false;
|
||||
private boolean isLastByteConsumed = false;
|
||||
private boolean isAnyByteConsumed = false;
|
||||
|
||||
public SeaweedInputStream getStream() {
|
||||
return stream;
|
||||
}
|
||||
|
||||
public void setStream(SeaweedInputStream stream) {
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
public long getOffset() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
public void setOffset(long offset) {
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public void setLength(int length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public int getRequestedLength() {
|
||||
return requestedLength;
|
||||
}
|
||||
|
||||
public void setRequestedLength(int requestedLength) {
|
||||
this.requestedLength = requestedLength;
|
||||
}
|
||||
|
||||
public byte[] getBuffer() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public void setBuffer(byte[] buffer) {
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
public int getBufferindex() {
|
||||
return bufferindex;
|
||||
}
|
||||
|
||||
public void setBufferindex(int bufferindex) {
|
||||
this.bufferindex = bufferindex;
|
||||
}
|
||||
|
||||
public ReadBufferStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(ReadBufferStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public CountDownLatch getLatch() {
|
||||
return latch;
|
||||
}
|
||||
|
||||
public void setLatch(CountDownLatch latch) {
|
||||
this.latch = latch;
|
||||
}
|
||||
|
||||
public long getTimeStamp() {
|
||||
return timeStamp;
|
||||
}
|
||||
|
||||
public void setTimeStamp(long timeStamp) {
|
||||
this.timeStamp = timeStamp;
|
||||
}
|
||||
|
||||
public boolean isFirstByteConsumed() {
|
||||
return isFirstByteConsumed;
|
||||
}
|
||||
|
||||
public void setFirstByteConsumed(boolean isFirstByteConsumed) {
|
||||
this.isFirstByteConsumed = isFirstByteConsumed;
|
||||
}
|
||||
|
||||
public boolean isLastByteConsumed() {
|
||||
return isLastByteConsumed;
|
||||
}
|
||||
|
||||
public void setLastByteConsumed(boolean isLastByteConsumed) {
|
||||
this.isLastByteConsumed = isLastByteConsumed;
|
||||
}
|
||||
|
||||
public boolean isAnyByteConsumed() {
|
||||
return isAnyByteConsumed;
|
||||
}
|
||||
|
||||
public void setAnyByteConsumed(boolean isAnyByteConsumed) {
|
||||
this.isAnyByteConsumed = isAnyByteConsumed;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package seaweed.hdfs;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
/**
|
||||
* The Read Buffer Manager for Rest AbfsClient.
|
||||
*/
|
||||
final class ReadBufferManager {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ReadBufferManager.class);
|
||||
|
||||
private static final int NUM_BUFFERS = 16;
|
||||
private static final int BLOCK_SIZE = 4 * 1024 * 1024;
|
||||
private static final int NUM_THREADS = 8;
|
||||
private static final int THRESHOLD_AGE_MILLISECONDS = 3000; // have to see if 3 seconds is a good threshold
|
||||
|
||||
private Thread[] threads = new Thread[NUM_THREADS];
|
||||
private byte[][] buffers; // array of byte[] buffers, to hold the data that is read
|
||||
private Stack<Integer> freeList = new Stack<>(); // indices in buffers[] array that are available
|
||||
|
||||
private Queue<ReadBuffer> readAheadQueue = new LinkedList<>(); // queue of requests that are not picked up by any worker thread yet
|
||||
private LinkedList<ReadBuffer> inProgressList = new LinkedList<>(); // requests being processed by worker threads
|
||||
private LinkedList<ReadBuffer> completedReadList = new LinkedList<>(); // buffers available for reading
|
||||
private static final ReadBufferManager BUFFER_MANAGER; // singleton, initialized in static initialization block
|
||||
|
||||
static {
|
||||
BUFFER_MANAGER = new ReadBufferManager();
|
||||
BUFFER_MANAGER.init();
|
||||
}
|
||||
|
||||
static ReadBufferManager getBufferManager() {
|
||||
return BUFFER_MANAGER;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
buffers = new byte[NUM_BUFFERS][];
|
||||
for (int i = 0; i < NUM_BUFFERS; i++) {
|
||||
buffers[i] = new byte[BLOCK_SIZE]; // same buffers are reused. The byte array never goes back to GC
|
||||
freeList.add(i);
|
||||
}
|
||||
for (int i = 0; i < NUM_THREADS; i++) {
|
||||
Thread t = new Thread(new ReadBufferWorker(i));
|
||||
t.setDaemon(true);
|
||||
threads[i] = t;
|
||||
t.setName("SeaweedFS-prefetch-" + i);
|
||||
t.start();
|
||||
}
|
||||
ReadBufferWorker.UNLEASH_WORKERS.countDown();
|
||||
}
|
||||
|
||||
// hide instance constructor
|
||||
private ReadBufferManager() {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
*
|
||||
* SeaweedInputStream-facing methods
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* {@link SeaweedInputStream} calls this method to queue read-aheads.
|
||||
*
|
||||
* @param stream The {@link SeaweedInputStream} for which to do the read-ahead
|
||||
* @param requestedOffset The offset in the file which shoukd be read
|
||||
* @param requestedLength The length to read
|
||||
*/
|
||||
void queueReadAhead(final SeaweedInputStream stream, final long requestedOffset, final int requestedLength) {
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Start Queueing readAhead for {} offset {} length {}",
|
||||
stream.getPath(), requestedOffset, requestedLength);
|
||||
}
|
||||
ReadBuffer buffer;
|
||||
synchronized (this) {
|
||||
if (isAlreadyQueued(stream, requestedOffset)) {
|
||||
return; // already queued, do not queue again
|
||||
}
|
||||
if (freeList.isEmpty() && !tryEvict()) {
|
||||
return; // no buffers available, cannot queue anything
|
||||
}
|
||||
|
||||
buffer = new ReadBuffer();
|
||||
buffer.setStream(stream);
|
||||
buffer.setOffset(requestedOffset);
|
||||
buffer.setLength(0);
|
||||
buffer.setRequestedLength(requestedLength);
|
||||
buffer.setStatus(ReadBufferStatus.NOT_AVAILABLE);
|
||||
buffer.setLatch(new CountDownLatch(1));
|
||||
|
||||
Integer bufferIndex = freeList.pop(); // will return a value, since we have checked size > 0 already
|
||||
|
||||
buffer.setBuffer(buffers[bufferIndex]);
|
||||
buffer.setBufferindex(bufferIndex);
|
||||
readAheadQueue.add(buffer);
|
||||
notifyAll();
|
||||
}
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Done q-ing readAhead for file {} offset {} buffer idx {}",
|
||||
stream.getPath(), requestedOffset, buffer.getBufferindex());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link SeaweedInputStream} calls this method read any bytes already available in a buffer (thereby saving a
|
||||
* remote read). This returns the bytes if the data already exists in buffer. If there is a buffer that is reading
|
||||
* the requested offset, then this method blocks until that read completes. If the data is queued in a read-ahead
|
||||
* but not picked up by a worker thread yet, then it cancels that read-ahead and reports cache miss. This is because
|
||||
* depending on worker thread availability, the read-ahead may take a while - the calling thread can do it's own
|
||||
* read to get the data faster (copmared to the read waiting in queue for an indeterminate amount of time).
|
||||
*
|
||||
* @param stream the file to read bytes for
|
||||
* @param position the offset in the file to do a read for
|
||||
* @param length the length to read
|
||||
* @param buffer the buffer to read data into. Note that the buffer will be written into from offset 0.
|
||||
* @return the number of bytes read
|
||||
*/
|
||||
int getBlock(final SeaweedInputStream stream, final long position, final int length, final byte[] buffer) {
|
||||
// not synchronized, so have to be careful with locking
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("getBlock for file {} position {} thread {}",
|
||||
stream.getPath(), position, Thread.currentThread().getName());
|
||||
}
|
||||
|
||||
waitForProcess(stream, position);
|
||||
|
||||
int bytesRead = 0;
|
||||
synchronized (this) {
|
||||
bytesRead = getBlockFromCompletedQueue(stream, position, length, buffer);
|
||||
}
|
||||
if (bytesRead > 0) {
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Done read from Cache for {} position {} length {}",
|
||||
stream.getPath(), position, bytesRead);
|
||||
}
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
// otherwise, just say we got nothing - calling thread can do its own read
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* Internal methods
|
||||
*
|
||||
*/
|
||||
|
||||
private void waitForProcess(final SeaweedInputStream stream, final long position) {
|
||||
ReadBuffer readBuf;
|
||||
synchronized (this) {
|
||||
clearFromReadAheadQueue(stream, position);
|
||||
readBuf = getFromList(inProgressList, stream, position);
|
||||
}
|
||||
if (readBuf != null) { // if in in-progress queue, then block for it
|
||||
try {
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("got a relevant read buffer for file {} offset {} buffer idx {}",
|
||||
stream.getPath(), readBuf.getOffset(), readBuf.getBufferindex());
|
||||
}
|
||||
readBuf.getLatch().await(); // blocking wait on the caller stream's thread
|
||||
// Note on correctness: readBuf gets out of inProgressList only in 1 place: after worker thread
|
||||
// is done processing it (in doneReading). There, the latch is set after removing the buffer from
|
||||
// inProgressList. So this latch is safe to be outside the synchronized block.
|
||||
// Putting it in synchronized would result in a deadlock, since this thread would be holding the lock
|
||||
// while waiting, so no one will be able to change any state. If this becomes more complex in the future,
|
||||
// then the latch cane be removed and replaced with wait/notify whenever inProgressList is touched.
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("latch done for file {} buffer idx {} length {}",
|
||||
stream.getPath(), readBuf.getBufferindex(), readBuf.getLength());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If any buffer in the completedlist can be reclaimed then reclaim it and return the buffer to free list.
|
||||
* The objective is to find just one buffer - there is no advantage to evicting more than one.
|
||||
*
|
||||
* @return whether the eviction succeeeded - i.e., were we able to free up one buffer
|
||||
*/
|
||||
private synchronized boolean tryEvict() {
|
||||
ReadBuffer nodeToEvict = null;
|
||||
if (completedReadList.size() <= 0) {
|
||||
return false; // there are no evict-able buffers
|
||||
}
|
||||
|
||||
// first, try buffers where all bytes have been consumed (approximated as first and last bytes consumed)
|
||||
for (ReadBuffer buf : completedReadList) {
|
||||
if (buf.isFirstByteConsumed() && buf.isLastByteConsumed()) {
|
||||
nodeToEvict = buf;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (nodeToEvict != null) {
|
||||
return evict(nodeToEvict);
|
||||
}
|
||||
|
||||
// next, try buffers where any bytes have been consumed (may be a bad idea? have to experiment and see)
|
||||
for (ReadBuffer buf : completedReadList) {
|
||||
if (buf.isAnyByteConsumed()) {
|
||||
nodeToEvict = buf;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeToEvict != null) {
|
||||
return evict(nodeToEvict);
|
||||
}
|
||||
|
||||
// next, try any old nodes that have not been consumed
|
||||
long earliestBirthday = Long.MAX_VALUE;
|
||||
for (ReadBuffer buf : completedReadList) {
|
||||
if (buf.getTimeStamp() < earliestBirthday) {
|
||||
nodeToEvict = buf;
|
||||
earliestBirthday = buf.getTimeStamp();
|
||||
}
|
||||
}
|
||||
if ((currentTimeMillis() - earliestBirthday > THRESHOLD_AGE_MILLISECONDS) && (nodeToEvict != null)) {
|
||||
return evict(nodeToEvict);
|
||||
}
|
||||
|
||||
// nothing can be evicted
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean evict(final ReadBuffer buf) {
|
||||
freeList.push(buf.getBufferindex());
|
||||
completedReadList.remove(buf);
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("Evicting buffer idx {}; was used for file {} offset {} length {}",
|
||||
buf.getBufferindex(), buf.getStream().getPath(), buf.getOffset(), buf.getLength());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isAlreadyQueued(final SeaweedInputStream stream, final long requestedOffset) {
|
||||
// returns true if any part of the buffer is already queued
|
||||
return (isInList(readAheadQueue, stream, requestedOffset)
|
||||
|| isInList(inProgressList, stream, requestedOffset)
|
||||
|| isInList(completedReadList, stream, requestedOffset));
|
||||
}
|
||||
|
||||
private boolean isInList(final Collection<ReadBuffer> list, final SeaweedInputStream stream, final long requestedOffset) {
|
||||
return (getFromList(list, stream, requestedOffset) != null);
|
||||
}
|
||||
|
||||
private ReadBuffer getFromList(final Collection<ReadBuffer> list, final SeaweedInputStream stream, final long requestedOffset) {
|
||||
for (ReadBuffer buffer : list) {
|
||||
if (buffer.getStream() == stream) {
|
||||
if (buffer.getStatus() == ReadBufferStatus.AVAILABLE
|
||||
&& requestedOffset >= buffer.getOffset()
|
||||
&& requestedOffset < buffer.getOffset() + buffer.getLength()) {
|
||||
return buffer;
|
||||
} else if (requestedOffset >= buffer.getOffset()
|
||||
&& requestedOffset < buffer.getOffset() + buffer.getRequestedLength()) {
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void clearFromReadAheadQueue(final SeaweedInputStream stream, final long requestedOffset) {
|
||||
ReadBuffer buffer = getFromList(readAheadQueue, stream, requestedOffset);
|
||||
if (buffer != null) {
|
||||
readAheadQueue.remove(buffer);
|
||||
notifyAll(); // lock is held in calling method
|
||||
freeList.push(buffer.getBufferindex());
|
||||
}
|
||||
}
|
||||
|
||||
private int getBlockFromCompletedQueue(final SeaweedInputStream stream, final long position, final int length,
|
||||
final byte[] buffer) {
|
||||
ReadBuffer buf = getFromList(completedReadList, stream, position);
|
||||
if (buf == null || position >= buf.getOffset() + buf.getLength()) {
|
||||
return 0;
|
||||
}
|
||||
int cursor = (int) (position - buf.getOffset());
|
||||
int availableLengthInBuffer = buf.getLength() - cursor;
|
||||
int lengthToCopy = Math.min(length, availableLengthInBuffer);
|
||||
System.arraycopy(buf.getBuffer(), cursor, buffer, 0, lengthToCopy);
|
||||
if (cursor == 0) {
|
||||
buf.setFirstByteConsumed(true);
|
||||
}
|
||||
if (cursor + lengthToCopy == buf.getLength()) {
|
||||
buf.setLastByteConsumed(true);
|
||||
}
|
||||
buf.setAnyByteConsumed(true);
|
||||
return lengthToCopy;
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* ReadBufferWorker-thread-facing methods
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* ReadBufferWorker thread calls this to get the next buffer that it should work on.
|
||||
*
|
||||
* @return {@link ReadBuffer}
|
||||
* @throws InterruptedException if thread is interrupted
|
||||
*/
|
||||
ReadBuffer getNextBlockToRead() throws InterruptedException {
|
||||
ReadBuffer buffer = null;
|
||||
synchronized (this) {
|
||||
//buffer = readAheadQueue.take(); // blocking method
|
||||
while (readAheadQueue.size() == 0) {
|
||||
wait();
|
||||
}
|
||||
buffer = readAheadQueue.remove();
|
||||
notifyAll();
|
||||
if (buffer == null) {
|
||||
return null; // should never happen
|
||||
}
|
||||
buffer.setStatus(ReadBufferStatus.READING_IN_PROGRESS);
|
||||
inProgressList.add(buffer);
|
||||
}
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("ReadBufferWorker picked file {} for offset {}",
|
||||
buffer.getStream().getPath(), buffer.getOffset());
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* ReadBufferWorker thread calls this method to post completion.
|
||||
*
|
||||
* @param buffer the buffer whose read was completed
|
||||
* @param result the {@link ReadBufferStatus} after the read operation in the worker thread
|
||||
* @param bytesActuallyRead the number of bytes that the worker thread was actually able to read
|
||||
*/
|
||||
void doneReading(final ReadBuffer buffer, final ReadBufferStatus result, final int bytesActuallyRead) {
|
||||
if (LOGGER.isTraceEnabled()) {
|
||||
LOGGER.trace("ReadBufferWorker completed file {} for offset {} bytes {}",
|
||||
buffer.getStream().getPath(), buffer.getOffset(), bytesActuallyRead);
|
||||
}
|
||||
synchronized (this) {
|
||||
inProgressList.remove(buffer);
|
||||
if (result == ReadBufferStatus.AVAILABLE && bytesActuallyRead > 0) {
|
||||
buffer.setStatus(ReadBufferStatus.AVAILABLE);
|
||||
buffer.setTimeStamp(currentTimeMillis());
|
||||
buffer.setLength(bytesActuallyRead);
|
||||
completedReadList.add(buffer);
|
||||
} else {
|
||||
freeList.push(buffer.getBufferindex());
|
||||
// buffer should go out of scope after the end of the calling method in ReadBufferWorker, and eligible for GC
|
||||
}
|
||||
}
|
||||
//outside the synchronized, since anyone receiving a wake-up from the latch must see safe-published results
|
||||
buffer.getLatch().countDown(); // wake up waiting threads (if any)
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to System.currentTimeMillis, except implemented with System.nanoTime().
|
||||
* System.currentTimeMillis can go backwards when system clock is changed (e.g., with NTP time synchronization),
|
||||
* making it unsuitable for measuring time intervals. nanotime is strictly monotonically increasing per CPU core.
|
||||
* Note: it is not monotonic across Sockets, and even within a CPU, its only the
|
||||
* more recent parts which share a clock across all cores.
|
||||
*
|
||||
* @return current time in milliseconds
|
||||
*/
|
||||
private long currentTimeMillis() {
|
||||
return System.nanoTime() / 1000 / 1000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package seaweed.hdfs;
|
||||
|
||||
/**
|
||||
* The ReadBufferStatus for Rest AbfsClient
|
||||
*/
|
||||
public enum ReadBufferStatus {
|
||||
NOT_AVAILABLE, // buffers sitting in readaheadqueue have this stats
|
||||
READING_IN_PROGRESS, // reading is in progress on this buffer. Buffer should be in inProgressList
|
||||
AVAILABLE, // data is available in buffer. It should be in completedList
|
||||
READ_FAILED // read completed, but failed.
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package seaweed.hdfs;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
class ReadBufferWorker implements Runnable {
|
||||
|
||||
protected static final CountDownLatch UNLEASH_WORKERS = new CountDownLatch(1);
|
||||
private int id;
|
||||
|
||||
ReadBufferWorker(final int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* return the ID of ReadBufferWorker.
|
||||
*/
|
||||
public int getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until a buffer becomes available in ReadAheadQueue.
|
||||
* Once a buffer becomes available, reads the file specified in it and then posts results back to buffer manager.
|
||||
* Rinse and repeat. Forever.
|
||||
*/
|
||||
public void run() {
|
||||
try {
|
||||
UNLEASH_WORKERS.await();
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
ReadBufferManager bufferManager = ReadBufferManager.getBufferManager();
|
||||
ReadBuffer buffer;
|
||||
while (true) {
|
||||
try {
|
||||
buffer = bufferManager.getNextBlockToRead(); // blocks, until a buffer is available for this thread
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
if (buffer != null) {
|
||||
try {
|
||||
// do the actual read, from the file.
|
||||
int bytesRead = buffer.getStream().readRemote(buffer.getOffset(), buffer.getBuffer(), 0, buffer.getRequestedLength());
|
||||
bufferManager.doneReading(buffer, ReadBufferStatus.AVAILABLE, bytesRead); // post result back to ReadBufferManager
|
||||
} catch (Exception ex) {
|
||||
bufferManager.doneReading(buffer, ReadBufferStatus.READ_FAILED, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
package seaweed.hdfs;
|
||||
|
||||
import org.apache.hadoop.conf.Configuration;
|
||||
import org.apache.hadoop.fs.FSDataInputStream;
|
||||
import org.apache.hadoop.fs.FSDataOutputStream;
|
||||
import org.apache.hadoop.fs.FileAlreadyExistsException;
|
||||
import org.apache.hadoop.fs.FileStatus;
|
||||
import org.apache.hadoop.fs.ParentNotDirectoryException;
|
||||
import org.apache.hadoop.fs.Path;
|
||||
import org.apache.hadoop.fs.UnsupportedFileSystemException;
|
||||
import org.apache.hadoop.fs.XAttrSetFlag;
|
||||
import org.apache.hadoop.fs.permission.AclEntry;
|
||||
import org.apache.hadoop.fs.permission.AclStatus;
|
||||
import org.apache.hadoop.fs.permission.FsPermission;
|
||||
import org.apache.hadoop.security.AccessControlException;
|
||||
import org.apache.hadoop.security.UserGroupInformation;
|
||||
import org.apache.hadoop.util.Progressable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.IO_FILE_BUFFER_SIZE_KEY;
|
||||
|
||||
public class SeaweedFileSystem extends org.apache.hadoop.fs.FileSystem {
|
||||
|
||||
public static final int FS_SEAWEED_DEFAULT_PORT = 8888;
|
||||
public static final String FS_SEAWEED_FILER_HOST = "fs.seaweed.filer.host";
|
||||
public static final String FS_SEAWEED_FILER_PORT = "fs.seaweed.filer.port";
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SeaweedFileSystem.class);
|
||||
private static int BUFFER_SIZE = 16 * 1024 * 1024;
|
||||
|
||||
private URI uri;
|
||||
private Path workingDirectory = new Path("/");
|
||||
private SeaweedFileSystemStore seaweedFileSystemStore;
|
||||
|
||||
public URI getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public String getScheme() {
|
||||
return "seaweedfs";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(URI uri, Configuration conf) throws IOException { // get
|
||||
super.initialize(uri, conf);
|
||||
|
||||
// get host information from uri (overrides info in conf)
|
||||
String host = uri.getHost();
|
||||
host = (host == null) ? conf.get(FS_SEAWEED_FILER_HOST, "localhost") : host;
|
||||
if (host == null) {
|
||||
throw new IOException("Invalid host specified");
|
||||
}
|
||||
conf.set(FS_SEAWEED_FILER_HOST, host);
|
||||
|
||||
// get port information from uri, (overrides info in conf)
|
||||
int port = uri.getPort();
|
||||
port = (port == -1) ? FS_SEAWEED_DEFAULT_PORT : port;
|
||||
conf.setInt(FS_SEAWEED_FILER_PORT, port);
|
||||
|
||||
conf.setInt(IO_FILE_BUFFER_SIZE_KEY, BUFFER_SIZE);
|
||||
|
||||
setConf(conf);
|
||||
this.uri = uri;
|
||||
|
||||
seaweedFileSystemStore = new SeaweedFileSystemStore(host, port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FSDataInputStream open(Path path, int bufferSize) throws IOException {
|
||||
|
||||
LOG.debug("open path: {} bufferSize:{}", path, bufferSize);
|
||||
|
||||
path = qualify(path);
|
||||
|
||||
try {
|
||||
InputStream inputStream = seaweedFileSystemStore.openFileForRead(path, statistics, bufferSize);
|
||||
return new FSDataInputStream(inputStream);
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FSDataOutputStream create(Path path, FsPermission permission, final boolean overwrite, final int bufferSize,
|
||||
final short replication, final long blockSize, final Progressable progress) throws IOException {
|
||||
|
||||
LOG.debug("create path: {} bufferSize:{} blockSize:{}", path, bufferSize, blockSize);
|
||||
|
||||
path = qualify(path);
|
||||
|
||||
try {
|
||||
String replicaPlacement = String.format("%03d", replication - 1);
|
||||
OutputStream outputStream = seaweedFileSystemStore.createFile(path, overwrite, permission, bufferSize, replicaPlacement);
|
||||
return new FSDataOutputStream(outputStream, statistics);
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FSDataOutputStream append(Path path, int bufferSize, Progressable progressable) throws IOException {
|
||||
|
||||
LOG.debug("append path: {} bufferSize:{}", path, bufferSize);
|
||||
|
||||
path = qualify(path);
|
||||
try {
|
||||
OutputStream outputStream = seaweedFileSystemStore.createFile(path, false, null, bufferSize, "");
|
||||
return new FSDataOutputStream(outputStream, statistics);
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rename(Path src, Path dst) {
|
||||
|
||||
LOG.debug("rename path: {} => {}", src, dst);
|
||||
|
||||
if (src.isRoot()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (src.equals(dst)) {
|
||||
return true;
|
||||
}
|
||||
FileStatus dstFileStatus = getFileStatus(dst);
|
||||
|
||||
String sourceFileName = src.getName();
|
||||
Path adjustedDst = dst;
|
||||
|
||||
if (dstFileStatus != null) {
|
||||
if (!dstFileStatus.isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
adjustedDst = new Path(dst, sourceFileName);
|
||||
}
|
||||
|
||||
Path qualifiedSrcPath = qualify(src);
|
||||
Path qualifiedDstPath = qualify(adjustedDst);
|
||||
|
||||
seaweedFileSystemStore.rename(qualifiedSrcPath, qualifiedDstPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(Path path, boolean recursive) {
|
||||
|
||||
LOG.debug("delete path: {} recursive:{}", path, recursive);
|
||||
|
||||
path = qualify(path);
|
||||
|
||||
FileStatus fileStatus = getFileStatus(path);
|
||||
|
||||
if (fileStatus == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return seaweedFileSystemStore.deleteEntries(path, fileStatus.isDirectory(), recursive);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileStatus[] listStatus(Path path) throws IOException {
|
||||
|
||||
LOG.debug("listStatus path: {}", path);
|
||||
|
||||
path = qualify(path);
|
||||
|
||||
return seaweedFileSystemStore.listEntries(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path getWorkingDirectory() {
|
||||
return workingDirectory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWorkingDirectory(Path path) {
|
||||
if (path.isAbsolute()) {
|
||||
workingDirectory = path;
|
||||
} else {
|
||||
workingDirectory = new Path(workingDirectory, path);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mkdirs(Path path, FsPermission fsPermission) throws IOException {
|
||||
|
||||
LOG.debug("mkdirs path: {}", path);
|
||||
|
||||
path = qualify(path);
|
||||
|
||||
FileStatus fileStatus = getFileStatus(path);
|
||||
|
||||
if (fileStatus == null) {
|
||||
|
||||
UserGroupInformation currentUser = UserGroupInformation.getCurrentUser();
|
||||
return seaweedFileSystemStore.createDirectory(path, currentUser,
|
||||
fsPermission == null ? FsPermission.getDirDefault() : fsPermission,
|
||||
FsPermission.getUMask(getConf()));
|
||||
|
||||
}
|
||||
|
||||
if (fileStatus.isDirectory()) {
|
||||
return true;
|
||||
} else {
|
||||
throw new FileAlreadyExistsException("Path is a file: " + path);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileStatus getFileStatus(Path path) {
|
||||
|
||||
LOG.debug("getFileStatus path: {}", path);
|
||||
|
||||
path = qualify(path);
|
||||
|
||||
return seaweedFileSystemStore.getFileStatus(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set owner of a path (i.e. a file or a directory).
|
||||
* The parameters owner and group cannot both be null.
|
||||
*
|
||||
* @param path The path
|
||||
* @param owner If it is null, the original username remains unchanged.
|
||||
* @param group If it is null, the original groupname remains unchanged.
|
||||
*/
|
||||
@Override
|
||||
public void setOwner(Path path, final String owner, final String group)
|
||||
throws IOException {
|
||||
LOG.debug("setOwner path: {}", path);
|
||||
path = qualify(path);
|
||||
|
||||
seaweedFileSystemStore.setOwner(path, owner, group);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set permission of a path.
|
||||
*
|
||||
* @param path The path
|
||||
* @param permission Access permission
|
||||
*/
|
||||
@Override
|
||||
public void setPermission(Path path, final FsPermission permission) throws IOException {
|
||||
LOG.debug("setPermission path: {}", path);
|
||||
|
||||
if (permission == null) {
|
||||
throw new IllegalArgumentException("The permission can't be null");
|
||||
}
|
||||
|
||||
path = qualify(path);
|
||||
|
||||
seaweedFileSystemStore.setPermission(path, permission);
|
||||
}
|
||||
|
||||
Path qualify(Path path) {
|
||||
return path.makeQualified(uri, workingDirectory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Concat existing files together.
|
||||
* @param trg the path to the target destination.
|
||||
* @param psrcs the paths to the sources to use for the concatenation.
|
||||
* @throws IOException IO failure
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default).
|
||||
*/
|
||||
@Override
|
||||
public void concat(final Path trg, final Path [] psrcs) throws IOException {
|
||||
throw new UnsupportedOperationException("Not implemented by the " +
|
||||
getClass().getSimpleName() + " FileSystem implementation");
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate the file in the indicated path to the indicated size.
|
||||
* <ul>
|
||||
* <li>Fails if path is a directory.</li>
|
||||
* <li>Fails if path does not exist.</li>
|
||||
* <li>Fails if path is not closed.</li>
|
||||
* <li>Fails if new size is greater than current size.</li>
|
||||
* </ul>
|
||||
* @param f The path to the file to be truncated
|
||||
* @param newLength The size the file is to be truncated to
|
||||
*
|
||||
* @return <code>true</code> if the file has been truncated to the desired
|
||||
* <code>newLength</code> and is immediately available to be reused for
|
||||
* write operations such as <code>append</code>, or
|
||||
* <code>false</code> if a background process of adjusting the length of
|
||||
* the last block has been started, and clients should wait for it to
|
||||
* complete before proceeding with further file updates.
|
||||
* @throws IOException IO failure
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default).
|
||||
*/
|
||||
@Override
|
||||
public boolean truncate(Path f, long newLength) throws IOException {
|
||||
throw new UnsupportedOperationException("Not implemented by the " +
|
||||
getClass().getSimpleName() + " FileSystem implementation");
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link FileContext#createSymlink(Path, Path, boolean)}.
|
||||
*/
|
||||
@Override
|
||||
public void createSymlink(final Path target, final Path link,
|
||||
final boolean createParent) throws AccessControlException,
|
||||
FileAlreadyExistsException, FileNotFoundException,
|
||||
ParentNotDirectoryException, UnsupportedFileSystemException,
|
||||
IOException {
|
||||
// Supporting filesystems should override this method
|
||||
throw new UnsupportedOperationException(
|
||||
"Filesystem does not support symlinks!");
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link AbstractFileSystem#supportsSymlinks()}.
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsSymlinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a snapshot.
|
||||
* @param path The directory where snapshots will be taken.
|
||||
* @param snapshotName The name of the snapshot
|
||||
* @return the snapshot path.
|
||||
* @throws IOException IO failure
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
*/
|
||||
@Override
|
||||
public Path createSnapshot(Path path, String snapshotName)
|
||||
throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support createSnapshot");
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a snapshot.
|
||||
* @param path The directory path where the snapshot was taken
|
||||
* @param snapshotOldName Old name of the snapshot
|
||||
* @param snapshotNewName New name of the snapshot
|
||||
* @throws IOException IO failure
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public void renameSnapshot(Path path, String snapshotOldName,
|
||||
String snapshotNewName) throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support renameSnapshot");
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a snapshot of a directory.
|
||||
* @param path The directory that the to-be-deleted snapshot belongs to
|
||||
* @param snapshotName The name of the snapshot
|
||||
* @throws IOException IO failure
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public void deleteSnapshot(Path path, String snapshotName)
|
||||
throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support deleteSnapshot");
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies ACL entries of files and directories. This method can add new ACL
|
||||
* entries or modify the permissions on existing ACL entries. All existing
|
||||
* ACL entries that are not specified in this call are retained without
|
||||
* changes. (Modifications are merged into the current ACL.)
|
||||
*
|
||||
* @param path Path to modify
|
||||
* @param aclSpec List<AclEntry> describing modifications
|
||||
* @throws IOException if an ACL could not be modified
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public void modifyAclEntries(Path path, List<AclEntry> aclSpec)
|
||||
throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support modifyAclEntries");
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes ACL entries from files and directories. Other ACL entries are
|
||||
* retained.
|
||||
*
|
||||
* @param path Path to modify
|
||||
* @param aclSpec List describing entries to remove
|
||||
* @throws IOException if an ACL could not be modified
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public void removeAclEntries(Path path, List<AclEntry> aclSpec)
|
||||
throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support removeAclEntries");
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all default ACL entries from files and directories.
|
||||
*
|
||||
* @param path Path to modify
|
||||
* @throws IOException if an ACL could not be modified
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public void removeDefaultAcl(Path path)
|
||||
throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support removeDefaultAcl");
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all but the base ACL entries of files and directories. The entries
|
||||
* for user, group, and others are retained for compatibility with permission
|
||||
* bits.
|
||||
*
|
||||
* @param path Path to modify
|
||||
* @throws IOException if an ACL could not be removed
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public void removeAcl(Path path)
|
||||
throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support removeAcl");
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully replaces ACL of files and directories, discarding all existing
|
||||
* entries.
|
||||
*
|
||||
* @param path Path to modify
|
||||
* @param aclSpec List describing modifications, which must include entries
|
||||
* for user, group, and others for compatibility with permission bits.
|
||||
* @throws IOException if an ACL could not be modified
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public void setAcl(Path path, List<AclEntry> aclSpec) throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support setAcl");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ACL of a file or directory.
|
||||
*
|
||||
* @param path Path to get
|
||||
* @return AclStatus describing the ACL of the file or directory
|
||||
* @throws IOException if an ACL could not be read
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public AclStatus getAclStatus(Path path) throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support getAclStatus");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an xattr of a file or directory.
|
||||
* The name must be prefixed with the namespace followed by ".". For example,
|
||||
* "user.attr".
|
||||
* <p>
|
||||
* Refer to the HDFS extended attributes user documentation for details.
|
||||
*
|
||||
* @param path Path to modify
|
||||
* @param name xattr name.
|
||||
* @param value xattr value.
|
||||
* @param flag xattr set flag
|
||||
* @throws IOException IO failure
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public void setXAttr(Path path, String name, byte[] value,
|
||||
EnumSet<XAttrSetFlag> flag) throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support setXAttr");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an xattr name and value for a file or directory.
|
||||
* The name must be prefixed with the namespace followed by ".". For example,
|
||||
* "user.attr".
|
||||
* <p>
|
||||
* Refer to the HDFS extended attributes user documentation for details.
|
||||
*
|
||||
* @param path Path to get extended attribute
|
||||
* @param name xattr name.
|
||||
* @return byte[] xattr value.
|
||||
* @throws IOException IO failure
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public byte[] getXAttr(Path path, String name) throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support getXAttr");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the xattr name/value pairs for a file or directory.
|
||||
* Only those xattrs which the logged-in user has permissions to view
|
||||
* are returned.
|
||||
* <p>
|
||||
* Refer to the HDFS extended attributes user documentation for details.
|
||||
*
|
||||
* @param path Path to get extended attributes
|
||||
* @return Map describing the XAttrs of the file or directory
|
||||
* @throws IOException IO failure
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public Map<String, byte[]> getXAttrs(Path path) throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support getXAttrs");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the xattrs name/value pairs for a file or directory.
|
||||
* Only those xattrs which the logged-in user has permissions to view
|
||||
* are returned.
|
||||
* <p>
|
||||
* Refer to the HDFS extended attributes user documentation for details.
|
||||
*
|
||||
* @param path Path to get extended attributes
|
||||
* @param names XAttr names.
|
||||
* @return Map describing the XAttrs of the file or directory
|
||||
* @throws IOException IO failure
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public Map<String, byte[]> getXAttrs(Path path, List<String> names)
|
||||
throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support getXAttrs");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the xattr names for a file or directory.
|
||||
* Only those xattr names which the logged-in user has permissions to view
|
||||
* are returned.
|
||||
* <p>
|
||||
* Refer to the HDFS extended attributes user documentation for details.
|
||||
*
|
||||
* @param path Path to get extended attributes
|
||||
* @return List{@literal <String>} of the XAttr names of the file or directory
|
||||
* @throws IOException IO failure
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public List<String> listXAttrs(Path path) throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support listXAttrs");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an xattr of a file or directory.
|
||||
* The name must be prefixed with the namespace followed by ".". For example,
|
||||
* "user.attr".
|
||||
* <p>
|
||||
* Refer to the HDFS extended attributes user documentation for details.
|
||||
*
|
||||
* @param path Path to remove extended attribute
|
||||
* @param name xattr name
|
||||
* @throws IOException IO failure
|
||||
* @throws UnsupportedOperationException if the operation is unsupported
|
||||
* (default outcome).
|
||||
*/
|
||||
@Override
|
||||
public void removeXAttr(Path path, String name) throws IOException {
|
||||
throw new UnsupportedOperationException(getClass().getSimpleName()
|
||||
+ " doesn't support removeXAttr");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
package seaweed.hdfs;
|
||||
|
||||
import org.apache.hadoop.fs.FileStatus;
|
||||
import org.apache.hadoop.fs.FileSystem;
|
||||
import org.apache.hadoop.fs.Path;
|
||||
import org.apache.hadoop.fs.permission.FsPermission;
|
||||
import org.apache.hadoop.security.UserGroupInformation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import seaweedfs.client.FilerClient;
|
||||
import seaweedfs.client.FilerGrpcClient;
|
||||
import seaweedfs.client.FilerProto;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class SeaweedFileSystemStore {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SeaweedFileSystemStore.class);
|
||||
|
||||
private FilerGrpcClient filerGrpcClient;
|
||||
private FilerClient filerClient;
|
||||
|
||||
public SeaweedFileSystemStore(String host, int port) {
|
||||
int grpcPort = 10000 + port;
|
||||
filerGrpcClient = new FilerGrpcClient(host, grpcPort);
|
||||
filerClient = new FilerClient(filerGrpcClient);
|
||||
}
|
||||
|
||||
public static String getParentDirectory(Path path) {
|
||||
return path.isRoot() ? "/" : path.getParent().toUri().getPath();
|
||||
}
|
||||
|
||||
static int permissionToMode(FsPermission permission, boolean isDirectory) {
|
||||
int p = permission.toShort();
|
||||
if (isDirectory) {
|
||||
p = p | 1 << 31;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
public boolean createDirectory(final Path path, UserGroupInformation currentUser,
|
||||
final FsPermission permission, final FsPermission umask) {
|
||||
|
||||
LOG.debug("createDirectory path: {} permission: {} umask: {}",
|
||||
path,
|
||||
permission,
|
||||
umask);
|
||||
|
||||
return filerClient.mkdirs(
|
||||
path.toUri().getPath(),
|
||||
permissionToMode(permission, true),
|
||||
currentUser.getUserName(),
|
||||
currentUser.getGroupNames()
|
||||
);
|
||||
}
|
||||
|
||||
public FileStatus[] listEntries(final Path path) {
|
||||
LOG.debug("listEntries path: {}", path);
|
||||
|
||||
List<FileStatus> fileStatuses = new ArrayList<FileStatus>();
|
||||
|
||||
List<FilerProto.Entry> entries = filerClient.listEntries(path.toUri().getPath());
|
||||
|
||||
for (FilerProto.Entry entry : entries) {
|
||||
|
||||
FileStatus fileStatus = doGetFileStatus(new Path(path, entry.getName()), entry);
|
||||
|
||||
fileStatuses.add(fileStatus);
|
||||
}
|
||||
return fileStatuses.toArray(new FileStatus[0]);
|
||||
}
|
||||
|
||||
public FileStatus getFileStatus(final Path path) {
|
||||
|
||||
FilerProto.Entry entry = lookupEntry(path);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
LOG.debug("doGetFileStatus path:{} entry:{}", path, entry);
|
||||
|
||||
FileStatus fileStatus = doGetFileStatus(path, entry);
|
||||
return fileStatus;
|
||||
}
|
||||
|
||||
public boolean deleteEntries(final Path path, boolean isDirectory, boolean recursive) {
|
||||
LOG.debug("deleteEntries path: {} isDirectory {} recursive: {}",
|
||||
path,
|
||||
String.valueOf(isDirectory),
|
||||
String.valueOf(recursive));
|
||||
|
||||
if (path.isRoot()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (recursive && isDirectory) {
|
||||
List<FilerProto.Entry> entries = filerClient.listEntries(path.toUri().getPath());
|
||||
for (FilerProto.Entry entry : entries) {
|
||||
deleteEntries(new Path(path, entry.getName()), entry.getIsDirectory(), true);
|
||||
}
|
||||
}
|
||||
|
||||
return filerClient.deleteEntry(getParentDirectory(path), path.getName(), true, recursive);
|
||||
}
|
||||
|
||||
private FileStatus doGetFileStatus(Path path, FilerProto.Entry entry) {
|
||||
FilerProto.FuseAttributes attributes = entry.getAttributes();
|
||||
long length = SeaweedRead.totalSize(entry.getChunksList());
|
||||
boolean isDir = entry.getIsDirectory();
|
||||
int block_replication = 1;
|
||||
int blocksize = 512;
|
||||
long modification_time = attributes.getMtime() * 1000; // milliseconds
|
||||
long access_time = 0;
|
||||
FsPermission permission = FsPermission.createImmutable((short) attributes.getFileMode());
|
||||
String owner = attributes.getUserName();
|
||||
String group = attributes.getGroupNameCount() > 0 ? attributes.getGroupName(0) : "";
|
||||
return new FileStatus(length, isDir, block_replication, blocksize,
|
||||
modification_time, access_time, permission, owner, group, null, path);
|
||||
}
|
||||
|
||||
private FilerProto.Entry lookupEntry(Path path) {
|
||||
|
||||
return filerClient.lookupEntry(getParentDirectory(path), path.getName());
|
||||
|
||||
}
|
||||
|
||||
public void rename(Path source, Path destination) {
|
||||
|
||||
LOG.debug("rename source: {} destination:{}", source, destination);
|
||||
|
||||
if (source.isRoot()) {
|
||||
return;
|
||||
}
|
||||
LOG.warn("rename lookupEntry source: {}", source);
|
||||
FilerProto.Entry entry = lookupEntry(source);
|
||||
if (entry == null) {
|
||||
LOG.warn("rename non-existing source: {}", source);
|
||||
return;
|
||||
}
|
||||
LOG.warn("rename moveEntry source: {}", source);
|
||||
moveEntry(source.getParent(), entry, destination);
|
||||
}
|
||||
|
||||
private boolean moveEntry(Path oldParent, FilerProto.Entry entry, Path destination) {
|
||||
|
||||
LOG.debug("moveEntry: {}/{} => {}", oldParent, entry.getName(), destination);
|
||||
|
||||
FilerProto.Entry.Builder newEntry = entry.toBuilder().setName(destination.getName());
|
||||
boolean isDirectoryCreated = filerClient.createEntry(getParentDirectory(destination), newEntry.build());
|
||||
|
||||
if (!isDirectoryCreated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entry.getIsDirectory()) {
|
||||
Path entryPath = new Path(oldParent, entry.getName());
|
||||
List<FilerProto.Entry> entries = filerClient.listEntries(entryPath.toUri().getPath());
|
||||
for (FilerProto.Entry ent : entries) {
|
||||
boolean isSucess = moveEntry(entryPath, ent, new Path(destination, ent.getName()));
|
||||
if (!isSucess) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filerClient.deleteEntry(
|
||||
oldParent.toUri().getPath(), entry.getName(), false, false);
|
||||
|
||||
}
|
||||
|
||||
public OutputStream createFile(final Path path,
|
||||
final boolean overwrite,
|
||||
FsPermission permission,
|
||||
int bufferSize,
|
||||
String replication) throws IOException {
|
||||
|
||||
permission = permission == null ? FsPermission.getFileDefault() : permission;
|
||||
|
||||
LOG.debug("createFile path: {} overwrite: {} permission: {}",
|
||||
path,
|
||||
overwrite,
|
||||
permission.toString());
|
||||
|
||||
UserGroupInformation userGroupInformation = UserGroupInformation.getCurrentUser();
|
||||
long now = System.currentTimeMillis() / 1000L;
|
||||
|
||||
FilerProto.Entry.Builder entry = null;
|
||||
long writePosition = 0;
|
||||
if (!overwrite) {
|
||||
FilerProto.Entry existingEntry = lookupEntry(path);
|
||||
LOG.debug("createFile merged entry path:{} existingEntry:{}", path, existingEntry);
|
||||
if (existingEntry != null) {
|
||||
entry = FilerProto.Entry.newBuilder();
|
||||
entry.mergeFrom(existingEntry);
|
||||
entry.getAttributesBuilder().setMtime(now);
|
||||
}
|
||||
LOG.debug("createFile merged entry path:{} entry:{} from:{}", path, entry, existingEntry);
|
||||
writePosition = SeaweedRead.totalSize(existingEntry.getChunksList());
|
||||
replication = existingEntry.getAttributes().getReplication();
|
||||
}
|
||||
if (entry == null) {
|
||||
entry = FilerProto.Entry.newBuilder()
|
||||
.setName(path.getName())
|
||||
.setIsDirectory(false)
|
||||
.setAttributes(FilerProto.FuseAttributes.newBuilder()
|
||||
.setFileMode(permissionToMode(permission, false))
|
||||
.setReplication(replication)
|
||||
.setCrtime(now)
|
||||
.setMtime(now)
|
||||
.setUserName(userGroupInformation.getUserName())
|
||||
.clearGroupName()
|
||||
.addAllGroupName(Arrays.asList(userGroupInformation.getGroupNames()))
|
||||
);
|
||||
}
|
||||
|
||||
return new SeaweedOutputStream(filerGrpcClient, path, entry, writePosition, bufferSize, replication);
|
||||
|
||||
}
|
||||
|
||||
public InputStream openFileForRead(final Path path, FileSystem.Statistics statistics,
|
||||
int bufferSize) throws IOException {
|
||||
|
||||
LOG.debug("openFileForRead path:{} bufferSize:{}", path, bufferSize);
|
||||
|
||||
int readAheadQueueDepth = 2;
|
||||
FilerProto.Entry entry = lookupEntry(path);
|
||||
|
||||
if (entry == null) {
|
||||
throw new FileNotFoundException("read non-exist file " + path);
|
||||
}
|
||||
|
||||
return new SeaweedInputStream(filerGrpcClient,
|
||||
statistics,
|
||||
path.toUri().getPath(),
|
||||
entry,
|
||||
bufferSize,
|
||||
readAheadQueueDepth);
|
||||
}
|
||||
|
||||
public void setOwner(Path path, String owner, String group) {
|
||||
|
||||
LOG.debug("setOwner path:{} owner:{} group:{}", path, owner, group);
|
||||
|
||||
FilerProto.Entry entry = lookupEntry(path);
|
||||
if (entry == null) {
|
||||
LOG.debug("setOwner path:{} entry:{}", path, entry);
|
||||
return;
|
||||
}
|
||||
|
||||
FilerProto.Entry.Builder entryBuilder = entry.toBuilder();
|
||||
FilerProto.FuseAttributes.Builder attributesBuilder = entry.getAttributes().toBuilder();
|
||||
|
||||
if (owner != null) {
|
||||
attributesBuilder.setUserName(owner);
|
||||
}
|
||||
if (group != null) {
|
||||
attributesBuilder.clearGroupName();
|
||||
attributesBuilder.addGroupName(group);
|
||||
}
|
||||
|
||||
entryBuilder.setAttributes(attributesBuilder);
|
||||
|
||||
LOG.debug("setOwner path:{} entry:{}", path, entryBuilder);
|
||||
|
||||
filerClient.updateEntry(getParentDirectory(path), entryBuilder.build());
|
||||
|
||||
}
|
||||
|
||||
public void setPermission(Path path, FsPermission permission) {
|
||||
|
||||
LOG.debug("setPermission path:{} permission:{}", path, permission);
|
||||
|
||||
FilerProto.Entry entry = lookupEntry(path);
|
||||
if (entry == null) {
|
||||
LOG.debug("setPermission path:{} entry:{}", path, entry);
|
||||
return;
|
||||
}
|
||||
|
||||
FilerProto.Entry.Builder entryBuilder = entry.toBuilder();
|
||||
FilerProto.FuseAttributes.Builder attributesBuilder = entry.getAttributes().toBuilder();
|
||||
|
||||
attributesBuilder.setFileMode(permissionToMode(permission, entry.getIsDirectory()));
|
||||
|
||||
entryBuilder.setAttributes(attributesBuilder);
|
||||
|
||||
LOG.debug("setPermission path:{} entry:{}", path, entryBuilder);
|
||||
|
||||
filerClient.updateEntry(getParentDirectory(path), entryBuilder.build());
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
package seaweed.hdfs;
|
||||
|
||||
// based on org.apache.hadoop.fs.azurebfs.services.AbfsInputStream
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.apache.hadoop.fs.FSExceptionMessages;
|
||||
import org.apache.hadoop.fs.FSInputStream;
|
||||
import org.apache.hadoop.fs.FileSystem.Statistics;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import seaweedfs.client.FilerGrpcClient;
|
||||
import seaweedfs.client.FilerProto;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class SeaweedInputStream extends FSInputStream {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SeaweedInputStream.class);
|
||||
|
||||
private final FilerGrpcClient filerGrpcClient;
|
||||
private final Statistics statistics;
|
||||
private final String path;
|
||||
private final FilerProto.Entry entry;
|
||||
private final List<SeaweedRead.VisibleInterval> visibleIntervalList;
|
||||
private final long contentLength;
|
||||
private final int bufferSize; // default buffer size
|
||||
private final int readAheadQueueDepth; // initialized in constructor
|
||||
private final boolean readAheadEnabled; // whether enable readAhead;
|
||||
|
||||
private byte[] buffer = null; // will be initialized on first use
|
||||
|
||||
private long fCursor = 0; // cursor of buffer within file - offset of next byte to read from remote server
|
||||
private long fCursorAfterLastRead = -1;
|
||||
private int bCursor = 0; // cursor of read within buffer - offset of next byte to be returned from buffer
|
||||
private int limit = 0; // offset of next byte to be read into buffer from service (i.e., upper marker+1
|
||||
// of valid bytes in buffer)
|
||||
private boolean closed = false;
|
||||
|
||||
public SeaweedInputStream(
|
||||
final FilerGrpcClient filerGrpcClient,
|
||||
final Statistics statistics,
|
||||
final String path,
|
||||
final FilerProto.Entry entry,
|
||||
final int bufferSize,
|
||||
final int readAheadQueueDepth) {
|
||||
this.filerGrpcClient = filerGrpcClient;
|
||||
this.statistics = statistics;
|
||||
this.path = path;
|
||||
this.entry = entry;
|
||||
this.contentLength = SeaweedRead.totalSize(entry.getChunksList());
|
||||
this.bufferSize = bufferSize;
|
||||
this.readAheadQueueDepth = (readAheadQueueDepth >= 0) ? readAheadQueueDepth : Runtime.getRuntime().availableProcessors();
|
||||
this.readAheadEnabled = true;
|
||||
|
||||
this.visibleIntervalList = SeaweedRead.nonOverlappingVisibleIntervals(entry.getChunksList());
|
||||
|
||||
LOG.debug("new path:{} entry:{} visibleIntervalList:{}", path, entry, visibleIntervalList);
|
||||
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
byte[] b = new byte[1];
|
||||
int numberOfBytesRead = read(b, 0, 1);
|
||||
if (numberOfBytesRead < 0) {
|
||||
return -1;
|
||||
} else {
|
||||
return (b[0] & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int read(final byte[] b, final int off, final int len) throws IOException {
|
||||
int currentOff = off;
|
||||
int currentLen = len;
|
||||
int lastReadBytes;
|
||||
int totalReadBytes = 0;
|
||||
do {
|
||||
lastReadBytes = readOneBlock(b, currentOff, currentLen);
|
||||
if (lastReadBytes > 0) {
|
||||
currentOff += lastReadBytes;
|
||||
currentLen -= lastReadBytes;
|
||||
totalReadBytes += lastReadBytes;
|
||||
}
|
||||
if (currentLen <= 0 || currentLen > b.length - currentOff) {
|
||||
break;
|
||||
}
|
||||
} while (lastReadBytes > 0);
|
||||
return totalReadBytes > 0 ? totalReadBytes : lastReadBytes;
|
||||
}
|
||||
|
||||
private int readOneBlock(final byte[] b, final int off, final int len) throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException(FSExceptionMessages.STREAM_IS_CLOSED);
|
||||
}
|
||||
|
||||
Preconditions.checkNotNull(b);
|
||||
|
||||
if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.available() == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (off < 0 || len < 0 || len > b.length - off) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
|
||||
//If buffer is empty, then fill the buffer.
|
||||
if (bCursor == limit) {
|
||||
//If EOF, then return -1
|
||||
if (fCursor >= contentLength) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
long bytesRead = 0;
|
||||
//reset buffer to initial state - i.e., throw away existing data
|
||||
bCursor = 0;
|
||||
limit = 0;
|
||||
if (buffer == null) {
|
||||
buffer = new byte[bufferSize];
|
||||
}
|
||||
|
||||
// Enable readAhead when reading sequentially
|
||||
if (-1 == fCursorAfterLastRead || fCursorAfterLastRead == fCursor || b.length >= bufferSize) {
|
||||
bytesRead = readInternal(fCursor, buffer, 0, bufferSize, false);
|
||||
} else {
|
||||
bytesRead = readInternal(fCursor, buffer, 0, b.length, true);
|
||||
}
|
||||
|
||||
if (bytesRead == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
limit += bytesRead;
|
||||
fCursor += bytesRead;
|
||||
fCursorAfterLastRead = fCursor;
|
||||
}
|
||||
|
||||
//If there is anything in the buffer, then return lesser of (requested bytes) and (bytes in buffer)
|
||||
//(bytes returned may be less than requested)
|
||||
int bytesRemaining = limit - bCursor;
|
||||
int bytesToRead = Math.min(len, bytesRemaining);
|
||||
System.arraycopy(buffer, bCursor, b, off, bytesToRead);
|
||||
bCursor += bytesToRead;
|
||||
if (statistics != null) {
|
||||
statistics.incrementBytesRead(bytesToRead);
|
||||
}
|
||||
return bytesToRead;
|
||||
}
|
||||
|
||||
|
||||
private int readInternal(final long position, final byte[] b, final int offset, final int length,
|
||||
final boolean bypassReadAhead) throws IOException {
|
||||
if (readAheadEnabled && !bypassReadAhead) {
|
||||
// try reading from read-ahead
|
||||
if (offset != 0) {
|
||||
throw new IllegalArgumentException("readahead buffers cannot have non-zero buffer offsets");
|
||||
}
|
||||
int receivedBytes;
|
||||
|
||||
// queue read-aheads
|
||||
int numReadAheads = this.readAheadQueueDepth;
|
||||
long nextSize;
|
||||
long nextOffset = position;
|
||||
while (numReadAheads > 0 && nextOffset < contentLength) {
|
||||
nextSize = Math.min((long) bufferSize, contentLength - nextOffset);
|
||||
ReadBufferManager.getBufferManager().queueReadAhead(this, nextOffset, (int) nextSize);
|
||||
nextOffset = nextOffset + nextSize;
|
||||
numReadAheads--;
|
||||
}
|
||||
|
||||
// try reading from buffers first
|
||||
receivedBytes = ReadBufferManager.getBufferManager().getBlock(this, position, length, b);
|
||||
if (receivedBytes > 0) {
|
||||
return receivedBytes;
|
||||
}
|
||||
|
||||
// got nothing from read-ahead, do our own read now
|
||||
receivedBytes = readRemote(position, b, offset, length);
|
||||
return receivedBytes;
|
||||
} else {
|
||||
return readRemote(position, b, offset, length);
|
||||
}
|
||||
}
|
||||
|
||||
int readRemote(long position, byte[] b, int offset, int length) throws IOException {
|
||||
if (position < 0) {
|
||||
throw new IllegalArgumentException("attempting to read from negative offset");
|
||||
}
|
||||
if (position >= contentLength) {
|
||||
return -1; // Hadoop prefers -1 to EOFException
|
||||
}
|
||||
if (b == null) {
|
||||
throw new IllegalArgumentException("null byte array passed in to read() method");
|
||||
}
|
||||
if (offset >= b.length) {
|
||||
throw new IllegalArgumentException("offset greater than length of array");
|
||||
}
|
||||
if (length < 0) {
|
||||
throw new IllegalArgumentException("requested read length is less than zero");
|
||||
}
|
||||
if (length > (b.length - offset)) {
|
||||
throw new IllegalArgumentException("requested read length is more than will fit after requested offset in buffer");
|
||||
}
|
||||
|
||||
long bytesRead = SeaweedRead.read(filerGrpcClient, visibleIntervalList, position, b, offset, length);
|
||||
if (bytesRead > Integer.MAX_VALUE) {
|
||||
throw new IOException("Unexpected Content-Length");
|
||||
}
|
||||
return (int) bytesRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to given position in stream.
|
||||
*
|
||||
* @param n position to seek to
|
||||
* @throws IOException if there is an error
|
||||
* @throws EOFException if attempting to seek past end of file
|
||||
*/
|
||||
@Override
|
||||
public synchronized void seek(long n) throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException(FSExceptionMessages.STREAM_IS_CLOSED);
|
||||
}
|
||||
if (n < 0) {
|
||||
throw new EOFException(FSExceptionMessages.NEGATIVE_SEEK);
|
||||
}
|
||||
if (n > contentLength) {
|
||||
throw new EOFException(FSExceptionMessages.CANNOT_SEEK_PAST_EOF);
|
||||
}
|
||||
|
||||
if (n >= fCursor - limit && n <= fCursor) { // within buffer
|
||||
bCursor = (int) (n - (fCursor - limit));
|
||||
return;
|
||||
}
|
||||
|
||||
// next read will read from here
|
||||
fCursor = n;
|
||||
|
||||
//invalidate buffer
|
||||
limit = 0;
|
||||
bCursor = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized long skip(long n) throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException(FSExceptionMessages.STREAM_IS_CLOSED);
|
||||
}
|
||||
long currentPos = getPos();
|
||||
if (currentPos == contentLength) {
|
||||
if (n > 0) {
|
||||
throw new EOFException(FSExceptionMessages.CANNOT_SEEK_PAST_EOF);
|
||||
}
|
||||
}
|
||||
long newPos = currentPos + n;
|
||||
if (newPos < 0) {
|
||||
newPos = 0;
|
||||
n = newPos - currentPos;
|
||||
}
|
||||
if (newPos > contentLength) {
|
||||
newPos = contentLength;
|
||||
n = newPos - currentPos;
|
||||
}
|
||||
seek(newPos);
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the size of the remaining available bytes
|
||||
* if the size is less than or equal to {@link Integer#MAX_VALUE},
|
||||
* otherwise, return {@link Integer#MAX_VALUE}.
|
||||
* <p>
|
||||
* This is to match the behavior of DFSInputStream.available(),
|
||||
* which some clients may rely on (HBase write-ahead log reading in
|
||||
* particular).
|
||||
*/
|
||||
@Override
|
||||
public synchronized int available() throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException(
|
||||
FSExceptionMessages.STREAM_IS_CLOSED);
|
||||
}
|
||||
final long remaining = this.contentLength - this.getPos();
|
||||
return remaining <= Integer.MAX_VALUE
|
||||
? (int) remaining : Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of the file that this stream refers to. Note that the length returned is the length
|
||||
* as of the time the Stream was opened. Specifically, if there have been subsequent appends to the file,
|
||||
* they wont be reflected in the returned length.
|
||||
*
|
||||
* @return length of the file.
|
||||
* @throws IOException if the stream is closed
|
||||
*/
|
||||
public long length() throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException(FSExceptionMessages.STREAM_IS_CLOSED);
|
||||
}
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current offset from the start of the file
|
||||
*
|
||||
* @throws IOException throws {@link IOException} if there is an error
|
||||
*/
|
||||
@Override
|
||||
public synchronized long getPos() throws IOException {
|
||||
if (closed) {
|
||||
throw new IOException(FSExceptionMessages.STREAM_IS_CLOSED);
|
||||
}
|
||||
return fCursor - limit + bCursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks a different copy of the data. Returns true if
|
||||
* found a new source, false otherwise.
|
||||
*
|
||||
* @throws IOException throws {@link IOException} if there is an error
|
||||
*/
|
||||
@Override
|
||||
public boolean seekToNewSource(long l) throws IOException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
closed = true;
|
||||
buffer = null; // de-reference the buffer so it can be GC'ed sooner
|
||||
}
|
||||
|
||||
/**
|
||||
* Not supported by this stream. Throws {@link UnsupportedOperationException}
|
||||
*
|
||||
* @param readlimit ignored
|
||||
*/
|
||||
@Override
|
||||
public synchronized void mark(int readlimit) {
|
||||
throw new UnsupportedOperationException("mark()/reset() not supported on this stream");
|
||||
}
|
||||
|
||||
/**
|
||||
* Not supported by this stream. Throws {@link UnsupportedOperationException}
|
||||
*/
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
throw new UnsupportedOperationException("mark()/reset() not supported on this stream");
|
||||
}
|
||||
|
||||
/**
|
||||
* gets whether mark and reset are supported by {@code ADLFileInputStream}. Always returns false.
|
||||
*
|
||||
* @return always {@code false}
|
||||
*/
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user