Compare commits
1300 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1a6c32d80 | |||
| bbd05a8f1c | |||
| 4393afb7e2 | |||
| 1a53734477 | |||
| e95c0bb913 | |||
| fce556b8a0 | |||
| b97f99766f | |||
| f7b6f7b22f | |||
| 2a0a62b78d | |||
| 67f66c40c1 | |||
| 5a2bdf6959 | |||
| 51e78a3e20 | |||
| 9eb52ec7c7 | |||
| 0827a2747b | |||
| e7a724de0d | |||
| d57d57ddf5 | |||
| a4d6195244 | |||
| 7d3eaf79ff | |||
| b63c37b28e | |||
| 8d59f35523 | |||
| f460eceb6d | |||
| d10679866c | |||
| a79a7e570c | |||
| 16ecaac4fc | |||
| ca6b4773ed | |||
| 7e7ae29d89 | |||
| 7d18f00116 | |||
| 719f8c258a | |||
| dcdb07433a | |||
| 26e9f78a86 | |||
| 34a1b58a75 | |||
| 32c6b62932 | |||
| 8d584bd819 | |||
| 031767791d | |||
| 3b917067d9 | |||
| 203ac8edaa | |||
| 8bbfb1b714 | |||
| b4ddccc2f7 | |||
| 6400f506da | |||
| 7195ac7f14 | |||
| 03df692ae2 | |||
| a011fe8450 | |||
| 95d97c2d6d | |||
| bc8b936d4b | |||
| d23ded0d83 | |||
| db3fed2279 | |||
| 866dffcd62 | |||
| 5eb210dd2e | |||
| 758d5458f0 | |||
| 8a40da3fd0 | |||
| 1d8a8c63db | |||
| 65a817fe8c | |||
| 1256b0b818 | |||
| 7e05b826fa | |||
| d82a1da511 | |||
| f598f4fd1b | |||
| e65d8fdc7d | |||
| b919462610 | |||
| 8287ab091c | |||
| f25bec6bf1 | |||
| 75865efb0e | |||
| 3d2d63f71e | |||
| 53e133e844 | |||
| b2cbade477 | |||
| 0c0d1e4150 | |||
| 3d0dced23c | |||
| ad53c5d859 | |||
| 45463b1d6b | |||
| d5895d3243 | |||
| db1edfe487 | |||
| 9c16f73334 | |||
| f50bec2987 | |||
| e2bff5cdc0 | |||
| 02a5f1e96a | |||
| 4deefa3695 | |||
| d28b3d8801 | |||
| 099b5293a9 | |||
| 4a0a491ca1 | |||
| db6b6e9518 | |||
| d8a11c8f4b | |||
| 0b546ddfd4 | |||
| d1712a46a7 | |||
| c46c2a6dd6 | |||
| 88938340b3 | |||
| 13a7033505 | |||
| 670e3e0c8b | |||
| 89d528a4ed | |||
| 53f3d2fd65 | |||
| a4f26aec00 | |||
| 60cc6184d2 | |||
| ce2d185211 | |||
| b05351c420 | |||
| b415c600e1 | |||
| 4c9fae90ff | |||
| 94670a387e | |||
| 789270af3c | |||
| a5fbe1e16c | |||
| 3c836b5f34 | |||
| 02af37a394 | |||
| 4879cd73f8 | |||
| b5dc4b5873 | |||
| 6dcfaa877c | |||
| 0bf981278e | |||
| d3eb5815d9 | |||
| 7e6b5bdbb7 | |||
| 4cadb33da2 | |||
| c3ff402fcb | |||
| aceaa1ec48 | |||
| 330f79b40e | |||
| 6d7df1d1cb | |||
| 3c0d3f7510 | |||
| f7ae3be586 | |||
| 70fec0a53f | |||
| fba1669966 | |||
| 71201273e2 | |||
| 79ba4d3f33 | |||
| f8024cadbb | |||
| f680b8482f | |||
| a56f7e2f23 | |||
| 6d5d49bfb1 | |||
| 8e4842b665 | |||
| b74ef6d5f4 | |||
| 9925640da8 | |||
| f45e0a44b8 | |||
| 3e632a49ee | |||
| 1bb3a03099 | |||
| 42b3795304 | |||
| 302fcb3b17 | |||
| b9fc4150f6 | |||
| f66239e82f | |||
| 6c2bc0568b | |||
| 0d154871d5 | |||
| 189b6682d6 | |||
| cf479eb401 | |||
| 72490bf8db | |||
| 5a73aecb5c | |||
| e26a706dff | |||
| 9e9c7b4f22 | |||
| ff8e2b5b4f | |||
| 61e0b1454a | |||
| 93f126364e | |||
| 6dca044ea8 | |||
| 9e9faf7b53 | |||
| 2bca51ab2c | |||
| 34e780e690 | |||
| 6cab6d802d | |||
| ea65350308 | |||
| 1bb9d49eaa | |||
| db5af1b126 | |||
| b233345f19 | |||
| 07ccf4c0d0 | |||
| 77d2870f5b | |||
| 3a73c675a6 | |||
| b694c1a4d7 | |||
| 2e6ee68409 | |||
| 7505bac037 | |||
| 9dca0c1889 | |||
| 69555f1224 | |||
| 879cef37a1 | |||
| 3d21119ec8 | |||
| a2f2044528 | |||
| 85fcee1919 | |||
| 617f2394fb | |||
| 1729e82361 | |||
| 58633a383a | |||
| d4b107adf4 | |||
| 53f84d6084 | |||
| d864e00e24 | |||
| b69aa9c4d0 | |||
| d20cdac304 | |||
| fffe4ac7e6 | |||
| 98b62cbec8 | |||
| 1978b9d8f9 | |||
| 3544e5ad01 | |||
| e7cdd8f02c | |||
| 3ff09b9b72 | |||
| b3b42c72a9 | |||
| 32da1aa9d6 | |||
| b10fa507b2 | |||
| 1e8925cea0 | |||
| f346ca44f0 | |||
| 6f7ace3d3e | |||
| 812142f007 | |||
| fa52d491c5 | |||
| 283ad7e5e5 | |||
| 748b1d6495 | |||
| b4dc6df35c | |||
| 218049300c | |||
| 5db533c024 | |||
| aeafe668d8 | |||
| 54f3a0946f | |||
| be6bd52978 | |||
| 75c43bfb6c | |||
| 0aca2ab970 | |||
| 28bc5899fd | |||
| fcc61fa46a | |||
| f7889e1af7 | |||
| bccc386994 | |||
| 0f401b67ad | |||
| 108decfa76 | |||
| 6afbd502e8 | |||
| ec5293ce29 | |||
| 4785555d34 | |||
| b918a6592f | |||
| eb787d8613 | |||
| 0a1501bc1b | |||
| f8bd9cfd83 | |||
| c59b995f7b | |||
| bc73a1a1cb | |||
| ce7c9c651d | |||
| c3dd7c1f6c | |||
| 2d23cd4f39 | |||
| ce7af3aae1 | |||
| 1b92c5136b | |||
| cc8a8cb877 | |||
| ce403fdaa0 | |||
| 45fbb0d618 | |||
| f5ce685aa1 | |||
| c83055500d | |||
| b0fbddc051 | |||
| 4e0c08e9c5 | |||
| 344f9ec608 | |||
| 28ff62716f | |||
| 1f69a75efa | |||
| 02a81ee564 | |||
| e91e9e8a38 | |||
| e45f9057d6 | |||
| 53e7fdc847 | |||
| 1af331c05c | |||
| 0e5b9c7fe4 | |||
| a86dc8a4c5 | |||
| c88dca984d | |||
| 1054f9cbf0 | |||
| aaacce4c43 | |||
| 7bd1f44491 | |||
| ba6d997d18 | |||
| eaa41e4086 | |||
| 6b70f429ed | |||
| 0a6e9a1834 | |||
| 15b65a8342 | |||
| 8b7df7da37 | |||
| 237c8af5ef | |||
| ff036c171f | |||
| a3b085300d | |||
| b90cefdb88 | |||
| f3506b8958 | |||
| 46897b1100 | |||
| cec8b238f3 | |||
| 13ab8e17e2 | |||
| 9d3d64df2d | |||
| 146bc3e638 | |||
| 0543d45fb3 | |||
| fe426944ea | |||
| c1a6ca0c33 | |||
| 5a78266821 | |||
| f5b4b0765a | |||
| 3928c1e14c | |||
| 94241cd153 | |||
| a86c2e2ce1 | |||
| e99244be02 | |||
| 58833711e0 | |||
| f99f218999 | |||
| 4fb5fc72d7 | |||
| d86973dcca | |||
| 8085ba4494 | |||
| 9bd0eb1a9e | |||
| e4e0abfc05 | |||
| 12b2fc894b | |||
| 2aa76e7407 | |||
| 0dab038858 | |||
| a76556ec1b | |||
| 91c7bb65c5 | |||
| 5d602034ea | |||
| b829ec4a6b | |||
| 9c50a9f567 | |||
| 52122c0309 | |||
| 3b9d313c87 | |||
| bfff251e2a | |||
| 52b500cce9 | |||
| c63cdca11f | |||
| 4430085981 | |||
| 5c78415b31 | |||
| bd67117756 | |||
| f3e5e9fb29 | |||
| 2de1921fe8 | |||
| dd7e47f264 | |||
| fe9d826bef | |||
| 6dbc5aba09 | |||
| de5374f74c | |||
| bf55591c64 | |||
| 2d814e340f | |||
| 0a8cf1a6b0 | |||
| 07949f68d8 | |||
| 909a89647b | |||
| 6508da5fde | |||
| 3379f005a5 | |||
| 4be609eb82 | |||
| dd5b975001 | |||
| 9ac3538141 | |||
| ace4f9fd15 | |||
| b283a2c21f | |||
| ed37bf3703 | |||
| d0f18dc1b5 | |||
| 8293f546af | |||
| af8071c86a | |||
| dce76d9307 | |||
| 11c4223f2c | |||
| 26903da8c2 | |||
| 353f2d3a6e | |||
| c8947af227 | |||
| 075b8903d7 | |||
| b8463a738c | |||
| 6f4862659f | |||
| f59d7a04b4 | |||
| b28d5fa633 | |||
| 48dbd49980 | |||
| 3dfe254a11 | |||
| ec2920e981 | |||
| 0f5483f497 | |||
| 8a6b0cc0cd | |||
| 2c56788f8d | |||
| 145328ac9f | |||
| 45a568dd85 | |||
| 7dcc1e92b4 | |||
| 999ae1cb96 | |||
| 6651c2fc5f | |||
| 976870a391 | |||
| f99987e47c | |||
| 1c3f244fc5 | |||
| 986aa8fabf | |||
| 2559614bfd | |||
| a4d1ef1b62 | |||
| 432bf7d99e | |||
| 18c4e5d357 | |||
| 64f4dbc272 | |||
| 9ba57a8df0 | |||
| 5621e6a494 | |||
| 0f0758aece | |||
| a0e02f43e1 | |||
| 28352f3f5d | |||
| 975972d57e | |||
| 458f22f37c | |||
| 0bba3cc8e3 | |||
| bac5303b10 | |||
| e01b2fc06d | |||
| 072fcf3ba6 | |||
| b78f6fbcc5 | |||
| 99ca8a2928 | |||
| 9b3dd44607 | |||
| 3c5db69ffd | |||
| 28c26a9e59 | |||
| 9136734c02 | |||
| 348aa6566c | |||
| 9d98bf1c0f | |||
| a767ad321a | |||
| 1886d94e95 | |||
| 542f7ae42c | |||
| 59b3e0b79b | |||
| 8975da4e84 | |||
| cae4683971 | |||
| 5db1e9f3dd | |||
| 465274cd21 | |||
| 87fb911d38 | |||
| 155a90403a | |||
| c63afabc9b | |||
| 0404e747cf | |||
| 3bc9e6101c | |||
| f2ed149714 | |||
| 5a16dcf4cf | |||
| fa3d5d0f46 | |||
| bb292e4e38 | |||
| d1971b9a4d | |||
| fa3f6d75b6 | |||
| 298b470f69 | |||
| fc774957fe | |||
| c6258f5e97 | |||
| 85bc6003e9 | |||
| 0d9de50e21 | |||
| d13aa1c42d | |||
| 610dbe3479 | |||
| 2df1e2e9a9 | |||
| e372b5ed67 | |||
| 76f4f20609 | |||
| 99ddd35343 | |||
| 01e9adc4b3 | |||
| 4c63fd06c6 | |||
| bb4efbf258 | |||
| a89c7299d1 | |||
| df027a8f51 | |||
| a02575ebf9 | |||
| 972a527b66 | |||
| 02910725c5 | |||
| 0ea8bfaf78 | |||
| 069cf9e8aa | |||
| 2121b78ea7 | |||
| 5016649f47 | |||
| 2b78444056 | |||
| 9b58a669e5 | |||
| e5b2e25caf | |||
| 4aa65910e5 | |||
| 8b272a3163 | |||
| f0b5c0ec7c | |||
| 44d53c9c67 | |||
| 6cf6828a4c | |||
| 01f04c717e | |||
| 0d32b22359 | |||
| cab298d68f | |||
| 1ea53b4d9f | |||
| d36dd80a8a | |||
| 3a539ce660 | |||
| 57c5c75611 | |||
| cf1fc45142 | |||
| 64c1c0f37d | |||
| 82857cd6df | |||
| 3cdc0c57c8 | |||
| dc365bca44 | |||
| 3b1626216d | |||
| 48aa2ac392 | |||
| 0d1e2ab509 | |||
| ebf4c447bb | |||
| 2b96d9f706 | |||
| 83abad0b37 | |||
| df346753e1 | |||
| f1d7780167 | |||
| 5a3c5aec31 | |||
| a4305742e8 | |||
| 640ebb2f79 | |||
| 402a5e3bea | |||
| f4dac979a2 | |||
| 3032f0f505 | |||
| 7765081db7 | |||
| 18bfe5cba6 | |||
| 14f0047295 | |||
| e6b2253da9 | |||
| ca64b86112 | |||
| 5f065e2a96 | |||
| a7afa469e2 | |||
| d103d5fb7c | |||
| 6683247080 | |||
| e4bd882f11 | |||
| 4448285a83 | |||
| 952c618441 | |||
| c5249c35d3 | |||
| de204a0a52 | |||
| 1b1b9e4801 | |||
| 1b0b2c1c76 | |||
| 7f5e037846 | |||
| 929a13f33f | |||
| a4209c10ea | |||
| b927523223 | |||
| 604417baf4 | |||
| 6e9ac8db59 | |||
| 0497d5c342 | |||
| 6b4f368dfe | |||
| 6507c30bbc | |||
| f44f2e341c | |||
| 60ea2b17ba | |||
| 5ed1a8ad23 | |||
| 27749c2124 | |||
| 1d99a560e3 | |||
| af82d27018 | |||
| f205689ff5 | |||
| 393c01d078 | |||
| e9037b9d36 | |||
| 4747adfcb4 | |||
| 52d8f564bf | |||
| 3fe33e7b15 | |||
| 4041e5f20d | |||
| 96c46c15e7 | |||
| 3bf67668b6 | |||
| e31e2c3bc2 | |||
| 214279aa57 | |||
| 4de6b15fca | |||
| 2bd694dbc8 | |||
| a2a8d54bb6 | |||
| 2040d32ef8 | |||
| f1015a5096 | |||
| 096427f973 | |||
| 5cec6bd80d | |||
| 6e9c91f43a | |||
| 2df8160f6a | |||
| 447fdd4097 | |||
| 28c5a887de | |||
| fbe8b3259d | |||
| e2aba9196f | |||
| 61b08137b0 | |||
| 1caad902cb | |||
| d99efa2c93 | |||
| 9ee83b89bb | |||
| a8cb43926a | |||
| b62cd8ed84 | |||
| 1b3a517683 | |||
| a9c0f1e0a4 | |||
| a099319e66 | |||
| 6e63904048 | |||
| 565ac4c861 | |||
| a4ecd8bca2 | |||
| 28eff0f6c1 | |||
| c27ece409b | |||
| 8c0ce2fee9 | |||
| d3cb79a57c | |||
| 11e15f9b4c | |||
| 4e92b2ecb8 | |||
| 80b83a51a3 | |||
| 777d12d928 | |||
| 607c8a9611 | |||
| 7f5a5b5e9d | |||
| d4ca2ee1a3 | |||
| 417ec0df56 | |||
| fbfb4fc5a0 | |||
| 97dc34fe06 | |||
| 9f9ba1e984 | |||
| 13c4ce3617 | |||
| 181e002c56 | |||
| 9d6e226692 | |||
| eb02261642 | |||
| 1396e91dd1 | |||
| 7ebf11b202 | |||
| 1c97dcb10a | |||
| 3a6111eff5 | |||
| 2463ae243a | |||
| c192e5c9b2 | |||
| d4eea224d4 | |||
| e55421ebdd | |||
| 0a5d57a91e | |||
| cea4cfa3a8 | |||
| 8426cf9aec | |||
| f3334159a4 | |||
| 3edff1501e | |||
| 21d73a3eef | |||
| 051f9bb5c6 | |||
| 6509589adb | |||
| ae4361cc45 | |||
| cad0d0eb7a | |||
| ba0c11757e | |||
| 96050c1e21 | |||
| a27d1b3d86 | |||
| a5e60706a2 | |||
| 5f7565762e | |||
| 3314501f19 | |||
| 9d53a646a1 | |||
| 5e0032e165 | |||
| 305952d734 | |||
| e77885d671 | |||
| 2410eb281e | |||
| e54025805f | |||
| d602495600 | |||
| 03937e7554 | |||
| e3b627a192 | |||
| ccc3349f0c | |||
| 85cbd875fc | |||
| 43317530d5 | |||
| 8a9852220d | |||
| 47ac410ab0 | |||
| 433225ab0d | |||
| 3e655a2c85 | |||
| a2eae54d11 | |||
| 79e0b9e69a | |||
| 29ff9674a0 | |||
| bff4d29415 | |||
| 436db49bf3 | |||
| 966818955e | |||
| e49efcb9d9 | |||
| 6f931d29c4 | |||
| e3fd4c0dd6 | |||
| b05c1c11d4 | |||
| 0bae3330e8 | |||
| 05e53f1b34 | |||
| 0c8c463a63 | |||
| 6df7bc42b8 | |||
| 6c00a57a7c | |||
| bc52d911ef | |||
| 28c53a3555 | |||
| e5d4e7aa9d | |||
| cddc684559 | |||
| 32d0d3d4ac | |||
| 98d17d2a97 | |||
| 208dd15245 | |||
| a179fc9658 | |||
| d1afd16955 | |||
| 2348ae7a19 | |||
| 480ea826dc | |||
| 09e9fd745c | |||
| 77c1998a38 | |||
| 91cf54f895 | |||
| 2b0ed21f08 | |||
| 472fa4a6ca | |||
| 097cec676a | |||
| fcad4a44fd | |||
| 9b3c014bab | |||
| c9619673fb | |||
| dce0345f8f | |||
| 461b2bbd37 | |||
| 69777b654e | |||
| 70d2cb5f4d | |||
| cff45db1b9 | |||
| bcc5b6e1ef | |||
| 25b5a0534f | |||
| 52d6678bf0 | |||
| d8950ba7c5 | |||
| cc28765025 | |||
| fe86319c56 | |||
| 99a12613a3 | |||
| 097dd7418a | |||
| a7d3ea8c15 | |||
| 2745bf2f1f | |||
| 70d825c608 | |||
| 6b68c0170f | |||
| 0a905e1a8a | |||
| 9eb8e375c5 | |||
| 7ea1de8245 | |||
| 54a18592e9 | |||
| 7e84c7427d | |||
| d12f3e06b1 | |||
| 0137ff498a | |||
| 34d9a6b46a | |||
| 50b4e54a75 | |||
| 8816b08aae | |||
| 271e3ecde5 | |||
| 611bd68739 | |||
| 25eeb88a8f | |||
| 3e770defae | |||
| ae4656e699 | |||
| f6a9e690bf | |||
| 7f86a21317 | |||
| fb39c7c26b | |||
| 13c3b9cbcb | |||
| 602dac8773 | |||
| c6e76160ad | |||
| a53b909fcd | |||
| 22909c849e | |||
| 440866d26c | |||
| 25b936c369 | |||
| 8f66cfa316 | |||
| 8912b6bf3b | |||
| a799351926 | |||
| 9df01035da | |||
| 1ad96ee09f | |||
| 1b4bb94ac4 | |||
| f38222c0cc | |||
| c9b1468c3b | |||
| 6800902b43 | |||
| e5773e11c6 | |||
| 0c7694894b | |||
| ce9aa2f2b2 | |||
| 7547f3c8a3 | |||
| 31dff87903 | |||
| fd72c21e0e | |||
| 310bf5bd36 | |||
| a170e44689 | |||
| 70e70446bb | |||
| 45d9cfa0c5 | |||
| 93fd269329 | |||
| 6717a0b68c | |||
| 533338bdeb | |||
| b6ebf2aba8 | |||
| ed4fcb63f7 | |||
| 4b4cb07fb6 | |||
| fc446e0b81 | |||
| f612a7dd85 | |||
| 0472e5c1e1 | |||
| 6a6c930f5b | |||
| 35ca3e5d9b | |||
| a9ab01731f | |||
| cd80e6df29 | |||
| 4dea4f3b89 | |||
| 1b30a3be2b | |||
| f7f103725b | |||
| 77a192a7b5 | |||
| cb48517a78 | |||
| e9394dc22d | |||
| 46743c7918 | |||
| b288eaddb3 | |||
| 2665aba555 | |||
| 9b6bcb30d9 | |||
| ac9ba13c19 | |||
| 864b8795aa | |||
| cc1575f944 | |||
| b408d0e87d | |||
| 69b81af93e | |||
| cf558ff6d1 | |||
| 5af7bd6a01 | |||
| 73f4f29110 | |||
| ccd949d8ca | |||
| e1bc99e4fe | |||
| 02194ee3c6 | |||
| 85f2b74cfd | |||
| 152cdf1c05 | |||
| 17dd1c19df | |||
| 8e49a3d047 | |||
| cccf77229d | |||
| dafdc74605 | |||
| c753b5dfeb | |||
| 82373e3d50 | |||
| 76b8abcd8e | |||
| dbb5408906 | |||
| 18c9d49a32 | |||
| d41dcb784b | |||
| 659d5aabd1 | |||
| b4343a28b7 | |||
| 0e693e0284 | |||
| fc880f9b23 | |||
| cead24b0f7 | |||
| 8489f22fe2 | |||
| fc6f804865 | |||
| d1d89116f1 | |||
| e1f64141a2 | |||
| 586058f079 | |||
| 4e926b292f | |||
| 47358e3104 | |||
| 3640c63289 | |||
| 51fa4f7fe3 | |||
| c3f7d1026f | |||
| 80f1387877 | |||
| 0674fa43ff | |||
| a17f1e875c | |||
| 9e1f1b50e0 | |||
| 0715032598 | |||
| dfc2ef3004 | |||
| 62f8343879 | |||
| f803bb4b3d | |||
| 20a65981bd | |||
| 09b450d610 | |||
| c194b9f5f1 | |||
| 98a6a2bcab | |||
| 1c699d8d3f | |||
| a054c73e22 | |||
| b580ad24e2 | |||
| f8a3b1e164 | |||
| bf2b8879b7 | |||
| caecd75a2a | |||
| 2959c104b3 | |||
| 50796e481d | |||
| f64f8b03cb | |||
| 7b85756c64 | |||
| d6a327fbc5 | |||
| f24753812a | |||
| 08b6cfb082 | |||
| 12a7a15daa | |||
| 7c6d77734a | |||
| b0cfceb211 | |||
| 0563a9235a | |||
| eb6d53d2f5 | |||
| 95d9e47353 | |||
| e1142e99f2 | |||
| a091fe3ed6 | |||
| f37f56ac15 | |||
| 2f4a7483ea | |||
| 464f9d34d6 | |||
| 926c75d0b5 | |||
| c8f57133a4 | |||
| cae62ce543 | |||
| 7bbb532b4b | |||
| 6d10f4c19a | |||
| cde6496172 | |||
| 8562b22823 | |||
| dd68cdd802 | |||
| 41cf580bb1 | |||
| 00baec1746 | |||
| f9025652df | |||
| ee0172dfe4 | |||
| 69559aa101 | |||
| 36fd317eb2 | |||
| e8ce3b64ed | |||
| 6ee27daac1 | |||
| cdc6c2d578 | |||
| 855ef4f1aa | |||
| 5878fcc086 | |||
| 1ac36a95aa | |||
| 15b4c49621 | |||
| f57f773189 | |||
| e7b4e4e105 | |||
| 1d134c1a94 | |||
| 329a910b86 | |||
| 4309727354 | |||
| ef3319a49d | |||
| d8e4d3c9c8 | |||
| faa6b1e925 | |||
| 85c6bb9809 | |||
| 5404dddcea | |||
| 4692fdbb8f | |||
| 1875a47495 | |||
| 9c9f390350 | |||
| 1b2b16998f | |||
| 7a17b2a585 | |||
| f7766b49aa | |||
| b2f920a868 | |||
| 8ceb969445 | |||
| 0db484c8f6 | |||
| 5c481fbf6e | |||
| 8e6e9301ce | |||
| 664ff063a1 | |||
| d9fd6f9a96 | |||
| b28ff50126 | |||
| 3fe2d77b70 | |||
| ab49498fc3 | |||
| 46fdd70114 | |||
| 29d72b84c0 | |||
| f53fcdf10e | |||
| 8e665105b1 | |||
| 2062add05f | |||
| 6b4e6bcebf | |||
| 2d6f8153fa | |||
| 5c10f4adf0 | |||
| 4daa0d2cee | |||
| cd0f350c02 | |||
| ac554bf663 | |||
| b363709c11 | |||
| 410b579e87 | |||
| 7b7c0bba58 | |||
| 0cef971832 | |||
| 5c9a95df32 | |||
| 3b455d6137 | |||
| b2a0e5754b | |||
| 63d9bb626a | |||
| 372da5eaf5 | |||
| cf17fc7774 | |||
| 67509453d3 | |||
| d31f256020 | |||
| d67f47927c | |||
| 85bb5870a9 | |||
| 4a9b205a15 | |||
| f3b346cbb3 | |||
| ff4ce0ee14 | |||
| 918924796f | |||
| feb6685359 | |||
| 0d59ea1e94 | |||
| 38edd94282 | |||
| 8021061bd8 | |||
| bc9509bc8a | |||
| e3b4910b66 | |||
| d056f19d07 | |||
| 834007728c | |||
| a609a4126c | |||
| bcd1a2308b | |||
| 694bad434c | |||
| 01f625824a | |||
| 9ccfb70104 | |||
| 201a20ac02 | |||
| f3322e94c8 | |||
| 46a6fde813 | |||
| fd1f09a66c | |||
| 752ed7915b | |||
| d3064e40b3 | |||
| 39f9324616 | |||
| a822b8e782 | |||
| a6922f94ee | |||
| b4d57bf4c4 | |||
| 0f2e493c9a | |||
| 4ef2d8940c | |||
| 96099a1e97 | |||
| 0e87f29de9 | |||
| dd74e5a809 | |||
| 12c1abed98 | |||
| e2ef95af7d | |||
| d711ff454e | |||
| c667d20dfc | |||
| 6efee2072d | |||
| 7a5bbf7a2e | |||
| 14cb3645a3 | |||
| f4f512fedd | |||
| 1983925dcf | |||
| 22c98d3fa2 | |||
| 01ae5bb39c | |||
| 11faf3f16d | |||
| d3df6c711a | |||
| ed6e781679 | |||
| baf1c1638d | |||
| 825000bc34 | |||
| c1ebcbcda2 | |||
| 41a3a9e402 | |||
| 1d4ac4b084 | |||
| 71b357e4f2 | |||
| 339c9019b9 | |||
| dd93f808c8 | |||
| 4098025c11 | |||
| bef0a50bc1 | |||
| 71ada9d6f8 | |||
| 33c022fcec | |||
| 273228fafa | |||
| aa98702908 | |||
| 5197649081 | |||
| 066f64d34a | |||
| afa4c7c3ef | |||
| 38537c7df2 | |||
| eed9ab0464 | |||
| 0b9f0d14a1 | |||
| ffbee70e04 | |||
| 2c3a2241e7 | |||
| 1c47365445 | |||
| 81eb7c0301 | |||
| 4a926d19a7 | |||
| 05dc52a206 | |||
| c91d3791f9 | |||
| df8153859c | |||
| 2f43709f85 | |||
| 61d67a061c | |||
| 1b85302161 | |||
| 6512d9978e | |||
| e216201901 | |||
| 7abcededf2 | |||
| 398421b9f5 | |||
| b8f0d9352f | |||
| f234c35020 | |||
| 3bcf7b7593 | |||
| 0c042a622a | |||
| c57a358c9d | |||
| 380d6c6435 | |||
| 716316f711 | |||
| 98e79b4b50 | |||
| e2216a0936 | |||
| e8d9d710d0 | |||
| a8ab02a73a | |||
| cf022de4d5 | |||
| 3995e21c5b | |||
| 2f7fb78692 | |||
| cf2fb30ac7 | |||
| 51d1e6f75b | |||
| 2f373684f5 | |||
| 5741a53d46 | |||
| dd0698d14c | |||
| 3e67bfcc88 | |||
| ea579f5b69 | |||
| daf6f3a5c0 | |||
| d44e9d6da9 | |||
| 9c2cfb5cb6 | |||
| 91a0ade908 | |||
| 5eb4002bf7 | |||
| 85a5c358d8 | |||
| 33c771bb3e | |||
| 286a8924fd | |||
| efca29b00e | |||
| 9161016962 | |||
| 5ff30777e1 | |||
| 754c0770d6 | |||
| 1105508453 | |||
| 1ba497950c | |||
| bf3c93a8cc | |||
| eb1bc67db1 | |||
| 8408dfaa6c | |||
| d192044915 | |||
| d9bab6b3bd | |||
| 4503491a0a | |||
| e6965ca066 | |||
| d9674f7524 | |||
| f024deb1f8 | |||
| 79601d27f2 | |||
| 226a69fe15 | |||
| 9bb799462e | |||
| 8bffa78f7f | |||
| 4b302173ae | |||
| 9db2205db3 | |||
| 6914fe436c | |||
| 807cc3c28d | |||
| f3c6c55719 | |||
| a773a6dce6 | |||
| 62dcee3b14 | |||
| 490159ea89 | |||
| 3e8cb8c937 | |||
| dd6ecf1193 | |||
| 90e1803798 | |||
| a47ce7ab22 | |||
| 0b3282ac9f | |||
| 79b98b5c25 | |||
| 6a57f2c1f0 | |||
| 0fa2477cb0 | |||
| 7ffb337cd7 | |||
| a9dd2793f9 | |||
| 9e2d0ac50b | |||
| 30dc11a931 | |||
| a15dc5fed5 | |||
| f7c20b97a1 | |||
| e9c45102b0 | |||
| 8871eb8e1e | |||
| 0f32efb825 | |||
| 63f1b4fdf4 | |||
| 31f2db6880 | |||
| 2c9b406f6c | |||
| 12cd2da265 | |||
| 485c0ea8bf | |||
| 49ba07d1d6 | |||
| 35e541e0b1 | |||
| 3977d6b7bd | |||
| 0905398459 | |||
| f41faf96b7 | |||
| 36639b65a9 | |||
| 7fc598b73f | |||
| 21d41ad7fd | |||
| fe56220d1a | |||
| 5782ec3ada | |||
| 8839c5105a | |||
| e293f079f5 | |||
| 6c2d5e3d05 | |||
| 5999a23d3e | |||
| e51be73ac7 | |||
| 4b5b363c6c | |||
| f2b0c08e34 | |||
| 83b364891d | |||
| 87af2dbc43 | |||
| 885bac330b | |||
| e3ab478c70 | |||
| ecaccefd2e | |||
| ece559afe2 | |||
| d3aaf50a40 | |||
| 43ce028840 | |||
| 32c3a558e9 | |||
| 41f9ab1c69 | |||
| 8876e0a80a | |||
| c21ac80268 | |||
| 0e2cd1a64d | |||
| 6274727b71 | |||
| 95567c68bf | |||
| c189337b6e | |||
| 6fc81dc162 | |||
| 7d50361ca9 | |||
| 19c51f3f3c | |||
| c3bbadacbf | |||
| 2208992e6a | |||
| bbba8e432a | |||
| 39331b6b4e | |||
| 6e748cb1cf | |||
| 2005d656e6 | |||
| 0f26ec8095 | |||
| 8d2347bc7b | |||
| 0ff359ca0e | |||
| f8e13fb00e | |||
| 686a610fc3 | |||
| 7fc1685b7a | |||
| b89609dc2e | |||
| fa50312220 | |||
| fee3f99a6e | |||
| ea406754a6 | |||
| d02cb963d5 | |||
| 334cdb5d64 | |||
| a681af6953 | |||
| 5c3639c1b7 | |||
| 903574db90 | |||
| f463d3ce42 | |||
| 17eeec6895 | |||
| 070d3610ff | |||
| f22862aa28 | |||
| 18be3bc95a | |||
| 2f520ed92f | |||
| c03ce0f74a | |||
| 0320a77dc0 | |||
| 3349153058 | |||
| 5e86352464 | |||
| 18d125ef1c | |||
| 31be826f51 | |||
| eb372d53df | |||
| 1788c58d5c | |||
| 17e49a9ed2 | |||
| 0aabc1d8d9 | |||
| 55f6828750 | |||
| 6303f26330 | |||
| b59bac670a | |||
| 84acc820c7 | |||
| 92858c7db2 | |||
| fdaa129a5b | |||
| df59967f59 | |||
| 73982c8cb6 | |||
| 6199aa0707 | |||
| a5921b5743 | |||
| f0538dbb5c | |||
| ff91ecb177 | |||
| a6357502c1 | |||
| fa8ea41cd9 | |||
| 3ac6790ca2 | |||
| 268b96058f | |||
| 0d031c432b | |||
| 6e8f3224c5 | |||
| 9c53e9f4c3 | |||
| 2d5e988a6d | |||
| 3cfb23750a | |||
| 57f75b1d9b | |||
| 4560cbc20c | |||
| e213172431 | |||
| f5f007e183 | |||
| 09463265ce | |||
| 8f4cf2a7d0 | |||
| 30183c4a9a | |||
| d1df5e0ae1 | |||
| f1bc9343a1 | |||
| e53a9f6cab | |||
| 97f4989945 | |||
| ed676667d0 | |||
| 1e5e213d24 | |||
| a8105ec068 | |||
| c6d2967b84 | |||
| bb9be02228 | |||
| 64f37bbf5b | |||
| 0fc96fa25c | |||
| 2d8c6f8288 | |||
| b94211bd66 | |||
| 6494b77d41 | |||
| 63a7ca1af0 | |||
| 95f544657a | |||
| 740a919e25 | |||
| 402c92beda | |||
| 6a9013b97c | |||
| 73b50aea2d | |||
| 315e66858c | |||
| 4f902d42b2 | |||
| 61a18ed48f | |||
| b5a6dd1395 | |||
| 83e6e1060e | |||
| ddeb8242d8 | |||
| a08052f640 | |||
| 5ecba587f7 | |||
| db3da97a50 | |||
| 5885ffc8ae | |||
| 9fb1c89f81 | |||
| 1e6afac3bd | |||
| 64083b9227 | |||
| 5fdd768903 | |||
| f72163f856 | |||
| 428629f577 | |||
| 1edd74dda2 | |||
| 740ecf530c | |||
| 10d2ef5449 | |||
| 8ea571c7f7 | |||
| ca3022d545 | |||
| 9417614a8e | |||
| 70d52bbc4c | |||
| 6aa2fc95c0 | |||
| cfbab22237 | |||
| be0e06c0aa | |||
| 1c941fd787 | |||
| 669783f875 | |||
| 27e474b3d2 | |||
| 1e53316241 | |||
| ca5ca8332b | |||
| aa579bbc20 | |||
| e375d822da | |||
| 4bc2eb9a4d | |||
| fc783f8407 | |||
| 79e0c52fc2 | |||
| ef22330563 | |||
| e1adbf83d8 | |||
| 1148f95292 | |||
| f7430ec09c | |||
| b8903d842c | |||
| 32f097b4d6 | |||
| 559ad38b8c | |||
| 90417d2dd6 | |||
| e66fb4bd7b | |||
| 018c90dae7 | |||
| a5066e8f76 | |||
| 0a3448c8b6 | |||
| c4964232eb | |||
| ef3c807b4a | |||
| 113b93346b | |||
| a955676986 | |||
| 725df557b5 | |||
| 250ac644d6 | |||
| 490056eee3 | |||
| 797d749322 | |||
| 79888bfff7 | |||
| 6ca57e81f1 | |||
| 010f61e91f | |||
| 9216981262 | |||
| b89a1cd482 | |||
| 7d37dea449 | |||
| df2ef64d20 | |||
| 7a549096de | |||
| a1ff351f21 | |||
| 03430d0db8 | |||
| 6f73d597e0 | |||
| e273a40345 | |||
| a84548d7ea | |||
| 19e01ceb19 | |||
| 1e6d67b16d | |||
| 6533927237 | |||
| da9ae574df | |||
| 16e4a7c200 | |||
| ee7e70c992 | |||
| 54a9f59a13 | |||
| e6fd664331 | |||
| b247ec9352 | |||
| 43befab8ef | |||
| eae0281c64 | |||
| 8bd78fbdfb | |||
| 66459a4ce0 | |||
| 182109f0de | |||
| 9998e9ea19 | |||
| ba2ba328da | |||
| 67b8080144 | |||
| 239a34ca97 | |||
| 81d8263ae2 | |||
| b4e4846e9f | |||
| 92e49eab5a | |||
| c4a7b950a0 | |||
| 1ce339abeb | |||
| a1f922315b | |||
| 3e284162d7 | |||
| ff99392102 | |||
| 9358ee011b | |||
| 37cbcae6ba | |||
| ae64b7fac8 | |||
| d936ed90ae | |||
| 7cc77eba45 | |||
| 07d232c7b4 | |||
| de67bca211 | |||
| 5c4dbc966f | |||
| bca1385683 | |||
| f655592ff5 | |||
| cbe87cb2ed | |||
| 0513b3ed07 | |||
| 7f92165c79 | |||
| 780ccc26f7 | |||
| 339425fd52 | |||
| fa6e9540a8 | |||
| 9605fde04d | |||
| 200d327737 | |||
| e1b0985b5b | |||
| c8dfc4cda4 | |||
| 7922a54c9a | |||
| bda6bcd5be | |||
| 7e3e24b394 | |||
| bb0466f4ce | |||
| 8797952409 | |||
| c33d1b8ee6 | |||
| 1f67c18222 | |||
| a4691611a7 | |||
| 7c7eb1475d | |||
| 0bce3d6d63 | |||
| 804d91ef61 | |||
| 229600ce9b | |||
| 43098df9d8 | |||
| 6908a0dcd4 | |||
| cae5761f16 | |||
| 83074ed57e | |||
| 64a7e6992e | |||
| 61af764f8a | |||
| b4172ad3c8 | |||
| 4917038f55 | |||
| b0f3f94163 | |||
| b7c169d71d | |||
| 3939c75345 | |||
| e55926e8cf | |||
| f82f535509 | |||
| 51e337228e | |||
| 241c56e6d7 | |||
| 895471afa1 | |||
| 85e2d886bc | |||
| 07aa02f196 | |||
| 8acf4d112a | |||
| e6abfb3b67 | |||
| 040af08473 | |||
| e3de237eb8 | |||
| 11785529fc | |||
| 32201a18ab | |||
| 421cccb1d7 | |||
| 400e9309f1 | |||
| a4afb312d4 | |||
| afe874f15a | |||
| b291dbe9c5 | |||
| bc8f34bfe7 | |||
| 2dc7ecc59b | |||
| 600551feb9 | |||
| 7398d737b5 | |||
| bf62ba57cf | |||
| 511a8cbe04 | |||
| 790ad68d4d | |||
| e79d2381fc | |||
| a3b4199e9b | |||
| 3aa0574c6b | |||
| 3118195e92 | |||
| 5716f1c199 | |||
| 2020afd200 | |||
| 7431acb2c4 | |||
| 2f9975c76c | |||
| d9bd73f4e8 | |||
| 8484d1c0ca | |||
| 7270ca4157 | |||
| b67c8970c9 | |||
| 418921de89 | |||
| ec4260d260 | |||
| c39d3db7a0 | |||
| 2da0cfc904 | |||
| 9dd0e3dc44 | |||
| 4722c94653 | |||
| 45c928e2f5 | |||
| 07506358ff | |||
| 975eb31973 | |||
| a3c509fd23 | |||
| 63bb78cfc6 | |||
| f2fd8b0265 | |||
| a8052889fe | |||
| bccf549463 | |||
| 73d1a46f3e | |||
| 95c16f51cb | |||
| 810dcbf34b | |||
| cb77586508 | |||
| e2498edb45 | |||
| 0b7dfab17a | |||
| 674fdc4304 | |||
| 10feb1af3f | |||
| d2b924cca8 | |||
| 13390d0c95 |
@@ -1,13 +1,33 @@
|
|||||||
## Expected behaviour
|
<!--- Provide a general summary of the issue in the Title above -->
|
||||||
|
|
||||||
## Actual behaviour
|
## Expected Behavior
|
||||||
|
<!--- If you're describing a bug, tell us what should happen -->
|
||||||
|
<!--- If you're suggesting a change/improvement, tell us how it should work -->
|
||||||
|
|
||||||
## Steps to reproduce the behaviour
|
## Current Behavior
|
||||||
|
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
|
||||||
|
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
|
||||||
|
|
||||||
## Minio version
|
## Possible Solution
|
||||||
- (paste output of `minio version`)
|
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||||
|
<!--- or ideas how to implement the addition or change -->
|
||||||
## System information
|
|
||||||
|
|
||||||
|
## Steps to Reproduce (for bugs)
|
||||||
|
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||||
|
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
4.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||||
|
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||||
|
|
||||||
|
## Your Environment
|
||||||
|
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||||
|
* Version used (`minio version`):
|
||||||
|
* Environment name and version (e.g. nginx 1.9.1):
|
||||||
|
* Server type and version:
|
||||||
|
* Operating System and version (`uname -a`):
|
||||||
|
* Link to your project:
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<!--- Provide a general summary of your changes in the Title above -->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
<!--- Describe your changes in detail -->
|
||||||
|
|
||||||
|
## Motivation and Context
|
||||||
|
<!--- Why is this change required? What problem does it solve? -->
|
||||||
|
<!--- If it fixes an open issue, please link to the issue here. -->
|
||||||
|
|
||||||
|
## How Has This Been Tested?
|
||||||
|
<!--- Please describe in detail how you tested your changes. -->
|
||||||
|
<!--- Include details of your testing environment, and the tests you ran to -->
|
||||||
|
<!--- see how your change affects other areas of the code, etc. -->
|
||||||
|
|
||||||
|
## Types of changes
|
||||||
|
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
||||||
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||||
|
|
||||||
|
## Checklist:
|
||||||
|
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
||||||
|
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||||
|
- [ ] My code follows the code style of this project.
|
||||||
|
- [ ] My change requires a change to the documentation.
|
||||||
|
- [ ] I have updated the documentation accordingly.
|
||||||
|
- [ ] I have added unit tests to cover my changes.
|
||||||
|
- [ ] I have added/updated functional tests in [mint](https://github.com/minio/mint). (If yes, add `mint` PR # here: )
|
||||||
|
- [ ] All new and existing tests passed.
|
||||||
@@ -16,3 +16,10 @@ release
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.syso
|
*.syso
|
||||||
coverage.txt
|
coverage.txt
|
||||||
|
.vscode/
|
||||||
|
.snap
|
||||||
|
*.tar.bz2
|
||||||
|
parts/
|
||||||
|
prime/
|
||||||
|
snap/.snapcraft/
|
||||||
|
stage/
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
|
go_import_path: github.com/minio/minio
|
||||||
sudo: required
|
sudo: required
|
||||||
|
|
||||||
|
services:
|
||||||
|
- docker
|
||||||
|
|
||||||
dist: trusty
|
dist: trusty
|
||||||
|
|
||||||
language: go
|
language: go
|
||||||
|
|
||||||
os:
|
os:
|
||||||
- linux
|
- linux
|
||||||
- osx
|
|
||||||
|
|
||||||
osx_image: xcode7.2
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- ARCH=x86_64
|
- ARCH=x86_64
|
||||||
- ARCH=i686
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- make test GOFLAGS="-race"
|
## Run all the tests
|
||||||
|
- make
|
||||||
|
- diff -au <(gofmt -d cmd) <(printf "")
|
||||||
|
- diff -au <(gofmt -d pkg) <(printf "")
|
||||||
|
- make test GOFLAGS="-timeout 15m -race -v"
|
||||||
- make coverage
|
- make coverage
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
- bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.6.2
|
- 1.9.1
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
### Install Golang
|
# Minio Contribution Guide [](https://slack.minio.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/) [](https://codecov.io/gh/minio/minio)
|
||||||
|
|
||||||
If you do not have a working Golang environment setup please follow [Golang Installation Guide](./INSTALLGO.md).
|
``Minio`` community welcomes your contribution. To make the process as seamless as possible, we recommend you read this contribution guide.
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
Start by forking the Minio GitHub repository, make changes in a branch and then send a pull request. We encourage pull requests to discuss code changes. Here are the steps in details:
|
||||||
|
|
||||||
### Setup your Minio Github Repository
|
### Setup your Minio Github Repository
|
||||||
Fork [Minio upstream](https://github.com/minio/minio/fork) source repository to your own personal repository. Copy the URL for minio from your personal github repo (you will need it for the `git clone` command below).
|
Fork [Minio upstream](https://github.com/minio/minio/fork) source repository to your own personal repository. Copy the URL of your Minio fork (you will need it for the `git clone` command below).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ mkdir -p $GOPATH/src/github.com/minio
|
$ mkdir -p $GOPATH/src/github.com/minio
|
||||||
$ cd $GOPATH/src/github.com/minio
|
$ cd $GOPATH/src/github.com/minio
|
||||||
@@ -11,61 +16,56 @@ $ git clone <paste saved URL for personal forked minio repo>
|
|||||||
$ cd minio
|
$ cd minio
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compiling Minio from source
|
### Set up git remote as ``upstream``
|
||||||
Minio uses ``Makefile`` to wrap around some of redundant checks done through command line.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ make
|
|
||||||
Checking if proper environment variables are set.. Done
|
|
||||||
...
|
|
||||||
Checking dependencies for Minio.. Done
|
|
||||||
Installed govet
|
|
||||||
Building Libraries
|
|
||||||
...
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Setting up git remote as ``upstream``
|
|
||||||
```sh
|
```sh
|
||||||
$ cd $GOPATH/src/github.com/minio/minio
|
$ cd $GOPATH/src/github.com/minio/minio
|
||||||
$ git remote add upstream https://github.com/minio/minio
|
$ git remote add upstream https://github.com/minio/minio
|
||||||
$ git fetch upstream
|
$ git fetch upstream
|
||||||
$ git merge upstream/master
|
$ git merge upstream/master
|
||||||
...
|
...
|
||||||
...
|
|
||||||
$ make
|
|
||||||
Checking if proper environment variables are set.. Done
|
|
||||||
...
|
|
||||||
Checking dependencies for Minio.. Done
|
|
||||||
Installed govet
|
|
||||||
Building Libraries
|
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Developer Guidelines
|
### Create your feature branch
|
||||||
``Minio`` community welcomes your contribution. To make the process as seamless as possible, we ask for the following:
|
Before making code changes, make sure you create a separate branch for these changes
|
||||||
* Go ahead and fork the project and make your changes. We encourage pull requests to discuss code changes.
|
|
||||||
- Fork it
|
|
||||||
- Create your feature branch (git checkout -b my-new-feature)
|
|
||||||
- Commit your changes (git commit -am 'Add some feature')
|
|
||||||
- Push to the branch (git push origin my-new-feature)
|
|
||||||
- Create new Pull Request
|
|
||||||
|
|
||||||
* If you have additional dependencies for ``Minio``, ``Minio`` manages its dependencies using [govendor](https://github.com/kardianos/govendor)
|
```
|
||||||
- Run `go get foo/bar`
|
$ git checkout -b my-new-feature
|
||||||
- Edit your code to import foo/bar
|
```
|
||||||
- Run `make pkg-add PKG=foo/bar` from top-level directory
|
|
||||||
|
|
||||||
* If you have dependencies for ``Minio`` which needs to be removed
|
### Test Minio server changes
|
||||||
- Edit your code to not import foo/bar
|
After your code changes, make sure
|
||||||
- Run `make pkg-remove PKG=foo/bar` from top-level directory
|
|
||||||
|
|
||||||
* When you're ready to create a pull request, be sure to:
|
- To add test cases for the new code. If you have questions about how to do it, please ask on our [Slack](slack.minio.io) channel.
|
||||||
- Have test cases for the new code. If you have questions about how to do it, please ask in your pull request.
|
- To run `make verifiers`
|
||||||
- Run `make verifiers`
|
- To squash your commits into a single commit. `git rebase -i`. It's okay to force update your pull request.
|
||||||
- Squash your commits into a single commit. `git rebase -i`. It's okay to force update your pull request.
|
- To run `go test -race ./...` and `go build` completes.
|
||||||
- Make sure `go test -race ./...` and `go build` completes.
|
|
||||||
|
|
||||||
* Read [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project
|
### Commit changes
|
||||||
- `Minio` project is fully conformant with Golang style
|
After verification, commit your changes. This is a [great post](https://chris.beams.io/posts/git-commit/) on how to write useful commit messages
|
||||||
- if you happen to observe offending code, please feel free to send a pull request
|
|
||||||
|
```
|
||||||
|
$ git commit -am 'Add some feature'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Push to the branch
|
||||||
|
Push your locally committed changes to the remote origin (your fork)
|
||||||
|
```
|
||||||
|
$ git push origin my-new-feature
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a Pull Request
|
||||||
|
Pull requests can be created via GitHub. Refer to [this document](https://help.github.com/articles/creating-a-pull-request/) for detailed steps on how to create a pull request. After a Pull Request gets peer reviewed and approved, it will be merged.
|
||||||
|
|
||||||
|
## FAQs
|
||||||
|
### How does ``Minio`` manages dependencies?
|
||||||
|
``Minio`` manages its dependencies using [govendor](https://github.com/kardianos/govendor). To add a dependency
|
||||||
|
- Run `go get foo/bar`
|
||||||
|
- Edit your code to import foo/bar
|
||||||
|
- Run `make pkg-add PKG=foo/bar` from top-level directory
|
||||||
|
|
||||||
|
To remove a dependency
|
||||||
|
- Edit your code to not import foo/bar
|
||||||
|
- Run `make pkg-remove PKG=foo/bar` from top-level directory
|
||||||
|
|
||||||
|
### What are the coding guidelines for Minio?
|
||||||
|
``Minio`` is fully conformant with Golang style. Refer: [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project. If you observe offending code, please feel free to send a pull request or ping us on [Slack](slack.minio.io).
|
||||||
|
|||||||
@@ -1,15 +1,31 @@
|
|||||||
FROM golang:1.6
|
FROM golang:1.9.1-alpine3.6
|
||||||
|
|
||||||
RUN mkdir -p /go/src/app
|
MAINTAINER Minio Inc <dev@minio.io>
|
||||||
WORKDIR /go/src/app
|
|
||||||
|
|
||||||
COPY . /go/src/app
|
ENV GOPATH /go
|
||||||
RUN go-wrapper download
|
ENV PATH $PATH:$GOPATH/bin
|
||||||
RUN go-wrapper install
|
ENV CGO_ENABLED 0
|
||||||
|
|
||||||
ENV ALLOW_CONTAINER_ROOT=1
|
WORKDIR /go/src/github.com/minio/
|
||||||
RUN mkdir -p /export/docker && cp /go/src/app/docs/Docker.md /export/docker/
|
|
||||||
|
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
apk add --no-cache ca-certificates curl && \
|
||||||
|
apk add --no-cache --virtual .build-deps git && \
|
||||||
|
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||||
|
go get -v -d github.com/minio/minio && \
|
||||||
|
cd /go/src/github.com/minio/minio && \
|
||||||
|
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
|
||||||
|
rm -rf /go/pkg /go/src /usr/local/go && apk del .build-deps
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
ENTRYPOINT ["go-wrapper", "run", "server"]
|
|
||||||
CMD ["/export"]
|
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
VOLUME ["/export"]
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s \
|
||||||
|
CMD /usr/bin/healthcheck.sh
|
||||||
|
|
||||||
|
CMD ["minio"]
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
FROM golang:1.9.1-alpine3.6
|
||||||
|
|
||||||
|
MAINTAINER Minio Inc <dev@minio.io>
|
||||||
|
|
||||||
|
ENV GOPATH /go
|
||||||
|
ENV PATH $PATH:$GOPATH/bin
|
||||||
|
ENV CGO_ENABLED 0
|
||||||
|
|
||||||
|
WORKDIR /go/src/github.com/minio/
|
||||||
|
|
||||||
|
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||||
|
|
||||||
|
COPY . /go/src/github.com/minio/minio
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
apk add --no-cache ca-certificates curl && \
|
||||||
|
apk add --no-cache --virtual .build-deps git && \
|
||||||
|
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||||
|
cd /go/src/github.com/minio/minio && \
|
||||||
|
go install -v -ldflags "$(go run buildscripts/gen-ldflags.go)" && \
|
||||||
|
rm -rf /go/pkg /go/src /usr/local/go && apk del .build-deps
|
||||||
|
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
VOLUME ["/export"]
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s \
|
||||||
|
CMD /usr/bin/healthcheck.sh
|
||||||
|
|
||||||
|
CMD ["minio"]
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
FROM alpine:3.6
|
||||||
|
|
||||||
|
MAINTAINER Minio Inc <dev@minio.io>
|
||||||
|
|
||||||
|
COPY dockerscripts/docker-entrypoint.sh dockerscripts/healthcheck.sh /usr/bin/
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
apk add --no-cache ca-certificates && \
|
||||||
|
apk add --no-cache --virtual .build-deps curl && \
|
||||||
|
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||||
|
curl https://dl.minio.io/server/minio/release/linux-amd64/minio > /usr/bin/minio && \
|
||||||
|
chmod +x /usr/bin/minio && \
|
||||||
|
chmod +x /usr/bin/docker-entrypoint.sh && \
|
||||||
|
chmod +x /usr/bin/healthcheck.sh
|
||||||
|
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
VOLUME ["/export"]
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s \
|
||||||
|
CMD /usr/bin/healthcheck.sh
|
||||||
|
|
||||||
|
CMD ["minio"]
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ $
|
|||||||
|
|
||||||
- Run `go get foo/bar`
|
- Run `go get foo/bar`
|
||||||
- Edit your code to import foo/bar
|
- Edit your code to import foo/bar
|
||||||
- Run `govendor add foo/bar` from top-level folder
|
- Run `govendor add foo/bar` from top-level directory
|
||||||
|
|
||||||
#### Remove dependencies
|
#### Remove dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -1,146 +1,101 @@
|
|||||||
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
|
||||||
PWD := $(shell pwd)
|
PWD := $(shell pwd)
|
||||||
GOPATH := $(shell go env GOPATH)
|
GOPATH := $(shell go env GOPATH)
|
||||||
|
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
||||||
|
|
||||||
BUILD_LDFLAGS := '$(LDFLAGS)'
|
BUILD_LDFLAGS := '$(LDFLAGS)'
|
||||||
TAG := latest
|
|
||||||
|
|
||||||
HOST ?= $(shell uname)
|
all: build
|
||||||
CPU ?= $(shell uname -m)
|
|
||||||
|
|
||||||
# if no host is identifed (no uname tool)
|
|
||||||
# we assume a Linux-64bit build
|
|
||||||
ifeq ($(HOST),)
|
|
||||||
HOST = Linux
|
|
||||||
endif
|
|
||||||
|
|
||||||
# identify CPU
|
|
||||||
ifeq ($(CPU), x86_64)
|
|
||||||
HOST := $(HOST)64
|
|
||||||
else
|
|
||||||
ifeq ($(CPU), amd64)
|
|
||||||
HOST := $(HOST)64
|
|
||||||
else
|
|
||||||
ifeq ($(CPU), i686)
|
|
||||||
HOST := $(HOST)32
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
|
|
||||||
|
|
||||||
#############################################
|
|
||||||
# now we find out the target OS for
|
|
||||||
# which we are going to compile in case
|
|
||||||
# the caller didn't yet define OS himself
|
|
||||||
ifndef (OS)
|
|
||||||
ifeq ($(HOST), Linux64)
|
|
||||||
arch = gcc
|
|
||||||
else
|
|
||||||
ifeq ($(HOST), Linux32)
|
|
||||||
arch = 32
|
|
||||||
else
|
|
||||||
ifeq ($(HOST), Darwin64)
|
|
||||||
arch = clang
|
|
||||||
else
|
|
||||||
ifeq ($(HOST), Darwin32)
|
|
||||||
arch = clang
|
|
||||||
else
|
|
||||||
ifeq ($(HOST), FreeBSD64)
|
|
||||||
arch = gcc
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
|
|
||||||
all: install
|
|
||||||
|
|
||||||
checks:
|
checks:
|
||||||
@echo "Checking deps:"
|
@echo "Check deps"
|
||||||
@(env bash $(PWD)/buildscripts/checkdeps.sh)
|
@(env bash $(PWD)/buildscripts/checkdeps.sh)
|
||||||
|
@echo "Checking project is in GOPATH"
|
||||||
@(env bash $(PWD)/buildscripts/checkgopath.sh)
|
@(env bash $(PWD)/buildscripts/checkgopath.sh)
|
||||||
|
|
||||||
getdeps: checks
|
getdeps: checks
|
||||||
@go get -u github.com/golang/lint/golint && echo "Installed golint:"
|
@echo "Installing golint" && go get -u github.com/golang/lint/golint
|
||||||
@go get -u github.com/fzipp/gocyclo && echo "Installed gocyclo:"
|
@echo "Installing gocyclo" && go get -u github.com/fzipp/gocyclo
|
||||||
@go get -u github.com/remyoudompheng/go-misc/deadcode && echo "Installed deadcode:"
|
@echo "Installing deadcode" && go get -u github.com/remyoudompheng/go-misc/deadcode
|
||||||
@go get -u github.com/client9/misspell/cmd/misspell && echo "Installed misspell:"
|
@echo "Installing misspell" && go get -u github.com/client9/misspell/cmd/misspell
|
||||||
@go get -u github.com/gordonklaus/ineffassign && echo "Installed ineffassign:"
|
@echo "Installing ineffassign" && go get -u github.com/gordonklaus/ineffassign
|
||||||
|
|
||||||
verifiers: vet fmt lint cyclo spelling
|
verifiers: getdeps vet fmt lint cyclo deadcode spelling
|
||||||
|
|
||||||
vet:
|
vet:
|
||||||
@echo "Running $@:"
|
@echo "Running $@"
|
||||||
@GO15VENDOREXPERIMENT=1 go tool vet -all *.go
|
@go tool vet -atomic -bool -copylocks -nilfunc -printf -shadow -rangeloops -unreachable -unsafeptr -unusedresult cmd
|
||||||
@GO15VENDOREXPERIMENT=1 go tool vet -all ./pkg
|
@go tool vet -atomic -bool -copylocks -nilfunc -printf -shadow -rangeloops -unreachable -unsafeptr -unusedresult pkg
|
||||||
@GO15VENDOREXPERIMENT=1 go tool vet -shadow=true *.go
|
|
||||||
@GO15VENDOREXPERIMENT=1 go tool vet -shadow=true ./pkg
|
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
@echo "Running $@:"
|
@echo "Running $@"
|
||||||
@GO15VENDOREXPERIMENT=1 gofmt -s -l *.go
|
@gofmt -d cmd
|
||||||
@GO15VENDOREXPERIMENT=1 gofmt -s -l pkg
|
@gofmt -d pkg
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@echo "Running $@:"
|
@echo "Running $@"
|
||||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/golint *.go
|
@${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/cmd...
|
||||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/golint github.com/minio/minio/pkg...
|
@${GOPATH}/bin/golint -set_exit_status github.com/minio/minio/pkg...
|
||||||
|
|
||||||
ineffassign:
|
ineffassign:
|
||||||
@echo "Running $@:"
|
@echo "Running $@"
|
||||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/ineffassign .
|
@${GOPATH}/bin/ineffassign .
|
||||||
|
|
||||||
cyclo:
|
cyclo:
|
||||||
@echo "Running $@:"
|
@echo "Running $@"
|
||||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/gocyclo -over 65 *.go
|
@${GOPATH}/bin/gocyclo -over 100 cmd
|
||||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/gocyclo -over 65 pkg
|
@${GOPATH}/bin/gocyclo -over 100 pkg
|
||||||
|
|
||||||
build: getdeps verifiers $(UI_ASSETS)
|
|
||||||
|
|
||||||
deadcode:
|
deadcode:
|
||||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/deadcode
|
@echo "Running $@"
|
||||||
|
@${GOPATH}/bin/deadcode -test $(shell go list ./...) || true
|
||||||
|
|
||||||
spelling:
|
spelling:
|
||||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell -error *.go
|
@${GOPATH}/bin/misspell -error `find cmd/`
|
||||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/misspell -error pkg/**/*
|
@${GOPATH}/bin/misspell -error `find pkg/`
|
||||||
|
@${GOPATH}/bin/misspell -error `find docs/`
|
||||||
|
|
||||||
test: build
|
# Builds minio, runs the verifiers then runs the tests.
|
||||||
@echo "Running all minio testing:"
|
check: test
|
||||||
@GO15VENDOREXPERIMENT=1 go test $(GOFLAGS) .
|
test: verifiers build
|
||||||
@GO15VENDOREXPERIMENT=1 go test $(GOFLAGS) github.com/minio/minio/pkg...
|
@echo "Running unit tests"
|
||||||
|
@go test $(GOFLAGS) .
|
||||||
|
@go test $(GOFLAGS) github.com/minio/minio/cmd...
|
||||||
|
@go test $(GOFLAGS) github.com/minio/minio/pkg...
|
||||||
|
@echo "Verifying build"
|
||||||
|
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
||||||
|
|
||||||
coverage: build
|
coverage: build
|
||||||
@echo "Running all coverage for minio:"
|
@echo "Running all coverage for minio"
|
||||||
@GO15VENDOREXPERIMENT=1 ./buildscripts/go-coverage.sh
|
@./buildscripts/go-coverage.sh
|
||||||
|
|
||||||
gomake-all: build
|
# Builds minio locally.
|
||||||
@echo "Installing minio:"
|
build:
|
||||||
@GO15VENDOREXPERIMENT=1 go build --ldflags $(BUILD_LDFLAGS) -o $(GOPATH)/bin/minio
|
@echo "Building minio binary: $(PWD)/minio"
|
||||||
|
@CGO_ENABLED=0 go build --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio
|
||||||
|
|
||||||
pkg-add:
|
pkg-add:
|
||||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/govendor add $(PKG)
|
@echo "Adding new package $(PKG)"
|
||||||
|
@${GOPATH}/bin/govendor add $(PKG)
|
||||||
|
|
||||||
pkg-update:
|
pkg-update:
|
||||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/govendor update $(PKG)
|
@echo "Updating new package $(PKG)"
|
||||||
|
@${GOPATH}/bin/govendor update $(PKG)
|
||||||
|
|
||||||
pkg-remove:
|
pkg-remove:
|
||||||
@GO15VENDOREXPERIMENT=1 ${GOPATH}/bin/govendor remove $(PKG)
|
@echo "Remove new package $(PKG)"
|
||||||
|
@${GOPATH}/bin/govendor remove $(PKG)
|
||||||
|
|
||||||
pkg-list:
|
pkg-list:
|
||||||
@GO15VENDOREXPERIMENT=1 $(GOPATH)/bin/govendor list
|
@$(GOPATH)/bin/govendor list
|
||||||
|
|
||||||
install: gomake-all
|
# Builds minio and installs it to $GOPATH/bin.
|
||||||
|
install: build
|
||||||
release: verifiers
|
@echo "Installing minio binary: $(GOPATH)/bin/minio"
|
||||||
@MINIO_RELEASE=RELEASE ./buildscripts/build.sh
|
@cp $(PWD)/minio $(GOPATH)/bin/minio
|
||||||
|
@echo "\nInstallation successful. To learn more, try \"minio --help\"."
|
||||||
experimental: verifiers
|
|
||||||
@MINIO_RELEASE=EXPERIMENTAL ./buildscripts/build.sh
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning up all the generated files:"
|
@echo "Cleaning up all the generated files"
|
||||||
@rm -fv minio minio.test cover.out
|
|
||||||
@find . -name '*.test' | xargs rm -fv
|
@find . -name '*.test' | xargs rm -fv
|
||||||
@rm -rf isa-l
|
|
||||||
@rm -rf build
|
@rm -rf build
|
||||||
@rm -rf release
|
@rm -rf release
|
||||||
|
|||||||
@@ -1,191 +1,115 @@
|
|||||||
# Minio Quickstart Guide [](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://codecov.io/gh/minio/minio)
|
# Minio Quickstart Guide
|
||||||
|
[](https://slack.minio.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/) [](https://codecov.io/gh/minio/minio) [](https://build.snapcraft.io/user/minio/minio)
|
||||||
|
|
||||||
Minio is an object storage server released under Apache License v2.0. It is compatible with Amazon S3 cloud storage service. It is best suited for storing unstructured data such as photos, videos, log files, backups and container / VM images. Size of an object can range from a few KBs to a maximum of 5TB.
|
Minio is an object storage server released under Apache License v2.0. It is compatible with Amazon S3 cloud storage service. It is best suited for storing unstructured data such as photos, videos, log files, backups and container / VM images. Size of an object can range from a few KBs to a maximum of 5TB.
|
||||||
|
|
||||||
## 1. Download
|
|
||||||
|
|
||||||
Minio server is light enough to be bundled with the application stack, similar to NodeJS, Redis and MySQL.
|
Minio server is light enough to be bundled with the application stack, similar to NodeJS, Redis and MySQL.
|
||||||
|
|
||||||
|
## Docker Container
|
||||||
|
### Stable
|
||||||
|
```
|
||||||
|
docker pull minio/minio
|
||||||
|
docker run -p 9000:9000 minio/minio server /data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edge
|
||||||
|
```
|
||||||
|
docker pull minio/minio:edge
|
||||||
|
docker run -p 9000:9000 minio/minio:edge server /data
|
||||||
|
```
|
||||||
|
Please visit Minio Docker quickstart guide for more [here](https://docs.minio.io/docs/minio-docker-quickstart-guide)
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
### Homebrew
|
||||||
|
Install minio packages using [Homebrew](http://brew.sh/)
|
||||||
|
```sh
|
||||||
|
brew install minio/stable/minio
|
||||||
|
minio server /data
|
||||||
|
```
|
||||||
|
#### Note
|
||||||
|
If you previously installed minio using `brew install minio` then it is recommended that you reinstall minio from `minio/stable/minio` official repo instead.
|
||||||
|
```sh
|
||||||
|
brew uninstall minio
|
||||||
|
brew install minio/stable/minio
|
||||||
|
```
|
||||||
|
|
||||||
|
### Binary Download
|
||||||
| Platform| Architecture | URL|
|
| Platform| Architecture | URL|
|
||||||
| ----------| -------- | ------|
|
| ----------| -------- | ------|
|
||||||
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio|
|
|Apple macOS|64-bit Intel|https://dl.minio.io/server/minio/release/darwin-amd64/minio |
|
||||||
||32-bit Intel|https://dl.minio.io/server/minio/release/linux-386/minio|
|
```sh
|
||||||
||32-bit ARM|https://dl.minio.io/server/minio/release/linux-arm/minio|
|
chmod 755 minio
|
||||||
|Apple OS X|64-bit Intel|https://dl.minio.io/server/minio/release/darwin-amd64/minio|
|
./minio server /data
|
||||||
|Microsoft Windows|64-bit|https://dl.minio.io/server/minio/release/windows-amd64/minio.exe|
|
```
|
||||||
||32-bit|https://dl.minio.io/server/minio/release/windows-386/minio.exe|
|
|
||||||
|FreeBSD|64-bit|https://dl.minio.io/server/minio/release/freebsd-amd64/minio|
|
|
||||||
|
|
||||||
### Install from Source
|
## GNU/Linux
|
||||||
|
### Binary Download
|
||||||
|
| Platform| Architecture | URL|
|
||||||
|
| ----------| -------- | ------|
|
||||||
|
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio |
|
||||||
|
```sh
|
||||||
|
chmod +x minio
|
||||||
|
./minio server /data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Snap
|
||||||
|
You can install the latest `minio` [snap](https://snapcraft.io), and help testing the most recent changes of the master branch in [all the supported Linux distros](https://snapcraft.io/docs/core/install) with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo snap install minio --edge
|
||||||
|
```
|
||||||
|
|
||||||
|
Every time a new version of `minio` is pushed to the store, you will get it updated automatically.
|
||||||
|
|
||||||
|
You will need to allow the minio snap to observe mounts in the system:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo snap connect minio:mount-observe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Microsoft Windows
|
||||||
|
### Binary Download
|
||||||
|
| Platform| Architecture | URL|
|
||||||
|
| ----------| -------- | ------|
|
||||||
|
|Microsoft Windows|64-bit|https://dl.minio.io/server/minio/release/windows-amd64/minio.exe |
|
||||||
|
```sh
|
||||||
|
minio.exe server D:\Photos
|
||||||
|
```
|
||||||
|
|
||||||
|
## FreeBSD
|
||||||
|
### Port
|
||||||
|
Install minio packages using [pkg](https://github.com/freebsd/pkg)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pkg install minio
|
||||||
|
sysrc minio_enable=yes
|
||||||
|
sysrc minio_disks=/home/user/Photos
|
||||||
|
service minio start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install from Source
|
||||||
|
|
||||||
Source installation is only intended for developers and advanced users. If you do not have a working Golang environment, please follow [How to install Golang](https://docs.minio.io/docs/how-to-install-golang).
|
Source installation is only intended for developers and advanced users. If you do not have a working Golang environment, please follow [How to install Golang](https://docs.minio.io/docs/how-to-install-golang).
|
||||||
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
go get -u github.com/minio/minio
|
||||||
$ go get -d github.com/minio/minio
|
|
||||||
$ cd $GOPATH/src/github.com/minio/minio
|
|
||||||
$ make
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. Run Minio Server
|
## Test using Minio Browser
|
||||||
|
Minio Server comes with an embedded web based object browser. Point your web browser to http://127.0.0.1:9000 ensure your server has started successfully.
|
||||||
|
|
||||||
### GNU/Linux
|
|
||||||
|
|
||||||
```sh
|
|
||||||
|
|
||||||
$ chmod +x minio
|
|
||||||
$ ./minio --help
|
|
||||||
$ ./minio server ~/Photos
|
|
||||||
|
|
||||||
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
|
||||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
|
||||||
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
|
||||||
Region: us-east-1
|
|
||||||
|
|
||||||
Browser Access:
|
|
||||||
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
|
||||||
|
|
||||||
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
|
|
||||||
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
|
||||||
|
|
||||||
Object API (Amazon S3 compatible):
|
|
||||||
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
|
|
||||||
Java: https://docs.minio.io/docs/java-client-quickstart-guide
|
|
||||||
Python: https://docs.minio.io/docs/python-client-quickstart-guide
|
|
||||||
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### OS X
|
|
||||||
|
|
||||||
|
|
||||||
```sh
|
|
||||||
|
|
||||||
$ chmod 755 minio
|
|
||||||
$ ./minio --help
|
|
||||||
$ ./minio server ~/Photos
|
|
||||||
|
|
||||||
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
|
||||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
|
||||||
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
|
||||||
Region: us-east-1
|
|
||||||
|
|
||||||
Browser Access:
|
|
||||||
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
|
||||||
|
|
||||||
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
|
|
||||||
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
|
||||||
|
|
||||||
Object API (Amazon S3 compatible):
|
|
||||||
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
|
|
||||||
Java: https://docs.minio.io/docs/java-client-quickstart-guide
|
|
||||||
Python: https://docs.minio.io/docs/python-client-quickstart-guide
|
|
||||||
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Microsoft Windows
|
|
||||||
|
|
||||||
```sh
|
|
||||||
|
|
||||||
C:\Users\Username\Downloads> minio.exe --help
|
|
||||||
C:\Users\Username\Downloads> minio.exe server D:\Photos
|
|
||||||
|
|
||||||
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
|
||||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
|
||||||
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
|
||||||
Region: us-east-1
|
|
||||||
|
|
||||||
Browser Access:
|
|
||||||
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
|
||||||
|
|
||||||
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
|
|
||||||
$ mc.exe config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
|
||||||
|
|
||||||
Object API (Amazon S3 compatible):
|
|
||||||
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
|
|
||||||
Java: https://docs.minio.io/docs/java-client-quickstart-guide
|
|
||||||
Python: https://docs.minio.io/docs/python-client-quickstart-guide
|
|
||||||
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Container
|
|
||||||
|
|
||||||
```sh
|
|
||||||
|
|
||||||
$ docker pull minio/minio
|
|
||||||
$ docker run -p 9000:9000 minio/minio
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### FreeBSD
|
|
||||||
|
|
||||||
```sh
|
|
||||||
|
|
||||||
$ chmod 755 minio
|
|
||||||
$ ./minio --help
|
|
||||||
$ ./minio server ~/Photos
|
|
||||||
|
|
||||||
Endpoint: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
|
||||||
AccessKey: USWUXHGYZQYFYFFIT3RE
|
|
||||||
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
|
||||||
Region: us-east-1
|
|
||||||
|
|
||||||
Browser Access:
|
|
||||||
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
|
||||||
|
|
||||||
Command-line Access: https://docs.minio.io/docs/minio-client-quickstart-guide
|
|
||||||
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
|
||||||
|
|
||||||
Object API (Amazon S3 compatible):
|
|
||||||
Go: https://docs.minio.io/docs/golang-client-quickstart-guide
|
|
||||||
Java: https://docs.minio.io/docs/java-client-quickstart-guide
|
|
||||||
Python: https://docs.minio.io/docs/python-client-quickstart-guide
|
|
||||||
JavaScript: https://docs.minio.io/docs/javascript-client-quickstart-guide
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Test Minio Server using Minio Browser
|
|
||||||
|
|
||||||
Open a web browser and navigate to http://127.0.0.1:9000 to view your buckets on minio server.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Test using Minio Client `mc`
|
||||||
|
`mc` provides a modern alternative to UNIX commands like ls, cat, cp, mirror, diff etc. It supports filesystems and Amazon S3 compatible cloud storage services. Follow the Minio Client [Quickstart Guide](https://docs.minio.io/docs/minio-client-quickstart-guide) for further instructions.
|
||||||
|
|
||||||
## 4. Test Minio Server using `mc`
|
## Explore Further
|
||||||
|
|
||||||
|
|
||||||
Install mc from [here](https://docs.minio.io/docs/minio-client-quickstart-guide). Use `mc ls` command to list all the buckets on your minio server.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
|
|
||||||
$ mc ls myminio/
|
|
||||||
[2015-08-05 08:13:22 IST] 0B andoria/
|
|
||||||
[2015-08-05 06:14:26 IST] 0B deflector/
|
|
||||||
[2015-08-05 08:13:11 IST] 0B ferenginar/
|
|
||||||
[2016-03-08 14:56:35 IST] 0B jarjarbing/
|
|
||||||
[2016-01-20 16:07:41 IST] 0B my.minio.io/
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
For more examples please navigate to [Minio Client Complete Guide](https://docs.minio.io/docs/minio-client-complete-guide).
|
|
||||||
|
|
||||||
|
|
||||||
## 5. Explore Further
|
|
||||||
|
|
||||||
- [Minio Erasure Code QuickStart Guide](https://docs.minio.io/docs/minio-erasure-code-quickstart-guide)
|
- [Minio Erasure Code QuickStart Guide](https://docs.minio.io/docs/minio-erasure-code-quickstart-guide)
|
||||||
- [Minio Docker Quickstart Guide](https://docs.minio.io/docs/minio-docker-quickstart-guide)
|
|
||||||
- [Use `mc` with Minio Server](https://docs.minio.io/docs/minio-client-quickstart-guide)
|
- [Use `mc` with Minio Server](https://docs.minio.io/docs/minio-client-quickstart-guide)
|
||||||
- [Use `aws-cli` with Minio Server](https://docs.minio.io/docs/aws-cli-with-minio)
|
- [Use `aws-cli` with Minio Server](https://docs.minio.io/docs/aws-cli-with-minio)
|
||||||
- [Use `s3cmd` with Minio Server](https://docs.minio.io/docs/s3cmd-with-minio)
|
- [Use `s3cmd` with Minio Server](https://docs.minio.io/docs/s3cmd-with-minio)
|
||||||
- [Use `minio-go` SDK with Minio Server](https://docs.minio.io/docs/golang-client-quickstart-guide)
|
- [Use `minio-go` SDK with Minio Server](https://docs.minio.io/docs/golang-client-quickstart-guide)
|
||||||
|
- [The Minio documentation website](https://docs.minio.io)
|
||||||
|
|
||||||
|
## Contribute to Minio Project
|
||||||
## 6. Contribute to Minio Project
|
|
||||||
Please follow Minio [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
|
Please follow Minio [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# Minio 快速入门 [](https://slack.minio.io) [](https://goreportcard.com/report/minio/minio) [](https://codecov.io/gh/minio/minio) [](https://build.snapcraft.io/user/minio/minio)
|
||||||
|
|
||||||
|
Minio是一个对象存储服务,基于Apache License v2.0协议. 它完全兼容亚马逊的S3云储存服务,非常适合于存储很多非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
|
||||||
|
|
||||||
|
## 1. 下载
|
||||||
|
|
||||||
|
Minio是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS, Redis 或者 MySQL.
|
||||||
|
|
||||||
|
| Platform| Architecture | URL|
|
||||||
|
| ----------| -------- | ------|
|
||||||
|
|GNU/Linux|64-bit Intel|https://dl.minio.io/server/minio/release/linux-amd64/minio|
|
||||||
|
|Apple OS X|64-bit Intel|https://dl.minio.io/server/minio/release/darwin-amd64/minio|
|
||||||
|
|Microsoft Windows|64-bit|https://dl.minio.io/server/minio/release/windows-amd64/minio.exe|
|
||||||
|
|FreeBSD|64-bit|https://dl.minio.io/server/minio/release/freebsd-amd64/minio|
|
||||||
|
|
||||||
|
### Homebrew 安装
|
||||||
|
|
||||||
|
使用[Homebrew](http://brew.sh/) 来安装minio
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ brew install minio
|
||||||
|
$ minio --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### 源码安装
|
||||||
|
|
||||||
|
源码安装只针对开发者和一些高级用户,如果你还没有golang的环境,请安装golang官网安装[How to install Golang](https://docs.minio.io/docs/zh-CN/how-to-install-golang).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
|
||||||
|
$ go get -u github.com/minio/minio
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 运行Minio服务
|
||||||
|
|
||||||
|
|
||||||
|
### GNU/Linux
|
||||||
|
|
||||||
|
```sh
|
||||||
|
|
||||||
|
$ chmod +x minio
|
||||||
|
$ ./minio --help
|
||||||
|
$ ./minio server /data
|
||||||
|
|
||||||
|
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||||
|
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||||
|
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||||
|
区域: us-east-1
|
||||||
|
|
||||||
|
浏览器访问入口:
|
||||||
|
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||||
|
|
||||||
|
命令行访问: https://docs.minio.io/docs/zh-CN/minio-client-quickstart-guide
|
||||||
|
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||||
|
|
||||||
|
对象操作API (兼容Amazon S3):
|
||||||
|
Go: https://docs.minio.io/docs/zh-CN/golang-client-quickstart-guide
|
||||||
|
Java: https://docs.minio.io/docs/zh-CN/java-client-quickstart-guide
|
||||||
|
Python: https://docs.minio.io/docs/zh-CN/python-client-quickstart-guide
|
||||||
|
JavaScript: https://docs.minio.io/docs/zh-CN/javascript-client-quickstart-guide
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### OS X
|
||||||
|
|
||||||
|
|
||||||
|
```sh
|
||||||
|
|
||||||
|
$ chmod 755 minio
|
||||||
|
$ ./minio --help
|
||||||
|
$ ./minio server /data
|
||||||
|
|
||||||
|
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||||
|
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||||
|
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||||
|
区域: us-east-1
|
||||||
|
|
||||||
|
浏览器访问入口:
|
||||||
|
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||||
|
|
||||||
|
命令行访问: https://docs.minio.io/docs/zh-CN/minio-client-quickstart-guide
|
||||||
|
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||||
|
|
||||||
|
对象操作API (兼容Amazon S3):
|
||||||
|
Go: https://docs.minio.io/docs/zh-CN/golang-client-quickstart-guide
|
||||||
|
Java: https://docs.minio.io/docs/zh-CN/java-client-quickstart-guide
|
||||||
|
Python: https://docs.minio.io/docs/zh-CN/python-client-quickstart-guide
|
||||||
|
JavaScript: https://docs.minio.io/docs/zh-CN/javascript-client-quickstart-guide
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Microsoft Windows
|
||||||
|
|
||||||
|
```sh
|
||||||
|
|
||||||
|
C:\Users\Username\Downloads> minio.exe --help
|
||||||
|
C:\Users\Username\Downloads> minio.exe server D:\Photos
|
||||||
|
|
||||||
|
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||||
|
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||||
|
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||||
|
区域: us-east-1
|
||||||
|
|
||||||
|
浏览器访问入口:
|
||||||
|
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||||
|
|
||||||
|
命令行访问: https://docs.minio.io/docs/zh-CN/minio-client-quickstart-guide
|
||||||
|
$ mc.exe config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||||
|
|
||||||
|
对象操作API (兼容Amazon S3):
|
||||||
|
Go: https://docs.minio.io/docs/zh-CN/golang-client-quickstart-guide
|
||||||
|
Java: https://docs.minio.io/docs/zh-CN/java-client-quickstart-guide
|
||||||
|
Python: https://docs.minio.io/docs/zh-CN/python-client-quickstart-guide
|
||||||
|
JavaScript: https://docs.minio.io/docs/zh-CN/javascript-client-quickstart-guide
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```sh
|
||||||
|
|
||||||
|
$ docker pull minio/minio
|
||||||
|
$ docker run -p 9000:9000 minio/minio
|
||||||
|
|
||||||
|
```
|
||||||
|
访问minio的docker入门指南获得更多内容 [here](https://docs.minio.io/docs/zh-CN/minio-docker-quickstart-guide)
|
||||||
|
|
||||||
|
### FreeBSD
|
||||||
|
|
||||||
|
```sh
|
||||||
|
|
||||||
|
$ chmod 755 minio
|
||||||
|
$ ./minio --help
|
||||||
|
$ ./minio server /data
|
||||||
|
|
||||||
|
端点: http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||||
|
AccessKey: USWUXHGYZQYFYFFIT3RE
|
||||||
|
SecretKey: MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||||
|
区域: us-east-1
|
||||||
|
|
||||||
|
浏览器访问入口:
|
||||||
|
http://10.0.0.10:9000 http://127.0.0.1:9000 http://172.17.0.1:9000
|
||||||
|
|
||||||
|
命令行访问: https://docs.minio.io/docs/zh-CN/minio-client-quickstart-guide
|
||||||
|
$ mc config host add myminio http://10.0.0.10:9000 USWUXHGYZQYFYFFIT3RE MOJRH0mkL1IPauahWITSVvyDrQbEEIwljvmxdq03
|
||||||
|
|
||||||
|
对象操作API (兼容Amazon S3):
|
||||||
|
Go: https://docs.minio.io/docs/zh-CN/golang-client-quickstart-guide
|
||||||
|
Java: https://docs.minio.io/docs/zh-CN/java-client-quickstart-guide
|
||||||
|
Python: https://docs.minio.io/docs/zh-CN/python-client-quickstart-guide
|
||||||
|
JavaScript: https://docs.minio.io/docs/zh-CN/javascript-client-quickstart-guide
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
请访问FreeBSD的官网指南获取更多详细信息[here](https://www.freebsd.org/doc/handbook/zfs-quickstart.html)
|
||||||
|
|
||||||
|
## 3. 使用浏览器测试minio服务
|
||||||
|
|
||||||
|
打开浏览器并输入 http://127.0.0.1:9000 查看在minio服务器上面的所有bucket
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## 4. 使用`mc`测试minio服务
|
||||||
|
|
||||||
|
|
||||||
|
按照 [这个](https://docs.minio.io/docs/minio-client-quickstart-guide) 安装mc. 使用 `mc ls` 命令显示所有在minio服务上面的bucket.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
|
||||||
|
$ mc ls myminio/
|
||||||
|
[2015-08-05 08:13:22 IST] 0B andoria/
|
||||||
|
[2015-08-05 06:14:26 IST] 0B deflector/
|
||||||
|
[2015-08-05 08:13:11 IST] 0B ferenginar/
|
||||||
|
[2016-03-08 14:56:35 IST] 0B jarjarbing/
|
||||||
|
[2016-01-20 16:07:41 IST] 0B my.minio.io/
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
查看更多的例子请访问 [Minio Client Complete Guide](https://docs.minio.io/docs/zh-CN/minio-client-complete-guide).
|
||||||
|
|
||||||
|
|
||||||
|
## 5. 更多内容
|
||||||
|
|
||||||
|
- [Minio Erasure Code 快速入门](https://docs.minio.io/docs/zh-CN/minio-erasure-code-quickstart-guide)
|
||||||
|
- [Minio Docker 快速入门](https://docs.minio.io/docs/zh-CN/minio-docker-quickstart-guide)
|
||||||
|
- [使用`mc`测试 Minio Server](https://docs.minio.io/docs/zh-CN/minio-client-quickstart-guide)
|
||||||
|
- [使用 `aws-cli` 测试 Minio Server](https://docs.minio.io/docs/zh-CN/aws-cli-with-minio)
|
||||||
|
- [使用 `s3cmd` 测试 Minio Server](https://docs.minio.io/docs/zh-CN/s3cmd-with-minio)
|
||||||
|
- [使用 `minio-go` SDK ce's测试 Minio Server](https://docs.minio.io/docs/zh-CN/golang-client-quickstart-guide)
|
||||||
|
|
||||||
|
|
||||||
|
## 6. 给Minio项目贡献
|
||||||
|
请按照Minio [贡献者指导手册](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"regexp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// credential container for access and secret keys.
|
|
||||||
type credential struct {
|
|
||||||
AccessKeyID string `json:"accessKey"`
|
|
||||||
SecretAccessKey string `json:"secretKey"`
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
minioAccessID = 20
|
|
||||||
minioSecretID = 40
|
|
||||||
)
|
|
||||||
|
|
||||||
// isValidSecretKey - validate secret key.
|
|
||||||
var isValidSecretKey = regexp.MustCompile(`^.{8,40}$`)
|
|
||||||
|
|
||||||
// isValidAccessKey - validate access key.
|
|
||||||
var isValidAccessKey = regexp.MustCompile(`^[a-zA-Z0-9\\-\\.\\_\\~]{5,20}$`)
|
|
||||||
|
|
||||||
// mustGenAccessKeys - must generate access credentials.
|
|
||||||
func mustGenAccessKeys() (creds credential) {
|
|
||||||
creds, err := genAccessKeys()
|
|
||||||
fatalIf(err, "Unable to generate access keys.")
|
|
||||||
return creds
|
|
||||||
}
|
|
||||||
|
|
||||||
// genAccessKeys - generate access credentials.
|
|
||||||
func genAccessKeys() (credential, error) {
|
|
||||||
accessKeyID, err := genAccessKeyID()
|
|
||||||
if err != nil {
|
|
||||||
return credential{}, err
|
|
||||||
}
|
|
||||||
secretAccessKey, err := genSecretAccessKey()
|
|
||||||
if err != nil {
|
|
||||||
return credential{}, err
|
|
||||||
}
|
|
||||||
creds := credential{
|
|
||||||
AccessKeyID: string(accessKeyID),
|
|
||||||
SecretAccessKey: string(secretAccessKey),
|
|
||||||
}
|
|
||||||
return creds, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// genAccessKeyID - generate random alpha numeric value using only uppercase characters
|
|
||||||
// takes input as size in integer
|
|
||||||
func genAccessKeyID() ([]byte, error) {
|
|
||||||
alpha := make([]byte, minioAccessID)
|
|
||||||
if _, err := rand.Read(alpha); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for i := 0; i < minioAccessID; i++ {
|
|
||||||
alpha[i] = alphaNumericTable[alpha[i]%byte(len(alphaNumericTable))]
|
|
||||||
}
|
|
||||||
return alpha, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// genSecretAccessKey - generate random base64 numeric value from a random seed.
|
|
||||||
func genSecretAccessKey() ([]byte, error) {
|
|
||||||
rb := make([]byte, minioSecretID)
|
|
||||||
if _, err := rand.Read(rb); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return []byte(base64.StdEncoding.EncodeToString(rb))[:minioSecretID], nil
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import router "github.com/gorilla/mux"
|
|
||||||
|
|
||||||
// objectAPIHandler implements and provides http handlers for S3 API.
|
|
||||||
type objectAPIHandlers struct {
|
|
||||||
ObjectAPI ObjectLayer
|
|
||||||
}
|
|
||||||
|
|
||||||
// registerAPIRouter - registers S3 compatible APIs.
|
|
||||||
func registerAPIRouter(mux *router.Router, api objectAPIHandlers) {
|
|
||||||
// API Router
|
|
||||||
apiRouter := mux.NewRoute().PathPrefix("/").Subrouter()
|
|
||||||
|
|
||||||
// Bucket router
|
|
||||||
bucket := apiRouter.PathPrefix("/{bucket}").Subrouter()
|
|
||||||
|
|
||||||
/// Object operations
|
|
||||||
|
|
||||||
// HeadObject
|
|
||||||
bucket.Methods("HEAD").Path("/{object:.+}").HandlerFunc(api.HeadObjectHandler)
|
|
||||||
// PutObjectPart
|
|
||||||
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(api.PutObjectPartHandler).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
|
||||||
// ListObjectPxarts
|
|
||||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(api.ListObjectPartsHandler).Queries("uploadId", "{uploadId:.*}")
|
|
||||||
// CompleteMultipartUpload
|
|
||||||
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(api.CompleteMultipartUploadHandler).Queries("uploadId", "{uploadId:.*}")
|
|
||||||
// NewMultipartUpload
|
|
||||||
bucket.Methods("POST").Path("/{object:.+}").HandlerFunc(api.NewMultipartUploadHandler).Queries("uploads", "")
|
|
||||||
// AbortMultipartUpload
|
|
||||||
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(api.AbortMultipartUploadHandler).Queries("uploadId", "{uploadId:.*}")
|
|
||||||
// GetObject
|
|
||||||
bucket.Methods("GET").Path("/{object:.+}").HandlerFunc(api.GetObjectHandler)
|
|
||||||
// CopyObject
|
|
||||||
bucket.Methods("PUT").Path("/{object:.+}").HeadersRegexp("X-Amz-Copy-Source", ".*?(\\/|%2F).*?").HandlerFunc(api.CopyObjectHandler)
|
|
||||||
// PutObject
|
|
||||||
bucket.Methods("PUT").Path("/{object:.+}").HandlerFunc(api.PutObjectHandler)
|
|
||||||
// DeleteObject
|
|
||||||
bucket.Methods("DELETE").Path("/{object:.+}").HandlerFunc(api.DeleteObjectHandler)
|
|
||||||
|
|
||||||
/// Bucket operations
|
|
||||||
|
|
||||||
// GetBucketLocation
|
|
||||||
bucket.Methods("GET").HandlerFunc(api.GetBucketLocationHandler).Queries("location", "")
|
|
||||||
// GetBucketPolicy
|
|
||||||
bucket.Methods("GET").HandlerFunc(api.GetBucketPolicyHandler).Queries("policy", "")
|
|
||||||
// GetBucketNotification
|
|
||||||
bucket.Methods("GET").HandlerFunc(api.GetBucketNotificationHandler).Queries("notification", "")
|
|
||||||
// ListenBucketNotification
|
|
||||||
bucket.Methods("GET").HandlerFunc(api.ListenBucketNotificationHandler).Queries("notificationARN", "{notificationARN:.*}")
|
|
||||||
// ListMultipartUploads
|
|
||||||
bucket.Methods("GET").HandlerFunc(api.ListMultipartUploadsHandler).Queries("uploads", "")
|
|
||||||
// ListObjectsV2
|
|
||||||
bucket.Methods("GET").HandlerFunc(api.ListObjectsV2Handler).Queries("list-type", "2")
|
|
||||||
// ListObjectsV1 (Legacy)
|
|
||||||
bucket.Methods("GET").HandlerFunc(api.ListObjectsV1Handler)
|
|
||||||
// PutBucketPolicy
|
|
||||||
bucket.Methods("PUT").HandlerFunc(api.PutBucketPolicyHandler).Queries("policy", "")
|
|
||||||
// PutBucketNotification
|
|
||||||
bucket.Methods("PUT").HandlerFunc(api.PutBucketNotificationHandler).Queries("notification", "")
|
|
||||||
// PutBucket
|
|
||||||
bucket.Methods("PUT").HandlerFunc(api.PutBucketHandler)
|
|
||||||
// HeadBucket
|
|
||||||
bucket.Methods("HEAD").HandlerFunc(api.HeadBucketHandler)
|
|
||||||
// PostPolicy
|
|
||||||
bucket.Methods("POST").HeadersRegexp("Content-Type", "multipart/form-data*").HandlerFunc(api.PostPolicyBucketHandler)
|
|
||||||
// DeleteMultipleObjects
|
|
||||||
bucket.Methods("POST").HandlerFunc(api.DeleteMultipleObjectsHandler)
|
|
||||||
// DeleteBucketPolicy
|
|
||||||
bucket.Methods("DELETE").HandlerFunc(api.DeleteBucketPolicyHandler).Queries("policy", "")
|
|
||||||
// DeleteBucket
|
|
||||||
bucket.Methods("DELETE").HandlerFunc(api.DeleteBucketHandler)
|
|
||||||
|
|
||||||
/// Root operation
|
|
||||||
|
|
||||||
// ListBuckets
|
|
||||||
apiRouter.Methods("GET").HandlerFunc(api.ListBucketsHandler)
|
|
||||||
}
|
|
||||||
@@ -9,31 +9,50 @@ platform: x64
|
|||||||
|
|
||||||
clone_folder: c:\gopath\src\github.com\minio\minio
|
clone_folder: c:\gopath\src\github.com\minio\minio
|
||||||
|
|
||||||
# environment variables
|
# Environment variables
|
||||||
environment:
|
environment:
|
||||||
GOPATH: c:\gopath
|
GOPATH: c:\gopath
|
||||||
GO15VENDOREXPERIMENT: 1
|
GOROOT: c:\go
|
||||||
|
|
||||||
# scripts that run after cloning repository
|
# scripts that run after cloning repository
|
||||||
install:
|
install:
|
||||||
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
|
- set PATH=%GOPATH%\bin;%GOROOT%\bin;%PATH%
|
||||||
- go version
|
- go version
|
||||||
- go env
|
- go env
|
||||||
|
- python --version
|
||||||
|
|
||||||
# to run your custom scripts instead of automatic MSBuild
|
# To run your custom scripts instead of automatic MSBuild
|
||||||
build_script:
|
build_script:
|
||||||
- go test -race .
|
# Compile
|
||||||
- go test -race github.com/minio/minio/pkg...
|
# We need to disable firewall - https://github.com/appveyor/ci/issues/1579#issuecomment-309830648
|
||||||
- go test -coverprofile=coverage.txt -covermode=atomic
|
- ps: Disable-NetFirewallRule -DisplayName 'File and Printer Sharing (SMB-Out)'
|
||||||
|
- appveyor AddCompilationMessage "Starting Compile"
|
||||||
|
- cd c:\gopath\src\github.com\minio\minio
|
||||||
- go run buildscripts/gen-ldflags.go > temp.txt
|
- go run buildscripts/gen-ldflags.go > temp.txt
|
||||||
- set /p BUILD_LDFLAGS=<temp.txt
|
- set /p BUILD_LDFLAGS=<temp.txt
|
||||||
- go build -ldflags="%BUILD_LDFLAGS%" -o %GOPATH%\bin\minio.exe
|
- go build -ldflags="%BUILD_LDFLAGS%" -o %GOPATH%\bin\minio.exe
|
||||||
|
- appveyor AddCompilationMessage "Compile Success"
|
||||||
|
|
||||||
|
# To run your custom scripts instead of automatic tests
|
||||||
|
test_script:
|
||||||
|
# Unit tests
|
||||||
|
- ps: Add-AppveyorTest "Unit Tests" -Outcome Running
|
||||||
|
- mkdir build\coverage
|
||||||
|
- go test -v -timeout 17m -race github.com/minio/minio/cmd...
|
||||||
|
- go test -v -race github.com/minio/minio/pkg...
|
||||||
|
# FIXME(aead): enable codecov after issue https://github.com/golang/go/issues/18468 is solved.
|
||||||
|
# - go test -v -timeout 17m -coverprofile=build\coverage\coverage.txt -covermode=atomic github.com/minio/minio/cmd
|
||||||
|
- ps: Update-AppveyorTest "Unit Tests" -Outcome Passed
|
||||||
|
|
||||||
after_test:
|
after_test:
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
# FIXME(aead): enable codecov after issue https://github.com/golang/go/issues/18468 is solved.
|
||||||
|
# - go tool cover -html=build\coverage\coverage.txt -o build\coverage\coverage.html
|
||||||
# to disable automatic tests
|
# - ps: Push-AppveyorArtifact build\coverage\coverage.txt
|
||||||
test: off
|
# - ps: Push-AppveyorArtifact build\coverage\coverage.html
|
||||||
|
# Upload coverage report.
|
||||||
|
# - "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%"
|
||||||
|
# - pip install codecov
|
||||||
|
# - codecov -X gcov -f "build\coverage\coverage.txt"
|
||||||
|
|
||||||
# to disable deployment
|
# to disable deployment
|
||||||
deploy: off
|
deploy: off
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"es2015",
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"transform-object-rest-spread"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"esformatter-jsx"
|
||||||
|
],
|
||||||
|
// Copied from https://github.com/royriojas/esformatter-jsx
|
||||||
|
"jsx": {
|
||||||
|
"formatJSX": true, //Duh! that's the default
|
||||||
|
"attrsOnSameLineAsTag": false, // move each attribute to its own line
|
||||||
|
"maxAttrsOnTag": 3, // if lower or equal than 3 attributes, they will be kept on a single line
|
||||||
|
"firstAttributeOnSameLine": true, // keep the first attribute in the same line as the tag
|
||||||
|
"formatJSXExpressions": true, // default true, if false jsxExpressions won't be recursively formatted
|
||||||
|
"JSXExpressionsSingleLine": true, // default true, if false the JSXExpressions might span several lines
|
||||||
|
"alignWithFirstAttribute": false, // do not align attributes with the first tag
|
||||||
|
"spaceInJSXExpressionContainers": " ", // default to one space. Make it empty if you don't like spaces between JSXExpressionContainers
|
||||||
|
"removeSpaceBeforeClosingJSX": false, // default false. if true <React.Something /> => <React.Something/>
|
||||||
|
"closingTagOnNewLine": false, // default false. if true attributes on multiple lines will close the tag on a new line
|
||||||
|
"JSXAttributeQuotes": "", // possible values "single" or "double". Leave it as empty string if you don't want to modify the attributes' quotes
|
||||||
|
"htmlOptions": {
|
||||||
|
// put here the options for js-beautify.html
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
**/*.swp
|
||||||
|
cover.out
|
||||||
|
*~
|
||||||
|
minio
|
||||||
|
!*/
|
||||||
|
site/
|
||||||
|
**/*.test
|
||||||
|
**/*.sublime-workspace
|
||||||
|
/.idea/
|
||||||
|
/Minio.iml
|
||||||
|
**/access.log
|
||||||
|
build
|
||||||
|
vendor/**/*.js
|
||||||
|
vendor/**/*.json
|
||||||
|
release
|
||||||
|
.DS_Store
|
||||||
|
*.syso
|
||||||
|
coverage.txt
|
||||||
|
node_modules
|
||||||
|
production
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Minio File Browser
|
||||||
|
|
||||||
|
``Minio Browser`` provides minimal set of UI to manage buckets and objects on ``minio`` server. ``Minio Browser`` is written in javascript and released under [Apache 2.0 License](./LICENSE).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Install yarn
|
||||||
|
```sh
|
||||||
|
curl -o- -L https://yarnpkg.com/install.sh | bash
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install `go-bindata` and `go-bindata-assetfs`
|
||||||
|
|
||||||
|
If you do not have a working Golang environment, please follow [Install Golang](https://docs.minio.io/docs/how-to-install-golang)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get github.com/jteeuwen/go-bindata/...
|
||||||
|
go get github.com/elazarl/go-bindata-assetfs/...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generating Assets
|
||||||
|
|
||||||
|
### Generate ui-assets.go
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn release
|
||||||
|
```
|
||||||
|
|
||||||
|
This generates ui-assets.go in the current directory. Now do `make` in the parent directory to build the minio binary with the newly generated ``ui-assets.go``
|
||||||
|
|
||||||
|
### Run Minio Browser with live reload
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:8080/minio/](http://localhost:8080/minio/) in your browser to play with the application
|
||||||
|
|
||||||
|
### Run Minio Browser with live reload on custom port
|
||||||
|
|
||||||
|
Edit `browser/webpack.config.js`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
diff --git a/browser/webpack.config.js b/browser/webpack.config.js
|
||||||
|
index 3ccdaba..9496c56 100644
|
||||||
|
--- a/browser/webpack.config.js
|
||||||
|
+++ b/browser/webpack.config.js
|
||||||
|
@@ -58,6 +58,7 @@ var exports = {
|
||||||
|
historyApiFallback: {
|
||||||
|
index: '/minio/'
|
||||||
|
},
|
||||||
|
+ port: 8888,
|
||||||
|
proxy: {
|
||||||
|
'/minio/webrpc': {
|
||||||
|
target: 'http://localhost:9000',
|
||||||
|
@@ -97,7 +98,7 @@ var exports = {
|
||||||
|
if (process.env.NODE_ENV === 'dev') {
|
||||||
|
exports.entry = [
|
||||||
|
'webpack/hot/dev-server',
|
||||||
|
- 'webpack-dev-server/client?http://localhost:8080',
|
||||||
|
+ 'webpack-dev-server/client?http://localhost:8888',
|
||||||
|
path.resolve(__dirname, 'app/index.js')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:8888/minio/](http://localhost:8888/minio/) in your browser to play with the application
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
.page-load {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: #32393F;
|
||||||
|
z-index: 100;
|
||||||
|
transition: opacity 200ms;
|
||||||
|
-webkit-transition: opacity 200ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-0{
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-1 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-inner {
|
||||||
|
position: absolute;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -50px;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -50px;
|
||||||
|
text-align: center;
|
||||||
|
-webkit-animation: fade-in 500ms;
|
||||||
|
animation: fade-in 500ms;
|
||||||
|
-webkit-animation-fill-mode: both;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
animation-delay: 350ms;
|
||||||
|
-webkit-animation-delay: 350ms;
|
||||||
|
-webkit-backface-visibility: visible;
|
||||||
|
backface-visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-inner:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
display: block;
|
||||||
|
-webkit-animation: spin 1000ms infinite linear;
|
||||||
|
animation: spin 1000ms infinite linear;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);;
|
||||||
|
border-left-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-inner > img {
|
||||||
|
width: 30px;
|
||||||
|
margin-top: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes fade-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes spin {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="139.0389584668397 284.78404581828653 12.617622649141168 6.417622649141265"><defs><path d="M139.04 290.7L144.95 284.78L145.46 285.29L139.54 291.2L139.04 290.7Z" id="NsdmgIWbGe"></path><path d="M145.24 285.29L151.15 291.2L151.66 290.7L145.74 284.78L145.24 285.29Z" id="VqPWmhvQEo"></path></defs><g visibility="inherit"><g><use xlink:href="#NsdmgIWbGe" opacity="1" fill="#000000" fill-opacity="1"></use></g><g><use xlink:href="#VqPWmhvQEo" opacity="1" fill="#000000" fill-opacity="1"></use></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 797 B |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewbox="0 0 160 256"
|
||||||
|
version="1.1"
|
||||||
|
id="svg3092"
|
||||||
|
height="218.14844"
|
||||||
|
width="137">
|
||||||
|
<defs
|
||||||
|
id="defs3094" />
|
||||||
|
<metadata
|
||||||
|
id="metadata3097">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
transform="translate(0.99999967,-982.85149)"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
transform="matrix(1.0112586,0,0,1.0112586,5.4603732,-13.223714)"
|
||||||
|
id="g4144">
|
||||||
|
<g
|
||||||
|
id="g4140">
|
||||||
|
<g
|
||||||
|
style="image-rendering:auto"
|
||||||
|
id="minio"
|
||||||
|
transform="matrix(1.0000023,0,0,0.99999799,-739.31646,295.2269)">
|
||||||
|
<title
|
||||||
|
id="title3337">Minio Logo</title>
|
||||||
|
<path
|
||||||
|
d="m 803.42903,801.80813 c 12.40802,4.17067 27.0499,9.11665 37.95186,12.80906 -12.01295,-21.20683 -27.84305,-34.11687 -37.95186,-40.78578 l 0,27.97672 z m 0,93.72113 -14.22303,8.96217 0,-92.45864 c -1.52985,-0.5139 -2.97948,-0.9981 -4.33405,-1.45259 -19.98593,-6.67189 -32.7207,-17.95703 -35.85168,-31.77904 -2.54577,-11.21386 1.55064,-23.02184 11.24654,-32.39691 8.84929,-8.55225 21.22761,-18.39964 31.17304,-26.31619 3.60329,-2.86658 6.73129,-5.3173 9.2028,-7.39669 2.31406,-1.93977 1.61598,-4.95488 0.57033,-6.21441 -1.74073,-2.09127 -4.61921,-1.74669 -6.56195,-0.32379 -0.10398,0.0802 -5.65595,4.40832 -5.65595,4.40832 l -8.58195,-11.57033 c 0,0 5.60843,-4.14096 5.8223,-4.30137 8.39777,-6.155 19.54034,-4.98758 25.92406,2.71509 3.19039,3.84093 4.68459,8.69779 4.20929,13.67051 -0.47232,4.9549 -2.84579,9.43153 -6.68078,12.5922 -2.58439,2.12988 -5.73912,4.64298 -9.39291,7.54522 -9.70779,7.72641 -21.78905,17.33915 -30.14226,25.41907 -6.1253,5.9233 -8.70671,12.67834 -7.26896,19.02345 1.9873,8.75424 11.33268,16.34105 26.32213,21.37911 l 0,-46.22486 c 40.29563,13.62298 68.76248,61.22321 78.20589,87.64039 0,0 -41.76308,-14.15768 -63.98286,-21.64051 l 0,78.7198"
|
||||||
|
style="fill:#f8f8f8;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path48" />
|
||||||
|
<path
|
||||||
|
d="m 803.42903,826.12513 -14.22303,-4.78261 0,-9.30973 14.22303,4.77667 0,9.31567"
|
||||||
|
style="fill:#cdccca;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path50" />
|
||||||
|
<path
|
||||||
|
d="m 734.75566,745.06155 c -0.23469,0.16636 -0.54956,0.14853 -0.73077,-0.0743 -0.17823,-0.22576 -0.13063,-0.53766 0.0802,-0.73373 4.93113,-4.51525 24.45661,-23.86844 46.30805,-45.2624 l 8.58193,11.57033 c 0,0 -53.54135,34.01288 -54.23942,34.50007"
|
||||||
|
style="fill:#f14621;fill-opacity:1;fill-rule:nonzero;stroke:none"
|
||||||
|
id="path52" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" width="16" height="4" viewBox="4 10 16 4"><defs><path d="M4 12C4 13.1 4.9 14 6 14C7.1 14 8 13.1 8 12C8 10.9 7.1 10 6 10C4.9 10 4 10.9 4 12ZM16 12C16 13.1 16.9 14 18 14C19.1 14 20 13.1 20 12C20 10.9 19.1 10 18 10C16.9 10 16 10.9 16 12ZM10 12C10 13.1 10.9 14 12 14C13.1 14 14 13.1 14 12C14 10.9 13.1 10 12 10C10.9 10 10 10.9 10 12Z" id="mccsKZxKL3"></path></defs><g visibility="visible"><g><use xlink:href="#mccsKZxKL3" opacity="1" fill="#eaeaea" fill-opacity="1"></use><g><use xlink:href="#mccsKZxKL3" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="0"></use></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 894 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M6 10c-1.1 0-2 0.9-2 2s0.9 2 2 2 2-0.9 2-2-0.9-2-2-2zm12 0c-1.1 0-2 0.9-2 2s0.9 2 2 2 2-0.9 2-2-0.9-2-2-2zm-6 0c-1.1 0-2 0.9-2 2s0.9 2 2 2 2-0.9 2-2-0.9-2-2-2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 261 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" width="9" height="9" viewBox="326.76441742035513 536.0133077721175 13 13"><defs><path d="M339.76 536.01L326.76 549.01L339.76 549.01L339.76 536.01Z" id="kt3PSf43ua"></path></defs><g visibility="visible"><g><use xlink:href="#kt3PSf43ua" opacity="1" fill="#dadada" fill-opacity="1"></use></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 586 B |
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Minio Browser</title>
|
||||||
|
<link rel="stylesheet" href="loader.css" type="text/css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="page-load">
|
||||||
|
<div class="pl-inner">
|
||||||
|
<img src="logo.svg" alt="">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
<!--[if lt IE 11]>
|
||||||
|
<div class="ie-warning">
|
||||||
|
<div class="iw-inner">
|
||||||
|
<i class="iwi-icon fa fa-warning"></i>
|
||||||
|
|
||||||
|
You are using Internet Explorer version 12.0 or lower. Due to security issues and lack of support for Web Standards it is highly recommended that you upgrade to a modern browser
|
||||||
|
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="http://www.google.com/chrome/">
|
||||||
|
<img src="chrome.png" alt="">
|
||||||
|
<div>Chrome</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://www.mozilla.org/en-US/firefox/new/">
|
||||||
|
<img src="firefox.png" alt="">
|
||||||
|
<div>Firefox</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://www.apple.com/safari/">
|
||||||
|
<img src="safari.png" alt="">
|
||||||
|
<div>Safari</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="iwi-skip">Skip & Continue</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<![endif]-->
|
||||||
|
|
||||||
|
<script>currentUiVersion = 'MINIO_UI_VERSION'</script>
|
||||||
|
<script src="index_bundle.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import 'babel-polyfill'
|
||||||
|
|
||||||
|
import './less/main.less'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import thunkMiddleware from 'redux-thunk'
|
||||||
|
import createStore from 'redux/lib/createStore'
|
||||||
|
import applyMiddleware from 'redux/lib/applyMiddleware'
|
||||||
|
|
||||||
|
import Route from 'react-router/lib/Route'
|
||||||
|
import Router from 'react-router/lib/Router'
|
||||||
|
import browserHistory from 'react-router/lib/browserHistory'
|
||||||
|
import IndexRoute from 'react-router/lib/IndexRoute'
|
||||||
|
|
||||||
|
import Provider from 'react-redux/lib/components/Provider'
|
||||||
|
import connect from 'react-redux/lib/components/connect'
|
||||||
|
|
||||||
|
import Moment from 'moment'
|
||||||
|
|
||||||
|
import { minioBrowserPrefix } from './js/constants.js'
|
||||||
|
import * as actions from './js/actions.js'
|
||||||
|
import reducer from './js/reducers.js'
|
||||||
|
|
||||||
|
import _Login from './js/components/Login.js'
|
||||||
|
import _Browse from './js/components/Browse.js'
|
||||||
|
import fontAwesome from 'font-awesome/css/font-awesome.css'
|
||||||
|
|
||||||
|
import Web from './js/web'
|
||||||
|
window.Web = Web
|
||||||
|
|
||||||
|
import storage from 'local-storage-fallback'
|
||||||
|
const store = applyMiddleware(thunkMiddleware)(createStore)(reducer)
|
||||||
|
const Browse = connect(state => state)(_Browse)
|
||||||
|
const Login = connect(state => state)(_Login)
|
||||||
|
|
||||||
|
let web = new Web(`${window.location.protocol}//${window.location.host}${minioBrowserPrefix}/webrpc`, store.dispatch)
|
||||||
|
|
||||||
|
window.web = web
|
||||||
|
|
||||||
|
store.dispatch(actions.setWeb(web))
|
||||||
|
|
||||||
|
function authNeeded(nextState, replace, cb) {
|
||||||
|
if (web.LoggedIn()) {
|
||||||
|
return cb()
|
||||||
|
}
|
||||||
|
if (location.pathname === minioBrowserPrefix || location.pathname === minioBrowserPrefix + '/') {
|
||||||
|
replace(`${minioBrowserPrefix}/login`)
|
||||||
|
}
|
||||||
|
return cb()
|
||||||
|
}
|
||||||
|
|
||||||
|
function authNotNeeded(nextState, replace) {
|
||||||
|
if (web.LoggedIn()) {
|
||||||
|
replace(`${minioBrowserPrefix}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const App = (props) => {
|
||||||
|
return <div>
|
||||||
|
{ props.children }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.render((
|
||||||
|
<Provider store={ store } web={ web }>
|
||||||
|
<Router history={ browserHistory }>
|
||||||
|
<Route path='/' component={ App }>
|
||||||
|
<Route path={ minioBrowserPrefix } component={ App }>
|
||||||
|
<IndexRoute component={ Browse } onEnter={ authNeeded } />
|
||||||
|
<Route path='login' component={ Login } onEnter={ authNotNeeded } />
|
||||||
|
<Route path=':bucket' component={ Browse } onEnter={ authNeeded } />
|
||||||
|
<Route path=':bucket/*' component={ Browse } onEnter={ authNeeded } />
|
||||||
|
</Route>
|
||||||
|
</Route>
|
||||||
|
</Router>
|
||||||
|
</Provider>
|
||||||
|
), document.getElementById('root'))
|
||||||
|
|
||||||
|
//Page loader
|
||||||
|
let delay = [0, 400]
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
function handleLoader() {
|
||||||
|
if (i < 2) {
|
||||||
|
setTimeout(function() {
|
||||||
|
document.querySelector('.page-load').classList.add('pl-' + i)
|
||||||
|
i++
|
||||||
|
handleLoader()
|
||||||
|
}, delay[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleLoader()
|
||||||
|
|
||||||
|
if (storage.getItem('newlyUpdated')) {
|
||||||
|
store.dispatch(actions.showAlert({
|
||||||
|
type: 'success',
|
||||||
|
message: "Updated to the latest UI Version."
|
||||||
|
}))
|
||||||
|
storage.removeItem('newlyUpdated')
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import expect from 'expect';
|
||||||
|
import JSONrpc from '../jsonrpc';
|
||||||
|
|
||||||
|
describe('jsonrpc', () => {
|
||||||
|
it('should fail with invalid endpoint', (done) => {
|
||||||
|
try {
|
||||||
|
let jsonRPC = new JSONrpc({
|
||||||
|
endpoint: 'htt://localhost:9000',
|
||||||
|
namespace: 'Test'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('should succeed with valid endpoint', () => {
|
||||||
|
let jsonRPC = new JSONrpc({
|
||||||
|
endpoint: 'http://localhost:9000/webrpc',
|
||||||
|
namespace: 'Test'
|
||||||
|
});
|
||||||
|
expect(jsonRPC.version).toEqual('2.0');
|
||||||
|
expect(jsonRPC.host).toEqual('localhost');
|
||||||
|
expect(jsonRPC.port).toEqual('9000');
|
||||||
|
expect(jsonRPC.path).toEqual('/webrpc');
|
||||||
|
expect(jsonRPC.scheme).toEqual('http');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,647 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Moment from 'moment'
|
||||||
|
import browserHistory from 'react-router/lib/browserHistory'
|
||||||
|
import storage from 'local-storage-fallback'
|
||||||
|
import { minioBrowserPrefix } from './constants'
|
||||||
|
|
||||||
|
export const SET_WEB = 'SET_WEB'
|
||||||
|
export const SET_CURRENT_BUCKET = 'SET_CURRENT_BUCKET'
|
||||||
|
export const SET_CURRENT_PATH = 'SET_CURRENT_PATH'
|
||||||
|
export const SET_BUCKETS = 'SET_BUCKETS'
|
||||||
|
export const ADD_BUCKET = 'ADD_BUCKET'
|
||||||
|
export const SET_VISIBLE_BUCKETS = 'SET_VISIBLE_BUCKETS'
|
||||||
|
export const SET_OBJECTS = 'SET_OBJECTS'
|
||||||
|
export const APPEND_OBJECTS = 'APPEND_OBJECTS'
|
||||||
|
export const RESET_OBJECTS = 'RESET_OBJECTS'
|
||||||
|
export const SET_STORAGE_INFO = 'SET_STORAGE_INFO'
|
||||||
|
export const SET_SERVER_INFO = 'SET_SERVER_INFO'
|
||||||
|
export const SHOW_MAKEBUCKET_MODAL = 'SHOW_MAKEBUCKET_MODAL'
|
||||||
|
export const ADD_UPLOAD = 'ADD_UPLOAD'
|
||||||
|
export const STOP_UPLOAD = 'STOP_UPLOAD'
|
||||||
|
export const UPLOAD_PROGRESS = 'UPLOAD_PROGRESS'
|
||||||
|
export const SET_ALERT = 'SET_ALERT'
|
||||||
|
export const SET_LOGIN_ERROR = 'SET_LOGIN_ERROR'
|
||||||
|
export const SET_SHOW_ABORT_MODAL = 'SET_SHOW_ABORT_MODAL'
|
||||||
|
export const SHOW_ABOUT = 'SHOW_ABOUT'
|
||||||
|
export const SET_SORT_NAME_ORDER = 'SET_SORT_NAME_ORDER'
|
||||||
|
export const SET_SORT_SIZE_ORDER = 'SET_SORT_SIZE_ORDER'
|
||||||
|
export const SET_SORT_DATE_ORDER = 'SET_SORT_DATE_ORDER'
|
||||||
|
export const SET_LATEST_UI_VERSION = 'SET_LATEST_UI_VERSION'
|
||||||
|
export const SET_SIDEBAR_STATUS = 'SET_SIDEBAR_STATUS'
|
||||||
|
export const SET_LOGIN_REDIRECT_PATH = 'SET_LOGIN_REDIRECT_PATH'
|
||||||
|
export const SET_LOAD_BUCKET = 'SET_LOAD_BUCKET'
|
||||||
|
export const SET_LOAD_PATH = 'SET_LOAD_PATH'
|
||||||
|
export const SHOW_SETTINGS = 'SHOW_SETTINGS'
|
||||||
|
export const SET_SETTINGS = 'SET_SETTINGS'
|
||||||
|
export const SHOW_BUCKET_POLICY = 'SHOW_BUCKET_POLICY'
|
||||||
|
export const SET_POLICIES = 'SET_POLICIES'
|
||||||
|
export const SET_SHARE_OBJECT = 'SET_SHARE_OBJECT'
|
||||||
|
export const DELETE_CONFIRMATION = 'DELETE_CONFIRMATION'
|
||||||
|
export const SET_PREFIX_WRITABLE = 'SET_PREFIX_WRITABLE'
|
||||||
|
export const REMOVE_OBJECT = 'REMOVE_OBJECT'
|
||||||
|
export const CHECKED_OBJECTS_ADD = 'CHECKED_OBJECTS_ADD'
|
||||||
|
export const CHECKED_OBJECTS_REMOVE = 'CHECKED_OBJECTS_REMOVE'
|
||||||
|
export const CHECKED_OBJECTS_RESET = 'CHECKED_OBJECTS_RESET'
|
||||||
|
|
||||||
|
export const showDeleteConfirmation = (object) => {
|
||||||
|
return {
|
||||||
|
type: DELETE_CONFIRMATION,
|
||||||
|
payload: {
|
||||||
|
object,
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hideDeleteConfirmation = () => {
|
||||||
|
return {
|
||||||
|
type: DELETE_CONFIRMATION,
|
||||||
|
payload: {
|
||||||
|
object: '',
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showShareObject = (object, url) => {
|
||||||
|
return {
|
||||||
|
type: SET_SHARE_OBJECT,
|
||||||
|
shareObject: {
|
||||||
|
object,
|
||||||
|
url,
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hideShareObject = () => {
|
||||||
|
return {
|
||||||
|
type: SET_SHARE_OBJECT,
|
||||||
|
shareObject: {
|
||||||
|
url: '',
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shareObject = (object, days, hours, minutes) => (dispatch, getState) => {
|
||||||
|
const {currentBucket, web} = getState()
|
||||||
|
let host = location.host
|
||||||
|
let bucket = currentBucket
|
||||||
|
|
||||||
|
if (!web.LoggedIn()) {
|
||||||
|
dispatch(showShareObject(object, `${host}/${bucket}/${object}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60
|
||||||
|
web.PresignedGet({
|
||||||
|
host,
|
||||||
|
bucket,
|
||||||
|
object,
|
||||||
|
expiry
|
||||||
|
})
|
||||||
|
.then(obj => {
|
||||||
|
dispatch(showShareObject(object, obj.url))
|
||||||
|
dispatch(showAlert({
|
||||||
|
type: 'success',
|
||||||
|
message: `Object shared. Expires in ${days} days ${hours} hours ${minutes} minutes.`
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
dispatch(showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: err.message
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setLoginRedirectPath = (path) => {
|
||||||
|
return {
|
||||||
|
type: SET_LOGIN_REDIRECT_PATH,
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setLoadPath = (loadPath) => {
|
||||||
|
return {
|
||||||
|
type: SET_LOAD_PATH,
|
||||||
|
loadPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setLoadBucket = (loadBucket) => {
|
||||||
|
return {
|
||||||
|
type: SET_LOAD_BUCKET,
|
||||||
|
loadBucket
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setWeb = web => {
|
||||||
|
return {
|
||||||
|
type: SET_WEB,
|
||||||
|
web
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setBuckets = buckets => {
|
||||||
|
return {
|
||||||
|
type: SET_BUCKETS,
|
||||||
|
buckets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addBucket = bucket => {
|
||||||
|
return {
|
||||||
|
type: ADD_BUCKET,
|
||||||
|
bucket
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showMakeBucketModal = () => {
|
||||||
|
return {
|
||||||
|
type: SHOW_MAKEBUCKET_MODAL,
|
||||||
|
showMakeBucketModal: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hideAlert = () => {
|
||||||
|
return {
|
||||||
|
type: SET_ALERT,
|
||||||
|
alert: {
|
||||||
|
show: false,
|
||||||
|
message: '',
|
||||||
|
type: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showAlert = alert => {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
let alertTimeout = null
|
||||||
|
if (alert.type !== 'danger') {
|
||||||
|
alertTimeout = setTimeout(() => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_ALERT,
|
||||||
|
alert: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
dispatch({
|
||||||
|
type: SET_ALERT,
|
||||||
|
alert: Object.assign({}, alert, {
|
||||||
|
show: true,
|
||||||
|
alertTimeout
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeObject = object => {
|
||||||
|
return {
|
||||||
|
type: REMOVE_OBJECT,
|
||||||
|
object
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setSidebarStatus = (status) => {
|
||||||
|
return {
|
||||||
|
type: SET_SIDEBAR_STATUS,
|
||||||
|
sidebarStatus: status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hideMakeBucketModal = () => {
|
||||||
|
return {
|
||||||
|
type: SHOW_MAKEBUCKET_MODAL,
|
||||||
|
showMakeBucketModal: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setVisibleBuckets = visibleBuckets => {
|
||||||
|
return {
|
||||||
|
type: SET_VISIBLE_BUCKETS,
|
||||||
|
visibleBuckets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendObjects = (objects, marker, istruncated) => {
|
||||||
|
return {
|
||||||
|
type: APPEND_OBJECTS,
|
||||||
|
objects,
|
||||||
|
marker,
|
||||||
|
istruncated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setObjects = (objects) => {
|
||||||
|
return {
|
||||||
|
type: SET_OBJECTS,
|
||||||
|
objects,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resetObjects = () => {
|
||||||
|
return {
|
||||||
|
type: RESET_OBJECTS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setCurrentBucket = currentBucket => {
|
||||||
|
return {
|
||||||
|
type: SET_CURRENT_BUCKET,
|
||||||
|
currentBucket
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setCurrentPath = currentPath => {
|
||||||
|
return {
|
||||||
|
type: SET_CURRENT_PATH,
|
||||||
|
currentPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setStorageInfo = storageInfo => {
|
||||||
|
return {
|
||||||
|
type: SET_STORAGE_INFO,
|
||||||
|
storageInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setServerInfo = serverInfo => {
|
||||||
|
return {
|
||||||
|
type: SET_SERVER_INFO,
|
||||||
|
serverInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setPrefixWritable = prefixWritable => {
|
||||||
|
return {
|
||||||
|
type: SET_PREFIX_WRITABLE,
|
||||||
|
prefixWritable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectBucket = (newCurrentBucket, prefix) => {
|
||||||
|
if (!prefix)
|
||||||
|
prefix = ''
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
let web = getState().web
|
||||||
|
let currentBucket = getState().currentBucket
|
||||||
|
|
||||||
|
if (currentBucket !== newCurrentBucket) dispatch(setLoadBucket(newCurrentBucket))
|
||||||
|
|
||||||
|
dispatch(setCurrentBucket(newCurrentBucket))
|
||||||
|
dispatch(selectPrefix(prefix))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listObjects = () => {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const {currentBucket, currentPath, marker, objects, istruncated, web} = getState()
|
||||||
|
if (!istruncated) return
|
||||||
|
web.ListObjects({
|
||||||
|
bucketName: currentBucket,
|
||||||
|
prefix: currentPath,
|
||||||
|
marker: marker
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
let objects = res.objects
|
||||||
|
if (!objects)
|
||||||
|
objects = []
|
||||||
|
objects = objects.map(object => {
|
||||||
|
object.name = object.name.replace(`${currentPath}`, '');
|
||||||
|
return object
|
||||||
|
})
|
||||||
|
dispatch(appendObjects(objects, res.nextmarker, res.istruncated))
|
||||||
|
dispatch(setPrefixWritable(res.writable))
|
||||||
|
dispatch(setLoadBucket(''))
|
||||||
|
dispatch(setLoadPath(''))
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
dispatch(showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: err.message
|
||||||
|
}))
|
||||||
|
dispatch(setLoadBucket(''))
|
||||||
|
dispatch(setLoadPath(''))
|
||||||
|
// Use browserHistory.replace instead of push so that browser back button works fine.
|
||||||
|
browserHistory.replace(`${minioBrowserPrefix}/login`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectPrefix = prefix => {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const {currentBucket, web} = getState()
|
||||||
|
dispatch(resetObjects())
|
||||||
|
dispatch(setLoadPath(prefix))
|
||||||
|
web.ListObjects({
|
||||||
|
bucketName: currentBucket,
|
||||||
|
prefix,
|
||||||
|
marker: ""
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
let objects = res.objects
|
||||||
|
if (!objects)
|
||||||
|
objects = []
|
||||||
|
objects = objects.map(object => {
|
||||||
|
object.name = object.name.replace(`${prefix}`, '');
|
||||||
|
return object
|
||||||
|
})
|
||||||
|
dispatch(appendObjects(
|
||||||
|
objects,
|
||||||
|
res.nextmarker,
|
||||||
|
res.istruncated
|
||||||
|
))
|
||||||
|
dispatch(setPrefixWritable(res.writable))
|
||||||
|
dispatch(setSortNameOrder(false))
|
||||||
|
dispatch(setCurrentPath(prefix))
|
||||||
|
dispatch(setLoadBucket(''))
|
||||||
|
dispatch(setLoadPath(''))
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
dispatch(showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: err.message
|
||||||
|
}))
|
||||||
|
dispatch(setLoadBucket(''))
|
||||||
|
dispatch(setLoadPath(''))
|
||||||
|
// Use browserHistory.replace instead of push so that browser back button works fine.
|
||||||
|
browserHistory.replace(`${minioBrowserPrefix}/login`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addUpload = options => {
|
||||||
|
return {
|
||||||
|
type: ADD_UPLOAD,
|
||||||
|
slug: options.slug,
|
||||||
|
size: options.size,
|
||||||
|
xhr: options.xhr,
|
||||||
|
name: options.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stopUpload = options => {
|
||||||
|
return {
|
||||||
|
type: STOP_UPLOAD,
|
||||||
|
slug: options.slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadProgress = options => {
|
||||||
|
return {
|
||||||
|
type: UPLOAD_PROGRESS,
|
||||||
|
slug: options.slug,
|
||||||
|
loaded: options.loaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setShowAbortModal = showAbortModal => {
|
||||||
|
return {
|
||||||
|
type: SET_SHOW_ABORT_MODAL,
|
||||||
|
showAbortModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setLoginError = () => {
|
||||||
|
return {
|
||||||
|
type: SET_LOGIN_ERROR,
|
||||||
|
loginError: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const downloadSelected = (url, req, xhr) => {
|
||||||
|
return (dispatch) => {
|
||||||
|
var anchor = document.createElement('a')
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
xhr.open('POST', url, true)
|
||||||
|
xhr.responseType = 'blob'
|
||||||
|
|
||||||
|
xhr.onload = function(e) {
|
||||||
|
if (this.status == 200) {
|
||||||
|
dispatch(checkedObjectsReset())
|
||||||
|
var blob = new Blob([this.response], {
|
||||||
|
type: 'octet/stream'
|
||||||
|
})
|
||||||
|
var blobUrl = window.URL.createObjectURL(blob);
|
||||||
|
var separator = req.prefix.length > 1 ? '-' : ''
|
||||||
|
|
||||||
|
anchor.href = blobUrl
|
||||||
|
anchor.download = req.bucketName+separator+req.prefix.slice(0, -1)+'.zip';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
anchor.click()
|
||||||
|
window.URL.revokeObjectURL(blobUrl)
|
||||||
|
anchor.remove()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(JSON.stringify(req));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadFile = (file, xhr) => {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const {currentBucket, currentPath} = getState()
|
||||||
|
const objectName = `${currentPath}${file.name}`
|
||||||
|
const uploadUrl = `${window.location.origin}${minioBrowserPrefix}/upload/${currentBucket}/${objectName}`
|
||||||
|
// The slug is a unique identifer for the file upload.
|
||||||
|
const slug = `${currentBucket}-${currentPath}-${file.name}`
|
||||||
|
|
||||||
|
xhr.open('PUT', uploadUrl, true)
|
||||||
|
xhr.withCredentials = false
|
||||||
|
const token = storage.getItem('token')
|
||||||
|
if (token) xhr.setRequestHeader("Authorization", 'Bearer ' + storage.getItem('token'))
|
||||||
|
xhr.setRequestHeader('x-amz-date', Moment().utc().format('YYYYMMDDTHHmmss') + 'Z')
|
||||||
|
dispatch(addUpload({
|
||||||
|
slug,
|
||||||
|
xhr,
|
||||||
|
size: file.size,
|
||||||
|
name: file.name
|
||||||
|
}))
|
||||||
|
|
||||||
|
xhr.onload = function(event) {
|
||||||
|
if (xhr.status == 401 || xhr.status == 403) {
|
||||||
|
setShowAbortModal(false)
|
||||||
|
dispatch(stopUpload({
|
||||||
|
slug
|
||||||
|
}))
|
||||||
|
dispatch(showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: 'Unauthorized request.'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (xhr.status == 500) {
|
||||||
|
setShowAbortModal(false)
|
||||||
|
dispatch(stopUpload({
|
||||||
|
slug
|
||||||
|
}))
|
||||||
|
dispatch(showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: xhr.responseText
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (xhr.status == 200) {
|
||||||
|
setShowAbortModal(false)
|
||||||
|
dispatch(stopUpload({
|
||||||
|
slug
|
||||||
|
}))
|
||||||
|
dispatch(showAlert({
|
||||||
|
type: 'success',
|
||||||
|
message: 'File \'' + file.name + '\' uploaded successfully.'
|
||||||
|
}))
|
||||||
|
dispatch(selectPrefix(currentPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.upload.addEventListener('error', event => {
|
||||||
|
dispatch(showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: 'Error occurred uploading \'' + file.name + '\'.'
|
||||||
|
}))
|
||||||
|
dispatch(stopUpload({
|
||||||
|
slug
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
xhr.upload.addEventListener('progress', event => {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
let loaded = event.loaded
|
||||||
|
let total = event.total
|
||||||
|
|
||||||
|
// Update the counter.
|
||||||
|
dispatch(uploadProgress({
|
||||||
|
slug,
|
||||||
|
loaded
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
xhr.send(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showAbout = () => {
|
||||||
|
return {
|
||||||
|
type: SHOW_ABOUT,
|
||||||
|
showAbout: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hideAbout = () => {
|
||||||
|
return {
|
||||||
|
type: SHOW_ABOUT,
|
||||||
|
showAbout: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setSortNameOrder = (sortNameOrder) => {
|
||||||
|
return {
|
||||||
|
type: SET_SORT_NAME_ORDER,
|
||||||
|
sortNameOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setSortSizeOrder = (sortSizeOrder) => {
|
||||||
|
return {
|
||||||
|
type: SET_SORT_SIZE_ORDER,
|
||||||
|
sortSizeOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setSortDateOrder = (sortDateOrder) => {
|
||||||
|
return {
|
||||||
|
type: SET_SORT_DATE_ORDER,
|
||||||
|
sortDateOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setLatestUIVersion = (latestUiVersion) => {
|
||||||
|
return {
|
||||||
|
type: SET_LATEST_UI_VERSION,
|
||||||
|
latestUiVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showSettings = () => {
|
||||||
|
return {
|
||||||
|
type: SHOW_SETTINGS,
|
||||||
|
showSettings: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hideSettings = () => {
|
||||||
|
return {
|
||||||
|
type: SHOW_SETTINGS,
|
||||||
|
showSettings: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setSettings = (settings) => {
|
||||||
|
return {
|
||||||
|
type: SET_SETTINGS,
|
||||||
|
settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showBucketPolicy = () => {
|
||||||
|
return {
|
||||||
|
type: SHOW_BUCKET_POLICY,
|
||||||
|
showBucketPolicy: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hideBucketPolicy = () => {
|
||||||
|
return {
|
||||||
|
type: SHOW_BUCKET_POLICY,
|
||||||
|
showBucketPolicy: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setPolicies = (policies) => {
|
||||||
|
return {
|
||||||
|
type: SET_POLICIES,
|
||||||
|
policies
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkedObjectsAdd = (objectName) => {
|
||||||
|
return {
|
||||||
|
type: CHECKED_OBJECTS_ADD,
|
||||||
|
objectName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkedObjectsRemove = (objectName) => {
|
||||||
|
return {
|
||||||
|
type: CHECKED_OBJECTS_REMOVE,
|
||||||
|
objectName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkedObjectsReset = (objectName) => {
|
||||||
|
return {
|
||||||
|
type: CHECKED_OBJECTS_RESET,
|
||||||
|
objectName
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,843 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import browserHistory from 'react-router/lib/browserHistory'
|
||||||
|
import humanize from 'humanize'
|
||||||
|
import Moment from 'moment'
|
||||||
|
import Modal from 'react-bootstrap/lib/Modal'
|
||||||
|
import ModalBody from 'react-bootstrap/lib/ModalBody'
|
||||||
|
import ModalHeader from 'react-bootstrap/lib/ModalHeader'
|
||||||
|
import Alert from 'react-bootstrap/lib/Alert'
|
||||||
|
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'
|
||||||
|
import Tooltip from 'react-bootstrap/lib/Tooltip'
|
||||||
|
import Dropdown from 'react-bootstrap/lib/Dropdown'
|
||||||
|
import MenuItem from 'react-bootstrap/lib/MenuItem'
|
||||||
|
import InputGroup from '../components/InputGroup'
|
||||||
|
import Dropzone from '../components/Dropzone'
|
||||||
|
import ObjectsList from '../components/ObjectsList'
|
||||||
|
import SideBar from '../components/SideBar'
|
||||||
|
import Path from '../components/Path'
|
||||||
|
import BrowserUpdate from '../components/BrowserUpdate'
|
||||||
|
import UploadModal from '../components/UploadModal'
|
||||||
|
import SettingsModal from '../components/SettingsModal'
|
||||||
|
import PolicyInput from '../components/PolicyInput'
|
||||||
|
import Policy from '../components/Policy'
|
||||||
|
import BrowserDropdown from '../components/BrowserDropdown'
|
||||||
|
import ConfirmModal from './ConfirmModal'
|
||||||
|
import logo from '../../img/logo.svg'
|
||||||
|
import * as actions from '../actions'
|
||||||
|
import * as utils from '../utils'
|
||||||
|
import * as mime from '../mime'
|
||||||
|
import { minioBrowserPrefix } from '../constants'
|
||||||
|
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||||
|
import storage from 'local-storage-fallback'
|
||||||
|
import InfiniteScroll from 'react-infinite-scroller';
|
||||||
|
|
||||||
|
export default class Browse extends React.Component {
|
||||||
|
componentDidMount() {
|
||||||
|
const {web, dispatch, currentBucket} = this.props
|
||||||
|
if (!web.LoggedIn()) return
|
||||||
|
web.StorageInfo()
|
||||||
|
.then(res => {
|
||||||
|
let storageInfo = Object.assign({}, {
|
||||||
|
total: res.storageInfo.Total,
|
||||||
|
free: res.storageInfo.Free
|
||||||
|
})
|
||||||
|
storageInfo.used = storageInfo.total - storageInfo.free
|
||||||
|
dispatch(actions.setStorageInfo(storageInfo))
|
||||||
|
return web.ServerInfo()
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
let serverInfo = Object.assign({}, {
|
||||||
|
version: res.MinioVersion,
|
||||||
|
memory: res.MinioMemory,
|
||||||
|
platform: res.MinioPlatform,
|
||||||
|
runtime: res.MinioRuntime,
|
||||||
|
info: res.MinioGlobalInfo
|
||||||
|
})
|
||||||
|
dispatch(actions.setServerInfo(serverInfo))
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: err.message
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
// Clear out any stale message in the alert of Login page
|
||||||
|
dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: ''
|
||||||
|
}))
|
||||||
|
if (web.LoggedIn()) {
|
||||||
|
web.ListBuckets()
|
||||||
|
.then(res => {
|
||||||
|
let buckets
|
||||||
|
if (!res.buckets)
|
||||||
|
buckets = []
|
||||||
|
else
|
||||||
|
buckets = res.buckets.map(bucket => bucket.name)
|
||||||
|
if (buckets.length) {
|
||||||
|
dispatch(actions.setBuckets(buckets))
|
||||||
|
dispatch(actions.setVisibleBuckets(buckets))
|
||||||
|
if (location.pathname === minioBrowserPrefix || location.pathname === minioBrowserPrefix + '/') {
|
||||||
|
browserHistory.push(utils.pathJoin(buckets[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.history = browserHistory.listen(({pathname}) => {
|
||||||
|
let decPathname = decodeURI(pathname)
|
||||||
|
if (decPathname === `${minioBrowserPrefix}/login`) return // FIXME: better organize routes and remove this
|
||||||
|
if (!decPathname.endsWith('/'))
|
||||||
|
decPathname += '/'
|
||||||
|
if (decPathname === minioBrowserPrefix + '/') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let obj = utils.pathSlice(decPathname)
|
||||||
|
if (!web.LoggedIn()) {
|
||||||
|
dispatch(actions.setBuckets([obj.bucket]))
|
||||||
|
dispatch(actions.setVisibleBuckets([obj.bucket]))
|
||||||
|
}
|
||||||
|
dispatch(actions.selectBucket(obj.bucket, obj.prefix))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.history()
|
||||||
|
}
|
||||||
|
|
||||||
|
selectBucket(e, bucket) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (bucket === this.props.currentBucket) return
|
||||||
|
browserHistory.push(utils.pathJoin(bucket))
|
||||||
|
}
|
||||||
|
|
||||||
|
searchBuckets(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
let {buckets} = this.props
|
||||||
|
this.props.dispatch(actions.setVisibleBuckets(buckets.filter(bucket => bucket.indexOf(e.target.value) > -1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
listObjects() {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.listObjects())
|
||||||
|
}
|
||||||
|
|
||||||
|
selectPrefix(e, prefix) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch, currentPath, web, currentBucket} = this.props
|
||||||
|
const encPrefix = encodeURI(prefix)
|
||||||
|
if (prefix.endsWith('/') || prefix === '') {
|
||||||
|
if (prefix === currentPath) return
|
||||||
|
browserHistory.push(utils.pathJoin(currentBucket, encPrefix))
|
||||||
|
} else {
|
||||||
|
if (!web.LoggedIn()) {
|
||||||
|
let url = `${window.location.origin}/minio/download/${currentBucket}/${encPrefix}?token=''`
|
||||||
|
window.location = url
|
||||||
|
} else {
|
||||||
|
// Download the selected file.
|
||||||
|
web.CreateURLToken()
|
||||||
|
.then(res => {
|
||||||
|
let url = `${window.location.origin}${minioBrowserPrefix}/download/${currentBucket}/${encPrefix}?token=${res.token}`
|
||||||
|
window.location = url
|
||||||
|
})
|
||||||
|
.catch(err => dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: err.message
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
makeBucket(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const bucketName = this.refs.makeBucketRef.value
|
||||||
|
this.refs.makeBucketRef.value = ''
|
||||||
|
const {web, dispatch} = this.props
|
||||||
|
this.hideMakeBucketModal()
|
||||||
|
web.MakeBucket({
|
||||||
|
bucketName
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
dispatch(actions.addBucket(bucketName))
|
||||||
|
dispatch(actions.selectBucket(bucketName))
|
||||||
|
})
|
||||||
|
.catch(err => dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: err.message
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
hideMakeBucketModal() {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.hideMakeBucketModal())
|
||||||
|
}
|
||||||
|
|
||||||
|
showMakeBucketModal(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.showMakeBucketModal())
|
||||||
|
}
|
||||||
|
|
||||||
|
showAbout(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.showAbout())
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAbout(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.hideAbout())
|
||||||
|
}
|
||||||
|
|
||||||
|
showBucketPolicy(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.showBucketPolicy())
|
||||||
|
}
|
||||||
|
|
||||||
|
hideBucketPolicy(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.hideBucketPolicy())
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFile(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch, buckets} = this.props
|
||||||
|
|
||||||
|
if (buckets.length === 0) {
|
||||||
|
dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: "Bucket needs to be created before trying to upload files."
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let file = e.target.files[0]
|
||||||
|
e.target.value = null
|
||||||
|
this.xhr = new XMLHttpRequest()
|
||||||
|
dispatch(actions.uploadFile(file, this.xhr))
|
||||||
|
}
|
||||||
|
|
||||||
|
removeObject() {
|
||||||
|
const {web, dispatch, currentPath, currentBucket, deleteConfirmation, checkedObjects} = this.props
|
||||||
|
let objects = []
|
||||||
|
if (checkedObjects.length > 0) {
|
||||||
|
objects = checkedObjects.map(obj => `${currentPath}${obj}`)
|
||||||
|
} else {
|
||||||
|
objects = [deleteConfirmation.object]
|
||||||
|
}
|
||||||
|
|
||||||
|
web.RemoveObject({
|
||||||
|
bucketname: currentBucket,
|
||||||
|
objects: objects
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.hideDeleteConfirmation()
|
||||||
|
if (checkedObjects.length > 0) {
|
||||||
|
for (let i = 0; i < checkedObjects.length; i++) {
|
||||||
|
dispatch(actions.removeObject(checkedObjects[i].replace(currentPath, '')))
|
||||||
|
}
|
||||||
|
dispatch(actions.checkedObjectsReset())
|
||||||
|
} else {
|
||||||
|
let delObject = deleteConfirmation.object.replace(currentPath, '')
|
||||||
|
dispatch(actions.removeObject(delObject))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: e.message
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAlert(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.hideAlert())
|
||||||
|
}
|
||||||
|
|
||||||
|
showDeleteConfirmation(e, object) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.showDeleteConfirmation(object))
|
||||||
|
}
|
||||||
|
|
||||||
|
hideDeleteConfirmation() {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.hideDeleteConfirmation())
|
||||||
|
}
|
||||||
|
|
||||||
|
shareObject(e, object) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch} = this.props
|
||||||
|
// let expiry = 5 * 24 * 60 * 60 // 5 days expiry by default
|
||||||
|
dispatch(actions.shareObject(object, 5, 0, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
hideShareObjectModal() {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.hideShareObject())
|
||||||
|
}
|
||||||
|
|
||||||
|
dataType(name, contentType) {
|
||||||
|
return mime.getDataType(name, contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
sortObjectsByName(e) {
|
||||||
|
const {dispatch, objects, sortNameOrder} = this.props
|
||||||
|
dispatch(actions.setObjects(utils.sortObjectsByName(objects, !sortNameOrder)))
|
||||||
|
dispatch(actions.setSortNameOrder(!sortNameOrder))
|
||||||
|
}
|
||||||
|
|
||||||
|
sortObjectsBySize() {
|
||||||
|
const {dispatch, objects, sortSizeOrder} = this.props
|
||||||
|
dispatch(actions.setObjects(utils.sortObjectsBySize(objects, !sortSizeOrder)))
|
||||||
|
dispatch(actions.setSortSizeOrder(!sortSizeOrder))
|
||||||
|
}
|
||||||
|
|
||||||
|
sortObjectsByDate() {
|
||||||
|
const {dispatch, objects, sortDateOrder} = this.props
|
||||||
|
dispatch(actions.setObjects(utils.sortObjectsByDate(objects, !sortDateOrder)))
|
||||||
|
dispatch(actions.setSortDateOrder(!sortDateOrder))
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(e) {
|
||||||
|
const {web} = this.props
|
||||||
|
e.preventDefault()
|
||||||
|
web.Logout()
|
||||||
|
browserHistory.push(`${minioBrowserPrefix}/login`)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullScreen(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
let el = document.documentElement
|
||||||
|
if (el.requestFullscreen) {
|
||||||
|
el.requestFullscreen()
|
||||||
|
}
|
||||||
|
if (el.mozRequestFullScreen) {
|
||||||
|
el.mozRequestFullScreen()
|
||||||
|
}
|
||||||
|
if (el.webkitRequestFullscreen) {
|
||||||
|
el.webkitRequestFullscreen()
|
||||||
|
}
|
||||||
|
if (el.msRequestFullscreen) {
|
||||||
|
el.msRequestFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSidebar(status) {
|
||||||
|
this.props.dispatch(actions.setSidebarStatus(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSidebar(event) {
|
||||||
|
let e = event || window.event;
|
||||||
|
|
||||||
|
// Support all browsers.
|
||||||
|
let target = e.srcElement || e.target;
|
||||||
|
if (target.nodeType === 3) // Safari support.
|
||||||
|
target = target.parentNode;
|
||||||
|
|
||||||
|
let targetID = target.id;
|
||||||
|
if (!(targetID === 'feh-trigger')) {
|
||||||
|
this.props.dispatch(actions.setSidebarStatus(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showSettings(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.showSettings())
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessage() {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.showAlert({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Link copied to clipboard!'
|
||||||
|
}))
|
||||||
|
this.hideShareObjectModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
selectTexts() {
|
||||||
|
this.refs.copyTextInput.select()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleExpireValue(targetInput, inc, object) {
|
||||||
|
let value = this.refs[targetInput].value
|
||||||
|
let maxValue = (targetInput == 'expireHours') ? 23 : (targetInput == 'expireMins') ? 59 : (targetInput == 'expireDays') ? 7 : 0
|
||||||
|
value = isNaN(value) ? 0 : value
|
||||||
|
|
||||||
|
// Use custom step count to support browser Edge
|
||||||
|
if((inc === -1)) {
|
||||||
|
if(value != 0) {
|
||||||
|
value--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(value != maxValue) {
|
||||||
|
value++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.refs[targetInput].value = value
|
||||||
|
|
||||||
|
// Reset hours and mins when days reaches it's max value
|
||||||
|
if (this.refs.expireDays.value == 7) {
|
||||||
|
this.refs.expireHours.value = 0
|
||||||
|
this.refs.expireMins.value = 0
|
||||||
|
}
|
||||||
|
if (this.refs.expireDays.value + this.refs.expireHours.value + this.refs.expireMins.value == 0) {
|
||||||
|
this.refs.expireDays.value = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.shareObject(object, this.refs.expireDays.value, this.refs.expireHours.value, this.refs.expireMins.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
checkObject(e, objectName) {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
e.target.checked ? dispatch(actions.checkedObjectsAdd(objectName)) : dispatch(actions.checkedObjectsRemove(objectName))
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSelected() {
|
||||||
|
const {dispatch, web} = this.props
|
||||||
|
let req = {
|
||||||
|
bucketName: this.props.currentBucket,
|
||||||
|
objects: this.props.checkedObjects,
|
||||||
|
prefix: this.props.currentPath
|
||||||
|
}
|
||||||
|
if (!web.LoggedIn()) {
|
||||||
|
let requestUrl = location.origin + "/minio/zip?token=''"
|
||||||
|
this.xhr = new XMLHttpRequest()
|
||||||
|
dispatch(actions.downloadSelected(requestUrl, req, this.xhr))
|
||||||
|
} else {
|
||||||
|
web.CreateURLToken()
|
||||||
|
.then(res => {
|
||||||
|
let requestUrl = location.origin + minioBrowserPrefix + "/zip?token=" + res.token
|
||||||
|
this.xhr = new XMLHttpRequest()
|
||||||
|
dispatch(actions.downloadSelected(requestUrl, req, this.xhr))
|
||||||
|
})
|
||||||
|
.catch(err => dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: err.message
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSelected() {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.checkedObjectsReset())
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {total, free} = this.props.storageInfo
|
||||||
|
const {showMakeBucketModal, alert, sortNameOrder, sortSizeOrder, sortDateOrder, showAbout, showBucketPolicy, checkedObjects} = this.props
|
||||||
|
const {version, memory, platform, runtime} = this.props.serverInfo
|
||||||
|
const {sidebarStatus} = this.props
|
||||||
|
const {showSettings} = this.props
|
||||||
|
const {policies, currentBucket, currentPath} = this.props
|
||||||
|
const {deleteConfirmation} = this.props
|
||||||
|
const {shareObject} = this.props
|
||||||
|
const {web, prefixWritable, istruncated} = this.props
|
||||||
|
|
||||||
|
// Don't always show the SettingsModal. This is done here instead of in
|
||||||
|
// SettingsModal.js so as to allow for #componentWillMount to handle
|
||||||
|
// the loading of the settings.
|
||||||
|
let settingsModal = showSettings ? <SettingsModal /> : <noscript></noscript>
|
||||||
|
|
||||||
|
let alertBox = <Alert className={ classNames({
|
||||||
|
'alert': true,
|
||||||
|
'animated': true,
|
||||||
|
'fadeInDown': alert.show,
|
||||||
|
'fadeOutUp': !alert.show
|
||||||
|
}) } bsStyle={ alert.type } onDismiss={ this.hideAlert.bind(this) }>
|
||||||
|
<div className='text-center'>
|
||||||
|
{ alert.message }
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
// Make sure you don't show a fading out alert box on the initial web-page load.
|
||||||
|
if (!alert.message)
|
||||||
|
alertBox = ''
|
||||||
|
|
||||||
|
let signoutTooltip = <Tooltip id="tt-sign-out">
|
||||||
|
Sign out
|
||||||
|
</Tooltip>
|
||||||
|
let uploadTooltip = <Tooltip id="tt-upload-file">
|
||||||
|
Upload file
|
||||||
|
</Tooltip>
|
||||||
|
let makeBucketTooltip = <Tooltip id="tt-create-bucket">
|
||||||
|
Create bucket
|
||||||
|
</Tooltip>
|
||||||
|
let loginButton = ''
|
||||||
|
let browserDropdownButton = ''
|
||||||
|
let storageUsageDetails = ''
|
||||||
|
|
||||||
|
let used = total - free
|
||||||
|
let usedPercent = (used / total) * 100 + '%'
|
||||||
|
let freePercent = free * 100 / total
|
||||||
|
|
||||||
|
if (web.LoggedIn()) {
|
||||||
|
browserDropdownButton = <BrowserDropdown fullScreenFunc={ this.fullScreen.bind(this) }
|
||||||
|
aboutFunc={ this.showAbout.bind(this) }
|
||||||
|
settingsFunc={ this.showSettings.bind(this) }
|
||||||
|
logoutFunc={ this.logout.bind(this) } />
|
||||||
|
} else {
|
||||||
|
loginButton = <a className='btn btn-danger' href={minioBrowserPrefix+'/login'}>Login</a>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (web.LoggedIn()) {
|
||||||
|
if (!(used === 0 && free === 0)) {
|
||||||
|
storageUsageDetails = <div className="feh-usage">
|
||||||
|
<div className="fehu-chart">
|
||||||
|
<div style={ { width: usedPercent } }></div>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span>Used: </span>
|
||||||
|
{ humanize.filesize(total - free) }
|
||||||
|
</li>
|
||||||
|
<li className="pull-right">
|
||||||
|
<span>Free: </span>
|
||||||
|
{ humanize.filesize(total - used) }
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let createButton = ''
|
||||||
|
if (web.LoggedIn()) {
|
||||||
|
createButton = <Dropdown dropup className="feb-actions" id="fe-action-toggle">
|
||||||
|
<Dropdown.Toggle noCaret className="feba-toggle">
|
||||||
|
<span><i className="fa fa-plus"></i></span>
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<OverlayTrigger placement="left" overlay={ uploadTooltip }>
|
||||||
|
<a href="#" className="feba-btn feba-upload">
|
||||||
|
<input type="file"
|
||||||
|
onChange={ this.uploadFile.bind(this) }
|
||||||
|
style={ { display: 'none' } }
|
||||||
|
id="file-input"></input>
|
||||||
|
<label htmlFor="file-input"> <i className="fa fa-cloud-upload"></i> </label>
|
||||||
|
</a>
|
||||||
|
</OverlayTrigger>
|
||||||
|
<OverlayTrigger placement="left" overlay={ makeBucketTooltip }>
|
||||||
|
<a href="#" className="feba-btn feba-bucket" onClick={ this.showMakeBucketModal.bind(this) }><i className="fa fa-hdd-o"></i></a>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (prefixWritable)
|
||||||
|
createButton = <Dropdown dropup className="feb-actions" id="fe-action-toggle">
|
||||||
|
<Dropdown.Toggle noCaret className="feba-toggle">
|
||||||
|
<span><i className="fa fa-plus"></i></span>
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<OverlayTrigger placement="left" overlay={ uploadTooltip }>
|
||||||
|
<a href="#" className="feba-btn feba-upload">
|
||||||
|
<input type="file"
|
||||||
|
onChange={ this.uploadFile.bind(this) }
|
||||||
|
style={ { display: 'none' } }
|
||||||
|
id="file-input"></input>
|
||||||
|
<label htmlFor="file-input"> <i className="fa fa-cloud-upload"></i> </label>
|
||||||
|
</a>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={ classNames({
|
||||||
|
'file-explorer': true,
|
||||||
|
'toggled': sidebarStatus
|
||||||
|
}) }>
|
||||||
|
<SideBar searchBuckets={ this.searchBuckets.bind(this) }
|
||||||
|
selectBucket={ this.selectBucket.bind(this) }
|
||||||
|
clickOutside={ this.hideSidebar.bind(this) }
|
||||||
|
showPolicy={ this.showBucketPolicy.bind(this) } />
|
||||||
|
<div className="fe-body">
|
||||||
|
<div className={ 'list-actions' + (classNames({
|
||||||
|
' list-actions-toggled': checkedObjects.length > 0
|
||||||
|
})) }>
|
||||||
|
<span className="la-label"><i className="fa fa-check-circle" /> { checkedObjects.length } Objects selected</span>
|
||||||
|
<span className="la-actions pull-right"><button onClick={ this.downloadSelected.bind(this) }> Download all as zip </button></span>
|
||||||
|
<span className="la-actions pull-right"><button onClick={ this.showDeleteConfirmation.bind(this) }> Delete selected </button></span>
|
||||||
|
<i className="la-close fa fa-times" onClick={ this.clearSelected.bind(this) }></i>
|
||||||
|
</div>
|
||||||
|
<Dropzone>
|
||||||
|
{ alertBox }
|
||||||
|
<header className="fe-header-mobile hidden-lg hidden-md">
|
||||||
|
<div id="feh-trigger" className={ 'feh-trigger ' + (classNames({
|
||||||
|
'feht-toggled': sidebarStatus
|
||||||
|
})) } onClick={ this.toggleSidebar.bind(this, !sidebarStatus) }>
|
||||||
|
<div className="feht-lines">
|
||||||
|
<div className="top"></div>
|
||||||
|
<div className="center"></div>
|
||||||
|
<div className="bottom"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img className="mh-logo" src={ logo } alt="" />
|
||||||
|
</header>
|
||||||
|
<header className="fe-header">
|
||||||
|
<Path selectPrefix={ this.selectPrefix.bind(this) } />
|
||||||
|
{ storageUsageDetails }
|
||||||
|
<ul className="feh-actions">
|
||||||
|
<BrowserUpdate />
|
||||||
|
{ loginButton }
|
||||||
|
{ browserDropdownButton }
|
||||||
|
</ul>
|
||||||
|
</header>
|
||||||
|
<div className="feb-container">
|
||||||
|
<header className="fesl-row" data-type="folder">
|
||||||
|
<div className="fesl-item fesl-item-icon"></div>
|
||||||
|
<div className="fesl-item fesl-item-name" onClick={ this.sortObjectsByName.bind(this) } data-sort="name">
|
||||||
|
Name
|
||||||
|
<i className={ classNames({
|
||||||
|
'fesli-sort': true,
|
||||||
|
'fa': true,
|
||||||
|
'fa-sort-alpha-desc': sortNameOrder,
|
||||||
|
'fa-sort-alpha-asc': !sortNameOrder
|
||||||
|
}) } />
|
||||||
|
</div>
|
||||||
|
<div className="fesl-item fesl-item-size" onClick={ this.sortObjectsBySize.bind(this) } data-sort="size">
|
||||||
|
Size
|
||||||
|
<i className={ classNames({
|
||||||
|
'fesli-sort': true,
|
||||||
|
'fa': true,
|
||||||
|
'fa-sort-amount-desc': sortSizeOrder,
|
||||||
|
'fa-sort-amount-asc': !sortSizeOrder
|
||||||
|
}) } />
|
||||||
|
</div>
|
||||||
|
<div className="fesl-item fesl-item-modified" onClick={ this.sortObjectsByDate.bind(this) } data-sort="last-modified">
|
||||||
|
Last Modified
|
||||||
|
<i className={ classNames({
|
||||||
|
'fesli-sort': true,
|
||||||
|
'fa': true,
|
||||||
|
'fa-sort-numeric-desc': sortDateOrder,
|
||||||
|
'fa-sort-numeric-asc': !sortDateOrder
|
||||||
|
}) } />
|
||||||
|
</div>
|
||||||
|
<div className="fesl-item fesl-item-actions"></div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
<div className="feb-container">
|
||||||
|
<InfiniteScroll loadMore={ this.listObjects.bind(this) }
|
||||||
|
hasMore={ istruncated }
|
||||||
|
useWindow={ true }
|
||||||
|
initialLoad={ false }>
|
||||||
|
<ObjectsList dataType={ this.dataType.bind(this) }
|
||||||
|
selectPrefix={ this.selectPrefix.bind(this) }
|
||||||
|
showDeleteConfirmation={ this.showDeleteConfirmation.bind(this) }
|
||||||
|
shareObject={ this.shareObject.bind(this) }
|
||||||
|
checkObject={ this.checkObject.bind(this) }
|
||||||
|
checkedObjectsArray={ checkedObjects } />
|
||||||
|
</InfiniteScroll>
|
||||||
|
<div className="text-center" style={ { display: (istruncated && currentBucket) ? 'block' : 'none' } }>
|
||||||
|
<span>Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UploadModal />
|
||||||
|
{ createButton }
|
||||||
|
<Modal className="modal-create-bucket"
|
||||||
|
bsSize="small"
|
||||||
|
animation={ false }
|
||||||
|
show={ showMakeBucketModal }
|
||||||
|
onHide={ this.hideMakeBucketModal.bind(this) }>
|
||||||
|
<button className="close close-alt" onClick={ this.hideMakeBucketModal.bind(this) }>
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
<ModalBody>
|
||||||
|
<form onSubmit={ this.makeBucket.bind(this) }>
|
||||||
|
<div className="input-group">
|
||||||
|
<input className="ig-text"
|
||||||
|
type="text"
|
||||||
|
ref="makeBucketRef"
|
||||||
|
placeholder="Bucket Name"
|
||||||
|
autoFocus/>
|
||||||
|
<i className="ig-helpers"></i>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
<Modal className="modal-about modal-dark"
|
||||||
|
animation={ false }
|
||||||
|
show={ showAbout }
|
||||||
|
onHide={ this.hideAbout.bind(this) }>
|
||||||
|
<button className="close" onClick={ this.hideAbout.bind(this) }>
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
<div className="ma-inner">
|
||||||
|
<div className="mai-item hidden-xs">
|
||||||
|
<a href="https://minio.io" target="_blank"><img className="maii-logo" src={ logo } alt="" /></a>
|
||||||
|
</div>
|
||||||
|
<div className="mai-item">
|
||||||
|
<ul className="maii-list">
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Version
|
||||||
|
</div>
|
||||||
|
<small>{ version }</small>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Memory
|
||||||
|
</div>
|
||||||
|
<small>{ memory }</small>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Platform
|
||||||
|
</div>
|
||||||
|
<small>{ platform }</small>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
Runtime
|
||||||
|
</div>
|
||||||
|
<small>{ runtime }</small>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<Modal className="modal-policy"
|
||||||
|
animation={ false }
|
||||||
|
show={ showBucketPolicy }
|
||||||
|
onHide={ this.hideBucketPolicy.bind(this) }>
|
||||||
|
<ModalHeader>
|
||||||
|
Bucket Policy (
|
||||||
|
{ currentBucket })
|
||||||
|
<button className="close close-alt" onClick={ this.hideBucketPolicy.bind(this) }>
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
</ModalHeader>
|
||||||
|
<div className="pm-body">
|
||||||
|
<PolicyInput bucket={ currentBucket } />
|
||||||
|
{ policies.map((policy, i) => <Policy key={ i } prefix={ policy.prefix } policy={ policy.policy } />
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
<ConfirmModal show={ deleteConfirmation.show }
|
||||||
|
icon='fa fa-exclamation-triangle mci-red'
|
||||||
|
text='Are you sure you want to delete?'
|
||||||
|
sub='This cannot be undone!'
|
||||||
|
okText='Delete'
|
||||||
|
cancelText='Cancel'
|
||||||
|
okHandler={ this.removeObject.bind(this) }
|
||||||
|
cancelHandler={ this.hideDeleteConfirmation.bind(this) }>
|
||||||
|
</ConfirmModal>
|
||||||
|
<Modal show={ shareObject.show }
|
||||||
|
animation={ false }
|
||||||
|
onHide={ this.hideShareObjectModal.bind(this) }
|
||||||
|
bsSize="small">
|
||||||
|
<ModalHeader>
|
||||||
|
Share Object
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<div className="input-group copy-text">
|
||||||
|
<label>
|
||||||
|
Shareable Link
|
||||||
|
</label>
|
||||||
|
<input type="text"
|
||||||
|
ref="copyTextInput"
|
||||||
|
readOnly="readOnly"
|
||||||
|
value={ window.location.protocol + '//' + shareObject.url }
|
||||||
|
onClick={ this.selectTexts.bind(this) } />
|
||||||
|
</div>
|
||||||
|
<div className="input-group" style={ { display: web.LoggedIn() ? 'block' : 'none' } }>
|
||||||
|
<label>
|
||||||
|
Expires in (Max 7 days)
|
||||||
|
</label>
|
||||||
|
<div className="set-expire">
|
||||||
|
<div className="set-expire-item">
|
||||||
|
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireDays', 1, shareObject.object) } />
|
||||||
|
<div className="set-expire-title">
|
||||||
|
Days
|
||||||
|
</div>
|
||||||
|
<div className="set-expire-value">
|
||||||
|
<input ref="expireDays"
|
||||||
|
type="number"
|
||||||
|
min={ 0 }
|
||||||
|
max={ 7 }
|
||||||
|
defaultValue={ 5 }
|
||||||
|
readOnly="readOnly"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireDays', -1, shareObject.object) } />
|
||||||
|
</div>
|
||||||
|
<div className="set-expire-item">
|
||||||
|
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireHours', 1, shareObject.object) } />
|
||||||
|
<div className="set-expire-title">
|
||||||
|
Hours
|
||||||
|
</div>
|
||||||
|
<div className="set-expire-value">
|
||||||
|
<input ref="expireHours"
|
||||||
|
type="number"
|
||||||
|
min={ 0 }
|
||||||
|
max={ 23 }
|
||||||
|
defaultValue={ 0 }
|
||||||
|
readOnly="readOnly"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireHours', -1, shareObject.object) } />
|
||||||
|
</div>
|
||||||
|
<div className="set-expire-item">
|
||||||
|
<i className="set-expire-increase" onClick={ this.handleExpireValue.bind(this, 'expireMins', 1, shareObject.object) } />
|
||||||
|
<div className="set-expire-title">
|
||||||
|
Minutes
|
||||||
|
</div>
|
||||||
|
<div className="set-expire-value">
|
||||||
|
<input ref="expireMins"
|
||||||
|
type="number"
|
||||||
|
min={ 0 }
|
||||||
|
max={ 59 }
|
||||||
|
defaultValue={ 0 }
|
||||||
|
readOnly="readOnly"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<i className="set-expire-decrease" onClick={ this.handleExpireValue.bind(this, 'expireMins', -1, shareObject.object) } />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<CopyToClipboard text={ window.location.protocol + '//' + shareObject.url } onCopy={ this.showMessage.bind(this) }>
|
||||||
|
<button className="btn btn-success">
|
||||||
|
Copy Link
|
||||||
|
</button>
|
||||||
|
</CopyToClipboard>
|
||||||
|
<button className="btn btn-link" onClick={ this.hideShareObjectModal.bind(this) }>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
{ settingsModal }
|
||||||
|
</Dropzone>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016, 2017 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import connect from 'react-redux/lib/components/connect'
|
||||||
|
import Dropdown from 'react-bootstrap/lib/Dropdown'
|
||||||
|
|
||||||
|
let BrowserDropdown = ({fullScreenFunc, aboutFunc, settingsFunc, logoutFunc}) => {
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<Dropdown pullRight id="top-right-menu">
|
||||||
|
<Dropdown.Toggle noCaret>
|
||||||
|
<i className="fa fa-reorder"></i>
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu className="dropdown-menu-right">
|
||||||
|
<li>
|
||||||
|
<a target="_blank" href="https://github.com/minio/minio">Github <i className="fa fa-github"></i></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="" onClick={ fullScreenFunc }>Fullscreen <i className="fa fa-expand"></i></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a target="_blank" href="https://docs.minio.io/">Documentation <i className="fa fa-book"></i></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a target="_blank" href="https://slack.minio.io">Ask for help <i className="fa fa-question-circle"></i></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="" onClick={ aboutFunc }>About <i className="fa fa-info-circle"></i></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="" onClick={ settingsFunc }>Settings <i className="fa fa-cog"></i></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="" onClick={ logoutFunc }>Sign Out <i className="fa fa-sign-out"></i></a>
|
||||||
|
</li>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(state => state)(BrowserDropdown)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import connect from 'react-redux/lib/components/connect'
|
||||||
|
|
||||||
|
import Tooltip from 'react-bootstrap/lib/Tooltip'
|
||||||
|
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'
|
||||||
|
|
||||||
|
let BrowserUpdate = ({latestUiVersion}) => {
|
||||||
|
// Don't show an update if we're already updated!
|
||||||
|
if (latestUiVersion === currentUiVersion) return ( <noscript></noscript> )
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="hidden-xs hidden-sm">
|
||||||
|
<a href="">
|
||||||
|
<OverlayTrigger placement="left" overlay={ <Tooltip id="tt-version-update">
|
||||||
|
New update available. Click to refresh.
|
||||||
|
</Tooltip> }> <i className="fa fa-refresh"></i> </OverlayTrigger>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(state => {
|
||||||
|
return {
|
||||||
|
latestUiVersion: state.latestUiVersion
|
||||||
|
}
|
||||||
|
})(BrowserUpdate)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import Modal from 'react-bootstrap/lib/Modal'
|
||||||
|
import ModalBody from 'react-bootstrap/lib/ModalBody'
|
||||||
|
|
||||||
|
let ConfirmModal = ({baseClass, icon, text, sub, okText, cancelText, okHandler, cancelHandler, show}) => {
|
||||||
|
return (
|
||||||
|
<Modal bsSize="small"
|
||||||
|
animation={ false }
|
||||||
|
show={ show }
|
||||||
|
className={ "modal-confirm " + (baseClass || '') }>
|
||||||
|
<ModalBody>
|
||||||
|
<div className="mc-icon">
|
||||||
|
<i className={ icon }></i>
|
||||||
|
</div>
|
||||||
|
<div className="mc-text">
|
||||||
|
{ text }
|
||||||
|
</div>
|
||||||
|
<div className="mc-sub">
|
||||||
|
{ sub }
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button className="btn btn-danger" onClick={ okHandler }>
|
||||||
|
{ okText }
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-link" onClick={ cancelHandler }>
|
||||||
|
{ cancelText }
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConfirmModal
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDropzone from 'react-dropzone'
|
||||||
|
import * as actions from '../actions'
|
||||||
|
|
||||||
|
// Dropzone is a drag-and-drop element for uploading files. It will create a
|
||||||
|
// landing zone of sorts that automatically receives the files.
|
||||||
|
export default class Dropzone extends React.Component {
|
||||||
|
|
||||||
|
onDrop(files) {
|
||||||
|
// FIXME: Currently you can upload multiple files, but only one abort
|
||||||
|
// modal will be shown, and progress updates will only occur for one
|
||||||
|
// file at a time. See #171.
|
||||||
|
files.forEach(file => {
|
||||||
|
let req = new XMLHttpRequest()
|
||||||
|
|
||||||
|
// Dispatch the upload.
|
||||||
|
web.dispatch(actions.uploadFile(file, req))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// Overwrite the default styling from react-dropzone; otherwise it
|
||||||
|
// won't handle child elements correctly.
|
||||||
|
const style = {
|
||||||
|
height: '100%',
|
||||||
|
borderWidth: '0',
|
||||||
|
borderStyle: 'dashed',
|
||||||
|
borderColor: '#fff'
|
||||||
|
}
|
||||||
|
const activeStyle = {
|
||||||
|
borderWidth: '2px',
|
||||||
|
borderColor: '#777'
|
||||||
|
}
|
||||||
|
const rejectStyle = {
|
||||||
|
backgroundColor: '#ffdddd'
|
||||||
|
}
|
||||||
|
|
||||||
|
// disableClick means that it won't trigger a file upload box when
|
||||||
|
// the user clicks on a file.
|
||||||
|
return (
|
||||||
|
<ReactDropzone style={ style }
|
||||||
|
activeStyle={ activeStyle }
|
||||||
|
rejectStyle={ rejectStyle }
|
||||||
|
disableClick={ true }
|
||||||
|
onDrop={ this.onDrop }>
|
||||||
|
{ this.props.children }
|
||||||
|
</ReactDropzone>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
let InputGroup = ({label, id, name, value, onChange, type, spellCheck, required, readonly, autoComplete, align, className}) => {
|
||||||
|
var input = <input id={ id }
|
||||||
|
name={ name }
|
||||||
|
value={ value }
|
||||||
|
onChange={ onChange }
|
||||||
|
className="ig-text"
|
||||||
|
type={ type }
|
||||||
|
spellCheck={ spellCheck }
|
||||||
|
required={ required }
|
||||||
|
autoComplete={ autoComplete } />
|
||||||
|
if (readonly)
|
||||||
|
input = <input id={ id }
|
||||||
|
name={ name }
|
||||||
|
value={ value }
|
||||||
|
onChange={ onChange }
|
||||||
|
className="ig-text"
|
||||||
|
type={ type }
|
||||||
|
spellCheck={ spellCheck }
|
||||||
|
required={ required }
|
||||||
|
autoComplete={ autoComplete }
|
||||||
|
disabled />
|
||||||
|
return <div className={ "input-group " + align + ' ' + className }>
|
||||||
|
{ input }
|
||||||
|
<i className="ig-helpers"></i>
|
||||||
|
<label className="ig-label">
|
||||||
|
{ label }
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InputGroup
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import logo from '../../img/logo.svg'
|
||||||
|
import Alert from 'react-bootstrap/lib/Alert'
|
||||||
|
import * as actions from '../actions'
|
||||||
|
import InputGroup from '../components/InputGroup'
|
||||||
|
|
||||||
|
export default class Login extends React.Component {
|
||||||
|
handleSubmit(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
const {web, dispatch, loginRedirectPath} = this.props
|
||||||
|
let message = ''
|
||||||
|
if (!document.getElementById('accessKey').value) {
|
||||||
|
message = 'Secret Key cannot be empty'
|
||||||
|
}
|
||||||
|
if (!document.getElementById('secretKey').value) {
|
||||||
|
message = 'Access Key cannot be empty'
|
||||||
|
}
|
||||||
|
if (message) {
|
||||||
|
dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
web.Login({
|
||||||
|
username: document.getElementById('accessKey').value,
|
||||||
|
password: document.getElementById('secretKey').value
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
this.context.router.push(loginRedirectPath)
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
dispatch(actions.setLoginError())
|
||||||
|
dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: e.message
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
// Clear out any stale message in the alert of previous page
|
||||||
|
dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: ''
|
||||||
|
}))
|
||||||
|
document.body.classList.add('is-guest')
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.body.classList.remove('is-guest')
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAlert() {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.hideAlert())
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {alert} = this.props
|
||||||
|
let alertBox = <Alert className={ 'alert animated ' + (alert.show ? 'fadeInDown' : 'fadeOutUp') } bsStyle={ alert.type } onDismiss={ this.hideAlert.bind(this) }>
|
||||||
|
<div className='text-center'>
|
||||||
|
{ alert.message }
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
// Make sure you don't show a fading out alert box on the initial web-page load.
|
||||||
|
if (!alert.message)
|
||||||
|
alertBox = ''
|
||||||
|
return (
|
||||||
|
<div className="login">
|
||||||
|
{ alertBox }
|
||||||
|
<div className="l-wrap">
|
||||||
|
<form onSubmit={ this.handleSubmit.bind(this) }>
|
||||||
|
<InputGroup className="ig-dark"
|
||||||
|
label="Access Key"
|
||||||
|
id="accessKey"
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
spellCheck="false"
|
||||||
|
required="required"
|
||||||
|
autoComplete="username">
|
||||||
|
</InputGroup>
|
||||||
|
<InputGroup className="ig-dark"
|
||||||
|
label="Secret Key"
|
||||||
|
id="secretKey"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
spellCheck="false"
|
||||||
|
required="required"
|
||||||
|
autoComplete="new-password">
|
||||||
|
</InputGroup>
|
||||||
|
<button className="lw-btn" type="submit">
|
||||||
|
<i className="fa fa-sign-in"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div className="l-footer">
|
||||||
|
<a className="lf-logo" href=""><img src={ logo } alt="" /></a>
|
||||||
|
<div className="lf-server">
|
||||||
|
{ window.location.host }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Login.contextTypes = {
|
||||||
|
router: React.PropTypes.object.isRequired
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import Moment from 'moment'
|
||||||
|
import humanize from 'humanize'
|
||||||
|
import connect from 'react-redux/lib/components/connect'
|
||||||
|
import Dropdown from 'react-bootstrap/lib/Dropdown'
|
||||||
|
|
||||||
|
let ObjectsList = ({objects, currentPath, selectPrefix, dataType, showDeleteConfirmation, shareObject, loadPath, checkObject, checkedObjectsArray}) => {
|
||||||
|
const list = objects.map((object, i) => {
|
||||||
|
let size = object.name.endsWith('/') ? '-' : humanize.filesize(object.size)
|
||||||
|
let lastModified = object.name.endsWith('/') ? '-' : Moment(object.lastModified).format('lll')
|
||||||
|
let loadingClass = loadPath === `${currentPath}${object.name}` ? 'fesl-loading' : ''
|
||||||
|
let actionButtons = ''
|
||||||
|
let deleteButton = ''
|
||||||
|
if (web.LoggedIn())
|
||||||
|
deleteButton = <a href="" className="fiad-action" onClick={ (e) => showDeleteConfirmation(e, `${currentPath}${object.name}`) }><i className="fa fa-trash"></i></a>
|
||||||
|
|
||||||
|
if (!checkedObjectsArray.length > 0) {
|
||||||
|
if (!object.name.endsWith('/')) {
|
||||||
|
actionButtons = <Dropdown id={ "fia-dropdown-" + object.name.replace('.', '-') }>
|
||||||
|
<Dropdown.Toggle noCaret className="fia-toggle"></Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
<a href="" className="fiad-action" onClick={ (e) => shareObject(e, `${currentPath}${object.name}`) }><i className="fa fa-copy"></i></a>
|
||||||
|
{ deleteButton }
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeClass = ''
|
||||||
|
let isChecked = ''
|
||||||
|
|
||||||
|
if (checkedObjectsArray.indexOf(object.name) > -1) {
|
||||||
|
activeClass = ' fesl-row-selected'
|
||||||
|
isChecked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={ i } className={ "fesl-row " + loadingClass + activeClass } data-type={ dataType(object.name, object.contentType) }>
|
||||||
|
<div className="fesl-item fesl-item-icon">
|
||||||
|
<div className="fi-select">
|
||||||
|
<input type="checkbox"
|
||||||
|
name={ object.name }
|
||||||
|
checked={ isChecked }
|
||||||
|
onChange={ (e) => checkObject(e, object.name) } />
|
||||||
|
<i className="fis-icon"></i>
|
||||||
|
<i className="fis-helper"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="fesl-item fesl-item-name">
|
||||||
|
<a href="" onClick={ (e) => selectPrefix(e, `${currentPath}${object.name}`) }>
|
||||||
|
{ object.name }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="fesl-item fesl-item-size">
|
||||||
|
{ size }
|
||||||
|
</div>
|
||||||
|
<div className="fesl-item fesl-item-modified">
|
||||||
|
{ lastModified }
|
||||||
|
</div>
|
||||||
|
<div className="fesl-item fesl-item-actions">
|
||||||
|
{ actionButtons }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{ list }
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe it to state changes.
|
||||||
|
export default connect(state => {
|
||||||
|
return {
|
||||||
|
objects: state.objects,
|
||||||
|
currentPath: state.currentPath,
|
||||||
|
loadPath: state.loadPath
|
||||||
|
}
|
||||||
|
})(ObjectsList)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import connect from 'react-redux/lib/components/connect'
|
||||||
|
|
||||||
|
let Path = ({currentBucket, currentPath, selectPrefix}) => {
|
||||||
|
let dirPath = []
|
||||||
|
let path = ''
|
||||||
|
if (currentPath) {
|
||||||
|
path = currentPath.split('/').map((dir, i) => {
|
||||||
|
dirPath.push(dir)
|
||||||
|
let dirPath_ = dirPath.join('/') + '/'
|
||||||
|
return <span key={ i }><a href="" onClick={ (e) => selectPrefix(e, dirPath_) }>{ dir }</a></span>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h2><span className="main"><a onClick={ (e) => selectPrefix(e, '') } href="">{ currentBucket }</a></span>{ path }</h2>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(state => {
|
||||||
|
return {
|
||||||
|
currentBucket: state.currentBucket,
|
||||||
|
currentPath: state.currentPath
|
||||||
|
}
|
||||||
|
})(Path)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { READ_ONLY, WRITE_ONLY, READ_WRITE } from '../constants'
|
||||||
|
|
||||||
|
import React, { Component, PropTypes } from 'react'
|
||||||
|
import connect from 'react-redux/lib/components/connect'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import * as actions from '../actions'
|
||||||
|
|
||||||
|
class Policy extends Component {
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context)
|
||||||
|
this.state = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePolicyChange(e) {
|
||||||
|
this.setState({
|
||||||
|
policy: {
|
||||||
|
policy: e.target.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
removePolicy(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch, currentBucket, prefix} = this.props
|
||||||
|
let newPrefix = prefix.replace(currentBucket + '/', '')
|
||||||
|
newPrefix = newPrefix.replace('*', '')
|
||||||
|
web.SetBucketPolicy({
|
||||||
|
bucketName: currentBucket,
|
||||||
|
prefix: newPrefix,
|
||||||
|
policy: 'none'
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
dispatch(actions.setPolicies(this.props.policies.filter(policy => policy.prefix != prefix)))
|
||||||
|
})
|
||||||
|
.catch(e => dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: e.message,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {policy, prefix, currentBucket} = this.props
|
||||||
|
let newPrefix = prefix.replace(currentBucket + '/', '')
|
||||||
|
newPrefix = newPrefix.replace('*', '')
|
||||||
|
|
||||||
|
if (!newPrefix)
|
||||||
|
newPrefix = '*'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pmb-list">
|
||||||
|
<div className="pmbl-item">
|
||||||
|
{ newPrefix }
|
||||||
|
</div>
|
||||||
|
<div className="pmbl-item">
|
||||||
|
<select className="form-control"
|
||||||
|
disabled
|
||||||
|
value={ policy }
|
||||||
|
onChange={ this.handlePolicyChange.bind(this) }>
|
||||||
|
<option value={ READ_ONLY }>
|
||||||
|
Read Only
|
||||||
|
</option>
|
||||||
|
<option value={ WRITE_ONLY }>
|
||||||
|
Write Only
|
||||||
|
</option>
|
||||||
|
<option value={ READ_WRITE }>
|
||||||
|
Read and Write
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="pmbl-item">
|
||||||
|
<button className="btn btn-block btn-danger" onClick={ this.removePolicy.bind(this) }>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(state => state)(Policy)
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { READ_ONLY, WRITE_ONLY, READ_WRITE } from '../constants'
|
||||||
|
import React, { Component, PropTypes } from 'react'
|
||||||
|
import connect from 'react-redux/lib/components/connect'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import * as actions from '../actions'
|
||||||
|
|
||||||
|
class PolicyInput extends Component {
|
||||||
|
componentDidMount() {
|
||||||
|
const {web, dispatch} = this.props
|
||||||
|
this.prefix.focus()
|
||||||
|
web.ListAllBucketPolicies({
|
||||||
|
bucketName: this.props.currentBucket
|
||||||
|
}).then(res => {
|
||||||
|
let policies = res.policies
|
||||||
|
if (policies) dispatch(actions.setPolicies(policies))
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: err.message
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.setPolicies([]))
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePolicySubmit(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {web, dispatch, currentBucket} = this.props
|
||||||
|
|
||||||
|
let prefix = currentBucket + '/' + this.prefix.value
|
||||||
|
let policy = this.policy.value
|
||||||
|
|
||||||
|
if (!prefix.endsWith('*')) prefix = prefix + '*'
|
||||||
|
|
||||||
|
let prefixAlreadyExists = this.props.policies.some(elem => prefix === elem.prefix)
|
||||||
|
|
||||||
|
if (prefixAlreadyExists) {
|
||||||
|
dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: "Policy for this prefix already exists."
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
web.SetBucketPolicy({
|
||||||
|
bucketName: this.props.currentBucket,
|
||||||
|
prefix: this.prefix.value,
|
||||||
|
policy: this.policy.value
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
dispatch(actions.setPolicies([{
|
||||||
|
policy, prefix
|
||||||
|
}, ...this.props.policies]))
|
||||||
|
this.prefix.value = ''
|
||||||
|
})
|
||||||
|
.catch(e => dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: e.message,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<header className="pmb-list">
|
||||||
|
<div className="pmbl-item">
|
||||||
|
<input type="text"
|
||||||
|
ref={ prefix => this.prefix = prefix }
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Prefix"
|
||||||
|
editable={ true } />
|
||||||
|
</div>
|
||||||
|
<div className="pmbl-item">
|
||||||
|
<select ref={ policy => this.policy = policy } className="form-control">
|
||||||
|
<option value={ READ_ONLY }>
|
||||||
|
Read Only
|
||||||
|
</option>
|
||||||
|
<option value={ WRITE_ONLY }>
|
||||||
|
Write Only
|
||||||
|
</option>
|
||||||
|
<option value={ READ_WRITE }>
|
||||||
|
Read and Write
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="pmbl-item">
|
||||||
|
<button className="btn btn-block btn-primary" onClick={ this.handlePolicySubmit.bind(this) }>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(state => state)(PolicyInput)
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import connect from 'react-redux/lib/components/connect'
|
||||||
|
import * as actions from '../actions'
|
||||||
|
|
||||||
|
import Tooltip from 'react-bootstrap/lib/Tooltip'
|
||||||
|
import Modal from 'react-bootstrap/lib/Modal'
|
||||||
|
import ModalBody from 'react-bootstrap/lib/ModalBody'
|
||||||
|
import ModalHeader from 'react-bootstrap/lib/ModalHeader'
|
||||||
|
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'
|
||||||
|
import InputGroup from './InputGroup'
|
||||||
|
|
||||||
|
class SettingsModal extends React.Component {
|
||||||
|
|
||||||
|
// When the settings are shown, it loads the access key and secret key.
|
||||||
|
componentWillMount() {
|
||||||
|
const {web, dispatch} = this.props
|
||||||
|
const {serverInfo} = this.props
|
||||||
|
|
||||||
|
let accessKeyEnv = ''
|
||||||
|
let secretKeyEnv = ''
|
||||||
|
// Check environment variables first.
|
||||||
|
if (serverInfo.info.isEnvCreds) {
|
||||||
|
dispatch(actions.setSettings({
|
||||||
|
accessKey: 'xxxxxxxxx',
|
||||||
|
secretKey: 'xxxxxxxxx',
|
||||||
|
keysReadOnly: true
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
web.GetAuth()
|
||||||
|
.then(data => {
|
||||||
|
dispatch(actions.setSettings({
|
||||||
|
accessKey: data.accessKey,
|
||||||
|
secretKey: data.secretKey
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When they are re-hidden, the keys are unloaded from memory.
|
||||||
|
componentWillUnmount() {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
|
||||||
|
dispatch(actions.setSettings({
|
||||||
|
accessKey: '',
|
||||||
|
secretKey: '',
|
||||||
|
secretKeyVisible: false
|
||||||
|
}))
|
||||||
|
dispatch(actions.hideSettings())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle field changes from inside the modal.
|
||||||
|
accessKeyChange(e) {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.setSettings({
|
||||||
|
accessKey: e.target.value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
secretKeyChange(e) {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.setSettings({
|
||||||
|
secretKey: e.target.value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
secretKeyVisible(secretKeyVisible) {
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.setSettings({
|
||||||
|
secretKeyVisible
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the auth params and set them.
|
||||||
|
setAuth(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {web, dispatch} = this.props
|
||||||
|
|
||||||
|
let accessKey = document.getElementById('accessKey').value
|
||||||
|
let secretKey = document.getElementById('secretKey').value
|
||||||
|
web.SetAuth({
|
||||||
|
accessKey,
|
||||||
|
secretKey
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
dispatch(actions.setSettings({
|
||||||
|
accessKey: '',
|
||||||
|
secretKey: '',
|
||||||
|
secretKeyVisible: false
|
||||||
|
}))
|
||||||
|
dispatch(actions.hideSettings())
|
||||||
|
dispatch(actions.showAlert({
|
||||||
|
type: 'success',
|
||||||
|
message: 'Changed credentials'
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
dispatch(actions.setSettings({
|
||||||
|
accessKey: '',
|
||||||
|
secretKey: '',
|
||||||
|
secretKeyVisible: false
|
||||||
|
}))
|
||||||
|
dispatch(actions.hideSettings())
|
||||||
|
dispatch(actions.showAlert({
|
||||||
|
type: 'danger',
|
||||||
|
message: err.message
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
generateAuth(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch} = this.props
|
||||||
|
|
||||||
|
web.GenerateAuth()
|
||||||
|
.then(data => {
|
||||||
|
dispatch(actions.setSettings({
|
||||||
|
secretKeyVisible: true
|
||||||
|
}))
|
||||||
|
dispatch(actions.setSettings({
|
||||||
|
accessKey: data.accessKey,
|
||||||
|
secretKey: data.secretKey
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSettings(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const {dispatch} = this.props
|
||||||
|
dispatch(actions.hideSettings())
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let {settings} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal bsSize="sm" animation={ false } show={ true }>
|
||||||
|
<ModalHeader>
|
||||||
|
Change Password
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody className="m-t-20">
|
||||||
|
<InputGroup value={ settings.accessKey }
|
||||||
|
onChange={ this.accessKeyChange.bind(this) }
|
||||||
|
id="accessKey"
|
||||||
|
label="Access Key"
|
||||||
|
name="accesskey"
|
||||||
|
type="text"
|
||||||
|
spellCheck="false"
|
||||||
|
required="required"
|
||||||
|
autoComplete="false"
|
||||||
|
align="ig-left"
|
||||||
|
readonly={ settings.keysReadOnly }></InputGroup>
|
||||||
|
<i onClick={ this.secretKeyVisible.bind(this, !settings.secretKeyVisible) } className={ "toggle-password fa fa-eye " + (settings.secretKeyVisible ? "toggled" : "") } />
|
||||||
|
<InputGroup value={ settings.secretKey }
|
||||||
|
onChange={ this.secretKeyChange.bind(this) }
|
||||||
|
id="secretKey"
|
||||||
|
label="Secret Key"
|
||||||
|
name="accesskey"
|
||||||
|
type={ settings.secretKeyVisible ? "text" : "password" }
|
||||||
|
spellCheck="false"
|
||||||
|
required="required"
|
||||||
|
autoComplete="false"
|
||||||
|
align="ig-left"
|
||||||
|
readonly={ settings.keysReadOnly }></InputGroup>
|
||||||
|
</ModalBody>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button className={ "btn btn-primary " + (settings.keysReadOnly ? "hidden" : "") } onClick={ this.generateAuth.bind(this) }>
|
||||||
|
Generate
|
||||||
|
</button>
|
||||||
|
<button href="" className={ "btn btn-success " + (settings.keysReadOnly ? "hidden" : "") } onClick={ this.setAuth.bind(this) }>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
<button href="" className="btn btn-link" onClick={ this.hideSettings.bind(this) }>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(state => {
|
||||||
|
return {
|
||||||
|
web: state.web,
|
||||||
|
settings: state.settings,
|
||||||
|
serverInfo: state.serverInfo
|
||||||
|
}
|
||||||
|
})(SettingsModal)
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import ClickOutHandler from 'react-onclickout'
|
||||||
|
import Scrollbars from 'react-custom-scrollbars/lib/Scrollbars'
|
||||||
|
import connect from 'react-redux/lib/components/connect'
|
||||||
|
|
||||||
|
import logo from '../../img/logo.svg'
|
||||||
|
|
||||||
|
let SideBar = ({visibleBuckets, loadBucket, currentBucket, selectBucket, searchBuckets, sidebarStatus, clickOutside, showPolicy}) => {
|
||||||
|
|
||||||
|
const list = visibleBuckets.map((bucket, i) => {
|
||||||
|
return <li className={ classNames({
|
||||||
|
'active': bucket === currentBucket
|
||||||
|
}) } key={ i } onClick={ (e) => selectBucket(e, bucket) }>
|
||||||
|
<a href="" className={ classNames({
|
||||||
|
'fesli-loading': bucket === loadBucket
|
||||||
|
}) }>
|
||||||
|
{ bucket }
|
||||||
|
</a>
|
||||||
|
<i className="fesli-trigger" onClick={ showPolicy }></i>
|
||||||
|
</li>
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClickOutHandler onClickOut={ clickOutside }>
|
||||||
|
<div className={ classNames({
|
||||||
|
'fe-sidebar': true,
|
||||||
|
'toggled': sidebarStatus
|
||||||
|
}) }>
|
||||||
|
<div className="fes-header clearfix hidden-sm hidden-xs">
|
||||||
|
<img src={ logo } alt="" />
|
||||||
|
<h2>Minio Browser</h2>
|
||||||
|
</div>
|
||||||
|
<div className="fes-list">
|
||||||
|
<div className="input-group ig-dark ig-left ig-search" style={ { display: web.LoggedIn() ? 'block' : 'none' } }>
|
||||||
|
<input className="ig-text"
|
||||||
|
type="text"
|
||||||
|
onChange={ searchBuckets }
|
||||||
|
placeholder="Search Buckets..." />
|
||||||
|
<i className="ig-helpers"></i>
|
||||||
|
</div>
|
||||||
|
<div className="fesl-inner">
|
||||||
|
<Scrollbars renderScrollbarVertical={ props => <div className="scrollbar-vertical" /> }>
|
||||||
|
<ul>
|
||||||
|
{ list }
|
||||||
|
</ul>
|
||||||
|
</Scrollbars>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="fes-host">
|
||||||
|
<i className="fa fa-globe"></i>
|
||||||
|
<a href="/">
|
||||||
|
{ window.location.host }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ClickOutHandler>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe it to state changes that affect only the sidebar.
|
||||||
|
export default connect(state => {
|
||||||
|
return {
|
||||||
|
visibleBuckets: state.visibleBuckets,
|
||||||
|
loadBucket: state.loadBucket,
|
||||||
|
currentBucket: state.currentBucket,
|
||||||
|
sidebarStatus: state.sidebarStatus
|
||||||
|
}
|
||||||
|
})(SideBar)
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import humanize from 'humanize'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import connect from 'react-redux/lib/components/connect'
|
||||||
|
|
||||||
|
import ProgressBar from 'react-bootstrap/lib/ProgressBar'
|
||||||
|
import ConfirmModal from './ConfirmModal'
|
||||||
|
|
||||||
|
import * as actions from '../actions'
|
||||||
|
|
||||||
|
// UploadModal is a modal that handles multiple file uploads.
|
||||||
|
// During the upload, it displays a progress bar, and can transform into an
|
||||||
|
// abort modal if the user decides to abort the uploads.
|
||||||
|
class UploadModal extends React.Component {
|
||||||
|
|
||||||
|
// Abort all the current uploads.
|
||||||
|
abortUploads(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch, uploads} = this.props
|
||||||
|
|
||||||
|
for (var slug in uploads) {
|
||||||
|
let upload = uploads[slug]
|
||||||
|
upload.xhr.abort()
|
||||||
|
dispatch(actions.stopUpload({
|
||||||
|
slug
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hideAbort(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the abort modal instead of the progress modal.
|
||||||
|
showAbort(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch} = this.props
|
||||||
|
|
||||||
|
dispatch(actions.setShowAbortModal(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the progress modal instead of the abort modal.
|
||||||
|
hideAbort(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
const {dispatch} = this.props
|
||||||
|
|
||||||
|
dispatch(actions.setShowAbortModal(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {uploads, showAbortModal} = this.props
|
||||||
|
|
||||||
|
// Show the abort modal.
|
||||||
|
if (showAbortModal) {
|
||||||
|
let baseClass = classNames({
|
||||||
|
'abort-upload': true
|
||||||
|
})
|
||||||
|
let okIcon = classNames({
|
||||||
|
'fa': true,
|
||||||
|
'fa-times': true
|
||||||
|
})
|
||||||
|
let cancelIcon = classNames({
|
||||||
|
'fa': true,
|
||||||
|
'fa-cloud-upload': true
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmModal show={ true }
|
||||||
|
baseClass={ baseClass }
|
||||||
|
text='Abort uploads in progress?'
|
||||||
|
icon='fa fa-info-circle mci-amber'
|
||||||
|
sub='This cannot be undone!'
|
||||||
|
okText='Abort'
|
||||||
|
okIcon={ okIcon }
|
||||||
|
cancelText='Upload'
|
||||||
|
cancelIcon={ cancelIcon }
|
||||||
|
okHandler={ this.abortUploads.bind(this) }
|
||||||
|
cancelHandler={ this.hideAbort.bind(this) }>
|
||||||
|
</ConfirmModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have any files uploading, don't show anything.
|
||||||
|
let numberUploading = Object.keys(uploads).length
|
||||||
|
if (numberUploading == 0)
|
||||||
|
return ( <noscript></noscript> )
|
||||||
|
|
||||||
|
let totalLoaded = 0
|
||||||
|
let totalSize = 0
|
||||||
|
|
||||||
|
// Iterate over each upload, adding together the total size and that
|
||||||
|
// which has been uploaded.
|
||||||
|
for (var slug in uploads) {
|
||||||
|
let upload = uploads[slug]
|
||||||
|
totalLoaded += upload.loaded
|
||||||
|
totalSize += upload.size
|
||||||
|
}
|
||||||
|
|
||||||
|
let percent = (totalLoaded / totalSize) * 100
|
||||||
|
|
||||||
|
// If more than one: "Uploading files (5)..."
|
||||||
|
// If only one: "Uploading myfile.txt..."
|
||||||
|
let text = 'Uploading ' + (numberUploading == 1 ? `'${uploads[Object.keys(uploads)[0]].name}'` : `files (${numberUploading})`) + '...'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="alert alert-info progress animated fadeInUp ">
|
||||||
|
<button type="button" className="close" onClick={ this.showAbort.bind(this) }>
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
<div className="text-center">
|
||||||
|
<small>{ text }</small>
|
||||||
|
</div>
|
||||||
|
<ProgressBar now={ percent } />
|
||||||
|
<div className="text-center">
|
||||||
|
<small>{ humanize.filesize(totalLoaded) } ({ percent.toFixed(2) } %)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(state => {
|
||||||
|
return {
|
||||||
|
uploads: state.uploads,
|
||||||
|
showAbortModal: state.showAbortModal
|
||||||
|
}
|
||||||
|
})(UploadModal)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
import React from 'react'
|
||||||
|
import ReactTestUtils, {renderIntoDocument} from 'react-addons-test-utils'
|
||||||
|
|
||||||
|
import expect from 'expect'
|
||||||
|
import Login from '../Login'
|
||||||
|
|
||||||
|
describe('Login', () => {
|
||||||
|
it('it should have empty credentials', () => {
|
||||||
|
const alert = {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
const dispatch = () => {}
|
||||||
|
let loginComponent = renderIntoDocument(<Login alert={alert} dispatch={dispatch} />)
|
||||||
|
const accessKey = document.getElementById('accessKey')
|
||||||
|
const secretKey = document.getElementById('secretKey')
|
||||||
|
// Validate default value.
|
||||||
|
expect(accessKey.value).toEqual('')
|
||||||
|
expect(secretKey.value).toEqual('')
|
||||||
|
})
|
||||||
|
it('it should set accessKey and secretKey', () => {
|
||||||
|
const alert = {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
const dispatch = () => {}
|
||||||
|
let loginComponent = renderIntoDocument(<Login alert={alert} dispatch={dispatch} />)
|
||||||
|
let accessKey = loginComponent.refs.accessKey
|
||||||
|
let secretKey = loginComponent.refs.secretKey
|
||||||
|
accessKey.value = 'demo-username'
|
||||||
|
secretKey.value = 'demo-password'
|
||||||
|
ReactTestUtils.Simulate.change(accessKey)
|
||||||
|
ReactTestUtils.Simulate.change(secretKey)
|
||||||
|
// Validate if the change has occurred.
|
||||||
|
expect(loginComponent.refs.accessKey.value).toEqual('demo-username')
|
||||||
|
expect(loginComponent.refs.secretKey.value).toEqual('demo-password')
|
||||||
|
})
|
||||||
|
});
|
||||||
|
*/
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// File for all the browser constants.
|
||||||
|
|
||||||
|
// minioBrowserPrefix absolute path.
|
||||||
|
var p = window.location.pathname
|
||||||
|
export const minioBrowserPrefix = p.slice(0, p.indexOf("/", 1))
|
||||||
|
|
||||||
|
export const READ_ONLY = 'readonly'
|
||||||
|
export const WRITE_ONLY = 'writeonly'
|
||||||
|
export const READ_WRITE = 'readwrite'
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import SuperAgent from 'superagent-es6-promise';
|
||||||
|
import url from 'url'
|
||||||
|
import Moment from 'moment'
|
||||||
|
|
||||||
|
export default class JSONrpc {
|
||||||
|
constructor(params) {
|
||||||
|
this.endpoint = params.endpoint
|
||||||
|
this.namespace = params.namespace
|
||||||
|
this.version = '2.0';
|
||||||
|
const parsedUrl = url.parse(this.endpoint)
|
||||||
|
this.host = parsedUrl.hostname
|
||||||
|
this.path = parsedUrl.path
|
||||||
|
this.port = parsedUrl.port
|
||||||
|
|
||||||
|
switch (parsedUrl.protocol) {
|
||||||
|
case 'http:': {
|
||||||
|
this.scheme = 'http'
|
||||||
|
if (parsedUrl.port === 0) {
|
||||||
|
this.port = 80
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'https:': {
|
||||||
|
this.scheme = 'https'
|
||||||
|
if (parsedUrl.port === 0) {
|
||||||
|
this.port = 443
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error('Unknown protocol: ' + parsedUrl.protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// call('Get', {id: NN, params: [...]}, function() {})
|
||||||
|
call(method, options, token) {
|
||||||
|
if (!options) {
|
||||||
|
options = {}
|
||||||
|
}
|
||||||
|
if (!options.id) {
|
||||||
|
options.id = 1;
|
||||||
|
}
|
||||||
|
if (!options.params) {
|
||||||
|
options.params = {};
|
||||||
|
}
|
||||||
|
const dataObj = {
|
||||||
|
id: options.id,
|
||||||
|
jsonrpc: this.version,
|
||||||
|
params: options.params ? options.params : {},
|
||||||
|
method: this.namespace ? this.namespace + '.' + method : method
|
||||||
|
}
|
||||||
|
let requestParams = {
|
||||||
|
host: this.host,
|
||||||
|
port: this.port,
|
||||||
|
path: this.path,
|
||||||
|
scheme: this.scheme,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-amz-date': Moment().utc().format('YYYYMMDDTHHmmss') + 'Z'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
requestParams.headers.Authorization = 'Bearer ' + token
|
||||||
|
}
|
||||||
|
|
||||||
|
let req = SuperAgent.post(this.endpoint)
|
||||||
|
for (let key in requestParams.headers) {
|
||||||
|
req.set(key, requestParams.headers[key])
|
||||||
|
}
|
||||||
|
// req.set('Access-Control-Allow-Origin', 'http://localhost:8080')
|
||||||
|
return req.send(JSON.stringify(dataObj)).then(res => res)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import mimedb from 'mime-types'
|
||||||
|
|
||||||
|
const isFolder = (name, contentType) => {
|
||||||
|
if (name.endsWith('/')) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPdf = (name, contentType) => {
|
||||||
|
if (contentType === 'application/pdf') return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isZip = (name, contentType) => {
|
||||||
|
if (!contentType || !contentType.includes('/')) return false
|
||||||
|
if (contentType.split('/')[1].includes('zip')) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCode = (name, contentType) => {
|
||||||
|
const codeExt = ['c', 'cpp', 'go', 'py', 'java', 'rb', 'js', 'pl', 'fs',
|
||||||
|
'php', 'css', 'less', 'scss', 'coffee', 'net', 'html',
|
||||||
|
'rs', 'exs', 'scala', 'hs', 'clj', 'el', 'scm', 'lisp',
|
||||||
|
'asp', 'aspx']
|
||||||
|
const ext = name.split('.').reverse()[0]
|
||||||
|
for (var i in codeExt) {
|
||||||
|
if (ext === codeExt[i]) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExcel = (name, contentType) => {
|
||||||
|
if (!contentType || !contentType.includes('/')) return false
|
||||||
|
const types = ['excel', 'spreadsheet']
|
||||||
|
const subType = contentType.split('/')[1]
|
||||||
|
for (var i in types) {
|
||||||
|
if (subType.includes(types[i])) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDoc = (name, contentType) => {
|
||||||
|
if (!contentType || !contentType.includes('/')) return false
|
||||||
|
const types = ['word', '.document']
|
||||||
|
const subType = contentType.split('/')[1]
|
||||||
|
for (var i in types) {
|
||||||
|
if (subType.includes(types[i])) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPresentation = (name, contentType) => {
|
||||||
|
if (!contentType || !contentType.includes('/')) return false
|
||||||
|
var types = ['powerpoint', 'presentation']
|
||||||
|
const subType = contentType.split('/')[1]
|
||||||
|
for (var i in types) {
|
||||||
|
if (subType.includes(types[i])) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeToIcon = (type) => {
|
||||||
|
return (name, contentType) => {
|
||||||
|
if (!contentType || !contentType.includes('/')) return false
|
||||||
|
if (contentType.split('/')[0] === type) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDataType = (name, contentType) => {
|
||||||
|
if (contentType === "") {
|
||||||
|
contentType = mimedb.lookup(name) || 'application/octet-stream'
|
||||||
|
}
|
||||||
|
const check = [
|
||||||
|
['folder', isFolder],
|
||||||
|
['code', isCode],
|
||||||
|
['audio', typeToIcon('audio')],
|
||||||
|
['image', typeToIcon('image')],
|
||||||
|
['video', typeToIcon('video')],
|
||||||
|
['text', typeToIcon('text')],
|
||||||
|
['pdf', isPdf],
|
||||||
|
['zip', isZip],
|
||||||
|
['excel', isExcel],
|
||||||
|
['doc', isDoc],
|
||||||
|
['presentation', isPresentation]
|
||||||
|
]
|
||||||
|
for (var i in check) {
|
||||||
|
if (check[i][1](name, contentType)) return check[i][0]
|
||||||
|
}
|
||||||
|
return 'other'
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as actions from './actions'
|
||||||
|
import { minioBrowserPrefix } from './constants'
|
||||||
|
|
||||||
|
export default (state = {
|
||||||
|
buckets: [],
|
||||||
|
visibleBuckets: [],
|
||||||
|
objects: [],
|
||||||
|
istruncated: true,
|
||||||
|
storageInfo: {},
|
||||||
|
serverInfo: {},
|
||||||
|
currentBucket: '',
|
||||||
|
currentPath: '',
|
||||||
|
showMakeBucketModal: false,
|
||||||
|
uploads: {},
|
||||||
|
alert: {
|
||||||
|
show: false,
|
||||||
|
type: 'danger',
|
||||||
|
message: ''
|
||||||
|
},
|
||||||
|
loginError: false,
|
||||||
|
sortNameOrder: false,
|
||||||
|
sortSizeOrder: false,
|
||||||
|
sortDateOrder: false,
|
||||||
|
latestUiVersion: currentUiVersion,
|
||||||
|
sideBarActive: false,
|
||||||
|
loginRedirectPath: minioBrowserPrefix,
|
||||||
|
settings: {
|
||||||
|
accessKey: '',
|
||||||
|
secretKey: '',
|
||||||
|
secretKeyVisible: false
|
||||||
|
},
|
||||||
|
showSettings: false,
|
||||||
|
policies: [],
|
||||||
|
deleteConfirmation: {
|
||||||
|
object: '',
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
shareObject: {
|
||||||
|
show: false,
|
||||||
|
url: '',
|
||||||
|
object: ''
|
||||||
|
},
|
||||||
|
prefixWritable: false,
|
||||||
|
checkedObjects: []
|
||||||
|
}, action) => {
|
||||||
|
let newState = Object.assign({}, state)
|
||||||
|
switch (action.type) {
|
||||||
|
case actions.SET_WEB:
|
||||||
|
newState.web = action.web
|
||||||
|
break
|
||||||
|
case actions.SET_BUCKETS:
|
||||||
|
newState.buckets = action.buckets
|
||||||
|
break
|
||||||
|
case actions.ADD_BUCKET:
|
||||||
|
newState.buckets = [action.bucket, ...newState.buckets]
|
||||||
|
newState.visibleBuckets = [action.bucket, ...newState.visibleBuckets]
|
||||||
|
break
|
||||||
|
case actions.SET_VISIBLE_BUCKETS:
|
||||||
|
newState.visibleBuckets = action.visibleBuckets
|
||||||
|
break
|
||||||
|
case actions.SET_CURRENT_BUCKET:
|
||||||
|
newState.currentBucket = action.currentBucket
|
||||||
|
break
|
||||||
|
case actions.APPEND_OBJECTS:
|
||||||
|
newState.objects = [...newState.objects, ...action.objects]
|
||||||
|
newState.marker = action.marker
|
||||||
|
newState.istruncated = action.istruncated
|
||||||
|
break
|
||||||
|
case actions.SET_OBJECTS:
|
||||||
|
newState.objects = [...action.objects]
|
||||||
|
break
|
||||||
|
case actions.RESET_OBJECTS:
|
||||||
|
newState.objects = []
|
||||||
|
newState.marker = ""
|
||||||
|
newState.istruncated = false
|
||||||
|
break
|
||||||
|
case actions.SET_CURRENT_PATH:
|
||||||
|
newState.currentPath = action.currentPath
|
||||||
|
break
|
||||||
|
case actions.SET_STORAGE_INFO:
|
||||||
|
newState.storageInfo = action.storageInfo
|
||||||
|
break
|
||||||
|
case actions.SET_SERVER_INFO:
|
||||||
|
newState.serverInfo = action.serverInfo
|
||||||
|
break
|
||||||
|
case actions.SHOW_MAKEBUCKET_MODAL:
|
||||||
|
newState.showMakeBucketModal = action.showMakeBucketModal
|
||||||
|
break
|
||||||
|
case actions.UPLOAD_PROGRESS:
|
||||||
|
newState.uploads = Object.assign({}, newState.uploads)
|
||||||
|
newState.uploads[action.slug].loaded = action.loaded
|
||||||
|
break
|
||||||
|
case actions.ADD_UPLOAD:
|
||||||
|
newState.uploads = Object.assign({}, newState.uploads, {
|
||||||
|
[action.slug]: {
|
||||||
|
loaded: 0,
|
||||||
|
size: action.size,
|
||||||
|
xhr: action.xhr,
|
||||||
|
name: action.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case actions.STOP_UPLOAD:
|
||||||
|
newState.uploads = Object.assign({}, newState.uploads)
|
||||||
|
delete newState.uploads[action.slug]
|
||||||
|
break
|
||||||
|
case actions.SET_ALERT:
|
||||||
|
if (newState.alert.alertTimeout) clearTimeout(newState.alert.alertTimeout)
|
||||||
|
if (!action.alert.show) {
|
||||||
|
newState.alert = Object.assign({}, newState.alert, {
|
||||||
|
show: false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
newState.alert = action.alert
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case actions.SET_LOGIN_ERROR:
|
||||||
|
newState.loginError = true
|
||||||
|
break
|
||||||
|
case actions.SET_SHOW_ABORT_MODAL:
|
||||||
|
newState.showAbortModal = action.showAbortModal
|
||||||
|
break
|
||||||
|
case actions.SHOW_ABOUT:
|
||||||
|
newState.showAbout = action.showAbout
|
||||||
|
break
|
||||||
|
case actions.SET_SORT_NAME_ORDER:
|
||||||
|
newState.sortNameOrder = action.sortNameOrder
|
||||||
|
break
|
||||||
|
case actions.SET_SORT_SIZE_ORDER:
|
||||||
|
newState.sortSizeOrder = action.sortSizeOrder
|
||||||
|
break
|
||||||
|
case actions.SET_SORT_DATE_ORDER:
|
||||||
|
newState.sortDateOrder = action.sortDateOrder
|
||||||
|
break
|
||||||
|
case actions.SET_LATEST_UI_VERSION:
|
||||||
|
newState.latestUiVersion = action.latestUiVersion
|
||||||
|
break
|
||||||
|
case actions.SET_SIDEBAR_STATUS:
|
||||||
|
newState.sidebarStatus = action.sidebarStatus
|
||||||
|
break
|
||||||
|
case actions.SET_LOGIN_REDIRECT_PATH:
|
||||||
|
newState.loginRedirectPath = action.path
|
||||||
|
case actions.SET_LOAD_BUCKET:
|
||||||
|
newState.loadBucket = action.loadBucket
|
||||||
|
break
|
||||||
|
case actions.SET_LOAD_PATH:
|
||||||
|
newState.loadPath = action.loadPath
|
||||||
|
break
|
||||||
|
case actions.SHOW_SETTINGS:
|
||||||
|
newState.showSettings = action.showSettings
|
||||||
|
break
|
||||||
|
case actions.SET_SETTINGS:
|
||||||
|
newState.settings = Object.assign({}, newState.settings, action.settings)
|
||||||
|
break
|
||||||
|
case actions.SHOW_BUCKET_POLICY:
|
||||||
|
newState.showBucketPolicy = action.showBucketPolicy
|
||||||
|
break
|
||||||
|
case actions.SET_POLICIES:
|
||||||
|
newState.policies = action.policies
|
||||||
|
break
|
||||||
|
case actions.DELETE_CONFIRMATION:
|
||||||
|
newState.deleteConfirmation = Object.assign({}, action.payload)
|
||||||
|
break
|
||||||
|
case actions.SET_SHARE_OBJECT:
|
||||||
|
newState.shareObject = Object.assign({}, action.shareObject)
|
||||||
|
break
|
||||||
|
case actions.SET_PREFIX_WRITABLE:
|
||||||
|
newState.prefixWritable = action.prefixWritable
|
||||||
|
break
|
||||||
|
case actions.REMOVE_OBJECT:
|
||||||
|
let idx = newState.objects.findIndex(object => object.name === action.object)
|
||||||
|
if (idx == -1) break
|
||||||
|
newState.objects = [...newState.objects.slice(0, idx), ...newState.objects.slice(idx + 1)]
|
||||||
|
break
|
||||||
|
|
||||||
|
case actions.CHECKED_OBJECTS_ADD:
|
||||||
|
newState.checkedObjects = [...newState.checkedObjects, action.objectName]
|
||||||
|
break
|
||||||
|
case actions.CHECKED_OBJECTS_REMOVE:
|
||||||
|
let index = newState.checkedObjects.indexOf(action.objectName)
|
||||||
|
if (index == -1) break
|
||||||
|
newState.checkedObjects = [...newState.checkedObjects.slice(0, index), ...newState.checkedObjects.slice(index + 1)]
|
||||||
|
break
|
||||||
|
case actions.CHECKED_OBJECTS_RESET:
|
||||||
|
newState.checkedObjects = []
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { minioBrowserPrefix } from './constants.js'
|
||||||
|
|
||||||
|
export const sortObjectsByName = (objects, order) => {
|
||||||
|
let folders = objects.filter(object => object.name.endsWith('/'))
|
||||||
|
let files = objects.filter(object => !object.name.endsWith('/'))
|
||||||
|
folders = folders.sort((a, b) => {
|
||||||
|
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1
|
||||||
|
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
files = files.sort((a, b) => {
|
||||||
|
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1
|
||||||
|
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
if (order) {
|
||||||
|
folders = folders.reverse()
|
||||||
|
files = files.reverse()
|
||||||
|
}
|
||||||
|
return [...folders, ...files]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sortObjectsBySize = (objects, order) => {
|
||||||
|
let folders = objects.filter(object => object.name.endsWith('/'))
|
||||||
|
let files = objects.filter(object => !object.name.endsWith('/'))
|
||||||
|
files = files.sort((a, b) => a.size - b.size)
|
||||||
|
if (order)
|
||||||
|
files = files.reverse()
|
||||||
|
return [...folders, ...files]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sortObjectsByDate = (objects, order) => {
|
||||||
|
let folders = objects.filter(object => object.name.endsWith('/'))
|
||||||
|
let files = objects.filter(object => !object.name.endsWith('/'))
|
||||||
|
files = files.sort((a, b) => new Date(a.lastModified).getTime() - new Date(b.lastModified).getTime())
|
||||||
|
if (order)
|
||||||
|
files = files.reverse()
|
||||||
|
return [...folders, ...files]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pathSlice = (path) => {
|
||||||
|
path = path.replace(minioBrowserPrefix, '')
|
||||||
|
let prefix = ''
|
||||||
|
let bucket = ''
|
||||||
|
if (!path) return {
|
||||||
|
bucket,
|
||||||
|
prefix
|
||||||
|
}
|
||||||
|
let objectIndex = path.indexOf('/', 1)
|
||||||
|
if (objectIndex == -1) {
|
||||||
|
bucket = path.slice(1)
|
||||||
|
return {
|
||||||
|
bucket,
|
||||||
|
prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bucket = path.slice(1, objectIndex)
|
||||||
|
prefix = path.slice(objectIndex + 1)
|
||||||
|
return {
|
||||||
|
bucket,
|
||||||
|
prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pathJoin = (bucket, prefix) => {
|
||||||
|
if (!prefix)
|
||||||
|
prefix = ''
|
||||||
|
return minioBrowserPrefix + '/' + bucket + '/' + prefix
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { browserHistory } from 'react-router'
|
||||||
|
import JSONrpc from './jsonrpc'
|
||||||
|
import * as actions from './actions'
|
||||||
|
import { minioBrowserPrefix } from './constants.js'
|
||||||
|
import Moment from 'moment'
|
||||||
|
import storage from 'local-storage-fallback'
|
||||||
|
|
||||||
|
export default class Web {
|
||||||
|
constructor(endpoint, dispatch) {
|
||||||
|
const namespace = 'Web'
|
||||||
|
this.dispatch = dispatch
|
||||||
|
this.JSONrpc = new JSONrpc({
|
||||||
|
endpoint,
|
||||||
|
namespace
|
||||||
|
})
|
||||||
|
}
|
||||||
|
makeCall(method, options) {
|
||||||
|
return this.JSONrpc.call(method, {
|
||||||
|
params: options
|
||||||
|
}, storage.getItem('token'))
|
||||||
|
.catch(err => {
|
||||||
|
if (err.status === 401) {
|
||||||
|
storage.removeItem('token')
|
||||||
|
browserHistory.push(`${minioBrowserPrefix}/login`)
|
||||||
|
throw new Error('Please re-login.')
|
||||||
|
}
|
||||||
|
if (err.status)
|
||||||
|
throw new Error(`Server returned error [${err.status}]`)
|
||||||
|
throw new Error('Minio server is unreachable')
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
let json = JSON.parse(res.text)
|
||||||
|
let result = json.result
|
||||||
|
let error = json.error
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message)
|
||||||
|
}
|
||||||
|
if (!Moment(result.uiVersion).isValid()) {
|
||||||
|
throw new Error("Invalid UI version in the JSON-RPC response")
|
||||||
|
}
|
||||||
|
if (result.uiVersion !== currentUiVersion
|
||||||
|
&& currentUiVersion !== 'MINIO_UI_VERSION') {
|
||||||
|
storage.setItem('newlyUpdated', true)
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
LoggedIn() {
|
||||||
|
return !!storage.getItem('token')
|
||||||
|
}
|
||||||
|
Login(args) {
|
||||||
|
return this.makeCall('Login', args)
|
||||||
|
.then(res => {
|
||||||
|
storage.setItem('token', `${res.token}`)
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Logout() {
|
||||||
|
storage.removeItem('token')
|
||||||
|
}
|
||||||
|
ServerInfo() {
|
||||||
|
return this.makeCall('ServerInfo')
|
||||||
|
}
|
||||||
|
StorageInfo() {
|
||||||
|
return this.makeCall('StorageInfo')
|
||||||
|
}
|
||||||
|
ListBuckets() {
|
||||||
|
return this.makeCall('ListBuckets')
|
||||||
|
}
|
||||||
|
MakeBucket(args) {
|
||||||
|
return this.makeCall('MakeBucket', args)
|
||||||
|
}
|
||||||
|
ListObjects(args) {
|
||||||
|
return this.makeCall('ListObjects', args)
|
||||||
|
}
|
||||||
|
PresignedGet(args) {
|
||||||
|
return this.makeCall('PresignedGet', args)
|
||||||
|
}
|
||||||
|
PutObjectURL(args) {
|
||||||
|
return this.makeCall('PutObjectURL', args)
|
||||||
|
}
|
||||||
|
RemoveObject(args) {
|
||||||
|
return this.makeCall('RemoveObject', args)
|
||||||
|
}
|
||||||
|
GetAuth() {
|
||||||
|
return this.makeCall('GetAuth')
|
||||||
|
}
|
||||||
|
GenerateAuth() {
|
||||||
|
return this.makeCall('GenerateAuth')
|
||||||
|
}
|
||||||
|
SetAuth(args) {
|
||||||
|
return this.makeCall('SetAuth', args)
|
||||||
|
.then(res => {
|
||||||
|
storage.setItem('token', `${res.token}`)
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
CreateURLToken() {
|
||||||
|
return this.makeCall('CreateURLToken')
|
||||||
|
}
|
||||||
|
GetBucketPolicy(args) {
|
||||||
|
return this.makeCall('GetBucketPolicy', args)
|
||||||
|
}
|
||||||
|
SetBucketPolicy(args) {
|
||||||
|
return this.makeCall('SetBucketPolicy', args)
|
||||||
|
}
|
||||||
|
ListAllBucketPolicies(args) {
|
||||||
|
return this.makeCall('ListAllBucketPolicies', args)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
.alert {
|
||||||
|
border: 0;
|
||||||
|
position: fixed;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
color: @white;
|
||||||
|
width: 100%;
|
||||||
|
right: 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 17px 50px 17px 17px;
|
||||||
|
z-index: 10010;
|
||||||
|
.animation-duration(800ms);
|
||||||
|
.animation-fill-mode(both);
|
||||||
|
|
||||||
|
&:not(.progress) {
|
||||||
|
top: 20px;
|
||||||
|
|
||||||
|
@media(min-width: (@screen-sm-min)) {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.progress {
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.alert-danger {
|
||||||
|
background: @red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.alert-success {
|
||||||
|
background: @green;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.alert-info {
|
||||||
|
background: @blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: (@screen-xs-max)) {
|
||||||
|
left: 20px;
|
||||||
|
width: ~"calc(100% - 40px)";
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
margin: 10px 10px 8px 0;
|
||||||
|
height: 5px;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 1px;
|
||||||
|
background-color: @blue;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: @white;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.animated{
|
||||||
|
&.infinite {
|
||||||
|
.animation-iteration-count(infinite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@import 'fadeIn';
|
||||||
|
@import 'fadeInDown';
|
||||||
|
@import 'fadeInUp';
|
||||||
|
@import 'fadeOut';
|
||||||
|
@import 'fadeOutDown';
|
||||||
|
@import 'fadeOutUp';
|
||||||
|
@import 'zoomIn';
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
@-webkit-keyframes fadeIn {
|
||||||
|
0% {opacity: 0;}
|
||||||
|
100% {opacity: 1;}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-moz-keyframes fadeIn {
|
||||||
|
0% {opacity: 0;}
|
||||||
|
100% {opacity: 1;}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-o-keyframes fadeIn {
|
||||||
|
0% {opacity: 0;}
|
||||||
|
100% {opacity: 1;}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
0% {opacity: 0;}
|
||||||
|
100% {opacity: 1;}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeIn {
|
||||||
|
-webkit-animation-name: fadeIn;
|
||||||
|
-moz-animation-name: fadeIn;
|
||||||
|
-o-animation-name: fadeIn;
|
||||||
|
animation-name: fadeIn;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@-webkit-keyframes fadeInDown {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-moz-keyframes fadeInDown {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
-moz-transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-moz-transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-o-keyframes fadeInDown {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
-ms-transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-ms-transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeInDown {
|
||||||
|
-webkit-animation-name: fadeInDown;
|
||||||
|
-moz-animation-name: fadeInDown;
|
||||||
|
-o-animation-name: fadeInDown;
|
||||||
|
animation-name: fadeInDown;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@-webkit-keyframes fadeInUp {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-moz-keyframes fadeInUp {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
-moz-transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-moz-transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-o-keyframes fadeInUp {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
-o-transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-o-transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeInUp {
|
||||||
|
-webkit-animation-name: fadeInUp;
|
||||||
|
-moz-animation-name: fadeInUp;
|
||||||
|
-o-animation-name: fadeInUp;
|
||||||
|
animation-name: fadeInUp;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
@-webkit-keyframes fadeOut {
|
||||||
|
0% {opacity: 1;}
|
||||||
|
100% {opacity: 0;}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-moz-keyframes fadeOut {
|
||||||
|
0% {opacity: 1;}
|
||||||
|
100% {opacity: 0;}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-o-keyframes fadeOut {
|
||||||
|
0% {opacity: 1;}
|
||||||
|
100% {opacity: 0;}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
0% {opacity: 1;}
|
||||||
|
100% {opacity: 0;}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeOut {
|
||||||
|
-webkit-animation-name: fadeOut;
|
||||||
|
-moz-animation-name: fadeOut;
|
||||||
|
-o-animation-name: fadeOut;
|
||||||
|
animation-name: fadeOut;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@-webkit-keyframes fadeOutDown {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-moz-keyframes fadeOutDown {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
-moz-transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
-moz-transform: translateY(20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-o-keyframes fadeOutDown {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
-o-transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
-o-transform: translateY(20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOutDown {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeOutDown {
|
||||||
|
-webkit-animation-name: fadeOutDown;
|
||||||
|
-moz-animation-name: fadeOutDown;
|
||||||
|
-o-animation-name: fadeOutDown;
|
||||||
|
animation-name: fadeOutDown;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
@-webkit-keyframes fadeOutUp {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-moz-keyframes fadeOutUp {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
-moz-transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
-moz-transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-o-keyframes fadeOutUp {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
-o-transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
-o-transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fadeOutUp {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fadeOutUp {
|
||||||
|
-webkit-animation-name: fadeOutUp;
|
||||||
|
-moz-animation-name: fadeOutUp;
|
||||||
|
-o-animation-name: fadeOutUp;
|
||||||
|
animation-name: fadeOutUp;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
@-webkit-keyframes zoomIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: scale3d(.3, .3, .3);
|
||||||
|
transform: scale3d(.3, .3, .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes zoomIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: scale3d(.3, .3, .3);
|
||||||
|
transform: scale3d(.3, .3, .3);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
* {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 10px;
|
||||||
|
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
.transition(color);
|
||||||
|
.transition-duration(300ms);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
.btn {
|
||||||
|
border: 0;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: 2px;
|
||||||
|
text-align: center;
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(300ms);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
.opacity(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*-----------------------------------
|
||||||
|
Button Variants
|
||||||
|
------------------------------------*/
|
||||||
|
.btn-variant(@bg-color, @color) {
|
||||||
|
color: @color;
|
||||||
|
background-color: @bg-color;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: @color;
|
||||||
|
background-color: darken(@bg-color, 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-block {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-white {
|
||||||
|
.btn-variant(#fff, darken(@text-color, 20%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
.btn-variant(#eee, #545454);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
.btn-variant(@red, @white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
.btn-variant(@blue, @white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
.btn-variant(@green, @white);
|
||||||
|
}
|
||||||
|
//-----------------------------------
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
.dropdown-menu {
|
||||||
|
padding: 15px 0;
|
||||||
|
top: 0;
|
||||||
|
margin-top: -1px;
|
||||||
|
|
||||||
|
& > li {
|
||||||
|
& > a {
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 15px;
|
||||||
|
|
||||||
|
& > i {
|
||||||
|
width: 20px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu-right {
|
||||||
|
& > li {
|
||||||
|
& > a {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/*------------------------------
|
||||||
|
Layout
|
||||||
|
--------------------------------*/
|
||||||
|
.file-explorer {
|
||||||
|
background-color: @white;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&.toggled {
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fe-body {
|
||||||
|
@media(min-width: @screen-md-min) {
|
||||||
|
padding: 0 0 40px @fe-sidebar-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: @screen-sm-max) {
|
||||||
|
padding: 75px 0 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
min-height:100vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*------------------------------
|
||||||
|
Create and Upload Button
|
||||||
|
--------------------------------*/
|
||||||
|
.feb-actions {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
right: 30px;
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
min-width: 55px;
|
||||||
|
width: 55px;
|
||||||
|
text-align: center;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
.feba-btn {
|
||||||
|
.scale(1);
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
.animation-name(feba-btn-anim);
|
||||||
|
.animation-duration(300ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
.animation-name(feba-btn-anim);
|
||||||
|
.animation-duration(100ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feba-toggle {
|
||||||
|
background: darken(@red, 10%);
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
.rotate(135deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feba-toggle {
|
||||||
|
width: 55px;
|
||||||
|
height: 55px;
|
||||||
|
line-height: 55px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: @red;
|
||||||
|
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.15);
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: @white;
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 58px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feba-toggle,
|
||||||
|
.feba-toggle > span {
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(250ms);
|
||||||
|
.backface-visibility(hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feba-btn {
|
||||||
|
width: 40px;
|
||||||
|
margin-top: 10px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
color: @white;
|
||||||
|
line-height: 40px;
|
||||||
|
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.15);
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: @white;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feba-bucket {
|
||||||
|
background: @orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feba-upload {
|
||||||
|
background: @yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes feba-btn-anim {
|
||||||
|
from {
|
||||||
|
.scale(0);
|
||||||
|
.opacity(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
.scale(1);
|
||||||
|
.opacity(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes feba-btn-anim {
|
||||||
|
from {
|
||||||
|
.scale(0);
|
||||||
|
.opacity(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
.scale(1);
|
||||||
|
.opacity(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: Lato;
|
||||||
|
src: url('../../fonts/lato/lato-normal.woff2') format('woff2'),
|
||||||
|
url('../../fonts/lato/lato-normal.woff') format('woff');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
.form-control {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid @input-border;
|
||||||
|
color: #32393F;
|
||||||
|
padding: 5px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 13px;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.form-control {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
border-radius: 0;
|
||||||
|
background: url(../../img/select-caret.svg) no-repeat bottom 7px right;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Input Group
|
||||||
|
----------------------------*/
|
||||||
|
.input-group {
|
||||||
|
position: relative;
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label:not(.ig-label) {
|
||||||
|
font-size: 13px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-label {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
bottom: 7px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(250ms);
|
||||||
|
padding: 2px 0 3px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-helpers {
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
&,
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
position: absolute;
|
||||||
|
height: 2px;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
width: 0;
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(250ms);
|
||||||
|
background-color: #03A9F4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
right: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-text {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent !important;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
color: #32393F;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
|
||||||
|
&:focus + .ig-helpers {
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:valid,
|
||||||
|
&:disabled,
|
||||||
|
&:focus {
|
||||||
|
& ~ .ig-label {
|
||||||
|
bottom: 35px;
|
||||||
|
font-size: 13px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
.opacity(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-dark {
|
||||||
|
.ig-text {
|
||||||
|
color: @white !important;
|
||||||
|
border-color: rgba(255,255,255,0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-helpers {
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
background-color: #dfdfdf;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-left {
|
||||||
|
.ig-label,
|
||||||
|
.ig-text {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-error {
|
||||||
|
.ig-label {
|
||||||
|
color: #E23F3F;
|
||||||
|
}
|
||||||
|
.ig-helpers i {
|
||||||
|
&:first-child,
|
||||||
|
&:first-child:before,
|
||||||
|
&:first-child:after {
|
||||||
|
background: rgba(226, 63, 63, 0.43);
|
||||||
|
}
|
||||||
|
&:last-child,
|
||||||
|
&:last-child:before,
|
||||||
|
&:last-child:after {
|
||||||
|
background: #E23F3F !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
content: "\f05a";
|
||||||
|
font-family: FontAwesome;
|
||||||
|
position: absolute;
|
||||||
|
top: 17px;
|
||||||
|
right: 9px;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #D33D3E;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-search {
|
||||||
|
&:before {
|
||||||
|
font-family: @font-family-icon;
|
||||||
|
content: '\f002';
|
||||||
|
font-size: 15px;
|
||||||
|
position: absolute;
|
||||||
|
left: 2px;
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-text {
|
||||||
|
padding-left: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Share Spinners
|
||||||
|
----------------------------*/
|
||||||
|
.set-expire {
|
||||||
|
border: 1px solid @input-border;
|
||||||
|
margin: 35px 0 30px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-expire-item {
|
||||||
|
padding: 9px 5px 3px;
|
||||||
|
position: relative;
|
||||||
|
display: table-cell;
|
||||||
|
width: 1%;
|
||||||
|
text-align: center;
|
||||||
|
.user-select(none);
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-right: 1px solid @input-border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-expire-title {
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-expire-value {
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
left: -8px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
.user-select(none);
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
right: -15px;
|
||||||
|
border: 0;
|
||||||
|
color: @text-strong-color;
|
||||||
|
padding: 0;
|
||||||
|
height: 25px;
|
||||||
|
width: 100%;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-expire-decrease,
|
||||||
|
.set-expire-increase {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: url(../../img/arrow.svg) no-repeat center;
|
||||||
|
background-size: 85%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -10px;
|
||||||
|
.opacity(0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.opacity(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-expire-increase {
|
||||||
|
top: -25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-expire-decrease {
|
||||||
|
bottom: -27px;
|
||||||
|
.rotate(-180deg);
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*----------------------------
|
||||||
|
Text Alignment
|
||||||
|
-----------------------------*/
|
||||||
|
.text-center { text-align: center !important; }
|
||||||
|
.text-left { text-align: left !important; }
|
||||||
|
.text-right { text-align: right !important; }
|
||||||
|
|
||||||
|
|
||||||
|
/*----------------------------
|
||||||
|
Float
|
||||||
|
-----------------------------*/
|
||||||
|
.clearfix { .clearfix(); }
|
||||||
|
.pull-right { float: right !important; }
|
||||||
|
.pull-left { float: left !important; }
|
||||||
|
|
||||||
|
|
||||||
|
/*----------------------------
|
||||||
|
Position
|
||||||
|
-----------------------------*/
|
||||||
|
.p-relative { position: relative; }
|
||||||
|
|
||||||
|
|
||||||
|
/*---------------------------------------------------------------------------
|
||||||
|
Generate Margin Class
|
||||||
|
margin, margin-top, margin-bottom, margin-left, margin-right
|
||||||
|
----------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
.margin (@label, @size: 1, @key:1) when (@size =< 30){
|
||||||
|
.m-@{key} {
|
||||||
|
margin: @size !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-t-@{key} {
|
||||||
|
margin-top: @size !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-b-@{key} {
|
||||||
|
margin-bottom: @size !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-l-@{key} {
|
||||||
|
margin-left: @size !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-r-@{key} {
|
||||||
|
margin-right: @size !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin(@label - 5; @size + 5; @key + 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin(25, 0px, 0);
|
||||||
|
|
||||||
|
|
||||||
|
/*---------------------------------------------------------------------------
|
||||||
|
Generate Padding Class
|
||||||
|
padding, padding-top, padding-bottom, padding-left, padding-right
|
||||||
|
----------------------------------------------------------------------------*/
|
||||||
|
.padding (@label, @size: 1, @key:1) when (@size =< 30){
|
||||||
|
.p-@{key} {
|
||||||
|
padding: @size !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-t-@{key} {
|
||||||
|
padding-top: @size !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-b-@{key} {
|
||||||
|
padding-bottom: @size !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-l-@{key} {
|
||||||
|
padding-left: @size !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-r-@{key} {
|
||||||
|
padding-right: @size !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding(@label - 5; @size + 5; @key + 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding(25, 0px, 0);
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
/*--------------------------
|
||||||
|
Header
|
||||||
|
----------------------------*/
|
||||||
|
.fe-header {
|
||||||
|
@media(min-width: (@screen-sm-min - 100)) {
|
||||||
|
position: relative;
|
||||||
|
padding: 40px 40px 20px 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: (@screen-xs-max - 100)) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
margin-bottom: 7px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
&:before {
|
||||||
|
content: '/';
|
||||||
|
margin: 0 4px;
|
||||||
|
color: @text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Disk usage
|
||||||
|
----------------------------*/
|
||||||
|
.feh-usage {
|
||||||
|
margin-top: 12px;
|
||||||
|
max-width: 285px;
|
||||||
|
|
||||||
|
@media(max-width: (@screen-xs-max - 100px)) {
|
||||||
|
max-width: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > ul {
|
||||||
|
margin-top: 7px;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
& > li {
|
||||||
|
padding-right: 0;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fehu-chart {
|
||||||
|
height: 5px;
|
||||||
|
background: #eee;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: @link-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Header Actions
|
||||||
|
----------------------------*/
|
||||||
|
.feh-actions {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 35px;
|
||||||
|
top: 30px;
|
||||||
|
z-index: 11;
|
||||||
|
|
||||||
|
@media(max-width: (@screen-sm-max)) {
|
||||||
|
top: 7px;
|
||||||
|
right: 10px;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > li {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: top;
|
||||||
|
line-height: 100%;
|
||||||
|
|
||||||
|
& > a,
|
||||||
|
& > .btn-group > button {
|
||||||
|
display: block;
|
||||||
|
height: 45px;
|
||||||
|
min-width: 45px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
|
||||||
|
@media(min-width: @screen-md-min) {
|
||||||
|
color: #7B7B7B;
|
||||||
|
font-size: 21px;
|
||||||
|
line-height: 45px;
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(300ms);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0,0,0,0.09);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: (@screen-sm-max)) {
|
||||||
|
background: url(../../img/more-h-light.svg) no-repeat center;
|
||||||
|
|
||||||
|
.fa-reorder {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Mobile Header
|
||||||
|
----------------------------*/
|
||||||
|
@media(max-width: @screen-sm-max) {
|
||||||
|
.fe-header-mobile {
|
||||||
|
background-color: @dark-gray;
|
||||||
|
padding: 10px 50px 9px 12px;
|
||||||
|
text-align: center;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.mh-logo {
|
||||||
|
height: 35px;
|
||||||
|
position: relative;
|
||||||
|
top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feh-trigger {
|
||||||
|
width: 41px;
|
||||||
|
height: 41px;
|
||||||
|
cursor: pointer;
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(300ms);
|
||||||
|
.scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feht-toggled {
|
||||||
|
&:before {
|
||||||
|
.scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feht-lines {
|
||||||
|
.rotate(180deg);
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
&.top {
|
||||||
|
width: 12px;
|
||||||
|
transform: translateX(8px) translateY(1px) rotate(45deg);
|
||||||
|
-webkit-transform: translateX(8px) translateY(1px) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom {
|
||||||
|
width: 12px;
|
||||||
|
transform: translateX(8px) translateY(-1px) rotate(-45deg);
|
||||||
|
-webkit-transform: translateX(8px) translateY(-1px) rotate(-45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feht-lines,
|
||||||
|
.feht-lines > div {
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(300ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feht-lines {
|
||||||
|
width: 18px;
|
||||||
|
height: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 14px;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
background-color: #EAEAEA;
|
||||||
|
width: 18px;
|
||||||
|
height: 2px;
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
margin: 3px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
.ie-warning {
|
||||||
|
background-color: #ff5252;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
width: 1px;
|
||||||
|
content: '';
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before,
|
||||||
|
.iw-inner {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.iw-inner {
|
||||||
|
width: 470px;
|
||||||
|
height: 300px;
|
||||||
|
background-color: @white;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 40px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 230px;
|
||||||
|
margin-left: 80px;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
& > li {
|
||||||
|
float: left;
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
display: block;
|
||||||
|
padding: 10px 15px 7px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 1px;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 40px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.iwi-icon {
|
||||||
|
color: #ff5252;
|
||||||
|
font-size: 40px;
|
||||||
|
display: block;
|
||||||
|
line-height: 100%;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iwi-skip {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: -35px;
|
||||||
|
width: 100%;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @white;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
/*--------------------------
|
||||||
|
Row
|
||||||
|
----------------------------*/
|
||||||
|
.fesl-row {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@media (min-width: (@screen-sm-min - 100px)) {
|
||||||
|
padding: 5px 20px 5px 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: (@screen-xs-max - 100px)) {
|
||||||
|
padding: 5px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix();
|
||||||
|
}
|
||||||
|
|
||||||
|
header.fesl-row {
|
||||||
|
@media (min-width:(@screen-sm-min - 100px)) {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 1px solid lighten(@text-muted-color, 20%);
|
||||||
|
padding-left: 30px;
|
||||||
|
|
||||||
|
.fesl-item,
|
||||||
|
.fesli-sort {
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(300ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fesl-item {
|
||||||
|
cursor: pointer;
|
||||||
|
color: @text-color;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: -5px;
|
||||||
|
|
||||||
|
& > .fesli-sort {
|
||||||
|
float: right;
|
||||||
|
margin: 4px 0 0;
|
||||||
|
.opacity(0);
|
||||||
|
color: @dark-gray;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.fesl-item-actions) {
|
||||||
|
background: lighten(@text-muted-color, 22%);
|
||||||
|
color: @dark-gray;
|
||||||
|
|
||||||
|
& > .fesli-sort {
|
||||||
|
.opacity(0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:(@screen-xs-max - 100px)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-type(@background, @icon) {
|
||||||
|
.fis-icon {
|
||||||
|
background-color: @background;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: @icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.fesl-row {
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
cursor: default;
|
||||||
|
.transition(background-color);
|
||||||
|
.transition-duration(500ms);
|
||||||
|
|
||||||
|
@media (max-width: (@screen-xs-max - 100px)) {
|
||||||
|
padding: 5px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.fesl-row-selected) {
|
||||||
|
&:nth-child(even) {
|
||||||
|
background-color: @list-row-even-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.fis-icon {
|
||||||
|
&:before {
|
||||||
|
.opacity(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fis-helper {
|
||||||
|
&:before {
|
||||||
|
.opacity(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Icons
|
||||||
|
----------------------------*/
|
||||||
|
&[data-type=folder] { .list-type(#a1d6dd, '\f114'); }
|
||||||
|
&[data-type=pdf] {.list-type(#fa7775, '\f1c1'); }
|
||||||
|
&[data-type=zip] { .list-type(#427089, '\f1c6'); }
|
||||||
|
&[data-type=audio] { .list-type(#009688, '\f1c7'); }
|
||||||
|
&[data-type=code] { .list-type(#997867, "\f1c9"); }
|
||||||
|
&[data-type=excel] { .list-type(#f1c3, '\f1c3'); }
|
||||||
|
&[data-type=image] { .list-type(#f06292, '\f1c5'); }
|
||||||
|
&[data-type=video] { .list-type(#f8c363, '\f1c8'); }
|
||||||
|
&[data-type=other] { .list-type(#afafaf, '\f016'); }
|
||||||
|
&[data-type=text] { .list-type(#8a8a8a, '\f0f6'); }
|
||||||
|
&[data-type=doc] { .list-type(#2196f5, '\f1c2'); }
|
||||||
|
&[data-type=presentation] { .list-type(#896ea6, '\f1c4'); }
|
||||||
|
|
||||||
|
&.fesl-loading{
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
.list-loader(20px, 20px, rgba(255, 255, 255, 0.5), @white);
|
||||||
|
left: 57px;
|
||||||
|
top: 17px;
|
||||||
|
|
||||||
|
@media (max-width: (@screen-xs-max - 100px)) {
|
||||||
|
left: 27px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fesl-row-selected {
|
||||||
|
background-color: @list-row-selected-bg;
|
||||||
|
|
||||||
|
&, .fesl-item a {
|
||||||
|
color: darken(@text-color, 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fi-select {
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
margin: 3px 0;
|
||||||
|
|
||||||
|
@media(max-width: (@screen-xs-max - 100px)) {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
z-index: 20;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
& ~ .fis-icon {
|
||||||
|
background-color: #32393F;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& ~ .fis-helper {
|
||||||
|
&:before {
|
||||||
|
.scale(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
.scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fis-icon {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
.transition(background-color);
|
||||||
|
.transition-duration(250ms);
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-family: @font-family-icon;
|
||||||
|
line-height: 35px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: @white;
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(300ms);
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fis-helper {
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
position: absolute;
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(250ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border: 2px solid @white;
|
||||||
|
z-index: 10;
|
||||||
|
border-radius: 2px;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
font-family: @font-family-icon;
|
||||||
|
content: '\f00c';
|
||||||
|
top: 8px;
|
||||||
|
left: 9px;
|
||||||
|
color: @white;
|
||||||
|
font-size: 14px;
|
||||||
|
.scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Files and Folders
|
||||||
|
----------------------------*/
|
||||||
|
.fesl-item {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: darken(@text-color, 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(min-width: (@screen-sm-min - 100px)) {
|
||||||
|
&:not(.fesl-item-actions):not(.fesl-item-icon) {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 10px 15px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fesl-item-name {
|
||||||
|
flex: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fesl-item-size {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fesl-item-modified {
|
||||||
|
width: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fesl-item-actions {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: (@screen-xs-max - 100px)) {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&.fesl-item-name {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fesl-item-size,
|
||||||
|
&.fesl-item-modified {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #B5B5B5;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fesl-item-modified {
|
||||||
|
max-width: 72px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fesl-item-size {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fesl-item-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Action buttons
|
||||||
|
----------------------------*/
|
||||||
|
.fia-toggle {
|
||||||
|
height: 36px;
|
||||||
|
width: 36px;
|
||||||
|
background: transparent url(../../img/more-h.svg) no-repeat center;
|
||||||
|
position: relative;
|
||||||
|
top: 3px;
|
||||||
|
.opacity(0.4);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.opacity(0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fesl-item-actions {
|
||||||
|
.dropdown-menu {
|
||||||
|
background-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0;
|
||||||
|
right: 38px;
|
||||||
|
left: auto;
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
&.open {
|
||||||
|
.dropdown-menu {
|
||||||
|
.fiad-action {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fiad-action {
|
||||||
|
height: 35px;
|
||||||
|
width: 35px;
|
||||||
|
background: @amber;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 35px;
|
||||||
|
font-weight: normal;
|
||||||
|
position: relative;
|
||||||
|
top: 4px;
|
||||||
|
margin-left: 5px;
|
||||||
|
.animation-name(fiad-action-anim);
|
||||||
|
.transform-origin(center center);
|
||||||
|
.backface-visibility(none);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
.animation-duration(100ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
.animation-duration(250ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > i {
|
||||||
|
font-size: 14px;
|
||||||
|
color: @white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: darken(@amber, 3%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-actions {
|
||||||
|
position: fixed;
|
||||||
|
.translate3d(0, -100%, 0);
|
||||||
|
.opacity(0);
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(200ms);
|
||||||
|
padding: 20px 70px 20px 25px;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
background-color: @brand-primary;
|
||||||
|
z-index: 20;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&.list-actions-toggled {
|
||||||
|
.translate3d(0, 0, 0);
|
||||||
|
.opacity(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.la-close {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 0;
|
||||||
|
color: #fff;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 30px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
font-weight: normal;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.la-label {
|
||||||
|
color: @white;
|
||||||
|
float: left;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
font-size: 22px;
|
||||||
|
vertical-align: top;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.la-actions {
|
||||||
|
button {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 2px solid rgba(255,255,255,0.9);
|
||||||
|
color: @white;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(300ms);
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: @white;
|
||||||
|
color: @brand-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes fiad-action-anim {
|
||||||
|
from {
|
||||||
|
.scale(0);
|
||||||
|
.opacity(0);
|
||||||
|
right: -20px;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
.scale(1);
|
||||||
|
.opacity(1);
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fiad-action-anim {
|
||||||
|
from {
|
||||||
|
.scale(0);
|
||||||
|
.opacity(0);
|
||||||
|
right: -20px;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
.scale(1);
|
||||||
|
.opacity(1);
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
.login {
|
||||||
|
height: 100vh;
|
||||||
|
min-height: 500px;
|
||||||
|
background: @dark-gray;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
&:before {
|
||||||
|
height: ~"calc(100% - 110px)";
|
||||||
|
width: 1px;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.l-wrap,
|
||||||
|
.login:before {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.l-wrap {
|
||||||
|
width: 80%;
|
||||||
|
max-width: 500px;
|
||||||
|
margin-top: -50px;
|
||||||
|
&.toggled {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group:not(:last-child) {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.l-footer {
|
||||||
|
height: 110px;
|
||||||
|
padding: 0 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lf-logo {
|
||||||
|
float: right;
|
||||||
|
img {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lf-server {
|
||||||
|
float: left;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
padding-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: @screen-sm-min) {
|
||||||
|
.lf-logo,
|
||||||
|
.lf-server {
|
||||||
|
float: none;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lf-logo {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lf-server {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lw-btn {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 1px solid @white;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 22px;
|
||||||
|
color: @white;
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(300ms);
|
||||||
|
opacity: 0.3;
|
||||||
|
background-color: transparent;
|
||||||
|
line-height: 45px;
|
||||||
|
padding: 0;
|
||||||
|
&:hover {
|
||||||
|
color: @white;
|
||||||
|
opacity: 0.8;
|
||||||
|
border-color: @white;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------------------------
|
||||||
|
Chrome autofill fix
|
||||||
|
-------------------------------*/
|
||||||
|
input:-webkit-autofill {
|
||||||
|
-webkit-box-shadow:0 0 0 50px @dark-gray inset !important;
|
||||||
|
-webkit-text-fill-color: @white !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/*--------------------------
|
||||||
|
Close
|
||||||
|
----------------------------*/
|
||||||
|
.close-variant(@color, @bg-color, @color-hover, @bg-color-hover) {
|
||||||
|
span {
|
||||||
|
background-color: @bg-color;
|
||||||
|
color: @color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
span {
|
||||||
|
background-color: @bg-color-hover;
|
||||||
|
color: @color-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
right: 15px;
|
||||||
|
font-weight: normal;
|
||||||
|
opacity: 1;
|
||||||
|
font-size: 18px;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
top: 16px;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
span {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
display: block;
|
||||||
|
border-radius: 50%;
|
||||||
|
line-height: 24px;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.close-alt) {
|
||||||
|
.close-variant(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.1), @white, rgba(255, 255, 255, 0.2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-alt {
|
||||||
|
.close-variant(#989898, #efefef, #7b7b7b, #e8e8e8);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Hidden
|
||||||
|
----------------------------*/
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Copy text
|
||||||
|
----------------------------*/
|
||||||
|
.copy-text {
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 1px;
|
||||||
|
border: 1px solid @input-border;
|
||||||
|
padding: 7px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 100%;
|
||||||
|
cursor: text;
|
||||||
|
.transition(border-color);
|
||||||
|
.transition-duration(300ms);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: darken(@input-border, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Sharing
|
||||||
|
----------------------------*/
|
||||||
|
.share-availability {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
&:before,
|
||||||
|
&:after {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -30px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '01 Sec';
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '7 days';
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.modal-aheader {
|
||||||
|
height: 100px;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*--------------------------
|
||||||
|
User Select
|
||||||
|
----------------------------*/
|
||||||
|
.user-select(@value) {
|
||||||
|
-webkit-user-select: @value;
|
||||||
|
-moz-user-select: @value;
|
||||||
|
-ms-user-select: @value;
|
||||||
|
user-select: @value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*----------------------------------------
|
||||||
|
CSS Animations based on animate.css
|
||||||
|
-----------------------------------------*/
|
||||||
|
.animated(@name, @duration) {
|
||||||
|
-webkit-animation-name: @name;
|
||||||
|
animation-name: @name;
|
||||||
|
-webkit-animation-duration: @duration;
|
||||||
|
animation-duration: @duration;
|
||||||
|
-webkit-animation-fill-mode: both;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*-------------------------------------------------
|
||||||
|
For loop mixin for generate custom classes
|
||||||
|
--------------------------------------------------*/
|
||||||
|
.for(@i, @n) {.-each(@i)}
|
||||||
|
.for(@n) when (isnumber(@n)) {.for(1, @n)}
|
||||||
|
.for(@i, @n) when not (@i = @n) {
|
||||||
|
.for((@i + (@n - @i) / abs(@n - @i)), @n);
|
||||||
|
}
|
||||||
|
|
||||||
|
.for(@array) when (default()) {.for-impl_(length(@array))}
|
||||||
|
.for-impl_(@i) when (@i > 1) {.for-impl_((@i - 1))}
|
||||||
|
.for-impl_(@i) when (@i > 0) {.-each(extract(@array, @i))}
|
||||||
|
|
||||||
|
/*----------------------------------------
|
||||||
|
List Loader
|
||||||
|
-----------------------------------------*/
|
||||||
|
.list-loader(@width, @height, @borderColor, @borderColorBottom) {
|
||||||
|
content: '';
|
||||||
|
width: @width;
|
||||||
|
height: @height;
|
||||||
|
border-radius: 50%;
|
||||||
|
.animated(zoomIn, 500ms);
|
||||||
|
border: 2px solid @borderColor;
|
||||||
|
border-bottom-color: @borderColorBottom;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
-webkit-animation: zoomIn 250ms, spin 700ms 250ms infinite linear;
|
||||||
|
animation: zoomIn 250ms, spin 700ms 250ms infinite linear;
|
||||||
|
}
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
/*--------------------------
|
||||||
|
Modal
|
||||||
|
----------------------------*/
|
||||||
|
.modal {
|
||||||
|
@media(min-width: @screen-sm-min) {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
height: 100%;
|
||||||
|
width: 1px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
text-align: left;
|
||||||
|
margin: 10px auto;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dark {
|
||||||
|
.modal-header {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: @dark-gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
.animated(fadeIn, 200ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
.animated(zoomIn, 200ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
color: @text-strong-color;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
small {
|
||||||
|
display: block;
|
||||||
|
text-transform: none;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 5px;
|
||||||
|
color: #a8a8a8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 0 30px 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Dialog
|
||||||
|
----------------------------*/
|
||||||
|
.modal-confirm {
|
||||||
|
.modal-dialog {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-icon {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
|
||||||
|
& > i {
|
||||||
|
font-size: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mci-red {
|
||||||
|
color: #ff8f8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mci-amber {
|
||||||
|
color: @amber;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mci-green {
|
||||||
|
color: #64e096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-text {
|
||||||
|
color: @text-strong-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-sub {
|
||||||
|
color: @text-muted-color;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
//--------------------------
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
About
|
||||||
|
----------------------------*/
|
||||||
|
.modal-about {
|
||||||
|
@media (max-width: @screen-xs-max) {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 90%;
|
||||||
|
margin: 20px auto 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ma-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 350px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@media (min-width: @screen-sm-min) {
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
width: 150px;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 3px 0 0px 3px;
|
||||||
|
background-color: #23282C;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mai-item {
|
||||||
|
&:first-child {
|
||||||
|
width: 150px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
flex: 4;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.maii-logo {
|
||||||
|
width: 70px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.maii-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
& > li {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
div {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//--------------------------
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Preferences
|
||||||
|
----------------------------*/
|
||||||
|
.toggle-password {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 30px;
|
||||||
|
right: 35px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 0;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: @white;
|
||||||
|
padding-top: 5px;
|
||||||
|
|
||||||
|
&.toggled {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//--------------------------
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Policy
|
||||||
|
----------------------------*/
|
||||||
|
.pm-body {
|
||||||
|
padding-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pmb-header {
|
||||||
|
margin-bottom: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pmb-list {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 35px;
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
background-color: #F7F7F7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header.pmb-list {
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pmbl-item {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
margin: 0 25px;
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.pmb-list {
|
||||||
|
select {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pml-item {
|
||||||
|
&:not(:last-child) {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//--------------------------
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Create Bucket
|
||||||
|
----------------------------*/
|
||||||
|
.modal-create-bucket {
|
||||||
|
.modal-dialog {
|
||||||
|
position: fixed;
|
||||||
|
right: 25px;
|
||||||
|
bottom: 95px;
|
||||||
|
margin: 0;
|
||||||
|
height: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//--------------------------
|
||||||
|
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
/*--------------------------
|
||||||
|
Sidebar
|
||||||
|
----------------------------*/
|
||||||
|
.fe-sidebar {
|
||||||
|
width: @fe-sidebar-width;
|
||||||
|
background-color: @dark-gray;
|
||||||
|
position: fixed;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 25px;
|
||||||
|
|
||||||
|
@media(min-width: @screen-md-min) {
|
||||||
|
.translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: @screen-sm-max) {
|
||||||
|
padding-top: 85px;
|
||||||
|
z-index: 9;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.65);
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(300ms);
|
||||||
|
.translate3d((-@fe-sidebar-width - 15px), 0, 0);
|
||||||
|
|
||||||
|
&.toggled {
|
||||||
|
.translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(255, 255, 255, 0.58);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Header
|
||||||
|
----------------------------*/
|
||||||
|
.fes-header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
|
||||||
|
img,
|
||||||
|
h2 {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 13px 0 0 10px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
List
|
||||||
|
----------------------------*/
|
||||||
|
.fesl-inner {
|
||||||
|
height: ~"calc(100vh - 260px)";
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 -25px;
|
||||||
|
|
||||||
|
& li {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
display: block;
|
||||||
|
padding: 10px 45px 12px 55px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
font-family: FontAwesome;
|
||||||
|
content: '\f0a0';
|
||||||
|
font-size: 17px;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 25px;
|
||||||
|
.opacity(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fesli-loading {
|
||||||
|
&:before {
|
||||||
|
.list-loader(20px, 20px, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.5));
|
||||||
|
left: 32px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #282e32;
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
color: @white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.active):hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
color: @white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.fesli-trigger {
|
||||||
|
.opacity(0.6);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.opacity(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .scrollbar-vertical {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fesli-trigger {
|
||||||
|
.opacity(0);
|
||||||
|
.transition(all);
|
||||||
|
.transition-duration(200ms);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 35px;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
background: url(../../img/more-h-light.svg) no-repeat top 20px left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
.scrollbar-vertical {
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
width: 4px;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
.transition(opacity);
|
||||||
|
.transition-duration(300ms);
|
||||||
|
|
||||||
|
div {
|
||||||
|
border-radius: 1px !important;
|
||||||
|
background-color: #6a6a6a !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Host
|
||||||
|
----------------------------*/
|
||||||
|
.fes-host {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background: @dark-gray;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 400;
|
||||||
|
width: @fe-sidebar-width;
|
||||||
|
padding: 20px;
|
||||||
|
.text-overflow();
|
||||||
|
|
||||||
|
& > i {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/*--------------------------
|
||||||
|
Base
|
||||||
|
----------------------------*/
|
||||||
|
@font-family-sans-serif : 'Lato', sans-serif;
|
||||||
|
@font-family-icon : 'fontAwesome';
|
||||||
|
@body-bg : #edecec;
|
||||||
|
@text-color : #8e8e8e;
|
||||||
|
@font-size-base : 15px;
|
||||||
|
@link-color : #46a5e0;
|
||||||
|
@link-hover-decoration : none;
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
File Explorer
|
||||||
|
----------------------------*/
|
||||||
|
@fe-sidebar-width : 320px;
|
||||||
|
@text-muted-color : #BDBDBD;
|
||||||
|
@text-strong-color : #333;
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Colors
|
||||||
|
----------------------------*/
|
||||||
|
@cyan : #2ED2FF;
|
||||||
|
@amber : #ffc107;
|
||||||
|
@red : #ff726f;
|
||||||
|
@grey : #f5f5f5;
|
||||||
|
@dark-blue : #0084d3;
|
||||||
|
@blue : #00a6f7;
|
||||||
|
@white : #ffffff;
|
||||||
|
@black : #1b1e25;
|
||||||
|
@blue : #50b2ff;
|
||||||
|
@light-blue : #c1d1e8;
|
||||||
|
@green : #33d46f;
|
||||||
|
@yellow : #FFC107;
|
||||||
|
@orange : #ffc155;
|
||||||
|
@purple : #9C27B0;
|
||||||
|
@teal : #009688;
|
||||||
|
@brown : #795548;
|
||||||
|
@blue-gray : #374952;
|
||||||
|
@dark-gray : #32393F;
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Dropdown
|
||||||
|
----------------------------*/
|
||||||
|
@dropdown-fallback-border : transparent;
|
||||||
|
@dropdown-border : transparent;
|
||||||
|
@dropdown-divider-bg : '';
|
||||||
|
@dropdown-link-hover-bg : rgba(0,0,0,0.05);
|
||||||
|
@dropdown-link-color : @text-color;
|
||||||
|
@dropdown-link-hover-color : #333;
|
||||||
|
@dropdown-link-disabled-color : #e4e4e4;
|
||||||
|
@dropdown-divider-bg : rgba(0,0,0,0.08);
|
||||||
|
@dropdown-link-active-color : #333;
|
||||||
|
@dropdown-link-active-bg : rgba(0, 0, 0, 0.075);
|
||||||
|
@dropdown-shadow : 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
|
||||||
|
/*--------------------------
|
||||||
|
Modal
|
||||||
|
----------------------------*/
|
||||||
|
@modal-content-fallback-border-color: transparent;
|
||||||
|
@modal-content-border-color: transparent;
|
||||||
|
@modal-backdrop-bg: rgba(0,0,0,0.1);
|
||||||
|
@modal-header-border-color: transparent;
|
||||||
|
@modal-title-line-height: transparent;
|
||||||
|
@modal-footer-border-color: transparent;
|
||||||
|
@modal-inner-padding: 30px 35px;
|
||||||
|
@modal-title-padding: 30px 35px 0px;
|
||||||
|
@modal-sm: 400px;
|
||||||
|
|
||||||
|
|
||||||
|
/*-------------------------
|
||||||
|
Buttons
|
||||||
|
--------------------------*/
|
||||||
|
@btn-border-radius-large: 2px;
|
||||||
|
@btn-border-radius-small: 2px;
|
||||||
|
@btn-border-radius-base: 2px;
|
||||||
|
|
||||||
|
|
||||||
|
/*-------------------------
|
||||||
|
Colors
|
||||||
|
--------------------------*/
|
||||||
|
@brand-primary: #2298f7;
|
||||||
|
@brand-success: #4CAF50;
|
||||||
|
@brand-info: #00BCD4;
|
||||||
|
@brand-warning: #FF9800;
|
||||||
|
@brand-danger: #FF5722;
|
||||||
|
|
||||||
|
|
||||||
|
/*-------------------------
|
||||||
|
Form
|
||||||
|
--------------------------*/
|
||||||
|
@input-border: #eee;
|
||||||
|
|
||||||
|
|
||||||
|
/*-------------------------
|
||||||
|
List
|
||||||
|
--------------------------*/
|
||||||
|
@list-row-selected-bg: #fbf2bf;
|
||||||
|
@list-row-even-bg: #fafafa;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*----------------------------
|
||||||
|
Bootstrap
|
||||||
|
-----------------------------*/
|
||||||
|
@import "../../node_modules/bootstrap/less/scaffolding.less";
|
||||||
|
@import "../../node_modules/bootstrap/less/variables.less";
|
||||||
|
@import "../../node_modules/bootstrap/less/grid.less";
|
||||||
|
@import "../../node_modules/bootstrap/less/mixins.less";
|
||||||
|
@import "../../node_modules/bootstrap/less/normalize.less";
|
||||||
|
@import "../../node_modules/bootstrap/less/dropdowns.less";
|
||||||
|
@import "../../node_modules/bootstrap/less/modals.less";
|
||||||
|
@import "../../node_modules/bootstrap/less/tooltip.less";
|
||||||
|
@import "../../node_modules/bootstrap/less/responsive-utilities.less";
|
||||||
|
|
||||||
|
|
||||||
|
/*----------------------------
|
||||||
|
App
|
||||||
|
-----------------------------*/
|
||||||
|
@import 'inc/mixin';
|
||||||
|
@import 'inc/variables';
|
||||||
|
@import 'inc/base';
|
||||||
|
@import 'inc/animate/animate';
|
||||||
|
@import 'inc/generics';
|
||||||
|
@import 'inc/font';
|
||||||
|
@import 'inc/form';
|
||||||
|
@import 'inc/buttons';
|
||||||
|
@import 'inc/misc';
|
||||||
|
@import 'inc/login';
|
||||||
|
@import 'inc/header';
|
||||||
|
@import 'inc/sidebar';
|
||||||
|
@import 'inc/list';
|
||||||
|
@import 'inc/file-explorer';
|
||||||
|
@import 'inc/ie-warning';
|
||||||
|
|
||||||
|
/*----------------------------
|
||||||
|
Boostrap
|
||||||
|
-----------------------------*/
|
||||||
|
@import 'inc/dropdown';
|
||||||
|
@import 'inc/alert';
|
||||||
|
@import 'inc/modal';
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var moment = require('moment')
|
||||||
|
var async = require('async')
|
||||||
|
var exec = require('child_process').exec
|
||||||
|
var fs = require('fs')
|
||||||
|
|
||||||
|
var isProduction = process.env.NODE_ENV == 'production' ? true : false
|
||||||
|
var assetsFileName = ''
|
||||||
|
var commitId = ''
|
||||||
|
var date = moment.utc()
|
||||||
|
var version = date.format('YYYY-MM-DDTHH:mm:ss') + 'Z'
|
||||||
|
var releaseTag = date.format('YYYY-MM-DDTHH-mm-ss') + 'Z'
|
||||||
|
var buildType = 'DEVELOPMENT'
|
||||||
|
if (process.env.MINIO_UI_BUILD) buildType = process.env.MINIO_UI_BUILD
|
||||||
|
|
||||||
|
rmDir = function(dirPath) {
|
||||||
|
try { var files = fs.readdirSync(dirPath); }
|
||||||
|
catch(e) { return; }
|
||||||
|
if (files.length > 0)
|
||||||
|
for (var i = 0; i < files.length; i++) {
|
||||||
|
var filePath = dirPath + '/' + files[i];
|
||||||
|
if (fs.statSync(filePath).isFile())
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
else
|
||||||
|
rmDir(filePath);
|
||||||
|
}
|
||||||
|
fs.rmdirSync(dirPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
async.waterfall([
|
||||||
|
function(cb) {
|
||||||
|
rmDir('production');
|
||||||
|
rmDir('dev');
|
||||||
|
var cmd = 'webpack -p --config webpack.production.config.js'
|
||||||
|
if (!isProduction) {
|
||||||
|
cmd = 'webpack';
|
||||||
|
}
|
||||||
|
console.log('Running', cmd)
|
||||||
|
exec(cmd, cb)
|
||||||
|
},
|
||||||
|
function(stdout, stderr, cb) {
|
||||||
|
if (isProduction) {
|
||||||
|
fs.renameSync('production/index_bundle.js',
|
||||||
|
'production/index_bundle-' + releaseTag + '.js')
|
||||||
|
} else {
|
||||||
|
fs.renameSync('dev/index_bundle.js',
|
||||||
|
'dev/index_bundle-' + releaseTag + '.js')
|
||||||
|
}
|
||||||
|
var cmd = 'git log --format="%H" -n1'
|
||||||
|
console.log('Running', cmd)
|
||||||
|
exec(cmd, cb)
|
||||||
|
},
|
||||||
|
function(stdout, stderr, cb) {
|
||||||
|
if (!stdout) throw new Error('commitId is empty')
|
||||||
|
commitId = stdout.replace('\n', '')
|
||||||
|
if (commitId.length !== 40) throw new Error('commitId invalid : ' + commitId)
|
||||||
|
assetsFileName = 'ui-assets.go';
|
||||||
|
var cmd = 'go-bindata-assetfs -pkg browser -nocompress=true production/...'
|
||||||
|
if (!isProduction) {
|
||||||
|
cmd = 'go-bindata-assetfs -pkg browser -nocompress=true dev/...'
|
||||||
|
}
|
||||||
|
console.log('Running', cmd)
|
||||||
|
exec(cmd, cb)
|
||||||
|
},
|
||||||
|
function(stdout, stderr, cb) {
|
||||||
|
var cmd = 'gofmt -s -w -l bindata_assetfs.go'
|
||||||
|
console.log('Running', cmd)
|
||||||
|
exec(cmd, cb)
|
||||||
|
},
|
||||||
|
function(stdout, stderr, cb) {
|
||||||
|
fs.renameSync('bindata_assetfs.go', assetsFileName)
|
||||||
|
fs.appendFileSync(assetsFileName, '\n')
|
||||||
|
fs.appendFileSync(assetsFileName, 'var UIReleaseTag = "' + buildType + '.' +
|
||||||
|
releaseTag + '"\n')
|
||||||
|
fs.appendFileSync(assetsFileName, 'var UICommitID = "' + commitId + '"\n')
|
||||||
|
fs.appendFileSync(assetsFileName, 'var UIVersion = "' + version + '"')
|
||||||
|
fs.appendFileSync(assetsFileName, '\n')
|
||||||
|
var contents;
|
||||||
|
if (isProduction) {
|
||||||
|
contents = fs.readFileSync(assetsFileName, 'utf8')
|
||||||
|
.replace(/_productionIndexHtml/g, '_productionIndexHTML')
|
||||||
|
.replace(/productionIndexHtmlBytes/g, 'productionIndexHTMLBytes')
|
||||||
|
.replace(/productionIndexHtml/g, 'productionIndexHTML')
|
||||||
|
.replace(/_productionIndex_bundleJs/g, '_productionIndexBundleJs')
|
||||||
|
.replace(/productionIndex_bundleJsBytes/g, 'productionIndexBundleJsBytes')
|
||||||
|
.replace(/productionIndex_bundleJs/g, 'productionIndexBundleJs')
|
||||||
|
.replace(/_productionJqueryUiMinJs/g, '_productionJqueryUIMinJs')
|
||||||
|
.replace(/productionJqueryUiMinJsBytes/g, 'productionJqueryUIMinJsBytes')
|
||||||
|
.replace(/productionJqueryUiMinJs/g, 'productionJqueryUIMinJs');
|
||||||
|
} else {
|
||||||
|
contents = fs.readFileSync(assetsFileName, 'utf8')
|
||||||
|
.replace(/_devIndexHtml/g, '_devIndexHTML')
|
||||||
|
.replace(/devIndexHtmlBytes/g, 'devIndexHTMLBytes')
|
||||||
|
.replace(/devIndexHtml/g, 'devIndexHTML')
|
||||||
|
.replace(/_devIndex_bundleJs/g, '_devIndexBundleJs')
|
||||||
|
.replace(/devIndex_bundleJsBytes/g, 'devIndexBundleJsBytes')
|
||||||
|
.replace(/devIndex_bundleJs/g, 'devIndexBundleJs')
|
||||||
|
.replace(/_devJqueryUiMinJs/g, '_devJqueryUIMinJs')
|
||||||
|
.replace(/devJqueryUiMinJsBytes/g, 'devJqueryUIMinJsBytes')
|
||||||
|
.replace(/devJqueryUiMinJs/g, 'devJqueryUIMinJs');
|
||||||
|
}
|
||||||
|
contents = contents.replace(/MINIO_UI_VERSION/g, version)
|
||||||
|
contents = contents.replace(/index_bundle.js/g, 'index_bundle-' + releaseTag + '.js')
|
||||||
|
|
||||||
|
fs.writeFileSync(assetsFileName, contents, 'utf8')
|
||||||
|
console.log('UI assets file :', assetsFileName)
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
], function(err) {
|
||||||
|
if (err) return console.log(err)
|
||||||
|
})
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
var webpack = require('webpack');
|
||||||
|
|
||||||
|
module.exports = function (config) {
|
||||||
|
config.set({
|
||||||
|
browsers: [ process.env.CONTINUOUS_INTEGRATION ? 'Firefox' : 'Chrome' ],
|
||||||
|
singleRun: true,
|
||||||
|
frameworks: [ 'mocha' ],
|
||||||
|
files: [
|
||||||
|
'tests.webpack.js'
|
||||||
|
],
|
||||||
|
preprocessors: {
|
||||||
|
'tests.webpack.js': [ 'webpack' ]
|
||||||
|
},
|
||||||
|
reporters: [ 'dots' ],
|
||||||
|
webpack: {
|
||||||
|
module: {
|
||||||
|
loaders: [{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: /(node_modules|bower_components)/,
|
||||||
|
loader: 'babel',
|
||||||
|
query: {
|
||||||
|
presets: ['react', 'es2015']
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
test: /\.less$/,
|
||||||
|
loader: 'style!css!less'
|
||||||
|
}, {
|
||||||
|
test: /\.css$/,
|
||||||
|
loader: 'style!css'
|
||||||
|
}, {
|
||||||
|
test: /\.(eot|woff|woff2|ttf|svg|png)/,
|
||||||
|
loader: 'url'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
webpackServer: {
|
||||||
|
noInfo: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"name": "browser",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Minio Browser",
|
||||||
|
"scripts": {
|
||||||
|
"test": "karma start",
|
||||||
|
"dev": "NODE_ENV=dev webpack-dev-server --devtool eval --progress --colors --hot --content-base dev",
|
||||||
|
"build": "NODE_ENV=dev node build.js",
|
||||||
|
"release": "NODE_ENV=production MINIO_UI_BUILD=RELEASE node build.js",
|
||||||
|
"format": "esformatter -i 'app/**/*.js'"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/minio/minio"
|
||||||
|
},
|
||||||
|
"author": "Minio Inc",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/minio/minio/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/minio/minio",
|
||||||
|
"devDependencies": {
|
||||||
|
"async": "^1.5.2",
|
||||||
|
"babel-cli": "^6.14.0",
|
||||||
|
"babel-core": "^6.14.0",
|
||||||
|
"babel-loader": "^6.2.5",
|
||||||
|
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
|
||||||
|
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
||||||
|
"babel-polyfill": "^6.23.0",
|
||||||
|
"babel-preset-es2015": "^6.14.0",
|
||||||
|
"babel-preset-react": "^6.11.1",
|
||||||
|
"babel-register": "^6.14.0",
|
||||||
|
"copy-webpack-plugin": "^0.3.3",
|
||||||
|
"css-loader": "^0.23.1",
|
||||||
|
"esformatter": "^0.10.0",
|
||||||
|
"esformatter-jsx": "^7.4.1",
|
||||||
|
"esformatter-jsx-ignore": "^1.0.6",
|
||||||
|
"html-webpack-plugin": "^2.22.0",
|
||||||
|
"json-loader": "^0.5.4",
|
||||||
|
"karma": "^0.13.22",
|
||||||
|
"karma-chrome-launcher": "^0.2.3",
|
||||||
|
"karma-cli": "^0.1.2",
|
||||||
|
"karma-firefox-launcher": "^0.1.7",
|
||||||
|
"karma-mocha": "^0.2.2",
|
||||||
|
"karma-webpack": "^1.7.0",
|
||||||
|
"less": "^2.7.1",
|
||||||
|
"less-loader": "^2.2.3",
|
||||||
|
"mocha": "^2.5.3",
|
||||||
|
"purifycss-webpack-plugin": "^2.0.3",
|
||||||
|
"style-loader": "^0.13.1",
|
||||||
|
"url-loader": "^0.5.7",
|
||||||
|
"webpack-dev-server": "^1.14.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap": "^3.3.6",
|
||||||
|
"classnames": "^2.2.3",
|
||||||
|
"expect": "^1.20.2",
|
||||||
|
"font-awesome": "^4.7.0",
|
||||||
|
"history": "^1.17.0",
|
||||||
|
"humanize": "0.0.9",
|
||||||
|
"json-loader": "^0.5.4",
|
||||||
|
"local-storage-fallback": "^1.3.0",
|
||||||
|
"mime-db": "^1.25.0",
|
||||||
|
"mime-types": "^2.1.13",
|
||||||
|
"moment": "^2.15.1",
|
||||||
|
"react": "^0.14.8",
|
||||||
|
"react-addons-test-utils": "^0.14.8",
|
||||||
|
"react-bootstrap": "^0.28.5",
|
||||||
|
"react-copy-to-clipboard": "^4.2.3",
|
||||||
|
"react-custom-scrollbars": "^2.2.2",
|
||||||
|
"react-dom": "^0.14.6",
|
||||||
|
"react-dropzone": "^3.5.3",
|
||||||
|
"react-infinite-scroller": "^1.0.6",
|
||||||
|
"react-onclickout": "2.0.4",
|
||||||
|
"react-redux": "^4.4.5",
|
||||||
|
"react-router": "^2.8.1",
|
||||||
|
"redux": "^3.6.0",
|
||||||
|
"redux-thunk": "^1.0.3",
|
||||||
|
"superagent": "^1.8.4",
|
||||||
|
"superagent-es6-promise": "^1.0.0",
|
||||||
|
"webpack": "^1.12.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
var context = require.context('./app', true, /-test\.js$/);
|
||||||
|
context.keys().forEach(context);
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Minio Cloud Storage (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var webpack = require('webpack')
|
||||||
|
var path = require('path')
|
||||||
|
var CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||||
|
var purify = require("purifycss-webpack-plugin")
|
||||||
|
|
||||||
|
var exports = {
|
||||||
|
context: __dirname,
|
||||||
|
entry: [
|
||||||
|
path.resolve(__dirname, 'app/index.js')
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'dev'),
|
||||||
|
filename: 'index_bundle.js',
|
||||||
|
publicPath: '/minio/'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: /(node_modules|bower_components)/,
|
||||||
|
loader: 'babel',
|
||||||
|
query: {
|
||||||
|
presets: ['react', 'es2015']
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
test: /\.less$/,
|
||||||
|
loader: 'style!css!less'
|
||||||
|
}, {
|
||||||
|
test: /\.json$/,
|
||||||
|
loader: 'json-loader'
|
||||||
|
},{
|
||||||
|
test: /\.css$/,
|
||||||
|
loader: 'style!css'
|
||||||
|
}, {
|
||||||
|
test: /\.(eot|woff|woff2|ttf|svg|png)/,
|
||||||
|
loader: 'url'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
node:{
|
||||||
|
fs:'empty'
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
historyApiFallback: {
|
||||||
|
index: '/minio/'
|
||||||
|
},
|
||||||
|
proxy: {
|
||||||
|
'/minio/webrpc': {
|
||||||
|
target: 'http://localhost:9000',
|
||||||
|
secure: false
|
||||||
|
},
|
||||||
|
'/minio/upload/*': {
|
||||||
|
target: 'http://localhost:9000',
|
||||||
|
secure: false
|
||||||
|
},
|
||||||
|
'/minio/download/*': {
|
||||||
|
target: 'http://localhost:9000',
|
||||||
|
secure: false
|
||||||
|
},
|
||||||
|
'/minio/zip': {
|
||||||
|
target: 'http://localhost:9000',
|
||||||
|
secure: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new CopyWebpackPlugin([
|
||||||
|
{from: 'app/css/loader.css'},
|
||||||
|
{from: 'app/img/favicon.ico'},
|
||||||
|
{from: 'app/img/browsers/chrome.png'},
|
||||||
|
{from: 'app/img/browsers/firefox.png'},
|
||||||
|
{from: 'app/img/browsers/safari.png'},
|
||||||
|
{from: 'app/img/logo.svg'},
|
||||||
|
{from: 'app/index.html'}
|
||||||
|
]),
|
||||||
|
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
|
||||||
|
new purify({
|
||||||
|
basePath: __dirname,
|
||||||
|
paths: [
|
||||||
|
"app/index.html",
|
||||||
|
"app/js/*.js"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'dev') {
|
||||||
|
exports.entry = [
|
||||||
|
'webpack/hot/dev-server',
|
||||||
|
'webpack-dev-server/client?http://localhost:8080',
|
||||||
|
path.resolve(__dirname, 'app/index.js')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Isomorphic Javascript library for Minio Browser JSON-RPC API, (C) 2016 Minio, Inc.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var webpack = require('webpack')
|
||||||
|
var path = require('path')
|
||||||
|
var CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||||
|
var purify = require("purifycss-webpack-plugin")
|
||||||
|
|
||||||
|
var exports = {
|
||||||
|
context: __dirname,
|
||||||
|
entry: [
|
||||||
|
path.resolve(__dirname, 'app/index.js')
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'production'),
|
||||||
|
filename: 'index_bundle.js'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
loaders: [{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: /(node_modules|bower_components)/,
|
||||||
|
loader: 'babel',
|
||||||
|
query: {
|
||||||
|
presets: ['react', 'es2015']
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
test: /\.less$/,
|
||||||
|
loader: 'style!css!less'
|
||||||
|
}, {
|
||||||
|
test: /\.json$/,
|
||||||
|
loader: 'json-loader'
|
||||||
|
}, {
|
||||||
|
test: /\.css$/,
|
||||||
|
loader: 'style!css'
|
||||||
|
}, {
|
||||||
|
test: /\.(eot|woff|woff2|ttf|svg|png)/,
|
||||||
|
loader: 'url'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
node:{
|
||||||
|
fs:'empty'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new CopyWebpackPlugin([
|
||||||
|
{from: 'app/css/loader.css'},
|
||||||
|
{from: 'app/img/favicon.ico'},
|
||||||
|
{from: 'app/img/browsers/chrome.png'},
|
||||||
|
{from: 'app/img/browsers/firefox.png'},
|
||||||
|
{from: 'app/img/browsers/safari.png'},
|
||||||
|
{from: 'app/img/logo.svg'},
|
||||||
|
{from: 'app/index.html'}
|
||||||
|
]),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.NODE_ENV': '"production"'
|
||||||
|
}),
|
||||||
|
new webpack.ContextReplacementPlugin(/moment[\\\/]locale$/, /^\.\/(en)$/),
|
||||||
|
new purify({
|
||||||
|
basePath: __dirname,
|
||||||
|
paths: [
|
||||||
|
"app/index.html",
|
||||||
|
"app/js/*.js"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'dev') {
|
||||||
|
exports.entry = [
|
||||||
|
'webpack/hot/dev-server',
|
||||||
|
'webpack-dev-server/client?http://localhost:8080',
|
||||||
|
path.resolve(__dirname, 'app/index.js')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports
|
||||||
@@ -1,447 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
mux "github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
|
||||||
// Enforces bucket policies for a bucket for a given tatusaction.
|
|
||||||
func enforceBucketPolicy(bucket string, action string, reqURL *url.URL) (s3Error APIErrorCode) {
|
|
||||||
if !IsValidBucketName(bucket) {
|
|
||||||
return ErrInvalidBucketName
|
|
||||||
}
|
|
||||||
// Fetch bucket policy, if policy is not set return access denied.
|
|
||||||
policy := globalBucketPolicies.GetBucketPolicy(bucket)
|
|
||||||
if policy == nil {
|
|
||||||
return ErrAccessDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct resource in 'arn:aws:s3:::examplebucket/object' format.
|
|
||||||
resource := AWSResourcePrefix + strings.TrimPrefix(reqURL.Path, "/")
|
|
||||||
|
|
||||||
// Get conditions for policy verification.
|
|
||||||
conditions := make(map[string]string)
|
|
||||||
for queryParam := range reqURL.Query() {
|
|
||||||
conditions[queryParam] = reqURL.Query().Get(queryParam)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate action, resource and conditions with current policy statements.
|
|
||||||
if !bucketPolicyEvalStatements(action, resource, conditions, policy.Statements) {
|
|
||||||
return ErrAccessDenied
|
|
||||||
}
|
|
||||||
return ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBucketLocationHandler - GET Bucket location.
|
|
||||||
// -------------------------
|
|
||||||
// This operation returns bucket location.
|
|
||||||
func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
switch getRequestAuthType(r) {
|
|
||||||
default:
|
|
||||||
// For all unknown auth types return error.
|
|
||||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
|
||||||
return
|
|
||||||
case authTypeAnonymous:
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
|
||||||
if s3Error := enforceBucketPolicy(bucket, "s3:GetBucketLocation", r.URL); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case authTypeSigned, authTypePresigned:
|
|
||||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := api.ObjectAPI.GetBucketInfo(bucket); err != nil {
|
|
||||||
errorIf(err, "Unable to fetch bucket info.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate response.
|
|
||||||
encodedSuccessResponse := encodeResponse(LocationResponse{})
|
|
||||||
// Get current region.
|
|
||||||
region := serverConfig.GetRegion()
|
|
||||||
if region != "us-east-1" {
|
|
||||||
encodedSuccessResponse = encodeResponse(LocationResponse{
|
|
||||||
Location: region,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setCommonHeaders(w) // Write headers.
|
|
||||||
writeSuccessResponse(w, encodedSuccessResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListMultipartUploadsHandler - GET Bucket (List Multipart uploads)
|
|
||||||
// -------------------------
|
|
||||||
// This operation lists in-progress multipart uploads. An in-progress
|
|
||||||
// multipart upload is a multipart upload that has been initiated,
|
|
||||||
// using the Initiate Multipart Upload request, but has not yet been
|
|
||||||
// completed or aborted. This operation returns at most 1,000 multipart
|
|
||||||
// uploads in the response.
|
|
||||||
//
|
|
||||||
func (api objectAPIHandlers) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
switch getRequestAuthType(r) {
|
|
||||||
default:
|
|
||||||
// For all unknown auth types return error.
|
|
||||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
|
||||||
return
|
|
||||||
case authTypeAnonymous:
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/mpuAndPermissions.html
|
|
||||||
if s3Error := enforceBucketPolicy(bucket, "s3:ListBucketMultipartUploads", r.URL); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case authTypePresigned, authTypeSigned:
|
|
||||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix, keyMarker, uploadIDMarker, delimiter, maxUploads, _ := getBucketMultipartResources(r.URL.Query())
|
|
||||||
if maxUploads < 0 {
|
|
||||||
writeErrorResponse(w, r, ErrInvalidMaxUploads, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if keyMarker != "" {
|
|
||||||
// Marker not common with prefix is not implemented.
|
|
||||||
if !strings.HasPrefix(keyMarker, prefix) {
|
|
||||||
writeErrorResponse(w, r, ErrNotImplemented, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listMultipartsInfo, err := api.ObjectAPI.ListMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to list multipart uploads.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// generate response
|
|
||||||
response := generateListMultipartUploadsResponse(bucket, listMultipartsInfo)
|
|
||||||
encodedSuccessResponse := encodeResponse(response)
|
|
||||||
// write headers.
|
|
||||||
setCommonHeaders(w)
|
|
||||||
// write success response.
|
|
||||||
writeSuccessResponse(w, encodedSuccessResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListBucketsHandler - GET Service
|
|
||||||
// -----------
|
|
||||||
// This implementation of the GET operation returns a list of all buckets
|
|
||||||
// owned by the authenticated sender of the request.
|
|
||||||
func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// List buckets does not support bucket policies, no need to enforce it.
|
|
||||||
if s3Error := checkAuth(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bucketsInfo, err := api.ObjectAPI.ListBuckets()
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to list buckets.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate response.
|
|
||||||
response := generateListBucketsResponse(bucketsInfo)
|
|
||||||
encodedSuccessResponse := encodeResponse(response)
|
|
||||||
// Write headers.
|
|
||||||
setCommonHeaders(w)
|
|
||||||
// Write response.
|
|
||||||
writeSuccessResponse(w, encodedSuccessResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteMultipleObjectsHandler - deletes multiple objects.
|
|
||||||
func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
switch getRequestAuthType(r) {
|
|
||||||
default:
|
|
||||||
// For all unknown auth types return error.
|
|
||||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
|
||||||
return
|
|
||||||
case authTypeAnonymous:
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
|
||||||
if s3Error := enforceBucketPolicy(bucket, "s3:DeleteObject", r.URL); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case authTypePresigned, authTypeSigned:
|
|
||||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content-Length is required and should be non-zero
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
|
||||||
if r.ContentLength <= 0 {
|
|
||||||
writeErrorResponse(w, r, ErrMissingContentLength, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content-Md5 is requied should be set
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
|
||||||
if _, ok := r.Header["Content-Md5"]; !ok {
|
|
||||||
writeErrorResponse(w, r, ErrMissingContentMD5, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate incoming content length bytes.
|
|
||||||
deleteXMLBytes := make([]byte, r.ContentLength)
|
|
||||||
|
|
||||||
// Read incoming body XML bytes.
|
|
||||||
if _, err := io.ReadFull(r.Body, deleteXMLBytes); err != nil {
|
|
||||||
errorIf(err, "Unable to read HTTP body.")
|
|
||||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal list of keys to be deleted.
|
|
||||||
deleteObjects := &DeleteObjectsRequest{}
|
|
||||||
if err := xml.Unmarshal(deleteXMLBytes, deleteObjects); err != nil {
|
|
||||||
errorIf(err, "Unable to unmarshal delete objects request XML.")
|
|
||||||
writeErrorResponse(w, r, ErrMalformedXML, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var deleteErrors []DeleteError
|
|
||||||
var deletedObjects []ObjectIdentifier
|
|
||||||
// Loop through all the objects and delete them sequentially.
|
|
||||||
for _, object := range deleteObjects.Objects {
|
|
||||||
err := api.ObjectAPI.DeleteObject(bucket, object.ObjectName)
|
|
||||||
if err == nil {
|
|
||||||
deletedObjects = append(deletedObjects, ObjectIdentifier{
|
|
||||||
ObjectName: object.ObjectName,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
errorIf(err, "Unable to delete object.")
|
|
||||||
deleteErrors = append(deleteErrors, DeleteError{
|
|
||||||
Code: errorCodeResponse[toAPIErrorCode(err)].Code,
|
|
||||||
Message: errorCodeResponse[toAPIErrorCode(err)].Description,
|
|
||||||
Key: object.ObjectName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Generate response
|
|
||||||
response := generateMultiDeleteResponse(deleteObjects.Quiet, deletedObjects, deleteErrors)
|
|
||||||
encodedSuccessResponse := encodeResponse(response)
|
|
||||||
// Write headers
|
|
||||||
setCommonHeaders(w)
|
|
||||||
// Write success response.
|
|
||||||
writeSuccessResponse(w, encodedSuccessResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutBucketHandler - PUT Bucket
|
|
||||||
// ----------
|
|
||||||
// This implementation of the PUT operation creates a new bucket for authenticated request
|
|
||||||
func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// PutBucket does not support policies, use checkAuth to validate signature.
|
|
||||||
if s3Error := checkAuth(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
// Validate if incoming location constraint is valid, reject
|
|
||||||
// requests which do not follow valid region requirements.
|
|
||||||
if s3Error := isValidLocationConstraint(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proceed to creating a bucket.
|
|
||||||
err := api.ObjectAPI.MakeBucket(bucket)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to create a bucket.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Make sure to add Location information here only for bucket
|
|
||||||
w.Header().Set("Location", getLocation(r))
|
|
||||||
writeSuccessResponse(w, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostPolicyBucketHandler - POST policy
|
|
||||||
// ----------
|
|
||||||
// This implementation of the POST operation handles object creation with a specified
|
|
||||||
// signature policy in multipart/form-data
|
|
||||||
func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Here the parameter is the size of the form data that should
|
|
||||||
// be loaded in memory, the remaining being put in temporary files.
|
|
||||||
reader, err := r.MultipartReader()
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to initialize multipart reader.")
|
|
||||||
writeErrorResponse(w, r, ErrMalformedPOSTRequest, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fileBody, fileName, formValues, err := extractPostPolicyFormValues(reader)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to parse form values.")
|
|
||||||
writeErrorResponse(w, r, ErrMalformedPOSTRequest, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bucket := mux.Vars(r)["bucket"]
|
|
||||||
formValues["Bucket"] = bucket
|
|
||||||
object := formValues["Key"]
|
|
||||||
|
|
||||||
if fileName != "" && strings.Contains(object, "${filename}") {
|
|
||||||
// S3 feature to replace ${filename} found in Key form field
|
|
||||||
// by the filename attribute passed in multipart
|
|
||||||
object = strings.Replace(object, "${filename}", fileName, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify policy signature.
|
|
||||||
apiErr := doesPolicySignatureMatch(formValues)
|
|
||||||
if apiErr != ErrNone {
|
|
||||||
writeErrorResponse(w, r, apiErr, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if apiErr = checkPostPolicy(formValues); apiErr != ErrNone {
|
|
||||||
writeErrorResponse(w, r, apiErr, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save metadata.
|
|
||||||
metadata := make(map[string]string)
|
|
||||||
// Nothing to store right now.
|
|
||||||
|
|
||||||
md5Sum, err := api.ObjectAPI.PutObject(bucket, object, -1, fileBody, metadata)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to create object.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if md5Sum != "" {
|
|
||||||
w.Header().Set("ETag", "\""+md5Sum+"\"")
|
|
||||||
}
|
|
||||||
encodedSuccessResponse := encodeResponse(PostResponse{
|
|
||||||
Location: getObjectLocation(bucket, object), // TODO Full URL is preferred
|
|
||||||
Bucket: bucket,
|
|
||||||
Key: object,
|
|
||||||
ETag: md5Sum,
|
|
||||||
})
|
|
||||||
|
|
||||||
setCommonHeaders(w)
|
|
||||||
|
|
||||||
writeSuccessResponse(w, encodedSuccessResponse)
|
|
||||||
|
|
||||||
// Fetch object info for notifications.
|
|
||||||
objInfo, err := api.ObjectAPI.GetObjectInfo(bucket, object)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to fetch object info for \"%s\"", path.Join(bucket, object))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if eventN.IsBucketNotificationSet(bucket) {
|
|
||||||
// Notify object created event.
|
|
||||||
eventNotify(eventData{
|
|
||||||
Type: ObjectCreatedPost,
|
|
||||||
Bucket: bucket,
|
|
||||||
ObjInfo: objInfo,
|
|
||||||
ReqParams: map[string]string{
|
|
||||||
"sourceIPAddress": r.RemoteAddr,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HeadBucketHandler - HEAD Bucket
|
|
||||||
// ----------
|
|
||||||
// This operation is useful to determine if a bucket exists.
|
|
||||||
// The operation returns a 200 OK if the bucket exists and you
|
|
||||||
// have permission to access it. Otherwise, the operation might
|
|
||||||
// return responses such as 404 Not Found and 403 Forbidden.
|
|
||||||
func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
switch getRequestAuthType(r) {
|
|
||||||
default:
|
|
||||||
// For all unknown auth types return error.
|
|
||||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
|
||||||
return
|
|
||||||
case authTypeAnonymous:
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/using-with-s3-actions.html
|
|
||||||
if s3Error := enforceBucketPolicy(bucket, "s3:ListBucket", r.URL); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case authTypePresigned, authTypeSigned:
|
|
||||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := api.ObjectAPI.GetBucketInfo(bucket); err != nil {
|
|
||||||
errorIf(err, "Unable to fetch bucket info.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeSuccessResponse(w, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBucketHandler - Delete bucket
|
|
||||||
func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// DeleteBucket does not support bucket policies, use checkAuth to validate signature.
|
|
||||||
if s3Error := checkAuth(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
// Attempt to delete bucket.
|
|
||||||
if err := api.ObjectAPI.DeleteBucket(bucket); err != nil {
|
|
||||||
errorIf(err, "Unable to delete a bucket.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete bucket access policy, if present - ignore any errors.
|
|
||||||
removeBucketPolicy(bucket, api.ObjectAPI)
|
|
||||||
|
|
||||||
// Delete notification config, if present - ignore any errors.
|
|
||||||
removeNotificationConfig(bucket, api.ObjectAPI)
|
|
||||||
|
|
||||||
// Write success response.
|
|
||||||
writeSuccessNoContent(w)
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
bucketConfigPrefix = "buckets"
|
|
||||||
bucketNotificationConfig = "notification.xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetBucketNotificationHandler - This implementation of the GET
|
|
||||||
// operation uses the notification subresource to return the
|
|
||||||
// notification configuration of a bucket. If notifications are
|
|
||||||
// not enabled on the bucket, the operation returns an empty
|
|
||||||
// NotificationConfiguration element.
|
|
||||||
func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Validate request authorization.
|
|
||||||
if s3Error := checkAuth(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
// Attempt to successfully load notification config.
|
|
||||||
nConfig, err := loadNotificationConfig(bucket, api.ObjectAPI)
|
|
||||||
if err != nil && err != errNoSuchNotifications {
|
|
||||||
errorIf(err, "Unable to read notification configuration.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// For no notifications we write a dummy XML.
|
|
||||||
if err == errNoSuchNotifications {
|
|
||||||
// Complies with the s3 behavior in this regard.
|
|
||||||
nConfig = ¬ificationConfig{}
|
|
||||||
}
|
|
||||||
notificationBytes, err := xml.Marshal(nConfig)
|
|
||||||
if err != nil {
|
|
||||||
// For any marshalling failure.
|
|
||||||
errorIf(err, "Unable to marshal notification configuration into XML.", err)
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Success.
|
|
||||||
writeSuccessResponse(w, notificationBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutBucketNotificationHandler - Minio notification feature enables
|
|
||||||
// you to receive notifications when certain events happen in your bucket.
|
|
||||||
// Using this API, you can replace an existing notification configuration.
|
|
||||||
// The configuration is an XML file that defines the event types that you
|
|
||||||
// want Minio to publish and the destination where you want Minio to publish
|
|
||||||
// an event notification when it detects an event of the specified type.
|
|
||||||
// By default, your bucket has no event notifications configured. That is,
|
|
||||||
// the notification configuration will be an empty NotificationConfiguration.
|
|
||||||
func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Validate request authorization.
|
|
||||||
if s3Error := checkAuth(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
_, err := api.ObjectAPI.GetBucketInfo(bucket)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to bucket info.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If Content-Length is unknown or zero, deny the request. PutBucketNotification
|
|
||||||
// always needs a Content-Length if incoming request is not chunked.
|
|
||||||
if !contains(r.TransferEncoding, "chunked") {
|
|
||||||
if r.ContentLength == -1 {
|
|
||||||
writeErrorResponse(w, r, ErrMissingContentLength, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reads the incoming notification configuration.
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
var bufferSize int64
|
|
||||||
if r.ContentLength >= 0 {
|
|
||||||
bufferSize, err = io.CopyN(&buffer, r.Body, r.ContentLength)
|
|
||||||
} else {
|
|
||||||
bufferSize, err = io.Copy(&buffer, r.Body)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to read incoming body.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var notificationCfg notificationConfig
|
|
||||||
// Unmarshal notification bytes.
|
|
||||||
notificationConfigBytes := buffer.Bytes()
|
|
||||||
if err = xml.Unmarshal(notificationConfigBytes, ¬ificationCfg); err != nil {
|
|
||||||
errorIf(err, "Unable to parse notification configuration XML.")
|
|
||||||
writeErrorResponse(w, r, ErrMalformedXML, r.URL.Path)
|
|
||||||
return
|
|
||||||
} // Successfully marshalled notification configuration.
|
|
||||||
|
|
||||||
// Validate unmarshalled bucket notification configuration.
|
|
||||||
if s3Error := validateNotificationConfig(notificationCfg); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proceed to save notification configuration.
|
|
||||||
notificationConfigPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
|
||||||
_, err = api.ObjectAPI.PutObject(minioMetaBucket, notificationConfigPath, bufferSize, bytes.NewReader(buffer.Bytes()), nil)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to write bucket notification configuration.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set bucket notification config.
|
|
||||||
eventN.SetBucketNotificationConfig(bucket, ¬ificationCfg)
|
|
||||||
|
|
||||||
// Success.
|
|
||||||
writeSuccessResponse(w, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeNotification marshals notification message before writing to client.
|
|
||||||
func writeNotification(w http.ResponseWriter, notification map[string][]NotificationEvent) error {
|
|
||||||
// Invalid response writer.
|
|
||||||
if w == nil {
|
|
||||||
return errInvalidArgument
|
|
||||||
}
|
|
||||||
// Invalid notification input.
|
|
||||||
if notification == nil {
|
|
||||||
return errInvalidArgument
|
|
||||||
}
|
|
||||||
// Marshal notification data into XML and write to client.
|
|
||||||
notificationBytes, err := json.Marshal(¬ification)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Add additional CRLF characters for client to
|
|
||||||
// differentiate the individual events properly.
|
|
||||||
_, err = w.Write(append(notificationBytes, crlf...))
|
|
||||||
// Make sure we have flushed, this would set Transfer-Encoding: chunked.
|
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CRLF character used for chunked transfer in accordance with HTTP standards.
|
|
||||||
var crlf = []byte("\r\n")
|
|
||||||
|
|
||||||
// sendBucketNotification - writes notification back to client on the response writer
|
|
||||||
// for each notification input, otherwise writes whitespace characters periodically
|
|
||||||
// to keep the connection active. Each notification messages are terminated by CRLF
|
|
||||||
// character. Upon any error received on response writer the for loop exits.
|
|
||||||
//
|
|
||||||
// TODO - do not log for all errors.
|
|
||||||
func sendBucketNotification(w http.ResponseWriter, arnListenerCh <-chan []NotificationEvent) {
|
|
||||||
var dummyEvents = map[string][]NotificationEvent{"Records": nil}
|
|
||||||
// Continuously write to client either timely empty structures
|
|
||||||
// every 5 seconds, or return back the notifications.
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case events := <-arnListenerCh:
|
|
||||||
if err := writeNotification(w, map[string][]NotificationEvent{"Records": events}); err != nil {
|
|
||||||
errorIf(err, "Unable to write notification to client.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
if err := writeNotification(w, dummyEvents); err != nil {
|
|
||||||
errorIf(err, "Unable to write notification to client.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenBucketNotificationHandler - list bucket notifications.
|
|
||||||
func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Validate request authorization.
|
|
||||||
if s3Error := checkAuth(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
// Get notification ARN.
|
|
||||||
topicARN := r.URL.Query().Get("notificationARN")
|
|
||||||
if topicARN == "" {
|
|
||||||
writeErrorResponse(w, r, ErrARNNotification, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate if bucket exists.
|
|
||||||
_, err := api.ObjectAPI.GetBucketInfo(bucket)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to bucket info.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationCfg := eventN.GetBucketNotificationConfig(bucket)
|
|
||||||
if notificationCfg == nil {
|
|
||||||
writeErrorResponse(w, r, ErrARNNotification, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set SNS notifications only if special "listen" sns is set in bucket
|
|
||||||
// notification configs.
|
|
||||||
if !isMinioSNSConfigured(topicARN, notificationCfg.TopicConfigs) {
|
|
||||||
writeErrorResponse(w, r, ErrARNNotification, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add all common headers.
|
|
||||||
setCommonHeaders(w)
|
|
||||||
|
|
||||||
// Create a new notification event channel.
|
|
||||||
nEventCh := make(chan []NotificationEvent)
|
|
||||||
// Close the listener channel.
|
|
||||||
defer close(nEventCh)
|
|
||||||
|
|
||||||
// Set sns target.
|
|
||||||
eventN.SetSNSTarget(topicARN, nEventCh)
|
|
||||||
// Remove sns listener after the writer has closed or the client disconnected.
|
|
||||||
defer eventN.RemoveSNSTarget(topicARN, nEventCh)
|
|
||||||
|
|
||||||
// Start sending bucket notifications.
|
|
||||||
sendBucketNotification(w, nEventCh)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes notification.xml for a given bucket, only used during DeleteBucket.
|
|
||||||
func removeNotificationConfig(bucket string, objAPI ObjectLayer) error {
|
|
||||||
// Verify bucket is valid.
|
|
||||||
if !IsValidBucketName(bucket) {
|
|
||||||
return BucketNameInvalid{Bucket: bucket}
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationConfigPath := path.Join(bucketConfigPrefix, bucket, bucketNotificationConfig)
|
|
||||||
return objAPI.DeleteObject(minioMetaBucket, notificationConfigPath)
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Implement a dummy flush writer.
|
|
||||||
type flushWriter struct {
|
|
||||||
io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush writer is a dummy writer compatible with http.Flusher and http.ResponseWriter.
|
|
||||||
func (f *flushWriter) Flush() {}
|
|
||||||
func (f *flushWriter) Write(b []byte) (n int, err error) { return f.Writer.Write(b) }
|
|
||||||
func (f *flushWriter) Header() http.Header { return http.Header{} }
|
|
||||||
func (f *flushWriter) WriteHeader(code int) {}
|
|
||||||
|
|
||||||
func newFlushWriter(writer io.Writer) *flushWriter {
|
|
||||||
return &flushWriter{writer}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests write notification code.
|
|
||||||
func TestWriteNotification(t *testing.T) {
|
|
||||||
// Initialize a new test config.
|
|
||||||
root, err := newTestConfig("us-east-1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unable to initialize test config %s", err)
|
|
||||||
}
|
|
||||||
defer removeAll(root)
|
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
// Collection of test cases for each event writer.
|
|
||||||
testCases := []struct {
|
|
||||||
writer *flushWriter
|
|
||||||
event map[string][]NotificationEvent
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
// Invalid input argument with writer `nil` - Test - 1
|
|
||||||
{
|
|
||||||
writer: nil,
|
|
||||||
event: nil,
|
|
||||||
err: errInvalidArgument,
|
|
||||||
},
|
|
||||||
// Invalid input argument with event `nil` - Test - 2
|
|
||||||
{
|
|
||||||
writer: newFlushWriter(ioutil.Discard),
|
|
||||||
event: nil,
|
|
||||||
err: errInvalidArgument,
|
|
||||||
},
|
|
||||||
// Unmarshal and write, validate last 5 bytes. - Test - 3
|
|
||||||
{
|
|
||||||
writer: newFlushWriter(&buffer),
|
|
||||||
event: map[string][]NotificationEvent{
|
|
||||||
"Records": {newNotificationEvent(eventData{
|
|
||||||
Type: ObjectCreatedPut,
|
|
||||||
Bucket: "testbucket",
|
|
||||||
ObjInfo: ObjectInfo{
|
|
||||||
Name: "key",
|
|
||||||
},
|
|
||||||
ReqParams: map[string]string{
|
|
||||||
"ip": "10.1.10.1",
|
|
||||||
}}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
err: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// Validates all the testcases for writing notification.
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
err := writeNotification(testCase.writer, testCase.event)
|
|
||||||
if err != testCase.err {
|
|
||||||
t.Errorf("Unable to write notification %s", err)
|
|
||||||
}
|
|
||||||
// Validates if the ending string has 'crlf'
|
|
||||||
if err == nil && !bytes.HasSuffix(buffer.Bytes(), crlf) {
|
|
||||||
buf := buffer.Bytes()[buffer.Len()-5 : 0]
|
|
||||||
t.Errorf("Invalid suffix found from the writer last 5 bytes %s, expected `\r\n`", string(buf))
|
|
||||||
}
|
|
||||||
// Not printing 'buf' on purpose, validates look for string '10.1.10.1'.
|
|
||||||
if err == nil && !bytes.Contains(buffer.Bytes(), []byte("10.1.10.1")) {
|
|
||||||
// Enable when debugging)
|
|
||||||
// fmt.Println(string(buffer.Bytes()))
|
|
||||||
t.Errorf("Requested content couldn't be found, expected `10.1.10.1`")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
mux "github.com/gorilla/mux"
|
|
||||||
"github.com/minio/minio/pkg/wildcard"
|
|
||||||
)
|
|
||||||
|
|
||||||
// maximum supported access policy size.
|
|
||||||
const maxAccessPolicySize = 20 * 1024 // 20KiB.
|
|
||||||
|
|
||||||
// Verify if a given action is valid for the url path based on the
|
|
||||||
// existing bucket access policy.
|
|
||||||
func bucketPolicyEvalStatements(action string, resource string, conditions map[string]string, statements []policyStatement) bool {
|
|
||||||
for _, statement := range statements {
|
|
||||||
if bucketPolicyMatchStatement(action, resource, conditions, statement) {
|
|
||||||
if statement.Effect == "Allow" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// Do not uncomment kept here for readability.
|
|
||||||
// else statement.Effect == "Deny"
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// None match so deny.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify if action, resource and conditions match input policy statement.
|
|
||||||
func bucketPolicyMatchStatement(action string, resource string, conditions map[string]string, statement policyStatement) bool {
|
|
||||||
// Verify if action matches.
|
|
||||||
if bucketPolicyActionMatch(action, statement) {
|
|
||||||
// Verify if resource matches.
|
|
||||||
if bucketPolicyResourceMatch(resource, statement) {
|
|
||||||
// Verify if condition matches.
|
|
||||||
if bucketPolicyConditionMatch(conditions, statement) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify if given action matches with policy statement.
|
|
||||||
func bucketPolicyActionMatch(action string, statement policyStatement) bool {
|
|
||||||
for _, policyAction := range statement.Actions {
|
|
||||||
if matched := actionMatch(policyAction, action); matched {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match function matches wild cards in 'pattern' for resource.
|
|
||||||
func resourceMatch(pattern, resource string) bool {
|
|
||||||
return wildcard.MatchExtended(pattern, resource)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match function matches wild cards in 'pattern' for action.
|
|
||||||
func actionMatch(pattern, action string) bool {
|
|
||||||
return wildcard.Match(pattern, action)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify if given resource matches with policy statement.
|
|
||||||
func bucketPolicyResourceMatch(resource string, statement policyStatement) bool {
|
|
||||||
for _, resourcep := range statement.Resources {
|
|
||||||
// the resource rule for object could contain "*" wild card.
|
|
||||||
// the requested object can be given access based on the already set bucket policy if
|
|
||||||
// the match is successful.
|
|
||||||
// More info: http://docs.aws.amazon.com/AmazonS3/latest/dev/s3-arn-format.html .
|
|
||||||
if matched := resourceMatch(resourcep, resource); matched {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify if given condition matches with policy statement.
|
|
||||||
func bucketPolicyConditionMatch(conditions map[string]string, statement policyStatement) bool {
|
|
||||||
// Supports following conditions.
|
|
||||||
// - StringEquals
|
|
||||||
// - StringNotEquals
|
|
||||||
//
|
|
||||||
// Supported applicable condition keys for each conditions.
|
|
||||||
// - s3:prefix
|
|
||||||
// - s3:max-keys
|
|
||||||
var conditionMatches = true
|
|
||||||
for condition, conditionKeys := range statement.Conditions {
|
|
||||||
if condition == "StringEquals" {
|
|
||||||
if conditionKeys["s3:prefix"] != conditions["prefix"] {
|
|
||||||
conditionMatches = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if conditionKeys["s3:max-keys"] != conditions["max-keys"] {
|
|
||||||
conditionMatches = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else if condition == "StringNotEquals" {
|
|
||||||
if conditionKeys["s3:prefix"] == conditions["prefix"] {
|
|
||||||
conditionMatches = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if conditionKeys["s3:max-keys"] == conditions["max-keys"] {
|
|
||||||
conditionMatches = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return conditionMatches
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutBucketPolicyHandler - PUT Bucket policy
|
|
||||||
// -----------------
|
|
||||||
// This implementation of the PUT operation uses the policy
|
|
||||||
// subresource to add to or replace a policy on a bucket
|
|
||||||
func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
switch getRequestAuthType(r) {
|
|
||||||
default:
|
|
||||||
// For all unknown auth types return error.
|
|
||||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
|
||||||
return
|
|
||||||
case authTypePresigned, authTypeSigned:
|
|
||||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If Content-Length is unknown or zero, deny the
|
|
||||||
// request. PutBucketPolicy always needs a Content-Length if
|
|
||||||
// incoming request is not chunked.
|
|
||||||
if !contains(r.TransferEncoding, "chunked") {
|
|
||||||
if r.ContentLength == -1 || r.ContentLength == 0 {
|
|
||||||
writeErrorResponse(w, r, ErrMissingContentLength, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// If Content-Length is greater than maximum allowed policy size.
|
|
||||||
if r.ContentLength > maxAccessPolicySize {
|
|
||||||
writeErrorResponse(w, r, ErrEntityTooLarge, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read access policy up to maxAccessPolicySize.
|
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
|
|
||||||
// bucket policies are limited to 20KB in size, using a limit reader.
|
|
||||||
policyBytes, err := ioutil.ReadAll(io.LimitReader(r.Body, maxAccessPolicySize))
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to read from client.")
|
|
||||||
writeErrorResponse(w, r, toAPIErrorCode(err), r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Parse bucket policy.
|
|
||||||
var policy = &bucketPolicy{}
|
|
||||||
err = parseBucketPolicy(bytes.NewReader(policyBytes), policy)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to parse bucket policy.")
|
|
||||||
writeErrorResponse(w, r, ErrInvalidPolicyDocument, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse check bucket policy.
|
|
||||||
if s3Error := checkBucketPolicyResources(bucket, policy); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save bucket policy.
|
|
||||||
if err = writeBucketPolicy(bucket, api.ObjectAPI, bytes.NewReader(policyBytes), int64(len(policyBytes))); err != nil {
|
|
||||||
errorIf(err, "Unable to write bucket policy.")
|
|
||||||
switch err.(type) {
|
|
||||||
case BucketNameInvalid:
|
|
||||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
|
||||||
default:
|
|
||||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the bucket policy in memory.
|
|
||||||
globalBucketPolicies.SetBucketPolicy(bucket, policy)
|
|
||||||
|
|
||||||
// Success.
|
|
||||||
writeSuccessNoContent(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBucketPolicyHandler - DELETE Bucket policy
|
|
||||||
// -----------------
|
|
||||||
// This implementation of the DELETE operation uses the policy
|
|
||||||
// subresource to add to remove a policy on a bucket.
|
|
||||||
func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
switch getRequestAuthType(r) {
|
|
||||||
default:
|
|
||||||
// For all unknown auth types return error.
|
|
||||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
|
||||||
return
|
|
||||||
case authTypePresigned, authTypeSigned:
|
|
||||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete bucket access policy.
|
|
||||||
if err := removeBucketPolicy(bucket, api.ObjectAPI); err != nil {
|
|
||||||
errorIf(err, "Unable to remove bucket policy.")
|
|
||||||
switch err.(type) {
|
|
||||||
case BucketNameInvalid:
|
|
||||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
|
||||||
case BucketPolicyNotFound:
|
|
||||||
writeErrorResponse(w, r, ErrNoSuchBucketPolicy, r.URL.Path)
|
|
||||||
default:
|
|
||||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove bucket policy.
|
|
||||||
globalBucketPolicies.RemoveBucketPolicy(bucket)
|
|
||||||
|
|
||||||
// Success.
|
|
||||||
writeSuccessNoContent(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBucketPolicyHandler - GET Bucket policy
|
|
||||||
// -----------------
|
|
||||||
// This operation uses the policy
|
|
||||||
// subresource to return the policy of a specified bucket.
|
|
||||||
func (api objectAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
switch getRequestAuthType(r) {
|
|
||||||
default:
|
|
||||||
// For all unknown auth types return error.
|
|
||||||
writeErrorResponse(w, r, ErrAccessDenied, r.URL.Path)
|
|
||||||
return
|
|
||||||
case authTypePresigned, authTypeSigned:
|
|
||||||
if s3Error := isReqAuthenticated(r); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(w, r, s3Error, r.URL.Path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read bucket access policy.
|
|
||||||
policy, err := readBucketPolicy(bucket, api.ObjectAPI)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to read bucket policy.")
|
|
||||||
switch err.(type) {
|
|
||||||
case BucketNameInvalid:
|
|
||||||
writeErrorResponse(w, r, ErrInvalidBucketName, r.URL.Path)
|
|
||||||
case BucketPolicyNotFound:
|
|
||||||
writeErrorResponse(w, r, ErrNoSuchBucketPolicy, r.URL.Path)
|
|
||||||
default:
|
|
||||||
writeErrorResponse(w, r, ErrInternalError, r.URL.Path)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to client.
|
|
||||||
fmt.Fprint(w, policy)
|
|
||||||
}
|
|
||||||
@@ -1,547 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tests validate Bucket policy resource matcher.
|
|
||||||
func TestBucketPolicyResourceMatch(t *testing.T) {
|
|
||||||
|
|
||||||
// generates statement with given resource..
|
|
||||||
generateStatement := func(resource string) policyStatement {
|
|
||||||
statement := policyStatement{}
|
|
||||||
statement.Resources = []string{resource}
|
|
||||||
return statement
|
|
||||||
}
|
|
||||||
|
|
||||||
// generates resource prefix.
|
|
||||||
generateResource := func(bucketName, objectName string) string {
|
|
||||||
return AWSResourcePrefix + bucketName + "/" + objectName
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
resourceToMatch string
|
|
||||||
statement policyStatement
|
|
||||||
expectedResourceMatch bool
|
|
||||||
}{
|
|
||||||
// Test case 1-4.
|
|
||||||
// Policy with resource ending with bucket/* allows access to all objects inside the given bucket.
|
|
||||||
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true},
|
|
||||||
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true},
|
|
||||||
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true},
|
|
||||||
{generateResource("minio-bucket", ""), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/*")), true},
|
|
||||||
// Test case - 5.
|
|
||||||
// Policy with resource ending with bucket/oo* should not allow access to bucket/output.txt.
|
|
||||||
{generateResource("minio-bucket", "output.txt"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/oo*")), false},
|
|
||||||
// Test case - 6.
|
|
||||||
// Policy with resource ending with bucket/oo* should allow access to bucket/ootput.txt.
|
|
||||||
{generateResource("minio-bucket", "ootput.txt"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/oo*")), true},
|
|
||||||
// Test case - 7.
|
|
||||||
// Policy with resource ending with bucket/oo* allows access to all subfolders starting with "oo" inside given bucket.
|
|
||||||
{generateResource("minio-bucket", "oop-bucket/my-file"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/oo*")), true},
|
|
||||||
// Test case - 8.
|
|
||||||
{generateResource("minio-bucket", "Asia/India/1.pjg"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/Asia/Japan/*")), false},
|
|
||||||
// Test case - 9.
|
|
||||||
{generateResource("minio-bucket", "Asia/India/1.pjg"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix, "minio-bucket"+"/Asia/Japan/*")), false},
|
|
||||||
// Test case - 10.
|
|
||||||
// Proves that the name space is flat.
|
|
||||||
{generateResource("minio-bucket", "Africa/Bihar/India/design_info.doc/Bihar"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix,
|
|
||||||
"minio-bucket"+"/*/India/*/Bihar")), true},
|
|
||||||
// Test case - 11.
|
|
||||||
// Proves that the name space is flat.
|
|
||||||
{generateResource("minio-bucket", "Asia/China/India/States/Bihar/output.txt"), generateStatement(fmt.Sprintf("%s%s", AWSResourcePrefix,
|
|
||||||
"minio-bucket"+"/*/India/*/Bihar/*")), true},
|
|
||||||
}
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
actualResourceMatch := bucketPolicyResourceMatch(testCase.resourceToMatch, testCase.statement)
|
|
||||||
if testCase.expectedResourceMatch != actualResourceMatch {
|
|
||||||
t.Errorf("Test %d: Expected Resource match to be `%v`, but instead found it to be `%v`", i+1, testCase.expectedResourceMatch, actualResourceMatch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestBucketPolicyActionMatch - Test validates whether given action on the
|
|
||||||
// bucket/object matches the allowed actions in policyStatement.
|
|
||||||
// This test preserves the allowed actions for all 3 sets of policies, that is read-write,read-only, write-only.
|
|
||||||
// The intention of the test is to catch any changes made to allowed action for on eof the above 3 major policy groups mentioned.
|
|
||||||
func TestBucketPolicyActionMatch(t *testing.T) {
|
|
||||||
bucketName := getRandomBucketName()
|
|
||||||
objectPrefix := "test-object"
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
action string
|
|
||||||
statement policyStatement
|
|
||||||
expectedResult bool
|
|
||||||
}{
|
|
||||||
// s3:GetBucketLocation is the action necessary to be present in the bucket policy to allow
|
|
||||||
// fetching of bucket location on an Anonymous/unsigned request.
|
|
||||||
|
|
||||||
//r ead-write bucket policy is expected to allow GetBucketLocation operation on an anonymous request (Test case - 1).
|
|
||||||
{"s3:GetBucketLocation", getReadWriteBucketStatement(bucketName, objectPrefix), true},
|
|
||||||
// write-only bucket policy is expected to allow GetBucketLocation operation on an anonymous request (Test case - 2).
|
|
||||||
{"s3:GetBucketLocation", getWriteOnlyBucketStatement(bucketName, objectPrefix), true},
|
|
||||||
// read-only bucket policy is expected to allow GetBucketLocation operation on an anonymous request (Test case - 3).
|
|
||||||
{"s3:GetBucketLocation", getReadOnlyBucketStatement(bucketName, objectPrefix), true},
|
|
||||||
|
|
||||||
// Any of the Object level access permissions shouldn't allow for GetBucketLocation operation on an Anonymous/unsigned request (Test cases 4-6).
|
|
||||||
{"s3:GetBucketLocation", getReadWriteObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:GetBucketLocation", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:GetBucketLocation", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
|
|
||||||
// s3:ListBucketMultipartUploads is the action necessary to be present in the bucket policy to allow
|
|
||||||
// Listing of multipart uploads in a given bucket for an Anonymous/unsigned request.
|
|
||||||
|
|
||||||
//read-write bucket policy is expected to allow ListBucketMultipartUploads operation on an anonymous request (Test case 7).
|
|
||||||
{"s3:ListBucketMultipartUploads", getReadWriteBucketStatement(bucketName, objectPrefix), true},
|
|
||||||
// write-only bucket policy is expected to allow ListBucketMultipartUploads operation on an anonymous request (Test case 8).
|
|
||||||
{"s3:ListBucketMultipartUploads", getWriteOnlyBucketStatement(bucketName, objectPrefix), true},
|
|
||||||
// read-only bucket policy is expected to not allow ListBucketMultipartUploads operation on an anonymous request (Test case 9).
|
|
||||||
// the allowed actions in read-only bucket statement are "s3:GetBucketLocation","s3:ListBucket",
|
|
||||||
// this should not allow for ListBucketMultipartUploads operations.
|
|
||||||
{"s3:ListBucketMultipartUploads", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
|
|
||||||
// Any of the object level policy will not allow for s3:ListBucketMultipartUploads (Test cases 10-12).
|
|
||||||
{"s3:ListBucketMultipartUploads", getReadWriteObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:ListBucketMultipartUploads", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:ListBucketMultipartUploads", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
|
|
||||||
// s3:ListBucket is the action necessary to be present in the bucket policy to allow
|
|
||||||
// listing of all objects inside a given bucket on an Anonymous/unsigned request.
|
|
||||||
|
|
||||||
// Cases for testing ListBucket access for different Bucket level access permissions.
|
|
||||||
// read-only bucket policy is expected to allow ListBucket operation on an anonymous request (Test case 13).
|
|
||||||
{"s3:ListBucket", getReadOnlyBucketStatement(bucketName, objectPrefix), true},
|
|
||||||
// read-write bucket policy is expected to allow ListBucket operation on an anonymous request (Test case 14).
|
|
||||||
{"s3:ListBucket", getReadWriteBucketStatement(bucketName, objectPrefix), true},
|
|
||||||
// write-only bucket policy is expected to not allow ListBucket operation on an anonymous request (Test case 15).
|
|
||||||
// the allowed actions in write-only bucket statement are "s3:GetBucketLocation", "s3:ListBucketMultipartUploads",
|
|
||||||
// this should not allow for ListBucket operations.
|
|
||||||
{"s3:ListBucket", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
|
|
||||||
// Cases for testing ListBucket access for different Object level access permissions (Test cases 16-18).
|
|
||||||
// Any of the Object level access permissions shouldn't allow for ListBucket operation on an Anonymous/unsigned request.
|
|
||||||
{"s3:ListBucket", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:ListBucket", getReadWriteObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:ListBucket", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
|
|
||||||
// s3:DeleteObject is the action necessary to be present in the bucket policy to allow
|
|
||||||
// deleting/removal of objects inside a given bucket for an Anonymous/unsigned request.
|
|
||||||
|
|
||||||
// Cases for testing DeleteObject access for different Bucket level access permissions (Test cases 19-21).
|
|
||||||
// Any of the Bucket level access permissions shouldn't allow for DeleteObject operation on an Anonymous/unsigned request.
|
|
||||||
{"s3:DeleteObject", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:DeleteObject", getReadWriteBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:DeleteObject", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
|
|
||||||
// Cases for testing DeleteObject access for different Object level access permissions (Test cases 22).
|
|
||||||
// read-only bucket policy is expected to not allow Delete Object operation on an anonymous request.
|
|
||||||
{"s3:DeleteObject", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
// read-write bucket policy is expected to allow Delete Bucket operation on an anonymous request (Test cases 23).
|
|
||||||
{"s3:DeleteObject", getReadWriteObjectStatement(bucketName, objectPrefix), true},
|
|
||||||
// write-only bucket policy is expected to allow Delete Object operation on an anonymous request (Test cases 24).
|
|
||||||
{"s3:DeleteObject", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
|
|
||||||
|
|
||||||
// s3:AbortMultipartUpload is the action necessary to be present in the bucket policy to allow
|
|
||||||
// cancelling or abortion of an already initiated multipart upload operation for an Anonymous/unsigned request.
|
|
||||||
|
|
||||||
// Cases for testing AbortMultipartUpload access for different Bucket level access permissions (Test cases 25-27).
|
|
||||||
// Any of the Bucket level access permissions shouldn't allow for AbortMultipartUpload operation on an Anonymous/unsigned request.
|
|
||||||
{"s3:AbortMultipartUpload", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:AbortMultipartUpload", getReadWriteBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:AbortMultipartUpload", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
|
|
||||||
// Cases for testing AbortMultipartUpload access for different Object level access permissions.
|
|
||||||
// read-only object policy is expected to not allow AbortMultipartUpload operation on an anonymous request (Test case 28).
|
|
||||||
{"s3:AbortMultipartUpload", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
// read-write object policy is expected to allow AbortMultipartUpload operation on an anonymous request (Test case 29).
|
|
||||||
{"s3:AbortMultipartUpload", getReadWriteObjectStatement(bucketName, objectPrefix), true},
|
|
||||||
// write-only object policy is expected to allow AbortMultipartUpload operation on an anonymous request (Test case 30).
|
|
||||||
{"s3:AbortMultipartUpload", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
|
|
||||||
|
|
||||||
// s3:PutObject is the action necessary to be present in the bucket policy to allow
|
|
||||||
// uploading of an object for an Anonymous/unsigned request.
|
|
||||||
|
|
||||||
// Cases for testing PutObject access for different Bucket level access permissions (Test cases 31-33).
|
|
||||||
// Any of the Bucket level access permissions shouldn't allow for PutObject operation on an Anonymous/unsigned request.
|
|
||||||
{"s3:PutObject", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:PutObject", getReadWriteBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:PutObject", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
|
|
||||||
// Cases for testing PutObject access for different Object level access permissions.
|
|
||||||
// read-only object policy is expected to not allow PutObject operation on an anonymous request (Test case 34).
|
|
||||||
{"s3:PutObject", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
// read-write object policy is expected to allow PutObject operation on an anonymous request (Test case 35).
|
|
||||||
{"s3:PutObject", getReadWriteObjectStatement(bucketName, objectPrefix), true},
|
|
||||||
// write-only object policy is expected to allow PutObject operation on an anonymous request (Test case 36).
|
|
||||||
{"s3:PutObject", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
|
|
||||||
|
|
||||||
// s3:GetObject is the action necessary to be present in the bucket policy to allow
|
|
||||||
// downloading of an object for an Anonymous/unsigned request.
|
|
||||||
|
|
||||||
// Cases for testing GetObject access for different Bucket level access permissions (Test cases 37-39).
|
|
||||||
// Any of the Bucket level access permissions shouldn't allow for GetObject operation on an Anonymous/unsigned request.
|
|
||||||
{"s3:GetObject", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:GetObject", getReadWriteBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:GetObject", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
|
|
||||||
// Cases for testing GetObject access for different Object level access permissions.
|
|
||||||
// read-only bucket policy is expected to allow downloading of an Object on an anonymous request (Test case 40).
|
|
||||||
{"s3:GetObject", getReadOnlyObjectStatement(bucketName, objectPrefix), true},
|
|
||||||
// read-write bucket policy is expected to allow downloading of an Object on an anonymous request (Test case 41).
|
|
||||||
{"s3:GetObject", getReadWriteObjectStatement(bucketName, objectPrefix), true},
|
|
||||||
// write-only bucket policy is expected to not allow downloading of an Object on an anonymous request (Test case 42).
|
|
||||||
{"s3:GetObject", getWriteOnlyObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
|
|
||||||
// s3:ListMultipartUploadParts is the action necessary to be present in the bucket policy to allow
|
|
||||||
// Listing of uploaded parts for an Anonymous/unsigned request.
|
|
||||||
|
|
||||||
// Any of the Bucket level access permissions shouldn't allow for ListMultipartUploadParts operation on an Anonymous/unsigned request.
|
|
||||||
// read-only bucket policy is expected to not allow ListMultipartUploadParts operation on an anonymous request (Test cases 43-45).
|
|
||||||
{"s3:ListMultipartUploadParts", getReadOnlyBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:ListMultipartUploadParts", getReadWriteBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
{"s3:ListMultipartUploadParts", getWriteOnlyBucketStatement(bucketName, objectPrefix), false},
|
|
||||||
|
|
||||||
// read-only object policy is expected to not allow ListMultipartUploadParts operation on an anonymous request (Test case 46).
|
|
||||||
{"s3:ListMultipartUploadParts", getReadOnlyObjectStatement(bucketName, objectPrefix), false},
|
|
||||||
// read-write object policy is expected to allow ListMultipartUploadParts operation on an anonymous request (Test case 47).
|
|
||||||
{"s3:ListMultipartUploadParts", getReadWriteObjectStatement(bucketName, objectPrefix), true},
|
|
||||||
// write-only object policy is expected to allow ListMultipartUploadParts operation on an anonymous request (Test case 48).
|
|
||||||
{"s3:ListMultipartUploadParts", getWriteOnlyObjectStatement(bucketName, objectPrefix), true},
|
|
||||||
}
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
actualResult := bucketPolicyActionMatch(testCase.action, testCase.statement)
|
|
||||||
if testCase.expectedResult != actualResult {
|
|
||||||
t.Errorf("Test %d: Expected the result to be `%v`, but instead found it to be `%v`", i+1, testCase.expectedResult, actualResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrapper for calling Put Bucket Policy HTTP handler tests for both XL multiple disks and single node setup.
|
|
||||||
func TestPutBucketPolicyHandler(t *testing.T) {
|
|
||||||
ExecObjectLayerTest(t, testPutBucketPolicyHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testPutBucketPolicyHandler - Test for Bucket policy end point.
|
|
||||||
// TODO: Add exhaustive cases with various combination of statement fields.
|
|
||||||
func testPutBucketPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
|
||||||
initBucketPolicies(obj)
|
|
||||||
|
|
||||||
// get random bucket name.
|
|
||||||
bucketName := getRandomBucketName()
|
|
||||||
// Create bucket.
|
|
||||||
err := obj.MakeBucket(bucketName)
|
|
||||||
if err != nil {
|
|
||||||
// failed to create newbucket, abort.
|
|
||||||
t.Fatalf("%s : %s", instanceType, err)
|
|
||||||
}
|
|
||||||
// Register the API end points with XL/FS object layer.
|
|
||||||
apiRouter := initTestAPIEndPoints(obj, []string{"PutBucketPolicy"})
|
|
||||||
// initialize the server and obtain the credentials and root.
|
|
||||||
// credentials are necessary to sign the HTTP request.
|
|
||||||
rootPath, err := newTestConfig("us-east-1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root folder after the test ends.
|
|
||||||
defer removeAll(rootPath)
|
|
||||||
|
|
||||||
credentials := serverConfig.GetCredential()
|
|
||||||
|
|
||||||
// template for constructing HTTP request body for PUT bucket policy.
|
|
||||||
bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetBucketLocation","s3:ListBucket"],"Resource":["arn:aws:s3:::%s"]},{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::%s/this*"]}]}`
|
|
||||||
|
|
||||||
// test cases with sample input and expected output.
|
|
||||||
testCases := []struct {
|
|
||||||
bucketName string
|
|
||||||
accessKey string
|
|
||||||
secretKey string
|
|
||||||
// expected Response.
|
|
||||||
expectedRespStatus int
|
|
||||||
}{
|
|
||||||
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, http.StatusNoContent},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterating over the test cases, calling the function under test and asserting the response.
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
// obtain the put bucket policy request body.
|
|
||||||
bucketPolicyStr := fmt.Sprintf(bucketPolicyTemplate, testCase.bucketName, testCase.bucketName)
|
|
||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
|
||||||
req, err := newTestSignedRequest("PUT", getPutPolicyURL("", testCase.bucketName),
|
|
||||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testCase.accessKey, testCase.secretKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
|
||||||
}
|
|
||||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler.
|
|
||||||
// Call the ServeHTTP to execute the handler.
|
|
||||||
apiRouter.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != testCase.expectedRespStatus {
|
|
||||||
t.Errorf("Test %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, rec.Code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrapper for calling Get Bucket Policy HTTP handler tests for both XL multiple disks and single node setup.
|
|
||||||
func TestGetBucketPolicyHandler(t *testing.T) {
|
|
||||||
ExecObjectLayerTest(t, testGetBucketPolicyHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testGetBucketPolicyHandler - Test for end point which fetches the access policy json of the given bucket.
|
|
||||||
// TODO: Add exhaustive cases with various combination of statement fields.
|
|
||||||
func testGetBucketPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
|
||||||
initBucketPolicies(obj)
|
|
||||||
|
|
||||||
// get random bucket name.
|
|
||||||
bucketName := getRandomBucketName()
|
|
||||||
// Create bucket.
|
|
||||||
err := obj.MakeBucket(bucketName)
|
|
||||||
if err != nil {
|
|
||||||
// failed to create newbucket, abort.
|
|
||||||
t.Fatalf("%s : %s", instanceType, err)
|
|
||||||
}
|
|
||||||
// Register the API end points with XL/FS object layer.
|
|
||||||
// Registering only the PutBucketPolicy and GetBucketPolicy handlers.
|
|
||||||
apiRouter := initTestAPIEndPoints(obj, []string{"PutBucketPolicy", "GetBucketPolicy"})
|
|
||||||
// initialize the server and obtain the credentials and root.
|
|
||||||
// credentials are necessary to sign the HTTP request.
|
|
||||||
rootPath, err := newTestConfig("us-east-1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root folder after the test ends.
|
|
||||||
defer removeAll(rootPath)
|
|
||||||
|
|
||||||
credentials := serverConfig.GetCredential()
|
|
||||||
|
|
||||||
// template for constructing HTTP request body for PUT bucket policy.
|
|
||||||
bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetBucketLocation","s3:ListBucket"],"Resource":["arn:aws:s3:::%s"]},{"Sid":"","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetObject"],"Resource":["arn:aws:s3:::%s/this*"]}]}`
|
|
||||||
|
|
||||||
// Writing bucket policy before running test on GetBucketPolicy.
|
|
||||||
putTestPolicies := []struct {
|
|
||||||
bucketName string
|
|
||||||
accessKey string
|
|
||||||
secretKey string
|
|
||||||
// expected Response.
|
|
||||||
expectedRespStatus int
|
|
||||||
}{
|
|
||||||
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, http.StatusNoContent},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterating over the cases and writing the bucket policy.
|
|
||||||
// its required to write the policies first before running tests on GetBucketPolicy.
|
|
||||||
for i, testPolicy := range putTestPolicies {
|
|
||||||
// obtain the put bucket policy request body.
|
|
||||||
bucketPolicyStr := fmt.Sprintf(bucketPolicyTemplate, testPolicy.bucketName, testPolicy.bucketName)
|
|
||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
|
||||||
req, err := newTestSignedRequest("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
|
||||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
|
||||||
}
|
|
||||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic ofthe handler.
|
|
||||||
// Call the ServeHTTP to execute the handler.
|
|
||||||
apiRouter.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != testPolicy.expectedRespStatus {
|
|
||||||
t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testPolicy.expectedRespStatus, rec.Code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// test cases with inputs and expected result for GetBucketPolicyHandler.
|
|
||||||
testCases := []struct {
|
|
||||||
bucketName string
|
|
||||||
accessKey string
|
|
||||||
secretKey string
|
|
||||||
// expected output.
|
|
||||||
expectedBucketPolicy string
|
|
||||||
expectedRespStatus int
|
|
||||||
}{
|
|
||||||
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, bucketPolicyTemplate, http.StatusOK},
|
|
||||||
}
|
|
||||||
// Iterating over the cases, fetching the policy and validating the response.
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
// expected bucket policy json string.
|
|
||||||
expectedBucketPolicyStr := fmt.Sprintf(testCase.expectedBucketPolicy, testCase.bucketName, testCase.bucketName)
|
|
||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
|
||||||
req, err := newTestSignedRequest("GET", getGetPolicyURL("", testCase.bucketName),
|
|
||||||
0, nil, testCase.accessKey, testCase.secretKey)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
|
||||||
}
|
|
||||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
|
||||||
// Call the ServeHTTP to execute the handler, GetBucketPolicyHandler handles the request.
|
|
||||||
apiRouter.ServeHTTP(rec, req)
|
|
||||||
// Assert the response code with the expected status.
|
|
||||||
if rec.Code != testCase.expectedRespStatus {
|
|
||||||
t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, rec.Code)
|
|
||||||
}
|
|
||||||
// read the response body.
|
|
||||||
bucketPolicyReadBuf, err := ioutil.ReadAll(rec.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d: %s: Failed parsing response body: <ERROR> %v", i+1, instanceType, err)
|
|
||||||
}
|
|
||||||
// Verify whether the bucket policy fetched is same as the one inserted.
|
|
||||||
if expectedBucketPolicyStr != string(bucketPolicyReadBuf) {
|
|
||||||
t.Errorf("Test %d: %s: Bucket policy differs from expected value.", i+1, instanceType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrapper for calling Delete Bucket Policy HTTP handler tests for both XL multiple disks and single node setup.
|
|
||||||
func TestDeleteBucketPolicyHandler(t *testing.T) {
|
|
||||||
ExecObjectLayerTest(t, testDeleteBucketPolicyHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testDeleteBucketPolicyHandler - Test for Delete bucket policy end point.
|
|
||||||
// TODO: Add exhaustive cases with various combination of statement fields.
|
|
||||||
func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType string, t TestErrHandler) {
|
|
||||||
initBucketPolicies(obj)
|
|
||||||
|
|
||||||
// get random bucket name.
|
|
||||||
bucketName := getRandomBucketName()
|
|
||||||
// Create bucket.
|
|
||||||
err := obj.MakeBucket(bucketName)
|
|
||||||
if err != nil {
|
|
||||||
// failed to create newbucket, abort.
|
|
||||||
t.Fatalf("%s : %s", instanceType, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the API end points with XL/FS object layer.
|
|
||||||
// Registering PutBucketPolicy and DeleteBucketPolicy handlers.
|
|
||||||
apiRouter := initTestAPIEndPoints(obj, []string{"PutBucketPolicy", "DeleteBucketPolicy"})
|
|
||||||
|
|
||||||
// initialize the server and obtain the credentials and root.
|
|
||||||
// credentials are necessary to sign the HTTP request.
|
|
||||||
rootPath, err := newTestConfig("us-east-1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Init Test config failed")
|
|
||||||
}
|
|
||||||
// remove the root folder after the test ends.
|
|
||||||
defer removeAll(rootPath)
|
|
||||||
|
|
||||||
credentials := serverConfig.GetCredential()
|
|
||||||
|
|
||||||
// template for constructing HTTP request body for PUT bucket policy.
|
|
||||||
bucketPolicyTemplate := `{
|
|
||||||
"Version": "2012-10-17",
|
|
||||||
"Statement": [
|
|
||||||
{
|
|
||||||
"Action": [
|
|
||||||
"s3:GetBucketLocation",
|
|
||||||
"s3:ListBucket"
|
|
||||||
],
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Principal": {
|
|
||||||
"AWS": [
|
|
||||||
"*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Resource": [
|
|
||||||
"arn:aws:s3:::%s"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Action": [
|
|
||||||
"s3:GetObject"
|
|
||||||
],
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Principal": {
|
|
||||||
"AWS": [
|
|
||||||
"*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Resource": [
|
|
||||||
"arn:aws:s3:::%s/this*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`
|
|
||||||
|
|
||||||
// Writing bucket policy before running test on DeleteBucketPolicy.
|
|
||||||
putTestPolicies := []struct {
|
|
||||||
bucketName string
|
|
||||||
accessKey string
|
|
||||||
secretKey string
|
|
||||||
// expected Response.
|
|
||||||
expectedRespStatus int
|
|
||||||
}{
|
|
||||||
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, http.StatusNoContent},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterating over the cases and writing the bucket policy.
|
|
||||||
// its required to write the policies first before running tests on GetBucketPolicy.
|
|
||||||
for i, testPolicy := range putTestPolicies {
|
|
||||||
// obtain the put bucket policy request body.
|
|
||||||
bucketPolicyStr := fmt.Sprintf(bucketPolicyTemplate, testPolicy.bucketName, testPolicy.bucketName)
|
|
||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
|
||||||
req, err := newTestSignedRequest("PUT", getPutPolicyURL("", testPolicy.bucketName),
|
|
||||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)), testPolicy.accessKey, testPolicy.secretKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, err)
|
|
||||||
}
|
|
||||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
|
||||||
// Call the ServeHTTP to execute the handler.
|
|
||||||
apiRouter.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != testPolicy.expectedRespStatus {
|
|
||||||
t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testPolicy.expectedRespStatus, rec.Code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// testcases with input and expected output for DeleteBucketPolicyHandler.
|
|
||||||
testCases := []struct {
|
|
||||||
bucketName string
|
|
||||||
accessKey string
|
|
||||||
secretKey string
|
|
||||||
// expected Response.
|
|
||||||
expectedRespStatus int
|
|
||||||
}{
|
|
||||||
{bucketName, credentials.AccessKeyID, credentials.SecretAccessKey, http.StatusNoContent},
|
|
||||||
}
|
|
||||||
// Iterating over the cases and deleting the bucket policy and then asserting response.
|
|
||||||
for i, testCase := range testCases {
|
|
||||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
// construct HTTP request for Delete bucket policy endpoint.
|
|
||||||
req, err := newTestSignedRequest("DELETE", getDeletePolicyURL("", testCase.bucketName),
|
|
||||||
0, nil, testCase.accessKey, testCase.secretKey)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
|
||||||
}
|
|
||||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
|
||||||
// Call the ServeHTTP to execute the handler, DeleteBucketPolicyHandler handles the request.
|
|
||||||
apiRouter.ServeHTTP(rec, req)
|
|
||||||
// Assert the response code with the expected status.
|
|
||||||
if rec.Code != testCase.expectedRespStatus {
|
|
||||||
t.Fatalf("Case %d: Expected the response status to be `%d`, but instead found `%d`", i+1, testCase.expectedRespStatus, rec.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const policyJSON = "policy.json"
|
|
||||||
|
|
||||||
func getBucketFromPolicyPath(oldPolicyPath string) string {
|
|
||||||
bucketPrefix, _ := filepath.Split(oldPolicyPath)
|
|
||||||
_, bucketName := filepath.Split(strings.TrimSuffix(bucketPrefix, slashSeparator))
|
|
||||||
return bucketName
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanupOldBucketPolicyConfigs() error {
|
|
||||||
// Get old bucket policy config directory.
|
|
||||||
oldBucketsConfigDir, err := getOldBucketsConfigPath()
|
|
||||||
fatalIf(err, "Unable to fetch buckets config path to migrate bucket policy")
|
|
||||||
|
|
||||||
// Recursively remove configDir/buckets/ - old bucket policy config location.
|
|
||||||
// N B This is called only if all bucket policies were successfully migrated.
|
|
||||||
return os.RemoveAll(oldBucketsConfigDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func migrateBucketPolicyConfig(objAPI ObjectLayer) error {
|
|
||||||
// Get old bucket policy config directory.
|
|
||||||
oldBucketsConfigDir, err := getOldBucketsConfigPath()
|
|
||||||
fatalIf(err, "Unable to fetch buckets config path to migrate bucket policy")
|
|
||||||
|
|
||||||
// Check if config directory holding bucket policy exists before
|
|
||||||
// migration.
|
|
||||||
_, err = os.Stat(oldBucketsConfigDir)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// WalkFunc that migrates access-policy.json to
|
|
||||||
// .minio.sys/buckets/bucketName/policy.json on all disks.
|
|
||||||
migrateBucketPolicy := func(policyPath string, fileInfo os.FileInfo, err error) error {
|
|
||||||
// policyFile - e.g /configDir/sample-bucket/access-policy.json
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Skip entries that aren't bucket policy files.
|
|
||||||
if fileInfo.Name() != "access-policy.json" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Get bucketName from old policy file path.
|
|
||||||
bucketName := getBucketFromPolicyPath(policyPath)
|
|
||||||
// Read bucket policy config from old location.
|
|
||||||
policyBytes, err := ioutil.ReadFile(policyPath)
|
|
||||||
fatalIf(err, "Unable to read bucket policy to migrate bucket policy", policyPath)
|
|
||||||
newPolicyPath := retainSlash(bucketConfigPrefix) + retainSlash(bucketName) + policyJSON
|
|
||||||
// Erasure code the policy config to all the disks.
|
|
||||||
_, err = objAPI.PutObject(minioMetaBucket, newPolicyPath, int64(len(policyBytes)), bytes.NewReader(policyBytes), nil)
|
|
||||||
fatalIf(err, "Unable to write bucket policy during migration.", newPolicyPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return filepath.Walk(oldBucketsConfigDir, migrateBucketPolicy)
|
|
||||||
}
|
|
||||||
@@ -1,343 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This file implements AWS Access Policy Language parser in
|
|
||||||
// accordance with http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"path"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// AWSResourcePrefix - bucket policy resource prefix.
|
|
||||||
AWSResourcePrefix = "arn:aws:s3:::"
|
|
||||||
)
|
|
||||||
|
|
||||||
// supportedActionMap - lists all the actions supported by minio.
|
|
||||||
var supportedActionMap = map[string]struct{}{
|
|
||||||
"*": {},
|
|
||||||
"s3:*": {},
|
|
||||||
"s3:GetObject": {},
|
|
||||||
"s3:ListBucket": {},
|
|
||||||
"s3:PutObject": {},
|
|
||||||
"s3:GetBucketLocation": {},
|
|
||||||
"s3:DeleteObject": {},
|
|
||||||
"s3:AbortMultipartUpload": {},
|
|
||||||
"s3:ListBucketMultipartUploads": {},
|
|
||||||
"s3:ListMultipartUploadParts": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// supported Conditions type.
|
|
||||||
var supportedConditionsType = map[string]struct{}{
|
|
||||||
"StringEquals": {},
|
|
||||||
"StringNotEquals": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate s3:prefix, s3:max-keys are present if not
|
|
||||||
// supported keys for the conditions.
|
|
||||||
var supportedConditionsKey = map[string]struct{}{
|
|
||||||
"s3:prefix": {},
|
|
||||||
"s3:max-keys": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// User - canonical users list.
|
|
||||||
type policyUser struct {
|
|
||||||
AWS []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Statement - minio policy statement
|
|
||||||
type policyStatement struct {
|
|
||||||
Sid string
|
|
||||||
Effect string
|
|
||||||
Principal policyUser `json:"Principal"`
|
|
||||||
Actions []string `json:"Action"`
|
|
||||||
Resources []string `json:"Resource"`
|
|
||||||
Conditions map[string]map[string]string `json:"Condition,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// bucketPolicy - collection of various bucket policy statements.
|
|
||||||
type bucketPolicy struct {
|
|
||||||
Version string // date in 0000-00-00 format
|
|
||||||
Statements []policyStatement `json:"Statement"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stringer implementation for the bucket policies.
|
|
||||||
func (b bucketPolicy) String() string {
|
|
||||||
bbytes, err := json.Marshal(&b)
|
|
||||||
if err != nil {
|
|
||||||
errorIf(err, "Unable to unmarshal bucket policy into JSON %#v", b)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(bbytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// supportedEffectMap - supported effects.
|
|
||||||
var supportedEffectMap = map[string]struct{}{
|
|
||||||
"Allow": {},
|
|
||||||
"Deny": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidActions - are actions valid.
|
|
||||||
func isValidActions(actions []string) (err error) {
|
|
||||||
// Statement actions cannot be empty.
|
|
||||||
if len(actions) == 0 {
|
|
||||||
err = errors.New("Action list cannot be empty.")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, action := range actions {
|
|
||||||
if _, ok := supportedActionMap[action]; !ok {
|
|
||||||
err = errors.New("Unsupported action found: ‘" + action + "’, please validate your policy document.")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidEffect - is effect valid.
|
|
||||||
func isValidEffect(effect string) error {
|
|
||||||
// Statement effect cannot be empty.
|
|
||||||
if len(effect) == 0 {
|
|
||||||
err := errors.New("Policy effect cannot be empty.")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, ok := supportedEffectMap[effect]
|
|
||||||
if !ok {
|
|
||||||
err := errors.New("Unsupported Effect found: ‘" + effect + "’, please validate your policy document.")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidResources - are valid resources.
|
|
||||||
func isValidResources(resources []string) (err error) {
|
|
||||||
// Statement resources cannot be empty.
|
|
||||||
if len(resources) == 0 {
|
|
||||||
err = errors.New("Resource list cannot be empty.")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, resource := range resources {
|
|
||||||
if !strings.HasPrefix(resource, AWSResourcePrefix) {
|
|
||||||
err = errors.New("Unsupported resource style found: ‘" + resource + "’, please validate your policy document.")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resourceSuffix := strings.SplitAfter(resource, AWSResourcePrefix)[1]
|
|
||||||
if len(resourceSuffix) == 0 || strings.HasPrefix(resourceSuffix, "/") {
|
|
||||||
err = errors.New("Invalid resource style found: ‘" + resource + "’, please validate your policy document.")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidPrincipals - are valid principals.
|
|
||||||
func isValidPrincipals(principals []string) (err error) {
|
|
||||||
// Statement principal should have a value.
|
|
||||||
if len(principals) == 0 {
|
|
||||||
err = errors.New("Principal cannot be empty.")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, principal := range principals {
|
|
||||||
// Minio does not support or implement IAM, "*" is the only valid value.
|
|
||||||
// Amazon s3 doc on principals: http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Principal
|
|
||||||
if principal != "*" {
|
|
||||||
err = fmt.Errorf("Unsupported principal style found: ‘%s’, please validate your policy document.", principal)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidConditions - are valid conditions.
|
|
||||||
func isValidConditions(conditions map[string]map[string]string) (err error) {
|
|
||||||
// Returns true if string 'a' is found in the list.
|
|
||||||
findString := func(a string, list []string) bool {
|
|
||||||
for _, b := range list {
|
|
||||||
if b == a {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
conditionKeyVal := make(map[string][]string)
|
|
||||||
// Verify conditions should be valid.
|
|
||||||
// Validate if stringEquals, stringNotEquals are present
|
|
||||||
// if not throw an error.
|
|
||||||
for conditionType := range conditions {
|
|
||||||
_, validType := supportedConditionsType[conditionType]
|
|
||||||
if !validType {
|
|
||||||
err = fmt.Errorf("Unsupported condition type '%s', please validate your policy document.", conditionType)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for key := range conditions[conditionType] {
|
|
||||||
_, validKey := supportedConditionsKey[key]
|
|
||||||
if !validKey {
|
|
||||||
err = fmt.Errorf("Unsupported condition key '%s', please validate your policy document.", conditionType)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
conditionArray, ok := conditionKeyVal[key]
|
|
||||||
if ok && findString(conditions[conditionType][key], conditionArray) {
|
|
||||||
err = fmt.Errorf("Ambigious condition values for key '%s', please validate your policy document.", key)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
conditionKeyVal[key] = append(conditionKeyVal[key], conditions[conditionType][key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List of actions for which prefixes are not allowed.
|
|
||||||
var invalidPrefixActions = map[string]struct{}{
|
|
||||||
"s3:GetBucketLocation": {},
|
|
||||||
"s3:ListBucket": {},
|
|
||||||
"s3:ListBucketMultipartUploads": {},
|
|
||||||
// Add actions which do not honor prefixes.
|
|
||||||
}
|
|
||||||
|
|
||||||
// resourcePrefix - provides the prefix removing any wildcards.
|
|
||||||
func resourcePrefix(resource string) string {
|
|
||||||
if strings.HasSuffix(resource, "*") {
|
|
||||||
resource = strings.TrimSuffix(resource, "*")
|
|
||||||
}
|
|
||||||
return path.Clean(resource)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkBucketPolicyResources validates Resources in unmarshalled bucket policy structure.
|
|
||||||
// First valation of Resources done for given set of Actions.
|
|
||||||
// Later its validated for recursive Resources.
|
|
||||||
func checkBucketPolicyResources(bucket string, bucketPolicy *bucketPolicy) APIErrorCode {
|
|
||||||
// Validate statements for special actions and collect resources
|
|
||||||
// for others to validate nesting.
|
|
||||||
var resourceMap = make(map[string]struct{})
|
|
||||||
for _, statement := range bucketPolicy.Statements {
|
|
||||||
for _, action := range statement.Actions {
|
|
||||||
for _, resource := range statement.Resources {
|
|
||||||
resourcePrefix := strings.SplitAfter(resource, AWSResourcePrefix)[1]
|
|
||||||
if _, ok := invalidPrefixActions[action]; ok {
|
|
||||||
// Resource prefix is not equal to bucket for
|
|
||||||
// prefix invalid actions, reject them.
|
|
||||||
if resourcePrefix != bucket {
|
|
||||||
return ErrMalformedPolicy
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For all other actions validate if resourcePrefix begins
|
|
||||||
// with bucket name, if not reject them.
|
|
||||||
if strings.Split(resourcePrefix, "/")[0] != bucket {
|
|
||||||
return ErrMalformedPolicy
|
|
||||||
}
|
|
||||||
// All valid resources collect them separately to verify nesting.
|
|
||||||
resourceMap[resourcePrefix] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var resources []string
|
|
||||||
for resource := range resourceMap {
|
|
||||||
resources = append(resources, resourcePrefix(resource))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort strings as shorter first.
|
|
||||||
sort.Strings(resources)
|
|
||||||
|
|
||||||
for len(resources) > 1 {
|
|
||||||
var resource string
|
|
||||||
resource, resources = resources[0], resources[1:]
|
|
||||||
// Loop through all resources, if one of them matches with
|
|
||||||
// previous shorter one, it means we have detected
|
|
||||||
// nesting. Reject such rules.
|
|
||||||
for _, otherResource := range resources {
|
|
||||||
// Common prefix reject such rules.
|
|
||||||
if strings.HasPrefix(otherResource, resource) {
|
|
||||||
return ErrPolicyNesting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No errors found.
|
|
||||||
return ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseBucketPolicy - parses and validates if bucket policy is of
|
|
||||||
// proper JSON and follows allowed restrictions with policy standards.
|
|
||||||
func parseBucketPolicy(bucketPolicyReader io.Reader, policy *bucketPolicy) (err error) {
|
|
||||||
// Parse bucket policy reader.
|
|
||||||
decoder := json.NewDecoder(bucketPolicyReader)
|
|
||||||
if err = decoder.Decode(&policy); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Policy version cannot be empty.
|
|
||||||
if len(policy.Version) == 0 {
|
|
||||||
err = errors.New("Policy version cannot be empty.")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Policy statements cannot be empty.
|
|
||||||
if len(policy.Statements) == 0 {
|
|
||||||
err = errors.New("Policy statement cannot be empty.")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through all policy statements and validate entries.
|
|
||||||
for _, statement := range policy.Statements {
|
|
||||||
// Statement effect should be valid.
|
|
||||||
if err := isValidEffect(statement.Effect); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Statement principal should be supported format.
|
|
||||||
if err := isValidPrincipals(statement.Principal.AWS); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Statement actions should be valid.
|
|
||||||
if err := isValidActions(statement.Actions); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Statement resources should be valid.
|
|
||||||
if err := isValidResources(statement.Resources); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Statement conditions should be valid.
|
|
||||||
if err := isValidConditions(statement.Conditions); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Separate deny and allow statements, so that we can apply deny
|
|
||||||
// statements in the beginning followed by Allow statements.
|
|
||||||
var denyStatements []policyStatement
|
|
||||||
var allowStatements []policyStatement
|
|
||||||
for _, statement := range policy.Statements {
|
|
||||||
if statement.Effect == "Deny" {
|
|
||||||
denyStatements = append(denyStatements, statement)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// else if statement.Effect == "Allow"
|
|
||||||
allowStatements = append(allowStatements, statement)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deny statements are enforced first once matched.
|
|
||||||
policy.Statements = append(denyStatements, allowStatements...)
|
|
||||||
|
|
||||||
// Return successfully parsed policy structure.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
/*
|
|
||||||
* Minio Cloud Storage, (C) 2015, 2016 Minio, Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"path"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Variable represents bucket policies in memory.
|
|
||||||
var globalBucketPolicies *bucketPolicies
|
|
||||||
|
|
||||||
// Global bucket policies list, policies are enforced on each bucket looking
|
|
||||||
// through the policies here.
|
|
||||||
type bucketPolicies struct {
|
|
||||||
rwMutex *sync.RWMutex
|
|
||||||
|
|
||||||
// Collection of 'bucket' policies.
|
|
||||||
bucketPolicyConfigs map[string]*bucketPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch bucket policy for a given bucket.
|
|
||||||
func (bp bucketPolicies) GetBucketPolicy(bucket string) *bucketPolicy {
|
|
||||||
bp.rwMutex.RLock()
|
|
||||||
defer bp.rwMutex.RUnlock()
|
|
||||||
return bp.bucketPolicyConfigs[bucket]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a new bucket policy for a bucket, this operation will overwrite
|
|
||||||
// any previous bucketpolicies for the bucket.
|
|
||||||
func (bp *bucketPolicies) SetBucketPolicy(bucket string, policy *bucketPolicy) error {
|
|
||||||
bp.rwMutex.Lock()
|
|
||||||
defer bp.rwMutex.Unlock()
|
|
||||||
if policy == nil {
|
|
||||||
return errors.New("invalid argument")
|
|
||||||
}
|
|
||||||
bp.bucketPolicyConfigs[bucket] = policy
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove bucket policy for a bucket, from in-memory map.
|
|
||||||
func (bp *bucketPolicies) RemoveBucketPolicy(bucket string) {
|
|
||||||
bp.rwMutex.Lock()
|
|
||||||
defer bp.rwMutex.Unlock()
|
|
||||||
delete(bp.bucketPolicyConfigs, bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads all bucket policies from persistent layer.
|
|
||||||
func loadAllBucketPolicies(objAPI ObjectLayer) (policies map[string]*bucketPolicy, err error) {
|
|
||||||
// List buckets to proceed loading all notification configuration.
|
|
||||||
buckets, err := objAPI.ListBuckets()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
policies = make(map[string]*bucketPolicy)
|
|
||||||
// Loads bucket policy.
|
|
||||||
for _, bucket := range buckets {
|
|
||||||
var policy *bucketPolicy
|
|
||||||
policy, err = readBucketPolicy(bucket.Name, objAPI)
|
|
||||||
if err != nil {
|
|
||||||
switch err.(type) {
|
|
||||||
case BucketPolicyNotFound:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
policies[bucket.Name] = policy
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success.
|
|
||||||
return policies, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intialize all bucket policies.
|
|
||||||
func initBucketPolicies(objAPI ObjectLayer) error {
|
|
||||||
if objAPI == nil {
|
|
||||||
return errInvalidArgument
|
|
||||||
}
|
|
||||||
// Read all bucket policies.
|
|
||||||
policies, err := loadAllBucketPolicies(objAPI)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
globalBucketPolicies = &bucketPolicies{
|
|
||||||
rwMutex: &sync.RWMutex{},
|
|
||||||
bucketPolicyConfigs: policies,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getOldBucketsConfigPath - get old buckets config path. (Only used for migrating old bucket policies)
|
|
||||||
func getOldBucketsConfigPath() (string, error) {
|
|
||||||
configPath, err := getConfigPath()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return path.Join(configPath, "buckets"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readBucketPolicy - reads bucket policy for an input bucket, returns BucketPolicyNotFound
|
|
||||||
// if bucket policy is not found. This function also parses the bucket policy into an object.
|
|
||||||
func readBucketPolicy(bucket string, objAPI ObjectLayer) (*bucketPolicy, error) {
|
|
||||||
// Verify bucket is valid.
|
|
||||||
if !IsValidBucketName(bucket) {
|
|
||||||
return nil, BucketNameInvalid{Bucket: bucket}
|
|
||||||
}
|
|
||||||
policyPath := pathJoin(bucketConfigPrefix, bucket, policyJSON)
|
|
||||||
objInfo, err := objAPI.GetObjectInfo(minioMetaBucket, policyPath)
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(ObjectNotFound); ok {
|
|
||||||
return nil, BucketPolicyNotFound{Bucket: bucket}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
err = objAPI.GetObject(minioMetaBucket, policyPath, 0, objInfo.Size, &buffer)
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(ObjectNotFound); ok {
|
|
||||||
return nil, BucketPolicyNotFound{Bucket: bucket}
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Parse the saved policy.
|
|
||||||
var policy = &bucketPolicy{}
|
|
||||||
err = parseBucketPolicy(&buffer, policy)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
|
|
||||||
}
|
|
||||||
return policy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeBucketPolicy - removes any previously written bucket policy. Returns BucketPolicyNotFound
|
|
||||||
// if no policies are found.
|
|
||||||
func removeBucketPolicy(bucket string, objAPI ObjectLayer) error {
|
|
||||||
// Verify bucket is valid.
|
|
||||||
if !IsValidBucketName(bucket) {
|
|
||||||
return BucketNameInvalid{Bucket: bucket}
|
|
||||||
}
|
|
||||||
|
|
||||||
policyPath := pathJoin(bucketConfigPrefix, bucket, policyJSON)
|
|
||||||
if err := objAPI.DeleteObject(minioMetaBucket, policyPath); err != nil {
|
|
||||||
if _, ok := err.(ObjectNotFound); ok {
|
|
||||||
return BucketPolicyNotFound{Bucket: bucket}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeBucketPolicy - save all bucket policies.
|
|
||||||
func writeBucketPolicy(bucket string, objAPI ObjectLayer, reader io.Reader, size int64) error {
|
|
||||||
// Verify if bucket path legal
|
|
||||||
if !IsValidBucketName(bucket) {
|
|
||||||
return BucketNameInvalid{Bucket: bucket}
|
|
||||||
}
|
|
||||||
|
|
||||||
policyPath := pathJoin(bucketConfigPrefix, bucket, policyJSON)
|
|
||||||
_, err := objAPI.PutObject(minioMetaBucket, policyPath, size, reader, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||